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