mirror of
https://origin.cursor.com/mrdevmx/panels.git
synced 2026-10-09 12:43:18 +00:00
Add budget and badge RPC functions (changesets 011-012)
Implement core.fn_budget_* for list, chapter/item CRUD, and atomic replace import; core.fn_badge_job_* for create, list, get, and delivery tracking. All functions use SECURITY INVOKER, rpc envelope helpers, Spanish messages, and GRANT EXECUTE to panels_core_app. Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
This commit is contained in:
parent
a73090bd11
commit
7f2e84e47c
2 changed files with 1030 additions and 0 deletions
697
db/core/changesets/011-rpc-budget.sql
Normal file
697
db/core/changesets/011-rpc-budget.sql
Normal file
|
|
@ -0,0 +1,697 @@
|
|||
--liquibase formatted sql
|
||||
-- PANELS · core · RPC presupuesto (capítulos, partidas, importación atómica)
|
||||
|
||||
--changeset panel:core-011a-fn-budget-list endDelimiter:; splitStatements:true
|
||||
CREATE OR REPLACE FUNCTION core.fn_budget_list(payload jsonb)
|
||||
RETURNS jsonb
|
||||
LANGUAGE plpgsql
|
||||
SECURITY INVOKER
|
||||
SET search_path = core
|
||||
AS $$
|
||||
DECLARE
|
||||
v_project_id bigint := NULLIF(payload->>'project_id', '')::bigint;
|
||||
v_chapters jsonb;
|
||||
v_items jsonb;
|
||||
v_subtotal numeric(14,2);
|
||||
v_iva numeric(14,2);
|
||||
v_item_count integer;
|
||||
BEGIN
|
||||
IF v_project_id IS NULL THEN
|
||||
RETURN core.rpc_err(
|
||||
'VALIDATION',
|
||||
'fn_budget_list: project_id es obligatorio para consultar el presupuesto',
|
||||
jsonb_build_object('fn', 'fn_budget_list', 'field', 'project_id')
|
||||
);
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM projects WHERE id = v_project_id) THEN
|
||||
RETURN core.rpc_err(
|
||||
'NOT_FOUND',
|
||||
format('fn_budget_list: proyecto id=%s no encontrado', v_project_id),
|
||||
jsonb_build_object('fn', 'fn_budget_list', 'project_id', v_project_id)
|
||||
);
|
||||
END IF;
|
||||
|
||||
SELECT COALESCE(jsonb_agg(to_jsonb(c) ORDER BY c.sort_order, c.id), '[]'::jsonb)
|
||||
INTO v_chapters
|
||||
FROM (
|
||||
SELECT
|
||||
bc.id,
|
||||
bc.project_id,
|
||||
bc.parent_id,
|
||||
bc.code,
|
||||
bc.name,
|
||||
COALESCE(bc.wbs, '') AS wbs,
|
||||
bc.sort_order
|
||||
FROM budget_chapters bc
|
||||
WHERE bc.project_id = v_project_id
|
||||
) c;
|
||||
|
||||
SELECT
|
||||
COALESCE(jsonb_agg(to_jsonb(i) ORDER BY COALESCE(i.ch_sort, 9999), i.sort_order, i.id), '[]'::jsonb),
|
||||
COALESCE(SUM(i.amount), 0),
|
||||
COUNT(*)::integer
|
||||
INTO v_items, v_subtotal, v_item_count
|
||||
FROM (
|
||||
SELECT
|
||||
bi.*,
|
||||
ch.name AS chapter_name,
|
||||
ch.code AS chapter_code,
|
||||
ch.sort_order AS ch_sort
|
||||
FROM budget_items bi
|
||||
LEFT JOIN budget_chapters ch ON ch.id = bi.chapter_id
|
||||
WHERE bi.project_id = v_project_id
|
||||
) i;
|
||||
|
||||
v_subtotal := round(COALESCE(v_subtotal, 0), 2);
|
||||
v_iva := round(v_subtotal * 0.16, 2);
|
||||
|
||||
RETURN core.rpc_ok(
|
||||
jsonb_build_object(
|
||||
'chapters', v_chapters,
|
||||
'items', v_items,
|
||||
'totals', jsonb_build_object(
|
||||
'subtotal', v_subtotal,
|
||||
'iva', v_iva,
|
||||
'total', round(v_subtotal + v_iva, 2),
|
||||
'item_count', v_item_count
|
||||
)
|
||||
),
|
||||
format('Presupuesto del proyecto %s: %s capítulo(s), %s partida(s), subtotal %s',
|
||||
v_project_id, jsonb_array_length(v_chapters), v_item_count, v_subtotal),
|
||||
jsonb_build_object(
|
||||
'fn', 'fn_budget_list',
|
||||
'project_id', v_project_id,
|
||||
'chapter_count', jsonb_array_length(v_chapters),
|
||||
'item_count', v_item_count
|
||||
)
|
||||
);
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
RETURN core.rpc_from_exception('fn_budget_list', SQLSTATE, SQLERRM);
|
||||
END;
|
||||
$$;
|
||||
|
||||
--changeset panel:core-011b-fn-budget-chapter-create endDelimiter:; splitStatements:true
|
||||
CREATE OR REPLACE FUNCTION core.fn_budget_chapter_create(payload jsonb)
|
||||
RETURNS jsonb
|
||||
LANGUAGE plpgsql
|
||||
SECURITY INVOKER
|
||||
SET search_path = core
|
||||
AS $$
|
||||
DECLARE
|
||||
v_project_id bigint := NULLIF(payload->>'project_id', '')::bigint;
|
||||
v_parent_id bigint := NULLIF(payload->>'parent_id', '')::bigint;
|
||||
v_name text := nullif(btrim(payload->>'name'), '');
|
||||
v_code text := coalesce(nullif(btrim(payload->>'code'), ''), '');
|
||||
v_wbs text := coalesce(nullif(btrim(payload->>'wbs'), ''), '');
|
||||
v_sort integer;
|
||||
v_row budget_chapters%ROWTYPE;
|
||||
BEGIN
|
||||
IF v_project_id IS NULL THEN
|
||||
RETURN core.rpc_err(
|
||||
'VALIDATION',
|
||||
'fn_budget_chapter_create: project_id es obligatorio',
|
||||
jsonb_build_object('fn', 'fn_budget_chapter_create', 'field', 'project_id')
|
||||
);
|
||||
END IF;
|
||||
IF v_name IS NULL THEN
|
||||
RETURN core.rpc_err(
|
||||
'VALIDATION',
|
||||
'fn_budget_chapter_create: nombre de capítulo obligatorio',
|
||||
jsonb_build_object('fn', 'fn_budget_chapter_create', 'field', 'name', 'project_id', v_project_id)
|
||||
);
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM projects WHERE id = v_project_id) THEN
|
||||
RETURN core.rpc_err(
|
||||
'NOT_FOUND',
|
||||
format('fn_budget_chapter_create: proyecto id=%s no encontrado', v_project_id),
|
||||
jsonb_build_object('fn', 'fn_budget_chapter_create', 'project_id', v_project_id)
|
||||
);
|
||||
END IF;
|
||||
IF v_parent_id IS NOT NULL AND NOT EXISTS (
|
||||
SELECT 1 FROM budget_chapters WHERE id = v_parent_id AND project_id = v_project_id
|
||||
) THEN
|
||||
RETURN core.rpc_err(
|
||||
'VALIDATION',
|
||||
format('fn_budget_chapter_create: capítulo padre id=%s no pertenece al proyecto %s', v_parent_id, v_project_id),
|
||||
jsonb_build_object('fn', 'fn_budget_chapter_create', 'project_id', v_project_id, 'parent_id', v_parent_id)
|
||||
);
|
||||
END IF;
|
||||
|
||||
SELECT COALESCE(MAX(sort_order), 0) + 1
|
||||
INTO v_sort
|
||||
FROM budget_chapters
|
||||
WHERE project_id = v_project_id;
|
||||
|
||||
INSERT INTO budget_chapters (project_id, parent_id, code, name, wbs, sort_order)
|
||||
VALUES (v_project_id, v_parent_id, v_code, v_name, v_wbs, v_sort)
|
||||
RETURNING * INTO v_row;
|
||||
|
||||
RETURN core.rpc_created(
|
||||
jsonb_build_object('chapter', to_jsonb(v_row)),
|
||||
format('Capítulo "%s" creado en proyecto %s (id=%s)', v_row.name, v_project_id, v_row.id),
|
||||
jsonb_build_object('fn', 'fn_budget_chapter_create', 'project_id', v_project_id, 'id', v_row.id)
|
||||
);
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
RETURN core.rpc_from_exception('fn_budget_chapter_create', SQLSTATE, SQLERRM);
|
||||
END;
|
||||
$$;
|
||||
|
||||
--changeset panel:core-011c-fn-budget-chapter-update endDelimiter:; splitStatements:true
|
||||
CREATE OR REPLACE FUNCTION core.fn_budget_chapter_update(payload jsonb)
|
||||
RETURNS jsonb
|
||||
LANGUAGE plpgsql
|
||||
SECURITY INVOKER
|
||||
SET search_path = core
|
||||
AS $$
|
||||
DECLARE
|
||||
v_project_id bigint := NULLIF(payload->>'project_id', '')::bigint;
|
||||
v_id bigint := NULLIF(payload->>'id', '')::bigint;
|
||||
v_cur budget_chapters%ROWTYPE;
|
||||
v_name text;
|
||||
v_code text;
|
||||
v_wbs text;
|
||||
v_parent_id bigint;
|
||||
BEGIN
|
||||
IF v_project_id IS NULL THEN
|
||||
RETURN core.rpc_err(
|
||||
'VALIDATION',
|
||||
'fn_budget_chapter_update: project_id es obligatorio',
|
||||
jsonb_build_object('fn', 'fn_budget_chapter_update', 'field', 'project_id')
|
||||
);
|
||||
END IF;
|
||||
IF v_id IS NULL THEN
|
||||
RETURN core.rpc_err(
|
||||
'VALIDATION',
|
||||
'fn_budget_chapter_update: id del capítulo es obligatorio',
|
||||
jsonb_build_object('fn', 'fn_budget_chapter_update', 'field', 'id', 'project_id', v_project_id)
|
||||
);
|
||||
END IF;
|
||||
|
||||
SELECT * INTO v_cur FROM budget_chapters WHERE id = v_id AND project_id = v_project_id;
|
||||
IF NOT FOUND THEN
|
||||
RETURN core.rpc_err(
|
||||
'NOT_FOUND',
|
||||
format('fn_budget_chapter_update: capítulo id=%s no encontrado en proyecto %s', v_id, v_project_id),
|
||||
jsonb_build_object('fn', 'fn_budget_chapter_update', 'project_id', v_project_id, 'id', v_id)
|
||||
);
|
||||
END IF;
|
||||
|
||||
v_name := coalesce(nullif(btrim(payload->>'name'), ''), v_cur.name);
|
||||
v_code := CASE WHEN payload ? 'code' THEN coalesce(nullif(btrim(payload->>'code'), ''), '') ELSE v_cur.code END;
|
||||
v_wbs := CASE WHEN payload ? 'wbs' THEN coalesce(nullif(btrim(payload->>'wbs'), ''), '') ELSE v_cur.wbs END;
|
||||
v_parent_id := CASE
|
||||
WHEN payload ? 'parent_id' THEN NULLIF(payload->>'parent_id', '')::bigint
|
||||
ELSE v_cur.parent_id
|
||||
END;
|
||||
|
||||
IF v_parent_id IS NOT NULL AND v_parent_id = v_id THEN
|
||||
RETURN core.rpc_err(
|
||||
'VALIDATION',
|
||||
format('fn_budget_chapter_update: un capítulo no puede ser padre de sí mismo (id=%s)', v_id),
|
||||
jsonb_build_object('fn', 'fn_budget_chapter_update', 'project_id', v_project_id, 'id', v_id)
|
||||
);
|
||||
END IF;
|
||||
IF v_parent_id IS NOT NULL AND NOT EXISTS (
|
||||
SELECT 1 FROM budget_chapters WHERE id = v_parent_id AND project_id = v_project_id
|
||||
) THEN
|
||||
RETURN core.rpc_err(
|
||||
'VALIDATION',
|
||||
format('fn_budget_chapter_update: capítulo padre id=%s no pertenece al proyecto %s', v_parent_id, v_project_id),
|
||||
jsonb_build_object('fn', 'fn_budget_chapter_update', 'project_id', v_project_id, 'parent_id', v_parent_id)
|
||||
);
|
||||
END IF;
|
||||
|
||||
UPDATE budget_chapters
|
||||
SET name = v_name,
|
||||
code = v_code,
|
||||
wbs = v_wbs,
|
||||
parent_id = v_parent_id
|
||||
WHERE id = v_id;
|
||||
|
||||
RETURN core.rpc_ok(
|
||||
jsonb_build_object('id', v_id, 'name', v_name, 'code', v_code),
|
||||
format('Capítulo id=%s actualizado en proyecto %s', v_id, v_project_id),
|
||||
jsonb_build_object('fn', 'fn_budget_chapter_update', 'project_id', v_project_id, 'id', v_id)
|
||||
);
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
RETURN core.rpc_from_exception('fn_budget_chapter_update', SQLSTATE, SQLERRM);
|
||||
END;
|
||||
$$;
|
||||
|
||||
--changeset panel:core-011d-fn-budget-chapter-delete endDelimiter:; splitStatements:true
|
||||
CREATE OR REPLACE FUNCTION core.fn_budget_chapter_delete(payload jsonb)
|
||||
RETURNS jsonb
|
||||
LANGUAGE plpgsql
|
||||
SECURITY INVOKER
|
||||
SET search_path = core
|
||||
AS $$
|
||||
DECLARE
|
||||
v_project_id bigint := NULLIF(payload->>'project_id', '')::bigint;
|
||||
v_id bigint := NULLIF(payload->>'id', '')::bigint;
|
||||
v_name text;
|
||||
v_orphans integer;
|
||||
BEGIN
|
||||
IF v_project_id IS NULL THEN
|
||||
RETURN core.rpc_err(
|
||||
'VALIDATION',
|
||||
'fn_budget_chapter_delete: project_id es obligatorio',
|
||||
jsonb_build_object('fn', 'fn_budget_chapter_delete', 'field', 'project_id')
|
||||
);
|
||||
END IF;
|
||||
IF v_id IS NULL THEN
|
||||
RETURN core.rpc_err(
|
||||
'VALIDATION',
|
||||
'fn_budget_chapter_delete: id del capítulo es obligatorio',
|
||||
jsonb_build_object('fn', 'fn_budget_chapter_delete', 'field', 'id', 'project_id', v_project_id)
|
||||
);
|
||||
END IF;
|
||||
|
||||
SELECT name INTO v_name FROM budget_chapters WHERE id = v_id AND project_id = v_project_id;
|
||||
IF NOT FOUND THEN
|
||||
RETURN core.rpc_err(
|
||||
'NOT_FOUND',
|
||||
format('fn_budget_chapter_delete: capítulo id=%s no encontrado en proyecto %s', v_id, v_project_id),
|
||||
jsonb_build_object('fn', 'fn_budget_chapter_delete', 'project_id', v_project_id, 'id', v_id)
|
||||
);
|
||||
END IF;
|
||||
|
||||
UPDATE budget_items
|
||||
SET chapter_id = NULL
|
||||
WHERE project_id = v_project_id
|
||||
AND chapter_id = v_id;
|
||||
GET DIAGNOSTICS v_orphans = ROW_COUNT;
|
||||
|
||||
DELETE FROM budget_chapters WHERE id = v_id AND project_id = v_project_id;
|
||||
|
||||
RETURN core.rpc_ok(
|
||||
jsonb_build_object('id', v_id, 'orphaned_items', v_orphans),
|
||||
format('Capítulo "%s" (id=%s) eliminado del proyecto %s; %s partida(s) quedaron sin capítulo',
|
||||
v_name, v_id, v_project_id, v_orphans),
|
||||
jsonb_build_object('fn', 'fn_budget_chapter_delete', 'project_id', v_project_id, 'id', v_id)
|
||||
);
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
RETURN core.rpc_from_exception('fn_budget_chapter_delete', SQLSTATE, SQLERRM);
|
||||
END;
|
||||
$$;
|
||||
|
||||
--changeset panel:core-011e-fn-budget-item-create endDelimiter:; splitStatements:true
|
||||
CREATE OR REPLACE FUNCTION core.fn_budget_item_create(payload jsonb)
|
||||
RETURNS jsonb
|
||||
LANGUAGE plpgsql
|
||||
SECURITY INVOKER
|
||||
SET search_path = core
|
||||
AS $$
|
||||
DECLARE
|
||||
v_project_id bigint := NULLIF(payload->>'project_id', '')::bigint;
|
||||
v_chapter_id bigint := NULLIF(payload->>'chapter_id', '')::bigint;
|
||||
v_description text := nullif(btrim(payload->>'description'), '');
|
||||
v_code text := upper(coalesce(nullif(btrim(payload->>'code'), ''), ''));
|
||||
v_unit text := upper(coalesce(nullif(btrim(payload->>'unit'), ''), ''));
|
||||
v_qty numeric(14,4) := coalesce((payload->>'quantity')::numeric(14,4), 0);
|
||||
v_price numeric(14,4) := coalesce((payload->>'unit_price')::numeric(14,4), 0);
|
||||
v_wbs text := coalesce(nullif(btrim(payload->>'wbs'), ''), '');
|
||||
v_amount numeric(14,2);
|
||||
v_sort integer;
|
||||
v_row budget_items%ROWTYPE;
|
||||
BEGIN
|
||||
IF v_project_id IS NULL THEN
|
||||
RETURN core.rpc_err(
|
||||
'VALIDATION',
|
||||
'fn_budget_item_create: project_id es obligatorio',
|
||||
jsonb_build_object('fn', 'fn_budget_item_create', 'field', 'project_id')
|
||||
);
|
||||
END IF;
|
||||
IF v_description IS NULL THEN
|
||||
RETURN core.rpc_err(
|
||||
'VALIDATION',
|
||||
'fn_budget_item_create: descripción de partida obligatoria',
|
||||
jsonb_build_object('fn', 'fn_budget_item_create', 'field', 'description', 'project_id', v_project_id)
|
||||
);
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM projects WHERE id = v_project_id) THEN
|
||||
RETURN core.rpc_err(
|
||||
'NOT_FOUND',
|
||||
format('fn_budget_item_create: proyecto id=%s no encontrado', v_project_id),
|
||||
jsonb_build_object('fn', 'fn_budget_item_create', 'project_id', v_project_id)
|
||||
);
|
||||
END IF;
|
||||
IF v_chapter_id IS NOT NULL AND NOT EXISTS (
|
||||
SELECT 1 FROM budget_chapters WHERE id = v_chapter_id AND project_id = v_project_id
|
||||
) THEN
|
||||
RETURN core.rpc_err(
|
||||
'VALIDATION',
|
||||
format('fn_budget_item_create: capítulo id=%s no pertenece al proyecto %s', v_chapter_id, v_project_id),
|
||||
jsonb_build_object('fn', 'fn_budget_item_create', 'project_id', v_project_id, 'chapter_id', v_chapter_id)
|
||||
);
|
||||
END IF;
|
||||
|
||||
v_amount := round(v_qty * v_price, 2);
|
||||
SELECT COALESCE(MAX(sort_order), 0) + 1
|
||||
INTO v_sort
|
||||
FROM budget_items
|
||||
WHERE project_id = v_project_id;
|
||||
|
||||
INSERT INTO budget_items (
|
||||
project_id, chapter_id, code, description, unit, quantity, unit_price, amount, wbs, sort_order
|
||||
) VALUES (
|
||||
v_project_id, v_chapter_id, v_code, v_description, v_unit, v_qty, v_price, v_amount, v_wbs, v_sort
|
||||
)
|
||||
RETURNING * INTO v_row;
|
||||
|
||||
RETURN core.rpc_created(
|
||||
jsonb_build_object('item', to_jsonb(v_row)),
|
||||
format('Partida "%s" creada en proyecto %s (id=%s, importe=%s)',
|
||||
v_row.description, v_project_id, v_row.id, v_row.amount),
|
||||
jsonb_build_object('fn', 'fn_budget_item_create', 'project_id', v_project_id, 'id', v_row.id)
|
||||
);
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
RETURN core.rpc_from_exception('fn_budget_item_create', SQLSTATE, SQLERRM);
|
||||
END;
|
||||
$$;
|
||||
|
||||
--changeset panel:core-011f-fn-budget-item-update endDelimiter:; splitStatements:true
|
||||
CREATE OR REPLACE FUNCTION core.fn_budget_item_update(payload jsonb)
|
||||
RETURNS jsonb
|
||||
LANGUAGE plpgsql
|
||||
SECURITY INVOKER
|
||||
SET search_path = core
|
||||
AS $$
|
||||
DECLARE
|
||||
v_project_id bigint := NULLIF(payload->>'project_id', '')::bigint;
|
||||
v_id bigint := NULLIF(payload->>'id', '')::bigint;
|
||||
v_cur budget_items%ROWTYPE;
|
||||
v_chapter_id bigint;
|
||||
v_description text;
|
||||
v_code text;
|
||||
v_unit text;
|
||||
v_qty numeric(14,4);
|
||||
v_price numeric(14,4);
|
||||
v_wbs text;
|
||||
v_amount numeric(14,2);
|
||||
BEGIN
|
||||
IF v_project_id IS NULL THEN
|
||||
RETURN core.rpc_err(
|
||||
'VALIDATION',
|
||||
'fn_budget_item_update: project_id es obligatorio',
|
||||
jsonb_build_object('fn', 'fn_budget_item_update', 'field', 'project_id')
|
||||
);
|
||||
END IF;
|
||||
IF v_id IS NULL THEN
|
||||
RETURN core.rpc_err(
|
||||
'VALIDATION',
|
||||
'fn_budget_item_update: id de partida es obligatorio',
|
||||
jsonb_build_object('fn', 'fn_budget_item_update', 'field', 'id', 'project_id', v_project_id)
|
||||
);
|
||||
END IF;
|
||||
|
||||
SELECT * INTO v_cur FROM budget_items WHERE id = v_id AND project_id = v_project_id;
|
||||
IF NOT FOUND THEN
|
||||
RETURN core.rpc_err(
|
||||
'NOT_FOUND',
|
||||
format('fn_budget_item_update: partida id=%s no encontrada en proyecto %s', v_id, v_project_id),
|
||||
jsonb_build_object('fn', 'fn_budget_item_update', 'project_id', v_project_id, 'id', v_id)
|
||||
);
|
||||
END IF;
|
||||
|
||||
v_chapter_id := CASE
|
||||
WHEN payload ? 'chapter_id' THEN NULLIF(payload->>'chapter_id', '')::bigint
|
||||
ELSE v_cur.chapter_id
|
||||
END;
|
||||
v_description := coalesce(nullif(btrim(payload->>'description'), ''), v_cur.description);
|
||||
v_code := upper(CASE WHEN payload ? 'code' THEN coalesce(nullif(btrim(payload->>'code'), ''), '') ELSE v_cur.code END);
|
||||
v_unit := upper(CASE WHEN payload ? 'unit' THEN coalesce(nullif(btrim(payload->>'unit'), ''), '') ELSE v_cur.unit END);
|
||||
v_qty := CASE WHEN payload ? 'quantity' THEN coalesce((payload->>'quantity')::numeric(14,4), 0) ELSE v_cur.quantity END;
|
||||
v_price := CASE WHEN payload ? 'unit_price' THEN coalesce((payload->>'unit_price')::numeric(14,4), 0) ELSE v_cur.unit_price END;
|
||||
v_wbs := CASE WHEN payload ? 'wbs' THEN coalesce(nullif(btrim(payload->>'wbs'), ''), '') ELSE v_cur.wbs END;
|
||||
|
||||
IF v_chapter_id IS NOT NULL AND NOT EXISTS (
|
||||
SELECT 1 FROM budget_chapters WHERE id = v_chapter_id AND project_id = v_project_id
|
||||
) THEN
|
||||
RETURN core.rpc_err(
|
||||
'VALIDATION',
|
||||
format('fn_budget_item_update: capítulo id=%s no pertenece al proyecto %s', v_chapter_id, v_project_id),
|
||||
jsonb_build_object('fn', 'fn_budget_item_update', 'project_id', v_project_id, 'chapter_id', v_chapter_id)
|
||||
);
|
||||
END IF;
|
||||
|
||||
v_amount := round(v_qty * v_price, 2);
|
||||
|
||||
UPDATE budget_items
|
||||
SET chapter_id = v_chapter_id,
|
||||
code = v_code,
|
||||
description = v_description,
|
||||
unit = v_unit,
|
||||
quantity = v_qty,
|
||||
unit_price = v_price,
|
||||
amount = v_amount,
|
||||
wbs = v_wbs
|
||||
WHERE id = v_id;
|
||||
|
||||
RETURN core.rpc_ok(
|
||||
jsonb_build_object('id', v_id, 'amount', v_amount),
|
||||
format('Partida id=%s actualizada en proyecto %s (importe=%s)', v_id, v_project_id, v_amount),
|
||||
jsonb_build_object('fn', 'fn_budget_item_update', 'project_id', v_project_id, 'id', v_id)
|
||||
);
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
RETURN core.rpc_from_exception('fn_budget_item_update', SQLSTATE, SQLERRM);
|
||||
END;
|
||||
$$;
|
||||
|
||||
--changeset panel:core-011g-fn-budget-item-delete endDelimiter:; splitStatements:true
|
||||
CREATE OR REPLACE FUNCTION core.fn_budget_item_delete(payload jsonb)
|
||||
RETURNS jsonb
|
||||
LANGUAGE plpgsql
|
||||
SECURITY INVOKER
|
||||
SET search_path = core
|
||||
AS $$
|
||||
DECLARE
|
||||
v_project_id bigint := NULLIF(payload->>'project_id', '')::bigint;
|
||||
v_id bigint := NULLIF(payload->>'id', '')::bigint;
|
||||
v_description text;
|
||||
BEGIN
|
||||
IF v_project_id IS NULL THEN
|
||||
RETURN core.rpc_err(
|
||||
'VALIDATION',
|
||||
'fn_budget_item_delete: project_id es obligatorio',
|
||||
jsonb_build_object('fn', 'fn_budget_item_delete', 'field', 'project_id')
|
||||
);
|
||||
END IF;
|
||||
IF v_id IS NULL THEN
|
||||
RETURN core.rpc_err(
|
||||
'VALIDATION',
|
||||
'fn_budget_item_delete: id de partida es obligatorio',
|
||||
jsonb_build_object('fn', 'fn_budget_item_delete', 'field', 'id', 'project_id', v_project_id)
|
||||
);
|
||||
END IF;
|
||||
|
||||
DELETE FROM budget_items
|
||||
WHERE id = v_id
|
||||
AND project_id = v_project_id
|
||||
RETURNING description INTO v_description;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RETURN core.rpc_err(
|
||||
'NOT_FOUND',
|
||||
format('fn_budget_item_delete: partida id=%s no encontrada en proyecto %s', v_id, v_project_id),
|
||||
jsonb_build_object('fn', 'fn_budget_item_delete', 'project_id', v_project_id, 'id', v_id)
|
||||
);
|
||||
END IF;
|
||||
|
||||
RETURN core.rpc_ok(
|
||||
jsonb_build_object('id', v_id),
|
||||
format('Partida "%s" (id=%s) eliminada del proyecto %s', v_description, v_id, v_project_id),
|
||||
jsonb_build_object('fn', 'fn_budget_item_delete', 'project_id', v_project_id, 'id', v_id)
|
||||
);
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
RETURN core.rpc_from_exception('fn_budget_item_delete', SQLSTATE, SQLERRM);
|
||||
END;
|
||||
$$;
|
||||
|
||||
--changeset panel:core-011h-fn-budget-replace endDelimiter:; splitStatements:true
|
||||
CREATE OR REPLACE FUNCTION core.fn_budget_replace(payload jsonb)
|
||||
RETURNS jsonb
|
||||
LANGUAGE plpgsql
|
||||
SECURITY INVOKER
|
||||
SET search_path = core
|
||||
AS $$
|
||||
DECLARE
|
||||
v_project_id bigint := NULLIF(payload->>'project_id', '')::bigint;
|
||||
v_chapters jsonb := COALESCE(payload->'chapters', '[]'::jsonb);
|
||||
v_items jsonb := COALESCE(payload->'items', '[]'::jsonb);
|
||||
v_ch jsonb;
|
||||
v_it jsonb;
|
||||
v_wbs text;
|
||||
v_parent_wbs text;
|
||||
v_chapter_wbs text;
|
||||
v_parent_id bigint;
|
||||
v_chapter_id bigint;
|
||||
v_new_id bigint;
|
||||
v_sort integer;
|
||||
v_item_sort integer;
|
||||
v_qty numeric(14,4);
|
||||
v_price numeric(14,4);
|
||||
v_amount numeric(14,2);
|
||||
v_chapter_count integer := 0;
|
||||
v_item_count integer := 0;
|
||||
v_wbs_map jsonb := '{}'::jsonb;
|
||||
BEGIN
|
||||
IF v_project_id IS NULL THEN
|
||||
RETURN core.rpc_err(
|
||||
'VALIDATION',
|
||||
'fn_budget_replace: project_id es obligatorio para reemplazar el presupuesto',
|
||||
jsonb_build_object('fn', 'fn_budget_replace', 'field', 'project_id')
|
||||
);
|
||||
END IF;
|
||||
IF jsonb_typeof(v_chapters) <> 'array' THEN
|
||||
RETURN core.rpc_err(
|
||||
'VALIDATION',
|
||||
'fn_budget_replace: chapters debe ser un arreglo JSON',
|
||||
jsonb_build_object('fn', 'fn_budget_replace', 'field', 'chapters', 'project_id', v_project_id)
|
||||
);
|
||||
END IF;
|
||||
IF jsonb_typeof(v_items) <> 'array' THEN
|
||||
RETURN core.rpc_err(
|
||||
'VALIDATION',
|
||||
'fn_budget_replace: items debe ser un arreglo JSON',
|
||||
jsonb_build_object('fn', 'fn_budget_replace', 'field', 'items', 'project_id', v_project_id)
|
||||
);
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM projects WHERE id = v_project_id) THEN
|
||||
RETURN core.rpc_err(
|
||||
'NOT_FOUND',
|
||||
format('fn_budget_replace: proyecto id=%s no encontrado', v_project_id),
|
||||
jsonb_build_object('fn', 'fn_budget_replace', 'project_id', v_project_id)
|
||||
);
|
||||
END IF;
|
||||
|
||||
DELETE FROM budget_items WHERE project_id = v_project_id;
|
||||
DELETE FROM budget_chapters WHERE project_id = v_project_id;
|
||||
|
||||
v_sort := 0;
|
||||
FOR v_ch IN
|
||||
SELECT elem
|
||||
FROM jsonb_array_elements(v_chapters) AS elem
|
||||
ORDER BY coalesce(nullif(btrim(elem->>'wbs'), ''), elem->>'sort_order', elem->>'name')
|
||||
LOOP
|
||||
v_sort := v_sort + 1;
|
||||
v_wbs := coalesce(nullif(btrim(v_ch->>'wbs'), ''), v_sort::text);
|
||||
v_parent_wbs := nullif(btrim(v_ch->>'parent_wbs'), '');
|
||||
v_parent_id := NULL;
|
||||
IF v_parent_wbs IS NOT NULL THEN
|
||||
v_parent_id := NULLIF(v_wbs_map->>v_parent_wbs, '')::bigint;
|
||||
IF v_parent_id IS NULL THEN
|
||||
RETURN core.rpc_err(
|
||||
'VALIDATION',
|
||||
format('fn_budget_replace: parent_wbs=%s no encontrado al insertar capítulo wbs=%s', v_parent_wbs, v_wbs),
|
||||
jsonb_build_object('fn', 'fn_budget_replace', 'project_id', v_project_id, 'wbs', v_wbs, 'parent_wbs', v_parent_wbs)
|
||||
);
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
IF nullif(btrim(v_ch->>'name'), '') IS NULL THEN
|
||||
RETURN core.rpc_err(
|
||||
'VALIDATION',
|
||||
format('fn_budget_replace: nombre de capítulo obligatorio (wbs=%s)', v_wbs),
|
||||
jsonb_build_object('fn', 'fn_budget_replace', 'project_id', v_project_id, 'wbs', v_wbs)
|
||||
);
|
||||
END IF;
|
||||
|
||||
INSERT INTO budget_chapters (project_id, parent_id, code, name, wbs, sort_order)
|
||||
VALUES (
|
||||
v_project_id,
|
||||
v_parent_id,
|
||||
coalesce(nullif(btrim(v_ch->>'code'), ''), ''),
|
||||
btrim(v_ch->>'name'),
|
||||
v_wbs,
|
||||
coalesce((v_ch->>'sort_order')::integer, v_sort)
|
||||
)
|
||||
RETURNING id INTO v_new_id;
|
||||
|
||||
v_wbs_map := v_wbs_map || jsonb_build_object(v_wbs, v_new_id::text);
|
||||
v_chapter_count := v_chapter_count + 1;
|
||||
END LOOP;
|
||||
|
||||
v_item_sort := 0;
|
||||
FOR v_it IN SELECT elem FROM jsonb_array_elements(v_items) AS elem
|
||||
LOOP
|
||||
v_item_sort := v_item_sort + 1;
|
||||
IF nullif(btrim(v_it->>'description'), '') IS NULL THEN
|
||||
RETURN core.rpc_err(
|
||||
'VALIDATION',
|
||||
format('fn_budget_replace: descripción obligatoria en partida #%s', v_item_sort),
|
||||
jsonb_build_object('fn', 'fn_budget_replace', 'project_id', v_project_id, 'item_index', v_item_sort)
|
||||
);
|
||||
END IF;
|
||||
|
||||
v_chapter_wbs := nullif(btrim(v_it->>'chapter_wbs'), '');
|
||||
v_chapter_id := NULL;
|
||||
IF v_chapter_wbs IS NOT NULL THEN
|
||||
v_chapter_id := NULLIF(v_wbs_map->>v_chapter_wbs, '')::bigint;
|
||||
IF v_chapter_id IS NULL THEN
|
||||
RETURN core.rpc_err(
|
||||
'VALIDATION',
|
||||
format('fn_budget_replace: chapter_wbs=%s no encontrado para partida #%s', v_chapter_wbs, v_item_sort),
|
||||
jsonb_build_object('fn', 'fn_budget_replace', 'project_id', v_project_id, 'chapter_wbs', v_chapter_wbs)
|
||||
);
|
||||
END IF;
|
||||
ELSIF v_it ? 'chapter_id' AND nullif(v_it->>'chapter_id', '') IS NOT NULL THEN
|
||||
v_chapter_id := (v_it->>'chapter_id')::bigint;
|
||||
IF NOT EXISTS (SELECT 1 FROM budget_chapters WHERE id = v_chapter_id AND project_id = v_project_id) THEN
|
||||
RETURN core.rpc_err(
|
||||
'VALIDATION',
|
||||
format('fn_budget_replace: chapter_id=%s inválido para partida #%s', v_chapter_id, v_item_sort),
|
||||
jsonb_build_object('fn', 'fn_budget_replace', 'project_id', v_project_id, 'chapter_id', v_chapter_id)
|
||||
);
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
v_qty := coalesce((v_it->>'quantity')::numeric(14,4), 0);
|
||||
v_price := coalesce((v_it->>'unit_price')::numeric(14,4), 0);
|
||||
v_amount := round(coalesce((v_it->>'amount')::numeric(14,2), v_qty * v_price), 2);
|
||||
|
||||
INSERT INTO budget_items (
|
||||
project_id, chapter_id, code, description, unit, quantity, unit_price, amount, wbs, sort_order
|
||||
) VALUES (
|
||||
v_project_id,
|
||||
v_chapter_id,
|
||||
upper(coalesce(nullif(btrim(v_it->>'code'), ''), '')),
|
||||
btrim(v_it->>'description'),
|
||||
upper(coalesce(nullif(btrim(v_it->>'unit'), ''), '')),
|
||||
v_qty,
|
||||
v_price,
|
||||
v_amount,
|
||||
coalesce(nullif(btrim(v_it->>'wbs'), ''), ''),
|
||||
coalesce((v_it->>'sort_order')::integer, v_item_sort)
|
||||
);
|
||||
v_item_count := v_item_count + 1;
|
||||
END LOOP;
|
||||
|
||||
RETURN core.rpc_ok(
|
||||
jsonb_build_object(
|
||||
'project_id', v_project_id,
|
||||
'chapter_count', v_chapter_count,
|
||||
'item_count', v_item_count
|
||||
),
|
||||
format('Presupuesto del proyecto %s reemplazado: %s capítulo(s) y %s partida(s) importados',
|
||||
v_project_id, v_chapter_count, v_item_count),
|
||||
jsonb_build_object(
|
||||
'fn', 'fn_budget_replace',
|
||||
'project_id', v_project_id,
|
||||
'chapter_count', v_chapter_count,
|
||||
'item_count', v_item_count
|
||||
)
|
||||
);
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
RETURN core.rpc_from_exception('fn_budget_replace', SQLSTATE, SQLERRM);
|
||||
END;
|
||||
$$;
|
||||
|
||||
--changeset panel:core-011i-fn-budget-grants endDelimiter:; splitStatements:true
|
||||
GRANT EXECUTE ON FUNCTION core.fn_budget_list(jsonb) TO panels_core_app;
|
||||
GRANT EXECUTE ON FUNCTION core.fn_budget_chapter_create(jsonb) TO panels_core_app;
|
||||
GRANT EXECUTE ON FUNCTION core.fn_budget_chapter_update(jsonb) TO panels_core_app;
|
||||
GRANT EXECUTE ON FUNCTION core.fn_budget_chapter_delete(jsonb) TO panels_core_app;
|
||||
GRANT EXECUTE ON FUNCTION core.fn_budget_item_create(jsonb) TO panels_core_app;
|
||||
GRANT EXECUTE ON FUNCTION core.fn_budget_item_update(jsonb) TO panels_core_app;
|
||||
GRANT EXECUTE ON FUNCTION core.fn_budget_item_delete(jsonb) TO panels_core_app;
|
||||
GRANT EXECUTE ON FUNCTION core.fn_budget_replace(jsonb) TO panels_core_app;
|
||||
333
db/core/changesets/012-rpc-badges.sql
Normal file
333
db/core/changesets/012-rpc-badges.sql
Normal file
|
|
@ -0,0 +1,333 @@
|
|||
--liquibase formatted sql
|
||||
-- PANELS · core · RPC gafetes (jobs de impresión y entrega)
|
||||
|
||||
--changeset panel:core-012a-fn-badge-job-create endDelimiter:; splitStatements:true
|
||||
CREATE OR REPLACE FUNCTION core.fn_badge_job_create(payload jsonb)
|
||||
RETURNS jsonb
|
||||
LANGUAGE plpgsql
|
||||
SECURITY INVOKER
|
||||
SET search_path = core
|
||||
AS $$
|
||||
DECLARE
|
||||
v_project_id bigint := NULLIF(payload->>'project_id', '')::bigint;
|
||||
v_created_by_id integer := NULLIF(payload->>'created_by_id', '')::integer;
|
||||
v_created_by_name text := coalesce(nullif(btrim(payload->>'created_by_name'), ''), '');
|
||||
v_worker_ids integer[];
|
||||
v_wid integer;
|
||||
v_job_id bigint;
|
||||
v_project record;
|
||||
v_missing integer;
|
||||
v_count integer := 0;
|
||||
BEGIN
|
||||
IF v_project_id IS NULL THEN
|
||||
RETURN core.rpc_err(
|
||||
'VALIDATION',
|
||||
'fn_badge_job_create: project_id es obligatorio para generar gafetes',
|
||||
jsonb_build_object('fn', 'fn_badge_job_create', 'field', 'project_id')
|
||||
);
|
||||
END IF;
|
||||
|
||||
SELECT p.id, p.code, p.name, p.status
|
||||
INTO v_project
|
||||
FROM projects p
|
||||
WHERE p.id = v_project_id;
|
||||
IF NOT FOUND THEN
|
||||
RETURN core.rpc_err(
|
||||
'NOT_FOUND',
|
||||
format('fn_badge_job_create: proyecto id=%s no encontrado', v_project_id),
|
||||
jsonb_build_object('fn', 'fn_badge_job_create', 'project_id', v_project_id)
|
||||
);
|
||||
END IF;
|
||||
IF v_project.status <> 'activo' THEN
|
||||
RETURN core.rpc_err(
|
||||
'VALIDATION',
|
||||
format('fn_badge_job_create: solo se generan gafetes de proyectos activos (proyecto %s está en %s)',
|
||||
v_project.code, v_project.status),
|
||||
jsonb_build_object('fn', 'fn_badge_job_create', 'project_id', v_project_id, 'status', v_project.status)
|
||||
);
|
||||
END IF;
|
||||
|
||||
IF payload->'worker_ids' IS NULL OR jsonb_typeof(payload->'worker_ids') <> 'array' THEN
|
||||
RETURN core.rpc_err(
|
||||
'VALIDATION',
|
||||
'fn_badge_job_create: worker_ids debe ser un arreglo de enteros',
|
||||
jsonb_build_object('fn', 'fn_badge_job_create', 'field', 'worker_ids', 'project_id', v_project_id)
|
||||
);
|
||||
END IF;
|
||||
|
||||
SELECT array_agg(DISTINCT (elem::text)::integer ORDER BY (elem::text)::integer)
|
||||
INTO v_worker_ids
|
||||
FROM jsonb_array_elements(payload->'worker_ids') AS elem
|
||||
WHERE nullif(btrim(elem::text), '') IS NOT NULL;
|
||||
|
||||
IF v_worker_ids IS NULL OR cardinality(v_worker_ids) = 0 THEN
|
||||
RETURN core.rpc_err(
|
||||
'VALIDATION',
|
||||
'fn_badge_job_create: debe indicar al menos un trabajador en worker_ids',
|
||||
jsonb_build_object('fn', 'fn_badge_job_create', 'field', 'worker_ids', 'project_id', v_project_id)
|
||||
);
|
||||
END IF;
|
||||
|
||||
SELECT COUNT(*)
|
||||
INTO v_missing
|
||||
FROM unnest(v_worker_ids) AS wid
|
||||
WHERE NOT EXISTS (SELECT 1 FROM workers w WHERE w.id = wid);
|
||||
IF v_missing > 0 THEN
|
||||
RETURN core.rpc_err(
|
||||
'VALIDATION',
|
||||
format('fn_badge_job_create: %s trabajador(es) en worker_ids no existen en el catálogo', v_missing),
|
||||
jsonb_build_object('fn', 'fn_badge_job_create', 'project_id', v_project_id, 'missing_count', v_missing)
|
||||
);
|
||||
END IF;
|
||||
|
||||
INSERT INTO badge_jobs (project_id, status, created_by_id, created_by_name)
|
||||
VALUES (v_project_id, 'done', v_created_by_id, v_created_by_name)
|
||||
RETURNING id INTO v_job_id;
|
||||
|
||||
FOREACH v_wid IN ARRAY v_worker_ids
|
||||
LOOP
|
||||
INSERT INTO badge_job_people (job_id, worker_id)
|
||||
VALUES (v_job_id::integer, v_wid)
|
||||
ON CONFLICT (job_id, worker_id) DO NOTHING;
|
||||
v_count := v_count + 1;
|
||||
END LOOP;
|
||||
|
||||
RETURN core.rpc_created(
|
||||
jsonb_build_object(
|
||||
'id', v_job_id,
|
||||
'project_id', v_project_id,
|
||||
'count', v_count,
|
||||
'worker_ids', to_jsonb(v_worker_ids)
|
||||
),
|
||||
format('Job de gafetes #%s creado para proyecto %s (%s): %s persona(s)',
|
||||
v_job_id, v_project.code, v_project.name, v_count),
|
||||
jsonb_build_object(
|
||||
'fn', 'fn_badge_job_create',
|
||||
'job_id', v_job_id,
|
||||
'project_id', v_project_id,
|
||||
'count', v_count,
|
||||
'created_by_id', v_created_by_id
|
||||
)
|
||||
);
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
RETURN core.rpc_from_exception('fn_badge_job_create', SQLSTATE, SQLERRM);
|
||||
END;
|
||||
$$;
|
||||
|
||||
--changeset panel:core-012b-fn-badge-job-list endDelimiter:; splitStatements:true
|
||||
CREATE OR REPLACE FUNCTION core.fn_badge_job_list(payload jsonb)
|
||||
RETURNS jsonb
|
||||
LANGUAGE plpgsql
|
||||
SECURITY INVOKER
|
||||
SET search_path = core
|
||||
AS $$
|
||||
DECLARE
|
||||
v_project_id bigint := NULLIF(payload->>'project_id', '')::bigint;
|
||||
v_jobs jsonb;
|
||||
v_count integer;
|
||||
BEGIN
|
||||
SELECT
|
||||
COALESCE(jsonb_agg(to_jsonb(x) ORDER BY x.id DESC), '[]'::jsonb),
|
||||
COUNT(*)::integer
|
||||
INTO v_jobs, v_count
|
||||
FROM (
|
||||
SELECT
|
||||
j.id,
|
||||
j.project_id,
|
||||
j.status,
|
||||
j.pdf_path,
|
||||
j.created_by_id,
|
||||
j.created_by_name,
|
||||
j.created_at,
|
||||
p.code AS project_code,
|
||||
p.name AS project_name,
|
||||
(SELECT COUNT(*) FROM badge_job_people bp WHERE bp.job_id = j.id) AS people,
|
||||
(SELECT COUNT(*) FROM badge_job_people bp WHERE bp.job_id = j.id AND bp.delivered = true) AS delivered
|
||||
FROM badge_jobs j
|
||||
JOIN projects p ON p.id = j.project_id
|
||||
WHERE v_project_id IS NULL OR j.project_id = v_project_id
|
||||
) x;
|
||||
|
||||
RETURN core.rpc_ok(
|
||||
jsonb_build_object('jobs', v_jobs),
|
||||
format('Listado de jobs de gafetes: %s registro(s)%s',
|
||||
v_count,
|
||||
CASE WHEN v_project_id IS NOT NULL THEN format(' para proyecto %s', v_project_id) ELSE '' END),
|
||||
jsonb_build_object(
|
||||
'fn', 'fn_badge_job_list',
|
||||
'project_id', v_project_id,
|
||||
'count', v_count
|
||||
)
|
||||
);
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
RETURN core.rpc_from_exception('fn_badge_job_list', SQLSTATE, SQLERRM);
|
||||
END;
|
||||
$$;
|
||||
|
||||
--changeset panel:core-012c-fn-badge-job-get endDelimiter:; splitStatements:true
|
||||
CREATE OR REPLACE FUNCTION core.fn_badge_job_get(payload jsonb)
|
||||
RETURNS jsonb
|
||||
LANGUAGE plpgsql
|
||||
SECURITY INVOKER
|
||||
SET search_path = core
|
||||
AS $$
|
||||
DECLARE
|
||||
v_job_id bigint := COALESCE(
|
||||
NULLIF(payload->>'job_id', '')::bigint,
|
||||
NULLIF(payload->>'id', '')::bigint
|
||||
);
|
||||
v_job jsonb;
|
||||
v_people jsonb;
|
||||
BEGIN
|
||||
IF v_job_id IS NULL THEN
|
||||
RETURN core.rpc_err(
|
||||
'VALIDATION',
|
||||
'fn_badge_job_get: job_id es obligatorio',
|
||||
jsonb_build_object('fn', 'fn_badge_job_get', 'field', 'job_id')
|
||||
);
|
||||
END IF;
|
||||
|
||||
SELECT to_jsonb(x)
|
||||
INTO v_job
|
||||
FROM (
|
||||
SELECT
|
||||
j.*,
|
||||
p.code AS project_code,
|
||||
p.name AS project_name
|
||||
FROM badge_jobs j
|
||||
JOIN projects p ON p.id = j.project_id
|
||||
WHERE j.id = v_job_id
|
||||
) x;
|
||||
|
||||
IF v_job IS NULL THEN
|
||||
RETURN core.rpc_err(
|
||||
'NOT_FOUND',
|
||||
format('fn_badge_job_get: job de gafetes id=%s no encontrado', v_job_id),
|
||||
jsonb_build_object('fn', 'fn_badge_job_get', 'job_id', v_job_id)
|
||||
);
|
||||
END IF;
|
||||
|
||||
SELECT COALESCE(jsonb_agg(to_jsonb(p) ORDER BY w.last_name_p, w.first_name), '[]'::jsonb)
|
||||
INTO v_people
|
||||
FROM (
|
||||
SELECT
|
||||
bp.job_id,
|
||||
bp.worker_id,
|
||||
bp.delivered,
|
||||
bp.delivered_at,
|
||||
w.first_name,
|
||||
w.last_name_p,
|
||||
w.last_name_m,
|
||||
w.position
|
||||
FROM badge_job_people bp
|
||||
JOIN workers w ON w.id = bp.worker_id
|
||||
WHERE bp.job_id = v_job_id::integer
|
||||
) p;
|
||||
|
||||
RETURN core.rpc_ok(
|
||||
jsonb_build_object('job', v_job, 'people', v_people),
|
||||
format('Job de gafetes #%s cargado: %s persona(s) en el lote',
|
||||
v_job_id, jsonb_array_length(v_people)),
|
||||
jsonb_build_object(
|
||||
'fn', 'fn_badge_job_get',
|
||||
'job_id', v_job_id,
|
||||
'people_count', jsonb_array_length(v_people)
|
||||
)
|
||||
);
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
RETURN core.rpc_from_exception('fn_badge_job_get', SQLSTATE, SQLERRM);
|
||||
END;
|
||||
$$;
|
||||
|
||||
--changeset panel:core-012d-fn-badge-job-set-delivered endDelimiter:; splitStatements:true
|
||||
CREATE OR REPLACE FUNCTION core.fn_badge_job_set_delivered(payload jsonb)
|
||||
RETURNS jsonb
|
||||
LANGUAGE plpgsql
|
||||
SECURITY INVOKER
|
||||
SET search_path = core
|
||||
AS $$
|
||||
DECLARE
|
||||
v_job_id bigint := NULLIF(payload->>'job_id', '')::bigint;
|
||||
v_worker_id integer := NULLIF(payload->>'worker_id', '')::integer;
|
||||
v_delivered boolean;
|
||||
v_updated boolean;
|
||||
BEGIN
|
||||
IF v_job_id IS NULL THEN
|
||||
RETURN core.rpc_err(
|
||||
'VALIDATION',
|
||||
'fn_badge_job_set_delivered: job_id es obligatorio',
|
||||
jsonb_build_object('fn', 'fn_badge_job_set_delivered', 'field', 'job_id')
|
||||
);
|
||||
END IF;
|
||||
IF v_worker_id IS NULL THEN
|
||||
RETURN core.rpc_err(
|
||||
'VALIDATION',
|
||||
'fn_badge_job_set_delivered: worker_id es obligatorio',
|
||||
jsonb_build_object('fn', 'fn_badge_job_set_delivered', 'field', 'worker_id', 'job_id', v_job_id)
|
||||
);
|
||||
END IF;
|
||||
IF payload->>'delivered' IS NULL THEN
|
||||
RETURN core.rpc_err(
|
||||
'VALIDATION',
|
||||
'fn_badge_job_set_delivered: delivered (boolean) es obligatorio',
|
||||
jsonb_build_object('fn', 'fn_badge_job_set_delivered', 'field', 'delivered', 'job_id', v_job_id, 'worker_id', v_worker_id)
|
||||
);
|
||||
END IF;
|
||||
|
||||
v_delivered := (payload->>'delivered')::boolean;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM badge_jobs WHERE id = v_job_id) THEN
|
||||
RETURN core.rpc_err(
|
||||
'NOT_FOUND',
|
||||
format('fn_badge_job_set_delivered: job de gafetes id=%s no encontrado', v_job_id),
|
||||
jsonb_build_object('fn', 'fn_badge_job_set_delivered', 'job_id', v_job_id)
|
||||
);
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM workers WHERE id = v_worker_id) THEN
|
||||
RETURN core.rpc_err(
|
||||
'VALIDATION',
|
||||
format('fn_badge_job_set_delivered: trabajador id=%s no encontrado', v_worker_id),
|
||||
jsonb_build_object('fn', 'fn_badge_job_set_delivered', 'worker_id', v_worker_id, 'job_id', v_job_id)
|
||||
);
|
||||
END IF;
|
||||
|
||||
UPDATE badge_job_people
|
||||
SET delivered = v_delivered,
|
||||
delivered_at = CASE WHEN v_delivered THEN now() ELSE NULL END
|
||||
WHERE job_id = v_job_id::integer
|
||||
AND worker_id = v_worker_id
|
||||
RETURNING true INTO v_updated;
|
||||
|
||||
IF NOT COALESCE(v_updated, false) THEN
|
||||
RETURN core.rpc_err(
|
||||
'NOT_FOUND',
|
||||
format('fn_badge_job_set_delivered: trabajador id=%s no pertenece al job %s', v_worker_id, v_job_id),
|
||||
jsonb_build_object('fn', 'fn_badge_job_set_delivered', 'job_id', v_job_id, 'worker_id', v_worker_id)
|
||||
);
|
||||
END IF;
|
||||
|
||||
RETURN core.rpc_ok(
|
||||
jsonb_build_object(
|
||||
'job_id', v_job_id,
|
||||
'worker_id', v_worker_id,
|
||||
'delivered', v_delivered
|
||||
),
|
||||
format('Entrega de gafete actualizada: trabajador %s en job #%s → %s',
|
||||
v_worker_id, v_job_id, CASE WHEN v_delivered THEN 'entregado' ELSE 'pendiente' END),
|
||||
jsonb_build_object(
|
||||
'fn', 'fn_badge_job_set_delivered',
|
||||
'job_id', v_job_id,
|
||||
'worker_id', v_worker_id,
|
||||
'delivered', v_delivered
|
||||
)
|
||||
);
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
RETURN core.rpc_from_exception('fn_badge_job_set_delivered', SQLSTATE, SQLERRM);
|
||||
END;
|
||||
$$;
|
||||
|
||||
--changeset panel:core-012e-fn-badge-grants endDelimiter:; splitStatements:true
|
||||
GRANT EXECUTE ON FUNCTION core.fn_badge_job_create(jsonb) TO panels_core_app;
|
||||
GRANT EXECUTE ON FUNCTION core.fn_badge_job_list(jsonb) TO panels_core_app;
|
||||
GRANT EXECUTE ON FUNCTION core.fn_badge_job_get(jsonb) TO panels_core_app;
|
||||
GRANT EXECUTE ON FUNCTION core.fn_badge_job_set_delivered(jsonb) TO panels_core_app;
|
||||
Loading…
Reference in a new issue