Preficicação, Convenio, Ajustes Agenda, Configurações Excessões
This commit is contained in:
@@ -33,6 +33,7 @@ const BASE_SELECT = `
|
||||
determined_commitment_id, link_online, extra_fields, modalidade,
|
||||
recurrence_id, recurrence_date,
|
||||
mirror_of_event_id, price,
|
||||
insurance_plan_id, insurance_guide_number, insurance_value,
|
||||
patients!agenda_eventos_patient_id_fkey (
|
||||
id, nome_completo, avatar_url
|
||||
),
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
// src/features/agenda/composables/useCommitmentServices.js
|
||||
//
|
||||
// CRUD de commitment_services — itens de serviço vinculados a um evento.
|
||||
// CRUD de recurrence_rule_services — template de serviços de uma regra de recorrência.
|
||||
//
|
||||
// Interface pública:
|
||||
// loadItems(eventId) → Array<CommitmentItem>
|
||||
// saveItems(eventId, items, opts?) → void (delete+insert; opts.markCustomized marca services_customized no evento)
|
||||
// loadRuleItems(ruleId) → Array<CommitmentItem>
|
||||
// saveRuleItems(ruleId, items) → void (delete+insert no template da regra)
|
||||
// loadItemsOrTemplate(eventId, ruleId) → Array<CommitmentItem> (próprios ou template)
|
||||
// propagateToSerie(ruleId, items, opts?) → void (ocorrências materializadas com services_customized=false)
|
||||
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
|
||||
// Shape interno de CommitmentItem:
|
||||
// {
|
||||
// service_id: uuid,
|
||||
// service_name: string, // display only — não gravado no banco
|
||||
// quantity: number,
|
||||
// unit_price: number, // snapshot de services.price no momento da adição
|
||||
// discount_pct: number,
|
||||
// discount_flat: number,
|
||||
// final_price: number,
|
||||
// }
|
||||
|
||||
/** Mapeia uma linha do banco para CommitmentItem (compartilhado entre commitment_services e recurrence_rule_services) */
|
||||
function _mapRow (r) {
|
||||
return {
|
||||
service_id: r.service_id,
|
||||
service_name: r.services?.name ?? '',
|
||||
quantity: Number(r.quantity),
|
||||
unit_price: Number(r.unit_price),
|
||||
discount_pct: Number(r.discount_pct ?? 0),
|
||||
discount_flat: Number(r.discount_flat ?? 0),
|
||||
final_price: Number(r.final_price),
|
||||
}
|
||||
}
|
||||
|
||||
export function useCommitmentServices () {
|
||||
|
||||
// ── Carregar itens de um evento ──────────────────────────────────────
|
||||
async function loadItems (eventId) {
|
||||
if (!eventId) return []
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('commitment_services')
|
||||
.select('service_id, quantity, unit_price, discount_pct, discount_flat, final_price, services(name)')
|
||||
.eq('commitment_id', eventId)
|
||||
.order('created_at', { ascending: true })
|
||||
|
||||
if (error) throw error
|
||||
return (data || []).map(_mapRow)
|
||||
}
|
||||
|
||||
// ── Salvar itens de um evento ────────────────────────────────────────
|
||||
// Estratégia: DELETE dos itens existentes + INSERT dos novos.
|
||||
// Garante idempotência em edições sem risco de duplicatas.
|
||||
//
|
||||
// opts.markCustomized = true: após salvar, marca services_customized = true
|
||||
// no agenda_eventos correspondente, impedindo que edições do evento raiz
|
||||
// sobrescrevam os serviços desta ocorrência individual.
|
||||
async function saveItems (eventId, items, { markCustomized = false } = {}) {
|
||||
if (!eventId) throw new Error('eventId é obrigatório para salvar commitment_services.')
|
||||
|
||||
// 1. Remove itens existentes deste evento
|
||||
const { error: deleteError } = await supabase
|
||||
.from('commitment_services')
|
||||
.delete()
|
||||
.eq('commitment_id', eventId)
|
||||
|
||||
if (deleteError) throw deleteError
|
||||
|
||||
// 2. Insere os novos itens (se houver)
|
||||
if (items?.length) {
|
||||
const rows = items.map(item => ({
|
||||
commitment_id: eventId,
|
||||
service_id: item.service_id,
|
||||
quantity: item.quantity,
|
||||
unit_price: item.unit_price,
|
||||
discount_pct: item.discount_pct ?? 0,
|
||||
discount_flat: item.discount_flat ?? 0,
|
||||
final_price: item.final_price,
|
||||
}))
|
||||
|
||||
const { error: insertError } = await supabase
|
||||
.from('commitment_services')
|
||||
.insert(rows)
|
||||
|
||||
if (insertError) throw insertError
|
||||
}
|
||||
|
||||
// 3. Marca a ocorrência como customizada (impede sobrescrita por edições do raiz)
|
||||
if (markCustomized) {
|
||||
const { error: updateError } = await supabase
|
||||
.from('agenda_eventos')
|
||||
.update({ services_customized: true })
|
||||
.eq('id', eventId)
|
||||
|
||||
if (updateError) throw updateError
|
||||
}
|
||||
}
|
||||
|
||||
// ── Carregar template de serviços de uma regra ───────────────────────
|
||||
// Retorna os itens armazenados em recurrence_rule_services para a regra.
|
||||
async function loadRuleItems (ruleId) {
|
||||
if (!ruleId) return []
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('recurrence_rule_services')
|
||||
.select('service_id, quantity, unit_price, discount_pct, discount_flat, final_price, services(name)')
|
||||
.eq('rule_id', ruleId)
|
||||
.order('created_at', { ascending: true })
|
||||
|
||||
if (error) throw error
|
||||
return (data || []).map(_mapRow)
|
||||
}
|
||||
|
||||
// ── Salvar template de serviços de uma regra ─────────────────────────
|
||||
// Estratégia: DELETE + INSERT — mesmo padrão de saveItems.
|
||||
// Chamado ao criar uma recorrência com serviços ou ao editar o evento
|
||||
// raiz com escopo 'todos' / 'este_e_seguintes'.
|
||||
async function saveRuleItems (ruleId, items) {
|
||||
if (!ruleId) throw new Error('ruleId é obrigatório para salvar recurrence_rule_services.')
|
||||
|
||||
const { error: deleteError } = await supabase
|
||||
.from('recurrence_rule_services')
|
||||
.delete()
|
||||
.eq('rule_id', ruleId)
|
||||
|
||||
if (deleteError) throw deleteError
|
||||
|
||||
if (!items?.length) return
|
||||
|
||||
const rows = items.map(item => ({
|
||||
rule_id: ruleId,
|
||||
service_id: item.service_id,
|
||||
quantity: item.quantity,
|
||||
unit_price: item.unit_price,
|
||||
discount_pct: item.discount_pct ?? 0,
|
||||
discount_flat: item.discount_flat ?? 0,
|
||||
final_price: item.final_price,
|
||||
}))
|
||||
|
||||
const { error: insertError } = await supabase
|
||||
.from('recurrence_rule_services')
|
||||
.insert(rows)
|
||||
|
||||
if (insertError) throw insertError
|
||||
}
|
||||
|
||||
// ── Carregar itens próprios ou herdar template da regra ──────────────
|
||||
// Retorna os commitment_services do evento se existirem.
|
||||
// Se o evento não tiver itens próprios e ruleId for fornecido,
|
||||
// retorna o template da regra (ocorrência ainda não customizada).
|
||||
async function loadItemsOrTemplate (eventId, ruleId, { allowEmpty = false } = {}) {
|
||||
const own = await loadItems(eventId)
|
||||
if (own.length > 0) return own
|
||||
if (allowEmpty) return []
|
||||
if (ruleId) return loadRuleItems(ruleId)
|
||||
return []
|
||||
}
|
||||
|
||||
// ── Propagar itens para ocorrências materializadas da série ──────────
|
||||
// Atualiza commitment_services nos agenda_eventos com recurrence_id = ruleId
|
||||
// onde services_customized = false (não foram editados individualmente).
|
||||
//
|
||||
// opts.fromDate: string ISO 'YYYY-MM-DD' — limita a ocorrências a partir
|
||||
// dessa data inclusive (escopo 'este_e_seguintes'). null = todas da série.
|
||||
async function propagateToSerie (ruleId, items, { fromDate = null, ignoreCustomized = false } = {}) {
|
||||
if (!ruleId) return
|
||||
|
||||
// Busca IDs das ocorrências materializadas elegíveis
|
||||
let q = supabase
|
||||
.from('agenda_eventos')
|
||||
.select('id')
|
||||
.eq('recurrence_id', ruleId)
|
||||
|
||||
if (!ignoreCustomized) {
|
||||
q = q.eq('services_customized', false)
|
||||
}
|
||||
|
||||
if (fromDate) {
|
||||
q = q.gte('inicio_em', fromDate)
|
||||
}
|
||||
|
||||
const { data: events, error: queryError } = await q
|
||||
if (queryError) throw queryError
|
||||
if (!events?.length) return
|
||||
|
||||
// Para cada evento elegível: delete + insert (padrão idempotente)
|
||||
for (const ev of events) {
|
||||
const { error: delErr } = await supabase
|
||||
.from('commitment_services')
|
||||
.delete()
|
||||
.eq('commitment_id', ev.id)
|
||||
if (delErr) throw delErr
|
||||
|
||||
if (items?.length) {
|
||||
const rows = items.map(item => ({
|
||||
commitment_id: ev.id,
|
||||
service_id: item.service_id,
|
||||
quantity: item.quantity,
|
||||
unit_price: item.unit_price,
|
||||
discount_pct: item.discount_pct ?? 0,
|
||||
discount_flat: item.discount_flat ?? 0,
|
||||
final_price: item.final_price,
|
||||
}))
|
||||
const { error: insErr } = await supabase
|
||||
.from('commitment_services')
|
||||
.insert(rows)
|
||||
if (insErr) throw insErr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
loadItems,
|
||||
saveItems,
|
||||
loadRuleItems,
|
||||
saveRuleItems,
|
||||
loadItemsOrTemplate,
|
||||
propagateToSerie,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
// src/features/agenda/composables/useFinancialExceptions.js
|
||||
//
|
||||
// CRUD sobre a tabela public.financial_exceptions.
|
||||
//
|
||||
// Interface pública:
|
||||
// exceptions – ref([]) lista de regras do owner (próprias + da clínica)
|
||||
// loading – ref(false)
|
||||
// error – ref('')
|
||||
//
|
||||
// load(ownerId) – carrega registros do owner + regras globais (owner_id IS NULL)
|
||||
// save(payload) – cria ou atualiza (id presente = update dos campos editáveis)
|
||||
// remove(id) – hard delete (apenas registros do próprio owner)
|
||||
|
||||
import { ref } from 'vue'
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
|
||||
export function useFinancialExceptions () {
|
||||
const exceptions = ref([])
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
|
||||
// ── Carregar exceções do owner + regras globais da clínica ───────────
|
||||
async function load (ownerId) {
|
||||
if (!ownerId) return
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
const { data, error: err } = await supabase
|
||||
.from('financial_exceptions')
|
||||
.select('*')
|
||||
.or(`owner_id.eq.${ownerId},owner_id.is.null`)
|
||||
.order('exception_type', { ascending: true })
|
||||
.order('created_at', { ascending: true })
|
||||
|
||||
if (err) throw err
|
||||
exceptions.value = data || []
|
||||
} catch (e) {
|
||||
error.value = e?.message || 'Falha ao carregar exceções financeiras.'
|
||||
exceptions.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ── Criar ou atualizar uma exceção ───────────────────────────────────
|
||||
// Para UPDATE, apenas os campos editáveis são enviados:
|
||||
// charge_mode, charge_value, charge_pct, min_hours_notice
|
||||
// Regras globais (owner_id IS NULL) não devem ser editadas — o chamador
|
||||
// é responsável por não chamar save() nesses registros.
|
||||
async function save (payload) {
|
||||
error.value = ''
|
||||
try {
|
||||
if (payload.id) {
|
||||
const { error: err } = await supabase
|
||||
.from('financial_exceptions')
|
||||
.update({
|
||||
charge_mode: payload.charge_mode,
|
||||
charge_value: payload.charge_value ?? null,
|
||||
charge_pct: payload.charge_pct ?? null,
|
||||
min_hours_notice: payload.min_hours_notice ?? null,
|
||||
})
|
||||
.eq('id', payload.id)
|
||||
if (err) throw err
|
||||
} else {
|
||||
const { error: err } = await supabase
|
||||
.from('financial_exceptions')
|
||||
.insert({
|
||||
owner_id: payload.owner_id,
|
||||
tenant_id: payload.tenant_id ?? null,
|
||||
exception_type: payload.exception_type,
|
||||
charge_mode: payload.charge_mode,
|
||||
charge_value: payload.charge_value ?? null,
|
||||
charge_pct: payload.charge_pct ?? null,
|
||||
min_hours_notice: payload.min_hours_notice ?? null,
|
||||
})
|
||||
if (err) throw err
|
||||
}
|
||||
} catch (e) {
|
||||
error.value = e?.message || 'Falha ao salvar exceção financeira.'
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
// ── Hard delete — apenas registros do próprio owner ──────────────────
|
||||
// Regras globais (owner_id IS NULL) são protegidas pelo RLS do banco;
|
||||
// a UI também deve esconder o botão de remover nesses casos.
|
||||
async function remove (id) {
|
||||
error.value = ''
|
||||
try {
|
||||
const { error: err } = await supabase
|
||||
.from('financial_exceptions')
|
||||
.delete()
|
||||
.eq('id', id)
|
||||
if (err) throw err
|
||||
exceptions.value = exceptions.value.filter(e => e.id !== id)
|
||||
} catch (e) {
|
||||
error.value = e?.message || 'Falha ao remover exceção financeira.'
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
return { exceptions, loading, error, load, save, remove }
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// src/features/agenda/composables/useInsurancePlans.js
|
||||
//
|
||||
// CRUD sobre a tabela public.insurance_plans.
|
||||
//
|
||||
// Interface pública:
|
||||
// plans – ref([]) lista de planos ativos do owner
|
||||
// loading – ref(false)
|
||||
// error – ref('')
|
||||
//
|
||||
// load(ownerId) – carrega todos os planos ativos
|
||||
// save(payload) – cria ou atualiza (id presente = update)
|
||||
// remove(id) – soft-delete (active = false)
|
||||
|
||||
import { ref } from 'vue'
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
|
||||
export function useInsurancePlans () {
|
||||
const plans = ref([])
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
|
||||
async function load (ownerId) {
|
||||
if (!ownerId) return
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
const { data, error: err } = await supabase
|
||||
.from('insurance_plans')
|
||||
.select('id, name, notes, default_value, active')
|
||||
.eq('owner_id', ownerId)
|
||||
.eq('active', true)
|
||||
.order('name', { ascending: true })
|
||||
|
||||
if (err) throw err
|
||||
plans.value = data || []
|
||||
} catch (e) {
|
||||
error.value = e?.message || 'Falha ao carregar convênios.'
|
||||
plans.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function save (payload) {
|
||||
error.value = ''
|
||||
try {
|
||||
if (payload.id) {
|
||||
const { id, owner_id, tenant_id, ...fields } = payload
|
||||
const { error: err } = await supabase
|
||||
.from('insurance_plans')
|
||||
.update(fields)
|
||||
.eq('id', id)
|
||||
.eq('owner_id', owner_id)
|
||||
if (err) throw err
|
||||
} else {
|
||||
const { error: err } = await supabase
|
||||
.from('insurance_plans')
|
||||
.insert(payload)
|
||||
if (err) throw err
|
||||
}
|
||||
} catch (e) {
|
||||
error.value = e?.message || 'Falha ao salvar convênio.'
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
async function remove (id) {
|
||||
error.value = ''
|
||||
try {
|
||||
const { error: err } = await supabase
|
||||
.from('insurance_plans')
|
||||
.update({ active: false })
|
||||
.eq('id', id)
|
||||
if (err) throw err
|
||||
plans.value = plans.value.filter(p => p.id !== id)
|
||||
} catch (e) {
|
||||
error.value = e?.message || 'Falha ao remover convênio.'
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
return { plans, loading, error, load, save, remove }
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
// src/features/agenda/composables/usePatientDiscounts.js
|
||||
//
|
||||
// CRUD completo sobre a tabela public.patient_discounts.
|
||||
//
|
||||
// Interface pública:
|
||||
// discounts – ref([]) lista de descontos do owner
|
||||
// loading – ref(false)
|
||||
// error – ref('')
|
||||
//
|
||||
// load(ownerId) – carrega todos os registros do owner
|
||||
// save(payload) – cria ou atualiza (id presente = update)
|
||||
// remove(id) – soft-delete (active = false)
|
||||
// loadActive(ownerId, patientId) – desconto ativo vigente para um paciente
|
||||
|
||||
import { ref } from 'vue'
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
|
||||
export function usePatientDiscounts () {
|
||||
const discounts = ref([])
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
|
||||
// ── Carregar todos os descontos do owner ─────────────────────────────
|
||||
async function load (ownerId) {
|
||||
if (!ownerId) return
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
const { data, error: err } = await supabase
|
||||
.from('patient_discounts')
|
||||
.select('*')
|
||||
.eq('owner_id', ownerId)
|
||||
.order('created_at', { ascending: false })
|
||||
|
||||
if (err) throw err
|
||||
discounts.value = data || []
|
||||
} catch (e) {
|
||||
error.value = e?.message || 'Falha ao carregar descontos.'
|
||||
discounts.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ── Criar ou atualizar um desconto ───────────────────────────────────
|
||||
// payload deve conter: { owner_id, tenant_id, patient_id, discount_pct, discount_flat, ... }
|
||||
// Se payload.id estiver presente, faz UPDATE; caso contrário, INSERT.
|
||||
async function save (payload) {
|
||||
error.value = ''
|
||||
try {
|
||||
if (payload.id) {
|
||||
const { id, owner_id, tenant_id, ...fields } = payload
|
||||
const { error: err } = await supabase
|
||||
.from('patient_discounts')
|
||||
.update(fields)
|
||||
.eq('id', id)
|
||||
.eq('owner_id', owner_id)
|
||||
if (err) throw err
|
||||
} else {
|
||||
const { error: err } = await supabase
|
||||
.from('patient_discounts')
|
||||
.insert(payload)
|
||||
if (err) throw err
|
||||
}
|
||||
} catch (e) {
|
||||
error.value = e?.message || 'Falha ao salvar desconto.'
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
// ── Soft-delete: marca active = false ───────────────────────────────
|
||||
async function remove (id) {
|
||||
error.value = ''
|
||||
try {
|
||||
const { error: err } = await supabase
|
||||
.from('patient_discounts')
|
||||
.update({ active: false })
|
||||
.eq('id', id)
|
||||
if (err) throw err
|
||||
discounts.value = discounts.value.filter(d => d.id !== id)
|
||||
} catch (e) {
|
||||
error.value = e?.message || 'Falha ao desativar desconto.'
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
// ── Desconto ativo vigente para um paciente específico ───────────────
|
||||
// Retorna o primeiro registro que satisfaz:
|
||||
// active = true
|
||||
// active_from IS NULL OR active_from <= now()
|
||||
// active_to IS NULL OR active_to >= now()
|
||||
// Ordenado por created_at DESC (mais recente tem precedência).
|
||||
async function loadActive (ownerId, patientId) {
|
||||
if (!ownerId || !patientId) return null
|
||||
try {
|
||||
const now = new Date().toISOString()
|
||||
const { data, error: err } = await supabase
|
||||
.from('patient_discounts')
|
||||
.select('*')
|
||||
.eq('owner_id', ownerId)
|
||||
.eq('patient_id', patientId)
|
||||
.eq('active', true)
|
||||
.or(`active_from.is.null,active_from.lte.${now}`)
|
||||
.or(`active_to.is.null,active_to.gte.${now}`)
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(1)
|
||||
.maybeSingle()
|
||||
|
||||
if (err) throw err
|
||||
return data || null
|
||||
} catch (e) {
|
||||
console.warn('[usePatientDiscounts] loadActive error:', e?.message)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return { discounts, loading, error, load, save, remove, loadActive }
|
||||
}
|
||||
@@ -346,7 +346,12 @@ export function mergeWithStoredSessions (occurrences, storedRows) {
|
||||
for (const row of storedRows || []) {
|
||||
if (!row.recurrence_id || !row.recurrence_date) continue
|
||||
const key = `${row.recurrence_id}::${row.recurrence_date}`
|
||||
realMap.set(key, { ...row, is_real_session: true, is_occurrence: false })
|
||||
realMap.set(key, {
|
||||
...row,
|
||||
is_real_session: true,
|
||||
is_occurrence: false,
|
||||
original_date: row.original_date ?? row.recurrence_date ?? null,
|
||||
})
|
||||
}
|
||||
|
||||
const result = []
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
// src/features/agenda/composables/useServices.js
|
||||
//
|
||||
// CRUD completo sobre a tabela public.services.
|
||||
//
|
||||
// Interface pública:
|
||||
// services – ref([]) lista de serviços ativos do owner
|
||||
// loading – ref(false)
|
||||
// error – ref('')
|
||||
//
|
||||
// load(ownerId) – carrega todos os serviços ativos
|
||||
// save(payload) – cria ou atualiza (id presente = update)
|
||||
// remove(id) – soft-delete (active = false)
|
||||
// getDefaultPrice() – preço do primeiro serviço ativo, ou null
|
||||
// getPriceFor(serviceId) – preço de um serviço específico, ou null
|
||||
|
||||
import { ref } from 'vue'
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
|
||||
export function useServices () {
|
||||
const services = ref([])
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
|
||||
// ── Carregar serviços ativos do owner ───────────────────────────────
|
||||
async function load (ownerId) {
|
||||
if (!ownerId) return
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
const { data, error: err } = await supabase
|
||||
.from('services')
|
||||
.select('id, name, description, price, duration_min, active')
|
||||
.eq('owner_id', ownerId)
|
||||
.eq('active', true)
|
||||
.order('created_at', { ascending: true })
|
||||
|
||||
if (err) throw err
|
||||
services.value = data || []
|
||||
} catch (e) {
|
||||
error.value = e?.message || 'Falha ao carregar serviços.'
|
||||
services.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ── Criar ou atualizar um serviço ───────────────────────────────────
|
||||
// payload deve conter: { owner_id, tenant_id, name, price, description?, duration_min? }
|
||||
// Se payload.id estiver presente, faz UPDATE; caso contrário, INSERT.
|
||||
async function save (payload) {
|
||||
error.value = ''
|
||||
try {
|
||||
if (payload.id) {
|
||||
const { id, owner_id, tenant_id, ...fields } = payload
|
||||
const { error: err } = await supabase
|
||||
.from('services')
|
||||
.update(fields)
|
||||
.eq('id', id)
|
||||
.eq('owner_id', owner_id)
|
||||
if (err) throw err
|
||||
} else {
|
||||
const { error: err } = await supabase
|
||||
.from('services')
|
||||
.insert(payload)
|
||||
if (err) throw err
|
||||
}
|
||||
} catch (e) {
|
||||
error.value = e?.message || 'Falha ao salvar serviço.'
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
// ── Soft-delete: marca active = false ───────────────────────────────
|
||||
async function remove (id) {
|
||||
error.value = ''
|
||||
try {
|
||||
const { error: err } = await supabase
|
||||
.from('services')
|
||||
.update({ active: false })
|
||||
.eq('id', id)
|
||||
if (err) throw err
|
||||
services.value = services.value.filter(s => s.id !== id)
|
||||
} catch (e) {
|
||||
error.value = e?.message || 'Falha ao remover serviço.'
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers de preço ────────────────────────────────────────────────
|
||||
|
||||
// Retorna o preço de um serviço específico (serviceId fornecido) ou
|
||||
// o preço do primeiro serviço ativo da lista (serviceId omitido).
|
||||
// Retorna null se não houver serviços ou o id não for encontrado.
|
||||
function getDefaultPrice (serviceId) {
|
||||
if (serviceId) {
|
||||
const svc = services.value.find(s => s.id === serviceId)
|
||||
return svc?.price != null ? Number(svc.price) : null
|
||||
}
|
||||
const first = services.value[0]
|
||||
return first?.price != null ? Number(first.price) : null
|
||||
}
|
||||
|
||||
// Alias explícito para clareza nos chamadores que conhecem o id
|
||||
function getPriceFor (serviceId) {
|
||||
return getDefaultPrice(serviceId)
|
||||
}
|
||||
|
||||
return { services, loading, error, load, save, remove, getDefaultPrice, getPriceFor }
|
||||
}
|
||||
Reference in New Issue
Block a user