Files
clinicpro/assets/admin/components/PatientCaseBanner.tsx
T
hamedandClaude Opus 4.8 be4d63744d feat(patients): port tauri /files-services detail pixel-for-pixel
Rebuild the patient case-file (/admin/patients/:uuid) to match tauri
files-services:
- breadcrumb (پرونده > name) + 140px patient banner (name + completion chip,
  file number, tag dots, phone/date rows, next appointment, یادداشت button)
  ported from BreadcrumbHeader + FileServicesHeader.
- services tab: replaced the plain list with the ServiceCard «مراجعه» grid —
  each card = a session (visit + performed services): success icon, services
  subtitle, doctor/date/notes rows, cost + remaining debt, تکمیل پرداخت
  (settles the session) / مشاهده فاکتور.
- مشاهده فاکتور opens InvoiceSummaryModal (خلاصه فاکتور tables) fed by the real
  invoice (GET /billing/invoices/{uuid}).
- tab bar now uses the tauri custom SVG icons.
- 20+ SVGs ported verbatim into components/icons/FilesServiceIcons.tsx; new
  components SessionServiceCard, PatientCaseBanner, InvoiceSummaryModal.

Frontend only — the sessions endpoint already returns invoice_uuid /
patient_debt_rials / is_paid. Tests updated (9 green).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 10:36:47 +03:30

101 lines
5.0 KiB
TypeScript

import { Link } from 'react-router-dom';
import { formatDate } from '../lib/utils';
import {
ArrowLeftPH, ArrowLeftD, FilesServicePhone, FilesServiceCalendar,
FilesServiceNotification, FilesServiceMessage,
} from './icons/FilesServiceIcons';
interface Tag { uuid: string; name: string; color: string }
/** پرونده > {name} breadcrumb, ported from tauri BreadcrumbHeader. */
export function Breadcrumb({ name, backTo }: { name: string; backTo: string }) {
return (
<div style={{ display: 'flex', alignItems: 'center', gap: 8, color: '#6B7280', fontSize: 12, marginBottom: 14 }}>
<Link to={backTo} className="bg-white dark:bg-[#222433]" style={{ display: 'flex', alignItems: 'center', gap: 4, padding: '6px 8px', borderRadius: 12, cursor: 'pointer', color: '#6B7280', textDecoration: 'none' }}>
<ArrowLeftPH />
<span>بازگشت</span>
</Link>
<ArrowLeftD />
<span>پرونده</span>
<ArrowLeftD />
<span className="dark:text-[#D7D8ED]" style={{ color: '#111827' }}>{name}</span>
</div>
);
}
function TagDots({ tags }: { tags?: Tag[] }) {
if (!tags || tags.length === 0) return <span style={{ fontSize: 13, color: 'var(--text-3)' }}></span>;
return (
<span style={{ display: 'inline-flex', alignItems: 'center' }}>
{tags.slice(0, 4).map((t, i) => (
<span key={t.uuid} title={t.name} style={{ width: 16, height: 16, borderRadius: '50%', background: t.color, border: '2px solid var(--surface)', marginInlineStart: i === 0 ? 0 : -6 }} />
))}
</span>
);
}
const InfoLine = ({ icon, label, value }: { icon: React.ReactNode; label: string; value: string }) => (
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
{icon}
<span className="dark:text-[#A1A1A1]" style={{ fontSize: 12, color: '#2f2f2f' }}>{label}</span>
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 14, fontWeight: 500, color: '#2f2f2f' }} dir="ltr">{value}</span>
</div>
);
/**
* The patient case-file banner — ported pixel-for-pixel from tauri
* FileServicesHeader (name + status chip, file number, tags, contact/date,
* next appointment, یادداشت button).
*/
export default function PatientCaseBanner({ name, recordNumber, mobile, createdAt, tags, nextAppointment, hasDebt, onAddNote }: {
name: string;
recordNumber?: string | null;
mobile?: string | null;
createdAt?: number;
tags?: Tag[];
nextAppointment?: number | null;
hasDebt?: boolean;
onAddNote: () => void;
}) {
const complete = !hasDebt;
return (
<div
className="bg-white dark:bg-[#222433] border border-[#EDEDED] dark:border-[#35343D]"
style={{ borderRadius: 16, padding: '14px 20px', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 24, marginBottom: 16, minHeight: 140, flexWrap: 'wrap' }}
>
{/* right — name + tags */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' }}>
<span className="dark:text-[#D7D8ED]" style={{ fontWeight: 700, fontSize: 20, color: '#111827' }}>{name}</span>
<span style={{
height: 29, minWidth: 95, borderRadius: 8, padding: '0 10px', fontSize: 13, display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
background: complete ? '#ECFDF3' : 'rgba(255,192,81,0.15)', color: complete ? '#3C9A4F' : '#f59e0b',
}}>{complete ? 'تکمیل شده' : 'تکمیل نشده'}</span>
</div>
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 16, color: '#525252' }}>شماره پرونده: {recordNumber || '—'}</span>
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 16 }}>
<span className="dark:text-[#A1A1A1]" style={{ fontSize: 16, color: '#6B7280' }}>برچسب ها:</span>
<TagDots tags={tags} />
</div>
</div>
{/* middle — contact + file date */}
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: 28 }}>
<InfoLine icon={<FilesServicePhone />} label="شماره تماس:" value={mobile || '—'} />
<InfoLine icon={<FilesServiceCalendar />} label="تاریخ تشکیل پرونده:" value={createdAt ? formatDate(createdAt) : '—'} />
</div>
{/* left — next appointment + note */}
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 28 }}>
<InfoLine icon={<FilesServiceNotification />} label="نوبت بعدی:" value={nextAppointment ? formatDate(nextAppointment) : '—'} />
<button
type="button" onClick={onAddNote}
style={{ display: 'inline-flex', alignItems: 'center', gap: 6, background: '#f17732', color: '#fff', border: 'none', borderRadius: 10, padding: '0 18px', height: 36, fontSize: 13, fontWeight: 600, cursor: 'pointer' }}
>
<FilesServiceMessage /> یادداشت
</button>
</div>
</div>
);
}