@flowselections/mailbox-orders 1.0.2 → 1.0.4

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 (35) hide show
  1. package/dist-lib/components/CartAssignmentWizard.d.ts +6 -0
  2. package/dist-lib/components/CartAssignmentWizard.d.ts.map +1 -0
  3. package/dist-lib/components/CartAssignmentWizard.js +199 -0
  4. package/dist-lib/components/MailboxInboxPage.d.ts.map +1 -1
  5. package/dist-lib/components/MailboxInboxPage.js +78 -19
  6. package/dist-lib/components/MailboxProcessedPage.d.ts.map +1 -1
  7. package/dist-lib/components/MailboxProcessedPage.js +25 -4
  8. package/dist-lib/components/MailboxProposalsPage.d.ts.map +1 -1
  9. package/dist-lib/components/MailboxProposalsPage.js +272 -34
  10. package/dist-lib/components/SearchableSelect.d.ts +2 -1
  11. package/dist-lib/components/SearchableSelect.d.ts.map +1 -1
  12. package/dist-lib/components/SearchableSelect.js +4 -3
  13. package/dist-lib/index.d.ts +0 -1
  14. package/dist-lib/index.d.ts.map +1 -1
  15. package/dist-lib/index.js +3 -7
  16. package/dist-lib/integrations/supabase/auth-attacher.d.ts.map +1 -1
  17. package/dist-lib/integrations/supabase/auth-attacher.js +37 -3
  18. package/dist-lib/integrations/supabase/auth-middleware.d.ts +7662 -1413
  19. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  20. package/dist-lib/integrations/supabase/client.d.ts +7662 -1413
  21. package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
  22. package/dist-lib/integrations/supabase/client.server.d.ts +7662 -1413
  23. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  24. package/dist-lib/integrations/supabase/types.d.ts +7837 -1406
  25. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  26. package/dist-lib/integrations/supabase/types.js +20 -1
  27. package/dist-lib/lib/mailbox-ai.server.d.ts +1 -1
  28. package/dist-lib/lib/mailbox-ai.server.d.ts.map +1 -1
  29. package/dist-lib/lib/mailbox-ai.server.js +6 -106
  30. package/dist-lib/lib/mailbox-template.functions.d.ts +49077 -11583
  31. package/dist-lib/lib/mailbox-template.functions.d.ts.map +1 -1
  32. package/dist-lib/lib/mailbox.functions.d.ts +143316 -19105
  33. package/dist-lib/lib/mailbox.functions.d.ts.map +1 -1
  34. package/dist-lib/lib/mailbox.functions.js +536 -173
  35. package/package.json +4 -8
@@ -1,12 +1,14 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useMemo, useState } from "react";
3
- import { useQuery, useQueryClient } from "@tanstack/react-query";
2
+ import { useEffect, useMemo, useRef, useState } from "react";
3
+ 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
- import { Check, X, Trash2, Plus, AlertTriangle } from "lucide-react";
7
- import { listProposals, approveProposal, rejectProposal, listCustomersForPicker, listProductsForPicker, } from "../lib/mailbox.functions";
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";
8
8
  import { listTemplateFields, listPlantTypesForPicker, listSuppliersForPicker, } from "../lib/mailbox-template.functions";
9
9
  import { SearchableSelect } from "./SearchableSelect";
10
+ import { CartAssignmentWizard } from "./CartAssignmentWizard";
11
+ import { supabase } from "../integrations/supabase/client";
10
12
  export function MailboxProposalsPage() {
11
13
  const qc = useQueryClient();
12
14
  const fetchList = useServerFn(listProposals);
@@ -17,29 +19,121 @@ export function MailboxProposalsPage() {
17
19
  const fetchProducts = useServerFn(listProductsForPicker);
18
20
  const fetchPlantTypes = useServerFn(listPlantTypesForPicker);
19
21
  const fetchSuppliers = useServerFn(listSuppliersForPicker);
20
- const [status, setStatus] = useState("pending");
22
+ const fetchCarriers = useServerFn(listCarriersForPicker);
23
+ const fetchPickupLocations = useServerFn(listPickupLocations);
24
+ const fetchActive = useServerFn(getActiveOrdersMailbox);
25
+ const fetchIngest = useServerFn(ingestEmailEvent);
26
+ const fetchBackfill = useServerFn(backfillRecentEvents);
27
+ // Server fns met requireSupabaseAuth gooien een raw Response bij een stale
28
+ // bearer-token. Die bubbelt op als "[object Response]" en blankt het scherm.
29
+ // Vang elke fout en geef een lege fallback terug — UI blijft bruikbaar.
30
+ const safe = async (fn) => {
31
+ try {
32
+ return await fn();
33
+ }
34
+ catch {
35
+ return { items: [] };
36
+ }
37
+ };
38
+ // Bestelvoorstellen toont uitsluitend nieuwe, nog niet gecontroleerde
39
+ // bestellingen ("Te controleren"). Goedgekeurd/afgewezen verschijnt in
40
+ // de module Verwerkt.
21
41
  const { data, isLoading } = useQuery({
22
- queryKey: ["mailbox-proposals", status],
23
- queryFn: () => fetchList({ data: { status } }),
42
+ queryKey: ["mailbox-proposals", "pending"],
43
+ queryFn: () => safe(() => fetchList({ data: { status: "pending" } })),
24
44
  });
25
45
  const { data: templateData } = useQuery({
26
46
  queryKey: ["mailbox-template-fields"],
27
- queryFn: () => fetchTemplate(),
47
+ queryFn: () => safe(() => fetchTemplate()),
28
48
  });
29
- const { data: customersData } = useQuery({ queryKey: ["mailbox-customers"], queryFn: () => fetchCustomers() });
30
- const { data: productsData } = useQuery({ queryKey: ["mailbox-products"], queryFn: () => fetchProducts() });
31
- const { data: plantData } = useQuery({ queryKey: ["mailbox-plant-types"], queryFn: () => fetchPlantTypes() });
32
- const { data: supplierData } = useQuery({ queryKey: ["mailbox-suppliers"], queryFn: () => fetchSuppliers() });
49
+ const { data: customersData } = useQuery({ queryKey: ["mailbox-customers"], queryFn: () => safe(() => fetchCustomers()) });
50
+ const { data: productsData } = useQuery({ queryKey: ["mailbox-products"], queryFn: () => safe(() => fetchProducts()) });
51
+ const { data: plantData } = useQuery({ queryKey: ["mailbox-plant-types"], queryFn: () => safe(() => fetchPlantTypes()) });
52
+ const { data: supplierData } = useQuery({ queryKey: ["mailbox-suppliers"], queryFn: () => safe(() => fetchSuppliers()) });
53
+ const { data: carriersData } = useQuery({ queryKey: ["mailbox-carriers"], queryFn: () => safe(() => fetchCarriers()) });
54
+ const { data: pickupLocData } = useQuery({ queryKey: ["mailbox-pickup-locations"], queryFn: () => safe(() => fetchPickupLocations()) });
55
+ // Realtime ingestie + backfill — voorheen in de Inbox-pagina.
56
+ const { data: activeMailbox } = useQuery({
57
+ queryKey: ["mailbox-active-account"],
58
+ queryFn: () => fetchActive(),
59
+ refetchInterval: 60000,
60
+ retry: 1,
61
+ });
62
+ const activeMailboxId = activeMailbox?.connected ? activeMailbox.id : null;
63
+ const backfillMut = useMutation({
64
+ mutationFn: async () => {
65
+ try {
66
+ return await fetchBackfill();
67
+ }
68
+ catch {
69
+ return { ok: false, inserted: 0 };
70
+ }
71
+ },
72
+ onSuccess: (res) => {
73
+ if (res?.ok && (res.inserted ?? 0) > 0) {
74
+ qc.invalidateQueries({ queryKey: ["mailbox-proposals"] });
75
+ }
76
+ },
77
+ });
78
+ const handleManualSync = () => {
79
+ const p = backfillMut.mutateAsync();
80
+ toast.promise(p, {
81
+ loading: "Mailbox synchroniseren...",
82
+ success: (res) => res?.ok
83
+ ? (res.inserted ?? 0) > 0
84
+ ? `${res.inserted} nieuwe bestelling(en) opgehaald`
85
+ : "Geen nieuwe bestellingen"
86
+ : `Sync mislukt: ${res?.error ?? "onbekende fout"}`,
87
+ error: "Sync mislukt",
88
+ });
89
+ };
90
+ useEffect(() => {
91
+ if (!activeMailboxId)
92
+ return;
93
+ const channel = supabase
94
+ .channel(`email_auth_events:${activeMailboxId}`)
95
+ .on("postgres_changes", {
96
+ event: "INSERT", schema: "public", table: "email_auth_events",
97
+ filter: `connection_id=eq.${activeMailboxId}`,
98
+ }, async (payload) => {
99
+ const evId = payload?.new?.id;
100
+ if (!evId)
101
+ return;
102
+ try {
103
+ const res = await fetchIngest({ data: { eventId: evId } });
104
+ if (res?.ok && (res.inserted ?? 0) > 0) {
105
+ toast.success("Nieuwe bestelling ontvangen");
106
+ qc.invalidateQueries({ queryKey: ["mailbox-proposals"] });
107
+ }
108
+ }
109
+ catch { /* backfill vangt op */ }
110
+ })
111
+ .subscribe();
112
+ return () => { void supabase.removeChannel(channel); };
113
+ }, [activeMailboxId, fetchIngest, qc]);
114
+ const didInitialBackfill = useRef(false);
115
+ useEffect(() => {
116
+ if (!activeMailboxId || didInitialBackfill.current)
117
+ return;
118
+ didInitialBackfill.current = true;
119
+ backfillMut.mutate();
120
+ const t = setInterval(() => backfillMut.mutate(), 300000);
121
+ return () => clearInterval(t);
122
+ // eslint-disable-next-line react-hooks/exhaustive-deps
123
+ }, [activeMailboxId]);
33
124
  const items = Array.isArray(data?.items) ? data.items : [];
34
125
  const template = Array.isArray(templateData?.items) ? templateData.items : [];
35
126
  const customers = Array.isArray(customersData?.items) ? customersData.items : [];
36
127
  const products = Array.isArray(productsData?.items) ? productsData.items : [];
37
128
  const plantTypes = Array.isArray(plantData?.items) ? plantData.items : [];
38
129
  const suppliers = Array.isArray(supplierData?.items) ? supplierData.items : [];
130
+ const carriers = Array.isArray(carriersData?.items) ? carriersData.items : [];
131
+ const pickupLocations = Array.isArray(pickupLocData?.items) ? pickupLocData.items : [];
39
132
  const [activeId, setActiveId] = useState(null);
133
+ const [cartOrderId, setCartOrderId] = useState(null);
40
134
  const active = items.find((p) => p.id === activeId) ?? null;
41
- const statusLabel = (s) => s === "approved" ? "Goedgekeurd" : s === "rejected" ? "Afgewezen" : "Nieuw";
42
- const statusVariant = (s) => s === "approved" ? "default" : s === "rejected" ? "destructive" : "secondary";
135
+ const statusLabel = (_s) => "Te controleren";
136
+ const statusVariant = (_s) => "secondary";
43
137
  const customerNameFor = (p) => {
44
138
  const fvs = Array.isArray(p.mailbox_proposal_field_values) ? p.mailbox_proposal_field_values : [];
45
139
  const fv = fvs.find((v) => v.field_key === "customer");
@@ -52,18 +146,20 @@ export function MailboxProposalsPage() {
52
146
  return p.mailbox_messages?.from_name || p.mailbox_messages?.from_email || "Onbekend";
53
147
  };
54
148
  const orderNumberFor = (p) => `MB-${String(p.id).slice(0, 8).toUpperCase()}`;
55
- return (_jsxs("div", { className: "p-6 space-y-4", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("h1", { className: "text-2xl font-semibold", children: "Voorstellen" }), _jsxs(Select, { value: status, onValueChange: (v) => setStatus(v), children: [_jsx(SelectTrigger, { className: "w-48", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "pending", children: "Nieuw / In behandeling" }), _jsx(SelectItem, { value: "approved", children: "Goedgekeurd" }), _jsx(SelectItem, { value: "rejected", children: "Afgekeurd" })] })] })] }), isLoading ? (_jsx("div", { className: "text-muted-foreground", children: "Laden..." })) : items.length === 0 ? (_jsx("p", { className: "text-muted-foreground", children: "Geen voorstellen in deze categorie." })) : (_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) => {
149
+ 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) => {
56
150
  const fvs = Array.isArray(p.mailbox_proposal_field_values) ? p.mailbox_proposal_field_values : [];
57
151
  const needsReview = fvs.some((v) => v.needs_review);
58
152
  const date = p.mailbox_messages?.received_at ?? p.created_at;
59
153
  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));
60
154
  }) })] }) }) })), _jsx(Dialog, { open: !!active, onOpenChange: (o) => { if (!o)
61
- setActiveId(null); }, children: _jsx(DialogContent, { className: "max-w-[95vw] w-[95vw] h-[92vh] max-h-[92vh] overflow-y-auto p-6", children: active && (_jsxs(_Fragment, { children: [_jsx(DialogHeader, { children: _jsxs(DialogTitle, { children: [orderNumberFor(active), " \u00B7 ", customerNameFor(active)] }) }), _jsx(ProposalEditor, { proposal: active, template: template, customers: customers, products: products, plantTypes: plantTypes, suppliers: suppliers, onApprove: async (payload) => {
155
+ setActiveId(null); }, children: _jsx(DialogContent, { className: "max-w-[95vw] w-[95vw] h-[92vh] max-h-[92vh] overflow-y-auto p-6", children: active && (_jsxs(_Fragment, { children: [_jsx(DialogHeader, { children: _jsxs(DialogTitle, { children: [orderNumberFor(active), " \u00B7 ", customerNameFor(active)] }) }), _jsx(ProposalEditor, { proposal: active, template: template, customers: customers, products: products, plantTypes: plantTypes, suppliers: suppliers, carriers: carriers, pickupLocations: pickupLocations, onApprove: async (payload) => {
62
156
  try {
63
- await fetchApprove({ data: payload });
157
+ const res = await fetchApprove({ data: payload });
64
158
  toast.success("Bestelling aangemaakt");
65
159
  qc.invalidateQueries({ queryKey: ["mailbox-proposals"] });
66
160
  setActiveId(null);
161
+ if (res?.orderId)
162
+ setCartOrderId(res.orderId);
67
163
  }
68
164
  catch (e) {
69
165
  toast.error(e?.message ?? "Goedkeuren mislukt");
@@ -78,9 +174,12 @@ export function MailboxProposalsPage() {
78
174
  catch (e) {
79
175
  toast.error(e?.message ?? "Afwijzen mislukt");
80
176
  }
81
- } }, active.id)] })) }) })] }));
177
+ } }, active.id)] })) }) }), _jsx(CartAssignmentWizard, { orderId: cartOrderId, open: !!cartOrderId, onClose: () => setCartOrderId(null) })] }));
82
178
  }
83
- function ProposalEditor({ proposal, template, customers, products, plantTypes, suppliers, onApprove, onReject, }) {
179
+ function ReadOnlyField({ label, value }) {
180
+ 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" }) })] }));
181
+ }
182
+ function ProposalEditor({ proposal, template, customers, products, plantTypes, suppliers, carriers, pickupLocations, onApprove, onReject, }) {
84
183
  const msg = proposal.mailbox_messages ?? {};
85
184
  const fieldValues = Array.isArray(proposal.mailbox_proposal_field_values)
86
185
  ? proposal.mailbox_proposal_field_values : [];
@@ -96,6 +195,10 @@ function ProposalEditor({ proposal, template, customers, products, plantTypes, s
96
195
  visibleFields.forEach((f) => {
97
196
  init[f.key] = fvMap[f.key]?.value ?? f.default_value ?? null;
98
197
  });
198
+ // Vul de auto-herkende klant in als er nog geen waarde uit het template komt
199
+ if (!init["customer"] && proposal.matched_customer_id) {
200
+ init["customer"] = proposal.matched_customer_id;
201
+ }
99
202
  return init;
100
203
  });
101
204
  const initialLines = Array.isArray(proposal.mailbox_proposal_lines)
@@ -114,12 +217,99 @@ function ProposalEditor({ proposal, template, customers, products, plantTypes, s
114
217
  const deliveryVal = values["delivery_date"];
115
218
  const notesVal = values["notes"];
116
219
  const setVal = (key, v) => setValues((prev) => ({ ...prev, [key]: v }));
220
+ const fetchDeliveryLocations = useServerFn(listDeliveryLocationsForCustomer);
221
+ const { data: deliveryLocData } = useQuery({
222
+ queryKey: ["mailbox-delivery-locations", customerVal],
223
+ enabled: !!customerVal,
224
+ queryFn: async () => {
225
+ try {
226
+ return await fetchDeliveryLocations({ data: { customerId: customerVal } });
227
+ }
228
+ catch {
229
+ return { items: [] };
230
+ }
231
+ },
232
+ });
233
+ const deliveryLocations = Array.isArray(deliveryLocData?.items) ? deliveryLocData.items : [];
234
+ const deliveryLocationVal = values["delivery_location"];
235
+ const carrierVal = values["carrier"];
236
+ const selectedLocation = deliveryLocations.find((l) => l.id === deliveryLocationVal) ?? null;
237
+ // Reset delivery_location wanneer klant wijzigt en huidige waarde niet meer past
238
+ if (deliveryLocationVal && deliveryLocations.length > 0 &&
239
+ !deliveryLocations.some((l) => l.id === deliveryLocationVal)) {
240
+ // schedule reset via microtask om setState-in-render te vermijden
241
+ queueMicrotask(() => setVal("delivery_location", null));
242
+ }
243
+ // Auto-vul vervoerder zodra een nieuwe locatie wordt gekozen (één keer per wissel).
244
+ const lastLocRef = useRef(deliveryLocationVal);
245
+ useEffect(() => {
246
+ if (deliveryLocationVal !== lastLocRef.current) {
247
+ lastLocRef.current = deliveryLocationVal;
248
+ const loc = deliveryLocations.find((l) => l.id === deliveryLocationVal);
249
+ if (loc?.carrier_id)
250
+ setVal("carrier", loc.carrier_id);
251
+ else if (deliveryLocationVal === null)
252
+ setVal("carrier", null);
253
+ }
254
+ // eslint-disable-next-line react-hooks/exhaustive-deps
255
+ }, [deliveryLocationVal, deliveryLocations.length]);
256
+ // ─── Eerst mogelijke leverdatum ────────────────────────────────────────────
257
+ const fetchPickDays = useServerFn(getOrderPickDays);
258
+ const { data: pickDaysData } = useQuery({
259
+ queryKey: ["mailbox-order-pick-days"],
260
+ queryFn: async () => {
261
+ try {
262
+ return await fetchPickDays();
263
+ }
264
+ catch {
265
+ return { pickDays: 0 };
266
+ }
267
+ },
268
+ });
269
+ const pickDays = Math.max(0, Number(pickDaysData?.pickDays ?? 0));
270
+ const selectedCarrier = carriers.find((c) => c.id === carrierVal) ?? null;
271
+ const carrierDeliveryDays = Math.max(0, Number(selectedCarrier?.delivery_days ?? 0));
272
+ const pickupLocationVal = values["pickup_location"];
273
+ const canPickDate = !!customerVal && !!deliveryLocationVal && !!pickupLocationVal && !!carrierVal;
274
+ const fmtISO = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
275
+ const earliestDate = useMemo(() => {
276
+ const base = new Date();
277
+ base.setHours(0, 0, 0, 0);
278
+ base.setDate(base.getDate() + pickDays + carrierDeliveryDays);
279
+ return base;
280
+ }, [pickDays, carrierDeliveryDays]);
281
+ const earliestISO = fmtISO(earliestDate);
282
+ const earliestLabel = earliestDate.toLocaleDateString("nl-NL", {
283
+ day: "numeric", month: "long", year: "numeric",
284
+ });
285
+ // Synchroniseer delivery_date wanneer de eerst mogelijke datum verandert.
286
+ useEffect(() => {
287
+ if (!canPickDate)
288
+ return;
289
+ if (!deliveryVal || deliveryVal < earliestISO) {
290
+ setVal("delivery_date", earliestISO);
291
+ }
292
+ // eslint-disable-next-line react-hooks/exhaustive-deps
293
+ }, [canPickDate, earliestISO]);
117
294
  const handleApprove = () => {
118
295
  const missing = template.filter((f) => f.required && (values[f.key] === null || values[f.key] === undefined || values[f.key] === ""));
119
296
  if (missing.length > 0) {
120
297
  toast.error(`Verplichte velden ontbreken: ${missing.map((m) => m.label).join(", ")}`);
121
298
  return;
122
299
  }
300
+ if (!deliveryLocationVal) {
301
+ toast.error("Selecteer een afleverlocatie.");
302
+ return;
303
+ }
304
+ if (!pickupLocationVal) {
305
+ toast.error("Selecteer een afhaallocatie (eigen locatie / tuin).");
306
+ return;
307
+ }
308
+ const validLines = lines.filter((l) => l.product_id && (l.quantity ?? 0) > 0);
309
+ if (validLines.length === 0) {
310
+ toast.error("Voeg minimaal één product toe aan de bestelling.");
311
+ return;
312
+ }
123
313
  const cust = customers.find((c) => c.id === customerVal);
124
314
  onApprove({
125
315
  proposalId: proposal.id,
@@ -127,11 +317,57 @@ function ProposalEditor({ proposal, template, customers, products, plantTypes, s
127
317
  customerName: cust?.company_name ?? msg.from_name ?? msg.from_email ?? "Onbekend",
128
318
  deliveryDate: deliveryVal || new Date(Date.now() + 86400000).toISOString().slice(0, 10),
129
319
  notes: notesVal || null,
130
- lines: lines.filter((l) => l.raw_product_text.trim() !== ""),
320
+ lines: validLines,
131
321
  fieldValues: Object.entries(values).map(([field_key, value]) => ({ field_key, value })),
132
322
  });
133
323
  };
134
- 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: [visibleFields.map((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))), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Bestelregels" }), lines.map((line, idx) => (_jsxs("div", { className: "grid grid-cols-12 gap-2 items-start", children: [_jsxs("div", { className: "col-span-6", children: [_jsx(SearchableSelect, { value: line.product_id, placeholder: "Kies product...", clearLabel: "\u2014 Handmatig \u2014", options: products.map((p) => ({ value: p.id, label: p.product, hint: p.unit })), onChange: (v) => {
324
+ 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: [(() => {
325
+ const customerField = visibleFields.find((f) => f.key === "customer");
326
+ const deliveryDateField = visibleFields.find((f) => f.key === "delivery_date");
327
+ const otherFields = visibleFields.filter((f) => f.key !== "customer" && f.key !== "delivery_date");
328
+ 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));
329
+ const payload = (proposal.parsed_payload ?? {});
330
+ const autoMatched = payload.auto_matched_customer === true;
331
+ const matchWarning = typeof payload.auto_match_warning === "string" ? payload.auto_match_warning : null;
332
+ 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
333
+ ? "Selecteer eerst een klant"
334
+ : deliveryLocations.length
335
+ ? "Kies locatie..."
336
+ : "Geen locaties bekend voor deze klant", clearLabel: "\u2014 Geen \u2014", disabled: !customerVal || deliveryLocations.length === 0, options: deliveryLocations.map((l) => {
337
+ const addr = [
338
+ l.address_line || [l.street, l.house_number].filter(Boolean).join(" "),
339
+ l.postal_code,
340
+ l.city,
341
+ ].filter(Boolean).join(", ");
342
+ return {
343
+ value: l.id,
344
+ label: (l.name || addr || "Locatie") + (l.is_primary ? " ★" : ""),
345
+ hint: l.name && addr ? addr : undefined,
346
+ };
347
+ }), 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
348
+ ? "Selecteer eerst een klant"
349
+ : !deliveryLocationVal
350
+ ? "Selecteer eerst een locatie"
351
+ : "Kies vervoerder...", clearLabel: "\u2014 Geen \u2014", disabled: !deliveryLocationVal, options: carriers.map((c) => ({
352
+ value: c.id,
353
+ label: c.name + (c.is_default ? " (standaard)" : ""),
354
+ })), 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) => {
355
+ const v = e.target.value || null;
356
+ if (v && v < earliestISO) {
357
+ toast.error(`Eerst mogelijke leverdatum is ${earliestLabel}.`);
358
+ setVal("delivery_date", earliestISO);
359
+ return;
360
+ }
361
+ setVal("delivery_date", v);
362
+ } }), !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)] }));
363
+ })(), _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) => {
364
+ const product = line.product_id ? products.find((p) => p.id === line.product_id) : null;
365
+ const ppc = Math.max(1, Number(product?.plants_per_crate ?? 1));
366
+ const plants = line.quantity ?? null;
367
+ const cratesExact = plants !== null ? plants / ppc : null;
368
+ const cratesDisplay = cratesExact === null ? "" :
369
+ Number.isInteger(cratesExact) ? String(cratesExact) : cratesExact.toFixed(2);
370
+ 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) => ({ value: p.id, label: p.product, hint: p.unit })), onChange: (v) => {
135
371
  const next = [...lines];
136
372
  if (v === null)
137
373
  next[idx] = { ...next[idx], product_id: null };
@@ -144,19 +380,21 @@ function ProposalEditor({ proposal, template, customers, products, plantTypes, s
144
380
  };
145
381
  }
146
382
  setLines(next);
147
- } }), _jsx(Input, { className: "mt-1", value: line.raw_product_text, onChange: (e) => {
148
- const next = [...lines];
149
- next[idx] = { ...next[idx], raw_product_text: e.target.value };
150
- setLines(next);
151
- }, placeholder: "Productomschrijving" })] }), _jsx(Input, { className: "col-span-2", type: "number", value: line.quantity ?? "", onChange: (e) => {
152
- const next = [...lines];
153
- next[idx] = { ...next[idx], quantity: e.target.value ? Number(e.target.value) : null };
154
- setLines(next);
155
- }, placeholder: "Aantal" }), _jsx(Input, { className: "col-span-3", value: line.unit ?? "", onChange: (e) => {
156
- const next = [...lines];
157
- next[idx] = { ...next[idx], unit: e.target.value };
158
- setLines(next);
159
- }, 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))), _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"] })] })] })] })] }));
383
+ } }) }), _jsx(Input, { className: "col-span-2", type: "number", min: 0, value: plants ?? "", onChange: (e) => {
384
+ const next = [...lines];
385
+ next[idx] = { ...next[idx], quantity: e.target.value ? Number(e.target.value) : null };
386
+ setLines(next);
387
+ }, placeholder: "Planten" }), _jsx(Input, { className: "col-span-2", type: "number", min: 0, step: "0.01", value: cratesDisplay, onChange: (e) => {
388
+ const next = [...lines];
389
+ const cr = e.target.value ? Number(e.target.value) : null;
390
+ next[idx] = { ...next[idx], quantity: cr === null ? null : Math.round(cr * ppc) };
391
+ setLines(next);
392
+ }, placeholder: "Fusten", title: `${ppc} planten per fust` }), _jsx(Input, { className: "col-span-2", value: line.unit ?? "", onChange: (e) => {
393
+ const next = [...lines];
394
+ next[idx] = { ...next[idx], unit: e.target.value };
395
+ setLines(next);
396
+ }, 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));
397
+ }), _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"] })] })] })] })] }));
160
398
  }
161
399
  function DynamicField({ field, value, meta, onChange, customers, products, plantTypes, suppliers, }) {
162
400
  const reviewClass = meta?.needs_review ? "border-yellow-500 ring-1 ring-yellow-500/40 rounded-md p-2" : "";
@@ -12,7 +12,8 @@ interface SearchableSelectProps {
12
12
  clearLabel?: string;
13
13
  onChange: (value: string | null) => void;
14
14
  className?: string;
15
+ disabled?: boolean;
15
16
  }
16
- export declare function SearchableSelect({ value, options, placeholder, emptyLabel, allowClear, clearLabel, onChange, className, }: SearchableSelectProps): import("react").JSX.Element;
17
+ export declare function SearchableSelect({ value, options, placeholder, emptyLabel, allowClear, clearLabel, onChange, className, disabled, }: SearchableSelectProps): import("react").JSX.Element;
17
18
  export {};
18
19
  //# sourceMappingURL=SearchableSelect.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"SearchableSelect.d.ts","sourceRoot":"","sources":["../../src/components/SearchableSelect.tsx"],"names":[],"mappings":"AAMA,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,UAAU,qBAAqB;IAC7B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,EAAE,gBAAgB,EAAE,CAAC;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IACzC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,KAAK,EAAE,OAAO,EAAE,WAAuB,EAAE,UAA6B,EACtE,UAAiB,EAAE,UAAiC,EACpD,QAAQ,EAAE,SAAS,GACpB,EAAE,qBAAqB,+BAsFvB"}
1
+ {"version":3,"file":"SearchableSelect.d.ts","sourceRoot":"","sources":["../../src/components/SearchableSelect.tsx"],"names":[],"mappings":"AAMA,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,UAAU,qBAAqB;IAC7B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,EAAE,gBAAgB,EAAE,CAAC;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IACzC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,KAAK,EAAE,OAAO,EAAE,WAAuB,EAAE,UAA6B,EACtE,UAAiB,EAAE,UAAiC,EACpD,QAAQ,EAAE,SAAS,EAAE,QAAgB,GACtC,EAAE,qBAAqB,+BAuFvB"}
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useMemo, useState } from "react";
3
3
  import { Button, Input, Popover, PopoverContent, PopoverTrigger, } from "@flowselections/core";
4
4
  import { Check, ChevronsUpDown, Search } from "lucide-react";
5
- export function SearchableSelect({ value, options, placeholder = "Kies...", emptyLabel = "Geen resultaat", allowClear = true, clearLabel = "— Geen koppeling —", onChange, className, }) {
5
+ export function SearchableSelect({ value, options, placeholder = "Kies...", emptyLabel = "Geen resultaat", allowClear = true, clearLabel = "— Geen koppeling —", onChange, className, disabled = false, }) {
6
6
  const [open, setOpen] = useState(false);
7
7
  const [q, setQ] = useState("");
8
8
  const selected = useMemo(() => options.find((o) => o.value === value) ?? null, [options, value]);
@@ -15,6 +15,7 @@ export function SearchableSelect({ value, options, placeholder = "Kies...", empt
15
15
  (o.hint ?? "").toLowerCase().includes(needle))
16
16
  .slice(0, 200);
17
17
  }, [options, q]);
18
- return (_jsxs(Popover, { open: open, onOpenChange: (o) => { setOpen(o); if (!o)
19
- setQ(""); }, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "outline", role: "combobox", "aria-expanded": open, className: `w-full justify-between font-normal ${className ?? ""}`, children: [_jsx("span", { className: `truncate ${selected ? "" : "text-muted-foreground"}`, children: selected?.label ?? placeholder }), _jsx(ChevronsUpDown, { className: "ml-2 h-4 w-4 shrink-0 opacity-50" })] }) }), _jsxs(PopoverContent, { className: "w-[--radix-popover-trigger-width] p-0", align: "start", children: [_jsx("div", { className: "p-2 border-b", children: _jsxs("div", { className: "relative", children: [_jsx(Search, { className: "absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" }), _jsx(Input, { autoFocus: true, value: q, onChange: (e) => setQ(e.target.value), placeholder: `Zoek in ${options.length} items...`, className: "pl-8 h-9" })] }) }), _jsxs("div", { className: "max-h-64 overflow-y-auto py-1", children: [allowClear && (_jsxs("button", { type: "button", className: "w-full text-left text-sm px-3 py-2 hover:bg-accent flex items-center gap-2", onClick: () => { onChange(null); setOpen(false); }, children: [value === null && _jsx(Check, { className: "h-3 w-3" }), _jsx("span", { className: "text-muted-foreground italic", children: clearLabel })] })), filtered.length === 0 ? (_jsx("div", { className: "px-3 py-6 text-sm text-muted-foreground text-center", children: emptyLabel })) : (filtered.map((o) => (_jsxs("button", { type: "button", className: "w-full text-left text-sm px-3 py-2 hover:bg-accent flex items-start gap-2", onClick: () => { onChange(o.value); setOpen(false); }, children: [_jsx(Check, { className: `h-3 w-3 mt-1 shrink-0 ${value === o.value ? "" : "invisible"}` }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("div", { className: "truncate", children: o.label }), o.hint && _jsx("div", { className: "text-xs text-muted-foreground truncate", children: o.hint })] })] }, o.value)))), filtered.length === 200 && (_jsx("div", { className: "px-3 py-2 text-xs text-muted-foreground text-center border-t", children: "Toon eerste 200 resultaten \u2014 verfijn je zoekopdracht" }))] })] })] }));
18
+ return (_jsxs(Popover, { open: open, onOpenChange: (o) => { if (disabled)
19
+ return; setOpen(o); if (!o)
20
+ setQ(""); }, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "outline", role: "combobox", "aria-expanded": open, disabled: disabled, className: `w-full justify-between font-normal ${className ?? ""}`, children: [_jsx("span", { className: `truncate ${selected ? "" : "text-muted-foreground"}`, children: selected?.label ?? placeholder }), _jsx(ChevronsUpDown, { className: "ml-2 h-4 w-4 shrink-0 opacity-50" })] }) }), _jsxs(PopoverContent, { className: "w-[--radix-popover-trigger-width] p-0", align: "start", children: [_jsx("div", { className: "p-2 border-b", children: _jsxs("div", { className: "relative", children: [_jsx(Search, { className: "absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" }), _jsx(Input, { autoFocus: true, value: q, onChange: (e) => setQ(e.target.value), placeholder: `Zoek in ${options.length} items...`, className: "pl-8 h-9" })] }) }), _jsxs("div", { className: "max-h-64 overflow-y-auto py-1", children: [allowClear && (_jsxs("button", { type: "button", className: "w-full text-left text-sm px-3 py-2 hover:bg-accent flex items-center gap-2", onClick: () => { onChange(null); setOpen(false); }, children: [value === null && _jsx(Check, { className: "h-3 w-3" }), _jsx("span", { className: "text-muted-foreground italic", children: clearLabel })] })), filtered.length === 0 ? (_jsx("div", { className: "px-3 py-6 text-sm text-muted-foreground text-center", children: emptyLabel })) : (filtered.map((o) => (_jsxs("button", { type: "button", className: "w-full text-left text-sm px-3 py-2 hover:bg-accent flex items-start gap-2", onClick: () => { onChange(o.value); setOpen(false); }, children: [_jsx(Check, { className: `h-3 w-3 mt-1 shrink-0 ${value === o.value ? "" : "invisible"}` }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("div", { className: "truncate", children: o.label }), o.hint && _jsx("div", { className: "text-xs text-muted-foreground truncate", children: o.hint })] })] }, o.value)))), filtered.length === 200 && (_jsx("div", { className: "px-3 py-2 text-xs text-muted-foreground text-center border-t", children: "Toon eerste 200 resultaten \u2014 verfijn je zoekopdracht" }))] })] })] }));
20
21
  }
@@ -1,6 +1,5 @@
1
1
  import type { FlowModule } from "@flowselections/core";
2
2
  export * from "./_core-safelist";
3
- export { MailboxInboxPage } from "./components/MailboxInboxPage";
4
3
  export { MailboxProposalsPage } from "./components/MailboxProposalsPage";
5
4
  export { SearchableSelect } from "./components/SearchableSelect";
6
5
  export { MailboxProcessedPage } from "./components/MailboxProcessedPage";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAKvD,cAAc,kBAAkB,CAAC;AACjC,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACjE,OAAO,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AACzE,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACjE,OAAO,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AAEzE,eAAO,MAAM,mBAAmB,EAAE,UAoBjC,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAGvD,cAAc,kBAAkB,CAAC;AACjC,OAAO,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AACzE,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACjE,OAAO,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AAEzE,eAAO,MAAM,mBAAmB,EAAE,UAkBjC,CAAC"}
package/dist-lib/index.js CHANGED
@@ -1,30 +1,26 @@
1
1
  // ============================================================================
2
2
  // src/index.ts — MODULE CONTRACT (Mailbox bestellingen)
3
3
  // ============================================================================
4
- import { Mail, Inbox, ClipboardCheck, CheckCircle2 } from "lucide-react";
5
- import { ImapAccountsCard } from "./components/settings/ImapAccountsCard";
4
+ import { Mail, ClipboardCheck, CheckCircle2 } from "lucide-react";
6
5
  import { MailboxOrderTemplateCard } from "./components/settings/MailboxOrderTemplateCard";
7
6
  export * from "./_core-safelist";
8
- export { MailboxInboxPage } from "./components/MailboxInboxPage";
9
7
  export { MailboxProposalsPage } from "./components/MailboxProposalsPage";
10
8
  export { SearchableSelect } from "./components/SearchableSelect";
11
9
  export { MailboxProcessedPage } from "./components/MailboxProcessedPage";
12
10
  export const mailboxOrdersModule = {
13
11
  id: "mailbox_orders",
14
12
  name: "Mailbox bestellingen",
15
- version: "1.1.0",
13
+ version: "1.2.0",
16
14
  nav: {
17
15
  label: "Mailbox",
18
- href: "/mailbox-orders",
16
+ href: "/mailbox-orders/voorstellen",
19
17
  icon: Mail,
20
18
  children: [
21
- { label: "Inbox", href: "/mailbox-orders", icon: Inbox },
22
19
  { label: "Voorstellen", href: "/mailbox-orders/voorstellen", icon: ClipboardCheck },
23
20
  { label: "Verwerkt", href: "/mailbox-orders/verwerkt", icon: CheckCircle2 },
24
21
  ],
25
22
  },
26
23
  settingsCards: [
27
- { component: ImapAccountsCard, order: 5 },
28
24
  { component: MailboxOrderTemplateCard, order: 20 },
29
25
  ],
30
26
  };
@@ -1 +1 @@
1
- {"version":3,"file":"auth-attacher.d.ts","sourceRoot":"","sources":["../../../src/integrations/supabase/auth-attacher.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,kBAAkB,mHAa9B,CAAA"}
1
+ {"version":3,"file":"auth-attacher.d.ts","sourceRoot":"","sources":["../../../src/integrations/supabase/auth-attacher.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,kBAAkB,mHAyC9B,CAAA"}
@@ -9,7 +9,41 @@ export const attachSupabaseAuth = createMiddleware({ type: 'function' }).client(
9
9
  const { supabase } = await import('./client');
10
10
  const { data } = await supabase.auth.getSession();
11
11
  const token = data.session?.access_token;
12
- return next({
13
- headers: token ? { Authorization: `Bearer ${token}` } : {},
14
- });
12
+ const redirectToLogin = async () => {
13
+ try {
14
+ await supabase.auth.signOut();
15
+ }
16
+ catch { /* noop */ }
17
+ try {
18
+ window.localStorage.clear();
19
+ }
20
+ catch { /* noop */ }
21
+ if (!window.location.pathname.startsWith('/login')) {
22
+ window.location.replace('/login');
23
+ }
24
+ };
25
+ try {
26
+ return await next({
27
+ headers: token ? { Authorization: `Bearer ${token}` } : {},
28
+ });
29
+ }
30
+ catch (err) {
31
+ // TanStack geeft door auth-middleware gegooide Response-objecten soms als
32
+ // raw Response terug aan de browser. Als we die opnieuw gooien, toont de
33
+ // runtime overlay alleen `[object Response]`. Zet ze daarom om naar óf een
34
+ // stille login-redirect (401) óf een normale Error met leesbare tekst.
35
+ if (err instanceof Response) {
36
+ if (err.status === 401) {
37
+ await redirectToLogin();
38
+ throw new Error('Sessie verlopen — log opnieuw in.');
39
+ }
40
+ let message = '';
41
+ try {
42
+ message = await err.clone().text();
43
+ }
44
+ catch { /* noop */ }
45
+ throw new Error(message || `Serverfout (${err.status})`);
46
+ }
47
+ throw err;
48
+ }
15
49
  });