From 6846a30f66ab25dec8437027c8e60da59cc83117 Mon Sep 17 00:00:00 2001 From: GagoDuBroca Date: Tue, 14 Oct 2025 10:18:06 -0300 Subject: [PATCH 1/5] disponibilidade --- app/doctor/disponibilidade/page.tsx | 130 ++++++++++++++++++++++++++++ components/doctor-layout.tsx | 8 +- services/availabilityApi.mjs | 8 ++ 3 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 app/doctor/disponibilidade/page.tsx create mode 100644 services/availabilityApi.mjs diff --git a/app/doctor/disponibilidade/page.tsx b/app/doctor/disponibilidade/page.tsx new file mode 100644 index 0000000..0b4c021 --- /dev/null +++ b/app/doctor/disponibilidade/page.tsx @@ -0,0 +1,130 @@ +"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" + +export default function AvailabilityPage() { + const [error, setError] = useState(null); + + useEffect(() => { + const fetchData = async () => { + try { + const response = await AvailabilityService.list(); + console.log(response); + } catch (e: any) { + alert(`${e?.error} ${e?.message}`); + } + }; + + fetchData(); +}, []); + return ( + +
+
+
+

Definir Disponibilidade

+

Defina sua disponibilidade para consultas

+
+
+ +
+
+

Dados

+ +
+
+
+ +
+ + + + + + + +
+
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+
+ +
+ + + + +
+
+
+
+ ); +} diff --git a/components/doctor-layout.tsx b/components/doctor-layout.tsx index 27d5cdc..42a3df6 100644 --- a/components/doctor-layout.tsx +++ b/components/doctor-layout.tsx @@ -103,7 +103,7 @@ useEffect(() => { const menuItems = [ { - href: "#", + href: "/doctor/dashboard", icon: Home, label: "Dashboard", // Botão para o dashboard do médico @@ -126,6 +126,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) { diff --git a/services/availabilityApi.mjs b/services/availabilityApi.mjs new file mode 100644 index 0000000..71be51e --- /dev/null +++ b/services/availabilityApi.mjs @@ -0,0 +1,8 @@ +import { api } from "./api.mjs"; + +export const AvailabilityService = { + list: () => api.get("/rest/v1/doctor_availability"), + 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}?id=eq.${id}`), +}; \ No newline at end of file From 1538d37e512f86b28df68fd98f30509e00a10a7c Mon Sep 17 00:00:00 2001 From: StsDanilo Date: Tue, 14 Oct 2025 20:57:33 -0300 Subject: [PATCH 2/5] =?UTF-8?q?adicionado=20hor=C3=A1rio=20semanal=20ao=20?= =?UTF-8?q?dashboard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/doctor/dashboard/page.tsx | 126 ++++++++++++++++++++++++++-- app/doctor/disponibilidade/page.tsx | 118 ++++++++++++++++++-------- 2 files changed, 205 insertions(+), 39 deletions(-) diff --git a/app/doctor/dashboard/page.tsx b/app/doctor/dashboard/page.tsx index d9615f2..5d90d6b 100644 --- a/app/doctor/dashboard/page.tsx +++ b/app/doctor/dashboard/page.tsx @@ -1,10 +1,91 @@ -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, Plus } from "lucide-react"; +import Link from "next/link"; +import { useEffect, useState } from "react"; +import { AvailabilityService } from "@/services/availabilityApi.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; +}; + +type Schedule = { + weekday: object; +}; export default function PatientDashboard() { + const userInfo = JSON.parse(localStorage.getItem("user_info") || "{}"); + const doctorId = "58ea5330-5cfe-4433-a218-2749844aee89"; //userInfo.id; + const [availability, setAvailability] = useState(null); + const [schedule, setSchedule] = useState>({}); + const formatTime = (time: string) => time.split(":").slice(0, 2).join(":"); + // 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 { + const response = await AvailabilityService.list(); + const filteredResponse = response.filter((disp: { doctor_id: any }) => disp.doctor_id == doctorId); + setAvailability(filteredResponse); + } catch (e: any) { + alert(`${e?.error} ${e?.message}`); + } + }; + fetchData(); + }, []); + + 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 +166,40 @@ 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

+ )} +
+
+
+ ); + })} +
+
+
- ) + ); } diff --git a/app/doctor/disponibilidade/page.tsx b/app/doctor/disponibilidade/page.tsx index 0b4c021..55c3cac 100644 --- a/app/doctor/disponibilidade/page.tsx +++ b/app/doctor/disponibilidade/page.tsx @@ -7,23 +7,77 @@ 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 { 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); - - useEffect(() => { - const fetchData = async () => { - try { - const response = await AvailabilityService.list(); - console.log(response); - } catch (e: any) { - alert(`${e?.error} ${e?.message}`); - } - }; + const [error, setError] = useState(null); + const router = useRouter(); + const [isLoading, setIsLoading] = useState(false); + const userInfo = JSON.parse(localStorage.getItem("user_info") || "{}"); + 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: userInfo.id, + created_by: userInfo.id, + 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); + } + }; - fetchData(); -}, []); return (
@@ -34,65 +88,65 @@ export default function AvailabilityPage() {
-
+

Dados

-
+
- -
+ +
- +
- +
- +
@@ -100,18 +154,16 @@ export default function AvailabilityPage() { - setModalidadeConsulta(value)} value={modalidadeConsulta}> Presencial Telemedicina -
-
@@ -120,7 +172,7 @@ export default function AvailabilityPage() {
From 88f8954dd3f27e63d636597df0eb72661b35b297 Mon Sep 17 00:00:00 2001 From: StsDanilo Date: Wed, 15 Oct 2025 19:12:12 -0300 Subject: [PATCH 3/5] =?UTF-8?q?Adicionado=20cria=C3=A7=C3=A3o=20de=20dispo?= =?UTF-8?q?nibilidade=20e=20exce=C3=A7=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/doctor/dashboard/page.tsx | 114 +++++++++- app/doctor/disponibilidade/excecoes/page.tsx | 219 +++++++++++++++++++ app/doctor/disponibilidade/page.tsx | 12 +- services/exceptionApi.mjs | 7 + 4 files changed, 346 insertions(+), 6 deletions(-) create mode 100644 app/doctor/disponibilidade/excecoes/page.tsx create mode 100644 services/exceptionApi.mjs diff --git a/app/doctor/dashboard/page.tsx b/app/doctor/dashboard/page.tsx index 5d90d6b..c8a6c2c 100644 --- a/app/doctor/dashboard/page.tsx +++ b/app/doctor/dashboard/page.tsx @@ -3,10 +3,13 @@ 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 { 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; @@ -29,10 +32,15 @@ type Schedule = { export default function PatientDashboard() { const userInfo = JSON.parse(localStorage.getItem("user_info") || "{}"); - const doctorId = "58ea5330-5cfe-4433-a218-2749844aee89"; //userInfo.id; + 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", @@ -47,9 +55,14 @@ export default function PatientDashboard() { 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}`); } @@ -57,6 +70,41 @@ export default function PatientDashboard() { 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) => { @@ -199,6 +247,68 @@ export default function PatientDashboard() { +
+ + + 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..6eafdde --- /dev/null +++ b/app/doctor/disponibilidade/excecoes/page.tsx @@ -0,0 +1,219 @@ +"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

+

{selectedCalendarDate?.toLocaleDateString("pt-BR", { weekday: "long" })}

+
+
+
+ + +
+
+ + +
+
+ +
+ + +
+
+ + +
+
+
+ +
+ + + + +
+
+ )} +
+
+
+
+ ); +} diff --git a/app/doctor/disponibilidade/page.tsx b/app/doctor/disponibilidade/page.tsx index 55c3cac..4a2beb0 100644 --- a/app/doctor/disponibilidade/page.tsx +++ b/app/doctor/disponibilidade/page.tsx @@ -16,6 +16,7 @@ export default function AvailabilityPage() { 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(() => { @@ -39,8 +40,8 @@ export default function AvailabilityPage() { const formData = new FormData(form); const apiPayload = { - doctor_id: userInfo.id, - created_by: userInfo.id, + 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, @@ -159,7 +160,7 @@ export default function AvailabilityPage() { - Presencial + Presencial Telemedicina @@ -168,7 +169,10 @@ export default function AvailabilityPage() {
- + + + +