86311ef305
Sprints 04-29 + 04-30 acumuladas. - MelissaConfiguracoes: hub 2-col com 6 grupos (Layout/Conta/Agenda/ Financeiro/WhatsApp/Sistema), tudo embedado via MelissaEmbed. - MelissaEmbed: wrapper generico que injeta layout-variant=melissa e remove cromos pra reaproveitar Pages tradicionais. - 9 Melissa Pages novas: CadastrosRecebidos, Compromissos, Configuracoes, Conversas, Embed, Grupos, Medicos, Recorrencias, Tags. - Dialog blueprint atualizado: bg-gray-100 (hardcoded light) -> bg-[var(--surface-ground)] (tema-aware). 22 dialogs migrados em 9 arquivos. Anti-pattern documentado. - PatientsCadastroPage: bug fix dropdown Grupo (optionLabel nome->name), toggle vertical/abas com persist localStorage, sticky margin-top. - Surface picker no popover do MelissaLayout (8 swatches). - useTopbarPlanMenu, useMelissaWhatsapp, useMelissaPacientesAside novos. - Migration: status agenda remarcado/confirmado. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1174 lines
44 KiB
Vue
1174 lines
44 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';
|
||
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 ? 'warn' : 'neutral' },
|
||
{ key: 'converted', label: 'Convertidos', value: c, cls: 'ok' },
|
||
{ key: 'rejected', label: 'Rejeitados', value: r, cls: '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 (default 10 por página) ──────────
|
||
// `fetchIntakes` traz a lista completa de patient_intake_requests do
|
||
// tenant — paginamos no client. Volume típico é baixo (cadastros
|
||
// recebidos via formulário externo), mas o Paginator + dropdown dão
|
||
// controle pro usuário caso a fila esteja grande.
|
||
const PAGE_SIZE_OPTIONS = [10, 20, 50, 100];
|
||
const pageMCR = ref(1);
|
||
const rowsMCR = ref(10);
|
||
const filteredPaginated = computed(() => {
|
||
const start = (pageMCR.value - 1) * rowsMCR.value;
|
||
return filtered.value.slice(start, start + rowsMCR.value);
|
||
});
|
||
function onPageMCRChange(event) {
|
||
pageMCR.value = event.page + 1;
|
||
rowsMCR.value = event.rows;
|
||
}
|
||
// Reset pra página 1 quando busca/status mudam.
|
||
watch([busca, statusFilter], () => { pageMCR.value = 1; });
|
||
// Clamp se a lista encolher (ex.: depois de converter/rejeitar e refetch).
|
||
watch(() => filtered.value.length, (len) => {
|
||
const maxPag = Math.max(1, Math.ceil(len / rowsMCR.value));
|
||
if (pageMCR.value > maxPag) pageMCR.value = maxPag;
|
||
});
|
||
|
||
// ── 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];
|
||
});
|
||
|
||
const { data: created, error: insErr } = await supabase.from('patients')
|
||
.insert(patientPayload).select('id').single();
|
||
if (insErr) throw insErr;
|
||
const patientId = created?.id;
|
||
if (!patientId) throw new Error('Falha ao obter ID do paciente.');
|
||
|
||
const { error: upErr } = await supabase.from('patient_intake_requests')
|
||
.update({ status: 'converted', converted_patient_id: patientId, updated_at: new Date().toISOString() })
|
||
.eq('id', item.id);
|
||
if (upErr) throw upErr;
|
||
|
||
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 text-amber-300" />
|
||
<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>
|
||
|
||
<div class="mcr-body">
|
||
<Teleport to="#mcr-mobile-drawer-target" :disabled="!isMobile">
|
||
<aside class="mcr-side">
|
||
<!-- Stats -->
|
||
<div class="mcr-w">
|
||
<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">
|
||
<div class="mcr-w__head">
|
||
<span class="mcr-w__title"><i class="pi pi-filter" /> Status</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>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Busca -->
|
||
<div class="mcr-w">
|
||
<div class="mcr-w__head">
|
||
<span class="mcr-w__title"><i class="pi pi-search" /> Buscar</span>
|
||
</div>
|
||
<input
|
||
v-model="busca"
|
||
type="text"
|
||
placeholder="Nome, email ou telefone…"
|
||
class="mcr-search__input"
|
||
/>
|
||
</div>
|
||
</aside>
|
||
</Teleport>
|
||
|
||
<div class="mcr-main">
|
||
<div class="mcr-list">
|
||
<template v-if="carregandoInicial">
|
||
<div v-for="i in 5" :key="`csk-${i}`" class="mcr-card mcr-card--skeleton" aria-busy="true">
|
||
<span class="mcr-card__avatar melissa-skeleton melissa-skeleton--avatar" />
|
||
<div style="flex:1; display:flex; flex-direction:column; gap:6px;">
|
||
<span class="melissa-skeleton melissa-skeleton--title" :style="{ width: `${50 + (i * 9) % 30}%` }" />
|
||
<span class="melissa-skeleton melissa-skeleton--text" style="width: 60%;" />
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<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>
|
||
|
||
<button
|
||
v-for="r in filteredPaginated"
|
||
v-else
|
||
:key="r.id"
|
||
class="mcr-card"
|
||
:class="statusClass(r.status)"
|
||
@click="openDetails(r)"
|
||
>
|
||
<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-card__main">
|
||
<div class="mcr-card__name-row">
|
||
<span class="mcr-card__name">{{ fNome(r) || '—' }}</span>
|
||
<span class="mcr-card__badge" :class="statusClass(r.status)">
|
||
{{ statusLabel(r.status) }}
|
||
</span>
|
||
</div>
|
||
<div class="mcr-card__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>
|
||
<div class="mcr-card__time">
|
||
<i class="pi pi-clock" /> {{ fmtRelative(r.created_at) }}
|
||
</div>
|
||
</button>
|
||
</div>
|
||
|
||
<Paginator
|
||
v-if="!carregandoInicial && filtered.length > 0"
|
||
class="mcr-paginator"
|
||
:rows="rowsMCR"
|
||
:totalRecords="filtered.length"
|
||
:first="(pageMCR - 1) * rowsMCR"
|
||
:rowsPerPageOptions="PAGE_SIZE_OPTIONS"
|
||
template="CurrentPageReport FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown"
|
||
currentPageReportTemplate="{first}–{last} de {totalRecords}"
|
||
@page="onPageMCRChange"
|
||
/>
|
||
</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;
|
||
}
|
||
.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);
|
||
}
|
||
|
||
.mcr-body {
|
||
flex: 1;
|
||
display: flex;
|
||
min-height: 0;
|
||
gap: 12px;
|
||
padding: 12px;
|
||
}
|
||
.mcr-side {
|
||
width: 280px;
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
overflow-y: auto;
|
||
}
|
||
.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 página/dialog (ambos --m-bg-soft). */
|
||
.mcr-w {
|
||
background: var(--m-bg-medium);
|
||
border: 1px solid var(--m-border);
|
||
border-radius: 12px;
|
||
padding: 12px;
|
||
}
|
||
.mcr-w__head { margin-bottom: 10px; }
|
||
.mcr-w__title {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: 0.78rem;
|
||
font-weight: 600;
|
||
}
|
||
.mcr-w__title > i { color: var(--m-text-muted); font-size: 0.78rem; }
|
||
|
||
.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;
|
||
}
|
||
.mcr-stat.is-warn .mcr-stat__val { color: rgb(251, 191, 36); }
|
||
.mcr-stat.is-ok .mcr-stat__val { color: rgb(74, 222, 128); }
|
||
|
||
.mcr-side__list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
.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;
|
||
}
|
||
.mcr-side__item:hover { background: var(--m-bg-soft-hover); }
|
||
.mcr-side__item.is-active.is-new {
|
||
background: rgba(251, 191, 36, 0.08);
|
||
border-color: rgba(251, 191, 36, 0.35);
|
||
}
|
||
.mcr-side__item.is-active.is-converted {
|
||
background: rgba(74, 222, 128, 0.08);
|
||
border-color: rgba(74, 222, 128, 0.35);
|
||
}
|
||
.mcr-side__item.is-active.is-rejected {
|
||
background: rgba(248, 113, 113, 0.08);
|
||
border-color: rgba(248, 113, 113, 0.35);
|
||
}
|
||
.mcr-side__item > i { font-size: 0.75rem; color: var(--m-text-muted); width: 14px; text-align: center; }
|
||
.mcr-side__item.is-active.is-new > i { color: rgb(251, 191, 36); }
|
||
.mcr-side__item.is-active.is-converted > i { color: rgb(74, 222, 128); }
|
||
.mcr-side__item.is-active.is-rejected > i { color: rgb(248, 113, 113); }
|
||
|
||
.mcr-search__input {
|
||
width: 100%;
|
||
/* Mais sutil que o card-pai pra ler como input dentro do card. */
|
||
background: var(--m-bg-soft);
|
||
border: 1px solid var(--m-border);
|
||
color: var(--m-text);
|
||
padding: 8px 12px;
|
||
border-radius: 9px;
|
||
font-size: 0.82rem;
|
||
font-family: inherit;
|
||
outline: none;
|
||
}
|
||
.mcr-search__input:focus { border-color: var(--m-border-strong); background: var(--m-bg-soft-hover); }
|
||
|
||
.mcr-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
|
||
.mcr-list {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding: 0 4px 4px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
.mcr-list::-webkit-scrollbar { width: 5px; }
|
||
.mcr-list::-webkit-scrollbar-thumb {
|
||
background: var(--m-border-strong);
|
||
border-radius: 3px;
|
||
}
|
||
|
||
/* Paginator no rodapé da col 2 — fixo (fora do scroll), com seletor de
|
||
tamanho (10/20/50/100). Mesma estética do .mp-paginator (MelissaPacientes). */
|
||
.mcr-paginator.p-paginator {
|
||
background: transparent;
|
||
border: none;
|
||
border-top: 1px solid var(--m-border);
|
||
padding: 8px 12px;
|
||
margin-top: 4px;
|
||
justify-content: space-between;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
}
|
||
.mcr-paginator.p-paginator .p-paginator-current {
|
||
color: var(--m-text-muted);
|
||
font-size: 0.78rem;
|
||
background: transparent;
|
||
border: none;
|
||
padding: 0 4px;
|
||
height: 30px;
|
||
min-width: auto;
|
||
}
|
||
.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-paginator-first.p-disabled,
|
||
.mcr-paginator.p-paginator .p-paginator-prev.p-disabled,
|
||
.mcr-paginator.p-paginator .p-paginator-next.p-disabled,
|
||
.mcr-paginator.p-paginator .p-paginator-last.p-disabled {
|
||
opacity: 0.35;
|
||
}
|
||
.mcr-paginator.p-paginator .p-select {
|
||
background: var(--m-bg-soft);
|
||
border: 1px solid var(--m-border);
|
||
border-radius: 8px;
|
||
height: 30px;
|
||
font-size: 0.78rem;
|
||
color: var(--m-text);
|
||
}
|
||
.mcr-paginator.p-paginator .p-select:hover { border-color: var(--m-border-strong); }
|
||
.mcr-paginator.p-paginator .p-select-label {
|
||
padding: 0 8px;
|
||
color: var(--m-text);
|
||
font-size: 0.78rem;
|
||
}
|
||
|
||
.mcr-card {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 12px 14px;
|
||
background: var(--m-bg-soft);
|
||
border: 1px solid var(--m-border);
|
||
border-radius: 10px;
|
||
cursor: pointer;
|
||
transition: background-color 140ms ease, border-color 140ms ease, transform 140ms ease;
|
||
text-align: left;
|
||
font-family: inherit;
|
||
color: var(--m-text);
|
||
}
|
||
.mcr-card:hover {
|
||
background: var(--m-bg-soft-hover);
|
||
border-color: var(--m-border-strong);
|
||
transform: translateY(-1px);
|
||
}
|
||
.mcr-card.is-new { border-left: 3px solid rgb(251, 191, 36); }
|
||
.mcr-card.is-done { opacity: 0.75; }
|
||
.mcr-card.is-rejected { opacity: 0.55; }
|
||
.mcr-card--skeleton { cursor: default; pointer-events: none; opacity: 0.95; }
|
||
.mcr-card--skeleton:hover { background: var(--m-bg-soft); transform: none; }
|
||
|
||
.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__main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
|
||
.mcr-card__name-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
}
|
||
.mcr-card__name {
|
||
font-size: 0.92rem;
|
||
font-weight: 600;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
.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(251, 191, 36);
|
||
background: rgba(251, 191, 36, 0.12);
|
||
border-color: rgba(251, 191, 36, 0.3);
|
||
}
|
||
.mcr-card__badge.is-done {
|
||
color: rgb(74, 222, 128);
|
||
background: rgba(74, 222, 128, 0.12);
|
||
border-color: rgba(74, 222, 128, 0.3);
|
||
}
|
||
.mcr-card__badge.is-rejected {
|
||
color: rgb(248, 113, 113);
|
||
background: rgba(248, 113, 113, 0.12);
|
||
border-color: rgba(248, 113, 113, 0.3);
|
||
}
|
||
.mcr-card__meta {
|
||
display: flex;
|
||
gap: 14px;
|
||
margin-top: 4px;
|
||
font-size: 0.7rem;
|
||
color: var(--m-text-muted);
|
||
}
|
||
.mcr-card__meta i { margin-right: 5px; font-size: 0.65rem; }
|
||
.mcr-card__time {
|
||
flex-shrink: 0;
|
||
font-size: 0.7rem;
|
||
color: var(--m-text-muted);
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
.mcr-card__time i { font-size: 0.65rem; }
|
||
|
||
.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;
|
||
}
|
||
.mcr-mobile-drawer__scroll .mcr-side {
|
||
width: 100%;
|
||
height: auto;
|
||
overflow: visible;
|
||
padding: 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) {
|
||
.mcr-body { flex-direction: column; padding: 8px; }
|
||
.mcr-main { width: 100%; }
|
||
.mcr-page__title > span:first-of-type { display: none; }
|
||
.mcr-menu-btn--mobile-only { display: inline-flex; }
|
||
.mcr-card__time { display: none; }
|
||
}
|
||
</style>
|