Files
agenciapsilmno/src/layout/melissa/MelissaCompromissos.vue
T
Leonardo 48bf2726a5 Drawer mobile + footer colado + Menu nomeado + tenant ensureLoaded
Tres ajustes globais nas Melissa Pages com sidebar:

1) FOOTER "Limpar filtros" colado no bottom do drawer mobile

   Problema: o sticky bottom precisa que algum container parent
   tenha altura definida e overflow. No drawer, o `.xx-side` tinha
   `height: auto` — entao o footer ficava no fluxo natural (logo
   apos os cards) mesmo com pouco conteudo, em vez de empurrado pro
   bottom do drawer.

   Fix: `.xx-mobile-drawer__scroll .xx-side` ganha
   `flex: 1; min-height: 0; display: flex; flex-direction: column`
   pra ocupar altura disponivel; o `.xx-side__footer` ganha
   `margin: auto -12px -24px` (margin-top: auto empurra pro fim).
   Sticky bottom continua pro caso de scroll com muito conteudo.

   Aplicado em: Compromissos, Grupos, Tags, Medicos, Conversas,
   Recorrencias, Pacientes (caso especial — separa .mp-side de
   .mp-quick), Cadastros Recebidos, FinanceiroLancamentos.

2) DRAWER MOBILE adicionado em Notificacoes, Documentos e
   Relatorios (estavam com sidebar virando topo via max-height
   50vh — faltava o pattern oficial das demais Melissa Pages).

   Pattern aplicado:
   - Aside host com id="<prefix>-mobile-drawer-target" + Transition
     backdrop com fade
   - Botao "Menu <Secao>" no header (esquerda do titulo)
   - <Teleport :disabled="!isMobile"> envolvendo a sidebar
   - Script: drawerOpen + isMobile + matchMedia listener registrado
     no onMounted, removido no onBeforeUnmount
   - CSS completo: .xx-mobile-drawer (fixed, transform translateX),
     __scroll (overflow + padding), __backdrop (rgba 0.45 + blur),
     overrides quando teleportada (sidebar perde bg/border-right,
     footer vira sticky bottom com margin-top auto)

3) Botao "Menu" passa a ter sufixo da pagina:
   - "Menu Lancamentos" (FinanceiroLancamentos)
   - "Menu Notificacoes" (Notificacoes)
   - "Menu Documentos" (Documentos)
   - "Menu Relatorios" (Relatorios)
   - "Menu Agendamentos" (AgendamentosRecebidos — corrigido tambem)

4) Bug de "lista vazia ao carregar via URL direto":

   FinanceiroLancamentos e Relatorios usam composables que dependem
   de tenantStore.activeTenantId. Quando aberta direto via URL
   (sem navegar pelo menu), o tenantStore pode nao estar inicializado
   ainda — entao fetchRecords() / loadSessions() retornam vazio.

   Fix: adicionar `await tenantStore.ensureLoaded()` no onMounted
   antes do fetch. Ja era pattern usado em outras Melissa Pages
   (Compromissos, etc).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 10:40:07 -03:00

1921 lines
66 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup>
/*
* MelissaCompromissos — Tipos de compromissos determinados.
* Segue blueprint melissa-table-page-blueprint.md (DataTable + sidebar com
* stats e filtros coloridos, view toggle list/grade, subheader explicativo).
*
* Diferença vs. blueprint canônico (MelissaCadastrosRecebidos):
* - Row design preservado: o "Compromisso" é uma única coluna larga com
* color-stripe vertical + name + badges + descrição + meta inline,
* espelhando o card antigo. Coluna "Ações" frozen é alargada pra 140px
* pra caber os 3 controles (ToggleSwitch + Editar + Excluir).
* - Dois grupos de filtros na sidebar: Status (Ativos/Inativos) e
* Tipo (Nativos/Meus), cada um com botão "Limpar filtro" próprio.
* - Stats: Total / Ativos (verde) / Inativos (amber) / Tempo total.
*/
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue';
import { useToast } from 'primevue/usetoast';
import { supabase } from '@/lib/supabase/client';
import { useTenantStore } from '@/stores/tenantStore';
import DeterminedCommitmentDialog from '@/features/agenda/components/DeterminedCommitmentDialog.vue';
// DataTable/Column/Paginator/ToggleSwitch/Dialog: auto-importados via PrimeVueResolver.
const emit = defineEmits(['close']);
const toast = useToast();
const tenantStore = useTenantStore();
// ── Breakpoints + drawer ──────────────────────────────────
const drawerOpen = ref(false);
const isMobile = ref(false);
let _mqMobile = null;
function _onMqMobileChange(e) {
isMobile.value = e.matches;
if (!e.matches) drawerOpen.value = false;
}
function toggleDrawer() { drawerOpen.value = !drawerOpen.value; }
function fecharDrawer() { drawerOpen.value = false; }
// ── Estado ─────────────────────────────────────────────────
const loading = ref(false);
const saving = ref(false);
const commitments = ref([]);
const totalsByCommitmentId = ref({});
const busca = ref('');
const statusFilter = ref(''); // '' | 'active' | 'inactive'
const typeFilter = ref(''); // '' | 'native' | 'custom'
// ── Helpers de status ─────────────────────────────────────
function rowStatusClass(c) {
if (!c) return '';
return c.active ? 'is-ok' : 'is-warn';
}
// ── Stats ─────────────────────────────────────────────────
const stats = computed(() => {
const all = commitments.value;
const ativos = all.filter((c) => c.active).length;
const inativos = all.filter((c) => !c.active).length;
const totalMin = Object.values(totalsByCommitmentId.value).reduce((a, b) => a + b, 0);
return [
{ key: 'total', label: 'Total', value: all.length, cls: 'neutral' },
{ key: 'ativos', label: 'Ativos', value: ativos, cls: ativos > 0 ? 'ok' : 'neutral' },
{ key: 'inativos', label: 'Inativos', value: inativos, cls: inativos > 0 ? 'warn' : 'neutral' },
{ key: 'tempo', label: 'Tempo', value: formatMinutes(totalMin), cls: 'neutral' }
];
});
const STATUS_FILTER_OPTIONS = [
{ key: 'active', label: 'Ativos', icon: 'pi pi-check-circle' },
{ key: 'inactive', label: 'Inativos', icon: 'pi pi-pause-circle' }
];
const TYPE_FILTER_OPTIONS = [
{ key: 'native', label: 'Nativos', icon: 'pi pi-shield' },
{ key: 'custom', label: 'Meus', icon: 'pi pi-user' }
];
function toggleStatusFilter(s) {
statusFilter.value = statusFilter.value === s ? '' : s;
}
function toggleTypeFilter(t) {
typeFilter.value = typeFilter.value === t ? '' : t;
}
const hasActiveFilters = computed(() =>
!!(busca.value || statusFilter.value || typeFilter.value)
);
function clearAllFilters() {
busca.value = '';
statusFilter.value = '';
typeFilter.value = '';
}
// ── Filtragem ─────────────────────────────────────────────
const filtered = computed(() => {
let list = commitments.value;
if (statusFilter.value === 'active') list = list.filter((c) => !!c.active);
if (statusFilter.value === 'inactive') list = list.filter((c) => !c.active);
if (typeFilter.value === 'native') list = list.filter((c) => !!c.is_native);
if (typeFilter.value === 'custom') list = list.filter((c) => !c.is_native);
const q = String(busca.value || '').trim().toLowerCase();
if (q) {
list = list.filter((c) =>
String(c.name || '').toLowerCase().includes(q) ||
String(c.description || '').toLowerCase().includes(q)
);
}
return list;
});
// ── Paginação compartilhada (DataTable + grid) ────────────
const PAGE_SIZE_OPTIONS = [10, 20, 50, 100];
const rowsMC = ref(10);
const firstMC = ref(0);
function onPage(event) {
firstMC.value = event.first;
rowsMC.value = event.rows;
}
watch([busca, statusFilter, typeFilter], () => { firstMC.value = 0; });
const pagedItems = computed(() =>
filtered.value.slice(firstMC.value, firstMC.value + rowsMC.value)
);
// ── View mode (list / grid) ───────────────────────────────
const VIEW_MODE_KEY = 'mc.viewMode.v1';
const viewMode = ref('list');
try {
const saved = localStorage.getItem(VIEW_MODE_KEY);
if (saved === 'list' || saved === 'grid') viewMode.value = saved;
} catch (_) { /* localStorage indisponível — mantém default */ }
function setViewMode(m) {
if (m !== 'list' && m !== 'grid') return;
viewMode.value = m;
try { localStorage.setItem(VIEW_MODE_KEY, m); } catch (_) { /* noop */ }
}
// ── Helpers de tempo ──────────────────────────────────────
function formatMinutes(minutes) {
const m = Math.max(0, Number(minutes) || 0);
const h = Math.floor(m / 60);
const mm = m % 60;
if (h <= 0) return `${mm}m`;
return `${h}h${String(mm).padStart(2, '0')}`;
}
function getTotalMinutes(id) {
return Number(totalsByCommitmentId.value?.[id] ?? 0);
}
// ── Fetch ─────────────────────────────────────────────────
function getTenantId() {
return tenantStore.activeTenantId || tenantStore.tenantId || null;
}
async function fetchAll() {
const tenantId = getTenantId();
if (!tenantId) {
toast.add({ severity: 'warn', summary: 'Atenção', detail: 'Tenant inválido.', life: 3000 });
return;
}
loading.value = true;
try {
const { data: cData, error: cErr } = await supabase
.from('determined_commitments')
.select('id, tenant_id, is_native, native_key, is_locked, active, name, description, bg_color, text_color, created_at, updated_at')
.eq('tenant_id', tenantId)
.order('is_native', { ascending: false })
.order('created_at', { ascending: false });
if (cErr) throw cErr;
const ids = (cData || []).map((x) => x.id);
let fieldsByCommitmentId = {};
if (ids.length > 0) {
const { data: fData, error: fErr } = await supabase
.from('determined_commitment_fields')
.select('id, tenant_id, commitment_id, key, label, field_type, required, sort_order')
.eq('tenant_id', tenantId)
.in('commitment_id', ids)
.order('sort_order', { ascending: true });
if (fErr) throw fErr;
fieldsByCommitmentId = (fData || []).reduce((acc, row) => {
if (!acc[row.commitment_id]) acc[row.commitment_id] = [];
acc[row.commitment_id].push({
id: row.id, key: row.key, label: row.label,
type: row.field_type, required: !!row.required, sort_order: row.sort_order
});
return acc;
}, {});
}
const { data: lData, error: lErr } = await supabase
.from('commitment_time_logs').select('commitment_id, minutes').eq('tenant_id', tenantId);
if (lErr) throw lErr;
const totals = {};
for (const row of lData || []) {
const cid = row.commitment_id;
totals[cid] = (totals[cid] || 0) + (Number(row.minutes ?? 0) || 0);
}
totalsByCommitmentId.value = totals;
commitments.value = (cData || []).map((c) => ({
...c,
fields: fieldsByCommitmentId[c.id] || []
}));
} catch (e) {
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message || 'Falha ao carregar.', life: 4500 });
} finally {
loading.value = false;
}
}
onMounted(async () => {
if (typeof window !== 'undefined' && window.matchMedia) {
_mqMobile = window.matchMedia('(max-width: 1023px)');
isMobile.value = _mqMobile.matches;
_mqMobile.addEventListener('change', _onMqMobileChange);
}
if (typeof tenantStore.loadSessionAndTenant === 'function') {
await tenantStore.loadSessionAndTenant();
}
await fetchAll();
});
onBeforeUnmount(() => {
if (_mqMobile) _mqMobile.removeEventListener('change', _onMqMobileChange);
});
// ── CRUD ──────────────────────────────────────────────────
const dlgOpen = ref(false);
const dlgMode = ref('create');
const editing = ref(null);
function openCreate() {
dlgMode.value = 'create';
editing.value = null;
dlgOpen.value = true;
}
function openEdit(c) {
dlgMode.value = 'edit';
editing.value = JSON.parse(JSON.stringify(c));
dlgOpen.value = true;
}
function onRowClick(event) {
if (event?.data) openEdit(event.data);
}
function isActiveLocked(c) { return !!c.is_locked; }
function isDeleteLocked(c) { return !!c.is_native; }
async function onToggleActive(c) {
if (isActiveLocked(c)) return;
const tenantId = getTenantId();
if (!tenantId) return;
saving.value = true;
try {
const { error } = await supabase.from('determined_commitments').update({ active: !!c.active }).eq('tenant_id', tenantId).eq('id', c.id);
if (error) throw error;
toast.add({ severity: 'success', summary: 'Atualizado', detail: `"${c.name}" ${c.active ? 'ativo' : 'inativo'}.`, life: 2200 });
} catch (e) {
c.active = !c.active;
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message || 'Falha ao atualizar.', life: 4500 });
} finally {
saving.value = false;
}
}
async function onSave(payload) {
const tenantId = getTenantId();
if (!tenantId) return;
saving.value = true;
try {
if (dlgMode.value === 'create') {
const { data: newC, error: cErr } = await supabase
.from('determined_commitments')
.insert({
tenant_id: tenantId,
is_native: false,
native_key: null,
is_locked: false,
active: !!payload.active,
name: payload.name,
description: payload.description,
bg_color: payload.bg_color || null,
text_color: payload.text_color || null
})
.select('id').single();
if (cErr) throw cErr;
const fields = Array.isArray(payload.fields) ? payload.fields : [];
if (fields.length > 0) {
const { error: fErr } = await supabase.from('determined_commitment_fields').insert(
fields.map((f, idx) => ({
tenant_id: tenantId,
commitment_id: newC.id,
key: f.key,
label: f.label,
field_type: f.type,
required: !!f.required,
sort_order: f.sort_order ?? idx
}))
);
if (fErr) throw fErr;
}
toast.add({ severity: 'success', summary: 'Criado', detail: 'Compromisso criado.', life: 2200 });
} else if (editing.value?.id) {
const cid = editing.value.id;
const { error: uErr } = await supabase.from('determined_commitments')
.update({
active: !!payload.active,
name: payload.name,
description: payload.description,
bg_color: payload.bg_color || null,
text_color: payload.text_color || null
})
.eq('tenant_id', tenantId).eq('id', cid);
if (uErr) throw uErr;
const { error: dErr } = await supabase.from('determined_commitment_fields')
.delete().eq('tenant_id', tenantId).eq('commitment_id', cid);
if (dErr) throw dErr;
const fields = Array.isArray(payload.fields) ? payload.fields : [];
if (fields.length > 0) {
const { error: fErr } = await supabase.from('determined_commitment_fields').insert(
fields.map((f, idx) => ({
tenant_id: tenantId,
commitment_id: cid,
key: f.key,
label: f.label,
field_type: f.type,
required: !!f.required,
sort_order: f.sort_order ?? idx
}))
);
if (fErr) throw fErr;
}
toast.add({ severity: 'success', summary: 'Atualizado', detail: 'Compromisso atualizado.', life: 2200 });
}
dlgOpen.value = false;
await fetchAll();
} catch (e) {
const msg = e?.message || '';
const detail = e?.code === '23505' || /duplicate key value/i.test(msg)
? 'Já existe um compromisso com esse nome. Escolha outro.'
: msg || 'Falha ao salvar.';
toast.add({ severity: 'error', summary: 'Erro', detail, life: 4500 });
} finally {
saving.value = false;
}
}
function confirmDelete(c) {
if (isDeleteLocked(c)) return;
if (!window.confirm(`Excluir "${c.name}"? Essa ação não pode ser desfeita.`)) return;
onDelete(c);
}
async function onDelete(c) {
if (isDeleteLocked(c)) return;
const tenantId = getTenantId();
if (!tenantId) return;
saving.value = true;
try {
const { error: fErr } = await supabase.from('determined_commitment_fields')
.delete().eq('tenant_id', tenantId).eq('commitment_id', c.id);
if (fErr) throw fErr;
const { error: lErr } = await supabase.from('commitment_time_logs')
.delete().eq('tenant_id', tenantId).eq('commitment_id', c.id);
if (lErr) throw lErr;
const { data: delRows, error: dErr } = await supabase.from('determined_commitments')
.delete().eq('tenant_id', tenantId).eq('id', c.id).eq('is_native', false).select('id');
if (dErr) throw dErr;
if (!delRows?.length) throw new Error('DELETE bloqueado por RLS.');
toast.add({ severity: 'success', summary: 'Excluído', detail: 'Compromisso removido.', life: 2200 });
dlgOpen.value = false;
await fetchAll();
} catch (e) {
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message || 'Falha ao excluir.', life: 4500 });
} finally {
saving.value = false;
}
}
</script>
<template>
<aside
class="mc-mobile-drawer"
:class="{ 'is-open': drawerOpen }"
v-show="isMobile"
aria-label="Estatísticas e filtros"
>
<div id="mc-mobile-drawer-target" class="mc-mobile-drawer__scroll" />
</aside>
<Transition name="mc-drawer-fade">
<div
v-if="isMobile && drawerOpen"
class="mc-mobile-drawer__backdrop"
@click="fecharDrawer"
/>
</Transition>
<section class="mc-page">
<header class="mc-page__head">
<button
class="mc-menu-btn mc-menu-btn--mobile-only"
v-tooltip.bottom="'Estatísticas & filtros'"
@click="toggleDrawer"
>
<i class="pi pi-bars" />
<span>Menu Compromissos</span>
</button>
<div class="mc-page__title">
<i class="pi pi-list mc-page__title-icon" />
<span>Compromissos</span>
<span class="mc-page__count">{{ filtered.length }}</span>
</div>
<div class="mc-page__actions">
<button
class="mc-act-btn mc-act-btn--primary"
v-tooltip.bottom="'Novo compromisso'"
:disabled="loading"
@click="openCreate()"
>
<i :class="saving ? 'pi pi-spin pi-spinner' : 'pi pi-plus'" />
<span>Novo</span>
</button>
<button
class="mc-head-btn"
v-tooltip.bottom="'Recarregar'"
:disabled="loading"
@click="fetchAll"
>
<i :class="loading ? 'pi pi-spin pi-spinner' : 'pi pi-refresh'" />
</button>
<button class="mc-close" v-tooltip.bottom="'Voltar (Esc)'" @click="emit('close')">
<i class="pi pi-times" />
</button>
</div>
</header>
<!-- Subheader explicativo (blueprint §9) -->
<div class="mc-subheader">
<i class="pi pi-info-circle mc-subheader__icon" />
<span class="mc-subheader__text">
Modelos de compromissos que aparecem na agenda além das sessões.
<strong>Crie tipos próprios</strong> com campos personalizados ou
<strong>edite/desative</strong> os nativos disponíveis pra todos os usuários.
</span>
</div>
<div class="mc-body">
<Teleport to="#mc-mobile-drawer-target" :disabled="!isMobile">
<aside class="mc-side">
<!-- Conteúdo scrollável: cards de estatísticas e filtros.
O footer com "Limpar filtros" fica fora desse scroll
(flex-shrink: 0 ancorado no bottom da .mc-side). -->
<div class="mc-side__scroll">
<!-- Stats -->
<div class="mc-w mc-w--side">
<div class="mc-w__head">
<span class="mc-w__title"><i class="pi pi-chart-bar" /> Estatísticas</span>
</div>
<div class="mc-stats">
<div
v-for="s in stats"
:key="s.key"
class="mc-stat"
:class="`is-${s.cls}`"
>
<div class="mc-stat__val">{{ s.value }}</div>
<div class="mc-stat__lbl">{{ s.label }}</div>
</div>
</div>
</div>
<!-- Filtro de status -->
<div class="mc-w mc-w--side">
<div class="mc-w__head">
<span class="mc-w__title"><i class="pi pi-filter" /> Status</span>
<button
v-if="statusFilter"
class="mc-side__clear-inline"
v-tooltip.top="'Limpar filtro de status'"
aria-label="Limpar filtro de status"
@click="statusFilter = ''"
>
<i class="pi pi-times" />
</button>
</div>
<div class="mc-side__list">
<button
v-for="o in STATUS_FILTER_OPTIONS"
:key="o.key"
class="mc-side__item"
:class="[`is-status-${o.key}`, { 'is-active': statusFilter === o.key }]"
@click="toggleStatusFilter(o.key)"
>
<i :class="o.icon" />
<span>{{ o.label }}</span>
</button>
</div>
</div>
<!-- Filtro de tipo -->
<div class="mc-w mc-w--side">
<div class="mc-w__head">
<span class="mc-w__title"><i class="pi pi-tag" /> Tipo</span>
<button
v-if="typeFilter"
class="mc-side__clear-inline"
v-tooltip.top="'Limpar filtro de tipo'"
aria-label="Limpar filtro de tipo"
@click="typeFilter = ''"
>
<i class="pi pi-times" />
</button>
</div>
<div class="mc-side__list">
<button
v-for="o in TYPE_FILTER_OPTIONS"
:key="o.key"
class="mc-side__item"
:class="[`is-type-${o.key}`, { 'is-active': typeFilter === o.key }]"
@click="toggleTypeFilter(o.key)"
>
<i :class="o.icon" />
<span>{{ o.label }}</span>
</button>
</div>
</div>
</div>
<!-- Footer fixo no bottom da sidebar (fora do scroll).
Botão "Limpar filtros" aparece se algum filtro
estiver ativo (busca, status, ou tipo). Em desktop
fica ancorado no fundo da .mc-side via flex; em
mobile teleportado fica sticky no bottom do drawer. -->
<Transition name="mc-clear">
<div v-if="hasActiveFilters" class="mc-side__footer">
<button class="mc-side__clear-all" @click="clearAllFilters">
<i class="pi pi-filter-slash" />
<span>Limpar filtros</span>
</button>
</div>
</Transition>
</aside>
</Teleport>
<div class="mc-main">
<!-- Toolbar (busca + view toggle) -->
<div class="mc-toolbar">
<div class="mc-search">
<i class="pi pi-search mc-search__icon" />
<input
v-model="busca"
type="text"
placeholder="Buscar por nome ou descrição…"
class="mc-search__input"
/>
<button
v-if="busca"
class="mc-search__clear"
v-tooltip.bottom="'Limpar busca'"
@click="busca = ''"
>
<i class="pi pi-times" />
</button>
</div>
<div class="mc-view-toggle" role="group" aria-label="Visualização">
<button
class="mc-view-toggle__btn"
:class="{ 'is-active': viewMode === 'list' }"
v-tooltip.bottom="'Lista'"
aria-label="Lista"
@click="setViewMode('list')"
>
<i class="pi pi-list" />
</button>
<button
class="mc-view-toggle__btn"
:class="{ 'is-active': viewMode === 'grid' }"
v-tooltip.bottom="'Grade'"
aria-label="Grade"
@click="setViewMode('grid')"
>
<i class="pi pi-th-large" />
</button>
</div>
</div>
<!-- View Lista (DataTable) row design preservado:
coluna "Compromisso" larga renderiza color-stripe + name +
badges + descrição + meta inline (mesmo conteúdo do card
antigo); coluna "Ações" frozen 140px com 3 controles. -->
<DataTable
v-if="viewMode === 'list'"
:value="filtered"
:loading="loading"
dataKey="id"
paginator
:rows="rowsMC"
:first="firstMC"
:rowsPerPageOptions="PAGE_SIZE_OPTIONS"
paginatorTemplate="RowsPerPageDropdown FirstPageLink PrevPageLink CurrentPageReport NextPageLink LastPageLink"
currentPageReportTemplate="{first}{last} de {totalRecords}"
:rowClass="rowStatusClass"
selectionMode="single"
scrollable
scrollHeight="flex"
tableStyle="min-width: 720px"
class="mc-table"
@row-click="onRowClick"
@page="onPage"
>
<Column header="Compromisso" style="min-width: 320px">
<template #body="{ data }">
<div class="mc-row__commit" :class="{ 'is-inactive': !data.active }">
<span
class="mc-row__color"
:style="{ background: data.bg_color ? `#${data.bg_color}` : 'var(--m-bg-soft-hover)' }"
/>
<div class="mc-row__main">
<div class="mc-row__name-row">
<span class="mc-row__name">{{ data.name }}</span>
<span v-if="data.is_native" class="mc-row__badge mc-row__badge--native">
<i class="pi pi-shield" /> Nativo
</span>
<span v-if="!data.active" class="mc-row__badge mc-row__badge--off">Inativo</span>
</div>
<div v-if="data.description" class="mc-row__desc">{{ data.description }}</div>
</div>
</div>
</template>
</Column>
<Column header="Atividade" style="width: 220px; min-width: 200px">
<template #body="{ data }">
<div class="mc-row__meta">
<span class="mc-row__meta-item" v-tooltip.top="'Tempo total registrado'">
<i class="pi pi-clock" />
{{ formatMinutes(getTotalMinutes(data.id)) }}
</span>
<span class="mc-row__meta-item" v-tooltip.top="'Campos extras'">
<i class="pi pi-list" />
{{ (data.fields || []).length }}
{{ (data.fields || []).length === 1 ? 'campo' : 'campos' }}
</span>
</div>
</template>
</Column>
<Column
header=""
:style="{ width: '140px', maxWidth: '140px', minWidth: '140px' }"
frozen
alignFrozen="right"
class="mc-col-acoes"
>
<template #body="{ data }">
<div class="mc-row__actions" @click.stop>
<ToggleSwitch
v-model="data.active"
:disabled="isActiveLocked(data) || saving"
@change="onToggleActive(data)"
v-tooltip.left="isActiveLocked(data) ? 'Bloqueado' : (data.active ? 'Desativar' : 'Ativar')"
/>
<button
class="mc-row__btn"
v-tooltip.left="'Editar'"
aria-label="Editar"
@click.stop="openEdit(data)"
>
<i class="pi pi-pencil" />
</button>
<button
class="mc-row__btn mc-row__btn--danger"
v-tooltip.left="isDeleteLocked(data) ? 'Não pode excluir nativos' : 'Excluir'"
aria-label="Excluir"
:disabled="isDeleteLocked(data)"
@click.stop="confirmDelete(data)"
>
<i class="pi pi-trash" />
</button>
</div>
</template>
</Column>
<template #empty>
<div class="mc-empty">
<i class="pi pi-list mc-empty__icon" />
<div class="mc-empty__title">Nenhum compromisso encontrado</div>
<div class="mc-empty__hint">
<template v-if="busca || statusFilter || typeFilter">
Ajuste os filtros pra ver mais resultados.
</template>
<template v-else>
Crie seu primeiro tipo de compromisso.
</template>
</div>
<button class="mc-act-btn mc-act-btn--primary mc-empty__btn" @click="openCreate()">
<i class="pi pi-plus" />
<span>Novo compromisso</span>
</button>
</div>
</template>
<template #loading>
<div class="mc-table__loading">
<i class="pi pi-spin pi-spinner" />
<span>Carregando compromissos</span>
</div>
</template>
</DataTable>
<!-- View Grade cards num CSS grid; mesmo conteúdo do row
preservado em formato vertical. Paginator standalone
compartilha rowsMC/firstMC com a list view. -->
<div v-else-if="viewMode === 'grid'" class="mc-grid-wrap">
<div v-if="loading && filtered.length === 0" class="mc-table__loading mc-grid__loading">
<i class="pi pi-spin pi-spinner" />
<span>Carregando compromissos</span>
</div>
<div v-else-if="filtered.length === 0" class="mc-empty">
<i class="pi pi-list mc-empty__icon" />
<div class="mc-empty__title">Nenhum compromisso encontrado</div>
<div class="mc-empty__hint">
<template v-if="busca || statusFilter || typeFilter">
Ajuste os filtros pra ver mais resultados.
</template>
<template v-else>
Crie seu primeiro tipo de compromisso.
</template>
</div>
</div>
<div v-else class="mc-grid">
<div
v-for="c in pagedItems"
:key="c.id"
class="mc-grid__card"
:class="[rowStatusClass(c), { 'is-inactive': !c.active }]"
role="button"
tabindex="0"
@click="openEdit(c)"
@keydown.enter.prevent="openEdit(c)"
@keydown.space.prevent="openEdit(c)"
>
<div class="mc-grid__top">
<span
class="mc-grid__color"
:style="{ background: c.bg_color ? `#${c.bg_color}` : 'var(--m-bg-soft-hover)' }"
/>
<div class="mc-grid__top-right" @click.stop>
<ToggleSwitch
v-model="c.active"
:disabled="isActiveLocked(c) || saving"
@change="onToggleActive(c)"
v-tooltip.left="isActiveLocked(c) ? 'Bloqueado' : (c.active ? 'Desativar' : 'Ativar')"
/>
</div>
</div>
<div class="mc-grid__name-row">
<span class="mc-grid__name">{{ c.name }}</span>
<span v-if="c.is_native" class="mc-row__badge mc-row__badge--native">
<i class="pi pi-shield" /> Nativo
</span>
<span v-if="!c.active" class="mc-row__badge mc-row__badge--off">Inativo</span>
</div>
<div v-if="c.description" class="mc-grid__desc">{{ c.description }}</div>
<div class="mc-grid__meta">
<span class="mc-row__meta-item" v-tooltip.top="'Tempo total registrado'">
<i class="pi pi-clock" />
{{ formatMinutes(getTotalMinutes(c.id)) }}
</span>
<span class="mc-row__meta-item" v-tooltip.top="'Campos extras'">
<i class="pi pi-list" />
{{ (c.fields || []).length }}
{{ (c.fields || []).length === 1 ? 'campo' : 'campos' }}
</span>
</div>
<div class="mc-grid__footer" @click.stop>
<button
class="mc-row__btn"
v-tooltip.top="'Editar'"
aria-label="Editar"
@click.stop="openEdit(c)"
>
<i class="pi pi-pencil" />
</button>
<button
class="mc-row__btn mc-row__btn--danger"
v-tooltip.top="isDeleteLocked(c) ? 'Não pode excluir nativos' : 'Excluir'"
aria-label="Excluir"
:disabled="isDeleteLocked(c)"
@click.stop="confirmDelete(c)"
>
<i class="pi pi-trash" />
</button>
</div>
</div>
</div>
<Paginator
v-if="filtered.length > 0"
class="mc-paginator"
:rows="rowsMC"
:totalRecords="filtered.length"
:first="firstMC"
:rowsPerPageOptions="PAGE_SIZE_OPTIONS"
template="RowsPerPageDropdown FirstPageLink PrevPageLink CurrentPageReport NextPageLink LastPageLink"
currentPageReportTemplate="{first}{last} de {totalRecords}"
@page="onPage"
/>
</div>
</div>
</div>
<!-- Dialog de criar/editar reutiliza o componente existente -->
<DeterminedCommitmentDialog
v-model="dlgOpen"
:mode="dlgMode"
:commitment="editing"
:saving="saving"
@save="onSave"
@delete="onDelete"
/>
</section>
</template>
<style scoped>
/* ─── Page chrome (espelha MelissaCadastrosRecebidos) ─── */
.mc-page {
position: absolute;
inset: 6px 6px calc(var(--m-dock-h, 76px) + 6px) 6px;
z-index: 40;
display: flex;
flex-direction: column;
background: var(--m-bg-medium);
backdrop-filter: blur(32px) saturate(160%);
-webkit-backdrop-filter: blur(32px) saturate(160%);
border: 1px solid var(--m-border);
border-radius: 18px;
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.4);
overflow: hidden;
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
color: var(--m-text);
animation: mc-page-enter 240ms cubic-bezier(0.2, 0.7, 0.3, 1);
}
@keyframes mc-page-enter {
from { opacity: 0; transform: scale(0.985); }
to { opacity: 1; transform: scale(1); }
}
.mc-page__head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 18px;
border-bottom: 1px solid var(--m-border);
flex-shrink: 0;
gap: 10px;
}
.mc-page__title {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
gap: 10px;
font-size: 1rem;
font-weight: 500;
}
.mc-page__title-icon {
color: var(--p-primary-color);
font-size: 1.05rem;
}
.mc-page__title > span:not(.mc-page__count) {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mc-page__count {
font-size: 0.7rem;
font-weight: 600;
color: var(--m-accent);
background: var(--m-accent-soft);
border: 1px solid color-mix(in srgb, var(--m-accent) 35%, transparent);
padding: 2px 8px;
border-radius: 999px;
}
.mc-page__actions { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
.mc-close, .mc-head-btn {
width: 32px; height: 32px;
display: grid; place-items: center;
background: var(--m-bg-soft);
border: 1px solid var(--m-border);
color: var(--m-text);
border-radius: 9px;
cursor: pointer;
font-family: inherit;
transition: background-color 140ms ease;
}
.mc-close:hover, .mc-head-btn:hover { background: var(--m-bg-soft-hover); }
.mc-head-btn > i { font-size: 0.85rem; }
.mc-act-btn {
display: inline-flex;
align-items: center;
gap: 6px;
height: 32px;
padding: 0 12px;
border-radius: 9px;
cursor: pointer;
font-family: inherit;
font-size: 0.78rem;
font-weight: 600;
transition: background-color 140ms ease, transform 140ms ease;
background: var(--m-bg-soft);
border: 1px solid var(--m-border);
color: var(--m-text);
}
.mc-act-btn--primary {
background: var(--m-accent);
border-color: var(--m-accent);
color: white;
}
.mc-act-btn--primary:hover {
background: color-mix(in srgb, var(--m-accent) 88%, white);
transform: translateY(-1px);
}
.mc-act-btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
.mc-act-btn > i { font-size: 0.78rem; }
.mc-menu-btn {
display: none;
height: 32px;
align-items: center;
gap: 6px;
flex-shrink: 0;
background: var(--m-accent);
border: 1px solid var(--m-accent);
color: white;
padding: 0 11px;
border-radius: 9px;
cursor: pointer;
font-family: inherit;
font-size: 0.78rem;
font-weight: 600;
transition: background-color 140ms ease, transform 140ms ease;
}
.mc-menu-btn:hover {
background: color-mix(in srgb, var(--m-accent) 88%, white);
transform: translateY(-1px);
}
/* Subheader (blueprint §9) */
.mc-subheader {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 10px 18px;
border-bottom: 1px solid var(--m-border);
background: var(--m-bg-soft);
font-size: 0.78rem;
color: var(--m-text-muted);
line-height: 1.45;
flex-shrink: 0;
}
.mc-subheader__icon {
color: var(--p-primary-color);
font-size: 0.92rem;
flex-shrink: 0;
margin-top: 1px;
}
.mc-subheader__text { flex: 1; min-width: 0; }
.mc-subheader__text strong { color: var(--m-text); font-weight: 600; }
.mc-body {
flex: 1;
display: flex;
min-height: 0;
gap: 0;
padding: 0;
}
/* ─── Sidebar ─── */
/* Layout flex column com 2 zonas: __scroll (cards, scrollável) e
__footer (Limpar filtros, fixo no bottom). overflow: hidden no
container externo evita que o scroll vaze pra fora. */
.mc-side {
width: 280px;
flex-shrink: 0;
display: flex;
flex-direction: column;
background: var(--m-bg-soft);
border-right: 1px solid var(--m-border);
overflow: hidden;
}
.mc-side__scroll {
flex: 1;
min-height: 0;
overflow-y: auto;
display: flex;
flex-direction: column;
}
.mc-side__scroll::-webkit-scrollbar { width: 5px; }
.mc-side__scroll::-webkit-scrollbar-thumb {
background: var(--m-border-strong);
border-radius: 3px;
}
/* Footer da sidebar — sempre ancorado no bottom (fora do scroll).
Bg um pouco mais opaco que --m-bg-soft pra "ler" como faixa fixa
separada. Border-top marca o limite com a área scrollável acima. */
.mc-side__footer {
flex-shrink: 0;
padding: 12px;
background: var(--m-bg-soft);
border-top: 1px solid var(--m-border);
}
.mc-side__clear-all {
width: 100%;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 9px 12px;
background: var(--m-bg-medium);
border: 1px solid var(--m-border);
color: var(--m-text);
border-radius: 10px;
cursor: pointer;
font-family: inherit;
font-size: 0.78rem;
font-weight: 600;
transition: background-color 140ms ease, border-color 140ms ease, color 140ms ease;
}
.mc-side__clear-all:hover {
background: var(--m-bg-soft-hover);
border-color: var(--m-border-strong);
color: var(--m-text);
}
.mc-side__clear-all > i {
font-size: 0.78rem;
color: var(--m-text-muted);
transition: color 140ms ease;
}
.mc-side__clear-all:hover > i { color: var(--m-text); }
.mc-w {
background: var(--m-bg-medium);
border: 1px solid var(--m-border);
border-radius: 12px;
padding: 12px;
}
.mc-w--side {
margin: 12px 12px 0;
flex-shrink: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
}
.mc-w--side:last-of-type { margin-bottom: 12px; }
.mc-w__head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.mc-w__title {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--m-text-muted);
font-weight: 600;
}
.mc-w__title > i { color: var(--m-text-muted); font-size: 0.7rem; }
.mc-w__count {
font-size: 0.65rem;
font-weight: 600;
color: var(--m-accent);
background: var(--m-accent-soft);
border: 1px solid color-mix(in srgb, var(--m-accent) 35%, transparent);
padding: 1px 7px;
border-radius: 999px;
}
/* Botão X inline ao lado do título do filter card — limpa o filtro
individual (espelha .mp-side__clear do MelissaPacientes). Cor vermelha
pra indicar ação destrutiva (remover filtro ativo). Aparece apenas
quando o filtro correspondente está ativo (v-if). */
.mc-side__clear-inline {
width: 18px;
height: 18px;
display: grid;
place-items: center;
background: transparent;
border: 1px solid color-mix(in srgb, rgb(220, 38, 38) 30%, var(--m-border));
color: rgb(220, 38, 38);
border-radius: 4px;
cursor: pointer;
font-family: inherit;
transition: background-color 140ms ease, border-color 140ms ease;
}
.mc-side__clear-inline:hover {
background: rgba(220, 38, 38, 0.10);
border-color: rgba(220, 38, 38, 0.55);
}
.mc-side__clear-inline > i { font-size: 0.6rem; }
.mc-stats {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 6px;
}
.mc-stat {
background: var(--m-bg-soft);
border: 1px solid var(--m-border);
border-radius: 10px;
padding: 8px 10px;
}
.mc-stat__val {
font-size: 1.1rem;
font-weight: 600;
line-height: 1.1;
}
.mc-stat__lbl {
font-size: 0.65rem;
color: var(--m-text-muted);
margin-top: 4px;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.mc-stat.is-ok .mc-stat__val { color: rgb(22, 163, 74); } /* green-600 */
.mc-stat.is-warn .mc-stat__val { color: rgb(217, 119, 6); } /* amber-600 */
/* ─── Filtros (blueprint §8) ─── */
.mc-side__list {
display: flex;
flex-direction: column;
gap: 4px;
}
.mc-side__item {
width: 100%;
display: flex;
align-items: center;
gap: 10px;
padding: 8px 10px;
background: transparent;
border: 1px solid transparent;
color: var(--m-text);
border-radius: 10px;
cursor: pointer;
font-family: inherit;
font-size: 0.82rem;
text-align: left;
transition: background-color 140ms ease, border-color 140ms ease, box-shadow 140ms ease;
}
.mc-side__item > i {
font-size: 0.78rem;
width: 14px;
text-align: center;
}
/* Status: Ativos = green-600, Inativos = amber-600 */
.mc-side__item.is-status-active {
background: rgba(22, 163, 74, 0.05);
border-color: rgba(22, 163, 74, 0.18);
}
.mc-side__item.is-status-active > i { color: rgb(22, 163, 74); }
.mc-side__item.is-status-active:hover {
background: rgba(22, 163, 74, 0.10);
border-color: rgba(22, 163, 74, 0.30);
}
.mc-side__item.is-active.is-status-active {
background: rgba(22, 163, 74, 0.16);
border-color: rgba(22, 163, 74, 0.55);
box-shadow: 0 0 0 1px rgba(22, 163, 74, 0.35);
}
.mc-side__item.is-status-inactive {
background: rgba(217, 119, 6, 0.05);
border-color: rgba(217, 119, 6, 0.18);
}
.mc-side__item.is-status-inactive > i { color: rgb(217, 119, 6); }
.mc-side__item.is-status-inactive:hover {
background: rgba(217, 119, 6, 0.10);
border-color: rgba(217, 119, 6, 0.30);
}
.mc-side__item.is-active.is-status-inactive {
background: rgba(217, 119, 6, 0.16);
border-color: rgba(217, 119, 6, 0.55);
box-shadow: 0 0 0 1px rgba(217, 119, 6, 0.35);
}
/* Tipo: Nativos = blue-600 (info), Meus = primary-tinted */
.mc-side__item.is-type-native {
background: rgba(37, 99, 235, 0.05);
border-color: rgba(37, 99, 235, 0.18);
}
.mc-side__item.is-type-native > i { color: rgb(37, 99, 235); }
.mc-side__item.is-type-native:hover {
background: rgba(37, 99, 235, 0.10);
border-color: rgba(37, 99, 235, 0.30);
}
.mc-side__item.is-active.is-type-native {
background: rgba(37, 99, 235, 0.16);
border-color: rgba(37, 99, 235, 0.55);
box-shadow: 0 0 0 1px rgba(37, 99, 235, 0.35);
}
.mc-side__item.is-type-custom {
background: color-mix(in srgb, var(--m-accent) 5%, transparent);
border-color: color-mix(in srgb, var(--m-accent) 18%, transparent);
}
.mc-side__item.is-type-custom > i { color: var(--m-accent); }
.mc-side__item.is-type-custom:hover {
background: color-mix(in srgb, var(--m-accent) 10%, transparent);
border-color: color-mix(in srgb, var(--m-accent) 30%, transparent);
}
.mc-side__item.is-active.is-type-custom {
background: color-mix(in srgb, var(--m-accent) 16%, transparent);
border-color: color-mix(in srgb, var(--m-accent) 55%, transparent);
box-shadow: 0 0 0 1px color-mix(in srgb, var(--m-accent) 35%, transparent);
}
/* Transition do footer "Limpar filtros" — fade + collapse vertical
pra entrada/saída suave quando o user ativa/desativa o último filtro. */
.mc-clear-enter-active,
.mc-clear-leave-active {
transition: opacity 220ms ease, transform 220ms ease, max-height 240ms ease;
overflow: hidden;
}
.mc-clear-enter-from,
.mc-clear-leave-to {
opacity: 0;
transform: translateY(6px);
max-height: 0;
}
.mc-clear-enter-to,
.mc-clear-leave-from {
opacity: 1;
transform: translateY(0);
max-height: 80px;
}
/* ─── Main column ─── */
.mc-main {
flex: 1;
min-width: 0;
min-height: 0;
display: flex;
flex-direction: column;
padding: 12px;
gap: 10px;
}
.mc-toolbar {
flex-shrink: 0;
display: flex;
align-items: center;
gap: 10px;
}
.mc-search {
position: relative;
flex: 1;
min-width: 0;
display: flex;
align-items: center;
}
.mc-search__icon {
position: absolute;
left: 12px;
color: var(--m-text-muted);
font-size: 0.78rem;
pointer-events: none;
}
.mc-search__input {
width: 100%;
background: var(--m-bg-medium);
border: 1px solid var(--m-border);
color: var(--m-text);
padding: 9px 36px 9px 34px;
border-radius: 10px;
font-size: 0.85rem;
font-family: inherit;
outline: none;
transition: background-color 140ms ease, border-color 140ms ease;
}
.mc-search__input::placeholder { color: var(--m-text-faint); }
.mc-search__input:focus {
border-color: var(--m-border-strong);
background: var(--m-bg-medium);
}
.mc-search__clear {
position: absolute;
right: 8px;
width: 22px;
height: 22px;
display: grid;
place-items: center;
background: transparent;
border: none;
color: var(--m-text-muted);
border-radius: 6px;
cursor: pointer;
font-family: inherit;
transition: background-color 140ms ease, color 140ms ease;
}
.mc-search__clear:hover {
background: var(--m-bg-soft-hover);
color: var(--m-text);
}
.mc-search__clear > i { font-size: 0.7rem; }
.mc-view-toggle {
flex-shrink: 0;
display: inline-flex;
background: var(--m-bg-medium);
border: 1px solid var(--m-border);
border-radius: 10px;
padding: 2px;
gap: 2px;
}
.mc-view-toggle__btn {
width: 32px;
height: 32px;
display: grid;
place-items: center;
background: transparent;
border: none;
color: var(--m-text-muted);
border-radius: 8px;
cursor: pointer;
font-family: inherit;
transition: background-color 140ms ease, color 140ms ease;
}
.mc-view-toggle__btn:hover {
background: var(--m-bg-soft-hover);
color: var(--m-text);
}
.mc-view-toggle__btn.is-active {
background: var(--m-accent-soft);
color: var(--m-accent);
}
.mc-view-toggle__btn > i { font-size: 0.85rem; }
/* ─── DataTable ─── */
.mc-table {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.mc-table :deep(.p-datatable) {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
background: transparent;
border: 1px solid var(--m-border);
border-radius: 10px;
overflow: hidden;
}
.mc-table :deep(.p-datatable-table-container) {
flex: 1;
min-height: 0;
background: transparent;
}
.mc-table :deep(.p-datatable-thead),
.mc-table :deep(.p-datatable-thead > tr) {
background: transparent !important;
}
.mc-table :deep(.p-datatable-thead > tr > th) {
background: var(--p-content-background) !important;
color: var(--m-text);
font-size: 0.78rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
padding: 10px 14px;
border-bottom: 1px solid var(--m-border);
}
.mc-table :deep(.p-datatable-tbody > tr) {
background: transparent;
color: var(--m-text);
cursor: pointer;
transition: background-color 140ms ease;
border-left: 3px solid var(--m-border);
}
.mc-table :deep(.p-datatable-tbody > tr > td) {
padding: 10px 14px;
border-bottom: 1px solid var(--m-border);
background: transparent;
vertical-align: middle;
}
.mc-table :deep(.p-datatable-tbody > tr:hover) {
background: var(--m-bg-soft-hover);
}
.mc-table :deep(.p-datatable-tbody > tr.p-datatable-row-selected) {
background: var(--m-accent-soft);
}
/* Border-left por status */
.mc-table :deep(.p-datatable-tbody > tr.is-ok) { border-left-color: rgb(22, 163, 74); }
.mc-table :deep(.p-datatable-tbody > tr.is-warn) { border-left-color: rgb(217, 119, 6); opacity: 0.85; }
.mc-table :deep(.p-datatable-loading-overlay) {
background: color-mix(in srgb, var(--m-bg-medium) 70%, transparent);
backdrop-filter: blur(2px);
}
.mc-table__loading {
display: inline-flex;
align-items: center;
gap: 8px;
color: var(--m-text);
font-size: 0.85rem;
}
/* Paginator (DataTable interno) */
.mc-table :deep(.p-paginator) {
background: var(--m-bg-medium);
border: none;
border-top: 1px solid var(--m-border);
padding: 8px 12px;
justify-content: center;
flex-wrap: wrap;
gap: 6px;
}
.mc-table :deep(.p-paginator-current) {
color: var(--m-text-muted);
font-size: 0.78rem;
background: transparent;
border: none;
padding: 0 6px;
}
.mc-table :deep(.p-paginator-first),
.mc-table :deep(.p-paginator-prev),
.mc-table :deep(.p-paginator-next),
.mc-table :deep(.p-paginator-last),
.mc-table :deep(.p-paginator-page) {
min-width: 30px;
height: 30px;
color: var(--m-text);
background: transparent;
border: 1px solid var(--m-border);
border-radius: 8px;
font-size: 0.8rem;
transition: background-color 140ms ease, border-color 140ms ease;
}
.mc-table :deep(.p-paginator-page.p-paginator-page-selected) {
background: var(--m-accent-soft);
border-color: var(--m-accent-strong);
color: var(--m-accent);
}
.mc-table :deep(.p-paginator-first:not(.p-disabled):hover),
.mc-table :deep(.p-paginator-prev:not(.p-disabled):hover),
.mc-table :deep(.p-paginator-next:not(.p-disabled):hover),
.mc-table :deep(.p-paginator-last:not(.p-disabled):hover),
.mc-table :deep(.p-paginator-page:not(.p-paginator-page-selected):hover) {
background: var(--m-bg-soft-hover);
border-color: var(--m-border-strong);
}
.mc-table :deep(.p-select) {
background: transparent;
border: 1px solid var(--m-border);
border-radius: 8px;
height: 30px;
font-size: 0.78rem;
color: var(--m-text);
display: inline-flex;
align-items: center;
}
.mc-table :deep(.p-select-label) {
padding: 0 8px;
color: var(--m-text);
font-size: 0.78rem;
display: flex;
align-items: center;
line-height: 1;
height: 100%;
background: transparent;
}
/* Coluna frozen "Ações" — bg sólido em ambos modos */
.mc-table :deep(td.p-datatable-frozen-column),
.mc-table :deep(th.p-datatable-frozen-column) {
background: var(--p-content-background) !important;
box-shadow: -3px 0 6px -3px rgba(0, 0, 0, 0.18);
z-index: 1;
}
.mc-table :deep(.p-datatable-tbody > tr:hover td.p-datatable-frozen-column) {
background: var(--m-bg-soft-hover);
}
.mc-table :deep(.p-datatable-tbody > tr.p-datatable-row-selected td.p-datatable-frozen-column) {
background: var(--m-accent-soft);
}
/* ─── Row content (design preservado do card antigo) ─── */
.mc-row__commit {
display: flex;
align-items: center;
gap: 12px;
min-width: 0;
}
.mc-row__commit.is-inactive { opacity: 0.65; }
.mc-row__color {
width: 6px;
height: 40px;
border-radius: 3px;
flex-shrink: 0;
}
.mc-row__main {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.mc-row__name-row {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.mc-row__name {
font-size: 0.92rem;
font-weight: 600;
color: var(--m-text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mc-row__desc {
font-size: 0.78rem;
color: var(--m-text-muted);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-top: 2px;
}
.mc-row__meta {
display: flex;
flex-direction: column;
gap: 4px;
font-size: 0.75rem;
color: var(--m-text-muted);
}
.mc-row__meta-item {
display: inline-flex;
align-items: center;
gap: 5px;
}
.mc-row__meta-item > i { font-size: 0.7rem; }
.mc-row__badge {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 0.62rem;
font-weight: 600;
padding: 2px 8px;
border-radius: 999px;
text-transform: uppercase;
letter-spacing: 0.04em;
border: 1px solid;
flex-shrink: 0;
}
.mc-row__badge--native {
color: rgb(37, 99, 235);
background: rgba(37, 99, 235, 0.12);
border-color: rgba(37, 99, 235, 0.3);
}
.mc-row__badge--off {
color: var(--m-text-muted);
background: var(--m-bg-medium);
border-color: var(--m-border);
}
.mc-row__badge > i { font-size: 0.55rem; }
/* Coluna de ações — toggle + 2 botões pequenos */
.mc-row__actions {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 6px;
flex-shrink: 0;
}
.mc-row__btn {
width: 28px;
height: 28px;
display: grid;
place-items: center;
background: var(--p-content-background);
border: 1px solid var(--m-border);
color: var(--m-text-muted);
border-radius: 8px;
cursor: pointer;
font-family: inherit;
transition: background-color 140ms ease, color 140ms ease, border-color 140ms ease;
flex-shrink: 0;
}
.mc-row__btn:hover {
background: color-mix(in srgb, var(--p-primary-color) 12%, var(--p-content-background));
border-color: var(--p-primary-color);
color: var(--p-primary-color);
}
.mc-row__btn--danger:hover {
background: rgba(220, 38, 38, 0.12);
border-color: rgba(220, 38, 38, 0.5);
color: rgb(220, 38, 38);
}
.mc-row__btn:disabled {
opacity: 0.4;
cursor: not-allowed;
background: transparent;
border-color: var(--m-border);
color: var(--m-text-muted);
}
.mc-row__btn:disabled:hover {
background: transparent;
border-color: var(--m-border);
color: var(--m-text-muted);
}
.mc-row__btn > i { font-size: 0.7rem; }
/* ─── Grid view ─── */
.mc-grid-wrap {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
gap: 10px;
border: 1px solid var(--m-border);
border-radius: 10px;
overflow: hidden;
background: transparent;
}
.mc-grid {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 12px;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 10px;
align-content: start;
}
.mc-grid::-webkit-scrollbar { width: 5px; }
.mc-grid::-webkit-scrollbar-thumb { background: var(--m-border-strong); border-radius: 3px; }
.mc-grid__loading {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 32px;
}
.mc-grid__card {
display: flex;
flex-direction: column;
gap: 8px;
padding: 12px;
background: var(--m-bg-soft);
border: 1px solid var(--m-border);
border-left: 3px solid var(--m-border-strong);
border-radius: 10px;
color: var(--m-text);
text-align: left;
cursor: pointer;
font-family: inherit;
transition: background-color 140ms ease, border-color 140ms ease, transform 140ms ease;
}
.mc-grid__card:hover {
background: var(--m-bg-soft-hover);
border-color: var(--m-border-strong);
transform: translateY(-1px);
}
.mc-grid__card:focus-visible {
outline: 2px solid var(--p-primary-color);
outline-offset: 2px;
}
.mc-grid__card.is-ok { border-left-color: rgb(22, 163, 74); }
.mc-grid__card.is-warn { border-left-color: rgb(217, 119, 6); }
.mc-grid__card.is-inactive { opacity: 0.7; }
.mc-grid__top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.mc-grid__color {
width: 28px;
height: 28px;
border-radius: 6px;
flex-shrink: 0;
}
.mc-grid__top-right {
display: inline-flex;
align-items: center;
gap: 6px;
}
.mc-grid__name-row {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
}
.mc-grid__name {
font-size: 0.92rem;
font-weight: 600;
color: var(--m-text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mc-grid__desc {
font-size: 0.78rem;
color: var(--m-text-muted);
line-height: 1.45;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.mc-grid__meta {
display: flex;
gap: 12px;
font-size: 0.75rem;
color: var(--m-text-muted);
margin-top: auto;
padding-top: 4px;
}
.mc-grid__footer {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 6px;
border-top: 1px solid var(--m-border);
padding-top: 8px;
margin-top: 4px;
}
/* Paginator standalone (grid view) */
.mc-paginator.p-paginator {
background: var(--m-bg-medium);
border: none;
border-top: 1px solid var(--m-border);
padding: 8px 12px;
justify-content: center;
flex-wrap: wrap;
gap: 6px;
flex-shrink: 0;
}
.mc-paginator.p-paginator .p-paginator-current {
color: var(--m-text-muted);
font-size: 0.78rem;
background: transparent;
border: none;
padding: 0 6px;
}
.mc-paginator.p-paginator .p-paginator-first,
.mc-paginator.p-paginator .p-paginator-prev,
.mc-paginator.p-paginator .p-paginator-next,
.mc-paginator.p-paginator .p-paginator-last,
.mc-paginator.p-paginator .p-paginator-page {
min-width: 30px;
height: 30px;
color: var(--m-text);
background: transparent;
border: 1px solid var(--m-border);
border-radius: 8px;
font-size: 0.8rem;
transition: background-color 140ms ease, border-color 140ms ease;
}
.mc-paginator.p-paginator .p-paginator-page.p-paginator-page-selected {
background: var(--m-accent-soft);
border-color: var(--m-accent-strong);
color: var(--m-accent);
}
.mc-paginator.p-paginator .p-paginator-first:not(.p-disabled):hover,
.mc-paginator.p-paginator .p-paginator-prev:not(.p-disabled):hover,
.mc-paginator.p-paginator .p-paginator-next:not(.p-disabled):hover,
.mc-paginator.p-paginator .p-paginator-last:not(.p-disabled):hover,
.mc-paginator.p-paginator .p-paginator-page:not(.p-paginator-page-selected):hover {
background: var(--m-bg-soft-hover);
border-color: var(--m-border-strong);
}
.mc-paginator.p-paginator .p-select {
background: transparent;
border: 1px solid var(--m-border);
border-radius: 8px;
height: 30px;
font-size: 0.78rem;
color: var(--m-text);
display: inline-flex;
align-items: center;
}
.mc-paginator.p-paginator .p-select-label {
padding: 0 8px;
color: var(--m-text);
font-size: 0.78rem;
display: flex;
align-items: center;
line-height: 1;
height: 100%;
background: transparent;
}
/* ─── Empty state ─── */
.mc-empty {
margin: 24px 0;
padding: 56px 28px;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
color: var(--m-text-muted);
border: 2px dashed var(--m-border-strong);
border-radius: 12px;
background: color-mix(in srgb, var(--m-bg-soft) 40%, transparent);
gap: 8px;
}
.mc-empty__icon { font-size: 2rem; color: var(--m-text-faint); margin-bottom: 4px; }
.mc-empty__title { font-size: 0.92rem; font-weight: 600; color: var(--m-text); }
.mc-empty__hint { font-size: 0.78rem; }
.mc-empty__btn { margin-top: 8px; }
/* ─── Drawer mobile ─── */
.mc-mobile-drawer {
position: fixed;
top: 0; left: 0;
height: 100vh;
height: 100dvh;
width: min(360px, 88vw);
z-index: 80;
background: var(--m-bg-medium);
backdrop-filter: blur(28px) saturate(160%);
-webkit-backdrop-filter: blur(28px) saturate(160%);
border-right: 1px solid var(--m-border);
transform: translateX(-100%);
transition: transform 250ms cubic-bezier(0.4, 0, 0.2, 1);
color: var(--m-text);
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
}
.mc-mobile-drawer.is-open { transform: translateX(0); }
.mc-mobile-drawer__scroll {
height: 100%;
overflow-y: auto;
padding: 12px 12px 24px;
display: flex;
flex-direction: column;
gap: 12px;
}
.mc-mobile-drawer__scroll::-webkit-scrollbar { width: 5px; }
.mc-mobile-drawer__scroll::-webkit-scrollbar-thumb {
background: var(--m-border-strong);
border-radius: 3px;
}
/* Em mobile a sidebar é teleportada pro drawer. O drawer tem seu próprio
scroll (.mc-mobile-drawer__scroll), então a .mc-side e o .mc-side__scroll
não devem ter overflow/altura próprios — viram containers passivos.
O .mc-side__footer mantém o "Limpar filtros" sempre visível no bottom
do drawer via position: sticky. */
.mc-mobile-drawer__scroll .mc-side {
width: 100%;
flex: 1;
min-height: 0;
overflow: visible;
padding: 0;
background: transparent;
border-right: none;
display: flex;
flex-direction: column;
}
.mc-mobile-drawer__scroll .mc-side__scroll {
flex: none;
min-height: 0;
overflow: visible;
display: flex;
flex-direction: column;
}
.mc-mobile-drawer__scroll .mc-side__footer {
position: sticky;
bottom: 0;
margin: auto -12px -24px; /* compensa o padding do drawer pra ficar de borda a borda */
background: var(--m-bg-medium);
border-top: 1px solid var(--m-border);
backdrop-filter: blur(24px) saturate(160%);
-webkit-backdrop-filter: blur(24px) saturate(160%);
z-index: 5;
}
.mc-mobile-drawer__scroll .mc-w--side {
margin: 0 0 12px;
}
.mc-mobile-drawer__scroll .mc-w--side:last-of-type {
margin-bottom: 0;
}
.mc-mobile-drawer__backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.45);
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
z-index: 79;
}
.mc-drawer-fade-enter-active,
.mc-drawer-fade-leave-active { transition: opacity 200ms ease; }
.mc-drawer-fade-enter-from,
.mc-drawer-fade-leave-to { opacity: 0; }
/* ─── Mobile (<1024px) ─── */
@media (max-width: 1023px) {
.mc-body { flex-direction: column; padding: 0; }
.mc-main { width: 100%; padding: 8px; }
.mc-page__title > span:first-of-type { display: none; }
.mc-menu-btn--mobile-only { display: inline-flex; }
/* Em mobile o "Novo" só ícone pra economizar espaço */
.mc-act-btn--primary span { display: none; }
.mc-act-btn--primary { width: 32px; padding: 0; justify-content: center; }
}
</style>