@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.
Files changed (67) hide show
  1. package/dist-lib/components/klanten/BulkStickerUploadDialog.d.ts.map +1 -1
  2. package/dist-lib/components/klanten/BulkStickerUploadDialog.js +30 -8
  3. package/dist-lib/components/klanten/CustomerAssortmentTab.d.ts.map +1 -1
  4. package/dist-lib/components/klanten/CustomerAssortmentTab.js +48 -10
  5. package/dist-lib/integrations/supabase/auth-middleware.d.ts +129 -0
  6. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  7. package/dist-lib/integrations/supabase/client.d.ts +129 -0
  8. package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
  9. package/dist-lib/integrations/supabase/client.server.d.ts +129 -0
  10. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  11. package/dist-lib/integrations/supabase/types.d.ts +129 -0
  12. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  13. package/dist-lib/lib/accounting/twinfield/client.server.d.ts +7 -8
  14. package/dist-lib/lib/accounting/twinfield/client.server.d.ts.map +1 -1
  15. package/dist-lib/lib/accounting/twinfield/client.server.js +147 -78
  16. package/dist-lib/lib/accounting/twinfield/worker.functions.d.ts +387 -0
  17. package/dist-lib/lib/accounting/twinfield/worker.functions.d.ts.map +1 -1
  18. package/dist-lib/lib/accounting/twinfield/worker.server.d.ts.map +1 -1
  19. package/dist-lib/lib/accounting/twinfield/worker.server.js +17 -0
  20. package/dist-lib/lib/accounting.functions.d.ts +387 -0
  21. package/dist-lib/lib/accounting.functions.d.ts.map +1 -1
  22. package/dist-lib/lib/customer-analytics.functions.d.ts +129 -0
  23. package/dist-lib/lib/customer-analytics.functions.d.ts.map +1 -1
  24. package/dist-lib/lib/customer-assortment.functions.d.ts +132 -3
  25. package/dist-lib/lib/customer-assortment.functions.d.ts.map +1 -1
  26. package/dist-lib/lib/customer-assortment.functions.js +12 -0
  27. package/dist-lib/lib/customer-notes.functions.d.ts +258 -0
  28. package/dist-lib/lib/customer-notes.functions.d.ts.map +1 -1
  29. package/dist-lib/lib/customer-revenue.functions.d.ts +129 -0
  30. package/dist-lib/lib/customer-revenue.functions.d.ts.map +1 -1
  31. package/dist-lib/lib/customer-stickers.functions.d.ts +1165 -0
  32. package/dist-lib/lib/customer-stickers.functions.d.ts.map +1 -1
  33. package/dist-lib/lib/customer-stickers.functions.js +122 -75
  34. package/dist-lib/lib/customers.functions.d.ts +1161 -0
  35. package/dist-lib/lib/customers.functions.d.ts.map +1 -1
  36. package/dist-lib/lib/delivery-locations.functions.d.ts +645 -0
  37. package/dist-lib/lib/delivery-locations.functions.d.ts.map +1 -1
  38. package/dist-lib/lib/external-customers.functions.d.ts +516 -0
  39. package/dist-lib/lib/external-customers.functions.d.ts.map +1 -1
  40. package/dist-lib/lib/floriday-assortment.functions.d.ts +129 -0
  41. package/dist-lib/lib/floriday-assortment.functions.d.ts.map +1 -1
  42. package/dist-lib/lib/floriday-discounts.functions.d.ts +258 -0
  43. package/dist-lib/lib/floriday-discounts.functions.d.ts.map +1 -1
  44. package/dist-lib/lib/floriday-price-import.functions.d.ts +258 -0
  45. package/dist-lib/lib/floriday-price-import.functions.d.ts.map +1 -1
  46. package/dist-lib/lib/floriday-sticker-import.functions.d.ts +27323 -0
  47. package/dist-lib/lib/floriday-sticker-import.functions.d.ts.map +1 -0
  48. package/dist-lib/lib/floriday-sticker-import.functions.js +307 -0
  49. package/dist-lib/lib/floriday-trade-items.functions.d.ts +129 -0
  50. package/dist-lib/lib/floriday-trade-items.functions.d.ts.map +1 -1
  51. package/dist-lib/lib/local-discounts.functions.d.ts +903 -0
  52. package/dist-lib/lib/local-discounts.functions.d.ts.map +1 -1
  53. package/dist-lib/lib/logistics-events.functions.d.ts +129 -0
  54. package/dist-lib/lib/logistics-events.functions.d.ts.map +1 -1
  55. package/dist-lib/lib/logistics-strategy.functions.d.ts +516 -0
  56. package/dist-lib/lib/logistics-strategy.functions.d.ts.map +1 -1
  57. package/dist-lib/lib/overdue-invoices.functions.d.ts +129 -0
  58. package/dist-lib/lib/overdue-invoices.functions.d.ts.map +1 -1
  59. package/dist-lib/lib/price-groups-local.functions.d.ts +1677 -0
  60. package/dist-lib/lib/price-groups-local.functions.d.ts.map +1 -1
  61. package/dist-lib/lib/price-groups.functions.d.ts +129 -0
  62. package/dist-lib/lib/price-groups.functions.d.ts.map +1 -1
  63. package/dist-lib/lib/twinfield-adopt.functions.d.ts +258 -0
  64. package/dist-lib/lib/twinfield-adopt.functions.d.ts.map +1 -1
  65. package/dist-lib/lib/twinfield-ledger.functions.d.ts +129 -0
  66. package/dist-lib/lib/twinfield-ledger.functions.d.ts.map +1 -1
  67. 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
- for (let i = 0; i < remaining.length; i += 8) {
279
- const batch = remaining.slice(i, i + 8);
280
- const started = Date.now();
281
- try {
282
- const parts = [
283
- {
284
- text: "Je koppelt stickerbestanden aan producten uit het assortiment van een bloemenhandel.\n" +
285
- "Producten:\n" +
286
- productList +
287
- "\n\nHieronder volgen stickers. Voor elke sticker krijg je eerst de bestandsnaam en daarna (indien beschikbaar) een afbeelding van de sticker.\n" +
288
- "Geef per sticker het best passende product terug. Kies alleen een product als je het redelijk zeker weet; anders tradeItemKey = null.\n" +
289
- 'Antwoord uitsluitend als JSON-array: [{"fileId":"...","tradeItemKey":"..."|null,"confidence":0.0-1.0,"reason":"korte uitleg in het Nederlands"}]',
290
- },
291
- ];
292
- for (const f of batch) {
293
- parts.push({ text: `Sticker fileId=${f.fileId} bestandsnaam="${f.fileName}"` });
294
- const t = f.thumbnail ?? "";
295
- const m = /^data:([^;,]+);base64,(.+)$/.exec(t);
296
- if (m)
297
- parts.push({ inline_data: { mime_type: m[1], data: m[2] } });
298
- }
299
- const url = model.providerSlug === "gemini"
300
- ? `${model.baseUrl}/v1beta/models/${model.modelSlug}:generateContent`
301
- : null;
302
- if (!url) {
303
- throw new Error(`AI-aanbieder "${model.providerSlug}" wordt hier nog niet ondersteund. Kies een Gemini-model bij AI-authenticatie.`);
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
- const res = await fetch(url, {
306
- method: "POST",
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: String(res.status),
322
- errorMessage: body.slice(0, 500),
333
+ errorCode: "network",
334
+ errorMessage: e instanceof Error ? e.message.slice(0, 500) : null,
323
335
  createdBy: context.userId ?? null,
324
336
  });
325
- if (res.status === 401 || res.status === 403) {
326
- throw new Error("De AI-sleutel wordt geweigerd. Controleer de sleutel bij AI-authenticatie.");
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
- const validKeys = new Set(products.map((p) => p.key));
344
- for (const row of Array.isArray(parsed) ? parsed : []) {
345
- const fileId = String(row?.fileId ?? "");
346
- if (!batch.some((b) => b.fileId === fileId))
347
- continue;
348
- const key = row?.tradeItemKey ? String(row.tradeItemKey) : null;
349
- suggestions.push({
350
- fileId,
351
- tradeItemKey: key && validKeys.has(key) ? key : null,
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
- for (const b of batch) {
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: "success",
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
- catch (e) {
371
- throw e instanceof Error ? e : new Error("AI-herkenning mislukt.");
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)