@flowselections/logistiek-engine 1.0.243 → 1.0.244

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.
@@ -53027,3 +53027,1780 @@ BEGIN
53027
53027
  END IF;
53028
53028
  RETURN v_res;
53029
53029
  END $function$;
53030
+
53031
+
53032
+ -- ===== Correctie Golf 1 (E1-C) (20261111081722) =====
53033
+ -- ===== Correctie Golf 1 (E1-C) =====
53034
+
53035
+ -- 4. Uitgang naar buiten per transactie uit te zetten (proefstand).
53036
+ CREATE OR REPLACE FUNCTION public.logistics_uitgang_uit()
53037
+ RETURNS boolean LANGUAGE sql STABLE SET search_path TO 'public'
53038
+ AS $function$ SELECT COALESCE(current_setting('logistics.uitgang_uit', true), '') = 'aan' $function$;
53039
+ REVOKE ALL ON FUNCTION public.logistics_uitgang_uit() FROM PUBLIC, anon;
53040
+ GRANT EXECUTE ON FUNCTION public.logistics_uitgang_uit() TO authenticated, service_role;
53041
+
53042
+ -- 4/7/8/2. Bestaande functies gericht aanpassen (zelfde naam en parameters).
53043
+ DO $e1c$
53044
+ DECLARE
53045
+ r record; d text; v_ret text; v_terug text;
53046
+ BEGIN
53047
+ -- Uitgang-uit-bewaking in alles wat mail, http of print naar buiten zet.
53048
+ FOR r IN SELECT p.oid::regprocedure AS sig, p.proname, format_type(p.prorettype, NULL) AS ret
53049
+ FROM pg_proc p
53050
+ WHERE p.pronamespace = 'public'::regnamespace
53051
+ AND p.proname IN ('logistics_queue_stock_email','logistics_queue_capacity_email','logistics_print_via_layout',
53052
+ 'logistics_plan_kick','logistics_recalc_tick_kick')
53053
+ LOOP
53054
+ d := pg_get_functiondef(r.sig);
53055
+ CONTINUE WHEN position('logistics_uitgang_uit()' IN d) > 0;
53056
+ v_terug := CASE WHEN r.ret = 'void' THEN 'RETURN;'
53057
+ WHEN r.ret = 'jsonb' THEN 'RETURN jsonb_build_object(''ok'', false, ''error'', ''Proefstand: er gaat niets naar buiten.'');'
53058
+ WHEN r.ret = 'boolean' THEN 'RETURN false;'
53059
+ ELSE 'RETURN NULL;' END;
53060
+ d := regexp_replace(d, E'\\nBEGIN\\n', E'\nBEGIN\n IF public.logistics_uitgang_uit() THEN ' || v_terug || E' END IF;\n');
53061
+ EXECUTE d;
53062
+ END LOOP;
53063
+
53064
+ -- 7. Verzendbrief-trace: alleen geldige statuswaarden.
53065
+ IF to_regprocedure('public.logistics_shipping_note_job_for_slip(uuid)') IS NOT NULL THEN
53066
+ d := pg_get_functiondef('public.logistics_shipping_note_job_for_slip(uuid)'::regprocedure);
53067
+ IF position('''geblokkeerd'',' IN d) > 0 THEN
53068
+ d := replace(d, '''geblokkeerd'',', '''wachtend'',');
53069
+ EXECUTE d;
53070
+ END IF;
53071
+ END IF;
53072
+
53073
+ -- 8. Vooruit gekozen pickdag: deelkar direct printbaar, met vraag; blijft printbaar in latere rondes.
53074
+ IF to_regprocedure('public.logistics_cart_release_reason(uuid)') IS NOT NULL THEN
53075
+ d := pg_get_functiondef('public.logistics_cart_release_reason(uuid)'::regprocedure);
53076
+ IF position('vooruitGekozen' IN d) = 0 THEN
53077
+ d := replace(d, 'PERFORM public.logistics_acc_guard(''ingelogd'', ''logistics_cart_release_reason'');',
53078
+ 'PERFORM public.logistics_acc_guard(''ingelogd'', ''logistics_cart_release_reason'');
53079
+ -- E1-C/8: bewust vooruit gekozen pickdag = de teamleider beslist; geen wachttijd voor aanvulling.
53080
+ IF NULLIF(current_setting(''logistics.forced_pick_day'', true), '''') IS NOT NULL
53081
+ OR EXISTS (SELECT 1 FROM public.pick_queue q WHERE q.reference = p_cart_id::text AND q.cancelled_at IS NULL
53082
+ AND nullif(q.payload->>''vooruitGekozen'', '''') IS NOT NULL) THEN
53083
+ RETURN NULL;
53084
+ END IF;');
53085
+ EXECUTE d;
53086
+ END IF;
53087
+ END IF;
53088
+
53089
+ IF to_regprocedure('public.logistics_sync_pick_queue_core()') IS NOT NULL THEN
53090
+ d := pg_get_functiondef('public.logistics_sync_pick_queue_core()'::regprocedure);
53091
+ IF position('vooruitGekozen' IN d) = 0 THEN
53092
+ d := replace(d, 'v_ok := v_forced IS NOT NULL OR public.logistics_cart_released(r_cart.cart_id);',
53093
+ 'v_ok := v_forced IS NOT NULL OR public.logistics_cart_released(r_cart.cart_id)
53094
+ OR EXISTS (SELECT 1 FROM public.pick_queue q0 WHERE q0.reference = r_cart.cart_id::text AND q0.cancelled_at IS NULL
53095
+ AND nullif(q0.payload->>''vooruitGekozen'', '''') IS NOT NULL);');
53096
+ d := replace(d, '''printbaar'', v_ok, ''wachtTot'', v_wait, ''wachtReden'', v_reason)',
53097
+ '''printbaar'', v_ok, ''wachtTot'', v_wait, ''wachtReden'', v_reason,
53098
+ ''vooruitGekozen'', COALESCE(v_forced::text, (SELECT nullif(q0.payload->>''vooruitGekozen'', '''') FROM public.pick_queue q0
53099
+ WHERE q0.reference = r_cart.cart_id::text AND q0.cancelled_at IS NULL
53100
+ AND nullif(q0.payload->>''vooruitGekozen'', '''') IS NOT NULL LIMIT 1)),
53101
+ ''bevestigVraag'', CASE WHEN v_ok AND NOT public.logistics_cart_is_full(r_cart.cart_id)
53102
+ AND NOT public.logistics_cart_released(r_cart.cart_id)
53103
+ THEN ''Deze kar is nog niet vol. Toch nu printen?'' END)');
53104
+ END IF;
53105
+ -- 2. Snelle uitweg als er sinds de vorige ronde niets is veranderd.
53106
+ IF position('logistics_pickwachtrij_overslaan' IN d) = 0 THEN
53107
+ d := replace(d, 'PERFORM public.logistics_acc_guard(''ingelogd'', ''logistics_sync_pick_queue_core'');',
53108
+ 'PERFORM public.logistics_acc_guard(''ingelogd'', ''logistics_sync_pick_queue_core'');
53109
+ -- E1-C/2: niets veranderd sinds de vorige ronde = direct klaar.
53110
+ IF NULLIF(current_setting(''logistics.forced_pick_day'', true), '''') IS NULL AND public.logistics_pickwachtrij_overslaan() THEN
53111
+ RETURN jsonb_build_object(''toegevoegd'', 0, ''afgesloten'', 0, ''kandidaten'', 0, ''guard'', ''niets veranderd sinds de vorige ronde'');
53112
+ END IF;');
53113
+ END IF;
53114
+ EXECUTE d;
53115
+ END IF;
53116
+ END $e1c$;
53117
+
53118
+ -- 2. Goedkope vingerafdruk van alles wat de pickwachtrij laat veranderen (ook tijd: venster open, wachttijd voorbij).
53119
+ CREATE OR REPLACE FUNCTION public.logistics_pickwachtrij_sig()
53120
+ RETURNS text LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public'
53121
+ AS $function$
53122
+ SELECT concat_ws('|',
53123
+ public.logistics_local_today()::text,
53124
+ (SELECT count(*) || ':' || count(*) FILTER (WHERE lt.planned_start <= now()) || ':' || COALESCE(max(lt.updated_at)::text, '')
53125
+ FROM public.logistics_tasks lt
53126
+ WHERE lt.task_type = 'pick.execute' AND lt.status NOT IN ('done','cancelled','superseded','skipped')),
53127
+ (SELECT count(*) || ':' || COALESCE(max(q.updated_at)::text, '') || ':'
53128
+ || count(*) FILTER (WHERE (q.payload->>'printbaar') = 'false'
53129
+ AND nullif(q.payload->>'wachtTot', '') IS NOT NULL
53130
+ AND (q.payload->>'wachtTot')::timestamptz <= now())
53131
+ FROM public.pick_queue q WHERE q.cancelled_at IS NULL AND q.completed_at IS NULL),
53132
+ (SELECT COALESCE(max(c.updated_at)::text, '') FROM public.order_carts c),
53133
+ (SELECT COALESCE(max(o.updated_at)::text, '') FROM public.orders o));
53134
+ $function$;
53135
+ REVOKE ALL ON FUNCTION public.logistics_pickwachtrij_sig() FROM PUBLIC, anon, authenticated;
53136
+ GRANT EXECUTE ON FUNCTION public.logistics_pickwachtrij_sig() TO service_role;
53137
+
53138
+ CREATE OR REPLACE FUNCTION public.logistics_pickwachtrij_overslaan()
53139
+ RETURNS boolean LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
53140
+ AS $function$
53141
+ DECLARE v_sig text := public.logistics_pickwachtrij_sig(); v_oud text; v_wanneer timestamptz;
53142
+ BEGIN
53143
+ SELECT cursor, bijgewerkt_op INTO v_oud, v_wanneer FROM public.logistics_werker_stand WHERE werker = 'pickwachtrij-vingerafdruk';
53144
+ IF v_oud = v_sig AND v_wanneer > now() - interval '10 minutes' THEN RETURN true; END IF;
53145
+ INSERT INTO public.logistics_werker_stand AS s (werker, cursor, bijgewerkt_op) VALUES ('pickwachtrij-vingerafdruk', v_sig, now())
53146
+ ON CONFLICT (werker) DO UPDATE SET cursor = EXCLUDED.cursor, bijgewerkt_op = now();
53147
+ RETURN false;
53148
+ END $function$;
53149
+ REVOKE ALL ON FUNCTION public.logistics_pickwachtrij_overslaan() FROM PUBLIC, anon, authenticated;
53150
+ GRANT EXECUTE ON FUNCTION public.logistics_pickwachtrij_overslaan() TO service_role;
53151
+
53152
+ -- 4. Voorraadmail: niet meer direct in de trigger, maar door de werker na commit (en minstens 2 minuten later).
53153
+ CREATE OR REPLACE FUNCTION public.logistics_stock_alert_email_trigger()
53154
+ RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
53155
+ AS $function$
53156
+ BEGIN
53157
+ -- E1-C/4: de melding blijft 'pending'; logistics_stock_email_klaarzetten zet hem pas na commit in de outbox.
53158
+ RETURN NULL;
53159
+ END; $function$;
53160
+
53161
+ CREATE OR REPLACE FUNCTION public.logistics_stock_email_klaarzetten(p_max integer DEFAULT 50)
53162
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
53163
+ AS $function$
53164
+ DECLARE r record; v_gezet int := 0; v_vervallen int := 0; v_fouten int := 0;
53165
+ BEGIN
53166
+ PERFORM public.logistics_acc_guard('server', 'logistics_stock_email_klaarzetten');
53167
+ IF public.logistics_uitgang_uit() THEN RETURN jsonb_build_object('overgeslagen', 'proefstand'); END IF;
53168
+ FOR r IN
53169
+ SELECT a.id, a.order_id, o.id AS o_id, o.deleted_at, o.cancelled_at, a.status
53170
+ FROM public.logistics_stock_alerts a LEFT JOIN public.orders o ON o.id = a.order_id
53171
+ WHERE a.email_status = 'pending' AND a.email_enabled AND a.created_at <= now() - interval '2 minutes'
53172
+ ORDER BY a.created_at LIMIT greatest(COALESCE(p_max, 50), 1)
53173
+ LOOP
53174
+ BEGIN
53175
+ IF r.status <> 'open' OR (r.order_id IS NOT NULL AND (r.o_id IS NULL OR r.deleted_at IS NOT NULL OR r.cancelled_at IS NOT NULL)) THEN
53176
+ UPDATE public.logistics_stock_alerts SET email_status = 'skipped',
53177
+ email_error = 'Niet verstuurd: de order bestaat niet meer of de melding is al afgehandeld.' WHERE id = r.id;
53178
+ v_vervallen := v_vervallen + 1;
53179
+ ELSE
53180
+ PERFORM public.logistics_queue_stock_email(r.id);
53181
+ v_gezet := v_gezet + 1;
53182
+ END IF;
53183
+ EXCEPTION WHEN OTHERS THEN
53184
+ v_fouten := v_fouten + 1;
53185
+ PERFORM public.logistics_melding('logistics_stock_email_klaarzetten', r.order_id,
53186
+ 'Voorraadmail kon niet worden klaargezet; de volgende ronde probeert het opnieuw', SQLERRM);
53187
+ END;
53188
+ END LOOP;
53189
+ RETURN jsonb_build_object('klaargezet', v_gezet, 'vervallen', v_vervallen, 'fouten', v_fouten);
53190
+ END $function$;
53191
+ REVOKE ALL ON FUNCTION public.logistics_stock_email_klaarzetten(integer) FROM PUBLIC, anon, authenticated;
53192
+ GRANT EXECUTE ON FUNCTION public.logistics_stock_email_klaarzetten(integer) TO service_role;
53193
+
53194
+ -- 1. Ophaalmoment: elke vervoerderswissel bepaalt het moment opnieuw (ook binnen een Engine-schrijfactie).
53195
+ CREATE OR REPLACE FUNCTION public.logistics_orders_pickup_moment()
53196
+ RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$
53197
+ DECLARE n jsonb; v_basis jsonb;
53198
+ BEGIN
53199
+ IF TG_OP = 'UPDATE' AND NEW.carrier_id IS DISTINCT FROM OLD.carrier_id THEN
53200
+ -- E1-C/1: andere vervoerder = nieuw moment, tenzij de planning is vastgezet.
53201
+ IF NEW.plan_locked_at IS NOT NULL THEN RETURN NEW; END IF;
53202
+ BEGIN
53203
+ v_basis := COALESCE(NEW.custom_fields, '{}'::jsonb) - 'pickup_moment' - 'pickup_moment_key' - 'pickup_moment_bron' - 'pickup_moment_reden';
53204
+ n := public.logistics_pickup_cf_nieuw(v_basis, NEW.carrier_id, NEW.delivery_date::text,
53205
+ NEW.deleted_at, NEW.cancelled_at, NEW.plan_locked_at, NULL);
53206
+ IF n IS NOT NULL THEN NEW.custom_fields := n; END IF;
53207
+ EXCEPTION WHEN OTHERS THEN
53208
+ RAISE WARNING 'logistiek-engine: ophaalmoment na vervoerderswissel mislukt voor order %: %', NEW.id, SQLERRM;
53209
+ PERFORM public.logistics_melding('logistics_orders_pickup_moment', NEW.id,
53210
+ 'Ophaalmoment kon na een vervoerderswissel niet opnieuw worden bepaald', SQLERRM);
53211
+ END;
53212
+ RETURN NEW;
53213
+ END IF;
53214
+ IF COALESCE(current_setting('logistics.pickup_writer', true), '') = 'engine' THEN RETURN NEW; END IF;
53215
+ IF TG_OP = 'UPDATE'
53216
+ AND NEW.delivery_date IS NOT DISTINCT FROM OLD.delivery_date
53217
+ AND (NEW.custom_fields->>'floriday_latest_delivery_at') IS NOT DISTINCT FROM (OLD.custom_fields->>'floriday_latest_delivery_at')
53218
+ AND NEW.deleted_at IS NOT DISTINCT FROM OLD.deleted_at
53219
+ AND (nullif(btrim(COALESCE(NEW.custom_fields->>'pickup_moment', '')), '') IS NOT NULL
53220
+ OR (NEW.custom_fields->'pickup_moment_reden') IS NOT NULL) THEN
53221
+ RETURN NEW;
53222
+ END IF;
53223
+ BEGIN
53224
+ n := public.logistics_pickup_cf_nieuw(NEW.custom_fields, NEW.carrier_id, NEW.delivery_date::text,
53225
+ NEW.deleted_at, NEW.cancelled_at, NEW.plan_locked_at, NULL);
53226
+ IF n IS NOT NULL THEN NEW.custom_fields := n; END IF;
53227
+ EXCEPTION WHEN OTHERS THEN
53228
+ RAISE WARNING 'logistiek-engine: ophaalmoment bepalen mislukt voor order %: %', NEW.id, SQLERRM;
53229
+ BEGIN
53230
+ PERFORM public._logistics_merge_melding('ophaalmoment_fout', NEW.id, NULL, NULL, 'ophaalmoment bepalen: ' || SQLERRM);
53231
+ EXCEPTION WHEN OTHERS THEN RAISE WARNING 'logistiek-engine: melding ophaalmoment mislukt: %', SQLERRM; END;
53232
+ END;
53233
+ RETURN NEW;
53234
+ END $$;
53235
+
53236
+ -- 1. Open orders waarvan het ophaalmoment niet past bij de huidige vervoerder.
53237
+ CREATE OR REPLACE FUNCTION public.logistics_pickup_afwijkend(p_voorbeelden integer DEFAULT 20)
53238
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public'
53239
+ AS $function$
53240
+ DECLARE v_regel jsonb := public.logistics_pickup_regel(); v jsonb;
53241
+ BEGIN
53242
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_pickup_afwijkend');
53243
+ WITH o AS (
53244
+ SELECT o.id, o.order_number, o.carrier_id, public.logistics_order_date(o.delivery_date::text) AS dag,
53245
+ o.custom_fields->>'pickup_moment' AS huidig,
53246
+ public.logistics_pickup_cf_nieuw(
53247
+ o.custom_fields - 'pickup_moment' - 'pickup_moment_key' - 'pickup_moment_bron' - 'pickup_moment_reden',
53248
+ o.carrier_id, o.delivery_date::text, o.deleted_at, o.cancelled_at, o.plan_locked_at, v_regel) AS n
53249
+ FROM public.orders o
53250
+ WHERE o.deleted_at IS NULL AND o.cancelled_at IS NULL AND o.plan_locked_at IS NULL AND o.carrier_id IS NOT NULL
53251
+ AND coalesce(o.delivery_date, '') ~ '^\d{4}-\d{2}-\d{2}'
53252
+ AND public.logistics_order_date(o.delivery_date::text) >= public.logistics_local_today()
53253
+ AND COALESCE(o.custom_fields->>'pickup_moment_bron', '') <> 'handmatig'
53254
+ AND NOT public.logistics_order_is_backfill(o.id)
53255
+ ), a AS (
53256
+ SELECT o.*, o.n->>'pickup_moment' AS verwacht
53257
+ FROM o
53258
+ WHERE o.n IS NOT NULL
53259
+ AND public.logistics_pickup_ts(o.huidig, o.dag) IS DISTINCT FROM public.logistics_pickup_ts(o.n->>'pickup_moment', o.dag)
53260
+ )
53261
+ SELECT jsonb_build_object('aantal', (SELECT count(*) FROM a),
53262
+ 'voorbeelden', COALESCE((SELECT jsonb_agg(jsonb_build_object('order', order_number, 'orderId', id, 'huidig', huidig, 'verwacht', verwacht))
53263
+ FROM (SELECT * FROM a ORDER BY dag, order_number LIMIT greatest(COALESCE(p_voorbeelden, 20), 0)) x), '[]'::jsonb))
53264
+ INTO v;
53265
+ RETURN v;
53266
+ END $function$;
53267
+ REVOKE ALL ON FUNCTION public.logistics_pickup_afwijkend(integer) FROM PUBLIC, anon;
53268
+ GRANT EXECUTE ON FUNCTION public.logistics_pickup_afwijkend(integer) TO authenticated, service_role;
53269
+
53270
+ CREATE OR REPLACE FUNCTION public.logistics_pickup_herstel_voorbeeld()
53271
+ RETURNS jsonb LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public'
53272
+ AS $function$ SELECT public.logistics_pickup_afwijkend(500) $function$;
53273
+ REVOKE ALL ON FUNCTION public.logistics_pickup_herstel_voorbeeld() FROM PUBLIC, anon;
53274
+ GRANT EXECUTE ON FUNCTION public.logistics_pickup_herstel_voorbeeld() TO authenticated, service_role;
53275
+
53276
+ CREATE OR REPLACE FUNCTION public.logistics_pickup_herstel_uitvoeren(p_max integer DEFAULT 500)
53277
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
53278
+ AS $function$
53279
+ DECLARE r jsonb; v_cf jsonb; o record; v_n jsonb; v_hersteld int := 0; v_fouten jsonb := '[]'::jsonb;
53280
+ BEGIN
53281
+ PERFORM public.logistics_acc_guard('server', 'logistics_pickup_herstel_uitvoeren');
53282
+ FOR r IN SELECT jsonb_array_elements(public.logistics_pickup_afwijkend(greatest(COALESCE(p_max, 500), 1))->'voorbeelden') LOOP
53283
+ BEGIN
53284
+ SELECT * INTO o FROM public.orders WHERE id = (r->>'orderId')::uuid FOR UPDATE;
53285
+ v_cf := COALESCE(o.custom_fields, '{}'::jsonb) - 'pickup_moment' - 'pickup_moment_key' - 'pickup_moment_bron' - 'pickup_moment_reden';
53286
+ v_n := public.logistics_pickup_cf_nieuw(v_cf, o.carrier_id, o.delivery_date::text, o.deleted_at, o.cancelled_at, o.plan_locked_at, NULL);
53287
+ IF v_n IS NOT NULL THEN
53288
+ UPDATE public.orders SET custom_fields = v_n, updated_at = now() WHERE id = o.id;
53289
+ INSERT INTO public.logistics_trace_events(phase, step, status, reason, order_ref, actor)
53290
+ VALUES ('ophaalmoment', 'Ophaalmoment hersteld', 'geslaagd',
53291
+ 'Was ' || COALESCE(r->>'huidig', 'leeg') || ', nu ' || COALESCE(v_n->>'pickup_moment', 'leeg') || ' (past bij de huidige vervoerder).',
53292
+ o.order_number, 'beheer');
53293
+ v_hersteld := v_hersteld + 1;
53294
+ END IF;
53295
+ EXCEPTION WHEN OTHERS THEN
53296
+ v_fouten := v_fouten || jsonb_build_array(jsonb_build_object('order', r->>'order', 'fout', SQLERRM));
53297
+ PERFORM public.logistics_melding('logistics_pickup_herstel_uitvoeren', (r->>'orderId')::uuid, 'Ophaalmoment herstellen mislukt', SQLERRM);
53298
+ END;
53299
+ END LOOP;
53300
+ RETURN jsonb_build_object('hersteld', v_hersteld, 'fouten', v_fouten, 'nogAfwijkend', (public.logistics_pickup_afwijkend(0)->>'aantal')::int);
53301
+ END $function$;
53302
+ REVOKE ALL ON FUNCTION public.logistics_pickup_herstel_uitvoeren(integer) FROM PUBLIC, anon, authenticated;
53303
+ GRANT EXECUTE ON FUNCTION public.logistics_pickup_herstel_uitvoeren(integer) TO service_role;
53304
+
53305
+ -- A. "Al aangemeld" alleen voor de vervoerder waarbij de order echt is aangemeld.
53306
+ CREATE OR REPLACE FUNCTION public.logistics_carrier_frozen_reason(p_order_id uuid)
53307
+ RETURNS text
53308
+ LANGUAGE plpgsql
53309
+ STABLE SECURITY DEFINER
53310
+ SET search_path TO 'public'
53311
+ AS $function$
53312
+ DECLARE v_locked timestamptz; v_carrier uuid; v_hit boolean;
53313
+ BEGIN
53314
+ PERFORM public.logistics_acc_guard('server', 'logistics_carrier_frozen_reason');
53315
+ SELECT plan_locked_at, carrier_id INTO v_locked, v_carrier FROM public.orders WHERE id = p_order_id;
53316
+ IF v_locked IS NOT NULL THEN RETURN 'de planning is vastgezet'; END IF;
53317
+ BEGIN
53318
+ IF public.logistics_order_is_frozen(p_order_id) THEN RETURN 'het picken is begonnen of de order staat op een vastgezette kar'; END IF;
53319
+ EXCEPTION WHEN undefined_function OR undefined_table THEN NULL; END;
53320
+ BEGIN
53321
+ IF public.logistics_order_frozen(p_order_id) THEN RETURN 'de order is gepickt, gecontroleerd of afgemeld'; END IF;
53322
+ EXCEPTION WHEN undefined_function OR undefined_table THEN NULL; END;
53323
+ IF to_regprocedure('public.afh_order_aangemeld_bij(uuid,uuid)') IS NOT NULL THEN
53324
+ -- E1-C/A: Afhandeling weet bij welke vervoerder de order echt is aangemeld.
53325
+ IF v_carrier IS NULL THEN RETURN NULL; END IF;
53326
+ BEGIN
53327
+ EXECUTE 'SELECT public.afh_order_aangemeld_bij($1, $2)' INTO v_hit USING p_order_id, v_carrier;
53328
+ IF COALESCE(v_hit, false) THEN RETURN 'de order is al bij deze vervoerder aangemeld'; END IF;
53329
+ EXCEPTION WHEN OTHERS THEN
53330
+ PERFORM public.logistics_melding('logistics_carrier_frozen_reason', p_order_id, 'Controle op vervoerdersaanmelding mislukt; de order is niet als aangemeld gezien', SQLERRM);
53331
+ END;
53332
+ RETURN NULL;
53333
+ END IF;
53334
+ IF to_regclass('public.carrier_signup_dispatches') IS NOT NULL THEN
53335
+ BEGIN
53336
+ EXECUTE 'SELECT EXISTS (SELECT 1 FROM public.carrier_signup_dispatches d
53337
+ WHERE nullif(to_jsonb(d)->>''sent_at'', '''') IS NOT NULL
53338
+ AND coalesce(to_jsonb(d)->''order_ids'', ''[]''::jsonb) ? $1)'
53339
+ INTO v_hit USING p_order_id::text;
53340
+ IF v_hit THEN RETURN 'de order is al bij de vervoerder aangemeld'; END IF;
53341
+ EXCEPTION WHEN OTHERS THEN
53342
+ PERFORM public.logistics_melding('logistics_carrier_frozen_reason', NULL, 'Controle op vervoerdersaanmelding mislukt; de order is niet als aangemeld gezien', SQLERRM); END;
53343
+ END IF;
53344
+ RETURN NULL;
53345
+ END $function$;
53346
+
53347
+ -- 2. Werkers: gespreide start, en bezette gegevens overslaan in plaats van wachten.
53348
+ CREATE OR REPLACE FUNCTION public.logistics_werker_draai(p_werker text, p_stappen jsonb)
53349
+ RETURNS jsonb
53350
+ LANGUAGE plpgsql
53351
+ SECURITY DEFINER
53352
+ SET search_path TO 'public'
53353
+ AS $function$
53354
+ DECLARE
53355
+ v_start timestamptz;
53356
+ v_eind timestamptz;
53357
+ v_n integer := jsonb_array_length(p_stappen);
53358
+ v_begin integer := 0; v_i integer; s jsonb; v_t timestamptz; v_txt text;
53359
+ v_uit jsonb := '[]'::jsonb; v_fouten integer := 0; v_uitgesteld integer := 0; v_eerste_uitgesteld integer; v_bezet integer := 0;
53360
+ BEGIN
53361
+ PERFORM public.logistics_acc_guard('server', 'logistics_werker_draai');
53362
+ IF NOT pg_try_advisory_xact_lock(hashtext('logistics_werker:' || p_werker)) THEN
53363
+ RETURN jsonb_build_object('werker', p_werker, 'overgeslagen', 'vorige ronde van deze werker loopt nog');
53364
+ END IF;
53365
+ -- E1-C/2: vaste korte startverschuiving per werker (nooit meer dan 1 s), zodat ze niet tegelijk beginnen.
53366
+ PERFORM pg_sleep(CASE p_werker WHEN 'pickwachtrij' THEN 0.3 WHEN 'taken' THEN 0.6 WHEN 'onderhoud' THEN 0.9 ELSE 0 END);
53367
+ -- Bezette gegevens: na 1 s overslaan; de volgende ronde pakt het op.
53368
+ PERFORM set_config('lock_timeout', '1000', true);
53369
+ v_start := clock_timestamp();
53370
+ v_eind := clock_timestamp() + make_interval(secs => public.logistics_werker_budget_ms() / 1000.0);
53371
+ SELECT COALESCE(NULLIF(cursor, '')::integer, 0) INTO v_begin FROM public.logistics_werker_stand WHERE werker = p_werker;
53372
+ v_begin := COALESCE(v_begin, 0) % GREATEST(v_n, 1);
53373
+ FOR k IN 0 .. v_n - 1 LOOP
53374
+ v_i := (v_begin + k) % v_n;
53375
+ s := p_stappen -> v_i;
53376
+ IF clock_timestamp() >= v_eind THEN
53377
+ v_uitgesteld := v_uitgesteld + 1;
53378
+ v_eerste_uitgesteld := COALESCE(v_eerste_uitgesteld, v_i);
53379
+ v_uit := v_uit || jsonb_build_object('stap', s->>'naam', 'uitkomst', 'uitgesteld naar de volgende ronde');
53380
+ CONTINUE;
53381
+ END IF;
53382
+ IF (s ? 'functie') AND to_regprocedure(s->>'functie') IS NULL THEN
53383
+ v_uit := v_uit || jsonb_build_object('stap', s->>'naam', 'uitkomst', 'overgeslagen: onderdeel ontbreekt in deze omgeving');
53384
+ CONTINUE;
53385
+ END IF;
53386
+ v_t := clock_timestamp();
53387
+ BEGIN
53388
+ EXECUTE format('SELECT (%s)::text', s->>'sql') INTO v_txt;
53389
+ v_uit := v_uit || jsonb_build_object('stap', s->>'naam',
53390
+ 'ms', (extract(epoch FROM clock_timestamp() - v_t) * 1000)::int, 'uitkomst', left(COALESCE(v_txt, ''), 300));
53391
+ EXCEPTION
53392
+ WHEN lock_not_available THEN
53393
+ v_bezet := v_bezet + 1;
53394
+ v_uit := v_uit || jsonb_build_object('stap', s->>'naam',
53395
+ 'ms', (extract(epoch FROM clock_timestamp() - v_t) * 1000)::int, 'uitkomst', 'overgeslagen: gegevens even bezet, volgende ronde');
53396
+ WHEN OTHERS THEN
53397
+ v_fouten := v_fouten + 1;
53398
+ v_uit := v_uit || jsonb_build_object('stap', s->>'naam',
53399
+ 'ms', (extract(epoch FROM clock_timestamp() - v_t) * 1000)::int, 'fout', left(SQLERRM, 300));
53400
+ PERFORM public.logistics_melding('werker ' || p_werker, NULL,
53401
+ 'Stap "' || (s->>'naam') || '" mislukt; de volgende ronde probeert het opnieuw', SQLERRM);
53402
+ END;
53403
+ END LOOP;
53404
+ PERFORM set_config('lock_timeout', '0', true);
53405
+
53406
+ INSERT INTO public.logistics_werker_stand AS w (werker, laatste_start, laatste_eind, laatste_duur_ms, uitgesteld, fouten, stappen, cursor, bijgewerkt_op)
53407
+ VALUES (p_werker, v_start, clock_timestamp(), (extract(epoch FROM clock_timestamp() - v_start) * 1000)::int,
53408
+ v_uitgesteld, v_fouten, v_uit, COALESCE(v_eerste_uitgesteld, 0)::text, now())
53409
+ ON CONFLICT (werker) DO UPDATE
53410
+ SET laatste_start = EXCLUDED.laatste_start, laatste_eind = EXCLUDED.laatste_eind,
53411
+ laatste_duur_ms = EXCLUDED.laatste_duur_ms, uitgesteld = EXCLUDED.uitgesteld, fouten = EXCLUDED.fouten,
53412
+ stappen = EXCLUDED.stappen, cursor = EXCLUDED.cursor, bijgewerkt_op = now();
53413
+
53414
+ RETURN jsonb_build_object('werker', p_werker, 'duurMs', (extract(epoch FROM clock_timestamp() - v_start) * 1000)::int,
53415
+ 'uitgesteld', v_uitgesteld, 'fouten', v_fouten, 'bezet', v_bezet, 'stappen', v_uit);
53416
+ END $function$;
53417
+
53418
+ -- 4/5. Pickwachtrij-werker zet voorraadmails klaar; planningswerker neemt de bufferbesluiten over van de oude taak.
53419
+ CREATE OR REPLACE FUNCTION public.logistics_werker_pickwachtrij()
53420
+ RETURNS jsonb
53421
+ LANGUAGE plpgsql
53422
+ SECURITY DEFINER
53423
+ SET search_path TO 'public'
53424
+ AS $function$
53425
+ BEGIN
53426
+ PERFORM public.logistics_acc_guard('server', 'logistics_werker_pickwachtrij');
53427
+ RETURN public.logistics_werker_draai('pickwachtrij', jsonb_build_array(
53428
+ jsonb_build_object('naam', 'Orders afronden', 'sql', 'public.logistics_order_finalize_run(25, 120)'),
53429
+ jsonb_build_object('naam', 'Pickwachtrij bijwerken', 'sql', 'public.logistics_sync_pick_queue()'),
53430
+ jsonb_build_object('naam', 'Laadtoestand', 'sql', 'public.logistics_refresh_plan_state_als_nodig()'),
53431
+ jsonb_build_object('naam', 'Verzendbrieven aanvullen', 'sql', 'public.logistics_ensure_shipping_notes(200)'),
53432
+ jsonb_build_object('naam', 'Verkeerde verzendbrieven herstellen', 'sql', 'public.logistics_fix_wrong_shipping_notes()'),
53433
+ jsonb_build_object('naam', 'Verzendbrieven vrijgeven', 'sql', 'public.logistics_shipping_note_release(NULL::text)'),
53434
+ jsonb_build_object('naam', 'Verzendbrieven na acceptatie', 'sql', 'public.logistics_shipping_note_ensure_accepted(NULL::uuid, 100)'),
53435
+ jsonb_build_object('naam', 'Voorraadmails klaarzetten', 'sql', 'public.logistics_stock_email_klaarzetten(50)'),
53436
+ jsonb_build_object('naam', 'Status voorraadmails', 'sql', 'public.logistics_sync_stock_alert_email_status(500)'),
53437
+ jsonb_build_object('naam', 'Printopdrachten zonder kar', 'sql', 'public.logistics_cleanup_orphan_print_jobs()'),
53438
+ jsonb_build_object('naam', 'Barcodeverzoeken', 'sql', 'public.logistics_fulfill_barcode_requests(100)')
53439
+ ));
53440
+ END $function$;
53441
+
53442
+ CREATE OR REPLACE FUNCTION public.logistics_werker_planning()
53443
+ RETURNS jsonb
53444
+ LANGUAGE plpgsql
53445
+ SECURITY DEFINER
53446
+ SET search_path TO 'public'
53447
+ AS $function$
53448
+ BEGIN
53449
+ PERFORM public.logistics_acc_guard('server', 'logistics_werker_planning');
53450
+ RETURN public.logistics_werker_draai('planning', jsonb_build_array(
53451
+ jsonb_build_object('naam', 'Werkset bijwerken', 'sql', 'public.logistics_werkset_herstel(500)'),
53452
+ jsonb_build_object('naam', 'Ophaalmomenten verversen', 'sql', 'public.logistics_refresh_pickup_moments(4)'),
53453
+ jsonb_build_object('naam', 'Wees-taken afsluiten', 'sql', 'public.logistics_cancel_orphan_tasks()'),
53454
+ jsonb_build_object('naam', 'Ontbrekende orders aanmelden', 'sql', 'public.logistics_reconcile_missing_plan_queue()'),
53455
+ jsonb_build_object('naam', 'Vervoerders bepalen', 'sql', 'public.logistics_carrier_recalc_run(50)'),
53456
+ jsonb_build_object('naam', 'Herindelen na wijziging', 'sql', 'public.logistics_rebuffer_pending()'),
53457
+ jsonb_build_object('naam', 'Bufferbesluiten', 'sql', 'public.logistics_reapply_buffer_decisions()'),
53458
+ jsonb_build_object('naam', 'Dubbele bufferregels opruimen', 'sql', 'public.logistics_prune_buffer_member_queue()'),
53459
+ jsonb_build_object('naam', 'Afgehandelde buffers sluiten', 'sql', 'public.logistics_buffer_detach_late_orders(NULL::uuid)'),
53460
+ jsonb_build_object('naam', 'Orders zonder kar bewaken', 'sql', 'public.logistics_bewaak_zonder_kar()'),
53461
+ jsonb_build_object('naam', 'Planwachtrij opruimen', 'sql', 'public.logistics_plan_housekeeping()'),
53462
+ jsonb_build_object('naam', 'Karindeling onderhoud', 'sql', 'public.logistics_karindeling_onderhoud()')
53463
+ ));
53464
+ END $function$;
53465
+
53466
+ -- 3. Logboek: technische regels niet zichtbaar; bekende gebeurtenissen in gewone taal.
53467
+ CREATE OR REPLACE FUNCTION public.logistics_logboek_tekst(p_tabel text, p_actie text)
53468
+ RETURNS text LANGUAGE sql IMMUTABLE SET search_path TO 'public'
53469
+ AS $function$
53470
+ SELECT CASE
53471
+ WHEN p_tabel = 'order_carts' AND p_actie = 'UPDATE' THEN 'Kar opnieuw ingedeeld'
53472
+ WHEN p_tabel = 'order_carts' AND p_actie = 'DELETE' THEN 'Kar vervallen'
53473
+ WHEN p_tabel = 'pick_queue' AND p_actie = 'INSERT' THEN 'Kar in de pickwachtrij gezet'
53474
+ WHEN p_tabel = 'pick_queue' AND p_actie = 'UPDATE' THEN 'Pickwachtrij bijgewerkt'
53475
+ WHEN p_tabel = 'logistics_plan_queue' AND p_actie = 'INSERT' THEN 'Indeling aangevraagd'
53476
+ ELSE NULL END
53477
+ $function$;
53478
+ REVOKE ALL ON FUNCTION public.logistics_logboek_tekst(text, text) FROM PUBLIC, anon;
53479
+ GRANT EXECUTE ON FUNCTION public.logistics_logboek_tekst(text, text) TO authenticated, service_role;
53480
+
53481
+ CREATE OR REPLACE FUNCTION public.logistics_logboek_doorsturen()
53482
+ RETURNS trigger
53483
+ LANGUAGE plpgsql
53484
+ SECURITY DEFINER
53485
+ SET search_path TO 'public'
53486
+ AS $function$
53487
+ DECLARE
53488
+ v_fout boolean := (NEW.status = 'mislukt' OR NEW.error IS NOT NULL);
53489
+ -- Vaste momenten en de markering 'ingehaald' altijd bewaren (Golf 1 / 8, M, 14).
53490
+ v_altijd boolean := (COALESCE(NEW.node_id, '') = 'stap:afmelden' OR COALESCE(NEW.decision, '') IN ('ingehaald', 'toch indelen'));
53491
+ v_order_id uuid; v_leverdag text; v_vervalt date; v_vorige record; v_tekst text;
53492
+ BEGIN
53493
+ -- E1-C/3: technische regels ("tabel INSERT/UPDATE") niet bewaren; bekende gebeurtenissen in gewone taal.
53494
+ IF NEW.step ~ '^[a-z_]+ (INSERT|UPDATE|DELETE)$' OR COALESCE(NEW.actor, '') LIKE 'database:%' THEN
53495
+ v_tekst := public.logistics_logboek_tekst(COALESCE(NEW.db_table, split_part(NEW.step, ' ', 1)),
53496
+ COALESCE(NEW.db_action, split_part(NEW.step, ' ', 2)));
53497
+ IF NEW.step ~ '^[a-z_]+ (INSERT|UPDATE|DELETE)$' THEN
53498
+ IF v_tekst IS NULL AND NOT v_fout THEN RETURN NULL; END IF;
53499
+ NEW.step := COALESCE(v_tekst, 'Wijziging kon niet worden opgeslagen');
53500
+ END IF;
53501
+ NEW.actor := 'systeem';
53502
+ END IF;
53503
+ IF NEW.order_ref IS NOT NULL THEN
53504
+ IF NOT v_fout AND NOT v_altijd THEN
53505
+ SELECT o.id, o.delivery_date::text INTO v_order_id, v_leverdag
53506
+ FROM public.orders o WHERE o.order_number = NEW.order_ref
53507
+ ORDER BY o.created_at DESC LIMIT 1;
53508
+ IF v_order_id IS NOT NULL AND (public.logistics_order_is_backfill(v_order_id)
53509
+ OR public.logistics_order_date(v_leverdag) < public.logistics_local_today() - 1) THEN
53510
+ RETURN NULL;
53511
+ END IF;
53512
+ END IF;
53513
+ IF NEW.actor = 'afgeleid' AND NEW.node_id IS NOT NULL THEN
53514
+ IF EXISTS (SELECT 1 FROM public.logistics_logboek_alles a
53515
+ WHERE a.order_ref = NEW.order_ref AND a.node_id = NEW.node_id AND a.actor = 'afgeleid') THEN
53516
+ RETURN NULL;
53517
+ END IF;
53518
+ ELSE
53519
+ SELECT a.status, a.decision, a.reason, a.error, a.db_old, a.db_new, a.output INTO v_vorige
53520
+ FROM public.logistics_logboek_alles a
53521
+ WHERE a.order_ref = NEW.order_ref AND a.step IS NOT DISTINCT FROM NEW.step
53522
+ ORDER BY a.created_at DESC LIMIT 1;
53523
+ IF FOUND
53524
+ AND v_vorige.status IS NOT DISTINCT FROM NEW.status
53525
+ AND v_vorige.decision IS NOT DISTINCT FROM NEW.decision
53526
+ AND v_vorige.reason IS NOT DISTINCT FROM NEW.reason
53527
+ AND v_vorige.error IS NOT DISTINCT FROM NEW.error
53528
+ AND v_vorige.db_old IS NOT DISTINCT FROM NEW.db_old
53529
+ AND v_vorige.db_new IS NOT DISTINCT FROM NEW.db_new
53530
+ AND v_vorige.output IS NOT DISTINCT FROM NEW.output THEN
53531
+ RETURN NULL;
53532
+ END IF;
53533
+ END IF;
53534
+ END IF;
53535
+
53536
+ v_vervalt := (COALESCE(NEW.created_at, now()) AT TIME ZONE public.logistics_tz())::date
53537
+ + public.logistics_retention_days(CASE WHEN v_fout THEN 'trace_error_retention_days' ELSE 'trace_retention_days' END);
53538
+ PERFORM public.logistics_logboek_partitie(v_vervalt);
53539
+
53540
+ INSERT INTO public.logistics_logboek (
53541
+ id, trace_id, order_ref, order_source, cart_ref, slip_ref, phase, step, status, actor,
53542
+ input, output, reads, decision, reason, values_used, rule, db_table, db_record, db_action, db_old, db_new,
53543
+ external_system, external_action, external_ref, external_status, external_response,
53544
+ error, attempts, next_attempt_at, duration_ms, created_at, node_id, error_class, calc, next_step, vervalt_op)
53545
+ VALUES (
53546
+ COALESCE(NEW.id, gen_random_uuid()), NEW.trace_id, NEW.order_ref, NEW.order_source, NEW.cart_ref, NEW.slip_ref,
53547
+ NEW.phase, NEW.step, NEW.status, NEW.actor, NEW.input, NEW.output, NEW.reads, NEW.decision, NEW.reason,
53548
+ NEW.values_used, NEW.rule, NEW.db_table, NEW.db_record, NEW.db_action, NEW.db_old, NEW.db_new,
53549
+ NEW.external_system, NEW.external_action, NEW.external_ref, NEW.external_status, NEW.external_response,
53550
+ NEW.error, NEW.attempts, NEW.next_attempt_at, NEW.duration_ms, COALESCE(NEW.created_at, now()),
53551
+ NEW.node_id, NEW.error_class, NEW.calc, NEW.next_step, v_vervalt);
53552
+ RETURN NULL;
53553
+ EXCEPTION WHEN OTHERS THEN
53554
+ PERFORM public.logistics_melding('logistics_logboek_doorsturen', v_order_id,
53555
+ 'Logboekregel kon niet worden bewaard; de verwerking ging door', SQLERRM);
53556
+ RETURN NULL;
53557
+ END $function$;
53558
+
53559
+ -- 9. Stand van de indeling per order.
53560
+ CREATE OR REPLACE FUNCTION public.logistics_order_indeling_stand(p_order_id uuid)
53561
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public'
53562
+ AS $function$
53563
+ DECLARE
53564
+ v_regels int; v_karren int; v_open timestamptz; v_laatst record; v_fp record; v_bij timestamptz;
53565
+ v_verouderd jsonb; v_reden text; v_volgende timestamptz := date_trunc('minute', now()) + interval '1 minute';
53566
+ BEGIN
53567
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_order_indeling_stand');
53568
+ SELECT count(*) INTO v_regels FROM logistics_active.order_items oi WHERE oi.order_id = p_order_id;
53569
+ IF COALESCE(v_regels, 0) = 0 THEN
53570
+ RETURN jsonb_build_object('stand', 'geen_regels', 'bezig_sinds', NULL, 'bijgewerkt_op', NULL, 'verwacht_klaar', NULL,
53571
+ 'tekst', 'Deze order heeft nog geen regels om in te delen.');
53572
+ END IF;
53573
+ SELECT min(q.enqueued_at) INTO v_open FROM public.logistics_plan_queue q
53574
+ WHERE q.order_id = p_order_id AND q.status NOT IN ('done','geblokkeerd','opgelost','gearchiveerd');
53575
+ SELECT q.status, q.error, q.reason_code, q.updated_at INTO v_laatst FROM public.logistics_plan_queue q
53576
+ WHERE q.order_id = p_order_id ORDER BY q.updated_at DESC NULLS LAST LIMIT 1;
53577
+ SELECT f.uitkomst, f.bijgewerkt_op INTO v_fp FROM public.logistics_plan_vingerafdruk f WHERE f.order_id = p_order_id;
53578
+ SELECT count(DISTINCT c.id), max(c.updated_at) INTO v_karren, v_bij FROM logistics_active.order_carts c
53579
+ WHERE c.order_id = p_order_id
53580
+ OR EXISTS (SELECT 1 FROM logistics_active.order_cart_items ci JOIN public.order_items oi ON oi.id = ci.order_item_id
53581
+ WHERE ci.cart_id = c.id AND oi.order_id = p_order_id);
53582
+ v_bij := GREATEST(v_bij, CASE WHEN v_fp.uitkomst IS DISTINCT FROM 'geblokkeerd' THEN v_fp.bijgewerkt_op END);
53583
+
53584
+ IF v_open IS NOT NULL THEN
53585
+ RETURN jsonb_build_object('stand', 'bezig', 'bezig_sinds', v_open, 'bijgewerkt_op', v_bij, 'verwacht_klaar', v_volgende,
53586
+ 'tekst', 'Indeling wordt gemaakt');
53587
+ END IF;
53588
+ IF v_laatst.status = 'geblokkeerd' THEN
53589
+ v_reden := CASE WHEN v_laatst.error IS NULL OR v_laatst.error ~* '(error|sqlstate|function|relation|column|syntax|violates|null value|timeout|exception|[a-z]+_[a-z_]+\()'
53590
+ THEN 'De indeling kon niet worden gemaakt. Probeer het later opnieuw of neem contact op met de beheerder.'
53591
+ ELSE v_laatst.error END;
53592
+ RETURN jsonb_build_object('stand', 'niet_gelukt', 'bezig_sinds', NULL, 'bijgewerkt_op', v_bij, 'verwacht_klaar', NULL,
53593
+ 'tekst', v_reden);
53594
+ END IF;
53595
+ v_verouderd := public.logistics_order_indeling_verouderd_core(p_order_id);
53596
+ IF COALESCE((v_verouderd->>'verouderd')::boolean, false) OR COALESCE(v_karren, 0) = 0 THEN
53597
+ RETURN jsonb_build_object('stand', 'bezig', 'bezig_sinds', COALESCE(v_laatst.updated_at, now()), 'bijgewerkt_op', v_bij,
53598
+ 'verwacht_klaar', v_volgende, 'tekst', 'Indeling wordt gemaakt');
53599
+ END IF;
53600
+ RETURN jsonb_build_object('stand', 'klaar', 'bezig_sinds', NULL, 'bijgewerkt_op', v_bij, 'verwacht_klaar', NULL,
53601
+ 'tekst', 'Indeling is bijgewerkt' || COALESCE(' om ' || public.logistics_local_hhmi(v_bij), ''));
53602
+ END $function$;
53603
+ REVOKE ALL ON FUNCTION public.logistics_order_indeling_stand(uuid) FROM PUBLIC, anon;
53604
+ GRANT EXECUTE ON FUNCTION public.logistics_order_indeling_stand(uuid) TO authenticated, service_role;
53605
+
53606
+ -- 10. Kar-tekening.
53607
+ CREATE OR REPLACE FUNCTION public.logistics_xml(p text)
53608
+ RETURNS text LANGUAGE sql IMMUTABLE SET search_path TO 'public'
53609
+ AS $function$
53610
+ SELECT replace(replace(replace(replace(replace(COALESCE(p, ''), '&', '&amp;'), '<', '&lt;'), '>', '&gt;'), '"', '&quot;'), '''', '&apos;')
53611
+ $function$;
53612
+ REVOKE ALL ON FUNCTION public.logistics_xml(text) FROM PUBLIC, anon;
53613
+ GRANT EXECUTE ON FUNCTION public.logistics_xml(text) TO authenticated, service_role;
53614
+
53615
+ -- Klantkleuren: instelling in de werkwijze (alleen namen van themakleuren), anders veilige standaardlijst.
53616
+ CREATE OR REPLACE FUNCTION public.logistics_klantkleuren()
53617
+ RETURNS text[] LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public'
53618
+ AS $function$
53619
+ WITH bron AS (
53620
+ SELECT NULLIF(COALESCE(rc.settings->'karrenindeling'->'fields'->>'klantkleuren', rc.settings->'karrenindeling'->>'klantkleuren'), '') AS v
53621
+ FROM public.logistics_rule_configs rc
53622
+ LEFT JOIN public.logistics_strategies s ON s.id = rc.strategy_id
53623
+ ORDER BY (rc.is_active AND coalesce(s.is_active, false)) DESC, rc.is_active DESC, rc.updated_at DESC NULLS LAST
53624
+ LIMIT 1),
53625
+ k AS (
53626
+ SELECT array_agg('var(' || t || ')' ORDER BY n) AS l
53627
+ FROM bron, regexp_split_to_table(bron.v, '\s*,\s*') WITH ORDINALITY AS x(t, n)
53628
+ WHERE t ~ '^--[a-z0-9-]{1,40}$')
53629
+ SELECT CASE WHEN COALESCE(cardinality(k.l), 0) > 0 THEN k.l
53630
+ ELSE ARRAY['var(--chart-1)','var(--chart-2)','var(--chart-3)','var(--chart-4)','var(--chart-5)'] END
53631
+ FROM k
53632
+ $function$;
53633
+ REVOKE ALL ON FUNCTION public.logistics_klantkleuren() FROM PUBLIC, anon;
53634
+ GRANT EXECUTE ON FUNCTION public.logistics_klantkleuren() TO authenticated, service_role;
53635
+
53636
+ CREATE OR REPLACE FUNCTION public.logistics_kar_tekening(p_order_id uuid, p_opties jsonb DEFAULT '{}'::jsonb)
53637
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public'
53638
+ AS $function$
53639
+ DECLARE
53640
+ v_stand jsonb; v_opt jsonb := COALESCE(p_opties, '{}'::jsonb); v_pakbon boolean;
53641
+ v_kleuren text[] := public.logistics_klantkleuren();
53642
+ v_order record; v_fase text; v_fl jsonb; v_fl_bron boolean;
53643
+ c record; v_totaal int; v_nr int := 0; v_soort text; v_woord text; v_max int;
53644
+ v_letters jsonb; v_lagen jsonb; v_laag jsonb; v_blokken jsonb; it record; v_cur int; v_span int; i int;
53645
+ v_svg text; v_y numeric; v_h numeric; v_x numeric; v_w numeric; v_som numeric; b jsonb; v_kleur text; v_tekst text;
53646
+ v_status text; v_waarsch jsonb; v_gedeeld int; v_dragers jsonb := '[]'::jsonb; v_rij text := ''; v_label text;
53647
+ v_fusten int; v_n int; v_m int; v_cellen int; v_fl_kar text;
53648
+ BEGIN
53649
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_kar_tekening');
53650
+ v_pakbon := COALESCE(v_opt->>'vorm', '') = 'pakbon';
53651
+ v_stand := public.logistics_order_indeling_stand(p_order_id);
53652
+ SELECT o.id, o.order_number, o.status, o.order_source, o.floriday_sales_order_id INTO v_order FROM public.orders o WHERE o.id = p_order_id;
53653
+ v_fase := public.logistics_status_fase(v_order.status);
53654
+ v_fl_bron := v_order.floriday_sales_order_id IS NOT NULL OR COALESCE(v_order.order_source, '') ILIKE '%floriday%';
53655
+ IF v_fl_bron THEN v_fl := public.logistics_floriday_gelijk(p_order_id); END IF;
53656
+
53657
+ SELECT count(*) INTO v_totaal FROM (
53658
+ SELECT DISTINCT c2.id FROM logistics_active.order_carts c2
53659
+ WHERE c2.order_id = p_order_id
53660
+ OR EXISTS (SELECT 1 FROM logistics_active.order_cart_items ci JOIN public.order_items oi ON oi.id = ci.order_item_id
53661
+ WHERE ci.cart_id = c2.id AND oi.order_id = p_order_id)) x;
53662
+
53663
+ FOR c IN
53664
+ SELECT DISTINCT ON (c2.cart_number, c2.id) c2.id, c2.cart_number, c2.occupancy_pct, c2.total_layers_used, c2.max_layers, c2.weight_kg,
53665
+ COALESCE(t.capacity_model, 'layered') AS soort, COALESCE(t.name, c2.carrier_type, 'Ladingdrager') AS naam,
53666
+ t.layers_per_carrier, t.max_crates_per_layer, t.max_weight_kg
53667
+ FROM logistics_active.order_carts c2
53668
+ LEFT JOIN public.load_carrier_types t ON t.id = c2.carrier_type_id
53669
+ WHERE c2.order_id = p_order_id
53670
+ OR EXISTS (SELECT 1 FROM logistics_active.order_cart_items ci JOIN public.order_items oi ON oi.id = ci.order_item_id
53671
+ WHERE ci.cart_id = c2.id AND oi.order_id = p_order_id)
53672
+ ORDER BY c2.cart_number, c2.id
53673
+ LOOP
53674
+ v_nr := v_nr + 1;
53675
+ CONTINUE WHEN (v_opt ? 'drager_id') AND (v_opt->>'drager_id') <> c.id::text;
53676
+ v_soort := CASE WHEN c.soort IN ('layered','stacked','single','none') THEN c.soort ELSE 'layered' END;
53677
+ v_woord := CASE v_soort WHEN 'layered' THEN 'Kar' WHEN 'none' THEN 'Colli' ELSE 'Pallet' END;
53678
+ v_max := GREATEST(1, COALESCE(c.max_layers, c.layers_per_carrier, CEIL(COALESCE(c.total_layers_used, 1))::int, 1));
53679
+
53680
+ -- Klantletters per order op deze drager: eigen order eerst (A).
53681
+ SELECT COALESCE(jsonb_object_agg(order_id::text, chr((64 + rn)::int)), '{}'::jsonb) INTO v_letters FROM (
53682
+ SELECT oi.order_id, row_number() OVER (ORDER BY (oi.order_id = p_order_id) DESC, min(o2.order_number)) AS rn
53683
+ FROM public.order_cart_items ci JOIN public.order_items oi ON oi.id = ci.order_item_id
53684
+ JOIN public.orders o2 ON o2.id = oi.order_id
53685
+ WHERE ci.cart_id = c.id GROUP BY oi.order_id) l;
53686
+ SELECT count(DISTINCT oi.order_id) FILTER (WHERE oi.order_id <> p_order_id) INTO v_gedeeld
53687
+ FROM public.order_cart_items ci JOIN public.order_items oi ON oi.id = ci.order_item_id WHERE ci.cart_id = c.id;
53688
+
53689
+ -- Lagen opbouwen: met laagnummer per regel, anders opeenvolgend volgens het aantal lagen per regel.
53690
+ v_lagen := '[]'::jsonb;
53691
+ FOR i IN 1 .. v_max LOOP
53692
+ v_lagen := v_lagen || jsonb_build_array(jsonb_build_object('index', i, 'hoogte_aandeel', round(1.0 / v_max, 4), 'blokken', '[]'::jsonb));
53693
+ END LOOP;
53694
+ v_cur := 1;
53695
+ FOR it IN
53696
+ SELECT ci.id, ci.layer_index, COALESCE(ci.crates_in_layer, ci.crates, ci.boxes, 0) AS fusten, ci.lagen, ci.layer_fraction,
53697
+ COALESCE(ci.product_name, oi.product_name, 'Product') AS product, oi.order_id
53698
+ FROM public.order_cart_items ci LEFT JOIN public.order_items oi ON oi.id = ci.order_item_id
53699
+ WHERE ci.cart_id = c.id ORDER BY ci.layer_index NULLS LAST, ci.created_at, ci.id
53700
+ LOOP
53701
+ IF it.layer_index IS NOT NULL THEN v_cur := it.layer_index; v_span := 1;
53702
+ ELSE v_span := GREATEST(1, CEIL(COALESCE(it.lagen, it.layer_fraction, 1))::int); END IF;
53703
+ FOR i IN v_cur .. v_cur + v_span - 1 LOOP
53704
+ IF i > jsonb_array_length(v_lagen) THEN
53705
+ v_lagen := v_lagen || jsonb_build_array(jsonb_build_object('index', i, 'hoogte_aandeel', 0, 'blokken', '[]'::jsonb));
53706
+ END IF;
53707
+ b := CASE WHEN it.order_id = p_order_id
53708
+ THEN jsonb_build_object('klantletter', v_letters->>it.order_id::text, 'product', it.product,
53709
+ 'fusten', CEIL(it.fusten::numeric / v_span)::int, 'eigen_order', true)
53710
+ ELSE jsonb_build_object('klantletter', NULL, 'product', NULL,
53711
+ 'fusten', CEIL(it.fusten::numeric / v_span)::int, 'eigen_order', false) END;
53712
+ v_lagen := jsonb_set(v_lagen, ARRAY[(i - 1)::text, 'blokken'], (v_lagen->(i - 1)->'blokken') || jsonb_build_array(b));
53713
+ END LOOP;
53714
+ IF it.layer_index IS NULL THEN v_cur := v_cur + v_span; END IF;
53715
+ END LOOP;
53716
+ -- Gelijke hoogtes als er meer lagen zijn ontstaan dan het maximum.
53717
+ IF jsonb_array_length(v_lagen) > v_max THEN
53718
+ SELECT jsonb_agg(jsonb_set(l, '{hoogte_aandeel}', to_jsonb(round(1.0 / jsonb_array_length(v_lagen), 4))) ORDER BY n) INTO v_lagen
53719
+ FROM jsonb_array_elements(v_lagen) WITH ORDINALITY x(l, n);
53720
+ END IF;
53721
+ SELECT COALESCE(sum((bb->>'fusten')::int), 0) INTO v_fusten
53722
+ FROM jsonb_array_elements(v_lagen) l, jsonb_array_elements(l->'blokken') bb;
53723
+
53724
+ -- Status uit bestaande pick- en controlegegevens.
53725
+ v_status := CASE
53726
+ WHEN v_fase IN ('verzonden','gereed_voor_facturatie','gefactureerd','afgehandeld_floriday') THEN 'afgemeld'
53727
+ WHEN public.logistics_cart_scanned(c.id) THEN 'gecontroleerd'
53728
+ WHEN EXISTS (SELECT 1 FROM public.pick_queue q WHERE q.reference = c.id::text AND q.cancelled_at IS NULL AND q.completed_at IS NOT NULL) THEN 'gepickt'
53729
+ WHEN EXISTS (SELECT 1 FROM public.pick_queue q WHERE q.reference = c.id::text AND q.cancelled_at IS NULL AND q.printed_at IS NOT NULL) THEN 'geprint'
53730
+ WHEN EXISTS (SELECT 1 FROM public.pick_queue q WHERE q.reference = c.id::text AND q.cancelled_at IS NULL AND (q.payload->>'printbaar') = 'true') THEN 'vrijgegeven'
53731
+ ELSE 'open' END;
53732
+
53733
+ v_waarsch := '[]'::jsonb;
53734
+ IF c.max_weight_kg IS NOT NULL AND c.weight_kg IS NOT NULL AND c.weight_kg > c.max_weight_kg THEN
53735
+ v_waarsch := v_waarsch || to_jsonb(v_woord || ' is te zwaar: ' || round(c.weight_kg) || ' kg, maximaal ' || round(c.max_weight_kg) || ' kg');
53736
+ END IF;
53737
+ IF COALESCE(c.occupancy_pct, 0) > 100 THEN
53738
+ v_waarsch := v_waarsch || to_jsonb(v_woord || ' is voller dan toegestaan (' || round(c.occupancy_pct) || '%)');
53739
+ END IF;
53740
+ IF jsonb_array_length(v_lagen) > v_max THEN
53741
+ v_waarsch := v_waarsch || to_jsonb('Er zijn meer lagen gevuld dan de ' || lower(v_woord) || ' heeft');
53742
+ END IF;
53743
+
53744
+ v_fl_kar := NULL;
53745
+ IF v_fl_bron THEN
53746
+ SELECT COALESCE(e->>'uitkomst', e->>'stand') INTO v_fl_kar
53747
+ FROM jsonb_array_elements(CASE WHEN jsonb_typeof(v_fl->'karren') = 'array' THEN v_fl->'karren' ELSE '[]'::jsonb END) e
53748
+ WHERE COALESCE(e->>'cartId', e->>'kar_id', e->>'id') = c.id::text LIMIT 1;
53749
+ v_fl_kar := COALESCE(v_fl_kar, v_fl->>'uitkomst', v_fl->>'stand');
53750
+ END IF;
53751
+
53752
+ v_label := CASE WHEN v_soort = 'none' THEN 'Colli 1–' || GREATEST(v_fusten, 1)
53753
+ ELSE v_woord || ' ' || v_nr || ' van ' || v_totaal END;
53754
+
53755
+ -- SVG: alleen themakleuren, alle tekst ge-escaped, geen scripts of externe verwijzingen.
53756
+ v_svg := '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 260" role="img" aria-label="' || public.logistics_xml(v_label || ', ' || c.naam) || '">'
53757
+ || '<title>' || public.logistics_xml(v_label || ' – ' || c.naam) || '</title>'
53758
+ || '<rect x="1" y="1" width="198" height="258" rx="6" fill="var(--card)" stroke="var(--border)"/>'
53759
+ || '<text x="10" y="18" font-size="12" fill="var(--foreground)">' || public.logistics_xml(v_label) || '</text>';
53760
+ IF v_soort = 'layered' THEN
53761
+ v_y := 240; v_h := 210.0 / GREATEST(jsonb_array_length(v_lagen), 1);
53762
+ FOR i IN 0 .. jsonb_array_length(v_lagen) - 1 LOOP
53763
+ v_laag := v_lagen->i; v_y := v_y - v_h;
53764
+ v_svg := v_svg || '<line x1="10" y1="' || round(v_y + v_h, 1) || '" x2="190" y2="' || round(v_y + v_h, 1) || '" stroke="var(--muted-foreground)" stroke-width="2"/>';
53765
+ SELECT COALESCE(sum((bb->>'fusten')::int), 0) INTO v_som FROM jsonb_array_elements(v_laag->'blokken') bb;
53766
+ v_som := GREATEST(v_som, COALESCE(c.max_crates_per_layer, 0), 1); v_x := 12;
53767
+ FOR b IN SELECT * FROM jsonb_array_elements(v_laag->'blokken') LOOP
53768
+ v_w := 176.0 * (b->>'fusten')::numeric / v_som;
53769
+ v_kleur := CASE WHEN (b->>'eigen_order')::boolean
53770
+ THEN v_kleuren[1 + ((ascii(COALESCE(b->>'klantletter', 'A')) - 65) % cardinality(v_kleuren))]
53771
+ ELSE 'var(--muted)' END;
53772
+ v_svg := v_svg || '<rect x="' || round(v_x, 1) || '" y="' || round(v_y + 2, 1) || '" width="' || round(GREATEST(v_w - 2, 1), 1)
53773
+ || '" height="' || round(GREATEST(v_h - 4, 1), 1) || '" rx="2" fill="' || v_kleur || '" stroke="var(--border)"/>';
53774
+ IF NOT v_pakbon AND v_w >= 24 AND v_h >= 12 THEN
53775
+ v_tekst := CASE WHEN (b->>'eigen_order')::boolean THEN COALESCE(b->>'klantletter', '') || ' ' || (b->>'fusten') ELSE (b->>'fusten') END;
53776
+ v_svg := v_svg || '<text x="' || round(v_x + 3, 1) || '" y="' || round(v_y + v_h / 2 + 4, 1) || '" font-size="9" fill="var(--foreground)">'
53777
+ || public.logistics_xml(v_tekst) || '</text>';
53778
+ END IF;
53779
+ v_x := v_x + v_w;
53780
+ END LOOP;
53781
+ END LOOP;
53782
+ ELSIF v_soort = 'stacked' THEN
53783
+ v_n := GREATEST(1, COALESCE(c.max_crates_per_layer, 4)); v_m := GREATEST(1, CEIL(v_fusten::numeric / v_n)::int);
53784
+ v_h := LEAST(30, 200.0 / v_m);
53785
+ FOR i IN 0 .. v_m - 1 LOOP
53786
+ FOR v_cur IN 0 .. LEAST(v_n, v_fusten - i * v_n) - 1 LOOP
53787
+ v_svg := v_svg || '<rect x="' || round(12 + v_cur * (176.0 / v_n), 1) || '" y="' || round(232 - (i + 1) * v_h, 1)
53788
+ || '" width="' || round(176.0 / v_n - 2, 1) || '" height="' || round(v_h - 2, 1) || '" fill="' || v_kleuren[1] || '" stroke="var(--border)"/>';
53789
+ END LOOP;
53790
+ END LOOP;
53791
+ v_svg := v_svg || '<rect x="8" y="234" width="184" height="8" fill="var(--muted-foreground)"/>'
53792
+ || '<text x="10" y="254" font-size="10" fill="var(--foreground)">' || public.logistics_xml(v_n || ' dozen per laag × ' || v_m || ' lagen') || '</text>';
53793
+ ELSIF v_soort = 'single' THEN
53794
+ SELECT string_agg(DISTINCT bb->>'product', ', ') INTO v_tekst
53795
+ FROM jsonb_array_elements(v_lagen) l, jsonb_array_elements(l->'blokken') bb WHERE (bb->>'eigen_order')::boolean;
53796
+ v_svg := v_svg || '<rect x="20" y="40" width="160" height="180" rx="4" fill="' || v_kleuren[1] || '" stroke="var(--border)"/>'
53797
+ || '<text x="28" y="130" font-size="11" fill="var(--foreground)">' || public.logistics_xml(left(COALESCE(v_tekst, 'Product'), 28)) || '</text>'
53798
+ || '<text x="28" y="148" font-size="11" fill="var(--foreground)">' || public.logistics_xml(v_fusten || ' stuks') || '</text>';
53799
+ ELSE
53800
+ v_cellen := LEAST(GREATEST(v_fusten, 1), 60);
53801
+ FOR i IN 0 .. v_cellen - 1 LOOP
53802
+ v_svg := v_svg || '<rect x="' || (12 + (i % 6) * 30) || '" y="' || (28 + (i / 6) * 22) || '" width="26" height="18" rx="2" fill="' || v_kleuren[1] || '" stroke="var(--border)"/>'
53803
+ || '<text x="' || (16 + (i % 6) * 30) || '" y="' || (41 + (i / 6) * 22) || '" font-size="9" fill="var(--foreground)">' || (i + 1) || '</text>';
53804
+ END LOOP;
53805
+ END IF;
53806
+ IF v_gedeeld > 0 THEN
53807
+ v_svg := v_svg || '<text x="10" y="32" font-size="9" fill="var(--muted-foreground)">'
53808
+ || public.logistics_xml('gedeeld met ' || v_gedeeld || CASE WHEN v_gedeeld = 1 THEN ' andere order' ELSE ' andere orders' END) || '</text>';
53809
+ END IF;
53810
+ v_svg := v_svg || '</svg>';
53811
+ v_rij := v_rij || '<g transform="translate(' || ((v_nr - 1) * 70) || ',0) scale(0.33)">' || substr(v_svg, position('>' IN v_svg) + 1, length(v_svg) - position('>' IN v_svg) - 6) || '</g>';
53812
+
53813
+ v_dragers := v_dragers || jsonb_build_array(jsonb_build_object(
53814
+ 'id', c.id, 'nummer', v_nr, 'totaal', v_totaal, 'soort', v_soort, 'naam', c.naam,
53815
+ 'vulgraad_pct', CASE WHEN c.occupancy_pct IS NOT NULL THEN round(c.occupancy_pct) END,
53816
+ 'lagen_gebruikt', CASE WHEN c.total_layers_used IS NOT NULL THEN round(c.total_layers_used, 1) END,
53817
+ 'lagen_max', CASE WHEN v_soort IN ('layered','stacked') THEN v_max END,
53818
+ 'gewicht_kg', CASE WHEN c.weight_kg IS NOT NULL THEN round(c.weight_kg, 1) END,
53819
+ 'status', v_status, 'floriday_gelijk', v_fl_kar, 'gedeeld_met', v_gedeeld,
53820
+ 'waarschuwingen', v_waarsch, 'lagen', v_lagen, 'svg', v_svg));
53821
+ END LOOP;
53822
+
53823
+ RETURN jsonb_build_object(
53824
+ 'stand', v_stand->'stand', 'bezig_sinds', v_stand->'bezig_sinds', 'bijgewerkt_op', v_stand->'bijgewerkt_op',
53825
+ 'verwacht_klaar', v_stand->'verwacht_klaar', 'tekst', v_stand->'tekst',
53826
+ 'svg_rij', CASE WHEN jsonb_array_length(v_dragers) = 0 THEN NULL
53827
+ ELSE '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ' || GREATEST(jsonb_array_length(v_dragers) * 70, 70)
53828
+ || ' 90" role="img" aria-label="Overzicht van ' || jsonb_array_length(v_dragers) || ' ladingdragers"><title>Overzicht ladingdragers</title>'
53829
+ || v_rij || '</svg>' END,
53830
+ 'dragers', v_dragers);
53831
+ END $function$;
53832
+ REVOKE ALL ON FUNCTION public.logistics_kar_tekening(uuid, jsonb) FROM PUBLIC, anon;
53833
+ GRANT EXECUTE ON FUNCTION public.logistics_kar_tekening(uuid, jsonb) TO authenticated, service_role;
53834
+
53835
+ -- Installatiecontrole E1-C (alleen lezen).
53836
+ CREATE OR REPLACE FUNCTION public.logistics_install_check_e1c()
53837
+ RETURNS TABLE(onderdeel text, ok boolean, toelichting text)
53838
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public'
53839
+ AS $function$
53840
+ DECLARE f text; v_order uuid; v jsonb; v_n int;
53841
+ BEGIN
53842
+ FOREACH f IN ARRAY ARRAY['logistics_uitgang_uit()','logistics_pickup_afwijkend(integer)','logistics_pickup_herstel_voorbeeld()',
53843
+ 'logistics_pickup_herstel_uitvoeren(integer)','logistics_stock_email_klaarzetten(integer)','logistics_pickwachtrij_overslaan()',
53844
+ 'logistics_order_indeling_stand(uuid)','logistics_kar_tekening(uuid,jsonb)','logistics_klantkleuren()','logistics_xml(text)'] LOOP
53845
+ onderdeel := 'functie ' || f;
53846
+ ok := to_regprocedure('public.' || f) IS NOT NULL;
53847
+ toelichting := CASE WHEN ok THEN 'aanwezig' ELSE 'ontbreekt: werk de Engine bij via Supabase bijwerken' END;
53848
+ RETURN NEXT;
53849
+ END LOOP;
53850
+ IF to_regprocedure('public.logistics_pickup_afwijkend(integer)') IS NOT NULL THEN
53851
+ v_n := (public.logistics_pickup_afwijkend(0)->>'aantal')::int;
53852
+ onderdeel := 'ophaalmoment past bij vervoerder'; ok := v_n = 0;
53853
+ toelichting := CASE WHEN ok THEN 'alle open orders kloppen'
53854
+ ELSE v_n || ' open orders hebben een ophaalmoment van een andere vervoerder: draai het herstel (handmatige stap)' END;
53855
+ RETURN NEXT;
53856
+ END IF;
53857
+ SELECT c.order_id INTO v_order FROM logistics_active.order_carts c WHERE c.order_id IS NOT NULL ORDER BY c.updated_at DESC LIMIT 1;
53858
+ IF v_order IS NOT NULL AND to_regprocedure('public.logistics_kar_tekening(uuid,jsonb)') IS NOT NULL THEN
53859
+ v := public.logistics_kar_tekening(v_order, '{}'::jsonb);
53860
+ onderdeel := 'stand en kar-tekening';
53861
+ ok := (v->>'stand') IN ('bezig','klaar','niet_gelukt','geen_regels') AND jsonb_typeof(v->'dragers') = 'array';
53862
+ toelichting := CASE WHEN ok THEN 'geldige uitkomst voor een bestaande order' ELSE 'ongeldige uitkomst' END;
53863
+ RETURN NEXT;
53864
+ END IF;
53865
+ END $function$;
53866
+ REVOKE ALL ON FUNCTION public.logistics_install_check_e1c() FROM PUBLIC, anon;
53867
+ GRANT EXECUTE ON FUNCTION public.logistics_install_check_e1c() TO authenticated, service_role;
53868
+
53869
+ -- Golf 1-controle neemt E1-C mee.
53870
+ DO $e1c2$
53871
+ DECLARE d text;
53872
+ BEGIN
53873
+ d := pg_get_functiondef('public.logistics_install_check_golf1()'::regprocedure);
53874
+ IF position('logistics_install_check_e1c' IN d) = 0 THEN
53875
+ d := regexp_replace(d, E'\\nEND \\$function\\$\\s*$', E'\n RETURN QUERY SELECT * FROM public.logistics_install_check_e1c();\nEND $function$');
53876
+ EXECUTE d;
53877
+ END IF;
53878
+ END $e1c2$;
53879
+
53880
+ CREATE OR REPLACE FUNCTION public.logistics_schema_versie()
53881
+ RETURNS text LANGUAGE sql IMMUTABLE SET search_path TO 'public'
53882
+ AS $function$ SELECT '2026-11-10f'::text $function$;
53883
+
53884
+ -- ===== E1-C: interne hulpfuncties alleen voor server (20261111081737) =====
53885
+ REVOKE EXECUTE ON FUNCTION public.logistics_klantkleuren() FROM authenticated;
53886
+ REVOKE EXECUTE ON FUNCTION public.logistics_install_check_e1c() FROM authenticated;
53887
+ REVOKE EXECUTE ON FUNCTION public.logistics_pickup_herstel_voorbeeld() FROM authenticated;
53888
+ REVOKE EXECUTE ON FUNCTION public.logistics_pickup_afwijkend(integer) FROM authenticated;
53889
+ -- ===== E1-C: klantletter in kar-tekening (20261111081807) =====
53890
+ DO $fix$
53891
+ DECLARE d text;
53892
+ BEGIN
53893
+ d := pg_get_functiondef('public.logistics_kar_tekening(uuid,jsonb)'::regprocedure);
53894
+ IF position('chr(64 + rn)' IN d) > 0 THEN
53895
+ d := replace(d, 'chr(64 + rn)', 'chr((64 + rn)::int)');
53896
+ EXECUTE d;
53897
+ END IF;
53898
+ END $fix$;
53899
+ -- ===== E1-C: achterstand pickwachtrij ook overslaan zonder wijziging (20261111082613) =====
53900
+ CREATE OR REPLACE FUNCTION public.logistics_sync_pick_queue()
53901
+ RETURNS jsonb
53902
+ LANGUAGE plpgsql
53903
+ SECURITY DEFINER
53904
+ SET search_path TO 'public'
53905
+ AS $function$
53906
+ DECLARE v_core jsonb; v_back jsonb;
53907
+ BEGIN
53908
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_sync_pick_queue');
53909
+ v_core := public.logistics_sync_pick_queue_core();
53910
+ -- E1-C/2: niets veranderd sinds de vorige ronde = ook de achterstand niet opnieuw doorrekenen.
53911
+ IF COALESCE(v_core->>'guard', '') = 'niets veranderd sinds de vorige ronde' THEN
53912
+ RETURN v_core || jsonb_build_object('achterstand', jsonb_build_object('overgeslagen', 'niets veranderd'));
53913
+ END IF;
53914
+ BEGIN
53915
+ v_back := public.logistics_sync_pick_queue_backlog();
53916
+ EXCEPTION WHEN others THEN
53917
+ v_back := jsonb_build_object('achterstand', 'fout', 'reden', SQLERRM);
53918
+ PERFORM public.logistics_melding('logistics_sync_pick_queue', NULL, 'Achterstand van de pickwachtrij kon niet worden bijgewerkt; de volgende ronde probeert het opnieuw', SQLERRM);
53919
+ END;
53920
+ RETURN COALESCE(v_core, '{}'::jsonb) || jsonb_build_object('achterstand', v_back);
53921
+ END $function$;
53922
+ -- ===== Vervolg E1-C (2026-11-10g): kar-tekening, afleveradressen, verzendmoment, laadtoestand =====
53923
+ -- ===== 2026-11-10g Vervolg E1-C =====
53924
+
53925
+ -- Punt 1: kar-tekening tekenen vanuit de dragergegevens (puur, geen tabellen).
53926
+ CREATE OR REPLACE FUNCTION public.logistics_kar_svg(p jsonb, p_klein boolean DEFAULT false)
53927
+ RETURNS text LANGUAGE plpgsql IMMUTABLE SET search_path TO 'public'
53928
+ AS $function$
53929
+ DECLARE
53930
+ v_soort text := coalesce(p->>'soort', 'layered');
53931
+ v_lagen jsonb := CASE WHEN jsonb_typeof(p->'lagen') = 'array' THEN p->'lagen' ELSE '[]'::jsonb END;
53932
+ v_kl jsonb := CASE WHEN jsonb_typeof(p->'kleuren') = 'array' AND jsonb_array_length(p->'kleuren') > 0 THEN p->'kleuren' ELSE '["#56B4E9"]'::jsonb END;
53933
+ v_fg text := 'var(--foreground, #1f2937)'; v_mut text := 'var(--muted-foreground, #6b7280)';
53934
+ v_bd text := 'var(--border, #d1d5db)'; v_card text := 'var(--card, #ffffff)'; v_red text := 'var(--destructive, #dc2626)';
53935
+ v_grijs text := 'var(--muted, #e5e7eb)'; v_prim text := 'var(--primary, #2563eb)';
53936
+ v_label text := coalesce(p->>'label', 'Ladingdrager'); v_naam text := coalesce(p->>'naam', '');
53937
+ v_pakbon boolean := coalesce((p->>'pakbon')::boolean, false);
53938
+ v_top numeric := 64; v_bot numeric; v_x0 numeric := 34; v_iw numeric := 166;
53939
+ v_n int; v_lh numeric; v_yb numeric; v_yt numeric; v_sh numeric; v_sw numeric;
53940
+ v_laag jsonb; b jsonb; i int; k int; v_P int; v_pos int; v_f int; v_s int; v_som int;
53941
+ v_kleur text; v_x numeric; v_bx numeric; v_titel text; v_body text := ''; v_desc text := '';
53942
+ v_pct numeric; v_g int; v_w jsonb; v_chars int; v_tekst text; v_eigen boolean; v_letter text; v_ly numeric; v_naam_ok boolean;
53943
+ BEGIN
53944
+ v_bot := CASE WHEN v_soort = 'layered' THEN 280 ELSE 282 END;
53945
+ -- Kop
53946
+ IF NOT p_klein THEN
53947
+ v_tekst := v_label || CASE WHEN v_naam <> '' THEN ' · ' || v_naam ELSE '' END;
53948
+ IF length(v_tekst) > 34 THEN v_tekst := left(v_tekst, 33) || '…'; END IF;
53949
+ v_body := v_body || '<text x="12" y="20" font-size="13" font-weight="600" fill="' || v_fg || '">' || public.logistics_xml(v_tekst) || '</text>';
53950
+ v_pct := (p->>'vulgraad_pct')::numeric;
53951
+ IF v_pct IS NOT NULL THEN
53952
+ v_body := v_body || '<rect x="12" y="28" width="166" height="8" rx="4" fill="' || v_grijs || '"/>'
53953
+ || '<rect x="12" y="28" width="' || round(greatest(166 * least(v_pct, 100) / 100, 2), 1) || '" height="8" rx="4" fill="'
53954
+ || CASE WHEN v_pct > 100 THEN v_red ELSE v_prim END || '"/>'
53955
+ || '<text x="186" y="36" font-size="11" fill="' || v_fg || '">' || round(v_pct) || ' %</text>';
53956
+ END IF;
53957
+ v_g := coalesce((p->>'gedeeld_met')::int, 0);
53958
+ IF v_g > 0 THEN
53959
+ v_body := v_body || '<text x="12" y="52" font-size="10" fill="' || v_mut || '">'
53960
+ || public.logistics_xml('gedeeld met ' || v_g || CASE WHEN v_g = 1 THEN ' andere order' ELSE ' andere orders' END) || '</text>';
53961
+ END IF;
53962
+ IF jsonb_typeof(p->'waarschuwingen') = 'array' AND jsonb_array_length(p->'waarschuwingen') > 0 THEN
53963
+ SELECT string_agg(x, '; ') INTO v_tekst FROM jsonb_array_elements_text(p->'waarschuwingen') x;
53964
+ v_body := v_body || '<g><title>' || public.logistics_xml(v_tekst) || '</title><circle cx="224" cy="16" r="9" fill="' || v_red || '"/>'
53965
+ || '<text x="224" y="21" font-size="13" font-weight="700" text-anchor="middle" fill="' || v_card || '">!</text></g>';
53966
+ v_desc := v_desc || ' Let op: ' || v_tekst || '.';
53967
+ END IF;
53968
+ ELSE
53969
+ v_body := v_body || '<text x="120" y="50" font-size="52" font-weight="700" text-anchor="middle" fill="' || v_fg || '">'
53970
+ || coalesce((p->>'nummer')::int, 1) || '</text>';
53971
+ END IF;
53972
+
53973
+ IF v_soort IN ('layered', 'stacked') THEN
53974
+ v_n := greatest(jsonb_array_length(v_lagen), 1);
53975
+ v_lh := (v_bot - v_top) / v_n;
53976
+ IF v_soort = 'layered' THEN
53977
+ v_body := v_body || '<rect x="26" y="' || (v_top - 8) || '" width="5" height="' || (v_bot - v_top + 10) || '" rx="2" fill="' || v_mut || '"/>'
53978
+ || '<rect x="203" y="' || (v_top - 8) || '" width="5" height="' || (v_bot - v_top + 10) || '" rx="2" fill="' || v_mut || '"/>'
53979
+ || '<rect x="22" y="' || v_bot || '" width="190" height="7" rx="2" fill="' || v_mut || '"/>'
53980
+ || '<circle cx="42" cy="' || (v_bot + 19) || '" r="8" fill="' || v_card || '" stroke="' || v_mut || '" stroke-width="3"/>'
53981
+ || '<circle cx="192" cy="' || (v_bot + 19) || '" r="8" fill="' || v_card || '" stroke="' || v_mut || '" stroke-width="3"/>'
53982
+ || '<circle cx="42" cy="' || (v_bot + 19) || '" r="2" fill="' || v_mut || '"/><circle cx="192" cy="' || (v_bot + 19) || '" r="2" fill="' || v_mut || '"/>';
53983
+ ELSE
53984
+ v_body := v_body || '<rect x="24" y="' || v_bot || '" width="186" height="5" rx="1" fill="' || v_mut || '"/>'
53985
+ || '<rect x="28" y="' || (v_bot + 5) || '" width="20" height="10" fill="' || v_mut || '"/>'
53986
+ || '<rect x="107" y="' || (v_bot + 5) || '" width="20" height="10" fill="' || v_mut || '"/>'
53987
+ || '<rect x="186" y="' || (v_bot + 5) || '" width="20" height="10" fill="' || v_mut || '"/>'
53988
+ || '<rect x="24" y="' || (v_bot + 15) || '" width="186" height="4" rx="1" fill="' || v_mut || '"/>';
53989
+ END IF;
53990
+ FOR i IN 0 .. v_n - 1 LOOP
53991
+ v_laag := coalesce(v_lagen->i, '{}'::jsonb);
53992
+ v_yb := v_bot - i * v_lh; v_yt := v_yb - v_lh;
53993
+ IF v_soort = 'layered' AND i > 0 THEN
53994
+ v_body := v_body || '<rect x="29" y="' || round(v_yb - 1.5, 1) || '" width="176" height="3" rx="1" fill="' || v_mut || '"/>';
53995
+ END IF;
53996
+ v_P := least(greatest(coalesce((v_laag->>'plekken')::int, 1), 1), 40);
53997
+ v_sw := v_iw / v_P; v_sh := greatest(v_lh - 7, 2);
53998
+ v_pos := 0; v_som := 0;
53999
+ FOR b IN SELECT x FROM jsonb_array_elements(CASE WHEN jsonb_typeof(v_laag->'blokken') = 'array' THEN v_laag->'blokken' ELSE '[]'::jsonb END) x LOOP
54000
+ EXIT WHEN v_pos >= v_P;
54001
+ v_f := least(greatest(coalesce((b->>'fusten')::int, 0), 0), v_P - v_pos);
54002
+ v_s := least(greatest(v_f, round(coalesce((b->>'aandeel')::numeric, 0) * v_P)::int), v_P - v_pos);
54003
+ CONTINUE WHEN v_s = 0;
54004
+ v_eigen := coalesce((b->>'eigen_order')::boolean, false) OR (b->>'klantletter') IS NOT NULL;
54005
+ v_letter := b->>'klantletter';
54006
+ v_kleur := CASE WHEN v_eigen THEN v_kl->>(coalesce((b->>'kleur_index')::int, 0) % jsonb_array_length(v_kl)) ELSE v_grijs END;
54007
+ v_bx := v_x0 + v_pos * v_sw;
54008
+ IF v_pos > 0 THEN
54009
+ v_body := v_body || '<line x1="' || round(v_bx, 1) || '" y1="' || round(v_yt + 2, 1) || '" x2="' || round(v_bx, 1) || '" y2="' || round(v_yb - 3, 1)
54010
+ || '" stroke="' || v_fg || '" stroke-width="0.8" opacity="0.6"/>';
54011
+ END IF;
54012
+ v_titel := concat_ws(' · ', CASE WHEN v_eigen THEN nullif(b->>'product', '') ELSE 'Andere order' END,
54013
+ CASE WHEN coalesce((b->>'eigen_order')::boolean, false) THEN nullif(b->>'klant', '') END,
54014
+ (b->>'fusten') || CASE WHEN (b->>'fusten') = '1' THEN ' fust' ELSE ' fusten' END,
54015
+ 'laag ' || (i + 1) || ': ' || round(coalesce((b->>'aandeel')::numeric, 0) * 100) || ' % van de laag');
54016
+ v_body := v_body || '<g><title>' || public.logistics_xml(v_titel) || '</title>';
54017
+ v_naam_ok := NOT p_klein AND NOT v_pakbon AND v_eigen AND nullif(b->>'product', '') IS NOT NULL AND v_s * v_sw >= 56 AND v_sh >= 34;
54018
+ v_ly := CASE WHEN v_naam_ok THEN v_yt + 3 + 14 ELSE v_yt + 3 + v_sh / 2 + 4 END;
54019
+ FOR k IN 0 .. v_f - 1 LOOP
54020
+ v_x := v_bx + k * v_sw;
54021
+ v_body := v_body || '<rect x="' || round(v_x + 1.5, 1) || '" y="' || round(v_yt + 3, 1) || '" width="' || round(greatest(v_sw - 3, 1), 1)
54022
+ || '" height="' || round(v_sh, 1) || '" rx="3" fill="' || v_kleur || '" stroke="' || v_fg || '" stroke-width="0.6"/>';
54023
+ IF NOT v_eigen THEN
54024
+ v_body := v_body || '<rect x="' || round(v_x + 1.5, 1) || '" y="' || round(v_yt + 3, 1) || '" width="' || round(greatest(v_sw - 3, 1), 1)
54025
+ || '" height="' || round(v_sh, 1) || '" rx="3" fill="url(#lgx-arc)"/>';
54026
+ ELSIF NOT p_klein AND v_letter IS NOT NULL AND v_sw >= 12 AND v_sh >= 14 THEN
54027
+ v_body := v_body || '<text x="' || round(v_x + v_sw / 2, 1) || '" y="' || round(v_ly, 1) || '" font-size="11" font-weight="700" text-anchor="middle" fill="'
54028
+ || v_fg || '" stroke="' || v_card || '" stroke-width="2.5" paint-order="stroke">' || public.logistics_xml(v_letter) || '</text>';
54029
+ END IF;
54030
+ END LOOP;
54031
+ FOR k IN v_f .. v_s - 1 LOOP
54032
+ v_x := v_bx + k * v_sw;
54033
+ v_body := v_body || '<rect x="' || round(v_x + 1.5, 1) || '" y="' || round(v_yt + 3, 1) || '" width="' || round(greatest(v_sw - 3, 1), 1)
54034
+ || '" height="' || round(v_sh, 1) || '" rx="3" fill="none" stroke="' || v_bd || '" stroke-dasharray="3 2"/>';
54035
+ END LOOP;
54036
+ IF v_naam_ok THEN
54037
+ v_chars := floor((v_s * v_sw - 8) / 6)::int;
54038
+ v_tekst := b->>'product';
54039
+ IF length(v_tekst) > v_chars THEN v_tekst := left(v_tekst, greatest(v_chars - 1, 1)) || '…'; END IF;
54040
+ v_body := v_body || '<text x="' || round(v_bx + 4, 1) || '" y="' || round(v_yb - 8, 1) || '" font-size="10" fill="' || v_fg
54041
+ || '" stroke="' || v_card || '" stroke-width="2.5" paint-order="stroke">' || public.logistics_xml(v_tekst) || '</text>';
54042
+ END IF;
54043
+ v_body := v_body || '</g>';
54044
+ v_pos := v_pos + v_s; v_som := v_som + v_f;
54045
+ END LOOP;
54046
+ FOR k IN v_pos .. v_P - 1 LOOP
54047
+ v_x := v_x0 + k * v_sw;
54048
+ v_body := v_body || '<rect x="' || round(v_x + 1.5, 1) || '" y="' || round(v_yt + 3, 1) || '" width="' || round(greatest(v_sw - 3, 1), 1)
54049
+ || '" height="' || round(v_sh, 1) || '" rx="3" fill="none" stroke="' || v_bd || '" stroke-dasharray="3 2"/>';
54050
+ END LOOP;
54051
+ IF NOT p_klein THEN
54052
+ v_body := v_body || '<text x="15" y="' || round(v_yt + v_lh / 2 + 4, 1) || '" font-size="10" text-anchor="middle" fill="' || v_mut || '">' || (i + 1) || '</text>'
54053
+ || '<text x="212" y="' || round(v_yt + v_lh / 2 + 4, 1) || '" font-size="10" fill="' || v_fg || '">' || v_som || '/' || v_P
54054
+ || CASE WHEN v_som >= v_P THEN '<tspan fill="' || v_prim || '" font-weight="700"> ✓</tspan>' ELSE '' END || '</text>';
54055
+ END IF;
54056
+ IF nullif(v_laag->>'waarschuwing', '') IS NOT NULL THEN
54057
+ v_body := v_body || '<g><title>' || public.logistics_xml(v_laag->>'waarschuwing') || '</title><rect x="33" y="' || round(v_yt + 1, 1) || '" width="168" height="'
54058
+ || round(greatest(v_lh - 2, 1), 1) || '" rx="3" fill="none" stroke="' || v_red || '" stroke-width="2"/>'
54059
+ || CASE WHEN NOT p_klein THEN '<text x="232" y="' || round(v_yt + v_lh / 2 + 4, 1) || '" font-size="12" font-weight="700" fill="' || v_red || '">!</text>' ELSE '' END
54060
+ || '</g>';
54061
+ END IF;
54062
+ v_desc := v_desc || ' Laag ' || (i + 1) || ': ' || v_som || ' van ' || v_P || ' plekken.';
54063
+ END LOOP;
54064
+ ELSIF v_soort = 'single' THEN
54065
+ SELECT x INTO b FROM jsonb_array_elements(v_lagen) l, jsonb_array_elements(coalesce(l->'blokken', '[]'::jsonb)) x
54066
+ WHERE coalesce((x->>'eigen_order')::boolean, false) LIMIT 1;
54067
+ SELECT coalesce(sum((x->>'fusten')::int), 0) INTO v_som FROM jsonb_array_elements(v_lagen) l, jsonb_array_elements(coalesce(l->'blokken', '[]'::jsonb)) x;
54068
+ v_kleur := CASE WHEN b IS NULL THEN v_grijs ELSE v_kl->>(coalesce((b->>'kleur_index')::int, 0) % jsonb_array_length(v_kl)) END;
54069
+ v_body := v_body || '<g><title>' || public.logistics_xml(concat_ws(' · ', coalesce(b->>'product', 'Product'), nullif(b->>'klant', ''), v_som || ' stuks')) || '</title>'
54070
+ || '<rect x="46" y="96" width="148" height="160" rx="8" fill="' || v_kleur || '" stroke="' || v_fg || '" stroke-width="1"/>'
54071
+ || '<line x1="46" y1="136" x2="194" y2="136" stroke="' || v_fg || '" stroke-width="0.8" opacity="0.5"/>'
54072
+ || '<line x1="46" y1="216" x2="194" y2="216" stroke="' || v_fg || '" stroke-width="0.8" opacity="0.5"/>'
54073
+ || '<rect x="40" y="88" width="160" height="12" rx="4" fill="' || v_kleur || '" stroke="' || v_fg || '" stroke-width="1"/>';
54074
+ IF NOT p_klein THEN
54075
+ v_tekst := coalesce(b->>'product', 'Product'); IF length(v_tekst) > 22 THEN v_tekst := left(v_tekst, 21) || '…'; END IF;
54076
+ v_body := v_body || '<circle cx="120" cy="168" r="18" fill="' || v_card || '" stroke="' || v_fg || '" stroke-width="1"/>'
54077
+ || '<text x="120" y="176" font-size="20" font-weight="700" text-anchor="middle" fill="' || v_fg || '">' || public.logistics_xml(coalesce(b->>'klantletter', '')) || '</text>'
54078
+ || CASE WHEN NOT v_pakbon THEN '<text x="120" y="122" font-size="12" text-anchor="middle" fill="' || v_fg || '" stroke="' || v_card || '" stroke-width="2.5" paint-order="stroke">' || public.logistics_xml(v_tekst) || '</text>' ELSE '' END
54079
+ || '<text x="120" y="240" font-size="12" text-anchor="middle" fill="' || v_fg || '" stroke="' || v_card || '" stroke-width="2.5" paint-order="stroke">' || v_som || ' stuks</text>';
54080
+ END IF;
54081
+ v_body := v_body || '</g>';
54082
+ v_desc := v_desc || ' Eén colli met ' || v_som || ' stuks.';
54083
+ ELSE
54084
+ v_pos := 0;
54085
+ FOR b IN SELECT x FROM jsonb_array_elements(v_lagen) l, jsonb_array_elements(coalesce(l->'blokken', '[]'::jsonb)) x LOOP
54086
+ v_eigen := coalesce((b->>'eigen_order')::boolean, false) OR (b->>'klantletter') IS NOT NULL;
54087
+ v_kleur := CASE WHEN v_eigen THEN v_kl->>(coalesce((b->>'kleur_index')::int, 0) % jsonb_array_length(v_kl)) ELSE v_grijs END;
54088
+ v_f := greatest(coalesce((b->>'fusten')::int, 0), 0);
54089
+ v_body := v_body || '<g><title>' || public.logistics_xml(concat_ws(' · ', CASE WHEN v_eigen THEN b->>'product' ELSE 'Andere order' END, v_f || ' colli')) || '</title>';
54090
+ FOR k IN 0 .. v_f - 1 LOOP
54091
+ EXIT WHEN v_pos >= 48;
54092
+ v_body := v_body || '<rect x="' || (14 + (v_pos % 6) * 36) || '" y="' || (66 + (v_pos / 6) * 27) || '' || '" width="32" height="23" rx="3" fill="' || v_kleur
54093
+ || '" stroke="' || v_fg || '" stroke-width="0.6"/>'
54094
+ || CASE WHEN NOT v_eigen THEN '<rect x="' || (14 + (v_pos % 6) * 36) || '" y="' || (66 + (v_pos / 6) * 27) || '" width="32" height="23" rx="3" fill="url(#lgx-arc)"/>' ELSE '' END
54095
+ || CASE WHEN NOT p_klein THEN '<text x="' || (30 + (v_pos % 6) * 36) || '" y="' || (82 + (v_pos / 6) * 27) || '" font-size="10" text-anchor="middle" fill="' || v_fg
54096
+ || '" stroke="' || v_card || '" stroke-width="2.5" paint-order="stroke">' || (v_pos + 1) || '</text>' ELSE '' END;
54097
+ v_pos := v_pos + 1;
54098
+ END LOOP;
54099
+ v_body := v_body || '</g>';
54100
+ END LOOP;
54101
+ v_desc := v_desc || ' ' || v_pos || ' colli.';
54102
+ END IF;
54103
+
54104
+ RETURN '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 320" role="img" aria-label="' || public.logistics_xml(v_label || ', ' || v_naam) || '">'
54105
+ || '<title>' || public.logistics_xml(v_label || CASE WHEN v_naam <> '' THEN ' · ' || v_naam ELSE '' END) || '</title>'
54106
+ || '<desc>' || public.logistics_xml(btrim(v_desc)) || '</desc>'
54107
+ || '<defs><pattern id="lgx-arc" width="6" height="6" patternUnits="userSpaceOnUse" patternTransform="rotate(45)"><line x1="0" y1="0" x2="0" y2="6" stroke="' || v_mut || '" stroke-width="1.5" opacity="0.6"/></pattern></defs>'
54108
+ || '<rect x="1" y="1" width="238" height="318" rx="10" fill="' || v_card || '" stroke="' || v_bd || '"/>'
54109
+ || v_body || '</svg>';
54110
+ END $function$;
54111
+ REVOKE ALL ON FUNCTION public.logistics_kar_svg(jsonb, boolean) FROM PUBLIC, anon;
54112
+ GRANT EXECUTE ON FUNCTION public.logistics_kar_svg(jsonb, boolean) TO authenticated, service_role;
54113
+
54114
+ CREATE OR REPLACE FUNCTION public.logistics_kar_tekening(p_order_id uuid, p_opties jsonb DEFAULT '{}'::jsonb)
54115
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public'
54116
+ AS $function$
54117
+ DECLARE
54118
+ v_stand jsonb; v_opt jsonb := COALESCE(p_opties, '{}'::jsonb); v_pakbon boolean; v_alle boolean;
54119
+ v_kleuren text[] := public.logistics_klantkleuren(); v_kl jsonb := '[]'::jsonb;
54120
+ v_fb text[] := ARRAY['#E69F00','#56B4E9','#009E73','#CC79A7','#0072B2','#D55E00','#F0E442'];
54121
+ v_order record; v_fase text; v_fl jsonb; v_fl_bron boolean;
54122
+ c record; v_totaal int; v_nr int := 0; v_soort text; v_woord text; v_max int;
54123
+ v_letters jsonb; v_lagen jsonb; it record; v_cur int; v_span int; i int; j int;
54124
+ v_frac numeric; v_vrij numeric; v_aandeel numeric; v_rest int; v_f int; b jsonb; v_eigen boolean; v_letter text;
54125
+ v_status text; v_waarsch jsonb; v_gedeeld int; v_dragers jsonb := '[]'::jsonb; v_rij text := ''; v_label text;
54126
+ v_fusten int; v_fl_kar text; v_P int; v_prevP int; v_sumA numeric; v_sumF int; v_d jsonb; v_svg text; v_klein text;
54127
+ BEGIN
54128
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_kar_tekening');
54129
+ v_pakbon := COALESCE(v_opt->>'vorm', '') = 'pakbon';
54130
+ v_alle := COALESCE((v_opt->>'alle_orders')::boolean, false) AND NOT v_pakbon;
54131
+ FOR i IN 1 .. cardinality(v_kleuren) LOOP
54132
+ v_kl := v_kl || to_jsonb(CASE WHEN v_kleuren[i] ~ '^var\(--[a-z0-9-]+\)$'
54133
+ THEN left(v_kleuren[i], length(v_kleuren[i]) - 1) || ', ' || v_fb[1 + ((i - 1) % 7)] || ')'
54134
+ ELSE v_kleuren[i] END);
54135
+ END LOOP;
54136
+ v_stand := public.logistics_order_indeling_stand(p_order_id);
54137
+ SELECT o.id, o.order_number, o.status, o.order_source, o.floriday_sales_order_id, o.customer_name INTO v_order FROM public.orders o WHERE o.id = p_order_id;
54138
+ v_fase := public.logistics_status_fase(v_order.status);
54139
+ v_fl_bron := v_order.floriday_sales_order_id IS NOT NULL OR COALESCE(v_order.order_source, '') ILIKE '%floriday%';
54140
+ IF v_fl_bron THEN v_fl := public.logistics_floriday_gelijk(p_order_id); END IF;
54141
+
54142
+ SELECT count(*) INTO v_totaal FROM (
54143
+ SELECT DISTINCT c2.id FROM logistics_active.order_carts c2
54144
+ WHERE c2.order_id = p_order_id
54145
+ OR EXISTS (SELECT 1 FROM logistics_active.order_cart_items ci JOIN public.order_items oi ON oi.id = ci.order_item_id
54146
+ WHERE ci.cart_id = c2.id AND oi.order_id = p_order_id)) x;
54147
+
54148
+ FOR c IN
54149
+ SELECT DISTINCT ON (c2.cart_number, c2.id) c2.id, c2.cart_number, c2.occupancy_pct, c2.total_layers_used, c2.max_layers, c2.weight_kg,
54150
+ COALESCE(t.capacity_model, 'layered') AS soort, COALESCE(t.name, c2.carrier_type, 'Ladingdrager') AS naam,
54151
+ t.layers_per_carrier, t.max_crates_per_layer, t.max_weight_kg
54152
+ FROM logistics_active.order_carts c2
54153
+ LEFT JOIN public.load_carrier_types t ON t.id = c2.carrier_type_id
54154
+ WHERE c2.order_id = p_order_id
54155
+ OR EXISTS (SELECT 1 FROM logistics_active.order_cart_items ci JOIN public.order_items oi ON oi.id = ci.order_item_id
54156
+ WHERE ci.cart_id = c2.id AND oi.order_id = p_order_id)
54157
+ ORDER BY c2.cart_number, c2.id
54158
+ LOOP
54159
+ v_nr := v_nr + 1;
54160
+ CONTINUE WHEN (v_opt ? 'drager_id') AND (v_opt->>'drager_id') <> c.id::text;
54161
+ v_soort := CASE WHEN c.soort IN ('layered','stacked','single','none') THEN c.soort ELSE 'layered' END;
54162
+ v_woord := CASE v_soort WHEN 'layered' THEN 'Kar' WHEN 'none' THEN 'Colli' WHEN 'single' THEN 'Colli' ELSE 'Pallet' END;
54163
+ v_max := GREATEST(1, COALESCE(c.max_layers, c.layers_per_carrier, CEIL(COALESCE(c.total_layers_used, 1))::int, 1));
54164
+
54165
+ SELECT COALESCE(jsonb_object_agg(order_id::text, chr((64 + rn)::int)), '{}'::jsonb) INTO v_letters FROM (
54166
+ SELECT oi.order_id, row_number() OVER (ORDER BY (oi.order_id = p_order_id) DESC, min(o2.order_number)) AS rn
54167
+ FROM public.order_cart_items ci JOIN public.order_items oi ON oi.id = ci.order_item_id
54168
+ JOIN public.orders o2 ON o2.id = oi.order_id
54169
+ WHERE ci.cart_id = c.id GROUP BY oi.order_id) l;
54170
+ SELECT count(DISTINCT oi.order_id) FILTER (WHERE oi.order_id <> p_order_id) INTO v_gedeeld
54171
+ FROM public.order_cart_items ci JOIN public.order_items oi ON oi.id = ci.order_item_id WHERE ci.cart_id = c.id;
54172
+
54173
+ -- Lagen: deel-lagen van verschillende regels delen één laag zolang ze passen.
54174
+ v_lagen := '[]'::jsonb;
54175
+ FOR i IN 1 .. v_max LOOP
54176
+ v_lagen := v_lagen || jsonb_build_array(jsonb_build_object('index', i, 'hoogte_aandeel', round(1.0 / v_max, 4), 'blokken', '[]'::jsonb));
54177
+ END LOOP;
54178
+ v_cur := 1; v_vrij := 1.0;
54179
+ FOR it IN
54180
+ SELECT ci.id, ci.layer_index, COALESCE(ci.crates_in_layer, ci.crates, ci.boxes, 0) AS fusten, ci.lagen, ci.layer_fraction,
54181
+ COALESCE(ci.product_name, oi.product_name, 'Product') AS product, oi.order_id
54182
+ FROM public.order_cart_items ci LEFT JOIN public.order_items oi ON oi.id = ci.order_item_id
54183
+ WHERE ci.cart_id = c.id ORDER BY ci.layer_index NULLS LAST, ci.created_at, ci.id
54184
+ LOOP
54185
+ v_frac := GREATEST(COALESCE(it.lagen, it.layer_fraction, 1), 0.01);
54186
+ v_eigen := it.order_id = p_order_id;
54187
+ v_letter := v_letters->>it.order_id::text;
54188
+ IF it.layer_index IS NOT NULL THEN
54189
+ v_cur := it.layer_index; v_span := 1;
54190
+ ELSIF v_frac < 0.9999 THEN
54191
+ IF v_vrij + 0.0001 < v_frac THEN v_cur := v_cur + 1; v_vrij := 1.0; END IF;
54192
+ v_span := 1;
54193
+ ELSE
54194
+ IF v_vrij < 0.9999 THEN v_cur := v_cur + 1; v_vrij := 1.0; END IF;
54195
+ v_span := CEIL(v_frac - 0.0001)::int;
54196
+ END IF;
54197
+ v_rest := it.fusten;
54198
+ FOR j IN 1 .. v_span LOOP
54199
+ i := v_cur + j - 1;
54200
+ WHILE i > jsonb_array_length(v_lagen) LOOP
54201
+ v_lagen := v_lagen || jsonb_build_array(jsonb_build_object('index', jsonb_array_length(v_lagen) + 1, 'hoogte_aandeel', 0, 'blokken', '[]'::jsonb));
54202
+ END LOOP;
54203
+ v_aandeel := CASE WHEN it.layer_index IS NOT NULL THEN LEAST(v_frac, 1) ELSE LEAST(1, v_frac - (j - 1)) END;
54204
+ v_f := CASE WHEN j = v_span THEN v_rest ELSE ROUND(it.fusten * v_aandeel / v_frac)::int END;
54205
+ v_rest := v_rest - v_f;
54206
+ b := CASE WHEN v_eigen OR v_alle
54207
+ THEN jsonb_build_object('klantletter', v_letter, 'product', CASE WHEN v_pakbon THEN NULL ELSE it.product END,
54208
+ 'klant', CASE WHEN v_eigen THEN v_order.customer_name END,
54209
+ 'fusten', v_f, 'eigen_order', v_eigen, 'aandeel', round(v_aandeel, 4),
54210
+ 'kleur_index', GREATEST(ascii(COALESCE(v_letter, 'A')) - 65, 0))
54211
+ ELSE jsonb_build_object('klantletter', NULL, 'product', NULL, 'fusten', v_f, 'eigen_order', false, 'aandeel', round(v_aandeel, 4)) END;
54212
+ v_lagen := jsonb_set(v_lagen, ARRAY[(i - 1)::text, 'blokken'], (v_lagen->(i - 1)->'blokken') || jsonb_build_array(b));
54213
+ END LOOP;
54214
+ IF it.layer_index IS NULL THEN
54215
+ IF v_span = 1 AND v_frac < 0.9999 THEN v_vrij := v_vrij - v_frac;
54216
+ ELSIF v_frac - (v_span - 1) < 0.9999 THEN v_cur := v_cur + v_span - 1; v_vrij := 1 - (v_frac - (v_span - 1));
54217
+ ELSE v_cur := v_cur + v_span; v_vrij := 1.0; END IF;
54218
+ END IF;
54219
+ END LOOP;
54220
+ IF jsonb_array_length(v_lagen) > v_max THEN
54221
+ SELECT jsonb_agg(jsonb_set(l, '{hoogte_aandeel}', to_jsonb(round(1.0 / jsonb_array_length(v_lagen), 4))) ORDER BY n) INTO v_lagen
54222
+ FROM jsonb_array_elements(v_lagen) WITH ORDINALITY x(l, n);
54223
+ END IF;
54224
+
54225
+ -- Plekken per laag uit de belading: fusten gedeeld door het deel van de laag dat ze innemen.
54226
+ v_prevP := c.max_crates_per_layer;
54227
+ FOR i IN 0 .. jsonb_array_length(v_lagen) - 1 LOOP
54228
+ SELECT max(CASE WHEN (bb->>'aandeel')::numeric > 0 THEN round((bb->>'fusten')::numeric / (bb->>'aandeel')::numeric) END),
54229
+ coalesce(sum((bb->>'aandeel')::numeric), 0), coalesce(sum((bb->>'fusten')::int), 0)
54230
+ INTO v_P, v_sumA, v_sumF
54231
+ FROM jsonb_array_elements(v_lagen->i->'blokken') bb;
54232
+ v_P := CASE WHEN v_sumF = 0 THEN COALESCE(v_prevP, c.max_crates_per_layer, 1) ELSE GREATEST(COALESCE(v_P, v_sumF), v_sumF, 1) END;
54233
+ v_P := LEAST(GREATEST(v_P, 1), 40);
54234
+ IF v_sumF > 0 THEN v_prevP := v_P; END IF;
54235
+ v_lagen := jsonb_set(v_lagen, ARRAY[i::text, 'plekken'], to_jsonb(v_P));
54236
+ IF i + 1 > v_max AND v_sumF > 0 THEN
54237
+ v_lagen := jsonb_set(v_lagen, ARRAY[i::text, 'waarschuwing'], to_jsonb('Te hoog: deze laag past niet meer op de ' || lower(v_woord)));
54238
+ ELSIF v_sumA > 1.001 THEN
54239
+ v_lagen := jsonb_set(v_lagen, ARRAY[i::text, 'waarschuwing'], to_jsonb('Te vol: deze laag is voor ' || round(v_sumA * 100) || ' % gevuld'));
54240
+ END IF;
54241
+ END LOOP;
54242
+ -- Lege plekken op lege lagen onderaan aanvullen met de maat van de eerste gevulde laag.
54243
+ SELECT (l->>'plekken')::int INTO v_P FROM jsonb_array_elements(v_lagen) l
54244
+ WHERE jsonb_array_length(l->'blokken') > 0 ORDER BY (l->>'index')::int LIMIT 1;
54245
+ IF v_P IS NOT NULL THEN
54246
+ FOR i IN 0 .. jsonb_array_length(v_lagen) - 1 LOOP
54247
+ EXIT WHEN jsonb_array_length(v_lagen->i->'blokken') > 0;
54248
+ v_lagen := jsonb_set(v_lagen, ARRAY[i::text, 'plekken'], to_jsonb(v_P));
54249
+ END LOOP;
54250
+ END IF;
54251
+ SELECT COALESCE(sum((bb->>'fusten')::int), 0) INTO v_fusten
54252
+ FROM jsonb_array_elements(v_lagen) l, jsonb_array_elements(l->'blokken') bb;
54253
+
54254
+ v_status := CASE
54255
+ WHEN v_fase IN ('verzonden','gereed_voor_facturatie','gefactureerd','afgehandeld_floriday') THEN 'afgemeld'
54256
+ WHEN public.logistics_cart_scanned(c.id) THEN 'gecontroleerd'
54257
+ WHEN EXISTS (SELECT 1 FROM public.pick_queue q WHERE q.reference = c.id::text AND q.cancelled_at IS NULL AND q.completed_at IS NOT NULL) THEN 'gepickt'
54258
+ WHEN EXISTS (SELECT 1 FROM public.pick_queue q WHERE q.reference = c.id::text AND q.cancelled_at IS NULL AND q.printed_at IS NOT NULL) THEN 'geprint'
54259
+ WHEN EXISTS (SELECT 1 FROM public.pick_queue q WHERE q.reference = c.id::text AND q.cancelled_at IS NULL AND (q.payload->>'printbaar') = 'true') THEN 'vrijgegeven'
54260
+ ELSE 'open' END;
54261
+
54262
+ v_waarsch := '[]'::jsonb;
54263
+ IF c.max_weight_kg IS NOT NULL AND c.weight_kg IS NOT NULL AND c.weight_kg > c.max_weight_kg THEN
54264
+ v_waarsch := v_waarsch || to_jsonb(v_woord || ' is te zwaar: ' || round(c.weight_kg) || ' kg, maximaal ' || round(c.max_weight_kg) || ' kg');
54265
+ END IF;
54266
+ IF COALESCE(c.occupancy_pct, 0) > 100 THEN
54267
+ v_waarsch := v_waarsch || to_jsonb(v_woord || ' is voller dan toegestaan (' || round(c.occupancy_pct) || '%)');
54268
+ END IF;
54269
+ IF jsonb_array_length(v_lagen) > v_max THEN
54270
+ v_waarsch := v_waarsch || to_jsonb('Er zijn meer lagen gevuld dan de ' || lower(v_woord) || ' heeft');
54271
+ END IF;
54272
+
54273
+ v_fl_kar := NULL;
54274
+ IF v_fl_bron THEN
54275
+ SELECT COALESCE(e->>'uitkomst', e->>'stand') INTO v_fl_kar
54276
+ FROM jsonb_array_elements(CASE WHEN jsonb_typeof(v_fl->'karren') = 'array' THEN v_fl->'karren' ELSE '[]'::jsonb END) e
54277
+ WHERE COALESCE(e->>'cartId', e->>'kar_id', e->>'id') = c.id::text LIMIT 1;
54278
+ v_fl_kar := COALESCE(v_fl_kar, v_fl->>'uitkomst', v_fl->>'stand');
54279
+ END IF;
54280
+
54281
+ v_label := CASE WHEN v_soort IN ('none','single') THEN v_woord || ' ' || v_nr || ' van ' || v_totaal
54282
+ ELSE v_woord || ' ' || v_nr || ' van ' || v_totaal END;
54283
+ v_d := jsonb_build_object('soort', v_soort, 'label', v_label, 'naam', c.naam, 'nummer', v_nr,
54284
+ 'vulgraad_pct', CASE WHEN c.occupancy_pct IS NOT NULL THEN round(c.occupancy_pct) END,
54285
+ 'gedeeld_met', v_gedeeld, 'kleuren', v_kl, 'waarschuwingen', v_waarsch, 'lagen', v_lagen, 'pakbon', v_pakbon);
54286
+ v_svg := public.logistics_kar_svg(v_d, false);
54287
+ v_klein := public.logistics_kar_svg(v_d, true);
54288
+ v_rij := v_rij || '<svg x="' || ((v_nr - 1) * 64) || '" y="0" width="60" height="80"' || substr(v_klein, length('<svg') + 1);
54289
+
54290
+ v_dragers := v_dragers || jsonb_build_array(jsonb_build_object(
54291
+ 'id', c.id, 'nummer', v_nr, 'totaal', v_totaal, 'soort', v_soort, 'naam', c.naam,
54292
+ 'vulgraad_pct', CASE WHEN c.occupancy_pct IS NOT NULL THEN round(c.occupancy_pct) END,
54293
+ 'lagen_gebruikt', CASE WHEN c.total_layers_used IS NOT NULL THEN round(c.total_layers_used, 1) END,
54294
+ 'lagen_max', CASE WHEN v_soort IN ('layered','stacked') THEN v_max END,
54295
+ 'gewicht_kg', CASE WHEN c.weight_kg IS NOT NULL THEN round(c.weight_kg, 1) END,
54296
+ 'status', v_status, 'floriday_gelijk', v_fl_kar, 'gedeeld_met', v_gedeeld,
54297
+ 'waarschuwingen', v_waarsch, 'lagen', v_lagen, 'svg', v_svg));
54298
+ END LOOP;
54299
+
54300
+ RETURN jsonb_build_object(
54301
+ 'stand', v_stand->'stand', 'bezig_sinds', v_stand->'bezig_sinds', 'bijgewerkt_op', v_stand->'bijgewerkt_op',
54302
+ 'verwacht_klaar', v_stand->'verwacht_klaar', 'tekst', v_stand->'tekst',
54303
+ 'svg_rij', CASE WHEN jsonb_array_length(v_dragers) = 0 THEN NULL
54304
+ ELSE '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ' || GREATEST(jsonb_array_length(v_dragers) * 64, 64)
54305
+ || ' 80" role="img" aria-label="Overzicht van ' || jsonb_array_length(v_dragers)
54306
+ || CASE WHEN jsonb_array_length(v_dragers) = 1 THEN ' ladingdrager' ELSE ' ladingdragers' END
54307
+ || '"><title>Overzicht ladingdragers</title>' || v_rij || '</svg>' END,
54308
+ 'dragers', v_dragers);
54309
+ END $function$;
54310
+ REVOKE ALL ON FUNCTION public.logistics_kar_tekening(uuid, jsonb) FROM PUBLIC, anon;
54311
+ GRANT EXECUTE ON FUNCTION public.logistics_kar_tekening(uuid, jsonb) TO authenticated, service_role;
54312
+
54313
+ -- Punt 2: afleveradressen per vervoerder (Afhandeling), ook zonder GLN. Ontbreekt de kolom: lege lijst.
54314
+ CREATE OR REPLACE FUNCTION public.logistics_carrier_adressen(p_carrier_id uuid)
54315
+ RETURNS jsonb LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public'
54316
+ AS $function$
54317
+ SELECT COALESCE((SELECT CASE WHEN jsonb_typeof(to_jsonb(ca)->'delivery_location_ids') = 'array'
54318
+ THEN to_jsonb(ca)->'delivery_location_ids' ELSE '[]'::jsonb END
54319
+ FROM public.carriers ca WHERE ca.id = p_carrier_id), '[]'::jsonb)
54320
+ $function$;
54321
+ REVOKE ALL ON FUNCTION public.logistics_carrier_adressen(uuid) FROM PUBLIC, anon, authenticated;
54322
+ GRANT EXECUTE ON FUNCTION public.logistics_carrier_adressen(uuid) TO service_role;
54323
+
54324
+ DO $mig$
54325
+ DECLARE v_def text; v_nieuw text; a1 text; a2 text;
54326
+ BEGIN
54327
+ v_def := pg_get_functiondef('public.logistics_resolve_carrier_core(jsonb,boolean)'::regprocedure);
54328
+ IF position('logistics_carrier_adressen' IN v_def) > 0 THEN RETURN; END IF;
54329
+ a1 := E' IF v_cust IS NOT NULL THEN\n SELECT to_jsonb(x) INTO cu FROM public.customers x WHERE x.id = v_cust;';
54330
+ a2 := E' v_skip := public.logistics_carrier_skip_reason(r.id);\n IF v_skip IS NULL AND NOT public.logistics_carrier_picks_up_for(r.id, v_date) THEN';
54331
+ IF position(a1 IN v_def) = 0 OR position(a2 IN v_def) = 0 THEN
54332
+ RAISE EXCEPTION 'logistics_resolve_carrier_core: verwachte tekst niet gevonden; aanpassing niet toegepast';
54333
+ END IF;
54334
+ v_nieuw := replace(v_def, a1, E' -- E1-C vervolg punt 2: vervoerders met dit afleveradres in hun lijst (ook zonder GLN).\n'
54335
+ || E' IF l IS NOT NULL AND (l->>''id'') IS NOT NULL THEN\n'
54336
+ || E' FOR r IN SELECT ca.id FROM public.carriers ca\n'
54337
+ || E' WHERE public.logistics_carrier_adressen(ca.id) ? (l->>''id'')\n'
54338
+ || E' ORDER BY ca.sort_order NULLS LAST, ca.id LOOP\n'
54339
+ || E' v_skip := public.logistics_carrier_skip_reason(r.id);\n'
54340
+ || E' IF v_skip IS NULL THEN\n'
54341
+ || E' RETURN public.logistics_carrier_result(r.id, ''afleveradres'', ''Vervoerder rijdt naar dit afleveradres.'', v_skips);\n'
54342
+ || E' END IF;\n'
54343
+ || E' v_skips := v_skips || (''afleveradressen: '' || v_skip);\n'
54344
+ || E' END LOOP;\n'
54345
+ || E' END IF;\n\n' || a1);
54346
+ v_nieuw := replace(v_nieuw, a2, E' v_skip := public.logistics_carrier_skip_reason(r.id);\n'
54347
+ || E' IF v_skip IS NULL AND jsonb_array_length(public.logistics_carrier_adressen(r.id)) > 0\n'
54348
+ || E' AND NOT (public.logistics_carrier_adressen(r.id) ? coalesce(l->>''id'', '''')) THEN\n'
54349
+ || E' v_skip := (SELECT name FROM public.carriers WHERE id = r.id) || '': rijdt niet naar dit afleveradres'';\n'
54350
+ || E' END IF;\n'
54351
+ || E' IF v_skip IS NULL AND NOT public.logistics_carrier_picks_up_for(r.id, v_date) THEN');
54352
+ EXECUTE v_nieuw;
54353
+ END $mig$;
54354
+
54355
+ -- Punt 3: sluitregel van buffer en karindeling volgens de werkwijze.
54356
+ CREATE OR REPLACE FUNCTION public.logistics_buffer_sluiten_modus()
54357
+ RETURNS text LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public'
54358
+ AS $function$
54359
+ SELECT COALESCE((
54360
+ SELECT CASE WHEN v IN ('verzendmoment', 'werkuren') THEN v ELSE 'werkuren' END FROM (
54361
+ SELECT public.logistics_rule_text(rc.settings, 'order_verwerking', 'buffer_sluiten', 'werkuren') AS v
54362
+ FROM public.logistics_rule_configs rc
54363
+ LEFT JOIN public.logistics_strategies s ON s.id = rc.strategy_id
54364
+ ORDER BY (rc.is_active AND coalesce(s.is_active, false)) DESC, rc.is_active DESC, rc.updated_at DESC NULLS LAST
54365
+ LIMIT 1) x), 'werkuren')
54366
+ $function$;
54367
+ REVOKE ALL ON FUNCTION public.logistics_buffer_sluiten_modus() FROM PUBLIC, anon;
54368
+ GRANT EXECUTE ON FUNCTION public.logistics_buffer_sluiten_modus() TO authenticated, service_role;
54369
+
54370
+ DO $mig$
54371
+ DECLARE v_def text; v_nieuw text; a0 text; a1 text; a2 text; a3 text; a4 text;
54372
+ BEGIN
54373
+ v_def := pg_get_functiondef('public.logistics_schedule_tasks(text)'::regprocedure);
54374
+ IF position('tmp_logistics_verzend' IN v_def) > 0 THEN RETURN; END IF;
54375
+ a0 := E'\n WITH pick AS (';
54376
+ a1 := E' SELECT id, pickup_at, new_rule, dep,\n CASE\n WHEN new_rule AND pickup_at IS NULL THEN NULL';
54377
+ a2 := E' CASE WHEN new_rule THEN\n CASE WHEN off_min <= dur_min THEN pickup_at';
54378
+ a3 := E' FROM work\n ), clamped AS (';
54379
+ a4 := E' || CASE WHEN c.shifted THEN';
54380
+ IF position(a0 IN v_def) = 0 OR position(a1 IN v_def) = 0 OR position(a2 IN v_def) = 0
54381
+ OR position(a3 IN v_def) = 0 OR position(a4 IN v_def) = 0 THEN
54382
+ RAISE EXCEPTION 'logistics_schedule_tasks: verwachte tekst niet gevonden; aanpassing niet toegepast';
54383
+ END IF;
54384
+ v_nieuw := replace(v_def, a0, E'\n -- E1-C vervolg punt 3: buffer en karindeling sluiten op het verzendmoment van de aanmelding (werkwijze).\n'
54385
+ || E' CREATE TEMP TABLE IF NOT EXISTS tmp_logistics_verzend (task_id uuid PRIMARY KEY, verzend_at timestamptz) ON COMMIT DROP;\n'
54386
+ || E' TRUNCATE tmp_logistics_verzend;\n'
54387
+ || E' IF public.logistics_buffer_sluiten_modus() = ''verzendmoment'' AND EXISTS (SELECT 1 FROM tmp_logistics_dep) THEN\n'
54388
+ || E' BEGIN\n'
54389
+ || E' INSERT INTO tmp_logistics_verzend(task_id, verzend_at)\n'
54390
+ || E' SELECT t.id, a.aanmeld_at\n'
54391
+ || E' FROM public.logistics_aanmeldmoment(ARRAY(SELECT d.order_id FROM tmp_logistics_dep d)) a\n'
54392
+ || E' JOIN tmp_logistics_dep d ON d.order_id = a.order_id\n'
54393
+ || E' JOIN public.logistics_tasks t ON t.order_ref = d.ref AND t.task_type IN (''buffer.close'', ''load.compose'') AND t.status IN (''planned'', ''ready'')\n'
54394
+ || E' WHERE a.aanmeld_at IS NOT NULL AND a.bron IN (''gepland'', ''verstuurd'')\n'
54395
+ || E' ON CONFLICT (task_id) DO NOTHING;\n'
54396
+ || E' EXCEPTION WHEN OTHERS THEN\n'
54397
+ || E' PERFORM public.logistics_melding(''logistics_schedule_tasks'', NULL,\n'
54398
+ || E' ''Verzendmoment van de aanmelding niet te lezen; buffer en karindeling volgen nu de werkuren'', SQLERRM);\n'
54399
+ || E' END;\n'
54400
+ || E' END IF;\n' || a0);
54401
+ v_nieuw := replace(v_nieuw, a1, E' SELECT id, pickup_at, new_rule, dep, vzz.verzend_at,\n CASE\n'
54402
+ || E' WHEN vzz.verzend_at IS NOT NULL AND task_type = ''buffer.close'' THEN vzz.verzend_at\n'
54403
+ || E' WHEN vzz.verzend_at IS NOT NULL AND task_type = ''load.compose'' THEN vzz.verzend_at - interval ''10 minutes''\n'
54404
+ || E' WHEN new_rule AND pickup_at IS NULL THEN NULL');
54405
+ v_nieuw := replace(v_nieuw, a2, E' CASE WHEN vzz.verzend_at IS NOT NULL AND task_type IN (''buffer.close'', ''load.compose'') THEN vzz.verzend_at\n'
54406
+ || E' WHEN new_rule THEN\n CASE WHEN off_min <= dur_min THEN pickup_at');
54407
+ v_nieuw := replace(v_nieuw, a3, E' FROM work\n LEFT JOIN tmp_logistics_verzend vzz ON vzz.task_id = work.id\n ), clamped AS (');
54408
+ v_nieuw := replace(v_nieuw, a4, E' || CASE WHEN c.task_type IN (''buffer.close'', ''load.compose'') THEN jsonb_build_object(\n'
54409
+ || E' ''sluitRegel'', CASE WHEN c.verzend_at IS NOT NULL THEN ''verzendmoment'' ELSE ''werkuren'' END,\n'
54410
+ || E' ''verzendAt'', c.verzend_at) ELSE ''{}''::jsonb END\n' || a4);
54411
+ EXECUTE v_nieuw;
54412
+ END $mig$;
54413
+
54414
+ -- Nieuwe werkwijze Standaard krijgt "Op het verzendmoment"; bestaande werkwijzen blijven ongewijzigd.
54415
+ DO $mig$
54416
+ DECLARE v_def text; v_nieuw text;
54417
+ BEGIN
54418
+ v_def := pg_get_functiondef('public.logistics_ensure_defaults()'::regprocedure);
54419
+ IF position('buffer_sluiten' IN v_def) > 0 THEN RETURN; END IF;
54420
+ IF (length(v_def) - length(replace(v_def, '''buffer_enabled'', true,', ''))) / length('''buffer_enabled'', true,') <> 1 THEN
54421
+ RAISE EXCEPTION 'logistics_ensure_defaults: verwachte tekst niet precies één keer gevonden';
54422
+ END IF;
54423
+ v_nieuw := replace(v_def, '''buffer_enabled'', true,', '''buffer_enabled'', true, ''buffer_sluiten'', ''verzendmoment'',');
54424
+ EXECUTE v_nieuw;
54425
+ END $mig$;
54426
+
54427
+ -- Punt 4: laadtoestand alleen bij een wijziging die hem raakt.
54428
+ CREATE OR REPLACE FUNCTION public.logistics_laadtoestand_sig()
54429
+ RETURNS text LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public'
54430
+ AS $function$
54431
+ SELECT md5(concat_ws('|',
54432
+ (SELECT string_agg(q.id::text, ',' ORDER BY q.id) FROM public.pick_queue q WHERE q.cancelled_at IS NULL AND q.completed_at IS NULL),
54433
+ (SELECT string_agg(pq.order_id::text || pq.status, ',' ORDER BY pq.order_id, pq.status) FROM public.logistics_plan_queue pq WHERE pq.status IN ('pending', 'running')),
54434
+ (SELECT count(*)::text || ':' || coalesce(max(c.updated_at)::text, '') FROM logistics_active.order_carts c),
54435
+ (SELECT string_agg(s || ':' || n, ',' ORDER BY s) FROM (SELECT t.status AS s, count(*) AS n FROM public.logistics_tasks t GROUP BY t.status) x),
54436
+ (SELECT count(*)::text FROM public.logistics_tasks t WHERE t.status IN ('planned', 'ready') AND t.planned_start <= now()),
54437
+ (SELECT count(*)::text || ':' || coalesce(max(r.created_at)::text, '') FROM public.logistics_pick_data_requests r WHERE r.finished_at IS NULL)))
54438
+ $function$;
54439
+ REVOKE ALL ON FUNCTION public.logistics_laadtoestand_sig() FROM PUBLIC, anon, authenticated;
54440
+ GRANT EXECUTE ON FUNCTION public.logistics_laadtoestand_sig() TO service_role;
54441
+
54442
+ CREATE OR REPLACE FUNCTION public.logistics_refresh_plan_state_als_nodig()
54443
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
54444
+ AS $function$
54445
+ DECLARE v_sig text; v_prev text; v_at timestamptz; v_res jsonb; v_bezig boolean;
54446
+ BEGIN
54447
+ PERFORM public.logistics_acc_guard('server', 'logistics_refresh_plan_state_als_nodig');
54448
+ v_sig := public.logistics_laadtoestand_sig();
54449
+ SELECT value->>'sig', (value->>'at')::timestamptz INTO v_prev, v_at
54450
+ FROM public.logistics_tick_state WHERE key = 'refresh_plan_state';
54451
+ SELECT (value->>'cursor') IS NOT NULL INTO v_bezig FROM public.logistics_tick_state WHERE key = 'refresh_plan_state_cursor';
54452
+ -- Vangnet: hooguit elke 15 minuten toch een volledige ronde.
54453
+ IF NOT coalesce(v_bezig, false) AND v_at IS NOT NULL AND v_sig = v_prev AND v_at > now() - interval '15 minutes' THEN
54454
+ RETURN jsonb_build_object('overgeslagen', 'niets veranderd sinds de vorige ronde');
54455
+ END IF;
54456
+ v_res := public.logistics_refresh_plan_state(p_limit => 500);
54457
+ INSERT INTO public.logistics_tick_state(key, value, updated_at)
54458
+ VALUES ('refresh_plan_state', jsonb_build_object('sig', v_sig, 'at', now()), now())
54459
+ ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = now();
54460
+ RETURN v_res;
54461
+ END $function$;
54462
+
54463
+ CREATE OR REPLACE FUNCTION public.logistics_install_check_e1c()
54464
+ RETURNS TABLE(onderdeel text, ok boolean, toelichting text)
54465
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public'
54466
+ AS $function$
54467
+ DECLARE f text; v_order uuid; v jsonb; v_n int;
54468
+ BEGIN
54469
+ FOREACH f IN ARRAY ARRAY['logistics_uitgang_uit()','logistics_pickup_afwijkend(integer)','logistics_pickup_herstel_voorbeeld()',
54470
+ 'logistics_pickup_herstel_uitvoeren(integer)','logistics_stock_email_klaarzetten(integer)','logistics_pickwachtrij_overslaan()',
54471
+ 'logistics_order_indeling_stand(uuid)','logistics_kar_tekening(uuid,jsonb)','logistics_klantkleuren()','logistics_xml(text)',
54472
+ 'logistics_kar_svg(jsonb,boolean)','logistics_carrier_adressen(uuid)','logistics_buffer_sluiten_modus()','logistics_laadtoestand_sig()'] LOOP
54473
+ onderdeel := 'functie ' || f;
54474
+ ok := to_regprocedure('public.' || f) IS NOT NULL;
54475
+ toelichting := CASE WHEN ok THEN 'aanwezig' ELSE 'ontbreekt: werk de Engine bij via Supabase bijwerken' END;
54476
+ RETURN NEXT;
54477
+ END LOOP;
54478
+ IF to_regprocedure('public.logistics_pickup_afwijkend(integer)') IS NOT NULL THEN
54479
+ v_n := (public.logistics_pickup_afwijkend(0)->>'aantal')::int;
54480
+ onderdeel := 'ophaalmoment past bij vervoerder'; ok := v_n = 0;
54481
+ toelichting := CASE WHEN ok THEN 'alle open orders kloppen'
54482
+ ELSE v_n || ' open orders hebben een ophaalmoment van een andere vervoerder: draai het herstel (handmatige stap)' END;
54483
+ RETURN NEXT;
54484
+ END IF;
54485
+ SELECT c.order_id INTO v_order FROM logistics_active.order_carts c WHERE c.order_id IS NOT NULL ORDER BY c.updated_at DESC LIMIT 1;
54486
+ IF v_order IS NOT NULL AND to_regprocedure('public.logistics_kar_tekening(uuid,jsonb)') IS NOT NULL THEN
54487
+ v := public.logistics_kar_tekening(v_order, '{}'::jsonb);
54488
+ onderdeel := 'stand en kar-tekening';
54489
+ ok := (v->>'stand') IN ('bezig','klaar','niet_gelukt','geen_regels') AND jsonb_typeof(v->'dragers') = 'array';
54490
+ toelichting := CASE WHEN ok THEN 'geldige uitkomst voor een bestaande order' ELSE 'ongeldige uitkomst' END;
54491
+ RETURN NEXT;
54492
+ END IF;
54493
+ END $function$;
54494
+
54495
+ CREATE OR REPLACE FUNCTION public.logistics_schema_versie()
54496
+ RETURNS text LANGUAGE sql IMMUTABLE SET search_path TO 'public'
54497
+ AS $function$ SELECT '2026-11-10g'::text $function$;
54498
+ REVOKE EXECUTE ON FUNCTION public.logistics_buffer_sluiten_modus() FROM authenticated;
54499
+ DO $mig$
54500
+ DECLARE v_def text; a text := 'CASE WHEN v_som >= v_P THEN';
54501
+ BEGIN
54502
+ v_def := pg_get_functiondef('public.logistics_kar_svg(jsonb,boolean)'::regprocedure);
54503
+ IF position('nullif(v_laag->>''waarschuwing'', '''') IS NULL AND v_som >= v_P' IN v_def) > 0 THEN RETURN; END IF;
54504
+ IF position(a IN v_def) = 0 THEN RAISE EXCEPTION 'logistics_kar_svg: verwachte tekst niet gevonden'; END IF;
54505
+ EXECUTE replace(v_def, a, 'CASE WHEN nullif(v_laag->>''waarschuwing'', '''') IS NULL AND v_som >= v_P THEN');
54506
+ END $mig$;
54507
+
54508
+ -- ===== Vervolg E1-E (2026-11-10h)
54509
+ -- E1-E (schemaversie 2026-11-10h): eigen ophaaltijd leidend; sluitregel omzetten geldt ook voor vandaag.
54510
+ CREATE OR REPLACE FUNCTION public.logistics_pickup_eigen_tijd(p_carrier_id uuid, p_cf jsonb)
54511
+ RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
54512
+ DECLARE v_soort text; v_pm text; v_bron text; v_geldig boolean;
54513
+ BEGIN
54514
+ IF p_carrier_id IS NULL THEN RETURN jsonb_build_object('eigen', false, 'soort', NULL); END IF;
54515
+ SELECT to_jsonb(c)->>'carrier_kind' INTO v_soort FROM public.carriers c WHERE c.id = p_carrier_id;
54516
+ IF COALESCE(v_soort, '') NOT IN ('klant_haalt_op', 'eigen_vervoer') THEN
54517
+ RETURN jsonb_build_object('eigen', false, 'soort', v_soort);
54518
+ END IF;
54519
+ v_pm := nullif(btrim(COALESCE(p_cf->>'pickup_moment', '')), '');
54520
+ v_bron := COALESCE(p_cf->>'pickup_moment_bron', '');
54521
+ v_geldig := v_pm ~ '^\d{4}-\d{2}-\d{2}\|\d{1,2}:\d{2}$';
54522
+ RETURN jsonb_build_object('eigen', v_geldig AND v_bron <> 'auto', 'soort', v_soort,
54523
+ 'tijd', CASE WHEN v_geldig AND v_bron <> 'auto' THEN v_pm END);
54524
+ END $$;
54525
+ REVOKE ALL ON FUNCTION public.logistics_pickup_eigen_tijd(uuid, jsonb) FROM PUBLIC, anon, authenticated;
54526
+ GRANT EXECUTE ON FUNCTION public.logistics_pickup_eigen_tijd(uuid, jsonb) TO service_role;
54527
+
54528
+ -- Ophaalmoment-trigger: een eigen tijd blijft staan, ook bij aanmaken en vervoerderswissel.
54529
+ CREATE OR REPLACE FUNCTION public.logistics_orders_pickup_moment()
54530
+ RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$
54531
+ DECLARE n jsonb; v_basis jsonb; v_eigen jsonb;
54532
+ BEGIN
54533
+ BEGIN
54534
+ v_eigen := public.logistics_pickup_eigen_tijd(NEW.carrier_id, NEW.custom_fields);
54535
+ EXCEPTION WHEN OTHERS THEN
54536
+ v_eigen := '{"eigen":false}'::jsonb;
54537
+ PERFORM public.logistics_melding('logistics_orders_pickup_moment', NEW.id, 'Soort vervoerder kon niet worden gelezen', SQLERRM);
54538
+ END;
54539
+ -- E1-E/1: eigen tijd (klant haalt op / eigen vervoer met tijd) is leidend.
54540
+ IF COALESCE((v_eigen->>'eigen')::boolean, false) THEN
54541
+ IF COALESCE(NEW.custom_fields->>'pickup_moment_bron', '') <> 'eigen' OR (NEW.custom_fields ? 'pickup_moment_reden') THEN
54542
+ NEW.custom_fields := (NEW.custom_fields - 'pickup_moment_reden' - 'pickup_moment_key') || jsonb_build_object('pickup_moment_bron', 'eigen');
54543
+ END IF;
54544
+ RETURN NEW;
54545
+ END IF;
54546
+ IF TG_OP = 'UPDATE' AND NEW.carrier_id IS DISTINCT FROM OLD.carrier_id THEN
54547
+ IF NEW.plan_locked_at IS NOT NULL THEN RETURN NEW; END IF;
54548
+ BEGIN
54549
+ v_basis := COALESCE(NEW.custom_fields, '{}'::jsonb) - 'pickup_moment' - 'pickup_moment_key' - 'pickup_moment_bron' - 'pickup_moment_reden';
54550
+ n := public.logistics_pickup_cf_nieuw(v_basis, NEW.carrier_id, NEW.delivery_date::text,
54551
+ NEW.deleted_at, NEW.cancelled_at, NEW.plan_locked_at, NULL);
54552
+ IF n IS NOT NULL THEN NEW.custom_fields := n; END IF;
54553
+ EXCEPTION WHEN OTHERS THEN
54554
+ RAISE WARNING 'logistiek-engine: ophaalmoment na vervoerderswissel mislukt voor order %: %', NEW.id, SQLERRM;
54555
+ PERFORM public.logistics_melding('logistics_orders_pickup_moment', NEW.id,
54556
+ 'Ophaalmoment kon na een vervoerderswissel niet opnieuw worden bepaald', SQLERRM);
54557
+ END;
54558
+ RETURN NEW;
54559
+ END IF;
54560
+ IF COALESCE(current_setting('logistics.pickup_writer', true), '') = 'engine' THEN RETURN NEW; END IF;
54561
+ IF TG_OP = 'UPDATE'
54562
+ AND NEW.delivery_date IS NOT DISTINCT FROM OLD.delivery_date
54563
+ AND (NEW.custom_fields->>'floriday_latest_delivery_at') IS NOT DISTINCT FROM (OLD.custom_fields->>'floriday_latest_delivery_at')
54564
+ AND NEW.deleted_at IS NOT DISTINCT FROM OLD.deleted_at
54565
+ AND (nullif(btrim(COALESCE(NEW.custom_fields->>'pickup_moment', '')), '') IS NOT NULL
54566
+ OR (NEW.custom_fields->'pickup_moment_reden') IS NOT NULL) THEN
54567
+ RETURN NEW;
54568
+ END IF;
54569
+ BEGIN
54570
+ n := public.logistics_pickup_cf_nieuw(NEW.custom_fields, NEW.carrier_id, NEW.delivery_date::text,
54571
+ NEW.deleted_at, NEW.cancelled_at, NEW.plan_locked_at, NULL);
54572
+ IF n IS NOT NULL THEN NEW.custom_fields := n; END IF;
54573
+ EXCEPTION WHEN OTHERS THEN
54574
+ RAISE WARNING 'logistiek-engine: ophaalmoment bepalen mislukt voor order %: %', NEW.id, SQLERRM;
54575
+ BEGIN
54576
+ PERFORM public._logistics_merge_melding('ophaalmoment_fout', NEW.id, NULL, NULL, 'ophaalmoment bepalen: ' || SQLERRM);
54577
+ EXCEPTION WHEN OTHERS THEN RAISE WARNING 'logistiek-engine: melding ophaalmoment mislukt: %', SQLERRM; END;
54578
+ END;
54579
+ RETURN NEW;
54580
+ END $$;
54581
+
54582
+ -- E1-E: gerichte aanpassingen op bestaande functies (idempotent: alleen als de nieuwe tekst er nog niet in staat).
54583
+
54584
+ DO $do$
54585
+ DECLARE d text := pg_get_functiondef('public.logistics_pickup_cf_nieuw(jsonb,uuid,text,timestamptz,timestamptz,timestamptz,jsonb)'::regprocedure); a text := $e1e0a$ BEGIN v_dag := public.logistics_order_date(p_delivery_date);$e1e0a$; b text := $e1e0b$ IF (public.logistics_pickup_eigen_tijd(p_carrier_id, cf)->>'soort') = 'klant_haalt_op' THEN
54586
+ n := (cf - 'pickup_moment' - 'pickup_moment_key')
54587
+ || jsonb_build_object('pickup_moment_bron', 'eigen', 'pickup_moment_reden', 'Ophaaltijd van de klant ontbreekt; vul die in bij de order');
54588
+ IF n = cf THEN RETURN NULL; END IF;
54589
+ RETURN n;
54590
+ END IF;
54591
+ BEGIN v_dag := public.logistics_order_date(p_delivery_date);$e1e0b$;
54592
+ BEGIN
54593
+ IF strpos(d, b) > 0 THEN RETURN; END IF;
54594
+ IF strpos(d, a) = 0 THEN RAISE EXCEPTION 'E1-E: verwachte tekst niet gevonden in logistics_pickup_cf_nieuw(jsonb,uuid,text,timestamptz,timestamptz,timestamptz,jsonb)'; END IF;
54595
+ EXECUTE replace(d, a, b);
54596
+ END $do$;
54597
+
54598
+ DO $do$
54599
+ DECLARE d text := pg_get_functiondef('public.logistics_pickup_afwijkend(integer)'::regprocedure); a text := $e1e1a$ AND NOT public.logistics_order_is_backfill(o.id)$e1e1a$; b text := $e1e1b$ AND NOT public.logistics_order_is_backfill(o.id)
54600
+ AND NOT COALESCE((public.logistics_pickup_eigen_tijd(o.carrier_id, o.custom_fields)->>'eigen')::boolean, false)$e1e1b$;
54601
+ BEGIN
54602
+ IF strpos(d, b) > 0 THEN RETURN; END IF;
54603
+ IF strpos(d, a) = 0 THEN RAISE EXCEPTION 'E1-E: verwachte tekst niet gevonden in logistics_pickup_afwijkend(integer)'; END IF;
54604
+ EXECUTE replace(d, a, b);
54605
+ END $do$;
54606
+
54607
+ DO $do$
54608
+ DECLARE d text := pg_get_functiondef('public.logistics_pickup_herstel_uitvoeren(integer)'::regprocedure); a text := $e1e2a$ SELECT * INTO o FROM public.orders WHERE id = (r->>'orderId')::uuid FOR UPDATE;$e1e2a$; b text := $e1e2b$ SELECT * INTO o FROM public.orders WHERE id = (r->>'orderId')::uuid FOR UPDATE;
54609
+ CONTINUE WHEN COALESCE((public.logistics_pickup_eigen_tijd(o.carrier_id, o.custom_fields)->>'eigen')::boolean, false);$e1e2b$;
54610
+ BEGIN
54611
+ IF strpos(d, b) > 0 THEN RETURN; END IF;
54612
+ IF strpos(d, a) = 0 THEN RAISE EXCEPTION 'E1-E: verwachte tekst niet gevonden in logistics_pickup_herstel_uitvoeren(integer)'; END IF;
54613
+ EXECUTE replace(d, a, b);
54614
+ END $do$;
54615
+
54616
+ DO $do$
54617
+ DECLARE d text := pg_get_functiondef('public.logistics_install_check()'::regprocedure); a text := $e1e3a$'logistics_pickup_herbereken()'] LOOP$e1e3a$; b text := $e1e3b$'logistics_pickup_herbereken()', 'logistics_pickup_eigen_tijd(uuid,jsonb)'] LOOP$e1e3b$;
54618
+ BEGIN
54619
+ IF strpos(d, b) > 0 THEN RETURN; END IF;
54620
+ IF strpos(d, a) = 0 THEN RAISE EXCEPTION 'E1-E: verwachte tekst niet gevonden in logistics_install_check()'; END IF;
54621
+ EXECUTE replace(d, a, b);
54622
+ END $do$;
54623
+
54624
+ DO $do$
54625
+ DECLARE d text := pg_get_functiondef('public.logistics_schedule_tasks(text)'::regprocedure); a text := $e1e4a$ AND tt.planned_start >= c.raw_start AND tt.planned_start <= now()$e1e4a$; b text := $e1e4b$ AND tt.planned_start >= c.raw_start AND tt.planned_start <= now()
54626
+ AND NOT (c.task_type IN ('buffer.close', 'load.compose')
54627
+ AND COALESCE(tt.payload->>'sluitRegel', 'werkuren')
54628
+ IS DISTINCT FROM CASE WHEN c.verzend_at IS NOT NULL THEN 'verzendmoment' ELSE 'werkuren' END)$e1e4b$;
54629
+ BEGIN
54630
+ IF strpos(d, b) > 0 THEN RETURN; END IF;
54631
+ IF strpos(d, a) = 0 THEN RAISE EXCEPTION 'E1-E: verwachte tekst niet gevonden in logistics_schedule_tasks(text)'; END IF;
54632
+ EXECUTE replace(d, a, b);
54633
+ END $do$;
54634
+
54635
+ DO $do$
54636
+ DECLARE d text := pg_get_functiondef('public.logistics_schedule_tasks(text)'::regprocedure); a text := $e1e5a$ WHERE n.id = t.id
54637
+ AND (t.planned_start$e1e5a$; b text := $e1e5b$ WHERE n.id = t.id
54638
+ AND NOT (t.task_type IN ('buffer.close', 'load.compose')
54639
+ AND public._logistics_order_karren_vast((SELECT o.id FROM public.orders o WHERE o.order_number = t.order_ref AND o.deleted_at IS NULL LIMIT 1)))
54640
+ AND (t.planned_start$e1e5b$;
54641
+ BEGIN
54642
+ IF strpos(d, b) > 0 THEN RETURN; END IF;
54643
+ IF strpos(d, a) = 0 THEN RAISE EXCEPTION 'E1-E: verwachte tekst niet gevonden in logistics_schedule_tasks(text)'; END IF;
54644
+ EXECUTE replace(d, a, b);
54645
+ END $do$;
54646
+
54647
+ -- E1-E/A: eigen tijd gewijzigd in Verkoop -> taken opnieuw plannen, zolang er niets geprint of gepickt is.
54648
+ CREATE OR REPLACE FUNCTION public.logistics_eigen_tijd_herplan()
54649
+ RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$
54650
+ BEGIN
54651
+ IF NEW.order_number IS NULL OR NEW.deleted_at IS NOT NULL THEN RETURN NULL; END IF;
54652
+ IF (NEW.custom_fields->>'pickup_moment') IS NOT DISTINCT FROM (OLD.custom_fields->>'pickup_moment')
54653
+ AND NEW.carrier_id IS NOT DISTINCT FROM OLD.carrier_id THEN RETURN NULL; END IF;
54654
+ BEGIN
54655
+ IF public._logistics_order_karren_vast(NEW.id) THEN
54656
+ INSERT INTO public.logistics_trace_events(phase, step, status, reason, order_ref, actor)
54657
+ VALUES ('ophaalmoment', 'Ophaaltijd gewijzigd', 'overgeslagen',
54658
+ 'Er is al een kar geprint of gepickt; de planning blijft zoals hij was.', NEW.order_number, 'engine');
54659
+ RETURN NULL;
54660
+ END IF;
54661
+ PERFORM public.logistics_schedule_tasks(NEW.order_number);
54662
+ INSERT INTO public.logistics_trace_events(phase, step, status, reason, order_ref, actor)
54663
+ VALUES ('ophaalmoment', 'Ophaaltijd gewijzigd', 'geslaagd',
54664
+ 'Was ' || COALESCE(OLD.custom_fields->>'pickup_moment', 'leeg') || ', nu ' || COALESCE(NEW.custom_fields->>'pickup_moment', 'leeg') || '; taken opnieuw gepland.',
54665
+ NEW.order_number, 'engine');
54666
+ EXCEPTION WHEN OTHERS THEN
54667
+ RAISE WARNING 'logistiek-engine: herplannen na ophaaltijd mislukt voor order %: %', NEW.id, SQLERRM;
54668
+ PERFORM public.logistics_melding('logistics_eigen_tijd_herplan', NEW.id,
54669
+ 'Taken konden na een gewijzigde ophaaltijd niet opnieuw worden gepland; de volgende ronde probeert het opnieuw', SQLERRM);
54670
+ END;
54671
+ RETURN NULL;
54672
+ END $$;
54673
+ REVOKE ALL ON FUNCTION public.logistics_eigen_tijd_herplan() FROM PUBLIC, anon, authenticated;
54674
+ DROP TRIGGER IF EXISTS trg_logistics_eigen_tijd_herplan ON public.orders;
54675
+ CREATE TRIGGER trg_logistics_eigen_tijd_herplan AFTER UPDATE OF custom_fields, carrier_id ON public.orders
54676
+ FOR EACH ROW EXECUTE FUNCTION public.logistics_eigen_tijd_herplan();
54677
+
54678
+ -- E1-E/2+B: wissel van sluitregel in de werkwijze -> alle open taken meteen opnieuw plannen (geen merge-run).
54679
+ CREATE OR REPLACE FUNCTION public.logistics_sluitregel_gewisseld()
54680
+ RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$
54681
+ BEGIN
54682
+ IF public.logistics_rule_text(NEW.settings, 'order_verwerking', 'buffer_sluiten', 'werkuren')
54683
+ IS NOT DISTINCT FROM public.logistics_rule_text(OLD.settings, 'order_verwerking', 'buffer_sluiten', 'werkuren') THEN
54684
+ RETURN NULL;
54685
+ END IF;
54686
+ BEGIN
54687
+ PERFORM public.logistics_schedule_tasks(NULL);
54688
+ EXCEPTION WHEN OTHERS THEN
54689
+ RAISE WARNING 'logistiek-engine: herplannen na wissel sluitregel mislukt: %', SQLERRM;
54690
+ PERFORM public.logistics_melding('logistics_sluitregel_gewisseld', NULL,
54691
+ 'Na het wisselen van de sluitregel konden de taken niet meteen opnieuw worden gepland; de volgende ronde doet het alsnog', SQLERRM);
54692
+ END;
54693
+ RETURN NULL;
54694
+ END $$;
54695
+ REVOKE ALL ON FUNCTION public.logistics_sluitregel_gewisseld() FROM PUBLIC, anon, authenticated;
54696
+ DROP TRIGGER IF EXISTS trg_logistics_sluitregel_gewisseld ON public.logistics_rule_configs;
54697
+ CREATE TRIGGER trg_logistics_sluitregel_gewisseld AFTER UPDATE OF settings ON public.logistics_rule_configs
54698
+ FOR EACH ROW EXECUTE FUNCTION public.logistics_sluitregel_gewisseld();
54699
+
54700
+ CREATE OR REPLACE FUNCTION public.logistics_schema_versie()
54701
+ RETURNS text LANGUAGE sql IMMUTABLE SET search_path TO 'public'
54702
+ AS $function$ SELECT '2026-11-10h'::text $function$;
54703
+
54704
+ -- ===== Correctie E1-E (2026-11-10i)
54705
+ -- E1-E correctie (2026-11-10i): een aanmelding telt alleen als "verstuurd" bij de huidige vervoerder en zonder onjuist-markering.
54706
+ CREATE OR REPLACE FUNCTION public.logistics_aanmelding_geldig_at(p_order_id uuid, p_carrier_id uuid)
54707
+ RETURNS timestamptz LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
54708
+ DECLARE v_ok boolean; v_sent timestamptz; v_onj boolean;
54709
+ BEGIN
54710
+ IF p_order_id IS NULL OR p_carrier_id IS NULL OR to_regclass('public.carrier_signup_dispatches') IS NULL THEN RETURN NULL; END IF;
54711
+ IF to_regprocedure('public.afh_order_aangemeld_bij(uuid,uuid)') IS NOT NULL THEN
54712
+ EXECUTE 'SELECT public.afh_order_aangemeld_bij($1, $2)' INTO v_ok USING p_order_id, p_carrier_id;
54713
+ IF NOT COALESCE(v_ok, false) THEN RETURN NULL; END IF;
54714
+ END IF;
54715
+ v_onj := EXISTS (SELECT 1 FROM information_schema.columns
54716
+ WHERE table_schema = 'public' AND table_name = 'carrier_signup_dispatches' AND column_name = 'onjuist_at');
54717
+ EXECUTE format('SELECT min(d.sent_at) FROM public.carrier_signup_dispatches d
54718
+ WHERE d.carrier_id = $2 AND d.sent_at IS NOT NULL
54719
+ AND $1 = ANY(COALESCE(d.order_ids, ''{}''::uuid[])) %s',
54720
+ CASE WHEN v_onj THEN 'AND d.onjuist_at IS NULL' ELSE '' END)
54721
+ INTO v_sent USING p_order_id, p_carrier_id;
54722
+ RETURN v_sent;
54723
+ END $$;
54724
+ REVOKE ALL ON FUNCTION public.logistics_aanmelding_geldig_at(uuid, uuid) FROM PUBLIC, anon, authenticated;
54725
+ GRANT EXECUTE ON FUNCTION public.logistics_aanmelding_geldig_at(uuid, uuid) TO service_role;
54726
+
54727
+ -- Gepland verzendmoment volgens de aanmeldgroepen van Afhandeling, bij voorkeur bij de huidige vervoerder.
54728
+ CREATE OR REPLACE FUNCTION public.logistics_aanmelding_gepland_at(p_order_id uuid, p_carrier_id uuid)
54729
+ RETURNS timestamptz LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$
54730
+ DECLARE v_cfg jsonb; v_lead int; v_dag date; v_groups jsonb; v_at timestamptz;
54731
+ BEGIN
54732
+ IF p_order_id IS NULL OR to_regprocedure('public.afh_signup_config()') IS NULL
54733
+ OR to_regprocedure('public.afh_signup_groups(date,date,text)') IS NULL THEN RETURN NULL; END IF;
54734
+ EXECUTE 'SELECT public.afh_signup_config()' INTO v_cfg;
54735
+ v_lead := nullif(v_cfg->>'lead_minutes', '')::int;
54736
+ SELECT public.logistics_order_date(o.delivery_date::text) INTO v_dag FROM public.orders o WHERE o.id = p_order_id;
54737
+ IF v_lead IS NULL OR v_dag IS NULL OR nullif(v_cfg->>'timezone', '') IS NULL THEN RETURN NULL; END IF;
54738
+ EXECUTE 'SELECT public.afh_signup_groups($1, $2, $3)->''groups''' INTO v_groups USING v_dag - 1, v_dag + 1, v_cfg->>'timezone';
54739
+ SELECT (g->>'signup_deadline')::timestamptz - make_interval(mins => v_lead) INTO v_at
54740
+ FROM jsonb_array_elements(COALESCE(v_groups, '[]'::jsonb)) g
54741
+ WHERE EXISTS (SELECT 1 FROM jsonb_array_elements(COALESCE(g->'orders', '[]'::jsonb)) e WHERE (e->>'orderId')::uuid = p_order_id)
54742
+ ORDER BY ((g->>'carrier_id')::uuid IS NOT DISTINCT FROM p_carrier_id) DESC, (g->>'signup_deadline')::timestamptz
54743
+ LIMIT 1;
54744
+ RETURN v_at;
54745
+ END $$;
54746
+ REVOKE ALL ON FUNCTION public.logistics_aanmelding_gepland_at(uuid, uuid) FROM PUBLIC, anon, authenticated;
54747
+ GRANT EXECUTE ON FUNCTION public.logistics_aanmelding_gepland_at(uuid, uuid) TO service_role;
54748
+
54749
+ CREATE OR REPLACE FUNCTION public.logistics_aanmeldmoment(p_order_ids uuid[])
54750
+ RETURNS TABLE(order_id uuid, aanmeld_at timestamp with time zone, bron text, tekst text)
54751
+ LANGUAGE plpgsql
54752
+ STABLE SECURITY DEFINER
54753
+ SET search_path TO 'public'
54754
+ AS $function$
54755
+ DECLARE v_tz text; r record; v_fout text; v_cid uuid; v_at timestamptz; v_bron text; v_sent timestamptz;
54756
+ BEGIN
54757
+ PERFORM public.logistics_acc_guard('ingelogd', 'logistics_aanmeldmoment');
54758
+ BEGIN v_tz := public.logistics_opening_timezone(); EXCEPTION WHEN others THEN v_tz := NULL; END;
54759
+ IF to_regprocedure('public.afh_signup_moment_for_orders(uuid[])') IS NOT NULL THEN
54760
+ BEGIN
54761
+ FOR r IN EXECUTE 'SELECT order_id, aanmeld_at, bron FROM public.afh_signup_moment_for_orders(p_order_ids => $1)'
54762
+ USING p_order_ids
54763
+ LOOP
54764
+ v_at := r.aanmeld_at; v_bron := r.bron;
54765
+ -- E1-E correctie: "verstuurd" alleen bij een geldige aanmelding voor de huidige vervoerder.
54766
+ IF v_bron = 'verstuurd' THEN
54767
+ SELECT o.carrier_id INTO v_cid FROM public.orders o WHERE o.id = r.order_id;
54768
+ v_sent := public.logistics_aanmelding_geldig_at(r.order_id, v_cid);
54769
+ IF v_sent IS NOT NULL THEN
54770
+ v_at := v_sent;
54771
+ ELSE
54772
+ v_at := public.logistics_aanmelding_gepland_at(r.order_id, v_cid);
54773
+ v_bron := CASE WHEN v_at IS NOT NULL THEN 'gepland' ELSE 'onbekend' END;
54774
+ END IF;
54775
+ END IF;
54776
+ order_id := r.order_id; aanmeld_at := v_at; bron := v_bron;
54777
+ tekst := CASE WHEN v_at IS NOT NULL AND v_tz IS NOT NULL AND v_bron IN ('verstuurd', 'gepland')
54778
+ THEN 'Verzendbrief komt vrij na de transportaanmelding om ' || to_char(v_at AT TIME ZONE v_tz, 'HH24:MI')
54779
+ ELSE 'Tijdstip van de transportaanmelding is nog niet bekend' END;
54780
+ RETURN NEXT;
54781
+ END LOOP;
54782
+ RETURN;
54783
+ EXCEPTION WHEN insufficient_privilege THEN v_fout := '42501';
54784
+ WHEN others THEN v_fout := SQLSTATE;
54785
+ END;
54786
+ END IF;
54787
+ FOR r IN SELECT unnest(coalesce(p_order_ids, '{}')) AS id LOOP
54788
+ order_id := r.id; aanmeld_at := NULL; bron := coalesce('fout_' || v_fout, 'onbekend');
54789
+ tekst := 'Tijdstip van de transportaanmelding is nog niet bekend';
54790
+ RETURN NEXT;
54791
+ END LOOP;
54792
+ END $function$;
54793
+
54794
+ DO $do$
54795
+ DECLARE d text := pg_get_functiondef('public.logistics_install_check()'::regprocedure);
54796
+ a text := $a$'logistics_pickup_eigen_tijd(uuid,jsonb)'] LOOP$a$;
54797
+ b text := $b$'logistics_pickup_eigen_tijd(uuid,jsonb)', 'logistics_aanmelding_geldig_at(uuid,uuid)', 'logistics_aanmelding_gepland_at(uuid,uuid)'] LOOP$b$;
54798
+ BEGIN
54799
+ IF strpos(d, b) > 0 THEN RETURN; END IF;
54800
+ IF strpos(d, a) = 0 THEN RAISE EXCEPTION 'E1-E correctie: verwachte tekst niet gevonden in logistics_install_check'; END IF;
54801
+ EXECUTE replace(d, a, b);
54802
+ END $do$;
54803
+
54804
+ CREATE OR REPLACE FUNCTION public.logistics_schema_versie()
54805
+ RETURNS text LANGUAGE sql IMMUTABLE SET search_path TO 'public'
54806
+ AS $function$ SELECT '2026-11-10i'::text $function$;