- Refactor color palette in `ui-design-spec.md` to utilize CSS variables exclusively, eliminating fixed hex values and Tailwind utility classes. - Complete dark mode implementation in `uiStore.ts`, ensuring proper theme application via `applyTheme()` and `applyBrand()`. - Create `admin-theme-dark-light-audit.md` to document the transition process, outlining issues with inline styles and fixed colors. - Introduce `theme-tokens.test.ts` to enforce rules against fixed hex colors and ensure compliance with the design system. - Update various components and styles to replace inline styles and fixed colors with CSS variables, ensuring consistent theming across light and dark modes. - Ensure all changes maintain visual integrity in both light and dark modes, with a focus on accessibility and contrast standards.
137 lines
5.6 KiB
TypeScript
137 lines
5.6 KiB
TypeScript
/**
|
|
* «لیست نوبتهای جدید» table — ported from clinic-pro-tauri
|
|
* `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`) or, when a
|
|
* `queryKey` is supplied, by the filtered doctor list endpoint. Status is
|
|
* editable in place only when the row carries a `version` (optimistic lock)
|
|
* and the parent passes the query key to invalidate.
|
|
*/
|
|
import React from 'react';
|
|
import { Link } from 'react-router-dom';
|
|
import AppointmentStatusDropdown, { STATUS_META } from '../ui/AppointmentStatusDropdown';
|
|
|
|
export interface ApptRow {
|
|
uuid: string;
|
|
patient_name: string | null;
|
|
patient_mobile?: string | null;
|
|
doctor_name?: string | null;
|
|
service_name?: string | null;
|
|
slot_start: number;
|
|
slot_end?: number | null;
|
|
status: string;
|
|
/** لازم برای قفل خوشبینانهٔ PATCH وضعیت؛ نبودنش یعنی وضعیت فقطخواندنی است. */
|
|
version?: number;
|
|
}
|
|
|
|
/**
|
|
* روزِ محلیِ نوبت (YYYY-MM-DD) برای دیپلینک «مشاهده» به همان تاریخ در لیست نوبتها.
|
|
* همان قراردادی که AppointmentDetailPage/AppointmentsPage استفاده میکنند.
|
|
*/
|
|
const isoDay = (ts?: number | null): string => {
|
|
if (!ts) return '';
|
|
const d = new Date(ts * 1000);
|
|
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
};
|
|
|
|
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 = ['ردیف', 'نام بیمار', 'شماره تماس', 'شروع', 'پایان', 'سرویس', 'پرسنل', 'وضعیت', 'عملیات'];
|
|
|
|
interface Props {
|
|
rows: ApptRow[];
|
|
loading?: boolean;
|
|
/** کلید کوئری برای invalidate پس از تغییر وضعیت. بدون آن وضعیت فقط نمایش داده میشود. */
|
|
queryKey?: unknown[];
|
|
emptyText?: string;
|
|
}
|
|
|
|
export function NewAppointmentsTable({ rows, loading, queryKey, emptyText }: Props) {
|
|
if (loading) {
|
|
return <div className="skeleton h-[180px] rounded-[8px]" />;
|
|
}
|
|
if (!rows.length) {
|
|
return (
|
|
<p className="text-center py-[32px] text-[13.5px] text-[var(--text-2)]">
|
|
{emptyText ?? 'نوبتی برای امروز ثبت نشده'}
|
|
</p>
|
|
);
|
|
}
|
|
return (
|
|
<div className="w-full overflow-x-auto rounded-[8px] border border-solid border-[var(--border)]">
|
|
<table className="w-full border-collapse">
|
|
<thead>
|
|
<tr className="bg-[var(--surface-2)]">
|
|
{HEAD.map((h, i) => (
|
|
<th
|
|
key={h}
|
|
className={`text-[var(--text-2)] dark:text-[var(--text)] text-[14px] font-normal py-[10px] px-[18px] whitespace-nowrap ${i === HEAD.length - 1 ? 'text-center' : 'text-start'}`}
|
|
>
|
|
{h}
|
|
</th>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{rows.map((r, idx) => (
|
|
<tr
|
|
key={r.uuid}
|
|
className="border-b border-[var(--border-2)] dark:border-transparent hover:bg-[var(--primary-soft)] dark:hover:bg-[var(--surface-2)] transition-colors"
|
|
>
|
|
<Cell>{new Intl.NumberFormat('fa-IR').format(idx + 1)}</Cell>
|
|
<Cell>{r.patient_name || '—'}</Cell>
|
|
<Cell>{r.patient_mobile ? <bdi dir="ltr">{r.patient_mobile}</bdi> : '—'}</Cell>
|
|
<Cell><bdi dir="ltr">{formatTime(r.slot_start)}</bdi></Cell>
|
|
<Cell><bdi dir="ltr">{formatTime(r.slot_end)}</bdi></Cell>
|
|
<Cell>{r.service_name || '—'}</Cell>
|
|
<Cell>{r.doctor_name || '—'}</Cell>
|
|
<Cell>
|
|
{queryKey && r.version != null
|
|
? <AppointmentStatusDropdown uuid={r.uuid} currentStatus={r.status} version={r.version} queryKey={queryKey} />
|
|
: <StatusPill status={r.status} />}
|
|
</Cell>
|
|
<td className="py-[10px] px-[18px] text-center">
|
|
<Link
|
|
to={isoDay(r.slot_start) ? `/admin/appointments?date=${isoDay(r.slot_start)}` : '/admin/appointments'}
|
|
className="text-[var(--primary)] text-[12px] font-medium hover:underline whitespace-nowrap"
|
|
>
|
|
مشاهده
|
|
</Link>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/** نمایش فقطخواندنی وضعیت — وقتی version یا queryKey در دسترس نیست. */
|
|
function StatusPill({ status }: { status: string }) {
|
|
const meta = STATUS_META[status] ?? { label: status, color: 'var(--text-3)' };
|
|
return (
|
|
<span
|
|
className="inline-flex items-center gap-[5px] px-[10px] py-[3px] rounded-full text-[12px] font-bold whitespace-nowrap"
|
|
style={{ background: `${meta.color}15`, border: `1.5px solid ${meta.color}30`, color: meta.color }}
|
|
>
|
|
<span className="w-[7px] h-[7px] rounded-full shrink-0" style={{ background: meta.color }} />
|
|
{meta.label}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
/** همهی سلولها text-start هستند تا دقیقاً زیر هدر همنامشان بنشینند. */
|
|
function Cell({ children, className = '' }: { children: React.ReactNode; className?: string }) {
|
|
return (
|
|
<td
|
|
className={`text-[var(--text-2)] text-[16px] font-medium py-[10px] px-[18px] whitespace-nowrap text-start ${className}`}
|
|
>
|
|
{children}
|
|
</td>
|
|
);
|
|
}
|