@flowselections/mailbox-orders 1.0.1
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/_core-safelist.d.ts +2 -0
- package/dist-lib/_core-safelist.d.ts.map +1 -0
- package/dist-lib/_core-safelist.js +15 -0
- package/dist-lib/components/MailboxInboxPage.d.ts +2 -0
- package/dist-lib/components/MailboxInboxPage.d.ts.map +1 -0
- package/dist-lib/components/MailboxInboxPage.js +61 -0
- package/dist-lib/components/MailboxProcessedPage.d.ts +2 -0
- package/dist-lib/components/MailboxProcessedPage.d.ts.map +1 -0
- package/dist-lib/components/MailboxProcessedPage.js +21 -0
- package/dist-lib/components/MailboxProposalsPage.d.ts +2 -0
- package/dist-lib/components/MailboxProposalsPage.d.ts.map +1 -0
- package/dist-lib/components/MailboxProposalsPage.js +198 -0
- package/dist-lib/components/SearchableSelect.d.ts +18 -0
- package/dist-lib/components/SearchableSelect.d.ts.map +1 -0
- package/dist-lib/components/SearchableSelect.js +20 -0
- package/dist-lib/components/settings/ImapAccountsCard.d.ts +2 -0
- package/dist-lib/components/settings/ImapAccountsCard.d.ts.map +1 -0
- package/dist-lib/components/settings/ImapAccountsCard.js +136 -0
- package/dist-lib/components/settings/ImapProfilesCard.d.ts +2 -0
- package/dist-lib/components/settings/ImapProfilesCard.d.ts.map +1 -0
- package/dist-lib/components/settings/ImapProfilesCard.js +101 -0
- package/dist-lib/components/settings/MailboxOrderTemplateCard.d.ts +2 -0
- package/dist-lib/components/settings/MailboxOrderTemplateCard.d.ts.map +1 -0
- package/dist-lib/components/settings/MailboxOrderTemplateCard.js +98 -0
- package/dist-lib/components/settings/MailboxSettingsCard.d.ts +2 -0
- package/dist-lib/components/settings/MailboxSettingsCard.d.ts.map +1 -0
- package/dist-lib/components/settings/MailboxSettingsCard.js +85 -0
- package/dist-lib/index.d.ts +12 -0
- package/dist-lib/index.d.ts.map +1 -0
- package/dist-lib/index.js +34 -0
- package/dist-lib/integrations/supabase/auth-attacher.d.ts +2 -0
- package/dist-lib/integrations/supabase/auth-attacher.d.ts.map +1 -0
- package/dist-lib/integrations/supabase/auth-attacher.js +15 -0
- package/dist-lib/integrations/supabase/auth-middleware.d.ts +2978 -0
- package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -0
- package/dist-lib/integrations/supabase/auth-middleware.js +52 -0
- package/dist-lib/integrations/supabase/client.d.ts +2974 -0
- package/dist-lib/integrations/supabase/client.d.ts.map +1 -0
- package/dist-lib/integrations/supabase/client.js +13 -0
- package/dist-lib/integrations/supabase/client.server.d.ts +2974 -0
- package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -0
- package/dist-lib/integrations/supabase/client.server.js +30 -0
- package/dist-lib/integrations/supabase/types.d.ts +3119 -0
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -0
- package/dist-lib/integrations/supabase/types.js +8 -0
- package/dist-lib/lib/imap.functions.d.ts +32852 -0
- package/dist-lib/lib/imap.functions.d.ts.map +1 -0
- package/dist-lib/lib/imap.functions.js +235 -0
- package/dist-lib/lib/mailbox-ai.server.d.ts +32 -0
- package/dist-lib/lib/mailbox-ai.server.d.ts.map +1 -0
- package/dist-lib/lib/mailbox-ai.server.js +107 -0
- package/dist-lib/lib/mailbox-auto.server.d.ts +9 -0
- package/dist-lib/lib/mailbox-auto.server.d.ts.map +1 -0
- package/dist-lib/lib/mailbox-auto.server.js +198 -0
- package/dist-lib/lib/mailbox-template.functions.d.ts +17913 -0
- package/dist-lib/lib/mailbox-template.functions.d.ts.map +1 -0
- package/dist-lib/lib/mailbox-template.functions.js +106 -0
- package/dist-lib/lib/mailbox.functions.d.ts +32888 -0
- package/dist-lib/lib/mailbox.functions.d.ts.map +1 -0
- package/dist-lib/lib/mailbox.functions.js +334 -0
- package/dist-lib/lib/utils.d.ts +3 -0
- package/dist-lib/lib/utils.d.ts.map +1 -0
- package/dist-lib/lib/utils.js +5 -0
- package/dist-lib/lib/validationSchemas.d.ts +15 -0
- package/dist-lib/lib/validationSchemas.d.ts.map +1 -0
- package/dist-lib/lib/validationSchemas.js +25 -0
- package/dist-lib/styles.css +1 -0
- package/package.json +96 -0
- package/public/flowselections-assets/template-module/README.md +15 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_core-safelist.d.ts","sourceRoot":"","sources":["../src/_core-safelist.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Forceert Tailwind classes die uit @flowselections/core komen maar niet
|
|
2
|
+
// voorkomen in de eigen broncode van deze module.
|
|
3
|
+
// Zonder dit bestand worden deze classes weggeoptimaliseerd op de
|
|
4
|
+
// Lovable productie build.
|
|
5
|
+
const _safelist = [
|
|
6
|
+
// Layout — Sidebar en shell structuur
|
|
7
|
+
'h-full', 'flex-col', 'flex-1', 'min-h-screen', 'ml-64', 'pt-16',
|
|
8
|
+
'items-start', 'items-center', 'justify-between', 'border-t',
|
|
9
|
+
'w-64', 'h-screen', 'h-20', 'overflow-y-auto',
|
|
10
|
+
// Grid — InstellingenPage
|
|
11
|
+
'grid', 'lg:grid-cols-2', 'gap-6', 'p-6', 'space-y-6',
|
|
12
|
+
// Toggle/Switch — aan/uit zichtbaarheid
|
|
13
|
+
'data-[state=checked]:bg-primary', 'data-[state=unchecked]:bg-input',
|
|
14
|
+
];
|
|
15
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MailboxInboxPage.d.ts","sourceRoot":"","sources":["../../src/components/MailboxInboxPage.tsx"],"names":[],"mappings":"AAmBA,wBAAgB,gBAAgB,gCAoH/B"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
3
|
+
import { useServerFn } from "@tanstack/react-start";
|
|
4
|
+
import { Button, Badge, Card, CardContent, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, toast, } from "@flowselections/core";
|
|
5
|
+
import { Mail, RefreshCw, Sparkles } from "lucide-react";
|
|
6
|
+
import { listMessages, pollMailbox, parseMessage } from "../lib/mailbox.functions";
|
|
7
|
+
const STATUS_VARIANT = {
|
|
8
|
+
new: "secondary",
|
|
9
|
+
parsing: "outline",
|
|
10
|
+
parsed: "default",
|
|
11
|
+
failed: "destructive",
|
|
12
|
+
ignored: "outline",
|
|
13
|
+
converted: "default",
|
|
14
|
+
};
|
|
15
|
+
export function MailboxInboxPage() {
|
|
16
|
+
const qc = useQueryClient();
|
|
17
|
+
const fetchList = useServerFn(listMessages);
|
|
18
|
+
const fetchPoll = useServerFn(pollMailbox);
|
|
19
|
+
const fetchParse = useServerFn(parseMessage);
|
|
20
|
+
const { data, isLoading } = useQuery({
|
|
21
|
+
queryKey: ["mailbox-messages"],
|
|
22
|
+
queryFn: () => fetchList(),
|
|
23
|
+
});
|
|
24
|
+
const pollMut = useMutation({
|
|
25
|
+
mutationFn: () => fetchPoll(),
|
|
26
|
+
onSuccess: (res) => {
|
|
27
|
+
if (res?.ok) {
|
|
28
|
+
const ins = res.inserted ?? 0;
|
|
29
|
+
const app = res.autoApproved ?? 0;
|
|
30
|
+
if (ins === 0)
|
|
31
|
+
toast.success("Geen nieuwe e-mails");
|
|
32
|
+
else if (app > 0)
|
|
33
|
+
toast.success(`${ins} nieuwe e-mail(s) — ${app} automatisch omgezet in bestelling`);
|
|
34
|
+
else
|
|
35
|
+
toast.success(`${ins} nieuwe e-mail(s) opgehaald`);
|
|
36
|
+
qc.invalidateQueries({ queryKey: ["mailbox-messages"] });
|
|
37
|
+
qc.invalidateQueries({ queryKey: ["mailbox-proposals"] });
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
toast.error(res?.error ?? "Ophalen mislukt");
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
onError: (e) => toast.error(e?.message ?? "Ophalen mislukt"),
|
|
44
|
+
});
|
|
45
|
+
const parseMut = useMutation({
|
|
46
|
+
mutationFn: (messageId) => fetchParse({ data: { messageId } }),
|
|
47
|
+
onSuccess: (res) => {
|
|
48
|
+
if (res?.ok) {
|
|
49
|
+
toast.success("E-mail geparsed naar voorstel");
|
|
50
|
+
qc.invalidateQueries({ queryKey: ["mailbox-messages"] });
|
|
51
|
+
qc.invalidateQueries({ queryKey: ["mailbox-proposals"] });
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
toast.error(res?.error ?? "Parsen mislukt");
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
onError: (e) => toast.error(e?.message ?? "Parsen mislukt"),
|
|
58
|
+
});
|
|
59
|
+
const items = Array.isArray(data?.items) ? data.items : [];
|
|
60
|
+
return (_jsxs("div", { className: "p-6 space-y-6", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsxs("h1", { className: "text-2xl font-semibold flex items-center gap-2", children: [_jsx(Mail, { className: "h-6 w-6 text-primary" }), " Inbox"] }), _jsx("p", { className: "text-muted-foreground text-sm mt-1", children: "Binnenkomende bestel-e-mails uit de mailbox" })] }), _jsxs(Button, { onClick: () => pollMut.mutate(), disabled: pollMut.isPending, children: [_jsx(RefreshCw, { className: `h-4 w-4 mr-2 ${pollMut.isPending ? "animate-spin" : ""}` }), "Nu ophalen"] })] }), _jsx(Card, { children: _jsx(CardContent, { className: "p-0", children: isLoading ? (_jsx("div", { className: "p-8 text-center text-muted-foreground", children: "Laden..." })) : items.length === 0 ? (_jsx("div", { className: "p-8 text-center text-muted-foreground", children: "Nog geen e-mails. Klik \"Nu ophalen\" om de mailbox te controleren." })) : (_jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Status" }), _jsx(TableHead, { children: "Afzender" }), _jsx(TableHead, { children: "Onderwerp" }), _jsx(TableHead, { children: "Ontvangen" }), _jsx(TableHead, { className: "text-right", children: "Actie" })] }) }), _jsx(TableBody, { children: items.map((m) => (_jsxs(TableRow, { children: [_jsx(TableCell, { children: _jsx(Badge, { variant: STATUS_VARIANT[m.status] ?? "secondary", children: m.status }) }), _jsxs(TableCell, { children: [_jsx("div", { className: "text-sm font-medium", children: m.from_name || m.from_email }), m.from_name && _jsx("div", { className: "text-xs text-muted-foreground", children: m.from_email })] }), _jsx(TableCell, { className: "max-w-md truncate", children: m.subject || "(geen onderwerp)" }), _jsx(TableCell, { className: "text-sm text-muted-foreground", children: m.received_at ? new Date(m.received_at).toLocaleString("nl-NL") : "-" }), _jsx(TableCell, { className: "text-right", children: m.status !== "converted" && (_jsxs(Button, { size: "sm", variant: "outline", disabled: parseMut.isPending, onClick: () => parseMut.mutate(m.id), children: [_jsx(Sparkles, { className: "h-3 w-3 mr-1" }), m.status === "parsed" ? "Opnieuw parsen" : "Parse"] })) })] }, m.id))) })] })) }) })] }));
|
|
61
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MailboxProcessedPage.d.ts","sourceRoot":"","sources":["../../src/components/MailboxProcessedPage.tsx"],"names":[],"mappings":"AAQA,wBAAgB,oBAAoB,gCAuDnC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useQuery } from "@tanstack/react-query";
|
|
3
|
+
import { useServerFn } from "@tanstack/react-start";
|
|
4
|
+
import { Badge, Card, CardContent, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@flowselections/core";
|
|
5
|
+
import { listProposals } from "../lib/mailbox.functions";
|
|
6
|
+
export function MailboxProcessedPage() {
|
|
7
|
+
const fetchList = useServerFn(listProposals);
|
|
8
|
+
const approved = useQuery({
|
|
9
|
+
queryKey: ["mailbox-proposals", "approved"],
|
|
10
|
+
queryFn: () => fetchList({ data: { status: "approved" } }),
|
|
11
|
+
});
|
|
12
|
+
const rejected = useQuery({
|
|
13
|
+
queryKey: ["mailbox-proposals", "rejected"],
|
|
14
|
+
queryFn: () => fetchList({ data: { status: "rejected" } }),
|
|
15
|
+
});
|
|
16
|
+
const items = [
|
|
17
|
+
...(Array.isArray(approved.data?.items) ? approved.data.items : []),
|
|
18
|
+
...(Array.isArray(rejected.data?.items) ? rejected.data.items : []),
|
|
19
|
+
].sort((a, b) => (b.reviewed_at ?? "").localeCompare(a.reviewed_at ?? ""));
|
|
20
|
+
return (_jsxs("div", { className: "p-6 space-y-4", children: [_jsx("h1", { className: "text-2xl font-semibold", children: "Verwerkt" }), _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(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") : "-" })] }, p.id))) })] })) }) })] }));
|
|
21
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MailboxProposalsPage.d.ts","sourceRoot":"","sources":["../../src/components/MailboxProposalsPage.tsx"],"names":[],"mappings":"AA+BA,wBAAgB,oBAAoB,gCAqKnC"}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo, useState } from "react";
|
|
3
|
+
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
4
|
+
import { useServerFn } from "@tanstack/react-start";
|
|
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
|
+
import { Check, X, Trash2, Plus, AlertTriangle } from "lucide-react";
|
|
7
|
+
import { listProposals, approveProposal, rejectProposal, listCustomersForPicker, listProductsForPicker, } from "../lib/mailbox.functions";
|
|
8
|
+
import { listTemplateFields, listPlantTypesForPicker, listSuppliersForPicker, } from "../lib/mailbox-template.functions";
|
|
9
|
+
import { SearchableSelect } from "./SearchableSelect";
|
|
10
|
+
export function MailboxProposalsPage() {
|
|
11
|
+
const qc = useQueryClient();
|
|
12
|
+
const fetchList = useServerFn(listProposals);
|
|
13
|
+
const fetchTemplate = useServerFn(listTemplateFields);
|
|
14
|
+
const fetchApprove = useServerFn(approveProposal);
|
|
15
|
+
const fetchReject = useServerFn(rejectProposal);
|
|
16
|
+
const fetchCustomers = useServerFn(listCustomersForPicker);
|
|
17
|
+
const fetchProducts = useServerFn(listProductsForPicker);
|
|
18
|
+
const fetchPlantTypes = useServerFn(listPlantTypesForPicker);
|
|
19
|
+
const fetchSuppliers = useServerFn(listSuppliersForPicker);
|
|
20
|
+
const [status, setStatus] = useState("pending");
|
|
21
|
+
const { data, isLoading } = useQuery({
|
|
22
|
+
queryKey: ["mailbox-proposals", status],
|
|
23
|
+
queryFn: () => fetchList({ data: { status } }),
|
|
24
|
+
});
|
|
25
|
+
const { data: templateData } = useQuery({
|
|
26
|
+
queryKey: ["mailbox-template-fields"],
|
|
27
|
+
queryFn: () => fetchTemplate(),
|
|
28
|
+
});
|
|
29
|
+
const { data: customersData } = useQuery({ queryKey: ["mailbox-customers"], queryFn: () => fetchCustomers() });
|
|
30
|
+
const { data: productsData } = useQuery({ queryKey: ["mailbox-products"], queryFn: () => fetchProducts() });
|
|
31
|
+
const { data: plantData } = useQuery({ queryKey: ["mailbox-plant-types"], queryFn: () => fetchPlantTypes() });
|
|
32
|
+
const { data: supplierData } = useQuery({ queryKey: ["mailbox-suppliers"], queryFn: () => fetchSuppliers() });
|
|
33
|
+
const items = Array.isArray(data?.items) ? data.items : [];
|
|
34
|
+
const template = Array.isArray(templateData?.items) ? templateData.items : [];
|
|
35
|
+
const customers = Array.isArray(customersData?.items) ? customersData.items : [];
|
|
36
|
+
const products = Array.isArray(productsData?.items) ? productsData.items : [];
|
|
37
|
+
const plantTypes = Array.isArray(plantData?.items) ? plantData.items : [];
|
|
38
|
+
const suppliers = Array.isArray(supplierData?.items) ? supplierData.items : [];
|
|
39
|
+
const [activeId, setActiveId] = useState(null);
|
|
40
|
+
const active = items.find((p) => p.id === activeId) ?? null;
|
|
41
|
+
const statusLabel = (s) => s === "approved" ? "Goedgekeurd" : s === "rejected" ? "Afgewezen" : "Nieuw";
|
|
42
|
+
const statusVariant = (s) => s === "approved" ? "default" : s === "rejected" ? "destructive" : "secondary";
|
|
43
|
+
const customerNameFor = (p) => {
|
|
44
|
+
const fvs = Array.isArray(p.mailbox_proposal_field_values) ? p.mailbox_proposal_field_values : [];
|
|
45
|
+
const fv = fvs.find((v) => v.field_key === "customer");
|
|
46
|
+
const id = fv?.value ?? p.matched_customer_id ?? null;
|
|
47
|
+
if (id) {
|
|
48
|
+
const c = customers.find((x) => x.id === id);
|
|
49
|
+
if (c?.company_name)
|
|
50
|
+
return c.company_name;
|
|
51
|
+
}
|
|
52
|
+
return p.mailbox_messages?.from_name || p.mailbox_messages?.from_email || "Onbekend";
|
|
53
|
+
};
|
|
54
|
+
const orderNumberFor = (p) => `MB-${String(p.id).slice(0, 8).toUpperCase()}`;
|
|
55
|
+
return (_jsxs("div", { className: "p-6 space-y-4", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("h1", { className: "text-2xl font-semibold", children: "Voorstellen" }), _jsxs(Select, { value: status, onValueChange: (v) => setStatus(v), children: [_jsx(SelectTrigger, { className: "w-48", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "pending", children: "Nieuw / In behandeling" }), _jsx(SelectItem, { value: "approved", children: "Goedgekeurd" }), _jsx(SelectItem, { value: "rejected", children: "Afgekeurd" })] })] })] }), isLoading ? (_jsx("div", { className: "text-muted-foreground", children: "Laden..." })) : items.length === 0 ? (_jsx("p", { className: "text-muted-foreground", children: "Geen voorstellen in deze categorie." })) : (_jsx(Card, { children: _jsx(CardContent, { className: "p-0", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Ordernummer" }), _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) => {
|
|
56
|
+
const fvs = Array.isArray(p.mailbox_proposal_field_values) ? p.mailbox_proposal_field_values : [];
|
|
57
|
+
const needsReview = fvs.some((v) => v.needs_review);
|
|
58
|
+
const date = p.mailbox_messages?.received_at ?? p.created_at;
|
|
59
|
+
return (_jsxs(TableRow, { className: "cursor-pointer hover:bg-muted/50", onClick: () => setActiveId(p.id), children: [_jsx(TableCell, { className: "font-mono text-xs", children: orderNumberFor(p) }), _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) }), 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));
|
|
60
|
+
}) })] }) }) })), _jsx(Dialog, { open: !!active, onOpenChange: (o) => { if (!o)
|
|
61
|
+
setActiveId(null); }, children: _jsx(DialogContent, { className: "max-w-[95vw] w-[95vw] h-[92vh] max-h-[92vh] overflow-y-auto p-6", children: active && (_jsxs(_Fragment, { children: [_jsx(DialogHeader, { children: _jsxs(DialogTitle, { children: [orderNumberFor(active), " \u00B7 ", customerNameFor(active)] }) }), _jsx(ProposalEditor, { proposal: active, template: template, customers: customers, products: products, plantTypes: plantTypes, suppliers: suppliers, onApprove: async (payload) => {
|
|
62
|
+
try {
|
|
63
|
+
await fetchApprove({ data: payload });
|
|
64
|
+
toast.success("Bestelling aangemaakt");
|
|
65
|
+
qc.invalidateQueries({ queryKey: ["mailbox-proposals"] });
|
|
66
|
+
setActiveId(null);
|
|
67
|
+
}
|
|
68
|
+
catch (e) {
|
|
69
|
+
toast.error(e?.message ?? "Goedkeuren mislukt");
|
|
70
|
+
}
|
|
71
|
+
}, onReject: async (reason) => {
|
|
72
|
+
try {
|
|
73
|
+
await fetchReject({ data: { proposalId: active.id, reason } });
|
|
74
|
+
toast.success("Voorstel afgewezen");
|
|
75
|
+
qc.invalidateQueries({ queryKey: ["mailbox-proposals"] });
|
|
76
|
+
setActiveId(null);
|
|
77
|
+
}
|
|
78
|
+
catch (e) {
|
|
79
|
+
toast.error(e?.message ?? "Afwijzen mislukt");
|
|
80
|
+
}
|
|
81
|
+
} }, active.id)] })) }) })] }));
|
|
82
|
+
}
|
|
83
|
+
function ProposalEditor({ proposal, template, customers, products, plantTypes, suppliers, onApprove, onReject, }) {
|
|
84
|
+
const msg = proposal.mailbox_messages ?? {};
|
|
85
|
+
const fieldValues = Array.isArray(proposal.mailbox_proposal_field_values)
|
|
86
|
+
? proposal.mailbox_proposal_field_values : [];
|
|
87
|
+
// Map field_key -> current row
|
|
88
|
+
const fvMap = useMemo(() => {
|
|
89
|
+
const m = {};
|
|
90
|
+
fieldValues.forEach((v) => { m[v.field_key] = v; });
|
|
91
|
+
return m;
|
|
92
|
+
}, [fieldValues]);
|
|
93
|
+
const visibleFields = template.filter((f) => f.visible);
|
|
94
|
+
const [values, setValues] = useState(() => {
|
|
95
|
+
const init = {};
|
|
96
|
+
visibleFields.forEach((f) => {
|
|
97
|
+
init[f.key] = fvMap[f.key]?.value ?? f.default_value ?? null;
|
|
98
|
+
});
|
|
99
|
+
return init;
|
|
100
|
+
});
|
|
101
|
+
const initialLines = Array.isArray(proposal.mailbox_proposal_lines)
|
|
102
|
+
? proposal.mailbox_proposal_lines.map((l) => ({
|
|
103
|
+
id: l.id,
|
|
104
|
+
product_id: l.product_id,
|
|
105
|
+
raw_product_text: l.raw_product_text ?? "",
|
|
106
|
+
quantity: l.quantity,
|
|
107
|
+
unit: l.unit,
|
|
108
|
+
}))
|
|
109
|
+
: [];
|
|
110
|
+
const [lines, setLines] = useState(initialLines.length ? initialLines : [
|
|
111
|
+
{ product_id: null, raw_product_text: "", quantity: 1, unit: "stuks" },
|
|
112
|
+
]);
|
|
113
|
+
const customerVal = values["customer"];
|
|
114
|
+
const deliveryVal = values["delivery_date"];
|
|
115
|
+
const notesVal = values["notes"];
|
|
116
|
+
const setVal = (key, v) => setValues((prev) => ({ ...prev, [key]: v }));
|
|
117
|
+
const handleApprove = () => {
|
|
118
|
+
const missing = template.filter((f) => f.required && (values[f.key] === null || values[f.key] === undefined || values[f.key] === ""));
|
|
119
|
+
if (missing.length > 0) {
|
|
120
|
+
toast.error(`Verplichte velden ontbreken: ${missing.map((m) => m.label).join(", ")}`);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const cust = customers.find((c) => c.id === customerVal);
|
|
124
|
+
onApprove({
|
|
125
|
+
proposalId: proposal.id,
|
|
126
|
+
customerId: customerVal ?? null,
|
|
127
|
+
customerName: cust?.company_name ?? msg.from_name ?? msg.from_email ?? "Onbekend",
|
|
128
|
+
deliveryDate: deliveryVal || new Date(Date.now() + 86400000).toISOString().slice(0, 10),
|
|
129
|
+
notes: notesVal || null,
|
|
130
|
+
lines: lines.filter((l) => l.raw_product_text.trim() !== ""),
|
|
131
|
+
fieldValues: Object.entries(values).map(([field_key, value]) => ({ field_key, value })),
|
|
132
|
+
});
|
|
133
|
+
};
|
|
134
|
+
return (_jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs(Card, { children: [_jsxs(CardHeader, { 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, { children: _jsx("pre", { className: "text-sm whitespace-pre-wrap font-sans max-h-[60vh] overflow-y-auto", children: msg.body_text || "(geen tekst)" }) })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: "Bestelvoorstel" }) }), _jsxs(CardContent, { className: "space-y-4", children: [visibleFields.map((field) => (_jsx(DynamicField, { field: field, value: values[field.key], meta: fvMap[field.key], onChange: (v) => setVal(field.key, v), customers: customers, products: products, plantTypes: plantTypes, suppliers: suppliers }, field.key))), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Bestelregels" }), lines.map((line, idx) => (_jsxs("div", { className: "grid grid-cols-12 gap-2 items-start", children: [_jsxs("div", { className: "col-span-6", children: [_jsx(SearchableSelect, { value: line.product_id, placeholder: "Kies product...", clearLabel: "\u2014 Handmatig \u2014", options: products.map((p) => ({ value: p.id, label: p.product, hint: p.unit })), onChange: (v) => {
|
|
135
|
+
const next = [...lines];
|
|
136
|
+
if (v === null)
|
|
137
|
+
next[idx] = { ...next[idx], product_id: null };
|
|
138
|
+
else {
|
|
139
|
+
const p = products.find((x) => x.id === v);
|
|
140
|
+
next[idx] = {
|
|
141
|
+
...next[idx], product_id: v,
|
|
142
|
+
raw_product_text: p?.product ?? next[idx].raw_product_text,
|
|
143
|
+
unit: next[idx].unit ?? p?.unit ?? "stuks",
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
setLines(next);
|
|
147
|
+
} }), _jsx(Input, { className: "mt-1", value: line.raw_product_text, onChange: (e) => {
|
|
148
|
+
const next = [...lines];
|
|
149
|
+
next[idx] = { ...next[idx], raw_product_text: e.target.value };
|
|
150
|
+
setLines(next);
|
|
151
|
+
}, placeholder: "Productomschrijving" })] }), _jsx(Input, { className: "col-span-2", type: "number", value: line.quantity ?? "", onChange: (e) => {
|
|
152
|
+
const next = [...lines];
|
|
153
|
+
next[idx] = { ...next[idx], quantity: e.target.value ? Number(e.target.value) : null };
|
|
154
|
+
setLines(next);
|
|
155
|
+
}, placeholder: "Aantal" }), _jsx(Input, { className: "col-span-3", value: line.unit ?? "", onChange: (e) => {
|
|
156
|
+
const next = [...lines];
|
|
157
|
+
next[idx] = { ...next[idx], unit: e.target.value };
|
|
158
|
+
setLines(next);
|
|
159
|
+
}, placeholder: "Eenheid" }), _jsx(Button, { variant: "ghost", size: "icon", className: "col-span-1", onClick: () => setLines(lines.filter((_, i) => i !== idx)), children: _jsx(Trash2, { className: "h-4 w-4" }) })] }, idx))), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setLines([...lines, { product_id: null, raw_product_text: "", quantity: 1, unit: "stuks" }]), children: [_jsx(Plus, { className: "h-3 w-3 mr-1" }), " Regel toevoegen"] })] }), _jsxs("div", { className: "flex gap-2 pt-2", children: [_jsxs(Button, { className: "flex-1", onClick: handleApprove, children: [_jsx(Check, { className: "h-4 w-4 mr-1" }), " Goedkeuren"] }), _jsxs(Button, { variant: "destructive", onClick: () => onReject(notesVal || ""), children: [_jsx(X, { className: "h-4 w-4 mr-1" }), " Afwijzen"] })] })] })] })] }));
|
|
160
|
+
}
|
|
161
|
+
function DynamicField({ field, value, meta, onChange, customers, products, plantTypes, suppliers, }) {
|
|
162
|
+
const reviewClass = meta?.needs_review ? "border-yellow-500 ring-1 ring-yellow-500/40 rounded-md p-2" : "";
|
|
163
|
+
const label = (_jsxs(Label, { className: "flex items-center gap-2", children: [field.label, field.required && _jsx("span", { className: "text-destructive", children: "*" }), meta?.needs_review && (_jsx(Badge, { variant: "outline", className: "text-xs border-yellow-500 text-yellow-700", children: "Controleren" })), typeof meta?.ai_confidence === "number" && (_jsxs("span", { className: "text-xs text-muted-foreground", children: [Math.round(meta.ai_confidence * 100), "%"] }))] }));
|
|
164
|
+
const renderControl = () => {
|
|
165
|
+
switch (field.field_type) {
|
|
166
|
+
case "text":
|
|
167
|
+
return _jsx(Input, { value: value ?? "", onChange: (e) => onChange(e.target.value) });
|
|
168
|
+
case "number":
|
|
169
|
+
return _jsx(Input, { type: "number", value: value ?? "", onChange: (e) => onChange(e.target.value ? Number(e.target.value) : null) });
|
|
170
|
+
case "date":
|
|
171
|
+
return _jsx(Input, { type: "date", value: value ?? "", onChange: (e) => onChange(e.target.value || null) });
|
|
172
|
+
case "boolean":
|
|
173
|
+
return _jsx("div", { children: _jsx(Switch, { checked: !!value, onCheckedChange: onChange }) });
|
|
174
|
+
case "note":
|
|
175
|
+
return _jsx(Textarea, { rows: 2, value: value ?? "", onChange: (e) => onChange(e.target.value) });
|
|
176
|
+
case "select":
|
|
177
|
+
return (_jsxs(Select, { value: value ?? "__none", onValueChange: (v) => onChange(v === "__none" ? null : v), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Kies..." }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "__none", children: "\u2014" }), (field.options ?? []).map((o) => _jsx(SelectItem, { value: o.value, children: o.label }, o.value))] })] }));
|
|
178
|
+
case "multiselect": {
|
|
179
|
+
const arr = Array.isArray(value) ? value : [];
|
|
180
|
+
return (_jsx("div", { className: "flex flex-wrap gap-1", children: (field.options ?? []).map((o) => {
|
|
181
|
+
const on = arr.includes(o.value);
|
|
182
|
+
return (_jsx(Badge, { variant: on ? "default" : "outline", className: "cursor-pointer", onClick: () => onChange(on ? arr.filter((x) => x !== o.value) : [...arr, o.value]), children: o.label }, o.value));
|
|
183
|
+
}) }));
|
|
184
|
+
}
|
|
185
|
+
case "customer":
|
|
186
|
+
return (_jsx(SearchableSelect, { value: value ?? null, placeholder: "Kies klant...", options: customers.map((c) => ({ value: c.id, label: c.company_name, hint: c.email })), onChange: onChange }));
|
|
187
|
+
case "product":
|
|
188
|
+
return (_jsx(SearchableSelect, { value: value ?? null, placeholder: "Kies product...", options: products.map((p) => ({ value: p.id, label: p.product })), onChange: onChange }));
|
|
189
|
+
case "plant_type":
|
|
190
|
+
return (_jsx(SearchableSelect, { value: value ?? null, placeholder: "Kies plantsoort...", options: plantTypes.map((p) => ({ value: p.id, label: p.name })), onChange: onChange }));
|
|
191
|
+
case "supplier":
|
|
192
|
+
return (_jsx(SearchableSelect, { value: value ?? null, placeholder: "Kies leverancier...", options: suppliers.map((s) => ({ value: s.id, label: s.name })), onChange: onChange }));
|
|
193
|
+
default:
|
|
194
|
+
return _jsx(Input, { value: value ?? "", onChange: (e) => onChange(e.target.value) });
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
return (_jsxs("div", { className: "space-y-1", children: [label, _jsx("div", { className: reviewClass, children: renderControl() })] }));
|
|
198
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface SearchableOption {
|
|
2
|
+
value: string;
|
|
3
|
+
label: string;
|
|
4
|
+
hint?: string | null;
|
|
5
|
+
}
|
|
6
|
+
interface SearchableSelectProps {
|
|
7
|
+
value: string | null;
|
|
8
|
+
options: SearchableOption[];
|
|
9
|
+
placeholder?: string;
|
|
10
|
+
emptyLabel?: string;
|
|
11
|
+
allowClear?: boolean;
|
|
12
|
+
clearLabel?: string;
|
|
13
|
+
onChange: (value: string | null) => void;
|
|
14
|
+
className?: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function SearchableSelect({ value, options, placeholder, emptyLabel, allowClear, clearLabel, onChange, className, }: SearchableSelectProps): import("react").JSX.Element;
|
|
17
|
+
export {};
|
|
18
|
+
//# sourceMappingURL=SearchableSelect.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SearchableSelect.d.ts","sourceRoot":"","sources":["../../src/components/SearchableSelect.tsx"],"names":[],"mappings":"AAMA,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,UAAU,qBAAqB;IAC7B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,EAAE,gBAAgB,EAAE,CAAC;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IACzC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,KAAK,EAAE,OAAO,EAAE,WAAuB,EAAE,UAA6B,EACtE,UAAiB,EAAE,UAAiC,EACpD,QAAQ,EAAE,SAAS,GACpB,EAAE,qBAAqB,+BAsFvB"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo, useState } from "react";
|
|
3
|
+
import { Button, Input, Popover, PopoverContent, PopoverTrigger, } from "@flowselections/core";
|
|
4
|
+
import { Check, ChevronsUpDown, Search } from "lucide-react";
|
|
5
|
+
export function SearchableSelect({ value, options, placeholder = "Kies...", emptyLabel = "Geen resultaat", allowClear = true, clearLabel = "— Geen koppeling —", onChange, className, }) {
|
|
6
|
+
const [open, setOpen] = useState(false);
|
|
7
|
+
const [q, setQ] = useState("");
|
|
8
|
+
const selected = useMemo(() => options.find((o) => o.value === value) ?? null, [options, value]);
|
|
9
|
+
const filtered = useMemo(() => {
|
|
10
|
+
const needle = q.trim().toLowerCase();
|
|
11
|
+
if (!needle)
|
|
12
|
+
return options.slice(0, 200);
|
|
13
|
+
return options
|
|
14
|
+
.filter((o) => o.label.toLowerCase().includes(needle) ||
|
|
15
|
+
(o.hint ?? "").toLowerCase().includes(needle))
|
|
16
|
+
.slice(0, 200);
|
|
17
|
+
}, [options, q]);
|
|
18
|
+
return (_jsxs(Popover, { open: open, onOpenChange: (o) => { setOpen(o); if (!o)
|
|
19
|
+
setQ(""); }, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "outline", role: "combobox", "aria-expanded": open, className: `w-full justify-between font-normal ${className ?? ""}`, children: [_jsx("span", { className: `truncate ${selected ? "" : "text-muted-foreground"}`, children: selected?.label ?? placeholder }), _jsx(ChevronsUpDown, { className: "ml-2 h-4 w-4 shrink-0 opacity-50" })] }) }), _jsxs(PopoverContent, { className: "w-[--radix-popover-trigger-width] p-0", align: "start", children: [_jsx("div", { className: "p-2 border-b", children: _jsxs("div", { className: "relative", children: [_jsx(Search, { className: "absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" }), _jsx(Input, { autoFocus: true, value: q, onChange: (e) => setQ(e.target.value), placeholder: `Zoek in ${options.length} items...`, className: "pl-8 h-9" })] }) }), _jsxs("div", { className: "max-h-64 overflow-y-auto py-1", children: [allowClear && (_jsxs("button", { type: "button", className: "w-full text-left text-sm px-3 py-2 hover:bg-accent flex items-center gap-2", onClick: () => { onChange(null); setOpen(false); }, children: [value === null && _jsx(Check, { className: "h-3 w-3" }), _jsx("span", { className: "text-muted-foreground italic", children: clearLabel })] })), filtered.length === 0 ? (_jsx("div", { className: "px-3 py-6 text-sm text-muted-foreground text-center", children: emptyLabel })) : (filtered.map((o) => (_jsxs("button", { type: "button", className: "w-full text-left text-sm px-3 py-2 hover:bg-accent flex items-start gap-2", onClick: () => { onChange(o.value); setOpen(false); }, children: [_jsx(Check, { className: `h-3 w-3 mt-1 shrink-0 ${value === o.value ? "" : "invisible"}` }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("div", { className: "truncate", children: o.label }), o.hint && _jsx("div", { className: "text-xs text-muted-foreground truncate", children: o.hint })] })] }, o.value)))), filtered.length === 200 && (_jsx("div", { className: "px-3 py-2 text-xs text-muted-foreground text-center border-t", children: "Toon eerste 200 resultaten \u2014 verfijn je zoekopdracht" }))] })] })] }));
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ImapAccountsCard.d.ts","sourceRoot":"","sources":["../../../src/components/settings/ImapAccountsCard.tsx"],"names":[],"mappings":"AAyCA,wBAAgB,gBAAgB,gCAwP/B"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle, Button, Input, Label, Switch, Badge, Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, toast, } from "@flowselections/core";
|
|
4
|
+
import { Server, Plus, Pencil, Trash2, TestTube2, RefreshCw, CheckCircle2, PlayCircle } from "lucide-react";
|
|
5
|
+
import { listImapAccounts, upsertImapAccount, deleteImapAccount, testImapAccount, listImapProfiles, activateImapAccount, pollImapProfile, } from "../../lib/imap.functions";
|
|
6
|
+
const emptyAccount = {
|
|
7
|
+
id: undefined,
|
|
8
|
+
name: "",
|
|
9
|
+
email: "",
|
|
10
|
+
host: "",
|
|
11
|
+
port: 993,
|
|
12
|
+
username: "",
|
|
13
|
+
use_tls: true,
|
|
14
|
+
password: "",
|
|
15
|
+
is_active: true,
|
|
16
|
+
};
|
|
17
|
+
export function ImapAccountsCard() {
|
|
18
|
+
const [items, setItems] = useState([]);
|
|
19
|
+
const [activeAccountId, setActiveAccountId] = useState(null);
|
|
20
|
+
const [activeProfileId, setActiveProfileId] = useState(null);
|
|
21
|
+
const [loading, setLoading] = useState(true);
|
|
22
|
+
const [editing, setEditing] = useState(null);
|
|
23
|
+
const [saving, setSaving] = useState(false);
|
|
24
|
+
const [testingId, setTestingId] = useState(null);
|
|
25
|
+
const [pollingId, setPollingId] = useState(null);
|
|
26
|
+
const reload = async () => {
|
|
27
|
+
setLoading(true);
|
|
28
|
+
try {
|
|
29
|
+
const [accRes, profRes] = await Promise.all([listImapAccounts(), listImapProfiles()]);
|
|
30
|
+
setItems(Array.isArray(accRes?.items) ? accRes.items : []);
|
|
31
|
+
const profs = Array.isArray(profRes?.items) ? profRes.items : [];
|
|
32
|
+
const orders = profs.find((p) => p.profile_key === "mailbox_orders") ?? profs[0] ?? null;
|
|
33
|
+
setActiveAccountId(orders?.account_id ?? null);
|
|
34
|
+
setActiveProfileId(orders?.id ?? null);
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
setLoading(false);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
useEffect(() => { reload(); }, []);
|
|
41
|
+
const handleSave = async () => {
|
|
42
|
+
if (!editing)
|
|
43
|
+
return;
|
|
44
|
+
setSaving(true);
|
|
45
|
+
try {
|
|
46
|
+
const saved = await upsertImapAccount({
|
|
47
|
+
data: {
|
|
48
|
+
id: editing.id,
|
|
49
|
+
name: editing.name,
|
|
50
|
+
email: editing.email || null,
|
|
51
|
+
host: editing.host,
|
|
52
|
+
port: Number(editing.port),
|
|
53
|
+
username: editing.username,
|
|
54
|
+
use_tls: editing.use_tls,
|
|
55
|
+
password: editing.password || undefined,
|
|
56
|
+
is_active: editing.is_active,
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
toast.success("Account opgeslagen");
|
|
60
|
+
// Nieuw account direct als actief markeren als er nog geen is
|
|
61
|
+
if (!editing.id && saved?.id && !activeAccountId) {
|
|
62
|
+
await activateImapAccount({ data: { account_id: saved.id } });
|
|
63
|
+
}
|
|
64
|
+
setEditing(null);
|
|
65
|
+
reload();
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
toast.error(e?.message ?? "Opslaan mislukt");
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
setSaving(false);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
const handleDelete = async (id) => {
|
|
75
|
+
if (!confirm("Account verwijderen?"))
|
|
76
|
+
return;
|
|
77
|
+
try {
|
|
78
|
+
await deleteImapAccount({ data: { id } });
|
|
79
|
+
toast.success("Verwijderd");
|
|
80
|
+
reload();
|
|
81
|
+
}
|
|
82
|
+
catch (e) {
|
|
83
|
+
toast.error(e?.message ?? "Verwijderen mislukt");
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
const handleTest = async (id) => {
|
|
87
|
+
setTestingId(id);
|
|
88
|
+
try {
|
|
89
|
+
const res = await testImapAccount({ data: { id } });
|
|
90
|
+
if (res?.ok)
|
|
91
|
+
toast.success("Verbinding OK");
|
|
92
|
+
else
|
|
93
|
+
toast.error(res?.error ?? "Verbinding mislukt");
|
|
94
|
+
reload();
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
setTestingId(null);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
const handleActivate = async (id) => {
|
|
101
|
+
try {
|
|
102
|
+
await activateImapAccount({ data: { account_id: id } });
|
|
103
|
+
toast.success("Account geactiveerd");
|
|
104
|
+
reload();
|
|
105
|
+
}
|
|
106
|
+
catch (e) {
|
|
107
|
+
toast.error(e?.message ?? "Activeren mislukt");
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
const handlePoll = async () => {
|
|
111
|
+
if (!activeProfileId)
|
|
112
|
+
return;
|
|
113
|
+
setPollingId(activeProfileId);
|
|
114
|
+
try {
|
|
115
|
+
const res = await pollImapProfile({ data: { profile_id: activeProfileId } });
|
|
116
|
+
if (res?.ok)
|
|
117
|
+
toast.success(`${res.inserted ?? 0} nieuwe e-mail(s) opgehaald`);
|
|
118
|
+
else
|
|
119
|
+
toast.error(res?.error ?? "Ophalen mislukt");
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
setPollingId(null);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
return (_jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between", children: [_jsxs("div", { children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Server, { className: "h-5 w-5 text-primary" }), _jsx(CardTitle, { children: "IMAP Account" })] }), _jsx(CardDescription, { children: "Beheer je e-mailaccounts en kies eenvoudig welke nu actief is voor de mailbox-bestellingen." })] }), _jsxs("div", { className: "flex gap-2", children: [_jsxs(Button, { variant: "outline", size: "sm", onClick: handlePoll, disabled: !activeProfileId || !!pollingId, children: [_jsx(PlayCircle, { className: `h-4 w-4 mr-2 ${pollingId ? "animate-pulse" : ""}` }), "Nu ophalen"] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: reload, disabled: loading, children: [_jsx(RefreshCw, { className: `h-4 w-4 mr-2 ${loading ? "animate-spin" : ""}` }), "Vernieuwen"] }), _jsxs(Button, { size: "sm", onClick: () => setEditing({ ...emptyAccount }), children: [_jsx(Plus, { className: "h-4 w-4 mr-2" }), "Account toevoegen"] })] })] }), _jsx(CardContent, { children: loading ? (_jsx("div", { className: "text-sm text-muted-foreground", children: "Laden..." })) : items.length === 0 ? (_jsx("div", { className: "text-sm text-muted-foreground", children: "Nog geen accounts. Klik op \"Account toevoegen\"." })) : (_jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { className: "w-[80px]", children: "Actief" }), _jsx(TableHead, { children: "Naam" }), _jsx(TableHead, { children: "E-mail" }), _jsx(TableHead, { children: "Server" }), _jsx(TableHead, { children: "Laatste test" }), _jsx(TableHead, { className: "text-right", children: "Acties" })] }) }), _jsx(TableBody, { children: items.map((a) => {
|
|
126
|
+
const isActive = a.id === activeAccountId;
|
|
127
|
+
return (_jsxs(TableRow, { className: isActive ? "bg-primary/5" : "", children: [_jsx(TableCell, { children: isActive ? (_jsxs(Badge, { variant: "default", className: "gap-1", children: [_jsx(CheckCircle2, { className: "h-3 w-3" }), "Actief"] })) : (_jsx(Button, { variant: "outline", size: "sm", onClick: () => handleActivate(a.id), children: "Activeer" })) }), _jsx(TableCell, { className: "font-medium", children: a.name }), _jsx(TableCell, { className: "text-muted-foreground", children: a.email ?? a.username }), _jsxs(TableCell, { className: "text-muted-foreground", children: [a.host, ":", a.port, a.use_tls ? " · TLS" : ""] }), _jsx(TableCell, { children: a.last_tested_at ? (a.last_test_ok
|
|
128
|
+
? _jsx(Badge, { variant: "default", children: "OK" })
|
|
129
|
+
: _jsx(Badge, { variant: "destructive", title: a.last_test_error ?? "", children: "Fout" })) : _jsx("span", { className: "text-xs text-muted-foreground", children: "\u2014" }) }), _jsx(TableCell, { className: "text-right", children: _jsxs("div", { className: "flex gap-1 justify-end", children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: () => handleTest(a.id), disabled: testingId === a.id, title: "Test verbinding", children: _jsx(TestTube2, { className: "h-4 w-4" }) }), _jsx(Button, { variant: "ghost", size: "sm", onClick: () => setEditing({
|
|
130
|
+
id: a.id, name: a.name, email: a.email ?? "", host: a.host, port: a.port,
|
|
131
|
+
username: a.username, use_tls: a.use_tls,
|
|
132
|
+
password: "",
|
|
133
|
+
is_active: a.is_active,
|
|
134
|
+
}), title: "Bewerken", children: _jsx(Pencil, { className: "h-4 w-4" }) }), _jsx(Button, { variant: "ghost", size: "sm", onClick: () => handleDelete(a.id), title: "Verwijderen", children: _jsx(Trash2, { className: "h-4 w-4 text-destructive" }) })] }) })] }, a.id));
|
|
135
|
+
}) })] })) }), _jsx(Dialog, { open: !!editing, onOpenChange: (o) => !o && setEditing(null), children: _jsxs(DialogContent, { className: "max-w-lg", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: editing?.id ? "Account bewerken" : "Nieuw account" }) }), editing && (_jsxs("div", { className: "grid grid-cols-2 gap-3", children: [_jsxs("div", { className: "space-y-1 col-span-2", children: [_jsx(Label, { children: "Naam" }), _jsx(Input, { value: editing.name, onChange: (e) => setEditing({ ...editing, name: e.target.value }), placeholder: "bv. Orders Mailbox" })] }), _jsxs("div", { className: "space-y-1 col-span-2", children: [_jsx(Label, { children: "E-mailadres" }), _jsx(Input, { type: "email", value: editing.email, onChange: (e) => setEditing({ ...editing, email: e.target.value }), placeholder: "bestellingen@example.com" })] }), _jsxs("div", { className: "space-y-1 col-span-2", children: [_jsx(Label, { children: "IMAP-server" }), _jsx(Input, { value: editing.host, onChange: (e) => setEditing({ ...editing, host: e.target.value }), placeholder: "imap.example.com" })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { children: "Poort" }), _jsx(Input, { type: "number", value: editing.port, onChange: (e) => setEditing({ ...editing, port: Number(e.target.value) }) })] }), _jsxs("div", { className: "space-y-1 flex flex-col", children: [_jsx(Label, { children: "TLS / SSL" }), _jsx(Switch, { checked: editing.use_tls, onCheckedChange: (v) => setEditing({ ...editing, use_tls: v }) })] }), _jsxs("div", { className: "space-y-1 col-span-2", children: [_jsx(Label, { children: "Gebruikersnaam" }), _jsx(Input, { value: editing.username, onChange: (e) => setEditing({ ...editing, username: e.target.value }) })] }), _jsxs("div", { className: "space-y-1 col-span-2", children: [_jsx(Label, { children: "Wachtwoord" }), _jsx(Input, { type: "password", value: editing.password, onChange: (e) => setEditing({ ...editing, password: e.target.value }), placeholder: editing.id ? "Laat leeg om huidig wachtwoord te behouden" : "Voer wachtwoord in", autoComplete: "new-password" }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Wachtwoord wordt versleuteld opgeslagen en alleen gebruikt voor de IMAP-verbinding." })] }), _jsxs("div", { className: "col-span-2 flex items-center justify-between", children: [_jsx(Label, { children: "Actief" }), _jsx(Switch, { checked: editing.is_active, onCheckedChange: (v) => setEditing({ ...editing, is_active: v }) })] })] })), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => setEditing(null), children: "Annuleren" }), _jsx(Button, { onClick: handleSave, disabled: saving, children: saving ? "Opslaan..." : "Opslaan" })] })] }) })] }));
|
|
136
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ImapProfilesCard.d.ts","sourceRoot":"","sources":["../../../src/components/settings/ImapProfilesCard.tsx"],"names":[],"mappings":"AAgCA,wBAAgB,gBAAgB,gCAgN/B"}
|