Correcao Sidebar Classico e Rail, Correcao Layout, Ajuste de Breakpoint para Tailwind, Ajuste AppTopbar, Ajuste Menu PopOver, Recriado Paleta de Cores, Inserido algumas animações leves, Reajuste Cor items NOVOS da tabela, Drawer Ajuda Corrigido no Logout, Whatsapp, sms, email, recursos extras

This commit is contained in:
Leonardo
2026-03-24 21:26:58 -03:00
parent a89d1f5560
commit 53a4980396
453 changed files with 121427 additions and 174407 deletions
+183 -199
View File
@@ -29,243 +29,227 @@
* await handleStatusChange(eventoOriginal, novoStatus, agendaEvents.update)
*/
import { ref } from 'vue'
import { supabase } from '@/lib/supabase/client'
import { useTenantStore } from '@/stores/tenantStore'
import { ref } from 'vue';
import { supabase } from '@/lib/supabase/client';
import { useTenantStore } from '@/stores/tenantStore';
// ─── Cache de exceções financeiras (vive enquanto o módulo estiver carregado) ─
// Chave: `${tenantId}:${exceptionType}` → FinancialException | null
const _exceptionsCache = new Map()
const _exceptionsCache = new Map();
// ─── helper ──────────────────────────────────────────────────────────────────
async function getUid () {
const { data, error } = await supabase.auth.getUser()
if (error) throw error
const uid = data?.user?.id
if (!uid) throw new Error('Usuário não autenticado.')
return uid
async function getUid() {
const { data, error } = await supabase.auth.getUser();
if (error) throw error;
const uid = data?.user?.id;
if (!uid) throw new Error('Usuário não autenticado.');
return uid;
}
// ─── mapeamento: status anterior → tipo de exceção a consultar ───────────────
const STATUS_TO_EXCEPTION = {
faltou: 'patient_no_show',
cancelado: 'patient_cancellation',
}
faltou: 'patient_no_show',
cancelado: 'patient_cancellation'
};
// ─── calcular valor cobrado por charge_mode ───────────────────────────────────
function calcChargeAmount (originalAmount, rule) {
if (!rule || rule.charge_mode === 'none') return 0
if (rule.charge_mode === 'full') return originalAmount
if (rule.charge_mode === 'fixed') return rule.charge_value ?? 0
if (rule.charge_mode === 'percentage') {
const pct = rule.charge_pct ?? 0
return parseFloat(((originalAmount * pct) / 100).toFixed(2))
}
return originalAmount
function calcChargeAmount(originalAmount, rule) {
if (!rule || rule.charge_mode === 'none') return 0;
if (rule.charge_mode === 'full') return originalAmount;
if (rule.charge_mode === 'fixed') return rule.charge_value ?? 0;
if (rule.charge_mode === 'percentage') {
const pct = rule.charge_pct ?? 0;
return parseFloat(((originalAmount * pct) / 100).toFixed(2));
}
return originalAmount;
}
// ─── composable ──────────────────────────────────────────────────────────────
export function useAgendaFinanceiro () {
const loading = ref(false)
const error = ref(null)
export function useAgendaFinanceiro() {
const loading = ref(false);
const error = ref(null);
// ── getFinancialExceptionRule ─────────────────────────────────────────────
/**
* Busca a regra de exceção financeira para um tipo, com cache em memória.
* Prioridade: regra própria do owner > regra global do tenant (owner_id IS NULL)
*
* @param {string} tenantId
* @param {'patient_no_show'|'patient_cancellation'|'professional_cancellation'} exceptionType
* @returns {Promise<Object|null>}
*/
async function getFinancialExceptionRule (tenantId, exceptionType) {
const cacheKey = `${tenantId}:${exceptionType}`
if (_exceptionsCache.has(cacheKey)) return _exceptionsCache.get(cacheKey)
// ── getFinancialExceptionRule ─────────────────────────────────────────────
/**
* Busca a regra de exceção financeira para um tipo, com cache em memória.
* Prioridade: regra própria do owner > regra global do tenant (owner_id IS NULL)
*
* @param {string} tenantId
* @param {'patient_no_show'|'patient_cancellation'|'professional_cancellation'} exceptionType
* @returns {Promise<Object|null>}
*/
async function getFinancialExceptionRule(tenantId, exceptionType) {
const cacheKey = `${tenantId}:${exceptionType}`;
if (_exceptionsCache.has(cacheKey)) return _exceptionsCache.get(cacheKey);
const uid = await getUid()
const uid = await getUid();
const { data, error: err } = await supabase
.from('financial_exceptions')
.select('*')
.eq('tenant_id', tenantId)
.eq('exception_type', exceptionType)
.or(`owner_id.eq.${uid},owner_id.is.null`)
.order('owner_id', { ascending: false, nullsLast: true }) // owner próprio tem prioridade
.limit(1)
.maybeSingle()
const { data, error: err } = await supabase
.from('financial_exceptions')
.select('*')
.eq('tenant_id', tenantId)
.eq('exception_type', exceptionType)
.or(`owner_id.eq.${uid},owner_id.is.null`)
.order('owner_id', { ascending: false, nullsLast: true }) // owner próprio tem prioridade
.limit(1)
.maybeSingle();
if (err) {
console.warn('[useAgendaFinanceiro] getFinancialExceptionRule:', err.message)
return null
if (err) {
console.warn('[useAgendaFinanceiro] getFinancialExceptionRule:', err.message);
return null;
}
_exceptionsCache.set(cacheKey, data ?? null);
return data ?? null;
}
_exceptionsCache.set(cacheKey, data ?? null)
return data ?? null
}
// ── gerarCobrancaManual ───────────────────────────────────────────────────
/**
* Gera cobrança para uma sessão existente com `billed = false`.
* Chama a RPC `create_financial_record_for_session`.
*
* @param {Object} evento - linha de agenda_eventos (com campo price)
* @returns {Promise<{ok: boolean, data?: Object, error?: string}>}
*/
async function gerarCobrancaManual(evento) {
if (evento.billing_contract_id) {
// sessão de pacote — não gera cobrança individual
return { ok: false, error: 'Sessão faz parte de um pacote. Cobrança gerenciada pelo contrato.' };
}
// ── gerarCobrancaManual ───────────────────────────────────────────────────
/**
* Gera cobrança para uma sessão existente com `billed = false`.
* Chama a RPC `create_financial_record_for_session`.
*
* @param {Object} evento - linha de agenda_eventos (com campo price)
* @returns {Promise<{ok: boolean, data?: Object, error?: string}>}
*/
async function gerarCobrancaManual (evento) {
if (evento.billing_contract_id) {
// sessão de pacote — não gera cobrança individual
return { ok: false, error: 'Sessão faz parte de um pacote. Cobrança gerenciada pelo contrato.' }
const tenantStore = useTenantStore();
const tenantId = tenantStore.activeTenantId;
if (!tenantId) return { ok: false, error: 'Tenant não identificado.' };
const ownerId = await getUid();
loading.value = true;
error.value = null;
try {
const amount = evento.price ?? 0;
const dueDate = evento.inicio_em ? new Date(evento.inicio_em).toISOString().slice(0, 10) : new Date().toISOString().slice(0, 10);
const { data, error: err } = await supabase.rpc('create_financial_record_for_session', {
p_tenant_id: tenantId,
p_owner_id: ownerId,
p_patient_id: evento.patient_id ?? evento.paciente_id ?? null,
p_agenda_evento_id: evento.id,
p_amount: amount,
p_due_date: dueDate
});
if (err) throw err;
return { ok: true, data };
} catch (e) {
error.value = e?.message || 'Erro ao gerar cobrança.';
return { ok: false, error: error.value };
} finally {
loading.value = false;
}
}
const tenantStore = useTenantStore()
const tenantId = tenantStore.activeTenantId
if (!tenantId) return { ok: false, error: 'Tenant não identificado.' }
// ── handleStatusChange ────────────────────────────────────────────────────
/**
* Orquestra a mudança de status de uma sessão + consequências financeiras.
*
* @param {Object} evento - linha atual de agenda_eventos (ANTES da mudança)
* @param {string} novoStatus - novo status a aplicar
* @param {Function} agendaUpdateFn - função que aplica o update na agenda (ex: agendaEvents.update)
* signature: (id, patch) => Promise<void>
* @returns {Promise<{ok: boolean, error?: string}>}
*/
async function handleStatusChange(evento, novoStatus, agendaUpdateFn) {
// bloqueios e sessões de pacote não têm cobrança individual
const ignorar = evento.tipo !== 'sessao' || !!evento.billing_contract_id;
const statusAnterior = evento.status;
const ownerId = await getUid()
loading.value = true;
error.value = null;
loading.value = true
error.value = null
try {
// 1. Aplica o update na agenda sempre (fonte da verdade é a agenda)
await agendaUpdateFn(evento.id, { status: novoStatus });
try {
const amount = evento.price ?? 0
const dueDate = evento.inicio_em
? new Date(evento.inicio_em).toISOString().slice(0, 10)
: new Date().toISOString().slice(0, 10)
if (ignorar) return { ok: true };
if (statusAnterior === novoStatus) return { ok: true };
const { data, error: err } = await supabase.rpc('create_financial_record_for_session', {
p_tenant_id: tenantId,
p_owner_id: ownerId,
p_patient_id: evento.patient_id ?? evento.paciente_id ?? null,
p_agenda_evento_id: evento.id,
p_amount: amount,
p_due_date: dueDate,
})
// 2. Lógica financeira por transição
const tenantStore = useTenantStore();
const tenantId = tenantStore.activeTenantId;
if (err) throw err
// ── faltou / cancelado → consultar exceção financeira ──────────────
const exceptionType = STATUS_TO_EXCEPTION[novoStatus];
return { ok: true, data }
} catch (e) {
error.value = e?.message || 'Erro ao gerar cobrança.'
return { ok: false, error: error.value }
} finally {
loading.value = false
}
}
if (exceptionType) {
const rule = await getFinancialExceptionRule(tenantId, exceptionType);
// ── handleStatusChange ────────────────────────────────────────────────────
/**
* Orquestra a mudança de status de uma sessão + consequências financeiras.
*
* @param {Object} evento - linha atual de agenda_eventos (ANTES da mudança)
* @param {string} novoStatus - novo status a aplicar
* @param {Function} agendaUpdateFn - função que aplica o update na agenda (ex: agendaEvents.update)
* signature: (id, patch) => Promise<void>
* @returns {Promise<{ok: boolean, error?: string}>}
*/
async function handleStatusChange (evento, novoStatus, agendaUpdateFn) {
// bloqueios e sessões de pacote não têm cobrança individual
const ignorar = evento.tipo !== 'sessao' || !!evento.billing_contract_id
const statusAnterior = evento.status
if (!rule || rule.charge_mode === 'none') {
// Cancelar cobrança existente, se houver
if (evento.billed) {
const { data: existingRec } = await supabase.from('financial_records').select('id, status').eq('agenda_evento_id', evento.id).in('status', ['pending', 'overdue']).maybeSingle();
loading.value = true
error.value = null
if (existingRec) {
await supabase.from('financial_records').update({ status: 'cancelled', updated_at: new Date().toISOString() }).eq('id', existingRec.id);
}
}
return { ok: true };
}
try {
// 1. Aplica o update na agenda sempre (fonte da verdade é a agenda)
await agendaUpdateFn(evento.id, { status: novoStatus })
// charge_mode != 'none' → ajustar valor da cobrança existente ou criar nova
const chargeAmount = calcChargeAmount(evento.price ?? 0, rule);
if (ignorar) return { ok: true }
if (statusAnterior === novoStatus) return { ok: true }
if (evento.billed) {
// Atualiza o valor da cobrança existente
const { data: existingRec } = await supabase.from('financial_records').select('id').eq('agenda_evento_id', evento.id).in('status', ['pending', 'overdue']).maybeSingle();
// 2. Lógica financeira por transição
const tenantStore = useTenantStore()
const tenantId = tenantStore.activeTenantId
if (existingRec) {
await supabase
.from('financial_records')
.update({
amount: chargeAmount,
final_amount: chargeAmount,
updated_at: new Date().toISOString()
})
.eq('id', existingRec.id);
}
} else if (chargeAmount > 0) {
// Sessão sem cobrança: gera uma nova com o valor ajustado
await gerarCobrancaManual({ ...evento, price: chargeAmount });
}
// ── faltou / cancelado → consultar exceção financeira ──────────────
const exceptionType = STATUS_TO_EXCEPTION[novoStatus]
if (exceptionType) {
const rule = await getFinancialExceptionRule(tenantId, exceptionType)
if (!rule || rule.charge_mode === 'none') {
// Cancelar cobrança existente, se houver
if (evento.billed) {
const { data: existingRec } = await supabase
.from('financial_records')
.select('id, status')
.eq('agenda_evento_id', evento.id)
.in('status', ['pending', 'overdue'])
.maybeSingle()
if (existingRec) {
await supabase
.from('financial_records')
.update({ status: 'cancelled', updated_at: new Date().toISOString() })
.eq('id', existingRec.id)
return { ok: true };
}
}
return { ok: true }
// ── remarcar → atualizar due_date da cobrança existente ────────────
if (novoStatus === 'remarcar' && evento.billed) {
// due_date mantém a data da sessão original por enquanto
// (a nova data virá quando a sessão for reagendada)
return { ok: true };
}
// ── agendado → realizado: nenhuma ação financeira automática ────────
return { ok: true };
} catch (e) {
error.value = e?.message || 'Erro ao processar mudança de status.';
return { ok: false, error: error.value };
} finally {
loading.value = false;
}
// charge_mode != 'none' → ajustar valor da cobrança existente ou criar nova
const chargeAmount = calcChargeAmount(evento.price ?? 0, rule)
if (evento.billed) {
// Atualiza o valor da cobrança existente
const { data: existingRec } = await supabase
.from('financial_records')
.select('id')
.eq('agenda_evento_id', evento.id)
.in('status', ['pending', 'overdue'])
.maybeSingle()
if (existingRec) {
await supabase
.from('financial_records')
.update({
amount: chargeAmount,
final_amount: chargeAmount,
updated_at: new Date().toISOString(),
})
.eq('id', existingRec.id)
}
} else if (chargeAmount > 0) {
// Sessão sem cobrança: gera uma nova com o valor ajustado
await gerarCobrancaManual({ ...evento, price: chargeAmount })
}
return { ok: true }
}
// ── remarcar → atualizar due_date da cobrança existente ────────────
if (novoStatus === 'remarcar' && evento.billed) {
// due_date mantém a data da sessão original por enquanto
// (a nova data virá quando a sessão for reagendada)
return { ok: true }
}
// ── agendado → realizado: nenhuma ação financeira automática ────────
return { ok: true }
} catch (e) {
error.value = e?.message || 'Erro ao processar mudança de status.'
return { ok: false, error: error.value }
} finally {
loading.value = false
}
}
// ── invalidar cache (use quando o usuário altera exceções financeiras) ───
function invalidateExceptionsCache () {
_exceptionsCache.clear()
}
// ── invalidar cache (use quando o usuário altera exceções financeiras) ───
function invalidateExceptionsCache() {
_exceptionsCache.clear();
}
return {
loading,
error,
handleStatusChange,
gerarCobrancaManual,
getFinancialExceptionRule,
invalidateExceptionsCache,
}
return {
loading,
error,
handleStatusChange,
gerarCobrancaManual,
getFinancialExceptionRule,
invalidateExceptionsCache
};
}