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:
@@ -10,6 +10,10 @@ vi.mock('../lib/api', () => ({
|
||||
import { api } from '../lib/api';
|
||||
import { useAuthStore } from '../stores/authStore';
|
||||
import DashboardPage from './DashboardPage';
|
||||
import { JALALI_MONTHS } from '../components/dashboard/TauriDashboardView';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const jalaali = require('jalaali-js') as { toJalaali: (d: Date) => { jy: number; jm: number } };
|
||||
|
||||
const get = api.get as ReturnType<typeof vi.fn>;
|
||||
|
||||
@@ -33,12 +37,18 @@ const clinicPayload = {
|
||||
{ label: '۸ خرداد', amount_rials: 160_000 },
|
||||
{ label: '۹ خرداد', amount_rials: 90_000 },
|
||||
],
|
||||
revenue_by_month: [
|
||||
{ label: 'فروردین', amount_rials: 60_000 },
|
||||
{ label: 'اردیبهشت', amount_rials: 160_000 },
|
||||
{ label: 'خرداد', amount_rials: 90_000 },
|
||||
],
|
||||
appointments_by_day: [
|
||||
{ label: '۷ خرداد', count: 15 },
|
||||
{ label: '۸ خرداد', count: 40 },
|
||||
{ label: '۹ خرداد', count: 48 },
|
||||
{ label: '۱', count: 15 },
|
||||
{ label: '۲', count: 40 },
|
||||
{ label: '۳', count: 48 },
|
||||
],
|
||||
},
|
||||
charts_period: { patients_year: 1405, patients_month: 4, revenue_year: 1405 },
|
||||
today_appointments: [
|
||||
{
|
||||
uuid: 'appt-1',
|
||||
@@ -84,7 +94,10 @@ describe('DashboardPage (ported clinic dashboard)', () => {
|
||||
expect(screen.getByText('دنیا خلیلی')).toBeInTheDocument();
|
||||
expect(screen.getByText('09136549874')).toBeInTheDocument();
|
||||
expect(screen.getByText('ویزیت عمومی')).toBeInTheDocument();
|
||||
expect(screen.getByText('ویزیت شده')).toBeInTheDocument();
|
||||
|
||||
// ستون «وضعیت» از داشبورد حذف شده — تغییر وضعیت فقط در صفحه نوبتهاست
|
||||
expect(screen.queryByText('وضعیت')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('ویزیت شده')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls the real clinic dashboard endpoint', async () => {
|
||||
@@ -92,4 +105,23 @@ describe('DashboardPage (ported clinic dashboard)', () => {
|
||||
await screen.findByText('تعداد کل مراجعین');
|
||||
expect(get).toHaveBeenCalledWith(expect.stringContaining('/api/v1/dashboard/clinic'));
|
||||
});
|
||||
|
||||
it('نمودارها را روی ماه و سال شمسی جاری درخواست میکند', async () => {
|
||||
renderWithProviders(<DashboardPage />, { route: '/admin/dashboard' });
|
||||
await screen.findByText('تعداد کل مراجعین');
|
||||
|
||||
const url = get.mock.calls.map(c => String(c[0])).find(u => u.startsWith('/api/v1/dashboard/clinic'))!;
|
||||
const params = new URLSearchParams(url.split('?')[1]);
|
||||
const now = jalaali.toJalaali(new Date());
|
||||
|
||||
expect(params.get('patients_year')).toBe(String(now.jy));
|
||||
expect(params.get('patients_month')).toBe(String(now.jm));
|
||||
expect(params.get('revenue_year')).toBe(String(now.jy));
|
||||
|
||||
// سلکتورها همان دوره را نشان میدهند
|
||||
expect(screen.getByText(JALALI_MONTHS[now.jm - 1])).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(`سال ${new Intl.NumberFormat('fa-IR', { useGrouping: false }).format(now.jy)}`)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,9 +13,37 @@ import { formatNumber, formatRial, formatDateTime } from '../lib/utils';
|
||||
import { useAuthStore } from '../stores/authStore';
|
||||
import InviteDoctorModal from '../components/ui/InviteDoctorModal';
|
||||
import { TauriDashboardView } from '../components/dashboard/TauriDashboardView';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const jalaali = require('jalaali-js') as {
|
||||
toJalaali: (date: Date) => { jy: number; jm: number; jd: number };
|
||||
};
|
||||
import { NewAppointmentsTable } from '../components/dashboard/NewAppointmentsTable';
|
||||
import { usePermissions } from '../hooks/usePermissions';
|
||||
|
||||
// ── Chart period (Jalali) ─────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* دورهی نمودارهای داشبورد: نمودار بیماران روی یک ماه شمسی و نمودار درآمد روی یک
|
||||
* سال شمسی. پیشفرض = ماه/سال جاری؛ تغییر سلکتور باعث fetch مجدد میشود.
|
||||
*/
|
||||
function useJalaliChartPeriod() {
|
||||
const now = useMemo(() => jalaali.toJalaali(new Date()), []);
|
||||
const [patientsMonth, setPatientsMonth] = useState(now.jm);
|
||||
const [revenueYear, setRevenueYear] = useState(now.jy);
|
||||
|
||||
return {
|
||||
currentJalaliYear: now.jy,
|
||||
patientsYear: now.jy,
|
||||
patientsMonth,
|
||||
setPatientsMonth,
|
||||
revenueYear,
|
||||
setRevenueYear,
|
||||
/** query string پارامترهای دورهی نمودار */
|
||||
query: `patients_year=${now.jy}&patients_month=${patientsMonth}&revenue_year=${revenueYear}`,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Shared Status Maps ────────────────────────────────────────────────────
|
||||
|
||||
const APPT_LABEL: Record<string, string> = {
|
||||
@@ -614,9 +642,10 @@ function ClinicDashboard() {
|
||||
const [inviteOpen, setInviteOpen] = useState(false);
|
||||
const preset: DatePreset = 'this_month';
|
||||
const range = getRange(preset);
|
||||
const chartPeriod = useJalaliChartPeriod();
|
||||
const q = useQuery({
|
||||
queryKey: ['dashboard-clinic', preset],
|
||||
queryFn: () => api.get<ApiResponse<ClinicDashboardData>>(`/api/v1/dashboard/clinic?from=${range.from}&to=${range.to}`),
|
||||
queryKey: ['dashboard-clinic', preset, chartPeriod.query],
|
||||
queryFn: () => api.get<ApiResponse<ClinicDashboardData>>(`/api/v1/dashboard/clinic?from=${range.from}&to=${range.to}&${chartPeriod.query}`),
|
||||
staleTime: 60_000,
|
||||
});
|
||||
|
||||
@@ -636,11 +665,16 @@ function ClinicDashboard() {
|
||||
todayAppointments: d?.stats.today_appointments ?? 0,
|
||||
}}
|
||||
patientBars={(d?.charts?.appointments_by_day ?? []).map(x => ({ label: x.label, value: x.count }))}
|
||||
incomeLine={(d?.charts?.revenue_by_day ?? []).map(x => ({ label: x.label, value: x.amount_rials }))}
|
||||
incomeLine={(d?.charts?.revenue_by_month ?? []).map(x => ({ label: x.label, value: x.amount_rials }))}
|
||||
appointments={d?.today_appointments ?? []}
|
||||
loading={q.isFetching}
|
||||
formatNumber={formatNumber}
|
||||
formatRial={formatRial}
|
||||
patientsMonth={chartPeriod.patientsMonth}
|
||||
onPatientsMonthChange={chartPeriod.setPatientsMonth}
|
||||
revenueYear={chartPeriod.revenueYear}
|
||||
onRevenueYearChange={chartPeriod.setRevenueYear}
|
||||
currentJalaliYear={chartPeriod.currentJalaliYear}
|
||||
/>
|
||||
|
||||
{/* پزشکان کلینیک — بخش عملکردی موجود clinicpro، حفظشده زیر نمای منتقلشده */}
|
||||
@@ -798,7 +832,11 @@ function DoctorClinicInvitationsCard() {
|
||||
// ── Doctor Dashboard ──────────────────────────────────────────────────────
|
||||
|
||||
interface DashboardCharts {
|
||||
/** ۷ روز اخیر — فقط برای کارتهای آماری */
|
||||
revenue_by_day: { label: string; amount_rials: number }[];
|
||||
/** ۱۲ ماه سال شمسی انتخابشده — نمودار «میزان درآمد» */
|
||||
revenue_by_month?: { label: string; amount_rials: number }[];
|
||||
/** روزهای ماه شمسی انتخابشده — نمودار «تعداد بیماران» */
|
||||
appointments_by_day: { label: string; count: number }[];
|
||||
}
|
||||
|
||||
@@ -818,9 +856,10 @@ interface DoctorDashboardData {
|
||||
function DoctorDashboard() {
|
||||
const preset: DatePreset = 'this_month';
|
||||
const range = getRange(preset);
|
||||
const chartPeriod = useJalaliChartPeriod();
|
||||
const q = useQuery({
|
||||
queryKey: ['dashboard-doctor', preset],
|
||||
queryFn: () => api.get<ApiResponse<DoctorDashboardData>>(`/api/v1/dashboard/doctor?from=${range.from}&to=${range.to}`),
|
||||
queryKey: ['dashboard-doctor', preset, chartPeriod.query],
|
||||
queryFn: () => api.get<ApiResponse<DoctorDashboardData>>(`/api/v1/dashboard/doctor?from=${range.from}&to=${range.to}&${chartPeriod.query}`),
|
||||
staleTime: 60_000,
|
||||
});
|
||||
|
||||
@@ -839,11 +878,16 @@ function DoctorDashboard() {
|
||||
todayAppointments: d?.stats.today_appointments ?? 0,
|
||||
}}
|
||||
patientBars={(d?.charts?.appointments_by_day ?? []).map(x => ({ label: x.label, value: x.count }))}
|
||||
incomeLine={(d?.charts?.revenue_by_day ?? []).map(x => ({ label: x.label, value: x.amount_rials }))}
|
||||
incomeLine={(d?.charts?.revenue_by_month ?? []).map(x => ({ label: x.label, value: x.amount_rials }))}
|
||||
appointments={d?.today_appointments ?? []}
|
||||
loading={q.isFetching}
|
||||
formatNumber={formatNumber}
|
||||
formatRial={formatRial}
|
||||
patientsMonth={chartPeriod.patientsMonth}
|
||||
onPatientsMonthChange={chartPeriod.setPatientsMonth}
|
||||
revenueYear={chartPeriod.revenueYear}
|
||||
onRevenueYearChange={chartPeriod.setRevenueYear}
|
||||
currentJalaliYear={chartPeriod.currentJalaliYear}
|
||||
/>
|
||||
|
||||
<div className="grid-2" style={{ marginTop: 'var(--gap)' }}>
|
||||
|
||||
Reference in New Issue
Block a user