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:
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user