Merge branch 'develop' into feature/user-profile-api
This commit is contained in:
commit
c9df4eceaa
@ -14,6 +14,35 @@ import ProtectedRoute from "@/components/ProtectedRoute"; // <-- IMPORTADO
|
||||
|
||||
import { listarMedicos, excluirMedico, Medico } from "@/lib/api";
|
||||
|
||||
function normalizeMedico(m: any): Medico {
|
||||
return {
|
||||
id: String(m.id ?? m.uuid ?? ""),
|
||||
nome: m.nome ?? m.full_name ?? "", // 👈 Supabase usa full_name
|
||||
nome_social: m.nome_social ?? m.social_name ?? null,
|
||||
cpf: m.cpf ?? "",
|
||||
rg: m.rg ?? m.document_number ?? null,
|
||||
sexo: m.sexo ?? m.sex ?? null,
|
||||
data_nascimento: m.data_nascimento ?? m.birth_date ?? null,
|
||||
telefone: m.telefone ?? m.phone_mobile ?? "",
|
||||
celular: m.celular ?? m.phone2 ?? null,
|
||||
contato_emergencia: m.contato_emergencia ?? null,
|
||||
email: m.email ?? "",
|
||||
crm: m.crm ?? "",
|
||||
estado_crm: m.estado_crm ?? m.crm_state ?? null,
|
||||
rqe: m.rqe ?? null,
|
||||
formacao_academica: m.formacao_academica ?? [],
|
||||
curriculo_url: m.curriculo_url ?? null,
|
||||
especialidade: m.especialidade ?? m.specialty ?? "",
|
||||
observacoes: m.observacoes ?? m.notes ?? null,
|
||||
foto_url: m.foto_url ?? null,
|
||||
tipo_vinculo: m.tipo_vinculo ?? null,
|
||||
dados_bancarios: m.dados_bancarios ?? null,
|
||||
agenda_horario: m.agenda_horario ?? null,
|
||||
valor_consulta: m.valor_consulta ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export default function DoutoresPage() {
|
||||
const [doctors, setDoctors] = useState<Medico[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@ -27,7 +56,8 @@ export default function DoutoresPage() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const list = await listarMedicos({ limit: 50 });
|
||||
setDoctors(list ?? []);
|
||||
setDoctors((list ?? []).map(normalizeMedico));
|
||||
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@ -53,6 +83,8 @@ export default function DoutoresPage() {
|
||||
setShowForm(true);
|
||||
}
|
||||
|
||||
|
||||
|
||||
function handleEdit(id: string) {
|
||||
setEditingId(id);
|
||||
setShowForm(true);
|
||||
@ -70,10 +102,29 @@ export default function DoutoresPage() {
|
||||
}
|
||||
|
||||
|
||||
async function handleSaved() {
|
||||
function handleSaved(savedDoctor?: Medico) {
|
||||
setShowForm(false);
|
||||
await load();
|
||||
|
||||
if (savedDoctor) {
|
||||
const normalized = normalizeMedico(savedDoctor);
|
||||
setDoctors((prev) => {
|
||||
const i = prev.findIndex((d) => String(d.id) === String(normalized.id));
|
||||
if (i < 0) {
|
||||
// Novo médico → adiciona no topo
|
||||
return [normalized, ...prev];
|
||||
} else {
|
||||
// Médico editado → substitui na lista
|
||||
const clone = [...prev];
|
||||
clone[i] = normalized;
|
||||
return clone;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// fallback → recarrega tudo
|
||||
load();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@ -17,30 +17,31 @@ import { PatientRegistrationForm } from "@/components/forms/patient-registration
|
||||
function normalizePaciente(p: any): Paciente {
|
||||
const endereco: Endereco = {
|
||||
cep: p.endereco?.cep ?? p.cep ?? "",
|
||||
logradouro: p.endereco?.logradouro ?? p.logradouro ?? "",
|
||||
numero: p.endereco?.numero ?? p.numero ?? "",
|
||||
complemento: p.endereco?.complemento ?? p.complemento ?? "",
|
||||
bairro: p.endereco?.bairro ?? p.bairro ?? "",
|
||||
cidade: p.endereco?.cidade ?? p.cidade ?? "",
|
||||
estado: p.endereco?.estado ?? p.estado ?? "",
|
||||
logradouro: p.endereco?.logradouro ?? p.street ?? "",
|
||||
numero: p.endereco?.numero ?? p.number ?? "",
|
||||
complemento: p.endereco?.complemento ?? p.complement ?? "",
|
||||
bairro: p.endereco?.bairro ?? p.neighborhood ?? "",
|
||||
cidade: p.endereco?.cidade ?? p.city ?? "",
|
||||
estado: p.endereco?.estado ?? p.state ?? "",
|
||||
};
|
||||
|
||||
return {
|
||||
id: String(p.id ?? p.uuid ?? p.paciente_id ?? ""),
|
||||
nome: p.nome ?? "",
|
||||
nome_social: p.nome_social ?? null,
|
||||
nome: p.full_name ?? "", // 👈 troca nome → full_name
|
||||
nome_social: p.social_name ?? null, // 👈 Supabase usa social_name
|
||||
cpf: p.cpf ?? "",
|
||||
rg: p.rg ?? null,
|
||||
sexo: p.sexo ?? null,
|
||||
data_nascimento: p.data_nascimento ?? null,
|
||||
telefone: p.telefone ?? "",
|
||||
rg: p.rg ?? p.document_number ?? null, // 👈 às vezes vem como document_number
|
||||
sexo: p.sexo ?? p.sex ?? null, // 👈 Supabase usa sex
|
||||
data_nascimento: p.data_nascimento ?? p.birth_date ?? null,
|
||||
telefone: p.telefone ?? p.phone_mobile ?? "",
|
||||
email: p.email ?? "",
|
||||
endereco,
|
||||
observacoes: p.observacoes ?? null,
|
||||
observacoes: p.observacoes ?? p.notes ?? null,
|
||||
foto_url: p.foto_url ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export default function PacientesPage() {
|
||||
const [patients, setPatients] = useState<Paciente[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
@ -4,9 +4,9 @@ import { AuthProvider } from "@/hooks/useAuth"
|
||||
import "./globals.css"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "MediConecta - Conectando Pacientes e Profissionais de Saúde",
|
||||
title: "MediConnect - Conectando Pacientes e Profissionais de Saúde",
|
||||
description:
|
||||
"Plataforma inovadora que conecta pacientes e médicos de forma prática, segura e humanizada. Experimente o futuro dos agendamentos médicos.",
|
||||
"Plataforma inovadora que conecta pacientes, clínicas, e médicos de forma prática, segura e humanizada. Experimente o futuro dos agendamentos médicos.",
|
||||
keywords: "saúde, médicos, pacientes, agendamento, telemedicina, SUS",
|
||||
generator: 'v0.app'
|
||||
}
|
||||
|
||||
@ -1514,24 +1514,39 @@ const ProfissionalPage = () => {
|
||||
);
|
||||
// --- LaudoEditor COMPONENT ---
|
||||
function LaudoEditor() {
|
||||
|
||||
const pacientes = [
|
||||
{ nome: "Ana Souza", cpf: "123.456.789-00", idade: 32, sexo: "Feminino" },
|
||||
{ nome: "Bruno Lima", cpf: "987.654.321-00", idade: 45, sexo: "Masculino" },
|
||||
{ nome: "Carla Menezes", cpf: "111.222.333-44", idade: 28, sexo: "Feminino" },
|
||||
];
|
||||
|
||||
const [conteudo, setConteudo] = useState("");
|
||||
const [paciente, setPaciente] = useState("");
|
||||
const [cpf, setCpf] = useState("");
|
||||
const [pacienteSelecionado, setPacienteSelecionado] = useState<any>(null);
|
||||
const [cid, setCid] = useState("");
|
||||
const [imagem, setImagem] = useState<string | null>(null);
|
||||
const [assinatura, setAssinatura] = useState<string | null>(null);
|
||||
const sigCanvasRef = useRef<any>(null);
|
||||
const [idade, setIdade] = useState("");
|
||||
const [sexo, setSexo] = useState("");
|
||||
const [cid, setCid] = useState("");
|
||||
const [laudos, setLaudos] = useState<any[]>([]);
|
||||
const [preview, setPreview] = useState(false);
|
||||
|
||||
|
||||
const handleSelectPaciente = (paciente: any) => {
|
||||
setPacienteSelecionado(paciente);
|
||||
};
|
||||
|
||||
const limparPaciente = () => setPacienteSelecionado(null);
|
||||
|
||||
const salvarLaudo = (status: string) => {
|
||||
if (!pacienteSelecionado) {
|
||||
alert('Selecione um paciente.');
|
||||
return;
|
||||
}
|
||||
const novoLaudo = {
|
||||
paciente,
|
||||
cpf,
|
||||
idade,
|
||||
sexo,
|
||||
paciente: pacienteSelecionado.nome,
|
||||
cpf: pacienteSelecionado.cpf,
|
||||
idade: pacienteSelecionado.idade,
|
||||
sexo: pacienteSelecionado.sexo,
|
||||
cid,
|
||||
conteudo,
|
||||
imagem,
|
||||
@ -1540,28 +1555,103 @@ function LaudoEditor() {
|
||||
status
|
||||
};
|
||||
setLaudos(prev => [novoLaudo, ...prev]);
|
||||
setPaciente(""); setCpf(""); setIdade(""); setSexo(""); setCid(""); setConteudo(""); setImagem(null); setAssinatura(null);
|
||||
setPacienteSelecionado(null); setCid(""); setConteudo(""); setImagem(null); setAssinatura(null);
|
||||
if (sigCanvasRef.current) sigCanvasRef.current.clear();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-muted p-6">
|
||||
<div className="flex flex-col md:flex-row gap-6">
|
||||
<div className="bg-white p-6 rounded-lg shadow-md w-full md:w-1/3 flex flex-col gap-4">
|
||||
<h2 className="text-2xl font-bold text-primary text-center mb-4">Informações do Paciente</h2>
|
||||
<input type="text" placeholder="Nome do paciente" value={paciente} onChange={e => setPaciente(e.target.value)} className="p-3 border rounded-md focus:ring-2 focus:ring-primary/50 focus:outline-none"/>
|
||||
<input type="text" placeholder="CPF" value={cpf} onChange={e => setCpf(e.target.value)} className="p-3 border rounded-md focus:ring-2 focus:ring-primary/50 focus:outline-none"/>
|
||||
<input type="number" placeholder="Idade" value={idade} onChange={e => setIdade(e.target.value)} className="p-3 border rounded-md focus:ring-2 focus:ring-primary/50 focus:outline-none"/>
|
||||
<select value={sexo} onChange={e => setSexo(e.target.value)} className="p-3 border rounded-md focus:ring-2 focus:ring-primary/50 focus:outline-none">
|
||||
<option value="">Sexo</option>
|
||||
<option value="Masculino">Masculino</option>
|
||||
<option value="Feminino">Feminino</option>
|
||||
<option value="Outro">Outro</option>
|
||||
<div className="min-h-screen bg-gradient-to-br from-primary/5 to-white p-2 md:p-4">
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<div className="bg-white border border-primary/10 shadow-lg rounded-xl p-4 md:p-6 mb-6">
|
||||
<h2 className="text-2xl font-bold text-primary mb-4 text-center tracking-tight">Laudo Médico</h2>
|
||||
|
||||
{!pacienteSelecionado ? (
|
||||
<div className="flex flex-col items-center justify-center py-8 px-1 bg-white rounded-xl shadow-sm">
|
||||
<div className="mb-2">
|
||||
<svg width="40" height="40" fill="none" viewBox="0 0 24 24" className="mx-auto text-gray-400">
|
||||
<circle cx="12" cy="8" r="4" stroke="currentColor" strokeWidth="2"/>
|
||||
<path d="M4 20c0-2.5 3.5-4.5 8-4.5s8 2 8 4.5" stroke="currentColor" strokeWidth="2"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="text-lg font-bold text-center text-gray-800 mb-1">Selecionar Paciente</h2>
|
||||
<p className="text-gray-500 text-center mb-4 max-w-xs text-sm">Escolha um paciente para visualizar o prontuário completo</p>
|
||||
<div className="w-full max-w-xs">
|
||||
<label htmlFor="select-paciente" className="block text-sm font-medium text-gray-700 mb-1">Escolha o paciente:</label>
|
||||
<div className="relative">
|
||||
<select
|
||||
id="select-paciente"
|
||||
className="w-full p-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-200 focus:border-blue-300 focus:outline-none bg-white text-sm text-gray-900 shadow-sm transition-all appearance-none"
|
||||
onChange={e => {
|
||||
const p = pacientes.find(p => p.cpf === e.target.value);
|
||||
if (p) handleSelectPaciente(p);
|
||||
}}
|
||||
defaultValue=""
|
||||
>
|
||||
<option value="" className="text-gray-400">Selecione um paciente...</option>
|
||||
{pacientes.map(p => (
|
||||
<option key={p.cpf} value={p.cpf}>{p.nome} - {p.cpf}</option>
|
||||
))}
|
||||
</select>
|
||||
<input type="text" placeholder="CID" value={cid} onChange={e => setCid(e.target.value)} className="p-3 border rounded-md focus:ring-2 focus:ring-primary/50 focus:outline-none"/>
|
||||
<span className="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 pointer-events-none">
|
||||
<svg width="16" height="16" fill="none" viewBox="0 0 24 24"><path d="M12 14l-4-4h8l-4 4z" fill="currentColor"/></svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-primary/10 border border-primary/20 rounded-lg p-3 mb-4 flex flex-col md:flex-row md:items-center md:justify-between gap-2">
|
||||
<div>
|
||||
<label className="block mb-1 font-medium text-primary">Imagem (opcional)</label>
|
||||
<input type="file" accept="image/*" onChange={e => {
|
||||
<div className="font-bold text-primary text-base mb-0.5">{pacienteSelecionado.nome}</div>
|
||||
<div className="text-xs text-gray-700">CPF: {pacienteSelecionado.cpf}</div>
|
||||
<div className="text-xs text-gray-700">Idade: {pacienteSelecionado.idade}</div>
|
||||
<div className="text-xs text-gray-700">Sexo: {pacienteSelecionado.sexo}</div>
|
||||
</div>
|
||||
<button type="button" onClick={limparPaciente} className="px-3 py-1 rounded bg-red-100 text-red-700 hover:bg-red-200 font-semibold shadow text-xs">Trocar paciente</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<label className="block text-sm font-semibold text-primary">CID</label>
|
||||
<input
|
||||
type="text"
|
||||
value={cid}
|
||||
onChange={e => setCid(e.target.value)}
|
||||
placeholder="Ex: I10, E11, etc."
|
||||
className="w-full p-2 border border-primary/20 rounded-md focus:ring-2 focus:ring-primary/30 focus:outline-none text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label className="block text-sm font-semibold text-primary">Conteúdo do Laudo *</label>
|
||||
{!preview ? (
|
||||
<ReactQuill
|
||||
value={conteudo}
|
||||
onChange={setConteudo}
|
||||
modules={{
|
||||
toolbar: [
|
||||
['bold', 'italic', 'underline'],
|
||||
[{ 'list': 'ordered' }, { 'list': 'bullet' }],
|
||||
[{ 'align': [] }],
|
||||
[{ 'size': ['small', false, 'large', 'huge'] }],
|
||||
['clean']
|
||||
]
|
||||
}}
|
||||
className="h-40 border border-primary/20 rounded-md text-sm"
|
||||
/>
|
||||
) : (
|
||||
<div className="border border-primary/20 p-2 rounded-md bg-muted overflow-auto">
|
||||
<h3 className="text-base font-semibold mb-1 text-primary">Pré-visualização:</h3>
|
||||
<div dangerouslySetInnerHTML={{ __html: conteudo }} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label className="block text-sm font-semibold text-primary mb-2">Imagem (opcional)</label>
|
||||
<div className="mb-2"></div>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={e => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
@ -1570,85 +1660,96 @@ function LaudoEditor() {
|
||||
} else {
|
||||
setImagem(null);
|
||||
}
|
||||
}} className="block w-full text-sm text-muted-foreground file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-primary/10 file:text-primary hover:file:bg-primary/20" />
|
||||
}}
|
||||
className="block w-full text-xs text-muted-foreground file:mr-2 file:py-1 file:px-2 file:rounded-md file:border-0 file:text-xs file:font-semibold file:bg-primary/10 file:text-primary hover:file:bg-primary/20"
|
||||
/>
|
||||
{imagem && (
|
||||
<img src={imagem} alt="Pré-visualização" className="mt-2 rounded-md max-h-32 border" />
|
||||
<img src={imagem} alt="Pré-visualização" className="mt-1 rounded-md max-h-20 border border-primary/20" />
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label className="block mb-1 font-medium text-primary">Assinatura Digital</label>
|
||||
<div className="bg-muted rounded-md border p-2 flex flex-col items-center">
|
||||
<div className="space-y-1">
|
||||
<label className="block text-sm font-semibold text-primary">Assinatura Digital</label>
|
||||
<div className="bg-muted rounded-md border border-primary/20 p-2 flex flex-col items-center">
|
||||
<SignatureCanvas
|
||||
ref={sigCanvasRef}
|
||||
penColor="#0f172a"
|
||||
backgroundColor="#fff"
|
||||
canvasProps={{ width: 300, height: 100, className: "rounded border bg-white" }}
|
||||
canvasProps={{ width: 220, height: 60, className: "rounded-md border bg-white shadow" }}
|
||||
onEnd={() => setAssinatura(sigCanvasRef.current?.isEmpty() ? null : sigCanvasRef.current?.toDataURL())}
|
||||
/>
|
||||
<div className="flex gap-2 mt-2">
|
||||
<button type="button" onClick={() => { sigCanvasRef.current?.clear(); setAssinatura(null); }} className="px-3 py-1 text-xs rounded bg-muted-foreground text-white hover:bg-muted">Limpar</button>
|
||||
<button type="button" onClick={() => setAssinatura(sigCanvasRef.current?.toDataURL())} className="px-3 py-1 text-xs rounded bg-primary text-primary-foreground hover:bg-primary/90">Salvar Assinatura</button>
|
||||
<button type="button" onClick={() => { sigCanvasRef.current?.clear(); setAssinatura(null); }} className="px-2 py-1 text-xs rounded-md bg-muted-foreground text-white hover:bg-muted font-semibold shadow">Limpar</button>
|
||||
</div>
|
||||
{assinatura && (
|
||||
<img src={assinatura} alt="Assinatura" className="mt-2 max-h-16 border rounded bg-white" />
|
||||
<img src={assinatura} alt="Assinatura" className="mt-2 max-h-10 border rounded-md bg-white shadow" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2 mt-4">
|
||||
<button type="button" onClick={() => salvarLaudo("Rascunho")} className="w-1/2 bg-muted-foreground text-white py-2 rounded-md hover:bg-muted">Salvar Rascunho</button>
|
||||
<button type="button" onClick={() => salvarLaudo("Entregue")} className="w-1/2 bg-primary text-primary-foreground py-2 rounded-md hover:bg-primary/90">Liberar Laudo</button>
|
||||
<div className="flex flex-col md:flex-row gap-2 mt-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => salvarLaudo("Rascunho")}
|
||||
className="w-full md:w-1/3 flex items-center justify-center gap-1 bg-gray-100 text-gray-700 py-2 rounded-lg font-semibold text-base shadow-sm hover:bg-gray-200 transition-all border border-gray-200"
|
||||
>
|
||||
<svg className="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><path d="M5 13l4 4L19 7" strokeLinecap="round" strokeLinejoin="round"/></svg>
|
||||
Salvar Rascunho
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => salvarLaudo("Entregue")}
|
||||
className="w-full md:w-1/3 flex items-center justify-center gap-1 bg-primary text-white py-2 rounded-lg font-semibold text-base shadow-sm hover:bg-primary/90 transition-all border border-primary/20"
|
||||
>
|
||||
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><path d="M9 12l2 2l4-4" strokeLinecap="round" strokeLinejoin="round"/></svg>
|
||||
Liberar Laudo
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setPreview(!preview)}
|
||||
className="w-full md:w-1/3 flex items-center justify-center gap-1 bg-white text-primary py-2 rounded-lg font-semibold text-base shadow-sm hover:bg-primary/10 transition-all border border-primary/20"
|
||||
>
|
||||
<svg className="w-4 h-4 text-primary" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><path d="M15 10l4.553 2.276A2 2 0 0121 14.09V17a2 2 0 01-2 2H5a2 2 0 01-2-2v-2.91a2 2 0 01.447-1.814L8 10m7-4v4m0 0l-4 4m4-4l4 4" strokeLinecap="round" strokeLinejoin="round"/></svg>
|
||||
Pré-visualizar Laudo
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" onClick={() => setPreview(!preview)} className="mt-2 w-full bg-primary text-primary-foreground py-2 rounded-md hover:bg-primary/90">Pré-visualizar Laudo</button>
|
||||
</form>
|
||||
</div>
|
||||
<div className="bg-white p-6 rounded-lg shadow-md w-full md:w-2/3 flex flex-col gap-4">
|
||||
<h2 className="text-2xl font-bold text-primary text-center mb-4">Editor de Laudo</h2>
|
||||
{!preview ? (
|
||||
<ReactQuill value={conteudo} onChange={setConteudo} modules={{
|
||||
toolbar: [
|
||||
['bold', 'italic', 'underline'],
|
||||
[{'list': 'ordered'}, {'list': 'bullet'}],
|
||||
[{'align': []}],
|
||||
[{'size': ['small', false, 'large', 'huge']}],
|
||||
['clean']
|
||||
]
|
||||
}} className="h-64"/>
|
||||
) : (
|
||||
<div className="border p-4 rounded-md bg-muted overflow-auto">
|
||||
<h3 className="text-lg font-semibold mb-2">Pré-visualização:</h3>
|
||||
<div dangerouslySetInnerHTML={{__html: conteudo}} />
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-6">
|
||||
<h3 className="text-xl font-bold text-primary mb-4 text-center">Histórico de Laudos</h3>
|
||||
|
||||
<div className="bg-white border border-primary/10 shadow-lg rounded-xl p-4 md:p-6">
|
||||
<h3 className="text-xl font-bold text-primary mb-3 text-center">Histórico de Laudos</h3>
|
||||
{laudos.length === 0 ? (
|
||||
<p className="text-muted-foreground text-center">Nenhum laudo registrado.</p>
|
||||
<p className="text-muted-foreground text-center text-sm">Nenhum laudo registrado.</p>
|
||||
) : (
|
||||
laudos.map((laudo: any, idx: number) => (
|
||||
<div key={idx} className="border border-primary/20 rounded-lg p-4 mb-4 bg-muted shadow-sm">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-2">
|
||||
<p className="font-semibold text-primary-foreground">{laudo.paciente}</p>
|
||||
<p className="text-muted-foreground">CPF: {laudo.cpf}</p>
|
||||
<p className="text-muted-foreground">Idade: {laudo.idade}</p>
|
||||
<p className="text-muted-foreground">Sexo: {laudo.sexo}</p>
|
||||
<p className="text-muted-foreground">CID: {laudo.cid}</p>
|
||||
<p className="text-muted-foreground">Status: {laudo.status}</p>
|
||||
<p className="text-muted-foreground">Data: {laudo.data}</p>
|
||||
<div key={idx} className="border border-primary/20 rounded-2xl p-4 mb-6 bg-primary/5 shadow-sm">
|
||||
<div className="flex flex-col md:flex-row md:justify-between md:items-center mb-3 gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg className="w-7 h-7 text-primary" fill="none" viewBox="0 0 24 24"><circle cx="12" cy="8" r="4" stroke="currentColor" strokeWidth="2"/><path d="M4 20c0-2.5 3.5-4.5 8-4.5s8 2 8 4.5" stroke="currentColor" strokeWidth="2"/></svg>
|
||||
<span className="font-bold text-lg md:text-xl text-primary drop-shadow-sm">{laudo.paciente}</span>
|
||||
</div>
|
||||
{laudo.imagem && (
|
||||
<div className="mb-2">
|
||||
<p className="font-semibold text-primary">Imagem:</p>
|
||||
<img src={laudo.imagem} alt="Imagem do laudo" className="rounded-md max-h-32 border mb-2" />
|
||||
<span className="text-base text-gray-500 font-medium">CPF: {laudo.cpf}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-2 mb-2 text-sm text-gray-700">
|
||||
<div>Idade: <span className="font-medium">{laudo.idade}</span></div>
|
||||
<div>Sexo: <span className="font-medium">{laudo.sexo}</span></div>
|
||||
<div>Status: <span className="font-medium">{laudo.status}</span></div>
|
||||
<div>CID: <span className="font-medium">{laudo.cid}</span></div>
|
||||
<div>Data: <span className="font-medium">{laudo.data}</span></div>
|
||||
</div>
|
||||
)}
|
||||
{laudo.assinatura && (
|
||||
<div className="mb-2">
|
||||
<p className="font-semibold text-primary">Assinatura Digital:</p>
|
||||
<img src={laudo.assinatura} alt="Assinatura digital" className="rounded-md max-h-16 border bg-white" />
|
||||
<p className="font-semibold text-primary text-sm mb-1">Assinatura Digital:</p>
|
||||
<img src={laudo.assinatura} alt="Assinatura digital" className="rounded-lg max-h-16 border bg-white shadow" />
|
||||
</div>
|
||||
)}
|
||||
{laudo.imagem && (
|
||||
<div className="mb-2">
|
||||
<p className="font-semibold text-primary">Conteúdo:</p>
|
||||
<div dangerouslySetInnerHTML={{__html: laudo.conteudo}}/>
|
||||
<p className="font-semibold text-primary text-sm mb-1">Imagem:</p>
|
||||
<img src={laudo.imagem} alt="Imagem do laudo" className="rounded-lg max-h-20 border border-primary/20 mb-1" />
|
||||
</div>
|
||||
)}
|
||||
<div className="mb-1">
|
||||
<p className="font-semibold text-primary text-sm mb-1">Conteúdo:</p>
|
||||
<div className="text-sm text-gray-800" dangerouslySetInnerHTML={{ __html: laudo.conteudo }} />
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
@ -1656,7 +1757,6 @@ function LaudoEditor() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -61,7 +61,7 @@ export function Sidebar() {
|
||||
|
||||
{/* este span some no modo ícone */}
|
||||
<span className="text-lg font-semibold text-sidebar-foreground group-data-[collapsible=icon]:hidden">
|
||||
MediConecta
|
||||
MediConnect
|
||||
</span>
|
||||
</Link>
|
||||
</SidebarHeader>
|
||||
|
||||
@ -17,7 +17,6 @@ import {
|
||||
Paciente,
|
||||
PacienteInput,
|
||||
buscarCepAPI,
|
||||
validarCPF,
|
||||
criarPaciente,
|
||||
atualizarPaciente,
|
||||
uploadFotoPaciente,
|
||||
@ -30,6 +29,11 @@ import {
|
||||
import { listarPerfis } from "@/lib/api/perfis";
|
||||
import { criarUsuario } from "@/lib/api/usuarios";
|
||||
|
||||
import { validarCPFLocal } from "@/lib/utils";
|
||||
import { verificarCpfDuplicado } from "@/lib/api";
|
||||
|
||||
|
||||
|
||||
type Mode = "create" | "edit";
|
||||
|
||||
export interface PatientRegistrationFormProps {
|
||||
@ -194,13 +198,13 @@ export function PatientRegistrationForm({
|
||||
telefone: form.telefone || null,
|
||||
email: form.email || null,
|
||||
endereco: {
|
||||
cep: form.cep || null,
|
||||
logradouro: form.logradouro || null,
|
||||
numero: form.numero || null,
|
||||
complemento: form.complemento || null,
|
||||
bairro: form.bairro || null,
|
||||
cidade: form.cidade || null,
|
||||
estado: form.estado || null,
|
||||
cep: form.cep || undefined,
|
||||
logradouro: form.logradouro || undefined,
|
||||
numero: form.numero || undefined,
|
||||
complemento: form.complemento || undefined,
|
||||
bairro: form.bairro || undefined,
|
||||
cidade: form.cidade || undefined,
|
||||
estado: form.estado || undefined,
|
||||
},
|
||||
observacoes: form.observacoes || null,
|
||||
};
|
||||
@ -212,18 +216,24 @@ export function PatientRegistrationForm({
|
||||
|
||||
|
||||
try {
|
||||
const { valido, existe } = await validarCPF(form.cpf);
|
||||
if (!valido) {
|
||||
setErrors((e) => ({ ...e, cpf: "CPF inválido (validação externa)" }));
|
||||
// 1) validação local
|
||||
if (!validarCPFLocal(form.cpf)) {
|
||||
setErrors((e) => ({ ...e, cpf: "CPF inválido" }));
|
||||
return;
|
||||
}
|
||||
if (existe && mode === "create") {
|
||||
|
||||
// 2) checar duplicidade no banco (apenas se criando novo paciente)
|
||||
if (mode === "create") {
|
||||
const existe = await verificarCpfDuplicado(form.cpf);
|
||||
if (existe) {
|
||||
setErrors((e) => ({ ...e, cpf: "CPF já cadastrado no sistema" }));
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Erro ao validar CPF", err);
|
||||
}
|
||||
|
||||
|
||||
setSubmitting(true);
|
||||
try {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
|
||||
// lib/api.ts
|
||||
|
||||
export type ApiOk<T = any> = {
|
||||
success: boolean;
|
||||
success?: boolean;
|
||||
data: T;
|
||||
message?: string;
|
||||
pagination?: {
|
||||
@ -12,6 +12,7 @@ export type ApiOk<T = any> = {
|
||||
};
|
||||
};
|
||||
|
||||
// ===== TIPOS COMUNS =====
|
||||
export type Endereco = {
|
||||
cep?: string;
|
||||
logradouro?: string;
|
||||
@ -22,6 +23,7 @@ export type Endereco = {
|
||||
estado?: string;
|
||||
};
|
||||
|
||||
// ===== PACIENTES =====
|
||||
export type Paciente = {
|
||||
id: string;
|
||||
nome?: string;
|
||||
@ -46,241 +48,11 @@ export type PacienteInput = {
|
||||
data_nascimento?: string | null;
|
||||
telefone?: string | null;
|
||||
email?: string | null;
|
||||
endereco?: {
|
||||
cep?: string | null;
|
||||
logradouro?: string | null;
|
||||
numero?: string | null;
|
||||
complemento?: string | null;
|
||||
bairro?: string | null;
|
||||
cidade?: string | null;
|
||||
estado?: string | null;
|
||||
};
|
||||
endereco?: Endereco;
|
||||
observacoes?: string | null;
|
||||
};
|
||||
|
||||
|
||||
|
||||
export const API_BASE = process.env.NEXT_PUBLIC_API_BASE ?? "https://mock.apidog.com/m1/1053378-0-default";
|
||||
const MEDICOS_BASE = process.env.NEXT_PUBLIC_MEDICOS_BASE_PATH ?? "/medicos";
|
||||
|
||||
export const PATHS = {
|
||||
// Pacientes (já existia)
|
||||
pacientes: "/pacientes",
|
||||
pacienteId: (id: string | number) => `/pacientes/${id}`,
|
||||
foto: (id: string | number) => `/pacientes/${id}/foto`,
|
||||
anexos: (id: string | number) => `/pacientes/${id}/anexos`,
|
||||
anexoId: (id: string | number, anexoId: string | number) => `/pacientes/${id}/anexos/${anexoId}`,
|
||||
validarCPF: "/pacientes/validar-cpf",
|
||||
cep: (cep: string) => `/utils/cep/${cep}`,
|
||||
|
||||
// Médicos (APONTANDO PARA PACIENTES por enquanto)
|
||||
medicos: MEDICOS_BASE,
|
||||
medicoId: (id: string | number) => `${MEDICOS_BASE}/${id}`,
|
||||
medicoFoto: (id: string | number) => `${MEDICOS_BASE}/${id}/foto`,
|
||||
medicoAnexos: (id: string | number) => `${MEDICOS_BASE}/${id}/anexos`,
|
||||
medicoAnexoId: (id: string | number, anexoId: string | number) => `${MEDICOS_BASE}/${id}/anexos/${anexoId}`,
|
||||
} as const;
|
||||
|
||||
|
||||
// Função para obter o token JWT do localStorage
|
||||
function getAuthToken(): string | null {
|
||||
if (typeof window === 'undefined') return null;
|
||||
return localStorage.getItem('auth_token');
|
||||
}
|
||||
|
||||
export function headers(kind: "json" | "form" = "json"): Record<string, string> {
|
||||
const h: Record<string, string> = {};
|
||||
|
||||
// API Key da Supabase sempre necessária
|
||||
h.apikey = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
|
||||
|
||||
// Bearer Token quando usuário está logado
|
||||
const jwtToken = getAuthToken();
|
||||
if (jwtToken) {
|
||||
h.Authorization = `Bearer ${jwtToken}`;
|
||||
}
|
||||
|
||||
if (kind === "json") h["Content-Type"] = "application/json";
|
||||
return h;
|
||||
}
|
||||
|
||||
export function logAPI(title: string, info: { url?: string; payload?: any; result?: any } = {}) {
|
||||
try {
|
||||
console.group(`[API] ${title}`);
|
||||
if (info.url) console.log("url:", info.url);
|
||||
if (info.payload !== undefined) console.log("payload:", info.payload);
|
||||
if (info.result !== undefined) console.log("API result:", info.result);
|
||||
console.groupEnd();
|
||||
} catch {}
|
||||
}
|
||||
|
||||
export async function parse<T>(res: Response): Promise<T> {
|
||||
let json: any = null;
|
||||
try {
|
||||
json = await res.json();
|
||||
} catch {
|
||||
// ignora erro de parse vazio
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
// 🔴 ADICIONE ESSA LINHA AQUI:
|
||||
console.error("[API ERROR]", res.url, res.status, json);
|
||||
|
||||
const code = json?.apidogError?.code ?? res.status;
|
||||
const msg = json?.apidogError?.message ?? res.statusText;
|
||||
throw new Error(`${code}: ${msg}`);
|
||||
}
|
||||
|
||||
return (json?.data ?? json) as T;
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Pacientes (CRUD)
|
||||
//
|
||||
export async function listarPacientes(params?: { page?: number; limit?: number; q?: string }): Promise<Paciente[]> {
|
||||
const query = new URLSearchParams();
|
||||
if (params?.page) query.set("page", String(params.page));
|
||||
if (params?.limit) query.set("limit", String(params.limit));
|
||||
if (params?.q) query.set("q", params.q);
|
||||
const url = `${API_BASE}${PATHS.pacientes}${query.toString() ? `?${query.toString()}` : ""}`;
|
||||
|
||||
const res = await fetch(url, { method: "GET", headers: headers("json") });
|
||||
const data = await parse<ApiOk<Paciente[]>>(res);
|
||||
logAPI("listarPacientes", { url, result: data });
|
||||
return data?.data ?? (data as any);
|
||||
}
|
||||
|
||||
export async function buscarPacientePorId(id: string | number): Promise<Paciente> {
|
||||
const url = `${API_BASE}${PATHS.pacienteId(id)}`;
|
||||
const res = await fetch(url, { method: "GET", headers: headers("json") });
|
||||
const data = await parse<ApiOk<Paciente>>(res);
|
||||
logAPI("buscarPacientePorId", { url, result: data });
|
||||
return data?.data ?? (data as any);
|
||||
}
|
||||
|
||||
export async function criarPaciente(input: PacienteInput): Promise<Paciente> {
|
||||
const url = `${API_BASE}${PATHS.pacientes}`;
|
||||
const res = await fetch(url, { method: "POST", headers: headers("json"), body: JSON.stringify(input) });
|
||||
const data = await parse<ApiOk<Paciente>>(res);
|
||||
logAPI("criarPaciente", { url, payload: input, result: data });
|
||||
return data?.data ?? (data as any);
|
||||
}
|
||||
|
||||
export async function atualizarPaciente(id: string | number, input: PacienteInput): Promise<Paciente> {
|
||||
const url = `${API_BASE}${PATHS.pacienteId(id)}`;
|
||||
const res = await fetch(url, { method: "PUT", headers: headers("json"), body: JSON.stringify(input) });
|
||||
const data = await parse<ApiOk<Paciente>>(res);
|
||||
logAPI("atualizarPaciente", { url, payload: input, result: data });
|
||||
return data?.data ?? (data as any);
|
||||
}
|
||||
|
||||
export async function excluirPaciente(id: string | number): Promise<void> {
|
||||
const url = `${API_BASE}${PATHS.pacienteId(id)}`;
|
||||
const res = await fetch(url, { method: "DELETE", headers: headers("json") });
|
||||
await parse<any>(res);
|
||||
logAPI("excluirPaciente", { url, result: { ok: true } });
|
||||
}
|
||||
|
||||
//
|
||||
// Foto
|
||||
//
|
||||
|
||||
export async function uploadFotoPaciente(id: string | number, file: File): Promise<{ foto_url?: string; thumbnail_url?: string }> {
|
||||
const url = `${API_BASE}${PATHS.foto(id)}`;
|
||||
const fd = new FormData();
|
||||
// nome de campo mais comum no mock
|
||||
fd.append("foto", file);
|
||||
const res = await fetch(url, { method: "POST", headers: headers("form"), body: fd });
|
||||
const data = await parse<ApiOk<{ foto_url?: string; thumbnail_url?: string }>>(res);
|
||||
logAPI("uploadFotoPaciente", { url, payload: { file: file.name }, result: data });
|
||||
return data?.data ?? (data as any);
|
||||
}
|
||||
|
||||
export async function removerFotoPaciente(id: string | number): Promise<void> {
|
||||
const url = `${API_BASE}${PATHS.foto(id)}`;
|
||||
const res = await fetch(url, { method: "DELETE", headers: headers("json") });
|
||||
await parse<any>(res);
|
||||
logAPI("removerFotoPaciente", { url, result: { ok: true } });
|
||||
}
|
||||
|
||||
//
|
||||
// Anexos
|
||||
//
|
||||
|
||||
export async function listarAnexos(id: string | number): Promise<any[]> {
|
||||
const url = `${API_BASE}${PATHS.anexos(id)}`;
|
||||
const res = await fetch(url, { method: "GET", headers: headers("json") });
|
||||
const data = await parse<ApiOk<any[]>>(res);
|
||||
logAPI("listarAnexos", { url, result: data });
|
||||
return data?.data ?? (data as any);
|
||||
}
|
||||
|
||||
export async function adicionarAnexo(id: string | number, file: File): Promise<any> {
|
||||
const url = `${API_BASE}${PATHS.anexos(id)}`;
|
||||
const fd = new FormData();
|
||||
|
||||
fd.append("arquivo", file);
|
||||
const res = await fetch(url, { method: "POST", body: fd, headers: headers("form") });
|
||||
const data = await parse<ApiOk<any>>(res);
|
||||
logAPI("adicionarAnexo", { url, payload: { file: file.name }, result: data });
|
||||
return data?.data ?? (data as any);
|
||||
}
|
||||
|
||||
export async function removerAnexo(id: string | number, anexoId: string | number): Promise<void> {
|
||||
const url = `${API_BASE}${PATHS.anexoId(id, anexoId)}`;
|
||||
const res = await fetch(url, { method: "DELETE", headers: headers("json") });
|
||||
await parse<any>(res);
|
||||
logAPI("removerAnexo", { url, result: { ok: true } });
|
||||
}
|
||||
|
||||
//
|
||||
// Validações
|
||||
//
|
||||
|
||||
export async function validarCPF(cpf: string): Promise<{ valido: boolean; existe: boolean; paciente_id: string | null }> {
|
||||
const url = `${API_BASE}${PATHS.validarCPF}`;
|
||||
const payload = { cpf };
|
||||
const res = await fetch(url, { method: "POST", headers: headers("json"), body: JSON.stringify(payload) });
|
||||
const data = await parse<ApiOk<{ valido: boolean; existe: boolean; paciente_id: string | null }>>(res);
|
||||
logAPI("validarCPF", { url, payload, result: data });
|
||||
return data?.data ?? (data as any);
|
||||
}
|
||||
|
||||
export async function buscarCepAPI(cep: string): Promise<{ logradouro?: string; bairro?: string; localidade?: string; uf?: string; erro?: boolean }> {
|
||||
const clean = (cep || "").replace(/\D/g, "");
|
||||
const urlMock = `${API_BASE}${PATHS.cep(clean)}`;
|
||||
|
||||
try {
|
||||
const res = await fetch(urlMock, { method: "GET", headers: headers("json") });
|
||||
const data = await parse<any>(res); // pode vir direto ou dentro de {data}
|
||||
logAPI("buscarCEP (mock)", { url: urlMock, payload: { cep: clean }, result: data });
|
||||
const d = data?.data ?? data ?? {};
|
||||
return {
|
||||
logradouro: d.logradouro ?? d.street ?? "",
|
||||
bairro: d.bairro ?? d.neighborhood ?? "",
|
||||
localidade: d.localidade ?? d.city ?? "",
|
||||
uf: d.uf ?? d.state ?? "",
|
||||
erro: false,
|
||||
};
|
||||
} catch {
|
||||
// fallback ViaCEP
|
||||
const urlVia = `https://viacep.com.br/ws/${clean}/json/`;
|
||||
const resV = await fetch(urlVia);
|
||||
const jsonV = await resV.json().catch(() => ({}));
|
||||
logAPI("buscarCEP (ViaCEP/fallback)", { url: urlVia, payload: { cep: clean }, result: jsonV });
|
||||
if (jsonV?.erro) return { erro: true };
|
||||
return {
|
||||
logradouro: jsonV.logradouro ?? "",
|
||||
bairro: jsonV.bairro ?? "",
|
||||
localidade: jsonV.localidade ?? "",
|
||||
uf: jsonV.uf ?? "",
|
||||
erro: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// >>> ADICIONE (ou mova) ESTES TIPOS <<<
|
||||
// ===== MÉDICOS =====
|
||||
export type FormacaoAcademica = {
|
||||
instituicao: string;
|
||||
curso: string;
|
||||
@ -344,86 +116,221 @@ export type MedicoInput = {
|
||||
valor_consulta?: number | string | null;
|
||||
};
|
||||
|
||||
// ===== CONFIG =====
|
||||
const API_BASE =
|
||||
process.env.NEXT_PUBLIC_API_BASE ?? "https://yuanqfswhberkoevtmfr.supabase.co";
|
||||
const REST = `${API_BASE}/rest/v1`;
|
||||
|
||||
//
|
||||
// MÉDICOS (CRUD)
|
||||
//
|
||||
// ======= MÉDICOS (forçando usar rotas de PACIENTES no mock) ========
|
||||
// Token salvo no browser (aceita auth_token ou token)
|
||||
function getAuthToken(): string | null {
|
||||
if (typeof window === "undefined") return null;
|
||||
return (
|
||||
localStorage.getItem("auth_token") ||
|
||||
localStorage.getItem("token") ||
|
||||
sessionStorage.getItem("auth_token") ||
|
||||
sessionStorage.getItem("token")
|
||||
);
|
||||
}
|
||||
|
||||
export async function listarMedicos(params?: { page?: number; limit?: number; q?: string }): Promise<Medico[]> {
|
||||
const query = new URLSearchParams();
|
||||
if (params?.page) query.set("page", String(params.page));
|
||||
if (params?.limit) query.set("limit", String(params.limit));
|
||||
if (params?.q) query.set("q", params.q);
|
||||
// Cabeçalhos base
|
||||
function baseHeaders(): Record<string, string> {
|
||||
const h: Record<string, string> = {
|
||||
apikey:
|
||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ",
|
||||
Accept: "application/json",
|
||||
};
|
||||
const jwt = getAuthToken();
|
||||
if (jwt) h.Authorization = `Bearer ${jwt}`;
|
||||
return h;
|
||||
}
|
||||
|
||||
// FORÇA /pacientes
|
||||
const url = `${API_BASE}/pacientes${query.toString() ? `?${query.toString()}` : ""}`;
|
||||
const res = await fetch(url, { method: "GET", headers: headers("json") });
|
||||
const data = await parse<ApiOk<Medico[]>>(res);
|
||||
return (data as any)?.data ?? (data as any);
|
||||
// Para POST/PATCH/DELETE e para GET com count
|
||||
function withPrefer(h: Record<string, string>, prefer: string) {
|
||||
return { ...h, Prefer: prefer };
|
||||
}
|
||||
|
||||
// Parse genérico
|
||||
async function parse<T>(res: Response): Promise<T> {
|
||||
let json: any = null;
|
||||
try {
|
||||
json = await res.json();
|
||||
} catch {}
|
||||
if (!res.ok) {
|
||||
console.error("[API ERROR]", res.url, res.status, json);
|
||||
const code = (json && (json.error?.code || json.code)) ?? res.status;
|
||||
const msg = (json && (json.error?.message || json.message)) ?? res.statusText;
|
||||
throw new Error(`${code}: ${msg}`);
|
||||
}
|
||||
return (json?.data ?? json) as T;
|
||||
}
|
||||
|
||||
// Helper de paginação (Range/Range-Unit)
|
||||
function rangeHeaders(page?: number, limit?: number): Record<string, string> {
|
||||
if (!page || !limit) return {};
|
||||
const start = (page - 1) * limit;
|
||||
const end = start + limit - 1;
|
||||
return { Range: `${start}-${end}`, "Range-Unit": "items" };
|
||||
}
|
||||
|
||||
// ===== PACIENTES (CRUD) =====
|
||||
export async function listarPacientes(params?: {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
q?: string;
|
||||
}): Promise<Paciente[]> {
|
||||
const qs = new URLSearchParams();
|
||||
if (params?.q) qs.set("q", params.q);
|
||||
|
||||
const url = `${REST}/patients${qs.toString() ? `?${qs.toString()}` : ""}`;
|
||||
const res = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
...baseHeaders(),
|
||||
...rangeHeaders(params?.page, params?.limit),
|
||||
},
|
||||
});
|
||||
return await parse<Paciente[]>(res);
|
||||
}
|
||||
|
||||
export async function buscarPacientePorId(id: string | number): Promise<Paciente> {
|
||||
const url = `${REST}/patients?id=eq.${id}`;
|
||||
const res = await fetch(url, { method: "GET", headers: baseHeaders() });
|
||||
const arr = await parse<Paciente[]>(res);
|
||||
if (!arr?.length) throw new Error("404: Paciente não encontrado");
|
||||
return arr[0];
|
||||
}
|
||||
|
||||
export async function criarPaciente(input: PacienteInput): Promise<Paciente> {
|
||||
const url = `${REST}/patients`;
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: withPrefer({ ...baseHeaders(), "Content-Type": "application/json" }, "return=representation"),
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
const arr = await parse<Paciente[] | Paciente>(res);
|
||||
return Array.isArray(arr) ? arr[0] : (arr as Paciente);
|
||||
}
|
||||
|
||||
export async function atualizarPaciente(id: string | number, input: PacienteInput): Promise<Paciente> {
|
||||
const url = `${REST}/patients?id=eq.${id}`;
|
||||
const res = await fetch(url, {
|
||||
method: "PATCH",
|
||||
headers: withPrefer({ ...baseHeaders(), "Content-Type": "application/json" }, "return=representation"),
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
const arr = await parse<Paciente[] | Paciente>(res);
|
||||
return Array.isArray(arr) ? arr[0] : (arr as Paciente);
|
||||
}
|
||||
|
||||
export async function excluirPaciente(id: string | number): Promise<void> {
|
||||
const url = `${REST}/patients?id=eq.${id}`;
|
||||
const res = await fetch(url, { method: "DELETE", headers: baseHeaders() });
|
||||
await parse<any>(res);
|
||||
}
|
||||
// ===== PACIENTES (Extra: verificação de CPF duplicado) =====
|
||||
export async function verificarCpfDuplicado(cpf: string): Promise<boolean> {
|
||||
const clean = (cpf || "").replace(/\D/g, "");
|
||||
const url = `${API_BASE}/rest/v1/patients?cpf=eq.${clean}&select=id`;
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: baseHeaders(),
|
||||
});
|
||||
|
||||
const data = await res.json().catch(() => []);
|
||||
return Array.isArray(data) && data.length > 0;
|
||||
}
|
||||
|
||||
|
||||
// ===== MÉDICOS (CRUD) =====
|
||||
export async function listarMedicos(params?: {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
q?: string;
|
||||
}): Promise<Medico[]> {
|
||||
const qs = new URLSearchParams();
|
||||
if (params?.q) qs.set("q", params.q);
|
||||
|
||||
const url = `${REST}/doctors${qs.toString() ? `?${qs.toString()}` : ""}`;
|
||||
const res = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
...baseHeaders(),
|
||||
...rangeHeaders(params?.page, params?.limit),
|
||||
},
|
||||
});
|
||||
return await parse<Medico[]>(res);
|
||||
}
|
||||
|
||||
export async function buscarMedicoPorId(id: string | number): Promise<Medico> {
|
||||
const url = `${API_BASE}/pacientes/${id}`; // FORÇA /pacientes
|
||||
const res = await fetch(url, { method: "GET", headers: headers("json") });
|
||||
const data = await parse<ApiOk<Medico>>(res);
|
||||
return (data as any)?.data ?? (data as any);
|
||||
const url = `${REST}/doctors?id=eq.${id}`;
|
||||
const res = await fetch(url, { method: "GET", headers: baseHeaders() });
|
||||
const arr = await parse<Medico[]>(res);
|
||||
if (!arr?.length) throw new Error("404: Médico não encontrado");
|
||||
return arr[0];
|
||||
}
|
||||
|
||||
export async function criarMedico(input: MedicoInput): Promise<Medico> {
|
||||
const url = `${API_BASE}/pacientes`; // FORÇA /pacientes
|
||||
const res = await fetch(url, { method: "POST", headers: headers("json"), body: JSON.stringify(input) });
|
||||
const data = await parse<ApiOk<Medico>>(res);
|
||||
return (data as any)?.data ?? (data as any);
|
||||
const url = `${REST}/doctors`;
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: withPrefer({ ...baseHeaders(), "Content-Type": "application/json" }, "return=representation"),
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
const arr = await parse<Medico[] | Medico>(res);
|
||||
return Array.isArray(arr) ? arr[0] : (arr as Medico);
|
||||
}
|
||||
|
||||
export async function atualizarMedico(id: string | number, input: MedicoInput): Promise<Medico> {
|
||||
const url = `${API_BASE}/pacientes/${id}`; // FORÇA /pacientes
|
||||
const res = await fetch(url, { method: "PUT", headers: headers("json"), body: JSON.stringify(input) });
|
||||
const data = await parse<ApiOk<Medico>>(res);
|
||||
return (data as any)?.data ?? (data as any);
|
||||
const url = `${REST}/doctors?id=eq.${id}`;
|
||||
const res = await fetch(url, {
|
||||
method: "PATCH",
|
||||
headers: withPrefer({ ...baseHeaders(), "Content-Type": "application/json" }, "return=representation"),
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
const arr = await parse<Medico[] | Medico>(res);
|
||||
return Array.isArray(arr) ? arr[0] : (arr as Medico);
|
||||
}
|
||||
|
||||
export async function excluirMedico(id: string | number): Promise<void> {
|
||||
const url = `${API_BASE}/pacientes/${id}`; // FORÇA /pacientes
|
||||
const res = await fetch(url, { method: "DELETE", headers: headers("json") });
|
||||
const url = `${REST}/doctors?id=eq.${id}`;
|
||||
const res = await fetch(url, { method: "DELETE", headers: baseHeaders() });
|
||||
await parse<any>(res);
|
||||
}
|
||||
|
||||
export async function uploadFotoMedico(id: string | number, file: File): Promise<{ foto_url?: string; thumbnail_url?: string }> {
|
||||
const url = `${API_BASE}/pacientes/${id}/foto`; // FORÇA /pacientes
|
||||
const fd = new FormData();
|
||||
fd.append("foto", file);
|
||||
const res = await fetch(url, { method: "POST", headers: headers("form"), body: fd });
|
||||
const data = await parse<ApiOk<{ foto_url?: string; thumbnail_url?: string }>>(res);
|
||||
return (data as any)?.data ?? (data as any);
|
||||
// ===== CEP (usado nos formulários) =====
|
||||
export async function buscarCepAPI(cep: string): Promise<{
|
||||
logradouro?: string;
|
||||
bairro?: string;
|
||||
localidade?: string;
|
||||
uf?: string;
|
||||
erro?: boolean;
|
||||
}> {
|
||||
const clean = (cep || "").replace(/\D/g, "");
|
||||
try {
|
||||
const res = await fetch(`https://viacep.com.br/ws/${clean}/json/`);
|
||||
const json = await res.json();
|
||||
if (json?.erro) return { erro: true };
|
||||
return {
|
||||
logradouro: json.logradouro ?? "",
|
||||
bairro: json.bairro ?? "",
|
||||
localidade: json.localidade ?? "",
|
||||
uf: json.uf ?? "",
|
||||
erro: false,
|
||||
};
|
||||
} catch {
|
||||
return { erro: true };
|
||||
}
|
||||
}
|
||||
|
||||
export async function removerFotoMedico(id: string | number): Promise<void> {
|
||||
const url = `${API_BASE}/pacientes/${id}/foto`; // FORÇA /pacientes
|
||||
const res = await fetch(url, { method: "DELETE", headers: headers("json") });
|
||||
await parse<any>(res);
|
||||
}
|
||||
|
||||
export async function listarAnexosMedico(id: string | number): Promise<any[]> {
|
||||
const url = `${API_BASE}/pacientes/${id}/anexos`; // FORÇA /pacientes
|
||||
const res = await fetch(url, { method: "GET", headers: headers("json") });
|
||||
const data = await parse<ApiOk<any[]>>(res);
|
||||
return (data as any)?.data ?? (data as any);
|
||||
}
|
||||
|
||||
export async function adicionarAnexoMedico(id: string | number, file: File): Promise<any> {
|
||||
const url = `${API_BASE}/pacientes/${id}/anexos`; // FORÇA /pacientes
|
||||
const fd = new FormData();
|
||||
fd.append("arquivo", file);
|
||||
const res = await fetch(url, { method: "POST", headers: headers("form"), body: fd });
|
||||
const data = await parse<ApiOk<any>>(res);
|
||||
return (data as any)?.data ?? (data as any);
|
||||
}
|
||||
|
||||
export async function removerAnexoMedico(id: string | number, anexoId: string | number): Promise<void> {
|
||||
const url = `${API_BASE}/pacientes/${id}/anexos/${anexoId}`; // FORÇA /pacientes
|
||||
const res = await fetch(url, { method: "DELETE", headers: headers("json") });
|
||||
await parse<any>(res);
|
||||
}
|
||||
// ======= FIM: médicos usando rotas de pacientes =======
|
||||
// ===== Stubs pra não quebrar imports dos forms (sem rotas de storage na doc) =====
|
||||
export async function listarAnexos(_id: string | number): Promise<any[]> { return []; }
|
||||
export async function adicionarAnexo(_id: string | number, _file: File): Promise<any> { return {}; }
|
||||
export async function removerAnexo(_id: string | number, _anexoId: string | number): Promise<void> {}
|
||||
export async function uploadFotoPaciente(_id: string | number, _file: File): Promise<{ foto_url?: string; thumbnail_url?: string }> { return {}; }
|
||||
export async function removerFotoPaciente(_id: string | number): Promise<void> {}
|
||||
export async function listarAnexosMedico(_id: string | number): Promise<any[]> { return []; }
|
||||
export async function adicionarAnexoMedico(_id: string | number, _file: File): Promise<any> { return {}; }
|
||||
export async function removerAnexoMedico(_id: string | number, _anexoId: string | number): Promise<void> {}
|
||||
export async function uploadFotoMedico(_id: string | number, _file: File): Promise<{ foto_url?: string; thumbnail_url?: string }> { return {}; }
|
||||
export async function removerFotoMedico(_id: string | number): Promise<void> {}
|
||||
|
||||
@ -4,3 +4,22 @@ import { twMerge } from "tailwind-merge"
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
||||
export function validarCPFLocal(cpf: string): boolean {
|
||||
if (!cpf) return false;
|
||||
cpf = cpf.replace(/[^\d]+/g, "");
|
||||
if (cpf.length !== 11) return false;
|
||||
if (/^(\d)\1{10}$/.test(cpf)) return false;
|
||||
|
||||
let soma = 0, resto = 0;
|
||||
for (let i = 1; i <= 9; i++) soma += parseInt(cpf.substring(i - 1, i)) * (11 - i);
|
||||
resto = (soma * 10) % 11; if (resto === 10 || resto === 11) resto = 0;
|
||||
if (resto !== parseInt(cpf.substring(9, 10))) return false;
|
||||
|
||||
soma = 0;
|
||||
for (let i = 1; i <= 10; i++) soma += parseInt(cpf.substring(i - 1, i)) * (12 - i);
|
||||
resto = (soma * 10) % 11; if (resto === 10 || resto === 11) resto = 0;
|
||||
if (resto !== parseInt(cpf.substring(10, 11))) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user