feat(appointment): log cancellation events and show them in a Timeline

Introduce the first per-appointment event system. On cancel (via the status
or general update endpoints) an AppointmentEvent (type=cancelled, «نوبت لغو
شد») is recorded with the actor, cancel time, and an optional cancel_reason,
plus a warning-level app_log entry. New GET /appointment/{uuid}/events
returns the ordered event list. The admin appointment detail page renders a
Timeline section and the cancel dialog now collects an optional reason.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
hamed
2026-07-17 11:13:23 +03:30
co-authored by Claude Fable 5
parent 36d7fe0303
commit 49b2ca60d7
8 changed files with 311 additions and 8 deletions
+43 -3
View File
@@ -417,8 +417,9 @@ Change appointment status.
### Request Body
```json
{
"status": "cancelled",
"version": 3
"status": "cancelled_by_doctor",
"version": 3,
"cancel_reason": "بیمار درخواست لغو داد"
}
```
@@ -426,6 +427,7 @@ Change appointment status.
|-------|------|----------|-------------|
| `status` | string | ✅ | New status value |
| `version` | integer | ❌ | Optimistic lock version (prevents double-submit) |
| `cancel_reason` | string | ❌ | Only when transitioning to `cancelled_by_doctor` / `cancelled_by_user`. Stored on the recorded cancellation event (Timeline). Ignored for other statuses. |
**Allowed Transitions by Role:**
| Actor | Allowed transitions |
@@ -434,6 +436,8 @@ Change appointment status.
| Doctor / Secretary | `pending → confirmed`, `confirmed → completed`, `confirmed → no_show` |
| Admin | Any transition |
> **Cancellation is logged.** When the status becomes `cancelled_by_doctor` or `cancelled_by_user`, an `AppointmentEvent` (type `cancelled`, title «نوبت لغو شد») is recorded with the actor (user id + name), the optional `cancel_reason`, and the cancel time — surfaced via `GET /api/v1/appointment/{uuid}/events`. A `warning`-level entry is also written to `app_log`.
### Response `200`
Updated appointment object.
@@ -448,6 +452,42 @@ Updated appointment object.
---
## GET `/api/v1/appointment/{uuid}/events`
Appointment Timeline — chronological event history for one appointment. Currently records cancellation events; the structure is generic for future event types.
**Permission:** `IS_AUTHENTICATED_FULLY` — caller must be able to manage the appointment (`canManage`).
### Path Parameters
| Param | Type | Description |
|-------|------|-------------|
| `uuid` | string (UUID) | Appointment UUID |
### Response `200`
```json
{
"success": true,
"data": [
{
"type": "cancelled",
"title": "نوبت لغو شد",
"actor_name": "دکتر حامد حسینی",
"reason": "بیمار درخواست لغو داد",
"created_at": 1784273931
}
]
}
```
Events are ordered oldest → newest. `data` is a flat array (single nesting). `actor_name` and `reason` may be `null`. `created_at` is a Unix timestamp.
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_006` | 403 | Not allowed to manage this appointment |
| `ERR_VALIDATION_002` | 404 | Appointment not found |
---
## POST `/api/v1/my/appointment`
Create a new appointment for a patient. Used by doctor/clinic/secretary to book appointments on behalf of patients. If no user exists with the given mobile, a new user account is created automatically.
@@ -619,7 +659,7 @@ General update (ویرایش / جا به جایی / انتقال به رزرو /
- `slot_start`/`slot_end` must be sent together; moving to an occupied slot → `409`.
- Relation uuids: empty string clears; unknown uuid → `422`.
- `status` follows the same transition rules as `PATCH /appointment/{uuid}/status`.
- `status` follows the same transition rules as `PATCH /appointment/{uuid}/status`. A transition to `cancelled_by_doctor`/`cancelled_by_user` records a cancellation event (Timeline) + `app_log` warning; an optional `cancel_reason` body field is stored on the event.
- Optimistic lock via `version``409` on concurrent edit.
Response `200`: `{ success, data: { data: <appointment.toArray()> } }`