Files
agenciapsilmno/src/layout/melissa/MelissaCompromissos.vue
T
Leonardo bad828cab3 Melissa pages: esconde page-title-icon em mobile
No mobile o botao "Menu Lancamentos/Notificacoes/etc" ja indica a sessao,
entao o pi-list/pi-bell ao lado do contador era redundante. Adiciona
.<prefix>-page__title-icon { display: none; } no @media max-width: 1023px.
Em MelissaConversas usa > i:first-child (icone nao tem classe dedicada).

Pages: FinanceiroLancamentos, Compromissos, Documentos, CadastrosRecebidos,
Conversas, AgendamentosRecebidos, Financeiro, Grupos, Notificacoes, Tags,
Medicos, Relatorios, Recorrencias.

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

1926 lines
67 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 ─── */
/* Pattern espelhado do AppMenu/layout-sidebar do Rail:
- .xx-mobile-drawer = flex column, altura 100vh
- __scroll = passagem (flex: 1, sem scroll proprio, overflow hidden)
- .xx-side teleportada = flex column ocupando todo o espaco
- .xx-side__scroll interno = scroll real (flex: 1, overflow-y auto)
- .xx-side__footer = flex-shrink 0, sempre colado no bottom */
.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;
display: flex;
flex-direction: column;
}
.mc-mobile-drawer.is-open { transform: translateX(0); }
.mc-mobile-drawer__scroll {
flex: 1;
min-height: 0;
overflow: hidden;
display: flex;
flex-direction: column;
}
.mc-mobile-drawer__scroll .mc-side {
flex: 1;
min-height: 0;
width: 100%;
overflow: hidden;
background: transparent;
border-right: none;
display: flex;
flex-direction: column;
}
.mc-mobile-drawer__scroll .mc-side__scroll {
flex: 1;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
padding: 12px;
display: flex;
flex-direction: column;
gap: 12px;
scrollbar-width: thin;
scrollbar-color: var(--m-border-strong) transparent;
}
.mc-mobile-drawer__scroll .mc-side__scroll::-webkit-scrollbar { width: 5px; }
.mc-mobile-drawer__scroll .mc-side__scroll::-webkit-scrollbar-thumb {
background: var(--m-border-strong);
border-radius: 3px;
}
.mc-mobile-drawer__scroll .mc-w--side {
margin: 0;
flex-shrink: 0;
}
.mc-mobile-drawer__scroll .mc-w--side:last-of-type { margin-bottom: 0; }
.mc-mobile-drawer__scroll .mc-side__footer {
flex-shrink: 0;
margin: 0;
padding: 12px;
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%);
}
.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-page__title-icon { 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>