feat(ui): quick status-action chip menus on offers/orders/projects; ProjectDetail transition buttons
- NEW shared StatusChipMenu: clickable status chip opens a dense menu of valid next states; picks confirm via the shared ConfirmDialog (danger variant, loading, stays open on failure); plain chip without edit permission; row-click-safe (stopPropagation both directions). - Offers list: draft -> Aktivovat (number assignment noted, PDF archived after finalize like the detail); active -> 'Vytvořit objednávku…' (opens the existing create-order modal — 'ordered' stays owned by that flow) + Zneplatnit; ordered -> Zneplatnit. - ReceivedOrders list: Zahájit realizaci / Dokončit / Stornovat / Obnovit with cascade notes; Czech quote pairs fixed („…“); OrderDetail transition button says 'Obnovit' when reopening. - Projects list + ProjectDetail: status combobox replaced by transition buttons (Dokončit/Zrušit/Obnovit projekt) rendered from valid_transitions; cascade notes only when a linked order will actually change; save no longer carries status; busy states symmetric. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,9 @@ import IconButton from "@mui/material/IconButton";
|
||||
import { useAlert } from "../context/AlertContext";
|
||||
import { useAuth } from "../context/AuthContext";
|
||||
import Forbidden from "../components/Forbidden";
|
||||
import StatusChipMenu, {
|
||||
type StatusChipAction,
|
||||
} from "../components/StatusChipMenu";
|
||||
import apiFetch from "../utils/api";
|
||||
import {
|
||||
formatCurrency,
|
||||
@@ -32,7 +35,6 @@ import {
|
||||
Field,
|
||||
TextField,
|
||||
Select,
|
||||
StatusChip,
|
||||
CheckboxField,
|
||||
FileUpload,
|
||||
FilterBar,
|
||||
@@ -180,6 +182,17 @@ export default function OrdersReceived({
|
||||
},
|
||||
});
|
||||
|
||||
// Quick status change from the table chip (StatusChipMenu). The `id` rides
|
||||
// along in the input only to build the URL; UpdateOrderSchema strips it.
|
||||
const statusMutation = useApiMutation<
|
||||
{ id: number; status: string },
|
||||
unknown
|
||||
>({
|
||||
url: ({ id }) => `${API_BASE}/orders/${id}`,
|
||||
method: () => "PUT",
|
||||
invalidate: ["orders", "offers", "projects", "invoices"],
|
||||
});
|
||||
|
||||
const [createForm, setCreateForm] = useState({
|
||||
customer_id: "",
|
||||
customer_order_number: "",
|
||||
@@ -352,6 +365,79 @@ export default function OrdersReceived({
|
||||
return <LoadingState />;
|
||||
}
|
||||
|
||||
// Quick actions for the status chip menu, mirroring the order status
|
||||
// machine (VALID_TRANSITIONS incl. the reopen edges). Rejections propagate
|
||||
// so the ConfirmDialog stays open per app convention; we toast here.
|
||||
const statusActions = (o: Order): StatusChipAction[] => {
|
||||
const changeStatus = (status: string) => async () => {
|
||||
try {
|
||||
await statusMutation.mutateAsync({ id: o.id, status });
|
||||
alert.success("Stav byl změněn");
|
||||
} catch (e) {
|
||||
alert.error(e instanceof Error ? e.message : "Chyba připojení");
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
const stornovat: StatusChipAction = {
|
||||
key: "stornovana",
|
||||
label: "Stornovat",
|
||||
danger: true,
|
||||
confirm: {
|
||||
title: "Stornovat objednávku",
|
||||
message: `Opravdu chcete stornovat objednávku „${o.order_number}“? Propojený projekt bude automaticky zrušen.`,
|
||||
confirmText: "Stornovat",
|
||||
},
|
||||
onAction: changeStatus("stornovana"),
|
||||
};
|
||||
switch (o.status) {
|
||||
case "prijata":
|
||||
return [
|
||||
{
|
||||
key: "v_realizaci",
|
||||
label: "Zahájit realizaci",
|
||||
confirm: {
|
||||
title: "Zahájit realizaci",
|
||||
message: `Opravdu chcete zahájit realizaci objednávky „${o.order_number}“?`,
|
||||
confirmText: "Zahájit realizaci",
|
||||
},
|
||||
onAction: changeStatus("v_realizaci"),
|
||||
},
|
||||
stornovat,
|
||||
];
|
||||
case "v_realizaci":
|
||||
return [
|
||||
{
|
||||
key: "dokoncena",
|
||||
label: "Dokončit",
|
||||
confirm: {
|
||||
title: "Dokončit objednávku",
|
||||
message: `Opravdu chcete dokončit objednávku „${o.order_number}“? Propojený projekt bude automaticky dokončen.`,
|
||||
confirmText: "Dokončit",
|
||||
},
|
||||
onAction: changeStatus("dokoncena"),
|
||||
},
|
||||
stornovat,
|
||||
];
|
||||
case "dokoncena":
|
||||
case "stornovana":
|
||||
// Reopen — deliberately no cascade to the linked project.
|
||||
return [
|
||||
{
|
||||
key: "v_realizaci",
|
||||
label: "Obnovit",
|
||||
confirm: {
|
||||
title: "Obnovit objednávku",
|
||||
message: `Opravdu chcete obnovit objednávku „${o.order_number}“? Objednávka se vrátí do stavu "V realizaci". Propojený projekt zůstane beze změny.`,
|
||||
confirmText: "Obnovit",
|
||||
},
|
||||
onAction: changeStatus("v_realizaci"),
|
||||
},
|
||||
];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const columns: DataColumn<Order>[] = [
|
||||
{
|
||||
key: "order_number",
|
||||
@@ -403,9 +489,11 @@ export default function OrdersReceived({
|
||||
width: "13%",
|
||||
sortKey: "status",
|
||||
render: (o) => (
|
||||
<StatusChip
|
||||
<StatusChipMenu
|
||||
label={statusLabel(ORDER_STATUS, o.status)}
|
||||
color={statusColor(ORDER_STATUS, o.status)}
|
||||
actions={statusActions(o)}
|
||||
disabled={!hasPermission("orders.edit")}
|
||||
/>
|
||||
),
|
||||
},
|
||||
@@ -589,7 +677,7 @@ export default function OrdersReceived({
|
||||
title="Smazat objednávku"
|
||||
message={
|
||||
deleteConfirm.order
|
||||
? `Opravdu chcete smazat objednávku „${deleteConfirm.order.order_number}"? Bude smazán i přidružený projekt. Tato akce je nevratná.`
|
||||
? `Opravdu chcete smazat objednávku „${deleteConfirm.order.order_number}“? Bude smazán i přidružený projekt. Tato akce je nevratná.`
|
||||
: ""
|
||||
}
|
||||
confirmText="Smazat"
|
||||
|
||||
Reference in New Issue
Block a user