feat: Implement Jalali calendar support for dashboard charts

- Added support for Jalali calendar in the dashboard, allowing charts to display data based on the current Jalali month and year.
- Updated the API to return `patients_year`, `patients_month`, and `revenue_year` parameters for the dashboard charts.
- Refactored the dashboard controller to handle Jalali date calculations and queries.
- Modified the frontend components to utilize the new Jalali date parameters and reflect changes in the UI.
- Removed the status column from the NewAppointmentsTable as status management is now handled on the appointments page.
- Added tests to ensure the correct functioning of the new Jalali chart period features.
This commit is contained in:
hamed
2026-07-18 21:44:01 +03:30
parent 7921407f33
commit 62a9fd87c3
7 changed files with 479 additions and 100 deletions
@@ -3,12 +3,11 @@
* `src/components/dashboard/list/` (CustomTable + DetailT + Status).
*
* Source data was static mock; here it is fed by the real dashboard API
* (`/api/v1/dashboard/{clinic,doctor}` → `today_appointments`). Statuses are
* clinicpro's real appointment statuses, mapped to the source's pill palette.
* (`/api/v1/dashboard/{clinic,doctor}` → `today_appointments`). Read-only —
* status display/editing lives on the appointments page, not the dashboard.
*/
import React from 'react';
import { Link } from 'react-router-dom';
import { ChevronDownIcon } from './dashboardIcons';
export interface ApptRow {
uuid: string;
@@ -21,50 +20,12 @@ export interface ApptRow {
status: string;
}
interface StatusStyle {
label: string;
/** text + chevron color */
fg: string;
/** pill background classes (light + dark) */
bg: string;
}
/**
* Map a clinicpro appointment status to the source pill palette
* (green/amber/blue/violet/red pastels). Unknown → neutral gray.
*/
const STATUS_STYLE: Record<string, StatusStyle> = {
visited: { label: 'ویزیت شده', fg: '#3c9a4f', bg: 'bg-[#e4f2ea] dark:bg-[#324A32]' },
completed: { label: 'تکمیل شده', fg: '#3c9a4f', bg: 'bg-[#e4f2ea] dark:bg-[#324A32]' },
reserved: { label: 'رزرو شده', fg: '#0088ff', bg: 'bg-[#E3F2FD] dark:bg-[#1A2836]' },
checked_in: { label: 'ورود به مطب', fg: '#0088ff', bg: 'bg-[#E3F2FD] dark:bg-[#1A2836]' },
waiting_for_payment: { label: 'انتظار پرداخت', fg: '#f59e0b', bg: 'bg-[#FFF3E0] dark:bg-[#3E2E1B]' },
waiting: { label: 'صف انتظار', fg: '#f59e0b', bg: 'bg-[#FFF3E0] dark:bg-[#3E2E1B]' },
in_progress: { label: 'در حال ویزیت', fg: '#7c3aed', bg: 'bg-[#F3E5F5] dark:bg-[#2D1B36]' },
cancelled_by_doctor: { label: 'لغو پزشک', fg: '#d32f2f', bg: 'bg-[#fde7e9] dark:bg-[rgba(255,84,80,0.20)]' },
cancelled_by_user: { label: 'لغو بیمار', fg: '#d32f2f', bg: 'bg-[#fde7e9] dark:bg-[rgba(255,84,80,0.20)]' },
auto_cancel_unpaid: { label: 'لغو خودکار', fg: '#d32f2f', bg: 'bg-[#fde7e9] dark:bg-[rgba(255,84,80,0.20)]' },
no_show: { label: 'غیبت', fg: '#d32f2f', bg: 'bg-[#fde7e9] dark:bg-[rgba(255,84,80,0.20)]' },
};
function StatusChip({ status }: { status: string }) {
const s = STATUS_STYLE[status] ?? { label: status, fg: '#616161', bg: 'bg-[#EFEFEF] dark:bg-[#35343D]' };
return (
<div className={`w-[130px] py-[4px] px-[8px] rounded-[4px] flex items-center justify-between select-none ${s.bg}`}>
<span className="text-[16px] font-medium flex-1 text-center" style={{ color: s.fg }}>
{s.label}
</span>
<ChevronDownIcon color={s.fg} />
</div>
);
}
function formatTime(ts?: number | null): string {
if (!ts) return '—';
return new Intl.DateTimeFormat('fa-IR', { timeZone: 'Asia/Tehran', hour: '2-digit', minute: '2-digit' }).format(new Date(ts * 1000));
}
const HEAD = ['ردیف', 'نام بیمار', 'شماره تماس', 'شروع', 'پایان', 'سرویس', 'پرسنل', 'وضعیت', 'عملیات'];
const HEAD = ['ردیف', 'نام بیمار', 'شماره تماس', 'شروع', 'پایان', 'سرویس', 'پرسنل', 'عملیات'];
export function NewAppointmentsTable({ rows, loading }: { rows: ApptRow[]; loading?: boolean }) {
if (loading) {
@@ -105,9 +66,6 @@ export function NewAppointmentsTable({ rows, loading }: { rows: ApptRow[]; loadi
<Cell className="text-right" dir="ltr">{formatTime(r.slot_end)}</Cell>
<Cell className="text-right">{r.service_name || '—'}</Cell>
<Cell className="text-start">{r.doctor_name ? `دکتر ${r.doctor_name}` : '—'}</Cell>
<td className="py-[10px] px-[18px]">
<StatusChip status={r.status} />
</td>
<td className="py-[10px] px-[18px] text-center">
<Link
to="/admin/appointments"