625 lines
25 KiB
PL/PgSQL
625 lines
25 KiB
PL/PgSQL
-- ============================================================
|
||
-- Migration 009: Dashboard RPC functions
|
||
-- Todas son SECURITY DEFINER + search_path fijado.
|
||
-- Aplican k-anonimato k>=5 ANTES de devolver.
|
||
-- NUNCA tocan question codes 3.4 (salario) ni 7.1 (texto libre).
|
||
-- El cliente NUNCA accede a `answers` directamente — solo via estas funciones.
|
||
-- ============================================================
|
||
|
||
-- ── Helper: responses filtradas por cohort ────────────────────
|
||
-- Devuelve IDs de respuestas que cumplen los filtros opcionales.
|
||
-- Filtros pasados como text arrays o NULL (sin filtro).
|
||
CREATE OR REPLACE FUNCTION public._dash_responses(
|
||
p_survey_id uuid,
|
||
p_depto text DEFAULT NULL,
|
||
p_genero text[] DEFAULT NULL,
|
||
p_etario text[] DEFAULT NULL,
|
||
p_modalidad text[] DEFAULT NULL
|
||
)
|
||
RETURNS TABLE(response_id uuid)
|
||
LANGUAGE sql STABLE SECURITY DEFINER
|
||
SET search_path = public
|
||
AS $$
|
||
SELECT r.id
|
||
FROM public.responses r
|
||
WHERE r.survey_id = p_survey_id
|
||
-- Departamento: A1
|
||
AND (p_depto IS NULL OR EXISTS (
|
||
SELECT 1 FROM public.answers a
|
||
JOIN public.questions q ON q.id = a.question_id
|
||
WHERE a.response_id = r.id AND q.code = 'A1'
|
||
AND a.value #>> '{}' = p_depto
|
||
))
|
||
-- Género: A6
|
||
AND (p_genero IS NULL OR cardinality(p_genero) = 0 OR EXISTS (
|
||
SELECT 1 FROM public.answers a
|
||
JOIN public.questions q ON q.id = a.question_id
|
||
WHERE a.response_id = r.id AND q.code = 'A6'
|
||
AND a.value #>> '{}' = ANY(p_genero)
|
||
))
|
||
-- Rango etario: qi_age_bucket en responses
|
||
AND (p_etario IS NULL OR cardinality(p_etario) = 0
|
||
OR r.qi_age_bucket = ANY(p_etario))
|
||
-- Modalidad: A7
|
||
AND (p_modalidad IS NULL OR cardinality(p_modalidad) = 0 OR EXISTS (
|
||
SELECT 1 FROM public.answers a
|
||
JOIN public.questions q ON q.id = a.question_id
|
||
WHERE a.response_id = r.id AND q.code = 'A7'
|
||
AND a.value #>> '{}' = ANY(p_modalidad)
|
||
));
|
||
$$;
|
||
|
||
-- ── Helper: parse filters JSONB → arrays ──────────────────────
|
||
-- Convierte el objeto de filtros recibido del cliente en arrays tipados.
|
||
-- Devuelve NULL para filtros no presentes o vacíos.
|
||
CREATE OR REPLACE FUNCTION public._dash_parse_filters(p_filters jsonb)
|
||
RETURNS TABLE(
|
||
f_depto text,
|
||
f_genero text[],
|
||
f_etario text[],
|
||
f_modalidad text[]
|
||
)
|
||
LANGUAGE sql IMMUTABLE SECURITY DEFINER
|
||
SET search_path = public
|
||
AS $$
|
||
SELECT
|
||
NULLIF(p_filters->>'depto',''),
|
||
ARRAY(SELECT jsonb_array_elements_text(p_filters->'genero'))::text[],
|
||
ARRAY(SELECT jsonb_array_elements_text(p_filters->'etario'))::text[],
|
||
ARRAY(SELECT jsonb_array_elements_text(p_filters->'modalidad'))::text[];
|
||
$$;
|
||
|
||
-- ── KPIs ─────────────────────────────────────────────────────
|
||
CREATE OR REPLACE FUNCTION public.rpc_dashboard_kpis(
|
||
p_survey_id uuid,
|
||
p_filters jsonb DEFAULT '{}'::jsonb
|
||
)
|
||
RETURNS jsonb
|
||
LANGUAGE plpgsql STABLE SECURITY DEFINER
|
||
SET search_path = public
|
||
AS $$
|
||
DECLARE
|
||
v_f record;
|
||
v_n bigint;
|
||
v_cuidadores bigint;
|
||
v_bienestar_avg numeric;
|
||
v_bienestar_n bigint;
|
||
v_ausencias_pct numeric;
|
||
BEGIN
|
||
SELECT * INTO v_f FROM public._dash_parse_filters(p_filters);
|
||
|
||
SELECT count(*) INTO v_n
|
||
FROM public._dash_responses(p_survey_id, v_f.f_depto, v_f.f_genero, v_f.f_etario, v_f.f_modalidad);
|
||
|
||
IF v_n < 5 THEN
|
||
RETURN jsonb_build_object('status','suppressed','min_n',5,'n',v_n);
|
||
END IF;
|
||
|
||
-- % cuidadores: algún trigger = 'Sí'
|
||
SELECT count(DISTINCT dr.response_id) INTO v_cuidadores
|
||
FROM public._dash_responses(p_survey_id, v_f.f_depto, v_f.f_genero, v_f.f_etario, v_f.f_modalidad) dr
|
||
WHERE EXISTS (
|
||
SELECT 1 FROM public.answers a
|
||
JOIN public.questions q ON q.id = a.question_id
|
||
WHERE a.response_id = dr.response_id
|
||
AND q.code IN ('1.1','1.6','1.13')
|
||
AND a.value #>> '{}' = 'Sí'
|
||
);
|
||
|
||
-- Score bienestar: media de 4.3 (rating 1-5), normalizado a 0-100
|
||
SELECT avg((a.value #>> '{}')::numeric), count(*)
|
||
INTO v_bienestar_avg, v_bienestar_n
|
||
FROM public._dash_responses(p_survey_id, v_f.f_depto, v_f.f_genero, v_f.f_etario, v_f.f_modalidad) dr
|
||
JOIN public.answers a ON a.response_id = dr.response_id
|
||
JOIN public.questions q ON q.id = a.question_id
|
||
WHERE q.code = '4.3'
|
||
AND a.value #>> '{}' ~ '^[0-9]+$';
|
||
|
||
-- % ausencias frecuentes: 5.2 = frecuentes (Varias veces o Con frecuencia)
|
||
SELECT count(DISTINCT dr.response_id) INTO v_ausencias_pct
|
||
FROM public._dash_responses(p_survey_id, v_f.f_depto, v_f.f_genero, v_f.f_etario, v_f.f_modalidad) dr
|
||
JOIN public.answers a ON a.response_id = dr.response_id
|
||
JOIN public.questions q ON q.id = a.question_id
|
||
WHERE q.code = '5.2'
|
||
AND a.value #>> '{}' IN ('Varias veces (4-10 veces al año)','Con frecuencia (más de 10 veces al año)');
|
||
|
||
RETURN jsonb_build_object(
|
||
'status', 'ok',
|
||
'n', v_n,
|
||
'pct_cuidadores', ROUND(v_cuidadores::numeric / v_n * 100, 1),
|
||
'score_bienestar', CASE WHEN v_bienestar_n > 0
|
||
THEN ROUND((v_bienestar_avg - 1) / 4 * 100, 1)
|
||
ELSE NULL END,
|
||
'pct_ausencias', ROUND(v_ausencias_pct::numeric / v_n * 100, 1)
|
||
);
|
||
END;
|
||
$$;
|
||
|
||
-- ── Prevalencia por departamento ──────────────────────────────
|
||
CREATE OR REPLACE FUNCTION public.rpc_prevalencia_por_depto(
|
||
p_survey_id uuid,
|
||
p_filters jsonb DEFAULT '{}'::jsonb
|
||
)
|
||
RETURNS jsonb
|
||
LANGUAGE plpgsql STABLE SECURITY DEFINER
|
||
SET search_path = public
|
||
AS $$
|
||
DECLARE
|
||
v_f record;
|
||
v_total bigint;
|
||
v_rows jsonb;
|
||
BEGIN
|
||
SELECT * INTO v_f FROM public._dash_parse_filters(p_filters);
|
||
|
||
SELECT count(*) INTO v_total
|
||
FROM public._dash_responses(p_survey_id, v_f.f_depto, v_f.f_genero, v_f.f_etario, v_f.f_modalidad);
|
||
|
||
IF v_total < 5 THEN
|
||
RETURN jsonb_build_object('status','suppressed','min_n',5,'n',v_total);
|
||
END IF;
|
||
|
||
SELECT jsonb_agg(row) INTO v_rows FROM (
|
||
SELECT jsonb_build_object(
|
||
'depto', depto_val,
|
||
'n', CASE WHEN n_depto >= 5 THEN n_depto ELSE NULL END,
|
||
'pct_cuidadores',CASE WHEN n_depto >= 5
|
||
THEN ROUND(n_cuid::numeric / n_depto * 100, 1)
|
||
ELSE NULL END,
|
||
'suppressed', (n_depto < 5)
|
||
) AS row
|
||
FROM (
|
||
SELECT
|
||
a.value #>> '{}' AS depto_val,
|
||
count(DISTINCT dr.response_id) AS n_depto,
|
||
count(DISTINCT dr.response_id) FILTER (
|
||
WHERE EXISTS (
|
||
SELECT 1 FROM public.answers a2
|
||
JOIN public.questions q2 ON q2.id = a2.question_id
|
||
WHERE a2.response_id = dr.response_id
|
||
AND q2.code IN ('1.1','1.6','1.13')
|
||
AND a2.value #>> '{}' = 'Sí'
|
||
)
|
||
) AS n_cuid
|
||
FROM public._dash_responses(p_survey_id, NULL, v_f.f_genero, v_f.f_etario, v_f.f_modalidad) dr
|
||
JOIN public.answers a ON a.response_id = dr.response_id
|
||
JOIN public.questions q ON q.id = a.question_id
|
||
WHERE q.code = 'A1'
|
||
GROUP BY a.value #>> '{}'
|
||
) sub
|
||
ORDER BY depto_val
|
||
) t;
|
||
|
||
RETURN jsonb_build_object('status','ok','n_total',v_total,'data', COALESCE(v_rows,'[]'::jsonb));
|
||
END;
|
||
$$;
|
||
|
||
-- ── Heatmap: horas de cuidado × ausencias ────────────────────
|
||
CREATE OR REPLACE FUNCTION public.rpc_heatmap_intensidad(
|
||
p_survey_id uuid,
|
||
p_filters jsonb DEFAULT '{}'::jsonb
|
||
)
|
||
RETURNS jsonb
|
||
LANGUAGE plpgsql STABLE SECURITY DEFINER
|
||
SET search_path = public
|
||
AS $$
|
||
DECLARE
|
||
v_f record;
|
||
v_total bigint;
|
||
v_rows jsonb;
|
||
BEGIN
|
||
SELECT * INTO v_f FROM public._dash_parse_filters(p_filters);
|
||
SELECT count(*) INTO v_total
|
||
FROM public._dash_responses(p_survey_id, v_f.f_depto, v_f.f_genero, v_f.f_etario, v_f.f_modalidad);
|
||
IF v_total < 5 THEN
|
||
RETURN jsonb_build_object('status','suppressed','min_n',5,'n',v_total);
|
||
END IF;
|
||
|
||
SELECT jsonb_agg(row) INTO v_rows FROM (
|
||
SELECT jsonb_build_object(
|
||
'horas', horas_bin,
|
||
'ausencias', aus_bin,
|
||
'n', CASE WHEN cnt >= 5 THEN cnt ELSE NULL END,
|
||
'suppressed', (cnt < 5)
|
||
) AS row
|
||
FROM (
|
||
SELECT
|
||
ah.value #>> '{}' AS horas_bin,
|
||
aa.value #>> '{}' AS aus_bin,
|
||
count(DISTINCT dr.response_id) AS cnt
|
||
FROM public._dash_responses(p_survey_id, v_f.f_depto, v_f.f_genero, v_f.f_etario, v_f.f_modalidad) dr
|
||
JOIN public.answers ah ON ah.response_id = dr.response_id
|
||
JOIN public.questions qh ON qh.id = ah.question_id AND qh.code = '2.2'
|
||
JOIN public.answers aa ON aa.response_id = dr.response_id
|
||
JOIN public.questions qa ON qa.id = aa.question_id AND qa.code = '5.2'
|
||
GROUP BY 1, 2
|
||
) sub
|
||
ORDER BY horas_bin, aus_bin
|
||
) t;
|
||
|
||
RETURN jsonb_build_object('status','ok','n_total',v_total,'data', COALESCE(v_rows,'[]'::jsonb));
|
||
END;
|
||
$$;
|
||
|
||
-- ── Distribución tipo de cuidado (donut) ─────────────────────
|
||
CREATE OR REPLACE FUNCTION public.rpc_distribucion_cuidado(
|
||
p_survey_id uuid,
|
||
p_filters jsonb DEFAULT '{}'::jsonb
|
||
)
|
||
RETURNS jsonb
|
||
LANGUAGE plpgsql STABLE SECURITY DEFINER
|
||
SET search_path = public
|
||
AS $$
|
||
DECLARE
|
||
v_f record;
|
||
v_total bigint;
|
||
v_disc bigint;
|
||
v_familiar bigint;
|
||
v_mayor bigint;
|
||
v_ninguno bigint;
|
||
BEGIN
|
||
SELECT * INTO v_f FROM public._dash_parse_filters(p_filters);
|
||
SELECT count(*) INTO v_total
|
||
FROM public._dash_responses(p_survey_id, v_f.f_depto, v_f.f_genero, v_f.f_etario, v_f.f_modalidad);
|
||
IF v_total < 5 THEN
|
||
RETURN jsonb_build_object('status','suppressed','min_n',5,'n',v_total);
|
||
END IF;
|
||
|
||
SELECT count(DISTINCT dr.response_id) INTO v_disc
|
||
FROM public._dash_responses(p_survey_id, v_f.f_depto, v_f.f_genero, v_f.f_etario, v_f.f_modalidad) dr
|
||
JOIN public.answers a ON a.response_id = dr.response_id
|
||
JOIN public.questions q ON q.id = a.question_id
|
||
WHERE q.code = '1.1' AND a.value #>> '{}' = 'Sí';
|
||
|
||
SELECT count(DISTINCT dr.response_id) INTO v_familiar
|
||
FROM public._dash_responses(p_survey_id, v_f.f_depto, v_f.f_genero, v_f.f_etario, v_f.f_modalidad) dr
|
||
JOIN public.answers a ON a.response_id = dr.response_id
|
||
JOIN public.questions q ON q.id = a.question_id
|
||
WHERE q.code = '1.6' AND a.value #>> '{}' = 'Sí';
|
||
|
||
SELECT count(DISTINCT dr.response_id) INTO v_mayor
|
||
FROM public._dash_responses(p_survey_id, v_f.f_depto, v_f.f_genero, v_f.f_etario, v_f.f_modalidad) dr
|
||
JOIN public.answers a ON a.response_id = dr.response_id
|
||
JOIN public.questions q ON q.id = a.question_id
|
||
WHERE q.code = '1.13' AND a.value #>> '{}' = 'Sí';
|
||
|
||
SELECT count(DISTINCT dr.response_id) INTO v_ninguno
|
||
FROM public._dash_responses(p_survey_id, v_f.f_depto, v_f.f_genero, v_f.f_etario, v_f.f_modalidad) dr
|
||
WHERE NOT EXISTS (
|
||
SELECT 1 FROM public.answers a
|
||
JOIN public.questions q ON q.id = a.question_id
|
||
WHERE a.response_id = dr.response_id
|
||
AND q.code IN ('1.1','1.6','1.13')
|
||
AND a.value #>> '{}' = 'Sí'
|
||
);
|
||
|
||
RETURN jsonb_build_object(
|
||
'status', 'ok',
|
||
'n', v_total,
|
||
'disc_propia', ROUND(v_disc::numeric / v_total * 100, 1),
|
||
'familiar', ROUND(v_familiar::numeric / v_total * 100, 1),
|
||
'adulto_mayor', ROUND(v_mayor::numeric / v_total * 100, 1),
|
||
'ninguno', ROUND(v_ninguno::numeric / v_total * 100, 1)
|
||
);
|
||
END;
|
||
$$;
|
||
|
||
-- ── Gap de políticas (barras) ─────────────────────────────────
|
||
CREATE OR REPLACE FUNCTION public.rpc_politicas_gap(
|
||
p_survey_id uuid,
|
||
p_filters jsonb DEFAULT '{}'::jsonb
|
||
)
|
||
RETURNS jsonb
|
||
LANGUAGE plpgsql STABLE SECURITY DEFINER
|
||
SET search_path = public
|
||
AS $$
|
||
DECLARE
|
||
v_f record;
|
||
v_total bigint;
|
||
v_rows jsonb;
|
||
BEGIN
|
||
SELECT * INTO v_f FROM public._dash_parse_filters(p_filters);
|
||
SELECT count(*) INTO v_total
|
||
FROM public._dash_responses(p_survey_id, v_f.f_depto, v_f.f_genero, v_f.f_etario, v_f.f_modalidad);
|
||
IF v_total < 5 THEN
|
||
RETURN jsonb_build_object('status','suppressed','min_n',5,'n',v_total);
|
||
END IF;
|
||
|
||
-- 6.2 = apoyos que ayudarían (necesitadas), 6.2 = ya existentes (same question, max_select=3)
|
||
-- Nota: el instrumento no tiene una pregunta separada "políticas existentes".
|
||
-- Widget 5 muestra TOP políticas demandadas (6.2) para priorización.
|
||
SELECT jsonb_agg(row ORDER BY cnt DESC) INTO v_rows FROM (
|
||
SELECT jsonb_build_object(
|
||
'politica', opt,
|
||
'n', cnt,
|
||
'pct', ROUND(cnt::numeric / v_total * 100, 1)
|
||
) AS row
|
||
FROM (
|
||
SELECT
|
||
jsonb_array_elements_text(a.value) AS opt,
|
||
count(DISTINCT dr.response_id) AS cnt
|
||
FROM public._dash_responses(p_survey_id, v_f.f_depto, v_f.f_genero, v_f.f_etario, v_f.f_modalidad) dr
|
||
JOIN public.answers a ON a.response_id = dr.response_id
|
||
JOIN public.questions q ON q.id = a.question_id
|
||
WHERE q.code = '6.2'
|
||
AND jsonb_typeof(a.value) = 'array'
|
||
GROUP BY 1
|
||
) sub
|
||
WHERE cnt >= 5
|
||
ORDER BY cnt DESC
|
||
LIMIT 8
|
||
) t;
|
||
|
||
RETURN jsonb_build_object('status','ok','n_total',v_total,'data', COALESCE(v_rows,'[]'::jsonb));
|
||
END;
|
||
$$;
|
||
|
||
-- ── Score organizacional (gauge) ──────────────────────────────
|
||
CREATE OR REPLACE FUNCTION public.rpc_score_organizacional(
|
||
p_survey_id uuid,
|
||
p_filters jsonb DEFAULT '{}'::jsonb
|
||
)
|
||
RETURNS jsonb
|
||
LANGUAGE plpgsql STABLE SECURITY DEFINER
|
||
SET search_path = public
|
||
AS $$
|
||
DECLARE
|
||
v_f record;
|
||
v_total bigint;
|
||
v_score_6_1 numeric;
|
||
v_score_4_3 numeric;
|
||
v_score_final numeric;
|
||
v_zona text;
|
||
BEGIN
|
||
SELECT * INTO v_f FROM public._dash_parse_filters(p_filters);
|
||
SELECT count(*) INTO v_total
|
||
FROM public._dash_responses(p_survey_id, v_f.f_depto, v_f.f_genero, v_f.f_etario, v_f.f_modalidad);
|
||
IF v_total < 5 THEN
|
||
RETURN jsonb_build_object('status','suppressed','min_n',5,'n',v_total);
|
||
END IF;
|
||
|
||
-- 6.1: Sí=100, Parcialmente=50, No=0
|
||
SELECT AVG(CASE
|
||
WHEN a.value #>> '{}' = 'Sí, me siento acompañado/a' THEN 100.0
|
||
WHEN a.value #>> '{}' = 'Parcialmente, hay cierta comprensión' THEN 50.0
|
||
ELSE 0.0
|
||
END) INTO v_score_6_1
|
||
FROM public._dash_responses(p_survey_id, v_f.f_depto, v_f.f_genero, v_f.f_etario, v_f.f_modalidad) dr
|
||
JOIN public.answers a ON a.response_id = dr.response_id
|
||
JOIN public.questions q ON q.id = a.question_id
|
||
WHERE q.code = '6.1';
|
||
|
||
-- 4.3: rating 1-5 → 0-100
|
||
SELECT AVG((a.value #>> '{}')::numeric - 1) / 4 * 100 INTO v_score_4_3
|
||
FROM public._dash_responses(p_survey_id, v_f.f_depto, v_f.f_genero, v_f.f_etario, v_f.f_modalidad) dr
|
||
JOIN public.answers a ON a.response_id = dr.response_id
|
||
JOIN public.questions q ON q.id = a.question_id
|
||
WHERE q.code = '4.3' AND a.value #>> '{}' ~ '^[0-9]+$';
|
||
|
||
v_score_final := ROUND(COALESCE(v_score_6_1, 0) * 0.6 + COALESCE(v_score_4_3, 0) * 0.4, 1);
|
||
|
||
v_zona := CASE
|
||
WHEN v_score_final < 34 THEN 'baja'
|
||
WHEN v_score_final < 67 THEN 'desarrollo'
|
||
ELSE 'consciente'
|
||
END;
|
||
|
||
RETURN jsonb_build_object('status','ok','n',v_total,'score',v_score_final,'zona',v_zona);
|
||
END;
|
||
$$;
|
||
|
||
-- ── Pirámide demográfica ──────────────────────────────────────
|
||
CREATE OR REPLACE FUNCTION public.rpc_piramide_demografica(
|
||
p_survey_id uuid,
|
||
p_filters jsonb DEFAULT '{}'::jsonb
|
||
)
|
||
RETURNS jsonb
|
||
LANGUAGE plpgsql STABLE SECURITY DEFINER
|
||
SET search_path = public
|
||
AS $$
|
||
DECLARE
|
||
v_f record;
|
||
v_total bigint;
|
||
v_rows jsonb;
|
||
BEGIN
|
||
SELECT * INTO v_f FROM public._dash_parse_filters(p_filters);
|
||
SELECT count(*) INTO v_total
|
||
FROM public._dash_responses(p_survey_id, v_f.f_depto, v_f.f_genero, v_f.f_etario, v_f.f_modalidad);
|
||
IF v_total < 5 THEN
|
||
RETURN jsonb_build_object('status','suppressed','min_n',5,'n',v_total);
|
||
END IF;
|
||
|
||
SELECT jsonb_agg(row) INTO v_rows FROM (
|
||
SELECT jsonb_build_object(
|
||
'etario', etario_val,
|
||
'genero', genero_val,
|
||
'n', CASE WHEN cnt >= 5 THEN cnt ELSE NULL END,
|
||
'suppressed', (cnt < 5)
|
||
) AS row
|
||
FROM (
|
||
SELECT
|
||
ae.value #>> '{}' AS etario_val,
|
||
ag.value #>> '{}' AS genero_val,
|
||
count(DISTINCT dr.response_id) AS cnt
|
||
FROM public._dash_responses(p_survey_id, v_f.f_depto, v_f.f_genero, v_f.f_etario, v_f.f_modalidad) dr
|
||
JOIN public.answers ae ON ae.response_id = dr.response_id
|
||
JOIN public.questions qe ON qe.id = ae.question_id AND qe.code = 'A5'
|
||
JOIN public.answers ag ON ag.response_id = dr.response_id
|
||
JOIN public.questions qg ON qg.id = ag.question_id AND qg.code = 'A6'
|
||
GROUP BY 1, 2
|
||
) sub
|
||
ORDER BY etario_val, genero_val
|
||
) t;
|
||
|
||
RETURN jsonb_build_object('status','ok','n_total',v_total,'data', COALESCE(v_rows,'[]'::jsonb));
|
||
END;
|
||
$$;
|
||
|
||
-- ── Intensidad de cuidado vs. bienestar (hexbin) ─────────────
|
||
CREATE OR REPLACE FUNCTION public.rpc_intensidad_vs_productividad(
|
||
p_survey_id uuid,
|
||
p_filters jsonb DEFAULT '{}'::jsonb
|
||
)
|
||
RETURNS jsonb
|
||
LANGUAGE plpgsql STABLE SECURITY DEFINER
|
||
SET search_path = public
|
||
AS $$
|
||
DECLARE
|
||
v_f record;
|
||
v_total bigint;
|
||
v_rows jsonb;
|
||
BEGIN
|
||
SELECT * INTO v_f FROM public._dash_parse_filters(p_filters);
|
||
SELECT count(*) INTO v_total
|
||
FROM public._dash_responses(p_survey_id, v_f.f_depto, v_f.f_genero, v_f.f_etario, v_f.f_modalidad);
|
||
IF v_total < 5 THEN
|
||
RETURN jsonb_build_object('status','suppressed','min_n',5,'n',v_total);
|
||
END IF;
|
||
|
||
SELECT jsonb_agg(row) INTO v_rows FROM (
|
||
SELECT jsonb_build_object(
|
||
'horas', horas_bin,
|
||
'bienestar', bien_bin,
|
||
'n', CASE WHEN cnt >= 5 THEN cnt ELSE NULL END,
|
||
'suppressed', (cnt < 5)
|
||
) AS row
|
||
FROM (
|
||
SELECT
|
||
ah.value #>> '{}' AS horas_bin,
|
||
ab.value #>> '{}' AS bien_bin,
|
||
count(DISTINCT dr.response_id) AS cnt
|
||
FROM public._dash_responses(p_survey_id, v_f.f_depto, v_f.f_genero, v_f.f_etario, v_f.f_modalidad) dr
|
||
JOIN public.answers ah ON ah.response_id = dr.response_id
|
||
JOIN public.questions qh ON qh.id = ah.question_id AND qh.code = '2.2'
|
||
JOIN public.answers ab ON ab.response_id = dr.response_id
|
||
JOIN public.questions qb ON qb.id = ab.question_id AND qb.code = '4.3'
|
||
WHERE ab.value #>> '{}' ~ '^[0-9]+$'
|
||
GROUP BY 1, 2
|
||
) sub
|
||
ORDER BY horas_bin, bien_bin
|
||
) t;
|
||
|
||
RETURN jsonb_build_object('status','ok','n_total',v_total,'data', COALESCE(v_rows,'[]'::jsonb));
|
||
END;
|
||
$$;
|
||
|
||
-- ── Tabla de riesgo de retención ──────────────────────────────
|
||
-- Sin columna de departamento (vector de reidentificación en N pequeños).
|
||
CREATE OR REPLACE FUNCTION public.rpc_riesgo_retencion(
|
||
p_survey_id uuid,
|
||
p_filters jsonb DEFAULT '{}'::jsonb
|
||
)
|
||
RETURNS jsonb
|
||
LANGUAGE plpgsql STABLE SECURITY DEFINER
|
||
SET search_path = public
|
||
AS $$
|
||
DECLARE
|
||
v_f record;
|
||
v_total bigint;
|
||
v_n_alto bigint;
|
||
v_n_medio bigint;
|
||
v_n_bajo bigint;
|
||
v_pol_alto text;
|
||
v_pol_medio text;
|
||
v_pol_bajo text;
|
||
BEGIN
|
||
SELECT * INTO v_f FROM public._dash_parse_filters(p_filters);
|
||
SELECT count(*) INTO v_total
|
||
FROM public._dash_responses(p_survey_id, v_f.f_depto, v_f.f_genero, v_f.f_etario, v_f.f_modalidad);
|
||
IF v_total < 5 THEN
|
||
RETURN jsonb_build_object('status','suppressed','min_n',5,'n',v_total);
|
||
END IF;
|
||
|
||
-- Alto: ausencias frecuentes AND empresa no comprende
|
||
SELECT count(DISTINCT dr.response_id) INTO v_n_alto
|
||
FROM public._dash_responses(p_survey_id, v_f.f_depto, v_f.f_genero, v_f.f_etario, v_f.f_modalidad) dr
|
||
WHERE EXISTS (
|
||
SELECT 1 FROM public.answers a JOIN public.questions q ON q.id = a.question_id
|
||
WHERE a.response_id = dr.response_id AND q.code = '5.2'
|
||
AND a.value #>> '{}' IN ('Varias veces (4-10 veces al año)','Con frecuencia (más de 10 veces al año)')
|
||
)
|
||
AND EXISTS (
|
||
SELECT 1 FROM public.answers a JOIN public.questions q ON q.id = a.question_id
|
||
WHERE a.response_id = dr.response_id AND q.code = '6.1'
|
||
AND a.value #>> '{}' IN ('No, es algo bastante invisible','No, nunca se ha hablado del tema')
|
||
);
|
||
|
||
-- Medio: exactamente una de las dos condiciones (XOR en SQL estándar)
|
||
SELECT count(DISTINCT dr.response_id) INTO v_n_medio
|
||
FROM public._dash_responses(p_survey_id, v_f.f_depto, v_f.f_genero, v_f.f_etario, v_f.f_modalidad) dr
|
||
WHERE (
|
||
EXISTS (
|
||
SELECT 1 FROM public.answers a JOIN public.questions q ON q.id = a.question_id
|
||
WHERE a.response_id = dr.response_id AND q.code = '5.2'
|
||
AND a.value #>> '{}' IN ('Varias veces (4-10 veces al año)','Con frecuencia (más de 10 veces al año)')
|
||
)
|
||
AND NOT EXISTS (
|
||
SELECT 1 FROM public.answers a JOIN public.questions q ON q.id = a.question_id
|
||
WHERE a.response_id = dr.response_id AND q.code = '6.1'
|
||
AND a.value #>> '{}' IN ('No, es algo bastante invisible','No, nunca se ha hablado del tema')
|
||
)
|
||
) OR (
|
||
NOT EXISTS (
|
||
SELECT 1 FROM public.answers a JOIN public.questions q ON q.id = a.question_id
|
||
WHERE a.response_id = dr.response_id AND q.code = '5.2'
|
||
AND a.value #>> '{}' IN ('Varias veces (4-10 veces al año)','Con frecuencia (más de 10 veces al año)')
|
||
)
|
||
AND EXISTS (
|
||
SELECT 1 FROM public.answers a JOIN public.questions q ON q.id = a.question_id
|
||
WHERE a.response_id = dr.response_id AND q.code = '6.1'
|
||
AND a.value #>> '{}' IN ('No, es algo bastante invisible','No, nunca se ha hablado del tema')
|
||
)
|
||
);
|
||
|
||
v_n_bajo := v_total - v_n_alto - v_n_medio;
|
||
|
||
-- Política más demandada por segmento (solo si N>=5)
|
||
IF v_n_alto >= 5 THEN
|
||
SELECT jsonb_array_elements_text(a.value) INTO v_pol_alto
|
||
FROM public._dash_responses(p_survey_id, v_f.f_depto, v_f.f_genero, v_f.f_etario, v_f.f_modalidad) dr
|
||
JOIN public.answers a ON a.response_id = dr.response_id
|
||
JOIN public.questions q ON q.id = a.question_id
|
||
WHERE q.code = '6.2' AND jsonb_typeof(a.value) = 'array'
|
||
AND EXISTS (
|
||
SELECT 1 FROM public.answers a2 JOIN public.questions q2 ON q2.id = a2.question_id
|
||
WHERE a2.response_id = dr.response_id AND q2.code = '5.2'
|
||
AND a2.value #>> '{}' IN ('Varias veces (4-10 veces al año)','Con frecuencia (más de 10 veces al año)')
|
||
)
|
||
AND EXISTS (
|
||
SELECT 1 FROM public.answers a3 JOIN public.questions q3 ON q3.id = a3.question_id
|
||
WHERE a3.response_id = dr.response_id AND q3.code = '6.1'
|
||
AND a3.value #>> '{}' IN ('No, es algo bastante invisible','No, nunca se ha hablado del tema')
|
||
)
|
||
GROUP BY jsonb_array_elements_text(a.value)
|
||
ORDER BY count(*) DESC LIMIT 1;
|
||
END IF;
|
||
|
||
RETURN jsonb_build_object(
|
||
'status', 'ok',
|
||
'n_total', v_total,
|
||
'segmentos', jsonb_build_array(
|
||
jsonb_build_object(
|
||
'segmento', 'Alto',
|
||
'n', CASE WHEN v_n_alto >= 5 THEN v_n_alto ELSE NULL END,
|
||
'pct', CASE WHEN v_n_alto >= 5 THEN ROUND(v_n_alto::numeric/v_total*100,1) ELSE NULL END,
|
||
'politica', CASE WHEN v_n_alto >= 5 THEN v_pol_alto ELSE NULL END,
|
||
'suppressed', (v_n_alto < 5)
|
||
),
|
||
jsonb_build_object(
|
||
'segmento', 'Medio',
|
||
'n', CASE WHEN v_n_medio >= 5 THEN v_n_medio ELSE NULL END,
|
||
'pct', CASE WHEN v_n_medio >= 5 THEN ROUND(v_n_medio::numeric/v_total*100,1) ELSE NULL END,
|
||
'politica', v_pol_medio,
|
||
'suppressed', (v_n_medio < 5)
|
||
),
|
||
jsonb_build_object(
|
||
'segmento', 'Bajo',
|
||
'n', CASE WHEN v_n_bajo >= 5 THEN v_n_bajo ELSE NULL END,
|
||
'pct', CASE WHEN v_n_bajo >= 5 THEN ROUND(v_n_bajo::numeric/v_total*100,1) ELSE NULL END,
|
||
'politica', v_pol_bajo,
|
||
'suppressed', (v_n_bajo < 5)
|
||
)
|
||
)
|
||
);
|
||
END;
|
||
$$;
|