@flowselections/mailbox-orders 1.0.53 → 1.0.57
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 +3 -3
- package/dist-lib/components/MailboxAuthBanner.d.ts.map +1 -1
- package/dist-lib/components/MailboxAuthBanner.js +20 -7
- package/dist-lib/components/MailboxPageFrame.d.ts +25 -0
- package/dist-lib/components/MailboxPageFrame.d.ts.map +1 -0
- package/dist-lib/components/MailboxPageFrame.js +103 -0
- package/dist-lib/components/MailboxProcessedPage.d.ts.map +1 -1
- package/dist-lib/components/MailboxProcessedPage.js +102 -61
- package/dist-lib/components/MailboxProposalsPage.d.ts.map +1 -1
- package/dist-lib/components/MailboxProposalsPage.js +191 -76
- package/dist-lib/components/MailboxSkippedPage.d.ts.map +1 -1
- package/dist-lib/components/MailboxSkippedPage.js +97 -19
- package/dist-lib/components/MailboxStatusLine.d.ts +8 -2
- package/dist-lib/components/MailboxStatusLine.d.ts.map +1 -1
- package/dist-lib/components/MailboxStatusLine.js +54 -8
- package/dist-lib/components/TablePagination.d.ts +2 -2
- package/dist-lib/components/TablePagination.d.ts.map +1 -1
- package/dist-lib/components/TablePagination.js +8 -12
- package/dist-lib/components/fs-ui/DataTable.d.ts +42 -0
- package/dist-lib/components/fs-ui/DataTable.d.ts.map +1 -0
- package/dist-lib/components/fs-ui/DataTable.js +40 -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/FilterPanel.d.ts +32 -0
- package/dist-lib/components/fs-ui/FilterPanel.d.ts.map +1 -0
- package/dist-lib/components/fs-ui/FilterPanel.js +20 -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 +6 -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/NoticeBar.d.ts +18 -0
- package/dist-lib/components/fs-ui/NoticeBar.d.ts.map +1 -0
- package/dist-lib/components/fs-ui/NoticeBar.js +24 -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 +16 -0
- package/dist-lib/components/fs-ui/PillTabs.d.ts.map +1 -0
- package/dist-lib/components/fs-ui/PillTabs.js +29 -0
- package/dist-lib/components/fs-ui/RowMenu.d.ts +13 -0
- package/dist-lib/components/fs-ui/RowMenu.d.ts.map +1 -0
- package/dist-lib/components/fs-ui/RowMenu.js +10 -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 +38 -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 +13 -0
- package/dist-lib/components/fs-ui/Toolbar.d.ts.map +1 -0
- package/dist-lib/components/fs-ui/Toolbar.js +10 -0
- package/dist-lib/components/fs-ui/UpdatedAt.d.ts +8 -0
- package/dist-lib/components/fs-ui/UpdatedAt.d.ts.map +1 -0
- package/dist-lib/components/fs-ui/UpdatedAt.js +17 -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.js +1 -1
- 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 +372 -0
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/lib/format-date.d.ts +5 -0
- package/dist-lib/lib/format-date.d.ts.map +1 -0
- package/dist-lib/lib/format-date.js +30 -0
- package/dist-lib/lib/labels.d.ts +15 -0
- package/dist-lib/lib/labels.d.ts.map +1 -0
- package/dist-lib/lib/labels.js +57 -0
- 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 +61 -0
- package/dist-lib/lib/mailbox.functions.js +3 -3
- package/package.json +1 -1
|
@@ -2,18 +2,31 @@ 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, 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, CellText } from "./fs-ui/DataTable.js";
|
|
17
|
+
import { FilterPanel, FilterOptions, PERIOD_OPTIONS } from "./fs-ui/FilterPanel.js";
|
|
18
|
+
import { fmtDayTime } from "../lib/format-date.js";
|
|
19
|
+
import { StatusIcon } from "./fs-ui/StatusIcon.js";
|
|
20
|
+
import { EmptyState } from "./fs-ui/EmptyState.js";
|
|
21
|
+
import { TableSkeleton } from "./fs-ui/TableSkeleton.js";
|
|
22
|
+
import { Toolbar } from "./fs-ui/Toolbar.js";
|
|
23
|
+
import { SearchInput } from "./fs-ui/SearchInput.js";
|
|
24
|
+
import { FilterChips } from "./fs-ui/FilterChips.js";
|
|
25
|
+
import { MobilePrimaryAction } from "./fs-ui/MobilePrimaryAction.js";
|
|
26
|
+
import { useListHotkeys } from "./fs-ui/useListHotkeys.js";
|
|
14
27
|
import { EmailPreview } from "./EmailPreview.js";
|
|
15
28
|
import { TablePagination } from "./TablePagination.js";
|
|
16
|
-
import {
|
|
29
|
+
import { usePersistedState } from "./MailboxFilterBar.js";
|
|
17
30
|
import { supabase } from "../integrations/supabase/client.js";
|
|
18
31
|
import { canOverrideLateSignup, checkLinePrices } from "../lib/verkoop-order.js";
|
|
19
32
|
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 +92,7 @@ function resolveCustomerId(raw, customers, fallbackId) {
|
|
|
79
92
|
}
|
|
80
93
|
export function MailboxProposalsPage() {
|
|
81
94
|
const qc = useQueryClient();
|
|
82
|
-
const fetchList = useServerFn(
|
|
95
|
+
const fetchList = useServerFn(listProposalsPage);
|
|
83
96
|
const fetchTemplate = useServerFn(listTemplateFields);
|
|
84
97
|
const fetchApprove = useServerFn(approveProposal);
|
|
85
98
|
const fetchReject = useServerFn(rejectProposal);
|
|
@@ -91,6 +104,11 @@ export function MailboxProposalsPage() {
|
|
|
91
104
|
const fetchPickupLocations = useServerFn(listPickupLocations);
|
|
92
105
|
const fetchActive = useServerFn(getActiveOrdersMailbox);
|
|
93
106
|
const fetchSync = useServerFn(syncMailboxNow);
|
|
107
|
+
const navigate = useNavigate();
|
|
108
|
+
/** Knop "Open order" in de melding na bevestigen (route van Verkooporders). */
|
|
109
|
+
const openOrderAction = (orderId) => orderId
|
|
110
|
+
? { action: { label: "Open order", onClick: () => void navigate({ to: `/floriday-verkoop/verkooporders/${orderId}` }) } }
|
|
111
|
+
: undefined;
|
|
94
112
|
// Server fns met requireSupabaseAuth gooien een raw Response bij een stale
|
|
95
113
|
// bearer-token. Die bubbelt op als "[object Response]" en blankt het scherm.
|
|
96
114
|
// Vang elke fout en geef een lege fallback terug — UI blijft bruikbaar.
|
|
@@ -106,11 +124,21 @@ export function MailboxProposalsPage() {
|
|
|
106
124
|
// zodra de dialoog voor het eerst opengaat, en blijven daarna gecachet.
|
|
107
125
|
const [activeId, setActiveId] = useState(null);
|
|
108
126
|
const [page, setPage] = useState(1);
|
|
109
|
-
const [pageSize, setPageSize] = useState(
|
|
127
|
+
const [pageSize, setPageSize] = useState(50);
|
|
110
128
|
const [search, setSearch] = usePersistedState("mailbox-proposals:search", "");
|
|
111
129
|
const [period, setPeriod] = usePersistedState("mailbox-proposals:period", "all");
|
|
112
130
|
const [sortOrder, setSortOrder] = usePersistedState("mailbox-proposals:sort", "date-desc");
|
|
113
|
-
const
|
|
131
|
+
const [listFilter, setListFilter] = usePersistedState("mailbox-proposals:filter", null);
|
|
132
|
+
const [cursor, setCursor] = useState(null);
|
|
133
|
+
const [helpOpen, setHelpOpen] = useState(false);
|
|
134
|
+
const [filtersOpen, setFiltersOpen] = useState(false);
|
|
135
|
+
const searchRef = useRef(null);
|
|
136
|
+
// Zware keuzelijsten (klanten, artikelen, …) pas laden zodra een voorstel
|
|
137
|
+
// voor het eerst geopend wordt; daarna blijven ze in de cache.
|
|
138
|
+
const [refsWanted, setRefsWanted] = useState(false);
|
|
139
|
+
useEffect(() => { if (activeId)
|
|
140
|
+
setRefsWanted(true); }, [activeId]);
|
|
141
|
+
const refsEnabled = refsWanted;
|
|
114
142
|
const refOpts = {
|
|
115
143
|
enabled: refsEnabled,
|
|
116
144
|
staleTime: 5 * 60000,
|
|
@@ -120,11 +148,23 @@ export function MailboxProposalsPage() {
|
|
|
120
148
|
// Bestelvoorstellen toont uitsluitend nieuwe, nog niet gecontroleerde
|
|
121
149
|
// bestellingen ("Te controleren"). Goedgekeurd/afgewezen verschijnt in
|
|
122
150
|
// de module Verwerkt.
|
|
123
|
-
const
|
|
124
|
-
|
|
151
|
+
const listParams = {
|
|
152
|
+
statuses: ["pending"],
|
|
153
|
+
search,
|
|
154
|
+
period,
|
|
155
|
+
sort: sortOrder === "date-asc" ? "asc" : "desc",
|
|
156
|
+
dateField: "received",
|
|
157
|
+
tz: browserTz(),
|
|
158
|
+
filter: listFilter,
|
|
159
|
+
page,
|
|
160
|
+
pageSize,
|
|
161
|
+
};
|
|
162
|
+
const { data, isLoading, error: listError, refetch: refetchList, dataUpdatedAt } = useQuery({
|
|
163
|
+
queryKey: ["mailbox-proposals", "pending", listParams],
|
|
125
164
|
// Bewust NIET stilzwijgend opvangen: als de server tijdelijk onbereikbaar is
|
|
126
165
|
// moet de gebruiker dat zien in plaats van een lege lijst ("geen mailtjes").
|
|
127
|
-
queryFn: () => fetchList({ data:
|
|
166
|
+
queryFn: () => fetchList({ data: listParams }),
|
|
167
|
+
placeholderData: (prev) => prev,
|
|
128
168
|
retry: 3,
|
|
129
169
|
retryDelay: (a) => Math.min(1000 * 2 ** a, 8000),
|
|
130
170
|
refetchInterval: 60000,
|
|
@@ -171,6 +211,7 @@ export function MailboxProposalsPage() {
|
|
|
171
211
|
onSuccess: (res) => {
|
|
172
212
|
if (res?.ok && ((res.inserted ?? 0) > 0 || (res.processed ?? 0) > 0)) {
|
|
173
213
|
qc.invalidateQueries({ queryKey: ["mailbox-proposals"] });
|
|
214
|
+
qc.invalidateQueries({ queryKey: ["mailbox-kpis"] });
|
|
174
215
|
}
|
|
175
216
|
},
|
|
176
217
|
});
|
|
@@ -221,8 +262,9 @@ export function MailboxProposalsPage() {
|
|
|
221
262
|
},
|
|
222
263
|
onSuccess: (res) => {
|
|
223
264
|
qc.invalidateQueries({ queryKey: ["mailbox-proposals"] });
|
|
265
|
+
qc.invalidateQueries({ queryKey: ["mailbox-kpis"] });
|
|
224
266
|
void refetchOutdated();
|
|
225
|
-
toast.success(`${res?.reparsed ?? 0}
|
|
267
|
+
toast.success(`${res?.reparsed ?? 0} bestelling(en) opnieuw uitgelezen`);
|
|
226
268
|
},
|
|
227
269
|
});
|
|
228
270
|
// Handmatig één voorstel opnieuw laten analyseren door de AI-agent.
|
|
@@ -237,22 +279,36 @@ export function MailboxProposalsPage() {
|
|
|
237
279
|
if (res?.ok) {
|
|
238
280
|
setReparseError(null);
|
|
239
281
|
qc.invalidateQueries({ queryKey: ["mailbox-proposals"] });
|
|
282
|
+
qc.invalidateQueries({ queryKey: ["mailbox-kpis"] });
|
|
240
283
|
qc.invalidateQueries({ queryKey: ["mailbox-proposal-detail"] });
|
|
241
|
-
toast.success("
|
|
284
|
+
toast.success("Mail opnieuw uitgelezen — gegevens bijgewerkt");
|
|
242
285
|
}
|
|
243
286
|
else {
|
|
244
|
-
|
|
287
|
+
if (res?.error)
|
|
288
|
+
console.warn("[mailbox] opnieuw uitlezen", res.error);
|
|
289
|
+
const msg = "Opnieuw uitlezen lukt niet. Probeer het over een paar minuten opnieuw.";
|
|
245
290
|
setReparseError(msg);
|
|
246
291
|
toast.error(msg);
|
|
247
292
|
}
|
|
248
293
|
},
|
|
249
294
|
onError: (e) => {
|
|
250
|
-
|
|
295
|
+
console.warn("[mailbox] opnieuw uitlezen", e);
|
|
296
|
+
const msg = "Opnieuw uitlezen lukt niet. Probeer het over een paar minuten opnieuw.";
|
|
251
297
|
setReparseError(msg);
|
|
252
298
|
toast.error(msg);
|
|
253
299
|
},
|
|
254
300
|
});
|
|
255
|
-
|
|
301
|
+
// Platte rijen uit de server terugzetten naar de vorm die de rest van de
|
|
302
|
+
// pagina (klantnaam, detailkop) al gebruikt.
|
|
303
|
+
const items = useMemo(() => (Array.isArray(data?.items) ? data.items : []).map((r) => ({
|
|
304
|
+
...r,
|
|
305
|
+
mailbox_messages: {
|
|
306
|
+
subject: r.subject, from_email: r.from_email, from_name: r.from_name,
|
|
307
|
+
received_at: r.received_at, connection_id: r.connection_id,
|
|
308
|
+
account_email: r.account_email, preview: String(r.preview ?? "").slice(0, 180),
|
|
309
|
+
},
|
|
310
|
+
})), [data]);
|
|
311
|
+
const total = Number(data?.total ?? 0);
|
|
256
312
|
const template = Array.isArray(templateData?.items) ? templateData.items : [];
|
|
257
313
|
const customers = Array.isArray(customersData?.items) ? customersData.items : [];
|
|
258
314
|
const products = Array.isArray(productsData?.items) ? productsData.items : [];
|
|
@@ -279,43 +335,99 @@ export function MailboxProposalsPage() {
|
|
|
279
335
|
}
|
|
280
336
|
return p.mailbox_messages?.from_name || p.mailbox_messages?.from_email || "Onbekend";
|
|
281
337
|
};
|
|
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));
|
|
338
|
+
// --- Zoeken, filteren en sorteren gebeurt op de server -----------------
|
|
339
|
+
const hasActiveFilters = !!search || period !== "all" || !!listFilter;
|
|
340
|
+
const clearFilters = () => { setSearch(""); setPeriod("all"); setSortOrder("date-desc"); setListFilter(null); };
|
|
341
|
+
useEffect(() => { setPage(1); setCursor(null); }, [search, period, sortOrder, listFilter]);
|
|
342
|
+
const pageCount = Math.max(1, Math.ceil(total / pageSize));
|
|
310
343
|
const currentPage = Math.min(page, pageCount);
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
344
|
+
// Oudste open voorstel openen (knop "Volgende controleren" op mobiel).
|
|
345
|
+
const openOldest = async () => {
|
|
346
|
+
try {
|
|
347
|
+
const res = await fetchList({ data: { statuses: ["pending"], sort: "asc", page: 1, pageSize: 10, tz: browserTz() } });
|
|
348
|
+
const first = Array.isArray(res?.items) ? res.items[0] : null;
|
|
349
|
+
if (first?.id)
|
|
350
|
+
setActiveId(first.id);
|
|
351
|
+
else
|
|
352
|
+
toast.info("Er zijn geen bestellingen meer om te controleren.");
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
toast.error("Openen lukt niet. Probeer het opnieuw.");
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
useListHotkeys({
|
|
359
|
+
disabled: !!activeId || helpOpen || filtersOpen,
|
|
360
|
+
onSearch: () => searchRef.current?.focus(),
|
|
361
|
+
onNext: () => setCursor((c) => (items.length ? Math.min((c ?? -1) + 1, items.length - 1) : null)),
|
|
362
|
+
onPrev: () => setCursor((c) => (items.length ? Math.max((c ?? 1) - 1, 0) : null)),
|
|
363
|
+
onOpen: cursor != null && items[cursor] ? () => setActiveId(items[cursor].id) : undefined,
|
|
364
|
+
onEscape: hasActiveFilters ? clearFilters : undefined,
|
|
365
|
+
onHelp: () => setHelpOpen(true),
|
|
366
|
+
});
|
|
367
|
+
const periodLabel = { today: "Vandaag", "7d": "Laatste 7 dagen", "30d": "Laatste 30 dagen" };
|
|
368
|
+
const chips = [
|
|
369
|
+
...(period !== "all" ? [{ key: "period", label: periodLabel[period] ?? period, icon: _jsx(CalendarClock, {}), onRemove: () => setPeriod("all") }] : []),
|
|
370
|
+
...(listFilter === "klant_onbekend" ? [{ key: "klant", label: "Klant onbekend", icon: _jsx(UserX, {}), onRemove: () => setListFilter(null) }] : []),
|
|
371
|
+
];
|
|
372
|
+
const activeKpi = listFilter === "klant_onbekend" ? "customer_unknown" : period === "today" ? "today" : null;
|
|
373
|
+
const columns = [
|
|
374
|
+
{
|
|
375
|
+
key: "klant", header: "Klant", width: 220,
|
|
376
|
+
cell: (p) => {
|
|
377
|
+
const name = customerNameFor(p);
|
|
378
|
+
return (_jsxs("div", { className: "flex min-w-0 items-center gap-3", children: [_jsx(InitialsAvatar, { name: name }), _jsx(CellText, { primary: name, secondary: p.customer_name ? undefined : "Klant nog niet herkend", strong: true })] }));
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
key: "bericht", header: "Bericht", width: 320,
|
|
383
|
+
cell: (p) => _jsx(CellText, { primary: p.mailbox_messages?.subject || "(geen onderwerp)", secondary: p.mailbox_messages?.preview || "Geen voorbeeldtekst" }),
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
key: "ontvangen_op", header: "Mailadres", width: 180,
|
|
387
|
+
cell: (p) => _jsx(CellText, { primary: p.mailbox_messages?.account_email || "–" }),
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
key: "datum", header: "Ontvangen", width: 132, sortable: true,
|
|
391
|
+
cell: (p) => _jsx("span", { className: "block truncate tabular-nums", children: fmtDayTime(p.mailbox_messages?.received_at ?? p.created_at) }),
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
key: "status", header: "Status", width: 120,
|
|
395
|
+
cell: (p) => (_jsxs("div", { className: "flex items-center gap-2", children: [p.readiness === "klaar"
|
|
396
|
+
? _jsx(StatusIcon, { tone: "done", label: "Klaar om te bevestigen" })
|
|
397
|
+
: _jsx(StatusIcon, { tone: "open", label: "Te controleren", detail: "Controleer de gegevens en bevestig de bestelling." }), p.duplicate && _jsx(StatusIcon, { tone: "busy", label: "Mogelijk dubbel", detail: "Deze mail lijkt op een eerdere bestelling." })] })),
|
|
398
|
+
},
|
|
399
|
+
];
|
|
400
|
+
const pageNotices = outdatedCount > 0 ? [{
|
|
401
|
+
key: "outdated",
|
|
402
|
+
tone: "warning",
|
|
403
|
+
title: `${outdatedCount} ${outdatedCount === 1 ? "bestelling is" : "bestellingen zijn"} met de vorige instelling uitgelezen.`,
|
|
404
|
+
action: (_jsx(Button, { size: "sm", variant: "outline", disabled: agentRefreshMut.isPending, onClick: () => agentRefreshMut.mutate(), children: agentRefreshMut.isPending ? "Bezig…" : "Opnieuw uitlezen" })),
|
|
405
|
+
}] : [];
|
|
406
|
+
return (_jsxs(MailboxPageFrame, { tab: "voorstellen", helpOpen: helpOpen, onHelpOpenChange: setHelpOpen, activeKpi: activeKpi, notices: pageNotices, onKpi: (k) => {
|
|
407
|
+
if (k === "today") {
|
|
408
|
+
setListFilter(null);
|
|
409
|
+
setPeriod("today");
|
|
410
|
+
return true;
|
|
411
|
+
}
|
|
412
|
+
if (k === "customer_unknown") {
|
|
413
|
+
setPeriod("all");
|
|
414
|
+
setListFilter("klant_onbekend");
|
|
415
|
+
return true;
|
|
416
|
+
}
|
|
417
|
+
if (k === "pending") {
|
|
418
|
+
clearFilters();
|
|
419
|
+
return true;
|
|
420
|
+
}
|
|
421
|
+
return false;
|
|
422
|
+
}, children: [_jsx(Toolbar, { onOpenFilters: () => setFiltersOpen(true), filterCount: chips.length, chips: _jsx(FilterChips, { chips: chips, onClearAll: clearFilters }), children: _jsx(SearchInput, { ref: searchRef, value: search, onChange: setSearch, placeholder: "Zoek op klant, afzender of onderwerp" }) }), _jsxs(FilterPanel, { open: filtersOpen, onOpenChange: setFiltersOpen, onClear: clearFilters, children: [_jsx(FilterOptions, { label: "Periode", value: period, options: PERIOD_OPTIONS, onChange: (v) => setPeriod(v) }), _jsx(FilterOptions, { label: "Klant", value: listFilter ?? "all", options: [{ value: "all", label: "Alle klanten" }, { value: "klant_onbekend", label: "Klant onbekend" }], onChange: (v) => setListFilter(v === "klant_onbekend" ? "klant_onbekend" : null) })] }), isLoading && !data ? (_jsx(TableSkeleton, { columns: [220, 320, 180, 132, 120] })) : listError && !data ? (_jsx("div", { className: "rounded-xl border border-border bg-card shadow-sm", children: _jsx(EmptyState, { icon: _jsx(Inbox, { className: "h-6 w-6" }), title: "De lijst kon niet worden geladen", text: "Je bestellingen zijn er nog; ze worden alleen nu niet getoond.", action: _jsx(Button, { size: "sm", variant: "outline", onClick: () => void refetchList(), children: "Opnieuw proberen" }) }) })) : 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) => {
|
|
423
|
+
const d = p.mailbox_messages?.received_at ?? p.created_at;
|
|
424
|
+
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"
|
|
425
|
+
? _jsx(StatusIcon, { tone: "done", label: "Klaar om te bevestigen" })
|
|
426
|
+
: _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: [fmtDayTime(d), p.duplicate ? " · mogelijk dubbel" : ""] })] }));
|
|
427
|
+
}, 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
428
|
setActiveId(null);
|
|
317
429
|
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 ? "
|
|
430
|
+
} }, 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
431
|
try {
|
|
320
432
|
const res = await fetchApprove({ data: payload });
|
|
321
433
|
if (res?.ok === false) {
|
|
@@ -323,35 +435,40 @@ export function MailboxProposalsPage() {
|
|
|
323
435
|
return res;
|
|
324
436
|
}
|
|
325
437
|
if (res?.alreadyApproved) {
|
|
326
|
-
toast.info(`Er bestaat al een order voor
|
|
438
|
+
toast.info(`Er bestaat al een order voor deze bestelling (${res.orderNumber}).`, openOrderAction(res.orderId));
|
|
327
439
|
qc.invalidateQueries({ queryKey: ["mailbox-proposals"] });
|
|
440
|
+
qc.invalidateQueries({ queryKey: ["mailbox-kpis"] });
|
|
328
441
|
setActiveId(null);
|
|
329
442
|
return res;
|
|
330
443
|
}
|
|
331
444
|
const shortages = Array.isArray(res?.shortages) ? res.shortages : [];
|
|
332
|
-
toast.success(
|
|
445
|
+
toast.success(`Order ${res?.orderNumber ?? ""} aangemaakt.`.replace(" ", " "), openOrderAction(res?.orderId));
|
|
333
446
|
if (shortages.length > 0) {
|
|
334
447
|
toast.warning(`Tekorten (levering zonder voorraad staat aan): ${shortages
|
|
335
448
|
.map((s) => `${s.product ?? s.productId}: besteld ${s.wanted}, beschikbaar ${s.available}, tekort ${s.missing}`)
|
|
336
449
|
.join(" · ")}`);
|
|
337
450
|
}
|
|
338
451
|
qc.invalidateQueries({ queryKey: ["mailbox-proposals"] });
|
|
452
|
+
qc.invalidateQueries({ queryKey: ["mailbox-kpis"] });
|
|
339
453
|
setActiveId(null);
|
|
340
454
|
return res;
|
|
341
455
|
}
|
|
342
456
|
catch (e) {
|
|
343
|
-
|
|
344
|
-
|
|
457
|
+
console.warn("[mailbox] bevestigen", e);
|
|
458
|
+
toast.error("Bevestigen lukt niet. Probeer het opnieuw.");
|
|
459
|
+
return { ok: false, reason: "other", message: "Bevestigen lukt niet. Probeer het opnieuw." };
|
|
345
460
|
}
|
|
346
461
|
}, onReject: async (reason) => {
|
|
347
462
|
try {
|
|
348
463
|
await fetchReject({ data: { proposalId: active.id, reason } });
|
|
349
|
-
toast.success("
|
|
464
|
+
toast.success("Bestelling afgewezen");
|
|
350
465
|
qc.invalidateQueries({ queryKey: ["mailbox-proposals"] });
|
|
466
|
+
qc.invalidateQueries({ queryKey: ["mailbox-kpis"] });
|
|
351
467
|
setActiveId(null);
|
|
352
468
|
}
|
|
353
469
|
catch (e) {
|
|
354
|
-
|
|
470
|
+
console.warn("[mailbox] afwijzen", e);
|
|
471
|
+
toast.error("Afwijzen lukt niet. Probeer het opnieuw.");
|
|
355
472
|
}
|
|
356
473
|
} }, active.id) })] })) }) })] }));
|
|
357
474
|
}
|
|
@@ -678,11 +795,11 @@ function ProposalEditor({ proposal, template, customers, products, allowBackorde
|
|
|
678
795
|
return null;
|
|
679
796
|
}
|
|
680
797
|
if (!deliveryLocationVal) {
|
|
681
|
-
toast.error("
|
|
798
|
+
toast.error("Kies een afleverlocatie.");
|
|
682
799
|
return null;
|
|
683
800
|
}
|
|
684
801
|
if (!pickupLocationVal) {
|
|
685
|
-
toast.error("
|
|
802
|
+
toast.error("Kies een afhaallocatie (eigen locatie of tuin).");
|
|
686
803
|
return null;
|
|
687
804
|
}
|
|
688
805
|
const validLines = lines.filter((l) => l.product_id && (l.quantity ?? 0) > 0);
|
|
@@ -773,7 +890,7 @@ function ProposalEditor({ proposal, template, customers, products, allowBackorde
|
|
|
773
890
|
setSubmitting(false);
|
|
774
891
|
}
|
|
775
892
|
};
|
|
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-
|
|
893
|
+
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
894
|
// Klantveld staat altijd bovenaan, ook als het template (nog) leeg is.
|
|
778
895
|
const customerField = visibleFields.find((f) => f.key === "customer") ?? {
|
|
779
896
|
id: "customer",
|
|
@@ -800,9 +917,9 @@ function ProposalEditor({ proposal, template, customers, products, allowBackorde
|
|
|
800
917
|
const metaFor = (key) => fieldMeta[key] ?? (key === "customer" && proposal.matched_customer_id
|
|
801
918
|
? { confidence: payload.auto_match_warning ? 0.6 : 1, source: payload.auto_match_forwarded ? "afgeleid" : "uit mail" }
|
|
802
919
|
: null);
|
|
803
|
-
return (_jsxs(_Fragment, { children: [dup && (_jsxs("div", { className: "rounded-md border border-
|
|
920
|
+
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
921
|
? "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-
|
|
922
|
+
: "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
923
|
? "Selecteer eerst een klant"
|
|
807
924
|
: deliveryLocations.length
|
|
808
925
|
? "Kies locatie..."
|
|
@@ -824,7 +941,7 @@ function ProposalEditor({ proposal, template, customers, products, allowBackorde
|
|
|
824
941
|
: "Kies vervoerder...", clearLabel: "\u2014 Geen \u2014", disabled: !deliveryLocationVal, options: carriers.map((c) => ({
|
|
825
942
|
value: c.id,
|
|
826
943
|
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-
|
|
944
|
+
})), 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
945
|
})(), _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
946
|
const product = line.product_id ? products.find((p) => p.id === line.product_id) : null;
|
|
830
947
|
const stdBelading = defaultBelading(product);
|
|
@@ -924,7 +1041,7 @@ function ProposalEditor({ proposal, template, customers, products, allowBackorde
|
|
|
924
1041
|
const pl = toPlants(next[idx].qty_in_mail, kind, effBelading);
|
|
925
1042
|
next[idx] = { ...next[idx], unit_kind: kind, quantity: pl ?? next[idx].quantity };
|
|
926
1043
|
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-
|
|
1044
|
+
}, 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
1045
|
const next = [...lines];
|
|
929
1046
|
const raw = e.target.value.replace(/[.,]/g, "");
|
|
930
1047
|
const cr = raw ? Math.max(1, Math.floor(Number(raw))) : null;
|
|
@@ -938,7 +1055,7 @@ function ProposalEditor({ proposal, template, customers, products, allowBackorde
|
|
|
938
1055
|
} }), (() => {
|
|
939
1056
|
const ag = line.product_id ? agreements[line.product_id] : null;
|
|
940
1057
|
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-
|
|
1058
|
+
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
1059
|
const next = [...lines];
|
|
943
1060
|
next[idx] = { ...next[idx], unit_price: ag.net_price, price_source: "afspraak" };
|
|
944
1061
|
setLines(next);
|
|
@@ -948,13 +1065,13 @@ function ProposalEditor({ proposal, template, customers, products, allowBackorde
|
|
|
948
1065
|
next[idx] = { ...next[idx], needs_sticker: e.target.checked };
|
|
949
1066
|
setLines(next);
|
|
950
1067
|
} }), "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-
|
|
1068
|
+
}), _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
1069
|
if (submitting)
|
|
953
1070
|
return;
|
|
954
1071
|
setConfirmOpen(o);
|
|
955
1072
|
if (!o)
|
|
956
1073
|
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-
|
|
1074
|
+
}, 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
1075
|
}
|
|
959
1076
|
/** Verplichte bevestiging: externe klant eerst als lokale klant aanmaken. */
|
|
960
1077
|
function NewCustomerDialog({ candidate, onClose, onCreated, }) {
|
|
@@ -1009,7 +1126,8 @@ function NewCustomerDialog({ candidate, onClose, onCreated, }) {
|
|
|
1009
1126
|
onCreated(res.id);
|
|
1010
1127
|
}
|
|
1011
1128
|
catch (e) {
|
|
1012
|
-
|
|
1129
|
+
console.warn("[mailbox] klant aanmaken", e);
|
|
1130
|
+
toast.error("Klant aanmaken lukt niet. Probeer het opnieuw.");
|
|
1013
1131
|
}
|
|
1014
1132
|
finally {
|
|
1015
1133
|
setBusy(false);
|
|
@@ -1019,8 +1137,8 @@ function NewCustomerDialog({ candidate, onClose, onCreated, }) {
|
|
|
1019
1137
|
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
1138
|
}
|
|
1021
1139
|
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(
|
|
1140
|
+
const reviewClass = meta?.needs_review ? "border-warning/30 ring-1 ring-warning/40 rounded-md p-2" : "";
|
|
1141
|
+
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(StatusIcon, { tone: "busy", label: "Controleren", detail: "Dit veld is niet zeker uit de mail gehaald." })), typeof meta?.ai_confidence === "number" && (_jsxs("span", { className: "text-xs text-muted-foreground", children: [Math.round(meta.ai_confidence * 100), "%"] }))] }));
|
|
1024
1142
|
const renderControl = () => {
|
|
1025
1143
|
switch (field.field_type) {
|
|
1026
1144
|
case "text":
|
|
@@ -1065,12 +1183,9 @@ function ConfBadge({ meta, thresholds, }) {
|
|
|
1065
1183
|
if (!meta)
|
|
1066
1184
|
return null;
|
|
1067
1185
|
const c = Math.max(0, Math.min(1, Number(meta.confidence ?? 0)));
|
|
1068
|
-
const
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
? "border-amber-300 bg-amber-50 text-amber-800"
|
|
1072
|
-
: "border-destructive/40 bg-destructive/5 text-destructive";
|
|
1073
|
-
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] }));
|
|
1186
|
+
const tone = c >= thresholds.green ? "done" : c >= thresholds.orange ? "busy" : "error";
|
|
1187
|
+
const text = c >= thresholds.green ? "Zeker" : c >= thresholds.orange ? "Controleren" : "Onzeker";
|
|
1188
|
+
return (_jsxs("span", { className: "inline-flex items-center gap-1 text-[11px] font-normal text-muted-foreground", children: [_jsx(StatusIcon, { tone: tone, label: text, detail: `${Math.round(c * 100)}% zeker · ${meta.source}` }), text] }));
|
|
1074
1189
|
}
|
|
1075
1190
|
function LateSignupChoice({ open, setOpen, reason, setReason }) {
|
|
1076
1191
|
if (!open) {
|
|
@@ -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":"AAyCA,wBAAgB,kBAAkB,gCAoKjC"}
|