refactor(admin): collapse session cards in the course detail
A finished session prints three areas with their device readings, so a course with any history pushed the sessions that still need work off the screen. Each session is a collapsible card now, with the scannable facts kept in the head — number, status, date, staff, and an area count so opening is a decision rather than a guess. History starts collapsed; a session that still needs booking starts open, because its button is the reason it is on the page and should not sit behind an extra click. A session with neither areas nor an action has no body and renders as a plain row rather than an empty toggle. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import {
|
import {
|
||||||
ChevronLeftIcon, ChevronRightIcon, ClipboardDocumentListIcon,
|
ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon, ClipboardDocumentListIcon,
|
||||||
CpuChipIcon, MagnifyingGlassIcon, XMarkIcon,
|
CpuChipIcon, MagnifyingGlassIcon, XMarkIcon,
|
||||||
} from '@heroicons/react/24/outline';
|
} from '@heroicons/react/24/outline';
|
||||||
import { api } from '../../lib/api';
|
import { api } from '../../lib/api';
|
||||||
@@ -312,6 +312,14 @@ function SessionCard({ session: s, summary, resource, nationalCode }: {
|
|||||||
nationalCode: string | null;
|
nationalCode: string | null;
|
||||||
}) {
|
}) {
|
||||||
const [booking, setBooking] = useState(false);
|
const [booking, setBooking] = useState(false);
|
||||||
|
/**
|
||||||
|
* سابقه بسته میآید و کارِ پیشِ رو باز.
|
||||||
|
*
|
||||||
|
* جلسهٔ انجامشده سه ناحیه با خواندههایشان دارد و بازِ پیشفرض، فهرست را دفن
|
||||||
|
* میکند؛ ولی جلسهای که باید رزرو شود، دکمهاش دلیلِ وجودش است و پشت یک کلیک
|
||||||
|
* اضافه نباید بماند.
|
||||||
|
*/
|
||||||
|
const [open, setOpen] = useState(!SETTLED.includes(s.status));
|
||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
const clinicUuid = useClinicContext();
|
const clinicUuid = useClinicContext();
|
||||||
|
|
||||||
@@ -322,10 +330,12 @@ function SessionCard({ session: s, summary, resource, nationalCode }: {
|
|||||||
|
|
||||||
const bookable = !SETTLED.includes(s.status) && s.appointment === null && summary.status === 'active';
|
const bookable = !SETTLED.includes(s.status) && s.appointment === null && summary.status === 'active';
|
||||||
const areas = s.areas ?? [];
|
const areas = s.areas ?? [];
|
||||||
|
// جلسهای که نه ناحیهای ثبت کرده و نه کاری برای انجام دارد، بدنهای ندارد که باز
|
||||||
|
// شود؛ کلپسِ خالی فقط یک کلیک بینتیجه است.
|
||||||
|
const hasBody = areas.length > 0 || bookable;
|
||||||
|
|
||||||
return (
|
const head = (
|
||||||
<div className="card card-pad" style={{ display: 'grid', gap: 8 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap', width: '100%' }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
|
|
||||||
<strong style={{ fontSize: 13.5 }}>
|
<strong style={{ fontSize: 13.5 }}>
|
||||||
جلسهٔ {formatNumber(s.session_number)} از {formatNumber(s.total_sessions)}
|
جلسهٔ {formatNumber(s.session_number)} از {formatNumber(s.total_sessions)}
|
||||||
</strong>
|
</strong>
|
||||||
@@ -338,10 +348,39 @@ function SessionCard({ session: s, summary, resource, nationalCode }: {
|
|||||||
{s.performed_by && (
|
{s.performed_by && (
|
||||||
<span style={{ fontSize: 12.5, color: 'var(--text-3)' }}>پرسنل: {s.performed_by.name}</span>
|
<span style={{ fontSize: 12.5, color: 'var(--text-3)' }}>پرسنل: {s.performed_by.name}</span>
|
||||||
)}
|
)}
|
||||||
|
{/* شمارهٔ نواحی در سرِ بسته میماند تا باز کردن، حدس نباشد. */}
|
||||||
|
{areas.length > 0 && !open && (
|
||||||
|
<span style={{ fontSize: 12.5, color: 'var(--text-3)' }}>{formatNumber(areas.length)} ناحیه</span>
|
||||||
|
)}
|
||||||
|
{hasBody && (
|
||||||
|
<ChevronDownIcon
|
||||||
|
style={{
|
||||||
|
width: 16, height: 16, marginInlineStart: 'auto', flexShrink: 0, color: 'var(--text-3)',
|
||||||
|
transition: 'transform .2s var(--ease)', transform: open ? 'rotate(180deg)' : 'none',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="card card-pad" style={{ display: 'grid', gap: 8 }}>
|
||||||
|
{hasBody ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setOpen((o) => !o)}
|
||||||
|
aria-expanded={open}
|
||||||
|
style={{
|
||||||
|
display: 'flex', width: '100%', padding: 0, background: 'none', border: 'none',
|
||||||
|
cursor: 'pointer', font: 'inherit', color: 'var(--text)', textAlign: 'start',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{head}
|
||||||
|
</button>
|
||||||
|
) : head}
|
||||||
|
|
||||||
{/* آنچه واقعاً انجام شد: ناحیه، دستگاه و خواندههایش. */}
|
{/* آنچه واقعاً انجام شد: ناحیه، دستگاه و خواندههایش. */}
|
||||||
{areas.length > 0 && (
|
{open && areas.length > 0 && (
|
||||||
<div style={{ display: 'grid', gap: 6 }}>
|
<div style={{ display: 'grid', gap: 6 }}>
|
||||||
{areas.map((a) => (
|
{areas.map((a) => (
|
||||||
<div key={a.uuid} style={{ display: 'flex', gap: 10, flexWrap: 'wrap', fontSize: 12.5 }}>
|
<div key={a.uuid} style={{ display: 'flex', gap: 10, flexWrap: 'wrap', fontSize: 12.5 }}>
|
||||||
@@ -364,7 +403,7 @@ function SessionCard({ session: s, summary, resource, nationalCode }: {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{bookable && (
|
{open && bookable && (
|
||||||
/* بدون دستگاه، فرم سرویسی برای انتخاب ندارد؛ دکمهٔ خاموشِ بیتوضیح بدتر از
|
/* بدون دستگاه، فرم سرویسی برای انتخاب ندارد؛ دکمهٔ خاموشِ بیتوضیح بدتر از
|
||||||
نبودنش است، پس دلیلش نوشته میشود. */
|
نبودنش است، پس دلیلش نوشته میشود. */
|
||||||
resource === null ? (
|
resource === null ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user