@flowselections/mailbox-orders 1.0.9 → 1.0.11

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 (79) hide show
  1. package/dist-lib/components/MailboxAuthBanner.d.ts +16 -0
  2. package/dist-lib/components/MailboxAuthBanner.d.ts.map +1 -0
  3. package/dist-lib/components/MailboxAuthBanner.js +14 -0
  4. package/dist-lib/components/MailboxProcessedPage.d.ts.map +1 -1
  5. package/dist-lib/components/MailboxProcessedPage.js +10 -2
  6. package/dist-lib/components/MailboxProposalsPage.d.ts +16 -0
  7. package/dist-lib/components/MailboxProposalsPage.d.ts.map +1 -1
  8. package/dist-lib/components/MailboxProposalsPage.js +520 -108
  9. package/dist-lib/components/SearchableSelect.d.ts +1 -0
  10. package/dist-lib/components/SearchableSelect.d.ts.map +1 -1
  11. package/dist-lib/components/SearchableSelect.js +2 -1
  12. package/dist-lib/components/settings/MailboxAiPromptCard.d.ts +2 -0
  13. package/dist-lib/components/settings/MailboxAiPromptCard.d.ts.map +1 -0
  14. package/dist-lib/components/settings/MailboxAiPromptCard.js +39 -0
  15. package/dist-lib/index.d.ts.map +1 -1
  16. package/dist-lib/index.js +2 -0
  17. package/dist-lib/integrations/supabase/auth-attacher.d.ts.map +1 -1
  18. package/dist-lib/integrations/supabase/auth-attacher.js +5 -1
  19. package/dist-lib/integrations/supabase/auth-middleware.d.ts +2 -11202
  20. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  21. package/dist-lib/integrations/supabase/auth-middleware.js +8 -7
  22. package/dist-lib/integrations/supabase/client.d.ts +16689 -7180
  23. package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
  24. package/dist-lib/integrations/supabase/client.server.d.ts +16691 -7181
  25. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  26. package/dist-lib/integrations/supabase/types.d.ts +16944 -7220
  27. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  28. package/dist-lib/integrations/supabase/types.js +63 -1
  29. package/dist-lib/lib/ai-config.server.d.ts +60 -0
  30. package/dist-lib/lib/ai-config.server.d.ts.map +1 -0
  31. package/dist-lib/lib/ai-config.server.js +112 -0
  32. package/dist-lib/lib/delivery-date.d.ts +37 -0
  33. package/dist-lib/lib/delivery-date.d.ts.map +1 -0
  34. package/dist-lib/lib/delivery-date.js +96 -0
  35. package/dist-lib/lib/load-carriers.d.ts +26 -0
  36. package/dist-lib/lib/load-carriers.d.ts.map +1 -0
  37. package/dist-lib/lib/load-carriers.js +73 -0
  38. package/dist-lib/lib/mailbox-ai-prompt.d.ts +9 -0
  39. package/dist-lib/lib/mailbox-ai-prompt.d.ts.map +1 -0
  40. package/dist-lib/lib/mailbox-ai-prompt.js +30 -0
  41. package/dist-lib/lib/mailbox-ai-settings.functions.d.ts +23 -0
  42. package/dist-lib/lib/mailbox-ai-settings.functions.d.ts.map +1 -0
  43. package/dist-lib/lib/mailbox-ai-settings.functions.js +58 -0
  44. package/dist-lib/lib/mailbox-ai.server.d.ts +56 -4
  45. package/dist-lib/lib/mailbox-ai.server.d.ts.map +1 -1
  46. package/dist-lib/lib/mailbox-ai.server.js +329 -6
  47. package/dist-lib/lib/mailbox-body.d.ts +7 -0
  48. package/dist-lib/lib/mailbox-body.d.ts.map +1 -0
  49. package/dist-lib/lib/mailbox-body.js +38 -0
  50. package/dist-lib/lib/mailbox-ingest.server.d.ts +89 -0
  51. package/dist-lib/lib/mailbox-ingest.server.d.ts.map +1 -0
  52. package/dist-lib/lib/mailbox-ingest.server.js +928 -0
  53. package/dist-lib/lib/mailbox-template.functions.d.ts +7 -67213
  54. package/dist-lib/lib/mailbox-template.functions.d.ts.map +1 -1
  55. package/dist-lib/lib/mailbox.functions.d.ts +130 -190511
  56. package/dist-lib/lib/mailbox.functions.d.ts.map +1 -1
  57. package/dist-lib/lib/mailbox.functions.js +641 -274
  58. package/package.json +4 -3
  59. package/dist-lib/components/CartAssignmentWizard.d.ts +0 -6
  60. package/dist-lib/components/CartAssignmentWizard.d.ts.map +0 -1
  61. package/dist-lib/components/CartAssignmentWizard.js +0 -199
  62. package/dist-lib/components/MailboxInboxPage.d.ts +0 -2
  63. package/dist-lib/components/MailboxInboxPage.d.ts.map +0 -1
  64. package/dist-lib/components/MailboxInboxPage.js +0 -120
  65. package/dist-lib/components/settings/ImapAccountsCard.d.ts +0 -2
  66. package/dist-lib/components/settings/ImapAccountsCard.d.ts.map +0 -1
  67. package/dist-lib/components/settings/ImapAccountsCard.js +0 -136
  68. package/dist-lib/components/settings/ImapProfilesCard.d.ts +0 -2
  69. package/dist-lib/components/settings/ImapProfilesCard.d.ts.map +0 -1
  70. package/dist-lib/components/settings/ImapProfilesCard.js +0 -101
  71. package/dist-lib/components/settings/MailboxSettingsCard.d.ts +0 -2
  72. package/dist-lib/components/settings/MailboxSettingsCard.d.ts.map +0 -1
  73. package/dist-lib/components/settings/MailboxSettingsCard.js +0 -85
  74. package/dist-lib/lib/imap.functions.d.ts +0 -32852
  75. package/dist-lib/lib/imap.functions.d.ts.map +0 -1
  76. package/dist-lib/lib/imap.functions.js +0 -235
  77. package/dist-lib/lib/mailbox-auto.server.d.ts +0 -9
  78. package/dist-lib/lib/mailbox-auto.server.d.ts.map +0 -1
  79. package/dist-lib/lib/mailbox-auto.server.js +0 -198
@@ -4,10 +4,74 @@ import { useQuery, useQueryClient, useMutation } from "@tanstack/react-query";
4
4
  import { useServerFn } from "@tanstack/react-start";
5
5
  import { Button, Badge, Card, CardContent, CardHeader, CardTitle, Input, Label, Textarea, Switch, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Dialog, DialogContent, DialogHeader, DialogTitle, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, toast, } from "@flowselections/core";
6
6
  import { Check, X, Trash2, Plus, AlertTriangle, RefreshCw } from "lucide-react";
7
- import { listProposals, approveProposal, rejectProposal, listCustomersForPicker, listProductsForPicker, listDeliveryLocationsForCustomer, listPickupLocations, listCarriersForPicker, getOrderPickDays, getActiveOrdersMailbox, ingestEmailEvent, backfillRecentEvents, } from "../lib/mailbox.functions";
7
+ import { computeEarliestDelivery, isValidDeliveryDate } from "../lib/delivery-date";
8
+ import { listProposals, approveProposal, rejectProposal, listCustomersForPicker, createCustomerFromGroup, listProductsForPicker, listDeliveryLocationsForCustomer, listPickupLocations, listCarriersForPicker, getOrderPickDays, getActiveOrdersMailbox, syncMailboxNow, getLastIngestRun, refreshProposalsForAgentChange, reparseProposalNow, listBeladingOptionsForProducts, listAllowedLoadCarriers, } from "../lib/mailbox.functions";
9
+ import { enabledLoadCarriers, normalizeAllowedLoadCarriers, EMPTY_LOAD_CARRIERS } from "../lib/load-carriers";
8
10
  import { listTemplateFields, listPlantTypesForPicker, listSuppliersForPicker, } from "../lib/mailbox-template.functions";
9
11
  import { SearchableSelect } from "./SearchableSelect";
12
+ import { MailboxAuthBanner } from "./MailboxAuthBanner";
10
13
  import { supabase } from "../integrations/supabase/client";
14
+ const beladingKey = (b) => `${b.plants_per_crate}x${b.crates_per_layer}x${b.layers_per_cart}|${b.crate_code ?? ""}|${b.carrier_code ?? ""}`;
15
+ const beladingLabel = (b) => `${b.plants_per_crate} planten/fust · ${b.crates_per_layer} fusten/laag · ${b.layers_per_cart} lagen`;
16
+ // Extra regel onder het label: fusten/kar + fusttype + kartype
17
+ const beladingHint = (b) => [
18
+ `${b.crates_per_layer * b.layers_per_cart} fusten/kar`,
19
+ b.crate_name ?? null,
20
+ b.carrier_name ?? null,
21
+ ].filter(Boolean).join(" · ");
22
+ /** Losse tekstvergelijking voor kartypes (code of label). */
23
+ const normCarrierText = (v) => v.toLowerCase().replace(/[^a-z0-9]+/g, "");
24
+ /**
25
+ * Alleen beladingen tonen waarvan het kartype door de Logistiek Engine is
26
+ * toegestaan. Beladingen zonder kartype worden niet getoond (behalve de
27
+ * standaardbelading van het product).
28
+ */
29
+ const beladingAllowedByEngine = (b, allowed) => {
30
+ if (allowed.length === 0)
31
+ return true; // Engine (nog) niet beschikbaar
32
+ const hay = [b.carrier_code, b.carrier_name].filter(Boolean).map((v) => normCarrierText(String(v)));
33
+ if (hay.length === 0)
34
+ return false;
35
+ return allowed.some((c) => {
36
+ const keys = [normCarrierText(c.code), normCarrierText(c.label)];
37
+ return keys.some((k) => k && hay.some((h) => h === k || h.includes(k) || k.includes(h)));
38
+ });
39
+ };
40
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
41
+ /** Alleen een echte lokale klant kan aan een bestelling worden gekoppeld. */
42
+ const isLocalCustomerId = (v) => typeof v === "string" && UUID_RE.test(v);
43
+ const providerLabel = (p) => p === "local" ? "Lokaal" : p === "twinfield" ? "Twinfield" : p === "floriday" ? "Floriday" : p;
44
+ const customerHint = (c) => [c.email, c.city, (c.providers ?? []).map(providerLabel).join(" + ")]
45
+ .filter(Boolean).join(" · ");
46
+ /**
47
+ * Zet een klantwaarde (id óf door AI geschreven klantnaam) om naar een geldige
48
+ * keuze uit de klantenlijst. Lokale klanten hebben voorrang.
49
+ */
50
+ function resolveCustomerId(raw, customers, fallbackId) {
51
+ const all = Array.isArray(customers) ? customers : [];
52
+ const list = [...all.filter((c) => c.is_local !== false), ...all.filter((c) => c.is_local === false)];
53
+ if (typeof raw === "string" && raw.trim()) {
54
+ const v = raw.trim();
55
+ if (UUID_RE.test(v))
56
+ return v;
57
+ if (v.startsWith("grp:") && list.some((c) => c.id === v))
58
+ return v;
59
+ const norm = v.toLowerCase();
60
+ const byName = list.find((c) => (c.company_name ?? "").trim().toLowerCase() === norm);
61
+ if (byName)
62
+ return byName.id;
63
+ const byEmail = list.find((c) => (c.email ?? "").trim().toLowerCase() === norm);
64
+ if (byEmail)
65
+ return byEmail.id;
66
+ const partial = list.find((c) => {
67
+ const n = (c.company_name ?? "").trim().toLowerCase();
68
+ return n.length > 2 && (n.includes(norm) || norm.includes(n));
69
+ });
70
+ if (partial)
71
+ return partial.id;
72
+ }
73
+ return fallbackId ?? null;
74
+ }
11
75
  export function MailboxProposalsPage() {
12
76
  const qc = useQueryClient();
13
77
  const fetchList = useServerFn(listProposals);
@@ -21,8 +85,8 @@ export function MailboxProposalsPage() {
21
85
  const fetchCarriers = useServerFn(listCarriersForPicker);
22
86
  const fetchPickupLocations = useServerFn(listPickupLocations);
23
87
  const fetchActive = useServerFn(getActiveOrdersMailbox);
24
- const fetchIngest = useServerFn(ingestEmailEvent);
25
- const fetchBackfill = useServerFn(backfillRecentEvents);
88
+ const fetchSync = useServerFn(syncMailboxNow);
89
+ const fetchLastRun = useServerFn(getLastIngestRun);
26
90
  // Server fns met requireSupabaseAuth gooien een raw Response bij een stale
27
91
  // bearer-token. Die bubbelt op als "[object Response]" en blankt het scherm.
28
92
  // Vang elke fout en geef een lege fallback terug — UI blijft bruikbaar.
@@ -37,9 +101,15 @@ export function MailboxProposalsPage() {
37
101
  // Bestelvoorstellen toont uitsluitend nieuwe, nog niet gecontroleerde
38
102
  // bestellingen ("Te controleren"). Goedgekeurd/afgewezen verschijnt in
39
103
  // de module Verwerkt.
40
- const { data, isLoading } = useQuery({
104
+ const { data, isLoading, error: listError, refetch: refetchList } = useQuery({
41
105
  queryKey: ["mailbox-proposals", "pending"],
42
- queryFn: () => safe(() => fetchList({ data: { status: "pending" } })),
106
+ // Bewust NIET stilzwijgend opvangen: als de server tijdelijk onbereikbaar is
107
+ // moet de gebruiker dat zien in plaats van een lege lijst ("geen mailtjes").
108
+ queryFn: () => fetchList({ data: { status: "pending" } }),
109
+ retry: 3,
110
+ retryDelay: (a) => Math.min(1000 * 2 ** a, 8000),
111
+ refetchInterval: 60000,
112
+ refetchOnWindowFocus: true,
43
113
  });
44
114
  const { data: templateData } = useQuery({
45
115
  queryKey: ["mailbox-template-fields"],
@@ -51,7 +121,6 @@ export function MailboxProposalsPage() {
51
121
  const { data: supplierData } = useQuery({ queryKey: ["mailbox-suppliers"], queryFn: () => safe(() => fetchSuppliers()) });
52
122
  const { data: carriersData } = useQuery({ queryKey: ["mailbox-carriers"], queryFn: () => safe(() => fetchCarriers()) });
53
123
  const { data: pickupLocData } = useQuery({ queryKey: ["mailbox-pickup-locations"], queryFn: () => safe(() => fetchPickupLocations()) });
54
- // Realtime ingestie + backfill — voorheen in de Inbox-pagina.
55
124
  const { data: activeMailbox } = useQuery({
56
125
  queryKey: ["mailbox-active-account"],
57
126
  queryFn: () => fetchActive(),
@@ -59,23 +128,27 @@ export function MailboxProposalsPage() {
59
128
  retry: 1,
60
129
  });
61
130
  const activeMailboxId = activeMailbox?.connected ? activeMailbox.id : null;
62
- const backfillMut = useMutation({
63
- mutationFn: async () => {
131
+ // Ingest wordt server-side gedreven (pg_cron, elke minuut). Deze knoppen /
132
+ // triggers hier zijn een extra vangnet zodat de UI direct bijwerkt — de
133
+ // pipeline is exact dezelfde als die van cron.
134
+ const syncMut = useMutation({
135
+ mutationFn: async (trigger) => {
64
136
  try {
65
- return await fetchBackfill();
137
+ return await fetchSync({ data: { trigger } });
66
138
  }
67
- catch {
68
- return { ok: false, inserted: 0 };
139
+ catch (e) {
140
+ return { ok: false, error: String(e?.message ?? e), inserted: 0, processed: 0, errors: 1 };
69
141
  }
70
142
  },
71
143
  onSuccess: (res) => {
72
- if (res?.ok && (res.inserted ?? 0) > 0) {
144
+ if (res?.ok && ((res.inserted ?? 0) > 0 || (res.processed ?? 0) > 0)) {
73
145
  qc.invalidateQueries({ queryKey: ["mailbox-proposals"] });
146
+ qc.invalidateQueries({ queryKey: ["mailbox-last-ingest-run"] });
74
147
  }
75
148
  },
76
149
  });
77
150
  const handleManualSync = () => {
78
- const p = backfillMut.mutateAsync();
151
+ const p = syncMut.mutateAsync("manual");
79
152
  toast.promise(p, {
80
153
  loading: "Mailbox synchroniseren...",
81
154
  success: (res) => res?.ok
@@ -86,6 +159,8 @@ export function MailboxProposalsPage() {
86
159
  error: "Sync mislukt",
87
160
  });
88
161
  };
162
+ // Realtime hint: zodra er een nieuw event binnenkomt vragen we de server om
163
+ // het meteen te verwerken (zelfde pipeline). Cron is de garantiestrook.
89
164
  useEffect(() => {
90
165
  if (!activeMailboxId)
91
166
  return;
@@ -94,36 +169,99 @@ export function MailboxProposalsPage() {
94
169
  .on("postgres_changes", {
95
170
  event: "INSERT", schema: "public", table: "email_auth_events",
96
171
  filter: `connection_id=eq.${activeMailboxId}`,
97
- }, async (payload) => {
98
- const evId = payload?.new?.id;
99
- if (!evId)
100
- return;
101
- try {
102
- const res = await fetchIngest({ data: { eventId: evId } });
103
- if (res?.ok && (res.inserted ?? 0) > 0) {
104
- toast.success("Nieuwe bestelling ontvangen");
105
- qc.invalidateQueries({ queryKey: ["mailbox-proposals"] });
106
- }
107
- }
108
- catch { /* backfill vangt op */ }
172
+ }, () => {
173
+ syncMut.mutate("realtime");
109
174
  })
110
175
  .subscribe();
111
176
  return () => { void supabase.removeChannel(channel); };
112
- }, [activeMailboxId, fetchIngest, qc]);
177
+ // eslint-disable-next-line react-hooks/exhaustive-deps
178
+ }, [activeMailboxId]);
179
+ // Bij eerste keer openen + elke 5 min extra vangnet naast pg_cron/realtime.
113
180
  const didInitialBackfill = useRef(false);
114
181
  useEffect(() => {
115
182
  if (!activeMailboxId || didInitialBackfill.current)
116
183
  return;
117
184
  didInitialBackfill.current = true;
118
- backfillMut.mutate();
119
- const t = setInterval(() => backfillMut.mutate(), 300000);
185
+ syncMut.mutate("manual");
186
+ const t = setInterval(() => syncMut.mutate("manual"), 300000);
120
187
  return () => clearInterval(t);
121
188
  // eslint-disable-next-line react-hooks/exhaustive-deps
122
189
  }, [activeMailboxId]);
190
+ // Laatste ingest-run tonen voor observability.
191
+ const { data: lastRunData } = useQuery({
192
+ queryKey: ["mailbox-last-ingest-run"],
193
+ queryFn: () => safe(() => fetchLastRun()),
194
+ refetchInterval: 60000,
195
+ });
196
+ const lastRun = lastRunData?.run ?? null;
197
+ // AI-agent gewijzigd? Dan worden openstaande voorstellen automatisch opnieuw
198
+ // geanalyseerd met de nieuwste agent. Server-side draait dit ook elke
199
+ // cron-run; hier reageren we direct op wijzigingen in de AI-module.
200
+ const fetchAgentRefresh = useServerFn(refreshProposalsForAgentChange);
201
+ const agentRefreshMut = useMutation({
202
+ mutationFn: async () => {
203
+ try {
204
+ return await fetchAgentRefresh();
205
+ }
206
+ catch {
207
+ return { reparsed: 0 };
208
+ }
209
+ },
210
+ onSuccess: (res) => {
211
+ if ((res?.reparsed ?? 0) > 0) {
212
+ qc.invalidateQueries({ queryKey: ["mailbox-proposals"] });
213
+ toast.success(`${res.reparsed} voorstel(len) opnieuw geanalyseerd met de bijgewerkte AI-agent`);
214
+ }
215
+ },
216
+ });
217
+ // Handmatig één voorstel opnieuw laten analyseren door de AI-agent.
218
+ const fetchReparse = useServerFn(reparseProposalNow);
219
+ const [reparseError, setReparseError] = useState(null);
220
+ const reparseMutation = useMutation({
221
+ mutationFn: async (proposalId) => {
222
+ setReparseError(null);
223
+ return (await fetchReparse({ data: { proposalId } }));
224
+ },
225
+ onSuccess: (res) => {
226
+ if (res?.ok) {
227
+ setReparseError(null);
228
+ qc.invalidateQueries({ queryKey: ["mailbox-proposals"] });
229
+ toast.success("E-mail opnieuw geanalyseerd — bestelgegevens bijgewerkt");
230
+ }
231
+ else {
232
+ const msg = res?.error ?? "Opnieuw analyseren mislukt";
233
+ setReparseError(msg);
234
+ toast.error(msg);
235
+ }
236
+ },
237
+ onError: (e) => {
238
+ const msg = e?.message ?? "Opnieuw analyseren mislukt";
239
+ setReparseError(msg);
240
+ toast.error(msg);
241
+ },
242
+ });
243
+ const didAgentCheck = useRef(false);
244
+ useEffect(() => {
245
+ if (didAgentCheck.current)
246
+ return;
247
+ didAgentCheck.current = true;
248
+ agentRefreshMut.mutate();
249
+ const channel = supabase
250
+ .channel("ai-agent-changes")
251
+ .on("postgres_changes", { event: "*", schema: "public", table: "ai_module_bindings" }, () => agentRefreshMut.mutate())
252
+ .on("postgres_changes", { event: "*", schema: "public", table: "ai_models" }, () => agentRefreshMut.mutate())
253
+ .on("postgres_changes", { event: "*", schema: "public", table: "ai_credentials" }, () => agentRefreshMut.mutate())
254
+ .subscribe();
255
+ const t = setInterval(() => agentRefreshMut.mutate(), 120000);
256
+ return () => { clearInterval(t); void supabase.removeChannel(channel); };
257
+ // eslint-disable-next-line react-hooks/exhaustive-deps
258
+ }, []);
123
259
  const items = Array.isArray(data?.items) ? data.items : [];
124
260
  const template = Array.isArray(templateData?.items) ? templateData.items : [];
125
261
  const customers = Array.isArray(customersData?.items) ? customersData.items : [];
126
262
  const products = Array.isArray(productsData?.items) ? productsData.items : [];
263
+ // Rule Engine bepaalt of doorverkopen zonder voorraad mag
264
+ const allowBackorder = productsData?.allowBackorder === true;
127
265
  const plantTypes = Array.isArray(plantData?.items) ? plantData.items : [];
128
266
  const suppliers = Array.isArray(supplierData?.items) ? supplierData.items : [];
129
267
  const carriers = Array.isArray(carriersData?.items) ? carriersData.items : [];
@@ -135,7 +273,7 @@ export function MailboxProposalsPage() {
135
273
  const customerNameFor = (p) => {
136
274
  const fvs = Array.isArray(p.mailbox_proposal_field_values) ? p.mailbox_proposal_field_values : [];
137
275
  const fv = fvs.find((v) => v.field_key === "customer");
138
- const id = fv?.value ?? p.matched_customer_id ?? null;
276
+ const id = resolveCustomerId(fv?.value, customers, p.matched_customer_id);
139
277
  if (id) {
140
278
  const c = customers.find((x) => x.id === id);
141
279
  if (c?.company_name)
@@ -143,17 +281,24 @@ export function MailboxProposalsPage() {
143
281
  }
144
282
  return p.mailbox_messages?.from_name || p.mailbox_messages?.from_email || "Onbekend";
145
283
  };
146
- const orderNumberFor = (p) => `MB-${String(p.id).slice(0, 8).toUpperCase()}`;
147
- return (_jsxs("div", { className: "p-6 space-y-4", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-2xl font-semibold", children: "Bestelvoorstellen" }), _jsx("p", { className: "text-sm text-muted-foreground", children: "Nieuw binnengekomen bestellingen die nog handmatig gecontroleerd moeten worden." })] }), _jsxs(Button, { onClick: handleManualSync, disabled: backfillMut.isPending, variant: "outline", children: [_jsx(RefreshCw, { className: `h-4 w-4 mr-2 ${backfillMut.isPending ? "animate-spin" : ""}` }), "Sync nu"] })] }), isLoading ? (_jsx("div", { className: "text-muted-foreground", children: "Laden..." })) : items.length === 0 ? (_jsx("p", { className: "text-muted-foreground", children: "Geen bestellingen te controleren." })) : (_jsx(Card, { children: _jsx(CardContent, { className: "p-0", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Ordernummer" }), _jsx(TableHead, { children: "Klantnaam" }), _jsx(TableHead, { children: "Orderdatum" }), _jsx(TableHead, { children: "Status" }), _jsx(TableHead, { className: "text-right", children: "Orderwaarde" })] }) }), _jsx(TableBody, { children: items.map((p) => {
148
- const fvs = Array.isArray(p.mailbox_proposal_field_values) ? p.mailbox_proposal_field_values : [];
149
- const needsReview = fvs.some((v) => v.needs_review);
150
- const date = p.mailbox_messages?.received_at ?? p.created_at;
151
- return (_jsxs(TableRow, { className: "cursor-pointer hover:bg-muted/50", onClick: () => setActiveId(p.id), children: [_jsx(TableCell, { className: "font-mono text-xs", children: orderNumberFor(p) }), _jsx(TableCell, { className: "font-medium", children: customerNameFor(p) }), _jsx(TableCell, { className: "text-sm text-muted-foreground", children: date ? new Date(date).toLocaleDateString("nl-NL") : "-" }), _jsx(TableCell, { children: _jsxs("div", { className: "flex items-center gap-1", children: [_jsx(Badge, { variant: statusVariant(p.status), children: statusLabel(p.status) }), needsReview && (_jsxs(Badge, { variant: "outline", className: "text-xs border-yellow-500 text-yellow-700", children: [_jsx(AlertTriangle, { className: "h-3 w-3 mr-1" }), "Controleren"] }))] }) }), _jsx(TableCell, { className: "text-right text-sm text-muted-foreground", children: "\u2014" })] }, p.id));
152
- }) })] }) }) })), _jsx(Dialog, { open: !!active, onOpenChange: (o) => { if (!o)
153
- setActiveId(null); }, children: _jsx(DialogContent, { className: "max-w-[95vw] w-[95vw] h-[92vh] max-h-[92vh] p-0 flex flex-col overflow-hidden", children: active && (_jsxs(_Fragment, { children: [_jsx(DialogHeader, { className: "px-6 pt-6 pb-3 shrink-0 border-b", children: _jsxs(DialogTitle, { children: [orderNumberFor(active), " \u00B7 ", customerNameFor(active)] }) }), _jsx("div", { className: "flex-1 min-h-0 overflow-y-auto overscroll-contain p-6", children: _jsx(ProposalEditor, { proposal: active, template: template, customers: customers, products: products, plantTypes: plantTypes, suppliers: suppliers, carriers: carriers, pickupLocations: pickupLocations, onApprove: async (payload) => {
284
+ return (_jsxs("div", { className: "p-6 flex flex-col gap-4 flex-1 min-h-0 overflow-hidden", children: [_jsx(MailboxAuthBanner, { active: activeMailbox }), _jsxs("div", { className: "flex items-center justify-between shrink-0", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-2xl font-semibold", children: "Bestelvoorstellen" }), _jsx("p", { className: "text-sm text-muted-foreground", children: "Nieuw binnengekomen bestellingen die nog handmatig gecontroleerd moeten worden." })] }), _jsxs("div", { className: "flex items-center gap-3", children: [lastRun?.finished_at && (_jsxs("span", { className: "text-xs text-muted-foreground", children: ["Laatste sync: ", new Date(lastRun.finished_at).toLocaleTimeString("nl-NL"), " \u2014 ", lastRun.processed, " verwerkt", lastRun.errors > 0 ? `, ${lastRun.errors} fouten` : ""] })), _jsxs(Button, { onClick: handleManualSync, disabled: syncMut.isPending, variant: "outline", children: [_jsx(RefreshCw, { className: `h-4 w-4 mr-2 ${syncMut.isPending ? "animate-spin" : ""}` }), "Sync nu"] })] })] }), _jsx("div", { className: "flex-1 min-h-0 overflow-y-auto overscroll-contain [scrollbar-gutter:stable]", children: isLoading ? (_jsx("div", { className: "text-muted-foreground", children: "Laden..." })) : listError ? (_jsxs("div", { className: "rounded-md border border-destructive/40 bg-destructive/5 p-4 text-sm", children: [_jsx("p", { className: "font-medium text-destructive", children: "Bestellingen konden niet worden opgehaald." }), _jsxs("p", { className: "text-muted-foreground mt-1", children: [listError?.message ?? "Onbekende fout", " \u2014 de lijst is niet leeg, alleen niet geladen."] }), _jsx(Button, { size: "sm", variant: "outline", className: "mt-3", onClick: () => void refetchList(), children: "Opnieuw proberen" })] })) : items.length === 0 ? (_jsx("p", { className: "text-muted-foreground", children: "Geen bestellingen te controleren." })) : (_jsx(Card, { children: _jsx(CardContent, { className: "p-0", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Klantnaam" }), _jsx(TableHead, { children: "Orderdatum" }), _jsx(TableHead, { children: "Status" }), _jsx(TableHead, { className: "text-right", children: "Orderwaarde" })] }) }), _jsx(TableBody, { children: items.map((p) => {
285
+ const fvs = Array.isArray(p.mailbox_proposal_field_values) ? p.mailbox_proposal_field_values : [];
286
+ const needsReview = fvs.some((v) => v.needs_review);
287
+ const date = p.mailbox_messages?.received_at ?? p.created_at;
288
+ return (_jsxs(TableRow, { className: "cursor-pointer hover:bg-muted/50", onClick: () => setActiveId(p.id), children: [_jsx(TableCell, { className: "font-medium", children: customerNameFor(p) }), _jsx(TableCell, { className: "text-sm text-muted-foreground", children: date ? new Date(date).toLocaleDateString("nl-NL") : "-" }), _jsx(TableCell, { children: _jsxs("div", { className: "flex items-center gap-1", children: [_jsx(Badge, { variant: statusVariant(p.status), children: statusLabel(p.status) }), p.parsed_payload?.ai?.ok === true && (_jsx(Badge, { variant: "outline", className: "text-xs border-sky-500 text-sky-700", children: "AI" })), needsReview && (_jsxs(Badge, { variant: "outline", className: "text-xs border-yellow-500 text-yellow-700", children: [_jsx(AlertTriangle, { className: "h-3 w-3 mr-1" }), "Controleren"] }))] }) }), _jsx(TableCell, { className: "text-right text-sm text-muted-foreground", children: "\u2014" })] }, p.id));
289
+ }) })] }) }) })) }), _jsx(Dialog, { open: !!active, onOpenChange: (o) => { if (!o) {
290
+ setActiveId(null);
291
+ setReparseError(null);
292
+ } }, children: _jsx(DialogContent, { className: "max-w-[95vw] w-[95vw] h-[92vh] max-h-[92vh] p-0 flex flex-col overflow-hidden", children: active && (_jsxs(_Fragment, { children: [_jsxs(DialogHeader, { className: "px-6 pt-6 pb-3 shrink-0 border-b", children: [_jsxs("div", { className: "flex items-start justify-between gap-4", children: [_jsx(DialogTitle, { children: customerNameFor(active) }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", className: "mr-8", disabled: reparseMutation.isPending, onClick: () => reparseMutation.mutate(active.id), children: [_jsx(RefreshCw, { className: `h-4 w-4 mr-2 ${reparseMutation.isPending ? "animate-spin" : ""}` }), reparseMutation.isPending ? "Analyseren…" : "Mail opnieuw analyseren"] })] }), reparseError && (_jsxs("div", { className: "mt-2 flex items-start gap-2 rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-sm text-destructive", children: [_jsx(AlertTriangle, { className: "h-4 w-4 mt-0.5 shrink-0" }), _jsx("span", { children: reparseError })] }))] }), _jsx("div", { className: "flex-1 min-h-0 overflow-hidden p-6", children: _jsx(ProposalEditor, { proposal: active, template: template, customers: customers, products: products, allowBackorder: allowBackorder, plantTypes: plantTypes, suppliers: suppliers, carriers: carriers, pickupLocations: pickupLocations, onApprove: async (payload) => {
154
293
  try {
155
- await fetchApprove({ data: payload });
294
+ const res = await fetchApprove({ data: payload });
295
+ const shortages = Array.isArray(res?.shortages) ? res.shortages : [];
156
296
  toast.success("Bestelling aangemaakt — karindeling wordt automatisch aangemaakt in de Verkoop-module");
297
+ if (shortages.length > 0) {
298
+ toast.warning(`Tekorten (levering zonder voorraad staat aan): ${shortages
299
+ .map((s) => `${s.product ?? s.productId}: besteld ${s.wanted}, beschikbaar ${s.available}, tekort ${s.missing}`)
300
+ .join(" · ")}`);
301
+ }
157
302
  qc.invalidateQueries({ queryKey: ["mailbox-proposals"] });
158
303
  setActiveId(null);
159
304
  }
@@ -175,7 +320,7 @@ export function MailboxProposalsPage() {
175
320
  function ReadOnlyField({ label, value }) {
176
321
  return (_jsxs("div", { className: "space-y-0.5", children: [_jsx("div", { className: "text-[11px] uppercase tracking-wide text-muted-foreground", children: label }), _jsx("div", { className: "text-sm", children: value && String(value).trim() !== "" ? value : _jsx("span", { className: "text-muted-foreground", children: "\u2014" }) })] }));
177
322
  }
178
- function ProposalEditor({ proposal, template, customers, products, plantTypes, suppliers, carriers, pickupLocations, onApprove, onReject, }) {
323
+ function ProposalEditor({ proposal, template, customers, products, allowBackorder, plantTypes, suppliers, carriers, pickupLocations, onApprove, onReject, }) {
179
324
  const msg = proposal.mailbox_messages ?? {};
180
325
  const fieldValues = Array.isArray(proposal.mailbox_proposal_field_values)
181
326
  ? proposal.mailbox_proposal_field_values : [];
@@ -191,12 +336,23 @@ function ProposalEditor({ proposal, template, customers, products, plantTypes, s
191
336
  visibleFields.forEach((f) => {
192
337
  init[f.key] = fvMap[f.key]?.value ?? f.default_value ?? null;
193
338
  });
194
- // Vul de auto-herkende klant in als er nog geen waarde uit het template komt
195
- if (!init["customer"] && proposal.matched_customer_id) {
196
- init["customer"] = proposal.matched_customer_id;
197
- }
339
+ // De AI schrijft soms een klantnaam i.p.v. een id — altijd omzetten naar een
340
+ // geldig klant-id, met de automatisch herkende klant als terugval.
341
+ init["customer"] = resolveCustomerId(init["customer"], customers, proposal.matched_customer_id);
198
342
  return init;
199
343
  });
344
+ // Klantenlijst kan later binnenkomen: alsnog omzetten zodra dat kan.
345
+ useEffect(() => {
346
+ setValues((prev) => {
347
+ const resolved = resolveCustomerId(prev["customer"], customers, proposal.matched_customer_id);
348
+ return resolved === prev["customer"] ? prev : { ...prev, customer: resolved };
349
+ });
350
+ }, [customers, proposal.matched_customer_id]);
351
+ const defaultBelading = (p) => p ? {
352
+ plants_per_crate: Math.max(1, Number(p.plants_per_crate ?? 1)),
353
+ crates_per_layer: Math.max(1, Number(p.crates_per_layer ?? 1)),
354
+ layers_per_cart: Math.max(1, Number(p.layers_per_cart ?? 1)),
355
+ } : null;
200
356
  const initialLines = Array.isArray(proposal.mailbox_proposal_lines)
201
357
  ? proposal.mailbox_proposal_lines.map((l) => {
202
358
  const p = products.find((x) => x.id === l.product_id);
@@ -207,22 +363,51 @@ function ProposalEditor({ proposal, template, customers, products, plantTypes, s
207
363
  id: l.id,
208
364
  product_id: l.product_id,
209
365
  raw_product_text: l.raw_product_text ?? "",
366
+ plant_type: l.plant_type ?? null,
210
367
  quantity: normalized || null,
211
368
  unit: l.unit,
369
+ // Geen belading bekend uit de mail → altijd de standaardbelading.
370
+ belading: defaultBelading(p),
212
371
  };
213
372
  })
214
373
  : [];
215
374
  const [lines, setLines] = useState(initialLines.length ? initialLines : [
216
- { product_id: null, raw_product_text: "", quantity: null, unit: "stuks" },
375
+ { product_id: null, raw_product_text: "", quantity: null, unit: "stuks", belading: null },
217
376
  ]);
377
+ // Beschikbare beladingen per gekozen product (zelfde bron als Verkoop-module).
378
+ const fetchBeladingen = useServerFn(listBeladingOptionsForProducts);
379
+ const lineProductIds = useMemo(() => Array.from(new Set(lines.map((l) => l.product_id).filter(Boolean))), [lines]);
380
+ const { data: beladingData, isFetching: loadingBeladingen } = useQuery({
381
+ queryKey: ["mailbox-beladingen", lineProductIds.join(",")],
382
+ enabled: lineProductIds.length > 0,
383
+ queryFn: async () => {
384
+ try {
385
+ return await fetchBeladingen({ data: { productIds: lineProductIds } });
386
+ }
387
+ catch {
388
+ return { byProduct: {} };
389
+ }
390
+ },
391
+ });
392
+ const beladingByProduct = beladingData?.byProduct && typeof beladingData.byProduct === "object"
393
+ ? beladingData.byProduct : {};
218
394
  const customerVal = values["customer"];
219
395
  const deliveryVal = values["delivery_date"];
220
396
  const notesVal = values["notes"];
221
397
  const setVal = (key, v) => setValues((prev) => ({ ...prev, [key]: v }));
398
+ // Externe klant gekozen die nog niet lokaal bestaat → verplichte aanmaakdialoog.
399
+ const [newCustomer, setNewCustomer] = useState(null);
400
+ const handleSelectCustomer = (v) => {
401
+ setVal("customer", v);
402
+ if (v && !isLocalCustomerId(v)) {
403
+ const c = customers.find((x) => x.id === v) ?? null;
404
+ setNewCustomer(c);
405
+ }
406
+ };
222
407
  const fetchDeliveryLocations = useServerFn(listDeliveryLocationsForCustomer);
223
408
  const { data: deliveryLocData } = useQuery({
224
409
  queryKey: ["mailbox-delivery-locations", customerVal],
225
- enabled: !!customerVal,
410
+ enabled: isLocalCustomerId(customerVal),
226
411
  queryFn: async () => {
227
412
  try {
228
413
  return await fetchDeliveryLocations({ data: { customerId: customerVal } });
@@ -242,6 +427,15 @@ function ProposalEditor({ proposal, template, customers, products, plantTypes, s
242
427
  // schedule reset via microtask om setState-in-render te vermijden
243
428
  queueMicrotask(() => setVal("delivery_location", null));
244
429
  }
430
+ // Geen locatie uit de mail/AI? Val terug op de hoofdafleverlocatie van de klant.
431
+ useEffect(() => {
432
+ if (deliveryLocationVal || deliveryLocations.length === 0)
433
+ return;
434
+ const primary = deliveryLocations.find((l) => l.is_primary === true) ?? deliveryLocations[0];
435
+ if (primary?.id)
436
+ setVal("delivery_location", primary.id);
437
+ // eslint-disable-next-line react-hooks/exhaustive-deps
438
+ }, [deliveryLocationVal, deliveryLocations.length, customerVal]);
245
439
  // Auto-vul vervoerder zodra een nieuwe locatie wordt gekozen (één keer per wissel).
246
440
  const lastLocRef = useRef(deliveryLocationVal);
247
441
  useEffect(() => {
@@ -272,57 +466,124 @@ function ProposalEditor({ proposal, template, customers, products, plantTypes, s
272
466
  const selectedCarrier = carriers.find((c) => c.id === carrierVal) ?? null;
273
467
  const carrierDeliveryDays = Math.max(0, Number(selectedCarrier?.delivery_days ?? 0));
274
468
  const pickupLocationVal = values["pickup_location"];
469
+ // ─── Ladingdragers uit de Rule Engine (nooit cachen) ──────────────────────
470
+ const fetchLoadCarriers = useServerFn(listAllowedLoadCarriers);
471
+ const { data: lcData } = useQuery({
472
+ queryKey: ["mailbox-load-carriers"],
473
+ staleTime: 0,
474
+ gcTime: 0,
475
+ refetchOnMount: "always",
476
+ refetchOnWindowFocus: true,
477
+ queryFn: async () => {
478
+ try {
479
+ return await fetchLoadCarriers();
480
+ }
481
+ catch {
482
+ return EMPTY_LOAD_CARRIERS;
483
+ }
484
+ },
485
+ });
486
+ const loadCarrierList = useMemo(() => normalizeAllowedLoadCarriers(lcData), [lcData]);
487
+ const loadCarrierOptions = enabledLoadCarriers(loadCarrierList);
488
+ const loadCarrierVal = values["load_carrier"];
489
+ const loadCarrierBlocked = proposal.parsed_payload?.load_carrier_blocked === true
490
+ ? (proposal.parsed_payload?.load_carrier_detected ?? "onbekend")
491
+ : null;
492
+ // Onbekende of uitgeschakelde drager wissen; niets herkend → standaard van de Engine.
493
+ useEffect(() => {
494
+ if (loadCarrierOptions.length === 0)
495
+ return;
496
+ if (loadCarrierVal && !loadCarrierOptions.some((c) => c.code === loadCarrierVal)) {
497
+ setVal("load_carrier", null);
498
+ return;
499
+ }
500
+ if (!loadCarrierVal) {
501
+ const def = loadCarrierOptions.find((c) => c.code === loadCarrierList.default) ?? loadCarrierOptions[0];
502
+ if (def)
503
+ setVal("load_carrier", def.code);
504
+ }
505
+ // eslint-disable-next-line react-hooks/exhaustive-deps
506
+ }, [loadCarrierVal, loadCarrierOptions.length, loadCarrierList.default]);
507
+ // Geen afhaallocatie uit de mail/AI? Val terug op de hoofd-afhaallocatie (isDefault).
508
+ useEffect(() => {
509
+ if (pickupLocations.length === 0)
510
+ return;
511
+ const known = pickupLocationVal && pickupLocations.some((l) => l.id === pickupLocationVal);
512
+ if (known)
513
+ return;
514
+ const main = pickupLocations.find((l) => l.is_default === true) ?? pickupLocations[0];
515
+ if (main?.id)
516
+ setVal("pickup_location", main.id);
517
+ // eslint-disable-next-line react-hooks/exhaustive-deps
518
+ }, [pickupLocationVal, pickupLocations.length]);
275
519
  const canPickDate = !!customerVal && !!deliveryLocationVal && !!pickupLocationVal && !!carrierVal;
276
520
  const fmtISO = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
277
- const earliestDate = useMemo(() => {
278
- const base = new Date();
279
- base.setHours(0, 0, 0, 0);
280
- base.setDate(base.getDate() + pickDays + carrierDeliveryDays);
281
- return base;
282
- }, [pickDays, carrierDeliveryDays]);
283
- const earliestISO = fmtISO(earliestDate);
521
+ const earliest = useMemo(() => computeEarliestDelivery(selectedCarrier, pickDays), [selectedCarrier, pickDays]);
522
+ const earliestDate = earliest.date;
523
+ const earliestISO = earliest.iso;
284
524
  const earliestLabel = earliestDate.toLocaleDateString("nl-NL", {
285
525
  day: "numeric", month: "long", year: "numeric",
286
526
  });
287
- // Synchroniseer delivery_date wanneer de eerst mogelijke datum verandert.
527
+ // Bij wisselen van vervoerder: bezorgdatum altijd opnieuw bepalen.
528
+ const lastCarrierRef = useRef(carrierVal);
529
+ useEffect(() => {
530
+ if (carrierVal === lastCarrierRef.current)
531
+ return;
532
+ lastCarrierRef.current = carrierVal;
533
+ if (!carrierVal)
534
+ return;
535
+ setVal("delivery_date", earliestISO);
536
+ // eslint-disable-next-line react-hooks/exhaustive-deps
537
+ }, [carrierVal, earliestISO]);
538
+ // Synchroniseer delivery_date wanneer de eerst mogelijke datum verandert of
539
+ // de huidige datum niet geldig is voor de gekozen vervoerder.
288
540
  useEffect(() => {
289
541
  if (!canPickDate)
290
542
  return;
291
- if (!deliveryVal || deliveryVal < earliestISO) {
543
+ if (!deliveryVal || !isValidDeliveryDate(selectedCarrier, deliveryVal, pickDays)) {
292
544
  setVal("delivery_date", earliestISO);
293
545
  }
294
546
  // eslint-disable-next-line react-hooks/exhaustive-deps
295
- }, [canPickDate, earliestISO]);
296
- const handleApprove = () => {
547
+ }, [canPickDate, earliestISO, deliveryVal, carrierVal]);
548
+ const [confirmOpen, setConfirmOpen] = useState(false);
549
+ const [submitting, setSubmitting] = useState(false);
550
+ const [checkedOk, setCheckedOk] = useState(false);
551
+ const buildPayload = () => {
297
552
  const missing = template.filter((f) => f.required && (values[f.key] === null || values[f.key] === undefined || values[f.key] === ""));
298
553
  if (missing.length > 0) {
299
554
  toast.error(`Verplichte velden ontbreken: ${missing.map((m) => m.label).join(", ")}`);
300
- return;
555
+ return null;
556
+ }
557
+ if (customerVal && !isLocalCustomerId(customerVal)) {
558
+ toast.error("Maak deze klant eerst aan voordat je de bestelling verwerkt.");
559
+ setNewCustomer(customers.find((c) => c.id === customerVal) ?? null);
560
+ return null;
301
561
  }
302
562
  if (!deliveryLocationVal) {
303
563
  toast.error("Selecteer een afleverlocatie.");
304
- return;
564
+ return null;
305
565
  }
306
566
  if (!pickupLocationVal) {
307
567
  toast.error("Selecteer een afhaallocatie (eigen locatie / tuin).");
308
- return;
568
+ return null;
309
569
  }
310
570
  const validLines = lines.filter((l) => l.product_id && (l.quantity ?? 0) > 0);
311
571
  if (validLines.length === 0) {
312
572
  toast.error("Voeg minimaal één product toe aan de bestelling.");
313
- return;
573
+ return null;
314
574
  }
315
575
  for (const l of validLines) {
316
576
  const p = products.find((x) => x.id === l.product_id);
317
- const ppc = Math.max(1, Number(p?.plants_per_crate ?? 1));
577
+ const b = l.belading ?? defaultBelading(p);
578
+ const ppc = Math.max(1, Number(b?.plants_per_crate ?? 1));
318
579
  const qty = l.quantity ?? 0;
319
580
  if (qty < ppc || qty % ppc !== 0) {
320
581
  toast.error(`"${p?.product ?? l.raw_product_text}" moet per hele fust besteld worden (minimaal 1 fust = ${ppc} planten).`);
321
- return;
582
+ return null;
322
583
  }
323
584
  }
324
585
  const cust = customers.find((c) => c.id === customerVal);
325
- onApprove({
586
+ return {
326
587
  proposalId: proposal.id,
327
588
  customerId: customerVal ?? null,
328
589
  customerName: cust?.company_name ?? msg.from_name ?? msg.from_email ?? "Onbekend",
@@ -330,17 +591,49 @@ function ProposalEditor({ proposal, template, customers, products, plantTypes, s
330
591
  notes: notesVal || null,
331
592
  lines: validLines,
332
593
  fieldValues: Object.entries(values).map(([field_key, value]) => ({ field_key, value })),
333
- });
594
+ };
334
595
  };
335
- return (_jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsx(CardTitle, { className: "text-base", children: msg.subject || "(geen onderwerp)" }), _jsxs("div", { className: "text-xs text-muted-foreground", children: [msg.from_name, " <", msg.from_email, "> \u00B7", " ", msg.received_at ? new Date(msg.received_at).toLocaleString("nl-NL") : ""] })] }), _jsx(CardContent, { children: _jsx("pre", { className: "text-sm whitespace-pre-wrap font-sans max-h-[60vh] overflow-y-auto", children: msg.body_text || "(geen tekst)" }) })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: "Bestelvoorstel" }) }), _jsxs(CardContent, { className: "space-y-4", children: [(() => {
336
- const customerField = visibleFields.find((f) => f.key === "customer");
596
+ // Stap 1: controleren → bevestigingsdialoog. Pas na bevestigen wordt de
597
+ // bestelling daadwerkelijk aangemaakt in Bestellingen.
598
+ const handleApprove = () => {
599
+ if (!buildPayload())
600
+ return;
601
+ setConfirmOpen(true);
602
+ };
603
+ const handleConfirm = async () => {
604
+ const payload = buildPayload();
605
+ if (!payload)
606
+ return;
607
+ setSubmitting(true);
608
+ try {
609
+ await onApprove(payload);
610
+ setConfirmOpen(false);
611
+ }
612
+ finally {
613
+ setSubmitting(false);
614
+ }
615
+ };
616
+ return (_jsxs("div", { className: "grid grid-cols-2 gap-4 h-full min-h-0", children: [_jsxs(Card, { className: "flex flex-col min-h-0 overflow-hidden", children: [_jsxs(CardHeader, { className: "shrink-0", children: [_jsx(CardTitle, { className: "text-base", children: msg.subject || "(geen onderwerp)" }), _jsxs("div", { className: "text-xs text-muted-foreground", children: [msg.from_name, " <", msg.from_email, "> \u00B7", " ", msg.received_at ? new Date(msg.received_at).toLocaleString("nl-NL") : ""] })] }), _jsx(CardContent, { className: "flex-1 min-h-0 overflow-y-auto overscroll-contain", children: _jsx("pre", { className: "text-sm whitespace-pre-wrap font-sans", children: msg.body_text || "(geen tekst)" }) })] }), _jsxs(Card, { className: "flex flex-col min-h-0 overflow-hidden", children: [_jsx(CardHeader, { className: "shrink-0", children: _jsx(CardTitle, { className: "text-base", children: "Bestelvoorstel" }) }), _jsxs(CardContent, { className: "space-y-4 flex-1 min-h-0 overflow-y-auto overscroll-contain", children: [(() => {
617
+ // Klantveld staat altijd bovenaan, ook als het template (nog) leeg is.
618
+ const customerField = visibleFields.find((f) => f.key === "customer") ?? {
619
+ id: "customer",
620
+ key: "customer",
621
+ label: "Klant",
622
+ field_type: "customer",
623
+ required: true,
624
+ visible: true,
625
+ };
337
626
  const deliveryDateField = visibleFields.find((f) => f.key === "delivery_date");
338
627
  const otherFields = visibleFields.filter((f) => f.key !== "customer" && f.key !== "delivery_date");
339
- const renderField = (field) => (_jsx(DynamicField, { field: field, value: values[field.key], meta: fvMap[field.key], onChange: (v) => setVal(field.key, v), customers: customers, products: products, plantTypes: plantTypes, suppliers: suppliers }, field.key));
628
+ const renderField = (field) => (_jsx(DynamicField, { field: field, value: values[field.key], meta: fvMap[field.key], onChange: (v) => setVal(field.key, v), customers: customers, products: products, plantTypes: plantTypes, suppliers: suppliers, onSelectCustomer: handleSelectCustomer }, field.key));
629
+ const customerNotLocal = !!customerVal && !isLocalCustomerId(customerVal);
340
630
  const payload = (proposal.parsed_payload ?? {});
341
631
  const autoMatched = payload.auto_matched_customer === true;
342
632
  const matchWarning = typeof payload.auto_match_warning === "string" ? payload.auto_match_warning : null;
343
- return (_jsxs(_Fragment, { children: [autoMatched && (_jsxs("div", { className: "rounded-md border border-emerald-200 bg-emerald-50 px-3 py-2 text-xs text-emerald-800", children: ["Klant automatisch herkend op basis van het e-mailadres van de afzender", payload.auto_match_email ? ` (${payload.auto_match_email})` : "", "."] })), matchWarning && (_jsx("div", { className: "rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-800", children: matchWarning })), customerField && renderField(customerField), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { children: "Locatie" }), _jsx(SearchableSelect, { value: deliveryLocationVal, placeholder: !customerVal
633
+ const ai = (payload.ai ?? null);
634
+ return (_jsxs(_Fragment, { children: [ai && ai.ok === false && (_jsx("div", { className: "rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-800", children: ai.retry_pending
635
+ ? "De AI-dienst was even bezet — we proberen deze mail automatisch opnieuw. Je kunt de velden intussen zelf invullen."
636
+ : "Deze mail is niet automatisch uitgelezen — vul de velden handmatig in." })), matchWarning && (_jsx("div", { className: "rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-800", children: matchWarning })), customerField && renderField(customerField), customerNotLocal && (_jsxs("div", { className: "rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-800 flex items-center justify-between gap-2", children: [_jsx("span", { children: "Deze klant bestaat nog niet lokaal. Maak de klant eerst aan." }), _jsx(Button, { size: "sm", variant: "outline", onClick: () => setNewCustomer(customers.find((c) => c.id === customerVal) ?? null), children: "Klant aanmaken" })] })), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { children: "Locatie" }), _jsx(SearchableSelect, { value: deliveryLocationVal, placeholder: !customerVal
344
637
  ? "Selecteer eerst een klant"
345
638
  : deliveryLocations.length
346
639
  ? "Kies locatie..."
@@ -355,14 +648,14 @@ function ProposalEditor({ proposal, template, customers, products, plantTypes, s
355
648
  label: (l.name || addr || "Locatie") + (l.is_primary ? " ★" : ""),
356
649
  hint: l.name && addr ? addr : undefined,
357
650
  };
358
- }), onChange: (v) => setVal("delivery_location", v) })] }), selectedLocation && (_jsxs("div", { className: "rounded-md border bg-muted/30 p-3 space-y-2", children: [_jsx("div", { className: "text-xs font-medium text-muted-foreground", children: "Adresgegevens" }), _jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsx(ReadOnlyField, { label: "Straat", value: selectedLocation.street }), _jsx(ReadOnlyField, { label: "Huisnummer", value: selectedLocation.house_number }), _jsx(ReadOnlyField, { label: "Postcode", value: selectedLocation.postal_code }), _jsx(ReadOnlyField, { label: "Plaats", value: selectedLocation.city }), _jsx(ReadOnlyField, { label: "Provincie", value: selectedLocation.province }), _jsx(ReadOnlyField, { label: "Land", value: selectedLocation.country })] })] })), _jsxs("div", { className: "space-y-1", children: [_jsxs(Label, { className: "flex items-center gap-2", children: ["Afhaallocatie / Tuin ", _jsx("span", { className: "text-destructive", children: "*" })] }), _jsx(SearchableSelect, { value: pickupLocationVal, placeholder: pickupLocations.length ? "Kies eigen locatie..." : "Geen eigen locaties bekend", clearLabel: "\u2014 Geen \u2014", allowClear: false, disabled: pickupLocations.length === 0, options: pickupLocations.map((l) => ({ value: l.id, label: l.name })), onChange: (v) => setVal("pickup_location", v) }), _jsx("div", { className: "text-xs text-muted-foreground", children: "Locatie van waaruit de bestelling wordt geleverd of opgehaald." })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { children: "Vervoerder" }), _jsx(SearchableSelect, { value: carrierVal, placeholder: !customerVal
651
+ }), onChange: (v) => setVal("delivery_location", v) })] }), selectedLocation && (_jsxs("div", { className: "rounded-md border bg-muted/30 p-3 space-y-2", children: [_jsx("div", { className: "text-xs font-medium text-muted-foreground", children: "Adresgegevens" }), _jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsx(ReadOnlyField, { label: "Straat", value: selectedLocation.street }), _jsx(ReadOnlyField, { label: "Huisnummer", value: selectedLocation.house_number }), _jsx(ReadOnlyField, { label: "Postcode", value: selectedLocation.postal_code }), _jsx(ReadOnlyField, { label: "Plaats", value: selectedLocation.city }), _jsx(ReadOnlyField, { label: "Provincie", value: selectedLocation.province }), _jsx(ReadOnlyField, { label: "Land", value: selectedLocation.country })] })] })), _jsxs("div", { className: "space-y-1", children: [_jsxs(Label, { className: "flex items-center gap-2", children: ["Afhaallocatie / Tuin ", _jsx("span", { className: "text-destructive", children: "*" })] }), _jsx(SearchableSelect, { value: pickupLocationVal, placeholder: pickupLocations.length ? "Kies eigen locatie..." : "Geen eigen locaties bekend", clearLabel: "\u2014 Geen \u2014", allowClear: false, disabled: pickupLocations.length === 0, options: pickupLocations.map((l) => ({ value: l.id, label: l.name + (l.is_default ? " ★" : "") })), onChange: (v) => setVal("pickup_location", v) }), _jsx("div", { className: "text-xs text-muted-foreground", children: "Locatie van waaruit de bestelling wordt geleverd of opgehaald." })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { children: "Vervoerder" }), _jsx(SearchableSelect, { value: carrierVal, placeholder: !customerVal
359
652
  ? "Selecteer eerst een klant"
360
653
  : !deliveryLocationVal
361
654
  ? "Selecteer eerst een locatie"
362
655
  : "Kies vervoerder...", clearLabel: "\u2014 Geen \u2014", disabled: !deliveryLocationVal, options: carriers.map((c) => ({
363
656
  value: c.id,
364
657
  label: c.name + (c.is_default ? " (standaard)" : ""),
365
- })), onChange: (v) => setVal("carrier", v) }), selectedLocation?.carrier_id && carrierVal !== selectedLocation.carrier_id && (_jsx("div", { className: "text-xs text-muted-foreground", children: "Wijkt af van standaard vervoerder van deze locatie \u2014 geldt alleen voor deze bestelling." }))] }), _jsxs("div", { className: "space-y-1", children: [_jsxs(Label, { className: "flex items-center gap-2", children: [deliveryDateField?.label ?? "Verzenddatum", deliveryDateField?.required && _jsx("span", { className: "text-destructive", children: "*" })] }), _jsx(Input, { type: "date", value: canPickDate ? (deliveryVal ?? "") : "", min: canPickDate ? earliestISO : undefined, disabled: !canPickDate, onChange: (e) => {
658
+ })), onChange: (v) => setVal("carrier", v) }), selectedLocation?.carrier_id && carrierVal !== selectedLocation.carrier_id && (_jsx("div", { className: "text-xs text-muted-foreground", children: "Wijkt af van standaard vervoerder van deze locatie \u2014 geldt alleen voor deze bestelling." }))] }), _jsxs("div", { className: "space-y-1", children: [_jsxs(Label, { className: "flex items-center gap-2", children: ["Bezorgdatum", deliveryDateField?.required && _jsx("span", { className: "text-destructive", children: "*" })] }), _jsx(Input, { type: "date", value: canPickDate ? (deliveryVal ?? "") : "", min: canPickDate ? earliestISO : undefined, disabled: !canPickDate, onChange: (e) => {
366
659
  const v = e.target.value || null;
367
660
  if (v && v < earliestISO) {
368
661
  toast.error(`Eerst mogelijke leverdatum is ${earliestLabel}.`);
@@ -370,10 +663,39 @@ function ProposalEditor({ proposal, template, customers, products, plantTypes, s
370
663
  return;
371
664
  }
372
665
  setVal("delivery_date", v);
373
- } }), !canPickDate ? (_jsx("div", { className: "text-xs text-muted-foreground", children: "Selecteer eerst klant, afleverlocatie, afhaallocatie en vervoerder." })) : (_jsxs("div", { className: "text-xs text-muted-foreground", children: ["Eerst beschikbare leverdatum is ", earliestLabel, ", op basis van orderpicktijd (", pickDays, "d) en levertijd vervoerder (", carrierDeliveryDays, "d)."] }))] }), otherFields.map(renderField)] }));
374
- })(), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Bestelregels" }), _jsxs("div", { className: "grid grid-cols-12 gap-2 text-xs text-muted-foreground px-1", children: [_jsx("div", { className: "col-span-5", children: "Product" }), _jsx("div", { className: "col-span-2", children: "Planten" }), _jsx("div", { className: "col-span-2", children: "Fusten" }), _jsx("div", { className: "col-span-2", children: "Eenheid" }), _jsx("div", { className: "col-span-1" })] }), lines.map((line, idx) => {
666
+ } }), !canPickDate ? (_jsx("div", { className: "text-xs text-muted-foreground", children: "Selecteer eerst klant, afleverlocatie, afhaallocatie en vervoerder." })) : (_jsxs("div", { className: "text-xs text-muted-foreground", children: ["Eerst beschikbare leverdatum is ", earliestLabel, earliest.signupAt
667
+ ? `, o.b.v. het eerstvolgende aanmeldmoment bij de vervoerder (${earliest.signupAt.toLocaleString("nl-NL", { weekday: "long", day: "numeric", month: "long", hour: "2-digit", minute: "2-digit" })})`
668
+ : `, op basis van orderpicktijd (${pickDays}d) en levertijd vervoerder (${carrierDeliveryDays}d)`, "."] }))] }), otherFields.map(renderField)] }));
669
+ })(), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Bestelregels" }), _jsxs("div", { className: "grid grid-cols-12 gap-2 text-xs text-muted-foreground px-1", children: [_jsx("div", { className: "col-span-4", children: "Product" }), _jsx("div", { className: "col-span-3", children: "Belading" }), _jsx("div", { className: "col-span-4", children: "Fusten" }), _jsx("div", { className: "col-span-1" })] }), lines.map((line, idx) => {
375
670
  const product = line.product_id ? products.find((p) => p.id === line.product_id) : null;
376
- const ppc = Math.max(1, Number(product?.plants_per_crate ?? 1));
671
+ const stdBelading = defaultBelading(product);
672
+ const available = (line.product_id ? (Array.isArray(beladingByProduct[line.product_id]) ? beladingByProduct[line.product_id] : []) : []).map((b) => ({
673
+ plants_per_crate: Math.max(1, Number(b.plants_per_crate ?? 1)),
674
+ crates_per_layer: Math.max(1, Number(b.crates_per_layer ?? 1)),
675
+ layers_per_cart: Math.max(1, Number(b.layers_per_cart ?? 1)),
676
+ crate_code: b.crate_code ?? null,
677
+ crate_name: b.crate_name ?? null,
678
+ carrier_code: b.carrier_code ?? null,
679
+ carrier_name: b.carrier_name ?? null,
680
+ })).filter((b) => beladingAllowedByEngine(b, loadCarrierOptions));
681
+ // Standaardbelading: gebruik de variant mét fust-/kartype als die bestaat.
682
+ const sameDims = (a, b) => a.plants_per_crate === b.plants_per_crate &&
683
+ a.crates_per_layer === b.crates_per_layer &&
684
+ a.layers_per_cart === b.layers_per_cart;
685
+ const richDefault = stdBelading ? available.find((b) => sameDims(b, stdBelading)) ?? null : null;
686
+ const effDefault = richDefault ?? stdBelading;
687
+ const optionMap = new Map();
688
+ for (const b of available)
689
+ optionMap.set(beladingKey(b), b);
690
+ if (effDefault)
691
+ optionMap.set(beladingKey(effDefault), effDefault);
692
+ const defaultKey = effDefault ? beladingKey(effDefault) : null;
693
+ // Geen belading in de mail → altijd de standaardbelading van het product.
694
+ const chosenBelading = line.belading ?? effDefault;
695
+ const effBelading = chosenBelading && !optionMap.has(beladingKey(chosenBelading))
696
+ ? (available.find((b) => sameDims(b, chosenBelading)) ?? chosenBelading)
697
+ : chosenBelading;
698
+ const ppc = Math.max(1, Number(effBelading?.plants_per_crate ?? product?.plants_per_crate ?? 1));
377
699
  const plants = line.quantity ?? null;
378
700
  const cratesExact = plants !== null ? plants / ppc : null;
379
701
  const cratesDisplay = cratesExact === null ? "" : String(Math.ceil(cratesExact));
@@ -382,48 +704,134 @@ function ProposalEditor({ proposal, template, customers, products, plantTypes, s
382
704
  e.preventDefault();
383
705
  }
384
706
  };
385
- return (_jsxs("div", { className: "grid grid-cols-12 gap-2 items-start", children: [_jsx("div", { className: "col-span-5", children: _jsx(SearchableSelect, { value: line.product_id, placeholder: "Kies product...", clearLabel: "\u2014 Handmatig \u2014", options: products.map((p) => ({
386
- value: p.id,
387
- label: p.product,
388
- hint: `${p.unit ?? ""}${p.available_stock != null ? ` · voorraad: ${p.available_stock}` : ""}`.trim(),
389
- })), onChange: (v) => {
390
- const next = [...lines];
391
- if (v === null)
392
- next[idx] = { ...next[idx], product_id: null };
393
- else {
394
- const p = products.find((x) => x.id === v);
395
- const newPpc = Math.max(1, Number(p?.plants_per_crate ?? 1));
396
- const currentQ = Number(next[idx].quantity ?? 0);
397
- const normalizedQ = currentQ > 0 ? Math.ceil(currentQ / newPpc) * newPpc : newPpc;
707
+ return (_jsxs("div", { className: "grid grid-cols-12 gap-2 items-start", children: [_jsxs("div", { className: "col-span-4", children: [_jsx(SearchableSelect, { value: line.product_id, placeholder: "Kies product...", clearLabel: "\u2014 Handmatig \u2014", options: products.map((p) => {
708
+ const stock = Number(p.available_stock ?? 0);
709
+ const out = stock <= 0;
710
+ return {
711
+ value: p.id,
712
+ label: p.product,
713
+ hint: `${p.unit ?? ""}${p.available_stock != null ? ` · voorraad: ${p.available_stock}` : ""}${out ? (allowBackorder ? " · niet op voorraad (doorverkoop toegestaan)" : " · niet op voorraad") : ""}`.trim(),
714
+ disabled: out && !allowBackorder && p.id !== line.product_id,
715
+ };
716
+ }), onChange: (v) => {
717
+ const next = [...lines];
718
+ if (v === null)
719
+ next[idx] = { ...next[idx], product_id: null, belading: null };
720
+ else {
721
+ const p = products.find((x) => x.id === v);
722
+ const b = defaultBelading(p);
723
+ const newPpc = Math.max(1, Number(b?.plants_per_crate ?? 1));
724
+ const currentQ = Number(next[idx].quantity ?? 0);
725
+ const normalizedQ = currentQ > 0 ? Math.ceil(currentQ / newPpc) * newPpc : newPpc;
726
+ next[idx] = {
727
+ ...next[idx], product_id: v,
728
+ raw_product_text: p?.product ?? next[idx].raw_product_text,
729
+ unit: next[idx].unit ?? p?.unit ?? "stuks",
730
+ quantity: normalizedQ,
731
+ belading: b,
732
+ };
733
+ }
734
+ setLines(next);
735
+ } }), (line.plant_type || line.raw_product_text) && (_jsx("div", { className: "mt-1 truncate text-[11px] text-muted-foreground", children: line.plant_type ? `Plantsoort: ${line.plant_type}` : `Uit mail: ${line.raw_product_text}` }))] }), _jsxs("div", { className: "col-span-3", children: [_jsx(SearchableSelect, { value: effBelading ? beladingKey(effBelading) : null, placeholder: !line.product_id ? "Kies eerst product"
736
+ : loadingBeladingen ? "Beladingen laden..."
737
+ : "Kies belading...", disabled: !line.product_id, allowClear: false, emptyLabel: "Geen belading gekoppeld aan dit product", options: Array.from(optionMap.entries()).map(([key, b]) => ({
738
+ value: key,
739
+ label: beladingLabel(b),
740
+ hint: (key === defaultKey ? "Standaard van product · " : "") +
741
+ beladingHint(b),
742
+ })), onChange: (v) => {
743
+ if (!v)
744
+ return;
745
+ const b = optionMap.get(v);
746
+ if (!b)
747
+ return;
748
+ const next = [...lines];
749
+ const crates = Math.max(1, Math.ceil(Number(next[idx].quantity ?? 0) / ppc));
398
750
  next[idx] = {
399
- ...next[idx], product_id: v,
400
- raw_product_text: p?.product ?? next[idx].raw_product_text,
401
- unit: next[idx].unit ?? p?.unit ?? "stuks",
402
- quantity: normalizedQ,
751
+ ...next[idx],
752
+ belading: b,
753
+ // Fusten behouden, planten herrekenen op de nieuwe belading.
754
+ quantity: crates * b.plants_per_crate,
403
755
  };
404
- }
405
- setLines(next);
406
- } }) }), _jsx(Input, { className: "col-span-2", type: "number", min: ppc, step: ppc, inputMode: "numeric", onKeyDown: blockDecimal, value: plants ?? "", onChange: (e) => {
407
- const next = [...lines];
408
- const raw = e.target.value.replace(/[.,]/g, "");
409
- const v = raw ? Number(raw) : null;
410
- const rounded = v === null ? null : Math.max(1, Math.ceil(v / ppc)) * ppc;
411
- next[idx] = { ...next[idx], quantity: rounded };
412
- setLines(next);
413
- }, placeholder: "Planten", title: `Verkoop per hele fust van ${ppc} planten` }), _jsx(Input, { className: "col-span-2", type: "number", min: 1, step: 1, inputMode: "numeric", onKeyDown: blockDecimal, value: cratesDisplay, onChange: (e) => {
756
+ setLines(next);
757
+ } }), effBelading && (_jsxs("div", { className: "mt-1 text-xs text-muted-foreground", children: [plants ?? 0, " planten", defaultKey && beladingKey(effBelading) !== defaultKey ? " · afwijkend van standaard" : ""] }))] }), _jsx(Input, { className: "col-span-4", type: "number", min: 1, step: 1, inputMode: "numeric", onKeyDown: blockDecimal, value: cratesDisplay, onChange: (e) => {
414
758
  const next = [...lines];
415
759
  const raw = e.target.value.replace(/[.,]/g, "");
416
760
  const cr = raw ? Math.max(1, Math.floor(Number(raw))) : null;
417
761
  next[idx] = { ...next[idx], quantity: cr === null ? null : cr * ppc };
418
762
  setLines(next);
419
- }, placeholder: "Fusten", title: `${ppc} planten per fust — verkoop per hele fust` }), _jsx(Input, { className: "col-span-2", value: line.unit ?? "", onChange: (e) => {
420
- const next = [...lines];
421
- next[idx] = { ...next[idx], unit: e.target.value };
422
- setLines(next);
423
- }, placeholder: "Eenheid" }), _jsx(Button, { variant: "ghost", size: "icon", className: "col-span-1", onClick: () => setLines(lines.filter((_, i) => i !== idx)), children: _jsx(Trash2, { className: "h-4 w-4" }) })] }, idx));
424
- }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setLines([...lines, { product_id: null, raw_product_text: "", quantity: 1, unit: "stuks" }]), children: [_jsx(Plus, { className: "h-3 w-3 mr-1" }), " Regel toevoegen"] })] }), _jsxs("div", { className: "flex gap-2 pt-2", children: [_jsxs(Button, { className: "flex-1", onClick: handleApprove, children: [_jsx(Check, { className: "h-4 w-4 mr-1" }), " Goedkeuren"] }), _jsxs(Button, { variant: "destructive", onClick: () => onReject(notesVal || ""), children: [_jsx(X, { className: "h-4 w-4 mr-1" }), " Afwijzen"] })] })] })] })] }));
763
+ }, placeholder: "Fusten", title: `${ppc} planten per fust — verkoop per hele fust` }), _jsx(Button, { variant: "ghost", size: "icon", className: "col-span-1", onClick: () => setLines(lines.filter((_, i) => i !== idx)), children: _jsx(Trash2, { className: "h-4 w-4" }) })] }, idx));
764
+ }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setLines([...lines, { product_id: null, raw_product_text: "", quantity: 1, unit: "stuks", belading: null }]), children: [_jsx(Plus, { className: "h-3 w-3 mr-1" }), " Regel toevoegen"] })] }), _jsxs("div", { className: "flex gap-2 pt-2", children: [_jsxs(Button, { className: "flex-1", onClick: handleApprove, children: [_jsx(Check, { className: "h-4 w-4 mr-1" }), " Controleren & bevestigen"] }), _jsxs(Button, { variant: "destructive", onClick: () => onReject(notesVal || ""), children: [_jsx(X, { className: "h-4 w-4 mr-1" }), " Afwijzen"] })] }), _jsx(Dialog, { open: confirmOpen, onOpenChange: (o) => {
765
+ if (submitting)
766
+ return;
767
+ setConfirmOpen(o);
768
+ if (!o)
769
+ setCheckedOk(false);
770
+ }, children: _jsxs(DialogContent, { className: "max-w-lg", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: "Bestelling bevestigen" }) }), _jsxs("div", { className: "space-y-3 text-sm", children: [_jsx("p", { className: "text-muted-foreground", children: "De bestelling wordt pas aangemaakt in Bestellingen nadat je hem hebt gecontroleerd en bevestigd." }), _jsxs("div", { className: "rounded-md border p-3 space-y-1", children: [_jsxs("div", { children: [_jsx("span", { className: "text-muted-foreground", children: "Klant:" }), " ", customers.find((c) => c.id === customerVal)?.company_name ?? "-"] }), _jsxs("div", { children: [_jsx("span", { className: "text-muted-foreground", children: "Bezorgdatum:" }), " ", deliveryVal || "-"] }), _jsxs("div", { children: [_jsx("span", { className: "text-muted-foreground", children: "Bestelregels:" }), " ", lines.filter((l) => l.product_id && (l.quantity ?? 0) > 0).length] })] }), _jsxs("label", { className: "flex items-start gap-2 cursor-pointer", children: [_jsx("input", { type: "checkbox", className: "mt-1", checked: checkedOk, onChange: (e) => setCheckedOk(e.target.checked) }), _jsx("span", { children: "Ik heb deze bestelling gecontroleerd en de gegevens kloppen." })] })] }), _jsxs("div", { className: "flex gap-2 pt-2", children: [_jsxs(Button, { className: "flex-1", disabled: !checkedOk || submitting, onClick: handleConfirm, children: [_jsx(Check, { className: "h-4 w-4 mr-1" }), submitting ? "Bezig…" : "Bevestigen en bestelling aanmaken"] }), _jsx(Button, { variant: "outline", disabled: submitting, onClick: () => setConfirmOpen(false), children: "Annuleren" })] })] }) }), _jsx(NewCustomerDialog, { candidate: newCustomer, onClose: () => setNewCustomer(null), onCreated: (id) => { setNewCustomer(null); setVal("customer", id); } })] })] })] }));
771
+ }
772
+ /** Verplichte bevestiging: externe klant eerst als lokale klant aanmaken. */
773
+ function NewCustomerDialog({ candidate, onClose, onCreated, }) {
774
+ const qc = useQueryClient();
775
+ const createCustomer = useServerFn(createCustomerFromGroup);
776
+ const [form, setForm] = useState({});
777
+ const [busy, setBusy] = useState(false);
778
+ useEffect(() => {
779
+ if (!candidate)
780
+ return;
781
+ setForm({
782
+ companyName: candidate.company_name ?? "",
783
+ email: candidate.email ?? "",
784
+ street: candidate.street ?? "",
785
+ houseNumber: candidate.house_number ?? "",
786
+ postalCode: candidate.postal_code ?? "",
787
+ city: candidate.city ?? "",
788
+ country: candidate.country ?? "",
789
+ gln: candidate.gln ?? "",
790
+ phone: candidate.phone ?? "",
791
+ });
792
+ }, [candidate?.id]);
793
+ const set = (k, v) => setForm((p) => ({ ...p, [k]: v }));
794
+ const field = (k, label, required = false) => (_jsxs("div", { className: "space-y-1", children: [_jsxs(Label, { children: [label, required && _jsx("span", { className: "text-destructive", children: " *" })] }), _jsx(Input, { value: form[k] ?? "", onChange: (e) => set(k, e.target.value) })] }));
795
+ const submit = async () => {
796
+ if (!candidate)
797
+ return;
798
+ if (!(form["companyName"] ?? "").trim()) {
799
+ toast.error("Bedrijfsnaam is verplicht.");
800
+ return;
801
+ }
802
+ setBusy(true);
803
+ try {
804
+ const res = await createCustomer({
805
+ data: {
806
+ groupKey: candidate.group_key ?? candidate.id,
807
+ companyName: form["companyName"].trim(),
808
+ email: (form["email"] ?? "").trim() || null,
809
+ street: (form["street"] ?? "").trim() || null,
810
+ houseNumber: (form["houseNumber"] ?? "").trim() || null,
811
+ postalCode: (form["postalCode"] ?? "").trim() || null,
812
+ city: (form["city"] ?? "").trim() || null,
813
+ country: (form["country"] ?? "").trim() || null,
814
+ gln: (form["gln"] ?? "").trim() || null,
815
+ phone: (form["phone"] ?? "").trim() || null,
816
+ },
817
+ });
818
+ if (!res?.id)
819
+ throw new Error("Klant kon niet worden aangemaakt.");
820
+ await qc.invalidateQueries({ queryKey: ["mailbox-customers"] });
821
+ toast.success(res.created ? "Klant aangemaakt." : "Bestaande klant gekoppeld.");
822
+ onCreated(res.id);
823
+ }
824
+ catch (e) {
825
+ toast.error(e?.message ?? "Aanmaken mislukt.");
826
+ }
827
+ finally {
828
+ setBusy(false);
829
+ }
830
+ };
831
+ return (_jsx(Dialog, { open: !!candidate, onOpenChange: (o) => { if (!o)
832
+ onClose(); }, children: _jsxs(DialogContent, { className: "max-w-xl", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: "Nieuwe klant aanmaken" }) }), _jsxs("p", { className: "text-sm text-muted-foreground", children: ["Deze klant bestaat alleen in ", (candidate?.providers ?? []).map(providerLabel).join(" + ") || "een externe bron", ". Controleer de gegevens en maak de klant aan voordat je de bestelling verwerkt."] }), _jsxs("div", { className: "grid grid-cols-2 gap-3 py-2", children: [field("companyName", "Bedrijfsnaam", true), field("email", "E-mail"), field("street", "Straat"), field("houseNumber", "Huisnummer"), field("postalCode", "Postcode"), field("city", "Plaats"), field("country", "Land"), field("gln", "GLN"), field("phone", "Telefoon")] }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { className: "flex-1", disabled: busy, onClick: submit, children: busy ? "Bezig…" : "Klant aanmaken en koppelen" }), _jsx(Button, { variant: "outline", disabled: busy, onClick: onClose, children: "Annuleren" })] })] }) }));
425
833
  }
426
- function DynamicField({ field, value, meta, onChange, customers, products, plantTypes, suppliers, }) {
834
+ function DynamicField({ field, value, meta, onChange, customers, products, plantTypes, suppliers, onSelectCustomer, }) {
427
835
  const reviewClass = meta?.needs_review ? "border-yellow-500 ring-1 ring-yellow-500/40 rounded-md p-2" : "";
428
836
  const label = (_jsxs(Label, { className: "flex items-center gap-2", children: [field.label, field.required && _jsx("span", { className: "text-destructive", children: "*" }), meta?.needs_review && (_jsx(Badge, { variant: "outline", className: "text-xs border-yellow-500 text-yellow-700", children: "Controleren" })), typeof meta?.ai_confidence === "number" && (_jsxs("span", { className: "text-xs text-muted-foreground", children: [Math.round(meta.ai_confidence * 100), "%"] }))] }));
429
837
  const renderControl = () => {
@@ -448,7 +856,11 @@ function DynamicField({ field, value, meta, onChange, customers, products, plant
448
856
  }) }));
449
857
  }
450
858
  case "customer":
451
- return (_jsx(SearchableSelect, { value: value ?? null, placeholder: "Kies klant...", options: customers.map((c) => ({ value: c.id, label: c.company_name, hint: c.email })), onChange: onChange }));
859
+ return (_jsx(SearchableSelect, { value: value ?? null, placeholder: "Kies klant...", options: customers.map((c) => ({
860
+ value: c.id,
861
+ label: c.company_name + (c.is_local === false ? " (nog niet aangemaakt)" : ""),
862
+ hint: customerHint(c),
863
+ })), onChange: (v) => (onSelectCustomer ? onSelectCustomer(v) : onChange(v)) }));
452
864
  case "product":
453
865
  return (_jsx(SearchableSelect, { value: value ?? null, placeholder: "Kies product...", options: products.map((p) => ({ value: p.id, label: p.product })), onChange: onChange }));
454
866
  case "plant_type":