@flowselections/mailbox-orders 1.0.12 → 1.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/dist-lib/components/EmailPreview.d.ts +26 -0
  2. package/dist-lib/components/EmailPreview.d.ts.map +1 -0
  3. package/dist-lib/components/EmailPreview.js +56 -0
  4. package/dist-lib/components/MailboxFilterBar.d.ts +31 -0
  5. package/dist-lib/components/MailboxFilterBar.d.ts.map +1 -0
  6. package/dist-lib/components/MailboxFilterBar.js +67 -0
  7. package/dist-lib/components/MailboxProcessedPage.d.ts.map +1 -1
  8. package/dist-lib/components/MailboxProcessedPage.js +45 -3
  9. package/dist-lib/components/MailboxProposalsPage.d.ts.map +1 -1
  10. package/dist-lib/components/MailboxProposalsPage.js +77 -38
  11. package/dist-lib/components/TablePagination.d.ts +12 -0
  12. package/dist-lib/components/TablePagination.d.ts.map +1 -0
  13. package/dist-lib/components/TablePagination.js +14 -0
  14. package/dist-lib/index.d.ts +3 -0
  15. package/dist-lib/index.d.ts.map +1 -1
  16. package/dist-lib/index.js +3 -0
  17. package/dist-lib/integrations/supabase/client.d.ts +140 -0
  18. package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
  19. package/dist-lib/integrations/supabase/client.server.d.ts +140 -0
  20. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  21. package/dist-lib/integrations/supabase/types.d.ts +140 -0
  22. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  23. package/dist-lib/lib/mailbox-body.d.ts +26 -0
  24. package/dist-lib/lib/mailbox-body.d.ts.map +1 -1
  25. package/dist-lib/lib/mailbox-body.js +77 -1
  26. package/dist-lib/lib/mailbox-ingest.server.d.ts.map +1 -1
  27. package/dist-lib/lib/mailbox-ingest.server.js +5 -0
  28. package/dist-lib/lib/mailbox.functions.d.ts +10 -0
  29. package/dist-lib/lib/mailbox.functions.d.ts.map +1 -1
  30. package/dist-lib/lib/mailbox.functions.js +63 -11
  31. package/package.json +1 -1
@@ -0,0 +1,26 @@
1
+ export type PreviewAddress = {
2
+ name?: string | null;
3
+ email?: string | null;
4
+ };
5
+ export type PreviewAttachment = {
6
+ name: string;
7
+ contentType?: string | null;
8
+ size?: number | null;
9
+ contentId?: string | null;
10
+ url?: string | null;
11
+ };
12
+ type Props = {
13
+ subject?: string | null;
14
+ fromName?: string | null;
15
+ fromEmail?: string | null;
16
+ receivedAt?: string | null;
17
+ to?: PreviewAddress[];
18
+ cc?: PreviewAddress[];
19
+ bodyHtml?: string | null;
20
+ bodyText?: string | null;
21
+ attachments?: PreviewAttachment[];
22
+ hasAttachments?: boolean;
23
+ };
24
+ export declare function EmailPreview({ subject, fromName, fromEmail, receivedAt, to, cc, bodyHtml, bodyText, attachments, hasAttachments, }: Props): import("react").JSX.Element;
25
+ export {};
26
+ //# sourceMappingURL=EmailPreview.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EmailPreview.d.ts","sourceRoot":"","sources":["../../src/components/EmailPreview.tsx"],"names":[],"mappings":"AAIA,MAAM,MAAM,cAAc,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC;AAC7E,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB,CAAC;AAEF,KAAK,KAAK,GAAG;IACX,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,EAAE,CAAC,EAAE,cAAc,EAAE,CAAC;IACtB,EAAE,CAAC,EAAE,cAAc,EAAE,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,WAAW,CAAC,EAAE,iBAAiB,EAAE,CAAC;IAClC,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B,CAAC;AAsEF,wBAAgB,YAAY,CAAC,EAC3B,OAAO,EACP,QAAQ,EACR,SAAS,EACT,UAAU,EACV,EAAO,EACP,EAAO,EACP,QAAQ,EACR,QAAQ,EACR,WAAgB,EAChB,cAAc,GACf,EAAE,KAAK,+BAsHP"}
@@ -0,0 +1,56 @@
1
+ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useState } from "react";
3
+ import { Button, Badge } from "@flowselections/core";
4
+ import { Paperclip, Download, Code, FileText, ShieldOff, Shield } from "lucide-react";
5
+ function addrLabel(a) {
6
+ if (a.name && a.email && a.name !== a.email)
7
+ return `${a.name} <${a.email}>`;
8
+ return a.email || a.name || "";
9
+ }
10
+ function AddressLine({ label, list }) {
11
+ const [expanded, setExpanded] = useState(false);
12
+ if (!list.length)
13
+ return null;
14
+ const shown = expanded ? list : list.slice(0, 3);
15
+ return (_jsxs("div", { className: "flex gap-1 text-xs text-muted-foreground", children: [_jsxs("span", { className: "shrink-0 font-medium", children: [label, ":"] }), _jsxs("span", { className: "break-all", children: [shown.map(addrLabel).filter(Boolean).join(", "), !expanded && list.length > 3 && (_jsxs("button", { type: "button", className: "ml-1 underline", onClick: () => setExpanded(true), children: ["en ", list.length - 3, " meer"] }))] })] }));
16
+ }
17
+ function formatSize(n) {
18
+ if (!n || n <= 0)
19
+ return "";
20
+ if (n < 1024)
21
+ return `${n} B`;
22
+ if (n < 1024 * 1024)
23
+ return `${Math.round(n / 1024)} kB`;
24
+ return `${(n / (1024 * 1024)).toFixed(1)} MB`;
25
+ }
26
+ /** Blokkeert externe requests door remote src/href-bronnen te neutraliseren. */
27
+ function blockRemote(html) {
28
+ return html
29
+ .replace(/\ssrc\s*=\s*(["'])https?:\/\/[^"']*\1/gi, ' data-blocked-src=""')
30
+ .replace(/\sbackground\s*=\s*(["'])https?:\/\/[^"']*\1/gi, "")
31
+ .replace(/url\((["']?)https?:\/\/[^)]*\)/gi, "none");
32
+ }
33
+ function EmailFrame({ html }) {
34
+ const doc = useMemo(() => `<!doctype html><html><head><meta charset="utf-8">` +
35
+ `<base target="_blank">` +
36
+ `<style>html,body{margin:0;padding:12px;background:#fff;color:#111;` +
37
+ `font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;font-size:14px;line-height:1.5;}` +
38
+ `img{max-width:100%;height:auto;}table{max-width:100%;}` +
39
+ `*{word-break:break-word;}</style></head><body>${html}</body></html>`, [html]);
40
+ return (_jsx("iframe", { title: "E-mailweergave", srcDoc: doc,
41
+ // Geen allow-scripts en geen allow-same-origin: de mail kan niets uitvoeren
42
+ // en niet bij de applicatie. Popups blijven werken voor links.
43
+ sandbox: "allow-popups allow-popups-to-escape-sandbox", referrerPolicy: "no-referrer", className: "h-full w-full rounded border bg-white" }));
44
+ }
45
+ export function EmailPreview({ subject, fromName, fromEmail, receivedAt, to = [], cc = [], bodyHtml, bodyText, attachments = [], hasAttachments, }) {
46
+ const html = (bodyHtml ?? "").trim();
47
+ const [mode, setMode] = useState(html ? "html" : "text");
48
+ const [blockRemoteContent, setBlockRemoteContent] = useState(false);
49
+ useEffect(() => {
50
+ setMode(html ? "html" : "text");
51
+ }, [html]);
52
+ const rendered = blockRemoteContent ? blockRemote(html) : html;
53
+ const safeAttachments = Array.isArray(attachments) ? attachments : [];
54
+ const inlineOnly = safeAttachments.every((a) => !!a.contentId);
55
+ return (_jsxs("div", { className: "flex h-full min-h-0 flex-col", children: [_jsxs("div", { className: "shrink-0 space-y-1 border-b pb-3", children: [_jsx("div", { className: "text-base font-semibold leading-snug", children: subject || "(geen onderwerp)" }), _jsxs("div", { className: "text-xs text-muted-foreground", children: [_jsx("span", { className: "font-medium text-foreground", children: fromName || fromEmail }), fromName && fromEmail ? ` <${fromEmail}>` : "", receivedAt ? ` · ${new Date(receivedAt).toLocaleString("nl-NL")}` : ""] }), _jsx(AddressLine, { label: "Aan", list: to }), _jsx(AddressLine, { label: "CC", list: cc }), (safeAttachments.length > 0 || hasAttachments) && (_jsxs("div", { className: "flex flex-wrap items-center gap-2 pt-2", children: [safeAttachments.length === 0 ? (_jsxs(Badge, { variant: "outline", className: "gap-1 font-normal", children: [_jsx(Paperclip, { className: "h-3 w-3" }), " Deze mail bevat bijlagen (nog niet opgehaald)"] })) : (safeAttachments.map((a, i) => (_jsxs("span", { className: "inline-flex items-center gap-1 rounded border bg-muted/40 px-2 py-1 text-xs", children: [_jsx(Paperclip, { className: "h-3 w-3 shrink-0" }), _jsx("span", { className: "max-w-[16rem] truncate", children: a.name }), formatSize(a.size) && (_jsx("span", { className: "text-muted-foreground", children: formatSize(a.size) })), a.url && (_jsx("a", { href: a.url, target: "_blank", rel: "noopener noreferrer", className: "text-primary", "aria-label": `Download ${a.name}`, children: _jsx(Download, { className: "h-3 w-3" }) }))] }, `${a.name}-${i}`)))), safeAttachments.length > 0 && inlineOnly && (_jsx("span", { className: "text-[11px] text-muted-foreground", children: "(in de mail ingesloten afbeeldingen)" }))] })), _jsxs("div", { className: "flex flex-wrap items-center gap-2 pt-2", children: [_jsx(Button, { type: "button", size: "sm", variant: "outline", onClick: () => setMode(mode === "html" ? "text" : "html"), disabled: !html, children: mode === "html" ? (_jsxs(_Fragment, { children: [_jsx(FileText, { className: "mr-1 h-3.5 w-3.5" }), " Platte tekst"] })) : (_jsxs(_Fragment, { children: [_jsx(Code, { className: "mr-1 h-3.5 w-3.5" }), " Opgemaakte mail"] })) }), _jsx(Button, { type: "button", size: "sm", variant: "ghost", disabled: mode !== "html" || !html, onClick: () => setBlockRemoteContent((v) => !v), children: blockRemoteContent ? (_jsxs(_Fragment, { children: [_jsx(Shield, { className: "mr-1 h-3.5 w-3.5" }), " Externe inhoud tonen"] })) : (_jsxs(_Fragment, { children: [_jsx(ShieldOff, { className: "mr-1 h-3.5 w-3.5" }), " Externe inhoud blokkeren"] })) })] })] }), _jsx("div", { className: "min-h-0 flex-1 overflow-hidden pt-3", children: mode === "html" && html ? (_jsx(EmailFrame, { html: rendered })) : (_jsx("div", { className: "h-full w-full overflow-y-auto overscroll-contain rounded border bg-white p-3", children: _jsx("pre", { className: "whitespace-pre-wrap break-words font-sans text-sm leading-relaxed text-[#111]", children: bodyText || "(geen tekst)" }) })) })] }));
56
+ }
@@ -0,0 +1,31 @@
1
+ /** State die bewaard blijft in localStorage (per pagina-sleutel). */
2
+ export declare function usePersistedState<T>(key: string, initial: T): readonly [T, import("react").Dispatch<import("react").SetStateAction<T>>];
3
+ export type PeriodValue = "all" | "today" | "7d" | "30d";
4
+ export type SortOrderValue = "date-desc" | "date-asc";
5
+ /** Hoofdletter- en accent-ongevoelig normaliseren voor zoeken. */
6
+ export declare function normalizeText(value: unknown): string;
7
+ /** Valt een datum binnen de gekozen periode? */
8
+ export declare function matchesPeriod(date: unknown, period: PeriodValue): boolean;
9
+ interface Props {
10
+ search: string;
11
+ onSearchChange: (value: string) => void;
12
+ period: PeriodValue;
13
+ onPeriodChange: (value: PeriodValue) => void;
14
+ sortOrder: SortOrderValue;
15
+ onSortOrderChange: (value: SortOrderValue) => void;
16
+ status?: string;
17
+ onStatusChange?: (value: string) => void;
18
+ statusOptions?: {
19
+ value: string;
20
+ label: string;
21
+ }[];
22
+ filteredCount: number;
23
+ totalCount: number;
24
+ countLabel?: string;
25
+ onClear: () => void;
26
+ hasActiveFilters: boolean;
27
+ searchPlaceholder?: string;
28
+ }
29
+ export declare function MailboxFilterBar({ search, onSearchChange, period, onPeriodChange, sortOrder, onSortOrderChange, status, onStatusChange, statusOptions, filteredCount, totalCount, countLabel, onClear, hasActiveFilters, searchPlaceholder, }: Props): import("react").JSX.Element;
30
+ export {};
31
+ //# sourceMappingURL=MailboxFilterBar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MailboxFilterBar.d.ts","sourceRoot":"","sources":["../../src/components/MailboxFilterBar.tsx"],"names":[],"mappings":"AAOA,qEAAqE;AACrE,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,6EAkB3D;AAED,MAAM,MAAM,WAAW,GAAG,KAAK,GAAG,OAAO,GAAG,IAAI,GAAG,KAAK,CAAC;AACzD,MAAM,MAAM,cAAc,GAAG,WAAW,GAAG,UAAU,CAAC;AAEtD,kEAAkE;AAClE,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAKpD;AAED,gDAAgD;AAChD,wBAAgB,aAAa,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAYzE;AAED,UAAU,KAAK;IACb,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,MAAM,EAAE,WAAW,CAAC;IACpB,cAAc,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAC;IAC7C,SAAS,EAAE,cAAc,CAAC;IAC1B,iBAAiB,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;IACnD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACzC,aAAa,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACnD,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAoBD,wBAAgB,gBAAgB,CAAC,EAC/B,MAAM,EAAE,cAAc,EACtB,MAAM,EAAE,cAAc,EACtB,SAAS,EAAE,iBAAiB,EAC5B,MAAM,EAAE,cAAc,EAAE,aAAa,EACrC,aAAa,EAAE,UAAU,EAAE,UAA0B,EACrD,OAAO,EAAE,gBAAgB,EACzB,iBAAkE,GACnE,EAAE,KAAK,+BA6FP"}
@@ -0,0 +1,67 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Button, Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@flowselections/core";
3
+ import { Search, X, ArrowDown, ArrowUp } from "lucide-react";
4
+ import { useEffect, useState } from "react";
5
+ /** State die bewaard blijft in localStorage (per pagina-sleutel). */
6
+ export function usePersistedState(key, initial) {
7
+ const [value, setValue] = useState(initial);
8
+ const [hydrated, setHydrated] = useState(false);
9
+ useEffect(() => {
10
+ try {
11
+ const raw = window.localStorage.getItem(key);
12
+ if (raw !== null)
13
+ setValue(JSON.parse(raw));
14
+ }
15
+ catch { /* negeer */ }
16
+ setHydrated(true);
17
+ }, [key]);
18
+ useEffect(() => {
19
+ if (!hydrated)
20
+ return;
21
+ try {
22
+ window.localStorage.setItem(key, JSON.stringify(value));
23
+ }
24
+ catch { /* negeer */ }
25
+ }, [key, value, hydrated]);
26
+ return [value, setValue];
27
+ }
28
+ /** Hoofdletter- en accent-ongevoelig normaliseren voor zoeken. */
29
+ export function normalizeText(value) {
30
+ return String(value ?? "")
31
+ .normalize("NFD")
32
+ .replace(/[\u0300-\u036f]/g, "")
33
+ .toLowerCase();
34
+ }
35
+ /** Valt een datum binnen de gekozen periode? */
36
+ export function matchesPeriod(date, period) {
37
+ if (period === "all")
38
+ return true;
39
+ if (!date)
40
+ return false;
41
+ const d = new Date(date);
42
+ if (Number.isNaN(d.getTime()))
43
+ return false;
44
+ const now = new Date();
45
+ if (period === "today") {
46
+ return d.toDateString() === now.toDateString();
47
+ }
48
+ const days = period === "7d" ? 7 : 30;
49
+ const from = new Date(now.getTime() - days * 24 * 60 * 60 * 1000);
50
+ return d.getTime() >= from.getTime();
51
+ }
52
+ function Chip({ label, onRemove }) {
53
+ return (_jsxs("span", { className: "inline-flex items-center gap-1 rounded-full border bg-muted/40 px-2.5 py-1 text-xs", children: [label, _jsx("button", { type: "button", "aria-label": `${label} verwijderen`, onClick: onRemove, className: "text-muted-foreground hover:text-foreground", children: _jsx(X, { className: "h-3 w-3" }) })] }));
54
+ }
55
+ const PERIOD_LABELS = {
56
+ all: "Alle periodes",
57
+ today: "Vandaag",
58
+ "7d": "Laatste 7 dagen",
59
+ "30d": "Laatste 30 dagen",
60
+ };
61
+ export function MailboxFilterBar({ search, onSearchChange, period, onPeriodChange, sortOrder, onSortOrderChange, status, onStatusChange, statusOptions, filteredCount, totalCount, countLabel = "voorstellen", onClear, hasActiveFilters, searchPlaceholder = "Zoek op klant, afzender, onderwerp of tekst…", }) {
62
+ const sortIcon = sortOrder === "date-desc"
63
+ ? _jsx(ArrowDown, { className: "h-4 w-4 text-muted-foreground" })
64
+ : _jsx(ArrowUp, { className: "h-4 w-4 text-muted-foreground" });
65
+ const statusLabel = statusOptions?.find((o) => o.value === status)?.label;
66
+ return (_jsxs("div", { className: "flex flex-col gap-2 shrink-0", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsxs("div", { className: "relative min-w-[240px] flex-1 max-w-md", children: [_jsx(Search, { className: "pointer-events-none absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { value: search, onChange: (e) => onSearchChange(e.target.value), placeholder: searchPlaceholder, className: "h-9 pl-8 pr-8" }), search && (_jsx("button", { type: "button", "aria-label": "Zoekopdracht wissen", className: "absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground", onClick: () => onSearchChange(""), children: _jsx(X, { className: "h-4 w-4" }) }))] }), _jsxs(Select, { value: period, onValueChange: (v) => onPeriodChange(v), children: [_jsx(SelectTrigger, { className: "h-9 w-[170px]", children: _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" })] })] }), _jsxs(Select, { value: sortOrder, onValueChange: (v) => onSortOrderChange(v), children: [_jsxs(SelectTrigger, { className: "h-9 w-[210px] flex items-center gap-2", children: [sortIcon, _jsx(SelectValue, {})] }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "date-desc", children: "Datum: nieuwste eerst" }), _jsx(SelectItem, { value: "date-asc", children: "Datum: oudste eerst" })] })] }), statusOptions && onStatusChange && (_jsxs(Select, { value: status ?? "all", onValueChange: onStatusChange, children: [_jsx(SelectTrigger, { className: "h-9 w-[160px]", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: statusOptions.map((o) => (_jsx(SelectItem, { value: o.value, children: o.label }, o.value))) })] })), _jsxs("div", { className: "ml-auto flex items-center gap-2", children: [_jsxs("span", { className: "text-sm text-muted-foreground", children: [filteredCount, " van ", totalCount, " ", countLabel] }), hasActiveFilters && (_jsx(Button, { size: "sm", variant: "ghost", onClick: onClear, children: "Filters wissen" }))] })] }), hasActiveFilters && (_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [search && _jsx(Chip, { label: `Zoeken: ${search}`, onRemove: () => onSearchChange("") }), period !== "all" && _jsx(Chip, { label: `Periode: ${PERIOD_LABELS[period]}`, onRemove: () => onPeriodChange("all") }), status && status !== "all" && onStatusChange && (_jsx(Chip, { label: `Status: ${statusLabel ?? status}`, onRemove: () => onStatusChange("all") })), sortOrder !== "date-desc" && (_jsx(Chip, { label: "Sortering: oudste eerst", onRemove: () => onSortOrderChange("date-desc") })), _jsx("button", { type: "button", onClick: onClear, className: "text-xs text-primary underline underline-offset-2", children: "Alles wissen" })] }))] }));
67
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"MailboxProcessedPage.d.ts","sourceRoot":"","sources":["../../src/components/MailboxProcessedPage.tsx"],"names":[],"mappings":"AAWA,wBAAgB,oBAAoB,gCA+FnC"}
1
+ {"version":3,"file":"MailboxProcessedPage.d.ts","sourceRoot":"","sources":["../../src/components/MailboxProcessedPage.tsx"],"names":[],"mappings":"AAaA,wBAAgB,oBAAoB,gCA4KnC"}
@@ -1,11 +1,13 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState } from "react";
2
+ import { useEffect, useMemo, useState } from "react";
3
3
  import { useQuery, useQueryClient } from "@tanstack/react-query";
4
4
  import { useServerFn } from "@tanstack/react-start";
5
5
  import { toast } from "sonner";
6
6
  import { Badge, Button, Card, CardContent, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@flowselections/core";
7
7
  import { listProposals, reopenProposal, getActiveOrdersMailbox } from "../lib/mailbox.functions";
8
8
  import { MailboxAuthBanner } from "./MailboxAuthBanner";
9
+ import { TablePagination } from "./TablePagination";
10
+ import { MailboxFilterBar, usePersistedState, normalizeText, matchesPeriod } from "./MailboxFilterBar";
9
11
  export function MailboxProcessedPage() {
10
12
  const fetchList = useServerFn(listProposals);
11
13
  const reopen = useServerFn(reopenProposal);
@@ -28,8 +30,44 @@ export function MailboxProcessedPage() {
28
30
  const items = [
29
31
  ...(Array.isArray(approved.data?.items) ? approved.data.items : []),
30
32
  ...(Array.isArray(rejected.data?.items) ? rejected.data.items : []),
31
- ].sort((a, b) => (b.reviewed_at ?? "").localeCompare(a.reviewed_at ?? ""));
33
+ ];
32
34
  const [busyId, setBusyId] = useState(null);
35
+ const [page, setPage] = useState(1);
36
+ const [pageSize, setPageSize] = useState(25);
37
+ const [search, setSearch] = usePersistedState("mailbox-processed:search", "");
38
+ const [period, setPeriod] = usePersistedState("mailbox-processed:period", "all");
39
+ const [sortOrder, setSortOrder] = usePersistedState("mailbox-processed:sort", "date-desc");
40
+ const [statusFilter, setStatusFilter] = usePersistedState("mailbox-processed:status", "all");
41
+ const customerNameFor = (p) => p.mailbox_messages?.from_name || p.mailbox_messages?.from_email || "Onbekend";
42
+ const filteredItems = useMemo(() => {
43
+ const q = normalizeText(search).trim();
44
+ const filtered = items.filter((p) => {
45
+ if (statusFilter !== "all" && p.status !== statusFilter)
46
+ return false;
47
+ if (!matchesPeriod(p.reviewed_at, period))
48
+ return false;
49
+ if (!q)
50
+ return true;
51
+ const haystack = normalizeText([
52
+ customerNameFor(p),
53
+ p.mailbox_messages?.from_email,
54
+ p.mailbox_messages?.subject,
55
+ p.mailbox_messages?.preview,
56
+ ].join(" "));
57
+ return q.split(/\s+/).every((token) => haystack.includes(token));
58
+ });
59
+ return filtered.sort((a, b) => {
60
+ const aDate = a.reviewed_at ?? "";
61
+ const bDate = b.reviewed_at ?? "";
62
+ return sortOrder === "date-desc" ? bDate.localeCompare(aDate) : aDate.localeCompare(bDate);
63
+ });
64
+ }, [items, search, period, sortOrder, statusFilter]);
65
+ const hasActiveFilters = !!search || period !== "all" || sortOrder !== "date-desc" || statusFilter !== "all";
66
+ const clearFilters = () => { setSearch(""); setPeriod("all"); setSortOrder("date-desc"); setStatusFilter("all"); };
67
+ useEffect(() => { setPage(1); }, [search, period, sortOrder, statusFilter]);
68
+ const pageCount = Math.max(1, Math.ceil(filteredItems.length / pageSize));
69
+ const currentPage = Math.min(page, pageCount);
70
+ const pagedItems = filteredItems.slice((currentPage - 1) * pageSize, currentPage * pageSize);
33
71
  const handleReopen = async (proposalId) => {
34
72
  setBusyId(proposalId);
35
73
  try {
@@ -44,5 +82,9 @@ export function MailboxProcessedPage() {
44
82
  setBusyId(null);
45
83
  }
46
84
  };
47
- return (_jsxs("div", { className: "p-6 flex flex-col gap-4 flex-1 min-h-0 overflow-hidden", children: [_jsx(MailboxAuthBanner, { active: activeMailbox }), _jsx("h1", { className: "text-2xl font-semibold shrink-0", children: "Verwerkt" }), _jsx("div", { className: "flex-1 min-h-0 overflow-y-auto overscroll-contain [scrollbar-gutter:stable]", children: _jsx(Card, { children: _jsx(CardContent, { className: "p-0", children: items.length === 0 ? (_jsx("div", { className: "p-8 text-center text-muted-foreground", children: "Nog geen verwerkte voorstellen." })) : (_jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Status" }), _jsx(TableHead, { children: "Afzender" }), _jsx(TableHead, { children: "Onderwerp" }), _jsx(TableHead, { children: "Verwerkt op" }), _jsx(TableHead, { className: "text-right", children: "Acties" })] }) }), _jsx(TableBody, { children: items.map((p) => (_jsxs(TableRow, { children: [_jsx(TableCell, { children: _jsx(Badge, { variant: p.status === "approved" ? "default" : "destructive", children: p.status === "approved" ? "Goedgekeurd" : "Afgewezen" }) }), _jsx(TableCell, { children: p.mailbox_messages?.from_email ?? "-" }), _jsx(TableCell, { className: "max-w-md truncate", children: p.mailbox_messages?.subject ?? "-" }), _jsx(TableCell, { className: "text-sm text-muted-foreground", children: p.reviewed_at ? new Date(p.reviewed_at).toLocaleString("nl-NL") : "-" }), _jsx(TableCell, { className: "text-right space-x-2", children: p.status === "rejected" && (_jsx(Button, { size: "sm", variant: "outline", disabled: busyId === p.id, onClick: () => handleReopen(p.id), children: busyId === p.id ? "Bezig..." : "Terugzetten" })) })] }, p.id))) })] })) }) }) })] }));
85
+ return (_jsxs("div", { className: "p-6 flex flex-col gap-4 flex-1 min-h-0 overflow-hidden", children: [_jsx(MailboxAuthBanner, { active: activeMailbox }), _jsx("h1", { className: "text-2xl font-semibold shrink-0", children: "Verwerkt" }), items.length > 0 && (_jsx(MailboxFilterBar, { search: search, onSearchChange: setSearch, period: period, onPeriodChange: setPeriod, sortOrder: sortOrder, onSortOrderChange: setSortOrder, status: statusFilter, onStatusChange: setStatusFilter, statusOptions: [
86
+ { value: "all", label: "Alle statussen" },
87
+ { value: "approved", label: "Goedgekeurd" },
88
+ { value: "rejected", label: "Afgewezen" },
89
+ ], filteredCount: filteredItems.length, totalCount: items.length, countLabel: "mails", onClear: clearFilters, hasActiveFilters: hasActiveFilters })), _jsx("div", { className: "flex-1 min-h-0 overflow-y-auto overscroll-contain [scrollbar-gutter:stable]", children: _jsx(Card, { children: _jsxs(CardContent, { className: "p-0", children: [items.length === 0 ? (_jsx("div", { className: "p-8 text-center text-muted-foreground", children: "Nog geen verwerkte voorstellen." })) : filteredItems.length === 0 ? (_jsx("div", { className: "p-8 text-center text-muted-foreground", children: "Geen mails gevonden voor deze zoekopdracht." })) : (_jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Status" }), _jsx(TableHead, { children: "Afzender" }), _jsx(TableHead, { children: "Onderwerp" }), _jsx(TableHead, { children: "Verwerkt op" }), _jsx(TableHead, { className: "text-right", children: "Acties" })] }) }), _jsx(TableBody, { children: pagedItems.map((p) => (_jsxs(TableRow, { children: [_jsx(TableCell, { children: _jsx(Badge, { variant: p.status === "approved" ? "default" : "destructive", children: p.status === "approved" ? "Goedgekeurd" : "Afgewezen" }) }), _jsx(TableCell, { children: p.mailbox_messages?.from_email ?? "-" }), _jsxs(TableCell, { className: "max-w-md", children: [_jsx("div", { className: "truncate", children: p.mailbox_messages?.subject ?? "-" }), _jsx("div", { className: "truncate text-xs text-muted-foreground", children: p.mailbox_messages?.preview || "" })] }), _jsx(TableCell, { className: "text-sm text-muted-foreground", children: p.reviewed_at ? new Date(p.reviewed_at).toLocaleString("nl-NL") : "-" }), _jsx(TableCell, { className: "text-right space-x-2", children: p.status === "rejected" && (_jsx(Button, { size: "sm", variant: "outline", disabled: busyId === p.id, onClick: () => handleReopen(p.id), children: busyId === p.id ? "Bezig..." : "Terugzetten" })) })] }, p.id))) })] })), items.length > 0 && (_jsx(TablePagination, { total: filteredItems.length, page: currentPage, pageSize: pageSize, onPageChange: setPage, onPageSizeChange: setPageSize }))] }) }) })] }));
48
90
  }
@@ -1 +1 @@
1
- {"version":3,"file":"MailboxProposalsPage.d.ts","sourceRoot":"","sources":["../../src/components/MailboxProposalsPage.tsx"],"names":[],"mappings":"AAyFA,6EAA6E;AAC7E,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;CACtB;AA2CD,wBAAgB,oBAAoB,gCAwYnC"}
1
+ {"version":3,"file":"MailboxProposalsPage.d.ts","sourceRoot":"","sources":["../../src/components/MailboxProposalsPage.tsx"],"names":[],"mappings":"AA6FA,6EAA6E;AAC7E,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;CACtB;AA2CD,wBAAgB,oBAAoB,gCA8bnC"}
@@ -5,11 +5,14 @@ import { useServerFn } from "@tanstack/react-start";
5
5
  import { Button, Badge, Card, CardContent, CardHeader, CardTitle, Input, Label, Textarea, Switch, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Dialog, DialogContent, DialogHeader, DialogTitle, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, toast, } from "@flowselections/core";
6
6
  import { Check, X, Trash2, Plus, AlertTriangle, RefreshCw } from "lucide-react";
7
7
  import { computeEarliestDelivery, isValidDeliveryDate } from "../lib/delivery-date";
8
- import { listProposals, approveProposal, rejectProposal, listCustomersForPicker, createCustomerFromGroup, listProductsForPicker, listDeliveryLocationsForCustomer, listPickupLocations, listCarriersForPicker, getOrderPickDays, getActiveOrdersMailbox, syncMailboxNow, getLastIngestRun, refreshProposalsForAgentChange, reparseProposalNow, listBeladingOptionsForProducts, listAllowedLoadCarriers, } from "../lib/mailbox.functions";
8
+ import { listProposals, getProposalDetail, approveProposal, rejectProposal, listCustomersForPicker, createCustomerFromGroup, listProductsForPicker, listDeliveryLocationsForCustomer, listPickupLocations, listCarriersForPicker, getOrderPickDays, getActiveOrdersMailbox, syncMailboxNow, refreshProposalsForAgentChange, reparseProposalNow, listBeladingOptionsForProducts, listAllowedLoadCarriers, } from "../lib/mailbox.functions";
9
9
  import { enabledLoadCarriers, normalizeAllowedLoadCarriers, EMPTY_LOAD_CARRIERS } from "../lib/load-carriers";
10
10
  import { listTemplateFields, listPlantTypesForPicker, listSuppliersForPicker, } from "../lib/mailbox-template.functions";
11
11
  import { SearchableSelect } from "./SearchableSelect";
12
12
  import { MailboxAuthBanner } from "./MailboxAuthBanner";
13
+ import { EmailPreview } from "./EmailPreview";
14
+ import { TablePagination } from "./TablePagination";
15
+ import { MailboxFilterBar, usePersistedState, normalizeText, matchesPeriod } from "./MailboxFilterBar";
13
16
  import { supabase } from "../integrations/supabase/client";
14
17
  const beladingKey = (b) => `${b.plants_per_crate}x${b.crates_per_layer}x${b.layers_per_cart}|${b.crate_code ?? ""}|${b.carrier_code ?? ""}`;
15
18
  const beladingLabel = (b) => `${b.plants_per_crate} planten/fust · ${b.crates_per_layer} fusten/laag · ${b.layers_per_cart} lagen`;
@@ -86,7 +89,6 @@ export function MailboxProposalsPage() {
86
89
  const fetchPickupLocations = useServerFn(listPickupLocations);
87
90
  const fetchActive = useServerFn(getActiveOrdersMailbox);
88
91
  const fetchSync = useServerFn(syncMailboxNow);
89
- const fetchLastRun = useServerFn(getLastIngestRun);
90
92
  // Server fns met requireSupabaseAuth gooien een raw Response bij een stale
91
93
  // bearer-token. Die bubbelt op als "[object Response]" en blankt het scherm.
92
94
  // Vang elke fout en geef een lege fallback terug — UI blijft bruikbaar.
@@ -98,6 +100,23 @@ export function MailboxProposalsPage() {
98
100
  return { items: [] };
99
101
  }
100
102
  };
103
+ // Welk voorstel staat open? De zware referentielijsten worden pas geladen
104
+ // zodra de dialoog voor het eerst opengaat, en blijven daarna gecachet.
105
+ const [activeId, setActiveId] = useState(null);
106
+ const [page, setPage] = useState(1);
107
+ const [pageSize, setPageSize] = useState(25);
108
+ const [search, setSearch] = usePersistedState("mailbox-proposals:search", "");
109
+ const [period, setPeriod] = usePersistedState("mailbox-proposals:period", "all");
110
+ const [sortOrder, setSortOrder] = usePersistedState("mailbox-proposals:sort", "date-desc");
111
+ const [refsEnabled, setRefsEnabled] = useState(false);
112
+ useEffect(() => { if (activeId)
113
+ setRefsEnabled(true); }, [activeId]);
114
+ const refOpts = {
115
+ enabled: refsEnabled,
116
+ staleTime: 5 * 60000,
117
+ gcTime: 30 * 60000,
118
+ refetchOnWindowFocus: false,
119
+ };
101
120
  // Bestelvoorstellen toont uitsluitend nieuwe, nog niet gecontroleerde
102
121
  // bestellingen ("Te controleren"). Goedgekeurd/afgewezen verschijnt in
103
122
  // de module Verwerkt.
@@ -111,16 +130,25 @@ export function MailboxProposalsPage() {
111
130
  refetchInterval: 60000,
112
131
  refetchOnWindowFocus: true,
113
132
  });
133
+ // Detail (mailtekst, bestelregels, veldwaarden) pas ophalen bij openen.
134
+ const fetchDetail = useServerFn(getProposalDetail);
135
+ const { data: detailData, isFetching: detailLoading } = useQuery({
136
+ queryKey: ["mailbox-proposal-detail", activeId],
137
+ enabled: !!activeId,
138
+ staleTime: 60000,
139
+ queryFn: () => fetchDetail({ data: { proposalId: activeId } }),
140
+ });
114
141
  const { data: templateData } = useQuery({
115
142
  queryKey: ["mailbox-template-fields"],
116
143
  queryFn: () => safe(() => fetchTemplate()),
144
+ ...refOpts,
117
145
  });
118
- const { data: customersData } = useQuery({ queryKey: ["mailbox-customers"], queryFn: () => safe(() => fetchCustomers()) });
119
- const { data: productsData } = useQuery({ queryKey: ["mailbox-products"], queryFn: () => safe(() => fetchProducts()) });
120
- const { data: plantData } = useQuery({ queryKey: ["mailbox-plant-types"], queryFn: () => safe(() => fetchPlantTypes()) });
121
- const { data: supplierData } = useQuery({ queryKey: ["mailbox-suppliers"], queryFn: () => safe(() => fetchSuppliers()) });
122
- const { data: carriersData } = useQuery({ queryKey: ["mailbox-carriers"], queryFn: () => safe(() => fetchCarriers()) });
123
- const { data: pickupLocData } = useQuery({ queryKey: ["mailbox-pickup-locations"], queryFn: () => safe(() => fetchPickupLocations()) });
146
+ const { data: customersData } = useQuery({ queryKey: ["mailbox-customers"], queryFn: () => safe(() => fetchCustomers()), ...refOpts });
147
+ const { data: productsData } = useQuery({ queryKey: ["mailbox-products"], queryFn: () => safe(() => fetchProducts()), ...refOpts });
148
+ const { data: plantData } = useQuery({ queryKey: ["mailbox-plant-types"], queryFn: () => safe(() => fetchPlantTypes()), ...refOpts });
149
+ const { data: supplierData } = useQuery({ queryKey: ["mailbox-suppliers"], queryFn: () => safe(() => fetchSuppliers()), ...refOpts });
150
+ const { data: carriersData } = useQuery({ queryKey: ["mailbox-carriers"], queryFn: () => safe(() => fetchCarriers()), ...refOpts });
151
+ const { data: pickupLocData } = useQuery({ queryKey: ["mailbox-pickup-locations"], queryFn: () => safe(() => fetchPickupLocations()), ...refOpts });
124
152
  const { data: activeMailbox } = useQuery({
125
153
  queryKey: ["mailbox-active-account"],
126
154
  queryFn: () => fetchActive(),
@@ -143,22 +171,9 @@ export function MailboxProposalsPage() {
143
171
  onSuccess: (res) => {
144
172
  if (res?.ok && ((res.inserted ?? 0) > 0 || (res.processed ?? 0) > 0)) {
145
173
  qc.invalidateQueries({ queryKey: ["mailbox-proposals"] });
146
- qc.invalidateQueries({ queryKey: ["mailbox-last-ingest-run"] });
147
174
  }
148
175
  },
149
176
  });
150
- const handleManualSync = () => {
151
- const p = syncMut.mutateAsync("manual");
152
- toast.promise(p, {
153
- loading: "Mailbox synchroniseren...",
154
- success: (res) => res?.ok
155
- ? (res.inserted ?? 0) > 0
156
- ? `${res.inserted} nieuwe bestelling(en) opgehaald`
157
- : "Geen nieuwe bestellingen"
158
- : `Sync mislukt: ${res?.error ?? "onbekende fout"}`,
159
- error: "Sync mislukt",
160
- });
161
- };
162
177
  // Realtime hint: zodra er een nieuw event binnenkomt vragen we de server om
163
178
  // het meteen te verwerken (zelfde pipeline). Cron is de garantiestrook.
164
179
  useEffect(() => {
@@ -187,13 +202,6 @@ export function MailboxProposalsPage() {
187
202
  return () => clearInterval(t);
188
203
  // eslint-disable-next-line react-hooks/exhaustive-deps
189
204
  }, [activeMailboxId]);
190
- // Laatste ingest-run tonen voor observability.
191
- const { data: lastRunData } = useQuery({
192
- queryKey: ["mailbox-last-ingest-run"],
193
- queryFn: () => safe(() => fetchLastRun()),
194
- refetchInterval: 60000,
195
- });
196
- const lastRun = lastRunData?.run ?? null;
197
205
  // AI-agent gewijzigd? Dan worden openstaande voorstellen automatisch opnieuw
198
206
  // geanalyseerd met de nieuwste agent. Server-side draait dit ook elke
199
207
  // cron-run; hier reageren we direct op wijzigingen in de AI-module.
@@ -226,6 +234,7 @@ export function MailboxProposalsPage() {
226
234
  if (res?.ok) {
227
235
  setReparseError(null);
228
236
  qc.invalidateQueries({ queryKey: ["mailbox-proposals"] });
237
+ qc.invalidateQueries({ queryKey: ["mailbox-proposal-detail"] });
229
238
  toast.success("E-mail opnieuw geanalyseerd — bestelgegevens bijgewerkt");
230
239
  }
231
240
  else {
@@ -266,8 +275,8 @@ export function MailboxProposalsPage() {
266
275
  const suppliers = Array.isArray(supplierData?.items) ? supplierData.items : [];
267
276
  const carriers = Array.isArray(carriersData?.items) ? carriersData.items : [];
268
277
  const pickupLocations = Array.isArray(pickupLocData?.items) ? pickupLocData.items : [];
269
- const [activeId, setActiveId] = useState(null);
270
- const active = items.find((p) => p.id === activeId) ?? null;
278
+ const activeSummary = items.find((p) => p.id === activeId) ?? null;
279
+ const active = (detailData?.item ?? null);
271
280
  const statusLabel = (_s) => "Te controleren";
272
281
  const statusVariant = (_s) => "secondary";
273
282
  const customerNameFor = (p) => {
@@ -281,15 +290,45 @@ export function MailboxProposalsPage() {
281
290
  }
282
291
  return p.mailbox_messages?.from_name || p.mailbox_messages?.from_email || "Onbekend";
283
292
  };
284
- return (_jsxs("div", { className: "p-6 flex flex-col gap-4 flex-1 min-h-0 overflow-hidden", children: [_jsx(MailboxAuthBanner, { active: activeMailbox }), _jsxs("div", { className: "flex items-center justify-between shrink-0", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-2xl font-semibold", children: "Bestelvoorstellen" }), _jsx("p", { className: "text-sm text-muted-foreground", children: "Nieuw binnengekomen bestellingen die nog handmatig gecontroleerd moeten worden." })] }), _jsxs("div", { className: "flex items-center gap-3", children: [lastRun?.finished_at && (_jsxs("span", { className: "text-xs text-muted-foreground", children: ["Laatste sync: ", new Date(lastRun.finished_at).toLocaleTimeString("nl-NL"), " \u2014 ", lastRun.processed, " verwerkt", lastRun.errors > 0 ? `, ${lastRun.errors} fouten` : ""] })), _jsxs(Button, { onClick: handleManualSync, disabled: syncMut.isPending, variant: "outline", children: [_jsx(RefreshCw, { className: `h-4 w-4 mr-2 ${syncMut.isPending ? "animate-spin" : ""}` }), "Sync nu"] })] })] }), _jsx("div", { className: "flex-1 min-h-0 overflow-y-auto overscroll-contain [scrollbar-gutter:stable]", children: isLoading ? (_jsx("div", { className: "text-muted-foreground", children: "Laden..." })) : listError ? (_jsxs("div", { className: "rounded-md border border-destructive/40 bg-destructive/5 p-4 text-sm", children: [_jsx("p", { className: "font-medium text-destructive", children: "Bestellingen konden niet worden opgehaald." }), _jsxs("p", { className: "text-muted-foreground mt-1", children: [listError?.message ?? "Onbekende fout", " \u2014 de lijst is niet leeg, alleen niet geladen."] }), _jsx(Button, { size: "sm", variant: "outline", className: "mt-3", onClick: () => void refetchList(), children: "Opnieuw proberen" })] })) : items.length === 0 ? (_jsx("p", { className: "text-muted-foreground", children: "Geen bestellingen te controleren." })) : (_jsx(Card, { children: _jsx(CardContent, { className: "p-0", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Klantnaam" }), _jsx(TableHead, { children: "Orderdatum" }), _jsx(TableHead, { children: "Status" }), _jsx(TableHead, { className: "text-right", children: "Orderwaarde" })] }) }), _jsx(TableBody, { children: items.map((p) => {
285
- const fvs = Array.isArray(p.mailbox_proposal_field_values) ? p.mailbox_proposal_field_values : [];
286
- const needsReview = fvs.some((v) => v.needs_review);
287
- const date = p.mailbox_messages?.received_at ?? p.created_at;
288
- return (_jsxs(TableRow, { className: "cursor-pointer hover:bg-muted/50", onClick: () => setActiveId(p.id), children: [_jsx(TableCell, { className: "font-medium", children: customerNameFor(p) }), _jsx(TableCell, { className: "text-sm text-muted-foreground", children: date ? new Date(date).toLocaleDateString("nl-NL") : "-" }), _jsx(TableCell, { children: _jsxs("div", { className: "flex items-center gap-1", children: [_jsx(Badge, { variant: statusVariant(p.status), children: statusLabel(p.status) }), p.parsed_payload?.ai?.ok === true && (_jsx(Badge, { variant: "outline", className: "text-xs border-sky-500 text-sky-700", children: "AI" })), needsReview && (_jsxs(Badge, { variant: "outline", className: "text-xs border-yellow-500 text-yellow-700", children: [_jsx(AlertTriangle, { className: "h-3 w-3 mr-1" }), "Controleren"] }))] }) }), _jsx(TableCell, { className: "text-right text-sm text-muted-foreground", children: "\u2014" })] }, p.id));
289
- }) })] }) }) })) }), _jsx(Dialog, { open: !!active, onOpenChange: (o) => { if (!o) {
293
+ // --- Zoeken, filteren en sorteren --------------------------------------
294
+ const filteredItems = useMemo(() => {
295
+ const q = normalizeText(search).trim();
296
+ const filtered = items.filter((p) => {
297
+ const name = customerNameFor(p);
298
+ if (!matchesPeriod(p.mailbox_messages?.received_at ?? p.created_at, period))
299
+ return false;
300
+ if (!q)
301
+ return true;
302
+ const haystack = normalizeText([
303
+ name,
304
+ p.mailbox_messages?.from_email,
305
+ p.mailbox_messages?.from_name,
306
+ p.mailbox_messages?.subject,
307
+ p.mailbox_messages?.preview,
308
+ ].join(" "));
309
+ return q.split(/\s+/).every((token) => haystack.includes(token));
310
+ });
311
+ return filtered.sort((a, b) => {
312
+ const aDate = a.mailbox_messages?.received_at ?? a.created_at ?? "";
313
+ const bDate = b.mailbox_messages?.received_at ?? b.created_at ?? "";
314
+ return sortOrder === "date-desc" ? bDate.localeCompare(aDate) : aDate.localeCompare(bDate);
315
+ });
316
+ }, [items, customers, search, sortOrder, period]);
317
+ const hasActiveFilters = !!search || period !== "all" || sortOrder !== "date-desc";
318
+ const clearFilters = () => { setSearch(""); setPeriod("all"); setSortOrder("date-desc"); };
319
+ useEffect(() => { setPage(1); }, [search, period, sortOrder]);
320
+ const pageCount = Math.max(1, Math.ceil(filteredItems.length / pageSize));
321
+ const currentPage = Math.min(page, pageCount);
322
+ const pagedItems = filteredItems.slice((currentPage - 1) * pageSize, currentPage * pageSize);
323
+ return (_jsxs("div", { className: "p-6 flex flex-col gap-4 flex-1 min-h-0 overflow-hidden", children: [_jsx(MailboxAuthBanner, { active: activeMailbox }), _jsx("div", { className: "flex items-center justify-between shrink-0", children: _jsxs("div", { children: [_jsx("h1", { className: "text-2xl font-semibold", children: "Bestelvoorstellen" }), _jsx("p", { className: "text-sm text-muted-foreground", children: "Nieuw binnengekomen bestellingen die nog handmatig gecontroleerd moeten worden." })] }) }), items.length > 0 && (_jsx(MailboxFilterBar, { search: search, onSearchChange: setSearch, period: period, onPeriodChange: setPeriod, sortOrder: sortOrder, onSortOrderChange: setSortOrder, filteredCount: filteredItems.length, totalCount: items.length, countLabel: "voorstellen", onClear: clearFilters, hasActiveFilters: hasActiveFilters })), _jsx("div", { className: "flex-1 min-h-0 overflow-y-auto overscroll-contain [scrollbar-gutter:stable]", children: isLoading ? (_jsx("div", { className: "text-muted-foreground", children: "Laden..." })) : listError ? (_jsxs("div", { className: "rounded-md border border-destructive/40 bg-destructive/5 p-4 text-sm", children: [_jsx("p", { className: "font-medium text-destructive", children: "Bestellingen konden niet worden opgehaald." }), _jsxs("p", { className: "text-muted-foreground mt-1", children: [listError?.message ?? "Onbekende fout", " \u2014 de lijst is niet leeg, alleen niet geladen."] }), _jsx(Button, { size: "sm", variant: "outline", className: "mt-3", onClick: () => void refetchList(), children: "Opnieuw proberen" })] })) : items.length === 0 ? (_jsx("p", { className: "text-muted-foreground", children: "Geen bestellingen te controleren." })) : filteredItems.length === 0 ? (_jsx("p", { className: "text-muted-foreground", children: "Geen voorstellen gevonden voor deze zoekopdracht." })) : (_jsx(Card, { children: _jsxs(CardContent, { className: "p-0", children: [_jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Klantnaam" }), _jsx(TableHead, { children: "Bericht" }), _jsx(TableHead, { children: "Orderdatum" }), _jsx(TableHead, { children: "Status" })] }) }), _jsx(TableBody, { children: pagedItems.map((p) => {
324
+ const fvs = Array.isArray(p.mailbox_proposal_field_values) ? p.mailbox_proposal_field_values : [];
325
+ const needsReview = fvs.some((v) => v.needs_review);
326
+ const date = p.mailbox_messages?.received_at ?? p.created_at;
327
+ return (_jsxs(TableRow, { className: "cursor-pointer hover:bg-muted/50", onClick: () => setActiveId(p.id), children: [_jsx(TableCell, { className: "font-medium", children: customerNameFor(p) }), _jsxs(TableCell, { className: "max-w-sm", children: [_jsx("div", { className: "truncate text-sm font-medium", children: p.mailbox_messages?.subject || "(geen onderwerp)" }), _jsx("div", { className: "truncate text-xs text-muted-foreground", children: p.mailbox_messages?.preview || "Geen voorbeeldtekst" })] }), _jsx(TableCell, { className: "text-sm text-muted-foreground", children: date ? new Date(date).toLocaleDateString("nl-NL") : "-" }), _jsx(TableCell, { children: _jsx(Badge, { variant: "secondary", children: "Te controleren" }) })] }, p.id));
328
+ }) })] }), _jsx(TablePagination, { total: filteredItems.length, page: currentPage, pageSize: pageSize, onPageChange: setPage, onPageSizeChange: setPageSize })] }) })) }), _jsx(Dialog, { open: !!activeId, onOpenChange: (o) => { if (!o) {
290
329
  setActiveId(null);
291
330
  setReparseError(null);
292
- } }, children: _jsx(DialogContent, { className: "max-w-[95vw] w-[95vw] h-[92vh] max-h-[92vh] p-0 flex flex-col overflow-hidden", children: active && (_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(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 ? "Analyseren…" : "Mail opnieuw analyseren"] })] }), 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) => {
331
+ } }, 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 ? "Analyseren…" : "Mail opnieuw analyseren"] })] }), 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) => {
293
332
  try {
294
333
  const res = await fetchApprove({ data: payload });
295
334
  const shortages = Array.isArray(res?.shortages) ? res.shortages : [];
@@ -613,7 +652,7 @@ function ProposalEditor({ proposal, template, customers, products, allowBackorde
613
652
  setSubmitting(false);
614
653
  }
615
654
  };
616
- return (_jsxs("div", { className: "grid grid-cols-2 gap-4 h-full min-h-0", children: [_jsxs(Card, { className: "flex flex-col min-h-0 overflow-hidden", children: [_jsxs(CardHeader, { className: "shrink-0", children: [_jsx(CardTitle, { className: "text-base", children: msg.subject || "(geen onderwerp)" }), _jsxs("div", { className: "text-xs text-muted-foreground", children: [msg.from_name, " <", msg.from_email, "> \u00B7", " ", msg.received_at ? new Date(msg.received_at).toLocaleString("nl-NL") : ""] })] }), _jsx(CardContent, { className: "flex-1 min-h-0 overflow-y-auto overscroll-contain", children: _jsx("pre", { className: "text-sm whitespace-pre-wrap font-sans", children: msg.body_text || "(geen tekst)" }) })] }), _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: [(() => {
655
+ 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: _jsx(CardContent, { className: "flex-1 min-h-0 p-4", children: _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: [(() => {
617
656
  // Klantveld staat altijd bovenaan, ook als het template (nog) leeg is.
618
657
  const customerField = visibleFields.find((f) => f.key === "customer") ?? {
619
658
  id: "customer",
@@ -0,0 +1,12 @@
1
+ export declare const PAGE_SIZE_OPTIONS: number[];
2
+ type Props = {
3
+ total: number;
4
+ page: number;
5
+ pageSize: number;
6
+ onPageChange: (page: number) => void;
7
+ onPageSizeChange: (size: number) => void;
8
+ };
9
+ /** Paginering onderaan een tabel: "1–14 van 14 · Per pagina · Vorige/Volgende". */
10
+ export declare function TablePagination({ total, page, pageSize, onPageChange, onPageSizeChange, }: Props): import("react").JSX.Element;
11
+ export {};
12
+ //# sourceMappingURL=TablePagination.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TablePagination.d.ts","sourceRoot":"","sources":["../../src/components/TablePagination.tsx"],"names":[],"mappings":"AASA,eAAO,MAAM,iBAAiB,UAAoB,CAAC;AAEnD,KAAK,KAAK,GAAG;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,gBAAgB,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAC1C,CAAC;AAEF,mFAAmF;AACnF,wBAAgB,eAAe,CAAC,EAC9B,KAAK,EACL,IAAI,EACJ,QAAQ,EACR,YAAY,EACZ,gBAAgB,GACjB,EAAE,KAAK,+BAqDP"}
@@ -0,0 +1,14 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@flowselections/core";
3
+ export const PAGE_SIZE_OPTIONS = [10, 25, 50, 100];
4
+ /** Paginering onderaan een tabel: "1–14 van 14 · Per pagina · Vorige/Volgende". */
5
+ export function TablePagination({ total, page, pageSize, onPageChange, onPageSizeChange, }) {
6
+ const pageCount = Math.max(1, Math.ceil(total / pageSize));
7
+ const current = Math.min(Math.max(1, page), pageCount);
8
+ const from = total === 0 ? 0 : (current - 1) * pageSize + 1;
9
+ const to = Math.min(current * pageSize, total);
10
+ return (_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3 border-t px-4 py-2 text-sm", children: [_jsxs("span", { className: "text-muted-foreground", children: [from, "\u2013", to, " van ", total] }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("span", { className: "text-muted-foreground", children: "Per pagina" }), _jsxs(Select, { value: String(pageSize), onValueChange: (v) => {
11
+ onPageSizeChange(Number(v));
12
+ onPageChange(1);
13
+ }, children: [_jsx(SelectTrigger, { className: "h-8 w-[80px]", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: PAGE_SIZE_OPTIONS.map((n) => (_jsx(SelectItem, { value: String(n), children: n }, n))) })] }), _jsx(Button, { size: "sm", variant: "outline", disabled: current <= 1, onClick: () => onPageChange(current - 1), children: "Vorige" }), _jsxs("span", { className: "text-muted-foreground", children: [current, " / ", pageCount] }), _jsx(Button, { size: "sm", variant: "outline", disabled: current >= pageCount, onClick: () => onPageChange(current + 1), children: "Volgende" })] })] }));
14
+ }
@@ -3,5 +3,8 @@ export * from "./_core-safelist";
3
3
  export { MailboxProposalsPage } from "./components/MailboxProposalsPage";
4
4
  export { SearchableSelect } from "./components/SearchableSelect";
5
5
  export { MailboxProcessedPage } from "./components/MailboxProcessedPage";
6
+ export { EmailPreview } from "./components/EmailPreview";
7
+ export { TablePagination } from "./components/TablePagination";
8
+ export { MailboxFilterBar } from "./components/MailboxFilterBar";
6
9
  export declare const mailboxOrdersModule: FlowModule;
7
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAIvD,cAAc,kBAAkB,CAAC;AACjC,OAAO,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AACzE,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACjE,OAAO,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AAEzE,eAAO,MAAM,mBAAmB,EAAE,UAmBjC,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAIvD,cAAc,kBAAkB,CAAC;AACjC,OAAO,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AACzE,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACjE,OAAO,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AACzE,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAC/D,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAEjE,eAAO,MAAM,mBAAmB,EAAE,UAmBjC,CAAC"}
package/dist-lib/index.js CHANGED
@@ -8,6 +8,9 @@ export * from "./_core-safelist";
8
8
  export { MailboxProposalsPage } from "./components/MailboxProposalsPage";
9
9
  export { SearchableSelect } from "./components/SearchableSelect";
10
10
  export { MailboxProcessedPage } from "./components/MailboxProcessedPage";
11
+ export { EmailPreview } from "./components/EmailPreview";
12
+ export { TablePagination } from "./components/TablePagination";
13
+ export { MailboxFilterBar } from "./components/MailboxFilterBar";
11
14
  export const mailboxOrdersModule = {
12
15
  id: "mailbox_orders",
13
16
  name: "Mailbox bestellingen",