Merge branch 'Stage' of https://github.com/m1guelmcf/MedConnect into ajustes-visuais-paginas

This commit is contained in:
StsDanilo 2025-11-26 18:44:50 -03:00
commit 00e8b4310e
6 changed files with 571 additions and 391 deletions

View File

@ -31,7 +31,7 @@ interface EnrichedAppointment {
} }
export default function DoctorAppointmentsPage() { export default function DoctorAppointmentsPage() {
const { user, isLoading: isAuthLoading } = useAuthLayout({ requiredRole: 'medico' }); const { user, isLoading: isAuthLoading } = useAuthLayout({ requiredRole: "medico" });
const [allAppointments, setAllAppointments] = useState<EnrichedAppointment[]>([]); const [allAppointments, setAllAppointments] = useState<EnrichedAppointment[]>([]);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
@ -111,13 +111,22 @@ export default function DoctorAppointmentsPage() {
return format(date, "EEEE, dd 'de' MMMM", { locale: ptBR }); return format(date, "EEEE, dd 'de' MMMM", { locale: ptBR });
}; };
const statusPT: Record<string, string> = {
confirmed: "Confirmada",
completed: "Concluída",
cancelled: "Cancelada",
requested: "Solicitada",
no_show: "oculta",
checked_in: "Aguardando",
};
const getStatusVariant = (status: EnrichedAppointment['status']) => { const getStatusVariant = (status: EnrichedAppointment['status']) => {
switch (status) { switch (status) {
case "confirmed": case "checked_in": return "default"; case "confirmed": case "checked_in": return "text-foreground bg-blue-100 hover:bg-blue-150";
case "completed": return "secondary"; case "completed": return "text-foreground bg-green-100 hover:bg-green-150";
case "cancelled": case "no_show": return "destructive"; case "cancelled": case "no_show": return "text-foreground bg-red-200 hover:bg-red-250";
case "requested": return "outline"; case "requested": return "text-foreground bg-yellow-100 hover:bg-yellow-150";
default: return "outline"; default: return "border-gray bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90";
} }
}; };
@ -191,7 +200,7 @@ export default function DoctorAppointmentsPage() {
{/* Coluna 2: Status e Telefone */} {/* Coluna 2: Status e Telefone */}
<div className="col-span-1 flex flex-col items-center gap-2"> <div className="col-span-1 flex flex-col items-center gap-2">
<Badge variant={getStatusVariant(appointment.status)} className="capitalize text-xs">{appointment.status.replace('_', ' ')}</Badge> <Badge variant="outline" className={getStatusVariant(appointment.status)}>{statusPT[appointment.status].replace('_', ' ')}</Badge>
<div className="flex items-center text-sm text-muted-foreground"> <div className="flex items-center text-sm text-muted-foreground">
<Phone className="mr-2 h-4 w-4" /> <Phone className="mr-2 h-4 w-4" />
{appointment.patientPhone} {appointment.patientPhone}

View File

@ -29,6 +29,7 @@ import { exceptionsService } from "@/services/exceptionApi.mjs";
import { doctorsService } from "@/services/doctorsApi.mjs"; import { doctorsService } from "@/services/doctorsApi.mjs";
import { usersService } from "@/services/usersApi.mjs"; import { usersService } from "@/services/usersApi.mjs";
import Sidebar from "@/components/Sidebar"; import Sidebar from "@/components/Sidebar";
import WeeklyScheduleCard from "@/components/ui/WeeklyScheduleCard";
type Availability = { type Availability = {
id: string; id: string;
@ -165,20 +166,17 @@ export default function PatientDashboard() {
); );
setAvailability(filteredAvail); setAvailability(filteredAvail);
// Busca exceções // Busca exceções
const exceptionsList = await exceptionsService.list(); const exceptionsList = await exceptionsService.list();
const filteredExc = exceptionsList.filter( const filteredExc = exceptionsList.filter((exc: { doctor_id: string }) => exc.doctor_id === doctor?.id);
(exc: { doctor_id: string }) => exc.doctor_id === doctor?.id setExceptions(filteredExc);
); } catch (e: any) {
console.log(exceptionsList); alert(`${e?.error} ${e?.message}`);
setExceptions(filteredExc); }
} catch (e: any) { };
alert(`${e?.error} ${e?.message}`);
}
};
fetchData(); fetchData();
}, []); }, []);
// Função auxiliar para filtrar o id do doctor correspondente ao user logado // Função auxiliar para filtrar o id do doctor correspondente ao user logado
function findDoctorById(id: string, doctors: Doctor[]) { function findDoctorById(id: string, doctors: Doctor[]) {
@ -320,82 +318,42 @@ export default function PatientDashboard() {
</CardContent> </CardContent>
</Card> </Card>
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Próximas Consultas</CardTitle> <CardTitle>Próximas Consultas</CardTitle>
<CardDescription>Suas consultas agendadas</CardDescription> <CardDescription>Suas consultas agendadas</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center justify-between p-3 bg-blue-50 rounded-lg"> <div className="flex items-center justify-between p-3 bg-blue-50 rounded-lg">
<div> <div>
<p className="font-medium">Dr. João Santos</p> <p className="font-medium">Dr. João Santos</p>
<p className="text-sm text-gray-600">Cardiologia</p> <p className="text-sm text-gray-600">Cardiologia</p>
</div> </div>
<div className="text-right"> <div className="text-right">
<p className="font-medium">02 out</p> <p className="font-medium">02 out</p>
<p className="text-sm text-gray-600">14:30</p> <p className="text-sm text-gray-600">14:30</p>
</div> </div>
</div>
</div>
</CardContent>
</Card>
</div> </div>
</div> <div className="grid md:grid-cols-1 gap-6">
</CardContent> <Card>
</Card> <CardHeader>
</div> <CardTitle>Horário Semanal</CardTitle>
<div className="grid md:grid-cols-1 gap-6"> <CardDescription>Confira rapidamente a sua disponibilidade da semana</CardDescription>
<Card> </CardHeader>
<CardHeader> <CardContent>{loggedDoctor && <WeeklyScheduleCard doctorId={loggedDoctor.id} />}</CardContent>
<CardTitle>Horário Semanal</CardTitle> </Card>
<CardDescription> </div>
Confira rapidamente a sua disponibilidade da semana <div className="grid md:grid-cols-1 gap-6">
</CardDescription> <Card>
</CardHeader> <CardHeader>
<CardContent className="space-y-4 grid md:grid-cols-7 gap-2"> <CardTitle>Exceções</CardTitle>
{[ <CardDescription>Bloqueios e liberações eventuais de agenda</CardDescription>
"sunday", </CardHeader>
"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"> <CardContent className="space-y-4 grid md:grid-cols-7 gap-2">
{exceptions && exceptions.length > 0 ? ( {exceptions && exceptions.length > 0 ? (
@ -411,75 +369,47 @@ export default function PatientDashboard() {
const startTime = formatTime(ex.start_time); const startTime = formatTime(ex.start_time);
const endTime = formatTime(ex.end_time); const endTime = formatTime(ex.end_time);
return ( return (
<div key={ex.id} className="space-y-4"> <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="flex flex-col items-center justify-between p-3 bg-blue-50 rounded-lg shadow-sm">
<div className="text-center"> <div className="text-center">
<p className="font-semibold capitalize">{date}</p> <p className="font-semibold capitalize">{date}</p>
<p className="text-sm text-gray-600"> <p className="text-sm text-gray-600">{startTime && endTime ? `${startTime} - ${endTime}` : "Dia todo"}</p>
{startTime && endTime </div>
? `${startTime} - ${endTime}` <div className="text-center mt-2">
: "Dia todo"} <p className={`text-sm font-medium ${ex.kind === "bloqueio" ? "text-red-600" : "text-green-600"}`}>{ex.kind === "bloqueio" ? "Bloqueio" : "Liberação"}</p>
</p> <p className="text-xs text-gray-500 italic">{ex.reason || "Sem motivo especificado"}</p>
</div> </div>
<div className="text-center mt-2"> <div>
<p <Button className="text-red-600" variant="outline" onClick={() => openDeleteDialog(String(ex.id))}>
className={`text-sm font-medium ${ <Trash2></Trash2>
ex.kind === "bloqueio" </Button>
? "text-red-600" </div>
: "text-green-600" </div>
}`} </div>
> );
{ex.kind === "bloqueio" ? "Bloqueio" : "Liberação"} })
</p> ) : (
<p className="text-xs text-gray-500 italic"> <p className="text-sm text-gray-400 italic col-span-7 text-center">Nenhuma exceção registrada.</p>
{ex.reason || "Sem motivo especificado"} )}
</p> </CardContent>
</div> </Card>
<div> </div>
<Button <AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
className="text-red-600" <AlertDialogContent>
variant="outline" <AlertDialogHeader>
onClick={() => openDeleteDialog(String(ex.id))} <AlertDialogTitle>Confirmar exclusão</AlertDialogTitle>
> <AlertDialogDescription>Tem certeza que deseja excluir esta exceção? Esta ação não pode ser desfeita.</AlertDialogDescription>
<Trash2></Trash2> </AlertDialogHeader>
</Button> <AlertDialogFooter>
</div> <AlertDialogCancel>Cancelar</AlertDialogCancel>
</div> <AlertDialogAction onClick={() => exceptionToDelete && handleDeleteException(exceptionToDelete)} className="bg-red-600 hover:bg-red-700">
</div> Excluir
); </AlertDialogAction>
}) </AlertDialogFooter>
) : ( </AlertDialogContent>
<p className="text-sm text-gray-400 italic col-span-7 text-center"> </AlertDialog>
Nenhuma exceção registrada. </div>
</p> </Sidebar>
)} );
</CardContent>
</Card>
</div>
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Confirmar exclusão</AlertDialogTitle>
<AlertDialogDescription>
Tem certeza que deseja excluir esta exceção? Esta ação não pode
ser desfeita.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancelar</AlertDialogCancel>
<AlertDialogAction
onClick={() =>
exceptionToDelete && handleDeleteException(exceptionToDelete)
}
className="bg-red-600 hover:bg-red-700"
>
Excluir
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</Sidebar>
);
} }

View File

@ -218,36 +218,36 @@ export default function AvailabilityPage() {
} }
}; };
// Mapa de tradução // Mapa de tradução
const weekdaysPT: Record<string, string> = { const weekdaysPT: Record<string, string> = {
sunday: "Domingo", sunday: "Domingo",
monday: "Segunda", monday: "Segunda",
tuesday: "Terça", tuesday: "Terça",
wednesday: "Quarta", wednesday: "Quarta",
thursday: "Quinta", thursday: "Quinta",
friday: "Sexta", friday: "Sexta",
saturday: "Sábado", saturday: "Sábado",
}; };
const fetchData = async () => { const fetchData = async () => {
try { try {
const loggedUser = await usersService.getMe(); const loggedUser = await usersService.getMe();
const doctorList = await doctorsService.list(); const doctorList = await doctorsService.list();
setUserData(loggedUser); setUserData(loggedUser);
const doctor = findDoctorById(loggedUser.user.id, doctorList); const doctor = findDoctorById(loggedUser.user.id, doctorList);
setDoctorId(doctor?.id); setDoctorId(doctor?.id);
console.log(doctor); console.log(doctor);
// Busca disponibilidade // Busca disponibilidade
const availabilityList = await AvailabilityService.list(); const availabilityList = await AvailabilityService.list();
// Filtra já com a variável local // Filtra já com a variável local
const filteredAvail = availabilityList.filter( const filteredAvail = availabilityList.filter(
(disp: { doctor_id: string }) => disp.doctor_id === doctor?.id (disp: { doctor_id: string }) => disp.doctor_id === doctor?.id
); );
setAvailability(filteredAvail); setAvailability(filteredAvail);
} catch (e: any) { } catch (e: any) {
alert(`${e?.error} ${e?.message}`); alert(`${e?.error} ${e?.message}`);
} }
}; };
useEffect(() => { useEffect(() => {
fetchData(); fetchData();
@ -320,20 +320,21 @@ export default function AvailabilityPage() {
} }
} catch {} } catch {}
toast({ toast({
title: "Sucesso", title: "Sucesso",
description: message, description: message,
}); });
router.push("#"); // adicionar página para listar a disponibilidade router.push("#"); // adicionar página para listar a disponibilidade
} catch (err: any) { } catch (err: any) {
toast({ toast({
title: "Erro", title: "Erro",
description: err?.message || "Não foi possível criar a disponibilidade", description: err?.message || "Não foi possível criar a disponibilidade",
}); });
} finally { } finally {
setIsLoading(false); fetchData()
} setIsLoading(false);
}; }
};
const openDeleteDialog = ( const openDeleteDialog = (
schedule: { start: string; end: string }, schedule: { start: string; end: string },
@ -343,38 +344,35 @@ export default function AvailabilityPage() {
setDeleteDialogOpen(true); setDeleteDialogOpen(true);
}; };
const handleDeleteAvailability = async (AvailabilityId: string) => { const handleDeleteAvailability = async (AvailabilityId: string) => {
try { try {
const res = await AvailabilityService.delete(AvailabilityId); const res = await AvailabilityService.delete(AvailabilityId);
let message = "Disponibilidade deletada com sucesso"; let message = "Disponibilidade deletada com sucesso";
try { try {
if (res) { if (res) {
throw new Error( throw new Error(`${res.error} ${res.message}` || "A API retornou erro");
`${res.error} ${res.message}` || "A API retornou erro" } else {
); console.log(message);
} else { }
console.log(message); } catch {}
}
} catch {}
toast({ toast({
title: "Sucesso", title: "Sucesso",
description: message, description: message,
}); });
setAvailability((prev: Availability[]) => setAvailability((prev: Availability[]) => prev.filter((p) => String(p.id) !== String(AvailabilityId)));
prev.filter((p) => String(p.id) !== String(AvailabilityId)) } catch (e: any) {
); toast({
} catch (e: any) { title: "Erro",
toast({ description: e?.message || "Não foi possível deletar a disponibilidade",
title: "Erro", });
description: e?.message || "Não foi possível deletar a disponibilidade", }
}); fetchData()
} setDeleteDialogOpen(false);
setDeleteDialogOpen(false); setSelectedAvailability(null);
setSelectedAvailability(null); };
};
return ( return (
<Sidebar> <Sidebar>
@ -542,142 +540,99 @@ export default function AvailabilityPage() {
</div> </div>
</div> </div>
{/* **AJUSTE DE RESPONSIVIDADE: BOTÕES DE AÇÃO** */} {/* **AJUSTE DE RESPONSIVIDADE: BOTÕES DE AÇÃO** */}
{/* Alinha à direita em telas maiores e empilha (com o botão primário no final) em telas menores */} {/* Alinha à direita em telas maiores e empilha (com o botão primário no final) em telas menores */}
{/* Alteração aqui: Adicionado w-full aos Links e Buttons para ocuparem a largura total em telas pequenas */} {/* Alteração aqui: Adicionado w-full aos Links e Buttons para ocuparem a largura total em telas pequenas */}
<div className="flex flex-col-reverse sm:flex-row sm:justify-between gap-4"> <div className="flex flex-col-reverse sm:flex-row sm:justify-between gap-4">
<Link <Link href="/doctor/disponibilidade/excecoes" className="w-full sm:w-auto">
href="/doctor/disponibilidade/excecoes" <Button variant="default" className="w-full sm:w-auto">Adicionar Exceção</Button>
className="w-full sm:w-auto" </Link>
> <div className="flex flex-col sm:flex-row gap-4 w-full sm:w-auto"> {/* Ajustado para empilhar os botões Cancelar e Salvar em telas pequenas */}
<Button <Link href="/doctor/dashboard" className="w-full sm:w-auto">
variant="default" <Button variant="outline" className="w-full sm:w-auto">Cancelar</Button>
className="w-full sm:w-auto bg-blue-600 hover:bg-blue-700 text-white cursor-pointer" </Link>
> <Button type="submit" className="bg-green-600 hover:bg-green-700 w-full sm:w-auto">
Adicionar Exceção Salvar Disponibilidade
</Button> </Button>
</Link> </div>
<div className="flex flex-col sm:flex-row gap-4 w-full sm:w-auto">
{" "}
{/* Ajustado para empilhar os botões Cancelar e Salvar em telas pequenas */}
<Link href="/doctor/dashboard" className="w-full sm:w-auto">
<Button
variant="outline"
className="w-full sm:w-auto cursor-pointer"
>
Cancelar
</Button>
</Link>
<Button
type="submit"
className="bg-blue-600 hover:bg-blue-700 w-full sm:w-auto cursor-pointer"
>
Salvar Disponibilidade
</Button>
</div>
</div>
</form>
{/* **AJUSTE DE RESPONSIVIDADE: CARD DE HORÁRIO SEMANAL** */}
<div>
<Card>
<CardHeader>
<CardTitle>Horário Semanal</CardTitle>
<CardDescription>
Confira ou altere a sua disponibilidade da semana
</CardDescription>
</CardHeader>
{/* Define um grid responsivo para os dias da semana (1 coluna em móvel, 2 em pequeno, 3 em médio e 7 em telas grandes) */}
<CardContent className="space-y-4 grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg: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 h-full">
<p className="font-medium capitalize text-center mb-2">
{weekdaysPT[day]}
</p>
<div className="text-center w-full">
{times.length > 0 ? (
times.map((t, i) => (
<div key={i}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<p className="text-sm text-gray-600 cursor-pointer p-1 rounded hover:text-accent-foreground hover:bg-gray-200 transition-colors duration-150">
{formatTime(t.start)} - {formatTime(t.end)}
</p>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() => handleOpenModal(t, day)}
>
<Edit className="w-4 h-4 mr-2" />
Editar
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => openDeleteDialog(t, day)}
className="text-red-600 focus:bg-red-50 focus:text-red-600"
>
<Trash2 className="w-4 h-4 mr-2" />
Excluir
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
))
) : (
<p className="text-sm text-gray-400 italic">
Sem horário
</p>
)}
</div>
</div> </div>
</div> </form>
);
})}
</CardContent>
</Card>
</div>
{/* AlertDialog e Modal de Edição (não precisam de grandes ajustes de layout, apenas garantindo que os componentes sejam responsivos internamente) */} {/* **AJUSTE DE RESPONSIVIDADE: CARD DE HORÁRIO SEMANAL** */}
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}> <div>
<AlertDialogContent> <Card>
<AlertDialogHeader> <CardHeader>
<AlertDialogTitle>Confirmar exclusão</AlertDialogTitle> <CardTitle>Horário Semanal</CardTitle>
<AlertDialogDescription> <CardDescription>Confira ou altere a sua disponibilidade da semana</CardDescription>
Tem certeza que deseja excluir esta disponibilidade? Esta ação </CardHeader>
não pode ser desfeita. {/* Define um grid responsivo para os dias da semana (1 coluna em móvel, 2 em pequeno, 3 em médio e 7 em telas grandes) */}
</AlertDialogDescription> <CardContent className="space-y-4 grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-7 gap-2">
</AlertDialogHeader> {["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"].map((day) => {
<AlertDialogFooter> const times = schedule[day] || [];
<AlertDialogCancel>Cancelar</AlertDialogCancel> return (
<AlertDialogAction <div key={day} className="space-y-4">
onClick={() => <div className="flex flex-col items-center justify-between p-3 bg-blue-50 rounded-lg h-full">
selectedAvailability && <p className="font-medium capitalize text-center mb-2">{weekdaysPT[day]}</p>
handleDeleteAvailability(selectedAvailability.id) <div className="text-center w-full">
} {times.length > 0 ? (
className="bg-red-600 hover:bg-red-700" times.map((t, i) => (
> <div key={i}>
Excluir <DropdownMenu>
</AlertDialogAction> <DropdownMenuTrigger asChild>
</AlertDialogFooter> <p className="text-sm text-gray-600 cursor-pointer rounded hover:text-accent-foreground hover:bg-gray-200 transition-colors duration-150">
</AlertDialogContent> {formatTime(t.start)} - {formatTime(t.end)}
</AlertDialog> </p>
</div> </DropdownMenuTrigger>
<AvailabilityEditModal <DropdownMenuContent align="end">
availability={selectedAvailability} <DropdownMenuItem onClick={() => handleOpenModal(t, day)}>
isOpen={isModalOpen} <Edit className="w-4 h-4 mr-2" />
onClose={handleCloseModal} Editar
onSubmit={handleEdit} </DropdownMenuItem>
/> <DropdownMenuItem
</Sidebar> onClick={() => openDeleteDialog(t, day)}
); className="text-red-600 focus:bg-red-50 focus:text-red-600">
<Trash2 className="w-4 h-4 mr-2" />
Excluir
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
))
) : (
<p className="text-sm text-gray-400 italic">Sem horário</p>
)}
</div>
</div>
</div>
);
})}
</CardContent>
</Card>
</div>
{/* AlertDialog e Modal de Edição (não precisam de grandes ajustes de layout, apenas garantindo que os componentes sejam responsivos internamente) */}
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Confirmar exclusão</AlertDialogTitle>
<AlertDialogDescription>Tem certeza que deseja excluir esta disponibilidade? Esta ação não pode ser desfeita.</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancelar</AlertDialogCancel>
<AlertDialogAction onClick={() => selectedAvailability && handleDeleteAvailability(selectedAvailability.id)} className="bg-red-600 hover:bg-red-700">
Excluir
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
<AvailabilityEditModal
availability={selectedAvailability}
isOpen={isModalOpen}
onClose={handleCloseModal}
onSubmit={handleEdit}
/>
</Sidebar>
);
} }

View File

@ -0,0 +1,185 @@
"use client";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import Sidebar from "@/components/Sidebar";
import WeeklyScheduleCard from "@/components/ui/WeeklyScheduleCard";
import { useEffect, useState, useMemo } from "react";
import { AvailabilityService } from "@/services/availabilityApi.mjs";
import { doctorsService } from "@/services/doctorsApi.mjs";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Button } from "@/components/ui/button";
import { Filter } from "lucide-react";
type Doctor = {
id: string;
full_name: string;
specialty: string;
active: boolean;
};
type Availability = {
id: string;
doctor_id: string;
weekday: string;
start_time: string;
end_time: string;
};
export default function AllAvailabilities() {
const [availabilities, setAvailabilities] = useState<Availability[] | null>(null);
const [doctors, setDoctors] = useState<Doctor[] | null>(null);
// 🔎 Filtros
const [search, setSearch] = useState("");
const [specialty, setSpecialty] = useState("all");
// 🔄 Paginação
const ITEMS_PER_PAGE = 6;
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchData = async () => {
try {
const doctorsList = await doctorsService.list();
setDoctors(doctorsList);
const availabilityList = await AvailabilityService.list();
setAvailabilities(availabilityList);
} catch (e: any) {
alert(`${e?.error} ${e?.message}`);
} finally {
setLoading(false);
}
};
fetchData();
}, []);
// 🎯 Obter todas as especialidades existentes
const specialties = useMemo(() => {
if (!doctors) return [];
const unique = Array.from(new Set(doctors.map((d) => d.specialty)));
return unique;
}, [doctors]);
// 🔍 Filtrar médicos por especialidade + nome
const filteredDoctors = useMemo(() => {
if (!doctors) return [];
return doctors.filter((doctor) => (specialty === "all" ? true : doctor.specialty === specialty)).filter((doctor) => doctor.full_name.toLowerCase().includes(search.toLowerCase()));
}, [doctors, search, specialty]);
// 📄 Paginação (após filtros!)
const totalPages = Math.ceil(filteredDoctors.length / ITEMS_PER_PAGE);
const paginatedDoctors = filteredDoctors.slice((page - 1) * ITEMS_PER_PAGE, page * ITEMS_PER_PAGE);
const goNext = () => setPage((p) => Math.min(p + 1, totalPages));
const goPrev = () => setPage((p) => Math.max(p - 1, 1));
if (loading) {
return (
<Sidebar>
<div className="p-6 text-gray-500">Carregando dados...</div>
</Sidebar>
);
}
if (!doctors || !availabilities) {
return (
<Sidebar>
<div className="p-6 text-red-600 font-medium">Não foi possível carregar médicos ou disponibilidades.</div>
</Sidebar>
);
}
return (
<Sidebar>
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold text-gray-900">Disponibilidade dos Médicos</h1>
<p className="text-gray-600">Visualize a agenda semanal individual de cada médico.</p>
</div>
<Card>
<CardContent>
{/* 🔎 Filtros */}
<div className="flex flex-col md:flex-row gap-4 items-center">
{/* Filtro por nome */}
<Filter className="w-4 h-4 mr-2" />
<Input
placeholder="Buscar por nome do médico..."
value={search}
onChange={(e) => {
setSearch(e.target.value);
setPage(1);
}}
className="w-full md:w-1/3"
/>
{/* Filtro por especialidade */}
<Select
onValueChange={(value) => {
setSpecialty(value);
setPage(1);
}}
defaultValue="all"
>
<SelectTrigger className="w-full md:w-64">
<SelectValue placeholder="Especialidade" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Todas as especialidades</SelectItem>
{specialties.map((sp) => (
<SelectItem key={sp} value={sp}>
{sp}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</CardContent>
</Card>
{/* GRID de cards */}
<div className="grid md:grid-cols-1 lg:grid-cols-1 gap-6">
{paginatedDoctors.map((doctor) => {
const doctorAvailabilities = availabilities.filter((a) => a.doctor_id === doctor.id);
return (
<Card key={doctor.id}>
<CardHeader>
<CardTitle className="text-xl font-semibold">{doctor.full_name}</CardTitle>
</CardHeader>
<CardContent>
<WeeklyScheduleCard doctorId={doctor.id} />
</CardContent>
</Card>
);
})}
</div>
{/* 📄 Paginação */}
{totalPages > 1 && (
<div className="flex justify-center items-center gap-4 pt-4">
<Button variant="outline" onClick={goPrev} disabled={page === 1}>
Anterior
</Button>
<span className="text-gray-700 font-medium">
Página {page} de {totalPages}
</span>
<Button variant="outline" onClick={goNext} disabled={page === totalPages}>
Próxima
</Button>
</div>
)}
</div>
</Sidebar>
);
}

View File

@ -188,18 +188,14 @@ export default function Sidebar({ children }: SidebarProps) {
}, },
]; ];
const managerItems: MenuItem[] = [ const managerItems: MenuItem[] = [
{ href: "/manager/dashboard", icon: Home, label: "Dashboard" }, { href: "/manager/dashboard", icon: Home, label: "Dashboard" },
{ href: "#", icon: ClipboardMinus, label: "Relatórios gerenciais" }, { href: "/manager/usuario", icon: Users, label: "Gestão de Usuários" },
{ href: "/manager/usuario", icon: Users, label: "Gestão de Usuários" }, { href: "/manager/home", icon: Stethoscope, label: "Gestão de Médicos" },
{ href: "/manager/home", icon: Stethoscope, label: "Gestão de Médicos" }, { href: "/manager/pacientes", icon: Users, label: "Gestão de Pacientes" },
{ href: "/manager/pacientes", icon: Users, label: "Gestão de Pacientes" }, { href: "/secretary/appointments", icon: CalendarCheck2, label: "Consultas" },
{ { href: "/manager/disponibilidade", icon: ClipboardList, label: "Disponibilidade" },
href: "/secretary/appointments", ];
icon: CalendarCheck2,
label: "Consultas",
},
];
switch (role) { switch (role) {
case "gestor": case "gestor":

View File

@ -0,0 +1,105 @@
"use client";
import { useEffect, useState } from "react";
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card";
import { AvailabilityService } from "@/services/availabilityApi.mjs";
import { doctorsService } from "@/services/doctorsApi.mjs";
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;
};
interface WeeklyScheduleProps {
doctorId?: string;
}
export default function WeeklyScheduleCard({ doctorId }: WeeklyScheduleProps) {
const [schedule, setSchedule] = useState<Record<string, { start: string; end: string }[]>>({});
const [loading, setLoading] = useState(true);
const weekdaysPT: Record<string, string> = {
sunday: "Domingo",
monday: "Segunda",
tuesday: "Terça",
wednesday: "Quarta",
thursday: "Quinta",
friday: "Sexta",
saturday: "Sábado",
};
const formatTime = (time?: string | null) => time?.split(":")?.slice(0, 2).join(":") ?? "";
function formatAvailability(data: Availability[]) {
const grouped = data.reduce((acc: any, item) => {
const { weekday, start_time, end_time } = item;
if (!acc[weekday]) acc[weekday] = [];
acc[weekday].push({ start: start_time, end: end_time });
return acc;
}, {});
return grouped;
}
useEffect(() => {
const fetchSchedule = async () => {
try {
const availabilityList = await AvailabilityService.list();
const filtered = availabilityList.filter((a: Availability) => a.doctor_id == doctorId);
const formatted = formatAvailability(filtered);
setSchedule(formatted);
} catch (err) {
console.error("Erro ao carregar horários:", err);
} finally {
setLoading(false);
}
};
fetchSchedule();
}, []);
return (
<div className="space-y-4 grid md:grid-cols-7 gap-2">
{loading ? (
<p className="text-sm text-gray-500 col-span-7 text-center">Carregando...</p>
) : (
["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">
<p className="font-medium capitalize">{weekdaysPT[day]}</p>
<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>
);
})
)}
</div>
);
}