--liquibase formatted sql -- PANELS · core · historial de alta, baja y reingreso --changeset panel:core-048a-table endDelimiter:; splitStatements:true ALTER TABLE core.workers ADD COLUMN IF NOT EXISTS imss_daily_wage NUMERIC(12,2); CREATE TABLE IF NOT EXISTS core.worker_movements ( id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, tenant_id INTEGER, worker_id BIGINT NOT NULL REFERENCES core.workers(id) ON DELETE CASCADE, kind TEXT NOT NULL CHECK (kind IN ('alta', 'baja', 'reingreso', 'alta_imss', 'baja_imss', 'modificacion_salario')), movement_date DATE NOT NULL, note TEXT NOT NULL DEFAULT '', actor_name TEXT NOT NULL DEFAULT '', created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_core_worker_movements_worker ON core.worker_movements (worker_id, movement_date, id); GRANT SELECT, INSERT, UPDATE, DELETE ON core.worker_movements TO panels_core_app; GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA core TO panels_core_app; --changeset panel:core-048b-backfill endDelimiter:; splitStatements:true INSERT INTO core.worker_movements (tenant_id, worker_id, kind, movement_date, note) SELECT w.tenant_id, w.id, 'alta', COALESCE(w.created_at::date, CURRENT_DATE), 'Alta de padrón' FROM core.workers w WHERE NOT EXISTS ( SELECT 1 FROM core.worker_movements m WHERE m.worker_id = w.id AND m.kind = 'alta' ); INSERT INTO core.worker_movements (tenant_id, worker_id, kind, movement_date, note) SELECT w.tenant_id, w.id, 'reingreso', w.last_rehire_at, 'Reingreso' FROM core.workers w WHERE w.last_rehire_at IS NOT NULL AND NOT EXISTS ( SELECT 1 FROM core.worker_movements m WHERE m.worker_id = w.id AND m.kind = 'reingreso' AND m.movement_date = w.last_rehire_at ); INSERT INTO core.worker_movements (tenant_id, worker_id, kind, movement_date, note) SELECT w.tenant_id, w.id, 'baja', COALESCE(w.updated_at::date, CURRENT_DATE), 'Baja de padrón' FROM core.workers w WHERE w.status = 'baja' AND NOT EXISTS (SELECT 1 FROM core.worker_movements m WHERE m.worker_id = w.id AND m.kind = 'baja'); --changeset panel:core-048c-trigger splitStatements:false CREATE OR REPLACE FUNCTION core.trg_worker_movement() RETURNS trigger LANGUAGE plpgsql SET search_path = core AS $$ BEGIN IF TG_OP = 'INSERT' THEN INSERT INTO worker_movements (tenant_id, worker_id, kind, movement_date, note) VALUES (NEW.tenant_id, NEW.id, 'alta', COALESCE(NEW.created_at::date, CURRENT_DATE), 'Alta de padrón'); RETURN NEW; END IF; IF OLD.status IS DISTINCT FROM 'baja' AND NEW.status = 'baja' THEN INSERT INTO worker_movements (tenant_id, worker_id, kind, movement_date, note) VALUES (NEW.tenant_id, NEW.id, 'baja', CURRENT_DATE, 'Baja de padrón'); ELSIF OLD.status = 'baja' AND NEW.status = 'activo' THEN INSERT INTO worker_movements (tenant_id, worker_id, kind, movement_date, note) VALUES (NEW.tenant_id, NEW.id, 'reingreso', COALESCE(NEW.last_rehire_at, CURRENT_DATE), 'Reingreso'); END IF; IF OLD.imss_status IS DISTINCT FROM 'alta' AND NEW.imss_status = 'alta' THEN INSERT INTO worker_movements (tenant_id, worker_id, kind, movement_date, note) VALUES (NEW.tenant_id, NEW.id, 'alta_imss', COALESCE(NEW.imss_alta_at, CURRENT_DATE), 'Alta IMSS'); ELSIF OLD.imss_status IS DISTINCT FROM 'baja_imss' AND NEW.imss_status = 'baja_imss' THEN INSERT INTO worker_movements (tenant_id, worker_id, kind, movement_date, note) VALUES (NEW.tenant_id, NEW.id, 'baja_imss', COALESCE(NEW.imss_baja_at, CURRENT_DATE), 'Baja IMSS'); END IF; IF NEW.imss_daily_wage IS NOT NULL AND NEW.imss_daily_wage IS DISTINCT FROM OLD.imss_daily_wage THEN INSERT INTO worker_movements (tenant_id, worker_id, kind, movement_date, note) VALUES (NEW.tenant_id, NEW.id, 'modificacion_salario', CURRENT_DATE, format('Jornal IMSS %s', NEW.imss_daily_wage)); END IF; RETURN NEW; END; $$; DROP TRIGGER IF EXISTS trg_worker_movement ON core.workers; CREATE TRIGGER trg_worker_movement AFTER INSERT OR UPDATE OF status, imss_status, imss_daily_wage ON core.workers FOR EACH ROW EXECUTE FUNCTION core.trg_worker_movement(); --changeset panel:core-048d-fn-list splitStatements:false CREATE OR REPLACE FUNCTION core.fn_worker_movement_list(payload jsonb) RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path = core AS $$ DECLARE v_worker_id bigint := NULLIF(payload->>'worker_id', '')::bigint; v_rows jsonb; BEGIN SELECT COALESCE(jsonb_agg(to_jsonb(m) ORDER BY m.movement_date, m.id), '[]'::jsonb) INTO v_rows FROM worker_movements m WHERE m.worker_id = v_worker_id; RETURN core.rpc_ok(jsonb_build_object('movements', v_rows), 'Historial de movimientos', jsonb_build_object('fn', 'fn_worker_movement_list', 'worker_id', v_worker_id)); EXCEPTION WHEN OTHERS THEN RETURN core.rpc_from_exception('fn_worker_movement_list', SQLSTATE, SQLERRM); END; $$; --changeset panel:core-048e-grants endDelimiter:; splitStatements:true GRANT EXECUTE ON FUNCTION core.trg_worker_movement() TO panels_core_app; GRANT EXECUTE ON FUNCTION core.fn_worker_movement_list(jsonb) TO panels_core_app;