Files
clinicpro/assets/admin/components/PolicyVersionDiff.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

98 lines
3.8 KiB
TypeScript

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>
);
}