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:
@@ -8,6 +8,8 @@ import { formatDate, formatNumber } from '../lib/utils';
|
||||
import { usePermissions } from '../hooks/usePermissions';
|
||||
import { useBranches } from '../hooks/useBranches';
|
||||
import { useNextSlotSuggestion, useTreatmentCourse } from '../hooks/useCourses';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { api, type ApiResponse } from '../lib/api';
|
||||
import type { CourseSessionRow } from '../types';
|
||||
|
||||
const SESSION_STATUS: Record<CourseSessionRow['status'], { label: string; className: string }> = {
|
||||
@@ -25,17 +27,27 @@ const SESSION_STATUS: Record<CourseSessionRow['status'], { label: string; classN
|
||||
*/
|
||||
export default function TreatmentCoursePage() {
|
||||
const { courseUuid } = useParams<{ courseUuid: string }>();
|
||||
const { course, loading, abandon } = useTreatmentCourse(courseUuid);
|
||||
const { course, loading, abandon, bookAll } = useTreatmentCourse(courseUuid);
|
||||
const { branches } = useBranches();
|
||||
const { can } = usePermissions();
|
||||
const canManage = can('appointment_settings', 'update');
|
||||
|
||||
const [branchUuid, setBranchUuid] = useState('');
|
||||
const [doctorUuid, setDoctorUuid] = useState('');
|
||||
const [abandoning, setAbandoning] = useState(false);
|
||||
const [reason, setReason] = useState('');
|
||||
|
||||
const { suggestion } = useNextSlotSuggestion(courseUuid, branchUuid || undefined);
|
||||
|
||||
/** همان اندپوینتی که صفحهٔ رزرو منبعمحور استفاده میکند — منشی فقط پزشکان خودش را میبیند. */
|
||||
const doctorsQuery = useQuery<ApiResponse<{ data: { uuid: string; name: string }[] }>>({
|
||||
queryKey: ['booking-doctors'],
|
||||
queryFn: () => api.get('/api/v1/my/clinic-doctors'),
|
||||
staleTime: 60_000,
|
||||
});
|
||||
|
||||
const doctors = doctorsQuery.data?.data?.data ?? [];
|
||||
|
||||
/**
|
||||
* دورهای که وسطش لغو شده، بیصدا کِش میآید: جلسه به «برنامهریزیشده» برمیگردد و
|
||||
* هیچکس خبردار نمیشود.
|
||||
@@ -173,6 +185,25 @@ export default function TreatmentCoursePage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* پکیجی که کفاف جلسات باقیمانده را نمیدهد، دوره را باطل نمیکند — بقیهاش
|
||||
نقدی میشود. ولی باید قبل از جلسهٔ ششم دانسته شود، نه سرِ آن. */}
|
||||
{(course.package_shortfall ?? 0) > 0 && (
|
||||
<span
|
||||
style={{
|
||||
fontSize: 13,
|
||||
lineHeight: 1.8,
|
||||
color: 'var(--warning)',
|
||||
background: 'var(--warning-bg)',
|
||||
borderRadius: 'var(--r-sm)',
|
||||
padding: '8px 10px',
|
||||
}}
|
||||
>
|
||||
اعتبار پکیج برای {formatNumber(course.package_shortfall ?? 0)} جلسه کم میآید
|
||||
(مانده: {formatNumber(course.package_balance ?? 0)}). بقیهٔ جلسات با قیمت
|
||||
عادی حساب میشوند.
|
||||
</span>
|
||||
)}
|
||||
|
||||
{overdue !== null && (
|
||||
<span
|
||||
style={{
|
||||
@@ -204,6 +235,36 @@ export default function TreatmentCoursePage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* رزرو گروهی پزشک لازم دارد؛ خودِ دوره پزشکی ندارد چون هر جلسه میتواند
|
||||
با پزشک دیگری باشد. */}
|
||||
{canManage && course.status === 'active' && (
|
||||
<div className="field-block" style={{ minWidth: 220, margin: 0 }}>
|
||||
<label>پزشک برای رزرو گروهی</label>
|
||||
<SearchableSelect
|
||||
value={doctorUuid}
|
||||
onChange={(v) => setDoctorUuid(String(v ?? ''))}
|
||||
options={doctors.map((d) => ({ value: d.uuid, label: d.name }))}
|
||||
placeholder="انتخاب پزشک"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{canManage && course.status === 'active' && (
|
||||
<button
|
||||
type="button"
|
||||
className="btn primary sm"
|
||||
disabled={branchUuid === '' || doctorUuid === '' || bookAll.isPending}
|
||||
title={
|
||||
branchUuid === '' || doctorUuid === ''
|
||||
? 'شعبه و پزشک را انتخاب کنید'
|
||||
: 'همهٔ جلسات برنامهریزیشده یکجا رزرو میشوند — همه یا هیچ'
|
||||
}
|
||||
onClick={() => bookAll.mutate({ branch_uuid: branchUuid, doctor_uuid: doctorUuid })}
|
||||
>
|
||||
رزرو همهٔ جلسات
|
||||
</button>
|
||||
)}
|
||||
|
||||
{canManage && course.status === 'active' && (
|
||||
<button type="button" className="btn secondary sm" onClick={() => setAbandoning(true)}>
|
||||
رهاکردن دوره
|
||||
|
||||
Reference in New Issue
Block a user