Merge branch 'feature/user-profile-api' of https://git.popcode.com.br/RiseUP/riseup-squad20 into feature/user-profile-api
This commit is contained in:
commit
2d30fab9b5
@ -10,9 +10,9 @@ import { Label } from "@/components/ui/label";
|
|||||||
import { MoreHorizontal, Plus, Search, Edit, Trash2, ArrowLeft, Eye } from "lucide-react";
|
import { MoreHorizontal, Plus, Search, Edit, Trash2, ArrowLeft, Eye } from "lucide-react";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { DoctorRegistrationForm } from "@/components/forms/doctor-registration-form";
|
import { DoctorRegistrationForm } from "@/components/forms/doctor-registration-form";
|
||||||
|
import ProtectedRoute from "@/components/ProtectedRoute"; // <-- IMPORTADO
|
||||||
|
|
||||||
|
import { listarMedicos, excluirMedico, Medico } from "@/lib/api";
|
||||||
import { listarMedicos, excluirMedico, buscarMedicos, buscarMedicoPorId, Medico } from "@/lib/api";
|
|
||||||
|
|
||||||
function normalizeMedico(m: any): Medico {
|
function normalizeMedico(m: any): Medico {
|
||||||
return {
|
return {
|
||||||
@ -296,176 +296,153 @@ export default function DoutoresPage() {
|
|||||||
<h1 className="text-2xl font-bold">{editingId ? "Editar Médico" : "Novo Médico"}</h1>
|
<h1 className="text-2xl font-bold">{editingId ? "Editar Médico" : "Novo Médico"}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DoctorRegistrationForm
|
<DoctorRegistrationForm
|
||||||
inline
|
inline
|
||||||
mode={editingId ? "edit" : "create"}
|
mode={editingId ? "edit" : "create"}
|
||||||
doctorId={editingId}
|
doctorId={editingId ? Number(editingId) : null}
|
||||||
onSaved={handleSaved}
|
onSaved={handleSaved}
|
||||||
onClose={() => setShowForm(false)}
|
onClose={() => setShowForm(false)}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-6 p-6">
|
|
||||||
<div className="flex items-center justify-between gap-4 flex-wrap">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-2xl font-bold">Médicos</h1>
|
|
||||||
<p className="text-muted-foreground">Gerencie os médicos da sua clínica</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
<div className="flex items-center gap-2">
|
<div className="space-y-6 p-6">
|
||||||
<div className="flex gap-2">
|
<div className="flex items-center justify-between gap-4 flex-wrap">
|
||||||
<div className="relative">
|
<div>
|
||||||
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
<h1 className="text-2xl font-bold">Médicos</h1>
|
||||||
<Input
|
<p className="text-muted-foreground">Gerencie os médicos da sua clínica</p>
|
||||||
className="pl-8 w-80"
|
|
||||||
placeholder="Digite para buscar por ID, nome, CRM ou especialidade…"
|
|
||||||
value={search}
|
|
||||||
onChange={handleSearchChange}
|
|
||||||
onKeyDown={handleSearchKeyDown}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<Button
|
|
||||||
variant="outline"
|
<div className="flex items-center gap-2">
|
||||||
onClick={handleClickBuscar}
|
<div className="relative">
|
||||||
disabled={loading || !search.trim()}
|
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||||
>
|
<Input
|
||||||
Buscar
|
className="pl-8 w-80"
|
||||||
</Button>
|
placeholder="Buscar por nome, CRM ou especialidade…"
|
||||||
{searchMode && (
|
value={search}
|
||||||
<Button
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
variant="ghost"
|
/>
|
||||||
onClick={() => {
|
</div>
|
||||||
setSearch("");
|
<Button onClick={handleAdd} disabled={loading}>
|
||||||
setSearchMode(false);
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
setSearchResults([]);
|
Novo Médico
|
||||||
}}
|
|
||||||
>
|
|
||||||
Limpar
|
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<Button onClick={handleAdd} disabled={loading}>
|
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
|
||||||
Novo Médico
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="border rounded-lg overflow-hidden">
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>Nome</TableHead>
|
|
||||||
<TableHead>Especialidade</TableHead>
|
|
||||||
<TableHead>CRM</TableHead>
|
|
||||||
<TableHead>Contato</TableHead>
|
|
||||||
<TableHead className="w-[100px]">Ações</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{loading ? (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell colSpan={5} className="text-center text-muted-foreground">
|
|
||||||
Carregando…
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
) : displayedDoctors.length > 0 ? (
|
|
||||||
displayedDoctors.map((doctor) => (
|
|
||||||
<TableRow key={doctor.id}>
|
|
||||||
<TableCell className="font-medium">{doctor.full_name}</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Badge variant="outline">{doctor.especialidade}</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>{doctor.crm}</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span>{doctor.email}</span>
|
|
||||||
<span className="text-sm text-muted-foreground">{doctor.telefone}</span>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<button className="h-8 w-8 p-0 flex items-center justify-center rounded-md hover:bg-accent">
|
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
|
||||||
<span className="sr-only">Abrir menu</span>
|
|
||||||
</button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end">
|
|
||||||
<DropdownMenuItem onClick={() => handleView(doctor)}>
|
|
||||||
<Eye className="mr-2 h-4 w-4" />
|
|
||||||
Ver
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={() => handleEdit(String(doctor.id))}>
|
|
||||||
<Edit className="mr-2 h-4 w-4" />
|
|
||||||
Editar
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={() => handleDelete(String(doctor.id))} className="text-destructive">
|
|
||||||
<Trash2 className="mr-2 h-4 w-4" />
|
|
||||||
Excluir
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell colSpan={5} className="text-center text-muted-foreground">
|
|
||||||
Nenhum médico encontrado
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
)}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{viewingDoctor && (
|
|
||||||
<Dialog open={!!viewingDoctor} onOpenChange={() => setViewingDoctor(null)}>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Detalhes do Médico</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Informações detalhadas de {viewingDoctor?.full_name}.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="grid gap-4 py-4">
|
|
||||||
<div className="grid grid-cols-4 items-center gap-4">
|
|
||||||
<Label className="text-right">Nome</Label>
|
|
||||||
<span className="col-span-3 font-medium">{viewingDoctor?.full_name}</span>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-4 items-center gap-4">
|
|
||||||
<Label className="text-right">Especialidade</Label>
|
|
||||||
<span className="col-span-3">
|
|
||||||
<Badge variant="outline">{viewingDoctor?.especialidade}</Badge>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-4 items-center gap-4">
|
|
||||||
<Label className="text-right">CRM</Label>
|
|
||||||
<span className="col-span-3">{viewingDoctor?.crm}</span>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-4 items-center gap-4">
|
|
||||||
<Label className="text-right">Email</Label>
|
|
||||||
<span className="col-span-3">{viewingDoctor?.email}</span>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-4 items-center gap-4">
|
|
||||||
<Label className="text-right">Telefone</Label>
|
|
||||||
<span className="col-span-3">{viewingDoctor?.telefone}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
</div>
|
||||||
<Button onClick={() => setViewingDoctor(null)}>Fechar</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="border rounded-lg overflow-hidden">
|
||||||
Mostrando {displayedDoctors.length} {searchMode ? 'resultado(s) da busca' : `de ${doctors.length}`}
|
<Table>
|
||||||
</div>
|
<TableHeader>
|
||||||
</div>
|
<TableRow>
|
||||||
|
<TableHead>Nome</TableHead>
|
||||||
|
<TableHead>Especialidade</TableHead>
|
||||||
|
<TableHead>CRM</TableHead>
|
||||||
|
<TableHead>Contato</TableHead>
|
||||||
|
<TableHead className="w-[100px]">Ações</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{loading ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={5} className="text-center text-muted-foreground">
|
||||||
|
Carregando…
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : filtered.length > 0 ? (
|
||||||
|
filtered.map((doctor) => (
|
||||||
|
<TableRow key={doctor.id}>
|
||||||
|
<TableCell className="font-medium">{doctor.nome}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge variant="outline">{doctor.especialidade}</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{doctor.crm}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span>{doctor.email}</span>
|
||||||
|
<span className="text-sm text-muted-foreground">{doctor.telefone}</span>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<button className="h-8 w-8 p-0 flex items-center justify-center rounded-md hover:bg-accent">
|
||||||
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
|
<span className="sr-only">Abrir menu</span>
|
||||||
|
</button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem onClick={() => handleView(doctor)}>
|
||||||
|
<Eye className="mr-2 h-4 w-4" />
|
||||||
|
Ver
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => handleEdit(String(doctor.id))}>
|
||||||
|
<Edit className="mr-2 h-4 w-4" />
|
||||||
|
Editar
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => handleDelete(String(doctor.id))} className="text-destructive">
|
||||||
|
<Trash2 className="mr-2 h-4 w-4" />
|
||||||
|
Excluir
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={5} className="text-center text-muted-foreground">
|
||||||
|
Nenhum médico encontrado
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{viewingDoctor && (
|
||||||
|
<Dialog open={!!viewingDoctor} onOpenChange={() => setViewingDoctor(null)}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Detalhes do Médico</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Informações detalhadas de {viewingDoctor?.nome}.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="grid gap-4 py-4">
|
||||||
|
<div className="grid grid-cols-4 items-center gap-4">
|
||||||
|
<Label className="text-right">Nome</Label>
|
||||||
|
<span className="col-span-3 font-medium">{viewingDoctor?.nome}</span>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-4 items-center gap-4">
|
||||||
|
<Label className="text-right">Especialidade</Label>
|
||||||
|
<span className="col-span-3">
|
||||||
|
<Badge variant="outline">{viewingDoctor?.especialidade}</Badge>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-4 items-center gap-4">
|
||||||
|
<Label className="text-right">CRM</Label>
|
||||||
|
<span className="col-span-3">{viewingDoctor?.crm}</span>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-4 items-center gap-4">
|
||||||
|
<Label className="text-right">Email</Label>
|
||||||
|
<span className="col-span-3">{viewingDoctor?.email}</span>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-4 items-center gap-4">
|
||||||
|
<Label className="text-right">Telefone</Label>
|
||||||
|
<span className="col-span-3">{viewingDoctor?.telefone}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button onClick={() => setViewingDoctor(null)}>Fechar</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
Mostrando {filtered.length} de {doctors.length}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,142 +18,6 @@ export type ApiOk<T = any> = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// ===== TIPOS COMUNS =====
|
|
||||||
export type Endereco = {
|
|
||||||
cep?: string;
|
|
||||||
logradouro?: string;
|
|
||||||
numero?: string;
|
|
||||||
complemento?: string;
|
|
||||||
bairro?: string;
|
|
||||||
cidade?: string;
|
|
||||||
estado?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
// ===== PACIENTES =====
|
|
||||||
export type Paciente = {
|
|
||||||
id: string;
|
|
||||||
full_name: string;
|
|
||||||
social_name?: string | null;
|
|
||||||
cpf?: string;
|
|
||||||
rg?: string | null;
|
|
||||||
sex?: string | null;
|
|
||||||
birth_date?: string | null;
|
|
||||||
phone_mobile?: string;
|
|
||||||
email?: string;
|
|
||||||
cep?: string | null;
|
|
||||||
street?: string | null;
|
|
||||||
number?: string | null;
|
|
||||||
complement?: string | null;
|
|
||||||
neighborhood?: string | null;
|
|
||||||
city?: string | null;
|
|
||||||
state?: string | null;
|
|
||||||
notes?: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type PacienteInput = {
|
|
||||||
full_name: string;
|
|
||||||
social_name?: string | null;
|
|
||||||
cpf: string;
|
|
||||||
rg?: string | null;
|
|
||||||
sex?: string | null;
|
|
||||||
birth_date?: string | null;
|
|
||||||
phone_mobile?: string | null;
|
|
||||||
email?: string | null;
|
|
||||||
cep?: string | null;
|
|
||||||
street?: string | null;
|
|
||||||
number?: string | null;
|
|
||||||
complement?: string | null;
|
|
||||||
neighborhood?: string | null;
|
|
||||||
city?: string | null;
|
|
||||||
state?: string | null;
|
|
||||||
notes?: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
// ===== MÉDICOS =====
|
|
||||||
export type FormacaoAcademica = {
|
|
||||||
instituicao: string;
|
|
||||||
curso: string;
|
|
||||||
ano_conclusao: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type DadosBancarios = {
|
|
||||||
banco: string;
|
|
||||||
agencia: string;
|
|
||||||
conta: string;
|
|
||||||
tipo_conta: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
// ===== MÉDICOS =====
|
|
||||||
export type Medico = {
|
|
||||||
id: string;
|
|
||||||
full_name: string; // Altere 'nome' para 'full_name'
|
|
||||||
nome_social?: string | null;
|
|
||||||
cpf?: string;
|
|
||||||
rg?: string | null;
|
|
||||||
sexo?: string | null;
|
|
||||||
data_nascimento?: string | null;
|
|
||||||
telefone?: string;
|
|
||||||
celular?: string;
|
|
||||||
contato_emergencia?: string;
|
|
||||||
email?: string;
|
|
||||||
crm?: string;
|
|
||||||
estado_crm?: string;
|
|
||||||
rqe?: string;
|
|
||||||
formacao_academica?: FormacaoAcademica[];
|
|
||||||
curriculo_url?: string | null;
|
|
||||||
especialidade?: string;
|
|
||||||
observacoes?: string | null;
|
|
||||||
foto_url?: string | null;
|
|
||||||
tipo_vinculo?: string;
|
|
||||||
dados_bancarios?: DadosBancarios;
|
|
||||||
agenda_horario?: string;
|
|
||||||
valor_consulta?: number | string;
|
|
||||||
active?: boolean;
|
|
||||||
cep?: string;
|
|
||||||
city?: string;
|
|
||||||
complement?: string;
|
|
||||||
neighborhood?: string;
|
|
||||||
number?: string;
|
|
||||||
phone2?: string;
|
|
||||||
state?: string;
|
|
||||||
street?: string;
|
|
||||||
created_at?: string;
|
|
||||||
created_by?: string;
|
|
||||||
updated_at?: string;
|
|
||||||
updated_by?: string;
|
|
||||||
user_id?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
// ===== MÉDICOS =====
|
|
||||||
// ...existing code...
|
|
||||||
export type MedicoInput = {
|
|
||||||
user_id?: string | null;
|
|
||||||
crm: string;
|
|
||||||
crm_uf: string;
|
|
||||||
specialty: string;
|
|
||||||
full_name: string;
|
|
||||||
cpf: string;
|
|
||||||
email: string;
|
|
||||||
phone_mobile: string;
|
|
||||||
phone2?: string | null;
|
|
||||||
cep: string;
|
|
||||||
street: string;
|
|
||||||
number: string;
|
|
||||||
complement?: string;
|
|
||||||
neighborhood?: string;
|
|
||||||
city: string;
|
|
||||||
state: string;
|
|
||||||
birth_date: string | null;
|
|
||||||
rg?: string | null;
|
|
||||||
active?: boolean;
|
|
||||||
created_by?: string | null;
|
|
||||||
updated_by?: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// ===== CONFIG =====
|
// ===== CONFIG =====
|
||||||
export const API_BASE =
|
export const API_BASE =
|
||||||
process.env.NEXT_PUBLIC_API_BASE ?? "https://yuanqfswhberkoevtmfr.supabase.co";
|
process.env.NEXT_PUBLIC_API_BASE ?? "https://yuanqfswhberkoevtmfr.supabase.co";
|
||||||
@ -215,349 +79,6 @@ export function rangeHeaders(page?: number, limit?: number): Record<string, stri
|
|||||||
return { Range: `${start}-${end}`, "Range-Unit": "items" };
|
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Nova função para busca avançada de pacientes
|
|
||||||
export async function buscarPacientes(termo: string): Promise<Paciente[]> {
|
|
||||||
if (!termo || termo.trim().length < 2) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const searchTerm = termo.toLowerCase().trim();
|
|
||||||
const digitsOnly = searchTerm.replace(/\D/g, '');
|
|
||||||
|
|
||||||
// Monta queries para buscar em múltiplos campos
|
|
||||||
const queries = [];
|
|
||||||
|
|
||||||
// Busca por ID se parece com UUID
|
|
||||||
if (searchTerm.includes('-') && searchTerm.length > 10) {
|
|
||||||
queries.push(`id=eq.${searchTerm}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Busca por CPF (com e sem formatação)
|
|
||||||
if (digitsOnly.length >= 11) {
|
|
||||||
queries.push(`cpf=eq.${digitsOnly}`);
|
|
||||||
} else if (digitsOnly.length >= 3) {
|
|
||||||
queries.push(`cpf=ilike.*${digitsOnly}*`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Busca por nome (usando ilike para busca case-insensitive)
|
|
||||||
if (searchTerm.length >= 2) {
|
|
||||||
queries.push(`full_name=ilike.*${searchTerm}*`);
|
|
||||||
queries.push(`social_name=ilike.*${searchTerm}*`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Busca por email se contém @
|
|
||||||
if (searchTerm.includes('@')) {
|
|
||||||
queries.push(`email=ilike.*${searchTerm}*`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const results: Paciente[] = [];
|
|
||||||
const seenIds = new Set<string>();
|
|
||||||
|
|
||||||
// Executa as buscas e combina resultados únicos
|
|
||||||
for (const query of queries) {
|
|
||||||
try {
|
|
||||||
const url = `${REST}/patients?${query}&limit=10`;
|
|
||||||
const res = await fetch(url, { method: "GET", headers: baseHeaders() });
|
|
||||||
const arr = await parse<Paciente[]>(res);
|
|
||||||
|
|
||||||
if (arr?.length > 0) {
|
|
||||||
for (const paciente of arr) {
|
|
||||||
if (!seenIds.has(paciente.id)) {
|
|
||||||
seenIds.add(paciente.id);
|
|
||||||
results.push(paciente);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`Erro na busca com query: ${query}`, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results.slice(0, 20); // Limita a 20 resultados
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nova função para busca avançada de médicos
|
|
||||||
export async function buscarMedicos(termo: string): Promise<Medico[]> {
|
|
||||||
if (!termo || termo.trim().length < 2) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const searchTerm = termo.toLowerCase().trim();
|
|
||||||
const digitsOnly = searchTerm.replace(/\D/g, '');
|
|
||||||
|
|
||||||
// Monta queries para buscar em múltiplos campos
|
|
||||||
const queries = [];
|
|
||||||
|
|
||||||
// Busca por ID se parece com UUID
|
|
||||||
if (searchTerm.includes('-') && searchTerm.length > 10) {
|
|
||||||
queries.push(`id=eq.${searchTerm}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Busca por CRM (com e sem formatação)
|
|
||||||
if (digitsOnly.length >= 3) {
|
|
||||||
queries.push(`crm=ilike.*${digitsOnly}*`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Busca por nome (usando ilike para busca case-insensitive)
|
|
||||||
if (searchTerm.length >= 2) {
|
|
||||||
queries.push(`full_name=ilike.*${searchTerm}*`);
|
|
||||||
queries.push(`nome_social=ilike.*${searchTerm}*`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Busca por email se contém @
|
|
||||||
if (searchTerm.includes('@')) {
|
|
||||||
queries.push(`email=ilike.*${searchTerm}*`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Busca por especialidade
|
|
||||||
if (searchTerm.length >= 2) {
|
|
||||||
queries.push(`specialty=ilike.*${searchTerm}*`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const results: Medico[] = [];
|
|
||||||
const seenIds = new Set<string>();
|
|
||||||
|
|
||||||
// Executa as buscas e combina resultados únicos
|
|
||||||
for (const query of queries) {
|
|
||||||
try {
|
|
||||||
const url = `${REST}/doctors?${query}&limit=10`;
|
|
||||||
const res = await fetch(url, { method: "GET", headers: baseHeaders() });
|
|
||||||
const arr = await parse<Medico[]>(res);
|
|
||||||
|
|
||||||
if (arr?.length > 0) {
|
|
||||||
for (const medico of arr) {
|
|
||||||
if (!seenIds.has(medico.id)) {
|
|
||||||
seenIds.add(medico.id);
|
|
||||||
results.push(medico);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`Erro na busca com query: ${query}`, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results.slice(0, 20); // Limita a 20 resultados
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function buscarMedicoPorId(id: string | number): Promise<Medico> {
|
|
||||||
// Primeiro tenta buscar no Supabase (dados reais)
|
|
||||||
try {
|
|
||||||
const url = `${REST}/doctors?id=eq.${id}`;
|
|
||||||
const res = await fetch(url, { method: "GET", headers: baseHeaders() });
|
|
||||||
const arr = await parse<Medico[]>(res);
|
|
||||||
if (arr && arr.length > 0) {
|
|
||||||
console.log('✅ Médico encontrado no Supabase:', arr[0]);
|
|
||||||
console.log('🔍 Campo especialidade no médico:', {
|
|
||||||
especialidade: arr[0].especialidade,
|
|
||||||
specialty: (arr[0] as any).specialty,
|
|
||||||
hasEspecialidade: !!arr[0].especialidade,
|
|
||||||
hasSpecialty: !!((arr[0] as any).specialty)
|
|
||||||
});
|
|
||||||
return arr[0];
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('⚠️ Erro ao buscar no Supabase, tentando mock API:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Se não encontrar no Supabase, tenta o mock API
|
|
||||||
try {
|
|
||||||
const url = `https://mock.apidog.com/m1/1053378-0-default/rest/v1/doctors/${id}`;
|
|
||||||
const res = await fetch(url, {
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
"Accept": "application/json"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
if (res.status === 404) {
|
|
||||||
throw new Error("404: Médico não encontrado");
|
|
||||||
}
|
|
||||||
throw new Error(`Erro ao buscar médico: ${res.status} ${res.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const medico = await res.json();
|
|
||||||
console.log('✅ Médico encontrado no Mock API:', medico);
|
|
||||||
return medico as Medico;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Erro ao buscar médico em ambas as APIs:', error);
|
|
||||||
throw new Error("404: Médico não encontrado");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dentro de lib/api.ts
|
|
||||||
export async function criarMedico(input: MedicoInput): Promise<Medico> {
|
|
||||||
console.log("Enviando os dados para a API:", input); // Log para depuração
|
|
||||||
|
|
||||||
const url = `${REST}/doctors`; // Endpoint de médicos
|
|
||||||
const res = await fetch(url, {
|
|
||||||
method: "POST",
|
|
||||||
headers: withPrefer({ ...baseHeaders(), "Content-Type": "application/json" }, "return=representation"),
|
|
||||||
body: JSON.stringify(input), // Enviando os dados padronizados
|
|
||||||
});
|
|
||||||
|
|
||||||
const arr = await parse<Medico[] | Medico>(res); // Resposta da API
|
|
||||||
return Array.isArray(arr) ? arr[0] : (arr as Medico); // Retorno do médico
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export async function atualizarMedico(id: string | number, input: MedicoInput): Promise<Medico> {
|
|
||||||
console.log(`🔄 Tentando atualizar médico ID: ${id}`);
|
|
||||||
console.log(`📤 Payload original:`, input);
|
|
||||||
|
|
||||||
// Criar um payload limpo apenas com campos básicos que sabemos que existem
|
|
||||||
const cleanPayload = {
|
|
||||||
full_name: input.full_name,
|
|
||||||
crm: input.crm,
|
|
||||||
specialty: input.specialty,
|
|
||||||
email: input.email,
|
|
||||||
phone_mobile: input.phone_mobile,
|
|
||||||
cpf: input.cpf,
|
|
||||||
cep: input.cep,
|
|
||||||
street: input.street,
|
|
||||||
number: input.number,
|
|
||||||
city: input.city,
|
|
||||||
state: input.state,
|
|
||||||
active: input.active ?? true
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log(`📤 Payload limpo:`, cleanPayload);
|
|
||||||
|
|
||||||
// Atualizar apenas no Supabase (dados reais)
|
|
||||||
try {
|
|
||||||
const url = `${REST}/doctors?id=eq.${id}`;
|
|
||||||
console.log(`🌐 URL de atualização: ${url}`);
|
|
||||||
|
|
||||||
const res = await fetch(url, {
|
|
||||||
method: "PATCH",
|
|
||||||
headers: withPrefer({ ...baseHeaders(), "Content-Type": "application/json" }, "return=representation"),
|
|
||||||
body: JSON.stringify(cleanPayload),
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`📡 Resposta do servidor: ${res.status} ${res.statusText}`);
|
|
||||||
|
|
||||||
if (res.ok) {
|
|
||||||
const arr = await parse<Medico[] | Medico>(res);
|
|
||||||
const result = Array.isArray(arr) ? arr[0] : (arr as Medico);
|
|
||||||
console.log('✅ Médico atualizado no Supabase:', result);
|
|
||||||
return result;
|
|
||||||
} else {
|
|
||||||
// Vamos tentar ver o erro detalhado
|
|
||||||
const errorText = await res.text();
|
|
||||||
console.error(`❌ Erro detalhado do Supabase:`, {
|
|
||||||
status: res.status,
|
|
||||||
statusText: res.statusText,
|
|
||||||
response: errorText,
|
|
||||||
headers: Object.fromEntries(res.headers.entries())
|
|
||||||
});
|
|
||||||
throw new Error(`Supabase error: ${res.status} ${res.statusText} - ${errorText}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Erro ao atualizar médico:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function excluirMedico(id: string | number): Promise<void> {
|
|
||||||
const url = `${REST}/doctors?id=eq.${id}`;
|
|
||||||
const res = await fetch(url, { method: "DELETE", headers: baseHeaders() });
|
|
||||||
await parse<any>(res);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== CEP (usado nos formulários) =====
|
// ===== CEP (usado nos formulários) =====
|
||||||
export async function buscarCepAPI(cep: string): Promise<{
|
export async function buscarCepAPI(cep: string): Promise<{
|
||||||
logradouro?: string;
|
logradouro?: string;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user