859 lines
43 KiB
HTML
859 lines
43 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="pt-br">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>CRUD de Pacientes</title>
|
||
<style>
|
||
:root{
|
||
--bg:#0f172a; /* slate-900 */
|
||
--panel:#111827; /* gray-900 */
|
||
--muted:#1f2937; /* gray-800 */
|
||
--line:#2a3648; /* custom */
|
||
--text:#e5e7eb; /* gray-200 */
|
||
--text-dim:#a1a1aa; /* zinc-400 */
|
||
--brand:#22d3ee; /* cyan-400 */
|
||
--accent:#a78bfa; /* violet-400 */
|
||
--danger:#ef4444; /* red-500 */
|
||
--ok:#22c55e; /* green-500 */
|
||
--warn:#f59e0b; /* amber-500 */
|
||
--shadow: 0 10px 30px rgba(0,0,0,.35);
|
||
--radius:16px;
|
||
}
|
||
*{box-sizing:border-box}
|
||
html,body{height:100%}
|
||
body{margin:0;font-family:Inter,system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif;background:linear-gradient(120deg,#0b1224,#0f172a 40%,#111827);color:var(--text)}
|
||
header{
|
||
position:sticky;top:0;z-index:20;background:rgba(17,24,39,.9);backdrop-filter:saturate(160%) blur(8px);
|
||
border-bottom:1px solid var(--line);
|
||
}
|
||
.container{max-width:1200px;margin:0 auto;padding:18px}
|
||
.toolbar{display:grid;grid-template-columns:1fr auto;gap:12px;align-items:center}
|
||
.searchbar{display:flex;gap:10px;align-items:center;background:var(--muted);border:1px solid var(--line);padding:10px 12px;border-radius:12px}
|
||
.searchbar input{flex:1;background:transparent;border:none;outline:none;color:var(--text);font-size:16px}
|
||
.chips{display:flex;flex-wrap:wrap;gap:8px}
|
||
.chip,button,.btn{border:1px solid var(--line);background:var(--muted);color:var(--text);padding:8px 12px;border-radius:12px;cursor:pointer;transition:.2s ease;box-shadow:none}
|
||
.chip[aria-pressed="true"], .btn.primary{background:linear-gradient(135deg,var(--brand),#60a5fa);color:#0b1224;border-color:transparent}
|
||
.btn.success{background:linear-gradient(135deg,var(--ok),#34d399);color:#03210e;border-color:transparent}
|
||
.btn.danger{background:linear-gradient(135deg,var(--danger),#fb7185);color:#300;border-color:transparent}
|
||
.btn.ghost{background:transparent}
|
||
.chip:hover,.btn:hover{transform:translateY(-1px);box-shadow:var(--shadow)}
|
||
|
||
/* grid + table */
|
||
.panel{background:linear-gradient(180deg,rgba(255,255,255,.02),rgba(255,255,255,.01));border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow)}
|
||
.list-head{display:flex;justify-content:space-between;align-items:center;padding:16px;border-bottom:1px solid var(--line)}
|
||
.list{max-height:calc(100vh - 220px);overflow:auto}
|
||
table{width:100%;border-collapse:separate;border-spacing:0}
|
||
thead th{position:sticky;top:0;background:rgba(17,24,39,.95);backdrop-filter:blur(6px);font-weight:600;text-align:left;padding:12px;border-bottom:1px solid var(--line)}
|
||
tbody td{padding:12px;border-bottom:1px solid var(--line);vertical-align:middle}
|
||
tbody tr:hover{background:rgba(255,255,255,.03)}
|
||
.tag{display:inline-flex;gap:6px;align-items:center;padding:3px 8px;border-radius:999px;border:1px solid var(--line);font-size:12px}
|
||
.tag.vip{border-color:var(--warn);color:#fde68a;background:rgba(245,158,11,.1)}
|
||
.muted{color:var(--text-dim)}
|
||
.avatar{width:36px;height:36px;border-radius:50%;object-fit:cover;border:1px solid var(--line)}
|
||
.row-actions{position:relative}
|
||
.menu{position:absolute;right:0;top:40px;background:var(--panel);border:1px solid var(--line);border-radius:12px;min-width:200px;display:none;box-shadow:var(--shadow)}
|
||
.menu.open{display:block}
|
||
.menu button{display:flex;width:100%;gap:10px;justify-content:flex-start;background:transparent;border:0;padding:10px 12px}
|
||
.menu button:hover{background:rgba(255,255,255,.04)}
|
||
|
||
/* forms */
|
||
.grid{display:grid;gap:12px}
|
||
.grid.col2{grid-template-columns:repeat(2,1fr)}
|
||
.grid.col3{grid-template-columns:repeat(3,1fr)}
|
||
.field{display:flex;flex-direction:column;gap:6px}
|
||
.field label{font-size:12px;color:var(--text-dim)}
|
||
.field input,.field select,.field textarea{background:var(--muted);border:1px solid var(--line);padding:10px 12px;color:var(--text);border-radius:10px;outline:none}
|
||
.field textarea{min-height:90px}
|
||
.switch{display:flex;align-items:center;gap:10px}
|
||
|
||
.footer-actions{display:flex;gap:12px;justify-content:flex-end;padding:12px}
|
||
|
||
/* modals */
|
||
dialog{border:none;border-radius:18px;background:var(--panel);color:var(--text);box-shadow:var(--shadow);width:min(900px,95vw)}
|
||
dialog::backdrop{background:rgba(0,0,0,.5)}
|
||
.modal-head{display:flex;justify-content:space-between;align-items:center;padding:14px 16px;border-bottom:1px solid var(--line)}
|
||
.modal-body{padding:16px}
|
||
|
||
.kpi{display:flex;gap:10px;align-items:center}
|
||
.kpi strong{font-size:20px}
|
||
|
||
.pill{border:1px dashed var(--line);padding:10px;border-radius:12px}
|
||
.files{display:flex;flex-direction:column;gap:10px}
|
||
|
||
/* toast */
|
||
.toast{position:fixed;right:16px;bottom:16px;display:flex;flex-direction:column;gap:8px;z-index:50}
|
||
.toast .msg{background:var(--panel);border:1px solid var(--line);padding:10px 12px;border-radius:12px;box-shadow:var(--shadow)}
|
||
|
||
@media (max-width:900px){
|
||
.grid.col3{grid-template-columns:1fr}
|
||
.grid.col2{grid-template-columns:1fr}
|
||
.list{max-height:unset}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<header>
|
||
<div class="container">
|
||
<div class="toolbar">
|
||
<div class="searchbar">
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M21 21L16.65 16.65M19 11C19 15.4183 15.4183 19 11 19C6.58172 19 3 15.4183 3 11C3 6.58172 6.58172 3 11 3C15.4183 3 19 6.58172 19 11Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||
<input id="q" type="search" placeholder="Pesquisar por nome, CPF ou RG..." />
|
||
<select id="filterConvenio" title="Convênio">
|
||
<option value="">Convênio (todos)</option>
|
||
</select>
|
||
<button class="chip" id="filterVip" aria-pressed="false">VIP</button>
|
||
<select id="filterMes" title="Aniversariantes">
|
||
<option value="">Aniversariantes (mês)</option>
|
||
<option value="1">Jan</option><option value="2">Fev</option><option value="3">Mar</option><option value="4">Abr</option>
|
||
<option value="5">Mai</option><option value="6">Jun</option><option value="7">Jul</option><option value="8">Ago</option>
|
||
<option value="9">Set</option><option value="10">Out</option><option value="11">Nov</option><option value="12">Dez</option>
|
||
</select>
|
||
<button class="chip" id="btnFiltroAvancado">Filtro avançado</button>
|
||
</div>
|
||
<div class="chips">
|
||
<button id="btnImport" class="btn">Importar CSV</button>
|
||
<button id="btnCampos" class="btn">Campos personalizados</button>
|
||
<button id="btnAdd" class="btn primary">+ Adicionar</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<main class="container">
|
||
<section class="panel">
|
||
<div class="list-head">
|
||
<div class="kpi"><span class="tag">Total: <strong id="kpiTotal">0</strong></span><span class="tag">Filtrados: <strong id="kpiFiltrados">0</strong></span></div>
|
||
<div class="muted">Scroll infinito ativo</div>
|
||
</div>
|
||
<div class="list" id="list">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Paciente</th>
|
||
<th>Telefone</th>
|
||
<th>Cidade/UF</th>
|
||
<th>Último atendimento</th>
|
||
<th>Próximo atendimento</th>
|
||
<th style="width:60px">Ações</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="tbody"></tbody>
|
||
</table>
|
||
<div id="sentinel" class="muted" style="text-align:center;padding:14px">Carregando...</div>
|
||
</div>
|
||
</section>
|
||
</main>
|
||
|
||
<!-- Modal Form Paciente -->
|
||
<dialog id="dlgForm">
|
||
<form method="dialog" id="formPaciente">
|
||
<div class="modal-head">
|
||
<h3 id="formTitle">Novo Paciente</h3>
|
||
<div class="chips">
|
||
<span class="tag" id="tagId">ID: —</span>
|
||
<span class="tag" id="tagVip" style="display:none">VIP</span>
|
||
<button type="button" class="btn ghost" onclick="dlgForm.close()">✕</button>
|
||
</div>
|
||
</div>
|
||
<div class="modal-body">
|
||
<!-- Dados pessoais -->
|
||
<h4>1. Dados pessoais</h4>
|
||
<div class="grid col3">
|
||
<div class="field">
|
||
<label>Foto</label>
|
||
<input type="file" id="foto" accept="image/*" />
|
||
<img id="fotoPreview" class="avatar" style="margin-top:8px;width:72px;height:72px" alt="Prévia"/>
|
||
</div>
|
||
<div class="field">
|
||
<label>Nome *</label>
|
||
<input required id="nome" />
|
||
</div>
|
||
<div class="field">
|
||
<label>Nome social</label>
|
||
<input id="nomeSocial" />
|
||
</div>
|
||
|
||
<div class="field">
|
||
<label>CPF</label>
|
||
<input id="cpf" maxlength="14" placeholder="___.___.___-__" />
|
||
</div>
|
||
<div class="field">
|
||
<label>RG</label>
|
||
<input id="rg" />
|
||
</div>
|
||
<div class="field">
|
||
<label>Outros documentos</label>
|
||
<div class="grid col2">
|
||
<select id="docTipo">
|
||
<option value="">—</option>
|
||
<option>CNH</option>
|
||
<option>Passaporte</option>
|
||
<option>RNE</option>
|
||
</select>
|
||
<input id="docNumero" placeholder="Número" />
|
||
</div>
|
||
</div>
|
||
|
||
<div class="field">
|
||
<label>Sexo</label>
|
||
<select id="sexo"><option value="">—</option><option>Masculino</option><option>Feminino</option><option>Outro</option></select>
|
||
</div>
|
||
<div class="field">
|
||
<label>Data de nascimento</label>
|
||
<input type="date" id="nascimento" />
|
||
</div>
|
||
<div class="field">
|
||
<label>Etnia (IBGE)</label>
|
||
<select id="etnia"><option value="">—</option><option>Branca</option><option>Preta</option><option>Parda</option><option>Amarela</option><option>Indígena</option></select>
|
||
</div>
|
||
|
||
<div class="field">
|
||
<label>Raça/Cor (IBGE)</label>
|
||
<select id="raca"><option value="">—</option><option>Branca</option><option>Preta</option><option>Parda</option><option>Amarela</option><option>Indígena</option></select>
|
||
</div>
|
||
<div class="field">
|
||
<label>Naturalidade</label>
|
||
<input id="naturalidade" placeholder="Cidade/UF" />
|
||
</div>
|
||
<div class="field">
|
||
<label>Nacionalidade</label>
|
||
<input id="nacionalidade" placeholder="País" />
|
||
</div>
|
||
|
||
<div class="field">
|
||
<label>Profissão</label>
|
||
<input id="profissao" />
|
||
</div>
|
||
<div class="field">
|
||
<label>Estado civil</label>
|
||
<select id="estadoCivil"><option value="">—</option><option>Solteiro(a)</option><option>Casado(a)</option><option>Divorciado(a)</option><option>Viúvo(a)</option><option>União estável</option></select>
|
||
</div>
|
||
<div class="field">
|
||
<label>Nome da mãe</label>
|
||
<input id="mae" />
|
||
</div>
|
||
|
||
<div class="field">
|
||
<label>Profissão da mãe</label>
|
||
<input id="profMae" />
|
||
</div>
|
||
<div class="field">
|
||
<label>Nome do pai</label>
|
||
<input id="pai" />
|
||
</div>
|
||
<div class="field">
|
||
<label>Profissão do pai</label>
|
||
<input id="profPai" />
|
||
</div>
|
||
|
||
<div class="field">
|
||
<label>Nome do responsável</label>
|
||
<input id="responsavel" />
|
||
</div>
|
||
<div class="field">
|
||
<label>CPF do responsável</label>
|
||
<input id="cpfResponsavel" maxlength="14" placeholder="___.___.___-__" />
|
||
</div>
|
||
<div class="field">
|
||
<label>Nome do esposo(a)</label>
|
||
<input id="conjuge" />
|
||
</div>
|
||
|
||
<div class="field switch">
|
||
<input type="checkbox" id="rnGuia" />
|
||
<label for="rnGuia">RN na Guia do convênio</label>
|
||
</div>
|
||
<div class="field">
|
||
<label>Código legado</label>
|
||
<input id="codigoLegado" />
|
||
</div>
|
||
<div class="field">
|
||
<label>Convênio</label>
|
||
<input id="convenio" placeholder="Ex.: Unimed, Hapvida..." />
|
||
</div>
|
||
|
||
<div class="field switch">
|
||
<input type="checkbox" id="vip" />
|
||
<label for="vip">VIP</label>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Observações e anexos -->
|
||
<h4 style="margin-top:16px">2. Observações e anexos</h4>
|
||
<div class="grid col2">
|
||
<div class="field">
|
||
<label>Observações</label>
|
||
<textarea id="obs" placeholder="Alergias, restrições..."></textarea>
|
||
</div>
|
||
<div class="field">
|
||
<label>Anexos do paciente</label>
|
||
<div class="pill">
|
||
<input type="file" id="inputAnexo" />
|
||
<div class="files" id="listaAnexos"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Contato -->
|
||
<h4 style="margin-top:16px">3. Contato</h4>
|
||
<div class="grid col3">
|
||
<div class="field"><label>E-mail</label><input id="email" type="email" placeholder="nome@dominio.com" /></div>
|
||
<div class="field"><label>Celular</label><input id="celular" placeholder="+55 (__) _____-____" maxlength="20"/></div>
|
||
<div class="field"><label>Telefone 1</label><input id="tel1" placeholder="(__) ____-_____" maxlength="20"/></div>
|
||
<div class="field"><label>Telefone 2</label><input id="tel2" placeholder="(__) ____-_____" maxlength="20"/></div>
|
||
</div>
|
||
|
||
<!-- Endereço -->
|
||
<h4 style="margin-top:16px">4. Endereço</h4>
|
||
<div class="grid col3">
|
||
<div class="field"><label>CEP</label><input id="cep" placeholder="_____‑___" maxlength="9"/></div>
|
||
<div class="field"><label>Logradouro</label><input id="logradouro"/></div>
|
||
<div class="field"><label>Número</label><input id="numero"/></div>
|
||
<div class="field"><label>Complemento</label><input id="complemento"/></div>
|
||
<div class="field"><label>Bairro</label><input id="bairro"/></div>
|
||
<div class="field"><label>Cidade</label><input id="cidade"/></div>
|
||
<div class="field"><label>Estado</label><input id="estado"/></div>
|
||
<div class="field"><label>Referência</label><input id="referencia"/></div>
|
||
</div>
|
||
|
||
<!-- Campos personalizados -->
|
||
<h4 style="margin-top:16px">5. Campos personalizados</h4>
|
||
<div id="customFields" class="grid col2"></div>
|
||
|
||
<div class="footer-actions">
|
||
<button type="button" class="btn danger" id="btnExcluir" style="margin-right:auto;display:none">Excluir</button>
|
||
<button type="button" class="btn" id="btnHistorico">Histórico de alterações</button>
|
||
<button type="button" class="btn" id="btnMarcarConsulta">Marcar consulta</button>
|
||
<button type="button" class="btn" onclick="dlgForm.close()">Cancelar</button>
|
||
<button type="submit" class="btn success" id="btnSalvar">Salvar</button>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</dialog>
|
||
|
||
<!-- Modal Filtro Avançado -->
|
||
<dialog id="dlgFiltro">
|
||
<div class="modal-head"><h3>Filtro avançado</h3><button class="btn ghost" onclick="dlgFiltro.close()">✕</button></div>
|
||
<div class="modal-body">
|
||
<div class="grid col3">
|
||
<div class="field"><label>Cidade</label><input id="fCidade"></div>
|
||
<div class="field"><label>Estado</label><input id="fEstado"></div>
|
||
<div class="field"><label>Idade mínima</label><input id="fIdadeMin" type="number" min="0"></div>
|
||
<div class="field"><label>Idade máxima</label><input id="fIdadeMax" type="number" min="0"></div>
|
||
<div class="field"><label>Último atendimento desde</label><input id="fUltimoDe" type="date"></div>
|
||
<div class="field"><label>Último atendimento até</label><input id="fUltimoAte" type="date"></div>
|
||
</div>
|
||
<div class="footer-actions"><button class="btn" onclick="resetAdvancedFilters()">Limpar</button><button class="btn primary" onclick="aplicarFiltrosAvancados()">Aplicar</button></div>
|
||
</div>
|
||
</dialog>
|
||
|
||
<!-- Modal Histórico -->
|
||
<dialog id="dlgHistorico">
|
||
<div class="modal-head"><h3>Histórico de alterações</h3><button class="btn ghost" onclick="dlgHistorico.close()">✕</button></div>
|
||
<div class="modal-body">
|
||
<div id="histBody" class="files"></div>
|
||
</div>
|
||
</dialog>
|
||
|
||
<!-- Modal Consulta -->
|
||
<dialog id="dlgConsulta">
|
||
<div class="modal-head"><h3>Marcar consulta</h3><button class="btn ghost" onclick="dlgConsulta.close()">✕</button></div>
|
||
<div class="modal-body">
|
||
<div class="grid col2">
|
||
<div class="field"><label>Data</label><input id="cData" type="date"></div>
|
||
<div class="field"><label>Hora</label><input id="cHora" type="time"></div>
|
||
<div class="field"><label>Observações</label><textarea id="cObs"></textarea></div>
|
||
</div>
|
||
<div class="footer-actions"><button class="btn" onclick="dlgConsulta.close()">Cancelar</button><button class="btn success" id="btnSalvarConsulta">Salvar</button></div>
|
||
</div>
|
||
</dialog>
|
||
|
||
<!-- Modal Importar CSV -->
|
||
<dialog id="dlgImport">
|
||
<div class="modal-head"><h3>Importação em massa (CSV)</h3><button class="btn ghost" onclick="dlgImport.close()">✕</button></div>
|
||
<div class="modal-body">
|
||
<p class="muted">Cabeçalhos aceitos: nome, cpf, rg, nascimento, email, celular, cidade, estado, convenio, vip</p>
|
||
<input type="file" id="csvFile" accept=".csv">
|
||
<div class="footer-actions"><button class="btn" onclick="dlgImport.close()">Fechar</button><button class="btn primary" id="btnImportCsv">Importar</button></div>
|
||
</div>
|
||
</dialog>
|
||
|
||
<!-- Modal Campos personalizados -->
|
||
<dialog id="dlgCampos">
|
||
<div class="modal-head"><h3>Campos personalizados</h3><button class="btn ghost" onclick="dlgCampos.close()">✕</button></div>
|
||
<div class="modal-body">
|
||
<div class="grid col3">
|
||
<div class="field"><label>Nome do campo</label><input id="cfNome" placeholder="Ex.: Plano odontológico"></div>
|
||
<div class="field"><label>Tipo</label><select id="cfTipo"><option>texto</option><option>numero</option><option>data</option></select></div>
|
||
<div class="field"><label>Chave (sem espaços)</label><input id="cfKey" placeholder="planoOdonto"></div>
|
||
</div>
|
||
<div class="footer-actions"><button class="btn" onclick="dlgCampos.close()">Fechar</button><button class="btn success" id="btnAddCampo">Adicionar</button></div>
|
||
<div id="cfLista" class="files" style="margin-top:10px"></div>
|
||
</div>
|
||
</dialog>
|
||
|
||
<div class="toast" id="toast"></div>
|
||
|
||
<script>
|
||
// ===== Util =====
|
||
const $ = sel => document.querySelector(sel);
|
||
const $$ = sel => Array.from(document.querySelectorAll(sel));
|
||
const uid = () => Math.random().toString(36).slice(2) + Date.now().toString(36);
|
||
const fmtDate = iso => iso ? new Date(iso).toLocaleString() : '—';
|
||
const fmtPhone = s => s ? s.replace(/\D/g,'').replace(/^(\d{2})(\d{5})(\d{4}).*/, '+55 ($1) $2-$3') : '';
|
||
const fmtTel = s => s ? s.replace(/\D/g,'').replace(/^(\d{2})(\d{4,5})(\d{4}).*/, '($1) $2-$3') : '';
|
||
const maskCPF = v => v.replace(/\D/g,'').replace(/(\d{3})(\d)/,'$1.$2').replace(/(\d{3})(\d)/,'$1.$2').replace(/(\d{3})(\d{1,2})$/,'$1-$2').slice(0,14);
|
||
const maskCEP = v => v.replace(/\D/g,'').replace(/(\d{5})(\d)/,'$1-$2').slice(0,9);
|
||
const toast = (msg, type='') => {
|
||
const el = document.createElement('div');
|
||
el.className = 'msg';
|
||
el.textContent = msg;
|
||
if(type==='ok') el.style.borderColor = 'var(--ok)';
|
||
if(type==='err') el.style.borderColor = 'var(--danger)';
|
||
$('#toast').appendChild(el);
|
||
setTimeout(()=>el.remove(), 3500);
|
||
}
|
||
const isCPF = cpf => {
|
||
cpf = (cpf||'').replace(/\D/g,'');
|
||
if(!cpf || cpf.length !== 11 || /^([0-9])\1+$/.test(cpf)) return false;
|
||
let s = 0; for(let i=0;i<9;i++) s += parseInt(cpf.charAt(i))*(10-i); let d1 = 11 - (s % 11); if(d1>9) d1=0;
|
||
s = 0; for(let i=0;i<10;i++) s += parseInt(cpf.charAt(i))*(11-i); let d2 = 11 - (s % 11); if(d2>9) d2=0;
|
||
return d1==cpf.charAt(9) && d2==cpf.charAt(10);
|
||
}
|
||
|
||
// ===== Storage =====
|
||
const DB_KEY = 'pacientesDB_v1';
|
||
const CF_KEY = 'customFields_v1';
|
||
const loadDB = () => JSON.parse(localStorage.getItem(DB_KEY)||'[]');
|
||
const saveDB = rows => localStorage.setItem(DB_KEY, JSON.stringify(rows));
|
||
const loadCF = () => JSON.parse(localStorage.getItem(CF_KEY)||'[]');
|
||
const saveCF = rows => localStorage.setItem(CF_KEY, JSON.stringify(rows));
|
||
|
||
// ===== Estado =====
|
||
let DB = loadDB();
|
||
let FILTERED = [];
|
||
let PAGE_SIZE = 20; let cursor = 0; // scroll infinito
|
||
let editingId = null; // id em edição
|
||
let viewOnly = false; // modo somente leitura
|
||
|
||
// ===== Mock inicial (se vazio) =====
|
||
if(DB.length===0){
|
||
const exemplos = Array.from({length:48}).map((_,i)=>({
|
||
id: uid(),
|
||
nome: `Paciente ${i+1}`,
|
||
nomeSocial:'',
|
||
cpf:'',
|
||
rg:'',
|
||
docTipo:'',docNumero:'',
|
||
sexo: i%2? 'Feminino':'Masculino',
|
||
nascimento: new Date(1980 + (i%30), (i%12), (i%28)+1).toISOString().slice(0,10),
|
||
etnia:'',raca:'',naturalidade:'',nacionalidade:'Brasil',
|
||
profissao:'',estadoCivil:'',
|
||
mae:'',profMae:'',pai:'',profPai:'',
|
||
responsavel:'',cpfResponsavel:'',conjuge:'',rnGuia:false,codigoLegado:'',
|
||
obs:'',
|
||
anexos:[],
|
||
email:'',celular:'',tel1:'',tel2:'',
|
||
endereco:{cep:'',logradouro:'',numero:'',complemento:'',bairro:'',cidade:'Maceió',estado:'AL',referencia:''},
|
||
foto:'',
|
||
convenio: ['Unimed','Hapvida','Particular'][i%3],
|
||
vip: i%10===0,
|
||
criadoEm: new Date().toISOString(),
|
||
atualizadoEm: new Date().toISOString(),
|
||
historico:[{quando:new Date().toISOString(), acao:'criado', por:'Usuário padrão'}],
|
||
atendimentos:{ultimo: new Date(2025, (i%12), (i%27)+1, 9, 0).toISOString(), proximo: i%4? null : new Date(2025, (i%12), (i%27)+2, 14, 30).toISOString()},
|
||
custom:{}
|
||
}));
|
||
DB = exemplos; saveDB(DB);
|
||
}
|
||
|
||
// ===== Render da lista =====
|
||
function uniq(arr){return [...new Set(arr.filter(Boolean))]}
|
||
|
||
function refreshConvenios(){
|
||
const opts = uniq(DB.map(r=>r.convenio)).sort();
|
||
const sel = $('#filterConvenio');
|
||
sel.innerHTML = '<option value="">Convênio (todos)</option>' + opts.map(v=>`<option>${v}</option>`).join('');
|
||
}
|
||
|
||
function applyFilters(){
|
||
const q = $('#q').value.trim().toLowerCase();
|
||
const conv = $('#filterConvenio').value;
|
||
const vip = $('#filterVip').getAttribute('aria-pressed')==='true';
|
||
const mes = $('#filterMes').value;
|
||
|
||
FILTERED = DB.filter(r=>{
|
||
const texto = [r.nome, r.cpf, r.rg].join(' ').toLowerCase();
|
||
if(q && !texto.includes(q)) return false;
|
||
if(conv && r.convenio !== conv) return false;
|
||
if(vip && !r.vip) return false;
|
||
if(mes){
|
||
const m = (r.nascimento||'').split('-')[1];
|
||
if(!m || parseInt(m) !== parseInt(mes)) return false;
|
||
}
|
||
// avançado em memória
|
||
if(ADV.cidade && (r.endereco?.cidade||'').toLowerCase() !== ADV.cidade.toLowerCase()) return false;
|
||
if(ADV.estado && (r.endereco?.estado||'').toLowerCase() !== ADV.estado.toLowerCase()) return false;
|
||
const idade = calcIdade(r.nascimento);
|
||
if(ADV.idMin!=null && idade!=null && idade < ADV.idMin) return false;
|
||
if(ADV.idMax!=null && idade!=null && idade > ADV.idMax) return false;
|
||
if(ADV.ultDe && r.atendimentos?.ultimo && new Date(r.atendimentos.ultimo) < new Date(ADV.ultDe)) return false;
|
||
if(ADV.ultAte && r.atendimentos?.ultimo && new Date(r.atendimentos.ultimo) > new Date(ADV.ultAte)) return false;
|
||
return true;
|
||
});
|
||
$('#kpiTotal').textContent = DB.length;
|
||
$('#kpiFiltrados').textContent = FILTERED.length;
|
||
|
||
cursor = 0;
|
||
$('#tbody').innerHTML = '';
|
||
loadMore();
|
||
}
|
||
|
||
function loadMore(){
|
||
const slice = FILTERED.slice(cursor, cursor + PAGE_SIZE);
|
||
const rows = slice.map(r=> rowHTML(r)).join('');
|
||
$('#tbody').insertAdjacentHTML('beforeend', rows);
|
||
cursor += PAGE_SIZE;
|
||
$('#sentinel').style.display = cursor < FILTERED.length ? 'block' : 'none';
|
||
}
|
||
|
||
function rowHTML(r){
|
||
const proximo = r.atendimentos?.proximo ? new Date(r.atendimentos.proximo) : null;
|
||
const proxTxt = proximo ? proximo.toLocaleString() : '<span class="muted">Nenhum atendimento agendado</span>';
|
||
return `<tr data-id="${r.id}">
|
||
<td>
|
||
<div style="display:flex;gap:10px;align-items:center">
|
||
<img class="avatar" src="${r.foto||'data:image/svg+xml;utf8,<svg xmlns=\'http://www.w3.org/2000/svg\' width=\'72\' height=\'72\'><rect width=\'100%\' height=\'100%\' fill=\'%23111827\'/><text x=\'50%\' y=\'55%\' dominant-baseline=\'middle\' text-anchor=\'middle\' fill=\'%23a1a1aa\' font-family=\'Arial\' font-size=\'14\'>Sem foto</text></svg>'}" alt="foto">
|
||
<div>
|
||
<div style="display:flex;gap:8px;align-items:center"><a href="#" class="linkNome" data-id="${r.id}"><strong>${r.nome}</strong></a>${r.vip?'<span class="tag vip">VIP</span>':''}</div>
|
||
<div class="muted">${r.convenio||'—'}</div>
|
||
</div>
|
||
</div>
|
||
</td>
|
||
<td>${fmtTel(r.celular||r.tel1||'')}</td>
|
||
<td>${(r.endereco?.cidade||'—')}/${(r.endereco?.estado||'—')}</td>
|
||
<td>${fmtDate(r.atendimentos?.ultimo)}</td>
|
||
<td>${proxTxt}</td>
|
||
<td class="row-actions">
|
||
<button class="btn" onclick="toggleMenu(this)">Ações ▾</button>
|
||
<div class="menu">
|
||
<button onclick="abrirProntuario('${r.id}', true)">Ver detalhes</button>
|
||
<button onclick="abrirProntuario('${r.id}', false)">Editar</button>
|
||
<button onclick="excluirPaciente('${r.id}')">Excluir</button>
|
||
<button onclick="abrirConsulta('${r.id}')">Marcar consulta</button>
|
||
</div>
|
||
</td>
|
||
</tr>`
|
||
}
|
||
|
||
function toggleMenu(btn){
|
||
const menu = btn.nextElementSibling; $$('.menu').forEach(m=>m.classList.remove('open')); menu.classList.add('open');
|
||
document.addEventListener('click', function doc(e){ if(!menu.contains(e.target) && e.target!==btn){ menu.classList.remove('open'); document.removeEventListener('click', doc); } }, {once:true});
|
||
}
|
||
|
||
// ===== Filtros avançados =====
|
||
const ADV = {cidade:'', estado:'', idMin:null, idMax:null, ultDe:'', ultAte:''};
|
||
function aplicarFiltrosAvancados(){
|
||
ADV.cidade = $('#fCidade').value.trim();
|
||
ADV.estado = $('#fEstado').value.trim();
|
||
ADV.idMin = $('#fIdadeMin').value? parseInt($('#fIdadeMin').value): null;
|
||
ADV.idMax = $('#fIdadeMax').value? parseInt($('#fIdadeMax').value): null;
|
||
ADV.ultDe = $('#fUltimoDe').value;
|
||
ADV.ultAte = $('#fUltimoAte').value;
|
||
dlgFiltro.close();
|
||
applyFilters();
|
||
}
|
||
function resetAdvancedFilters(){
|
||
$('#fCidade').value=''; $('#fEstado').value=''; $('#fIdadeMin').value=''; $('#fIdadeMax').value=''; $('#fUltimoDe').value=''; $('#fUltimoAte').value='';
|
||
ADV.cidade=''; ADV.estado=''; ADV.idMin=null; ADV.idMax=null; ADV.ultDe=''; ADV.ultAte='';
|
||
applyFilters();
|
||
}
|
||
|
||
// ===== Form =====
|
||
function abrirProntuario(id, somenteLeitura=false){
|
||
const r = DB.find(x=>x.id===id);
|
||
if(!r) return;
|
||
viewOnly = !!somenteLeitura; editingId = id;
|
||
$('#formTitle').textContent = (viewOnly? 'Prontuário — ':'Editar — ') + r.nome;
|
||
$('#tagId').textContent = 'ID: ' + r.id; $('#tagVip').style.display = r.vip? 'inline-flex':'none';
|
||
preencherForm(r);
|
||
$('#btnExcluir').style.display = viewOnly? 'none':'inline-flex';
|
||
$('#btnSalvar').style.display = viewOnly? 'none':'inline-flex';
|
||
disableForm(viewOnly);
|
||
dlgForm.showModal();
|
||
}
|
||
|
||
function novoPaciente(){
|
||
viewOnly = false; editingId = null;
|
||
$('#formTitle').textContent = 'Novo Paciente'; $('#tagId').textContent = 'ID: —'; $('#tagVip').style.display='none';
|
||
limparForm();
|
||
disableForm(false);
|
||
$('#btnExcluir').style.display = 'none';
|
||
$('#btnSalvar').style.display = 'inline-flex';
|
||
dlgForm.showModal();
|
||
}
|
||
|
||
function disableForm(dis){
|
||
$$('#formPaciente input, #formPaciente select, #formPaciente textarea').forEach(el=>{
|
||
if(['btnHistorico','btnMarcarConsulta'].includes(el.id)) return;
|
||
el.disabled = dis;
|
||
});
|
||
$('#inputAnexo').disabled = dis; $('#foto').disabled = dis;
|
||
}
|
||
|
||
function limparForm(){
|
||
$$('#formPaciente input, #formPaciente textarea').forEach(el=>{ if(el.type!=='checkbox' && el.type!=='file') el.value=''; if(el.type==='checkbox') el.checked=false; });
|
||
$('#fotoPreview').src='';
|
||
$('#listaAnexos').innerHTML='';
|
||
renderCustomFields({});
|
||
}
|
||
|
||
function preencherForm(r){
|
||
$('#fotoPreview').src = r.foto||'';
|
||
$('#nome').value = r.nome||''; $('#nomeSocial').value=r.nomeSocial||'';
|
||
$('#cpf').value = maskCPF(r.cpf||''); $('#rg').value = r.rg||'';
|
||
$('#docTipo').value = r.docTipo||''; $('#docNumero').value = r.docNumero||'';
|
||
$('#sexo').value = r.sexo||''; $('#nascimento').value = r.nascimento||'';
|
||
$('#etnia').value=r.etnia||''; $('#raca').value=r.raca||''; $('#naturalidade').value=r.naturalidade||''; $('#nacionalidade').value=r.nacionalidade||'';
|
||
$('#profissao').value=r.profissao||''; $('#estadoCivil').value=r.estadoCivil||'';
|
||
$('#mae').value=r.mae||''; $('#profMae').value=r.profMae||''; $('#pai').value=r.pai||''; $('#profPai').value=r.profPai||'';
|
||
$('#responsavel').value=r.responsavel||''; $('#cpfResponsavel').value=maskCPF(r.cpfResponsavel||''); $('#conjuge').value=r.conjuge||'';
|
||
$('#rnGuia').checked=!!r.rnGuia; $('#codigoLegado').value=r.codigoLegado||''; $('#convenio').value=r.convenio||''; $('#vip').checked=!!r.vip;
|
||
$('#obs').value=r.obs||'';
|
||
$('#email').value=r.email||''; $('#celular').value=r.celular||''; $('#tel1').value=r.tel1||''; $('#tel2').value=r.tel2||'';
|
||
const e = r.endereco||{}; $('#cep').value=e.cep||''; $('#logradouro').value=e.logradouro||''; $('#numero').value=e.numero||''; $('#complemento').value=e.complemento||''; $('#bairro').value=e.bairro||''; $('#cidade').value=e.cidade||''; $('#estado').value=e.estado||''; $('#referencia').value=e.referencia||'';
|
||
// anexos
|
||
$('#listaAnexos').innerHTML = (r.anexos||[]).map((a,idx)=>`<div class="tag" style="justify-content:space-between"><span>${a.nome}</span><button class="btn" onclick="removerAnexo(${idx})">Excluir</button></div>`).join('');
|
||
renderCustomFields(r.custom||{});
|
||
}
|
||
|
||
function coletarForm(){
|
||
const get = id=>$("#"+id).value;
|
||
const chk = id=>$("#"+id).checked;
|
||
const endereco = {cep:get('cep'),logradouro:get('logradouro'),numero:get('numero'),complemento:get('complemento'),bairro:get('bairro'),cidade:get('cidade'),estado:get('estado'),referencia:get('referencia')};
|
||
const custom = collectCustomFields();
|
||
const base = {
|
||
nome:get('nome').trim(), nomeSocial:get('nomeSocial'), cpf:get('cpf'), rg:get('rg'), docTipo:get('docTipo'), docNumero:get('docNumero'),
|
||
sexo:get('sexo'), nascimento:get('nascimento'), etnia:get('etnia'), raca:get('raca'), naturalidade:get('naturalidade'), nacionalidade:get('nacionalidade'),
|
||
profissao:get('profissao'), estadoCivil:get('estadoCivil'), mae:get('mae'), profMae:get('profMae'), pai:get('pai'), profPai:get('profPai'),
|
||
responsavel:get('responsavel'), cpfResponsavel:get('cpfResponsavel'), conjuge:get('conjuge'), rnGuia:chk('rnGuia'), codigoLegado:get('codigoLegado'),
|
||
convenio:get('convenio'), vip:chk('vip'), obs:get('obs'), email:get('email'), celular:get('celular'), tel1:get('tel1'), tel2:get('tel2'), endereco, custom
|
||
};
|
||
return base;
|
||
}
|
||
|
||
function validar(r){
|
||
const erros = [];
|
||
if(!r.nome) erros.push('Nome é obrigatório');
|
||
if(r.cpf){ if(!isCPF(r.cpf)) erros.push('CPF inválido'); }
|
||
if(r.email && !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(r.email)) erros.push('E-mail inválido');
|
||
if(r.celular && r.celular.replace(/\D/g,'').length < 10) erros.push('Celular inválido');
|
||
// duplicidade
|
||
const normal = s => (s||'').replace(/\D/g,'');
|
||
const alvoCpf = normal(r.cpf);
|
||
const alvoNome = (r.nome||'').trim().toLowerCase();
|
||
const alvoNasc = r.nascimento||'';
|
||
const dup = DB.find(p => p.id!==editingId && ((alvoCpf && normal(p.cpf)===alvoCpf) || (alvoNome && p.nome.trim().toLowerCase()===alvoNome && (p.nascimento||'')===alvoNasc)));
|
||
if(dup) erros.push('Duplicidade detectada: já existe paciente com mesmo CPF ou (nome + data de nascimento).');
|
||
return erros;
|
||
}
|
||
|
||
function salvar(e){
|
||
e.preventDefault();
|
||
const base = coletarForm();
|
||
base.cpf = base.cpf.replace(/\D/g,'');
|
||
base.cpfResponsavel = base.cpfResponsavel.replace(/\D/g,'');
|
||
base.celular = fmtTel(base.celular); base.tel1 = fmtTel(base.tel1); base.tel2 = fmtTel(base.tel2);
|
||
const erros = validar(base);
|
||
if(erros.length){ toast('Erros: '+erros.join(' | '), 'err'); return; }
|
||
let r;
|
||
if(editingId){
|
||
r = DB.find(x=>x.id===editingId);
|
||
Object.assign(r, base);
|
||
r.atualizadoEm = new Date().toISOString();
|
||
(r.historico||[]).push({quando:new Date().toISOString(), acao:'atualizado', por:'Usuário padrão'});
|
||
} else {
|
||
r = {id:uid(), foto: $('#fotoPreview').src || '', anexos:[], criadoEm:new Date().toISOString(), atualizadoEm:new Date().toISOString(), historico:[{quando:new Date().toISOString(), acao:'criado', por:'Usuário padrão'}], atendimentos:{ultimo:null, proximo:null}, ...base};
|
||
DB.unshift(r);
|
||
}
|
||
saveDB(DB);
|
||
refreshConvenios();
|
||
applyFilters();
|
||
dlgForm.close();
|
||
toast('Paciente salvo com sucesso', 'ok');
|
||
}
|
||
|
||
function excluirPaciente(id){
|
||
const r = DB.find(x=>x.id===id);
|
||
if(!r) return;
|
||
// valida se há atendimentos futuros vinculados
|
||
if(r.atendimentos?.proximo){
|
||
toast('Não é possível excluir: há atendimento agendado. Cancele antes.', 'err');
|
||
return;
|
||
}
|
||
if(confirm('Confirma excluir este paciente?')){
|
||
DB = DB.filter(x=>x.id!==id); saveDB(DB); applyFilters(); toast('Excluído', 'ok');
|
||
}
|
||
}
|
||
|
||
// ===== CEP lookup (ViaCEP) =====
|
||
async function buscarCEP(){
|
||
const cep = $('#cep').value.replace(/\D/g,'');
|
||
if(cep.length!==8) return;
|
||
try{
|
||
const r = await fetch(`https://viacep.com.br/ws/${cep}/json/`);
|
||
const j = await r.json();
|
||
if(j.erro){ toast('CEP não encontrado','err'); return; }
|
||
$('#logradouro').value=j.logradouro||''; $('#bairro').value=j.bairro||''; $('#cidade').value=j.localidade||''; $('#estado').value=j.uf||'';
|
||
}catch(err){ console.error(err); toast('Falha ao consultar CEP','err'); }
|
||
}
|
||
|
||
// ===== Histórico =====
|
||
function abrirHistorico(){
|
||
const r = DB.find(x=>x.id===editingId); if(!r) return;
|
||
const body = $('#histBody'); body.innerHTML = (r.historico||[]).map(h=>`<div class="tag"><strong>${new Date(h.quando).toLocaleString()}</strong> — ${h.acao} por ${h.por}</div>`).join('');
|
||
dlgHistorico.showModal();
|
||
}
|
||
|
||
// ===== Consulta =====
|
||
function abrirConsulta(id){ editingId=id; $('#cData').value=''; $('#cHora').value=''; $('#cObs').value=''; dlgConsulta.showModal(); }
|
||
function salvarConsulta(){
|
||
const r = DB.find(x=>x.id===editingId); if(!r) return;
|
||
if(!$('#cData').value || !$('#cHora').value){ toast('Informe data e hora','err'); return; }
|
||
const when = new Date(`${$('#cData').value}T${$('#cHora').value}:00`);
|
||
r.atendimentos = r.atendimentos||{}; r.atendimentos.proximo = when.toISOString(); r.historico.push({quando:new Date().toISOString(), acao:'consulta agendada', por:'Usuário padrão'});
|
||
saveDB(DB); applyFilters(); dlgConsulta.close(); toast('Consulta marcada', 'ok');
|
||
}
|
||
|
||
// ===== Anexos =====
|
||
function removerAnexo(idx){
|
||
const r = DB.find(x=>x.id===editingId); if(!r) return; r.anexos.splice(idx,1); saveDB(DB); preencherForm(r);
|
||
}
|
||
|
||
// ===== CSV Import =====
|
||
async function importCSV(file){
|
||
const text = await file.text();
|
||
const lines = text.split(/\r?\n/).filter(Boolean);
|
||
const head = lines.shift().split(',').map(s=>s.trim().toLowerCase());
|
||
const rows = lines.map(l=>{
|
||
const cols = l.split(','); const rec = {};
|
||
head.forEach((h,i)=> rec[h] = (cols[i]||'').trim());
|
||
return rec;
|
||
});
|
||
let ok=0, fail=0;
|
||
rows.forEach(rec=>{
|
||
const novo = {
|
||
id: uid(), nome: rec.nome||'', nomeSocial:'', cpf:(rec.cpf||'').replace(/\D/g,''), rg:rec.rg||'', docTipo:'', docNumero:'',
|
||
sexo:'', nascimento: rec.nascimento||'', etnia:'', raca:'', naturalidade:'', nacionalidade:'Brasil',
|
||
profissao:'', estadoCivil:'', mae:'', profMae:'', pai:'', profPai:'', responsavel:'', cpfResponsavel:'', conjuge:'', rnGuia:false, codigoLegado:'',
|
||
convenio: rec.convenio||'', vip: /^true|1|sim$/i.test(rec.vip||''), obs:'', email: rec.email||'', celular: rec.celular||'', tel1:'', tel2:'',
|
||
endereco:{cep:'',logradouro:'',numero:'',complemento:'',bairro:'',cidade:rec.cidade||'',estado:rec.estado||'',referencia:''},
|
||
foto:'', anexos:[], criadoEm:new Date().toISOString(), atualizadoEm:new Date().toISOString(), historico:[{quando:new Date().toISOString(), acao:'importado CSV', por:'Usuário padrão'}], atendimentos:{ultimo:null, proximo:null}, custom:{}
|
||
};
|
||
const errs = validar(novo);
|
||
if(errs.length){ fail++; } else { DB.push(novo); ok++; }
|
||
});
|
||
saveDB(DB); refreshConvenios(); applyFilters();
|
||
toast(`Importação concluída. Sucesso: ${ok} | Ignorados: ${fail}`, ok? 'ok':'');
|
||
}
|
||
|
||
// ===== Campos personalizados =====
|
||
function renderCFAdmin(){
|
||
const list = loadCF();
|
||
$('#cfLista').innerHTML = list.length? list.map((c,idx)=>`<div class="tag">${c.nome} <span class="muted">(${c.tipo})</span> — <code>${c.key}</code> <button class="btn" onclick="remCF(${idx})">Remover</button></div>`).join('') : '<div class="muted">Nenhum campo adicionado.</div>'
|
||
}
|
||
function remCF(idx){ const list = loadCF(); list.splice(idx,1); saveCF(list); renderCFAdmin(); }
|
||
function addCF(){
|
||
const nome = $('#cfNome').value.trim(); const tipo=$('#cfTipo').value; const key=$('#cfKey').value.trim();
|
||
if(!nome||!key) return toast('Informe nome e chave','err');
|
||
const list = loadCF(); if(list.find(x=>x.key===key)) return toast('Chave já existe','err');
|
||
list.push({nome, tipo, key}); saveCF(list); $('#cfNome').value=''; $('#cfKey').value=''; renderCFAdmin(); toast('Campo adicionado','ok');
|
||
}
|
||
function renderCustomFields(values){
|
||
const defs = loadCF();
|
||
const host = $('#customFields'); host.innerHTML='';
|
||
defs.forEach(def=>{
|
||
const wrap = document.createElement('div'); wrap.className='field';
|
||
wrap.innerHTML = `<label>${def.nome}</label>${
|
||
def.tipo==='data'? `<input type="date" data-cf="${def.key}" value="${values[def.key]||''}">`:
|
||
def.tipo==='numero'? `<input type="number" data-cf="${def.key}" value="${values[def.key]||''}">`:
|
||
`<input data-cf="${def.key}" value="${values[def.key]||''}">`
|
||
}`;
|
||
host.appendChild(wrap);
|
||
})
|
||
}
|
||
function collectCustomFields(){
|
||
const values={}; $$('[data-cf]').forEach(el=> values[el.getAttribute('data-cf')] = el.value ); return values;
|
||
}
|
||
|
||
// ===== Helpers =====
|
||
function calcIdade(nasc){ if(!nasc) return null; const d=new Date(nasc); const t=new Date(); let a=t.getFullYear()-d.getFullYear(); const m=t.getMonth()-d.getMonth(); if(m<0||(m===0 && t.getDate()<d.getDate())) a--; return a; }
|
||
|
||
// ===== Eventos =====
|
||
const dlgForm = $('#dlgForm'); const dlgFiltro = $('#dlgFiltro'); const dlgHistorico = $('#dlgHistorico'); const dlgConsulta = $('#dlgConsulta'); const dlgImport = $('#dlgImport'); const dlgCampos = $('#dlgCampos');
|
||
|
||
// busca e filtros básicos
|
||
$('#q').addEventListener('input', applyFilters);
|
||
$('#filterConvenio').addEventListener('change', applyFilters);
|
||
$('#filterVip').addEventListener('click', e=>{ const on = e.currentTarget.getAttribute('aria-pressed')==='true'; e.currentTarget.setAttribute('aria-pressed', String(!on)); applyFilters(); })
|
||
$('#filterMes').addEventListener('change', applyFilters);
|
||
$('#btnFiltroAvancado').addEventListener('click', ()=> dlgFiltro.showModal());
|
||
|
||
// botões topo
|
||
$('#btnAdd').addEventListener('click', novoPaciente);
|
||
$('#btnImport').addEventListener('click', ()=> dlgImport.showModal());
|
||
$('#btnCampos').addEventListener('click', ()=>{ renderCFAdmin(); dlgCampos.showModal(); });
|
||
|
||
// form
|
||
$('#formPaciente').addEventListener('submit', salvar);
|
||
$('#btnExcluir').addEventListener('click', ()=>{ if(editingId) excluirPaciente(editingId); dlgForm.close(); });
|
||
$('#btnHistorico').addEventListener('click', abrirHistorico);
|
||
$('#btnMarcarConsulta').addEventListener('click', ()=> abrirConsulta(editingId));
|
||
|
||
// foto
|
||
$('#foto').addEventListener('change', e=>{
|
||
const f = e.target.files[0]; if(!f) return; const rd = new FileReader(); rd.onload = ev => $('#fotoPreview').src = ev.target.result; rd.readAsDataURL(f);
|
||
})
|
||
|
||
// anexos
|
||
$('#inputAnexo').addEventListener('change', e=>{
|
||
const f = e.target.files[0]; if(!f) return; const rd = new FileReader(); rd.onload = ev =>{
|
||
const r = DB.find(x=>x.id===editingId); if(!r){ toast('Salve o cadastro antes de anexar','err'); return; }
|
||
(r.anexos=r.anexos||[]).push({nome:f.name, data:ev.target.result, dataIso:new Date().toISOString()});
|
||
r.historico.push({quando:new Date().toISOString(), acao:`anexo adicionado (${f.name})`, por:'Usuário padrão'});
|
||
saveDB(DB); preencherForm(r); toast('Anexo adicionado','ok');
|
||
}; rd.readAsDataURL(f);
|
||
})
|
||
|
||
// mascaras
|
||
$('#cpf').addEventListener('input', e=> e.target.value = maskCPF(e.target.value));
|
||
$('#cpfResponsavel').addEventListener('input', e=> e.target.value = maskCPF(e.target.value));
|
||
$('#cep').addEventListener('input', e=> e.target.value = maskCEP(e.target.value));
|
||
$('#cep').addEventListener('blur', buscarCEP);
|
||
|
||
// nome clicável abre prontuário
|
||
document.addEventListener('click', e=>{ if(e.target.classList.contains('linkNome')){ e.preventDefault(); abrirProntuario(e.target.dataset.id, true); }});
|
||
|
||
// salvar consulta
|
||
$('#btnSalvarConsulta').addEventListener('click', salvarConsulta);
|
||
|
||
// importar csv
|
||
$('#btnImportCsv').addEventListener('click', ()=>{ const f = $('#csvFile').files[0]; if(!f) return toast('Selecione um arquivo CSV','err'); importCSV(f); dlgImport.close(); });
|
||
|
||
// campos personalizados
|
||
$('#btnAddCampo').addEventListener('click', addCF);
|
||
|
||
// scroll infinito
|
||
const io = new IntersectionObserver(entries=>{
|
||
entries.forEach(en=>{ if(en.isIntersecting) loadMore(); })
|
||
});
|
||
io.observe($('#sentinel'));
|
||
|
||
// init
|
||
refreshConvenios();
|
||
applyFilters();
|
||
</script>
|
||
</body>
|
||
</html>
|