@flowselections/mailbox-orders 1.0.62 → 1.0.65
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/EmailPreview.d.ts +5 -1
- package/dist-lib/components/EmailPreview.d.ts.map +1 -1
- package/dist-lib/components/EmailPreview.js +32 -4
- package/dist-lib/components/MailboxProposalsPage.js +1 -1
- package/dist-lib/lib/mailbox-ai.server.d.ts +19 -2
- package/dist-lib/lib/mailbox-ai.server.d.ts.map +1 -1
- package/dist-lib/lib/mailbox-ai.server.js +76 -12
- package/dist-lib/lib/mailbox-attachments.d.ts +22 -0
- package/dist-lib/lib/mailbox-attachments.d.ts.map +1 -0
- package/dist-lib/lib/mailbox-attachments.js +58 -0
- package/dist-lib/lib/mailbox-attachments.server.d.ts +13 -0
- package/dist-lib/lib/mailbox-attachments.server.d.ts.map +1 -0
- package/dist-lib/lib/mailbox-attachments.server.js +45 -0
- package/dist-lib/lib/mailbox-body.d.ts +4 -1
- package/dist-lib/lib/mailbox-body.d.ts.map +1 -1
- package/dist-lib/lib/mailbox-body.js +20 -7
- package/dist-lib/lib/mailbox-ingest.server.d.ts.map +1 -1
- package/dist-lib/lib/mailbox-ingest.server.js +15 -6
- package/dist-lib/lib/mailbox.functions.d.ts +18 -0
- package/dist-lib/lib/mailbox.functions.d.ts.map +1 -1
- package/dist-lib/lib/mailbox.functions.js +56 -2
- package/package.json +1 -1
|
@@ -8,8 +8,12 @@ export type PreviewAttachment = {
|
|
|
8
8
|
size?: number | null;
|
|
9
9
|
contentId?: string | null;
|
|
10
10
|
url?: string | null;
|
|
11
|
+
isInline?: boolean;
|
|
12
|
+
downloadable?: boolean;
|
|
13
|
+
skippedReason?: string | null;
|
|
11
14
|
};
|
|
12
15
|
type Props = {
|
|
16
|
+
messageId?: string | null;
|
|
13
17
|
subject?: string | null;
|
|
14
18
|
fromName?: string | null;
|
|
15
19
|
fromEmail?: string | null;
|
|
@@ -21,6 +25,6 @@ type Props = {
|
|
|
21
25
|
attachments?: PreviewAttachment[];
|
|
22
26
|
hasAttachments?: boolean;
|
|
23
27
|
};
|
|
24
|
-
export declare function EmailPreview({ subject, fromName, fromEmail, receivedAt, to, cc, bodyHtml, bodyText, attachments, hasAttachments, }: Props): import("react").JSX.Element;
|
|
28
|
+
export declare function EmailPreview({ messageId, subject, fromName, fromEmail, receivedAt, to, cc, bodyHtml, bodyText, attachments, hasAttachments, }: Props): import("react").JSX.Element;
|
|
25
29
|
export {};
|
|
26
30
|
//# sourceMappingURL=EmailPreview.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EmailPreview.d.ts","sourceRoot":"","sources":["../../src/components/EmailPreview.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"EmailPreview.d.ts","sourceRoot":"","sources":["../../src/components/EmailPreview.tsx"],"names":[],"mappings":"AASA,MAAM,MAAM,cAAc,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC;AAC7E,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B,CAAC;AAEF,KAAK,KAAK,GAAG;IACX,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,EAAE,CAAC,EAAE,cAAc,EAAE,CAAC;IACtB,EAAE,CAAC,EAAE,cAAc,EAAE,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,WAAW,CAAC,EAAE,iBAAiB,EAAE,CAAC;IAClC,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B,CAAC;AAsEF,wBAAgB,YAAY,CAAC,EAC3B,SAAS,EACT,OAAO,EACP,QAAQ,EACR,SAAS,EACT,UAAU,EACV,EAAO,EACP,EAAO,EACP,QAAQ,EACR,QAAQ,EACR,WAAgB,EAChB,cAAc,GACf,EAAE,KAAK,+BAkJP"}
|
|
@@ -3,6 +3,10 @@ import { fmtDayTime } from "../lib/format-date.js";
|
|
|
3
3
|
import { useEffect, useMemo, useState } from "react";
|
|
4
4
|
import { Button, Badge } from "@flowselections/core";
|
|
5
5
|
import { Paperclip, Download, Code, FileText, ShieldOff, Shield } from "lucide-react";
|
|
6
|
+
import { useServerFn } from "@tanstack/react-start";
|
|
7
|
+
import { toast } from "sonner";
|
|
8
|
+
import { getMailboxAttachmentUrl } from "../lib/mailbox.functions.js";
|
|
9
|
+
import { skippedLabel } from "../lib/mailbox-attachments.js";
|
|
6
10
|
function addrLabel(a) {
|
|
7
11
|
if (a.name && a.email && a.name !== a.email)
|
|
8
12
|
return `${a.name} <${a.email}>`;
|
|
@@ -43,7 +47,7 @@ function EmailFrame({ html }) {
|
|
|
43
47
|
// en niet bij de applicatie. Popups blijven werken voor links.
|
|
44
48
|
sandbox: "allow-popups allow-popups-to-escape-sandbox", referrerPolicy: "no-referrer", className: "h-full w-full rounded border bg-white" }));
|
|
45
49
|
}
|
|
46
|
-
export function EmailPreview({ subject, fromName, fromEmail, receivedAt, to = [], cc = [], bodyHtml, bodyText, attachments = [], hasAttachments, }) {
|
|
50
|
+
export function EmailPreview({ messageId, subject, fromName, fromEmail, receivedAt, to = [], cc = [], bodyHtml, bodyText, attachments = [], hasAttachments, }) {
|
|
47
51
|
const html = (bodyHtml ?? "").trim();
|
|
48
52
|
const [mode, setMode] = useState(html ? "html" : "text");
|
|
49
53
|
const [blockRemoteContent, setBlockRemoteContent] = useState(false);
|
|
@@ -51,7 +55,31 @@ export function EmailPreview({ subject, fromName, fromEmail, receivedAt, to = []
|
|
|
51
55
|
setMode(html ? "html" : "text");
|
|
52
56
|
}, [html]);
|
|
53
57
|
const rendered = blockRemoteContent ? blockRemote(html) : html;
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
const allAttachments = Array.isArray(attachments) ? attachments : [];
|
|
59
|
+
// Ingesloten afbeeldingen staan in de mail zelf, niet als losse bijlage.
|
|
60
|
+
const safeAttachments = allAttachments
|
|
61
|
+
.map((a, index) => ({ a, index }))
|
|
62
|
+
.filter(({ a }) => !(a.isInline ?? !!a.contentId));
|
|
63
|
+
const fetchUrl = useServerFn(getMailboxAttachmentUrl);
|
|
64
|
+
const [busy, setBusy] = useState(null);
|
|
65
|
+
async function download(index) {
|
|
66
|
+
if (!messageId)
|
|
67
|
+
return;
|
|
68
|
+
setBusy(index);
|
|
69
|
+
try {
|
|
70
|
+
const res = await fetchUrl({ data: { messageId, index } });
|
|
71
|
+
if (res?.url)
|
|
72
|
+
window.open(res.url, "_blank", "noopener,noreferrer");
|
|
73
|
+
else
|
|
74
|
+
toast("Deze bijlage is niet meer beschikbaar.");
|
|
75
|
+
}
|
|
76
|
+
catch (e) {
|
|
77
|
+
console.error("[mailbox] downloadlink ophalen mislukt", e);
|
|
78
|
+
toast.error("Downloaden lukte niet. Probeer het opnieuw of neem contact op met de beheerder.");
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
setBusy(null);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return (_jsxs("div", { className: "flex h-full min-h-0 flex-col", children: [_jsxs("div", { className: "shrink-0 space-y-1 border-b pb-3", children: [_jsx("div", { className: "text-base font-semibold leading-snug", children: subject || "(geen onderwerp)" }), _jsxs("div", { className: "text-xs text-muted-foreground", children: [_jsx("span", { className: "font-medium text-foreground", children: fromName || fromEmail }), fromName && fromEmail ? ` <${fromEmail}>` : "", receivedAt ? ` · ${fmtDayTime(receivedAt)}` : ""] }), _jsx(AddressLine, { label: "Aan", list: to }), _jsx(AddressLine, { label: "CC", list: cc }), (safeAttachments.length > 0 || (hasAttachments && allAttachments.length === 0)) && (_jsx("div", { className: "flex flex-wrap items-center gap-2 pt-2", children: safeAttachments.length === 0 ? (_jsxs(Badge, { variant: "outline", className: "gap-1 font-normal", children: [_jsx(Paperclip, { className: "h-3 w-3" }), " Deze mail bevat bijlagen (nog niet opgehaald)"] })) : (safeAttachments.map(({ a, index: i }) => (_jsxs("span", { className: "inline-flex items-center gap-1 rounded border bg-muted/40 px-2 py-1 text-xs", children: [_jsx(Paperclip, { className: "h-3 w-3 shrink-0" }), _jsx("span", { className: "max-w-[16rem] truncate", children: a.name }), formatSize(a.size) && (_jsx("span", { className: "text-muted-foreground", children: formatSize(a.size) })), skippedLabel(a.skippedReason) && (_jsxs("span", { className: "text-muted-foreground", children: ["\u00B7 ", skippedLabel(a.skippedReason)] })), a.downloadable && messageId ? (_jsx("button", { type: "button", className: "text-primary disabled:opacity-50", disabled: busy === i, onClick: () => download(i), "aria-label": `Download ${a.name}`, children: _jsx(Download, { className: "h-3 w-3" }) })) : a.url && !a.skippedReason && (_jsx("a", { href: a.url, target: "_blank", rel: "noopener noreferrer", className: "text-primary", "aria-label": `Download ${a.name}`, children: _jsx(Download, { className: "h-3 w-3" }) }))] }, `${a.name}-${i}`)))) })), _jsxs("div", { className: "flex flex-wrap items-center gap-2 pt-2", children: [_jsx(Button, { type: "button", size: "sm", variant: "outline", onClick: () => setMode(mode === "html" ? "text" : "html"), disabled: !html, children: mode === "html" ? (_jsxs(_Fragment, { children: [_jsx(FileText, { className: "mr-1 h-3.5 w-3.5" }), " Platte tekst"] })) : (_jsxs(_Fragment, { children: [_jsx(Code, { className: "mr-1 h-3.5 w-3.5" }), " Opgemaakte mail"] })) }), _jsx(Button, { type: "button", size: "sm", variant: "ghost", disabled: mode !== "html" || !html, onClick: () => setBlockRemoteContent((v) => !v), children: blockRemoteContent ? (_jsxs(_Fragment, { children: [_jsx(Shield, { className: "mr-1 h-3.5 w-3.5" }), " Externe inhoud tonen"] })) : (_jsxs(_Fragment, { children: [_jsx(ShieldOff, { className: "mr-1 h-3.5 w-3.5" }), " Externe inhoud blokkeren"] })) })] })] }), _jsx("div", { className: "min-h-0 flex-1 overflow-hidden pt-3", children: mode === "html" && html ? (_jsx(EmailFrame, { html: rendered })) : (_jsx("div", { className: "h-full w-full overflow-y-auto overscroll-contain rounded border bg-white p-3", children: _jsx("pre", { className: "whitespace-pre-wrap break-words font-sans text-sm leading-relaxed text-[#111]", children: bodyText || "(geen tekst)" }) })) })] }));
|
|
57
85
|
}
|
|
@@ -890,7 +890,7 @@ function ProposalEditor({ proposal, template, customers, products, allowBackorde
|
|
|
890
890
|
setSubmitting(false);
|
|
891
891
|
}
|
|
892
892
|
};
|
|
893
|
-
return (_jsxs("div", { className: "grid grid-cols-2 gap-4 h-full min-h-0", children: [_jsx(Card, { className: "flex flex-col min-h-0 overflow-hidden", children: _jsxs(CardContent, { className: "flex-1 min-h-0 p-4 flex flex-col gap-2", children: [msg.account_email && (_jsxs("div", { className: "shrink-0 text-xs text-muted-foreground", children: ["Ontvangen op: ", msg.account_email] })), proposal.parsed_payload?.reason === "geen_ai_agent" && (_jsxs("div", { className: "shrink-0 rounded-md border border-warning/30 bg-warning-soft px-3 py-2 text-xs text-warning-soft-foreground", children: ["Automatisch uitlezen staat uit. Vul de gegevens zelf in; een beheerder kan dit aanzetten bij Instellingen.", " ", _jsx("a", { href: AI_AUTH_PATH, className: "underline font-medium", children: "Naar Instellingen" })] })), _jsx(EmailPreview, { subject: msg.subject, fromName: msg.from_name, fromEmail: msg.from_email, receivedAt: msg.received_at, to: Array.isArray(msg.to_recipients) ? msg.to_recipients : [], cc: Array.isArray(msg.cc_recipients) ? msg.cc_recipients : [], bodyHtml: msg.body_html, bodyText: msg.body_text, attachments: Array.isArray(msg.attachments) ? msg.attachments : [], hasAttachments: msg.has_attachments === true })] }) }), _jsxs(Card, { className: "flex flex-col min-h-0 overflow-hidden", children: [_jsx(CardHeader, { className: "shrink-0", children: _jsx(CardTitle, { className: "text-base", children: "Bestelvoorstel" }) }), _jsxs(CardContent, { className: "space-y-4 flex-1 min-h-0 overflow-y-auto overscroll-contain", children: [(() => {
|
|
893
|
+
return (_jsxs("div", { className: "grid grid-cols-2 gap-4 h-full min-h-0", children: [_jsx(Card, { className: "flex flex-col min-h-0 overflow-hidden", children: _jsxs(CardContent, { className: "flex-1 min-h-0 p-4 flex flex-col gap-2", children: [msg.account_email && (_jsxs("div", { className: "shrink-0 text-xs text-muted-foreground", children: ["Ontvangen op: ", msg.account_email] })), proposal.parsed_payload?.reason === "geen_ai_agent" && (_jsxs("div", { className: "shrink-0 rounded-md border border-warning/30 bg-warning-soft px-3 py-2 text-xs text-warning-soft-foreground", children: ["Automatisch uitlezen staat uit. Vul de gegevens zelf in; een beheerder kan dit aanzetten bij Instellingen.", " ", _jsx("a", { href: AI_AUTH_PATH, className: "underline font-medium", children: "Naar Instellingen" })] })), _jsx(EmailPreview, { messageId: msg.id ?? proposal.message_id ?? null, subject: msg.subject, fromName: msg.from_name, fromEmail: msg.from_email, receivedAt: msg.received_at, to: Array.isArray(msg.to_recipients) ? msg.to_recipients : [], cc: Array.isArray(msg.cc_recipients) ? msg.cc_recipients : [], bodyHtml: msg.body_html, bodyText: msg.body_text, attachments: Array.isArray(msg.attachments) ? msg.attachments : [], hasAttachments: msg.has_attachments === true })] }) }), _jsxs(Card, { className: "flex flex-col min-h-0 overflow-hidden", children: [_jsx(CardHeader, { className: "shrink-0", children: _jsx(CardTitle, { className: "text-base", children: "Bestelvoorstel" }) }), _jsxs(CardContent, { className: "space-y-4 flex-1 min-h-0 overflow-y-auto overscroll-contain", children: [(() => {
|
|
894
894
|
// Klantveld staat altijd bovenaan, ook als het template (nog) leeg is.
|
|
895
895
|
const customerField = visibleFields.find((f) => f.key === "customer") ?? {
|
|
896
896
|
id: "customer",
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { SupabaseClient } from "@supabase/supabase-js";
|
|
2
2
|
import type { Database } from "../integrations/supabase/types.js";
|
|
3
3
|
import { DEFAULT_MAILBOX_SYSTEM_PROMPT } from "./mailbox-ai-prompt.js";
|
|
4
|
+
import type { AiFile } from "./mailbox-attachments.server.js";
|
|
5
|
+
export declare const ATTACHMENT_PROMPT_LINE = "Bestelregels kunnen ook in de bijgevoegde bestanden staan. Lees die net zo goed uit als de mailtekst.";
|
|
4
6
|
export { DEFAULT_MAILBOX_SYSTEM_PROMPT };
|
|
5
7
|
type AdminClient = SupabaseClient<Database>;
|
|
6
8
|
export declare const MAILBOX_AI_MODULE_KEY = "mailbox-bestellingen";
|
|
@@ -68,6 +70,9 @@ export interface AiParseMeta {
|
|
|
68
70
|
}
|
|
69
71
|
/** Huidige vingerafdruk van de gekoppelde AI-agent, of null als AI niet beschikbaar is. */
|
|
70
72
|
export declare function currentAiFingerprint(admin: AdminClient): Promise<string | null>;
|
|
73
|
+
export declare function geminiParts(user: string, files: AiFile[]): any[];
|
|
74
|
+
export declare function openAiContent(user: string, files: AiFile[]): any;
|
|
75
|
+
export declare function anthropicContent(user: string, files: AiFile[]): any;
|
|
71
76
|
/** Concrete ankerdatums zodat het model relatieve aanduidingen niet hoeft te rekenen. */
|
|
72
77
|
export declare function buildDateContext(now?: Date): {
|
|
73
78
|
today: string;
|
|
@@ -84,7 +89,19 @@ export declare class AiUnavailableError extends Error {
|
|
|
84
89
|
readonly attempts: number;
|
|
85
90
|
constructor(message: string, transient: boolean, attempts: number);
|
|
86
91
|
}
|
|
87
|
-
|
|
92
|
+
/**
|
|
93
|
+
* Leest de mail uit, met bijlagen als die er zijn. Weigert de AI de bijlagen
|
|
94
|
+
* met een blijvende fout, dan volgt één nieuwe poging zonder bijlagen en
|
|
95
|
+
* zonder bijlage-promptzin; `readAttachmentIndexes` is dan leeg.
|
|
96
|
+
*/
|
|
97
|
+
export declare function parseOrderEmail(admin: AdminClient, args: Parameters<typeof parseOrderEmailOnce>[1] & {
|
|
98
|
+
files?: AiFile[];
|
|
99
|
+
}): Promise<{
|
|
100
|
+
parsed: ParsedOrder;
|
|
101
|
+
meta: AiParseMeta;
|
|
102
|
+
readAttachmentIndexes: number[];
|
|
103
|
+
}>;
|
|
104
|
+
declare function parseOrderEmailOnce(admin: AdminClient, args: {
|
|
88
105
|
bodyText: string;
|
|
89
106
|
subject: string;
|
|
90
107
|
templateFields: TemplateFieldSpec[];
|
|
@@ -98,7 +115,7 @@ export declare function parseOrderEmail(admin: AdminClient, args: {
|
|
|
98
115
|
systemPrompt?: string | null;
|
|
99
116
|
now?: Date;
|
|
100
117
|
maxAttemptsPerModel?: number;
|
|
101
|
-
}): Promise<{
|
|
118
|
+
}, files?: AiFile[]): Promise<{
|
|
102
119
|
parsed: ParsedOrder;
|
|
103
120
|
meta: AiParseMeta;
|
|
104
121
|
}>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mailbox-ai.server.d.ts","sourceRoot":"","sources":["../../src/lib/mailbox-ai.server.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,+BAA+B,CAAC;AAE9D,OAAO,EAAE,6BAA6B,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"mailbox-ai.server.d.ts","sourceRoot":"","sources":["../../src/lib/mailbox-ai.server.ts"],"names":[],"mappings":"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"}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { resolveModuleAi, logAiCall } from "./ai-config.server.js";
|
|
5
5
|
import { DEFAULT_MAILBOX_SYSTEM_PROMPT } from "./mailbox-ai-prompt.js";
|
|
6
6
|
import { stripVersionSuffix } from "./ai-base-url.js";
|
|
7
|
+
export const ATTACHMENT_PROMPT_LINE = "Bestelregels kunnen ook in de bijgevoegde bestanden staan. Lees die net zo goed uit als de mailtekst.";
|
|
7
8
|
export { DEFAULT_MAILBOX_SYSTEM_PROMPT };
|
|
8
9
|
export const MAILBOX_AI_MODULE_KEY = "mailbox-bestellingen";
|
|
9
10
|
/** Huidige vingerafdruk van de gekoppelde AI-agent, of null als AI niet beschikbaar is. */
|
|
@@ -11,7 +12,7 @@ export async function currentAiFingerprint(admin) {
|
|
|
11
12
|
const res = await resolveModuleAi(admin, MAILBOX_AI_MODULE_KEY);
|
|
12
13
|
return res.ok ? res.config.fingerprint : null;
|
|
13
14
|
}
|
|
14
|
-
async function callGemini(cfg, system, user) {
|
|
15
|
+
async function callGemini(cfg, system, user, files = []) {
|
|
15
16
|
const base = stripVersionSuffix(cfg.baseUrl, ["v1beta", "v1"]);
|
|
16
17
|
const url = `${base}/v1beta/models/${encodeURIComponent(cfg.modelSlug)}:generateContent?key=${encodeURIComponent(cfg.apiKey)}`;
|
|
17
18
|
const res = await fetch(url, {
|
|
@@ -19,7 +20,7 @@ async function callGemini(cfg, system, user) {
|
|
|
19
20
|
headers: { "Content-Type": "application/json" },
|
|
20
21
|
body: JSON.stringify({
|
|
21
22
|
systemInstruction: { parts: [{ text: system }] },
|
|
22
|
-
contents: [{ role: "user", parts:
|
|
23
|
+
contents: [{ role: "user", parts: geminiParts(user, files) }],
|
|
23
24
|
generationConfig: { temperature: 0, responseMimeType: "application/json" },
|
|
24
25
|
}),
|
|
25
26
|
});
|
|
@@ -36,7 +37,7 @@ async function callGemini(cfg, system, user) {
|
|
|
36
37
|
outputTokens: json?.usageMetadata?.candidatesTokenCount ?? null,
|
|
37
38
|
};
|
|
38
39
|
}
|
|
39
|
-
async function callOpenAi(cfg, system, user) {
|
|
40
|
+
async function callOpenAi(cfg, system, user, files = []) {
|
|
40
41
|
const res = await fetch(`${cfg.baseUrl}/chat/completions`, {
|
|
41
42
|
method: "POST",
|
|
42
43
|
headers: {
|
|
@@ -47,7 +48,7 @@ async function callOpenAi(cfg, system, user) {
|
|
|
47
48
|
model: cfg.modelSlug,
|
|
48
49
|
messages: [
|
|
49
50
|
{ role: "system", content: system },
|
|
50
|
-
{ role: "user", content: user },
|
|
51
|
+
{ role: "user", content: openAiContent(user, files) },
|
|
51
52
|
],
|
|
52
53
|
response_format: { type: "json_object" },
|
|
53
54
|
}),
|
|
@@ -62,7 +63,7 @@ async function callOpenAi(cfg, system, user) {
|
|
|
62
63
|
outputTokens: json?.usage?.completion_tokens ?? null,
|
|
63
64
|
};
|
|
64
65
|
}
|
|
65
|
-
async function callAnthropic(cfg, system, user) {
|
|
66
|
+
async function callAnthropic(cfg, system, user, files = []) {
|
|
66
67
|
const base = stripVersionSuffix(cfg.baseUrl, ["v1"]);
|
|
67
68
|
const res = await fetch(`${base}/v1/messages`, {
|
|
68
69
|
method: "POST",
|
|
@@ -75,7 +76,7 @@ async function callAnthropic(cfg, system, user) {
|
|
|
75
76
|
model: cfg.modelSlug,
|
|
76
77
|
max_tokens: 4096,
|
|
77
78
|
system,
|
|
78
|
-
messages: [{ role: "user", content: user }],
|
|
79
|
+
messages: [{ role: "user", content: anthropicContent(user, files) }],
|
|
79
80
|
}),
|
|
80
81
|
});
|
|
81
82
|
const body = await res.text();
|
|
@@ -91,15 +92,43 @@ async function callAnthropic(cfg, system, user) {
|
|
|
91
92
|
outputTokens: json?.usage?.output_tokens ?? null,
|
|
92
93
|
};
|
|
93
94
|
}
|
|
94
|
-
|
|
95
|
+
// Zonder bijlagen blijft de inhoud exact gelijk aan vroeger (alleen tekst).
|
|
96
|
+
export function geminiParts(user, files) {
|
|
97
|
+
return [
|
|
98
|
+
{ text: user },
|
|
99
|
+
...files.map((f) => ({ inline_data: { mime_type: f.mimeType, data: f.data } })),
|
|
100
|
+
];
|
|
101
|
+
}
|
|
102
|
+
export function openAiContent(user, files) {
|
|
103
|
+
if (files.length === 0)
|
|
104
|
+
return user;
|
|
105
|
+
return [
|
|
106
|
+
{ type: "text", text: user },
|
|
107
|
+
...files.map((f, i) => f.mimeType === "application/pdf"
|
|
108
|
+
? { type: "file", file: { filename: `bijlage-${i + 1}.pdf`, file_data: `data:${f.mimeType};base64,${f.data}` } }
|
|
109
|
+
: { type: "image_url", image_url: { url: `data:${f.mimeType};base64,${f.data}` } }),
|
|
110
|
+
];
|
|
111
|
+
}
|
|
112
|
+
export function anthropicContent(user, files) {
|
|
113
|
+
if (files.length === 0)
|
|
114
|
+
return user;
|
|
115
|
+
return [
|
|
116
|
+
...files.map((f) => ({
|
|
117
|
+
type: f.mimeType === "application/pdf" ? "document" : "image",
|
|
118
|
+
source: { type: "base64", media_type: f.mimeType, data: f.data },
|
|
119
|
+
})),
|
|
120
|
+
{ type: "text", text: user },
|
|
121
|
+
];
|
|
122
|
+
}
|
|
123
|
+
async function callProvider(cfg, system, user, files = []) {
|
|
95
124
|
switch (cfg.providerSlug) {
|
|
96
125
|
case "gemini":
|
|
97
126
|
case "google":
|
|
98
|
-
return callGemini(cfg, system, user);
|
|
127
|
+
return callGemini(cfg, system, user, files);
|
|
99
128
|
case "openai":
|
|
100
|
-
return callOpenAi(cfg, system, user);
|
|
129
|
+
return callOpenAi(cfg, system, user, files);
|
|
101
130
|
case "anthropic":
|
|
102
|
-
return callAnthropic(cfg, system, user);
|
|
131
|
+
return callAnthropic(cfg, system, user, files);
|
|
103
132
|
default:
|
|
104
133
|
throw new Error(`Provider '${cfg.providerSlug}' wordt nog niet ondersteund.`);
|
|
105
134
|
}
|
|
@@ -291,20 +320,49 @@ export class AiUnavailableError extends Error {
|
|
|
291
320
|
this.attempts = attempts;
|
|
292
321
|
}
|
|
293
322
|
}
|
|
323
|
+
/**
|
|
324
|
+
* Leest de mail uit, met bijlagen als die er zijn. Weigert de AI de bijlagen
|
|
325
|
+
* met een blijvende fout, dan volgt één nieuwe poging zonder bijlagen en
|
|
326
|
+
* zonder bijlage-promptzin; `readAttachmentIndexes` is dan leeg.
|
|
327
|
+
*/
|
|
294
328
|
export async function parseOrderEmail(admin, args) {
|
|
329
|
+
const files = Array.isArray(args.files) ? args.files : [];
|
|
330
|
+
if (files.length === 0) {
|
|
331
|
+
const res = await parseOrderEmailOnce(admin, args);
|
|
332
|
+
return { ...res, readAttachmentIndexes: [] };
|
|
333
|
+
}
|
|
334
|
+
try {
|
|
335
|
+
const res = await parseOrderEmailOnce(admin, args, files);
|
|
336
|
+
return { ...res, readAttachmentIndexes: files.map((f) => f.index) };
|
|
337
|
+
}
|
|
338
|
+
catch (e) {
|
|
339
|
+
const transient = e?.name === "AiUnavailableError" ? Boolean(e.transient) : isTransientAiError(String(e?.message ?? e));
|
|
340
|
+
if (transient)
|
|
341
|
+
throw e;
|
|
342
|
+
console.error("[mailbox] AI weigerde bijlagen; opnieuw zonder bijlagen", {
|
|
343
|
+
types: files.map((f) => f.mimeType),
|
|
344
|
+
reason: String(e?.message ?? e).slice(0, 300),
|
|
345
|
+
});
|
|
346
|
+
const res = await parseOrderEmailOnce(admin, args);
|
|
347
|
+
return { ...res, readAttachmentIndexes: [] };
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
async function parseOrderEmailOnce(admin, args, files = []) {
|
|
295
351
|
const { resolveModuleAiAll } = await import("./ai-config.server.js");
|
|
296
352
|
const resolution = await resolveModuleAiAll(admin, MAILBOX_AI_MODULE_KEY);
|
|
297
353
|
if (!resolution.ok)
|
|
298
354
|
throw new AiUnavailableError(resolution.reason, false, 0);
|
|
299
355
|
const configs = resolution.configs;
|
|
300
356
|
const primary = configs[0];
|
|
301
|
-
const
|
|
357
|
+
const prompt = buildPrompt(args.bodyText, args.subject, args.templateFields, args.productCatalog, {
|
|
302
358
|
deliveryLocations: args.deliveryLocations,
|
|
303
359
|
plantTypes: args.plantTypes,
|
|
304
360
|
requiredFields: args.requiredFields,
|
|
305
361
|
systemPrompt: args.systemPrompt,
|
|
306
362
|
now: args.now,
|
|
307
363
|
});
|
|
364
|
+
const user = prompt.user;
|
|
365
|
+
const system = files.length > 0 ? `${prompt.system}\n\n${ATTACHMENT_PROMPT_LINE}` : prompt.system;
|
|
308
366
|
// Per model een begrensd aantal pogingen (instelling) met exponentiële
|
|
309
367
|
// backoff; daarna uitwijken naar het volgende model in de keten.
|
|
310
368
|
const MAX_ATTEMPTS_PER_MODEL = Math.max(1, Math.min(5, Number(args.maxAttemptsPerModel ?? 3)));
|
|
@@ -316,7 +374,7 @@ export async function parseOrderEmail(admin, args) {
|
|
|
316
374
|
attemptNo++;
|
|
317
375
|
const started = Date.now();
|
|
318
376
|
try {
|
|
319
|
-
const completion = await callProvider(cfg, system, user);
|
|
377
|
+
const completion = await callProvider(cfg, system, user, files);
|
|
320
378
|
const parsed = normalize(extractJson(completion.text), args.productCatalog);
|
|
321
379
|
const durationMs = Date.now() - started;
|
|
322
380
|
await logAiCall(admin, {
|
|
@@ -355,6 +413,12 @@ export async function parseOrderEmail(admin, args) {
|
|
|
355
413
|
errorCode: errorStatus(lastMessage) ? String(errorStatus(lastMessage)) : null,
|
|
356
414
|
attempt: attemptNo,
|
|
357
415
|
});
|
|
416
|
+
if (files.length > 0) {
|
|
417
|
+
console.error("[mailbox] AI-fout met bijlagen", {
|
|
418
|
+
provider: cfg.providerSlug,
|
|
419
|
+
types: files.map((f) => f.mimeType),
|
|
420
|
+
});
|
|
421
|
+
}
|
|
358
422
|
if (!lastTransient)
|
|
359
423
|
break; // permanente fout: direct volgend model
|
|
360
424
|
if (i < MAX_ATTEMPTS_PER_MODEL - 1)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { EmailAttachment } from "./mailbox-body.js";
|
|
2
|
+
export declare const ATTACHMENT_BUCKET = "email-auth-attachments";
|
|
3
|
+
export declare const AI_MAX_ATTACHMENTS = 3;
|
|
4
|
+
export declare const AI_MAX_ATTACHMENT_BYTES: number;
|
|
5
|
+
export declare const AI_MAX_TOTAL_BYTES: number;
|
|
6
|
+
export declare const INLINE_URL_SECONDS: number;
|
|
7
|
+
export declare const DOWNLOAD_URL_SECONDS: number;
|
|
8
|
+
export declare const AI_READABLE_TYPES: Set<string>;
|
|
9
|
+
/** Ingesloten afbeelding (cid:) in de mailtekst, geen losse bijlage. */
|
|
10
|
+
export declare function isInlineAttachment(a: EmailAttachment): boolean;
|
|
11
|
+
export declare function normalizedMime(a: EmailAttachment): string;
|
|
12
|
+
/** Type en status laten toe dat de AI de bijlage leest (los van de totaalgrens). */
|
|
13
|
+
export declare function isAiReadableAttachment(a: EmailAttachment): boolean;
|
|
14
|
+
/** Indexen (in de originele lijst) die naar de AI gaan: max 3 en samen max 10 MB. */
|
|
15
|
+
export declare function selectAiAttachments(list: EmailAttachment[]): number[];
|
|
16
|
+
/**
|
|
17
|
+
* True als er een echte (niet-ingesloten) bijlage is die de AI niet heeft
|
|
18
|
+
* gelezen. `readIndexes` = bijlagen die écht zijn meegestuurd en gelezen.
|
|
19
|
+
*/
|
|
20
|
+
export declare function computeAttachmentUnread(list: EmailAttachment[], hasAttachments: boolean, readIndexes: ReadonlySet<number>): boolean;
|
|
21
|
+
export declare function skippedLabel(reason: string | null | undefined): string | null;
|
|
22
|
+
//# sourceMappingURL=mailbox-attachments.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mailbox-attachments.d.ts","sourceRoot":"","sources":["../../src/lib/mailbox-attachments.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEtD,eAAO,MAAM,iBAAiB,2BAA2B,CAAC;AAC1D,eAAO,MAAM,kBAAkB,IAAI,CAAC;AACpC,eAAO,MAAM,uBAAuB,QAAmB,CAAC;AACxD,eAAO,MAAM,kBAAkB,QAAmB,CAAC;AACnD,eAAO,MAAM,kBAAkB,QAAU,CAAC;AAC1C,eAAO,MAAM,oBAAoB,QAAS,CAAC;AAE3C,eAAO,MAAM,iBAAiB,aAAwE,CAAC;AAEvG,wEAAwE;AACxE,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,eAAe,GAAG,OAAO,CAE9D;AAED,wBAAgB,cAAc,CAAC,CAAC,EAAE,eAAe,GAAG,MAAM,CAGzD;AAED,oFAAoF;AACpF,wBAAgB,sBAAsB,CAAC,CAAC,EAAE,eAAe,GAAG,OAAO,CAMlE;AAED,qFAAqF;AACrF,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,eAAe,EAAE,GAAG,MAAM,EAAE,CAWrE;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,eAAe,EAAE,EACvB,cAAc,EAAE,OAAO,EACvB,WAAW,EAAE,WAAW,CAAC,MAAM,CAAC,GAC/B,OAAO,CAGT;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAI7E"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export const ATTACHMENT_BUCKET = "email-auth-attachments";
|
|
2
|
+
export const AI_MAX_ATTACHMENTS = 3;
|
|
3
|
+
export const AI_MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024;
|
|
4
|
+
export const AI_MAX_TOTAL_BYTES = 10 * 1024 * 1024;
|
|
5
|
+
export const INLINE_URL_SECONDS = 60 * 60;
|
|
6
|
+
export const DOWNLOAD_URL_SECONDS = 5 * 60;
|
|
7
|
+
export const AI_READABLE_TYPES = new Set(["application/pdf", "image/png", "image/jpeg", "image/webp"]);
|
|
8
|
+
/** Ingesloten afbeelding (cid:) in de mailtekst, geen losse bijlage. */
|
|
9
|
+
export function isInlineAttachment(a) {
|
|
10
|
+
return a.isInline === true;
|
|
11
|
+
}
|
|
12
|
+
export function normalizedMime(a) {
|
|
13
|
+
const t = String(a.contentType ?? "").toLowerCase().split(";")[0].trim();
|
|
14
|
+
return t === "image/jpg" ? "image/jpeg" : t;
|
|
15
|
+
}
|
|
16
|
+
/** Type en status laten toe dat de AI de bijlage leest (los van de totaalgrens). */
|
|
17
|
+
export function isAiReadableAttachment(a) {
|
|
18
|
+
if (isInlineAttachment(a))
|
|
19
|
+
return false;
|
|
20
|
+
if (!a.storagePath || a.skippedReason)
|
|
21
|
+
return false;
|
|
22
|
+
if (!AI_READABLE_TYPES.has(normalizedMime(a)))
|
|
23
|
+
return false;
|
|
24
|
+
if (typeof a.size === "number" && a.size > AI_MAX_ATTACHMENT_BYTES)
|
|
25
|
+
return false;
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
/** Indexen (in de originele lijst) die naar de AI gaan: max 3 en samen max 10 MB. */
|
|
29
|
+
export function selectAiAttachments(list) {
|
|
30
|
+
const out = [];
|
|
31
|
+
let total = 0;
|
|
32
|
+
list.forEach((a, i) => {
|
|
33
|
+
if (out.length >= AI_MAX_ATTACHMENTS || !isAiReadableAttachment(a))
|
|
34
|
+
return;
|
|
35
|
+
const size = typeof a.size === "number" && a.size > 0 ? a.size : 0;
|
|
36
|
+
if (total + size > AI_MAX_TOTAL_BYTES)
|
|
37
|
+
return;
|
|
38
|
+
total += size;
|
|
39
|
+
out.push(i);
|
|
40
|
+
});
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* True als er een echte (niet-ingesloten) bijlage is die de AI niet heeft
|
|
45
|
+
* gelezen. `readIndexes` = bijlagen die écht zijn meegestuurd en gelezen.
|
|
46
|
+
*/
|
|
47
|
+
export function computeAttachmentUnread(list, hasAttachments, readIndexes) {
|
|
48
|
+
if (list.length === 0)
|
|
49
|
+
return hasAttachments;
|
|
50
|
+
return list.some((a, i) => !isInlineAttachment(a) && !readIndexes.has(i));
|
|
51
|
+
}
|
|
52
|
+
export function skippedLabel(reason) {
|
|
53
|
+
if (reason === "te_groot")
|
|
54
|
+
return "Te groot om op te halen";
|
|
55
|
+
if (reason === "ophalen_mislukt")
|
|
56
|
+
return "Bijlage kon niet worden opgehaald";
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { SupabaseClient } from "@supabase/supabase-js";
|
|
2
|
+
import type { EmailAttachment } from "./mailbox-body.js";
|
|
3
|
+
export interface AiFile {
|
|
4
|
+
index: number;
|
|
5
|
+
mimeType: string;
|
|
6
|
+
data: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Downloadt de bijlagen die de AI mag lezen. Ontbrekende bestanden of een
|
|
10
|
+
* ontbrekende bucket worden overgeslagen (tellen dan als niet gelezen).
|
|
11
|
+
*/
|
|
12
|
+
export declare function loadAiAttachmentFiles(admin: SupabaseClient<any>, list: EmailAttachment[]): Promise<AiFile[]>;
|
|
13
|
+
//# sourceMappingURL=mailbox-attachments.server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mailbox-attachments.server.d.ts","sourceRoot":"","sources":["../../src/lib/mailbox-attachments.server.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAStD,MAAM,WAAW,MAAM;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd;AAYD;;;GAGG;AACH,wBAAsB,qBAAqB,CACzC,KAAK,EAAE,cAAc,CAAC,GAAG,CAAC,EAC1B,IAAI,EAAE,eAAe,EAAE,GACtB,OAAO,CAAC,MAAM,EAAE,CAAC,CA6BnB"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { ATTACHMENT_BUCKET, AI_MAX_ATTACHMENT_BYTES, AI_MAX_TOTAL_BYTES, normalizedMime, selectAiAttachments, } from "./mailbox-attachments.js";
|
|
2
|
+
function toBase64(buf) {
|
|
3
|
+
const bytes = new Uint8Array(buf);
|
|
4
|
+
let bin = "";
|
|
5
|
+
const chunk = 0x8000;
|
|
6
|
+
for (let i = 0; i < bytes.length; i += chunk) {
|
|
7
|
+
bin += String.fromCharCode(...bytes.subarray(i, i + chunk));
|
|
8
|
+
}
|
|
9
|
+
return btoa(bin);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Downloadt de bijlagen die de AI mag lezen. Ontbrekende bestanden of een
|
|
13
|
+
* ontbrekende bucket worden overgeslagen (tellen dan als niet gelezen).
|
|
14
|
+
*/
|
|
15
|
+
export async function loadAiAttachmentFiles(admin, list) {
|
|
16
|
+
const files = [];
|
|
17
|
+
let total = 0;
|
|
18
|
+
for (const index of selectAiAttachments(list)) {
|
|
19
|
+
const a = list[index];
|
|
20
|
+
try {
|
|
21
|
+
const { data, error } = await admin.storage.from(ATTACHMENT_BUCKET).download(a.storagePath);
|
|
22
|
+
if (error || !data) {
|
|
23
|
+
console.error("[mailbox] bijlage niet beschikbaar, overgeslagen", {
|
|
24
|
+
type: normalizedMime(a),
|
|
25
|
+
reason: error?.message ?? "leeg",
|
|
26
|
+
});
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const buf = await data.arrayBuffer();
|
|
30
|
+
if (buf.byteLength > AI_MAX_ATTACHMENT_BYTES || total + buf.byteLength > AI_MAX_TOTAL_BYTES) {
|
|
31
|
+
console.error("[mailbox] bijlage boven grens, overgeslagen", { type: normalizedMime(a), bytes: buf.byteLength });
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
total += buf.byteLength;
|
|
35
|
+
files.push({ index, mimeType: normalizedMime(a), data: toBase64(buf) });
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
console.error("[mailbox] bijlage ophalen mislukt, overgeslagen", {
|
|
39
|
+
type: normalizedMime(a),
|
|
40
|
+
reason: String(e?.message ?? e).slice(0, 200),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return files;
|
|
45
|
+
}
|
|
@@ -26,8 +26,11 @@ export type EmailAttachment = {
|
|
|
26
26
|
contentId?: string | null;
|
|
27
27
|
contentBytes?: string | null;
|
|
28
28
|
url?: string | null;
|
|
29
|
+
storagePath?: string | null;
|
|
30
|
+
isInline?: boolean;
|
|
31
|
+
skippedReason?: string | null;
|
|
29
32
|
};
|
|
30
33
|
export declare function normalizeAttachments(value: unknown): EmailAttachment[];
|
|
31
34
|
/** Vervangt `cid:`-verwijzingen door data-URL's van de bijbehorende bijlage. */
|
|
32
|
-
export declare function inlineCidImages(html: string, attachments: EmailAttachment[]): string;
|
|
35
|
+
export declare function inlineCidImages(html: string, attachments: EmailAttachment[], signedUrls?: ReadonlyMap<number, string>): string;
|
|
33
36
|
//# sourceMappingURL=mailbox-body.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mailbox-body.d.ts","sourceRoot":"","sources":["../../src/lib/mailbox-body.ts"],"names":[],"mappings":"AAIA,mFAAmF;AACnF,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAKhD;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAsB/C;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACnC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAClC,MAAM,CAMR;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAWzE;AAED,MAAM,MAAM,YAAY,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC;AAE3E,mFAAmF;AACnF,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,YAAY,EAAE,CAenE;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"mailbox-body.d.ts","sourceRoot":"","sources":["../../src/lib/mailbox-body.ts"],"names":[],"mappings":"AAIA,mFAAmF;AACnF,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAKhD;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAsB/C;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACnC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAClC,MAAM,CAMR;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAWzE;AAED,MAAM,MAAM,YAAY,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC;AAE3E,mFAAmF;AACnF,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,YAAY,EAAE,CAenE;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B,CAAC;AAEF,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,eAAe,EAAE,CAsBtE;AAED,gFAAgF;AAChF,wBAAgB,eAAe,CAC7B,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,eAAe,EAAE,EAC9B,UAAU,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,GACvC,MAAM,CAgBR"}
|
|
@@ -87,7 +87,13 @@ export function normalizeAttachments(value) {
|
|
|
87
87
|
if (!v || typeof v !== "object")
|
|
88
88
|
continue;
|
|
89
89
|
const o = v;
|
|
90
|
+
const cid = o["contentId"] ?? o["content_id"] ?? o["cid"] ?? null;
|
|
91
|
+
const inlineFlag = o["isInline"] ?? o["is_inline"] ?? o["inline"];
|
|
90
92
|
out.push({
|
|
93
|
+
storagePath: o["storagePath"] ?? o["storage_path"] ?? null,
|
|
94
|
+
// Oud formaat kende geen isInline: daar gold een contentId als ingesloten.
|
|
95
|
+
isInline: typeof inlineFlag === "boolean" ? inlineFlag : !!cid,
|
|
96
|
+
skippedReason: o["skippedReason"] ?? o["skipped_reason"] ?? null,
|
|
91
97
|
name: String(o["name"] ?? o["filename"] ?? o["fileName"] ?? "bijlage"),
|
|
92
98
|
contentType: o["contentType"] ?? o["content_type"] ?? o["mimeType"] ?? null,
|
|
93
99
|
size: typeof o["size"] === "number" ? o["size"] : Number(o["size"]) || null,
|
|
@@ -99,16 +105,23 @@ export function normalizeAttachments(value) {
|
|
|
99
105
|
return out;
|
|
100
106
|
}
|
|
101
107
|
/** Vervangt `cid:`-verwijzingen door data-URL's van de bijbehorende bijlage. */
|
|
102
|
-
export function inlineCidImages(html, attachments) {
|
|
108
|
+
export function inlineCidImages(html, attachments, signedUrls) {
|
|
103
109
|
if (!html || attachments.length === 0)
|
|
104
110
|
return html;
|
|
105
111
|
let out = html;
|
|
106
|
-
|
|
107
|
-
if (!a.contentId
|
|
108
|
-
|
|
112
|
+
attachments.forEach((a, i) => {
|
|
113
|
+
if (!a.contentId)
|
|
114
|
+
return;
|
|
115
|
+
const signed = signedUrls?.get(i);
|
|
116
|
+
const target = signed
|
|
117
|
+
? signed
|
|
118
|
+
: a.contentBytes
|
|
119
|
+
? `data:${a.contentType || "application/octet-stream"};base64,${a.contentBytes}`
|
|
120
|
+
: null;
|
|
121
|
+
if (!target)
|
|
122
|
+
return;
|
|
109
123
|
const cid = a.contentId.replace(/^<|>$/g, "");
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
124
|
+
out = out.replace(new RegExp(`cid:${cid.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, "gi"), target);
|
|
125
|
+
});
|
|
113
126
|
return out;
|
|
114
127
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mailbox-ingest.server.d.ts","sourceRoot":"","sources":["../../src/lib/mailbox-ingest.server.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"mailbox-ingest.server.d.ts","sourceRoot":"","sources":["../../src/lib/mailbox-ingest.server.ts"],"names":[],"mappings":"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"}
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
// GEEN imports van "@/integrations/supabase/client.server" op module-scope —
|
|
9
9
|
// die worden per aanroep dynamisch geladen (client-reachable modules mogen
|
|
10
10
|
// server-only helpers niet direct binnenhalen).
|
|
11
|
+
import { normalizeAttachments } from "./mailbox-body.js";
|
|
12
|
+
import { computeAttachmentUnread, selectAiAttachments } from "./mailbox-attachments.js";
|
|
11
13
|
import { emailBodyText } from "./mailbox-body.js";
|
|
12
14
|
import { detectLoadCarrierInText, isLoadCarrierAllowed, loadCarrierLabel, normalizeAllowedLoadCarriers, } from "./load-carriers.js";
|
|
13
15
|
import { classifyMessage, extractLatestPart } from "./mailbox-filter.js";
|
|
@@ -268,6 +270,12 @@ async function aiParseMessage(admin, args) {
|
|
|
268
270
|
throw new Error("Bericht bevat geen tekst om te lezen.");
|
|
269
271
|
const ctx = await buildAiContext(admin, args.customerId, `${args.subject}\n${args.text}`, args.settings.max_candidates, args.idx);
|
|
270
272
|
const { parseOrderEmail } = await import("./mailbox-ai.server.js");
|
|
273
|
+
const attList = normalizeAttachments(args.attachments);
|
|
274
|
+
let files = [];
|
|
275
|
+
if (selectAiAttachments(attList).length > 0) {
|
|
276
|
+
const { loadAiAttachmentFiles } = await import("./mailbox-attachments.server.js");
|
|
277
|
+
files = await loadAiAttachmentFiles(admin, attList);
|
|
278
|
+
}
|
|
271
279
|
const res = await parseOrderEmail(admin, {
|
|
272
280
|
bodyText: args.text,
|
|
273
281
|
subject: args.subject,
|
|
@@ -279,6 +287,7 @@ async function aiParseMessage(admin, args) {
|
|
|
279
287
|
systemPrompt: ctx.settings.systemPrompt,
|
|
280
288
|
now: args.receivedAt,
|
|
281
289
|
maxAttemptsPerModel: args.settings.max_retry_attempts,
|
|
290
|
+
files,
|
|
282
291
|
});
|
|
283
292
|
return {
|
|
284
293
|
parsed: res.parsed,
|
|
@@ -287,6 +296,7 @@ async function aiParseMessage(admin, args) {
|
|
|
287
296
|
transient: false,
|
|
288
297
|
requiredFields: ctx.settings.requiredFields,
|
|
289
298
|
candidateIds: new Set(ctx.candidates.map((c) => c.id)),
|
|
299
|
+
readAttachmentIndexes: Array.isArray(res.readAttachmentIndexes) ? res.readAttachmentIndexes : [],
|
|
290
300
|
};
|
|
291
301
|
}
|
|
292
302
|
catch (e) {
|
|
@@ -529,11 +539,9 @@ async function loadMessage(admin, messageId) {
|
|
|
529
539
|
.maybeSingle();
|
|
530
540
|
return data ?? null;
|
|
531
541
|
}
|
|
532
|
-
/**
|
|
533
|
-
function hasUnreadAttachment(msg) {
|
|
534
|
-
|
|
535
|
-
const real = list.filter((a) => !(a?.isInline === true || a?.inline === true || a?.contentId));
|
|
536
|
-
return real.length > 0 || (msg.has_attachments === true && list.length === 0);
|
|
542
|
+
/** Echte bijlage die de AI niet heeft kunnen lezen (type, grootte, ontbrekend of geweigerd). */
|
|
543
|
+
function hasUnreadAttachment(msg, readIndexes = []) {
|
|
544
|
+
return computeAttachmentUnread(normalizeAttachments(msg.attachments), msg.has_attachments === true, new Set(readIndexes));
|
|
537
545
|
}
|
|
538
546
|
async function senderMatch(admin, email, s) {
|
|
539
547
|
const match = await matchCustomerByEmail(admin, email);
|
|
@@ -635,6 +643,7 @@ async function analyzeAndBuild(admin, msg, customerId, text, s) {
|
|
|
635
643
|
receivedAt: msg.received_at ? new Date(msg.received_at) : new Date(),
|
|
636
644
|
settings: s,
|
|
637
645
|
idx,
|
|
646
|
+
attachments: msg.attachments,
|
|
638
647
|
});
|
|
639
648
|
const parsed = attempt.parsed;
|
|
640
649
|
const fieldMeta = {};
|
|
@@ -709,7 +718,7 @@ async function analyzeAndBuild(admin, msg, customerId, text, s) {
|
|
|
709
718
|
load_carrier_detected: lc.detectedLabel,
|
|
710
719
|
field_meta: fieldMeta,
|
|
711
720
|
duplicate_of: duplicate,
|
|
712
|
-
attachment_unread: hasUnreadAttachment(msg),
|
|
721
|
+
attachment_unread: hasUnreadAttachment(msg, attempt.readAttachmentIndexes ?? []),
|
|
713
722
|
readiness: readiness(validation, lines, !!loc, !!customerId),
|
|
714
723
|
thresholds: { green: Number(s.conf_green), orange: Number(s.conf_orange) },
|
|
715
724
|
blockedMessageStatus: lc.blocked,
|
|
@@ -92,6 +92,24 @@ export declare const getProposalDetail: import("@tanstack/start-client-core").Op
|
|
|
92
92
|
}, Promise<{
|
|
93
93
|
item: any;
|
|
94
94
|
}>>;
|
|
95
|
+
/** Korte downloadlink (5 minuten) voor één bijlage van een bericht. */
|
|
96
|
+
export declare const getMailboxAttachmentUrl: import("@tanstack/start-client-core").OptionalFetcher<readonly [import("@tanstack/start-client-core").FunctionMiddlewareAfterServer<{}, unknown, undefined, {
|
|
97
|
+
supabase: import("../integrations/supabase/auth-middleware.js").AuthedSupabaseClient;
|
|
98
|
+
userId: string;
|
|
99
|
+
claims: import("@supabase/auth-js").User;
|
|
100
|
+
}, undefined, undefined, undefined>], (input: unknown) => {
|
|
101
|
+
messageId: string;
|
|
102
|
+
index: number;
|
|
103
|
+
}, Promise<{
|
|
104
|
+
url: string | null;
|
|
105
|
+
error: "not_found";
|
|
106
|
+
} | {
|
|
107
|
+
url: null;
|
|
108
|
+
error: "not_available";
|
|
109
|
+
} | {
|
|
110
|
+
url: string | null;
|
|
111
|
+
error: null;
|
|
112
|
+
}>>;
|
|
95
113
|
export declare const listAllowedLoadCarriers: import("@tanstack/start-client-core").OptionalFetcher<readonly [import("@tanstack/start-client-core").FunctionMiddlewareAfterServer<{}, unknown, undefined, {
|
|
96
114
|
supabase: import("../integrations/supabase/auth-middleware.js").AuthedSupabaseClient;
|
|
97
115
|
userId: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mailbox.functions.d.ts","sourceRoot":"","sources":["../../src/lib/mailbox.functions.ts"],"names":[],"mappings":"AACA,OAAO,EAAyB,KAAK,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"mailbox.functions.d.ts","sourceRoot":"","sources":["../../src/lib/mailbox.functions.ts"],"names":[],"mappings":"AACA,OAAO,EAAyB,KAAK,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAgCnF,eAAO,MAAM,sBAAsB,2GAqB/B,CAAC;AAWL,eAAO,MAAM,cAAc,2EACD,OAAO;;;;;;;;GA+B7B,CAAC;AAGL,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;GAWzB,CAAC;AAEL,+EAA+E;AAC/E,eAAO,MAAM,kBAAkB;;;;8CAEL,OAAO;;;;;GAK7B,CAAC;AAEL,iFAAiF;AACjF,eAAO,MAAM,8BAA8B;;;;;;;;;GAMvC,CAAC;AAML,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;GAWrB,CAAC;AAEL,eAAO,MAAM,YAAY;;;;8CAEC,OAAO;;;;GAa7B,CAAC;AAKL,eAAO,MAAM,aAAa;;;;8CAEA,OAAO;;;;GAwD7B,CAAC;AAIL,qEAAqE;AACrE,eAAO,MAAM,iBAAiB;;;;8CAEJ,OAAO;;;;GA2D7B,CAAC;AAOL,uEAAuE;AACvE,eAAO,MAAM,uBAAuB;;;;8CAEV,OAAO;;;;SAaI,MAAM,GAAG,IAAI;;;;;;SAgBjB,MAAM,GAAG,IAAI;;GAC1C,CAAC;AAIL,eAAO,MAAM,uBAAuB;;;;;WAO+B,MAAM,GAAG,IAAI;;;;GAC5E,CAAC;AA2BL,eAAO,MAAM,eAAe;;;;8CAEF,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;aAkKF,MAAM;iBACE,MAAM;;;;;;GAIzC,CAAC;AAyCL,eAAO,MAAM,cAAc;;;;8CAED,OAAO;;;;;GAY7B,CAAC;AAGL,eAAO,MAAM,cAAc;;;;8CAED,OAAO;;;;GAW7B,CAAC;AAQL,eAAO,MAAM,sBAAsB;;;;;;GAiD/B,CAAC;AAIL,eAAO,MAAM,uBAAuB;;;;8CAEV,OAAO;;;;;;;;;;;;QAyBiB,MAAM;;;QAoBxB,MAAM;;GAClC,CAAC;AAKL,eAAO,MAAM,qBAAqB;;;;;;;GAiH9B,CAAC;AAiBL,eAAO,MAAM,oBAAoB;;;;8CAEP,OAAO;;;;;qBAqBS,MAAM;sBAAgB,MAAM;gBAAU,MAAM;gBAAU,MAAM;kBAAY,MAAM;;;;uBAhCvG,MAAM;oBACT,MAAM,GAAG,IAAI;sBACX,MAAM;0BAEF,MAAM;0BACN,MAAM;yBACP,MAAM;;;qBA0FW,MAAM;eAClB,MAAM,GAAG,IAAI;;;;GAoBjC,CAAC;AAiBL,eAAO,MAAM,eAAe;;;;8CAEF,OAAO;;;;;;;;;;;;;;;;GAuC7B,CAAC;AAEL,eAAO,MAAM,cAAc;;;;0CAEL;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE;aAAR,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAQnC,CAAC;AAEL,eAAO,MAAM,gCAAgC;;;;0CAEvB;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE;gBAAR,MAAM;;;;;;;;;;;;;;;;;;;GAUtC,CAAC;AAIL,eAAO,MAAM,mBAAmB;;;;;;GAK5B,CAAC;AAEL,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;GAS9B,CAAC;AAGL,eAAO,MAAM,gBAAgB;;;;;;GAUzB,CAAC;AAiBL,eAAO,MAAM,8BAA8B;;;;8CAEjB,OAAO;;;;0BAOT,MAAM;0BAAoB,MAAM;yBAAmB,MAAM;oBAC/D,OAAO;oBACP,MAAM,GAAG,IAAI;oBAAc,MAAM,GAAG,IAAI;sBACtC,MAAM,GAAG,IAAI;sBAAgB,MAAM,GAAG,IAAI;;yBAEO,MAAM;yBAAmB,MAAM;;;0BAL5E,MAAM;0BAAoB,MAAM;yBAAmB,MAAM;oBAC/D,OAAO;oBACP,MAAM,GAAG,IAAI;oBAAc,MAAM,GAAG,IAAI;sBACtC,MAAM,GAAG,IAAI;sBAAgB,MAAM,GAAG,IAAI;;;;;GAsM1D,CAAC;AAUL,eAAO,MAAM,mBAAmB;;;;8CAEN,OAAO;;;;;;;;;;;WAgBc,MAAM,GAAG,IAAI;aAA4B,GAAG;GACvF,CAAC;AAGL,eAAO,MAAM,kBAAkB;;;;8CAEL,OAAO;;;;;;;;;mBAQU,MAAM;kBAAY,MAAM,GAAG,IAAI;gBAAU,MAAM,GAAG,IAAI;;GAc7F,CAAC;AAGL,eAAO,MAAM,mBAAmB;;;;;;GAgB5B,CAAC;AAEL,+EAA+E;AAC/E,eAAO,MAAM,iBAAiB;;;;8CAEJ,OAAO;;;;WAOO,MAAM,GAAG,IAAI;;;;GAIjD,CAAC;AAEL,oFAAoF;AACpF,eAAO,MAAM,sBAAsB;;;;;;GAM/B,CAAC;AAKL,eAAO,MAAM,kBAAkB;;;;8CAEL,OAAO;;;;;cAS+B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;;;GAWnF,CAAC;AAIL,gGAAgG;AAChG,wBAAsB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAMzE;AAED,eAAO,MAAM,uBAAuB;;;;;;GAMhC,CAAC;AAEL,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAClD,CAAC;AAEF,eAAO,MAAM,gBAAgB;;;;wEAuFzB,CAAC"}
|
|
@@ -7,6 +7,7 @@ import { requireSupabaseAuth } from "../integrations/supabase/auth-middleware.js
|
|
|
7
7
|
import { createClient } from "@supabase/supabase-js";
|
|
8
8
|
import { z } from "zod";
|
|
9
9
|
import { emailBodyText, inlineCidImages, normalizeAddressList, normalizeAttachments, sanitizeEmailHtml, stripInvisible, } from "./mailbox-body.js";
|
|
10
|
+
import { ATTACHMENT_BUCKET, DOWNLOAD_URL_SECONDS, INLINE_URL_SECONDS } from "./mailbox-attachments.js";
|
|
10
11
|
import { EMPTY_LOAD_CARRIERS, isLoadCarrierAllowed, loadCarrierLabel, normalizeAllowedLoadCarriers, } from "./load-carriers.js";
|
|
11
12
|
// ─── Actieve mailbox (uit gedeelde e-mail-auth module) ──────────────────────
|
|
12
13
|
// De RPC email_auth_for_purpose vereist een ingelogde gebruiker (anders 42501).
|
|
@@ -204,7 +205,7 @@ export const getProposalDetail = createServerFn({ method: "GET" })
|
|
|
204
205
|
const sel = (s) => s;
|
|
205
206
|
const { data: row, error } = await supabaseAdmin
|
|
206
207
|
.from("mailbox_order_proposals")
|
|
207
|
-
.select(sel("*, mailbox_messages(subject, from_email, from_name, body_text, body_html, received_at, connection_id, account_email, to_recipients, cc_recipients, attachments, has_attachments), " +
|
|
208
|
+
.select(sel("*, mailbox_messages(id, subject, from_email, from_name, body_text, body_html, received_at, connection_id, account_email, to_recipients, cc_recipients, attachments, has_attachments), " +
|
|
208
209
|
"mailbox_proposal_lines(*), mailbox_proposal_field_values(*)"))
|
|
209
210
|
.eq("id", data.proposalId)
|
|
210
211
|
.maybeSingle();
|
|
@@ -216,7 +217,21 @@ export const getProposalDetail = createServerFn({ method: "GET" })
|
|
|
216
217
|
if (!m)
|
|
217
218
|
return { item: row };
|
|
218
219
|
const attachments = normalizeAttachments(m.attachments);
|
|
219
|
-
|
|
220
|
+
// Ingesloten afbeeldingen: korte ondertekende link (1 uur) uit de privé bucket.
|
|
221
|
+
const signed = new Map();
|
|
222
|
+
await Promise.all(attachments.map(async (a, i) => {
|
|
223
|
+
if (!a.contentId || !a.storagePath)
|
|
224
|
+
return;
|
|
225
|
+
const { data: s, error: e } = await supabaseAdmin.storage
|
|
226
|
+
.from(ATTACHMENT_BUCKET)
|
|
227
|
+
.createSignedUrl(a.storagePath, INLINE_URL_SECONDS);
|
|
228
|
+
if (e || !s?.signedUrl) {
|
|
229
|
+
console.error("[mailbox] ingesloten afbeelding niet beschikbaar", { reason: e?.message ?? "leeg" });
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
signed.set(i, s.signedUrl);
|
|
233
|
+
}));
|
|
234
|
+
const html = inlineCidImages(sanitizeEmailHtml(m.body_html), attachments, signed);
|
|
220
235
|
return {
|
|
221
236
|
item: {
|
|
222
237
|
...row,
|
|
@@ -233,12 +248,51 @@ export const getProposalDetail = createServerFn({ method: "GET" })
|
|
|
233
248
|
size: a.size ?? null,
|
|
234
249
|
contentId: a.contentId ?? null,
|
|
235
250
|
url: a.url ?? null,
|
|
251
|
+
isInline: a.isInline === true,
|
|
252
|
+
downloadable: !!a.storagePath && !a.skippedReason,
|
|
253
|
+
skippedReason: a.skippedReason ?? null,
|
|
236
254
|
})),
|
|
237
255
|
has_attachments: m.has_attachments === true || attachments.length > 0,
|
|
238
256
|
},
|
|
239
257
|
},
|
|
240
258
|
};
|
|
241
259
|
});
|
|
260
|
+
/** Korte downloadlink (5 minuten) voor één bijlage van een bericht. */
|
|
261
|
+
export const getMailboxAttachmentUrl = createServerFn({ method: "POST" })
|
|
262
|
+
.middleware([requireSupabaseAuth])
|
|
263
|
+
.inputValidator((input) => z.object({ messageId: z.string().uuid(), index: z.number().int().min(0).max(500) }).parse(input))
|
|
264
|
+
.handler(async ({ data }) => {
|
|
265
|
+
const { supabaseAdmin } = await import("../integrations/supabase/client.server.js");
|
|
266
|
+
// Zelfde regel als getProposalDetail: ingelogd en het bericht hoort bij een voorstel.
|
|
267
|
+
const { data: prop, error: pe } = await supabaseAdmin
|
|
268
|
+
.from("mailbox_order_proposals")
|
|
269
|
+
.select("id")
|
|
270
|
+
.eq("message_id", data.messageId)
|
|
271
|
+
.limit(1)
|
|
272
|
+
.maybeSingle();
|
|
273
|
+
if (pe)
|
|
274
|
+
throw new Error(pe.message);
|
|
275
|
+
if (!prop)
|
|
276
|
+
return { url: null, error: "not_found" };
|
|
277
|
+
const { data: m, error } = await supabaseAdmin
|
|
278
|
+
.from("mailbox_messages")
|
|
279
|
+
.select("attachments")
|
|
280
|
+
.eq("id", data.messageId)
|
|
281
|
+
.maybeSingle();
|
|
282
|
+
if (error)
|
|
283
|
+
throw new Error(error.message);
|
|
284
|
+
const a = normalizeAttachments(m?.attachments)[data.index];
|
|
285
|
+
if (!a || !a.storagePath || a.skippedReason)
|
|
286
|
+
return { url: null, error: "not_available" };
|
|
287
|
+
const { data: s, error: se } = await supabaseAdmin.storage
|
|
288
|
+
.from(ATTACHMENT_BUCKET)
|
|
289
|
+
.createSignedUrl(a.storagePath, DOWNLOAD_URL_SECONDS, { download: a.name });
|
|
290
|
+
if (se || !s?.signedUrl) {
|
|
291
|
+
console.error("[mailbox] bijlage niet meer beschikbaar", { reason: se?.message ?? "leeg" });
|
|
292
|
+
return { url: null, error: "not_available" };
|
|
293
|
+
}
|
|
294
|
+
return { url: s.signedUrl, error: null };
|
|
295
|
+
});
|
|
242
296
|
// ─── Ladingdragers uit de Logistics Engine ─────────────────────────────────
|
|
243
297
|
// Nooit cachen: de instelling kan op elk moment wijzigen.
|
|
244
298
|
export const listAllowedLoadCarriers = createServerFn({ method: "GET" })
|