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:
@@ -15,459 +15,449 @@
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
import { useTenantStore } from '@/stores/tenantStore'
|
||||
import { useLayout } from '@/layout/composables/layout'
|
||||
import { ref, computed, watch, onMounted } from 'vue';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
import { useLayout } from '@/layout/composables/layout';
|
||||
|
||||
const { layoutConfig, isDarkTheme } = useLayout()
|
||||
const tenantStore = useTenantStore()
|
||||
const { layoutConfig, isDarkTheme } = useLayout();
|
||||
const tenantStore = useTenantStore();
|
||||
|
||||
// ── Período ───────────────────────────────────────────────
|
||||
const PERIODS = [
|
||||
{ label: 'Esta semana', value: 'week' },
|
||||
{ label: 'Este mês', value: 'month' },
|
||||
{ label: 'Últimos 3 meses', value: '3months' },
|
||||
{ label: 'Últimos 6 meses', value: '6months' },
|
||||
]
|
||||
{ label: 'Esta semana', value: 'week' },
|
||||
{ label: 'Este mês', value: 'month' },
|
||||
{ label: 'Últimos 3 meses', value: '3months' },
|
||||
{ label: 'Últimos 6 meses', value: '6months' }
|
||||
];
|
||||
|
||||
const selectedPeriod = ref('month')
|
||||
const selectedPeriod = ref('month');
|
||||
|
||||
function periodRange (period) {
|
||||
const now = new Date()
|
||||
let start, end
|
||||
if (period === 'week') {
|
||||
start = new Date(now); start.setDate(now.getDate() - now.getDay()); start.setHours(0, 0, 0, 0)
|
||||
end = new Date(now); end.setHours(23, 59, 59, 999)
|
||||
} else if (period === 'month') {
|
||||
start = new Date(now.getFullYear(), now.getMonth(), 1, 0, 0, 0, 0)
|
||||
end = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59, 999)
|
||||
} else if (period === '3months') {
|
||||
start = new Date(now.getFullYear(), now.getMonth() - 2, 1, 0, 0, 0, 0)
|
||||
end = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59, 999)
|
||||
} else if (period === '6months') {
|
||||
start = new Date(now.getFullYear(), now.getMonth() - 5, 1, 0, 0, 0, 0)
|
||||
end = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59, 999)
|
||||
}
|
||||
return { start, end }
|
||||
function periodRange(period) {
|
||||
const now = new Date();
|
||||
let start, end;
|
||||
if (period === 'week') {
|
||||
start = new Date(now);
|
||||
start.setDate(now.getDate() - now.getDay());
|
||||
start.setHours(0, 0, 0, 0);
|
||||
end = new Date(now);
|
||||
end.setHours(23, 59, 59, 999);
|
||||
} else if (period === 'month') {
|
||||
start = new Date(now.getFullYear(), now.getMonth(), 1, 0, 0, 0, 0);
|
||||
end = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59, 999);
|
||||
} else if (period === '3months') {
|
||||
start = new Date(now.getFullYear(), now.getMonth() - 2, 1, 0, 0, 0, 0);
|
||||
end = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59, 999);
|
||||
} else if (period === '6months') {
|
||||
start = new Date(now.getFullYear(), now.getMonth() - 5, 1, 0, 0, 0, 0);
|
||||
end = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59, 999);
|
||||
}
|
||||
return { start, end };
|
||||
}
|
||||
|
||||
// ── Dados ─────────────────────────────────────────────────
|
||||
const loading = ref(false)
|
||||
const hasLoaded = ref(false)
|
||||
const sessions = ref([])
|
||||
const loadError = ref('')
|
||||
const loading = ref(false);
|
||||
const hasLoaded = ref(false);
|
||||
const sessions = ref([]);
|
||||
const loadError = ref('');
|
||||
|
||||
async function loadSessions () {
|
||||
const uid = tenantStore.user?.id || null
|
||||
const tenantId = tenantStore.activeTenantId || null
|
||||
if (!uid || !tenantId) return
|
||||
async function loadSessions() {
|
||||
const uid = tenantStore.user?.id || null;
|
||||
const tenantId = tenantStore.activeTenantId || null;
|
||||
if (!uid || !tenantId) return;
|
||||
|
||||
const { start, end } = periodRange(selectedPeriod.value)
|
||||
loading.value = true
|
||||
loadError.value = ''
|
||||
sessions.value = []
|
||||
const { start, end } = periodRange(selectedPeriod.value);
|
||||
loading.value = true;
|
||||
loadError.value = '';
|
||||
sessions.value = [];
|
||||
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('agenda_eventos')
|
||||
.select('id, inicio_em, fim_em, status, modalidade, tipo, titulo, titulo_custom, patient_id, patients(nome_completo)')
|
||||
.eq('tenant_id', tenantId)
|
||||
.eq('owner_id', uid)
|
||||
.gte('inicio_em', start.toISOString())
|
||||
.lte('inicio_em', end.toISOString())
|
||||
.order('inicio_em', { ascending: false })
|
||||
.limit(500)
|
||||
if (error) throw error
|
||||
sessions.value = data || []
|
||||
} catch (e) {
|
||||
loadError.value = e?.message || 'Falha ao carregar relatório.'
|
||||
} finally {
|
||||
loading.value = false
|
||||
hasLoaded.value = true
|
||||
}
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('agenda_eventos')
|
||||
.select('id, inicio_em, fim_em, status, modalidade, tipo, titulo, titulo_custom, patient_id, patients(nome_completo)')
|
||||
.eq('tenant_id', tenantId)
|
||||
.eq('owner_id', uid)
|
||||
.gte('inicio_em', start.toISOString())
|
||||
.lte('inicio_em', end.toISOString())
|
||||
.order('inicio_em', { ascending: false })
|
||||
.limit(500);
|
||||
if (error) throw error;
|
||||
sessions.value = data || [];
|
||||
} catch (e) {
|
||||
loadError.value = e?.message || 'Falha ao carregar relatório.';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
hasLoaded.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Métricas ──────────────────────────────────────────────
|
||||
const total = computed(() => sessions.value.length)
|
||||
const realizadas = computed(() => sessions.value.filter(s => s.status === 'realizado').length)
|
||||
const faltas = computed(() => sessions.value.filter(s => s.status === 'faltou').length)
|
||||
const canceladas = computed(() => sessions.value.filter(s => s.status === 'cancelado').length)
|
||||
const agendadas = computed(() => sessions.value.filter(s => !s.status || s.status === 'agendado').length)
|
||||
const remarcadas = computed(() => sessions.value.filter(s => s.status === 'remarcado').length)
|
||||
const total = computed(() => sessions.value.length);
|
||||
const realizadas = computed(() => sessions.value.filter((s) => s.status === 'realizado').length);
|
||||
const faltas = computed(() => sessions.value.filter((s) => s.status === 'faltou').length);
|
||||
const canceladas = computed(() => sessions.value.filter((s) => s.status === 'cancelado').length);
|
||||
const agendadas = computed(() => sessions.value.filter((s) => !s.status || s.status === 'agendado').length);
|
||||
const remarcadas = computed(() => sessions.value.filter((s) => s.status === 'remarcado').length);
|
||||
const taxaRealizacao = computed(() => {
|
||||
const denom = realizadas.value + faltas.value + canceladas.value
|
||||
if (!denom) return null
|
||||
return Math.round((realizadas.value / denom) * 100)
|
||||
})
|
||||
const denom = realizadas.value + faltas.value + canceladas.value;
|
||||
if (!denom) return null;
|
||||
return Math.round((realizadas.value / denom) * 100);
|
||||
});
|
||||
|
||||
// ── Filtro de status na tabela ────────────────────────────
|
||||
const filtroTabela = ref(null) // null = todos
|
||||
const filtroTabela = ref(null); // null = todos
|
||||
|
||||
const sessionsFiltradas = computed(() => {
|
||||
if (!filtroTabela.value) return sessions.value
|
||||
if (filtroTabela.value === 'agendado') return sessions.value.filter(s => !s.status || s.status === 'agendado')
|
||||
return sessions.value.filter(s => s.status === filtroTabela.value)
|
||||
})
|
||||
if (!filtroTabela.value) return sessions.value;
|
||||
if (filtroTabela.value === 'agendado') return sessions.value.filter((s) => !s.status || s.status === 'agendado');
|
||||
return sessions.value.filter((s) => s.status === filtroTabela.value);
|
||||
});
|
||||
|
||||
function toggleFiltroTabela (val) {
|
||||
filtroTabela.value = filtroTabela.value === val ? null : val
|
||||
function toggleFiltroTabela(val) {
|
||||
filtroTabela.value = filtroTabela.value === val ? null : val;
|
||||
}
|
||||
|
||||
// ── Quick-stats config ────────────────────────────────────
|
||||
const quickStats = computed(() => [
|
||||
{ label: 'Total', value: total.value, filter: null, cls: '', valCls: 'text-[var(--text-color)]' },
|
||||
{ label: 'Realizadas', value: realizadas.value, filter: 'realizado', cls: 'qs-ok', valCls: 'text-green-500' },
|
||||
{ label: 'Faltas', value: faltas.value, filter: 'faltou', cls: 'qs-danger', valCls: 'text-red-500' },
|
||||
{ label: 'Canceladas', value: canceladas.value, filter: 'cancelado', cls: 'qs-warn', valCls: 'text-orange-500' },
|
||||
{ label: 'Agendadas', value: agendadas.value, filter: 'agendado', cls: 'qs-info', valCls: 'text-sky-500' },
|
||||
{ label: 'Taxa realização', value: taxaRealizacao.value != null ? `${taxaRealizacao.value}%` : '—', filter: null, cls: taxaRealizacao.value != null && taxaRealizacao.value >= 85 ? 'qs-ok' : '', valCls: taxaRealizacao.value != null && taxaRealizacao.value >= 85 ? 'text-green-500' : 'text-[var(--text-color)]' },
|
||||
])
|
||||
{ label: 'Total', value: total.value, filter: null, cls: '', valCls: 'text-[var(--text-color)]' },
|
||||
{ label: 'Realizadas', value: realizadas.value, filter: 'realizado', cls: 'qs-ok', valCls: 'text-green-500' },
|
||||
{ label: 'Faltas', value: faltas.value, filter: 'faltou', cls: 'qs-danger', valCls: 'text-red-500' },
|
||||
{ label: 'Canceladas', value: canceladas.value, filter: 'cancelado', cls: 'qs-warn', valCls: 'text-orange-500' },
|
||||
{ label: 'Agendadas', value: agendadas.value, filter: 'agendado', cls: 'qs-info', valCls: 'text-sky-500' },
|
||||
{
|
||||
label: 'Taxa realização',
|
||||
value: taxaRealizacao.value != null ? `${taxaRealizacao.value}%` : '—',
|
||||
filter: null,
|
||||
cls: taxaRealizacao.value != null && taxaRealizacao.value >= 85 ? 'qs-ok' : '',
|
||||
valCls: taxaRealizacao.value != null && taxaRealizacao.value >= 85 ? 'text-green-500' : 'text-[var(--text-color)]'
|
||||
}
|
||||
]);
|
||||
|
||||
// ── Gráfico ───────────────────────────────────────────────
|
||||
function isoWeek (d) {
|
||||
const dt = new Date(d)
|
||||
const day = dt.getDay() || 7
|
||||
dt.setDate(dt.getDate() + 4 - day)
|
||||
const yearStart = new Date(dt.getFullYear(), 0, 1)
|
||||
const wk = Math.ceil((((dt - yearStart) / 86400000) + 1) / 7)
|
||||
return `${dt.getFullYear()}-S${String(wk).padStart(2, '0')}`
|
||||
function isoWeek(d) {
|
||||
const dt = new Date(d);
|
||||
const day = dt.getDay() || 7;
|
||||
dt.setDate(dt.getDate() + 4 - day);
|
||||
const yearStart = new Date(dt.getFullYear(), 0, 1);
|
||||
const wk = Math.ceil(((dt - yearStart) / 86400000 + 1) / 7);
|
||||
return `${dt.getFullYear()}-S${String(wk).padStart(2, '0')}`;
|
||||
}
|
||||
function isoMonth (d) {
|
||||
const dt = new Date(d)
|
||||
return `${dt.getFullYear()}-${String(dt.getMonth() + 1).padStart(2, '0')}`
|
||||
function isoMonth(d) {
|
||||
const dt = new Date(d);
|
||||
return `${dt.getFullYear()}-${String(dt.getMonth() + 1).padStart(2, '0')}`;
|
||||
}
|
||||
function monthLabel (key) {
|
||||
const [y, m] = key.split('-')
|
||||
const names = ['Jan','Fev','Mar','Abr','Mai','Jun','Jul','Ago','Set','Out','Nov','Dez']
|
||||
return `${names[Number(m) - 1]}/${y}`
|
||||
function monthLabel(key) {
|
||||
const [y, m] = key.split('-');
|
||||
const names = ['Jan', 'Fev', 'Mar', 'Abr', 'Mai', 'Jun', 'Jul', 'Ago', 'Set', 'Out', 'Nov', 'Dez'];
|
||||
return `${names[Number(m) - 1]}/${y}`;
|
||||
}
|
||||
|
||||
const chartData = computed(() => {
|
||||
const groupBy = selectedPeriod.value === 'week' ? isoWeek : isoMonth
|
||||
const labelFn = selectedPeriod.value === 'week' ? k => k : monthLabel
|
||||
const buckets = {}
|
||||
for (const s of sessions.value) {
|
||||
const key = groupBy(s.inicio_em)
|
||||
if (!buckets[key]) buckets[key] = { realizado: 0, faltou: 0, cancelado: 0, outros: 0 }
|
||||
const st = s.status || 'agendado'
|
||||
if (st === 'realizado') buckets[key].realizado++
|
||||
else if (st === 'faltou') buckets[key].faltou++
|
||||
else if (st === 'cancelado') buckets[key].cancelado++
|
||||
else buckets[key].outros++
|
||||
}
|
||||
const keys = Object.keys(buckets).sort()
|
||||
return {
|
||||
labels: keys.map(labelFn),
|
||||
datasets: [
|
||||
{ label: 'Realizadas', backgroundColor: '#22c55e', data: keys.map(k => buckets[k].realizado), barThickness: 20 },
|
||||
{ label: 'Faltas', backgroundColor: '#ef4444', data: keys.map(k => buckets[k].faltou), barThickness: 20 },
|
||||
{ label: 'Canceladas', backgroundColor: '#f97316', data: keys.map(k => buckets[k].cancelado), barThickness: 20 },
|
||||
{ label: 'Outros', backgroundColor: '#93c5fd', data: keys.map(k => buckets[k].outros), barThickness: 20 },
|
||||
]
|
||||
}
|
||||
})
|
||||
const groupBy = selectedPeriod.value === 'week' ? isoWeek : isoMonth;
|
||||
const labelFn = selectedPeriod.value === 'week' ? (k) => k : monthLabel;
|
||||
const buckets = {};
|
||||
for (const s of sessions.value) {
|
||||
const key = groupBy(s.inicio_em);
|
||||
if (!buckets[key]) buckets[key] = { realizado: 0, faltou: 0, cancelado: 0, outros: 0 };
|
||||
const st = s.status || 'agendado';
|
||||
if (st === 'realizado') buckets[key].realizado++;
|
||||
else if (st === 'faltou') buckets[key].faltou++;
|
||||
else if (st === 'cancelado') buckets[key].cancelado++;
|
||||
else buckets[key].outros++;
|
||||
}
|
||||
const keys = Object.keys(buckets).sort();
|
||||
return {
|
||||
labels: keys.map(labelFn),
|
||||
datasets: [
|
||||
{ label: 'Realizadas', backgroundColor: '#22c55e', data: keys.map((k) => buckets[k].realizado), barThickness: 20 },
|
||||
{ label: 'Faltas', backgroundColor: '#ef4444', data: keys.map((k) => buckets[k].faltou), barThickness: 20 },
|
||||
{ label: 'Canceladas', backgroundColor: '#f97316', data: keys.map((k) => buckets[k].cancelado), barThickness: 20 },
|
||||
{ label: 'Outros', backgroundColor: '#93c5fd', data: keys.map((k) => buckets[k].outros), barThickness: 20 }
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
const chartOptions = computed(() => {
|
||||
const ds = getComputedStyle(document.documentElement)
|
||||
const borderColor = ds.getPropertyValue('--surface-border') || '#e2e8f0'
|
||||
const textMutedColor = ds.getPropertyValue('--text-color-secondary') || '#64748b'
|
||||
return {
|
||||
maintainAspectRatio: false,
|
||||
plugins: { legend: { labels: { color: textMutedColor } } },
|
||||
scales: {
|
||||
x: { stacked: true, ticks: { color: textMutedColor }, grid: { color: 'transparent' } },
|
||||
y: { stacked: true, ticks: { color: textMutedColor, precision: 0 }, grid: { color: borderColor, drawTicks: false } }
|
||||
}
|
||||
}
|
||||
})
|
||||
const ds = getComputedStyle(document.documentElement);
|
||||
const borderColor = ds.getPropertyValue('--surface-border') || '#e2e8f0';
|
||||
const textMutedColor = ds.getPropertyValue('--text-color-secondary') || '#64748b';
|
||||
return {
|
||||
maintainAspectRatio: false,
|
||||
plugins: { legend: { labels: { color: textMutedColor } } },
|
||||
scales: {
|
||||
x: { stacked: true, ticks: { color: textMutedColor }, grid: { color: 'transparent' } },
|
||||
y: { stacked: true, ticks: { color: textMutedColor, precision: 0 }, grid: { color: borderColor, drawTicks: false } }
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// ── Tabela helpers ────────────────────────────────────────
|
||||
const STATUS_LABEL = {
|
||||
agendado: 'Agendado', realizado: 'Realizado', faltou: 'Faltou',
|
||||
cancelado: 'Cancelado', remarcado: 'Remarcado', bloqueado: 'Bloqueado',
|
||||
}
|
||||
agendado: 'Agendado',
|
||||
realizado: 'Realizado',
|
||||
faltou: 'Faltou',
|
||||
cancelado: 'Cancelado',
|
||||
remarcado: 'Remarcado',
|
||||
bloqueado: 'Bloqueado'
|
||||
};
|
||||
const STATUS_SEVERITY = {
|
||||
agendado: 'info', realizado: 'success', faltou: 'danger',
|
||||
cancelado: 'warn', remarcado: 'secondary', bloqueado: 'secondary',
|
||||
}
|
||||
agendado: 'info',
|
||||
realizado: 'success',
|
||||
faltou: 'danger',
|
||||
cancelado: 'warn',
|
||||
remarcado: 'secondary',
|
||||
bloqueado: 'secondary'
|
||||
};
|
||||
|
||||
function fmtDateTimeBR (iso) {
|
||||
if (!iso) return '—'
|
||||
const d = new Date(iso)
|
||||
if (Number.isNaN(d.getTime())) return iso
|
||||
const dd = String(d.getDate()).padStart(2, '0')
|
||||
const mm = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const yy = d.getFullYear()
|
||||
const hh = String(d.getHours()).padStart(2, '0')
|
||||
const mi = String(d.getMinutes()).padStart(2, '0')
|
||||
return `${dd}/${mm}/${yy} ${hh}:${mi}`
|
||||
function fmtDateTimeBR(iso) {
|
||||
if (!iso) return '—';
|
||||
const d = new Date(iso);
|
||||
if (Number.isNaN(d.getTime())) return iso;
|
||||
const dd = String(d.getDate()).padStart(2, '0');
|
||||
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const yy = d.getFullYear();
|
||||
const hh = String(d.getHours()).padStart(2, '0');
|
||||
const mi = String(d.getMinutes()).padStart(2, '0');
|
||||
return `${dd}/${mm}/${yy} ${hh}:${mi}`;
|
||||
}
|
||||
function sessionTitle(s) {
|
||||
return s.titulo_custom || s.titulo || (s.tipo ? s.tipo : 'Sessão');
|
||||
}
|
||||
function patientName(s) {
|
||||
return s.patients?.nome_completo || '—';
|
||||
}
|
||||
function sessionTitle (s) { return s.titulo_custom || s.titulo || (s.tipo ? s.tipo : 'Sessão') }
|
||||
function patientName (s) { return s.patients?.nome_completo || '—' }
|
||||
|
||||
// ── Watch & mount ─────────────────────────────────────────
|
||||
watch(selectedPeriod, () => { filtroTabela.value = null; loadSessions() })
|
||||
watch([() => layoutConfig.primary, () => layoutConfig.surface, isDarkTheme], () => {})
|
||||
onMounted(loadSessions)
|
||||
watch(selectedPeriod, () => {
|
||||
filtroTabela.value = null;
|
||||
loadSessions();
|
||||
});
|
||||
watch([() => layoutConfig.primary, () => layoutConfig.surface, isDarkTheme], () => {});
|
||||
onMounted(loadSessions);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Sentinel -->
|
||||
<div class="h-px" />
|
||||
<!-- Sentinel -->
|
||||
<div class="h-px" />
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════
|
||||
<!-- ══════════════════════════════════════════════════════
|
||||
HERO sticky
|
||||
═══════════════════════════════════════════════════════ -->
|
||||
<section
|
||||
class="sticky mx-3 md:mx-4 mb-3 z-20 overflow-hidden rounded-md border border-[var(--surface-border,#e2e8f0)] bg-[var(--surface-card,#fff)] px-3 py-2.5 transition-[border-radius] duration-200"
|
||||
:style="{ top: 'var(--layout-sticky-top, 56px)' }"
|
||||
>
|
||||
<!-- Blobs -->
|
||||
<div class="absolute inset-0 pointer-events-none overflow-hidden" aria-hidden="true">
|
||||
<div class="absolute w-64 h-64 -top-16 -right-8 rounded-full blur-[60px] bg-indigo-500/10" />
|
||||
<div class="absolute w-72 h-72 top-0 -left-16 rounded-full blur-[60px] bg-emerald-400/[0.08]" />
|
||||
</div>
|
||||
|
||||
<div class="relative z-[1] flex items-center gap-3 flex-wrap">
|
||||
|
||||
<!-- Brand -->
|
||||
<div class="flex items-center gap-2 flex-shrink-0">
|
||||
<div class="grid place-items-center w-9 h-9 rounded-md flex-shrink-0 bg-indigo-500/10 text-indigo-500">
|
||||
<i class="pi pi-chart-bar text-base" />
|
||||
<section
|
||||
class="sticky mx-3 md:mx-4 mb-3 z-20 overflow-hidden rounded-md border border-[var(--surface-border,#e2e8f0)] bg-[var(--surface-card,#fff)] px-3 py-2.5 transition-[border-radius] duration-200"
|
||||
:style="{ top: 'var(--layout-sticky-top, 56px)' }"
|
||||
>
|
||||
<!-- Blobs -->
|
||||
<div class="absolute inset-0 pointer-events-none overflow-hidden" aria-hidden="true">
|
||||
<div class="absolute w-64 h-64 -top-16 -right-8 rounded-full blur-[60px] bg-indigo-500/10" />
|
||||
<div class="absolute w-72 h-72 top-0 -left-16 rounded-full blur-[60px] bg-emerald-400/[0.08]" />
|
||||
</div>
|
||||
<div class="min-w-0 hidden lg:block">
|
||||
<div class="text-[1rem] font-bold tracking-tight text-[var(--text-color)]">Relatórios</div>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)]">Visão geral das suas sessões</div>
|
||||
|
||||
<div class="relative z-[1] flex items-center gap-3 flex-wrap">
|
||||
<!-- Brand -->
|
||||
<div class="flex items-center gap-2 flex-shrink-0">
|
||||
<div class="grid place-items-center w-9 h-9 rounded-md flex-shrink-0 bg-indigo-500/10 text-indigo-500">
|
||||
<i class="pi pi-chart-bar text-base" />
|
||||
</div>
|
||||
<div class="min-w-0 hidden lg:block">
|
||||
<div class="text-[1rem] font-bold tracking-tight text-[var(--text-color)]">Relatórios</div>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)]">Visão geral das suas sessões</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Seletor de período -->
|
||||
<div class="flex-1 min-w-0 hidden xl:flex items-center mx-2">
|
||||
<SelectButton v-model="selectedPeriod" :options="PERIODS" option-label="label" option-value="value" :allow-empty="false" size="small" />
|
||||
</div>
|
||||
|
||||
<!-- Refresh -->
|
||||
<div class="flex items-center gap-1.5 flex-shrink-0 ml-auto">
|
||||
<Button icon="pi pi-refresh" severity="secondary" outlined class="h-9 w-9 rounded-full" :loading="loading" title="Recarregar" @click="loadSessions" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Seletor de período -->
|
||||
<div class="flex-1 min-w-0 hidden xl:flex items-center mx-2">
|
||||
<SelectButton
|
||||
v-model="selectedPeriod"
|
||||
:options="PERIODS"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
:allow-empty="false"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
<!-- Seletor de período — mobile (abaixo da linha principal) -->
|
||||
<div class="xl:hidden relative z-[1] mt-2.5 flex flex-wrap gap-1.5">
|
||||
<button
|
||||
v-for="p in PERIODS"
|
||||
:key="p.value"
|
||||
class="inline-flex items-center px-3 py-1 rounded-full text-[1rem] font-semibold border-[1.5px] cursor-pointer transition-all duration-150 select-none"
|
||||
:class="
|
||||
selectedPeriod === p.value
|
||||
? 'bg-[var(--primary-color,#6366f1)] border-[var(--primary-color,#6366f1)] text-white'
|
||||
: 'border-[var(--surface-border,#e2e8f0)] bg-[var(--surface-ground,#f8fafc)] text-[var(--text-color-secondary)] hover:border-indigo-300 hover:text-[var(--text-color)]'
|
||||
"
|
||||
@click="selectedPeriod = p.value"
|
||||
>
|
||||
{{ p.label }}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Refresh -->
|
||||
<div class="flex items-center gap-1.5 flex-shrink-0 ml-auto">
|
||||
<Button
|
||||
icon="pi pi-refresh"
|
||||
severity="secondary"
|
||||
outlined
|
||||
class="h-9 w-9 rounded-full"
|
||||
:loading="loading"
|
||||
title="Recarregar"
|
||||
@click="loadSessions"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Seletor de período — mobile (abaixo da linha principal) -->
|
||||
<div class="xl:hidden relative z-[1] mt-2.5 flex flex-wrap gap-1.5">
|
||||
<button
|
||||
v-for="p in PERIODS"
|
||||
:key="p.value"
|
||||
class="inline-flex items-center px-3 py-1 rounded-full text-[1rem] font-semibold border-[1.5px] cursor-pointer transition-all duration-150 select-none"
|
||||
:class="selectedPeriod === p.value
|
||||
? 'bg-[var(--primary-color,#6366f1)] border-[var(--primary-color,#6366f1)] text-white'
|
||||
: 'border-[var(--surface-border,#e2e8f0)] bg-[var(--surface-ground,#f8fafc)] text-[var(--text-color-secondary)] hover:border-indigo-300 hover:text-[var(--text-color)]'"
|
||||
@click="selectedPeriod = p.value"
|
||||
>
|
||||
{{ p.label }}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════
|
||||
<!-- ══════════════════════════════════════════════════════
|
||||
CONTEÚDO
|
||||
═══════════════════════════════════════════════════════ -->
|
||||
<div class="px-3 md:px-4 pb-8 flex flex-col gap-3">
|
||||
<div class="px-3 md:px-4 pb-8 flex flex-col gap-3">
|
||||
<!-- Erro -->
|
||||
<Message v-if="loadError" severity="error">{{ loadError }}</Message>
|
||||
|
||||
<!-- Erro -->
|
||||
<Message v-if="loadError" severity="error">{{ loadError }}</Message>
|
||||
<!-- Loading skeleton -->
|
||||
<div v-if="loading" class="flex flex-col gap-3">
|
||||
<!-- Stats skeleton -->
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<div v-for="n in 6" :key="n" class="flex-1 min-w-[80px] h-[72px] rounded-md bg-[var(--surface-border,#e2e8f0)] animate-pulse" />
|
||||
</div>
|
||||
<!-- Chart skeleton -->
|
||||
<div class="rounded-md border border-[var(--surface-border,#e2e8f0)] bg-[var(--surface-card,#fff)] h-[280px] animate-pulse" />
|
||||
</div>
|
||||
|
||||
<!-- Loading skeleton -->
|
||||
<div v-if="loading" class="flex flex-col gap-3">
|
||||
<!-- Stats skeleton -->
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<div v-for="n in 6" :key="n" class="flex-1 min-w-[80px] h-[72px] rounded-md bg-[var(--surface-border,#e2e8f0)] animate-pulse" />
|
||||
</div>
|
||||
<!-- Chart skeleton -->
|
||||
<div class="rounded-md border border-[var(--surface-border,#e2e8f0)] bg-[var(--surface-card,#fff)] h-[280px] animate-pulse" />
|
||||
<template v-else>
|
||||
<!-- ── QUICK-STATS clicáveis ────────────────────── -->
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<div
|
||||
v-for="s in quickStats"
|
||||
:key="s.label"
|
||||
class="flex flex-col gap-0.5 px-4 py-2.5 rounded-md border flex-1 min-w-[80px] transition-[border-color,box-shadow,background] duration-150"
|
||||
:class="[
|
||||
s.filter !== null ? 'cursor-pointer select-none' : '',
|
||||
s.filter !== null && filtroTabela === s.filter
|
||||
? 'border-[var(--primary-color,#6366f1)] shadow-[0_0_0_3px_rgba(99,102,241,0.15)] bg-[var(--surface-card,#fff)]'
|
||||
: s.cls === 'qs-ok'
|
||||
? 'border-green-500/25 bg-green-500/5 hover:border-green-500/40'
|
||||
: s.cls === 'qs-danger'
|
||||
? 'border-red-500/25 bg-red-500/5 hover:border-red-500/40'
|
||||
: s.cls === 'qs-warn'
|
||||
? 'border-orange-500/25 bg-orange-500/5 hover:border-orange-500/40'
|
||||
: s.cls === 'qs-info'
|
||||
? 'border-sky-500/25 bg-sky-500/5 hover:border-sky-500/40'
|
||||
: 'border-[var(--surface-border,#e2e8f0)] bg-[var(--surface-card,#fff)] hover:border-indigo-300/50'
|
||||
]"
|
||||
@click="s.filter !== null ? toggleFiltroTabela(s.filter) : null"
|
||||
>
|
||||
<div class="text-[1.35rem] font-bold leading-none" :class="s.valCls">{{ s.value }}</div>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] opacity-75 whitespace-nowrap">{{ s.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chip de filtro ativo na tabela -->
|
||||
<div v-if="filtroTabela" class="flex items-center gap-2">
|
||||
<span class="text-[1rem] text-[var(--text-color-secondary)]">Filtrando por:</span>
|
||||
<span class="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-[0.75rem] font-semibold bg-[var(--primary-color,#6366f1)] text-white">
|
||||
{{ STATUS_LABEL[filtroTabela] || filtroTabela }}
|
||||
<button class="ml-0.5 opacity-70 hover:opacity-100" @click="filtroTabela = null">
|
||||
<i class="pi pi-times text-[0.6rem]" />
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- ── GRÁFICO ──────────────────────────────────── -->
|
||||
<div v-if="total > 0" class="rounded-md border border-[var(--surface-border,#e2e8f0)] bg-[var(--surface-card,#fff)] overflow-hidden">
|
||||
<div class="flex items-center justify-between px-4 py-3 border-b border-[var(--surface-border,#e2e8f0)]">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="pi pi-chart-bar text-[var(--text-color-secondary)] opacity-60" />
|
||||
<span class="font-semibold text-[1rem] text-[var(--text-color)]"> Sessões por {{ selectedPeriod === 'week' ? 'semana' : 'mês' }} </span>
|
||||
</div>
|
||||
<span class="text-[0.72rem] text-[var(--text-color-secondary)] opacity-60">{{ total }} sessão{{ total !== 1 ? 'ões' : '' }}</span>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<Chart type="bar" :data="chartData" :options="chartOptions" class="h-64" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── TABELA ───────────────────────────────────── -->
|
||||
<div class="rounded-md border border-[var(--surface-border,#e2e8f0)] bg-[var(--surface-card,#fff)] overflow-hidden">
|
||||
<!-- Cabeçalho da seção -->
|
||||
<div class="flex items-center justify-between px-4 py-3 border-b border-[var(--surface-border,#e2e8f0)]">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="pi pi-table text-[var(--text-color-secondary)] opacity-60" />
|
||||
<span class="font-semibold text-[1rem] text-[var(--text-color)]">Sessões no período</span>
|
||||
<span v-if="filtroTabela" class="text-[0.72rem] text-[var(--text-color-secondary)] opacity-70">(filtrado)</span>
|
||||
</div>
|
||||
<span class="inline-flex items-center justify-center min-w-[22px] h-[22px] px-1.5 rounded-full bg-[var(--primary-color,#6366f1)] text-white text-[1rem] font-bold">
|
||||
{{ sessionsFiltradas.length }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Empty state (sem dados no período) -->
|
||||
<div v-if="!sessions.length" class="flex flex-col items-center justify-center gap-3 py-14 px-6 text-center border-2 border-dashed border-[var(--surface-border,#e2e8f0)] mx-4 my-4 rounded-md bg-[var(--surface-ground,#f8fafc)]">
|
||||
<div class="relative">
|
||||
<div class="grid place-items-center w-16 h-16 rounded-2xl bg-[var(--surface-card,#fff)] border border-[var(--surface-border,#e2e8f0)] shadow-sm text-[var(--text-color-secondary)]">
|
||||
<i class="pi pi-chart-bar text-3xl opacity-25" />
|
||||
</div>
|
||||
<div class="absolute -top-1.5 -right-1.5 w-6 h-6 rounded-full bg-[var(--surface-card,#fff)] border border-[var(--surface-border,#e2e8f0)] shadow-sm grid place-items-center">
|
||||
<i class="pi pi-times text-[0.58rem] text-[var(--text-color-secondary)] opacity-50" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold text-[0.9rem] text-[var(--text-color)] mb-0.5">Nenhuma sessão no período</div>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] max-w-xs">Tente selecionar um período diferente.</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2 mt-1 justify-center">
|
||||
<button
|
||||
v-for="p in PERIODS"
|
||||
:key="p.value"
|
||||
class="inline-flex items-center px-3 py-1 rounded-full text-[0.75rem] font-semibold border cursor-pointer transition-colors duration-150"
|
||||
:class="
|
||||
selectedPeriod === p.value ? 'bg-[var(--primary-color,#6366f1)] border-[var(--primary-color,#6366f1)] text-white' : 'border-[var(--surface-border,#e2e8f0)] text-[var(--text-color-secondary)] hover:border-indigo-300'
|
||||
"
|
||||
@click="selectedPeriod = p.value"
|
||||
>
|
||||
{{ p.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty state (filtro sem resultado) -->
|
||||
<div v-else-if="!sessionsFiltradas.length" class="flex flex-col items-center gap-2 py-10 text-center text-[var(--text-color-secondary)]">
|
||||
<i class="pi pi-filter-slash text-2xl opacity-30" />
|
||||
<div class="font-semibold text-[0.88rem]">Nenhuma sessão com este status</div>
|
||||
<Button label="Limpar filtro" icon="pi pi-times" severity="secondary" outlined size="small" class="rounded-full mt-1" @click="filtroTabela = null" />
|
||||
</div>
|
||||
|
||||
<!-- DataTable -->
|
||||
<DataTable v-else :value="sessionsFiltradas" :rows="20" paginator :rows-per-page-options="[10, 20, 50]" scrollable scroll-height="480px" class="rel-datatable">
|
||||
<Column field="inicio_em" header="Data / Hora" :sortable="true" style="min-width: 140px">
|
||||
<template #body="{ data }">
|
||||
<span class="font-medium">{{ fmtDateTimeBR(data.inicio_em) }}</span>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column header="Paciente" style="min-width: 160px">
|
||||
<template #body="{ data }">{{ patientName(data) }}</template>
|
||||
</Column>
|
||||
|
||||
<Column header="Sessão" style="min-width: 160px">
|
||||
<template #body="{ data }">{{ sessionTitle(data) }}</template>
|
||||
</Column>
|
||||
|
||||
<Column field="modalidade" header="Modalidade" style="min-width: 110px">
|
||||
<template #body="{ data }">
|
||||
{{ data.modalidade === 'online' ? 'Online' : data.modalidade === 'presencial' ? 'Presencial' : data.modalidade || '—' }}
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="status" header="Status" style="min-width: 110px">
|
||||
<template #body="{ data }">
|
||||
<Tag :value="STATUS_LABEL[data.status] || data.status || 'Agendado'" :severity="STATUS_SEVERITY[data.status] || 'info'" />
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
</div>
|
||||
|
||||
<LoadedPhraseBlock v-if="hasLoaded" />
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
|
||||
<!-- ── QUICK-STATS clicáveis ────────────────────── -->
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<div
|
||||
v-for="s in quickStats"
|
||||
:key="s.label"
|
||||
class="flex flex-col gap-0.5 px-4 py-2.5 rounded-md border flex-1 min-w-[80px] transition-[border-color,box-shadow,background] duration-150"
|
||||
:class="[
|
||||
s.filter !== null ? 'cursor-pointer select-none' : '',
|
||||
s.filter !== null && filtroTabela === s.filter
|
||||
? 'border-[var(--primary-color,#6366f1)] shadow-[0_0_0_3px_rgba(99,102,241,0.15)] bg-[var(--surface-card,#fff)]'
|
||||
: s.cls === 'qs-ok'
|
||||
? 'border-green-500/25 bg-green-500/5 hover:border-green-500/40'
|
||||
: s.cls === 'qs-danger'
|
||||
? 'border-red-500/25 bg-red-500/5 hover:border-red-500/40'
|
||||
: s.cls === 'qs-warn'
|
||||
? 'border-orange-500/25 bg-orange-500/5 hover:border-orange-500/40'
|
||||
: s.cls === 'qs-info'
|
||||
? 'border-sky-500/25 bg-sky-500/5 hover:border-sky-500/40'
|
||||
: 'border-[var(--surface-border,#e2e8f0)] bg-[var(--surface-card,#fff)] hover:border-indigo-300/50'
|
||||
]"
|
||||
@click="s.filter !== null ? toggleFiltroTabela(s.filter) : null"
|
||||
>
|
||||
<div class="text-[1.35rem] font-bold leading-none" :class="s.valCls">{{ s.value }}</div>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] opacity-75 whitespace-nowrap">{{ s.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chip de filtro ativo na tabela -->
|
||||
<div v-if="filtroTabela" class="flex items-center gap-2">
|
||||
<span class="text-[1rem] text-[var(--text-color-secondary)]">Filtrando por:</span>
|
||||
<span class="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-[0.75rem] font-semibold bg-[var(--primary-color,#6366f1)] text-white">
|
||||
{{ STATUS_LABEL[filtroTabela] || filtroTabela }}
|
||||
<button class="ml-0.5 opacity-70 hover:opacity-100" @click="filtroTabela = null">
|
||||
<i class="pi pi-times text-[0.6rem]" />
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- ── GRÁFICO ──────────────────────────────────── -->
|
||||
<div v-if="total > 0" class="rounded-md border border-[var(--surface-border,#e2e8f0)] bg-[var(--surface-card,#fff)] overflow-hidden">
|
||||
<div class="flex items-center justify-between px-4 py-3 border-b border-[var(--surface-border,#e2e8f0)]">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="pi pi-chart-bar text-[var(--text-color-secondary)] opacity-60" />
|
||||
<span class="font-semibold text-[1rem] text-[var(--text-color)]">
|
||||
Sessões por {{ selectedPeriod === 'week' ? 'semana' : 'mês' }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="text-[0.72rem] text-[var(--text-color-secondary)] opacity-60">{{ total }} sessão{{ total !== 1 ? 'ões' : '' }}</span>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<Chart type="bar" :data="chartData" :options="chartOptions" class="h-64" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── TABELA ───────────────────────────────────── -->
|
||||
<div class="rounded-md border border-[var(--surface-border,#e2e8f0)] bg-[var(--surface-card,#fff)] overflow-hidden">
|
||||
|
||||
<!-- Cabeçalho da seção -->
|
||||
<div class="flex items-center justify-between px-4 py-3 border-b border-[var(--surface-border,#e2e8f0)]">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="pi pi-table text-[var(--text-color-secondary)] opacity-60" />
|
||||
<span class="font-semibold text-[1rem] text-[var(--text-color)]">Sessões no período</span>
|
||||
<span
|
||||
v-if="filtroTabela"
|
||||
class="text-[0.72rem] text-[var(--text-color-secondary)] opacity-70"
|
||||
>(filtrado)</span>
|
||||
</div>
|
||||
<span class="inline-flex items-center justify-center min-w-[22px] h-[22px] px-1.5 rounded-full bg-[var(--primary-color,#6366f1)] text-white text-[1rem] font-bold">
|
||||
{{ sessionsFiltradas.length }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Empty state (sem dados no período) -->
|
||||
<div
|
||||
v-if="!sessions.length"
|
||||
class="flex flex-col items-center justify-center gap-3 py-14 px-6 text-center border-2 border-dashed border-[var(--surface-border,#e2e8f0)] mx-4 my-4 rounded-md bg-[var(--surface-ground,#f8fafc)]"
|
||||
>
|
||||
<div class="relative">
|
||||
<div class="grid place-items-center w-16 h-16 rounded-2xl bg-[var(--surface-card,#fff)] border border-[var(--surface-border,#e2e8f0)] shadow-sm text-[var(--text-color-secondary)]">
|
||||
<i class="pi pi-chart-bar text-3xl opacity-25" />
|
||||
</div>
|
||||
<div class="absolute -top-1.5 -right-1.5 w-6 h-6 rounded-full bg-[var(--surface-card,#fff)] border border-[var(--surface-border,#e2e8f0)] shadow-sm grid place-items-center">
|
||||
<i class="pi pi-times text-[0.58rem] text-[var(--text-color-secondary)] opacity-50" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold text-[0.9rem] text-[var(--text-color)] mb-0.5">Nenhuma sessão no período</div>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] max-w-xs">Tente selecionar um período diferente.</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2 mt-1 justify-center">
|
||||
<button
|
||||
v-for="p in PERIODS"
|
||||
:key="p.value"
|
||||
class="inline-flex items-center px-3 py-1 rounded-full text-[0.75rem] font-semibold border cursor-pointer transition-colors duration-150"
|
||||
:class="selectedPeriod === p.value
|
||||
? 'bg-[var(--primary-color,#6366f1)] border-[var(--primary-color,#6366f1)] text-white'
|
||||
: 'border-[var(--surface-border,#e2e8f0)] text-[var(--text-color-secondary)] hover:border-indigo-300'"
|
||||
@click="selectedPeriod = p.value"
|
||||
>
|
||||
{{ p.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty state (filtro sem resultado) -->
|
||||
<div
|
||||
v-else-if="!sessionsFiltradas.length"
|
||||
class="flex flex-col items-center gap-2 py-10 text-center text-[var(--text-color-secondary)]"
|
||||
>
|
||||
<i class="pi pi-filter-slash text-2xl opacity-30" />
|
||||
<div class="font-semibold text-[0.88rem]">Nenhuma sessão com este status</div>
|
||||
<Button label="Limpar filtro" icon="pi pi-times" severity="secondary" outlined size="small" class="rounded-full mt-1" @click="filtroTabela = null" />
|
||||
</div>
|
||||
|
||||
<!-- DataTable -->
|
||||
<DataTable
|
||||
v-else
|
||||
:value="sessionsFiltradas"
|
||||
:rows="20"
|
||||
paginator
|
||||
:rows-per-page-options="[10, 20, 50]"
|
||||
scrollable
|
||||
scroll-height="480px"
|
||||
class="rel-datatable"
|
||||
>
|
||||
<Column field="inicio_em" header="Data / Hora" :sortable="true" style="min-width: 140px">
|
||||
<template #body="{ data }">
|
||||
<span class="font-medium">{{ fmtDateTimeBR(data.inicio_em) }}</span>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column header="Paciente" style="min-width: 160px">
|
||||
<template #body="{ data }">{{ patientName(data) }}</template>
|
||||
</Column>
|
||||
|
||||
<Column header="Sessão" style="min-width: 160px">
|
||||
<template #body="{ data }">{{ sessionTitle(data) }}</template>
|
||||
</Column>
|
||||
|
||||
<Column field="modalidade" header="Modalidade" style="min-width: 110px">
|
||||
<template #body="{ data }">
|
||||
{{ data.modalidade === 'online' ? 'Online' : data.modalidade === 'presencial' ? 'Presencial' : data.modalidade || '—' }}
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="status" header="Status" style="min-width: 110px">
|
||||
<template #body="{ data }">
|
||||
<Tag
|
||||
:value="STATUS_LABEL[data.status] || data.status || 'Agendado'"
|
||||
:severity="STATUS_SEVERITY[data.status] || 'info'"
|
||||
/>
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
</div>
|
||||
|
||||
<LoadedPhraseBlock v-if="hasLoaded" />
|
||||
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.rel-datatable :deep(.p-datatable-table-container) { border-radius: 0; }
|
||||
.rel-datatable :deep(th) { background: var(--surface-ground) !important; font-size: 0.82rem; }
|
||||
.rel-datatable :deep(td) { font-size: 0.85rem; }
|
||||
</style>
|
||||
.rel-datatable :deep(.p-datatable-table-container) {
|
||||
border-radius: 0;
|
||||
}
|
||||
.rel-datatable :deep(th) {
|
||||
background: var(--surface-ground) !important;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
.rel-datatable :deep(td) {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user