@flowselections/mailbox-orders 1.0.30 → 1.0.32

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 (39) hide show
  1. package/dist-lib/components/MailboxProcessedPage.js +1 -1
  2. package/dist-lib/components/MailboxProposalsPage.d.ts.map +1 -1
  3. package/dist-lib/components/MailboxProposalsPage.js +202 -122
  4. package/dist-lib/components/MailboxSkippedPage.d.ts +2 -0
  5. package/dist-lib/components/MailboxSkippedPage.d.ts.map +1 -0
  6. package/dist-lib/components/MailboxSkippedPage.js +71 -0
  7. package/dist-lib/components/settings/MailboxFilterSettingsCard.d.ts +2 -0
  8. package/dist-lib/components/settings/MailboxFilterSettingsCard.d.ts.map +1 -0
  9. package/dist-lib/components/settings/MailboxFilterSettingsCard.js +78 -0
  10. package/dist-lib/components/settings/MailboxTestSetCard.d.ts +2 -0
  11. package/dist-lib/components/settings/MailboxTestSetCard.d.ts.map +1 -0
  12. package/dist-lib/components/settings/MailboxTestSetCard.js +69 -0
  13. package/dist-lib/index.d.ts +1 -0
  14. package/dist-lib/index.d.ts.map +1 -1
  15. package/dist-lib/index.js +8 -2
  16. package/dist-lib/integrations/supabase/types.d.ts +864 -1
  17. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  18. package/dist-lib/lib/mailbox-ai.server.d.ts +33 -12
  19. package/dist-lib/lib/mailbox-ai.server.d.ts.map +1 -1
  20. package/dist-lib/lib/mailbox-ai.server.js +47 -17
  21. package/dist-lib/lib/mailbox-filter-settings.functions.d.ts +143 -0
  22. package/dist-lib/lib/mailbox-filter-settings.functions.d.ts.map +1 -0
  23. package/dist-lib/lib/mailbox-filter-settings.functions.js +241 -0
  24. package/dist-lib/lib/mailbox-filter.d.ts +34 -0
  25. package/dist-lib/lib/mailbox-filter.d.ts.map +1 -0
  26. package/dist-lib/lib/mailbox-filter.js +121 -0
  27. package/dist-lib/lib/mailbox-ingest.server.d.ts +114 -17
  28. package/dist-lib/lib/mailbox-ingest.server.d.ts.map +1 -1
  29. package/dist-lib/lib/mailbox-ingest.server.js +594 -266
  30. package/dist-lib/lib/mailbox-match.d.ts +28 -0
  31. package/dist-lib/lib/mailbox-match.d.ts.map +1 -0
  32. package/dist-lib/lib/mailbox-match.js +74 -0
  33. package/dist-lib/lib/mailbox-units.d.ts +18 -0
  34. package/dist-lib/lib/mailbox-units.d.ts.map +1 -0
  35. package/dist-lib/lib/mailbox-units.js +80 -0
  36. package/dist-lib/lib/mailbox.functions.d.ts +74 -0
  37. package/dist-lib/lib/mailbox.functions.d.ts.map +1 -1
  38. package/dist-lib/lib/mailbox.functions.js +170 -25
  39. package/package.json +6 -1
@@ -38,7 +38,7 @@ export function MailboxProcessedPage() {
38
38
  const [period, setPeriod] = usePersistedState("mailbox-processed:period", "all");
39
39
  const [sortOrder, setSortOrder] = usePersistedState("mailbox-processed:sort", "date-desc");
40
40
  const [statusFilter, setStatusFilter] = usePersistedState("mailbox-processed:status", "all");
41
- const customerNameFor = (p) => p.mailbox_messages?.from_name || p.mailbox_messages?.from_email || "Onbekend";
41
+ const customerNameFor = (p) => p.customer_name || p.mailbox_messages?.from_name || p.mailbox_messages?.from_email || "Onbekend";
42
42
  const filteredItems = useMemo(() => {
43
43
  const q = normalizeText(search).trim();
44
44
  const filtered = items.filter((p) => {
@@ -1 +1 @@
1
- {"version":3,"file":"MailboxProposalsPage.d.ts","sourceRoot":"","sources":["../../src/components/MailboxProposalsPage.tsx"],"names":[],"mappings":"AA6FA,6EAA6E;AAC7E,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;CACtB;AA2CD,wBAAgB,oBAAoB,gCA8bnC"}
1
+ {"version":3,"file":"MailboxProposalsPage.d.ts","sourceRoot":"","sources":["../../src/components/MailboxProposalsPage.tsx"],"names":[],"mappings":"AA6GA,6EAA6E;AAC7E,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;CACtB;AA2CD,wBAAgB,oBAAoB,gCAubnC"}
@@ -4,8 +4,8 @@ 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 { computeEarliestDelivery, isValidDeliveryDate } from "../lib/delivery-date.js";
8
- import { listProposals, getProposalDetail, approveProposal, rejectProposal, listCustomersForPicker, createCustomerFromGroup, listProductsForPicker, listDeliveryLocationsForCustomer, listPickupLocations, listCarriersForPicker, getOrderPickDays, getActiveOrdersMailbox, syncMailboxNow, refreshProposalsForAgentChange, reparseProposalNow, listBeladingOptionsForProducts, listAllowedLoadCarriers, } from "../lib/mailbox.functions.js";
7
+ import { listProposals, getProposalDetail, approveProposal, rejectProposal, listCustomersForPicker, createCustomerFromGroup, listProductsForPicker, listDeliveryLocationsForCustomer, listPickupLocations, listCarriersForPicker, getActiveOrdersMailbox, syncMailboxNow, refreshProposalsForAgentChange, reparseProposalNow, listBeladingOptionsForProducts, listAllowedLoadCarriers, getDeparturePreview, getAgreementPrices, countOutdatedProposals, } from "../lib/mailbox.functions.js";
8
+ import { normalizeUnitKind, toPlants, describeConversion, lineTotal, UNIT_KIND_LABEL, } from "../lib/mailbox-units.js";
9
9
  import { enabledLoadCarriers, normalizeAllowedLoadCarriers, EMPTY_LOAD_CARRIERS } from "../lib/load-carriers.js";
10
10
  import { listTemplateFields, listPlantTypesForPicker, listSuppliersForPicker, } from "../lib/mailbox-template.functions.js";
11
11
  import { SearchableSelect } from "./SearchableSelect.js";
@@ -108,9 +108,7 @@ export function MailboxProposalsPage() {
108
108
  const [search, setSearch] = usePersistedState("mailbox-proposals:search", "");
109
109
  const [period, setPeriod] = usePersistedState("mailbox-proposals:period", "all");
110
110
  const [sortOrder, setSortOrder] = usePersistedState("mailbox-proposals:sort", "date-desc");
111
- const [refsEnabled, setRefsEnabled] = useState(false);
112
- useEffect(() => { if (activeId)
113
- setRefsEnabled(true); }, [activeId]);
111
+ const refsEnabled = true;
114
112
  const refOpts = {
115
113
  enabled: refsEnabled,
116
114
  staleTime: 5 * 60000,
@@ -185,27 +183,31 @@ export function MailboxProposalsPage() {
185
183
  event: "INSERT", schema: "public", table: "email_auth_events",
186
184
  filter: `connection_id=eq.${activeMailboxId}`,
187
185
  }, () => {
188
- syncMut.mutate("realtime");
186
+ // Geen eigen verwerkingsrun per open tabblad: de cron verwerkt de mail.
187
+ // We verversen alleen de lijst, kort nadat de verwerking klaar is.
188
+ setTimeout(() => qc.invalidateQueries({ queryKey: ["mailbox-proposals"] }), 20000);
189
+ setTimeout(() => qc.invalidateQueries({ queryKey: ["mailbox-proposals"] }), 70000);
189
190
  })
190
191
  .subscribe();
191
192
  return () => { void supabase.removeChannel(channel); };
192
193
  // eslint-disable-next-line react-hooks/exhaustive-deps
193
194
  }, [activeMailboxId]);
194
- // Bij eerste keer openen + elke 5 min extra vangnet naast pg_cron/realtime.
195
- const didInitialBackfill = useRef(false);
196
- useEffect(() => {
197
- if (!activeMailboxId || didInitialBackfill.current)
198
- return;
199
- didInitialBackfill.current = true;
200
- syncMut.mutate("manual");
201
- const t = setInterval(() => syncMut.mutate("manual"), 300000);
202
- return () => clearInterval(t);
203
- // eslint-disable-next-line react-hooks/exhaustive-deps
204
- }, [activeMailboxId]);
205
- // AI-agent gewijzigd? Dan worden openstaande voorstellen automatisch opnieuw
206
- // geanalyseerd met de nieuwste agent. Server-side draait dit ook elke
207
- // cron-run; hier reageren we direct op wijzigingen in de AI-module.
195
+ void syncMut;
196
+ // AI-agent gewijzigd? Niet automatisch opnieuw analyseren (kost AI-analyses):
197
+ // we tonen hoeveel voorstellen het betreft en een knop.
208
198
  const fetchAgentRefresh = useServerFn(refreshProposalsForAgentChange);
199
+ const fetchOutdated = useServerFn(countOutdatedProposals);
200
+ const { data: outdatedData, refetch: refetchOutdated } = useQuery({
201
+ queryKey: ["mailbox-outdated-count"],
202
+ queryFn: async () => { try {
203
+ return await fetchOutdated();
204
+ }
205
+ catch {
206
+ return { count: 0 };
207
+ } },
208
+ refetchInterval: 300000,
209
+ });
210
+ const outdatedCount = Number(outdatedData?.count ?? 0);
209
211
  const agentRefreshMut = useMutation({
210
212
  mutationFn: async () => {
211
213
  try {
@@ -216,10 +218,9 @@ export function MailboxProposalsPage() {
216
218
  }
217
219
  },
218
220
  onSuccess: (res) => {
219
- if ((res?.reparsed ?? 0) > 0) {
220
- qc.invalidateQueries({ queryKey: ["mailbox-proposals"] });
221
- toast.success(`${res.reparsed} voorstel(len) opnieuw geanalyseerd met de bijgewerkte AI-agent`);
222
- }
221
+ qc.invalidateQueries({ queryKey: ["mailbox-proposals"] });
222
+ void refetchOutdated();
223
+ toast.success(`${res?.reparsed ?? 0} voorstel(len) opnieuw geanalyseerd`);
223
224
  },
224
225
  });
225
226
  // Handmatig één voorstel opnieuw laten analyseren door de AI-agent.
@@ -249,22 +250,6 @@ export function MailboxProposalsPage() {
249
250
  toast.error(msg);
250
251
  },
251
252
  });
252
- const didAgentCheck = useRef(false);
253
- useEffect(() => {
254
- if (didAgentCheck.current)
255
- return;
256
- didAgentCheck.current = true;
257
- agentRefreshMut.mutate();
258
- const channel = supabase
259
- .channel("ai-agent-changes")
260
- .on("postgres_changes", { event: "*", schema: "public", table: "ai_module_bindings" }, () => agentRefreshMut.mutate())
261
- .on("postgres_changes", { event: "*", schema: "public", table: "ai_models" }, () => agentRefreshMut.mutate())
262
- .on("postgres_changes", { event: "*", schema: "public", table: "ai_credentials" }, () => agentRefreshMut.mutate())
263
- .subscribe();
264
- const t = setInterval(() => agentRefreshMut.mutate(), 120000);
265
- return () => { clearInterval(t); void supabase.removeChannel(channel); };
266
- // eslint-disable-next-line react-hooks/exhaustive-deps
267
- }, []);
268
253
  const items = Array.isArray(data?.items) ? data.items : [];
269
254
  const template = Array.isArray(templateData?.items) ? templateData.items : [];
270
255
  const customers = Array.isArray(customersData?.items) ? customersData.items : [];
@@ -280,6 +265,8 @@ export function MailboxProposalsPage() {
280
265
  const statusLabel = (_s) => "Te controleren";
281
266
  const statusVariant = (_s) => "secondary";
282
267
  const customerNameFor = (p) => {
268
+ if (typeof p?.customer_name === "string" && p.customer_name)
269
+ return p.customer_name;
283
270
  const fvs = Array.isArray(p.mailbox_proposal_field_values) ? p.mailbox_proposal_field_values : [];
284
271
  const fv = fvs.find((v) => v.field_key === "customer");
285
272
  const id = resolveCustomerId(fv?.value, customers, p.matched_customer_id);
@@ -320,11 +307,9 @@ export function MailboxProposalsPage() {
320
307
  const pageCount = Math.max(1, Math.ceil(filteredItems.length / pageSize));
321
308
  const currentPage = Math.min(page, pageCount);
322
309
  const pagedItems = filteredItems.slice((currentPage - 1) * pageSize, currentPage * pageSize);
323
- return (_jsxs("div", { className: "flex h-[calc(100dvh-var(--app-header-height)-var(--core-topbar-h,0px))] max-h-[calc(100dvh-var(--app-header-height)-var(--core-topbar-h,0px))] min-h-0 w-full flex-col gap-4 overflow-hidden p-6", children: [_jsx(MailboxAuthBanner, { active: activeMailbox }), _jsx("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." })] }) }), items.length > 0 && (_jsx(MailboxFilterBar, { search: search, onSearchChange: setSearch, period: period, onPeriodChange: setPeriod, sortOrder: sortOrder, onSortOrderChange: setSortOrder, filteredCount: filteredItems.length, totalCount: items.length, countLabel: "voorstellen", onClear: clearFilters, hasActiveFilters: hasActiveFilters })), _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." })) : filteredItems.length === 0 ? (_jsx("p", { className: "text-muted-foreground", children: "Geen voorstellen gevonden voor deze zoekopdracht." })) : (_jsx(Card, { children: _jsxs(CardContent, { className: "p-0", children: [_jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Klantnaam" }), _jsx(TableHead, { children: "Bericht" }), _jsx(TableHead, { children: "Orderdatum" }), _jsx(TableHead, { children: "Status" })] }) }), _jsx(TableBody, { children: pagedItems.map((p) => {
324
- const fvs = Array.isArray(p.mailbox_proposal_field_values) ? p.mailbox_proposal_field_values : [];
325
- const needsReview = fvs.some((v) => v.needs_review);
310
+ return (_jsxs("div", { className: "flex h-[calc(100dvh-var(--app-header-height)-var(--core-topbar-h,0px))] max-h-[calc(100dvh-var(--app-header-height)-var(--core-topbar-h,0px))] min-h-0 w-full flex-col gap-4 overflow-hidden p-6", children: [_jsx(MailboxAuthBanner, { active: activeMailbox }), _jsx("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." })] }) }), outdatedCount > 0 && (_jsxs("div", { className: "shrink-0 flex items-center justify-between gap-3 rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-800", children: [_jsxs("span", { children: ["De AI-instelling is gewijzigd. ", outdatedCount, " voorstel(len) zijn nog met de vorige instelling gelezen."] }), _jsx(Button, { size: "sm", variant: "outline", disabled: agentRefreshMut.isPending, onClick: () => agentRefreshMut.mutate(), children: agentRefreshMut.isPending ? "Bezig…" : "Opnieuw analyseren" })] })), items.length > 0 && (_jsx(MailboxFilterBar, { search: search, onSearchChange: setSearch, period: period, onPeriodChange: setPeriod, sortOrder: sortOrder, onSortOrderChange: setSortOrder, filteredCount: filteredItems.length, totalCount: items.length, countLabel: "voorstellen", onClear: clearFilters, hasActiveFilters: hasActiveFilters })), _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." })) : filteredItems.length === 0 ? (_jsx("p", { className: "text-muted-foreground", children: "Geen voorstellen gevonden voor deze zoekopdracht." })) : (_jsx(Card, { children: _jsxs(CardContent, { className: "p-0", children: [_jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Klantnaam" }), _jsx(TableHead, { children: "Bericht" }), _jsx(TableHead, { children: "Orderdatum" }), _jsx(TableHead, { children: "Status" })] }) }), _jsx(TableBody, { children: pagedItems.map((p) => {
326
311
  const date = p.mailbox_messages?.received_at ?? p.created_at;
327
- return (_jsxs(TableRow, { className: "cursor-pointer hover:bg-muted/50", onClick: () => setActiveId(p.id), children: [_jsx(TableCell, { className: "font-medium", children: customerNameFor(p) }), _jsxs(TableCell, { className: "max-w-sm", children: [_jsx("div", { className: "truncate text-sm font-medium", children: p.mailbox_messages?.subject || "(geen onderwerp)" }), _jsx("div", { className: "truncate text-xs text-muted-foreground", children: p.mailbox_messages?.preview || "Geen voorbeeldtekst" })] }), _jsx(TableCell, { className: "text-sm text-muted-foreground", children: date ? new Date(date).toLocaleDateString("nl-NL") : "-" }), _jsx(TableCell, { children: _jsx(Badge, { variant: "secondary", children: "Te controleren" }) })] }, p.id));
312
+ return (_jsxs(TableRow, { className: "cursor-pointer hover:bg-muted/50", onClick: () => setActiveId(p.id), children: [_jsx(TableCell, { className: "font-medium", children: customerNameFor(p) }), _jsxs(TableCell, { className: "max-w-sm", children: [_jsx("div", { className: "truncate text-sm font-medium", children: p.mailbox_messages?.subject || "(geen onderwerp)" }), _jsx("div", { className: "truncate text-xs text-muted-foreground", children: p.mailbox_messages?.preview || "Geen voorbeeldtekst" })] }), _jsx(TableCell, { className: "text-sm text-muted-foreground", children: date ? new Date(date).toLocaleDateString("nl-NL") : "-" }), _jsx(TableCell, { children: _jsxs("div", { className: "flex flex-wrap gap-1", children: [p.readiness === "klaar" ? (_jsx(Badge, { variant: "outline", className: "border-emerald-300 bg-emerald-50 text-emerald-800", children: "Klaar om te bevestigen" })) : (_jsx(Badge, { variant: "secondary", children: "Controleren" })), p.duplicate && _jsx(Badge, { variant: "outline", className: "border-amber-300 bg-amber-50 text-amber-800", children: "Mogelijk dubbel" })] }) })] }, p.id));
328
313
  }) })] }), _jsx(TablePagination, { total: filteredItems.length, page: currentPage, pageSize: pageSize, onPageChange: setPage, onPageSizeChange: setPageSize })] }) })) }), _jsx(Dialog, { open: !!activeId, onOpenChange: (o) => { if (!o) {
329
314
  setActiveId(null);
330
315
  setReparseError(null);
@@ -372,9 +357,15 @@ function ProposalEditor({ proposal, template, customers, products, allowBackorde
372
357
  const visibleFields = template.filter((f) => f.visible);
373
358
  const [values, setValues] = useState(() => {
374
359
  const init = {};
360
+ // Alle velden uit de mail overnemen, ook als het template leeg is.
361
+ Object.entries(fvMap).forEach(([k, v]) => { init[k] = v?.value ?? null; });
375
362
  visibleFields.forEach((f) => {
376
363
  init[f.key] = fvMap[f.key]?.value ?? f.default_value ?? null;
377
364
  });
365
+ if (!init["delivery_date"] && proposal.delivery_date)
366
+ init["delivery_date"] = proposal.delivery_date;
367
+ if (!init["reference"] && proposal.customer_reference)
368
+ init["reference"] = proposal.customer_reference;
378
369
  // De AI schrijft soms een klantnaam i.p.v. een id — altijd omzetten naar een
379
370
  // geldig klant-id, met de automatisch herkende klant als terugval.
380
371
  init["customer"] = resolveCustomerId(init["customer"], customers, proposal.matched_customer_id);
@@ -395,18 +386,41 @@ function ProposalEditor({ proposal, template, customers, products, allowBackorde
395
386
  const initialLines = Array.isArray(proposal.mailbox_proposal_lines)
396
387
  ? proposal.mailbox_proposal_lines.map((l) => {
397
388
  const p = products.find((x) => x.id === l.product_id);
398
- const ppc = Math.max(1, Number(p?.plants_per_crate ?? 1));
399
- const q = Number(l.quantity ?? 0);
400
- const normalized = q > 0 ? Math.ceil(q / ppc) * ppc : q;
389
+ const savedBel = l.belading && typeof l.belading === "object" ? l.belading : null;
390
+ const bel = savedBel ?? defaultBelading(p);
391
+ const kind = l.unit_kind ? normalizeUnitKind(l.unit_kind) : null;
392
+ // Nieuw schema: het aantal staat al in planten (omgerekend op de server).
393
+ // Oud schema (zonder eenheid): aantal was planten, afronden op hele fusten.
394
+ let quantity;
395
+ if (kind) {
396
+ quantity = l.plants !== null && l.plants !== undefined ? Number(l.plants) : toPlants(l.qty_in_mail, kind, bel);
397
+ }
398
+ else {
399
+ const ppc = Math.max(1, Number(bel?.plants_per_crate ?? 1));
400
+ const q = Number(l.quantity ?? 0);
401
+ quantity = q > 0 ? Math.ceil(q / ppc) * ppc : null;
402
+ }
403
+ const conf = (l.confidence ?? {});
401
404
  return {
402
405
  id: l.id,
403
406
  product_id: l.product_id,
404
407
  raw_product_text: l.raw_product_text ?? "",
405
408
  plant_type: l.plant_type ?? null,
406
- quantity: normalized || null,
407
- unit: l.unit,
408
- // Geen belading bekend uit de mail → altijd de standaardbelading.
409
- belading: defaultBelading(p),
409
+ quantity: quantity || null,
410
+ unit: "planten",
411
+ belading: bel,
412
+ unit_kind: kind,
413
+ qty_in_mail: l.qty_in_mail ?? null,
414
+ unit_text: l.unit_text ?? null,
415
+ pieces_per_unit: l.pieces_per_unit ?? null,
416
+ unit_price: l.unit_price !== null && l.unit_price !== undefined ? Number(l.unit_price) : null,
417
+ price_source: l.unit_price !== null && l.unit_price !== undefined ? "uit mail" : null,
418
+ mail_price: conf.mail_price ?? null,
419
+ price_per: l.price_per ?? null,
420
+ raw_code: l.raw_code ?? null,
421
+ match_source: l.match_source ?? null,
422
+ product_confidence: typeof conf.product === "number" ? conf.product : (l.match_confidence ?? null),
423
+ warnings: Array.isArray(l.warnings) ? l.warnings : [],
410
424
  };
411
425
  })
412
426
  : [];
@@ -470,9 +484,10 @@ function ProposalEditor({ proposal, template, customers, products, allowBackorde
470
484
  useEffect(() => {
471
485
  if (deliveryLocationVal || deliveryLocations.length === 0)
472
486
  return;
473
- const primary = deliveryLocations.find((l) => l.is_primary === true) ?? deliveryLocations[0];
474
- if (primary?.id)
475
- setVal("delivery_location", primary.id);
487
+ // Alleen de vaste locatie (is_primary) voorselecteren; anders niets kiezen.
488
+ const primary = deliveryLocations.filter((l) => l.is_primary === true);
489
+ if (primary.length === 1 && primary[0]?.id)
490
+ setVal("delivery_location", primary[0].id);
476
491
  // eslint-disable-next-line react-hooks/exhaustive-deps
477
492
  }, [deliveryLocationVal, deliveryLocations.length, customerVal]);
478
493
  // Auto-vul vervoerder zodra een nieuwe locatie wordt gekozen (één keer per wissel).
@@ -488,22 +503,7 @@ function ProposalEditor({ proposal, template, customers, products, allowBackorde
488
503
  }
489
504
  // eslint-disable-next-line react-hooks/exhaustive-deps
490
505
  }, [deliveryLocationVal, deliveryLocations.length]);
491
- // ─── Eerst mogelijke leverdatum ────────────────────────────────────────────
492
- const fetchPickDays = useServerFn(getOrderPickDays);
493
- const { data: pickDaysData } = useQuery({
494
- queryKey: ["mailbox-order-pick-days"],
495
- queryFn: async () => {
496
- try {
497
- return await fetchPickDays();
498
- }
499
- catch {
500
- return { pickDays: 0 };
501
- }
502
- },
503
- });
504
- const pickDays = Math.max(0, Number(pickDaysData?.pickDays ?? 0));
505
506
  const selectedCarrier = carriers.find((c) => c.id === carrierVal) ?? null;
506
- const carrierDeliveryDays = Math.max(0, Number(selectedCarrier?.delivery_days ?? 0));
507
507
  const pickupLocationVal = values["pickup_location"];
508
508
  // ─── Ladingdragers uit de Rule Engine (nooit cachen) ──────────────────────
509
509
  const fetchLoadCarriers = useServerFn(listAllowedLoadCarriers);
@@ -556,34 +556,52 @@ function ProposalEditor({ proposal, template, customers, products, allowBackorde
556
556
  // eslint-disable-next-line react-hooks/exhaustive-deps
557
557
  }, [pickupLocationVal, pickupLocations.length]);
558
558
  const canPickDate = !!customerVal && !!deliveryLocationVal && !!pickupLocationVal && !!carrierVal;
559
- const fmtISO = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
560
- const earliest = useMemo(() => computeEarliestDelivery(selectedCarrier, pickDays), [selectedCarrier, pickDays]);
561
- const earliestDate = earliest.date;
562
- const earliestISO = earliest.iso;
563
- const earliestLabel = earliestDate.toLocaleDateString("nl-NL", {
564
- day: "numeric", month: "long", year: "numeric",
559
+ // ─── Leverdatum: gevraagde datum is leidend, de Engine toetst ──────────────
560
+ const fetchPreview = useServerFn(getDeparturePreview);
561
+ const { data: previewData, isFetching: previewLoading } = useQuery({
562
+ queryKey: ["mailbox-departure-preview", carrierVal, deliveryLocationVal, deliveryVal],
563
+ enabled: !!carrierVal && !!deliveryLocationVal && !!deliveryVal && /^\d{4}-\d{2}-\d{2}$/.test(deliveryVal ?? ""),
564
+ staleTime: 0,
565
+ queryFn: async () => {
566
+ try {
567
+ return await fetchPreview({ data: { carrierId: carrierVal, deliveryLocationId: deliveryLocationVal, deliveryDate: deliveryVal } });
568
+ }
569
+ catch (e) {
570
+ return { ok: false, error: String(e?.message ?? e), preview: null };
571
+ }
572
+ },
565
573
  });
566
- // Bij wisselen van vervoerder: bezorgdatum altijd opnieuw bepalen.
567
- const lastCarrierRef = useRef(carrierVal);
568
- useEffect(() => {
569
- if (carrierVal === lastCarrierRef.current)
570
- return;
571
- lastCarrierRef.current = carrierVal;
572
- if (!carrierVal)
573
- return;
574
- setVal("delivery_date", earliestISO);
575
- // eslint-disable-next-line react-hooks/exhaustive-deps
576
- }, [carrierVal, earliestISO]);
577
- // Synchroniseer delivery_date wanneer de eerst mogelijke datum verandert of
578
- // de huidige datum niet geldig is voor de gekozen vervoerder.
579
- useEffect(() => {
580
- if (!canPickDate)
581
- return;
582
- if (!deliveryVal || !isValidDeliveryDate(selectedCarrier, deliveryVal, pickDays)) {
583
- setVal("delivery_date", earliestISO);
584
- }
585
- // eslint-disable-next-line react-hooks/exhaustive-deps
586
- }, [canPickDate, earliestISO, deliveryVal, carrierVal]);
574
+ const preview = (previewData?.ok ? previewData.preview : null);
575
+ const previewError = previewData?.ok === false ? String(previewData.error ?? "") : null;
576
+ const dateFeasible = preview?.ok === true && preview?.deliveryDateFeasible === true;
577
+ const fmtDate = (iso) => iso ? new Date(`${iso}T12:00:00`).toLocaleDateString("nl-NL", { weekday: "long", day: "numeric", month: "long", year: "numeric" }) : "";
578
+ const fmtDateTime = (iso) => iso ? new Date(iso).toLocaleString("nl-NL", { weekday: "short", day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" }) : "";
579
+ // ─── Prijsafspraak ter vergelijking ───────────────────────────────────────
580
+ const fetchAgreement = useServerFn(getAgreementPrices);
581
+ const agreementKey = lines.map((l) => `${l.product_id}:${l.quantity}`).join(",");
582
+ const { data: agreementData } = useQuery({
583
+ queryKey: ["mailbox-agreement-prices", customerVal, deliveryVal, agreementKey],
584
+ enabled: isLocalCustomerId(customerVal) && !!deliveryVal && lines.some((l) => l.product_id),
585
+ queryFn: async () => {
586
+ try {
587
+ return await fetchAgreement({
588
+ data: {
589
+ customerId: customerVal,
590
+ onDate: deliveryVal,
591
+ lines: lines.filter((l) => l.product_id).map((l) => ({ productId: l.product_id, quantity: Number(l.quantity ?? 0) })),
592
+ },
593
+ });
594
+ }
595
+ catch {
596
+ return { prices: {} };
597
+ }
598
+ },
599
+ });
600
+ const agreements = agreementData?.prices ?? {};
601
+ const [acceptNoPrice, setAcceptNoPrice] = useState(false);
602
+ const orderTotal = lines.reduce((sum, l) => sum + lineTotal(l.quantity ?? null, l.unit_price ?? null), 0);
603
+ const linesWithoutPrice = lines.filter((l) => l.product_id && (l.unit_price === null || l.unit_price === undefined)).length;
604
+ const euro = (n) => n.toLocaleString("nl-NL", { style: "currency", currency: "EUR" });
587
605
  const [confirmOpen, setConfirmOpen] = useState(false);
588
606
  const [submitting, setSubmitting] = useState(false);
589
607
  const [checkedOk, setCheckedOk] = useState(false);
@@ -611,6 +629,18 @@ function ProposalEditor({ proposal, template, customers, products, allowBackorde
611
629
  toast.error("Voeg minimaal één product toe aan de bestelling.");
612
630
  return null;
613
631
  }
632
+ if (lines.some((l) => l.product_id && l.unit_kind === "onbekend")) {
633
+ toast.error("Kies bij elke regel met een onbekende eenheid of het om fust, laag, kar of stuks gaat.");
634
+ return null;
635
+ }
636
+ if (!carrierVal) {
637
+ toast.error("Kies een vervoerder.");
638
+ return null;
639
+ }
640
+ if (!dateFeasible) {
641
+ toast.error(preview?.reason ? `Leverdatum niet haalbaar: ${preview.reason}` : "De leverdatum is nog niet gecontroleerd of niet haalbaar.");
642
+ return null;
643
+ }
614
644
  for (const l of validLines) {
615
645
  const p = products.find((x) => x.id === l.product_id);
616
646
  const b = l.belading ?? defaultBelading(p);
@@ -626,10 +656,25 @@ function ProposalEditor({ proposal, template, customers, products, allowBackorde
626
656
  proposalId: proposal.id,
627
657
  customerId: customerVal ?? null,
628
658
  customerName: cust?.company_name ?? msg.from_name ?? msg.from_email ?? "Onbekend",
629
- deliveryDate: deliveryVal || new Date(Date.now() + 86400000).toISOString().slice(0, 10),
659
+ deliveryDate: deliveryVal,
630
660
  notes: notesVal || null,
631
- lines: validLines,
661
+ lines: validLines.map((l) => {
662
+ const p = products.find((x) => x.id === l.product_id);
663
+ const b = l.belading ?? defaultBelading(p);
664
+ return {
665
+ id: l.id,
666
+ product_id: l.product_id,
667
+ raw_product_text: l.raw_product_text,
668
+ quantity: l.quantity,
669
+ unit: "planten",
670
+ unit_price: l.unit_price ?? null,
671
+ belading: b ? { plants_per_crate: b.plants_per_crate, crates_per_layer: b.crates_per_layer, layers_per_cart: b.layers_per_cart } : null,
672
+ };
673
+ }),
632
674
  fieldValues: Object.entries(values).map(([field_key, value]) => ({ field_key, value })),
675
+ customerReference: typeof values["reference"] === "string" ? values["reference"].trim() || null : null,
676
+ momentKey: preview?.chosen?.momentKey ?? null,
677
+ acceptMissingPrice: acceptNoPrice,
633
678
  };
634
679
  };
635
680
  // Stap 1: controleren → bevestigingsdialoog. Pas na bevestigen wordt de
@@ -663,16 +708,25 @@ function ProposalEditor({ proposal, template, customers, products, allowBackorde
663
708
  visible: true,
664
709
  };
665
710
  const deliveryDateField = visibleFields.find((f) => f.key === "delivery_date");
666
- const otherFields = visibleFields.filter((f) => f.key !== "customer" && f.key !== "delivery_date");
711
+ const otherFields = visibleFields.filter((f) => f.key !== "customer" && f.key !== "delivery_date" && f.key !== "reference");
667
712
  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));
668
713
  const customerNotLocal = !!customerVal && !isLocalCustomerId(customerVal);
669
714
  const payload = (proposal.parsed_payload ?? {});
670
715
  const autoMatched = payload.auto_matched_customer === true;
671
716
  const matchWarning = typeof payload.auto_match_warning === "string" ? payload.auto_match_warning : null;
672
717
  const ai = (payload.ai ?? null);
673
- 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
718
+ const fieldMeta = (payload.field_meta ?? {});
719
+ const thresholds = { green: Number(payload.thresholds?.green ?? 0.8), orange: Number(payload.thresholds?.orange ?? 0.5) };
720
+ const validation = (proposal.ai_validation ?? payload.validation ?? null);
721
+ const missingList = Array.isArray(validation?.missing) ? validation.missing : [];
722
+ const uncertainList = Array.isArray(validation?.uncertain) ? validation.uncertain : [];
723
+ const dup = payload.duplicate_of;
724
+ const metaFor = (key) => fieldMeta[key] ?? (key === "customer" && proposal.matched_customer_id
725
+ ? { confidence: payload.auto_match_warning ? 0.6 : 1, source: payload.auto_match_forwarded ? "afgeleid" : "uit mail" }
726
+ : null);
727
+ return (_jsxs(_Fragment, { children: [dup && (_jsxs("div", { className: "rounded-md border border-amber-300 bg-amber-50 px-3 py-2 text-xs text-amber-800", children: [_jsx("strong", { children: "Mogelijk dubbel:" }), " ", dup.kind === "referentie" ? "dezelfde referentie" : "vrijwel dezelfde regels", " als ", dup.label, "."] })), payload.attachment_unread && (_jsx("div", { className: "rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-800", children: "Deze mail heeft een bijlage. De bijlage is niet automatisch gelezen; controleer hem zelf." })), (missingList.length > 0 || uncertainList.length > 0) && (_jsxs("div", { className: "grid gap-2 sm:grid-cols-2", children: [missingList.length > 0 && (_jsxs("div", { className: "rounded-md border border-destructive/40 bg-destructive/5 px-3 py-2 text-xs", children: [_jsx("div", { className: "font-medium text-destructive mb-1", children: "Ontbreekt" }), _jsx("ul", { className: "list-disc pl-4 space-y-0.5", children: missingList.map((m) => _jsx("li", { children: m }, m)) })] })), uncertainList.length > 0 && (_jsxs("div", { className: "rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-800", children: [_jsx("div", { className: "font-medium mb-1", children: "Twijfel" }), _jsx("ul", { className: "list-disc pl-4 space-y-0.5", children: uncertainList.map((m) => _jsx("li", { children: m }, m)) })] }))] })), 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
674
728
  ? "De AI-dienst was even bezet — we proberen deze mail automatisch opnieuw. Je kunt de velden intussen zelf invullen."
675
- : "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
729
+ : "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), _jsx(ConfBadge, { meta: metaFor("customer"), thresholds: thresholds }), 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
676
730
  ? "Selecteer eerst een klant"
677
731
  : deliveryLocations.length
678
732
  ? "Kies locatie..."
@@ -687,25 +741,15 @@ function ProposalEditor({ proposal, template, customers, products, allowBackorde
687
741
  label: (l.name || addr || "Locatie") + (l.is_primary ? " ★" : ""),
688
742
  hint: l.name && addr ? addr : undefined,
689
743
  };
690
- }), 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
744
+ }), onChange: (v) => setVal("delivery_location", v) }), _jsx(ConfBadge, { meta: metaFor("delivery_location"), thresholds: thresholds })] }), 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
691
745
  ? "Selecteer eerst een klant"
692
746
  : !deliveryLocationVal
693
747
  ? "Selecteer eerst een locatie"
694
748
  : "Kies vervoerder...", clearLabel: "\u2014 Geen \u2014", disabled: !deliveryLocationVal, options: carriers.map((c) => ({
695
749
  value: c.id,
696
750
  label: c.name + (c.is_default ? " (standaard)" : ""),
697
- })), 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) => {
698
- const v = e.target.value || null;
699
- if (v && v < earliestISO) {
700
- toast.error(`Eerst mogelijke leverdatum is ${earliestLabel}.`);
701
- setVal("delivery_date", earliestISO);
702
- return;
703
- }
704
- setVal("delivery_date", v);
705
- } }), !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
706
- ? `, 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" })})`
707
- : `, op basis van orderpicktijd (${pickDays}d) en levertijd vervoerder (${carrierDeliveryDays}d)`, "."] }))] }), otherFields.map(renderField)] }));
708
- })(), _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) => {
751
+ })), 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 ", _jsx("span", { className: "text-destructive", children: "*" })] }), _jsx(Input, { type: "date", value: deliveryVal ?? "", onChange: (e) => setVal("delivery_date", e.target.value || null) }), _jsx(ConfBadge, { meta: metaFor("delivery_date"), thresholds: thresholds }), !carrierVal || !deliveryLocationVal ? (_jsx("div", { className: "text-xs text-muted-foreground", children: "Kies een afleverlocatie en vervoerder; daarna controleren we of deze datum haalbaar is." })) : !deliveryVal ? (_jsx("div", { className: "text-xs text-muted-foreground", children: "De mail noemt geen datum. Kies zelf een datum." })) : previewLoading ? (_jsx("div", { className: "text-xs text-muted-foreground", children: "Haalbaarheid controleren\u2026" })) : previewError ? (_jsxs("div", { className: "text-xs text-destructive", children: ["De haalbaarheid kon niet worden gecontroleerd: ", previewError] })) : dateFeasible ? (_jsxs("div", { className: "rounded-md border border-emerald-200 bg-emerald-50 px-3 py-2 text-xs text-emerald-800", children: [fmtDate(deliveryVal), " is haalbaar.", preview?.chosen?.departureAt ? ` Ophaalmoment: ${fmtDateTime(preview.chosen.departureAt)}` : "", preview?.chosen?.signupDeadline ? ` (aanmelden tot ${fmtDateTime(preview.chosen.signupDeadline)}).` : "."] })) : (_jsxs("div", { className: "rounded-md border border-destructive/40 bg-destructive/5 px-3 py-2 text-xs space-y-2", children: [_jsxs("div", { className: "text-destructive", children: [fmtDate(deliveryVal), " is niet haalbaar", preview?.reason ? `: ${preview.reason}` : "."] }), preview?.firstFeasibleDeliveryDate && preview.firstFeasibleDeliveryDate !== deliveryVal && (_jsxs(Button, { size: "sm", variant: "outline", onClick: () => setVal("delivery_date", preview.firstFeasibleDeliveryDate), children: ["Gebruik ", fmtDate(preview.firstFeasibleDeliveryDate)] }))] }))] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { children: "Referentie klant" }), _jsx(Input, { value: typeof values["reference"] === "string" ? values["reference"] : "", placeholder: "Bijv. ordernummer van de klant", onChange: (e) => setVal("reference", e.target.value) }), _jsx(ConfBadge, { meta: metaFor("reference"), thresholds: thresholds })] }), otherFields.map(renderField)] }));
752
+ })(), _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-2", children: "Fusten" }), _jsx("div", { className: "col-span-2", children: "Prijs per stuk" }), _jsx("div", { className: "col-span-1" })] }), lines.map((line, idx) => {
709
753
  const product = line.product_id ? products.find((p) => p.id === line.product_id) : null;
710
754
  const stdBelading = defaultBelading(product);
711
755
  const available = (line.product_id ? (Array.isArray(beladingByProduct[line.product_id]) ? beladingByProduct[line.product_id] : []) : []).map((b) => ({
@@ -762,16 +806,19 @@ function ProposalEditor({ proposal, template, customers, products, allowBackorde
762
806
  const newPpc = Math.max(1, Number(b?.plants_per_crate ?? 1));
763
807
  const currentQ = Number(next[idx].quantity ?? 0);
764
808
  const normalizedQ = currentQ > 0 ? Math.ceil(currentQ / newPpc) * newPpc : newPpc;
809
+ const kind = next[idx].unit_kind;
810
+ const fromMail = kind && kind !== "onbekend" ? toPlants(next[idx].qty_in_mail, kind, b) : null;
765
811
  next[idx] = {
766
812
  ...next[idx], product_id: v,
767
- raw_product_text: p?.product ?? next[idx].raw_product_text,
768
- unit: next[idx].unit ?? p?.unit ?? "stuks",
769
- quantity: normalizedQ,
813
+ raw_product_text: next[idx].raw_product_text || p?.product || "",
814
+ unit: "planten",
815
+ quantity: fromMail ?? normalizedQ,
770
816
  belading: b,
817
+ warnings: (next[idx].warnings ?? []).filter((w) => !w.startsWith("Onbekend artikel")),
771
818
  };
772
819
  }
773
820
  setLines(next);
774
- } }), (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"
821
+ } }), line.raw_product_text && (_jsxs("div", { className: "mt-1 text-[11px] text-muted-foreground break-words", children: ["Uit mail: ", line.raw_product_text] })), line.match_source && line.product_id && (_jsxs("div", { className: "text-[11px] text-muted-foreground", children: [line.match_source === "code" ? `Gekoppeld op code ${line.raw_code ?? ""}`.trim() : line.match_source === "naam" ? "Gekoppeld op naam — controleer" : "Gekozen door de AI", typeof line.product_confidence === "number" ? ` · ${Math.round(line.product_confidence * 100)}%` : ""] })), !line.product_id && line.raw_product_text && (_jsx("div", { className: "text-[11px] text-destructive", children: "Onbekend artikel: kies zelf een product." }))] }), _jsxs("div", { className: "col-span-3", children: [_jsx(SearchableSelect, { value: effBelading ? beladingKey(effBelading) : null, placeholder: !line.product_id ? "Kies eerst product"
775
822
  : loadingBeladingen ? "Beladingen laden..."
776
823
  : "Kies belading...", disabled: !line.product_id, allowClear: false, emptyLabel: "Geen belading gekoppeld aan dit product", options: Array.from(optionMap.entries()).map(([key, b]) => ({
777
824
  value: key,
@@ -793,20 +840,41 @@ function ProposalEditor({ proposal, template, customers, products, allowBackorde
793
840
  quantity: crates * b.plants_per_crate,
794
841
  };
795
842
  setLines(next);
796
- } }), 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) => {
843
+ } }), effBelading && (_jsxs("div", { className: "mt-1 text-xs text-muted-foreground", children: [line.unit_kind && line.unit_kind !== "onbekend" && line.qty_in_mail
844
+ ? describeConversion(line.qty_in_mail, line.unit_kind, effBelading) ?? `${plants ?? 0} planten`
845
+ : `${plants ?? 0} planten`, defaultKey && beladingKey(effBelading) !== defaultKey ? " · afwijkend van standaard" : ""] })), line.unit_kind === "onbekend" && (_jsxs("div", { className: "mt-1 space-y-1", children: [_jsxs("div", { className: "text-[11px] text-destructive", children: ["Eenheid onbekend", line.unit_text ? ` ("${line.qty_in_mail ?? ""} ${line.unit_text}")` : "", ". Kies wat er bedoeld wordt:"] }), _jsxs(Select, { value: "__none", onValueChange: (v) => {
846
+ const kind = v;
847
+ const next = [...lines];
848
+ const pl = toPlants(next[idx].qty_in_mail, kind, effBelading);
849
+ next[idx] = { ...next[idx], unit_kind: kind, quantity: pl ?? next[idx].quantity };
850
+ setLines(next);
851
+ }, children: [_jsx(SelectTrigger, { className: "h-8", children: _jsx(SelectValue, { placeholder: "Kies eenheid" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "__none", disabled: true, children: "Kies eenheid" }), ["fust", "laag", "kar", "stuk"].map((k) => (_jsx(SelectItem, { value: k, children: UNIT_KIND_LABEL[k] }, k)))] })] })] })), (line.warnings ?? []).filter((w) => !w.startsWith("Onbekend artikel") && !w.startsWith("Eenheid onbekend")).map((w) => (_jsx("div", { className: "mt-1 text-[11px] text-amber-700", children: w }, w)))] }), _jsx(Input, { className: "col-span-2", type: "number", min: 1, step: 1, inputMode: "numeric", onKeyDown: blockDecimal, value: cratesDisplay, onChange: (e) => {
797
852
  const next = [...lines];
798
853
  const raw = e.target.value.replace(/[.,]/g, "");
799
854
  const cr = raw ? Math.max(1, Math.floor(Number(raw))) : null;
800
855
  next[idx] = { ...next[idx], quantity: cr === null ? null : cr * ppc };
801
856
  setLines(next);
802
- }, 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));
803
- }), _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) => {
857
+ }, placeholder: "Fusten", title: `${ppc} planten per fust — verkoop per hele fust` }), _jsxs("div", { className: "col-span-2 space-y-1", children: [_jsx(Input, { type: "number", min: 0, step: "0.01", inputMode: "decimal", value: line.unit_price ?? "", placeholder: "\u20AC", onChange: (e) => {
858
+ const next = [...lines];
859
+ const v = e.target.value === "" ? null : Math.max(0, Number(e.target.value.replace(",", ".")));
860
+ next[idx] = { ...next[idx], unit_price: v, price_source: v === null ? null : "handmatig" };
861
+ setLines(next);
862
+ } }), (() => {
863
+ const ag = line.product_id ? agreements[line.product_id] : null;
864
+ const src = line.price_source;
865
+ return (_jsxs("div", { className: "text-[11px] text-muted-foreground space-y-0.5", children: [line.unit_price === null || line.unit_price === undefined ? (_jsx("div", { className: "text-amber-700", children: "Geen prijs bekend" })) : (_jsxs("div", { children: [src === "uit mail" ? "Uit mail" : src === "afspraak" ? "Prijsafspraak" : "Zelf ingevuld", line.mail_price && line.price_per === "eenheid" ? ` (${euro(line.mail_price)} per ${UNIT_KIND_LABEL[(line.unit_kind ?? "fust")]})` : "", line.quantity ? ` · ${euro(lineTotal(line.quantity, line.unit_price))}` : ""] })), ag && Math.abs(ag.net_price - Number(line.unit_price ?? -1)) > 0.0001 && (_jsxs("div", { className: "text-amber-700", children: ["Afspraak: ", euro(ag.net_price), " ", _jsx("button", { type: "button", className: "underline", onClick: () => {
866
+ const next = [...lines];
867
+ next[idx] = { ...next[idx], unit_price: ag.net_price, price_source: "afspraak" };
868
+ setLines(next);
869
+ }, children: "gebruiken" })] }))] }));
870
+ })()] }), _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));
871
+ }), _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 justify-between border-t pt-2 text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: "Totaal" }), _jsx("span", { className: "font-medium", children: euro(orderTotal) })] }), linesWithoutPrice > 0 && (_jsxs("div", { className: "rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-800", children: ["Geen prijs bekend voor ", linesWithoutPrice, " regel(s). Vul een prijs in of bevestig straks dat je zonder prijs doorgaat."] }))] }), _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) => {
804
872
  if (submitting)
805
873
  return;
806
874
  setConfirmOpen(o);
807
875
  if (!o)
808
876
  setCheckedOk(false);
809
- }, 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); } })] })] })] }));
877
+ }, 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("div", { children: [_jsx("span", { className: "text-muted-foreground", children: "Referentie:" }), " ", values["reference"] || "-"] }), _jsxs("div", { children: [_jsx("span", { className: "text-muted-foreground", children: "Totaal:" }), " ", euro(orderTotal)] })] }), linesWithoutPrice > 0 && (_jsxs("label", { className: "flex items-start gap-2 cursor-pointer rounded-md border border-amber-200 bg-amber-50 p-2 text-amber-800", children: [_jsx("input", { type: "checkbox", className: "mt-1", checked: acceptNoPrice, onChange: (e) => setAcceptNoPrice(e.target.checked) }), _jsxs("span", { children: ["Geen prijs bekend voor ", linesWithoutPrice, " regel(s). Ik wil toch zonder prijs bevestigen."] })] })), _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 || (linesWithoutPrice > 0 && !acceptNoPrice), 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); } })] })] })] }));
810
878
  }
811
879
  /** Verplichte bevestiging: externe klant eerst als lokale klant aanmaken. */
812
880
  function NewCustomerDialog({ candidate, onClose, onCreated, }) {
@@ -912,3 +980,15 @@ function DynamicField({ field, value, meta, onChange, customers, products, plant
912
980
  };
913
981
  return (_jsxs("div", { className: "space-y-1", children: [label, _jsx("div", { className: reviewClass, children: renderControl() })] }));
914
982
  }
983
+ /** Betrouwbaarheid + bron per veld, met kleur volgens de drempels in de instellingen. */
984
+ function ConfBadge({ meta, thresholds, }) {
985
+ if (!meta)
986
+ return null;
987
+ const c = Math.max(0, Math.min(1, Number(meta.confidence ?? 0)));
988
+ const cls = c >= thresholds.green
989
+ ? "border-emerald-300 bg-emerald-50 text-emerald-800"
990
+ : c >= thresholds.orange
991
+ ? "border-amber-300 bg-amber-50 text-amber-800"
992
+ : "border-destructive/40 bg-destructive/5 text-destructive";
993
+ return (_jsxs("span", { className: `inline-flex items-center rounded border px-1.5 py-0.5 text-[11px] ${cls}`, children: [Math.round(c * 100), "% \u00B7 ", meta.source] }));
994
+ }
@@ -0,0 +1,2 @@
1
+ export declare function MailboxSkippedPage(): import("react").JSX.Element;
2
+ //# sourceMappingURL=MailboxSkippedPage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MailboxSkippedPage.d.ts","sourceRoot":"","sources":["../../src/components/MailboxSkippedPage.tsx"],"names":[],"mappings":"AAkBA,wBAAgB,kBAAkB,gCAoJjC"}