diff --git a/app/doctor/dashboard/page.tsx b/app/doctor/dashboard/page.tsx index d9615f2..bf91156 100644 --- a/app/doctor/dashboard/page.tsx +++ b/app/doctor/dashboard/page.tsx @@ -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(null); + const [exceptions, setExceptions] = useState(null); + const [schedule, setSchedule] = useState>({}); + const formatTime = (time: string) => time.split(":").slice(0, 2).join(":"); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [patientToDelete, setPatientToDelete] = useState(null); + const [error, setError] = useState(null); + + // Mapa de tradução + const weekdaysPT: Record = { + 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); + + return schedule; + } + + useEffect(() => { + if (availability) { + const formatted = formatAvailability(availability); + setSchedule(formatted); + } + }, [availability]); + return (
@@ -85,7 +214,102 @@ export default function PatientDashboard() {
+
+ + + Horário Semanal + Confira rapidamente a sua disponibilidade da semana + + + {["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"].map((day) => { + const times = schedule[day] || []; + return ( +
+
+
+

{weekdaysPT[day]}

+
+
+ {times.length > 0 ? ( + times.map((t, i) => ( +

+ {formatTime(t.start)}
{formatTime(t.end)} +

+ )) + ) : ( +

Sem horário

+ )} +
+
+
+ ); + })} +
+
+
+
+ + + Exceções + Bloqueios e liberações eventuais de agenda + + + + {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 ( +
+
+
+

{date}

+

+ {startTime} - {endTime}
- +

+
+
+

{ex.kind === "bloqueio" ? "Bloqueio" : "Liberação"}

+

{ex.reason || "Sem motivo especificado"}

+
+
+ +
+
+
+ ); + }) + ) : ( +

Nenhuma exceção registrada.

+ )} +
+
+
+ + + + Confirmar exclusão + Tem certeza que deseja excluir este paciente? Esta ação não pode ser desfeita. + + + Cancelar + patientToDelete && handleDeletePatient(patientToDelete)} className="bg-red-600 hover:bg-red-700"> + Excluir + + + +
- ) + ); } diff --git a/app/doctor/disponibilidade/excecoes/page.tsx b/app/doctor/disponibilidade/excecoes/page.tsx new file mode 100644 index 0000000..115ff54 --- /dev/null +++ b/app/doctor/disponibilidade/excecoes/page.tsx @@ -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([]); + const router = useRouter(); + const [filteredAppointments, setFilteredAppointments] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const userInfo = JSON.parse(localStorage.getItem("user_info") || "{}"); + const doctorIdTemp = "3bb9ee4a-cfdd-4d81-b628-383907dfa225"; + const [tipo, setTipo] = useState(""); + + // NOVO ESTADO 1: Armazena os dias com consultas (para o calendário) + const [bookedDays, setBookedDays] = useState([]); + + // NOVO ESTADO 2: Armazena a data selecionada no calendário + const [selectedCalendarDate, setSelectedCalendarDate] = useState(new Date()); + + const handleSubmit = async (e: React.FormEvent) => { + 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 ( + +
+
+

Adicione exceções

+

Altere a disponibilidade em casos especiais para o Dr. {userInfo.user_metadata.full_name}

+
+ +
+

Consultas para: {displayDate}

+ +
+ + {/* NOVO LAYOUT DE DUAS COLUNAS */} +
+ {/* COLUNA 1: CALENDÁRIO */} +
+ + + + + Calendário + +

Selecione a data desejada.

+
+ + + +
+
+ + {/* COLUNA 2: FORM PARA ADICIONAR EXCEÇÃO */} +
+ {isLoading ? ( +

Carregando a agenda...

+ ) : !selectedCalendarDate ? ( +

Selecione uma data.

+ ) : ( +
+
+

Dados

+
+
+
+ + +
+
+ + +
+
+ +
+ + +
+
+ + +
+
+
+ +
+ + + + +
+
+ )} +
+
+
+
+ ); +} diff --git a/app/doctor/disponibilidade/page.tsx b/app/doctor/disponibilidade/page.tsx new file mode 100644 index 0000000..127b93f --- /dev/null +++ b/app/doctor/disponibilidade/page.tsx @@ -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(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(""); + + 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) => { + 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 ( + +
+
+
+

Definir Disponibilidade

+

Defina sua disponibilidade para consultas

+
+
+ +
+
+

Dados

+ +
+
+
+ +
+ + + + + + + +
+
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+
+ +
+ + + + + + + +
+
+
+
+ ); +} diff --git a/components/doctor-layout.tsx b/components/doctor-layout.tsx index 69df494..509af00 100644 --- a/components/doctor-layout.tsx +++ b/components/doctor-layout.tsx @@ -5,7 +5,7 @@ import { useState, useEffect } from "react"; import { useRouter, usePathname } from "next/navigation"; import Link from "next/link"; import Cookies from "js-cookie"; // Manteremos para o logout, se necessário -import { api } from '@/services/api.mjs'; +import { api } from "@/services/api.mjs"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -48,7 +48,7 @@ export default function DoctorLayout({ children }: PatientLayoutProps) { if (userInfoString && token) { const userInfo = JSON.parse(userInfoString); - + setDoctorData({ id: userInfo.id || "", name: userInfo.user_metadata?.full_name || "Doutor(a)", @@ -86,24 +86,23 @@ export default function DoctorLayout({ children }: PatientLayoutProps) { setShowLogoutDialog(true); }; - // --- 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 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); @@ -114,10 +113,36 @@ export default function DoctorLayout({ children }: PatientLayoutProps) { }; const menuItems = [ - { href: "#", icon: Home, label: "Dashboard" }, - { href: "/doctor/medicos/consultas", icon: Calendar, label: "Consultas" }, - { href: "#", icon: Clock, label: "Editor de Laudo" }, - { href: "/doctor/medicos", icon: User, label: "Pacientes" }, + { + href: "/doctor/dashboard", + icon: Home, + label: "Dashboard", + // Botão para o dashboard do médico + }, + { + href: "/doctor/medicos/consultas", + icon: Calendar, + label: "Consultas", + // Botão para página de consultas marcadas do médico atual + }, + { + href: "#", + icon: Clock, + label: "Editor de Laudo", + // Botão para página do editor de laudo + }, + { + href: "/doctor/medicos", + icon: User, + 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) { @@ -143,7 +168,6 @@ export default function DoctorLayout({ children }: PatientLayoutProps) { - - // ... (seu código anterior) - - {/* Sidebar para desktop */} -
-
-
- {!sidebarCollapsed && ( -
-
-
+ {/* Sidebar para desktop */} +
+
+
+ {!sidebarCollapsed && ( +
+
+
+
+ MediConnect
- MediConnect -
- )} - + )} + +
-
- -
-
- {!sidebarCollapsed && ( - <> - +
+
+ {!sidebarCollapsed && ( + <> + + + + {doctorData.name + .split(" ") + .map((n) => n[0]) + .join("")} + + +
+

{doctorData.name}

+

{doctorData.specialty}

+
+ + )} + {sidebarCollapsed && ( + {doctorData.name @@ -209,39 +247,17 @@ export default function DoctorLayout({ children }: PatientLayoutProps) { .join("")} -
-

{doctorData.name}

-

{doctorData.specialty}

-
- - )} - {sidebarCollapsed && ( - - - - {doctorData.name - .split(" ") - .map((n) => n[0]) - .join("")} - - - )} -
+ )} +
-
- - {!sidebarCollapsed && Sair} +
+ + {!sidebarCollapsed && Sair} +
- -
- {isMobileMenuOpen && ( -
- )} + {isMobileMenuOpen &&
}
@@ -287,7 +303,15 @@ export default function DoctorLayout({ children }: PatientLayoutProps) {

{doctorData.specialty}

- @@ -335,4 +359,4 @@ export default function DoctorLayout({ children }: PatientLayoutProps) {
); -} \ No newline at end of file +} diff --git a/services/availabilityApi.mjs b/services/availabilityApi.mjs new file mode 100644 index 0000000..40b7fcf --- /dev/null +++ b/services/availabilityApi.mjs @@ -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}`), +}; diff --git a/services/exceptionApi.mjs b/services/exceptionApi.mjs new file mode 100644 index 0000000..62e893e --- /dev/null +++ b/services/exceptionApi.mjs @@ -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}`), +}; diff --git a/services/patientsApi.mjs b/services/patientsApi.mjs index 2a5ba54..5eb1e7c 100644 --- a/services/patientsApi.mjs +++ b/services/patientsApi.mjs @@ -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}`), };