main #28

Merged
lucasrodrigues-bit2 merged 17 commits from StsDanilo/riseup-squad21:main into main 2025-10-17 21:46:03 +00:00
25 changed files with 1790 additions and 747 deletions

2
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,2 @@
{
}

View File

@ -1,10 +1,139 @@
import DoctorLayout from "@/components/doctor-layout"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Calendar, Clock, User, Plus } from "lucide-react"
import Link from "next/link"
"use client";
import DoctorLayout from "@/components/doctor-layout";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Calendar, Clock, User, Trash2 } from "lucide-react";
import Link from "next/link";
import { useEffect, useState } from "react";
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
import { AvailabilityService } from "@/services/availabilityApi.mjs";
import { exceptionsService } from "@/services/exceptionApi.mjs";
import { toast } from "@/hooks/use-toast";
type Availability = {
id: string;
doctor_id: string;
weekday: string;
start_time: string;
end_time: string;
slot_minutes: number;
appointment_type: string;
active: boolean;
created_at: string;
updated_at: string;
created_by: string;
updated_by: string | null;
};
type Schedule = {
weekday: object;
};
export default function PatientDashboard() {
const userInfo = JSON.parse(localStorage.getItem("user_info") || "{}");
const doctorId = "3bb9ee4a-cfdd-4d81-b628-383907dfa225"; //userInfo.id;
const [availability, setAvailability] = useState<any | null>(null);
const [exceptions, setExceptions] = useState<any | null>(null);
const [schedule, setSchedule] = useState<Record<string, { start: string; end: string }[]>>({});
const formatTime = (time: string) => time.split(":").slice(0, 2).join(":");
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [patientToDelete, setPatientToDelete] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
// Mapa de tradução
const weekdaysPT: Record<string, string> = {
sunday: "Domingo",
monday: "Segunda",
tuesday: "Terça",
wednesday: "Quarta",
thursday: "Quinta",
friday: "Sexta",
saturday: "Sábado",
};
useEffect(() => {
const fetchData = async () => {
try {
// fetch para disponibilidade
const response = await AvailabilityService.list();
const filteredResponse = response.filter((disp: { doctor_id: any }) => disp.doctor_id == doctorId);
setAvailability(filteredResponse);
// fetch para exceções
const res = await exceptionsService.list();
const filteredRes = res.filter((disp: { doctor_id: any }) => disp.doctor_id == doctorId);
setExceptions(filteredRes);
} catch (e: any) {
alert(`${e?.error} ${e?.message}`);
}
};
fetchData();
}, []);
const openDeleteDialog = (patientId: string) => {
setPatientToDelete(patientId);
setDeleteDialogOpen(true);
};
const handleDeletePatient = async (patientId: string) => {
// Remove from current list (client-side deletion)
try {
const res = await exceptionsService.delete(patientId);
let message = "Exceção deletada com sucesso";
try {
if (res) {
throw new Error(`${res.error} ${res.message}` || "A API retornou erro");
} else {
console.log(message);
}
} catch {}
toast({
title: "Sucesso",
description: message,
});
setExceptions((prev: any[]) => prev.filter((p) => String(p.id) !== String(patientId)));
} catch (e: any) {
toast({
title: "Erro",
description: e?.message || "Não foi possível deletar a exceção",
});
}
setDeleteDialogOpen(false);
setPatientToDelete(null);
};
function formatAvailability(data: Availability[]) {
// Agrupar os horários por dia da semana
const schedule = data.reduce((acc: any, item) => {
const { weekday, start_time, end_time } = item;
// Se o dia ainda não existe, cria o array
if (!acc[weekday]) {
acc[weekday] = [];
}
// Adiciona o horário do dia
acc[weekday].push({
start: start_time,
end: end_time,
});
return acc;
}, {} as Record<string, { start: string; end: string }[]>);
return schedule;
}
useEffect(() => {
if (availability) {
const formatted = formatAvailability(availability);
setSchedule(formatted);
}
}, [availability]);
return (
<DoctorLayout>
<div className="space-y-6">
@ -85,7 +214,102 @@ export default function PatientDashboard() {
</CardContent>
</Card>
</div>
<div className="grid md:grid-cols-1 gap-6">
<Card>
<CardHeader>
<CardTitle>Horário Semanal</CardTitle>
<CardDescription>Confira rapidamente a sua disponibilidade da semana</CardDescription>
</CardHeader>
<CardContent className="space-y-4 grid md:grid-cols-7 gap-2">
{["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"].map((day) => {
const times = schedule[day] || [];
return (
<div key={day} className="space-y-4">
<div className="flex flex-col items-center justify-between p-3 bg-blue-50 rounded-lg">
<div>
<p className="font-medium capitalize">{weekdaysPT[day]}</p>
</div>
<div className="text-center">
{times.length > 0 ? (
times.map((t, i) => (
<p key={i} className="text-sm text-gray-600">
{formatTime(t.start)} <br /> {formatTime(t.end)}
</p>
))
) : (
<p className="text-sm text-gray-400 italic">Sem horário</p>
)}
</div>
</div>
</div>
);
})}
</CardContent>
</Card>
</div>
<div className="grid md:grid-cols-1 gap-6">
<Card>
<CardHeader>
<CardTitle>Exceções</CardTitle>
<CardDescription>Bloqueios e liberações eventuais de agenda</CardDescription>
</CardHeader>
<CardContent className="space-y-4 grid md:grid-cols-7 gap-2">
{exceptions && exceptions.length > 0 ? (
exceptions.map((ex: any) => {
// Formata data e hora
const date = new Date(ex.date).toLocaleDateString("pt-BR", {
weekday: "long",
day: "2-digit",
month: "long",
});
const startTime = formatTime(ex.start_time);
const endTime = formatTime(ex.end_time);
return (
<div key={ex.id} className="space-y-4">
<div className="flex flex-col items-center justify-between p-3 bg-blue-50 rounded-lg shadow-sm">
<div className="text-center">
<p className="font-semibold capitalize">{date}</p>
<p className="text-sm text-gray-600">
{startTime} - {endTime} <br/> -
</p>
</div>
<div className="text-center mt-2">
<p className={`text-sm font-medium ${ex.kind === "bloqueio" ? "text-red-600" : "text-green-600"}`}>{ex.kind === "bloqueio" ? "Bloqueio" : "Liberação"}</p>
<p className="text-xs text-gray-500 italic">{ex.reason || "Sem motivo especificado"}</p>
</div>
<div>
<Button className="text-red-600" variant="outline" onClick={() => openDeleteDialog(String(ex.id))}>
<Trash2></Trash2>
</Button>
</div>
</div>
</div>
);
})
) : (
<p className="text-sm text-gray-400 italic col-span-7 text-center">Nenhuma exceção registrada.</p>
)}
</CardContent>
</Card>
</div>
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Confirmar exclusão</AlertDialogTitle>
<AlertDialogDescription>Tem certeza que deseja excluir este paciente? Esta ação não pode ser desfeita.</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancelar</AlertDialogCancel>
<AlertDialogAction onClick={() => patientToDelete && handleDeletePatient(patientToDelete)} className="bg-red-600 hover:bg-red-700">
Excluir
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</DoctorLayout>
)
);
}

View File

@ -0,0 +1,218 @@
"use client";
import type React from "react";
import Link from "next/link";
import { useState, useEffect } from "react";
import DoctorLayout from "@/components/doctor-layout";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Clock, Calendar as CalendarIcon, MapPin, Phone, User, X, RefreshCw } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { useRouter } from "next/navigation";
import { toast } from "@/hooks/use-toast";
import { exceptionsService } from "@/services/exceptionApi.mjs";
// IMPORTAR O COMPONENTE CALENDÁRIO DA SHADCN
import { Calendar } from "@/components/ui/calendar";
import { format } from "date-fns"; // Usaremos o date-fns para formatação e comparação de datas
const APPOINTMENTS_STORAGE_KEY = "clinic-appointments";
// --- TIPAGEM DA CONSULTA SALVA NO LOCALSTORAGE ---
interface LocalStorageAppointment {
id: number;
patientName: string;
doctor: string;
specialty: string;
date: string; // Data no formato YYYY-MM-DD
time: string; // Hora no formato HH:MM
status: "agendada" | "confirmada" | "cancelada" | "realizada";
location: string;
phone: string;
}
const LOGGED_IN_DOCTOR_NAME = "Dr. João Santos";
// Função auxiliar para comparar se duas datas (Date objects) são o mesmo dia
const isSameDay = (date1: Date, date2: Date) => {
return date1.getFullYear() === date2.getFullYear() && date1.getMonth() === date2.getMonth() && date1.getDate() === date2.getDate();
};
// --- COMPONENTE PRINCIPAL ---
export default function ExceptionPage() {
const [allAppointments, setAllAppointments] = useState<LocalStorageAppointment[]>([]);
const router = useRouter();
const [filteredAppointments, setFilteredAppointments] = useState<LocalStorageAppointment[]>([]);
const [isLoading, setIsLoading] = useState(false);
const userInfo = JSON.parse(localStorage.getItem("user_info") || "{}");
const doctorIdTemp = "3bb9ee4a-cfdd-4d81-b628-383907dfa225";
const [tipo, setTipo] = useState<string>("");
// NOVO ESTADO 1: Armazena os dias com consultas (para o calendário)
const [bookedDays, setBookedDays] = useState<Date[]>([]);
// NOVO ESTADO 2: Armazena a data selecionada no calendário
const [selectedCalendarDate, setSelectedCalendarDate] = useState<Date | undefined>(new Date());
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (isLoading) return;
//setIsLoading(true);
const form = e.currentTarget;
const formData = new FormData(form);
const apiPayload = {
doctor_id: doctorIdTemp,
created_by: doctorIdTemp,
date: selectedCalendarDate ? format(selectedCalendarDate, "yyyy-MM-dd") : "",
start_time: ((formData.get("horarioEntrada") + ":00") as string) || undefined,
end_time: ((formData.get("horarioSaida") + ":00") as string) || undefined,
kind: tipo || undefined,
reason: formData.get("reason"),
};
console.log(apiPayload);
try {
const res = await exceptionsService.create(apiPayload);
console.log(res);
let message = "Exceção cadastrada com sucesso";
try {
if (!res[0].id) {
throw new Error(`${res.error} ${res.message}` || "A API retornou erro");
} else {
console.log(message);
}
} catch {}
toast({
title: "Sucesso",
description: message,
});
router.push("/doctor/dashboard"); // adicionar página para listar a disponibilidade
} catch (err: any) {
toast({
title: "Erro",
description: err?.message || "Não foi possível cadastrar a exceção",
});
} finally {
setIsLoading(false);
}
};
const displayDate = selectedCalendarDate ? new Date(selectedCalendarDate).toLocaleDateString("pt-BR", { weekday: "long", day: "2-digit", month: "long" }) : "Selecione uma data";
return (
<DoctorLayout>
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold text-gray-900">Adicione exceções</h1>
<p className="text-gray-600">Altere a disponibilidade em casos especiais para o Dr. {userInfo.user_metadata.full_name}</p>
</div>
<div className="flex justify-between items-center">
<h2 className="text-xl font-semibold">Consultas para: {displayDate}</h2>
<Button disabled={isLoading} variant="outline" size="sm">
<RefreshCw className={`mr-2 h-4 w-4 ${isLoading ? "animate-spin" : ""}`} />
Atualizar Agenda
</Button>
</div>
{/* NOVO LAYOUT DE DUAS COLUNAS */}
<div className="grid lg:grid-cols-3 gap-6">
{/* COLUNA 1: CALENDÁRIO */}
<div className="lg:col-span-1">
<Card>
<CardHeader>
<CardTitle className="flex items-center">
<CalendarIcon className="mr-2 h-5 w-5" />
Calendário
</CardTitle>
<p className="text-sm text-gray-500">Selecione a data desejada.</p>
</CardHeader>
<CardContent className="flex justify-center p-2">
<Calendar
mode="single"
selected={selectedCalendarDate}
onSelect={setSelectedCalendarDate}
autoFocus
// A CHAVE DO HIGHLIGHT: Passa o array de datas agendadas
modifiers={{ booked: bookedDays }}
// Define o estilo CSS para o modificador 'booked'
modifiersClassNames={{
booked: "bg-blue-600 text-white aria-selected:!bg-blue-700 hover:!bg-blue-700/90",
}}
className="rounded-md border p-2"
/>
</CardContent>
</Card>
</div>
{/* COLUNA 2: FORM PARA ADICIONAR EXCEÇÃO */}
<div className="lg:col-span-2 space-y-4">
{isLoading ? (
<p className="text-center text-lg text-gray-500">Carregando a agenda...</p>
) : !selectedCalendarDate ? (
<p className="text-center text-lg text-gray-500">Selecione uma data.</p>
) : (
<form className="space-y-6" onSubmit={handleSubmit}>
<div className="bg-white rounded-lg border border-gray-200 p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-6">Dados </h2>
<div className="space-y-6">
<div className="grid md:grid-cols-5 gap-6">
<div>
<Label htmlFor="horarioEntrada" className="text-sm font-medium text-gray-700">
Horario De Entrada
</Label>
<Input type="time" id="horarioEntrada" name="horarioEntrada" required className="mt-1" />
</div>
<div>
<Label htmlFor="horarioSaida" className="text-sm font-medium text-gray-700">
Horario De Saida
</Label>
<Input type="time" id="horarioSaida" name="horarioSaida" required className="mt-1" />
</div>
</div>
<div>
<Label htmlFor="tipo" className="text-sm font-medium text-gray-700">
Tipo
</Label>
<Select onValueChange={(value) => setTipo(value)} value={tipo}>
<SelectTrigger className="mt-1">
<SelectValue placeholder="Selecione" />
</SelectTrigger>
<SelectContent>
<SelectItem value="bloqueio">Bloqueio </SelectItem>
<SelectItem value="liberacao">Liberação</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="reason" className="text-sm font-medium text-gray-700">
Motivo
</Label>
<Input type="textarea" id="reason" name="reason" required className="mt-1" />
</div>
</div>
</div>
<div className="flex justify-end gap-4">
<Link href="/doctor/disponibilidade">
<Button variant="outline">Cancelar</Button>
</Link>
<Button type="submit" className="bg-green-600 hover:bg-green-700">
Salvar Exceção
</Button>
</div>
</form>
)}
</div>
</div>
</div>
</DoctorLayout>
);
}

View File

@ -0,0 +1,186 @@
"use client";
import { useState, useEffect } from "react";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import DoctorLayout from "@/components/doctor-layout";
import { AvailabilityService } from "@/services/availabilityApi.mjs";
import { toast } from "@/hooks/use-toast";
import { useRouter } from "next/navigation";
export default function AvailabilityPage() {
const [error, setError] = useState<string | null>(null);
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const userInfo = JSON.parse(localStorage.getItem("user_info") || "{}");
const doctorIdTemp = "3bb9ee4a-cfdd-4d81-b628-383907dfa225";
const [modalidadeConsulta, setModalidadeConsulta] = useState<string>("");
useEffect(() => {
const fetchData = async () => {
try {
const response = await AvailabilityService.list();
console.log(response);
} catch (e: any) {
alert(`${e?.error} ${e?.message}`);
}
};
fetchData();
}, []);
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (isLoading) return;
setIsLoading(true);
const form = e.currentTarget;
const formData = new FormData(form);
const apiPayload = {
doctor_id: doctorIdTemp,
created_by: doctorIdTemp,
weekday: (formData.get("weekday") as string) || undefined,
start_time: (formData.get("horarioEntrada") as string) || undefined,
end_time: (formData.get("horarioSaida") as string) || undefined,
slot_minutes: Number(formData.get("duracaoConsulta")) || undefined,
appointment_type: modalidadeConsulta || undefined,
active: true,
};
console.log(apiPayload);
try {
const res = await AvailabilityService.create(apiPayload);
console.log(res);
let message = "disponibilidade cadastrada com sucesso";
try {
if (!res[0].id) {
throw new Error(`${res.error} ${res.message}` || "A API retornou erro");
} else {
console.log(message);
}
} catch {}
toast({
title: "Sucesso",
description: message,
});
router.push("#"); // adicionar página para listar a disponibilidade
} catch (err: any) {
toast({
title: "Erro",
description: err?.message || "Não foi possível cadastrar o paciente",
});
} finally {
setIsLoading(false);
}
};
return (
<DoctorLayout>
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900">Definir Disponibilidade</h1>
<p className="text-gray-600">Defina sua disponibilidade para consultas </p>
</div>
</div>
<form className="space-y-6" onSubmit={handleSubmit}>
<div className="bg-white rounded-lg border border-gray-200 p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-6">Dados </h2>
<div className="space-y-6">
<div className="grid md:grid-cols-3 gap-4">
<div>
<Label className="text-sm font-medium text-gray-700">Dia Da Semana</Label>
<div className="flex gap-4 mt-2 flex-nowrap">
<label className="flex items-center gap-2">
<input type="radio" name="weekday" value="monday" className="text-blue-600" />
<span className="whitespace-nowrap text-sm">Segunda-Feira</span>
</label>
<label className="flex items-center gap-2">
<input type="radio" name="weekday" value="tuesday" className="text-blue-600" />
<span className="whitespace-nowrap text-sm">Terça-Feira</span>
</label>
<label className="flex items-center gap-2">
<input type="radio" name="weekday" value="wednesday" className="text-blue-600" />
<span className="whitespace-nowrap text-sm">Quarta-Feira</span>
</label>
<label className="flex items-center gap-2">
<input type="radio" name="weekday" value="thursday" className="text-blue-600" />
<span className="whitespace-nowrap text-sm">Quinta-Feira</span>
</label>
<label className="flex items-center gap-2">
<input type="radio" name="weekday" value="friday" className="text-blue-600" />
<span className="whitespace-nowrap text-sm">Sexta-Feira</span>
</label>
<label className="flex items-center gap-2">
<input type="radio" name="weekday" value="saturday" className="text-blue-600" />
<span className="whitespace-nowrap text-sm">Sabado</span>
</label>
<label className="flex items-center gap-2">
<input type="radio" name="weekday" value="sunday" className="text-blue-600" />
<span className="whitespace-nowrap text-sm">Domingo</span>
</label>
</div>
</div>
</div>
<div className="grid md:grid-cols-5 gap-6">
<div>
<Label htmlFor="horarioEntrada" className="text-sm font-medium text-gray-700">
Horario De Entrada
</Label>
<Input type="time" id="horarioEntrada" name="horarioEntrada" required className="mt-1" />
</div>
<div>
<Label htmlFor="horarioSaida" className="text-sm font-medium text-gray-700">
Horario De Saida
</Label>
<Input type="time" id="horarioSaida" name="horarioSaida" required className="mt-1" />
</div>
<div>
<Label htmlFor="duracaoConsulta" className="text-sm font-medium text-gray-700">
Duração Da Consulta (min)
</Label>
<Input type="number" id="duracaoConsulta" name="duracaoConsulta" required className="mt-1" />
</div>
</div>
<div>
<Label htmlFor="modalidadeConsulta" className="text-sm font-medium text-gray-700">
Modalidade De Consulta
</Label>
<Select onValueChange={(value) => setModalidadeConsulta(value)} value={modalidadeConsulta}>
<SelectTrigger className="mt-1">
<SelectValue placeholder="Selecione" />
</SelectTrigger>
<SelectContent>
<SelectItem value="presencial">Presencial </SelectItem>
<SelectItem value="telemedicina">Telemedicina</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
<div className="flex justify-end gap-4">
<Link href="/doctor/disponibilidade/excecoes">
<Button variant="outline">Adicionar Exceção</Button>
</Link>
<Link href="/doctor/dashboard">
<Button variant="outline">Cancelar</Button>
</Link>
<Button type="submit" className="bg-green-600 hover:bg-green-700">
Salvar Disponibilidade
</Button>
</div>
</form>
</div>
</DoctorLayout>
);
}

View File

@ -1,11 +1,31 @@
// Caminho: app/(doctor)/login/page.tsx
import { LoginForm } from "@/components/LoginForm";
import Link from "next/link"; // Adicionado para o link de "Voltar"
export default function DoctorLoginPage() {
// NOTA: Esta página se tornou obsoleta com a criação do /login central.
// O ideal no futuro é deletar esta página e redirecionar os usuários.
return (
<div className="min-h-screen bg-gradient-to-br from-green-50 via-white to-green-50 flex items-center justify-center p-4">
<LoginForm title="Área do Médico" description="Acesse o sistema médico" role="doctor" themeColor="green" redirectPath="/doctor/medicos" />
<div className="w-full max-w-md text-center">
<h1 className="text-3xl font-bold text-foreground mb-2">Área do Médico</h1>
<p className="text-muted-foreground mb-8">Acesse o sistema médico</p>
{/* --- ALTERAÇÃO PRINCIPAL AQUI --- */}
{/* Chamando o LoginForm unificado sem props desnecessárias */}
<LoginForm>
{/* Adicionamos um link de "Voltar" como filho (children) */}
<div className="mt-6 text-center text-sm">
<Link href="/">
<span className="font-semibold text-primary hover:underline cursor-pointer">
Voltar à página inicial
</span>
</Link>
</div>
</LoginForm>
</div>
</div>
);
}
}

View File

@ -1,12 +1,31 @@
// Caminho: app/(finance)/login/page.tsx
import { LoginForm } from "@/components/LoginForm";
import Link from "next/link"; // Adicionado para o link de "Voltar"
export default function FinanceLoginPage() {
// NOTA: Esta página se tornou obsoleta com a criação do /login central.
// O ideal no futuro é deletar esta página e redirecionar os usuários.
return (
// Fundo com gradiente laranja, como no seu código original
<div className="min-h-screen bg-gradient-to-br from-orange-50 via-white to-orange-50 flex items-center justify-center p-4">
<LoginForm title="Área Financeira" description="Acesse o sistema de faturamento" role="finance" themeColor="orange" redirectPath="/finance/home" />
<div className="w-full max-w-md text-center">
<h1 className="text-3xl font-bold text-foreground mb-2">Área Financeira</h1>
<p className="text-muted-foreground mb-8">Acesse o sistema de faturamento</p>
{/* --- ALTERAÇÃO PRINCIPAL AQUI --- */}
{/* Chamando o LoginForm unificado sem props desnecessárias */}
<LoginForm>
{/* Adicionamos um link de "Voltar" como filho (children) */}
<div className="mt-6 text-center text-sm">
<Link href="/">
<span className="font-semibold text-primary hover:underline cursor-pointer">
Voltar à página inicial
</span>
</Link>
</div>
</LoginForm>
</div>
</div>
);
}
}

82
app/login/page.tsx Normal file
View File

@ -0,0 +1,82 @@
// Caminho: app/login/page.tsx
import { LoginForm } from "@/components/LoginForm";
import Link from "next/link";
import Image from "next/image";
import { Button } from "@/components/ui/button";
import { ArrowLeft } from "lucide-react"; // Importa o ícone de seta
export default function LoginPage() {
return (
<div className="min-h-screen grid grid-cols-1 lg:grid-cols-2">
{/* PAINEL ESQUERDO: O Formulário */}
<div className="relative flex flex-col items-center justify-center p-8 bg-background">
{/* Link para Voltar */}
<div className="absolute top-8 left-8">
<Link href="/" className="inline-flex items-center text-muted-foreground hover:text-primary transition-colors font-medium">
<ArrowLeft className="w-4 h-4 mr-2" />
Voltar à página inicial
</Link>
</div>
{/* O contêiner principal que agora terá a sombra e o estilo de card */}
<div className="w-full max-w-md bg-card p-10 rounded-2xl shadow-xl">
<div className="text-center mb-8">
<h1 className="text-3xl font-bold text-foreground">Acesse sua conta</h1>
<p className="text-muted-foreground mt-2">Bem-vindo(a) de volta ao MedConnect!</p>
</div>
<LoginForm>
{/* Children para o LoginForm */}
<div className="mt-4 text-center text-sm">
<Link href="/esqueci-minha-senha">
<span className="text-muted-foreground hover:text-primary cursor-pointer underline">
Esqueceu sua senha?
</span>
</Link>
</div>
</LoginForm>
<div className="mt-6 text-center text-sm">
<span className="text-muted-foreground">Não tem uma conta de paciente? </span>
<Link href="/patient/register">
<span className="font-semibold text-primary hover:underline cursor-pointer">
Crie uma agora
</span>
</Link>
</div>
</div>
</div>
{/* PAINEL DIREITO: A Imagem e Branding */}
<div className="hidden lg:block relative">
{/* Usamos o componente <Image> para otimização e performance */}
<Image
src="https://images.unsplash.com/photo-1576091160550-2173dba999ef?q=80&w=2070" // Uma imagem profissional de alta qualidade
alt="Médica utilizando um tablet na clínica MedConnect"
fill
style={{ objectFit: 'cover' }}
priority // Ajuda a carregar a imagem mais rápido
/>
{/* Camada de sobreposição para escurecer a imagem e destacar o texto */}
<div className="absolute inset-0 bg-primary/80 flex flex-col items-start justify-end p-12 text-left">
{/* BLOCO DE NOME ADICIONADO */}
<div className="mb-6 border-l-4 border-primary-foreground pl-4">
<h1 className="text-5xl font-extrabold text-primary-foreground tracking-wider">
MedConnect
</h1>
</div>
<h2 className="text-4xl font-bold text-primary-foreground leading-tight">
Tecnologia e Cuidado a Serviço da Sua Saúde.
</h2>
<p className="mt-4 text-lg text-primary-foreground/80">
Acesse seu portal para uma experiência de saúde integrada, segura e eficiente.
</p>
</div>
</div>
</div>
);
}

View File

@ -1,41 +1,105 @@
import ManagerLayout from "@/components/manager-layout"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Calendar, Clock, User, Plus } from "lucide-react"
import Link from "next/link"
"use client";
import ManagerLayout from "@/components/manager-layout";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Calendar, Clock, Plus, User } from "lucide-react";
import Link from "next/link";
import React, { useState, useEffect } from "react";
import { usersService } from "services/usersApi.mjs";
import { doctorsService } from "services/doctorsApi.mjs";
export default function ManagerDashboard() {
// 🔹 Estados para usuários
const [firstUser, setFirstUser] = useState<any>(null);
const [loadingUser, setLoadingUser] = useState(true);
// 🔹 Estados para médicos
const [doctors, setDoctors] = useState<any[]>([]);
const [loadingDoctors, setLoadingDoctors] = useState(true);
// 🔹 Buscar primeiro usuário
useEffect(() => {
async function fetchFirstUser() {
try {
const data = await usersService.list_roles();
if (Array.isArray(data) && data.length > 0) {
setFirstUser(data[0]);
}
} catch (error) {
console.error("Erro ao carregar usuário:", error);
} finally {
setLoadingUser(false);
}
}
fetchFirstUser();
}, []);
// 🔹 Buscar 3 primeiros médicos
useEffect(() => {
async function fetchDoctors() {
try {
const data = await doctorsService.list(); // ajuste se seu service tiver outro método
if (Array.isArray(data)) {
setDoctors(data.slice(0, 3)); // pega os 3 primeiros
}
} catch (error) {
console.error("Erro ao carregar médicos:", error);
} finally {
setLoadingDoctors(false);
}
}
fetchDoctors();
}, []);
return (
<ManagerLayout>
<div className="space-y-6">
{/* Cabeçalho */}
<div>
<h1 className="text-3xl font-bold text-gray-900">Dashboard</h1>
<p className="text-gray-600">Bem-vindo ao seu portal de consultas médicas</p>
</div>
{/* Cards principais */}
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{/* Card 1 */}
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Relatórios gerenciais</CardTitle>
<Calendar className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">3</div>
<p className="text-xs text-muted-foreground">2 não lidos, 1 lido</p>
<div className="text-2xl font-bold">0</div>
<p className="text-xs text-muted-foreground">Relatórios disponíveis</p>
</CardContent>
</Card>
{/* Card 2 — Gestão de usuários */}
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Gestão de usuários</CardTitle>
<Clock className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">João Marques</div>
<p className="text-xs text-muted-foreground">fez login a 13min</p>
{loadingUser ? (
<div className="text-gray-500 text-sm">Carregando usuário...</div>
) : firstUser ? (
<>
<div className="text-2xl font-bold">{firstUser.full_name || "Sem nome"}</div>
<p className="text-xs text-muted-foreground">
{firstUser.email || "Sem e-mail cadastrado"}
</p>
</>
) : (
<div className="text-sm text-gray-500">Nenhum usuário encontrado</div>
)}
</CardContent>
</Card>
{/* Card 3 — Perfil */}
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Perfil</CardTitle>
@ -48,66 +112,79 @@ export default function ManagerDashboard() {
</Card>
</div>
{/* Cards secundários */}
<div className="grid md:grid-cols-2 gap-6">
{/* Card — Ações rápidas */}
<Card>
<CardHeader>
<CardTitle>Ações Rápidas</CardTitle>
<CardDescription>Acesse rapidamente as principais funcionalidades</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<Link href="##">
<Link href="/manager/home">
<Button className="w-full justify-start">
<Plus className="mr-2 h-4 w-4" />
#
<User className="mr-2 h-4 w-4" />
Gestão de Médicos
</Button>
</Link>
<Link href="##">
<Button variant="outline" className="w-full justify-start bg-transparent">
<Calendar className="mr-2 h-4 w-4" />
#
</Button>
</Link>
<Link href="##">
<Link href="/manager/usuario">
<Button variant="outline" className="w-full justify-start bg-transparent">
<User className="mr-2 h-4 w-4" />
#
Usuários Cadastrados
</Button>
</Link>
<Link href="/manager/home/novo">
<Button variant="outline" className="w-full justify-start bg-transparent">
<Plus className="mr-2 h-4 w-4" />
Adicionar Novo Médico
</Button>
</Link>
<Link href="/manager/usuario/novo">
<Button variant="outline" className="w-full justify-start bg-transparent">
<Plus className="mr-2 h-4 w-4" />
Criar novo Usuário
</Button>
</Link>
</CardContent>
</Card>
{/* Card — Gestão de Médicos */}
<Card>
<CardHeader>
<CardTitle>Gestão de Médicos</CardTitle>
<CardDescription>Médicos online</CardDescription>
<CardDescription>Médicos cadastrados recentemente</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex items-center justify-between p-3 bg-blue-50 rounded-lg">
<div>
<p className="font-medium">Dr. Silva</p>
<p className="text-sm text-gray-600">Cardiologia</p>
</div>
<div className="text-right">
<p className="font-medium">On-line</p>
<p className="text-sm text-gray-600"></p>
</div>
{loadingDoctors ? (
<p className="text-sm text-gray-500">Carregando médicos...</p>
) : doctors.length === 0 ? (
<p className="text-sm text-gray-500">Nenhum médico cadastrado.</p>
) : (
<div className="space-y-4">
{doctors.map((doc, index) => (
<div
key={index}
className="flex items-center justify-between p-3 bg-green-50 rounded-lg border border-green-100"
>
<div>
<p className="font-medium">{doc.full_name || "Sem nome"}</p>
<p className="text-sm text-gray-600">
{doc.specialty || "Sem especialidade"}
</p>
</div>
<div className="text-right">
<p className="font-medium text-green-700">
{doc.active ? "Ativo" : "Inativo"}
</p>
</div>
</div>
))}
</div>
<div className="flex items-center justify-between p-3 bg-green-50 rounded-lg">
<div>
<p className="font-medium">Dra. Santos</p>
<p className="text-sm text-gray-600">Dermatologia</p>
</div>
<div className="text-right">
<p className="font-medium">Off-line</p>
<p className="text-sm text-gray-600">Visto as 8:33</p>
</div>
</div>
</div>
)}
</CardContent>
</Card>
</div>
</div>
</ManagerLayout>
)
);
}

View File

@ -1,12 +1,31 @@
// Caminho: app/(manager)/login/page.tsx
import { LoginForm } from "@/components/LoginForm";
import Link from "next/link"; // Adicionado para o link de "Voltar"
export default function ManagerLoginPage() {
// NOTA: Esta página se tornou obsoleta com a criação do /login central.
// O ideal no futuro é deletar esta página e redirecionar os usuários.
return (
// Mantemos o seu plano de fundo original
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-blue-50 flex items-center justify-center p-4">
<LoginForm title="Área do Gestor" description="Acesse o sistema médico" role="manager" themeColor="blue" redirectPath="/manager/home" />
<div className="w-full max-w-md text-center">
<h1 className="text-3xl font-bold text-foreground mb-2">Área do Gestor</h1>
<p className="text-muted-foreground mb-8">Acesse o sistema médico</p>
{/* --- ALTERAÇÃO PRINCIPAL AQUI --- */}
{/* Chamando o LoginForm unificado sem props desnecessárias */}
<LoginForm>
{/* Adicionamos um link de "Voltar" como filho (children) */}
<div className="mt-6 text-center text-sm">
<Link href="/">
<span className="font-semibold text-primary hover:underline cursor-pointer">
Voltar à página inicial
</span>
</Link>
</div>
</LoginForm>
</div>
</div>
);
}
}

View File

@ -18,14 +18,13 @@ import ManagerLayout from "@/components/manager-layout";
import { usersService } from "services/usersApi.mjs";
import { login } from "services/api.mjs";
// Adicionada a propriedade 'senha' e 'confirmarSenha'
interface UserFormData {
email: string;
nomeCompleto: string;
telefone: string;
papel: string;
senha: string;
confirmarSenha: string; // Novo campo para confirmação
confirmarSenha: string;
}
const defaultFormData: UserFormData = {
@ -37,7 +36,6 @@ const defaultFormData: UserFormData = {
confirmarSenha: "",
};
// Funções de formatação de telefone
const cleanNumber = (value: string): string => value.replace(/\D/g, "");
const formatPhone = (value: string): string => {
const cleaned = cleanNumber(value).substring(0, 11);
@ -63,13 +61,17 @@ export default function NovoUsuarioPage() {
e.preventDefault();
setError(null);
// Validação de campos obrigatórios
if (!formData.email || !formData.nomeCompleto || !formData.papel || !formData.senha || !formData.confirmarSenha) {
if (
!formData.email ||
!formData.nomeCompleto ||
!formData.papel ||
!formData.senha ||
!formData.confirmarSenha
) {
setError("Por favor, preencha todos os campos obrigatórios.");
return;
}
// Validação de senhas
if (formData.senha !== formData.confirmarSenha) {
setError("A Senha e a Confirmação de Senha não coincidem.");
return;
@ -85,19 +87,20 @@ export default function NovoUsuarioPage() {
email: formData.email.trim().toLowerCase(),
phone: formData.telefone || null,
role: formData.papel,
password: formData.senha, // Senha adicionada
password: formData.senha,
};
console.log("📤 Enviando payload:", payload);
await usersService.create_user(payload);
router.push("/manager/usuario");
} catch (e: any) {
console.error("Erro ao criar usuário:", e);
const msg =
e.message ||
"Não foi possível criar o usuário. Verifique os dados e tente novamente.";
setError(msg);
setError(
e?.message ||
"Não foi possível criar o usuário. Verifique os dados e tente novamente."
);
} finally {
setIsSaving(false);
}
@ -105,14 +108,13 @@ export default function NovoUsuarioPage() {
return (
<ManagerLayout>
{/* Container principal: w-full e centralizado. max-w-screen-lg para evitar expansão excessiva */}
<div className="w-full h-full p-4 md:p-8 flex justify-center items-start">
<div className="w-full max-w-screen-lg space-y-8">
{/* Cabeçalho */}
<div className="flex items-center justify-between border-b pb-4">
<div>
<h1 className="text-3xl font-extrabold text-gray-900">Novo Usuário</h1>
<h1 className="text-3xl font-extrabold text-gray-900">
Novo Usuário
</h1>
<p className="text-md text-gray-500">
Preencha os dados para cadastrar um novo usuário no sistema.
</p>
@ -122,7 +124,6 @@ export default function NovoUsuarioPage() {
</Link>
</div>
{/* Formulário */}
<form
onSubmit={handleSubmit}
className="space-y-6 bg-white p-6 md:p-10 border rounded-xl shadow-lg"
@ -130,14 +131,11 @@ export default function NovoUsuarioPage() {
{error && (
<div className="p-4 bg-red-50 text-red-700 rounded-lg border border-red-300">
<p className="font-semibold">Erro no Cadastro:</p>
<p className="text-sm">{error}</p>
<p className="text-sm break-words">{error}</p>
</div>
)}
{/* Campos de Entrada - Usando Grid de 2 colunas para organização */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Nome Completo - Largura total */}
<div className="space-y-2 md:col-span-2">
<Label htmlFor="nomeCompleto">Nome Completo *</Label>
<Input
@ -151,7 +149,6 @@ export default function NovoUsuarioPage() {
/>
</div>
{/* E-mail (Coluna 1) */}
<div className="space-y-2">
<Label htmlFor="email">E-mail *</Label>
<Input
@ -164,7 +161,6 @@ export default function NovoUsuarioPage() {
/>
</div>
{/* Papel (Função) (Coluna 2) */}
<div className="space-y-2">
<Label htmlFor="papel">Papel (Função) *</Label>
<Select
@ -185,7 +181,6 @@ export default function NovoUsuarioPage() {
</Select>
</div>
{/* Senha (Coluna 1) */}
<div className="space-y-2">
<Label htmlFor="senha">Senha *</Label>
<Input
@ -194,28 +189,32 @@ export default function NovoUsuarioPage() {
value={formData.senha}
onChange={(e) => handleInputChange("senha", e.target.value)}
placeholder="Mínimo 8 caracteres"
minLength={8}
minLength={8}
required
/>
</div>
{/* Confirmar Senha (Coluna 2) */}
<div className="space-y-2">
<Label htmlFor="confirmarSenha">Confirmar Senha *</Label>
<Input
id="confirmarSenha"
type="password"
value={formData.confirmarSenha}
onChange={(e) => handleInputChange("confirmarSenha", e.target.value)}
onChange={(e) =>
handleInputChange("confirmarSenha", e.target.value)
}
placeholder="Repita a senha"
required
/>
{formData.senha && formData.confirmarSenha && formData.senha !== formData.confirmarSenha && (
<p className="text-xs text-red-500">As senhas não coincidem.</p>
)}
{formData.senha &&
formData.confirmarSenha &&
formData.senha !== formData.confirmarSenha && (
<p className="text-xs text-red-500">
As senhas não coincidem.
</p>
)}
</div>
{/* Telefone (Opcional, mas mantido no grid) */}
<div className="space-y-2">
<Label htmlFor="telefone">Telefone</Label>
<Input
@ -228,10 +227,8 @@ export default function NovoUsuarioPage() {
maxLength={15}
/>
</div>
</div>
{/* Botões de Ação */}
<div className="flex justify-end gap-4 pt-6 border-t mt-6">
<Link href="/manager/usuario">
<Button type="button" variant="outline" disabled={isSaving}>
@ -256,4 +253,4 @@ export default function NovoUsuarioPage() {
</div>
</ManagerLayout>
);
}
}

View File

@ -17,7 +17,7 @@ export default function InicialPage() {
<header className="bg-card shadow-md py-4 px-6 flex justify-between items-center">
<h1 className="text-2xl font-bold text-primary">MediConnect</h1>
<nav className="flex space-x-6 text-muted-foreground font-medium">
<a href="#home" className="hover:text-primary">Home</a>
<a href="#home" className="hover:text-primary"><Link href="/cadastro">Home</Link></a>
<a href="#about" className="hover:text-primary">Sobre</a>
<a href="#departments" className="hover:text-primary">Departamentos</a>
<a href="#doctors" className="hover:text-primary">Médicos</a>
@ -25,7 +25,7 @@ export default function InicialPage() {
</nav>
<div className="flex space-x-4">
{}
<Link href="/cadastro">
<Link href="/login">
<Button
variant="outline"
className="rounded-full px-6 py-2 border-2 transition cursor-pointer"

View File

@ -1,4 +1,4 @@
// Caminho: app/(patient)/login/page.tsx
// Caminho: app/patient/login/page.tsx
import Link from "next/link";
import { LoginForm } from "@/components/LoginForm";
@ -6,6 +6,12 @@ import { Button } from "@/components/ui/button";
import { ArrowLeft } from "lucide-react";
export default function PatientLoginPage() {
// NOTA: Esta página de login específica para pacientes se tornou obsoleta
// com a criação da nossa página de login central em /login.
// Mantemos este arquivo por enquanto para evitar quebrar outras partes do código,
// mas o ideal no futuro seria deletar esta página e redirecionar
// /patient/login para /login.
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-50 flex flex-col items-center justify-center p-4">
<div className="w-full max-w-md">
@ -16,20 +22,25 @@ export default function PatientLoginPage() {
</Link>
</div>
<LoginForm title="Área do Paciente" description="Acesse sua conta para gerenciar consultas" role="patient" themeColor="blue" redirectPath="/patient/dashboard">
{/* --- ALTERAÇÃO PRINCIPAL AQUI --- */}
{/* Removemos as props desnecessárias (title, description, role, etc.) */}
{/* O novo LoginForm é autônomo e não precisa mais delas. */}
<LoginForm>
{/* Este bloco é passado como 'children' para o LoginForm */}
<Link href="/patient/register" passHref>
<Button variant="outline" className="w-full h-12 text-base">
Criar nova conta
</Button>
</Link>
<div className="mt-6 text-center text-sm">
<span className="text-muted-foreground">Não tem uma conta? </span>
<Link href="/patient/register">
<span className="font-semibold text-primary hover:underline cursor-pointer">
Crie uma agora
</span>
</Link>
</div>
</LoginForm>
{/* Conteúdo e espaçamento restaurados */}
<div className="mt-8 text-center">
<p className="text-sm text-muted-foreground">Problemas para acessar? Entre em contato conosco</p>
</div>
</div>
</div>
</div>
);
}
}

View File

@ -1,41 +1,207 @@
import SecretaryLayout from "@/components/secretary-layout"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Calendar, Clock, User, Plus } from "lucide-react"
import Link from "next/link"
"use client";
import SecretaryLayout from "@/components/secretary-layout";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Calendar, Clock, User, Plus } from "lucide-react";
import Link from "next/link";
import React, { useState, useEffect } from "react";
import { patientsService } from "@/services/patientsApi.mjs";
import { appointmentsService } from "@/services/appointmentsApi.mjs";
export default function SecretaryDashboard() {
// Estados
const [patients, setPatients] = useState<any[]>([]);
const [loadingPatients, setLoadingPatients] = useState(true);
const [firstConfirmed, setFirstConfirmed] = useState<any>(null);
const [nextAgendada, setNextAgendada] = useState<any>(null);
const [loadingAppointments, setLoadingAppointments] = useState(true);
// 🔹 Buscar pacientes
useEffect(() => {
async function fetchPatients() {
try {
const data = await patientsService.list();
if (Array.isArray(data)) {
setPatients(data.slice(0, 3));
}
} catch (error) {
console.error("Erro ao carregar pacientes:", error);
} finally {
setLoadingPatients(false);
}
}
fetchPatients();
}, []);
// 🔹 Buscar consultas (confirmadas + 1ª do mês)
useEffect(() => {
async function fetchAppointments() {
try {
const hoje = new Date();
const inicioMes = new Date(hoje.getFullYear(), hoje.getMonth(), 1);
const fimMes = new Date(hoje.getFullYear(), hoje.getMonth() + 1, 0);
// Mesmo parâmetro de ordenação da página /secretary/appointments
const queryParams = "order=scheduled_at.desc";
const data = await appointmentsService.search_appointment(queryParams);
if (!Array.isArray(data) || data.length === 0) {
setFirstConfirmed(null);
setNextAgendada(null);
return;
}
// 🩵 1⃣ Consultas confirmadas (para o card “Próxima Consulta Confirmada”)
const confirmadas = data.filter((apt: any) => {
const dataConsulta = new Date(apt.scheduled_at || apt.date);
return apt.status === "confirmed" && dataConsulta >= hoje;
});
confirmadas.sort(
(a: any, b: any) =>
new Date(a.scheduled_at || a.date).getTime() -
new Date(b.scheduled_at || b.date).getTime()
);
setFirstConfirmed(confirmadas[0] || null);
// 💙 2⃣ Consultas deste mês — pegar sempre a 1ª (mais próxima)
const consultasMes = data.filter((apt: any) => {
const dataConsulta = new Date(apt.scheduled_at);
return dataConsulta >= inicioMes && dataConsulta <= fimMes;
});
if (consultasMes.length > 0) {
consultasMes.sort(
(a: any, b: any) =>
new Date(a.scheduled_at).getTime() -
new Date(b.scheduled_at).getTime()
);
setNextAgendada(consultasMes[0]);
} else {
setNextAgendada(null);
}
} catch (error) {
console.error("Erro ao carregar consultas:", error);
} finally {
setLoadingAppointments(false);
}
}
fetchAppointments();
}, []);
return (
<SecretaryLayout>
<div className="space-y-6">
{/* Cabeçalho */}
<div>
<h1 className="text-3xl font-bold text-gray-900">Dashboard</h1>
<p className="text-gray-600">Bem-vindo ao seu portal de consultas médicas</p>
</div>
{/* Cards principais */}
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{/* Próxima Consulta Confirmada */}
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Próxima Consulta</CardTitle>
<CardTitle className="text-sm font-medium">
Próxima Consulta Confirmada
</CardTitle>
<Calendar className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">15 Jan</div>
<p className="text-xs text-muted-foreground">Dr. Silva - 14:30</p>
{loadingAppointments ? (
<div className="text-gray-500 text-sm">
Carregando próxima consulta...
</div>
) : firstConfirmed ? (
<>
<div className="text-2xl font-bold">
{new Date(
firstConfirmed.scheduled_at || firstConfirmed.date
).toLocaleDateString("pt-BR")}
</div>
<p className="text-xs text-muted-foreground">
{firstConfirmed.doctor_name
? `Dr(a). ${firstConfirmed.doctor_name}`
: "Médico não informado"}{" "}
-{" "}
{new Date(
firstConfirmed.scheduled_at
).toLocaleTimeString("pt-BR", {
hour: "2-digit",
minute: "2-digit",
})}
</p>
</>
) : (
<div className="text-sm text-gray-500">
Nenhuma consulta confirmada encontrada
</div>
)}
</CardContent>
</Card>
{/* Consultas Este Mês */}
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Consultas Este Mês</CardTitle>
<CardTitle className="text-sm font-medium">
Consultas Este Mês
</CardTitle>
<Clock className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">3</div>
<p className="text-xs text-muted-foreground">2 realizadas, 1 agendada</p>
{loadingAppointments ? (
<div className="text-gray-500 text-sm">
Carregando consultas...
</div>
) : nextAgendada ? (
<>
<div className="text-lg font-bold text-gray-900">
{new Date(
nextAgendada.scheduled_at
).toLocaleDateString("pt-BR", {
day: "2-digit",
month: "2-digit",
year: "numeric",
})}{" "}
às{" "}
{new Date(
nextAgendada.scheduled_at
).toLocaleTimeString("pt-BR", {
hour: "2-digit",
minute: "2-digit",
})}
</div>
<p className="text-xs text-muted-foreground">
{nextAgendada.doctor_name
? `Dr(a). ${nextAgendada.doctor_name}`
: "Médico não informado"}
</p>
<p className="text-xs text-muted-foreground">
{nextAgendada.patient_name
? `Paciente: ${nextAgendada.patient_name}`
: ""}
</p>
</>
) : (
<div className="text-sm text-gray-500">
Nenhuma consulta agendada neste mês
</div>
)}
</CardContent>
</Card>
{/* Perfil */}
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Perfil</CardTitle>
@ -48,11 +214,15 @@ export default function SecretaryDashboard() {
</Card>
</div>
{/* Cards Secundários */}
<div className="grid md:grid-cols-2 gap-6">
{/* Ações rápidas */}
<Card>
<CardHeader>
<CardTitle>Ações Rápidas</CardTitle>
<CardDescription>Acesse rapidamente as principais funcionalidades</CardDescription>
<CardDescription>
Acesse rapidamente as principais funcionalidades
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<Link href="/secretary/schedule">
@ -62,52 +232,73 @@ export default function SecretaryDashboard() {
</Button>
</Link>
<Link href="/secretary/appointments">
<Button variant="outline" className="w-full justify-start bg-transparent">
<Button
variant="outline"
className="w-full justify-start bg-transparent"
>
<Calendar className="mr-2 h-4 w-4" />
Ver Consultas
</Button>
</Link>
<Link href="##">
<Button variant="outline" className="w-full justify-start bg-transparent">
<Link href="/secretary/pacientes">
<Button
variant="outline"
className="w-full justify-start bg-transparent"
>
<User className="mr-2 h-4 w-4" />
Atualizar Dados
Gerenciar Pacientes
</Button>
</Link>
</CardContent>
</Card>
{/* Pacientes */}
<Card>
<CardHeader>
<CardTitle>Próximas Consultas</CardTitle>
<CardDescription>Suas consultas agendadas</CardDescription>
<CardTitle>Pacientes</CardTitle>
<CardDescription>
Últimos pacientes cadastrados
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex items-center justify-between p-3 bg-blue-50 rounded-lg">
<div>
<p className="font-medium">Dr. Silva</p>
<p className="text-sm text-gray-600">Cardiologia</p>
</div>
<div className="text-right">
<p className="font-medium">15 Jan</p>
<p className="text-sm text-gray-600">14:30</p>
</div>
{loadingPatients ? (
<p className="text-sm text-gray-500">
Carregando pacientes...
</p>
) : patients.length === 0 ? (
<p className="text-sm text-gray-500">
Nenhum paciente cadastrado.
</p>
) : (
<div className="space-y-4">
{patients.map((patient, index) => (
<div
key={index}
className="flex items-center justify-between p-3 bg-blue-50 rounded-lg border border-blue-100"
>
<div>
<p className="font-medium text-gray-900">
{patient.full_name || "Sem nome"}
</p>
<p className="text-sm text-gray-600">
{patient.phone_mobile ||
patient.phone1 ||
"Sem telefone"}
</p>
</div>
<div className="text-right">
<p className="font-medium text-blue-700">
{patient.convenio || "Particular"}
</p>
</div>
</div>
))}
</div>
<div className="flex items-center justify-between p-3 bg-green-50 rounded-lg">
<div>
<p className="font-medium">Dra. Santos</p>
<p className="text-sm text-gray-600">Dermatologia</p>
</div>
<div className="text-right">
<p className="font-medium">22 Jan</p>
<p className="text-sm text-gray-600">10:00</p>
</div>
</div>
</div>
)}
</CardContent>
</Card>
</div>
</div>
</SecretaryLayout>
)
);
}

View File

@ -1,11 +1,31 @@
// Caminho: app/(secretary)/login/page.tsx
import { LoginForm } from "@/components/LoginForm";
import Link from "next/link"; // Adicionado para o link de "Voltar"
export default function SecretaryLoginPage() {
// NOTA: Esta página se tornou obsoleta com a criação do /login central.
// O ideal no futuro é deletar esta página e redirecionar os usuários.
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-blue-50 flex items-center justify-center p-4">
<LoginForm title="Área da Secretária" description="Acesse o sistema de gerenciamento" role="secretary" themeColor="blue" redirectPath="/secretary/pacientes" />
</div>
<div className="w-full max-w-md text-center">
<h1 className="text-3xl font-bold text-foreground mb-2">Área da Secretária</h1>
<p className="text-muted-foreground mb-8">Acesse o sistema de gerenciamento</p>
{/* --- ALTERAÇÃO PRINCIPAL AQUI --- */}
{/* Chamando o LoginForm unificado sem props desnecessárias */}
<LoginForm>
{/* Adicionamos um link de "Voltar" como filho (children) */}
<div className="mt-6 text-center text-sm">
<Link href="/">
<span className="font-semibold text-primary hover:underline cursor-pointer">
Voltar à página inicial
</span>
</Link>
</div>
</LoginForm>
</div>
</div>
);
}
}

View File

@ -1,21 +1,21 @@
// Caminho: components/LoginForm.tsx
"use client";
"use client"
import type React from "react";
import { useState } from "react";
import { useRouter } from "next/navigation";
import Link from "next/link";
import Cookies from "js-cookie";
import { jwtDecode } from "jwt-decode";
import { cn } from "@/lib/utils";
import type React from "react"
import { useState } from "react"
import { useRouter } from "next/navigation"
import Link from "next/link"
import { cn } from "@/lib/utils"
// Nossos serviços de API centralizados
import { loginWithEmailAndPassword, api } from "@/services/api.mjs";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import { apikey } from "@/services/api.mjs";
import { useToast } from "@/hooks/use-toast";
@ -24,200 +24,137 @@ import { useToast } from "@/hooks/use-toast";
import { Eye, EyeOff, Mail, Lock, Loader2, UserCheck, Stethoscope, IdCard, Receipt } from "lucide-react";
interface LoginFormProps {
title: string;
description: string;
role: "secretary" | "doctor" | "patient" | "admin" | "manager" | "finance";
themeColor: "blue" | "green" | "orange";
redirectPath: string;
children?: React.ReactNode;
children?: React.ReactNode
}
interface FormState {
email: string;
password: string;
email: string
password: string
}
// Supondo que o payload do seu token tenha esta estrutura
interface DecodedToken {
name: string;
email: string;
role: string;
exp: number;
// Adicione outros campos que seu token possa ter
}
export function LoginForm({ children }: LoginFormProps) {
const [form, setForm] = useState<FormState>({ email: "", password: "" })
const [showPassword, setShowPassword] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const router = useRouter()
const { toast } = useToast()
const themeClasses = {
blue: {
iconBg: "bg-blue-100",
iconText: "text-blue-600",
button: "bg-blue-600 hover:bg-blue-700",
link: "text-blue-600 hover:text-blue-700",
focus: "focus:border-blue-500 focus:ring-blue-500",
},
green: {
iconBg: "bg-green-100",
iconText: "text-green-600",
button: "bg-green-600 hover:bg-green-700",
link: "text-green-600 hover:text-green-700",
focus: "focus:border-green-500 focus:ring-green-500",
},
orange: {
iconBg: "bg-orange-100",
iconText: "text-orange-600",
button: "bg-orange-600 hover:bg-orange-700",
link: "text-orange-600 hover:text-orange-700",
focus: "focus:border-orange-500 focus:ring-orange-500",
},
};
// ==================================================================
// LÓGICA DE LOGIN INTELIGENTE E CENTRALIZADA
// ==================================================================
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
localStorage.removeItem("token");
localStorage.removeItem("user_info");
const roleIcons = {
secretary: UserCheck,
patient: Stethoscope,
doctor: Stethoscope,
admin: UserCheck,
manager: IdCard,
finance: Receipt,
};
export function LoginForm({ title, description, role, themeColor, redirectPath, children }: LoginFormProps) {
const [form, setForm] = useState<FormState>({ email: "", password: "" });
const [showPassword, setShowPassword] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const router = useRouter();
const { toast } = useToast();
const currentTheme = themeClasses[themeColor];
const Icon = roleIcons[role];
// ==================================================================
// AJUSTE PRINCIPAL NA LÓGICA DE LOGIN
// ==================================================================
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
const LOGIN_URL = "https://yuanqfswhberkoevtmfr.supabase.co/auth/v1/token?grant_type=password";
const API_KEY = apikey;
if (!API_KEY) {
toast({
title: "Erro de Configuração",
description: "A chave da API não foi encontrada.",
});
setIsLoading(false);
return;
try {
const authData = await loginWithEmailAndPassword(form.email, form.password);
const user = authData.user;
if (!user || !user.id) {
throw new Error("Resposta de autenticação inválida: ID do usuário não encontrado.");
}
try {
const response = await fetch(LOGIN_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
apikey: API_KEY,
},
body: JSON.stringify({ email: form.email, password: form.password }),
});
const rolesData = await api.get(`/rest/v1/user_roles?user_id=eq.${user.id}&select=role`);
const data = await response.json();
if (!response.ok) {
throw new Error(data.error_description || "Credenciais inválidas. Tente novamente.");
}
const accessToken = data.access_token;
const user = data.user;
/* =================== Verificação de Role Desativada Temporariamente =================== */
// if (user.user_metadata.role !== role) {
// toast({ title: "Acesso Negado", ... });
// return;
// }
/* ===================================================================================== */
Cookies.set("access_token", accessToken, { expires: 1, secure: true });
localStorage.setItem("user_info", JSON.stringify(user));
toast({
title: "Login bem-sucedido!",
description: `Bem-vindo(a), ${user.user_metadata.full_name || "usuário"}! Redirecionando...`,
});
router.push(redirectPath);
} catch (error) {
toast({
title: "Erro no Login",
description: error instanceof Error ? error.message : "Ocorreu um erro inesperado.",
});
} finally {
setIsLoading(false);
if (!rolesData || rolesData.length === 0) {
throw new Error("Login bem-sucedido, mas nenhum perfil de acesso foi encontrado para este usuário.");
}
};
// O JSX do return permanece exatamente o mesmo, preservando seus ajustes.
return (
<Card className="w-full max-w-md shadow-xl border-0 bg-white/80 backdrop-blur-sm">
<CardHeader className="text-center space-y-4 pb-8">
<div className={cn("mx-auto w-16 h-16 rounded-full flex items-center justify-center", currentTheme.iconBg)}>
<Icon className={cn("w-8 h-8", currentTheme.iconText)} />
</div>
<div>
<CardTitle className="text-2xl font-bold text-gray-900">{title}</CardTitle>
<CardDescription className="text-gray-600 mt-2">{description}</CardDescription>
</div>
</CardHeader>
<CardContent className="px-8 pb-8">
<form onSubmit={handleSubmit} className="space-y-6">
{/* Inputs e Botão */}
<div className="space-y-2">
<Label htmlFor="email">E-mail</Label>
<div className="relative">
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" />
<Input id="email" type="email" placeholder="seu.email@clinica.com" value={form.email} onChange={(e) => setForm({ ...form, email: e.target.value })} className={cn("pl-11 h-12 border-slate-200", currentTheme.focus)} required disabled={isLoading} />
</div>
</div>
<div className="space-y-2">
<Label htmlFor="password">Senha</Label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" />
<Input id="password" type={showPassword ? "text" : "password"} placeholder="Digite sua senha" value={form.password} onChange={(e) => setForm({ ...form, password: e.target.value })} className={cn("pl-11 pr-12 h-12 border-slate-200", currentTheme.focus)} required disabled={isLoading} />
<button type="button" onClick={() => setShowPassword(!showPassword)} className="absolute right-2 top-1/2 -translate-y-1/2 h-8 w-8 p-0 text-gray-400 hover:text-gray-600" disabled={isLoading}>
{showPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button>
</div>
</div>
<Button type="submit" className={cn("w-full h-12 text-base font-semibold", currentTheme.button)} disabled={isLoading}>
{isLoading ? <Loader2 className="w-5 h-5 animate-spin" /> : "Entrar"}
</Button>
</form>
{/* Conteúdo Extra (children) */}
<div className="mt-8">
{children ? (
<div className="space-y-4">
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-slate-200"></div>
</div>
<div className="relative flex justify-center text-sm">
<span className="px-4 bg-white text-slate-500">Novo por aqui?</span>
</div>
</div>
{children}
</div>
) : (
<>
<div className="relative">
<Separator className="my-6" />
<span className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 bg-white px-3 text-sm text-gray-500">ou</span>
</div>
<div className="text-center">
<Link href="/" className={cn("text-sm font-medium hover:underline", currentTheme.link)}>
Voltar à página inicial
</Link>
</div>
</>
)}
</div>
</CardContent>
</Card>
);
}
const userRole = rolesData[0].role;
const completeUserInfo = { ...user, user_metadata: { ...user.user_metadata, role: userRole } };
localStorage.setItem('user_info', JSON.stringify(completeUserInfo));
let redirectPath = "";
switch (userRole) {
case "admin":
case "manager": redirectPath = "/manager/home"; break;
case "medico": redirectPath = "/doctor/medicos"; break;
case "secretary": redirectPath = "/secretary/pacientes"; break;
case "patient": redirectPath = "/patient/dashboard"; break;
case "finance": redirectPath = "/finance/home"; break;
}
if (!redirectPath) {
throw new Error(`O perfil de acesso '${userRole}' não é válido para login. Contate o suporte.`);
}
toast({
title: "Login bem-sucedido!",
description: `Bem-vindo(a)! Redirecionando...`,
});
router.push(redirectPath);
} catch (error) {
localStorage.removeItem("token");
localStorage.removeItem("user_info");
console.error("ERRO DETALHADO NO CATCH:", error);
toast({
title: "Erro no Login",
description: error instanceof Error ? error.message : "Ocorreu um erro inesperado.",
});
} finally {
setIsLoading(false);
}
}
// ==================================================================
// JSX VISUALMENTE RICO E UNIFICADO
// ==================================================================
return (
// Usamos Card e CardContent para manter a consistência, mas o estilo principal
// virá da página 'app/login/page.tsx' que envolve este componente.
<Card className="w-full bg-transparent border-0 shadow-none">
<CardContent className="p-0"> {/* Removemos o padding para dar controle à página pai */}
<form onSubmit={handleSubmit} className="space-y-6">
<div className="space-y-2">
<Label htmlFor="email">E-mail</Label>
<div className="relative">
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground w-5 h-5" />
<Input
id="email"
type="email"
placeholder="seu.email@exemplo.com"
value={form.email}
onChange={(e) => setForm({ ...form, email: e.target.value })}
className="pl-10 h-11"
required
disabled={isLoading}
autoComplete="username" // Boa prática de acessibilidade
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="password">Senha</Label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground w-5 h-5" />
<Input
id="password"
type={showPassword ? "text" : "password"}
placeholder="Digite sua senha"
value={form.password}
onChange={(e) => setForm({ ...form, password: e.target.value })}
className="pl-10 pr-12 h-11"
required
disabled={isLoading}
autoComplete="current-password" // Boa prática de acessibilidade
/>
<button type="button" onClick={() => setShowPassword(!showPassword)} className="absolute right-2 top-1/2 -translate-y-1/2 h-8 w-8 p-0 text-muted-foreground hover:text-foreground" disabled={isLoading}>
{showPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button>
</div>
</div>
<Button type="submit" className="w-full h-11 text-base font-semibold" disabled={isLoading}>
{isLoading ? <Loader2 className="w-5 h-5 animate-spin" /> : "Entrar"}
</Button>
</form>
{/* O children permite que a página de login adicione links extras aqui */}
{children}
</CardContent>
</Card>
)
}

View File

@ -4,7 +4,8 @@ import type React from "react";
import { useState, useEffect } from "react";
import { useRouter, usePathname } from "next/navigation";
import Link from "next/link";
import Cookies from "js-cookie"; // <-- 1. IMPORTAÇÃO ADICIONADA
import Cookies from "js-cookie"; // Manteremos para o logout, se necessário
import { api } from "@/services/api.mjs";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
@ -39,23 +40,20 @@ export default function DoctorLayout({ children }: PatientLayoutProps) {
const router = useRouter();
const pathname = usePathname();
// ==================================================================
// 2. BLOCO DE SEGURANÇA CORRIGIDO
// ==================================================================
useEffect(() => {
const userInfoString = localStorage.getItem("user_info");
const token = Cookies.get("access_token");
// --- ALTERAÇÃO PRINCIPAL AQUI ---
// Procurando o token no localStorage, onde ele foi realmente salvo.
const token = localStorage.getItem("token");
if (userInfoString && token) {
const userInfo = JSON.parse(userInfoString);
// 3. "TRADUZIMOS" os dados da API para o formato que o layout espera
setDoctorData({
id: userInfo.id || "",
name: userInfo.user_metadata?.full_name || "Doutor(a)",
email: userInfo.email || "",
specialty: userInfo.user_metadata?.specialty || "Especialidade",
// Campos que não vêm do login, definidos como vazios para não quebrar
phone: userInfo.phone || "",
cpf: "",
crm: "",
@ -63,34 +61,47 @@ export default function DoctorLayout({ children }: PatientLayoutProps) {
permissions: {},
});
} else {
// Se faltar o token ou os dados, volta para o login
router.push("/doctor/login");
// Se não encontrar, aí sim redireciona.
router.push("/login");
}
}, [router]);
// O restante do seu código permanece exatamente o mesmo...
useEffect(() => {
const handleResize = () => setWindowWidth(window.innerWidth);
handleResize(); // inicializa com a largura atual
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
const handleResize = () => setWindowWidth(window.innerWidth);
handleResize();
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
useEffect(() => {
if (isMobile) {
setSidebarCollapsed(true);
} else {
setSidebarCollapsed(false);
}
}, [isMobile]);
useEffect(() => {
if (isMobile) {
setSidebarCollapsed(true);
} else {
setSidebarCollapsed(false);
}
}, [isMobile]);
const handleLogout = () => {
setShowLogoutDialog(true);
};
const confirmLogout = () => {
localStorage.removeItem("doctorData");
setShowLogoutDialog(false);
router.push("/");
// --- ALTERAÇÃO 2: A função de logout agora é MUITO mais simples ---
const confirmLogout = async () => {
try {
// Chama a função centralizada para fazer o logout no servidor
await api.logout();
} catch (error) {
// O erro já é logado dentro da função api.logout, não precisamos fazer nada aqui
} finally {
// A responsabilidade do componente é apenas limpar o estado local e redirecionar
localStorage.removeItem("user_info");
localStorage.removeItem("token");
Cookies.remove("access_token"); // Limpeza de segurança
setShowLogoutDialog(false);
router.push("/"); // Redireciona para a home
}
};
const cancelLogout = () => {
@ -103,7 +114,7 @@ useEffect(() => {
const menuItems = [
{
href: "#",
href: "/doctor/dashboard",
icon: Home,
label: "Dashboard",
// Botão para o dashboard do médico
@ -126,6 +137,12 @@ useEffect(() => {
label: "Pacientes",
// Botão para a página de visualização de todos os pacientes
},
{
href: "/doctor/disponibilidade",
icon: Calendar,
label: "Disponibilidade",
// Botão para o dashboard do médico
},
];
if (!doctorData) {
@ -133,10 +150,10 @@ useEffect(() => {
}
return (
<div className="min-h-screen bg-gray-50 flex">
{/* Sidebar para desktop */}
<div className={`bg-white border-r border-gray-200 transition-all duration-300 ${sidebarCollapsed ? "w-16" : "w-64"} fixed left-0 top-0 h-screen flex flex-col z-50`}>
<div className="p-4 border-b border-gray-200">
// O restante do seu código JSX permanece exatamente o mesmo
<div className="min-h-screen bg-background flex">
<div className={`bg-card border-r border transition-all duration-300 ${sidebarCollapsed ? "w-16" : "w-64"} fixed left-0 top-0 h-screen flex flex-col z-50`}>
<div className="p-4 border-b border">
<div className="flex items-center justify-between">
{!sidebarCollapsed && (
<div className="flex items-center gap-2">
@ -151,7 +168,6 @@ useEffect(() => {
</Button>
</div>
</div>
<nav className="flex-1 p-2 overflow-y-auto">
{menuItems.map((item) => {
const Icon = item.icon;
@ -167,49 +183,62 @@ useEffect(() => {
);
})}
</nav>
// ... (seu código anterior)
{/* Sidebar para desktop */}
<div className={`bg-white border-r border-gray-200 transition-all duration-300 ${sidebarCollapsed ? "w-16" : "w-64"} fixed left-0 top-0 h-screen flex flex-col z-50`}>
<div className="p-4 border-b border-gray-200">
<div className="flex items-center justify-between">
{!sidebarCollapsed && (
<div className="flex items-center gap-2">
<div className="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center">
<div className="w-4 h-4 bg-white rounded-sm"></div>
{/* Sidebar para desktop */}
<div className={`bg-white border-r border-gray-200 transition-all duration-300 ${sidebarCollapsed ? "w-16" : "w-64"} fixed left-0 top-0 h-screen flex flex-col z-50`}>
<div className="p-4 border-b border-gray-200">
<div className="flex items-center justify-between">
{!sidebarCollapsed && (
<div className="flex items-center gap-2">
<div className="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center">
<div className="w-4 h-4 bg-white rounded-sm"></div>
</div>
<span className="font-semibold text-gray-900">MediConnect</span>
</div>
<span className="font-semibold text-gray-900">MediConnect</span>
</div>
)}
<Button variant="ghost" size="sm" onClick={() => setSidebarCollapsed(!sidebarCollapsed)} className="p-1">
{sidebarCollapsed ? <ChevronRight className="w-4 h-4" /> : <ChevronLeft className="w-4 h-4" />}
</Button>
)}
<Button variant="ghost" size="sm" onClick={() => setSidebarCollapsed(!sidebarCollapsed)} className="p-1">
{sidebarCollapsed ? <ChevronRight className="w-4 h-4" /> : <ChevronLeft className="w-4 h-4" />}
</Button>
</div>
</div>
</div>
<nav className="flex-1 p-2 overflow-y-auto">
{menuItems.map((item) => {
const Icon = item.icon;
const isActive = pathname === item.href || (item.href !== "/" && pathname.startsWith(item.href));
<nav className="flex-1 p-2 overflow-y-auto">
{menuItems.map((item) => {
const Icon = item.icon;
const isActive = pathname === item.href || (item.href !== "/" && pathname.startsWith(item.href));
return (
<Link key={item.href} href={item.href}>
<div className={`flex items-center gap-3 px-3 py-2 rounded-lg mb-1 transition-colors ${isActive ? "bg-blue-50 text-blue-600 border-r-2 border-blue-600" : "text-gray-600 hover:bg-gray-50"}`}>
<Icon className="w-5 h-5 flex-shrink-0" />
{!sidebarCollapsed && <span className="font-medium">{item.label}</span>}
</div>
</Link>
);
})}
</nav>
return (
<Link key={item.href} href={item.href}>
<div className={`flex items-center gap-3 px-3 py-2 rounded-lg mb-1 transition-colors ${isActive ? "bg-blue-50 text-blue-600 border-r-2 border-blue-600" : "text-gray-600 hover:bg-gray-50"}`}>
<Icon className="w-5 h-5 flex-shrink-0" />
{!sidebarCollapsed && <span className="font-medium">{item.label}</span>}
</div>
</Link>
);
})}
</nav>
<div className="border-t p-4 mt-auto">
<div className="flex items-center space-x-3 mb-4">
{/* Se a sidebar estiver recolhida, o avatar e o texto do usuário também devem ser condensados ou ocultados */}
{!sidebarCollapsed && (
<>
<Avatar>
<div className="border-t p-4 mt-auto">
<div className="flex items-center space-x-3 mb-4">
{!sidebarCollapsed && (
<>
<Avatar>
<AvatarImage src="/placeholder.svg?height=40&width=40" />
<AvatarFallback>
{doctorData.name
.split(" ")
.map((n) => n[0])
.join("")}
</AvatarFallback>
</Avatar>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-900 truncate">{doctorData.name}</p>
<p className="text-xs text-gray-500 truncate">{doctorData.specialty}</p>
</div>
</>
)}
{sidebarCollapsed && (
<Avatar className="mx-auto">
<AvatarImage src="/placeholder.svg?height=40&width=40" />
<AvatarFallback>
{doctorData.name
@ -218,40 +247,17 @@ useEffect(() => {
.join("")}
</AvatarFallback>
</Avatar>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-900 truncate">{doctorData.name}</p>
<p className="text-xs text-gray-500 truncate">{doctorData.specialty}</p>
</div>
</>
)}
{sidebarCollapsed && (
<Avatar className="mx-auto"> {/* Centraliza o avatar quando recolhido */}
<AvatarImage src="/placeholder.svg?height=40&width=40" />
<AvatarFallback>
{doctorData.name
.split(" ")
.map((n) => n[0])
.join("")}
</AvatarFallback>
</Avatar>
)}
</div>
)}
</div>
{/* Novo botão de sair, usando a mesma estrutura dos itens de menu */}
<div
className={`flex items-center gap-3 px-3 py-2 rounded-lg mb-1 transition-colors text-muted-foreground hover:bg-accent cursor-pointer ${sidebarCollapsed ? "justify-center" : ""}`}
onClick={handleLogout}
>
<LogOut className="w-5 h-5 flex-shrink-0" />
{!sidebarCollapsed && <span className="font-medium">Sair</span>}
<div className={`flex items-center gap-3 px-3 py-2 rounded-lg mb-1 transition-colors text-muted-foreground hover:bg-accent cursor-pointer ${sidebarCollapsed ? "justify-center" : ""}`} onClick={handleLogout}>
<LogOut className="w-5 h-5 flex-shrink-0" />
{!sidebarCollapsed && <span className="font-medium">Sair</span>}
</div>
</div>
</div>
</div>
</div>
{isMobileMenuOpen && (
<div className="fixed inset-0 bg-black bg-opacity-50 z-40 md:hidden" onClick={toggleMobileMenu}></div>
)}
{isMobileMenuOpen && <div className="fixed inset-0 bg-black bg-opacity-50 z-40 md:hidden" onClick={toggleMobileMenu}></div>}
<div className={`bg-white border-r border-gray-200 fixed left-0 top-0 h-screen flex flex-col z-50 transition-transform duration-300 md:hidden ${isMobileMenuOpen ? "translate-x-0 w-64" : "-translate-x-full w-64"}`}>
<div className="p-4 border-b border-gray-200 flex items-center justify-between">
<div className="flex items-center gap-2">
@ -271,7 +277,7 @@ useEffect(() => {
const isActive = pathname === item.href || (item.href !== "/" && pathname.startsWith(item.href));
return (
<Link key={item.href} href={item.href} onClick={toggleMobileMenu}> {/* Fechar menu ao clicar */}
<Link key={item.href} href={item.href} onClick={toggleMobileMenu}>
<div className={`flex items-center gap-3 px-3 py-2 rounded-lg mb-1 transition-colors ${isActive ? "bg-accent text-accent-foreground border-r-2 border-primary" : "text-muted-foreground hover:bg-accent"}`}>
<Icon className="w-5 h-5 flex-shrink-0" />
<span className="font-medium">{item.label}</span>
@ -297,17 +303,22 @@ useEffect(() => {
<p className="text-xs text-gray-500 truncate">{doctorData.specialty}</p>
</div>
</div>
<Button variant="outline" size="sm" className="w-full bg-transparent" onClick={() => { handleLogout(); toggleMobileMenu(); }}> {/* Fechar menu ao deslogar */}
<Button
variant="outline"
size="sm"
className="w-full bg-transparent"
onClick={() => {
handleLogout();
toggleMobileMenu();
}}
>
<LogOut className="mr-2 h-4 w-4" />
Sair
</Button>
</div>
</div>
{/* Main Content */}
<div className={`flex-1 flex flex-col transition-all duration-300 ${sidebarCollapsed ? "ml-16" : "ml-64"}`}>
{/* Header */}
<div className={`flex-1 flex flex-col transition-all duration-300 ${sidebarCollapsed ? "ml-16" : "ml-64"}`}>
<header className="bg-card border-b border px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4 flex-1">
@ -326,11 +337,9 @@ useEffect(() => {
</div>
</header>
{/* Page Content */}
<main className="flex-1 p-6">{children}</main>
</div>
{/* Logout confirmation dialog */}
<Dialog open={showLogoutDialog} onOpenChange={setShowLogoutDialog}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
@ -350,4 +359,4 @@ useEffect(() => {
</Dialog>
</div>
);
}
}

View File

@ -1,3 +1,4 @@
// Caminho: [seu-caminho]/FinancierLayout.tsx
"use client";
import Cookies from "js-cookie";
@ -5,32 +6,14 @@ import type React from "react";
import { useState, useEffect } from "react";
import { useRouter, usePathname } from "next/navigation";
import Link from "next/link";
import { api } from '@/services/api.mjs';
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
Search,
Bell,
Calendar,
Clock,
User,
LogOut,
Menu,
X,
Home,
FileText,
ChevronLeft,
ChevronRight,
} from "lucide-react";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Search, Bell, Calendar, Clock, User, LogOut, Menu, X, Home, FileText, ChevronLeft, ChevronRight } from "lucide-react";
interface FinancierData {
id: string;
@ -47,37 +30,45 @@ interface PatientLayoutProps {
}
export default function FinancierLayout({ children }: PatientLayoutProps) {
const [financierData, setFinancierData] = useState<FinancierData | null>(
null
);
const [financierData, setFinancierData] = useState<FinancierData | null>(null);
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const [showLogoutDialog, setShowLogoutDialog] = useState(false);
const router = useRouter();
const pathname = usePathname();
useEffect(() => {
const data = localStorage.getItem("financierData");
if (data) {
setFinancierData(JSON.parse(data));
const userInfoString = localStorage.getItem("user_info");
// --- ALTERAÇÃO 1: Buscando o token no localStorage ---
const token = localStorage.getItem("token");
if (userInfoString && token) {
const userInfo = JSON.parse(userInfoString);
setFinancierData({
id: userInfo.id || "",
name: userInfo.user_metadata?.full_name || "Financeiro",
email: userInfo.email || "",
department: userInfo.user_metadata?.department || "Departamento Financeiro",
phone: userInfo.phone || "",
cpf: "",
permissions: {},
});
} else {
router.push("/finance/login");
// --- ALTERAÇÃO 2: Redirecionando para o login central ---
router.push("/login");
}
}, [router]);
// 🔥 Responsividade automática da sidebar
useEffect(() => {
const handleResize = () => {
// Ajuste o breakpoint conforme necessário. 1024px (lg) ou 768px (md) são comuns.
if (window.innerWidth < 1024) {
setSidebarCollapsed(true);
} else {
setSidebarCollapsed(false);
}
};
handleResize(); // executa na primeira carga
handleResize();
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
@ -85,10 +76,22 @@ export default function FinancierLayout({ children }: PatientLayoutProps) {
setShowLogoutDialog(true);
};
const confirmLogout = () => {
localStorage.removeItem("financierData");
setShowLogoutDialog(false);
router.push("/");
// --- ALTERAÇÃO 2: A função de logout agora é MUITO mais simples ---
const confirmLogout = async () => {
try {
// Chama a função centralizada para fazer o logout no servidor
await api.logout();
} catch (error) {
// O erro já é logado dentro da função api.logout, não precisamos fazer nada aqui
} finally {
// A responsabilidade do componente é apenas limpar o estado local e redirecionar
localStorage.removeItem("user_info");
localStorage.removeItem("token");
Cookies.remove("access_token"); // Limpeza de segurança
setShowLogoutDialog(false);
router.push("/"); // Redireciona para a home
}
};
const cancelLogout = () => {
@ -96,35 +99,19 @@ export default function FinancierLayout({ children }: PatientLayoutProps) {
};
const menuItems = [
{
href: "#",
icon: Home,
label: "Dashboard",
},
{
href: "#",
icon: Calendar,
label: "Relatórios financeiros",
},
{
href: "#",
icon: User,
label: "Finanças Gerais",
},
{
href: "#",
icon: Calendar,
label: "Configurações",
},
{ href: "#", icon: Home, label: "Dashboard" },
{ href: "#", icon: Calendar, label: "Relatórios financeiros" },
{ href: "#", icon: User, label: "Finanças Gerais" },
{ href: "#", icon: Calendar, label: "Configurações" },
];
if (!financierData) {
return <div>Carregando...</div>;
return <div className="flex h-screen w-full items-center justify-center">Carregando...</div>;
}
return (
// O restante do seu código JSX permanece inalterado
<div className="min-h-screen bg-background flex">
{/* Sidebar */}
<div
className={`bg-card border-r border-border transition-all duration-300 ${
sidebarCollapsed ? "w-16" : "w-64"
@ -183,7 +170,6 @@ export default function FinancierLayout({ children }: PatientLayoutProps) {
})}
</nav>
{/* Footer user info */}
<div className="border-t p-4 mt-auto">
<div className="flex items-center space-x-3 mb-4">
<Avatar>
@ -206,34 +192,29 @@ export default function FinancierLayout({ children }: PatientLayoutProps) {
</div>
)}
</div>
{/* Botão Sair - ajustado para responsividade */}
<Button
variant="outline"
size="sm"
className={
sidebarCollapsed
? "w-full bg-transparent flex justify-center items-center p-2" // Centraliza o ícone quando colapsado
? "w-full bg-transparent flex justify-center items-center p-2"
: "w-full bg-transparent"
}
onClick={handleLogout}
>
<LogOut
className={sidebarCollapsed ? "h-5 w-5" : "mr-2 h-4 w-4"}
/>{" "}
{/* Remove margem quando colapsado */}
{!sidebarCollapsed && "Sair"}{" "}
{/* Mostra o texto apenas quando não está colapsado */}
/>
{!sidebarCollapsed && "Sair"}
</Button>
</div>
</div>
{/* Main Content */}
<div
className={`flex-1 flex flex-col transition-all duration-300 ${
sidebarCollapsed ? "ml-16" : "ml-64"
}`}
>
{/* Header */}
<header className="bg-card border-b border-border px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4 flex-1 max-w-md">
@ -257,11 +238,9 @@ export default function FinancierLayout({ children }: PatientLayoutProps) {
</div>
</header>
{/* Page Content */}
<main className="flex-1 p-6">{children}</main>
</div>
{/* Logout confirmation dialog */}
<Dialog open={showLogoutDialog} onOpenChange={setShowLogoutDialog}>
<DialogContent className="sm:max-w-md">
<DialogHeader>

View File

@ -1,33 +1,19 @@
// Caminho: [seu-caminho]/ManagerLayout.tsx
"use client";
import type React from "react";
import { useState, useEffect } from "react";
import { useRouter, usePathname } from "next/navigation";
import Link from "next/link";
import Cookies from "js-cookie"; // <-- 1. IMPORTAÇÃO ADICIONADA
import Cookies from "js-cookie"; // Mantido apenas para a limpeza de segurança no logout
import { api } from '@/services/api.mjs';
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
Search,
Bell,
Calendar,
User,
LogOut,
ChevronLeft,
ChevronRight,
Home,
} from "lucide-react";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Search, Bell, Calendar, User, LogOut, ChevronLeft, ChevronRight, Home } from "lucide-react";
interface ManagerData {
id: string;
@ -39,7 +25,7 @@ interface ManagerData {
permissions: object;
}
interface ManagerLayoutProps { // Corrigi o nome da prop aqui
interface ManagerLayoutProps {
children: React.ReactNode;
}
@ -50,80 +36,81 @@ export default function ManagerLayout({ children }: ManagerLayoutProps) {
const router = useRouter();
const pathname = usePathname();
// ==================================================================
// 2. BLOCO DE SEGURANÇA CORRIGIDO
// ==================================================================
useEffect(() => {
const userInfoString = localStorage.getItem("user_info");
const token = Cookies.get("access_token");
// --- ALTERAÇÃO 1: Buscando o token no localStorage ---
const token = localStorage.getItem("token");
if (userInfoString && token) {
const userInfo = JSON.parse(userInfoString);
// 3. "TRADUZIMOS" os dados da API para o formato que o layout espera
setManagerData({
id: userInfo.id || "",
name: userInfo.user_metadata?.full_name || "Gestor(a)",
email: userInfo.email || "",
department: userInfo.user_metadata?.role || "Gestão",
// Campos que não vêm do login, definidos como vazios para não quebrar
phone: userInfo.phone || "",
cpf: "",
permissions: {},
});
} else {
// Se faltar o token ou os dados, volta para o login
router.push("/manager/login");
// O redirecionamento para /login já estava correto. Ótimo!
router.push("/login");
}
}, [router]);
// 🔥 Responsividade automática da sidebar
useEffect(() => {
const handleResize = () => {
if (window.innerWidth < 1024) {
setSidebarCollapsed(true); // colapsa em telas pequenas (lg breakpoint ~ 1024px)
setSidebarCollapsed(true);
} else {
setSidebarCollapsed(false); // expande em desktop
setSidebarCollapsed(false);
}
};
handleResize(); // roda na primeira carga
handleResize();
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
const handleLogout = () => setShowLogoutDialog(true);
const confirmLogout = () => {
localStorage.removeItem("managerData");
setShowLogoutDialog(false);
router.push("/");
// --- ALTERAÇÃO 2: A função de logout agora é MUITO mais simples ---
const confirmLogout = async () => {
try {
// Chama a função centralizada para fazer o logout no servidor
await api.logout();
} catch (error) {
// O erro já é logado dentro da função api.logout, não precisamos fazer nada aqui
} finally {
// A responsabilidade do componente é apenas limpar o estado local e redirecionar
localStorage.removeItem("user_info");
localStorage.removeItem("token");
Cookies.remove("access_token"); // Limpeza de segurança
setShowLogoutDialog(false);
router.push("/"); // Redireciona para a home
}
};
const cancelLogout = () => setShowLogoutDialog(false);
const menuItems = [
{ href: "/manager/dashboard/", icon: Home, label: "Dashboard" },
{ href: "#", icon: Calendar, label: "Relatórios gerenciais" },
{ href: "/manager/usuario/", icon: User, label: "Gestão de Usuários" },
{ href: "/manager/home", icon: User, label: "Gestão de Médicos" },
{ href: "#", icon: Calendar, label: "Configurações" },
{ href: "#dashboard", icon: Home, label: "Dashboard" },
{ href: "#reports", icon: Calendar, label: "Relatórios gerenciais" },
{ href: "#users", icon: User, label: "Gestão de Usuários" },
{ href: "#doctors", icon: User, label: "Gestão de Médicos" },
{ href: "#settings", icon: Calendar, label: "Configurações" },
];
if (!managerData) {
return <div>Carregando...</div>;
return <div className="flex h-screen w-full items-center justify-center">Carregando...</div>;
}
return (
<div className="min-h-screen bg-gray-50 flex">
{/* Sidebar */}
<div
className={`bg-white border-r border-gray-200 transition-all duration-300 fixed top-0 h-screen flex flex-col z-30
${sidebarCollapsed ? "w-16" : "w-64"}`}
className={`bg-white border-r border-gray-200 transition-all duration-300 fixed top-0 h-screen flex flex-col z-30 ${sidebarCollapsed ? "w-16" : "w-64"}`}
>
{/* Logo + collapse button */}
<div className="p-4 border-b border-gray-200 flex items-center justify-between">
{!sidebarCollapsed && (
<div className="flex items-center gap-2">
@ -141,136 +128,79 @@ export default function ManagerLayout({ children }: ManagerLayoutProps) {
onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
className="p-1"
>
{sidebarCollapsed ? (
<ChevronRight className="w-4 h-4" />
) : (
<ChevronLeft className="w-4 h-4" />
)}
{sidebarCollapsed ? <ChevronRight className="w-4 h-4" /> : <ChevronLeft className="w-4 h-4" />}
</Button>
</div>
{/* Menu Items */}
<nav className="flex-1 p-2 overflow-y-auto">
{menuItems.map((item) => {
const Icon = item.icon;
const isActive =
pathname === item.href ||
(item.href !== "/" && pathname.startsWith(item.href));
const isActive = pathname === item.href;
return (
<Link key={item.href} href={item.href}>
<Link key={item.label} href={item.href}>
<div
className={`flex items-center gap-3 px-3 py-2 rounded-lg mb-1 transition-colors ${
isActive
? "bg-blue-50 text-blue-600 border-r-2 border-blue-600"
: "text-gray-600 hover:bg-gray-50"
}`}
className={`flex items-center gap-3 px-3 py-2 rounded-lg mb-1 transition-colors ${isActive ? "bg-blue-50 text-blue-600 border-r-2 border-blue-600" : "text-gray-600 hover:bg-gray-50"}`}
>
<Icon className="w-5 h-5 flex-shrink-0" />
{!sidebarCollapsed && (
<span className="font-medium">{item.label}</span>
)}
{!sidebarCollapsed && <span className="font-medium">{item.label}</span>}
</div>
</Link>
);
})}
</nav>
{/* Perfil no rodapé */}
<div className="border-t p-4 mt-auto">
<div className="flex items-center space-x-3 mb-4">
<Avatar>
<AvatarImage src="/placeholder.svg?height=40&width=40" />
<AvatarFallback>
{managerData.name
.split(" ")
.map((n) => n[0])
.join("")}
</AvatarFallback>
<AvatarFallback>{managerData.name.split(" ").map((n) => n[0]).join("")}</AvatarFallback>
</Avatar>
{!sidebarCollapsed && (
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-900 truncate">
{managerData.name}
</p>
<p className="text-xs text-gray-500 truncate">
{managerData.department}
</p>
<p className="text-sm font-medium text-gray-900 truncate">{managerData.name}</p>
<p className="text-xs text-gray-500 truncate">{managerData.department}</p>
</div>
)}
</div>
{/* Botão Sair - ajustado para responsividade */}
<Button
variant="outline"
size="sm"
className={
sidebarCollapsed
? "w-full bg-transparent flex justify-center items-center p-2" // Centraliza o ícone quando colapsado
: "w-full bg-transparent"
}
className={sidebarCollapsed ? "w-full bg-transparent flex justify-center items-center p-2" : "w-full bg-transparent"}
onClick={handleLogout}
>
<LogOut
className={sidebarCollapsed ? "h-5 w-5" : "mr-2 h-4 w-4"}
/>{" "}
{/* Remove margem quando colapsado */}
{!sidebarCollapsed && "Sair"}{" "}
{/* Mostra o texto apenas quando não está colapsado */}
<LogOut className={sidebarCollapsed ? "h-5 w-5" : "mr-2 h-4 w-4"} />
{!sidebarCollapsed && "Sair"}
</Button>
</div>
</div>
{/* Conteúdo principal */}
<div
className={`flex-1 flex flex-col transition-all duration-300 w-full
${sidebarCollapsed ? "ml-16" : "ml-64"}`}
>
{/* Header */}
<div className={`flex-1 flex flex-col transition-all duration-300 w-full ${sidebarCollapsed ? "ml-16" : "ml-64"}`}>
<header className="bg-white border-b border-gray-200 px-4 md:px-6 py-4 flex items-center justify-between">
{/* Search */}
<div className="flex items-center gap-4 flex-1 max-w-md">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<Input
placeholder="Buscar paciente"
className="pl-10 bg-gray-50 border-gray-200"
/>
<Input placeholder="Buscar paciente" className="pl-10 bg-gray-50 border-gray-200" />
</div>
</div>
{/* Notifications */}
<div className="flex items-center gap-4 ml-auto">
<Button variant="ghost" size="sm" className="relative">
<Bell className="w-5 h-5" />
<Badge className="absolute -top-1 -right-1 w-5 h-5 p-0 flex items-center justify-center bg-red-500 text-white text-xs">
1
</Badge>
<Badge className="absolute -top-1 -right-1 w-5 h-5 p-0 flex items-center justify-center bg-red-500 text-white text-xs">1</Badge>
</Button>
</div>
</header>
{/* Page Content */}
<main className="flex-1 p-4 md:p-6">{children}</main>
</div>
{/* Logout confirmation dialog */}
<Dialog open={showLogoutDialog} onOpenChange={setShowLogoutDialog}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Confirmar Saída</DialogTitle>
<DialogDescription>
Deseja realmente sair do sistema? Você precisará fazer login
novamente para acessar sua conta.
</DialogDescription>
<DialogDescription>Deseja realmente sair do sistema? Você precisará fazer login novamente para acessar sua conta.</DialogDescription>
</DialogHeader>
<DialogFooter className="flex gap-2">
<Button variant="outline" onClick={cancelLogout}>
Cancelar
</Button>
<Button variant="destructive" onClick={confirmLogout}>
<LogOut className="mr-2 h-4 w-4" />
Sair
</Button>
<Button variant="outline" onClick={cancelLogout}>Cancelar</Button>
<Button variant="destructive" onClick={confirmLogout}><LogOut className="mr-2 h-4 w-4" />Sair</Button>
</DialogFooter>
</DialogContent>
</Dialog>

View File

@ -1,36 +1,18 @@
"use client"
import Cookies from "js-cookie";
import type React from "react"
import { useState, useEffect } from "react"
import Link from "next/link"
import { useRouter, usePathname } from "next/navigation"
import { api } from "@/services/api.mjs"; // Importando nosso cliente de API
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Badge } from "@/components/ui/badge"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import {
Search,
Bell,
User,
LogOut,
FileText,
Clock,
Calendar,
Home,
ChevronLeft,
ChevronRight,
} from "lucide-react"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Search, Bell, User, LogOut, FileText, Clock, Calendar, Home, ChevronLeft, ChevronRight } from "lucide-react"
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"
interface PatientData {
name: string
@ -41,65 +23,72 @@ interface PatientData {
address: string
}
interface HospitalLayoutProps {
interface PatientLayoutProps {
children: React.ReactNode
}
export default function HospitalLayout({ children }: HospitalLayoutProps) {
// --- ALTERAÇÃO 1: Renomeando o componente para maior clareza ---
export default function PatientLayout({ children }: PatientLayoutProps) {
const [patientData, setPatientData] = useState<PatientData | null>(null)
const [sidebarCollapsed, setSidebarCollapsed] = useState(false)
const [showLogoutDialog, setShowLogoutDialog] = useState(false)
const router = useRouter()
const pathname = usePathname()
// 🔹 Ajuste automático no resize
useEffect(() => {
const handleResize = () => {
if (window.innerWidth < 1024) {
setSidebarCollapsed(true) // colapsa no mobile
setSidebarCollapsed(true)
} else {
setSidebarCollapsed(false) // expande no desktop
setSidebarCollapsed(false)
}
}
handleResize()
window.addEventListener("resize", handleResize)
return () => window.removeEventListener("resize", handleResize)
}, [])
useEffect(() => {
// 1. Procuramos pela chave correta: 'user_info'
const userInfoString = localStorage.getItem("user_info");
// 2. Para mais segurança, verificamos também se o token de acesso existe no cookie
const token = Cookies.get("access_token");
// --- ALTERAÇÃO 2: Buscando o token no localStorage ---
const token = localStorage.getItem("token");
if (userInfoString && token) {
const userInfo = JSON.parse(userInfoString);
// 3. Adaptamos os dados para a estrutura que seu layout espera (PatientData)
// Usamos os dados do objeto 'user' que a API do Supabase nos deu
setPatientData({
name: userInfo.user_metadata?.full_name || "Paciente",
email: userInfo.email || "",
// Os campos abaixo não vêm do login, então os deixamos vazios por enquanto
phone: userInfo.phone || "",
cpf: "",
birthDate: "",
address: "",
});
} else {
// Se as informações do usuário ou o token não forem encontrados, mandamos para o login.
router.push("/patient/login");
// --- ALTERAÇÃO 3: Redirecionando para o login central ---
router.push("/login");
}
}, [router]);
const handleLogout = () => setShowLogoutDialog(true)
const confirmLogout = () => {
localStorage.removeItem("patientData")
setShowLogoutDialog(false)
router.push("/")
}
// --- ALTERAÇÃO 4: Função de logout completa e padronizada ---
const confirmLogout = async () => {
try {
// Chama a função centralizada para fazer o logout no servidor
await api.logout();
} catch (error) {
console.error("Erro ao tentar fazer logout no servidor:", error);
} finally {
// Limpeza completa e consistente do estado local
localStorage.removeItem("user_info");
localStorage.removeItem("token");
Cookies.remove("access_token"); // Limpeza de segurança
setShowLogoutDialog(false);
router.push("/"); // Redireciona para a página inicial
}
};
const cancelLogout = () => setShowLogoutDialog(false)
@ -112,7 +101,7 @@ export default function HospitalLayout({ children }: HospitalLayoutProps) {
]
if (!patientData) {
return <div>Carregando...</div>
return <div className="flex h-screen w-full items-center justify-center">Carregando...</div>;
}
return (

View File

@ -1,3 +1,4 @@
// Caminho: app/(secretary)/layout.tsx (ou o caminho do seu arquivo)
"use client"
import type React from "react"
@ -5,30 +6,14 @@ import { useState, useEffect } from "react"
import { useRouter, usePathname } from "next/navigation"
import Link from "next/link"
import Cookies from "js-cookie";
import { api } from '@/services/api.mjs'; // Importando nosso cliente de API central
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Badge } from "@/components/ui/badge"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import {
Search,
Bell,
Calendar,
Clock,
User,
LogOut,
Home,
ChevronLeft,
ChevronRight,
} from "lucide-react"
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Search, Bell, Calendar, Clock, User, LogOut, Home, ChevronLeft, ChevronRight } from "lucide-react"
interface SecretaryData {
id: string
@ -46,12 +31,36 @@ interface SecretaryLayoutProps {
}
export default function SecretaryLayout({ children }: SecretaryLayoutProps) {
const [secretaryData, setSecretaryData] = useState<SecretaryData | null>(null);
const [sidebarCollapsed, setSidebarCollapsed] = useState(false)
const [showLogoutDialog, setShowLogoutDialog] = useState(false)
const router = useRouter()
const pathname = usePathname()
// 🔹 Colapsar no mobile e expandir no desktop automaticamente
useEffect(() => {
const userInfoString = localStorage.getItem("user_info");
// --- ALTERAÇÃO 1: Buscando o token no localStorage ---
const token = localStorage.getItem("token");
if (userInfoString && token) {
const userInfo = JSON.parse(userInfoString);
setSecretaryData({
id: userInfo.id || "",
name: userInfo.user_metadata?.full_name || "Secretária",
email: userInfo.email || "",
department: userInfo.user_metadata?.department || "Atendimento",
phone: userInfo.phone || "",
cpf: "",
employeeId: "",
permissions: {},
});
} else {
// --- ALTERAÇÃO 2: Redirecionando para o login central ---
router.push("/login");
}
}, [router]);
useEffect(() => {
const handleResize = () => {
if (window.innerWidth < 1024) {
@ -66,10 +75,25 @@ export default function SecretaryLayout({ children }: SecretaryLayoutProps) {
}, [])
const handleLogout = () => setShowLogoutDialog(true)
const confirmLogout = () => {
setShowLogoutDialog(false)
router.push("/")
}
// --- ALTERAÇÃO 3: Função de logout completa e padronizada ---
const confirmLogout = async () => {
try {
// Chama a função centralizada para fazer o logout no servidor
await api.logout();
} catch (error) {
console.error("Erro ao tentar fazer logout no servidor:", error);
} finally {
// Limpeza completa e consistente do estado local
localStorage.removeItem("user_info");
localStorage.removeItem("token");
Cookies.remove("access_token"); // Limpeza de segurança
setShowLogoutDialog(false);
router.push("/"); // Redireciona para a página inicial
}
};
const cancelLogout = () => setShowLogoutDialog(false)
const menuItems = [
@ -79,17 +103,11 @@ export default function SecretaryLayout({ children }: SecretaryLayoutProps) {
{ href: "/secretary/pacientes", icon: User, label: "Pacientes" },
]
const secretaryData: SecretaryData = {
id: "1",
name: "Secretária Exemplo",
email: "secretaria@hospital.com",
phone: "999999999",
cpf: "000.000.000-00",
employeeId: "12345",
department: "Atendimento",
permissions: {},
if (!secretaryData) {
return <div className="flex h-screen w-full items-center justify-center">Carregando...</div>;
}
return (
<div className="min-h-screen bg-background flex">
{/* Sidebar */}
@ -165,23 +183,20 @@ export default function SecretaryLayout({ children }: SecretaryLayoutProps) {
</div>
)}
</div>
{/* Botão Sair - ajustado para responsividade */}
<Button
variant="outline"
size="sm"
className={
sidebarCollapsed
? "w-full bg-transparent flex justify-center items-center p-2" // Centraliza o ícone quando colapsado
? "w-full bg-transparent flex justify-center items-center p-2"
: "w-full bg-transparent"
}
onClick={handleLogout}
>
<LogOut
className={sidebarCollapsed ? "h-5 w-5" : "mr-2 h-4 w-4"}
/>{" "}
{/* Remove margem quando colapsado */}
{!sidebarCollapsed && "Sair"}{" "}
{/* Mostra o texto apenas quando não está colapsado */}
/>
{!sidebarCollapsed && "Sair"}
</Button>
</div>
</div>
@ -191,7 +206,6 @@ export default function SecretaryLayout({ children }: SecretaryLayoutProps) {
className={`flex-1 flex flex-col transition-all duration-300 ${sidebarCollapsed ? "ml-16" : "ml-64"
}`}
>
{/* Header */}
<header className="bg-card border-b border-border px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4 flex-1 max-w-md">
@ -205,13 +219,6 @@ export default function SecretaryLayout({ children }: SecretaryLayoutProps) {
</div>
<div className="flex items-center gap-4">
{/* Este botão no header parece ter sido uma cópia do botão "Sair" da sidebar.
Removi a lógica de sidebarCollapsed aqui, pois o header é independente.
Se a intenção era ter um botão de logout no header, ele não deve ser afetado pela sidebar.
Ajustei para ser um botão de sino de notificação, como nos exemplos anteriores,
que você tem o ícone Bell importado e uma badge para notificação.
Se você quer um botão de LogOut aqui, por favor, me avise!
*/}
<Button variant="ghost" size="sm" className="relative">
<Bell className="w-5 h-5" />
<Badge className="absolute -top-1 -right-1 w-5 h-5 p-0 flex items-center justify-center bg-destructive text-destructive-foreground text-xs">
@ -222,7 +229,6 @@ export default function SecretaryLayout({ children }: SecretaryLayoutProps) {
</div>
</header>
{/* Page Content */}
<main className="flex-1 p-6">{children}</main>
</div>

View File

@ -1,12 +1,69 @@
// Caminho: [seu-caminho]/services/api.mjs
const BASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const API_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
export async function loginWithEmailAndPassword(email, password) {
const response = await fetch(`${BASE_URL}/auth/v1/token?grant_type=password`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"apikey": API_KEY,
},
body: JSON.stringify({ email, password }),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error_description || "Credenciais inválidas.");
}
if (data.access_token && typeof window !== 'undefined') {
// Padronizando para salvar o token no localStorage
localStorage.setItem("token", data.access_token);
}
return data;
}
// --- NOVA FUNÇÃO DE LOGOUT CENTRALIZADA ---
async function logout() {
const token = localStorage.getItem("token");
if (!token) return; // Se não há token, não há o que fazer
try {
await fetch(`${BASE_URL}/auth/v1/logout`, {
method: "POST",
headers: {
"apikey": API_KEY,
"Authorization": `Bearer ${token}`,
},
});
} catch (error) {
// Mesmo que a chamada falhe, o logout no cliente deve continuar.
// O token pode já ter expirado no servidor, por exemplo.
console.error("Falha ao invalidar token no servidor (isso pode ser normal se o token já expirou):", error);
}
}
async function request(endpoint, options = {}) {
const token = typeof window !== 'undefined' ? localStorage.getItem("token") : null;
const headers = {
"Content-Type": "application/json",
"apikey": API_KEY,
...(token ? { "Authorization": `Bearer ${token}` } : {}),
...options.headers,
};
const API_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
export const apikey = API_KEY;
let loginPromise = null;
// 🔹 Autenticação
export async function login() {
console.log("🔐 Iniciando login...");
const res = await fetch(`${BASE_URL}/auth/v1/token?grant_type=password`, {
method: "POST",
headers: {
@ -20,10 +77,19 @@ export async function login() {
}),
});
if (!res.ok) {
const msg = await res.text();
console.error("❌ Erro no login:", res.status, msg);
throw new Error(`Erro ao autenticar: ${res.status} - ${msg}`);
}
const data = await res.json();
if (typeof window !== "undefined") {
console.log("✅ Login bem-sucedido:", data);
if (typeof window !== "undefined" && data.access_token) {
localStorage.setItem("token", data.access_token);
}
return data;
}
@ -33,28 +99,69 @@ async function request(endpoint, options = {}) {
try {
await loginPromise;
} catch (error) {
console.error("Falha ao autenticar:", error);
console.error("⚠️ Falha ao autenticar:", error);
} finally {
loginPromise = null;
}
const token =
let token =
typeof window !== "undefined" ? localStorage.getItem("token") : null;
if (!token) {
console.warn("⚠️ Token não encontrado, refazendo login...");
const data = await login();
token = data.access_token;
}
const headers = {
"Content-Type": "application/json",
apikey: API_KEY,
...(token ? { Authorization: `Bearer ${token}` } : {}),
Authorization: `Bearer ${token}`,
...options.headers,
};
const response = await fetch(`${BASE_URL}${endpoint}`, {
const fullUrl =
endpoint.startsWith("/rest/v1") || endpoint.startsWith("/functions/")
? `${BASE_URL}${endpoint}`
: `${BASE_URL}/rest/v1${endpoint}`;
console.log("🌐 Requisição para:", fullUrl, "com headers:", headers);
const response = await fetch(fullUrl, {
...options,
headers,
});
if (!response.ok) {
let errorBody;
try {
errorBody = await response.json();
} catch (e) {
errorBody = await response.text();
}
throw new Error(`Erro HTTP: ${response.status} - ${JSON.stringify(errorBody)}`);
}
if (response.status === 204) return {};
return await response.json();
} catch (error) {
console.error("Erro na requisição:", error);
throw error;
}
}
// Adicionamos a função de logout ao nosso objeto de API exportado
export const api = {
get: (endpoint, options) => request(endpoint, { method: "GET", ...options }),
post: (endpoint, data, options) => request(endpoint, { method: "POST", body: JSON.stringify(data), ...options }),
patch: (endpoint, data, options) => request(endpoint, { method: "PATCH", body: JSON.stringify(data), ...options }),
delete: (endpoint, options) => request(endpoint, { method: "DELETE", ...options }),
logout: logout, // <-- EXPORTANDO A NOVA FUNÇÃO
};
if (!response.ok) {
const msg = await response.text();
console.error("❌ Erro HTTP:", response.status, msg);
throw new Error(`Erro HTTP: ${response.status} - Detalhes: ${msg}`);
}
@ -71,3 +178,4 @@ export const api = {
request(endpoint, { method: "PATCH", body: JSON.stringify(data) }),
delete: (endpoint) => request(endpoint, { method: "DELETE" }),
};

View File

@ -0,0 +1,9 @@
import { api } from "./api.mjs";
export const AvailabilityService = {
list: () => api.get("/rest/v1/doctor_availability"),
listById: (id) => api.get(`/rest/v1/doctor_availability?doctor_id=eq.${id}`),
create: (data) => api.post("/rest/v1/doctor_availability", data),
update: (id, data) => api.patch(`/rest/v1/doctor_availability?id=eq.${id}`, data),
delete: (id) => api.delete(`/rest/v1/doctor_availability?id=eq.${id}`),
};

View File

@ -0,0 +1,8 @@
import { api } from "./api.mjs";
export const exceptionsService = {
list: () => api.get("/rest/v1/doctor_exceptions"),
listById: () => api.get(`/rest/v1/doctor_exceptions?id=eq.${id}`),
create: (data) => api.post("/rest/v1/doctor_exceptions", data),
delete: (id) => api.delete(`/rest/v1/doctor_exceptions?id=eq.${id}`),
};

View File

@ -1,9 +1,9 @@
import { api } from "./api.mjs";
export const patientsService = {
list: () => api.get("/rest/v1/patients"),
getById: (id) => api.get(`/rest/v1/patients?id=eq.${id}`),
create: (data) => api.post("/rest/v1/patients", data),
update: (id, data) => api.patch(`/rest/v1/patients?id=eq.${id}`, data),
delete: (id) => api.delete(`/rest/v1/patients?id=eq.${id}`),
list: () => api.get("/rest/v1/patients"),
getById: (id) => api.get(`/rest/v1/patients?id=eq.${id}`),
create: (data) => api.post("/rest/v1/patients", data),
update: (id, data) => api.patch(`/rest/v1/patients?id=eq.${id}`, data),
delete: (id) => api.delete(`/rest/v1/patients?id=eq.${id}`),
};

View File

@ -2,14 +2,16 @@ import { api } from "./api.mjs";
export const usersService = {
async list_roles() {
// continua usando /rest/v1 normalmente
return await api.get(`/rest/v1/user_roles?select=id,user_id,role,created_at`);
},
async create_user(data) {
// continua usando a Edge Function corretamente
return await api.post(`/functions/v1/user-create`, data);
},
// 🚀 Busca dados completos do usuário direto do banco (sem função bloqueada)
// 🚀 Busca dados completos do usuário direto do banco
async full_data(user_id) {
if (!user_id) throw new Error("user_id é obrigatório");