feat(drafts): two-button create (Vytvořit / Uložit koncept) + draft finalize UX across offers/invoices/issued-orders
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -65,10 +65,15 @@ import {
|
||||
ISSUED_ORDER_STATUS,
|
||||
statusLabel,
|
||||
statusColor,
|
||||
documentNumberLabel,
|
||||
} from "../lib/documentStatus";
|
||||
|
||||
const API_BASE = "/api/admin";
|
||||
|
||||
// The live (finalized) status for an issued order — assigning it (on create or
|
||||
// on finalizing a draft) makes the backend consume the official PO number.
|
||||
const LIVE_STATUS = "sent";
|
||||
|
||||
// Labels for the buttons that trigger a status transition.
|
||||
const TRANSITION_LABELS: Record<string, string> = {
|
||||
sent: "Odeslat",
|
||||
@@ -627,6 +632,17 @@ export default function IssuedOrderDetail() {
|
||||
customersQuery.isLoading,
|
||||
]);
|
||||
|
||||
// Keep the displayed PO number in sync once it becomes available — a draft
|
||||
// has none until it is finalized (draft → sent), at which point the
|
||||
// invalidated detail query refetches with the freshly-assigned number. The
|
||||
// one-shot hydration effect above won't re-run (dataReady stays true), so
|
||||
// mirror the latest non-empty po_number here.
|
||||
useEffect(() => {
|
||||
if (isEdit && detail?.po_number && detail.po_number !== poNumber) {
|
||||
setPoNumber(detail.po_number);
|
||||
}
|
||||
}, [isEdit, detail?.po_number, poNumber]);
|
||||
|
||||
// Form is editable only in create mode or while draft/sent.
|
||||
const editable = !isEdit || form.status === "draft" || form.status === "sent";
|
||||
const canExport = hasPermission("orders.export");
|
||||
@@ -706,9 +722,7 @@ export default function IssuedOrderDetail() {
|
||||
};
|
||||
|
||||
// ─── Submit (create + edit) ───
|
||||
const handleSubmit = async (e?: React.FormEvent) => {
|
||||
e?.preventDefault();
|
||||
|
||||
const handleSubmit = async (targetStatus?: string) => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
if (!form.customer_id) newErrors.customer_id = "Vyberte dodavatele";
|
||||
if (!form.order_date) newErrors.order_date = "Zadejte datum";
|
||||
@@ -747,11 +761,21 @@ export default function IssuedOrderDetail() {
|
||||
position: i,
|
||||
})),
|
||||
};
|
||||
// Only set status when a target is given (create-as-draft / create-as-live
|
||||
// / finalize). Editing a live order sends no status — the backend keeps
|
||||
// its current status. The PO number is never user-editable.
|
||||
if (targetStatus) payload.status = targetStatus;
|
||||
|
||||
const data = await saveMutation.mutateAsync(payload);
|
||||
alert.success(
|
||||
isEdit ? "Objednávka byla uložena" : "Objednávka byla vytvořena",
|
||||
);
|
||||
// Finalizing a draft (draft → sent) assigns the PO number server-side;
|
||||
// reflect the new status locally so the form switches out of the draft
|
||||
// button branch (the invalidated query refetch repopulates the number).
|
||||
if (isEdit && targetStatus && targetStatus !== "draft") {
|
||||
setForm((prev) => ({ ...prev, status: targetStatus }));
|
||||
}
|
||||
if (!isEdit) navigate(`/orders/issued/${data.id}`);
|
||||
} catch (err) {
|
||||
alert.error(
|
||||
@@ -766,6 +790,16 @@ export default function IssuedOrderDetail() {
|
||||
}
|
||||
};
|
||||
|
||||
// Native form submit (e.g. Enter): route to the right save path. Create →
|
||||
// finalize live; editing a draft → save draft (the visible primary button);
|
||||
// editing a live order → plain save (no status change).
|
||||
const handleFormSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!isEdit) void handleSubmit(LIVE_STATUS);
|
||||
else if (form.status === "draft") void handleSubmit("draft");
|
||||
else void handleSubmit();
|
||||
};
|
||||
|
||||
// ─── Status transition ───
|
||||
const handleStatusChange = async () => {
|
||||
if (!statusConfirm.status) return;
|
||||
@@ -866,12 +900,14 @@ export default function IssuedOrderDetail() {
|
||||
>
|
||||
<Typography variant="h4">
|
||||
{isEdit ? "Objednávka vydaná" : "Nová objednávka vydaná"}
|
||||
{poNumber && (
|
||||
{/* Edit mode: a draft has no PO number yet → show "Koncept".
|
||||
Create mode: show the previewed next-number when available. */}
|
||||
{(isEdit || poNumber) && (
|
||||
<Box
|
||||
component="span"
|
||||
sx={{ color: "text.secondary", ml: 1, fontWeight: 400 }}
|
||||
>
|
||||
{poNumber}
|
||||
{isEdit ? documentNumberLabel(poNumber) : poNumber}
|
||||
</Box>
|
||||
)}
|
||||
</Typography>
|
||||
@@ -901,35 +937,118 @@ export default function IssuedOrderDetail() {
|
||||
Export PDF
|
||||
</Button>
|
||||
)}
|
||||
{editable && (
|
||||
<Button onClick={handleSubmit} disabled={saving}>
|
||||
{saving ? (
|
||||
<>
|
||||
<CircularProgress size={16} color="inherit" sx={{ mr: 1 }} />
|
||||
Ukládání...
|
||||
</>
|
||||
) : (
|
||||
"Uložit"
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
{isEdit &&
|
||||
hasPermission("orders.edit") &&
|
||||
detail?.valid_transitions?.map((status) => (
|
||||
{/* ── Create mode: two-button save ── */}
|
||||
{!isEdit && (
|
||||
<>
|
||||
<Button
|
||||
key={status}
|
||||
onClick={() => setStatusConfirm({ show: true, status })}
|
||||
variant={status === "cancelled" ? "outlined" : "contained"}
|
||||
color={status === "cancelled" ? "error" : "primary"}
|
||||
disabled={statusChanging === status}
|
||||
variant="outlined"
|
||||
color="inherit"
|
||||
onClick={() => handleSubmit("draft")}
|
||||
disabled={saving}
|
||||
>
|
||||
{statusChanging === status ? (
|
||||
<CircularProgress size={14} color="inherit" />
|
||||
{saving ? (
|
||||
<CircularProgress size={16} color="inherit" />
|
||||
) : (
|
||||
TRANSITION_LABELS[status] || status
|
||||
"Uložit koncept"
|
||||
)}
|
||||
</Button>
|
||||
))}
|
||||
<Button
|
||||
onClick={() => handleSubmit(LIVE_STATUS)}
|
||||
disabled={saving}
|
||||
>
|
||||
{saving ? (
|
||||
<>
|
||||
<CircularProgress
|
||||
size={16}
|
||||
color="inherit"
|
||||
sx={{ mr: 1 }}
|
||||
/>
|
||||
Ukládání...
|
||||
</>
|
||||
) : (
|
||||
"Vytvořit"
|
||||
)}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* ── Edit mode, draft: save-draft + finalize (Odeslat). A draft is
|
||||
deleted rather than cancelled, so the generic transition buttons
|
||||
(sent/cancelled) are suppressed — the finalize button IS the
|
||||
draft → sent transition. ── */}
|
||||
{isEdit && form.status === "draft" && (
|
||||
<>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="inherit"
|
||||
onClick={() => handleSubmit("draft")}
|
||||
disabled={saving}
|
||||
>
|
||||
{saving ? (
|
||||
<CircularProgress size={16} color="inherit" />
|
||||
) : (
|
||||
"Uložit koncept"
|
||||
)}
|
||||
</Button>
|
||||
{hasPermission("orders.edit") && (
|
||||
<Button
|
||||
onClick={() => handleSubmit(LIVE_STATUS)}
|
||||
disabled={saving}
|
||||
>
|
||||
{saving ? (
|
||||
<>
|
||||
<CircularProgress
|
||||
size={16}
|
||||
color="inherit"
|
||||
sx={{ mr: 1 }}
|
||||
/>
|
||||
Ukládání...
|
||||
</>
|
||||
) : (
|
||||
"Odeslat"
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* ── Edit mode, live (non-draft) order — unchanged save + transitions ── */}
|
||||
{isEdit && form.status !== "draft" && (
|
||||
<>
|
||||
{editable && (
|
||||
<Button onClick={() => handleSubmit()} disabled={saving}>
|
||||
{saving ? (
|
||||
<>
|
||||
<CircularProgress
|
||||
size={16}
|
||||
color="inherit"
|
||||
sx={{ mr: 1 }}
|
||||
/>
|
||||
Ukládání...
|
||||
</>
|
||||
) : (
|
||||
"Uložit"
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
{hasPermission("orders.edit") &&
|
||||
detail?.valid_transitions?.map((status) => (
|
||||
<Button
|
||||
key={status}
|
||||
onClick={() => setStatusConfirm({ show: true, status })}
|
||||
variant={status === "cancelled" ? "outlined" : "contained"}
|
||||
color={status === "cancelled" ? "error" : "primary"}
|
||||
disabled={statusChanging === status}
|
||||
>
|
||||
{statusChanging === status ? (
|
||||
<CircularProgress size={14} color="inherit" />
|
||||
) : (
|
||||
TRANSITION_LABELS[status] || status
|
||||
)}
|
||||
</Button>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{/* A completed order is terminal/finalized — never deletable from the
|
||||
UI. draft/sent/confirmed/cancelled may be deleted. (Backend
|
||||
rejection of deleting an order is a separate hardening, out of
|
||||
@@ -948,7 +1067,7 @@ export default function IssuedOrderDetail() {
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<form onSubmit={handleFormSubmit}>
|
||||
{/* Basic info */}
|
||||
<Card sx={{ mb: 3 }}>
|
||||
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
|
||||
|
||||
Reference in New Issue
Block a user