inspection-editor: блок «Заметки механика» в форме нового/in-progress осмотра
Раньше textarea для заметок жил только в InspectionReview (завершённый осмотр). При создании нового планового осмотра (через '/inspections/<id>/edit') поля ввода не было — приходилось завершить осмотр, потом открыть карточку и только там вводить. Теперь блок есть в обеих страницах: - InspectionEditor (in_progress): textarea + кнопки Сохранить/Отменить - InspectionEditor (completed): read-only вывод текста - InspectionReview: оставлен прежний редактор как был Бонус: при нажатии «Завершить осмотр» текущий draft заметки коммитится тем же PATCH вместе со status=completed — не теряется если механик ввёл текст и сразу нажал завершение, не сохранив отдельно.
This commit is contained in:
@@ -125,6 +125,10 @@ export default function InspectionEditor() {
|
|||||||
const [flow, setFlow] = useState<DamageFlow>({ kind: "idle" });
|
const [flow, setFlow] = useState<DamageFlow>({ kind: "idle" });
|
||||||
const [zoneLabels, setZoneLabels] = useState<Record<string, string> | null>(null);
|
const [zoneLabels, setZoneLabels] = useState<Record<string, string> | null>(null);
|
||||||
const [tireWearOpen, setTireWearOpen] = useState(false);
|
const [tireWearOpen, setTireWearOpen] = useState(false);
|
||||||
|
// Local draft заметок механика — синхронизируется с ins.notes когда осмотр
|
||||||
|
// прогружается / обновляется. Кнопка «Сохранить» появляется только когда
|
||||||
|
// draft отличается от сохранённого (см. notesDirty ниже).
|
||||||
|
const [notesDraft, setNotesDraft] = useState<string>("");
|
||||||
|
|
||||||
useEffect(() => { loadZoneLabels().then(setZoneLabels); }, []);
|
useEffect(() => { loadZoneLabels().then(setZoneLabels); }, []);
|
||||||
|
|
||||||
@@ -133,8 +137,18 @@ export default function InspectionEditor() {
|
|||||||
queryFn: () => getInspection(insId),
|
queryFn: () => getInspection(insId),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (ins?.notes !== undefined) setNotesDraft(ins.notes ?? "");
|
||||||
|
}, [ins?.notes]);
|
||||||
|
|
||||||
const finalize = useMutation({
|
const finalize = useMutation({
|
||||||
mutationFn: () => patchInspection(insId, { status: "completed" }),
|
mutationFn: () =>
|
||||||
|
// При завершении осмотра коммитим текст заметки тем же patch'ем —
|
||||||
|
// если механик что-то ввёл и сразу нажал «Завершить», не теряем.
|
||||||
|
patchInspection(insId, {
|
||||||
|
status: "completed",
|
||||||
|
notes: (notesDraft ?? "").trim() || undefined,
|
||||||
|
}),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success("Осмотр завершён");
|
toast.success("Осмотр завершён");
|
||||||
navigate(`/inspections/${insId}`);
|
navigate(`/inspections/${insId}`);
|
||||||
@@ -142,6 +156,15 @@ export default function InspectionEditor() {
|
|||||||
onError: () => toast.error("Не удалось завершить"),
|
onError: () => toast.error("Не удалось завершить"),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const notesMutation = useMutation({
|
||||||
|
mutationFn: (next: string) => patchInspection(insId, { notes: next }),
|
||||||
|
onSuccess: (updated) => {
|
||||||
|
toast.success("Заметка сохранена");
|
||||||
|
qc.setQueryData(["inspection", insId], updated);
|
||||||
|
},
|
||||||
|
onError: () => toast.error("Не удалось сохранить заметку"),
|
||||||
|
});
|
||||||
|
|
||||||
const removeMarker = useMutation({
|
const removeMarker = useMutation({
|
||||||
mutationFn: (markerId: number) => deleteMarker(markerId),
|
mutationFn: (markerId: number) => deleteMarker(markerId),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@@ -445,6 +468,46 @@ export default function InspectionEditor() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<Card className="p-4 space-y-2">
|
||||||
|
<h2 className="text-sm font-semibold text-muted-foreground">
|
||||||
|
Заметки механика
|
||||||
|
</h2>
|
||||||
|
{editable ? (
|
||||||
|
<>
|
||||||
|
<textarea
|
||||||
|
value={notesDraft}
|
||||||
|
onChange={(e) => setNotesDraft(e.target.value)}
|
||||||
|
placeholder="Текст осмотра, замечания, рекомендации…"
|
||||||
|
rows={Math.max(3, Math.min(notesDraft.split("\n").length + 1, 12))}
|
||||||
|
className="w-full text-sm p-2 rounded border border-input bg-background resize-y focus:outline-none focus:ring-2 focus:ring-ring"
|
||||||
|
/>
|
||||||
|
{(notesDraft ?? "") !== (ins.notes ?? "") && (
|
||||||
|
<div className="flex gap-2 justify-end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setNotesDraft(ins.notes ?? "")}
|
||||||
|
className="text-xs text-muted-foreground hover:text-foreground px-3 py-1"
|
||||||
|
disabled={notesMutation.isPending}
|
||||||
|
>
|
||||||
|
Отменить
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => notesMutation.mutate(notesDraft)}
|
||||||
|
disabled={notesMutation.isPending}
|
||||||
|
className="text-xs bg-primary text-primary-foreground rounded px-3 py-1 disabled:opacity-60"
|
||||||
|
>
|
||||||
|
{notesMutation.isPending ? "Сохраняю…" : "Сохранить"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
// Осмотр уже завершён — показываем как read-only текст.
|
||||||
|
<div className="text-sm whitespace-pre-wrap">{ins.notes || "—"}</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
|
||||||
{editable && (
|
{editable && (
|
||||||
<Button
|
<Button
|
||||||
className="w-full"
|
className="w-full"
|
||||||
|
|||||||
Reference in New Issue
Block a user