@flowselections/floriday-afhandeling-module 1.0.43 → 1.0.45

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 (68) hide show
  1. package/dist-lib/components/afhandeling/ControlePage.d.ts.map +1 -1
  2. package/dist-lib/components/afhandeling/ControlePage.js +92 -54
  3. package/dist-lib/components/afhandeling/WeekAgendaView.d.ts.map +1 -1
  4. package/dist-lib/components/afhandeling/WeekAgendaView.js +24 -1
  5. package/dist-lib/hooks/useCartIndex.d.ts.map +1 -1
  6. package/dist-lib/hooks/useCartIndex.js +16 -7
  7. package/dist-lib/hooks/useEngineBufferEvents.d.ts +12 -0
  8. package/dist-lib/hooks/useEngineBufferEvents.d.ts.map +1 -0
  9. package/dist-lib/hooks/useEngineBufferEvents.js +52 -0
  10. package/dist-lib/hooks/useEngineTasks.d.ts.map +1 -1
  11. package/dist-lib/hooks/useEngineTasks.js +8 -18
  12. package/dist-lib/hooks/useKnownOrders.d.ts +1 -0
  13. package/dist-lib/hooks/useKnownOrders.d.ts.map +1 -1
  14. package/dist-lib/hooks/useKnownOrders.js +28 -2
  15. package/dist-lib/hooks/useOrderPickDates.d.ts +1 -1
  16. package/dist-lib/hooks/useOrderPickDates.d.ts.map +1 -1
  17. package/dist-lib/hooks/useOrderPickDates.js +5 -3
  18. package/dist-lib/hooks/usePickAgenda.d.ts +4 -0
  19. package/dist-lib/hooks/usePickAgenda.d.ts.map +1 -1
  20. package/dist-lib/hooks/usePickAgenda.js +23 -16
  21. package/dist-lib/hooks/usePickMode.d.ts +8 -2
  22. package/dist-lib/hooks/usePickMode.d.ts.map +1 -1
  23. package/dist-lib/hooks/usePickMode.js +17 -9
  24. package/dist-lib/index.d.ts +1 -2
  25. package/dist-lib/index.d.ts.map +1 -1
  26. package/dist-lib/index.js +1 -33
  27. package/dist-lib/integrations/supabase/auth-middleware.d.ts +3256 -191
  28. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  29. package/dist-lib/integrations/supabase/client.server.d.ts +3256 -191
  30. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  31. package/dist-lib/integrations/supabase/moduleClient.d.ts +3257 -190
  32. package/dist-lib/integrations/supabase/moduleClient.d.ts.map +1 -1
  33. package/dist-lib/integrations/supabase/types.d.ts +3354 -261
  34. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  35. package/dist-lib/lib/afhandelingSettings.functions.d.ts +6144 -14
  36. package/dist-lib/lib/afhandelingSettings.functions.d.ts.map +1 -1
  37. package/dist-lib/lib/carriers.functions.d.ts +15375 -40
  38. package/dist-lib/lib/carriers.functions.d.ts.map +1 -1
  39. package/dist-lib/lib/cartAssignments.functions.d.ts +15360 -35
  40. package/dist-lib/lib/cartAssignments.functions.d.ts.map +1 -1
  41. package/dist-lib/lib/cartControl.functions.d.ts +33902 -104
  42. package/dist-lib/lib/cartControl.functions.d.ts.map +1 -1
  43. package/dist-lib/lib/cartControl.functions.js +389 -259
  44. package/dist-lib/lib/deliveryNote.functions.d.ts +3256 -191
  45. package/dist-lib/lib/deliveryNote.functions.d.ts.map +1 -1
  46. package/dist-lib/lib/engineLayouts.functions.d.ts +3256 -191
  47. package/dist-lib/lib/engineLayouts.functions.d.ts.map +1 -1
  48. package/dist-lib/lib/floriday.functions.d.ts +6146 -14
  49. package/dist-lib/lib/floriday.functions.d.ts.map +1 -1
  50. package/dist-lib/lib/floriday.functions.js +1 -0
  51. package/dist-lib/lib/orderFulfilment.functions.d.ts +3256 -191
  52. package/dist-lib/lib/orderFulfilment.functions.d.ts.map +1 -1
  53. package/dist-lib/lib/pickDates.functions.d.ts +3260 -190
  54. package/dist-lib/lib/pickDates.functions.d.ts.map +1 -1
  55. package/dist-lib/lib/pickDates.functions.js +30 -3
  56. package/dist-lib/lib/pickMode.d.ts +20 -2
  57. package/dist-lib/lib/pickMode.d.ts.map +1 -1
  58. package/dist-lib/lib/pickMode.js +65 -16
  59. package/dist-lib/lib/realtimeBus.d.ts +20 -0
  60. package/dist-lib/lib/realtimeBus.d.ts.map +1 -0
  61. package/dist-lib/lib/realtimeBus.js +56 -0
  62. package/dist-lib/lib/serverFnError.d.ts +12 -0
  63. package/dist-lib/lib/serverFnError.d.ts.map +1 -1
  64. package/dist-lib/lib/serverFnError.js +48 -1
  65. package/dist-lib/moduleDefinition.d.ts +3 -0
  66. package/dist-lib/moduleDefinition.d.ts.map +1 -0
  67. package/dist-lib/moduleDefinition.js +36 -0
  68. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"ControlePage.d.ts","sourceRoot":"","sources":["../../../src/components/afhandeling/ControlePage.tsx"],"names":[],"mappings":"AAkJA,wBAAgB,YAAY,CAAC,EAAE,MAAM,EAAE,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;CAAO,+BAu3BpE"}
1
+ {"version":3,"file":"ControlePage.d.ts","sourceRoot":"","sources":["../../../src/components/afhandeling/ControlePage.tsx"],"names":[],"mappings":"AAkJA,wBAAgB,YAAY,CAAC,EAAE,MAAM,EAAE,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;CAAO,+BAo+BpE"}
@@ -1,6 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useEffect, useMemo, useRef, useState } from "react";
3
- import { supabase } from "../../integrations/supabase/moduleClient";
4
3
  import { Button, Card, Checkbox, Dialog, DialogDescription, DialogTitle, Input, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Textarea, Badge, toast, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@flowselections/core";
5
4
  import { Printer, CheckCircle2, AlertTriangle, History, ChevronLeft, ChevronRight, ScanLine } from "lucide-react";
6
5
  import { ResponsiveDialogBody, ResponsiveDialogContent, ResponsiveDialogFooter, ResponsiveDialogHeader, } from "../../components/ui/responsive-dialog";
@@ -10,6 +9,7 @@ import { getTodaysControlCarts, getControlRescheduleProposals, submitControlSess
10
9
  import { ScanControleView } from "./ScanControleView";
11
10
  import { renderControleLijstHtml } from "./ControleLijstPrint";
12
11
  import { printHtmlDocument, printFoutMelding } from "../../lib/printing";
12
+ import { callServerFn } from "../../lib/serverFnError";
13
13
  function dayDiffFromToday(iso) {
14
14
  const d = new Date(iso);
15
15
  if (Number.isNaN(d.getTime()))
@@ -101,10 +101,15 @@ export function ControlePage({ onExit } = {}) {
101
101
  const saveDraft = useServerFn(saveControlDraft);
102
102
  const cartsQ = useQuery({
103
103
  queryKey: ["control", "today"],
104
- queryFn: () => fetchToday(),
104
+ queryFn: () => callServerFn(() => fetchToday(), "getTodaysControlCarts", { timeoutMs: 8000, maxAttempts: 1 }),
105
+ // Zelfde houdbaarheid als het verversritme: bij elke terugkomst op het
106
+ // tabblad opnieuw laden gaf onnodig zware opvragingen.
107
+ staleTime: 30000,
105
108
  refetchInterval: 60000,
106
109
  refetchIntervalInBackground: false,
107
- refetchOnWindowFocus: true,
110
+ refetchOnWindowFocus: false,
111
+ placeholderData: (prev) => prev,
112
+ retry: false,
108
113
  });
109
114
  const histQ = useQuery({
110
115
  queryKey: ["control", "history"],
@@ -123,7 +128,7 @@ export function ControlePage({ onExit } = {}) {
123
128
  const draftQ = useQuery({
124
129
  queryKey: controlDraftQueryKey,
125
130
  queryFn: async () => (await fetchDraft()),
126
- staleTime: 0,
131
+ staleTime: 30000,
127
132
  refetchOnMount: "always",
128
133
  refetchOnWindowFocus: true,
129
134
  });
@@ -228,6 +233,7 @@ export function ControlePage({ onExit } = {}) {
228
233
  const [savingState, setSavingState] = useState("idle");
229
234
  const autosaveTimer = useRef(null);
230
235
  const controlFinalizedRef = useRef(false);
236
+ const requestKeyRef = useRef(null);
231
237
  useEffect(() => {
232
238
  if (controlFinalizedRef.current)
233
239
  return;
@@ -305,9 +311,19 @@ export function ControlePage({ onExit } = {}) {
305
311
  },
306
312
  }));
307
313
  };
314
+ // Vinkt alleen de karren aan/uit die op de huidige pagina staan.
308
315
  const toggleAll = (on) => {
309
- const keys = carts.map(controlCartKey);
310
- setChecked(on ? new Set(keys) : new Set());
316
+ const keys = pagedCarts.map(controlCartKey);
317
+ setChecked((prev) => {
318
+ const next = new Set(prev);
319
+ for (const key of keys) {
320
+ if (on)
321
+ next.add(key);
322
+ else
323
+ next.delete(key);
324
+ }
325
+ return next;
326
+ });
311
327
  const nowIso = new Date().toISOString();
312
328
  setCartStates((prev) => {
313
329
  const next = { ...prev };
@@ -325,13 +341,18 @@ export function ControlePage({ onExit } = {}) {
325
341
  mutationFn: (input) => {
326
342
  const visibleKeys = new Set(carts.map(controlCartKey));
327
343
  const checkedCartKeys = [...checked].filter((k) => visibleKeys.has(k));
328
- return submit({
344
+ // Eén aanvraag-kenmerk per controlesessie: opnieuw indienen na een
345
+ // onderbroken verbinding levert nooit een tweede sessie op.
346
+ if (!requestKeyRef.current)
347
+ requestKeyRef.current = crypto.randomUUID();
348
+ return callServerFn(() => submit({
329
349
  data: {
330
350
  checkedCartKeys,
331
351
  confirmedRescheduleCartKeys: input?.confirmedRescheduleCartKeys ?? [],
332
352
  notes,
353
+ requestKey: requestKeyRef.current ?? undefined,
333
354
  },
334
- });
355
+ }), "submitControlSession", { timeoutMs: 120000, maxAttempts: 1 });
335
356
  },
336
357
  onMutate: () => {
337
358
  controlFinalizedRef.current = true;
@@ -340,9 +361,23 @@ export function ControlePage({ onExit } = {}) {
340
361
  },
341
362
  onSuccess: (r) => {
342
363
  setResult(r);
364
+ requestKeyRef.current = null;
365
+ const stillOpenCount = r.stillOpen?.length ?? 0;
343
366
  toast.success(`Controle opgeslagen — ${r.checkedCount} klaar, ${r.rescheduledCount} doorgeschoven, ${r.blockedCount} geblokkeerd.`);
344
- qc.invalidateQueries({ queryKey: ["control"] });
345
- qc.invalidateQueries({ queryKey: ["floriday", "orders"] });
367
+ if (stillOpenCount > 0) {
368
+ const labels = r.stillOpen
369
+ .map((c) => c.cartNumber ?? c.orderNumber ?? "onbekend")
370
+ .join(", ");
371
+ toast.warning(`${stillOpenCount} kar(ren) zijn nog niet afgemeld (${labels}). Ze blijven in de lijst staan; probeer die opnieuw.`);
372
+ }
373
+ qc.invalidateQueries({ queryKey: ["control", "today"] });
374
+ qc.invalidateQueries({ queryKey: ["control", "delivered"] });
375
+ qc.invalidateQueries({ queryKey: ["control", "history"] });
376
+ qc.invalidateQueries({ queryKey: ["floriday"] });
377
+ qc.invalidateQueries({ queryKey: ["orders"] });
378
+ qc.invalidateQueries({ queryKey: ["logistics-cart-index"] });
379
+ qc.invalidateQueries({ queryKey: ["logistics-pick-agenda"] });
380
+ qc.invalidateQueries({ queryKey: ["logistics-delivery-agenda"] });
346
381
  qc.invalidateQueries({ queryKey: ["cart-overrides"] });
347
382
  setChecked(new Set());
348
383
  setCartStates({});
@@ -359,13 +394,44 @@ export function ControlePage({ onExit } = {}) {
359
394
  qc.setQueryData(controlDraftQueryKey, buildDraftDto(emptyDraftPayload, {}, null));
360
395
  qc.invalidateQueries({ queryKey: controlDraftQueryKey });
361
396
  },
362
- onError: (e) => {
397
+ onError: async (e) => {
398
+ const message = e instanceof Error ? e.message : "Opslaan mislukt";
399
+ // Een time-out betekent alleen dat wij niet langer wachten; de database
400
+ // kan het afhandelen wél hebben afgerond. Eerst de werkelijke stand
401
+ // ophalen voordat we een medewerker een foutmelding voorschotelen.
402
+ if (/time-?out/i.test(message)) {
403
+ const submittedKeys = new Set(checked);
404
+ const fresh = await qc
405
+ .fetchQuery({
406
+ queryKey: ["control", "today"],
407
+ queryFn: () => callServerFn(() => fetchToday(), "getTodaysControlCarts", {
408
+ timeoutMs: 15000,
409
+ maxAttempts: 1,
410
+ }),
411
+ staleTime: 0,
412
+ })
413
+ .catch(() => null);
414
+ if (fresh) {
415
+ const openAgain = fresh.carts.filter((c) => submittedKeys.has(controlCartKey(c))).length;
416
+ qc.invalidateQueries({ queryKey: ["control", "delivered"] });
417
+ qc.invalidateQueries({ queryKey: ["control", "history"] });
418
+ if (openAgain === 0) {
419
+ toast.success("Het afhandelen duurde langer dan verwacht, maar is afgerond: deze karren staan niet meer in de controlelijst.");
420
+ setChecked(new Set());
421
+ setCartStates({});
422
+ return;
423
+ }
424
+ controlFinalizedRef.current = false;
425
+ toast.warning(`Het afhandelen duurde te lang. ${openAgain} van de ${submittedKeys.size} karren staan nog in de lijst; probeer die opnieuw.`);
426
+ return;
427
+ }
428
+ }
363
429
  controlFinalizedRef.current = false;
364
- toast.error(e instanceof Error ? e.message : "Opslaan mislukt");
430
+ toast.error(message);
365
431
  },
366
432
  });
367
433
  const proposalM = useMutation({
368
- mutationFn: () => fetchProposals({ data: { checkedCartKeys: [...checked] } }),
434
+ mutationFn: () => callServerFn(() => fetchProposals({ data: { checkedCartKeys: [...checked] } }), "getControlRescheduleProposals", { timeoutMs: 8000, maxAttempts: 1 }),
369
435
  onSuccess: (r) => {
370
436
  if (r.proposals.length > 0 || r.blocked.length > 0) {
371
437
  setPendingProposal(r);
@@ -447,42 +513,7 @@ export function ControlePage({ onExit } = {}) {
447
513
  const pendingNotReadyCount = pendingProposals.length + pendingBlocked.length;
448
514
  const criticalTomorrowCount = pendingProposals.filter((p) => isTomorrowIso(p.pickupDate)).length +
449
515
  pendingBlocked.filter((b) => isTomorrowIso(b.pickupDate)).length;
450
- // Realtime sync: invalidate control queries on any change to relevant tables.
451
- const invalidateTimer = useRef(null);
452
- useEffect(() => {
453
- const scheduleInvalidate = () => {
454
- if (invalidateTimer.current)
455
- clearTimeout(invalidateTimer.current);
456
- invalidateTimer.current = setTimeout(() => {
457
- qc.invalidateQueries({ queryKey: ["control"] });
458
- }, 300);
459
- };
460
- const tables = [
461
- "cart_numbers",
462
- "cart_control_sessions",
463
- "cart_control_items",
464
- "cart_pick_overrides",
465
- "orders",
466
- "floriday_salesorders_cache",
467
- "dashboard_critical_alerts",
468
- "logistics_tasks",
469
- "pick_queue",
470
- "print_jobs",
471
- "order_carts",
472
- "order_cart_items",
473
- ];
474
- const channel = supabase.channel("controle-page-realtime");
475
- for (const table of tables) {
476
- channel.on("postgres_changes", { event: "*", schema: "public", table }, scheduleInvalidate);
477
- }
478
- channel.subscribe();
479
- return () => {
480
- if (invalidateTimer.current)
481
- clearTimeout(invalidateTimer.current);
482
- supabase.removeChannel(channel);
483
- };
484
- }, [qc]);
485
- return (_jsxs("main", { className: "p-6 space-y-6", children: [_jsxs("div", { className: "flex items-start justify-between gap-4", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-2xl font-semibold", children: "Controle karren" }), _jsxs("p", { className: "text-sm text-muted-foreground", children: ["Dagelijkse eindcontrole \u2014 vandaag ", new Date().toLocaleDateString("nl-NL")] })] }), _jsxs("div", { className: "flex gap-2", children: [_jsxs(Button, { variant: "outline", disabled: printBezig || carts.length === 0, onClick: async () => {
516
+ return (_jsxs("main", { className: "h-full overflow-y-auto p-6 space-y-6", children: [_jsxs("div", { className: "flex items-start justify-between gap-4", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-2xl font-semibold", children: "Controle karren" }), _jsxs("p", { className: "text-sm text-muted-foreground", children: ["Dagelijkse eindcontrole \u2014 vandaag ", new Date().toLocaleDateString("nl-NL")] })] }), _jsxs("div", { className: "flex gap-2", children: [_jsxs(Button, { variant: "outline", disabled: printBezig || carts.length === 0, onClick: async () => {
486
517
  setPrintBezig(true);
487
518
  try {
488
519
  await printHtmlDocument({
@@ -499,14 +530,21 @@ export function ControlePage({ onExit } = {}) {
499
530
  finally {
500
531
  setPrintBezig(false);
501
532
  }
502
- }, children: [_jsx(Printer, { className: "h-4 w-4 mr-2" }), printBezig ? "Bezig met printen…" : "Controlelijst afdrukken"] }), _jsxs(Button, { onClick: runControl, disabled: submitM.isPending || proposalM.isPending || carts.length === 0 || tab !== "controle", children: [_jsx(CheckCircle2, { className: "h-4 w-4 mr-2" }), proposalM.isPending ? "Controleren…" : "Controle uitvoeren"] })] })] }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { variant: tab === "controle" ? "default" : "outline", onClick: () => setTab("controle"), children: "Controle" }), _jsxs(Button, { variant: tab === "scan" ? "default" : "outline", onClick: () => setTab("scan"), children: [_jsx(ScanLine, { className: "h-4 w-4 mr-2" }), "Scancontrole"] }), _jsxs(Button, { variant: tab === "afgeleverd" ? "default" : "outline", onClick: () => setTab("afgeleverd"), children: [_jsx(CheckCircle2, { className: "h-4 w-4 mr-2" }), "Afgeleverd"] }), _jsxs(Button, { variant: tab === "historie" ? "default" : "outline", onClick: () => setTab("historie"), children: [_jsx(History, { className: "h-4 w-4 mr-2" }), "Historie"] })] }), tab === "controle" && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex flex-wrap gap-3 items-center", children: [_jsxs(Card, { className: "p-3", children: [_jsx("div", { className: "text-xs text-muted-foreground", children: "Totaal karren vandaag" }), _jsx("div", { className: "text-2xl font-semibold", children: stats.total })] }), _jsxs(Card, { className: "p-3", children: [_jsx("div", { className: "text-xs text-muted-foreground", children: "Geprint" }), _jsx("div", { className: "text-2xl font-semibold", children: stats.printed })] }), _jsxs(Card, { className: "p-3", children: [_jsx("div", { className: "text-xs text-muted-foreground", children: "Aangevinkt" }), _jsx("div", { className: "text-2xl font-semibold", children: stats.checked })] })] }), _jsxs(Card, { className: "p-0 overflow-hidden", children: [_jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { className: "w-12", children: _jsx(Checkbox, { checked: carts.length > 0 && carts.every((c) => checked.has(controlCartKey(c)))
533
+ }, children: [_jsx(Printer, { className: "h-4 w-4 mr-2" }), printBezig ? "Bezig met printen…" : "Controlelijst afdrukken"] }), _jsxs(Button, { onClick: runControl, disabled: submitM.isPending || proposalM.isPending || carts.length === 0 || tab !== "controle", children: [_jsx(CheckCircle2, { className: "h-4 w-4 mr-2" }), proposalM.isPending ? "Controleren…" : "Controle uitvoeren"] })] })] }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { variant: tab === "controle" ? "default" : "outline", onClick: () => setTab("controle"), children: "Controle" }), _jsxs(Button, { variant: tab === "scan" ? "default" : "outline", onClick: () => setTab("scan"), children: [_jsx(ScanLine, { className: "h-4 w-4 mr-2" }), "Scancontrole"] }), _jsxs(Button, { variant: tab === "afgeleverd" ? "default" : "outline", onClick: () => setTab("afgeleverd"), children: [_jsx(CheckCircle2, { className: "h-4 w-4 mr-2" }), "Afgeleverd"] }), _jsxs(Button, { variant: tab === "historie" ? "default" : "outline", onClick: () => setTab("historie"), children: [_jsx(History, { className: "h-4 w-4 mr-2" }), "Historie"] })] }), tab === "controle" && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex flex-wrap gap-3 items-center", children: [_jsxs(Card, { className: "p-3", children: [_jsx("div", { className: "text-xs text-muted-foreground", children: "Totaal karren vandaag" }), _jsx("div", { className: "text-2xl font-semibold", children: stats.total })] }), _jsxs(Card, { className: "p-3", children: [_jsx("div", { className: "text-xs text-muted-foreground", children: "Geprint" }), _jsx("div", { className: "text-2xl font-semibold", children: stats.printed })] }), _jsxs(Card, { className: "p-3", children: [_jsx("div", { className: "text-xs text-muted-foreground", children: "Aangevinkt" }), _jsx("div", { className: "text-2xl font-semibold", children: stats.checked })] })] }), cartsQ.isError && (_jsx(Card, { className: "border-destructive/40 bg-destructive/10 p-4", children: _jsxs("div", { className: "flex items-start gap-2 text-destructive", children: [_jsx(AlertTriangle, { className: "mt-0.5 h-4 w-4 shrink-0" }), _jsxs("div", { className: "space-y-2 text-sm", children: [_jsx("div", { className: "font-medium", children: "De karren konden niet worden opgehaald" }), _jsx("p", { children: cartsQ.error instanceof Error
534
+ ? cartsQ.error.message
535
+ : "Onbekende fout bij het ophalen van de karren." }), _jsx(Button, { size: "sm", variant: "outline", onClick: () => void cartsQ.refetch(), children: "Opnieuw proberen" })] })] }) })), !cartsQ.isError && cartsQ.data && (cartsQ.data.warnings ?? []).length > 0 && (_jsx(Card, { className: "border-amber-500/40 bg-amber-500/10 p-4", children: _jsxs("div", { className: "flex items-start gap-2 text-amber-700", children: [_jsx(AlertTriangle, { className: "mt-0.5 h-4 w-4 shrink-0" }), _jsxs("div", { className: "space-y-1 text-sm", children: [_jsx("div", { className: "font-medium", children: "Niet alle gegevens konden worden opgehaald" }), (cartsQ.data.warnings ?? []).map((w) => (_jsx("p", { children: w }, w)))] })] }) })), _jsxs(Card, { className: "p-0 overflow-hidden", children: [_jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { className: "w-12", children: _jsx(Checkbox, { checked: pagedCarts.length > 0 &&
536
+ pagedCarts.every((c) => checked.has(controlCartKey(c)))
503
537
  ? true
504
- : carts.some((c) => checked.has(controlCartKey(c)))
538
+ : pagedCarts.some((c) => checked.has(controlCartKey(c)))
505
539
  ? "indeterminate"
506
- : false, onCheckedChange: (v) => toggleAll(!!v) }) }), _jsx(TableHead, { children: "Ordernummer" }), _jsx(TableHead, { children: "Karnummer" }), _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) => {
540
+ : false, onCheckedChange: (v) => toggleAll(!!v) }) }), _jsx(TableHead, { children: "Ordernummer" }), _jsx(TableHead, { children: "Karnummer" }), _jsx(TableHead, { children: "Oorspronkelijk gepland" }), _jsx(TableHead, { children: "Uitgeprint" })] }) }), _jsxs(TableBody, { children: [cartsQ.isLoading && (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 5, className: "text-center text-muted-foreground py-8", children: "Laden\u2026" }) })), !cartsQ.isLoading && !cartsQ.isError && carts.length === 0 && (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 5, className: "text-center text-muted-foreground py-8", children: (cartsQ.data?.rpcCount ?? 0) > 0
541
+ ? `De planning gaf ${cartsQ.data?.rpcCount} karren voor vandaag, maar geen daarvan hoort nog bij een openstaande bestelling in deze module.`
542
+ : "De planning heeft voor vandaag geen karren om te controleren." }) })), pagedCarts.map((c) => {
507
543
  const k = controlCartKey(c);
508
- return (_jsxs(TableRow, { children: [_jsx(TableCell, { children: _jsx(Checkbox, { checked: checked.has(k), onCheckedChange: (v) => toggle(k, !!v) }) }), _jsxs(TableCell, { className: "font-mono font-semibold", children: [c.orderNumber || "—", _jsxs("span", { className: "ml-2 text-muted-foreground font-normal", children: ["[", c.cartSeq, " - ", c.cartTotal ?? c.cartSeq, "]"] })] }), _jsx(TableCell, { className: "font-mono font-semibold", children: c.cartNumber ?? "—" }), _jsx(TableCell, { children: (c.printedToday ?? (c.printedCount ?? 0) > 0) ? (_jsx(Badge, { variant: "secondary", children: "Ja" })) : (_jsx(Badge, { variant: "outline", children: "Nee" })) })] }, k));
509
- })] })] }), 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) => {
544
+ return (_jsxs(TableRow, { children: [_jsx(TableCell, { children: _jsx(Checkbox, { checked: checked.has(k), onCheckedChange: (v) => toggle(k, !!v) }) }), _jsxs(TableCell, { className: "font-mono font-semibold", children: [c.orderNumber || "—", _jsxs("span", { className: "ml-2 text-muted-foreground font-normal", children: ["[", c.cartSeq, " - ", c.cartTotal ?? c.cartSeq, "]"] })] }), _jsx(TableCell, { className: "font-mono font-semibold", children: c.cartNumber ?? "—" }), _jsxs(TableCell, { className: "whitespace-nowrap", children: [c.originalPickDate ? (_jsxs("span", { className: "flex items-center gap-2", children: [_jsx("span", { children: c.originalPickDate }), c.originalPickDate < c.pickDate ? (_jsx(Badge, { variant: "destructive", children: "Achterstand" })) : null] })) : (_jsx("span", { className: "text-muted-foreground", children: "\u2014" })), c.pickupTime ? (_jsxs("div", { className: "text-xs text-muted-foreground", children: ["Ophalen ", c.pickupTime] })) : null] }), _jsx(TableCell, { children: (c.printedToday ?? (c.printedCount ?? 0) > 0) ? (_jsx(Badge, { variant: "secondary", children: "Ja" })) : (_jsx(Badge, { variant: "outline", children: "Nee" })) })] }, k));
545
+ })] })] }), 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", typeof cartsQ.data?.rpcCount === "number" && (_jsxs(_Fragment, { children: [" · planning gaf ", cartsQ.data.rpcCount, (cartsQ.data.filteredOut ?? 0) > 0
546
+ ? `, ${cartsQ.data.filteredOut} niet gekoppeld aan een bestelling`
547
+ : ""] }))] }), _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) => {
510
548
  setPageSize(Number(v));
511
549
  setPage(1);
512
550
  }, children: [_jsx(SelectTrigger, { className: "h-8 w-[80px]", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: [10, 20, 50, 100].map((n) => (_jsx(SelectItem, { value: String(n), children: n }, n))) })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { variant: "outline", size: "icon", className: "h-8 w-8", "aria-label": "Vorige pagina", disabled: currentPage <= 1, onClick: () => setPage((p) => Math.max(1, p - 1)), children: _jsx(ChevronLeft, { className: "h-4 w-4" }) }), _jsxs("span", { className: "tabular-nums", children: [currentPage, " / ", pageCount] }), _jsx(Button, { variant: "outline", size: "icon", className: "h-8 w-8", "aria-label": "Volgende pagina", disabled: currentPage >= pageCount, onClick: () => setPage((p) => Math.min(pageCount, p + 1)), children: _jsx(ChevronRight, { className: "h-4 w-4" }) })] })] })] }))] }), _jsxs("div", { className: "max-w-xl", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("label", { className: "text-sm font-medium", children: "Opmerkingen bij deze controle" }), _jsx("span", { className: "text-xs text-muted-foreground", "aria-live": "polite", children: savingState === "saving"
@@ -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;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"}
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;AAgFD,wBAAgB,cAAc,CAAC,EAC7B,OAAO,EACP,IAAI,EACJ,SAAS,EACT,YAAY,EACZ,UAAU,EACV,KAAK,EACL,YAAY,EACZ,UAAU,GACX,EAAE,mBAAmB,+BAwKrB"}
@@ -14,6 +14,23 @@ function isOrderReady(o) {
14
14
  const s = (o.status ?? "").toLowerCase();
15
15
  return s.includes("ready") || s.includes("verzendklaar") || s.includes("completed") || s.includes("delivered") || s.includes("afgerond");
16
16
  }
17
+ /** Volledig afgehandeld: de order hoeft niets meer te doen op deze dag. */
18
+ function isDoneStatus(status) {
19
+ const s = (status ?? "").toLowerCase();
20
+ if (!s)
21
+ return false;
22
+ return [
23
+ "done",
24
+ "completed",
25
+ "afgerond",
26
+ "afgehandeld",
27
+ "shipped",
28
+ "verzonden",
29
+ "delivered",
30
+ "geleverd",
31
+ "afgeleverd",
32
+ ].some((k) => s.includes(k));
33
+ }
17
34
  function getISOWeek(d) {
18
35
  const date = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
19
36
  const dayNum = date.getUTCDay() || 7;
@@ -85,6 +102,9 @@ export function WeekAgendaView({ planned, mode, isLoading, onOrderClick, onDayCl
85
102
  planten: row?.plantCount ?? 0,
86
103
  verzendklaar: row?.ready ?? 0,
87
104
  voorbereiding: row?.inPreparation ?? 0,
105
+ afgehandeld: row
106
+ ? Math.max(row.doneCount, row.orders.filter((o) => o.fulfilled || isDoneStatus(o.status)).length)
107
+ : 0,
88
108
  };
89
109
  }
90
110
  const entries = [];
@@ -116,6 +136,7 @@ export function WeekAgendaView({ planned, mode, isLoading, onOrderClick, onDayCl
116
136
  planten,
117
137
  verzendklaar,
118
138
  voorbereiding,
139
+ afgehandeld: entries.filter((e) => isDoneStatus(e.order.status)).length,
119
140
  };
120
141
  });
121
142
  }, [planned, weekStart, mode, pickAgenda.byDate]);
@@ -147,7 +168,9 @@ function DayRow({ day, onOrderClick, onDayClick, capacity, }) {
147
168
  const today = new Date();
148
169
  today.setHours(0, 0, 0, 0);
149
170
  const isToday = isSameDay(day.date, today);
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" })] }));
171
+ // Alles van deze dag is afgehandeld: toon een groene bevestiging.
172
+ const dagAfgesloten = day.bestellingen > 0 && day.afgehandeld >= day.bestellingen;
173
+ 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: [dagAfgesloten && (_jsxs("div", { className: "flex items-center gap-2 rounded-md border border-green-600/30 bg-green-500/15 px-3 py-1.5 text-sm font-medium text-green-700", children: [_jsx(CheckCircle2, { className: "h-4 w-4" }), "Dag is afgesloten"] })), 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" })] }));
151
174
  }
152
175
  function Stat({ icon, bg, value, label, }) {
153
176
  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 })] })] }));
@@ -1 +1 @@
1
- {"version":3,"file":"useCartIndex.d.ts","sourceRoot":"","sources":["../../src/hooks/useCartIndex.ts"],"names":[],"mappings":"AAYA;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI;6BAqBrB,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,KAAG,MAAM,GAAG,IAAI;;;EA4B1F"}
1
+ {"version":3,"file":"useCartIndex.d.ts","sourceRoot":"","sources":["../../src/hooks/useCartIndex.ts"],"names":[],"mappings":"AAYA;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI;6BA2CrB,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,KAAG,MAAM,GAAG,IAAI;;;EAc1F"}
@@ -1,4 +1,4 @@
1
- import { useCallback } from "react";
1
+ import { useCallback, useMemo } from "react";
2
2
  import { useQuery } from "@tanstack/react-query";
3
3
  import { supabase } from "../integrations/supabase/moduleClient";
4
4
  import { useKnownOrders } from "./useKnownOrders";
@@ -26,8 +26,10 @@ export function useCartIndex(from, to) {
26
26
  });
27
27
  const { isKnownRef } = useKnownOrders();
28
28
  const rows = query.data;
29
- // Per orderreferentie de unieke karren tellen.
30
- const cartsForOrder = useCallback((...refs) => {
29
+ // Eén keer tellen zodra de gegevens binnenkomen. Deze telling stond eerder in
30
+ // `cartsForOrder` zelf en werd dus per orderregel opnieuw uitgevoerd — bij
31
+ // honderden orders liep de browser daarop vast.
32
+ const countsByRef = useMemo(() => {
31
33
  const perOrder = new Map();
32
34
  for (const r of rows ?? []) {
33
35
  const cart = r.cart_id ?? "";
@@ -44,15 +46,22 @@ export function useCartIndex(from, to) {
44
46
  perOrder.set(ref, set);
45
47
  }
46
48
  }
49
+ const counts = new Map();
50
+ for (const [ref, set] of perOrder)
51
+ counts.set(ref, set.size);
52
+ return counts;
53
+ }, [rows, isKnownRef]);
54
+ /** Alleen opzoeken, niet meer rekenen. */
55
+ const cartsForOrder = useCallback((...refs) => {
47
56
  for (const ref of refs) {
48
57
  if (!ref)
49
58
  continue;
50
- const set = perOrder.get(ref);
51
- if (set)
52
- return set.size;
59
+ const count = countsByRef.get(ref);
60
+ if (count !== undefined)
61
+ return count;
53
62
  }
54
63
  return null;
55
- }, [rows, isKnownRef]);
64
+ }, [countsByRef]);
56
65
  return {
57
66
  cartsForOrder,
58
67
  isLoading: query.isLoading,
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Luistert naar `public.logistics_events` en ververst de agenda's, de
3
+ * karrencontrole en alle karrentellingen zodra de Engine meldt dat een buffer
4
+ * is gesloten of dat een order is losgekoppeld.
5
+ *
6
+ * Seintjes worden samengevoegd en we invalideren alleen: React Query haalt de
7
+ * schermen die daadwerkelijk in beeld staan zelf opnieuw op. Een directe
8
+ * `refetchQueries({type:"active"})` per seintje veroorzaakte een stortvloed aan
9
+ * gelijktijdige opvragingen en liet het scherm blokkeren.
10
+ */
11
+ export declare function useEngineBufferEvents(): void;
12
+ //# sourceMappingURL=useEngineBufferEvents.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useEngineBufferEvents.d.ts","sourceRoot":"","sources":["../../src/hooks/useEngineBufferEvents.ts"],"names":[],"mappings":"AA0BA;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,IAAI,IAAI,CAmB5C"}
@@ -0,0 +1,52 @@
1
+ import { useEffect } from "react";
2
+ import { useQueryClient } from "@tanstack/react-query";
3
+ import { debounce, subscribeTable } from "../lib/realtimeBus";
4
+ /** Seintjes van de Engine die de indeling van buffers en karren wijzigen. */
5
+ const BUFFER_EVENTS = new Set(["buffer.closed", "buffer.order_detached"]);
6
+ /**
7
+ * Querykeys die opnieuw bij de Engine opgehaald moeten worden zodra een buffer
8
+ * sluit of een order wordt losgekoppeld. Alles komt van de Engine
9
+ * (logistics_pick_agenda, logistics_delivery_agenda, control_get_todays_carts,
10
+ * logistics_cart_units); deze module telt of cachet niets zelf.
11
+ */
12
+ const KEYS = [
13
+ ["logistics-pick-agenda"],
14
+ ["logistics-delivery-agenda"],
15
+ ["logistics-cart-index"],
16
+ ["control"],
17
+ ["order-buffers"],
18
+ ["order-pick-dates"],
19
+ ["engine"],
20
+ ["orders"],
21
+ ["known-orders"],
22
+ ["floriday"],
23
+ ];
24
+ /**
25
+ * Luistert naar `public.logistics_events` en ververst de agenda's, de
26
+ * karrencontrole en alle karrentellingen zodra de Engine meldt dat een buffer
27
+ * is gesloten of dat een order is losgekoppeld.
28
+ *
29
+ * Seintjes worden samengevoegd en we invalideren alleen: React Query haalt de
30
+ * schermen die daadwerkelijk in beeld staan zelf opnieuw op. Een directe
31
+ * `refetchQueries({type:"active"})` per seintje veroorzaakte een stortvloed aan
32
+ * gelijktijdige opvragingen en liet het scherm blokkeren.
33
+ */
34
+ export function useEngineBufferEvents() {
35
+ const qc = useQueryClient();
36
+ useEffect(() => {
37
+ const refresh = debounce(() => {
38
+ for (const key of KEYS)
39
+ qc.invalidateQueries({ queryKey: key });
40
+ }, 1000);
41
+ const off = subscribeTable("logistics_events", (row) => {
42
+ const eventType = row.event_type;
43
+ if (typeof eventType !== "string" || !BUFFER_EVENTS.has(eventType))
44
+ return;
45
+ refresh();
46
+ });
47
+ return () => {
48
+ refresh.cancel();
49
+ off();
50
+ };
51
+ }, [qc]);
52
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"useEngineTasks.d.ts","sourceRoot":"","sources":["../../src/hooks/useEngineTasks.ts"],"names":[],"mappings":"AAGA,OAAO,EAAmB,KAAK,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAEjF,eAAO,MAAM,gBAAgB,UAAsB,CAAC;AAIpD,wBAAgB,cAAc,CAAC,OAAO,CAAC,EAAE;IACvC,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;;;;;EAmDA;AAED,mDAAmD;AACnD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,UAAU,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,UAAU,EAAE,CAAC,CAShF"}
1
+ {"version":3,"file":"useEngineTasks.d.ts","sourceRoot":"","sources":["../../src/hooks/useEngineTasks.ts"],"names":[],"mappings":"AAEA,OAAO,EAAmB,KAAK,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAGjF,eAAO,MAAM,gBAAgB,UAAsB,CAAC;AAIpD,wBAAgB,cAAc,CAAC,OAAO,CAAC,EAAE;IACvC,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;;;;;EAuCA;AAED,mDAAmD;AACnD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,UAAU,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,UAAU,EAAE,CAAC,CAShF"}
@@ -1,7 +1,7 @@
1
- import { useEffect, useMemo, useRef } from "react";
1
+ import { useEffect, useMemo } from "react";
2
2
  import { useQuery, useQueryClient } from "@tanstack/react-query";
3
- import { supabase } from "../integrations/supabase/moduleClient";
4
3
  import { listEngineTasks } from "../lib/logisticsEngine/engine";
4
+ import { debounce, subscribeTable } from "../lib/realtimeBus";
5
5
  export const ENGINE_TASKS_KEY = ["engine", "tasks"];
6
6
  const OPEN_STATUSES = ["planned", "ready", "in_progress"];
7
7
  export function useEngineTasks(options) {
@@ -22,24 +22,14 @@ export function useEngineTasks(options) {
22
22
  });
23
23
  // Realtime-updates komen in bursts binnen als de Engine een batch plant;
24
24
  // daarom gebundeld invalideren in plaats van per rij.
25
- const timerRef = useRef(null);
26
25
  useEffect(() => {
27
- const channel = supabase
28
- .channel(`engine-tasks-${Math.random().toString(36).slice(2)}`)
29
- .on("postgres_changes", { event: "*", schema: "public", table: "logistics_tasks" }, () => {
30
- if (timerRef.current)
31
- return;
32
- timerRef.current = setTimeout(() => {
33
- timerRef.current = null;
34
- void qc.invalidateQueries({ queryKey: ENGINE_TASKS_KEY });
35
- }, 2000);
36
- })
37
- .subscribe();
26
+ const refresh = debounce(() => {
27
+ void qc.invalidateQueries({ queryKey: ENGINE_TASKS_KEY });
28
+ }, 2000);
29
+ const off = subscribeTable("logistics_tasks", refresh);
38
30
  return () => {
39
- if (timerRef.current)
40
- clearTimeout(timerRef.current);
41
- timerRef.current = null;
42
- void supabase.removeChannel(channel);
31
+ refresh.cancel();
32
+ off();
43
33
  };
44
34
  }, [qc]);
45
35
  const tasks = useMemo(() => query.data ?? [], [query.data]);
@@ -5,6 +5,7 @@
5
5
  export declare function useKnownOrders(): {
6
6
  refs: Set<string>;
7
7
  isKnownRef: (...candidates: (string | null | undefined)[]) => boolean;
8
+ isFulfilledRef: (...candidates: (string | null | undefined)[]) => boolean;
8
9
  isReady: boolean;
9
10
  isLoading: boolean;
10
11
  };
@@ -1 +1 @@
1
- {"version":3,"file":"useKnownOrders.d.ts","sourceRoot":"","sources":["../../src/hooks/useKnownOrders.ts"],"names":[],"mappings":"AAaA;;;GAGG;AACH,wBAAgB,cAAc;;gCA4BV,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,KAAG,OAAO;;;EAY1D"}
1
+ {"version":3,"file":"useKnownOrders.d.ts","sourceRoot":"","sources":["../../src/hooks/useKnownOrders.ts"],"names":[],"mappings":"AAgBA;;;GAGG;AACH,wBAAgB,cAAc;;gCAyCV,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,KAAG,OAAO;oCAavC,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,KAAG,OAAO;;;EAW1D"}
@@ -3,6 +3,8 @@ import { useQuery } from "@tanstack/react-query";
3
3
  import { useServerFn } from "@tanstack/react-start";
4
4
  import { listKnownOrders } from "../lib/floriday.functions";
5
5
  import { normalizeOrderNumber } from "../lib/orderRefs";
6
+ import { isFulfilledOrderStatus } from "../lib/orderStatus";
7
+ import { callServerFn } from "../lib/serverFnError";
6
8
  function keyOf(value) {
7
9
  if (!value)
8
10
  return null;
@@ -19,7 +21,7 @@ export function useKnownOrders() {
19
21
  const fetchKnown = useServerFn(listKnownOrders);
20
22
  const query = useQuery({
21
23
  queryKey: ["known-orders"],
22
- queryFn: () => fetchKnown(),
24
+ queryFn: () => callServerFn(() => fetchKnown(), "listKnownOrders"),
23
25
  retry: 1,
24
26
  staleTime: 30000,
25
27
  refetchInterval: 60000,
@@ -37,6 +39,21 @@ export function useKnownOrders() {
37
39
  }
38
40
  return set;
39
41
  }, [query.data]);
42
+ /** Referenties waarvan de Verkoop-module zegt dat ze afgehandeld zijn. */
43
+ const fulfilledRefs = useMemo(() => {
44
+ const set = new Set();
45
+ for (const o of query.data?.orders ?? []) {
46
+ if (!isFulfilledOrderStatus(o.status))
47
+ continue;
48
+ const id = keyOf(o.id);
49
+ const num = keyOf(o.orderNumber);
50
+ if (id)
51
+ set.add(id);
52
+ if (num)
53
+ set.add(num);
54
+ }
55
+ return set;
56
+ }, [query.data]);
40
57
  // Zodra de lijst geladen is filteren we, ook als er niets open staat.
41
58
  const isReady = !query.isLoading && !!query.data && !query.data.error;
42
59
  /** Onbekende referentie? Dan niet tonen. Zolang de lijst laadt: niets filteren. */
@@ -50,5 +67,14 @@ export function useKnownOrders() {
50
67
  }
51
68
  return false;
52
69
  }, [refs, isReady]);
53
- return { refs, isKnownRef, isReady, isLoading: query.isLoading };
70
+ /** Waar = deze order staat in de Verkoop-module als afgehandeld. */
71
+ const isFulfilledRef = useCallback((...candidates) => {
72
+ for (const c of candidates) {
73
+ const k = keyOf(c);
74
+ if (k && fulfilledRefs.has(k))
75
+ return true;
76
+ }
77
+ return false;
78
+ }, [fulfilledRefs]);
79
+ return { refs, isKnownRef, isFulfilledRef, isReady, isLoading: query.isLoading };
54
80
  }
@@ -3,7 +3,7 @@
3
3
  * door naar vandaag; een order hoort te staan op de dag waarop hij ooit
4
4
  * gepland is en nooit ná zijn leverdatum.
5
5
  */
6
- export declare function useOrderPickDates(): {
6
+ export declare function useOrderPickDates(from?: string | null, to?: string | null): {
7
7
  correctPickDate: (orderRef: string, enginePickDate: string) => string;
8
8
  isLoading: boolean;
9
9
  };
@@ -1 +1 @@
1
- {"version":3,"file":"useOrderPickDates.d.ts","sourceRoot":"","sources":["../../src/hooks/useOrderPickDates.ts"],"names":[],"mappings":"AAaA;;;;GAIG;AACH,wBAAgB,iBAAiB;gCA+BlB,MAAM,kBAAkB,MAAM,KAAG,MAAM;;EAerD"}
1
+ {"version":3,"file":"useOrderPickDates.d.ts","sourceRoot":"","sources":["../../src/hooks/useOrderPickDates.ts"],"names":[],"mappings":"AAcA;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,EAAE,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI;gCAoC3D,MAAM,kBAAkB,MAAM,KAAG,MAAM;;EAerD"}
@@ -3,6 +3,7 @@ import { useQuery } from "@tanstack/react-query";
3
3
  import { useServerFn } from "@tanstack/react-start";
4
4
  import { listOrderPickDates } from "../lib/pickDates.functions";
5
5
  import { normalizeOrderNumber } from "../lib/orderRefs";
6
+ import { callServerFn } from "../lib/serverFnError";
6
7
  function keyOf(value) {
7
8
  if (!value)
8
9
  return null;
@@ -16,13 +17,14 @@ function keyOf(value) {
16
17
  * door naar vandaag; een order hoort te staan op de dag waarop hij ooit
17
18
  * gepland is en nooit ná zijn leverdatum.
18
19
  */
19
- export function useOrderPickDates() {
20
+ export function useOrderPickDates(from, to) {
20
21
  const fetchDates = useServerFn(listOrderPickDates);
21
22
  const query = useQuery({
22
- queryKey: ["order-pick-dates"],
23
- queryFn: () => fetchDates(),
23
+ queryKey: ["order-pick-dates", from ?? null, to ?? null],
24
+ queryFn: () => callServerFn(() => fetchDates({ data: { from: from ?? undefined, to: to ?? undefined } }), "listOrderPickDates"),
24
25
  staleTime: 60000,
25
26
  retry: 1,
27
+ placeholderData: (prev) => prev,
26
28
  });
27
29
  const map = useMemo(() => {
28
30
  const m = new Map();