Compare commits

...

26 Commits

Author SHA1 Message Date
2d30fab9b5 Merge branch 'feature/user-profile-api' of https://git.popcode.com.br/RiseUP/riseup-squad20 into feature/user-profile-api 2025-10-02 14:39:51 -03:00
89da7f159e Merge branch 'develop' into feature/user-profile-api 2025-10-02 12:45:23 -03:00
b8369dd248 Merge pull request 'feat: ajustes na página do profissional' (#30) from feature/pacientes-corect into develop
Reviewed-on: #30
2025-10-02 14:25:08 +00:00
efdf89e2f5 feat: ajustes na página do profissional 2025-10-02 11:21:46 -03:00
398c409187 Merge pull request 'chore: update pnpm-lock.yaml to sync dependencies' (#29) from fix/pnpm-lock-update into develop
Reviewed-on: #29
2025-10-02 13:57:36 +00:00
0196b9b5e8 feat: Update pnpm-lock.yaml to synchronize dependencies 2025-10-02 03:25:46 -03:00
e85fbdeb15 Merge pull request 'adicionando-atualização-medicos' (#28) from feature/ajustes-form-medico into develop
Reviewed-on: #28
2025-10-02 06:07:30 +00:00
João Gustavo
ca3df1d1cf Merge remote-tracking branch 'origin/develop' into feature/ajustes-form-medico 2025-10-02 03:05:35 -03:00
João Gustavo
e9929e04f7 removing-test-pages 2025-10-02 02:59:50 -03:00
João Gustavo
6030263128 add-doctor-edit 2025-10-02 02:51:18 -03:00
389aa8adfb fix(doctor-form): load existing doctor data on edit mode
- Fix doctorId type to accept string | number | null to handle UUID values
- Remove Number() conversion that was causing NaN errors on edit
- Add debug console logs to track data loading process
- Improve error handling in useEffect for doctor and attachments loading
- Ensure form fields are properly populated with doctor data when editing
2025-10-02 02:14:39 -03:00
a1f8a7995c fix(patient-form): load existing patient data on edit mode
- Fix patientId type to accept string | number | null to handle UUID values
- Remove Number() conversion that was causing NaN errors on edit
- Add debug console logs to track data loading process
- Remove reference to non-existent photo_url field from Paciente type                                - Ensure form fields are properly populated with patient data when editing
2025-10-02 01:53:21 -03:00
João Gustavo
8d1473a148 add-doctor-id 2025-10-02 01:49:54 -03:00
ed6e33890a merge(feature/correc-api): merge branch 'feature/correc-api' into feature/ajustes-form-medico 2025-10-02 01:14:18 -03:00
a032465773 feat(doctor-form): add search doctor by ID button and logic to registration form 2025-10-02 00:54:43 -03:00
João Gustavo
b2ee5987c6 correcting-id-endpoint 2025-10-02 00:44:45 -03:00
ea63a73b43 feat(api): implementações e ajustes nas APIs de médicos e pacientes 2025-10-01 23:40:01 -03:00
dea39d9421 refactor(api): Separates the API modules for patients and physicians
Moves the functions and types related to patients and physicians from the single file lib/api.ts to their own dedicated files in lib/api/pacientes.ts and lib/api/medicos.ts.
2025-10-01 23:21:51 -03:00
cde4c42309 feat(auth): Creates user in patient registration
- Adds automatic user creation in the new patient registration flow.              - To avoid merge conflicts, the user and profile APIs have been separated from the main lib/api.ts file.
2025-10-01 23:20:35 -03:00
cb70c0a45a feat(auth): Implements user API and fixes login flows.
- Adds new API functions for user management (createUser, etc.).    - Fixes multiple bugs that prevented administrator login and broke the project build.
2025-10-01 23:17:39 -03:00
5655d0c607 feat(auth): implement user profile and access control
Adds user profile data fetching after login and protects the Doctors page so only administrators can access it.
2025-10-01 22:50:44 -03:00
9795011028 Merge pull request 'feature/setup-eslint' (#26) from feature/setup-eslint into develop
Reviewed-on: #26
2025-10-02 00:46:33 +00:00
3e2fd84287 chore: update package dependencies and types 2025-10-01 18:21:54 -03:00
a123013b51 chore: add prettier config to eslint 2025-10-01 18:00:39 -03:00
4da0388c27 fix(forms): correct broken re-export in appointment form 2025-10-01 15:37:39 -03:00
e8ab0a2970 chore: install eslint and plugins 2025-10-01 15:34:01 -03:00
12 changed files with 9280 additions and 491 deletions

View File

@ -17,7 +17,7 @@ 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
full_name: m.full_name ?? m.nome ?? "", // 👈 Correção: usar full_name como padrão
nome_social: m.nome_social ?? m.social_name ?? null,
cpf: m.cpf ?? "",
rg: m.rg ?? m.document_number ?? null,
@ -39,6 +39,20 @@ function normalizeMedico(m: any): Medico {
dados_bancarios: m.dados_bancarios ?? null,
agenda_horario: m.agenda_horario ?? null,
valor_consulta: m.valor_consulta ?? null,
active: m.active ?? true,
cep: m.cep ?? "",
city: m.city ?? "",
complement: m.complement ?? null,
neighborhood: m.neighborhood ?? "",
number: m.number ?? "",
phone2: m.phone2 ?? null,
state: m.state ?? "",
street: m.street ?? "",
created_at: m.created_at ?? null,
created_by: m.created_by ?? null,
updated_at: m.updated_at ?? null,
updated_by: m.updated_by ?? null,
user_id: m.user_id ?? null,
};
}
@ -50,33 +64,178 @@ export default function DoutoresPage() {
const [showForm, setShowForm] = useState(false);
const [editingId, setEditingId] = useState<string | null>(null);
const [viewingDoctor, setViewingDoctor] = useState<Medico | null>(null);
const [searchResults, setSearchResults] = useState<Medico[]>([]);
const [searchMode, setSearchMode] = useState(false);
const [searchTimeout, setSearchTimeout] = useState<NodeJS.Timeout | null>(null);
async function load() {
setLoading(true);
try {
const list = await listarMedicos({ limit: 50 });
setDoctors((list ?? []).map(normalizeMedico));
const normalized = (list ?? []).map(normalizeMedico);
console.log('🏥 Médicos carregados:', normalized);
setDoctors(normalized);
} finally {
setLoading(false);
}
}
// Função para detectar se é um UUID válido
function isValidUUID(str: string): boolean {
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
return uuidRegex.test(str);
}
// Função para buscar médicos no servidor
async function handleBuscarServidor(termoBusca?: string) {
const termo = (termoBusca || search).trim();
if (!termo) {
setSearchMode(false);
setSearchResults([]);
return;
}
console.log('🔍 Buscando médico por:', termo);
setLoading(true);
try {
// Se parece com UUID, tenta busca direta por ID
if (isValidUUID(termo)) {
console.log('📋 Detectado UUID, buscando por ID...');
try {
const medico = await buscarMedicoPorId(termo);
const normalizado = normalizeMedico(medico);
console.log('✅ Médico encontrado por ID:', normalizado);
setSearchResults([normalizado]);
setSearchMode(true);
return;
} catch (error) {
console.log('❌ Não encontrado por ID, tentando busca geral...');
}
}
// Busca geral
const resultados = await buscarMedicos(termo);
const normalizados = resultados.map(normalizeMedico);
console.log('📋 Resultados da busca geral:', normalizados);
setSearchResults(normalizados);
setSearchMode(true);
} catch (error) {
console.error('❌ Erro na busca:', error);
setSearchResults([]);
setSearchMode(true);
} finally {
setLoading(false);
}
}
// Handler para mudança no campo de busca com busca automática
function handleSearchChange(e: React.ChangeEvent<HTMLInputElement>) {
const valor = e.target.value;
setSearch(valor);
// Limpa o timeout anterior se existir
if (searchTimeout) {
clearTimeout(searchTimeout);
}
// Se limpar a busca, volta ao modo normal
if (!valor.trim()) {
setSearchMode(false);
setSearchResults([]);
return;
}
// Busca automática com debounce ajustável
// Para IDs (UUID) longos, faz busca no servidor
// Para busca parcial, usa apenas filtro local
const isLikeUUID = valor.includes('-') && valor.length > 10;
const shouldSearchServer = isLikeUUID || valor.length >= 3;
if (shouldSearchServer) {
const debounceTime = isLikeUUID ? 300 : 500;
const newTimeout = setTimeout(() => {
handleBuscarServidor(valor);
}, debounceTime);
setSearchTimeout(newTimeout);
} else {
// Para termos curtos, apenas usa filtro local
setSearchMode(false);
setSearchResults([]);
}
}
// Handler para Enter no campo de busca
function handleSearchKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
if (e.key === 'Enter') {
e.preventDefault();
handleBuscarServidor();
}
}
// Handler para o botão de busca
function handleClickBuscar() {
handleBuscarServidor();
}
useEffect(() => {
load();
}, []);
const filtered = useMemo(() => {
// Limpa o timeout quando o componente é desmontado
useEffect(() => {
return () => {
if (searchTimeout) {
clearTimeout(searchTimeout);
}
};
}, [searchTimeout]);
// Lista de médicos a exibir (busca ou filtro local)
const displayedDoctors = useMemo(() => {
console.log('🔍 Filtro - search:', search, 'searchMode:', searchMode, 'doctors:', doctors.length, 'searchResults:', searchResults.length);
// Se não tem busca, mostra todos os médicos
if (!search.trim()) return doctors;
const q = search.toLowerCase();
return doctors.filter((d) => {
const byName = (d.nome || "").toLowerCase().includes(q);
const byCrm = (d.crm || "").toLowerCase().includes(q);
const q = search.toLowerCase().trim();
const qDigits = q.replace(/\D/g, "");
// Se estamos em modo de busca (servidor), filtra os resultados da busca
const sourceList = searchMode ? searchResults : doctors;
console.log('🔍 Usando sourceList:', searchMode ? 'searchResults' : 'doctors', '- tamanho:', sourceList.length);
const filtered = sourceList.filter((d) => {
// Busca por nome
const byName = (d.full_name || "").toLowerCase().includes(q);
// Busca por CRM (remove formatação se necessário)
const byCrm = qDigits.length >= 3 && (d.crm || "").replace(/\D/g, "").includes(qDigits);
// Busca por ID (UUID completo ou parcial)
const byId = (d.id || "").toLowerCase().includes(q);
// Busca por email
const byEmail = (d.email || "").toLowerCase().includes(q);
// Busca por especialidade
const byEspecialidade = (d.especialidade || "").toLowerCase().includes(q);
return byName || byCrm || byEspecialidade;
const match = byName || byCrm || byId || byEmail || byEspecialidade;
if (match) {
console.log('✅ Match encontrado:', d.full_name, d.id, 'por:', { byName, byCrm, byId, byEmail, byEspecialidade });
}
return match;
});
}, [doctors, search]);
console.log('🔍 Resultados filtrados:', filtered.length);
return filtered;
}, [doctors, search, searchMode, searchResults]);
function handleAdd() {
setEditingId(null);

View File

@ -10,34 +10,29 @@ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, Di
import { Label } from "@/components/ui/label";
import { MoreHorizontal, Plus, Search, Eye, Edit, Trash2, ArrowLeft } from "lucide-react";
import { Paciente, Endereco, listarPacientes, buscarPacientePorId, excluirPaciente } from "@/lib/api";
import { Paciente, Endereco, listarPacientes, buscarPacientes, buscarPacientePorId, excluirPaciente } from "@/lib/api";
import { PatientRegistrationForm } from "@/components/forms/patient-registration-form";
function normalizePaciente(p: any): Paciente {
const endereco: Endereco = {
cep: p.endereco?.cep ?? p.cep ?? "",
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.full_name ?? "", // 👈 troca nome → full_name
nome_social: p.social_name ?? null, // 👈 Supabase usa social_name
full_name: p.full_name ?? p.name ?? p.nome ?? "",
social_name: p.social_name ?? p.nome_social ?? null,
cpf: p.cpf ?? "",
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 ?? "",
rg: p.rg ?? p.document_number ?? null,
sex: p.sex ?? p.sexo ?? null,
birth_date: p.birth_date ?? p.data_nascimento ?? null,
phone_mobile: p.phone_mobile ?? p.telefone ?? "",
email: p.email ?? "",
endereco,
observacoes: p.observacoes ?? p.notes ?? null,
foto_url: p.foto_url ?? null,
cep: p.cep ?? "",
street: p.street ?? p.logradouro ?? "",
number: p.number ?? p.numero ?? "",
complement: p.complement ?? p.complemento ?? "",
neighborhood: p.neighborhood ?? p.bairro ?? "",
city: p.city ?? p.cidade ?? "",
state: p.state ?? p.estado ?? "",
notes: p.notes ?? p.observacoes ?? null,
};
}
@ -56,7 +51,12 @@ export default function PacientesPage() {
try {
setLoading(true);
const data = await listarPacientes({ page: 1, limit: 20 });
setPatients((data ?? []).map(normalizePaciente));
if (Array.isArray(data)) {
setPatients(data.map(normalizePaciente));
} else {
setPatients([]);
}
setError(null);
} catch (e: any) {
setPatients([]);
@ -72,13 +72,23 @@ export default function PacientesPage() {
const filtered = useMemo(() => {
if (!search.trim()) return patients;
const q = search.toLowerCase();
const q = search.toLowerCase().trim();
const qDigits = q.replace(/\D/g, "");
return patients.filter((p) => {
const byName = (p.nome || "").toLowerCase().includes(q);
const byCPF = (p.cpf || "").replace(/\D/g, "").includes(qDigits);
const byId = String(p.id || "").includes(qDigits);
return byName || byCPF || byId;
// Busca por nome
const byName = (p.full_name || "").toLowerCase().includes(q);
// Busca por CPF (remove formatação)
const byCPF = qDigits.length >= 3 && (p.cpf || "").replace(/\D/g, "").includes(qDigits);
// Busca por ID (UUID completo ou parcial)
const byId = (p.id || "").toLowerCase().includes(q);
// Busca por email
const byEmail = (p.email || "").toLowerCase().includes(q);
return byName || byCPF || byId || byEmail;
});
}, [patients, search]);
@ -122,25 +132,33 @@ export default function PacientesPage() {
const q = search.trim();
if (!q) return loadAll();
if (/^\d+$/.test(q)) {
try {
setLoading(true);
try {
setLoading(true);
setError(null);
// Se parece com ID (UUID), busca diretamente
if (q.includes('-') && q.length > 10) {
const one = await buscarPacientePorId(q);
setPatients(one ? [normalizePaciente(one)] : []);
setError(one ? null : "Paciente não encontrado.");
} catch (e: any) {
setPatients([]);
setError(e?.message || "Paciente não encontrado.");
} finally {
setLoading(false);
// Limpa o campo de busca para que o filtro não interfira
setSearch("");
return;
}
return;
}
await loadAll();
setTimeout(() => setSearch(q), 0);
// Para outros termos, usa busca avançada
const results = await buscarPacientes(q);
setPatients(results.map(normalizePaciente));
setError(results.length === 0 ? "Nenhum paciente encontrado." : null);
// Limpa o campo de busca para que o filtro não interfira
setSearch("");
} catch (e: any) {
setPatients([]);
setError(e?.message || "Erro na busca.");
} finally {
setLoading(false);
}
}
if (loading) return <p>Carregando pacientes...</p>;
@ -159,7 +177,7 @@ export default function PacientesPage() {
<PatientRegistrationForm
inline
mode={editingId ? "edit" : "create"}
patientId={editingId ? Number(editingId) : null}
patientId={editingId}
onSaved={handleSaved}
onClose={() => setShowForm(false)}
/>
@ -210,11 +228,11 @@ export default function PacientesPage() {
{filtered.length > 0 ? (
filtered.map((p) => (
<TableRow key={p.id}>
<TableCell className="font-medium">{p.nome || "(sem nome)"}</TableCell>
<TableCell className="font-medium">{p.full_name || "(sem nome)"}</TableCell>
<TableCell>{p.cpf || "-"}</TableCell>
<TableCell>{p.telefone || "-"}</TableCell>
<TableCell>{p.endereco?.cidade || "-"}</TableCell>
<TableCell>{p.endereco?.estado || "-"}</TableCell>
<TableCell>{p.phone_mobile || "-"}</TableCell>
<TableCell>{p.city || "-"}</TableCell>
<TableCell>{p.state || "-"}</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
@ -258,13 +276,13 @@ export default function PacientesPage() {
<DialogHeader>
<DialogTitle>Detalhes do Paciente</DialogTitle>
<DialogDescription>
Informações detalhadas de {viewingPatient.nome}.
Informações detalhadas de {viewingPatient.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">{viewingPatient.nome}</span>
<span className="col-span-3 font-medium">{viewingPatient.full_name}</span>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label className="text-right">CPF</Label>
@ -272,17 +290,17 @@ export default function PacientesPage() {
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label className="text-right">Telefone</Label>
<span className="col-span-3">{viewingPatient.telefone}</span>
<span className="col-span-3">{viewingPatient.phone_mobile}</span>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label className="text-right">Endereço</Label>
<span className="col-span-3">
{`${viewingPatient.endereco?.logradouro || ''}, ${viewingPatient.endereco?.numero || ''} - ${viewingPatient.endereco?.bairro || ''}, ${viewingPatient.endereco?.cidade || ''} - ${viewingPatient.endereco?.estado || ''}`}
{`${viewingPatient.street || ''}, ${viewingPatient.number || ''} - ${viewingPatient.neighborhood || ''}, ${viewingPatient.city || ''} - ${viewingPatient.state || ''}`}
</span>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label className="text-right">Observações</Label>
<span className="col-span-3">{viewingPatient.observacoes || "Nenhuma"}</span>
<span className="col-span-3">{viewingPatient.notes || "Nenhuma"}</span>
</div>
</div>
<DialogFooter>

View File

@ -7,6 +7,7 @@ import "react-quill/dist/quill.snow.css";
import Link from "next/link";
import ProtectedRoute from "@/components/ProtectedRoute";
import { useAuth } from "@/hooks/useAuth";
import { buscarPacientes } from "@/lib/api";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@ -566,53 +567,190 @@ const ProfissionalPage = () => {
};
const renderPacientesSection = () => (
<div className="bg-white shadow-md rounded-lg p-6">
<h2 className="text-2xl font-bold mb-4">Gerenciamento de Pacientes</h2>
<Table>
<TableHeader>
<TableRow>
<TableHead>Paciente</TableHead>
<TableHead>CPF</TableHead>
<TableHead>Idade</TableHead>
<TableHead>Status do laudo</TableHead>
<TableHead>Ações</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{pacientes.map((paciente) => (
<TableRow key={paciente.cpf}>
<TableCell className="font-medium">{paciente.nome}</TableCell>
<TableCell>{paciente.cpf}</TableCell>
<TableCell>{paciente.idade}</TableCell>
<TableCell>{paciente.statusLaudo}</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<div className="relative group">
<Button
variant="outline"
size="sm"
className="border-primary text-primary hover:bg-primary hover:text-white cursor-pointer"
function PacientesSection({ handleAbrirProntuario, setActiveSection }) {
// Estados para busca de pacientes
const [buscaPaciente, setBuscaPaciente] = useState("");
const [pacientesBusca, setPacientesBusca] = useState<any[]>([]);
const [carregandoBusca, setCarregandoBusca] = useState(false);
const [erroBusca, setErroBusca] = useState<string | null>(null);
// Função para buscar pacientes
const handleBuscarPaciente = async () => {
if (!buscaPaciente.trim()) {
setPacientesBusca([]);
setErroBusca(null);
return;
}
setCarregandoBusca(true);
setErroBusca(null);
try {
// Importa a função de busca
const { buscarPacientes } = await import("@/lib/api");
const resultados = await buscarPacientes(buscaPaciente.trim());
if (resultados.length === 0) {
setErroBusca("Nenhum paciente encontrado com os critérios informados.");
setPacientesBusca([]);
} else {
// Transforma os dados da API para o formato usado no componente
const pacientesFormatados = resultados.map(p => ({
nome: p.full_name || "Nome não informado",
cpf: p.cpf || "CPF não informado",
idade: p.birth_date ? new Date().getFullYear() - new Date(p.birth_date).getFullYear() : "N/A",
statusLaudo: "Pendente", // Status padrão
id: p.id
}));
setPacientesBusca(pacientesFormatados);
setErroBusca(null);
}
} catch (error: any) {
console.error("Erro ao buscar pacientes:", error);
setErroBusca(error.message || "Erro ao buscar pacientes. Tente novamente.");
setPacientesBusca([]);
} finally {
setCarregandoBusca(false);
}
};
const handleLimparBusca = () => {
setBuscaPaciente("");
setPacientesBusca([]);
setErroBusca(null);
};
return (
<div className="bg-white shadow-md rounded-lg p-6">
<h2 className="text-2xl font-bold mb-4">Gerenciamento de Pacientes</h2>
{/* Campo de busca */}
<div className="mb-6 p-4 bg-gray-50 rounded-lg">
<h3 className="text-lg font-semibold mb-3">Buscar Paciente</h3>
<div className="flex gap-2">
<div className="flex-1">
<Input
placeholder="Digite ID, CPF, nome ou email do paciente..."
value={buscaPaciente}
onChange={(e) => setBuscaPaciente(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleBuscarPaciente()}
className="w-full"
/>
</div>
<Button
onClick={handleBuscarPaciente}
disabled={carregandoBusca}
className="flex items-center gap-2"
>
{carregandoBusca ? (
<>
<div className="animate-spin h-4 w-4 border-2 border-white border-t-transparent rounded-full"></div>
Buscando...
</>
) : (
<>
<User className="h-4 w-4" />
Buscar
</>
)}
</Button>
{(buscaPaciente || pacientesBusca.length > 0 || erroBusca) && (
<Button
variant="outline"
onClick={handleLimparBusca}
className="flex items-center gap-2"
>
<X className="h-4 w-4" />
Limpar
</Button>
)}
</div>
{/* Resultados da busca */}
{erroBusca && (
<div className="mt-3 p-3 bg-red-50 border border-red-200 rounded-md">
<p className="text-red-700 text-sm">{erroBusca}</p>
</div>
)}
{pacientesBusca.length > 0 && (
<div className="mt-4">
<h4 className="text-md font-medium mb-2">Resultados da busca ({pacientesBusca.length}):</h4>
<div className="space-y-2">
{pacientesBusca.map((paciente, index) => (
<div key={index} className="flex items-center justify-between p-3 bg-white border rounded-lg hover:shadow-sm">
<div>
<p className="font-medium">{paciente.nome}</p>
<p className="text-sm text-gray-600">CPF: {paciente.cpf} Idade: {paciente.idade} anos</p>
</div>
<Button
size="sm"
onClick={() => {
handleAbrirProntuario(paciente);
setActiveSection('prontuario');
}}
className="flex items-center gap-2"
>
<FolderOpen className="h-4 w-4" />
Abrir Prontuário
</Button>
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-3 py-1 bg-gray-900 text-white text-xs rounded-md opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none whitespace-nowrap z-50">
Ver informações do paciente
<div className="absolute top-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-gray-900"></div>
</div>
</div>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
))}
</div>
</div>
)}
</div>
{/* Tabela de pacientes padrão */}
<div>
<h3 className="text-lg font-semibold mb-3">Pacientes Recentes</h3>
<Table>
<TableHeader>
<TableRow>
<TableHead>Paciente</TableHead>
<TableHead>CPF</TableHead>
<TableHead>Idade</TableHead>
<TableHead>Status do laudo</TableHead>
<TableHead>Ações</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{pacientes.map((paciente) => (
<TableRow key={paciente.cpf}>
<TableCell className="font-medium">{paciente.nome}</TableCell>
<TableCell>{paciente.cpf}</TableCell>
<TableCell>{paciente.idade}</TableCell>
<TableCell>{paciente.statusLaudo}</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<div className="relative group">
<Button
variant="outline"
size="sm"
className="border-primary text-primary hover:bg-primary hover:text-white cursor-pointer"
onClick={() => {
handleAbrirProntuario(paciente);
setActiveSection('prontuario');
}}
>
<FolderOpen className="h-4 w-4" />
</Button>
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-3 py-1 bg-gray-900 text-white text-xs rounded-md opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none whitespace-nowrap z-50">
Ver informações do paciente
<div className="absolute top-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-gray-900"></div>
</div>
</div>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
);
};
const renderProntuarioSection = () => (
@ -2291,7 +2429,7 @@ function LaudoEditor() {
case 'calendario':
return renderCalendarioSection();
case 'pacientes':
return renderPacientesSection();
return <PacientesSection handleAbrirProntuario={handleAbrirProntuario} setActiveSection={setActiveSection} />;
case 'prontuario':
return renderProntuarioSection();
case 'laudos':

View File

@ -1,6 +1,6 @@
"use client"
import { Bell, Search, ChevronDown } from "lucide-react"
import { Bell, ChevronDown } from "lucide-react"
import { useAuth } from "@/hooks/useAuth"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
@ -40,11 +40,6 @@ export function PagesHeader({ title = "", subtitle = "" }: { title?: string, sub
</div>
<div className="flex items-center space-x-4">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
<Input placeholder="Buscar paciente" className="pl-10 w-64" />
</div>
<Button variant="ghost" size="icon">
<Bell className="h-4 w-4" />
</Button>

View File

@ -1,6 +1,7 @@
"use client";
import { useEffect, useMemo, useState } from "react";
import { buscarPacientePorId } from "@/lib/api";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@ -21,8 +22,10 @@ import {
listarAnexosMedico,
adicionarAnexoMedico,
removerAnexoMedico,
MedicoInput,
MedicoInput, // 👈 importado do lib/api
Medico, // 👈 adicionado import do tipo Medico
} from "@/lib/api";
;
import { buscarCepAPI } from "@/lib/api";
@ -39,39 +42,16 @@ type DadosBancarios = {
tipo_conta: string;
};
export type Medico = {
id: string;
nome?: string;
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;
};
type Mode = "create" | "edit";
export interface DoctorRegistrationFormProps {
open?: boolean;
onOpenChange?: (open: boolean) => void;
doctorId?: number | null;
doctorId?: string | number | null;
inline?: boolean;
mode?: Mode;
onSaved?: (medico: Medico) => void;
@ -80,7 +60,7 @@ export interface DoctorRegistrationFormProps {
type FormData = {
photo: File | null;
nome: string;
full_name: string; // Substitua 'nome' por 'full_name'
nome_social: string;
crm: string;
estado_crm: string;
@ -107,14 +87,13 @@ type FormData = {
anexos: File[];
tipo_vinculo: string;
dados_bancarios: DadosBancarios;
agenda_horario: string;
valor_consulta: string;
};
const initial: FormData = {
photo: null,
nome: "",
full_name: "",
nome_social: "",
crm: "",
estado_crm: "",
@ -128,7 +107,7 @@ const initial: FormData = {
data_nascimento: "",
email: "",
telefone: "",
celular: "",
celular: "", // Aqui, 'celular' pode ser 'phone_mobile'
contato_emergencia: "",
cep: "",
logradouro: "",
@ -152,6 +131,7 @@ const initial: FormData = {
export function DoctorRegistrationForm({
open = true,
onOpenChange,
@ -175,46 +155,78 @@ export function DoctorRegistrationForm({
let alive = true;
async function load() {
if (mode === "edit" && doctorId) {
const medico = await buscarMedicoPorId(doctorId);
if (!alive) return;
setForm({
photo: null,
nome: medico.nome ?? "",
nome_social: medico.nome_social ?? "",
crm: medico.crm ?? "",
estado_crm: medico.estado_crm ?? "",
rqe: medico.rqe ?? "",
formacao_academica: medico.formacao_academica ?? [],
curriculo: null,
especialidade: medico.especialidade ?? "",
cpf: medico.cpf ?? "",
rg: medico.rg ?? "",
sexo: medico.sexo ?? "",
data_nascimento: medico.data_nascimento ?? "",
email: medico.email ?? "",
telefone: medico.telefone ?? "",
celular: medico.celular ?? "",
contato_emergencia: medico.contato_emergencia ?? "",
cep: "",
logradouro: "",
numero: "",
complemento: "",
bairro: "",
cidade: "",
estado: "",
observacoes: medico.observacoes ?? "",
anexos: [],
tipo_vinculo: medico.tipo_vinculo ?? "",
dados_bancarios: medico.dados_bancarios ?? { banco: "", agencia: "", conta: "", tipo_conta: "" },
agenda_horario: medico.agenda_horario ?? "",
valor_consulta: medico.valor_consulta ? String(medico.valor_consulta) : "",
});
try {
const list = await listarAnexosMedico(doctorId);
setServerAnexos(list ?? []);
} catch {}
console.log("[DoctorForm] Carregando médico ID:", doctorId);
const medico = await buscarMedicoPorId(String(doctorId));
console.log("[DoctorForm] Dados recebidos do API:", medico);
console.log("[DoctorForm] Campos principais:", {
full_name: medico.full_name,
crm: medico.crm,
especialidade: medico.especialidade,
specialty: (medico as any).specialty,
cpf: medico.cpf,
email: medico.email
});
console.log("[DoctorForm] Verificando especialidade:", {
'medico.especialidade': medico.especialidade,
'medico.specialty': (medico as any).specialty,
'typeof especialidade': typeof medico.especialidade,
'especialidade length': medico.especialidade?.length
});
if (!alive) return;
// Busca a especialidade em diferentes campos possíveis
const especialidade = medico.especialidade ||
(medico as any).specialty ||
(medico as any).speciality ||
"";
console.log('🎯 Especialidade encontrada:', especialidade);
const formData = {
photo: null,
full_name: String(medico.full_name || ""),
nome_social: String(medico.nome_social || ""),
crm: String(medico.crm || ""),
estado_crm: String(medico.estado_crm || ""),
rqe: String(medico.rqe || ""),
formacao_academica: Array.isArray(medico.formacao_academica) ? medico.formacao_academica : [],
curriculo: null,
especialidade: String(especialidade),
cpf: String(medico.cpf || ""),
rg: String(medico.rg || ""),
sexo: String(medico.sexo || ""),
data_nascimento: String(medico.data_nascimento || ""),
email: String(medico.email || ""),
telefone: String(medico.telefone || ""),
celular: String(medico.celular || ""),
contato_emergencia: String(medico.contato_emergencia || ""),
cep: String(medico.cep || ""),
logradouro: String(medico.street || ""),
numero: String(medico.number || ""),
complemento: String(medico.complement || ""),
bairro: String(medico.neighborhood || ""),
cidade: String(medico.city || ""),
estado: String(medico.state || ""),
observacoes: String(medico.observacoes || ""),
anexos: [],
tipo_vinculo: String(medico.tipo_vinculo || ""),
dados_bancarios: medico.dados_bancarios || { banco: "", agencia: "", conta: "", tipo_conta: "" },
agenda_horario: String(medico.agenda_horario || ""),
valor_consulta: medico.valor_consulta ? String(medico.valor_consulta) : "",
};
console.log("[DoctorForm] Dados do formulário preparados:", formData);
setForm(formData);
try {
const list = await listarAnexosMedico(String(doctorId));
setServerAnexos(list ?? []);
} catch (err) {
console.error("[DoctorForm] Erro ao carregar anexos:", err);
}
} catch (err) {
console.error("[DoctorForm] Erro ao carregar médico:", err);
}
}
}
load();
@ -222,10 +234,11 @@ export function DoctorRegistrationForm({
}, [mode, doctorId]);
function setField<T extends keyof FormData>(k: T, v: FormData[T]) {
setForm((s) => ({ ...s, [k]: v }));
if (errors[k as string]) setErrors((e) => ({ ...e, [k]: "" }));
}
function setField<T extends keyof FormData>(k: T, v: FormData[T]) {
setForm((s) => ({ ...s, [k]: v }));
if (errors[k as string]) setErrors((e) => ({ ...e, [k]: "" }));
}
function addFormacao() {
@ -299,76 +312,92 @@ export function DoctorRegistrationForm({
}
function validateLocal(): boolean {
const e: Record<string, string> = {};
if (!form.nome.trim()) e.nome = "Nome é obrigatório";
if (!form.cpf.trim()) e.cpf = "CPF é obrigatório";
if (!form.crm.trim()) e.crm = "CRM é obrigatório";
if (!form.especialidade.trim()) e.especialidade = "Especialidade é obrigatória";
setErrors(e);
return Object.keys(e).length === 0;
}
function validateLocal(): boolean {
const e: Record<string, string> = {};
async function handleSubmit(ev: React.FormEvent) {
if (!form.full_name.trim()) e.full_name = "Nome é obrigatório";
if (!form.cpf.trim()) e.cpf = "CPF é obrigatório";
if (!form.crm.trim()) e.crm = "CRM é obrigatório";
if (!form.especialidade.trim()) e.especialidade = "Especialidade é obrigatória";
if (!form.cep.trim()) e.cep = "CEP é obrigatório"; // Verifique se o CEP está preenchido
if (!form.bairro.trim()) e.bairro = "Bairro é obrigatório"; // Verifique se o bairro está preenchido
if (!form.cidade.trim()) e.cidade = "Cidade é obrigatória"; // Verifique se a cidade está preenchida
setErrors(e);
return Object.keys(e).length === 0;
}
async function handleSubmit(ev: React.FormEvent) {
ev.preventDefault();
if (!validateLocal()) return;
console.log("Submitting the form..."); // Verifique se a função está sendo chamada
if (!validateLocal()) {
console.log("Validation failed");
return; // Se a validação falhar, saia da função.
}
setSubmitting(true);
setErrors((e) => ({ ...e, submit: "" }));
try {
const payload: MedicoInput = {
nome: form.nome,
nome_social: form.nome_social || null,
cpf: form.cpf || null,
rg: form.rg || null,
sexo: form.sexo || null,
data_nascimento: form.data_nascimento || null,
telefone: form.telefone || null,
celular: form.celular || null,
contato_emergencia: form.contato_emergencia || null,
email: form.email || null,
crm: form.crm,
estado_crm: form.estado_crm || null,
rqe: form.rqe || null,
formacao_academica: form.formacao_academica ?? [],
curriculo_url: null,
especialidade: form.especialidade,
observacoes: form.observacoes || null,
tipo_vinculo: form.tipo_vinculo || null,
dados_bancarios: form.dados_bancarios ?? null,
agenda_horario: form.agenda_horario || null,
valor_consulta: form.valor_consulta || null,
};
const payload: MedicoInput = {
user_id: null,
crm: form.crm || "",
crm_uf: form.estado_crm || "",
specialty: form.especialidade || "",
full_name: form.full_name || "",
cpf: form.cpf || "",
email: form.email || "",
phone_mobile: form.celular || "",
phone2: form.telefone || null,
cep: form.cep || "",
street: form.logradouro || "",
number: form.numero || "",
complement: form.complemento || undefined,
neighborhood: form.bairro || undefined,
city: form.cidade || "",
state: form.estado || "",
birth_date: form.data_nascimento || null,
rg: form.rg || null,
active: true,
created_by: null,
updated_by: null,
};
// Validação dos campos obrigatórios
const requiredFields = ['crm', 'crm_uf', 'specialty', 'full_name', 'cpf', 'email', 'phone_mobile', 'cep', 'street', 'number', 'city', 'state'];
const missingFields = requiredFields.filter(field => !payload[field as keyof MedicoInput]);
if (missingFields.length > 0) {
console.warn('⚠️ Campos obrigatórios vazios:', missingFields);
}
console.log("📤 Payload being sent:", payload);
console.log("🔧 Mode:", mode, "DoctorId:", doctorId);
try {
if (mode === "edit" && !doctorId) {
throw new Error("ID do médico não fornecido para edição");
}
const saved = mode === "create"
? await criarMedico(payload)
: await atualizarMedico(doctorId as number, payload);
: await atualizarMedico(String(doctorId), payload);
const medicoId = saved.id;
if (form.photo) {
try {
await uploadFotoMedico(medicoId, form.photo);
} catch (e) {
console.warn("Falha ao enviar foto:", e);
}
}
if (form.anexos?.length) {
for (const f of form.anexos) {
try {
await adicionarAnexoMedico(medicoId, f);
} catch (e) {
console.warn("Falha ao enviar anexo:", f.name, e);
}
}
}
console.log("✅ Médico salvo com sucesso:", saved);
onSaved?.(saved);
if (inline) onClose?.();
else onOpenChange?.(false);
setSubmitting(false);
} catch (err: any) {
console.error("❌ Erro ao salvar médico:", err);
console.error("❌ Detalhes do erro:", {
message: err?.message,
status: err?.status,
stack: err?.stack
});
setErrors((e) => ({ ...e, submit: err?.message || "Erro ao salvar médico" }));
} finally {
setSubmitting(false);
@ -376,6 +405,10 @@ export function DoctorRegistrationForm({
}
function handlePhoto(e: React.ChangeEvent<HTMLInputElement>) {
const f = e.target.files?.[0];
if (!f) return;
@ -449,8 +482,10 @@ export function DoctorRegistrationForm({
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Nome *</Label>
<Input value={form.nome} onChange={(e) => setField("nome", e.target.value)} className={errors.nome ? "border-destructive" : ""} />
{errors.nome && <p className="text-sm text-destructive">{errors.nome}</p>}
<Input value={form.full_name} onChange={(e) => setField("full_name", e.target.value)} />
{errors.full_name && <p className="text-sm text-destructive">{errors.full_name}</p>}
</div>
<div className="space-y-2">
<Label>Nome Social</Label>
@ -471,16 +506,21 @@ export function DoctorRegistrationForm({
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Especialidade *</Label>
<Input value={form.especialidade} onChange={(e) => setField("especialidade", e.target.value)} className={errors.especialidade ? "border-destructive" : ""} />
{errors.especialidade && <p className="text-sm text-destructive">{errors.especialidade}</p>}
</div>
<div className="space-y-2">
<Label>RQE</Label>
<Input value={form.rqe} onChange={(e) => setField("rqe", e.target.value)} />
</div>
</div>
<div className="space-y-2">
<Label>Especialidade *</Label>
<Input
value={form.especialidade} // Mantenha o nome no form como 'especialidade'
onChange={(e) => setField("especialidade", e.target.value)} // Envia o valor correto
className={errors.especialidade ? "border-destructive" : ""}
/>
{errors.especialidade && <p className="text-sm text-destructive">{errors.especialidade}</p>}
</div>
<div className="space-y-2">
<Label>RQE</Label>
<Input value={form.rqe} onChange={(e) => setField("rqe", e.target.value)} />
</div>
</div>
<div className="space-y-2">
<Label>Currículo</Label>
@ -629,14 +669,25 @@ export function DoctorRegistrationForm({
<Label>E-mail</Label>
<Input value={form.email} onChange={(e) => setField("email", e.target.value)} />
</div>
<div className="space-y-2">
<Label>Telefone</Label>
<Input
value={form.telefone}
onChange={(e) => setField("telefone", formatPhone(e.target.value))}
placeholder="(XX) XXXXX-XXXX"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Telefone</Label>
<Input
value={form.telefone}
onChange={(e) => setField("telefone", formatPhone(e.target.value))}
placeholder="(XX) XXXXX-XXXX"
/>
</div>
<div className="space-y-2">
<Label>Celular</Label>
<Input
value={form.celular}
onChange={(e) => setField("celular", formatPhone(e.target.value))}
placeholder="(XX) XXXXX-XXXX"
/>
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
@ -703,11 +754,14 @@ export function DoctorRegistrationForm({
<div className="space-y-2">
<Label>Agenda/Horário</Label>
<Textarea
value={form.agenda_horario}
onChange={(e) => setField("agenda_horario", e.target.value)}
placeholder="Descreva os dias e horários de atendimento"
/>
// Dentro do form, apenas exiba o campo se precisar dele visualmente, mas não envie
<textarea
value={form.agenda_horario}
onChange={(e) => setField("agenda_horario", e.target.value)}
placeholder="Descreva os dias e horários de atendimento"
disabled={true} // Torne o campo apenas visual, sem enviar
/>
</div>
<div className="space-y-4">

View File

@ -39,7 +39,7 @@ type Mode = "create" | "edit";
export interface PatientRegistrationFormProps {
open?: boolean;
onOpenChange?: (open: boolean) => void;
patientId?: number | null;
patientId?: string | number | null;
inline?: boolean;
mode?: Mode;
onSaved?: (paciente: Paciente) => void;
@ -53,7 +53,7 @@ type FormData = {
cpf: string;
rg: string;
sexo: string;
data_nascimento: string;
birth_date: string; // 👈 corrigido
email: string;
telefone: string;
cep: string;
@ -74,7 +74,7 @@ const initial: FormData = {
cpf: "",
rg: "",
sexo: "",
data_nascimento: "",
birth_date: "", // 👈 corrigido
email: "",
telefone: "",
cep: "",
@ -88,6 +88,8 @@ const initial: FormData = {
anexos: [],
};
export function PatientRegistrationForm({
open = true,
onOpenChange,
@ -112,30 +114,33 @@ export function PatientRegistrationForm({
async function load() {
if (mode !== "edit" || patientId == null) return;
try {
console.log("[PatientForm] Carregando paciente ID:", patientId);
const p = await buscarPacientePorId(String(patientId));
console.log("[PatientForm] Dados recebidos:", p);
setForm((s) => ({
...s,
nome: p.nome || "",
nome_social: p.nome_social || "",
cpf: p.cpf || "",
rg: p.rg || "",
sexo: p.sexo || "",
data_nascimento: (p.data_nascimento as string) || "",
telefone: p.telefone || "",
email: p.email || "",
cep: p.endereco?.cep || "",
logradouro: p.endereco?.logradouro || "",
numero: p.endereco?.numero || "",
complemento: p.endereco?.complemento || "",
bairro: p.endereco?.bairro || "",
cidade: p.endereco?.cidade || "",
estado: p.endereco?.estado || "",
observacoes: p.observacoes || "",
}));
...s,
nome: p.full_name || "", // 👈 trocar nome → full_name
nome_social: p.social_name || "",
cpf: p.cpf || "",
rg: p.rg || "",
sexo: p.sex || "",
birth_date: p.birth_date || "", // 👈 trocar data_nascimento → birth_date
telefone: p.phone_mobile || "",
email: p.email || "",
cep: p.cep || "",
logradouro: p.street || "",
numero: p.number || "",
complemento: p.complement || "",
bairro: p.neighborhood || "",
cidade: p.city || "",
estado: p.state || "",
observacoes: p.notes || "",
}));
const ax = await listarAnexos(String(patientId)).catch(() => []);
setServerAnexos(Array.isArray(ax) ? ax : []);
} catch {
} catch (err) {
console.error("[PatientForm] Erro ao carregar paciente:", err);
}
}
load();
@ -188,27 +193,27 @@ export function PatientRegistrationForm({
}
function toPayload(): PacienteInput {
return {
nome: form.nome,
nome_social: form.nome_social || null,
cpf: form.cpf,
rg: form.rg || null,
sexo: form.sexo || null,
data_nascimento: form.data_nascimento || null,
telefone: form.telefone || null,
email: form.email || null,
endereco: {
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,
};
}
return {
full_name: form.nome, // 👈 troca 'nome' por 'full_name'
social_name: form.nome_social || null,
cpf: form.cpf,
rg: form.rg || null,
sex: form.sexo || null,
birth_date: form.birth_date || null, // 👈 troca data_nascimento → birth_date
phone_mobile: form.telefone || null,
email: form.email || null,
cep: form.cep || null,
street: form.logradouro || null,
number: form.numero || null,
complement: form.complemento || null,
neighborhood: form.bairro || null,
city: form.cidade || null,
state: form.estado || null,
notes: form.observacoes || null,
};
}
async function handleSubmit(ev: React.FormEvent) {
ev.preventDefault();
@ -435,7 +440,8 @@ export function PatientRegistrationForm({
</div>
<div className="space-y-2">
<Label>Data de Nascimento</Label>
<Input type="date" value={form.data_nascimento} onChange={(e) => setField("data_nascimento", e.target.value)} />
<Input type="date" value={form.birth_date} onChange={(e) => setField("birth_date", e.target.value)} />
</div>
</div>
</CardContent>

View File

@ -0,0 +1,35 @@
// eslint.config.js
import globals from "globals";
import tseslint from "typescript-eslint";
import eslint from "@eslint/js";
import nextPlugin from "@next/eslint-plugin-next";
import unicornPlugin from "eslint-plugin-unicorn";
import prettierConfig from "eslint-config-prettier";
export default [
eslint.configs.recommended,
...tseslint.configs.recommended,
{
files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"],
plugins: {
"@next/next": nextPlugin,
"unicorn": unicornPlugin,
},
languageOptions: {
globals: {
...globals.browser,
...globals.node,
},
parser: tseslint.parser,
parserOptions: {
project: "./tsconfig.json",
},
},
rules: {
...nextPlugin.configs.recommended.rules,
...nextPlugin.configs["core-web-vitals"].rules,
...unicornPlugin.configs.recommended.rules,
}
},
prettierConfig,
];

View File

@ -56,16 +56,21 @@ export async function parse<T>(res: Response): Promise<T> {
let json: any = null;
try {
json = await res.json();
} catch {}
} catch (err) {
console.error("Erro ao parsear a resposta:", err);
}
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)
export function rangeHeaders(page?: number, limit?: number): Record<string, string> {
if (!page || !limit) return {};

File diff suppressed because it is too large Load Diff

View File

@ -2,6 +2,7 @@
"name": "my-v0-project",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"build": "next build",
"dev": "next dev",
@ -53,7 +54,6 @@
"input-otp": "latest",
"jspdf": "^3.0.3",
"lucide-react": "^0.454.0",
"next": "14.2.16",
"next-themes": "latest",
"react": "^18",
"react-day-picker": "latest",
@ -70,13 +70,22 @@
"zod": "3.25.67"
},
"devDependencies": {
"@eslint/js": "^9.36.0",
"@tailwindcss/postcss": "^4.1.9",
"@types/node": "^22",
"@types/react": "^18",
"@types/react-dom": "^18",
"@typescript-eslint/eslint-plugin": "^8.45.0",
"@typescript-eslint/parser": "^8.45.0",
"eslint": "^9.36.0",
"eslint-config-next": "^15.5.4",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-unicorn": "^61.0.2",
"next": "^15.5.4",
"postcss": "^8.5",
"tailwindcss": "^4.1.9",
"tw-animate-css": "1.3.3",
"typescript": "^5"
"typescript": "^5",
"typescript-eslint": "^8.45.0"
}
}

3115
susconecta/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff