From 8a8d2e05bdd1630e16bdfb31f6acf55795b182bc Mon Sep 17 00:00:00 2001 From: Leonardo Date: Fri, 8 May 2026 09:53:59 -0300 Subject: [PATCH] MelissaPaciente Fase 5: Tab Agenda completa (KPIs + filtros + grupos por mes + acoes) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit EXTENSAO src/features/patients/utils/patientFormatters.js: +2 helpers - fmtHourShort (HH:MM 24h pt-br) — usado na coluna data dos cards - fmtDayShort (DOW abreviado pt-br sem ponto) — usado na coluna data EXTENSAO src/features/patients/composables/usePatientSessions.js - Novo ref `busy` pra disable de buttons durante mutation - _lastPatientId guardado internamente pra auto-reload - Nova funcao `updateStatus(sessionId, novoStatus)` que faz supabase.from('agenda_eventos').update({status}) + auto-reload da lista de sessoes. Retorna {ok, error?}. MELISSAPACIENTE.VUE — script - agendaFilter ref ('all' default) + AGENDA_FILTERS array com 6 opcoes (Todas, Proximas, Passadas, Realizadas, Faltas, Canceladas) - agendaSessoesFiltradas computed: filtra por future/past/status (regex) - agendaAgrupadas computed: agrupa por "Mes de YYYY" DESC - updateSessionStatus(ev, status, msg): chama sessionsHook.updateStatus + toast de sucesso/erro - Removido `void toast` (toast usado de verdade agora) MELISSAPACIENTE.VUE — Tab Agenda reescrita (substitui placeholder Fase 1) - 4 KPI cards no padrao Visao Geral (numerados 01-04): Total / Realizadas (% do total) / Faltas (cor adaptativa) / Proxima - 6 filter chips redondas (cor primary quando active) - Empty state contextual (sem sessoes vs filtro vazio) - Grupos por mes com header (label + badge count) - Cards 3-col: data column (DOW + dia + hora) | main (status tag + chips modalidade/duracao + relative + titulo + note 2-line clamp) | actions (3 buttons: ok/warn/danger com tooltip + cor adaptativa no hover) - Mobile: stack date+main em 2 cols; actions full-width abaixo CSS: ~150L novos. Padrao visual Melissa: data column estilo calendario, actions hover muda cor por intent (verde realiz / amarelo falta / vermelho cancel), border-left por status. ESLint: 0 errors da minha mudanca. Co-Authored-By: Claude Opus 4.7 (1M context) --- Obsidian/Brain/log.md | 45 ++ .../composables/usePatientSessions.js | 28 ++ .../patients/utils/patientFormatters.js | 20 + src/layout/melissa/MelissaPaciente.vue | 422 +++++++++++++++++- 4 files changed, 499 insertions(+), 16 deletions(-) diff --git a/Obsidian/Brain/log.md b/Obsidian/Brain/log.md index 6e33ab9..685c7af 100644 --- a/Obsidian/Brain/log.md +++ b/Obsidian/Brain/log.md @@ -50,6 +50,51 @@ Touched: none ## [2026-05-08 00:00] session | Melissa cfg-* nativas + temas + cronometro DB Touched: none +## [2026-05-08 16:30] session | MelissaPaciente Fase 5 — Tab Agenda completa +Touched: none +Detalhes: Tab Agenda com KPIs, filtros, agrupamento por mes e acoes +rapidas (mark realizada/falta/cancelar). Espelha o legacy. + +EXTENSAO patientFormatters.js: +2 helpers +- fmtHourShort (HH:MM 24h pt-br) e fmtDayShort (DOW abbreviado pt-br + sem ponto) — usados na coluna data dos cards. + +EXTENSAO usePatientSessions.js: mutation + busy flag +- Novo ref `busy` pra disable de buttons durante mutation. +- _lastPatientId guardado pra auto-reload depois de mutation. +- Nova funcao `updateStatus(sessionId, novoStatus)` que faz + supabase.from('agenda_eventos').update({status}) + auto-reload da + lista. Retorna {ok, error?}. + +MELISSAPACIENTE.VUE — script +- agendaFilter ref ('all' default) + AGENDA_FILTERS array com 6 opcoes + (Todas, Proximas, Passadas, Realizadas, Faltas, Canceladas). +- agendaSessoesFiltradas computed: filtra sessoes por future/past/status. +- agendaAgrupadas computed: agrupa por "Mes de YYYY" mantendo ordem DESC. +- updateSessionStatus(ev, status, msg) handler que chama + sessionsHook.updateStatus + toast de sucesso/erro. +- Removido `void toast` (toast usado de verdade agora). + +MELISSAPACIENTE.VUE — Tab Agenda reescrita (substitui placeholder Fase 1) +- 4 KPI cards no padrao Visao Geral (numerados 01-04): + - 01 Total + cap "sessoes registradas" + - 02 Realizadas + cap "% do total" + - 03 Faltas + cap "+ N cancel." (cor vermelha quando > 0, cinza quando 0) + - 04 Proxima + relative + datetime +- 6 filter chips redondas (estilo Melissa: cor primary quando active). +- Empty state contextual (sem sessoes vs filtro vazio). +- Grupos por mes com header (label + badge count). +- Cards com 3 colunas: data column (DOW + dia + hora curta) | main + (status tag + chips modalidade/duracao + relative + titulo + note) | + actions (3 buttons: ok/warn/danger com tooltip + cor adaptativa hover). +- Mobile: stack date+main em 2 cols; actions full-width abaixo. + +CSS: ~150L novos pros componentes (mpa-ag__group/list/item/date/main/ +actions). Padrao visual Melissa: data column estilo calendario, actions +hover muda cor por intent (verde realiz / amarelo falta / vermelho cancel). + +ESLint: 0 errors da minha mudanca. + ## [2026-05-08 15:30] session | MelissaPaciente Fase 4 — Tab Prontuario MVP Touched: none Detalhes: O legacy PatientProntuario.vue tem a aba Prontuario como diff --git a/src/features/patients/composables/usePatientSessions.js b/src/features/patients/composables/usePatientSessions.js index 0d2c568..49e5846 100644 --- a/src/features/patients/composables/usePatientSessions.js +++ b/src/features/patients/composables/usePatientSessions.js @@ -16,8 +16,11 @@ export function usePatientSessions() { const sessions = ref([]); const loading = ref(false); const error = ref(''); + const busy = ref(false); // mutations em curso (updateStatus etc) + let _lastPatientId = null; async function load(patientId) { + _lastPatientId = patientId || null; if (!patientId) { sessions.value = []; return; @@ -81,11 +84,36 @@ export function usePatientSessions() { .slice(0, 6) ); + /** + * Atualiza o status de uma sessao (mutation). Recarrega a lista do paciente + * ao final pra refletir o novo estado nos computeds derivados. + * Retorna {ok: true} ou {ok: false, error: msg}. + */ + async function updateStatus(sessionId, novoStatus) { + if (!sessionId || busy.value) return { ok: false, error: 'busy' }; + busy.value = true; + try { + const { error: err } = await supabase + .from('agenda_eventos') + .update({ status: novoStatus }) + .eq('id', sessionId); + if (err) throw err; + if (_lastPatientId) await load(_lastPatientId); + return { ok: true }; + } catch (e) { + return { ok: false, error: e?.message || 'Erro ao atualizar status' }; + } finally { + busy.value = false; + } + } + return { sessions, loading, error, + busy, load, + updateStatus, proximaSessao, ultimaSessao, totalSessoes, diff --git a/src/features/patients/utils/patientFormatters.js b/src/features/patients/utils/patientFormatters.js index 2320756..de0776a 100644 --- a/src/features/patients/utils/patientFormatters.js +++ b/src/features/patients/utils/patientFormatters.js @@ -122,6 +122,26 @@ export function fmtDateBR(v) { return `${String(d.getDate()).padStart(2, '0')}/${String(d.getMonth() + 1).padStart(2, '0')}/${d.getFullYear()}`; } +/** + * Hora curta HH:MM (24h pt-br). + */ +export function fmtHourShort(iso) { + if (!iso) return ''; + const d = new Date(iso); + if (Number.isNaN(d.getTime())) return ''; + return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`; +} + +/** + * Dia da semana abreviado pt-br (seg/ter/qua...). + */ +export function fmtDayShort(iso) { + if (!iso) return ''; + const d = new Date(iso); + if (Number.isNaN(d.getTime())) return ''; + return d.toLocaleDateString('pt-BR', { weekday: 'short' }).replace('.', ''); +} + export function fmtDateTimeBR(iso) { if (!iso) return '—'; const d = new Date(iso); diff --git a/src/layout/melissa/MelissaPaciente.vue b/src/layout/melissa/MelissaPaciente.vue index 4e5c1c9..1448961 100644 --- a/src/layout/melissa/MelissaPaciente.vue +++ b/src/layout/melissa/MelissaPaciente.vue @@ -33,6 +33,8 @@ import { fmtDateBR, fmtDateTimeBR, fmtCurrency, + fmtHourShort, + fmtDayShort, fmtCPF, fmtRG, fmtGender, @@ -101,6 +103,17 @@ const PRON_FILTERS = [ { value: 'cancel', label: 'Cancelamentos', icon: 'pi pi-ban' } ]; +// Filtros da aba Agenda (lista cronologica + agrupamento por mes) +const agendaFilter = ref('all'); // all | future | past | realiz | falt | cancel +const AGENDA_FILTERS = [ + { value: 'all', label: 'Todas', icon: 'pi pi-list' }, + { value: 'future', label: 'Próximas', icon: 'pi pi-calendar-plus' }, + { value: 'past', label: 'Passadas', icon: 'pi pi-history' }, + { value: 'realiz', label: 'Realizadas', icon: 'pi pi-check-circle' }, + { value: 'falt', label: 'Faltas', icon: 'pi pi-user-minus' }, + { value: 'cancel', label: 'Canceladas', icon: 'pi pi-ban' } +]; + // Sub-nav da aba Perfil const PROFILE_SECTIONS = [ { key: 'pessoais', label: 'Informações Pessoais', icon: 'pi pi-pencil' }, @@ -195,6 +208,56 @@ const groupNames = computed(() => detail.groups.value.map((g) => g?.name).filter const groupLabel = computed(() => groupNames.value.length ? groupNames.value.join(', ') : '—'); const groupCountLabel = computed(() => groupNames.value.length <= 1 ? 'Grupo' : 'Grupos'); +// ── Tab Agenda: filtros + agrupamento por mes ────────────── +const agendaSessoesFiltradas = computed(() => { + const list = sessionsHook.sessions.value; + const now = Date.now(); + switch (agendaFilter.value) { + case 'future': return list.filter((s) => s.inicio_em && new Date(s.inicio_em).getTime() > now); + case 'past': return list.filter((s) => s.inicio_em && new Date(s.inicio_em).getTime() <= now); + case 'realiz': return list.filter((s) => /realiz|present/i.test(String(s.status || ''))); + case 'falt': return list.filter((s) => /falt/i.test(String(s.status || ''))) ; + case 'cancel': return list.filter((s) => /cancel|remarca/i.test(String(s.status || ''))); + default: return list; + } +}); +// Agrupa por "Mes de YYYY" mantendo ordem DESC (mais recente primeiro) +const agendaAgrupadas = computed(() => { + const groups = []; + let key = null; + for (const ev of agendaSessoesFiltradas.value) { + if (!ev.inicio_em) continue; + const d = new Date(ev.inicio_em); + const k = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`; + if (k !== key) { + key = k; + groups.push({ + key: k, + label: d.toLocaleDateString('pt-BR', { month: 'long', year: 'numeric' }) + .replace(/(^|\s)\S/g, (l) => l.toUpperCase()), + items: [] + }); + } + groups[groups.length - 1].items.push(ev); + } + return groups; +}); + +// Handler de mutacao de status (Realizada / Falta / Cancelar) +async function updateSessionStatus(ev, novoStatus, msg) { + const result = await sessionsHook.updateStatus(ev.id, novoStatus); + if (result.ok) { + toast.add({ severity: 'success', summary: msg, life: 2200 }); + } else { + toast.add({ + severity: 'error', + summary: 'Falha ao atualizar', + detail: result.error || 'Erro inesperado', + life: 4000 + }); + } +} + // ── Tab Prontuario: lista filtrada de sessoes ────────────── // MVP enquanto anamnese/evolucao_clinica nao existem no schema: // usa agenda_eventos.observacoes como nota evolutiva. @@ -267,8 +330,6 @@ onBeforeUnmount(() => { } }); -// Suprime "unused" do toast (vai ser usado nas Fases 2+) -void toast;