569 lines
22 KiB
TypeScript
569 lines
22 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import { useTranslations } from 'next-intl';
|
|
import { Button } from '@/components/ui/shared/Button';
|
|
import { Checkbox } from '@/components/ui/shared/Checkbox';
|
|
import { ConnectedSelectionBadge } from '@/components/ui/treatment/ConnectedSelectionBadge';
|
|
import { isDetailReadyForLabDispatch, isLabCaseCompleted } from '@/components/treatment/treatmentDetailRules';
|
|
import { AppDateInput } from '@/components/ui/shared/AppDateInput';
|
|
import { toDateInputValue } from '@/components/lab/labCaseDueDateDisplay';
|
|
import { FORM_SELECT_CLASS } from '@/components/shared/formSelectStyles';
|
|
import { LinkedOrganizationSearchCombobox } from '@/components/ui/treatment/LinkedOrganizationSearchCombobox';
|
|
import { CaseSentLabel } from '@/components/ui/treatment/CaseSentLabel';
|
|
import { DetailLabCaseCommentsSection } from '@/components/ui/treatment/DetailLabCaseCommentsSection';
|
|
import { LabCaseTrackerCard } from '@/components/ui/treatment/LabCaseTrackerCard';
|
|
import { treatmentTypeLabelFromCatalog } from '@/components/shared/treatmentTypeDisplay';
|
|
import { treatmentsApi } from '@/lib/api/treatments';
|
|
import { prosthesisCatalogApi } from '@/lib/api/prosthesis-catalog';
|
|
import type { ProsthesisCatalogEntry, TreatmentCatalogEntry } from '@/types/treatment-catalog';
|
|
import type { LabCaseDraft, LinkedOrganizationOption, TreatmentDetailDraft } from '@/types/treatment';
|
|
import type { PatientLabCaseSummary } from '@/types/lab-case-activity';
|
|
import { getUserFacingError } from '@/components/shared/formatApiError';
|
|
import { groupsFromFlatTeeth } from '@/components/treatment/toothSelectionGroups';
|
|
|
|
interface LabCasesDispatchPanelProps {
|
|
details: TreatmentDetailDraft[];
|
|
activeDetailId: string;
|
|
labCases: LabCaseDraft[];
|
|
labDependentCodes: Set<string>;
|
|
treatmentCatalog: TreatmentCatalogEntry[];
|
|
labCaseSummary?: PatientLabCaseSummary | null;
|
|
locale: string;
|
|
onLabCaseSummaryChange?: (summary: PatientLabCaseSummary) => void;
|
|
onLabCaseMarkedRead?: (labCaseId: string) => void;
|
|
onLabCaseActivityChange?: () => void;
|
|
activeLabCaseId: string | null;
|
|
onLabCasesChange: (labCases: LabCaseDraft[]) => void;
|
|
disabled: boolean;
|
|
canEdit: boolean;
|
|
orgs: LinkedOrganizationOption[];
|
|
organizationSearch: string;
|
|
onOrganizationSearchChange: (value: string) => void;
|
|
recentOrganizationIds: string[];
|
|
canInviteLab?: boolean;
|
|
onInviteLab?: () => void;
|
|
sendBusyId: string | null;
|
|
onSendLabCase: (labCase: LabCaseDraft, comment?: string) => void | Promise<void>;
|
|
onCommentError?: (message: string) => void;
|
|
}
|
|
|
|
type ProsthesisGroupRow = {
|
|
groupId: string;
|
|
kind: 'connected' | 'single';
|
|
teeth: string[];
|
|
detailClientId: string;
|
|
detailNumber: number;
|
|
};
|
|
|
|
function prosthesisGroupRows(
|
|
labCase: LabCaseDraft,
|
|
activeDetail: TreatmentDetailDraft,
|
|
detailNumber: number,
|
|
): ProsthesisGroupRow[] {
|
|
if (labCase.detailClientId !== activeDetail.clientId) return [];
|
|
if (activeDetail.treatmentType !== 'prosthesis') return [];
|
|
|
|
const groups =
|
|
activeDetail.toothSelectionGroups.length > 0
|
|
? activeDetail.toothSelectionGroups
|
|
: groupsFromFlatTeeth(activeDetail.teeth);
|
|
|
|
return groups.map((g) => ({
|
|
groupId: g.groupId,
|
|
kind: g.kind,
|
|
teeth: g.teeth,
|
|
detailClientId: activeDetail.clientId,
|
|
detailNumber,
|
|
}));
|
|
}
|
|
|
|
function isProsthesisMapComplete(
|
|
labCase: LabCaseDraft,
|
|
rows: ProsthesisGroupRow[],
|
|
): boolean {
|
|
if (rows.length === 0) return true;
|
|
return rows.every((row) =>
|
|
row.teeth.every((tooth) =>
|
|
labCase.toothProsthesis.some(
|
|
(tp) =>
|
|
tp.detailClientId === row.detailClientId &&
|
|
tp.tooth === tooth &&
|
|
tp.selectionGroupId === row.groupId &&
|
|
Boolean(tp.prosthesisTypeCode),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
function toothProsthesisForRows(
|
|
rows: ProsthesisGroupRow[],
|
|
prosthesisTypeCode: string,
|
|
): LabCaseDraft['toothProsthesis'] {
|
|
return rows.flatMap((row) =>
|
|
row.teeth.map((tooth) => ({
|
|
detailClientId: row.detailClientId,
|
|
tooth,
|
|
prosthesisTypeCode,
|
|
selectionGroupId: row.groupId,
|
|
})),
|
|
);
|
|
}
|
|
|
|
export function LabCasesDispatchPanel({
|
|
details,
|
|
activeDetailId,
|
|
labCases,
|
|
labDependentCodes,
|
|
treatmentCatalog,
|
|
labCaseSummary,
|
|
locale,
|
|
onLabCaseSummaryChange,
|
|
onLabCaseMarkedRead,
|
|
onLabCaseActivityChange,
|
|
activeLabCaseId,
|
|
onLabCasesChange,
|
|
disabled,
|
|
canEdit,
|
|
orgs,
|
|
organizationSearch,
|
|
onOrganizationSearchChange,
|
|
recentOrganizationIds,
|
|
canInviteLab = false,
|
|
onInviteLab,
|
|
sendBusyId,
|
|
onSendLabCase,
|
|
onCommentError,
|
|
}: LabCasesDispatchPanelProps) {
|
|
const t = useTranslations('treatment');
|
|
const tErrors = useTranslations('errors');
|
|
const [prosthesisOptions, setProsthesisOptions] = useState<ProsthesisCatalogEntry[]>([]);
|
|
const [applyAllProsthesis, setApplyAllProsthesis] = useState('');
|
|
const [pendingComment, setPendingComment] = useState('');
|
|
const hasTrackerSummary = Boolean(labCaseSummary && labCaseSummary.labCaseId);
|
|
|
|
const activeLinkedOrganizations = orgs.filter((o) => o.active);
|
|
const recentOrganizations = recentOrganizationIds
|
|
.map((id) => activeLinkedOrganizations.find((o) => o.id === id))
|
|
.filter(Boolean) as LinkedOrganizationOption[];
|
|
|
|
const activeDetail = details.find((d) => d.clientId === activeDetailId) ?? null;
|
|
const isLabDependentDetail = Boolean(
|
|
activeDetail && isDetailReadyForLabDispatch(activeDetail, labDependentCodes),
|
|
);
|
|
|
|
const labCaseForActiveDetail =
|
|
labCases.find((lc) => lc.detailClientId === activeDetailId) ?? null;
|
|
|
|
const activeLabCase =
|
|
labCaseForActiveDetail ??
|
|
(activeLabCaseId
|
|
? labCases.find(
|
|
(lc) =>
|
|
lc.clientId === activeLabCaseId &&
|
|
(lc.detailClientId == null || lc.detailClientId === activeDetailId),
|
|
)
|
|
: null) ??
|
|
null;
|
|
|
|
const sent = Boolean(activeLabCase?.sentAt);
|
|
const activeDetailNumber = details.findIndex((d) => d.clientId === activeDetailId) + 1;
|
|
|
|
const activeLabOrgName = activeLabCase?.destinationOrganizationId
|
|
? orgs.find((o) => o.id === activeLabCase.destinationOrganizationId)?.name
|
|
: null;
|
|
|
|
const prosthesisRows = activeLabCase && activeDetail
|
|
? prosthesisGroupRows(activeLabCase, activeDetail, activeDetailNumber)
|
|
: [];
|
|
const prosthesisComplete = activeLabCase
|
|
? isProsthesisMapComplete(activeLabCase, prosthesisRows)
|
|
: true;
|
|
const flatToothCount = prosthesisRows.reduce((sum, row) => sum + row.teeth.length, 0);
|
|
|
|
useEffect(() => {
|
|
if (!activeLabCase?.destinationOrganizationId) {
|
|
setProsthesisOptions([]);
|
|
return;
|
|
}
|
|
|
|
let cancelled = false;
|
|
void prosthesisCatalogApi
|
|
.list(activeLabCase.destinationOrganizationId)
|
|
.then((res) => {
|
|
if (!cancelled) setProsthesisOptions(res.data);
|
|
})
|
|
.catch(() => {
|
|
if (!cancelled) setProsthesisOptions([]);
|
|
});
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [activeLabCase?.destinationOrganizationId]);
|
|
|
|
useEffect(() => {
|
|
setPendingComment('');
|
|
setApplyAllProsthesis('');
|
|
}, [activeLabCase?.clientId]);
|
|
|
|
function updateActiveLabCase(patch: Partial<LabCaseDraft>) {
|
|
if (!activeLabCase) return;
|
|
onLabCasesChange(
|
|
labCases.map((lc) => (lc.clientId === activeLabCase.clientId ? { ...lc, ...patch } : lc)),
|
|
);
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (!activeLabCase || sent || !activeDetail) return;
|
|
const ids = activeDetail.attachmentMetas.map((a) => a.id);
|
|
const missing = ids.filter((id) => !activeLabCase.attachmentIds.includes(id));
|
|
if (missing.length === 0) return;
|
|
updateActiveLabCase({ attachmentIds: [...activeLabCase.attachmentIds, ...missing] });
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- only sync newly uploaded files
|
|
}, [activeDetail?.attachmentMetas, activeLabCase?.clientId, sent]);
|
|
|
|
if (!activeDetail || !isLabDependentDetail) {
|
|
return null;
|
|
}
|
|
|
|
function setGroupProsthesis(row: ProsthesisGroupRow, prosthesisTypeCode: string) {
|
|
if (!activeLabCase) return;
|
|
const toothSet = new Set(row.teeth);
|
|
const rest = activeLabCase.toothProsthesis.filter(
|
|
(tp) => !(tp.detailClientId === row.detailClientId && toothSet.has(tp.tooth)),
|
|
);
|
|
const next = prosthesisTypeCode
|
|
? [
|
|
...rest,
|
|
...row.teeth.map((tooth) => ({
|
|
detailClientId: row.detailClientId,
|
|
tooth,
|
|
prosthesisTypeCode,
|
|
selectionGroupId: row.groupId,
|
|
})),
|
|
]
|
|
: rest;
|
|
updateActiveLabCase({ toothProsthesis: next });
|
|
}
|
|
|
|
function applyProsthesisToAll(code: string) {
|
|
if (!activeLabCase || !code) return;
|
|
updateActiveLabCase({ toothProsthesis: toothProsthesisForRows(prosthesisRows, code) });
|
|
}
|
|
|
|
function toggleAttachmentInActiveLabCase(attachmentId: string, checked: boolean) {
|
|
if (!activeLabCase || sent) return;
|
|
const set = new Set(activeLabCase.attachmentIds);
|
|
if (checked) set.add(attachmentId);
|
|
else set.delete(attachmentId);
|
|
updateActiveLabCase({ attachmentIds: [...set] });
|
|
}
|
|
|
|
function handleSelectOrganization(org: LinkedOrganizationOption) {
|
|
updateActiveLabCase({
|
|
destinationOrganizationId: org.id,
|
|
toothProsthesis: [],
|
|
});
|
|
setApplyAllProsthesis('');
|
|
}
|
|
|
|
const caseFullyComplete = isLabCaseCompleted(activeLabCase?.taskProgress);
|
|
const canEditDueDate = canEdit && !disabled && (!sent || !caseFullyComplete);
|
|
const canPostComments = canEdit && !caseFullyComplete;
|
|
const canShowComments = Boolean(activeLabCase?.id);
|
|
const commentsDeferSubmit = Boolean(!sent);
|
|
|
|
async function handleSentDueDateBlur(nextValue: string) {
|
|
if (!activeLabCase?.id || !sent || !canEditDueDate) return;
|
|
const dueDate = nextValue || null;
|
|
if (dueDate === (activeLabCase.dueDate?.slice(0, 10) ?? null)) return;
|
|
try {
|
|
const response = await treatmentsApi.updateLabCaseDueDate(activeLabCase.id, dueDate);
|
|
updateActiveLabCase({
|
|
dueDate: response.data.dueDate,
|
|
taskProgress: response.data.taskProgress ?? activeLabCase.taskProgress,
|
|
});
|
|
} catch (error) {
|
|
onCommentError?.(getUserFacingError(error, tErrors, t('dueDateUpdateError')));
|
|
}
|
|
}
|
|
|
|
function renderDueDateField() {
|
|
if (!activeLabCase) return null;
|
|
const inputValue = toDateInputValue(activeLabCase.dueDate);
|
|
const dueDateInputId = `lab-case-due-date-${activeLabCase.clientId}`;
|
|
|
|
return (
|
|
<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"
|
|
>
|
|
{t('dueDateLabel')}{' '}
|
|
<span className="font-normal text-text-muted">({t('dueDateOptional')})</span>
|
|
</label>
|
|
<AppDateInput
|
|
id={dueDateInputId}
|
|
value={inputValue}
|
|
disabled={!canEditDueDate}
|
|
onChange={(next) => {
|
|
updateActiveLabCase({ dueDate: next || null });
|
|
}}
|
|
onBlur={(committed) => {
|
|
if (sent) void handleSentDueDateBlur(committed);
|
|
}}
|
|
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 ? (
|
|
<p className="mt-1.5 text-[11px] text-text-muted sm:text-end">{t('dueDateLockedCompleted')}</p>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const typeLabel = treatmentTypeLabelFromCatalog(activeDetail.treatmentType, treatmentCatalog);
|
|
const teethLabel = activeDetail.teeth.length ? [...activeDetail.teeth].sort().join(', ') : t('teethNone');
|
|
const activeDetailAttachments = activeDetail.attachmentMetas ?? [];
|
|
|
|
return (
|
|
<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">
|
|
{typeLabel} · {teethLabel}
|
|
</p>
|
|
</div>
|
|
{activeLabCase ? renderDueDateField() : null}
|
|
</div>
|
|
|
|
{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 ? (
|
|
<>
|
|
{activeLabOrgName ? (
|
|
<div>
|
|
<p className="text-xs font-medium text-text-secondary">{t('selectLab')}</p>
|
|
<p className="text-sm text-text-primary mt-1">{activeLabOrgName}</p>
|
|
</div>
|
|
) : null}
|
|
|
|
<CaseSentLabel
|
|
treatmentCase={{
|
|
destinationOrganizationId: activeLabCase.destinationOrganizationId,
|
|
sendToOrganizationIds: activeLabCase.destinationOrganizationId
|
|
? [activeLabCase.destinationOrganizationId]
|
|
: [],
|
|
sentAt: activeLabCase.sentAt ?? null,
|
|
sends: activeLabCase.sends,
|
|
}}
|
|
orgs={orgs}
|
|
/>
|
|
|
|
{hasTrackerSummary && labCaseSummary ? (
|
|
<LabCaseTrackerCard
|
|
summary={labCaseSummary}
|
|
locale={locale}
|
|
onSummaryChange={onLabCaseSummaryChange}
|
|
onMarkedRead={onLabCaseMarkedRead}
|
|
/>
|
|
) : null}
|
|
|
|
{canShowComments && activeLabCase?.id ? (
|
|
<DetailLabCaseCommentsSection
|
|
labCaseId={activeLabCase.id}
|
|
canPost={canPostComments}
|
|
onError={onCommentError}
|
|
onMarkRead={onLabCaseMarkedRead}
|
|
onActivityChange={onLabCaseActivityChange}
|
|
/>
|
|
) : null}
|
|
</>
|
|
) : (
|
|
<>
|
|
<div className="space-y-2">
|
|
<p className="text-xs font-medium text-text-secondary">{t('selectLab')}</p>
|
|
<LinkedOrganizationSearchCombobox
|
|
search={organizationSearch}
|
|
onSearchChange={onOrganizationSearchChange}
|
|
organizations={activeLinkedOrganizations}
|
|
selectedOrganizationId={activeLabCase.destinationOrganizationId}
|
|
onSelectOrganization={handleSelectOrganization}
|
|
disabled={disabled}
|
|
canInviteLab={canInviteLab}
|
|
onInviteLab={onInviteLab}
|
|
noPermissionMessage={t('noOrgInvitePermission')}
|
|
/>
|
|
{recentOrganizations.length > 0 && (
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<span className="text-xs text-text-muted">{t('recent')}</span>
|
|
{recentOrganizations.map((o) => (
|
|
<button
|
|
key={o.id}
|
|
type="button"
|
|
disabled={disabled}
|
|
onClick={() => handleSelectOrganization(o)}
|
|
className="text-xs rounded-[var(--radius-sm)] border border-border/70 px-2 py-1 text-text-secondary hover:text-text-primary hover:border-border focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/35 disabled:opacity-50"
|
|
>
|
|
{o.name}
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{prosthesisRows.length > 0 && activeLabCase.destinationOrganizationId ? (
|
|
<div className="space-y-3 border-t border-border/60 pt-3">
|
|
<p className="text-xs font-medium text-text-secondary">
|
|
{t('prosthesisTypesTitle')}
|
|
</p>
|
|
{flatToothCount > 1 ? (
|
|
<label className="block text-xs text-text-muted space-y-1">
|
|
{t('prosthesisApplyAll')}
|
|
<select
|
|
value={applyAllProsthesis}
|
|
disabled={disabled || prosthesisOptions.length === 0}
|
|
onChange={(e) => {
|
|
const code = e.target.value;
|
|
setApplyAllProsthesis(code);
|
|
if (code) applyProsthesisToAll(code);
|
|
}}
|
|
className={`${FORM_SELECT_CLASS} w-full mt-1`}
|
|
>
|
|
<option value="">{t('prosthesisSelectPlaceholder')}</option>
|
|
{prosthesisOptions.map((opt) => (
|
|
<option key={opt.code} value={opt.code}>
|
|
{opt.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
) : null}
|
|
<div className="space-y-3">
|
|
{prosthesisRows.map((row) => {
|
|
const current =
|
|
activeLabCase.toothProsthesis.find(
|
|
(tp) =>
|
|
tp.detailClientId === row.detailClientId &&
|
|
tp.selectionGroupId === row.groupId &&
|
|
row.teeth.includes(tp.tooth),
|
|
)?.prosthesisTypeCode ??
|
|
activeLabCase.toothProsthesis.find(
|
|
(tp) =>
|
|
tp.detailClientId === row.detailClientId &&
|
|
row.teeth.includes(tp.tooth),
|
|
)?.prosthesisTypeCode ??
|
|
'';
|
|
return (
|
|
<label
|
|
key={row.groupId}
|
|
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="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>
|
|
</label>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
|
|
{activeDetailAttachments.length > 0 ? (
|
|
<div>
|
|
<p className="text-xs font-medium text-text-secondary mb-1">
|
|
{t('labShipmentAttachments')}
|
|
</p>
|
|
<p className="text-[11px] text-text-muted mb-2">{t('labShipmentAttachmentsHint')}</p>
|
|
<div className="flex flex-col gap-2">
|
|
{activeDetailAttachments.map((att) => (
|
|
<Checkbox
|
|
key={att.id}
|
|
checked={activeLabCase.attachmentIds.includes(att.id)}
|
|
disabled={disabled}
|
|
onChange={(next) => toggleAttachmentInActiveLabCase(att.id, next)}
|
|
label={`${att.fileName} (${(att.sizeBytes / 1024).toFixed(1)} KB)`}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
|
|
{hasTrackerSummary && labCaseSummary ? (
|
|
<LabCaseTrackerCard
|
|
summary={labCaseSummary}
|
|
locale={locale}
|
|
onSummaryChange={onLabCaseSummaryChange}
|
|
onMarkedRead={onLabCaseMarkedRead}
|
|
/>
|
|
) : null}
|
|
|
|
{canShowComments && activeLabCase?.id ? (
|
|
<DetailLabCaseCommentsSection
|
|
labCaseId={activeLabCase.id}
|
|
canPost={canPostComments}
|
|
deferSubmit={commentsDeferSubmit}
|
|
composerValue={pendingComment}
|
|
onComposerValueChange={setPendingComment}
|
|
onError={onCommentError}
|
|
onMarkRead={onLabCaseMarkedRead}
|
|
onActivityChange={onLabCaseActivityChange}
|
|
/>
|
|
) : null}
|
|
|
|
<div className="flex flex-wrap items-center gap-3 pt-1">
|
|
<Button
|
|
type="button"
|
|
variant="primary"
|
|
className="w-full sm:w-auto"
|
|
disabled={
|
|
disabled ||
|
|
sendBusyId === activeLabCase.clientId ||
|
|
!activeLabCase.destinationOrganizationId ||
|
|
!activeLabCase.detailClientId ||
|
|
!prosthesisComplete
|
|
}
|
|
isLoading={sendBusyId === activeLabCase.clientId}
|
|
onClick={() => onSendLabCase(activeLabCase, pendingComment.trim())}
|
|
>
|
|
{t('sendToLab')}
|
|
</Button>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|