@flowselections/logistiek-engine 1.0.195 → 1.0.200

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 (59) hide show
  1. package/db/schema/logistiek-engine.sql +1042 -13
  2. package/dist-lib/components/BeheerPage.d.ts.map +1 -1
  3. package/dist-lib/components/BeheerPage.js +3 -1
  4. package/dist-lib/components/DataGereedheidCard.d.ts +47 -0
  5. package/dist-lib/components/DataGereedheidCard.d.ts.map +1 -0
  6. package/dist-lib/components/DataGereedheidCard.js +133 -0
  7. package/dist-lib/components/GeblokkeerdeOrdersCard.d.ts.map +1 -1
  8. package/dist-lib/components/GeblokkeerdeOrdersCard.js +3 -1
  9. package/dist-lib/components/GeblokkeerdeStickersCard.d.ts.map +1 -1
  10. package/dist-lib/components/GeblokkeerdeStickersCard.js +11 -1
  11. package/dist-lib/components/LadingdragerVoorstellenCard.d.ts +4 -0
  12. package/dist-lib/components/LadingdragerVoorstellenCard.d.ts.map +1 -0
  13. package/dist-lib/components/LadingdragerVoorstellenCard.js +44 -0
  14. package/dist-lib/components/LadingdragersPage.d.ts.map +1 -1
  15. package/dist-lib/components/LadingdragersPage.js +2 -1
  16. package/dist-lib/components/StartlijstCard.d.ts +2 -0
  17. package/dist-lib/components/StartlijstCard.d.ts.map +1 -0
  18. package/dist-lib/components/StartlijstCard.js +54 -0
  19. package/dist-lib/hooks/useGeblokkeerdeOrders.d.ts.map +1 -1
  20. package/dist-lib/hooks/useGeblokkeerdeOrders.js +2 -0
  21. package/dist-lib/index.d.ts +3 -0
  22. package/dist-lib/index.d.ts.map +1 -1
  23. package/dist-lib/index.js +3 -0
  24. package/dist-lib/integrations/supabase/types.d.ts +353 -122
  25. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  26. package/dist-lib/integrations/supabase/types.js +1 -0
  27. package/dist-lib/lib/beladingRegels.d.ts +22 -0
  28. package/dist-lib/lib/beladingRegels.d.ts.map +1 -1
  29. package/dist-lib/lib/beladingRegels.js +46 -4
  30. package/dist-lib/lib/dimensionData.server.js +15 -12
  31. package/dist-lib/lib/dimensionPlanner.d.ts +11 -0
  32. package/dist-lib/lib/dimensionPlanner.d.ts.map +1 -1
  33. package/dist-lib/lib/dimensionPlanner.js +30 -2
  34. package/dist-lib/lib/handlers/index.d.ts +1 -0
  35. package/dist-lib/lib/handlers/index.d.ts.map +1 -1
  36. package/dist-lib/lib/handlers/index.js +1 -0
  37. package/dist-lib/lib/handlers/logistics-plan-now.server.d.ts +18 -0
  38. package/dist-lib/lib/handlers/logistics-plan-now.server.d.ts.map +1 -0
  39. package/dist-lib/lib/handlers/logistics-plan-now.server.js +114 -0
  40. package/dist-lib/lib/handlers/logistics-tick.server.d.ts +5 -0
  41. package/dist-lib/lib/handlers/logistics-tick.server.d.ts.map +1 -1
  42. package/dist-lib/lib/handlers/logistics-tick.server.js +7 -0
  43. package/dist-lib/lib/invoerTeksten.d.ts +4 -0
  44. package/dist-lib/lib/invoerTeksten.d.ts.map +1 -0
  45. package/dist-lib/lib/invoerTeksten.js +19 -0
  46. package/dist-lib/lib/layoutPrint.server.d.ts +8 -0
  47. package/dist-lib/lib/layoutPrint.server.d.ts.map +1 -1
  48. package/dist-lib/lib/layoutPrint.server.js +32 -0
  49. package/dist-lib/lib/loadCarrierPlanning.server.d.ts +4 -0
  50. package/dist-lib/lib/loadCarrierPlanning.server.d.ts.map +1 -1
  51. package/dist-lib/lib/loadCarrierPlanning.server.js +209 -12
  52. package/dist-lib/lib/moduleVersion.d.ts +1 -1
  53. package/dist-lib/lib/moduleVersion.js +1 -1
  54. package/dist-lib/lib/ruleEngineSchema.d.ts.map +1 -1
  55. package/dist-lib/lib/ruleEngineSchema.js +14 -0
  56. package/dist-lib/lib/stickerQueue.server.d.ts +12 -0
  57. package/dist-lib/lib/stickerQueue.server.d.ts.map +1 -1
  58. package/dist-lib/lib/stickerQueue.server.js +40 -2
  59. package/package.json +11 -1
@@ -13,6 +13,25 @@
13
13
  -- Gegenereerd uit de database; niet met de hand bewerken.
14
14
  -- ============================================================================
15
15
 
16
+ -- 1.0.196: vroege definitie van de toegangscontrole, zodat SQL-functies verderop
17
+ -- bij een eerste installatie (lege database) al kunnen verwijzen. Identiek aan de
18
+ -- definitie verderop; die blijft leidend.
19
+ CREATE OR REPLACE FUNCTION public.logistics_acc_guard(p_level text, p_fn text)
20
+ RETURNS void LANGUAGE plpgsql STABLE SET search_path TO 'public'
21
+ AS $function$
22
+ BEGIN
23
+ IF public.logistics_acc_is_server() THEN RETURN; END IF;
24
+ IF pg_trigger_depth() > 0 THEN RETURN; END IF;
25
+ IF NOT public.logistics_acc_direct(p_fn) THEN RETURN; END IF;
26
+ IF p_level = 'ingelogd' AND auth.uid() IS NOT NULL THEN RETURN; END IF;
27
+ IF p_level = 'beheer' AND auth.uid() IS NOT NULL AND public.logistics_is_beheer() THEN RETURN; END IF;
28
+ RAISE EXCEPTION '%', CASE p_level
29
+ WHEN 'server' THEN 'Geen toegang: ' || p_fn || ' is alleen voor de server.'
30
+ WHEN 'beheer' THEN 'Geen toegang: ' || p_fn || ' is alleen voor beheer.'
31
+ ELSE 'Geen toegang: log in om ' || p_fn || ' te gebruiken.' END
32
+ USING ERRCODE = '42501';
33
+ END $function$;
34
+
16
35
  SET check_function_bodies = off;
17
36
 
18
37
  DO $fs$ BEGIN
@@ -26902,7 +26921,7 @@ BEGIN
26902
26921
  AND coalesce(o.order_source, '') <> 'floriday'
26903
26922
  AND o.deleted_at IS NULL AND o.cancelled_at IS NULL
26904
26923
  AND coalesce(o.status, '') <> 'geannuleerd'
26905
- AND o.delivery_date >= current_date - 1
26924
+ AND public.logistics_order_date(o.delivery_date) >= current_date - 1
26906
26925
  GROUP BY o.id, o.order_number, o.customer_id, oi.product_id
26907
26926
  LIMIT greatest(1, p_limit)
26908
26927
  LOOP
@@ -26989,14 +27008,12 @@ $function$;
26989
27008
  -- 6. Pakbon van het pickstation: alleen de centrale beslisregel (geen layout uit payload of werkwijze).
26990
27009
  REVOKE EXECUTE ON FUNCTION public.logistics_own_sticker_sync(integer) FROM PUBLIC, anon, authenticated;
26991
27010
  GRANT EXECUTE ON FUNCTION public.logistics_own_sticker_sync(integer) TO service_role;
26992
- REVOKE EXECUTE ON FUNCTION public.logistics_document_for_order(uuid, text) FROM PUBLIC, anon;
26993
- GRANT EXECUTE ON FUNCTION public.logistics_document_for_order(uuid, text) TO authenticated, service_role;
27011
+ -- 1.0.197: rechten van logistics_document_for_order staan direct na de definitie (verderop).
26994
27012
  REVOKE EXECUTE ON FUNCTION public.logistics_document_selection_status() FROM PUBLIC, anon;
26995
27013
  GRANT EXECUTE ON FUNCTION public.logistics_document_selection_status() TO authenticated, service_role;
26996
27014
 
26997
27015
  -- Veiligheid: draaien met de rechten van de aanroeper.
26998
27016
  ALTER FUNCTION public.logistics_document_selection_status() SECURITY INVOKER;
26999
- ALTER FUNCTION public.logistics_document_for_order(uuid, text) SECURITY INVOKER;
27000
27017
 
27001
27018
  -- ===== 1.0.176 reserveringen FO + pakbon centrale keuze =====
27002
27019
  -- 1.0.176: reserveringen (Voorraad 1.0.93) en pakbon via centrale documentkeuze (Pickschermen 1.0.56).
@@ -27813,13 +27830,19 @@ BEGIN
27813
27830
  'documenttype', v_type, 'breedte_mm', v_l.breedte_mm, 'hoogte_mm', v_l.hoogte_mm, 'reden', NULL) || v_extra;
27814
27831
  END;
27815
27832
  $function$;
27833
+ -- 1.0.197: rechten na de definitie (een nieuwe Shell kent de functie hiervoor nog niet).
27834
+ REVOKE EXECUTE ON FUNCTION public.logistics_document_for_order(uuid, text) FROM PUBLIC, anon;
27835
+ GRANT EXECUTE ON FUNCTION public.logistics_document_for_order(uuid, text) TO authenticated, service_role;
27836
+ ALTER FUNCTION public.logistics_document_for_order(uuid, text) SECURITY INVOKER;
27816
27837
 
27817
27838
 
27818
27839
  -- ===== 1.0.178: fusten en land per kar, eigen traystickers =====
27819
27840
  -- 1.0.178 A: fusten en land per kar in control_get_todays_carts (achteraan toegevoegd)
27820
27841
  DO $m$ BEGIN
27821
- IF pg_get_function_result('public.control_get_todays_carts()'::regprocedure) NOT LIKE '%crates_on_cart%' THEN
27822
- DROP FUNCTION public.control_get_todays_carts();
27842
+ IF to_regprocedure('public.control_get_todays_carts()') IS NOT NULL THEN
27843
+ IF pg_get_function_result('public.control_get_todays_carts()'::regprocedure) NOT LIKE '%crates_on_cart%' THEN
27844
+ DROP FUNCTION public.control_get_todays_carts();
27845
+ END IF;
27823
27846
  END IF;
27824
27847
  END $m$;
27825
27848
 
@@ -28396,7 +28419,7 @@ BEGIN
28396
28419
  AND o.deleted_at IS NULL AND o.cancelled_at IS NULL
28397
28420
  AND coalesce(o.status, '') <> 'geannuleerd'
28398
28421
  AND oi.quarantined_at IS NULL
28399
- AND o.delivery_date >= current_date - 1
28422
+ AND public.logistics_order_date(o.delivery_date) >= current_date - 1
28400
28423
  )
28401
28424
  SELECT g.*,
28402
28425
  (SELECT min(x.regelnaam) FROM regels x WHERE x.order_id = g.order_id AND coalesce(x.crates, 0) <= 0) AS onbekende_regel
@@ -28445,7 +28468,7 @@ BEGIN
28445
28468
  SET status = 'niet_printen', reden = 'Fust hoort niet meer bij deze kar of order vervallen.', updated_at = now()
28446
28469
  WHERE p.soort = 'eigen_traysticker' AND p.status IN ('wacht_op_moment', 'geblokkeerd', 'klaar')
28447
28470
  AND NOT (p.bron_id = ANY (v_keep))
28448
- AND p.order_id IN (SELECT o.id FROM public.orders o WHERE o.delivery_date >= current_date - 1);
28471
+ AND p.order_id IN (SELECT o.id FROM public.orders o WHERE public.logistics_order_date(o.delivery_date) >= current_date - 1);
28449
28472
  GET DIAGNOSTICS n_uit = ROW_COUNT;
28450
28473
 
28451
28474
  UPDATE public.logistics_sticker_prints p
@@ -28679,7 +28702,7 @@ BEGIN
28679
28702
  FROM public.orders o
28680
28703
  JOIN public.floriday_salesorders_cache s ON s.floriday_id = o.floriday_sales_order_id AND NOT coalesce(s.is_deleted,false)
28681
28704
  CROSS JOIN LATERAL jsonb_array_elements(CASE WHEN jsonb_typeof(s.data->'additionalServices') = 'array' THEN s.data->'additionalServices' ELSE '[]'::jsonb END) e
28682
- WHERE o.deleted_at IS NULL AND o.delivery_date >= current_date - 2
28705
+ WHERE o.deleted_at IS NULL AND public.logistics_order_date(o.delivery_date) >= current_date - 2
28683
28706
  AND upper(coalesce(e->>'unit','')) = 'LABEL' AND (e->>'numberOfUnits') ~ '^\d+$'
28684
28707
  AND nullif(btrim(coalesce(e->>'stickerId','')),'') IS NOT NULL
28685
28708
  AND NOT EXISTS (SELECT 1 FROM public.floriday_customer_stickers_cache c WHERE c.floriday_id = e->>'stickerId')
@@ -28713,7 +28736,7 @@ BEGIN
28713
28736
  SELECT o.id, o.order_number, nullif(btrim(o.floriday_sales_order_id),'') AS sales
28714
28737
  FROM public.orders o
28715
28738
  WHERE o.deleted_at IS NULL AND nullif(btrim(o.floriday_sales_order_id),'') IS NOT NULL
28716
- AND o.delivery_date >= current_date - 2
28739
+ AND public.logistics_order_date(o.delivery_date) >= current_date - 2
28717
28740
  AND EXISTS (SELECT 1 FROM logistics_active.order_carts oc WHERE oc.order_id = o.id)
28718
28741
  ORDER BY o.delivery_date LIMIT greatest(p_limit,1)
28719
28742
  LOOP
@@ -29987,6 +30010,8 @@ DO $fs$ DECLARE f text; BEGIN
29987
30010
  'logistics_set_order_carrier(uuid,uuid)','logistics_carrier_status()','logistics_carrier_set_backfill(boolean)',
29988
30011
  'logistics_carrier_preview_existing(integer)','logistics_carrier_promote_oud(uuid[],integer)',
29989
30012
  'logistics_order_carrier_label(uuid)','logistics_install_check()','logistics_order_departure(uuid)','logistics_resolve_carrier_for_location_many(jsonb)'] LOOP
30013
+ -- 1.0.197: in een nieuwe Shell kan een functie hier nog ontbreken (verderop gedefinieerd, eigen rechten daar).
30014
+ CONTINUE WHEN to_regprocedure('public.' || f) IS NULL;
29990
30015
  EXECUTE format('REVOKE ALL ON FUNCTION public.%s FROM PUBLIC, anon', f);
29991
30016
  EXECUTE format('GRANT EXECUTE ON FUNCTION public.%s TO authenticated, service_role', f);
29992
30017
  END LOOP;
@@ -30005,9 +30030,10 @@ END $fs$;
30005
30030
  -- 1.0.184: karrenlijst toont de vervoerder uit orders.carrier_id (achteraan toegevoegd).
30006
30031
  -- "Eigen vervoer" en "Afhalen" op carrier_kind / self_pickup, nooit op naam.
30007
30032
  DO $m$ BEGIN
30008
- IF to_regprocedure('public.control_get_todays_carts()') IS NOT NULL
30009
- AND pg_get_function_result('public.control_get_todays_carts()'::regprocedure) NOT LIKE '%carrier_name%' THEN
30010
- DROP FUNCTION public.control_get_todays_carts();
30033
+ IF to_regprocedure('public.control_get_todays_carts()') IS NOT NULL THEN
30034
+ IF pg_get_function_result('public.control_get_todays_carts()'::regprocedure) NOT LIKE '%carrier_name%' THEN
30035
+ DROP FUNCTION public.control_get_todays_carts();
30036
+ END IF;
30011
30037
  END IF;
30012
30038
  END $m$;
30013
30039
 
@@ -33232,6 +33258,7 @@ BEGIN
33232
33258
  AND (q.printed_at IS NOT NULL OR q.completed_at IS NOT NULL)) THEN RETURN true; END IF;
33233
33259
  IF EXISTS (SELECT 1 FROM public.cart_control_items ci WHERE ci.order_number = v_number) THEN RETURN true; END IF;
33234
33260
  IF public.logistics_order_print_actief(v_number) THEN RETURN true; END IF;
33261
+ IF public.logistics_order_print_actief(v_number) THEN RETURN true; END IF;
33235
33262
  RETURN false;
33236
33263
  END;
33237
33264
  $function$
@@ -33295,6 +33322,7 @@ begin
33295
33322
  and not exists (select 1 from public.pick_queue q
33296
33323
  where q.reference = c.id::text and q.printed_at is not null)
33297
33324
  and not public.logistics_order_print_actief(o.order_number)
33325
+ and not public.logistics_order_print_actief(o.order_number)
33298
33326
  and public.logistics_cart_checkin_at(c.id) is not null
33299
33327
  limit greatest(coalesce(p_limit, 50), 1)
33300
33328
  loop
@@ -33329,6 +33357,7 @@ AS $function$
33329
33357
  WHERE oc.order_id = p_order_id AND public.logistics_cart_is_frozen(oc.id)
33330
33358
  )
33331
33359
  OR public.logistics_order_print_actief((SELECT o.order_number FROM public.orders o WHERE o.id = p_order_id))
33360
+ OR public.logistics_order_print_actief((SELECT o.order_number FROM public.orders o WHERE o.id = p_order_id))
33332
33361
  OR EXISTS (
33333
33362
  SELECT 1 FROM logistics_active.order_carts oc
33334
33363
  JOIN logistics_active.order_cart_items oci ON oci.cart_id = oc.id
@@ -33354,6 +33383,7 @@ AS $function$
33354
33383
  OR public.logistics_cart_handled(COALESCE(po.order_number, p.peer_order_id::text), p.cart_number)
33355
33384
  OR public.logistics_order_afgemeld(p.peer_order_id)
33356
33385
  OR public.logistics_order_print_actief(po.order_number)
33386
+ OR public.logistics_order_print_actief(po.order_number)
33357
33387
  OR public.logistics_cart_is_frozen(p.cart_id)
33358
33388
  OR EXISTS (SELECT 1 FROM public.pick_queue q WHERE q.cancelled_at IS NULL
33359
33389
  AND q.payload->>'cartId' = p.cart_id::text
@@ -33444,6 +33474,34 @@ BEGIN
33444
33474
  END;
33445
33475
  v_result := v_result || jsonb_build_object('instellingen_ms', (extract(epoch from clock_timestamp()-v_t)*1000)::int);
33446
33476
 
33477
+ -- 1.0.194: lichte ronde — pickwachtrij en laadtoestand in SQL, niet meer via HTTP.
33478
+ v_t := clock_timestamp();
33479
+ BEGIN
33480
+ PERFORM set_config('statement_timeout', '15000', true);
33481
+ v_result := v_result || jsonb_build_object('pickwachtrij', public.logistics_sync_pick_queue());
33482
+ EXCEPTION WHEN OTHERS THEN
33483
+ v_err := true; v_result := v_result || jsonb_build_object('pickwachtrij', left(SQLERRM, 200));
33484
+ END;
33485
+ v_result := v_result || jsonb_build_object('pickwachtrij_ms', (extract(epoch from clock_timestamp()-v_t)*1000)::int);
33486
+
33487
+ v_t := clock_timestamp();
33488
+ BEGIN
33489
+ PERFORM set_config('statement_timeout', '10000', true);
33490
+ v_result := v_result || jsonb_build_object('laadtoestand', public.logistics_refresh_plan_state_als_nodig());
33491
+ EXCEPTION WHEN OTHERS THEN
33492
+ v_err := true; v_result := v_result || jsonb_build_object('laadtoestand', left(SQLERRM, 200));
33493
+ END;
33494
+ v_result := v_result || jsonb_build_object('laadtoestand_ms', (extract(epoch from clock_timestamp()-v_t)*1000)::int);
33495
+
33496
+ v_t := clock_timestamp();
33497
+ BEGIN
33498
+ PERFORM set_config('statement_timeout', '10000', true);
33499
+ v_result := v_result || jsonb_build_object('instellingen', public.logistics_ensure_periodiek());
33500
+ EXCEPTION WHEN OTHERS THEN
33501
+ v_err := true; v_result := v_result || jsonb_build_object('instellingen', left(SQLERRM, 200));
33502
+ END;
33503
+ v_result := v_result || jsonb_build_object('instellingen_ms', (extract(epoch from clock_timestamp()-v_t)*1000)::int);
33504
+
33447
33505
  BEGIN
33448
33506
  INSERT INTO public.logistics_tick_runs(started_at, finished_at, duration_ms, has_error, steps)
33449
33507
  VALUES (v_start, clock_timestamp(), (extract(epoch from clock_timestamp()-v_start)*1000)::int, v_err, v_result);
@@ -33553,6 +33611,7 @@ BEGIN
33553
33611
  ORDER BY e.created_at DESC LIMIT 1), '') END;
33554
33612
  RETURN NEXT;
33555
33613
  RETURN QUERY SELECT * FROM public.logistics_install_check_194();
33614
+ RETURN QUERY SELECT * FROM public.logistics_install_check_196();
33556
33615
  RETURN QUERY SELECT * FROM public.logistics_access_check();
33557
33616
  RETURN QUERY SELECT * FROM public.logistics_overload_check();
33558
33617
  RETURN QUERY SELECT * FROM public.logistics_check_190();
@@ -33572,3 +33631,973 @@ GRANT EXECUTE ON FUNCTION public.logistics_order_date(date) TO authenticated, se
33572
33631
  GRANT EXECUTE ON FUNCTION public.logistics_print_status(uuid[]) TO authenticated, service_role;
33573
33632
  GRANT EXECUTE ON FUNCTION public.logistics_kick_metingen(integer) TO authenticated, service_role;
33574
33633
  GRANT EXECUTE ON FUNCTION public.logistics_install_check_194() TO authenticated, service_role;
33634
+
33635
+ -- ============================================================================
33636
+ -- logistiek-engine 1.0.196 — direct wekken, achterstand in één aanroep, gegevens
33637
+ -- voor karindeling, sticker opnieuw proberen, rebuffer-slot (additief, idempotent)
33638
+ -- ============================================================================
33639
+ -- 1.0.196 — direct wekken, achterstand in één aanroep, gegevens voor karindeling, sticker opnieuw, rebuffer-slot
33640
+
33641
+ CREATE OR REPLACE FUNCTION public.logistics_plan_now_gate(p_order_ids uuid[])
33642
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$
33643
+ DECLARE
33644
+ v_running boolean; v_last timestamptz;
33645
+ v_debounce int := public.logistics_plan_setting('kick_debounce_seconds', 5)::int;
33646
+ v_n int := coalesce(cardinality(p_order_ids), 0);
33647
+ BEGIN
33648
+ PERFORM public.logistics_acc_guard('server', 'logistics_plan_now_gate');
33649
+ IF v_n > 1000 THEN
33650
+ RAISE EXCEPTION 'Maximaal 1000 orders per aanroep (ontvangen: %).', v_n USING ERRCODE = '22023';
33651
+ END IF;
33652
+ SELECT EXISTS (SELECT 1 FROM public.logistics_plan_locks WHERE lock_key = 'tick:ronde' AND locked_until > now())
33653
+ INTO v_running;
33654
+ IF v_running THEN
33655
+ INSERT INTO public.logistics_plan_settings(key, value, updated_at)
33656
+ VALUES ('kick_pending', clock_timestamp()::text, now())
33657
+ ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = now();
33658
+ BEGIN INSERT INTO public.logistics_kick_log(skipped_reason) VALUES ('plan_now_vlag_gezet'); EXCEPTION WHEN OTHERS THEN NULL; END;
33659
+ RETURN jsonb_build_object('actie', 'vlag_gezet', 'orders', v_n);
33660
+ END IF;
33661
+ SELECT NULLIF(value,'')::timestamptz INTO v_last FROM public.logistics_plan_settings WHERE key = 'last_kick_at';
33662
+ IF v_last IS NOT NULL AND v_last > now() - make_interval(secs => v_debounce) THEN
33663
+ -- Binnen de wachttijd: niet laten vallen, de eerstvolgende ronde pakt de vlag op.
33664
+ INSERT INTO public.logistics_plan_settings(key, value, updated_at)
33665
+ VALUES ('kick_pending', clock_timestamp()::text, now())
33666
+ ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = now();
33667
+ BEGIN INSERT INTO public.logistics_kick_log(skipped_reason) VALUES ('plan_now_debounce'); EXCEPTION WHEN OTHERS THEN NULL; END;
33668
+ RETURN jsonb_build_object('actie', 'debounce', 'orders', v_n);
33669
+ END IF;
33670
+ INSERT INTO public.logistics_plan_settings(key, value, updated_at)
33671
+ VALUES ('last_kick_at', now()::text, now())
33672
+ ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = now();
33673
+ BEGIN INSERT INTO public.logistics_kick_log(skipped_reason) VALUES ('plan_now_start'); EXCEPTION WHEN OTHERS THEN NULL; END;
33674
+ RETURN jsonb_build_object('actie', 'start', 'orders', v_n);
33675
+ END $$;
33676
+ GRANT EXECUTE ON FUNCTION public.logistics_plan_now_gate(uuid[]) TO service_role;
33677
+
33678
+ CREATE OR REPLACE FUNCTION public.logistics_order_backlog_info_many(p_order_ids uuid[])
33679
+ RETURNS TABLE(order_id uuid, info jsonb, achterstand boolean, leverdatum date, dagen integer)
33680
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
33681
+ BEGIN
33682
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_order_backlog_info_many');
33683
+ IF coalesce(cardinality(p_order_ids), 0) > 1000 THEN
33684
+ RAISE EXCEPTION 'Maximaal 1000 orders per aanroep (ontvangen: %).', cardinality(p_order_ids) USING ERRCODE = '22023';
33685
+ END IF;
33686
+ RETURN QUERY
33687
+ WITH ids AS (SELECT DISTINCT u AS id FROM unnest(coalesce(p_order_ids, '{}'::uuid[])) u WHERE u IS NOT NULL),
33688
+ today AS (SELECT (now() AT TIME ZONE public.logistics_tz())::date AS d),
33689
+ hit AS (
33690
+ SELECT o.id, (o.delivery_date::text)::date AS ld
33691
+ FROM public.orders o JOIN ids ON ids.id = o.id, today
33692
+ WHERE o.deleted_at IS NULL AND o.cancelled_at IS NULL
33693
+ AND COALESCE(o.is_reservation, false) = false
33694
+ AND lower(coalesce(o.status, '')) = 'nog te picken'
33695
+ AND NULLIF(o.delivery_date::text, '') IS NOT NULL
33696
+ AND (o.delivery_date::text)::date < today.d
33697
+ AND NOT public.logistics_order_handled(o.id)
33698
+ AND NOT EXISTS (SELECT 1 FROM public.floriday_salesorders_cache fc
33699
+ WHERE o.floriday_sales_order_id IS NOT NULL AND fc.floriday_id = o.floriday_sales_order_id AND fc.controlled_at IS NOT NULL)
33700
+ )
33701
+ SELECT ids.id,
33702
+ CASE WHEN h.id IS NULL THEN NULL
33703
+ ELSE jsonb_build_object('achterstand', true, 'leverdatum', h.ld, 'dagen', t.d - h.ld) END,
33704
+ h.id IS NOT NULL, h.ld, CASE WHEN h.id IS NULL THEN NULL ELSE t.d - h.ld END
33705
+ FROM ids CROSS JOIN today t LEFT JOIN hit h ON h.id = ids.id;
33706
+ END $$;
33707
+ GRANT EXECUTE ON FUNCTION public.logistics_order_backlog_info_many(uuid[]) TO authenticated, service_role;
33708
+
33709
+ -- E4/E5: gegevens voor karindeling
33710
+ CREATE OR REPLACE FUNCTION public.logistics_data_gereedheid(p_product_ids uuid[] DEFAULT NULL)
33711
+ RETURNS TABLE(product_id uuid, product text, ladingdrager text, methode text, in_open_order boolean,
33712
+ ontbrekend text[], velden jsonb, voorstel jsonb, uitleg text)
33713
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
33714
+ DECLARE v_methode text; v_carrier text; c record;
33715
+ BEGIN
33716
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_data_gereedheid');
33717
+ SELECT CASE WHEN coalesce(s.settings->'karrenindeling'->'fields'->>'cart_layout_method', s.settings->'karrenindeling'->>'cart_layout_method') = 'afmetingen'
33718
+ THEN 'afmetingen' ELSE 'belading' END,
33719
+ nullif(btrim(coalesce(s.settings->'karrenindeling'->'fields'->>'default_carrier_type', s.settings->'karrenindeling'->>'default_carrier_type')), '')
33720
+ INTO v_methode, v_carrier
33721
+ FROM public.logistics_rule_configs s WHERE s.is_active ORDER BY s.updated_at DESC LIMIT 1;
33722
+ v_methode := coalesce(v_methode, 'belading');
33723
+ SELECT t.code, t.name, t.layer_length_cm, t.layer_width_cm, t.max_height_cm INTO c
33724
+ FROM public.load_carrier_types t
33725
+ WHERE t.is_active AND (t.code = v_carrier OR (v_carrier IS NULL AND t.is_default))
33726
+ ORDER BY (t.code = v_carrier) DESC NULLS LAST LIMIT 1;
33727
+
33728
+ RETURN QUERY
33729
+ WITH open_p AS (
33730
+ SELECT DISTINCT oi.product_id FROM public.orders o JOIN public.order_items oi ON oi.order_id = o.id
33731
+ WHERE o.deleted_at IS NULL AND o.cancelled_at IS NULL AND coalesce(o.is_reservation,false) = false
33732
+ AND lower(coalesce(o.status,'')) = 'nog te picken' AND oi.product_id IS NOT NULL
33733
+ ),
33734
+ p AS (
33735
+ SELECT pr.id, pr.product, pr.plants_per_crate, pr.crates_per_layer, pr.layers_per_cart,
33736
+ NULLIF(regexp_replace(coalesce(pr.weight::text,''), '[^0-9.,]', '', 'g'), '') AS weight_txt,
33737
+ pr.plant_height_cm, pr.fust_type_id, pr.floriday_packing_source,
33738
+ ft.length_mm, ft.width_mm, ft.height_mm,
33739
+ (op.product_id IS NOT NULL) AS in_open
33740
+ FROM public.products pr
33741
+ LEFT JOIN public.fust_types ft ON ft.id = pr.fust_type_id
33742
+ LEFT JOIN open_p op ON op.product_id = pr.id
33743
+ WHERE (p_product_ids IS NULL AND op.product_id IS NOT NULL) OR pr.id = ANY(p_product_ids)
33744
+ ),
33745
+ m AS (
33746
+ SELECT p.*,
33747
+ array_remove(ARRAY[
33748
+ CASE WHEN coalesce(p.plants_per_crate,0) <= 0 THEN 'plants_per_crate' END,
33749
+ CASE WHEN v_methode = 'belading' AND coalesce(p.crates_per_layer,0) <= 0 THEN 'crates_per_layer' END,
33750
+ CASE WHEN v_methode = 'belading' AND coalesce(p.layers_per_cart,0) <= 0 THEN 'layers_per_cart' END,
33751
+ CASE WHEN v_methode = 'afmetingen' AND p.fust_type_id IS NULL THEN 'fust_type_id' END,
33752
+ CASE WHEN v_methode = 'afmetingen' AND coalesce(p.plant_height_cm,0) <= 0 THEN 'plant_height_cm' END,
33753
+ CASE WHEN p.weight_txt IS NULL THEN 'weight' END
33754
+ ], NULL) AS mist,
33755
+ -- Fusten per laag uit fustmaat × laagmaat (beide oriëntaties, raster).
33756
+ CASE WHEN p.length_mm > 0 AND p.width_mm > 0 AND c.layer_length_cm > 0 AND c.layer_width_cm > 0 THEN
33757
+ greatest(floor(c.layer_length_cm*10 / p.length_mm) * floor(c.layer_width_cm*10 / p.width_mm),
33758
+ floor(c.layer_length_cm*10 / p.width_mm) * floor(c.layer_width_cm*10 / p.length_mm))::int END AS calc_cpl,
33759
+ -- Lagen per kar uit planthoogte + fusthoogte tegen hoogte ladingdrager.
33760
+ CASE WHEN coalesce(p.plant_height_cm,0) > 0 AND c.max_height_cm > 0 THEN
33761
+ floor(c.max_height_cm / (p.plant_height_cm + coalesce(p.height_mm,0)/10.0))::int END AS calc_lpc
33762
+ FROM p
33763
+ )
33764
+ SELECT m.id, m.product, c.code, v_methode, m.in_open, m.mist,
33765
+ jsonb_build_object(
33766
+ 'plants_per_crate', jsonb_build_object('waarde', m.plants_per_crate, 'bron', CASE WHEN m.plants_per_crate > 0 THEN coalesce(m.floriday_packing_source, 'product') END),
33767
+ 'crates_per_layer', jsonb_build_object('waarde', m.crates_per_layer, 'bron', CASE WHEN m.crates_per_layer > 0 THEN coalesce(m.floriday_packing_source, 'product') END),
33768
+ 'layers_per_cart', jsonb_build_object('waarde', m.layers_per_cart, 'bron', CASE WHEN m.layers_per_cart > 0 THEN coalesce(m.floriday_packing_source, 'product') END),
33769
+ 'weight', jsonb_build_object('waarde', m.weight_txt, 'bron', CASE WHEN m.weight_txt IS NOT NULL THEN 'product' END),
33770
+ 'plant_height_cm', jsonb_build_object('waarde', m.plant_height_cm, 'bron', CASE WHEN m.plant_height_cm > 0 THEN 'product' END),
33771
+ 'fust_type_id', jsonb_build_object('waarde', m.fust_type_id, 'bron', CASE WHEN m.fust_type_id IS NOT NULL THEN 'product' END)),
33772
+ jsonb_strip_nulls(jsonb_build_object(
33773
+ 'crates_per_layer', CASE WHEN m.calc_cpl > 0 THEN jsonb_build_object('waarde', m.calc_cpl, 'bron', 'berekend',
33774
+ 'berekening', format('fust %s×%s mm op laag %s×%s cm (%s)', m.length_mm, m.width_mm, c.layer_length_cm, c.layer_width_cm, c.name))
33775
+ ELSE (SELECT jsonb_build_object('waarde', x.v, 'bron', 'vergelijkbaar', 'berekening', format('meest gebruikt bij %s product(en) met dezelfde fust', x.n))
33776
+ FROM (SELECT p2.crates_per_layer v, count(*) n FROM public.products p2
33777
+ WHERE m.fust_type_id IS NOT NULL AND p2.fust_type_id = m.fust_type_id AND p2.crates_per_layer > 0 AND p2.id <> m.id
33778
+ GROUP BY 1 ORDER BY 2 DESC LIMIT 1) x) END,
33779
+ 'layers_per_cart', CASE WHEN m.calc_lpc > 0 THEN jsonb_build_object('waarde', m.calc_lpc, 'bron', 'berekend',
33780
+ 'berekening', format('hoogte %s cm / (plant %s cm + fust %s cm)', c.max_height_cm, m.plant_height_cm, round(coalesce(m.height_mm,0)/10.0,1)))
33781
+ ELSE (SELECT jsonb_build_object('waarde', x.v, 'bron', 'vergelijkbaar', 'berekening', format('meest gebruikt bij %s product(en) met dezelfde fust', x.n))
33782
+ FROM (SELECT p2.layers_per_cart v, count(*) n FROM public.products p2
33783
+ WHERE m.fust_type_id IS NOT NULL AND p2.fust_type_id = m.fust_type_id AND p2.layers_per_cart > 0 AND p2.id <> m.id
33784
+ GROUP BY 1 ORDER BY 2 DESC LIMIT 1) x) END)),
33785
+ CASE WHEN cardinality(m.mist) = 0 THEN 'Compleet.'
33786
+ ELSE format('Bij "%s" ontbreekt: %s. Vul dit in bij het product (Voorraad) of hier met "Invullen".', m.product,
33787
+ array_to_string(ARRAY(SELECT CASE f WHEN 'plants_per_crate' THEN 'planten per fust' WHEN 'crates_per_layer' THEN 'fusten per laag'
33788
+ WHEN 'layers_per_cart' THEN 'lagen per kar' WHEN 'fust_type_id' THEN 'fustsoort' WHEN 'plant_height_cm' THEN 'planthoogte'
33789
+ WHEN 'weight' THEN 'gewicht per fust' ELSE f END FROM unnest(m.mist) f), ', ')) END
33790
+ FROM m
33791
+ ORDER BY cardinality(m.mist) DESC, m.in_open DESC, m.product
33792
+ LIMIT 2000;
33793
+ END $$;
33794
+ GRANT EXECUTE ON FUNCTION public.logistics_data_gereedheid(uuid[]) TO authenticated, service_role;
33795
+
33796
+ CREATE OR REPLACE FUNCTION public.logistics_data_vul_in(p_product_id uuid, p_veld text, p_waarde numeric)
33797
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$
33798
+ BEGIN
33799
+ PERFORM public.logistics_acc_guard('beheer', 'logistics_data_vul_in');
33800
+ IF p_waarde IS NULL OR p_waarde <= 0 THEN
33801
+ RAISE EXCEPTION 'Vul een getal groter dan 0 in.' USING ERRCODE = '22023';
33802
+ END IF;
33803
+ IF p_veld = 'plants_per_crate' THEN UPDATE public.products SET plants_per_crate = p_waarde, updated_at = now() WHERE id = p_product_id;
33804
+ ELSIF p_veld = 'crates_per_layer' THEN UPDATE public.products SET crates_per_layer = p_waarde, updated_at = now() WHERE id = p_product_id;
33805
+ ELSIF p_veld = 'layers_per_cart' THEN UPDATE public.products SET layers_per_cart = p_waarde, updated_at = now() WHERE id = p_product_id;
33806
+ ELSIF p_veld = 'plant_height_cm' THEN UPDATE public.products SET plant_height_cm = p_waarde, updated_at = now() WHERE id = p_product_id;
33807
+ ELSIF p_veld = 'weight' THEN UPDATE public.products SET weight = p_waarde, updated_at = now() WHERE id = p_product_id;
33808
+ ELSE RAISE EXCEPTION 'Dit veld kan hier niet worden ingevuld: %', p_veld USING ERRCODE = '22023';
33809
+ END IF;
33810
+ IF NOT FOUND THEN RAISE EXCEPTION 'Product niet gevonden.' USING ERRCODE = 'P0002'; END IF;
33811
+ RETURN jsonb_build_object('ok', true, 'productId', p_product_id, 'veld', p_veld, 'waarde', p_waarde);
33812
+ END $$;
33813
+ GRANT EXECUTE ON FUNCTION public.logistics_data_vul_in(uuid, text, numeric) TO authenticated, service_role;
33814
+
33815
+ -- 4: geweigerde sticker opnieuw proberen (status blijft binnen bestaande CHECK)
33816
+ CREATE OR REPLACE FUNCTION public.logistics_sticker_retry(p_id uuid)
33817
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$
33818
+ BEGIN
33819
+ PERFORM public.logistics_acc_guard('beheer', 'logistics_sticker_retry');
33820
+ UPDATE public.logistics_sticker_prints
33821
+ SET status = 'klaar', reden = NULL, print_job_id = NULL, attempts = 0, next_attempt_at = NULL, updated_at = now()
33822
+ WHERE id = p_id AND status IN ('geblokkeerd', 'mislukt');
33823
+ IF NOT FOUND THEN
33824
+ RAISE EXCEPTION 'Deze sticker is niet geblokkeerd of mislukt; opnieuw proberen is niet nodig.' USING ERRCODE = '22023';
33825
+ END IF;
33826
+ RETURN jsonb_build_object('ok', true, 'id', p_id);
33827
+ END $$;
33828
+ GRANT EXECUTE ON FUNCTION public.logistics_sticker_retry(uuid) TO authenticated, service_role;
33829
+
33830
+ -- 6: rebuffer-instelling onder advisory lock (geen deadlock meer bij gelijktijdige aanroepen)
33831
+ CREATE OR REPLACE FUNCTION public.logistics_ensure_rebuffer_setting()
33832
+ RETURNS integer LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$
33833
+ DECLARE v_n integer := 0;
33834
+ BEGIN
33835
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_ensure_rebuffer_setting');
33836
+ -- 1.0.196: één tegelijk; bezet = overslaan zonder fout. Daarna rijen in vaste volgorde vergrendelen.
33837
+ IF NOT pg_try_advisory_xact_lock(hashtext('logistics_ensure_rebuffer_setting')) THEN
33838
+ RETURN 0;
33839
+ END IF;
33840
+ PERFORM 1 FROM public.logistics_rule_configs c
33841
+ WHERE NOT coalesce(c.settings->'order_verwerking'->'fields' ? 'rebuffer_on_change', false)
33842
+ AND NOT coalesce(c.settings->'order_verwerking' ? 'rebuffer_on_change', false)
33843
+ ORDER BY c.id FOR UPDATE SKIP LOCKED;
33844
+ UPDATE public.logistics_rule_configs c
33845
+ SET settings = jsonb_set(coalesce(c.settings, '{}'::jsonb), '{order_verwerking}',
33846
+ coalesce(c.settings->'order_verwerking', '{}'::jsonb)
33847
+ || jsonb_build_object('fields', coalesce(c.settings->'order_verwerking'->'fields', '{}'::jsonb)
33848
+ || jsonb_build_object('rebuffer_on_change', true)), true)
33849
+ WHERE c.id IS NOT NULL
33850
+ AND NOT coalesce(c.settings->'order_verwerking'->'fields' ? 'rebuffer_on_change', false)
33851
+ AND NOT coalesce(c.settings->'order_verwerking' ? 'rebuffer_on_change', false);
33852
+ GET DIAGNOSTICS v_n = ROW_COUNT;
33853
+ RETURN v_n;
33854
+ END $$;
33855
+
33856
+ CREATE OR REPLACE FUNCTION public.logistics_install_check_196()
33857
+ RETURNS TABLE(controle text, ok boolean, toelichting text)
33858
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
33859
+ BEGIN
33860
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_install_check_196');
33861
+ RETURN QUERY SELECT 'Engine: direct wekken (plan-now) – sleutel ingesteld'::text,
33862
+ EXISTS (SELECT 1 FROM public.module_runtime_config WHERE key = 'logistics_tick_apikey' AND nullif(btrim(value),'') IS NOT NULL),
33863
+ 'module_runtime_config logistics_tick_apikey; zonder sleutel antwoordt de route 500.'::text;
33864
+ RETURN QUERY SELECT 'Engine: poort direct wekken aanwezig', to_regprocedure('public.logistics_plan_now_gate(uuid[])') IS NOT NULL, 'logistics_plan_now_gate';
33865
+ RETURN QUERY SELECT 'Engine: achterstand in één aanroep', to_regprocedure('public.logistics_order_backlog_info_many(uuid[])') IS NOT NULL, 'logistics_order_backlog_info_many';
33866
+ RETURN QUERY SELECT 'Engine: gegevens voor karindeling', to_regprocedure('public.logistics_data_gereedheid(uuid[])') IS NOT NULL, 'logistics_data_gereedheid';
33867
+ RETURN QUERY SELECT 'Engine: sticker opnieuw proberen', to_regprocedure('public.logistics_sticker_retry(uuid)') IS NOT NULL, 'logistics_sticker_retry';
33868
+ RETURN QUERY SELECT 'Engine: rebuffer-instelling onder slot',
33869
+ coalesce(pg_get_functiondef('public.logistics_ensure_rebuffer_setting()'::regprocedure) LIKE '%pg_try_advisory_xact_lock%', false),
33870
+ 'voorkomt deadlock bij gelijktijdige aanroep';
33871
+ END $$;
33872
+ GRANT EXECUTE ON FUNCTION public.logistics_install_check_196() TO authenticated, service_role;
33873
+ DO $do$ DECLARE d text;
33874
+ BEGIN
33875
+ d := pg_get_functiondef('public.logistics_install_check()'::regprocedure);
33876
+ IF position('logistics_install_check_196' in d) = 0 THEN
33877
+ d := replace(d, $r$ RETURN QUERY SELECT * FROM public.logistics_access_check();$r$,
33878
+ $r$ RETURN QUERY SELECT * FROM public.logistics_install_check_196();
33879
+ RETURN QUERY SELECT * FROM public.logistics_access_check();$r$);
33880
+ EXECUTE d;
33881
+ END IF;
33882
+ EXCEPTION WHEN undefined_function THEN NULL;
33883
+ END $do$;
33884
+
33885
+ -- ===== 1.0.197 =====
33886
+ -- ============================================================================
33887
+ -- logistiek-engine 1.0.197 — unieke karnummering per buffer (incl. bevroren
33888
+ -- karren uit eerdere buffers), "van N" over alle karren, orderscherm telt via de
33889
+ -- buffer, fysieke beladingscontrole in de installatiecontrole (additief, idempotent)
33890
+ -- ============================================================================
33891
+
33892
+ -- Kern zonder guard en zonder SECURITY DEFINER: RLS van de aanroeper blijft gelden.
33893
+ CREATE OR REPLACE FUNCTION public.logistics_order_unit_carts_core(p_order_ids uuid[])
33894
+ RETURNS TABLE(cart_id uuid, cart_number integer, buffer_id uuid, order_id uuid, display_seq integer, display_total integer)
33895
+ LANGUAGE sql STABLE SET search_path TO 'public' AS $$
33896
+ WITH ords AS (
33897
+ SELECT o.id, o.buffer_id FROM public.orders o WHERE o.id = ANY(coalesce(p_order_ids, '{}'::uuid[]))
33898
+ ),
33899
+ bufs AS (SELECT DISTINCT b.buffer_id FROM ords b WHERE b.buffer_id IS NOT NULL),
33900
+ ords2 AS (
33901
+ SELECT o.id FROM ords o
33902
+ UNION
33903
+ SELECT o.id FROM public.orders o
33904
+ WHERE o.buffer_id IN (SELECT bb.buffer_id FROM bufs bb) AND o.deleted_at IS NULL AND o.cancelled_at IS NULL
33905
+ ),
33906
+ k AS (
33907
+ SELECT c.id FROM logistics_active.order_carts c WHERE c.order_id IN (SELECT x.id FROM ords2 x)
33908
+ UNION
33909
+ SELECT ci.cart_id FROM logistics_active.order_cart_items ci
33910
+ JOIN public.order_items oi ON oi.id = ci.order_item_id
33911
+ WHERE oi.order_id IN (SELECT x.id FROM ords2 x)
33912
+ UNION
33913
+ SELECT c.id FROM logistics_active.order_carts c
33914
+ WHERE c.order_id IS NULL AND c.buffer_id IN (SELECT bb.buffer_id FROM bufs bb)
33915
+ ),
33916
+ base AS (
33917
+ SELECT c.id, c.cart_number, c.buffer_id, c.order_id, c.created_at,
33918
+ min(c.created_at) OVER (PARTITION BY coalesce(c.buffer_id, c.id)) AS grp_at
33919
+ FROM logistics_active.order_carts c WHERE c.id IN (SELECT k.id FROM k)
33920
+ ),
33921
+ f AS (
33922
+ SELECT count(*)::int AS n, count(DISTINCT b.cart_number)::int AS nd,
33923
+ coalesce(max(b.cart_number), 0)::int AS mx,
33924
+ count(*) FILTER (WHERE b.cart_number IS NULL)::int AS leeg
33925
+ FROM base b
33926
+ )
33927
+ SELECT b.id, b.cart_number, b.buffer_id, b.order_id,
33928
+ CASE WHEN f.nd = f.n AND f.leeg = 0 THEN b.cart_number
33929
+ ELSE row_number() OVER (ORDER BY b.grp_at, coalesce(b.buffer_id, b.id), b.cart_number NULLS LAST, b.id)::int END,
33930
+ CASE WHEN f.nd = f.n AND f.leeg = 0 THEN greatest(f.n, f.mx) ELSE f.n END
33931
+ FROM base b CROSS JOIN f
33932
+ $$;
33933
+
33934
+ CREATE OR REPLACE FUNCTION public.logistics_order_unit_carts(p_order_ids uuid[])
33935
+ RETURNS TABLE(cart_id uuid, cart_number integer, buffer_id uuid, order_id uuid, display_seq integer, display_total integer)
33936
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
33937
+ BEGIN
33938
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_order_unit_carts');
33939
+ IF coalesce(cardinality(p_order_ids), 0) > 1000 THEN
33940
+ RAISE EXCEPTION 'Maximaal 1000 orders per aanroep (ontvangen: %).', cardinality(p_order_ids) USING ERRCODE = '22023';
33941
+ END IF;
33942
+ RETURN QUERY SELECT * FROM public.logistics_order_unit_carts_core(p_order_ids);
33943
+ END $$;
33944
+ GRANT EXECUTE ON FUNCTION public.logistics_order_unit_carts(uuid[]) TO authenticated, service_role;
33945
+
33946
+ -- Bestaande handtekening blijft; weergave nu uniek binnen de hele eenheid.
33947
+ CREATE OR REPLACE FUNCTION public.logistics_cart_fixed_numbering(p_cart_id uuid)
33948
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
33949
+ DECLARE c record; v_ids uuid[]; u record; v_total int;
33950
+ BEGIN
33951
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_cart_fixed_numbering');
33952
+ SELECT * INTO c FROM public.order_carts WHERE id = p_cart_id;
33953
+ IF NOT FOUND THEN RETURN NULL; END IF;
33954
+ IF c.archived_at IS NULL THEN
33955
+ SELECT array_agg(DISTINCT s.x) INTO v_ids FROM (
33956
+ SELECT c.order_id AS x
33957
+ UNION SELECT oi.order_id FROM public.order_cart_items ci JOIN public.order_items oi ON oi.id = ci.order_item_id WHERE ci.cart_id = p_cart_id
33958
+ UNION SELECT o.id FROM public.orders o WHERE c.buffer_id IS NOT NULL AND o.buffer_id = c.buffer_id AND o.deleted_at IS NULL
33959
+ ) s WHERE s.x IS NOT NULL;
33960
+ IF v_ids IS NOT NULL THEN
33961
+ SELECT * INTO u FROM public.logistics_order_unit_carts_core(v_ids) z WHERE z.cart_id = p_cart_id;
33962
+ IF FOUND AND u.display_seq IS NOT NULL THEN
33963
+ RETURN jsonb_build_object('cartSeq', u.display_seq, 'cartIndex', u.display_seq, 'cartTotal', u.display_total,
33964
+ 'cartLabel', 'Kar ' || u.display_seq || ' van ' || u.display_total, 'storedCartNumber', c.cart_number);
33965
+ END IF;
33966
+ END IF;
33967
+ END IF;
33968
+ -- Terugval (gearchiveerde kar of geen orders): oude berekening.
33969
+ SELECT greatest(coalesce(max(c2.total_carts), 0), count(*)::int, coalesce(c.cart_number, 0)) INTO v_total
33970
+ FROM public.order_carts c2
33971
+ WHERE (c.order_id IS NULL AND c.buffer_id IS NOT NULL AND c2.order_id IS NULL AND c2.buffer_id = c.buffer_id)
33972
+ OR (c.order_id IS NOT NULL AND c2.order_id = c.order_id)
33973
+ OR (c.order_id IS NULL AND c.buffer_id IS NULL AND c2.id = c.id);
33974
+ RETURN jsonb_build_object('cartSeq', c.cart_number, 'cartIndex', c.cart_number, 'cartTotal', v_total,
33975
+ 'cartLabel', 'Kar ' || c.cart_number || ' van ' || v_total);
33976
+ END $$;
33977
+
33978
+ -- Orderscherm: tel alle karren van de eenheid (buffer + karren met goederen van de order).
33979
+ CREATE OR REPLACE FUNCTION public.logistics_live_cart_count(p_order_ref text)
33980
+ RETURNS integer LANGUAGE sql STABLE SET search_path TO 'public' AS $$
33981
+ SELECT COALESCE(
33982
+ NULLIF((SELECT count(*)::int FROM public.logistics_order_unit_carts_core(
33983
+ ARRAY(SELECT o.id FROM public.orders o WHERE o.order_number = p_order_ref AND o.deleted_at IS NULL))), 0),
33984
+ public.logistics_order_cart_count(p_order_ref),
33985
+ 0);
33986
+ $$;
33987
+
33988
+ -- Controle: actieve pakbonnen met dezelfde naam (alleen melden, niets wijzigen).
33989
+ CREATE OR REPLACE FUNCTION public.logistics_dubbele_pakbonnamen()
33990
+ RETURNS TABLE(document_number text, aantal integer, karren text[])
33991
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
33992
+ BEGIN
33993
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_dubbele_pakbonnamen');
33994
+ RETURN QUERY
33995
+ SELECT q.document_number, count(*)::int, array_agg(q.reference ORDER BY q.reference)
33996
+ FROM public.pick_queue q
33997
+ WHERE q.cancelled_at IS NULL AND q.document_number LIKE 'PB-%-K%'
33998
+ GROUP BY q.document_number HAVING count(*) > 1;
33999
+ END $$;
34000
+ GRANT EXECUTE ON FUNCTION public.logistics_dubbele_pakbonnamen() TO authenticated, service_role;
34001
+
34002
+ CREATE OR REPLACE FUNCTION public.logistics_install_check_197()
34003
+ RETURNS TABLE(controle text, ok boolean, toelichting text)
34004
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
34005
+ DECLARE v_dub int;
34006
+ BEGIN
34007
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_install_check_197');
34008
+ RETURN QUERY SELECT 'Engine: karnummering per eenheid'::text,
34009
+ to_regprocedure('public.logistics_order_unit_carts(uuid[])') IS NOT NULL
34010
+ AND coalesce(pg_get_functiondef('public.logistics_cart_fixed_numbering(uuid)'::regprocedure) LIKE '%logistics_order_unit_carts_core%', false),
34011
+ 'logistics_order_unit_carts + logistics_cart_fixed_numbering'::text;
34012
+ RETURN QUERY SELECT 'Engine: orderscherm telt karren via de buffer',
34013
+ coalesce(pg_get_functiondef('public.logistics_live_cart_count(text)'::regprocedure) LIKE '%logistics_order_unit_carts_core%', false),
34014
+ 'logistics_live_cart_count';
34015
+ BEGIN
34016
+ SELECT count(*) INTO v_dub FROM public.logistics_dubbele_pakbonnamen();
34017
+ RETURN QUERY SELECT 'Engine: geen dubbele pakbonnamen', v_dub = 0,
34018
+ CASE WHEN v_dub = 0 THEN 'geen' ELSE v_dub || ' naam/namen dubbel; de eerstvolgende pakbonronde zet het unieke nummer' END;
34019
+ EXCEPTION WHEN undefined_table THEN
34020
+ RETURN QUERY SELECT 'Engine: geen dubbele pakbonnamen', true, 'pick_queue ontbreekt in deze Shell';
34021
+ END;
34022
+ END $$;
34023
+ GRANT EXECUTE ON FUNCTION public.logistics_install_check_197() TO authenticated, service_role;
34024
+
34025
+ DO $do$ DECLARE d text;
34026
+ BEGIN
34027
+ d := pg_get_functiondef('public.logistics_install_check()'::regprocedure);
34028
+ IF position('logistics_install_check_197' in d) = 0 THEN
34029
+ d := replace(d, $r$ RETURN QUERY SELECT * FROM public.logistics_access_check();$r$,
34030
+ $r$ RETURN QUERY SELECT * FROM public.logistics_install_check_197();
34031
+ RETURN QUERY SELECT * FROM public.logistics_access_check();$r$);
34032
+ EXECUTE d;
34033
+ END IF;
34034
+ EXCEPTION WHEN undefined_function THEN NULL;
34035
+ END $do$;
34036
+
34037
+ -- ============================================================================
34038
+ -- 1.0.198: maten hergebruiken uit Floricode/Floriday/Voorraad; niets stil invullen
34039
+ -- ============================================================================
34040
+
34041
+ -- Bronregistratie van overgenomen of bevestigde waarden (Engine-eigendom).
34042
+ CREATE TABLE IF NOT EXISTS public.logistics_gegevens_bron (
34043
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
34044
+ onderwerp text NOT NULL,
34045
+ sleutel text NOT NULL,
34046
+ veld text NOT NULL,
34047
+ waarde text,
34048
+ bron text NOT NULL,
34049
+ toelichting text,
34050
+ vastgelegd_door uuid,
34051
+ vastgelegd_op timestamptz NOT NULL DEFAULT now(),
34052
+ UNIQUE (onderwerp, sleutel, veld)
34053
+ );
34054
+ GRANT SELECT ON public.logistics_gegevens_bron TO authenticated;
34055
+ GRANT ALL ON public.logistics_gegevens_bron TO service_role;
34056
+ ALTER TABLE public.logistics_gegevens_bron ENABLE ROW LEVEL SECURITY;
34057
+ DO $do$ BEGIN
34058
+ IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'logistics_gegevens_bron' AND policyname = 'logistics_gegevens_bron_lezen') THEN
34059
+ CREATE POLICY logistics_gegevens_bron_lezen ON public.logistics_gegevens_bron FOR SELECT TO authenticated USING (true);
34060
+ END IF;
34061
+ END $do$;
34062
+
34063
+ -- Positief getal uit tekst, anders NULL. Nooit een standaardwaarde.
34064
+ CREATE OR REPLACE FUNCTION public.logistics_pos_num(p text)
34065
+ RETURNS numeric LANGUAGE sql IMMUTABLE SET search_path TO 'public' AS $$
34066
+ SELECT CASE WHEN replace(btrim(coalesce(p, '')), ',', '.') ~ '^[0-9]+(\.[0-9]+)?$'
34067
+ THEN NULLIF(replace(btrim(p), ',', '.')::numeric, 0) END
34068
+ $$;
34069
+ GRANT EXECUTE ON FUNCTION public.logistics_pos_num(text) TO authenticated, service_role;
34070
+
34071
+ -- VBN-kenmerkwaarde in cm. Met Floricode: alleen een omschrijving "N cm" telt;
34072
+ -- een bereik of andere tekst = onbekend. Zonder Floricode-tabel: de code zelf (cm).
34073
+ CREATE OR REPLACE FUNCTION public.logistics_vbn_cm(p_feature text, p_code text)
34074
+ RETURNS numeric LANGUAGE plpgsql STABLE SET search_path TO 'public' AS $$
34075
+ DECLARE v_desc text; v_found boolean := false;
34076
+ BEGIN
34077
+ IF p_code IS NULL OR btrim(p_code) = '' THEN RETURN NULL; END IF;
34078
+ IF to_regclass('public.floricode_records') IS NOT NULL THEN
34079
+ EXECUTE $q$SELECT r.data->>'description' FROM public.floricode_records r
34080
+ WHERE r.resource = 'FeatureValue' AND r.data->>'feature_type_id' = $1 AND r.data->>'code' = $2
34081
+ ORDER BY (r.data->>'expiry_date') IS NULL DESC LIMIT 1$q$
34082
+ INTO v_desc USING p_feature, p_code;
34083
+ v_found := v_desc IS NOT NULL;
34084
+ END IF;
34085
+ IF v_found THEN
34086
+ IF v_desc ~* '^\s*[0-9]+([.,][0-9]+)?\s*cm\s*$' THEN
34087
+ RETURN public.logistics_pos_num(regexp_replace(v_desc, '[^0-9.,]', '', 'g'));
34088
+ END IF;
34089
+ RETURN NULL;
34090
+ END IF;
34091
+ RETURN public.logistics_pos_num(p_code);
34092
+ END $$;
34093
+ GRANT EXECUTE ON FUNCTION public.logistics_vbn_cm(text, text) TO authenticated, service_role;
34094
+
34095
+ -- 1: eigen terugval voor fustmaten. Per veld: Floricode -> Floriday custom package -> handmatig -> onbekend.
34096
+ CREATE OR REPLACE FUNCTION public.logistics_fust_maten_terugval()
34097
+ RETURNS TABLE(fust_type_id uuid, fust_key text, vbn_code integer, name text,
34098
+ length_mm numeric, width_mm numeric, height_mm numeric,
34099
+ lengte_bron text, breedte_bron text, hoogte_bron text)
34100
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
34101
+ #variable_conflict use_column
34102
+ DECLARE v_fc text; v_fd text;
34103
+ BEGIN
34104
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_fust_maten_terugval');
34105
+ IF to_regprocedure('public.floricode_packaging_types()') IS NOT NULL THEN
34106
+ v_fc := $q$SELECT DISTINCT ON (c.code::text)
34107
+ CASE WHEN c.code::text ~ '^[0-9]+$' THEN c.code::text::int END AS code,
34108
+ public.logistics_pos_num(c.length_mm::text) AS l, public.logistics_pos_num(c.width_mm::text) AS w,
34109
+ public.logistics_pos_num(c.height_mm::text) AS h
34110
+ FROM public.floricode_packaging_types() c ORDER BY c.code::text, c.expiry_date DESC NULLS FIRST$q$;
34111
+ ELSIF to_regclass('public.floricode_records') IS NOT NULL THEN
34112
+ v_fc := $q$SELECT DISTINCT ON (r.data->>'code')
34113
+ CASE WHEN (r.data->>'code') ~ '^[0-9]+$' THEN (r.data->>'code')::int END AS code,
34114
+ public.logistics_pos_num(r.data->>'length') AS l, public.logistics_pos_num(r.data->>'width') AS w,
34115
+ public.logistics_pos_num(r.data->>'height') AS h
34116
+ FROM public.floricode_records r WHERE r.resource = 'LoadCarrier'
34117
+ ORDER BY r.data->>'code', (r.data->>'expiry_date') DESC NULLS FIRST$q$;
34118
+ ELSE
34119
+ v_fc := $q$SELECT NULL::int AS code, NULL::numeric AS l, NULL::numeric AS w, NULL::numeric AS h WHERE false$q$;
34120
+ END IF;
34121
+ IF to_regclass('public.floriday_packings_cache') IS NOT NULL THEN
34122
+ v_fd := $q$SELECT DISTINCT ON (p.floriday_id) p.floriday_id AS id, p.data->>'customPackageId' AS id2,
34123
+ public.logistics_pos_num(coalesce(p.data->'dimension'->>'length', p.data->'dimensions'->>'length', p.data->>'lengthInMm', p.data->>'length')) AS l,
34124
+ public.logistics_pos_num(coalesce(p.data->'dimension'->>'width', p.data->'dimensions'->>'width', p.data->>'widthInMm', p.data->>'width')) AS w,
34125
+ public.logistics_pos_num(coalesce(p.data->'dimension'->>'height', p.data->'dimensions'->>'height', p.data->>'heightInMm', p.data->>'height')) AS h
34126
+ FROM public.floriday_packings_cache p WHERE coalesce(p.is_deleted, false) = false
34127
+ ORDER BY p.floriday_id, p.updated_at DESC NULLS LAST$q$;
34128
+ ELSE
34129
+ v_fd := $q$SELECT NULL::text AS id, NULL::text AS id2, NULL::numeric AS l, NULL::numeric AS w, NULL::numeric AS h WHERE false$q$;
34130
+ END IF;
34131
+ RETURN QUERY EXECUTE format($q$
34132
+ WITH fc AS (%s), fd AS (%s)
34133
+ SELECT f.id, f.fust_key, f.vbn_code, f.name,
34134
+ coalesce(fc.l, fdx.l, public.logistics_pos_num(f.length_mm::text)),
34135
+ coalesce(fc.w, fdx.w, public.logistics_pos_num(f.width_mm::text)),
34136
+ coalesce(fc.h, fdx.h, public.logistics_pos_num(f.height_mm::text)),
34137
+ CASE WHEN fc.l IS NOT NULL THEN 'floricode' WHEN fdx.l IS NOT NULL THEN 'floriday' WHEN public.logistics_pos_num(f.length_mm::text) IS NOT NULL THEN 'handmatig' ELSE 'onbekend' END,
34138
+ CASE WHEN fc.w IS NOT NULL THEN 'floricode' WHEN fdx.w IS NOT NULL THEN 'floriday' WHEN public.logistics_pos_num(f.width_mm::text) IS NOT NULL THEN 'handmatig' ELSE 'onbekend' END,
34139
+ CASE WHEN fc.h IS NOT NULL THEN 'floricode' WHEN fdx.h IS NOT NULL THEN 'floriday' WHEN public.logistics_pos_num(f.height_mm::text) IS NOT NULL THEN 'handmatig' ELSE 'onbekend' END
34140
+ FROM public.fust_types f
34141
+ LEFT JOIN fc ON fc.code = coalesce(f.vbn_code, CASE WHEN f.fust_key ~ '^vbn:[0-9]+$' THEN substr(f.fust_key, 5)::int END)
34142
+ LEFT JOIN LATERAL (SELECT fd.l, fd.w, fd.h FROM fd
34143
+ WHERE nullif(f.custom_package_id, '') IS NOT NULL AND (fd.id = f.custom_package_id OR fd.id2 = f.custom_package_id)
34144
+ LIMIT 1) fdx ON true
34145
+ $q$, v_fc, v_fd);
34146
+ END $$;
34147
+ GRANT EXECUTE ON FUNCTION public.logistics_fust_maten_terugval() TO authenticated, service_role;
34148
+
34149
+ -- 1: fustmaten. Voorraad (voorraad_fust_dimensions) als die bestaat, anders de eigen terugval.
34150
+ CREATE OR REPLACE FUNCTION public.logistics_fust_maten()
34151
+ RETURNS TABLE(fust_type_id uuid, fust_key text, vbn_code integer, name text,
34152
+ length_mm numeric, width_mm numeric, height_mm numeric,
34153
+ lengte_bron text, breedte_bron text, hoogte_bron text, pad text)
34154
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
34155
+ #variable_conflict use_column
34156
+ DECLARE j jsonb; v_src text;
34157
+ BEGIN
34158
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_fust_maten');
34159
+ IF to_regprocedure('public.voorraad_fust_dimensions()') IS NOT NULL THEN
34160
+ FOR j IN EXECUTE 'SELECT to_jsonb(x) FROM public.voorraad_fust_dimensions() x' LOOP
34161
+ fust_type_id := (j->>'fust_type_id')::uuid;
34162
+ fust_key := j->>'fust_key';
34163
+ vbn_code := CASE WHEN (j->>'vbn_code') ~ '^[0-9]+$' THEN (j->>'vbn_code')::int END;
34164
+ name := j->>'name';
34165
+ length_mm := public.logistics_pos_num(j->>'length_mm');
34166
+ width_mm := public.logistics_pos_num(j->>'width_mm');
34167
+ height_mm := public.logistics_pos_num(j->>'height_mm');
34168
+ v_src := nullif(j->>'dimension_source', 'onbekend');
34169
+ lengte_bron := CASE WHEN length_mm IS NULL THEN 'onbekend' ELSE coalesce(j->>'length_source', j->>'lengte_bron', v_src, 'voorraad') END;
34170
+ breedte_bron := CASE WHEN width_mm IS NULL THEN 'onbekend' ELSE coalesce(j->>'width_source', j->>'breedte_bron', v_src, 'voorraad') END;
34171
+ hoogte_bron := CASE WHEN height_mm IS NULL THEN 'onbekend' ELSE coalesce(j->>'height_source', j->>'hoogte_bron', v_src, 'voorraad') END;
34172
+ pad := 'voorraad';
34173
+ RETURN NEXT;
34174
+ END LOOP;
34175
+ RETURN;
34176
+ END IF;
34177
+ RETURN QUERY SELECT t.fust_type_id, t.fust_key, t.vbn_code, t.name, t.length_mm, t.width_mm, t.height_mm,
34178
+ t.lengte_bron, t.breedte_bron, t.hoogte_bron, 'terugval'::text
34179
+ FROM public.logistics_fust_maten_terugval() t;
34180
+ END $$;
34181
+ GRANT EXECUTE ON FUNCTION public.logistics_fust_maten() TO authenticated, service_role;
34182
+
34183
+ -- 2: eigen terugval voor artikelkenmerken (S15, S02, fust) uit het Floriday-handelsitem.
34184
+ -- Leest nooit products.custom_fields.
34185
+ CREATE OR REPLACE FUNCTION public.logistics_artikel_vbn_terugval(p_product_id uuid)
34186
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
34187
+ DECLARE v_ti text; v jsonb;
34188
+ BEGIN
34189
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_artikel_vbn_terugval');
34190
+ SELECT coalesce(nullif(p.floriday_trade_item_id, ''), nullif(p.trade_item_key, '')) INTO v_ti FROM public.products p WHERE p.id = p_product_id;
34191
+ IF v_ti IS NULL OR to_regclass('public.floriday_trade_items_cache') IS NULL THEN
34192
+ RETURN jsonb_build_object('bron', 'geen', 's02_cm', NULL, 's15_cm', NULL, 'vbn_package_code', NULL, 'custom_package_id', NULL);
34193
+ END IF;
34194
+ EXECUTE $q$
34195
+ SELECT jsonb_build_object(
34196
+ 'bron', 'floriday_handelsitem',
34197
+ 's15_cm', public.logistics_vbn_cm('S15', (SELECT x->>'vbnValueCode' FROM jsonb_array_elements(coalesce(c.data->'characteristics', '[]'::jsonb)) x WHERE x->>'vbnCode' = 'S15' LIMIT 1)),
34198
+ 's02_cm', public.logistics_vbn_cm('S02', (SELECT x->>'vbnValueCode' FROM jsonb_array_elements(coalesce(c.data->'characteristics', '[]'::jsonb)) x WHERE x->>'vbnCode' = 'S02' LIMIT 1)),
34199
+ 'vbn_package_code', (SELECT CASE WHEN (pc->'package'->>'vbnPackageCode') ~ '^[0-9]+$' THEN (pc->'package'->>'vbnPackageCode')::int END
34200
+ FROM jsonb_array_elements(coalesce(c.data->'packingConfigurations', '[]'::jsonb)) pc
34201
+ ORDER BY (pc->>'primary')::boolean DESC NULLS LAST LIMIT 1),
34202
+ 'custom_package_id', (SELECT nullif(pc->'package'->>'customPackageId', '')
34203
+ FROM jsonb_array_elements(coalesce(c.data->'packingConfigurations', '[]'::jsonb)) pc
34204
+ ORDER BY (pc->>'primary')::boolean DESC NULLS LAST LIMIT 1))
34205
+ FROM public.floriday_trade_items_cache c
34206
+ WHERE (c.floriday_id = $1 OR c.data->>'tradeItemId' = $1) AND coalesce(c.is_deleted, false) = false
34207
+ LIMIT 1$q$ INTO v USING v_ti;
34208
+ RETURN coalesce(v, jsonb_build_object('bron', 'geen', 's02_cm', NULL, 's15_cm', NULL, 'vbn_package_code', NULL, 'custom_package_id', NULL));
34209
+ END $$;
34210
+ GRANT EXECUTE ON FUNCTION public.logistics_artikel_vbn_terugval(uuid) TO authenticated, service_role;
34211
+
34212
+ -- 2: artikelkenmerken. Voorraad (voorraad_article_vbn) als die bestaat, anders de eigen terugval.
34213
+ CREATE OR REPLACE FUNCTION public.logistics_artikel_vbn(p_product_id uuid)
34214
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
34215
+ DECLARE j jsonb;
34216
+ BEGIN
34217
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_artikel_vbn');
34218
+ IF to_regprocedure('public.voorraad_article_vbn(uuid)') IS NOT NULL THEN
34219
+ EXECUTE 'SELECT to_jsonb(x) FROM public.voorraad_article_vbn($1) x LIMIT 1' INTO j USING p_product_id;
34220
+ IF j ? 'voorraad_article_vbn' AND jsonb_typeof(j->'voorraad_article_vbn') = 'object' THEN j := j->'voorraad_article_vbn'; END IF;
34221
+ j := coalesce(j, '{}'::jsonb);
34222
+ RETURN jsonb_build_object(
34223
+ 'pad', 'voorraad',
34224
+ 'bron', coalesce(j->>'bron', j->>'source', 'voorraad'),
34225
+ 's15_cm', coalesce(public.logistics_pos_num(j->>'s15_cm'), public.logistics_vbn_cm('S15', j->'features'->>'S15')),
34226
+ 's02_cm', coalesce(public.logistics_pos_num(j->>'s02_cm'),
34227
+ CASE WHEN j->>'s02_source' = 'handmatig' THEN public.logistics_pos_num(j->>'s02')
34228
+ ELSE public.logistics_vbn_cm('S02', coalesce(j->>'s02', j->'features'->>'S02')) END),
34229
+ 'vbn_package_code', CASE WHEN coalesce(j->>'vbn_package_code', j->'fust'->>'vbn_code') ~ '^[0-9]+$' THEN coalesce(j->>'vbn_package_code', j->'fust'->>'vbn_code')::int END,
34230
+ 'custom_package_id', nullif(coalesce(j->>'custom_package_id', j->'fust'->>'custom_package_id'), ''));
34231
+ END IF;
34232
+ RETURN public.logistics_artikel_vbn_terugval(p_product_id) || jsonb_build_object('pad', 'terugval');
34233
+ END $$;
34234
+ GRANT EXECUTE ON FUNCTION public.logistics_artikel_vbn(uuid) TO authenticated, service_role;
34235
+
34236
+ -- 1+2: maten per product in één bron. Fustkeuze: handmatig -> VBN-code (rij met de meeste maten) -> custom package.
34237
+ -- Maten per veld; ontbreekt een veld bij de gekozen rij, dan uit een andere rij met dezelfde fust_key.
34238
+ -- Planthoogte geldt altijd INCLUSIEF pot: handmatig -> S15 (transporthoogte) -> S02 (min. planthoogte incl. pot).
34239
+ CREATE OR REPLACE FUNCTION public.logistics_product_maten(p_product_ids uuid[])
34240
+ RETURNS TABLE(product_id uuid, product_name text, plants_per_crate integer, fust_type_id uuid, fust_name text, fust_bron text,
34241
+ fust_length_mm numeric, fust_width_mm numeric, fust_height_mm numeric,
34242
+ lengte_bron text, breedte_bron text, hoogte_bron text,
34243
+ plant_height_cm numeric, height_source text, weight_kg numeric,
34244
+ pad_fust text, pad_artikel text, missing_reason text)
34245
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
34246
+ #variable_conflict use_column
34247
+ BEGIN
34248
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_product_maten');
34249
+ IF p_product_ids IS NOT NULL AND cardinality(p_product_ids) > 2000 THEN
34250
+ RAISE EXCEPTION 'Maximaal 2000 producten per aanvraag.' USING ERRCODE = '22023';
34251
+ END IF;
34252
+ RETURN QUERY
34253
+ WITH fm AS (SELECT * FROM public.logistics_fust_maten()),
34254
+ pr AS (
34255
+ SELECT p.id, p.product, p.plants_per_crate, p.fust_type_id AS hand_fid, p.plant_height_cm AS hand_h, p.weight,
34256
+ public.logistics_artikel_vbn(p.id) AS a
34257
+ FROM public.products p
34258
+ WHERE p_product_ids IS NULL OR p.id = ANY(p_product_ids)
34259
+ ),
34260
+ keuze AS (
34261
+ SELECT pr.*,
34262
+ (SELECT m.fust_type_id FROM fm m
34263
+ WHERE (pr.a->>'vbn_package_code') ~ '^[0-9]+$'
34264
+ AND (m.vbn_code = (pr.a->>'vbn_package_code')::int OR m.fust_key = 'vbn:' || (pr.a->>'vbn_package_code'))
34265
+ ORDER BY ((m.length_mm IS NOT NULL)::int + (m.width_mm IS NOT NULL)::int + (m.height_mm IS NOT NULL)::int) DESC, m.fust_type_id
34266
+ LIMIT 1) AS vbn_fid,
34267
+ (SELECT f.id FROM public.fust_types f
34268
+ WHERE nullif(pr.a->>'custom_package_id', '') IS NOT NULL AND f.custom_package_id = pr.a->>'custom_package_id'
34269
+ ORDER BY f.created_at, f.id LIMIT 1) AS cp_fid
34270
+ FROM pr
34271
+ ),
34272
+ k2 AS (
34273
+ SELECT k.*, coalesce(k.hand_fid, k.vbn_fid, k.cp_fid) AS fid,
34274
+ CASE WHEN k.hand_fid IS NOT NULL THEN 'handmatig' WHEN k.vbn_fid IS NOT NULL THEN 'vbn' WHEN k.cp_fid IS NOT NULL THEN 'floriday' END AS fbron
34275
+ FROM keuze k
34276
+ ),
34277
+ maat AS (
34278
+ SELECT k.*, m.name AS mname, m.pad AS mpad,
34279
+ coalesce(m.length_mm, lx.length_mm) AS l, CASE WHEN m.length_mm IS NOT NULL THEN m.lengte_bron ELSE coalesce(lx.lengte_bron, 'onbekend') END AS lb,
34280
+ coalesce(m.width_mm, wx.width_mm) AS w, CASE WHEN m.width_mm IS NOT NULL THEN m.breedte_bron ELSE coalesce(wx.breedte_bron, 'onbekend') END AS wb,
34281
+ coalesce(m.height_mm, hx.height_mm) AS h, CASE WHEN m.height_mm IS NOT NULL THEN m.hoogte_bron ELSE coalesce(hx.hoogte_bron, 'onbekend') END AS hb
34282
+ FROM k2 k
34283
+ LEFT JOIN fm m ON m.fust_type_id = k.fid
34284
+ LEFT JOIN LATERAL (SELECT m2.length_mm, m2.lengte_bron FROM fm m2 WHERE m.fust_key IS NOT NULL AND m2.fust_key = m.fust_key AND m2.length_mm IS NOT NULL ORDER BY m2.fust_type_id LIMIT 1) lx ON true
34285
+ LEFT JOIN LATERAL (SELECT m2.width_mm, m2.breedte_bron FROM fm m2 WHERE m.fust_key IS NOT NULL AND m2.fust_key = m.fust_key AND m2.width_mm IS NOT NULL ORDER BY m2.fust_type_id LIMIT 1) wx ON true
34286
+ LEFT JOIN LATERAL (SELECT m2.height_mm, m2.hoogte_bron FROM fm m2 WHERE m.fust_key IS NOT NULL AND m2.fust_key = m.fust_key AND m2.height_mm IS NOT NULL ORDER BY m2.fust_type_id LIMIT 1) hx ON true
34287
+ ),
34288
+ hoogte AS (
34289
+ SELECT x.*,
34290
+ public.logistics_pos_num(x.hand_h::text) AS hh,
34291
+ public.logistics_pos_num(x.a->>'s15_cm') AS s15,
34292
+ public.logistics_pos_num(x.a->>'s02_cm') AS s02,
34293
+ (SELECT b.bron FROM public.logistics_gegevens_bron b
34294
+ WHERE b.onderwerp = 'product' AND b.sleutel = x.id::text AND b.veld = 'plant_height_cm'
34295
+ AND public.logistics_pos_num(b.waarde) = public.logistics_pos_num(x.hand_h::text) LIMIT 1) AS hand_bron
34296
+ FROM maat x
34297
+ )
34298
+ SELECT h.id, h.product, h.plants_per_crate::int, h.fid, h.mname, h.fbron,
34299
+ h.l, h.w, h.h, h.lb, h.wb, h.hb,
34300
+ coalesce(h.hh, h.s15, h.s02),
34301
+ CASE WHEN h.hh IS NOT NULL THEN coalesce(h.hand_bron, 'handmatig')
34302
+ WHEN h.s15 IS NOT NULL THEN 'floricode_s15'
34303
+ WHEN h.s02 IS NOT NULL THEN 'floricode_s02' END,
34304
+ public.logistics_pos_num(regexp_replace(coalesce(h.weight::text, ''), '[^0-9.,]', '', 'g')),
34305
+ coalesce(h.mpad, (SELECT fm.pad FROM fm LIMIT 1)), h.a->>'pad',
34306
+ CASE
34307
+ WHEN h.fid IS NULL AND h.a->>'vbn_package_code' IS NULL AND h.a->>'custom_package_id' IS NULL
34308
+ THEN format('Bij "%s" is geen fustsoort bekend: kies de fustsoort bij het product in Voorraad.', h.product)
34309
+ WHEN h.fid IS NULL
34310
+ THEN format('Bij "%s" bestaat de fustsoort (VBN %s) niet in Voorraad → Fusten.', h.product, coalesce(h.a->>'vbn_package_code', h.a->>'custom_package_id'))
34311
+ WHEN h.l IS NULL OR h.w IS NULL OR h.h IS NULL
34312
+ THEN format('Fust %s heeft geen volledige maten in Floricode, Floriday of Voorraad → Fusten (ontbreekt: %s).', coalesce(h.mname, 'onbekend'),
34313
+ array_to_string(array_remove(ARRAY[CASE WHEN h.l IS NULL THEN 'lengte' END, CASE WHEN h.w IS NULL THEN 'breedte' END, CASE WHEN h.h IS NULL THEN 'hoogte' END], NULL), ', '))
34314
+ WHEN coalesce(h.hh, h.s15, h.s02) IS NULL
34315
+ THEN format('Bij "%s" is de planthoogte inclusief pot onbekend (geen S15/S02 in Floricode of Floriday, niet handmatig ingevuld).', h.product)
34316
+ END
34317
+ FROM hoogte h;
34318
+ END $$;
34319
+ GRANT EXECUTE ON FUNCTION public.logistics_product_maten(uuid[]) TO authenticated, service_role;
34320
+
34321
+ -- Bestaande view blijft met dezelfde kolommen werken; de bron is nu logistics_product_maten.
34322
+ DO $fs$ BEGIN
34323
+ EXECUTE $v$CREATE OR REPLACE VIEW public.logistics_product_dimensions AS
34324
+ SELECT m.product_id, m.product_name, m.plants_per_crate, m.fust_type_id,
34325
+ CASE m.fust_bron WHEN 'vbn' THEN 'floriday' ELSE m.fust_bron END AS fust_source,
34326
+ m.fust_name, m.fust_length_mm, m.fust_width_mm, m.fust_height_mm,
34327
+ m.plant_height_cm, m.height_source,
34328
+ CASE WHEN m.missing_reason IS NULL THEN coalesce(nullif(m.fust_bron, 'vbn'), 'floriday') ELSE 'onbekend' END AS dimension_source,
34329
+ m.missing_reason
34330
+ FROM public.logistics_product_maten(NULL) m$v$;
34331
+ EXCEPTION WHEN undefined_table OR undefined_column OR undefined_function OR invalid_table_definition THEN
34332
+ RAISE NOTICE 'logistiek-engine: view logistics_product_dimensions niet vervangen: %', SQLERRM;
34333
+ END $fs$;
34334
+
34335
+ -- 3+E4: gegevens voor karindeling, nu met bronnen uit logistics_product_maten.
34336
+ -- Gewicht per fust alleen gevraagd als de gebruikte ladingdrager een kg-grens heeft.
34337
+ -- Lagen per kar: laaghoogte = max(fusthoogte, planthoogte inclusief pot).
34338
+ CREATE OR REPLACE FUNCTION public.logistics_data_gereedheid(p_product_ids uuid[] DEFAULT NULL)
34339
+ RETURNS TABLE(product_id uuid, product text, ladingdrager text, methode text, in_open_order boolean,
34340
+ ontbrekend text[], velden jsonb, voorstel jsonb, uitleg text)
34341
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
34342
+ #variable_conflict use_column
34343
+ DECLARE v_methode text; v_carrier text; c record; v_ids uuid[];
34344
+ BEGIN
34345
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_data_gereedheid');
34346
+ SELECT CASE WHEN coalesce(s.settings->'karrenindeling'->'fields'->>'cart_layout_method', s.settings->'karrenindeling'->>'cart_layout_method') = 'afmetingen'
34347
+ THEN 'afmetingen' ELSE 'belading' END,
34348
+ nullif(btrim(coalesce(s.settings->'karrenindeling'->'fields'->>'default_carrier_type', s.settings->'karrenindeling'->>'default_carrier_type')), '')
34349
+ INTO v_methode, v_carrier
34350
+ FROM public.logistics_rule_configs s WHERE s.is_active ORDER BY s.updated_at DESC LIMIT 1;
34351
+ v_methode := coalesce(v_methode, 'belading');
34352
+ SELECT t.code, t.name, t.layer_length_cm, t.layer_width_cm, t.max_height_cm, t.max_weight_kg INTO c
34353
+ FROM public.load_carrier_types t
34354
+ WHERE t.is_active AND (t.code = v_carrier OR (v_carrier IS NULL AND t.is_default))
34355
+ ORDER BY (t.code = v_carrier) DESC NULLS LAST LIMIT 1;
34356
+
34357
+ SELECT array_agg(DISTINCT x) INTO v_ids FROM (
34358
+ SELECT oi.product_id AS x FROM public.orders o JOIN public.order_items oi ON oi.order_id = o.id
34359
+ WHERE p_product_ids IS NULL AND o.deleted_at IS NULL AND o.cancelled_at IS NULL AND coalesce(o.is_reservation, false) = false
34360
+ AND lower(coalesce(o.status, '')) = 'nog te picken' AND oi.product_id IS NOT NULL
34361
+ UNION SELECT unnest(p_product_ids)) q WHERE x IS NOT NULL;
34362
+ IF v_ids IS NULL THEN RETURN; END IF;
34363
+
34364
+ RETURN QUERY
34365
+ WITH open_p AS (
34366
+ SELECT DISTINCT oi.product_id FROM public.orders o JOIN public.order_items oi ON oi.order_id = o.id
34367
+ WHERE o.deleted_at IS NULL AND o.cancelled_at IS NULL AND coalesce(o.is_reservation, false) = false
34368
+ AND lower(coalesce(o.status, '')) = 'nog te picken' AND oi.product_id = ANY(v_ids)
34369
+ ),
34370
+ p AS (
34371
+ SELECT pr.id, pr.product, pr.plants_per_crate, pr.crates_per_layer, pr.layers_per_cart, pr.floriday_packing_source,
34372
+ mt.fust_type_id, mt.fust_name, mt.fust_bron, mt.fust_length_mm AS length_mm, mt.fust_width_mm AS width_mm, mt.fust_height_mm AS height_mm,
34373
+ mt.lengte_bron, mt.breedte_bron, mt.hoogte_bron, mt.plant_height_cm, mt.height_source, mt.weight_kg,
34374
+ (op.product_id IS NOT NULL) AS in_open
34375
+ FROM public.products pr
34376
+ JOIN public.logistics_product_maten(v_ids) mt ON mt.product_id = pr.id
34377
+ LEFT JOIN open_p op ON op.product_id = pr.id
34378
+ ),
34379
+ m AS (
34380
+ SELECT p.*,
34381
+ array_remove(ARRAY[
34382
+ CASE WHEN coalesce(p.plants_per_crate, 0) <= 0 THEN 'plants_per_crate' END,
34383
+ CASE WHEN v_methode = 'belading' AND coalesce(p.crates_per_layer, 0) <= 0 THEN 'crates_per_layer' END,
34384
+ CASE WHEN v_methode = 'belading' AND coalesce(p.layers_per_cart, 0) <= 0 THEN 'layers_per_cart' END,
34385
+ CASE WHEN v_methode = 'afmetingen' AND p.fust_type_id IS NULL THEN 'fust_type_id' END,
34386
+ CASE WHEN v_methode = 'afmetingen' AND p.fust_type_id IS NOT NULL AND (p.length_mm IS NULL OR p.width_mm IS NULL OR p.height_mm IS NULL) THEN 'fust_maten' END,
34387
+ CASE WHEN v_methode = 'afmetingen' AND p.plant_height_cm IS NULL THEN 'plant_height_cm' END,
34388
+ CASE WHEN coalesce(c.max_weight_kg, 0) > 0 AND p.weight_kg IS NULL THEN 'weight' END
34389
+ ], NULL) AS mist,
34390
+ CASE WHEN p.length_mm > 0 AND p.width_mm > 0 AND c.layer_length_cm > 0 AND c.layer_width_cm > 0 THEN
34391
+ greatest(floor(c.layer_length_cm * 10 / p.length_mm) * floor(c.layer_width_cm * 10 / p.width_mm),
34392
+ floor(c.layer_length_cm * 10 / p.width_mm) * floor(c.layer_width_cm * 10 / p.length_mm))::int END AS calc_cpl,
34393
+ CASE WHEN p.plant_height_cm > 0 AND c.max_height_cm > 0 THEN
34394
+ floor(c.max_height_cm / greatest(p.plant_height_cm, coalesce(p.height_mm, 0) / 10.0))::int END AS calc_lpc
34395
+ FROM p
34396
+ )
34397
+ SELECT m.id, m.product, c.code, v_methode, m.in_open, m.mist,
34398
+ jsonb_build_object(
34399
+ 'plants_per_crate', jsonb_build_object('waarde', m.plants_per_crate, 'bron', CASE WHEN m.plants_per_crate > 0 THEN coalesce(m.floriday_packing_source, 'product') END),
34400
+ 'crates_per_layer', jsonb_build_object('waarde', m.crates_per_layer, 'bron', CASE WHEN m.crates_per_layer > 0 THEN coalesce(m.floriday_packing_source, 'product') END),
34401
+ 'layers_per_cart', jsonb_build_object('waarde', m.layers_per_cart, 'bron', CASE WHEN m.layers_per_cart > 0 THEN coalesce(m.floriday_packing_source, 'product') END),
34402
+ 'weight', jsonb_build_object('waarde', m.weight_kg, 'bron', CASE WHEN m.weight_kg IS NOT NULL THEN 'product' END,
34403
+ 'nodig', coalesce(c.max_weight_kg, 0) > 0, 'grens_kg', c.max_weight_kg),
34404
+ 'plant_height_cm', jsonb_build_object('waarde', m.plant_height_cm, 'bron', m.height_source, 'inclusief_pot', true),
34405
+ 'fust_type_id', jsonb_build_object('waarde', m.fust_type_id, 'bron', m.fust_bron, 'naam', m.fust_name),
34406
+ 'fust_length_mm', jsonb_build_object('waarde', m.length_mm, 'bron', m.lengte_bron),
34407
+ 'fust_width_mm', jsonb_build_object('waarde', m.width_mm, 'bron', m.breedte_bron),
34408
+ 'fust_height_mm', jsonb_build_object('waarde', m.height_mm, 'bron', m.hoogte_bron)),
34409
+ jsonb_strip_nulls(jsonb_build_object(
34410
+ 'crates_per_layer', CASE WHEN m.calc_cpl > 0 THEN jsonb_build_object('waarde', m.calc_cpl, 'bron', 'berekend',
34411
+ 'berekening', format('fust %s×%s mm op laag %s×%s cm (%s)', m.length_mm, m.width_mm, c.layer_length_cm, c.layer_width_cm, c.name)) END,
34412
+ 'layers_per_cart', CASE WHEN m.calc_lpc > 0 THEN jsonb_build_object('waarde', m.calc_lpc, 'bron', 'berekend',
34413
+ 'berekening', format('hoogte %s cm / grootste van (plant incl. pot %s cm, fust %s cm)', c.max_height_cm, m.plant_height_cm, round(coalesce(m.height_mm, 0) / 10.0, 1))) END)),
34414
+ CASE WHEN cardinality(m.mist) = 0 THEN 'Compleet.'
34415
+ ELSE format('Bij "%s" ontbreekt: %s. Vul dit in bij het product (Voorraad) of hier met "Invullen".', m.product,
34416
+ array_to_string(ARRAY(SELECT CASE f WHEN 'plants_per_crate' THEN 'planten per fust' WHEN 'crates_per_layer' THEN 'fusten per laag'
34417
+ WHEN 'layers_per_cart' THEN 'lagen per kar' WHEN 'fust_type_id' THEN 'fustsoort' WHEN 'fust_maten' THEN 'fustmaten'
34418
+ WHEN 'plant_height_cm' THEN 'planthoogte inclusief pot'
34419
+ WHEN 'weight' THEN format('gewicht per fust (%s heeft een grens van %s kg)', c.name, c.max_weight_kg) ELSE f END FROM unnest(m.mist) f), ', ')) END
34420
+ FROM m
34421
+ ORDER BY cardinality(m.mist) DESC, m.in_open DESC, m.product
34422
+ LIMIT 2000;
34423
+ END $$;
34424
+ GRANT EXECUTE ON FUNCTION public.logistics_data_gereedheid(uuid[]) TO authenticated, service_role;
34425
+
34426
+ -- 5: "Alles overnemen uit Floricode/Floriday": alleen lege velden, bron vastgelegd.
34427
+ -- p_uitvoeren = false geeft alleen de voorvertoning.
34428
+ CREATE OR REPLACE FUNCTION public.logistics_data_overnemen(p_uitvoeren boolean)
34429
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$
34430
+ #variable_conflict use_column
34431
+ DECLARE v_lijst jsonb := '[]'::jsonb; r record; v_n int := 0; v_ids uuid[];
34432
+ BEGIN
34433
+ PERFORM public.logistics_acc_guard(CASE WHEN p_uitvoeren THEN 'beheer' ELSE 'ingelogd' END, 'logistics_data_overnemen');
34434
+ SELECT array_agg(DISTINCT oi.product_id) INTO v_ids FROM public.orders o JOIN public.order_items oi ON oi.order_id = o.id
34435
+ WHERE o.deleted_at IS NULL AND o.cancelled_at IS NULL AND coalesce(o.is_reservation, false) = false
34436
+ AND lower(coalesce(o.status, '')) = 'nog te picken' AND oi.product_id IS NOT NULL;
34437
+ IF v_ids IS NULL THEN RETURN jsonb_build_object('uitgevoerd', false, 'velden', v_lijst, 'aantal', 0); END IF;
34438
+ FOR r IN
34439
+ SELECT m.*, p.fust_type_id AS hand_fid, p.plant_height_cm AS hand_h, f.length_mm AS f_l, f.width_mm AS f_w, f.height_mm AS f_h
34440
+ FROM public.logistics_product_maten(v_ids) m
34441
+ JOIN public.products p ON p.id = m.product_id
34442
+ LEFT JOIN public.fust_types f ON f.id = m.fust_type_id
34443
+ LOOP
34444
+ IF r.hand_h IS NULL AND r.height_source IN ('floricode_s15', 'floricode_s02') THEN
34445
+ v_lijst := v_lijst || jsonb_build_object('onderwerp', 'product', 'sleutel', r.product_id, 'naam', r.product_name, 'veld', 'plant_height_cm', 'waarde', r.plant_height_cm, 'bron', r.height_source);
34446
+ IF p_uitvoeren THEN
34447
+ UPDATE public.products SET plant_height_cm = r.plant_height_cm, updated_at = now() WHERE id = r.product_id AND plant_height_cm IS NULL;
34448
+ END IF;
34449
+ END IF;
34450
+ IF r.hand_fid IS NULL AND r.fust_type_id IS NOT NULL THEN
34451
+ v_lijst := v_lijst || jsonb_build_object('onderwerp', 'product', 'sleutel', r.product_id, 'naam', r.product_name, 'veld', 'fust_type_id', 'waarde', r.fust_type_id,
34452
+ 'bron', CASE r.fust_bron WHEN 'vbn' THEN 'floriday_vbn' ELSE r.fust_bron END);
34453
+ IF p_uitvoeren THEN
34454
+ UPDATE public.products SET fust_type_id = r.fust_type_id, updated_at = now() WHERE id = r.product_id AND fust_type_id IS NULL;
34455
+ END IF;
34456
+ END IF;
34457
+ IF r.fust_type_id IS NOT NULL AND r.f_l IS NULL AND r.fust_length_mm IS NOT NULL AND r.lengte_bron IN ('floricode', 'floriday') THEN
34458
+ v_lijst := v_lijst || jsonb_build_object('onderwerp', 'fust', 'sleutel', r.fust_type_id, 'naam', r.fust_name, 'veld', 'length_mm', 'waarde', r.fust_length_mm, 'bron', r.lengte_bron);
34459
+ IF p_uitvoeren THEN UPDATE public.fust_types SET length_mm = round(r.fust_length_mm)::int, updated_at = now() WHERE id = r.fust_type_id AND length_mm IS NULL; END IF;
34460
+ END IF;
34461
+ IF r.fust_type_id IS NOT NULL AND r.f_w IS NULL AND r.fust_width_mm IS NOT NULL AND r.breedte_bron IN ('floricode', 'floriday') THEN
34462
+ v_lijst := v_lijst || jsonb_build_object('onderwerp', 'fust', 'sleutel', r.fust_type_id, 'naam', r.fust_name, 'veld', 'width_mm', 'waarde', r.fust_width_mm, 'bron', r.breedte_bron);
34463
+ IF p_uitvoeren THEN UPDATE public.fust_types SET width_mm = round(r.fust_width_mm)::int, updated_at = now() WHERE id = r.fust_type_id AND width_mm IS NULL; END IF;
34464
+ END IF;
34465
+ IF r.fust_type_id IS NOT NULL AND r.f_h IS NULL AND r.fust_height_mm IS NOT NULL AND r.hoogte_bron IN ('floricode', 'floriday') THEN
34466
+ v_lijst := v_lijst || jsonb_build_object('onderwerp', 'fust', 'sleutel', r.fust_type_id, 'naam', r.fust_name, 'veld', 'height_mm', 'waarde', r.fust_height_mm, 'bron', r.hoogte_bron);
34467
+ IF p_uitvoeren THEN UPDATE public.fust_types SET height_mm = round(r.fust_height_mm)::int, updated_at = now() WHERE id = r.fust_type_id AND height_mm IS NULL; END IF;
34468
+ END IF;
34469
+ END LOOP;
34470
+ -- Ontdubbelen (meerdere producten met hetzelfde fust).
34471
+ SELECT coalesce(jsonb_agg(DISTINCT e), '[]'::jsonb) INTO v_lijst FROM jsonb_array_elements(v_lijst) e;
34472
+ IF p_uitvoeren THEN
34473
+ INSERT INTO public.logistics_gegevens_bron(onderwerp, sleutel, veld, waarde, bron, toelichting, vastgelegd_door)
34474
+ SELECT e->>'onderwerp', e->>'sleutel', e->>'veld', e->>'waarde', e->>'bron', 'Overgenomen via de startlijst', auth.uid()
34475
+ FROM jsonb_array_elements(v_lijst) e
34476
+ ON CONFLICT (onderwerp, sleutel, veld) DO UPDATE SET waarde = EXCLUDED.waarde, bron = EXCLUDED.bron, toelichting = EXCLUDED.toelichting,
34477
+ vastgelegd_door = EXCLUDED.vastgelegd_door, vastgelegd_op = now();
34478
+ END IF;
34479
+ v_n := jsonb_array_length(v_lijst);
34480
+ RETURN jsonb_build_object('uitgevoerd', p_uitvoeren, 'aantal', v_n, 'velden', v_lijst,
34481
+ 'per_bron', (SELECT coalesce(jsonb_object_agg(b, n), '{}'::jsonb) FROM (SELECT e->>'bron' b, count(*) n FROM jsonb_array_elements(v_lijst) e GROUP BY 1) q));
34482
+ END $$;
34483
+ GRANT EXECUTE ON FUNCTION public.logistics_data_overnemen(boolean) TO authenticated, service_role;
34484
+
34485
+ -- 4: algemene standaard-laagmaten per Floriday-ladingdragertype (vaste, klant-onafhankelijke referentie).
34486
+ -- Alleen lengte en breedte; hoogte en gewichtsgrens hebben geen algemene standaard en vult de beheerder zelf in.
34487
+ CREATE OR REPLACE FUNCTION public.logistics_ladingdrager_referentie()
34488
+ RETURNS TABLE(floriday_code text, lengte_cm integer, breedte_cm integer, bron text)
34489
+ LANGUAGE sql IMMUTABLE SET search_path TO 'public' AS $$
34490
+ VALUES ('DANISH_TROLLEY', 135, 56, 'Standaard Deense container (CC), 1350×565 mm'),
34491
+ ('DANISH_TROLLEY_NON_CC_RFID', 135, 56, 'Standaard Deense container, 1350×565 mm'),
34492
+ ('DANISH_TROLLEY_WATER_TRAY', 135, 56, 'Standaard Deense container met waterbak, 1350×565 mm'),
34493
+ ('AUCTION_TROLLEY', 135, 56, 'Standaard veilingstapelwagen, 1350×565 mm'),
34494
+ ('EURO_PALLET', 120, 80, 'Europallet (EPAL), 1200×800 mm'),
34495
+ ('HT_EURO_PALLET', 120, 80, 'Europallet (EPAL, HT), 1200×800 mm'),
34496
+ ('BLOCK_PALLET', 120, 100, 'Blokpallet, 1200×1000 mm')
34497
+ $$;
34498
+ GRANT EXECUTE ON FUNCTION public.logistics_ladingdrager_referentie() TO authenticated, service_role;
34499
+
34500
+ CREATE OR REPLACE FUNCTION public.logistics_ladingdrager_voorstellen()
34501
+ RETURNS TABLE(code text, naam text, floriday_code text, veld text, huidig numeric, voorstel numeric, bron text, actie text)
34502
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
34503
+ #variable_conflict use_column
34504
+ BEGIN
34505
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_ladingdrager_voorstellen');
34506
+ RETURN QUERY
34507
+ WITH k AS (
34508
+ SELECT DISTINCT ON (t.code) t.code, t.name, m.floriday_code, t.layer_length_cm, t.layer_width_cm, r.lengte_cm, r.breedte_cm, r.bron
34509
+ FROM public.load_carrier_types t
34510
+ JOIN public.logistics_floriday_carrier_map m ON m.carrier_code = t.code
34511
+ JOIN public.logistics_ladingdrager_referentie() r ON r.floriday_code = m.floriday_code
34512
+ WHERE t.is_active
34513
+ ORDER BY t.code, m.floriday_code
34514
+ )
34515
+ SELECT k.code, k.name, k.floriday_code, v.veld, v.huidig, v.voorstel, k.bron,
34516
+ CASE WHEN coalesce(v.huidig, 0) <= 0 THEN 'invullen' WHEN v.huidig = v.voorstel THEN 'gelijk' ELSE 'afwijkend' END
34517
+ FROM k CROSS JOIN LATERAL (VALUES ('layer_length_cm', k.layer_length_cm::numeric, k.lengte_cm::numeric),
34518
+ ('layer_width_cm', k.layer_width_cm::numeric, k.breedte_cm::numeric)) v(veld, huidig, voorstel)
34519
+ ORDER BY k.code, v.veld;
34520
+ END $$;
34521
+ GRANT EXECUTE ON FUNCTION public.logistics_ladingdrager_voorstellen() TO authenticated, service_role;
34522
+
34523
+ -- Bevestigen per ladingdrager: schrijft alleen lege velden en legt de bron vast. Overschrijft nooit.
34524
+ CREATE OR REPLACE FUNCTION public.logistics_ladingdrager_voorstel_bevestigen(p_code text)
34525
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$
34526
+ #variable_conflict use_column
34527
+ DECLARE r record; v_n int := 0;
34528
+ BEGIN
34529
+ PERFORM public.logistics_acc_guard('beheer', 'logistics_ladingdrager_voorstel_bevestigen');
34530
+ FOR r IN SELECT * FROM public.logistics_ladingdrager_voorstellen() v WHERE v.code = p_code AND v.actie = 'invullen' LOOP
34531
+ IF r.veld = 'layer_length_cm' THEN
34532
+ UPDATE public.load_carrier_types SET layer_length_cm = r.voorstel::int, updated_at = now() WHERE code = p_code AND coalesce(layer_length_cm, 0) <= 0;
34533
+ ELSIF r.veld = 'layer_width_cm' THEN
34534
+ UPDATE public.load_carrier_types SET layer_width_cm = r.voorstel::int, updated_at = now() WHERE code = p_code AND coalesce(layer_width_cm, 0) <= 0;
34535
+ END IF;
34536
+ IF FOUND THEN
34537
+ v_n := v_n + 1;
34538
+ INSERT INTO public.logistics_gegevens_bron(onderwerp, sleutel, veld, waarde, bron, toelichting, vastgelegd_door)
34539
+ VALUES ('ladingdrager', p_code, r.veld, r.voorstel::text, 'standaard:' || r.floriday_code, r.bron, auth.uid())
34540
+ ON CONFLICT (onderwerp, sleutel, veld) DO UPDATE SET waarde = EXCLUDED.waarde, bron = EXCLUDED.bron, toelichting = EXCLUDED.toelichting,
34541
+ vastgelegd_door = EXCLUDED.vastgelegd_door, vastgelegd_op = now();
34542
+ END IF;
34543
+ END LOOP;
34544
+ RETURN jsonb_build_object('ok', true, 'code', p_code, 'ingevuld', v_n);
34545
+ END $$;
34546
+ GRANT EXECUTE ON FUNCTION public.logistics_ladingdrager_voorstel_bevestigen(text) TO authenticated, service_role;
34547
+
34548
+ -- 5: startlijst voor een nieuwe shell.
34549
+ CREATE OR REPLACE FUNCTION public.logistics_startlijst()
34550
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
34551
+ DECLARE v_drag int; v_drag_mist int; v_werk boolean; v_prod int; v_prod_mist int;
34552
+ BEGIN
34553
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_startlijst');
34554
+ SELECT count(*), count(*) FILTER (WHERE coalesce(t.layer_length_cm, 0) <= 0 OR coalesce(t.layer_width_cm, 0) <= 0 OR coalesce(t.max_height_cm, 0) <= 0)
34555
+ INTO v_drag, v_drag_mist
34556
+ FROM public.load_carrier_types t
34557
+ WHERE t.is_active AND NOT EXISTS (SELECT 1 FROM public.logistics_floriday_carrier_map m WHERE m.carrier_code = t.code AND m.floriday_code = 'NONE');
34558
+ SELECT EXISTS (SELECT 1 FROM public.logistics_rule_configs s WHERE s.is_active AND s.settings ? 'karrenindeling') INTO v_werk;
34559
+ SELECT count(*), count(*) FILTER (WHERE cardinality(g.ontbrekend) > 0) INTO v_prod, v_prod_mist FROM public.logistics_data_gereedheid(NULL) g;
34560
+ RETURN jsonb_build_object(
34561
+ 'ladingdragers', jsonb_build_object('klaar', v_drag > 0 AND v_drag_mist = 0, 'aantal', v_drag, 'onvolledig', v_drag_mist),
34562
+ 'werkwijze', jsonb_build_object('klaar', v_werk),
34563
+ 'gereedheid', jsonb_build_object('klaar', v_prod_mist = 0, 'producten', v_prod, 'onvolledig', v_prod_mist));
34564
+ END $$;
34565
+ GRANT EXECUTE ON FUNCTION public.logistics_startlijst() TO authenticated, service_role;
34566
+
34567
+ CREATE OR REPLACE FUNCTION public.logistics_install_check_198()
34568
+ RETURNS TABLE(controle text, ok boolean, toelichting text)
34569
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
34570
+ BEGIN
34571
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_install_check_198');
34572
+ RETURN QUERY SELECT 'Engine: fustmaten uit één bron'::text,
34573
+ to_regprocedure('public.logistics_fust_maten()') IS NOT NULL,
34574
+ CASE WHEN to_regprocedure('public.voorraad_fust_dimensions()') IS NOT NULL THEN 'via Voorraad (voorraad_fust_dimensions)'
34575
+ ELSE 'eigen terugval: Floricode → Floriday → handmatig' END;
34576
+ RETURN QUERY SELECT 'Engine: artikelkenmerken (S15/S02, fust)',
34577
+ to_regprocedure('public.logistics_artikel_vbn(uuid)') IS NOT NULL,
34578
+ CASE WHEN to_regprocedure('public.voorraad_article_vbn(uuid)') IS NOT NULL THEN 'via Voorraad (voorraad_article_vbn)'
34579
+ ELSE 'eigen terugval: Floriday-handelsitem' END;
34580
+ RETURN QUERY SELECT 'Engine: view logistics_product_dimensions gebruikt logistics_product_maten',
34581
+ coalesce(pg_get_viewdef('public.logistics_product_dimensions'::regclass) LIKE '%logistics_product_maten%', false), 'view';
34582
+ RETURN QUERY SELECT 'Engine: gereedheid vraagt gewicht alleen bij kg-grens',
34583
+ coalesce(pg_get_functiondef('public.logistics_data_gereedheid(uuid[])'::regprocedure) LIKE '%max_weight_kg%', false), 'logistics_data_gereedheid';
34584
+ RETURN QUERY SELECT 'Engine: startlijst en standaardmaten ladingdragers',
34585
+ to_regprocedure('public.logistics_startlijst()') IS NOT NULL AND to_regprocedure('public.logistics_ladingdrager_voorstel_bevestigen(text)') IS NOT NULL,
34586
+ 'logistics_startlijst + logistics_ladingdrager_voorstel_bevestigen';
34587
+ RETURN QUERY SELECT 'Engine: bronregistratie', to_regclass('public.logistics_gegevens_bron') IS NOT NULL, 'logistics_gegevens_bron';
34588
+ EXCEPTION WHEN undefined_table OR undefined_function THEN
34589
+ RETURN QUERY SELECT 'Engine: maten 1.0.198'::text, false, SQLERRM;
34590
+ END $$;
34591
+ GRANT EXECUTE ON FUNCTION public.logistics_install_check_198() TO authenticated, service_role;
34592
+
34593
+ DO $do$ DECLARE d text;
34594
+ BEGIN
34595
+ d := pg_get_functiondef('public.logistics_install_check()'::regprocedure);
34596
+ IF position('logistics_install_check_198' in d) = 0 THEN
34597
+ d := replace(d, $r$ RETURN QUERY SELECT * FROM public.logistics_access_check();$r$,
34598
+ $r$ RETURN QUERY SELECT * FROM public.logistics_install_check_198();
34599
+ RETURN QUERY SELECT * FROM public.logistics_access_check();$r$);
34600
+ EXECUTE d;
34601
+ END IF;
34602
+ EXCEPTION WHEN undefined_function THEN NULL;
34603
+ END $do$;