Merge pull request 'fix/visual-adjustments' (#62) from fix/visual-adjustments into develop
Reviewed-on: #62
This commit is contained in:
commit
0122bd0fd0
@ -99,16 +99,16 @@ export default function NovoAgendamentoPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col bg-background">
|
||||
<div className="flex flex-col h-full bg-background">
|
||||
<HeaderAgenda />
|
||||
<main className="flex-1 mx-auto w-full max-w-7xl px-8 py-8">
|
||||
<CalendarRegistrationForm
|
||||
formData={formData as any}
|
||||
onFormChange={handleFormChange as any}
|
||||
createMode
|
||||
/>
|
||||
<main className="flex-1 mx-auto w-full max-w-7xl px-8 py-8 overflow-auto">
|
||||
<CalendarRegistrationForm
|
||||
formData={formData as any}
|
||||
onFormChange={handleFormChange as any}
|
||||
createMode
|
||||
/>
|
||||
</main>
|
||||
<FooterAgenda onSave={handleSave} onCancel={handleCancel} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { useEffect, useState, useCallback, useMemo } from "react";
|
||||
import {
|
||||
MoreHorizontal,
|
||||
PlusCircle,
|
||||
@ -87,6 +87,10 @@ export default function ConsultasPage() {
|
||||
// Local form state used when editing. Keep hook at top-level to avoid Hooks order changes.
|
||||
const [localForm, setLocalForm] = useState<any | null>(null);
|
||||
|
||||
// Paginação
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [itemsPerPage, setItemsPerPage] = useState(10);
|
||||
|
||||
const mapAppointmentToFormData = (appointment: any) => {
|
||||
// prefer scheduled_at (ISO) if available
|
||||
const scheduledBase = appointment.scheduled_at || appointment.time || appointment.created_at || null;
|
||||
@ -177,8 +181,8 @@ export default function ConsultasPage() {
|
||||
let duration_minutes = 30;
|
||||
try {
|
||||
if (formData.startTime && formData.endTime) {
|
||||
const [sh, sm] = String(formData.startTime).split(":").map((n: string) => Number(n));
|
||||
const [eh, em] = String(formData.endTime).split(":").map((n: string) => Number(n));
|
||||
const [sh, sm] = String(formData.startTime).split(":").map(Number);
|
||||
const [eh, em] = String(formData.endTime).split(":").map(Number);
|
||||
const start = (sh || 0) * 60 + (sm || 0);
|
||||
const end = (eh || 0) * 60 + (em || 0);
|
||||
if (!Number.isNaN(start) && !Number.isNaN(end) && end > start) duration_minutes = end - start;
|
||||
@ -404,12 +408,28 @@ export default function ConsultasPage() {
|
||||
performSearch(searchValue);
|
||||
}, 250);
|
||||
return () => clearTimeout(t);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [searchValue, originalAppointments]);
|
||||
|
||||
useEffect(() => {
|
||||
applyFilters();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedStatus, filterDate, originalAppointments]);
|
||||
|
||||
// Dados paginados
|
||||
const paginatedAppointments = useMemo(() => {
|
||||
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||
const endIndex = startIndex + itemsPerPage;
|
||||
return appointments.slice(startIndex, endIndex);
|
||||
}, [appointments, currentPage, itemsPerPage]);
|
||||
|
||||
const totalPages = Math.ceil(appointments.length / itemsPerPage);
|
||||
|
||||
// Reset para página 1 quando mudar a busca ou itens por página
|
||||
useEffect(() => {
|
||||
setCurrentPage(1);
|
||||
}, [searchValue, selectedStatus, filterDate, itemsPerPage]);
|
||||
|
||||
// Keep localForm synchronized with editingAppointment
|
||||
useEffect(() => {
|
||||
if (showForm && editingAppointment) {
|
||||
@ -437,7 +457,7 @@ export default function ConsultasPage() {
|
||||
</div>
|
||||
<CalendarRegistrationForm formData={localForm} onFormChange={onFormChange} createMode={true} />
|
||||
<div className="flex gap-2 justify-end">
|
||||
<Button variant="outline" onClick={handleCancel}>
|
||||
<Button variant="outline" className="hover:bg-primary/10 hover:text-primary dark:hover:bg-accent dark:hover:text-accent-foreground" onClick={handleCancel}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button onClick={saveLocal}>Salvar</Button>
|
||||
@ -515,7 +535,7 @@ export default function ConsultasPage() {
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{appointments.map((appointment) => {
|
||||
{paginatedAppointments.map((appointment) => {
|
||||
// appointment.professional may now contain the doctor's name (resolved)
|
||||
const professionalLookup = mockProfessionals.find((p) => p.id === appointment.professional);
|
||||
const professionalName = typeof appointment.professional === "string" && appointment.professional && !professionalLookup
|
||||
@ -574,6 +594,64 @@ export default function ConsultasPage() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Controles de paginação */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-muted-foreground">Itens por página:</span>
|
||||
<select
|
||||
value={itemsPerPage}
|
||||
onChange={(e) => setItemsPerPage(Number(e.target.value))}
|
||||
className="h-9 rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm focus:outline-none focus:ring-1 focus:ring-ring"
|
||||
>
|
||||
<option value={10}>10</option>
|
||||
<option value={15}>15</option>
|
||||
<option value={20}>20</option>
|
||||
</select>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Mostrando {paginatedAppointments.length > 0 ? (currentPage - 1) * itemsPerPage + 1 : 0} a{" "}
|
||||
{Math.min(currentPage * itemsPerPage, appointments.length)} de {appointments.length}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage(1)}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
Primeira
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage((prev) => Math.max(1, prev - 1))}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
Anterior
|
||||
</Button>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Página {currentPage} de {totalPages || 1}
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage((prev) => Math.min(totalPages, prev + 1))}
|
||||
disabled={currentPage === totalPages || totalPages === 0}
|
||||
>
|
||||
Próxima
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage(totalPages)}
|
||||
disabled={currentPage === totalPages || totalPages === 0}
|
||||
>
|
||||
Última
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{viewingAppointment && (
|
||||
<Dialog open={!!viewingAppointment} onOpenChange={() => setViewingAppointment(null)}>
|
||||
<DialogContent>
|
||||
|
||||
@ -283,15 +283,15 @@ export default function DashboardPage() {
|
||||
<Plus className="h-4 w-4" />
|
||||
Novo Paciente
|
||||
</Button>
|
||||
<Button onClick={() => router.push('/agenda')} variant="outline" className="gap-2">
|
||||
<Button onClick={() => router.push('/agenda')} variant="outline" className="gap-2 hover:bg-primary/10 hover:text-primary dark:hover:bg-accent dark:hover:text-accent-foreground">
|
||||
<Calendar className="h-4 w-4" />
|
||||
Novo Agendamento
|
||||
</Button>
|
||||
<Button onClick={() => setShowDoctorForm(true)} variant="outline" className="gap-2">
|
||||
<Button onClick={() => setShowDoctorForm(true)} variant="outline" className="gap-2 hover:bg-primary/10 hover:text-primary dark:hover:bg-accent dark:hover:text-accent-foreground">
|
||||
<Stethoscope className="h-4 w-4" />
|
||||
Novo Médico
|
||||
</Button>
|
||||
<Button onClick={() => router.push('/dashboard/relatorios')} variant="outline" className="gap-2">
|
||||
<Button onClick={() => router.push('/dashboard/relatorios')} variant="outline" className="gap-2 hover:bg-primary/10 hover:text-primary dark:hover:bg-accent dark:hover:text-accent-foreground">
|
||||
<FileText className="h-4 w-4" />
|
||||
Ver Relatórios
|
||||
</Button>
|
||||
|
||||
@ -102,7 +102,7 @@ export default function RelatoriosPage() {
|
||||
<div className="bg-card border border-border rounded-lg shadow p-6">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2"><BarChart2 className="w-5 h-5" /> Consultas por Período</h2>
|
||||
<Button size="sm" variant="outline" onClick={() => exportPDF("Consultas por Período", "Resumo das consultas realizadas por período.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
|
||||
<Button size="sm" variant="outline" className="hover:bg-primary/10 hover:text-primary dark:hover:bg-accent dark:hover:text-accent-foreground" onClick={() => exportPDF("Consultas por Período", "Resumo das consultas realizadas por período.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
|
||||
</div>
|
||||
<ResponsiveContainer width="100%" height={220}>
|
||||
<BarChart data={consultasPorPeriodo}>
|
||||
@ -119,7 +119,7 @@ export default function RelatoriosPage() {
|
||||
<div className="bg-card border border-border rounded-lg shadow p-6">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2"><DollarSign className="w-5 h-5" /> Faturamento Mensal</h2>
|
||||
<Button size="sm" variant="outline" onClick={() => exportPDF("Faturamento Mensal", "Resumo do faturamento mensal.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
|
||||
<Button size="sm" variant="outline" className="hover:bg-primary/10 hover:text-primary dark:hover:bg-accent dark:hover:text-accent-foreground" onClick={() => exportPDF("Faturamento Mensal", "Resumo do faturamento mensal.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
|
||||
</div>
|
||||
<ResponsiveContainer width="100%" height={220}>
|
||||
<LineChart data={faturamentoMensal}>
|
||||
@ -138,7 +138,7 @@ export default function RelatoriosPage() {
|
||||
<div className="bg-card border border-border rounded-lg shadow p-6">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2"><UserCheck className="w-5 h-5" /> Taxa de No-show</h2>
|
||||
<Button size="sm" variant="outline" onClick={() => exportPDF("Taxa de No-show", "Resumo da taxa de no-show.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
|
||||
<Button size="sm" variant="outline" className="hover:bg-primary/10 hover:text-primary dark:hover:bg-accent dark:hover:text-accent-foreground" onClick={() => exportPDF("Taxa de No-show", "Resumo da taxa de no-show.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
|
||||
</div>
|
||||
<ResponsiveContainer width="100%" height={220}>
|
||||
<LineChart data={taxaNoShow}>
|
||||
@ -155,7 +155,7 @@ export default function RelatoriosPage() {
|
||||
<div className="bg-card border border-border rounded-lg shadow p-6">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2"><ThumbsUp className="w-5 h-5" /> Satisfação dos Pacientes</h2>
|
||||
<Button size="sm" variant="outline" onClick={() => exportPDF("Satisfação dos Pacientes", "Resumo dos indicadores de satisfação.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
|
||||
<Button size="sm" variant="outline" className="hover:bg-primary/10 hover:text-primary dark:hover:bg-accent dark:hover:text-accent-foreground" onClick={() => exportPDF("Satisfação dos Pacientes", "Resumo dos indicadores de satisfação.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-center h-[220px]">
|
||||
<span className="text-5xl font-bold text-green-500">92%</span>
|
||||
@ -169,7 +169,7 @@ export default function RelatoriosPage() {
|
||||
<div className="bg-card border border-border rounded-lg shadow p-6">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2"><Users className="w-5 h-5" /> Pacientes Mais Atendidos</h2>
|
||||
<Button size="sm" variant="outline" onClick={() => exportPDF("Pacientes Mais Atendidos", "Lista dos pacientes mais atendidos.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
|
||||
<Button size="sm" variant="outline" className="hover:bg-primary/10 hover:text-primary dark:hover:bg-accent dark:hover:text-accent-foreground" onClick={() => exportPDF("Pacientes Mais Atendidos", "Lista dos pacientes mais atendidos.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
|
||||
</div>
|
||||
<table className="w-full text-sm mt-4">
|
||||
<thead>
|
||||
@ -193,7 +193,7 @@ export default function RelatoriosPage() {
|
||||
<div className="bg-card border border-border rounded-lg shadow p-6">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2"><Briefcase className="w-5 h-5" /> Médicos Mais Produtivos</h2>
|
||||
<Button size="sm" variant="outline" onClick={() => exportPDF("Médicos Mais Produtivos", "Lista dos médicos mais produtivos.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
|
||||
<Button size="sm" variant="outline" className="hover:bg-primary/10 hover:text-primary dark:hover:bg-accent dark:hover:text-accent-foreground" onClick={() => exportPDF("Médicos Mais Produtivos", "Lista dos médicos mais produtivos.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
|
||||
</div>
|
||||
<table className="w-full text-sm mt-4">
|
||||
<thead>
|
||||
@ -219,7 +219,7 @@ export default function RelatoriosPage() {
|
||||
<div className="bg-card border border-border rounded-lg shadow p-6">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2"><DollarSign className="w-5 h-5" /> Análise de Convênios</h2>
|
||||
<Button size="sm" variant="outline" onClick={() => exportPDF("Análise de Convênios", "Resumo da análise de convênios.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
|
||||
<Button size="sm" variant="outline" className="hover:bg-primary/10 hover:text-primary dark:hover:bg-accent dark:hover:text-accent-foreground" onClick={() => exportPDF("Análise de Convênios", "Resumo da análise de convênios.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
|
||||
</div>
|
||||
<ResponsiveContainer width="100%" height={220}>
|
||||
<PieChart>
|
||||
@ -238,7 +238,7 @@ export default function RelatoriosPage() {
|
||||
<div className="bg-card border border-border rounded-lg shadow p-6">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2"><TrendingUp className="w-5 h-5" /> Performance por Médico</h2>
|
||||
<Button size="sm" variant="outline" onClick={() => exportPDF("Performance por Médico", "Resumo da performance por médico.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
|
||||
<Button size="sm" variant="outline" className="hover:bg-primary/10 hover:text-primary dark:hover:bg-accent dark:hover:text-accent-foreground" onClick={() => exportPDF("Performance por Médico", "Resumo da performance por médico.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
|
||||
</div>
|
||||
<table className="w-full text-sm mt-4">
|
||||
<thead>
|
||||
|
||||
@ -141,6 +141,10 @@ export default function DoutoresPage() {
|
||||
const [searchMode, setSearchMode] = useState(false);
|
||||
const [searchTimeout, setSearchTimeout] = useState<NodeJS.Timeout | null>(null);
|
||||
|
||||
// Paginação
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [itemsPerPage, setItemsPerPage] = useState(10);
|
||||
|
||||
|
||||
async function load() {
|
||||
setLoading(true);
|
||||
@ -310,6 +314,20 @@ export default function DoutoresPage() {
|
||||
return filtered;
|
||||
}, [doctors, search, searchMode, searchResults]);
|
||||
|
||||
// Dados paginados
|
||||
const paginatedDoctors = useMemo(() => {
|
||||
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||
const endIndex = startIndex + itemsPerPage;
|
||||
return displayedDoctors.slice(startIndex, endIndex);
|
||||
}, [displayedDoctors, currentPage, itemsPerPage]);
|
||||
|
||||
const totalPages = Math.ceil(displayedDoctors.length / itemsPerPage);
|
||||
|
||||
// Reset para página 1 quando mudar a busca ou itens por página
|
||||
useEffect(() => {
|
||||
setCurrentPage(1);
|
||||
}, [search, itemsPerPage, searchMode]);
|
||||
|
||||
function handleAdd() {
|
||||
setEditingId(null);
|
||||
setShowForm(true);
|
||||
@ -480,8 +498,8 @@ export default function DoutoresPage() {
|
||||
Carregando…
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : displayedDoctors.length > 0 ? (
|
||||
displayedDoctors.map((doctor) => (
|
||||
) : paginatedDoctors.length > 0 ? (
|
||||
paginatedDoctors.map((doctor) => (
|
||||
<TableRow key={doctor.id}>
|
||||
<TableCell className="font-medium">{doctor.full_name}</TableCell>
|
||||
<TableCell>
|
||||
@ -580,6 +598,64 @@ export default function DoutoresPage() {
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* Controles de paginação */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-muted-foreground">Itens por página:</span>
|
||||
<select
|
||||
value={itemsPerPage}
|
||||
onChange={(e) => setItemsPerPage(Number(e.target.value))}
|
||||
className="h-9 rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm focus:outline-none focus:ring-1 focus:ring-ring"
|
||||
>
|
||||
<option value={10}>10</option>
|
||||
<option value={15}>15</option>
|
||||
<option value={20}>20</option>
|
||||
</select>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Mostrando {paginatedDoctors.length > 0 ? (currentPage - 1) * itemsPerPage + 1 : 0} a{" "}
|
||||
{Math.min(currentPage * itemsPerPage, displayedDoctors.length)} de {displayedDoctors.length}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage(1)}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
Primeira
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage((prev) => Math.max(1, prev - 1))}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
Anterior
|
||||
</Button>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Página {currentPage} de {totalPages || 1}
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage((prev) => Math.min(totalPages, prev + 1))}
|
||||
disabled={currentPage === totalPages || totalPages === 0}
|
||||
>
|
||||
Próxima
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage(totalPages)}
|
||||
disabled={currentPage === totalPages || totalPages === 0}
|
||||
>
|
||||
Última
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{viewingDoctor && (
|
||||
<Dialog open={!!viewingDoctor} onOpenChange={() => setViewingDoctor(null)}>
|
||||
<DialogContent>
|
||||
@ -753,7 +829,7 @@ export default function DoutoresPage() {
|
||||
)}
|
||||
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Mostrando {displayedDoctors.length} {searchMode ? 'resultado(s) da busca' : `de ${doctors.length}`}
|
||||
{searchMode ? 'Resultado(s) da busca' : `Total de ${doctors.length} médico(s)`}
|
||||
</div>
|
||||
{/* Dialog para pacientes atribuídos */}
|
||||
<Dialog open={assignedDialogOpen} onOpenChange={(open) => { if (!open) { setAssignedDialogOpen(false); setAssignedPatients([]); setAssignedDoctor(null); } }}>
|
||||
|
||||
@ -1,27 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Search, ChevronDown, Calculator, DollarSign } from "lucide-react";
|
||||
import { Plus } from "lucide-react";
|
||||
import { Calculator, DollarSign } from "lucide-react";
|
||||
import HeaderAgenda from "@/components/agenda/HeaderAgenda";
|
||||
import FooterAgenda from "@/components/agenda/FooterAgenda";
|
||||
|
||||
export default function FinanceiroPage() {
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const [bloqueio, setBloqueio] = useState(false);
|
||||
const [formaTipo, setFormaTipo] = useState("");
|
||||
const [parcelas, setParcelas] = useState("1");
|
||||
|
||||
const isAg = pathname?.startsWith("/agendamento");
|
||||
const isPr = pathname?.startsWith("/procedimento");
|
||||
const isFi = pathname?.startsWith("/financeiro");
|
||||
|
||||
const handleSave = () => {
|
||||
// Lógica de salvar será implementada
|
||||
@ -33,12 +22,11 @@ export default function FinanceiroPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full min-h-screen flex flex-col bg-background">
|
||||
{/* HEADER */}
|
||||
<div className="flex flex-col h-full bg-background">
|
||||
<HeaderAgenda />
|
||||
|
||||
{/* CORPO */}
|
||||
<main className="mx-auto w-full max-w-7xl px-8 py-6 space-y-6 flex-grow">
|
||||
<main className="mx-auto w-full max-w-7xl px-8 py-6 space-y-6 flex-1 overflow-auto">
|
||||
{/* INFORMAÇÕES FINANCEIRAS */}
|
||||
<section className="space-y-6">
|
||||
{/* Selo Financeiro */}
|
||||
@ -58,7 +46,7 @@ export default function FinanceiroPage() {
|
||||
Valor do Atendimento
|
||||
</Label>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-muted-foreground">Valor Particular</Label>
|
||||
<div className="relative">
|
||||
<DollarSign className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
@ -68,7 +56,7 @@ export default function FinanceiroPage() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-muted-foreground">Valor Convênio</Label>
|
||||
<div className="relative">
|
||||
<DollarSign className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
@ -90,7 +78,7 @@ export default function FinanceiroPage() {
|
||||
Forma de Pagamento
|
||||
</Label>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-muted-foreground">Tipo</Label>
|
||||
<select value={formaTipo} onChange={(e) => setFormaTipo(e.target.value)} className="h-10 w-full rounded-md border border-gray-300 dark:border-input bg-background text-foreground pr-8 pl-3 text-[13px] appearance-none transition-colors hover:bg-muted/30 hover:border-gray-400">
|
||||
<option value="">Selecionar</option>
|
||||
@ -100,7 +88,7 @@ export default function FinanceiroPage() {
|
||||
<option value="convenio">Convênio</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-muted-foreground">Parcelas</Label>
|
||||
<select className="h-10 w-full rounded-md border border-gray-300 dark:border-input bg-background text-foreground pr-8 pl-3 text-[13px] appearance-none transition-colors hover:bg-muted/30 hover:border-gray-400">
|
||||
<option value="1">1x</option>
|
||||
@ -111,7 +99,7 @@ export default function FinanceiroPage() {
|
||||
<option value="6">6x</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-muted-foreground">Desconto</Label>
|
||||
<div className="relative">
|
||||
<Calculator className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
@ -156,4 +144,4 @@ export default function FinanceiroPage() {
|
||||
<FooterAgenda onSave={handleSave} onCancel={handleCancel} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -49,6 +49,10 @@ export default function PacientesPage() {
|
||||
const [viewingPatient, setViewingPatient] = useState<Paciente | null>(null);
|
||||
const [assignDialogOpen, setAssignDialogOpen] = useState(false);
|
||||
const [assignPatientId, setAssignPatientId] = useState<string | null>(null);
|
||||
|
||||
// Paginação
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [itemsPerPage, setItemsPerPage] = useState(10);
|
||||
|
||||
async function loadAll() {
|
||||
try {
|
||||
@ -95,6 +99,20 @@ export default function PacientesPage() {
|
||||
});
|
||||
}, [patients, search]);
|
||||
|
||||
// Dados paginados
|
||||
const paginatedData = useMemo(() => {
|
||||
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||
const endIndex = startIndex + itemsPerPage;
|
||||
return filtered.slice(startIndex, endIndex);
|
||||
}, [filtered, currentPage, itemsPerPage]);
|
||||
|
||||
const totalPages = Math.ceil(filtered.length / itemsPerPage);
|
||||
|
||||
// Reset para página 1 quando mudar a busca ou itens por página
|
||||
useEffect(() => {
|
||||
setCurrentPage(1);
|
||||
}, [search, itemsPerPage]);
|
||||
|
||||
function handleAdd() {
|
||||
setEditingId(null);
|
||||
setShowForm(true);
|
||||
@ -228,8 +246,8 @@ export default function PacientesPage() {
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filtered.length > 0 ? (
|
||||
filtered.map((p) => (
|
||||
{paginatedData.length > 0 ? (
|
||||
paginatedData.map((p) => (
|
||||
<TableRow key={p.id}>
|
||||
<TableCell className="font-medium">{p.full_name || "(sem nome)"}</TableCell>
|
||||
<TableCell>{p.cpf || "-"}</TableCell>
|
||||
@ -277,6 +295,64 @@ export default function PacientesPage() {
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* Controles de paginação */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-muted-foreground">Itens por página:</span>
|
||||
<select
|
||||
value={itemsPerPage}
|
||||
onChange={(e) => setItemsPerPage(Number(e.target.value))}
|
||||
className="h-9 rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm focus:outline-none focus:ring-1 focus:ring-ring"
|
||||
>
|
||||
<option value={10}>10</option>
|
||||
<option value={15}>15</option>
|
||||
<option value={20}>20</option>
|
||||
</select>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Mostrando {paginatedData.length > 0 ? (currentPage - 1) * itemsPerPage + 1 : 0} a{" "}
|
||||
{Math.min(currentPage * itemsPerPage, filtered.length)} de {filtered.length}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage(1)}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
Primeira
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage((prev) => Math.max(1, prev - 1))}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
Anterior
|
||||
</Button>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Página {currentPage} de {totalPages || 1}
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage((prev) => Math.min(totalPages, prev + 1))}
|
||||
disabled={currentPage === totalPages || totalPages === 0}
|
||||
>
|
||||
Próxima
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage(totalPages)}
|
||||
disabled={currentPage === totalPages || totalPages === 0}
|
||||
>
|
||||
Última
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{viewingPatient && (
|
||||
<Dialog open={!!viewingPatient} onOpenChange={() => setViewingPatient(null)}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
@ -326,8 +402,6 @@ export default function PacientesPage() {
|
||||
onSaved={() => { setAssignDialogOpen(false); setAssignPatientId(null); loadAll(); }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="text-sm text-muted-foreground">Mostrando {filtered.length} de {patients.length}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -115,7 +115,7 @@ export default function PerfilPage() {
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => router.back()}
|
||||
className="mt-4"
|
||||
className="mt-4 hover:bg-primary/10 hover:text-primary dark:hover:bg-accent dark:hover:text-accent-foreground"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||
Voltar
|
||||
@ -641,6 +641,7 @@ export default function PerfilPage() {
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => router.back()}
|
||||
className="hover:bg-primary/10 hover:text-primary dark:hover:bg-accent dark:hover:text-accent-foreground"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||
Voltar
|
||||
|
||||
@ -1,25 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Search, ChevronDown, RotateCcw } from "lucide-react";
|
||||
import { Search, ChevronDown } from "lucide-react";
|
||||
import { Plus } from "lucide-react";
|
||||
import HeaderAgenda from "@/components/agenda/HeaderAgenda";
|
||||
import FooterAgenda from "@/components/agenda/FooterAgenda";
|
||||
|
||||
export default function ProcedimentoPage() {
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const [bloqueio, setBloqueio] = useState(false);
|
||||
|
||||
const isAg = pathname?.startsWith("/agendamento");
|
||||
const isPr = pathname?.startsWith("/procedimento");
|
||||
const isFi = pathname?.startsWith("/financeiro");
|
||||
|
||||
const handleSave = () => {
|
||||
// Lógica de salvar será implementada
|
||||
@ -30,20 +22,12 @@ export default function ProcedimentoPage() {
|
||||
router.push("/calendar");
|
||||
};
|
||||
|
||||
const tab = (active: boolean, extra = "") =>
|
||||
`px-4 py-1.5 text-[13px] border ${
|
||||
active
|
||||
? "border-sky-500 bg-sky-50 dark:bg-sky-900/30 text-sky-700 dark:text-sky-300 font-medium"
|
||||
: "text-muted-foreground hover:bg-muted border-border"
|
||||
} ${extra}`;
|
||||
|
||||
return (
|
||||
<div className="w-full min-h-screen flex flex-col bg-background">
|
||||
{/* HEADER */}
|
||||
<div className="flex flex-col h-full bg-background">
|
||||
<HeaderAgenda />
|
||||
|
||||
{/* CORPO */}
|
||||
<main className="mx-auto w-full max-w-7xl px-8 py-6 space-y-6 flex-grow">
|
||||
<main className="mx-auto w-full max-w-7xl px-8 py-6 space-y-6 flex-1 overflow-auto">
|
||||
{/* ATENDIMENTOS */}
|
||||
<section className="space-y-6">
|
||||
{/* Selo Atendimento com + dentro da bolinha */}
|
||||
@ -558,72 +558,21 @@ export default function PacientePage() {
|
||||
</header>
|
||||
|
||||
<div className="space-y-6 rounded-lg border border-border bg-muted/40 p-6">
|
||||
<div className="space-y-3">
|
||||
<Label>Tipo de consulta</Label>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<Button
|
||||
type="button"
|
||||
className={tipoConsulta === 'teleconsulta' ? activeToggleClass : inactiveToggleClass}
|
||||
aria-pressed={tipoConsulta === 'teleconsulta'}
|
||||
onClick={() => setTipoConsulta('teleconsulta')}
|
||||
>
|
||||
Teleconsulta
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
className={tipoConsulta === 'presencial' ? activeToggleClass : inactiveToggleClass}
|
||||
aria-pressed={tipoConsulta === 'presencial'}
|
||||
onClick={() => setTipoConsulta('presencial')}
|
||||
>
|
||||
Consulta no local
|
||||
</Button>
|
||||
</div>
|
||||
{/* Remover campos de especialidade e localização, deixar só o botão centralizado */}
|
||||
<div className="flex justify-center">
|
||||
<Button asChild className={`w-full md:w-40 ${hoverPrimaryClass}`}>
|
||||
<Link href={buildResultadosHref()} prefetch={false}>
|
||||
Pesquisar
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label>Especialidade</Label>
|
||||
<Select value={especialidade} onValueChange={setEspecialidade}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Selecione a especialidade" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="cardiologia">Cardiologia</SelectItem>
|
||||
<SelectItem value="pediatria">Pediatria</SelectItem>
|
||||
<SelectItem value="dermatologia">Dermatologia</SelectItem>
|
||||
<SelectItem value="ortopedia">Ortopedia</SelectItem>
|
||||
<SelectItem value="ginecologia">Ginecologia</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Localização (opcional)</Label>
|
||||
<div className="relative">
|
||||
<MapPin className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
value={localizacao}
|
||||
onChange={event => setLocalizacao(event.target.value)}
|
||||
placeholder="Cidade ou estado"
|
||||
className="pl-9"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Botão agora redireciona direto para /resultados */}
|
||||
<Button asChild className={`w-full md:w-auto md:self-start ${hoverPrimaryClass}`}>
|
||||
<Link href={buildResultadosHref()} prefetch={false}>
|
||||
Pesquisar
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="transition duration-200 bg-white text-[#1e293b] border border-black/10 rounded-md shadow-[0_2px_6px_rgba(0,0,0,0.03)] hover:bg-[#2563eb] hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#2563eb] dark:bg-slate-800 dark:text-slate-100 dark:border-white/10 dark:hover:bg-[#2563eb] dark:hover:text-white"
|
||||
className="transition duration-200 bg-[#2563eb] text-white border border-[#2563eb]/40 rounded-md shadow-[0_2px_6px_rgba(0,0,0,0.03)] hover:bg-[#1e40af] hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#2563eb]/60 dark:bg-[#2563eb] dark:text-white dark:border-[#2563eb]/50 dark:hover:bg-[#1e40af]"
|
||||
onClick={() => setMostrarAgendadas(true)}
|
||||
>
|
||||
Ver consultas agendadas
|
||||
@ -848,7 +797,7 @@ export default function PacientePage() {
|
||||
<div className="text-sm text-muted-foreground">Data: {new Date(r.report_date || r.created_at || Date.now()).toLocaleDateString('pt-BR')}</div>
|
||||
</div>
|
||||
<div className="flex gap-2 mt-2 md:mt-0">
|
||||
<Button variant="outline" onClick={async () => { setSelectedReport(r); }}>{strings.visualizarLaudo}</Button>
|
||||
<Button variant="outline" className="hover:bg-primary/10 hover:text-primary dark:hover:bg-accent dark:hover:text-accent-foreground" onClick={async () => { setSelectedReport(r); }}>{strings.visualizarLaudo}</Button>
|
||||
<Button variant="secondary" onClick={async () => { try { await navigator.clipboard.writeText(JSON.stringify(r)); setToast({ type: 'success', msg: 'Laudo copiado.' }) } catch { setToast({ type: 'error', msg: 'Falha ao copiar.' }) } }}>{strings.compartilhar}</Button>
|
||||
</div>
|
||||
</div>
|
||||
@ -940,9 +889,15 @@ export default function PacientePage() {
|
||||
Editar Perfil
|
||||
</Button>
|
||||
) : (
|
||||
<div className="flex gap-2">
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={handleSaveProfile} className="flex items-center gap-2">Salvar</Button>
|
||||
<Button variant="outline" onClick={handleCancelEdit}>Cancelar</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleCancelEdit}
|
||||
className="transition duration-200 hover:bg-primary/10 hover:text-primary dark:hover:bg-accent dark:hover:text-accent-foreground"
|
||||
>
|
||||
Cancelar
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -1042,11 +997,39 @@ export default function PacientePage() {
|
||||
<div className="flex flex-1 min-h-0">
|
||||
{/* Sidebar vertical */}
|
||||
<nav aria-label="Navegação do dashboard" className="w-56 bg-card border-r flex flex-col py-6 px-2 gap-2">
|
||||
<Button variant={tab==='dashboard'?'secondary':'ghost'} aria-current={tab==='dashboard'} onClick={()=>setTab('dashboard')} className="justify-start"><Calendar className="mr-2 h-5 w-5" />{strings.dashboard}</Button>
|
||||
<Button variant={tab==='consultas'?'secondary':'ghost'} aria-current={tab==='consultas'} onClick={()=>setTab('consultas')} className="justify-start"><Calendar className="mr-2 h-5 w-5" />{strings.consultas}</Button>
|
||||
<Button variant={tab==='exames'?'secondary':'ghost'} aria-current={tab==='exames'} onClick={()=>setTab('exames')} className="justify-start"><FileText className="mr-2 h-5 w-5" />{strings.exames}</Button>
|
||||
<Button
|
||||
variant={tab==='dashboard'?'secondary':'ghost'}
|
||||
aria-current={tab==='dashboard'}
|
||||
onClick={()=>setTab('dashboard')}
|
||||
className={`justify-start ${tab==='dashboard' ? 'bg-primary/10 text-primary' : ''}`}
|
||||
>
|
||||
<Calendar className="mr-2 h-5 w-5" />{strings.dashboard}
|
||||
</Button>
|
||||
<Button
|
||||
variant={tab==='consultas'?'secondary':'ghost'}
|
||||
aria-current={tab==='consultas'}
|
||||
onClick={()=>setTab('consultas')}
|
||||
className={`justify-start ${tab==='consultas' ? 'bg-primary/10 text-primary' : ''}`}
|
||||
>
|
||||
<Calendar className="mr-2 h-5 w-5" />{strings.consultas}
|
||||
</Button>
|
||||
<Button
|
||||
variant={tab==='exames'?'secondary':'ghost'}
|
||||
aria-current={tab==='exames'}
|
||||
onClick={()=>setTab('exames')}
|
||||
className={`justify-start ${tab==='exames' ? 'bg-primary/10 text-primary' : ''}`}
|
||||
>
|
||||
<FileText className="mr-2 h-5 w-5" />{strings.exames}
|
||||
</Button>
|
||||
|
||||
<Button variant={tab==='perfil'?'secondary':'ghost'} aria-current={tab==='perfil'} onClick={()=>setTab('perfil')} className="justify-start"><UserCog className="mr-2 h-5 w-5" />{strings.perfil}</Button>
|
||||
<Button
|
||||
variant={tab==='perfil'?'secondary':'ghost'}
|
||||
aria-current={tab==='perfil'}
|
||||
onClick={()=>setTab('perfil')}
|
||||
className={`justify-start ${tab==='perfil' ? 'bg-primary/10 text-primary' : ''}`}
|
||||
>
|
||||
<UserCog className="mr-2 h-5 w-5" />{strings.perfil}
|
||||
</Button>
|
||||
</nav>
|
||||
{/* Conteúdo principal */}
|
||||
<div className="flex-1 min-w-0 p-4 max-w-4xl mx-auto w-full">
|
||||
|
||||
@ -900,7 +900,7 @@ const ProfissionalPage = () => {
|
||||
variant={selectedRange === 'todos' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setSelectedRange('todos')}
|
||||
className="hover:bg-blue-50"
|
||||
className="hover:bg-primary/10 hover:text-primary"
|
||||
>
|
||||
Todos
|
||||
</Button>
|
||||
@ -908,7 +908,7 @@ const ProfissionalPage = () => {
|
||||
variant={selectedRange === 'semana' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setSelectedRange('semana')}
|
||||
className="hover:bg-blue-50"
|
||||
className="hover:bg-primary/10 hover:text-primary"
|
||||
>
|
||||
Semana
|
||||
</Button>
|
||||
@ -916,7 +916,7 @@ const ProfissionalPage = () => {
|
||||
variant={selectedRange === 'mes' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setSelectedRange('mes')}
|
||||
className="hover:bg-blue-50"
|
||||
className="hover:bg-primary/10 hover:text-primary"
|
||||
>
|
||||
Mês
|
||||
</Button>
|
||||
@ -2191,25 +2191,25 @@ const ProfissionalPage = () => {
|
||||
title="Cor da fonte"
|
||||
/>
|
||||
{/* Alinhamento */}
|
||||
<Button variant="outline" size="sm" onClick={() => formatText('align-left')} title="Alinhar à esquerda" className="px-1"><svg width="16" height="16" fill="none"><rect x="2" y="4" width="12" height="2" rx="1" fill="currentColor"/><rect x="2" y="7" width="8" height="2" rx="1" fill="currentColor"/><rect x="2" y="10" width="10" height="2" rx="1" fill="currentColor"/></svg></Button>
|
||||
<Button variant="outline" size="sm" onClick={() => formatText('align-center')} title="Centralizar" className="px-1"><svg width="16" height="16" fill="none"><rect x="4" y="4" width="8" height="2" rx="1" fill="currentColor"/><rect x="2" y="7" width="12" height="2" rx="1" fill="currentColor"/><rect x="3" y="10" width="10" height="2" rx="1" fill="currentColor"/></svg></Button>
|
||||
<Button variant="outline" size="sm" onClick={() => formatText('align-right')} title="Alinhar à direita" className="px-1"><svg width="16" height="16" fill="none"><rect x="6" y="4" width="8" height="2" rx="1" fill="currentColor"/><rect x="2" y="7" width="12" height="2" rx="1" fill="currentColor"/><rect x="4" y="10" width="10" height="2" rx="1" fill="currentColor"/></svg></Button>
|
||||
<Button variant="outline" size="sm" onClick={() => formatText('align-justify')} title="Justificar" className="px-1"><svg width="16" height="16" fill="none"><rect x="2" y="4" width="12" height="2" rx="1" fill="currentColor"/><rect x="2" y="7" width="12" height="2" rx="1" fill="currentColor"/><rect x="2" y="10" width="12" height="2" rx="1" fill="currentColor"/></svg></Button>
|
||||
<Button variant="outline" size="sm" onClick={() => formatText('align-left')} title="Alinhar à esquerda" className="px-1 hover:bg-primary/10 hover:text-primary"><svg width="16" height="16" fill="none"><rect x="2" y="4" width="12" height="2" rx="1" fill="currentColor"/><rect x="2" y="7" width="8" height="2" rx="1" fill="currentColor"/><rect x="2" y="10" width="10" height="2" rx="1" fill="currentColor"/></svg></Button>
|
||||
<Button variant="outline" size="sm" onClick={() => formatText('align-center')} title="Centralizar" className="px-1 hover:bg-primary/10 hover:text-primary"><svg width="16" height="16" fill="none"><rect x="4" y="4" width="8" height="2" rx="1" fill="currentColor"/><rect x="2" y="7" width="12" height="2" rx="1" fill="currentColor"/><rect x="3" y="10" width="10" height="2" rx="1" fill="currentColor"/></svg></Button>
|
||||
<Button variant="outline" size="sm" onClick={() => formatText('align-right')} title="Alinhar à direita" className="px-1 hover:bg-primary/10 hover:text-primary"><svg width="16" height="16" fill="none"><rect x="6" y="4" width="8" height="2" rx="1" fill="currentColor"/><rect x="2" y="7" width="12" height="2" rx="1" fill="currentColor"/><rect x="4" y="10" width="10" height="2" rx="1" fill="currentColor"/></svg></Button>
|
||||
<Button variant="outline" size="sm" onClick={() => formatText('align-justify')} title="Justificar" className="px-1 hover:bg-primary/10 hover:text-primary"><svg width="16" height="16" fill="none"><rect x="2" y="4" width="12" height="2" rx="1" fill="currentColor"/><rect x="2" y="7" width="12" height="2" rx="1" fill="currentColor"/><rect x="2" y="10" width="12" height="2" rx="1" fill="currentColor"/></svg></Button>
|
||||
{/* Listas */}
|
||||
<Button variant="outline" size="sm" onClick={() => formatText('list-ol')} title="Lista numerada" className="px-1">1.</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => formatText('list-ul')} title="Lista com marcadores" className="px-1">•</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => formatText('list-ol')} title="Lista numerada" className="px-1 hover:bg-primary/10 hover:text-primary">1.</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => formatText('list-ul')} title="Lista com marcadores" className="px-1 hover:bg-primary/10 hover:text-primary">•</Button>
|
||||
{/* Recuo */}
|
||||
<Button variant="outline" size="sm" onClick={() => formatText('indent')} title="Aumentar recuo" className="px-1">→</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => formatText('outdent')} title="Diminuir recuo" className="px-1">←</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => formatText('indent')} title="Aumentar recuo" className="px-1 hover:bg-primary/10 hover:text-primary">→</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => formatText('outdent')} title="Diminuir recuo" className="px-1 hover:bg-primary/10 hover:text-primary">←</Button>
|
||||
{/* Desfazer/Refazer */}
|
||||
<Button variant="outline" size="sm" onClick={handleUndo} title="Desfazer" className="px-1">↺</Button>
|
||||
<Button variant="outline" size="sm" onClick={handleUndo} title="Desfazer" className="px-1 hover:bg-primary/10 hover:text-primary">↺</Button>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{templates.map((template, idx) => (
|
||||
<Button
|
||||
key={idx}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-xs h-auto p-1 px-2"
|
||||
className="text-xs h-auto p-1 px-2 hover:bg-primary/10 hover:text-primary"
|
||||
onClick={() => insertTemplate(template)}
|
||||
>
|
||||
{template.substring(0, 30)}...
|
||||
@ -2779,7 +2779,7 @@ const ProfissionalPage = () => {
|
||||
</Avatar>
|
||||
{isEditingProfile && (
|
||||
<div className="space-y-2">
|
||||
<Button variant="outline" size="sm">
|
||||
<Button variant="outline" size="sm" className="hover:bg-primary/10 hover:text-primary dark:hover:bg-accent dark:hover:text-accent-foreground">
|
||||
Alterar Foto
|
||||
</Button>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
|
||||
@ -617,7 +617,7 @@ export default function ResultadosClient() {
|
||||
</Toggle>
|
||||
|
||||
<Select value={convenio} onValueChange={setConvenio}>
|
||||
<SelectTrigger className="h-10 min-w-[180px] rounded-full border border-primary/40 bg-primary/10 text-primary hover:bg-primary hover:text-primary-foreground">
|
||||
<SelectTrigger className="h-10 min-w-[180px] rounded-full border border-primary/40 bg-primary/10 text-primary transition duration-200 hover:bg-primary/10 hover:text-primary">
|
||||
<SelectValue placeholder="Convênio" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@ -631,7 +631,7 @@ export default function ResultadosClient() {
|
||||
</Select>
|
||||
|
||||
<Select value={bairro} onValueChange={setBairro}>
|
||||
<SelectTrigger className="h-10 min-w-[160px] rounded-full border border-primary/40 bg-primary/10 text-primary hover:bg-primary hover:text-primary-foreground">
|
||||
<SelectTrigger className="h-10 min-w-[160px] rounded-full border border-primary/40 bg-primary/10 text-primary transition duration-200 hover:bg-primary/10 hover:text-primary">
|
||||
<SelectValue placeholder="Bairro" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@ -644,7 +644,7 @@ export default function ResultadosClient() {
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
className="rounded-full border border-primary/40 bg-primary/10 text-primary hover:bg-primary hover:text-primary-foreground"
|
||||
className="rounded-full border border-primary/40 bg-primary/10 text-primary transition duration-200 hover:bg-primary/10 hover:text-primary"
|
||||
>
|
||||
<Filter className="mr-2 h-4 w-4" />
|
||||
Mais filtros
|
||||
@ -782,7 +782,7 @@ export default function ResultadosClient() {
|
||||
>
|
||||
Agendar consulta
|
||||
</Button>
|
||||
<Button variant="outline" className="h-11 rounded-full border-primary/40 bg-primary/10 text-primary hover:bg-primary hover:text-primary-foreground">
|
||||
<Button variant="outline" className="h-11 rounded-full border-primary/40 bg-primary/10 text-primary transition duration-200 hover:bg-primary/10 hover:text-primary">
|
||||
Enviar mensagem
|
||||
</Button>
|
||||
<Button
|
||||
|
||||
@ -1080,8 +1080,8 @@ export function CalendarRegistrationForm({ formData, onFormChange, createMode =
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-3 mt-3">
|
||||
<div>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[13px]">Status</Label>
|
||||
<select name="status" className="h-11 w-full rounded-md border border-gray-300 dark:border-input bg-background text-foreground pr-3 text-[13px]" value={formData.status || ''} onChange={handleChange}>
|
||||
<option value="">Selecione</option>
|
||||
@ -1094,50 +1094,50 @@ export function CalendarRegistrationForm({ formData, onFormChange, createMode =
|
||||
<option value="no_show">Não compareceu</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[13px]">Duração (min)</Label>
|
||||
<Input name="duration_minutes" type="number" min={1} className="h-11 w-full rounded-md" value={formData.duration_minutes ?? ''} onChange={handleChange} readOnly={lockedDurationFromSlot} disabled={lockedDurationFromSlot} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[13px]">Convênio</Label>
|
||||
<Input name="insurance_provider" placeholder="Operadora" className="h-11 w-full rounded-md" value={formData.insurance_provider || ''} onChange={handleChange} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-2 mt-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-[13px]">Observações</Label>
|
||||
|
||||
</div>
|
||||
<Textarea name="notes" rows={4} className="text-[13px] min-h-[80px] resize-none rounded-md transition-colors hover:bg-muted/30" value={formData.notes || ''} onChange={handleChange} />
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-3">
|
||||
<div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[13px]">Queixa principal</Label>
|
||||
<Textarea name="chief_complaint" rows={3} className="text-[13px] rounded-md" value={formData.chief_complaint || ''} onChange={handleChange} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[13px]">Notas do paciente</Label>
|
||||
<Textarea name="patient_notes" rows={3} className="text-[13px] rounded-md" value={formData.patient_notes || ''} onChange={handleChange} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-3 mt-3">
|
||||
<div>
|
||||
<div className="grid grid-cols-3 gap-3 mt-4">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[13px]">Horário de check-in</Label>
|
||||
<Input name="checked_in_at" type="datetime-local" className="h-11 w-full rounded-md" value={isoToDatetimeLocal(formData.checked_in_at as any)} onChange={handleChange} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[13px]">Concluído em</Label>
|
||||
<Input name="completed_at" type="datetime-local" className="h-11 w-full rounded-md" value={isoToDatetimeLocal(formData.completed_at as any)} onChange={handleChange} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[13px]">Cancelado em</Label>
|
||||
<Input name="cancelled_at" type="datetime-local" className="h-11 w-full rounded-md" value={isoToDatetimeLocal(formData.cancelled_at as any)} onChange={handleChange} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3">
|
||||
<div className="mt-4 space-y-1">
|
||||
<Label className="text-[13px]">Motivo do cancelamento</Label>
|
||||
<Input name="cancellation_reason" className="h-11 w-full rounded-md" value={formData.cancellation_reason || ''} onChange={handleChange} />
|
||||
</div>
|
||||
|
||||
@ -935,14 +935,6 @@ async function handleSubmit(ev: React.FormEvent) {
|
||||
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Celular</Label>
|
||||
<Input
|
||||
value={form.celular}
|
||||
onChange={(e) => setField("celular", formatPhone(e.target.value))}
|
||||
placeholder="(XX) XXXXX-XXXX"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Contato de Emergência</Label>
|
||||
<Input
|
||||
@ -1165,7 +1157,7 @@ async function handleSubmit(ev: React.FormEvent) {
|
||||
</Collapsible>
|
||||
|
||||
<div className="flex justify-end gap-4 pt-6 border-t">
|
||||
<Button type="button" variant="outline" onClick={() => (inline ? onClose?.() : onOpenChange?.(false))} disabled={isSubmitting}>
|
||||
<Button type="button" variant="outline" className="hover:bg-primary/10 hover:text-primary dark:hover:bg-accent dark:hover:text-accent-foreground" onClick={() => (inline ? onClose?.() : onOpenChange?.(false))} disabled={isSubmitting}>
|
||||
<XCircle className="mr-2 h-4 w-4" />
|
||||
Cancelar
|
||||
</Button>
|
||||
|
||||
@ -98,6 +98,30 @@ export function PatientRegistrationForm({
|
||||
const [form, setForm] = useState<FormData>(initial);
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
const [expanded, setExpanded] = useState({ dados: true, contato: false, endereco: false, obs: false });
|
||||
|
||||
// Funções de formatação
|
||||
const formatRG = (value: string) => {
|
||||
const cleaned = value.replace(/\D/g, '');
|
||||
if (cleaned.length <= 9) {
|
||||
return cleaned.replace(/(\d{2})(\d{3})(\d{3})(\d{1})/, '$1.$2.$3-$4');
|
||||
}
|
||||
return cleaned.slice(0, 9);
|
||||
};
|
||||
|
||||
const formatTelefone = (value: string) => {
|
||||
const cleaned = value.replace(/\D/g, '');
|
||||
if (cleaned.length <= 10) {
|
||||
return cleaned.replace(/(\d{2})(\d{4})(\d{4})/, '($1) $2-$3');
|
||||
}
|
||||
return cleaned.replace(/(\d{2})(\d{5})(\d{4})/, '($1) $2-$3');
|
||||
};
|
||||
|
||||
const formatDataNascimento = (value: string) => {
|
||||
const cleaned = value.replace(/\D/g, '');
|
||||
if (cleaned.length <= 2) return cleaned;
|
||||
if (cleaned.length <= 4) return `${cleaned.slice(0, 2)}/${cleaned.slice(2)}`;
|
||||
return `${cleaned.slice(0, 2)}/${cleaned.slice(2, 4)}/${cleaned.slice(4, 8)}`;
|
||||
};
|
||||
const [isSubmitting, setSubmitting] = useState(false);
|
||||
const [isUploadingPhoto, setUploadingPhoto] = useState(false);
|
||||
const [isSearchingCEP, setSearchingCEP] = useState(false);
|
||||
@ -362,7 +386,7 @@ export function PatientRegistrationForm({
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2"><Label>CPF *</Label><Input value={form.cpf} onChange={(e) => handleCPFChange(e.target.value)} placeholder="000.000.000-00" maxLength={14} className={errors.cpf ? "border-destructive" : ""} />{errors.cpf && <p className="text-sm text-destructive">{errors.cpf}</p>}</div>
|
||||
<div className="space-y-2"><Label>RG</Label><Input value={form.rg} onChange={(e) => setField("rg", e.target.value)} /></div>
|
||||
<div className="space-y-2"><Label>RG</Label><Input value={form.rg} onChange={(e) => setField("rg", formatRG(e.target.value))} placeholder="00.000.000-0" maxLength={12} /></div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
@ -372,7 +396,7 @@ export function PatientRegistrationForm({
|
||||
<SelectContent><SelectItem value="masculino">Masculino</SelectItem><SelectItem value="feminino">Feminino</SelectItem><SelectItem value="outro">Outro</SelectItem></SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2"><Label>Data de Nascimento</Label><Input placeholder="dd/mm/aaaa" value={form.birth_date} onChange={(e) => { const v = e.target.value.replace(/[^0-9\/]*/g, "").slice(0, 10); setField("birth_date", v); }} onBlur={() => { const raw = form.birth_date; const parts = raw.split(/\D+/).filter(Boolean); if (parts.length === 3) { const d = `${parts[0].padStart(2,'0')}/${parts[1].padStart(2,'0')}/${parts[2].padStart(4,'0')}`; setField("birth_date", d); } }} /></div>
|
||||
<div className="space-y-2"><Label>Data de Nascimento</Label><Input placeholder="dd/mm/aaaa" value={form.birth_date} onChange={(e) => setField("birth_date", formatDataNascimento(e.target.value))} maxLength={10} /></div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</CollapsibleContent>
|
||||
@ -388,7 +412,7 @@ export function PatientRegistrationForm({
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<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><Input value={form.telefone} onChange={(e) => setField("telefone", e.target.value)} />{errors.telefone && <p className="text-sm text-destructive">{errors.telefone}</p>}</div>
|
||||
<div className="space-y-2"><Label>Telefone</Label><Input value={form.telefone} onChange={(e) => setField("telefone", formatTelefone(e.target.value))} placeholder="(00) 00000-0000" maxLength={15} />{errors.telefone && <p className="text-sm text-destructive">{errors.telefone}</p>}</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</CollapsibleContent>
|
||||
@ -476,7 +500,7 @@ export function PatientRegistrationForm({
|
||||
</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" /> Cancelar</Button>
|
||||
<Button type="button" variant="outline" className="hover:bg-primary/10 hover:text-primary dark:hover:bg-accent dark:hover:text-accent-foreground" onClick={() => (inline ? onClose?.() : onOpenChange?.(false))} disabled={isSubmitting}><XCircle className="mr-2 h-4 w-4" /> Cancelar</Button>
|
||||
<Button type="submit" disabled={isSubmitting}>{isSubmitting ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Save className="mr-2 h-4 w-4" />}{isSubmitting ? "Salvando..." : mode === "create" ? "Salvar Paciente" : "Atualizar Paciente"}</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@ -90,6 +90,7 @@ export function UploadAvatar({ userId, currentAvatarUrl, onAvatarChange, userNam
|
||||
size="sm"
|
||||
onClick={() => document.getElementById('avatar-upload')?.click()}
|
||||
disabled={isUploading}
|
||||
className="transition duration-200 hover:bg-primary/10 hover:text-primary dark:hover:bg-accent dark:hover:text-accent-foreground"
|
||||
>
|
||||
<Upload className="h-4 w-4 mr-2" />
|
||||
{isUploading ? 'Enviando...' : 'Upload'}
|
||||
@ -100,6 +101,7 @@ export function UploadAvatar({ userId, currentAvatarUrl, onAvatarChange, userNam
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleDownload}
|
||||
className="transition duration-200 hover:bg-primary/10 hover:text-primary dark:hover:bg-accent dark:hover:text-accent-foreground"
|
||||
>
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
Download
|
||||
|
||||
@ -652,28 +652,26 @@ function withPrefer(h: Record<string, string>, prefer: string) {
|
||||
// Helper: fetch seguro que tenta urls alternativas caso a requisição primária falhe
|
||||
async function fetchWithFallback<T = any>(url: string, headers: Record<string, string>, altUrls?: string[]): Promise<T | null> {
|
||||
try {
|
||||
console.debug('[fetchWithFallback] tentando URL:', url);
|
||||
// Log removido por segurança
|
||||
const res = await fetch(url, { method: 'GET', headers });
|
||||
if (res.ok) {
|
||||
return await parse<T>(res);
|
||||
}
|
||||
const raw = await res.clone().text().catch(() => '');
|
||||
console.warn('[fetchWithFallback] falha na URL primária:', url, 'status:', res.status, 'raw:', raw);
|
||||
// Log removido por segurança
|
||||
if (!altUrls || !altUrls.length) return null;
|
||||
for (const alt of altUrls) {
|
||||
try {
|
||||
console.debug('[fetchWithFallback] tentando fallback URL:', alt);
|
||||
// Log removido por segurança
|
||||
const r2 = await fetch(alt, { method: 'GET', headers });
|
||||
if (r2.ok) return await parse<T>(r2);
|
||||
const raw2 = await r2.clone().text().catch(() => '');
|
||||
console.warn('[fetchWithFallback] fallback falhou:', alt, 'status:', r2.status, 'raw:', raw2);
|
||||
// Log removido por segurança
|
||||
} catch (e) {
|
||||
console.warn('[fetchWithFallback] erro no fallback:', alt, e);
|
||||
// Log removido por segurança
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
console.warn('[fetchWithFallback] erro fetch primario:', url, e);
|
||||
// Log removido por segurança
|
||||
if (!altUrls || !altUrls.length) return null;
|
||||
for (const alt of altUrls) {
|
||||
try {
|
||||
@ -724,22 +722,17 @@ async function parse<T>(res: Response): Promise<T> {
|
||||
|
||||
// Special-case authentication/authorization errors to reduce noisy logs
|
||||
if (res.status === 401) {
|
||||
// If the server returned an empty body, avoid dumping raw text to console.error
|
||||
if (!rawText && !json) {
|
||||
console.warn('[API AUTH] 401 Unauthorized for', res.url, '- no auth token or token expired.');
|
||||
} else {
|
||||
console.warn('[API AUTH] 401 Unauthorized for', res.url, 'response:', json ?? rawText);
|
||||
}
|
||||
// Log removido por segurança - não expor URL da Supabase
|
||||
throw new Error('Você não está autenticado. Faça login novamente.');
|
||||
}
|
||||
|
||||
if (res.status === 403) {
|
||||
console.warn('[API AUTH] 403 Forbidden for', res.url, (json ?? rawText) ? 'response: ' + (json ?? rawText) : '');
|
||||
// Log removido por segurança - não expor URL da Supabase
|
||||
throw new Error('Você não tem permissão para executar esta ação.');
|
||||
}
|
||||
|
||||
// For other errors, log a concise error and try to produce a friendly message
|
||||
console.error('[API ERROR]', res.url, res.status, json ? json : 'no-json', rawText ? 'raw body present' : 'no raw body');
|
||||
console.error('[API ERROR] Status:', res.status, json ? 'JSON response' : 'no-json', rawText ? 'raw body present' : 'no raw body');
|
||||
|
||||
// Mensagens amigáveis para erros comuns
|
||||
let friendlyMessage = msg;
|
||||
@ -877,9 +870,7 @@ export async function buscarPacientes(termo: string): Promise<Paciente[]> {
|
||||
params.set('limit', '10');
|
||||
const url = `${REST}/patients?${params.toString()}`;
|
||||
const headers = baseHeaders();
|
||||
const masked = (headers['Authorization'] as string | undefined) ? `${String(headers['Authorization']).slice(0,6)}...${String(headers['Authorization']).slice(-6)}` : null;
|
||||
console.debug('[buscarPacientes] URL:', url);
|
||||
console.debug('[buscarPacientes] Headers (masked):', { ...headers, Authorization: masked ? '<<masked>>' : undefined });
|
||||
// Logs removidos por segurança
|
||||
const res = await fetch(url, { method: "GET", headers });
|
||||
const arr = await parse<Paciente[]>(res);
|
||||
|
||||
@ -908,7 +899,7 @@ export async function buscarPacientePorUserId(userId?: string | null): Promise<P
|
||||
try {
|
||||
const url = `${REST}/patients?user_id=eq.${encodeURIComponent(String(userId))}&limit=1`;
|
||||
const headers = baseHeaders();
|
||||
console.debug('[buscarPacientePorUserId] URL:', url);
|
||||
// Log removido por segurança
|
||||
const arr = await fetchWithFallback<Paciente[]>(url, headers).catch(() => []);
|
||||
if (arr && arr.length) return arr[0];
|
||||
return null;
|
||||
@ -925,7 +916,7 @@ export async function buscarPacientePorId(id: string | number): Promise<Paciente
|
||||
// Tenta buscar por id (UUID ou string) primeiro
|
||||
try {
|
||||
const url = `${ENV_CONFIG.SUPABASE_URL}/rest/v1/patients?id=eq.${encodeURIComponent(idParam)}`;
|
||||
console.debug('[buscarPacientePorId] tentando por id URL:', url);
|
||||
// Log removido por segurança
|
||||
const arr = await fetchWithFallback<Paciente[]>(url, headers);
|
||||
if (arr && arr.length) return arr[0];
|
||||
} catch (e) {
|
||||
@ -943,7 +934,7 @@ export async function buscarPacientePorId(id: string | number): Promise<Paciente
|
||||
altParams.set('social_name', `ilike.*${String(id)}*`);
|
||||
altParams.set('limit', '5');
|
||||
const alt = `${REST}/patients?${altParams.toString()}`;
|
||||
console.debug('[buscarPacientePorId] tentando por nome URL:', url);
|
||||
// Log removido por segurança
|
||||
const arr2 = await fetchWithFallback<Paciente[]>(url, headers, [alt]);
|
||||
if (arr2 && arr2.length) return arr2[0];
|
||||
}
|
||||
@ -1348,31 +1339,31 @@ export async function buscarRelatorioPorId(id: string | number): Promise<Report>
|
||||
// 1) tenta por id (UUID ou campo id)
|
||||
try {
|
||||
const urlById = `${REST}/reports?id=eq.${encodeURIComponent(sId)}`;
|
||||
console.debug('[buscarRelatorioPorId] tentando por id URL:', urlById);
|
||||
// Log removido por segurança
|
||||
const arr = await fetchWithFallback<Report[]>(urlById, headers);
|
||||
if (arr && arr.length) return arr[0];
|
||||
} catch (e) {
|
||||
console.warn('[buscarRelatorioPorId] falha ao buscar por id:', e);
|
||||
// Falha silenciosa - tenta próxima estratégia
|
||||
}
|
||||
|
||||
// 2) tenta por order_number (caso o usuário cole um código legível)
|
||||
try {
|
||||
const urlByOrder = `${REST}/reports?order_number=eq.${encodeURIComponent(sId)}`;
|
||||
console.debug('[buscarRelatorioPorId] tentando por order_number URL:', urlByOrder);
|
||||
// Log removido por segurança
|
||||
const arr2 = await fetchWithFallback<Report[]>(urlByOrder, headers);
|
||||
if (arr2 && arr2.length) return arr2[0];
|
||||
} catch (e) {
|
||||
console.warn('[buscarRelatorioPorId] falha ao buscar por order_number:', e);
|
||||
// Falha silenciosa - tenta próxima estratégia
|
||||
}
|
||||
|
||||
// 3) tenta por patient_id (caso o usuário passe um patient_id em vez do report id)
|
||||
try {
|
||||
const urlByPatient = `${REST}/reports?patient_id=eq.${encodeURIComponent(sId)}`;
|
||||
console.debug('[buscarRelatorioPorId] tentando por patient_id URL:', urlByPatient);
|
||||
// Log removido por segurança
|
||||
const arr3 = await fetchWithFallback<Report[]>(urlByPatient, headers);
|
||||
if (arr3 && arr3.length) return arr3[0];
|
||||
} catch (e) {
|
||||
console.warn('[buscarRelatorioPorId] falha ao buscar por patient_id:', e);
|
||||
// Falha silenciosa - não encontrado
|
||||
}
|
||||
|
||||
// Não encontrado
|
||||
@ -1424,7 +1415,7 @@ export async function buscarPacientesPorIds(ids: Array<string | number>): Promis
|
||||
altParams.set('limit', '100');
|
||||
const alt = `${REST}/patients?${altParams.toString()}`;
|
||||
const headers = baseHeaders();
|
||||
console.debug('[buscarPacientesPorIds] URL (patient by name):', url);
|
||||
// Log removido por segurança
|
||||
const arr = await fetchWithFallback<Paciente[]>(url, headers, [alt]);
|
||||
if (arr && arr.length) results.push(...arr);
|
||||
} catch (e) {
|
||||
@ -1567,7 +1558,7 @@ export async function criarPaciente(input: PacienteInput): Promise<Paciente> {
|
||||
const a = maskedHeaders.Authorization as string;
|
||||
maskedHeaders.Authorization = `${a.slice(0,6)}...${a.slice(-6)}`;
|
||||
}
|
||||
console.debug('[criarPaciente] POST', u, 'headers(masked):', maskedHeaders, 'payloadKeys:', Object.keys(payload));
|
||||
// Log removido por segurança
|
||||
const res = await fetch(u, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
@ -1766,8 +1757,7 @@ export async function buscarMedicos(termo: string): Promise<Medico[]> {
|
||||
queries.push(`specialty=ilike.*${q}*`);
|
||||
}
|
||||
|
||||
// debug: mostrar queries construídas
|
||||
console.debug('[buscarMedicos] queries construídas:', queries);
|
||||
// Debug removido por segurança
|
||||
|
||||
const results: Medico[] = [];
|
||||
const seenIds = new Set<string>();
|
||||
@ -1783,10 +1773,7 @@ export async function buscarMedicos(termo: string): Promise<Medico[]> {
|
||||
params.set('limit', '10');
|
||||
const url = `${REST}/doctors?${params.toString()}`;
|
||||
const headers = baseHeaders();
|
||||
const masked = (headers['Authorization'] as string | undefined) ? `${String(headers['Authorization']).slice(0,6)}...${String(headers['Authorization']).slice(-6)}` : null;
|
||||
console.debug('[buscarMedicos] URL params:', params.toString());
|
||||
console.debug('[buscarMedicos] URL:', url);
|
||||
console.debug('[buscarMedicos] Headers (masked):', { ...headers, Authorization: masked ? '<<masked>>' : undefined });
|
||||
// Logs removidos por segurança
|
||||
const res = await fetch(url, { method: 'GET', headers });
|
||||
const arr = await parse<Medico[]>(res);
|
||||
|
||||
@ -1819,7 +1806,7 @@ export async function buscarMedicoPorId(id: string | number): Promise<Medico | n
|
||||
// 1) Se parece UUID, busca por id direto
|
||||
if (isString && uuidRegex.test(sId)) {
|
||||
const url = `${REST}/doctors?id=eq.${encodeURIComponent(sId)}`;
|
||||
console.debug('[buscarMedicoPorId] tentando por id URL:', url);
|
||||
// Log removido por segurança
|
||||
const arr = await fetchWithFallback<Medico[]>(url, baseHeaders());
|
||||
if (arr && arr.length > 0) return arr[0];
|
||||
}
|
||||
@ -1868,18 +1855,16 @@ export async function buscarMedicoPorId(id: string | number): Promise<Medico | n
|
||||
// Se não encontrar no Supabase, tenta o mock API
|
||||
try {
|
||||
const mockUrl = `https://yuanqog.com/m1/1053378-0-default/rest/v1/doctors/${encodeURIComponent(String(id))}`;
|
||||
console.debug('[buscarMedicoPorId] tentando mock API URL:', mockUrl);
|
||||
// Log removido por segurança
|
||||
try {
|
||||
const medico = await fetchWithFallback<any>(mockUrl, { Accept: 'application/json' });
|
||||
if (medico) {
|
||||
console.log('✅ Médico encontrado no Mock API:', medico);
|
||||
return medico as Medico;
|
||||
}
|
||||
// fetchWithFallback returned null -> not found
|
||||
console.warn('[buscarMedicoPorId] mock API returned no result for id:', id);
|
||||
return null;
|
||||
} catch (fetchErr) {
|
||||
console.warn('[buscarMedicoPorId] mock API fetch failed or returned no result:', fetchErr);
|
||||
// Falha silenciosa
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
@ -1928,7 +1913,7 @@ export async function buscarMedicosPorIds(ids: Array<string | number>): Promise<
|
||||
altParams.set('limit', '200');
|
||||
const alt = `${REST}/doctors?${altParams.toString()}`;
|
||||
const headers = baseHeaders();
|
||||
console.debug('[buscarMedicosPorIds] URL (doctor by name):', url);
|
||||
// Log removido por segurança
|
||||
const socialAltParams = new URLSearchParams();
|
||||
socialAltParams.set('social_name', `ilike.*${name}*`);
|
||||
socialAltParams.set('limit', '200');
|
||||
@ -2047,7 +2032,7 @@ export async function criarMedico(input: MedicoInput): Promise<Medico> {
|
||||
const a = maskedHeaders.Authorization as string;
|
||||
maskedHeaders.Authorization = `${a.slice(0,6)}...${a.slice(-6)}`;
|
||||
}
|
||||
console.debug('[criarMedico] POST', u, 'headers(masked):', maskedHeaders, 'payloadKeys:', Object.keys(payload));
|
||||
// Log removido por segurança
|
||||
|
||||
const res = await fetch(u, {
|
||||
method: 'POST',
|
||||
@ -2106,12 +2091,7 @@ export async function criarMedico(input: MedicoInput): Promise<Medico> {
|
||||
|
||||
const url = `${API_BASE}/functions/v1/create-doctor`;
|
||||
const headers = { ...baseHeaders(), 'Content-Type': 'application/json' } as Record<string, string>;
|
||||
const maskedHeaders = { ...headers } as Record<string, string>;
|
||||
if (maskedHeaders.Authorization) {
|
||||
const a = maskedHeaders.Authorization as string;
|
||||
maskedHeaders.Authorization = `${a.slice(0,6)}...${a.slice(-6)}`;
|
||||
}
|
||||
console.debug('[criarMedico fallback] POST', url, 'headers(masked):', maskedHeaders, 'body:', JSON.stringify(fallbackPayload));
|
||||
// Logs removidos por segurança
|
||||
|
||||
const res = await fetch(url, { method: 'POST', headers, body: JSON.stringify(fallbackPayload) });
|
||||
const parsed = await parse<any>(res as Response);
|
||||
@ -2198,19 +2178,14 @@ export async function vincularUserIdPaciente(pacienteId: string | number, userId
|
||||
const uuidRegex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
||||
const looksLikeUuid = uuidRegex.test(idStr);
|
||||
// Allow non-UUID ids (legacy) but log a debug warning when it's not UUID
|
||||
if (!looksLikeUuid) console.warn('[vincularUserIdPaciente] pacienteId does not look like a UUID:', idStr);
|
||||
// Log removido por segurança
|
||||
|
||||
const url = `${REST}/patients?id=eq.${encodeURIComponent(idStr)}`;
|
||||
const payload = { user_id: String(userId) };
|
||||
|
||||
// Debug-friendly masked headers
|
||||
const headers = withPrefer({ ...baseHeaders(), 'Content-Type': 'application/json' }, 'return=representation');
|
||||
const maskedHeaders = { ...headers } as Record<string, string>;
|
||||
if (maskedHeaders.Authorization) {
|
||||
const a = maskedHeaders.Authorization as string;
|
||||
maskedHeaders.Authorization = a.slice(0,6) + '...' + a.slice(-6);
|
||||
}
|
||||
console.debug('[vincularUserIdPaciente] PATCH', url, 'payload:', { ...payload }, 'headers(masked):', maskedHeaders);
|
||||
// Logs removidos por segurança
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: 'PATCH',
|
||||
@ -2223,7 +2198,7 @@ export async function vincularUserIdPaciente(pacienteId: string | number, userId
|
||||
const arr = await parse<Paciente[] | Paciente>(res);
|
||||
return Array.isArray(arr) ? arr[0] : (arr as Paciente);
|
||||
} catch (err) {
|
||||
console.error('[vincularUserIdPaciente] erro ao vincular:', { pacienteId: idStr, userId, url });
|
||||
console.error('[vincularUserIdPaciente] erro ao vincular - falha na requisição');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@ -2232,8 +2207,7 @@ export async function vincularUserIdPaciente(pacienteId: string | number, userId
|
||||
|
||||
|
||||
export async function atualizarMedico(id: string | number, input: MedicoInput): Promise<Medico> {
|
||||
console.log(`Tentando atualizar médico ID: ${id}`);
|
||||
console.log(`Payload original:`, input);
|
||||
// Logs removidos por segurança
|
||||
|
||||
// Criar um payload limpo apenas com campos básicos que sabemos que existem
|
||||
const cleanPayload = {
|
||||
@ -2280,7 +2254,7 @@ export async function atualizarMedico(id: string | number, input: MedicoInput):
|
||||
// Atualizar apenas no Supabase (dados reais)
|
||||
try {
|
||||
const url = `${REST}/doctors?id=eq.${id}`;
|
||||
console.log(`URL de atualização: ${url}`);
|
||||
// Log removido por segurança
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: "PATCH",
|
||||
@ -2288,12 +2262,12 @@ export async function atualizarMedico(id: string | number, input: MedicoInput):
|
||||
body: JSON.stringify(cleanPayload),
|
||||
});
|
||||
|
||||
console.log(`Resposta do servidor: ${res.status} ${res.statusText}`);
|
||||
// Log removido por segurança
|
||||
|
||||
if (res.ok) {
|
||||
const arr = await parse<Medico[] | Medico>(res);
|
||||
const result = Array.isArray(arr) ? arr[0] : (arr as Medico);
|
||||
console.log('Médico atualizado no Supabase:', result);
|
||||
// Log removido por segurança
|
||||
return result;
|
||||
} else {
|
||||
// Vamos tentar ver o erro detalhado
|
||||
|
||||
@ -139,15 +139,12 @@ export async function listAssignmentsForPatient(patientId: string): Promise<Pati
|
||||
* Ú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}`);
|
||||
// Log removido por segurança
|
||||
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,
|
||||
});
|
||||
// Logs removidos por segurança
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
@ -156,7 +153,7 @@ export async function listAssignmentsForUser(userId: string): Promise<PatientAss
|
||||
// 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>');
|
||||
// Log removido por segurança
|
||||
|
||||
if (!response.ok) {
|
||||
const errorBody = txt || '';
|
||||
|
||||
@ -140,22 +140,14 @@ export async function listarRelatorios(filtros?: { patient_id?: string; status?:
|
||||
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,
|
||||
});
|
||||
// Logs removidos por segurança
|
||||
|
||||
const resposta = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: cabecalhos,
|
||||
});
|
||||
console.log('[listarRelatorios] Status:', resposta.status, resposta.statusText);
|
||||
// Logs removidos por segurança
|
||||
const dados = await resposta.json().catch(() => null);
|
||||
console.log('[listarRelatorios] Payload:', dados);
|
||||
if (!resposta.ok) throw new Error('Erro ao buscar relatórios');
|
||||
if (Array.isArray(dados)) return dados;
|
||||
if (dados && Array.isArray(dados.data)) return dados.data;
|
||||
@ -170,14 +162,14 @@ 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);
|
||||
// Log removido por segurança
|
||||
const resposta = await fetch(`${BASE_API_RELATORIOS}?id=eq.${id}`, {
|
||||
method: 'GET',
|
||||
headers: obterCabecalhos(),
|
||||
});
|
||||
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);
|
||||
// Log removido por segurança
|
||||
if (!relatorio) throw new Error('Relatório não encontrado');
|
||||
return relatorio;
|
||||
} catch (erro) {
|
||||
@ -191,16 +183,14 @@ export async function buscarRelatorioPorId(id: string): Promise<Report> {
|
||||
*/
|
||||
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 });
|
||||
// Logs removidos por segurança
|
||||
|
||||
const resposta = await fetch(BASE_API_RELATORIOS, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(dadosRelatorio),
|
||||
});
|
||||
console.log('[criarRelatorio] Status:', resposta.status, resposta.statusText);
|
||||
// Log removido por segurança
|
||||
if (!resposta.ok) {
|
||||
let mensagemErro = `HTTP ${resposta.status}: ${resposta.statusText}`;
|
||||
try {
|
||||
@ -229,8 +219,7 @@ export async function criarRelatorio(dadosRelatorio: CreateReportData, token?: s
|
||||
*/
|
||||
export async function atualizarRelatorio(id: string, dadosRelatorio: UpdateReportData): Promise<Report> {
|
||||
try {
|
||||
console.log('📝 [API RELATÓRIOS] Atualizando relatório ID:', id);
|
||||
console.log('📤 [API RELATÓRIOS] Dados:', dadosRelatorio);
|
||||
// Logs removidos por segurança
|
||||
const resposta = await fetch(`${BASE_API_RELATORIOS}?id=eq.${id}`, {
|
||||
method: 'PATCH',
|
||||
headers: obterCabecalhos(),
|
||||
@ -238,7 +227,7 @@ export async function atualizarRelatorio(id: string, dadosRelatorio: UpdateRepor
|
||||
});
|
||||
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);
|
||||
// Log removido por segurança
|
||||
if (!relatorio) throw new Error('Relatório não encontrado');
|
||||
return relatorio;
|
||||
} catch (erro) {
|
||||
@ -252,13 +241,13 @@ export async function atualizarRelatorio(id: string, dadosRelatorio: UpdateRepor
|
||||
*/
|
||||
export async function deletarRelatorio(id: string): Promise<void> {
|
||||
try {
|
||||
console.log('🗑️ [API RELATÓRIOS] Deletando relatório ID:', id);
|
||||
// Log removido por segurança
|
||||
const resposta = await fetch(`${BASE_API_RELATORIOS}/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: obterCabecalhos(),
|
||||
});
|
||||
await tratarRespostaApi<void>(resposta);
|
||||
console.log('✅ [API RELATÓRIOS] Relatório deletado com sucesso');
|
||||
// Log removido por segurança
|
||||
} catch (erro) {
|
||||
console.error('❌ [API RELATÓRIOS] Erro ao deletar relatório:', erro);
|
||||
throw erro;
|
||||
@ -270,20 +259,19 @@ 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);
|
||||
// Logs removidos por segurança
|
||||
// Try a strict eq lookup first (encode the id)
|
||||
const encodedId = encodeURIComponent(String(idPaciente));
|
||||
let url = `${BASE_API_RELATORIOS}?patient_id=eq.${encodedId}`;
|
||||
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 });
|
||||
// Logs removidos por segurança
|
||||
const resposta = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
});
|
||||
const resultado = await tratarRespostaApi<Report[]>(resposta);
|
||||
console.log('✅ [API RELATÓRIOS] Relatórios do paciente encontrados (eq):', resultado.length);
|
||||
// Log removido por segurança
|
||||
// If eq returned results, return them. Otherwise retry using `in.(id)` which some setups prefer.
|
||||
if (Array.isArray(resultado) && resultado.length) return resultado;
|
||||
|
||||
@ -291,13 +279,13 @@ export async function listarRelatoriosPorPaciente(idPaciente: string): Promise<R
|
||||
try {
|
||||
const inClause = encodeURIComponent(`(${String(idPaciente)})`);
|
||||
const urlIn = `${BASE_API_RELATORIOS}?patient_id=in.${inClause}`;
|
||||
console.debug('[listarRelatoriosPorPaciente] retrying with IN clause URL:', urlIn);
|
||||
// Log removido por segurança
|
||||
const resp2 = await fetch(urlIn, { method: 'GET', headers });
|
||||
const res2 = await tratarRespostaApi<Report[]>(resp2);
|
||||
console.log('✅ [API RELATÓRIOS] Relatórios do paciente encontrados (in):', Array.isArray(res2) ? res2.length : 0);
|
||||
// Log removido por segurança
|
||||
return Array.isArray(res2) ? res2 : [];
|
||||
} catch (e) {
|
||||
console.warn('[listarRelatoriosPorPaciente] fallback in.() failed', e);
|
||||
// Log removido por segurança
|
||||
}
|
||||
|
||||
return [];
|
||||
@ -315,15 +303,13 @@ export async function listarRelatoriosPorMedico(idMedico: string): Promise<Repor
|
||||
console.log('👨⚕️ [API RELATÓRIOS] Buscando relatórios do médico:', 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 });
|
||||
// Logs removidos por segurança
|
||||
const resposta = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: obterCabecalhos(),
|
||||
});
|
||||
const resultado = await tratarRespostaApi<Report[]>(resposta);
|
||||
console.log('✅ [API RELATÓRIOS] Relatórios do médico encontrados:', resultado.length);
|
||||
// Log removido por segurança
|
||||
return resultado;
|
||||
} catch (erro) {
|
||||
console.error('❌ [API RELATÓRIOS] Erro ao buscar relatórios do médico:', erro);
|
||||
@ -346,13 +332,11 @@ export async function listarRelatoriosPorPacientes(ids: string[]): Promise<Repor
|
||||
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 });
|
||||
// Logs removidos por segurança
|
||||
|
||||
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);
|
||||
// Log removido por segurança
|
||||
return resultado;
|
||||
} catch (erro) {
|
||||
console.error('❌ [API RELATÓRIOS] Erro ao buscar relatórios para vários pacientes:', erro);
|
||||
@ -368,26 +352,26 @@ export async function listarRelatoriosPorPacientes(ids: string[]): Promise<Repor
|
||||
export async function listarRelatoriosParaMedicoAtribuido(userId?: string): Promise<Report[]> {
|
||||
try {
|
||||
if (!userId) {
|
||||
console.warn('[listarRelatoriosParaMedicoAtribuido] userId ausente, retornando array vazio');
|
||||
// Log removido por segurança
|
||||
return [];
|
||||
}
|
||||
|
||||
console.log('[listarRelatoriosParaMedicoAtribuido] buscando assignments para user:', userId);
|
||||
// Log removido por segurança
|
||||
// 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);
|
||||
// Log removido por segurança
|
||||
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');
|
||||
// Log removido por segurança
|
||||
return [];
|
||||
}
|
||||
|
||||
console.log('[listarRelatoriosParaMedicoAtribuido] carregando relatórios para pacientes:', patientIds);
|
||||
// Log removido por segurança
|
||||
const rels = await listarRelatoriosPorPacientes(patientIds);
|
||||
return rels || [];
|
||||
} catch (err) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user