Copyright, Financeiro, Lançamentos, aprimoramentos de ui
This commit is contained in:
@@ -1,5 +1,19 @@
|
||||
// src/composables/useDocsHealth.js
|
||||
// Singleton que computa métricas de saúde dos documentos.
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/composables/Usedocshealth.js
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
// Critério de "atenção": mais de 30% dos votos são negativos
|
||||
// (mínimo 3 votos totais para evitar falso-positivo com 1 voto negativo).
|
||||
//
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/composables/useAgendaFinanceiro.js
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* useAgendaFinanceiro
|
||||
*
|
||||
* Camada de orquestração entre agenda e financeiro.
|
||||
* Não modifica useAgendaEvents diretamente — recebe a função de update
|
||||
* como parâmetro para manter o desacoplamento.
|
||||
*
|
||||
* Uso:
|
||||
* const { handleStatusChange, gerarCobrancaManual } = useAgendaFinanceiro()
|
||||
*
|
||||
* // No handler de save do componente pai:
|
||||
* await handleStatusChange(eventoOriginal, novoStatus, agendaEvents.update)
|
||||
*/
|
||||
|
||||
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()
|
||||
|
||||
// ─── 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
|
||||
}
|
||||
|
||||
// ─── mapeamento: status anterior → tipo de exceção a consultar ───────────────
|
||||
const STATUS_TO_EXCEPTION = {
|
||||
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
|
||||
}
|
||||
|
||||
// ─── composable ──────────────────────────────────────────────────────────────
|
||||
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)
|
||||
|
||||
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()
|
||||
|
||||
if (err) {
|
||||
console.warn('[useAgendaFinanceiro] getFinancialExceptionRule:', err.message)
|
||||
return 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.' }
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// ── 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
|
||||
|
||||
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 })
|
||||
|
||||
if (ignorar) return { ok: true }
|
||||
if (statusAnterior === novoStatus) return { ok: true }
|
||||
|
||||
// 2. Lógica financeira por transição
|
||||
const tenantStore = useTenantStore()
|
||||
const tenantId = tenantStore.activeTenantId
|
||||
|
||||
// ── 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 }
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
return {
|
||||
loading,
|
||||
error,
|
||||
handleStatusChange,
|
||||
gerarCobrancaManual,
|
||||
getFinancialExceptionRule,
|
||||
invalidateExceptionsCache,
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,19 @@
|
||||
// src/composables/useAjuda.js
|
||||
// Composable singleton para o drawer de ajuda.
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/composables/useAjuda.js
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
// - Home: docs da sessão atual + outros docs paginados + FAQ
|
||||
// - Navegação interna com stack (voltar)
|
||||
// - Votação por usuário (útil / não útil)
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/composables/useAuth.js
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* ---------------------------------------------------------
|
||||
* useAuth()
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
// src/composables/useDocsAdmin.js
|
||||
// Estado compartilhado para abrir o dialog de edição de um doc
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/composables/useDocsAdmin.js
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
// a partir de outra página (ex: SaasFaqPage → SaasDocsPage).
|
||||
|
||||
import { ref } from 'vue'
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
// src/composables/useFeriados.js
|
||||
// Fonte única de verdade para feriados: nacionais (algoritmo) + municipais (Supabase).
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/composables/useFeriados.js
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
import { ref, computed } from 'vue'
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
|
||||
@@ -0,0 +1,328 @@
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/composables/useFinancialRecords.js
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
import { ref, computed } from 'vue'
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
import { useTenantStore } from '@/stores/tenantStore'
|
||||
|
||||
// ─── helpers internos ────────────────────────────────────────────────────────
|
||||
|
||||
function assertTenantId (tenantId) {
|
||||
if (!tenantId || tenantId === 'null' || tenantId === 'undefined') {
|
||||
throw new Error('Tenant ativo inválido. Selecione a clínica antes de operar no financeiro.')
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// ─── select base com joins ───────────────────────────────────────────────────
|
||||
|
||||
const BASE_SELECT = `
|
||||
id, tenant_id, owner_id, patient_id, agenda_evento_id,
|
||||
amount, discount_amount, final_amount,
|
||||
status, due_date, paid_at, payment_method,
|
||||
notes, created_at, updated_at,
|
||||
patients!patient_id (
|
||||
id, nome_completo, identification_color
|
||||
),
|
||||
agenda_eventos!agenda_evento_id (
|
||||
id, inicio_em, status, tipo
|
||||
)
|
||||
`.trim()
|
||||
|
||||
// ─── composable ──────────────────────────────────────────────────────────────
|
||||
|
||||
export function useFinancialRecords () {
|
||||
const records = ref([])
|
||||
const loading = ref(false)
|
||||
const error = ref(null)
|
||||
|
||||
// ── computed: resumo financeiro ─────────────────────────────────────────
|
||||
|
||||
const summary = computed(() => {
|
||||
const now = new Date()
|
||||
const thisYear = now.getFullYear()
|
||||
const thisMonth = now.getMonth() // 0-indexed
|
||||
|
||||
const countByStatus = { pending: 0, paid: 0, overdue: 0, cancelled: 0 }
|
||||
|
||||
let totalPending = 0
|
||||
let totalOverdue = 0
|
||||
let totalPaidThisMonth = 0
|
||||
|
||||
for (const r of records.value) {
|
||||
countByStatus[r.status] = (countByStatus[r.status] ?? 0) + 1
|
||||
|
||||
if (r.status === 'pending') {
|
||||
totalPending += r.final_amount ?? r.amount ?? 0
|
||||
}
|
||||
|
||||
if (r.status === 'overdue') {
|
||||
totalOverdue += r.final_amount ?? r.amount ?? 0
|
||||
}
|
||||
|
||||
if (r.status === 'paid' && r.paid_at) {
|
||||
const d = new Date(r.paid_at)
|
||||
if (d.getFullYear() === thisYear && d.getMonth() === thisMonth) {
|
||||
totalPaidThisMonth += r.final_amount ?? r.amount ?? 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { totalPending, totalOverdue, totalPaidThisMonth, countByStatus }
|
||||
})
|
||||
|
||||
// ── fetchRecords ─────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* @param {Object} [filters]
|
||||
* @param {string} [filters.status] - 'pending'|'paid'|'overdue'|'partial'|'cancelled'|'refunded'
|
||||
* @param {string} [filters.type] - 'receita'|'despesa'
|
||||
* @param {string} [filters.patient_id] - UUID
|
||||
* @param {string} [filters.due_date_from] - ISO date string
|
||||
* @param {string} [filters.due_date_to] - ISO date string
|
||||
* @param {number} [filters.limit] - rows per page (default 50)
|
||||
* @param {number} [filters.offset] - row offset for pagination (default 0)
|
||||
* @returns {Promise<{total: number}>}
|
||||
*/
|
||||
async function fetchRecords (filters = {}) {
|
||||
const tenantStore = useTenantStore()
|
||||
const tenantId = tenantStore.activeTenantId
|
||||
assertTenantId(tenantId)
|
||||
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
const limit = filters.limit ?? 50
|
||||
const offset = filters.offset ?? 0
|
||||
|
||||
try {
|
||||
let query = supabase
|
||||
.from('financial_records')
|
||||
.select(BASE_SELECT, { count: 'exact' })
|
||||
.eq('tenant_id', tenantId)
|
||||
.is('deleted_at', null)
|
||||
.order('due_date', { ascending: false })
|
||||
.range(offset, offset + limit - 1)
|
||||
|
||||
if (filters.status) query = query.eq('status', filters.status)
|
||||
if (filters.type) query = query.eq('type', filters.type)
|
||||
if (filters.patient_id) query = query.eq('patient_id', filters.patient_id)
|
||||
if (filters.due_date_from) query = query.gte('due_date', filters.due_date_from)
|
||||
if (filters.due_date_to) query = query.lte('due_date', filters.due_date_to)
|
||||
|
||||
const { data, error: err, count } = await query
|
||||
if (err) throw err
|
||||
|
||||
records.value = data ?? []
|
||||
return { total: count ?? 0 }
|
||||
} catch (e) {
|
||||
error.value = e?.message || 'Erro ao carregar registros financeiros.'
|
||||
records.value = []
|
||||
return { total: 0 }
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ── createRecord (via RPC — para sessões) ────────────────────────────────
|
||||
|
||||
/**
|
||||
* @param {Object} payload
|
||||
* @param {string} payload.patient_id
|
||||
* @param {string} payload.agenda_evento_id
|
||||
* @param {number} payload.amount
|
||||
* @param {string} payload.due_date - ISO date string
|
||||
*/
|
||||
async function createRecord (payload) {
|
||||
const tenantStore = useTenantStore()
|
||||
const tenantId = tenantStore.activeTenantId
|
||||
assertTenantId(tenantId)
|
||||
|
||||
const ownerId = await getUid()
|
||||
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const { data, error: err } = await supabase.rpc('create_financial_record_for_session', {
|
||||
p_tenant_id: tenantId,
|
||||
p_owner_id: ownerId,
|
||||
p_patient_id: payload.patient_id,
|
||||
p_agenda_evento_id: payload.agenda_evento_id,
|
||||
p_amount: payload.amount,
|
||||
p_due_date: payload.due_date,
|
||||
})
|
||||
|
||||
if (err) throw err
|
||||
|
||||
// Re-fetch para garantir joins completos
|
||||
await fetchRecords()
|
||||
|
||||
return { ok: true, data }
|
||||
} catch (e) {
|
||||
error.value = e?.message || 'Erro ao criar cobrança.'
|
||||
return { ok: false, error: e?.message }
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ── createManualRecord (INSERT direto — sem sessão) ──────────────────────
|
||||
|
||||
/**
|
||||
* @param {Object} payload
|
||||
* @param {string} payload.patient_id
|
||||
* @param {number} payload.amount
|
||||
* @param {number} [payload.discount_amount]
|
||||
* @param {string} payload.due_date
|
||||
* @param {string} [payload.status] - default 'pending'
|
||||
* @param {string} [payload.payment_method]
|
||||
* @param {string} [payload.notes]
|
||||
*/
|
||||
async function createManualRecord (payload) {
|
||||
const tenantStore = useTenantStore()
|
||||
const tenantId = tenantStore.activeTenantId
|
||||
assertTenantId(tenantId)
|
||||
|
||||
const ownerId = await getUid()
|
||||
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const discount = payload.discount_amount ?? 0
|
||||
const amount = payload.amount ?? 0
|
||||
|
||||
const { data, error: err } = await supabase
|
||||
.from('financial_records')
|
||||
.insert([{
|
||||
tenant_id: tenantId,
|
||||
owner_id: ownerId,
|
||||
patient_id: payload.patient_id ?? null,
|
||||
agenda_evento_id: null,
|
||||
amount,
|
||||
discount_amount: discount,
|
||||
final_amount: amount - discount,
|
||||
status: payload.status ?? 'pending',
|
||||
due_date: payload.due_date,
|
||||
payment_method: payload.payment_method ?? null,
|
||||
notes: payload.notes ?? null,
|
||||
}])
|
||||
.select(BASE_SELECT)
|
||||
.single()
|
||||
|
||||
if (err) throw err
|
||||
|
||||
records.value = [data, ...records.value]
|
||||
|
||||
return { ok: true, data }
|
||||
} catch (e) {
|
||||
error.value = e?.message || 'Erro ao criar lançamento manual.'
|
||||
return { ok: false, error: e?.message }
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ── markAsPaid ───────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* @param {string} recordId
|
||||
* @param {string} paymentMethod - 'pix' | 'deposito' | 'dinheiro' | 'cartao' | 'convenio'
|
||||
*/
|
||||
async function markAsPaid (recordId, paymentMethod) {
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const { data, error: err } = await supabase.rpc('mark_as_paid', {
|
||||
p_financial_record_id: recordId,
|
||||
p_payment_method: paymentMethod,
|
||||
})
|
||||
|
||||
if (err) throw err
|
||||
|
||||
// RPC retorna SETOF (array) — patch local direto, sem depender do retorno
|
||||
const idx = records.value.findIndex(r => r.id === recordId)
|
||||
if (idx !== -1) {
|
||||
records.value[idx] = {
|
||||
...records.value[idx],
|
||||
status: 'paid',
|
||||
paid_at: new Date().toISOString(),
|
||||
payment_method: paymentMethod,
|
||||
}
|
||||
}
|
||||
|
||||
return { ok: true }
|
||||
} catch (e) {
|
||||
error.value = e?.message || 'Erro ao marcar como pago.'
|
||||
return { ok: false, error: e?.message }
|
||||
}
|
||||
}
|
||||
|
||||
// ── cancelRecord ─────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* @param {string} recordId
|
||||
*/
|
||||
async function cancelRecord (recordId) {
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const { error: err } = await supabase
|
||||
.from('financial_records')
|
||||
.update({ status: 'cancelled', updated_at: new Date().toISOString() })
|
||||
.eq('id', recordId)
|
||||
|
||||
if (err) throw err
|
||||
|
||||
// Atualiza localmente sem re-fetch
|
||||
const idx = records.value.findIndex(r => r.id === recordId)
|
||||
if (idx !== -1) {
|
||||
records.value[idx] = { ...records.value[idx], status: 'cancelled' }
|
||||
}
|
||||
|
||||
return { ok: true }
|
||||
} catch (e) {
|
||||
error.value = e?.message || 'Erro ao cancelar registro.'
|
||||
return { ok: false, error: e?.message }
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
return {
|
||||
// estado
|
||||
records,
|
||||
loading,
|
||||
error,
|
||||
// computed
|
||||
summary,
|
||||
// ações
|
||||
fetchRecords,
|
||||
createRecord,
|
||||
createManualRecord,
|
||||
markAsPaid,
|
||||
cancelRecord,
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,19 @@
|
||||
// src/composables/useMenuBadges.js
|
||||
// Singleton — contadores para badges do menu (agenda hoje, cadastros e agendamentos recebidos)
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/composables/useMenuBadges.js
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
import { ref } from 'vue'
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
|
||||
@@ -1,4 +1,19 @@
|
||||
// src/composables/useNotifications.js
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/composables/useNotifications.js
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
import { onMounted, onUnmounted } from 'vue'
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
import { useNotificationStore } from '@/stores/notificationStore'
|
||||
|
||||
@@ -1,4 +1,19 @@
|
||||
// src/composables/usePatientLifecycle.js
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/composables/usePatientLifecycle.js
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
|
||||
export function usePatientLifecycle () {
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
// src/composables/usePlatformPermissions.js
|
||||
//
|
||||
// Permissões de PLATAFORMA (globais, não vinculadas a tenant).
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/composables/usePlatformPermissions.js
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
// Distinto do RBAC de tenant (useRoleGuard).
|
||||
//
|
||||
// O campo `platform_roles text[]` na tabela `profiles` do Supabase
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/composables/useRoleGuard.js
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
import { computed } from 'vue'
|
||||
import { useTenantStore } from '@/stores/tenantStore'
|
||||
|
||||
|
||||
@@ -1,4 +1,19 @@
|
||||
// src/composables/useUserSettingsPersistence.js
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/composables/useUserSettingsPersistence.js
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
import { ref } from 'vue'
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
import { useLayout } from '@/layout/composables/layout'
|
||||
|
||||
Reference in New Issue
Block a user