diff --git a/src/layout/melissa/MelissaAgenda.vue b/src/layout/melissa/MelissaAgenda.vue index 04a2b14..31ce1b1 100644 --- a/src/layout/melissa/MelissaAgenda.vue +++ b/src/layout/melissa/MelissaAgenda.vue @@ -115,6 +115,16 @@ const fcRef = ref(null); // Title visível na toolbar (atualizado pelo FC via datesSet) const currentTitle = ref(''); +// Botão "Hoje" fica desabilitado quando o range visível JÁ inclui hoje +// (evita click ruidoso e dá affordance de "está em hoje"). +const refDateIsToday = computed(() => { + const hoje = new Date(); + const inicio = viewStart.value; + const fim = viewEnd.value; + if (!inicio || !fim) return false; + return hoje >= inicio && hoje < fim; +}); + // Range visível atual (passa pro composable que refetcha) const viewStart = ref(new Date()); const viewEnd = ref(new Date()); @@ -149,22 +159,47 @@ function dateKey(date) { return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; } +// ── Filtro por stat ──────────────────────────────────────────── +// Click num stat do "Hoje" filtra fcEvents + sessoesHoje pelo predicado +// correspondente. Click no stat ativo limpa o filtro. Stats com value=0 +// ficam disabled. Feriados continuam sempre (background events). +const STAT_FILTERS = { + total: () => true, + sessoes: (e) => e.tipo === 'sessao', + realizadas: (e) => /realizad/i.test(e.status), + faltas: (e) => /falt/i.test(e.status) +}; +const STAT_LABELS = { + total: 'Total', sessoes: 'Sessões', realizadas: 'Realizadas', faltas: 'Faltas' +}; +const statFiltroAtivo = ref(null); // null | 'total' | 'sessoes' | 'realizadas' | 'faltas' + +function toggleStatFiltro(key) { + statFiltroAtivo.value = statFiltroAtivo.value === key ? null : key; +} +function statLabel(key) { + return STAT_LABELS[key] || ''; +} + // FC events derivados dos eventos do composable + feriados como background. // feriadoFcEvents vem do useFeriados (display:'background', cor amber suave). // Pattern espelha AgendaTerapeutaPage:601. -const fcEvents = computed(() => [ - ...eventosSemana.value.map((ev) => ({ - id: ev.id, - title: ev.label, - start: ev.inicio_em, - end: ev.fim_em, - backgroundColor: `${ev.color}26`, // ~15% opacity - borderColor: ev.color, - textColor: 'white', - extendedProps: ev - })), - ...feriadoFcEvents.value -]); +const fcEvents = computed(() => { + const pred = statFiltroAtivo.value ? STAT_FILTERS[statFiltroAtivo.value] : () => true; + return [ + ...eventosSemana.value.filter(pred).map((ev) => ({ + id: ev.id, + title: ev.label, + start: ev.inicio_em, + end: ev.fim_em, + backgroundColor: `${ev.color}26`, // ~15% opacity + borderColor: ev.color, + textColor: 'white', + extendedProps: ev + })), + ...feriadoFcEvents.value + ]; +}); function escHtml(s) { return String(s ?? '') @@ -476,23 +511,23 @@ const eventosHojeReal = computed(() => { }); // ── Stats do dia (real) ──────────────────────────────────────── +// Cada stat tem `key` que casa com STAT_FILTERS pra interatividade. const statsHoje = computed(() => { const arr = eventosHojeReal.value; - const total = arr.length; - const sessoes = arr.filter((e) => e.tipo === 'sessao').length; - const realizadas = arr.filter((e) => /realizad/i.test(e.status)).length; - const faltas = arr.filter((e) => /falt/i.test(e.status)).length; return [ - { label: 'Total', value: total, cls: '' }, - { label: 'Sessões', value: sessoes, cls: 'ok' }, - { label: 'Realizadas', value: realizadas, cls: '' }, - { label: 'Faltas', value: faltas, cls: 'warn' } + { key: 'total', label: 'Total', value: arr.length, cls: '' }, + { key: 'sessoes', label: 'Sessões', value: arr.filter(STAT_FILTERS.sessoes).length, cls: 'ok' }, + { key: 'realizadas', label: 'Realizadas', value: arr.filter(STAT_FILTERS.realizadas).length, cls: '' }, + { key: 'faltas', label: 'Faltas', value: arr.filter(STAT_FILTERS.faltas).length, cls: 'warn' } ]; }); // ── Sessões hoje (lista, ordenadas por hora) ────────────────── +// Aplica o mesmo filtro dos stats pra coerência visual: clicar no stat +// "Faltas" mostra só faltas tanto no FC quanto na lista logo abaixo. const sessoesHoje = computed(() => { - return [...eventosHojeReal.value].sort((a, b) => a.startH - b.startH); + const pred = statFiltroAtivo.value ? STAT_FILTERS[statFiltroAtivo.value] : () => true; + return [...eventosHojeReal.value].filter(pred).sort((a, b) => a.startH - b.startH); }); // ── Cadastro de paciente (popover + dialogs) ────────────────── @@ -621,15 +656,18 @@ const kebabItems = computed(() => { {{ sessoesHoje.length }}
-
{{ s.value }}
{{ s.label }}
-
+
@@ -724,17 +762,28 @@ const kebabItems = computed(() => {
- +
- - -
{{ currentTitle }}
- +
+ + + + +
+
{{ currentTitle }}
+
@@ -1160,67 +1222,114 @@ const kebabItems = computed(() => { padding: 12px 14px; border-bottom: 1px solid var(--m-border); flex-shrink: 0; - gap: 10px; + gap: 14px; + min-width: 0; } .ma-cal__nav { display: flex; align-items: center; - gap: 6px; + gap: 10px; + min-width: 0; + flex: 1; +} +/* Cluster Hoje + chevrons num pill único pra ler como uma unidade */ +.ma-cal__nav-cluster { + display: flex; + align-items: center; + gap: 2px; + background: var(--m-bg-soft); + border: 1px solid var(--m-border); + border-radius: 10px; + padding: 2px; + flex-shrink: 0; +} +.ma-cal__nav-divider { + width: 1px; + height: 18px; + background: var(--m-border); + margin: 0 2px; + flex-shrink: 0; } .ma-cal__btn { - background: var(--m-bg-soft); - border: 1px solid var(--m-border); + background: transparent; + border: none; color: var(--m-text); - padding: 6px 12px; + padding: 5px 12px; border-radius: 8px; font-size: 0.78rem; + font-weight: 500; font-family: inherit; cursor: pointer; - transition: background-color 140ms ease; + transition: background-color 140ms ease, opacity 140ms ease; } .ma-cal__btn:hover { background: var(--m-bg-soft-hover); } +.ma-cal__btn:focus-visible { + outline: 2px solid var(--m-accent); + outline-offset: 2px; +} +.ma-cal__btn.is-today-active { + opacity: 0.45; + cursor: default; +} +.ma-cal__btn.is-today-active:hover { background: transparent; } .ma-cal__icon { - width: 30px; height: 30px; + width: 28px; height: 28px; display: grid; place-items: center; - background: var(--m-bg-soft); - border: 1px solid var(--m-border); + background: transparent; + border: none; color: var(--m-text); - border-radius: 8px; + border-radius: 7px; cursor: pointer; - font-size: 0.7rem; + font-size: 0.78rem; transition: background-color 140ms ease; } .ma-cal__icon:hover { background: var(--m-bg-soft-hover); } +.ma-cal__icon:focus-visible { + outline: 2px solid var(--m-accent); + outline-offset: 2px; +} .ma-cal__period { - padding: 4px 14px; + flex: 1; + min-width: 0; + padding: 4px 4px; color: var(--m-text); - font-size: 0.85rem; - font-weight: 500; - min-width: 130px; - text-align: center; + font-size: 0.92rem; + font-weight: 600; + letter-spacing: 0.01em; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + text-transform: capitalize; } .ma-cal__view { display: flex; background: var(--m-bg-soft); border: 1px solid var(--m-border); - border-radius: 8px; + border-radius: 10px; padding: 2px; + flex-shrink: 0; } .ma-cal__view-btn { background: transparent; border: none; color: var(--m-text-muted); - padding: 4px 12px; - border-radius: 6px; + padding: 5px 12px; + border-radius: 8px; font-size: 0.75rem; + font-weight: 500; font-family: inherit; cursor: pointer; transition: all 140ms ease; } .ma-cal__view-btn:hover { color: var(--m-text); } +.ma-cal__view-btn:focus-visible { + outline: 2px solid var(--m-accent); + outline-offset: 2px; +} .ma-cal__view-btn.is-active { background: var(--m-bg-soft-hover); color: var(--m-text); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08); } /* ─── FullCalendar wrapper ─────────────────────────────────── */ @@ -1228,6 +1337,7 @@ const kebabItems = computed(() => { flex: 1; min-height: 0; overflow: hidden; + position: relative; /* ancora .ma-cal__filter-chip */ /* Vars CSS do próprio FC sobrescritas pra glass aesthetic adaptativo */ --fc-border-color: var(--m-border); --fc-page-bg-color: transparent; @@ -1240,6 +1350,51 @@ const kebabItems = computed(() => { font-family: inherit; } +/* Chip flutuante "Filtrando: X" — flutua sobre o canto superior direito + do FC quando há filtro de stat ativo. Click limpa. */ +.ma-cal__filter-chip { + position: absolute; + top: 8px; + right: 8px; + z-index: 5; + display: inline-flex; + align-items: center; + gap: 8px; + padding: 6px 12px; + background: color-mix(in srgb, var(--m-accent) 22%, var(--m-bg-medium)); + border: 1px solid var(--m-accent); + border-radius: 999px; + color: var(--m-text); + font-size: 0.72rem; + font-weight: 600; + font-family: inherit; + letter-spacing: 0.02em; + cursor: pointer; + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + transition: background-color 140ms ease, transform 140ms ease; +} +.ma-cal__filter-chip:hover { + background: color-mix(in srgb, var(--m-accent) 32%, var(--m-bg-medium)); + transform: translateY(-1px); +} +.ma-cal__filter-chip:focus-visible { + outline: 2px solid var(--m-accent); + outline-offset: 2px; +} + +/* Transition do chip — pop sutil de cima */ +.ma-filter-chip-enter-active, +.ma-filter-chip-leave-active { + transition: opacity 180ms ease, transform 180ms cubic-bezier(0.34, 1.56, 0.64, 1); +} +.ma-filter-chip-enter-from, +.ma-filter-chip-leave-to { + opacity: 0; + transform: translateY(-6px) scale(0.92); +} + /* ─── Override do FullCalendar pra glass (via :deep) ───────── */ .ma-cal__fc :deep(.fc) { height: 100%; @@ -1651,6 +1806,26 @@ const kebabItems = computed(() => { border-radius: 8px; padding: 8px 4px; text-align: center; + cursor: pointer; + font-family: inherit; + transition: background-color 140ms ease, border-color 140ms ease, transform 140ms ease, opacity 140ms ease; +} +.ma-stat:hover:not(:disabled) { + background: var(--m-bg-soft-hover); + transform: translateY(-1px); +} +.ma-stat:focus-visible { + outline: 2px solid var(--m-accent); + outline-offset: 2px; +} +.ma-stat:disabled { + opacity: 0.4; + cursor: not-allowed; +} +.ma-stat.is-active { + border-color: var(--m-accent); + background: color-mix(in srgb, var(--m-accent) 16%, var(--m-bg-soft)); + box-shadow: 0 0 0 1px var(--m-accent); } .ma-stat__val { font-size: 1.1rem;