@flowselections/mailbox-orders 1.0.53 → 1.0.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-lib/components/MailboxAuthBanner.d.ts.map +1 -1
- package/dist-lib/components/MailboxAuthBanner.js +6 -5
- package/dist-lib/components/MailboxPageFrame.d.ts +19 -0
- package/dist-lib/components/MailboxPageFrame.d.ts.map +1 -0
- package/dist-lib/components/MailboxPageFrame.js +95 -0
- package/dist-lib/components/MailboxProcessedPage.d.ts.map +1 -1
- package/dist-lib/components/MailboxProcessedPage.js +94 -62
- package/dist-lib/components/MailboxProposalsPage.d.ts.map +1 -1
- package/dist-lib/components/MailboxProposalsPage.js +178 -71
- package/dist-lib/components/MailboxSkippedPage.d.ts.map +1 -1
- package/dist-lib/components/MailboxSkippedPage.js +80 -14
- package/dist-lib/components/MailboxStatusLine.d.ts +3 -1
- package/dist-lib/components/MailboxStatusLine.d.ts.map +1 -1
- package/dist-lib/components/MailboxStatusLine.js +31 -8
- package/dist-lib/components/fs-ui/DataTable.d.ts +36 -0
- package/dist-lib/components/fs-ui/DataTable.d.ts.map +1 -0
- package/dist-lib/components/fs-ui/DataTable.js +33 -0
- package/dist-lib/components/fs-ui/EmptyState.d.ts +9 -0
- package/dist-lib/components/fs-ui/EmptyState.d.ts.map +1 -0
- package/dist-lib/components/fs-ui/EmptyState.js +5 -0
- package/dist-lib/components/fs-ui/FilterChips.d.ts +13 -0
- package/dist-lib/components/fs-ui/FilterChips.d.ts.map +1 -0
- package/dist-lib/components/fs-ui/FilterChips.js +8 -0
- package/dist-lib/components/fs-ui/HoverTip.d.ts +15 -0
- package/dist-lib/components/fs-ui/HoverTip.d.ts.map +1 -0
- package/dist-lib/components/fs-ui/HoverTip.js +24 -0
- package/dist-lib/components/fs-ui/InlineAlert.d.ts +9 -0
- package/dist-lib/components/fs-ui/InlineAlert.d.ts.map +1 -0
- package/dist-lib/components/fs-ui/InlineAlert.js +14 -0
- package/dist-lib/components/fs-ui/KpiRow.d.ts +6 -0
- package/dist-lib/components/fs-ui/KpiRow.d.ts.map +1 -0
- package/dist-lib/components/fs-ui/KpiRow.js +7 -0
- package/dist-lib/components/fs-ui/KpiTile.d.ts +21 -0
- package/dist-lib/components/fs-ui/KpiTile.d.ts.map +1 -0
- package/dist-lib/components/fs-ui/KpiTile.js +40 -0
- package/dist-lib/components/fs-ui/MobilePrimaryAction.d.ts +10 -0
- package/dist-lib/components/fs-ui/MobilePrimaryAction.d.ts.map +1 -0
- package/dist-lib/components/fs-ui/MobilePrimaryAction.js +7 -0
- package/dist-lib/components/fs-ui/PageHeader.d.ts +8 -0
- package/dist-lib/components/fs-ui/PageHeader.d.ts.map +1 -0
- package/dist-lib/components/fs-ui/PageHeader.js +5 -0
- package/dist-lib/components/fs-ui/PillTabs.d.ts +15 -0
- package/dist-lib/components/fs-ui/PillTabs.d.ts.map +1 -0
- package/dist-lib/components/fs-ui/PillTabs.js +14 -0
- package/dist-lib/components/fs-ui/SearchInput.d.ts +12 -0
- package/dist-lib/components/fs-ui/SearchInput.d.ts.map +1 -0
- package/dist-lib/components/fs-ui/SearchInput.js +33 -0
- package/dist-lib/components/fs-ui/ShortcutHelp.d.ts +7 -0
- package/dist-lib/components/fs-ui/ShortcutHelp.d.ts.map +1 -0
- package/dist-lib/components/fs-ui/ShortcutHelp.js +6 -0
- package/dist-lib/components/fs-ui/StatusIcon.d.ts +8 -0
- package/dist-lib/components/fs-ui/StatusIcon.d.ts.map +1 -0
- package/dist-lib/components/fs-ui/StatusIcon.js +16 -0
- package/dist-lib/components/fs-ui/TableSkeleton.d.ts +6 -0
- package/dist-lib/components/fs-ui/TableSkeleton.d.ts.map +1 -0
- package/dist-lib/components/fs-ui/TableSkeleton.js +6 -0
- package/dist-lib/components/fs-ui/Toolbar.d.ts +8 -0
- package/dist-lib/components/fs-ui/Toolbar.d.ts.map +1 -0
- package/dist-lib/components/fs-ui/Toolbar.js +5 -0
- package/dist-lib/components/fs-ui/UpdatedAt.d.ts +7 -0
- package/dist-lib/components/fs-ui/UpdatedAt.d.ts.map +1 -0
- package/dist-lib/components/fs-ui/UpdatedAt.js +9 -0
- package/dist-lib/components/fs-ui/useListHotkeys.d.ts +18 -0
- package/dist-lib/components/fs-ui/useListHotkeys.d.ts.map +1 -0
- package/dist-lib/components/fs-ui/useListHotkeys.js +48 -0
- package/dist-lib/components/settings/MailboxAiPromptCard.d.ts +2 -1
- package/dist-lib/components/settings/MailboxAiPromptCard.d.ts.map +1 -1
- package/dist-lib/components/settings/MailboxAiPromptCard.js +9 -1
- package/dist-lib/components/settings/MailboxFilterSettingsCard.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/mailbox-list.functions.d.ts +67 -0
- package/dist-lib/lib/mailbox-list.functions.d.ts.map +1 -0
- package/dist-lib/lib/mailbox-list.functions.js +63 -0
- package/package.json +1 -1
|
@@ -1,34 +1,61 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useMemo, useState } from "react";
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
4
4
|
import { useServerFn } from "@tanstack/react-start";
|
|
5
|
-
import {
|
|
5
|
+
import { CalendarClock, Filter, MailX, Search } from "lucide-react";
|
|
6
|
+
import { Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, toast } from "@flowselections/core";
|
|
6
7
|
import { listSkippedMessages, analyzeMessageNow } from "../lib/mailbox.functions.js";
|
|
7
8
|
import { TablePagination } from "./TablePagination.js";
|
|
8
|
-
import {
|
|
9
|
+
import { usePersistedState, normalizeText, matchesPeriod } from "./MailboxFilterBar.js";
|
|
10
|
+
import { MailboxPageFrame } from "./MailboxPageFrame.js";
|
|
11
|
+
import { DataTable } from "./fs-ui/DataTable.js";
|
|
12
|
+
import { StatusIcon } from "./fs-ui/StatusIcon.js";
|
|
13
|
+
import { EmptyState } from "./fs-ui/EmptyState.js";
|
|
14
|
+
import { InlineAlert } from "./fs-ui/InlineAlert.js";
|
|
15
|
+
import { TableSkeleton } from "./fs-ui/TableSkeleton.js";
|
|
16
|
+
import { Toolbar } from "./fs-ui/Toolbar.js";
|
|
17
|
+
import { SearchInput } from "./fs-ui/SearchInput.js";
|
|
18
|
+
import { FilterChips } from "./fs-ui/FilterChips.js";
|
|
19
|
+
import { useListHotkeys } from "./fs-ui/useListHotkeys.js";
|
|
9
20
|
const statusOf = (m) => m.analysis_state === "wacht"
|
|
10
|
-
? { label: "Wacht op
|
|
21
|
+
? { label: "Wacht op uitlezen", tone: "amber" }
|
|
11
22
|
: m.filter_outcome === "te_oud"
|
|
12
23
|
? { label: "Te oud", tone: "grey" }
|
|
13
24
|
: m.filter_outcome === "geen_bestelling"
|
|
14
25
|
? { label: "Geen bestelling", tone: "grey" }
|
|
15
26
|
: { label: "Overgeslagen", tone: "grey" };
|
|
27
|
+
/** Reden in gewone woorden (codes uit de verwerking worden vertaald). */
|
|
28
|
+
const reasonText = (m) => {
|
|
29
|
+
const raw = String(m.analysis_note || m.filter_reason || "");
|
|
30
|
+
if (m.filter_outcome === "te_oud")
|
|
31
|
+
return "Te oud bij eerste ophalen";
|
|
32
|
+
if (/niet uit een gevolgde map/i.test(raw))
|
|
33
|
+
return "Uit een map die niet gevolgd wordt";
|
|
34
|
+
if (/map onbekend/i.test(raw))
|
|
35
|
+
return "Map niet bekend";
|
|
36
|
+
return raw;
|
|
37
|
+
};
|
|
38
|
+
const PERIOD_LABEL = { today: "Vandaag", "7d": "Laatste 7 dagen", "30d": "Laatste 30 dagen" };
|
|
16
39
|
export function MailboxSkippedPage() {
|
|
17
40
|
const qc = useQueryClient();
|
|
18
41
|
const fetchList = useServerFn(listSkippedMessages);
|
|
19
42
|
const analyze = useServerFn(analyzeMessageNow);
|
|
20
|
-
const { data, isLoading, error } = useQuery({
|
|
43
|
+
const { data, isLoading, error, refetch } = useQuery({
|
|
21
44
|
queryKey: ["mailbox-skipped"],
|
|
22
45
|
queryFn: () => fetchList(),
|
|
23
46
|
refetchInterval: 60000,
|
|
47
|
+
placeholderData: (prev) => prev,
|
|
24
48
|
});
|
|
25
49
|
const items = Array.isArray(data?.items) ? data.items : [];
|
|
26
50
|
const [busyId, setBusyId] = useState(null);
|
|
27
51
|
const [page, setPage] = useState(1);
|
|
28
|
-
const [pageSize, setPageSize] = useState(
|
|
52
|
+
const [pageSize, setPageSize] = useState(50);
|
|
29
53
|
const [search, setSearch] = usePersistedState("mailbox-skipped:search", "");
|
|
30
54
|
const [period, setPeriod] = usePersistedState("mailbox-skipped:period", "all");
|
|
31
55
|
const [sortOrder, setSortOrder] = usePersistedState("mailbox-skipped:sort", "date-desc");
|
|
56
|
+
const [cursor, setCursor] = useState(null);
|
|
57
|
+
const [helpOpen, setHelpOpen] = useState(false);
|
|
58
|
+
const searchRef = useRef(null);
|
|
32
59
|
const filtered = useMemo(() => {
|
|
33
60
|
const q = normalizeText(search).trim();
|
|
34
61
|
return items
|
|
@@ -44,30 +71,69 @@ export function MailboxSkippedPage() {
|
|
|
44
71
|
: String(a.received_at ?? "").localeCompare(String(b.received_at ?? "")));
|
|
45
72
|
}, [items, search, period, sortOrder]);
|
|
46
73
|
const waiting = items.filter((m) => m.analysis_state === "wacht").length;
|
|
74
|
+
useEffect(() => { setPage(1); setCursor(null); }, [search, period, sortOrder]);
|
|
47
75
|
const pageCount = Math.max(1, Math.ceil(filtered.length / pageSize));
|
|
48
76
|
const current = Math.min(page, pageCount);
|
|
49
77
|
const paged = filtered.slice((current - 1) * pageSize, current * pageSize);
|
|
78
|
+
const hasActiveFilters = !!search || period !== "all";
|
|
79
|
+
const clearFilters = () => { setSearch(""); setPeriod("all"); setSortOrder("date-desc"); };
|
|
50
80
|
const run = async (id) => {
|
|
51
81
|
setBusyId(id);
|
|
52
82
|
try {
|
|
53
83
|
const res = await analyze({ data: { messageId: id } });
|
|
54
84
|
if (res?.ok) {
|
|
55
|
-
toast.success("Mail
|
|
85
|
+
toast.success("Mail uitgelezen. Je vindt hem nu bij Te controleren.");
|
|
56
86
|
void qc.invalidateQueries({ queryKey: ["mailbox-skipped"] });
|
|
57
87
|
void qc.invalidateQueries({ queryKey: ["mailbox-proposals"] });
|
|
88
|
+
void qc.invalidateQueries({ queryKey: ["mailbox-kpis"] });
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
if (res?.error)
|
|
92
|
+
console.warn("[mailbox] toch uitlezen", res.error);
|
|
93
|
+
toast.error(res?.error === "Deze mail wordt al geanalyseerd." ? "Deze mail wordt al uitgelezen." : "Uitlezen lukt niet. Probeer het opnieuw.");
|
|
58
94
|
}
|
|
59
|
-
else
|
|
60
|
-
toast.error(res?.error ?? "Analyseren mislukt");
|
|
61
95
|
}
|
|
62
96
|
catch (e) {
|
|
63
|
-
|
|
97
|
+
console.warn("[mailbox] toch uitlezen", e);
|
|
98
|
+
toast.error("Uitlezen lukt niet. Probeer het opnieuw.");
|
|
64
99
|
}
|
|
65
100
|
finally {
|
|
66
101
|
setBusyId(null);
|
|
67
102
|
}
|
|
68
103
|
};
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
104
|
+
useListHotkeys({
|
|
105
|
+
disabled: helpOpen,
|
|
106
|
+
onSearch: () => searchRef.current?.focus(),
|
|
107
|
+
onNext: () => setCursor((c) => (paged.length ? Math.min((c ?? -1) + 1, paged.length - 1) : null)),
|
|
108
|
+
onPrev: () => setCursor((c) => (paged.length ? Math.max((c ?? 1) - 1, 0) : null)),
|
|
109
|
+
onEscape: hasActiveFilters ? clearFilters : undefined,
|
|
110
|
+
onHelp: () => setHelpOpen(true),
|
|
111
|
+
});
|
|
112
|
+
const actionBtn = (m) => (_jsx(Button, { size: "sm", variant: "outline", className: "h-8", disabled: busyId === m.id, onClick: () => run(m.id), children: busyId === m.id ? "Bezig…" : m.analysis_state === "wacht" ? "Nu uitlezen" : "Toch uitlezen" }));
|
|
113
|
+
const columns = [
|
|
114
|
+
{
|
|
115
|
+
key: "afzender", header: "Afzender", width: "minmax(160px,1.2fr)",
|
|
116
|
+
cell: (m) => (_jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: "truncate font-medium", children: m.from_name || m.from_email || "Onbekend" }), m.from_name && _jsx("div", { className: "truncate text-xs text-muted-foreground", children: m.from_email })] })),
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
key: "bericht", header: "Bericht", width: "minmax(240px,2.2fr)",
|
|
120
|
+
cell: (m) => (_jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: "truncate font-medium", children: m.subject || "(geen onderwerp)" }), _jsx("div", { className: "truncate text-xs text-muted-foreground", children: m.preview || "" })] })),
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
key: "datum", header: "Ontvangen", width: "120px", sortable: true,
|
|
124
|
+
cell: (m) => _jsx("span", { className: "tabular-nums text-muted-foreground", children: m.received_at ? new Date(m.received_at).toLocaleDateString("nl-NL") : "–" }),
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
key: "reden", header: "Reden", width: "minmax(180px,1.4fr)",
|
|
128
|
+
cell: (m) => {
|
|
129
|
+
const st = statusOf(m);
|
|
130
|
+
return (_jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [_jsx(StatusIcon, { tone: st.tone === "amber" ? "busy" : "open", label: st.label, detail: reasonText(m) || undefined }), _jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: "truncate text-sm", children: st.label }), _jsx("div", { className: "truncate text-xs text-muted-foreground", children: reasonText(m) })] })] }));
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
{ key: "actie", header: "", width: "130px", cell: (m) => actionBtn(m) },
|
|
134
|
+
];
|
|
135
|
+
const chips = period !== "all" ? [{ key: "period", label: PERIOD_LABEL[period] ?? period, icon: _jsx(CalendarClock, {}), onRemove: () => setPeriod("all") }] : [];
|
|
136
|
+
return (_jsxs(MailboxPageFrame, { tab: "overgeslagen", helpOpen: helpOpen, onHelpOpenChange: setHelpOpen, children: [waiting > 0 && (_jsx(InlineAlert, { tone: "warning", title: `${waiting} mail(s) wachten op uitlezen.`, children: "De limiet voor vandaag is bereikt of automatisch uitlezen staat uit. Je kunt een mail ook zelf laten uitlezen." })), _jsx(Toolbar, { chips: _jsx(FilterChips, { chips: chips, onClearAll: clearFilters }), right: _jsxs(Select, { value: period, onValueChange: (v) => setPeriod(v), children: [_jsxs(SelectTrigger, { className: "h-9 w-[150px]", "aria-label": "Periode", children: [_jsx(Filter, { className: "mr-1 h-4 w-4" }), _jsx(SelectValue, {})] }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "all", children: "Alle periodes" }), _jsx(SelectItem, { value: "today", children: "Vandaag" }), _jsx(SelectItem, { value: "7d", children: "Laatste 7 dagen" }), _jsx(SelectItem, { value: "30d", children: "Laatste 30 dagen" })] })] }), children: _jsx(SearchInput, { ref: searchRef, value: search, onChange: setSearch, placeholder: "Zoek op afzender, onderwerp of reden" }) }), isLoading && !data ? (_jsx(TableSkeleton, { columns: ["1.2fr", "2.2fr", "120px", "1.4fr", "130px"] })) : error && !data ? (_jsx(InlineAlert, { tone: "error", title: "De lijst kon niet worden geladen.", action: _jsx(Button, { size: "sm", variant: "outline", onClick: () => void refetch(), children: "Opnieuw proberen" }), children: "Probeer het opnieuw." })) : filtered.length === 0 ? (_jsx("div", { className: "rounded-xl border border-border bg-card shadow-sm", children: hasActiveFilters
|
|
137
|
+
? _jsx(EmptyState, { icon: _jsx(Search, { className: "h-6 w-6" }), title: "Niets gevonden", text: "Er zijn geen overgeslagen mails die bij je zoekopdracht passen.", action: _jsx(Button, { size: "sm", variant: "outline", onClick: clearFilters, children: "Filters wissen" }) })
|
|
138
|
+
: _jsx(EmptyState, { icon: _jsx(MailX, { className: "h-6 w-6" }), title: "Geen overgeslagen mails", text: "Mails die geen bestelling lijken, verschijnen hier met de reden." }) })) : (_jsx(DataTable, { rows: paged, columns: columns, rowKey: (m) => m.id, activeIndex: cursor, sort: { key: "datum", dir: sortOrder === "date-asc" ? "asc" : "desc" }, onSortChange: () => setSortOrder(sortOrder === "date-asc" ? "date-desc" : "date-asc"), mobileCard: (m) => (_jsxs("div", { className: "min-w-0 space-y-1", children: [_jsx("div", { className: "truncate font-semibold", children: m.from_name || m.from_email || "Onbekend" }), _jsx("div", { className: "truncate text-muted-foreground", children: m.subject || "(geen onderwerp)" }), _jsxs("div", { className: "text-xs text-muted-foreground", children: [m.received_at ? new Date(m.received_at).toLocaleDateString("nl-NL") : "–", " \u00B7 ", statusOf(m).label] }), _jsx("div", { className: "pt-1", children: actionBtn(m) })] })), footer: _jsx(TablePagination, { total: filtered.length, page: current, pageSize: pageSize, onPageChange: setPage, onPageSizeChange: setPageSize }) }))] }));
|
|
73
139
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** Pad naar de AI-koppelingen in de shell (Instellingen › AI-Authenticatie). */
|
|
2
2
|
export declare const AI_AUTH_PATH = "/ai-authenticatie";
|
|
3
|
-
/**
|
|
3
|
+
/** Korte regel onder de paginatitel: wanneer opgehaald, hoeveel wachten. */
|
|
4
|
+
export declare function MailboxStatusMeta(): import("react").JSX.Element | null;
|
|
5
|
+
/** Meldingen over het ophalen (alleen als de gebruiker er iets mee moet). */
|
|
4
6
|
export declare function MailboxStatusLine(): import("react").JSX.Element | null;
|
|
5
7
|
//# sourceMappingURL=MailboxStatusLine.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MailboxStatusLine.d.ts","sourceRoot":"","sources":["../../src/components/MailboxStatusLine.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"MailboxStatusLine.d.ts","sourceRoot":"","sources":["../../src/components/MailboxStatusLine.tsx"],"names":[],"mappings":"AAMA,gFAAgF;AAChF,eAAO,MAAM,YAAY,sBAAsB,CAAC;AAehD,4EAA4E;AAC5E,wBAAgB,iBAAiB,uCAYhC;AAED,6EAA6E;AAC7E,wBAAgB,iBAAiB,uCAgChC"}
|
|
@@ -1,23 +1,46 @@
|
|
|
1
|
-
import { jsxs as _jsxs,
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useQuery } from "@tanstack/react-query";
|
|
3
3
|
import { useServerFn } from "@tanstack/react-start";
|
|
4
|
-
import {
|
|
4
|
+
import { useIsSuperadmin } from "@flowselections/core";
|
|
5
5
|
import { getMailboxStatus } from "../lib/mailbox.functions.js";
|
|
6
|
+
import { InlineAlert } from "./fs-ui/InlineAlert.js";
|
|
6
7
|
/** Pad naar de AI-koppelingen in de shell (Instellingen › AI-Authenticatie). */
|
|
7
8
|
export const AI_AUTH_PATH = "/ai-authenticatie";
|
|
8
|
-
const fmt = (iso) => iso ? new Date(iso).toLocaleString("nl-NL", { day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit" }) :
|
|
9
|
-
|
|
10
|
-
export function MailboxStatusLine() {
|
|
9
|
+
const fmt = (iso) => iso ? new Date(iso).toLocaleString("nl-NL", { day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit" }) : null;
|
|
10
|
+
function useMailboxStatus() {
|
|
11
11
|
const fetchStatus = useServerFn(getMailboxStatus);
|
|
12
12
|
const { data } = useQuery({
|
|
13
13
|
queryKey: ["mailbox-status"],
|
|
14
14
|
queryFn: () => fetchStatus(),
|
|
15
15
|
refetchInterval: 60000,
|
|
16
16
|
});
|
|
17
|
-
|
|
17
|
+
return (data ?? null);
|
|
18
|
+
}
|
|
19
|
+
/** Korte regel onder de paginatitel: wanneer opgehaald, hoeveel wachten. */
|
|
20
|
+
export function MailboxStatusMeta() {
|
|
21
|
+
const s = useMailboxStatus();
|
|
18
22
|
if (!s)
|
|
19
23
|
return null;
|
|
20
|
-
const disconnected = Array.isArray(s.disconnected) ? s.disconnected : [];
|
|
21
24
|
const addresses = Array.isArray(s.addresses) ? s.addresses : [];
|
|
22
|
-
|
|
25
|
+
const parts = [];
|
|
26
|
+
const last = fmt(s.lastOkAt);
|
|
27
|
+
parts.push(last ? `Mails opgehaald ${last}` : "Nog geen mails opgehaald");
|
|
28
|
+
if (s.waiting > 0)
|
|
29
|
+
parts.push(`${s.waiting} wachten op verwerking`);
|
|
30
|
+
if (addresses.length > 0)
|
|
31
|
+
parts.push(addresses.join(", "));
|
|
32
|
+
if (s.ignoredNotFollowed > 0)
|
|
33
|
+
parts.push(`${s.ignoredNotFollowed} uit mappen die niet gevolgd worden`);
|
|
34
|
+
if (s.ignoredUnknown > 0)
|
|
35
|
+
parts.push(`${s.ignoredUnknown} uit een onbekende map overgeslagen`);
|
|
36
|
+
return _jsx("span", { className: "tabular-nums", children: parts.join(" · ") });
|
|
37
|
+
}
|
|
38
|
+
/** Meldingen over het ophalen (alleen als de gebruiker er iets mee moet). */
|
|
39
|
+
export function MailboxStatusLine() {
|
|
40
|
+
const s = useMailboxStatus();
|
|
41
|
+
const { isAdmin } = useIsSuperadmin();
|
|
42
|
+
if (!s)
|
|
43
|
+
return null;
|
|
44
|
+
const disconnected = Array.isArray(s.disconnected) ? s.disconnected : [];
|
|
45
|
+
return (_jsxs(_Fragment, { children: [disconnected.map((d) => (_jsx(InlineAlert, { tone: "info", title: `${d.count} mails van ${d.email} niet verwerkt (adres ontkoppeld)` }, d.email))), s.problem && (_jsxs(InlineAlert, { tone: "error", title: "Er komen mails binnen, maar ze worden nu niet verwerkt.", children: ["We proberen het elke minuut opnieuw. Blijft dit staan, neem dan contact op met je beheerder.", isAdmin && (_jsxs("details", { className: "mt-1 text-xs text-muted-foreground", children: [_jsx("summary", { className: "cursor-pointer", children: "Details" }), s.problem] }))] })), !s.aiAgent && (_jsx(InlineAlert, { tone: "warning", title: "Automatisch uitlezen staat uit.", action: _jsx("a", { href: AI_AUTH_PATH, className: "text-sm font-medium underline underline-offset-2", children: "Naar Instellingen" }), children: "Bestellingen komen wel binnen, maar je vult ze zelf in. Een beheerder kan dit aanzetten bij Instellingen." }))] }));
|
|
23
46
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
export interface DataColumn<T> {
|
|
3
|
+
key: string;
|
|
4
|
+
header: string;
|
|
5
|
+
/** CSS grid-breedte, bijv. "2fr" of "140px". Schaalt evenredig mee. */
|
|
6
|
+
width: string;
|
|
7
|
+
cell: (row: T) => ReactNode;
|
|
8
|
+
/** Klikbare kop voor sorteren. */
|
|
9
|
+
sortable?: boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Tabel in één afgerond vlak. Kop blijft zichtbaar bij scrollen, vaste
|
|
13
|
+
* kolombreedtes, rij 60 px, geselecteerde rij met groene streep links.
|
|
14
|
+
* Onder 768 px als kaarten (mobileCard).
|
|
15
|
+
*/
|
|
16
|
+
export declare function DataTable<T>({ rows, columns, rowKey, onRowClick, activeIndex, mobileCard, sort, onSortChange, footer, rowAction, }: {
|
|
17
|
+
rows: T[];
|
|
18
|
+
columns: DataColumn<T>[];
|
|
19
|
+
rowKey: (row: T) => string;
|
|
20
|
+
onRowClick?: (row: T, index: number) => void;
|
|
21
|
+
activeIndex?: number | null;
|
|
22
|
+
mobileCard: (row: T) => ReactNode;
|
|
23
|
+
sort?: {
|
|
24
|
+
key: string;
|
|
25
|
+
dir: "asc" | "desc";
|
|
26
|
+
} | null;
|
|
27
|
+
onSortChange?: (key: string) => void;
|
|
28
|
+
footer?: ReactNode;
|
|
29
|
+
/** Extra actie (menu of knop) rechts in de rij; klik opent de rij niet. */
|
|
30
|
+
rowAction?: (row: T) => ReactNode;
|
|
31
|
+
}): import("react").JSX.Element;
|
|
32
|
+
/** Initialen-avatar voor klantnamen. */
|
|
33
|
+
export declare function InitialsAvatar({ name }: {
|
|
34
|
+
name: string;
|
|
35
|
+
}): import("react").JSX.Element;
|
|
36
|
+
//# sourceMappingURL=DataTable.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DataTable.d.ts","sourceRoot":"","sources":["../../../src/components/fs-ui/DataTable.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAqB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAG1D,MAAM,WAAW,UAAU,CAAC,CAAC;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,uEAAuE;IACvE,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,SAAS,CAAC;IAC5B,kCAAkC;IAClC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,EAC3B,IAAI,EACJ,OAAO,EACP,MAAM,EACN,UAAU,EACV,WAAW,EACX,UAAU,EACV,IAAI,EACJ,YAAY,EACZ,MAAM,EACN,SAAS,GACV,EAAE;IACD,IAAI,EAAE,CAAC,EAAE,CAAC;IACV,OAAO,EAAE,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC;IACzB,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,MAAM,CAAC;IAC3B,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7C,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,UAAU,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,SAAS,CAAC;IAClC,IAAI,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,KAAK,GAAG,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACnD,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,2EAA2E;IAC3E,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,SAAS,CAAC;CACnC,+BA4EA;AAED,wCAAwC;AACxC,wBAAgB,cAAc,CAAC,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,+BAQxD"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
|
+
import { ArrowDown, ArrowUp, ChevronRight } from "lucide-react";
|
|
4
|
+
/**
|
|
5
|
+
* Tabel in één afgerond vlak. Kop blijft zichtbaar bij scrollen, vaste
|
|
6
|
+
* kolombreedtes, rij 60 px, geselecteerde rij met groene streep links.
|
|
7
|
+
* Onder 768 px als kaarten (mobileCard).
|
|
8
|
+
*/
|
|
9
|
+
export function DataTable({ rows, columns, rowKey, onRowClick, activeIndex, mobileCard, sort, onSortChange, footer, rowAction, }) {
|
|
10
|
+
const grid = columns.map((c) => c.width).join(" ") + (rowAction || onRowClick ? " 48px" : "");
|
|
11
|
+
const activeRef = useRef(null);
|
|
12
|
+
useEffect(() => { activeRef.current?.scrollIntoView({ block: "nearest" }); }, [activeIndex]);
|
|
13
|
+
return (_jsxs("div", { className: "flex min-h-0 flex-1 flex-col overflow-hidden rounded-xl border border-border bg-card shadow-sm", children: [_jsx("div", { className: "hidden min-h-0 flex-1 overflow-auto overscroll-contain md:block", children: _jsxs("div", { className: "min-w-[900px]", role: "table", children: [_jsxs("div", { role: "row", className: "sticky top-0 z-10 grid h-10 items-center gap-4 border-b border-border bg-card px-4", style: { gridTemplateColumns: grid }, children: [columns.map((c) => {
|
|
14
|
+
const active = sort?.key === c.key;
|
|
15
|
+
const label = _jsx("span", { className: "text-[11px] font-semibold uppercase tracking-wider text-muted-foreground", children: c.header });
|
|
16
|
+
return (_jsx("div", { role: "columnheader", className: "min-w-0 truncate text-left", children: c.sortable && onSortChange ? (_jsxs("button", { type: "button", onClick: () => onSortChange(c.key), className: "inline-flex items-center gap-1 rounded-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", "aria-label": `Sorteer op ${c.header}`, children: [label, active && (sort.dir === "asc" ? _jsx(ArrowUp, { className: "h-3 w-3 text-muted-foreground" }) : _jsx(ArrowDown, { className: "h-3 w-3 text-muted-foreground" }))] })) : label }, c.key));
|
|
17
|
+
}), (rowAction || onRowClick) && _jsx("div", {})] }), rows.map((r, i) => {
|
|
18
|
+
const active = activeIndex === i;
|
|
19
|
+
return (_jsxs("div", { role: "row", ref: active ? activeRef : undefined, tabIndex: onRowClick ? 0 : undefined, onClick: onRowClick ? () => onRowClick(r, i) : undefined, onKeyDown: onRowClick ? (e) => { if (e.key === "Enter" && e.target === e.currentTarget) {
|
|
20
|
+
e.preventDefault();
|
|
21
|
+
onRowClick(r, i);
|
|
22
|
+
} } : undefined, className: "group relative grid h-[60px] items-center gap-4 border-b border-border px-4 text-sm text-foreground last:border-0 " +
|
|
23
|
+
"transition-colors duration-150 ease-out motion-reduce:transition-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring " +
|
|
24
|
+
(onRowClick ? "cursor-pointer hover:bg-muted/50 " : "") +
|
|
25
|
+
(active ? "bg-muted/50 before:absolute before:inset-y-0 before:left-0 before:w-[3px] before:bg-primary" : ""), style: { gridTemplateColumns: grid }, children: [columns.map((c) => _jsx("div", { role: "cell", className: "min-w-0", children: c.cell(r) }, c.key)), (rowAction || onRowClick) && (_jsx("div", { className: "flex justify-end", onClick: rowAction ? (e) => e.stopPropagation() : undefined, children: rowAction ? rowAction(r) : (_jsx(ChevronRight, { className: "h-4 w-4 text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100", "aria-hidden": true })) }))] }, rowKey(r)));
|
|
26
|
+
})] }) }), _jsx("div", { className: "min-h-0 flex-1 divide-y divide-border overflow-y-auto overscroll-contain md:hidden", children: rows.map((r, i) => (_jsx("div", { role: onRowClick ? "button" : undefined, tabIndex: onRowClick ? 0 : undefined, onClick: onRowClick ? () => onRowClick(r, i) : undefined, className: "min-h-11 p-4 text-sm transition-colors active:bg-muted " + (activeIndex === i ? "bg-muted/50" : ""), children: _jsxs("div", { className: "flex items-start gap-3", children: [_jsx("div", { className: "min-w-0 flex-1", children: mobileCard(r) }), rowAction && _jsx("div", { onClick: (e) => e.stopPropagation(), children: rowAction(r) })] }) }, rowKey(r)))) }), footer && _jsx("div", { className: "shrink-0 border-t border-border", children: footer })] }));
|
|
27
|
+
}
|
|
28
|
+
/** Initialen-avatar voor klantnamen. */
|
|
29
|
+
export function InitialsAvatar({ name }) {
|
|
30
|
+
const parts = name.trim().split(/\s+/).filter(Boolean);
|
|
31
|
+
const ini = ((parts[0]?.[0] ?? "") + (parts[1]?.[0] ?? "")).toUpperCase() || "?";
|
|
32
|
+
return (_jsx("span", { className: "inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-primary/10 text-xs font-semibold text-primary", "aria-hidden": true, children: ini }));
|
|
33
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
/** Lege toestand: groot zacht icoon, korte zin, één actie. */
|
|
3
|
+
export declare function EmptyState({ icon, title, text, action, }: {
|
|
4
|
+
icon: ReactNode;
|
|
5
|
+
title: string;
|
|
6
|
+
text?: string;
|
|
7
|
+
action?: ReactNode;
|
|
8
|
+
}): import("react").JSX.Element;
|
|
9
|
+
//# sourceMappingURL=EmptyState.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EmptyState.d.ts","sourceRoot":"","sources":["../../../src/components/fs-ui/EmptyState.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,8DAA8D;AAC9D,wBAAgB,UAAU,CAAC,EACzB,IAAI,EACJ,KAAK,EACL,IAAI,EACJ,MAAM,GACP,EAAE;IACD,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,SAAS,CAAC;CACpB,+BAaA"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/** Lege toestand: groot zacht icoon, korte zin, één actie. */
|
|
3
|
+
export function EmptyState({ icon, title, text, action, }) {
|
|
4
|
+
return (_jsxs("div", { className: "flex flex-col items-center gap-3 px-4 py-14 text-center", children: [_jsx("span", { className: "inline-flex h-14 w-14 items-center justify-center rounded-full bg-muted text-muted-foreground", "aria-hidden": true, children: icon }), _jsxs("div", { children: [_jsx("div", { className: "text-sm font-semibold text-foreground", children: title }), text && _jsx("div", { className: "mt-1 max-w-sm text-sm text-muted-foreground", children: text })] }), action] }));
|
|
5
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export interface FilterChip {
|
|
3
|
+
key: string;
|
|
4
|
+
label: string;
|
|
5
|
+
icon?: ReactNode;
|
|
6
|
+
onRemove: () => void;
|
|
7
|
+
}
|
|
8
|
+
/** Actieve filters als ronde chips met kruisje, plus "Alles wissen". */
|
|
9
|
+
export declare function FilterChips({ chips, onClearAll }: {
|
|
10
|
+
chips: FilterChip[];
|
|
11
|
+
onClearAll?: () => void;
|
|
12
|
+
}): import("react").JSX.Element | null;
|
|
13
|
+
//# sourceMappingURL=FilterChips.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FilterChips.d.ts","sourceRoot":"","sources":["../../../src/components/fs-ui/FilterChips.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAGvC,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,QAAQ,EAAE,MAAM,IAAI,CAAC;CACtB;AAED,wEAAwE;AACxE,wBAAgB,WAAW,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE;IAAE,KAAK,EAAE,UAAU,EAAE,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,IAAI,CAAA;CAAE,sCAyBlG"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { X } from "lucide-react";
|
|
3
|
+
/** Actieve filters als ronde chips met kruisje, plus "Alles wissen". */
|
|
4
|
+
export function FilterChips({ chips, onClearAll }) {
|
|
5
|
+
if (chips.length === 0)
|
|
6
|
+
return null;
|
|
7
|
+
return (_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [chips.map((c) => (_jsxs("span", { className: "inline-flex h-8 items-center gap-1.5 rounded-full border border-border bg-card pl-3 pr-1 text-xs font-medium text-foreground [&_svg]:h-3.5 [&_svg]:w-3.5", children: [c.icon, c.label, _jsx("button", { type: "button", "aria-label": `Filter ${c.label} weghalen`, onClick: c.onRemove, className: "inline-flex h-6 w-6 items-center justify-center rounded-full text-muted-foreground hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", children: _jsx(X, {}) })] }, c.key))), onClearAll && chips.length > 1 && (_jsx("button", { type: "button", onClick: onClearAll, className: "h-8 rounded-lg px-2 text-xs font-medium text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", children: "Alles wissen" }))] }));
|
|
8
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Tooltip op basis van Popover (core heeft geen Tooltip-export): opent bij
|
|
4
|
+
* aanwijzen én bij toetsenbordfocus, sluit met Esc of bij verlaten. De
|
|
5
|
+
* trigger is een knop met een toegankelijke naam.
|
|
6
|
+
*/
|
|
7
|
+
export declare function HoverTip({ content, label, children, side, align, className, }: {
|
|
8
|
+
align?: "start" | "center" | "end";
|
|
9
|
+
content: ReactNode;
|
|
10
|
+
label: string;
|
|
11
|
+
children: ReactNode;
|
|
12
|
+
side?: "top" | "bottom" | "left" | "right";
|
|
13
|
+
className?: string;
|
|
14
|
+
}): import("react").JSX.Element;
|
|
15
|
+
//# sourceMappingURL=HoverTip.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HoverTip.d.ts","sourceRoot":"","sources":["../../../src/components/fs-ui/HoverTip.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAoB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAGzD;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,EACvB,OAAO,EACP,KAAK,EACL,QAAQ,EACR,IAAY,EACZ,KAAgB,EAChB,SAAc,GACf,EAAE;IACD,KAAK,CAAC,EAAE,OAAO,GAAG,QAAQ,GAAG,KAAK,CAAC;IACnC,OAAO,EAAE,SAAS,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,SAAS,CAAC;IACpB,IAAI,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,CAAC;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,+BA4CA"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useRef, useState } from "react";
|
|
3
|
+
import { Popover, PopoverContent, PopoverTrigger } from "@flowselections/core";
|
|
4
|
+
/**
|
|
5
|
+
* Tooltip op basis van Popover (core heeft geen Tooltip-export): opent bij
|
|
6
|
+
* aanwijzen én bij toetsenbordfocus, sluit met Esc of bij verlaten. De
|
|
7
|
+
* trigger is een knop met een toegankelijke naam.
|
|
8
|
+
*/
|
|
9
|
+
export function HoverTip({ content, label, children, side = "top", align = "center", className = "", }) {
|
|
10
|
+
const [open, setOpen] = useState(false);
|
|
11
|
+
const timer = useRef(null);
|
|
12
|
+
const show = () => {
|
|
13
|
+
if (timer.current)
|
|
14
|
+
clearTimeout(timer.current);
|
|
15
|
+
setOpen(true);
|
|
16
|
+
};
|
|
17
|
+
const hide = () => {
|
|
18
|
+
if (timer.current)
|
|
19
|
+
clearTimeout(timer.current);
|
|
20
|
+
timer.current = setTimeout(() => setOpen(false), 80);
|
|
21
|
+
};
|
|
22
|
+
return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { type: "button", "aria-label": label, onPointerEnter: show, onPointerLeave: hide, onFocus: show, onBlur: hide, onClick: (e) => e.stopPropagation(), className: "inline-flex shrink-0 items-center justify-center rounded-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring " +
|
|
23
|
+
className, children: children }), _jsx(PopoverContent, { side: side, align: align, avoidCollisions: true, collisionPadding: 8, onOpenAutoFocus: (e) => e.preventDefault(), onCloseAutoFocus: (e) => e.preventDefault(), onPointerEnter: show, onPointerLeave: hide, onClick: (e) => e.stopPropagation(), className: "w-auto min-w-[14rem] max-w-[20rem] rounded-lg p-3 text-xs leading-snug shadow-lg motion-reduce:animate-none", children: content })] }));
|
|
24
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
/** Melding: zacht vlak, icoon links, actie rechts. Op mobiel ingeklapt tot één regel. */
|
|
3
|
+
export declare function InlineAlert({ tone, title, children, action, }: {
|
|
4
|
+
tone?: "warning" | "error" | "info";
|
|
5
|
+
title: string;
|
|
6
|
+
children?: ReactNode;
|
|
7
|
+
action?: ReactNode;
|
|
8
|
+
}): import("react").JSX.Element;
|
|
9
|
+
//# sourceMappingURL=InlineAlert.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"InlineAlert.d.ts","sourceRoot":"","sources":["../../../src/components/fs-ui/InlineAlert.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAY,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAGjD,yFAAyF;AACzF,wBAAgB,WAAW,CAAC,EAC1B,IAAgB,EAChB,KAAK,EACL,QAAQ,EACR,MAAM,GACP,EAAE;IACD,IAAI,CAAC,EAAE,SAAS,GAAG,OAAO,GAAG,MAAM,CAAC;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,MAAM,CAAC,EAAE,SAAS,CAAC;CACpB,+BA0BA"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { AlertTriangle, Info, XCircle } from "lucide-react";
|
|
4
|
+
/** Melding: zacht vlak, icoon links, actie rechts. Op mobiel ingeklapt tot één regel. */
|
|
5
|
+
export function InlineAlert({ tone = "warning", title, children, action, }) {
|
|
6
|
+
const [open, setOpen] = useState(false);
|
|
7
|
+
const Icon = tone === "error" ? XCircle : tone === "info" ? Info : AlertTriangle;
|
|
8
|
+
const cls = tone === "error"
|
|
9
|
+
? "border-destructive/30 bg-destructive/5 text-foreground [&_.ia-icon]:text-destructive"
|
|
10
|
+
: tone === "info"
|
|
11
|
+
? "border-border bg-muted/50 text-foreground [&_.ia-icon]:text-muted-foreground"
|
|
12
|
+
: "border-warning/30 bg-warning-soft text-warning-soft-foreground [&_.ia-icon]:text-warning-soft-foreground";
|
|
13
|
+
return (_jsxs("div", { role: tone === "error" ? "alert" : "status", className: "flex shrink-0 items-start gap-3 rounded-lg border px-3 py-2.5 text-sm " + cls, children: [_jsx(Icon, { className: "ia-icon mt-0.5 h-4 w-4 shrink-0", "aria-hidden": true }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("button", { type: "button", onClick: () => setOpen((v) => !v), className: "block w-full text-left font-medium sm:pointer-events-none", "aria-expanded": open, children: _jsx("span", { className: open ? "" : "line-clamp-1 sm:line-clamp-none", children: title }) }), children && _jsx("div", { className: "mt-0.5 text-sm opacity-90 " + (open ? "" : "hidden sm:block"), children: children })] }), action && _jsx("div", { className: "shrink-0", children: action })] }));
|
|
14
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
/** Rij KPI-tegels: 4 naast elkaar vanaf desktop, 2 op tablet, veegbaar op mobiel. */
|
|
3
|
+
export declare function KpiRow({ children }: {
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
}): import("react").JSX.Element;
|
|
6
|
+
//# sourceMappingURL=KpiRow.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"KpiRow.d.ts","sourceRoot":"","sources":["../../../src/components/fs-ui/KpiRow.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,qFAAqF;AACrF,wBAAgB,MAAM,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,+BAY3D"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/** Rij KPI-tegels: 4 naast elkaar vanaf desktop, 2 op tablet, veegbaar op mobiel. */
|
|
3
|
+
export function KpiRow({ children }) {
|
|
4
|
+
return (_jsx("div", { className: "-mx-4 flex shrink-0 snap-x snap-mandatory gap-3 overflow-x-auto px-4 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden " +
|
|
5
|
+
"md:mx-0 md:grid md:grid-cols-2 md:overflow-visible md:px-0 lg:grid-cols-4 " +
|
|
6
|
+
"[&>*]:w-[78%] [&>*]:shrink-0 [&>*]:snap-start md:[&>*]:w-auto", children: children }));
|
|
7
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export interface KpiTrend {
|
|
3
|
+
/** Procentuele wijziging t.o.v. de vorige periode (null = geen vergelijking). */
|
|
4
|
+
pct: number | null;
|
|
5
|
+
label: string;
|
|
6
|
+
/** Hoger is beter (omzet) of slechter (actie nodig). */
|
|
7
|
+
goodWhenUp?: boolean;
|
|
8
|
+
}
|
|
9
|
+
/** KPI-tegel: icoon, label, groot getal, subregel en optionele trend. Alleen weergave. */
|
|
10
|
+
export declare function KpiTile({ title, value, sub, icon, tone, loading, onClick, active, trend, }: {
|
|
11
|
+
title: string;
|
|
12
|
+
value: ReactNode;
|
|
13
|
+
sub?: ReactNode;
|
|
14
|
+
icon?: ReactNode;
|
|
15
|
+
tone?: "default" | "warning";
|
|
16
|
+
loading?: boolean;
|
|
17
|
+
onClick?: () => void;
|
|
18
|
+
active?: boolean;
|
|
19
|
+
trend?: KpiTrend | null;
|
|
20
|
+
}): import("react").JSX.Element;
|
|
21
|
+
//# sourceMappingURL=KpiTile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"KpiTile.d.ts","sourceRoot":"","sources":["../../../src/components/fs-ui/KpiTile.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAIvC,MAAM,WAAW,QAAQ;IACvB,iFAAiF;IACjF,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,wDAAwD;IACxD,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAoCD,0FAA0F;AAC1F,wBAAgB,OAAO,CAAC,EACtB,KAAK,EACL,KAAK,EACL,GAAG,EACH,IAAI,EACJ,IAAgB,EAChB,OAAO,EACP,OAAO,EACP,MAAM,EACN,KAAK,GACN,EAAE;IACD,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,SAAS,CAAC;IACjB,GAAG,CAAC,EAAE,SAAS,CAAC;IAChB,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,IAAI,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;IAC7B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAC;CACzB,+BA4DA"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Card, Skeleton } from "@flowselections/core";
|
|
3
|
+
import { ChevronRight, Minus, TrendingDown, TrendingUp } from "lucide-react";
|
|
4
|
+
function Trend({ trend }) {
|
|
5
|
+
if (trend.pct == null)
|
|
6
|
+
return null;
|
|
7
|
+
const flat = Math.abs(trend.pct) < 0.5;
|
|
8
|
+
const up = trend.pct > 0;
|
|
9
|
+
const good = flat ? null : up === (trend.goodWhenUp ?? true);
|
|
10
|
+
const Icon = flat ? Minus : up ? TrendingUp : TrendingDown;
|
|
11
|
+
const tone = good == null ? "text-muted-foreground" : good ? "text-primary" : "text-destructive";
|
|
12
|
+
return (_jsxs("span", { className: "inline-flex min-w-0 items-center gap-1 whitespace-nowrap text-xs font-medium tabular-nums " + tone, title: trend.label, children: [_jsx(Icon, { className: "h-3.5 w-3.5", "aria-hidden": true }), flat ? "0%" : `${up ? "+" : ""}${Math.round(trend.pct)}%`, _jsx("span", { className: "truncate font-normal text-muted-foreground", children: trend.label })] }));
|
|
13
|
+
}
|
|
14
|
+
/** Klein trendlijntje van vorige naar huidige periode (alleen weergave). */
|
|
15
|
+
function Spark({ trend }) {
|
|
16
|
+
if (trend.pct == null)
|
|
17
|
+
return null;
|
|
18
|
+
const up = trend.pct > 0.5;
|
|
19
|
+
const down = trend.pct < -0.5;
|
|
20
|
+
const good = up || down ? up === (trend.goodWhenUp ?? true) : null;
|
|
21
|
+
const tone = good == null ? "text-muted-foreground" : good ? "text-primary" : "text-destructive";
|
|
22
|
+
const y0 = up ? 26 : down ? 6 : 16;
|
|
23
|
+
const y1 = up ? 6 : down ? 26 : 16;
|
|
24
|
+
const mid = (y0 + y1) / 2;
|
|
25
|
+
const d = `M2 ${y0} C 14 ${y0}, 18 ${mid + (up ? 4 : down ? -4 : 0)}, 28 ${mid} S 44 ${mid - (up ? 6 : down ? -6 : 0)}, 50 ${(mid + y1) / 2} S 58 ${y1}, 62 ${y1}`;
|
|
26
|
+
return (_jsx("svg", { viewBox: "0 0 64 32", className: "hidden h-8 w-16 shrink-0 self-end sm:block " + tone, "aria-hidden": true, children: _jsx("path", { d: d, fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }) }));
|
|
27
|
+
}
|
|
28
|
+
/** KPI-tegel: icoon, label, groot getal, subregel en optionele trend. Alleen weergave. */
|
|
29
|
+
export function KpiTile({ title, value, sub, icon, tone = "default", loading, onClick, active, trend, }) {
|
|
30
|
+
const body = (_jsxs("div", { className: "flex items-center gap-3 sm:gap-4", children: [icon && (_jsx("span", { className: "inline-flex h-10 w-10 sm:h-12 sm:w-12 shrink-0 items-center justify-center rounded-xl [&_svg]:h-5 [&_svg]:w-5 sm:[&_svg]:h-6 sm:[&_svg]:w-6 " +
|
|
31
|
+
(tone === "warning" ? "bg-warning-soft text-warning-soft-foreground" : "bg-primary/10 text-primary"), "aria-hidden": true, children: icon })), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { className: "truncate text-[11px] font-semibold uppercase tracking-wider text-muted-foreground", children: title }), _jsx("div", { className: "mt-0.5 text-2xl font-semibold leading-tight tabular-nums text-foreground", children: loading ? _jsx(Skeleton, { className: "h-7 w-24" }) : value }), _jsx("div", { className: "mt-1 flex min-h-4 flex-col gap-0.5 text-xs text-muted-foreground", children: loading ? _jsx(Skeleton, { className: "h-3 w-32" }) : (_jsxs(_Fragment, { children: [_jsx("div", { className: "truncate", children: sub }), trend && _jsx(Trend, { trend: trend })] })) })] }), !loading && trend && _jsx(Spark, { trend: trend }), onClick && tone === "warning" && _jsx(ChevronRight, { className: "h-5 w-5 shrink-0 text-muted-foreground", "aria-hidden": true })] }));
|
|
32
|
+
return (_jsx(Card, { className: "h-full p-4 sm:p-5 transition-shadow " +
|
|
33
|
+
(onClick ? "hover:border-ring/50 hover:shadow-md " : "") +
|
|
34
|
+
(active ? "ring-2 ring-ring" : ""), children: onClick ? (_jsx("div", { role: "button", tabIndex: 0, onClick: onClick, onKeyDown: (e) => {
|
|
35
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
36
|
+
e.preventDefault();
|
|
37
|
+
onClick();
|
|
38
|
+
}
|
|
39
|
+
}, className: "block w-full cursor-pointer rounded-sm text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", children: body })) : (body) }));
|
|
40
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
/** Vaste, schermbrede hoofdknop onderin op mobiel (onder 768 px). */
|
|
3
|
+
export declare function MobilePrimaryAction({ label, icon, onClick, disabled, hidden, }: {
|
|
4
|
+
label: string;
|
|
5
|
+
icon?: ReactNode;
|
|
6
|
+
onClick: () => void;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
hidden?: boolean;
|
|
9
|
+
}): import("react").JSX.Element | null;
|
|
10
|
+
//# sourceMappingURL=MobilePrimaryAction.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MobilePrimaryAction.d.ts","sourceRoot":"","sources":["../../../src/components/fs-ui/MobilePrimaryAction.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,qEAAqE;AACrE,wBAAgB,mBAAmB,CAAC,EAClC,KAAK,EACL,IAAI,EACJ,OAAO,EACP,QAAQ,EACR,MAAM,GACP,EAAE;IACD,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,sCAeA"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/** Vaste, schermbrede hoofdknop onderin op mobiel (onder 768 px). */
|
|
3
|
+
export function MobilePrimaryAction({ label, icon, onClick, disabled, hidden, }) {
|
|
4
|
+
if (hidden)
|
|
5
|
+
return null;
|
|
6
|
+
return (_jsx("div", { className: "fixed inset-x-0 bottom-0 z-30 border-t border-border bg-background/95 px-4 pt-3 shadow-lg backdrop-blur md:hidden", style: { paddingBottom: "calc(env(safe-area-inset-bottom) + 12px)" }, children: _jsxs("button", { type: "button", onClick: onClick, disabled: disabled, className: "inline-flex h-12 w-full items-center justify-center gap-2 rounded-lg bg-primary text-base font-semibold text-primary-foreground transition-opacity active:opacity-80 disabled:opacity-50 [&_svg]:h-5 [&_svg]:w-5", children: [icon, label] }) }));
|
|
7
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
/** Paginakop: titel + bijgewerkt-tijd links, acties rechts. Alleen weergave. */
|
|
3
|
+
export declare function PageHeader({ title, meta, actions, }: {
|
|
4
|
+
title: string;
|
|
5
|
+
meta?: ReactNode;
|
|
6
|
+
actions?: ReactNode;
|
|
7
|
+
}): import("react").JSX.Element;
|
|
8
|
+
//# sourceMappingURL=PageHeader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PageHeader.d.ts","sourceRoot":"","sources":["../../../src/components/fs-ui/PageHeader.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,gFAAgF;AAChF,wBAAgB,UAAU,CAAC,EACzB,KAAK,EACL,IAAI,EACJ,OAAO,GACR,EAAE;IACD,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,OAAO,CAAC,EAAE,SAAS,CAAC;CACrB,+BAUA"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/** Paginakop: titel + bijgewerkt-tijd links, acties rechts. Alleen weergave. */
|
|
3
|
+
export function PageHeader({ title, meta, actions, }) {
|
|
4
|
+
return (_jsxs("div", { className: "flex shrink-0 flex-wrap items-start justify-between gap-2", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("h1", { className: "truncate text-xl font-bold leading-tight text-foreground sm:text-2xl", children: title }), meta && _jsx("div", { className: "mt-0.5 text-xs text-muted-foreground", children: meta })] }), actions && _jsx("div", { className: "flex flex-wrap items-center gap-2", children: actions })] }));
|
|
5
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export interface PillTab {
|
|
3
|
+
key: string;
|
|
4
|
+
label: string;
|
|
5
|
+
icon?: ReactNode;
|
|
6
|
+
count?: number | null;
|
|
7
|
+
}
|
|
8
|
+
/** Tabbladen als pillen met icoon, label en teller. Veegbaar op mobiel. */
|
|
9
|
+
export declare function PillTabs({ tabs, active, onSelect, ariaLabel, }: {
|
|
10
|
+
tabs: PillTab[];
|
|
11
|
+
active: string;
|
|
12
|
+
onSelect: (key: string) => void;
|
|
13
|
+
ariaLabel?: string;
|
|
14
|
+
}): import("react").JSX.Element;
|
|
15
|
+
//# sourceMappingURL=PillTabs.d.ts.map
|