improvement: attachment selection for lab dispatch added. attachment preview added to cases feature.
This commit is contained in:
63
frontend/src/components/ui/lab/CaseToothChartPanel.tsx
Normal file
63
frontend/src/components/ui/lab/CaseToothChartPanel.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { FdiToothChart } from '@/components/ui/treatment/FdiToothChart';
|
||||
import { prosthesisTypeColor } from '@/components/ui/treatment/prosthesisTypeDisplay';
|
||||
import type { FdiToothId } from '@/types/treatment';
|
||||
|
||||
export interface CaseToothChartDetail {
|
||||
teeth: string[];
|
||||
}
|
||||
|
||||
export interface CaseToothChartProsthesisRow {
|
||||
teeth: string[];
|
||||
prosthesisTypeCode: string;
|
||||
}
|
||||
|
||||
interface CaseToothChartPanelProps {
|
||||
details: CaseToothChartDetail[];
|
||||
/** Prosthesis mapping from case tasks or toothProsthesis rows. */
|
||||
prosthesisRows: CaseToothChartProsthesisRow[];
|
||||
scale?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/** Read-only FDI chart for lab case detail — prosthesis-type glow on selected teeth. */
|
||||
export function CaseToothChartPanel({
|
||||
details,
|
||||
prosthesisRows,
|
||||
scale = 0.5,
|
||||
className = '',
|
||||
}: CaseToothChartPanelProps) {
|
||||
const selected = useMemo(() => {
|
||||
const set = new Set<FdiToothId>();
|
||||
for (const detail of details) {
|
||||
for (const tooth of detail.teeth) set.add(tooth as FdiToothId);
|
||||
}
|
||||
return set;
|
||||
}, [details]);
|
||||
|
||||
const toothColors = useMemo(() => {
|
||||
const colors: Partial<Record<FdiToothId, string>> = {};
|
||||
prosthesisRows.forEach((row, index) => {
|
||||
const color = prosthesisTypeColor(row.prosthesisTypeCode, index);
|
||||
for (const tooth of row.teeth) {
|
||||
colors[tooth as FdiToothId] = color;
|
||||
}
|
||||
});
|
||||
return colors;
|
||||
}, [prosthesisRows]);
|
||||
|
||||
if (selected.size === 0) return null;
|
||||
|
||||
return (
|
||||
<FdiToothChart
|
||||
selected={selected}
|
||||
readOnly
|
||||
scale={scale}
|
||||
toothColors={toothColors}
|
||||
compact
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
}
|
||||
67
frontend/src/components/ui/lab/LabCaseAttachmentPreview.tsx
Normal file
67
frontend/src/components/ui/lab/LabCaseAttachmentPreview.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { FileText } from 'lucide-react';
|
||||
import type { LabCaseAttachmentMeta } from '@/types/cases';
|
||||
|
||||
interface LabCaseAttachmentPreviewProps {
|
||||
caseId: string;
|
||||
attachment: LabCaseAttachmentMeta;
|
||||
loadBlob: (caseId: string, attachmentId: string) => Promise<Blob>;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function LabCaseAttachmentPreview({
|
||||
caseId,
|
||||
attachment,
|
||||
loadBlob,
|
||||
className = 'aspect-square w-full max-w-[11rem]',
|
||||
}: LabCaseAttachmentPreviewProps) {
|
||||
const [url, setUrl] = useState<string | null>(null);
|
||||
const [failed, setFailed] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
let objectUrl: string | null = null;
|
||||
|
||||
void (async () => {
|
||||
try {
|
||||
const blob = await loadBlob(caseId, 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);
|
||||
};
|
||||
}, [caseId, attachment.id, loadBlob]);
|
||||
|
||||
const isImage = attachment.mimeType.startsWith('image/');
|
||||
const isPdf = attachment.mimeType === 'application/pdf';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${className} rounded-[var(--radius-md)] border border-border/60 bg-background-secondary overflow-hidden`}
|
||||
title={attachment.fileName}
|
||||
>
|
||||
{url && isImage ? (
|
||||
<img src={url} alt={attachment.fileName} className="h-full w-full object-cover" />
|
||||
) : url && isPdf ? (
|
||||
<iframe src={url} title={attachment.fileName} className="h-full w-full border-0" />
|
||||
) : (
|
||||
<div className="flex h-full w-full flex-col items-center justify-center gap-1.5 p-2 text-text-muted">
|
||||
<FileText className="h-8 w-8 shrink-0 icon-flat" aria-hidden />
|
||||
<span className="line-clamp-2 text-center text-[10px] leading-tight">
|
||||
{failed ? 'Preview unavailable' : attachment.fileName}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,17 +1,25 @@
|
||||
import type { CSSProperties } from 'react';
|
||||
import type { BadgeVariant } from '@/components/ui/shared/Badge';
|
||||
import type { LabTaskStatus } from '@/types/cases';
|
||||
|
||||
export function labTaskStatusVariant(status: LabTaskStatus): BadgeVariant {
|
||||
return status === 'COMPLETED' ? 'success' : 'default';
|
||||
return status === 'COMPLETED' ? 'success' : 'warning';
|
||||
}
|
||||
|
||||
export function labTaskStatusSelectClass(status: LabTaskStatus): string {
|
||||
switch (status) {
|
||||
case 'COMPLETED':
|
||||
return 'border-success/60 text-success';
|
||||
case 'IN_PROGRESS':
|
||||
return 'border-primary/60 text-primary';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
/**
|
||||
* Inline style for the closed status <select> so its text/border reflect the
|
||||
* current value (yellow = in progress, green = completed). Uses the same badge
|
||||
* token colors as the badges for consistency. Native <option> colors have
|
||||
* limited cross-browser support, so only the closed control is themed.
|
||||
*/
|
||||
export function labTaskStatusSelectStyle(status: LabTaskStatus): CSSProperties {
|
||||
const color =
|
||||
status === 'COMPLETED'
|
||||
? 'var(--color-badge-success-fg)'
|
||||
: 'var(--color-badge-warning-fg)';
|
||||
const borderColor =
|
||||
status === 'COMPLETED'
|
||||
? 'var(--color-badge-success-border)'
|
||||
: 'var(--color-badge-warning-border)';
|
||||
return { color, borderColor };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user