@flowselections/floriday-verkoop-module 1.0.12 → 1.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-lib/components/verkoop/AutoFulfillmentStatusCard.d.ts +4 -0
- package/dist-lib/components/verkoop/AutoFulfillmentStatusCard.d.ts.map +1 -0
- package/dist-lib/components/verkoop/AutoFulfillmentStatusCard.js +95 -0
- package/dist-lib/components/verkoop/CartAssignmentWizard.d.ts +6 -0
- package/dist-lib/components/verkoop/CartAssignmentWizard.d.ts.map +1 -0
- package/dist-lib/components/verkoop/CartAssignmentWizard.js +194 -0
- package/dist-lib/components/verkoop/CreateOrderPage.d.ts.map +1 -1
- package/dist-lib/components/verkoop/CreateOrderPage.js +14 -4
- package/dist-lib/components/verkoop/OrderAfleverbrievenCard.d.ts +4 -0
- package/dist-lib/components/verkoop/OrderAfleverbrievenCard.d.ts.map +1 -0
- package/dist-lib/components/verkoop/OrderAfleverbrievenCard.js +44 -0
- package/dist-lib/components/verkoop/OrderKarindelingCard.d.ts +5 -0
- package/dist-lib/components/verkoop/OrderKarindelingCard.d.ts.map +1 -0
- package/dist-lib/components/verkoop/OrderKarindelingCard.js +160 -0
- package/dist-lib/components/verkoop/SalesOrderDetailView.d.ts.map +1 -1
- package/dist-lib/components/verkoop/SalesOrderDetailView.js +6 -3
- package/dist-lib/components/verkoop/VerkoopOrdersPage.d.ts.map +1 -1
- package/dist-lib/components/verkoop/VerkoopOrdersPage.js +39 -3
- package/dist-lib/integrations/supabase/auth-middleware.d.ts +605 -4
- package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.d.ts +605 -4
- package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.server.d.ts +605 -4
- package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.server.js +4 -4
- package/dist-lib/integrations/supabase/types.d.ts +613 -4
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/lib/cart-lock.functions.d.ts +11356 -0
- package/dist-lib/lib/cart-lock.functions.d.ts.map +1 -0
- package/dist-lib/lib/cart-lock.functions.js +47 -0
- package/dist-lib/lib/cart-planner.d.ts +38 -0
- package/dist-lib/lib/cart-planner.d.ts.map +1 -0
- package/dist-lib/lib/cart-planner.js +217 -0
- package/dist-lib/lib/cart-replan.functions.d.ts +22729 -0
- package/dist-lib/lib/cart-replan.functions.d.ts.map +1 -0
- package/dist-lib/lib/cart-replan.functions.js +203 -0
- package/dist-lib/lib/create-order.functions.d.ts +26999 -76
- package/dist-lib/lib/create-order.functions.d.ts.map +1 -1
- package/dist-lib/lib/create-order.functions.js +158 -3
- package/dist-lib/lib/floriday-auto-fulfillment.functions.d.ts +34033 -0
- package/dist-lib/lib/floriday-auto-fulfillment.functions.d.ts.map +1 -0
- package/dist-lib/lib/floriday-auto-fulfillment.functions.js +65 -0
- package/dist-lib/lib/floriday-auto-fulfillment.server.d.ts +21 -0
- package/dist-lib/lib/floriday-auto-fulfillment.server.d.ts.map +1 -0
- package/dist-lib/lib/floriday-auto-fulfillment.server.js +352 -0
- package/dist-lib/lib/floriday-logistic-labels.functions.d.ts +11351 -0
- package/dist-lib/lib/floriday-logistic-labels.functions.d.ts.map +1 -0
- package/dist-lib/lib/floriday-logistic-labels.functions.js +171 -0
- package/dist-lib/lib/floriday-public.functions.d.ts +4237 -44
- package/dist-lib/lib/floriday-public.functions.d.ts.map +1 -1
- package/dist-lib/lib/floriday-public.functions.js +45 -47
- package/dist-lib/lib/floriday-writes.functions.d.ts +3025 -20
- package/dist-lib/lib/floriday-writes.functions.d.ts.map +1 -1
- package/dist-lib/lib/salesorders-mapping.d.ts +2 -0
- package/dist-lib/lib/salesorders-mapping.d.ts.map +1 -1
- package/dist-lib/lib/salesorders-mapping.js +46 -20
- package/dist-lib/lib/salesorders.functions.d.ts +1831 -28
- package/dist-lib/lib/salesorders.functions.d.ts.map +1 -1
- package/dist-lib/lib/salesorders.functions.js +8 -0
- package/dist-lib/lib/tradeitems.functions.d.ts +605 -4
- package/dist-lib/lib/tradeitems.functions.d.ts.map +1 -1
- package/dist-lib/lib/useSalesOrdersQuery.d.ts +1 -0
- package/dist-lib/lib/useSalesOrdersQuery.d.ts.map +1 -1
- package/dist-lib/lib/useSalesOrdersQuery.js +121 -39
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AutoFulfillmentStatusCard.d.ts","sourceRoot":"","sources":["../../../src/components/verkoop/AutoFulfillmentStatusCard.tsx"],"names":[],"mappings":"AAyDA,wBAAgB,yBAAyB,CAAC,EAAE,YAAY,EAAE,EAAE;IAAE,YAAY,EAAE,MAAM,CAAA;CAAE,kDA6FnF"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
3
|
+
import { useServerFn } from "@tanstack/react-start";
|
|
4
|
+
import { useEffect } from "react";
|
|
5
|
+
import { supabase } from "../../integrations/supabase/client";
|
|
6
|
+
import { getAutoFulfillmentStatus, retryAutoFulfillment, getPreferredWarehouseStatus, } from "../../lib/floriday-auto-fulfillment.functions";
|
|
7
|
+
import { Button } from "@flowselections/core";
|
|
8
|
+
import { toast } from "sonner";
|
|
9
|
+
const REASON_LABELS = {
|
|
10
|
+
"no-preferred-warehouse": "Er is nog geen voorkeursmagazijn ingesteld op de Floriday-connectie.",
|
|
11
|
+
"no-warehouse": "Geen despatch-magazijn gevonden (voorkeur, salesorder, én cache leeg).",
|
|
12
|
+
"no-delivery-order": "Nog geen delivery order (met fulfillment-requests) ontvangen van Floriday voor deze salesorder.",
|
|
13
|
+
"existing-fulfillment": "Er bestaat al een fulfillment order voor deze salesorder.",
|
|
14
|
+
"sales-order-deleted": "Salesorder is verwijderd bij Floriday.",
|
|
15
|
+
"already-created": "Fulfillment order is al automatisch aangemaakt.",
|
|
16
|
+
"connection-not-found": "Floriday-connectie niet gevonden.",
|
|
17
|
+
"no-packing-fallback": "Belading ontbreekt; er is een enkele kar als fallback gebruikt.",
|
|
18
|
+
"warehouse-fallback:order": "Despatch-magazijn overgenomen uit de salesorder (geen voorkeursmagazijn ingesteld).",
|
|
19
|
+
"warehouse-fallback:cache": "Fallback-magazijn uit cache gebruikt; stel eventueel een voorkeur in bij de Authenticatiemodule.",
|
|
20
|
+
};
|
|
21
|
+
function statusStyle(status) {
|
|
22
|
+
switch (status) {
|
|
23
|
+
case "created":
|
|
24
|
+
return "bg-emerald-50 text-emerald-800 border-emerald-200";
|
|
25
|
+
case "failed":
|
|
26
|
+
return "bg-red-50 text-red-800 border-red-200";
|
|
27
|
+
case "skipped":
|
|
28
|
+
return "bg-amber-50 text-amber-800 border-amber-200";
|
|
29
|
+
default:
|
|
30
|
+
return "bg-slate-50 text-slate-700 border-slate-200";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function statusLabel(status) {
|
|
34
|
+
switch (status) {
|
|
35
|
+
case "created":
|
|
36
|
+
return "Fulfillment order automatisch aangemaakt";
|
|
37
|
+
case "failed":
|
|
38
|
+
return "Automatische fulfillment mislukt";
|
|
39
|
+
case "skipped":
|
|
40
|
+
return "Automatische fulfillment overgeslagen";
|
|
41
|
+
default:
|
|
42
|
+
return "Fulfillment nog niet automatisch aangemaakt";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function AutoFulfillmentStatusCard({ salesOrderId }) {
|
|
46
|
+
const queryClient = useQueryClient();
|
|
47
|
+
const fetchStatus = useServerFn(getAutoFulfillmentStatus);
|
|
48
|
+
const retryFn = useServerFn(retryAutoFulfillment);
|
|
49
|
+
const queryKey = ["auto-fulfillment-status", salesOrderId];
|
|
50
|
+
const { data, isLoading } = useQuery({
|
|
51
|
+
queryKey,
|
|
52
|
+
queryFn: () => fetchStatus({ data: { salesOrderId } }),
|
|
53
|
+
});
|
|
54
|
+
const fetchWarehouse = useServerFn(getPreferredWarehouseStatus);
|
|
55
|
+
const { data: warehouseStatus } = useQuery({
|
|
56
|
+
queryKey: ["preferred-warehouse-status"],
|
|
57
|
+
queryFn: () => fetchWarehouse(),
|
|
58
|
+
staleTime: 60000,
|
|
59
|
+
});
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
const channel = supabase
|
|
62
|
+
.channel(`auto-fulfillment-${salesOrderId}`)
|
|
63
|
+
.on("postgres_changes", {
|
|
64
|
+
event: "*",
|
|
65
|
+
schema: "public",
|
|
66
|
+
table: "floriday_auto_fulfillment_log",
|
|
67
|
+
filter: `sales_order_id=eq.${salesOrderId}`,
|
|
68
|
+
}, () => queryClient.invalidateQueries({ queryKey }))
|
|
69
|
+
.subscribe();
|
|
70
|
+
return () => {
|
|
71
|
+
supabase.removeChannel(channel);
|
|
72
|
+
};
|
|
73
|
+
}, [salesOrderId, queryClient]);
|
|
74
|
+
const mutation = useMutation({
|
|
75
|
+
mutationFn: () => retryFn({ data: { salesOrderId } }),
|
|
76
|
+
onSuccess: (res) => {
|
|
77
|
+
queryClient.invalidateQueries({ queryKey });
|
|
78
|
+
if (res.status === "created")
|
|
79
|
+
toast.success("Fulfillment order aangemaakt");
|
|
80
|
+
else if (res.status === "failed")
|
|
81
|
+
toast.error(`Mislukt: ${res.reason ?? "onbekend"}`);
|
|
82
|
+
else
|
|
83
|
+
toast.info(`Overgeslagen: ${res.reason ?? ""}`);
|
|
84
|
+
},
|
|
85
|
+
onError: (err) => toast.error(err.message),
|
|
86
|
+
});
|
|
87
|
+
if (isLoading)
|
|
88
|
+
return null;
|
|
89
|
+
const row = data?.row ?? null;
|
|
90
|
+
const status = row?.status ?? "pending";
|
|
91
|
+
const canRetry = status === "failed" || status === "skipped" || !row;
|
|
92
|
+
return (_jsx("div", { className: `mt-4 rounded-md border p-3 text-sm ${statusStyle(status)}`, children: _jsxs("div", { className: "flex items-start justify-between gap-3", children: [_jsxs("div", { children: [_jsx("div", { className: "font-medium", children: statusLabel(status) }), row?.fulfillment_order_id ? (_jsxs("div", { className: "mt-1 text-xs opacity-80", children: ["Fulfillment ID: ", _jsx("code", { children: row.fulfillment_order_id })] })) : null, row?.reason ? (_jsx("div", { className: "mt-1 text-xs opacity-80", children: REASON_LABELS[row.reason] ?? row.reason })) : null, row?.error ? (_jsxs("div", { className: "mt-1 text-xs opacity-80", children: ["Fout: ", row.error] })) : null, warehouseStatus &&
|
|
93
|
+
!warehouseStatus.hasPreferredWarehouse &&
|
|
94
|
+
(row?.reason === "no-preferred-warehouse" || row?.reason === "no-warehouse") ? (_jsx("div", { className: "mt-2 rounded border border-amber-300 bg-amber-50 p-2 text-xs text-amber-900", children: "Er is nog geen voorkeursmagazijn ingesteld. Ga naar de Floriday-authenticatiemodule en kies een voorkeursmagazijn \u2014 daarna worden fulfillment orders automatisch aangemaakt." })) : null] }), canRetry ? (_jsx(Button, { size: "sm", variant: "outline", disabled: mutation.isPending, onClick: () => mutation.mutate(), children: mutation.isPending ? "Bezig..." : "Opnieuw proberen" })) : null] }) }));
|
|
95
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CartAssignmentWizard.d.ts","sourceRoot":"","sources":["../../../src/components/verkoop/CartAssignmentWizard.tsx"],"names":[],"mappings":"AA8CA,wBAAgB,oBAAoB,CAAC,EACnC,OAAO,EAAE,IAAI,EAAE,OAAO,GACvB,EAAE;IAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,IAAI,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,IAAI,CAAA;CAAE,2CA4ThE"}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// Karindeling-wizard — identiek aan de Mailbox-module.
|
|
3
|
+
// Gebruikt planAndSaveAutoCarts + saveManualCarts uit create-order.functions,
|
|
4
|
+
// zodat handmatige orders exact dezelfde logica en schermen krijgen als mailbox-orders.
|
|
5
|
+
import { useEffect, useMemo, useState } from "react";
|
|
6
|
+
import { useQuery } from "@tanstack/react-query";
|
|
7
|
+
import { useServerFn } from "@tanstack/react-start";
|
|
8
|
+
import { Button, Badge, Card, CardContent, CardHeader, CardTitle, Input, Label, Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, toast, } from "@flowselections/core";
|
|
9
|
+
import { Plus, Trash2, AlertTriangle, CheckCircle2 } from "lucide-react";
|
|
10
|
+
import { planAndSaveAutoCarts, saveManualCarts } from "../../lib/create-order.functions";
|
|
11
|
+
export function CartAssignmentWizard({ orderId, open, onClose, }) {
|
|
12
|
+
const fetchPlan = useServerFn(planAndSaveAutoCarts);
|
|
13
|
+
const fetchSave = useServerFn(saveManualCarts);
|
|
14
|
+
const { data, isLoading } = useQuery({
|
|
15
|
+
queryKey: ["cart-plan", orderId],
|
|
16
|
+
enabled: !!orderId && open,
|
|
17
|
+
queryFn: async () => {
|
|
18
|
+
try {
|
|
19
|
+
return await fetchPlan({ data: { orderId: orderId } });
|
|
20
|
+
}
|
|
21
|
+
catch (e) {
|
|
22
|
+
toast.error(e?.message ?? "Indelen mislukt");
|
|
23
|
+
return { autoCarts: [], remainders: [], existingManualCarts: [], nextCartNumber: 1 };
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
const autoCarts = Array.isArray(data?.autoCarts) ? data.autoCarts : [];
|
|
28
|
+
const remainders = Array.isArray(data?.remainders) ? data.remainders : [];
|
|
29
|
+
const existingManualCarts = Array.isArray(data?.existingManualCarts) ? data.existingManualCarts : [];
|
|
30
|
+
const [pool, setPool] = useState({});
|
|
31
|
+
const [manual, setManual] = useState([]);
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
const p = {};
|
|
34
|
+
remainders.forEach((r) => { p[r.order_item_id] = r.crates_remaining; });
|
|
35
|
+
setPool(p);
|
|
36
|
+
const remMeta = {};
|
|
37
|
+
remainders.forEach((r) => { remMeta[r.order_item_id] = r; });
|
|
38
|
+
setManual(existingManualCarts.map((c) => ({
|
|
39
|
+
id: crypto.randomUUID(),
|
|
40
|
+
items: c.items.map((i) => {
|
|
41
|
+
const meta = i.order_item_id ? remMeta[i.order_item_id] : null;
|
|
42
|
+
return {
|
|
43
|
+
...i,
|
|
44
|
+
crates_per_layer: meta?.crates_per_layer ?? i.crates_per_layer ?? 1,
|
|
45
|
+
layers_per_cart: meta?.layers_per_cart ?? i.layers_per_cart ?? 1,
|
|
46
|
+
};
|
|
47
|
+
}),
|
|
48
|
+
})));
|
|
49
|
+
}, [data]);
|
|
50
|
+
const remByItem = useMemo(() => {
|
|
51
|
+
const m = {};
|
|
52
|
+
remainders.forEach((r) => { m[r.order_item_id] = r; });
|
|
53
|
+
return m;
|
|
54
|
+
}, [remainders]);
|
|
55
|
+
const addCart = () => setManual((m) => [...m, { id: crypto.randomUUID(), items: [] }]);
|
|
56
|
+
const addItemToCart = (cartId, orderItemId, crates) => {
|
|
57
|
+
const rem = remByItem[orderItemId];
|
|
58
|
+
if (!rem)
|
|
59
|
+
return;
|
|
60
|
+
const available = pool[orderItemId] ?? 0;
|
|
61
|
+
const take = Math.max(0, Math.min(crates, available));
|
|
62
|
+
if (take === 0)
|
|
63
|
+
return;
|
|
64
|
+
setPool((p) => ({ ...p, [orderItemId]: available - take }));
|
|
65
|
+
setManual((carts) => carts.map((c) => {
|
|
66
|
+
if (c.id !== cartId)
|
|
67
|
+
return c;
|
|
68
|
+
const existingIdx = c.items.findIndex((i) => i.order_item_id === orderItemId);
|
|
69
|
+
if (existingIdx >= 0) {
|
|
70
|
+
const next = [...c.items];
|
|
71
|
+
next[existingIdx] = { ...next[existingIdx], crates: next[existingIdx].crates + take };
|
|
72
|
+
return { ...c, items: next };
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
...c,
|
|
76
|
+
items: [...c.items, {
|
|
77
|
+
order_item_id: orderItemId,
|
|
78
|
+
product_id: rem.product_id,
|
|
79
|
+
product_name: rem.product_name,
|
|
80
|
+
crates: take,
|
|
81
|
+
plants_per_crate: rem.plants_per_crate,
|
|
82
|
+
crates_per_layer: rem.crates_per_layer,
|
|
83
|
+
layers_per_cart: rem.layers_per_cart,
|
|
84
|
+
}],
|
|
85
|
+
};
|
|
86
|
+
}));
|
|
87
|
+
};
|
|
88
|
+
const removeItem = (cartId, orderItemId) => {
|
|
89
|
+
setManual((carts) => carts.map((c) => {
|
|
90
|
+
if (c.id !== cartId)
|
|
91
|
+
return c;
|
|
92
|
+
const item = c.items.find((i) => i.order_item_id === orderItemId);
|
|
93
|
+
if (!item)
|
|
94
|
+
return c;
|
|
95
|
+
setPool((p) => ({ ...p, [orderItemId]: (p[orderItemId] ?? 0) + item.crates }));
|
|
96
|
+
return { ...c, items: c.items.filter((i) => i.order_item_id !== orderItemId) };
|
|
97
|
+
}));
|
|
98
|
+
};
|
|
99
|
+
const removeCart = (cartId) => {
|
|
100
|
+
setManual((carts) => {
|
|
101
|
+
const c = carts.find((x) => x.id === cartId);
|
|
102
|
+
if (c) {
|
|
103
|
+
setPool((p) => {
|
|
104
|
+
const np = { ...p };
|
|
105
|
+
c.items.forEach((i) => {
|
|
106
|
+
if (!i.order_item_id)
|
|
107
|
+
return;
|
|
108
|
+
np[i.order_item_id] = (np[i.order_item_id] ?? 0) + i.crates;
|
|
109
|
+
});
|
|
110
|
+
return np;
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return carts.filter((x) => x.id !== cartId);
|
|
114
|
+
});
|
|
115
|
+
};
|
|
116
|
+
const cartCapacity = (cart) => cart.items.length === 0
|
|
117
|
+
? 0
|
|
118
|
+
: Math.max(...cart.items.map((i) => i.crates_per_layer * i.layers_per_cart));
|
|
119
|
+
const cartUsed = (cart) => cart.items.reduce((s, i) => s + i.crates, 0);
|
|
120
|
+
const totalRemainingInPool = Object.values(pool).reduce((s, n) => s + n, 0);
|
|
121
|
+
const handleSave = async () => {
|
|
122
|
+
if (totalRemainingInPool > 0) {
|
|
123
|
+
toast.error(`Nog ${totalRemainingInPool} fust niet ingedeeld — verdeel alles over een kar voordat je opslaat`);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (manual.some((c) => c.items.length === 0)) {
|
|
127
|
+
toast.error("Lege karren niet toegestaan");
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const overfull = manual.find((c) => cartUsed(c) > cartCapacity(c));
|
|
131
|
+
if (overfull) {
|
|
132
|
+
toast.error("Een kar overschrijdt de capaciteit");
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
await fetchSave({
|
|
137
|
+
data: {
|
|
138
|
+
orderId: orderId,
|
|
139
|
+
carts: manual.map((c) => ({
|
|
140
|
+
is_complete: cartUsed(c) >= cartCapacity(c),
|
|
141
|
+
items: c.items.map((i) => ({
|
|
142
|
+
order_item_id: i.order_item_id,
|
|
143
|
+
product_id: i.product_id,
|
|
144
|
+
product_name: i.product_name,
|
|
145
|
+
crates: Math.max(1, Math.trunc(i.crates)),
|
|
146
|
+
plants_per_crate: Math.max(1, Math.trunc(i.plants_per_crate || 1)),
|
|
147
|
+
crates_per_layer: Math.max(1, Math.trunc(i.crates_per_layer || 1)),
|
|
148
|
+
})),
|
|
149
|
+
})),
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
toast.success("Karindeling opgeslagen");
|
|
153
|
+
onClose();
|
|
154
|
+
}
|
|
155
|
+
catch (e) {
|
|
156
|
+
let msg = "Opslaan mislukt";
|
|
157
|
+
if (e instanceof Response) {
|
|
158
|
+
try {
|
|
159
|
+
msg = (await e.text()) || `HTTP ${e.status}`;
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
msg = `HTTP ${e.status}`;
|
|
163
|
+
}
|
|
164
|
+
if (e.status === 401)
|
|
165
|
+
msg = "Sessie verlopen — log opnieuw in";
|
|
166
|
+
}
|
|
167
|
+
else if (e?.message) {
|
|
168
|
+
msg = e.message;
|
|
169
|
+
}
|
|
170
|
+
toast.error(msg);
|
|
171
|
+
console.error("saveManualCarts failed:", e);
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
return (_jsx(Dialog, { open: open, onOpenChange: (o) => { if (!o)
|
|
175
|
+
onClose(); }, children: _jsxs(DialogContent, { className: "max-w-[95vw] w-[95vw] h-[92vh] max-h-[92vh] overflow-y-auto p-6", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: "Karindeling" }) }), isLoading ? (_jsx("div", { className: "text-muted-foreground", children: "Karren worden berekend..." })) : (_jsxs("div", { className: "space-y-6", children: [_jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between", children: [_jsx(CardTitle, { className: "text-base", children: "Automatisch ingedeelde karren" }), _jsxs(Badge, { variant: "secondary", children: [autoCarts.length, " kar(ren)"] })] }), _jsx(CardContent, { children: autoCarts.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Geen volledige karren mogelijk." })) : (_jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Kar #" }), _jsx(TableHead, { children: "Artikel" }), _jsx(TableHead, { className: "text-right", children: "Fusten" }), _jsx(TableHead, { className: "text-right", children: "Planten" }), _jsx(TableHead, { className: "text-right", children: "Bezetting" }), _jsx(TableHead, { children: "Status" })] }) }), _jsx(TableBody, { children: autoCarts.map((c) => (_jsxs(TableRow, { children: [_jsx(TableCell, { className: "font-mono", children: c.cart_number }), _jsx(TableCell, { children: c.product_name }), _jsx(TableCell, { className: "text-right", children: c.crates }), _jsx(TableCell, { className: "text-right", children: c.plants }), _jsx(TableCell, { className: "text-right", children: "100%" }), _jsx(TableCell, { children: _jsxs(Badge, { className: "gap-1", children: [_jsx(CheckCircle2, { className: "h-3 w-3" }), " Volledig"] }) })] }, c.cart_number))) })] })) })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: "Handmatig in te delen fusten" }) }), _jsx(CardContent, { className: "space-y-3", children: remainders.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Geen restant \u2014 alles past volledig op karren." })) : (_jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Artikel" }), _jsx(TableHead, { className: "text-right", children: "Beschikbaar (fusten)" }), _jsx(TableHead, { className: "text-right", children: "Planten/fust" }), _jsx(TableHead, { className: "text-right", children: "Capaciteit/kar" })] }) }), _jsx(TableBody, { children: remainders.map((r) => (_jsxs(TableRow, { children: [_jsx(TableCell, { children: r.product_name }), _jsxs(TableCell, { className: "text-right font-mono", children: [pool[r.order_item_id] ?? 0, " / ", r.crates_remaining] }), _jsx(TableCell, { className: "text-right", children: r.plants_per_crate }), _jsx(TableCell, { className: "text-right", children: r.crates_per_layer * r.layers_per_cart })] }, r.order_item_id))) })] })) })] }), remainders.length > 0 && (_jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between", children: [_jsx(CardTitle, { className: "text-base", children: "Restant-karren samenstellen" }), _jsxs(Button, { size: "sm", onClick: addCart, children: [_jsx(Plus, { className: "h-3 w-3 mr-1" }), " Kar toevoegen"] })] }), _jsxs(CardContent, { className: "space-y-3", children: [manual.length === 0 && (_jsx("p", { className: "text-sm text-muted-foreground", children: "Voeg een kar toe om restant-fusten in te delen." })), manual.map((cart, idx) => {
|
|
176
|
+
const used = cartUsed(cart);
|
|
177
|
+
const cap = cartCapacity(cart);
|
|
178
|
+
const pct = cap > 0 ? Math.round((used / cap) * 100) : 0;
|
|
179
|
+
const over = cap > 0 && used > cap;
|
|
180
|
+
return (_jsxs(Card, { className: over ? "border-destructive" : "", children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between py-3", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsxs("span", { className: "font-medium", children: ["Restant-kar ", idx + 1] }), _jsxs(Badge, { variant: over ? "destructive" : "secondary", children: [used, " / ", cap || "?", " fusten (", pct, "%)"] }), over && (_jsxs("span", { className: "text-xs text-destructive flex items-center gap-1", children: [_jsx(AlertTriangle, { className: "h-3 w-3" }), " Capaciteit overschreden"] }))] }), _jsx(Button, { variant: "ghost", size: "icon", onClick: () => removeCart(cart.id), children: _jsx(Trash2, { className: "h-4 w-4" }) })] }), _jsxs(CardContent, { className: "space-y-2", children: [cart.items.map((i) => (_jsxs("div", { className: "flex items-center justify-between text-sm", children: [_jsxs("div", { children: [_jsx("span", { className: "font-medium", children: i.product_name }), _jsxs("span", { className: "text-muted-foreground", children: [" \u00B7 ", i.crates, " fust (", i.crates * i.plants_per_crate, " planten)"] })] }), _jsx(Button, { variant: "ghost", size: "sm", onClick: () => removeItem(cart.id, i.order_item_id), children: _jsx(Trash2, { className: "h-3 w-3" }) })] }, i.order_item_id))), _jsx(AddItemRow, { remainders: remainders, pool: pool, onAdd: (itemId, crates) => addItemToCart(cart.id, itemId, crates) })] })] }, cart.id));
|
|
181
|
+
})] })] }))] })), _jsxs(DialogFooter, { className: "gap-2", children: [_jsx(Button, { variant: "outline", onClick: onClose, children: "Annuleren" }), _jsxs(Button, { onClick: handleSave, disabled: isLoading || totalRemainingInPool > 0, children: ["Karindeling opslaan", totalRemainingInPool > 0 && (_jsxs(Badge, { variant: "destructive", className: "ml-2", children: ["Nog ", totalRemainingInPool, " fust in te delen"] }))] })] })] }) }));
|
|
182
|
+
}
|
|
183
|
+
function AddItemRow({ remainders, pool, onAdd, }) {
|
|
184
|
+
const available = remainders.filter((r) => (pool[r.order_item_id] ?? 0) > 0);
|
|
185
|
+
const [sel, setSel] = useState("");
|
|
186
|
+
const [qty, setQty] = useState(1);
|
|
187
|
+
if (available.length === 0)
|
|
188
|
+
return null;
|
|
189
|
+
return (_jsxs("div", { className: "flex items-end gap-2 pt-2 border-t", children: [_jsxs("div", { className: "flex-1", children: [_jsx(Label, { className: "text-xs", children: "Artikel" }), _jsxs(Select, { value: sel, onValueChange: setSel, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Kies restant-artikel..." }) }), _jsx(SelectContent, { children: available.map((r) => (_jsxs(SelectItem, { value: r.order_item_id, children: [r.product_name, " (", pool[r.order_item_id], " fust beschikbaar)"] }, r.order_item_id))) })] })] }), _jsxs("div", { className: "w-24", children: [_jsx(Label, { className: "text-xs", children: "Fusten" }), _jsx(Input, { type: "number", min: 1, value: qty, onChange: (e) => setQty(Math.max(1, Number(e.target.value) || 1)) })] }), _jsxs(Button, { size: "sm", disabled: !sel, onClick: () => { if (sel) {
|
|
190
|
+
onAdd(sel, qty);
|
|
191
|
+
setSel("");
|
|
192
|
+
setQty(1);
|
|
193
|
+
} }, children: [_jsx(Plus, { className: "h-3 w-3 mr-1" }), " Toevoegen"] })] }));
|
|
194
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CreateOrderPage.d.ts","sourceRoot":"","sources":["../../../src/components/verkoop/CreateOrderPage.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"CreateOrderPage.d.ts","sourceRoot":"","sources":["../../../src/components/verkoop/CreateOrderPage.tsx"],"names":[],"mappings":"AA6CA,wBAAgB,eAAe,4CA+b9B"}
|
|
@@ -11,6 +11,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
|
11
11
|
import { ArrowLeft, Plus, Trash2 } from "lucide-react";
|
|
12
12
|
import { useServerFn } from "@tanstack/react-start";
|
|
13
13
|
import { SearchableSelect } from "./SearchableSelect";
|
|
14
|
+
import { CartAssignmentWizard } from "./CartAssignmentWizard";
|
|
14
15
|
import { listCustomersForCreateOrder, listProductsForCreateOrder, listDeliveryLocationsForCustomerCO, listPickupLocationsCO, listCarriersForCreateOrder, getOrderPickDaysCO, createManualOrder, } from "../../lib/create-order.functions";
|
|
15
16
|
function ReadOnlyField({ label, value }) {
|
|
16
17
|
return (_jsxs("div", { className: "space-y-0.5", children: [_jsx("div", { className: "text-[11px] uppercase tracking-wide text-muted-foreground", children: label }), _jsx("div", { className: "text-sm", children: value && String(value).trim() !== "" ? value : _jsx("span", { className: "text-muted-foreground", children: "\u2014" }) })] }));
|
|
@@ -60,6 +61,7 @@ export function CreateOrderPage() {
|
|
|
60
61
|
const [lines, setLines] = useState([
|
|
61
62
|
{ product_id: null, product_name: "", quantity: null, unit: "stuks", unit_price: null },
|
|
62
63
|
]);
|
|
64
|
+
const [cartOrderId, setCartOrderId] = useState(null);
|
|
63
65
|
// Afleverlocaties van gekozen klant
|
|
64
66
|
const deliveryLocQ = useQuery({
|
|
65
67
|
queryKey: ["co-delivery-locations", customerId],
|
|
@@ -167,10 +169,15 @@ export function CreateOrderPage() {
|
|
|
167
169
|
};
|
|
168
170
|
return await fetchCreate({ data: payload });
|
|
169
171
|
},
|
|
170
|
-
onSuccess: () => {
|
|
171
|
-
toast.success("Bestelling aangemaakt");
|
|
172
|
+
onSuccess: (res) => {
|
|
173
|
+
toast.success("Bestelling aangemaakt — deel nu de karren in");
|
|
172
174
|
qc.invalidateQueries({ queryKey: ["floriday", "salesorders-cache"] });
|
|
173
|
-
|
|
175
|
+
if (res?.orderId) {
|
|
176
|
+
setCartOrderId(res.orderId);
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
navigate({ to: "/floriday-verkoop/verkooporders" });
|
|
180
|
+
}
|
|
174
181
|
},
|
|
175
182
|
onError: (e) => toast.error(e?.message ?? "Aanmaken mislukt"),
|
|
176
183
|
});
|
|
@@ -242,5 +249,8 @@ export function CreateOrderPage() {
|
|
|
242
249
|
next[idx] = { ...next[idx], unit_price: v };
|
|
243
250
|
setLines(next);
|
|
244
251
|
}, placeholder: "\u20AC prijs" }), _jsx(Button, { variant: "ghost", size: "icon", className: "col-span-1", onClick: () => setLines(lines.filter((_, i) => i !== idx)), "aria-label": "Regel verwijderen", children: _jsx(Trash2, { className: "h-4 w-4" }) })] }, idx));
|
|
245
|
-
}), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setLines([...lines, { product_id: null, product_name: "", quantity: null, unit: "stuks", unit_price: null }]), children: [_jsx(Plus, { className: "h-3 w-3 mr-1" }), " Regel toevoegen"] })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { children: "Opmerkingen" }), _jsx(Textarea, { value: notes, onChange: (e) => setNotes(e.target.value), placeholder: "Optionele notities bij deze bestelling", rows: 3 })] }), _jsxs("div", { className: "flex items-center justify-end gap-2 pt-2", children: [_jsx(Button, { variant: "outline", onClick: () => navigate({ to: "/floriday-verkoop/verkooporders" }), disabled: createMut.isPending, children: "Annuleren" }), _jsx(Button, { onClick: () => createMut.mutate(), disabled: createMut.isPending, children: createMut.isPending ? "Bezig..." : "Bestelling Aanmaken" })] })] })] })
|
|
252
|
+
}), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setLines([...lines, { product_id: null, product_name: "", quantity: null, unit: "stuks", unit_price: null }]), children: [_jsx(Plus, { className: "h-3 w-3 mr-1" }), " Regel toevoegen"] })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { children: "Opmerkingen" }), _jsx(Textarea, { value: notes, onChange: (e) => setNotes(e.target.value), placeholder: "Optionele notities bij deze bestelling", rows: 3 })] }), _jsxs("div", { className: "flex items-center justify-end gap-2 pt-2", children: [_jsx(Button, { variant: "outline", onClick: () => navigate({ to: "/floriday-verkoop/verkooporders" }), disabled: createMut.isPending, children: "Annuleren" }), _jsx(Button, { onClick: () => createMut.mutate(), disabled: createMut.isPending, children: createMut.isPending ? "Bezig..." : "Bestelling Aanmaken" })] })] })] }), _jsx(CartAssignmentWizard, { orderId: cartOrderId, open: !!cartOrderId, onClose: () => {
|
|
253
|
+
setCartOrderId(null);
|
|
254
|
+
navigate({ to: "/floriday-verkoop/verkooporders" });
|
|
255
|
+
} })] }));
|
|
246
256
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OrderAfleverbrievenCard.d.ts","sourceRoot":"","sources":["../../../src/components/verkoop/OrderAfleverbrievenCard.tsx"],"names":[],"mappings":"AA+BA,wBAAgB,uBAAuB,CAAC,EAAE,YAAY,EAAE,EAAE;IAAE,YAAY,EAAE,MAAM,CAAA;CAAE,2CA4FjF"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useQuery } from "@tanstack/react-query";
|
|
3
|
+
import { useServerFn } from "@tanstack/react-start";
|
|
4
|
+
import { Badge, Button, Card, CardContent, CardHeader, CardTitle } from "@flowselections/core";
|
|
5
|
+
import { FileText, Printer, ExternalLink, RefreshCw } from "lucide-react";
|
|
6
|
+
import { listLogisticLabelsForOrder } from "../../lib/floriday-logistic-labels.functions";
|
|
7
|
+
const LC_SHORT = {
|
|
8
|
+
DANISH_TROLLEY: "DC",
|
|
9
|
+
DANISH_CONTAINER: "DC",
|
|
10
|
+
CC_CONTAINER: "CC",
|
|
11
|
+
CC_TROLLEY: "CC",
|
|
12
|
+
EURO_PALLET: "EP",
|
|
13
|
+
BLOCK_PALLET: "BP",
|
|
14
|
+
BOX: "BX",
|
|
15
|
+
};
|
|
16
|
+
function printPdf(url) {
|
|
17
|
+
const w = window.open(url, "_blank");
|
|
18
|
+
if (!w)
|
|
19
|
+
return;
|
|
20
|
+
// Kick off print dialog once loaded (browsers may block cross-origin, then user prints from tab)
|
|
21
|
+
const timer = setTimeout(() => {
|
|
22
|
+
try {
|
|
23
|
+
w.focus();
|
|
24
|
+
w.print();
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
/* ignore */
|
|
28
|
+
}
|
|
29
|
+
}, 1200);
|
|
30
|
+
w.addEventListener?.("beforeunload", () => clearTimeout(timer));
|
|
31
|
+
}
|
|
32
|
+
export function OrderAfleverbrievenCard({ salesOrderId }) {
|
|
33
|
+
const fetchLabels = useServerFn(listLogisticLabelsForOrder);
|
|
34
|
+
const { data, isLoading, error, refetch, isFetching } = useQuery({
|
|
35
|
+
queryKey: ["floriday-logistic-labels", salesOrderId],
|
|
36
|
+
queryFn: () => fetchLabels({ data: { salesOrderId } }),
|
|
37
|
+
staleTime: 60000,
|
|
38
|
+
});
|
|
39
|
+
const labels = data?.labels ?? [];
|
|
40
|
+
return (_jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between", children: [_jsx(CardTitle, { className: "text-base", children: "Afleverbrieven" }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs(Badge, { variant: "secondary", children: [labels.length, " brief(en)"] }), _jsx(Button, { variant: "ghost", size: "sm", onClick: () => refetch(), disabled: isFetching, title: "Opnieuw ophalen bij Floriday", children: _jsx(RefreshCw, { className: `h-4 w-4 ${isFetching ? "animate-spin" : ""}` }) })] })] }), _jsxs(CardContent, { children: [isLoading && (_jsx("p", { className: "text-sm text-muted-foreground", children: "Afleverbrieven ophalen\u2026" })), error && (_jsx("p", { className: "text-sm text-destructive", children: error.message })), !isLoading && !error && labels.length === 0 && (_jsx("p", { className: "text-sm text-muted-foreground", children: "Nog geen afleverbrief beschikbaar (order nog niet vrijgegeven door Floriday)." })), labels.length > 0 && (_jsx("div", { className: "divide-y divide-border rounded-md border border-border", children: labels.map((l) => {
|
|
41
|
+
const lcShort = l.loadCarrierType ? LC_SHORT[l.loadCarrierType] ?? l.loadCarrierType.slice(0, 2) : "";
|
|
42
|
+
return (_jsxs("div", { className: "flex items-center justify-between gap-4 px-3 py-2 text-sm", children: [_jsxs("div", { className: "flex items-center gap-3 min-w-0", children: [_jsx(FileText, { className: "h-4 w-4 shrink-0 text-muted-foreground" }), _jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "font-mono font-medium", children: [l.code, l.letter ? _jsxs("span", { className: "ml-1 text-primary", children: ["\u00B7", l.letter] }) : null] }), _jsxs("div", { className: "text-xs text-muted-foreground", children: ["Kar ", l.loadCarrierIndex, lcShort ? ` · ${lcShort}` : "", l.packages ? ` · ${l.packages} fusten` : ""] })] })] }), _jsx("div", { className: "flex items-center gap-2", children: l.hasPdf && l.signedUrl ? (_jsxs(_Fragment, { children: [_jsx(Button, { asChild: true, variant: "outline", size: "sm", children: _jsxs("a", { href: l.signedUrl, target: "_blank", rel: "noreferrer", children: [_jsx(ExternalLink, { className: "mr-1 h-3.5 w-3.5" }), "Openen"] }) }), _jsxs(Button, { variant: "default", size: "sm", onClick: () => printPdf(l.signedUrl), children: [_jsx(Printer, { className: "mr-1 h-3.5 w-3.5" }), "Printen"] })] })) : (_jsx("span", { className: "text-xs text-muted-foreground", children: "PDF nog niet vrijgegeven" })) })] }, `${l.fulfillmentOrderId}-${l.code}-${l.letter ?? ""}`));
|
|
43
|
+
}) }))] })] }));
|
|
44
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OrderKarindelingCard.d.ts","sourceRoot":"","sources":["../../../src/components/verkoop/OrderKarindelingCard.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AA6F/D,wBAAgB,oBAAoB,CAAC,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE,aAAa,CAAA;CAAE,2CA+KvE"}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// Toont karindeling per order. De belading (planten×fusten×lagen) komt
|
|
3
|
+
// ALTIJD uit de producten in Voorraad (tabel `products`) — nooit uit de
|
|
4
|
+
// Floriday-packingConfiguration — zodat verkoop en voorraad synchroon zijn.
|
|
5
|
+
// Alleen wanneer de order geen `product_id`-koppeling heeft, valt de kaart
|
|
6
|
+
// terug op de Floriday packingConfiguration.
|
|
7
|
+
import { useEffect, useMemo, useState } from "react";
|
|
8
|
+
import { Badge, Card, CardContent, CardHeader, CardTitle, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@flowselections/core";
|
|
9
|
+
import { CheckCircle2, Lock } from "lucide-react";
|
|
10
|
+
import { supabase } from "../../integrations/supabase/client";
|
|
11
|
+
import { planCarts } from "../../lib/cart-planner";
|
|
12
|
+
function deriveCartsFromFloriday(order) {
|
|
13
|
+
const o = order.raw.order ?? {};
|
|
14
|
+
const pack = o?.packingConfiguration ?? {};
|
|
15
|
+
const ppc = Math.max(1, Number(pack.piecesPerPackage ?? order.quantity.count ?? 1));
|
|
16
|
+
const cpl = Math.max(1, Number(pack.packagesPerLayer ?? 1));
|
|
17
|
+
const lpc = Math.max(1, Number(pack.layersPerLoadCarrier ?? 1));
|
|
18
|
+
const totalPieces = Math.max(0, Number(o.numberOfPieces ?? order.totalPieces ?? 0));
|
|
19
|
+
if (totalPieces <= 0)
|
|
20
|
+
return [];
|
|
21
|
+
const totalCrates = Math.ceil(totalPieces / ppc);
|
|
22
|
+
const cratesPerCart = cpl * lpc;
|
|
23
|
+
const fullCarts = Math.floor(totalCrates / cratesPerCart);
|
|
24
|
+
const remainderCrates = totalCrates - fullCarts * cratesPerCart;
|
|
25
|
+
const carts = [];
|
|
26
|
+
let n = 1;
|
|
27
|
+
for (let i = 0; i < fullCarts; i++) {
|
|
28
|
+
carts.push({
|
|
29
|
+
cart_number: n++,
|
|
30
|
+
is_complete: true,
|
|
31
|
+
occupancy_pct: 100,
|
|
32
|
+
total_layers_used: lpc,
|
|
33
|
+
items: [{
|
|
34
|
+
product_name: order.productName,
|
|
35
|
+
crates: cratesPerCart,
|
|
36
|
+
plants: cratesPerCart * ppc,
|
|
37
|
+
plants_per_crate: ppc,
|
|
38
|
+
crates_per_layer: cpl,
|
|
39
|
+
layers_per_cart: lpc,
|
|
40
|
+
layer_share_pct: 100,
|
|
41
|
+
}],
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
if (remainderCrates > 0) {
|
|
45
|
+
const layersUsed = Math.ceil(remainderCrates / cpl);
|
|
46
|
+
const occ = Math.round(((layersUsed / lpc) * 100) * 100) / 100;
|
|
47
|
+
carts.push({
|
|
48
|
+
cart_number: n,
|
|
49
|
+
is_complete: false,
|
|
50
|
+
occupancy_pct: occ,
|
|
51
|
+
total_layers_used: layersUsed,
|
|
52
|
+
items: [{
|
|
53
|
+
product_name: order.productName,
|
|
54
|
+
crates: remainderCrates,
|
|
55
|
+
plants: remainderCrates * ppc,
|
|
56
|
+
plants_per_crate: ppc,
|
|
57
|
+
crates_per_layer: cpl,
|
|
58
|
+
layers_per_cart: lpc,
|
|
59
|
+
layer_share_pct: occ,
|
|
60
|
+
}],
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return carts;
|
|
64
|
+
}
|
|
65
|
+
function belading(i) {
|
|
66
|
+
return `${i.plants_per_crate}×${i.crates_per_layer}×${i.layers_per_cart}`;
|
|
67
|
+
}
|
|
68
|
+
export function OrderKarindelingCard({ order }) {
|
|
69
|
+
// Lokale/mailbox-orders hebben een prefix (`local:`, `mailbox-proposal:`) in
|
|
70
|
+
// hun DTO-id. De DB-tabellen (`orders`, `order_items`) gebruiken de kale UUID,
|
|
71
|
+
// dus die prefix moet eraf voor de query — anders faalt de UUID-cast en valt
|
|
72
|
+
// de kaart terug op de Floriday-modus met slechts één artikel.
|
|
73
|
+
const rawId = order?.id ?? order.raw?.order?.id ?? null;
|
|
74
|
+
const orderId = typeof rawId === "string" ? rawId.replace(/^(local:|mailbox-proposal:)/, "") : rawId;
|
|
75
|
+
const [lockedAt, setLockedAt] = useState(null);
|
|
76
|
+
const [dbItems, setDbItems] = useState(null);
|
|
77
|
+
// Laadt order_items + gekoppelde products (voorraad-belading) en
|
|
78
|
+
// ververst live bij mutaties op order_items, orders of products.
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (!orderId)
|
|
81
|
+
return;
|
|
82
|
+
let active = true;
|
|
83
|
+
const load = async () => {
|
|
84
|
+
const [{ data: itemRows }, { data: orderRow }] = await Promise.all([
|
|
85
|
+
supabase
|
|
86
|
+
.from("order_items")
|
|
87
|
+
.select("id, product_id, product_name, quantity, products(plants_per_crate, crates_per_layer, layers_per_cart)")
|
|
88
|
+
.eq("order_id", orderId),
|
|
89
|
+
supabase.from("orders").select("plan_locked_at").eq("id", orderId).maybeSingle(),
|
|
90
|
+
]);
|
|
91
|
+
if (!active)
|
|
92
|
+
return;
|
|
93
|
+
setDbItems(itemRows ?? []);
|
|
94
|
+
setLockedAt(orderRow?.plan_locked_at ?? null);
|
|
95
|
+
};
|
|
96
|
+
void load();
|
|
97
|
+
const channel = supabase
|
|
98
|
+
.channel(`order-karindeling-${orderId}`)
|
|
99
|
+
.on("postgres_changes", { event: "UPDATE", schema: "public", table: "orders", filter: `id=eq.${orderId}` }, (payload) => setLockedAt(payload.new?.plan_locked_at ?? null))
|
|
100
|
+
.on("postgres_changes", { event: "*", schema: "public", table: "order_items", filter: `order_id=eq.${orderId}` }, () => void load())
|
|
101
|
+
.on("postgres_changes", { event: "UPDATE", schema: "public", table: "products" }, () => void load())
|
|
102
|
+
.subscribe();
|
|
103
|
+
return () => {
|
|
104
|
+
active = false;
|
|
105
|
+
void supabase.removeChannel(channel);
|
|
106
|
+
};
|
|
107
|
+
}, [orderId]);
|
|
108
|
+
// Bouw plannerinput uit voorraad-belading. Alleen items met een gekoppeld
|
|
109
|
+
// product (product_id + belading in products-tabel) worden meegenomen.
|
|
110
|
+
const plannerCarts = useMemo(() => {
|
|
111
|
+
if (!dbItems || dbItems.length === 0)
|
|
112
|
+
return null;
|
|
113
|
+
const inputs = [];
|
|
114
|
+
for (const it of dbItems) {
|
|
115
|
+
const p = it.products;
|
|
116
|
+
if (!it.product_id || !p)
|
|
117
|
+
continue;
|
|
118
|
+
const ppc = Math.max(1, Number(p.plants_per_crate ?? 0));
|
|
119
|
+
const cpl = Math.max(1, Number(p.crates_per_layer ?? 0));
|
|
120
|
+
const lpc = Math.max(1, Number(p.layers_per_cart ?? 0));
|
|
121
|
+
const plants = Math.max(0, Number(it.quantity ?? 0));
|
|
122
|
+
if (plants <= 0)
|
|
123
|
+
continue;
|
|
124
|
+
inputs.push({
|
|
125
|
+
order_item_id: it.id,
|
|
126
|
+
product_id: it.product_id,
|
|
127
|
+
product_name: it.product_name,
|
|
128
|
+
plants,
|
|
129
|
+
plants_per_crate: ppc,
|
|
130
|
+
crates_per_layer: cpl,
|
|
131
|
+
layers_per_cart: lpc,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
if (inputs.length === 0)
|
|
135
|
+
return null;
|
|
136
|
+
return planCarts(inputs).carts;
|
|
137
|
+
}, [dbItems]);
|
|
138
|
+
const floridayCarts = useMemo(() => deriveCartsFromFloriday(order), [order]);
|
|
139
|
+
const usingVoorraad = plannerCarts !== null;
|
|
140
|
+
const carts = usingVoorraad
|
|
141
|
+
? plannerCarts.map((c) => ({
|
|
142
|
+
cart_number: c.cart_number,
|
|
143
|
+
is_complete: c.is_complete,
|
|
144
|
+
occupancy_pct: c.occupancy_pct,
|
|
145
|
+
total_layers_used: c.total_layers_used,
|
|
146
|
+
items: c.items.map((i) => ({
|
|
147
|
+
product_name: i.product_name,
|
|
148
|
+
crates: i.crates,
|
|
149
|
+
plants: i.plants,
|
|
150
|
+
plants_per_crate: i.plants_per_crate,
|
|
151
|
+
crates_per_layer: i.crates_per_layer,
|
|
152
|
+
layers_per_cart: i.layers_per_cart,
|
|
153
|
+
layer_share_pct: i.layer_share_pct,
|
|
154
|
+
})),
|
|
155
|
+
}))
|
|
156
|
+
: floridayCarts;
|
|
157
|
+
const source = usingVoorraad ? "voorraad" : "floriday";
|
|
158
|
+
const lockedDate = lockedAt ? new Date(lockedAt).toLocaleString("nl-NL") : null;
|
|
159
|
+
return (_jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between", children: [_jsx(CardTitle, { className: "text-base", children: "Karindeling" }), _jsxs("div", { className: "flex items-center gap-2", children: [lockedAt && (_jsxs(Badge, { variant: "outline", className: "gap-1", children: [_jsx(Lock, { className: "h-3 w-3" }), "Vergrendeld \u2014 pakbon geprint ", lockedDate] })), _jsxs(Badge, { variant: "secondary", children: [carts.length, " kar(ren) \u00B7 bron: ", source === "voorraad" ? "Voorraad" : "Floriday"] })] })] }), _jsx(CardContent, { children: carts.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Nog geen karindeling beschikbaar voor deze order." })) : (_jsx("div", { className: "space-y-4", children: carts.map((c) => (_jsxs("div", { className: "rounded-md border border-border", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-border bg-muted/30 px-3 py-2 text-sm", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsxs("span", { className: "font-mono font-medium", children: ["Kar ", c.cart_number] }), _jsxs(Badge, { variant: c.is_complete ? "default" : "secondary", className: "gap-1", children: [c.is_complete && _jsx(CheckCircle2, { className: "h-3 w-3" }), c.is_complete ? "Volledig" : "Deels"] })] }), _jsxs("div", { className: "text-muted-foreground", children: [c.total_layers_used, " lagen \u00B7 benutting ", c.occupancy_pct, "%"] })] }), _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Artikel" }), _jsx(TableHead, { className: "text-right", children: "Planten" }), _jsx(TableHead, { className: "text-right", children: "Fusten" }), _jsx(TableHead, { children: "Belading" }), _jsx(TableHead, { className: "text-right", children: "Laag-aandeel" })] }) }), _jsx(TableBody, { children: c.items.map((i, idx) => (_jsxs(TableRow, { children: [_jsx(TableCell, { children: i.product_name }), _jsx(TableCell, { className: "text-right", children: i.plants }), _jsx(TableCell, { className: "text-right", children: i.crates }), _jsx(TableCell, { className: "font-mono text-xs", children: belading(i) }), _jsxs(TableCell, { className: "text-right", children: [Math.round(i.layer_share_pct * 10) / 10, "%"] })] }, idx))) })] })] }, c.cart_number))) })) })] }));
|
|
160
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SalesOrderDetailView.d.ts","sourceRoot":"","sources":["../../../src/components/verkoop/SalesOrderDetailView.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;
|
|
1
|
+
{"version":3,"file":"SalesOrderDetailView.d.ts","sourceRoot":"","sources":["../../../src/components/verkoop/SalesOrderDetailView.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AA+HnE,wBAAgB,oBAAoB,CAAC,EACnC,KAAK,EACL,cAAc,GACf,EAAE;IACD,KAAK,EAAE,aAAa,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC,2CA8RA"}
|