improvement: treatment preview wizard optimized. comments component updated and unified accross the app.
This commit is contained in:
@@ -39,6 +39,7 @@ export function DetailLabCaseCommentsSection({
|
||||
<div className="border-t border-border/60 pt-4">
|
||||
<LabCaseCommentsPanel
|
||||
caseId={labCaseId}
|
||||
viewerSide="CLINIC"
|
||||
canPost={canPost}
|
||||
canToggleVisibility={false}
|
||||
deferSubmit={deferSubmit}
|
||||
|
||||
@@ -341,7 +341,7 @@ export function FdiToothChart({
|
||||
</div>
|
||||
<div className="flex flex-col items-start gap-1.5 sm:items-end shrink-0">
|
||||
{headerControl}
|
||||
<p className="text-[11px] text-text-secondary tabular-nums sm:text-right">
|
||||
<p className="text-[11px] text-text-secondary tabular-nums sm:text-end">
|
||||
{t('selectedLabel')}{' '}
|
||||
{selected.size === 0 ? t('selectedEmpty') : [...selected].sort().join(', ')}
|
||||
</p>
|
||||
|
||||
@@ -45,19 +45,10 @@ interface LabCasesDispatchPanelProps {
|
||||
canInviteLab?: boolean;
|
||||
onInviteLab?: () => void;
|
||||
sendBusyId: string | null;
|
||||
onAddLabCase: () => void | Promise<void>;
|
||||
onSendLabCase: (labCase: LabCaseDraft, comment?: string) => void | Promise<void>;
|
||||
onCommentError?: (message: string) => void;
|
||||
}
|
||||
|
||||
function sentDetailClientIds(labCases: LabCaseDraft[]): Set<string> {
|
||||
const ids = new Set<string>();
|
||||
for (const lc of labCases) {
|
||||
if (lc.sentAt && lc.detailClientId) ids.add(lc.detailClientId);
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
type ProsthesisGroupRow = {
|
||||
groupId: string;
|
||||
kind: 'connected' | 'single';
|
||||
@@ -129,7 +120,6 @@ export function LabCasesDispatchPanel({
|
||||
canInviteLab = false,
|
||||
onInviteLab,
|
||||
sendBusyId,
|
||||
onAddLabCase,
|
||||
onSendLabCase,
|
||||
onCommentError,
|
||||
}: LabCasesDispatchPanelProps) {
|
||||
@@ -157,8 +147,6 @@ export function LabCasesDispatchPanel({
|
||||
(activeLabCaseId ? labCases.find((lc) => lc.clientId === activeLabCaseId) : null);
|
||||
|
||||
const detailAlreadyInShipment = Boolean(labCaseForActiveDetail);
|
||||
const canAddLabShipment =
|
||||
!detailAlreadyInShipment && !sentDetailClientIds(labCases).has(activeDetailId);
|
||||
|
||||
const sent = Boolean(activeLabCase?.sentAt);
|
||||
const activeDetailNumber = details.findIndex((d) => d.clientId === activeDetailId) + 1;
|
||||
@@ -289,23 +277,14 @@ export function LabCasesDispatchPanel({
|
||||
}
|
||||
}
|
||||
|
||||
function renderShipmentCardHeader() {
|
||||
return (
|
||||
<div className="flex flex-col gap-3 border-b border-border/60 pb-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<p className="text-xs font-semibold text-text-primary">{t('labShipmentIncludedDetails')}</p>
|
||||
{renderDueDateField()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderDueDateField() {
|
||||
if (!activeLabCase) return null;
|
||||
const inputValue = toDateInputValue(activeLabCase.dueDate);
|
||||
const dueDateInputId = `lab-case-due-date-${activeLabCase.clientId}`;
|
||||
|
||||
return (
|
||||
<div className="shrink-0 sm:text-end">
|
||||
<div className="flex items-center gap-2 sm:justify-end">
|
||||
<div className="w-full min-w-0 sm:w-auto sm:shrink-0 sm:text-end">
|
||||
<div className="flex flex-wrap items-center gap-2 sm:justify-end">
|
||||
<label
|
||||
htmlFor={dueDateInputId}
|
||||
className="text-xs font-medium text-text-secondary shrink-0"
|
||||
@@ -323,7 +302,7 @@ export function LabCasesDispatchPanel({
|
||||
onBlur={(committed) => {
|
||||
if (sent) void handleSentDueDateBlur(committed);
|
||||
}}
|
||||
className={`${FORM_SELECT_CLASS} w-full max-w-[11rem] rounded-md py-1.5 text-sm`}
|
||||
className={`${FORM_SELECT_CLASS} w-full min-w-0 max-w-full sm:max-w-[11rem] rounded-md py-1.5 text-sm`}
|
||||
/>
|
||||
</div>
|
||||
{sent && caseFullyComplete && activeLabCase.dueDate ? (
|
||||
@@ -335,7 +314,6 @@ export function LabCasesDispatchPanel({
|
||||
|
||||
function renderIncludedDetailSummary() {
|
||||
if (!activeDetail) return null;
|
||||
const typeLabel = treatmentTypeLabelFromCatalog(activeDetail.treatmentType, treatmentCatalog);
|
||||
|
||||
if (activeDetail.treatmentType !== 'prosthesis' || !activeLabCase) {
|
||||
return (
|
||||
@@ -375,9 +353,6 @@ export function LabCasesDispatchPanel({
|
||||
|
||||
return (
|
||||
<div className="text-sm text-text-primary rounded-[var(--radius-sm)] border border-border/50 bg-background-secondary/50 px-3 py-2 space-y-1">
|
||||
<p className="text-text-primary">
|
||||
{t('detailLabel', { n: activeDetailNumber })} · {typeLabel}
|
||||
</p>
|
||||
{rows.map((g) => (
|
||||
<p
|
||||
key={g.groupId}
|
||||
@@ -401,31 +376,19 @@ export function LabCasesDispatchPanel({
|
||||
const activeDetailAttachments = activeDetail.attachmentMetas ?? [];
|
||||
|
||||
return (
|
||||
<div className="surface-card p-4 space-y-4">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<div className="surface-card p-3 sm:p-4 space-y-4 min-w-0 overflow-x-hidden">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div className="min-w-0">
|
||||
<h3 className="text-sm font-semibold text-text-primary">{t('labDispatchTitle')}</h3>
|
||||
<p className="text-xs text-text-muted mt-0.5">
|
||||
{t('labDispatchSubtitle')} {t('labDispatchSendHint')}
|
||||
</p>
|
||||
</div>
|
||||
{canAddLabShipment && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="primary"
|
||||
disabled={!canEdit || disabled}
|
||||
onClick={onAddLabCase}
|
||||
>
|
||||
{t('addLabShipment')}
|
||||
</Button>
|
||||
)}
|
||||
{detailAlreadyInShipment ? renderDueDateField() : null}
|
||||
</div>
|
||||
|
||||
{!detailAlreadyInShipment ? (
|
||||
<p className="text-xs text-text-muted">{t('labDispatchEmpty')}</p>
|
||||
) : activeLabCase ? (
|
||||
<div className="space-y-4 border border-border/60 rounded-[var(--radius-md)] p-3 sm:p-4 bg-background-secondary/30">
|
||||
{renderShipmentCardHeader()}
|
||||
{activeLabCase ? (
|
||||
<div className="space-y-4 border border-border/60 rounded-[var(--radius-md)] p-3 sm:p-4 bg-background-secondary/30 min-w-0">
|
||||
{sent ? (
|
||||
<>
|
||||
{renderIncludedDetailSummary()}
|
||||
@@ -589,39 +552,38 @@ export function LabCasesDispatchPanel({
|
||||
return (
|
||||
<label
|
||||
key={row.groupId}
|
||||
className="block text-xs text-text-muted space-y-1 rounded-[var(--radius-sm)] border border-border/50 bg-background-secondary/40 px-3 py-2"
|
||||
className="block text-xs text-text-muted rounded-[var(--radius-sm)] border border-border/50 bg-background-secondary/40 px-3 py-2 min-w-0"
|
||||
>
|
||||
<span className="flex flex-wrap items-center gap-2 text-text-secondary">
|
||||
{row.kind === 'connected' ? <ConnectedSelectionBadge /> : null}
|
||||
<span>
|
||||
{row.kind === 'connected'
|
||||
? t('prosthesisConnectedLabel')
|
||||
: t('prosthesisColTooth')}
|
||||
{': '}
|
||||
<span className="text-text-primary">{row.teeth.join(', ')}</span>
|
||||
</span>
|
||||
<span className="text-text-muted">
|
||||
· {t('detailLabel', { n: row.detailNumber })}
|
||||
<span className="grid grid-cols-1 gap-2 md:grid-cols-2 md:items-center md:gap-3">
|
||||
<span className="flex flex-wrap items-center gap-2 text-text-secondary min-w-0 break-words">
|
||||
{row.kind === 'connected' ? <ConnectedSelectionBadge /> : null}
|
||||
<span className="min-w-0">
|
||||
{row.kind === 'connected'
|
||||
? t('prosthesisConnectedLabel')
|
||||
: t('prosthesisColTooth')}
|
||||
{': '}
|
||||
<span className="text-text-primary">{row.teeth.join(', ')}</span>
|
||||
</span>
|
||||
</span>
|
||||
<select
|
||||
value={current}
|
||||
disabled={disabled}
|
||||
onChange={(e) => setGroupProsthesis(row, e.target.value)}
|
||||
className={`${FORM_SELECT_CLASS} w-full min-w-0`}
|
||||
aria-label={
|
||||
row.kind === 'connected'
|
||||
? t('prosthesisConnectedLabel')
|
||||
: t('prosthesisColType')
|
||||
}
|
||||
>
|
||||
<option value="">{t('prosthesisSelectPlaceholder')}</option>
|
||||
{prosthesisOptions.map((opt) => (
|
||||
<option key={opt.code} value={opt.code}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</span>
|
||||
<select
|
||||
value={current}
|
||||
disabled={disabled}
|
||||
onChange={(e) => setGroupProsthesis(row, e.target.value)}
|
||||
className={`${FORM_SELECT_CLASS} w-full mt-1`}
|
||||
aria-label={
|
||||
row.kind === 'connected'
|
||||
? t('prosthesisConnectedLabel')
|
||||
: t('prosthesisColType')
|
||||
}
|
||||
>
|
||||
<option value="">{t('prosthesisSelectPlaceholder')}</option>
|
||||
{prosthesisOptions.map((opt) => (
|
||||
<option key={opt.code} value={opt.code}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -221,9 +221,9 @@ export function TreatmentDetailsEditor({
|
||||
);
|
||||
}}
|
||||
placeholder={t('commentsPlaceholder')}
|
||||
rows={5}
|
||||
rows={2}
|
||||
disabled={readOnly}
|
||||
className="mt-1.5 w-full rounded-[var(--radius-md)] border border-border bg-background-secondary/90 text-text-primary text-sm px-3 py-2 placeholder:text-text-muted focus:outline-none focus:ring-2 focus:ring-primary/35 resize-y min-h-[120px]"
|
||||
className="mt-1.5 w-full rounded-[var(--radius-md)] border border-border bg-background-secondary/90 text-text-primary text-sm px-3 py-2 placeholder:text-text-muted focus:outline-none focus:ring-2 focus:ring-primary/35 resize-y min-h-[60px]"
|
||||
/>
|
||||
</label>
|
||||
|
||||
|
||||
@@ -1547,6 +1547,27 @@ export function TreatmentWorkspace({
|
||||
t,
|
||||
]);
|
||||
|
||||
// Auto-open shipment draft when entering Lab (no manual "Add lab shipment" click).
|
||||
useEffect(() => {
|
||||
if (entryStep !== 'lab') return;
|
||||
if (!canEditTreatmentForDay || !selectedAppointment) return;
|
||||
const activeDetail = details.find((d) => d.clientId === activeDetailId);
|
||||
if (!activeDetail || !isDetailReadyForLabDispatch(activeDetail, labDependentCodes)) return;
|
||||
if (isLabDependentDetailMissingTeeth(activeDetail, labDependentCodes)) return;
|
||||
const hasDraft = labCaseDrafts.some((lc) => lc.detailClientId === activeDetailId);
|
||||
if (hasDraft) return;
|
||||
void handleAddLabCase();
|
||||
}, [
|
||||
activeDetailId,
|
||||
canEditTreatmentForDay,
|
||||
details,
|
||||
entryStep,
|
||||
handleAddLabCase,
|
||||
labCaseDrafts,
|
||||
labDependentCodes,
|
||||
selectedAppointment,
|
||||
]);
|
||||
|
||||
const handleSendLabCase = useCallback(
|
||||
async (labCase: LabCaseDraft, comment?: string) => {
|
||||
if (!canEditTreatmentForDay || !selectedAppointment) return;
|
||||
@@ -2058,7 +2079,6 @@ export function TreatmentWorkspace({
|
||||
});
|
||||
}}
|
||||
sendBusyId={sendBusyId}
|
||||
onAddLabCase={() => handleAddLabCase()}
|
||||
onSendLabCase={(lc, comment) => handleSendLabCase(lc, comment)}
|
||||
onCommentError={showError}
|
||||
canInviteLab={canAccessOrganizations}
|
||||
|
||||
Reference in New Issue
Block a user