feat(api): add dashboard endpoints for clinic, doctor, and secretary roles
- Implemented GET /api/v1/dashboard/clinic to return clinic stats and today's schedule for clinic owners. - Implemented GET /api/v1/dashboard/doctor to return doctor's stats and today's schedule for doctors. - Implemented GET /api/v1/dashboard/secretary to return stats and conditional appointments for secretaries. feat(migrations): create user_active_context and mobile_verification_otp tables - Added migration to create user_active_context table for tracking active user sessions. - Added migration to create mobile_verification_otp table for handling mobile number verification. feat(migrations): create site_config table for application settings - Added migration to create site_config table to store various site configuration settings. feat(appointments): create MyAppointmentsController for user-specific appointments - Added MyAppointmentsController to handle fetching user-specific appointments with pagination and filtering. feat(auth): implement NotificationMobileController for mobile number verification - Added NotificationMobileController to handle OTP requests and verification for mobile number changes. feat(auth): create MobileVerificationOtp entity for OTP management - Created MobileVerificationOtp entity to manage OTP records for mobile verification. feat(auth): create UserActiveContext entity for user session management - Created UserActiveContext entity to manage user active sessions. feat(config): implement SiteConfigController for managing site settings - Added SiteConfigController to handle fetching and updating site configuration settings. feat(config): create SiteConfig entity and repository for configuration management - Created SiteConfig entity and repository to manage site configuration data.
This commit is contained in:
@@ -677,3 +677,48 @@ List all SMS templates.
|
||||
| `POST /api/v1/admin/clinic/invitation/{invUuid}/resend` | Resend SMS |
|
||||
| `PATCH /api/v1/admin/clinic/invitation/{invUuid}/status` | Change status |
|
||||
| `DELETE /api/v1/admin/clinic/invitation/{invUuid}` | Delete |
|
||||
|
||||
---
|
||||
|
||||
## Settings
|
||||
|
||||
### GET /api/v1/admin/settings
|
||||
|
||||
Returns all site configuration values.
|
||||
|
||||
**Response `200`**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"commission_enabled": "0",
|
||||
"commission_percent": "0",
|
||||
"site_name": "ClinicPro",
|
||||
"support_phone": "",
|
||||
"max_cancel_hours_before": "24",
|
||||
"appointment_reminder_hours": "2"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
All values are strings. Missing keys return their default values.
|
||||
|
||||
### PATCH /api/v1/admin/settings
|
||||
|
||||
Update one or more settings. Unknown keys are silently ignored.
|
||||
|
||||
**Request body** (partial update — send only keys to change):
|
||||
```json
|
||||
{
|
||||
"commission_enabled": "1",
|
||||
"commission_percent": "5",
|
||||
"site_name": "کلینیکپرو"
|
||||
}
|
||||
```
|
||||
|
||||
**Response `200`** — same shape as GET, returns all settings after save.
|
||||
|
||||
**Commission rules:**
|
||||
- `commission_enabled` — `"1"` = active, `"0"` = inactive
|
||||
- `commission_percent` — integer string, `0`–`100`
|
||||
- Commission applies only to regular users (`booked_by = user`); secretaries are exempt
|
||||
|
||||
@@ -275,3 +275,54 @@ Updated appointment object.
|
||||
| `ERR_NOT_FOUND_001` | 404 | Appointment not found |
|
||||
| `ERR_CONFLICT_001` | 409 | Version mismatch (optimistic lock) |
|
||||
| `ERR_VALIDATION_001` | 422 | Invalid status value |
|
||||
|
||||
---
|
||||
|
||||
## GET /api/v1/my/appointments
|
||||
|
||||
Role-aware paginated list of appointments. Returns only what the authenticated user is authorized to see.
|
||||
|
||||
**Auth:** `IS_AUTHENTICATED_FULLY` (any role)
|
||||
|
||||
**Role behavior:**
|
||||
| Role | Scope |
|
||||
|------|-------|
|
||||
| `ROLE_ADMIN` | All appointments |
|
||||
| `ROLE_CLINIC` | Appointments for doctors in this clinic |
|
||||
| `ROLE_DOCTOR` | Appointments for this doctor |
|
||||
| `ROLE_SECRETARY` | Appointments for the linked doctor (empty if `appointments.view` permission is false) |
|
||||
|
||||
### Query Parameters
|
||||
| Param | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| `page` | int | 1 | Page number |
|
||||
| `limit` | int | 15 | Items per page (max 100) |
|
||||
| `search` | string | — | Search by mobile, real name, or doctor name |
|
||||
| `status` | string | — | Filter by appointment status |
|
||||
| `date` | string | — | Filter by date in `Y-m-d` format |
|
||||
|
||||
### Response `200`
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"uuid": "string",
|
||||
"patient_name": "string",
|
||||
"patient_mobile": "string",
|
||||
"doctor_name": "string",
|
||||
"clinic_name": "string | null",
|
||||
"appointment_date": "2026-07-25",
|
||||
"appointment_time": "14:30",
|
||||
"slot_start": 1700000000,
|
||||
"status": "reserved",
|
||||
"amount": 0,
|
||||
"created_at": "ISO 8601 string"
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"totalRecords": 8000,
|
||||
"totalPages": 533,
|
||||
"currentPage": 1
|
||||
}
|
||||
}
|
||||
|
||||
+231
-6
@@ -225,7 +225,7 @@ Refresh expired JWT using refresh token.
|
||||
|
||||
## GET `/oauth/userinfo`
|
||||
|
||||
Get authenticated user info.
|
||||
Get authenticated user info — extended with multi-context support.
|
||||
|
||||
**Permission:** `AUTH` — requires valid JWT
|
||||
|
||||
@@ -239,12 +239,96 @@ Authorization: Bearer <token>
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": 4766,
|
||||
"uuid": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"mobile_number": "09123456789",
|
||||
"real_name": "علی احمدی",
|
||||
"roles": ["ROLE_USER"],
|
||||
"status": "active",
|
||||
"created_at": 1717000000
|
||||
"realName": "دکتر وحید درویشی",
|
||||
"status": 1,
|
||||
"roles": ["ROLE_USER", "ROLE_DOCTOR"],
|
||||
"primary_role": "doctor",
|
||||
"db_uuid": "a6ef5d29-38b8-4e69-b1ef-27a304696966",
|
||||
"db_key": "hmac-sha256-hash...",
|
||||
"context": {
|
||||
"type": "doctor",
|
||||
"db_uuid": "a6ef5d29-38b8-4e69-b1ef-27a304696966",
|
||||
"name": "مطب شخصی دکتر وحید درویشی",
|
||||
"role": "doctor"
|
||||
},
|
||||
"available_contexts": [
|
||||
{
|
||||
"type": "doctor",
|
||||
"db_uuid": "a6ef5d29-38b8-4e69-b1ef-27a304696966",
|
||||
"name": "مطب شخصی دکتر وحید درویشی",
|
||||
"role": "doctor"
|
||||
},
|
||||
{
|
||||
"type": "clinic",
|
||||
"db_uuid": "clinic-uuid-...",
|
||||
"name": "کلینیک سلامت",
|
||||
"role": "doctor"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| فیلد | نوع | توضیح |
|
||||
|------|-----|-------|
|
||||
| `primary_role` | string | نقش اصلی: `admin` \| `clinic` \| `doctor` \| `secretary` \| `user` |
|
||||
| `db_uuid` | string\|null | UUID موجودیت فعال (null = هنوز context انتخاب نشده) |
|
||||
| `db_key` | string\|null | `HMAC-SHA256(db_uuid, APP_SECRET)` برای اعتبارسنجی |
|
||||
| `context` | object\|null | context فعال انتخابشده |
|
||||
| `available_contexts` | array | همه محیطهای کاری قابل انتخاب |
|
||||
|
||||
**قانون `primary_role`** (اولویتبندی):
|
||||
- `ROLE_ADMIN` → `"admin"`
|
||||
- `ROLE_CLINIC` → `"clinic"`
|
||||
- `ROLE_DOCTOR` → `"doctor"`
|
||||
- `ROLE_SECRETARY` → `"secretary"`
|
||||
- بقیه → `"user"`
|
||||
|
||||
**قانون `db_uuid`**:
|
||||
- اگر یک context وجود دارد: خودکار فعال میشود
|
||||
- اگر چند context وجود دارد و کاربر هنوز انتخاب نکرده: `null` — frontend باید صفحه انتخاب نشان دهد
|
||||
- پس از `POST /api/v1/auth/switch-context`: برابر context انتخابشده
|
||||
|
||||
### Errors
|
||||
| Code | HTTP | Description |
|
||||
|------|------|-------------|
|
||||
| `ERR_AUTH_001` | 401 | Invalid or missing token |
|
||||
|
||||
---
|
||||
|
||||
## POST `/api/v1/auth/switch-context`
|
||||
|
||||
تغییر محیط کاری فعال — باید از لیست `available_contexts` انتخاب شود.
|
||||
|
||||
**Permission:** `AUTH`
|
||||
|
||||
### Request Body
|
||||
```json
|
||||
{
|
||||
"db_uuid": "clinic-uuid-..."
|
||||
}
|
||||
```
|
||||
|
||||
| فیلد | نوع | Required | توضیح |
|
||||
|------|-----|----------|-------|
|
||||
| `db_uuid` | string (UUID) | ✅ | UUID محیط کاری از لیست `available_contexts` |
|
||||
|
||||
### Response `200`
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"db_uuid": "clinic-uuid-...",
|
||||
"db_key": "new-hmac-hash...",
|
||||
"context": {
|
||||
"type": "clinic",
|
||||
"db_uuid": "clinic-uuid-...",
|
||||
"name": "کلینیک سلامت",
|
||||
"role": "doctor"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -252,7 +336,148 @@ Authorization: Bearer <token>
|
||||
### Errors
|
||||
| Code | HTTP | Description |
|
||||
|------|------|-------------|
|
||||
| `ERR_AUTH_001` | 401 | Invalid or missing token |
|
||||
| `ERR_AUTH_001` | 401 | Missing or invalid token |
|
||||
| `ERR_AUTH_006` | 403 | `db_uuid` در لیست context های این کاربر نیست |
|
||||
| `ERR_VALIDATION_001` | 422 | `db_uuid` ارسال نشده |
|
||||
|
||||
---
|
||||
|
||||
## Notification Mobile (OTP)
|
||||
|
||||
Endpoints for setting/verifying a separate SMS notification number for doctors and clinics. This number receives appointment SMS notifications instead of the account login mobile.
|
||||
|
||||
> **Permission:** All 4 endpoints require `IS_AUTHENTICATED_FULLY` (JWT).
|
||||
|
||||
---
|
||||
|
||||
## POST `/api/v1/notification-mobile/request-otp`
|
||||
|
||||
Request an OTP code to verify a new notification mobile number.
|
||||
|
||||
**Permission:** `AUTH`
|
||||
|
||||
### Request Body
|
||||
```json
|
||||
{
|
||||
"target": "doctor",
|
||||
"new_mobile": "09123456789"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Required | Validation |
|
||||
|-------|------|----------|------------|
|
||||
| `target` | string | ✅ | `doctor` or `clinic` |
|
||||
| `new_mobile` | string | ✅ | Format: `09XXXXXXXXX` |
|
||||
|
||||
### Response `200`
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"message": "کد تأیید ارسال شد",
|
||||
"expires_in": 300
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Errors
|
||||
| Code | HTTP | Description |
|
||||
|------|------|-------------|
|
||||
| `ERR_VALIDATION_001` | 422 | Invalid `target` or mobile format |
|
||||
| `ERR_NOT_FOUND_001` | 404 | No doctor/clinic profile found for this user |
|
||||
|
||||
### Notes
|
||||
- Previous unused OTPs for the entity are deleted before creating a new one
|
||||
- OTP is 6-digit, expires in 5 minutes
|
||||
- Sends via SMS asynchronously
|
||||
|
||||
---
|
||||
|
||||
## POST `/api/v1/notification-mobile/verify`
|
||||
|
||||
Verify the OTP and save the notification mobile number.
|
||||
|
||||
**Permission:** `AUTH`
|
||||
|
||||
### Request Body
|
||||
```json
|
||||
{
|
||||
"target": "doctor",
|
||||
"otp_code": "123456"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Required |
|
||||
|-------|------|----------|
|
||||
| `target` | string | ✅ `doctor` or `clinic` |
|
||||
| `otp_code` | string | ✅ 6-digit code |
|
||||
|
||||
### Response `200`
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"notification_mobile": "09123456789",
|
||||
"message": "شماره اعلان با موفقیت ذخیره شد"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Errors
|
||||
| Code | HTTP | Description |
|
||||
|------|------|-------------|
|
||||
| `ERR_VALIDATION_001` | 422 | Invalid target / OTP expired / wrong code |
|
||||
| `ERR_NOT_FOUND_001` | 404 | No OTP request found or profile not found |
|
||||
|
||||
---
|
||||
|
||||
## GET `/api/v1/notification-mobile/{target}`
|
||||
|
||||
Get the current notification mobile for the authenticated user.
|
||||
|
||||
**Permission:** `AUTH`
|
||||
**Path param:** `target` — `doctor` or `clinic`
|
||||
|
||||
### Response `200`
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"notification_mobile": "09123456789"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- Returns `null` for `notification_mobile` if not set
|
||||
|
||||
### Errors
|
||||
| Code | HTTP | Description |
|
||||
|------|------|-------------|
|
||||
| `ERR_NOT_FOUND_001` | 404 | No profile found for this user |
|
||||
|
||||
---
|
||||
|
||||
## DELETE `/api/v1/notification-mobile/{target}`
|
||||
|
||||
Remove the notification mobile number.
|
||||
|
||||
**Permission:** `AUTH`
|
||||
**Path param:** `target` — `doctor` or `clinic`
|
||||
|
||||
### Response `200`
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"message": "شماره اعلان حذف شد"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Errors
|
||||
| Code | HTTP | Description |
|
||||
|------|------|-------------|
|
||||
| `ERR_NOT_FOUND_001` | 404 | No profile found for this user |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
# Dashboard API
|
||||
|
||||
Role-specific dashboard endpoints. Each endpoint requires the corresponding role JWT.
|
||||
|
||||
---
|
||||
|
||||
## GET /api/v1/dashboard/clinic
|
||||
|
||||
Returns stats and today's schedule for the authenticated clinic owner.
|
||||
|
||||
**Auth:** `ROLE_CLINIC` required
|
||||
|
||||
### Response `200`
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"clinic": {
|
||||
"uuid": "string",
|
||||
"name": "string",
|
||||
"is_active": true,
|
||||
"logo": "string | null"
|
||||
},
|
||||
"stats": {
|
||||
"total_doctors": 5,
|
||||
"today_appointments": 12,
|
||||
"this_month_appointments": 87,
|
||||
"pending_invitations": 2
|
||||
},
|
||||
"today_appointments": [
|
||||
{
|
||||
"uuid": "string",
|
||||
"patient_name": "string | null",
|
||||
"doctor_name": "string",
|
||||
"slot_start": 1700000000,
|
||||
"status": "reserved"
|
||||
}
|
||||
],
|
||||
"doctors": [
|
||||
{
|
||||
"uuid": "string",
|
||||
"name": "string",
|
||||
"today_count": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`today_appointments` — up to 5 records, ordered by `slot_start ASC`.
|
||||
`doctors` — all doctors belonging to this clinic; each includes their appointment count for today.
|
||||
|
||||
### Errors
|
||||
|
||||
| Code | HTTP | Description |
|
||||
|------|------|-------------|
|
||||
| `ERR_NOT_FOUND_001` | 404 | Clinic not found for this user |
|
||||
|
||||
---
|
||||
|
||||
## GET /api/v1/dashboard/doctor
|
||||
|
||||
Returns stats and today's schedule for the authenticated doctor.
|
||||
|
||||
**Auth:** `ROLE_DOCTOR` required
|
||||
|
||||
### Response `200`
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"doctor": {
|
||||
"uuid": "string",
|
||||
"name": "string",
|
||||
"degree": "string | null"
|
||||
},
|
||||
"stats": {
|
||||
"today_appointments": 8,
|
||||
"tomorrow_appointments": 5,
|
||||
"this_month_appointments": 62,
|
||||
"avg_rating": 4.6,
|
||||
"total_ratings": 34
|
||||
},
|
||||
"today_appointments": [
|
||||
{
|
||||
"uuid": "string",
|
||||
"patient_name": "string | null",
|
||||
"patient_mobile": "string",
|
||||
"slot_start": 1700000000,
|
||||
"status": "reserved"
|
||||
}
|
||||
],
|
||||
"clinics": [
|
||||
{
|
||||
"uuid": "string",
|
||||
"name": "string",
|
||||
"logo": "string | null"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`today_appointments` — up to 10 records, ordered by `slot_start ASC`.
|
||||
`avg_rating` — rounded to 1 decimal; `null` if no ratings yet.
|
||||
`clinics` — all clinics the doctor belongs to.
|
||||
|
||||
### Errors
|
||||
|
||||
| Code | HTTP | Description |
|
||||
|------|------|-------------|
|
||||
| `ERR_NOT_FOUND_001` | 404 | Doctor profile not found for this user |
|
||||
|
||||
---
|
||||
|
||||
## GET /api/v1/dashboard/secretary
|
||||
|
||||
Returns stats for the authenticated secretary and (conditionally) today's appointments.
|
||||
|
||||
**Auth:** `ROLE_SECRETARY` required
|
||||
|
||||
### Response `200`
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"doctor": {
|
||||
"uuid": "string",
|
||||
"name": "string",
|
||||
"degree": "string | null"
|
||||
},
|
||||
"permissions": {
|
||||
"resources": {
|
||||
"appointments": {
|
||||
"view": true,
|
||||
"edit": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"stats": {
|
||||
"today_appointments": 8,
|
||||
"tomorrow_appointments": 5
|
||||
},
|
||||
"today_appointments": [
|
||||
{
|
||||
"uuid": "string",
|
||||
"patient_name": "string | null",
|
||||
"patient_mobile": "string",
|
||||
"slot_start": 1700000000,
|
||||
"status": "reserved"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`today_appointments` — only populated when `permissions.resources.appointments.view === true`; otherwise empty array.
|
||||
`today_appointments` — up to 10 records when visible.
|
||||
|
||||
### Errors
|
||||
|
||||
| Code | HTTP | Description |
|
||||
|------|------|-------------|
|
||||
| `ERR_FORBIDDEN_001` | 403 | Secretary relation not configured or inactive |
|
||||
Reference in New Issue
Block a user