f17e9ee786
Fluxos anon identificam tenant por token/slug e nao resolvem o schema fisico. Decisao (opcao C): manter em public com RLS por token. Volta a global: patient_intake_requests, patient_invites, patient_invite_attempts, document_share_links, agendador_configuracoes, agendador_solicitacoes. - migration 20260613000001_f1b: remove as 6 do _tenant_template (template v2, 78 tabelas). Smoke: clone gera 78, zero tabelas anon no schema, drop limpo - frontend: 38 cadeias em 14 arquivos revertidas tenantDb().from() -> supabase.from() com tenant_id/owner_id restaurado (via comparacao com main) - edge: convert-abandoned-intakes restaurada do main (SELECT global) - save-intake-progress: ja usava public, sem mudanca - doc F0 atualizado: 78 tenant + 59 global Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1901 lines
70 KiB
Vue
1901 lines
70 KiB
Vue
<script setup>
|
||
/*
|
||
* MelissaCadastrosRecebidos — Cadastros recebidos (intake requests externos).
|
||
* Segue blueprint melissa-page-blueprint.md.
|
||
*
|
||
* Layout 2-col:
|
||
* - COL 1 — Aside (~280px): stats por status (Novo/Convertido/Rejeitado)
|
||
* + filtros (status pill toggle) + busca
|
||
* - COL 2 — Lista de cards de cadastro (avatar + nome + telefone/email +
|
||
* status badge + tempo)
|
||
*
|
||
* Click num card → dialog com detalhes + ações (Rejeitar / Converter em paciente).
|
||
* Reaproveita a lógica de patient_intake_requests da página antiga.
|
||
*/
|
||
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue';
|
||
import { useToast } from 'primevue/usetoast';
|
||
import { useConfirm } from 'primevue/useconfirm';
|
||
import { supabase } from '@/lib/supabase/client';
|
||
import { useTenantStore } from '@/stores/tenantStore';
|
||
// Fase 2 (Graphify hotspot): convertToPatient duplicado em 2 pages — extração pro repository.
|
||
import { createPatient, markIntakeConverted } from '@/features/patients/services/patientsRepository';
|
||
import { brToISO, isoToBR } from '@/utils/dateBR';
|
||
// Dialog/Textarea/Button auto-imported via PrimeVueResolver
|
||
|
||
const emit = defineEmits(['close']);
|
||
const toast = useToast();
|
||
const confirm = useConfirm();
|
||
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 converting = ref(false);
|
||
const rows = ref([]);
|
||
const busca = ref('');
|
||
const statusFilter = ref('');
|
||
|
||
const carregandoInicial = computed(
|
||
() => loading.value && rows.value.length === 0
|
||
);
|
||
|
||
// ── Helpers de campos (espelhados da CadastrosRecebidosPage) ──
|
||
function pickField(obj, keys) {
|
||
for (const k of keys) {
|
||
const v = obj?.[k];
|
||
if (v !== undefined && v !== null && String(v).trim() !== '') return v;
|
||
}
|
||
return null;
|
||
}
|
||
const fNome = (i) => pickField(i, ['nome_completo', 'name']);
|
||
const fEmail = (i) => pickField(i, ['email_principal', 'email']);
|
||
const fEmailAlt = (i) => pickField(i, ['email_alternativo', 'email_alt']);
|
||
const fTel = (i) => pickField(i, ['telefone', 'phone']);
|
||
const fTelAlt = (i) => pickField(i, ['telefone_alternativo', 'phone_alt']);
|
||
const fNasc = (i) => pickField(i, ['data_nascimento', 'birth_date']);
|
||
const fGenero = (i) => pickField(i, ['genero', 'gender']);
|
||
const fProf = (i) => pickField(i, ['profissao', 'profession']);
|
||
const fCep = (i) => pickField(i, ['cep']);
|
||
const fEndereco = (i) => pickField(i, ['endereco', 'address_street']);
|
||
const fNumero = (i) => pickField(i, ['numero', 'address_number']);
|
||
const fBairro = (i) => pickField(i, ['bairro', 'address_neighborhood']);
|
||
const fCidade = (i) => pickField(i, ['cidade', 'address_city']);
|
||
const fEstado = (i) => pickField(i, ['estado', 'address_state']);
|
||
const fEstadoCivil = (i) => pickField(i, ['estado_civil', 'marital_status']);
|
||
const fNaturalidade = (i) => pickField(i, ['naturalidade', 'place_of_birth']);
|
||
const fEscolaridade = (i) => pickField(i, ['escolaridade', 'education_level']);
|
||
const fOndeConheceu = (i) => pickField(i, ['onde_nos_conheceu', 'lead_source']);
|
||
const fEncaminhado = (i) => pickField(i, ['encaminhado_por', 'referred_by']);
|
||
const fObs = (i) => pickField(i, ['observacoes', 'notes_short']);
|
||
const fComplemento = (i) => pickField(i, ['complemento', 'address_complement']);
|
||
const fPais = (i) => pickField(i, ['pais', 'country']) || 'Brasil';
|
||
const fEmailAlt2 = (i) => pickField(i, ['email_alternativo', 'email_alt']);
|
||
|
||
// ── Formatters ─────────────────────────────────────────────
|
||
function dash(v) {
|
||
const s = String(v ?? '').trim();
|
||
return s ? s : '—';
|
||
}
|
||
function onlyDigits(v) { return String(v ?? '').replace(/\D/g, ''); }
|
||
function fmtPhoneBR(v) {
|
||
const d = onlyDigits(v);
|
||
if (!d) return '—';
|
||
if (d.length === 11) return `(${d.slice(0,2)}) ${d.slice(2,7)}-${d.slice(7,11)}`;
|
||
if (d.length === 10) return `(${d.slice(0,2)}) ${d.slice(2,6)}-${d.slice(6,10)}`;
|
||
return d;
|
||
}
|
||
function fmtCPF(v) {
|
||
const d = onlyDigits(v);
|
||
if (!d || d.length !== 11) return d || '—';
|
||
return `${d.slice(0,3)}.${d.slice(3,6)}.${d.slice(6,9)}-${d.slice(9,11)}`;
|
||
}
|
||
function fmtBirth(v) {
|
||
if (!v) return '—';
|
||
const s = String(v).trim();
|
||
if (/^\d{2}-\d{2}-\d{4}$/.test(s)) return s;
|
||
if (/^\d{4}-\d{2}-\d{2}/.test(s)) return isoToBR(s.slice(0, 10)) || s;
|
||
return s;
|
||
}
|
||
function normalizeBirthToISO(v) {
|
||
if (!v) return null;
|
||
const s = String(v).trim();
|
||
if (!s) return null;
|
||
if (/^\d{2}-\d{2}-\d{4}$/.test(s)) return brToISO(s);
|
||
if (/^\d{4}-\d{2}-\d{2}/.test(s)) return s.slice(0, 10);
|
||
const d = new Date(s);
|
||
if (Number.isNaN(d.getTime())) return null;
|
||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
||
}
|
||
function fmtRelative(iso) {
|
||
if (!iso) return '—';
|
||
const d = new Date(iso);
|
||
const now = new Date();
|
||
const diff = Math.floor((now - d) / 1000);
|
||
if (diff < 60) return 'agora';
|
||
if (diff < 3600) return `${Math.floor(diff / 60)}m`;
|
||
if (diff < 86400) return `${Math.floor(diff / 3600)}h`;
|
||
if (diff < 604800) return `${Math.floor(diff / 86400)}d`;
|
||
return d.toLocaleDateString('pt-BR');
|
||
}
|
||
function pacienteIniciais(nome) {
|
||
if (!nome) return '?';
|
||
const partes = String(nome).trim().split(/\s+/);
|
||
if (partes.length === 1) return partes[0][0]?.toUpperCase() || '?';
|
||
return (partes[0][0] + partes[partes.length - 1][0]).toUpperCase();
|
||
}
|
||
|
||
// ── Avatar URL ─────────────────────────────────────────────
|
||
const AVATAR_BUCKET = 'avatars';
|
||
function looksLikeUrl(s) { return /^https?:\/\//i.test(String(s || '')); }
|
||
function firstNonEmpty(...vals) {
|
||
for (const v of vals) {
|
||
const s = String(v ?? '').trim();
|
||
if (s) return s;
|
||
}
|
||
return '';
|
||
}
|
||
function avatarUrl(row) {
|
||
const p = row?.payload || row?.data || row?.form || null;
|
||
const direct = firstNonEmpty(row?.avatar_url, row?.foto_url, row?.photo_url, p?.avatar_url, p?.foto_url, p?.photo_url);
|
||
if (direct && looksLikeUrl(direct)) return direct;
|
||
const path = firstNonEmpty(row?.avatar_path, row?.photo_path, p?.avatar_path, p?.photo_path, direct);
|
||
if (!path) return null;
|
||
if (looksLikeUrl(path)) return path;
|
||
const { data } = supabase.storage.from(AVATAR_BUCKET).getPublicUrl(path);
|
||
return data?.publicUrl || null;
|
||
}
|
||
|
||
// ── Status helpers ─────────────────────────────────────────
|
||
function statusLabel(s) {
|
||
if (s === 'new') return 'Novo';
|
||
if (s === 'converted') return 'Convertido';
|
||
if (s === 'rejected') return 'Rejeitado';
|
||
return s || '—';
|
||
}
|
||
function statusClass(s) {
|
||
if (s === 'new') return 'is-new';
|
||
if (s === 'converted') return 'is-done';
|
||
if (s === 'rejected') return 'is-rejected';
|
||
return '';
|
||
}
|
||
|
||
// ── Stats ──────────────────────────────────────────────────
|
||
const stats = computed(() => {
|
||
const all = rows.value;
|
||
const n = all.filter((r) => r.status === 'new').length;
|
||
const c = all.filter((r) => r.status === 'converted').length;
|
||
const r = all.filter((r) => r.status === 'rejected').length;
|
||
return [
|
||
{ key: 'total', label: 'Total', value: all.length, cls: 'neutral' },
|
||
{ key: 'new', label: 'Novos', value: n, cls: n > 0 ? 'info' : 'neutral' },
|
||
{ key: 'converted', label: 'Convertidos', value: c, cls: 'ok' },
|
||
{ key: 'rejected', label: 'Rejeitados', value: r, cls: r > 0 ? 'danger' : 'neutral' }
|
||
];
|
||
});
|
||
|
||
const STATUS_FILTER_OPTIONS = [
|
||
{ key: 'new', label: 'Novos', icon: 'pi pi-bell' },
|
||
{ key: 'converted', label: 'Convertidos', icon: 'pi pi-check' },
|
||
{ key: 'rejected', label: 'Rejeitados', icon: 'pi pi-times' }
|
||
];
|
||
|
||
function toggleStatusFilter(s) {
|
||
statusFilter.value = statusFilter.value === s ? '' : s;
|
||
}
|
||
|
||
// ── Filtragem ──────────────────────────────────────────────
|
||
const filtered = computed(() => {
|
||
const term = String(busca.value || '').trim().toLowerCase();
|
||
let list = rows.value;
|
||
if (statusFilter.value) list = list.filter((r) => r.status === statusFilter.value);
|
||
if (!term) return list;
|
||
return list.filter((r) => {
|
||
const nome = String(fNome(r) || '').toLowerCase();
|
||
const email = String(fEmail(r) || '').toLowerCase();
|
||
const tel = String(fTel(r) || '').toLowerCase();
|
||
return nome.includes(term) || email.includes(term) || tel.includes(term);
|
||
});
|
||
});
|
||
|
||
// ── Paginação client-side (DataTable controla internamente) ──────
|
||
// `fetchIntakes` traz a lista completa de patient_intake_requests do
|
||
// tenant — o DataTable pagina internamente via prop `paginator`. A
|
||
// gente só precisa controlar `rows` (tamanho da página) e `first`
|
||
// (índice do primeiro item) pra reagir a filtros e mudanças de tamanho.
|
||
const PAGE_SIZE_OPTIONS = [10, 20, 50, 100];
|
||
const rowsMCR = ref(10);
|
||
const firstMCR = ref(0);
|
||
function onPage(event) {
|
||
firstMCR.value = event.first;
|
||
rowsMCR.value = event.rows;
|
||
}
|
||
// Reset pra página 1 quando busca/status mudam.
|
||
watch([busca, statusFilter], () => { firstMCR.value = 0; });
|
||
|
||
// Click numa row → abre o dialog. selectionMode="single" é só pra
|
||
// marcar visualmente — não persistimos seleção.
|
||
function onRowClick(event) {
|
||
if (event?.data) openDetails(event.data);
|
||
}
|
||
function rowStatusClass(data) {
|
||
return statusClass(data?.status);
|
||
}
|
||
|
||
// ── View mode (list / grid) ────────────────────────────────
|
||
// Toggle no topo do main column. Persiste em localStorage pra manter
|
||
// a preferência do user entre sessões. List = DataTable; Grid = cards
|
||
// num CSS grid + Paginator standalone (compartilha rowsMCR/firstMCR).
|
||
const VIEW_MODE_KEY = 'mcr.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 */ }
|
||
}
|
||
|
||
// Slice pra grid (DataTable já pagina internamente; grid precisa
|
||
// fatiar o `filtered` com base no firstMCR/rowsMCR compartilhados).
|
||
const pagedItems = computed(() => {
|
||
return filtered.value.slice(firstMCR.value, firstMCR.value + rowsMCR.value);
|
||
});
|
||
|
||
// ── Fetch ──────────────────────────────────────────────────
|
||
async function fetchIntakes() {
|
||
loading.value = true;
|
||
try {
|
||
const { data, error } = await supabase
|
||
.from('patient_intake_requests').select('*')
|
||
.order('created_at', { ascending: false });
|
||
if (error) throw error;
|
||
const weight = (s) => (s === 'new' ? 0 : s === 'converted' ? 1 : s === 'rejected' ? 2 : 9);
|
||
rows.value = (data || []).slice().sort((a, b) => {
|
||
const wa = weight(a.status), wb = weight(b.status);
|
||
if (wa !== wb) return wa - wb;
|
||
return new Date(b.created_at || 0).getTime() - new Date(a.created_at || 0).getTime();
|
||
});
|
||
} catch (e) {
|
||
toast.add({ severity: 'error', summary: 'Erro ao carregar', detail: e?.message || String(e), life: 3500 });
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
}
|
||
|
||
// ── Dialog detalhes ────────────────────────────────────────
|
||
const dlg = ref({ open: false, item: null, saving: false, reject_note: '' });
|
||
|
||
function openDetails(row) {
|
||
dlg.value.open = true;
|
||
dlg.value.item = row;
|
||
dlg.value.reject_note = row?.rejected_reason || '';
|
||
}
|
||
function closeDlg() {
|
||
dlg.value.open = false;
|
||
dlg.value.saving = false;
|
||
dlg.value.item = null;
|
||
dlg.value.reject_note = '';
|
||
}
|
||
|
||
const dlgAvatarUrl = computed(() => dlg.value.item ? avatarUrl(dlg.value.item) : null);
|
||
|
||
const dlgSections = computed(() => {
|
||
const i = dlg.value.item;
|
||
if (!i) return [];
|
||
const sec = (title, rs) => ({ title, rows: rs.filter((r) => r.value && r.value !== '—') });
|
||
return [
|
||
sec('Identificação', [
|
||
{ label: 'Email', value: dash(fEmail(i)) },
|
||
{ label: 'Email alt.', value: dash(fEmailAlt(i)) },
|
||
{ label: 'Telefone', value: fmtPhoneBR(fTel(i)) },
|
||
{ label: 'Tel. alt.', value: fmtPhoneBR(fTelAlt(i)) },
|
||
{ label: 'Nascimento', value: fmtBirth(fNasc(i)) },
|
||
{ label: 'Gênero', value: dash(fGenero(i)) },
|
||
{ label: 'Estado civil', value: dash(fEstadoCivil(i)) }
|
||
]),
|
||
sec('Documentos', [
|
||
{ label: 'CPF', value: fmtCPF(i.cpf) },
|
||
{ label: 'RG', value: dash(i.rg) }
|
||
]),
|
||
sec('Endereço', [
|
||
{ label: 'CEP', value: dash(fCep(i)) },
|
||
{ label: 'Endereço', value: [fEndereco(i), fNumero(i)].filter(Boolean).join(', ') || '—' },
|
||
{ label: 'Complemento', value: dash(fComplemento(i)) },
|
||
{ label: 'Bairro', value: dash(fBairro(i)) },
|
||
{ label: 'Cidade/UF', value: [fCidade(i), fEstado(i)].filter(Boolean).join('/') || '—' },
|
||
{ label: 'País', value: fPais(i) }
|
||
]),
|
||
sec('Profissional', [
|
||
{ label: 'Profissão', value: dash(fProf(i)) },
|
||
{ label: 'Escolaridade', value: dash(fEscolaridade(i)) },
|
||
{ label: 'Naturalidade', value: dash(fNaturalidade(i)) },
|
||
{ label: 'Onde conheceu', value: dash(fOndeConheceu(i)) },
|
||
{ label: 'Encaminhado por', value: dash(fEncaminhado(i)) }
|
||
]),
|
||
sec('Observações', [
|
||
{ label: 'Obs.', value: dash(fObs(i)) },
|
||
{ label: 'Notas internas', value: dash(i.notas_internas || i.notes) }
|
||
])
|
||
].filter((s) => s.rows.length > 0);
|
||
});
|
||
|
||
// ── Rejeitar ───────────────────────────────────────────────
|
||
async function markRejected() {
|
||
const item = dlg.value.item;
|
||
if (!item) return;
|
||
confirm.require({
|
||
message: 'Marcar este cadastro como rejeitado?',
|
||
header: 'Confirmar rejeição',
|
||
icon: 'pi pi-exclamation-triangle',
|
||
acceptLabel: 'Rejeitar',
|
||
rejectLabel: 'Cancelar',
|
||
acceptSeverity: 'danger',
|
||
accept: async () => {
|
||
dlg.value.saving = true;
|
||
try {
|
||
const reason = String(dlg.value.reject_note || '').trim() || null;
|
||
const { error } = await supabase.from('patient_intake_requests')
|
||
.update({ status: 'rejected', rejected_reason: reason, updated_at: new Date().toISOString() })
|
||
.eq('id', item.id);
|
||
if (error) throw error;
|
||
toast.add({ severity: 'success', summary: 'Rejeitado', life: 2200 });
|
||
await fetchIntakes();
|
||
const updated = rows.value.find((r) => r.id === item.id);
|
||
if (updated) openDetails(updated); else closeDlg();
|
||
} catch (e) {
|
||
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message, life: 4500 });
|
||
} finally {
|
||
dlg.value.saving = false;
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// ── Converter ──────────────────────────────────────────────
|
||
async function getResponsibleMemberId(tenantId, userId) {
|
||
const { data, error } = await supabase.from('tenant_members')
|
||
.select('id').eq('tenant_id', tenantId).eq('user_id', userId)
|
||
.eq('status', 'active').maybeSingle();
|
||
if (error) throw error;
|
||
if (!data?.id) throw new Error('Membro responsável não encontrado.');
|
||
return data.id;
|
||
}
|
||
|
||
async function convertToPatient() {
|
||
const item = dlg.value?.item;
|
||
if (!item?.id || converting.value) return;
|
||
if (item.status === 'converted') {
|
||
toast.add({ severity: 'warn', summary: 'Já convertido', life: 2500 });
|
||
return;
|
||
}
|
||
converting.value = true;
|
||
try {
|
||
const { data: userData, error: userErr } = await supabase.auth.getUser();
|
||
if (userErr) throw userErr;
|
||
const ownerId = userData?.user?.id;
|
||
if (!ownerId) throw new Error('Sessão inválida.');
|
||
|
||
const tenantId = tenantStore.activeTenantId || tenantStore.tenantId || null;
|
||
if (!tenantId) throw new Error('Tenant não inicializado.');
|
||
|
||
const responsibleMemberId = await getResponsibleMemberId(tenantId, ownerId);
|
||
|
||
const cleanStr = (v) => { const s = String(v ?? '').trim(); return s ? s : null; };
|
||
const digitsOnly = (v) => { const d = String(v ?? '').replace(/\D/g, ''); return d ? d : null; };
|
||
const intakeAvatar = cleanStr(item.avatar_url) || cleanStr(item.foto_url) || cleanStr(item.photo_url) || null;
|
||
|
||
const patientPayload = {
|
||
tenant_id: tenantId,
|
||
responsible_member_id: responsibleMemberId,
|
||
owner_id: ownerId,
|
||
nome_completo: cleanStr(fNome(item)),
|
||
email_principal: cleanStr(fEmail(item))?.toLowerCase() || null,
|
||
email_alternativo: cleanStr(fEmailAlt2(item))?.toLowerCase() || null,
|
||
telefone: digitsOnly(fTel(item)),
|
||
telefone_alternativo: digitsOnly(fTelAlt(item)),
|
||
data_nascimento: normalizeBirthToISO(fNasc(item)),
|
||
naturalidade: cleanStr(fNaturalidade(item)),
|
||
genero: cleanStr(fGenero(item)),
|
||
estado_civil: cleanStr(fEstadoCivil(item)),
|
||
cpf: digitsOnly(item.cpf),
|
||
rg: cleanStr(item.rg),
|
||
pais: cleanStr(fPais(item)) || 'Brasil',
|
||
cep: digitsOnly(fCep(item)),
|
||
cidade: cleanStr(fCidade(item)),
|
||
estado: cleanStr(fEstado(item)) || 'SP',
|
||
endereco: cleanStr(fEndereco(item)),
|
||
numero: cleanStr(fNumero(item)),
|
||
bairro: cleanStr(fBairro(item)),
|
||
complemento: cleanStr(fComplemento(item)),
|
||
escolaridade: cleanStr(fEscolaridade(item)),
|
||
profissao: cleanStr(fProf(item)),
|
||
onde_nos_conheceu: cleanStr(fOndeConheceu(item)),
|
||
encaminhado_por: cleanStr(fEncaminhado(item)),
|
||
observacoes: cleanStr(fObs(item)),
|
||
notas_internas: cleanStr(item.notas_internas || item.notes),
|
||
avatar_url: intakeAvatar
|
||
};
|
||
Object.keys(patientPayload).forEach((k) => {
|
||
if (patientPayload[k] === undefined) delete patientPayload[k];
|
||
});
|
||
|
||
// Repository chamadas (Fase 2 — convertToPatient de-dup).
|
||
const created = await createPatient(patientPayload);
|
||
const patientId = created?.id;
|
||
if (!patientId) throw new Error('Falha ao obter ID do paciente.');
|
||
|
||
await markIntakeConverted(item.id, patientId);
|
||
|
||
toast.add({ severity: 'success', summary: 'Convertido em paciente', life: 2500 });
|
||
closeDlg();
|
||
await fetchIntakes();
|
||
} catch (e) {
|
||
toast.add({ severity: 'error', summary: 'Falha ao converter', detail: e?.message, life: 4500 });
|
||
} finally {
|
||
converting.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 fetchIntakes();
|
||
});
|
||
onBeforeUnmount(() => {
|
||
if (_mqMobile) _mqMobile.removeEventListener('change', _onMqMobileChange);
|
||
});
|
||
</script>
|
||
|
||
<template>
|
||
<aside
|
||
class="mcr-mobile-drawer"
|
||
:class="{ 'is-open': drawerOpen }"
|
||
v-show="isMobile"
|
||
aria-label="Estatísticas e filtros"
|
||
>
|
||
<div id="mcr-mobile-drawer-target" class="mcr-mobile-drawer__scroll" />
|
||
</aside>
|
||
<Transition name="mcr-drawer-fade">
|
||
<div
|
||
v-if="isMobile && drawerOpen"
|
||
class="mcr-mobile-drawer__backdrop"
|
||
@click="fecharDrawer"
|
||
/>
|
||
</Transition>
|
||
|
||
<section class="mcr-page">
|
||
<header class="mcr-page__head">
|
||
<button
|
||
class="mcr-menu-btn mcr-menu-btn--mobile-only"
|
||
v-tooltip.bottom="'Estatísticas & filtros'"
|
||
@click="toggleDrawer"
|
||
>
|
||
<i class="pi pi-bars" />
|
||
<span>Menu Cadastros</span>
|
||
</button>
|
||
<div class="mcr-page__title">
|
||
<i class="pi pi-inbox mcr-page__title-icon" />
|
||
<span>Cadastros recebidos</span>
|
||
<span class="mcr-page__count">{{ filtered.length }}</span>
|
||
</div>
|
||
<div class="mcr-page__actions">
|
||
<button
|
||
class="mcr-head-btn"
|
||
v-tooltip.bottom="'Recarregar'"
|
||
:disabled="loading"
|
||
@click="fetchIntakes"
|
||
>
|
||
<i :class="loading ? 'pi pi-spin pi-spinner' : 'pi pi-refresh'" />
|
||
</button>
|
||
<button class="mcr-close" v-tooltip.bottom="'Voltar (Esc)'" @click="emit('close')">
|
||
<i class="pi pi-times" />
|
||
</button>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- Subheader explicativo — diferencia visualmente desta página
|
||
(vs. Agendamentos Recebidos, que tem layout idêntico). Padrão
|
||
do melissa-table-page-blueprint.md. -->
|
||
<div class="mcr-subheader">
|
||
<i class="pi pi-info-circle mcr-subheader__icon" />
|
||
<span class="mcr-subheader__text">
|
||
Cadastros completos enviados por pacientes via formulário
|
||
externo (link público). Revise os dados,
|
||
<strong>converta em paciente ativo</strong> com 1 clique ou
|
||
<strong>rejeite</strong> com motivo opcional.
|
||
</span>
|
||
</div>
|
||
|
||
<div class="mcr-body">
|
||
<Teleport to="#mcr-mobile-drawer-target" :disabled="!isMobile">
|
||
<aside class="mcr-side">
|
||
<!-- Stats -->
|
||
<div class="mcr-w mcr-w--side">
|
||
<div class="mcr-w__head">
|
||
<span class="mcr-w__title"><i class="pi pi-chart-bar" /> Estatísticas</span>
|
||
</div>
|
||
<div class="mcr-stats">
|
||
<template v-if="carregandoInicial">
|
||
<div v-for="i in 4" :key="`stsk-${i}`" class="mcr-stat" aria-busy="true">
|
||
<div class="mcr-stat__val melissa-skeleton melissa-skeleton--number" />
|
||
<div class="mcr-stat__lbl melissa-skeleton melissa-skeleton--text" style="width: 60%; margin-top: 6px;" />
|
||
</div>
|
||
</template>
|
||
<div
|
||
v-for="s in stats"
|
||
v-else
|
||
:key="s.key"
|
||
class="mcr-stat"
|
||
:class="`is-${s.cls}`"
|
||
>
|
||
<div class="mcr-stat__val">{{ s.value }}</div>
|
||
<div class="mcr-stat__lbl">{{ s.label }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Filtro de status -->
|
||
<div class="mcr-w mcr-w--side">
|
||
<div class="mcr-w__head">
|
||
<span class="mcr-w__title"><i class="pi pi-filter" /> Status</span>
|
||
<span v-if="statusFilter" class="mcr-w__count">1</span>
|
||
</div>
|
||
<div class="mcr-side__list">
|
||
<button
|
||
v-for="o in STATUS_FILTER_OPTIONS"
|
||
:key="o.key"
|
||
class="mcr-side__item"
|
||
:class="[`is-${o.key}`, { 'is-active': statusFilter === o.key }]"
|
||
@click="toggleStatusFilter(o.key)"
|
||
>
|
||
<i :class="o.icon" />
|
||
<span>{{ o.label }}</span>
|
||
</button>
|
||
<Transition name="mcr-clear">
|
||
<button
|
||
v-if="statusFilter"
|
||
class="mcr-side__item is-clear"
|
||
@click="statusFilter = ''"
|
||
>
|
||
<i class="pi pi-filter-slash" />
|
||
<span>Limpar filtro</span>
|
||
</button>
|
||
</Transition>
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
</Teleport>
|
||
|
||
<div class="mcr-main">
|
||
<!-- Toolbar com busca + toggle de visualização (lista/grade).
|
||
Search no topo da coluna que contém os dados. Count
|
||
reflete o filtrado pra feedback imediato ao digitar. -->
|
||
<div class="mcr-toolbar">
|
||
<div class="mcr-search">
|
||
<i class="pi pi-search mcr-search__icon" />
|
||
<input
|
||
v-model="busca"
|
||
type="text"
|
||
placeholder="Buscar por nome, email ou telefone…"
|
||
class="mcr-search__input"
|
||
/>
|
||
<button
|
||
v-if="busca"
|
||
class="mcr-search__clear"
|
||
v-tooltip.bottom="'Limpar busca'"
|
||
@click="busca = ''"
|
||
>
|
||
<i class="pi pi-times" />
|
||
</button>
|
||
</div>
|
||
<div class="mcr-view-toggle" role="group" aria-label="Visualização">
|
||
<button
|
||
class="mcr-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="mcr-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>
|
||
|
||
<DataTable
|
||
v-if="viewMode === 'list'"
|
||
:value="filtered"
|
||
:loading="loading"
|
||
dataKey="id"
|
||
paginator
|
||
:rows="rowsMCR"
|
||
:first="firstMCR"
|
||
: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: 640px"
|
||
class="mcr-table"
|
||
@row-click="onRowClick"
|
||
@page="onPage"
|
||
>
|
||
<Column header="Paciente" style="min-width: 220px">
|
||
<template #body="{ data }">
|
||
<div class="mcr-row__patient">
|
||
<span class="mcr-card__avatar mcr-card__avatar--sm">
|
||
<img v-if="avatarUrl(data)" :src="avatarUrl(data)" :alt="fNome(data) || 'Avatar'" />
|
||
<span v-else>{{ pacienteIniciais(fNome(data)) }}</span>
|
||
</span>
|
||
<div class="mcr-row__patient-text">
|
||
<span class="mcr-row__name">{{ fNome(data) || '—' }}</span>
|
||
<span class="mcr-card__badge" :class="statusClass(data.status)">
|
||
{{ statusLabel(data.status) }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</Column>
|
||
|
||
<Column header="Contato" style="min-width: 220px">
|
||
<template #body="{ data }">
|
||
<div class="mcr-row__contact">
|
||
<span v-if="fEmail(data)"><i class="pi pi-envelope" /> {{ fEmail(data) }}</span>
|
||
<span v-if="fTel(data)"><i class="pi pi-phone" /> {{ fmtPhoneBR(fTel(data)) }}</span>
|
||
<span v-if="!fEmail(data) && !fTel(data)" class="mcr-row__empty">—</span>
|
||
</div>
|
||
</template>
|
||
</Column>
|
||
|
||
<Column header="Recebido" style="width: 130px">
|
||
<template #body="{ data }">
|
||
<span class="mcr-row__time">
|
||
<i class="pi pi-clock" /> {{ fmtRelative(data.created_at) }}
|
||
</span>
|
||
</template>
|
||
</Column>
|
||
|
||
<Column
|
||
header=""
|
||
:style="{ width: '60px', maxWidth: '60px', minWidth: '60px' }"
|
||
frozen
|
||
alignFrozen="right"
|
||
class="mcr-col-acao"
|
||
>
|
||
<template #body="{ data }">
|
||
<button
|
||
class="mcr-row__action"
|
||
v-tooltip.left="'Editar'"
|
||
aria-label="Editar"
|
||
@click.stop="openDetails(data)"
|
||
>
|
||
<i class="pi pi-pencil" />
|
||
</button>
|
||
</template>
|
||
</Column>
|
||
|
||
<template #empty>
|
||
<div class="mcr-empty">
|
||
<i class="pi pi-inbox mcr-empty__icon" />
|
||
<div class="mcr-empty__title">Nenhum cadastro encontrado</div>
|
||
<div class="mcr-empty__hint">
|
||
<template v-if="busca || statusFilter">Ajuste os filtros pra ver mais.</template>
|
||
<template v-else>Cadastros vindos do agendador externo aparecem aqui.</template>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<template #loading>
|
||
<div class="mcr-table__loading">
|
||
<i class="pi pi-spin pi-spinner" />
|
||
<span>Carregando cadastros…</span>
|
||
</div>
|
||
</template>
|
||
</DataTable>
|
||
|
||
<!-- Grid view — cards num CSS grid, mesma fonte de dados
|
||
(filtered + pagedItems) e Paginator standalone que
|
||
compartilha rowsMCR/firstMCR com a list view. -->
|
||
<div v-else-if="viewMode === 'grid'" class="mcr-grid-wrap">
|
||
<div v-if="loading && filtered.length === 0" class="mcr-table__loading mcr-grid__loading">
|
||
<i class="pi pi-spin pi-spinner" />
|
||
<span>Carregando cadastros…</span>
|
||
</div>
|
||
<div v-else-if="filtered.length === 0" class="mcr-empty">
|
||
<i class="pi pi-inbox mcr-empty__icon" />
|
||
<div class="mcr-empty__title">Nenhum cadastro encontrado</div>
|
||
<div class="mcr-empty__hint">
|
||
<template v-if="busca || statusFilter">Ajuste os filtros pra ver mais.</template>
|
||
<template v-else>Cadastros vindos do agendador externo aparecem aqui.</template>
|
||
</div>
|
||
</div>
|
||
<div v-else class="mcr-grid">
|
||
<div
|
||
v-for="r in pagedItems"
|
||
:key="r.id"
|
||
class="mcr-grid__card"
|
||
:class="statusClass(r.status)"
|
||
role="button"
|
||
tabindex="0"
|
||
@click="openDetails(r)"
|
||
@keydown.enter.prevent="openDetails(r)"
|
||
@keydown.space.prevent="openDetails(r)"
|
||
>
|
||
<div class="mcr-grid__top">
|
||
<span class="mcr-card__avatar">
|
||
<img v-if="avatarUrl(r)" :src="avatarUrl(r)" :alt="fNome(r) || 'Avatar'" />
|
||
<span v-else>{{ pacienteIniciais(fNome(r)) }}</span>
|
||
</span>
|
||
<div class="mcr-grid__top-right">
|
||
<span class="mcr-card__badge" :class="statusClass(r.status)">
|
||
{{ statusLabel(r.status) }}
|
||
</span>
|
||
<button
|
||
class="mcr-row__action"
|
||
v-tooltip.left="'Editar'"
|
||
aria-label="Editar"
|
||
@click.stop="openDetails(r)"
|
||
>
|
||
<i class="pi pi-pencil" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="mcr-grid__name">{{ fNome(r) || '—' }}</div>
|
||
<div class="mcr-grid__meta">
|
||
<span v-if="fEmail(r)"><i class="pi pi-envelope" /> {{ fEmail(r) }}</span>
|
||
<span v-if="fTel(r)"><i class="pi pi-phone" /> {{ fmtPhoneBR(fTel(r)) }}</span>
|
||
</div>
|
||
<div class="mcr-grid__time">
|
||
<i class="pi pi-clock" /> {{ fmtRelative(r.created_at) }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<Paginator
|
||
v-if="filtered.length > 0"
|
||
class="mcr-paginator"
|
||
:rows="rowsMCR"
|
||
:totalRecords="filtered.length"
|
||
:first="firstMCR"
|
||
:rowsPerPageOptions="PAGE_SIZE_OPTIONS"
|
||
template="RowsPerPageDropdown FirstPageLink PrevPageLink CurrentPageReport NextPageLink LastPageLink"
|
||
currentPageReportTemplate="{first}–{last} de {totalRecords}"
|
||
@page="onPage"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Dialog detalhes -->
|
||
<Dialog
|
||
:visible="dlg.open"
|
||
modal
|
||
dismissable-mask
|
||
:style="{ width: '640px', maxWidth: '94vw' }"
|
||
:header="fNome(dlg.item) || 'Cadastro'"
|
||
@update:visible="(v) => !v && closeDlg()"
|
||
>
|
||
<div v-if="dlg.item" class="flex flex-col gap-4">
|
||
<!-- Avatar + status + ações principais -->
|
||
<div class="flex items-start gap-3">
|
||
<div class="mcr-dlg__avatar">
|
||
<img v-if="dlgAvatarUrl" :src="dlgAvatarUrl" :alt="fNome(dlg.item)" />
|
||
<span v-else>{{ pacienteIniciais(fNome(dlg.item)) }}</span>
|
||
</div>
|
||
<div class="flex-1 min-w-0">
|
||
<div class="text-sm font-bold">{{ fNome(dlg.item) }}</div>
|
||
<div class="text-xs text-[var(--text-color-secondary)] mt-1">
|
||
<span class="mcr-card__badge" :class="statusClass(dlg.item.status)">{{ statusLabel(dlg.item.status) }}</span>
|
||
· Criado {{ fmtRelative(dlg.item.created_at) }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Seções -->
|
||
<div v-for="sec in dlgSections" :key="sec.title" class="flex flex-col gap-1">
|
||
<div class="text-[0.62rem] uppercase tracking-wider font-semibold text-[var(--text-color-secondary)] opacity-70">{{ sec.title }}</div>
|
||
<div class="grid grid-cols-2 gap-x-4 gap-y-1 text-xs">
|
||
<template v-for="r in sec.rows" :key="r.label">
|
||
<div class="text-[var(--text-color-secondary)]">{{ r.label }}</div>
|
||
<div class="text-[var(--text-color)]">{{ r.value }}</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Motivo da rejeição -->
|
||
<div v-if="dlg.item.status !== 'converted'" class="flex flex-col gap-1">
|
||
<label class="text-[0.62rem] uppercase tracking-wider font-semibold text-[var(--text-color-secondary)] opacity-70">
|
||
Motivo da rejeição (opcional)
|
||
</label>
|
||
<Textarea v-model="dlg.reject_note" autoResize rows="2" class="w-full text-xs" :disabled="dlg.saving" />
|
||
</div>
|
||
</div>
|
||
<template #footer>
|
||
<div class="flex items-center gap-2 w-full">
|
||
<Button label="Fechar" text @click="closeDlg" />
|
||
<div class="flex-1" />
|
||
<Button
|
||
v-if="dlg.item && dlg.item.status !== 'rejected' && dlg.item.status !== 'converted'"
|
||
label="Rejeitar"
|
||
severity="danger"
|
||
outlined
|
||
:disabled="dlg.saving || converting"
|
||
@click="markRejected"
|
||
/>
|
||
<Button
|
||
v-if="dlg.item && dlg.item.status !== 'converted'"
|
||
:label="converting ? 'Convertendo…' : 'Converter em paciente'"
|
||
:loading="converting"
|
||
:disabled="dlg.saving || converting"
|
||
@click="convertToPatient"
|
||
/>
|
||
</div>
|
||
</template>
|
||
</Dialog>
|
||
</section>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.mcr-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: mcr-page-enter 240ms cubic-bezier(0.2, 0.7, 0.3, 1);
|
||
}
|
||
@keyframes mcr-page-enter {
|
||
from { opacity: 0; transform: scale(0.985); }
|
||
to { opacity: 1; transform: scale(1); }
|
||
}
|
||
|
||
.mcr-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;
|
||
}
|
||
.mcr-page__title {
|
||
flex: 1;
|
||
min-width: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
font-size: 1rem;
|
||
font-weight: 500;
|
||
}
|
||
/* Ícone do título — cor primary do tema (espelha o token PrimeVue).
|
||
Tamanho levemente maior que o label pra ler como ícone identitário. */
|
||
.mcr-page__title-icon {
|
||
color: var(--p-primary-color);
|
||
font-size: 1.05rem;
|
||
}
|
||
.mcr-page__title > span:not(.mcr-page__count) {
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
.mcr-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;
|
||
}
|
||
.mcr-page__actions { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
|
||
|
||
.mcr-close, .mcr-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;
|
||
}
|
||
.mcr-close:hover, .mcr-head-btn:hover { background: var(--m-bg-soft-hover); }
|
||
.mcr-head-btn > i { font-size: 0.85rem; }
|
||
|
||
.mcr-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;
|
||
}
|
||
.mcr-menu-btn:hover {
|
||
background: color-mix(in srgb, var(--m-accent) 88%, white);
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
/* Subheader explicativo — faixa abaixo do page__head com texto contextual
|
||
sobre a página. Bg sutil + ícone primary + tipografia menor. Diferencia
|
||
esta página de outras tabelas Melissa que têm layout visual idêntico. */
|
||
.mcr-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;
|
||
}
|
||
.mcr-subheader__icon {
|
||
color: var(--p-primary-color);
|
||
font-size: 0.92rem;
|
||
flex-shrink: 0;
|
||
margin-top: 1px;
|
||
}
|
||
.mcr-subheader__text {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
.mcr-subheader__text strong {
|
||
color: var(--m-text);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.mcr-body {
|
||
flex: 1;
|
||
display: flex;
|
||
min-height: 0;
|
||
gap: 0;
|
||
padding: 0;
|
||
}
|
||
/* Sidebar — espelha .ma-side do MelissaAgenda: bg colorido próprio
|
||
(--m-bg-soft) + border-right pra separar visualmente da coluna
|
||
principal. Cards internos (.mcr-w--side) ficam mais opacos
|
||
(--m-bg-medium) pra contrastar com o bg da sidebar. */
|
||
.mcr-side {
|
||
width: 280px;
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0;
|
||
overflow-y: auto;
|
||
background: var(--m-bg-soft);
|
||
border-right: 1px solid var(--m-border);
|
||
}
|
||
.mcr-side::-webkit-scrollbar { width: 5px; }
|
||
.mcr-side::-webkit-scrollbar-thumb {
|
||
background: var(--m-border-strong);
|
||
border-radius: 3px;
|
||
}
|
||
|
||
/* Card-base — alinhado com .ma-w (MelissaAgenda) e .mp-w (MelissaPacientes):
|
||
surface --m-bg-medium pra destacar do bg da sidebar (--m-bg-soft). */
|
||
.mcr-w {
|
||
background: var(--m-bg-medium);
|
||
border: 1px solid var(--m-border);
|
||
border-radius: 12px;
|
||
padding: 12px;
|
||
}
|
||
/* Modificador pra usar .mcr-w dentro da sidebar (col 1) — adiciona
|
||
margem nas bordas (a .mcr-side não tem padding interno) + sombra
|
||
sutil pra elevação visual sobre o bg colorido da sidebar.
|
||
Espelha .ma-w--side do MelissaAgenda. */
|
||
.mcr-w--side {
|
||
margin: 12px 12px 0;
|
||
flex-shrink: 0;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
|
||
}
|
||
.mcr-w--side:last-of-type { margin-bottom: 12px; }
|
||
/* Header e título no padrão do .ma-w__head/__title — uppercase com
|
||
letter-spacing pra hierarquia visual de label da sidebar. */
|
||
.mcr-w__head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 10px;
|
||
}
|
||
.mcr-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;
|
||
}
|
||
.mcr-w__title > i { color: var(--m-text-muted); font-size: 0.7rem; }
|
||
.mcr-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;
|
||
}
|
||
|
||
.mcr-stats {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 6px;
|
||
}
|
||
.mcr-stat {
|
||
/* Mais sutil que o card-pai (.mcr-w em --m-bg-medium) pra criar contraste
|
||
interno. Antes usava --m-bg-medium e ficava igual ao pai. */
|
||
background: var(--m-bg-soft);
|
||
border: 1px solid var(--m-border);
|
||
border-radius: 10px;
|
||
padding: 8px 10px;
|
||
}
|
||
.mcr-stat__val {
|
||
font-size: 1.1rem;
|
||
font-weight: 600;
|
||
line-height: 1.1;
|
||
}
|
||
.mcr-stat__lbl {
|
||
font-size: 0.65rem;
|
||
color: var(--m-text-muted);
|
||
margin-top: 4px;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.06em;
|
||
}
|
||
/* Cores fortes (Tailwind 600) pra ler bem em ambos os modos. Light:
|
||
suficientemente escuras pra contrastar com bg branco. Dark: ainda
|
||
saturadas o bastante pra destacar do bg quase preto. */
|
||
.mcr-stat.is-info .mcr-stat__val { color: rgb(37, 99, 235); } /* blue-600 */
|
||
.mcr-stat.is-ok .mcr-stat__val { color: rgb(22, 163, 74); } /* green-600 */
|
||
.mcr-stat.is-danger .mcr-stat__val { color: rgb(220, 38, 38); } /* red-600 */
|
||
|
||
.mcr-side__list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
/* Botões do filtro de status — coloridos em 3 níveis:
|
||
- Default: tint 5% bg + 18% border + ícone full color → identifica
|
||
o status mesmo sem clicar
|
||
- Hover: tint 10% bg + 30% border
|
||
- Active: tint 16% bg + 55% border + box-shadow ring pro destaque
|
||
Cores Tailwind 600: blue/green/red — fortes pra ler em light. */
|
||
.mcr-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;
|
||
}
|
||
.mcr-side__item > i {
|
||
font-size: 0.78rem;
|
||
width: 14px;
|
||
text-align: center;
|
||
}
|
||
|
||
/* Novo — azul */
|
||
.mcr-side__item.is-new {
|
||
background: rgba(37, 99, 235, 0.05);
|
||
border-color: rgba(37, 99, 235, 0.18);
|
||
}
|
||
.mcr-side__item.is-new > i { color: rgb(37, 99, 235); }
|
||
.mcr-side__item.is-new:hover {
|
||
background: rgba(37, 99, 235, 0.10);
|
||
border-color: rgba(37, 99, 235, 0.30);
|
||
}
|
||
.mcr-side__item.is-active.is-new {
|
||
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);
|
||
}
|
||
|
||
/* Convertido — verde */
|
||
.mcr-side__item.is-converted {
|
||
background: rgba(22, 163, 74, 0.05);
|
||
border-color: rgba(22, 163, 74, 0.18);
|
||
}
|
||
.mcr-side__item.is-converted > i { color: rgb(22, 163, 74); }
|
||
.mcr-side__item.is-converted:hover {
|
||
background: rgba(22, 163, 74, 0.10);
|
||
border-color: rgba(22, 163, 74, 0.30);
|
||
}
|
||
.mcr-side__item.is-active.is-converted {
|
||
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);
|
||
}
|
||
|
||
/* Rejeitado — vermelho */
|
||
.mcr-side__item.is-rejected {
|
||
background: rgba(220, 38, 38, 0.05);
|
||
border-color: rgba(220, 38, 38, 0.18);
|
||
}
|
||
.mcr-side__item.is-rejected > i { color: rgb(220, 38, 38); }
|
||
.mcr-side__item.is-rejected:hover {
|
||
background: rgba(220, 38, 38, 0.10);
|
||
border-color: rgba(220, 38, 38, 0.30);
|
||
}
|
||
.mcr-side__item.is-active.is-rejected {
|
||
background: rgba(220, 38, 38, 0.16);
|
||
border-color: rgba(220, 38, 38, 0.55);
|
||
box-shadow: 0 0 0 1px rgba(220, 38, 38, 0.35);
|
||
}
|
||
|
||
/* Limpar filtro — botão neutro que aparece como 4ª linha quando há
|
||
um filtro ativo. Cor sutil, apenas pra ler como ação secundária. */
|
||
.mcr-side__item.is-clear {
|
||
margin-top: 4px;
|
||
background: var(--m-bg-soft);
|
||
border-color: var(--m-border);
|
||
color: var(--m-text-muted);
|
||
font-style: italic;
|
||
}
|
||
.mcr-side__item.is-clear > i { color: var(--m-text-muted); }
|
||
.mcr-side__item.is-clear:hover {
|
||
background: var(--m-bg-soft-hover);
|
||
border-color: var(--m-border-strong);
|
||
color: var(--m-text);
|
||
}
|
||
.mcr-side__item.is-clear:hover > i { color: var(--m-text); }
|
||
|
||
/* Transition do botão "Limpar filtro" — fade + leve slide vertical
|
||
pra entrada/saída suave. Aproveita os 4px de margin-top como ponto
|
||
de partida do slide (sai de cima pra encaixar). */
|
||
.mcr-clear-enter-active,
|
||
.mcr-clear-leave-active {
|
||
transition: opacity 220ms ease, transform 220ms ease, max-height 220ms ease, margin-top 220ms ease;
|
||
overflow: hidden;
|
||
}
|
||
.mcr-clear-enter-from,
|
||
.mcr-clear-leave-to {
|
||
opacity: 0;
|
||
transform: translateY(-4px);
|
||
max-height: 0;
|
||
margin-top: 0;
|
||
}
|
||
.mcr-clear-enter-to,
|
||
.mcr-clear-leave-from {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
max-height: 40px;
|
||
}
|
||
|
||
/* Main column (col 2) — busca no topo (toolbar) + lista scrollável +
|
||
paginator fixo no rodapé. min-height:0 é CRÍTICO pro flex:1 da lista
|
||
funcionar dentro de um flex container (sem isso, .mcr-list cresce
|
||
indefinidamente em vez de scrollar). */
|
||
.mcr-main {
|
||
flex: 1;
|
||
min-width: 0;
|
||
min-height: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
padding: 12px;
|
||
gap: 10px;
|
||
}
|
||
|
||
/* Toolbar — wrapper da busca no topo da coluna principal. Mesmo padrão
|
||
visual do .ma-tsearch (toolbar de busca da MelissaAgenda). */
|
||
.mcr-toolbar {
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
.mcr-search {
|
||
position: relative;
|
||
flex: 1;
|
||
min-width: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
.mcr-search__icon {
|
||
position: absolute;
|
||
left: 12px;
|
||
color: var(--m-text-muted);
|
||
font-size: 0.78rem;
|
||
pointer-events: none;
|
||
}
|
||
.mcr-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;
|
||
}
|
||
.mcr-search__input::placeholder { color: var(--m-text-faint); }
|
||
.mcr-search__input:focus {
|
||
border-color: var(--m-border-strong);
|
||
background: var(--m-bg-medium);
|
||
}
|
||
.mcr-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;
|
||
}
|
||
.mcr-search__clear:hover {
|
||
background: var(--m-bg-soft-hover);
|
||
color: var(--m-text);
|
||
}
|
||
.mcr-search__clear > i { font-size: 0.7rem; }
|
||
|
||
/* Toggle de visualização (lista/grade) — segmented control compacto
|
||
à direita da busca. Botão ativo ganha bg primary-tinted. */
|
||
.mcr-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;
|
||
}
|
||
.mcr-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;
|
||
}
|
||
.mcr-view-toggle__btn:hover {
|
||
background: var(--m-bg-soft-hover);
|
||
color: var(--m-text);
|
||
}
|
||
.mcr-view-toggle__btn.is-active {
|
||
background: var(--m-accent-soft);
|
||
color: var(--m-accent);
|
||
}
|
||
.mcr-view-toggle__btn > i { font-size: 0.85rem; }
|
||
|
||
/* ─── DataTable (.mcr-table) ─────────────────────────────────────
|
||
Substitui a antiga lista de cards. `:loading="loading"` cobre o
|
||
estado de carregamento (template #loading) e `paginator` integra
|
||
o seletor de tamanho + navegação. scrollHeight="flex" faz a tabela
|
||
preencher o flex restante da .mcr-main e scrollar internamente. */
|
||
.mcr-table {
|
||
flex: 1;
|
||
min-height: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
.mcr-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;
|
||
}
|
||
.mcr-table :deep(.p-datatable-table-container) {
|
||
flex: 1;
|
||
min-height: 0;
|
||
background: transparent;
|
||
}
|
||
/* Header — usa --p-content-background (token "surface" do tema PrimeVue).
|
||
Em light mode → branco; em dark mode → surface configurado (dark).
|
||
Mesma fonte que os cards Estatísticas/Status visualizam (eles usam
|
||
--m-bg-medium, que em light é --p-content-background opaco). */
|
||
.mcr-table :deep(.p-datatable-thead),
|
||
.mcr-table :deep(.p-datatable-thead > tr) {
|
||
background: transparent !important;
|
||
}
|
||
.mcr-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);
|
||
}
|
||
.mcr-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);
|
||
}
|
||
.mcr-table :deep(.p-datatable-tbody > tr > td) {
|
||
padding: 10px 14px;
|
||
border-bottom: 1px solid var(--m-border);
|
||
background: transparent;
|
||
vertical-align: middle;
|
||
}
|
||
.mcr-table :deep(.p-datatable-tbody > tr:hover) {
|
||
background: var(--m-bg-soft-hover);
|
||
}
|
||
.mcr-table :deep(.p-datatable-tbody > tr.p-datatable-row-selected) {
|
||
background: var(--m-accent-soft);
|
||
}
|
||
/* Border-left colorido por status — espelha o padrão .ma-sess. */
|
||
.mcr-table :deep(.p-datatable-tbody > tr.is-new) { border-left-color: rgb(37, 99, 235); }
|
||
.mcr-table :deep(.p-datatable-tbody > tr.is-done) { border-left-color: rgb(22, 163, 74); }
|
||
.mcr-table :deep(.p-datatable-tbody > tr.is-rejected) { border-left-color: rgb(220, 38, 38); opacity: 0.85; }
|
||
|
||
/* Loading overlay */
|
||
.mcr-table :deep(.p-datatable-loading-overlay) {
|
||
background: color-mix(in srgb, var(--m-bg-medium) 70%, transparent);
|
||
backdrop-filter: blur(2px);
|
||
}
|
||
.mcr-table__loading {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
color: var(--m-text);
|
||
font-size: 0.85rem;
|
||
}
|
||
|
||
/* Paginator integrado do DataTable — centralizado (sem refresh à
|
||
esquerda; o refresh já vive no header da página). */
|
||
.mcr-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;
|
||
}
|
||
.mcr-table :deep(.p-paginator-current) {
|
||
color: var(--m-text-muted);
|
||
font-size: 0.78rem;
|
||
background: transparent;
|
||
border: none;
|
||
padding: 0 6px;
|
||
}
|
||
.mcr-table :deep(.p-paginator-first),
|
||
.mcr-table :deep(.p-paginator-prev),
|
||
.mcr-table :deep(.p-paginator-next),
|
||
.mcr-table :deep(.p-paginator-last),
|
||
.mcr-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;
|
||
}
|
||
.mcr-table :deep(.p-paginator-page.p-paginator-page-selected) {
|
||
background: var(--m-accent-soft);
|
||
border-color: var(--m-accent-strong);
|
||
color: var(--m-accent);
|
||
}
|
||
.mcr-table :deep(.p-paginator-first:not(.p-disabled):hover),
|
||
.mcr-table :deep(.p-paginator-prev:not(.p-disabled):hover),
|
||
.mcr-table :deep(.p-paginator-next:not(.p-disabled):hover),
|
||
.mcr-table :deep(.p-paginator-last:not(.p-disabled):hover),
|
||
.mcr-table :deep(.p-paginator-page:not(.p-paginator-page-selected):hover) {
|
||
background: var(--m-bg-soft-hover);
|
||
border-color: var(--m-border-strong);
|
||
}
|
||
.mcr-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;
|
||
}
|
||
.mcr-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;
|
||
}
|
||
|
||
/* ─── Grid view ───────────────────────────────────────────────
|
||
Cards num CSS grid responsivo (auto-fill 220px+). Cada card tem
|
||
avatar + badge no topo, nome em destaque, contato e tempo abaixo.
|
||
Border-left colorido por status (mesmo padrão da list view). */
|
||
.mcr-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;
|
||
}
|
||
.mcr-grid {
|
||
flex: 1;
|
||
min-height: 0;
|
||
overflow-y: auto;
|
||
padding: 12px;
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||
gap: 10px;
|
||
align-content: start;
|
||
}
|
||
.mcr-grid::-webkit-scrollbar { width: 5px; }
|
||
.mcr-grid::-webkit-scrollbar-thumb { background: var(--m-border-strong); border-radius: 3px; }
|
||
.mcr-grid__loading {
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 32px;
|
||
}
|
||
.mcr-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;
|
||
}
|
||
.mcr-grid__card:hover {
|
||
background: var(--m-bg-soft-hover);
|
||
border-color: var(--m-border-strong);
|
||
transform: translateY(-1px);
|
||
}
|
||
.mcr-grid__card:focus-visible {
|
||
outline: 2px solid var(--p-primary-color);
|
||
outline-offset: 2px;
|
||
}
|
||
.mcr-grid__card.is-new { border-left-color: rgb(37, 99, 235); }
|
||
.mcr-grid__card.is-done { border-left-color: rgb(22, 163, 74); }
|
||
.mcr-grid__card.is-rejected { border-left-color: rgb(220, 38, 38); opacity: 0.85; }
|
||
.mcr-grid__top {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 8px;
|
||
}
|
||
/* Cluster badge + pencil à direita do top do card. */
|
||
.mcr-grid__top-right {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
.mcr-grid__name {
|
||
font-size: 0.92rem;
|
||
font-weight: 600;
|
||
color: var(--m-text);
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
.mcr-grid__meta {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
font-size: 0.75rem;
|
||
color: var(--m-text-muted);
|
||
min-width: 0;
|
||
}
|
||
.mcr-grid__meta i { margin-right: 5px; font-size: 0.7rem; }
|
||
.mcr-grid__meta span {
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
.mcr-grid__time {
|
||
font-size: 0.7rem;
|
||
color: var(--m-text-faint);
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
margin-top: auto;
|
||
}
|
||
.mcr-grid__time i { font-size: 0.65rem; }
|
||
|
||
/* Paginator standalone (grid view) — espelha o estilo do paginator
|
||
interno do DataTable, mas via classe direta (sem :deep). Centralizado
|
||
e sem bg de surface (consistente com o pedido de "fundo transparente"). */
|
||
.mcr-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;
|
||
}
|
||
.mcr-paginator.p-paginator .p-paginator-current {
|
||
color: var(--m-text-muted);
|
||
font-size: 0.78rem;
|
||
background: transparent;
|
||
border: none;
|
||
padding: 0 6px;
|
||
}
|
||
.mcr-paginator.p-paginator .p-paginator-first,
|
||
.mcr-paginator.p-paginator .p-paginator-prev,
|
||
.mcr-paginator.p-paginator .p-paginator-next,
|
||
.mcr-paginator.p-paginator .p-paginator-last,
|
||
.mcr-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;
|
||
}
|
||
.mcr-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);
|
||
}
|
||
.mcr-paginator.p-paginator .p-paginator-first:not(.p-disabled):hover,
|
||
.mcr-paginator.p-paginator .p-paginator-prev:not(.p-disabled):hover,
|
||
.mcr-paginator.p-paginator .p-paginator-next:not(.p-disabled):hover,
|
||
.mcr-paginator.p-paginator .p-paginator-last:not(.p-disabled):hover,
|
||
.mcr-paginator.p-paginator .p-paginator-page:not(.p-paginator-page-selected):hover {
|
||
background: var(--m-bg-soft-hover);
|
||
border-color: var(--m-border-strong);
|
||
}
|
||
.mcr-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;
|
||
}
|
||
.mcr-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;
|
||
}
|
||
|
||
/* Conteúdo das células — substitui o antigo .mcr-card__* (interno). */
|
||
.mcr-row__patient {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
min-width: 0;
|
||
}
|
||
.mcr-row__patient-text {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
min-width: 0;
|
||
}
|
||
.mcr-row__name {
|
||
font-size: 0.88rem;
|
||
font-weight: 600;
|
||
color: var(--m-text);
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
.mcr-row__contact {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
font-size: 0.78rem;
|
||
color: var(--m-text-muted);
|
||
min-width: 0;
|
||
}
|
||
.mcr-row__contact i { margin-right: 5px; font-size: 0.7rem; }
|
||
.mcr-row__contact span {
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
.mcr-row__empty { color: var(--m-text-faint); }
|
||
.mcr-row__time {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
font-size: 0.78rem;
|
||
color: var(--m-text-muted);
|
||
}
|
||
.mcr-row__time i { font-size: 0.7rem; }
|
||
|
||
/* Botão de ação (pencil) — ícone primary + bg matching da célula
|
||
pra não ficar "vazado" durante scroll horizontal (a célula frozen
|
||
da Ação usa --m-bg-medium; o botão herda o mesmo token, que adapta
|
||
a dark/light automaticamente). @click.stop no template impede
|
||
propagar pro row-click do DataTable. */
|
||
.mcr-row__action {
|
||
width: 30px;
|
||
height: 30px;
|
||
display: grid;
|
||
place-items: center;
|
||
background: var(--p-content-background);
|
||
border: 1px solid color-mix(in srgb, var(--p-primary-color) 30%, var(--m-border));
|
||
color: var(--p-primary-color);
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
transition: background-color 140ms ease, border-color 140ms ease, color 140ms ease;
|
||
}
|
||
.mcr-row__action: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);
|
||
}
|
||
.mcr-row__action > i { font-size: 0.78rem; }
|
||
|
||
/* Coluna "Ação" frozen à direita — bg sólido em ambos os modos
|
||
(--p-content-background): branco no light, surface configurada
|
||
no dark. Mesma referência visual que o header e os cards. */
|
||
.mcr-table :deep(td.p-datatable-frozen-column),
|
||
.mcr-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;
|
||
}
|
||
.mcr-table :deep(.p-datatable-tbody > tr:hover td.p-datatable-frozen-column) {
|
||
background: var(--m-bg-soft-hover);
|
||
}
|
||
.mcr-table :deep(.p-datatable-tbody > tr.p-datatable-row-selected td.p-datatable-frozen-column) {
|
||
background: var(--m-accent-soft);
|
||
}
|
||
|
||
/* Avatar — usado no row da tabela (modificador --sm) e no dialog (size full). */
|
||
.mcr-card__avatar {
|
||
width: 40px; height: 40px;
|
||
border-radius: 50%;
|
||
background: var(--m-accent-strong);
|
||
border: 1px solid var(--m-accent);
|
||
color: white;
|
||
font-size: 0.78rem;
|
||
font-weight: 600;
|
||
display: grid; place-items: center;
|
||
flex-shrink: 0;
|
||
overflow: hidden;
|
||
}
|
||
.mcr-card__avatar img { width: 100%; height: 100%; object-fit: cover; }
|
||
.mcr-card__avatar--sm {
|
||
width: 32px; height: 32px;
|
||
font-size: 0.7rem;
|
||
}
|
||
|
||
.mcr-card__badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
font-size: 0.62rem;
|
||
font-weight: 600;
|
||
padding: 2px 8px;
|
||
border-radius: 999px;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.04em;
|
||
border: 1px solid;
|
||
}
|
||
.mcr-card__badge.is-new {
|
||
color: rgb(37, 99, 235);
|
||
background: rgba(37, 99, 235, 0.12);
|
||
border-color: rgba(37, 99, 235, 0.3);
|
||
}
|
||
.mcr-card__badge.is-done {
|
||
color: rgb(22, 163, 74);
|
||
background: rgba(22, 163, 74, 0.12);
|
||
border-color: rgba(22, 163, 74, 0.3);
|
||
}
|
||
.mcr-card__badge.is-rejected {
|
||
color: rgb(220, 38, 38);
|
||
background: rgba(220, 38, 38, 0.12);
|
||
border-color: rgba(220, 38, 38, 0.3);
|
||
}
|
||
.mcr-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;
|
||
}
|
||
.mcr-empty__icon { font-size: 2rem; color: var(--m-text-faint); margin-bottom: 4px; }
|
||
.mcr-empty__title { font-size: 0.92rem; font-weight: 600; color: var(--m-text); }
|
||
.mcr-empty__hint { font-size: 0.78rem; }
|
||
|
||
/* Dialog avatar */
|
||
.mcr-dlg__avatar {
|
||
width: 56px; height: 56px;
|
||
border-radius: 50%;
|
||
background: var(--m-accent-strong);
|
||
border: 1px solid var(--m-accent);
|
||
color: white;
|
||
font-size: 1rem;
|
||
font-weight: 600;
|
||
display: grid; place-items: center;
|
||
flex-shrink: 0;
|
||
overflow: hidden;
|
||
}
|
||
.mcr-dlg__avatar img { width: 100%; height: 100%; object-fit: cover; }
|
||
|
||
/* Drawer mobile */
|
||
.mcr-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;
|
||
}
|
||
.mcr-mobile-drawer.is-open { transform: translateX(0); }
|
||
.mcr-mobile-drawer__scroll {
|
||
height: 100%;
|
||
overflow-y: auto;
|
||
padding: 12px 12px 24px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
.mcr-mobile-drawer__scroll::-webkit-scrollbar { width: 5px; }
|
||
.mcr-mobile-drawer__scroll::-webkit-scrollbar-thumb {
|
||
background: var(--m-border-strong);
|
||
border-radius: 3px;
|
||
}
|
||
/* Reset da .mcr-side quando teleportada pro drawer mobile — o drawer
|
||
já tem bg/border próprios (.mcr-mobile-drawer), então a sidebar não
|
||
precisa do bg/border-right que tem em desktop. */
|
||
.mcr-mobile-drawer__scroll .mcr-side {
|
||
width: 100%;
|
||
flex: 1;
|
||
min-height: 0;
|
||
overflow: visible;
|
||
padding: 0;
|
||
background: transparent;
|
||
border-right: none;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
.mcr-mobile-drawer__scroll .mcr-w--side {
|
||
margin: 0 0 12px;
|
||
}
|
||
.mcr-mobile-drawer__scroll .mcr-w--side:last-of-type {
|
||
margin-bottom: 0;
|
||
}
|
||
.mcr-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;
|
||
}
|
||
.mcr-drawer-fade-enter-active,
|
||
.mcr-drawer-fade-leave-active { transition: opacity 200ms ease; }
|
||
.mcr-drawer-fade-enter-from,
|
||
.mcr-drawer-fade-leave-to { opacity: 0; }
|
||
|
||
@media (max-width: 1023px) {
|
||
/* Em mobile, a sidebar é teleportada pro drawer (mcr-mobile-drawer),
|
||
então .mcr-body só contém .mcr-main. Padding de 0 no body, padding
|
||
no .mcr-main já cobre o spacing interno. */
|
||
.mcr-body { flex-direction: column; padding: 0; }
|
||
.mcr-main { width: 100%; padding: 8px; }
|
||
.mcr-page__title > span:first-of-type { display: none; }
|
||
.mcr-page__title-icon { display: none; }
|
||
.mcr-menu-btn--mobile-only { display: inline-flex; }
|
||
/* Em mobile mostra todas as colunas — scroll horizontal habilitado
|
||
via tableStyle min-width:640px. Coluna "Ação" frozen à direita
|
||
continua fixa enquanto o user scrolla horizontalmente. */
|
||
}
|
||
|
||
/* ═══════ Fake dialog: largura adaptativa (>=1024px) ═══════
|
||
Espelha pattern de MelissaAgendador/Negocio — fica mesma janela,
|
||
drawer a esquerda, sobra espaco a direita pra dock e contexto.
|
||
- 1024–1012px : full-width (right: 6px) — overlap minimo
|
||
- 1012–2012px : width = 1000px fixo (right cresce com viewport)
|
||
- >= 2012px : width = ~50% do viewport (right: 50%) */
|
||
@media (min-width: 1024px) {
|
||
.mcr-page {
|
||
right: max(6px, min(50%, calc(100% - 1006px)));
|
||
}
|
||
}
|
||
</style>
|