MelissaPaciente Fase 5: Tab Agenda completa (KPIs + filtros + grupos por mes + acoes)
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) <noreply@anthropic.com>
This commit is contained in:
@@ -50,6 +50,51 @@ Touched: none
|
|||||||
## [2026-05-08 00:00] session | Melissa cfg-* nativas + temas + cronometro DB
|
## [2026-05-08 00:00] session | Melissa cfg-* nativas + temas + cronometro DB
|
||||||
Touched: none
|
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
|
## [2026-05-08 15:30] session | MelissaPaciente Fase 4 — Tab Prontuario MVP
|
||||||
Touched: none
|
Touched: none
|
||||||
Detalhes: O legacy PatientProntuario.vue tem a aba Prontuario como
|
Detalhes: O legacy PatientProntuario.vue tem a aba Prontuario como
|
||||||
|
|||||||
@@ -16,8 +16,11 @@ export function usePatientSessions() {
|
|||||||
const sessions = ref([]);
|
const sessions = ref([]);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const error = ref('');
|
const error = ref('');
|
||||||
|
const busy = ref(false); // mutations em curso (updateStatus etc)
|
||||||
|
let _lastPatientId = null;
|
||||||
|
|
||||||
async function load(patientId) {
|
async function load(patientId) {
|
||||||
|
_lastPatientId = patientId || null;
|
||||||
if (!patientId) {
|
if (!patientId) {
|
||||||
sessions.value = [];
|
sessions.value = [];
|
||||||
return;
|
return;
|
||||||
@@ -81,11 +84,36 @@ export function usePatientSessions() {
|
|||||||
.slice(0, 6)
|
.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 {
|
return {
|
||||||
sessions,
|
sessions,
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
|
busy,
|
||||||
load,
|
load,
|
||||||
|
updateStatus,
|
||||||
proximaSessao,
|
proximaSessao,
|
||||||
ultimaSessao,
|
ultimaSessao,
|
||||||
totalSessoes,
|
totalSessoes,
|
||||||
|
|||||||
@@ -122,6 +122,26 @@ export function fmtDateBR(v) {
|
|||||||
return `${String(d.getDate()).padStart(2, '0')}/${String(d.getMonth() + 1).padStart(2, '0')}/${d.getFullYear()}`;
|
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) {
|
export function fmtDateTimeBR(iso) {
|
||||||
if (!iso) return '—';
|
if (!iso) return '—';
|
||||||
const d = new Date(iso);
|
const d = new Date(iso);
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ import {
|
|||||||
fmtDateBR,
|
fmtDateBR,
|
||||||
fmtDateTimeBR,
|
fmtDateTimeBR,
|
||||||
fmtCurrency,
|
fmtCurrency,
|
||||||
|
fmtHourShort,
|
||||||
|
fmtDayShort,
|
||||||
fmtCPF,
|
fmtCPF,
|
||||||
fmtRG,
|
fmtRG,
|
||||||
fmtGender,
|
fmtGender,
|
||||||
@@ -101,6 +103,17 @@ const PRON_FILTERS = [
|
|||||||
{ value: 'cancel', label: 'Cancelamentos', icon: 'pi pi-ban' }
|
{ 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
|
// Sub-nav da aba Perfil
|
||||||
const PROFILE_SECTIONS = [
|
const PROFILE_SECTIONS = [
|
||||||
{ key: 'pessoais', label: 'Informações Pessoais', icon: 'pi pi-pencil' },
|
{ 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 groupLabel = computed(() => groupNames.value.length ? groupNames.value.join(', ') : '—');
|
||||||
const groupCountLabel = computed(() => groupNames.value.length <= 1 ? 'Grupo' : 'Grupos');
|
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 ──────────────
|
// ── Tab Prontuario: lista filtrada de sessoes ──────────────
|
||||||
// MVP enquanto anamnese/evolucao_clinica nao existem no schema:
|
// MVP enquanto anamnese/evolucao_clinica nao existem no schema:
|
||||||
// usa agenda_eventos.observacoes como nota evolutiva.
|
// usa agenda_eventos.observacoes como nota evolutiva.
|
||||||
@@ -267,8 +330,6 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Suprime "unused" do toast (vai ser usado nas Fases 2+)
|
|
||||||
void toast;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -1206,23 +1267,191 @@ void toast;
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ABA: Agenda -->
|
<!-- ABA: Agenda (Fase 5 — KPIs + filtros + grupos por mes + acoes) -->
|
||||||
<div v-else-if="activeTab === 'agenda'" class="mpa-tab">
|
<div v-else-if="activeTab === 'agenda'" class="mpa-tab">
|
||||||
<div class="mpa-w">
|
<!-- Loading -->
|
||||||
<div class="mpa-w__head">
|
<div v-if="sessionsHook.loading.value" class="mpa-empty">
|
||||||
<div class="mpa-w__icon mpa-w__icon--green"><i class="pi pi-calendar" /></div>
|
<i class="pi pi-spin pi-spinner mr-2" /> Carregando…
|
||||||
<div class="mpa-w__title">
|
</div>
|
||||||
<div class="mpa-w__title-text">Agenda — Fase 5</div>
|
|
||||||
<div class="mpa-w__sub">Sessões agendadas + ações rápidas</div>
|
<template v-else>
|
||||||
|
<!-- KPIs (4) -->
|
||||||
|
<div class="mpa-kpis">
|
||||||
|
<article class="mpa-kpi" style="--c:var(--p-primary-color)">
|
||||||
|
<span class="mpa-kpi__num">01</span>
|
||||||
|
<header class="mpa-kpi__head">
|
||||||
|
<div class="mpa-kpi__icon"><i class="pi pi-calendar" /></div>
|
||||||
|
<span class="mpa-kpi__tag">Total</span>
|
||||||
|
</header>
|
||||||
|
<div class="mpa-kpi__big">{{ sessionsHook.totalSessoes.value }}</div>
|
||||||
|
<div class="mpa-kpi__cap">sessões registradas</div>
|
||||||
|
</article>
|
||||||
|
<article class="mpa-kpi" style="--c:#4ade80">
|
||||||
|
<span class="mpa-kpi__num">02</span>
|
||||||
|
<header class="mpa-kpi__head">
|
||||||
|
<div class="mpa-kpi__icon"><i class="pi pi-check-circle" /></div>
|
||||||
|
<span class="mpa-kpi__tag">Realizadas</span>
|
||||||
|
</header>
|
||||||
|
<div class="mpa-kpi__big">{{ sessionsHook.totalRealizadas.value }}</div>
|
||||||
|
<div class="mpa-kpi__cap">
|
||||||
|
<template v-if="sessionsHook.totalSessoes.value">
|
||||||
|
{{ Math.round((sessionsHook.totalRealizadas.value / sessionsHook.totalSessoes.value) * 100) }}% do total
|
||||||
|
</template>
|
||||||
|
<template v-else>—</template>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
<article
|
||||||
|
class="mpa-kpi"
|
||||||
|
:style="sessionsHook.totalFaltas.value > 0 ? '--c:#f87171' : '--c:#94a3b8'"
|
||||||
|
>
|
||||||
|
<span class="mpa-kpi__num">03</span>
|
||||||
|
<header class="mpa-kpi__head">
|
||||||
|
<div class="mpa-kpi__icon"><i class="pi pi-user-minus" /></div>
|
||||||
|
<span class="mpa-kpi__tag">Faltas</span>
|
||||||
|
</header>
|
||||||
|
<div class="mpa-kpi__big">{{ sessionsHook.totalFaltas.value }}</div>
|
||||||
|
<div class="mpa-kpi__cap">
|
||||||
|
<template v-if="sessionsHook.totalCanceladas.value">
|
||||||
|
+ {{ sessionsHook.totalCanceladas.value }} cancel.
|
||||||
|
</template>
|
||||||
|
<template v-else>nenhuma falta</template>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
<article class="mpa-kpi" style="--c:#60a5fa">
|
||||||
|
<span class="mpa-kpi__num">04</span>
|
||||||
|
<header class="mpa-kpi__head">
|
||||||
|
<div class="mpa-kpi__icon"><i class="pi pi-calendar-clock" /></div>
|
||||||
|
<span class="mpa-kpi__tag">Próxima</span>
|
||||||
|
</header>
|
||||||
|
<template v-if="sessionsHook.proximaSessao.value">
|
||||||
|
<div class="mpa-kpi__big mpa-kpi__big--small">
|
||||||
|
{{ fmtRelative(sessionsHook.proximaSessao.value.inicio_em) }}
|
||||||
|
</div>
|
||||||
|
<div class="mpa-kpi__cap">
|
||||||
|
{{ fmtDateBR(sessionsHook.proximaSessao.value.inicio_em) }}
|
||||||
|
· {{ fmtHourShort(sessionsHook.proximaSessao.value.inicio_em) }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="mpa-kpi__big mpa-kpi__big--small">—</div>
|
||||||
|
<div class="mpa-kpi__cap">Sem futura</div>
|
||||||
|
</template>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filter chips -->
|
||||||
|
<div class="mpa-pron-filters" role="tablist">
|
||||||
|
<button
|
||||||
|
v-for="f in AGENDA_FILTERS"
|
||||||
|
:key="f.value"
|
||||||
|
type="button"
|
||||||
|
role="tab"
|
||||||
|
:aria-selected="agendaFilter === f.value"
|
||||||
|
class="mpa-pron-filter"
|
||||||
|
:class="{ 'is-active': agendaFilter === f.value }"
|
||||||
|
@click="agendaFilter = f.value"
|
||||||
|
>
|
||||||
|
<i :class="f.icon" />
|
||||||
|
<span>{{ f.label }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty state contextual -->
|
||||||
|
<div v-if="!agendaAgrupadas.length" class="mpa-empty mpa-empty--rich">
|
||||||
|
<div class="mpa-empty__icon"><i class="pi pi-calendar-times" /></div>
|
||||||
|
<div class="mpa-empty__title">
|
||||||
|
{{ sessionsHook.sessions.value.length ? 'Nenhuma sessão neste filtro' : 'Sem sessões registradas' }}
|
||||||
|
</div>
|
||||||
|
<div class="mpa-empty__sub">
|
||||||
|
<template v-if="sessionsHook.sessions.value.length">
|
||||||
|
Tente outro filtro acima ou veja "Todas" pra listar o histórico completo.
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
As sessões agendadas e atendidas com este paciente aparecerão aqui.
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mpa-w__body">
|
|
||||||
<p class="mpa-placeholder">
|
<!-- Grupos por mes -->
|
||||||
Em desenvolvimento — <strong>Fase 5</strong>. Já disponível: <strong>{{ kpiSessoes }}</strong>
|
<section
|
||||||
sessões totais ({{ kpiRealizadas }} realizadas) via composable.
|
v-for="g in agendaAgrupadas"
|
||||||
</p>
|
:key="g.key"
|
||||||
</div>
|
class="mpa-panel mpa-ag__group"
|
||||||
</div>
|
>
|
||||||
|
<header class="mpa-panel__head">
|
||||||
|
<div class="mpa-panel__title"><i class="pi pi-calendar" /> {{ g.label }}</div>
|
||||||
|
<span class="mpa-panel__badge">{{ g.items.length }}</span>
|
||||||
|
</header>
|
||||||
|
<ul class="mpa-ag__list">
|
||||||
|
<li
|
||||||
|
v-for="s in g.items"
|
||||||
|
:key="s.id"
|
||||||
|
class="mpa-ag__item"
|
||||||
|
:data-status="String(s.status || 'agendado').toLowerCase()"
|
||||||
|
>
|
||||||
|
<!-- Coluna data -->
|
||||||
|
<div class="mpa-ag__date">
|
||||||
|
<span class="mpa-ag__date-dow">{{ fmtDayShort(s.inicio_em) }}</span>
|
||||||
|
<span class="mpa-ag__date-day">{{ new Date(s.inicio_em).getDate() }}</span>
|
||||||
|
<span class="mpa-ag__date-time">{{ fmtHourShort(s.inicio_em) }}</span>
|
||||||
|
</div>
|
||||||
|
<!-- Coluna main -->
|
||||||
|
<div class="mpa-ag__main">
|
||||||
|
<div class="mpa-ag__top">
|
||||||
|
<Tag
|
||||||
|
:value="STATUS_LABEL[s.status] || s.status || 'Agendado'"
|
||||||
|
:severity="STATUS_SEVERITY[s.status] || 'info'"
|
||||||
|
class="mpa-ag__tag"
|
||||||
|
/>
|
||||||
|
<span v-if="s.modalidade" class="mpa-tl__chip">
|
||||||
|
<i :class="s.modalidade === 'online' ? 'pi pi-video' : 'pi pi-map-marker'" />
|
||||||
|
{{ s.modalidade === 'online' ? 'Online' : 'Presencial' }}
|
||||||
|
</span>
|
||||||
|
<span v-if="sessionDuration(s.inicio_em, s.fim_em)" class="mpa-tl__chip mpa-tl__chip--dim">
|
||||||
|
<i class="pi pi-clock" />
|
||||||
|
{{ sessionDuration(s.inicio_em, s.fim_em) }}
|
||||||
|
</span>
|
||||||
|
<span class="mpa-ag__rel">{{ fmtRelative(s.inicio_em) }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="s.titulo_custom || s.titulo" class="mpa-ag__title">
|
||||||
|
{{ s.titulo_custom || s.titulo }}
|
||||||
|
</div>
|
||||||
|
<p v-if="s.observacoes" class="mpa-ag__note">{{ s.observacoes }}</p>
|
||||||
|
</div>
|
||||||
|
<!-- Coluna actions -->
|
||||||
|
<div class="mpa-ag__actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
v-tooltip.left="'Marcar como realizada'"
|
||||||
|
class="mpa-ag__act mpa-ag__act--ok"
|
||||||
|
:disabled="sessionsHook.busy.value"
|
||||||
|
@click="updateSessionStatus(s, 'realizado', 'Sessão marcada como realizada')"
|
||||||
|
>
|
||||||
|
<i class="pi pi-check-circle" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
v-tooltip.left="'Marcar como falta'"
|
||||||
|
class="mpa-ag__act mpa-ag__act--warn"
|
||||||
|
:disabled="sessionsHook.busy.value"
|
||||||
|
@click="updateSessionStatus(s, 'faltou', 'Marcada como falta')"
|
||||||
|
>
|
||||||
|
<i class="pi pi-user-minus" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
v-tooltip.left="'Cancelar'"
|
||||||
|
class="mpa-ag__act mpa-ag__act--danger"
|
||||||
|
:disabled="sessionsHook.busy.value"
|
||||||
|
@click="updateSessionStatus(s, 'cancelado', 'Sessão cancelada')"
|
||||||
|
>
|
||||||
|
<i class="pi pi-ban" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ABA: Financeiro -->
|
<!-- ABA: Financeiro -->
|
||||||
@@ -2269,6 +2498,167 @@ void toast;
|
|||||||
font-size: 0.66rem !important;
|
font-size: 0.66rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ═══════ Tab Agenda (Fase 5) ═══════ */
|
||||||
|
.mpa-ag__group + .mpa-ag__group { margin-top: 10px; }
|
||||||
|
.mpa-ag__list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.mpa-ag__item {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 64px 1fr auto;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-top: 1px solid var(--m-border);
|
||||||
|
border-left: 3px solid transparent;
|
||||||
|
transition: background-color 120ms ease;
|
||||||
|
}
|
||||||
|
.mpa-ag__item:first-child { border-top: none; }
|
||||||
|
.mpa-ag__item:hover { background: var(--m-bg-medium); }
|
||||||
|
.mpa-ag__item[data-status*="realiz"],
|
||||||
|
.mpa-ag__item[data-status*="present"] { border-left-color: rgb(34, 197, 94); }
|
||||||
|
.mpa-ag__item[data-status*="falt"] { border-left-color: rgb(239, 68, 68); }
|
||||||
|
.mpa-ag__item[data-status*="cancel"],
|
||||||
|
.mpa-ag__item[data-status*="remarc"] { border-left-color: rgb(245, 158, 11); }
|
||||||
|
.mpa-ag__item[data-status*="agendado"] { border-left-color: rgb(96, 165, 250); }
|
||||||
|
|
||||||
|
/* Coluna data (esquerda) */
|
||||||
|
.mpa-ag__date {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--m-bg-medium);
|
||||||
|
border: 1px solid var(--m-border);
|
||||||
|
min-width: 56px;
|
||||||
|
}
|
||||||
|
.mpa-ag__date-dow {
|
||||||
|
font-size: 0.62rem;
|
||||||
|
color: var(--m-text-muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.mpa-ag__date-day {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--m-text);
|
||||||
|
line-height: 1.05;
|
||||||
|
margin: 2px 0;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
.mpa-ag__date-time {
|
||||||
|
font-size: 0.66rem;
|
||||||
|
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
||||||
|
color: var(--m-text-muted);
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Coluna main (centro) */
|
||||||
|
.mpa-ag__main {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.mpa-ag__top {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.mpa-ag__tag { font-size: 0.66rem !important; }
|
||||||
|
.mpa-ag__rel {
|
||||||
|
margin-left: auto;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--m-text-muted);
|
||||||
|
}
|
||||||
|
.mpa-ag__title {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--m-text);
|
||||||
|
}
|
||||||
|
.mpa-ag__note {
|
||||||
|
font-size: 0.74rem;
|
||||||
|
color: var(--m-text-muted);
|
||||||
|
line-height: 1.4;
|
||||||
|
overflow: hidden;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Coluna actions (direita) */
|
||||||
|
.mpa-ag__actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.mpa-ag__act {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
border-radius: 7px;
|
||||||
|
background: var(--m-bg-soft);
|
||||||
|
border: 1px solid var(--m-border);
|
||||||
|
color: var(--m-text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: inherit;
|
||||||
|
transition: background-color 120ms ease, color 120ms ease, border-color 120ms ease;
|
||||||
|
}
|
||||||
|
.mpa-ag__act > i { font-size: 0.85rem; }
|
||||||
|
.mpa-ag__act:hover:not(:disabled) {
|
||||||
|
background: var(--m-bg-soft-hover);
|
||||||
|
color: var(--m-text);
|
||||||
|
border-color: var(--m-border-strong);
|
||||||
|
}
|
||||||
|
.mpa-ag__act:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
.mpa-ag__act--ok:hover:not(:disabled) {
|
||||||
|
background: rgba(34, 197, 94, 0.12);
|
||||||
|
color: rgb(34, 197, 94);
|
||||||
|
border-color: rgba(34, 197, 94, 0.40);
|
||||||
|
}
|
||||||
|
.mpa-ag__act--warn:hover:not(:disabled) {
|
||||||
|
background: rgba(245, 158, 11, 0.12);
|
||||||
|
color: rgb(245, 158, 11);
|
||||||
|
border-color: rgba(245, 158, 11, 0.40);
|
||||||
|
}
|
||||||
|
.mpa-ag__act--danger:hover:not(:disabled) {
|
||||||
|
background: rgba(239, 68, 68, 0.12);
|
||||||
|
color: rgb(239, 68, 68);
|
||||||
|
border-color: rgba(239, 68, 68, 0.40);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile: stack date + main em coluna; actions vai pra baixo */
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.mpa-ag__item {
|
||||||
|
grid-template-columns: 56px 1fr;
|
||||||
|
grid-template-rows: auto auto;
|
||||||
|
}
|
||||||
|
.mpa-ag__actions {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
.mpa-ag__rel {
|
||||||
|
flex-basis: 100%;
|
||||||
|
margin-left: 0;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ═══════ Tab Prontuario (Fase 4 MVP) ═══════ */
|
/* ═══════ Tab Prontuario (Fase 4 MVP) ═══════ */
|
||||||
.mpa-pron-hint {
|
.mpa-pron-hint {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
Reference in New Issue
Block a user