@flowselections/logistiek-engine 1.0.177 → 1.0.180

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 (28) hide show
  1. package/db/schema/logistiek-engine.sql +850 -0
  2. package/dist-lib/components/BeladingIndelingCard.d.ts.map +1 -1
  3. package/dist-lib/components/BeladingIndelingCard.js +2 -0
  4. package/dist-lib/integrations/supabase/previewAuthStorage.d.ts +11 -5
  5. package/dist-lib/integrations/supabase/previewAuthStorage.d.ts.map +1 -1
  6. package/dist-lib/integrations/supabase/previewAuthStorage.js +38 -21
  7. package/dist-lib/integrations/supabase/types.d.ts +137 -0
  8. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  9. package/dist-lib/lib/beladingRegels.d.ts +78 -0
  10. package/dist-lib/lib/beladingRegels.d.ts.map +1 -0
  11. package/dist-lib/lib/beladingRegels.js +143 -0
  12. package/dist-lib/lib/bulkLoadCarrierPlanning.server.js +2 -2
  13. package/dist-lib/lib/eigenTraysticker.d.ts +17 -0
  14. package/dist-lib/lib/eigenTraysticker.d.ts.map +1 -0
  15. package/dist-lib/lib/eigenTraysticker.js +32 -0
  16. package/dist-lib/lib/loadCarrierPlanning.server.d.ts +35 -3
  17. package/dist-lib/lib/loadCarrierPlanning.server.d.ts.map +1 -1
  18. package/dist-lib/lib/loadCarrierPlanning.server.js +251 -89
  19. package/dist-lib/lib/modulePrompts.d.ts.map +1 -1
  20. package/dist-lib/lib/modulePrompts.js +76 -0
  21. package/dist-lib/lib/moduleVersion.d.ts +1 -1
  22. package/dist-lib/lib/moduleVersion.js +1 -1
  23. package/dist-lib/lib/planQueue.server.d.ts.map +1 -1
  24. package/dist-lib/lib/planQueue.server.js +26 -2
  25. package/dist-lib/lib/stickerQueue.server.d.ts +1 -1
  26. package/dist-lib/lib/stickerQueue.server.d.ts.map +1 -1
  27. package/dist-lib/lib/stickerQueue.server.js +88 -0
  28. package/package.json +1 -1
@@ -29523,3 +29523,853 @@ BEGIN
29523
29523
  'documenttype', v_type, 'breedte_mm', v_l.breedte_mm, 'hoogte_mm', v_l.hoogte_mm, 'reden', NULL) || v_extra;
29524
29524
  END;
29525
29525
  $function$;
29526
+
29527
+
29528
+ -- ===== 1.0.178: fusten en land per kar, eigen traystickers =====
29529
+ -- 1.0.178 A: fusten en land per kar in control_get_todays_carts (achteraan toegevoegd)
29530
+ DO $m$ BEGIN
29531
+ IF pg_get_function_result('public.control_get_todays_carts()'::regprocedure) NOT LIKE '%crates_on_cart%' THEN
29532
+ DROP FUNCTION public.control_get_todays_carts();
29533
+ END IF;
29534
+ END $m$;
29535
+
29536
+ CREATE OR REPLACE FUNCTION public.control_get_todays_carts()
29537
+ RETURNS TABLE(cart_number_id uuid, cart_number text, order_source text, order_id text, cart_index integer, order_number text, pick_date date, status text, printed_at timestamp with time zone, printed_today boolean, crates_on_cart integer, country_code text)
29538
+ LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public'
29539
+ AS $function$
29540
+ WITH vandaag AS (
29541
+ SELECT (now() AT TIME ZONE public.logistics_tz())::date AS d
29542
+ ),
29543
+ idx AS (
29544
+ SELECT DISTINCT ON (i.cart_id) i.*
29545
+ FROM public.logistics_cart_index((SELECT d FROM vandaag), (SELECT d FROM vandaag)) i
29546
+ ORDER BY i.cart_id, i.order_number
29547
+ ),
29548
+ gecontroleerd AS (
29549
+ SELECT cci.cart_number_id, cci.order_source, cci.order_id, cci.cart_index
29550
+ FROM public.cart_control_items cci
29551
+ JOIN public.cart_control_sessions ccs ON ccs.id = cci.session_id
29552
+ WHERE ccs.control_date = (SELECT d FROM vandaag)
29553
+ AND cci.action = 'checked'
29554
+ )
29555
+ SELECT
29556
+ i.cart_id,
29557
+ COALESCE(cn.cart_number, i.cart_label),
29558
+ i.order_source,
29559
+ i.order_uuid::text,
29560
+ i.cart_index,
29561
+ i.order_number,
29562
+ i.pick_date,
29563
+ COALESCE(cn.status, 'pending'),
29564
+ i.printed_at,
29565
+ (i.printed_at >= (SELECT d FROM vandaag)::timestamptz),
29566
+ (SELECT sum(ci.crates)::int FROM public.order_cart_items ci WHERE ci.cart_id = i.cart_id),
29567
+ (SELECT CASE WHEN upper(btrim(l.country)) ~ '^[A-Z]{2}$' THEN upper(btrim(l.country)) END
29568
+ FROM public.orders o
29569
+ JOIN public.customer_delivery_locations l
29570
+ ON l.id = public.logistics_try_uuid(o.custom_fields->>'delivery_location')
29571
+ WHERE o.id = i.order_uuid
29572
+ LIMIT 1)
29573
+ FROM idx i
29574
+ LEFT JOIN public.cart_numbers cn
29575
+ ON cn.order_source = i.order_source
29576
+ AND cn.order_id = i.order_uuid::text
29577
+ AND cn.cart_index = i.cart_index
29578
+ LEFT JOIN gecontroleerd g1 ON g1.cart_number_id = i.cart_id
29579
+ LEFT JOIN gecontroleerd g2
29580
+ ON g2.order_source = i.order_source
29581
+ AND g2.order_id = i.order_uuid::text
29582
+ AND g2.cart_index = i.cart_index
29583
+ WHERE COALESCE(cn.status, 'pending') <> 'delivered'
29584
+ AND g1.cart_number_id IS NULL
29585
+ AND g2.order_id IS NULL
29586
+ ORDER BY i.order_number, i.cart_index;
29587
+ $function$;
29588
+ REVOKE ALL ON FUNCTION public.control_get_todays_carts() FROM PUBLIC, anon;
29589
+ GRANT EXECUTE ON FUNCTION public.control_get_todays_carts() TO authenticated, service_role;
29590
+
29591
+ -- 1.0.178 B: eigen traystickers
29592
+ ALTER TABLE public.logistics_sticker_prints DROP CONSTRAINT IF EXISTS logistics_sticker_prints_soort_check;
29593
+ ALTER TABLE public.logistics_sticker_prints ADD CONSTRAINT logistics_sticker_prints_soort_check
29594
+ CHECK (soort = ANY (ARRAY['sticker','traysticker','eigen_sticker','eigen_traysticker']));
29595
+
29596
+ CREATE OR REPLACE FUNCTION public.logistics_own_traysticker_sync(p_limit integer DEFAULT 200)
29597
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
29598
+ AS $function$
29599
+ DECLARE
29600
+ s jsonb; v_tray text; v_rel text; v_station text;
29601
+ r record; v_bron text; v_keep text[] := '{}'; n int := 0; n_geblokkeerd int := 0; n_vrij int := 0; n_uit int := 0;
29602
+ BEGIN
29603
+ SELECT settings INTO s FROM public.logistics_rule_configs
29604
+ ORDER BY is_active DESC, (status = 'actief') DESC, updated_at DESC LIMIT 1;
29605
+ v_tray := public.logistics_rule_text(s, 'pickstations', 'traysticker_release', 'uit');
29606
+ IF v_tray = 'uit' THEN
29607
+ RETURN jsonb_build_object('trayRelease', v_tray, 'regels', 0);
29608
+ END IF;
29609
+ v_rel := public.logistics_rule_text(s, 'pickstations', 'sticker_release', 'with_pick_release');
29610
+ v_station := CASE WHEN v_rel = 'with_packing_slip'
29611
+ THEN public.logistics_rule_text(s, 'pickstations', 'packing_slip_station', '')
29612
+ ELSE public.logistics_rule_text(s, 'pickstations', 'sticker_station', '') END;
29613
+
29614
+ FOR r IN
29615
+ SELECT o.id AS order_id, o.order_number, ci.cart_id,
29616
+ bool_or(ci.layer_index IS NULL) AS zonder_indeling,
29617
+ array_agg(DISTINCT ci.layer_index) FILTER (WHERE ci.layer_index IS NOT NULL) AS lagen
29618
+ FROM public.orders o
29619
+ JOIN public.order_items oi ON oi.order_id = o.id
29620
+ JOIN public.order_cart_items ci ON ci.order_item_id = oi.id
29621
+ WHERE coalesce(o.order_source, '') <> 'floriday'
29622
+ AND o.deleted_at IS NULL AND o.cancelled_at IS NULL
29623
+ AND coalesce(o.status, '') <> 'geannuleerd'
29624
+ AND oi.quarantined_at IS NULL
29625
+ AND o.delivery_date >= current_date - 1
29626
+ GROUP BY o.id, o.order_number, ci.cart_id
29627
+ LIMIT greatest(1, p_limit)
29628
+ LOOP
29629
+ IF r.zonder_indeling OR r.lagen IS NULL THEN
29630
+ v_bron := r.order_id::text || ':' || r.cart_id::text || ':geen-indeling';
29631
+ v_keep := v_keep || v_bron;
29632
+ INSERT INTO public.logistics_sticker_prints (soort, bron_id, order_id, order_number, cart_id, kopieen, status, reden)
29633
+ VALUES ('eigen_traysticker', v_bron, r.order_id, r.order_number, r.cart_id, 1, 'geblokkeerd',
29634
+ 'Trayindeling ontbreekt voor deze kar: niet elke orderregel heeft een tray (laag). Deel de kar opnieuw in; de rest van de order gaat door.')
29635
+ ON CONFLICT DO NOTHING;
29636
+ n_geblokkeerd := n_geblokkeerd + 1;
29637
+ CONTINUE;
29638
+ END IF;
29639
+ FOR i IN 1 .. array_length(r.lagen, 1) LOOP
29640
+ v_bron := r.order_id::text || ':' || r.cart_id::text || ':' || r.lagen[i]::text;
29641
+ v_keep := v_keep || v_bron;
29642
+ INSERT INTO public.logistics_sticker_prints (soort, bron_id, order_id, order_number, cart_id, kopieen, print_plek_id, status, reden)
29643
+ VALUES ('eigen_traysticker', v_bron, r.order_id, r.order_number, r.cart_id, 1,
29644
+ public.logistics_sticker_cart_plek(r.cart_id, v_station), 'wacht_op_moment', 'Wacht op de pakbon van deze kar.')
29645
+ ON CONFLICT DO NOTHING;
29646
+ n := n + 1;
29647
+ END LOOP;
29648
+ END LOOP;
29649
+
29650
+ -- Indeling hersteld of tray vervallen: niet meer printen (alleen nog niet geprinte rijen).
29651
+ UPDATE public.logistics_sticker_prints p
29652
+ SET status = 'niet_printen', reden = 'Tray hoort niet meer bij deze kar of order vervallen.', updated_at = now()
29653
+ WHERE p.soort = 'eigen_traysticker' AND p.status IN ('wacht_op_moment', 'geblokkeerd', 'klaar')
29654
+ AND NOT (p.bron_id = ANY (v_keep))
29655
+ AND p.order_id IN (SELECT o.id FROM public.orders o WHERE o.delivery_date >= current_date - 1);
29656
+ GET DIAGNOSTICS n_uit = ROW_COUNT;
29657
+
29658
+ -- Vrijgave: met de pakbon van de kar (zoals eigen stickers).
29659
+ UPDATE public.logistics_sticker_prints p
29660
+ SET status = 'klaar', reden = NULL,
29661
+ print_plek_id = coalesce(p.print_plek_id, public.logistics_sticker_cart_plek(p.cart_id, v_station)),
29662
+ updated_at = now()
29663
+ WHERE p.soort = 'eigen_traysticker' AND p.status = 'wacht_op_moment'
29664
+ AND p.cart_id IS NOT NULL AND public.logistics_cart_slip_queued(p.cart_id);
29665
+ GET DIAGNOSTICS n_vrij = ROW_COUNT;
29666
+
29667
+ RETURN jsonb_build_object('trayRelease', v_tray, 'trays', n, 'geblokkeerd', n_geblokkeerd, 'vrijgegeven', n_vrij, 'niet_printen', n_uit);
29668
+ END; $function$;
29669
+ REVOKE ALL ON FUNCTION public.logistics_own_traysticker_sync(integer) FROM PUBLIC, anon, authenticated;
29670
+ GRANT EXECUTE ON FUNCTION public.logistics_own_traysticker_sync(integer) TO service_role;
29671
+
29672
+ CREATE OR REPLACE FUNCTION public.logistics_own_traysticker_payload(p_id uuid)
29673
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public'
29674
+ AS $function$
29675
+ DECLARE p record; o record; v_laag int; v_nr int; v_tot int; v_loc text; v_prod jsonb;
29676
+ BEGIN
29677
+ SELECT * INTO p FROM public.logistics_sticker_prints WHERE id = p_id AND soort = 'eigen_traysticker';
29678
+ IF p.id IS NULL THEN RETURN jsonb_build_object('ok', false, 'reden', 'Traysticker niet gevonden.'); END IF;
29679
+ v_laag := nullif(split_part(p.bron_id, ':', 3), '')::int;
29680
+ SELECT * INTO o FROM public.orders WHERE id = p.order_id;
29681
+ SELECT count(DISTINCT ci.layer_index),
29682
+ count(DISTINCT ci.layer_index) FILTER (WHERE ci.layer_index <= v_laag)
29683
+ INTO v_tot, v_nr
29684
+ FROM public.order_cart_items ci JOIN public.order_items oi ON oi.id = ci.order_item_id
29685
+ WHERE oi.order_id = p.order_id AND ci.cart_id = p.cart_id AND ci.layer_index IS NOT NULL;
29686
+ SELECT coalesce(jsonb_agg(jsonb_build_object(
29687
+ 'artikelnummer', prd.article_number,
29688
+ 'naam', coalesce(ci.product_name, oi.product_name),
29689
+ 'aantal', coalesce(ci.crates_in_layer, ci.crates)) ORDER BY coalesce(ci.product_name, oi.product_name)), '[]'::jsonb)
29690
+ INTO v_prod
29691
+ FROM public.order_cart_items ci JOIN public.order_items oi ON oi.id = ci.order_item_id
29692
+ LEFT JOIN public.products prd ON prd.id = coalesce(ci.product_id, oi.product_id)
29693
+ WHERE oi.order_id = p.order_id AND ci.cart_id = p.cart_id AND ci.layer_index = v_laag;
29694
+ SELECT nullif(btrim(concat_ws(', ', nullif(btrim(l.name), ''), nullif(btrim(l.city), ''), nullif(btrim(l.country), ''))), '')
29695
+ INTO v_loc
29696
+ FROM public.customer_delivery_locations l
29697
+ WHERE l.id = public.logistics_try_uuid(o.custom_fields->>'delivery_location');
29698
+ IF v_tot IS NULL OR v_tot = 0 OR jsonb_array_length(v_prod) = 0 THEN
29699
+ RETURN jsonb_build_object('ok', false, 'reden', 'Trayindeling ontbreekt voor deze tray. Deel de kar opnieuw in.');
29700
+ END IF;
29701
+ RETURN jsonb_build_object('ok', true, 'payload', jsonb_build_object(
29702
+ 'order_id', o.id, 'order_number', o.order_number, 'ordernummer', o.order_number,
29703
+ 'klantnaam', o.customer_name, 'leverdatum', o.delivery_date, 'afleverlocatie', v_loc,
29704
+ 'cart_id', p.cart_id, 'tray_nummer', v_nr, 'tray_totaal', v_tot, 'tray_producten', v_prod));
29705
+ END; $function$;
29706
+ REVOKE ALL ON FUNCTION public.logistics_own_traysticker_payload(uuid) FROM PUBLIC, anon, authenticated;
29707
+ GRANT EXECUTE ON FUNCTION public.logistics_own_traysticker_payload(uuid) TO service_role;
29708
+ -- ===== 1.0.178 correctie: eigen traysticker per fust =====
29709
+ -- 1.0.178 correctie: eigen traysticker per FUST (niet per laag).
29710
+ -- Sleutel: order:kar:karregel:fustnummer. Crates onbekend: één geblokkeerde rij per karregel.
29711
+ -- Planten op fust x van y binnen een orderregel: ppc, laatste fust = de rest (nooit <= 0).
29712
+ CREATE OR REPLACE FUNCTION public.logistics_tray_plants(p_ppc integer, p_regel_totaal integer, p_fust integer, p_fusten integer)
29713
+ RETURNS integer LANGUAGE sql IMMUTABLE SET search_path = public AS $tp$
29714
+ SELECT CASE
29715
+ WHEN p_ppc IS NULL OR p_ppc <= 0 THEN NULL
29716
+ WHEN p_fust < p_fusten OR p_regel_totaal IS NULL THEN p_ppc
29717
+ WHEN p_regel_totaal - p_ppc * (p_fusten - 1) > 0 THEN p_regel_totaal - p_ppc * (p_fusten - 1)
29718
+ ELSE p_ppc
29719
+ END
29720
+ $tp$;
29721
+
29722
+ CREATE OR REPLACE FUNCTION public.logistics_own_traysticker_sync(p_limit integer DEFAULT 200)
29723
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
29724
+ AS $function$
29725
+ DECLARE
29726
+ s jsonb; v_tray text; v_rel text; v_station text;
29727
+ r record; v_bron text; v_keep text[] := '{}'; n int := 0; n_geblokkeerd int := 0; n_vrij int := 0; n_uit int := 0;
29728
+ BEGIN
29729
+ SELECT settings INTO s FROM public.logistics_rule_configs
29730
+ ORDER BY is_active DESC, (status = 'actief') DESC, updated_at DESC LIMIT 1;
29731
+ v_tray := public.logistics_rule_text(s, 'pickstations', 'traysticker_release', 'uit');
29732
+ IF v_tray = 'uit' THEN
29733
+ RETURN jsonb_build_object('trayRelease', v_tray, 'regels', 0);
29734
+ END IF;
29735
+ v_rel := public.logistics_rule_text(s, 'pickstations', 'sticker_release', 'with_pick_release');
29736
+ v_station := CASE WHEN v_rel = 'with_packing_slip'
29737
+ THEN public.logistics_rule_text(s, 'pickstations', 'packing_slip_station', '')
29738
+ ELSE public.logistics_rule_text(s, 'pickstations', 'sticker_station', '') END;
29739
+
29740
+ FOR r IN
29741
+ SELECT o.id AS order_id, o.order_number, ci.cart_id, ci.id AS ci_id, ci.crates
29742
+ FROM public.orders o
29743
+ JOIN public.order_items oi ON oi.order_id = o.id
29744
+ JOIN public.order_cart_items ci ON ci.order_item_id = oi.id
29745
+ WHERE coalesce(o.order_source, '') <> 'floriday'
29746
+ AND o.deleted_at IS NULL AND o.cancelled_at IS NULL
29747
+ AND coalesce(o.status, '') <> 'geannuleerd'
29748
+ AND oi.quarantined_at IS NULL
29749
+ AND o.delivery_date >= current_date - 1
29750
+ ORDER BY o.order_number, ci.cart_id, ci.created_at, ci.id
29751
+ LIMIT greatest(1, p_limit)
29752
+ LOOP
29753
+ IF r.crates IS NULL OR r.crates <= 0 THEN
29754
+ v_bron := r.order_id::text || ':' || r.cart_id::text || ':' || r.ci_id::text || ':onbekend';
29755
+ v_keep := v_keep || v_bron;
29756
+ INSERT INTO public.logistics_sticker_prints (soort, bron_id, order_id, order_number, cart_id, kopieen, status, reden)
29757
+ VALUES ('eigen_traysticker', v_bron, r.order_id, r.order_number, r.cart_id, 1, 'geblokkeerd',
29758
+ 'Aantal fusten onbekend voor deze regel op de kar. De rest van de order gaat door.')
29759
+ ON CONFLICT DO NOTHING;
29760
+ n_geblokkeerd := n_geblokkeerd + 1;
29761
+ CONTINUE;
29762
+ END IF;
29763
+ FOR k IN 1 .. r.crates LOOP
29764
+ v_bron := r.order_id::text || ':' || r.cart_id::text || ':' || r.ci_id::text || ':' || k::text;
29765
+ v_keep := v_keep || v_bron;
29766
+ INSERT INTO public.logistics_sticker_prints (soort, bron_id, order_id, order_number, cart_id, kopieen, print_plek_id, status, reden)
29767
+ VALUES ('eigen_traysticker', v_bron, r.order_id, r.order_number, r.cart_id, 1,
29768
+ public.logistics_sticker_cart_plek(r.cart_id, v_station), 'wacht_op_moment', 'Wacht op de pakbon van deze kar.')
29769
+ ON CONFLICT DO NOTHING;
29770
+ n := n + 1;
29771
+ END LOOP;
29772
+ END LOOP;
29773
+
29774
+ -- Oude laagsleutels, vervallen fusten of orders: niet meer printen (alleen nog niet geprinte rijen, geen DELETE).
29775
+ UPDATE public.logistics_sticker_prints p
29776
+ SET status = 'niet_printen', reden = 'Fust hoort niet meer bij deze kar of order vervallen.', updated_at = now()
29777
+ WHERE p.soort = 'eigen_traysticker' AND p.status IN ('wacht_op_moment', 'geblokkeerd', 'klaar')
29778
+ AND NOT (p.bron_id = ANY (v_keep))
29779
+ AND p.order_id IN (SELECT o.id FROM public.orders o WHERE o.delivery_date >= current_date - 1);
29780
+ GET DIAGNOSTICS n_uit = ROW_COUNT;
29781
+
29782
+ UPDATE public.logistics_sticker_prints p
29783
+ SET status = 'klaar', reden = NULL,
29784
+ print_plek_id = coalesce(p.print_plek_id, public.logistics_sticker_cart_plek(p.cart_id, v_station)),
29785
+ updated_at = now()
29786
+ WHERE p.soort = 'eigen_traysticker' AND p.status = 'wacht_op_moment'
29787
+ AND p.cart_id IS NOT NULL AND public.logistics_cart_slip_queued(p.cart_id);
29788
+ GET DIAGNOSTICS n_vrij = ROW_COUNT;
29789
+
29790
+ RETURN jsonb_build_object('trayRelease', v_tray, 'fusten', n, 'geblokkeerd', n_geblokkeerd, 'vrijgegeven', n_vrij, 'niet_printen', n_uit);
29791
+ END; $function$;
29792
+ REVOKE ALL ON FUNCTION public.logistics_own_traysticker_sync(integer) FROM PUBLIC, anon, authenticated;
29793
+ GRANT EXECUTE ON FUNCTION public.logistics_own_traysticker_sync(integer) TO service_role;
29794
+
29795
+ CREATE OR REPLACE FUNCTION public.logistics_own_traysticker_payload(p_id uuid)
29796
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public'
29797
+ AS $function$
29798
+ DECLARE
29799
+ p record; o record; c record; v_ci uuid; v_k int; v_nr int; v_tot int; v_loc text;
29800
+ v_ppc int; v_regel_tot int; v_item_voor int; v_item_fusten int; v_aantal int; v_prod jsonb;
29801
+ BEGIN
29802
+ SELECT * INTO p FROM public.logistics_sticker_prints WHERE id = p_id AND soort = 'eigen_traysticker';
29803
+ IF p.id IS NULL THEN RETURN jsonb_build_object('ok', false, 'reden', 'Traysticker niet gevonden.'); END IF;
29804
+ v_ci := public.logistics_try_uuid(split_part(p.bron_id, ':', 3));
29805
+ v_k := CASE WHEN split_part(p.bron_id, ':', 4) ~ '^[0-9]+$' THEN split_part(p.bron_id, ':', 4)::int END;
29806
+ IF v_ci IS NULL OR v_k IS NULL THEN
29807
+ RETURN jsonb_build_object('ok', false, 'reden', 'Aantal fusten onbekend');
29808
+ END IF;
29809
+ SELECT ci.*, oi.unit, oi.quantity, oi.plants_per_crate AS ppc_regel, oi.product_name AS oi_naam,
29810
+ prd.article_number, prd.plants_per_crate AS ppc_product, ci.order_item_id AS item_id
29811
+ INTO c
29812
+ FROM public.order_cart_items ci JOIN public.order_items oi ON oi.id = ci.order_item_id
29813
+ LEFT JOIN public.products prd ON prd.id = coalesce(ci.product_id, oi.product_id)
29814
+ WHERE ci.id = v_ci;
29815
+ IF c.id IS NULL OR c.crates IS NULL OR c.crates < v_k THEN
29816
+ RETURN jsonb_build_object('ok', false, 'reden', 'Aantal fusten onbekend');
29817
+ END IF;
29818
+ SELECT * INTO o FROM public.orders WHERE id = p.order_id;
29819
+
29820
+ -- Fustnummer x van y binnen de order (alle karren), volgorde kar, regel, fust.
29821
+ WITH alle AS (
29822
+ SELECT ci.id, ci.crates, row_number() OVER (ORDER BY ci.cart_id, ci.created_at, ci.id) AS rn
29823
+ FROM public.order_cart_items ci JOIN public.order_items oi ON oi.id = ci.order_item_id
29824
+ WHERE oi.order_id = p.order_id AND oi.quarantined_at IS NULL AND coalesce(ci.crates, 0) > 0
29825
+ )
29826
+ SELECT sum(crates)::int,
29827
+ coalesce(sum(crates) FILTER (WHERE rn < (SELECT rn FROM alle WHERE id = v_ci)), 0)::int + v_k
29828
+ INTO v_tot, v_nr FROM alle;
29829
+
29830
+ -- Planten per fust: karregel, dan orderregel, dan product. Laatste fust van de regel: de rest.
29831
+ v_ppc := coalesce(nullif(c.plants_per_crate, 0), nullif(c.ppc_regel, 0), nullif(c.ppc_product, 0))::int;
29832
+ IF v_ppc IS NULL THEN
29833
+ RETURN jsonb_build_object('ok', false, 'reden', 'Planten per fust onbekend (niet op de regel en niet bij het product). Vul planten per fust in.');
29834
+ END IF;
29835
+ v_regel_tot := public.logistics_own_sticker_count(c.unit, c.quantity, c.ppc_regel, c.ppc_product);
29836
+ SELECT coalesce(sum(ci.crates) FILTER (WHERE (ci.cart_id, ci.created_at, ci.id) < (c.cart_id, c.created_at, c.id)), 0)::int,
29837
+ coalesce(sum(ci.crates), 0)::int
29838
+ INTO v_item_voor, v_item_fusten
29839
+ FROM public.order_cart_items ci WHERE ci.order_item_id = c.item_id AND coalesce(ci.crates, 0) > 0;
29840
+ v_aantal := public.logistics_tray_plants(v_ppc, v_regel_tot, v_item_voor + v_k, v_item_fusten);
29841
+
29842
+ v_prod := jsonb_build_array(jsonb_strip_nulls(jsonb_build_object(
29843
+ 'artikelnummer', c.article_number,
29844
+ 'naam', coalesce(c.product_name, c.oi_naam),
29845
+ 'aantal', v_aantal,
29846
+ 'onderdelen', CASE WHEN jsonb_typeof(to_jsonb(c.components)) = 'array' AND jsonb_array_length(to_jsonb(c.components)) > 0
29847
+ THEN to_jsonb(c.components) END)));
29848
+
29849
+ SELECT nullif(btrim(concat_ws(', ', nullif(btrim(l.name), ''), nullif(btrim(l.city), ''), nullif(btrim(l.country), ''))), '')
29850
+ INTO v_loc
29851
+ FROM public.customer_delivery_locations l
29852
+ WHERE l.id = public.logistics_try_uuid(o.custom_fields->>'delivery_location');
29853
+
29854
+ RETURN jsonb_build_object('ok', true, 'payload', jsonb_build_object(
29855
+ 'order_id', o.id, 'order_number', o.order_number, 'ordernummer', o.order_number,
29856
+ 'klantnaam', o.customer_name, 'leverdatum', o.delivery_date, 'afleverlocatie', v_loc,
29857
+ 'cart_id', p.cart_id, 'tray_nummer', v_nr, 'tray_totaal', v_tot, 'tray_producten', v_prod));
29858
+ END; $function$;
29859
+ REVOKE ALL ON FUNCTION public.logistics_own_traysticker_payload(uuid) FROM PUBLIC, anon, authenticated;
29860
+ GRANT EXECUTE ON FUNCTION public.logistics_own_traysticker_payload(uuid) TO service_role;
29861
+
29862
+ -- ===== 1.0.179: noodreparaties karindeling =====
29863
+ -- 1.0.179 fase 1: noodreparaties karindeling (alle ordersoorten FO/HO/MO/AO).
29864
+ -- Alleen eigen Engine-objecten; idempotent; geen omgevingswaarden; geen datacorrectie.
29865
+
29866
+ -- 3. Bevestigingen van een onwaarschijnlijke belading: één per order en verpakking.
29867
+ CREATE TABLE IF NOT EXISTS public.logistics_belading_bevestigingen (
29868
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
29869
+ order_id uuid NOT NULL,
29870
+ verpakking_sleutel text NOT NULL,
29871
+ redenen text[] NOT NULL DEFAULT '{}',
29872
+ bevestigd_door uuid,
29873
+ bevestigd_op timestamptz NOT NULL DEFAULT now(),
29874
+ created_at timestamptz NOT NULL DEFAULT now(),
29875
+ UNIQUE (order_id, verpakking_sleutel)
29876
+ );
29877
+ GRANT SELECT ON public.logistics_belading_bevestigingen TO authenticated;
29878
+ GRANT ALL ON public.logistics_belading_bevestigingen TO service_role;
29879
+ ALTER TABLE public.logistics_belading_bevestigingen ENABLE ROW LEVEL SECURITY;
29880
+ DO $p$ BEGIN
29881
+ IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'logistics_belading_bevestigingen' AND policyname = 'beheer leest bevestigingen') THEN
29882
+ CREATE POLICY "beheer leest bevestigingen" ON public.logistics_belading_bevestigingen
29883
+ FOR SELECT TO authenticated USING (public.logistics_is_beheer());
29884
+ END IF;
29885
+ END $p$;
29886
+
29887
+ CREATE OR REPLACE FUNCTION public.logistics_belading_bevestig(p_order_id uuid, p_sleutel text, p_redenen text[] DEFAULT '{}')
29888
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
29889
+ AS $function$
29890
+ BEGIN
29891
+ IF NOT public.logistics_is_beheer() THEN
29892
+ RAISE EXCEPTION 'Alleen beheer mag een belading bevestigen.';
29893
+ END IF;
29894
+ IF p_order_id IS NULL OR coalesce(btrim(p_sleutel), '') = '' THEN
29895
+ RAISE EXCEPTION 'Order en verpakking zijn verplicht.';
29896
+ END IF;
29897
+ INSERT INTO public.logistics_belading_bevestigingen (order_id, verpakking_sleutel, redenen, bevestigd_door)
29898
+ VALUES (p_order_id, p_sleutel, coalesce(p_redenen, '{}'), auth.uid())
29899
+ ON CONFLICT (order_id, verpakking_sleutel) DO NOTHING;
29900
+ PERFORM public.logistics_plan_enqueue(p_order_id, 'belading bevestigd');
29901
+ RETURN jsonb_build_object('ok', true);
29902
+ END; $function$;
29903
+ REVOKE ALL ON FUNCTION public.logistics_belading_bevestig(uuid, text, text[]) FROM PUBLIC, anon;
29904
+ GRANT EXECUTE ON FUNCTION public.logistics_belading_bevestig(uuid, text, text[]) TO authenticated, service_role;
29905
+
29906
+ -- 5. Lease bezet: opdracht blijft open en komt later terug (nooit 'done' zonder indeling).
29907
+ CREATE OR REPLACE FUNCTION public.logistics_plan_retry_later(p_id uuid, p_seconds integer DEFAULT 30)
29908
+ RETURNS void LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
29909
+ AS $function$
29910
+ BEGIN
29911
+ UPDATE public.logistics_plan_queue
29912
+ SET status = 'pending', claimed_at = NULL,
29913
+ next_attempt_at = now() + make_interval(secs => greatest(5, least(coalesce(p_seconds, 30), 600))),
29914
+ error = 'Indeling van deze buffer was bezet; later opnieuw.', updated_at = now()
29915
+ WHERE id = p_id AND status IN ('running', 'pending');
29916
+ END; $function$;
29917
+ REVOKE ALL ON FUNCTION public.logistics_plan_retry_later(uuid, integer) FROM PUBLIC, anon, authenticated;
29918
+ GRANT EXECUTE ON FUNCTION public.logistics_plan_retry_later(uuid, integer) TO service_role;
29919
+
29920
+ -- 6. Oude SQL-indeler onbruikbaar: geen EXECUTE voor PUBLIC, anon of authenticated.
29921
+ DO $r$ BEGIN
29922
+ IF to_regprocedure('public.logistics_plan_all(uuid)') IS NOT NULL THEN
29923
+ REVOKE ALL ON FUNCTION public.logistics_plan_all(uuid) FROM PUBLIC, anon, authenticated;
29924
+ END IF;
29925
+ IF to_regprocedure('public.logistics_plan_buffer_sql(uuid)') IS NOT NULL THEN
29926
+ REVOKE ALL ON FUNCTION public.logistics_plan_buffer_sql(uuid) FROM PUBLIC, anon, authenticated;
29927
+ END IF;
29928
+ END $r$;
29929
+
29930
+ -- 7. Karnummers aan het kar-id (stabiel na herindeling). Kolom en index alleen toevoegen.
29931
+ DO $c$ BEGIN
29932
+ IF to_regclass('public.cart_numbers') IS NULL THEN
29933
+ RAISE NOTICE 'logistiek-engine: cart_numbers ontbreekt (andere module), kar-id-koppeling overgeslagen';
29934
+ ELSE
29935
+ ALTER TABLE public.cart_numbers ADD COLUMN IF NOT EXISTS cart_id uuid;
29936
+ IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'cart_numbers_pick_date_cart_id_uniq') THEN
29937
+ IF EXISTS (SELECT 1 FROM public.cart_numbers WHERE cart_id IS NOT NULL GROUP BY pick_date, cart_id HAVING count(*) > 1) THEN
29938
+ RAISE NOTICE 'logistiek-engine: dubbele kar-id per dag in cart_numbers; unieke index niet aangemaakt';
29939
+ ELSE
29940
+ CREATE UNIQUE INDEX cart_numbers_pick_date_cart_id_uniq ON public.cart_numbers (pick_date, cart_id) WHERE cart_id IS NOT NULL;
29941
+ END IF;
29942
+ END IF;
29943
+ END IF;
29944
+ END $c$;
29945
+
29946
+ -- Dubbele kar-indexen per order (twee karren met hetzelfde volgnummer): alleen-lezen lijst.
29947
+ CREATE OR REPLACE FUNCTION public.logistics_cart_seq_dubbel(p_order_uuid uuid DEFAULT NULL)
29948
+ RETURNS TABLE(order_uuid uuid, cart_index integer, cart_ids uuid[])
29949
+ LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public'
29950
+ AS $function$
29951
+ WITH ruw AS (
29952
+ SELECT oc.id AS cart_id, oc.cart_number AS cart_index, oc.order_id AS order_uuid
29953
+ FROM logistics_active.order_carts oc
29954
+ WHERE oc.order_id IS NOT NULL AND (p_order_uuid IS NULL OR oc.order_id = p_order_uuid)
29955
+ UNION
29956
+ SELECT oc.id, oc.cart_number, oi.order_id
29957
+ FROM logistics_active.order_carts oc
29958
+ JOIN public.order_cart_items oci ON oci.cart_id = oc.id
29959
+ JOIN logistics_active.order_items oi ON oi.id = oci.order_item_id
29960
+ WHERE oc.order_id IS NULL AND oi.order_id IS NOT NULL AND (p_order_uuid IS NULL OR oi.order_id = p_order_uuid)
29961
+ )
29962
+ SELECT order_uuid, cart_index, array_agg(DISTINCT cart_id ORDER BY cart_id)
29963
+ FROM ruw GROUP BY order_uuid, cart_index HAVING count(DISTINCT cart_id) > 1;
29964
+ $function$;
29965
+ REVOKE ALL ON FUNCTION public.logistics_cart_seq_dubbel(uuid) FROM PUBLIC, anon, authenticated;
29966
+ GRANT EXECUTE ON FUNCTION public.logistics_cart_seq_dubbel(uuid) TO service_role;
29967
+
29968
+ -- logistics_cart_seq: nooit stil een kar laten vallen. Bij een dubbel volgnummer: fout.
29969
+ -- Karnummer eerst via kar-id, anders via order + index (bestaande nummers).
29970
+ CREATE OR REPLACE FUNCTION public.logistics_cart_seq(p_order_uuid uuid DEFAULT NULL::uuid)
29971
+ RETURNS TABLE(order_uuid uuid, cart_id uuid, cart_index integer, cart_seq integer, cart_number text, cart_total integer)
29972
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public'
29973
+ AS $function$
29974
+ DECLARE d record;
29975
+ BEGIN
29976
+ SELECT * INTO d FROM public.logistics_cart_seq_dubbel(p_order_uuid) LIMIT 1;
29977
+ IF d.order_uuid IS NOT NULL THEN
29978
+ RAISE EXCEPTION 'Karnummering ongeldig: order % heeft % karren met volgnummer %. Deel de order opnieuw in; er wordt geen kar weggelaten.',
29979
+ d.order_uuid, array_length(d.cart_ids, 1), d.cart_index;
29980
+ END IF;
29981
+ RETURN QUERY
29982
+ WITH deelname AS (
29983
+ SELECT oc.id AS cart_id, oc.cart_number AS cart_index, oc.order_id AS order_uuid
29984
+ FROM public.order_carts oc
29985
+ WHERE oc.order_id IS NOT NULL AND (p_order_uuid IS NULL OR oc.order_id = p_order_uuid)
29986
+ UNION
29987
+ SELECT oc.id, oc.cart_number, oi.order_id
29988
+ FROM public.order_carts oc
29989
+ JOIN public.order_cart_items oci ON oci.cart_id = oc.id
29990
+ JOIN logistics_active.order_items oi ON oi.id = oci.order_item_id
29991
+ WHERE oc.order_id IS NULL AND oi.order_id IS NOT NULL AND (p_order_uuid IS NULL OR oi.order_id = p_order_uuid)
29992
+ ),
29993
+ genummerd AS (
29994
+ SELECT dd.order_uuid, dd.cart_id, dd.cart_index,
29995
+ dense_rank() OVER (PARTITION BY dd.order_uuid ORDER BY dd.cart_index)::int AS cart_seq,
29996
+ count(*) OVER (PARTITION BY dd.order_uuid)::int AS cart_total
29997
+ FROM deelname dd
29998
+ )
29999
+ SELECT g.order_uuid, g.cart_id, g.cart_index, g.cart_seq,
30000
+ COALESCE(
30001
+ (SELECT cn.cart_number FROM public.cart_numbers cn WHERE cn.cart_id = g.cart_id ORDER BY cn.assigned_at DESC LIMIT 1),
30002
+ (SELECT cn.cart_number FROM public.cart_numbers cn WHERE cn.order_id = g.order_uuid::text AND cn.cart_index = g.cart_index LIMIT 1),
30003
+ g.cart_seq::text),
30004
+ g.cart_total
30005
+ FROM genummerd g;
30006
+ END; $function$;
30007
+ REVOKE ALL ON FUNCTION public.logistics_cart_seq(uuid) FROM PUBLIC, anon;
30008
+ GRANT EXECUTE ON FUNCTION public.logistics_cart_seq(uuid) TO authenticated, service_role;
30009
+
30010
+ -- 8. Kar wisselen: eerst vergrendelen en pickup_locked controleren, dan pas verwijderen; geen stille 1.
30011
+ CREATE OR REPLACE FUNCTION public.logistics_apply_cart_swap(p_delete_ids uuid[], p_new_carts jsonb)
30012
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
30013
+ AS $function$
30014
+ DECLARE
30015
+ v_del uuid[] := COALESCE(p_delete_ids, ARRAY[]::uuid[]);
30016
+ v_cart jsonb; v_row jsonb; v_id uuid; v_map jsonb := '[]'::jsonb;
30017
+ v_jobs int := 0; v_carts int := 0; v_items int := 0; v_locked int;
30018
+ BEGIN
30019
+ IF jsonb_typeof(COALESCE(p_new_carts, '[]'::jsonb)) <> 'array' THEN
30020
+ RAISE EXCEPTION 'Nieuwe karren ontbreken of zijn ongeldig.';
30021
+ END IF;
30022
+ IF EXISTS (
30023
+ SELECT 1 FROM jsonb_array_elements(COALESCE(p_new_carts, '[]'::jsonb)) c,
30024
+ jsonb_array_elements(COALESCE(c.value->'items', '[]'::jsonb)) i
30025
+ WHERE coalesce((i.value->>'plants_per_crate')::numeric, 0) <= 0
30026
+ OR coalesce((i.value->>'crates')::numeric, 0) < 0) THEN
30027
+ RAISE EXCEPTION 'Karwissel geweigerd: een karregel mist planten per fust (belading_onvolledig).';
30028
+ END IF;
30029
+
30030
+ IF array_length(v_del, 1) IS NOT NULL THEN
30031
+ PERFORM 1 FROM public.order_carts WHERE id = ANY(v_del) ORDER BY id FOR UPDATE;
30032
+ SELECT count(*) INTO v_locked FROM public.order_carts WHERE id = ANY(v_del) AND COALESCE(pickup_locked, false);
30033
+ IF v_locked > 0 THEN
30034
+ RAISE EXCEPTION 'Karwissel geweigerd: % kar(ren) zijn al vastgezet (opgehaald). Er is niets gewijzigd.', v_locked;
30035
+ END IF;
30036
+ UPDATE public.print_jobs
30037
+ SET status = 'geannuleerd'::print_job_status,
30038
+ foutmelding = 'Vervallen: kar verwijderd bij herindeling',
30039
+ updated_at = now()
30040
+ WHERE status IN ('wachtend'::print_job_status, 'bezig'::print_job_status)
30041
+ AND (payload->>'cart_id') = ANY(SELECT id::text FROM unnest(v_del) AS id);
30042
+ GET DIAGNOSTICS v_jobs = ROW_COUNT;
30043
+ DELETE FROM public.pakbon_barcode_inhoud WHERE cart_id = ANY(SELECT id::text FROM unnest(v_del) AS id);
30044
+ DELETE FROM public.order_cart_layer_positions WHERE cart_id = ANY(v_del);
30045
+ DELETE FROM public.order_cart_items WHERE cart_id = ANY(v_del);
30046
+ DELETE FROM public.order_carts WHERE id = ANY(v_del);
30047
+ GET DIAGNOSTICS v_carts = ROW_COUNT;
30048
+ END IF;
30049
+
30050
+ FOR v_cart IN SELECT value FROM jsonb_array_elements(COALESCE(p_new_carts, '[]'::jsonb)) LOOP
30051
+ v_row := v_cart->'row';
30052
+ v_id := gen_random_uuid();
30053
+ INSERT INTO public.order_carts (id, order_id, buffer_id, cart_number, total_carts, is_auto, is_complete, source,
30054
+ occupancy_pct, total_layers_used, carrier_type, carrier_type_id, max_layers, weight_kg, algorithm_version, layout_method)
30055
+ 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),
30056
+ COALESCE(r.source, 'auto'), COALESCE(r.occupancy_pct, 0), COALESCE(r.total_layers_used, 0),
30057
+ COALESCE(r.carrier_type, 'trolley'), r.carrier_type_id, r.max_layers, r.weight_kg, COALESCE(r.algorithm_version, 1), r.layout_method
30058
+ FROM jsonb_to_record(v_row) AS r(order_id uuid, buffer_id uuid, cart_number integer, total_carts integer, is_auto boolean,
30059
+ is_complete boolean, source text, occupancy_pct numeric, total_layers_used numeric, carrier_type text, carrier_type_id uuid,
30060
+ max_layers integer, weight_kg numeric, algorithm_version integer, layout_method text);
30061
+
30062
+ INSERT INTO public.order_cart_items (cart_id, order_item_id, product_id, product_name, crates, boxes, weight_kg,
30063
+ plants_per_crate, crates_per_layer, layers_per_cart, layer_fraction, layer_share_pct, layer_index, crates_in_layer, components)
30064
+ SELECT v_id, i.order_item_id, i.product_id, i.product_name, i.crates, i.boxes, i.weight_kg,
30065
+ i.plants_per_crate, COALESCE(i.crates_per_layer, 1), COALESCE(i.layers_per_cart, 1),
30066
+ COALESCE(i.layer_fraction, 0), COALESCE(i.layer_share_pct, 0), i.layer_index, i.crates_in_layer,
30067
+ CASE WHEN jsonb_typeof(i.components) = 'array' THEN i.components END
30068
+ FROM jsonb_to_recordset(COALESCE(v_cart->'items', '[]'::jsonb)) AS i(order_item_id uuid, product_id uuid, product_name text,
30069
+ crates integer, boxes integer, weight_kg numeric, plants_per_crate integer, crates_per_layer integer, layers_per_cart integer,
30070
+ layer_fraction numeric, layer_share_pct numeric, layer_index integer, crates_in_layer integer, components jsonb);
30071
+ v_items := v_items + jsonb_array_length(COALESCE(v_cart->'items', '[]'::jsonb));
30072
+ v_map := v_map || jsonb_build_object('planIndex', (v_cart->>'planIndex')::integer, 'id', v_id);
30073
+ END LOOP;
30074
+
30075
+ RETURN jsonb_build_object('karren', v_map, 'verwijderd', v_carts, 'printopdrachtenGeannuleerd', v_jobs, 'regels', v_items);
30076
+ END;
30077
+ $function$;
30078
+ REVOKE ALL ON FUNCTION public.logistics_apply_cart_swap(uuid[], jsonb) FROM PUBLIC, anon, authenticated;
30079
+ GRANT EXECUTE ON FUNCTION public.logistics_apply_cart_swap(uuid[], jsonb) TO service_role;
30080
+
30081
+ -- 1. Autofix schrijft alleen een volledige, positieve Floriday-verpakking weg (0 = onbekend).
30082
+ CREATE OR REPLACE FUNCTION public.logistics_packing_autofix(p_limit integer DEFAULT 100)
30083
+ RETURNS integer LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
30084
+ AS $function$
30085
+ DECLARE r record; v_reden text; v_aantal integer := 0; v_ppc int; v_cpl int; v_lpc int;
30086
+ BEGIN
30087
+ FOR r IN
30088
+ SELECT d.* FROM public.logistics_packing_deviations d
30089
+ WHERE d.resolved_at IS NULL AND d.scope = 'order_line' AND d.needs_human = false
30090
+ AND d.order_id IS NOT NULL AND d.order_item_id IS NOT NULL
30091
+ ORDER BY d.first_seen_at
30092
+ LIMIT GREATEST(1, LEAST(p_limit, 500))
30093
+ LOOP
30094
+ v_reden := public.logistics_packing_block_reason(r.order_id, r.order_item_id);
30095
+ IF v_reden IS NOT NULL THEN
30096
+ UPDATE public.logistics_packing_deviations SET needs_human = true, human_reason = v_reden WHERE id = r.id;
30097
+ CONTINUE;
30098
+ END IF;
30099
+ v_ppc := nullif(greatest(coalesce((r.floriday_packing->>'pieces_per_package')::numeric, 0), 0), 0)::int;
30100
+ v_cpl := nullif(greatest(coalesce((r.floriday_packing->>'packages_per_layer')::numeric, 0), 0), 0)::int;
30101
+ v_lpc := nullif(greatest(coalesce((r.floriday_packing->>'layers_per_load_carrier')::numeric, 0), 0), 0)::int;
30102
+ IF v_ppc IS NULL OR v_cpl IS NULL OR v_lpc IS NULL THEN
30103
+ UPDATE public.logistics_packing_deviations
30104
+ SET needs_human = true,
30105
+ human_reason = 'Floriday-verpakking onvolledig (0 of leeg = onbekend); niet automatisch overgenomen.'
30106
+ WHERE id = r.id;
30107
+ CONTINUE;
30108
+ END IF;
30109
+ UPDATE public.order_items oi
30110
+ SET plants_per_crate = v_ppc, crates_per_layer = v_cpl, layers_per_cart = v_lpc
30111
+ WHERE oi.id = r.order_item_id AND oi.packing_locked_at IS NULL;
30112
+ IF FOUND THEN
30113
+ v_aantal := v_aantal + 1;
30114
+ UPDATE public.logistics_packing_deviations
30115
+ SET auto_fixed_at = now(), resolved_at = now(), human_reason = 'automatisch bijgewerkt naar de Floriday-stand'
30116
+ WHERE id = r.id;
30117
+ PERFORM public.logistics_trace_event(r.order_id, 'stap:belading_synchronisatie', 'ok', 'belading bijgewerkt naar de Floriday-stand');
30118
+ END IF;
30119
+ END LOOP;
30120
+ RETURN v_aantal;
30121
+ END;
30122
+ $function$;
30123
+
30124
+ -- 9. Reconcile alleen bij een echte wijziging sinds de laatste afgeronde of geblokkeerde opdracht; kleinere porties.
30125
+ CREATE OR REPLACE FUNCTION public.logistics_reconcile_missing_plan_queue()
30126
+ RETURNS integer LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
30127
+ AS $function$
30128
+ DECLARE r record; n integer := 0;
30129
+ BEGIN
30130
+ FOR r IN
30131
+ SELECT o.id FROM public.orders o
30132
+ WHERE o.deleted_at IS NULL
30133
+ AND COALESCE(o.is_reservation,false) = false
30134
+ AND o.plan_locked_at IS NULL
30135
+ AND o.order_number IS NOT NULL
30136
+ AND (o.created_at >= now() - interval '14 days' OR o.updated_at >= now() - interval '14 days')
30137
+ AND NOT EXISTS (SELECT 1 FROM public.logistics_tasks t WHERE t.order_ref = o.order_number)
30138
+ AND NOT EXISTS (SELECT 1 FROM public.logistics_plan_queue q
30139
+ WHERE q.order_id = o.id AND q.status IN ('pending','running'))
30140
+ AND NOT EXISTS (
30141
+ SELECT 1 FROM public.logistics_plan_queue q
30142
+ WHERE q.order_id = o.id AND q.status IN ('geblokkeerd', 'done', 'opgelost')
30143
+ AND q.updated_at >= COALESCE(o.updated_at, o.created_at)
30144
+ AND NOT EXISTS (SELECT 1 FROM logistics_active.order_items oi WHERE oi.order_id = o.id AND oi.created_at > q.updated_at)
30145
+ AND NOT EXISTS (SELECT 1 FROM logistics_active.order_items oi JOIN public.products p ON p.id = oi.product_id
30146
+ WHERE oi.order_id = o.id AND p.updated_at > q.updated_at))
30147
+ ORDER BY o.created_at ASC
30148
+ LIMIT 100
30149
+ LOOP
30150
+ PERFORM public.logistics_plan_enqueue(r.id, 'reconcile');
30151
+ n := n + 1;
30152
+ END LOOP;
30153
+ RETURN n;
30154
+ END;
30155
+ $function$;
30156
+
30157
+ -- 9. Eén open opdracht per order (index alleen als er geen dubbele open opdrachten zijn).
30158
+ DO $q$ BEGIN
30159
+ IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'logistics_plan_queue_open_uniq') THEN
30160
+ IF EXISTS (SELECT 1 FROM public.logistics_plan_queue WHERE status IN ('pending','running','geblokkeerd')
30161
+ GROUP BY order_id HAVING count(*) > 1) THEN
30162
+ RAISE NOTICE 'logistiek-engine: dubbele open planopdrachten; unieke index wacht op samenvoegen (akkoord nodig)';
30163
+ ELSE
30164
+ CREATE UNIQUE INDEX logistics_plan_queue_open_uniq ON public.logistics_plan_queue (order_id)
30165
+ WHERE status IN ('pending','running','geblokkeerd');
30166
+ END IF;
30167
+ END IF;
30168
+ END $q$;
30169
+
30170
+ -- Alleen-lezen lijst: orders met meer dan één open planopdracht.
30171
+ CREATE OR REPLACE FUNCTION public.logistics_plan_queue_dubbel()
30172
+ RETURNS TABLE(order_id uuid, open_opdrachten bigint, oudste timestamptz, nieuwste timestamptz)
30173
+ LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public'
30174
+ AS $function$
30175
+ SELECT q.order_id, count(*), min(q.enqueued_at), max(q.enqueued_at)
30176
+ FROM public.logistics_plan_queue q
30177
+ WHERE q.status IN ('pending','running','geblokkeerd')
30178
+ GROUP BY q.order_id HAVING count(*) > 1;
30179
+ $function$;
30180
+ REVOKE ALL ON FUNCTION public.logistics_plan_queue_dubbel() FROM PUBLIC, anon, authenticated;
30181
+ GRANT EXECUTE ON FUNCTION public.logistics_plan_queue_dubbel() TO service_role;
30182
+
30183
+ -- 10. Buffer uit: elke order krijgt een eigen buffer (sleutel solo:<order-id>) en wordt dus per order ingedeeld.
30184
+ CREATE OR REPLACE FUNCTION public.logistics_apply_buffer_decision(p_order_id uuid)
30185
+ RETURNS uuid LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
30186
+ AS $function$
30187
+ DECLARE
30188
+ o public.orders%ROWTYPE;
30189
+ cf jsonb; d jsonb; v_ref text; v_close timestamptz; v_key text;
30190
+ v_buffer uuid; v_closed boolean; v_try int := 0; v_on_cart boolean; v_solo boolean := false;
30191
+ BEGIN
30192
+ SELECT * INTO o FROM public.orders WHERE id = p_order_id;
30193
+ IF NOT FOUND OR o.deleted_at IS NOT NULL THEN RETURN NULL; END IF;
30194
+ cf := COALESCE(o.custom_fields, '{}'::jsonb);
30195
+
30196
+ IF public.logistics_order_buffer_excluded(o.id) THEN
30197
+ IF o.buffer_id IS NOT NULL THEN
30198
+ SELECT EXISTS (SELECT 1 FROM public.order_items oi
30199
+ JOIN logistics_active.order_cart_items ci ON ci.order_item_id = oi.id
30200
+ JOIN logistics_active.order_carts oc ON oc.id = ci.cart_id
30201
+ WHERE oi.order_id = o.id AND oc.buffer_id = o.buffer_id)
30202
+ INTO v_on_cart;
30203
+ IF NOT v_on_cart THEN
30204
+ PERFORM set_config('logistics.buffer_writer', 'engine', true);
30205
+ UPDATE public.orders SET buffer_id = NULL WHERE id = o.id;
30206
+ BEGIN
30207
+ INSERT INTO public.logistics_planner_trace(order_id, message)
30208
+ VALUES (o.id, 'afgemeld; uit buffer gehaald');
30209
+ EXCEPTION WHEN others THEN NULL; END;
30210
+ END IF;
30211
+ END IF;
30212
+ RETURN NULL;
30213
+ END IF;
30214
+
30215
+ d := public.logistics_buffer_decision(public.logistics_buffer_payload(o.id), NULL);
30216
+
30217
+ IF (d ->> 'reason') = 'buffering_disabled' THEN
30218
+ v_solo := true;
30219
+ ELSIF COALESCE((d ->> 'buffer')::boolean, false) IS NOT TRUE OR (d ->> 'bufferRef') IS NULL THEN
30220
+ IF o.buffer_id IS NOT NULL THEN
30221
+ UPDATE public.order_carts SET buffer_id = NULL WHERE order_id = o.id;
30222
+ PERFORM set_config('logistics.buffer_writer', 'engine', true);
30223
+ UPDATE public.orders SET buffer_id = NULL WHERE id = o.id;
30224
+ END IF;
30225
+ DELETE FROM public.order_buffers b
30226
+ WHERE NOT EXISTS (SELECT 1 FROM public.orders x WHERE x.buffer_id = b.id)
30227
+ AND NOT EXISTS (SELECT 1 FROM public.order_carts oc WHERE oc.buffer_id = b.id);
30228
+ RETURN NULL;
30229
+ END IF;
30230
+
30231
+ IF v_solo THEN
30232
+ v_ref := 'solo:' || o.id::text;
30233
+ v_close := NULL;
30234
+ ELSE
30235
+ v_ref := d ->> 'bufferRef';
30236
+ BEGIN v_close := NULLIF(d ->> 'bufferCloseAt', '')::timestamptz;
30237
+ EXCEPTION WHEN others THEN v_close := NULL; END;
30238
+ END IF;
30239
+
30240
+ LOOP
30241
+ v_key := CASE WHEN v_try = 0 THEN v_ref ELSE v_ref || '#' || v_try END;
30242
+ SELECT id INTO v_buffer FROM public.order_buffers WHERE buffer_key = v_key;
30243
+ IF v_buffer IS NULL THEN
30244
+ INSERT INTO public.order_buffers (buffer_key, grouping_mode, pickup_date, pickup_location, customer_id, close_at)
30245
+ VALUES (v_key, CASE WHEN v_solo THEN 'solo' ELSE 'engine' END,
30246
+ COALESCE(cf ->> 'pickup_moment', cf ->> 'pickup_date', o.delivery_date::text),
30247
+ COALESCE(cf ->> 'delivery_location_gln', cf ->> 'delivery_location_id', cf ->> 'delivery_location'),
30248
+ o.customer_id, v_close)
30249
+ ON CONFLICT (buffer_key) DO UPDATE SET grouping_mode = EXCLUDED.grouping_mode
30250
+ RETURNING id INTO v_buffer;
30251
+ EXIT;
30252
+ END IF;
30253
+ IF o.buffer_id = v_buffer OR v_solo THEN EXIT; END IF;
30254
+
30255
+ v_closed := public.logistics_buffer_closed_for_join(v_buffer);
30256
+ IF NOT COALESCE(v_closed, false) THEN
30257
+ UPDATE public.order_buffers
30258
+ SET close_at = LEAST(COALESCE(close_at, v_close), COALESCE(v_close, close_at)), updated_at = now()
30259
+ WHERE id = v_buffer;
30260
+ EXIT;
30261
+ END IF;
30262
+ UPDATE public.order_buffers SET closed_at = COALESCE(closed_at, now()), updated_at = now() WHERE id = v_buffer;
30263
+ v_try := v_try + 1; v_buffer := NULL;
30264
+ IF v_try > 50 THEN RAISE EXCEPTION 'Kan geen open orderbuffer vinden voor sleutel %', v_ref; END IF;
30265
+ END LOOP;
30266
+
30267
+ IF o.buffer_id IS DISTINCT FROM v_buffer THEN
30268
+ PERFORM set_config('logistics.buffer_writer', 'engine', true);
30269
+ UPDATE public.orders SET buffer_id = v_buffer WHERE id = o.id;
30270
+ DELETE FROM public.order_buffers b
30271
+ WHERE NOT EXISTS (SELECT 1 FROM public.orders x WHERE x.buffer_id = b.id)
30272
+ AND NOT EXISTS (SELECT 1 FROM public.order_carts oc WHERE oc.buffer_id = b.id);
30273
+ END IF;
30274
+ RETURN v_buffer;
30275
+ END;
30276
+ $function$;
30277
+
30278
+ -- 13. Eigen traystickers: zolang bij één regel van de order het fustaantal onbekend is, wachten ALLE
30279
+ -- eigen traystickers van die order ("Fustaantal van regel X onbekend"), zodat x van y klopt.
30280
+ CREATE OR REPLACE FUNCTION public.logistics_own_traysticker_sync(p_limit integer DEFAULT 200)
30281
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
30282
+ AS $function$
30283
+ DECLARE
30284
+ s jsonb; v_tray text; v_rel text; v_station text;
30285
+ r record; v_bron text; v_keep text[] := '{}'; v_wacht_orders uuid[] := '{}';
30286
+ n int := 0; n_geblokkeerd int := 0; n_vrij int := 0; n_uit int := 0;
30287
+ BEGIN
30288
+ SELECT settings INTO s FROM public.logistics_rule_configs
30289
+ ORDER BY is_active DESC, (status = 'actief') DESC, updated_at DESC LIMIT 1;
30290
+ v_tray := public.logistics_rule_text(s, 'pickstations', 'traysticker_release', 'uit');
30291
+ IF v_tray = 'uit' THEN
30292
+ RETURN jsonb_build_object('trayRelease', v_tray, 'regels', 0);
30293
+ END IF;
30294
+ v_rel := public.logistics_rule_text(s, 'pickstations', 'sticker_release', 'with_pick_release');
30295
+ v_station := CASE WHEN v_rel = 'with_packing_slip'
30296
+ THEN public.logistics_rule_text(s, 'pickstations', 'packing_slip_station', '')
30297
+ ELSE public.logistics_rule_text(s, 'pickstations', 'sticker_station', '') END;
30298
+
30299
+ FOR r IN
30300
+ WITH regels AS (
30301
+ SELECT o.id AS order_id, o.order_number, ci.cart_id, ci.id AS ci_id, ci.crates, ci.created_at,
30302
+ coalesce(ci.product_name, oi.product_name, 'onbekend') AS regelnaam
30303
+ FROM public.orders o
30304
+ JOIN public.order_items oi ON oi.order_id = o.id
30305
+ JOIN public.order_cart_items ci ON ci.order_item_id = oi.id
30306
+ WHERE coalesce(o.order_source, '') <> 'floriday'
30307
+ AND o.deleted_at IS NULL AND o.cancelled_at IS NULL
30308
+ AND coalesce(o.status, '') <> 'geannuleerd'
30309
+ AND oi.quarantined_at IS NULL
30310
+ AND o.delivery_date >= current_date - 1
30311
+ )
30312
+ SELECT g.*,
30313
+ (SELECT min(x.regelnaam) FROM regels x WHERE x.order_id = g.order_id AND coalesce(x.crates, 0) <= 0) AS onbekende_regel
30314
+ FROM regels g
30315
+ ORDER BY g.order_number, g.cart_id, g.created_at, g.ci_id
30316
+ LIMIT greatest(1, p_limit)
30317
+ LOOP
30318
+ IF r.onbekende_regel IS NOT NULL THEN
30319
+ v_wacht_orders := v_wacht_orders || r.order_id;
30320
+ v_bron := r.order_id::text || ':fustaantal-onbekend';
30321
+ v_keep := v_keep || v_bron;
30322
+ INSERT INTO public.logistics_sticker_prints (soort, bron_id, order_id, order_number, cart_id, kopieen, status, reden)
30323
+ VALUES ('eigen_traysticker', v_bron, r.order_id, r.order_number, NULL, 1, 'geblokkeerd',
30324
+ 'Fustaantal van regel ' || r.onbekende_regel || ' onbekend. Alle traystickers van deze order wachten, zodat de nummering klopt.')
30325
+ ON CONFLICT DO NOTHING;
30326
+ n_geblokkeerd := n_geblokkeerd + 1;
30327
+ CONTINUE;
30328
+ END IF;
30329
+ FOR k IN 1 .. r.crates LOOP
30330
+ v_bron := r.order_id::text || ':' || r.cart_id::text || ':' || r.ci_id::text || ':' || k::text;
30331
+ v_keep := v_keep || v_bron;
30332
+ INSERT INTO public.logistics_sticker_prints (soort, bron_id, order_id, order_number, cart_id, kopieen, print_plek_id, status, reden)
30333
+ VALUES ('eigen_traysticker', v_bron, r.order_id, r.order_number, r.cart_id, 1,
30334
+ public.logistics_sticker_cart_plek(r.cart_id, v_station), 'wacht_op_moment', 'Wacht op de pakbon van deze kar.')
30335
+ ON CONFLICT DO NOTHING;
30336
+ -- Was hij eerder tegengehouden door een onbekend fustaantal: weer laten wachten op het moment.
30337
+ UPDATE public.logistics_sticker_prints
30338
+ SET status = 'wacht_op_moment', reden = 'Wacht op de pakbon van deze kar.', updated_at = now()
30339
+ WHERE soort = 'eigen_traysticker' AND bron_id = v_bron AND status = 'geblokkeerd'
30340
+ AND reden LIKE 'Fustaantal van regel %';
30341
+ n := n + 1;
30342
+ END LOOP;
30343
+ END LOOP;
30344
+
30345
+ -- Order met onbekend fustaantal: alle nog niet geprinte stickers wachten (niet vervallen).
30346
+ UPDATE public.logistics_sticker_prints p
30347
+ SET status = 'geblokkeerd',
30348
+ reden = (SELECT b.reden FROM public.logistics_sticker_prints b
30349
+ WHERE b.soort = 'eigen_traysticker' AND b.bron_id = p.order_id::text || ':fustaantal-onbekend' LIMIT 1),
30350
+ updated_at = now()
30351
+ WHERE p.soort = 'eigen_traysticker' AND p.status IN ('wacht_op_moment', 'klaar')
30352
+ AND p.order_id = ANY (v_wacht_orders);
30353
+ v_keep := v_keep || ARRAY(SELECT p.bron_id FROM public.logistics_sticker_prints p
30354
+ WHERE p.soort = 'eigen_traysticker' AND p.order_id = ANY (v_wacht_orders)
30355
+ AND p.reden LIKE 'Fustaantal van regel %');
30356
+
30357
+ UPDATE public.logistics_sticker_prints p
30358
+ SET status = 'niet_printen', reden = 'Fust hoort niet meer bij deze kar of order vervallen.', updated_at = now()
30359
+ WHERE p.soort = 'eigen_traysticker' AND p.status IN ('wacht_op_moment', 'geblokkeerd', 'klaar')
30360
+ AND NOT (p.bron_id = ANY (v_keep))
30361
+ AND p.order_id IN (SELECT o.id FROM public.orders o WHERE o.delivery_date >= current_date - 1);
30362
+ GET DIAGNOSTICS n_uit = ROW_COUNT;
30363
+
30364
+ UPDATE public.logistics_sticker_prints p
30365
+ SET status = 'klaar', reden = NULL,
30366
+ print_plek_id = coalesce(p.print_plek_id, public.logistics_sticker_cart_plek(p.cart_id, v_station)),
30367
+ updated_at = now()
30368
+ WHERE p.soort = 'eigen_traysticker' AND p.status = 'wacht_op_moment'
30369
+ AND p.cart_id IS NOT NULL AND public.logistics_cart_slip_queued(p.cart_id);
30370
+ GET DIAGNOSTICS n_vrij = ROW_COUNT;
30371
+
30372
+ RETURN jsonb_build_object('trayRelease', v_tray, 'fusten', n, 'geblokkeerd', n_geblokkeerd, 'vrijgegeven', n_vrij, 'niet_printen', n_uit);
30373
+ END; $function$;
30374
+ REVOKE ALL ON FUNCTION public.logistics_own_traysticker_sync(integer) FROM PUBLIC, anon, authenticated;
30375
+ GRANT EXECUTE ON FUNCTION public.logistics_own_traysticker_sync(integer) TO service_role;