CRUD
This commit is contained in:
commit
f44a0ce81c
564
Cadastro.html
Normal file
564
Cadastro.html
Normal file
@ -0,0 +1,564 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Cadastro/Edição de Paciente</title>
|
||||
<style>
|
||||
:root{
|
||||
--bg:#0b1221;--panel:#0f172a;--muted:#111827;--border:#263244;--text:#e5e7eb;--dim:#9ca3af;--accent:#22d3ee;--accent2:#60a5fa;--danger:#ef4444;--ok:#22c55e
|
||||
}
|
||||
*{box-sizing:border-box}
|
||||
html,body{height:100%}
|
||||
body{margin:0;background:linear-gradient(180deg,#08101e,var(--bg));color:var(--text);font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial}
|
||||
.container{max-width:1100px;margin:24px auto;padding:0 16px}
|
||||
header{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:16px}
|
||||
.title{font-weight:800;font-size:clamp(18px,3.6vw,28px)}
|
||||
.subtitle{color:var(--dim);font-size:14px}
|
||||
.card{background:rgba(17,24,39,.7);border:1px solid var(--border);border-radius:16px;padding:16px;margin-bottom:12px;backdrop-filter:blur(6px)}
|
||||
.row{display:grid;grid-template-columns:repeat(12,1fr);gap:12px}
|
||||
.col-12{grid-column:span 12}.col-9{grid-column:span 9}.col-8{grid-column:span 8}.col-6{grid-column:span 6}.col-4{grid-column:span 4}.col-3{grid-column:span 3}.col-2{grid-column:span 2}
|
||||
label{display:block;color:var(--dim);font-size:12px;margin:2px 0 6px}
|
||||
input[type=text],input[type=date],input[type=datetime-local],input[type=number],input[type=email],input[type=tel],select,textarea{width:100%;border:1px solid var(--border);border-radius:12px;background:#0b1221;color:var(--text);padding:10px 12px}
|
||||
textarea{resize:vertical}
|
||||
.btn{border:none;border-radius:12px;padding:10px 14px;font-weight:700;cursor:pointer}
|
||||
.btn.primary{background:linear-gradient(180deg,var(--accent),var(--accent2));color:#04121c;box-shadow:0 10px 24px rgba(34,211,238,.2)}
|
||||
.btn.secondary{background:#0b1221;border:1px solid var(--border);color:var(--text)}
|
||||
.btn.ghost{background:transparent;border:1px dashed var(--border);color:var(--text)}
|
||||
.btn.danger{background:var(--danger);color:white}
|
||||
.toolbar{display:flex;gap:8px;flex-wrap:wrap;align-items:center}
|
||||
.switch{display:inline-flex;align-items:center;gap:8px}
|
||||
.hint{color:var(--dim);font-size:12px}
|
||||
.thumb{width:96px;height:96px;border-radius:12px;border:1px dashed var(--border);display:flex;align-items:center;justify-content:center;overflow:hidden;background:#0b1221}
|
||||
.thumb img{width:100%;height:100%;object-fit:cover}
|
||||
.collapse{border-radius:16px;overflow:hidden;border:1px solid var(--border);margin-bottom:12px}
|
||||
.collapse summary{list-style:none;cursor:pointer;padding:12px 16px;background:#0b1221;color:var(--text);display:flex;align-items:center;justify-content:space-between}
|
||||
.collapse summary::-webkit-details-marker{display:none}
|
||||
.collapse .content{padding:16px;background:rgba(17,24,39,.7)}
|
||||
.tag{display:inline-flex;gap:6px;align-items:center;border:1px solid var(--border);border-radius:999px;padding:6px 10px;font-size:12px;color:var(--dim)}
|
||||
.table{width:100%;border-collapse:collapse}
|
||||
.table th,.table td{padding:10px;border-bottom:1px solid var(--border);text-align:left}
|
||||
.right{display:flex;justify-content:flex-end;gap:8px}
|
||||
.toast{position:fixed;right:16px;bottom:16px;background:#0b1221;border:1px solid var(--border);padding:12px 16px;border-radius:12px;box-shadow:0 12px 30px rgba(0,0,0,.35);display:none}
|
||||
.error{color:#fecaca}
|
||||
.ok{color:#bbf7d0}
|
||||
|
||||
@media(max-width:920px){.col-9{grid-column:span 12}.col-8{grid-column:span 12}.col-6{grid-column:span 12}.col-4{grid-column:span 12}.col-3{grid-column:span 12}.col-2{grid-column:span 12}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<div>
|
||||
<div class="title">Formulário de Paciente</div>
|
||||
<div class="subtitle">Cadastro e edição com validações, anexos, máscara de telefone e busca de CEP</div>
|
||||
</div>
|
||||
<div class="toolbar">
|
||||
<button class="btn secondary" id="btnCancelar">Cancelar</button>
|
||||
<button class="btn primary" id="btnSalvar">Salvar</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- DADOS PESSOAIS -->
|
||||
<section class="card" aria-labelledby="secDadosPessoais">
|
||||
<h2 id="secDadosPessoais" style="margin:0 0 8px">1. Dados pessoais</h2>
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
<label>Foto</label>
|
||||
<div class="row" style="align-items:end">
|
||||
<div class="thumb" id="fotoPreview" aria-label="Miniatura da foto">📷</div>
|
||||
<div class="col-12" style="margin-top:8px">
|
||||
<input type="file" id="fotoInput" accept="image/*" />
|
||||
<div class="hint">Clique em Carregar e selecione uma imagem</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-9">
|
||||
<div class="row">
|
||||
<div class="col-8">
|
||||
<label>Nome <span class="error">*</span></label>
|
||||
<input type="text" id="nome" required />
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<label>Nome social</label>
|
||||
<input type="text" id="nomeSocial" />
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<label>CPF</label>
|
||||
<input type="text" id="cpf" inputmode="numeric" placeholder="000.000.000-00" />
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<label>RG</label>
|
||||
<input type="text" id="rg" />
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<label>Outros documentos</label>
|
||||
<select id="docTipo">
|
||||
<option value="">Selecionar…</option>
|
||||
<option>CNH</option>
|
||||
<option>Passaporte</option>
|
||||
<option>RNE</option>
|
||||
<option>CRM</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<label>Número do documento</label>
|
||||
<input type="text" id="docNumero" placeholder="Preencha após escolher o tipo" />
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<label>Sexo</label>
|
||||
<div class="switch">
|
||||
<label><input type="radio" name="sexo" value="M"> Masculino</label>
|
||||
<label><input type="radio" name="sexo" value="F"> Feminino</label>
|
||||
<label><input type="radio" name="sexo" value="O"> Outro</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<label>Data de nascimento</label>
|
||||
<input type="date" id="nascimento" />
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<label>Estado civil</label>
|
||||
<select id="estadoCivil">
|
||||
<option value="">Selecionar…</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="col-4">
|
||||
<label>Raça/Cor (IBGE)</label>
|
||||
<select id="racaIbge">
|
||||
<option value="">Selecionar…</option>
|
||||
<option>Branca</option>
|
||||
<option>Preta</option>
|
||||
<option>Parda</option>
|
||||
<option>Amarela</option>
|
||||
<option>Indígena</option>
|
||||
<option>Não declarada</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<label>Etnia (se indígena)</label>
|
||||
<input type="text" id="etniaIbge" placeholder="Povo / Etnia" disabled />
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<label>Profissão</label>
|
||||
<input type="text" id="profissao" />
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<label>Naturalidade (Cidade/UF)</label>
|
||||
<input type="text" id="naturalidade" placeholder="Ex.: Maceió/AL" />
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<label>Nacionalidade</label>
|
||||
<select id="nacionalidade">
|
||||
<option>Brasil</option>
|
||||
<option>Argentina</option>
|
||||
<option>Portugal</option>
|
||||
<option>Estados Unidos</option>
|
||||
<option>Outra</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<label>Nome da mãe</label>
|
||||
<input type="text" id="mae" />
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<label>Profissão da mãe</label>
|
||||
<input type="text" id="profMae" />
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<label>Nome do pai</label>
|
||||
<input type="text" id="pai" />
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<label>Profissão do pai</label>
|
||||
<input type="text" id="profPai" />
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<label>Nome do responsável</label>
|
||||
<input type="text" id="responsavel" />
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<label>CPF do responsável</label>
|
||||
<input type="text" id="cpfResponsavel" placeholder="000.000.000-00" />
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<label>Nome do(a) esposo(a) (opcional)</label>
|
||||
<input type="text" id="conjuge" />
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<label>RN na Guia do convênio</label>
|
||||
<label class="switch"><input type="checkbox" id="rnConvenio"/> <span>Sim</span></label>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<label>Código legado</label>
|
||||
<input type="text" id="codigoLegado" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- OBSERVAÇÕES E ANEXOS -->
|
||||
<details class="collapse" open>
|
||||
<summary>
|
||||
<span>2. Observações e anexos</span>
|
||||
<span class="hint">Clique para expandir/retrair</span>
|
||||
</summary>
|
||||
<div class="content">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<label>Observações</label>
|
||||
<textarea id="observacoes" rows="4" placeholder="Alergias, restrições, uso de medicamentos, etc."></textarea>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="toolbar" style="justify-content:space-between">
|
||||
<div class="tag">📎 Anexos do paciente</div>
|
||||
<div>
|
||||
<input type="file" id="anexoInput" multiple />
|
||||
<button class="btn secondary" id="btnAddAnexo">Adicionar</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="overflow:auto;border:1px solid var(--border);border-radius:12px;margin-top:8px">
|
||||
<table class="table" id="anexosTable" aria-label="Lista de anexos">
|
||||
<thead>
|
||||
<tr><th>Nome</th><th>Tamanho</th><th>Data</th><th style="width:1%"></th></tr>
|
||||
</thead>
|
||||
<tbody id="anexosBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="hint">Arquivos ficam somente no navegador (localStorage) para este exemplo.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<!-- CONTATO -->
|
||||
<section class="card" aria-labelledby="secContato">
|
||||
<h2 id="secContato" style="margin:0 0 8px">3. Contato</h2>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<label>E-mail</label>
|
||||
<input type="email" id="email" placeholder="nome@dominio.com" />
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label>Celular</label>
|
||||
<input type="tel" id="celular" placeholder="+55 (82) 98888-8888" />
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label>Telefone 1</label>
|
||||
<input type="tel" id="tel1" placeholder="(82) 3333-3333" />
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label>Telefone 2</label>
|
||||
<input type="tel" id="tel2" placeholder="(82) 3444-4444" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ENDEREÇO -->
|
||||
<details class="collapse" open>
|
||||
<summary>
|
||||
<span>4. Endereço</span>
|
||||
<span class="hint">Com busca de CEP automática</span>
|
||||
</summary>
|
||||
<div class="content">
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
<label>CEP</label>
|
||||
<input type="text" id="cep" placeholder="00000-000" inputmode="numeric" />
|
||||
</div>
|
||||
<div class="col-7">
|
||||
<label>Logradouro</label>
|
||||
<input type="text" id="logradouro" />
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<label>Número</label>
|
||||
<input type="text" id="numero" />
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<label>Complemento</label>
|
||||
<input type="text" id="complemento" />
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<label>Bairro</label>
|
||||
<input type="text" id="bairro" />
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<label>Cidade</label>
|
||||
<input type="text" id="cidade" />
|
||||
</div>
|
||||
<div class="col-1">
|
||||
<label>UF</label>
|
||||
<input type="text" id="uf" maxlength="2" />
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label>Referência</label>
|
||||
<input type="text" id="referencia" placeholder="Ponto de referência (opcional)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div class="right" style="margin-top:16px">
|
||||
<button class="btn secondary" id="btnCancelar2">Cancelar</button>
|
||||
<button class="btn primary" id="btnSalvar2">Salvar</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toast" id="toast"></div>
|
||||
|
||||
<script>
|
||||
// ===== Utilidades =====
|
||||
const fmtBytes = (n)=>{
|
||||
if (n<1024) return n+" B"; if (n<1024**2) return (n/1024).toFixed(1)+" KB"; if (n<1024**3) return (n/1024**2).toFixed(1)+" MB"; return (n/1024**3).toFixed(1)+" GB";
|
||||
}
|
||||
const showToast=(msg,type='ok')=>{ const t=document.getElementById('toast'); t.textContent=msg; t.style.display='block'; t.className = 'toast '+type; setTimeout(()=>t.style.display='none', 2600); }
|
||||
|
||||
// Máscaras simples
|
||||
const onlyDigits = (s)=> (s||'').replace(/\D/g,'');
|
||||
const maskCPF = (s)=>{
|
||||
const d=onlyDigits(s).slice(0,11);
|
||||
return d.replace(/(\d{3})(\d)/,'$1.$2').replace(/(\d{3})(\d)/,'$1.$2').replace(/(\d{3})(\d{1,2})$/,'$1-$2');
|
||||
}
|
||||
const maskCEP = s=>{
|
||||
const d=onlyDigits(s).slice(0,8);
|
||||
return d.replace(/(\d{5})(\d{0,3})/,'$1-$2');
|
||||
}
|
||||
const maskPhoneBR = s=>{
|
||||
const d=onlyDigits(s).slice(0,13); // inclui 55
|
||||
// +55 (82) 98888-8888 ou (82) 3333-3333
|
||||
let out = d;
|
||||
if (out.startsWith('55')) out = '+'+out.slice(0,2)+' '+out.slice(2);
|
||||
out = out.replace(/^(\+55)\s?(\d{0,2})/, (m,p1,ddd)=> p1 + (ddd?` (${ddd}) `:' '));
|
||||
const rest = onlyDigits(out.replace(/\+55|\(|\)|\s|-/g,''));
|
||||
if (rest.length<=10) return out.split(' ').length>1? out.split(' ')[0]+" ("+rest.slice(0,2)+") "+rest.slice(2,6)+(rest.length>6?"-"+rest.slice(6):'') : out;
|
||||
return `+55 (${rest.slice(0,2)}) ${rest.slice(2,7)}-${rest.slice(7,11)}`;
|
||||
}
|
||||
const maskPhoneLocal = s=>{
|
||||
const d=onlyDigits(s).slice(0,11);
|
||||
if (d.length<=10) return d.replace(/(\d{2})(\d{4})(\d{0,4})/, '($1) $2-$3');
|
||||
return d.replace(/(\d{2})(\d{5})(\d{0,4})/, '($1) $2-$3');
|
||||
}
|
||||
|
||||
// Validador de CPF
|
||||
function isValidCPF(v){
|
||||
const d = onlyDigits(v);
|
||||
if (d.length!==11) return false;
|
||||
if (/^(\d)\1{10}$/.test(d)) return false; // repetidos
|
||||
let sum=0; for(let i=0;i<9;i++) sum += parseInt(d[i]) * (10-i);
|
||||
let r = 11 - (sum % 11); const d1 = (r>=10)?0:r;
|
||||
sum=0; for(let i=0;i<10;i++) sum += parseInt(d[i]) * (11-i);
|
||||
r = 11 - (sum % 11); const d2 = (r>=10)?0:r;
|
||||
return d1==d[9] && d2==d[10];
|
||||
}
|
||||
|
||||
// Elementos
|
||||
const fotoInput = document.getElementById('fotoInput');
|
||||
const fotoPreview = document.getElementById('fotoPreview');
|
||||
const nome = document.getElementById('nome');
|
||||
const nomeSocial = document.getElementById('nomeSocial');
|
||||
const cpf = document.getElementById('cpf');
|
||||
const rg = document.getElementById('rg');
|
||||
const docTipo = document.getElementById('docTipo');
|
||||
const docNumero = document.getElementById('docNumero');
|
||||
const nascimento = document.getElementById('nascimento');
|
||||
const racaIbge = document.getElementById('racaIbge');
|
||||
const etniaIbge = document.getElementById('etniaIbge');
|
||||
const profissao = document.getElementById('profissao');
|
||||
const naturalidade = document.getElementById('naturalidade');
|
||||
const nacionalidade = document.getElementById('nacionalidade');
|
||||
const mae = document.getElementById('mae');
|
||||
const profMae = document.getElementById('profMae');
|
||||
const pai = document.getElementById('pai');
|
||||
const profPai = document.getElementById('profPai');
|
||||
const responsavel = document.getElementById('responsavel');
|
||||
const cpfResponsavel = document.getElementById('cpfResponsavel');
|
||||
const conjuge = document.getElementById('conjuge');
|
||||
const rnConvenio = document.getElementById('rnConvenio');
|
||||
const codigoLegado = document.getElementById('codigoLegado');
|
||||
|
||||
const observacoes = document.getElementById('observacoes');
|
||||
const anexoInput = document.getElementById('anexoInput');
|
||||
const btnAddAnexo = document.getElementById('btnAddAnexo');
|
||||
const anexosBody = document.getElementById('anexosBody');
|
||||
|
||||
const email = document.getElementById('email');
|
||||
const celular = document.getElementById('celular');
|
||||
const tel1 = document.getElementById('tel1');
|
||||
const tel2 = document.getElementById('tel2');
|
||||
|
||||
const cep = document.getElementById('cep');
|
||||
const logradouro = document.getElementById('logradouro');
|
||||
const numero = document.getElementById('numero');
|
||||
const complemento = document.getElementById('complemento');
|
||||
const bairro = document.getElementById('bairro');
|
||||
const cidade = document.getElementById('cidade');
|
||||
const uf = document.getElementById('uf');
|
||||
const referencia = document.getElementById('referencia');
|
||||
|
||||
const btnSalvar = document.getElementById('btnSalvar');
|
||||
const btnSalvar2 = document.getElementById('btnSalvar2');
|
||||
const btnCancelar = document.getElementById('btnCancelar');
|
||||
const btnCancelar2 = document.getElementById('btnCancelar2');
|
||||
|
||||
// Estado de anexos (mantido no navegador)
|
||||
let ANEXOS = []; // {name,size,ts,dataURL}
|
||||
|
||||
// Foto upload
|
||||
fotoInput.addEventListener('change', async (e)=>{
|
||||
const f = e.target.files?.[0];
|
||||
if (!f) return;
|
||||
const url = URL.createObjectURL(f);
|
||||
fotoPreview.innerHTML = `<img alt="foto" src="${url}">`;
|
||||
});
|
||||
|
||||
// Habilita etnia apenas quando raça=Indígena
|
||||
racaIbge.addEventListener('change', ()=>{
|
||||
if (racaIbge.value === 'Indígena'){ etniaIbge.disabled = false; etniaIbge.placeholder = 'Informe o povo/etnia'; }
|
||||
else { etniaIbge.disabled = true; etniaIbge.value=''; etniaIbge.placeholder = 'Povo / Etnia'; }
|
||||
});
|
||||
|
||||
// Doc. número habilita apenas se tipo escolhido
|
||||
docTipo.addEventListener('change', ()=>{ docNumero.disabled = !docTipo.value; if (!docTipo.value) docNumero.value=''; });
|
||||
|
||||
// Máscaras
|
||||
const applyMask = (el, fn) => el.addEventListener('input', ()=> { const pos=el.selectionStart; const before=el.value; el.value = fn(el.value); if (document.activeElement===el && before!==el.value) el.setSelectionRange(el.value.length, el.value.length); });
|
||||
applyMask(cpf, maskCPF);
|
||||
applyMask(cpfResponsavel, maskCPF);
|
||||
applyMask(cep, maskCEP);
|
||||
applyMask(celular, maskPhoneBR);
|
||||
applyMask(tel1, maskPhoneLocal);
|
||||
applyMask(tel2, maskPhoneLocal);
|
||||
|
||||
// CEP -> ViaCEP
|
||||
async function fetchCEP(v){
|
||||
const d = onlyDigits(v);
|
||||
if (d.length!==8) return;
|
||||
try{
|
||||
const res = await fetch(`https://viacep.com.br/ws/${d}/json/`);
|
||||
const j = await res.json();
|
||||
if (j.erro){ showToast('CEP não encontrado','error'); return; }
|
||||
logradouro.value = j.logradouro||''; bairro.value=j.bairro||''; cidade.value=j.localidade||''; uf.value=j.uf||'';
|
||||
}catch(err){ console.error(err); showToast('Erro ao consultar CEP','error'); }
|
||||
}
|
||||
cep.addEventListener('change', ()=> fetchCEP(cep.value));
|
||||
|
||||
// Anexos: adicionar e listar
|
||||
btnAddAnexo.addEventListener('click', async ()=>{
|
||||
const files = Array.from(anexoInput.files||[]);
|
||||
if (!files.length){ showToast('Selecione arquivos para anexar','error'); return; }
|
||||
for (const f of files){
|
||||
const dataURL = await new Promise(res=>{ const r=new FileReader(); r.onload=()=>res(r.result); r.readAsDataURL(f); });
|
||||
ANEXOS.push({ name:f.name, size:f.size, ts:Date.now(), dataURL });
|
||||
}
|
||||
anexoInput.value='';
|
||||
renderAnexos();
|
||||
});
|
||||
|
||||
function renderAnexos(){
|
||||
anexosBody.innerHTML='';
|
||||
ANEXOS.sort((a,b)=>b.ts-a.ts).forEach((a,idx)=>{
|
||||
const tr=document.createElement('tr');
|
||||
tr.innerHTML = `<td><a download="${a.name}" href="${a.dataURL}">${a.name}</a></td><td>${fmtBytes(a.size)}</td><td>${new Date(a.ts).toLocaleString('pt-BR')}</td>`;
|
||||
const td=document.createElement('td');
|
||||
const del=document.createElement('button'); del.className='btn danger'; del.textContent='Excluir'; del.addEventListener('click',()=>{ ANEXOS.splice(idx,1); renderAnexos(); });
|
||||
td.appendChild(del); tr.appendChild(td); anexosBody.appendChild(tr);
|
||||
});
|
||||
if (!ANEXOS.length){
|
||||
const tr=document.createElement('tr'); tr.innerHTML = '<td colspan="4" class="hint">Nenhum anexo adicionado.</td>'; anexosBody.appendChild(tr);
|
||||
}
|
||||
}
|
||||
renderAnexos();
|
||||
|
||||
// Validações básicas
|
||||
function validate(){
|
||||
const errors=[];
|
||||
if (!nome.value.trim()) errors.push('Nome é obrigatório.');
|
||||
if (cpf.value.trim() && !isValidCPF(cpf.value)) errors.push('CPF inválido.');
|
||||
if (cpfResponsavel.value.trim() && !isValidCPF(cpfResponsavel.value)) errors.push('CPF do responsável inválido.');
|
||||
if (email.value.trim() && !/^\S+@\S+\.\S+$/.test(email.value)) errors.push('E-mail em formato inválido.');
|
||||
if (docTipo.value && !docNumero.value.trim()) errors.push('Informe o número do documento selecionado.');
|
||||
return errors;
|
||||
}
|
||||
|
||||
function getSexo(){ const r = document.querySelector('input[name="sexo"]:checked'); return r? r.value : ''; }
|
||||
|
||||
// Salvar no localStorage como exemplo
|
||||
function collectData(){
|
||||
return {
|
||||
foto: fotoPreview.querySelector('img')?.src || null,
|
||||
nome: nome.value.trim(),
|
||||
nomeSocial: nomeSocial.value.trim(),
|
||||
cpf: cpf.value.trim(),
|
||||
rg: rg.value.trim(),
|
||||
docTipo: docTipo.value,
|
||||
docNumero: docNumero.value.trim(),
|
||||
sexo: getSexo(),
|
||||
nascimento: nascimento.value||null,
|
||||
racaIbge: racaIbge.value,
|
||||
etniaIbge: etniaIbge.value.trim(),
|
||||
profissao: profissao.value.trim(),
|
||||
naturalidade: naturalidade.value.trim(),
|
||||
nacionalidade: nacionalidade.value,
|
||||
mae: mae.value.trim(),
|
||||
profMae: profMae.value.trim(),
|
||||
pai: pai.value.trim(),
|
||||
profPai: profPai.value.trim(),
|
||||
responsavel: responsavel.value.trim(),
|
||||
cpfResponsavel: cpfResponsavel.value.trim(),
|
||||
conjuge: conjuge.value.trim(),
|
||||
rnConvenio: rnConvenio.checked,
|
||||
codigoLegado: codigoLegado.value.trim(),
|
||||
observacoes: observacoes.value,
|
||||
anexos: ANEXOS,
|
||||
contato: { email: email.value.trim(), celular: celular.value.trim(), tel1: tel1.value.trim(), tel2: tel2.value.trim() },
|
||||
endereco: { cep: cep.value.trim(), logradouro: logradouro.value.trim(), numero: numero.value.trim(), complemento: complemento.value.trim(), bairro: bairro.value.trim(), cidade: cidade.value.trim(), uf: uf.value.trim().toUpperCase(), referencia: referencia.value.trim() },
|
||||
updatedAt: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
function save(){
|
||||
const errs = validate();
|
||||
if (errs.length){ alert('Corrija os seguintes erros:\n\n- '+errs.join('\n- ')); return; }
|
||||
const data = collectData();
|
||||
const listKey = 'lista_pacientes_data_v1';
|
||||
const exists = JSON.parse(localStorage.getItem(listKey)||'[]');
|
||||
// Se já existir um registro com mesmo CPF, atualiza; senão cria novo
|
||||
const idx = data.cpf ? exists.findIndex(p=> (p.doc||'') === data.cpf) : -1;
|
||||
const toSave = {
|
||||
id: idx>=0 ? exists[idx].id : crypto.randomUUID(),
|
||||
nome: data.nome || '(sem nome)',
|
||||
doc: data.cpf || '',
|
||||
vip: exists[idx]?.vip || false,
|
||||
telefone: exists[idx]?.telefone || '',
|
||||
cidade: exists[idx]?.cidade || '',
|
||||
uf: exists[idx]?.uf || '',
|
||||
convenio: exists[idx]?.convenio || 'Particular',
|
||||
idade: exists[idx]?.idade || null,
|
||||
nascimento: data.nascimento,
|
||||
ultimo: exists[idx]?.ultimo || null,
|
||||
proximo: exists[idx]?.proximo || null,
|
||||
obs: data.observacoes || ''
|
||||
};
|
||||
if (idx>=0) exists[idx] = toSave; else exists.unshift(toSave);
|
||||
localStorage.setItem(listKey, JSON.stringify(exists));
|
||||
localStorage.setItem('paciente_detalhe', JSON.stringify(data)); // dump completo
|
||||
showToast('Paciente salvo com sucesso','ok');
|
||||
}
|
||||
|
||||
function cancel(){
|
||||
if (confirm('Deseja cancelar? Alterações não salvas serão perdidas.')){
|
||||
window.location.href = 'lista-pacientes.html'; // se existir; caso contrário, permanece
|
||||
}
|
||||
}
|
||||
|
||||
btnSalvar.addEventListener('click', save); btnSalvar2.addEventListener('click', save);
|
||||
btnCancelar.addEventListener('click', cancel); btnCancelar2.addEventListener('click', cancel);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
703
Lista de pacientes.html
Normal file
703
Lista de pacientes.html
Normal file
@ -0,0 +1,703 @@
|
||||
<!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>
|
27
Página inicial.html
Normal file
27
Página inicial.html
Normal file
@ -0,0 +1,27 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-br">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Página Inicial</title>
|
||||
<style>
|
||||
button {
|
||||
padding: 10px 20px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>Bem-vindo à Clínica</h1>
|
||||
|
||||
<button onclick="abrirFormulario()">Adicionar Paciente</button>
|
||||
|
||||
<script>
|
||||
function abrirFormulario() {
|
||||
window.location.href = 'Cadastro.html';
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
858
Teste do site completo.html
Normal file
858
Teste do site completo.html
Normal file
@ -0,0 +1,858 @@
|
||||
<!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>
|
Loading…
x
Reference in New Issue
Block a user