Files
clinicpro/assets/admin/pages/PolicySimulationPage.tsx
T
hamedandClaude Opus 5 5c754244f2 feat(admin): finish the screens that were stopping one step short
Five places where the data existed and the screen did not use it.

Booking a whole course had no button because it needs a doctor and the course
does not carry one — each session can be with a different doctor. The page now
asks for the doctor the same way the resource booking page does, and the button
explains that it is all-or-nothing before it is pressed.

A course whose package does not cover the remaining sessions is still valid —
the rest is simply charged normally — but nobody was told. The course response
carries package_balance and the shortfall, and the page warns. Before session
six, not during it.

The credit ledger already returned who recorded a row and which appointment it
belonged to, and showed neither. An adjustable ledger without the name of the
person who adjusted it is half an audit trail.

Version history printed a JSON blob of each version's effects, which does not
answer the question anyone actually has: what changed? It now diffs each
version against the previous one, field by field, and says so plainly when a
version changed nothing meaningful.

A resource with no calendar showed "—" for utilization. Null means undefined,
not zero, and the next step is always the same: set up the calendar. It is a
link now. The report range also accepts a custom from/to, kept in the URL like
the rest.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 15:07:27 +03:30

185 lines
7.5 KiB
TypeScript

import React, { useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { BeakerIcon } from '@heroicons/react/24/outline';
import PageHeader from '../components/ui/PageHeader';
import ConfirmDialog from '../components/ui/ConfirmDialog';
import DataTable, { type Column } from '../components/ui/DataTable';
import PolicyVersionDiff from '../components/PolicyVersionDiff';
import { formatDate } from '../lib/utils';
import { usePermissions } from '../hooks/usePermissions';
import { usePolicies, usePolicy, usePolicySimulation } from '../hooks/usePolicies';
import type { PolicySimulationRun, SimulationRow } from '../types';
const SEVERITY: Record<PolicySimulationRun['severity'], { label: string; className: string; note: string }> = {
none: {
label: 'بدون اثر',
className: 'badge',
note: 'این قانون روی هیچ نوبتی از نمونه اثر نداشت — احتمالاً شرطش هرگز برقرار نمی‌شود.',
},
low: { label: 'کم', className: 'badge green', note: 'اثر محدود و قابل انتظار.' },
medium: { label: 'متوسط', className: 'badge amber', note: 'بخش قابل‌توجهی از نوبت‌ها تغییر می‌کنند.' },
high: {
label: 'زیاد',
className: 'badge red',
note: 'بیشتر نوبت‌های نمونه تغییر می‌کنند. مطمئنید قانون درست نوشته شده؟',
},
};
/**
* آزمایش قانون روی نوبت‌های واقعی گذشته.
*
* ستون «وضعیت فعلی → با این قانون» تنها چیزی است که کاربر غیرفنی می‌فهمد؛ درصد و شدت
* هم لازم‌اند چون قانونی که ۹۸٪ نوبت‌ها را عوض می‌کند تقریباً همیشه اشتباه است.
*/
export default function PolicySimulationPage() {
const { policyUuid } = useParams<{ policyUuid: string }>();
const navigate = useNavigate();
const { policy } = usePolicy(policyUuid);
const { runs, loading, run } = usePolicySimulation(policyUuid);
const { activate } = usePolicies();
const { can } = usePermissions();
const canManage = can('appointment_settings', 'update');
const [latest, setLatest] = useState<PolicySimulationRun | null>(null);
const [confirming, setConfirming] = useState(false);
const current = latest ?? runs[0] ?? null;
const isCurrentVersion = !!policy && !!current && current.policy_version === policy.version;
const severity = current ? SEVERITY[current.severity] : null;
const columns: Column<SimulationRow>[] = [
{
key: 'patient_name',
header: 'بیمار',
render: (r) => <span style={{ fontSize: 13 }}>{r.patient_name}</span>,
},
{
key: 'slot_start',
header: 'تاریخ نوبت',
render: (r) => <span style={{ fontSize: 13 }}>{formatDate(r.slot_start)}</span>,
},
{
key: 'change',
header: 'وضعیت فعلی ← با این قانون',
render: (r) => (
<span style={{ fontSize: 13 }}>
<span style={{ color: 'var(--text-3)' }}>{r.before}</span>
{' ← '}
<span style={{ fontWeight: 600 }}>{r.after}</span>
</span>
),
},
{
key: 'reason',
header: 'توضیح',
render: (r) => <span style={{ fontSize: 12, color: 'var(--text-2)' }}>{r.reason}</span>,
},
];
return (
<div className="fade-in">
<PageHeader
title={policy ? `آزمایش قانون: ${policy.name}` : 'آزمایش قانون'}
description="اجرای قانون روی نوبت‌های واقعی گذشته. هیچ چیزی ثبت یا تغییر نمی‌شود."
backTo="/admin/policies"
action={
<button
type="button"
className="btn primary sm"
disabled={run.isPending}
onClick={async () => setLatest((await run.mutateAsync(50)).data)}
>
<BeakerIcon style={{ width: 15 }} /> اجرای آزمایش
</button>
}
/>
{current && severity && (
<div className="card" style={{ marginBottom: 16, display: 'flex', flexDirection: 'column', gap: 8 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' }}>
<span style={{ fontSize: 14 }}>
نمونه: {current.sample_size} نوبت · تحت تأثیر: {current.affected_count} نوبت (
{current.affected_percent}٪)
</span>
<span className={severity.className}>
<span className="bdot" /> شدت: {severity.label}
</span>
<span style={{ fontSize: 12, color: 'var(--text-3)' }}>
نسخهٔ آزمایش‌شده: {current.policy_version}
</span>
</div>
<p style={{ margin: 0, fontSize: 13, color: 'var(--text-2)' }}>
{current.warning ?? severity.note}
</p>
{!isCurrentVersion && (
<p style={{ margin: 0, fontSize: 13, color: 'var(--warning)' }}>
این گزارش برای نسخهٔ {current.policy_version} است و قانون اکنون نسخهٔ{' '}
{policy?.version} است. برای فعال‌سازی، دوباره آزمایش کنید.
</p>
)}
{canManage && (
<div style={{ display: 'flex', gap: 8, marginTop: 4 }}>
<button
type="button"
className="btn primary sm"
disabled={!isCurrentVersion || activate.isPending || policy?.active}
onClick={() => {
// شدت زیاد یعنی احتمالاً قانون اشتباه نوشته شده؛ یک قدم مکث لازم است.
if (current.severity === 'high') {
setConfirming(true);
return;
}
activate.mutate(policyUuid!, { onSuccess: () => navigate('/admin/policies') });
}}
>
{policy?.active ? 'قانون فعال است' : 'فعال‌سازی قانون'}
</button>
<button
type="button"
className="btn secondary sm"
onClick={() => navigate('/admin/policies')}
>
بازگشت به فهرست
</button>
</div>
)}
</div>
)}
<ConfirmDialog
open={confirming}
title="فعال‌سازی قانون پرتأثیر"
message={`این قانون ${current?.affected_percent ?? 0}٪ نوبت‌های نمونه را تغییر می‌دهد. مطمئنید می‌خواهید فعالش کنید؟`}
confirmLabel="بله، فعال کن"
danger
loading={activate.isPending}
onCancel={() => setConfirming(false)}
onConfirm={() => {
setConfirming(false);
activate.mutate(policyUuid!, { onSuccess: () => navigate('/admin/policies') });
}}
/>
{policy?.versions && policy.versions.length > 1 && (
<PolicyVersionDiff versions={policy.versions as never} />
)}
<div style={{ overflowX: 'auto' }}>
<DataTable
columns={columns}
data={current?.rows ?? []}
loading={loading || run.isPending}
emptyMessage={
current
? 'هیچ نوبتی از نمونه با این قانون تغییر نمی‌کرد'
: 'برای دیدن اثر این قانون، یک آزمایش اجرا کنید'
}
/>
</div>
</div>
);
}