Full code review of the case-detail page (14 components) surfaced these,
all fixed here:
Logic bugs
- case-edit-dialog: form.reset ran on the 5s useCase refetch while the dialog
was open, clobbering in-progress edits. Now resets only on open→true.
- status-changer: `selected` never synced to async/external `currentStatus`
(stale dropdown). Reworked to track currentStatus until an explicit pick;
resets to tracking after save.
- decision-blocks-panel: `block.content`/`word_count` accessed without null
guards (endpoint has no response model) → potential render crash. Coerced
with `?? ""` / `?? 0`. `STATUS_LABELS[status]` now falls back to the raw
status instead of rendering literal "undefined".
- document-type-editor: `await mutateAsync()` in async click handlers without
try/catch → unhandled promise rejection. Wrapped (errors still surface via
isError).
Resilience / hygiene
- page.tsx: a transient 5xx on the background poll flipped the WHOLE page to
the error card and discarded loaded data. Now gated on `!data`, plus a
"נסה שוב" retry.
- cases.ts useUpdateCase: invalidated casesKeys.all, which re-invalidated the
detail it had just optimistically patched. Scoped to the list prefix.
RTL correctness (logical properties)
- agent-status-widget `mr-auto`→`ms-auto`; agent-activity-feed `mr-auto`→
`me-auto`, icon `ml-1/ml-2`→`me-1/me-2`, required `*` `mr-1`→`ms-1`;
document-type-editor list `pr-4`→`ps-4`.
Minors
- drafts-panel: `<a href>`→`next/link` (operations + citation links) for SPA
nav. agent-activity-feed: issueMap memoized; comment Textarea aria-label.
upload-sheet: `unknown` status no longer shown as green success (neutral
icon + "רענן לאישור"). citations.ts: case_name typed `string | null`.
Design gate: visual-touching items (RTL gap side, retry button, neutral
upload icon) were chair-authorized via the reviewed-findings approval ("fix
all"); none alter an approved page layout — they are correctness fixes.
tsc clean; eslint clean (1 pre-existing form.watch warning, untouched line).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
86 lines
2.9 KiB
TypeScript
86 lines
2.9 KiB
TypeScript
"use client";
|
||
|
||
import { useState } from "react";
|
||
import { toast } from "sonner";
|
||
import {
|
||
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
|
||
} from "@/components/ui/select";
|
||
import { Button } from "@/components/ui/button";
|
||
import { STATUS_LABELS, STATUS_ICONS } from "@/components/cases/status-badge";
|
||
import { useUpdateCase } from "@/lib/api/cases";
|
||
import { CASE_STATUSES, type CaseStatus } from "@/lib/api/case-status";
|
||
|
||
/*
|
||
* Dropdown for manually overriding the case status — skip a step
|
||
* or revert to an earlier stage. Calls PUT /api/cases/:caseNumber
|
||
* with { status: newValue }. The option list is the SSoT lifecycle.
|
||
*/
|
||
|
||
const ALL_STATUSES: readonly CaseStatus[] = CASE_STATUSES;
|
||
|
||
export function StatusChanger({
|
||
caseNumber,
|
||
currentStatus,
|
||
}: {
|
||
caseNumber: string;
|
||
currentStatus?: CaseStatus;
|
||
}) {
|
||
// `null` = untouched → the dropdown tracks the live `currentStatus` (which
|
||
// arrives async and changes on the 5s poll / external updates). Only an
|
||
// explicit pick overrides it, until save resets back to tracking.
|
||
const [picked, setPicked] = useState<CaseStatus | null>(null);
|
||
const mutate = useUpdateCase(caseNumber);
|
||
const effective: CaseStatus | "" = picked ?? currentStatus ?? "";
|
||
const canSave = Boolean(effective) && effective !== currentStatus;
|
||
|
||
const handleSave = async () => {
|
||
if (!canSave || !effective) return;
|
||
try {
|
||
await mutate.mutateAsync({ status: effective });
|
||
toast.success(`הסטטוס עודכן ל${STATUS_LABELS[effective]}`);
|
||
setPicked(null);
|
||
} catch (e) {
|
||
toast.error(e instanceof Error ? e.message : "שגיאה בעדכון הסטטוס");
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="mt-4 border-t border-rule pt-3 space-y-2">
|
||
<label className="text-[0.72rem] text-ink-muted block">שינוי סטטוס ידני</label>
|
||
<div className="flex items-center gap-2">
|
||
<Select
|
||
value={effective || "__current__"}
|
||
onValueChange={(v) => setPicked(v === "__current__" ? null : v as CaseStatus)}
|
||
dir="rtl"
|
||
>
|
||
<SelectTrigger className="text-[0.75rem] h-8">
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{ALL_STATUSES.map((s) => {
|
||
const Icon = STATUS_ICONS[s];
|
||
return (
|
||
<SelectItem key={s} value={s} className="text-[0.75rem]">
|
||
<span className="inline-flex items-center gap-1.5">
|
||
<Icon className="w-3 h-3 shrink-0" />
|
||
{STATUS_LABELS[s]}
|
||
</span>
|
||
</SelectItem>
|
||
);
|
||
})}
|
||
</SelectContent>
|
||
</Select>
|
||
<Button
|
||
size="sm"
|
||
variant="outline"
|
||
className="h-8 text-[0.72rem] px-3 shrink-0"
|
||
disabled={!canSave || mutate.isPending}
|
||
onClick={handleSave}
|
||
>
|
||
{mutate.isPending ? "שומר…" : "עדכן"}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|