improvement: pdf generation added to cases feature.
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useMemo, useState, type ReactNode } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { MessageSquare } from 'lucide-react';
|
||||
import { useLocale, useTranslations } from 'next-intl';
|
||||
import { Download, MessageSquare } from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/shared/Badge';
|
||||
import { Button } from '@/components/ui/shared/Button';
|
||||
import { Checkbox } from '@/components/ui/shared/Checkbox';
|
||||
import { Input } from '@/components/ui/shared/Input';
|
||||
import { CaseToothChartPanel } from '@/components/ui/lab/CaseToothChartPanel';
|
||||
import { FORM_SELECT_CLASS } from '@/components/shared/formSelectStyles';
|
||||
import { LabCaseAttachmentPreview } from '@/components/ui/lab/LabCaseAttachmentPreview';
|
||||
@@ -22,11 +23,15 @@ import {
|
||||
} from '@/components/treatment/prosthesisTypeDisplay';
|
||||
import { prosthesisCatalogApi } from '@/lib/api/prosthesis-catalog';
|
||||
import {
|
||||
buildCaseConnectedTeeth,
|
||||
buildCaseProsthesisRows,
|
||||
formatCaseDateTime,
|
||||
formatPatientName,
|
||||
latestCaseAttachment,
|
||||
} from '@/components/lab/caseDetailUtils';
|
||||
import { downloadCaseSheetPdf } from '@/components/lab/caseSheetPdf';
|
||||
import { ConnectedSelectionBadge } from '@/components/ui/treatment/ConnectedSelectionBadge';
|
||||
import { useToast } from '@/lib/hooks/useToast';
|
||||
import type { AssignableTaskStaff, LabCaseDetail, LabTaskStatus } from '@/types/cases';
|
||||
import type { ProsthesisCatalogEntry } from '@/types/treatment-catalog';
|
||||
|
||||
@@ -65,6 +70,8 @@ export interface CaseDetailPanelProps {
|
||||
canEditImportant?: boolean;
|
||||
updatingImportant?: boolean;
|
||||
onImportantChange?: (checked: boolean) => void;
|
||||
updatingExternalCode?: boolean;
|
||||
onExternalCodeChange?: (externalCode: string | null) => void;
|
||||
commentsSection?: ReactNode;
|
||||
assignableStaff?: AssignableTaskStaff[];
|
||||
canAssignTasks?: boolean;
|
||||
@@ -85,6 +92,8 @@ export function CaseDetailPanel({
|
||||
canEditImportant = false,
|
||||
updatingImportant = false,
|
||||
onImportantChange,
|
||||
updatingExternalCode = false,
|
||||
onExternalCodeChange,
|
||||
commentsSection,
|
||||
assignableStaff = [],
|
||||
canAssignTasks = false,
|
||||
@@ -92,9 +101,18 @@ export function CaseDetailPanel({
|
||||
onAssignTask,
|
||||
}: CaseDetailPanelProps) {
|
||||
const t = useTranslations('cases');
|
||||
const tTreatment = useTranslations('treatment');
|
||||
const intlLocale = useLocale();
|
||||
const toast = useToast();
|
||||
const [attachmentsDialogOpen, setAttachmentsDialogOpen] = useState(false);
|
||||
const [shareQrDialogOpen, setShareQrDialogOpen] = useState(false);
|
||||
const [prosthesisCatalog, setProsthesisCatalog] = useState<ProsthesisCatalogEntry[]>([]);
|
||||
const [downloadingPdf, setDownloadingPdf] = useState(false);
|
||||
const [externalCodeDraft, setExternalCodeDraft] = useState(labCase.externalCode ?? '');
|
||||
|
||||
useEffect(() => {
|
||||
setExternalCodeDraft(labCase.externalCode ?? '');
|
||||
}, [labCase.id, labCase.externalCode]);
|
||||
|
||||
useEffect(() => {
|
||||
void prosthesisCatalogApi
|
||||
@@ -104,8 +122,40 @@ export function CaseDetailPanel({
|
||||
}, []);
|
||||
|
||||
const prosthesisRows = useMemo(() => buildCaseProsthesisRows(labCase), [labCase]);
|
||||
const connectedTeeth = useMemo(() => buildCaseConnectedTeeth(labCase), [labCase]);
|
||||
const previewAttachment = useMemo(() => latestCaseAttachment(labCase), [labCase]);
|
||||
|
||||
async function handleDownloadCaseSheet() {
|
||||
setDownloadingPdf(true);
|
||||
try {
|
||||
await downloadCaseSheetPdf({
|
||||
labCase,
|
||||
locale: intlLocale,
|
||||
prosthesisCatalog,
|
||||
labels: {
|
||||
title: t('caseSheetTitle'),
|
||||
orderNumber: t('caseSheetOrderNumber'),
|
||||
orderDate: t('caseSheetOrderDate'),
|
||||
dateDue: t('caseSheetDateDue'),
|
||||
sender: t('caseSheetSender'),
|
||||
recipient: t('caseSheetRecipient'),
|
||||
patient: t('caseSheetPatient'),
|
||||
patientId: t('caseSheetPatientId'),
|
||||
prosthesis: t('caseSheetProsthesis'),
|
||||
toothChart: t('caseSheetToothChart'),
|
||||
connected: tTreatment('connectedBadge'),
|
||||
teeth: t('teethLabel'),
|
||||
comments: t('caseSheetComments'),
|
||||
noComments: t('caseSheetNoComments'),
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
toast.showError(t('errorDownloadCaseSheet'));
|
||||
} finally {
|
||||
setDownloadingPdf(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<header className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between border-b border-border pb-3">
|
||||
@@ -144,22 +194,55 @@ export function CaseDetailPanel({
|
||||
|
||||
<div className="flex w-full sm:w-auto shrink-0 flex-col items-end gap-2">
|
||||
{canEditImportant ? (
|
||||
<Checkbox
|
||||
checked={labCase.isImportant ?? false}
|
||||
disabled={updatingImportant}
|
||||
label={t('markCaseImportant')}
|
||||
className="shrink-0"
|
||||
onChange={(checked) => onImportantChange?.(checked)}
|
||||
/>
|
||||
<div className="flex w-full max-w-sm items-center gap-3">
|
||||
<Checkbox
|
||||
checked={labCase.isImportant ?? false}
|
||||
disabled={updatingImportant}
|
||||
label={t('markCaseImportant')}
|
||||
labelPosition="start"
|
||||
className="shrink-0"
|
||||
onChange={(checked) => onImportantChange?.(checked)}
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<Input
|
||||
value={externalCodeDraft}
|
||||
placeholder={t('externalCodePlaceholder')}
|
||||
disabled={updatingExternalCode}
|
||||
maxLength={64}
|
||||
aria-label={t('externalCodePlaceholder')}
|
||||
onChange={(e) => setExternalCodeDraft(e.target.value)}
|
||||
onBlur={() => {
|
||||
const next = externalCodeDraft.trim() || null;
|
||||
const prev = labCase.externalCode?.trim() || null;
|
||||
if (next === prev) return;
|
||||
onExternalCodeChange?.(next);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : labCase.externalCode?.trim() ? (
|
||||
<p className="text-sm text-text-muted">{labCase.externalCode.trim()}</p>
|
||||
) : null}
|
||||
{showCommentsButton && onCommentsClick ? (
|
||||
<Button type="button" variant="outline" size="sm" onClick={onCommentsClick}>
|
||||
<MessageSquare className="h-4 w-4 me-1.5" />
|
||||
{commentCount > 0
|
||||
? t('commentsCount', { count: commentCount })
|
||||
: t('showComments')}
|
||||
<div className="flex w-full sm:w-auto flex-wrap items-center justify-end gap-2">
|
||||
{showCommentsButton && onCommentsClick ? (
|
||||
<Button type="button" variant="outline" size="sm" onClick={onCommentsClick}>
|
||||
<MessageSquare className="h-4 w-4 me-1.5" />
|
||||
{commentCount > 0
|
||||
? t('commentsCount', { count: commentCount })
|
||||
: t('showComments')}
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
isLoading={downloadingPdf}
|
||||
onClick={() => void handleDownloadCaseSheet()}
|
||||
>
|
||||
<Download className="h-4 w-4 me-1.5" />
|
||||
{t('downloadCaseSheet')}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
{labCase.shareUrl || (previewAttachment && labCase.attachments.length > 0) ? (
|
||||
<div className="flex flex-row items-center gap-2 shrink-0">
|
||||
{previewAttachment && labCase.attachments.length > 0 ? (
|
||||
@@ -192,6 +275,7 @@ export function CaseDetailPanel({
|
||||
<CaseToothChartPanel
|
||||
details={labCase.detail ? [{ teeth: labCase.detail.teeth }] : []}
|
||||
prosthesisRows={prosthesisRows}
|
||||
connectedTeeth={connectedTeeth}
|
||||
prosthesisCatalog={prosthesisCatalog}
|
||||
className="w-full"
|
||||
/>
|
||||
@@ -201,8 +285,11 @@ export function CaseDetailPanel({
|
||||
<h3 className="text-sm font-medium text-text-primary">{t('treatmentDetails')}</h3>
|
||||
<div className="rounded-md bg-background border border-border p-2 text-sm">
|
||||
<div className="font-medium">{treatmentLabel(labCase.detail.treatmentType)}</div>
|
||||
<div className="text-text-muted">
|
||||
{t('teethLabel')}: {labCase.detail.teeth.join(', ') || '—'}
|
||||
<div className="flex flex-wrap items-center gap-2 text-text-muted">
|
||||
<span>
|
||||
{t('teethLabel')}: {labCase.detail.teeth.join(', ') || '—'}
|
||||
</span>
|
||||
{connectedTeeth.size > 0 ? <ConnectedSelectionBadge /> : null}
|
||||
</div>
|
||||
{labCase.detail.comment ? (
|
||||
<div className="text-text-muted mt-1">{labCase.detail.comment}</div>
|
||||
@@ -218,7 +305,7 @@ export function CaseDetailPanel({
|
||||
) : (
|
||||
labCase.tasksByTooth.map((group) => (
|
||||
<div
|
||||
key={`${group.treatmentDetailId}-${group.prosthesisTypeCode}`}
|
||||
key={`${group.treatmentDetailId}-${group.selectionGroupId ?? ''}-${group.prosthesisTypeCode}`}
|
||||
className="rounded-md border border-border p-3 space-y-2"
|
||||
>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
@@ -238,6 +325,7 @@ export function CaseDetailPanel({
|
||||
prosthesis: group.prosthesisTypeLabel,
|
||||
})}
|
||||
</span>
|
||||
{group.connected ? <ConnectedSelectionBadge /> : null}
|
||||
</div>
|
||||
<ul className="space-y-2">
|
||||
{group.tasks.map((task) => (
|
||||
|
||||
Reference in New Issue
Block a user