@flowselections/floriday-klanten-module 1.0.36 → 1.0.37
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/klanten/BulkStickerUploadDialog.d.ts.map +1 -1
- package/dist-lib/components/klanten/BulkStickerUploadDialog.js +30 -8
- package/dist-lib/components/klanten/CustomerAssortmentTab.d.ts.map +1 -1
- package/dist-lib/components/klanten/CustomerAssortmentTab.js +48 -10
- package/dist-lib/integrations/supabase/auth-middleware.d.ts +129 -0
- package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.d.ts +129 -0
- package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.server.d.ts +129 -0
- package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.d.ts +129 -0
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/lib/accounting/twinfield/client.server.d.ts +7 -8
- package/dist-lib/lib/accounting/twinfield/client.server.d.ts.map +1 -1
- package/dist-lib/lib/accounting/twinfield/client.server.js +147 -78
- package/dist-lib/lib/accounting/twinfield/worker.functions.d.ts +387 -0
- package/dist-lib/lib/accounting/twinfield/worker.functions.d.ts.map +1 -1
- package/dist-lib/lib/accounting/twinfield/worker.server.d.ts.map +1 -1
- package/dist-lib/lib/accounting/twinfield/worker.server.js +17 -0
- package/dist-lib/lib/accounting.functions.d.ts +387 -0
- package/dist-lib/lib/accounting.functions.d.ts.map +1 -1
- package/dist-lib/lib/customer-analytics.functions.d.ts +129 -0
- package/dist-lib/lib/customer-analytics.functions.d.ts.map +1 -1
- package/dist-lib/lib/customer-assortment.functions.d.ts +132 -3
- package/dist-lib/lib/customer-assortment.functions.d.ts.map +1 -1
- package/dist-lib/lib/customer-assortment.functions.js +12 -0
- package/dist-lib/lib/customer-notes.functions.d.ts +258 -0
- package/dist-lib/lib/customer-notes.functions.d.ts.map +1 -1
- package/dist-lib/lib/customer-revenue.functions.d.ts +129 -0
- package/dist-lib/lib/customer-revenue.functions.d.ts.map +1 -1
- package/dist-lib/lib/customer-stickers.functions.d.ts +1165 -0
- package/dist-lib/lib/customer-stickers.functions.d.ts.map +1 -1
- package/dist-lib/lib/customer-stickers.functions.js +122 -75
- package/dist-lib/lib/customers.functions.d.ts +1161 -0
- package/dist-lib/lib/customers.functions.d.ts.map +1 -1
- package/dist-lib/lib/delivery-locations.functions.d.ts +645 -0
- package/dist-lib/lib/delivery-locations.functions.d.ts.map +1 -1
- package/dist-lib/lib/external-customers.functions.d.ts +516 -0
- package/dist-lib/lib/external-customers.functions.d.ts.map +1 -1
- package/dist-lib/lib/floriday-assortment.functions.d.ts +129 -0
- package/dist-lib/lib/floriday-assortment.functions.d.ts.map +1 -1
- package/dist-lib/lib/floriday-discounts.functions.d.ts +258 -0
- package/dist-lib/lib/floriday-discounts.functions.d.ts.map +1 -1
- package/dist-lib/lib/floriday-price-import.functions.d.ts +258 -0
- package/dist-lib/lib/floriday-price-import.functions.d.ts.map +1 -1
- package/dist-lib/lib/floriday-sticker-import.functions.d.ts +27323 -0
- package/dist-lib/lib/floriday-sticker-import.functions.d.ts.map +1 -0
- package/dist-lib/lib/floriday-sticker-import.functions.js +307 -0
- package/dist-lib/lib/floriday-trade-items.functions.d.ts +129 -0
- package/dist-lib/lib/floriday-trade-items.functions.d.ts.map +1 -1
- package/dist-lib/lib/local-discounts.functions.d.ts +903 -0
- package/dist-lib/lib/local-discounts.functions.d.ts.map +1 -1
- package/dist-lib/lib/logistics-events.functions.d.ts +129 -0
- package/dist-lib/lib/logistics-events.functions.d.ts.map +1 -1
- package/dist-lib/lib/logistics-strategy.functions.d.ts +516 -0
- package/dist-lib/lib/logistics-strategy.functions.d.ts.map +1 -1
- package/dist-lib/lib/overdue-invoices.functions.d.ts +129 -0
- package/dist-lib/lib/overdue-invoices.functions.d.ts.map +1 -1
- package/dist-lib/lib/price-groups-local.functions.d.ts +1677 -0
- package/dist-lib/lib/price-groups-local.functions.d.ts.map +1 -1
- package/dist-lib/lib/price-groups.functions.d.ts +129 -0
- package/dist-lib/lib/price-groups.functions.d.ts.map +1 -1
- package/dist-lib/lib/twinfield-adopt.functions.d.ts +258 -0
- package/dist-lib/lib/twinfield-adopt.functions.d.ts.map +1 -1
- package/dist-lib/lib/twinfield-ledger.functions.d.ts +129 -0
- package/dist-lib/lib/twinfield-ledger.functions.d.ts.map +1 -1
- package/package.json +2 -2
|
@@ -20,7 +20,7 @@ export const listCustomerStickers = createServerFn({ method: "POST" })
|
|
|
20
20
|
const sb = context.supabase;
|
|
21
21
|
const { data: rows, error } = await sb
|
|
22
22
|
.from("customer_product_stickers")
|
|
23
|
-
.select("trade_item_key, file_path, file_name, mime_type, file_size, updated_at")
|
|
23
|
+
.select("trade_item_key, file_path, file_name, mime_type, file_size, updated_at, source, width_mm, height_mm")
|
|
24
24
|
.eq("customer_key", data.customerKey);
|
|
25
25
|
if (error)
|
|
26
26
|
throw new Error(error.message);
|
|
@@ -34,6 +34,9 @@ export const listCustomerStickers = createServerFn({ method: "POST" })
|
|
|
34
34
|
file_size: r.file_size ?? null,
|
|
35
35
|
updated_at: r.updated_at,
|
|
36
36
|
url: await signed(sb, r.file_path),
|
|
37
|
+
source: r.source === "floriday" ? "floriday" : "manual",
|
|
38
|
+
width_mm: r.width_mm != null ? Number(r.width_mm) : null,
|
|
39
|
+
height_mm: r.height_mm != null ? Number(r.height_mm) : null,
|
|
37
40
|
});
|
|
38
41
|
}
|
|
39
42
|
const { data: fmt } = await sb
|
|
@@ -82,6 +85,9 @@ export const confirmStickerUpload = createServerFn({ method: "POST" })
|
|
|
82
85
|
mime_type: data.mimeType,
|
|
83
86
|
file_size: data.fileSize ?? null,
|
|
84
87
|
uploaded_by: context.userId ?? null,
|
|
88
|
+
source: "manual",
|
|
89
|
+
floriday_sticker_id: null,
|
|
90
|
+
content_hash: null,
|
|
85
91
|
updated_at: new Date().toISOString(),
|
|
86
92
|
}, { onConflict: "customer_key,trade_item_key" });
|
|
87
93
|
if (error)
|
|
@@ -275,102 +281,140 @@ export const suggestStickerMatches = createServerFn({ method: "POST" })
|
|
|
275
281
|
(p.vbn ? ` | VBN: ${p.vbn}` : "") +
|
|
276
282
|
(p.botanical ? ` | botanisch: ${p.botanical}` : ""))
|
|
277
283
|
.join("\n");
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
284
|
+
let aiError = null;
|
|
285
|
+
for (let i = 0; i < remaining.length; i += 4) {
|
|
286
|
+
const batch = remaining.slice(i, i + 4);
|
|
287
|
+
const parts = [
|
|
288
|
+
{
|
|
289
|
+
text: "Je koppelt stickerbestanden aan producten uit het assortiment van een bloemenhandel.\n" +
|
|
290
|
+
"Producten:\n" +
|
|
291
|
+
productList +
|
|
292
|
+
"\n\nHieronder volgen stickers. Voor elke sticker krijg je eerst de bestandsnaam en daarna (indien beschikbaar) een afbeelding van de sticker.\n" +
|
|
293
|
+
"Geef per sticker het best passende product terug. Kies alleen een product als je het redelijk zeker weet; anders tradeItemKey = null.\n" +
|
|
294
|
+
'Antwoord uitsluitend als JSON-array: [{"fileId":"...","tradeItemKey":"..."|null,"confidence":0.0-1.0,"reason":"korte uitleg in het Nederlands"}]',
|
|
295
|
+
},
|
|
296
|
+
];
|
|
297
|
+
for (const f of batch) {
|
|
298
|
+
parts.push({ text: `Sticker fileId=${f.fileId} bestandsnaam="${f.fileName}"` });
|
|
299
|
+
const t = f.thumbnail ?? "";
|
|
300
|
+
const m = /^data:([^;,]+);base64,(.+)$/.exec(t);
|
|
301
|
+
if (m)
|
|
302
|
+
parts.push({ inline_data: { mime_type: m[1], data: m[2] } });
|
|
303
|
+
}
|
|
304
|
+
const url = model.providerSlug === "gemini"
|
|
305
|
+
? `${model.baseUrl}/v1beta/models/${model.modelSlug}:generateContent`
|
|
306
|
+
: null;
|
|
307
|
+
if (!url) {
|
|
308
|
+
throw new Error(`AI-aanbieder "${model.providerSlug}" wordt hier nog niet ondersteund. Kies een Gemini-model bij AI-authenticatie.`);
|
|
309
|
+
}
|
|
310
|
+
let json = null;
|
|
311
|
+
let lastMessage = "AI-herkenning mislukt.";
|
|
312
|
+
for (let attempt = 0; attempt < 4; attempt++) {
|
|
313
|
+
const started = Date.now();
|
|
314
|
+
let res;
|
|
315
|
+
try {
|
|
316
|
+
res = await fetch(url, {
|
|
317
|
+
method: "POST",
|
|
318
|
+
headers: { "Content-Type": "application/json", "x-goog-api-key": model.apiKey },
|
|
319
|
+
body: JSON.stringify({
|
|
320
|
+
contents: [{ role: "user", parts }],
|
|
321
|
+
generationConfig: { responseMimeType: "application/json", temperature: 0 },
|
|
322
|
+
}),
|
|
323
|
+
});
|
|
304
324
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
headers: { "Content-Type": "application/json", "x-goog-api-key": model.apiKey },
|
|
308
|
-
body: JSON.stringify({
|
|
309
|
-
contents: [{ role: "user", parts }],
|
|
310
|
-
generationConfig: { responseMimeType: "application/json", temperature: 0 },
|
|
311
|
-
}),
|
|
312
|
-
});
|
|
313
|
-
if (!res.ok) {
|
|
314
|
-
const body = await res.text();
|
|
325
|
+
catch (e) {
|
|
326
|
+
lastMessage = "De AI-dienst is niet bereikbaar. Probeer het zo nog eens.";
|
|
315
327
|
await logAiCall({
|
|
316
328
|
moduleKey: AI_MODULE_KEY,
|
|
317
329
|
providerSlug: model.providerSlug,
|
|
318
330
|
modelSlug: model.modelSlug,
|
|
319
331
|
status: "error",
|
|
320
332
|
durationMs: Date.now() - started,
|
|
321
|
-
errorCode:
|
|
322
|
-
errorMessage:
|
|
333
|
+
errorCode: "network",
|
|
334
|
+
errorMessage: e instanceof Error ? e.message.slice(0, 500) : null,
|
|
323
335
|
createdBy: context.userId ?? null,
|
|
324
336
|
});
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
}
|
|
328
|
-
if (res.status === 429) {
|
|
329
|
-
throw new Error("De AI-dienst is tijdelijk overbelast. Probeer het zo nog eens.");
|
|
330
|
-
}
|
|
331
|
-
throw new Error(`De AI-dienst gaf een fout (${res.status}).`);
|
|
332
|
-
}
|
|
333
|
-
const json = await res.json();
|
|
334
|
-
const text = json?.candidates?.[0]?.content?.parts?.map((p) => p?.text ?? "").join("") ?? "";
|
|
335
|
-
const cleaned = text.trim().replace(/^```(?:json)?/i, "").replace(/```$/, "");
|
|
336
|
-
let parsed = [];
|
|
337
|
-
try {
|
|
338
|
-
parsed = JSON.parse(cleaned);
|
|
339
|
-
}
|
|
340
|
-
catch {
|
|
341
|
-
parsed = [];
|
|
337
|
+
await new Promise((r) => setTimeout(r, 1500 * 2 ** attempt));
|
|
338
|
+
continue;
|
|
342
339
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
confidence: Number.isFinite(Number(row?.confidence)) ? Number(row.confidence) : 0,
|
|
353
|
-
reason: row?.reason ? String(row.reason).slice(0, 200) : null,
|
|
340
|
+
if (res.ok) {
|
|
341
|
+
json = await res.json();
|
|
342
|
+
await logAiCall({
|
|
343
|
+
moduleKey: AI_MODULE_KEY,
|
|
344
|
+
providerSlug: model.providerSlug,
|
|
345
|
+
modelSlug: model.modelSlug,
|
|
346
|
+
status: "success",
|
|
347
|
+
durationMs: Date.now() - started,
|
|
348
|
+
createdBy: context.userId ?? null,
|
|
354
349
|
});
|
|
350
|
+
break;
|
|
355
351
|
}
|
|
356
|
-
|
|
357
|
-
if (!suggestions.some((s) => s.fileId === b.fileId)) {
|
|
358
|
-
suggestions.push({ fileId: b.fileId, tradeItemKey: null, confidence: 0, reason: null });
|
|
359
|
-
}
|
|
360
|
-
}
|
|
352
|
+
const body = await res.text();
|
|
361
353
|
await logAiCall({
|
|
362
354
|
moduleKey: AI_MODULE_KEY,
|
|
363
355
|
providerSlug: model.providerSlug,
|
|
364
356
|
modelSlug: model.modelSlug,
|
|
365
|
-
status: "
|
|
357
|
+
status: "error",
|
|
366
358
|
durationMs: Date.now() - started,
|
|
359
|
+
errorCode: String(res.status),
|
|
360
|
+
errorMessage: body.slice(0, 500),
|
|
367
361
|
createdBy: context.userId ?? null,
|
|
368
362
|
});
|
|
363
|
+
if (res.status === 401 || res.status === 403) {
|
|
364
|
+
throw new Error("De AI-sleutel wordt geweigerd. Controleer de sleutel bij AI-authenticatie.");
|
|
365
|
+
}
|
|
366
|
+
if (res.status === 400 || res.status === 404) {
|
|
367
|
+
throw new Error(`Het ingestelde AI-model werd geweigerd (${res.status}). Controleer het model bij AI-authenticatie.`);
|
|
368
|
+
}
|
|
369
|
+
if (res.status === 429 || res.status >= 500) {
|
|
370
|
+
lastMessage =
|
|
371
|
+
"De AI-dienst is tijdelijk overbelast. De stickers zijn geüpload; koppel ze handmatig of probeer de herkenning zo nog eens.";
|
|
372
|
+
const retryAfter = Number(res.headers.get("retry-after"));
|
|
373
|
+
const waitMs = Number.isFinite(retryAfter) && retryAfter > 0
|
|
374
|
+
? retryAfter * 1000
|
|
375
|
+
: 1500 * 2 ** attempt;
|
|
376
|
+
if (attempt < 3)
|
|
377
|
+
await new Promise((r) => setTimeout(r, waitMs));
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
throw new Error(`De AI-dienst gaf een fout (${res.status}).`);
|
|
369
381
|
}
|
|
370
|
-
|
|
371
|
-
|
|
382
|
+
if (!json) {
|
|
383
|
+
aiError = lastMessage;
|
|
384
|
+
for (const b of batch) {
|
|
385
|
+
suggestions.push({ fileId: b.fileId, tradeItemKey: null, confidence: 0, reason: null });
|
|
386
|
+
}
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
const text = json?.candidates?.[0]?.content?.parts?.map((p) => p?.text ?? "").join("") ?? "";
|
|
390
|
+
const cleaned = text.trim().replace(/^```(?:json)?/i, "").replace(/```$/, "");
|
|
391
|
+
let parsed = [];
|
|
392
|
+
try {
|
|
393
|
+
parsed = JSON.parse(cleaned);
|
|
394
|
+
}
|
|
395
|
+
catch {
|
|
396
|
+
parsed = [];
|
|
397
|
+
}
|
|
398
|
+
const validKeys = new Set(products.map((p) => p.key));
|
|
399
|
+
for (const row of Array.isArray(parsed) ? parsed : []) {
|
|
400
|
+
const fileId = String(row?.fileId ?? "");
|
|
401
|
+
if (!batch.some((b) => b.fileId === fileId))
|
|
402
|
+
continue;
|
|
403
|
+
const key = row?.tradeItemKey ? String(row.tradeItemKey) : null;
|
|
404
|
+
suggestions.push({
|
|
405
|
+
fileId,
|
|
406
|
+
tradeItemKey: key && validKeys.has(key) ? key : null,
|
|
407
|
+
confidence: Number.isFinite(Number(row?.confidence)) ? Number(row.confidence) : 0,
|
|
408
|
+
reason: row?.reason ? String(row.reason).slice(0, 200) : null,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
for (const b of batch) {
|
|
412
|
+
if (!suggestions.some((s) => s.fileId === b.fileId)) {
|
|
413
|
+
suggestions.push({ fileId: b.fileId, tradeItemKey: null, confidence: 0, reason: null });
|
|
414
|
+
}
|
|
372
415
|
}
|
|
373
416
|
}
|
|
417
|
+
return { suggestions, aiError };
|
|
374
418
|
return { suggestions };
|
|
375
419
|
});
|
|
376
420
|
export const commitStickerBatch = createServerFn({ method: "POST" })
|
|
@@ -430,6 +474,9 @@ export const commitStickerBatch = createServerFn({ method: "POST" })
|
|
|
430
474
|
mime_type: it.mimeType,
|
|
431
475
|
file_size: it.fileSize ?? null,
|
|
432
476
|
uploaded_by: context.userId ?? null,
|
|
477
|
+
source: "manual",
|
|
478
|
+
floriday_sticker_id: null,
|
|
479
|
+
content_hash: null,
|
|
433
480
|
updated_at: new Date().toISOString(),
|
|
434
481
|
}, { onConflict: "customer_key,trade_item_key" });
|
|
435
482
|
if (upErr)
|