@flowselections/floriday-afhandeling-module 1.0.25 → 1.0.26
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/afhandeling/CapacityWarning.d.ts +10 -0
- package/dist-lib/components/afhandeling/CapacityWarning.d.ts.map +1 -0
- package/dist-lib/components/afhandeling/CapacityWarning.js +12 -0
- package/dist-lib/components/afhandeling/ControlePage.d.ts.map +1 -1
- package/dist-lib/components/afhandeling/ControlePage.js +7 -2
- package/dist-lib/components/afhandeling/DagDetailDialog.d.ts.map +1 -1
- package/dist-lib/components/afhandeling/DagDetailDialog.js +158 -62
- package/dist-lib/components/afhandeling/LeveringenPage.d.ts.map +1 -1
- package/dist-lib/components/afhandeling/LeveringenPage.js +2 -10
- package/dist-lib/components/afhandeling/PickAgendaView.d.ts.map +1 -1
- package/dist-lib/components/afhandeling/PickAgendaView.js +4 -11
- package/dist-lib/components/afhandeling/VervoerdersPage.d.ts.map +1 -1
- package/dist-lib/components/afhandeling/VervoerdersPage.js +2 -5
- package/dist-lib/components/afhandeling/VerzetOrderDialog.d.ts +14 -0
- package/dist-lib/components/afhandeling/VerzetOrderDialog.d.ts.map +1 -0
- package/dist-lib/components/afhandeling/VerzetOrderDialog.js +203 -0
- package/dist-lib/components/afhandeling/WeekAgendaView.d.ts.map +1 -1
- package/dist-lib/components/afhandeling/WeekAgendaView.js +7 -3
- package/dist-lib/components/pickstation/PickstationPage.d.ts.map +1 -1
- package/dist-lib/components/pickstation/PickstationPage.js +1 -0
- package/dist-lib/components/settings/VervoerdersSettingsCard.d.ts.map +1 -1
- package/dist-lib/components/settings/VervoerdersSettingsCard.js +2 -5
- package/dist-lib/hooks/useCapacityCheck.d.ts +10 -0
- package/dist-lib/hooks/useCapacityCheck.d.ts.map +1 -0
- package/dist-lib/hooks/useCapacityCheck.js +22 -0
- package/dist-lib/hooks/useEngineTasks.d.ts +14 -0
- package/dist-lib/hooks/useEngineTasks.d.ts.map +1 -0
- package/dist-lib/hooks/useEngineTasks.js +49 -0
- package/dist-lib/hooks/useOrderPlanning.d.ts +8 -2
- package/dist-lib/hooks/useOrderPlanning.d.ts.map +1 -1
- package/dist-lib/hooks/useOrderPlanning.js +95 -93
- package/dist-lib/index.d.ts +0 -3
- package/dist-lib/index.d.ts.map +1 -1
- package/dist-lib/index.js +1 -5
- package/dist-lib/integrations/supabase/auth-middleware.d.ts +9010 -5433
- package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.d.ts +9010 -5433
- package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.server.d.ts +9010 -5433
- package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.d.ts +9184 -5544
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/lib/afhandelingSettings.functions.d.ts +7186 -32
- package/dist-lib/lib/afhandelingSettings.functions.d.ts.map +1 -1
- package/dist-lib/lib/carriers.functions.d.ts +17965 -80
- package/dist-lib/lib/carriers.functions.d.ts.map +1 -1
- package/dist-lib/lib/cartAssignments.functions.d.ts +17965 -80
- package/dist-lib/lib/cartAssignments.functions.d.ts.map +1 -1
- package/dist-lib/lib/cartControl.functions.d.ts +35962 -170
- package/dist-lib/lib/cartControl.functions.d.ts.map +1 -1
- package/dist-lib/lib/cartControl.functions.js +103 -13
- package/dist-lib/lib/floriday.functions.d.ts +9010 -5433
- package/dist-lib/lib/floriday.functions.d.ts.map +1 -1
- package/dist-lib/lib/logisticsEngine/capacity.d.ts +19 -0
- package/dist-lib/lib/logisticsEngine/capacity.d.ts.map +1 -0
- package/dist-lib/lib/logisticsEngine/capacity.js +91 -0
- package/dist-lib/lib/logisticsEngine/engine.d.ts +99 -0
- package/dist-lib/lib/logisticsEngine/engine.d.ts.map +1 -0
- package/dist-lib/lib/logisticsEngine/engine.js +102 -0
- package/dist-lib/lib/logisticsEngine/reschedule.d.ts +65 -0
- package/dist-lib/lib/logisticsEngine/reschedule.d.ts.map +1 -0
- package/dist-lib/lib/logisticsEngine/reschedule.js +86 -0
- package/dist-lib/lib/logisticsEngine/staffPlanning.d.ts +37 -0
- package/dist-lib/lib/logisticsEngine/staffPlanning.d.ts.map +1 -0
- package/dist-lib/lib/logisticsEngine/staffPlanning.js +100 -0
- package/dist-lib/lib/serverFnError.d.ts +10 -0
- package/dist-lib/lib/serverFnError.d.ts.map +1 -0
- package/dist-lib/lib/serverFnError.js +29 -0
- package/dist-lib/lib/serverRoutes/index.d.ts +0 -1
- package/dist-lib/lib/serverRoutes/index.d.ts.map +1 -1
- package/dist-lib/lib/serverRoutes/index.js +0 -1
- package/dist-lib/lib/timezone.d.ts +27 -0
- package/dist-lib/lib/timezone.d.ts.map +1 -0
- package/dist-lib/lib/timezone.js +68 -0
- package/package.json +6 -21
- package/dist-lib/components/engine/CapaciteitsmaatregelenPage.d.ts +0 -2
- package/dist-lib/components/engine/CapaciteitsmaatregelenPage.d.ts.map +0 -1
- package/dist-lib/components/engine/CapaciteitsmaatregelenPage.js +0 -40
- package/dist-lib/components/engine/CapaciteitsmonitorPage.d.ts +0 -2
- package/dist-lib/components/engine/CapaciteitsmonitorPage.d.ts.map +0 -1
- package/dist-lib/components/engine/CapaciteitsmonitorPage.js +0 -49
- package/dist-lib/components/engine/FormulesPage.d.ts +0 -2
- package/dist-lib/components/engine/FormulesPage.d.ts.map +0 -1
- package/dist-lib/components/engine/FormulesPage.js +0 -252
- package/dist-lib/components/engine/PlaceholderPage.d.ts +0 -6
- package/dist-lib/components/engine/PlaceholderPage.d.ts.map +0 -1
- package/dist-lib/components/engine/PlaceholderPage.js +0 -6
- package/dist-lib/components/engine/StrategieenPage.d.ts +0 -2
- package/dist-lib/components/engine/StrategieenPage.d.ts.map +0 -1
- package/dist-lib/components/engine/StrategieenPage.js +0 -80
- package/dist-lib/lib/logisticsEngine/capacity.functions.d.ts +0 -64978
- package/dist-lib/lib/logisticsEngine/capacity.functions.d.ts.map +0 -1
- package/dist-lib/lib/logisticsEngine/capacity.functions.js +0 -90
- package/dist-lib/lib/logisticsEngine/config.d.ts +0 -140
- package/dist-lib/lib/logisticsEngine/config.d.ts.map +0 -1
- package/dist-lib/lib/logisticsEngine/config.js +0 -488
- package/dist-lib/lib/logisticsEngine/enginePickTime.d.ts +0 -29
- package/dist-lib/lib/logisticsEngine/enginePickTime.d.ts.map +0 -1
- package/dist-lib/lib/logisticsEngine/enginePickTime.js +0 -136
- package/dist-lib/lib/logisticsEngine/evaluate.functions.d.ts +0 -16278
- package/dist-lib/lib/logisticsEngine/evaluate.functions.d.ts.map +0 -1
- package/dist-lib/lib/logisticsEngine/evaluate.functions.js +0 -104
- package/dist-lib/lib/logisticsEngine/expression.d.ts +0 -21
- package/dist-lib/lib/logisticsEngine/expression.d.ts.map +0 -1
- package/dist-lib/lib/logisticsEngine/expression.js +0 -263
- package/dist-lib/lib/logisticsEngine/formulas.functions.d.ts +0 -32512
- package/dist-lib/lib/logisticsEngine/formulas.functions.d.ts.map +0 -1
- package/dist-lib/lib/logisticsEngine/formulas.functions.js +0 -58
- package/dist-lib/lib/logisticsEngine/safetyLayer.d.ts +0 -83
- package/dist-lib/lib/logisticsEngine/safetyLayer.d.ts.map +0 -1
- package/dist-lib/lib/logisticsEngine/safetyLayer.js +0 -182
- package/dist-lib/lib/logisticsEngine/strategies.functions.d.ts +0 -129961
- package/dist-lib/lib/logisticsEngine/strategies.functions.d.ts.map +0 -1
- package/dist-lib/lib/logisticsEngine/strategies.functions.js +0 -176
- package/dist-lib/lib/logisticsEngine/variables.d.ts +0 -29
- package/dist-lib/lib/logisticsEngine/variables.d.ts.map +0 -1
- package/dist-lib/lib/logisticsEngine/variables.js +0 -144
- package/dist-lib/lib/logisticsEngine/wizard.functions.d.ts +0 -64976
- package/dist-lib/lib/logisticsEngine/wizard.functions.d.ts.map +0 -1
- package/dist-lib/lib/logisticsEngine/wizard.functions.js +0 -175
- package/dist-lib/lib/serverRoutes/logisticsTick.handler.d.ts +0 -3
- package/dist-lib/lib/serverRoutes/logisticsTick.handler.d.ts.map +0 -1
- package/dist-lib/lib/serverRoutes/logisticsTick.handler.js +0 -42
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Alle datum-/tijdlogica in deze module rekent met de tijdzone van Amsterdam,
|
|
3
|
+
* ongeacht de tijdzone van het apparaat van de gebruiker of de server.
|
|
4
|
+
*/
|
|
5
|
+
export const APP_TIME_ZONE = "Europe/Amsterdam";
|
|
6
|
+
const partsFormatter = new Intl.DateTimeFormat("en-CA", {
|
|
7
|
+
timeZone: APP_TIME_ZONE,
|
|
8
|
+
year: "numeric",
|
|
9
|
+
month: "2-digit",
|
|
10
|
+
day: "2-digit",
|
|
11
|
+
hour: "2-digit",
|
|
12
|
+
minute: "2-digit",
|
|
13
|
+
second: "2-digit",
|
|
14
|
+
hour12: false,
|
|
15
|
+
});
|
|
16
|
+
/** Splits een datum op in kalenderonderdelen zoals Amsterdam ze ziet. */
|
|
17
|
+
export function amsterdamParts(input) {
|
|
18
|
+
const d = input instanceof Date ? input : new Date(input);
|
|
19
|
+
if (Number.isNaN(d.getTime()))
|
|
20
|
+
return null;
|
|
21
|
+
const p = Object.fromEntries(partsFormatter.formatToParts(d).filter((x) => x.type !== "literal").map((x) => [x.type, Number(x.value)]));
|
|
22
|
+
return {
|
|
23
|
+
year: p.year,
|
|
24
|
+
month: p.month,
|
|
25
|
+
day: p.day,
|
|
26
|
+
hour: p.hour === 24 ? 0 : p.hour,
|
|
27
|
+
minute: p.minute,
|
|
28
|
+
second: p.second,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
32
|
+
/** YYYY-MM-DD in Amsterdamse tijd. */
|
|
33
|
+
export function amsterdamDay(input) {
|
|
34
|
+
const p = amsterdamParts(input);
|
|
35
|
+
return p ? `${p.year}-${pad(p.month)}-${pad(p.day)}` : "";
|
|
36
|
+
}
|
|
37
|
+
/** HH:MM in Amsterdamse tijd. */
|
|
38
|
+
export function amsterdamTime(input) {
|
|
39
|
+
const p = amsterdamParts(input);
|
|
40
|
+
return p ? `${pad(p.hour)}:${pad(p.minute)}` : "";
|
|
41
|
+
}
|
|
42
|
+
/** Offset (in minuten) van Amsterdam t.o.v. UTC op dat moment. */
|
|
43
|
+
function amsterdamOffsetMinutes(utcMillis) {
|
|
44
|
+
const p = amsterdamParts(new Date(utcMillis));
|
|
45
|
+
if (!p)
|
|
46
|
+
return 0;
|
|
47
|
+
const asUtc = Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute, p.second);
|
|
48
|
+
return (asUtc - utcMillis) / 60000;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Bouw een echte tijdstempel uit een Amsterdamse kalenderdag + kloktijd.
|
|
52
|
+
* Houdt rekening met zomer-/wintertijd.
|
|
53
|
+
*/
|
|
54
|
+
export function amsterdamToInstant(day, hour = 8, minute = 0) {
|
|
55
|
+
const [y, m, d] = day.split("-").map(Number);
|
|
56
|
+
const naive = Date.UTC(y ?? 1970, (m ?? 1) - 1, d ?? 1, hour, minute, 0, 0);
|
|
57
|
+
let millis = naive - amsterdamOffsetMinutes(naive) * 60000;
|
|
58
|
+
// Tweede pass voor DST-overgangen.
|
|
59
|
+
millis = naive - amsterdamOffsetMinutes(millis) * 60000;
|
|
60
|
+
return new Date(millis);
|
|
61
|
+
}
|
|
62
|
+
/** Datum weergeven in Amsterdamse tijd. */
|
|
63
|
+
export function formatAmsterdam(input, options = { dateStyle: "full", timeStyle: "short" }) {
|
|
64
|
+
const d = input instanceof Date ? input : new Date(input);
|
|
65
|
+
if (Number.isNaN(d.getTime()))
|
|
66
|
+
return "";
|
|
67
|
+
return d.toLocaleString("nl-NL", { timeZone: APP_TIME_ZONE, ...options });
|
|
68
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flowselections/floriday-afhandeling-module",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.26",
|
|
4
4
|
"private": false,
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"type": "module",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"@cloudflare/vite-plugin": "^1.25.5",
|
|
48
48
|
"@eslint/js": "^9.32.0",
|
|
49
49
|
"@flowselections/core": "1.0.16",
|
|
50
|
-
"@lovable.dev/vite-tanstack-config": "2.
|
|
50
|
+
"@lovable.dev/vite-tanstack-config": "2.9.0",
|
|
51
51
|
"@rsbuild/core": "^1.0.0",
|
|
52
52
|
"@tailwindcss/vite": "^4.2.1",
|
|
53
53
|
"@tanstack/react-query": "^5.83.0",
|
|
@@ -99,13 +99,15 @@
|
|
|
99
99
|
"export": "PickstationPage",
|
|
100
100
|
"route": "pickstation",
|
|
101
101
|
"title": "Pickstation",
|
|
102
|
-
"nav": false
|
|
102
|
+
"nav": false,
|
|
103
|
+
"public": true
|
|
103
104
|
},
|
|
104
105
|
{
|
|
105
106
|
"export": "KioskControle",
|
|
106
107
|
"route": "controle",
|
|
107
108
|
"title": "KioskControle",
|
|
108
|
-
"nav": false
|
|
109
|
+
"nav": false,
|
|
110
|
+
"public": true
|
|
109
111
|
},
|
|
110
112
|
{
|
|
111
113
|
"export": "VervoerdersPage",
|
|
@@ -124,23 +126,6 @@
|
|
|
124
126
|
"route": "afhandeling/vervoerders/$carrierId/bewerken",
|
|
125
127
|
"title": "Vervoerder bewerken",
|
|
126
128
|
"nav": false
|
|
127
|
-
},
|
|
128
|
-
{
|
|
129
|
-
"export": "StrategieenPage",
|
|
130
|
-
"route": "afhandeling/engine/strategieen",
|
|
131
|
-
"title": "Logistieke Strategieën"
|
|
132
|
-
},
|
|
133
|
-
{
|
|
134
|
-
"export": "FormulesPage",
|
|
135
|
-
"route": "afhandeling/engine/formules",
|
|
136
|
-
"title": "Formules",
|
|
137
|
-
"nav": false
|
|
138
|
-
},
|
|
139
|
-
{
|
|
140
|
-
"export": "CapaciteitsmaatregelenPage",
|
|
141
|
-
"route": "afhandeling/engine/capaciteit",
|
|
142
|
-
"title": "Capaciteitsmaatregelen",
|
|
143
|
-
"nav": false
|
|
144
129
|
}
|
|
145
130
|
]
|
|
146
131
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"CapaciteitsmaatregelenPage.d.ts","sourceRoot":"","sources":["../../../src/components/engine/CapaciteitsmaatregelenPage.tsx"],"names":[],"mappings":"AAiBA,wBAAgB,0BAA0B,gCAyIzC"}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useState } from "react";
|
|
3
|
-
import { useMutation, useQueryClient, useSuspenseQuery } from "@tanstack/react-query";
|
|
4
|
-
import { useServerFn } from "@tanstack/react-start";
|
|
5
|
-
import { Card, CardContent, CardHeader, CardTitle, CardDescription, Button, Input, Label, Switch, Separator, toast, } from "@flowselections/core";
|
|
6
|
-
import { ChevronDown, ChevronUp, Save, ShieldCheck } from "lucide-react";
|
|
7
|
-
import { DEFAULT_CAPACITY_SETTINGS, MEASURE_LABELS, MEASURE_UNITS, } from "../../lib/logisticsEngine/safetyLayer";
|
|
8
|
-
import { getCapacitySettings, saveCapacitySettings } from "../../lib/logisticsEngine/capacity.functions";
|
|
9
|
-
export function CapaciteitsmaatregelenPage() {
|
|
10
|
-
const getFn = useServerFn(getCapacitySettings);
|
|
11
|
-
const saveFn = useServerFn(saveCapacitySettings);
|
|
12
|
-
const qc = useQueryClient();
|
|
13
|
-
const { data } = useSuspenseQuery({
|
|
14
|
-
queryKey: ["capacity-settings"],
|
|
15
|
-
queryFn: () => getFn(),
|
|
16
|
-
});
|
|
17
|
-
const [settings, setSettings] = useState(data.settings ?? DEFAULT_CAPACITY_SETTINGS);
|
|
18
|
-
useEffect(() => { setSettings(data.settings ?? DEFAULT_CAPACITY_SETTINGS); }, [data.settings]);
|
|
19
|
-
const save = useMutation({
|
|
20
|
-
mutationFn: () => saveFn({ data: { settings } }),
|
|
21
|
-
onSuccess: () => {
|
|
22
|
-
toast.success("Instellingen opgeslagen");
|
|
23
|
-
qc.invalidateQueries({ queryKey: ["capacity-settings"] });
|
|
24
|
-
},
|
|
25
|
-
onError: (e) => toast.error(e instanceof Error ? e.message : "Opslaan mislukt"),
|
|
26
|
-
});
|
|
27
|
-
const move = (idx, dir) => {
|
|
28
|
-
const next = [...settings.measures];
|
|
29
|
-
const j = idx + dir;
|
|
30
|
-
if (j < 0 || j >= next.length)
|
|
31
|
-
return;
|
|
32
|
-
[next[idx], next[j]] = [next[j], next[idx]];
|
|
33
|
-
setSettings({ ...settings, measures: next });
|
|
34
|
-
};
|
|
35
|
-
const patch = (idx, p) => {
|
|
36
|
-
const next = settings.measures.map((m, i) => (i === idx ? { ...m, ...p } : m));
|
|
37
|
-
setSettings({ ...settings, measures: next });
|
|
38
|
-
};
|
|
39
|
-
return (_jsxs("main", { className: "p-6 space-y-6 max-w-4xl", children: [_jsxs("div", { className: "flex items-start justify-between gap-4", children: [_jsxs("div", { children: [_jsxs("h1", { className: "text-2xl font-semibold flex items-center gap-2", children: [_jsx(ShieldCheck, { className: "w-6 h-6" }), " Capaciteitsmaatregelen"] }), _jsx("p", { className: "text-sm text-muted-foreground", children: "Bepaal welke maatregelen de engine mag inzetten wanneer de planning niet meer haalbaar is, en in welke volgorde." })] }), _jsxs(Button, { onClick: () => save.mutate(), disabled: save.isPending, children: [_jsx(Save, { className: "w-4 h-4 mr-2" }), " Opslaan"] })] }), _jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsx(CardTitle, { className: "text-base", children: "Drempels en modus" }), _jsx(CardDescription, { children: "Wanneer wordt de planning oranje of rood en mag de engine zelf ingrijpen?" })] }), _jsxs(CardContent, { className: "grid gap-4 sm:grid-cols-2", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { children: "Oranje drempel (min reserve)" }), _jsx(Input, { type: "number", value: settings.amberThresholdMinutes, onChange: (e) => setSettings({ ...settings, amberThresholdMinutes: Number(e.target.value) || 0 }) })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { children: "Rood drempel (min reserve)" }), _jsx(Input, { type: "number", value: settings.redThresholdMinutes, onChange: (e) => setSettings({ ...settings, redThresholdMinutes: Number(e.target.value) || 0 }) })] }), _jsxs("div", { className: "flex items-center justify-between rounded-md border p-3", children: [_jsxs("div", { children: [_jsx("div", { className: "text-sm font-medium", children: "Automatisch toepassen" }), _jsx("div", { className: "text-xs text-muted-foreground", children: "Voer maatregelen automatisch uit zodra ze nodig zijn." })] }), _jsx(Switch, { checked: settings.autoApply, onCheckedChange: (v) => setSettings({ ...settings, autoApply: Boolean(v), warningOnly: v ? false : settings.warningOnly }) })] }), _jsxs("div", { className: "flex items-center justify-between rounded-md border p-3", children: [_jsxs("div", { children: [_jsx("div", { className: "text-sm font-medium", children: "Nooit automatisch ingrijpen" }), _jsx("div", { className: "text-xs text-muted-foreground", children: "Alleen waarschuwingen tonen, geen maatregelen voorstellen." })] }), _jsx(Switch, { checked: settings.warningOnly, onCheckedChange: (v) => setSettings({ ...settings, warningOnly: Boolean(v), autoApply: v ? false : settings.autoApply }) })] })] })] }), _jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsx(CardTitle, { className: "text-base", children: "Maatregelen op volgorde" }), _jsx(CardDescription, { children: "De engine probeert maatregelen van boven naar beneden totdat het tekort is opgelost." })] }), _jsx(CardContent, { className: "space-y-3", children: settings.measures.map((m, idx) => (_jsxs("div", { className: "rounded-md border p-3", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsxs("div", { className: "flex flex-col", children: [_jsx("button", { className: "p-1 text-muted-foreground hover:text-foreground disabled:opacity-30", disabled: idx === 0, onClick: () => move(idx, -1), "aria-label": "Omhoog", children: _jsx(ChevronUp, { className: "w-4 h-4" }) }), _jsx("button", { className: "p-1 text-muted-foreground hover:text-foreground disabled:opacity-30", disabled: idx === settings.measures.length - 1, onClick: () => move(idx, 1), "aria-label": "Omlaag", children: _jsx(ChevronDown, { className: "w-4 h-4" }) })] }), _jsxs("div", { className: "flex-1", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsxs("div", { className: "font-medium", children: [idx + 1, ". ", MEASURE_LABELS[m.kind]] }), _jsxs("div", { className: "text-xs text-muted-foreground", children: ["Prioriteit ", idx + 1] })] }), _jsx(Switch, { checked: m.enabled, onCheckedChange: (v) => patch(idx, { enabled: Boolean(v) }) })] }), m.enabled && (_jsxs("div", { className: "mt-3 grid gap-3 sm:grid-cols-2", children: [_jsxs("div", { children: [_jsxs(Label, { children: ["Maximum (", MEASURE_UNITS[m.kind], ")"] }), _jsx(Input, { type: "number", value: m.max, onChange: (e) => patch(idx, { max: Number(e.target.value) || 0 }) })] }), m.kind === "extra-pickers" && (_jsxs("div", { children: [_jsx(Label, { children: "Karren/uur per picker" }), _jsx(Input, { type: "number", value: m.perUnit ?? 10, onChange: (e) => patch(idx, { perUnit: Number(e.target.value) || 0 }) })] }))] }))] })] }), idx < settings.measures.length - 1 && _jsx(Separator, { className: "mt-3" })] }, m.kind))) })] })] }));
|
|
40
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"CapaciteitsmonitorPage.d.ts","sourceRoot":"","sources":["../../../src/components/engine/CapaciteitsmonitorPage.tsx"],"names":[],"mappings":"AAuBA,wBAAgB,sBAAsB,gCAiLrC"}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useMemo, useState } from "react";
|
|
3
|
-
import { useQuery } from "@tanstack/react-query";
|
|
4
|
-
import { useServerFn } from "@tanstack/react-start";
|
|
5
|
-
import { Card, CardContent, CardHeader, CardTitle, CardDescription, Input, Label, Badge, Separator, } from "@flowselections/core";
|
|
6
|
-
import { Activity, AlertTriangle, CheckCircle2, PlayCircle, TimerReset } from "lucide-react";
|
|
7
|
-
import { DEFAULT_CAPACITY_SETTINGS, evaluateCapacity, } from "../../lib/logisticsEngine/safetyLayer";
|
|
8
|
-
import { getCapacitySettings, listCapacityEvents } from "../../lib/logisticsEngine/capacity.functions";
|
|
9
|
-
function statusMeta(s) {
|
|
10
|
-
if (s === "green")
|
|
11
|
-
return { color: "bg-emerald-500", label: "Groen — planning haalbaar", icon: CheckCircle2 };
|
|
12
|
-
if (s === "amber")
|
|
13
|
-
return { color: "bg-amber-500", label: "Oranje — planning wordt kritisch", icon: AlertTriangle };
|
|
14
|
-
return { color: "bg-red-500", label: "Rood — planning niet meer haalbaar", icon: AlertTriangle };
|
|
15
|
-
}
|
|
16
|
-
export function CapaciteitsmonitorPage() {
|
|
17
|
-
const getSettingsFn = useServerFn(getCapacitySettings);
|
|
18
|
-
const listEventsFn = useServerFn(listCapacityEvents);
|
|
19
|
-
const settingsQ = useQuery({ queryKey: ["capacity-settings"], queryFn: () => getSettingsFn() });
|
|
20
|
-
const eventsQ = useQuery({ queryKey: ["capacity-events"], queryFn: () => listEventsFn(), refetchInterval: 30000 });
|
|
21
|
-
const settings = settingsQ.data?.settings ?? DEFAULT_CAPACITY_SETTINGS;
|
|
22
|
-
const [input, setInput] = useState({
|
|
23
|
-
remainingWorkHours: 4,
|
|
24
|
-
cartsPerHour: 10,
|
|
25
|
-
pickers: 1,
|
|
26
|
-
requiredCarts: 40,
|
|
27
|
-
remainingBreakMinutes: 30,
|
|
28
|
-
earlierStartRoom: 1,
|
|
29
|
-
});
|
|
30
|
-
const [extraSim, setExtraSim] = useState(10);
|
|
31
|
-
const evalNow = useMemo(() => evaluateCapacity(input, settings), [input, settings]);
|
|
32
|
-
const evalSim = useMemo(() => evaluateCapacity({ ...input, requiredCarts: input.requiredCarts + extraSim }, settings), [input, extraSim, settings]);
|
|
33
|
-
const meta = statusMeta(evalNow.status);
|
|
34
|
-
const Icon = meta.icon;
|
|
35
|
-
return (_jsxs("main", { className: "p-6 space-y-6 max-w-6xl", children: [_jsxs("div", { children: [_jsxs("h1", { className: "text-2xl font-semibold flex items-center gap-2", children: [_jsx(Activity, { className: "w-6 h-6" }), " Capaciteitsmonitor"] }), _jsx("p", { className: "text-sm text-muted-foreground", children: "Live gezondheid van de logistieke planning. Simuleer extra orders en bekijk welke maatregelen ingezet zouden worden." })] }), _jsxs("div", { className: "grid gap-4 lg:grid-cols-3", children: [_jsxs(Card, { className: "lg:col-span-2", children: [_jsx(CardHeader, { className: "pb-2", children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("span", { className: `inline-flex h-8 w-8 items-center justify-center rounded-full ${meta.color}`, children: _jsx(Icon, { className: "w-4 h-4 text-white" }) }), _jsxs("div", { children: [_jsx(CardTitle, { className: "text-base", children: meta.label }), _jsx(CardDescription, { children: evalNow.reason })] })] }) }), _jsxs(CardContent, { className: "grid gap-4 sm:grid-cols-4", children: [_jsx(Stat, { label: "Beschikbaar", value: `${evalNow.availableCarts} karren` }), _jsx(Stat, { label: "Benodigd", value: `${evalNow.requiredCarts} karren` }), _jsx(Stat, { label: "Reserve", value: `${evalNow.reserveMinutes} min` }), _jsx(Stat, { label: "Verwachte eindtijd", value: `+${evalNow.expectedFinishMinutes} min` })] }), _jsxs(CardContent, { children: [_jsx("div", { className: "text-sm font-medium mb-2", children: "Actievoorstellen" }), evalNow.measures.length === 0 ? (_jsx("div", { className: "text-xs text-muted-foreground", children: "Geen maatregelen nodig \u2014 planning past binnen de capaciteit." })) : (_jsx("ul", { className: "space-y-2", children: evalNow.measures.map((m, i) => (_jsxs("li", { className: "flex items-center justify-between rounded-md border p-2 text-sm", children: [_jsxs("div", { children: [_jsxs("div", { className: "font-medium", children: ["\u2714 ", m.label] }), _jsxs("div", { className: "text-xs text-muted-foreground", children: [m.amount, " ", m.unit, m.addsCarts > 0 ? ` → +${m.addsCarts} karren capaciteit` : ""] })] }), _jsxs(Badge, { variant: "secondary", children: ["stap ", i + 1] })] }, i))) })), evalNow.stillShortAfterMeasures > 0 && (_jsxs("div", { className: "mt-2 text-xs text-red-600", children: ["Zelfs met alle maatregelen blijft er een tekort van ", evalNow.stillShortAfterMeasures, " karren."] }))] })] }), _jsxs(Card, { children: [_jsxs(CardHeader, { className: "pb-2", children: [_jsxs(CardTitle, { className: "text-base flex items-center gap-2", children: [_jsx(TimerReset, { className: "w-4 h-4" }), " Huidige situatie"] }), _jsx(CardDescription, { children: "Pas aan om de monitor door te rekenen." })] }), _jsx(CardContent, { className: "space-y-2", children: [
|
|
36
|
-
["remainingWorkHours", "Resterende werkuren"],
|
|
37
|
-
["cartsPerHour", "Karren/uur (per picker)"],
|
|
38
|
-
["pickers", "Aantal pickers"],
|
|
39
|
-
["requiredCarts", "Benodigde karren"],
|
|
40
|
-
["remainingBreakMinutes", "Resterende pauze (min)"],
|
|
41
|
-
["earlierStartRoom", "Ruimte om eerder te starten (uur)"],
|
|
42
|
-
].map(([key, label]) => (_jsxs("div", { className: "grid grid-cols-2 items-center gap-2", children: [_jsx(Label, { className: "text-xs", children: label }), _jsx(Input, { type: "number", value: input[key], onChange: (e) => setInput({ ...input, [key]: Number(e.target.value) || 0 }) })] }, key))) })] })] }), _jsxs(Card, { children: [_jsxs(CardHeader, { className: "pb-2", children: [_jsxs(CardTitle, { className: "text-base flex items-center gap-2", children: [_jsx(PlayCircle, { className: "w-4 h-4" }), " Simulatie"] }), _jsx(CardDescription, { children: "Test wat er gebeurt bij een plotselinge extra order." })] }), _jsxs(CardContent, { className: "space-y-3", children: [_jsxs("div", { className: "flex items-end gap-3", children: [_jsxs("div", { className: "w-40", children: [_jsx(Label, { className: "text-xs", children: "Extra karren" }), _jsx(Input, { type: "number", value: extraSim, onChange: (e) => setExtraSim(Number(e.target.value) || 0) })] }), _jsxs("div", { className: "text-sm", children: [_jsx("span", { className: "font-medium", children: "Resultaat: " }), _jsx(Badge, { variant: evalSim.status === "green" ? "secondary" : "destructive", children: evalSim.status === "green" ? "Haalbaar" : evalSim.status === "amber" ? "Kritisch" : "Niet haalbaar" })] })] }), _jsx("div", { className: "text-sm text-muted-foreground", children: evalSim.reason }), evalSim.measures.length > 0 && (_jsx("ul", { className: "text-sm list-disc list-inside", children: evalSim.measures.map((m, i) => (_jsxs("li", { children: [m.label, " \u2014 ", m.amount, " ", m.unit, m.addsCarts > 0 ? ` (+${m.addsCarts} karren)` : ""] }, i))) }))] })] }), _jsxs(Card, { children: [_jsxs(CardHeader, { className: "pb-2", children: [_jsx(CardTitle, { className: "text-base", children: "Auditlog" }), _jsx(CardDescription, { children: "Laatste 50 capaciteitsberekeningen." })] }), _jsx(CardContent, { children: eventsQ.isLoading ? (_jsx("div", { className: "text-xs text-muted-foreground", children: "Laden\u2026" })) : (eventsQ.data?.events ?? []).length === 0 ? (_jsx("div", { className: "text-xs text-muted-foreground", children: "Nog geen evaluaties gelogd." })) : (_jsx("ul", { className: "divide-y", children: (eventsQ.data?.events ?? []).map((ev) => {
|
|
43
|
-
const m = statusMeta(ev.status);
|
|
44
|
-
return (_jsxs("li", { className: "py-2 flex items-center gap-3 text-sm", children: [_jsx("span", { className: `h-2 w-2 rounded-full ${m.color}` }), _jsx("span", { className: "w-40 text-muted-foreground text-xs", children: new Date(ev.created_at).toLocaleString("nl-NL") }), _jsxs("span", { className: "flex-1", children: ["Benodigd ", ev.required_carts, ", beschikbaar ", ev.available_carts, ", reserve ", ev.reserve_minutes, " min", ev.shortage_carts > 0 ? ` — tekort ${ev.shortage_carts}` : ""] }), _jsx(Badge, { variant: "outline", children: ev.outcome })] }, ev.id));
|
|
45
|
-
}) })) })] }), _jsx(Separator, {}), _jsxs("p", { className: "text-xs text-muted-foreground", children: ["Instellingen aanpassen? Ga naar ", _jsx("a", { className: "underline", href: "/afhandeling/engine/capaciteit", children: "Capaciteitsmaatregelen" }), "."] })] }));
|
|
46
|
-
}
|
|
47
|
-
function Stat({ label, value }) {
|
|
48
|
-
return (_jsxs("div", { className: "rounded-md border p-3", children: [_jsx("div", { className: "text-xs text-muted-foreground", children: label }), _jsx("div", { className: "text-lg font-semibold", children: value })] }));
|
|
49
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"FormulesPage.d.ts","sourceRoot":"","sources":["../../../src/components/engine/FormulesPage.tsx"],"names":[],"mappings":"AA8DA,wBAAgB,YAAY,gCA2L3B"}
|
|
@@ -1,252 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useMemo, useState } from "react";
|
|
3
|
-
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
4
|
-
import { useServerFn } from "@tanstack/react-start";
|
|
5
|
-
import { useSearch } from "@tanstack/react-router";
|
|
6
|
-
import { Card, CardContent, CardHeader, CardTitle, CardDescription, Button, Input, Label, Badge, Checkbox, Switch, Separator, Textarea, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, toast, } from "@flowselections/core";
|
|
7
|
-
import { Save, Sparkles, Code2, Loader2 } from "lucide-react";
|
|
8
|
-
import { listStrategies } from "../../lib/logisticsEngine/strategies.functions";
|
|
9
|
-
import { loadWizardConfig, saveWizardConfig, aiSuggestConfig } from "../../lib/logisticsEngine/wizard.functions";
|
|
10
|
-
import { DEFAULT_CONFIG, compileConfig, summarizeConfig, computePreview, } from "../../lib/logisticsEngine/config";
|
|
11
|
-
import { WeekdayPicker } from "../afhandeling/WeekdayPicker";
|
|
12
|
-
function normalizeConfig(c) {
|
|
13
|
-
const pv = (c.pickstationVisibility ?? {});
|
|
14
|
-
const d = DEFAULT_CONFIG.pickstationVisibility;
|
|
15
|
-
const legacy = pv;
|
|
16
|
-
const pickstationVisibility = {
|
|
17
|
-
mode: (pv.mode ?? "auto"),
|
|
18
|
-
logistics: {
|
|
19
|
-
batchClosed: pv.logistics?.batchClosed ?? legacy.batchClosed ?? d.logistics.batchClosed,
|
|
20
|
-
cartBuilt: pv.logistics?.cartBuilt ?? d.logistics.cartBuilt,
|
|
21
|
-
cartFull: pv.logistics?.cartFull ?? d.logistics.cartFull,
|
|
22
|
-
plannerApproved: pv.logistics?.plannerApproved ?? legacy.plannerApproved ?? d.logistics.plannerApproved,
|
|
23
|
-
capacityReleased: pv.logistics?.capacityReleased ?? d.logistics.capacityReleased,
|
|
24
|
-
},
|
|
25
|
-
documents: {
|
|
26
|
-
packingSlip: pv.documents?.packingSlip ?? legacy.packingSlip ?? d.documents.packingSlip,
|
|
27
|
-
deliveryNote: pv.documents?.deliveryNote ?? d.documents.deliveryNote,
|
|
28
|
-
transportLabel: pv.documents?.transportLabel ?? d.documents.transportLabel,
|
|
29
|
-
},
|
|
30
|
-
control: (pv.control ?? (legacy.controlDone ? "cart" : legacy.manual ? "manual" : "none")),
|
|
31
|
-
};
|
|
32
|
-
const sm = (c.startMoment ?? {});
|
|
33
|
-
const startMoment = {
|
|
34
|
-
autoWhenCapacity: sm.autoWhenCapacity ?? DEFAULT_CONFIG.startMoment.autoWhenCapacity,
|
|
35
|
-
safetyBufferMinutes: sm.safetyBufferMinutes ?? DEFAULT_CONFIG.startMoment.safetyBufferMinutes,
|
|
36
|
-
capacityMode: sm.capacityMode ?? DEFAULT_CONFIG.startMoment.capacityMode,
|
|
37
|
-
pickCapacityPerHour: sm.pickCapacityPerHour ?? DEFAULT_CONFIG.startMoment.pickCapacityPerHour,
|
|
38
|
-
minutesPerCart: sm.minutesPerCart ?? DEFAULT_CONFIG.startMoment.minutesPerCart,
|
|
39
|
-
};
|
|
40
|
-
const continuous = { ...DEFAULT_CONFIG.continuous, ...(c.continuous ?? {}), enabled: true };
|
|
41
|
-
const workingSchedule = {
|
|
42
|
-
...DEFAULT_CONFIG.workingSchedule,
|
|
43
|
-
...(c.workingSchedule ?? {}),
|
|
44
|
-
breaks: (c.workingSchedule?.breaks ?? DEFAULT_CONFIG.workingSchedule.breaks) ?? [],
|
|
45
|
-
};
|
|
46
|
-
const pickPlanningRelease = {
|
|
47
|
-
...DEFAULT_CONFIG.pickPlanningRelease,
|
|
48
|
-
...(c.pickPlanningRelease ?? {}),
|
|
49
|
-
};
|
|
50
|
-
return { ...c, pickstationVisibility, startMoment, continuous, workingSchedule, pickPlanningRelease };
|
|
51
|
-
}
|
|
52
|
-
export function FormulesPage() {
|
|
53
|
-
const search = useSearch({ strict: false });
|
|
54
|
-
const qc = useQueryClient();
|
|
55
|
-
const fetchStrategies = useServerFn(listStrategies);
|
|
56
|
-
const loadCfg = useServerFn(loadWizardConfig);
|
|
57
|
-
const saveCfg = useServerFn(saveWizardConfig);
|
|
58
|
-
const aiCfg = useServerFn(aiSuggestConfig);
|
|
59
|
-
const stratQ = useQuery({
|
|
60
|
-
queryKey: ["logistics", "strategies"],
|
|
61
|
-
queryFn: async () => (await fetchStrategies()).strategies,
|
|
62
|
-
});
|
|
63
|
-
const [strategyId, setStrategyId] = useState(null);
|
|
64
|
-
useEffect(() => {
|
|
65
|
-
if (!strategyId) {
|
|
66
|
-
const first = search.strategy ?? stratQ.data?.[0]?.id ?? null;
|
|
67
|
-
if (first)
|
|
68
|
-
setStrategyId(first);
|
|
69
|
-
}
|
|
70
|
-
}, [stratQ.data, search.strategy, strategyId]);
|
|
71
|
-
const cfgQ = useQuery({
|
|
72
|
-
queryKey: ["logistics", "wizard", strategyId],
|
|
73
|
-
queryFn: async () => (await loadCfg({ data: { strategy_id: strategyId } })).config,
|
|
74
|
-
enabled: !!strategyId,
|
|
75
|
-
});
|
|
76
|
-
const [config, setConfig] = useState(DEFAULT_CONFIG);
|
|
77
|
-
useEffect(() => { if (cfgQ.data)
|
|
78
|
-
setConfig(normalizeConfig(cfgQ.data)); }, [cfgQ.data]);
|
|
79
|
-
const update = (patch) => {
|
|
80
|
-
setConfig((prev) => typeof patch === "function" ? patch(prev) : { ...prev, ...patch });
|
|
81
|
-
void 0;
|
|
82
|
-
};
|
|
83
|
-
const saveMut = useMutation({
|
|
84
|
-
mutationFn: () => saveCfg({ data: { strategy_id: strategyId, config } }),
|
|
85
|
-
onSuccess: () => {
|
|
86
|
-
qc.invalidateQueries({ queryKey: ["logistics", "wizard", strategyId] });
|
|
87
|
-
qc.invalidateQueries({ queryKey: ["logistics", "formulas", strategyId] });
|
|
88
|
-
toast.success("Configuratie opgeslagen");
|
|
89
|
-
},
|
|
90
|
-
onError: (e) => toast.error(e.message),
|
|
91
|
-
});
|
|
92
|
-
const compiled = useMemo(() => compileConfig(config), [config]);
|
|
93
|
-
const summary = useMemo(() => summarizeConfig(config), [config]);
|
|
94
|
-
const preview = useMemo(() => computePreview(config), [config]);
|
|
95
|
-
const [advanced, setAdvanced] = useState(false);
|
|
96
|
-
const [aiOpen, setAiOpen] = useState(false);
|
|
97
|
-
const [aiText, setAiText] = useState("");
|
|
98
|
-
const [aiBusy, setAiBusy] = useState(false);
|
|
99
|
-
const runAi = async () => {
|
|
100
|
-
if (!aiText.trim())
|
|
101
|
-
return;
|
|
102
|
-
setAiBusy(true);
|
|
103
|
-
try {
|
|
104
|
-
const r = await aiCfg({ data: { description: aiText, current: config } });
|
|
105
|
-
setConfig(r.config);
|
|
106
|
-
setAiOpen(false);
|
|
107
|
-
setAiText("");
|
|
108
|
-
toast.success("Configuratie ingevuld door AI — controleer en sla op");
|
|
109
|
-
}
|
|
110
|
-
catch (e) {
|
|
111
|
-
toast.error(e.message);
|
|
112
|
-
}
|
|
113
|
-
finally {
|
|
114
|
-
setAiBusy(false);
|
|
115
|
-
}
|
|
116
|
-
};
|
|
117
|
-
return (_jsxs("main", { className: "p-6 space-y-6", children: [_jsxs("div", { className: "flex items-start justify-between gap-4 flex-wrap", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-2xl font-semibold", children: "Logistieke Configurator" }), _jsx("p", { className: "text-sm text-muted-foreground", children: "Beschrijf je werkwijze \u2014 het systeem bouwt de formules automatisch." })] }), _jsx("div", { className: "flex gap-2 items-center", children: _jsxs(Button, { onClick: () => saveMut.mutate(), disabled: !strategyId || saveMut.isPending, children: [saveMut.isPending ? _jsx(Loader2, { className: "w-4 h-4 mr-2 animate-spin" }) : _jsx(Save, { className: "w-4 h-4 mr-2" }), " Opslaan"] }) })] }), _jsx("div", { className: "flex gap-3 flex-wrap items-end", children: _jsxs("div", { children: [_jsx(Label, { children: "Strategie" }), _jsxs(Select, { value: strategyId ?? "", onValueChange: setStrategyId, children: [_jsx(SelectTrigger, { className: "min-w-64", children: _jsx(SelectValue, { placeholder: "Kies strategie" }) }), _jsx(SelectContent, { children: (stratQ.data ?? []).map((s) => (_jsxs(SelectItem, { value: s.id, children: [s.name, s.is_active ? " (actief)" : ""] }, s.id))) })] })] }) }), _jsxs("div", { className: "grid gap-6 lg:grid-cols-[1fr_360px]", children: [_jsxs("div", { className: "space-y-6", children: [_jsx(Step1, { config: config, onChange: setConfig }), _jsx(Step5, { config: config, onChange: setConfig }), _jsx(Step6Combined, { config: config, onChange: setConfig }), _jsxs(Card, { children: [_jsx(CardHeader, { className: "pb-2", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsxs(CardTitle, { className: "text-base flex items-center gap-2", children: [_jsx(Code2, { className: "w-4 h-4" }), " Geavanceerde modus"] }), _jsx(CardDescription, { children: "Bekijk de automatisch gegenereerde formules." })] }), _jsx(Switch, { checked: advanced, onCheckedChange: setAdvanced })] }) }), advanced && (_jsx(CardContent, { children: _jsx("div", { className: "space-y-2 font-mono text-xs", children: compiled.map((f) => (_jsxs("div", { className: "border rounded p-2 bg-muted/40", children: [_jsx("div", { className: "text-muted-foreground text-[10px] uppercase", children: f.slot }), _jsx("div", { className: "mt-1", children: f.expression })] }, f.slot))) }) }))] })] }), _jsxs("aside", { className: "space-y-4", children: [_jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsx(CardTitle, { className: "text-base", children: "Jouw logistieke strategie" }), _jsx(CardDescription, { children: "Automatisch bijgewerkt." })] }), _jsx(CardContent, { children: _jsx("ul", { className: "text-sm space-y-1.5", children: summary.map((line, i) => _jsxs("li", { className: "flex gap-2", children: [_jsx("span", { className: "text-primary", children: "\u2022" }), _jsx("span", { children: line })] }, i)) }) })] }), _jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsx(CardTitle, { className: "text-base", children: "Praktijkvoorbeeld" }), _jsx(CardDescription, { children: "Illustratieve dag met deze instellingen." })] }), _jsxs(CardContent, { children: [_jsx(FlowLine, { label: `${preview.orders} orders vandaag` }), _jsx(FlowArrow, {}), _jsx(FlowLine, { label: `${preview.carts} karren` }), _jsx(FlowArrow, {}), _jsxs("div", { className: "text-xs text-muted-foreground pl-4 space-y-0.5", children: [_jsxs("div", { children: [preview.full, " volle karren"] }), _jsxs("div", { children: [preview.combined, " gecombineerde karren"] }), _jsxs("div", { children: [preview.rest, " restkar"] })] }), _jsx(FlowArrow, {}), _jsx(FlowLine, { label: `Pakken start om ${preview.pickStart}` }), _jsx(FlowArrow, {}), _jsx(FlowLine, { label: `Pakbonnen om ${preview.packingSlipTime}` }), _jsx(FlowArrow, {}), _jsx(FlowLine, { label: `Pickstation vanaf ${preview.pickstationStart}` })] })] })] })] }), _jsx(Dialog, { open: aiOpen, onOpenChange: setAiOpen, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: "Vertel hoe jouw logistiek werkt" }), _jsx(DialogDescription, { children: "Beschrijf in gewone taal wat je wil. De AI vertaalt dit naar instellingen." })] }), _jsx(Textarea, { rows: 8, value: aiText, onChange: (e) => setAiText(e.target.value), placeholder: `Bv. "Wij willen altijd eerst volledige karren picken. Veilingorders tot drie lagen mogen gecombineerd worden. BVO-orders nooit. We kunnen tien karren per uur picken en willen altijd dertig minuten reserve houden."` }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => setAiOpen(false), disabled: aiBusy, children: "Annuleer" }), _jsxs(Button, { onClick: runAi, disabled: aiBusy || !aiText.trim(), children: [aiBusy ? _jsx(Loader2, { className: "w-4 h-4 mr-2 animate-spin" }) : _jsx(Sparkles, { className: "w-4 h-4 mr-2" }), " Vul in"] })] })] }) })] }));
|
|
118
|
-
}
|
|
119
|
-
/* --------------------------- Building blocks --------------------------- */
|
|
120
|
-
function FlowLine({ label }) {
|
|
121
|
-
return _jsx("div", { className: "text-sm font-medium", children: label });
|
|
122
|
-
}
|
|
123
|
-
function FlowArrow() {
|
|
124
|
-
return _jsx("div", { className: "text-muted-foreground text-center text-xs my-1", children: "\u2193" });
|
|
125
|
-
}
|
|
126
|
-
function StepCard({ n, title, question, children }) {
|
|
127
|
-
return (_jsxs(Card, { children: [_jsx(CardHeader, { className: "pb-3", children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Badge, { variant: "outline", className: "rounded-full h-7 w-7 flex items-center justify-center p-0", children: n }), _jsxs("div", { children: [_jsx(CardTitle, { className: "text-base", children: title }), _jsx(CardDescription, { children: question })] })] }) }), _jsx(CardContent, { className: "space-y-4", children: children })] }));
|
|
128
|
-
}
|
|
129
|
-
function RadioRow({ value, onChange, options }) {
|
|
130
|
-
return (_jsx("div", { className: "flex flex-wrap gap-2", children: options.map((o) => {
|
|
131
|
-
const active = o.value === value;
|
|
132
|
-
return (_jsx("button", { type: "button", onClick: () => onChange(o.value), className: `px-3 py-2 rounded border text-sm transition ${active ? "border-primary bg-primary/10 text-primary font-medium" : "hover:bg-muted"}`, children: o.label }, o.value));
|
|
133
|
-
}) }));
|
|
134
|
-
}
|
|
135
|
-
function CheckLine({ checked, onChange, label }) {
|
|
136
|
-
return (_jsxs("label", { className: "flex items-center gap-2 text-sm cursor-pointer", children: [_jsx(Checkbox, { checked: checked, onCheckedChange: (v) => onChange(!!v) }), _jsx("span", { children: label })] }));
|
|
137
|
-
}
|
|
138
|
-
function Step1({ config, onChange }) {
|
|
139
|
-
return (_jsx(StepCard, { n: 1, title: "Verwerking", question: "Hoe wil je orders verwerken?", children: _jsx(RadioRow, { value: config.orderMode, onChange: (v) => onChange({ ...config, orderMode: v }), options: [
|
|
140
|
-
{ value: "per-order", label: "Per order" },
|
|
141
|
-
{ value: "per-customer", label: "Per klant" },
|
|
142
|
-
{ value: "per-location", label: "Per afleverlocatie" },
|
|
143
|
-
{ value: "per-cart", label: "Per kar" },
|
|
144
|
-
] }) }));
|
|
145
|
-
}
|
|
146
|
-
function FitExample(_) {
|
|
147
|
-
return null;
|
|
148
|
-
}
|
|
149
|
-
function Step5({ config, onChange }) {
|
|
150
|
-
const d = config.documents;
|
|
151
|
-
const set = (patch) => onChange({ ...config, documents: { ...d, ...patch } });
|
|
152
|
-
return (_jsxs(StepCard, { n: 5, title: "Documenten", question: "Wanneer wordt het document vastgesteld (definitief gegenereerd)? Printen gebeurt altijd pas in Pickstation.", children: [_jsxs("div", { className: "grid sm:grid-cols-2 gap-4", children: [_jsxs("div", { children: [_jsx(Label, { className: "mb-1 block", children: "Pakbon" }), _jsx(RadioRow, { value: d.packingSlip, onChange: (v) => set({ packingSlip: v }), options: [
|
|
153
|
-
{ value: "cart-full", label: "Bij kar vol" },
|
|
154
|
-
{ value: "batch-close", label: "Bij batch sluit" },
|
|
155
|
-
{ value: "pickstation", label: "Bij opname in Pickstation" },
|
|
156
|
-
] })] }), _jsxs("div", { children: [_jsx(Label, { className: "mb-1 block", children: "Afleverbrief" }), _jsx(RadioRow, { value: d.deliveryNote, onChange: (v) => set({ deliveryNote: v }), options: [
|
|
157
|
-
{ value: "floriday", label: "Via Floriday" },
|
|
158
|
-
{ value: "pickstation", label: "Bij opname in Pickstation" },
|
|
159
|
-
{ value: "manual", label: "Handmatig" },
|
|
160
|
-
] })] })] }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Vaststellen = het documentbestand definitief aanmaken zodat het klaarstaat. Het daadwerkelijk afdrukken gebeurt altijd pas wanneer de medewerker in Pickstation op \"Printen\" klikt." })] }));
|
|
161
|
-
}
|
|
162
|
-
function Step6Combined({ config, onChange }) {
|
|
163
|
-
return (_jsxs(StepCard, { n: 6, title: "Pickplanning & startmoment", question: "Regelt in drie deelvragen wanneer een order de buffer verlaat, wanneer de opgebouwde kar op het Pickstation verschijnt en wanneer het pickteam daadwerkelijk begint.", children: [_jsxs("div", { className: "rounded border bg-muted/40 p-3 text-xs text-muted-foreground leading-relaxed space-y-2", children: [_jsx("div", { className: "font-medium text-foreground", children: "Tijdlijn van een order" }), _jsxs("div", { className: "font-mono text-[11px] leading-5", children: ["order binnen", _jsx("br", {}), "\u00A0\u00A0\u2193 buffer \u2014 orders combineren, karren bouwen", _jsx("br", {}), _jsx("span", { className: "text-primary", children: "6a" }), " order vrijgegeven aan pickplanning", _jsx("br", {}), "\u00A0\u00A0\u2193 kar opgebouwd, batch sluit, documenten aangemaakt", _jsx("br", {}), _jsx("span", { className: "text-primary", children: "6b" }), " kar verschijnt op Pickstation", _jsx("br", {}), "\u00A0\u00A0\u2193 kar wacht op picker", _jsx("br", {}), _jsx("span", { className: "text-primary", children: "6c" }), " picker start (capaciteit + buffer)", _jsx("br", {}), "\u00A0\u00A0\u2193 picken"] })] }), _jsx(SubBlock, { n: 1, title: "Werkdagen & werktijden", question: "Op welke dagen en tussen welke tijden werkt het pickteam? Deze basis wordt gebruikt voor alle capaciteits- en tijdsberekeningen in de volgende stappen.", children: _jsx(WorkingScheduleInner, { config: config, onChange: onChange }) }), _jsx(Separator, {}), _jsx(SubBlock, { n: 2, title: `Wanneer mag ${config.orderMode === "per-cart" ? "een kar" : "een order"} de buffer uit?`, question: `Bepaalt wanneer ${config.orderMode === "per-cart" ? "een kar" : "een order"} definitief wordt ingepland. Tot dat moment kunnen ${config.orderMode === "per-cart" ? "karren nog verder opgebouwd en samengevoegd worden" : "orders nog worden samengevoegd en karren nog worden opgebouwd"}.`, children: _jsx(ReleaseInner, { config: config, onChange: onChange }) }), config.pickPlanningRelease.mode === "latest" && (_jsxs(_Fragment, { children: [_jsx(Separator, {}), _jsx(SubBlock, { n: 3, title: "Wanneer start het pickteam?", question: "Bepaalt op basis van pickcapaciteit en veiligheidsbuffer wanneer daadwerkelijk met picken wordt begonnen.", children: _jsx(StartInner, { config: config, onChange: onChange }) })] }))] }));
|
|
164
|
-
}
|
|
165
|
-
function WorkingScheduleInner({ config, onChange }) {
|
|
166
|
-
const w = config.workingSchedule;
|
|
167
|
-
const set = (patch) => onChange({ ...config, workingSchedule: { ...w, ...patch } });
|
|
168
|
-
const breaks = w.breaks ?? [];
|
|
169
|
-
const updateBreak = (i, patch) => {
|
|
170
|
-
const next = breaks.map((b, idx) => (idx === i ? { ...b, ...patch } : b));
|
|
171
|
-
set({ breaks: next });
|
|
172
|
-
};
|
|
173
|
-
const addBreak = () => set({ breaks: [...breaks, { start: "12:00", end: "12:30", label: "Pauze" }] });
|
|
174
|
-
const removeBreak = (i) => set({ breaks: breaks.filter((_, idx) => idx !== i) });
|
|
175
|
-
return (_jsxs("div", { className: "space-y-3", children: [_jsxs("div", { children: [_jsx(Label, { className: "mb-2 block", children: "Werkdagen" }), _jsx(WeekdayPicker, { value: w.weekdays, onChange: (days) => set({ weekdays: days }), idPrefix: "engine-workdays" })] }), _jsxs("div", { className: "grid grid-cols-2 gap-3", children: [_jsxs("div", { children: [_jsx(Label, { children: "Starttijd" }), _jsx(Input, { type: "time", value: w.startTime, onChange: (e) => set({ startTime: e.target.value }) })] }), _jsxs("div", { children: [_jsx(Label, { children: "Eindtijd" }), _jsx(Input, { type: "time", value: w.endTime, onChange: (e) => set({ endTime: e.target.value }) })] })] }), _jsxs("div", { className: "pt-3 border-t", children: [_jsxs("div", { className: "flex items-center justify-between mb-2", children: [_jsx(Label, { className: "block", children: "Pauzes" }), _jsx(Button, { type: "button", size: "sm", variant: "outline", onClick: addBreak, children: "+ Pauze toevoegen" })] }), _jsx("p", { className: "text-xs text-muted-foreground mb-2", children: "Pauzes worden afgetrokken van de beschikbare werktijd bij alle capaciteits- en tijdsberekeningen." }), breaks.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground italic", children: "Geen pauzes ingesteld." })) : (_jsx("div", { className: "space-y-2", children: breaks.map((b, i) => (_jsxs("div", { className: "grid grid-cols-[1fr_auto_auto_auto] gap-2 items-end", children: [_jsxs("div", { children: [_jsx(Label, { className: "text-xs", children: "Naam" }), _jsx(Input, { value: b.label ?? "", placeholder: "Bijv. Lunch", onChange: (e) => updateBreak(i, { label: e.target.value }) })] }), _jsxs("div", { children: [_jsx(Label, { className: "text-xs", children: "Van" }), _jsx(Input, { type: "time", value: b.start, onChange: (e) => updateBreak(i, { start: e.target.value }) })] }), _jsxs("div", { children: [_jsx(Label, { className: "text-xs", children: "Tot" }), _jsx(Input, { type: "time", value: b.end, onChange: (e) => updateBreak(i, { end: e.target.value }) })] }), _jsx(Button, { type: "button", size: "sm", variant: "ghost", onClick: () => removeBreak(i), "aria-label": "Pauze verwijderen", children: "\u2715" })] }, i))) }))] })] }));
|
|
176
|
-
}
|
|
177
|
-
function ReleaseInner({ config, onChange }) {
|
|
178
|
-
const p = config.pickPlanningRelease;
|
|
179
|
-
const set = (patch) => onChange({ ...config, pickPlanningRelease: { ...p, ...patch } });
|
|
180
|
-
const isCart = config.orderMode === "per-cart";
|
|
181
|
-
const U = isCart ? "kar" : "order";
|
|
182
|
-
const Us = isCart ? "Karren" : "Orders";
|
|
183
|
-
const us = isCart ? "karren" : "orders";
|
|
184
|
-
const modes = [
|
|
185
|
-
{ value: "latest", label: "Zo laat mogelijk", recommended: true,
|
|
186
|
-
hint: `Het systeem berekent continu hoeveel capaciteit nog beschikbaar is. ${Us} worden pas vrijgegeven wanneer er nog precies voldoende tijd over is om alle karren vóór vertrek van de vervoerder te picken. Zo blijven ${us} zo lang mogelijk combineerbaar.` },
|
|
187
|
-
{ value: "between-signup-window", label: "Binnen venster vóór aanmeldmoment",
|
|
188
|
-
hint: `Een ${U} mag vanaf X werkuren vóór het aanmeldmoment naar het Pickstation, tot en met het aanmeldmoment zelf. Wordt het te druk, dan mogen ${us} ook X werkuren eerder naar het Pickstation.` },
|
|
189
|
-
];
|
|
190
|
-
return (_jsxs("div", { className: "space-y-3", children: [_jsx("div", { className: "space-y-2", children: modes.map((m) => (_jsx(ModeOption, { active: p.mode === m.value, onClick: () => set({ mode: m.value }), title: m.label, recommended: m.recommended, description: m.hint }, m.value))) }), p.mode === "between-signup-window" && (_jsxs("div", { className: "border rounded p-4 space-y-4 bg-background", children: [_jsxs("div", { children: [_jsxs(Label, { children: ["Werkuren v\u00F3\u00F3r aanmeldtijd \u2014 ", us, " mogen naar het Pickstation"] }), _jsxs("p", { className: "text-xs text-muted-foreground mb-1", children: ["Vanaf zoveel werkuren v\u00F3\u00F3r het aanmeldmoment mogen ", us, " naar het Pickstation gaan."] }), _jsx(Input, { type: "number", min: 0, value: p.hoursBeforeSignupWindow, onChange: (e) => set({ hoursBeforeSignupWindow: Number(e.target.value) || 0 }), className: "max-w-32" })] }), _jsxs("div", { children: [_jsxs(Label, { children: ["Extra werkuren bij drukte \u2014 ", us, " mogen eerder naar het Pickstation"] }), _jsxs("p", { className: "text-xs text-muted-foreground mb-1", children: ["Als er te veel ", us, " in dit venster zouden vallen, mogen ", us, " zoveel werkuren eerder naar het Pickstation."] }), _jsx(Input, { type: "number", min: 0, value: p.earlyPickHours, onChange: (e) => set({ earlyPickHours: Number(e.target.value) || 0 }), className: "max-w-32" })] }), _jsxs("div", { className: "pt-2 border-t", children: [_jsxs(Label, { className: "mb-2 block", children: ["Niet-volle ", us] }), _jsxs("div", { className: "space-y-2", children: [_jsx(ModeOption, { active: !p.incompleteCartsBeforeSignup, onClick: () => set({ incompleteCartsBeforeSignup: false }), title: "Alleen na het aanmeldmoment", description: `Niet-volle ${us} blijven open tot het aanmeldmoment en gaan pas daarna naar het Pickstation, zodat ze maximaal gecombineerd kunnen worden.` }), _jsx(ModeOption, { active: p.incompleteCartsBeforeSignup, onClick: () => set({ incompleteCartsBeforeSignup: true }), title: "Ook v\u00F3\u00F3r het aanmeldmoment", description: `Niet-volle ${us} mogen ook binnen het venster vóór het aanmeldmoment al naar het Pickstation.` })] })] })] })), p.mode === "latest" && (_jsxs("div", { className: "border rounded p-4 space-y-4 bg-background", children: [_jsxs("div", { children: [_jsx("div", { className: "text-sm font-medium", children: "Buffer tussen laatste order en ophaalmoment" }), _jsx("p", { className: "text-xs text-muted-foreground mt-1", children: "Hoeveel reserve wil je altijd overhouden tussen de laatste order die nog verwerkt mag worden en het daadwerkelijke ophaalmoment van de vervoerder? Bedoeld voor spoedorders, wijzigingen, vertragingen en onverwachte extra karren." })] }), _jsxs("div", { className: "flex gap-2", children: [_jsx("button", { type: "button", onClick: () => set({ safetyBufferMode: "minutes" }), className: `px-3 py-1.5 rounded border text-sm transition ${p.safetyBufferMode === "minutes" ? "border-primary bg-primary/10 text-primary font-medium" : "hover:bg-muted"}`, children: "Vaste tijd" }), _jsx("button", { type: "button", onClick: () => set({ safetyBufferMode: "percent" }), className: `px-3 py-1.5 rounded border text-sm transition ${p.safetyBufferMode === "percent" ? "border-primary bg-primary/10 text-primary font-medium" : "hover:bg-muted"}`, children: "Percentage van picktijd" })] }), p.safetyBufferMode === "minutes" ? (_jsxs("div", { className: "flex flex-wrap gap-2", children: [[30, 60, 120].map((min) => {
|
|
191
|
-
const active = p.safetyBufferMinutes === min;
|
|
192
|
-
const label = min >= 60 ? `${min / 60} uur` : `${min} minuten`;
|
|
193
|
-
return (_jsx("button", { type: "button", onClick: () => set({ safetyBufferMinutes: min }), className: `px-3 py-2 rounded border text-sm transition ${active ? "border-primary bg-primary/10 text-primary font-medium" : "hover:bg-muted"}`, children: label }, min));
|
|
194
|
-
}), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Input, { type: "number", min: 0, value: p.safetyBufferMinutes, onChange: (e) => set({ safetyBufferMinutes: Number(e.target.value) || 0 }), className: "w-24" }), _jsx("span", { className: "text-xs text-muted-foreground", children: "min. (zelf bepalen)" })] })] })) : (_jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "flex flex-wrap gap-2", children: [[10, 20, 30, 50].map((pct) => {
|
|
195
|
-
const active = p.safetyBufferPercent === pct;
|
|
196
|
-
return (_jsxs("button", { type: "button", onClick: () => set({ safetyBufferPercent: pct }), className: `px-3 py-2 rounded border text-sm transition ${active ? "border-primary bg-primary/10 text-primary font-medium" : "hover:bg-muted"}`, children: [pct, "%"] }, pct));
|
|
197
|
-
}), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Input, { type: "number", min: 0, max: 100, value: p.safetyBufferPercent, onChange: (e) => set({ safetyBufferPercent: Math.max(0, Math.min(100, Number(e.target.value) || 0)) }), className: "w-24" }), _jsx("span", { className: "text-xs text-muted-foreground", children: "% (zelf bepalen)" })] })] }), _jsxs("p", { className: "text-xs text-muted-foreground", children: ["Voorbeeld: bij 6 uur benodigde picktijd en ", p.safetyBufferPercent, "% buffer \u2192", " ", (() => {
|
|
198
|
-
const totalMin = Math.round(360 * (p.safetyBufferPercent / 100));
|
|
199
|
-
const h = Math.floor(totalMin / 60);
|
|
200
|
-
const m = totalMin % 60;
|
|
201
|
-
return h > 0 ? `${h} uur${m ? ` en ${m} minuten` : ""}` : `${m} minuten`;
|
|
202
|
-
})(), " reserve."] })] }))] })), _jsxs("div", { className: "rounded border bg-background p-4 space-y-3", children: [_jsx("div", { className: "text-xs font-medium uppercase text-muted-foreground", children: "Live voorbeeld" }), _jsxs("div", { className: "border-t pt-3 first:border-t-0 first:pt-0 text-xs space-y-1", children: [_jsx("div", { className: "font-medium", children: "Voorbeeld 1 \u2014 Order ruim v\u00F3\u00F3r levering" }), _jsx("div", { children: "Vandaag: 1 augustus \u00B7 Leverdatum: 21 augustus" }), _jsx("div", { className: "text-muted-foreground", children: "\u2192 Order blijft buiten de pickplanning. Nieuwe orders kunnen nog gecombineerd worden." })] }), _jsxs("div", { className: "border-t pt-3 text-xs space-y-1", children: [_jsx("div", { className: "font-medium", children: "Voorbeeld 2 \u2014 Op de leverdag" }), _jsx("div", { children: "21 augustus 06:45 \u00B7 Beschikbare capaciteit: 32 karren \u00B7 Verwacht werk: 30 karren" }), _jsx("div", { className: "text-primary", children: "\u2192 Order wordt nu opgenomen in de pickplanning." })] }), _jsxs("div", { className: "border-t pt-3 text-xs space-y-1", children: [_jsx("div", { className: "font-medium", children: "Voorbeeld 3 \u2014 Nieuwe order tijdens pickronde" }), _jsx("div", { children: "21 augustus 07:20 \u00B7 Nieuwe order komt binnen" }), _jsx("div", { className: "text-muted-foreground", children: "Past deze nog binnen de beschikbare capaciteit?" }), _jsx("div", { className: "text-primary", children: "\u2714 Ja \u2192 toevoegen aan huidige batch" }), _jsx("div", { className: "text-destructive", children: "\u2716 Nee \u2192 nieuwe batch of volgende pickronde" })] })] })] }));
|
|
203
|
-
}
|
|
204
|
-
function VisibilityInner({ config, onChange }) {
|
|
205
|
-
const v = config.pickstationVisibility;
|
|
206
|
-
const setLog = (patch) => onChange({ ...config, pickstationVisibility: { ...v, logistics: { ...v.logistics, ...patch } } });
|
|
207
|
-
const setDoc = (patch) => onChange({ ...config, pickstationVisibility: { ...v, documents: { ...v.documents, ...patch } } });
|
|
208
|
-
const setMode = (mode) => onChange({ ...config, pickstationVisibility: { ...v, mode } });
|
|
209
|
-
const setCtrl = (control) => onChange({ ...config, pickstationVisibility: { ...v, control } });
|
|
210
|
-
return (_jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "space-y-3", children: [_jsx(ModeOption, { active: v.mode === "auto", onClick: () => setMode("auto"), title: "Automatisch zodra alles gereed is", recommended: true, description: "Het systeem bepaalt zelf wanneer een kar klaar is. Een kar wordt vrijgegeven zodra de kar volledig is opgebouwd, er geen producten meer bij passen, alle verplichte documenten aanwezig zijn en de batch is afgesloten. Documenten die voor het ordertype niet nodig zijn worden automatisch genegeerd." }), _jsx(ModeOption, { active: v.mode === "custom", onClick: () => setMode("custom"), title: "Automatisch volgens mijn eigen voorwaarden", description: "Kies zelf welke logistieke voorwaarden, documenten en controles vereist zijn voordat een kar zichtbaar wordt op het Pickstation." })] }), v.mode === "custom" && (_jsxs("div", { className: "space-y-5 border rounded-lg p-4 bg-muted/30", children: [_jsxs(SubBlock, { n: 1, title: "Logistiek", question: "Welke logistieke voorwaarden moeten eerst voltooid zijn?", children: [_jsx(ExplainedCheck, { checked: v.logistics.batchClosed, onChange: (x) => setLog({ batchClosed: x }), label: "Batch afgesloten", hint: "De kar wordt pas vrijgegeven wanneer er geen nieuwe orders meer aan deze batch toegevoegd kunnen worden." }), _jsx(ExplainedCheck, { checked: v.logistics.cartBuilt, onChange: (x) => setLog({ cartBuilt: x }), label: "Kar volledig opgebouwd", hint: "Alle producten zijn definitief aan de kar toegewezen." }), _jsx(ExplainedCheck, { checked: v.logistics.cartFull, onChange: (x) => setLog({ cartFull: x }), label: "Er passen geen producten meer op de kar", hint: "Het systeem heeft gecontroleerd dat er volgens alle logistieke regels geen extra lagen, fusten of gecombineerde orders meer bij kunnen." }), _jsx(ExplainedCheck, { checked: v.logistics.plannerApproved, onChange: (x) => setLog({ plannerApproved: x }), label: "Planner heeft de kar vrijgegeven", hint: "De kar verschijnt pas op het Pickstation na een expliciet akkoord van de planner." }), _jsx(ExplainedCheck, { checked: v.logistics.capacityReleased, onChange: (x) => setLog({ capacityReleased: x }), label: "Capaciteit vrijgegeven", hint: "Wachten tot er pickcapaciteit beschikbaar is voordat de kar wordt getoond." })] }), _jsxs(SubBlock, { n: 2, title: "Documenten", question: "Welke documenten moeten beschikbaar zijn?", children: [_jsx(ExplainedCheck, { checked: v.documents.packingSlip, onChange: (x) => setDoc({ packingSlip: x }), label: "Pakbon", hint: "De pakbon moet beschikbaar zijn voordat de kar naar het Pickstation gaat." }), _jsx(ExplainedCheck, { checked: v.documents.deliveryNote, onChange: (x) => setDoc({ deliveryNote: x }), label: "Afleverbrief (alleen indien nodig)", hint: "Alleen vereist wanneer de afleverbrief bij deze order verplicht is; anders wordt de controle overgeslagen." }), _jsx(ExplainedCheck, { checked: v.documents.transportLabel, onChange: (x) => setDoc({ transportLabel: x }), label: "Transportlabel", hint: "Het transportlabel is aanwezig en de kar is klaar om aangemeld te worden." })] }), _jsx(SubBlock, { n: 3, title: "Controle", question: "Moet een kar eerst gecontroleerd worden?", children: _jsx(RadioRow, { value: v.control, onChange: setCtrl, options: [
|
|
211
|
-
{ value: "none", label: "Nee" },
|
|
212
|
-
{ value: "quality", label: "Alleen kwaliteitscontrole" },
|
|
213
|
-
{ value: "cart", label: "Alleen karcontrole" },
|
|
214
|
-
{ value: "both", label: "Beide controles" },
|
|
215
|
-
{ value: "manual", label: "Handmatige vrijgave door planner" },
|
|
216
|
-
] }) })] })), _jsxs("div", { className: "rounded border bg-background p-4 space-y-3", children: [_jsx("div", { className: "text-xs font-medium uppercase text-muted-foreground", children: "Live voorbeeld" }), _jsx(ReleaseExample, { title: "Kar 14", checks: [
|
|
217
|
-
{ ok: true, label: "Batch afgesloten" },
|
|
218
|
-
{ ok: true, label: "Kar volledig opgebouwd" },
|
|
219
|
-
{ ok: true, label: "Pakbon aanwezig" },
|
|
220
|
-
{ ok: true, label: "Afleverbrief aanwezig" },
|
|
221
|
-
], outcome: "Kar wordt automatisch zichtbaar op het Pickstation", positive: true }), _jsx(ReleaseExample, { title: "Kar 18", checks: [
|
|
222
|
-
{ ok: true, label: "Batch afgesloten" },
|
|
223
|
-
{ ok: true, label: "Pakbon aanwezig" },
|
|
224
|
-
{ ok: false, label: "Afleverbrief ontbreekt" },
|
|
225
|
-
], outcome: "Kar blijft wachten", positive: false }), _jsx(ReleaseExample, { title: "Kar 22 (BVO)", checks: [
|
|
226
|
-
{ ok: true, label: "Batch afgesloten" },
|
|
227
|
-
{ ok: true, label: "Pakbon aanwezig" },
|
|
228
|
-
{ ok: true, label: "Afleverbrief aanwezig" },
|
|
229
|
-
], outcome: "Kar wordt automatisch zichtbaar op het Pickstation", positive: true })] })] }));
|
|
230
|
-
}
|
|
231
|
-
function ModeOption({ active, onClick, title, description, recommended }) {
|
|
232
|
-
return (_jsx("button", { type: "button", onClick: onClick, className: `w-full text-left rounded-lg border p-4 transition ${active ? "border-primary bg-primary/5" : "hover:bg-muted"}`, children: _jsxs("div", { className: "flex items-start gap-3", children: [_jsx("span", { className: `mt-0.5 h-4 w-4 rounded-full border-2 flex-shrink-0 ${active ? "border-primary" : "border-muted-foreground/40"}`, children: active && _jsx("span", { className: "block h-full w-full rounded-full bg-primary scale-50" }) }), _jsxs("div", { className: "flex-1", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "font-medium text-sm", children: title }), recommended && _jsx(Badge, { variant: "secondary", className: "text-[10px]", children: "Aanbevolen" })] }), _jsx("p", { className: "text-xs text-muted-foreground mt-1 leading-relaxed", children: description })] })] }) }));
|
|
233
|
-
}
|
|
234
|
-
function SubBlock({ n, title, question, children }) {
|
|
235
|
-
return (_jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Badge, { variant: "outline", className: "rounded-full h-5 w-5 flex items-center justify-center p-0 text-[10px]", children: n }), _jsx("div", { className: "text-sm font-medium", children: title })] }), _jsx("p", { className: "text-xs text-muted-foreground", children: question }), _jsx("div", { className: "space-y-2 pl-1", children: children })] }));
|
|
236
|
-
}
|
|
237
|
-
function ExplainedCheck({ checked, onChange, label, hint }) {
|
|
238
|
-
return (_jsxs("label", { className: "flex items-start gap-2 text-sm cursor-pointer", children: [_jsx(Checkbox, { checked: checked, onCheckedChange: (v) => onChange(!!v), className: "mt-0.5" }), _jsxs("div", { children: [_jsx("div", { children: label }), _jsx("div", { className: "text-xs text-muted-foreground leading-relaxed", children: hint })] })] }));
|
|
239
|
-
}
|
|
240
|
-
function ReleaseExample({ title, checks, outcome, positive }) {
|
|
241
|
-
return (_jsxs("div", { className: "border-t pt-3 first:border-t-0 first:pt-0 text-xs", children: [_jsx("div", { className: "font-medium mb-1", children: title }), _jsx("ul", { className: "space-y-0.5 mb-1", children: checks.map((c, i) => (_jsxs("li", { className: c.ok ? "text-primary" : "text-destructive", children: [c.ok ? "✔" : "✖", " ", c.label] }, i))) }), _jsxs("div", { className: `font-medium ${positive ? "text-primary" : "text-muted-foreground"}`, children: ["\u2192 ", outcome] })] }));
|
|
242
|
-
}
|
|
243
|
-
function StartInner({ config, onChange }) {
|
|
244
|
-
const s = config.startMoment;
|
|
245
|
-
const set = (patch) => onChange({ ...config, startMoment: { ...s, ...patch } });
|
|
246
|
-
return (_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Switch, { checked: s.autoWhenCapacity, onCheckedChange: (v) => set({ autoWhenCapacity: v }) }), _jsx("span", { className: "text-sm", children: "Begint automatisch wanneer capaciteit bijna bereikt is" })] }), _jsx("div", { className: "grid sm:grid-cols-2 gap-4", children: _jsxs("div", { children: [_jsx(Label, { children: "Veiligheidsbuffer (minuten)" }), _jsx(Input, { type: "number", min: 0, value: s.safetyBufferMinutes, onChange: (e) => set({ safetyBufferMinutes: Number(e.target.value) || 0 }) })] }) }), _jsxs("div", { children: [_jsx(Label, { className: "mb-2 block", children: "Hoe wordt de pickcapaciteit bepaald?" }), _jsxs("div", { className: "flex flex-wrap gap-2", children: [_jsx(Button, { type: "button", size: "sm", variant: s.capacityMode === "carts-per-hour" ? "default" : "outline", onClick: () => set({ capacityMode: "carts-per-hour" }), children: "Karren per uur" }), _jsx(Button, { type: "button", size: "sm", variant: s.capacityMode === "minutes-per-cart" ? "default" : "outline", onClick: () => set({ capacityMode: "minutes-per-cart" }), children: "Picktijd per kar" })] })] }), _jsx("div", { className: "grid sm:grid-cols-2 gap-4", children: s.capacityMode === "minutes-per-cart" ? (_jsxs("div", { children: [_jsx(Label, { children: "Picktijd per kar (minuten)" }), _jsx(Input, { type: "number", min: 1, value: s.minutesPerCart, onChange: (e) => set({ minutesPerCart: Number(e.target.value) || 1 }) }), _jsxs("p", { className: "mt-1 text-xs text-muted-foreground", children: ["Komt neer op ", s.minutesPerCart > 0 ? Math.round((60 / s.minutesPerCart) * 10) / 10 : 0, " karren per uur."] })] })) : (_jsxs("div", { children: [_jsx(Label, { children: "Pickcapaciteit (karren per uur)" }), _jsx(Input, { type: "number", min: 1, value: s.pickCapacityPerHour, onChange: (e) => set({ pickCapacityPerHour: Number(e.target.value) || 1 }) })] })) })] }));
|
|
247
|
-
}
|
|
248
|
-
function Step7Continuous({ config, onChange }) {
|
|
249
|
-
const co = config.continuous;
|
|
250
|
-
const set = (patch) => onChange({ ...config, continuous: { ...co, ...patch, enabled: true } });
|
|
251
|
-
return (_jsxs(_Fragment, { children: [_jsx(Separator, {}), _jsxs(SubBlock, { n: 4, title: "Orders na het aanmeldmoment", question: "Wat gebeurt er met nieuwe orders die n\u00E1 het aanmeldmoment binnenkomen?", children: [_jsx(ModeOption, { active: co.ordersAfterRegistration === "current-batch", onClick: () => set({ ordersAfterRegistration: "current-batch" }), title: "Toevoegen aan huidige batch", description: "De afgesloten batch wordt heropend om de order toe te voegen." }), _jsx(ModeOption, { active: co.ordersAfterRegistration === "new-batch", onClick: () => set({ ordersAfterRegistration: "new-batch" }), title: "Nieuwe batch maken", description: "Er wordt direct een nieuwe batch aangemaakt voor de order." }), _jsx(ModeOption, { active: co.ordersAfterRegistration === "next-round", onClick: () => set({ ordersAfterRegistration: "next-round" }), title: "Doorschuiven naar volgend aanmeldmoment", recommended: true, description: "De order gaat automatisch naar de volgende pickronde. Een afgesloten batch hoeft dus nooit meer gewijzigd te worden." })] }), _jsx(Separator, {}), _jsxs(SubBlock, { n: 5, title: "Pickstation altijd gevuld houden", question: "Hoe wil je het Pickstation laten werken?", children: [_jsx(ModeOption, { active: !co.keepStationFilled, onClick: () => set({ keepStationFilled: false }), title: "Alleen tonen wanneer nodig", description: "Karren verschijnen pas wanneer ze volgens de standaardvoorwaarden vrijgegeven zijn." }), _jsx(ModeOption, { active: co.keepStationFilled, onClick: () => set({ keepStationFilled: true }), title: "Altijd gevuld houden", recommended: true, description: "Zodra een picker een kar afrondt verschijnt automatisch de volgende beschikbare kar. De engine plant vooruit zodat de picker nooit hoeft te wachten." })] }), _jsx(Separator, {}), _jsxs(SubBlock, { n: 6, title: "Werkdruk gelijkmatig verdelen", question: "Hoe moet de werkdruk over de picktijd verdeeld worden?", children: [_jsx(ModeOption, { active: !co.evenWorkload, onClick: () => set({ evenWorkload: false }), title: "Zo snel mogelijk wegwerken", description: "De engine start direct met de eerste beschikbare karren, zonder gelijkmatige spreiding." }), _jsx(ModeOption, { active: co.evenWorkload, onClick: () => set({ evenWorkload: true }), title: "Zo gelijkmatig mogelijk verdelen", recommended: true, description: "De engine verdeelt de karren zo gelijkmatig mogelijk over de beschikbare picktijd zodat er een constante werkstroom ontstaat, in plaats van eerst 2 uur extreem druk en daarna niets." })] }), _jsx(Separator, {}), _jsx(SubBlock, { n: 7, title: "Prioriteit van karren", question: "De engine gebruikt automatisch de volgende volgorde:", children: _jsxs("ol", { className: "text-xs text-muted-foreground space-y-1 list-decimal pl-5", children: [_jsx("li", { children: "Volledige karren binnen de normale pickperiode." }), _jsx("li", { children: "Volledige karren die eerder gepickt moeten worden wegens capaciteitsproblemen." }), _jsx("li", { children: "Niet-volledige karren na het aanmeldmoment." }), _jsx("li", { children: "Nieuwe orders na het aanmeldmoment gaan automatisch naar de volgende batch." })] }) })] }));
|
|
252
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"PlaceholderPage.d.ts","sourceRoot":"","sources":["../../../src/components/engine/PlaceholderPage.tsx"],"names":[],"mappings":"AAGA,wBAAgB,eAAe,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,EAAE;IAC7D,KAAK,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CACtD,+BA0BA"}
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@flowselections/core";
|
|
3
|
-
import { Construction } from "lucide-react";
|
|
4
|
-
export function PlaceholderPage({ title, description, items }) {
|
|
5
|
-
return (_jsxs("main", { className: "p-6 space-y-6", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-2xl font-semibold", children: title }), _jsx("p", { className: "text-sm text-muted-foreground", children: description })] }), _jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsxs(CardTitle, { className: "flex items-center gap-2 text-base", children: [_jsx(Construction, { className: "w-4 h-4" }), " Binnenkort beschikbaar"] }), _jsx(CardDescription, { children: "Deze pagina hoort bij de Logistieke Engine en wordt in een volgende fase gebouwd." })] }), items && (_jsx(CardContent, { children: _jsx("ul", { className: "list-disc list-inside text-sm space-y-1 text-muted-foreground", children: items.map((i) => _jsx("li", { children: i }, i)) }) }))] })] }));
|
|
6
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"StrategieenPage.d.ts","sourceRoot":"","sources":["../../../src/components/engine/StrategieenPage.tsx"],"names":[],"mappings":"AAeA,wBAAgB,eAAe,gCA6J9B"}
|