riseup-squad19/Cadastro.html
2025-08-16 07:56:07 -03:00

564 lines
25 KiB
HTML

<!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>