@flowselections/twinfield-authenticatie-module-15-06-2026 2.0.4 → 2.0.8
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/settings/TwinfieldAuthSettingsCard.d.ts.map +1 -1
- package/dist-lib/components/settings/TwinfieldAuthSettingsCard.js +28 -5
- package/dist-lib/integrations/supabase/types.d.ts +530 -0
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/lib/twinfield-adapter.functions.d.ts.map +1 -1
- package/dist-lib/lib/twinfield-adapter.functions.js +19 -2
- package/dist-lib/lib/twinfield-sync.functions.d.ts +11 -0
- package/dist-lib/lib/twinfield-sync.functions.d.ts.map +1 -1
- package/dist-lib/lib/twinfield-sync.functions.js +33 -0
- package/package.json +1 -1
- package/public/flowselections-assets/twinfield-authenticatie/consumer-api.md +3 -0
- package/public/flowselections-assets/twinfield-authenticatie/schema.sql +26 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TwinfieldAuthSettingsCard.d.ts","sourceRoot":"","sources":["../../../src/components/settings/TwinfieldAuthSettingsCard.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"TwinfieldAuthSettingsCard.d.ts","sourceRoot":"","sources":["../../../src/components/settings/TwinfieldAuthSettingsCard.tsx"],"names":[],"mappings":"AA0eA,wBAAgB,yBAAyB,CAAC,EACxC,eAAsB,GACvB,GAAE;IACD,eAAe,CAAC,EAAE,OAAO,CAAC;CACtB,+BAqeL"}
|
|
@@ -2,12 +2,16 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import { useEffect, useState } from "react";
|
|
3
3
|
// Maximum aantal pogingen in de klantenwachtrij (gedeeld contract met Klanten).
|
|
4
4
|
const TWINFIELD_PUSH_MAX_ATTEMPTS = 5;
|
|
5
|
+
// Haalt alleen een technisch voorvoegsel "fieldN: " weg uit een Twinfield-melding.
|
|
6
|
+
function stripFieldPrefix(message) {
|
|
7
|
+
return message.replace(/(—\s*|^\s*)field\d+:\s*/i, "$1");
|
|
8
|
+
}
|
|
5
9
|
import { useServerFn } from "@tanstack/react-start";
|
|
6
10
|
import { Card, Button, Badge, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, toast, } from "@flowselections/core";
|
|
7
11
|
import { ChevronUp, ChevronDown, Link2, Unlink } from "lucide-react";
|
|
8
12
|
const twinfieldLogo = "/flowselections-assets/twinfield-authenticatie/twinfield-logo.svg";
|
|
9
13
|
import { startTwinfieldOAuth, syncTwinfieldCentralAccounts, selectTwinfieldCentralAccount, listTwinfieldConnections, disconnectTwinfieldConnection, setTwinfieldConnectionOffice, listTwinfieldOfficesForConnection, getTwinfieldSyncOverview, autoRegisterTwinfieldCron, } from "../../lib/twinfield.functions.js";
|
|
10
|
-
import { triggerTwinfieldSync, getTwinfieldSyncState, getTwinfieldPublicBaseUrl, setTwinfieldPublicBaseUrl, getTwinfieldPageSummary, } from "../../lib/twinfield-sync.functions.js";
|
|
14
|
+
import { triggerTwinfieldSync, getTwinfieldOfficeKind, getTwinfieldSyncState, getTwinfieldPublicBaseUrl, setTwinfieldPublicBaseUrl, getTwinfieldPageSummary, } from "../../lib/twinfield-sync.functions.js";
|
|
11
15
|
import { listTwinfieldPushQueue, retryTwinfieldQueueItem, } from "../../lib/twinfield-adapter.functions.js";
|
|
12
16
|
import { cn } from "../../lib/utils.js";
|
|
13
17
|
/** PostgREST meldt ontbrekende kolommen als schema-cachefout. Dat betekent in de
|
|
@@ -63,6 +67,21 @@ function ConnectionCard({ conn, onChanged, onDisconnected, isAdmin = false, }) {
|
|
|
63
67
|
const disconnect = useServerFn(disconnectTwinfieldConnection);
|
|
64
68
|
const trigger = useServerFn(triggerTwinfieldSync);
|
|
65
69
|
const startOAuthForCard = useServerFn(startTwinfieldOAuth);
|
|
70
|
+
const loadOfficeKind = useServerFn(getTwinfieldOfficeKind);
|
|
71
|
+
const [officeDemo, setOfficeDemo] = useState(null);
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
let alive = true;
|
|
74
|
+
if (!conn.connected || !conn.office_code) {
|
|
75
|
+
setOfficeDemo(null);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
loadOfficeKind({ data: { connection_id: conn.id } })
|
|
79
|
+
.then((r) => { if (alive)
|
|
80
|
+
setOfficeDemo(r?.demo ?? null); })
|
|
81
|
+
.catch(() => { if (alive)
|
|
82
|
+
setOfficeDemo(null); });
|
|
83
|
+
return () => { alive = false; };
|
|
84
|
+
}, [conn.id, conn.connected, conn.office_code, loadOfficeKind]);
|
|
66
85
|
const [offices, setOffices] = useState([]);
|
|
67
86
|
const [officesStatus, setOfficesStatus] = useState("empty");
|
|
68
87
|
const [officesMessage, setOfficesMessage] = useState(null);
|
|
@@ -203,7 +222,11 @@ function ConnectionCard({ conn, onChanged, onDisconnected, isAdmin = false, }) {
|
|
|
203
222
|
const cardNeedsReauth = conn.reauthorize_required ||
|
|
204
223
|
(Array.isArray(conn.missing_scopes) && conn.missing_scopes.length > 0) ||
|
|
205
224
|
needsReauth;
|
|
206
|
-
return (_jsxs("div", { className: "rounded-lg border p-4 space-y-4", children: [_jsxs("div", { className: "flex items-start justify-between gap-3", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [_jsx("span", { className: "font-semibold text-foreground truncate", children: displayName }), _jsx(Badge, { variant: cardNeedsReauth ? "destructive" : conn.connected ? "default" : "secondary", className: "font-normal", children: cardNeedsReauth ? "Autorisatie vereist" : conn.connected ? "Verbonden" : "Onvolledig" }), _jsx(Badge, { variant: "outline", className: "font-normal", children:
|
|
225
|
+
return (_jsxs("div", { className: "rounded-lg border p-4 space-y-4", children: [_jsxs("div", { className: "flex items-start justify-between gap-3", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [_jsx("span", { className: "font-semibold text-foreground truncate", children: displayName }), _jsx(Badge, { variant: cardNeedsReauth ? "destructive" : conn.connected ? "default" : "secondary", className: "font-normal", children: cardNeedsReauth ? "Autorisatie vereist" : conn.connected ? "Verbonden" : "Onvolledig" }), _jsx(Badge, { variant: "outline", className: "font-normal", children: officeDemo === true
|
|
226
|
+
? "Testadministratie"
|
|
227
|
+
: conn.environment === "staging" || conn.environment === "sandbox"
|
|
228
|
+
? "Testomgeving"
|
|
229
|
+
: "Live" })] }), _jsxs("div", { className: "mt-1 text-xs text-muted-foreground", children: [effectiveOfficeCode ? (_jsx("span", { className: "font-mono", children: effectiveOfficeCode })) : (_jsx("span", { children: "Kies hieronder een administratie om te kunnen synchroniseren." })), isAdmin && conn.account_key && (_jsxs("span", { className: "ml-2", children: ["\u00B7 account ", _jsx("span", { className: "font-mono", children: conn.account_key })] })), conn.connected_at && (_jsxs("span", { className: "ml-2", children: ["\u00B7 gekoppeld ", relativeTime(conn.connected_at)] }))] }), conn.connected && !effectiveOfficeCode && !cardNeedsReauth && (_jsx("div", { className: "mt-2 rounded-md border border-primary/30 bg-primary/5 px-3 py-2 text-xs text-foreground", children: "Kies een administratie om te beginnen met synchroniseren." })), cardNeedsReauth && (_jsxs("div", { className: "mt-2 rounded-md border border-destructive/30 bg-destructive/5 px-3 py-2 text-xs text-destructive", children: [_jsx("div", { className: "font-medium", children: conn.reauth_message ?? "De toestemming bij Twinfield moet worden vernieuwd. Autoriseer opnieuw." }), isAdmin && Array.isArray(conn.missing_scopes) && conn.missing_scopes.length > 0 && (_jsxs("div", { className: "mt-0.5", children: ["Ontbrekende scopes: ", conn.missing_scopes.join(", ")] })), _jsxs(Button, { size: "sm", variant: "outline", className: "mt-2 border-destructive/50 text-destructive hover:bg-destructive/10", onClick: handleReauthorize, disabled: reauthorizing, children: [_jsx(Link2, { className: "h-3.5 w-3.5 mr-1.5" }), reauthorizing ? "Bezig…" : "Opnieuw autoriseren"] })] }))] }), _jsxs(Button, { size: "sm", variant: "ghost", onClick: handleDisconnect, disabled: disconnecting, className: "text-muted-foreground hover:text-destructive", children: [_jsx(Unlink, { className: "h-4 w-4 mr-1.5" }), disconnecting ? "Bezig…" : "Verbreken"] })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx("div", { className: "text-xs font-medium text-muted-foreground", children: "Administratie" }), offices.length > 0 ? (_jsxs(Select, { value: effectiveOfficeCode ?? undefined, onValueChange: handleOfficeChange, disabled: savingOffice, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Kies een administratie", children: (() => {
|
|
207
230
|
const selected = offices.find((o) => o.code === effectiveOfficeCode);
|
|
208
231
|
if (!selected)
|
|
209
232
|
return null;
|
|
@@ -365,12 +388,12 @@ export function TwinfieldAuthSettingsCard({ defaultExpanded = true, } = {}) {
|
|
|
365
388
|
? "Koppeling controleren…"
|
|
366
389
|
: anyConnected
|
|
367
390
|
? "Verbonden"
|
|
368
|
-
: "Niet verbonden" })] })] }), _jsx("p", { className: "text-sm text-muted-foreground", children: "E\u00E9n Twinfield-account per omgeving. Verbreek eerst de huidige koppeling om te wisselen naar een ander account." })] })] }), _jsx("button", { type: "button", onClick: () => setIsExpanded((v) => !v), className: "shrink-0 p-1 rounded-md hover:bg-muted transition-colors", "aria-label": isExpanded ? "Samenvouwen" : "Uitklappen", children: isExpanded ? (_jsx(ChevronUp, { className: "h-5 w-5 text-muted-foreground" })) : (_jsx(ChevronDown, { className: "h-5 w-5 text-muted-foreground" })) })] }), isExpanded && (_jsxs("div", { className: "space-y-6 p-6", children: [summary && _jsx(SummaryPanel, { summary: summary }), centralNotice && (_jsx("div", { className: "rounded-lg border bg-muted/30 px-4 py-3 text-sm text-muted-foreground", "aria-live": "polite", children: centralNotice })), !hasAny && !loading && (_jsxs("div", { className: "rounded-lg border border-dashed p-8 text-center space-y-4", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-base font-semibold text-foreground", children: "Nog niet verbonden met Twinfield" }), _jsx("p", { className: "text-sm text-muted-foreground mt-1", children: "Log in bij Twinfield en geef eenmalig toestemming. Wij bewaren de toegang veilig en verversen tokens automatisch." })] }), _jsxs(Button, { size: "lg", onClick: handleConnect, disabled: connecting, children: [_jsx(Link2, { className: "h-4 w-4 mr-2" }), connecting ? "Koppelen…" : "Verbind met Twinfield"] })] })), selection && (selection.needs_choice || selection.replaced || actionIgnored(selection.ignored).length > 0) && (_jsxs("div", { className: "rounded-lg border p-4 space-y-3 text-sm", children: [selection.replaced && (_jsxs("div", { className: "rounded-md border border-primary/30 bg-primary/5 px-3 py-2", children: ["Koppeling vervangen: ", _jsx("span", { className: "font-mono", children: selection.replaced.from }), " (", selection.replaced.reason, ") \u2192 ", _jsx("span", { className: "font-mono", children: selection.replaced.to }), "."] })), selection.needs_choice && (_jsxs("div", { className: "space-y-2", children: [_jsx("div", { className: "font-semibold text-foreground", children: "Kies Twinfield-account" }), _jsx("div", { className: "text-xs text-muted-foreground", children: "Er zijn meerdere bruikbare accounts. Kies welk account deze module gebruikt." }), _jsx("div", { className: "divide-y rounded-md border", children: selection.choices.map((ch) => (_jsxs("div", { className: "flex items-center justify-between gap-3 px-3 py-2", children: [_jsxs("div", { className: "min-w-0 text-xs", children: [_jsx("div", { className: "font-mono text-foreground", children: ch.account_key }), _jsxs("div", { className: "text-muted-foreground", children: [ch.environment === "live" ? "Live" : "Test", " \u00B7", " ", ch.company_name ?? ch.office_code ?? "administratie nog niet bekend"] })] }), _jsx(Button, { size: "sm", variant: "outline", disabled: choosing !== null, onClick: () => handleChoose(ch.account_key), children: choosing === ch.account_key ? "Bezig…" : "Kiezen" })] }, ch.account_key))) })] })), actionIgnored(selection.ignored).length > 0 && (_jsxs("div", { children: [_jsx("div", { className: "text-xs font-medium text-muted-foreground", children: "Account vraagt om actie: koppel opnieuw met Twinfield" }), _jsx("ul", { className: "mt-1 space-y-0.5 text-xs text-muted-foreground", children: actionIgnored(selection.ignored).map((ig) => (_jsxs("li", { children: [_jsx("span", { className: "font-mono", children: ig.account_key }), " \u00B7 ", ig.reason] }, ig.account_key))) })] }))] })), hasAny && (_jsxs("div", { className: "space-y-3", children: [_jsx("h3", { className: "text-base font-semibold text-foreground", children: "Gekoppelde administratie" }), _jsx("div", { className: "space-y-3", children: connections.map((c) => (_jsx(ConnectionCard, { isAdmin: isAdmin, conn: c, onChanged: () => loadAll(), onDisconnected: async () => {
|
|
391
|
+
: "Niet verbonden" })] })] }), _jsx("p", { className: "text-sm text-muted-foreground", children: "E\u00E9n Twinfield-account per omgeving. Verbreek eerst de huidige koppeling om te wisselen naar een ander account." })] })] }), _jsx("button", { type: "button", onClick: () => setIsExpanded((v) => !v), className: "shrink-0 p-1 rounded-md hover:bg-muted transition-colors", "aria-label": isExpanded ? "Samenvouwen" : "Uitklappen", children: isExpanded ? (_jsx(ChevronUp, { className: "h-5 w-5 text-muted-foreground" })) : (_jsx(ChevronDown, { className: "h-5 w-5 text-muted-foreground" })) })] }), isExpanded && (_jsxs("div", { className: "space-y-6 p-6", children: [summary && _jsx(SummaryPanel, { summary: summary }), centralNotice && (_jsx("div", { className: "rounded-lg border bg-muted/30 px-4 py-3 text-sm text-muted-foreground", "aria-live": "polite", children: centralNotice })), !hasAny && !loading && (_jsxs("div", { className: "rounded-lg border border-dashed p-8 text-center space-y-4", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-base font-semibold text-foreground", children: "Nog niet verbonden met Twinfield" }), _jsx("p", { className: "text-sm text-muted-foreground mt-1", children: "Log in bij Twinfield en geef eenmalig toestemming. Wij bewaren de toegang veilig en verversen tokens automatisch." })] }), _jsxs(Button, { size: "lg", onClick: handleConnect, disabled: connecting, children: [_jsx(Link2, { className: "h-4 w-4 mr-2" }), connecting ? "Koppelen…" : "Verbind met Twinfield"] })] })), selection && (selection.needs_choice || selection.replaced || actionIgnored(selection.ignored).length > 0) && (_jsxs("div", { className: "rounded-lg border p-4 space-y-3 text-sm", children: [selection.replaced && (_jsxs("div", { className: "rounded-md border border-primary/30 bg-primary/5 px-3 py-2", children: ["Koppeling vervangen: ", _jsx("span", { className: "font-mono", children: selection.replaced.from }), " (", selection.replaced.reason, ") \u2192 ", _jsx("span", { className: "font-mono", children: selection.replaced.to }), "."] })), selection.needs_choice && (_jsxs("div", { className: "space-y-2", children: [_jsx("div", { className: "font-semibold text-foreground", children: "Kies Twinfield-account" }), _jsx("div", { className: "text-xs text-muted-foreground", children: "Er zijn meerdere bruikbare accounts. Kies welk account deze module gebruikt." }), _jsx("div", { className: "divide-y rounded-md border", children: selection.choices.map((ch) => (_jsxs("div", { className: "flex items-center justify-between gap-3 px-3 py-2", children: [_jsxs("div", { className: "min-w-0 text-xs", children: [isAdmin && _jsx("div", { className: "font-mono text-foreground", children: ch.account_key }), _jsxs("div", { className: "text-muted-foreground", children: [ch.environment === "live" ? "Live" : "Test", " \u00B7", " ", ch.company_name ?? ch.office_code ?? "administratie nog niet bekend"] })] }), _jsx(Button, { size: "sm", variant: "outline", disabled: choosing !== null, onClick: () => handleChoose(ch.account_key), children: choosing === ch.account_key ? "Bezig…" : "Kiezen" })] }, ch.account_key))) })] })), actionIgnored(selection.ignored).length > 0 && (_jsxs("div", { children: [_jsx("div", { className: "text-xs font-medium text-muted-foreground", children: "Account vraagt om actie: koppel opnieuw met Twinfield" }), _jsx("ul", { className: "mt-1 space-y-0.5 text-xs text-muted-foreground", children: actionIgnored(selection.ignored).map((ig) => (_jsxs("li", { children: [_jsx("span", { className: "font-mono", children: ig.account_key }), " \u00B7 ", ig.reason] }, ig.account_key))) })] }))] })), hasAny && (_jsxs("div", { className: "space-y-3", children: [_jsx("h3", { className: "text-base font-semibold text-foreground", children: "Gekoppelde administratie" }), _jsx("div", { className: "space-y-3", children: connections.map((c) => (_jsx(ConnectionCard, { isAdmin: isAdmin, conn: c, onChanged: () => loadAll(), onDisconnected: async () => {
|
|
369
392
|
// Optimistisch: verwijder rij direct uit UI en sla Central-sync over,
|
|
370
393
|
// anders wordt de rij vanuit Central Auth meteen opnieuw opgehaald.
|
|
371
394
|
setConnections((prev) => prev.filter((row) => row.id !== c.id));
|
|
372
395
|
await loadAll({ skipCentralSync: true });
|
|
373
|
-
} }, c.id))) })] })), connections.find((c) => c.connected) && (_jsx(SyncOverviewSection, { connectionId: connections.find((c) => c.connected).id, part: "defaults" })), anyConnected && pushQueue.length > 0 && (_jsxs("div", { className: "rounded-lg border p-4", children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsxs("div", { children: [_jsx("div", { className: "font-semibold text-foreground", children: "Uitgaande klantwijzigingen naar Twinfield" }), _jsx("div", { className: "text-sm text-muted-foreground mt-0.5", children: "De laatste 20 klanten die vanuit de klantenlijst naar Twinfield zijn gestuurd. Facturen staan hier niet in." })] }), _jsx(Button, { size: "sm", variant: "outline", onClick: () => void loadAll(), children: "Vernieuwen" })] }), _jsx("div", { className: "mt-3 divide-y rounded-md border", children: pushQueue.map((q) => {
|
|
396
|
+
} }, c.id))) })] })), connections.find((c) => c.connected) && (_jsx(SyncOverviewSection, { connectionId: connections.find((c) => c.connected).id, part: "defaults" })), anyConnected && pushQueue.length > 0 && (_jsxs("div", { className: "rounded-lg border p-4", children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsxs("div", { children: [_jsx("div", { className: "font-semibold text-foreground", children: "Uitgaande klantwijzigingen naar Twinfield" }), _jsx("div", { className: "text-sm text-muted-foreground mt-0.5", children: "De laatste 20 klanten die vanuit de klantenlijst naar Twinfield zijn gestuurd, met per klant de laatste verzending. Facturen staan hier niet in." })] }), _jsx(Button, { size: "sm", variant: "outline", onClick: () => void loadAll(), children: "Vernieuwen" })] }), _jsx("div", { className: "mt-3 divide-y rounded-md border", children: pushQueue.map((q) => {
|
|
374
397
|
// 'success' (deze module) en 'done' (Klanten-module) = afgehandeld.
|
|
375
398
|
const isDone = q.status === "success" || q.status === "done";
|
|
376
399
|
const isFailed = q.status === "dead" || q.status === "failed";
|
|
@@ -390,7 +413,7 @@ export function TwinfieldAuthSettingsCard({ defaultExpanded = true, } = {}) {
|
|
|
390
413
|
: q.status === "pending"
|
|
391
414
|
? "In wachtrij"
|
|
392
415
|
: q.status;
|
|
393
|
-
return (_jsxs("div", { className: "flex items-start justify-between gap-3 px-3 py-2", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Badge, { variant: variant, children: label }), isFailed && (_jsxs("span", { className: "text-xs text-muted-foreground", children: ["poging ", Math.min(Number(q.attempts ?? 0), TWINFIELD_PUSH_MAX_ATTEMPTS), "/", TWINFIELD_PUSH_MAX_ATTEMPTS] })), _jsx("span", { className: "text-sm text-foreground", children: q.customer_name ?? "Onbekende klant" }), q.external_id && (_jsxs("span", { className: "text-xs font-mono text-muted-foreground", children: ["debiteur ", q.external_id] }))] }), q.last_error && (_jsx("div", { className: "mt-1 text-xs text-muted-foreground break-all", children: q.last_error })), _jsx("div", { className: "mt-0.5 text-[11px] text-muted-foreground", children: relativeTime(q.updated_at ?? q.created_at) })] }), isFailed && (_jsx(Button, { size: "sm", variant: "outline", disabled: retrying === q.id, onClick: async () => {
|
|
416
|
+
return (_jsxs("div", { className: "flex items-start justify-between gap-3 px-3 py-2", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Badge, { variant: variant, children: label }), isFailed && (_jsxs("span", { className: "text-xs text-muted-foreground", children: ["poging ", Math.min(Number(q.attempts ?? 0), TWINFIELD_PUSH_MAX_ATTEMPTS), "/", TWINFIELD_PUSH_MAX_ATTEMPTS] })), _jsx("span", { className: "text-sm text-foreground", children: q.customer_name ?? "Onbekende klant" }), q.external_id && (_jsxs("span", { className: "text-xs font-mono text-muted-foreground", children: ["debiteur ", q.external_id] }))] }), q.last_error && (_jsx("div", { className: "mt-1 text-xs text-muted-foreground break-all", children: stripFieldPrefix(q.last_error) })), isAdmin && q.last_error && stripFieldPrefix(q.last_error) !== q.last_error && (_jsxs("details", { className: "mt-1 text-[11px] text-muted-foreground", children: [_jsx("summary", { className: "cursor-pointer", children: "Details voor beheer" }), _jsx("div", { className: "break-all font-mono", children: q.last_error })] })), Number(q.send_count ?? 1) > 1 && (_jsxs("div", { className: "mt-0.5 text-[11px] text-muted-foreground", children: [q.send_count, " keer verstuurd"] })), _jsx("div", { className: "mt-0.5 text-[11px] text-muted-foreground", children: relativeTime(q.updated_at ?? q.created_at) })] }), isFailed && (_jsx(Button, { size: "sm", variant: "outline", disabled: retrying === q.id, onClick: async () => {
|
|
394
417
|
setRetrying(q.id);
|
|
395
418
|
try {
|
|
396
419
|
await retryQueueItem({ data: { id: q.id } });
|