From 633c939560efa1abfd65f6d665efbb49e96c6f2e Mon Sep 17 00:00:00 2001 From: Admin Date: Tue, 19 May 2026 23:43:43 +0330 Subject: [PATCH] feature: treatment frontend wired with the newly implemented backend. no mocked data no more. --- .../modules/treatments/treatments.service.ts | 20 +- .../src/components/treatment/caseSendLabel.ts | 40 ++ .../treatment/CaseLatestAttachmentPreview.tsx | 72 ++ .../components/ui/treatment/CaseSentLabel.tsx | 31 + .../components/ui/treatment/FdiToothChart.tsx | 82 ++- .../ui/treatment/PastTreatmentsPanel.tsx | 123 ++-- .../ui/treatment/TreatmentCasesEditor.tsx | 296 ++++++++ .../TreatmentLatestAttachmentPreview.tsx | 104 +++ .../ui/treatment/TreatmentPreviewCard.tsx | 56 ++ .../ui/treatment/TreatmentPreviewDialog.tsx | 235 +++++++ .../ui/treatment/TreatmentWorkspace.tsx | 650 ++++++------------ frontend/src/lib/api/treatments.ts | 10 + frontend/src/types/treatment.ts | 8 + 13 files changed, 1199 insertions(+), 528 deletions(-) create mode 100644 frontend/src/components/treatment/caseSendLabel.ts create mode 100644 frontend/src/components/ui/treatment/CaseLatestAttachmentPreview.tsx create mode 100644 frontend/src/components/ui/treatment/CaseSentLabel.tsx create mode 100644 frontend/src/components/ui/treatment/TreatmentCasesEditor.tsx create mode 100644 frontend/src/components/ui/treatment/TreatmentLatestAttachmentPreview.tsx create mode 100644 frontend/src/components/ui/treatment/TreatmentPreviewCard.tsx create mode 100644 frontend/src/components/ui/treatment/TreatmentPreviewDialog.tsx diff --git a/backend/src/modules/treatments/treatments.service.ts b/backend/src/modules/treatments/treatments.service.ts index 954279b..e44fb96 100644 --- a/backend/src/modules/treatments/treatments.service.ts +++ b/backend/src/modules/treatments/treatments.service.ts @@ -22,7 +22,10 @@ const treatmentInclude = { orderBy: [{ sortOrder: 'asc' as const }], include: { attachments: { orderBy: [{ createdAt: 'asc' as const }] }, - sends: { orderBy: [{ sentAt: 'asc' as const }] }, + sends: { + orderBy: [{ sentAt: 'asc' as const }], + include: { organization: { select: { id: true, name: true } } }, + }, }, }, }; @@ -332,7 +335,10 @@ export class TreatmentsService { where: { id: caseId }, include: { attachments: { orderBy: [{ createdAt: 'asc' }] }, - sends: { orderBy: [{ sentAt: 'asc' }] }, + sends: { + orderBy: [{ sentAt: 'asc' }], + include: { organization: { select: { id: true, name: true } } }, + }, }, }); @@ -459,7 +465,7 @@ export class TreatmentsService { mimeType: string; sizeBytes: number; }>; - sends: Array<{ organizationId: string; sentAt: Date }>; + sends: Array<{ organizationId: string; sentAt: Date; organization: { id: string; name: string } }>; }>; }) { const documents = treatment.cases.flatMap((c) => @@ -491,7 +497,7 @@ export class TreatmentsService { mimeType: string; sizeBytes: number; }>; - sends?: Array<{ organizationId: string; sentAt: Date }>; + sends?: Array<{ organizationId: string; sentAt: Date; organization?: { id: string; name: string } }>; }) { return { id: c.id, @@ -501,6 +507,12 @@ export class TreatmentsService { notes: c.comment ?? null, sentAt: c.sentAt?.toISOString() ?? null, sendToOrganizationIds: c.sends?.map((s) => s.organizationId) ?? [], + sends: + c.sends?.map((s) => ({ + organizationId: s.organizationId, + organizationName: s.organization?.name ?? 'Unknown organization', + sentAt: s.sentAt.toISOString(), + })) ?? [], attachmentMetas: (c.attachments ?? []).map((a) => this.mapAttachment(a)), }; } diff --git a/frontend/src/components/treatment/caseSendLabel.ts b/frontend/src/components/treatment/caseSendLabel.ts new file mode 100644 index 0000000..029dc3e --- /dev/null +++ b/frontend/src/components/treatment/caseSendLabel.ts @@ -0,0 +1,40 @@ +import type { LinkedOrganizationOption, TreatmentCaseSendInfo } from '@/types/treatment'; + +export function formatCaseSentLines( + sends: TreatmentCaseSendInfo[] | undefined, + fallback?: { + organizationIds: string[]; + sentAt: string | null; + orgs?: LinkedOrganizationOption[]; + }, +): string[] { + if (sends?.length) { + return sends.map((s) => { + const at = new Date(s.sentAt).toLocaleString(); + return `Sent to ${s.organizationName} at ${at}`; + }); + } + + if (fallback?.sentAt && fallback.organizationIds.length > 0) { + const at = new Date(fallback.sentAt).toLocaleString(); + const nameById = new Map(fallback.orgs?.map((o) => [o.id, o.name]) ?? []); + return fallback.organizationIds.map((id) => { + const name = nameById.get(id) ?? 'organization'; + return `Sent to ${name} at ${at}`; + }); + } + + return []; +} + +export function formatCaseSentSummary( + sends: TreatmentCaseSendInfo[] | undefined, + fallback?: { + organizationIds: string[]; + sentAt: string | null; + orgs?: LinkedOrganizationOption[]; + }, +): string | null { + const lines = formatCaseSentLines(sends, fallback); + return lines.length > 0 ? lines.join(' · ') : null; +} diff --git a/frontend/src/components/ui/treatment/CaseLatestAttachmentPreview.tsx b/frontend/src/components/ui/treatment/CaseLatestAttachmentPreview.tsx new file mode 100644 index 0000000..c9f363d --- /dev/null +++ b/frontend/src/components/ui/treatment/CaseLatestAttachmentPreview.tsx @@ -0,0 +1,72 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { FileText } from 'lucide-react'; +import { treatmentsApi } from '@/lib/api/treatments'; +import type { TreatmentAttachmentMeta } from '@/types/treatment'; + +interface CaseLatestAttachmentPreviewProps { + attachment: TreatmentAttachmentMeta; + className?: string; +} + +export function CaseLatestAttachmentPreview({ + attachment, + className = 'aspect-square w-full max-w-[11rem]', +}: CaseLatestAttachmentPreviewProps) { + const [url, setUrl] = useState(null); + const [failed, setFailed] = useState(false); + + useEffect(() => { + let cancelled = false; + let objectUrl: string | null = null; + + void (async () => { + try { + const blob = await treatmentsApi.getAttachmentFileBlob(attachment.id); + if (cancelled) return; + objectUrl = URL.createObjectURL(blob); + setUrl(objectUrl); + setFailed(false); + } catch { + if (!cancelled) setFailed(true); + } + })(); + + return () => { + cancelled = true; + if (objectUrl) URL.revokeObjectURL(objectUrl); + }; + }, [attachment.id]); + + const isImage = attachment.mimeType.startsWith('image/'); + const isPdf = attachment.mimeType === 'application/pdf'; + + return ( +
+ {url && isImage ? ( + {attachment.fileName} + ) : url && isPdf ? ( +