Files
clinicpro/assets/admin/pages/PlanAccuracyPage.tsx
T
hamedandClaude Opus 5 635bf3d2a8 fix(admin): correct two design-system mismatches found by looking at the pages
Screenshotting the pages under dark mode and compact density (rather than
trusting that design tokens were enough) turned up two mistakes repeated across
every page this feature set added:

- `.card` carries only the surface, border and radius — padding comes from the
  separate `.card-pad`. Fifteen cards were rendering with their content flush
  against the edges.
- `.field` *is* the input box, a 40px-tall flex row. Wrapping a label plus a
  control in it produced a joined addon rather than a label above its field.
  `.field-block` is the label-above layout, and thirty-seven wrappers now use it.

Both were invisible to type-checking and to the tests, which is exactly why the
visual pass was worth running. Numbers in the new UI now go through
formatNumber so they render as Persian digits, and the utilization page's
header no longer repeats the sentence that appears under its filters verbatim.

The QA driver gained a `--ui` flag: theme and density live in
localStorage['clinicpro-ui'], so without seeding them dark mode and compact
density cannot be screenshotted at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 21:42:11 +03:30

124 lines
4.3 KiB
TypeScript

import React, { useMemo, useState } from 'react';
import PageHeader from '../components/ui/PageHeader';
import DataTable, { type Column } from '../components/ui/DataTable';
import SearchableSelect from '../components/ui/SearchableSelect';
import { Link } from 'react-router-dom';
import { usePlanAccuracy } from '../hooks/useReports';
import type { AccuracyRow } from '../types';
const RANGES = [
{ value: '30', label: 'ماه گذشته' },
{ value: '90', label: 'سه ماه گذشته' },
];
const SEVERITY: Record<AccuracyRow['severity'], { label: string; className: string }> = {
none: { label: 'دقیق', className: 'badge green' },
low: { label: 'کم', className: 'badge' },
medium: { label: 'متوسط', className: 'badge amber' },
high: { label: 'زیاد', className: 'badge red' },
};
/**
* مدت پیش‌بینی‌شده در برابر مدت واقعی.
*
* سرویسی که یک ساعت پیش‌بینی شده ولی یک‌ساعت‌ونیم طول می‌کشد، هر روز نیم ساعت از ظرفیت
* کلینیک را بی‌صدا می‌خورد — این صفحه تنها جایی است که آن را نشان می‌دهد.
*/
export default function PlanAccuracyPage() {
const [days, setDays] = useState('30');
const range = useMemo(() => {
const to = Math.floor(Date.now() / 1000);
return { from: to - Number(days) * 86400, to };
}, [days]);
const { rows, loading } = usePlanAccuracy(range.from, range.to);
const columns: Column<AccuracyRow>[] = [
{
key: 'service_name',
header: 'خدمت',
render: (r) => (
<div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<span style={{ fontWeight: 600 }}>{r.service_name}</span>
<span style={{ fontSize: 12, color: 'var(--text-3)' }}>{r.sample_size} نوبت</span>
</div>
),
},
{
key: 'planned_minutes',
header: 'پیش‌بینی',
render: (r) => <span style={{ fontSize: 13 }}>{r.planned_minutes} دقیقه</span>,
},
{
key: 'actual_minutes',
header: 'واقعی',
render: (r) => <span style={{ fontSize: 13 }}>{r.actual_minutes} دقیقه</span>,
},
{
key: 'deviation_percent',
header: 'انحراف',
render: (r) => (
<span
style={{
fontSize: 13,
fontWeight: 600,
color: r.severity === 'high' ? 'var(--danger)' : r.severity === 'medium' ? 'var(--warning)' : undefined,
}}
>
{r.deviation_percent > 0 ? `+${r.deviation_percent}` : r.deviation_percent}٪
</span>
),
},
{
key: 'fix',
header: '',
// گزارشی که راه اصلاح ندهد خوانده نمی‌شود.
render: (r) => (
<Link className="btn secondary sm" to={`/admin/services?service=${r.service_uuid}`}>
ویرایش بخش‌های این خدمت
</Link>
),
},
{
key: 'severity',
header: 'شدت',
render: (r) => (
<span className={SEVERITY[r.severity].className}>
<span className="bdot" />
{SEVERITY[r.severity].label}
</span>
),
},
];
return (
<div className="fade-in">
<PageHeader
title="دقت برنامهٔ نوبت"
description="مقایسهٔ مدت پیش‌بینی‌شدهٔ هر خدمت با مدت واقعی نوبت‌های انجام‌شده."
backTo="/admin/settings-menu"
/>
<div className="card card-pad" style={{ marginBottom: 16, display: 'flex', gap: 12, flexWrap: 'wrap' }}>
<div className="field-block" style={{ minWidth: 200, margin: 0 }}>
<label>بازه</label>
<SearchableSelect value={days} onChange={(v) => setDays(String(v ?? '30'))} options={RANGES} />
</div>
<span style={{ fontSize: 12, color: 'var(--text-3)', alignSelf: 'flex-end' }}>
خدماتی با کمتر از سه نوبت انجام‌شده در گزارش نمی‌آیند.
</span>
</div>
<div style={{ overflowX: 'auto' }}>
<DataTable
columns={columns}
data={rows}
loading={loading}
emptyMessage="داده‌ای برای این بازه نیست"
/>
</div>
</div>
);
}