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
@@ -9,269 +9,279 @@
* Não usa Supabase — sem mocks necessários.
*/
import { describe, it, expect } from 'vitest'
import { generateDates, expandRules, mergeWithStoredSessions } from '../useRecurrence.js'
import { describe, it, expect } from 'vitest';
import { generateDates, expandRules, mergeWithStoredSessions } from '../useRecurrence.js';
// ─── helpers de fixture ───────────────────────────────────────────────────────
function d (iso) {
const [y, m, day] = iso.split('-').map(Number)
return new Date(y, m - 1, day)
function d(iso) {
const [y, m, day] = iso.split('-').map(Number);
return new Date(y, m - 1, day);
}
function rule (overrides = {}) {
return {
id: 'rule-1',
owner_id: 'owner-1',
tenant_id: 'tenant-1',
patient_id: 'patient-1',
therapist_id: 'therapist-1',
status: 'ativo',
type: 'weekly',
weekdays: [1], // segunda
interval: 1,
start_date: '2026-03-02', // segunda
end_date: null,
max_occurrences: null,
open_ended: true,
start_time: '09:00',
end_time: '10:00',
...overrides,
}
function rule(overrides = {}) {
return {
id: 'rule-1',
owner_id: 'owner-1',
tenant_id: 'tenant-1',
patient_id: 'patient-1',
therapist_id: 'therapist-1',
status: 'ativo',
type: 'weekly',
weekdays: [1], // segunda
interval: 1,
start_date: '2026-03-02', // segunda
end_date: null,
max_occurrences: null,
open_ended: true,
start_time: '09:00',
end_time: '10:00',
...overrides
};
}
function exception (overrides = {}) {
return {
id: 'exc-1',
recurrence_id: 'rule-1',
original_date: '2026-03-09',
type: 'cancel_session',
new_date: null,
...overrides,
}
function exception(overrides = {}) {
return {
id: 'exc-1',
recurrence_id: 'rule-1',
original_date: '2026-03-09',
type: 'cancel_session',
new_date: null,
...overrides
};
}
// ─── generateDates ────────────────────────────────────────────────────────────
describe('generateDates — weekly', () => {
it('gera ocorrências semanais dentro do range', () => {
const r = rule({ type: 'weekly', weekdays: [1], start_date: '2026-03-02' })
const dates = generateDates(r, d('2026-03-01'), d('2026-03-31'))
const isos = dates.map(d => `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`)
expect(isos).toEqual(['2026-03-02', '2026-03-09', '2026-03-16', '2026-03-23', '2026-03-30'])
})
it('gera ocorrências semanais dentro do range', () => {
const r = rule({ type: 'weekly', weekdays: [1], start_date: '2026-03-02' });
const dates = generateDates(r, d('2026-03-01'), d('2026-03-31'));
const isos = dates.map((d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`);
expect(isos).toEqual(['2026-03-02', '2026-03-09', '2026-03-16', '2026-03-23', '2026-03-30']);
});
it('não gera antes do start_date da regra', () => {
const r = rule({ type: 'weekly', weekdays: [1], start_date: '2026-03-16' })
const dates = generateDates(r, d('2026-03-01'), d('2026-03-31'))
expect(dates.every(d => d >= new Date(2026, 2, 16))).toBe(true)
})
it('não gera antes do start_date da regra', () => {
const r = rule({ type: 'weekly', weekdays: [1], start_date: '2026-03-16' });
const dates = generateDates(r, d('2026-03-01'), d('2026-03-31'));
expect(dates.every((d) => d >= new Date(2026, 2, 16))).toBe(true);
});
it('não gera após o end_date da regra', () => {
const r = rule({ type: 'weekly', weekdays: [1], start_date: '2026-03-02', end_date: '2026-03-16' })
const dates = generateDates(r, d('2026-03-01'), d('2026-03-31'))
expect(dates.length).toBe(3) // 02, 09, 16
})
it('não gera após o end_date da regra', () => {
const r = rule({ type: 'weekly', weekdays: [1], start_date: '2026-03-02', end_date: '2026-03-16' });
const dates = generateDates(r, d('2026-03-01'), d('2026-03-31'));
expect(dates.length).toBe(3); // 02, 09, 16
});
it('respeita max_occurrences dentro do range', () => {
const r = rule({ type: 'weekly', weekdays: [1], start_date: '2026-03-02', max_occurrences: 2 })
const dates = generateDates(r, d('2026-03-01'), d('2026-03-31'))
expect(dates.length).toBe(2)
})
it('respeita max_occurrences dentro do range', () => {
const r = rule({ type: 'weekly', weekdays: [1], start_date: '2026-03-02', max_occurrences: 2 });
const dates = generateDates(r, d('2026-03-01'), d('2026-03-31'));
expect(dates.length).toBe(2);
});
it('respeita max_occurrences globalmente — range começa na 3ª semana', () => {
// 4 ocorrências totais, range começa na semana 3 → só 2 dentro do range
const r = rule({ type: 'weekly', weekdays: [1], start_date: '2026-03-02', max_occurrences: 4 })
const dates = generateDates(r, d('2026-03-15'), d('2026-04-30'))
// 16, 23, 30 → mas max=4 globalmente (2 antes do range + 2 no range)
expect(dates.length).toBe(2) // 2026-03-16, 2026-03-23
})
})
it('respeita max_occurrences globalmente — range começa na 3ª semana', () => {
// 4 ocorrências totais, range começa na semana 3 → só 2 dentro do range
const r = rule({ type: 'weekly', weekdays: [1], start_date: '2026-03-02', max_occurrences: 4 });
const dates = generateDates(r, d('2026-03-15'), d('2026-04-30'));
// 16, 23, 30 → mas max=4 globalmente (2 antes do range + 2 no range)
expect(dates.length).toBe(2); // 2026-03-16, 2026-03-23
});
});
describe('generateDates — biweekly', () => {
it('gera ocorrências a cada 2 semanas', () => {
const r = rule({ type: 'biweekly', weekdays: [1], interval: 2, start_date: '2026-03-02' })
const dates = generateDates(r, d('2026-03-01'), d('2026-04-30'))
const isos = dates.map(d => `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`)
expect(isos).toEqual(['2026-03-02', '2026-03-16', '2026-03-30', '2026-04-13', '2026-04-27'])
})
})
it('gera ocorrências a cada 2 semanas', () => {
const r = rule({ type: 'biweekly', weekdays: [1], interval: 2, start_date: '2026-03-02' });
const dates = generateDates(r, d('2026-03-01'), d('2026-04-30'));
const isos = dates.map((d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`);
expect(isos).toEqual(['2026-03-02', '2026-03-16', '2026-03-30', '2026-04-13', '2026-04-27']);
});
});
describe('generateDates — custom_weekdays', () => {
it('gera ocorrências em múltiplos dias da semana', () => {
const r = rule({ type: 'custom_weekdays', weekdays: [1, 3], start_date: '2026-03-02' }) // seg e qua
const dates = generateDates(r, d('2026-03-01'), d('2026-03-08'))
const isos = dates.map(d => `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`)
expect(isos).toEqual(['2026-03-02', '2026-03-04'])
})
it('gera ocorrências em múltiplos dias da semana', () => {
const r = rule({ type: 'custom_weekdays', weekdays: [1, 3], start_date: '2026-03-02' }); // seg e qua
const dates = generateDates(r, d('2026-03-01'), d('2026-03-08'));
const isos = dates.map((d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`);
expect(isos).toEqual(['2026-03-02', '2026-03-04']);
});
it('respeita max_occurrences globalmente com custom_weekdays', () => {
// 2 dias/semana, max=3 → semana 1 (02,04), semana 2 (09) e para
const r = rule({ type: 'custom_weekdays', weekdays: [1, 3], start_date: '2026-03-02', max_occurrences: 3 })
const dates = generateDates(r, d('2026-03-01'), d('2026-03-31'))
expect(dates.length).toBe(3)
})
it('respeita max_occurrences globalmente com custom_weekdays', () => {
// 2 dias/semana, max=3 → semana 1 (02,04), semana 2 (09) e para
const r = rule({ type: 'custom_weekdays', weekdays: [1, 3], start_date: '2026-03-02', max_occurrences: 3 });
const dates = generateDates(r, d('2026-03-01'), d('2026-03-31'));
expect(dates.length).toBe(3);
});
it('max_occurrences globalmente — range começa na semana 2', () => {
// semana 1 já consumiu 2 ocorrências (02, 04), max=3 → só 1 no range (09)
const r = rule({ type: 'custom_weekdays', weekdays: [1, 3], start_date: '2026-03-02', max_occurrences: 3 })
const dates = generateDates(r, d('2026-03-08'), d('2026-03-31'))
expect(dates.length).toBe(1)
})
})
it('max_occurrences globalmente — range começa na semana 2', () => {
// semana 1 já consumiu 2 ocorrências (02, 04), max=3 → só 1 no range (09)
const r = rule({ type: 'custom_weekdays', weekdays: [1, 3], start_date: '2026-03-02', max_occurrences: 3 });
const dates = generateDates(r, d('2026-03-08'), d('2026-03-31'));
expect(dates.length).toBe(1);
});
});
describe('generateDates — monthly', () => {
it('gera ocorrências mensais no mesmo dia', () => {
const r = rule({ type: 'monthly', weekdays: [1], start_date: '2026-01-15' })
const dates = generateDates(r, d('2026-01-01'), d('2026-04-30'))
expect(dates.length).toBe(4)
expect(dates.every(d => d.getDate() === 15)).toBe(true)
})
it('gera ocorrências mensais no mesmo dia', () => {
const r = rule({ type: 'monthly', weekdays: [1], start_date: '2026-01-15' });
const dates = generateDates(r, d('2026-01-01'), d('2026-04-30'));
expect(dates.length).toBe(4);
expect(dates.every((d) => d.getDate() === 15)).toBe(true);
});
it('respeita max_occurrences globalmente — range começa no mês 3', () => {
const r = rule({ type: 'monthly', weekdays: [1], start_date: '2026-01-15', max_occurrences: 3 })
const dates = generateDates(r, d('2026-03-01'), d('2026-12-31'))
expect(dates.length).toBe(1) // só março (jan+fev já consumiram 2 de 3)
})
})
it('respeita max_occurrences globalmente — range começa no mês 3', () => {
const r = rule({ type: 'monthly', weekdays: [1], start_date: '2026-01-15', max_occurrences: 3 });
const dates = generateDates(r, d('2026-03-01'), d('2026-12-31'));
expect(dates.length).toBe(1); // só março (jan+fev já consumiram 2 de 3)
});
});
describe('generateDates — yearly', () => {
it('gera ocorrências anuais', () => {
const r = rule({ type: 'yearly', weekdays: [1], start_date: '2024-06-15' })
const dates = generateDates(r, d('2024-01-01'), d('2027-12-31'))
expect(dates.length).toBe(4) // 2024, 2025, 2026, 2027
})
it('gera ocorrências anuais', () => {
const r = rule({ type: 'yearly', weekdays: [1], start_date: '2024-06-15' });
const dates = generateDates(r, d('2024-01-01'), d('2027-12-31'));
expect(dates.length).toBe(4); // 2024, 2025, 2026, 2027
});
it('respeita max_occurrences globalmente — range começa no ano 3', () => {
const r = rule({ type: 'yearly', weekdays: [1], start_date: '2024-06-15', max_occurrences: 3 })
const dates = generateDates(r, d('2026-01-01'), d('2030-12-31'))
expect(dates.length).toBe(1) // só 2026 (2024+2025 já consumiram 2 de 3)
})
})
it('respeita max_occurrences globalmente — range começa no ano 3', () => {
const r = rule({ type: 'yearly', weekdays: [1], start_date: '2024-06-15', max_occurrences: 3 });
const dates = generateDates(r, d('2026-01-01'), d('2030-12-31'));
expect(dates.length).toBe(1); // só 2026 (2024+2025 já consumiram 2 de 3)
});
});
// ─── expandRules ─────────────────────────────────────────────────────────────
describe('expandRules', () => {
it('gera ocorrência normal sem exceção', () => {
const rules = [rule()]
const occs = expandRules(rules, [], d('2026-03-01'), d('2026-03-08'))
expect(occs.length).toBe(1)
expect(occs[0].status).toBe('agendado')
expect(occs[0].exception_type).toBeNull()
})
it('gera ocorrência normal sem exceção', () => {
const rules = [rule()];
const occs = expandRules(rules, [], d('2026-03-01'), d('2026-03-08'));
expect(occs.length).toBe(1);
expect(occs[0].status).toBe('agendado');
expect(occs[0].exception_type).toBeNull();
});
it('cancela ocorrência com cancel_session', () => {
const rules = [rule()]
const excs = [exception({ type: 'cancel_session', original_date: '2026-03-02' })]
const occs = expandRules(rules, excs, d('2026-03-01'), d('2026-03-08'))
expect(occs[0].status).toBe('cancelado')
expect(occs[0].exception_type).toBe('cancel_session')
})
it('cancela ocorrência com cancel_session', () => {
const rules = [rule()];
const excs = [exception({ type: 'cancel_session', original_date: '2026-03-02' })];
const occs = expandRules(rules, excs, d('2026-03-01'), d('2026-03-08'));
expect(occs[0].status).toBe('cancelado');
expect(occs[0].exception_type).toBe('cancel_session');
});
it('marca falta com patient_missed', () => {
const rules = [rule()]
const excs = [exception({ type: 'patient_missed', original_date: '2026-03-02' })]
const occs = expandRules(rules, excs, d('2026-03-01'), d('2026-03-08'))
expect(occs[0].status).toBe('faltou')
})
it('marca falta com patient_missed', () => {
const rules = [rule()];
const excs = [exception({ type: 'patient_missed', original_date: '2026-03-02' })];
const occs = expandRules(rules, excs, d('2026-03-01'), d('2026-03-08'));
expect(occs[0].status).toBe('faltou');
});
it('remarca ocorrência para nova data', () => {
const rules = [rule()]
const excs = [exception({
type: 'reschedule_session',
original_date: '2026-03-02',
new_date: '2026-03-04',
})]
const occs = expandRules(rules, excs, d('2026-03-01'), d('2026-03-08'))
// A ocorrência do dia 02 foi movida para 04
expect(occs[0].status).toBe('remarcado')
expect(occs[0].exception_type).toBe('reschedule_session')
// inicio_em reflete a nova data (04); original_date no main loop recebe new_date
expect(occs[0].inicio_em).toContain('2026-03-04')
})
it('remarca ocorrência para nova data', () => {
const rules = [rule()];
const excs = [
exception({
type: 'reschedule_session',
original_date: '2026-03-02',
new_date: '2026-03-04'
})
];
const occs = expandRules(rules, excs, d('2026-03-01'), d('2026-03-08'));
// A ocorrência do dia 02 foi movida para 04
expect(occs[0].status).toBe('remarcado');
expect(occs[0].exception_type).toBe('reschedule_session');
// inicio_em reflete a nova data (04); original_date no main loop recebe new_date
expect(occs[0].inicio_em).toContain('2026-03-04');
});
it('post-pass: remarcação inbound — original fora do range, new_date dentro', () => {
// Regra começa em 02/03 (segunda). original_date = 09/03 está FORA do range 16-22.
// new_date = 17/03 está DENTRO do range.
const rules = [rule({ start_date: '2026-03-02' })]
const excs = [exception({
type: 'reschedule_session',
original_date: '2026-03-09', // fora do range
new_date: '2026-03-17', // dentro do range
})]
const occs = expandRules(rules, excs, d('2026-03-16'), d('2026-03-22'))
const remarcado = occs.find(o => o.status === 'remarcado')
expect(remarcado).toBeDefined()
expect(remarcado.original_date).toBe('2026-03-09')
expect(remarcado.inicio_em).toContain('2026-03-17')
})
it('post-pass: remarcação inbound — original fora do range, new_date dentro', () => {
// Regra começa em 02/03 (segunda). original_date = 09/03 está FORA do range 16-22.
// new_date = 17/03 está DENTRO do range.
const rules = [rule({ start_date: '2026-03-02' })];
const excs = [
exception({
type: 'reschedule_session',
original_date: '2026-03-09', // fora do range
new_date: '2026-03-17' // dentro do range
})
];
const occs = expandRules(rules, excs, d('2026-03-16'), d('2026-03-22'));
const remarcado = occs.find((o) => o.status === 'remarcado');
expect(remarcado).toBeDefined();
expect(remarcado.original_date).toBe('2026-03-09');
expect(remarcado.inicio_em).toContain('2026-03-17');
});
it('ignora regra cancelada', () => {
const rules = [rule({ status: 'cancelado' })]
const occs = expandRules(rules, [], d('2026-03-01'), d('2026-03-31'))
expect(occs.length).toBe(0)
})
})
it('ignora regra cancelada', () => {
const rules = [rule({ status: 'cancelado' })];
const occs = expandRules(rules, [], d('2026-03-01'), d('2026-03-31'));
expect(occs.length).toBe(0);
});
});
// ─── mergeWithStoredSessions ──────────────────────────────────────────────────
describe('mergeWithStoredSessions', () => {
it('sessão real substitui ocorrência virtual para a mesma regra+data', () => {
const occs = [{
recurrence_id: 'rule-1',
original_date: '2026-03-02',
status: 'agendado',
is_occurrence: true,
is_real_session: false,
titulo: 'Virtual',
}]
const storedRows = [{
id: 'ev-real-1',
recurrence_id: 'rule-1',
recurrence_date: '2026-03-02',
status: 'realizado',
titulo: 'Real',
}]
const merged = mergeWithStoredSessions(occs, storedRows)
expect(merged.length).toBe(1)
expect(merged[0].is_real_session).toBe(true)
expect(merged[0].status).toBe('realizado')
expect(merged[0].titulo).toBe('Real')
})
it('sessão real substitui ocorrência virtual para a mesma regra+data', () => {
const occs = [
{
recurrence_id: 'rule-1',
original_date: '2026-03-02',
status: 'agendado',
is_occurrence: true,
is_real_session: false,
titulo: 'Virtual'
}
];
const storedRows = [
{
id: 'ev-real-1',
recurrence_id: 'rule-1',
recurrence_date: '2026-03-02',
status: 'realizado',
titulo: 'Real'
}
];
const merged = mergeWithStoredSessions(occs, storedRows);
expect(merged.length).toBe(1);
expect(merged[0].is_real_session).toBe(true);
expect(merged[0].status).toBe('realizado');
expect(merged[0].titulo).toBe('Real');
});
it('mantém ocorrência virtual quando não há sessão real', () => {
const occs = [{
recurrence_id: 'rule-1',
original_date: '2026-03-02',
status: 'agendado',
is_occurrence: true,
}]
const merged = mergeWithStoredSessions(occs, [])
expect(merged.length).toBe(1)
expect(merged[0].is_occurrence).toBe(true)
})
it('mantém ocorrência virtual quando não há sessão real', () => {
const occs = [
{
recurrence_id: 'rule-1',
original_date: '2026-03-02',
status: 'agendado',
is_occurrence: true
}
];
const merged = mergeWithStoredSessions(occs, []);
expect(merged.length).toBe(1);
expect(merged[0].is_occurrence).toBe(true);
});
it('adiciona sessão real órfã (sem ocorrência correspondente)', () => {
const storedRows = [{
id: 'ev-orphan',
recurrence_id: 'rule-1',
recurrence_date: '2026-03-30', // data fora do range expandido
status: 'agendado',
}]
const merged = mergeWithStoredSessions([], storedRows)
expect(merged.length).toBe(1)
expect(merged[0].is_real_session).toBe(true)
})
it('adiciona sessão real órfã (sem ocorrência correspondente)', () => {
const storedRows = [
{
id: 'ev-orphan',
recurrence_id: 'rule-1',
recurrence_date: '2026-03-30', // data fora do range expandido
status: 'agendado'
}
];
const merged = mergeWithStoredSessions([], storedRows);
expect(merged.length).toBe(1);
expect(merged[0].is_real_session).toBe(true);
});
it('não duplica quando há tanto ocorrência quanto sessão real', () => {
const occs = [
{ recurrence_id: 'rule-1', original_date: '2026-03-02', is_occurrence: true },
{ recurrence_id: 'rule-1', original_date: '2026-03-09', is_occurrence: true },
]
const stored = [
{ recurrence_id: 'rule-1', recurrence_date: '2026-03-02', status: 'realizado' }
]
const merged = mergeWithStoredSessions(occs, stored)
expect(merged.length).toBe(2)
})
})
it('não duplica quando há tanto ocorrência quanto sessão real', () => {
const occs = [
{ recurrence_id: 'rule-1', original_date: '2026-03-02', is_occurrence: true },
{ recurrence_id: 'rule-1', original_date: '2026-03-09', is_occurrence: true }
];
const stored = [{ recurrence_id: 'rule-1', recurrence_date: '2026-03-02', status: 'realizado' }];
const merged = mergeWithStoredSessions(occs, stored);
expect(merged.length).toBe(2);
});
});
@@ -14,77 +14,72 @@
| © 2026 — Todos os direitos reservados
|--------------------------------------------------------------------------
*/
import { ref } from 'vue'
import {
listClinicEvents,
createClinicAgendaEvento,
updateClinicAgendaEvento,
deleteClinicAgendaEvento
} from '@/features/agenda/services/agendaClinicRepository'
import { ref } from 'vue';
import { listClinicEvents, createClinicAgendaEvento, updateClinicAgendaEvento, deleteClinicAgendaEvento } from '@/features/agenda/services/agendaClinicRepository';
export function useAgendaClinicEvents () {
const loading = ref(false)
const error = ref('')
const rows = ref([])
export function useAgendaClinicEvents() {
const loading = ref(false);
const error = ref('');
const rows = ref([]);
async function loadClinicRange ({ tenantId, ownerIds, startISO, endISO }) {
loading.value = true
error.value = ''
try {
rows.value = await listClinicEvents({ tenantId, ownerIds, startISO, endISO })
} catch (e) {
error.value = e?.message || 'Falha ao carregar eventos.'
} finally {
loading.value = false
async function loadClinicRange({ tenantId, ownerIds, startISO, endISO }) {
loading.value = true;
error.value = '';
try {
rows.value = await listClinicEvents({ tenantId, ownerIds, startISO, endISO });
} catch (e) {
error.value = e?.message || 'Falha ao carregar eventos.';
} finally {
loading.value = false;
}
}
}
async function createClinic (payload, { tenantId } = {}) {
loading.value = true
error.value = ''
try {
return await createClinicAgendaEvento(payload, { tenantId })
} catch (e) {
error.value = e?.message || 'Falha ao criar evento.'
throw e
} finally {
loading.value = false
async function createClinic(payload, { tenantId } = {}) {
loading.value = true;
error.value = '';
try {
return await createClinicAgendaEvento(payload, { tenantId });
} catch (e) {
error.value = e?.message || 'Falha ao criar evento.';
throw e;
} finally {
loading.value = false;
}
}
}
async function updateClinic (id, patch, { tenantId } = {}) {
loading.value = true
error.value = ''
try {
return await updateClinicAgendaEvento(id, patch, { tenantId })
} catch (e) {
error.value = e?.message || 'Falha ao atualizar evento.'
throw e
} finally {
loading.value = false
async function updateClinic(id, patch, { tenantId } = {}) {
loading.value = true;
error.value = '';
try {
return await updateClinicAgendaEvento(id, patch, { tenantId });
} catch (e) {
error.value = e?.message || 'Falha ao atualizar evento.';
throw e;
} finally {
loading.value = false;
}
}
}
async function removeClinic (id, { tenantId } = {}) {
loading.value = true
error.value = ''
try {
return await deleteClinicAgendaEvento(id, { tenantId })
} catch (e) {
error.value = e?.message || 'Falha ao remover evento.'
throw e
} finally {
loading.value = false
async function removeClinic(id, { tenantId } = {}) {
loading.value = true;
error.value = '';
try {
return await deleteClinicAgendaEvento(id, { tenantId });
} catch (e) {
error.value = e?.message || 'Falha ao remover evento.';
throw e;
} finally {
loading.value = false;
}
}
}
return {
loading,
error,
rows,
loadClinicRange,
createClinic,
updateClinic,
removeClinic
}
}
return {
loading,
error,
rows,
loadClinicRange,
createClinic,
updateClinic,
removeClinic
};
}
@@ -14,26 +14,26 @@
| © 2026 — Todos os direitos reservados
|--------------------------------------------------------------------------
*/
import { ref } from 'vue'
import { listTenantStaff } from '../services/agendaRepository'
import { ref } from 'vue';
import { listTenantStaff } from '../services/agendaRepository';
export function useAgendaClinicStaff () {
const loading = ref(false)
const error = ref('')
const staff = ref([])
export function useAgendaClinicStaff() {
const loading = ref(false);
const error = ref('');
const staff = ref([]);
async function load (tenantId) {
loading.value = true
error.value = ''
try {
staff.value = await listTenantStaff(tenantId)
} catch (e) {
error.value = e?.message || 'Falha ao carregar profissionais.'
staff.value = []
} finally {
loading.value = false
async function load(tenantId) {
loading.value = true;
error.value = '';
try {
staff.value = await listTenantStaff(tenantId);
} catch (e) {
error.value = e?.message || 'Falha ao carregar profissionais.';
staff.value = [];
} finally {
loading.value = false;
}
}
}
return { loading, error, staff, load }
}
return { loading, error, staff, load };
}
+113 -136
View File
@@ -22,24 +22,24 @@
* Sessões com recurrence_id são sessões reais de uma série.
*/
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';
// ─── helpers internos ────────────────────────────────────────────────────────
function assertTenantId (tenantId) {
if (!tenantId || tenantId === 'null' || tenantId === 'undefined') {
throw new Error('Tenant ativo inválido. Selecione a clínica/tenant antes de operar na agenda.')
}
function assertTenantId(tenantId) {
if (!tenantId || tenantId === 'null' || tenantId === 'undefined') {
throw new Error('Tenant ativo inválido. Selecione a clínica/tenant antes de operar na agenda.');
}
}
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;
}
const BASE_SELECT = `
@@ -56,149 +56,126 @@ const BASE_SELECT = `
determined_commitments!agenda_eventos_determined_commitment_fk (
id, bg_color, text_color
)
`.trim()
`.trim();
export function useAgendaEvents () {
const rows = ref([])
const loading = ref(false)
const error = ref(null)
export function useAgendaEvents() {
const rows = ref([]);
const loading = ref(false);
const error = ref(null);
async function loadMyRange (start, end, ownerId) {
if (!ownerId) return
async function loadMyRange(start, end, ownerId) {
if (!ownerId) return;
const tenantStore = useTenantStore()
const tenantId = tenantStore.activeTenantId
assertTenantId(tenantId)
const tenantStore = useTenantStore();
const tenantId = tenantStore.activeTenantId;
assertTenantId(tenantId);
loading.value = true
error.value = null
try {
const { data, error: err } = await supabase
.from('agenda_eventos')
.select(BASE_SELECT)
.eq('tenant_id', tenantId)
.eq('owner_id', ownerId)
.is('mirror_of_event_id', null)
.gte('inicio_em', start)
.lte('inicio_em', end)
.order('inicio_em', { ascending: true })
loading.value = true;
error.value = null;
try {
const { data, error: err } = await supabase
.from('agenda_eventos')
.select(BASE_SELECT)
.eq('tenant_id', tenantId)
.eq('owner_id', ownerId)
.is('mirror_of_event_id', null)
.gte('inicio_em', start)
.lte('inicio_em', end)
.order('inicio_em', { ascending: true });
if (err) throw err
rows.value = (data || []).map(flattenRow)
} catch (e) {
error.value = e?.message || 'Erro ao carregar eventos'
rows.value = []
} finally {
loading.value = false
}
}
/**
* Cria um evento injetando tenant_id e owner_id automaticamente.
* owner_id é sempre o usuário autenticado — nunca vem do payload externo.
* tenant_id vem do tenantStore ativo — nunca do payload externo.
*/
async function create (payload) {
const tenantStore = useTenantStore()
const tenantId = tenantStore.activeTenantId
assertTenantId(tenantId)
const uid = await getUid()
// eslint-disable-next-line no-unused-vars
const { paciente_id: _dropped, ...rest } = payload
const safePayload = {
...rest,
tenant_id: tenantId,
owner_id: uid,
if (err) throw err;
rows.value = (data || []).map(flattenRow);
} catch (e) {
error.value = e?.message || 'Erro ao carregar eventos';
rows.value = [];
} finally {
loading.value = false;
}
}
const { data, error: err } = await supabase
.from('agenda_eventos')
.insert([safePayload])
.select(BASE_SELECT)
.single()
if (err) throw err
return flattenRow(data)
}
/**
* Cria um evento injetando tenant_id e owner_id automaticamente.
* owner_id é sempre o usuário autenticado — nunca vem do payload externo.
* tenant_id vem do tenantStore ativo — nunca do payload externo.
*/
async function create(payload) {
const tenantStore = useTenantStore();
const tenantId = tenantStore.activeTenantId;
assertTenantId(tenantId);
async function update (id, patch) {
if (!id) throw new Error('ID inválido.')
const uid = await getUid();
const tenantStore = useTenantStore()
const tenantId = tenantStore.activeTenantId
assertTenantId(tenantId)
// eslint-disable-next-line no-unused-vars
const { paciente_id: _dropped, ...rest } = payload;
const safePayload = {
...rest,
tenant_id: tenantId,
owner_id: uid
};
// eslint-disable-next-line no-unused-vars
const { paciente_id: _dropped, ...safePatch } = patch
const { data, error: err } = await supabase.from('agenda_eventos').insert([safePayload]).select(BASE_SELECT).single();
if (err) throw err;
return flattenRow(data);
}
const { data, error: err } = await supabase
.from('agenda_eventos')
.update(safePatch)
.eq('id', id)
.eq('tenant_id', tenantId)
.select(BASE_SELECT)
.single()
if (err) throw err
return flattenRow(data)
}
async function update(id, patch) {
if (!id) throw new Error('ID inválido.');
async function remove (id) {
if (!id) throw new Error('ID inválido.')
const tenantStore = useTenantStore();
const tenantId = tenantStore.activeTenantId;
assertTenantId(tenantId);
const tenantStore = useTenantStore()
const tenantId = tenantStore.activeTenantId
assertTenantId(tenantId)
// eslint-disable-next-line no-unused-vars
const { paciente_id: _dropped, ...safePatch } = patch;
const { error: err } = await supabase
.from('agenda_eventos')
.delete()
.eq('id', id)
.eq('tenant_id', tenantId)
if (err) throw err
}
const { data, error: err } = await supabase.from('agenda_eventos').update(safePatch).eq('id', id).eq('tenant_id', tenantId).select(BASE_SELECT).single();
if (err) throw err;
return flattenRow(data);
}
async function removeSeriesFrom (recurrenceId, fromDateISO) {
if (!recurrenceId) throw new Error('recurrenceId inválido.')
async function remove(id) {
if (!id) throw new Error('ID inválido.');
const tenantStore = useTenantStore()
const tenantId = tenantStore.activeTenantId
assertTenantId(tenantId)
const tenantStore = useTenantStore();
const tenantId = tenantStore.activeTenantId;
assertTenantId(tenantId);
const { error: err } = await supabase
.from('agenda_eventos')
.delete()
.eq('recurrence_id', recurrenceId)
.eq('tenant_id', tenantId)
.gte('recurrence_date', fromDateISO)
if (err) throw err
}
const { error: err } = await supabase.from('agenda_eventos').delete().eq('id', id).eq('tenant_id', tenantId);
if (err) throw err;
}
async function removeAllSeries (recurrenceId) {
if (!recurrenceId) throw new Error('recurrenceId inválido.')
async function removeSeriesFrom(recurrenceId, fromDateISO) {
if (!recurrenceId) throw new Error('recurrenceId inválido.');
const tenantStore = useTenantStore()
const tenantId = tenantStore.activeTenantId
assertTenantId(tenantId)
const tenantStore = useTenantStore();
const tenantId = tenantStore.activeTenantId;
assertTenantId(tenantId);
const { error: err } = await supabase
.from('agenda_eventos')
.delete()
.eq('recurrence_id', recurrenceId)
.eq('tenant_id', tenantId)
if (err) throw err
}
const { error: err } = await supabase.from('agenda_eventos').delete().eq('recurrence_id', recurrenceId).eq('tenant_id', tenantId).gte('recurrence_date', fromDateISO);
if (err) throw err;
}
return { rows, loading, error, loadMyRange, create, update, remove, removeSeriesFrom, removeAllSeries }
async function removeAllSeries(recurrenceId) {
if (!recurrenceId) throw new Error('recurrenceId inválido.');
const tenantStore = useTenantStore();
const tenantId = tenantStore.activeTenantId;
assertTenantId(tenantId);
const { error: err } = await supabase.from('agenda_eventos').delete().eq('recurrence_id', recurrenceId).eq('tenant_id', tenantId);
if (err) throw err;
}
return { rows, loading, error, loadMyRange, create, update, remove, removeSeriesFrom, removeAllSeries };
}
function flattenRow (r) {
if (!r) return r
const patient = r.patients || null
const out = { ...r }
delete out.patients
out.paciente_nome = patient?.nome_completo || out.paciente_nome || ''
out.paciente_avatar = patient?.avatar_url || out.paciente_avatar || ''
out.paciente_status = patient?.status || out.paciente_status || ''
return out
}
function flattenRow(r) {
if (!r) return r;
const patient = r.patients || null;
const out = { ...r };
delete out.patients;
out.paciente_nome = patient?.nome_completo || out.paciente_nome || '';
out.paciente_avatar = patient?.avatar_url || out.paciente_avatar || '';
out.paciente_status = patient?.status || out.paciente_status || '';
return out;
}
@@ -14,33 +14,30 @@
| © 2026 — Todos os direitos reservados
|--------------------------------------------------------------------------
*/
import { ref } from 'vue'
import { getMyAgendaSettings, getMyWorkSchedule } from '../services/agendaRepository'
import { ref } from 'vue';
import { getMyAgendaSettings, getMyWorkSchedule } from '../services/agendaRepository';
export function useAgendaSettings () {
const loading = ref(false)
const error = ref('')
const settings = ref(null)
const workRules = ref([]) // [{ dia_semana, hora_inicio, hora_fim }]
export function useAgendaSettings() {
const loading = ref(false);
const error = ref('');
const settings = ref(null);
const workRules = ref([]); // [{ dia_semana, hora_inicio, hora_fim }]
async function load () {
loading.value = true
error.value = ''
try {
const [cfg, rules] = await Promise.all([
getMyAgendaSettings(),
getMyWorkSchedule()
])
settings.value = cfg
workRules.value = rules
} catch (e) {
error.value = e?.message || 'Falha ao carregar configurações da agenda.'
settings.value = null
workRules.value = []
} finally {
loading.value = false
async function load() {
loading.value = true;
error.value = '';
try {
const [cfg, rules] = await Promise.all([getMyAgendaSettings(), getMyWorkSchedule()]);
settings.value = cfg;
workRules.value = rules;
} catch (e) {
error.value = e?.message || 'Falha ao carregar configurações da agenda.';
settings.value = null;
workRules.value = [];
} finally {
loading.value = false;
}
}
}
return { loading, error, settings, workRules, load }
return { loading, error, settings, workRules, load };
}
@@ -25,7 +25,7 @@
// 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'
import { supabase } from '@/lib/supabase/client';
// Shape interno de CommitmentItem:
// {
@@ -39,201 +39,171 @@ import { supabase } from '@/lib/supabase/client'
// }
/** 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),
}
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 () {
export function useCommitmentServices() {
// ── Carregar itens de um evento ──────────────────────────────────────
async function loadItems(eventId) {
if (!eventId) return [];
// ── 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 });
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
if (error) throw error;
return (data || []).map(_mapRow);
}
// 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)
// ── 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.');
if (updateError) throw updateError
}
}
// 1. Remove itens existentes deste evento
const { error: deleteError } = await supabase.from('commitment_services').delete().eq('commitment_id', eventId);
// ── 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 []
if (deleteError) throw deleteError;
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 })
// 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
}));
if (error) throw error
return (data || []).map(_mapRow)
}
const { error: insertError } = await supabase.from('commitment_services').insert(rows);
// ── 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.')
if (insertError) throw insertError;
}
const { error: deleteError } = await supabase
.from('recurrence_rule_services')
.delete()
.eq('rule_id', ruleId)
// 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 (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 (updateError) throw updateError;
}
}
if (fromDate) {
q = q.gte('inicio_em', fromDate)
// ── 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);
}
const { data: events, error: queryError } = await q
if (queryError) throw queryError
if (!events?.length) return
// ── 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.');
// 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
const { error: deleteError } = await supabase.from('recurrence_rule_services').delete().eq('rule_id', ruleId);
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
}
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;
}
}
return {
loadItems,
saveItems,
loadRuleItems,
saveRuleItems,
loadItemsOrTemplate,
propagateToSerie,
}
// ── 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
};
}
@@ -14,48 +14,48 @@
| © 2026 — Todos os direitos reservados
|--------------------------------------------------------------------------
*/
import { computed, ref } from 'vue'
import { supabase } from '@/lib/supabase/client'
import { computed, ref } from 'vue';
import { supabase } from '@/lib/supabase/client';
export function useDeterminedCommitments (tenantIdRef) {
const loading = ref(false)
const error = ref('')
const rows = ref([])
export function useDeterminedCommitments(tenantIdRef) {
const loading = ref(false);
const error = ref('');
const rows = ref([]);
const tenantId = computed(() => {
const v = tenantIdRef?.value ?? tenantIdRef
return v ? String(v) : ''
})
const tenantId = computed(() => {
const v = tenantIdRef?.value ?? tenantIdRef;
return v ? String(v) : '';
});
async function load () {
try {
if (!tenantId.value) {
rows.value = []
error.value = ''
return
}
if (loading.value) return
async function load() {
try {
if (!tenantId.value) {
rows.value = [];
error.value = '';
return;
}
if (loading.value) return;
loading.value = true
error.value = ''
loading.value = true;
error.value = '';
const { data, error: err } = await supabase
.from('determined_commitments')
.select('id,tenant_id,created_by,is_native,native_key,is_locked,active,name,description,bg_color,text_color,created_at,determined_commitment_fields(id,key,label,field_type,required,sort_order)')
.eq('tenant_id', tenantId.value) // ✅ SOMENTE tenant corrente
.eq('active', true)
.order('is_native', { ascending: false })
.order('name', { ascending: true })
const { data, error: err } = await supabase
.from('determined_commitments')
.select('id,tenant_id,created_by,is_native,native_key,is_locked,active,name,description,bg_color,text_color,created_at,determined_commitment_fields(id,key,label,field_type,required,sort_order)')
.eq('tenant_id', tenantId.value) // ✅ SOMENTE tenant corrente
.eq('active', true)
.order('is_native', { ascending: false })
.order('name', { ascending: true });
if (err) throw err
rows.value = data || []
} catch (e) {
error.value = e?.message || 'Falha ao carregar compromissos determinísticos.'
rows.value = []
} finally {
loading.value = false
if (err) throw err;
rows.value = data || [];
} catch (e) {
error.value = e?.message || 'Falha ao carregar compromissos determinísticos.';
rows.value = [];
} finally {
loading.value = false;
}
}
}
return { loading, error, rows, load }
}
return { loading, error, rows, load };
}
@@ -25,93 +25,83 @@
// 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'
import { ref } from 'vue';
import { supabase } from '@/lib/supabase/client';
export function useFinancialExceptions () {
const exceptions = ref([])
const loading = ref(false)
const error = ref('')
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 })
// ── 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
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
// ── 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
// ── 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 }
return { exceptions, loading, error, load, save, remove };
}
@@ -27,175 +27,166 @@
// togglePlanService(id, active) alterna active do procedimento
// removePlanService(id) DELETE definitivo do procedimento
import { ref } from 'vue'
import { supabase } from '@/lib/supabase/client'
import { ref } from 'vue';
import { supabase } from '@/lib/supabase/client';
export function useInsurancePlans () {
const plans = ref([])
const loading = ref(false)
const error = ref(null)
export function useInsurancePlans() {
const plans = ref([]);
const loading = ref(false);
const error = ref(null);
async function load (ownerId) {
if (!ownerId) return
loading.value = true
error.value = null
try {
const { data, error: err } = await supabase
.from('insurance_plans')
.select(`
async function load(ownerId) {
if (!ownerId) return;
loading.value = true;
error.value = null;
try {
const { data, error: err } = await supabase
.from('insurance_plans')
.select(
`
*,
insurance_plan_services (
id, name, value, active
)
`)
.eq('owner_id', ownerId)
.order('name')
if (err) throw err
plans.value = data || []
} catch (e) {
error.value = e?.message || 'Erro ao carregar convênios'
plans.value = []
} finally {
loading.value = false
`
)
.eq('owner_id', ownerId)
.order('name');
if (err) throw err;
plans.value = data || [];
} catch (e) {
error.value = e?.message || 'Erro ao carregar convênios';
plans.value = [];
} finally {
loading.value = false;
}
}
}
async function save (payload) {
error.value = null
try {
if (payload.id) {
const { error: err } = await supabase
.from('insurance_plans')
.update({
name: payload.name,
notes: payload.notes || null,
updated_at: new Date().toISOString(),
})
.eq('id', payload.id)
if (err) throw err
} else {
const { error: err } = await supabase
.from('insurance_plans')
.insert({
owner_id: payload.owner_id,
tenant_id: payload.tenant_id,
name: payload.name,
notes: payload.notes || null,
})
if (err) throw err
}
} catch (e) {
error.value = e?.message || 'Erro ao salvar convênio'
throw e
async function save(payload) {
error.value = null;
try {
if (payload.id) {
const { error: err } = await supabase
.from('insurance_plans')
.update({
name: payload.name,
notes: payload.notes || null,
updated_at: new Date().toISOString()
})
.eq('id', payload.id);
if (err) throw err;
} else {
const { error: err } = await supabase.from('insurance_plans').insert({
owner_id: payload.owner_id,
tenant_id: payload.tenant_id,
name: payload.name,
notes: payload.notes || null
});
if (err) throw err;
}
} catch (e) {
error.value = e?.message || 'Erro ao salvar convênio';
throw e;
}
}
}
async function toggle (id, active) {
error.value = null
try {
const { error: err } = await supabase
.from('insurance_plans')
.update({ active })
.eq('id', id)
if (err) throw err
const plan = plans.value.find(p => p.id === id)
if (plan) plan.active = active
} catch (e) {
error.value = e?.message || 'Erro ao atualizar convênio'
throw e
async function toggle(id, active) {
error.value = null;
try {
const { error: err } = await supabase.from('insurance_plans').update({ active }).eq('id', id);
if (err) throw err;
const plan = plans.value.find((p) => p.id === id);
if (plan) plan.active = active;
} catch (e) {
error.value = e?.message || 'Erro ao atualizar convênio';
throw e;
}
}
}
async function remove (id) {
error.value = null
try {
const { error: err } = await supabase
.from('insurance_plans')
.update({ active: false })
.eq('id', id)
if (err) throw err
const plan = plans.value.find(p => p.id === id)
if (plan) plan.active = false
} catch (e) {
error.value = e?.message || 'Erro ao remover convênio'
throw e
async function remove(id) {
error.value = null;
try {
const { error: err } = await supabase.from('insurance_plans').update({ active: false }).eq('id', id);
if (err) throw err;
const plan = plans.value.find((p) => p.id === id);
if (plan) plan.active = false;
} catch (e) {
error.value = e?.message || 'Erro ao remover convênio';
throw e;
}
}
}
async function savePlanService (payload) {
error.value = null
try {
if (payload.id) {
const { error: err } = await supabase
.from('insurance_plan_services')
.update({
name: payload.name,
value: payload.value,
})
.eq('id', payload.id)
if (err) throw err
} else {
const { error: err } = await supabase
.from('insurance_plan_services')
.insert({
insurance_plan_id: payload.insurance_plan_id,
name: payload.name,
value: payload.value,
})
if (err) throw err
}
} catch (e) {
error.value = e?.message || 'Erro ao salvar procedimento'
throw e
async function savePlanService(payload) {
error.value = null;
try {
if (payload.id) {
const { error: err } = await supabase
.from('insurance_plan_services')
.update({
name: payload.name,
value: payload.value
})
.eq('id', payload.id);
if (err) throw err;
} else {
const { error: err } = await supabase.from('insurance_plan_services').insert({
insurance_plan_id: payload.insurance_plan_id,
name: payload.name,
value: payload.value
});
if (err) throw err;
}
} catch (e) {
error.value = e?.message || 'Erro ao salvar procedimento';
throw e;
}
}
}
async function togglePlanService (id, active) {
error.value = null
try {
const { error: err } = await supabase
.from('insurance_plan_services')
.update({ active })
.eq('id', id)
if (err) throw err
} catch (e) {
error.value = e?.message || 'Erro ao atualizar procedimento'
throw e
async function togglePlanService(id, active) {
error.value = null;
try {
const { error: err } = await supabase.from('insurance_plan_services').update({ active }).eq('id', id);
if (err) throw err;
} catch (e) {
error.value = e?.message || 'Erro ao atualizar procedimento';
throw e;
}
}
}
async function removeDefinitivo (id) {
error.value = null
try {
const { error: err } = await supabase
.from('insurance_plans')
.delete()
.eq('id', id)
if (err) throw err
plans.value = plans.value.filter(p => p.id !== id)
} catch (e) {
error.value = e?.message || 'Erro ao remover convênio'
throw e
async function removeDefinitivo(id) {
error.value = null;
try {
const { error: err } = await supabase.from('insurance_plans').delete().eq('id', id);
if (err) throw err;
plans.value = plans.value.filter((p) => p.id !== id);
} catch (e) {
error.value = e?.message || 'Erro ao remover convênio';
throw e;
}
}
}
async function removePlanService (id) {
error.value = null
try {
const { error: err } = await supabase
.from('insurance_plan_services')
.delete()
.eq('id', id)
if (err) throw err
} catch (e) {
error.value = e?.message || 'Erro ao remover procedimento'
throw e
async function removePlanService(id) {
error.value = null;
try {
const { error: err } = await supabase.from('insurance_plan_services').delete().eq('id', id);
if (err) throw err;
} catch (e) {
error.value = e?.message || 'Erro ao remover procedimento';
throw e;
}
}
}
return {
plans, loading, error,
load, save, toggle, remove, removeDefinitivo,
savePlanService, togglePlanService, removePlanService,
}
return {
plans,
loading,
error,
load,
save,
toggle,
remove,
removeDefinitivo,
savePlanService,
togglePlanService,
removePlanService
};
}
@@ -26,107 +26,94 @@
// 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'
import { ref } from 'vue';
import { supabase } from '@/lib/supabase/client';
export function usePatientDiscounts () {
const discounts = ref([])
const loading = ref(false)
const error = ref('')
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 })
// ── 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
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
// ── 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
// ── 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()
// ── 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
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 }
return { discounts, loading, error, load, save, remove, loadActive };
}
@@ -20,52 +20,49 @@
// null = commitment_id
// Regra: lookup exato → fallback NULL → null se nenhum configurado
import { ref } from 'vue'
import { supabase } from '@/lib/supabase/client'
import { ref } from 'vue';
import { supabase } from '@/lib/supabase/client';
export function useProfessionalPricing () {
const rows = ref([]) // professional_pricing rows
const loading = ref(false)
const error = ref('')
export function useProfessionalPricing() {
const rows = ref([]); // professional_pricing rows
const loading = ref(false);
const error = ref('');
// ── Carregar todos os preços do owner ──────────────────────────────
async function load (ownerId) {
if (!ownerId) return
loading.value = true
error.value = ''
try {
const { data, error: err } = await supabase
.from('professional_pricing')
.select('id, determined_commitment_id, price, notes')
.eq('owner_id', ownerId)
// ── Carregar todos os preços do owner ──────────────────────────────
async function load(ownerId) {
if (!ownerId) return;
loading.value = true;
error.value = '';
try {
const { data, error: err } = await supabase.from('professional_pricing').select('id, determined_commitment_id, price, notes').eq('owner_id', ownerId);
if (err) throw err
rows.value = data || []
} catch (e) {
error.value = e?.message || 'Falha ao carregar precificação.'
rows.value = []
} finally {
loading.value = false
}
}
// ── Consulta: preço para um tipo de compromisso ────────────────────
// 1. Linha com determined_commitment_id === commitmentId
// 2. Fallback: linha com determined_commitment_id === null (preço padrão)
// 3. null se nada configurado
function getPriceFor (commitmentId) {
if (!rows.value.length) return null
// match exato
if (commitmentId) {
const exact = rows.value.find(r => r.determined_commitment_id === commitmentId)
if (exact && exact.price != null) return Number(exact.price)
if (err) throw err;
rows.value = data || [];
} catch (e) {
error.value = e?.message || 'Falha ao carregar precificação.';
rows.value = [];
} finally {
loading.value = false;
}
}
// fallback padrão (commitment_id IS NULL)
const def = rows.value.find(r => r.determined_commitment_id === null)
return def && def.price != null ? Number(def.price) : null
}
// ── Consulta: preço para um tipo de compromisso ────────────────────
// 1. Linha com determined_commitment_id === commitmentId
// 2. Fallback: linha com determined_commitment_id === null (preço padrão)
// 3. null se nada configurado
function getPriceFor(commitmentId) {
if (!rows.value.length) return null;
return { rows, loading, error, load, getPriceFor }
// match exato
if (commitmentId) {
const exact = rows.value.find((r) => r.determined_commitment_id === commitmentId);
if (exact && exact.price != null) return Number(exact.price);
}
// fallback padrão (commitment_id IS NULL)
const def = rows.value.find((r) => r.determined_commitment_id === null);
return def && def.price != null ? Number(def.price) : null;
}
return { rows, loading, error, load, getPriceFor };
}
File diff suppressed because it is too large Load Diff
+72 -88
View File
@@ -26,106 +26,90 @@
// getDefaultPrice() preço do primeiro serviço ativo, ou null
// getPriceFor(id) preço de um serviço específico, ou null
import { ref } from 'vue'
import { supabase } from '@/lib/supabase/client'
import { ref } from 'vue';
import { supabase } from '@/lib/supabase/client';
export function useServices () {
const services = ref([])
const loading = ref(false)
const error = ref('')
export function useServices() {
const services = 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('services')
.select('id, name, description, price, duration_min, active')
.eq('owner_id', ownerId)
.order('created_at', { ascending: true })
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).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
if (err) throw err;
services.value = data || [];
} catch (e) {
error.value = e?.message || 'Falha ao carregar serviços.';
services.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('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
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;
}
}
}
async function toggle (id, active) {
error.value = ''
try {
const { error: err } = await supabase
.from('services')
.update({ active })
.eq('id', id)
if (err) throw err
const svc = services.value.find(s => s.id === id)
if (svc) svc.active = active
} catch (e) {
error.value = e?.message || 'Falha ao atualizar serviço.'
throw e
async function toggle(id, active) {
error.value = '';
try {
const { error: err } = await supabase.from('services').update({ active }).eq('id', id);
if (err) throw err;
const svc = services.value.find((s) => s.id === id);
if (svc) svc.active = active;
} catch (e) {
error.value = e?.message || 'Falha ao atualizar serviço.';
throw e;
}
}
}
async function remove (id) {
error.value = ''
try {
const { error: err } = await supabase
.from('services')
.delete()
.eq('id', id)
if (err) throw err
services.value = services.value.filter(s => s.id !== id)
} catch (e) {
const msg = String(e?.message || '')
if (msg.includes('commitment_services_service_id_fkey') || msg.includes('violates foreign key constraint')) {
error.value = 'Este serviço está vinculado a sessões e não pode ser removido. Use Desativar para ocultá-lo.'
} else {
error.value = e?.message || 'Falha ao remover serviço.'
}
throw e
async function remove(id) {
error.value = '';
try {
const { error: err } = await supabase.from('services').delete().eq('id', id);
if (err) throw err;
services.value = services.value.filter((s) => s.id !== id);
} catch (e) {
const msg = String(e?.message || '');
if (msg.includes('commitment_services_service_id_fkey') || msg.includes('violates foreign key constraint')) {
error.value = 'Este serviço está vinculado a sessões e não pode ser removido. Use Desativar para ocultá-lo.';
} else {
error.value = e?.message || 'Falha ao remover serviço.';
}
throw e;
}
}
}
function getDefaultPrice (serviceId) {
if (serviceId) {
const svc = services.value.find(s => s.id === serviceId)
return svc?.price != null ? Number(svc.price) : null
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.find((s) => s.active);
return first?.price != null ? Number(first.price) : null;
}
const first = services.value.find(s => s.active)
return first?.price != null ? Number(first.price) : null
}
function getPriceFor (serviceId) {
return getDefaultPrice(serviceId)
}
function getPriceFor(serviceId) {
return getDefaultPrice(serviceId);
}
return { services, loading, error, load, save, toggle, remove, getDefaultPrice, getPriceFor }
return { services, loading, error, load, save, toggle, remove, getDefaultPrice, getPriceFor };
}