improvement: lab case dispatch UI improved.
This commit is contained in:
@@ -467,6 +467,9 @@
|
|||||||
"addLabShipment": "Add lab shipment",
|
"addLabShipment": "Add lab shipment",
|
||||||
"labShipmentLabel": "Shipment {n}",
|
"labShipmentLabel": "Shipment {n}",
|
||||||
"includeDetails": "Include treatment details",
|
"includeDetails": "Include treatment details",
|
||||||
|
"labShipmentIncludedDetails": "Included in this shipment",
|
||||||
|
"labShipmentNoIncludedDetails": "No details were included in this shipment.",
|
||||||
|
"labShipmentNoDetailsAvailable": "All lab details are already in other shipments or have been sent.",
|
||||||
"labDetailLine": "Detail {n} · {type} · {teeth}",
|
"labDetailLine": "Detail {n} · {type} · {teeth}",
|
||||||
"noLabDetails": "No lab-dependent treatment details yet. Add a lab type (e.g. endo) in treatment details above.",
|
"noLabDetails": "No lab-dependent treatment details yet. Add a lab type (e.g. endo) in treatment details above.",
|
||||||
"labDispatchEmpty": "Add a lab shipment to group details and send them to a lab.",
|
"labDispatchEmpty": "Add a lab shipment to group details and send them to a lab.",
|
||||||
|
|||||||
@@ -467,6 +467,9 @@
|
|||||||
"addLabShipment": "افزودن محموله لاب",
|
"addLabShipment": "افزودن محموله لاب",
|
||||||
"labShipmentLabel": "محموله {n}",
|
"labShipmentLabel": "محموله {n}",
|
||||||
"includeDetails": "شامل جزئیات درمان",
|
"includeDetails": "شامل جزئیات درمان",
|
||||||
|
"labShipmentIncludedDetails": "شامل این محموله",
|
||||||
|
"labShipmentNoIncludedDetails": "جزئیاتی در این محموله گنجانده نشده است.",
|
||||||
|
"labShipmentNoDetailsAvailable": "همه جزئیات لاب در محمولههای دیگر هستند یا ارسال شدهاند.",
|
||||||
"labDetailLine": "جزئیات {n} · {type} · {teeth}",
|
"labDetailLine": "جزئیات {n} · {type} · {teeth}",
|
||||||
"noLabDetails": "هنوز جزئیات وابسته به لاب وجود ندارد. نوع لاب (مثلاً اندو) در جزئیات درمان بالا اضافه کنید.",
|
"noLabDetails": "هنوز جزئیات وابسته به لاب وجود ندارد. نوع لاب (مثلاً اندو) در جزئیات درمان بالا اضافه کنید.",
|
||||||
"labDispatchEmpty": "یک محموله لاب اضافه کنید تا جزئیات را گروهبندی و ارسال کنید.",
|
"labDispatchEmpty": "یک محموله لاب اضافه کنید تا جزئیات را گروهبندی و ارسال کنید.",
|
||||||
|
|||||||
@@ -467,6 +467,9 @@
|
|||||||
"addLabShipment": "Labzending toevoegen",
|
"addLabShipment": "Labzending toevoegen",
|
||||||
"labShipmentLabel": "Zending {n}",
|
"labShipmentLabel": "Zending {n}",
|
||||||
"includeDetails": "Behandeldetails opnemen",
|
"includeDetails": "Behandeldetails opnemen",
|
||||||
|
"labShipmentIncludedDetails": "Opgenomen in deze zending",
|
||||||
|
"labShipmentNoIncludedDetails": "Geen details opgenomen in deze zending.",
|
||||||
|
"labShipmentNoDetailsAvailable": "Alle labdetails zitten al in andere zendingen of zijn verzonden.",
|
||||||
"labDetailLine": "Detail {n} · {type} · {teeth}",
|
"labDetailLine": "Detail {n} · {type} · {teeth}",
|
||||||
"noLabDetails": "Nog geen lab-afhankelijke details. Voeg een labtype (bijv. endo) toe in de behandeldetails hierboven.",
|
"noLabDetails": "Nog geen lab-afhankelijke details. Voeg een labtype (bijv. endo) toe in de behandeldetails hierboven.",
|
||||||
"labDispatchEmpty": "Voeg een labzending toe om details te groeperen en naar een lab te sturen.",
|
"labDispatchEmpty": "Voeg een labzending toe om details te groeperen en naar een lab te sturen.",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useMemo } from 'react';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { Button } from '@/components/ui/shared/Button';
|
import { Button } from '@/components/ui/shared/Button';
|
||||||
import { Checkbox } from '@/components/ui/shared/Checkbox';
|
import { Checkbox } from '@/components/ui/shared/Checkbox';
|
||||||
@@ -29,6 +30,65 @@ interface LabCasesDispatchPanelProps {
|
|||||||
onSendLabCase: (labCase: LabCaseDraft) => void;
|
onSendLabCase: (labCase: LabCaseDraft) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sentDetailClientIds(labCases: LabCaseDraft[]): Set<string> {
|
||||||
|
const ids = new Set<string>();
|
||||||
|
for (const lc of labCases) {
|
||||||
|
if (!lc.sentAt) continue;
|
||||||
|
for (const id of lc.detailClientIds) ids.add(id);
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
function detailInOtherDraftShipment(
|
||||||
|
detailClientId: string,
|
||||||
|
labCases: LabCaseDraft[],
|
||||||
|
activeLabCaseClientId: string,
|
||||||
|
): boolean {
|
||||||
|
return labCases.some(
|
||||||
|
(lc) =>
|
||||||
|
!lc.sentAt &&
|
||||||
|
lc.clientId !== activeLabCaseClientId &&
|
||||||
|
lc.detailClientIds.includes(detailClientId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Lab-dependent details not yet sent to any lab. */
|
||||||
|
function unsentLabDetails(
|
||||||
|
details: TreatmentDetailDraft[],
|
||||||
|
labCases: LabCaseDraft[],
|
||||||
|
labDependentCodes: Set<string>,
|
||||||
|
): TreatmentDetailDraft[] {
|
||||||
|
const sent = sentDetailClientIds(labCases);
|
||||||
|
return details.filter((d) => labDependentCodes.has(d.treatmentType) && !sent.has(d.clientId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Unsent lab details not already assigned to another draft shipment. */
|
||||||
|
function detailsAvailableForNewShipment(
|
||||||
|
details: TreatmentDetailDraft[],
|
||||||
|
labCases: LabCaseDraft[],
|
||||||
|
labDependentCodes: Set<string>,
|
||||||
|
): TreatmentDetailDraft[] {
|
||||||
|
return unsentLabDetails(details, labCases, labDependentCodes).filter(
|
||||||
|
(d) => !detailInOtherDraftShipment(d.clientId, labCases, ''),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Details the user can pick for the active draft shipment. */
|
||||||
|
function selectableDetailsForDraftShipment(
|
||||||
|
details: TreatmentDetailDraft[],
|
||||||
|
labCases: LabCaseDraft[],
|
||||||
|
labDependentCodes: Set<string>,
|
||||||
|
activeLabCase: LabCaseDraft,
|
||||||
|
): TreatmentDetailDraft[] {
|
||||||
|
const sent = sentDetailClientIds(labCases);
|
||||||
|
return details.filter((d) => {
|
||||||
|
if (!labDependentCodes.has(d.treatmentType)) return false;
|
||||||
|
if (sent.has(d.clientId)) return false;
|
||||||
|
if (activeLabCase.detailClientIds.includes(d.clientId)) return true;
|
||||||
|
return !detailInOtherDraftShipment(d.clientId, labCases, activeLabCase.clientId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function LabCasesDispatchPanel({
|
export function LabCasesDispatchPanel({
|
||||||
details,
|
details,
|
||||||
labCases,
|
labCases,
|
||||||
@@ -58,19 +118,37 @@ export function LabCasesDispatchPanel({
|
|||||||
.map((id) => activeLinkedOrganizations.find((o) => o.id === id))
|
.map((id) => activeLinkedOrganizations.find((o) => o.id === id))
|
||||||
.filter(Boolean) as LinkedOrganizationOption[];
|
.filter(Boolean) as LinkedOrganizationOption[];
|
||||||
|
|
||||||
const labEligibleDetails = details.filter((d) => labDependentCodes.has(d.treatmentType));
|
const labEligibleDetails = useMemo(
|
||||||
|
() => details.filter((d) => labDependentCodes.has(d.treatmentType)),
|
||||||
|
[details, labDependentCodes],
|
||||||
|
);
|
||||||
|
|
||||||
|
const canAddLabShipment = useMemo(
|
||||||
|
() => detailsAvailableForNewShipment(details, labCases, labDependentCodes).length > 0,
|
||||||
|
[details, labCases, labDependentCodes],
|
||||||
|
);
|
||||||
|
|
||||||
const activeLabCase =
|
const activeLabCase =
|
||||||
labCases.find((lc) => lc.clientId === activeLabCaseId) ?? labCases[0] ?? null;
|
labCases.find((lc) => lc.clientId === activeLabCaseId) ?? labCases[0] ?? null;
|
||||||
const sent = Boolean(activeLabCase?.sentAt);
|
const sent = Boolean(activeLabCase?.sentAt);
|
||||||
|
|
||||||
function detailSummary(d: TreatmentDetailDraft, idx: number) {
|
const activeLabOrgName = activeLabCase?.destinationOrganizationId
|
||||||
|
? orgs.find((o) => o.id === activeLabCase.destinationOrganizationId)?.name
|
||||||
|
: null;
|
||||||
|
|
||||||
|
function detailNumber(d: TreatmentDetailDraft) {
|
||||||
|
const idx = details.findIndex((row) => row.clientId === d.clientId);
|
||||||
|
return idx >= 0 ? idx + 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function detailSummary(d: TreatmentDetailDraft) {
|
||||||
const typeKey = treatmentTypeLabelKey(d.treatmentType);
|
const typeKey = treatmentTypeLabelKey(d.treatmentType);
|
||||||
const typeLabel =
|
const typeLabel =
|
||||||
d.treatmentType in TREATMENT_TYPE_KEYS
|
d.treatmentType in TREATMENT_TYPE_KEYS
|
||||||
? t(typeKey as 'typeEndo')
|
? t(typeKey as 'typeEndo')
|
||||||
: d.treatmentType;
|
: d.treatmentType;
|
||||||
const teeth = d.teeth.length ? d.teeth.join(', ') : t('teethNone');
|
const teeth = d.teeth.length ? d.teeth.join(', ') : t('teethNone');
|
||||||
return `${t('detailLabel', { n: idx + 1 })} · ${typeLabel} · ${teeth}`;
|
return `${t('detailLabel', { n: detailNumber(d) })} · ${typeLabel} · ${teeth}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateActiveLabCase(patch: Partial<LabCaseDraft>) {
|
function updateActiveLabCase(patch: Partial<LabCaseDraft>) {
|
||||||
@@ -105,16 +183,6 @@ export function LabCasesDispatchPanel({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function detailAssignedElsewhere(detailClientId: string): boolean {
|
|
||||||
if (!activeLabCase) return false;
|
|
||||||
return labCases.some(
|
|
||||||
(lc) =>
|
|
||||||
!lc.sentAt &&
|
|
||||||
lc.clientId !== activeLabCase.clientId &&
|
|
||||||
lc.detailClientIds.includes(detailClientId),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (labEligibleDetails.length === 0) {
|
if (labEligibleDetails.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="surface-card p-4 space-y-2">
|
<div className="surface-card p-4 space-y-2">
|
||||||
@@ -124,6 +192,15 @@ export function LabCasesDispatchPanel({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const includedInActiveShipment = activeLabCase
|
||||||
|
? labEligibleDetails.filter((d) => activeLabCase.detailClientIds.includes(d.clientId))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const pickableForActiveDraft =
|
||||||
|
activeLabCase && !sent
|
||||||
|
? selectableDetailsForDraftShipment(details, labCases, labDependentCodes, activeLabCase)
|
||||||
|
: [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="surface-card p-4 space-y-4">
|
<div className="surface-card p-4 space-y-4">
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||||
@@ -133,6 +210,7 @@ export function LabCasesDispatchPanel({
|
|||||||
{t('labDispatchSubtitle')} {t('labDispatchSendHint')}
|
{t('labDispatchSubtitle')} {t('labDispatchSendHint')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
{canAddLabShipment && (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
@@ -141,6 +219,7 @@ export function LabCasesDispatchPanel({
|
|||||||
>
|
>
|
||||||
{t('addLabShipment')}
|
{t('addLabShipment')}
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{labCases.length === 0 ? (
|
{labCases.length === 0 ? (
|
||||||
@@ -182,29 +261,80 @@ export function LabCasesDispatchPanel({
|
|||||||
|
|
||||||
{activeLabCase && (
|
{activeLabCase && (
|
||||||
<div className="space-y-4 border border-border/60 rounded-[var(--radius-md)] p-4 bg-background-secondary/30">
|
<div className="space-y-4 border border-border/60 rounded-[var(--radius-md)] p-4 bg-background-secondary/30">
|
||||||
|
{sent ? (
|
||||||
|
<>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-medium text-text-secondary mb-2">{t('includeDetails')}</p>
|
<p className="text-xs font-medium text-text-secondary mb-2">
|
||||||
<div className="flex flex-col gap-2">
|
{t('labShipmentIncludedDetails')}
|
||||||
{labEligibleDetails.map((d, idx) => {
|
</p>
|
||||||
const assignedElsewhere = detailAssignedElsewhere(d.clientId);
|
{includedInActiveShipment.length === 0 ? (
|
||||||
const inSentCase = labCases.some(
|
<p className="text-xs text-text-muted">{t('labShipmentNoIncludedDetails')}</p>
|
||||||
(lc) => lc.sentAt && lc.detailClientIds.includes(d.clientId),
|
) : (
|
||||||
);
|
<ul className="space-y-1.5">
|
||||||
const checked = activeLabCase.detailClientIds.includes(d.clientId);
|
{includedInActiveShipment.map((d) => (
|
||||||
const itemDisabled =
|
<li
|
||||||
disabled || sent || inSentCase || assignedElsewhere;
|
key={d.clientId}
|
||||||
|
className="text-sm text-text-primary rounded-[var(--radius-sm)] border border-border/50 bg-background-secondary/50 px-3 py-2"
|
||||||
|
>
|
||||||
|
{detailSummary(d)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{activeLabCase.labComment.trim() ? (
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-medium text-text-secondary">{t('labComment')}</p>
|
||||||
|
<p className="text-sm text-text-primary mt-1 whitespace-pre-wrap">
|
||||||
|
{activeLabCase.labComment}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{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}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-medium text-text-secondary mb-2">
|
||||||
|
{t('includeDetails')}
|
||||||
|
</p>
|
||||||
|
{pickableForActiveDraft.length === 0 ? (
|
||||||
|
<p className="text-xs text-text-muted">{t('labShipmentNoDetailsAvailable')}</p>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{pickableForActiveDraft.map((d) => {
|
||||||
|
const checked = activeLabCase.detailClientIds.includes(d.clientId);
|
||||||
return (
|
return (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
key={d.clientId}
|
key={d.clientId}
|
||||||
checked={checked || inSentCase}
|
checked={checked}
|
||||||
disabled={itemDisabled}
|
disabled={disabled}
|
||||||
onChange={(next) => toggleDetailInActiveLabCase(d.clientId, next)}
|
onChange={(next) => toggleDetailInActiveLabCase(d.clientId, next)}
|
||||||
label={detailSummary(d, idx)}
|
label={detailSummary(d)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label className="block text-xs font-medium text-text-secondary">
|
<label className="block text-xs font-medium text-text-secondary">
|
||||||
@@ -214,7 +344,7 @@ export function LabCasesDispatchPanel({
|
|||||||
onChange={(e) => updateActiveLabCase({ labComment: e.target.value })}
|
onChange={(e) => updateActiveLabCase({ labComment: e.target.value })}
|
||||||
placeholder={t('labCommentPlaceholder')}
|
placeholder={t('labCommentPlaceholder')}
|
||||||
rows={3}
|
rows={3}
|
||||||
disabled={disabled || sent}
|
disabled={disabled}
|
||||||
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"
|
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"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
@@ -234,7 +364,7 @@ export function LabCasesDispatchPanel({
|
|||||||
<button
|
<button
|
||||||
key={o.id}
|
key={o.id}
|
||||||
type="button"
|
type="button"
|
||||||
disabled={disabled || sent}
|
disabled={disabled}
|
||||||
onClick={() => onRecentOrganizationPick(o.id)}
|
onClick={() => onRecentOrganizationPick(o.id)}
|
||||||
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"
|
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"
|
||||||
>
|
>
|
||||||
@@ -250,7 +380,7 @@ export function LabCasesDispatchPanel({
|
|||||||
destinationOrganizationId: e.target.value || null,
|
destinationOrganizationId: e.target.value || null,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
disabled={disabled || sent || filteredOrganizations.length === 0}
|
disabled={disabled || filteredOrganizations.length === 0}
|
||||||
>
|
>
|
||||||
<option value="">{t('selectLabPlaceholder')}</option>
|
<option value="">{t('selectLabPlaceholder')}</option>
|
||||||
{filteredOrganizations.map((o) => (
|
{filteredOrganizations.map((o) => (
|
||||||
@@ -270,7 +400,6 @@ export function LabCasesDispatchPanel({
|
|||||||
variant="primary"
|
variant="primary"
|
||||||
disabled={
|
disabled={
|
||||||
disabled ||
|
disabled ||
|
||||||
sent ||
|
|
||||||
sendBusyId === activeLabCase.clientId ||
|
sendBusyId === activeLabCase.clientId ||
|
||||||
!activeLabCase.destinationOrganizationId ||
|
!activeLabCase.destinationOrganizationId ||
|
||||||
activeLabCase.detailClientIds.length === 0
|
activeLabCase.detailClientIds.length === 0
|
||||||
@@ -280,23 +409,11 @@ export function LabCasesDispatchPanel({
|
|||||||
>
|
>
|
||||||
{t('sendToLab')}
|
{t('sendToLab')}
|
||||||
</Button>
|
</Button>
|
||||||
{sent && (
|
</div>
|
||||||
<CaseSentLabel
|
</>
|
||||||
treatmentCase={{
|
|
||||||
destinationOrganizationId: activeLabCase.destinationOrganizationId,
|
|
||||||
sendToOrganizationIds: activeLabCase.destinationOrganizationId
|
|
||||||
? [activeLabCase.destinationOrganizationId]
|
|
||||||
: [],
|
|
||||||
sentAt: activeLabCase.sentAt ?? null,
|
|
||||||
sends: activeLabCase.sends,
|
|
||||||
}}
|
|
||||||
orgs={orgs}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user