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:
BOHA
2026-06-09 18:44:10 +02:00
parent ce636215dc
commit e86c2e9a80
4 changed files with 392 additions and 55 deletions

View File

@@ -74,6 +74,7 @@ import {
INVOICE_STATUS,
statusLabel,
statusColor,
documentNumberLabel,
} from "../lib/documentStatus";
const API_BASE = "/api/admin";
@@ -107,6 +108,10 @@ function addDaysLocalStr(days: number, base?: string): string {
const TRANSITION_LABELS: Record<string, string> = { paid: "Zaplaceno" };
// The live (finalized) status for an invoice — assigning it (on create or on
// finalizing a draft) makes the backend consume the official invoice number.
const LIVE_STATUS = "issued";
const BackIcon = (
<svg
width="20"
@@ -943,9 +948,7 @@ export default function InvoiceDetail() {
invalidate: ["invoices", "orders"],
});
const handleCreateSubmit = async (e?: React.FormEvent) => {
e?.preventDefault();
const handleCreateSubmit = async (targetStatus?: string) => {
const newErrors: Record<string, string> = {};
if (!form.customer_id) newErrors.customer_id = "Vyberte zákazníka";
if (!form.issue_date) newErrors.issue_date = "Zadejte datum";
@@ -974,7 +977,14 @@ export default function InvoiceDetail() {
position: i,
})),
};
if (isEdit) payload.invoice_number = invoiceNumber;
// Only set status when a target is given (create-as-draft / create-as-live
// / finalize). Editing a live invoice sends no status — the backend keeps
// its current status.
if (targetStatus) payload.status = targetStatus;
// The invoice number is NOT user-editable for drafts (it is NULL until
// finalized). Send it back only when editing an already-numbered invoice.
if (isEdit && invoice?.status !== "draft")
payload.invoice_number = invoiceNumber;
const data = await saveMutation.mutateAsync(payload);
alert.success(isEdit ? "Faktura byla uložena" : "Faktura byla vytvořena");
@@ -995,6 +1005,16 @@ export default function InvoiceDetail() {
}
};
// 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 invoice → plain save (no status change).
const handleFormSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!isEdit) void handleCreateSubmit(LIVE_STATUS);
else if (invoice?.status === "draft") void handleCreateSubmit("draft");
else void handleCreateSubmit();
};
// ─── Edit mode: status change ───
const handleStatusChange = async () => {
if (!statusConfirm.status) return;
@@ -1098,7 +1118,7 @@ export default function InvoiceDetail() {
}}
>
<Typography variant="h4">
Faktura {invoice.invoice_number}
Faktura {documentNumberLabel(invoice.invoice_number)}
</Typography>
<StatusChip
label={statusLabel(INVOICE_STATUS, invoice.status)}
@@ -1619,7 +1639,7 @@ export default function InvoiceDetail() {
}}
>
<Typography variant="h4">
Faktura {invoice.invoice_number}
Faktura {documentNumberLabel(invoice.invoice_number)}
</Typography>
<StatusChip
label={statusLabel(INVOICE_STATUS, invoice.status)}
@@ -1667,18 +1687,105 @@ export default function InvoiceDetail() {
Zobrazit fakturu
</Button>
)}
<Button onClick={handleCreateSubmit} disabled={saving}>
{saving ? (
<>
<CircularProgress size={16} color="inherit" sx={{ mr: 1 }} />
Ukládání...
</>
) : (
"Uložit"
)}
</Button>
{isEdit && invoice && (
{/* ── Create mode: two-button save ── */}
{!isEdit && (
<>
<Button
variant="outlined"
color="inherit"
onClick={() => handleCreateSubmit("draft")}
disabled={saving}
>
{saving ? (
<CircularProgress size={16} color="inherit" />
) : (
"Uložit koncept"
)}
</Button>
<Button
onClick={() => handleCreateSubmit(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 ── */}
{isEdit && invoice && invoice.status === "draft" && (
<>
<Button
variant="outlined"
color="inherit"
onClick={() => handleCreateSubmit("draft")}
disabled={saving}
>
{saving ? (
<CircularProgress size={16} color="inherit" />
) : (
"Uložit koncept"
)}
</Button>
{hasPermission("invoices.edit") && (
<Button
onClick={() => handleCreateSubmit(LIVE_STATUS)}
disabled={saving}
>
{saving ? (
<>
<CircularProgress
size={16}
color="inherit"
sx={{ mr: 1 }}
/>
Ukládání...
</>
) : (
"Vystavit"
)}
</Button>
)}
{/* A draft is deleted rather than cancelled. */}
{hasPermission("invoices.delete") && (
<Button
onClick={() => setDeleteConfirm(true)}
variant="outlined"
color="error"
>
Smazat
</Button>
)}
</>
)}
{/* ── Edit mode: live (non-draft) doc — unchanged ── */}
{isEdit && invoice && invoice.status !== "draft" && (
<>
<Button onClick={() => handleCreateSubmit()} disabled={saving}>
{saving ? (
<>
<CircularProgress
size={16}
color="inherit"
sx={{ mr: 1 }}
/>
Ukládání...
</>
) : (
"Uložit"
)}
</Button>
{hasPermission("invoices.edit") &&
invoice.valid_transitions?.map((status) => (
<Button
@@ -1723,7 +1830,7 @@ export default function InvoiceDetail() {
</Box>
</Box>
<form onSubmit={handleCreateSubmit}>
<form onSubmit={handleFormSubmit}>
{/* Basic info */}
<Card sx={{ mb: 3 }}>
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>