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>
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
import React from 'react';
|
||||
import { formatDate } from '../lib/utils';
|
||||
|
||||
interface VersionRow {
|
||||
version: number;
|
||||
created_at: number;
|
||||
snapshot: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/** فقط فیلدهایی که تغییرشان معنا دارد — `updated_at` و نسخه خودشان همیشه فرق دارند. */
|
||||
const WATCHED: Array<{ key: string; label: string }> = [
|
||||
{ key: 'name', label: 'نام' },
|
||||
{ key: 'priority', label: 'اولویت' },
|
||||
{ key: 'active', label: 'فعال' },
|
||||
{ key: 'condition', label: 'شرط' },
|
||||
{ key: 'effects', label: 'اثرها' },
|
||||
{ key: 'valid_from', label: 'شروع اعتبار' },
|
||||
{ key: 'valid_to', label: 'پایان اعتبار' },
|
||||
{ key: 'service_uuid', label: 'سرویس' },
|
||||
{ key: 'address_uuid', label: 'شعبه' },
|
||||
];
|
||||
|
||||
function show(value: unknown): string {
|
||||
if (value === null || value === undefined) return '—';
|
||||
if (typeof value === 'boolean') return value ? 'بله' : 'خیر';
|
||||
if (typeof value === 'object') return JSON.stringify(value, null, 0);
|
||||
|
||||
return String(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* تفاوت هر نسخه با نسخهٔ پیش از خودش.
|
||||
*
|
||||
* فهرست کامل اثرها روی هر نسخه، سؤال واقعی را جواب نمیدهد: «چه چیزی عوض شد؟». وقتی
|
||||
* نوبتی به نسخهٔ ۳ ارجاع میدهد و کسی میپرسد چرا قیمتش فرق دارد، همین ستون جواب است.
|
||||
*/
|
||||
export default function PolicyVersionDiff({ versions }: { versions: VersionRow[] }) {
|
||||
if (!versions || versions.length === 0) return null;
|
||||
|
||||
const ordered = [...versions].sort((a, b) => a.version - b.version);
|
||||
|
||||
return (
|
||||
<div className="card card-pad" style={{ marginBottom: 16 }}>
|
||||
<h3 style={{ fontSize: 14, margin: '0 0 10px' }}>تاریخچهٔ نسخهها</h3>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
{ordered.map((v, i) => {
|
||||
const previous = i === 0 ? null : ordered[i - 1].snapshot;
|
||||
|
||||
const changes = previous
|
||||
? WATCHED.filter((f) => show(v.snapshot[f.key]) !== show(previous[f.key]))
|
||||
: [];
|
||||
|
||||
return (
|
||||
<div
|
||||
key={v.version}
|
||||
style={{ borderTop: i === 0 ? undefined : '1px solid var(--border)', paddingTop: i === 0 ? 0 : 10 }}
|
||||
>
|
||||
<div style={{ display: 'flex', gap: 10, fontSize: 12, alignItems: 'center' }}>
|
||||
<span style={{ fontWeight: 600, minWidth: 60 }}>نسخهٔ {v.version}</span>
|
||||
<span style={{ color: 'var(--text-3)' }}>{formatDate(v.created_at)}</span>
|
||||
{previous === null && (
|
||||
<span style={{ color: 'var(--text-3)' }}>نسخهٔ نخست</span>
|
||||
)}
|
||||
{previous !== null && changes.length === 0 && (
|
||||
<span style={{ color: 'var(--text-3)' }}>بدون تغییرِ معنادار</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{changes.map((f) => (
|
||||
<div
|
||||
key={f.key}
|
||||
style={{ display: 'flex', gap: 8, fontSize: 12, marginTop: 6, flexWrap: 'wrap' }}
|
||||
>
|
||||
<span style={{ minWidth: 90, color: 'var(--text-2)' }}>{f.label}</span>
|
||||
<span
|
||||
style={{
|
||||
color: 'var(--danger)',
|
||||
textDecoration: 'line-through',
|
||||
wordBreak: 'break-all',
|
||||
}}
|
||||
>
|
||||
{show(previous?.[f.key])}
|
||||
</span>
|
||||
<span style={{ color: 'var(--text-3)' }}>←</span>
|
||||
<span style={{ color: 'var(--success)', wordBreak: 'break-all' }}>
|
||||
{show(v.snapshot[f.key])}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user