- Integrated TourButton component into SettingsMenuPage, SkillsPage, SmsWalletPage, StaffPage, StaffSessionDetailPage, StaffTreatmentSessionsPage, SubscriptionPage, TagsSettingsPage, TreatmentCasesPage to enhance user onboarding experience. - Created new tour definitions for appointments, clinics, staff management, financial management, and patient management, ensuring comprehensive guidance for users navigating the admin panel. - Updated documentation to reflect the addition of tours and their implementation details.
10 KiB
User Profile API
Prefix:
/api/v1/user-profile(plus the per-user tour endpoints under/api/v1/my/tours)
User medical profiles store health information that can be shared with doctors.
POST /api/v1/user-profile
Create a medical profile for the authenticated user.
Permission: AUTH
Request Body (application/json)
{
"name": "علی",
"family": "احمدی",
"fathers_name": "محمد",
"national_code": "0012345678",
"gender": "male",
"blood_type": "A+",
"marital_status": "married",
"education": "لیسانس",
"job": "مهندس",
"address": "تهران، خیابان ولیعصر",
"home_phone": "02112345678",
"work_phone": "02198765432",
"insurance_id": 1,
"description": "توضیحات بیشتر",
"sharing_with_user": true,
"birthday": "1370-06-15",
"date_of_birth": 648172800,
"basic_insurance": "بیمه تأمین اجتماعی",
"supplementary_insurance": "بیمه ایران",
"other": {}
}
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | ❌ | First name |
family |
string | ❌ | Last name |
fathers_name |
string | ❌ | Father's name |
national_code |
string | ❌ | National ID (10 digits) |
gender |
string | ❌ | "male" or "female" |
blood_type |
string | ❌ | "A+", "A-", "B+", "B-", "AB+", "AB-", "O+", "O-" |
marital_status |
string | ❌ | "single" or "married" |
education |
string | ❌ | Education level |
job |
string | ❌ | Occupation |
address |
string | ❌ | Home address |
home_phone |
string | ❌ | Home phone number |
work_phone |
string | ❌ | Work phone number |
insurance_id |
integer | ❌ | Insurance ID |
description |
string | ❌ | Additional notes |
sharing_with_user |
boolean | ❌ | Allow doctors to view this profile |
birthday |
string | ❌ | Birthday in Jalali format YYYY-MM-DD |
date_of_birth |
integer | ❌ | Birthday as Unix timestamp |
basic_insurance |
string | ❌ | Basic insurance name |
supplementary_insurance |
string | ❌ | Supplementary insurance name |
other |
object | ❌ | Extra metadata |
Response 201
{
"success": true,
"data": {
"uuid": "profile-uuid-...",
"name": "علی",
"family": "احمدی",
"gender": "male",
"blood_type": "A+",
"national_code": "0012345678",
"sharing_with_user": true,
"created_at": 1717000000
}
}
Errors
| Code | HTTP | Description |
|---|---|---|
ERR_AUTH_001 |
401 | Missing token |
ERR_VALIDATION_001 |
422 | national_code نامعتبر (رقم کنترلی/طول غلط) (field: national_code) |
ERR_PROFILE_001 |
409 | کد ملی قبلاً برای پروفایل کاربر دیگری ثبت شده (field: national_code) |
ERR_CONFLICT_001 |
409 | Profile already exists for this user |
GET /api/v1/user-profile/{uuid}
Get a user profile.
Permission: AUTH — must be the profile owner, or a doctor if sharing_with_user: true, or ROLE_ADMIN
Path Parameters
| Param | Type | Description |
|---|---|---|
uuid |
string (UUID) | Profile UUID or the owning User UUID — both are accepted |
Auto-resolve & lazy-create:
{uuid}is first looked up as a profile uuid, then as a user uuid. If it is the current user's own uuid and they have no profile yet, an empty profile is created and returned (HTTP 200) — so a freshly registered user always gets an editable profile instead of a 404. An admin reading another user's missing profile gets404(no profile is created as a side effect of reading). The response includes both the profileuuidanduser_uuid; keep the profileuuidfor subsequent PATCHes.
Response 200
Full profile object including all fields (all null for a newly created empty profile). Response is double-nested: extract with data.data.
Errors
| Code | HTTP | Description |
|---|---|---|
ERR_AUTH_001 |
401 | Missing token |
ERR_FORBIDDEN_001 |
403 | Not authorized to view this profile |
ERR_VALIDATION_002 |
404 | No profile or user matches the uuid |
PATCH /api/v1/user-profile/{uuid}
Update a user profile.
Permission: AUTH — must be the profile owner
Path Parameters
| Param | Type | Description |
|---|---|---|
uuid |
string (UUID) | Profile UUID or the owning User UUID — both accepted; the profile is created if the current user has none yet |
Request Body
Same fields as POST — all optional.
national_codeserver-side validation: اگرnational_codeارسال شود و غیرخالی باشد، با الگوریتم رقم کنترلیِ کد ملی ایران اعتبارسنجی میشود (ارقام فارسی/عربی به انگلیسی نرمال و بهصورت لاتین ذخیره میشوند). مقدارِ نامعتبر با422رد میشود. ارسالnullیا رشتهی خالی مجاز است (کد ملی اختیاری) و فیلد را پاک میکند.یکتایی کد ملی: کد ملی در کل سیستم یکتاست (یک کد ملی = یک بیمار). اگر کد ملی ارسالی قبلاً به پروفایل کاربر دیگری تعلق داشته باشد، با
409و کدERR_PROFILE_001رد میشود (field: national_code). پیام خطا شامل شماره موبایلِ ماسکشدهی صاحب کد است (مثلاً «این کد ملی قبلاً با شماره 0912****56 ثبت شده است»). در سطح دیتابیس هم باUNIQUE INDEX uniq_profiles_national_codeتضمین شده (مقادیرNULLآزادند). همین قید روی مسیر ساخت بیمار توسط منشی (POST /api/v1/patient) نیز اعمال میشود.
Response 200
Updated profile object.
Errors
| Code | HTTP | Description |
|---|---|---|
ERR_AUTH_001 |
401 | Missing token |
ERR_VALIDATION_001 |
422 | national_code نامعتبر (رقم کنترلی/طول غلط) (field: national_code) |
ERR_PROFILE_001 |
409 | کد ملی قبلاً برای پروفایل کاربر دیگری ثبت شده (field: national_code) |
ERR_FORBIDDEN_001 |
403 | Not the profile owner |
ERR_NOT_FOUND_001 |
404 | Profile not found |
DELETE /api/v1/user-profile/{uuid}
Delete a user profile.
Permission: ROLE_ADMIN
Response 200
{ "success": true, "data": { "message": "پروفایل حذف شد" } }
Errors
| Code | HTTP | Description |
|---|---|---|
ERR_AUTH_001 |
401 | Missing token |
ERR_AUTH_006 |
403 | Not admin |
ERR_NOT_FOUND_001 |
404 | Profile not found |
POST /api/v1/user-profile/avatar
Upload the current user's profile avatar. Creates the profile if it doesn't exist yet.
Permission: IS_AUTHENTICATED_FULLY
Request
Raw file body with Content-Disposition: filename="..." and Content-Type: application/octet-stream.
Response 200
{ "success": true, "data": { "avatar": "/uploads/avatars/2026-06/xxx.jpg", "url": "/uploads/avatars/2026-06/xxx.jpg" } }
The stored path is also returned as avatar in the profile GET/PATCH responses.
Errors
| Code | HTTP | Description |
|---|---|---|
ERR_VALIDATION_001 |
422 | Missing or invalid file |
ERR_AUTH_001 |
401 | Missing token |
Guided tours (admin panel)
The admin SPA ships a step-by-step tour per page. A tour runs automatically the first time a
user opens that page, and is available afterwards from the ? button in the page header.
Which tours a user has already been through is stored server-side, so it follows the account
across browsers and devices.
Each tour carries a version in the frontend registry (assets/admin/lib/tour/tours/*.ts).
The tour auto-runs again when its version is higher than the stored one, which is how a rewritten
tour reaches users who already saw the old text.
Every non-admin page of the panel ships a tour. Adding one to a new page means: a file under
assets/admin/lib/tour/tours/, one line in registry.ts, and tourId on the page's PageHeader
— the shared data-tour anchors (page-title, page-action, page-search, page-table,
page-pagination) already come from PageHeader, DataTable and Pagination. A step whose
anchor is not on screen is dropped silently, so role-specific pages need no branching.
PageHeader auto-runs the tour once the page is ready; pass tourReady={!isLoading} when the
data arrives late.
GET /api/v1/my/tours
Tours the current user has already been through.
Permission: IS_AUTHENTICATED_FULLY
Response 200
{ "success": true, "data": { "seen": { "appointments": 1 } } }
seen is always an object; it is {} for a user who has not finished any tour yet.
| Field | Type | Description |
|---|---|---|
seen |
object | tour id → highest version the user has seen |
Errors
| Code | HTTP | Description |
|---|---|---|
ERR_AUTH_001 |
401 | Missing token |
POST /api/v1/my/tours/{tourId}/seen
Record that the current user has been through a tour. Closing a tour part-way counts as seen — the client sends this on tour close as well as on completion.
Permission: IS_AUTHENTICATED_FULLY
| Param | Type | Description |
|---|---|---|
tourId |
string | Route-constrained to [a-z0-9-]{1,64}; anything else is a 404 |
Request Body (application/json)
{ "version": 1 }
| Field | Type | Required | Description |
|---|---|---|---|
version |
integer ≥ 1 | ✅ | Version of the tour definition that was shown |
Response 200
{ "success": true, "data": { "tourId": "appointments", "version": 1 } }
Repeat calls update the same row. The stored version only ever moves forward, so replaying an
old tour from the ? button cannot re-trigger a newer one.
Errors
{ "success": false, "data": null, "errors": [ { "code": "ERR_VALIDATION_001", "message": "نسخهٔ راهنما باید عددی بزرگتر از صفر باشد", "field": "version" } ] }
| Code | HTTP | Description |
|---|---|---|
ERR_VALIDATION_001 |
422 | version missing, not an integer, or below 1 |
ERR_AUTH_001 |
401 | Missing token |
| — | 404 | tourId does not match the route constraint |