From 12aa0e34e1acf8aad3d0199485c1ba495981ec03 Mon Sep 17 00:00:00 2001 From: GagoDuBroca Date: Sun, 23 Nov 2025 21:55:57 -0300 Subject: [PATCH 01/11] 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 02/11] =?UTF-8?q?Remo=C3=A7=C3=A3o=20But=C3=A3o=20Aniversa?= =?UTF-8?q?rio,=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 03/11] =?UTF-8?q?Adi=C3=A7=C3=A3o=20da=20barra=20de=20pesq?= =?UTF-8?q?uisa=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} +
+
+ ))} +
+
+
+
+
-
+
-
+