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:
@@ -3,6 +3,9 @@ import { useUrlState } from '../hooks/useUrlState';
|
||||
import PageHeader from '../components/ui/PageHeader';
|
||||
import DataTable, { type Column } from '../components/ui/DataTable';
|
||||
import SearchableSelect from '../components/ui/SearchableSelect';
|
||||
import PersianDateInput from '../components/ui/PersianDateInput';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { isoToUnix, unixToIso } from '../lib/utils';
|
||||
import { useBranches } from '../hooks/useBranches';
|
||||
import { useResourceUtilization } from '../hooks/useReports';
|
||||
import type { UtilizationRow } from '../types';
|
||||
@@ -11,6 +14,7 @@ const RANGES = [
|
||||
{ value: '7', label: 'هفتهٔ گذشته' },
|
||||
{ value: '30', label: 'ماه گذشته' },
|
||||
{ value: '90', label: 'سه ماه گذشته' },
|
||||
{ value: 'custom', label: 'بازهٔ دلخواه' },
|
||||
];
|
||||
|
||||
function percent(value: number | null): string {
|
||||
@@ -27,17 +31,28 @@ export default function ResourceUtilizationPage() {
|
||||
const { branches } = useBranches();
|
||||
// بازه و شعبه در URL مینشینند نه در state: بازگشت از صفحهٔ منبع باید همان گزارش را
|
||||
// برگرداند، و لینکِ گزارش باید همان چیزی را نشان بدهد که فرستنده دیده.
|
||||
const [urlState, setUrlState] = useUrlState({ branch: '', days: '7' });
|
||||
const [urlState, setUrlState] = useUrlState({ branch: '', days: '7', from: '', to: '' });
|
||||
const branchUuid = urlState.branch;
|
||||
const days = urlState.days;
|
||||
|
||||
const setBranchUuid = (v: string) => setUrlState({ branch: v });
|
||||
const setDays = (v: string) => setUrlState({ days: v });
|
||||
|
||||
/**
|
||||
* بازهٔ آماده برای حالت عادی، بازهٔ دلخواه برای وقتی که کاربر دقیقاً میداند چه
|
||||
* میخواهد — مثلاً مقایسهٔ دو ماه مشخص با هم.
|
||||
*/
|
||||
const range = useMemo(() => {
|
||||
if (days === 'custom') {
|
||||
return {
|
||||
from: isoToUnix(urlState.from) ?? Math.floor(Date.now() / 1000) - 7 * 86400,
|
||||
to: isoToUnix(urlState.to) ?? Math.floor(Date.now() / 1000),
|
||||
};
|
||||
}
|
||||
|
||||
const to = Math.floor(Date.now() / 1000);
|
||||
return { from: to - Number(days) * 86400, to };
|
||||
}, [days]);
|
||||
}, [days, urlState.from, urlState.to]);
|
||||
|
||||
const { rows, loading } = useResourceUtilization(branchUuid || undefined, range.from, range.to);
|
||||
|
||||
@@ -70,14 +85,20 @@ export default function ResourceUtilizationPage() {
|
||||
{
|
||||
key: 'utilization',
|
||||
header: 'بهرهوری',
|
||||
render: (r) => (
|
||||
<span
|
||||
style={{ fontSize: 13 }}
|
||||
title={r.utilization === null ? 'برای این منبع تقویمی تعریف نشده است' : undefined}
|
||||
>
|
||||
{percent(r.utilization)}
|
||||
</span>
|
||||
),
|
||||
// `null` یعنی «تعریفنشده» نه «صفر»، و کار بعدی روشن است: تقویم منبع را بساز.
|
||||
// بدون لینک، کاربر عدد را میبیند و نمیداند کجا باید برود.
|
||||
render: (r) =>
|
||||
r.utilization === null ? (
|
||||
<Link
|
||||
to={`/admin/resources/${r.resource_uuid}/calendar`}
|
||||
style={{ fontSize: 12, color: 'var(--primary)' }}
|
||||
title="برای این منبع تقویمی تعریف نشده است"
|
||||
>
|
||||
تنظیم تقویم
|
||||
</Link>
|
||||
) : (
|
||||
<span style={{ fontSize: 13 }}>{percent(r.utilization)}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'active_ratio',
|
||||
@@ -126,6 +147,25 @@ export default function ResourceUtilizationPage() {
|
||||
<label>بازه</label>
|
||||
<SearchableSelect value={days} onChange={(v) => setDays(String(v ?? '7'))} options={RANGES} />
|
||||
</div>
|
||||
|
||||
{days === 'custom' && (
|
||||
<>
|
||||
<div className="field-block" style={{ minWidth: 170, margin: 0 }}>
|
||||
<label>از تاریخ</label>
|
||||
<PersianDateInput
|
||||
value={urlState.from || unixToIso(range.from)}
|
||||
onChange={(v) => setUrlState({ from: v })}
|
||||
/>
|
||||
</div>
|
||||
<div className="field-block" style={{ minWidth: 170, margin: 0 }}>
|
||||
<label>تا تاریخ</label>
|
||||
<PersianDateInput
|
||||
value={urlState.to || unixToIso(range.to)}
|
||||
onChange={(v) => setUrlState({ to: v })}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p style={{ fontSize: 12, color: 'var(--text-3)', margin: '0 0 10px' }}>
|
||||
|
||||
Reference in New Issue
Block a user