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>
185 lines
7.5 KiB
TypeScript
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>
|
|
);
|
|
}
|