@flowselections/twinfield-authenticatie-module-15-06-2026 1.1.4 → 1.3.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/components/settings/TwinfieldAuthSettingsCard.d.ts +1 -1
- package/dist-lib/components/settings/TwinfieldAuthSettingsCard.d.ts.map +1 -1
- package/dist-lib/components/settings/TwinfieldAuthSettingsCard.js +228 -241
- package/dist-lib/index.d.ts +2 -1
- package/dist-lib/index.d.ts.map +1 -1
- package/dist-lib/index.js +3 -4
- package/dist-lib/integrations/supabase/auth-middleware.d.ts +9712 -4587
- package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.d.ts +9712 -4587
- package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.js +4 -2
- package/dist-lib/integrations/supabase/client.server.d.ts +9712 -4587
- package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/twinfield-admin.server.d.ts +9712 -4587
- package/dist-lib/integrations/supabase/twinfield-admin.server.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.d.ts +9878 -4600
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.js +59 -8
- package/dist-lib/lib/accounting/twinfield/actions.functions.d.ts +60338 -23940
- package/dist-lib/lib/accounting/twinfield/actions.functions.d.ts.map +1 -1
- package/dist-lib/lib/accounting/twinfield/actions.functions.js +145 -38
- package/dist-lib/lib/accounting/twinfield/client.server.d.ts +1 -5
- package/dist-lib/lib/accounting/twinfield/client.server.d.ts.map +1 -1
- package/dist-lib/lib/accounting/twinfield/conformance.functions.d.ts +9712 -4587
- package/dist-lib/lib/accounting/twinfield/conformance.functions.d.ts.map +1 -1
- package/dist-lib/lib/accounting/twinfield/conformance.functions.js +4 -16
- package/dist-lib/lib/accounting/twinfield/mapping.d.ts +25 -0
- package/dist-lib/lib/accounting/twinfield/mapping.d.ts.map +1 -1
- package/dist-lib/lib/accounting/twinfield/mapping.js +81 -22
- package/dist-lib/lib/accounting/twinfield/session.server.d.ts.map +1 -1
- package/dist-lib/lib/accounting/twinfield/session.server.js +6 -6
- package/dist-lib/lib/accounting/twinfield/writes.functions.d.ts +47683 -0
- package/dist-lib/lib/accounting/twinfield/writes.functions.d.ts.map +1 -0
- package/dist-lib/lib/accounting/twinfield/writes.functions.js +182 -0
- package/dist-lib/lib/central/central-accounts.server.d.ts +33 -0
- package/dist-lib/lib/central/central-accounts.server.d.ts.map +1 -0
- package/dist-lib/lib/central/central-accounts.server.js +217 -0
- package/dist-lib/lib/central/central-config.server.d.ts +9 -0
- package/dist-lib/lib/central/central-config.server.d.ts.map +1 -0
- package/dist-lib/lib/central/central-config.server.js +24 -0
- package/dist-lib/lib/central/central-token.server.d.ts +11 -0
- package/dist-lib/lib/central/central-token.server.d.ts.map +1 -0
- package/dist-lib/lib/central/central-token.server.js +44 -0
- package/dist-lib/lib/twinfield-adapter.functions.d.ts +22467 -12217
- package/dist-lib/lib/twinfield-adapter.functions.d.ts.map +1 -1
- package/dist-lib/lib/twinfield-sync.functions.d.ts +65503 -7559
- package/dist-lib/lib/twinfield-sync.functions.d.ts.map +1 -1
- package/dist-lib/lib/twinfield-sync.functions.js +52 -0
- package/dist-lib/lib/twinfield-sync.server.d.ts +94 -1
- package/dist-lib/lib/twinfield-sync.server.d.ts.map +1 -1
- package/dist-lib/lib/twinfield-sync.server.js +1035 -70
- package/dist-lib/lib/twinfield-token.server.d.ts +13 -28
- package/dist-lib/lib/twinfield-token.server.d.ts.map +1 -1
- package/dist-lib/lib/twinfield-token.server.js +70 -248
- package/dist-lib/lib/twinfield.functions.d.ts +132122 -1409
- package/dist-lib/lib/twinfield.functions.d.ts.map +1 -1
- package/dist-lib/lib/twinfield.functions.js +314 -207
- package/package.json +3 -3
- package/public/flowselections-assets/twinfield-authenticatie/twinfield-logo.svg +217 -217
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare function TwinfieldAuthSettingsCard({ defaultExpanded }?: {
|
|
1
|
+
export declare function TwinfieldAuthSettingsCard({ defaultExpanded, }?: {
|
|
2
2
|
defaultExpanded?: boolean;
|
|
3
3
|
}): import("react").JSX.Element;
|
|
4
4
|
//# sourceMappingURL=TwinfieldAuthSettingsCard.d.ts.map
|
|
@@ -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":"AAmTA,wBAAgB,yBAAyB,CAAC,EACxC,eAAsB,GACvB,GAAE;IACD,eAAe,CAAC,EAAE,OAAO,CAAC;CACtB,+BA6UL"}
|
|
@@ -1,22 +1,34 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useState } from "react";
|
|
3
3
|
import { useServerFn } from "@tanstack/react-start";
|
|
4
|
-
import { Card, Button,
|
|
5
|
-
import { ChevronUp, ChevronDown } from "lucide-react";
|
|
4
|
+
import { Card, Button, Badge, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, toast, } from "@flowselections/core";
|
|
5
|
+
import { ChevronUp, ChevronDown, Link2, Unlink } from "lucide-react";
|
|
6
6
|
const twinfieldLogo = "/flowselections-assets/twinfield-authenticatie/twinfield-logo.svg";
|
|
7
|
-
import {
|
|
7
|
+
import { startTwinfieldOAuth, syncTwinfieldCentralAccounts, listTwinfieldConnections, disconnectTwinfieldConnection, setTwinfieldConnectionOffice, listTwinfieldOfficesForConnection, } from "../../lib/twinfield.functions";
|
|
8
8
|
import { triggerTwinfieldSync, getTwinfieldSyncState, } from "../../lib/twinfield-sync.functions";
|
|
9
9
|
import { listTwinfieldPushQueue, retryTwinfieldQueueItem, } from "../../lib/twinfield-adapter.functions";
|
|
10
10
|
import { cn } from "../../lib/utils";
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
return (_jsx("div", { className: "flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground text-sm font-semibold", children: n }));
|
|
11
|
+
function hasTwinfieldReturnParams() {
|
|
12
|
+
if (typeof window === "undefined")
|
|
13
|
+
return false;
|
|
14
|
+
const params = new URLSearchParams(window.location.search);
|
|
15
|
+
return ["success", "code", "state", "accountKey", "account_key", "error"].some((key) => params.has(key));
|
|
17
16
|
}
|
|
18
|
-
function
|
|
19
|
-
|
|
17
|
+
function clearTwinfieldReturnParams() {
|
|
18
|
+
if (typeof window === "undefined")
|
|
19
|
+
return;
|
|
20
|
+
const url = new URL(window.location.href);
|
|
21
|
+
const keys = ["success", "code", "state", "accountKey", "account_key", "error"];
|
|
22
|
+
let changed = false;
|
|
23
|
+
for (const key of keys) {
|
|
24
|
+
if (url.searchParams.has(key)) {
|
|
25
|
+
url.searchParams.delete(key);
|
|
26
|
+
changed = true;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (changed) {
|
|
30
|
+
window.history.replaceState({}, "", `${url.pathname}${url.search}${url.hash}`);
|
|
31
|
+
}
|
|
20
32
|
}
|
|
21
33
|
function relativeTime(iso) {
|
|
22
34
|
if (!iso)
|
|
@@ -33,272 +45,247 @@ function relativeTime(iso) {
|
|
|
33
45
|
const d = Math.floor(hr / 24);
|
|
34
46
|
return `${d} dag${d === 1 ? "" : "en"} geleden`;
|
|
35
47
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const fetchStatus = useServerFn(getTwinfieldStatus);
|
|
41
|
-
const saveSettings = useServerFn(saveTwinfieldSettings);
|
|
42
|
-
const testConnection = useServerFn(testTwinfieldConnection);
|
|
43
|
-
const setEnv = useServerFn(setTwinfieldActiveEnvironment);
|
|
48
|
+
function ConnectionCard({ conn, onChanged, onDisconnected, }) {
|
|
49
|
+
const loadOffices = useServerFn(listTwinfieldOfficesForConnection);
|
|
50
|
+
const saveOffice = useServerFn(setTwinfieldConnectionOffice);
|
|
51
|
+
const disconnect = useServerFn(disconnectTwinfieldConnection);
|
|
44
52
|
const trigger = useServerFn(triggerTwinfieldSync);
|
|
45
|
-
const getState = useServerFn(getTwinfieldSyncState);
|
|
46
|
-
const fetchOffices = useServerFn(listTwinfieldOffices);
|
|
47
|
-
// Connection state
|
|
48
|
-
const [clientId, setClientId] = useState("");
|
|
49
|
-
const [clientSecret, setClientSecret] = useState("");
|
|
50
|
-
const [environment, setEnvironment] = useState("sandbox");
|
|
51
|
-
const [administrationCode, setAdministrationCode] = useState("");
|
|
52
|
-
const [loading, setLoading] = useState(true);
|
|
53
|
-
const [saving, setSaving] = useState(false);
|
|
54
|
-
const [savingAdmin, setSavingAdmin] = useState(false);
|
|
55
|
-
const [testing, setTesting] = useState(false);
|
|
56
|
-
const [connected, setConnected] = useState(false);
|
|
57
|
-
const [hasSecret, setHasSecret] = useState(false);
|
|
58
|
-
const [hasClientId, setHasClientId] = useState(false);
|
|
59
|
-
const [existingClientId, setExistingClientId] = useState("");
|
|
60
|
-
// Sync state
|
|
61
|
-
const [busy, setBusy] = useState(null);
|
|
62
|
-
const [counts, setCounts] = useState({ customers: 0, invoices: 0 });
|
|
63
|
-
const [states, setStates] = useState([]);
|
|
64
53
|
const [offices, setOffices] = useState([]);
|
|
65
|
-
const [
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
const [
|
|
69
|
-
const [
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
try {
|
|
81
|
-
const res = await fetchStatus();
|
|
82
|
-
setConnected(!!res.connected);
|
|
83
|
-
setHasSecret(!!res.has_client_secret);
|
|
84
|
-
setEnvironment(res.active_environment ?? "sandbox");
|
|
85
|
-
if (res.settings) {
|
|
86
|
-
const existing = res.settings.client_id ?? "";
|
|
87
|
-
setExistingClientId(existing);
|
|
88
|
-
setHasClientId(!!existing);
|
|
89
|
-
setClientId("");
|
|
90
|
-
setAdministrationCode(res.settings.administration_code ?? "");
|
|
54
|
+
const [loadingOffices, setLoadingOffices] = useState(false);
|
|
55
|
+
const [savingOffice, setSavingOffice] = useState(false);
|
|
56
|
+
const [optimisticOffice, setOptimisticOffice] = useState(null);
|
|
57
|
+
const [disconnecting, setDisconnecting] = useState(false);
|
|
58
|
+
const [busy, setBusy] = useState(null);
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
let cancelled = false;
|
|
61
|
+
(async () => {
|
|
62
|
+
setLoadingOffices(true);
|
|
63
|
+
try {
|
|
64
|
+
const res = await loadOffices({ data: { id: conn.id } });
|
|
65
|
+
if (!cancelled) {
|
|
66
|
+
const list = Array.isArray(res?.offices) ? res.offices : [];
|
|
67
|
+
setOffices(list);
|
|
68
|
+
}
|
|
91
69
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
setHasClientId(false);
|
|
95
|
-
setClientId("");
|
|
96
|
-
setAdministrationCode("");
|
|
70
|
+
catch {
|
|
71
|
+
/* silent */
|
|
97
72
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const res = await getState();
|
|
109
|
-
const safeStates = Array.isArray(res?.states) ? res.states : [];
|
|
110
|
-
setStates(safeStates);
|
|
111
|
-
setCounts(res?.counts ?? { customers: 0, invoices: 0 });
|
|
112
|
-
}
|
|
113
|
-
catch {
|
|
114
|
-
// silent
|
|
115
|
-
}
|
|
116
|
-
};
|
|
117
|
-
const loadOffices = async () => {
|
|
118
|
-
try {
|
|
119
|
-
const res = await fetchOffices();
|
|
120
|
-
const list = Array.isArray(res?.offices) ? res.offices : [];
|
|
121
|
-
setOffices(list);
|
|
122
|
-
// Auto-vullen wanneer er precies één administratie is en het veld leeg is
|
|
123
|
-
setAdministrationCode((cur) => !cur && list.length === 1 ? list[0].code : cur);
|
|
124
|
-
}
|
|
125
|
-
catch {
|
|
126
|
-
// silent — handmatig typen blijft mogelijk
|
|
127
|
-
}
|
|
128
|
-
};
|
|
129
|
-
useEffect(() => {
|
|
130
|
-
loadStatus();
|
|
131
|
-
loadSyncState();
|
|
132
|
-
loadPushQueue();
|
|
133
|
-
}, []);
|
|
73
|
+
finally {
|
|
74
|
+
if (!cancelled)
|
|
75
|
+
setLoadingOffices(false);
|
|
76
|
+
}
|
|
77
|
+
})();
|
|
78
|
+
return () => {
|
|
79
|
+
cancelled = true;
|
|
80
|
+
};
|
|
81
|
+
}, [conn.id, loadOffices]);
|
|
82
|
+
// Wanneer server-waarde overeenkomt met optimistische keuze: opruimen.
|
|
134
83
|
useEffect(() => {
|
|
135
|
-
if (
|
|
136
|
-
|
|
137
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
138
|
-
}, [connected]);
|
|
139
|
-
const handleSave = async () => {
|
|
140
|
-
const effectiveClientId = clientId.trim() || existingClientId;
|
|
141
|
-
if (!effectiveClientId) {
|
|
142
|
-
toast.error("Client ID is verplicht");
|
|
143
|
-
return;
|
|
84
|
+
if (optimisticOffice && conn.office_code === optimisticOffice.code) {
|
|
85
|
+
setOptimisticOffice(null);
|
|
144
86
|
}
|
|
145
|
-
|
|
87
|
+
}, [conn.office_code, optimisticOffice]);
|
|
88
|
+
const handleOfficeChange = async (code) => {
|
|
89
|
+
const picked = offices.find((o) => o.code === code);
|
|
90
|
+
setOptimisticOffice({ code, name: picked?.name ?? null });
|
|
91
|
+
setSavingOffice(true);
|
|
146
92
|
try {
|
|
147
|
-
await
|
|
148
|
-
data: {
|
|
149
|
-
client_id: effectiveClientId,
|
|
150
|
-
client_secret: clientSecret || undefined,
|
|
151
|
-
environment,
|
|
152
|
-
administration_code: administrationCode || undefined,
|
|
153
|
-
},
|
|
93
|
+
await saveOffice({
|
|
94
|
+
data: { id: conn.id, office_code: code, company_name: picked?.name ?? null },
|
|
154
95
|
});
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
toast.success("Wijzigingen opgeslagen.");
|
|
158
|
-
await loadStatus();
|
|
96
|
+
toast.success("Administratie opgeslagen.");
|
|
97
|
+
await onChanged();
|
|
159
98
|
}
|
|
160
99
|
catch (e) {
|
|
161
|
-
|
|
162
|
-
toast.error(
|
|
100
|
+
setOptimisticOffice(null);
|
|
101
|
+
toast.error(e?.message ?? "Opslaan mislukt.");
|
|
163
102
|
}
|
|
164
103
|
finally {
|
|
165
|
-
|
|
104
|
+
setSavingOffice(false);
|
|
166
105
|
}
|
|
167
106
|
};
|
|
168
|
-
const
|
|
169
|
-
if (!
|
|
170
|
-
toast.error("Vul eerst je verbindingsgegevens in.");
|
|
107
|
+
const handleDisconnect = async () => {
|
|
108
|
+
if (!confirm("Weet je zeker dat je deze Twinfield-koppeling wilt verbreken?"))
|
|
171
109
|
return;
|
|
172
|
-
|
|
173
|
-
setSavingAdmin(true);
|
|
110
|
+
setDisconnecting(true);
|
|
174
111
|
try {
|
|
175
|
-
await
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
await loadStatus();
|
|
112
|
+
const res = (await disconnect({ data: { id: conn.id } }));
|
|
113
|
+
if (res?.centralRevokeFailed) {
|
|
114
|
+
toast.warning(`Lokaal verbroken, maar Central Auth kon niet worden ingetrokken: ${res.centralRevokeMessage ?? "onbekende fout"}. Controleer Central Auth.`);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
toast.success("Verbinding verbroken.");
|
|
118
|
+
}
|
|
119
|
+
await onDisconnected();
|
|
184
120
|
}
|
|
185
121
|
catch (e) {
|
|
186
|
-
|
|
187
|
-
toast.error(msg && msg.length < 120 ? msg : "Opslaan mislukt.");
|
|
122
|
+
toast.error(e?.message ?? "Verbreken mislukt.");
|
|
188
123
|
}
|
|
189
124
|
finally {
|
|
190
|
-
|
|
125
|
+
setDisconnecting(false);
|
|
191
126
|
}
|
|
192
127
|
};
|
|
193
|
-
const
|
|
194
|
-
|
|
128
|
+
const runSync = async (entity) => {
|
|
129
|
+
setBusy(entity);
|
|
195
130
|
try {
|
|
196
|
-
const res = await
|
|
197
|
-
if (res.ok)
|
|
198
|
-
toast.success(res.
|
|
199
|
-
|
|
200
|
-
|
|
131
|
+
const res = await trigger({ data: { entity_type: entity } });
|
|
132
|
+
if (res.ok) {
|
|
133
|
+
toast.success(`${entity === "customers" ? "Debiteuren" : "Verkoopfacturen"} bijgewerkt (${res.upserted} rij(en)).`);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
toast.error(`Bijwerken mislukt: ${res.error ?? "onbekende fout"}`);
|
|
137
|
+
}
|
|
138
|
+
await onChanged();
|
|
201
139
|
}
|
|
202
|
-
catch {
|
|
203
|
-
toast.error(
|
|
140
|
+
catch (e) {
|
|
141
|
+
toast.error(e?.message ?? "Bijwerken mislukt.");
|
|
204
142
|
}
|
|
205
143
|
finally {
|
|
206
|
-
|
|
144
|
+
setBusy(null);
|
|
207
145
|
}
|
|
208
146
|
};
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
147
|
+
const effectiveOfficeCode = conn.office_code ?? optimisticOffice?.code ?? null;
|
|
148
|
+
const effectiveOfficeName = conn.company_name ?? optimisticOffice?.name ?? null;
|
|
149
|
+
const displayName = effectiveOfficeName ?? effectiveOfficeCode ?? conn.company_code ?? "Nog geen administratie gekozen";
|
|
150
|
+
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: conn.connected ? "default" : "secondary", className: "font-normal", children: conn.connected ? "Verbonden" : "Onvolledig" }), _jsx(Badge, { variant: "outline", className: "font-normal", children: conn.environment === "sandbox" ? "Test" : "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." })), conn.connected_at && (_jsxs("span", { className: "ml-2", children: ["\u00B7 gekoppeld ", relativeTime(conn.connected_at)] }))] })] }), _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: (() => {
|
|
151
|
+
const selected = offices.find((o) => o.code === effectiveOfficeCode);
|
|
152
|
+
if (!selected)
|
|
153
|
+
return null;
|
|
154
|
+
return (_jsxs("span", { className: "flex items-baseline gap-2 truncate", children: [_jsx("span", { className: "truncate", children: selected.name ?? selected.code }), _jsx("span", { className: "text-[11px] text-muted-foreground font-mono", children: selected.code })] }));
|
|
155
|
+
})() }) }), _jsx(SelectContent, { children: offices.map((o) => (_jsx(SelectItem, { value: o.code, children: _jsxs("div", { className: "flex flex-col", children: [_jsx("span", { children: o.name ?? o.code }), _jsx("span", { className: "text-[11px] text-muted-foreground font-mono", children: o.code })] }) }, o.code))) })] })) : (_jsx("div", { className: "text-xs text-muted-foreground", children: loadingOffices ? "Administraties ophalen…" : "Geen administraties gevonden." }))] }), effectiveOfficeCode && (_jsxs("div", { className: "flex flex-wrap gap-2 pt-1", children: [_jsx(Button, { size: "sm", variant: "outline", onClick: () => runSync("customers"), disabled: busy === "customers", children: busy === "customers" ? "Debiteuren…" : "Debiteuren bijwerken" }), _jsx(Button, { size: "sm", variant: "outline", onClick: () => runSync("invoices"), disabled: busy === "invoices", children: busy === "invoices" ? "Facturen…" : "Facturen bijwerken" })] }))] }));
|
|
156
|
+
}
|
|
157
|
+
export function TwinfieldAuthSettingsCard({ defaultExpanded = true, } = {}) {
|
|
158
|
+
const startOAuth = useServerFn(startTwinfieldOAuth);
|
|
159
|
+
const syncCentralAccounts = useServerFn(syncTwinfieldCentralAccounts);
|
|
160
|
+
const listConnections = useServerFn(listTwinfieldConnections);
|
|
161
|
+
const getSyncState = useServerFn(getTwinfieldSyncState);
|
|
162
|
+
const fetchPushQueue = useServerFn(listTwinfieldPushQueue);
|
|
163
|
+
const retryQueueItem = useServerFn(retryTwinfieldQueueItem);
|
|
164
|
+
const [connections, setConnections] = useState([]);
|
|
165
|
+
const [loading, setLoading] = useState(true);
|
|
166
|
+
const [checkingCentral, setCheckingCentral] = useState(false);
|
|
167
|
+
const [centralNotice, setCentralNotice] = useState(null);
|
|
168
|
+
const [connecting, setConnecting] = useState(false);
|
|
169
|
+
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
|
|
170
|
+
const [syncStates, setSyncStates] = useState([]);
|
|
171
|
+
const [counts, setCounts] = useState({
|
|
172
|
+
customers: 0,
|
|
173
|
+
invoices: 0,
|
|
174
|
+
});
|
|
175
|
+
const [pushQueue, setPushQueue] = useState([]);
|
|
176
|
+
const [retrying, setRetrying] = useState(null);
|
|
177
|
+
const loadAll = async (opts = {}) => {
|
|
178
|
+
setLoading(true);
|
|
179
|
+
if (!opts.skipCentralSync)
|
|
180
|
+
setCheckingCentral(true);
|
|
181
|
+
setCentralNotice(null);
|
|
182
|
+
let syncResult = null;
|
|
213
183
|
try {
|
|
214
|
-
|
|
215
|
-
|
|
184
|
+
if (!opts.skipCentralSync) {
|
|
185
|
+
try {
|
|
186
|
+
syncResult = await syncCentralAccounts({ data: undefined });
|
|
187
|
+
}
|
|
188
|
+
catch (e) {
|
|
189
|
+
const message = e?.message ?? "Central Auth synchronisatie mislukt.";
|
|
190
|
+
setCentralNotice(message);
|
|
191
|
+
if (opts.afterOAuthReturn)
|
|
192
|
+
toast.error(message);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const [connRes, stateRes, queueRes] = await Promise.all([
|
|
196
|
+
listConnections(),
|
|
197
|
+
getSyncState().catch(() => null),
|
|
198
|
+
fetchPushQueue().catch(() => null),
|
|
199
|
+
]);
|
|
200
|
+
const list = Array.isArray(connRes?.connections) ? connRes.connections : [];
|
|
201
|
+
setConnections(list);
|
|
202
|
+
if (opts.afterOAuthReturn) {
|
|
203
|
+
if (list.length > 0) {
|
|
204
|
+
toast.success("Twinfield-koppeling bijgewerkt.");
|
|
205
|
+
}
|
|
206
|
+
else if (syncResult && (syncResult.received ?? 0) === 0) {
|
|
207
|
+
setCentralNotice("Twinfield heeft toestemming geregistreerd, maar Central Auth geeft nog geen account terug aan deze module.");
|
|
208
|
+
}
|
|
209
|
+
clearTwinfieldReturnParams();
|
|
210
|
+
}
|
|
211
|
+
else if (syncResult && syncResult.ok === false && syncResult.error) {
|
|
212
|
+
setCentralNotice(syncResult.error);
|
|
213
|
+
}
|
|
214
|
+
if (stateRes) {
|
|
215
|
+
setSyncStates(Array.isArray(stateRes.states) ? stateRes.states : []);
|
|
216
|
+
setCounts(stateRes.counts ?? { customers: 0, invoices: 0 });
|
|
217
|
+
}
|
|
218
|
+
if (queueRes) {
|
|
219
|
+
setPushQueue(queueRes.items ?? []);
|
|
220
|
+
}
|
|
216
221
|
}
|
|
217
222
|
catch {
|
|
218
|
-
toast.error("
|
|
223
|
+
toast.error("Kon Twinfield-status niet laden.");
|
|
224
|
+
}
|
|
225
|
+
finally {
|
|
226
|
+
setCheckingCentral(false);
|
|
227
|
+
setLoading(false);
|
|
219
228
|
}
|
|
220
229
|
};
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
230
|
+
useEffect(() => {
|
|
231
|
+
loadAll({ afterOAuthReturn: hasTwinfieldReturnParams() });
|
|
232
|
+
// Achtergrond-polling elke 30s zonder spinner, zodat de UI live blijft.
|
|
233
|
+
const iv = setInterval(() => {
|
|
234
|
+
loadAll({ skipCentralSync: true }).catch(() => { });
|
|
235
|
+
}, 30000);
|
|
236
|
+
return () => clearInterval(iv);
|
|
237
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
238
|
+
}, []);
|
|
239
|
+
const handleConnect = async () => {
|
|
240
|
+
setConnecting(true);
|
|
228
241
|
try {
|
|
229
|
-
const res = await
|
|
230
|
-
if (res
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
await loadSyncState();
|
|
242
|
+
const res = await startOAuth({ data: undefined });
|
|
243
|
+
if (!res?.url)
|
|
244
|
+
throw new Error("Kon geen koppelingsverzoek starten.");
|
|
245
|
+
// Same-tab redirect: Twinfield stuurt via callback terug naar de app
|
|
246
|
+
window.location.href = res.url;
|
|
235
247
|
}
|
|
236
248
|
catch (e) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}
|
|
240
|
-
finally {
|
|
241
|
-
setBusy(null);
|
|
249
|
+
toast.error(e?.message ?? "Koppelen niet gelukt.");
|
|
250
|
+
setConnecting(false);
|
|
242
251
|
}
|
|
243
252
|
};
|
|
244
|
-
const
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
})()] }, item.key));
|
|
283
|
-
}) }), connected && pushQueue.length > 0 && (_jsxs("div", { className: "mt-6 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 facturen (push-wachtrij)" }), _jsx("div", { className: "text-sm text-muted-foreground mt-0.5", children: "Laatste 20 facturen die vanuit de facturatiemodule naar Twinfield zijn gestuurd." })] }), _jsx(Button, { size: "sm", variant: "outline", onClick: loadPushQueue, children: "Vernieuwen" })] }), _jsx("div", { className: "mt-3 divide-y rounded-md border", children: pushQueue.map((q) => {
|
|
284
|
-
const variant = q.status === "done"
|
|
285
|
-
? "default"
|
|
286
|
-
: q.status === "dead" || q.status === "error"
|
|
287
|
-
? "destructive"
|
|
288
|
-
: "secondary";
|
|
289
|
-
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: q.status }), _jsxs("span", { className: "text-xs text-muted-foreground", children: ["poging ", q.attempts, "/5"] }), q.external_id && (_jsxs("span", { className: "text-xs font-mono text-foreground", children: ["#", 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) })] }), (q.status === "error" || q.status === "dead") && (_jsx(Button, { size: "sm", variant: "outline", disabled: retrying === q.id, onClick: async () => {
|
|
290
|
-
setRetrying(q.id);
|
|
291
|
-
try {
|
|
292
|
-
await retryQueueItem({ data: { id: q.id } });
|
|
293
|
-
toast.success("Opnieuw in de wachtrij geplaatst.");
|
|
294
|
-
await loadPushQueue();
|
|
295
|
-
}
|
|
296
|
-
catch (e) {
|
|
297
|
-
toast.error(e?.message ?? "Opnieuw proberen mislukt");
|
|
298
|
-
}
|
|
299
|
-
finally {
|
|
300
|
-
setRetrying(null);
|
|
301
|
-
}
|
|
302
|
-
}, children: retrying === q.id ? "Bezig…" : "Opnieuw" }))] }, q.id));
|
|
303
|
-
}) })] }))] })] })), isExpanded && (_jsx("div", { className: "border-t px-6 py-4 text-center", children: _jsx("a", { href: "https://accounting.twinfield.com/webservices/documentation/", target: "_blank", rel: "noreferrer", className: "text-sm text-primary hover:underline", children: "Waar vind ik deze gegevens in Twinfield?" }) }))] }));
|
|
253
|
+
const hasAny = connections.length > 0;
|
|
254
|
+
const anyConnected = connections.some((c) => c.connected);
|
|
255
|
+
const statusLoading = loading || checkingCentral;
|
|
256
|
+
const stateFor = (e) => syncStates.find((s) => s.entity_type === e);
|
|
257
|
+
return (_jsxs(Card, { className: "overflow-hidden", children: [_jsxs("div", { className: cn("flex items-start justify-between gap-4 p-6", isExpanded && "border-b"), children: [_jsxs("div", { className: "flex items-start gap-4", children: [_jsx("div", { className: "flex h-11 w-11 shrink-0 items-center justify-center rounded-lg bg-muted p-2", children: _jsx("img", { src: twinfieldLogo, alt: "Twinfield", className: "h-full w-full object-contain" }) }), _jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [_jsx("h2", { className: "text-lg font-semibold text-foreground", children: "Twinfield koppeling" }), _jsxs("span", { className: "inline-flex items-center gap-1.5 text-sm", children: [_jsx("span", { className: `h-2 w-2 rounded-full ${anyConnected ? "bg-green-500" : "bg-muted-foreground/40"}` }), _jsx("span", { className: anyConnected ? "text-foreground" : "text-muted-foreground", children: statusLoading
|
|
258
|
+
? "Koppeling controleren…"
|
|
259
|
+
: anyConnected
|
|
260
|
+
? "Verbonden"
|
|
261
|
+
: "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: [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"] })] })), 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, { conn: c, onChanged: () => loadAll(), onDisconnected: async () => {
|
|
262
|
+
// Optimistisch: verwijder rij direct uit UI en sla Central-sync over,
|
|
263
|
+
// anders wordt de rij vanuit Central Auth meteen opnieuw opgehaald.
|
|
264
|
+
setConnections((prev) => prev.filter((row) => row.id !== c.id));
|
|
265
|
+
await loadAll({ skipCentralSync: true });
|
|
266
|
+
} }, c.id))) })] })), anyConnected && (_jsxs("div", { className: "rounded-lg border p-4 space-y-3", children: [_jsxs("div", { children: [_jsx("div", { className: "font-semibold text-foreground", children: "Automatische sync" }), _jsx("div", { className: "text-sm text-muted-foreground mt-0.5", children: "Debiteuren worden elke 15 minuten en verkoopfacturen elke 10 minuten automatisch bijgewerkt. Incomplete records worden elke 5 minuten aangevuld. Deze weergave ververst zichzelf elke 30 seconden." })] }), _jsx("div", { className: "grid grid-cols-2 gap-3", children: ["customers", "invoices"].map((k) => {
|
|
267
|
+
const s = stateFor(k);
|
|
268
|
+
const count = k === "customers" ? counts.customers : counts.invoices;
|
|
269
|
+
return (_jsxs("div", { className: "rounded-md border bg-muted/30 px-3 py-2", children: [_jsx("div", { className: "text-xs text-muted-foreground", children: k === "customers" ? "Debiteuren" : "Verkoopfacturen" }), _jsxs("div", { className: "text-sm font-medium text-foreground mt-0.5", children: [count, " \u00B7 laatst ", relativeTime(s?.last_synced_at ?? null)] }), s?.last_error && (_jsx("div", { className: "text-[11px] text-amber-700 mt-1 break-all", children: s.last_error }))] }, k));
|
|
270
|
+
}) })] })), 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 facturen (push-wachtrij)" }), _jsx("div", { className: "text-sm text-muted-foreground mt-0.5", children: "Laatste 20 facturen die naar Twinfield zijn gestuurd." })] }), _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) => {
|
|
271
|
+
const variant = q.status === "done"
|
|
272
|
+
? "default"
|
|
273
|
+
: q.status === "dead" || q.status === "error"
|
|
274
|
+
? "destructive"
|
|
275
|
+
: "secondary";
|
|
276
|
+
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: q.status }), _jsxs("span", { className: "text-xs text-muted-foreground", children: ["poging ", q.attempts, "/5"] }), q.external_id && (_jsxs("span", { className: "text-xs font-mono text-foreground", children: ["#", 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) })] }), (q.status === "error" || q.status === "dead") && (_jsx(Button, { size: "sm", variant: "outline", disabled: retrying === q.id, onClick: async () => {
|
|
277
|
+
setRetrying(q.id);
|
|
278
|
+
try {
|
|
279
|
+
await retryQueueItem({ data: { id: q.id } });
|
|
280
|
+
toast.success("Opnieuw in de wachtrij geplaatst.");
|
|
281
|
+
await loadAll();
|
|
282
|
+
}
|
|
283
|
+
catch (e) {
|
|
284
|
+
toast.error(e?.message ?? "Opnieuw proberen mislukt");
|
|
285
|
+
}
|
|
286
|
+
finally {
|
|
287
|
+
setRetrying(null);
|
|
288
|
+
}
|
|
289
|
+
}, children: retrying === q.id ? "Bezig…" : "Opnieuw" }))] }, q.id));
|
|
290
|
+
}) })] }))] }))] }));
|
|
304
291
|
}
|
package/dist-lib/index.d.ts
CHANGED
|
@@ -4,8 +4,9 @@ export * from './components/TemplatePage';
|
|
|
4
4
|
export * from './components/TemplateModulePage';
|
|
5
5
|
export { TemplateModuleLogo } from './components/TemplateModuleLogo';
|
|
6
6
|
export { TemplateModulePage } from "./components/TemplateModulePage";
|
|
7
|
-
export { downloadTwinfieldInvoicePdf, resendTwinfieldInvoice, sendPaymentReminder, duplicateTwinfieldInvoice, } from "./lib/accounting/twinfield/actions.functions";
|
|
7
|
+
export { downloadTwinfieldInvoicePdf, downloadTwinfieldInvoicePdfsBulk, resendTwinfieldInvoice, sendPaymentReminder, duplicateTwinfieldInvoice, } from "./lib/accounting/twinfield/actions.functions";
|
|
8
8
|
export { getTwinfieldConformance } from "./lib/accounting/twinfield/conformance.functions";
|
|
9
|
+
export { createTwinfieldCustomer, createTwinfieldInvoice, checkTwinfieldWritePermissions, } from "./lib/accounting/twinfield/writes.functions";
|
|
9
10
|
export { TwinfieldConformanceCard } from "./components/settings/TwinfieldConformanceCard";
|
|
10
11
|
export declare const twinfieldAuthenticatieModule: FlowModule;
|
|
11
12
|
export declare const myModule: FlowModule;
|
package/dist-lib/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAEvD,cAAc,kBAAkB,CAAC;AACjC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,iCAAiC,CAAC;AAChD,OAAO,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AAErE,OAAO,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AAKrE,OAAO,EACL,2BAA2B,EAC3B,sBAAsB,EACtB,mBAAmB,EACnB,yBAAyB,GAC1B,MAAM,8CAA8C,CAAC;AACtD,OAAO,EAAE,uBAAuB,EAAE,MAAM,kDAAkD,CAAC;AAC3F,OAAO,EAAE,wBAAwB,EAAE,MAAM,gDAAgD,CAAC;AAE1F,eAAO,MAAM,4BAA4B,EAAE,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAEvD,cAAc,kBAAkB,CAAC;AACjC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,iCAAiC,CAAC;AAChD,OAAO,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AAErE,OAAO,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AAKrE,OAAO,EACL,2BAA2B,EAC3B,gCAAgC,EAChC,sBAAsB,EACtB,mBAAmB,EACnB,yBAAyB,GAC1B,MAAM,8CAA8C,CAAC;AACtD,OAAO,EAAE,uBAAuB,EAAE,MAAM,kDAAkD,CAAC;AAC3F,OAAO,EACL,uBAAuB,EACvB,sBAAsB,EACtB,8BAA8B,GAC/B,MAAM,6CAA6C,CAAC;AACrD,OAAO,EAAE,wBAAwB,EAAE,MAAM,gDAAgD,CAAC;AAE1F,eAAO,MAAM,4BAA4B,EAAE,UAY1C,CAAC;AAEF,eAAO,MAAM,QAAQ,YAA+B,CAAC;AACrD,eAAO,MAAM,4BAA4B,YAA+B,CAAC"}
|