@flowselections/logistiek-engine 1.0.189 → 1.0.193

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 (61) hide show
  1. package/db/schema/logistiek-engine.sql +1468 -0
  2. package/dist-lib/components/BeladingIndelingCard.d.ts.map +1 -1
  3. package/dist-lib/components/BeladingIndelingCard.js +4 -2
  4. package/dist-lib/components/GeblokkeerdeOrdersCard.d.ts.map +1 -1
  5. package/dist-lib/components/GeblokkeerdeOrdersCard.js +43 -4
  6. package/dist-lib/components/InplanVoortgangCard.d.ts +16 -0
  7. package/dist-lib/components/InplanVoortgangCard.d.ts.map +1 -0
  8. package/dist-lib/components/InplanVoortgangCard.js +34 -0
  9. package/dist-lib/components/LadingdragersPage.d.ts.map +1 -1
  10. package/dist-lib/components/LadingdragersPage.js +4 -1
  11. package/dist-lib/components/OrdersZonderKarCard.d.ts +2 -0
  12. package/dist-lib/components/OrdersZonderKarCard.d.ts.map +1 -0
  13. package/dist-lib/components/OrdersZonderKarCard.js +36 -0
  14. package/dist-lib/components/RegiePage.d.ts.map +1 -1
  15. package/dist-lib/components/RegiePage.js +3 -1
  16. package/dist-lib/components/RuleEnginePage.js +8 -3
  17. package/dist-lib/hooks/useFloricodeLadingdragers.d.ts +2 -0
  18. package/dist-lib/hooks/useFloricodeLadingdragers.d.ts.map +1 -0
  19. package/dist-lib/hooks/useFloricodeLadingdragers.js +25 -0
  20. package/dist-lib/hooks/useGeblokkeerdeOrders.d.ts +1 -0
  21. package/dist-lib/hooks/useGeblokkeerdeOrders.d.ts.map +1 -1
  22. package/dist-lib/hooks/useGeblokkeerdeOrders.js +38 -1
  23. package/dist-lib/hooks/useRecalcJob.d.ts +1 -0
  24. package/dist-lib/hooks/useRecalcJob.d.ts.map +1 -1
  25. package/dist-lib/hooks/useRecalcJob.js +11 -1
  26. package/dist-lib/index.d.ts +1 -0
  27. package/dist-lib/index.d.ts.map +1 -1
  28. package/dist-lib/index.js +1 -0
  29. package/dist-lib/integrations/supabase/types.d.ts +580 -1
  30. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  31. package/dist-lib/lib/beladingRegels.d.ts +18 -0
  32. package/dist-lib/lib/beladingRegels.d.ts.map +1 -1
  33. package/dist-lib/lib/beladingRegels.js +28 -0
  34. package/dist-lib/lib/handlers/logistics-tick.server.d.ts.map +1 -1
  35. package/dist-lib/lib/handlers/logistics-tick.server.js +14 -0
  36. package/dist-lib/lib/loadCarrierPlanner.d.ts +3 -0
  37. package/dist-lib/lib/loadCarrierPlanner.d.ts.map +1 -1
  38. package/dist-lib/lib/loadCarrierPlanner.js +18 -13
  39. package/dist-lib/lib/loadCarrierPlanning.server.d.ts +16 -0
  40. package/dist-lib/lib/loadCarrierPlanning.server.d.ts.map +1 -1
  41. package/dist-lib/lib/loadCarrierPlanning.server.js +394 -24
  42. package/dist-lib/lib/loadCarrierTypes.d.ts +7 -0
  43. package/dist-lib/lib/loadCarrierTypes.d.ts.map +1 -1
  44. package/dist-lib/lib/loadCarrierTypes.js +17 -0
  45. package/dist-lib/lib/logisticsRecalcJob.server.d.ts +1 -0
  46. package/dist-lib/lib/logisticsRecalcJob.server.d.ts.map +1 -1
  47. package/dist-lib/lib/logisticsRecalcJob.server.js +5 -2
  48. package/dist-lib/lib/logisticsRecalculate.functions.js +2 -2
  49. package/dist-lib/lib/moduleVersion.d.ts +1 -1
  50. package/dist-lib/lib/moduleVersion.js +1 -1
  51. package/dist-lib/lib/planQueue.server.d.ts.map +1 -1
  52. package/dist-lib/lib/planQueue.server.js +8 -2
  53. package/dist-lib/lib/ruleEngineSchema.d.ts.map +1 -1
  54. package/dist-lib/lib/ruleEngineSchema.js +9 -0
  55. package/dist-lib/lib/scheduleTasks.server.d.ts +24 -0
  56. package/dist-lib/lib/scheduleTasks.server.d.ts.map +1 -0
  57. package/dist-lib/lib/scheduleTasks.server.js +113 -0
  58. package/dist-lib/lib/voorraadFouten.d.ts +14 -0
  59. package/dist-lib/lib/voorraadFouten.d.ts.map +1 -0
  60. package/dist-lib/lib/voorraadFouten.js +18 -0
  61. package/package.json +1 -1
@@ -30364,3 +30364,1471 @@ END $function$;
30364
30364
  REVOKE ALL ON FUNCTION public.logistics_plan_fail_log(text, uuid, text, text) FROM PUBLIC, anon, authenticated;
30365
30365
  GRANT EXECUTE ON FUNCTION public.logistics_plan_fail_log(text, uuid, text, text) TO service_role;
30366
30366
  COMMENT ON FUNCTION public.logistics_plan_fail_log(text, uuid, text, text) IS 'Logistiek Engine: legt een mislukte aanmelding voor (her)planning vast in het logboek en zet de order alsnog in de planwachtrij.';
30367
+
30368
+ -- ===========================================================================
30369
+ -- 1.0.190: dubbelzinnige overloads, verzendbrief zonder terugval, controle.
30370
+ -- Herhaalbaar; verwijdert niets.
30371
+ -- ===========================================================================
30372
+ CREATE OR REPLACE FUNCTION public.logistics_scan_delays()
30373
+ RETURNS integer
30374
+ LANGUAGE plpgsql
30375
+ SECURITY DEFINER
30376
+ SET search_path TO 'public'
30377
+ AS $function$
30378
+ DECLARE
30379
+ rec record;
30380
+ cnt integer := 0;
30381
+ BEGIN
30382
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_scan_delays');
30383
+ FOR rec IN
30384
+ SELECT o.id
30385
+ FROM public.orders o
30386
+ WHERE o.deleted_at IS NULL
30387
+ AND o.delivery_date IS NOT NULL
30388
+ AND NULLIF(o.delivery_date::text, '')::date < current_date
30389
+ AND COALESCE(o.status::text, '') NOT IN ('delivered', 'cancelled', 'Geleverd', 'Geannuleerd')
30390
+ AND NOT EXISTS (
30391
+ SELECT 1 FROM public.logistics_delay_alerts d
30392
+ WHERE d.order_id = o.id AND d.status = 'open')
30393
+ LIMIT 200
30394
+ LOOP
30395
+ PERFORM public.logistics_report_delay(p_order_id => rec.id, p_reason => 'Geplande leverdatum is verstreken', p_expected_date => NULL::date, p_force_customer_email => false);
30396
+ cnt := cnt + 1;
30397
+ END LOOP;
30398
+ RETURN cnt;
30399
+ END;
30400
+ $function$;
30401
+
30402
+ CREATE OR REPLACE FUNCTION public.logistics_document_type_map(_document_code text)
30403
+ RETURNS doc_layout_type[]
30404
+ LANGUAGE sql
30405
+ IMMUTABLE
30406
+ SET search_path TO 'public'
30407
+ AS $function$
30408
+ WITH c AS (SELECT lower(btrim(coalesce(_document_code, ''))) AS code)
30409
+ SELECT CASE
30410
+ WHEN c.code = '' THEN ARRAY[]::public.doc_layout_type[]
30411
+ WHEN c.code LIKE '%pakbon%' THEN ARRAY['pakbon']::public.doc_layout_type[]
30412
+ WHEN c.code LIKE '%afleverbrief%' OR c.code LIKE '%afleverbon%'
30413
+ THEN ARRAY['afleverbrief','transportdocument']::public.doc_layout_type[]
30414
+ WHEN c.code LIKE '%veilingbrief%'
30415
+ THEN ARRAY['veilingbrief','transportdocument']::public.doc_layout_type[]
30416
+ WHEN c.code LIKE '%veilingbon%'
30417
+ THEN ARRAY['veilingbon','veilingbrief','transportdocument']::public.doc_layout_type[]
30418
+ WHEN c.code LIKE '%verzendbrief%' OR c.code LIKE '%transportbrief%'
30419
+ THEN ARRAY['afleverbrief','transportdocument']::public.doc_layout_type[]
30420
+ WHEN c.code LIKE '%verzendlabel%'
30421
+ THEN ARRAY['verzendlabel','transportdocument']::public.doc_layout_type[]
30422
+ WHEN c.code LIKE '%productiebon%'
30423
+ THEN ARRAY['productiebon']::public.doc_layout_type[]
30424
+ WHEN c.code LIKE '%productielijst%'
30425
+ THEN ARRAY['magazijnlijst','productiebon']::public.doc_layout_type[]
30426
+ WHEN c.code LIKE '%controlelijst%' OR c.code LIKE '%controlebrief%'
30427
+ THEN ARRAY['controlelijst']::public.doc_layout_type[]
30428
+ WHEN c.code LIKE '%magazijnlijst%'
30429
+ THEN ARRAY['magazijnlijst']::public.doc_layout_type[]
30430
+ WHEN c.code LIKE '%sticker%'
30431
+ THEN ARRAY['sticker','etiket']::public.doc_layout_type[]
30432
+ WHEN c.code LIKE '%etiket%'
30433
+ THEN ARRAY['etiket','sticker']::public.doc_layout_type[]
30434
+ WHEN c.code LIKE '%productlabel%'
30435
+ THEN ARRAY['productlabel','etiket']::public.doc_layout_type[]
30436
+ WHEN c.code LIKE '%barcode%'
30437
+ THEN ARRAY['barcode_label','etiket']::public.doc_layout_type[]
30438
+ WHEN c.code LIKE '%label%'
30439
+ THEN ARRAY['etiket']::public.doc_layout_type[]
30440
+ WHEN c.code LIKE '%factuur%' THEN ARRAY['factuur']::public.doc_layout_type[]
30441
+ WHEN c.code LIKE '%offerte%' THEN ARRAY['offerte']::public.doc_layout_type[]
30442
+ WHEN c.code LIKE '%orderbevestiging%' THEN ARRAY['orderbevestiging']::public.doc_layout_type[]
30443
+ WHEN c.code LIKE '%retourbon%' THEN ARRAY['vrij']::public.doc_layout_type[]
30444
+ ELSE ARRAY[]::public.doc_layout_type[]
30445
+ END
30446
+ FROM c;
30447
+ $function$;
30448
+
30449
+ CREATE OR REPLACE FUNCTION public.logistics_shipping_note_job_for_slip(_slip_job_id uuid)
30450
+ RETURNS uuid
30451
+ LANGUAGE plpgsql
30452
+ SECURITY DEFINER
30453
+ SET search_path TO 'public'
30454
+ AS $function$
30455
+ DECLARE
30456
+ slip public.print_jobs;
30457
+ layout_meta jsonb;
30458
+ v_layout uuid;
30459
+ v_doc_type text;
30460
+ v_layout_type text;
30461
+ v_key text;
30462
+ v_bron boolean;
30463
+ v_order text;
30464
+ v_res jsonb;
30465
+ BEGIN
30466
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_shipping_note_job_for_slip');
30467
+ SELECT * INTO slip FROM public.print_jobs WHERE id = _slip_job_id;
30468
+ IF NOT FOUND THEN RETURN NULL; END IF;
30469
+ v_order := coalesce(slip.payload->>'ordernummer', slip.payload->>'order_number');
30470
+
30471
+ v_bron := public.logistics_payload_is_floriday(slip.payload);
30472
+ IF v_bron IS TRUE THEN
30473
+ INSERT INTO public.logistics_events(source_module, event_type, status, processed_at, payload)
30474
+ VALUES ('logistiek-engine', 'shippingnote.blocked', 'verwerkt', now(), jsonb_build_object(
30475
+ 'slipJobId', _slip_job_id, 'orderNumber', v_order,
30476
+ 'reason', 'floriday_order_geen_eigen_verzendbrief'));
30477
+ RETURN NULL;
30478
+ END IF;
30479
+
30480
+ v_key := 'verzendbrief-bij-pakbon:' || _slip_job_id::text;
30481
+
30482
+ -- 1.0.190: geen terugval meer. Herkomst onbekend of geen layout = fail closed met reden.
30483
+ IF v_bron IS NULL THEN
30484
+ BEGIN
30485
+ INSERT INTO public.logistics_trace_events(phase, step, status, reason, order_ref, slip_ref)
30486
+ VALUES ('verzendbrief', 'Verzendbrief wacht: herkomst onbekend', 'geblokkeerd',
30487
+ 'HERKOMST_ONBEKEND: de herkomst van de order kon niet worden bepaald; er wordt geen verzendbrief gegokt.',
30488
+ coalesce(v_order, slip.payload->>'order_id'), _slip_job_id::text);
30489
+ EXCEPTION WHEN OTHERS THEN NULL;
30490
+ END;
30491
+ RETURN NULL;
30492
+ END IF;
30493
+
30494
+ layout_meta := public.logistics_document_for_order(
30495
+ public.logistics_try_uuid(coalesce(slip.payload->>'order_id', slip.payload->>'orderId')), 'verzendbrief');
30496
+ v_layout := public.logistics_try_uuid(layout_meta->>'layout_id');
30497
+ SELECT l.documenttype::text INTO v_layout_type FROM public.document_layouts l WHERE l.id = v_layout AND l.actief IS TRUE;
30498
+ IF NOT coalesce((layout_meta->>'ok')::boolean, false) OR v_layout_type IS NULL THEN
30499
+ BEGIN
30500
+ INSERT INTO public.logistics_trace_events(phase, step, status, reason, order_ref, slip_ref)
30501
+ VALUES ('verzendbrief', 'Verzendbrief wacht: geen layout', 'geblokkeerd',
30502
+ 'GEEN_LAYOUT_VERZENDBRIEF: ' || coalesce(nullif(layout_meta->>'reden', ''), 'Er is geen actieve layout voor de verzendbrief van deze order.'),
30503
+ coalesce(v_order, slip.payload->>'order_id'), _slip_job_id::text);
30504
+ EXCEPTION WHEN OTHERS THEN NULL;
30505
+ END;
30506
+ RETURN NULL;
30507
+ END IF;
30508
+ v_doc_type := v_layout_type;
30509
+
30510
+ v_res := public.logistics_print_via_layout(
30511
+ v_doc_type, coalesce(slip.documentnaam, 'Verzendbrief') || ' · verzendbrief', NULL, NULL,
30512
+ v_layout, nullif(slip.payload->>'print_plek_id', '')::uuid, 'eigen', coalesce(slip.is_herprint, false),
30513
+ (coalesce(slip.payload, '{}'::jsonb) - 'cartId' - 'pick_station' - 'printPlekId' - 'print_place_id' - 'printPlaceId')
30514
+ || jsonb_build_object(
30515
+ 'ordernummer', v_order,
30516
+ 'document_type_code', 'verzendbrief', 'documentType', 'verzendbrief', 'layout_id', v_layout,
30517
+ 'trigger', 'with_packing_slip', 'is_floriday', false,
30518
+ 'layout_keuze', layout_meta->>'keuze',
30519
+ 'packing_slip_job_id', _slip_job_id, 'print_batch_key', 'kar-print:' || _slip_job_id::text),
30520
+ v_key);
30521
+
30522
+ IF NOT coalesce((v_res->>'ok')::boolean, false) THEN
30523
+ BEGIN
30524
+ INSERT INTO public.logistics_trace_events(phase, step, status, reason, order_ref, slip_ref)
30525
+ VALUES ('verzendbrief', 'Verzendbrief bij pakbon niet geprint', 'mislukt', v_res->>'error',
30526
+ coalesce(v_order, slip.payload->>'order_id'), _slip_job_id::text);
30527
+ EXCEPTION WHEN OTHERS THEN NULL;
30528
+ END;
30529
+ RETURN NULL;
30530
+ END IF;
30531
+ RETURN (v_res->>'job_id')::uuid;
30532
+ END;
30533
+ $function$;
30534
+
30535
+ CREATE OR REPLACE FUNCTION public.logistics_overload_check()
30536
+ RETURNS TABLE(onderdeel text, ok boolean, toelichting text)
30537
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public'
30538
+ AS $function$
30539
+ DECLARE r record; v_n integer := 0;
30540
+ BEGIN
30541
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_overload_check');
30542
+ -- Dubbelzinnig: een kortere overload waarvan de aanroep ook op een langere past
30543
+ -- (alle extra parameters van de langere hebben een standaardwaarde).
30544
+ FOR r IN
30545
+ SELECT a.proname,
30546
+ pg_get_function_identity_arguments(a.oid) AS kort,
30547
+ pg_get_function_identity_arguments(b.oid) AS lang
30548
+ FROM pg_proc a
30549
+ JOIN pg_proc b ON b.proname = a.proname AND b.pronamespace = a.pronamespace AND b.oid <> a.oid
30550
+ WHERE a.pronamespace = 'public'::regnamespace
30551
+ AND a.proname LIKE 'logistics\_%'
30552
+ AND b.pronargs > a.pronargs
30553
+ AND b.pronargs - b.pronargdefaults <= a.pronargs
30554
+ AND (a.pronargs = 0 OR (b.proargtypes::oid[])[0:a.pronargs - 1] = (a.proargtypes::oid[])[0:a.pronargs - 1])
30555
+ LOOP
30556
+ v_n := v_n + 1;
30557
+ onderdeel := 'overload:' || r.proname;
30558
+ ok := false;
30559
+ toelichting := format('Dubbelzinnig: (%s) en (%s). Aanroepen gebeuren met benoemde argumenten; Shell-objecten worden niet verwijderd.', r.kort, r.lang);
30560
+ RETURN NEXT;
30561
+ END LOOP;
30562
+ IF v_n = 0 THEN
30563
+ onderdeel := 'overloads'; ok := true; toelichting := 'Geen dubbelzinnige overloads'; RETURN NEXT;
30564
+ END IF;
30565
+ END $function$;
30566
+ REVOKE ALL ON FUNCTION public.logistics_overload_check() FROM PUBLIC, anon;
30567
+ GRANT EXECUTE ON FUNCTION public.logistics_overload_check() TO authenticated, service_role;
30568
+
30569
+ DO $ic190$
30570
+ DECLARE v_def text;
30571
+ BEGIN
30572
+ v_def := pg_get_functiondef('public.logistics_install_check()'::regprocedure);
30573
+ IF v_def NOT LIKE '%logistics_overload_check()%' THEN
30574
+ v_def := regexp_replace(v_def, 'END \$function\$\s*$', E' RETURN QUERY SELECT * FROM public.logistics_overload_check();\nEND $function$');
30575
+ EXECUTE v_def;
30576
+ END IF;
30577
+ END $ic190$;
30578
+ -- 1.0.190 deel 2: verzendbriefstatus, barcode-pad, stickerreden, stickermaten, beladingbevestiging. Herhaalbaar; verwijdert niets.
30579
+ CREATE OR REPLACE FUNCTION public.logistics_shipping_note_status(p_slip_job_id uuid DEFAULT NULL::uuid, p_cart_id uuid DEFAULT NULL::uuid)
30580
+ RETURNS jsonb
30581
+ LANGUAGE plpgsql
30582
+ STABLE SECURITY DEFINER
30583
+ SET search_path TO 'public'
30584
+ AS $function$
30585
+ DECLARE
30586
+ v_note public.logistics_slip_shipping_note;
30587
+ v_slip public.print_jobs;
30588
+ v_job public.print_jobs;
30589
+ v_state text; v_message text; v_cart uuid;
30590
+ v_vrijgave boolean := false; v_scan_ok boolean;
30591
+ BEGIN
30592
+ IF p_slip_job_id IS NULL AND p_cart_id IS NULL THEN
30593
+ RETURN jsonb_build_object('status','mislukt','message',
30594
+ 'De pakbon of kar ontbreekt; de verzendbriefstatus kan niet worden opgehaald.','attempts',0,'nextAttemptAt',NULL);
30595
+ END IF;
30596
+ IF p_slip_job_id IS NOT NULL THEN
30597
+ SELECT * INTO v_slip FROM public.print_jobs WHERE id = p_slip_job_id;
30598
+ ELSE
30599
+ SELECT * INTO v_slip FROM public.print_jobs p
30600
+ WHERE lower(coalesce(p.payload->>'document_type_code', p.payload->>'documentType', p.documenttype::text, '')) LIKE '%pakbon%'
30601
+ AND nullif(coalesce(p.payload->>'cart_id', p.payload->'cart'->>'cartId'), '') = p_cart_id::text
30602
+ ORDER BY p.created_at DESC LIMIT 1;
30603
+ END IF;
30604
+ IF v_slip.id IS NULL THEN
30605
+ RETURN jsonb_build_object('status','mislukt','message','De bijbehorende pakbon is niet gevonden.',
30606
+ 'attempts',0,'nextAttemptAt',NULL,'cartId',p_cart_id);
30607
+ END IF;
30608
+ BEGIN
30609
+ v_cart := coalesce(nullif(v_slip.payload->>'cart_id','')::uuid, nullif(v_slip.payload->'cart'->>'cartId','')::uuid, p_cart_id);
30610
+ EXCEPTION WHEN invalid_text_representation THEN v_cart := p_cart_id;
30611
+ END;
30612
+ SELECT * INTO v_note FROM public.logistics_slip_shipping_note s
30613
+ WHERE s.slip_job_id = v_slip.id ORDER BY s.created_at DESC LIMIT 1;
30614
+
30615
+ IF v_note.id IS NULL THEN
30616
+ SELECT * INTO v_job FROM public.print_jobs p
30617
+ WHERE p.idempotentie_sleutel = 'verzendbrief-bij-pakbon:' || v_slip.id::text
30618
+ OR (p.payload->>'packing_slip_job_id' = v_slip.id::text
30619
+ AND lower(coalesce(p.payload->>'document_type_code', p.documenttype::text, '')) ~ '(verzend|aflever)')
30620
+ ORDER BY p.created_at DESC LIMIT 1;
30621
+ IF v_job.id IS NULL THEN
30622
+ IF v_slip.created_at < now() - interval '15 minutes' THEN
30623
+ v_state := 'mislukt';
30624
+ v_message := 'Er is geen verzendbrief bij deze pakbon aangemaakt. Controleer de printwachtrij of print de pakbon opnieuw.';
30625
+ ELSE
30626
+ v_state := 'klaar_voor_print'; v_message := 'Verzendbrief wordt klaargezet.';
30627
+ END IF;
30628
+ ELSIF v_job.status = 'voltooid' THEN
30629
+ v_state := 'geprint'; v_message := 'Pakbon en verzendbrief zijn naar de printer gestuurd.';
30630
+ ELSIF v_job.status IN ('wachtend','bezig') THEN
30631
+ v_state := 'klaar_voor_print'; v_message := 'Verzendbrief wordt klaargezet.';
30632
+ ELSE
30633
+ v_state := 'mislukt'; v_message := 'De verzendbrief kon niet worden geprint. Controleer de printwachtrij.';
30634
+ END IF;
30635
+ RETURN jsonb_build_object('status',v_state,'message',v_message,
30636
+ 'attempts',coalesce(v_job.pogingen,0),'nextAttemptAt',v_job.volgende_poging,
30637
+ 'slipJobId',v_slip.id,'shippingNoteJobId',v_job.id,'cartId',v_cart,
30638
+ 'orderNumber',coalesce(v_slip.payload->>'order_number', v_slip.payload->'data'->>'orderNumber'));
30639
+ END IF;
30640
+
30641
+ IF v_note.print_job_id IS NOT NULL THEN
30642
+ SELECT * INTO v_job FROM public.print_jobs WHERE id = v_note.print_job_id;
30643
+ END IF;
30644
+ v_scan_ok := EXISTS (SELECT 1 FROM public.logistics_control_scan cs
30645
+ WHERE cs.cart_id = coalesce(v_note.cart_id, v_cart) AND cs.approved_at IS NOT NULL);
30646
+
30647
+ IF v_note.status = 'mislukt' THEN
30648
+ v_state := 'mislukt'; v_message := 'De officiële Floriday-afleverbrief kon niet worden klaargezet. Controleer de verzendbriefwachtrij.';
30649
+ ELSIF v_note.status = 'wacht_op_vrijgave'
30650
+ OR (v_note.status = 'wachtend' AND v_note.retry_class IN ('wacht_op_vrijgave','wacht_op_kenmerk'))
30651
+ OR (v_note.status = 'wachtend' AND v_note.retry_class IS NULL AND coalesce(v_note.attempts,0) = 0 AND NOT v_scan_ok) THEN
30652
+ v_state := 'wacht_op_floriday'; v_vrijgave := true;
30653
+ -- 1.0.190: echte wachtreden uit de wachtrij, geen vaste tekst meer.
30654
+ v_message := 'Pakbon geprint — verzendbrief wacht: ' || coalesce(nullif(btrim(v_note.error), ''),
30655
+ CASE WHEN NOT v_scan_ok THEN 'op een goedgekeurde controlescan.'
30656
+ ELSE 'tot Afhandeling de belading naar Floriday heeft gestuurd (na het aanmeldmoment van het transport).' END);
30657
+ ELSIF v_note.status = 'wachtend' AND v_note.retry_class = 'nog_niet_beschikbaar' THEN
30658
+ v_state := 'wacht_op_floriday'; v_message := 'Pakbon geprint — wachten op officiële Floriday-afleverbrief.';
30659
+ ELSIF v_note.status = 'wachtend' AND v_note.retry_class = 'tijdelijk' AND coalesce(v_note.attempts,0) > 0
30660
+ AND coalesce(v_note.error,'') ~* '(5\d\d|429|time-?out|netwerk|verbinding|niet bereikbaar)' THEN
30661
+ v_state := 'klaar_voor_print'; v_message := 'Floriday is tijdelijk niet bereikbaar. De verzendbrief wordt automatisch opnieuw geprobeerd.';
30662
+ ELSIF v_note.status = 'wachtend' AND v_note.retry_class = 'limiet_bereikt' THEN
30663
+ v_state := 'mislukt'; v_message := 'Het maximale aantal pogingen voor de verzendbrief is bereikt. Controleer de verzendbriefwachtrij.';
30664
+ ELSIF v_note.status = 'wachtend' THEN
30665
+ v_state := 'klaar_voor_print'; v_message := 'Verzendbrief wordt klaargezet.';
30666
+ ELSIF v_job.status = 'voltooid' THEN
30667
+ v_state := 'geprint'; v_message := 'Pakbon en verzendbrief zijn naar de printer gestuurd.';
30668
+ ELSIF v_job.status IN ('mislukt','geannuleerd') THEN
30669
+ v_state := 'mislukt'; v_message := 'De verzendbrief kon niet worden geprint. Controleer de printwachtrij.';
30670
+ ELSE
30671
+ v_state := 'klaar_voor_print'; v_message := 'De officiële Floriday-afleverbrief wordt naar dezelfde printer gestuurd.';
30672
+ END IF;
30673
+
30674
+ RETURN jsonb_build_object('status',v_state,'message',v_message,
30675
+ 'attempts',coalesce(v_note.attempts,0),'nextAttemptAt',v_note.next_attempt_at,
30676
+ 'slipJobId',v_slip.id,'shippingNoteJobId',v_note.print_job_id,
30677
+ 'cartId',coalesce(v_note.cart_id, v_cart),'orderNumber',v_note.order_number,
30678
+ 'wachtOpVrijgave', v_vrijgave,
30679
+ 'wachtReden', CASE WHEN v_vrijgave THEN coalesce(nullif(btrim(v_note.error), ''), v_message) END,
30680
+ 'wachtCode', CASE WHEN v_vrijgave THEN coalesce(v_note.retry_class, v_note.status) END,
30681
+ -- Verwacht tijdstip: volgt zodra Afhandeling het aanmeldmoment levert (koppelprompt).
30682
+ 'verwachtOm', NULL::timestamptz);
30683
+ END;
30684
+ $function$;
30685
+
30686
+ CREATE OR REPLACE FUNCTION public.logistics_shipping_note_from_barcode(p_barcode text, p_allow_reprint boolean DEFAULT false, p_station_id uuid DEFAULT NULL::uuid)
30687
+ RETURNS jsonb
30688
+ LANGUAGE plpgsql
30689
+ SECURITY DEFINER
30690
+ SET search_path TO 'public'
30691
+ AS $function$
30692
+ DECLARE
30693
+ v_look jsonb;
30694
+ v_cart jsonb;
30695
+ v_cart_id uuid;
30696
+ v_order_id uuid;
30697
+ v_order_number text;
30698
+ v_layout jsonb;
30699
+ v_layout_id uuid;
30700
+ v_doc_type text;
30701
+ v_plek uuid;
30702
+ v_herprint boolean;
30703
+ v_volgnr integer;
30704
+ v_res jsonb;
30705
+ BEGIN
30706
+ v_look := public.logistics_barcode_lookup(p_barcode);
30707
+ IF NOT COALESCE((v_look->>'ok')::boolean, false) THEN
30708
+ RETURN v_look;
30709
+ END IF;
30710
+
30711
+ v_cart := v_look->'cart';
30712
+ v_cart_id := nullif(v_cart->>'cartId', '')::uuid;
30713
+ v_order_id := nullif(v_look->>'orderId', '')::uuid;
30714
+ v_order_number := v_look->'order'->>'orderNumber';
30715
+
30716
+ IF COALESCE((v_look->>'alreadyPrinted')::boolean, false) AND NOT COALESCE(p_allow_reprint, false) THEN
30717
+ RETURN v_look || jsonb_build_object(
30718
+ 'ok', false,
30719
+ 'errorCode', 'al_geprint',
30720
+ 'error', 'Voor deze kar is al een verzendbrief gemaakt. Kies bewust voor opnieuw printen als dat de bedoeling is.');
30721
+ END IF;
30722
+
30723
+ -- 1.0.190: layout via de centrale documentkeuze per order; geen terugval op 'verzendlabel'.
30724
+ v_layout := public.logistics_document_for_order(v_order_id, 'verzendbrief');
30725
+ v_layout_id := public.logistics_try_uuid(v_layout->>'layout_id');
30726
+ SELECT l.documenttype::text INTO v_doc_type FROM public.document_layouts l WHERE l.id = v_layout_id AND l.actief IS TRUE;
30727
+ IF NOT coalesce((v_layout->>'ok')::boolean, false) OR v_doc_type IS NULL THEN
30728
+ RETURN v_look || jsonb_build_object(
30729
+ 'ok', false,
30730
+ 'errorCode', 'GEEN_LAYOUT_VERZENDBRIEF',
30731
+ 'error', coalesce(nullif(v_layout->>'reden', ''), 'Er is geen actieve layout voor de verzendbrief van deze order.'),
30732
+ 'printJobId', NULL, 'layoutId', NULL, 'documentType', NULL, 'printPlekId', NULL);
30733
+ END IF;
30734
+
30735
+ PERFORM public.logistics_ensure_cart_barcode(v_cart_id);
30736
+
30737
+ IF p_station_id IS NOT NULL THEN
30738
+ SELECT k.print_plek_id INTO v_plek FROM public.print_plek_koppelingen k
30739
+ WHERE k.bron = 'pickstation' AND k.bron_id = p_station_id::text
30740
+ ORDER BY k.created_at LIMIT 1;
30741
+ END IF;
30742
+
30743
+ v_herprint := COALESCE(p_allow_reprint, false) AND COALESCE((v_look->>'alreadyPrinted')::boolean, false);
30744
+ SELECT count(*) INTO v_volgnr FROM public.logistics_shipping_note_scan WHERE cart_id = v_cart_id;
30745
+
30746
+ v_res := public.logistics_print_via_layout(
30747
+ v_doc_type, 'Verzendbrief ' || coalesce(v_order_number, ''), NULL, NULL,
30748
+ v_layout_id, v_plek, NULL, v_herprint,
30749
+ jsonb_build_object(
30750
+ 'source', 'pakbon_scan',
30751
+ 'barcode', v_look->>'barcode',
30752
+ 'order_id', v_order_id,
30753
+ 'ordernummer', v_order_number,
30754
+ 'order_number', v_order_number,
30755
+ 'customer_name', v_look->'order'->>'customerName',
30756
+ 'cart_id', v_cart_id,
30757
+ 'cart_index', v_cart->>'cartIndex',
30758
+ 'cart_total', v_cart->>'cartTotal',
30759
+ 'cart_label', v_cart->>'cartLabel',
30760
+ 'print_plek_id', v_plek,
30761
+ 'station_id', p_station_id,
30762
+ 'layout_keuze', v_layout->>'keuze',
30763
+ 'data', v_cart),
30764
+ 'verzendbrief-scan:' || coalesce(v_cart_id::text, v_look->>'barcode') || ':' || v_volgnr::text);
30765
+
30766
+ IF NOT coalesce((v_res->>'ok')::boolean, false) THEN
30767
+ RETURN v_look || jsonb_build_object(
30768
+ 'ok', false,
30769
+ 'errorCode', lower(v_res->>'error_code'),
30770
+ 'error', v_res->>'error',
30771
+ 'printJobId', NULL, 'layoutId', v_layout_id, 'documentType', v_doc_type, 'printPlekId', v_plek);
30772
+ END IF;
30773
+
30774
+ IF NOT coalesce((v_res->>'bestaand')::boolean, false) THEN
30775
+ INSERT INTO public.logistics_shipping_note_scan(cart_id, order_id, barcode, print_job_id, is_reprint)
30776
+ VALUES (v_cart_id, v_order_id, v_look->>'barcode', (v_res->>'job_id')::uuid,
30777
+ COALESCE((v_look->>'alreadyPrinted')::boolean, false));
30778
+ END IF;
30779
+
30780
+ RETURN v_look || jsonb_build_object(
30781
+ 'ok', true,
30782
+ 'printJobId', (v_res->>'job_id')::uuid,
30783
+ 'layoutId', v_layout_id,
30784
+ 'documentType', v_doc_type,
30785
+ 'printPlekId', v_plek,
30786
+ 'warning', NULL);
30787
+ END;
30788
+ $function$;
30789
+
30790
+ CREATE OR REPLACE FUNCTION public.logistics_cart_sticker_state(p_cart_id uuid)
30791
+ RETURNS jsonb
30792
+ LANGUAGE sql
30793
+ STABLE SECURITY DEFINER
30794
+ SET search_path TO 'public'
30795
+ AS $function$
30796
+ WITH s AS (SELECT * FROM public.logistics_sticker_prints WHERE cart_id = p_cart_id),
30797
+ vraag AS (
30798
+ SELECT count(*) FILTER (WHERE coalesce(oi.needs_sticker, false)) AS met_sticker, count(*) AS regels
30799
+ FROM public.order_cart_items ci
30800
+ LEFT JOIN public.order_items oi ON oi.id::text = split_part(ci.order_item_id::text, '::', 1)
30801
+ WHERE ci.cart_id = p_cart_id
30802
+ ),
30803
+ t AS (
30804
+ SELECT
30805
+ count(*) FILTER (WHERE status <> 'niet_printen') AS totaal,
30806
+ count(*) FILTER (WHERE status IN ('in_printwachtrij','afgemeld')) AS in_wachtrij,
30807
+ count(*) FILTER (WHERE status IN ('wacht_op_moment','klaar')) AS wachtend,
30808
+ count(*) FILTER (WHERE status IN ('geblokkeerd','mislukt')) AS geblokkeerd,
30809
+ count(*) FILTER (WHERE status = 'niet_printen') AS niet_printen,
30810
+ (array_agg(reden ORDER BY soort, bron_id) FILTER (WHERE status = 'niet_printen' AND coalesce(btrim(reden), '') <> ''))[1] AS niet_reden,
30811
+ coalesce(jsonb_agg(jsonb_build_object('soort', soort, 'stickerId', bron_id, 'kopieen', kopieen,
30812
+ 'status', status, 'reden', reden, 'printJobId', print_job_id, 'breedteMm', breedte_mm, 'hoogteMm', hoogte_mm)
30813
+ ORDER BY soort, bron_id), '[]'::jsonb) AS regels
30814
+ FROM s
30815
+ )
30816
+ SELECT jsonb_build_object(
30817
+ 'cartId', p_cart_id,
30818
+ 'totaal', t.totaal,
30819
+ 'inPrintwachtrij', t.in_wachtrij,
30820
+ 'wachtend', t.wachtend,
30821
+ 'geblokkeerd', t.geblokkeerd,
30822
+ 'regels', t.regels,
30823
+ -- 1.0.190: waarom deze kar geen stickers heeft, in gewone taal.
30824
+ 'stickersNodig', t.totaal > 0,
30825
+ 'geenStickersReden', CASE
30826
+ WHEN t.totaal > 0 THEN NULL
30827
+ WHEN t.niet_printen > 0 THEN coalesce(t.niet_reden, 'De stickers van deze kar hoeven niet geprint te worden.')
30828
+ WHEN v.regels = 0 THEN 'Deze kar heeft geen regels.'
30829
+ WHEN v.met_sticker = 0 THEN 'Geen regel op deze kar vraagt om een sticker.'
30830
+ ELSE 'Er vraagt een regel om een sticker, maar die is nog niet klaargezet. De stickerronde pakt dit automatisch op.'
30831
+ END)
30832
+ FROM t, vraag v;
30833
+ $function$;
30834
+
30835
+ -- Layouts 1.69.11: benodigde stickermaten per dag en printplek.
30836
+ CREATE OR REPLACE FUNCTION public.logistics_sticker_maten_per_dag(p_van date, p_tot date)
30837
+ RETURNS TABLE(dag date, print_plek_id text, breedte_mm numeric, hoogte_mm numeric, aantal integer)
30838
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public'
30839
+ AS $function$
30840
+ BEGIN
30841
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_sticker_maten_per_dag');
30842
+ IF p_van IS NULL OR p_tot IS NULL OR p_tot < p_van THEN
30843
+ RAISE EXCEPTION 'Geef een geldige periode op (van t/m tot).' USING ERRCODE = '22023';
30844
+ END IF;
30845
+ RETURN QUERY
30846
+ SELECT coalesce(c.departure_at::date, o.delivery_date) AS dag,
30847
+ s.print_plek_id,
30848
+ s.breedte_mm, -- leeg = maat onbekend (apart geteld)
30849
+ s.hoogte_mm,
30850
+ sum(greatest(coalesce(s.kopieen, 1), 1))::integer AS aantal
30851
+ FROM public.logistics_sticker_prints s
30852
+ LEFT JOIN public.order_carts c ON c.id = s.cart_id
30853
+ LEFT JOIN public.orders o ON o.id = s.order_id
30854
+ WHERE s.status NOT IN ('niet_printen', 'afgemeld')
30855
+ AND coalesce(c.departure_at::date, o.delivery_date) BETWEEN p_van AND p_tot
30856
+ GROUP BY 1, 2, 3, 4
30857
+ ORDER BY 1, 2, 3, 4;
30858
+ END $function$;
30859
+ REVOKE ALL ON FUNCTION public.logistics_sticker_maten_per_dag(date, date) FROM PUBLIC, anon;
30860
+ GRANT EXECUTE ON FUNCTION public.logistics_sticker_maten_per_dag(date, date) TO authenticated, service_role;
30861
+
30862
+ -- C: wat wacht op bevestiging van de belading (per order en verpakking).
30863
+ CREATE OR REPLACE FUNCTION public.logistics_belading_wacht(p_order_id uuid DEFAULT NULL)
30864
+ RETURNS TABLE(order_id uuid, order_ref text, product text, sleutel text, redenen text[], sinds timestamptz)
30865
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public'
30866
+ AS $function$
30867
+ BEGIN
30868
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_belading_wacht');
30869
+ RETURN QUERY
30870
+ SELECT DISTINCT ON (x.oid, x.sleutel)
30871
+ x.oid, o.order_number::text, x.product, x.sleutel, x.redenen, x.sinds
30872
+ FROM (
30873
+ SELECT public.logistics_try_uuid(c->>'order_id') AS oid,
30874
+ c->>'product' AS product,
30875
+ c->>'sleutel' AS sleutel,
30876
+ ARRAY(SELECT jsonb_array_elements_text(coalesce(c->'redenen', '[]'::jsonb))) AS redenen,
30877
+ e.created_at AS sinds
30878
+ FROM public.logistics_events e,
30879
+ LATERAL jsonb_path_query(e.payload, '$.**.bevestigen[*]') c
30880
+ WHERE e.event_type = 'plan.blocked'
30881
+ AND e.created_at > now() - interval '14 days'
30882
+ ) x
30883
+ JOIN public.orders o ON o.id = x.oid
30884
+ WHERE x.oid IS NOT NULL AND coalesce(x.sleutel, '') <> ''
30885
+ AND (p_order_id IS NULL OR x.oid = p_order_id)
30886
+ AND EXISTS (SELECT 1 FROM public.logistics_plan_queue q
30887
+ WHERE q.order_id = x.oid AND q.status = 'geblokkeerd'
30888
+ AND q.reason_code = 'belading_onwaarschijnlijk')
30889
+ AND NOT EXISTS (SELECT 1 FROM public.logistics_belading_bevestigingen b
30890
+ WHERE b.order_id = x.oid AND b.verpakking_sleutel = x.sleutel)
30891
+ ORDER BY x.oid, x.sleutel, x.sinds DESC;
30892
+ END $function$;
30893
+ REVOKE ALL ON FUNCTION public.logistics_belading_wacht(uuid) FROM PUBLIC, anon;
30894
+ GRANT EXECUTE ON FUNCTION public.logistics_belading_wacht(uuid) TO authenticated, service_role;
30895
+
30896
+ CREATE OR REPLACE FUNCTION public.logistics_check_190()
30897
+ RETURNS TABLE(onderdeel text, ok boolean, toelichting text)
30898
+ LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public'
30899
+ AS $function$
30900
+ SELECT 'functie:' || f, to_regprocedure(f) IS NOT NULL,
30901
+ CASE WHEN to_regprocedure(f) IS NOT NULL THEN 'aanwezig' ELSE 'ontbreekt (schemasync controleren)' END
30902
+ FROM unnest(ARRAY[
30903
+ 'public.logistics_sticker_maten_per_dag(date,date)',
30904
+ 'public.logistics_belading_wacht(uuid)',
30905
+ 'public.logistics_overload_check()',
30906
+ 'public.logistics_cart_sticker_state(uuid)']) f;
30907
+ $function$;
30908
+ REVOKE ALL ON FUNCTION public.logistics_check_190() FROM PUBLIC, anon;
30909
+ GRANT EXECUTE ON FUNCTION public.logistics_check_190() TO authenticated, service_role;
30910
+
30911
+ DO $ic190b$
30912
+ DECLARE v_def text;
30913
+ BEGIN
30914
+ v_def := pg_get_functiondef('public.logistics_install_check()'::regprocedure);
30915
+ IF v_def NOT LIKE '%logistics_check_190()%' THEN
30916
+ v_def := regexp_replace(v_def, 'END \$function\$\s*$', E' RETURN QUERY SELECT * FROM public.logistics_check_190();\nEND $function$');
30917
+ EXECUTE v_def;
30918
+ END IF;
30919
+ END $ic190b$;
30920
+
30921
+ -- ===== 1.0.190 deel 3 =====
30922
+ -- Logistiek Engine 1.0.190 deel 3: plausibiliteit-waarschuwingen, lagen, opnieuw combineren,
30923
+ -- correctiefuncties, pickstation-voorbeeld, aanmeldmoment. Herhaalbaar; geen datacorrecties.
30924
+
30925
+ ALTER TABLE public.order_cart_items ADD COLUMN IF NOT EXISTS lagen numeric;
30926
+ COMMENT ON COLUMN public.order_cart_items.layer_fraction IS 'Vulling van één laag (0–1).';
30927
+ COMMENT ON COLUMN public.order_cart_items.lagen IS 'Aantal lagen = fusten ÷ fusten per laag (mag groter zijn dan 1).';
30928
+
30929
+ CREATE TABLE IF NOT EXISTS public.logistics_belading_waarschuwingen (
30930
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
30931
+ order_id uuid NOT NULL,
30932
+ sleutel text NOT NULL,
30933
+ tekst text NOT NULL,
30934
+ created_at timestamptz NOT NULL DEFAULT now(),
30935
+ updated_at timestamptz NOT NULL DEFAULT now(),
30936
+ UNIQUE (order_id, sleutel)
30937
+ );
30938
+ GRANT SELECT ON public.logistics_belading_waarschuwingen TO authenticated;
30939
+ GRANT ALL ON public.logistics_belading_waarschuwingen TO service_role;
30940
+ ALTER TABLE public.logistics_belading_waarschuwingen ENABLE ROW LEVEL SECURITY;
30941
+ DROP POLICY IF EXISTS "Ingelogd mag beladingwaarschuwingen lezen" ON public.logistics_belading_waarschuwingen;
30942
+ CREATE POLICY "Ingelogd mag beladingwaarschuwingen lezen" ON public.logistics_belading_waarschuwingen
30943
+ FOR SELECT TO authenticated USING (true);
30944
+
30945
+ CREATE OR REPLACE FUNCTION public.logistics_belading_waarschuwingen_zet(p_order_ids uuid[], p_items jsonb)
30946
+ RETURNS integer LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$
30947
+ DECLARE v_n integer := 0;
30948
+ BEGIN
30949
+ PERFORM public.logistics_acc_guard('server', 'logistics_belading_waarschuwingen_zet');
30950
+ IF p_order_ids IS NULL OR array_length(p_order_ids, 1) IS NULL THEN RETURN 0; END IF;
30951
+ DELETE FROM public.logistics_belading_waarschuwingen w
30952
+ WHERE w.order_id = ANY(p_order_ids)
30953
+ AND NOT EXISTS (SELECT 1 FROM jsonb_array_elements(coalesce(p_items, '[]'::jsonb)) i
30954
+ WHERE (i.value->>'order_id')::uuid = w.order_id AND i.value->>'sleutel' = w.sleutel);
30955
+ INSERT INTO public.logistics_belading_waarschuwingen (order_id, sleutel, tekst)
30956
+ SELECT DISTINCT ON ((i.value->>'order_id')::uuid, i.value->>'sleutel')
30957
+ (i.value->>'order_id')::uuid, i.value->>'sleutel', left(i.value->>'tekst', 500)
30958
+ FROM jsonb_array_elements(coalesce(p_items, '[]'::jsonb)) i
30959
+ WHERE (i.value->>'order_id') IS NOT NULL AND (i.value->>'order_id')::uuid = ANY(p_order_ids)
30960
+ ON CONFLICT (order_id, sleutel) DO UPDATE SET tekst = EXCLUDED.tekst, updated_at = now()
30961
+ WHERE public.logistics_belading_waarschuwingen.tekst IS DISTINCT FROM EXCLUDED.tekst;
30962
+ GET DIAGNOSTICS v_n = ROW_COUNT;
30963
+ RETURN v_n;
30964
+ END $$;
30965
+ REVOKE ALL ON FUNCTION public.logistics_belading_waarschuwingen_zet(uuid[], jsonb) FROM PUBLIC, anon, authenticated;
30966
+ GRANT EXECUTE ON FUNCTION public.logistics_belading_waarschuwingen_zet(uuid[], jsonb) TO service_role;
30967
+
30968
+ CREATE OR REPLACE FUNCTION public.logistics_apply_cart_swap(p_delete_ids uuid[], p_new_carts jsonb)
30969
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
30970
+ AS $function$
30971
+ DECLARE
30972
+ v_del uuid[] := COALESCE(p_delete_ids, ARRAY[]::uuid[]);
30973
+ v_cart jsonb; v_row jsonb; v_id uuid; v_map jsonb := '[]'::jsonb;
30974
+ v_jobs int := 0; v_carts int := 0; v_items int := 0; v_locked int;
30975
+ BEGIN
30976
+ PERFORM public.logistics_acc_guard('server', 'logistics_apply_cart_swap');
30977
+ IF jsonb_typeof(COALESCE(p_new_carts, '[]'::jsonb)) <> 'array' THEN
30978
+ RAISE EXCEPTION 'Nieuwe karren ontbreken of zijn ongeldig.';
30979
+ END IF;
30980
+ IF EXISTS (
30981
+ SELECT 1 FROM jsonb_array_elements(COALESCE(p_new_carts, '[]'::jsonb)) c,
30982
+ jsonb_array_elements(COALESCE(c.value->'items', '[]'::jsonb)) i
30983
+ WHERE coalesce((i.value->>'plants_per_crate')::numeric, 0) <= 0
30984
+ OR coalesce((i.value->>'crates')::numeric, 0) < 0) THEN
30985
+ RAISE EXCEPTION 'Karwissel geweigerd: een karregel mist planten per fust (belading_onvolledig).';
30986
+ END IF;
30987
+ IF EXISTS (SELECT 1 FROM jsonb_array_elements(COALESCE(p_new_carts, '[]'::jsonb)) c
30988
+ WHERE nullif(btrim(coalesce(c.value->'row'->>'carrier_type', '')), '') IS NULL) THEN
30989
+ RAISE EXCEPTION 'Karwissel geweigerd: een kar heeft geen ladingdrager (Ladingdrager onbekend). Er is niets gewijzigd.';
30990
+ END IF;
30991
+ IF EXISTS (
30992
+ SELECT 1 FROM jsonb_array_elements(COALESCE(p_new_carts, '[]'::jsonb)) c,
30993
+ jsonb_array_elements(COALESCE(c.value->'items', '[]'::jsonb)) i
30994
+ WHERE coalesce((i.value->>'crates_per_layer')::numeric, 0) <= 0
30995
+ OR coalesce((i.value->>'layers_per_cart')::numeric, 0) <= 0) THEN
30996
+ RAISE EXCEPTION 'Karwissel geweigerd: een karregel mist fusten per laag of lagen (belading_onvolledig). Er is niets gewijzigd.';
30997
+ END IF;
30998
+
30999
+ IF array_length(v_del, 1) IS NOT NULL THEN
31000
+ PERFORM 1 FROM public.order_carts WHERE id = ANY(v_del) ORDER BY id FOR UPDATE;
31001
+ SELECT count(*) INTO v_locked FROM public.order_carts WHERE id = ANY(v_del) AND COALESCE(pickup_locked, false);
31002
+ IF v_locked > 0 THEN
31003
+ RAISE EXCEPTION 'Karwissel geweigerd: % kar(ren) zijn al vastgezet (opgehaald). Er is niets gewijzigd.', v_locked;
31004
+ END IF;
31005
+ UPDATE public.print_jobs
31006
+ SET status = 'geannuleerd'::print_job_status,
31007
+ foutmelding = 'Vervallen: kar verwijderd bij herindeling',
31008
+ updated_at = now()
31009
+ WHERE status IN ('wachtend'::print_job_status, 'bezig'::print_job_status)
31010
+ AND (payload->>'cart_id') = ANY(SELECT id::text FROM unnest(v_del) AS id);
31011
+ GET DIAGNOSTICS v_jobs = ROW_COUNT;
31012
+ DELETE FROM public.pakbon_barcode_inhoud WHERE cart_id = ANY(SELECT id::text FROM unnest(v_del) AS id);
31013
+ DELETE FROM public.order_cart_layer_positions WHERE cart_id = ANY(v_del);
31014
+ DELETE FROM public.order_cart_items WHERE cart_id = ANY(v_del);
31015
+ DELETE FROM public.order_carts WHERE id = ANY(v_del);
31016
+ GET DIAGNOSTICS v_carts = ROW_COUNT;
31017
+ END IF;
31018
+
31019
+ FOR v_cart IN SELECT value FROM jsonb_array_elements(COALESCE(p_new_carts, '[]'::jsonb)) LOOP
31020
+ v_row := v_cart->'row';
31021
+ v_id := gen_random_uuid();
31022
+ INSERT INTO public.order_carts (id, order_id, buffer_id, cart_number, total_carts, is_auto, is_complete, source,
31023
+ occupancy_pct, total_layers_used, carrier_type, carrier_type_id, max_layers, weight_kg, algorithm_version, layout_method)
31024
+ SELECT v_id, r.order_id, r.buffer_id, r.cart_number, r.total_carts, COALESCE(r.is_auto, true), COALESCE(r.is_complete, false),
31025
+ COALESCE(r.source, 'auto'), COALESCE(r.occupancy_pct, 0), COALESCE(r.total_layers_used, 0),
31026
+ r.carrier_type, r.carrier_type_id, r.max_layers, r.weight_kg, COALESCE(r.algorithm_version, 1), r.layout_method
31027
+ FROM jsonb_to_record(v_row) AS r(order_id uuid, buffer_id uuid, cart_number integer, total_carts integer, is_auto boolean,
31028
+ is_complete boolean, source text, occupancy_pct numeric, total_layers_used numeric, carrier_type text, carrier_type_id uuid,
31029
+ max_layers integer, weight_kg numeric, algorithm_version integer, layout_method text);
31030
+
31031
+ INSERT INTO public.order_cart_items (cart_id, order_item_id, product_id, product_name, crates, boxes, weight_kg,
31032
+ plants_per_crate, crates_per_layer, layers_per_cart, layer_fraction, lagen, layer_share_pct, layer_index, crates_in_layer, components)
31033
+ SELECT v_id, i.order_item_id, i.product_id, i.product_name, i.crates, i.boxes, i.weight_kg,
31034
+ i.plants_per_crate, i.crates_per_layer, i.layers_per_cart,
31035
+ least(1, greatest(0, COALESCE(i.layer_fraction, 0))),
31036
+ COALESCE(i.lagen, round(i.crates::numeric / nullif(i.crates_per_layer, 0), 3)),
31037
+ COALESCE(i.layer_share_pct, 0), i.layer_index, i.crates_in_layer,
31038
+ CASE WHEN jsonb_typeof(i.components) = 'array' THEN i.components END
31039
+ FROM jsonb_to_recordset(COALESCE(v_cart->'items', '[]'::jsonb)) AS i(order_item_id uuid, product_id uuid, product_name text,
31040
+ crates integer, boxes integer, weight_kg numeric, plants_per_crate integer, crates_per_layer integer, layers_per_cart integer,
31041
+ layer_fraction numeric, lagen numeric, layer_share_pct numeric, layer_index integer, crates_in_layer integer, components jsonb);
31042
+ v_items := v_items + jsonb_array_length(COALESCE(v_cart->'items', '[]'::jsonb));
31043
+ v_map := v_map || jsonb_build_object('planIndex', (v_cart->>'planIndex')::integer, 'id', v_id);
31044
+ END LOOP;
31045
+
31046
+ RETURN jsonb_build_object('karren', v_map, 'verwijderd', v_carts, 'printopdrachtenGeannuleerd', v_jobs, 'regels', v_items);
31047
+ END;
31048
+ $function$;
31049
+
31050
+ CREATE OR REPLACE FUNCTION public.logistics_rebuffer_aan()
31051
+ RETURNS boolean LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
31052
+ DECLARE v_settings jsonb;
31053
+ BEGIN
31054
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_rebuffer_aan');
31055
+ SELECT settings INTO v_settings FROM public.logistics_rule_configs
31056
+ WHERE is_active ORDER BY updated_at DESC LIMIT 1;
31057
+ RETURN public.logistics_rule_bool(coalesce(v_settings, '{}'::jsonb), 'order_verwerking', 'rebuffer_on_change', true);
31058
+ END $$;
31059
+ REVOKE ALL ON FUNCTION public.logistics_rebuffer_aan() FROM PUBLIC, anon;
31060
+ GRANT EXECUTE ON FUNCTION public.logistics_rebuffer_aan() TO authenticated, service_role;
31061
+
31062
+ CREATE OR REPLACE FUNCTION public.logistics_ensure_rebuffer_setting()
31063
+ RETURNS integer LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$
31064
+ DECLARE v_n integer := 0;
31065
+ BEGIN
31066
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_ensure_rebuffer_setting');
31067
+ UPDATE public.logistics_rule_configs c
31068
+ SET settings = jsonb_set(coalesce(c.settings, '{}'::jsonb), '{order_verwerking}',
31069
+ coalesce(c.settings->'order_verwerking', '{}'::jsonb)
31070
+ || jsonb_build_object('fields', coalesce(c.settings->'order_verwerking'->'fields', '{}'::jsonb)
31071
+ || jsonb_build_object('rebuffer_on_change', true)), true)
31072
+ WHERE c.id IS NOT NULL
31073
+ AND NOT coalesce(c.settings->'order_verwerking'->'fields' ? 'rebuffer_on_change', false)
31074
+ AND NOT coalesce(c.settings->'order_verwerking' ? 'rebuffer_on_change', false);
31075
+ GET DIAGNOSTICS v_n = ROW_COUNT;
31076
+ RETURN v_n;
31077
+ END $$;
31078
+ REVOKE ALL ON FUNCTION public.logistics_ensure_rebuffer_setting() FROM PUBLIC, anon;
31079
+ GRANT EXECUTE ON FUNCTION public.logistics_ensure_rebuffer_setting() TO authenticated, service_role;
31080
+
31081
+ CREATE OR REPLACE FUNCTION public.logistics_rebuffer_orders(p_days integer DEFAULT 30)
31082
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
31083
+ AS $function$
31084
+ DECLARE
31085
+ v_mode text; v_checked int := 0; v_updated int := 0; v_would int := 0; v_vast int := 0;
31086
+ v_start timestamptz := clock_timestamp(); v_ms int; r record; v_after uuid; v_report jsonb;
31087
+ BEGIN
31088
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_rebuffer_orders');
31089
+ v_mode := CASE WHEN public.logistics_rebuffer_aan() THEN 'apply' ELSE 'report' END;
31090
+ BEGIN
31091
+ CREATE TEMP TABLE _rb_sim ON COMMIT DROP AS SELECT * FROM public.logistics_rebuffer_simulate_open();
31092
+ EXCEPTION WHEN OTHERS THEN
31093
+ UPDATE public.logistics_rebuffer_state SET pending = false, last_run_at = now(),
31094
+ last_error = left(SQLERRM, 300), updated_at = now() WHERE id;
31095
+ RAISE;
31096
+ END;
31097
+ SELECT count(*), count(*) FILTER (WHERE changed),
31098
+ coalesce(jsonb_agg(jsonb_build_object('orderId', s.id, 'orderNumber', s.order_number, 'deliveryDate', s.delivery_date,
31099
+ 'huidig', s.cur_label, 'nieuw', s.new_label, 'wijzigt', s.changed, 'reden', s.reason) ORDER BY s.delivery_date, s.order_number), '[]'::jsonb)
31100
+ INTO v_checked, v_would, v_report FROM _rb_sim s;
31101
+ IF v_mode = 'apply' THEN
31102
+ FOR r IN SELECT s.id, s.buffer_id, s.order_number FROM _rb_sim s WHERE s.changed LOOP
31103
+ IF coalesce(public.logistics_order_released(r.order_number), false)
31104
+ OR EXISTS (SELECT 1 FROM public.pick_queue q
31105
+ WHERE q.order_number = r.order_number AND q.cancelled_at IS NULL AND q.completed_at IS NULL)
31106
+ OR coalesce(public.logistics_buffer_frozen_order(r.id), false) THEN
31107
+ v_vast := v_vast + 1;
31108
+ CONTINUE;
31109
+ END IF;
31110
+ v_after := public.logistics_apply_buffer_decision(r.id);
31111
+ IF v_after IS DISTINCT FROM r.buffer_id THEN v_updated := v_updated + 1; END IF;
31112
+ END LOOP;
31113
+ END IF;
31114
+ DROP TABLE IF EXISTS _rb_sim;
31115
+ v_ms := greatest(0, (extract(epoch from (clock_timestamp() - v_start)) * 1000)::int);
31116
+ UPDATE public.logistics_rebuffer_state
31117
+ SET pending = false, last_run_at = now(), last_checked = v_checked, last_updated = v_updated,
31118
+ last_would_change = v_would, last_mode = v_mode, last_duration_ms = v_ms,
31119
+ last_error = NULL, last_report = v_report, updated_at = now()
31120
+ WHERE id;
31121
+ RETURN jsonb_build_object('checked', v_checked, 'updated', v_updated, 'durationMs', v_ms,
31122
+ 'mode', v_mode, 'wouldChange', v_would, 'vastgezet', v_vast,
31123
+ 'scope', 'leverdag_vanaf_vandaag_open');
31124
+ END $function$;
31125
+
31126
+ CREATE OR REPLACE FUNCTION public.trg_logistics_rule_config_rebuffer()
31127
+ RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
31128
+ AS $function$
31129
+ BEGIN
31130
+ UPDATE public.logistics_rebuffer_state
31131
+ SET pending = true, requested_at = now(), updated_at = now()
31132
+ WHERE id;
31133
+ BEGIN
31134
+ PERFORM public.logistics_rebuffer_orders(30);
31135
+ EXCEPTION WHEN others THEN
31136
+ UPDATE public.logistics_rebuffer_state
31137
+ SET last_error = left('Opnieuw combineren mislukt: ' || SQLERRM, 300), updated_at = now()
31138
+ WHERE id;
31139
+ RAISE WARNING 'logistics_rebuffer_orders mislukt: %', SQLERRM;
31140
+ END;
31141
+ RETURN NEW;
31142
+ END;
31143
+ $function$;
31144
+
31145
+ CREATE TABLE IF NOT EXISTS public.logistics_correctie_log (
31146
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
31147
+ correctie text NOT NULL,
31148
+ object_id uuid,
31149
+ omschrijving text,
31150
+ oud jsonb,
31151
+ nieuw jsonb,
31152
+ created_at timestamptz NOT NULL DEFAULT now()
31153
+ );
31154
+ GRANT SELECT ON public.logistics_correctie_log TO authenticated;
31155
+ GRANT ALL ON public.logistics_correctie_log TO service_role;
31156
+ ALTER TABLE public.logistics_correctie_log ENABLE ROW LEVEL SECURITY;
31157
+ DROP POLICY IF EXISTS "Beheer mag correctielog lezen" ON public.logistics_correctie_log;
31158
+ CREATE POLICY "Beheer mag correctielog lezen" ON public.logistics_correctie_log
31159
+ FOR SELECT TO authenticated USING (public.logistics_is_beheer());
31160
+
31161
+ CREATE OR REPLACE FUNCTION public.logistics_correctie_buffers_geannuleerd(p_uitvoeren boolean DEFAULT false)
31162
+ RETURNS TABLE(order_id uuid, order_number text, reden text, buffer_id uuid, actieve_karren integer,
31163
+ andere_open_orders text, uitgevoerd boolean)
31164
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$
31165
+ #variable_conflict use_column
31166
+ DECLARE r record;
31167
+ BEGIN
31168
+ PERFORM public.logistics_acc_guard('server', 'logistics_correctie_buffers_geannuleerd');
31169
+ FOR r IN
31170
+ SELECT o.id, o.order_number, o.buffer_id,
31171
+ CASE WHEN o.deleted_at IS NOT NULL THEN 'verwijderd' ELSE 'geannuleerd' END AS reden,
31172
+ (SELECT count(*)::int FROM public.order_carts c WHERE c.order_id = o.id AND c.archived_at IS NULL) AS karren,
31173
+ (SELECT string_agg(x.order_number, ' ' ORDER BY x.order_number) FROM public.orders x
31174
+ WHERE x.buffer_id = o.buffer_id AND x.id <> o.id AND x.deleted_at IS NULL AND x.cancelled_at IS NULL) AS anderen
31175
+ FROM public.orders o
31176
+ WHERE o.buffer_id IS NOT NULL AND (o.deleted_at IS NOT NULL OR o.cancelled_at IS NOT NULL)
31177
+ ORDER BY o.order_number
31178
+ LOOP
31179
+ order_id := r.id; order_number := r.order_number; reden := r.reden; buffer_id := r.buffer_id;
31180
+ actieve_karren := r.karren; andere_open_orders := r.anderen; uitgevoerd := false;
31181
+ IF p_uitvoeren AND r.karren = 0 THEN
31182
+ PERFORM set_config('logistics.buffer_writer', 'engine', true);
31183
+ UPDATE public.orders SET buffer_id = NULL WHERE id = r.id AND buffer_id = r.buffer_id;
31184
+ INSERT INTO public.logistics_correctie_log (correctie, object_id, omschrijving, oud, nieuw)
31185
+ VALUES ('buffers_geannuleerd', r.id, r.order_number || ' (' || r.reden || ') losgemaakt uit buffer',
31186
+ jsonb_build_object('buffer_id', r.buffer_id), jsonb_build_object('buffer_id', NULL));
31187
+ IF r.anderen IS NOT NULL THEN
31188
+ BEGIN
31189
+ PERFORM public.logistics_plan_enqueue(p_order_id => x.id, p_reason => 'correctie_buffers_geannuleerd')
31190
+ FROM public.orders x WHERE x.buffer_id = r.buffer_id AND x.deleted_at IS NULL AND x.cancelled_at IS NULL;
31191
+ EXCEPTION WHEN undefined_function THEN NULL;
31192
+ END;
31193
+ END IF;
31194
+ uitgevoerd := true;
31195
+ END IF;
31196
+ RETURN NEXT;
31197
+ END LOOP;
31198
+ END $$;
31199
+ REVOKE ALL ON FUNCTION public.logistics_correctie_buffers_geannuleerd(boolean) FROM PUBLIC, anon, authenticated;
31200
+ GRANT EXECUTE ON FUNCTION public.logistics_correctie_buffers_geannuleerd(boolean) TO service_role;
31201
+
31202
+ CREATE OR REPLACE FUNCTION public.logistics_correctie_laagaandeel(p_uitvoeren boolean DEFAULT false)
31203
+ RETURNS TABLE(item_id uuid, cart_id uuid, orders text, fusten integer, fusten_per_laag integer,
31204
+ huidig numeric, nieuw_layer_fraction numeric, nieuw_lagen numeric, uitgevoerd boolean)
31205
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$
31206
+ DECLARE r record;
31207
+ BEGIN
31208
+ PERFORM public.logistics_acc_guard('server', 'logistics_correctie_laagaandeel');
31209
+ FOR r IN
31210
+ SELECT ci.id, ci.cart_id, ci.crates, ci.crates_per_layer, ci.layer_fraction, ci.lagen,
31211
+ least(1, round(ci.crates::numeric / ci.crates_per_layer, 3)) AS nf,
31212
+ round(ci.crates::numeric / ci.crates_per_layer, 3) AS nl,
31213
+ (SELECT string_agg(DISTINCT o.order_number, ' ') FROM public.order_cart_items x
31214
+ JOIN public.order_items oi ON oi.id = x.order_item_id JOIN public.orders o ON o.id = oi.order_id
31215
+ WHERE x.cart_id = ci.cart_id) AS ords
31216
+ FROM public.order_cart_items ci
31217
+ JOIN public.order_carts c ON c.id = ci.cart_id
31218
+ WHERE c.archived_at IS NULL AND NOT coalesce(c.pickup_locked, false)
31219
+ AND ci.crates_per_layer > 0 AND ci.layer_index IS NULL
31220
+ AND (ci.layer_fraction IS DISTINCT FROM least(1, round(ci.crates::numeric / ci.crates_per_layer, 3))
31221
+ OR ci.lagen IS DISTINCT FROM round(ci.crates::numeric / ci.crates_per_layer, 3))
31222
+ AND NOT EXISTS (SELECT 1 FROM public.print_jobs j WHERE j.status = 'voltooid' AND j.payload->>'cart_id' = c.id::text)
31223
+ AND NOT EXISTS (SELECT 1 FROM public.logistics_control_scan s WHERE s.cart_id = c.id AND s.status <> 'geannuleerd')
31224
+ ORDER BY ci.cart_id, ci.id
31225
+ LOOP
31226
+ item_id := r.id; cart_id := r.cart_id; orders := r.ords; fusten := r.crates; fusten_per_laag := r.crates_per_layer;
31227
+ huidig := r.layer_fraction; nieuw_layer_fraction := r.nf; nieuw_lagen := r.nl; uitgevoerd := false;
31228
+ IF p_uitvoeren THEN
31229
+ UPDATE public.order_cart_items SET layer_fraction = r.nf, lagen = r.nl WHERE id = r.id;
31230
+ INSERT INTO public.logistics_correctie_log (correctie, object_id, omschrijving, oud, nieuw)
31231
+ VALUES ('laagaandeel', r.id, 'karregel van kar ' || r.cart_id,
31232
+ jsonb_build_object('layer_fraction', r.layer_fraction, 'lagen', r.lagen),
31233
+ jsonb_build_object('layer_fraction', r.nf, 'lagen', r.nl));
31234
+ uitgevoerd := true;
31235
+ END IF;
31236
+ RETURN NEXT;
31237
+ END LOOP;
31238
+ END $$;
31239
+ REVOKE ALL ON FUNCTION public.logistics_correctie_laagaandeel(boolean) FROM PUBLIC, anon, authenticated;
31240
+ GRANT EXECUTE ON FUNCTION public.logistics_correctie_laagaandeel(boolean) TO service_role;
31241
+
31242
+ CREATE OR REPLACE FUNCTION public.logistics_pick_voorbeeld_layout(p_queue_ids uuid[])
31243
+ RETURNS TABLE(queue_id uuid, documentsoort text, layout_id uuid, layout_naam text, breedte_mm numeric,
31244
+ hoogte_mm numeric, ok boolean, reden text)
31245
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
31246
+ DECLARE r record; v_res jsonb;
31247
+ BEGIN
31248
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_pick_voorbeeld_layout');
31249
+ FOR r IN
31250
+ SELECT q.id, t.code, (SELECT o.id FROM public.orders o WHERE o.order_number = q.order_number AND o.deleted_at IS NULL LIMIT 1) AS oid
31251
+ FROM public.pick_queue q LEFT JOIN public.pick_document_types t ON t.id = q.document_type_id
31252
+ WHERE q.id = ANY(coalesce(p_queue_ids, '{}'))
31253
+ LOOP
31254
+ queue_id := r.id; layout_id := NULL; layout_naam := NULL; breedte_mm := NULL; hoogte_mm := NULL;
31255
+ IF r.oid IS NULL OR r.code IS NULL THEN
31256
+ documentsoort := r.code; ok := false;
31257
+ reden := CASE WHEN r.code IS NULL THEN 'Documentsoort van deze regel is onbekend.' ELSE 'Order van deze regel is niet gevonden.' END;
31258
+ ELSE
31259
+ v_res := public.logistics_document_for_order(p_order_id => r.oid, p_documenttype => r.code);
31260
+ documentsoort := coalesce(v_res->>'documenttype', r.code);
31261
+ ok := coalesce((v_res->>'ok')::boolean, false);
31262
+ reden := v_res->>'reden';
31263
+ layout_id := nullif(v_res->>'layout_id', '')::uuid;
31264
+ breedte_mm := nullif(v_res->>'breedte_mm', '')::numeric;
31265
+ hoogte_mm := nullif(v_res->>'hoogte_mm', '')::numeric;
31266
+ IF layout_id IS NOT NULL THEN
31267
+ SELECT l.naam INTO layout_naam FROM public.document_layouts l WHERE l.id = layout_id;
31268
+ END IF;
31269
+ END IF;
31270
+ RETURN NEXT;
31271
+ END LOOP;
31272
+ END $$;
31273
+ REVOKE ALL ON FUNCTION public.logistics_pick_voorbeeld_layout(uuid[]) FROM PUBLIC, anon;
31274
+ GRANT EXECUTE ON FUNCTION public.logistics_pick_voorbeeld_layout(uuid[]) TO authenticated, service_role;
31275
+
31276
+ CREATE OR REPLACE FUNCTION public.logistics_aanmeldmoment(p_order_ids uuid[])
31277
+ RETURNS TABLE(order_id uuid, aanmeld_at timestamptz, bron text, tekst text)
31278
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
31279
+ DECLARE v_tz text; r record; v_fout text;
31280
+ BEGIN
31281
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_aanmeldmoment');
31282
+ BEGIN v_tz := public.logistics_opening_timezone(); EXCEPTION WHEN others THEN v_tz := NULL; END;
31283
+ IF to_regprocedure('public.afh_signup_moment_for_orders(uuid[])') IS NOT NULL THEN
31284
+ BEGIN
31285
+ FOR r IN EXECUTE 'SELECT order_id, aanmeld_at, bron FROM public.afh_signup_moment_for_orders(p_order_ids => $1)'
31286
+ USING p_order_ids
31287
+ LOOP
31288
+ order_id := r.order_id; aanmeld_at := r.aanmeld_at; bron := r.bron;
31289
+ tekst := CASE WHEN r.aanmeld_at IS NOT NULL AND v_tz IS NOT NULL AND r.bron IN ('verstuurd', 'gepland')
31290
+ THEN 'Verzendbrief komt vrij na de transportaanmelding om ' || to_char(r.aanmeld_at AT TIME ZONE v_tz, 'HH24:MI')
31291
+ ELSE 'Tijdstip van de transportaanmelding is nog niet bekend' END;
31292
+ RETURN NEXT;
31293
+ END LOOP;
31294
+ RETURN;
31295
+ EXCEPTION WHEN insufficient_privilege THEN v_fout := '42501';
31296
+ WHEN others THEN v_fout := SQLSTATE;
31297
+ END;
31298
+ END IF;
31299
+ FOR r IN SELECT unnest(coalesce(p_order_ids, '{}')) AS id LOOP
31300
+ order_id := r.id; aanmeld_at := NULL; bron := coalesce('fout_' || v_fout, 'onbekend');
31301
+ tekst := 'Tijdstip van de transportaanmelding is nog niet bekend';
31302
+ RETURN NEXT;
31303
+ END LOOP;
31304
+ END $$;
31305
+ REVOKE ALL ON FUNCTION public.logistics_aanmeldmoment(uuid[]) FROM PUBLIC, anon;
31306
+ GRANT EXECUTE ON FUNCTION public.logistics_aanmeldmoment(uuid[]) TO authenticated, service_role;
31307
+
31308
+ CREATE OR REPLACE FUNCTION public.logistics_check_190()
31309
+ RETURNS TABLE(onderdeel text, ok boolean, toelichting text)
31310
+ LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public'
31311
+ AS $function$
31312
+ SELECT 'functie:' || f, to_regprocedure(f) IS NOT NULL,
31313
+ CASE WHEN to_regprocedure(f) IS NOT NULL THEN 'aanwezig' ELSE 'ontbreekt (schemasync controleren)' END
31314
+ FROM unnest(ARRAY[
31315
+ 'public.logistics_sticker_maten_per_dag(date,date)',
31316
+ 'public.logistics_belading_wacht(uuid)',
31317
+ 'public.logistics_overload_check()',
31318
+ 'public.logistics_cart_sticker_state(uuid)',
31319
+ 'public.logistics_belading_waarschuwingen_zet(uuid[],jsonb)',
31320
+ 'public.logistics_rebuffer_aan()',
31321
+ 'public.logistics_ensure_rebuffer_setting()',
31322
+ 'public.logistics_correctie_buffers_geannuleerd(boolean)',
31323
+ 'public.logistics_correctie_laagaandeel(boolean)',
31324
+ 'public.logistics_pick_voorbeeld_layout(uuid[])',
31325
+ 'public.logistics_aanmeldmoment(uuid[])']) f
31326
+ UNION ALL
31327
+ SELECT 'kolom:order_cart_items.lagen',
31328
+ EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'order_cart_items' AND column_name = 'lagen'),
31329
+ 'nieuw veld lagen'
31330
+ UNION ALL
31331
+ SELECT 'tabel:' || t, to_regclass('public.' || t) IS NOT NULL, 'nieuw in 1.0.190'
31332
+ FROM unnest(ARRAY['logistics_belading_waarschuwingen', 'logistics_correctie_log']) t
31333
+ UNION ALL
31334
+ SELECT 'afhandeling:afh_signup_moment_for_orders(uuid[])',
31335
+ to_regprocedure('public.afh_signup_moment_for_orders(uuid[])') IS NOT NULL,
31336
+ 'optioneel: ontbreekt = tekst "nog niet bekend", niets geblokkeerd';
31337
+ $function$;CREATE OR REPLACE FUNCTION public.logistics_shipping_note_status(p_slip_job_id uuid DEFAULT NULL::uuid, p_cart_id uuid DEFAULT NULL::uuid)
31338
+ RETURNS jsonb
31339
+ LANGUAGE plpgsql
31340
+ STABLE SECURITY DEFINER
31341
+ SET search_path TO 'public'
31342
+ AS $function$
31343
+ DECLARE
31344
+ v_note public.logistics_slip_shipping_note;
31345
+ v_slip public.print_jobs;
31346
+ v_job public.print_jobs;
31347
+ v_state text; v_message text; v_cart uuid;
31348
+ v_vrijgave boolean := false; v_scan_ok boolean;
31349
+ v_aanmeld_at timestamptz; v_aanmeld_tekst text; v_order uuid;
31350
+ BEGIN
31351
+ IF p_slip_job_id IS NULL AND p_cart_id IS NULL THEN
31352
+ RETURN jsonb_build_object('status','mislukt','message',
31353
+ 'De pakbon of kar ontbreekt; de verzendbriefstatus kan niet worden opgehaald.','attempts',0,'nextAttemptAt',NULL);
31354
+ END IF;
31355
+ IF p_slip_job_id IS NOT NULL THEN
31356
+ SELECT * INTO v_slip FROM public.print_jobs WHERE id = p_slip_job_id;
31357
+ ELSE
31358
+ SELECT * INTO v_slip FROM public.print_jobs p
31359
+ WHERE lower(coalesce(p.payload->>'document_type_code', p.payload->>'documentType', p.documenttype::text, '')) LIKE '%pakbon%'
31360
+ AND nullif(coalesce(p.payload->>'cart_id', p.payload->'cart'->>'cartId'), '') = p_cart_id::text
31361
+ ORDER BY p.created_at DESC LIMIT 1;
31362
+ END IF;
31363
+ IF v_slip.id IS NULL THEN
31364
+ RETURN jsonb_build_object('status','mislukt','message','De bijbehorende pakbon is niet gevonden.',
31365
+ 'attempts',0,'nextAttemptAt',NULL,'cartId',p_cart_id);
31366
+ END IF;
31367
+ BEGIN
31368
+ v_cart := coalesce(nullif(v_slip.payload->>'cart_id','')::uuid, nullif(v_slip.payload->'cart'->>'cartId','')::uuid, p_cart_id);
31369
+ EXCEPTION WHEN invalid_text_representation THEN v_cart := p_cart_id;
31370
+ END;
31371
+ SELECT * INTO v_note FROM public.logistics_slip_shipping_note s
31372
+ WHERE s.slip_job_id = v_slip.id ORDER BY s.created_at DESC LIMIT 1;
31373
+
31374
+ IF v_note.id IS NULL THEN
31375
+ SELECT * INTO v_job FROM public.print_jobs p
31376
+ WHERE p.idempotentie_sleutel = 'verzendbrief-bij-pakbon:' || v_slip.id::text
31377
+ OR (p.payload->>'packing_slip_job_id' = v_slip.id::text
31378
+ AND lower(coalesce(p.payload->>'document_type_code', p.documenttype::text, '')) ~ '(verzend|aflever)')
31379
+ ORDER BY p.created_at DESC LIMIT 1;
31380
+ IF v_job.id IS NULL THEN
31381
+ IF v_slip.created_at < now() - interval '15 minutes' THEN
31382
+ v_state := 'mislukt';
31383
+ v_message := 'Er is geen verzendbrief bij deze pakbon aangemaakt. Controleer de printwachtrij of print de pakbon opnieuw.';
31384
+ ELSE
31385
+ v_state := 'klaar_voor_print'; v_message := 'Verzendbrief wordt klaargezet.';
31386
+ END IF;
31387
+ ELSIF v_job.status = 'voltooid' THEN
31388
+ v_state := 'geprint'; v_message := 'Pakbon en verzendbrief zijn naar de printer gestuurd.';
31389
+ ELSIF v_job.status IN ('wachtend','bezig') THEN
31390
+ v_state := 'klaar_voor_print'; v_message := 'Verzendbrief wordt klaargezet.';
31391
+ ELSE
31392
+ v_state := 'mislukt'; v_message := 'De verzendbrief kon niet worden geprint. Controleer de printwachtrij.';
31393
+ END IF;
31394
+ RETURN jsonb_build_object('status',v_state,'message',v_message,
31395
+ 'attempts',coalesce(v_job.pogingen,0),'nextAttemptAt',v_job.volgende_poging,
31396
+ 'slipJobId',v_slip.id,'shippingNoteJobId',v_job.id,'cartId',v_cart,
31397
+ 'orderNumber',coalesce(v_slip.payload->>'order_number', v_slip.payload->'data'->>'orderNumber'));
31398
+ END IF;
31399
+
31400
+ IF v_note.print_job_id IS NOT NULL THEN
31401
+ SELECT * INTO v_job FROM public.print_jobs WHERE id = v_note.print_job_id;
31402
+ END IF;
31403
+ v_scan_ok := EXISTS (SELECT 1 FROM public.logistics_control_scan cs
31404
+ WHERE cs.cart_id = coalesce(v_note.cart_id, v_cart) AND cs.approved_at IS NOT NULL);
31405
+
31406
+ IF NOT v_scan_ok OR v_note.status IN ('wacht_op_vrijgave','wachtend') THEN
31407
+ BEGIN
31408
+ v_order := v_note.order_id;
31409
+ IF v_order IS NOT NULL THEN
31410
+ SELECT a.aanmeld_at, a.tekst INTO v_aanmeld_at, v_aanmeld_tekst
31411
+ FROM public.logistics_aanmeldmoment(p_order_ids => ARRAY[v_order]) a LIMIT 1;
31412
+ END IF;
31413
+ EXCEPTION WHEN others THEN v_aanmeld_at := NULL; v_aanmeld_tekst := NULL;
31414
+ END;
31415
+ END IF;
31416
+
31417
+ IF v_note.status = 'mislukt' THEN
31418
+ v_state := 'mislukt'; v_message := 'De officiële Floriday-afleverbrief kon niet worden klaargezet. Controleer de verzendbriefwachtrij.';
31419
+ ELSIF v_note.status = 'wacht_op_vrijgave'
31420
+ OR (v_note.status = 'wachtend' AND v_note.retry_class IN ('wacht_op_vrijgave','wacht_op_kenmerk'))
31421
+ OR (v_note.status = 'wachtend' AND v_note.retry_class IS NULL AND coalesce(v_note.attempts,0) = 0 AND NOT v_scan_ok) THEN
31422
+ v_state := 'wacht_op_floriday'; v_vrijgave := true;
31423
+ v_message := CASE WHEN v_scan_ok
31424
+ THEN 'Pakbon geprint — ' || coalesce(v_aanmeld_tekst, 'Tijdstip van de transportaanmelding is nog niet bekend') || '.'
31425
+ ELSE 'Pakbon geprint — verzendbrief wacht: ' || coalesce(nullif(btrim(v_note.error), ''), 'op een goedgekeurde controlescan.') END;
31426
+ ELSIF v_note.status = 'wachtend' AND v_note.retry_class = 'nog_niet_beschikbaar' THEN
31427
+ v_state := 'wacht_op_floriday'; v_message := 'Pakbon geprint — wachten op officiële Floriday-afleverbrief.';
31428
+ ELSIF v_note.status = 'wachtend' AND v_note.retry_class = 'tijdelijk' AND coalesce(v_note.attempts,0) > 0
31429
+ AND coalesce(v_note.error,'') ~* '(5\d\d|429|time-?out|netwerk|verbinding|niet bereikbaar)' THEN
31430
+ v_state := 'klaar_voor_print'; v_message := 'Floriday is tijdelijk niet bereikbaar. De verzendbrief wordt automatisch opnieuw geprobeerd.';
31431
+ ELSIF v_note.status = 'wachtend' AND v_note.retry_class = 'limiet_bereikt' THEN
31432
+ v_state := 'mislukt'; v_message := 'Het maximale aantal pogingen voor de verzendbrief is bereikt. Controleer de verzendbriefwachtrij.';
31433
+ ELSIF v_note.status = 'wachtend' THEN
31434
+ v_state := 'klaar_voor_print'; v_message := 'Verzendbrief wordt klaargezet.';
31435
+ ELSIF v_job.status = 'voltooid' THEN
31436
+ v_state := 'geprint'; v_message := 'Pakbon en verzendbrief zijn naar de printer gestuurd.';
31437
+ ELSIF v_job.status IN ('mislukt','geannuleerd') THEN
31438
+ v_state := 'mislukt'; v_message := 'De verzendbrief kon niet worden geprint. Controleer de printwachtrij.';
31439
+ ELSE
31440
+ v_state := 'klaar_voor_print'; v_message := 'De officiële Floriday-afleverbrief wordt naar dezelfde printer gestuurd.';
31441
+ END IF;
31442
+
31443
+ RETURN jsonb_build_object('status',v_state,'message',v_message,
31444
+ 'attempts',coalesce(v_note.attempts,0),'nextAttemptAt',v_note.next_attempt_at,
31445
+ 'slipJobId',v_slip.id,'shippingNoteJobId',v_note.print_job_id,
31446
+ 'cartId',coalesce(v_note.cart_id, v_cart),'orderNumber',v_note.order_number,
31447
+ 'wachtOpVrijgave', v_vrijgave,
31448
+ 'wachtReden', CASE WHEN v_vrijgave THEN CASE WHEN v_scan_ok THEN v_message ELSE coalesce(nullif(btrim(v_note.error), ''), v_message) END END,
31449
+ 'wachtCode', CASE WHEN v_vrijgave THEN coalesce(v_note.retry_class, v_note.status) END,
31450
+ 'verwachtOm', CASE WHEN v_vrijgave THEN v_aanmeld_at END,
31451
+ 'aanmeldTekst', CASE WHEN v_vrijgave AND v_scan_ok THEN coalesce(v_aanmeld_tekst, 'Tijdstip van de transportaanmelding is nog niet bekend') END);
31452
+ END;
31453
+ $function$;
31454
+ -- ===== 1.0.190 deel 4 =====
31455
+ -- Logistiek Engine 1.0.190 deel 4: rebuffer zonder 'volle kar = vast', bewaking open orders zonder kar, stickermaten-datum. Herhaalbaar; geen datacorrecties.
31456
+ CREATE OR REPLACE FUNCTION public.logistics_rebuffer_vastgezet(p_order_id uuid)
31457
+ RETURNS boolean LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
31458
+ SELECT public.logistics_acc_guard('server', 'logistics_rebuffer_vastgezet');
31459
+ SELECT EXISTS (SELECT 1 FROM public.orders o WHERE o.id = p_order_id AND o.plan_locked_at IS NOT NULL)
31460
+ OR EXISTS (SELECT 1 FROM public.orders o WHERE o.id = p_order_id
31461
+ AND coalesce(public.logistics_order_released(o.order_number), false))
31462
+ OR EXISTS (SELECT 1 FROM public.orders o JOIN public.pick_queue q ON q.order_number = o.order_number
31463
+ WHERE o.id = p_order_id AND q.cancelled_at IS NULL AND q.completed_at IS NULL)
31464
+ OR EXISTS (
31465
+ SELECT 1 FROM public.orders o
31466
+ JOIN logistics_active.order_carts oc
31467
+ ON oc.buffer_id = o.buffer_id AND (oc.order_id = o.id OR oc.order_id IS NULL)
31468
+ WHERE o.id = p_order_id
31469
+ AND (oc.pickup_locked IS TRUE
31470
+ OR public.logistics_cart_handled(COALESCE(o.order_number, o.id::text), oc.cart_number)
31471
+ OR public.logistics_cart_is_frozen(oc.id)));
31472
+ $$;
31473
+ REVOKE ALL ON FUNCTION public.logistics_rebuffer_vastgezet(uuid) FROM PUBLIC, anon, authenticated;
31474
+ GRANT EXECUTE ON FUNCTION public.logistics_rebuffer_vastgezet(uuid) TO service_role;
31475
+
31476
+ CREATE OR REPLACE FUNCTION public.logistics_rebuffer_orders(p_days integer DEFAULT 30)
31477
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
31478
+ AS $function$
31479
+ DECLARE
31480
+ v_mode text; v_checked int := 0; v_updated int := 0; v_would int := 0; v_vast int := 0;
31481
+ v_start timestamptz := clock_timestamp(); v_ms int; r record; v_after uuid; v_report jsonb;
31482
+ BEGIN
31483
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_rebuffer_orders');
31484
+ v_mode := CASE WHEN public.logistics_rebuffer_aan() THEN 'apply' ELSE 'report' END;
31485
+ BEGIN
31486
+ CREATE TEMP TABLE _rb_sim ON COMMIT DROP AS SELECT * FROM public.logistics_rebuffer_simulate_open();
31487
+ EXCEPTION WHEN OTHERS THEN
31488
+ UPDATE public.logistics_rebuffer_state SET pending = false, last_run_at = now(),
31489
+ last_error = left(SQLERRM, 300), updated_at = now() WHERE id;
31490
+ RAISE;
31491
+ END;
31492
+ SELECT count(*), count(*) FILTER (WHERE changed),
31493
+ coalesce(jsonb_agg(jsonb_build_object('orderId', s.id, 'orderNumber', s.order_number, 'deliveryDate', s.delivery_date,
31494
+ 'huidig', s.cur_label, 'nieuw', s.new_label, 'wijzigt', s.changed, 'reden', s.reason) ORDER BY s.delivery_date, s.order_number), '[]'::jsonb)
31495
+ INTO v_checked, v_would, v_report FROM _rb_sim s;
31496
+ IF v_mode = 'apply' THEN
31497
+ FOR r IN SELECT s.id, s.buffer_id, s.order_number FROM _rb_sim s WHERE s.changed LOOP
31498
+ IF public.logistics_rebuffer_vastgezet(r.id) THEN
31499
+ v_vast := v_vast + 1;
31500
+ CONTINUE;
31501
+ END IF;
31502
+ v_after := public.logistics_apply_buffer_decision(r.id);
31503
+ IF v_after IS DISTINCT FROM r.buffer_id THEN v_updated := v_updated + 1; END IF;
31504
+ END LOOP;
31505
+ END IF;
31506
+ DROP TABLE IF EXISTS _rb_sim;
31507
+ v_ms := greatest(0, (extract(epoch from (clock_timestamp() - v_start)) * 1000)::int);
31508
+ UPDATE public.logistics_rebuffer_state
31509
+ SET pending = false, last_run_at = now(), last_checked = v_checked, last_updated = v_updated,
31510
+ last_would_change = v_would, last_mode = v_mode, last_duration_ms = v_ms,
31511
+ last_error = CASE WHEN v_vast > 0 THEN v_vast || ' order(s) niet verplaatst: vrijgegeven, in de pickrij of geprinte/gescande kar.' END,
31512
+ last_report = v_report, updated_at = now()
31513
+ WHERE id;
31514
+ RETURN jsonb_build_object('checked', v_checked, 'updated', v_updated, 'durationMs', v_ms,
31515
+ 'mode', v_mode, 'wouldChange', v_would, 'vastgezet', v_vast,
31516
+ 'scope', 'leverdag_vanaf_vandaag_open');
31517
+ END $function$;CREATE TABLE IF NOT EXISTS public.logistics_zonder_kar_pogingen (
31518
+ order_id uuid PRIMARY KEY,
31519
+ order_number text,
31520
+ pogingen integer NOT NULL DEFAULT 0,
31521
+ laatste_poging_at timestamptz,
31522
+ gemeld_at timestamptz,
31523
+ opgelost_at timestamptz,
31524
+ created_at timestamptz NOT NULL DEFAULT now(),
31525
+ updated_at timestamptz NOT NULL DEFAULT now()
31526
+ );
31527
+ GRANT SELECT ON public.logistics_zonder_kar_pogingen TO authenticated;
31528
+ GRANT ALL ON public.logistics_zonder_kar_pogingen TO service_role;
31529
+ ALTER TABLE public.logistics_zonder_kar_pogingen ENABLE ROW LEVEL SECURITY;
31530
+ DROP POLICY IF EXISTS "Ingelogd mag zonder-kar-pogingen lezen" ON public.logistics_zonder_kar_pogingen;
31531
+ CREATE POLICY "Ingelogd mag zonder-kar-pogingen lezen" ON public.logistics_zonder_kar_pogingen
31532
+ FOR SELECT TO authenticated USING (true);
31533
+
31534
+ CREATE OR REPLACE FUNCTION public.logistics_open_orders_zonder_kar()
31535
+ RETURNS TABLE(order_id uuid, order_number text, delivery_date text, laatste_reden text)
31536
+ LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
31537
+ SELECT public.logistics_acc_guard('ingelogd', 'logistics_open_orders_zonder_kar');
31538
+ WITH lq AS (
31539
+ SELECT DISTINCT ON (q.order_id) q.order_id, q.status, q.reason_code, q.error
31540
+ FROM public.logistics_plan_queue q ORDER BY q.order_id, q.updated_at DESC
31541
+ )
31542
+ SELECT o.id, o.order_number, o.delivery_date, lq.error
31543
+ FROM public.orders o
31544
+ LEFT JOIN lq ON lq.order_id = o.id
31545
+ WHERE o.deleted_at IS NULL AND o.cancelled_at IS NULL
31546
+ AND lower(coalesce(o.status,'')) NOT IN ('cancelled','geannuleerd','deleted','verwijderd','afgehandeld','completed','delivered','geleverd')
31547
+ AND o.delivery_date ~ '^\d{4}-\d{2}-\d{2}' AND left(o.delivery_date,10)::date >= public.logistics_local_today()
31548
+ AND EXISTS (SELECT 1 FROM public.order_items oi WHERE oi.order_id = o.id)
31549
+ AND NOT coalesce(public.logistics_order_buffer_excluded(o.id), false)
31550
+ AND NOT (coalesce(lq.status,'') IN ('geblokkeerd','pending','running'))
31551
+ AND NOT (coalesce(lq.reason_code,'') IN ('belading_onwaarschijnlijk','belading_onvolledig','belading_onbekend','artikel_kiezen','ladingdrager_onbekend'))
31552
+ AND NOT EXISTS (SELECT 1 FROM logistics_active.order_carts c WHERE c.order_id = o.id)
31553
+ AND NOT EXISTS (SELECT 1 FROM public.order_items oi
31554
+ JOIN logistics_active.order_cart_items ci ON ci.order_item_id = oi.id
31555
+ JOIN logistics_active.order_carts c ON c.id = ci.cart_id
31556
+ WHERE oi.order_id = o.id);
31557
+ $$;
31558
+ REVOKE ALL ON FUNCTION public.logistics_open_orders_zonder_kar() FROM PUBLIC, anon;
31559
+ GRANT EXECUTE ON FUNCTION public.logistics_open_orders_zonder_kar() TO authenticated, service_role;
31560
+
31561
+ CREATE OR REPLACE FUNCTION public.logistics_bewaak_zonder_kar()
31562
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$
31563
+ DECLARE r record; v_nieuw int := 0; v_gemeld int := 0; v_p int;
31564
+ BEGIN
31565
+ PERFORM public.logistics_acc_guard('server', 'logistics_bewaak_zonder_kar');
31566
+ -- Opgelost: weer een kar of niet meer in de lijst.
31567
+ UPDATE public.logistics_zonder_kar_pogingen p SET opgelost_at = now(), updated_at = now()
31568
+ WHERE p.opgelost_at IS NULL
31569
+ AND NOT EXISTS (SELECT 1 FROM public.logistics_open_orders_zonder_kar() z WHERE z.order_id = p.order_id);
31570
+ FOR r IN SELECT * FROM public.logistics_open_orders_zonder_kar() LOOP
31571
+ INSERT INTO public.logistics_zonder_kar_pogingen(order_id, order_number) VALUES (r.order_id, r.order_number)
31572
+ ON CONFLICT (order_id) DO UPDATE SET opgelost_at = NULL,
31573
+ pogingen = CASE WHEN logistics_zonder_kar_pogingen.opgelost_at IS NOT NULL THEN 0 ELSE logistics_zonder_kar_pogingen.pogingen END,
31574
+ gemeld_at = CASE WHEN logistics_zonder_kar_pogingen.opgelost_at IS NOT NULL THEN NULL ELSE logistics_zonder_kar_pogingen.gemeld_at END
31575
+ RETURNING pogingen INTO v_p;
31576
+ IF v_p < 3 THEN
31577
+ -- Niet vaker dan eens per 10 minuten.
31578
+ IF EXISTS (SELECT 1 FROM public.logistics_zonder_kar_pogingen p WHERE p.order_id = r.order_id
31579
+ AND (p.laatste_poging_at IS NULL OR p.laatste_poging_at < now() - interval '10 minutes')) THEN
31580
+ PERFORM public.logistics_plan_enqueue(p_order_id => r.order_id, p_reason => 'bewaking: open order zonder kar');
31581
+ UPDATE public.logistics_zonder_kar_pogingen SET pogingen = pogingen + 1, laatste_poging_at = now(), updated_at = now()
31582
+ WHERE order_id = r.order_id;
31583
+ v_nieuw := v_nieuw + 1;
31584
+ END IF;
31585
+ ELSE
31586
+ UPDATE public.logistics_zonder_kar_pogingen SET gemeld_at = coalesce(gemeld_at, now()), updated_at = now()
31587
+ WHERE order_id = r.order_id;
31588
+ v_gemeld := v_gemeld + 1;
31589
+ END IF;
31590
+ END LOOP;
31591
+ RETURN jsonb_build_object('opnieuwIngepland', v_nieuw, 'gemeld', v_gemeld);
31592
+ END $$;
31593
+ REVOKE ALL ON FUNCTION public.logistics_bewaak_zonder_kar() FROM PUBLIC, anon, authenticated;
31594
+ GRANT EXECUTE ON FUNCTION public.logistics_bewaak_zonder_kar() TO service_role;CREATE OR REPLACE FUNCTION public.logistics_open_orders_zonder_kar()
31595
+ RETURNS TABLE(order_id uuid, order_number text, delivery_date text, laatste_reden text)
31596
+ LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
31597
+ SELECT public.logistics_acc_guard('ingelogd', 'logistics_open_orders_zonder_kar');
31598
+ WITH lq AS (
31599
+ SELECT DISTINCT ON (q.order_id) q.order_id, q.status, q.reason_code, q.error
31600
+ FROM public.logistics_plan_queue q ORDER BY q.order_id, q.updated_at DESC
31601
+ )
31602
+ SELECT o.id, o.order_number, o.delivery_date, lq.error
31603
+ FROM public.orders o
31604
+ LEFT JOIN lq ON lq.order_id = o.id
31605
+ WHERE o.deleted_at IS NULL AND o.cancelled_at IS NULL
31606
+ AND lower(coalesce(o.status,'')) NOT IN ('cancelled','geannuleerd','deleted','verwijderd','afgehandeld','completed','delivered','geleverd')
31607
+ AND o.delivery_date ~ '^\d{4}-\d{2}-\d{2}' AND left(o.delivery_date,10)::date >= public.logistics_local_today()
31608
+ AND EXISTS (SELECT 1 FROM public.order_items oi WHERE oi.order_id = o.id)
31609
+ AND NOT coalesce(public.logistics_order_buffer_excluded(o.id), false)
31610
+ AND NOT (coalesce(lq.status,'') IN ('geblokkeerd','pending','running'))
31611
+ AND NOT (coalesce(lq.status,'') = 'gearchiveerd'
31612
+ AND coalesce(lq.reason_code,'') IN ('belading_onwaarschijnlijk','belading_onvolledig','belading_onbekend','artikel_kiezen','ladingdrager_onbekend'))
31613
+ AND NOT EXISTS (SELECT 1 FROM logistics_active.order_carts c WHERE c.order_id = o.id)
31614
+ AND NOT EXISTS (SELECT 1 FROM public.order_items oi
31615
+ JOIN logistics_active.order_cart_items ci ON ci.order_item_id = oi.id
31616
+ JOIN logistics_active.order_carts c ON c.id = ci.cart_id
31617
+ WHERE oi.order_id = o.id);
31618
+ $$;CREATE OR REPLACE FUNCTION public.logistics_sticker_maten_per_dag(p_van date, p_tot date)
31619
+ RETURNS TABLE(dag date, print_plek_id text, breedte_mm numeric, hoogte_mm numeric, aantal integer)
31620
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public'
31621
+ AS $function$
31622
+ BEGIN
31623
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_sticker_maten_per_dag');
31624
+ IF p_van IS NULL OR p_tot IS NULL OR p_tot < p_van THEN
31625
+ RAISE EXCEPTION 'Geef een geldige periode op (van t/m tot).' USING ERRCODE = '22023';
31626
+ END IF;
31627
+ RETURN QUERY
31628
+ WITH b AS (
31629
+ SELECT coalesce(c.departure_at::date,
31630
+ CASE WHEN o.delivery_date ~ '^\d{4}-\d{2}-\d{2}' THEN left(o.delivery_date, 10)::date END) AS d,
31631
+ s.print_plek_id, s.breedte_mm, s.hoogte_mm, s.kopieen
31632
+ FROM public.logistics_sticker_prints s
31633
+ LEFT JOIN public.order_carts c ON c.id = s.cart_id
31634
+ LEFT JOIN public.orders o ON o.id = s.order_id
31635
+ WHERE s.status NOT IN ('niet_printen', 'afgemeld'))
31636
+ SELECT b.d, b.print_plek_id, b.breedte_mm, b.hoogte_mm, sum(greatest(coalesce(b.kopieen, 1), 1))::integer
31637
+ FROM b WHERE b.d BETWEEN p_van AND p_tot
31638
+ GROUP BY 1, 2, 3, 4 ORDER BY 1, 2, 3, 4;
31639
+ END $function$;
31640
+ -- ===== 1.0.192: inplannen in porties (db/migrations/20261023190000) =====
31641
+ -- Logistiek Engine 1.0.192: taken opnieuw inplannen in porties met vingerafdruk per order.
31642
+ -- Herhaalbaar. Geen datacorrecties, geen deletes. Bestaande functies (o.a. logistics_schedule_tasks)
31643
+ -- blijven ongewijzigd, zodat gepubliceerde 1.0.190 blijft werken.
31644
+
31645
+ CREATE TABLE IF NOT EXISTS public.logistics_schedule_fp (
31646
+ order_ref text PRIMARY KEY,
31647
+ fp text NOT NULL,
31648
+ ingepland_at timestamptz NOT NULL DEFAULT now()
31649
+ );
31650
+ GRANT SELECT ON public.logistics_schedule_fp TO authenticated;
31651
+ GRANT ALL ON public.logistics_schedule_fp TO service_role;
31652
+ ALTER TABLE public.logistics_schedule_fp ENABLE ROW LEVEL SECURITY;
31653
+ DROP POLICY IF EXISTS "Ingelogd mag inplan-vingerafdruk lezen" ON public.logistics_schedule_fp;
31654
+ CREATE POLICY "Ingelogd mag inplan-vingerafdruk lezen" ON public.logistics_schedule_fp
31655
+ FOR SELECT TO authenticated USING (true);
31656
+
31657
+ CREATE TABLE IF NOT EXISTS public.logistics_schedule_werklijst (
31658
+ run_id uuid NOT NULL,
31659
+ order_ref text NOT NULL,
31660
+ verwerkt_at timestamptz,
31661
+ uitkomst text,
31662
+ PRIMARY KEY (run_id, order_ref)
31663
+ );
31664
+ GRANT SELECT ON public.logistics_schedule_werklijst TO authenticated;
31665
+ GRANT ALL ON public.logistics_schedule_werklijst TO service_role;
31666
+ ALTER TABLE public.logistics_schedule_werklijst ENABLE ROW LEVEL SECURITY;
31667
+ DROP POLICY IF EXISTS "Ingelogd mag inplan-werklijst lezen" ON public.logistics_schedule_werklijst;
31668
+ CREATE POLICY "Ingelogd mag inplan-werklijst lezen" ON public.logistics_schedule_werklijst
31669
+ FOR SELECT TO authenticated USING (true);
31670
+
31671
+ CREATE TABLE IF NOT EXISTS public.logistics_schedule_voortgang (
31672
+ id boolean PRIMARY KEY DEFAULT true CHECK (id),
31673
+ run_id uuid,
31674
+ gestart_at timestamptz,
31675
+ bijgewerkt_at timestamptz,
31676
+ totaal integer NOT NULL DEFAULT 0,
31677
+ verwerkt integer NOT NULL DEFAULT 0,
31678
+ ingepland integer NOT NULL DEFAULT 0,
31679
+ overgeslagen integer NOT NULL DEFAULT 0,
31680
+ fouten integer NOT NULL DEFAULT 0,
31681
+ laatste_fout text,
31682
+ klaar boolean NOT NULL DEFAULT true,
31683
+ duur_ms integer
31684
+ );
31685
+ GRANT SELECT ON public.logistics_schedule_voortgang TO authenticated;
31686
+ GRANT ALL ON public.logistics_schedule_voortgang TO service_role;
31687
+ ALTER TABLE public.logistics_schedule_voortgang ENABLE ROW LEVEL SECURITY;
31688
+ DROP POLICY IF EXISTS "Ingelogd mag inplan-voortgang lezen" ON public.logistics_schedule_voortgang;
31689
+ CREATE POLICY "Ingelogd mag inplan-voortgang lezen" ON public.logistics_schedule_voortgang
31690
+ FOR SELECT TO authenticated USING (true);
31691
+
31692
+ CREATE TABLE IF NOT EXISTS public.logistics_index_log (
31693
+ index_naam text PRIMARY KEY,
31694
+ tabel text NOT NULL,
31695
+ rijen bigint,
31696
+ duur_ms integer,
31697
+ aangemaakt_at timestamptz NOT NULL DEFAULT now()
31698
+ );
31699
+ GRANT SELECT ON public.logistics_index_log TO authenticated;
31700
+ GRANT ALL ON public.logistics_index_log TO service_role;
31701
+ ALTER TABLE public.logistics_index_log ENABLE ROW LEVEL SECURITY;
31702
+ DROP POLICY IF EXISTS "Ingelogd mag indexlog lezen" ON public.logistics_index_log;
31703
+ CREATE POLICY "Ingelogd mag indexlog lezen" ON public.logistics_index_log
31704
+ FOR SELECT TO authenticated USING (true);
31705
+
31706
+ -- Index voor "open taken per order", met gemeten duur (schrijfblokkade op logistics_tasks zolang hij loopt).
31707
+ DO $idx$
31708
+ DECLARE t0 timestamptz; n bigint;
31709
+ BEGIN
31710
+ IF to_regclass('public.idx_logistics_tasks_open_order') IS NULL THEN
31711
+ SELECT count(*) INTO n FROM public.logistics_tasks;
31712
+ t0 := clock_timestamp();
31713
+ CREATE INDEX idx_logistics_tasks_open_order ON public.logistics_tasks (order_ref)
31714
+ WHERE status IN ('planned', 'ready') AND order_ref IS NOT NULL;
31715
+ INSERT INTO public.logistics_index_log (index_naam, tabel, rijen, duur_ms)
31716
+ VALUES ('idx_logistics_tasks_open_order', 'logistics_tasks', n,
31717
+ (extract(epoch FROM clock_timestamp() - t0) * 1000)::int)
31718
+ ON CONFLICT (index_naam) DO NOTHING;
31719
+ END IF;
31720
+ END $idx$;
31721
+
31722
+ -- Algemene invoer van het inplannen: werkwijze, werktijden, afwijkende dagen, capaciteit,
31723
+ -- ladingdragers, ophaalmomenten (vervoerders) en het lopende uur (taken mogen niet in het verleden starten).
31724
+ CREATE OR REPLACE FUNCTION public.logistics_schedule_global_fp()
31725
+ RETURNS text LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
31726
+ DECLARE
31727
+ v_tab text; v_h text; v_all text := '';
31728
+ BEGIN
31729
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_schedule_global_fp');
31730
+ FOREACH v_tab IN ARRAY ARRAY['logistics_time_settings','logistics_opening_hours','logistics_opening_exceptions',
31731
+ 'logistics_rule_configs','cart_planning_settings','cart_planning_pick_times','cart_types','carriers'] LOOP
31732
+ IF to_regclass('public.' || v_tab) IS NOT NULL THEN
31733
+ EXECUTE format('SELECT md5(coalesce(string_agg(x, ''|'' ORDER BY x), '''')) FROM (SELECT to_jsonb(t)::text AS x FROM public.%I t) s', v_tab)
31734
+ INTO v_h;
31735
+ v_all := v_all || v_tab || ':' || coalesce(v_h, '') || ';';
31736
+ END IF;
31737
+ END LOOP;
31738
+ v_all := v_all || 'uur:' || to_char(now() AT TIME ZONE coalesce(public.logistics_tz(), 'UTC'), 'YYYY-MM-DD HH24');
31739
+ RETURN md5(v_all);
31740
+ END $$;
31741
+ REVOKE ALL ON FUNCTION public.logistics_schedule_global_fp() FROM PUBLIC, anon;
31742
+ GRANT EXECUTE ON FUNCTION public.logistics_schedule_global_fp() TO authenticated, service_role;
31743
+
31744
+ CREATE OR REPLACE FUNCTION public.logistics_schedule_order_fp(p_order_ref text, p_global text)
31745
+ RETURNS text LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
31746
+ DECLARE v_o text; v_t text; v_c text;
31747
+ BEGIN
31748
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_schedule_order_fp');
31749
+ SELECT (to_jsonb(o) - 'updated_at')::text, array_to_string(public.logistics_order_cart_ids(o.id), ',')
31750
+ INTO v_o, v_c FROM public.orders o WHERE o.order_number = p_order_ref;
31751
+ SELECT string_agg(t.id::text || t.task_type || t.status, ',' ORDER BY t.id) INTO v_t
31752
+ FROM public.logistics_tasks t WHERE t.order_ref = p_order_ref AND t.status IN ('planned', 'ready');
31753
+ RETURN md5(coalesce(p_global, '') || '#' || coalesce(v_o, '') || '#' || coalesce(v_t, '') || '#' || coalesce(v_c, ''));
31754
+ END $$;
31755
+ REVOKE ALL ON FUNCTION public.logistics_schedule_order_fp(text, text) FROM PUBLIC, anon;
31756
+ GRANT EXECUTE ON FUNCTION public.logistics_schedule_order_fp(text, text) TO authenticated, service_role;
31757
+
31758
+ -- Portie-inplanner. Opties (jsonb): start (bool), run_id (uuid), max_orders (int, 25), budget_ms (int, 20000), force (bool).
31759
+ -- Per order een eigen subtransactie: lukt het niet, dan blijft de oude planning staan.
31760
+ CREATE OR REPLACE FUNCTION public.logistics_schedule_tasks_batch(p_opties jsonb)
31761
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$
31762
+ DECLARE
31763
+ v_start timestamptz := clock_timestamp();
31764
+ v_run uuid := nullif(p_opties->>'run_id', '')::uuid;
31765
+ v_max int := greatest(1, least(coalesce((p_opties->>'max_orders')::int, 25), 500));
31766
+ v_budget int := greatest(1000, least(coalesce((p_opties->>'budget_ms')::int, 20000), 120000));
31767
+ v_force boolean := coalesce((p_opties->>'force')::boolean, false);
31768
+ v_since timestamptz; v_160 timestamptz;
31769
+ v_global text; v_fp text; v_old text; r record;
31770
+ v_done int := 0; v_plan int := 0; v_skip int := 0; v_err int := 0; v_fouten jsonb := '[]'::jsonb;
31771
+ v_totaal int; v_rest int;
31772
+ BEGIN
31773
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_schedule_tasks_batch');
31774
+ IF v_run IS NULL OR coalesce((p_opties->>'start')::boolean, false) THEN
31775
+ v_run := gen_random_uuid();
31776
+ SELECT departure_rule_since, rule_160_since INTO v_since, v_160
31777
+ FROM public.logistics_time_settings ORDER BY updated_at DESC NULLS LAST LIMIT 1;
31778
+ INSERT INTO public.logistics_schedule_werklijst (run_id, order_ref)
31779
+ SELECT v_run, o.order_number FROM public.orders o
31780
+ WHERE o.deleted_at IS NULL AND o.order_number IS NOT NULL
31781
+ AND EXISTS (SELECT 1 FROM public.logistics_tasks t WHERE t.order_ref = o.order_number AND t.status IN ('planned','ready'))
31782
+ AND NOT (v_since IS NOT NULL AND v_160 IS NOT NULL AND o.created_at >= v_since AND o.created_at < v_160)
31783
+ ON CONFLICT DO NOTHING;
31784
+ GET DIAGNOSTICS v_totaal = ROW_COUNT;
31785
+ INSERT INTO public.logistics_schedule_voortgang (id, run_id, gestart_at, bijgewerkt_at, totaal, verwerkt, ingepland, overgeslagen, fouten, laatste_fout, klaar, duur_ms)
31786
+ VALUES (true, v_run, now(), now(), v_totaal, 0, 0, 0, 0, NULL, v_totaal = 0, NULL)
31787
+ ON CONFLICT (id) DO UPDATE SET run_id = EXCLUDED.run_id, gestart_at = EXCLUDED.gestart_at, bijgewerkt_at = EXCLUDED.bijgewerkt_at,
31788
+ totaal = EXCLUDED.totaal, verwerkt = 0, ingepland = 0, overgeslagen = 0, fouten = 0, laatste_fout = NULL,
31789
+ klaar = EXCLUDED.klaar, duur_ms = NULL;
31790
+ END IF;
31791
+
31792
+ v_global := public.logistics_schedule_global_fp();
31793
+ FOR r IN SELECT w.order_ref FROM public.logistics_schedule_werklijst w
31794
+ WHERE w.run_id = v_run AND w.verwerkt_at IS NULL ORDER BY w.order_ref LIMIT v_max LOOP
31795
+ EXIT WHEN (extract(epoch FROM clock_timestamp() - v_start) * 1000) > v_budget;
31796
+ v_fp := public.logistics_schedule_order_fp(r.order_ref, v_global);
31797
+ SELECT f.fp INTO v_old FROM public.logistics_schedule_fp f WHERE f.order_ref = r.order_ref;
31798
+ IF NOT v_force AND v_old IS NOT DISTINCT FROM v_fp THEN
31799
+ v_skip := v_skip + 1;
31800
+ UPDATE public.logistics_schedule_werklijst SET verwerkt_at = now(), uitkomst = 'ongewijzigd'
31801
+ WHERE run_id = v_run AND order_ref = r.order_ref;
31802
+ ELSE
31803
+ BEGIN
31804
+ PERFORM public.logistics_schedule_tasks(p_order_ref => r.order_ref);
31805
+ INSERT INTO public.logistics_schedule_fp (order_ref, fp, ingepland_at) VALUES (r.order_ref, v_fp, now())
31806
+ ON CONFLICT (order_ref) DO UPDATE SET fp = EXCLUDED.fp, ingepland_at = EXCLUDED.ingepland_at;
31807
+ v_plan := v_plan + 1;
31808
+ UPDATE public.logistics_schedule_werklijst SET verwerkt_at = now(), uitkomst = 'ingepland'
31809
+ WHERE run_id = v_run AND order_ref = r.order_ref;
31810
+ EXCEPTION WHEN OTHERS THEN
31811
+ v_err := v_err + 1;
31812
+ v_fouten := v_fouten || jsonb_build_object('order', r.order_ref, 'fout', left(SQLERRM, 300));
31813
+ UPDATE public.logistics_schedule_werklijst SET verwerkt_at = now(), uitkomst = 'fout: ' || left(SQLERRM, 300)
31814
+ WHERE run_id = v_run AND order_ref = r.order_ref;
31815
+ END;
31816
+ END IF;
31817
+ v_done := v_done + 1;
31818
+ END LOOP;
31819
+
31820
+ SELECT count(*) FILTER (WHERE verwerkt_at IS NULL), count(*) INTO v_rest, v_totaal
31821
+ FROM public.logistics_schedule_werklijst WHERE run_id = v_run;
31822
+ UPDATE public.logistics_schedule_voortgang
31823
+ SET bijgewerkt_at = now(), verwerkt = v_totaal - v_rest, ingepland = ingepland + v_plan,
31824
+ overgeslagen = overgeslagen + v_skip, fouten = fouten + v_err,
31825
+ laatste_fout = CASE WHEN v_err > 0 THEN (v_fouten->0->>'order') || ': ' || (v_fouten->0->>'fout') ELSE laatste_fout END,
31826
+ klaar = (v_rest = 0),
31827
+ duur_ms = CASE WHEN v_rest = 0 THEN (extract(epoch FROM now() - gestart_at) * 1000)::int ELSE duur_ms END
31828
+ WHERE id AND run_id = v_run;
31829
+ RETURN jsonb_build_object('runId', v_run, 'totaal', v_totaal, 'verwerkt', v_totaal - v_rest, 'resterend', v_rest,
31830
+ 'ingepland', v_plan, 'overgeslagen', v_skip, 'fouten', v_fouten,
31831
+ 'durationMs', (extract(epoch FROM clock_timestamp() - v_start) * 1000)::int);
31832
+ END $$;
31833
+ REVOKE ALL ON FUNCTION public.logistics_schedule_tasks_batch(jsonb) FROM PUBLIC, anon;
31834
+ GRANT EXECUTE ON FUNCTION public.logistics_schedule_tasks_batch(jsonb) TO authenticated, service_role;