From b9b49cba42c1802c28231b386e60ee4802a05c9d Mon Sep 17 00:00:00 2001 From: DaniloSts <71015891+StsDanilo@users.noreply.github.com> Date: Tue, 18 Nov 2025 13:40:01 -0300 Subject: [PATCH 1/7] =?UTF-8?q?Update=20p=C3=A1gina=20inicial?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/page.tsx b/app/page.tsx index fcefd3c..81fb7f2 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -27,7 +27,7 @@ export default function InicialPage() { /> {/* 2. NOME DO SITE */} -

MediConnect

+

MedConnect

{/* Botão do menu hambúrguer para telas menores */} From da35ebbff53212e896c417ce5c5c549b83e6bbd0 Mon Sep 17 00:00:00 2001 From: StsDanilo Date: Tue, 18 Nov 2025 15:09:36 -0300 Subject: [PATCH 2/7] =?UTF-8?q?Cria=C3=A7=C3=A3o=20de=20p=C3=A1gina=20de?= =?UTF-8?q?=20disponibilidade=20para=20o=20gestor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/doctor/dashboard/page.tsx | 159 +++++++++-------------- app/manager/disponibilidade/page.tsx | 185 +++++++++++++++++++++++++++ components/Sidebar.tsx | 3 +- components/ui/WeeklyScheduleCard.tsx | 105 +++++++++++++++ 4 files changed, 355 insertions(+), 97 deletions(-) create mode 100644 app/manager/disponibilidade/page.tsx create mode 100644 components/ui/WeeklyScheduleCard.tsx diff --git a/app/doctor/dashboard/page.tsx b/app/doctor/dashboard/page.tsx index 060d8e4..af05f6d 100644 --- a/app/doctor/dashboard/page.tsx +++ b/app/doctor/dashboard/page.tsx @@ -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() { 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

- )} -
-
-
- ); - })} -
+ {loggedDoctor && }
@@ -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() {

{date}

-

- {startTime && endTime - ? `${startTime} - ${endTime}` - : "Dia todo"} -

+

{startTime && endTime ? `${startTime} - ${endTime}` : "Dia todo"}

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

diff --git a/app/manager/disponibilidade/page.tsx b/app/manager/disponibilidade/page.tsx new file mode 100644 index 0000000..adfcd80 --- /dev/null +++ b/app/manager/disponibilidade/page.tsx @@ -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(null); + const [doctors, setDoctors] = useState(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 ( + +
Carregando dados...
+
+ ); + } + + if (!doctors || !availabilities) { + return ( + +
Não foi possível carregar médicos ou disponibilidades.
+
+ ); + } + + return ( + +
+
+

Disponibilidade dos Médicos

+

Visualize a agenda semanal individual de cada médico.

+
+ + + {/* 🔎 Filtros */} +
+ {/* Filtro por nome */} + + { + setSearch(e.target.value); + setPage(1); + }} + className="w-full md:w-1/3" + /> + + {/* Filtro por especialidade */} + +
+
+
+ {/* GRID de cards */} +
+ {paginatedDoctors.map((doctor) => { + const doctorAvailabilities = availabilities.filter((a) => a.doctor_id === doctor.id); + + return ( + + + {doctor.full_name} + + + + + + + ); + })} +
+ + {/* 📄 Paginação */} + {totalPages > 1 && ( +
+ + + + Página {page} de {totalPages} + + + +
+ )} +
+
+ ); +} diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx index 577d49c..c135d91 100644 --- a/components/Sidebar.tsx +++ b/components/Sidebar.tsx @@ -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[]; diff --git a/components/ui/WeeklyScheduleCard.tsx b/components/ui/WeeklyScheduleCard.tsx new file mode 100644 index 0000000..2b33ba0 --- /dev/null +++ b/components/ui/WeeklyScheduleCard.tsx @@ -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>({}); + const [loading, setLoading] = useState(true); + + const weekdaysPT: Record = { + 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 ( +
+ {loading ? ( +

Carregando...

+ ) : ( + ["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

+ )} +
+
+
+ ); + }) + )} +
+ ); +} From 12aa0e34e1acf8aad3d0199485c1ba495981ec03 Mon Sep 17 00:00:00 2001 From: GagoDuBroca Date: Sun, 23 Nov 2025 21:55:57 -0300 Subject: [PATCH 3/7] Barra de Pesquisa --- app/manager/home/page.tsx | 158 ++++++++++++++++++++++------------- app/manager/usuario/page.tsx | 137 ++++++++++++++++++------------ 2 files changed, 182 insertions(+), 113 deletions(-) diff --git a/app/manager/home/page.tsx b/app/manager/home/page.tsx index afa89ed..fe49726 100644 --- a/app/manager/home/page.tsx +++ b/app/manager/home/page.tsx @@ -6,7 +6,8 @@ import { useRouter } from "next/navigation"; import { Button } from "@/components/ui/button" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" -import { Edit, Trash2, Eye, Calendar, Filter, Loader2 } from "lucide-react" +import { Input } from "@/components/ui/input" // <--- 1. Importação adicionada +import { Edit, Trash2, Eye, Calendar, Filter, Loader2, Search } from "lucide-react" // <--- Adicionado ícone Search import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog" import { doctorsService } from "services/doctorsApi.mjs"; @@ -56,6 +57,7 @@ export default function DoctorsPage() { const [doctorToDeleteId, setDoctorToDeleteId] = useState(null); // --- Estados para Filtros --- + const [searchTerm, setSearchTerm] = useState(""); // <--- 2. Novo estado para a busca const [specialtyFilter, setSpecialtyFilter] = useState("all"); const [statusFilter, setStatusFilter] = useState("all"); @@ -129,10 +131,21 @@ export default function DoctorsPage() { return [...new Set(specialties)]; }, [doctors]); + // --- 3. Atualização da Lógica de Filtragem --- const filteredDoctors = doctors.filter((doctor) => { const specialtyMatch = specialtyFilter === "all" || doctor.specialty === specialtyFilter; const statusMatch = statusFilter === "all" || doctor.status === statusFilter; - return specialtyMatch && statusMatch; + + // Lógica da barra de pesquisa + const searchLower = searchTerm.toLowerCase(); + const nameMatch = doctor.full_name?.toLowerCase().includes(searchLower); + const phoneMatch = doctor.phone_mobile?.includes(searchLower); + // Opcional: buscar também por CRM se desejar + const crmMatch = doctor.crm?.toLowerCase().includes(searchLower); + + const searchMatch = searchTerm === "" || nameMatch || phoneMatch || crmMatch; + + return specialtyMatch && statusMatch && searchMatch; }); const totalPages = Math.ceil(filteredDoctors.length / itemsPerPage); @@ -189,55 +202,64 @@ export default function DoctorsPage() {
- {/* Filtros e Itens por Página */} -
-
- Especialidade - + {/* --- Filtros e Barra de Pesquisa Atualizada --- */} +
+ + {/* Barra de Pesquisa (Estilo similar à foto) */} +
+ + setSearchTerm(e.target.value)} + className="pl-10 w-full bg-gray-50 border-gray-200 focus:bg-white transition-colors" + />
-
- Status - + +
+
+ +
+ +
+ +
+ +
+ +
-
- Itens por página - -
-
{/* Tabela de Médicos (Visível em Telas Médias e Maiores) */} @@ -272,10 +294,20 @@ export default function DoctorsPage() { {currentItems.map((doctor) => ( - {doctor.full_name} + + {doctor.full_name} +
{doctor.phone_mobile}
+ {doctor.crm} {doctor.specialty} - {doctor.status || "N/A"} + + + {doctor.status || "N/A"} + + {(doctor.city || doctor.state) ? `${doctor.city || ""}${doctor.city && doctor.state ? '/' : ''}${doctor.state || ""}` @@ -284,7 +316,7 @@ export default function DoctorsPage() { -
Ações
+
Ações
openDetailsDialog(doctor)}> @@ -335,14 +367,26 @@ export default function DoctorsPage() { ) : (
{currentItems.map((doctor) => ( -
+
{doctor.full_name}
+
{doctor.phone_mobile}
{doctor.specialty}
+
+ + {doctor.status || "N/A"} + +
-
Ações
+
openDetailsDialog(doctor)}> @@ -355,10 +399,6 @@ export default function DoctorsPage() { Editar - - - Marcar consulta - openDeleteDialog(doctor.id)}> Excluir @@ -406,7 +446,7 @@ export default function DoctorsPage() {
)} - {/* Dialogs de Exclusão e Detalhes */} + {/* Dialogs (Exclusão e Detalhes) mantidos igual ao original... */} diff --git a/app/manager/usuario/page.tsx b/app/manager/usuario/page.tsx index 9cc8bbc..df0ece3 100644 --- a/app/manager/usuario/page.tsx +++ b/app/manager/usuario/page.tsx @@ -4,7 +4,8 @@ import React, { useEffect, useState, useCallback } from "react"; import Link from "next/link"; import { Button } from "@/components/ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { Plus, Eye, Filter, Loader2 } from "lucide-react"; +import { Input } from "@/components/ui/input"; // <--- 1. Importação Adicionada +import { Plus, Eye, Filter, Loader2, Search } from "lucide-react"; // <--- 1. Ícone Search Adicionado import { AlertDialog, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog"; import { api, login } from "services/api.mjs"; import { usersService } from "services/usersApi.mjs"; @@ -34,6 +35,9 @@ export default function UsersPage() { const [userDetails, setUserDetails] = useState( null ); + + // --- Estados de Filtro --- + const [searchTerm, setSearchTerm] = useState(""); // <--- 2. Estado da busca const [selectedRole, setSelectedRole] = useState("all"); // --- Lógica de Paginação INÍCIO --- @@ -118,10 +122,21 @@ export default function UsersPage() { } }; - const filteredUsers = - selectedRole && selectedRole !== "all" - ? users.filter((u) => u.role === selectedRole) - : users; + // --- 3. Lógica de Filtragem Atualizada --- + const filteredUsers = users.filter((u) => { + // Filtro por Papel (Role) + const roleMatch = selectedRole === "all" || u.role === selectedRole; + + // Filtro da Barra de Pesquisa (Nome, Email ou Telefone) + const searchLower = searchTerm.toLowerCase(); + const nameMatch = u.full_name?.toLowerCase().includes(searchLower); + const emailMatch = u.email?.toLowerCase().includes(searchLower); + const phoneMatch = u.phone?.includes(searchLower); + + const searchMatch = !searchTerm || nameMatch || emailMatch || phoneMatch; + + return roleMatch && searchMatch; + }); const indexOfLastItem = currentPage * itemsPerPage; const indexOfFirstItem = indexOfLastItem - itemsPerPage; @@ -180,60 +195,71 @@ export default function UsersPage() {
- {/* Filtro e Itens por Página */} -
+ {/* --- 4. Filtro (Barra de Pesquisa + Selects) --- */} +
- {/* Select de Filtro por Papel - Ajustado para resetar a página */} -
- - Filtrar por papel - - { + setSearchTerm(e.target.value); + setCurrentPage(1); // Reseta a paginação ao pesquisar }} - value={selectedRole}> - - {/* w-full para mobile, w-[180px] para sm+ */} - - - - Todos - Admin - Gestor - Médico - Secretária - Usuário - - + className="pl-10 w-full bg-gray-50 border-gray-200 focus:bg-white transition-colors" + />
- {/* Select de Itens por Página */} -
- - Itens por página - - +
+ {/* Select de Filtro por Papel */} +
+ +
+ + {/* Select de Itens por Página */} +
+ +
+ +
-
- {/* Fim do Filtro e Itens por Página */} + {/* Fim do Filtro */} {/* Tabela/Lista */}
@@ -299,7 +325,10 @@ export default function UsersPage() {
{u.full_name || "—"}
-
+
+ {u.email} +
+
{u.role || "—"}
From d9f361defbdfa6fd7fff0c3d07102bf186facb53 Mon Sep 17 00:00:00 2001 From: GagoDuBroca Date: Sun, 23 Nov 2025 22:15:50 -0300 Subject: [PATCH 4/7] =?UTF-8?q?Remo=C3=A7=C3=A3o=20But=C3=A3o=20Aniversari?= =?UTF-8?q?o,=20Ajuste=20no=20ver=20detalhes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/secretary/pacientes/page.tsx | 7 +- components/ui/patient-details-modal.tsx | 139 ++++++++++++++++-------- 2 files changed, 97 insertions(+), 49 deletions(-) diff --git a/app/secretary/pacientes/page.tsx b/app/secretary/pacientes/page.tsx index 9990980..1871d10 100644 --- a/app/secretary/pacientes/page.tsx +++ b/app/secretary/pacientes/page.tsx @@ -209,11 +209,8 @@ export default function PacientesPage() {
- {/* Aniversariantes - Ocupa 100% no mobile, e se alinha à direita no md+ */} - + +
{/* --- SEÇÃO DE TABELA (VISÍVEL EM TELAS MAIORES OU IGUAIS A MD) --- */} diff --git a/components/ui/patient-details-modal.tsx b/components/ui/patient-details-modal.tsx index fea1040..8a37050 100644 --- a/components/ui/patient-details-modal.tsx +++ b/components/ui/patient-details-modal.tsx @@ -1,96 +1,147 @@ -'use client' +"use client"; import { - Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, } from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; + +interface Paciente { + id: string; + nome: string; + telefone: string; + cidade: string; + estado: string; + email?: string; + birth_date?: string; + cpf?: string; + blood_type?: string; + weight_kg?: number; + height_m?: number; + street?: string; + number?: string; + complement?: string; + neighborhood?: string; + cep?: string; + [key: string]: any; // Para permitir outras propriedades se necessário +} interface PatientDetailsModalProps { + patient: Paciente | null; isOpen: boolean; - patient: any; onClose: () => void; } -export function PatientDetailsModal({ patient, isOpen, onClose }: PatientDetailsModalProps) { +export function PatientDetailsModal({ + patient, + isOpen, + onClose, +}: PatientDetailsModalProps) { if (!patient) return null; return ( - + - Detalhes do Paciente - Informações detalhadas sobre o paciente. + Detalhes do Paciente + + Informações detalhadas sobre o paciente. + -
-
+ +
+ {/* Grid Principal */} +
-

Nome Completo

-

{patient.nome}

+

Nome Completo

+

{patient.nome}

+ + {/* CORREÇÃO AQUI: Adicionado 'break-all' para quebrar o email */}
-

Email

-

{patient.email}

+

Email

+

{patient.email || "N/A"}

+
-

Telefone

-

{patient.telefone}

+

Telefone

+

{patient.telefone}

+
-

Data de Nascimento

-

{patient.birth_date}

+

Data de Nascimento

+

{patient.birth_date || "N/A"}

+
-

CPF

-

{patient.cpf}

+

CPF

+

{patient.cpf || "N/A"}

+
-

Tipo Sanguíneo

-

{patient.blood_type}

+

Tipo Sanguíneo

+

{patient.blood_type || "N/A"}

+
-

Peso (kg)

-

{patient.weight_kg}

+

Peso (kg)

+

{patient.weight_kg || "0"}

+
-

Altura (m)

-

{patient.height_m}

+

Altura (m)

+

{patient.height_m || "0"}

-
-

Endereço

-
+ +
+ + {/* Seção de Endereço */} +
+

Endereço

+
-

Rua

-

{`${patient.street}, ${patient.number}`}

+

Rua

+

+ {patient.street && patient.street !== "N/A" + ? `${patient.street}, ${patient.number || ""}` + : "N/A"} +

-

Complemento

-

{patient.complement}

+

Complemento

+

{patient.complement || "N/A"}

-

Bairro

-

{patient.neighborhood}

+

Bairro

+

{patient.neighborhood || "N/A"}

-

Cidade

-

{patient.cidade}

+

Cidade

+

{patient.cidade || "N/A"}

-

Estado

-

{patient.estado}

+

Estado

+

{patient.estado || "N/A"}

-

CEP

-

{patient.cep}

+

CEP

+

{patient.cep || "N/A"}

+ - - - +
); -} +} \ No newline at end of file From b9f8efb039639aec9b9a1795b0a0c2a305ba5ace Mon Sep 17 00:00:00 2001 From: GagoDuBroca Date: Sun, 23 Nov 2025 22:31:41 -0300 Subject: [PATCH 5/7] =?UTF-8?q?Adi=C3=A7=C3=A3o=20da=20barra=20de=20pesqui?= =?UTF-8?q?sa=20agenda=20consulta?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/schedule/schedule-form.tsx | 334 +++++++++++++++----------- 1 file changed, 192 insertions(+), 142 deletions(-) diff --git a/components/schedule/schedule-form.tsx b/components/schedule/schedule-form.tsx index 89ffb05..ecdfba5 100644 --- a/components/schedule/schedule-form.tsx +++ b/components/schedule/schedule-form.tsx @@ -13,10 +13,25 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@ import { Textarea } from "@/components/ui/textarea"; import { Calendar as CalendarShadcn } from "@/components/ui/calendar"; import { format, addDays } from "date-fns"; -import { User, StickyNote, Calendar } from "lucide-react"; -import {smsService } from "@/services/Sms.mjs" +import { User, StickyNote, Check, ChevronsUpDown } from "lucide-react"; +import { smsService } from "@/services/Sms.mjs"; import { toast } from "@/hooks/use-toast"; +import { cn } from "@/lib/utils"; +// Componentes do Combobox (Barra de Pesquisa) +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; export default function ScheduleForm() { // Estado do usuário e role @@ -26,8 +41,12 @@ export default function ScheduleForm() { // Listas e seleções const [patients, setPatients] = useState([]); const [selectedPatient, setSelectedPatient] = useState(""); + const [openPatientCombobox, setOpenPatientCombobox] = useState(false); + const [doctors, setDoctors] = useState([]); const [selectedDoctor, setSelectedDoctor] = useState(""); + const [openDoctorCombobox, setOpenDoctorCombobox] = useState(false); // Novo estado para médico + const [selectedDate, setSelectedDate] = useState(""); const [selectedTime, setSelectedTime] = useState(""); const [notes, setNotes] = useState(""); @@ -204,123 +223,86 @@ export default function ScheduleForm() { } }, []); - useEffect(() => { - if (selectedDoctor && selectedDate) fetchAvailableSlots(selectedDoctor, selectedDate); - }, [selectedDoctor, selectedDate, fetchAvailableSlots]); - // 🔹 Submeter agendamento - // 🔹 Submeter agendamento - // 🔹 Submeter agendamento -// 🔹 Submeter agendamento -// 🔹 Submeter agendamento -const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); - const isSecretaryLike = ["secretaria", "admin", "gestor"].includes(role); - const patientId = isSecretaryLike ? selectedPatient : userId; + const isSecretaryLike = ["secretaria", "admin", "gestor"].includes(role); + const patientId = isSecretaryLike ? selectedPatient : userId; - if (!patientId || !selectedDoctor || !selectedDate || !selectedTime) { - toast({ title: "Campos obrigatórios", description: "Preencha todos os campos." }); - return; - } + if (!patientId || !selectedDoctor || !selectedDate || !selectedTime) { + toast({ title: "Campos obrigatórios", description: "Preencha todos os campos." }); + return; + } - try { - const body = { - doctor_id: selectedDoctor, - patient_id: patientId, - scheduled_at: `${selectedDate}T${selectedTime}:00`, - duration_minutes: Number(duracao), - notes, - appointment_type: tipoConsulta, - }; + try { + const body = { + doctor_id: selectedDoctor, + patient_id: patientId, + scheduled_at: `${selectedDate}T${selectedTime}:00`, + duration_minutes: Number(duracao), + notes, + appointment_type: tipoConsulta, + }; - // ✅ mantém o fluxo original de criação (funcional) - await appointmentsService.create(body); + await appointmentsService.create(body); - const dateFormatted = selectedDate.split("-").reverse().join("/"); + const dateFormatted = selectedDate.split("-").reverse().join("/"); - toast({ - title: "Consulta agendada!", - description: `Consulta marcada para ${dateFormatted} às ${selectedTime} com o(a) médico(a) ${ - doctors.find((d) => d.id === selectedDoctor)?.full_name || "" - }.`, - }); + toast({ + title: "Consulta agendada!", + description: `Consulta marcada para ${dateFormatted} às ${selectedTime} com o(a) médico(a) ${ + doctors.find((d) => d.id === selectedDoctor)?.full_name || "" + }.`, + }); -let phoneNumber = "+5511999999999"; // fallback - -try { - if (isSecretaryLike) { - // Secretária/admin → telefone do paciente selecionado - const patient = patients.find((p: any) => p.id === patientId); - - // Pacientes criados no sistema podem ter phone ou phone_mobile - const rawPhone = patient?.phone || patient?.phone_mobile || null; - - if (rawPhone) phoneNumber = rawPhone; - } else { - // Paciente → telefone vem do perfil do próprio usuário logado - const me = await usersService.getMe(); - - -const rawPhone = - me?.profile?.phone || - (typeof me?.profile === "object" && "phone_mobile" in me.profile ? (me.profile as any).phone_mobile : null) || - (typeof me === "object" && "user_metadata" in me ? (me as any).user_metadata?.phone : null) || - null; - - if (rawPhone) phoneNumber = rawPhone; - } - - // 🔹 Normaliza para formato internacional (+55) - if (phoneNumber) { - phoneNumber = phoneNumber.replace(/\D/g, ""); - if (!phoneNumber.startsWith("55")) phoneNumber = `55${phoneNumber}`; - phoneNumber = `+${phoneNumber}`; - } - - console.log("📞 Telefone usado:", phoneNumber); -} catch (err) { - console.warn("⚠️ Não foi possível obter telefone do paciente:", err); -} - - - // 💬 envia o SMS de confirmação - // 💬 Envia o SMS de lembrete (sem mostrar nada ao paciente) -// 💬 Envia o SMS de lembrete (somente loga no console, não mostra no sistema) -try { - const smsRes = await smsService.sendSms({ - phone_number: phoneNumber, - message: `Lembrete: sua consulta é em ${dateFormatted} às ${selectedTime} na Clínica MediConnect.`, - patient_id: patientId, - }); - - if (smsRes?.success) { - console.log("✅ SMS enviado com sucesso:", smsRes.message_sid); - } else { - console.warn("⚠️ Falha no envio do SMS:", smsRes); - } -} catch (smsErr) { - console.error("❌ Erro ao enviar SMS:", smsErr); -} - - - - - // 🧹 limpa os campos - setSelectedDoctor(""); - setSelectedDate(""); - setSelectedTime(""); - setNotes(""); - setSelectedPatient(""); - } catch (err) { - console.error("❌ Erro ao agendar consulta:", err); - toast({ title: "Erro", description: "Falha ao agendar consulta." }); - } -}; + let phoneNumber = "+5511999999999"; + try { + if (isSecretaryLike) { + const patient = patients.find((p: any) => p.id === patientId); + const rawPhone = patient?.phone || patient?.phone_mobile || null; + if (rawPhone) phoneNumber = rawPhone; + } else { + const me = await usersService.getMe(); + const rawPhone = + me?.profile?.phone || + (typeof me?.profile === "object" && "phone_mobile" in me.profile ? (me.profile as any).phone_mobile : null) || + (typeof me === "object" && "user_metadata" in me ? (me as any).user_metadata?.phone : null) || + null; + if (rawPhone) phoneNumber = rawPhone; + } + if (phoneNumber) { + phoneNumber = phoneNumber.replace(/\D/g, ""); + if (!phoneNumber.startsWith("55")) phoneNumber = `55${phoneNumber}`; + phoneNumber = `+${phoneNumber}`; + } + } catch (err) { + console.warn("⚠️ Não foi possível obter telefone do paciente:", err); + } + try { + const smsRes = await smsService.sendSms({ + phone_number: phoneNumber, + message: `Lembrete: sua consulta é em ${dateFormatted} às ${selectedTime} na Clínica MediConnect.`, + patient_id: patientId, + }); + if (smsRes?.success) console.log("✅ SMS enviado:", smsRes.message_sid); + } catch (smsErr) { + console.error("❌ Erro ao enviar SMS:", smsErr); + } + setSelectedDoctor(""); + setSelectedDate(""); + setSelectedTime(""); + setNotes(""); + setSelectedPatient(""); + } catch (err) { + console.error("❌ Erro ao agendar consulta:", err); + toast({ title: "Erro", description: "Falha ao agendar consulta." }); + } + }; // 🔹 Tooltip no calendário useEffect(() => { @@ -360,45 +342,113 @@ try {
-
- {/* Se secretária/gestor/admin → mostrar campo Paciente */} +
{/* Ajuste: maior espaçamento vertical geral */} + + {/* Se secretária/gestor/admin → COMBOBOX de Paciente */} {["secretaria", "gestor", "admin"].includes(role) && ( -
+
{/* Ajuste: gap entre Label e Input */} - + + + + + + + + + Nenhum paciente encontrado. + + {patients.map((patient) => ( + { + setSelectedPatient(patient.id === selectedPatient ? "" : patient.id); + setOpenPatientCombobox(false); + }} + > + + {patient.full_name} + + ))} + + + + +
)} -
+ {/* COMBOBOX de Médico (Nova funcionalidade) */} +
{/* Ajuste: gap entre Label e Input */} - + + + + + + + + + Nenhum médico encontrado. + + {doctors.map((doctor) => ( + { + setSelectedDoctor(doctor.id === selectedDoctor ? "" : doctor.id); + setOpenDoctorCombobox(false); + }} + > + +
+ {doctor.full_name} + {doctor.specialty} +
+
+ ))} +
+
+
+
+
-
+
-
+