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
@@ -11,278 +11,275 @@
* - buildWeeklyBreakBackgroundEvents
*/
import { describe, it, expect } from 'vitest'
import {
mapAgendaEventosToCalendarEvents,
mapAgendaEventosToClinicResourceEvents,
buildNextSessions,
minutesToDuration,
tituloFallback,
calcDefaultSlotDuration,
buildWeeklyBreakBackgroundEvents,
} from '../agendaMappers.js'
import { describe, it, expect } from 'vitest';
import { mapAgendaEventosToCalendarEvents, mapAgendaEventosToClinicResourceEvents, buildNextSessions, minutesToDuration, tituloFallback, calcDefaultSlotDuration, buildWeeklyBreakBackgroundEvents } from '../agendaMappers.js';
// ─── fixtures ─────────────────────────────────────────────────────────────────
function evento (overrides = {}) {
return {
id: 'ev-1',
titulo: 'Sessão Teste',
tipo: 'sessao',
status: 'agendado',
inicio_em: '2026-03-10T09:00:00',
fim_em: '2026-03-10T10:00:00',
owner_id: 'owner-1',
tenant_id: 'tenant-1',
patient_id: 'patient-1',
modalidade: 'presencial',
...overrides,
}
function evento(overrides = {}) {
return {
id: 'ev-1',
titulo: 'Sessão Teste',
tipo: 'sessao',
status: 'agendado',
inicio_em: '2026-03-10T09:00:00',
fim_em: '2026-03-10T10:00:00',
owner_id: 'owner-1',
tenant_id: 'tenant-1',
patient_id: 'patient-1',
modalidade: 'presencial',
...overrides
};
}
// ─── mapAgendaEventosToCalendarEvents ─────────────────────────────────────────
describe('mapAgendaEventosToCalendarEvents', () => {
it('mapeia um evento simples para o shape do FullCalendar', () => {
const [ev] = mapAgendaEventosToCalendarEvents([evento()])
expect(ev.id).toBe('ev-1')
expect(ev.start).toBe('2026-03-10T09:00:00')
expect(ev.end).toBe('2026-03-10T10:00:00')
expect(ev.extendedProps.tipo).toBe('sessao')
expect(ev.extendedProps.status).toBe('agendado')
})
it('mapeia um evento simples para o shape do FullCalendar', () => {
const [ev] = mapAgendaEventosToCalendarEvents([evento()]);
expect(ev.id).toBe('ev-1');
expect(ev.start).toBe('2026-03-10T09:00:00');
expect(ev.end).toBe('2026-03-10T10:00:00');
expect(ev.extendedProps.tipo).toBe('sessao');
expect(ev.extendedProps.status).toBe('agendado');
});
it('filtra rows null/undefined', () => {
const result = mapAgendaEventosToCalendarEvents([null, undefined, evento()])
expect(result.length).toBe(1)
})
it('filtra rows null/undefined', () => {
const result = mapAgendaEventosToCalendarEvents([null, undefined, evento()]);
expect(result.length).toBe(1);
});
it('retorna array vazio para input vazio', () => {
expect(mapAgendaEventosToCalendarEvents([])).toEqual([])
expect(mapAgendaEventosToCalendarEvents(null)).toEqual([])
})
it('retorna array vazio para input vazio', () => {
expect(mapAgendaEventosToCalendarEvents([])).toEqual([]);
expect(mapAgendaEventosToCalendarEvents(null)).toEqual([]);
});
it('inclui ícone ✓ no título para status realizado', () => {
const [ev] = mapAgendaEventosToCalendarEvents([evento({ status: 'realizado' })])
expect(ev.title).toContain('✓')
})
it('inclui ícone ✓ no título para status realizado', () => {
const [ev] = mapAgendaEventosToCalendarEvents([evento({ status: 'realizado' })]);
expect(ev.title).toContain('✓');
});
it('inclui ícone ✗ no título para status faltou', () => {
const [ev] = mapAgendaEventosToCalendarEvents([evento({ status: 'faltou' })])
expect(ev.title).toContain('✗')
})
it('inclui ícone ✗ no título para status faltou', () => {
const [ev] = mapAgendaEventosToCalendarEvents([evento({ status: 'faltou' })]);
expect(ev.title).toContain('✗');
});
it('inclui ícone ∅ no título para status cancelado', () => {
const [ev] = mapAgendaEventosToCalendarEvents([evento({ status: 'cancelado' })])
expect(ev.title).toContain('∅')
})
it('inclui ícone ∅ no título para status cancelado', () => {
const [ev] = mapAgendaEventosToCalendarEvents([evento({ status: 'cancelado' })]);
expect(ev.title).toContain('∅');
});
it('inclui ícone ↺ no título para status remarcado', () => {
const [ev] = mapAgendaEventosToCalendarEvents([evento({ status: 'remarcado' })])
expect(ev.title).toContain('↺')
})
it('inclui ícone ↺ no título para status remarcado', () => {
const [ev] = mapAgendaEventosToCalendarEvents([evento({ status: 'remarcado' })]);
expect(ev.title).toContain('↺');
});
it('inclui ícone ↻ para ocorrências de série', () => {
const [ev] = mapAgendaEventosToCalendarEvents([evento({ recurrence_id: 'rule-1', is_occurrence: true })])
expect(ev.title).toContain('↻')
})
it('inclui ícone ↻ para ocorrências de série', () => {
const [ev] = mapAgendaEventosToCalendarEvents([evento({ recurrence_id: 'rule-1', is_occurrence: true })]);
expect(ev.title).toContain('↻');
});
it('aplica cor de fundo para status faltou', () => {
const [ev] = mapAgendaEventosToCalendarEvents([evento({ status: 'faltou' })])
expect(ev.backgroundColor).toBe('#ef4444')
})
it('aplica cor de fundo para status faltou', () => {
const [ev] = mapAgendaEventosToCalendarEvents([evento({ status: 'faltou' })]);
expect(ev.backgroundColor).toBe('#ef4444');
});
it('aplica cor de fundo para status cancelado', () => {
const [ev] = mapAgendaEventosToCalendarEvents([evento({ status: 'cancelado' })])
expect(ev.backgroundColor).toBe('#f97316')
})
it('aplica cor de fundo para status cancelado', () => {
const [ev] = mapAgendaEventosToCalendarEvents([evento({ status: 'cancelado' })]);
expect(ev.backgroundColor).toBe('#f97316');
});
it('aplica cor de fundo para status remarcado', () => {
const [ev] = mapAgendaEventosToCalendarEvents([evento({ status: 'remarcado' })])
expect(ev.backgroundColor).toBe('#a855f7')
})
it('aplica cor de fundo para status remarcado', () => {
const [ev] = mapAgendaEventosToCalendarEvents([evento({ status: 'remarcado' })]);
expect(ev.backgroundColor).toBe('#a855f7');
});
it('usa titulo_custom quando disponível', () => {
const [ev] = mapAgendaEventosToCalendarEvents([evento({ titulo_custom: 'Personalizado' })])
expect(ev.title).toContain('Personalizado')
})
it('usa titulo_custom quando disponível', () => {
const [ev] = mapAgendaEventosToCalendarEvents([evento({ titulo_custom: 'Personalizado' })]);
expect(ev.title).toContain('Personalizado');
});
it('usa nome do paciente via patients join quando titulo ausente', () => {
const [ev] = mapAgendaEventosToCalendarEvents([evento({
titulo: null,
titulo_custom: null,
patients: { nome_completo: 'João Silva', avatar_url: null }
})])
expect(ev.title).toContain('João Silva')
})
it('usa nome do paciente via patients join quando titulo ausente', () => {
const [ev] = mapAgendaEventosToCalendarEvents([
evento({
titulo: null,
titulo_custom: null,
patients: { nome_completo: 'João Silva', avatar_url: null }
})
]);
expect(ev.title).toContain('João Silva');
});
it('mapeia patient_id corretamente', () => {
const [ev] = mapAgendaEventosToCalendarEvents([evento({ patient_id: 'p-123' })])
expect(ev.extendedProps.patient_id).toBe('p-123')
expect(ev.extendedProps.paciente_id).toBe('p-123') // alias
})
it('mapeia patient_id corretamente', () => {
const [ev] = mapAgendaEventosToCalendarEvents([evento({ patient_id: 'p-123' })]);
expect(ev.extendedProps.patient_id).toBe('p-123');
expect(ev.extendedProps.paciente_id).toBe('p-123'); // alias
});
it('mapeia recurrence_id e original_date', () => {
const [ev] = mapAgendaEventosToCalendarEvents([evento({
recurrence_id: 'rule-abc',
original_date: '2026-03-10',
})])
expect(ev.extendedProps.recurrence_id).toBe('rule-abc')
expect(ev.extendedProps.original_date).toBe('2026-03-10')
})
it('mapeia recurrence_id e original_date', () => {
const [ev] = mapAgendaEventosToCalendarEvents([
evento({
recurrence_id: 'rule-abc',
original_date: '2026-03-10'
})
]);
expect(ev.extendedProps.recurrence_id).toBe('rule-abc');
expect(ev.extendedProps.original_date).toBe('2026-03-10');
});
it('mapeia exception_type', () => {
const [ev] = mapAgendaEventosToCalendarEvents([evento({
exception_type: 'patient_missed',
status: 'faltou',
})])
expect(ev.extendedProps.exception_type).toBe('patient_missed')
expect(ev.extendedProps.is_exception).toBe(true)
})
})
it('mapeia exception_type', () => {
const [ev] = mapAgendaEventosToCalendarEvents([
evento({
exception_type: 'patient_missed',
status: 'faltou'
})
]);
expect(ev.extendedProps.exception_type).toBe('patient_missed');
expect(ev.extendedProps.is_exception).toBe(true);
});
});
// ─── mapAgendaEventosToClinicResourceEvents ───────────────────────────────────
describe('mapAgendaEventosToClinicResourceEvents', () => {
it('adiciona resourceId baseado em owner_id', () => {
const [ev] = mapAgendaEventosToClinicResourceEvents([evento({ owner_id: 'owner-99' })])
expect(ev.resourceId).toBe('owner-99')
})
it('adiciona resourceId baseado em owner_id', () => {
const [ev] = mapAgendaEventosToClinicResourceEvents([evento({ owner_id: 'owner-99' })]);
expect(ev.resourceId).toBe('owner-99');
});
it('usa terapeuta_id como fallback para resourceId', () => {
const [ev] = mapAgendaEventosToClinicResourceEvents([evento({ owner_id: null, terapeuta_id: 'tera-1' })])
expect(ev.resourceId).toBe('tera-1')
})
})
it('usa terapeuta_id como fallback para resourceId', () => {
const [ev] = mapAgendaEventosToClinicResourceEvents([evento({ owner_id: null, terapeuta_id: 'tera-1' })]);
expect(ev.resourceId).toBe('tera-1');
});
});
// ─── buildNextSessions ────────────────────────────────────────────────────────
describe('buildNextSessions', () => {
it('filtra sessões no passado', () => {
const now = new Date('2026-03-10T12:00:00')
const rows = [
evento({ id: 'past', fim_em: '2026-03-09T10:00:00' }),
evento({ id: 'future', fim_em: '2026-03-11T10:00:00' }),
]
const result = buildNextSessions(rows, now)
expect(result.length).toBe(1)
expect(result[0].id).toBe('future')
})
it('filtra sessões no passado', () => {
const now = new Date('2026-03-10T12:00:00');
const rows = [evento({ id: 'past', fim_em: '2026-03-09T10:00:00' }), evento({ id: 'future', fim_em: '2026-03-11T10:00:00' })];
const result = buildNextSessions(rows, now);
expect(result.length).toBe(1);
expect(result[0].id).toBe('future');
});
it('inclui sessão cujo fim_em é agora (mesmo ms)', () => {
const now = new Date('2026-03-10T10:00:00')
const rows = [evento({ fim_em: '2026-03-10T10:00:00' })]
const result = buildNextSessions(rows, now)
expect(result.length).toBe(1)
})
it('inclui sessão cujo fim_em é agora (mesmo ms)', () => {
const now = new Date('2026-03-10T10:00:00');
const rows = [evento({ fim_em: '2026-03-10T10:00:00' })];
const result = buildNextSessions(rows, now);
expect(result.length).toBe(1);
});
it('limita a 6 sessões', () => {
const now = new Date('2026-01-01')
const rows = Array.from({ length: 10 }, (_, i) => evento({
id: `ev-${i}`,
fim_em: `2026-03-${String(i + 10).padStart(2,'0')}T10:00:00`,
}))
const result = buildNextSessions(rows, now)
expect(result.length).toBe(6)
})
it('limita a 6 sessões', () => {
const now = new Date('2026-01-01');
const rows = Array.from({ length: 10 }, (_, i) =>
evento({
id: `ev-${i}`,
fim_em: `2026-03-${String(i + 10).padStart(2, '0')}T10:00:00`
})
);
const result = buildNextSessions(rows, now);
expect(result.length).toBe(6);
});
it('retorna shape correto', () => {
const now = new Date('2026-01-01')
const [s] = buildNextSessions([evento()], now)
expect(s).toMatchObject({
id: 'ev-1',
title: 'Sessão Teste',
startISO: '2026-03-10T09:00:00',
endISO: '2026-03-10T10:00:00',
tipo: 'sessao',
status: 'agendado',
})
})
it('retorna shape correto', () => {
const now = new Date('2026-01-01');
const [s] = buildNextSessions([evento()], now);
expect(s).toMatchObject({
id: 'ev-1',
title: 'Sessão Teste',
startISO: '2026-03-10T09:00:00',
endISO: '2026-03-10T10:00:00',
tipo: 'sessao',
status: 'agendado'
});
});
it('mapeia pacienteId de patient_id', () => {
const now = new Date('2026-01-01')
const [s] = buildNextSessions([evento({ patient_id: 'p-999' })], now)
expect(s.pacienteId).toBe('p-999')
})
})
it('mapeia pacienteId de patient_id', () => {
const now = new Date('2026-01-01');
const [s] = buildNextSessions([evento({ patient_id: 'p-999' })], now);
expect(s.pacienteId).toBe('p-999');
});
});
// ─── minutesToDuration ────────────────────────────────────────────────────────
describe('minutesToDuration', () => {
it('30 minutos → 00:30:00', () => expect(minutesToDuration(30)).toBe('00:30:00'))
it('60 minutos → 01:00:00', () => expect(minutesToDuration(60)).toBe('01:00:00'))
it('90 minutos → 01:30:00', () => expect(minutesToDuration(90)).toBe('01:30:00'))
it('0 minutos → 00:00:00', () => expect(minutesToDuration(0)).toBe('00:00:00'))
})
it('30 minutos → 00:30:00', () => expect(minutesToDuration(30)).toBe('00:30:00'));
it('60 minutos → 01:00:00', () => expect(minutesToDuration(60)).toBe('01:00:00'));
it('90 minutos → 01:30:00', () => expect(minutesToDuration(90)).toBe('01:30:00'));
it('0 minutos → 00:00:00', () => expect(minutesToDuration(0)).toBe('00:00:00'));
});
// ─── tituloFallback ───────────────────────────────────────────────────────────
describe('tituloFallback', () => {
it('sessao → Sessão', () => expect(tituloFallback('sessao')).toBe('Sessão'))
it('bloqueio → Bloqueio', () => expect(tituloFallback('bloqueio')).toBe('Bloqueio'))
it('pessoal → Pessoal', () => expect(tituloFallback('pessoal')).toBe('Pessoal'))
it('clinica → Clínica', () => expect(tituloFallback('clinica')).toBe('Clínica'))
it('desconhecido → Compromisso', () => expect(tituloFallback('outro')).toBe('Compromisso'))
it('null → Compromisso', () => expect(tituloFallback(null)).toBe('Compromisso'))
})
it('sessao → Sessão', () => expect(tituloFallback('sessao')).toBe('Sessão'));
it('bloqueio → Bloqueio', () => expect(tituloFallback('bloqueio')).toBe('Bloqueio'));
it('pessoal → Pessoal', () => expect(tituloFallback('pessoal')).toBe('Pessoal'));
it('clinica → Clínica', () => expect(tituloFallback('clinica')).toBe('Clínica'));
it('desconhecido → Compromisso', () => expect(tituloFallback('outro')).toBe('Compromisso'));
it('null → Compromisso', () => expect(tituloFallback(null)).toBe('Compromisso'));
});
// ─── calcDefaultSlotDuration ──────────────────────────────────────────────────
describe('calcDefaultSlotDuration', () => {
it('usa granularidade custom quando ativa', () => {
const s = { usar_granularidade_custom: true, granularidade_min: 15 }
expect(calcDefaultSlotDuration(s)).toBe('00:15:00')
})
it('usa granularidade custom quando ativa', () => {
const s = { usar_granularidade_custom: true, granularidade_min: 15 };
expect(calcDefaultSlotDuration(s)).toBe('00:15:00');
});
it('usa admin_slot_visual_minutos como fallback', () => {
const s = { admin_slot_visual_minutos: 20 }
expect(calcDefaultSlotDuration(s)).toBe('00:20:00')
})
it('usa admin_slot_visual_minutos como fallback', () => {
const s = { admin_slot_visual_minutos: 20 };
expect(calcDefaultSlotDuration(s)).toBe('00:20:00');
});
it('usa 30 min como padrão quando nenhuma configuração', () => {
expect(calcDefaultSlotDuration({})).toBe('00:30:00')
expect(calcDefaultSlotDuration(null)).toBe('00:30:00')
})
})
it('usa 30 min como padrão quando nenhuma configuração', () => {
expect(calcDefaultSlotDuration({})).toBe('00:30:00');
expect(calcDefaultSlotDuration(null)).toBe('00:30:00');
});
});
// ─── buildWeeklyBreakBackgroundEvents ────────────────────────────────────────
describe('buildWeeklyBreakBackgroundEvents', () => {
it('retorna vazio para input vazio', () => {
const result = buildWeeklyBreakBackgroundEvents([], new Date('2026-03-01'), new Date('2026-03-08'))
expect(result).toEqual([])
})
it('retorna vazio para input vazio', () => {
const result = buildWeeklyBreakBackgroundEvents([], new Date('2026-03-01'), new Date('2026-03-08'));
expect(result).toEqual([]);
});
it('gera eventos de background para pausa no dia correto', () => {
const pausas = [{ weekday: 1, start: '12:00', end: '13:00', label: 'Almoço' }] // segunda
const result = buildWeeklyBreakBackgroundEvents(
pausas,
new Date(2026, 2, 1), // dom
new Date(2026, 2, 8), // dom
)
expect(result.length).toBe(1)
expect(result[0].display).toBe('background')
expect(result[0].start).toContain('2026-03-02') // segunda
expect(result[0].extendedProps.label).toBe('Almoço')
})
it('gera eventos de background para pausa no dia correto', () => {
const pausas = [{ weekday: 1, start: '12:00', end: '13:00', label: 'Almoço' }]; // segunda
const result = buildWeeklyBreakBackgroundEvents(
pausas,
new Date(2026, 2, 1), // dom
new Date(2026, 2, 8) // dom
);
expect(result.length).toBe(1);
expect(result[0].display).toBe('background');
expect(result[0].start).toContain('2026-03-02'); // segunda
expect(result[0].extendedProps.label).toBe('Almoço');
});
it('gera uma pausa por semana quando range cobre 2 semanas', () => {
const pausas = [{ weekday: 1, start: '12:00', end: '13:00' }] // toda segunda
const result = buildWeeklyBreakBackgroundEvents(
pausas,
new Date(2026, 2, 1), // dom 01/03
new Date(2026, 2, 15), // dom 15/03
)
expect(result.length).toBe(2) // seg 02 e seg 09
})
it('gera uma pausa por semana quando range cobre 2 semanas', () => {
const pausas = [{ weekday: 1, start: '12:00', end: '13:00' }]; // toda segunda
const result = buildWeeklyBreakBackgroundEvents(
pausas,
new Date(2026, 2, 1), // dom 01/03
new Date(2026, 2, 15) // dom 15/03
);
expect(result.length).toBe(2); // seg 02 e seg 09
});
it('não gera para dias diferentes', () => {
const pausas = [{ weekday: 5, start: '12:00', end: '13:00' }] // sexta
const result = buildWeeklyBreakBackgroundEvents(
pausas,
new Date(2026, 2, 2), // seg
new Date(2026, 2, 5), // qui
)
expect(result.length).toBe(0)
})
})
it('não gera para dias diferentes', () => {
const pausas = [{ weekday: 5, start: '12:00', end: '13:00' }]; // sexta
const result = buildWeeklyBreakBackgroundEvents(
pausas,
new Date(2026, 2, 2), // seg
new Date(2026, 2, 5) // qui
);
expect(result.length).toBe(0);
});
});
@@ -14,62 +14,58 @@
| © 2026 — Todos os direitos reservados
|--------------------------------------------------------------------------
*/
import { supabase } from '@/lib/supabase/client'
import { supabase } from '@/lib/supabase/client';
function assertValidTenantId (tenantId) {
if (!tenantId || tenantId === 'null' || tenantId === 'undefined') {
throw new Error('Tenant ativo inválido. Selecione a clínica/tenant antes de carregar a agenda.')
}
function assertValidTenantId(tenantId) {
if (!tenantId || tenantId === 'null' || tenantId === 'undefined') {
throw new Error('Tenant ativo inválido. Selecione a clínica/tenant antes de carregar a agenda.');
}
}
function assertValidIsoRange (startISO, endISO) {
if (!startISO || !endISO) throw new Error('Intervalo inválido (startISO/endISO).')
function assertValidIsoRange(startISO, endISO) {
if (!startISO || !endISO) throw new Error('Intervalo inválido (startISO/endISO).');
}
function sanitizeOwnerIds (ownerIds) {
return (ownerIds || [])
.filter(id => typeof id === 'string' && id && id !== 'null' && id !== 'undefined')
function sanitizeOwnerIds(ownerIds) {
return (ownerIds || []).filter((id) => typeof id === 'string' && id && id !== 'null' && id !== 'undefined');
}
/**
* Lista eventos para mosaico da clínica (admin/secretaria) dentro de um tenant específico.
* IMPORTANTE: SEM tenant_id aqui vira vazamento multi-tenant.
*/
export async function listClinicEvents ({ tenantId, ownerIds, startISO, endISO } = {}) {
assertValidTenantId(tenantId)
if (!ownerIds?.length) return []
assertValidIsoRange(startISO, endISO)
export async function listClinicEvents({ tenantId, ownerIds, startISO, endISO } = {}) {
assertValidTenantId(tenantId);
if (!ownerIds?.length) return [];
assertValidIsoRange(startISO, endISO);
const safeOwnerIds = sanitizeOwnerIds(ownerIds)
if (!safeOwnerIds.length) return []
const safeOwnerIds = sanitizeOwnerIds(ownerIds);
if (!safeOwnerIds.length) return [];
const { data, error } = await supabase
.from('agenda_eventos')
.select('*, patients!agenda_eventos_patient_id_fkey(id, nome_completo, avatar_url, status), determined_commitments!agenda_eventos_determined_commitment_fk(id, bg_color, text_color)')
.eq('tenant_id', tenantId)
.in('owner_id', safeOwnerIds)
.gte('inicio_em', startISO)
.lt('inicio_em', endISO)
.order('inicio_em', { ascending: true })
const { data, error } = await supabase
.from('agenda_eventos')
.select('*, patients!agenda_eventos_patient_id_fkey(id, nome_completo, avatar_url, status), determined_commitments!agenda_eventos_determined_commitment_fk(id, bg_color, text_color)')
.eq('tenant_id', tenantId)
.in('owner_id', safeOwnerIds)
.gte('inicio_em', startISO)
.lt('inicio_em', endISO)
.order('inicio_em', { ascending: true });
if (error) throw error
return data || []
if (error) throw error;
return data || [];
}
/**
* Lista profissionais/membros para montar colunas no mosaico.
* Usando view "v_tenant_staff" (como você já tem).
*/
export async function listTenantStaff (tenantId) {
assertValidTenantId(tenantId)
export async function listTenantStaff(tenantId) {
assertValidTenantId(tenantId);
const { data, error } = await supabase
.from('v_tenant_staff')
.select('*')
.eq('tenant_id', tenantId)
const { data, error } = await supabase.from('v_tenant_staff').select('*').eq('tenant_id', tenantId);
if (error) throw error
return data || []
if (error) throw error;
return data || [];
}
/**
@@ -81,28 +77,24 @@ export async function listTenantStaff (tenantId) {
* - clinic_admin/tenant_admin pode criar para qualquer owner dentro do tenant
* - therapist não deve conseguir passar daqui (guard + RLS)
*/
export async function createClinicAgendaEvento (payload, { tenantId } = {}) {
assertValidTenantId(tenantId)
if (!payload) throw new Error('Payload vazio.')
export async function createClinicAgendaEvento(payload, { tenantId } = {}) {
assertValidTenantId(tenantId);
if (!payload) throw new Error('Payload vazio.');
const ownerId = payload.owner_id
if (!ownerId || ownerId === 'null' || ownerId === 'undefined') {
throw new Error('owner_id é obrigatório para criação pela clínica.')
}
const ownerId = payload.owner_id;
if (!ownerId || ownerId === 'null' || ownerId === 'undefined') {
throw new Error('owner_id é obrigatório para criação pela clínica.');
}
const insertPayload = {
...payload,
tenant_id: tenantId
}
const insertPayload = {
...payload,
tenant_id: tenantId
};
const { data, error } = await supabase
.from('agenda_eventos')
.insert(insertPayload)
.select('*')
.single()
const { data, error } = await supabase.from('agenda_eventos').insert(insertPayload).select('*').single();
if (error) throw error
return data
if (error) throw error;
return data;
}
/**
@@ -110,37 +102,27 @@ export async function createClinicAgendaEvento (payload, { tenantId } = {}) {
* - filtra por id + tenant_id (evita update cruzado)
* - permite editar owner_id (caso você mova evento para outro profissional)
*/
export async function updateClinicAgendaEvento (id, patch, { tenantId } = {}) {
if (!id) throw new Error('ID inválido.')
if (!patch) throw new Error('Patch vazio.')
assertValidTenantId(tenantId)
export async function updateClinicAgendaEvento(id, patch, { tenantId } = {}) {
if (!id) throw new Error('ID inválido.');
if (!patch) throw new Error('Patch vazio.');
assertValidTenantId(tenantId);
const { data, error } = await supabase
.from('agenda_eventos')
.update(patch)
.eq('id', id)
.eq('tenant_id', tenantId)
.select('*')
.single()
const { data, error } = await supabase.from('agenda_eventos').update(patch).eq('id', id).eq('tenant_id', tenantId).select('*').single();
if (error) throw error
return data
if (error) throw error;
return data;
}
/**
* Delete seguro para clínica:
* - filtra por id + tenant_id
*/
export async function deleteClinicAgendaEvento (id, { tenantId } = {}) {
if (!id) throw new Error('ID inválido.')
assertValidTenantId(tenantId)
export async function deleteClinicAgendaEvento(id, { tenantId } = {}) {
if (!id) throw new Error('ID inválido.');
assertValidTenantId(tenantId);
const { error } = await supabase
.from('agenda_eventos')
.delete()
.eq('id', id)
.eq('tenant_id', tenantId)
const { error } = await supabase.from('agenda_eventos').delete().eq('id', id).eq('tenant_id', tenantId);
if (error) throw error
return true
}
if (error) throw error;
return true;
}
+190 -191
View File
@@ -24,278 +24,277 @@
// mapAgendaEventosToCalendarEvents
// ─────────────────────────────────────────────────────────────────────────────
export function mapAgendaEventosToCalendarEvents (rows) {
return (rows || []).map(_mapRow).filter(Boolean)
export function mapAgendaEventosToCalendarEvents(rows) {
return (rows || []).map(_mapRow).filter(Boolean);
}
// ─────────────────────────────────────────────────────────────────────────────
// mapAgendaEventosToClinicResourceEvents
// ─────────────────────────────────────────────────────────────────────────────
export function mapAgendaEventosToClinicResourceEvents (rows) {
return (rows || []).map((r) => {
const ev = _mapRow(r)
if (!ev) return null
ev.resourceId = normalizeId(r?.owner_id ?? r?.terapeuta_id ?? null)
return ev
}).filter(Boolean)
export function mapAgendaEventosToClinicResourceEvents(rows) {
return (rows || [])
.map((r) => {
const ev = _mapRow(r);
if (!ev) return null;
ev.resourceId = normalizeId(r?.owner_id ?? r?.terapeuta_id ?? null);
return ev;
})
.filter(Boolean);
}
// ─────────────────────────────────────────────────────────────────────────────
// mapper interno
// ─────────────────────────────────────────────────────────────────────────────
function _mapRow (r) {
if (!r) return null
function _mapRow(r) {
if (!r) return null;
const isOccurrence = !!r.is_occurrence
const isRealSession = !isOccurrence
const isOccurrence = !!r.is_occurrence;
const isRealSession = !isOccurrence;
const ownerId = normalizeId(r?.owner_id ?? r?.terapeuta_id ?? null)
const ownerId = normalizeId(r?.owner_id ?? r?.terapeuta_id ?? null);
// commitment / cores
const commitment = r.determined_commitments ?? r.commitment ?? null
const baseBg = commitment?.bg_color ? `#${commitment.bg_color}` : null
const baseTxt = commitment?.text_color ? `#${commitment.text_color}` : null
const statusBg = _statusBgColor(r.status)
const bgColor = statusBg ?? baseBg ?? undefined
const txtColor = baseTxt ?? (statusBg ? '#ffffff' : undefined)
// commitment / cores
const commitment = r.determined_commitments ?? r.commitment ?? null;
const baseBg = commitment?.bg_color ? `#${commitment.bg_color}` : null;
const baseTxt = commitment?.text_color ? `#${commitment.text_color}` : null;
const statusBg = _statusBgColor(r.status);
const bgColor = statusBg ?? baseBg ?? undefined;
const txtColor = baseTxt ?? (statusBg ? '#ffffff' : undefined);
// título
const nomeP = r.patients?.nome_completo ?? r.paciente_nome ?? r.patient_name ?? ''
const titleBase = r.titulo_custom || r.titulo || (nomeP ? nomeP : tituloFallback(r.tipo))
const icon = _statusIcon(r.status, isOccurrence, !!r.recurrence_id)
const title = `${icon}${titleBase}`
// título
const nomeP = r.patients?.nome_completo ?? r.paciente_nome ?? r.patient_name ?? '';
const titleBase = r.titulo_custom || r.titulo || (nomeP ? nomeP : tituloFallback(r.tipo));
const icon = _statusIcon(r.status, isOccurrence, !!r.recurrence_id);
const title = `${icon}${titleBase}`;
// recorrência — nova + fallback legada
const recurrenceId = r.recurrence_id ?? null
const originalDate = r.original_date ?? r.recurrence_date ?? null
const exceptionType = r.exception_type ?? null
// recorrência — nova + fallback legada
const recurrenceId = r.recurrence_id ?? null;
const originalDate = r.original_date ?? r.recurrence_date ?? null;
const exceptionType = r.exception_type ?? null;
return {
id: r.id ?? `occ::${recurrenceId}::${originalDate}`,
title,
start: r.inicio_em,
end: r.fim_em,
return {
id: r.id ?? `occ::${recurrenceId}::${originalDate}`,
title,
start: r.inicio_em,
end: r.fim_em,
...(bgColor && { backgroundColor: bgColor, borderColor: bgColor }),
...(txtColor && { textColor: txtColor }),
...(bgColor && { backgroundColor: bgColor, borderColor: bgColor }),
...(txtColor && { textColor: txtColor }),
extendedProps: {
// identidade
dbId: r.id ?? null,
isOccurrence,
isRealSession,
extendedProps: {
// identidade
dbId: r.id ?? null,
isOccurrence,
isRealSession,
// owner
owner_id: ownerId,
terapeuta_id: normalizeId(r?.terapeuta_id ?? null),
// owner
owner_id: ownerId,
terapeuta_id: normalizeId(r?.terapeuta_id ?? null),
// compromisso
tipo: r.tipo ?? null,
status: r.status ?? null,
determined_commitment_id: r.determined_commitment_id ?? null,
commitment_bg_color: bgColor ?? null,
commitment_text_color: txtColor ?? null,
// compromisso
tipo: r.tipo ?? null,
status: r.status ?? null,
determined_commitment_id: r.determined_commitment_id ?? null,
commitment_bg_color: bgColor ?? null,
commitment_text_color: txtColor ?? null,
// paciente
patient_id: r.patient_id ?? null,
paciente_id: r.patient_id ?? null, // alias para compatibilidade com dialog/form
paciente_nome: nomeP,
paciente_avatar: r.patients?.avatar_url ?? r.paciente_avatar ?? null,
paciente_status: r.patients?.status ?? r.paciente_status ?? null,
// paciente
patient_id: r.patient_id ?? null,
paciente_id: r.patient_id ?? null, // alias para compatibilidade com dialog/form
paciente_nome: nomeP,
paciente_avatar: r.patients?.avatar_url ?? r.paciente_avatar ?? null,
paciente_status: r.patients?.status ?? r.paciente_status ?? null,
// campos
observacoes: r.observacoes ?? null,
titulo_custom: r.titulo_custom ?? null,
extra_fields: r.extra_fields ?? null,
modalidade: r.modalidade ?? null,
// campos
observacoes: r.observacoes ?? null,
titulo_custom: r.titulo_custom ?? null,
extra_fields: r.extra_fields ?? null,
modalidade: r.modalidade ?? null,
// privacidade (clínica)
visibility_scope: r.visibility_scope ?? null,
masked: !!r.masked,
// privacidade (clínica)
visibility_scope: r.visibility_scope ?? null,
masked: !!r.masked,
// recorrência — NOVA arquitetura
recurrence_id: recurrenceId,
original_date: originalDate,
exception_type: exceptionType,
exception_id: r.exception_id ?? null,
exception_reason: r.exception_reason ?? null,
// recorrência — NOVA arquitetura
recurrence_id: recurrenceId,
original_date: originalDate,
exception_type: exceptionType,
exception_id: r.exception_id ?? null,
exception_reason: r.exception_reason ?? null,
// recorrência — fallback LEGADA (não quebra enquanto migra)
serie_id: r.serie_id ?? recurrenceId ?? null,
serie_dia_semana: r.agenda_series?.dia_semana ?? r.serie_dia_semana ?? null,
serie_hora: r.agenda_series?.hora_inicio ?? r.serie_hora ?? null,
serie_duracao: r.agenda_series?.duracao_min ?? r.serie_duracao ?? null,
serie_status: r.agenda_series?.status ?? r.serie_status ?? null,
is_exception: r.is_exception ?? (exceptionType != null),
// recorrência — fallback LEGADA (não quebra enquanto migra)
serie_id: r.serie_id ?? recurrenceId ?? null,
serie_dia_semana: r.agenda_series?.dia_semana ?? r.serie_dia_semana ?? null,
serie_hora: r.agenda_series?.hora_inicio ?? r.serie_hora ?? null,
serie_duracao: r.agenda_series?.duracao_min ?? r.serie_duracao ?? null,
serie_status: r.agenda_series?.status ?? r.serie_status ?? null,
is_exception: r.is_exception ?? exceptionType != null,
// financeiro
price: r.price ?? null,
billed: r.billed ?? false,
billing_contract_id: r.billing_contract_id ?? null,
insurance_plan_id: r.insurance_plan_id ?? null,
insurance_guide_number: r.insurance_guide_number ?? null,
insurance_value: r.insurance_value != null ? Number(r.insurance_value) : null,
insurance_plan_service_id: r.insurance_plan_service_id ?? null,
// financeiro
price: r.price ?? null,
billed: r.billed ?? false,
billing_contract_id: r.billing_contract_id ?? null,
insurance_plan_id: r.insurance_plan_id ?? null,
insurance_guide_number: r.insurance_guide_number ?? null,
insurance_value: r.insurance_value != null ? Number(r.insurance_value) : null,
insurance_plan_service_id: r.insurance_plan_service_id ?? null,
// timestamps
inicio_em: r.inicio_em,
fim_em: r.fim_em,
tenant_id: r.tenant_id ?? null,
}
}
// timestamps
inicio_em: r.inicio_em,
fim_em: r.fim_em,
tenant_id: r.tenant_id ?? null
}
};
}
// ─────────────────────────────────────────────────────────────────────────────
// buildNextSessions
// ─────────────────────────────────────────────────────────────────────────────
export function buildNextSessions (rows, now = new Date()) {
const nowMs = now.getTime()
return (rows || [])
.filter(r => new Date(r.fim_em).getTime() >= nowMs)
.slice(0, 6)
.map(r => ({
id: r.id,
title: r.titulo || tituloFallback(r.tipo),
startISO: r.inicio_em,
endISO: r.fim_em,
tipo: r.tipo,
status: r.status,
pacienteId: r.patient_id || null
}))
export function buildNextSessions(rows, now = new Date()) {
const nowMs = now.getTime();
return (rows || [])
.filter((r) => new Date(r.fim_em).getTime() >= nowMs)
.slice(0, 6)
.map((r) => ({
id: r.id,
title: r.titulo || tituloFallback(r.tipo),
startISO: r.inicio_em,
endISO: r.fim_em,
tipo: r.tipo,
status: r.status,
pacienteId: r.patient_id || null
}));
}
// ─────────────────────────────────────────────────────────────────────────────
// calcDefaultSlotDuration
// ─────────────────────────────────────────────────────────────────────────────
export function calcDefaultSlotDuration (settings) {
const min =
((settings?.usar_granularidade_custom && settings?.granularidade_min) || 0) ||
settings?.admin_slot_visual_minutos ||
30
return minutesToDuration(min)
export function calcDefaultSlotDuration(settings) {
const min = (settings?.usar_granularidade_custom && settings?.granularidade_min) || 0 || settings?.admin_slot_visual_minutos || 30;
return minutesToDuration(min);
}
// ─────────────────────────────────────────────────────────────────────────────
// buildWeeklyBreakBackgroundEvents — código original preservado integralmente
// ─────────────────────────────────────────────────────────────────────────────
export function buildWeeklyBreakBackgroundEvents (pausas, rangeStart, rangeEnd) {
if (!Array.isArray(pausas) || pausas.length === 0) return []
export function buildWeeklyBreakBackgroundEvents(pausas, rangeStart, rangeEnd) {
if (!Array.isArray(pausas) || pausas.length === 0) return [];
const out = []
const dayMs = 24 * 60 * 60 * 1000
const out = [];
const dayMs = 24 * 60 * 60 * 1000;
for (let ts = startOfDay(rangeStart).getTime(); ts < rangeEnd.getTime(); ts += dayMs) {
const d = new Date(ts)
const dow = d.getDay()
for (let ts = startOfDay(rangeStart).getTime(); ts < rangeEnd.getTime(); ts += dayMs) {
const d = new Date(ts);
const dow = d.getDay();
for (const p of pausas) {
const wd = normalizeWeekday(p?.weekday ?? p?.dia_semana)
if (wd === null || wd !== dow) continue
for (const p of pausas) {
const wd = normalizeWeekday(p?.weekday ?? p?.dia_semana);
if (wd === null || wd !== dow) continue;
const start = asTime(p?.start ?? p?.inicio ?? p?.from)
const end = asTime(p?.end ?? p?.fim ?? p?.to)
if (!start || !end) continue
const start = asTime(p?.start ?? p?.inicio ?? p?.from);
const end = asTime(p?.end ?? p?.fim ?? p?.to);
if (!start || !end) continue;
out.push({
id: `break-${ts}-${start}-${end}`,
start: combineDateTimeISO(d, start),
end: combineDateTimeISO(d, end),
display: 'background',
overlap: false,
extendedProps: { kind: 'break', label: p?.label ?? 'Pausa' }
})
out.push({
id: `break-${ts}-${start}-${end}`,
start: combineDateTimeISO(d, start),
end: combineDateTimeISO(d, end),
display: 'background',
overlap: false,
extendedProps: { kind: 'break', label: p?.label ?? 'Pausa' }
});
}
}
}
return out
return out;
}
// ─────────────────────────────────────────────────────────────────────────────
// minutesToDuration / tituloFallback
// ─────────────────────────────────────────────────────────────────────────────
export function minutesToDuration (min) {
const h = Math.floor(min / 60)
const m = min % 60
return `${String(h).padStart(2,'0')}:${String(m).padStart(2,'0')}:00`
export function minutesToDuration(min) {
const h = Math.floor(min / 60);
const m = min % 60;
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:00`;
}
export function tituloFallback (tipo) {
const t = String(tipo || '').toLowerCase()
if (t.includes('sess')) return 'Sessão'
if (t.includes('block') || t.includes('bloq')) return 'Bloqueio'
if (t.includes('pessoal')) return 'Pessoal'
if (t.includes('clin')) return 'Clínica'
return 'Compromisso'
export function tituloFallback(tipo) {
const t = String(tipo || '').toLowerCase();
if (t.includes('sess')) return 'Sessão';
if (t.includes('block') || t.includes('bloq')) return 'Bloqueio';
if (t.includes('pessoal')) return 'Pessoal';
if (t.includes('clin')) return 'Clínica';
return 'Compromisso';
}
// ─────────────────────────────────────────────────────────────────────────────
// helpers de status
// ─────────────────────────────────────────────────────────────────────────────
function _statusBgColor (status) {
const map = {
agendado: '#3b82f6', // azul
realizado: '#22c55e', // verde
faltou: '#f97316', // laranja
cancelado: '#ef4444', // vermelho
remarcar: '#a855f7', // roxo
bloqueado: '#6b7280', // cinza
}
return map[status] ?? null
function _statusBgColor(status) {
const map = {
agendado: '#3b82f6', // azul
realizado: '#22c55e', // verde
faltou: '#f97316', // laranja
cancelado: '#ef4444', // vermelho
remarcar: '#a855f7', // roxo
bloqueado: '#6b7280' // cinza
};
return map[status] ?? null;
}
function _statusIcon (status, isOccurrence, hasSerie) {
if (status === 'realizado') return '✓ '
if (status === 'faltou') return '✗ '
if (status === 'cancelado') return '∅ '
if (status === 'remarcar') return '↺ '
if (status === 'bloqueado') return '⊘ '
if (hasSerie || isOccurrence) return '↻ '
return ''
function _statusIcon(status, isOccurrence, hasSerie) {
if (status === 'realizado') return '✓ ';
if (status === 'faltou') return '✗ ';
if (status === 'cancelado') return '∅ ';
if (status === 'remarcar') return '↺ ';
if (status === 'bloqueado') return '⊘ ';
if (hasSerie || isOccurrence) return '↻ ';
return '';
}
// ─────────────────────────────────────────────────────────────────────────────
// helpers internos — originais preservados
// ─────────────────────────────────────────────────────────────────────────────
function normalizeId (v) {
if (v === null || v === undefined) return null
const s = String(v).trim()
return s ? s : null
function normalizeId(v) {
if (v === null || v === undefined) return null;
const s = String(v).trim();
return s ? s : null;
}
function normalizeWeekday (value) {
if (value === null || value === undefined) return null
const n = Number(value)
if (Number.isNaN(n)) return null
if (n >= 0 && n <= 6) return n
if (n >= 1 && n <= 7) return n === 7 ? 0 : n
return null
function normalizeWeekday(value) {
if (value === null || value === undefined) return null;
const n = Number(value);
if (Number.isNaN(n)) return null;
if (n >= 0 && n <= 6) return n;
if (n >= 1 && n <= 7) return n === 7 ? 0 : n;
return null;
}
function asTime (v) {
if (!v || typeof v !== 'string') return null
const s = v.trim()
if (/^\d{2}:\d{2}$/.test(s)) return `${s}:00`
if (/^\d{2}:\d{2}:\d{2}$/.test(s)) return s
return null
function asTime(v) {
if (!v || typeof v !== 'string') return null;
const s = v.trim();
if (/^\d{2}:\d{2}$/.test(s)) return `${s}:00`;
if (/^\d{2}:\d{2}:\d{2}$/.test(s)) return s;
return null;
}
function startOfDay (d) {
const x = new Date(d)
x.setHours(0, 0, 0, 0)
return x
function startOfDay(d) {
const x = new Date(d);
x.setHours(0, 0, 0, 0);
return x;
}
function combineDateTimeISO (date, timeHHMMSS) {
const yyyy = date.getFullYear()
const mm = String(date.getMonth() + 1).padStart(2, '0')
const dd = String(date.getDate()).padStart(2, '0')
return `${yyyy}-${mm}-${dd}T${timeHHMMSS}`
}
function combineDateTimeISO(date, timeHHMMSS) {
const yyyy = date.getFullYear();
const mm = String(date.getMonth() + 1).padStart(2, '0');
const dd = String(date.getDate()).padStart(2, '0');
return `${yyyy}-${mm}-${dd}T${timeHHMMSS}`;
}
+108 -141
View File
@@ -14,125 +14,110 @@
| © 2026 — Todos os direitos reservados
|--------------------------------------------------------------------------
*/
import { supabase } from '@/lib/supabase/client'
import { useTenantStore } from '@/stores/tenantStore'
import { supabase } from '@/lib/supabase/client';
import { useTenantStore } from '@/stores/tenantStore';
function assertValidTenantId (tenantId) {
if (!tenantId || tenantId === 'null' || tenantId === 'undefined') {
throw new Error('Tenant ativo inválido. Selecione a clínica/tenant antes de carregar a agenda.')
}
function assertValidTenantId(tenantId) {
if (!tenantId || tenantId === 'null' || tenantId === 'undefined') {
throw new Error('Tenant ativo inválido. Selecione a clínica/tenant antes de carregar a agenda.');
}
}
async function getUid () {
const { data: userRes, error: userErr } = await supabase.auth.getUser()
if (userErr) throw userErr
async function getUid() {
const { data: userRes, error: userErr } = await supabase.auth.getUser();
if (userErr) throw userErr;
const uid = userRes?.user?.id
if (!uid) throw new Error('Usuário não autenticado.')
return uid
const uid = userRes?.user?.id;
if (!uid) throw new Error('Usuário não autenticado.');
return uid;
}
/**
* Configurações da agenda (por owner)
* Se você decidir que configurações são por tenant também, adicionamos tenant_id aqui.
*/
export async function getMyAgendaSettings () {
const uid = await getUid()
export async function getMyAgendaSettings() {
const uid = await getUid();
const { data, error } = await supabase
.from('agenda_configuracoes')
.select('*')
.eq('owner_id', uid)
.order('created_at', { ascending: false })
.limit(1)
.maybeSingle()
const { data, error } = await supabase.from('agenda_configuracoes').select('*').eq('owner_id', uid).order('created_at', { ascending: false }).limit(1).maybeSingle();
if (error) throw error
return data
if (error) throw error;
return data;
}
/**
* Regras semanais de jornada (agenda_regras_semanais):
* retorna os dias ativos com hora_inicio/hora_fim por dia.
*/
export async function getMyWorkSchedule () {
const uid = await getUid()
export async function getMyWorkSchedule() {
const uid = await getUid();
const { data, error } = await supabase
.from('agenda_regras_semanais')
.select('dia_semana, hora_inicio, hora_fim, ativo')
.eq('owner_id', uid)
.eq('ativo', true)
.order('dia_semana')
const { data, error } = await supabase.from('agenda_regras_semanais').select('dia_semana, hora_inicio, hora_fim, ativo').eq('owner_id', uid).eq('ativo', true).order('dia_semana');
if (error) throw error
return data || []
if (error) throw error;
return data || [];
}
/**
* Lista agenda do terapeuta (somente do owner logado) dentro do tenant ativo.
* Isso impede misturar eventos caso o terapeuta atue em múltiplas clínicas.
*/
export async function listMyAgendaEvents ({ startISO, endISO, tenantId: tenantIdArg } = {}) {
const uid = await getUid()
export async function listMyAgendaEvents({ startISO, endISO, tenantId: tenantIdArg } = {}) {
const uid = await getUid();
const tenantStore = useTenantStore()
const tenantId = tenantIdArg || tenantStore.activeTenantId
assertValidTenantId(tenantId)
const tenantStore = useTenantStore();
const tenantId = tenantIdArg || tenantStore.activeTenantId;
assertValidTenantId(tenantId);
if (!startISO || !endISO) throw new Error('Intervalo inválido (startISO/endISO).')
if (!startISO || !endISO) throw new Error('Intervalo inválido (startISO/endISO).');
const { data, error } = await supabase
.from('agenda_eventos')
.select('*, patients(id, nome_completo, avatar_url), determined_commitments!determined_commitment_id(id, bg_color, text_color)')
.eq('tenant_id', tenantId)
.eq('owner_id', uid)
.gte('inicio_em', startISO)
.lt('inicio_em', endISO)
.order('inicio_em', { ascending: true })
const { data, error } = await supabase
.from('agenda_eventos')
.select('*, patients(id, nome_completo, avatar_url), determined_commitments!determined_commitment_id(id, bg_color, text_color)')
.eq('tenant_id', tenantId)
.eq('owner_id', uid)
.gte('inicio_em', startISO)
.lt('inicio_em', endISO)
.order('inicio_em', { ascending: true });
if (error) throw error
return data || []
if (error) throw error;
return data || [];
}
/**
* Lista eventos para mosaico da clínica (admin/secretaria) dentro de um tenant específico.
* IMPORTANTE: SEM tenant_id aqui vira vazamento multi-tenant.
*/
export async function listClinicEvents ({ tenantId, ownerIds, startISO, endISO }) {
assertValidTenantId(tenantId)
if (!ownerIds?.length) return []
if (!startISO || !endISO) throw new Error('Intervalo inválido (startISO/endISO).')
export async function listClinicEvents({ tenantId, ownerIds, startISO, endISO }) {
assertValidTenantId(tenantId);
if (!ownerIds?.length) return [];
if (!startISO || !endISO) throw new Error('Intervalo inválido (startISO/endISO).');
// Sanitiza ownerIds
const safeOwnerIds = ownerIds
.filter(id => typeof id === 'string' && id && id !== 'null' && id !== 'undefined')
// Sanitiza ownerIds
const safeOwnerIds = ownerIds.filter((id) => typeof id === 'string' && id && id !== 'null' && id !== 'undefined');
if (!safeOwnerIds.length) return []
if (!safeOwnerIds.length) return [];
const { data, error } = await supabase
.from('agenda_eventos')
.select('*, determined_commitments!determined_commitment_id(id, bg_color, text_color)')
.eq('tenant_id', tenantId)
.in('owner_id', safeOwnerIds)
.gte('inicio_em', startISO)
.lt('inicio_em', endISO)
.order('inicio_em', { ascending: true })
const { data, error } = await supabase
.from('agenda_eventos')
.select('*, determined_commitments!determined_commitment_id(id, bg_color, text_color)')
.eq('tenant_id', tenantId)
.in('owner_id', safeOwnerIds)
.gte('inicio_em', startISO)
.lt('inicio_em', endISO)
.order('inicio_em', { ascending: true });
if (error) throw error
return data || []
if (error) throw error;
return data || [];
}
export async function listTenantStaff (tenantId) {
assertValidTenantId(tenantId)
export async function listTenantStaff(tenantId) {
assertValidTenantId(tenantId);
const { data, error } = await supabase
.from('v_tenant_staff')
.select('*')
.eq('tenant_id', tenantId)
const { data, error } = await supabase.from('v_tenant_staff').select('*').eq('tenant_id', tenantId);
if (error) throw error
return data || []
if (error) throw error;
return data || [];
}
/**
@@ -145,28 +130,24 @@ export async function listTenantStaff (tenantId) {
* (ex.: createClinicAgendaEvento) que permita owner_id explicitamente.
* Por enquanto, deixo esta função como "safe default" para terapeuta.
*/
export async function createAgendaEvento (payload) {
const uid = await getUid()
const tenantStore = useTenantStore()
const tenantId = tenantStore.activeTenantId
assertValidTenantId(tenantId)
export async function createAgendaEvento(payload) {
const uid = await getUid();
const tenantStore = useTenantStore();
const tenantId = tenantStore.activeTenantId;
assertValidTenantId(tenantId);
if (!payload) throw new Error('Payload vazio.')
if (!payload) throw new Error('Payload vazio.');
const insertPayload = {
...payload,
tenant_id: tenantId,
owner_id: uid
}
const insertPayload = {
...payload,
tenant_id: tenantId,
owner_id: uid
};
const { data, error } = await supabase
.from('agenda_eventos')
.insert(insertPayload)
.select('*')
.single()
const { data, error } = await supabase.from('agenda_eventos').insert(insertPayload).select('*').single();
if (error) throw error
return data
if (error) throw error;
return data;
}
/**
@@ -174,45 +155,35 @@ export async function createAgendaEvento (payload) {
* - filtra por id + tenant_id (evita update cruzado por acidente)
* RLS deve reforçar isso no banco.
*/
export async function updateAgendaEvento (id, patch, { tenantId: tenantIdArg } = {}) {
if (!id) throw new Error('ID inválido.')
if (!patch) throw new Error('Patch vazio.')
export async function updateAgendaEvento(id, patch, { tenantId: tenantIdArg } = {}) {
if (!id) throw new Error('ID inválido.');
if (!patch) throw new Error('Patch vazio.');
const tenantStore = useTenantStore()
const tenantId = tenantIdArg || tenantStore.activeTenantId
assertValidTenantId(tenantId)
const tenantStore = useTenantStore();
const tenantId = tenantIdArg || tenantStore.activeTenantId;
assertValidTenantId(tenantId);
const { data, error } = await supabase
.from('agenda_eventos')
.update(patch)
.eq('id', id)
.eq('tenant_id', tenantId)
.select('*')
.single()
const { data, error } = await supabase.from('agenda_eventos').update(patch).eq('id', id).eq('tenant_id', tenantId).select('*').single();
if (error) throw error
return data
if (error) throw error;
return data;
}
/**
* Delete seguro:
* - filtra por id + tenant_id
*/
export async function deleteAgendaEvento (id, { tenantId: tenantIdArg } = {}) {
if (!id) throw new Error('ID inválido.')
export async function deleteAgendaEvento(id, { tenantId: tenantIdArg } = {}) {
if (!id) throw new Error('ID inválido.');
const tenantStore = useTenantStore()
const tenantId = tenantIdArg || tenantStore.activeTenantId
assertValidTenantId(tenantId)
const tenantStore = useTenantStore();
const tenantId = tenantIdArg || tenantStore.activeTenantId;
assertValidTenantId(tenantId);
const { error } = await supabase
.from('agenda_eventos')
.delete()
.eq('id', id)
.eq('tenant_id', tenantId)
const { error } = await supabase.from('agenda_eventos').delete().eq('id', id).eq('tenant_id', tenantId);
if (error) throw error
return true
if (error) throw error;
return true;
}
// Adicione no mesmo arquivo: src/features/agenda/services/agendaRepository.js
@@ -226,29 +197,25 @@ export async function deleteAgendaEvento (id, { tenantId: tenantIdArg } = {}) {
* - clinic_admin/tenant_admin pode criar para qualquer owner dentro do tenant
* - therapist não deve conseguir passar daqui (guard + RLS)
*/
export async function createClinicAgendaEvento (payload, { tenantId: tenantIdArg } = {}) {
const tenantStore = useTenantStore()
const tenantId = tenantIdArg || tenantStore.activeTenantId
assertValidTenantId(tenantId)
export async function createClinicAgendaEvento(payload, { tenantId: tenantIdArg } = {}) {
const tenantStore = useTenantStore();
const tenantId = tenantIdArg || tenantStore.activeTenantId;
assertValidTenantId(tenantId);
if (!payload) throw new Error('Payload vazio.')
if (!payload) throw new Error('Payload vazio.');
const ownerId = payload.owner_id
if (!ownerId || ownerId === 'null' || ownerId === 'undefined') {
throw new Error('owner_id é obrigatório para criação pela clínica.')
}
const ownerId = payload.owner_id;
if (!ownerId || ownerId === 'null' || ownerId === 'undefined') {
throw new Error('owner_id é obrigatório para criação pela clínica.');
}
const insertPayload = {
...payload,
tenant_id: tenantId
}
const insertPayload = {
...payload,
tenant_id: tenantId
};
const { data, error } = await supabase
.from('agenda_eventos')
.insert(insertPayload)
.select('*')
.single()
const { data, error } = await supabase.from('agenda_eventos').insert(insertPayload).select('*').single();
if (error) throw error
return data
}
if (error) throw error;
return data;
}