feat(admin): session action menu (view invoice + archive) and archive filter

SessionServiceCard's '...' icon becomes a real dropdown with «مشاهده فاکتور»
and «آرشیو»/«خروج از آرشیو» (click-outside close), plus an «آرشیو» badge on
archived cards. PatientDetailPage adds a فعال‌ها/همه/آرشیو segmented filter on
the services tab (default active), threads the filter into the sessions query,
and wires the archive action to PATCH /session/{uuid} { archived }.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
hamed
2026-07-17 14:48:08 +03:30
co-authored by Claude Fable 5
parent 666833fc69
commit 0d943d8fc5
2 changed files with 58 additions and 12 deletions
+21 -8
View File
@@ -81,6 +81,7 @@ export default function PatientDetailPage() {
const qc = useQueryClient();
const nav = useNavigate();
const [invoiceUuid, setInvoiceUuid] = useState<string | null>(null);
const [sessionFilter, setSessionFilter] = useState<'active' | 'all' | 'archived'>('active');
// «مشاهده فاکتور»: اگر session هنوز فاکتور ندارد، اول صادر می‌شود (idempotent).
const issueInvoiceMut = useIssueInvoice(uuid);
@@ -92,6 +93,16 @@ export default function PatientDetailPage() {
});
};
const archiveMut = useMutation({
mutationFn: ({ sessionUuid, archived }: { sessionUuid: string; archived: boolean }) =>
api.patch(`/api/v1/session/${sessionUuid}`, { archived }),
onSuccess: (_r, v) => {
qc.invalidateQueries({ queryKey: ['patient-sessions', uuid] });
toast.success(v.archived ? 'مراجعه آرشیو شد' : 'مراجعه از آرشیو خارج شد');
},
onError: (e: any) => toast.error(e?.message || 'خطا در آرشیو'),
});
// ── فرم «اطلاعات پرونده» (اینلاین، معادل tauri FileInfoSection) ──────────────
// استان/شهر (Location) و بیمهٔ پایه (insurance-pricing) برای گزینه‌های فرم.
const [editProvinceId, setEditProvinceId] = useState<number | null>(null);
@@ -138,8 +149,8 @@ export default function PatientDetailPage() {
// sessions + appointments load eagerly so the banner (debt / next visit) is ready.
const sessionsQ = useQuery<ApiResponse<SessionCardData[]>>({
queryKey: ['patient-sessions', uuid],
queryFn: () => api.get(`/api/v1/patient/${uuid}/sessions`),
queryKey: ['patient-sessions', uuid, sessionFilter],
queryFn: () => api.get(`/api/v1/patient/${uuid}/sessions?filter=${sessionFilter}`),
enabled: !!uuid,
});
const appointmentsQ = useQuery<ApiResponse<any[]>>({
@@ -212,11 +223,13 @@ export default function PatientDetailPage() {
</div>
) : tab === 'services' ? (
<div>
{/* filter + new-service (tauri ServiceCardsSection header) */}
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8, marginBottom: 16 }}>
<button type="button" aria-label="فیلتر" className="flex items-center justify-center rounded-[4px] cursor-pointer" style={{ width: 48, height: 48, border: '1px solid var(--border)', background: 'transparent' }}>
<TurnsFilter color="#5559ce" />
</button>
{/* filter (همه/فعال/آرشیو) + new-service */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 8, marginBottom: 16, flexWrap: 'wrap' }}>
<div className="seg">
<button className={sessionFilter === 'active' ? 'on' : ''} onClick={() => setSessionFilter('active')}>فعالها</button>
<button className={sessionFilter === 'all' ? 'on' : ''} onClick={() => setSessionFilter('all')}>همه</button>
<button className={sessionFilter === 'archived' ? 'on' : ''} onClick={() => setSessionFilter('archived')}>آرشیو</button>
</div>
<Link to={`/admin/patients/${uuid}/session/new`} className="flex items-center justify-center gap-2 rounded-[8px]" style={{ height: 48, minWidth: 137, background: '#5559ce', color: '#fff', textDecoration: 'none', padding: '0 16px', fontSize: 14 }}>
<AddTurn color="#fff" /> سرویس جدید
</Link>
@@ -228,7 +241,7 @@ export default function PatientDetailPage() {
) : (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(240px, 1fr))', columnGap: 6, rowGap: 10, alignItems: 'stretch' }}>
{sessions.map((s) => (
<SessionServiceCard key={s.uuid} session={s} onSettle={(u) => nav(`/admin/patients/${uuid}/session/${u}/pay`)} onViewInvoice={viewInvoice} issuing={issueInvoiceMut.isPending} />
<SessionServiceCard key={s.uuid} session={s} onSettle={(u) => nav(`/admin/patients/${uuid}/session/${u}/pay`)} onViewInvoice={viewInvoice} onArchive={(sess, archived) => archiveMut.mutate({ sessionUuid: sess.uuid, archived })} issuing={issueInvoiceMut.isPending} />
))}
</div>
)}