- Added seed_realistic_data.php to clean existing data and populate the database with realistic entries for doctors, clinics, and secretaries. - Created a structured approach to generate 100 doctors per city with diverse specialties and services. - Implemented database cleanup routines to ensure a fresh start for data seeding. - Enhanced the DoctorSecretaryRepository with improved comments for clarity.
27 KiB
پیادهسازی فرانتاند فاز ۲ — TASK-10 تا TASK-16
زمینه
بکاند فاز ۲ کاملاً پیادهسازی و تست شده است. این پرامپت فقط فرانتاند ۷ تسک را پیادهسازی میکند.
هر تسک که تمام شد → git commit بزن (message فارسی مختصر).
بعد از هر تسک بلافاصله yarn dev اجرا کن تا خطاهای TypeScript مشخص شوند.
قوانین ثابت پروژه
- Paginated: items از
data?.data، total ازdata?.meta?.totalRecords - Single resource: از
data?.data(ممکن است double-nested باشد —data?.data?.data) - Category API: triple-nested →
data?.data?.data ?? [] - تاریخ نمایش: همیشه با
formatDate()یاformatDateTime()ازlib/utils - Forms: React Hook Form + Zod resolver
- Data fetching: TanStack Query v5 (
useQuery/useMutation) - UI: از
DataTable,Pagination,Modal,ConfirmDialog,PageHeaderاستفاده کن — کتابخانه جدید اضافه نکن - RTL: همه استایلها RTL باشند
- کامیت بعد از هر تسک:
git addفایلهای تغییریافته →git commit -m "feat: ..."
TASK-10 — مدیریت پرسنل (Staff)
API
GET /api/v1/staff → لیست پرسنل فعال entity جاری
POST /api/v1/staff → ایجاد
PATCH /api/v1/staff/{uuid} → ویرایش
PATCH /api/v1/staff/{uuid}/toggle → فعال/غیرفعال
Response GET:
{
"success": true,
"data": [
{
"uuid": "...",
"full_name": "علی رضایی",
"phone": "09121234567",
"job_title": "پرستار",
"address": null,
"national_code": "0012345678",
"active": true,
"created_at": 1718000000
}
]
}
Response PATCH toggle:
{ "success": true, "data": { "active": false } }
فایلهای جدید
assets/admin/pages/StaffPage.tsx — صفحه مدیریت پرسنل:
useQuery(['staff'])→GET /api/v1/staff→data?.data ?? []- جدول با ستونها: نام، سمت، تلفن، کد ملی، وضعیت (badge)، عملیات
- دکمه «افزودن پرسنل» → Modal ایجاد
- دکمه ویرایش (PencilIcon) → Modal ویرایش
- دکمه toggle (EyeIcon/EyeSlashIcon) با
ConfirmDialogبرای غیرفعالسازی - حذف سخت نداریم — فقط toggle
فیلدهای فرم:
interface StaffFormData {
full_name: string; // required
phone?: string;
job_title?: string;
address?: string;
national_code?: string;
}
Zod schema:
const schema = z.object({
full_name: z.string().min(2, "نام حداقل ۲ کاراکتر باید باشد"),
phone: z.string().optional(),
job_title: z.string().optional(),
address: z.string().optional(),
national_code: z.string().optional(),
});
Route و Sidebar
در App.tsx اضافه کن:
<Route
path="staff"
element={
<RoleRoute roles={["doctor", "clinic"]}>
<StaffPage />
</RoleRoute>
}
/>
در Sidebar (فایل components/layout/AdminLayout.tsx یا Sidebar.tsx) لینک «پرسنل» را برای نقشهای doctor و clinic اضافه کن.
کامیت
git add assets/admin/pages/StaffPage.tsx assets/admin/App.tsx assets/admin/components/layout/...
git commit -m "feat: add staff management page (TASK-10)"
TASK-11 — پنل اشتراکی (Subscription)
API
GET /api/v1/subscription/plans → لیست پلنها (public)
GET /api/v1/subscription/my → اشتراک فعال من
POST /api/v1/subscription/trial → فعالسازی تریال
POST /api/v1/subscription-payment → شروع پرداخت { period_uuid, gateway }
Response GET /plans:
{
"success": true,
"data": [
{
"uuid": "...",
"name": "basic",
"level": 1,
"max_secretaries": 2,
"features": { "patient_records": true, "services": true },
"periods": [
{
"uuid": "...",
"label": "تریال ۱ ماهه",
"duration_months": 1,
"price_rials": 0,
"is_trial": true
},
{
"uuid": "...",
"label": "۱ ماهه",
"duration_months": 1,
"price_rials": 250000,
"is_trial": false
}
]
}
]
}
Response GET /my:
{
"success": true,
"data": {
"plan": {
"name": "basic",
"level": 1,
"features": { "patient_records": true, "services": true }
},
"period": { "label": "۶ ماهه", "duration_months": 6 },
"is_trial": false,
"starts_at": 1718000000,
"expires_at": 1733360000,
"used_trial": true,
"days_remaining": 42
}
}
فایلهای جدید
assets/admin/pages/SubscriptionPage.tsx — صفحه اشتراک:
بخش ۱ — وضعیت فعلی:
- نمایش پلن فعلی (نام، تاریخ انقضا به شمسی، روزهای باقیمانده)
- اگر
used_trial = false→ دکمه «فعالسازی تریال رایگان» (Basic یک ماهه) - نوار پیشرفت روزهای باقیمانده (اگر
expires_atدارد)
بخش ۲ — کارتهای پلن:
- سه کارت: Free / Basic / Professional
- هر کارت: نام پلن، ویژگیها (لیست چکمارک)، دورههای قیمت به صورت badge/tab
- دکمه «خرید» برای هر دوره → modal انتخاب gateway →
POST /api/v1/subscription-payment→ redirect به URL برگشتی - اگر پلن فعلی همان پلن است → دکمه «تمدید»
Types جدید (assets/admin/types/index.ts):
export interface SubscriptionPlan {
uuid: string;
name: string;
level: number;
max_secretaries: number;
features: Record<string, boolean>;
periods: SubscriptionPeriod[];
}
export interface SubscriptionPeriod {
uuid: string;
label: string;
duration_months: number;
price_rials: number;
is_trial: boolean;
}
export interface MySubscription {
plan: { name: string; level: number; features: Record<string, boolean> };
period?: { label: string; duration_months: number };
is_trial: boolean;
starts_at?: number;
expires_at?: number | null;
used_trial: boolean;
days_remaining?: number;
}
Route
<Route
path="subscription"
element={
<RoleRoute roles={["doctor", "clinic"]}>
<SubscriptionPage />
</RoleRoute>
}
/>
کامیت
git commit -m "feat: add subscription management page (TASK-11)"
TASK-12 — تکمیل Permissions منشی
هیچ صفحه جدیدی لازم نیست — فقط assets/admin/pages/SecretariesPage.tsx تغییر میکند.
وضعیت فعلی SecretariesPage.tsx
فایل موجود است و لیست منشی ها را نشان میدهد. باید:
- Modal ویرایش permissions اضافه کن:
- دکمه «دسترسیها» (ShieldCheckIcon) کنار هر ردیف
- Modal با checkbox matrix:
// ساختار permissions که باید از API بخوانی و ذخیره کنی
const DEFAULT_PERMISSIONS = {
appointments: {
view: true,
create: false,
cancel: false,
update_status: false,
},
addresses: { view: true, create: false, update: false, delete: false },
clinic_info: { view: true, update: false },
insurances: { view: true, create: false, update: false, delete: false },
};
const PERMISSION_LABELS = {
appointments: {
label: "نوبتها",
actions: [
{ key: "view", label: "مشاهده" },
{ key: "create", label: "ایجاد" },
{ key: "cancel", label: "لغو" },
{ key: "update_status", label: "تغییر وضعیت" },
],
},
addresses: {
label: "آدرسها",
actions: [
{ key: "view", label: "مشاهده" },
{ key: "create", label: "ایجاد" },
{ key: "update", label: "ویرایش" },
{ key: "delete", label: "حذف" },
],
},
clinic_info: {
label: "اطلاعات کلینیک",
actions: [
{ key: "view", label: "مشاهده" },
{ key: "update", label: "ویرایش" },
],
},
insurances: {
label: "بیمهها",
actions: [
{ key: "view", label: "مشاهده" },
{ key: "create", label: "ایجاد" },
{ key: "update", label: "ویرایش" },
{ key: "delete", label: "حذف" },
],
},
};
- PATCH
/api/v1/secretary/{uuid}با body:
{ "permissions": { "appointments": { "view": true, ... }, ... } }
- خطای
ERR_SECRETARY_LIMIT_REACHED(403) را هنگام ایجاد منشی handle کن:
// در onError useMutation create:
if (err.code === "ERR_SECRETARY_LIMIT_REACHED") {
toast.error(
"سقف تعداد منشی پنل فعلی شما پر است. برای افزودن منشی بیشتر پنل را ارتقا دهید.",
);
}
کامیت
git commit -m "feat: add secretary permissions matrix and subscription limit handling (TASK-12)"
TASK-13 — سرویسهای کلینیک
API
GET /api/v1/service-sections → لیست بخشها
POST /api/v1/service-section → ایجاد بخش { name }
PATCH /api/v1/service-section/{uuid} → ویرایش { name }
DELETE /api/v1/service-section/{uuid} → حذف بخش
GET /api/v1/service-items/{sectionUuid} → لیست آیتمهای یک بخش
POST /api/v1/service-item → ایجاد آیتم
PATCH /api/v1/service-item/{uuid} → ویرایش آیتم
DELETE /api/v1/service-item/{uuid} → حذف آیتم
Response GET /service-sections:
{
"success": true,
"data": [{ "uuid": "...", "name": "تزریقات", "active": true }]
}
Response GET /service-items/{sectionUuid}:
{
"success": true,
"data": [
{
"uuid": "...",
"name": "سرم ۵۰۰cc",
"price_rials": 85000,
"staff": { "uuid": "...", "full_name": "علی رضایی" },
"active": true
}
]
}
فایلهای جدید
assets/admin/pages/ClinicServicesPage.tsx — صفحه دوبخشی:
ستون چپ — لیست بخشها (ServiceSection):
- لیست ساده با دکمههای ویرایش/حذف
- دکمه «+ بخش جدید» → Modal با فیلد
name - کلیک روی بخش → بارگذاری آیتمهای آن در ستون راست
ستون راست — لیست آیتمهای بخش انتخابی (ServiceItem):
- جدول: نام، قیمت (
formatRial()), پرسنل مسئول، وضعیت - دکمه «+ آیتم جدید»
- حذف: اگر
ERR_SERVICE_ITEM_IN_USE(409) → toast.error('این سرویس در پرونده بیمار استفاده شده و قابل حذف نیست')
فرم آیتم:
interface ServiceItemForm {
section_uuid: string; // hidden — از state
name: string; // required
price_rials: number; // required
staff_uuid?: string; // SearchableSelect از GET /api/v1/staff
}
Types جدید:
export interface ServiceSection {
uuid: string;
name: string;
active: boolean;
}
export interface ServiceItem {
uuid: string;
name: string;
price_rials: number;
staff: { uuid: string; full_name: string } | null;
active: boolean;
}
Route
<Route
path="clinic-services"
element={
<RoleRoute roles={["doctor", "clinic"]}>
<ClinicServicesPage />
</RoleRoute>
}
/>
کامیت
git commit -m "feat: add clinic services management page (TASK-13)"
TASK-14 — پنل پیامکی
API
GET /api/v1/sms/wallet/balance → موجودی
POST /api/v1/sms/wallet/charge → شارژ { gateway, amount_rials } → redirect URL
GET /api/v1/sms/wallet/logs → تاریخچه (paginated)
GET /api/v1/sms/settings → تنظیمات
PATCH /api/v1/sms/settings → ذخیره تنظیمات
Response GET /sms/wallet/balance:
{
"success": true,
"data": {
"balance_rials": 150000,
"sms_price_rials": 250,
"estimated_sms_count": 600
}
}
Response GET /sms/wallet/logs:
{
"success": true,
"data": [
{
"uuid": "...",
"type": "credit",
"amount_rials": 500000,
"description": "شارژ",
"created_at": 1718000000
},
{
"uuid": "...",
"type": "debit",
"amount_rials": 250,
"description": "ارسال پیامک یادآوری",
"created_at": 1718001000
}
],
"meta": { "totalRecords": 45, "totalPages": 5, "currentPage": 1 }
}
Response GET /sms/settings:
{
"success": true,
"data": {
"reminder_enabled": true,
"reminder_hours_before": 3,
"post_visit_enabled": false,
"post_visit_text": null
}
}
فایلهای جدید
assets/admin/pages/SmsWalletPage.tsx — صفحه کیف پول پیامک:
بخش ۱ — کارت موجودی:
- نمایش
balance_rialsباformatRial() - نمایش
estimated_sms_count(تعداد پیامک قابل ارسال) - دکمه «شارژ کیف پول» → Modal با فیلد مبلغ + انتخاب gateway → redirect به URL
بخش ۲ — تنظیمات:
- Toggle «یادآوری قبل از نوبت» + فیلد عددی «چند ساعت قبل»
- Toggle «پیامک بعد از ویزیت» + textarea متن پیامک
- دکمه ذخیره →
PATCH /api/v1/sms/settings
بخش ۳ — تاریخچه تراکنشها:
- جدول: نوع (badge سبز credit / قرمز debit)، مبلغ، توضیحات، تاریخ
- Pagination
Types جدید:
export interface SmsWalletBalance {
balance_rials: number;
sms_price_rials: number;
estimated_sms_count: number;
}
export interface SmsWalletLog {
uuid: string;
type: "credit" | "debit";
amount_rials: number;
description: string;
created_at: number;
}
export interface SmsSettings {
reminder_enabled: boolean;
reminder_hours_before: number;
post_visit_enabled: boolean;
post_visit_text: string | null;
}
Route
<Route
path="sms-wallet"
element={
<RoleRoute roles={["doctor", "clinic"]}>
<SmsWalletPage />
</RoleRoute>
}
/>
کامیت
git commit -m "feat: add SMS wallet and settings page (TASK-14)"
TASK-15 — پرونده بیمار
API
GET /api/v1/patients → لیست بیماران (paginated, ?search=)
POST /api/v1/patient → ایجاد پرونده { user_uuid }
GET /api/v1/patient/{uuid} → جزئیات پرونده
GET /api/v1/patient/{uuid}/sessions → لیست مراجعات (paginated)
POST /api/v1/patient/{uuid}/session → ثبت مراجعه جدید
PATCH /api/v1/session/{uuid} → ویرایش مراجعه
Response GET /patients:
{
"success": true,
"data": [
{
"uuid": "...",
"user": {
"uuid": "...",
"fullName": "علی محمدی",
"phone": "09121234567"
},
"created_at": 1710000000
}
],
"meta": { "totalRecords": 12, "totalPages": 2, "currentPage": 1 }
}
Response GET /patient/{uuid}/sessions:
{
"success": true,
"data": [
{
"uuid": "...",
"appointment_uuid": null,
"visit_price_rials": 500000,
"base_insurance_discount_percent": "30.00",
"supplementary_discount_percent": "0.00",
"services_total_rials": 130000,
"final_price_rials": 445000,
"payment_method": "cash",
"notes": "...",
"created_at": 1718000000,
"updated_at": 1718000000
}
],
"meta": { "totalRecords": 5, "totalPages": 1, "currentPage": 1 }
}
Request POST /patient/{uuid}/session:
{
"visit_price_rials": 500000,
"base_insurance_discount_percent": 30,
"supplementary_discount_percent": 0,
"payment_method": "cash",
"notes": "...",
"services": [{ "service_item_uuid": "...", "staff_uuid": null }]
}
فایلهای جدید/تغییر
assets/admin/pages/MyPatientsPage.tsx — بازنویسی کامل:
نمای لیست بیماران:
- جستجو (
?search=) با debounce ۴۰۰ms - جدول: نام بیمار، تلفن، تاریخ ثبت، عملیات
- دکمه «+ بیمار جدید» → Modal با
SearchableSelectبرای پیدا کردن کاربر (search by phone) - کلیک روی ردیف → نمای جزئیات پرونده (state محلی یا route
/my-patients/:uuid)
نمای جزئیات پرونده:
- مشخصات بیمار + دکمه «+ مراجعه جدید»
- لیست مراجعات (accordion یا جدول):
- قیمت ویزیت، تخفیف بیمه پایه، تخفیف تکمیلی، مجموع سرویسها، قیمت نهایی
- وضعیت پرداخت (
payment_method) - یادداشت
- دکمه ویرایش (فقط
notesوpayment_method)
Modal ثبت مراجعه جدید:
interface SessionFormData {
visit_price_rials: number;
base_insurance_discount_percent: number; // 0-100
supplementary_discount_percent: number; // 0-100
payment_method: "cash" | "card" | "insurance" | "online" | "pending";
notes?: string;
services: { service_item_uuid: string; staff_uuid?: string }[];
}
- محاسبه
final_price_rialsبه صورت live در فرانت:
const afterBase = visitPrice * (1 - baseDiscount / 100);
const afterSupp = afterBase * (1 - suppDiscount / 100);
const servicesTotal = services.reduce(
(sum, s) => sum + (s.price_rials || 0),
0,
);
const finalPrice = Math.round(afterSupp) + servicesTotal;
- افزودن سرویس:
SearchableSelectازGET /api/v1/service-items/{sectionUuid}(باید ابتدا بخش را انتخاب کند)
Types جدید:
export interface PatientRecord {
uuid: string;
user: { uuid: string; fullName: string; phone: string };
created_at: number;
}
export interface PatientSession {
uuid: string;
appointment_uuid: string | null;
visit_price_rials: number;
base_insurance_discount_percent: string;
supplementary_discount_percent: string;
services_total_rials: number;
final_price_rials: number;
payment_method: string;
notes: string | null;
created_at: number;
updated_at: number;
}
Route
// مسیر موجود کافی است:
<Route
path="my-patients"
element={
<RoleRoute roles={["doctor", "secretary", "clinic"]}>
<MyPatientsPage />
</RoleRoute>
}
/>
کامیت
git commit -m "feat: implement patient records and sessions page (TASK-15)"
TASK-16 — داشبورد هوشمند با فیلتر زمانی
تغییر API
GET /api/v1/dashboard/clinic?from=UNIX&to=UNIX → فیلدهای جدید در stats
GET /api/v1/dashboard/doctor?from=UNIX&to=UNIX → فیلدهای جدید در stats
GET /api/v1/admin/dashboard/charts?from=UNIX&to=UNIX → سریهای زمانی
فیلدهای جدید در Response clinic/doctor:
{
"stats": {
"...فیلدهای قبلی...",
"sms_wallet_balance": 150000,
"unique_patients_count": 45,
"revenue_period_rials": 12500000
},
"period": { "from": 1717200000, "to": 1719792000 }
}
Response GET /admin/dashboard/charts?from=UNIX&to=UNIX:
{
"success": true,
"data": {
"appointments_by_day": [
{ "date": "06/01", "count": 12 }
],
"revenue_by_day": [
{ "date": "06/01", "amount": 3500000 }
],
"subscription_sales_by_plan": [
{ "plan": "basic", "count": 15, "revenue": 3750000 }
],
"appointment_status": [...],
"top_specialties": [...]
}
}
تغییر assets/admin/pages/DashboardPage.tsx
۱. فیلتر زمانی — اضافه کن:
type Preset = "this_week" | "this_month" | "3_months" | "custom";
// تبدیل به Unix:
function getRange(preset: Preset): { from: number; to: number } {
const now = Math.floor(Date.now() / 1000);
switch (preset) {
case "this_week":
// شنبهی این هفته — سادهترین راه: now - روزهای گذشته از شنبه
const dayOfWeek = new Date().getDay(); // 0=Sun,6=Sat
const daysSinceSat = (dayOfWeek + 1) % 7;
return { from: now - daysSinceSat * 86400, to: now };
case "this_month":
const d = new Date();
d.setDate(1);
d.setHours(0, 0, 0, 0);
return { from: Math.floor(d.getTime() / 1000), to: now };
case "3_months":
return { from: now - 90 * 86400, to: now };
default:
return { from: now - 30 * 86400, to: now };
}
}
preset buttons:
<div style={{ display: "flex", gap: 8, marginBottom: 16 }}>
{(["this_week", "this_month", "3_months"] as Preset[]).map((p) => (
<button
key={p}
className={`btn sm ${preset === p ? "primary" : ""}`}
onClick={() => setPreset(p)}
>
{p === "this_week"
? "این هفته"
: p === "this_month"
? "این ماه"
: "۳ ماه"}
</button>
))}
</div>
۲. فیلدهای جدید در کارتهای آمار:
- کارت «موجودی پیامک»:
stats.sms_wallet_balance→formatRial() - کارت «بیماران یکتا»:
stats.unique_patients_count - کارت «درآمد دوره»:
stats.revenue_period_rials→formatRial()
۳. نمودارها (برای admin role):
از SVG inline موجود در DashboardPage استفاده کن (کلاس SvgLineChart که در فایل موجود است):
// appointments_by_day و revenue_by_day دادههای خطی دارند
// فقط آرایه عددی را استخراج کن:
const apptData = charts?.appointments_by_day?.map((d: any) => d.count) ?? [];
const revData = charts?.revenue_by_day?.map((d: any) => d.amount) ?? [];
<SvgLineChart data={apptData} color="#3b82f6" />
<SvgLineChart data={revData} color="#22c55e" />
subscription_sales_by_plan — جدول ساده:
<table>
<thead>
<tr>
<th>پلن</th>
<th>تعداد</th>
<th>درآمد</th>
</tr>
</thead>
<tbody>
{subData.map((row) => (
<tr key={row.plan}>
<td>{row.plan}</td>
<td>{row.count}</td>
<td>{formatRial(row.revenue)}</td>
</tr>
))}
</tbody>
</table>
API call با from/to:
const [preset, setPreset] = useState<Preset>("this_month");
const range = getRange(preset);
// برای admin:
const { data: charts } = useQuery({
queryKey: ["dashboard-charts", range.from, range.to],
queryFn: () =>
api.get(
`/api/v1/admin/dashboard/charts?from=${range.from}&to=${range.to}`,
),
});
// برای doctor:
const { data: doctorDash } = useQuery({
queryKey: ["dashboard-doctor", range.from, range.to],
queryFn: () =>
api.get(`/api/v1/dashboard/doctor?from=${range.from}&to=${range.to}`),
enabled: primaryRole === "doctor",
});
کامیت
git commit -m "feat: add date range filter and new stats to dashboard (TASK-16)"
ترتیب اجرا و چکلیست
[ ] TASK-10: StaffPage.tsx → route → sidebar → yarn dev → commit
[ ] TASK-11: SubscriptionPage.tsx → route → sidebar → yarn dev → commit
[ ] TASK-12: SecretariesPage.tsx (permissions modal + limit error) → yarn dev → commit
[ ] TASK-13: ClinicServicesPage.tsx → route → sidebar → yarn dev → commit
[ ] TASK-14: SmsWalletPage.tsx → route → sidebar → yarn dev → commit
[ ] TASK-15: MyPatientsPage.tsx (بازنویسی) → yarn dev → commit
[ ] TASK-16: DashboardPage.tsx (date range + new stats + charts) → yarn dev → commit
قوانین اجرا
- یک تسک در یک زمان — هرگز دو تسک را همزمان شروع نکن
- بعد از هر تسک
ddev exec yarn dev— هر TypeScript error را همانجا رفع کن - اگر API error داشت — فایل
docs/api/مربوطه را بخوان - Types جدید را در
assets/admin/types/index.tsاضافه کن - Sidebar — برای هر صفحه جدید، لینک sidebar را هم اضافه کن (در
AdminLayout.tsxیاSidebar.tsx) - کامیت بعد از
yarn devبدون خطا — نه قبل از آن
فایلهای مرجع مهم
| فایل | نقش |
|---|---|
assets/admin/lib/api.ts |
fetch wrapper — api.get/post/patch/delete |
assets/admin/lib/utils.ts |
formatRial, formatDate, formatDateTime, formatNumber |
assets/admin/types/index.ts |
همه TypeScript interfaces |
assets/admin/stores/authStore.ts |
primaryRole, dbUuid, doctorUuid |
assets/admin/components/ui/DataTable.tsx |
جدول — Column<T>[] |
assets/admin/components/ui/Modal.tsx |
Modal |
assets/admin/components/ui/SearchableSelect.tsx |
dropdown با جستجو |
docs/api/staff.md |
API staff |
docs/api/subscription.md |
API subscription |
docs/api/clinic-services.md |
API services |
docs/api/sms.md |
API SMS wallet |
docs/api/patient.md |
API patient records |
docs/api/dashboard.md |
API dashboard |