feature/patiente-medical-assignment #45
148
package-lock.json
generated
148
package-lock.json
generated
@ -7,6 +7,7 @@
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^2.2.7",
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"@supabase/supabase-js": "^2.75.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"react-big-calendar": "^1.19.4",
|
||||
"react-signature-canvas": "^1.1.0-alpha.2"
|
||||
@ -222,6 +223,80 @@
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/auth-js": {
|
||||
"version": "2.75.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.75.0.tgz",
|
||||
"integrity": "sha512-J8TkeqCOMCV4KwGKVoxmEBuDdHRwoInML2vJilthOo7awVCro2SM+tOcpljORwuBQ1vHUtV62Leit+5wlxrNtw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@supabase/node-fetch": "2.6.15"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/functions-js": {
|
||||
"version": "2.75.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.75.0.tgz",
|
||||
"integrity": "sha512-18yk07Moj/xtQ28zkqswxDavXC3vbOwt1hDuYM3/7xPnwwpKnsmPyZ7bQ5th4uqiJzQ135t74La9tuaxBR6e7w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@supabase/node-fetch": "2.6.15"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/node-fetch": {
|
||||
"version": "2.6.15",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz",
|
||||
"integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "4.x || >=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/postgrest-js": {
|
||||
"version": "2.75.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.75.0.tgz",
|
||||
"integrity": "sha512-YfBz4W/z7eYCFyuvHhfjOTTzRrQIvsMG2bVwJAKEVVUqGdzqfvyidXssLBG0Fqlql1zJFgtsPpK1n4meHrI7tg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@supabase/node-fetch": "2.6.15"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/realtime-js": {
|
||||
"version": "2.75.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.75.0.tgz",
|
||||
"integrity": "sha512-B4Xxsf2NHd5cEnM6MGswOSPSsZKljkYXpvzKKmNxoUmNQOfB7D8HOa6NwHcUBSlxcjV+vIrYKcYXtavGJqeGrw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@supabase/node-fetch": "2.6.15",
|
||||
"@types/phoenix": "^1.6.6",
|
||||
"@types/ws": "^8.18.1",
|
||||
"ws": "^8.18.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/storage-js": {
|
||||
"version": "2.75.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.75.0.tgz",
|
||||
"integrity": "sha512-wpJMYdfFDckDiHQaTpK+Ib14N/O2o0AAWWhguKvmmMurB6Unx17GGmYp5rrrqCTf8S1qq4IfIxTXxS4hzrUySg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@supabase/node-fetch": "2.6.15"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/supabase-js": {
|
||||
"version": "2.75.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.75.0.tgz",
|
||||
"integrity": "sha512-8UN/vATSgS2JFuJlMVr51L3eUDz+j1m7Ww63wlvHLKULzCDaVWYzvacCjBTLW/lX/vedI2LBI4Vg+01G9ufsJQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@supabase/auth-js": "2.75.0",
|
||||
"@supabase/functions-js": "2.75.0",
|
||||
"@supabase/node-fetch": "2.6.15",
|
||||
"@supabase/postgrest-js": "2.75.0",
|
||||
"@supabase/realtime-js": "2.75.0",
|
||||
"@supabase/storage-js": "2.75.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/helpers": {
|
||||
"version": "0.5.17",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz",
|
||||
@ -258,6 +333,21 @@
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.1.tgz",
|
||||
"integrity": "sha512-CmyhGZanP88uuC5GpWU9q+fI61j2SkhO3UGMUdfYRE6Bcy0ccyzn1Rqj9YAB/ZY4kOXmNf0ocah5GtphmLMP6Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/phoenix": {
|
||||
"version": "1.6.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz",
|
||||
"integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.12.tgz",
|
||||
@ -279,6 +369,15 @@
|
||||
"integrity": "sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
"version": "8.18.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||
@ -576,6 +675,12 @@
|
||||
"integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/trim-canvas": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/trim-canvas/-/trim-canvas-0.1.2.tgz",
|
||||
@ -603,6 +708,12 @@
|
||||
"react": ">=15.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz",
|
||||
"integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/use-sync-external-store": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
|
||||
@ -620,6 +731,43 @@
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tr46": "~0.0.3",
|
||||
"webidl-conversions": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.18.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
||||
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^2.2.7",
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"@supabase/supabase-js": "^2.75.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"react-big-calendar": "^1.19.4",
|
||||
"react-signature-canvas": "^1.1.0-alpha.2"
|
||||
|
||||
2565
susconecta/Documentação API.md
Normal file
2565
susconecta/Documentação API.md
Normal file
File diff suppressed because it is too large
Load Diff
@ -7,12 +7,13 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
||||
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, Users } from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { DoctorRegistrationForm } from "@/components/forms/doctor-registration-form";
|
||||
|
||||
|
||||
import { listarMedicos, excluirMedico, buscarMedicos, buscarMedicoPorId, Medico } from "@/lib/api";
|
||||
import { listarMedicos, excluirMedico, buscarMedicos, buscarMedicoPorId, buscarPacientesPorIds, Medico } from "@/lib/api";
|
||||
import { listAssignmentsForUser } from '@/lib/assignment';
|
||||
|
||||
function normalizeMedico(m: any): Medico {
|
||||
return {
|
||||
@ -64,6 +65,10 @@ export default function DoutoresPage() {
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
const [viewingDoctor, setViewingDoctor] = useState<Medico | null>(null);
|
||||
const [assignedDialogOpen, setAssignedDialogOpen] = useState(false);
|
||||
const [assignedPatients, setAssignedPatients] = useState<any[]>([]);
|
||||
const [assignedLoading, setAssignedLoading] = useState(false);
|
||||
const [assignedDoctor, setAssignedDoctor] = useState<Medico | null>(null);
|
||||
const [searchResults, setSearchResults] = useState<Medico[]>([]);
|
||||
const [searchMode, setSearchMode] = useState(false);
|
||||
const [searchTimeout, setSearchTimeout] = useState<NodeJS.Timeout | null>(null);
|
||||
@ -179,7 +184,7 @@ export default function DoutoresPage() {
|
||||
|
||||
// Handler para o botão de busca
|
||||
function handleClickBuscar() {
|
||||
handleBuscarServidor();
|
||||
void handleBuscarServidor();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@ -253,6 +258,28 @@ export default function DoutoresPage() {
|
||||
setViewingDoctor(doctor);
|
||||
}
|
||||
|
||||
async function handleViewAssignedPatients(doctor: Medico) {
|
||||
setAssignedDoctor(doctor);
|
||||
setAssignedLoading(true);
|
||||
setAssignedPatients([]);
|
||||
try {
|
||||
const assigns = await listAssignmentsForUser(String(doctor.user_id ?? doctor.id));
|
||||
const patientIds = Array.isArray(assigns) ? assigns.map((a:any) => String(a.patient_id)).filter(Boolean) : [];
|
||||
if (patientIds.length) {
|
||||
const patients = await buscarPacientesPorIds(patientIds);
|
||||
setAssignedPatients(patients || []);
|
||||
} else {
|
||||
setAssignedPatients([]);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[DoutoresPage] erro ao carregar pacientes atribuídos:', e);
|
||||
setAssignedPatients([]);
|
||||
} finally {
|
||||
setAssignedLoading(false);
|
||||
setAssignedDialogOpen(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function handleDelete(id: string) {
|
||||
if (!confirm("Excluir este médico?")) return;
|
||||
@ -328,7 +355,7 @@ export default function DoutoresPage() {
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handleBuscarServidor}
|
||||
onClick={() => void handleBuscarServidor()}
|
||||
disabled={loading}
|
||||
className="hover:bg-primary hover:text-white"
|
||||
>
|
||||
@ -399,6 +426,13 @@ export default function DoutoresPage() {
|
||||
<Eye className="mr-2 h-4 w-4" />
|
||||
Ver
|
||||
</DropdownMenuItem>
|
||||
|
||||
{/* Ver pacientes atribuídos ao médico */}
|
||||
<DropdownMenuItem onClick={() => handleViewAssignedPatients(doctor)}>
|
||||
<Users className="mr-2 h-4 w-4" />
|
||||
Ver pacientes atribuídos
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem onClick={() => handleEdit(String(doctor.id))}>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
Editar
|
||||
@ -466,6 +500,36 @@ export default function DoutoresPage() {
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Mostrando {displayedDoctors.length} {searchMode ? 'resultado(s) da busca' : `de ${doctors.length}`}
|
||||
</div>
|
||||
{/* Dialog para pacientes atribuídos */}
|
||||
<Dialog open={assignedDialogOpen} onOpenChange={(open) => { if (!open) { setAssignedDialogOpen(false); setAssignedPatients([]); setAssignedDoctor(null); } }}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Pacientes atribuídos{assignedDoctor ? ` - ${assignedDoctor.full_name}` : ''}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Lista de pacientes atribuídos a este médico.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="py-4">
|
||||
{assignedLoading ? (
|
||||
<div>Carregando pacientes...</div>
|
||||
) : assignedPatients && assignedPatients.length ? (
|
||||
<div className="space-y-2">
|
||||
{assignedPatients.map((p:any) => (
|
||||
<div key={p.id} className="p-2 border rounded">
|
||||
<div className="font-medium">{p.full_name ?? p.nome ?? p.name ?? '(sem nome)'}</div>
|
||||
<div className="text-xs text-muted-foreground">ID: {p.id} {p.cpf ? `• CPF: ${p.cpf}` : ''}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div>Nenhum paciente atribuído encontrado.</div>
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button onClick={() => setAssignedDialogOpen(false)}>Fechar</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -12,6 +12,7 @@ import { MoreHorizontal, Plus, Search, Eye, Edit, Trash2, ArrowLeft } from "luci
|
||||
|
||||
import { Paciente, Endereco, listarPacientes, buscarPacientes, buscarPacientePorId, excluirPaciente } from "@/lib/api";
|
||||
import { PatientRegistrationForm } from "@/components/forms/patient-registration-form";
|
||||
import AssignmentForm from "@/components/admin/AssignmentForm";
|
||||
|
||||
|
||||
function normalizePaciente(p: any): Paciente {
|
||||
@ -46,6 +47,8 @@ export default function PacientesPage() {
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
const [viewingPatient, setViewingPatient] = useState<Paciente | null>(null);
|
||||
const [assignDialogOpen, setAssignDialogOpen] = useState(false);
|
||||
const [assignPatientId, setAssignPatientId] = useState<string | null>(null);
|
||||
|
||||
async function loadAll() {
|
||||
try {
|
||||
@ -204,7 +207,7 @@ export default function PacientesPage() {
|
||||
onKeyDown={(e) => e.key === "Enter" && handleBuscarServidor()}
|
||||
/>
|
||||
</div>
|
||||
<Button variant="secondary" onClick={handleBuscarServidor} className="hover:bg-primary hover:text-white">Buscar</Button>
|
||||
<Button variant="secondary" onClick={() => void handleBuscarServidor()} className="hover:bg-primary hover:text-white">Buscar</Button>
|
||||
<Button onClick={handleAdd}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Novo paciente
|
||||
@ -254,6 +257,10 @@ export default function PacientesPage() {
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Excluir
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => { setAssignPatientId(String(p.id)); setAssignDialogOpen(true); }}>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
Atribuir profissional
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
@ -310,6 +317,16 @@ export default function PacientesPage() {
|
||||
</Dialog>
|
||||
)}
|
||||
|
||||
{/* Assignment dialog */}
|
||||
{assignDialogOpen && assignPatientId && (
|
||||
<AssignmentForm
|
||||
patientId={assignPatientId}
|
||||
open={assignDialogOpen}
|
||||
onClose={() => { setAssignDialogOpen(false); setAssignPatientId(null); }}
|
||||
onSaved={() => { setAssignDialogOpen(false); setAssignPatientId(null); loadAll(); }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="text-sm text-muted-foreground">Mostrando {filtered.length} de {patients.length}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
import type { UserType } from '@/types/auth'
|
||||
@ -17,8 +17,12 @@ export default function ProtectedRoute({
|
||||
const { authStatus, user } = useAuth()
|
||||
const router = useRouter()
|
||||
const isRedirecting = useRef(false)
|
||||
const [mounted, setMounted] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
// marca que o componente já montou no cliente
|
||||
setMounted(true)
|
||||
|
||||
// Evitar múltiplos redirects
|
||||
if (isRedirecting.current) return
|
||||
|
||||
@ -85,6 +89,9 @@ export default function ProtectedRoute({
|
||||
|
||||
// Durante loading, mostrar spinner
|
||||
if (authStatus === 'loading') {
|
||||
// evitar render no servidor para não causar mismatch de hidratação
|
||||
if (!mounted) return null
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
@ -97,6 +104,9 @@ export default function ProtectedRoute({
|
||||
|
||||
// Se não autenticado ou redirecionando, mostrar spinner
|
||||
if (authStatus === 'unauthenticated' || isRedirecting.current) {
|
||||
// evitar render no servidor para não causar mismatch de hidratação
|
||||
if (!mounted) return null
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
|
||||
113
susconecta/components/admin/AssignmentForm.tsx
Normal file
113
susconecta/components/admin/AssignmentForm.tsx
Normal file
@ -0,0 +1,113 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
|
||||
import { assignRoleToUser, listAssignmentsForPatient, PatientAssignmentRole } from "@/lib/assignment";
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import { listarProfissionais } from "@/lib/api";
|
||||
|
||||
type Props = {
|
||||
patientId: string;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSaved?: () => void;
|
||||
};
|
||||
|
||||
export default function AssignmentForm({ patientId, open, onClose, onSaved }: Props) {
|
||||
const { toast } = useToast();
|
||||
const { user } = useAuth();
|
||||
const [professionals, setProfessionals] = useState<any[]>([]);
|
||||
const [selectedProfessional, setSelectedProfessional] = useState<string | null>(null);
|
||||
// default to Portuguese role values expected by the backend
|
||||
const [role, setRole] = useState<PatientAssignmentRole>("medico");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [existing, setExisting] = useState<any[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
async function load() {
|
||||
try {
|
||||
const pros = await listarProfissionais();
|
||||
setProfessionals(pros || []);
|
||||
} catch (e) {
|
||||
console.warn('Erro ao carregar profissionais', e);
|
||||
setProfessionals([]);
|
||||
}
|
||||
|
||||
try {
|
||||
const a = await listAssignmentsForPatient(patientId);
|
||||
setExisting(a || []);
|
||||
} catch (e) {
|
||||
setExisting([]);
|
||||
}
|
||||
}
|
||||
|
||||
if (open) load();
|
||||
}, [open, patientId]);
|
||||
|
||||
async function handleSave() {
|
||||
if (!selectedProfessional) return toast({ title: 'Selecione um profissional', variant: 'default' });
|
||||
setLoading(true);
|
||||
try {
|
||||
await assignRoleToUser({ patient_id: patientId, user_id: String(selectedProfessional), role, created_by: user?.id ?? null });
|
||||
toast({ title: 'Atribuição criada', variant: 'default' });
|
||||
onSaved && onSaved();
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
toast({ title: 'Erro ao criar atribuição', description: err?.message });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(v) => { if (!v) onClose(); }}>
|
||||
<DialogContent className="max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Atribuir profissional ao paciente</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid gap-4 py-4">
|
||||
<div>
|
||||
<Label>Profissional</Label>
|
||||
<Select onValueChange={(v) => setSelectedProfessional(v)}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Selecione um profissional" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{professionals.map((p) => (
|
||||
// prefer the auth user id (p.user_id) when available; fallback to p.id
|
||||
<SelectItem key={p.id} value={String(p.user_id ?? p.id)}>{p.full_name || p.name || p.email || p.user_id || p.id}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* role input removed - only professional select remains; role defaults to 'medico' on submit */}
|
||||
|
||||
{existing && existing.length > 0 && (
|
||||
<div>
|
||||
<Label>Atribuições existentes</Label>
|
||||
<ul className="pl-4 list-disc text-sm text-muted-foreground">
|
||||
{existing.map((it) => (
|
||||
<li key={it.id}>{it.user_id} — {it.role}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="ghost" onClick={onClose}>Cancelar</Button>
|
||||
<Button onClick={handleSave} disabled={loading}>{loading ? 'Salvando...' : 'Salvar'}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@ -65,13 +65,6 @@ export function CredentialsDialog({
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<Alert className="bg-blue-50 border-blue-200">
|
||||
<AlertDescription className="text-blue-900">
|
||||
<strong>📧 Confirme o email:</strong> Um email de confirmação foi enviado para <strong>{email}</strong>.
|
||||
O {userType} deve clicar no link de confirmação antes de fazer o primeiro login.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email de Acesso</Label>
|
||||
@ -129,30 +122,6 @@ export function CredentialsDialog({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-md p-3 text-sm text-blue-900">
|
||||
<strong>Próximos passos:</strong>
|
||||
<ol className="list-decimal list-inside mt-2 space-y-1">
|
||||
<li>Compartilhe estas credenciais com o {userType}</li>
|
||||
<li>
|
||||
<strong className="text-blue-700">O {userType} deve confirmar o email</strong> clicando no link enviado para{" "}
|
||||
<strong>{email}</strong> (verifique também a pasta de spam)
|
||||
</li>
|
||||
<li>
|
||||
Após confirmar o email, o {userType} deve acessar:{" "}
|
||||
<code className="bg-blue-100 px-1 py-0.5 rounded text-xs font-mono">
|
||||
{userType === "médico" ? "/login" : "/login-paciente"}
|
||||
</code>
|
||||
</li>
|
||||
<li>
|
||||
Após o login, terá acesso à área:{" "}
|
||||
<code className="bg-blue-100 px-1 py-0.5 rounded text-xs font-mono">
|
||||
{userType === "médico" ? "/profissional" : "/paciente"}
|
||||
</code>
|
||||
</li>
|
||||
<li>Recomende trocar a senha no primeiro acesso</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="flex-col sm:flex-row gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
|
||||
@ -22,10 +22,10 @@ import {
|
||||
listarAnexosMedico,
|
||||
adicionarAnexoMedico,
|
||||
removerAnexoMedico,
|
||||
MedicoInput, // 👈 importado do lib/api
|
||||
Medico, // 👈 adicionado import do tipo Medico
|
||||
MedicoInput,
|
||||
Medico,
|
||||
criarUsuarioMedico,
|
||||
CreateUserWithPasswordResponse,
|
||||
gerarSenhaAleatoria,
|
||||
} from "@/lib/api";
|
||||
;
|
||||
|
||||
@ -155,9 +155,13 @@ export function DoctorRegistrationForm({
|
||||
const [serverAnexos, setServerAnexos] = useState<any[]>([]);
|
||||
|
||||
// Estados para o dialog de credenciais
|
||||
const [showCredentials, setShowCredentials] = useState(false);
|
||||
const [credentials, setCredentials] = useState<CreateUserWithPasswordResponse | null>(null);
|
||||
const [savedDoctor, setSavedDoctor] = useState<Medico | null>(null);
|
||||
const [showCredentialsDialog, setShowCredentialsDialog] = useState(false);
|
||||
const [credentials, setCredentials] = useState<{
|
||||
email: string;
|
||||
password: string;
|
||||
userName: string;
|
||||
userType: 'médico' | 'paciente';
|
||||
} | null>(null);
|
||||
|
||||
const title = useMemo(() => (mode === "create" ? "Cadastro de Médico" : "Editar Médico"), [mode]);
|
||||
|
||||
@ -169,6 +173,10 @@ export function DoctorRegistrationForm({
|
||||
console.log("[DoctorForm] Carregando médico ID:", doctorId);
|
||||
const medico = await buscarMedicoPorId(String(doctorId));
|
||||
console.log("[DoctorForm] Dados recebidos do API:", medico);
|
||||
if (!medico) {
|
||||
console.warn('[DoctorForm] Médico não encontrado para ID:', doctorId);
|
||||
return;
|
||||
}
|
||||
console.log("[DoctorForm] Campos principais:", {
|
||||
full_name: medico.full_name,
|
||||
crm: medico.crm,
|
||||
@ -337,21 +345,21 @@ function setField<T extends keyof FormData>(k: T, v: FormData[T]) {
|
||||
return Object.keys(e).length === 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function handleSubmit(ev: React.FormEvent) {
|
||||
ev.preventDefault();
|
||||
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.
|
||||
function toPayload(): MedicoInput {
|
||||
// Converte dd/MM/yyyy para ISO (yyyy-MM-dd) se possível
|
||||
let isoDate: string | null = null;
|
||||
try {
|
||||
const parts = String(form.data_nascimento).split(/\D+/).filter(Boolean);
|
||||
if (parts.length === 3) {
|
||||
const [d, m, y] = parts;
|
||||
const date = new Date(Number(y), Number(m) - 1, Number(d));
|
||||
if (!isNaN(date.getTime())) {
|
||||
isoDate = date.toISOString().slice(0, 10);
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
setSubmitting(true);
|
||||
setErrors((e) => ({ ...e, submit: "" }));
|
||||
|
||||
const payload: MedicoInput = {
|
||||
return {
|
||||
user_id: null,
|
||||
crm: form.crm || "",
|
||||
crm_uf: form.estado_crm || "",
|
||||
@ -368,119 +376,137 @@ const payload: MedicoInput = {
|
||||
neighborhood: form.bairro || undefined,
|
||||
city: form.cidade || "",
|
||||
state: form.estado || "",
|
||||
// converte dd/MM/yyyy para ISO
|
||||
birth_date: (() => {
|
||||
try {
|
||||
const parts = String(form.data_nascimento).split(/\D+/).filter(Boolean);
|
||||
if (parts.length === 3) {
|
||||
const [d, m, y] = parts;
|
||||
const date = new Date(Number(y), Number(m) - 1, Number(d));
|
||||
if (!isNaN(date.getTime())) return date.toISOString().slice(0, 10);
|
||||
}
|
||||
} catch {}
|
||||
return null;
|
||||
})(),
|
||||
birth_date: isoDate,
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
async function handleSubmit(ev: React.FormEvent) {
|
||||
ev.preventDefault();
|
||||
if (!validateLocal()) return;
|
||||
|
||||
console.log("📤 Payload being sent:", payload);
|
||||
console.log("🔧 Mode:", mode, "DoctorId:", doctorId);
|
||||
setSubmitting(true);
|
||||
setErrors({});
|
||||
try {
|
||||
if (mode === "edit") {
|
||||
if (!doctorId) throw new Error("ID do médico não fornecido para edição");
|
||||
const payload = toPayload();
|
||||
const saved = await atualizarMedico(String(doctorId), payload);
|
||||
onSaved?.(saved);
|
||||
alert("Médico atualizado com sucesso!");
|
||||
if (inline) onClose?.();
|
||||
else onOpenChange?.(false);
|
||||
|
||||
} else {
|
||||
// --- FLUXO DE CRIAÇÃO DE MÉDICO ---
|
||||
console.log('🏥 [CRIAR MÉDICO] Iniciando processo completo...');
|
||||
|
||||
const medicoPayload = toPayload();
|
||||
console.log("Enviando os dados para a API:", medicoPayload);
|
||||
|
||||
// 1. Cria o perfil do médico na tabela doctors
|
||||
const savedDoctorProfile = await criarMedico(medicoPayload);
|
||||
console.log("✅ Perfil do médico criado:", savedDoctorProfile);
|
||||
|
||||
// 2. Cria usuário no Supabase Auth (direto via /auth/v1/signup)
|
||||
console.log('🔐 Criando usuário de autenticação...');
|
||||
|
||||
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(String(doctorId), payload);
|
||||
|
||||
console.log("✅ Médico salvo com sucesso:", saved);
|
||||
|
||||
// Se for criação de novo médico e tiver email válido, cria usuário
|
||||
if (mode === "create" && form.email && form.email.includes('@')) {
|
||||
console.log("🔐 Iniciando criação de usuário para o médico...");
|
||||
console.log("📧 Email:", form.email);
|
||||
console.log("👤 Nome:", form.full_name);
|
||||
console.log("📱 Telefone:", form.celular);
|
||||
|
||||
try {
|
||||
const userCredentials = await criarUsuarioMedico({
|
||||
const authResponse = await criarUsuarioMedico({
|
||||
email: form.email,
|
||||
full_name: form.full_name,
|
||||
phone_mobile: form.celular,
|
||||
phone_mobile: form.celular || '',
|
||||
});
|
||||
|
||||
console.log("✅ Usuário criado com sucesso!", userCredentials);
|
||||
console.log("🔑 Senha gerada:", userCredentials.password);
|
||||
if (authResponse.success && authResponse.user) {
|
||||
console.log('✅ Usuário Auth criado:', authResponse.user.id);
|
||||
|
||||
// Armazena as credenciais e mostra o dialog
|
||||
setCredentials(userCredentials);
|
||||
setShowCredentials(true);
|
||||
setSavedDoctor(saved); // Salva médico para chamar onSaved depois
|
||||
|
||||
console.log("📋 Credenciais definidas, dialog deve aparecer!");
|
||||
|
||||
// NÃO chama onSaved aqui! Isso fecha o formulário.
|
||||
// O dialog vai chamar onSaved quando o usuário fechar
|
||||
setSubmitting(false);
|
||||
return; // ← IMPORTANTE: Impede que o código abaixo seja executado
|
||||
|
||||
} catch (userError: any) {
|
||||
console.error("❌ ERRO ao criar usuário:", userError);
|
||||
console.error("📋 Stack trace:", userError?.stack);
|
||||
const errorMessage = userError?.message || "Erro desconhecido";
|
||||
console.error("💬 Mensagem:", errorMessage);
|
||||
|
||||
// Mostra erro mas fecha o formulário normalmente
|
||||
alert(`Médico cadastrado com sucesso!\n\n⚠️ Porém, houve erro ao criar usuário de acesso:\n${errorMessage}\n\nVerifique os logs do console (F12) para mais detalhes.`);
|
||||
|
||||
// Fecha o formulário mesmo com erro na criação de usuário
|
||||
setForm(initial);
|
||||
setPhotoPreview(null);
|
||||
setServerAnexos([]);
|
||||
onSaved?.(saved);
|
||||
if (inline) onClose?.();
|
||||
else onOpenChange?.(false);
|
||||
setSubmitting(false);
|
||||
return;
|
||||
// Attempt to link the created auth user id to the doctors record
|
||||
try {
|
||||
// savedDoctorProfile may be an array or object depending on API
|
||||
const docId = (savedDoctorProfile && (savedDoctorProfile.id || (Array.isArray(savedDoctorProfile) ? savedDoctorProfile[0]?.id : undefined))) || null;
|
||||
if (docId) {
|
||||
console.log('[DoctorForm] Vinculando user_id ao médico:', { doctorId: docId, userId: authResponse.user.id });
|
||||
// dynamic import to avoid circular deps in some bundlers
|
||||
const api = await import('@/lib/api');
|
||||
if (api && typeof api.vincularUserIdMedico === 'function') {
|
||||
await api.vincularUserIdMedico(String(docId), String(authResponse.user.id));
|
||||
console.log('[DoctorForm] user_id vinculado com sucesso.');
|
||||
}
|
||||
} else {
|
||||
console.log("⚠️ Não criará usuário. Motivo:");
|
||||
console.log(" - Mode:", mode);
|
||||
console.log(" - Email:", form.email);
|
||||
console.log(" - Tem @:", form.email?.includes('@'));
|
||||
console.warn('[DoctorForm] Não foi possível determinar o ID do médico para vincular user_id. Doctor profile:', savedDoctorProfile);
|
||||
}
|
||||
} catch (linkErr) {
|
||||
console.warn('[DoctorForm] Falha ao vincular user_id ao médico:', linkErr);
|
||||
}
|
||||
|
||||
// Se não for criar usuário, fecha normalmente
|
||||
// 3. Exibe popup com credenciais
|
||||
setCredentials({
|
||||
email: authResponse.email,
|
||||
password: authResponse.password,
|
||||
userName: form.full_name,
|
||||
userType: 'médico',
|
||||
});
|
||||
setShowCredentialsDialog(true);
|
||||
|
||||
// 4. Limpa formulário
|
||||
setForm(initial);
|
||||
setPhotoPreview(null);
|
||||
setServerAnexos([]);
|
||||
onSaved?.(saved);
|
||||
|
||||
// 5. Notifica componente pai
|
||||
onSaved?.(savedDoctorProfile);
|
||||
} else {
|
||||
throw new Error('Falha ao criar usuário de autenticação');
|
||||
}
|
||||
|
||||
} catch (authError: any) {
|
||||
console.error('❌ Erro ao criar usuário Auth:', authError);
|
||||
|
||||
const errorMsg = authError?.message || String(authError);
|
||||
|
||||
// Mensagens específicas de erro
|
||||
if (errorMsg.toLowerCase().includes('already registered') ||
|
||||
errorMsg.toLowerCase().includes('already been registered') ||
|
||||
errorMsg.toLowerCase().includes('já está cadastrado')) {
|
||||
alert(
|
||||
`⚠️ EMAIL JÁ CADASTRADO\n\n` +
|
||||
`O email "${form.email}" já possui uma conta no sistema.\n\n` +
|
||||
`✅ O perfil do médico "${form.full_name}" foi salvo com sucesso.\n\n` +
|
||||
`❌ Porém, não foi possível criar o login porque este email já está em uso.\n\n` +
|
||||
`SOLUÇÃO:\n` +
|
||||
`• Use um email diferente para este médico, OU\n` +
|
||||
`• Se o médico já tem conta, edite o perfil e vincule ao usuário existente`
|
||||
);
|
||||
} else {
|
||||
alert(
|
||||
`⚠️ Médico cadastrado com sucesso, mas houve um problema ao criar o acesso ao sistema.\n\n` +
|
||||
`✅ Perfil do médico salvo: ${form.full_name}\n\n` +
|
||||
`❌ Erro ao criar login: ${errorMsg}\n\n` +
|
||||
`Por favor, entre em contato com o administrador para criar o acesso manualmente.`
|
||||
);
|
||||
}
|
||||
|
||||
// Limpa formulário mesmo com erro
|
||||
setForm(initial);
|
||||
setPhotoPreview(null);
|
||||
setServerAnexos([]);
|
||||
onSaved?.(savedDoctorProfile);
|
||||
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" }));
|
||||
console.error("❌ Erro no handleSubmit:", err);
|
||||
// Exibe mensagem amigável ao usuário
|
||||
const userMessage = err?.message?.includes("toPayload")
|
||||
? "Erro ao processar os dados do formulário. Por favor, verifique os campos e tente novamente."
|
||||
: err?.message || "Erro ao salvar médico. Por favor, tente novamente.";
|
||||
setErrors({ submit: userMessage });
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
@ -1038,32 +1064,23 @@ if (missingFields.length > 0) {
|
||||
{/* Dialog de credenciais */}
|
||||
{credentials && (
|
||||
<CredentialsDialog
|
||||
open={showCredentials}
|
||||
open={showCredentialsDialog}
|
||||
onOpenChange={(open) => {
|
||||
console.log("🔄 CredentialsDialog (inline) onOpenChange:", open);
|
||||
setShowCredentials(open);
|
||||
setShowCredentialsDialog(open);
|
||||
if (!open) {
|
||||
console.log("🔄 Dialog fechando - chamando onSaved e limpando formulário");
|
||||
|
||||
// Chama onSaved com o médico salvo
|
||||
if (savedDoctor) {
|
||||
console.log("✅ Chamando onSaved com médico:", savedDoctor.id);
|
||||
onSaved?.(savedDoctor);
|
||||
}
|
||||
|
||||
// Limpa o formulário e fecha
|
||||
setForm(initial);
|
||||
setPhotoPreview(null);
|
||||
setServerAnexos([]);
|
||||
// Quando o dialog de credenciais fecha, fecha o formulário também
|
||||
setCredentials(null);
|
||||
setSavedDoctor(null);
|
||||
if (inline) {
|
||||
onClose?.();
|
||||
} else {
|
||||
onOpenChange?.(false);
|
||||
}
|
||||
}
|
||||
}}
|
||||
email={credentials.email}
|
||||
password={credentials.password}
|
||||
userName={form.full_name}
|
||||
userType="médico"
|
||||
userName={credentials.userName}
|
||||
userType={credentials.userType}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
@ -1086,32 +1103,18 @@ if (missingFields.length > 0) {
|
||||
{/* Dialog de credenciais */}
|
||||
{credentials && (
|
||||
<CredentialsDialog
|
||||
open={showCredentials}
|
||||
open={showCredentialsDialog}
|
||||
onOpenChange={(open) => {
|
||||
console.log("🔄 CredentialsDialog (dialog mode) onOpenChange:", open);
|
||||
setShowCredentials(open);
|
||||
setShowCredentialsDialog(open);
|
||||
if (!open) {
|
||||
console.log("🔄 Dialog fechando - chamando onSaved e fechando modal principal");
|
||||
|
||||
// Chama onSaved com o médico salvo
|
||||
if (savedDoctor) {
|
||||
console.log("✅ Chamando onSaved com médico:", savedDoctor.id);
|
||||
onSaved?.(savedDoctor);
|
||||
}
|
||||
|
||||
// Limpa o formulário e fecha o modal principal
|
||||
setForm(initial);
|
||||
setPhotoPreview(null);
|
||||
setServerAnexos([]);
|
||||
setCredentials(null);
|
||||
setSavedDoctor(null);
|
||||
onOpenChange?.(false);
|
||||
}
|
||||
}}
|
||||
email={credentials.email}
|
||||
password={credentials.password}
|
||||
userName={form.full_name}
|
||||
userType="médico"
|
||||
userName={credentials.userName}
|
||||
userType={credentials.userType}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
@ -18,7 +17,6 @@ import {
|
||||
Paciente,
|
||||
PacienteInput,
|
||||
buscarCepAPI,
|
||||
criarPaciente,
|
||||
atualizarPaciente,
|
||||
uploadFotoPaciente,
|
||||
removerFotoPaciente,
|
||||
@ -27,15 +25,13 @@ import {
|
||||
removerAnexo,
|
||||
buscarPacientePorId,
|
||||
criarUsuarioPaciente,
|
||||
CreateUserWithPasswordResponse,
|
||||
criarPaciente,
|
||||
} from "@/lib/api";
|
||||
|
||||
import { validarCPFLocal } from "@/lib/utils";
|
||||
import { verificarCpfDuplicado } from "@/lib/api";
|
||||
import { CredentialsDialog } from "@/components/credentials-dialog";
|
||||
|
||||
|
||||
|
||||
type Mode = "create" | "edit";
|
||||
|
||||
export interface PatientRegistrationFormProps {
|
||||
@ -55,7 +51,7 @@ type FormData = {
|
||||
cpf: string;
|
||||
rg: string;
|
||||
sexo: string;
|
||||
birth_date: string; // 👈 corrigido
|
||||
birth_date: string;
|
||||
email: string;
|
||||
telefone: string;
|
||||
cep: string;
|
||||
@ -76,7 +72,7 @@ const initial: FormData = {
|
||||
cpf: "",
|
||||
rg: "",
|
||||
sexo: "",
|
||||
birth_date: "", // 👈 corrigido
|
||||
birth_date: "",
|
||||
email: "",
|
||||
telefone: "",
|
||||
cep: "",
|
||||
@ -90,8 +86,6 @@ const initial: FormData = {
|
||||
anexos: [],
|
||||
};
|
||||
|
||||
|
||||
|
||||
export function PatientRegistrationForm({
|
||||
open = true,
|
||||
onOpenChange,
|
||||
@ -110,13 +104,16 @@ export function PatientRegistrationForm({
|
||||
const [serverAnexos, setServerAnexos] = useState<any[]>([]);
|
||||
|
||||
// Estados para o dialog de credenciais
|
||||
const [showCredentials, setShowCredentials] = useState(false);
|
||||
const [credentials, setCredentials] = useState<CreateUserWithPasswordResponse | null>(null);
|
||||
const [savedPatient, setSavedPatient] = useState<Paciente | null>(null);
|
||||
const [showCredentialsDialog, setShowCredentialsDialog] = useState(false);
|
||||
const [credentials, setCredentials] = useState<{
|
||||
email: string;
|
||||
password: string;
|
||||
userName: string;
|
||||
userType: 'médico' | 'paciente';
|
||||
} | null>(null);
|
||||
|
||||
const title = useMemo(() => (mode === "create" ? "Cadastro de Paciente" : "Editar Paciente"), [mode]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
async function load() {
|
||||
if (mode !== "edit" || patientId == null) return;
|
||||
@ -126,7 +123,7 @@ export function PatientRegistrationForm({
|
||||
console.log("[PatientForm] Dados recebidos:", p);
|
||||
setForm((s) => ({
|
||||
...s,
|
||||
nome: p.full_name || "", // 👈 trocar nome → full_name
|
||||
nome: p.full_name || "",
|
||||
nome_social: p.social_name || "",
|
||||
cpf: p.cpf || "",
|
||||
rg: p.rg || "",
|
||||
@ -197,12 +194,12 @@ export function PatientRegistrationForm({
|
||||
const e: Record<string, string> = {};
|
||||
if (!form.nome.trim()) e.nome = "Nome é obrigatório";
|
||||
if (!form.cpf.trim()) e.cpf = "CPF é obrigatório";
|
||||
if (mode === 'create' && !form.email.trim()) e.email = "Email é obrigatório para criar um usuário";
|
||||
setErrors(e);
|
||||
return Object.keys(e).length === 0;
|
||||
}
|
||||
|
||||
function toPayload(): PacienteInput {
|
||||
// converte dd/MM/yyyy para ISO (yyyy-MM-dd) se possível
|
||||
let isoDate: string | null = null;
|
||||
try {
|
||||
const parts = String(form.birth_date).split(/\D+/).filter(Boolean);
|
||||
@ -216,12 +213,12 @@ export function PatientRegistrationForm({
|
||||
} catch {}
|
||||
|
||||
return {
|
||||
full_name: form.nome, // 👈 troca 'nome' por 'full_name'
|
||||
full_name: form.nome,
|
||||
social_name: form.nome_social || null,
|
||||
cpf: form.cpf,
|
||||
rg: form.rg || null,
|
||||
sex: form.sexo || null,
|
||||
birth_date: isoDate, // enviar ISO ou null
|
||||
birth_date: isoDate,
|
||||
phone_mobile: form.telefone || null,
|
||||
email: form.email || null,
|
||||
cep: form.cep || null,
|
||||
@ -235,21 +232,15 @@ export function PatientRegistrationForm({
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function handleSubmit(ev: React.FormEvent) {
|
||||
ev.preventDefault();
|
||||
if (!validateLocal()) return;
|
||||
|
||||
|
||||
try {
|
||||
// 1) validação local
|
||||
if (!validarCPFLocal(form.cpf)) {
|
||||
setErrors((e) => ({ ...e, cpf: "CPF inválido" }));
|
||||
return;
|
||||
}
|
||||
|
||||
// 2) checar duplicidade no banco (apenas se criando novo paciente)
|
||||
if (mode === "create") {
|
||||
const existe = await verificarCpfDuplicado(form.cpf);
|
||||
if (existe) {
|
||||
@ -259,119 +250,127 @@ export function PatientRegistrationForm({
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Erro ao validar CPF", err);
|
||||
setErrors({ submit: "Erro ao validar CPF." });
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const payload = toPayload();
|
||||
|
||||
let saved: Paciente;
|
||||
if (mode === "create") {
|
||||
saved = await criarPaciente(payload);
|
||||
} else {
|
||||
if (mode === "edit") {
|
||||
if (patientId == null) throw new Error("Paciente inexistente para edição");
|
||||
saved = await atualizarPaciente(String(patientId), payload);
|
||||
}
|
||||
const payload = toPayload();
|
||||
const saved = await atualizarPaciente(String(patientId), payload);
|
||||
onSaved?.(saved);
|
||||
alert("Paciente atualizado com sucesso!");
|
||||
|
||||
if (form.photo && saved?.id) {
|
||||
setForm(initial);
|
||||
setPhotoPreview(null);
|
||||
setServerAnexos([]);
|
||||
if (inline) onClose?.();
|
||||
else onOpenChange?.(false);
|
||||
|
||||
} else {
|
||||
// --- NOVA LÓGICA DE CRIAÇÃO ---
|
||||
const patientPayload = toPayload();
|
||||
const savedPatientProfile = await criarPaciente(patientPayload);
|
||||
console.log(" Perfil do paciente criado:", savedPatientProfile);
|
||||
|
||||
if (form.email && form.email.includes('@')) {
|
||||
console.log(" Criando usuário de autenticação (paciente)...");
|
||||
try {
|
||||
await uploadFotoPaciente(saved.id, form.photo);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (form.anexos.length && saved?.id) {
|
||||
for (const f of form.anexos) {
|
||||
try {
|
||||
await adicionarAnexo(saved.id, f);
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
// Se for criação de novo paciente e tiver email válido, cria usuário
|
||||
if (mode === "create" && form.email && form.email.includes('@')) {
|
||||
console.log("🔐 Iniciando criação de usuário para o paciente...");
|
||||
console.log("📧 Email:", form.email);
|
||||
console.log("👤 Nome:", form.nome);
|
||||
console.log("📱 Telefone:", form.telefone);
|
||||
|
||||
try {
|
||||
const userCredentials = await criarUsuarioPaciente({
|
||||
const userResponse = await criarUsuarioPaciente({
|
||||
email: form.email,
|
||||
full_name: form.nome,
|
||||
phone_mobile: form.telefone,
|
||||
});
|
||||
|
||||
console.log("✅ Usuário criado com sucesso!", userCredentials);
|
||||
console.log("🔑 Senha gerada:", userCredentials.password);
|
||||
if (userResponse.success && userResponse.user) {
|
||||
console.log(" Usuário de autenticação criado:", userResponse.user);
|
||||
|
||||
// Armazena as credenciais e mostra o dialog
|
||||
console.log("📋 Antes de setCredentials - credentials atual:", credentials);
|
||||
console.log("📋 Antes de setShowCredentials - showCredentials atual:", showCredentials);
|
||||
// Mostra credenciais no dialog usando as credenciais retornadas
|
||||
setCredentials({
|
||||
email: userResponse.email ?? form.email,
|
||||
password: userResponse.password ?? '',
|
||||
userName: form.nome,
|
||||
userType: 'paciente',
|
||||
});
|
||||
setShowCredentialsDialog(true);
|
||||
|
||||
setCredentials(userCredentials);
|
||||
setShowCredentials(true);
|
||||
// Tenta vincular o user_id ao perfil do paciente recém-criado
|
||||
try {
|
||||
const apiMod = await import('@/lib/api');
|
||||
const pacienteId = savedPatientProfile?.id || (savedPatientProfile && (savedPatientProfile as any).id);
|
||||
const userId = (userResponse.user as any)?.id || (userResponse.user as any)?.user_id || (userResponse.user as any)?.id;
|
||||
if (pacienteId && userId && typeof apiMod.vincularUserIdPaciente === 'function') {
|
||||
console.log('[PatientForm] Vinculando user_id ao paciente:', pacienteId, userId);
|
||||
try {
|
||||
await apiMod.vincularUserIdPaciente(pacienteId, String(userId));
|
||||
console.log('[PatientForm] user_id vinculado com sucesso ao paciente');
|
||||
} catch (linkErr) {
|
||||
console.warn('[PatientForm] Falha ao vincular user_id ao paciente:', linkErr);
|
||||
}
|
||||
}
|
||||
} catch (dynErr) {
|
||||
console.warn('[PatientForm] Não foi possível importar helper para vincular user_id:', dynErr);
|
||||
}
|
||||
|
||||
console.log("📋 Depois de set - credentials:", userCredentials);
|
||||
console.log("📋 Depois de set - showCredentials: true");
|
||||
console.log("📋 Modo inline?", inline);
|
||||
console.log("📋 userCredentials completo:", JSON.stringify(userCredentials));
|
||||
|
||||
// Força re-render
|
||||
setTimeout(() => {
|
||||
console.log("⏰ Timeout - credentials:", credentials);
|
||||
console.log("⏰ Timeout - showCredentials:", showCredentials);
|
||||
}, 100);
|
||||
|
||||
console.log("📋 Credenciais definidas, dialog deve aparecer!");
|
||||
|
||||
// Salva o paciente para chamar onSaved depois
|
||||
setSavedPatient(saved);
|
||||
|
||||
// ⚠️ NÃO chama onSaved aqui! O dialog vai chamar quando fechar.
|
||||
// Se chamar agora, o formulário fecha e o dialog desaparece.
|
||||
console.log("⚠️ NÃO chamando onSaved ainda - aguardando dialog fechar");
|
||||
|
||||
// RETORNA AQUI para não executar o código abaixo
|
||||
return;
|
||||
|
||||
} catch (userError: any) {
|
||||
console.error("❌ ERRO ao criar usuário:", userError);
|
||||
console.error("📋 Stack trace:", userError?.stack);
|
||||
const errorMessage = userError?.message || "Erro desconhecido";
|
||||
console.error("<22> Mensagem:", errorMessage);
|
||||
|
||||
// Mostra erro mas fecha o formulário normalmente
|
||||
alert(`Paciente cadastrado com sucesso!\n\n⚠️ Porém, houve erro ao criar usuário de acesso:\n${errorMessage}\n\nVerifique os logs do console (F12) para mais detalhes.`);
|
||||
|
||||
// Fecha o formulário mesmo com erro na criação de usuário
|
||||
// Limpa formulário mas NÃO fecha ainda - fechará quando o dialog de credenciais fechar
|
||||
setForm(initial);
|
||||
setPhotoPreview(null);
|
||||
setServerAnexos([]);
|
||||
onSaved?.(savedPatientProfile);
|
||||
return;
|
||||
} else {
|
||||
throw new Error((userResponse as any).message || "Falhou ao criar o usuário de acesso.");
|
||||
}
|
||||
} catch (userError: any) {
|
||||
console.error(" Erro ao criar usuário via signup:", userError);
|
||||
|
||||
// Mensagem de erro específica para email duplicado
|
||||
const errorMsg = userError?.message || String(userError);
|
||||
|
||||
if (errorMsg.toLowerCase().includes('already registered') ||
|
||||
errorMsg.toLowerCase().includes('já está cadastrado') ||
|
||||
errorMsg.toLowerCase().includes('já existe')) {
|
||||
alert(
|
||||
` Este email já está cadastrado no sistema.\n\n` +
|
||||
` O perfil do paciente foi salvo com sucesso.\n\n` +
|
||||
`Para criar acesso ao sistema, use um email diferente ou recupere a senha do email existente.`
|
||||
);
|
||||
} else {
|
||||
alert(
|
||||
` Paciente cadastrado com sucesso!\n\n` +
|
||||
` Porém houve um problema ao criar o acesso:\n${errorMsg}\n\n` +
|
||||
`O cadastro do paciente foi salvo, mas será necessário criar o acesso manualmente.`
|
||||
);
|
||||
}
|
||||
|
||||
// Limpa formulário e fecha
|
||||
setForm(initial);
|
||||
setPhotoPreview(null);
|
||||
setServerAnexos([]);
|
||||
onSaved?.(savedPatientProfile);
|
||||
if (inline) onClose?.();
|
||||
else onOpenChange?.(false);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
console.log("⚠️ Não criará usuário. Motivo:");
|
||||
console.log(" - Mode:", mode);
|
||||
console.log(" - Email:", form.email);
|
||||
console.log(" - Tem @:", form.email?.includes('@'));
|
||||
|
||||
// Se não for criar usuário, fecha normalmente
|
||||
alert("Paciente cadastrado com sucesso (sem usuário de acesso - email não fornecido).");
|
||||
onSaved?.(savedPatientProfile);
|
||||
setForm(initial);
|
||||
setPhotoPreview(null);
|
||||
setServerAnexos([]);
|
||||
|
||||
if (inline) onClose?.();
|
||||
else onOpenChange?.(false);
|
||||
|
||||
alert(mode === "create" ? "Paciente cadastrado!" : "Paciente atualizado!");
|
||||
}
|
||||
|
||||
onSaved?.(saved);
|
||||
}
|
||||
} catch (err: any) {
|
||||
setErrors({ submit: err?.message || "Erro ao salvar paciente." });
|
||||
console.error("❌ Erro no handleSubmit:", err);
|
||||
// Exibe mensagem amigável ao usuário
|
||||
const userMessage = err?.message?.includes("toPayload") || err?.message?.includes("is not defined")
|
||||
? "Erro ao processar os dados do formulário. Por favor, verifique os campos e tente novamente."
|
||||
: err?.message || "Erro ao salvar paciente. Por favor, tente novamente.";
|
||||
setErrors({ submit: userMessage });
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
@ -430,7 +429,6 @@ export function PatientRegistrationForm({
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{}
|
||||
<Collapsible open={expanded.dados} onOpenChange={() => setExpanded((s) => ({ ...s, dados: !s.dados }))}>
|
||||
<Card>
|
||||
<CollapsibleTrigger asChild>
|
||||
@ -449,7 +447,6 @@ export function PatientRegistrationForm({
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-24 h-24 border-2 border-dashed border-muted-foreground rounded-lg flex items-center justify-center overflow-hidden">
|
||||
{photoPreview ? (
|
||||
|
||||
<img src={photoPreview} alt="Preview" className="w-full h-full object-cover" />
|
||||
) : (
|
||||
<FileImage className="h-8 w-8 text-muted-foreground" />
|
||||
@ -524,12 +521,10 @@ export function PatientRegistrationForm({
|
||||
placeholder="dd/mm/aaaa"
|
||||
value={form.birth_date}
|
||||
onChange={(e) => {
|
||||
// permita apenas números e '/'
|
||||
const v = e.target.value.replace(/[^0-9\/]/g, "").slice(0, 10);
|
||||
setField("birth_date", v);
|
||||
}}
|
||||
onBlur={() => {
|
||||
// tenta formatar automaticamente se for uma data válida
|
||||
const raw = form.birth_date;
|
||||
const parts = raw.split(/\D+/).filter(Boolean);
|
||||
if (parts.length === 3) {
|
||||
@ -545,7 +540,6 @@ export function PatientRegistrationForm({
|
||||
</Card>
|
||||
</Collapsible>
|
||||
|
||||
{}
|
||||
<Collapsible open={expanded.contato} onOpenChange={() => setExpanded((s) => ({ ...s, contato: !s.contato }))}>
|
||||
<Card>
|
||||
<CollapsibleTrigger asChild>
|
||||
@ -562,6 +556,7 @@ export function PatientRegistrationForm({
|
||||
<div className="space-y-2">
|
||||
<Label>E-mail</Label>
|
||||
<Input value={form.email} onChange={(e) => setField("email", e.target.value)} />
|
||||
{errors.email && <p className="text-sm text-destructive">{errors.email}</p>}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Telefone</Label>
|
||||
@ -573,7 +568,6 @@ export function PatientRegistrationForm({
|
||||
</Card>
|
||||
</Collapsible>
|
||||
|
||||
{}
|
||||
<Collapsible open={expanded.endereco} onOpenChange={() => setExpanded((s) => ({ ...s, endereco: !s.endereco }))}>
|
||||
<Card>
|
||||
<CollapsibleTrigger asChild>
|
||||
@ -642,7 +636,6 @@ export function PatientRegistrationForm({
|
||||
</Card>
|
||||
</Collapsible>
|
||||
|
||||
{}
|
||||
<Collapsible open={expanded.obs} onOpenChange={() => setExpanded((s) => ({ ...s, obs: !s.obs }))}>
|
||||
<Card>
|
||||
<CollapsibleTrigger asChild>
|
||||
@ -709,7 +702,6 @@ export function PatientRegistrationForm({
|
||||
</Card>
|
||||
</Collapsible>
|
||||
|
||||
{}
|
||||
<div className="flex justify-end gap-4 pt-6 border-t">
|
||||
<Button type="button" variant="outline" onClick={() => (inline ? onClose?.() : onOpenChange?.(false))} disabled={isSubmitting}>
|
||||
<XCircle className="mr-2 h-4 w-4" />
|
||||
@ -729,38 +721,26 @@ export function PatientRegistrationForm({
|
||||
<>
|
||||
<div className="space-y-6">{content}</div>
|
||||
|
||||
{/* Debug */}
|
||||
{console.log("🎨 RENDER inline - credentials:", credentials, "showCredentials:", showCredentials)}
|
||||
|
||||
{/* Dialog de credenciais */}
|
||||
{credentials && (
|
||||
<CredentialsDialog
|
||||
open={showCredentials}
|
||||
open={showCredentialsDialog}
|
||||
onOpenChange={(open) => {
|
||||
console.log("🔄 CredentialsDialog onOpenChange:", open);
|
||||
setShowCredentials(open);
|
||||
setShowCredentialsDialog(open);
|
||||
if (!open) {
|
||||
console.log("🔄 Dialog fechando - chamando onSaved e limpando formulário");
|
||||
|
||||
// Chama onSaved com o paciente salvo
|
||||
if (savedPatient) {
|
||||
console.log("✅ Chamando onSaved com paciente:", savedPatient.id);
|
||||
onSaved?.(savedPatient);
|
||||
}
|
||||
|
||||
// Limpa o formulário e fecha
|
||||
setForm(initial);
|
||||
setPhotoPreview(null);
|
||||
setServerAnexos([]);
|
||||
// Quando o dialog de credenciais fecha, fecha o formulário também
|
||||
setCredentials(null);
|
||||
setSavedPatient(null);
|
||||
if (inline) {
|
||||
onClose?.();
|
||||
} else {
|
||||
onOpenChange?.(false);
|
||||
}
|
||||
}
|
||||
}}
|
||||
email={credentials.email}
|
||||
password={credentials.password}
|
||||
userName={form.nome}
|
||||
userType="paciente"
|
||||
userName={credentials.userName}
|
||||
userType={credentials.userType}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
@ -769,8 +749,6 @@ export function PatientRegistrationForm({
|
||||
|
||||
return (
|
||||
<>
|
||||
{console.log("🎨 RENDER dialog - credentials:", credentials, "showCredentials:", showCredentials)}
|
||||
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
@ -785,22 +763,18 @@ export function PatientRegistrationForm({
|
||||
{/* Dialog de credenciais */}
|
||||
{credentials && (
|
||||
<CredentialsDialog
|
||||
open={showCredentials}
|
||||
open={showCredentialsDialog}
|
||||
onOpenChange={(open) => {
|
||||
setShowCredentials(open);
|
||||
setShowCredentialsDialog(open);
|
||||
if (!open) {
|
||||
// Quando fechar o dialog, limpa o formulário e fecha o modal principal
|
||||
setForm(initial);
|
||||
setPhotoPreview(null);
|
||||
setServerAnexos([]);
|
||||
setCredentials(null);
|
||||
onOpenChange?.(false);
|
||||
}
|
||||
}}
|
||||
email={credentials.email}
|
||||
password={credentials.password}
|
||||
userName={form.nome}
|
||||
userType="paciente"
|
||||
userName={credentials.userName}
|
||||
userType={credentials.userType}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
import { createContext, useContext, useEffect, useState, ReactNode, useCallback, useMemo, useRef } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { loginUser, logoutUser, AuthenticationError } from '@/lib/auth'
|
||||
import { getUserInfo } from '@/lib/api'
|
||||
import { ENV_CONFIG } from '@/lib/env-config'
|
||||
import { isExpired, parseJwt } from '@/lib/jwt'
|
||||
import { httpClient } from '@/lib/http'
|
||||
import type {
|
||||
@ -118,7 +120,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
return
|
||||
}
|
||||
} catch (refreshError) {
|
||||
console.log('❌ [AUTH] Falha no refresh automático')
|
||||
console.log(' [AUTH] Falha no refresh automático')
|
||||
await new Promise(resolve => setTimeout(resolve, 400))
|
||||
}
|
||||
}
|
||||
@ -130,6 +132,35 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
// Restaurar sessão válida
|
||||
const userData = JSON.parse(storedUser) as UserData
|
||||
setToken(storedToken)
|
||||
// Tentar buscar profile consolidado (user-info) e mesclar
|
||||
try {
|
||||
const info = await getUserInfo()
|
||||
if (info?.profile) {
|
||||
const mapped = {
|
||||
cpf: (info.profile as any).cpf ?? userData.profile?.cpf,
|
||||
crm: (info.profile as any).crm ?? userData.profile?.crm,
|
||||
telefone: info.profile.phone ?? userData.profile?.telefone,
|
||||
foto_url: info.profile.avatar_url ?? userData.profile?.foto_url,
|
||||
}
|
||||
if (userData.profile) {
|
||||
userData.profile = { ...userData.profile, ...mapped }
|
||||
} else {
|
||||
userData.profile = mapped
|
||||
}
|
||||
// Persistir o usuário atualizado no localStorage para evitar
|
||||
// que 'auth_user.profile' fique vazio após um reload completo
|
||||
try {
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem(AUTH_STORAGE_KEYS.USER, JSON.stringify(userData))
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[AUTH] Falha ao persistir user (profile) no localStorage:', e)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[AUTH] Falha ao buscar user-info na restauração de sessão:', err)
|
||||
}
|
||||
|
||||
setUser(userData)
|
||||
setAuthStatus('authenticated')
|
||||
|
||||
@ -158,6 +189,62 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
const response = await loginUser(email, password, userType)
|
||||
|
||||
// Após receber token, buscar roles/permissions reais e reconciliar userType
|
||||
try {
|
||||
const infoRes = await fetch(`${ENV_CONFIG.SUPABASE_URL}/functions/v1/user-info`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${response.access_token}`,
|
||||
'apikey': ENV_CONFIG.SUPABASE_ANON_KEY,
|
||||
}
|
||||
})
|
||||
|
||||
if (infoRes.ok) {
|
||||
const info = await infoRes.json().catch(() => null)
|
||||
const roles: string[] = Array.isArray(info?.roles) ? info.roles : (info?.roles ? [info.roles] : [])
|
||||
|
||||
// Derivar tipo de usuário a partir dos roles
|
||||
let derived: UserType = 'paciente'
|
||||
if (roles.includes('admin') || roles.includes('gestor') || roles.includes('secretaria')) {
|
||||
derived = 'administrador'
|
||||
} else if (roles.includes('medico') || roles.includes('enfermeiro')) {
|
||||
derived = 'profissional'
|
||||
}
|
||||
|
||||
// Atualizar userType caso seja diferente
|
||||
if (response.user && response.user.userType !== derived) {
|
||||
response.user.userType = derived
|
||||
console.log('[AUTH] userType reconciled from roles ->', derived)
|
||||
}
|
||||
} else {
|
||||
console.warn('[AUTH] Falha ao obter user-info para reconciliar roles:', infoRes.status)
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[AUTH] Erro ao buscar user-info após login (não crítico):', err)
|
||||
}
|
||||
|
||||
// Após login, tentar buscar profile consolidado e mesclar antes de persistir
|
||||
try {
|
||||
const info = await getUserInfo()
|
||||
if (info?.profile && response.user) {
|
||||
const mapped = {
|
||||
cpf: (info.profile as any).cpf ?? response.user.profile?.cpf,
|
||||
crm: (info.profile as any).crm ?? response.user.profile?.crm,
|
||||
telefone: info.profile.phone ?? response.user.profile?.telefone,
|
||||
foto_url: info.profile.avatar_url ?? response.user.profile?.foto_url,
|
||||
}
|
||||
if (response.user.profile) {
|
||||
response.user.profile = { ...response.user.profile, ...mapped }
|
||||
} else {
|
||||
response.user.profile = mapped
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[AUTH] Falha ao buscar user-info após login (não crítico):', err)
|
||||
}
|
||||
|
||||
saveAuthData(
|
||||
response.access_token,
|
||||
response.user,
|
||||
|
||||
@ -4,8 +4,7 @@ import { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Report,
|
||||
CreateReportData,
|
||||
UpdateReportData,
|
||||
ApiError
|
||||
UpdateReportData
|
||||
} from '@/types/report-types';
|
||||
import {
|
||||
listarRelatorios,
|
||||
@ -16,6 +15,7 @@ import {
|
||||
listarRelatoriosPorPaciente,
|
||||
listarRelatoriosPorMedico
|
||||
} from '@/lib/reports';
|
||||
import { buscarPacientePorId, buscarMedicoPorId, buscarPacientesPorIds, buscarMedicosPorIds } from '@/lib/api';
|
||||
|
||||
interface UseReportsReturn {
|
||||
// Estados
|
||||
@ -42,6 +42,12 @@ export function useReports(): UseReportsReturn {
|
||||
const [selectedReport, setSelectedReport] = useState<Report | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
// Caches em memória para evitar múltiplas buscas pelo mesmo ID durante a sessão
|
||||
const patientsCacheRef = (globalThis as any).__reportsPatientsCache__ || new Map<string, any>();
|
||||
const doctorsCacheRef = (globalThis as any).__reportsDoctorsCache__ || new Map<string, any>();
|
||||
// store back to globalThis so cache persiste entre hot reloads
|
||||
(globalThis as any).__reportsPatientsCache__ = patientsCacheRef;
|
||||
(globalThis as any).__reportsDoctorsCache__ = doctorsCacheRef;
|
||||
|
||||
// Função para tratar erros
|
||||
const handleError = useCallback((error: any) => {
|
||||
@ -63,7 +69,134 @@ export function useReports(): UseReportsReturn {
|
||||
|
||||
try {
|
||||
const data = await listarRelatorios();
|
||||
setReports(data);
|
||||
|
||||
// Enriquecer relatórios: quando o backend retorna apenas IDs para paciente/executante,
|
||||
// buscamos os detalhes (nome, cpf, etc) em batch e anexamos como `paciente` e `executante`.
|
||||
const reportsWithRelations = await (async (arr: any[]) => {
|
||||
if (!arr || !arr.length) return arr;
|
||||
|
||||
const patientIds: string[] = [];
|
||||
const doctorIds: string[] = [];
|
||||
|
||||
for (const r of arr) {
|
||||
const pid = r.patient_id ?? r.patient ?? r.paciente;
|
||||
if (pid && typeof pid === 'string' && !patientIds.includes(pid) && !patientsCacheRef.has(String(pid))) patientIds.push(pid);
|
||||
|
||||
const did = r.requested_by ?? r.created_by ?? r.executante;
|
||||
if (did && typeof did === 'string' && !doctorIds.includes(did) && !doctorsCacheRef.has(String(did))) doctorIds.push(did);
|
||||
}
|
||||
|
||||
const patientsById = new Map<string, any>();
|
||||
if (patientIds.length) {
|
||||
try {
|
||||
const patients = await buscarPacientesPorIds(patientIds);
|
||||
for (const p of patients) {
|
||||
patientsById.set(String(p.id), p);
|
||||
patientsCacheRef.set(String(p.id), p);
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore batch failure
|
||||
}
|
||||
}
|
||||
|
||||
// fallback individual para quaisquer IDs que não foram resolvidos no batch
|
||||
const unresolvedPatientIds = patientIds.filter(id => !patientsById.has(String(id)) && !patientsCacheRef.has(String(id)));
|
||||
if (unresolvedPatientIds.length) {
|
||||
await Promise.all(unresolvedPatientIds.map(async (id) => {
|
||||
try {
|
||||
const p = await buscarPacientePorId(id);
|
||||
if (p) {
|
||||
patientsById.set(String(id), p);
|
||||
patientsCacheRef.set(String(id), p);
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore individual failure
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
const doctorsById = new Map<string, any>();
|
||||
if (doctorIds.length) {
|
||||
try {
|
||||
const doctors = await buscarMedicosPorIds(doctorIds);
|
||||
for (const d of doctors) {
|
||||
doctorsById.set(String(d.id), d);
|
||||
doctorsCacheRef.set(String(d.id), d);
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
const unresolvedDoctorIds = doctorIds.filter(id => !doctorsById.has(String(id)) && !doctorsCacheRef.has(String(id)));
|
||||
if (unresolvedDoctorIds.length) {
|
||||
await Promise.all(unresolvedDoctorIds.map(async (id) => {
|
||||
try {
|
||||
const d = await buscarMedicoPorId(id);
|
||||
if (d) {
|
||||
doctorsById.set(String(id), d);
|
||||
doctorsCacheRef.set(String(id), d);
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
const mapped = arr.map((r) => {
|
||||
const copy = { ...r } as any;
|
||||
|
||||
// Heurísticas: prefira nomes já presentes no payload do relatório
|
||||
const possiblePatientName = r.patient_name ?? r.patient_full_name ?? r.patientFullName ?? r.paciente?.full_name ?? r.paciente?.nome ?? r.patient?.full_name ?? r.patient?.nome;
|
||||
if (possiblePatientName) {
|
||||
copy.paciente = copy.paciente || {};
|
||||
copy.paciente.full_name = possiblePatientName;
|
||||
}
|
||||
|
||||
const pid = r.patient_id ?? r.patient ?? r.paciente;
|
||||
if (!copy.paciente && pid) {
|
||||
if (patientsById.has(String(pid))) copy.paciente = patientsById.get(String(pid));
|
||||
else if (patientsCacheRef.has(String(pid))) copy.paciente = patientsCacheRef.get(String(pid));
|
||||
}
|
||||
|
||||
// Executante: prefira campos de nome já fornecidos
|
||||
const possibleExecutorName = r.requested_by_name ?? r.requester_name ?? r.requestedByName ?? r.executante_name ?? r.executante?.nome ?? r.executante;
|
||||
if (possibleExecutorName) {
|
||||
copy.executante = possibleExecutorName;
|
||||
} else {
|
||||
const did = r.requested_by ?? r.created_by ?? r.executante;
|
||||
if (did) {
|
||||
if (doctorsById.has(String(did))) copy.executante = doctorsById.get(String(did))?.full_name ?? doctorsById.get(String(did))?.nome ?? copy.executante;
|
||||
else if (doctorsCacheRef.has(String(did))) copy.executante = doctorsCacheRef.get(String(did))?.full_name ?? doctorsCacheRef.get(String(did))?.nome ?? copy.executante;
|
||||
}
|
||||
}
|
||||
|
||||
return copy;
|
||||
});
|
||||
|
||||
// Debug: identificar relatórios que ainda não tiveram paciente/doctor resolvido
|
||||
try {
|
||||
const unresolvedPatients: string[] = [];
|
||||
const unresolvedDoctors: string[] = [];
|
||||
for (const r of mapped) {
|
||||
const pid = r.patient_id ?? r.patient ?? r.paciente;
|
||||
if (pid && !r.paciente) unresolvedPatients.push(String(pid));
|
||||
const did = r.requested_by ?? r.created_by ?? r.executante;
|
||||
// note: if executante was resolved to a name, r.executante will be string name; if still ID, it may be ID
|
||||
if (did && (typeof r.executante === 'undefined' || (typeof r.executante === 'string' && r.executante.length > 30 && r.executante.includes('-')))) {
|
||||
unresolvedDoctors.push(String(did));
|
||||
}
|
||||
}
|
||||
if (unresolvedPatients.length) console.warn('[useReports] Pacientes não resolvidos (após batch+fallback):', Array.from(new Set(unresolvedPatients)).slice(0,50));
|
||||
if (unresolvedDoctors.length) console.warn('[useReports] Executantes não resolvidos (após batch+fallback):', Array.from(new Set(unresolvedDoctors)).slice(0,50));
|
||||
} catch (e) {
|
||||
// ignore logging errors
|
||||
}
|
||||
|
||||
return mapped;
|
||||
})(data as any[]);
|
||||
|
||||
setReports(reportsWithRelations || data);
|
||||
} catch (err) {
|
||||
handleError(err);
|
||||
} finally {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
184
susconecta/lib/assignment.ts
Normal file
184
susconecta/lib/assignment.ts
Normal file
@ -0,0 +1,184 @@
|
||||
// lib/assignment.ts
|
||||
|
||||
import { ENV_CONFIG } from '@/lib/env-config';
|
||||
|
||||
// ===== TIPOS =====
|
||||
|
||||
// Roles válidos para patient_assignments conforme documentação
|
||||
export type PatientAssignmentRole = "medico" | "enfermeiro";
|
||||
|
||||
export interface PatientAssignment {
|
||||
id: string;
|
||||
patient_id: string;
|
||||
user_id: string;
|
||||
role: PatientAssignmentRole;
|
||||
created_at: string;
|
||||
created_by: string | null;
|
||||
}
|
||||
|
||||
export interface CreateAssignmentInput {
|
||||
patient_id: string;
|
||||
user_id: string;
|
||||
role: PatientAssignmentRole;
|
||||
created_by?: string | null;
|
||||
}
|
||||
|
||||
// ===== CONSTANTES =====
|
||||
|
||||
const ASSIGNMENTS_URL = `${ENV_CONFIG.SUPABASE_URL}/rest/v1/patient_assignments`;
|
||||
|
||||
// ===== FUNÇÕES DA API =====
|
||||
|
||||
/**
|
||||
* Obtém o token de autenticação do localStorage.
|
||||
*/
|
||||
function getAuthToken(): string | null {
|
||||
if (typeof window === "undefined") return null;
|
||||
return localStorage.getItem("token") || localStorage.getItem("auth_token");
|
||||
}
|
||||
|
||||
/**
|
||||
* Cria os cabeçalhos padrão para as requisições.
|
||||
*/
|
||||
function getHeaders(): Record<string, string> {
|
||||
const token = getAuthToken();
|
||||
const headers: Record<string, string> = {
|
||||
"apikey": ENV_CONFIG.SUPABASE_ANON_KEY,
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
};
|
||||
if (token) {
|
||||
headers["Authorization"] = `Bearer ${token}`;
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Atribui uma função (role) a um usuário para um paciente específico.
|
||||
* @param input - Os dados para a nova atribuição.
|
||||
* @returns A atribuição criada.
|
||||
*/
|
||||
export async function assignRoleToUser(input: CreateAssignmentInput): Promise<PatientAssignment> {
|
||||
console.log("📝 [ASSIGNMENT] Atribuindo função:", input);
|
||||
|
||||
try {
|
||||
const response = await fetch(ASSIGNMENTS_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...getHeaders(),
|
||||
'Prefer': 'return=representation', // Pede ao Supabase para retornar o objeto criado
|
||||
},
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorBody = await response.text();
|
||||
console.error("❌ [ASSIGNMENT] Erro na resposta da API:", {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
body: errorBody,
|
||||
});
|
||||
// Include body (when available) to help debugging (e.g., constraint violations)
|
||||
const bodySnippet = errorBody ? ` - body: ${errorBody}` : '';
|
||||
throw new Error(`Erro ao atribuir função: ${response.statusText} (${response.status})${bodySnippet}`);
|
||||
}
|
||||
|
||||
const createdAssignment = await response.json();
|
||||
|
||||
// O Supabase retorna um array com o item criado
|
||||
if (Array.isArray(createdAssignment) && createdAssignment.length > 0) {
|
||||
console.log("✅ [ASSIGNMENT] Função atribuída com sucesso:", createdAssignment[0]);
|
||||
return createdAssignment[0];
|
||||
}
|
||||
|
||||
throw new Error("A API não retornou a atribuição criada.");
|
||||
|
||||
} catch (error) {
|
||||
console.error("❌ [ASSIGNMENT] Erro inesperado ao atribuir função:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lista todas as atribuições de um paciente.
|
||||
* @param patientId - O ID do paciente.
|
||||
* @returns Uma lista de atribuições.
|
||||
*/
|
||||
export async function listAssignmentsForPatient(patientId: string): Promise<PatientAssignment[]> {
|
||||
console.log(`🔍 [ASSIGNMENT] Listando atribuições para o paciente: ${patientId}`);
|
||||
|
||||
const url = `${ASSIGNMENTS_URL}?patient_id=eq.${patientId}`;
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: getHeaders(),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorBody = await response.text();
|
||||
console.error("❌ [ASSIGNMENT] Erro ao listar atribuições:", {
|
||||
status: response.status,
|
||||
body: errorBody,
|
||||
});
|
||||
throw new Error(`Erro ao listar atribuições: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const assignments = await response.json();
|
||||
console.log(`✅ [ASSIGNMENT] ${assignments.length} atribuições encontradas.`);
|
||||
return assignments;
|
||||
|
||||
} catch (error) {
|
||||
console.error("❌ [ASSIGNMENT] Erro inesperado ao listar atribuições:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lista todas as atribuições para um dado usuário (médico/enfermeiro).
|
||||
* Útil para obter os patient_id dos pacientes atribuídos ao usuário.
|
||||
*/
|
||||
export async function listAssignmentsForUser(userId: string): Promise<PatientAssignment[]> {
|
||||
console.log(`🔍 [ASSIGNMENT] Listando atribuições para o usuário: ${userId}`);
|
||||
const url = `${ASSIGNMENTS_URL}?user_id=eq.${encodeURIComponent(userId)}`;
|
||||
|
||||
try {
|
||||
const headers = getHeaders();
|
||||
console.debug('[ASSIGNMENT] GET', url, 'headers(masked)=', {
|
||||
...headers,
|
||||
Authorization: headers.Authorization ? '<<masked>>' : undefined,
|
||||
});
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
});
|
||||
|
||||
// dump raw text for debugging when content-type isn't JSON or when empty
|
||||
const contentType = response.headers.get('content-type') || '';
|
||||
const txt = await response.clone().text().catch(() => '');
|
||||
console.debug('[ASSIGNMENT] response status=', response.status, response.statusText, 'content-type=', contentType, 'bodyPreview=', txt ? (txt.length > 1000 ? txt.slice(0,1000) + '...[truncated]' : txt) : '<empty>');
|
||||
|
||||
if (!response.ok) {
|
||||
const errorBody = txt || '';
|
||||
console.error("❌ [ASSIGNMENT] Erro ao listar atribuições por usuário:", {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
body: errorBody,
|
||||
});
|
||||
throw new Error(`Erro ao listar atribuições por usuário: ${response.status} ${response.statusText} - body: ${errorBody}`);
|
||||
}
|
||||
|
||||
let assignments: any = [];
|
||||
try {
|
||||
assignments = await response.json();
|
||||
} catch (e) {
|
||||
console.warn('[ASSIGNMENT] não foi possível parsear JSON, usando texto cru como fallback');
|
||||
assignments = txt ? JSON.parse(txt) : [];
|
||||
}
|
||||
console.log(`✅ [ASSIGNMENT] ${Array.isArray(assignments) ? assignments.length : 0} atribuições encontradas para o usuário.`);
|
||||
return Array.isArray(assignments) ? assignments : [];
|
||||
} catch (error) {
|
||||
console.error("❌ [ASSIGNMENT] Erro inesperado ao listar atribuições por usuário:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -6,7 +6,7 @@ import type {
|
||||
UserData
|
||||
} from '@/types/auth';
|
||||
|
||||
import { API_CONFIG, AUTH_ENDPOINTS, DEFAULT_HEADERS, API_KEY, buildApiUrl } from '@/lib/config';
|
||||
import { API_CONFIG, AUTH_ENDPOINTS, DEFAULT_HEADERS, buildApiUrl } from '@/lib/config';
|
||||
import { debugRequest } from '@/lib/debug-utils';
|
||||
import { ENV_CONFIG } from '@/lib/env-config';
|
||||
|
||||
@ -31,7 +31,7 @@ function getAuthHeaders(token: string): Record<string, string> {
|
||||
return {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
"apikey": API_KEY,
|
||||
"apikey": ENV_CONFIG.SUPABASE_ANON_KEY,
|
||||
"Authorization": `Bearer ${token}`,
|
||||
};
|
||||
}
|
||||
@ -43,7 +43,7 @@ function getLoginHeaders(): Record<string, string> {
|
||||
return {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
"apikey": API_KEY,
|
||||
"apikey": ENV_CONFIG.SUPABASE_ANON_KEY,
|
||||
};
|
||||
}
|
||||
|
||||
@ -267,7 +267,7 @@ export async function refreshAuthToken(refreshToken: string): Promise<RefreshTok
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
"apikey": API_KEY,
|
||||
"apikey": ENV_CONFIG.SUPABASE_ANON_KEY,
|
||||
},
|
||||
body: JSON.stringify({ refresh_token: refreshToken }),
|
||||
});
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
|
||||
import { AUTH_STORAGE_KEYS } from '@/types/auth'
|
||||
import { isExpired } from '@/lib/jwt'
|
||||
import { API_KEY } from '@/lib/config'
|
||||
import { ENV_CONFIG } from '@/lib/env-config'
|
||||
|
||||
interface QueuedRequest {
|
||||
resolve: (value: any) => void
|
||||
@ -58,11 +58,11 @@ class HttpClient {
|
||||
timestamp: new Date().toLocaleTimeString()
|
||||
})
|
||||
|
||||
const response = await fetch('https://yuanqfswhberkoevtmfr.supabase.co/auth/v1/token?grant_type=refresh_token', {
|
||||
const response = await fetch(ENV_CONFIG.AUTH_ENDPOINTS.REFRESH, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'apikey': API_KEY // API Key sempre necessária
|
||||
'apikey': ENV_CONFIG.SUPABASE_ANON_KEY // API Key sempre necessária
|
||||
},
|
||||
body: JSON.stringify({ refresh_token: refreshToken })
|
||||
})
|
||||
@ -141,7 +141,7 @@ class HttpClient {
|
||||
// Reexecutar requisição original
|
||||
const newHeaders = {
|
||||
...config.headers,
|
||||
'apikey': API_KEY, // Garantir API Key
|
||||
'apikey': ENV_CONFIG.SUPABASE_ANON_KEY, // Garantir API Key
|
||||
Authorization: `Bearer ${newToken}`
|
||||
}
|
||||
|
||||
@ -207,7 +207,7 @@ class HttpClient {
|
||||
url: url.startsWith('http') ? url : `${this.baseURL}${url}`,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'apikey': API_KEY, // API Key da Supabase sempre presente
|
||||
'apikey': ENV_CONFIG.SUPABASE_ANON_KEY, // API Key da Supabase sempre presente
|
||||
...(token && { Authorization: `Bearer ${token}` }), // Bearer Token quando usuário logado
|
||||
...options.headers
|
||||
},
|
||||
@ -254,7 +254,7 @@ class HttpClient {
|
||||
}
|
||||
|
||||
// Instância única do cliente HTTP
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'https://mock.apidog.com/m1/1053378-0-default'
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'https://yuanqog.com/m1/1053378-0-default'
|
||||
export const httpClient = new HttpClient(API_BASE_URL)
|
||||
|
||||
export default httpClient
|
||||
@ -45,29 +45,39 @@ import {
|
||||
CreateReportData,
|
||||
UpdateReportData,
|
||||
ReportsResponse,
|
||||
ReportResponse,
|
||||
ApiError
|
||||
ReportResponse
|
||||
} from '@/types/report-types';
|
||||
|
||||
// URL base da API Mock
|
||||
const BASE_API_RELATORIOS = 'https://mock.apidog.com/m1/1053378-0-default/rest/v1/reports';
|
||||
// Definição local para ApiError
|
||||
type ApiError = {
|
||||
message: string;
|
||||
code: string;
|
||||
};
|
||||
|
||||
// URL base da API Supabase
|
||||
const BASE_API_RELATORIOS = 'https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/reports';
|
||||
|
||||
// Cabeçalhos base para as requisições Supabase
|
||||
function obterCabecalhos(token?: string): HeadersInit {
|
||||
// If token not passed explicitly, try the same fallbacks as lib/api.ts
|
||||
if (!token && typeof window !== 'undefined') {
|
||||
token =
|
||||
localStorage.getItem('auth_token') ||
|
||||
localStorage.getItem('token') ||
|
||||
sessionStorage.getItem('auth_token') ||
|
||||
sessionStorage.getItem('token') ||
|
||||
undefined;
|
||||
}
|
||||
|
||||
// Cabeçalhos base para as requisições
|
||||
function obterCabecalhos(): HeadersInit {
|
||||
const cabecalhos: HeadersInit = {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'apikey': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ',
|
||||
'Prefer': 'return=representation',
|
||||
};
|
||||
|
||||
// Adiciona token de autenticação do localStorage se disponível
|
||||
if (typeof window !== 'undefined') {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
cabecalhos['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
}
|
||||
|
||||
return cabecalhos;
|
||||
}
|
||||
|
||||
@ -75,12 +85,15 @@ function obterCabecalhos(): HeadersInit {
|
||||
async function tratarRespostaApi<T>(resposta: Response): Promise<T> {
|
||||
if (!resposta.ok) {
|
||||
let mensagemErro = `HTTP ${resposta.status}: ${resposta.statusText}`;
|
||||
let rawText = '';
|
||||
try {
|
||||
const dadosErro = await resposta.json();
|
||||
rawText = await resposta.clone().text();
|
||||
const dadosErro = JSON.parse(rawText || '{}');
|
||||
mensagemErro = dadosErro.message || dadosErro.error || mensagemErro;
|
||||
} catch (e) {
|
||||
// Se não conseguir parsear como JSON, usa a mensagem de status HTTP
|
||||
// Se não conseguir parsear como JSON, manter rawText para debug
|
||||
}
|
||||
console.error('[tratarRespostaApi] response raw:', rawText);
|
||||
const erro: ApiError = {
|
||||
message: mensagemErro,
|
||||
code: resposta.status.toString(),
|
||||
@ -106,25 +119,44 @@ export async function listarRelatorios(filtros?: { patient_id?: string; status?:
|
||||
url += `?${params.toString()}`;
|
||||
}
|
||||
|
||||
// Busca o token do usuário (compatível com lib/api.ts keys)
|
||||
let token: string | undefined = undefined;
|
||||
if (typeof window !== 'undefined') {
|
||||
token =
|
||||
localStorage.getItem('auth_token') ||
|
||||
localStorage.getItem('token') ||
|
||||
sessionStorage.getItem('auth_token') ||
|
||||
sessionStorage.getItem('token') ||
|
||||
undefined;
|
||||
}
|
||||
|
||||
// Monta cabeçalhos conforme cURL
|
||||
const cabecalhos: HeadersInit = {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'apikey': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ',
|
||||
};
|
||||
if (typeof window !== 'undefined') {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
cabecalhos['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Logs de depuração (mask token)
|
||||
const masked = token ? `${token.slice(0, 6)}...${token.slice(-6)}` : null;
|
||||
console.log('[listarRelatorios] URL:', url);
|
||||
console.log('[listarRelatorios] Authorization (masked):', masked);
|
||||
console.log('[listarRelatorios] Headers (masked):', {
|
||||
...cabecalhos,
|
||||
Authorization: cabecalhos['Authorization'] ? '<<masked>>' : undefined,
|
||||
});
|
||||
|
||||
const resposta = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: cabecalhos,
|
||||
});
|
||||
console.log('[listarRelatorios] Status:', resposta.status, resposta.statusText);
|
||||
const dados = await resposta.json().catch(() => null);
|
||||
console.log('[listarRelatorios] Payload:', dados);
|
||||
if (!resposta.ok) throw new Error('Erro ao buscar relatórios');
|
||||
const dados = await resposta.json();
|
||||
if (Array.isArray(dados)) return dados;
|
||||
if (dados && Array.isArray(dados.data)) return dados.data;
|
||||
for (const chave in dados) {
|
||||
@ -139,13 +171,15 @@ export async function listarRelatorios(filtros?: { patient_id?: string; status?:
|
||||
export async function buscarRelatorioPorId(id: string): Promise<Report> {
|
||||
try {
|
||||
console.log('🔍 [API RELATÓRIOS] Buscando relatório ID:', id);
|
||||
const resposta = await fetch(`${BASE_API_RELATORIOS}/${id}`, {
|
||||
const resposta = await fetch(`${BASE_API_RELATORIOS}?id=eq.${id}`, {
|
||||
method: 'GET',
|
||||
headers: obterCabecalhos(),
|
||||
});
|
||||
const resultado = await tratarRespostaApi<ReportResponse>(resposta);
|
||||
console.log('✅ [API RELATÓRIOS] Relatório encontrado:', resultado.data);
|
||||
return resultado.data;
|
||||
const resultado = await tratarRespostaApi<Report[]>(resposta);
|
||||
const relatorio = Array.isArray(resultado) && resultado.length > 0 ? resultado[0] : null;
|
||||
console.log('✅ [API RELATÓRIOS] Relatório encontrado:', relatorio);
|
||||
if (!relatorio) throw new Error('Relatório não encontrado');
|
||||
return relatorio;
|
||||
} catch (erro) {
|
||||
console.error('❌ [API RELATÓRIOS] Erro ao buscar relatório:', erro);
|
||||
throw erro;
|
||||
@ -155,60 +189,39 @@ export async function buscarRelatorioPorId(id: string): Promise<Report> {
|
||||
/**
|
||||
* Cria um novo relatório médico
|
||||
*/
|
||||
export async function criarRelatorio(dadosRelatorio: CreateReportData): Promise<Report> {
|
||||
try {
|
||||
console.log('📝 [API RELATÓRIOS] Criando novo relatório...');
|
||||
console.log('📤 [API RELATÓRIOS] Dados enviados:', dadosRelatorio);
|
||||
export async function criarRelatorio(dadosRelatorio: CreateReportData, token?: string): Promise<Report> {
|
||||
const headers = obterCabecalhos(token);
|
||||
const masked = (headers as any)['Authorization'] ? String((headers as any)['Authorization']).replace(/Bearer\s+(.+)/, 'Bearer <token_masked>') : null;
|
||||
console.log('[criarRelatorio] POST', BASE_API_RELATORIOS);
|
||||
console.log('[criarRelatorio] Headers (masked):', { ...headers, Authorization: masked });
|
||||
|
||||
const resposta = await fetch(BASE_API_RELATORIOS, {
|
||||
method: 'POST',
|
||||
headers: obterCabecalhos(),
|
||||
headers,
|
||||
body: JSON.stringify(dadosRelatorio),
|
||||
});
|
||||
console.log('📝 [API RELATÓRIOS] Status da criação:', resposta.status);
|
||||
console.log('📝 [API RELATÓRIOS] Response OK:', resposta.ok);
|
||||
console.log('📝 [API RELATÓRIOS] Response URL:', resposta.url);
|
||||
console.log('[criarRelatorio] Status:', resposta.status, resposta.statusText);
|
||||
if (!resposta.ok) {
|
||||
let mensagemErro = `HTTP ${resposta.status}: ${resposta.statusText}`;
|
||||
try {
|
||||
const dadosErro = await resposta.json();
|
||||
mensagemErro = dadosErro.message || dadosErro.error || mensagemErro;
|
||||
console.log('📝 [API RELATÓRIOS] Erro da API:', dadosErro);
|
||||
console.error('[criarRelatorio] error body:', dadosErro);
|
||||
} catch (e) {
|
||||
console.log('📝 [API RELATÓRIOS] Não foi possível parsear erro como JSON');
|
||||
console.error('[criarRelatorio] erro ao parsear body de erro');
|
||||
}
|
||||
const erro: ApiError = {
|
||||
const erro: any = {
|
||||
message: mensagemErro,
|
||||
code: resposta.status.toString(),
|
||||
};
|
||||
throw erro;
|
||||
}
|
||||
const resultadoBruto = await resposta.json();
|
||||
console.log('📝 [API RELATÓRIOS] Resposta bruta da criação:', resultadoBruto);
|
||||
console.log('📝 [API RELATÓRIOS] Tipo da resposta:', typeof resultadoBruto);
|
||||
console.log('📝 [API RELATÓRIOS] Chaves da resposta:', Object.keys(resultadoBruto || {}));
|
||||
let relatorioCriado: Report;
|
||||
// Verifica formato da resposta similar ao listarRelatorios
|
||||
if (resultadoBruto && resultadoBruto.data) {
|
||||
relatorioCriado = resultadoBruto.data;
|
||||
} else if (resultadoBruto && resultadoBruto.id) {
|
||||
relatorioCriado = resultadoBruto;
|
||||
} else if (Array.isArray(resultadoBruto) && resultadoBruto.length > 0) {
|
||||
relatorioCriado = resultadoBruto[0];
|
||||
} else {
|
||||
console.warn('📝 [API RELATÓRIOS] Formato de resposta inesperado, criando relatório local');
|
||||
relatorioCriado = {
|
||||
id: 'local-' + Date.now() + '-' + Math.random().toString(36).substr(2, 5),
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
...dadosRelatorio
|
||||
};
|
||||
}
|
||||
console.log('✅ [API RELATÓRIOS] Relatório processado:', relatorioCriado);
|
||||
return relatorioCriado;
|
||||
} catch (erro) {
|
||||
console.error('❌ [API RELATÓRIOS] Erro ao criar relatório:', erro);
|
||||
throw erro;
|
||||
const resultado = await resposta.json();
|
||||
// Supabase retorna array
|
||||
if (Array.isArray(resultado) && resultado.length > 0) {
|
||||
return resultado[0];
|
||||
}
|
||||
throw new Error('Resposta inesperada da API Supabase');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -218,14 +231,16 @@ export async function atualizarRelatorio(id: string, dadosRelatorio: UpdateRepor
|
||||
try {
|
||||
console.log('📝 [API RELATÓRIOS] Atualizando relatório ID:', id);
|
||||
console.log('📤 [API RELATÓRIOS] Dados:', dadosRelatorio);
|
||||
const resposta = await fetch(`${BASE_API_RELATORIOS}/${id}`, {
|
||||
const resposta = await fetch(`${BASE_API_RELATORIOS}?id=eq.${id}`, {
|
||||
method: 'PATCH',
|
||||
headers: obterCabecalhos(),
|
||||
body: JSON.stringify(dadosRelatorio),
|
||||
});
|
||||
const resultado = await tratarRespostaApi<ReportResponse>(resposta);
|
||||
console.log('✅ [API RELATÓRIOS] Relatório atualizado:', resultado.data);
|
||||
return resultado.data;
|
||||
const resultado = await tratarRespostaApi<Report[]>(resposta);
|
||||
const relatorio = Array.isArray(resultado) && resultado.length > 0 ? resultado[0] : null;
|
||||
console.log('✅ [API RELATÓRIOS] Relatório atualizado:', relatorio);
|
||||
if (!relatorio) throw new Error('Relatório não encontrado');
|
||||
return relatorio;
|
||||
} catch (erro) {
|
||||
console.error('❌ [API RELATÓRIOS] Erro ao atualizar relatório:', erro);
|
||||
throw erro;
|
||||
@ -256,13 +271,18 @@ export async function deletarRelatorio(id: string): Promise<void> {
|
||||
export async function listarRelatoriosPorPaciente(idPaciente: string): Promise<Report[]> {
|
||||
try {
|
||||
console.log('👤 [API RELATÓRIOS] Buscando relatórios do paciente:', idPaciente);
|
||||
const resposta = await fetch(`${BASE_API_RELATORIOS}?patient_id=${idPaciente}`, {
|
||||
const url = `${BASE_API_RELATORIOS}?patient_id=eq.${idPaciente}`;
|
||||
const headers = obterCabecalhos();
|
||||
const masked = (headers as any)['Authorization'] ? `${String((headers as any)['Authorization']).slice(0,6)}...${String((headers as any)['Authorization']).slice(-6)}` : null;
|
||||
console.debug('[listarRelatoriosPorPaciente] URL:', url);
|
||||
console.debug('[listarRelatoriosPorPaciente] Headers (masked):', { ...headers, Authorization: masked ? '<<masked>>' : undefined });
|
||||
const resposta = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: obterCabecalhos(),
|
||||
headers,
|
||||
});
|
||||
const resultado = await tratarRespostaApi<ReportsResponse>(resposta);
|
||||
console.log('✅ [API RELATÓRIOS] Relatórios do paciente encontrados:', resultado.data?.length || 0);
|
||||
return resultado.data || [];
|
||||
const resultado = await tratarRespostaApi<Report[]>(resposta);
|
||||
console.log('✅ [API RELATÓRIOS] Relatórios do paciente encontrados:', resultado.length);
|
||||
return resultado;
|
||||
} catch (erro) {
|
||||
console.error('❌ [API RELATÓRIOS] Erro ao buscar relatórios do paciente:', erro);
|
||||
throw erro;
|
||||
@ -275,15 +295,85 @@ export async function listarRelatoriosPorPaciente(idPaciente: string): Promise<R
|
||||
export async function listarRelatoriosPorMedico(idMedico: string): Promise<Report[]> {
|
||||
try {
|
||||
console.log('👨⚕️ [API RELATÓRIOS] Buscando relatórios do médico:', idMedico);
|
||||
const resposta = await fetch(`${BASE_API_RELATORIOS}?doctor_id=${idMedico}`, {
|
||||
const url = `${BASE_API_RELATORIOS}?requested_by=eq.${idMedico}`;
|
||||
const headers = obterCabecalhos();
|
||||
const masked = (headers as any)['Authorization'] ? `${String((headers as any)['Authorization']).slice(0,6)}...${String((headers as any)['Authorization']).slice(-6)}` : null;
|
||||
console.debug('[listarRelatoriosPorMedico] URL:', url);
|
||||
console.debug('[listarRelatoriosPorMedico] Headers (masked):', { ...headers, Authorization: masked ? '<<masked>>' : undefined });
|
||||
const resposta = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: obterCabecalhos(),
|
||||
});
|
||||
const resultado = await tratarRespostaApi<ReportsResponse>(resposta);
|
||||
console.log('✅ [API RELATÓRIOS] Relatórios do médico encontrados:', resultado.data?.length || 0);
|
||||
return resultado.data || [];
|
||||
const resultado = await tratarRespostaApi<Report[]>(resposta);
|
||||
console.log('✅ [API RELATÓRIOS] Relatórios do médico encontrados:', resultado.length);
|
||||
return resultado;
|
||||
} catch (erro) {
|
||||
console.error('❌ [API RELATÓRIOS] Erro ao buscar relatórios do médico:', erro);
|
||||
throw erro;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lista relatórios para vários pacientes em uma única chamada (usa in.(...)).
|
||||
* Retorna array vazio se nenhum id for fornecido.
|
||||
*/
|
||||
export async function listarRelatoriosPorPacientes(ids: string[]): Promise<Report[]> {
|
||||
try {
|
||||
if (!ids || !ids.length) return [];
|
||||
// sanitize ids and remove empties
|
||||
const cleaned = ids.map(i => String(i).trim()).filter(Boolean);
|
||||
if (!cleaned.length) return [];
|
||||
|
||||
// monta cláusula in.(id1,id2,...)
|
||||
const inClause = cleaned.join(',');
|
||||
const url = `${BASE_API_RELATORIOS}?patient_id=in.(${inClause})`;
|
||||
const headers = obterCabecalhos();
|
||||
const masked = (headers as any)['Authorization'] ? '<<masked>>' : undefined;
|
||||
console.debug('[listarRelatoriosPorPacientes] URL:', url);
|
||||
console.debug('[listarRelatoriosPorPacientes] Headers (masked):', { ...headers, Authorization: masked ? '<<masked>>' : undefined });
|
||||
|
||||
const resposta = await fetch(url, { method: 'GET', headers });
|
||||
const resultado = await tratarRespostaApi<Report[]>(resposta);
|
||||
console.log('✅ [API RELATÓRIOS] Relatórios encontrados para pacientes:', resultado.length);
|
||||
return resultado;
|
||||
} catch (erro) {
|
||||
console.error('❌ [API RELATÓRIOS] Erro ao buscar relatórios para vários pacientes:', erro);
|
||||
throw erro;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lista relatórios apenas para pacientes que foram atribuídos ao médico (userId).
|
||||
* - Recupera as atribuições via `listAssignmentsForUser(userId)`
|
||||
* - Extrai os patient_id e chama `listarRelatoriosPorPacientes` em batch
|
||||
*/
|
||||
export async function listarRelatoriosParaMedicoAtribuido(userId?: string): Promise<Report[]> {
|
||||
try {
|
||||
if (!userId) {
|
||||
console.warn('[listarRelatoriosParaMedicoAtribuido] userId ausente, retornando array vazio');
|
||||
return [];
|
||||
}
|
||||
|
||||
console.log('[listarRelatoriosParaMedicoAtribuido] buscando assignments para user:', userId);
|
||||
// importe dinamicamente para evitar possíveis ciclos
|
||||
const assignmentMod = await import('./assignment');
|
||||
const assigns = await assignmentMod.listAssignmentsForUser(String(userId));
|
||||
if (!assigns || !Array.isArray(assigns) || assigns.length === 0) {
|
||||
console.log('[listarRelatoriosParaMedicoAtribuido] nenhum paciente atribuído encontrado para user:', userId);
|
||||
return [];
|
||||
}
|
||||
|
||||
const patientIds = Array.from(new Set(assigns.map((a: any) => String(a.patient_id)).filter(Boolean)));
|
||||
if (!patientIds.length) {
|
||||
console.log('[listarRelatoriosParaMedicoAtribuido] nenhuma patient_id válida encontrada nas atribuições');
|
||||
return [];
|
||||
}
|
||||
|
||||
console.log('[listarRelatoriosParaMedicoAtribuido] carregando relatórios para pacientes:', patientIds);
|
||||
const rels = await listarRelatoriosPorPacientes(patientIds);
|
||||
return rels || [];
|
||||
} catch (err) {
|
||||
console.error('[listarRelatoriosParaMedicoAtribuido] erro:', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@ -43,6 +43,7 @@
|
||||
"@radix-ui/react-toggle": "latest",
|
||||
"@radix-ui/react-toggle-group": "latest",
|
||||
"@radix-ui/react-tooltip": "latest",
|
||||
"@supabase/supabase-js": "^2.75.0",
|
||||
"@vercel/analytics": "1.3.1",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
|
||||
111
susconecta/pnpm-lock.yaml
generated
111
susconecta/pnpm-lock.yaml
generated
@ -107,6 +107,9 @@ importers:
|
||||
'@radix-ui/react-tooltip':
|
||||
specifier: latest
|
||||
version: 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@supabase/supabase-js':
|
||||
specifier: ^2.75.0
|
||||
version: 2.75.0
|
||||
'@vercel/analytics':
|
||||
specifier: 1.3.1
|
||||
version: 1.3.1(next@15.5.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
|
||||
@ -1230,6 +1233,28 @@ packages:
|
||||
'@standard-schema/utils@0.3.0':
|
||||
resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==}
|
||||
|
||||
'@supabase/auth-js@2.75.0':
|
||||
resolution: {integrity: sha512-J8TkeqCOMCV4KwGKVoxmEBuDdHRwoInML2vJilthOo7awVCro2SM+tOcpljORwuBQ1vHUtV62Leit+5wlxrNtw==}
|
||||
|
||||
'@supabase/functions-js@2.75.0':
|
||||
resolution: {integrity: sha512-18yk07Moj/xtQ28zkqswxDavXC3vbOwt1hDuYM3/7xPnwwpKnsmPyZ7bQ5th4uqiJzQ135t74La9tuaxBR6e7w==}
|
||||
|
||||
'@supabase/node-fetch@2.6.15':
|
||||
resolution: {integrity: sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==}
|
||||
engines: {node: 4.x || >=6.0.0}
|
||||
|
||||
'@supabase/postgrest-js@2.75.0':
|
||||
resolution: {integrity: sha512-YfBz4W/z7eYCFyuvHhfjOTTzRrQIvsMG2bVwJAKEVVUqGdzqfvyidXssLBG0Fqlql1zJFgtsPpK1n4meHrI7tg==}
|
||||
|
||||
'@supabase/realtime-js@2.75.0':
|
||||
resolution: {integrity: sha512-B4Xxsf2NHd5cEnM6MGswOSPSsZKljkYXpvzKKmNxoUmNQOfB7D8HOa6NwHcUBSlxcjV+vIrYKcYXtavGJqeGrw==}
|
||||
|
||||
'@supabase/storage-js@2.75.0':
|
||||
resolution: {integrity: sha512-wpJMYdfFDckDiHQaTpK+Ib14N/O2o0AAWWhguKvmmMurB6Unx17GGmYp5rrrqCTf8S1qq4IfIxTXxS4hzrUySg==}
|
||||
|
||||
'@supabase/supabase-js@2.75.0':
|
||||
resolution: {integrity: sha512-8UN/vATSgS2JFuJlMVr51L3eUDz+j1m7Ww63wlvHLKULzCDaVWYzvacCjBTLW/lX/vedI2LBI4Vg+01G9ufsJQ==}
|
||||
|
||||
'@swc/helpers@0.5.15':
|
||||
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
|
||||
|
||||
@ -1366,6 +1391,9 @@ packages:
|
||||
'@types/pako@2.0.4':
|
||||
resolution: {integrity: sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==}
|
||||
|
||||
'@types/phoenix@1.6.6':
|
||||
resolution: {integrity: sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==}
|
||||
|
||||
'@types/prop-types@15.7.15':
|
||||
resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==}
|
||||
|
||||
@ -1392,6 +1420,9 @@ packages:
|
||||
'@types/use-sync-external-store@0.0.6':
|
||||
resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==}
|
||||
|
||||
'@types/ws@8.18.1':
|
||||
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.45.0':
|
||||
resolution: {integrity: sha512-HC3y9CVuevvWCl/oyZuI47dOeDF9ztdMEfMH8/DW/Mhwa9cCLnK1oD7JoTVGW/u7kFzNZUKUoyJEqkaJh5y3Wg==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
@ -3117,6 +3148,9 @@ packages:
|
||||
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
||||
engines: {node: '>=8.0'}
|
||||
|
||||
tr46@0.0.3:
|
||||
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
|
||||
|
||||
trim-canvas@0.1.2:
|
||||
resolution: {integrity: sha512-nd4Ga3iLFV94mdhW9JFMLpQbHUyCQuhFOD71PEAt1NjtMD5wbZctzhX8c3agHNybMR5zXD1XTGoIEWk995E6pQ==}
|
||||
|
||||
@ -3223,6 +3257,12 @@ packages:
|
||||
victory-vendor@37.3.6:
|
||||
resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==}
|
||||
|
||||
webidl-conversions@3.0.1:
|
||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||
|
||||
whatwg-url@5.0.0:
|
||||
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
||||
|
||||
which-boxed-primitive@1.1.1:
|
||||
resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@ -3248,6 +3288,18 @@ packages:
|
||||
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
ws@8.18.3:
|
||||
resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
peerDependencies:
|
||||
bufferutil: ^4.0.1
|
||||
utf-8-validate: '>=5.0.2'
|
||||
peerDependenciesMeta:
|
||||
bufferutil:
|
||||
optional: true
|
||||
utf-8-validate:
|
||||
optional: true
|
||||
|
||||
yallist@5.0.0:
|
||||
resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==}
|
||||
engines: {node: '>=18'}
|
||||
@ -4240,6 +4292,48 @@ snapshots:
|
||||
|
||||
'@standard-schema/utils@0.3.0': {}
|
||||
|
||||
'@supabase/auth-js@2.75.0':
|
||||
dependencies:
|
||||
'@supabase/node-fetch': 2.6.15
|
||||
|
||||
'@supabase/functions-js@2.75.0':
|
||||
dependencies:
|
||||
'@supabase/node-fetch': 2.6.15
|
||||
|
||||
'@supabase/node-fetch@2.6.15':
|
||||
dependencies:
|
||||
whatwg-url: 5.0.0
|
||||
|
||||
'@supabase/postgrest-js@2.75.0':
|
||||
dependencies:
|
||||
'@supabase/node-fetch': 2.6.15
|
||||
|
||||
'@supabase/realtime-js@2.75.0':
|
||||
dependencies:
|
||||
'@supabase/node-fetch': 2.6.15
|
||||
'@types/phoenix': 1.6.6
|
||||
'@types/ws': 8.18.1
|
||||
ws: 8.18.3
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- utf-8-validate
|
||||
|
||||
'@supabase/storage-js@2.75.0':
|
||||
dependencies:
|
||||
'@supabase/node-fetch': 2.6.15
|
||||
|
||||
'@supabase/supabase-js@2.75.0':
|
||||
dependencies:
|
||||
'@supabase/auth-js': 2.75.0
|
||||
'@supabase/functions-js': 2.75.0
|
||||
'@supabase/node-fetch': 2.6.15
|
||||
'@supabase/postgrest-js': 2.75.0
|
||||
'@supabase/realtime-js': 2.75.0
|
||||
'@supabase/storage-js': 2.75.0
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- utf-8-validate
|
||||
|
||||
'@swc/helpers@0.5.15':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
@ -4357,6 +4451,8 @@ snapshots:
|
||||
|
||||
'@types/pako@2.0.4': {}
|
||||
|
||||
'@types/phoenix@1.6.6': {}
|
||||
|
||||
'@types/prop-types@15.7.15': {}
|
||||
|
||||
'@types/quill@1.3.10':
|
||||
@ -4382,6 +4478,10 @@ snapshots:
|
||||
|
||||
'@types/use-sync-external-store@0.0.6': {}
|
||||
|
||||
'@types/ws@8.18.1':
|
||||
dependencies:
|
||||
'@types/node': 22.18.5
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.45.0(@typescript-eslint/parser@8.45.0(eslint@9.36.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.36.0(jiti@2.5.1))(typescript@5.9.2)':
|
||||
dependencies:
|
||||
'@eslint-community/regexpp': 4.12.1
|
||||
@ -6328,6 +6428,8 @@ snapshots:
|
||||
dependencies:
|
||||
is-number: 7.0.0
|
||||
|
||||
tr46@0.0.3: {}
|
||||
|
||||
trim-canvas@0.1.2: {}
|
||||
|
||||
ts-api-utils@2.1.0(typescript@5.9.2):
|
||||
@ -6488,6 +6590,13 @@ snapshots:
|
||||
d3-time: 3.1.0
|
||||
d3-timer: 3.0.1
|
||||
|
||||
webidl-conversions@3.0.1: {}
|
||||
|
||||
whatwg-url@5.0.0:
|
||||
dependencies:
|
||||
tr46: 0.0.3
|
||||
webidl-conversions: 3.0.1
|
||||
|
||||
which-boxed-primitive@1.1.1:
|
||||
dependencies:
|
||||
is-bigint: 1.1.0
|
||||
@ -6535,6 +6644,8 @@ snapshots:
|
||||
|
||||
word-wrap@1.2.5: {}
|
||||
|
||||
ws@8.18.3: {}
|
||||
|
||||
yallist@5.0.0: {}
|
||||
|
||||
yocto-queue@0.1.0: {}
|
||||
|
||||
18
susconecta/public/forward-client-logs.js
Normal file
18
susconecta/public/forward-client-logs.js
Normal file
@ -0,0 +1,18 @@
|
||||
(function () {
|
||||
// Snippet to paste into browser console to forward console errors to the dev server
|
||||
const origError = console.error;
|
||||
window.__forwardClientLogs = true;
|
||||
|
||||
console.error = function (...args) {
|
||||
try {
|
||||
fetch('/api/client-logs', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ level: 'error', args: args, url: location.href, timestamp: new Date().toISOString() })
|
||||
}).catch(() => {})
|
||||
} catch (e) {}
|
||||
origError.apply(console, args);
|
||||
}
|
||||
|
||||
console.log('✅ Client log forwarder installed. console.error() will be forwarded to /api/client-logs')
|
||||
})();
|
||||
64
susconecta/src/app/api/assign-role/route.ts
Normal file
64
susconecta/src/app/api/assign-role/route.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { ENV_CONFIG } from '@/lib/env-config'
|
||||
|
||||
type Body = {
|
||||
user_id: string
|
||||
role: string
|
||||
}
|
||||
|
||||
async function getRequesterIdFromToken(token: string | null): Promise<string | null> {
|
||||
if (!token) return null
|
||||
try {
|
||||
const url = `${ENV_CONFIG.SUPABASE_URL}/auth/v1/user`
|
||||
const res = await fetch(url, { method: 'GET', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'apikey': ENV_CONFIG.SUPABASE_ANON_KEY, Authorization: `Bearer ${token}` } })
|
||||
if (!res.ok) return null
|
||||
const data = await res.json().catch(() => null)
|
||||
return data?.id ?? null
|
||||
} catch (err) {
|
||||
console.error('[assign-role] erro ao obter requester id', err)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const body = (await req.json()) as Body
|
||||
if (!body || !body.user_id || !body.role) return NextResponse.json({ error: 'user_id and role required' }, { status: 400 })
|
||||
|
||||
const authHeader = req.headers.get('authorization')
|
||||
const token = authHeader?.startsWith('Bearer ') ? authHeader.split(' ')[1] : null
|
||||
|
||||
const requesterId = await getRequesterIdFromToken(token)
|
||||
if (!requesterId) return NextResponse.json({ error: 'unauthenticated' }, { status: 401 })
|
||||
|
||||
// Check if requester is administrador
|
||||
const checkUrl = `${ENV_CONFIG.SUPABASE_URL}/rest/v1/user_roles?user_id=eq.${requesterId}&role=eq.administrador`
|
||||
const checkRes = await fetch(checkUrl, { method: 'GET', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', apikey: ENV_CONFIG.SUPABASE_ANON_KEY, Authorization: `Bearer ${token}` } })
|
||||
if (!checkRes.ok) return NextResponse.json({ error: 'forbidden' }, { status: 403 })
|
||||
const arr = await checkRes.json().catch(() => [])
|
||||
if (!Array.isArray(arr) || arr.length === 0) return NextResponse.json({ error: 'forbidden' }, { status: 403 })
|
||||
|
||||
// Insert role using service role key from environment (must be set on the server)
|
||||
const svcKey = process.env.SUPABASE_SERVICE_ROLE_KEY
|
||||
if (!svcKey) return NextResponse.json({ error: 'server misconfigured' }, { status: 500 })
|
||||
|
||||
const insertUrl = `${ENV_CONFIG.SUPABASE_URL}/rest/v1/user_roles`
|
||||
const insertRes = await fetch(insertUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Accept: 'application/json', apikey: svcKey, Authorization: `Bearer ${svcKey}` },
|
||||
body: JSON.stringify({ user_id: body.user_id, role: body.role }),
|
||||
})
|
||||
|
||||
if (!insertRes.ok) {
|
||||
const errBody = await insertRes.text().catch(() => null)
|
||||
console.error('[assign-role] insert failed', insertRes.status, errBody)
|
||||
return NextResponse.json({ error: 'failed to assign role', details: errBody }, { status: insertRes.status })
|
||||
}
|
||||
|
||||
const result = await insertRes.json().catch(() => null)
|
||||
return NextResponse.json({ ok: true, data: result })
|
||||
} catch (err) {
|
||||
console.error('[assign-role] unexpected error', err)
|
||||
return NextResponse.json({ error: 'internal error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@ -4,60 +4,66 @@ export interface ApiError {
|
||||
code?: string;
|
||||
}
|
||||
// Este arquivo foi renomeado de report.ts para report-types.ts para evitar confusão com outros arquivos de lógica.
|
||||
// Tipos para o endpoint de Relatórios Médicos
|
||||
// Tipos para o endpoint Supabase de Relatórios Médicos
|
||||
export interface Report {
|
||||
id: string;
|
||||
patient_id: string;
|
||||
doctor_id: string;
|
||||
report_type: string;
|
||||
chief_complaint: string;
|
||||
clinical_history: string;
|
||||
symptoms_and_signs: string;
|
||||
physical_examination: string;
|
||||
complementary_exams: string;
|
||||
exam_results: string;
|
||||
order_number: string;
|
||||
exam: string;
|
||||
diagnosis: string;
|
||||
prognosis?: string;
|
||||
treatment_performed: string;
|
||||
objective_recommendations: string;
|
||||
icd_code?: string;
|
||||
report_date: string;
|
||||
conclusion: string;
|
||||
cid_code: string;
|
||||
content_html: string;
|
||||
content_json: any;
|
||||
status: string;
|
||||
requested_by: string;
|
||||
due_at: string;
|
||||
hide_date: boolean;
|
||||
hide_signature: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
|
||||
// Dados expandidos (quando incluir dados relacionados)
|
||||
patient?: {
|
||||
id: string;
|
||||
full_name: string;
|
||||
cpf?: string;
|
||||
birth_date?: string;
|
||||
};
|
||||
|
||||
doctor?: {
|
||||
id: string;
|
||||
full_name: string;
|
||||
crm?: string;
|
||||
specialty?: string;
|
||||
};
|
||||
created_by: string;
|
||||
}
|
||||
|
||||
export interface CreateReportData {
|
||||
patient_id: string;
|
||||
order_number: string;
|
||||
exam: string;
|
||||
diagnosis: string;
|
||||
conclusion: string;
|
||||
cid_code: string;
|
||||
content_html: string;
|
||||
content_json: any;
|
||||
status: string;
|
||||
requested_by: string;
|
||||
due_at: string;
|
||||
hide_date: boolean;
|
||||
hide_signature: boolean;
|
||||
created_by: string;
|
||||
}
|
||||
|
||||
export interface UpdateReportData extends Partial<CreateReportData> {
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export type ReportsResponse = Report[];
|
||||
export type ReportResponse = Report;
|
||||
|
||||
// Dados para criar um novo relatório
|
||||
export interface CreateReportData {
|
||||
patient_id: string;
|
||||
doctor_id: string;
|
||||
report_type: string;
|
||||
chief_complaint: string;
|
||||
clinical_history: string;
|
||||
symptoms_and_signs: string;
|
||||
physical_examination: string;
|
||||
complementary_exams: string;
|
||||
exam_results: string;
|
||||
order_number: string;
|
||||
exam: string;
|
||||
diagnosis: string;
|
||||
prognosis?: string;
|
||||
treatment_performed: string;
|
||||
objective_recommendations: string;
|
||||
icd_code?: string;
|
||||
report_date: string;
|
||||
conclusion: string;
|
||||
cid_code: string;
|
||||
content_html: string;
|
||||
content_json: any;
|
||||
status: string;
|
||||
requested_by: string;
|
||||
due_at: string;
|
||||
hide_date: boolean;
|
||||
hide_signature: boolean;
|
||||
}
|
||||
|
||||
// Dados para atualizar um relatório existente
|
||||
@ -66,18 +72,6 @@ export interface UpdateReportData extends Partial<CreateReportData> {
|
||||
}
|
||||
|
||||
// Resposta da API ao listar relatórios
|
||||
export interface ReportsResponse {
|
||||
data: Report[];
|
||||
success: boolean;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
// Resposta da API ao criar/atualizar um relatório
|
||||
export interface ReportResponse {
|
||||
data: Report;
|
||||
success: boolean;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
// Dados do formulário (adaptado para a estrutura do front-end existente)
|
||||
export interface ReportFormData {
|
||||
@ -86,22 +80,19 @@ export interface ReportFormData {
|
||||
profissionalCrm: string;
|
||||
|
||||
// Identificação do Paciente
|
||||
pacienteId: string;
|
||||
pacienteNome: string;
|
||||
pacienteCpf: string;
|
||||
pacienteIdade: string;
|
||||
patient_id: string;
|
||||
report_type: string;
|
||||
symptoms_and_signs: string;
|
||||
diagnosis: string;
|
||||
prognosis?: string;
|
||||
treatment_performed: string;
|
||||
icd_code?: string;
|
||||
report_date: string;
|
||||
hipotesesDiagnosticas: string;
|
||||
condutaMedica: string;
|
||||
prescricoes: string;
|
||||
retornoAgendado: string;
|
||||
// cid10: string; // Removed, not present in schema
|
||||
|
||||
// Informações do Relatório
|
||||
motivoRelatorio: string;
|
||||
cid?: string;
|
||||
dataRelatorio: string;
|
||||
|
||||
// Histórico Clínico
|
||||
historicoClinico: string;
|
||||
|
||||
// Sinais, Sintomas e Exames
|
||||
sinaisSintomas: string;
|
||||
examesRealizados: string;
|
||||
resultadosExames: string;
|
||||
// ...restante do código...
|
||||
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user