704 lines
33 KiB
HTML
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>
|