riseup-squad19/Lista de pacientes.html
2025-08-16 07:56:07 -03:00

704 lines
33 KiB
HTML

<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Lista de Pacientes</title>
<style>
:root {
--bg: #0f172a; /* slate-900 */
--panel: #111827; /* gray-900 */
--muted: #1f2937; /* gray-800 */
--border: #374151; /* gray-700 */
--text: #e5e7eb; /* gray-200 */
--text-dim: #9ca3af; /* gray-400 */
--accent: #22d3ee; /* cyan-400 */
--accent-2: #60a5fa; /* blue-400 */
--danger: #ef4444;
--success: #22c55e;
--warning: #f59e0b;
}
* { box-sizing: border-box }
html, body { height: 100% }
body {
margin: 0;
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji";
background: linear-gradient(180deg, #0b1221, var(--bg));
color: var(--text);
}
.container { max-width: 1200px; margin: 24px auto; padding: 0 16px; }
header { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 16px; }
.title { font-size: clamp(20px, 4vw, 28px); font-weight: 800; letter-spacing: .4px }
.actions { display: flex; gap: 8px; align-items: center; }
.btn {
background: linear-gradient(180deg, var(--accent), var(--accent-2));
border: none; color: #04121c; font-weight: 700; padding: 10px 14px; border-radius: 12px; cursor: pointer;
box-shadow: 0 8px 24px rgba(34,211,238,.2);
}
.btn.secondary { background: #111827; color: var(--text); border: 1px solid var(--border); box-shadow: none }
.btn.ghost { background: transparent; color: var(--text); border: 1px dashed var(--border) }
.btn.danger { background: var(--danger); color: white; box-shadow: 0 8px 24px rgba(239,68,68,.25) }
.toolbar {
display: grid; gap: 8px; grid-template-columns: 1fr repeat(5, max-content);
align-items: center; background: rgba(17,24,39,.6); border: 1px solid var(--border); border-radius: 16px; padding: 12px;
position: sticky; top: 12px; z-index: 5; backdrop-filter: blur(8px);
}
.search { position: relative; display: flex; align-items: center; }
.search input { width: 100%; padding: 12px 40px; border-radius: 12px; border: 1px solid var(--border); background: #0b1221; color: var(--text); }
.search .icon { position: absolute; left: 12px; font-size: 18px; color: var(--text-dim) }
.search .clear { position: absolute; right: 8px; background: transparent; border: none; color: var(--text-dim); cursor: pointer }
.chip, select, .date-input, .month-input {
background: #0b1221; color: var(--text); border: 1px solid var(--border); border-radius: 12px; padding: 10px 12px; cursor: pointer;
}
.toggle { display: inline-flex; gap: 8px; align-items: center; font-size: 14px; color: var(--text-dim) }
.table { margin-top: 12px; background: rgba(17,24,39,.6); border: 1px solid var(--border); border-radius: 16px; overflow: hidden; }
table { width: 100%; border-collapse: collapse; }
thead th {
text-align: left; font-size: 12px; text-transform: uppercase; letter-spacing: .08em; color: var(--text-dim); background: #0b1221;
padding: 12px; border-bottom: 1px solid var(--border);
position: sticky; top: 64px; z-index: 2; /* stays below toolbar */
}
tbody tr { border-bottom: 1px solid var(--border); }
tbody tr:hover { background: rgba(96,165,250,.06) }
td { padding: 12px; vertical-align: middle }
a.name-link { color: white; font-weight: 700; text-decoration: none }
a.name-link:hover { text-decoration: underline; color: var(--accent) }
.status.vip { background: rgba(250,204,21,.15); color: #facc15; padding: 4px 8px; border-radius: 999px; font-size: 12px; border: 1px solid rgba(250,204,21,.25) }
.status.no-apt { color: var(--text-dim); font-style: italic }
.row-actions { position: relative }
.menu-btn { background: #0b1221; border: 1px solid var(--border); color: var(--text); border-radius: 10px; padding: 8px 10px; cursor: pointer }
.dropdown { position: absolute; right: 0; top: 44px; background: #0b1221; border: 1px solid var(--border); border-radius: 12px; min-width: 220px; box-shadow: 0 8px 32px rgba(0,0,0,.4); display: none }
.dropdown.open { display: block }
.dropdown button { display: block; width: 100%; text-align: left; padding: 10px 12px; background: transparent; border: none; color: var(--text); cursor: pointer }
.dropdown button:hover { background: rgba(96,165,250,.12) }
.dropdown .danger { color: #fecaca }
.empty { text-align: center; padding: 40px; color: var(--text-dim) }
/* Modals */
dialog { border: 1px solid var(--border); border-radius: 16px; background: #0b1221; color: var(--text); padding: 0; width: min(720px, 92vw); }
dialog::backdrop { background: rgba(0,0,0,.6); backdrop-filter: blur(2px) }
.modal-header { display: flex; justify-content: space-between; align-items: center; padding: 16px; border-bottom: 1px solid var(--border) }
.modal-body { padding: 16px; max-height: 70vh; overflow: auto }
.modal-footer { display: flex; justify-content: flex-end; gap: 8px; padding: 16px; border-top: 1px solid var(--border) }
form.grid { display: grid; grid-template-columns: repeat(12, 1fr); gap: 12px }
.col-6 { grid-column: span 6 }
.col-4 { grid-column: span 4 }
.col-3 { grid-column: span 3 }
.col-12 { grid-column: span 12 }
label { display: block; font-size: 12px; color: var(--text-dim); margin-bottom: 6px }
input[type="text"], input[type="tel"], input[type="date"], input[type="datetime-local"], select, textarea, input[type="number"] { width: 100%; padding: 10px 12px; border-radius: 10px; border: 1px solid var(--border); background: #0b1221; color: var(--text); }
.switch { display: inline-flex; align-items: center; gap: 8px }
.pill { display: inline-flex; gap: 6px; align-items: center; border: 1px solid var(--border); background: #0b1221; padding: 6px 10px; border-radius: 999px; font-size: 12px; color: var(--text-dim) }
/* Infinite loader */
.loader { text-align: center; padding: 18px; color: var(--text-dim) }
@media (max-width: 860px) {
.toolbar { grid-template-columns: 1fr 1fr 1fr; }
thead { display: none }
table, tbody, tr, td { display: block; width: 100% }
tbody tr { border: 1px solid var(--border); margin: 10px; border-radius: 12px; padding: 8px }
td { padding: 6px 8px }
td[data-col]::before { content: attr(data-col) ": "; color: var(--text-dim); font-size: 12px; text-transform: uppercase; display: inline-block; width: 140px }
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="title">Lista de Pacientes</div>
<div class="actions">
<button class="btn secondary" id="advancedFilterBtn" aria-haspopup="dialog">Filtro avançado</button>
<button class="btn" id="addBtn" aria-haspopup="dialog">Adicionar</button>
</div>
</header>
<section class="toolbar" aria-label="Barra de pesquisa e filtros">
<div class="search" style="grid-column: span 1">
<span class="icon">🔎</span>
<input id="searchInput" type="text" placeholder="Pesquisar por nome ou documento…" autocomplete="off" aria-label="Pesquisar por nome ou documento" />
<button class="clear" id="clearSearch" title="Limpar" aria-label="Limpar busca"></button>
</div>
<select id="convenioFilter" title="Convênio" aria-label="Filtrar por convênio">
<option value="">Convênio (todos)</option>
<option>Particular</option>
<option>Unimed</option>
<option>Amil</option>
<option>Bradesco</option>
<option>Hapvida</option>
</select>
<label class="toggle" title="VIP">
<input type="checkbox" id="vipFilter" /> VIP
</label>
<select id="aniversarioTipo" title="Aniversariantes: por mês ou data específica" aria-label="Escolher tipo de filtro de aniversariantes">
<option value="">Aniversariantes</option>
<option value="mes">Por mês</option>
<option value="data">Por data</option>
</select>
<input type="month" id="aniversarioMes" class="month-input" style="display:none" aria-label="Selecionar mês de aniversário" />
<input type="date" id="aniversarioData" class="date-input" style="display:none" aria-label="Selecionar data exata de aniversário" />
</section>
<section class="table" role="region" aria-labelledby="listaTitulo">
<table aria-describedby="listaDesc">
<caption id="listaDesc" class="sr-only" style="display:none">Lista de pacientes com pesquisa, filtros e ações por linha.</caption>
<thead>
<tr>
<th>Nome</th>
<th>Telefone</th>
<th>Cidade/UF</th>
<th>Último atendimento</th>
<th>Próximo atendimento</th>
<th style="width:1%"></th>
</tr>
</thead>
<tbody id="tbody"></tbody>
</table>
<div id="loader" class="loader">Carregando…</div>
<div id="empty" class="empty" style="display:none">Nenhum paciente encontrado.</div>
</section>
</div>
<!-- Modal: Filtro Avançado -->
<dialog id="advancedFilterModal" aria-labelledby="afTitle">
<div class="modal-header">
<h3 id="afTitle">Filtro avançado</h3>
<button class="btn ghost" id="afClose">Fechar</button>
</div>
<div class="modal-body">
<form class="grid" id="advancedForm" onsubmit="return false;">
<div class="col-6">
<label>Cidade</label>
<input type="text" id="afCidade" placeholder="Ex.: Maceió" />
</div>
<div class="col-6">
<label>Estado (UF)</label>
<select id="afUF">
<option value="">Todos</option>
<option>AL</option><option>PE</option><option>BA</option><option>SE</option><option>PB</option><option>RN</option><option>CE</option>
<option>SP</option><option>RJ</option><option>MG</option><option>PR</option><option>SC</option><option>RS</option>
</select>
</div>
<div class="col-6">
<label>Idade (mín.)</label>
<input type="number" id="afIdadeMin" min="0" max="120" />
</div>
<div class="col-6">
<label>Idade (máx.)</label>
<input type="number" id="afIdadeMax" min="0" max="120" />
</div>
<div class="col-6">
<label>Último atendimento (de)</label>
<input type="date" id="afUltimoDe" />
</div>
<div class="col-6">
<label>Último atendimento (até)</label>
<input type="date" id="afUltimoAte" />
</div>
</form>
</div>
<div class="modal-footer">
<button class="btn secondary" id="afLimpar">Limpar</button>
<button class="btn" id="afAplicar">Aplicar</button>
</div>
</dialog>
<!-- Modal: Ver/Editar/Adicionar Paciente -->
<dialog id="patientModal" aria-labelledby="pmTitle">
<div class="modal-header">
<h3 id="pmTitle">Paciente</h3>
<div>
<span id="pmVipBadge" class="pill" title="VIP" style="display:none">⭐ VIP</span>
<button class="btn ghost" id="pmClose">Fechar</button>
</div>
</div>
<div class="modal-body">
<form class="grid" id="patientForm" onsubmit="return false;">
<input type="hidden" id="pmId" />
<div class="col-6">
<label>Nome completo</label>
<input type="text" id="pmNome" required />
</div>
<div class="col-3">
<label>Documento (CPF)</label>
<input type="text" id="pmDoc" placeholder="000.000.000-00" />
</div>
<div class="col-3">
<label>VIP?</label>
<label class="switch"><input type="checkbox" id="pmVip" /> <span>Marcar como VIP</span></label>
</div>
<div class="col-4">
<label>Telefone</label>
<input type="tel" id="pmFone" placeholder="(00) 00000-0000" />
</div>
<div class="col-4">
<label>Convênio</label>
<select id="pmConvenio">
<option>Particular</option><option>Unimed</option><option>Amil</option><option>Bradesco</option><option>Hapvida</option>
</select>
</div>
<div class="col-4">
<label>Data de nascimento</label>
<input type="date" id="pmNascimento" />
</div>
<div class="col-6">
<label>Cidade</label>
<input type="text" id="pmCidade" />
</div>
<div class="col-3">
<label>Estado (UF)</label>
<select id="pmUF">
<option>AL</option><option>PE</option><option>BA</option><option>SE</option><option>PB</option><option>RN</option><option>CE</option>
<option>SP</option><option>RJ</option><option>MG</option><option>PR</option><option>SC</option><option>RS</option>
</select>
</div>
<div class="col-3">
<label>Idade</label>
<input type="number" id="pmIdade" min="0" max="120" />
</div>
<div class="col-12">
<label>Observações</label>
<textarea id="pmObs" rows="3"></textarea>
</div>
<div class="col-6">
<label>Último atendimento</label>
<input type="datetime-local" id="pmUltimo" />
</div>
<div class="col-6">
<label>Próximo atendimento</label>
<input type="datetime-local" id="pmProximo" />
</div>
</form>
</div>
<div class="modal-footer">
<button class="btn secondary" id="pmCancel">Cancelar</button>
<button class="btn" id="pmSalvar">Salvar</button>
</div>
</dialog>
<!-- Modal: Marcar Consulta -->
<dialog id="scheduleModal" aria-labelledby="scTitle">
<div class="modal-header">
<h3 id="scTitle">Marcar consulta</h3>
<button class="btn ghost" id="scClose">Fechar</button>
</div>
<div class="modal-body">
<div class="pill" id="scPaciente"></div>
<div style="margin-top:12px">
<label for="scDate">Data e hora</label>
<input type="datetime-local" id="scDate" />
</div>
</div>
<div class="modal-footer">
<button class="btn secondary" id="scCancel">Cancelar</button>
<button class="btn" id="scSalvar">Salvar</button>
</div>
</dialog>
<script>
// ==== Utilidades ====
const fmtDate = (d, withTime=true) => {
if (!d) return '';
const date = (d instanceof Date) ? d : new Date(d);
try {
return new Intl.DateTimeFormat('pt-BR', withTime ? { dateStyle: 'short', timeStyle: 'short' } : { dateStyle: 'short' }).format(date);
} catch(e) { return date.toLocaleString('pt-BR'); }
};
const fmtPhoneBR = (v) => {
if (!v) return '';
const digits = (''+v).replace(/\D/g, '').slice(0,11);
if (digits.length <= 10) return digits.replace(/(\d{2})(\d{4})(\d{0,4})/, '($1) $2-$3');
return digits.replace(/(\d{2})(\d{5})(\d{0,4})/, '($1) $2-$3');
};
const maskPhoneInput = (el) => { el.addEventListener('input', () => { const c = el.selectionStart; const raw = el.value; const masked = fmtPhoneBR(raw); el.value = masked; }); };
const debounce = (fn, ms=250) => { let t; return (...args)=>{ clearTimeout(t); t=setTimeout(()=>fn(...args), ms); } };
// ==== Dados simulados/persistidos ====
const LS_KEY = 'lista_pacientes_data_v1';
const firstNames = ['Ana','Bruno','Carla','Diego','Eduarda','Felipe','Giovana','Henrique','Isabela','João','Karen','Leonardo','Mariana','Nuno','Olívia','Paulo','Queila','Rafael','Sofia','Tiago','Úrsula','Viviane','William','Xavier','Yasmin','Zeca'];
const lastNames = ['Almeida','Barros','Cardoso','Dias','Esteves','Farias','Gomes','Hernandes','Ibiapina','Jesus','Klein','Lima','Moraes','Nascimento','Oliveira','Pereira','Queiroz','Ramos','Silva','Teixeira','Uchoa','Vieira','Wagner','Ximenes','Yamamoto','Zanetti'];
const cidades = [
{cidade:'Maceió', uf:'AL'}, {cidade:'Arapiraca', uf:'AL'}, {cidade:'Recife', uf:'PE'}, {cidade:'Petrolina', uf:'PE'},
{cidade:'Salvador', uf:'BA'}, {cidade:'Aracaju', uf:'SE'}, {cidade:'João Pessoa', uf:'PB'}, {cidade:'Natal', uf:'RN'},
{cidade:'Fortaleza', uf:'CE'}, {cidade:'São Paulo', uf:'SP'}, {cidade:'Rio de Janeiro', uf:'RJ'}, {cidade:'Belo Horizonte', uf:'MG'}
];
const convenios = ['Particular','Unimed','Amil','Bradesco','Hapvida'];
const rand = (n) => Math.floor(Math.random()*n);
const sample = (arr) => arr[rand(arr.length)];
function makePatients(n=200){
const out=[];
const now = new Date();
for (let i=1;i<=n;i++){
const nome = `${sample(firstNames)} ${sample(lastNames)}`;
const cidadeUF = sample(cidades);
const idade = 5 + rand(90);
const nascimento = new Date(now.getFullYear()-idade, rand(12), 1+rand(28));
const lastDays = rand(360);
const nextDelta = rand(5)===0 ? null : rand(120) - 60; // pode não ter próximo
const lastAt = new Date(now); lastAt.setDate(now.getDate() - lastDays); lastAt.setHours(rand(10)+8, [0,15,30,45][rand(4)]);
const nextAt = nextDelta==null ? null : new Date(now.getFullYear(), now.getMonth(), now.getDate()+nextDelta, rand(10)+8, [0,30][rand(2)]);
const ddd = sample(['11','21','31','41','51','61','71','81','82']);
const fone = `${ddd}${90000+rand(9999)}${1000+rand(8999)}`;
const cpf = `${100+rand(800)}.${100+rand(800)}.${100+rand(800)}-${String(10+rand(90)).padStart(2,'0')}`;
out.push({
id: crypto.randomUUID(),
nome, doc: cpf, vip: Math.random()<0.12, telefone: fone,
cidade: cidadeUF.cidade, uf: cidadeUF.uf,
convenio: sample(convenios), idade, nascimento: nascimento.toISOString().slice(0,10),
ultimo: lastAt.toISOString(), proximo: nextAt ? nextAt.toISOString() : null,
obs: '', atendimentosVinculados: rand(4) // usado para validar exclusão
})
}
return out;
}
function loadData(){
try { const raw = localStorage.getItem(LS_KEY); if (raw) return JSON.parse(raw); } catch(e){}
const data = makePatients();
try { localStorage.setItem(LS_KEY, JSON.stringify(data)); } catch(e){}
return data;
}
function saveData(){ try { localStorage.setItem(LS_KEY, JSON.stringify(DATA)); } catch(e){} }
let DATA = loadData();
// ==== Estado e filtros ====
const state = {
q: '', convenio: '', vip: false,
aniversTipo: '', aniversMes: '', aniversData: '',
afCidade: '', afUF: '', idadeMin: '', idadeMax: '', ultimoDe: '', ultimoAte: '',
pageSize: 30, loaded: 0, view: []
};
// ==== Elementos ====
const tbody = document.getElementById('tbody');
const loader = document.getElementById('loader');
const empty = document.getElementById('empty');
// Toolbar elements
const searchInput = document.getElementById('searchInput');
const clearSearch = document.getElementById('clearSearch');
const convenioFilter = document.getElementById('convenioFilter');
const vipFilter = document.getElementById('vipFilter');
const aniversarioTipo = document.getElementById('aniversarioTipo');
const aniversarioMes = document.getElementById('aniversarioMes');
const aniversarioData = document.getElementById('aniversarioData');
// Modals
const advancedFilterModal = document.getElementById('advancedFilterModal');
const advancedFilterBtn = document.getElementById('advancedFilterBtn');
const afClose = document.getElementById('afClose');
const afCidade = document.getElementById('afCidade');
const afUF = document.getElementById('afUF');
const afIdadeMin = document.getElementById('afIdadeMin');
const afIdadeMax = document.getElementById('afIdadeMax');
const afUltimoDe = document.getElementById('afUltimoDe');
const afUltimoAte = document.getElementById('afUltimoAte');
const afAplicar = document.getElementById('afAplicar');
const afLimpar = document.getElementById('afLimpar');
const addBtn = document.getElementById('addBtn');
const patientModal = document.getElementById('patientModal');
const pmClose = document.getElementById('pmClose');
const pmCancel = document.getElementById('pmCancel');
const pmId = document.getElementById('pmId');
const pmNome = document.getElementById('pmNome');
const pmDoc = document.getElementById('pmDoc');
const pmVip = document.getElementById('pmVip');
const pmVipBadge = document.getElementById('pmVipBadge');
const pmFone = document.getElementById('pmFone');
const pmConvenio = document.getElementById('pmConvenio');
const pmNascimento = document.getElementById('pmNascimento');
const pmCidade = document.getElementById('pmCidade');
const pmUF = document.getElementById('pmUF');
const pmIdade = document.getElementById('pmIdade');
const pmObs = document.getElementById('pmObs');
const pmUltimo = document.getElementById('pmUltimo');
const pmProximo = document.getElementById('pmProximo');
const pmSalvar = document.getElementById('pmSalvar');
const scheduleModal = document.getElementById('scheduleModal');
const scClose = document.getElementById('scClose');
const scCancel = document.getElementById('scCancel');
const scPaciente = document.getElementById('scPaciente');
const scDate = document.getElementById('scDate');
const scSalvar = document.getElementById('scSalvar');
// ==== Renderização ====
function applyFilters(){
const q = state.q.trim().toLowerCase();
const res = DATA.filter(p => {
// search
if (q) {
const hay = `${p.nome} ${p.doc}`.toLowerCase();
if (!hay.includes(q)) return false;
}
// convenio
if (state.convenio && p.convenio !== state.convenio) return false;
// vip
if (state.vip && !p.vip) return false;
// aniversariantes
if (state.aniversTipo==='mes' && state.aniversMes) {
const month = new Date(p.nascimento).toISOString().slice(0,7); // yyyy-mm
if (month !== state.aniversMes) return false;
}
if (state.aniversTipo==='data' && state.aniversData) {
const md = new Date(p.nascimento); const sel = new Date(state.aniversData);
if (md.getDate() !== sel.getDate() || md.getMonth() !== sel.getMonth()) return false;
}
// advanced filters
if (state.afCidade && !p.cidade.toLowerCase().includes(state.afCidade.toLowerCase())) return false;
if (state.afUF && p.uf !== state.afUF) return false;
if (state.idadeMin && p.idade < Number(state.idadeMin)) return false;
if (state.idadeMax && p.idade > Number(state.idadeMax)) return false;
if (state.ultimoDe) {
const d = new Date(state.ultimoDe);
if (new Date(p.ultimo) < d) return false;
}
if (state.ultimoAte) {
const d = new Date(state.ultimoAte);
if (new Date(p.ultimo) > new Date(d.getFullYear(), d.getMonth(), d.getDate(), 23,59,59)) return false;
}
return true;
});
state.view = res.sort((a,b)=> a.nome.localeCompare(b.nome,'pt-BR'));
state.loaded = 0;
tbody.innerHTML = '';
empty.style.display = res.length? 'none':'block';
loader.style.display = res.length? 'block':'none';
loadMore();
}
function loadMore(){
const start = state.loaded;
const end = Math.min(state.loaded + state.pageSize, state.view.length);
if (start >= end) { loader.style.display = 'none'; return; }
const frag = document.createDocumentFragment();
for (let i = start; i < end; i++){
frag.appendChild(renderRow(state.view[i]));
}
tbody.appendChild(frag);
state.loaded = end;
loader.style.display = (state.loaded < state.view.length) ? 'block' : 'none';
}
function renderRow(p){
const tr = document.createElement('tr');
const tdNome = document.createElement('td');
tdNome.dataset.col = 'Nome';
const link = document.createElement('a');
link.href = '#'; link.className = 'name-link'; link.textContent = p.nome;
link.addEventListener('click', (e)=>{ e.preventDefault(); openPatient(p, true); });
tdNome.appendChild(link);
if (p.vip){ const s=document.createElement('span'); s.className='status vip'; s.textContent='VIP'; s.style.marginLeft='8px'; tdNome.appendChild(s); }
const tdFone = document.createElement('td'); tdFone.dataset.col='Telefone';
tdFone.textContent = fmtPhoneBR(p.telefone);
const tdCidade = document.createElement('td'); tdCidade.dataset.col='Cidade/UF';
tdCidade.textContent = `${p.cidade}/${p.uf}`;
const tdUlt = document.createElement('td'); tdUlt.dataset.col='Último atendimento';
tdUlt.textContent = fmtDate(p.ultimo);
const tdProx = document.createElement('td'); tdProx.dataset.col='Próximo atendimento';
tdProx.textContent = p.proximo ? fmtDate(p.proximo) : 'Nenhum atendimento agendado';
if (!p.proximo) tdProx.classList.add('status','no-apt');
const tdActions = document.createElement('td');
tdActions.className = 'row-actions';
tdActions.appendChild(actionMenu(p));
tr.append(tdNome, tdFone, tdCidade, tdUlt, tdProx, tdActions);
return tr;
}
function actionMenu(p){
const wrap = document.createElement('div');
const btn = document.createElement('button');
btn.className='menu-btn'; btn.textContent='Ações ▾'; btn.setAttribute('aria-expanded','false');
const dd = document.createElement('div'); dd.className='dropdown';
const mkBtn = (label, cb, classes='')=>{ const b=document.createElement('button'); b.textContent=label; if(classes) b.className=classes; b.addEventListener('click', (e)=>{ e.stopPropagation(); e.preventDefault(); dd.classList.remove('open'); cb(); }); return b; };
dd.appendChild(mkBtn('Ver detalhes', ()=> openPatient(p, true)));
dd.appendChild(mkBtn('Editar', ()=> openPatient(p, false)));
dd.appendChild(mkBtn('Marcar consulta', ()=> openSchedule(p)));
dd.appendChild(document.createElement('hr'));
dd.appendChild(mkBtn('Excluir', ()=> removePatient(p), 'danger'));
btn.addEventListener('click', (e)=>{ e.stopPropagation(); const isOpen = dd.classList.toggle('open'); btn.setAttribute('aria-expanded', String(isOpen)); });
document.addEventListener('click', ()=> dd.classList.remove('open'));
wrap.append(btn, dd);
return wrap;
}
// ==== Modais & Ações ====
function openPatient(p, readOnly=false){
pmId.value = p.id;
pmNome.value = p.nome;
pmDoc.value = p.doc||'';
pmVip.checked = !!p.vip;
pmVipBadge.style.display = p.vip ? 'inline-flex' : 'none';
pmFone.value = fmtPhoneBR(p.telefone);
pmConvenio.value = p.convenio;
pmNascimento.value = p.nascimento || '';
pmCidade.value = p.cidade || '';
pmUF.value = p.uf || 'AL';
pmIdade.value = p.idade || '';
pmObs.value = p.obs || '';
pmUltimo.value = p.ultimo ? p.ultimo.slice(0,16) : '';
pmProximo.value = p.proximo ? p.proximo.slice(0,16) : '';
// Toggle read-only
[pmNome, pmDoc, pmVip, pmFone, pmConvenio, pmNascimento, pmCidade, pmUF, pmIdade, pmObs, pmUltimo, pmProximo].forEach(el=>{ el.disabled = readOnly; });
document.getElementById('pmTitle').textContent = readOnly ? 'Detalhes do paciente' : 'Editar paciente';
pmSalvar.style.display = readOnly ? 'none' : 'inline-flex';
patientModal.showModal();
}
function openNewPatient(){
pmId.value = '';
pmNome.value=''; pmDoc.value=''; pmVip.checked=false; pmVipBadge.style.display='none';
pmFone.value=''; pmConvenio.value='Particular'; pmNascimento.value=''; pmCidade.value=''; pmUF.value='AL'; pmIdade.value=''; pmObs.value=''; pmUltimo.value=''; pmProximo.value='';
[pmNome, pmDoc, pmVip, pmFone, pmConvenio, pmNascimento, pmCidade, pmUF, pmIdade, pmObs, pmUltimo, pmProximo].forEach(el=>{ el.disabled = false; });
document.getElementById('pmTitle').textContent = 'Adicionar paciente';
pmSalvar.style.display = 'inline-flex';
patientModal.showModal();
}
function savePatient(){
const isNew = !pmId.value;
const base = {
id: isNew ? crypto.randomUUID() : pmId.value,
nome: pmNome.value.trim(),
doc: pmDoc.value.trim(),
vip: pmVip.checked,
telefone: pmFone.value,
convenio: pmConvenio.value,
nascimento: pmNascimento.value || null,
cidade: pmCidade.value.trim(),
uf: pmUF.value,
idade: pmIdade.value ? Number(pmIdade.value) : null,
obs: pmObs.value,
ultimo: pmUltimo.value ? new Date(pmUltimo.value).toISOString() : null,
proximo: pmProximo.value ? new Date(pmProximo.value).toISOString() : null,
atendimentosVinculados: isNew ? 0 : (DATA.find(x=>x.id===pmId.value)?.atendimentosVinculados || 0)
};
if (!base.nome){ alert('Informe o nome do paciente.'); return; }
if (isNew) DATA.unshift(base); else { const i = DATA.findIndex(x=>x.id===base.id); if (i>=0) DATA[i]=base; }
saveData();
patientModal.close();
applyFilters();
}
function removePatient(p){
if (p.atendimentosVinculados > 0){
alert('Não é possível excluir: existem atendimentos vinculados a este paciente.');
return;
}
if (!confirm(`Confirmar exclusão de "${p.nome}"?`)) return;
DATA = DATA.filter(x=>x.id!==p.id);
saveData();
applyFilters();
}
let schedulePatient = null;
function openSchedule(p){
schedulePatient = p;
scPaciente.textContent = `Paciente: ${p.nome}`;
scDate.value = p.proximo ? p.proximo.slice(0,16) : '';
scheduleModal.showModal();
}
function saveSchedule(){
if (!schedulePatient) return;
const value = scDate.value ? new Date(scDate.value).toISOString() : null;
schedulePatient.proximo = value;
// Se marcou consulta, presumir que agora existe pelo menos 1 atendimento
if (value && schedulePatient.atendimentosVinculados===0) schedulePatient.atendimentosVinculados = 1;
saveData();
scheduleModal.close();
applyFilters();
}
// ==== Eventos ====
const onSearch = debounce(()=>{ state.q = searchInput.value; applyFilters(); }, 250);
searchInput.addEventListener('input', onSearch);
clearSearch.addEventListener('click', ()=>{ searchInput.value=''; state.q=''; applyFilters(); });
convenioFilter.addEventListener('change', ()=>{ state.convenio = convenioFilter.value; applyFilters(); });
vipFilter.addEventListener('change', ()=>{ state.vip = vipFilter.checked; applyFilters(); });
aniversarioTipo.addEventListener('change', ()=>{
state.aniversTipo = aniversarioTipo.value;
aniversarioMes.style.display = aniversarioTipo.value==='mes' ? '' : 'none';
aniversarioData.style.display = aniversarioTipo.value==='data' ? '' : 'none';
if (!aniversarioTipo.value){ state.aniversMes=''; state.aniversData=''; aniversarioMes.value=''; aniversarioData.value=''; }
applyFilters();
});
aniversarioMes.addEventListener('change', ()=>{ state.aniversMes = aniversarioMes.value; applyFilters(); });
aniversarioData.addEventListener('change', ()=>{ state.aniversData = aniversarioData.value; applyFilters(); });
advancedFilterBtn.addEventListener('click', ()=> advancedFilterModal.showModal());
afClose.addEventListener('click', ()=> advancedFilterModal.close());
afAplicar.addEventListener('click', ()=>{
state.afCidade = afCidade.value.trim();
state.afUF = afUF.value;
state.idadeMin = afIdadeMin.value;
state.idadeMax = afIdadeMax.value;
state.ultimoDe = afUltimoDe.value;
state.ultimoAte = afUltimoAte.value;
advancedFilterModal.close();
applyFilters();
});
afLimpar.addEventListener('click', ()=>{
afCidade.value=''; afUF.value=''; afIdadeMin.value=''; afIdadeMax.value=''; afUltimoDe.value=''; afUltimoAte.value='';
state.afCidade=''; state.afUF=''; state.idadeMin=''; state.idadeMax=''; state.ultimoDe=''; state.ultimoAte='';
applyFilters();
});
addBtn.addEventListener('click', openNewPatient);
pmClose.addEventListener('click', ()=> patientModal.close());
pmCancel.addEventListener('click', ()=> patientModal.close());
pmSalvar.addEventListener('click', savePatient);
scClose.addEventListener('click', ()=> scheduleModal.close());
scCancel.addEventListener('click', ()=> scheduleModal.close());
scSalvar.addEventListener('click', saveSchedule);
// Phone mask
maskPhoneInput(pmFone);
// Infinite scroll
window.addEventListener('scroll', ()=>{
if (loader.style.display==='none') return;
const nearBottom = window.innerHeight + window.scrollY >= document.body.offsetHeight - 200;
if (nearBottom) loadMore();
});
// Inicialização
applyFilters();
</script>
</body>
</html>