@flowselections/floriday-afhandeling-module 1.0.40 → 1.0.41

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.
Files changed (40) hide show
  1. package/dist-lib/components/afhandeling/ControleLijstPrint.js +54 -54
  2. package/dist-lib/components/afhandeling/ControlePage.d.ts.map +1 -1
  3. package/dist-lib/components/afhandeling/ControlePage.js +36 -2
  4. package/dist-lib/components/afhandeling/DagDetailDialog.d.ts.map +1 -1
  5. package/dist-lib/components/afhandeling/DagDetailDialog.js +3 -2
  6. package/dist-lib/components/afhandeling/WeekAgendaView.d.ts.map +1 -1
  7. package/dist-lib/components/afhandeling/WeekAgendaView.js +38 -45
  8. package/dist-lib/components/pickstation/PakbonTemplate.js +70 -70
  9. package/dist-lib/hooks/useCartIndex.d.ts +11 -0
  10. package/dist-lib/hooks/useCartIndex.d.ts.map +1 -0
  11. package/dist-lib/hooks/useCartIndex.js +56 -0
  12. package/dist-lib/hooks/useConsistencyCheck.d.ts +8 -0
  13. package/dist-lib/hooks/useConsistencyCheck.d.ts.map +1 -0
  14. package/dist-lib/hooks/useConsistencyCheck.js +22 -0
  15. package/dist-lib/hooks/useOrderPlanning.d.ts.map +1 -1
  16. package/dist-lib/hooks/useOrderPlanning.js +6 -2
  17. package/dist-lib/hooks/usePickAgenda.d.ts +32 -0
  18. package/dist-lib/hooks/usePickAgenda.d.ts.map +1 -0
  19. package/dist-lib/hooks/usePickAgenda.js +75 -0
  20. package/dist-lib/integrations/supabase/auth-middleware.d.ts +164 -1
  21. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  22. package/dist-lib/integrations/supabase/client.server.d.ts +164 -1
  23. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  24. package/dist-lib/integrations/supabase/moduleClient.d.ts +164 -1
  25. package/dist-lib/integrations/supabase/moduleClient.d.ts.map +1 -1
  26. package/dist-lib/integrations/supabase/types.d.ts +164 -1
  27. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  28. package/dist-lib/lib/afhandelingSettings.functions.d.ts +330 -4
  29. package/dist-lib/lib/afhandelingSettings.functions.d.ts.map +1 -1
  30. package/dist-lib/lib/carrierSignupMail.server.js +26 -26
  31. package/dist-lib/lib/carriers.functions.d.ts +820 -5
  32. package/dist-lib/lib/carriers.functions.d.ts.map +1 -1
  33. package/dist-lib/lib/cartAssignments.functions.d.ts +820 -5
  34. package/dist-lib/lib/cartAssignments.functions.d.ts.map +1 -1
  35. package/dist-lib/lib/cartControl.functions.d.ts +1743 -124
  36. package/dist-lib/lib/cartControl.functions.d.ts.map +1 -1
  37. package/dist-lib/lib/cartControl.functions.js +92 -127
  38. package/dist-lib/lib/floriday.functions.d.ts +165 -2
  39. package/dist-lib/lib/floriday.functions.d.ts.map +1 -1
  40. package/package.json +1 -1
@@ -70,62 +70,62 @@ export function renderControleLijstHtml(carts, lang = "nl") {
70
70
  });
71
71
  const rows = sorted.length
72
72
  ? sorted
73
- .map(({ cart: c, klaarzet }) => `
74
- <tr>
75
- <td class="mono bold">${esc(c.cartNumber ?? "—")}</td>
76
- <td class="right">${esc(c.cratesOnCart ?? "—")}</td>
77
- <td class="bold">${esc(countryCode(c.country) ?? c.country ?? "—")}</td>
78
- <td class="bold">${esc(klaarzet.label)}</td>
79
- <td>${esc(statusLabel(c.status, c.printedCount))}</td>
73
+ .map(({ cart: c, klaarzet }) => `
74
+ <tr>
75
+ <td class="mono bold">${esc(c.cartNumber ?? "—")}</td>
76
+ <td class="right">${esc(c.cratesOnCart ?? "—")}</td>
77
+ <td class="bold">${esc(countryCode(c.country) ?? c.country ?? "—")}</td>
78
+ <td class="bold">${esc(klaarzet.label)}</td>
79
+ <td>${esc(statusLabel(c.status, c.printedCount))}</td>
80
80
  </tr>`)
81
81
  .join("")
82
82
  : `<tr><td colspan="5" class="empty">${esc(t.noCartsToday)}</td></tr>`;
83
- return `<!DOCTYPE html>
84
- <html lang="${esc(lang)}">
85
- <head>
86
- <meta charset="utf-8" />
87
- <title>${esc(t.checklistTitle)}</title>
88
- <style>
89
- @page { size: A4 portrait; margin: 12mm; }
90
- body { font-family: Arial, Helvetica, sans-serif; color: #000; margin: 0; }
91
- h1 { font-size: 20px; margin: 0; }
92
- .head { display: flex; align-items: baseline; justify-content: space-between;
93
- border-bottom: 1px solid #000; padding-bottom: 6px; margin-bottom: 12px; }
94
- .total { font-size: 12px; margin-bottom: 10px; }
95
- table { width: 100%; border-collapse: collapse; font-size: 11px; }
96
- th { text-align: left; border-bottom: 1px solid #000; padding: 3px 6px 3px 0; }
97
- td { border-bottom: 1px solid #ccc; padding: 6px 6px 6px 0; white-space: nowrap; }
98
- td.right, th.right { text-align: right; }
99
- .mono { font-family: "Courier New", monospace; }
100
- .bold { font-weight: 700; }
101
- .empty { text-align: center; color: #666; padding: 24px 0; border: none; }
102
- .sign { margin-top: 24px; font-size: 11px; }
103
- .sign div { margin-top: 8px; }
104
- .line { display: inline-block; border-bottom: 1px solid #000; width: 220px; }
105
- tr { page-break-inside: avoid; }
106
- thead { display: table-header-group; }
107
- </style>
108
- </head>
109
- <body>
110
- <div class="head"><h1>${esc(t.checklistTitle)}</h1><div>${esc(today)}</div></div>
111
- <div class="total">${esc(t.totalCarts)}: <strong>${carts.length}</strong></div>
112
- <table>
113
- <thead>
114
- <tr>
115
- <th>${esc(t.colKarnr)}</th>
116
- <th class="right">${esc(t.colFusten)}</th>
117
- <th>${esc(t.colLand)}</th>
118
- <th>${esc(t.colKlaarzetten)}</th>
119
- <th>${esc(t.colStatus)}</th>
120
- </tr>
121
- </thead>
122
- <tbody>${rows}</tbody>
123
- </table>
124
- <div class="sign">
125
- <div>${esc(t.checkedBy)}: <span class="line"></span></div>
126
- <div>${esc(t.dateTime)}: <span class="line"></span></div>
127
- <div>${esc(t.signature)}: <span class="line"></span></div>
128
- </div>
129
- </body>
83
+ return `<!DOCTYPE html>
84
+ <html lang="${esc(lang)}">
85
+ <head>
86
+ <meta charset="utf-8" />
87
+ <title>${esc(t.checklistTitle)}</title>
88
+ <style>
89
+ @page { size: A4 portrait; margin: 12mm; }
90
+ body { font-family: Arial, Helvetica, sans-serif; color: #000; margin: 0; }
91
+ h1 { font-size: 20px; margin: 0; }
92
+ .head { display: flex; align-items: baseline; justify-content: space-between;
93
+ border-bottom: 1px solid #000; padding-bottom: 6px; margin-bottom: 12px; }
94
+ .total { font-size: 12px; margin-bottom: 10px; }
95
+ table { width: 100%; border-collapse: collapse; font-size: 11px; }
96
+ th { text-align: left; border-bottom: 1px solid #000; padding: 3px 6px 3px 0; }
97
+ td { border-bottom: 1px solid #ccc; padding: 6px 6px 6px 0; white-space: nowrap; }
98
+ td.right, th.right { text-align: right; }
99
+ .mono { font-family: "Courier New", monospace; }
100
+ .bold { font-weight: 700; }
101
+ .empty { text-align: center; color: #666; padding: 24px 0; border: none; }
102
+ .sign { margin-top: 24px; font-size: 11px; }
103
+ .sign div { margin-top: 8px; }
104
+ .line { display: inline-block; border-bottom: 1px solid #000; width: 220px; }
105
+ tr { page-break-inside: avoid; }
106
+ thead { display: table-header-group; }
107
+ </style>
108
+ </head>
109
+ <body>
110
+ <div class="head"><h1>${esc(t.checklistTitle)}</h1><div>${esc(today)}</div></div>
111
+ <div class="total">${esc(t.totalCarts)}: <strong>${carts.length}</strong></div>
112
+ <table>
113
+ <thead>
114
+ <tr>
115
+ <th>${esc(t.colKarnr)}</th>
116
+ <th class="right">${esc(t.colFusten)}</th>
117
+ <th>${esc(t.colLand)}</th>
118
+ <th>${esc(t.colKlaarzetten)}</th>
119
+ <th>${esc(t.colStatus)}</th>
120
+ </tr>
121
+ </thead>
122
+ <tbody>${rows}</tbody>
123
+ </table>
124
+ <div class="sign">
125
+ <div>${esc(t.checkedBy)}: <span class="line"></span></div>
126
+ <div>${esc(t.dateTime)}: <span class="line"></span></div>
127
+ <div>${esc(t.signature)}: <span class="line"></span></div>
128
+ </div>
129
+ </body>
130
130
  </html>`;
131
131
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ControlePage.d.ts","sourceRoot":"","sources":["../../../src/components/afhandeling/ControlePage.tsx"],"names":[],"mappings":"AAmHA,wBAAgB,YAAY,CAAC,EAAE,MAAM,EAAE,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;CAAO,+BAyzBpE"}
1
+ {"version":3,"file":"ControlePage.d.ts","sourceRoot":"","sources":["../../../src/components/afhandeling/ControlePage.tsx"],"names":[],"mappings":"AA6HA,wBAAgB,YAAY,CAAC,EAAE,MAAM,EAAE,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;CAAO,+BAo1BpE"}
@@ -38,6 +38,10 @@ function blockedKey(b) {
38
38
  return `${b.orderSource}#${b.orderId}#${b.cartIndex}`;
39
39
  }
40
40
  function controlCartKey(c) {
41
+ return c.cartNumberId ?? `${c.orderSource}#${c.orderId}#${c.cartIndex}`;
42
+ }
43
+ /** Oude concept-sleutels (`bron#order#index`) omzetten naar de nieuwe kar-sleutel. */
44
+ function legacyCartKey(c) {
41
45
  return `${c.orderSource}#${c.orderId}#${c.cartIndex}`;
42
46
  }
43
47
  function stableStringifyDraft(input) {
@@ -162,6 +166,31 @@ export function ControlePage({ onExit } = {}) {
162
166
  draftLoadedRef.current = true;
163
167
  setDraftHydrated(true);
164
168
  }, [draftQ.data, draftQ.isError, draftQ.isFetching, draftQ.isLoading]);
169
+ // Migreer concept-sleutels van het oude formaat naar de nieuwe kar-sleutel.
170
+ const keysMigratedRef = useRef(false);
171
+ useEffect(() => {
172
+ if (keysMigratedRef.current)
173
+ return;
174
+ if (!draftHydrated || carts.length === 0)
175
+ return;
176
+ const mapping = new Map();
177
+ for (const cart of carts) {
178
+ const legacy = legacyCartKey(cart);
179
+ const next = controlCartKey(cart);
180
+ if (legacy !== next)
181
+ mapping.set(legacy, next);
182
+ }
183
+ keysMigratedRef.current = true;
184
+ if (mapping.size === 0)
185
+ return;
186
+ setChecked((prev) => new Set([...prev].map((k) => mapping.get(k) ?? k)));
187
+ setCartStates((prev) => {
188
+ const next = {};
189
+ for (const [k, v] of Object.entries(prev))
190
+ next[mapping.get(k) ?? k] = v;
191
+ return next;
192
+ });
193
+ }, [carts, draftHydrated]);
165
194
  // Debounced autosave of the complete draft state.
166
195
  const [savingState, setSavingState] = useState("idle");
167
196
  const autosaveTimer = useRef(null);
@@ -365,7 +394,7 @@ export function ControlePage({ onExit } = {}) {
365
394
  };
366
395
  const stats = useMemo(() => {
367
396
  const total = carts.length;
368
- const printed = carts.filter((c) => c.printedCount > 0).length;
397
+ const printed = carts.filter((c) => c.printedToday ?? c.printedCount > 0).length;
369
398
  const checkedCount = carts.filter((c) => checked.has(controlCartKey(c))).length;
370
399
  return { total, printed, checked: checkedCount };
371
400
  }, [carts, checked]);
@@ -402,6 +431,11 @@ export function ControlePage({ onExit } = {}) {
402
431
  "orders",
403
432
  "floriday_salesorders_cache",
404
433
  "dashboard_critical_alerts",
434
+ "logistics_tasks",
435
+ "pick_queue",
436
+ "print_jobs",
437
+ "order_carts",
438
+ "order_cart_items",
405
439
  ];
406
440
  const channel = supabase.channel("controle-page-realtime");
407
441
  for (const table of tables) {
@@ -437,7 +471,7 @@ export function ControlePage({ onExit } = {}) {
437
471
  ? "indeterminate"
438
472
  : false, onCheckedChange: (v) => toggleAll(!!v) }) }), _jsx(TableHead, { children: "Ordernummer" }), _jsx(TableHead, { children: "Ophaaldatum" }), _jsx(TableHead, { children: "Uitgeprint" })] }) }), _jsxs(TableBody, { children: [cartsQ.isLoading && (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 4, className: "text-center text-muted-foreground py-8", children: "Laden\u2026" }) })), !cartsQ.isLoading && carts.length === 0 && (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 4, className: "text-center text-muted-foreground py-8", children: "Geen karren om vandaag te controleren." }) })), pagedCarts.map((c) => {
439
473
  const k = controlCartKey(c);
440
- return (_jsxs(TableRow, { children: [_jsx(TableCell, { children: _jsx(Checkbox, { checked: checked.has(k), onCheckedChange: (v) => toggle(k, !!v) }) }), _jsx(TableCell, { className: "font-mono font-semibold", children: (c.orderNumbers?.length ?? 0) > 0 ? (_jsx("div", { className: "flex flex-col gap-0.5", children: c.orderNumbers.map((n) => (_jsx("span", { children: n }, n))) })) : (c.orderNumber || "—") }), _jsx(TableCell, { children: fmtDate(c.pickupDate) }), _jsx(TableCell, { children: (c.printedCount ?? 0) > 0 ? (_jsx(Badge, { variant: "secondary", children: "Ja" })) : (_jsx(Badge, { variant: "outline", children: "Nee" })) })] }, k));
474
+ return (_jsxs(TableRow, { children: [_jsx(TableCell, { children: _jsx(Checkbox, { checked: checked.has(k), onCheckedChange: (v) => toggle(k, !!v) }) }), _jsx(TableCell, { className: "font-mono font-semibold", children: (c.orderNumbers?.length ?? 0) > 0 ? (_jsx("div", { className: "flex flex-col gap-0.5", children: c.orderNumbers.map((n) => (_jsx("span", { children: n }, n))) })) : (c.orderNumber || "—") }), _jsx(TableCell, { children: fmtDate(c.pickupDate) }), _jsx(TableCell, { children: (c.printedToday ?? (c.printedCount ?? 0) > 0) ? (_jsx(Badge, { variant: "secondary", children: "Ja" })) : (_jsx(Badge, { variant: "outline", children: "Nee" })) })] }, k));
441
475
  })] })] }), carts.length > 0 && (_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3 border-t bg-muted/20 px-4 py-3 text-sm", children: [_jsxs("span", { className: "text-muted-foreground", children: [carts.length, " resultaten"] }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("span", { className: "text-muted-foreground", children: "Resultaten per pagina" }), _jsxs(Select, { value: String(pageSize), onValueChange: (v) => {
442
476
  setPageSize(Number(v));
443
477
  setPage(1);
@@ -1 +1 @@
1
- {"version":3,"file":"DagDetailDialog.d.ts","sourceRoot":"","sources":["../../../src/components/afhandeling/DagDetailDialog.tsx"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAsCjE,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,OAAO,CAAC;IACd,IAAI,EAAE,IAAI,GAAG,IAAI,CAAC;IAClB,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;CACvC;AAID,wBAAgB,eAAe,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,YAAY,EAAE,EAAE,oBAAoB,sCAgF1F"}
1
+ {"version":3,"file":"DagDetailDialog.d.ts","sourceRoot":"","sources":["../../../src/components/afhandeling/DagDetailDialog.tsx"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAsCjE,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,OAAO,CAAC;IACd,IAAI,EAAE,IAAI,GAAG,IAAI,CAAC;IAClB,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;CACvC;AAID,wBAAgB,eAAe,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,YAAY,EAAE,EAAE,oBAAoB,sCAiF1F"}
@@ -29,7 +29,8 @@ export function DagDetailDialog({ open, date, planned, onOpenChange }) {
29
29
  const cartRows = [];
30
30
  for (const p of ordersOnDay) {
31
31
  const assigns = p.planning.pickAssignments ?? [];
32
- const totalCarts = p.order.cartCount ?? 0;
32
+ // Karren komen uit de Engine-planning (gedeelde karindex), niet uit payloads.
33
+ const totalCarts = assigns.reduce((sum, a) => sum + a.cartCount, 0);
33
34
  if (totalCarts <= 0)
34
35
  continue;
35
36
  let runningIdx = 0;
@@ -169,7 +170,7 @@ function OrderRow({ planned: p, cartsToday }) {
169
170
  /* doorgaan met lege regels */
170
171
  }
171
172
  }
172
- const cartTotal = Math.max(1, p.order.cartCount ?? 1);
173
+ const cartTotal = Math.max(1, (p.planning?.pickAssignments ?? []).reduce((sum, a) => sum + a.cartCount, 0));
173
174
  const pickDateIso = p.planning?.pickAssignments?.[0]?.date?.toISOString?.().slice(0, 10) ??
174
175
  new Date().toISOString().slice(0, 10);
175
176
  const pakbonLines = lines.map((l) => ({
@@ -1 +1 @@
1
- {"version":3,"file":"WeekAgendaView.d.ts","sourceRoot":"","sources":["../../../src/components/afhandeling/WeekAgendaView.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAElE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAKjE,MAAM,MAAM,cAAc,GAAG,UAAU,GAAG,QAAQ,GAAG,oBAAoB,GAAG,MAAM,CAAC;AAEnF,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,IAAI,EAAE,cAAc,CAAC;IACrB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IAC9C,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,CAAC;IAClC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yEAAyE;IACzE,YAAY,CAAC,EAAE,CAAC,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,KAAK,IAAI,CAAC;IACxD,4EAA4E;IAC5E,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AA+DD,wBAAgB,cAAc,CAAC,EAC7B,OAAO,EACP,IAAI,EACJ,SAAS,EACT,YAAY,EACZ,UAAU,EACV,KAAK,EACL,YAAY,EACZ,UAAU,GACX,EAAE,mBAAmB,+BAwJrB"}
1
+ {"version":3,"file":"WeekAgendaView.d.ts","sourceRoot":"","sources":["../../../src/components/afhandeling/WeekAgendaView.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAElE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAOjE,MAAM,MAAM,cAAc,GAAG,UAAU,GAAG,QAAQ,GAAG,oBAAoB,GAAG,MAAM,CAAC;AAEnF,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,IAAI,EAAE,cAAc,CAAC;IACrB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IAC9C,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,CAAC;IAClC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yEAAyE;IACzE,YAAY,CAAC,EAAE,CAAC,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,KAAK,IAAI,CAAC;IACxD,4EAA4E;IAC5E,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AA4DD,wBAAgB,cAAc,CAAC,EAC7B,OAAO,EACP,IAAI,EACJ,SAAS,EACT,YAAY,EACZ,UAAU,EACV,KAAK,EACL,YAAY,EACZ,UAAU,GACX,EAAE,mBAAmB,+BA8JrB"}
@@ -4,6 +4,8 @@ import { Button, Badge } from "@flowselections/core";
4
4
  import { ChevronLeft, ChevronRight, Package, Truck, Sprout, Clock, CheckCircle2, Scale } from "lucide-react";
5
5
  import { isSameDay, startOfWeek, addDays } from "../../lib/pickDate";
6
6
  import { useCapacityCheck } from "../../hooks/useCapacityCheck";
7
+ import { usePickAgenda } from "../../hooks/usePickAgenda";
8
+ import { useConsistencyCheck } from "../../hooks/useConsistencyCheck";
7
9
  import { CapacityWarning } from "./CapacityWarning";
8
10
  import { toDayKey } from "../../lib/logisticsEngine/capacity";
9
11
  const WEEKDAY_LABELS = ["MAANDAG", "DINSDAG", "WOENSDAG", "DONDERDAG", "VRIJDAG", "ZATERDAG", "ZONDAG"];
@@ -29,11 +31,6 @@ function dateForPlanned(p, mode) {
29
31
  case "pick": return p.planning.scheduledPickDate ?? p.planning.pickStartDate;
30
32
  }
31
33
  }
32
- function isMovedEarlier(p, mode) {
33
- if (mode !== "pick" || !p.planning)
34
- return false;
35
- return Boolean(p.planning.movedEarlier);
36
- }
37
34
  const MODE_LABELS = {
38
35
  delivery: { summary: "Leveringen deze week" },
39
36
  pickup: { summary: "Ophalingen deze week" },
@@ -58,6 +55,8 @@ export function WeekAgendaView({ planned, mode, isLoading, onOrderClick, onDayCl
58
55
  // de dichtstbijzijnde week met orders.
59
56
  const jumpedToRef = useRef(null);
60
57
  useEffect(() => {
58
+ if (mode === "pick")
59
+ return; // pickagenda komt uit de Engine-RPC
61
60
  if (isLoading || planned.length > 0 || !jumpToDate)
62
61
  return;
63
62
  if (jumpedToRef.current === jumpToDate)
@@ -67,58 +66,51 @@ export function WeekAgendaView({ planned, mode, isLoading, onOrderClick, onDayCl
67
66
  return;
68
67
  jumpedToRef.current = jumpToDate;
69
68
  setCursor(startOfWeek(target));
70
- }, [jumpToDate, planned.length, isLoading]);
69
+ }, [jumpToDate, planned.length, isLoading, mode]);
70
+ // Pickagenda komt volledig uit de Engine-RPC; deze module rekent zelf geen
71
+ // pickdagen meer uit en doet geen datum-, werkdagen- of tijdzonelogica.
72
+ const pickAgenda = usePickAgenda(mode === "pick" ? toDayKey(weekStart) : null, mode === "pick" ? toDayKey(weekEnd) : null);
71
73
  const days = useMemo(() => {
72
74
  return Array.from({ length: 7 }, (_, i) => {
73
75
  const date = addDays(weekStart, i);
76
+ if (mode === "pick") {
77
+ // Dagen zonder rij tonen simpelweg nullen.
78
+ const row = pickAgenda.byDate.get(toDayKey(date));
79
+ return {
80
+ date,
81
+ entries: [],
82
+ shiftedCount: row?.shiftedCount ?? 0,
83
+ bestellingen: row?.orderCount ?? 0,
84
+ karren: row?.cartCount ?? 0,
85
+ planten: row?.plantCount ?? 0,
86
+ verzendklaar: row?.ready ?? 0,
87
+ voorbereiding: row?.inPreparation ?? 0,
88
+ };
89
+ }
74
90
  const entries = [];
75
91
  for (const p of planned) {
76
92
  if (!p.planning)
77
93
  continue;
78
- if (mode === "pick") {
79
- const assigns = p.planning.pickAssignments ?? [
80
- { date: p.planning.scheduledPickDate ?? p.planning.pickStartDate, cartCount: p.order.cartCount ?? 0 },
81
- ];
82
- const total = assigns.length;
83
- assigns.forEach((a, idx) => {
84
- if (!isSameDay(a.date, date))
85
- return;
86
- const totalCarts = p.order.cartCount ?? 0;
87
- const ratio = totalCarts > 0 ? a.cartCount / totalCarts : 1 / Math.max(total, 1);
88
- entries.push({
89
- order: p.order,
90
- cartCount: a.cartCount,
91
- plantCount: Math.round((p.order.plantCount ?? 0) * ratio),
92
- movedEarlier: a.date.getTime() !== p.planning.pickStartDate.getTime(),
93
- splitPart: total > 1 ? { index: idx + 1, total } : undefined,
94
- reason: p.planning.reason,
95
- carrierName: p.planning.carrierName,
96
- });
97
- });
98
- }
99
- else {
100
- const d = dateForPlanned(p, mode);
101
- if (!d || !isSameDay(d, date))
102
- continue;
103
- entries.push({
104
- order: p.order,
105
- cartCount: p.order.cartCount ?? 0,
106
- plantCount: p.order.plantCount ?? 0,
107
- movedEarlier: false,
108
- reason: p.planning.reason,
109
- carrierName: p.planning.carrierName,
110
- });
111
- }
94
+ const d = dateForPlanned(p, mode);
95
+ if (!d || !isSameDay(d, date))
96
+ continue;
97
+ entries.push({
98
+ order: p.order,
99
+ cartCount: (p.planning.pickAssignments ?? []).reduce((sum, a) => sum + a.cartCount, 0),
100
+ plantCount: p.order.plantCount ?? 0,
101
+ movedEarlier: false,
102
+ reason: p.planning.reason,
103
+ carrierName: p.planning.carrierName,
104
+ });
112
105
  }
113
106
  const karren = entries.reduce((s, e) => s + e.cartCount, 0);
114
107
  const planten = entries.reduce((s, e) => s + e.plantCount, 0);
115
108
  const verzendklaar = entries.filter((e) => isOrderReady(e.order)).length;
116
109
  const voorbereiding = entries.length - verzendklaar;
117
- const movedEarlierOrderIds = new Set(entries.filter((e) => e.movedEarlier).map((e) => e.order.id));
118
110
  return {
119
111
  date,
120
112
  entries,
121
- movedEarlierOrderIds,
113
+ shiftedCount: 0,
122
114
  bestellingen: entries.length,
123
115
  karren,
124
116
  planten,
@@ -126,7 +118,7 @@ export function WeekAgendaView({ planned, mode, isLoading, onOrderClick, onDayCl
126
118
  voorbereiding,
127
119
  };
128
120
  });
129
- }, [planned, weekStart, mode]);
121
+ }, [planned, weekStart, mode, pickAgenda.byDate]);
130
122
  const totals = useMemo(() => {
131
123
  const leveringen = days.reduce((s, d) => s + d.bestellingen, 0);
132
124
  const verzendklaar = days.reduce((s, d) => s + d.verzendklaar, 0);
@@ -134,6 +126,7 @@ export function WeekAgendaView({ planned, mode, isLoading, onOrderClick, onDayCl
134
126
  const totaalStuks = days.reduce((s, d) => s + d.planten, 0);
135
127
  return { leveringen, verzendklaar, voorbereiding, totaalStuks };
136
128
  }, [days]);
129
+ const { misaligned } = useConsistencyCheck();
137
130
  const capacity = useCapacityCheck(toDayKey(weekStart), toDayKey(weekEnd));
138
131
  const headerLabel = `Week ${weekNumber} - ${weekStart.getDate()} ${MONTHS_NL_SHORT[weekStart.getMonth()]}. t/m ${weekEnd.getDate()} ${MONTHS_NL_SHORT[weekEnd.getMonth()]}. ${weekEnd.getFullYear()}`;
139
132
  const goPrev = () => setCursor((c) => addDays(c, -7));
@@ -143,7 +136,7 @@ export function WeekAgendaView({ planned, mode, isLoading, onOrderClick, onDayCl
143
136
  d.setHours(0, 0, 0, 0);
144
137
  setCursor(startOfWeek(d));
145
138
  };
146
- return (_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3", children: [_jsx("h2", { className: "text-xl font-semibold tracking-tight", children: title ?? headerLabel }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs(Button, { variant: "outline", size: "sm", onClick: goPrev, children: [_jsx(ChevronLeft, { className: "mr-1 h-4 w-4" }), " Vorige week"] }), _jsx(Button, { variant: "outline", size: "sm", onClick: goToday, children: "Deze week" }), _jsxs(Button, { variant: "outline", size: "sm", onClick: goNext, children: ["Volgende week ", _jsx(ChevronRight, { className: "ml-1 h-4 w-4" })] })] })] }), _jsxs("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4", children: [_jsx(SummaryCard, { label: MODE_LABELS[mode].summary, value: totals.leveringen, tone: "default" }), _jsx(SummaryCard, { label: "Verzendklaar", value: totals.verzendklaar, tone: "success" }), _jsx(SummaryCard, { label: "In voorbereiding", value: totals.voorbereiding, tone: "muted" }), _jsx(SummaryCard, { label: "Totaal stuks", value: totals.totaalStuks, tone: "default" })] }), _jsx("div", { className: "max-h-[520px] space-y-2 overflow-y-auto rounded-lg border bg-muted/20 p-2", children: isLoading ? (_jsx("div", { className: "rounded-lg border bg-card p-10 text-center text-sm text-muted-foreground", children: "Laden\u2026" })) : (days.map((day) => (_jsx(DayRow, { day: day, onOrderClick: onOrderClick, onDayClick: onDayClick, capacity: mode === "delivery" ? undefined : capacity.byDate.get(toDayKey(day.date)) }, day.date.toISOString())))) })] }));
139
+ return (_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3", children: [_jsx("h2", { className: "text-xl font-semibold tracking-tight", children: title ?? headerLabel }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs(Button, { variant: "outline", size: "sm", onClick: goPrev, children: [_jsx(ChevronLeft, { className: "mr-1 h-4 w-4" }), " Vorige week"] }), _jsx(Button, { variant: "outline", size: "sm", onClick: goToday, children: "Deze week" }), _jsxs(Button, { variant: "outline", size: "sm", onClick: goNext, children: ["Volgende week ", _jsx(ChevronRight, { className: "ml-1 h-4 w-4" })] })] })] }), misaligned && (_jsx("div", { className: "rounded-md border bg-muted/40 px-3 py-2 text-xs text-muted-foreground", children: "Aantallen worden opnieuw uitgelijnd." })), _jsxs("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4", children: [_jsx(SummaryCard, { label: MODE_LABELS[mode].summary, value: totals.leveringen, tone: "default" }), _jsx(SummaryCard, { label: "Verzendklaar", value: totals.verzendklaar, tone: "success" }), _jsx(SummaryCard, { label: "In voorbereiding", value: totals.voorbereiding, tone: "muted" }), _jsx(SummaryCard, { label: "Totaal stuks", value: totals.totaalStuks, tone: "default" })] }), _jsx("div", { className: "max-h-[520px] space-y-2 overflow-y-auto rounded-lg border bg-muted/20 p-2", children: isLoading || (mode === "pick" && pickAgenda.isLoading) ? (_jsx("div", { className: "rounded-lg border bg-card p-10 text-center text-sm text-muted-foreground", children: "Laden\u2026" })) : (days.map((day) => (_jsx(DayRow, { day: day, onOrderClick: onOrderClick, onDayClick: onDayClick, capacity: mode === "delivery" ? undefined : capacity.byDate.get(toDayKey(day.date)) }, day.date.toISOString())))) })] }));
147
140
  }
148
141
  function SummaryCard({ label, value, tone, }) {
149
142
  const valueClass = tone === "success" ? "text-green-600" : "text-foreground";
@@ -154,7 +147,7 @@ function DayRow({ day, onOrderClick, onDayClick, capacity, }) {
154
147
  const today = new Date();
155
148
  today.setHours(0, 0, 0, 0);
156
149
  const isToday = isSameDay(day.date, today);
157
- return (_jsxs("div", { className: `rounded-lg border bg-card ${isToday ? "ring-1 ring-primary" : ""}`, children: [_jsxs("div", { className: "flex flex-wrap items-center gap-6 px-4 py-4 sm:px-6", children: [_jsxs("div", { className: "w-20 shrink-0", children: [_jsx("div", { className: "text-xs font-medium tracking-wider text-muted-foreground", children: WEEKDAY_LABELS[weekdayIdx] }), _jsx("div", { className: "mt-1 text-3xl font-bold leading-none", children: day.date.getDate() }), _jsxs("div", { className: "text-sm text-muted-foreground", children: [MONTHS_NL_SHORT[day.date.getMonth()], "."] })] }), _jsxs("div", { className: "flex flex-1 flex-wrap items-center gap-6", children: [_jsx(Stat, { icon: _jsx(Package, { className: "h-4 w-4 text-slate-500" }), bg: "bg-slate-100", value: day.bestellingen, label: "bestellingen" }), _jsx(Stat, { icon: _jsx(Truck, { className: "h-4 w-4 text-amber-600" }), bg: "bg-amber-100", value: day.karren, label: "karren" }), _jsx(Stat, { icon: _jsx(Sprout, { className: "h-4 w-4 text-green-600" }), bg: "bg-green-100", value: day.planten, label: "planten" })] }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [day.movedEarlierOrderIds.size > 0 && (_jsxs(Badge, { className: "bg-purple-500/10 text-purple-700 hover:bg-purple-500/10", variant: "secondary", title: "Aantal orders dat vervroegd is ingepland door Pickverdeling.", children: [_jsx(Scale, { className: "mr-1 h-3.5 w-3.5" }), day.movedEarlierOrderIds.size, " vervroegd"] })), day.verzendklaar > 0 && (_jsxs(Badge, { className: "bg-green-500/15 text-green-700 hover:bg-green-500/15", variant: "secondary", children: [_jsx(CheckCircle2, { className: "mr-1 h-3.5 w-3.5" }), day.verzendklaar, " verzendklaar"] })), day.voorbereiding > 0 && (_jsxs(Badge, { className: "bg-blue-500/10 text-blue-700 hover:bg-blue-500/10", variant: "secondary", children: [_jsx(Clock, { className: "mr-1 h-3.5 w-3.5" }), day.voorbereiding, " in voorbereiding"] })), onDayClick && day.bestellingen > 0 && (_jsx(Button, { size: "sm", variant: "outline", onClick: () => onDayClick(day.date), children: "Dagdetails" }))] })] }), _jsx(CapacityWarning, { day: capacity, className: "mx-4 mb-4 sm:mx-6" })] }));
150
+ return (_jsxs("div", { className: `rounded-lg border bg-card ${isToday ? "ring-1 ring-primary" : ""}`, children: [_jsxs("div", { className: "flex flex-wrap items-center gap-6 px-4 py-4 sm:px-6", children: [_jsxs("div", { className: "w-20 shrink-0", children: [_jsx("div", { className: "text-xs font-medium tracking-wider text-muted-foreground", children: WEEKDAY_LABELS[weekdayIdx] }), _jsx("div", { className: "mt-1 text-3xl font-bold leading-none", children: day.date.getDate() }), _jsxs("div", { className: "text-sm text-muted-foreground", children: [MONTHS_NL_SHORT[day.date.getMonth()], "."] })] }), _jsxs("div", { className: "flex flex-1 flex-wrap items-center gap-6", children: [_jsx(Stat, { icon: _jsx(Package, { className: "h-4 w-4 text-slate-500" }), bg: "bg-slate-100", value: day.bestellingen, label: "bestellingen" }), _jsx(Stat, { icon: _jsx(Truck, { className: "h-4 w-4 text-amber-600" }), bg: "bg-amber-100", value: day.karren, label: "karren" }), _jsx(Stat, { icon: _jsx(Sprout, { className: "h-4 w-4 text-green-600" }), bg: "bg-green-100", value: day.planten, label: "planten" })] }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [day.shiftedCount > 0 && (_jsxs(Badge, { className: "bg-purple-500/10 text-purple-700 hover:bg-purple-500/10", variant: "secondary", title: "Aantal orders dat te laat is aangemeld en door de Engine is verschoven.", children: [_jsx(Scale, { className: "mr-1 h-3.5 w-3.5" }), day.shiftedCount, " te laat aangemeld"] })), day.verzendklaar > 0 && (_jsxs(Badge, { className: "bg-green-500/15 text-green-700 hover:bg-green-500/15", variant: "secondary", children: [_jsx(CheckCircle2, { className: "mr-1 h-3.5 w-3.5" }), day.verzendklaar, " verzendklaar"] })), day.voorbereiding > 0 && (_jsxs(Badge, { className: "bg-blue-500/10 text-blue-700 hover:bg-blue-500/10", variant: "secondary", children: [_jsx(Clock, { className: "mr-1 h-3.5 w-3.5" }), day.voorbereiding, " in voorbereiding"] })), onDayClick && day.bestellingen > 0 && (_jsx(Button, { size: "sm", variant: "outline", onClick: () => onDayClick(day.date), children: "Dagdetails" }))] })] }), _jsx(CapacityWarning, { day: capacity, className: "mx-4 mb-4 sm:mx-6" })] }));
158
151
  }
159
152
  function Stat({ icon, bg, value, label, }) {
160
153
  return (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: `grid h-8 w-8 place-items-center rounded-full ${bg}`, children: icon }), _jsxs("div", { children: [_jsx("div", { className: "text-lg font-bold leading-none", children: value }), _jsx("div", { className: "text-xs text-muted-foreground", children: label })] })] }));
@@ -14,82 +14,82 @@ export function renderPakbonHtml(order, lang = "nl") {
14
14
  };
15
15
  const rows = order.lines.length
16
16
  ? order.lines
17
- .map((l) => `
18
- <tr>
19
- <td>${escapeHtml(l.description)}</td>
20
- <td style="text-align:right">${l.crates ?? ""}</td>
21
- <td>${escapeHtml(l.unit ?? "")}</td>
17
+ .map((l) => `
18
+ <tr>
19
+ <td>${escapeHtml(l.description)}</td>
20
+ <td style="text-align:right">${l.crates ?? ""}</td>
21
+ <td>${escapeHtml(l.unit ?? "")}</td>
22
22
  </tr>`)
23
23
  .join("")
24
24
  : `<tr><td colspan="3" style="text-align:center;color:#666">${escapeHtml(t.noLines)}</td></tr>`;
25
25
  const cartLabel = t.cartOf(order.cartIndex, order.cartTotal);
26
26
  const fustenLabel = order.cratesOnCart != null ? `${order.cratesOnCart}` : "—";
27
27
  const cartNumber = order.cartNumber ?? `${order.orderNumber}-${order.cartIndex}`;
28
- return `<!doctype html>
29
- <html lang="${escapeHtml(htmlLang)}">
30
- <head>
31
- <meta charset="utf-8">
32
- <title>${escapeHtml(t.packingSlip)} ${escapeHtml(order.orderNumber)} · ${escapeHtml(cartNumber)}</title>
33
- <style>
34
- @page {
35
- size: A4;
36
- margin: 16mm;
37
- @top-center { content: element(cartno); }
38
- }
39
- body { font-family: -apple-system, "Segoe UI", Helvetica, Arial, sans-serif; color:#111; }
40
- h1 { margin: 0 0 4px; font-size: 26px; }
41
- .sub { color:#444; margin-bottom: 8px; }
42
-
43
- .cart-number-hero {
44
- position: running(cartno);
45
- font-size: 88px;
46
- font-weight: 900;
47
- letter-spacing: 4px;
48
- line-height: 1;
49
- color: #000;
50
- background: #fff;
51
- border: 4px solid #000;
52
- padding: 12px 20px;
53
- display: inline-block;
54
- margin: 6px 0 20px;
55
- font-family: "Arial Black", "Helvetica Neue", Helvetica, Arial, sans-serif;
56
- }
57
-
58
- .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px 24px; margin: 8px 0 22px; }
59
- .grid div { font-size: 14px; }
60
- .label { color:#666; font-size: 11px; text-transform: uppercase; letter-spacing: .04em; }
61
- .value { font-size: 16px; font-weight: 600; }
62
- table { width:100%; border-collapse: collapse; font-size: 14px; }
63
- th, td { padding: 8px 6px; border-bottom: 1px solid #ddd; vertical-align: top; }
64
- th { text-align: left; background:#f5f5f5; }
65
- .barcode-fallback { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 18px; letter-spacing: 4px; padding: 8px 12px; border: 2px solid #111; display: inline-block; margin-top: 18px; }
66
- .footer { margin-top: 28px; font-size: 11px; color:#666; }
67
- </style>
68
- </head>
69
- <body>
70
- <h1>${escapeHtml(t.packingSlip)}</h1>
71
- <div class="sub">${escapeHtml(t.order)} ${escapeHtml(order.orderNumber)} · ${escapeHtml(cartLabel)}</div>
72
-
73
- <div class="cart-number-hero">${escapeHtml(cartNumber)}</div>
74
-
75
- <div class="grid">
76
- <div><div class="label">${escapeHtml(t.customer)}</div><div class="value">${escapeHtml(order.customerName)}</div></div>
77
- <div><div class="label">${escapeHtml(t.country)}</div><div class="value">${escapeHtml(order.country ?? "—")}</div></div>
78
- <div><div class="label">${escapeHtml(t.deliveryDate)}</div><div class="value">${fmt(order.deliveryDate)}</div></div>
79
- <div><div class="label">${escapeHtml(t.pickDate)}</div><div class="value">${fmt(order.pickDate)}</div></div>
80
- <div><div class="label">${escapeHtml(t.cart)}</div><div class="value">${escapeHtml(cartLabel)}</div></div>
81
- <div><div class="label">${escapeHtml(t.cratesOnThisCart)}</div><div class="value">${escapeHtml(fustenLabel)}</div></div>
82
- </div>
83
-
84
- <table>
85
- <thead><tr><th>${escapeHtml(t.description)}</th><th style="text-align:right;width:90px">${escapeHtml(t.crates)}</th><th style="width:90px">${escapeHtml(t.unit)}</th></tr></thead>
86
- <tbody>${rows}</tbody>
87
- </table>
88
-
89
- <div class="barcode-fallback">*${escapeHtml(order.orderNumber)}*</div>
90
-
91
- <div class="footer">${escapeHtml(t.printedVia)} · ${new Date().toLocaleString(locale)}</div>
92
- </body>
28
+ return `<!doctype html>
29
+ <html lang="${escapeHtml(htmlLang)}">
30
+ <head>
31
+ <meta charset="utf-8">
32
+ <title>${escapeHtml(t.packingSlip)} ${escapeHtml(order.orderNumber)} · ${escapeHtml(cartNumber)}</title>
33
+ <style>
34
+ @page {
35
+ size: A4;
36
+ margin: 16mm;
37
+ @top-center { content: element(cartno); }
38
+ }
39
+ body { font-family: -apple-system, "Segoe UI", Helvetica, Arial, sans-serif; color:#111; }
40
+ h1 { margin: 0 0 4px; font-size: 26px; }
41
+ .sub { color:#444; margin-bottom: 8px; }
42
+
43
+ .cart-number-hero {
44
+ position: running(cartno);
45
+ font-size: 88px;
46
+ font-weight: 900;
47
+ letter-spacing: 4px;
48
+ line-height: 1;
49
+ color: #000;
50
+ background: #fff;
51
+ border: 4px solid #000;
52
+ padding: 12px 20px;
53
+ display: inline-block;
54
+ margin: 6px 0 20px;
55
+ font-family: "Arial Black", "Helvetica Neue", Helvetica, Arial, sans-serif;
56
+ }
57
+
58
+ .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px 24px; margin: 8px 0 22px; }
59
+ .grid div { font-size: 14px; }
60
+ .label { color:#666; font-size: 11px; text-transform: uppercase; letter-spacing: .04em; }
61
+ .value { font-size: 16px; font-weight: 600; }
62
+ table { width:100%; border-collapse: collapse; font-size: 14px; }
63
+ th, td { padding: 8px 6px; border-bottom: 1px solid #ddd; vertical-align: top; }
64
+ th { text-align: left; background:#f5f5f5; }
65
+ .barcode-fallback { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 18px; letter-spacing: 4px; padding: 8px 12px; border: 2px solid #111; display: inline-block; margin-top: 18px; }
66
+ .footer { margin-top: 28px; font-size: 11px; color:#666; }
67
+ </style>
68
+ </head>
69
+ <body>
70
+ <h1>${escapeHtml(t.packingSlip)}</h1>
71
+ <div class="sub">${escapeHtml(t.order)} ${escapeHtml(order.orderNumber)} · ${escapeHtml(cartLabel)}</div>
72
+
73
+ <div class="cart-number-hero">${escapeHtml(cartNumber)}</div>
74
+
75
+ <div class="grid">
76
+ <div><div class="label">${escapeHtml(t.customer)}</div><div class="value">${escapeHtml(order.customerName)}</div></div>
77
+ <div><div class="label">${escapeHtml(t.country)}</div><div class="value">${escapeHtml(order.country ?? "—")}</div></div>
78
+ <div><div class="label">${escapeHtml(t.deliveryDate)}</div><div class="value">${fmt(order.deliveryDate)}</div></div>
79
+ <div><div class="label">${escapeHtml(t.pickDate)}</div><div class="value">${fmt(order.pickDate)}</div></div>
80
+ <div><div class="label">${escapeHtml(t.cart)}</div><div class="value">${escapeHtml(cartLabel)}</div></div>
81
+ <div><div class="label">${escapeHtml(t.cratesOnThisCart)}</div><div class="value">${escapeHtml(fustenLabel)}</div></div>
82
+ </div>
83
+
84
+ <table>
85
+ <thead><tr><th>${escapeHtml(t.description)}</th><th style="text-align:right;width:90px">${escapeHtml(t.crates)}</th><th style="width:90px">${escapeHtml(t.unit)}</th></tr></thead>
86
+ <tbody>${rows}</tbody>
87
+ </table>
88
+
89
+ <div class="barcode-fallback">*${escapeHtml(order.orderNumber)}*</div>
90
+
91
+ <div class="footer">${escapeHtml(t.printedVia)} · ${new Date().toLocaleString(locale)}</div>
92
+ </body>
93
93
  </html>`;
94
94
  }
95
95
  function escapeHtml(s) {
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Gedeelde karindex van de Engine. Dit is de enige bron voor karrentellingen:
3
+ * taak-payloads (cartCount) en eigen tellingen op order_carts zijn niet meer
4
+ * toegestaan. Een gedeelde kar telt bij elke order waar hij in zit.
5
+ */
6
+ export declare function useCartIndex(from: string | null, to: string | null): {
7
+ cartsForOrder: (...refs: (string | null | undefined)[]) => number | null;
8
+ isLoading: boolean;
9
+ error: string | null;
10
+ };
11
+ //# sourceMappingURL=useCartIndex.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useCartIndex.d.ts","sourceRoot":"","sources":["../../src/hooks/useCartIndex.ts"],"names":[],"mappings":"AAWA;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI;6BAoBrB,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,KAAG,MAAM,GAAG,IAAI;;;EA0B1F"}
@@ -0,0 +1,56 @@
1
+ import { useCallback } from "react";
2
+ import { useQuery } from "@tanstack/react-query";
3
+ import { supabase } from "../integrations/supabase/moduleClient";
4
+ /**
5
+ * Gedeelde karindex van de Engine. Dit is de enige bron voor karrentellingen:
6
+ * taak-payloads (cartCount) en eigen tellingen op order_carts zijn niet meer
7
+ * toegestaan. Een gedeelde kar telt bij elke order waar hij in zit.
8
+ */
9
+ export function useCartIndex(from, to) {
10
+ const query = useQuery({
11
+ queryKey: ["logistics-cart-index", from, to],
12
+ enabled: !!from && !!to,
13
+ staleTime: 30000,
14
+ retry: false,
15
+ refetchInterval: 60000,
16
+ queryFn: async () => {
17
+ const { data, error } = await supabase.rpc("logistics_cart_index", {
18
+ p_from: from,
19
+ p_to: to,
20
+ });
21
+ if (error)
22
+ throw new Error(error.message);
23
+ return Array.isArray(data) ? data : [];
24
+ },
25
+ });
26
+ const rows = query.data;
27
+ // Per orderreferentie de unieke karren tellen.
28
+ const cartsForOrder = useCallback((...refs) => {
29
+ const perOrder = new Map();
30
+ for (const r of rows ?? []) {
31
+ const cart = r.cart_id ?? "";
32
+ if (!cart)
33
+ continue;
34
+ for (const ref of [r.order_number, r.order_uuid]) {
35
+ if (!ref)
36
+ continue;
37
+ const set = perOrder.get(ref) ?? new Set();
38
+ set.add(cart);
39
+ perOrder.set(ref, set);
40
+ }
41
+ }
42
+ for (const ref of refs) {
43
+ if (!ref)
44
+ continue;
45
+ const set = perOrder.get(ref);
46
+ if (set)
47
+ return set.size;
48
+ }
49
+ return null;
50
+ }, [rows]);
51
+ return {
52
+ cartsForOrder,
53
+ isLoading: query.isLoading,
54
+ error: query.error instanceof Error ? query.error.message : null,
55
+ };
56
+ }