F6.2 Lote G: funcoes SQL puras -> plpgsql + roteamento (completa F6.2)
DB (supabase_admin, manual/f6_2g_sql_to_plpgsql.supabase_admin.sql): SQL puro nao permite set_config dinamico -> converte 5 pra plpgsql + p_tenant_id + _tenant_route: - get_financial_summary, get_financial_report, get_patient_session_counts (remove filtro tenant_id IN, schema-scoped) - list_financial_records: RETURNS SETOF financial_records -> jsonb (array, transparente no FE) - get_entity_primary_phone (interno, 0 callers): herda search_path do chamador (sem SET, unqualified) Smoke: get_financial_summary + list_financial_records (array 5) OK. Frontend (3 arquivos): p_tenant_id de activeTenantId/resolveTenantId nas chamadas de get_financial_summary/list_financial_records/get_patient_session_ counts. Build passa. === F6.2 COMPLETA: 66 funcoes migradas (triggers A/B/C + RPCs D/E/F/G) === Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
-- =============================================================================
|
||||
-- F6.2 Lote G — funções SQL puras → plpgsql + roteamento por tenant
|
||||
--
|
||||
-- ⚠️ APLICAR COMO supabase_admin.
|
||||
--
|
||||
-- SQL puro não permite set_config dinâmico do search_path (limitação 3 do
|
||||
-- blueprint) → converter pra plpgsql. Adicionam p_tenant_id + _tenant_route.
|
||||
-- RETURNS SETOF <tabela_tenant> → jsonb. get_entity_primary_phone (interno,
|
||||
-- 0 callers) herda search_path do chamador (sem SET, unqualified).
|
||||
-- =============================================================================
|
||||
|
||||
BEGIN;
|
||||
|
||||
DROP FUNCTION IF EXISTS public.get_financial_summary(uuid, integer, integer);
|
||||
DROP FUNCTION IF EXISTS public.get_financial_summary(uuid, uuid, integer, integer);
|
||||
CREATE FUNCTION public.get_financial_summary(p_tenant_id uuid, p_owner_id uuid, p_year integer, p_month integer)
|
||||
RETURNS TABLE(total_receitas numeric, total_despesas numeric, total_pendente numeric, saldo_liquido numeric, total_repasse numeric, count_receitas bigint, count_despesas bigint)
|
||||
LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public','pg_temp'
|
||||
AS $$
|
||||
BEGIN
|
||||
PERFORM set_config('search_path', public._tenant_route(p_tenant_id) || ',public,pg_temp', true);
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
COALESCE(SUM(amount) FILTER (WHERE type='receita' AND status='paid'), 0),
|
||||
COALESCE(SUM(amount) FILTER (WHERE type='despesa' AND status='paid'), 0),
|
||||
COALESCE(SUM(amount) FILTER (WHERE status IN ('pending','overdue')), 0),
|
||||
COALESCE(SUM(amount) FILTER (WHERE type='receita' AND status='paid'), 0) - COALESCE(SUM(amount) FILTER (WHERE type='despesa' AND status='paid'), 0),
|
||||
COALESCE(SUM(clinic_fee_amount) FILTER (WHERE type='receita' AND status='paid'), 0),
|
||||
COUNT(*) FILTER (WHERE type='receita' AND deleted_at IS NULL),
|
||||
COUNT(*) FILTER (WHERE type='despesa' AND deleted_at IS NULL)
|
||||
FROM financial_records
|
||||
WHERE owner_id = p_owner_id AND deleted_at IS NULL
|
||||
AND EXTRACT(YEAR FROM COALESCE(paid_at::date, due_date, created_at::date)) = p_year
|
||||
AND EXTRACT(MONTH FROM COALESCE(paid_at::date, due_date, created_at::date)) = p_month;
|
||||
END $$;
|
||||
|
||||
-- list_financial_records: RETURNS SETOF financial_records → jsonb (array)
|
||||
DROP FUNCTION IF EXISTS public.list_financial_records(uuid, integer, integer, text, text, uuid, integer, integer);
|
||||
DROP FUNCTION IF EXISTS public.list_financial_records(uuid, uuid, integer, integer, text, text, uuid, integer, integer);
|
||||
CREATE FUNCTION public.list_financial_records(p_tenant_id uuid, p_owner_id uuid, p_year integer DEFAULT NULL, p_month integer DEFAULT NULL, p_type text DEFAULT NULL, p_status text DEFAULT NULL, p_patient_id uuid DEFAULT NULL, p_limit integer DEFAULT 50, p_offset integer DEFAULT 0)
|
||||
RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public','pg_temp'
|
||||
AS $$
|
||||
DECLARE v_result jsonb;
|
||||
BEGIN
|
||||
PERFORM set_config('search_path', public._tenant_route(p_tenant_id) || ',public,pg_temp', true);
|
||||
SELECT COALESCE(jsonb_agg(row_json), '[]'::jsonb) INTO v_result FROM (
|
||||
SELECT to_jsonb(fr) AS row_json
|
||||
FROM financial_records fr
|
||||
WHERE fr.owner_id = p_owner_id AND fr.deleted_at IS NULL
|
||||
AND (p_type IS NULL OR fr.type::text = p_type)
|
||||
AND (p_status IS NULL OR fr.status = p_status)
|
||||
AND (p_patient_id IS NULL OR fr.patient_id = p_patient_id)
|
||||
AND (p_year IS NULL OR EXTRACT(YEAR FROM COALESCE(fr.paid_at::date, fr.due_date, fr.created_at::date)) = p_year)
|
||||
AND (p_month IS NULL OR EXTRACT(MONTH FROM COALESCE(fr.paid_at::date, fr.due_date, fr.created_at::date)) = p_month)
|
||||
ORDER BY COALESCE(fr.paid_at, fr.due_date::timestamptz, fr.created_at) DESC
|
||||
LIMIT p_limit OFFSET p_offset
|
||||
) sub;
|
||||
RETURN v_result;
|
||||
END $$;
|
||||
|
||||
DROP FUNCTION IF EXISTS public.get_patient_session_counts(uuid[]);
|
||||
DROP FUNCTION IF EXISTS public.get_patient_session_counts(uuid, uuid[]);
|
||||
CREATE FUNCTION public.get_patient_session_counts(p_tenant_id uuid, p_patient_ids uuid[])
|
||||
RETURNS TABLE(patient_id uuid, session_count integer, last_session_at timestamptz)
|
||||
LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public','pg_temp'
|
||||
AS $$
|
||||
BEGIN
|
||||
PERFORM set_config('search_path', public._tenant_route(p_tenant_id) || ',public,pg_temp', true);
|
||||
RETURN QUERY
|
||||
SELECT ae.patient_id, COUNT(*)::int, MAX(ae.inicio_em)
|
||||
FROM agenda_eventos ae
|
||||
WHERE ae.patient_id = ANY(p_patient_ids)
|
||||
GROUP BY ae.patient_id;
|
||||
END $$;
|
||||
|
||||
DROP FUNCTION IF EXISTS public.get_financial_report(uuid, date, date, text);
|
||||
DROP FUNCTION IF EXISTS public.get_financial_report(uuid, uuid, date, date, text);
|
||||
CREATE FUNCTION public.get_financial_report(p_tenant_id uuid, p_owner_id uuid, p_start_date date, p_end_date date, p_group_by text DEFAULT 'month')
|
||||
RETURNS TABLE(group_key text, group_label text, total_receitas numeric, total_despesas numeric, saldo numeric, total_pendente numeric, total_overdue numeric, count_records bigint)
|
||||
LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public','pg_temp'
|
||||
AS $$
|
||||
BEGIN
|
||||
PERFORM set_config('search_path', public._tenant_route(p_tenant_id) || ',public,pg_temp', true);
|
||||
RETURN QUERY
|
||||
WITH base AS (
|
||||
SELECT fr.type, fr.amount, fr.final_amount, fr.status, fr.deleted_at,
|
||||
CASE p_group_by
|
||||
WHEN 'month' THEN TO_CHAR(COALESCE(fr.paid_at::date, fr.due_date, fr.created_at::date),'YYYY-MM')
|
||||
WHEN 'week' THEN TO_CHAR(COALESCE(fr.paid_at::date, fr.due_date, fr.created_at::date),'IYYY-"W"IW')
|
||||
WHEN 'category' THEN COALESCE(fr.category_id::text, fr.category, 'sem_categoria')
|
||||
WHEN 'patient' THEN COALESCE(fr.patient_id::text, 'sem_paciente')
|
||||
ELSE NULL END AS gkey,
|
||||
CASE p_group_by
|
||||
WHEN 'month' THEN TO_CHAR(COALESCE(fr.paid_at::date, fr.due_date, fr.created_at::date),'YYYY-MM')
|
||||
WHEN 'week' THEN TO_CHAR(COALESCE(fr.paid_at::date, fr.due_date, fr.created_at::date),'IYYY-"W"IW')
|
||||
WHEN 'category' THEN COALESCE(fc.name, fr.category, 'Sem categoria')
|
||||
WHEN 'patient' THEN COALESCE(p.nome_completo, fr.patient_id::text, 'Sem paciente')
|
||||
ELSE NULL END AS glabel
|
||||
FROM financial_records fr
|
||||
LEFT JOIN financial_categories fc ON fc.id = fr.category_id
|
||||
LEFT JOIN patients p ON p.id = fr.patient_id
|
||||
WHERE fr.owner_id = p_owner_id AND fr.deleted_at IS NULL
|
||||
AND COALESCE(fr.paid_at::date, fr.due_date, fr.created_at::date) BETWEEN p_start_date AND p_end_date
|
||||
)
|
||||
SELECT gkey, glabel,
|
||||
COALESCE(SUM(final_amount) FILTER (WHERE type='receita' AND status='paid'),0),
|
||||
COALESCE(SUM(final_amount) FILTER (WHERE type='despesa' AND status='paid'),0),
|
||||
COALESCE(SUM(final_amount) FILTER (WHERE type='receita' AND status='paid'),0) - COALESCE(SUM(final_amount) FILTER (WHERE type='despesa' AND status='paid'),0),
|
||||
COALESCE(SUM(final_amount) FILTER (WHERE status='pending'),0),
|
||||
COALESCE(SUM(final_amount) FILTER (WHERE status='overdue'),0),
|
||||
COUNT(*)
|
||||
FROM base WHERE gkey IS NOT NULL GROUP BY gkey, glabel ORDER BY gkey ASC;
|
||||
END $$;
|
||||
|
||||
-- get_entity_primary_phone: interno (0 callers). Sem SET search_path → herda do
|
||||
-- chamador (que roteia pro schema). Unqualified. Mantém assinatura.
|
||||
CREATE OR REPLACE FUNCTION public.get_entity_primary_phone(p_entity_type text, p_entity_id uuid)
|
||||
RETURNS text LANGUAGE sql STABLE SECURITY DEFINER
|
||||
AS $$
|
||||
SELECT number FROM contact_phones
|
||||
WHERE entity_type = p_entity_type AND entity_id = p_entity_id
|
||||
ORDER BY is_primary DESC, position ASC, created_at ASC
|
||||
LIMIT 1;
|
||||
$$;
|
||||
|
||||
COMMIT;
|
||||
@@ -21,8 +21,10 @@ import { useRouter } from 'vue-router';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
// ─── helpers ─────────────────────────────────────────────────────────────────
|
||||
const router = useRouter();
|
||||
const tenantStore = useTenantStore();
|
||||
|
||||
const _brl = new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' });
|
||||
function fmtBRL(v) {
|
||||
@@ -67,6 +69,7 @@ async function loadSummary(uid) {
|
||||
try {
|
||||
// Receitas e despesas pagas no mês via RPC
|
||||
const { data: rpc } = await supabase.rpc('get_financial_summary', {
|
||||
p_tenant_id: tenantStore.activeTenantId,
|
||||
p_owner_id: uid,
|
||||
p_year: year,
|
||||
p_month: month
|
||||
@@ -117,7 +120,7 @@ async function loadChart(uid) {
|
||||
chartLoading.value = true;
|
||||
const months = getLast6Months();
|
||||
try {
|
||||
const results = await Promise.all(months.map((m) => supabase.rpc('get_financial_summary', { p_owner_id: uid, p_year: m.year, p_month: m.month })));
|
||||
const results = await Promise.all(months.map((m) => supabase.rpc('get_financial_summary', { p_tenant_id: tenantStore.activeTenantId, p_owner_id: uid, p_year: m.year, p_month: m.month })));
|
||||
const receitas = results.map((r) => Number((Array.isArray(r.data) ? r.data[0] : r.data)?.total_receitas ?? 0));
|
||||
const despesas = results.map((r) => Number((Array.isArray(r.data) ? r.data[0] : r.data)?.total_despesas ?? 0));
|
||||
|
||||
@@ -169,6 +172,7 @@ async function loadRecent(uid) {
|
||||
recentLoading.value = true;
|
||||
try {
|
||||
const { data } = await supabase.rpc('list_financial_records', {
|
||||
p_tenant_id: tenantStore.activeTenantId,
|
||||
p_owner_id: uid,
|
||||
p_limit: 5,
|
||||
p_offset: 0
|
||||
|
||||
@@ -510,7 +510,8 @@ export async function markIntakeConverted(intakeId, patientId, { tenantId } = {}
|
||||
*/
|
||||
export async function getSessionCounts(patientIds) {
|
||||
if (!patientIds?.length) return [];
|
||||
const { data, error } = await supabase.rpc('get_patient_session_counts', { p_patient_ids: patientIds });
|
||||
const tid = resolveTenantId();
|
||||
const { data, error } = await supabase.rpc('get_patient_session_counts', { p_tenant_id: tid, p_patient_ids: patientIds });
|
||||
if (error) throw error;
|
||||
return data || [];
|
||||
}
|
||||
|
||||
@@ -19,10 +19,12 @@ import { ref, computed, onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { tenantDb } from '@/lib/supabase/tenantClient';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
// Chart/DataTable/Column/Skeleton/Tag: auto via PrimeVueResolver
|
||||
|
||||
const emit = defineEmits(['close']);
|
||||
const router = useRouter();
|
||||
const tenantStore = useTenantStore();
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────
|
||||
const _brl = new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' });
|
||||
@@ -72,7 +74,7 @@ async function loadSummary(uid) {
|
||||
const month = now.getMonth() + 1;
|
||||
try {
|
||||
const { data: rpc } = await supabase.rpc('get_financial_summary', {
|
||||
p_owner_id: uid, p_year: year, p_month: month
|
||||
p_tenant_id: tenantStore.activeTenantId, p_owner_id: uid, p_year: year, p_month: month
|
||||
});
|
||||
const s = Array.isArray(rpc) ? rpc[0] : rpc;
|
||||
totalRecebido.value = Number(s?.total_receitas ?? 0);
|
||||
@@ -122,7 +124,7 @@ async function loadChart(uid) {
|
||||
try {
|
||||
const results = await Promise.all(
|
||||
months.map((m) => supabase.rpc('get_financial_summary', {
|
||||
p_owner_id: uid, p_year: m.year, p_month: m.month
|
||||
p_tenant_id: tenantStore.activeTenantId, p_owner_id: uid, p_year: m.year, p_month: m.month
|
||||
}))
|
||||
);
|
||||
const receitas = results.map((r) => Number((Array.isArray(r.data) ? r.data[0] : r.data)?.total_receitas ?? 0));
|
||||
@@ -178,7 +180,7 @@ async function loadRecent(uid) {
|
||||
recentLoading.value = true;
|
||||
try {
|
||||
const { data } = await supabase.rpc('list_financial_records', {
|
||||
p_owner_id: uid, p_limit: 5, p_offset: 0
|
||||
p_tenant_id: tenantStore.activeTenantId, p_owner_id: uid, p_limit: 5, p_offset: 0
|
||||
});
|
||||
recentRecords.value = data ?? [];
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user