Melissa: hub Configuracoes + Embed + 9 Pages novas + dialog blueprint dark
Sprints 04-29 + 04-30 acumuladas. - MelissaConfiguracoes: hub 2-col com 6 grupos (Layout/Conta/Agenda/ Financeiro/WhatsApp/Sistema), tudo embedado via MelissaEmbed. - MelissaEmbed: wrapper generico que injeta layout-variant=melissa e remove cromos pra reaproveitar Pages tradicionais. - 9 Melissa Pages novas: CadastrosRecebidos, Compromissos, Configuracoes, Conversas, Embed, Grupos, Medicos, Recorrencias, Tags. - Dialog blueprint atualizado: bg-gray-100 (hardcoded light) -> bg-[var(--surface-ground)] (tema-aware). 22 dialogs migrados em 9 arquivos. Anti-pattern documentado. - PatientsCadastroPage: bug fix dropdown Grupo (optionLabel nome->name), toggle vertical/abas com persist localStorage, sticky margin-top. - Surface picker no popover do MelissaLayout (8 swatches). - useTopbarPlanMenu, useMelissaWhatsapp, useMelissaPacientesAside novos. - Migration: status agenda remarcado/confirmado. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -63,7 +63,9 @@ function normalizeEvent(r) {
|
||||
fim_em: r.fim_em,
|
||||
startH: isoToDecimalHour(r.inicio_em),
|
||||
endH: isoToDecimalHour(r.fim_em),
|
||||
dateKey: String(r.inicio_em || '').slice(0, 10)
|
||||
dateKey: String(r.inicio_em || '').slice(0, 10),
|
||||
price: r.price != null ? Number(r.price) : 0,
|
||||
billed: !!r.billed
|
||||
};
|
||||
}
|
||||
|
||||
@@ -81,7 +83,7 @@ async function _fetchRange(start, end) {
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('agenda_eventos')
|
||||
.select('id, tipo, status, titulo, inicio_em, fim_em, modalidade, observacoes, patient_id, patients!agenda_eventos_patient_id_fkey(nome_completo, status)')
|
||||
.select('id, tipo, status, titulo, inicio_em, fim_em, modalidade, observacoes, patient_id, price, billed, patients!agenda_eventos_patient_id_fkey(nome_completo, status)')
|
||||
.eq('owner_id', userId)
|
||||
.is('mirror_of_event_id', null)
|
||||
.gte('inicio_em', start.toISOString())
|
||||
@@ -185,6 +187,137 @@ export function useMelissaEventosRange(startRef, endRef) {
|
||||
return { eventos, loading, error, refetch: fetch };
|
||||
}
|
||||
|
||||
// ── Busca server-side: por nome de paciente ou título do evento ──
|
||||
// Usado pela busca da toolbar do MelissaAgenda. Procura sem range temporal
|
||||
// (acha sessões fora do que está visível no FC). Limite de 20 resultados,
|
||||
// ordenados por inicio_em DESC (mais recente primeiro).
|
||||
//
|
||||
// Acento-insensitive: troca cada vogal/c do termo por um character class
|
||||
// que casa com todas as variantes ("andre" vira "[aáàâã]ndr[eéèê]") e usa
|
||||
// `imatch` (operador POSIX `~*` do Postgres — case-insensitive regex).
|
||||
// Estratégia evita depender da extensão `unaccent` no DB.
|
||||
function _buildAccentInsensitivePattern(term) {
|
||||
// Escapa regex specials primeiro pra não quebrar o pattern.
|
||||
const escaped = String(term || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const map = {
|
||||
a: '[aáàâãAÁÀÂÃ]', e: '[eéèêEÉÈÊ]', i: '[iíìîIÍÌÎ]',
|
||||
o: '[oóòôõOÓÒÔÕ]', u: '[uúùûUÚÙÛ]', c: '[cçCÇ]',
|
||||
A: '[aáàâãAÁÀÂÃ]', E: '[eéèêEÉÈÊ]', I: '[iíìîIÍÌÎ]',
|
||||
O: '[oóòôõOÓÒÔÕ]', U: '[uúùûUÚÙÛ]', C: '[cçCÇ]'
|
||||
};
|
||||
return escaped.split('').map((ch) => map[ch] || ch).join('');
|
||||
}
|
||||
|
||||
export async function searchEventosByText(termo) {
|
||||
const term = String(termo || '').trim();
|
||||
if (term.length < 2) return [];
|
||||
|
||||
const tenantStore = useTenantStore();
|
||||
const { data: userData } = await supabase.auth.getUser();
|
||||
const userId = userData?.user?.id || null;
|
||||
if (typeof tenantStore.ensureLoaded === 'function') {
|
||||
await tenantStore.ensureLoaded();
|
||||
}
|
||||
const tid = tenantStore.activeTenantId || tenantStore.tenantId || null;
|
||||
if (!userId || !tid) return [];
|
||||
|
||||
const SELECT = 'id, tipo, status, titulo, inicio_em, fim_em, modalidade, observacoes, patient_id, price, billed, patients!agenda_eventos_patient_id_fkey(nome_completo, status)';
|
||||
const pattern = _buildAccentInsensitivePattern(term);
|
||||
|
||||
try {
|
||||
const [byPatient, byTitle] = await Promise.all([
|
||||
supabase
|
||||
.from('agenda_eventos')
|
||||
.select(SELECT.replace('patients!agenda_eventos_patient_id_fkey', 'patients!inner!agenda_eventos_patient_id_fkey'))
|
||||
.eq('owner_id', userId)
|
||||
.is('mirror_of_event_id', null)
|
||||
.filter('patients.nome_completo', 'imatch', pattern)
|
||||
.order('inicio_em', { ascending: false })
|
||||
.limit(20),
|
||||
supabase
|
||||
.from('agenda_eventos')
|
||||
.select(SELECT)
|
||||
.eq('owner_id', userId)
|
||||
.is('mirror_of_event_id', null)
|
||||
.filter('titulo', 'imatch', pattern)
|
||||
.order('inicio_em', { ascending: false })
|
||||
.limit(20)
|
||||
]);
|
||||
if (byPatient.error) throw byPatient.error;
|
||||
if (byTitle.error) throw byTitle.error;
|
||||
|
||||
const merged = [...(byPatient.data || []), ...(byTitle.data || [])];
|
||||
const seen = new Set();
|
||||
const unique = [];
|
||||
for (const r of merged) {
|
||||
if (seen.has(r.id)) continue;
|
||||
seen.add(r.id);
|
||||
unique.push(r);
|
||||
}
|
||||
unique.sort((a, b) => String(b.inicio_em).localeCompare(String(a.inicio_em)));
|
||||
return unique.slice(0, 20).map(normalizeEvent);
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('[searchEventosByText]', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// ── COMPOSABLE 4: todas as sessões de um paciente (sem range) ──
|
||||
// Usado pelo banner "Ver todas" da MelissaAgenda quando o usuário
|
||||
// quer escapar do range visível e ver o histórico completo do
|
||||
// paciente selecionado. Diferente do useMelissaEventosRange, aqui
|
||||
// filtramos por patient_id e ignoramos qualquer range temporal.
|
||||
//
|
||||
// Retorna eventos ordenados por inicio_em DESC (mais recente primeiro)
|
||||
// — coerente com listas de "histórico" no resto do sistema.
|
||||
export function useMelissaTodasSessoesPaciente() {
|
||||
const eventos = ref([]);
|
||||
const loading = ref(false);
|
||||
const error = ref(null);
|
||||
|
||||
async function fetch(patientId) {
|
||||
if (!patientId) { eventos.value = []; return; }
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const tenantStore = useTenantStore();
|
||||
const { data: userData } = await supabase.auth.getUser();
|
||||
const userId = userData?.user?.id || null;
|
||||
if (typeof tenantStore.ensureLoaded === 'function') {
|
||||
await tenantStore.ensureLoaded();
|
||||
}
|
||||
const tid = tenantStore.activeTenantId || tenantStore.tenantId || null;
|
||||
if (!userId || !tid) { eventos.value = []; return; }
|
||||
|
||||
const { data, error: err } = await supabase
|
||||
.from('agenda_eventos')
|
||||
.select('id, tipo, status, titulo, inicio_em, fim_em, modalidade, observacoes, patient_id, price, billed, patients!agenda_eventos_patient_id_fkey(nome_completo, status)')
|
||||
.eq('owner_id', userId)
|
||||
.eq('patient_id', patientId)
|
||||
.is('mirror_of_event_id', null)
|
||||
.order('inicio_em', { ascending: false });
|
||||
|
||||
if (err) throw err;
|
||||
eventos.value = (data || []).map(normalizeEvent);
|
||||
} catch (e) {
|
||||
error.value = e?.message || 'Erro ao carregar sessões';
|
||||
eventos.value = [];
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('[useMelissaTodasSessoesPaciente]', e);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
eventos.value = [];
|
||||
error.value = null;
|
||||
}
|
||||
|
||||
return { eventos, loading, error, fetch, reset };
|
||||
}
|
||||
|
||||
// ── COMPOSABLE 2: apenas hoje (MelissaLayout) ──────────────────
|
||||
export function useMelissaEventosHoje() {
|
||||
const eventos = ref([]);
|
||||
|
||||
Reference in New Issue
Block a user