@flowselections/mailbox-orders 1.0.51 → 1.0.55
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/MailboxAuthBanner.d.ts.map +1 -1
- package/dist-lib/components/MailboxAuthBanner.js +6 -5
- package/dist-lib/components/MailboxPageFrame.d.ts +19 -0
- package/dist-lib/components/MailboxPageFrame.d.ts.map +1 -0
- package/dist-lib/components/MailboxPageFrame.js +95 -0
- package/dist-lib/components/MailboxProcessedPage.d.ts.map +1 -1
- package/dist-lib/components/MailboxProcessedPage.js +94 -62
- package/dist-lib/components/MailboxProposalsPage.d.ts.map +1 -1
- package/dist-lib/components/MailboxProposalsPage.js +178 -71
- package/dist-lib/components/MailboxSkippedPage.d.ts.map +1 -1
- package/dist-lib/components/MailboxSkippedPage.js +80 -14
- package/dist-lib/components/MailboxStatusLine.d.ts +3 -1
- package/dist-lib/components/MailboxStatusLine.d.ts.map +1 -1
- package/dist-lib/components/MailboxStatusLine.js +31 -8
- package/dist-lib/components/fs-ui/DataTable.d.ts +36 -0
- package/dist-lib/components/fs-ui/DataTable.d.ts.map +1 -0
- package/dist-lib/components/fs-ui/DataTable.js +33 -0
- package/dist-lib/components/fs-ui/EmptyState.d.ts +9 -0
- package/dist-lib/components/fs-ui/EmptyState.d.ts.map +1 -0
- package/dist-lib/components/fs-ui/EmptyState.js +5 -0
- package/dist-lib/components/fs-ui/FilterChips.d.ts +13 -0
- package/dist-lib/components/fs-ui/FilterChips.d.ts.map +1 -0
- package/dist-lib/components/fs-ui/FilterChips.js +8 -0
- package/dist-lib/components/fs-ui/HoverTip.d.ts +15 -0
- package/dist-lib/components/fs-ui/HoverTip.d.ts.map +1 -0
- package/dist-lib/components/fs-ui/HoverTip.js +24 -0
- package/dist-lib/components/fs-ui/InlineAlert.d.ts +9 -0
- package/dist-lib/components/fs-ui/InlineAlert.d.ts.map +1 -0
- package/dist-lib/components/fs-ui/InlineAlert.js +14 -0
- package/dist-lib/components/fs-ui/KpiRow.d.ts +6 -0
- package/dist-lib/components/fs-ui/KpiRow.d.ts.map +1 -0
- package/dist-lib/components/fs-ui/KpiRow.js +7 -0
- package/dist-lib/components/fs-ui/KpiTile.d.ts +21 -0
- package/dist-lib/components/fs-ui/KpiTile.d.ts.map +1 -0
- package/dist-lib/components/fs-ui/KpiTile.js +40 -0
- package/dist-lib/components/fs-ui/MobilePrimaryAction.d.ts +10 -0
- package/dist-lib/components/fs-ui/MobilePrimaryAction.d.ts.map +1 -0
- package/dist-lib/components/fs-ui/MobilePrimaryAction.js +7 -0
- package/dist-lib/components/fs-ui/PageHeader.d.ts +8 -0
- package/dist-lib/components/fs-ui/PageHeader.d.ts.map +1 -0
- package/dist-lib/components/fs-ui/PageHeader.js +5 -0
- package/dist-lib/components/fs-ui/PillTabs.d.ts +15 -0
- package/dist-lib/components/fs-ui/PillTabs.d.ts.map +1 -0
- package/dist-lib/components/fs-ui/PillTabs.js +14 -0
- package/dist-lib/components/fs-ui/SearchInput.d.ts +12 -0
- package/dist-lib/components/fs-ui/SearchInput.d.ts.map +1 -0
- package/dist-lib/components/fs-ui/SearchInput.js +33 -0
- package/dist-lib/components/fs-ui/ShortcutHelp.d.ts +7 -0
- package/dist-lib/components/fs-ui/ShortcutHelp.d.ts.map +1 -0
- package/dist-lib/components/fs-ui/ShortcutHelp.js +6 -0
- package/dist-lib/components/fs-ui/StatusIcon.d.ts +8 -0
- package/dist-lib/components/fs-ui/StatusIcon.d.ts.map +1 -0
- package/dist-lib/components/fs-ui/StatusIcon.js +16 -0
- package/dist-lib/components/fs-ui/TableSkeleton.d.ts +6 -0
- package/dist-lib/components/fs-ui/TableSkeleton.d.ts.map +1 -0
- package/dist-lib/components/fs-ui/TableSkeleton.js +6 -0
- package/dist-lib/components/fs-ui/Toolbar.d.ts +8 -0
- package/dist-lib/components/fs-ui/Toolbar.d.ts.map +1 -0
- package/dist-lib/components/fs-ui/Toolbar.js +5 -0
- package/dist-lib/components/fs-ui/UpdatedAt.d.ts +7 -0
- package/dist-lib/components/fs-ui/UpdatedAt.d.ts.map +1 -0
- package/dist-lib/components/fs-ui/UpdatedAt.js +9 -0
- package/dist-lib/components/fs-ui/useListHotkeys.d.ts +18 -0
- package/dist-lib/components/fs-ui/useListHotkeys.d.ts.map +1 -0
- package/dist-lib/components/fs-ui/useListHotkeys.js +48 -0
- package/dist-lib/components/settings/MailboxAiPromptCard.d.ts +2 -1
- package/dist-lib/components/settings/MailboxAiPromptCard.d.ts.map +1 -1
- package/dist-lib/components/settings/MailboxAiPromptCard.js +9 -1
- package/dist-lib/components/settings/MailboxFilterSettingsCard.d.ts.map +1 -1
- package/dist-lib/components/settings/MailboxFilterSettingsCard.js +13 -2
- package/dist-lib/components/settings/MailboxTestSetCard.d.ts +2 -1
- package/dist-lib/components/settings/MailboxTestSetCard.d.ts.map +1 -1
- package/dist-lib/components/settings/MailboxTestSetCard.js +10 -2
- package/dist-lib/integrations/supabase/types.d.ts +833 -1
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/lib/mailbox-filter-settings.functions.d.ts +8 -0
- package/dist-lib/lib/mailbox-filter-settings.functions.d.ts.map +1 -1
- package/dist-lib/lib/mailbox-filter-settings.functions.js +11 -1
- package/dist-lib/lib/mailbox-ingest.server.d.ts +3 -2
- package/dist-lib/lib/mailbox-ingest.server.d.ts.map +1 -1
- package/dist-lib/lib/mailbox-ingest.server.js +16 -14
- package/dist-lib/lib/mailbox-list.functions.d.ts +67 -0
- package/dist-lib/lib/mailbox-list.functions.d.ts.map +1 -0
- package/dist-lib/lib/mailbox-list.functions.js +63 -0
- package/dist-lib/lib/mailbox.functions.d.ts +1 -6
- package/dist-lib/lib/mailbox.functions.d.ts.map +1 -1
- package/dist-lib/lib/mailbox.functions.js +4 -28
- package/dist-lib/lib/pickup-locations.d.ts +17 -0
- package/dist-lib/lib/pickup-locations.d.ts.map +1 -0
- package/dist-lib/lib/pickup-locations.js +64 -0
- package/package.json +1 -1
|
@@ -2,18 +2,30 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { useQuery, useQueryClient, useMutation } from "@tanstack/react-query";
|
|
4
4
|
import { useServerFn } from "@tanstack/react-start";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
5
|
+
import { useNavigate } from "@tanstack/react-router";
|
|
6
|
+
import { Button, Badge, Card, CardContent, CardHeader, CardTitle, Input, Label, Textarea, Switch, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Dialog, DialogContent, DialogHeader, DialogTitle, toast, } from "@flowselections/core";
|
|
7
|
+
import { Check, X, Trash2, Plus, AlertTriangle, RefreshCw, CalendarClock, UserX, Filter, Search, Inbox, ClipboardCheck } from "lucide-react";
|
|
8
|
+
import { getProposalDetail, approveProposal, rejectProposal, getStickerDefaults, listCustomersForPicker, createCustomerFromGroup, listProductsForPicker, listDeliveryLocationsForCustomer, listPickupLocations, listCarriersForPicker, getActiveOrdersMailbox, syncMailboxNow, refreshProposalsForAgentChange, reparseProposalNow, listBeladingOptionsForProducts, listAllowedLoadCarriers, getDeparturePreview, getAgreementPrices, countOutdatedProposals, } from "../lib/mailbox.functions.js";
|
|
8
9
|
import { normalizeUnitKind, toPlants, describeConversion, lineTotal, UNIT_KIND_LABEL, } from "../lib/mailbox-units.js";
|
|
9
10
|
import { enabledLoadCarriers, normalizeAllowedLoadCarriers, EMPTY_LOAD_CARRIERS } from "../lib/load-carriers.js";
|
|
10
11
|
import { listTemplateFields, listPlantTypesForPicker, listSuppliersForPicker, } from "../lib/mailbox-template.functions.js";
|
|
11
12
|
import { SearchableSelect } from "./SearchableSelect.js";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
13
|
+
import { AI_AUTH_PATH } from "./MailboxStatusLine.js";
|
|
14
|
+
import { listProposalsPage } from "../lib/mailbox-list.functions.js";
|
|
15
|
+
import { MailboxPageFrame, browserTz } from "./MailboxPageFrame.js";
|
|
16
|
+
import { DataTable, InitialsAvatar } from "./fs-ui/DataTable.js";
|
|
17
|
+
import { StatusIcon } from "./fs-ui/StatusIcon.js";
|
|
18
|
+
import { EmptyState } from "./fs-ui/EmptyState.js";
|
|
19
|
+
import { InlineAlert } from "./fs-ui/InlineAlert.js";
|
|
20
|
+
import { TableSkeleton } from "./fs-ui/TableSkeleton.js";
|
|
21
|
+
import { Toolbar } from "./fs-ui/Toolbar.js";
|
|
22
|
+
import { SearchInput } from "./fs-ui/SearchInput.js";
|
|
23
|
+
import { FilterChips } from "./fs-ui/FilterChips.js";
|
|
24
|
+
import { MobilePrimaryAction } from "./fs-ui/MobilePrimaryAction.js";
|
|
25
|
+
import { useListHotkeys } from "./fs-ui/useListHotkeys.js";
|
|
14
26
|
import { EmailPreview } from "./EmailPreview.js";
|
|
15
27
|
import { TablePagination } from "./TablePagination.js";
|
|
16
|
-
import {
|
|
28
|
+
import { usePersistedState } from "./MailboxFilterBar.js";
|
|
17
29
|
import { supabase } from "../integrations/supabase/client.js";
|
|
18
30
|
import { canOverrideLateSignup, checkLinePrices } from "../lib/verkoop-order.js";
|
|
19
31
|
const beladingKey = (b) => `${b.plants_per_crate}x${b.crates_per_layer}x${b.layers_per_cart}|${b.crate_code ?? ""}|${b.carrier_code ?? ""}`;
|
|
@@ -79,7 +91,7 @@ function resolveCustomerId(raw, customers, fallbackId) {
|
|
|
79
91
|
}
|
|
80
92
|
export function MailboxProposalsPage() {
|
|
81
93
|
const qc = useQueryClient();
|
|
82
|
-
const fetchList = useServerFn(
|
|
94
|
+
const fetchList = useServerFn(listProposalsPage);
|
|
83
95
|
const fetchTemplate = useServerFn(listTemplateFields);
|
|
84
96
|
const fetchApprove = useServerFn(approveProposal);
|
|
85
97
|
const fetchReject = useServerFn(rejectProposal);
|
|
@@ -91,6 +103,11 @@ export function MailboxProposalsPage() {
|
|
|
91
103
|
const fetchPickupLocations = useServerFn(listPickupLocations);
|
|
92
104
|
const fetchActive = useServerFn(getActiveOrdersMailbox);
|
|
93
105
|
const fetchSync = useServerFn(syncMailboxNow);
|
|
106
|
+
const navigate = useNavigate();
|
|
107
|
+
/** Knop "Open order" in de melding na bevestigen (route van Verkooporders). */
|
|
108
|
+
const openOrderAction = (orderId) => orderId
|
|
109
|
+
? { action: { label: "Open order", onClick: () => void navigate({ to: `/floriday-verkoop/verkooporders/${orderId}` }) } }
|
|
110
|
+
: undefined;
|
|
94
111
|
// Server fns met requireSupabaseAuth gooien een raw Response bij een stale
|
|
95
112
|
// bearer-token. Die bubbelt op als "[object Response]" en blankt het scherm.
|
|
96
113
|
// Vang elke fout en geef een lege fallback terug — UI blijft bruikbaar.
|
|
@@ -106,10 +123,14 @@ export function MailboxProposalsPage() {
|
|
|
106
123
|
// zodra de dialoog voor het eerst opengaat, en blijven daarna gecachet.
|
|
107
124
|
const [activeId, setActiveId] = useState(null);
|
|
108
125
|
const [page, setPage] = useState(1);
|
|
109
|
-
const [pageSize, setPageSize] = useState(
|
|
126
|
+
const [pageSize, setPageSize] = useState(50);
|
|
110
127
|
const [search, setSearch] = usePersistedState("mailbox-proposals:search", "");
|
|
111
128
|
const [period, setPeriod] = usePersistedState("mailbox-proposals:period", "all");
|
|
112
129
|
const [sortOrder, setSortOrder] = usePersistedState("mailbox-proposals:sort", "date-desc");
|
|
130
|
+
const [listFilter, setListFilter] = usePersistedState("mailbox-proposals:filter", null);
|
|
131
|
+
const [cursor, setCursor] = useState(null);
|
|
132
|
+
const [helpOpen, setHelpOpen] = useState(false);
|
|
133
|
+
const searchRef = useRef(null);
|
|
113
134
|
const refsEnabled = true;
|
|
114
135
|
const refOpts = {
|
|
115
136
|
enabled: refsEnabled,
|
|
@@ -120,11 +141,23 @@ export function MailboxProposalsPage() {
|
|
|
120
141
|
// Bestelvoorstellen toont uitsluitend nieuwe, nog niet gecontroleerde
|
|
121
142
|
// bestellingen ("Te controleren"). Goedgekeurd/afgewezen verschijnt in
|
|
122
143
|
// de module Verwerkt.
|
|
123
|
-
const
|
|
124
|
-
|
|
144
|
+
const listParams = {
|
|
145
|
+
statuses: ["pending"],
|
|
146
|
+
search,
|
|
147
|
+
period,
|
|
148
|
+
sort: sortOrder === "date-asc" ? "asc" : "desc",
|
|
149
|
+
dateField: "received",
|
|
150
|
+
tz: browserTz(),
|
|
151
|
+
filter: listFilter,
|
|
152
|
+
page,
|
|
153
|
+
pageSize,
|
|
154
|
+
};
|
|
155
|
+
const { data, isLoading, error: listError, refetch: refetchList, dataUpdatedAt } = useQuery({
|
|
156
|
+
queryKey: ["mailbox-proposals", "pending", listParams],
|
|
125
157
|
// Bewust NIET stilzwijgend opvangen: als de server tijdelijk onbereikbaar is
|
|
126
158
|
// moet de gebruiker dat zien in plaats van een lege lijst ("geen mailtjes").
|
|
127
|
-
queryFn: () => fetchList({ data:
|
|
159
|
+
queryFn: () => fetchList({ data: listParams }),
|
|
160
|
+
placeholderData: (prev) => prev,
|
|
128
161
|
retry: 3,
|
|
129
162
|
retryDelay: (a) => Math.min(1000 * 2 ** a, 8000),
|
|
130
163
|
refetchInterval: 60000,
|
|
@@ -171,6 +204,7 @@ export function MailboxProposalsPage() {
|
|
|
171
204
|
onSuccess: (res) => {
|
|
172
205
|
if (res?.ok && ((res.inserted ?? 0) > 0 || (res.processed ?? 0) > 0)) {
|
|
173
206
|
qc.invalidateQueries({ queryKey: ["mailbox-proposals"] });
|
|
207
|
+
qc.invalidateQueries({ queryKey: ["mailbox-kpis"] });
|
|
174
208
|
}
|
|
175
209
|
},
|
|
176
210
|
});
|
|
@@ -221,8 +255,9 @@ export function MailboxProposalsPage() {
|
|
|
221
255
|
},
|
|
222
256
|
onSuccess: (res) => {
|
|
223
257
|
qc.invalidateQueries({ queryKey: ["mailbox-proposals"] });
|
|
258
|
+
qc.invalidateQueries({ queryKey: ["mailbox-kpis"] });
|
|
224
259
|
void refetchOutdated();
|
|
225
|
-
toast.success(`${res?.reparsed ?? 0}
|
|
260
|
+
toast.success(`${res?.reparsed ?? 0} bestelling(en) opnieuw uitgelezen`);
|
|
226
261
|
},
|
|
227
262
|
});
|
|
228
263
|
// Handmatig één voorstel opnieuw laten analyseren door de AI-agent.
|
|
@@ -237,22 +272,36 @@ export function MailboxProposalsPage() {
|
|
|
237
272
|
if (res?.ok) {
|
|
238
273
|
setReparseError(null);
|
|
239
274
|
qc.invalidateQueries({ queryKey: ["mailbox-proposals"] });
|
|
275
|
+
qc.invalidateQueries({ queryKey: ["mailbox-kpis"] });
|
|
240
276
|
qc.invalidateQueries({ queryKey: ["mailbox-proposal-detail"] });
|
|
241
|
-
toast.success("
|
|
277
|
+
toast.success("Mail opnieuw uitgelezen — gegevens bijgewerkt");
|
|
242
278
|
}
|
|
243
279
|
else {
|
|
244
|
-
|
|
280
|
+
if (res?.error)
|
|
281
|
+
console.warn("[mailbox] opnieuw uitlezen", res.error);
|
|
282
|
+
const msg = "Opnieuw uitlezen lukt niet. Probeer het over een paar minuten opnieuw.";
|
|
245
283
|
setReparseError(msg);
|
|
246
284
|
toast.error(msg);
|
|
247
285
|
}
|
|
248
286
|
},
|
|
249
287
|
onError: (e) => {
|
|
250
|
-
|
|
288
|
+
console.warn("[mailbox] opnieuw uitlezen", e);
|
|
289
|
+
const msg = "Opnieuw uitlezen lukt niet. Probeer het over een paar minuten opnieuw.";
|
|
251
290
|
setReparseError(msg);
|
|
252
291
|
toast.error(msg);
|
|
253
292
|
},
|
|
254
293
|
});
|
|
255
|
-
|
|
294
|
+
// Platte rijen uit de server terugzetten naar de vorm die de rest van de
|
|
295
|
+
// pagina (klantnaam, detailkop) al gebruikt.
|
|
296
|
+
const items = useMemo(() => (Array.isArray(data?.items) ? data.items : []).map((r) => ({
|
|
297
|
+
...r,
|
|
298
|
+
mailbox_messages: {
|
|
299
|
+
subject: r.subject, from_email: r.from_email, from_name: r.from_name,
|
|
300
|
+
received_at: r.received_at, connection_id: r.connection_id,
|
|
301
|
+
account_email: r.account_email, preview: String(r.preview ?? "").slice(0, 180),
|
|
302
|
+
},
|
|
303
|
+
})), [data]);
|
|
304
|
+
const total = Number(data?.total ?? 0);
|
|
256
305
|
const template = Array.isArray(templateData?.items) ? templateData.items : [];
|
|
257
306
|
const customers = Array.isArray(customersData?.items) ? customersData.items : [];
|
|
258
307
|
const products = Array.isArray(productsData?.items) ? productsData.items : [];
|
|
@@ -279,43 +328,95 @@ export function MailboxProposalsPage() {
|
|
|
279
328
|
}
|
|
280
329
|
return p.mailbox_messages?.from_name || p.mailbox_messages?.from_email || "Onbekend";
|
|
281
330
|
};
|
|
282
|
-
// --- Zoeken, filteren en sorteren
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
if (!matchesPeriod(p.mailbox_messages?.received_at ?? p.created_at, period))
|
|
288
|
-
return false;
|
|
289
|
-
if (!q)
|
|
290
|
-
return true;
|
|
291
|
-
const haystack = normalizeText([
|
|
292
|
-
name,
|
|
293
|
-
p.mailbox_messages?.from_email,
|
|
294
|
-
p.mailbox_messages?.from_name,
|
|
295
|
-
p.mailbox_messages?.subject,
|
|
296
|
-
p.mailbox_messages?.preview,
|
|
297
|
-
].join(" "));
|
|
298
|
-
return q.split(/\s+/).every((token) => haystack.includes(token));
|
|
299
|
-
});
|
|
300
|
-
return filtered.sort((a, b) => {
|
|
301
|
-
const aDate = a.mailbox_messages?.received_at ?? a.created_at ?? "";
|
|
302
|
-
const bDate = b.mailbox_messages?.received_at ?? b.created_at ?? "";
|
|
303
|
-
return sortOrder === "date-desc" ? bDate.localeCompare(aDate) : aDate.localeCompare(bDate);
|
|
304
|
-
});
|
|
305
|
-
}, [items, customers, search, sortOrder, period]);
|
|
306
|
-
const hasActiveFilters = !!search || period !== "all" || sortOrder !== "date-desc";
|
|
307
|
-
const clearFilters = () => { setSearch(""); setPeriod("all"); setSortOrder("date-desc"); };
|
|
308
|
-
useEffect(() => { setPage(1); }, [search, period, sortOrder]);
|
|
309
|
-
const pageCount = Math.max(1, Math.ceil(filteredItems.length / pageSize));
|
|
331
|
+
// --- Zoeken, filteren en sorteren gebeurt op de server -----------------
|
|
332
|
+
const hasActiveFilters = !!search || period !== "all" || !!listFilter;
|
|
333
|
+
const clearFilters = () => { setSearch(""); setPeriod("all"); setSortOrder("date-desc"); setListFilter(null); };
|
|
334
|
+
useEffect(() => { setPage(1); setCursor(null); }, [search, period, sortOrder, listFilter]);
|
|
335
|
+
const pageCount = Math.max(1, Math.ceil(total / pageSize));
|
|
310
336
|
const currentPage = Math.min(page, pageCount);
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
337
|
+
// Oudste open voorstel openen (knop "Volgende controleren" op mobiel).
|
|
338
|
+
const openOldest = async () => {
|
|
339
|
+
try {
|
|
340
|
+
const res = await fetchList({ data: { statuses: ["pending"], sort: "asc", page: 1, pageSize: 10, tz: browserTz() } });
|
|
341
|
+
const first = Array.isArray(res?.items) ? res.items[0] : null;
|
|
342
|
+
if (first?.id)
|
|
343
|
+
setActiveId(first.id);
|
|
344
|
+
else
|
|
345
|
+
toast.info("Er zijn geen bestellingen meer om te controleren.");
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
toast.error("Openen lukt niet. Probeer het opnieuw.");
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
useListHotkeys({
|
|
352
|
+
disabled: !!activeId || helpOpen,
|
|
353
|
+
onSearch: () => searchRef.current?.focus(),
|
|
354
|
+
onNext: () => setCursor((c) => (items.length ? Math.min((c ?? -1) + 1, items.length - 1) : null)),
|
|
355
|
+
onPrev: () => setCursor((c) => (items.length ? Math.max((c ?? 1) - 1, 0) : null)),
|
|
356
|
+
onOpen: cursor != null && items[cursor] ? () => setActiveId(items[cursor].id) : undefined,
|
|
357
|
+
onEscape: hasActiveFilters ? clearFilters : undefined,
|
|
358
|
+
onHelp: () => setHelpOpen(true),
|
|
359
|
+
});
|
|
360
|
+
const periodLabel = { today: "Vandaag", "7d": "Laatste 7 dagen", "30d": "Laatste 30 dagen" };
|
|
361
|
+
const chips = [
|
|
362
|
+
...(period !== "all" ? [{ key: "period", label: periodLabel[period] ?? period, icon: _jsx(CalendarClock, {}), onRemove: () => setPeriod("all") }] : []),
|
|
363
|
+
...(listFilter === "klant_onbekend" ? [{ key: "klant", label: "Klant onbekend", icon: _jsx(UserX, {}), onRemove: () => setListFilter(null) }] : []),
|
|
364
|
+
];
|
|
365
|
+
const columns = [
|
|
366
|
+
{
|
|
367
|
+
key: "klant", header: "Klant", width: "minmax(180px,1.4fr)",
|
|
368
|
+
cell: (p) => {
|
|
369
|
+
const name = customerNameFor(p);
|
|
370
|
+
return (_jsxs("div", { className: "flex min-w-0 items-center gap-3", children: [_jsx(InitialsAvatar, { name: name }), _jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: "truncate font-medium", children: name }), !p.customer_name && _jsx("div", { className: "truncate text-xs text-muted-foreground", children: "Klant nog niet herkend" })] })] }));
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
key: "bericht", header: "Bericht", width: "minmax(240px,2.2fr)",
|
|
375
|
+
cell: (p) => (_jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: "truncate 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" })] })),
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
key: "ontvangen_op", header: "Ontvangen op", width: "minmax(140px,1fr)",
|
|
379
|
+
cell: (p) => _jsx("span", { className: "block truncate text-xs text-muted-foreground", children: p.mailbox_messages?.account_email || "–" }),
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
key: "datum", header: "Ontvangen", width: "120px", sortable: true,
|
|
383
|
+
cell: (p) => {
|
|
384
|
+
const d = p.mailbox_messages?.received_at ?? p.created_at;
|
|
385
|
+
return _jsx("span", { className: "tabular-nums text-muted-foreground", children: d ? new Date(d).toLocaleDateString("nl-NL") : "–" });
|
|
386
|
+
},
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
key: "status", header: "Status", width: "150px",
|
|
390
|
+
cell: (p) => (_jsxs("div", { className: "flex items-center gap-2", children: [p.readiness === "klaar"
|
|
391
|
+
? _jsx(StatusIcon, { tone: "done", label: "Klaar om te bevestigen" })
|
|
392
|
+
: _jsx(StatusIcon, { tone: "open", label: "Te controleren", detail: "Controleer de gegevens en bevestig de bestelling." }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: p.readiness === "klaar" ? "Klaar" : "Controleren" }), p.duplicate && _jsx(StatusIcon, { tone: "busy", label: "Mogelijk dubbel", detail: "Deze mail lijkt op een eerdere bestelling." })] })),
|
|
393
|
+
},
|
|
394
|
+
];
|
|
395
|
+
return (_jsxs(MailboxPageFrame, { tab: "voorstellen", helpOpen: helpOpen, onHelpOpenChange: setHelpOpen, onKpi: (k) => {
|
|
396
|
+
if (k === "today") {
|
|
397
|
+
setListFilter(null);
|
|
398
|
+
setPeriod("today");
|
|
399
|
+
return true;
|
|
400
|
+
}
|
|
401
|
+
if (k === "customer_unknown") {
|
|
402
|
+
setPeriod("all");
|
|
403
|
+
setListFilter("klant_onbekend");
|
|
404
|
+
return true;
|
|
405
|
+
}
|
|
406
|
+
if (k === "pending") {
|
|
407
|
+
clearFilters();
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
return false;
|
|
411
|
+
}, children: [outdatedCount > 0 && (_jsx(InlineAlert, { tone: "warning", title: `${outdatedCount} bestelling(en) zijn met de vorige instelling uitgelezen.`, action: _jsx(Button, { size: "sm", variant: "outline", disabled: agentRefreshMut.isPending, onClick: () => agentRefreshMut.mutate(), children: agentRefreshMut.isPending ? "Bezig…" : "Opnieuw uitlezen" }) })), _jsx(Toolbar, { chips: _jsx(FilterChips, { chips: chips, onClearAll: clearFilters }), right: _jsxs(Select, { value: period, onValueChange: (v) => setPeriod(v), children: [_jsxs(SelectTrigger, { className: "h-9 w-[150px]", "aria-label": "Periode", children: [_jsx(Filter, { className: "mr-1 h-4 w-4" }), _jsx(SelectValue, {})] }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "all", children: "Alle periodes" }), _jsx(SelectItem, { value: "today", children: "Vandaag" }), _jsx(SelectItem, { value: "7d", children: "Laatste 7 dagen" }), _jsx(SelectItem, { value: "30d", children: "Laatste 30 dagen" })] })] }), children: _jsx(SearchInput, { ref: searchRef, value: search, onChange: setSearch, placeholder: "Zoek op klant, afzender of onderwerp" }) }), isLoading && !data ? (_jsx(TableSkeleton, { columns: ["1.4fr", "2.2fr", "1fr", "120px", "150px"] })) : listError && !data ? (_jsx(InlineAlert, { tone: "error", title: "De lijst kon niet worden geladen.", action: _jsx(Button, { size: "sm", variant: "outline", onClick: () => void refetchList(), children: "Opnieuw proberen" }), children: "Je bestellingen zijn er nog; ze worden alleen nu niet getoond. Probeer het opnieuw." })) : total === 0 ? (_jsx("div", { className: "rounded-xl border border-border bg-card shadow-sm", children: hasActiveFilters ? (_jsx(EmptyState, { icon: _jsx(Search, { className: "h-6 w-6" }), title: "Niets gevonden", text: "Er zijn geen bestellingen die bij je zoekopdracht of filters passen.", action: _jsx(Button, { size: "sm", variant: "outline", onClick: clearFilters, children: "Filters wissen" }) })) : (_jsx(EmptyState, { icon: _jsx(Inbox, { className: "h-6 w-6" }), title: "Geen bestellingen om te controleren", text: "Nieuwe bestellingen uit je mailbox verschijnen hier vanzelf." })) })) : (_jsx(DataTable, { rows: items, columns: columns, rowKey: (p) => p.id, activeIndex: cursor, onRowClick: (p, i) => { setCursor(i); setActiveId(p.id); }, sort: { key: "datum", dir: sortOrder === "date-asc" ? "asc" : "desc" }, onSortChange: () => setSortOrder(sortOrder === "date-asc" ? "date-desc" : "date-asc"), mobileCard: (p) => {
|
|
412
|
+
const d = p.mailbox_messages?.received_at ?? p.created_at;
|
|
413
|
+
return (_jsxs("div", { className: "min-w-0 space-y-1", children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx("span", { className: "truncate font-semibold", children: customerNameFor(p) }), p.readiness === "klaar"
|
|
414
|
+
? _jsx(StatusIcon, { tone: "done", label: "Klaar om te bevestigen" })
|
|
415
|
+
: _jsx(StatusIcon, { tone: "open", label: "Te controleren" })] }), _jsx("div", { className: "truncate text-muted-foreground", children: p.mailbox_messages?.subject || "(geen onderwerp)" }), _jsxs("div", { className: "text-xs tabular-nums text-muted-foreground", children: [d ? new Date(d).toLocaleDateString("nl-NL") : "–", p.duplicate ? " · mogelijk dubbel" : ""] })] }));
|
|
416
|
+
}, footer: _jsx(TablePagination, { total: total, page: currentPage, pageSize: pageSize, onPageChange: setPage, onPageSizeChange: setPageSize }) })), _jsx("span", { className: "sr-only", "aria-live": "polite", children: dataUpdatedAt ? `${total} bestellingen` : "" }), _jsx(MobilePrimaryAction, { label: "Volgende controleren", icon: _jsx(ClipboardCheck, {}), onClick: () => void openOldest(), hidden: !!activeId || total === 0 }), _jsx(Dialog, { open: !!activeId, onOpenChange: (o) => { if (!o) {
|
|
316
417
|
setActiveId(null);
|
|
317
418
|
setReparseError(null);
|
|
318
|
-
} }, children: _jsx(DialogContent, { className: "max-w-[95vw] w-[95vw] h-[92vh] max-h-[92vh] p-0 flex flex-col overflow-hidden", children: !active ? (_jsxs("div", { className: "flex-1 flex flex-col items-center justify-center gap-2 text-sm text-muted-foreground", children: [_jsx(DialogTitle, { className: "text-base", children: "Bestelling" }), detailLoading ? "Bestelling laden…" : "Bestelling niet gevonden."] })) : (_jsxs(_Fragment, { children: [_jsxs(DialogHeader, { className: "px-6 pt-6 pb-3 shrink-0 border-b", children: [_jsxs("div", { className: "flex items-start justify-between gap-4", children: [_jsx(DialogTitle, { children: customerNameFor(activeSummary ?? active) }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", className: "mr-8", disabled: reparseMutation.isPending, onClick: () => reparseMutation.mutate(active.id), children: [_jsx(RefreshCw, { className: `h-4 w-4 mr-2 ${reparseMutation.isPending ? "animate-spin" : ""}` }), reparseMutation.isPending ? "
|
|
419
|
+
} }, children: _jsx(DialogContent, { className: "max-w-[95vw] w-[95vw] h-[92vh] max-h-[92vh] p-0 flex flex-col overflow-hidden", children: !active ? (_jsxs("div", { className: "flex-1 flex flex-col items-center justify-center gap-2 text-sm text-muted-foreground", children: [_jsx(DialogTitle, { className: "text-base", children: "Bestelling" }), detailLoading ? "Bestelling laden…" : "Bestelling niet gevonden."] })) : (_jsxs(_Fragment, { children: [_jsxs(DialogHeader, { className: "px-6 pt-6 pb-3 shrink-0 border-b", children: [_jsxs("div", { className: "flex items-start justify-between gap-4", children: [_jsx(DialogTitle, { children: customerNameFor(activeSummary ?? active) }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", className: "mr-8", disabled: reparseMutation.isPending, onClick: () => reparseMutation.mutate(active.id), children: [_jsx(RefreshCw, { className: `h-4 w-4 mr-2 ${reparseMutation.isPending ? "animate-spin" : ""}` }), reparseMutation.isPending ? "Uitlezen…" : "Mail opnieuw uitlezen"] })] }), reparseError && (_jsxs("div", { className: "mt-2 flex items-start gap-2 rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-sm text-destructive", children: [_jsx(AlertTriangle, { className: "h-4 w-4 mt-0.5 shrink-0" }), _jsx("span", { children: reparseError })] }))] }), _jsx("div", { className: "flex-1 min-h-0 overflow-hidden p-6", children: _jsx(ProposalEditor, { proposal: active, template: template, customers: customers, products: products, allowBackorder: allowBackorder, plantTypes: plantTypes, suppliers: suppliers, carriers: carriers, pickupLocations: pickupLocations, onApprove: async (payload) => {
|
|
319
420
|
try {
|
|
320
421
|
const res = await fetchApprove({ data: payload });
|
|
321
422
|
if (res?.ok === false) {
|
|
@@ -323,35 +424,40 @@ export function MailboxProposalsPage() {
|
|
|
323
424
|
return res;
|
|
324
425
|
}
|
|
325
426
|
if (res?.alreadyApproved) {
|
|
326
|
-
toast.info(`Er bestaat al een order voor
|
|
427
|
+
toast.info(`Er bestaat al een order voor deze bestelling (${res.orderNumber}).`, openOrderAction(res.orderId));
|
|
327
428
|
qc.invalidateQueries({ queryKey: ["mailbox-proposals"] });
|
|
429
|
+
qc.invalidateQueries({ queryKey: ["mailbox-kpis"] });
|
|
328
430
|
setActiveId(null);
|
|
329
431
|
return res;
|
|
330
432
|
}
|
|
331
433
|
const shortages = Array.isArray(res?.shortages) ? res.shortages : [];
|
|
332
|
-
toast.success(
|
|
434
|
+
toast.success(`Order ${res?.orderNumber ?? ""} aangemaakt.`.replace(" ", " "), openOrderAction(res?.orderId));
|
|
333
435
|
if (shortages.length > 0) {
|
|
334
436
|
toast.warning(`Tekorten (levering zonder voorraad staat aan): ${shortages
|
|
335
437
|
.map((s) => `${s.product ?? s.productId}: besteld ${s.wanted}, beschikbaar ${s.available}, tekort ${s.missing}`)
|
|
336
438
|
.join(" · ")}`);
|
|
337
439
|
}
|
|
338
440
|
qc.invalidateQueries({ queryKey: ["mailbox-proposals"] });
|
|
441
|
+
qc.invalidateQueries({ queryKey: ["mailbox-kpis"] });
|
|
339
442
|
setActiveId(null);
|
|
340
443
|
return res;
|
|
341
444
|
}
|
|
342
445
|
catch (e) {
|
|
343
|
-
|
|
344
|
-
|
|
446
|
+
console.warn("[mailbox] bevestigen", e);
|
|
447
|
+
toast.error("Bevestigen lukt niet. Probeer het opnieuw.");
|
|
448
|
+
return { ok: false, reason: "other", message: "Bevestigen lukt niet. Probeer het opnieuw." };
|
|
345
449
|
}
|
|
346
450
|
}, onReject: async (reason) => {
|
|
347
451
|
try {
|
|
348
452
|
await fetchReject({ data: { proposalId: active.id, reason } });
|
|
349
|
-
toast.success("
|
|
453
|
+
toast.success("Bestelling afgewezen");
|
|
350
454
|
qc.invalidateQueries({ queryKey: ["mailbox-proposals"] });
|
|
455
|
+
qc.invalidateQueries({ queryKey: ["mailbox-kpis"] });
|
|
351
456
|
setActiveId(null);
|
|
352
457
|
}
|
|
353
458
|
catch (e) {
|
|
354
|
-
|
|
459
|
+
console.warn("[mailbox] afwijzen", e);
|
|
460
|
+
toast.error("Afwijzen lukt niet. Probeer het opnieuw.");
|
|
355
461
|
}
|
|
356
462
|
} }, active.id) })] })) }) })] }));
|
|
357
463
|
}
|
|
@@ -678,11 +784,11 @@ function ProposalEditor({ proposal, template, customers, products, allowBackorde
|
|
|
678
784
|
return null;
|
|
679
785
|
}
|
|
680
786
|
if (!deliveryLocationVal) {
|
|
681
|
-
toast.error("
|
|
787
|
+
toast.error("Kies een afleverlocatie.");
|
|
682
788
|
return null;
|
|
683
789
|
}
|
|
684
790
|
if (!pickupLocationVal) {
|
|
685
|
-
toast.error("
|
|
791
|
+
toast.error("Kies een afhaallocatie (eigen locatie of tuin).");
|
|
686
792
|
return null;
|
|
687
793
|
}
|
|
688
794
|
const validLines = lines.filter((l) => l.product_id && (l.quantity ?? 0) > 0);
|
|
@@ -773,7 +879,7 @@ function ProposalEditor({ proposal, template, customers, products, allowBackorde
|
|
|
773
879
|
setSubmitting(false);
|
|
774
880
|
}
|
|
775
881
|
};
|
|
776
|
-
return (_jsxs("div", { className: "grid grid-cols-2 gap-4 h-full min-h-0", children: [_jsx(Card, { className: "flex flex-col min-h-0 overflow-hidden", children: _jsxs(CardContent, { className: "flex-1 min-h-0 p-4 flex flex-col gap-2", children: [msg.account_email && (_jsxs("div", { className: "shrink-0 text-xs text-muted-foreground", children: ["Ontvangen op: ", msg.account_email] })), proposal.parsed_payload?.reason === "geen_ai_agent" && (_jsxs("div", { className: "shrink-0 rounded-md border border-
|
|
882
|
+
return (_jsxs("div", { className: "grid grid-cols-2 gap-4 h-full min-h-0", children: [_jsx(Card, { className: "flex flex-col min-h-0 overflow-hidden", children: _jsxs(CardContent, { className: "flex-1 min-h-0 p-4 flex flex-col gap-2", children: [msg.account_email && (_jsxs("div", { className: "shrink-0 text-xs text-muted-foreground", children: ["Ontvangen op: ", msg.account_email] })), proposal.parsed_payload?.reason === "geen_ai_agent" && (_jsxs("div", { className: "shrink-0 rounded-md border border-warning/30 bg-warning-soft px-3 py-2 text-xs text-warning-soft-foreground", children: ["Automatisch uitlezen staat uit. Vul de gegevens zelf in; een beheerder kan dit aanzetten bij Instellingen.", " ", _jsx("a", { href: AI_AUTH_PATH, className: "underline font-medium", children: "Naar Instellingen" })] })), _jsx(EmailPreview, { subject: msg.subject, fromName: msg.from_name, fromEmail: msg.from_email, receivedAt: msg.received_at, to: Array.isArray(msg.to_recipients) ? msg.to_recipients : [], cc: Array.isArray(msg.cc_recipients) ? msg.cc_recipients : [], bodyHtml: msg.body_html, bodyText: msg.body_text, attachments: Array.isArray(msg.attachments) ? msg.attachments : [], hasAttachments: msg.has_attachments === true })] }) }), _jsxs(Card, { className: "flex flex-col min-h-0 overflow-hidden", children: [_jsx(CardHeader, { className: "shrink-0", children: _jsx(CardTitle, { className: "text-base", children: "Bestelvoorstel" }) }), _jsxs(CardContent, { className: "space-y-4 flex-1 min-h-0 overflow-y-auto overscroll-contain", children: [(() => {
|
|
777
883
|
// Klantveld staat altijd bovenaan, ook als het template (nog) leeg is.
|
|
778
884
|
const customerField = visibleFields.find((f) => f.key === "customer") ?? {
|
|
779
885
|
id: "customer",
|
|
@@ -800,9 +906,9 @@ function ProposalEditor({ proposal, template, customers, products, allowBackorde
|
|
|
800
906
|
const metaFor = (key) => fieldMeta[key] ?? (key === "customer" && proposal.matched_customer_id
|
|
801
907
|
? { confidence: payload.auto_match_warning ? 0.6 : 1, source: payload.auto_match_forwarded ? "afgeleid" : "uit mail" }
|
|
802
908
|
: null);
|
|
803
|
-
return (_jsxs(_Fragment, { children: [dup && (_jsxs("div", { className: "rounded-md border border-
|
|
909
|
+
return (_jsxs(_Fragment, { children: [dup && (_jsxs("div", { className: "rounded-md border border-warning/30 bg-warning-soft px-3 py-2 text-xs text-warning-soft-foreground", 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-warning/30 bg-warning-soft px-3 py-2 text-xs text-warning-soft-foreground", 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-warning/30 bg-warning-soft px-3 py-2 text-xs text-warning-soft-foreground", 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-warning/30 bg-warning-soft px-3 py-2 text-xs text-warning-soft-foreground", children: ai.retry_pending
|
|
804
910
|
? "De AI-dienst was even bezet — we proberen deze mail automatisch opnieuw. Je kunt de velden intussen zelf invullen."
|
|
805
|
-
: "Deze mail is niet automatisch uitgelezen — vul de velden handmatig in." })), matchWarning && (_jsx("div", { className: "rounded-md border border-
|
|
911
|
+
: "Deze mail is niet automatisch uitgelezen — vul de velden handmatig in." })), matchWarning && (_jsx("div", { className: "rounded-md border border-warning/30 bg-warning-soft px-3 py-2 text-xs text-warning-soft-foreground", children: matchWarning })), customerField && renderField(customerField), _jsx(ConfBadge, { meta: metaFor("customer"), thresholds: thresholds }), customerNotLocal && (_jsxs("div", { className: "rounded-md border border-warning/30 bg-warning-soft px-3 py-2 text-xs text-warning-soft-foreground 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
|
|
806
912
|
? "Selecteer eerst een klant"
|
|
807
913
|
: deliveryLocations.length
|
|
808
914
|
? "Kies locatie..."
|
|
@@ -824,7 +930,7 @@ function ProposalEditor({ proposal, template, customers, products, allowBackorde
|
|
|
824
930
|
: "Kies vervoerder...", clearLabel: "\u2014 Geen \u2014", disabled: !deliveryLocationVal, options: carriers.map((c) => ({
|
|
825
931
|
value: c.id,
|
|
826
932
|
label: c.name + (c.is_default ? " (standaard)" : ""),
|
|
827
|
-
})), 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-
|
|
933
|
+
})), 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-primary/30 bg-primary/10 px-3 py-2 text-xs text-primary", 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)] })), preview?.ok === true && previewKind === "late_signup" && (_jsx(LateSignupChoice, { open: lateOpen, setOpen: setLateOpen, reason: lateReason, setReason: setLateReason }))] }))] }), _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)] }));
|
|
828
934
|
})(), _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 text-center", children: stickerAvailable ? "Sticker" : "" })] }), lines.map((line, idx) => {
|
|
829
935
|
const product = line.product_id ? products.find((p) => p.id === line.product_id) : null;
|
|
830
936
|
const stdBelading = defaultBelading(product);
|
|
@@ -924,7 +1030,7 @@ function ProposalEditor({ proposal, template, customers, products, allowBackorde
|
|
|
924
1030
|
const pl = toPlants(next[idx].qty_in_mail, kind, effBelading);
|
|
925
1031
|
next[idx] = { ...next[idx], unit_kind: kind, quantity: pl ?? next[idx].quantity };
|
|
926
1032
|
setLines(next);
|
|
927
|
-
}, 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-
|
|
1033
|
+
}, 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-warning-soft-foreground", children: w }, w)))] }), _jsx(Input, { className: "col-span-2", type: "number", min: 1, step: 1, inputMode: "numeric", onKeyDown: blockDecimal, value: cratesDisplay, onChange: (e) => {
|
|
928
1034
|
const next = [...lines];
|
|
929
1035
|
const raw = e.target.value.replace(/[.,]/g, "");
|
|
930
1036
|
const cr = raw ? Math.max(1, Math.floor(Number(raw))) : null;
|
|
@@ -938,7 +1044,7 @@ function ProposalEditor({ proposal, template, customers, products, allowBackorde
|
|
|
938
1044
|
} }), (() => {
|
|
939
1045
|
const ag = line.product_id ? agreements[line.product_id] : null;
|
|
940
1046
|
const src = line.price_source;
|
|
941
|
-
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-
|
|
1047
|
+
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-warning-soft-foreground", 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-warning-soft-foreground", children: ["Afspraak: ", euro(ag.net_price), " ", _jsx("button", { type: "button", className: "underline", onClick: () => {
|
|
942
1048
|
const next = [...lines];
|
|
943
1049
|
next[idx] = { ...next[idx], unit_price: ag.net_price, price_source: "afspraak" };
|
|
944
1050
|
setLines(next);
|
|
@@ -948,13 +1054,13 @@ function ProposalEditor({ proposal, template, customers, products, allowBackorde
|
|
|
948
1054
|
next[idx] = { ...next[idx], needs_sticker: e.target.checked };
|
|
949
1055
|
setLines(next);
|
|
950
1056
|
} }), "Sticker"] })), _jsx(Button, { variant: "ghost", size: "icon", onClick: () => setLines(lines.filter((_, i) => i !== idx)), children: _jsx(Trash2, { className: "h-4 w-4" }) })] })] }, idx));
|
|
951
|
-
}), _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-
|
|
1057
|
+
}), _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-warning/30 bg-warning-soft px-3 py-2 text-xs text-warning-soft-foreground", children: ["Geen prijs bekend voor ", linesWithoutPrice, " regel(s). Vul een prijs in; zonder prijs kan de bestelling niet worden aangemaakt."] })), zeroPriceLines > 0 && (_jsxs("div", { className: "rounded-md border border-destructive/40 bg-destructive/5 px-3 py-2 text-xs text-destructive space-y-1", children: [_jsxs("div", { children: [zeroPriceLines, " regel(s) met prijs \u20AC 0. Vul de prijs in, of bevestig dat \u20AC 0 klopt."] }), _jsxs("label", { className: "flex items-center gap-2 text-foreground", children: [_jsx("input", { type: "checkbox", className: "h-4 w-4 accent-primary", checked: zeroConfirmed, onChange: (e) => setZeroConfirmed(e.target.checked) }), _jsx("span", { children: "Prijs \u20AC 0 klopt" }), "Prijs \u20AC 0 klopt"] })] }))] }), refusal && (_jsxs("div", { className: "rounded-md border border-destructive/40 bg-destructive/5 px-3 py-2 text-sm space-y-2", children: [_jsx("div", { className: "font-medium text-destructive", children: "Bestelling niet aangemaakt \u2014 het voorstel blijft open." }), _jsx("div", { className: "text-destructive", children: refusal.message }), _jsx("div", { className: "flex flex-wrap gap-2", children: refusal.nextDeliveryDate && refusal.nextDeliveryDate !== deliveryVal && (_jsxs(Button, { size: "sm", variant: "outline", onClick: () => setVal("delivery_date", refusal.nextDeliveryDate), children: ["Volgend haalbaar moment: ", fmtDate(refusal.nextDeliveryDate)] })) }), refusal.kind === "late_signup" && (_jsx(LateSignupChoice, { open: lateOpen, setOpen: setLateOpen, reason: lateReason, setReason: setLateReason }))] })), _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) => {
|
|
952
1058
|
if (submitting)
|
|
953
1059
|
return;
|
|
954
1060
|
setConfirmOpen(o);
|
|
955
1061
|
if (!o)
|
|
956
1062
|
setCheckedOk(false);
|
|
957
|
-
}, 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)] })] }), lateOverride && (_jsxs("div", { className: "rounded-md border border-
|
|
1063
|
+
}, 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)] })] }), lateOverride && (_jsxs("div", { className: "rounded-md border border-warning/30 bg-warning-soft p-2 text-warning-soft-foreground", children: ["Je maakt de order toch aan na de aanmeldtijd. Reden: ", lateReason.trim()] })), _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); } })] })] })] }));
|
|
958
1064
|
}
|
|
959
1065
|
/** Verplichte bevestiging: externe klant eerst als lokale klant aanmaken. */
|
|
960
1066
|
function NewCustomerDialog({ candidate, onClose, onCreated, }) {
|
|
@@ -1009,7 +1115,8 @@ function NewCustomerDialog({ candidate, onClose, onCreated, }) {
|
|
|
1009
1115
|
onCreated(res.id);
|
|
1010
1116
|
}
|
|
1011
1117
|
catch (e) {
|
|
1012
|
-
|
|
1118
|
+
console.warn("[mailbox] klant aanmaken", e);
|
|
1119
|
+
toast.error("Klant aanmaken lukt niet. Probeer het opnieuw.");
|
|
1013
1120
|
}
|
|
1014
1121
|
finally {
|
|
1015
1122
|
setBusy(false);
|
|
@@ -1019,8 +1126,8 @@ function NewCustomerDialog({ candidate, onClose, onCreated, }) {
|
|
|
1019
1126
|
onClose(); }, children: _jsxs(DialogContent, { className: "max-w-xl", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: "Nieuwe klant aanmaken" }) }), _jsxs("p", { className: "text-sm text-muted-foreground", children: ["Deze klant bestaat alleen in ", (candidate?.providers ?? []).map(providerLabel).join(" + ") || "een externe bron", ". Controleer de gegevens en maak de klant aan voordat je de bestelling verwerkt."] }), _jsxs("div", { className: "grid grid-cols-2 gap-3 py-2", children: [field("companyName", "Bedrijfsnaam", true), field("email", "E-mail"), field("street", "Straat"), field("houseNumber", "Huisnummer"), field("postalCode", "Postcode"), field("city", "Plaats"), field("country", "Land"), field("gln", "GLN"), field("phone", "Telefoon")] }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { className: "flex-1", disabled: busy, onClick: submit, children: busy ? "Bezig…" : "Klant aanmaken en koppelen" }), _jsx(Button, { variant: "outline", disabled: busy, onClick: onClose, children: "Annuleren" })] })] }) }));
|
|
1020
1127
|
}
|
|
1021
1128
|
function DynamicField({ field, value, meta, onChange, customers, products, plantTypes, suppliers, onSelectCustomer, }) {
|
|
1022
|
-
const reviewClass = meta?.needs_review ? "border-
|
|
1023
|
-
const label = (_jsxs(Label, { className: "flex items-center gap-2", children: [field.label, field.required && _jsx("span", { className: "text-destructive", children: "*" }), meta?.needs_review && (_jsx(Badge, { variant: "outline", className: "text-xs border-
|
|
1129
|
+
const reviewClass = meta?.needs_review ? "border-warning/30 ring-1 ring-warning/40 rounded-md p-2" : "";
|
|
1130
|
+
const label = (_jsxs(Label, { className: "flex items-center gap-2", children: [field.label, field.required && _jsx("span", { className: "text-destructive", children: "*" }), meta?.needs_review && (_jsx(Badge, { variant: "outline", className: "text-xs border-warning/30 text-warning-soft-foreground", children: "Controleren" })), typeof meta?.ai_confidence === "number" && (_jsxs("span", { className: "text-xs text-muted-foreground", children: [Math.round(meta.ai_confidence * 100), "%"] }))] }));
|
|
1024
1131
|
const renderControl = () => {
|
|
1025
1132
|
switch (field.field_type) {
|
|
1026
1133
|
case "text":
|
|
@@ -1066,9 +1173,9 @@ function ConfBadge({ meta, thresholds, }) {
|
|
|
1066
1173
|
return null;
|
|
1067
1174
|
const c = Math.max(0, Math.min(1, Number(meta.confidence ?? 0)));
|
|
1068
1175
|
const cls = c >= thresholds.green
|
|
1069
|
-
? "border-
|
|
1176
|
+
? "border-primary/30 bg-primary/10 text-primary"
|
|
1070
1177
|
: c >= thresholds.orange
|
|
1071
|
-
? "border-
|
|
1178
|
+
? "border-warning/30 bg-warning-soft text-warning-soft-foreground"
|
|
1072
1179
|
: "border-destructive/40 bg-destructive/5 text-destructive";
|
|
1073
1180
|
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] }));
|
|
1074
1181
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MailboxSkippedPage.d.ts","sourceRoot":"","sources":["../../src/components/MailboxSkippedPage.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"MailboxSkippedPage.d.ts","sourceRoot":"","sources":["../../src/components/MailboxSkippedPage.tsx"],"names":[],"mappings":"AAuCA,wBAAgB,kBAAkB,gCAyLjC"}
|