@flowselections/logistiek-engine 1.0.225 → 1.0.229

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 (38) hide show
  1. package/db/schema/logistiek-engine.sql +757 -0
  2. package/dist-lib/components/BeheerPage.d.ts.map +1 -1
  3. package/dist-lib/components/BeheerPage.js +2 -0
  4. package/dist-lib/components/FlowPage.d.ts.map +1 -1
  5. package/dist-lib/components/FlowPage.js +2 -0
  6. package/dist-lib/components/LadingdragersPage.d.ts.map +1 -1
  7. package/dist-lib/components/LadingdragersPage.js +2 -0
  8. package/dist-lib/components/LogistiekEnginePage.d.ts.map +1 -1
  9. package/dist-lib/components/LogistiekEnginePage.js +2 -1
  10. package/dist-lib/components/LogistiekEngineQueuePage.d.ts.map +1 -1
  11. package/dist-lib/components/LogistiekEngineQueuePage.js +2 -0
  12. package/dist-lib/components/PlanWachtrijPage.d.ts.map +1 -1
  13. package/dist-lib/components/PlanWachtrijPage.js +2 -0
  14. package/dist-lib/components/ProefindelingPage.d.ts.map +1 -1
  15. package/dist-lib/components/ProefindelingPage.js +2 -0
  16. package/dist-lib/components/RegiePage.d.ts.map +1 -1
  17. package/dist-lib/components/RegiePage.js +2 -0
  18. package/dist-lib/components/RuleEnginePage.d.ts.map +1 -1
  19. package/dist-lib/components/RuleEnginePage.js +2 -0
  20. package/dist-lib/components/StandaardgegevensCard.d.ts +2 -0
  21. package/dist-lib/components/StandaardgegevensCard.d.ts.map +1 -0
  22. package/dist-lib/components/StandaardgegevensCard.js +42 -0
  23. package/dist-lib/hooks/useEngineZelfherstel.d.ts +22 -0
  24. package/dist-lib/hooks/useEngineZelfherstel.d.ts.map +1 -0
  25. package/dist-lib/hooks/useEngineZelfherstel.js +61 -0
  26. package/dist-lib/index.d.ts +2 -0
  27. package/dist-lib/index.d.ts.map +1 -1
  28. package/dist-lib/index.js +2 -0
  29. package/dist-lib/integrations/supabase/types.d.ts +701 -0
  30. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  31. package/dist-lib/lib/handlers/logistics-tick.server.d.ts.map +1 -1
  32. package/dist-lib/lib/handlers/logistics-tick.server.js +11 -1
  33. package/dist-lib/lib/moduleVersion.d.ts +1 -1
  34. package/dist-lib/lib/moduleVersion.js +1 -1
  35. package/dist-lib/lib/planQueue.server.d.ts +4 -1
  36. package/dist-lib/lib/planQueue.server.d.ts.map +1 -1
  37. package/dist-lib/lib/planQueue.server.js +5 -1
  38. package/package.json +2 -2
@@ -38972,3 +38972,760 @@ RETURNS TABLE(punt text, ok boolean, uitleg text) LANGUAGE sql STABLE SET search
38972
38972
  $$;
38973
38973
  REVOKE ALL ON FUNCTION public.logistics_install_check_223() FROM PUBLIC, anon, authenticated;
38974
38974
  GRANT EXECUTE ON FUNCTION public.logistics_install_check_223() TO service_role;
38975
+
38976
+ -- ===== 1.0.227 = Standaardgegevens aanvullen (zelfherstel) =====
38977
+ -- bron: migratie 1.0.227
38978
+ -- 1.0.227: standaardgegevens Engine aanvullen (zelfherstel in nieuwe shells)
38979
+ CREATE OR REPLACE FUNCTION public.logistics_ensure_defaults()
38980
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$
38981
+ DECLARE
38982
+ v_ladingdragers text[] := '{}';
38983
+ v_ladingdrager_velden text[] := '{}';
38984
+ v_vertalingen text[] := '{}';
38985
+ v_instellingen text[] := '{}';
38986
+ v_tijd text := NULL;
38987
+ v_strategie boolean := false;
38988
+ v_meldingen text[] := '{}';
38989
+ v_gewijzigd boolean;
38990
+ v_res jsonb;
38991
+ v_heeft_orders boolean;
38992
+ BEGIN
38993
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_ensure_defaults');
38994
+ PERFORM pg_advisory_xact_lock(hashtext('logistics_ensure_defaults'));
38995
+
38996
+ -- 1. Ladingdragers (systeemgegevens, vaste codes; Voorraad koppelt via engine_code).
38997
+ WITH std(code, name, capacity_model, layers, len, wid, max_crates, max_boxes, max_kg, max_cm, mix, sort_order) AS (VALUES
38998
+ ('trolley','Stapelwagen','layered',5,135,56,NULL::int,NULL::int,500::numeric,190,true,10),
38999
+ ('danish_container','Deense container','layered',5,135,56,NULL,NULL,500,190,true,20),
39000
+ ('euro_rolcontainer','Euro-rolcontainer','layered',6,120,80,NULL,NULL,600,200,true,30),
39001
+ ('pallet','Pallet','stacked',4,120,80,NULL,40,800,180,true,40),
39002
+ ('euro_pallet','Euro-pallet','single',1,60,40,1,1,20,40,false,50),
39003
+ ('loose','Geen ladingdrager','none',1,0,0,NULL,NULL,NULL,NULL,false,60)
39004
+ ), ins AS (
39005
+ INSERT INTO public.load_carrier_types (code, name, capacity_model, layers_per_carrier, layer_length_cm, layer_width_cm,
39006
+ max_crates_per_layer, max_boxes, max_weight_kg, max_height_cm, allow_mixing, is_active, is_default, sort_order)
39007
+ SELECT s.code, s.name, s.capacity_model, s.layers, s.len, s.wid, s.max_crates, s.max_boxes, s.max_kg, s.max_cm, s.mix, true,
39008
+ (s.code = 'trolley' AND NOT EXISTS (SELECT 1 FROM public.load_carrier_types d WHERE d.is_default)), s.sort_order
39009
+ FROM std s
39010
+ WHERE NOT EXISTS (SELECT 1 FROM public.load_carrier_types l WHERE l.code = s.code)
39011
+ ON CONFLICT (code) DO NOTHING
39012
+ RETURNING code
39013
+ )
39014
+ SELECT coalesce(array_agg(code ORDER BY code), '{}') INTO v_ladingdragers FROM ins;
39015
+
39016
+ -- Bestaande codes: alleen lege velden aanvullen (ingevulde waarden zijn klantwaarden).
39017
+ WITH std(code, name, capacity_model, layers, len, wid, max_crates, max_boxes, max_kg, max_cm) AS (VALUES
39018
+ ('trolley','Stapelwagen','layered',5,135,56,NULL::int,NULL::int,500::numeric,190),
39019
+ ('danish_container','Deense container','layered',5,135,56,NULL,NULL,500,190),
39020
+ ('euro_rolcontainer','Euro-rolcontainer','layered',6,120,80,NULL,NULL,600,200),
39021
+ ('pallet','Pallet','stacked',4,120,80,NULL,40,800,180),
39022
+ ('euro_pallet','Euro-pallet','single',1,60,40,1,1,20,40),
39023
+ ('loose','Geen ladingdrager','none',1,0,0,NULL,NULL,NULL,NULL)
39024
+ ), upd AS (
39025
+ UPDATE public.load_carrier_types l SET
39026
+ name = coalesce(nullif(btrim(l.name),''), s.name),
39027
+ capacity_model = coalesce(l.capacity_model, s.capacity_model),
39028
+ layers_per_carrier = coalesce(l.layers_per_carrier, s.layers),
39029
+ layer_length_cm = coalesce(l.layer_length_cm, s.len),
39030
+ layer_width_cm = coalesce(l.layer_width_cm, s.wid),
39031
+ max_crates_per_layer = coalesce(l.max_crates_per_layer, s.max_crates),
39032
+ max_boxes = coalesce(l.max_boxes, s.max_boxes),
39033
+ max_weight_kg = coalesce(l.max_weight_kg, s.max_kg),
39034
+ max_height_cm = coalesce(l.max_height_cm, s.max_cm),
39035
+ updated_at = now()
39036
+ FROM std s
39037
+ WHERE l.code = s.code AND NOT (l.code = ANY(v_ladingdragers))
39038
+ AND ( nullif(btrim(l.name),'') IS NULL
39039
+ OR l.capacity_model IS NULL OR l.layers_per_carrier IS NULL OR l.layer_length_cm IS NULL OR l.layer_width_cm IS NULL
39040
+ OR (l.max_crates_per_layer IS NULL AND s.max_crates IS NOT NULL)
39041
+ OR (l.max_boxes IS NULL AND s.max_boxes IS NOT NULL)
39042
+ OR (l.max_weight_kg IS NULL AND s.max_kg IS NOT NULL)
39043
+ OR (l.max_height_cm IS NULL AND s.max_cm IS NOT NULL))
39044
+ RETURNING l.code
39045
+ )
39046
+ SELECT coalesce(array_agg(code ORDER BY code), '{}') INTO v_ladingdrager_velden FROM upd;
39047
+ -- Vervallen systeemcodes: lijst is leeg. Komt er ooit een code bij die vervalt:
39048
+ -- UPDATE ... SET is_active = false WHERE code = ANY('{...}'); nooit verwijderen.
39049
+
39050
+ -- 2. Vertaling Floriday -> Engine (systeemgegevens): alleen ontbrekende floriday_code toevoegen.
39051
+ IF to_regclass('public.logistics_floriday_carrier_map') IS NOT NULL THEN
39052
+ WITH v(f, c) AS (VALUES
39053
+ ('DANISH_TROLLEY','danish_container'), ('DANISH_TROLLEY_NON_CC_RFID','danish_container'),
39054
+ ('DANISH_TROLLEY_WATER_TRAY','danish_container'), ('ONE_THIRD_DANISH_TROLLEY','danish_container'),
39055
+ ('AUCTION_TROLLEY','trolley'),
39056
+ ('EURO_TROLLEY','euro_rolcontainer'), ('WATERING_CONTAINER','euro_rolcontainer'),
39057
+ ('EURO_PALLET','euro_pallet'), ('HT_EURO_PALLET','euro_pallet'),
39058
+ ('PALLET','pallet'), ('BLOCK_PALLET','pallet'), ('IFCO_PALLET','pallet'),
39059
+ ('CHEP_PALLET_BLUE','pallet'), ('CHEP_PALLET_RED','pallet'),
39060
+ ('NONE','loose')
39061
+ ), ins AS (
39062
+ INSERT INTO public.logistics_floriday_carrier_map (floriday_code, carrier_code)
39063
+ SELECT v.f, v.c FROM v
39064
+ WHERE NOT EXISTS (SELECT 1 FROM public.logistics_floriday_carrier_map m WHERE upper(m.floriday_code) = v.f)
39065
+ ON CONFLICT DO NOTHING
39066
+ RETURNING floriday_code
39067
+ )
39068
+ SELECT coalesce(array_agg(floriday_code ORDER BY floriday_code), '{}') INTO v_vertalingen FROM ins;
39069
+ END IF;
39070
+
39071
+ -- 3. Plan-instellingen: alleen sleutels met één eenduidige terugval in de code, met precies die waarde.
39072
+ WITH v(k, w) AS (VALUES ('batch_size','10'), ('retention_days','7'), ('kick_debounce_seconds','5')),
39073
+ ins AS (
39074
+ INSERT INTO public.logistics_plan_settings (key, value)
39075
+ SELECT v.k, v.w FROM v WHERE NOT EXISTS (SELECT 1 FROM public.logistics_plan_settings p WHERE p.key = v.k)
39076
+ ON CONFLICT (key) DO NOTHING
39077
+ RETURNING key
39078
+ )
39079
+ SELECT coalesce(array_agg(key ORDER BY key), '{}') INTO v_instellingen FROM ins;
39080
+
39081
+ -- 4. Tijdinstellingen (klant): alleen als de rij ontbreekt.
39082
+ IF NOT EXISTS (SELECT 1 FROM public.logistics_time_settings) THEN
39083
+ v_heeft_orders := EXISTS (SELECT 1 FROM public.orders);
39084
+ IF v_heeft_orders THEN
39085
+ -- Zelfde gedrag als zonder rij: vertrekregel uit, regel 160 valt terug op "nu".
39086
+ INSERT INTO public.logistics_time_settings (id, timezone, departure_rule_since, rule_160_since)
39087
+ VALUES (true, 'Europe/Amsterdam', NULL, NULL) ON CONFLICT DO NOTHING;
39088
+ v_tijd := 'aangemaakt zonder ingangsdata (er zijn al orders)';
39089
+ ELSE
39090
+ INSERT INTO public.logistics_time_settings (id, timezone, departure_rule_since, rule_160_since)
39091
+ VALUES (true, 'Europe/Amsterdam', now(), now()) ON CONFLICT DO NOTHING;
39092
+ v_tijd := 'aangemaakt met ingangsdata nu (lege shell)';
39093
+ END IF;
39094
+ END IF;
39095
+
39096
+ -- 5. Strategie (klant): alleen als er nog geen enkele bestaat.
39097
+ -- 5. Strategie (klant): alleen als er nog geen enkele bestaat. Een strategie hoort bij
39098
+ -- een gebruiker (user_id verplicht); zonder ingelogde gebruiker (serverronde) wachten.
39099
+ IF NOT EXISTS (SELECT 1 FROM public.logistics_strategies) THEN
39100
+ IF auth.uid() IS NULL THEN
39101
+ v_meldingen := array_append(v_meldingen, 'Basisstrategie wordt aangemaakt bij het openen van de Engine door een gebruiker'::text);
39102
+ ELSE
39103
+ WITH s AS (
39104
+ INSERT INTO public.logistics_strategies (name, is_active, user_id)
39105
+ VALUES ('Basisstrategie', true, auth.uid()) RETURNING id
39106
+ )
39107
+ INSERT INTO public.logistics_rule_configs (name, status, is_active, settings, strategy_id, user_id)
39108
+ SELECT 'Werkwijze Basisstrategie', 'actief',
39109
+ NOT EXISTS (SELECT 1 FROM public.logistics_rule_configs r WHERE r.is_active),
39110
+ '{}'::jsonb, s.id, auth.uid() FROM s;
39111
+ v_strategie := true;
39112
+ END IF;
39113
+ END IF;
39114
+ IF NOT EXISTS (SELECT 1 FROM public.logistics_strategies WHERE is_active) THEN
39115
+ v_meldingen := array_append(v_meldingen, 'Geen actieve strategie'::text);
39116
+ END IF;
39117
+
39118
+ -- 6. Tabellen van andere modules: alleen melden.
39119
+ IF to_regclass('public.pick_station_types') IS NOT NULL AND NOT EXISTS (SELECT 1 FROM public.pick_station_types) THEN
39120
+ v_meldingen := array_append(v_meldingen, 'Pickstation-soorten leeg (Pickmodule)'::text);
39121
+ END IF;
39122
+ IF to_regclass('public.pick_screen_document_types') IS NOT NULL AND NOT EXISTS (SELECT 1 FROM public.pick_screen_document_types) THEN
39123
+ v_meldingen := array_append(v_meldingen, 'Pickscherm-documentsoorten leeg (Pickmodule)'::text);
39124
+ END IF;
39125
+ IF to_regclass('public.cart_planning_settings') IS NOT NULL AND NOT EXISTS (SELECT 1 FROM public.cart_planning_settings) THEN
39126
+ v_meldingen := array_append(v_meldingen, 'Karplanning-instellingen leeg (Verkoop)'::text);
39127
+ END IF;
39128
+
39129
+ v_gewijzigd := cardinality(v_ladingdragers) + cardinality(v_ladingdrager_velden) + cardinality(v_vertalingen)
39130
+ + cardinality(v_instellingen) > 0 OR v_tijd IS NOT NULL OR v_strategie;
39131
+ v_res := jsonb_build_object(
39132
+ 'gewijzigd', v_gewijzigd, 'op', now(),
39133
+ 'ladingdragers', to_jsonb(v_ladingdragers),
39134
+ 'ladingdrager_velden', to_jsonb(v_ladingdrager_velden),
39135
+ 'floriday_vertalingen', to_jsonb(v_vertalingen),
39136
+ 'plan_instellingen', to_jsonb(v_instellingen),
39137
+ 'tijdinstellingen', v_tijd,
39138
+ 'basisstrategie', v_strategie,
39139
+ 'meldingen', to_jsonb(v_meldingen));
39140
+
39141
+ -- Alleen wegschrijven als er iets is aangevuld (geen schrijfactie per ronde).
39142
+ IF v_gewijzigd THEN
39143
+ INSERT INTO public.logistics_plan_settings (key, value) VALUES ('defaults_last_result', v_res::text)
39144
+ ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = now();
39145
+ END IF;
39146
+ RETURN v_res;
39147
+ END $$;
39148
+ REVOKE ALL ON FUNCTION public.logistics_ensure_defaults() FROM PUBLIC, anon;
39149
+ GRANT EXECUTE ON FUNCTION public.logistics_ensure_defaults() TO authenticated, service_role;
39150
+
39151
+ CREATE OR REPLACE FUNCTION public.logistics_defaults_status()
39152
+ RETURNS jsonb LANGUAGE sql STABLE SECURITY DEFINER SET search_path = public AS $$
39153
+ SELECT public.logistics_acc_guard('ingelogd', 'logistics_defaults_status');
39154
+ SELECT coalesce((SELECT value::jsonb FROM public.logistics_plan_settings WHERE key = 'defaults_last_result'), 'null'::jsonb);
39155
+ $$;
39156
+ REVOKE ALL ON FUNCTION public.logistics_defaults_status() FROM PUBLIC, anon;
39157
+ GRANT EXECUTE ON FUNCTION public.logistics_defaults_status() TO authenticated, service_role;
39158
+
39159
+ CREATE OR REPLACE FUNCTION public.logistics_install_check_227()
39160
+ RETURNS TABLE(punt text, ok boolean, uitleg text) LANGUAGE sql STABLE SET search_path TO 'public' AS $$
39161
+ SELECT 'standaardgegevens-functie', to_regprocedure('public.logistics_ensure_defaults()') IS NOT NULL,
39162
+ 'logistics_ensure_defaults is aanwezig'
39163
+ UNION ALL
39164
+ SELECT 'rechten standaardgegevens',
39165
+ NOT has_function_privilege('anon', 'public.logistics_ensure_defaults()', 'EXECUTE')
39166
+ AND has_function_privilege('authenticated', 'public.logistics_ensure_defaults()', 'EXECUTE'),
39167
+ 'Alleen ingelogde gebruikers en server mogen aanvullen'
39168
+ UNION ALL
39169
+ SELECT 'ladingdragers', (SELECT count(*) FROM public.load_carrier_types WHERE code IN
39170
+ ('trolley','danish_container','euro_rolcontainer','pallet','euro_pallet','loose')) = 6,
39171
+ 'De zes vaste ladingdragercodes zijn aanwezig'
39172
+ UNION ALL
39173
+ SELECT 'Floriday-vertaling', (SELECT count(*) FROM public.logistics_floriday_carrier_map) >= 15,
39174
+ 'Vertaling Floriday naar Engine-ladingdrager is gevuld'
39175
+ UNION ALL
39176
+ SELECT 'tijdinstelling', EXISTS (SELECT 1 FROM public.logistics_time_settings), 'Tijdinstelling aanwezig'
39177
+ $$;
39178
+ REVOKE ALL ON FUNCTION public.logistics_install_check_227() FROM PUBLIC, anon, authenticated;
39179
+ GRANT EXECUTE ON FUNCTION public.logistics_install_check_227() TO service_role;
39180
+
39181
+
39182
+ -- ===== 1.0.228: NONE → loose, lus planwachtrij, opruimen, bewaartermijnen, onderhoudsstatus =====
39183
+ -- 1.0.228: NONE eenduidig naar loose; lus in de planwachtrij stoppen.
39184
+
39185
+ -- A1. Vertaling: alleen leeg = ontbreekt; NONE gaat via de vertaaltabel (bron floriday).
39186
+ CREATE OR REPLACE FUNCTION public.logistics_ladingdrager_resolve(p_code text)
39187
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
39188
+ DECLARE v text := nullif(btrim(p_code), ''); n int; m text;
39189
+ BEGIN
39190
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_ladingdrager_resolve');
39191
+ IF v IS NULL THEN
39192
+ RETURN jsonb_build_object('code', NULL, 'invoer', v, 'bron', NULL, 'fout', 'ladingdrager ontbreekt');
39193
+ END IF;
39194
+ IF EXISTS (SELECT 1 FROM public.load_carrier_types t WHERE t.is_active AND t.code = v) THEN
39195
+ RETURN jsonb_build_object('code', v, 'invoer', v, 'bron', 'beheer', 'fout', NULL);
39196
+ END IF;
39197
+ IF to_regclass('public.logistics_floriday_carrier_map') IS NULL THEN
39198
+ RETURN jsonb_build_object('code', NULL, 'invoer', v, 'bron', NULL, 'fout', format('ladingdrager %s onbekend in Beheer', v));
39199
+ END IF;
39200
+ SELECT count(DISTINCT c.carrier_code), min(c.carrier_code) INTO n, m
39201
+ FROM public.logistics_floriday_carrier_map c WHERE upper(c.floriday_code) = upper(v);
39202
+ IF coalesce(n, 0) = 0 THEN
39203
+ RETURN jsonb_build_object('code', NULL, 'invoer', v, 'bron', NULL, 'fout', format('ladingdrager %s onbekend in Beheer', v));
39204
+ ELSIF n > 1 THEN
39205
+ RETURN jsonb_build_object('code', NULL, 'invoer', v, 'bron', 'floriday', 'fout', format('ladingdrager niet eenduidig (%s)', v));
39206
+ ELSIF NOT EXISTS (SELECT 1 FROM public.load_carrier_types t WHERE t.is_active AND t.code = m) THEN
39207
+ RETURN jsonb_build_object('code', NULL, 'invoer', v, 'bron', 'floriday', 'fout', format('ladingdrager %s onbekend in Beheer', v));
39208
+ END IF;
39209
+ RETURN jsonb_build_object('code', m, 'invoer', v, 'bron', 'floriday', 'fout', NULL);
39210
+ END $$;
39211
+ REVOKE ALL ON FUNCTION public.logistics_ladingdrager_resolve(text) FROM PUBLIC, anon;
39212
+ GRANT EXECUTE ON FUNCTION public.logistics_ladingdrager_resolve(text) TO authenticated, service_role;
39213
+
39214
+ -- A2. Planner-variant met het oude gedrag (regel 1.0.213: geen ladingdrager = niet op een kar).
39215
+ CREATE OR REPLACE FUNCTION public.logistics_ladingdrager_resolve_plan(p_code text)
39216
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
39217
+ DECLARE rc jsonb;
39218
+ BEGIN
39219
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_ladingdrager_resolve_plan');
39220
+ IF upper(nullif(btrim(p_code), '')) = 'NONE' THEN
39221
+ RETURN jsonb_build_object('code', NULL, 'invoer', nullif(btrim(p_code), ''), 'bron', NULL, 'fout', 'ladingdrager ontbreekt');
39222
+ END IF;
39223
+ rc := public.logistics_ladingdrager_resolve(p_code);
39224
+ IF rc->>'code' = 'loose' AND rc->>'bron' = 'floriday' THEN
39225
+ RETURN jsonb_build_object('code', NULL, 'invoer', rc->>'invoer', 'bron', 'floriday', 'fout', 'ladingdrager ontbreekt');
39226
+ END IF;
39227
+ RETURN rc;
39228
+ END $$;
39229
+ REVOKE ALL ON FUNCTION public.logistics_ladingdrager_resolve_plan(text) FROM PUBLIC, anon;
39230
+ GRANT EXECUTE ON FUNCTION public.logistics_ladingdrager_resolve_plan(text) TO authenticated, service_role;
39231
+
39232
+ -- B1. Vingerafdruk van wat de planning van een order gebruikt (zonder eigen Engine-velden).
39233
+ CREATE OR REPLACE FUNCTION public.logistics_plan_inhoud_fp(p_order_id uuid)
39234
+ RETURNS text LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
39235
+ SELECT md5(concat_ws('|',
39236
+ o.status, o.delivery_date, o.customer_id, o.deleted_at, o.is_reservation, to_jsonb(o)->>'carrier_id',
39237
+ o.custom_fields->>'delivery_location', o.custom_fields->>'delivery_location_id', o.custom_fields->>'delivery_location_gln',
39238
+ o.custom_fields->>'pickup_moment', o.custom_fields->>'pickup_datetime', o.custom_fields->>'manual_pick_day',
39239
+ o.custom_fields->>'own_transport', o.custom_fields->>'carrier',
39240
+ (SELECT string_agg(concat_ws(':', oi.id, oi.product_id, oi.quantity, oi.crates_per_layer, oi.layers_per_cart,
39241
+ oi.plants_per_crate, oi.quarantined_at), ',' ORDER BY oi.id)
39242
+ FROM logistics_active.order_items oi WHERE oi.order_id = o.id),
39243
+ (SELECT max(p.updated_at) FROM logistics_active.order_items oi JOIN public.products p ON p.id = oi.product_id
39244
+ WHERE oi.order_id = o.id)))
39245
+ FROM public.orders o WHERE o.id = p_order_id
39246
+ $$;
39247
+ REVOKE ALL ON FUNCTION public.logistics_plan_inhoud_fp(uuid) FROM PUBLIC, anon, authenticated;
39248
+ GRANT EXECUTE ON FUNCTION public.logistics_plan_inhoud_fp(uuid) TO service_role;
39249
+
39250
+ ALTER TABLE public.logistics_plan_queue ADD COLUMN IF NOT EXISTS order_fp text;
39251
+ CREATE INDEX IF NOT EXISTS logistics_plan_queue_order_upd_idx ON public.logistics_plan_queue (order_id, updated_at DESC);
39252
+
39253
+ -- B2. Reconcile: gearchiveerd = afgehandeld zolang de inhoud van de order niet is gewijzigd.
39254
+ CREATE OR REPLACE FUNCTION public.logistics_reconcile_missing_plan_queue()
39255
+ RETURNS integer LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $function$
39256
+ DECLARE r record; n integer := 0;
39257
+ BEGIN
39258
+ PERFORM public.logistics_acc_guard('server', 'logistics_reconcile_missing_plan_queue');
39259
+ FOR r IN
39260
+ SELECT o.id FROM public.orders o
39261
+ WHERE o.deleted_at IS NULL
39262
+ AND COALESCE(o.is_reservation,false) = false
39263
+ AND o.plan_locked_at IS NULL
39264
+ AND o.order_number IS NOT NULL
39265
+ AND (o.created_at >= now() - interval '14 days' OR o.updated_at >= now() - interval '14 days')
39266
+ AND NOT EXISTS (SELECT 1 FROM public.logistics_tasks t WHERE t.order_ref = o.order_number)
39267
+ AND NOT EXISTS (SELECT 1 FROM public.logistics_plan_queue q
39268
+ WHERE q.order_id = o.id AND q.status IN ('pending','running'))
39269
+ AND NOT EXISTS (
39270
+ SELECT 1 FROM public.logistics_plan_queue q
39271
+ WHERE q.order_id = o.id AND q.status IN ('geblokkeerd', 'done', 'opgelost')
39272
+ AND q.updated_at >= COALESCE(o.updated_at, o.created_at)
39273
+ AND NOT EXISTS (SELECT 1 FROM logistics_active.order_items oi WHERE oi.order_id = o.id AND oi.created_at > q.updated_at)
39274
+ AND NOT EXISTS (SELECT 1 FROM logistics_active.order_items oi JOIN public.products p ON p.id = oi.product_id
39275
+ WHERE oi.order_id = o.id AND p.updated_at > q.updated_at))
39276
+ -- 1.0.228: gearchiveerd telt als afgehandeld; alleen een inhoudelijke wijziging biedt opnieuw aan.
39277
+ -- Oude rijen zonder vingerafdruk tellen ook als afgehandeld ("Opnieuw aanbieden" blijft mogelijk).
39278
+ AND NOT EXISTS (
39279
+ SELECT 1 FROM public.logistics_plan_queue q
39280
+ WHERE q.order_id = o.id AND q.status = 'gearchiveerd'
39281
+ AND (q.order_fp IS NULL OR q.order_fp = public.logistics_plan_inhoud_fp(o.id)))
39282
+ ORDER BY o.created_at ASC
39283
+ LIMIT 100
39284
+ LOOP
39285
+ PERFORM public.logistics_plan_enqueue(r.id, 'reconcile');
39286
+ n := n + 1;
39287
+ END LOOP;
39288
+ RETURN n;
39289
+ END;
39290
+ $function$;
39291
+
39292
+ -- B3. Housekeeping: vingerafdruk bewaren; per order hooguit één archiveringsmelding.
39293
+ CREATE OR REPLACE FUNCTION public.logistics_plan_housekeeping()
39294
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $function$
39295
+ DECLARE v_resolved int := 0; v_archived int := 0; v_nieuw int := 0; v_alerts int := 0; v_gone int := 0; v_refs text;
39296
+ BEGIN
39297
+ PERFORM public.logistics_acc_guard('server', 'logistics_plan_housekeeping');
39298
+ UPDATE public.logistics_plan_queue q SET status='opgelost', error='opgelost: order is al verzonden',
39299
+ claimed_at=NULL, updated_at=now()
39300
+ FROM public.orders o
39301
+ WHERE o.id = q.order_id AND q.status IN ('geblokkeerd','failed')
39302
+ AND o.status IN ('Verzonden','Gereed voor facturatie');
39303
+ GET DIAGNOSTICS v_resolved = ROW_COUNT;
39304
+ UPDATE public.logistics_plan_queue q SET status='opgelost', error='order bestaat niet meer', claimed_at=NULL, updated_at=now()
39305
+ WHERE q.status IN ('geblokkeerd','failed')
39306
+ AND NOT EXISTS (SELECT 1 FROM public.orders o WHERE o.id = q.order_id AND o.deleted_at IS NULL);
39307
+
39308
+ WITH kand AS (
39309
+ SELECT q.id, q.order_id, o.order_number, public.logistics_plan_inhoud_fp(o.id) AS fp
39310
+ FROM public.logistics_plan_queue q JOIN public.orders o ON o.id = q.order_id
39311
+ WHERE q.status IN ('geblokkeerd','failed')
39312
+ AND o.deleted_at IS NULL
39313
+ AND COALESCE(o.status,'') NOT IN ('Verzonden','Gereed voor facturatie','geannuleerd')
39314
+ AND o.delivery_date IS NOT NULL AND o.delivery_date::date < current_date - 2
39315
+ ), a AS (
39316
+ UPDATE public.logistics_plan_queue q SET status='gearchiveerd', error=COALESCE(q.error, q.reason),
39317
+ order_fp = k.fp, claimed_at=NULL, updated_at=now()
39318
+ FROM kand k WHERE q.id = k.id
39319
+ RETURNING q.id, k.order_id, k.order_number, k.fp
39320
+ ), nieuw AS (
39321
+ SELECT DISTINCT a.order_number FROM a
39322
+ WHERE NOT EXISTS (SELECT 1 FROM public.logistics_plan_queue e
39323
+ WHERE e.order_id = a.order_id AND e.status = 'gearchiveerd' AND e.id <> a.id
39324
+ AND (e.order_fp IS NULL OR e.order_fp = a.fp))
39325
+ )
39326
+ SELECT (SELECT count(*) FROM a), (SELECT count(*) FROM nieuw), (SELECT string_agg(order_number, ', ' ORDER BY order_number) FROM nieuw)
39327
+ INTO v_archived, v_nieuw, v_refs;
39328
+
39329
+ IF v_nieuw > 0 THEN
39330
+ INSERT INTO public.notifications(user_id, title, message, read)
39331
+ SELECT DISTINCT ur.user_id, 'Niet geleverd en gearchiveerd',
39332
+ v_nieuw || ' geblokkeerde order(s) gearchiveerd (leverdatum voorbij): ' || v_refs ||
39333
+ '. Zie Engine › Wachtrij; "Opnieuw aanbieden" zet ze terug.', false
39334
+ FROM public.user_roles ur WHERE ur.role IN ('planner','admin');
39335
+ END IF;
39336
+
39337
+ UPDATE public.logistics_stock_alerts a SET status='resolved', resolved_at=now(),
39338
+ resolution_reason='automatisch opgelost: order verzonden/afgerond', updated_at=now()
39339
+ FROM public.orders o
39340
+ WHERE o.id = a.order_id AND a.status='open'
39341
+ AND o.status IN ('Verzonden','Gereed voor facturatie','geannuleerd');
39342
+ GET DIAGNOSTICS v_alerts = ROW_COUNT;
39343
+
39344
+ UPDATE public.logistics_stock_alerts a SET status='resolved', resolved_at=now(),
39345
+ resolution_reason='order bestaat niet meer', updated_at=now()
39346
+ WHERE a.status='open' AND a.order_id IS NOT NULL
39347
+ AND NOT EXISTS (SELECT 1 FROM public.orders o WHERE o.id = a.order_id AND o.deleted_at IS NULL);
39348
+ GET DIAGNOSTICS v_gone = ROW_COUNT;
39349
+
39350
+ RETURN jsonb_build_object('blokkadesOpgelost', v_resolved, 'gearchiveerd', v_archived, 'gemeld', v_nieuw,
39351
+ 'meldingenOpgelost', v_alerts, 'meldingenZonderOrder', v_gone);
39352
+ END $function$;
39353
+
39354
+ -- B4. Prune: ook gearchiveerd/opgelost; nieuwste rij per order blijft; porties met tijdslimiet.
39355
+ CREATE OR REPLACE FUNCTION public.logistics_plan_prune()
39356
+ RETURNS integer LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $function$
39357
+ DECLARE v_n int := 0; r int; v_days int := LEAST(GREATEST(public.logistics_plan_setting('retention_days', 7)::int, 1), 365);
39358
+ v_eind timestamptz := clock_timestamp() + interval '5 seconds'; v_rondes int := 0;
39359
+ BEGIN
39360
+ PERFORM public.logistics_acc_guard('server', 'logistics_plan_prune');
39361
+ LOOP
39362
+ WITH rang AS (
39363
+ SELECT q.id, q.status, q.updated_at,
39364
+ row_number() OVER (PARTITION BY q.order_id ORDER BY q.updated_at DESC, q.id DESC) AS rn
39365
+ FROM public.logistics_plan_queue q
39366
+ ), weg AS (
39367
+ SELECT id FROM rang
39368
+ WHERE rn > 1 AND status IN ('done','failed','gearchiveerd','opgelost')
39369
+ AND updated_at < now() - make_interval(days => v_days)
39370
+ LIMIT 5000)
39371
+ DELETE FROM public.logistics_plan_queue d USING weg WHERE d.id = weg.id;
39372
+ GET DIAGNOSTICS r = ROW_COUNT;
39373
+ v_n := v_n + r; v_rondes := v_rondes + 1;
39374
+ EXIT WHEN r < 5000 OR clock_timestamp() >= v_eind;
39375
+ END LOOP;
39376
+ PERFORM public.logistics_planner_diff_prune();
39377
+ IF v_n > 0 THEN
39378
+ INSERT INTO public.logistics_plan_settings (key, value)
39379
+ VALUES ('prune_last_result', jsonb_build_object('op', now(), 'verwijderd', v_n, 'porties', v_rondes)::text)
39380
+ ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = now();
39381
+ END IF;
39382
+ RETURN v_n;
39383
+ END;
39384
+ $function$;
39385
+
39386
+ -- B5. Bewaartermijnen: werkwijze die de planner gebruikt, anders veilige standaard; maximaal 365 dagen.
39387
+ CREATE OR REPLACE FUNCTION public.logistics_retention_days(p_key text)
39388
+ RETURNS integer LANGUAGE sql STABLE SET search_path TO 'public' AS $function$
39389
+ SELECT LEAST(365, GREATEST(1, COALESCE(
39390
+ (SELECT NULLIF(COALESCE(rc.settings->'opruimen'->'fields'->>p_key, rc.settings->'opruimen'->>p_key), '')::integer
39391
+ FROM public.logistics_rule_configs rc
39392
+ LEFT JOIN public.logistics_strategies s ON s.id = rc.strategy_id
39393
+ WHERE NULLIF(COALESCE(rc.settings->'opruimen'->'fields'->>p_key, rc.settings->'opruimen'->>p_key), '') IS NOT NULL
39394
+ ORDER BY (rc.is_active AND coalesce(s.is_active, false)) DESC, rc.is_active DESC, rc.updated_at DESC NULLS LAST
39395
+ LIMIT 1),
39396
+ CASE p_key WHEN 'task_retention_days' THEN 90 ELSE 30 END)))
39397
+ $function$;
39398
+
39399
+ -- B6. Status onderhoud voor de Engine-pagina.
39400
+ CREATE OR REPLACE FUNCTION public.logistics_onderhoud_status()
39401
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = public AS $$
39402
+ DECLARE v_status jsonb; v_trace timestamptz; v_prune jsonb;
39403
+ BEGIN
39404
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_onderhoud_status');
39405
+ SELECT coalesce(jsonb_object_agg(status, n), '{}'::jsonb) INTO v_status
39406
+ FROM (SELECT status, count(*) n FROM public.logistics_plan_queue GROUP BY status) x;
39407
+ IF to_regclass('public.logistics_trace_events') IS NOT NULL THEN
39408
+ EXECUTE 'SELECT min(created_at) FROM public.logistics_trace_events' INTO v_trace;
39409
+ END IF;
39410
+ SELECT value::jsonb INTO v_prune FROM public.logistics_plan_settings WHERE key = 'prune_last_result';
39411
+ RETURN jsonb_build_object('wachtrij', v_status, 'oudsteTrace', v_trace, 'laatsteOpruimronde', v_prune,
39412
+ 'bewaartermijnen', jsonb_build_object('trace', public.logistics_retention_days('trace_retention_days'),
39413
+ 'pickhistorie', public.logistics_retention_days('pick_history_retention_days'),
39414
+ 'barcodes', public.logistics_retention_days('barcode_retention_days'),
39415
+ 'taken', public.logistics_retention_days('task_retention_days')));
39416
+ END $$;
39417
+ REVOKE ALL ON FUNCTION public.logistics_onderhoud_status() FROM PUBLIC, anon;
39418
+ GRANT EXECUTE ON FUNCTION public.logistics_onderhoud_status() TO authenticated, service_role;
39419
+
39420
+ CREATE OR REPLACE FUNCTION public.logistics_install_check_228()
39421
+ RETURNS TABLE(punt text, ok boolean, uitleg text) LANGUAGE sql STABLE SET search_path TO 'public' AS $$
39422
+ SELECT 'NONE wordt loose', position('''NONE''' IN pg_get_functiondef('public.logistics_ladingdrager_resolve(text)'::regprocedure)) = 0,
39423
+ 'resolve vertaalt NONE via de vertaaltabel'
39424
+ UNION ALL SELECT 'planner-variant', to_regprocedure('public.logistics_ladingdrager_resolve_plan(text)') IS NOT NULL, 'Planner houdt regel 1.0.213'
39425
+ UNION ALL SELECT 'reconcile gearchiveerd', position('gearchiveerd' IN pg_get_functiondef('public.logistics_reconcile_missing_plan_queue()'::regprocedure)) > 0,
39426
+ 'Gearchiveerde orders worden niet steeds opnieuw aangeboden'
39427
+ UNION ALL SELECT 'index wachtrij', EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'logistics_plan_queue_order_upd_idx'), 'Opruimen per order via index'
39428
+ $$;
39429
+ REVOKE ALL ON FUNCTION public.logistics_install_check_228() FROM PUBLIC, anon, authenticated;
39430
+ GRANT EXECUTE ON FUNCTION public.logistics_install_check_228() TO service_role;
39431
+
39432
+ -- 1.0.228: aanroepers van de ladingdragervertaling
39433
+ CREATE OR REPLACE FUNCTION public.logistics_ladingdrager_kies(p_regels jsonb)
39434
+ RETURNS jsonb
39435
+ LANGUAGE plpgsql
39436
+ STABLE SECURITY DEFINER
39437
+ SET search_path TO 'public'
39438
+ AS $function$
39439
+ DECLARE e jsonb; uit jsonb := '{}'; rc jsonb; hv jsonb; v_pid uuid; v_key text; v_def text; v_ti text; v_lc text;
39440
+ v_cache boolean := to_regclass('public.floriday_trade_items_cache') IS NOT NULL;
39441
+ BEGIN
39442
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_ladingdrager_kies');
39443
+ IF p_regels IS NULL OR jsonb_typeof(p_regels) <> 'array' THEN RETURN '{}'; END IF;
39444
+ IF jsonb_array_length(p_regels) > 2000 THEN
39445
+ RAISE EXCEPTION 'Maximaal 2000 regels per aanroep.' USING ERRCODE = '22023';
39446
+ END IF;
39447
+ SELECT nullif(btrim(coalesce(s.settings->'karrenindeling'->'fields'->>'default_carrier_type', s.settings->'karrenindeling'->>'default_carrier_type')), '')
39448
+ INTO v_def FROM public.logistics_rule_configs s WHERE s.is_active ORDER BY s.updated_at DESC LIMIT 1;
39449
+ IF v_def IS NULL OR NOT EXISTS (SELECT 1 FROM public.load_carrier_types t WHERE t.is_active AND t.code = v_def) THEN
39450
+ SELECT t.code INTO v_def FROM public.load_carrier_types t WHERE t.is_active AND t.is_default ORDER BY t.sort_order LIMIT 1;
39451
+ END IF;
39452
+ FOR e IN SELECT x FROM jsonb_array_elements(p_regels) x LOOP
39453
+ v_key := e->>'key';
39454
+ CONTINUE WHEN v_key IS NULL;
39455
+ v_pid := CASE WHEN nullif(btrim(e->>'product_id'), '') ~* '^[0-9a-f-]{36}$' THEN (e->>'product_id')::uuid END;
39456
+ -- 1. orderregel
39457
+ rc := public.logistics_ladingdrager_resolve_plan(e->>'regel_code');
39458
+ IF rc->>'code' IS NOT NULL THEN
39459
+ uit := uit || jsonb_build_object(v_key, jsonb_build_object('code', rc->>'code', 'bron', 'orderregel', 'invoer', rc->>'invoer'));
39460
+ CONTINUE;
39461
+ END IF;
39462
+ -- 2/3. product, dan voorraadartikel
39463
+ IF v_pid IS NOT NULL THEN
39464
+ hv := public.logistics_product_hoofdverpakking(v_pid);
39465
+ IF nullif(hv->>'ladingdrager', '') IS NOT NULL THEN
39466
+ rc := public.logistics_ladingdrager_resolve_plan(hv->>'ladingdrager');
39467
+ IF rc->>'code' IS NOT NULL THEN
39468
+ uit := uit || jsonb_build_object(v_key, jsonb_build_object('code', rc->>'code', 'bron', hv->>'bron', 'invoer', rc->>'invoer',
39469
+ 'voorraadartikelId', hv->'voorraadartikelId'));
39470
+ CONTINUE;
39471
+ ELSIF upper(hv->>'ladingdrager') = 'NONE' OR rc->>'fout' = 'ladingdrager ontbreekt' THEN
39472
+ uit := uit || jsonb_build_object(v_key, jsonb_build_object('code', NULL, 'bron', hv->>'bron', 'fout', 'ladingdrager ontbreekt (geen ladingdrager)'));
39473
+ CONTINUE;
39474
+ END IF;
39475
+ ELSIF hv->>'fout' IS NOT NULL THEN
39476
+ uit := uit || jsonb_build_object(v_key, jsonb_build_object('code', NULL, 'bron', hv->>'bron', 'fout', hv->>'fout'));
39477
+ CONTINUE;
39478
+ END IF;
39479
+ -- 3b. Floriday-verpakking van het artikel (bestaand gedrag sinds 1.0.190).
39480
+ IF v_cache THEN
39481
+ v_lc := NULL;
39482
+ SELECT p.floriday_trade_item_id INTO v_ti FROM public.products p WHERE p.id = v_pid;
39483
+ IF v_ti IS NOT NULL THEN
39484
+ EXECUTE $q$SELECT c->>'loadCarrier' FROM public.floriday_trade_items_cache t,
39485
+ jsonb_array_elements(coalesce(t.data->'packingConfigurations', '[]')) c
39486
+ WHERE t.floriday_id = $1 AND NOT coalesce(t.is_deleted, false) AND nullif(c->>'loadCarrier', '') IS NOT NULL
39487
+ ORDER BY coalesce((c->>'primary')::boolean, false) DESC LIMIT 1$q$ INTO v_lc USING v_ti;
39488
+ rc := public.logistics_ladingdrager_resolve_plan(v_lc);
39489
+ IF rc->>'code' IS NOT NULL THEN
39490
+ uit := uit || jsonb_build_object(v_key, jsonb_build_object('code', rc->>'code', 'bron', 'floriday_verpakking', 'invoer', rc->>'invoer'));
39491
+ CONTINUE;
39492
+ END IF;
39493
+ END IF;
39494
+ END IF;
39495
+ END IF;
39496
+ -- 4. werkwijze, zichtbaar
39497
+ uit := uit || jsonb_build_object(v_key, jsonb_build_object('code', v_def, 'bron', CASE WHEN v_def IS NOT NULL THEN 'werkwijze' END,
39498
+ 'waarschuwing', CASE WHEN v_def IS NOT NULL THEN 'ladingdrager uit de werkwijze, niet uit het artikel' END,
39499
+ 'fout', CASE WHEN v_def IS NULL THEN 'ladingdrager ontbreekt' END));
39500
+ END LOOP;
39501
+ RETURN uit;
39502
+ END $function$;
39503
+
39504
+ CREATE OR REPLACE FUNCTION public.logistics_product_oordeel_many(p_product_ids uuid[])
39505
+ RETURNS TABLE(product_id uuid, oordeel text, tekst text, "actieUrl" text)
39506
+ LANGUAGE plpgsql
39507
+ STABLE SECURITY DEFINER
39508
+ SET search_path TO 'public'
39509
+ AS $function$
39510
+ #variable_conflict use_column
39511
+ DECLARE r record; c record; cd record; v_carrier text; hv jsonb; rs jsonb; v_code text; v_lfout text; ls int; fh numeric; ph numeric; cpl numeric; lpc numeric; laag numeric;
39512
+ blok text[]; waar text[]; mist text[]; hmist text[]; naam text; bron text;
39513
+ c_url constant text := '/logistiek-engine/beheer#gegevens-voor-karindeling';
39514
+ BEGIN
39515
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_product_oordeel_many');
39516
+ IF coalesce(cardinality(p_product_ids), 0) = 0 THEN RETURN; END IF;
39517
+ IF cardinality(p_product_ids) > 1000 THEN
39518
+ RAISE EXCEPTION 'Maximaal 1000 producten per aanroep (ontvangen: %).', cardinality(p_product_ids) USING ERRCODE = '22023';
39519
+ END IF;
39520
+ SELECT nullif(btrim(coalesce(s.settings->'karrenindeling'->'fields'->>'default_carrier_type', s.settings->'karrenindeling'->>'default_carrier_type')), '')
39521
+ INTO v_carrier FROM public.logistics_rule_configs s WHERE s.is_active ORDER BY s.updated_at DESC LIMIT 1;
39522
+ SELECT t.code, t.name, t.layer_length_cm, t.layer_width_cm, t.max_height_cm, t.max_crates_per_layer, t.layers_per_carrier INTO cd
39523
+ FROM public.load_carrier_types t
39524
+ WHERE t.is_active AND (t.code = v_carrier OR (v_carrier IS NULL AND t.is_default))
39525
+ ORDER BY (t.code = v_carrier) DESC NULLS LAST LIMIT 1;
39526
+
39527
+ FOR r IN SELECT * FROM public.logistics_data_gereedheid(p_product_ids) LOOP
39528
+ blok := '{}'; waar := '{}'; hmist := '{}';
39529
+ mist := array_remove(coalesce(r.ontbrekend, '{}'), 'weight');
39530
+ hv := public.logistics_product_hoofdverpakking(r.product_id);
39531
+ v_code := NULL; v_lfout := NULL;
39532
+ IF hv IS NOT NULL AND NOT coalesce((hv->>'aanwezig')::boolean, false) THEN
39533
+ mist := mist || 'belading niet ingesteld'::text;
39534
+ END IF;
39535
+ IF hv IS NOT NULL AND coalesce((hv->>'aanwezig')::boolean, false) AND nullif(hv->>'ladingdrager', '') IS NOT NULL THEN
39536
+ rs := public.logistics_ladingdrager_resolve(hv->>'ladingdrager');
39537
+ v_code := rs->>'code'; v_lfout := rs->>'fout';
39538
+ -- 1.0.228: geen ladingdrager (loose) = uitleg, geen blokkade; geen capaciteit, geen menging.
39539
+ IF v_code = 'loose' THEN
39540
+ product_id := r.product_id; "actieUrl" := c_url; oordeel := 'waarschuwing';
39541
+ tekst := format('%s: geen ladingdrager, niet op een kar.', r.product);
39542
+ RETURN NEXT;
39543
+ CONTINUE;
39544
+ END IF;
39545
+ END IF;
39546
+ SELECT NULL::text AS code, NULL::text AS name, NULL::int AS layer_length_cm, NULL::int AS layer_width_cm, NULL::int AS max_height_cm, NULL::int AS max_crates_per_layer, NULL::int AS layers_per_carrier INTO c;
39547
+ IF v_code IS NOT NULL THEN
39548
+ SELECT t.code, t.name, t.layer_length_cm, t.layer_width_cm, t.max_height_cm, t.max_crates_per_layer, t.layers_per_carrier INTO c
39549
+ FROM public.load_carrier_types t WHERE t.is_active AND t.code = v_code LIMIT 1;
39550
+ ELSIF v_lfout IS NULL THEN
39551
+ SELECT cd.code, cd.name, cd.layer_length_cm, cd.layer_width_cm, cd.max_height_cm, cd.max_crates_per_layer, cd.layers_per_carrier INTO c;
39552
+ ELSE
39553
+ mist := mist || v_lfout;
39554
+ END IF;
39555
+ naam := coalesce(c.name, c.code, 'de ladingdrager');
39556
+ bron := coalesce(r.velden->'crates_per_layer'->>'bron', 'product');
39557
+ cpl := coalesce(nullif(r.velden->'crates_per_layer'->>'waarde', '')::numeric, 0);
39558
+ lpc := coalesce(nullif(r.velden->'layers_per_cart'->>'waarde', '')::numeric, 0);
39559
+ fh := nullif(r.velden->'fust_height_mm'->>'waarde', '')::numeric / 10.0;
39560
+ ph := nullif(r.velden->'plant_height_cm'->>'waarde', '')::numeric;
39561
+ ls := public.logistics_max_fusten_per_laag(c.layer_length_cm, c.layer_width_cm,
39562
+ nullif(r.velden->'fust_length_mm'->>'waarde', '')::numeric / 10.0, nullif(r.velden->'fust_width_mm'->>'waarde', '')::numeric / 10.0);
39563
+ IF coalesce(fh,0) > 0 AND coalesce(ph,0) > 0 AND coalesce(c.max_height_cm,0) > 0 THEN
39564
+ laag := greatest(fh, ph);
39565
+ IF lpc * laag > c.max_height_cm + 1e-9 AND ph >= fh AND coalesce(r.velden->'plant_height_cm'->>'bron', '') ILIKE 'floricode%' THEN
39566
+ waar := waar || format('planthoogte volgens Floricode; controleer: %s lagen × %s cm = %s cm, hoger dan %s (%s cm)',
39567
+ lpc, trim_scale(round(laag,1)), trim_scale(round(lpc*laag,1)), naam, trim_scale(round(c.max_height_cm::numeric,1)));
39568
+ ELSIF lpc * laag > c.max_height_cm + 1e-9 THEN
39569
+ blok := blok || format('%s lagen × %s cm = %s cm, terwijl %s %s cm hoog is; er passen maximaal %s lagen',
39570
+ lpc, trim_scale(round(laag,1)), trim_scale(round(lpc*laag,1)), naam, trim_scale(round(c.max_height_cm::numeric,1)), floor(c.max_height_cm / laag));
39571
+ END IF;
39572
+ ELSE
39573
+ IF coalesce(fh,0) <= 0 THEN hmist := hmist || 'fusthoogte'::text; END IF;
39574
+ IF coalesce(ph,0) <= 0 THEN hmist := hmist || 'planthoogte'::text; END IF;
39575
+ IF coalesce(c.max_height_cm,0) <= 0 THEN hmist := hmist || ('hoogte van ' || naam); END IF;
39576
+ END IF;
39577
+ IF public.logistics_laag_tolerantie_tekst(c.layer_length_cm, c.layer_width_cm,
39578
+ nullif(r.velden->'fust_length_mm'->>'waarde', '')::numeric / 10.0, nullif(r.velden->'fust_width_mm'->>'waarde', '')::numeric / 10.0, cpl) IS NOT NULL THEN
39579
+ waar := waar || public.logistics_laag_tolerantie_tekst(c.layer_length_cm, c.layer_width_cm,
39580
+ nullif(r.velden->'fust_length_mm'->>'waarde', '')::numeric / 10.0, nullif(r.velden->'fust_width_mm'->>'waarde', '')::numeric / 10.0, cpl);
39581
+ END IF;
39582
+ IF ls IS NOT NULL AND cpl > ls THEN
39583
+ blok := blok || format('%s fust per laag (bron: %s), maar op een laag van %s passen er maximaal %s', cpl, bron, naam, ls);
39584
+ END IF;
39585
+ IF coalesce(c.max_crates_per_layer,0) > 0 AND cpl > c.max_crates_per_layer THEN
39586
+ blok := blok || format('%s fust per laag (bron: %s), maar %s staat maximaal %s fust per laag toe', cpl, bron, naam, c.max_crates_per_layer);
39587
+ END IF;
39588
+ IF cardinality(blok) = 0 AND cardinality(hmist) > 0 AND coalesce(c.layers_per_carrier,0) > 0 AND lpc > c.layers_per_carrier THEN
39589
+ waar := waar || format('%s lagen opgegeven, standaard %s; niet te controleren omdat de %s ontbreekt', lpc, c.layers_per_carrier, array_to_string(hmist, ', '));
39590
+ END IF;
39591
+ IF 'weight' = ANY(coalesce(r.ontbrekend, '{}')) THEN waar := waar || 'gewicht per fust ontbreekt'::text; END IF;
39592
+
39593
+ product_id := r.product_id; "actieUrl" := c_url;
39594
+ IF cardinality(blok) > 0 THEN
39595
+ oordeel := 'onmogelijk'; tekst := format('%s past niet op %s: %s.', r.product, naam, array_to_string(blok, '; '));
39596
+ ELSIF cardinality(mist) > 0 THEN
39597
+ oordeel := 'mist_gegevens';
39598
+ tekst := CASE WHEN 'belading niet ingesteld' = ANY(mist) THEN format('%s: belading niet ingesteld.', r.product)
39599
+ WHEN v_lfout IS NOT NULL AND v_lfout = ANY(mist) THEN format('%s: %s.', r.product, v_lfout)
39600
+ ELSE r.uitleg END;
39601
+ ELSIF cardinality(waar) > 0 THEN
39602
+ oordeel := 'waarschuwing'; tekst := format('%s: %s.', r.product, array_to_string(waar, '; '));
39603
+ ELSE
39604
+ oordeel := 'compleet'; tekst := format('%s: gegevens voor karindeling compleet.', r.product);
39605
+ END IF;
39606
+ RETURN NEXT;
39607
+ END LOOP;
39608
+ END $function$;
39609
+
39610
+ CREATE OR REPLACE FUNCTION public.logistics_product_oordeel_belading(p_regels jsonb)
39611
+ RETURNS jsonb
39612
+ LANGUAGE plpgsql
39613
+ STABLE SECURITY DEFINER
39614
+ SET search_path TO 'public'
39615
+ AS $function$
39616
+ DECLARE e jsonb; gj jsonb; gmap jsonb := '{}'::jsonb; c record; rc jsonb; hv jsonb; v_code text; v_eigen text; ids uuid[]; uit jsonb := '[]'::jsonb;
39617
+ ls int; fh numeric; ph numeric; cpl numeric; lpc numeric; ppc numeric; laag numeric; naam text; v_prod text; fl numeric; fb numeric;
39618
+ blok text[]; waar text[]; mist text[]; hmist text[]; v_pid uuid; v_oordeel text; v_tekst text; v_heeft_kol boolean; v_cfout text;
39619
+ BEGIN
39620
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_product_oordeel_belading');
39621
+ IF p_regels IS NULL OR jsonb_typeof(p_regels) <> 'array' THEN RAISE EXCEPTION 'p_regels moet een lijst zijn.' USING ERRCODE = '22023'; END IF;
39622
+ IF jsonb_array_length(p_regels) > 1000 THEN
39623
+ RAISE EXCEPTION 'Maximaal 1000 regels per aanroep (ontvangen: %).', jsonb_array_length(p_regels) USING ERRCODE = '22023';
39624
+ END IF;
39625
+ SELECT array_agg(DISTINCT (x->>'product_id')::uuid) INTO ids FROM jsonb_array_elements(p_regels) x
39626
+ WHERE nullif(btrim(x->>'product_id'), '') ~* '^[0-9a-f-]{36}$';
39627
+ IF ids IS NOT NULL THEN
39628
+ SELECT coalesce(jsonb_object_agg(g.product_id::text, to_jsonb(g)), '{}'::jsonb) INTO gmap
39629
+ FROM public.logistics_data_gereedheid(ids) g;
39630
+ END IF;
39631
+ SELECT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = to_regclass('public.products') AND attname = 'load_carrier_type' AND NOT attisdropped) INTO v_heeft_kol;
39632
+
39633
+ FOR e IN SELECT x FROM jsonb_array_elements(p_regels) x LOOP
39634
+ blok := '{}'; waar := '{}'; mist := '{}'; hmist := '{}'; v_eigen := NULL; v_cfout := NULL;
39635
+ SELECT NULL::text AS code, NULL::text AS name, NULL::numeric AS layer_length_cm, NULL::numeric AS layer_width_cm,
39636
+ NULL::numeric AS max_height_cm, NULL::integer AS max_crates_per_layer, NULL::integer AS layers_per_carrier INTO c;
39637
+ v_pid := CASE WHEN nullif(btrim(e->>'product_id'), '') ~* '^[0-9a-f-]{36}$' THEN (e->>'product_id')::uuid END;
39638
+ gj := CASE WHEN v_pid IS NULL THEN NULL ELSE gmap->(v_pid::text) END;
39639
+ IF gj IS NULL THEN
39640
+ uit := uit || jsonb_build_array(jsonb_build_object('product_id', v_pid, 'oordeel', 'mist_gegevens', 'tekst', 'product onbekend',
39641
+ 'regels', jsonb_build_array('Product onbekend.')));
39642
+ CONTINUE;
39643
+ END IF;
39644
+ v_prod := coalesce(gj->>'product', 'Product');
39645
+ hv := public.logistics_product_hoofdverpakking(v_pid);
39646
+ v_code := nullif(btrim(e->>'carrier_code'), '');
39647
+ IF v_code IS NULL THEN v_code := nullif(hv->>'ladingdrager', ''); END IF;
39648
+ IF v_code IS NULL AND v_heeft_kol THEN
39649
+ EXECUTE 'SELECT nullif(btrim(load_carrier_type::text), '''') FROM public.products WHERE id = $1' INTO v_eigen USING v_pid;
39650
+ v_code := v_eigen;
39651
+ END IF;
39652
+ rc := public.logistics_ladingdrager_resolve(v_code);
39653
+ v_cfout := rc->>'fout';
39654
+ -- 1.0.228: geen ladingdrager (loose) = uitleg, geen blokkade; geen capaciteit, geen menging.
39655
+ IF rc->>'code' = 'loose' THEN
39656
+ uit := uit || jsonb_build_array(jsonb_build_object('product_id', v_pid, 'oordeel', 'waarschuwing',
39657
+ 'tekst', format('%s: geen ladingdrager, niet op een kar.', v_prod),
39658
+ 'ladingdrager', 'loose', 'ladingdragerInvoer', rc->>'invoer', 'ladingdragerBron', rc->>'bron',
39659
+ 'regels', jsonb_build_array('Geen ladingdrager: niet op een kar.')));
39660
+ CONTINUE;
39661
+ END IF;
39662
+ IF rc->>'code' IS NOT NULL THEN
39663
+ SELECT t.code, t.name, t.layer_length_cm, t.layer_width_cm, t.max_height_cm, t.max_crates_per_layer, t.layers_per_carrier INTO c
39664
+ FROM public.load_carrier_types t WHERE t.is_active AND t.code = rc->>'code' LIMIT 1;
39665
+ END IF;
39666
+ naam := coalesce(c.name, c.code, 'de ladingdrager');
39667
+ ppc := nullif(e->>'plants_per_crate', '')::numeric;
39668
+ cpl := nullif(e->>'crates_per_layer', '')::numeric;
39669
+ lpc := nullif(e->>'layers_per_cart', '')::numeric;
39670
+ fh := nullif(gj->'velden'->'fust_height_mm'->>'waarde', '')::numeric / 10.0;
39671
+ ph := nullif(gj->'velden'->'plant_height_cm'->>'waarde', '')::numeric;
39672
+ fl := nullif(gj->'velden'->'fust_length_mm'->>'waarde', '')::numeric / 10.0;
39673
+ fb := nullif(gj->'velden'->'fust_width_mm'->>'waarde', '')::numeric / 10.0;
39674
+ IF hv IS NOT NULL AND NOT coalesce((hv->>'aanwezig')::boolean, false) THEN mist := mist || 'belading niet ingesteld'::text; END IF;
39675
+ IF coalesce(ppc, 0) <= 0 THEN mist := mist || 'planten per fust'::text; END IF;
39676
+ IF coalesce(cpl, 0) <= 0 THEN mist := mist || 'fusten per laag'::text; END IF;
39677
+ IF coalesce(lpc, 0) <= 0 THEN mist := mist || 'lagen per kar'::text; END IF;
39678
+
39679
+ IF c.code IS NULL THEN
39680
+ mist := mist || coalesce(v_cfout, 'ladingdrager ontbreekt');
39681
+ ls := public.logistics_max_fusten_per_laag_alle(fl, fb);
39682
+ IF ls IS NOT NULL AND coalesce(cpl,0) > ls THEN
39683
+ blok := blok || format('%s fust per laag, maar op geen enkele ladingdrager in Beheer passen er meer dan %s', cpl, ls);
39684
+ END IF;
39685
+ ELSE
39686
+ ls := public.logistics_max_fusten_per_laag(c.layer_length_cm, c.layer_width_cm, fl, fb);
39687
+ IF public.logistics_laag_tolerantie_tekst(c.layer_length_cm, c.layer_width_cm, fl, fb, cpl) IS NOT NULL THEN
39688
+ waar := waar || public.logistics_laag_tolerantie_tekst(c.layer_length_cm, c.layer_width_cm, fl, fb, cpl);
39689
+ END IF;
39690
+ IF coalesce(lpc,0) > 0 AND coalesce(fh,0) > 0 AND coalesce(ph,0) > 0 AND coalesce(c.max_height_cm,0) > 0 THEN
39691
+ laag := greatest(fh, ph);
39692
+ IF lpc * laag > c.max_height_cm + 1e-9 AND ph >= fh AND coalesce(gj->'velden'->'plant_height_cm'->>'bron', '') ILIKE 'floricode%' THEN
39693
+ waar := waar || format('planthoogte volgens Floricode; controleer: %s lagen × %s cm = %s cm, hoger dan %s (%s cm)',
39694
+ lpc, trim_scale(round(laag,1)), trim_scale(round(lpc*laag,1)), naam, trim_scale(round(c.max_height_cm::numeric,1)));
39695
+ ELSIF lpc * laag > c.max_height_cm + 1e-9 THEN
39696
+ blok := blok || format('%s lagen × %s cm = %s cm, terwijl %s %s cm hoog is; er passen maximaal %s lagen',
39697
+ lpc, trim_scale(round(laag,1)), trim_scale(round(lpc*laag,1)), naam, trim_scale(round(c.max_height_cm::numeric,1)), floor(c.max_height_cm / laag));
39698
+ END IF;
39699
+ ELSE
39700
+ IF coalesce(fh,0) <= 0 THEN hmist := hmist || 'fusthoogte'::text; END IF;
39701
+ IF coalesce(ph,0) <= 0 THEN hmist := hmist || 'planthoogte'::text; END IF;
39702
+ IF coalesce(c.max_height_cm,0) <= 0 THEN hmist := hmist || ('hoogte van ' || naam); END IF;
39703
+ END IF;
39704
+ IF ls IS NOT NULL AND coalesce(cpl,0) > ls THEN
39705
+ blok := blok || format('%s fust per laag, maar op een laag van %s passen er maximaal %s', cpl, naam, ls);
39706
+ END IF;
39707
+ IF coalesce(c.max_crates_per_layer,0) > 0 AND coalesce(cpl,0) > c.max_crates_per_layer THEN
39708
+ blok := blok || format('%s fust per laag, maar %s staat maximaal %s fust per laag toe', cpl, naam, c.max_crates_per_layer);
39709
+ END IF;
39710
+ IF cardinality(blok) = 0 AND cardinality(hmist) > 0 AND coalesce(c.layers_per_carrier,0) > 0 AND coalesce(lpc,0) > c.layers_per_carrier THEN
39711
+ waar := waar || format('%s lagen opgegeven, standaard %s; niet te controleren omdat de %s ontbreekt', lpc, c.layers_per_carrier, array_to_string(hmist, ', '));
39712
+ END IF;
39713
+ END IF;
39714
+ IF coalesce(gj->'ontbrekend', '[]'::jsonb) ? 'weight' THEN waar := waar || 'gewicht per fust ontbreekt'::text; END IF;
39715
+ IF cardinality(blok) > 0 THEN
39716
+ v_oordeel := 'onmogelijk'; v_tekst := format('%s past niet op %s.', v_prod, CASE WHEN c.code IS NULL THEN 'een ladingdrager' ELSE naam END);
39717
+ ELSIF cardinality(mist) > 0 THEN
39718
+ v_oordeel := 'mist_gegevens'; v_tekst := format('%s: %s.', v_prod, array_to_string(mist, ', '));
39719
+ ELSIF cardinality(waar) > 0 THEN
39720
+ v_oordeel := 'waarschuwing'; v_tekst := format('%s: belading mogelijk, met waarschuwing.', v_prod);
39721
+ ELSE
39722
+ v_oordeel := 'compleet'; v_tekst := format('%s: belading past.', v_prod);
39723
+ END IF;
39724
+ uit := uit || jsonb_build_array(jsonb_build_object('product_id', v_pid, 'oordeel', v_oordeel, 'tekst', v_tekst,
39725
+ 'ladingdrager', rc->>'code', 'ladingdragerInvoer', rc->>'invoer', 'ladingdragerBron', rc->>'bron',
39726
+ 'regels', to_jsonb(ARRAY(SELECT upper(left(z,1)) || substr(z,2) || '.' FROM unnest(blok || mist || waar) z))));
39727
+ END LOOP;
39728
+ RETURN uit;
39729
+ END $function$;
39730
+ REVOKE ALL ON FUNCTION public.logistics_ladingdrager_resolve_plan(text) FROM authenticated;
39731
+