Criação de página de disponibilidade para o gestor
This commit is contained in:
parent
01aecc4485
commit
da35ebbff5
@ -14,6 +14,7 @@ import { exceptionsService } from "@/services/exceptionApi.mjs";
|
||||
import { doctorsService } from "@/services/doctorsApi.mjs";
|
||||
import { usersService } from "@/services/usersApi.mjs";
|
||||
import Sidebar from "@/components/Sidebar";
|
||||
import WeeklyScheduleCard from "@/components/ui/WeeklyScheduleCard";
|
||||
|
||||
type Availability = {
|
||||
id: string;
|
||||
@ -35,33 +36,33 @@ type Schedule = {
|
||||
};
|
||||
|
||||
type Doctor = {
|
||||
id: string;
|
||||
user_id: string | null;
|
||||
crm: string;
|
||||
crm_uf: string;
|
||||
specialty: string;
|
||||
full_name: string;
|
||||
cpf: string;
|
||||
email: string;
|
||||
phone_mobile: string | null;
|
||||
phone2: string | null;
|
||||
cep: string | null;
|
||||
street: string | null;
|
||||
number: string | null;
|
||||
complement: string | null;
|
||||
neighborhood: string | null;
|
||||
city: string | null;
|
||||
state: string | null;
|
||||
birth_date: string | null;
|
||||
rg: string | null;
|
||||
active: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
created_by: string;
|
||||
updated_by: string | null;
|
||||
max_days_in_advance: number;
|
||||
rating: number | null;
|
||||
}
|
||||
id: string;
|
||||
user_id: string | null;
|
||||
crm: string;
|
||||
crm_uf: string;
|
||||
specialty: string;
|
||||
full_name: string;
|
||||
cpf: string;
|
||||
email: string;
|
||||
phone_mobile: string | null;
|
||||
phone2: string | null;
|
||||
cep: string | null;
|
||||
street: string | null;
|
||||
number: string | null;
|
||||
complement: string | null;
|
||||
neighborhood: string | null;
|
||||
city: string | null;
|
||||
state: string | null;
|
||||
birth_date: string | null;
|
||||
rg: string | null;
|
||||
active: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
created_by: string;
|
||||
updated_by: string | null;
|
||||
max_days_in_advance: number;
|
||||
rating: number | null;
|
||||
};
|
||||
|
||||
interface UserPermissions {
|
||||
isAdmin: boolean;
|
||||
@ -94,15 +95,15 @@ interface UserData {
|
||||
}
|
||||
|
||||
interface Exception {
|
||||
id: string; // id da exceção
|
||||
doctor_id: string;
|
||||
date: string; // formato YYYY-MM-DD
|
||||
start_time: string | null; // null = dia inteiro
|
||||
end_time: string | null; // null = dia inteiro
|
||||
kind: "bloqueio" | "disponibilidade"; // tipos conhecidos
|
||||
reason: string | null; // pode ser null
|
||||
created_at: string; // timestamp ISO
|
||||
created_by: string;
|
||||
id: string; // id da exceção
|
||||
doctor_id: string;
|
||||
date: string; // formato YYYY-MM-DD
|
||||
start_time: string | null; // null = dia inteiro
|
||||
end_time: string | null; // null = dia inteiro
|
||||
kind: "bloqueio" | "disponibilidade"; // tipos conhecidos
|
||||
reason: string | null; // pode ser null
|
||||
created_at: string; // timestamp ISO
|
||||
created_by: string;
|
||||
}
|
||||
|
||||
export default function PatientDashboard() {
|
||||
@ -128,44 +129,38 @@ export default function PatientDashboard() {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const doctorsList: Doctor[] = await doctorsService.list();
|
||||
const doctor = doctorsList[0];
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const doctorsList: Doctor[] = await doctorsService.list();
|
||||
const doctor = doctorsList[0];
|
||||
|
||||
// Salva no estado
|
||||
setLoggedDoctor(doctor);
|
||||
// Salva no estado
|
||||
setLoggedDoctor(doctor);
|
||||
|
||||
// Busca disponibilidade
|
||||
const availabilityList = await AvailabilityService.list();
|
||||
|
||||
// Filtra já com a variável local
|
||||
const filteredAvail = availabilityList.filter(
|
||||
(disp: { doctor_id: string }) => disp.doctor_id === doctor?.id
|
||||
);
|
||||
setAvailability(filteredAvail);
|
||||
// Busca disponibilidade
|
||||
const availabilityList = await AvailabilityService.list();
|
||||
|
||||
// Busca exceções
|
||||
const exceptionsList = await exceptionsService.list();
|
||||
const filteredExc = exceptionsList.filter(
|
||||
(exc: { doctor_id: string }) => exc.doctor_id === doctor?.id
|
||||
);
|
||||
console.log(exceptionsList)
|
||||
setExceptions(filteredExc);
|
||||
// Filtra já com a variável local
|
||||
const filteredAvail = availabilityList.filter((disp: { doctor_id: string }) => disp.doctor_id === doctor?.id);
|
||||
setAvailability(filteredAvail);
|
||||
|
||||
} catch (e: any) {
|
||||
alert(`${e?.error} ${e?.message}`);
|
||||
}
|
||||
};
|
||||
// Busca exceções
|
||||
const exceptionsList = await exceptionsService.list();
|
||||
const filteredExc = exceptionsList.filter((exc: { doctor_id: string }) => exc.doctor_id === doctor?.id);
|
||||
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
|
||||
function findDoctorById(id: string, doctors: Doctor[]) {
|
||||
return doctors.find((doctor) => doctor.user_id === id);
|
||||
}
|
||||
|
||||
|
||||
const openDeleteDialog = (exceptionId: string) => {
|
||||
setExceptionToDelete(exceptionId);
|
||||
setDeleteDialogOpen(true);
|
||||
@ -173,7 +168,7 @@ export default function PatientDashboard() {
|
||||
|
||||
const handleDeleteException = async (ExceptionId: string) => {
|
||||
try {
|
||||
alert(ExceptionId)
|
||||
alert(ExceptionId);
|
||||
const res = await exceptionsService.delete(ExceptionId);
|
||||
|
||||
let message = "Exceção deletada com sucesso";
|
||||
@ -316,31 +311,7 @@ export default function PatientDashboard() {
|
||||
<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>
|
||||
<CardContent>{loggedDoctor && <WeeklyScheduleCard doctorId={loggedDoctor.id} />}</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="grid md:grid-cols-1 gap-6">
|
||||
@ -358,7 +329,7 @@ export default function PatientDashboard() {
|
||||
weekday: "long",
|
||||
day: "2-digit",
|
||||
month: "long",
|
||||
timeZone: "UTC"
|
||||
timeZone: "UTC",
|
||||
});
|
||||
|
||||
const startTime = formatTime(ex.start_time);
|
||||
@ -369,11 +340,7 @@ export default function PatientDashboard() {
|
||||
<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
|
||||
? `${startTime} - ${endTime}`
|
||||
: "Dia todo"}
|
||||
</p>
|
||||
<p className="text-sm text-gray-600">{startTime && endTime ? `${startTime} - ${endTime}` : "Dia todo"}</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>
|
||||
|
||||
185
app/manager/disponibilidade/page.tsx
Normal file
185
app/manager/disponibilidade/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -178,7 +178,8 @@ export default function Sidebar({ children }: SidebarProps) {
|
||||
{ href: "/manager/usuario", icon: Users, label: "Gestão de Usuários" },
|
||||
{ href: "/manager/home", icon: Stethoscope, label: "Gestão de Médicos" },
|
||||
{ href: "/manager/pacientes", icon: Users, label: "Gestão de Pacientes" },
|
||||
{ href: "/secretary/appointments", icon: CalendarCheck2, label: "Consultas" }, //adicionar botão de voltar pra pagina anterior
|
||||
{ href: "/secretary/appointments", icon: CalendarCheck2, label: "Consultas" },
|
||||
{ href: "/manager/disponibilidade", icon: ClipboardList, label: "Disponibilidade" },
|
||||
];
|
||||
|
||||
let menuItems: MenuItem[];
|
||||
|
||||
105
components/ui/WeeklyScheduleCard.tsx
Normal file
105
components/ui/WeeklyScheduleCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user