@flowselections/mailbox-orders 1.0.29 → 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.
- package/dist-lib/components/MailboxProcessedPage.js +1 -1
- package/dist-lib/components/MailboxProposalsPage.d.ts.map +1 -1
- package/dist-lib/components/MailboxProposalsPage.js +202 -122
- package/dist-lib/components/MailboxSkippedPage.d.ts +2 -0
- package/dist-lib/components/MailboxSkippedPage.d.ts.map +1 -0
- package/dist-lib/components/MailboxSkippedPage.js +71 -0
- package/dist-lib/components/settings/MailboxFilterSettingsCard.d.ts +2 -0
- package/dist-lib/components/settings/MailboxFilterSettingsCard.d.ts.map +1 -0
- package/dist-lib/components/settings/MailboxFilterSettingsCard.js +78 -0
- package/dist-lib/components/settings/MailboxTestSetCard.d.ts +2 -0
- package/dist-lib/components/settings/MailboxTestSetCard.d.ts.map +1 -0
- package/dist-lib/components/settings/MailboxTestSetCard.js +69 -0
- package/dist-lib/index.d.ts +1 -0
- package/dist-lib/index.d.ts.map +1 -1
- package/dist-lib/index.js +8 -2
- package/dist-lib/integrations/supabase/client.server.js +1 -1
- package/dist-lib/integrations/supabase/types.d.ts +864 -1
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/lib/mailbox-ai.server.d.ts +33 -12
- package/dist-lib/lib/mailbox-ai.server.d.ts.map +1 -1
- package/dist-lib/lib/mailbox-ai.server.js +47 -17
- package/dist-lib/lib/mailbox-filter-settings.functions.d.ts +143 -0
- package/dist-lib/lib/mailbox-filter-settings.functions.d.ts.map +1 -0
- package/dist-lib/lib/mailbox-filter-settings.functions.js +241 -0
- package/dist-lib/lib/mailbox-filter.d.ts +34 -0
- package/dist-lib/lib/mailbox-filter.d.ts.map +1 -0
- package/dist-lib/lib/mailbox-filter.js +121 -0
- package/dist-lib/lib/mailbox-ingest.server.d.ts +114 -17
- package/dist-lib/lib/mailbox-ingest.server.d.ts.map +1 -1
- package/dist-lib/lib/mailbox-ingest.server.js +594 -266
- package/dist-lib/lib/mailbox-match.d.ts +28 -0
- package/dist-lib/lib/mailbox-match.d.ts.map +1 -0
- package/dist-lib/lib/mailbox-match.js +74 -0
- package/dist-lib/lib/mailbox-units.d.ts +18 -0
- package/dist-lib/lib/mailbox-units.d.ts.map +1 -0
- package/dist-lib/lib/mailbox-units.js +80 -0
- package/dist-lib/lib/mailbox.functions.d.ts +75 -1
- package/dist-lib/lib/mailbox.functions.d.ts.map +1 -1
- package/dist-lib/lib/mailbox.functions.js +174 -29
- package/package.json +6 -1
- package/dist-lib/components/CartAssignmentWizard.d.ts +0 -6
- package/dist-lib/components/CartAssignmentWizard.d.ts.map +0 -1
- package/dist-lib/components/CartAssignmentWizard.js +0 -199
- package/dist-lib/components/MailboxInboxPage.d.ts +0 -2
- package/dist-lib/components/MailboxInboxPage.d.ts.map +0 -1
- package/dist-lib/components/MailboxInboxPage.js +0 -120
- package/dist-lib/components/settings/ImapAccountsCard.d.ts +0 -2
- package/dist-lib/components/settings/ImapAccountsCard.d.ts.map +0 -1
- package/dist-lib/components/settings/ImapAccountsCard.js +0 -136
- package/dist-lib/components/settings/ImapProfilesCard.d.ts +0 -2
- package/dist-lib/components/settings/ImapProfilesCard.d.ts.map +0 -1
- package/dist-lib/components/settings/ImapProfilesCard.js +0 -101
- package/dist-lib/components/settings/MailboxSettingsCard.d.ts +0 -2
- package/dist-lib/components/settings/MailboxSettingsCard.d.ts.map +0 -1
- package/dist-lib/components/settings/MailboxSettingsCard.js +0 -85
- package/dist-lib/lib/imap.functions.d.ts +0 -32852
- package/dist-lib/lib/imap.functions.d.ts.map +0 -1
- package/dist-lib/lib/imap.functions.js +0 -235
- package/dist-lib/lib/mailbox-auto.server.d.ts +0 -9
- package/dist-lib/lib/mailbox-auto.server.d.ts.map +0 -1
- package/dist-lib/lib/mailbox-auto.server.js +0 -198
|
@@ -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":"
|
|
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 {
|
|
8
|
-
import {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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: "
|
|
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
|
|
399
|
-
const
|
|
400
|
-
const
|
|
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:
|
|
407
|
-
unit:
|
|
408
|
-
|
|
409
|
-
|
|
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
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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
|
-
|
|
560
|
-
const
|
|
561
|
-
const
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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
|
-
|
|
567
|
-
const
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
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
|
|
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
|
-
|
|
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",
|
|
698
|
-
|
|
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:
|
|
768
|
-
unit:
|
|
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
|
-
} }),
|
|
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: [
|
|
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` }),
|
|
803
|
-
|
|
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 @@
|
|
|
1
|
+
{"version":3,"file":"MailboxSkippedPage.d.ts","sourceRoot":"","sources":["../../src/components/MailboxSkippedPage.tsx"],"names":[],"mappings":"AAkBA,wBAAgB,kBAAkB,gCAoJjC"}
|