From 0d943d8fc57a9d9a68d5bd7831549c98e69d531f Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Fri, 17 Jul 2026 14:48:08 +0330 Subject: [PATCH] feat(admin): session action menu (view invoice + archive) and archive filter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../admin/components/SessionServiceCard.tsx | 41 +++++++++++++++++-- assets/admin/pages/PatientDetailPage.tsx | 29 +++++++++---- 2 files changed, 58 insertions(+), 12 deletions(-) diff --git a/assets/admin/components/SessionServiceCard.tsx b/assets/admin/components/SessionServiceCard.tsx index 391ef77f..46cfd0ff 100644 --- a/assets/admin/components/SessionServiceCard.tsx +++ b/assets/admin/components/SessionServiceCard.tsx @@ -1,4 +1,4 @@ -import type { CSSProperties } from 'react'; +import { useEffect, useRef, useState, type CSSProperties } from 'react'; import { formatDate, formatRial } from '../lib/utils'; import { FilesServiceSuccess, FilesServiceMore } from './icons/FilesServiceIcons'; @@ -23,6 +23,7 @@ export interface SessionCardData { is_paid?: boolean; invoice_uuid?: string | null; notes?: string | null; + archived?: boolean; created_at?: number; // تسویه چندتکه + تخفیف (backend session settlement fields) discount_type?: 'percent' | 'fixed' | null; @@ -49,16 +50,26 @@ function Row({ label, value, valueStyle }: { label: string; value: string; value * pixel-for-pixel from tauri files/services/ServiceCard. Paid cards show * «مشاهده فاکتور», unpaid ones «تکمیل پرداخت». */ -export default function SessionServiceCard({ session, onSettle, onViewInvoice, settling, issuing }: { +export default function SessionServiceCard({ session, onSettle, onViewInvoice, onArchive, settling, issuing }: { session: SessionCardData; onSettle: (uuid: string) => void; /** کل session پاس می‌شود؛ اگر invoice_uuid نداشت، caller فاکتور را می‌سازد. */ onViewInvoice: (session: SessionCardData) => void; + /** آرشیو/خروج از آرشیو مراجعه. */ + onArchive?: (session: SessionCardData, archived: boolean) => void; settling?: boolean; /** true وقتی صدور فاکتور همین لحظه در جریان است (دکمه قفل می‌شود). */ issuing?: boolean; }) { const paid = !!session.is_paid; + const [menuOpen, setMenuOpen] = useState(false); + const menuRef = useRef(null); + useEffect(() => { + if (!menuOpen) return; + const onDoc = (e: MouseEvent) => { if (menuRef.current && !menuRef.current.contains(e.target as Node)) setMenuOpen(false); }; + document.addEventListener('mousedown', onDoc); + return () => document.removeEventListener('mousedown', onDoc); + }, [menuOpen]); const names = (session.services ?? []).map((s) => s.service_name || s.name).filter(Boolean) as string[]; if ((session.visit_price_rials ?? 0) > 0) names.unshift('ویزیت'); // A «مراجعه» = the visit plus the services performed in it. @@ -75,11 +86,33 @@ export default function SessionServiceCard({ session, onSettle, onViewInvoice, s
- {title} + + {title} + {session.archived && آرشیو} + {subtitle}
- +
+ + {menuOpen && ( +
+ + {onArchive && ( + + )} +
+ )} +
diff --git a/assets/admin/pages/PatientDetailPage.tsx b/assets/admin/pages/PatientDetailPage.tsx index f3a85a0e..df45b912 100644 --- a/assets/admin/pages/PatientDetailPage.tsx +++ b/assets/admin/pages/PatientDetailPage.tsx @@ -81,6 +81,7 @@ export default function PatientDetailPage() { const qc = useQueryClient(); const nav = useNavigate(); const [invoiceUuid, setInvoiceUuid] = useState(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(null); @@ -138,8 +149,8 @@ export default function PatientDetailPage() { // sessions + appointments load eagerly so the banner (debt / next visit) is ready. const sessionsQ = useQuery>({ - 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>({ @@ -212,11 +223,13 @@ export default function PatientDetailPage() {
) : tab === 'services' ? (
- {/* filter + new-service (tauri ServiceCardsSection header) */} -
- + {/* filter (همه/فعال/آرشیو) + new-service */} +
+
+ + + +
سرویس جدید @@ -228,7 +241,7 @@ export default function PatientDetailPage() { ) : (
{sessions.map((s) => ( - nav(`/admin/patients/${uuid}/session/${u}/pay`)} onViewInvoice={viewInvoice} issuing={issueInvoiceMut.isPending} /> + nav(`/admin/patients/${uuid}/session/${u}/pay`)} onViewInvoice={viewInvoice} onArchive={(sess, archived) => archiveMut.mutate({ sessionUuid: sess.uuid, archived })} issuing={issueInvoiceMut.isPending} /> ))}
)}