From c4ca03cf48a71ee1e6629a57d67eb51f25103725 Mon Sep 17 00:00:00 2001 From: Lucas Rodrigues Date: Fri, 10 Oct 2025 14:59:42 -0300 Subject: [PATCH 1/5] Adionando os endpoints de users --- app/manager/usuario/novo/page.tsx | 66 +++-- app/manager/usuario/page.tsx | 422 +++++++++++++++++------------- components/LoginForm.tsx | 6 +- services/usersApi.mjs | 8 +- 4 files changed, 274 insertions(+), 228 deletions(-) diff --git a/app/manager/usuario/novo/page.tsx b/app/manager/usuario/novo/page.tsx index 781f35d..1eb7b28 100644 --- a/app/manager/usuario/novo/page.tsx +++ b/app/manager/usuario/novo/page.tsx @@ -3,54 +3,51 @@ import { useState } from "react" import { useRouter } from "next/navigation" 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 { 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 { Save, Loader2 } from "lucide-react" -import ManagerLayout from "@/components/manager-layout" +import ManagerLayout from "components/manager-layout" +import { usersService } from "services/usersApi.mjs"; -// Mock user service for demonstration. Replace with your actual API service. -const usersService = { - create: async (payload: any) => { - console.log("API Call: Creating user with payload:", payload); - // Simulate network delay - await new Promise(resolve => setTimeout(resolve, 1000)); - // Simulate a success response - return { id: Date.now(), ...payload }; - // To simulate an error, you could throw an error here: - // throw new Error("O e-mail informado já está em uso."); - } -}; -// Define the structure for our form data interface UserFormData { email: string; password: string; nomeCompleto: string; telefone: string; - papel: string; // e.g., 'admin', 'gestor', 'medico', etc. + cargo: string; } -// Define the initial state for the form + const defaultFormData: UserFormData = { email: '', password: '', nomeCompleto: '', telefone: '', - papel: '', + cargo: '', }; -// Helper function to remove non-digit characters const cleanNumber = (value: string): string => value.replace(/\D/g, ''); -// Helper function to format a phone number + + const formatPhone = (value: string): string => { - const cleaned = cleanNumber(value).substring(0, 11); - if (cleaned.length > 10) { - return cleaned.replace(/(\d{2})(\d{5})(\d{4})/, '($1) $2-$3'); - } - return cleaned.replace(/(\d{2})(\d{4})(\d{4})/, '($1) $2-$3'); + const cleaned = cleanNumber(value).substring(0, 11); + + + if (cleaned.length === 11) { + return cleaned.replace(/(\d{2})(\d{5})(\d{4})/, '($1) $2-$3'); + } + + + if (cleaned.length === 10) { + return cleaned.replace(/(\d{2})(\d{4})(\d{4})/, '($1) $2-$3'); + } + + + return cleaned; }; export default function NovoUsuarioPage() { @@ -59,7 +56,7 @@ export default function NovoUsuarioPage() { const [isSaving, setIsSaving] = useState(false); const [error, setError] = useState(null); - // Handles changes in form inputs + const handleInputChange = (key: keyof UserFormData, value: string) => { const updatedValue = key === 'telefone' ? formatPhone(value) : value; setFormData((prev) => ({ ...prev, [key]: updatedValue })); @@ -71,7 +68,7 @@ export default function NovoUsuarioPage() { setError(null); // Basic validation - if (!formData.email || !formData.password || !formData.nomeCompleto || !formData.papel) { + if (!formData.email || !formData.password || !formData.nomeCompleto || !formData.cargo) { setError("Por favor, preencha todos os campos obrigatórios."); return; } @@ -83,13 +80,12 @@ export default function NovoUsuarioPage() { email: formData.email, password: formData.password, full_name: formData.nomeCompleto, - phone: formData.telefone.trim() || null, // Send null if empty - role: formData.papel, + phone: formData.telefone.trim() || null, + role: formData.cargo, }; try { - await usersService.create(payload); - // On success, redirect to the main user list page + await usersService.create_user(payload); router.push("/manager/usuario"); } catch (e: any) { console.error("Erro ao criar usuário:", e); @@ -174,7 +170,7 @@ export default function NovoUsuarioPage() {
- handleInputChange("cargo", v)} required> diff --git a/app/manager/usuario/page.tsx b/app/manager/usuario/page.tsx index 00c4836..12a4cc4 100644 --- a/app/manager/usuario/page.tsx +++ b/app/manager/usuario/page.tsx @@ -1,12 +1,12 @@ "use client"; -import React, { useEffect, useState, useCallback } from "react" +import React, { useEffect, useState, useCallback } from "react"; import ManagerLayout from "@/components/manager-layout"; -import Link from "next/link" +import Link from "next/link"; import { useRouter } from "next/navigation"; -import { Button } from "@/components/ui/button" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" -import { Plus, Edit, Trash2, Eye, Filter, Loader2 } from "lucide-react" +import { Button } from "@/components/ui/button"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Plus, Edit, Trash2, Eye, Filter, Loader2 } from "lucide-react"; import { AlertDialog, AlertDialogAction, @@ -16,57 +16,79 @@ import { AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, -} from "@/components/ui/alert-dialog" +} from "@/components/ui/alert-dialog"; -// Mock user service for demonstration. Replace with your actual API service. -const usersService = { - list: async (): Promise => { - console.log("API Call: Fetching users..."); - await new Promise(resolve => setTimeout(resolve, 500)); // Simulate network delay - return [ - { id: 1, full_name: 'Alice Admin', email: 'alice.admin@example.com', phone: '(11) 98765-4321', role: 'user' }, +import { usersService } from "services/usersApi.mjs"; - ]; - }, - delete: async (id: number): Promise => { - console.log(`API Call: Deleting user with ID ${id}`); - await new Promise(resolve => setTimeout(resolve, 700)); - // In a real app, you'd handle potential errors here - } -}; - -// Interface for a User object interface User { - id: number; - full_name: string; + user: { + id: string; email: string; - phone: string | null; - role: 'admin' | 'gestor' | 'medico' | 'secretaria' | 'user'; + email_confirmed_at?: string; + created_at?: string; + last_sign_in_at?: string; + }; + profile: { + id?: string; + full_name?: string; + email?: string; + phone?: string | null; + avatar_url?: string; + disabled?: boolean; + created_at?: string; + updated_at?: string; + }; + roles: string[]; + permissions: { + isAdmin?: boolean; + isManager?: boolean; + isDoctor?: boolean; + isSecretary?: boolean; + isAdminOrManager?: boolean; + [key: string]: boolean | undefined; + }; +} + + +interface FlatUser { + id: string; + full_name?: string; + email: string; + phone?: string | null; + role: string; } -// Interface for User Details (can be the same as User for this case) -interface UserDetails extends User {} export default function UsersPage() { const router = useRouter(); - const [users, setUsers] = useState([]); + + const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [detailsDialogOpen, setDetailsDialogOpen] = useState(false); - const [userDetails, setUserDetails] = useState(null); + const [userDetails, setUserDetails] = useState(null); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [userToDeleteId, setUserToDeleteId] = useState(null); - + const [selectedRole, setSelectedRole] = useState(""); + const fetchUsers = useCallback(async () => { setLoading(true); setError(null); try { - const data: User[] = await usersService.list(); - setUsers(data || []); - } catch (e: any) { - console.error("Erro ao carregar lista de usuários:", e); - setError("Não foi possível carregar a lista de usuários. Tente novamente."); + + const { data, error } = await usersService.list_roles(); + if (error) throw error; + + if (Array.isArray(data)) { + setUsers(data as FlatUser[]); + } else { + console.warn("Formato inesperado recebido:", data); + setUsers([]); + } + } catch (err: any) { + console.error("Erro ao buscar usuários:", err); + setError("Não foi possível carregar os usuários. Tente novamente."); setUsers([]); } finally { setLoading(false); @@ -77,169 +99,197 @@ export default function UsersPage() { fetchUsers(); }, [fetchUsers]); - const openDetailsDialog = (user: User) => { - setUserDetails(user); + + const openDetailsDialog = async (flatUser: FlatUser) => { setDetailsDialogOpen(true); - }; - - const handleDelete = async () => { - if (userToDeleteId === null) return; - - setLoading(true); + setUserDetails(null); + try { - await usersService.delete(userToDeleteId); - console.log(`Usuário com ID ${userToDeleteId} excluído com sucesso!`); - setDeleteDialogOpen(false); - setUserToDeleteId(null); - await fetchUsers(); // Refresh the list after deletion - } catch (e) { - console.error("Erro ao excluir:", e); - alert("Erro ao excluir usuário."); - } finally { - setLoading(false); + + const fullUserData: User = await usersService.full_data(flatUser.id); + + + setUserDetails(fullUserData); + + } catch (err: any) { + console.error("Erro ao buscar detalhes do usuário:", err); + + setUserDetails({ + user: { + id: flatUser.id, + email: flatUser.email || "", + created_at: "Erro ao Carregar", + last_sign_in_at: "Erro ao Carregar", + }, + profile: { + full_name: "Erro ao Carregar Detalhes", + phone: "—", + }, + roles: [], + permissions: {}, + } as any); } }; - const openDeleteDialog = (userId: number) => { - setUserToDeleteId(userId); - setDeleteDialogOpen(true); - }; - - const handleEdit = (userId: number) => { - // Assuming the edit page is at a similar path - router.push(`/manager/usuario/${userId}/editar`); - }; + + + const filteredUsers = selectedRole + ? users.filter((u) => u.role === selectedRole) + : users; return ( -
-
-
-

Usuários Cadastrados

-

Gerencie todos os usuários do sistema.

-
- - - -
- - {/* Filters Section */} -
- - -
- - {/* Users Table */} -
- {loading ? ( -
- - Carregando usuários... -
- ) : error ? ( -
- {error} -
- ) : users.length === 0 ? ( -
- Nenhum usuário cadastrado. Adicione um novo. -
- ) : ( -
- - - - - - - - - - - - {users.map((user) => ( - - - - - - - - ))} - -
Nome CompletoE-mailTelefonePapelAções
{user.full_name}{user.email}{user.phone || "N/A"}{user.role} -
- - - -
-
+
+
+
+

Usuários Cadastrados

+

Gerencie todos os usuários e seus papéis no sistema.

- )} -
- - {/* Delete Confirmation Dialog */} - - - - Confirma a exclusão? - - Esta ação é irreversível e excluirá permanentemente o registro deste usuário. - - - - Cancelar - - {loading ? : null} - Excluir - - - - + + + +
- {/* User Details Dialog */} - - - - {userDetails?.full_name} - - {userDetails && ( + +
+ + +
+ + +
+ {loading ? ( +
+ + Carregando usuários... +
+ ) : error ? ( +
{error}
+ ) : filteredUsers.length === 0 ? ( +
+ Nenhum usuário encontrado.{" "} + + Adicione um novo + + . +
+ ) : ( +
+ + + + + + + + + + + + + {filteredUsers.map((user) => ( + + + + + + + + + + ))} + +
IDNomeE-mailTelefonePapelAções
{user.id}{user.full_name || "—"}{user.email || "—"}{user.phone || "—"}{user.role || "—"} +
+ +
+
+
+ )} +
+ + + + + + + {userDetails?.profile?.full_name || userDetails?.user?.email || "Detalhes do Usuário"} + + + + {!userDetails ? ( +
+ + Buscando dados completos... +
+ ) : (
-
-
E-mail: {userDetails.email}
-
Telefone: {userDetails.phone || 'Não informado'}
-
Papel: {userDetails.role}
+ +
ID: {userDetails.user.id}
+
E-mail: {userDetails.user.email}
+
Email confirmado em: {userDetails.user.email_confirmed_at || "—"}
+
Último login: {userDetails.user.last_sign_in_at || "—"}
+
Criado em: {userDetails.user.created_at || "—"}
+ + +
Nome completo: {userDetails.profile.full_name || "—"}
+
Telefone: {userDetails.profile.phone || "—"}
+ {userDetails.profile.avatar_url && ( +
Avatar:
+ )} +
Conta desativada: {userDetails.profile.disabled ? "Sim" : "Não"}
+
Profile criado em: {userDetails.profile.created_at || "—"}
+
Profile atualizado em: {userDetails.profile.updated_at || "—"}
+ + +
+ Roles: +
    + {userDetails.roles.map((role, idx) =>
  • {role}
  • )} +
+
+ +
+ Permissões: +
    + {Object.entries(userDetails.permissions).map(([key, value]) => ( +
  • {key}: {value ? "Sim" : "Não"}
  • + ))} +
)} - - - - Fechar - - - -
+ + +
+
+ + Fechar + +
+
+
); } \ No newline at end of file diff --git a/components/LoginForm.tsx b/components/LoginForm.tsx index 71a6ab4..77037a7 100644 --- a/components/LoginForm.tsx +++ b/components/LoginForm.tsx @@ -9,7 +9,7 @@ import Cookies from "js-cookie"; import { jwtDecode } from "jwt-decode"; import { cn } from "@/lib/utils"; -// Componentes Shadcn UI + import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -17,10 +17,10 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com import { Separator } from "@/components/ui/separator"; import { apikey } from "@/services/api.mjs"; -// Hook customizado + import { useToast } from "@/hooks/use-toast"; -// Ícones + import { Eye, EyeOff, Mail, Lock, Loader2, UserCheck, Stethoscope, IdCard, Receipt } from "lucide-react"; interface LoginFormProps { diff --git a/services/usersApi.mjs b/services/usersApi.mjs index d4aa18f..f37bc14 100644 --- a/services/usersApi.mjs +++ b/services/usersApi.mjs @@ -1,8 +1,8 @@ import { api } from "./api.mjs"; export const usersService = { - create_user: (data) => api.post('/functions/v1/create-user'), - list_roles: () => api.get("/rest/v1/user_roles"), - full_data: () => api.get(`/functions/v1/user-info`), - summary_data: () => api.get('/auth/v1/user') + create_user: (data) => api.post(`/functions/v1/create-user`), + list_roles: () => api.get(`/rest/v1/user_roles`), + full_data: (id) => api.get(`/functions/v1/user-info?user_id=${id}`), + summary_data: () => api.get(`/auth/v1/user`) } \ No newline at end of file From 612a70ee902b589c8dc98f8567be2c00eb4bfca6 Mon Sep 17 00:00:00 2001 From: Lucas Rodrigues Date: Fri, 10 Oct 2025 20:04:17 -0300 Subject: [PATCH 2/5] criando users --- components/doctor-layout.tsx | 231 ++++++++++++++++++---------------- components/manager-layout.tsx | 66 ++++++---- 2 files changed, 170 insertions(+), 127 deletions(-) diff --git a/components/doctor-layout.tsx b/components/doctor-layout.tsx index 34ab89e..27d5cdc 100644 --- a/components/doctor-layout.tsx +++ b/components/doctor-layout.tsx @@ -1,10 +1,11 @@ "use client"; import type React from "react"; - import { useState, useEffect } from "react"; import { useRouter, usePathname } from "next/navigation"; import Link from "next/link"; +import Cookies from "js-cookie"; // <-- 1. IMPORTAÇÃO ADICIONADA + import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; @@ -24,43 +25,63 @@ interface DoctorData { permissions: object; } -interface DoctorLayoutProps { +interface PatientLayoutProps { children: React.ReactNode; } -export default function DoctorLayout({ children }: DoctorLayoutProps) { +export default function DoctorLayout({ children }: PatientLayoutProps) { const [doctorData, setDoctorData] = useState(null); const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const [showLogoutDialog, setShowLogoutDialog] = useState(false); - const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); // Novo estado para menu mobile + const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const [windowWidth, setWindowWidth] = useState(0); - const isMobile = windowWidth < 1024; // breakpoint lg + const isMobile = windowWidth < 1024; const router = useRouter(); const pathname = usePathname(); + // ================================================================== + // 2. BLOCO DE SEGURANÇA CORRIGIDO + // ================================================================== useEffect(() => { - const data = localStorage.getItem("doctorData"); - if (data) { - setDoctorData(JSON.parse(data)); + const userInfoString = localStorage.getItem("user_info"); + const token = Cookies.get("access_token"); + + if (userInfoString && token) { + const userInfo = JSON.parse(userInfoString); + + // 3. "TRADUZIMOS" os dados da API para o formato que o layout espera + setDoctorData({ + id: userInfo.id || "", + name: userInfo.user_metadata?.full_name || "Doutor(a)", + email: userInfo.email || "", + specialty: userInfo.user_metadata?.specialty || "Especialidade", + // Campos que não vêm do login, definidos como vazios para não quebrar + phone: userInfo.phone || "", + cpf: "", + crm: "", + department: "", + permissions: {}, + }); } else { + // Se faltar o token ou os dados, volta para o login router.push("/doctor/login"); } }, [router]); useEffect(() => { - const handleResize = () => setWindowWidth(window.innerWidth); - handleResize(); // inicializa com a largura atual - window.addEventListener("resize", handleResize); - return () => window.removeEventListener("resize", handleResize); - }, []); + const handleResize = () => setWindowWidth(window.innerWidth); + handleResize(); // inicializa com a largura atual + window.addEventListener("resize", handleResize); + return () => window.removeEventListener("resize", handleResize); +}, []); - useEffect(() => { - if (isMobile) { - setSidebarCollapsed(true); - } else { - setSidebarCollapsed(false); - } - }, [isMobile]); +useEffect(() => { + if (isMobile) { + setSidebarCollapsed(true); + } else { + setSidebarCollapsed(false); + } +}, [isMobile]); const handleLogout = () => { setShowLogoutDialog(true); @@ -82,7 +103,7 @@ export default function DoctorLayout({ children }: DoctorLayoutProps) { const menuItems = [ { - href: "/doctor/dashboard", + href: "#", icon: Home, label: "Dashboard", // Botão para o dashboard do médico @@ -112,17 +133,17 @@ export default function DoctorLayout({ children }: DoctorLayoutProps) { } return ( -
+
{/* Sidebar para desktop */} -
-
+
+
{!sidebarCollapsed && (
-
-
+
+
- MidConnecta + MidConnecta
)} -
+ MedConnect +
+ )} +
+
- -
-
- {/* Se a sidebar estiver recolhida, o avatar e o texto do usuário também devem ser condensados ou ocultados */} - {!sidebarCollapsed && ( - <> - - - - {doctorData.name - .split(" ") - .map((n) => n[0]) - .join("")} - - -
-

{doctorData.name}

-

{doctorData.specialty}

-
- - )} - {sidebarCollapsed && ( - {/* Centraliza o avatar quando recolhido */} +
+
+ {/* Se a sidebar estiver recolhida, o avatar e o texto do usuário também devem ser condensados ou ocultados */} + {!sidebarCollapsed && ( + <> + {doctorData.name @@ -213,33 +218,49 @@ export default function DoctorLayout({ children }: DoctorLayoutProps) { .join("")} - )} -
+
+

{doctorData.name}

+

{doctorData.specialty}

+
+ + )} + {sidebarCollapsed && ( + {/* Centraliza o avatar quando recolhido */} + + + {doctorData.name + .split(" ") + .map((n) => n[0]) + .join("")} + + + )} +
- {/* Novo botão de sair, usando a mesma estrutura dos itens de menu */} -
- - {!sidebarCollapsed && Sair} -
+ {/* Novo botão de sair, usando a mesma estrutura dos itens de menu */} +
+ + {!sidebarCollapsed && Sair}
- +
+
{/* Sidebar para mobile (apresentado como um menu overlay) */} {isMobileMenuOpen && ( -
+
)} -
-
+
+
-
-
+
+
- Hospital System + Hospital System
@@ -331,4 +352,4 @@ export default function DoctorLayout({ children }: DoctorLayoutProps) {
); -} +} \ No newline at end of file diff --git a/components/manager-layout.tsx b/components/manager-layout.tsx index 9f94be4..47ff1d2 100644 --- a/components/manager-layout.tsx +++ b/components/manager-layout.tsx @@ -4,6 +4,8 @@ import type React from "react"; import { useState, useEffect } from "react"; import { useRouter, usePathname } from "next/navigation"; import Link from "next/link"; +import Cookies from "js-cookie"; // <-- 1. IMPORTAÇÃO ADICIONADA + import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; @@ -37,7 +39,7 @@ interface ManagerData { permissions: object; } -interface ManagerLayoutProps { +interface ManagerLayoutProps { // Corrigi o nome da prop aqui children: React.ReactNode; } @@ -48,15 +50,34 @@ export default function ManagerLayout({ children }: ManagerLayoutProps) { const router = useRouter(); const pathname = usePathname(); + // ================================================================== + // 2. BLOCO DE SEGURANÇA CORRIGIDO + // ================================================================== useEffect(() => { - const data = localStorage.getItem("managerData"); - if (data) { - setManagerData(JSON.parse(data)); + const userInfoString = localStorage.getItem("user_info"); + const token = Cookies.get("access_token"); + + if (userInfoString && token) { + const userInfo = JSON.parse(userInfoString); + + // 3. "TRADUZIMOS" os dados da API para o formato que o layout espera + setManagerData({ + id: userInfo.id || "", + name: userInfo.user_metadata?.full_name || "Gestor(a)", + email: userInfo.email || "", + department: userInfo.user_metadata?.role || "Gestão", + // Campos que não vêm do login, definidos como vazios para não quebrar + phone: userInfo.phone || "", + cpf: "", + permissions: {}, + }); } else { + // Se faltar o token ou os dados, volta para o login router.push("/manager/login"); } }, [router]); + // 🔥 Responsividade automática da sidebar useEffect(() => { const handleResize = () => { @@ -84,9 +105,9 @@ export default function ManagerLayout({ children }: ManagerLayoutProps) { const cancelLogout = () => setShowLogoutDialog(false); const menuItems = [ - { href: "/manager/dashboard", icon: Home, label: "Dashboard" }, + { href: "/manager/dashboard/", icon: Home, label: "Dashboard" }, { href: "#", icon: Calendar, label: "Relatórios gerenciais" }, - { href: "/manager/usuario", icon: User, label: "Gestão de Usuários" }, + { href: "/manager/usuario/", icon: User, label: "Gestão de Usuários" }, { href: "/manager/home", icon: User, label: "Gestão de Médicos" }, { href: "#", icon: Calendar, label: "Configurações" }, ]; @@ -96,20 +117,20 @@ export default function ManagerLayout({ children }: ManagerLayoutProps) { } return ( -
+
{/* Sidebar */}
{/* Logo + collapse button */} -
+
{!sidebarCollapsed && (
-
-
+
+
- + MidConnecta
@@ -139,10 +160,11 @@ export default function ManagerLayout({ children }: ManagerLayoutProps) { return (
{!sidebarCollapsed && ( @@ -168,10 +190,10 @@ export default function ManagerLayout({ children }: ManagerLayoutProps) { {!sidebarCollapsed && (
-

+

{managerData.name}

-

+

{managerData.department}

@@ -204,14 +226,14 @@ export default function ManagerLayout({ children }: ManagerLayoutProps) { ${sidebarCollapsed ? "ml-16" : "ml-64"}`} > {/* Header */} -
+
{/* Search */}
- +
@@ -220,7 +242,7 @@ export default function ManagerLayout({ children }: ManagerLayoutProps) {
From 1b477c10f00a4a8d04e486708f4aa7fabdf53730 Mon Sep 17 00:00:00 2001 From: Lucas Rodrigues Date: Fri, 10 Oct 2025 20:04:48 -0300 Subject: [PATCH 3/5] criando users --- app/manager/usuario/novo/page.tsx | 387 ++++++++++++++++-------------- services/usersApi.mjs | 19 +- 2 files changed, 221 insertions(+), 185 deletions(-) diff --git a/app/manager/usuario/novo/page.tsx b/app/manager/usuario/novo/page.tsx index 1eb7b28..e9f50e5 100644 --- a/app/manager/usuario/novo/page.tsx +++ b/app/manager/usuario/novo/page.tsx @@ -3,208 +3,229 @@ import { useState } from "react" import { useRouter } from "next/navigation" 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 { Save, Loader2 } from "lucide-react" -import ManagerLayout from "components/manager-layout" +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 { Save, Loader2 } from "lucide-react" +import ManagerLayout from "@/components/manager-layout" import { usersService } from "services/usersApi.mjs"; - interface UserFormData { - email: string; - password: string; - nomeCompleto: string; - telefone: string; - cargo: string; + email: string; + password: string; + nomeCompleto: string; + telefone: string; + papel: string; } - const defaultFormData: UserFormData = { - email: '', - password: '', - nomeCompleto: '', - telefone: '', - cargo: '', + email: '', + password: '', + nomeCompleto: '', + telefone: '', + papel: '', }; +// Remove todos os caracteres não numéricos const cleanNumber = (value: string): string => value.replace(/\D/g, ''); +// Definição do requisito mínimo de senha +const MIN_PASSWORD_LENGTH = 8; const formatPhone = (value: string): string => { - const cleaned = cleanNumber(value).substring(0, 11); - - - if (cleaned.length === 11) { - return cleaned.replace(/(\d{2})(\d{5})(\d{4})/, '($1) $2-$3'); - } - - - if (cleaned.length === 10) { - return cleaned.replace(/(\d{2})(\d{4})(\d{4})/, '($1) $2-$3'); - } - - - return cleaned; + const cleaned = cleanNumber(value).substring(0, 11); + + if (cleaned.length === 11) { + return cleaned.replace(/(\d{2})(\d{5})(\d{4})/, '($1) $2-$3'); + } + + if (cleaned.length === 10) { + return cleaned.replace(/(\d{2})(\d{4})(\d{4})/, '($1) $2-$3'); + } + return cleaned; }; + export default function NovoUsuarioPage() { - const router = useRouter(); - const [formData, setFormData] = useState(defaultFormData); - const [isSaving, setIsSaving] = useState(false); - const [error, setError] = useState(null); + const router = useRouter(); + const [formData, setFormData] = useState(defaultFormData); + const [isSaving, setIsSaving] = useState(false); + const [error, setError] = useState(null); - - const handleInputChange = (key: keyof UserFormData, value: string) => { - const updatedValue = key === 'telefone' ? formatPhone(value) : value; - setFormData((prev) => ({ ...prev, [key]: updatedValue })); - }; - - // Handles form submission - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setError(null); - - // Basic validation - if (!formData.email || !formData.password || !formData.nomeCompleto || !formData.cargo) { - setError("Por favor, preencha todos os campos obrigatórios."); - return; - } - - setIsSaving(true); - - // Prepare payload for the API - const payload = { - email: formData.email, - password: formData.password, - full_name: formData.nomeCompleto, - phone: formData.telefone.trim() || null, - role: formData.cargo, + const handleInputChange = (key: keyof UserFormData, value: string) => { + const updatedValue = key === 'telefone' ? formatPhone(value) : value; + setFormData((prev) => ({ ...prev, [key]: updatedValue })); }; - try { - await usersService.create_user(payload); - router.push("/manager/usuario"); - } catch (e: any) { - console.error("Erro ao criar usuário:", e); - setError(e.message || "Ocorreu um erro inesperado. Tente novamente."); - } finally { - setIsSaving(false); - } - }; + // Handles form submission + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(null); - return ( - -
-
-
-

Novo Usuário

-

- Preencha os dados para cadastrar um novo usuário no sistema. -

-
- - - -
+ // Basic validation + if (!formData.email || !formData.password || !formData.nomeCompleto || !formData.papel) { + setError("Por favor, preencha todos os campos obrigatórios."); + return; + } -
- - {/* Error Message Display */} - {error && ( -
-

Erro no Cadastro:

-

{error}

-
- )} - -
-
- - handleInputChange("nomeCompleto", e.target.value)} - placeholder="Nome e Sobrenome" - required - /> + // Validação de comprimento mínimo da senha + if (formData.password.length < MIN_PASSWORD_LENGTH) { + setError(`A senha deve ter no mínimo ${MIN_PASSWORD_LENGTH} caracteres.`); + return; + } + + setIsSaving(true); + + // ---------------------------------------------------------------------- + // CORREÇÃO FINAL: Usa o formato de telefone que o mock API comprovadamente aceitou. + // ---------------------------------------------------------------------- + const phoneValue = formData.telefone.trim(); + + // Prepara o payload com os campos obrigatórios + const payload: any = { + email: formData.email, + password: formData.password, + full_name: formData.nomeCompleto, + role: formData.papel, + }; + + // Adiciona o telefone APENAS se estiver preenchido, enviando o formato FORMATADO. + if (phoneValue.length > 0) { + payload.phone = phoneValue; + } + // ---------------------------------------------------------------------- + + try { + await usersService.create_user(payload); + router.push("/manager/usuario"); + } catch (e: any) { + console.error("Erro ao criar usuário:", e); + // Melhorando a mensagem de erro para o usuário final + const apiErrorMsg = e.message?.includes("500") + ? "Erro interno do servidor. Verifique os logs do backend ou tente novamente mais tarde. (Possível problema: E-mail já em uso ou falha de conexão.)" + : e.message || "Ocorreu um erro inesperado. Tente novamente."; + + setError(apiErrorMsg); + } finally { + setIsSaving(false); + } + }; + + return ( + +
+
+
+

Novo Usuário

+

+ Preencha os dados para cadastrar um novo usuário no sistema. +

+
+ + + +
+ + + + {/* Error Message Display */} + {error && ( +
+

Erro no Cadastro:

+

{error}

+
+ )} + +
+
+ + handleInputChange("nomeCompleto", e.target.value)} + placeholder="Nome e Sobrenome" + required + /> +
+ +
+
+ + handleInputChange("email", e.target.value)} + placeholder="exemplo@dominio.com" + required + /> +
+
+ + handleInputChange("password", e.target.value)} + placeholder="••••••••" + required + minLength={MIN_PASSWORD_LENGTH} // Adiciona validação HTML + /> + {/* MENSAGEM DE AJUDA PARA SENHA */} +

Mínimo de {MIN_PASSWORD_LENGTH} caracteres.

+
+
+ +
+
+ + handleInputChange("telefone", e.target.value)} + placeholder="(00) 00000-0000" + maxLength={15} + /> +
+
+ + +
+
+
+ + {/* Action Buttons */} +
+ + + + +
+
- -
-
- - handleInputChange("email", e.target.value)} - placeholder="exemplo@dominio.com" - required - /> -
-
- - handleInputChange("password", e.target.value)} - placeholder="••••••••" - required - /> -
-
- -
-
- - handleInputChange("telefone", e.target.value)} - placeholder="(00) 00000-0000" - maxLength={15} - /> -
-
- - -
-
-
- - {/* Action Buttons */} -
- - - - -
- -
- - ); -} \ No newline at end of file + + ); +} diff --git a/services/usersApi.mjs b/services/usersApi.mjs index f37bc14..60be5de 100644 --- a/services/usersApi.mjs +++ b/services/usersApi.mjs @@ -1,8 +1,23 @@ +// services/usersApi.mjs (Versão Corrigida) + import { api } from "./api.mjs"; export const usersService = { create_user: (data) => api.post(`/functions/v1/create-user`), - list_roles: () => api.get(`/rest/v1/user_roles`), - full_data: (id) => api.get(`/functions/v1/user-info?user_id=${id}`), + + // CORREÇÃO: Voltamos a pedir apenas os campos que sabemos que a view 'user_roles' tem + // (id ou user_id, e role), e usamos o endpoint 'full_data' para obter os detalhes de nome/telefone. + // SE a sua view 'user_roles' contiver uma coluna chamada 'user_id', tente a próxima linha: + // list_roles: () => api.get(`/rest/v1/user_roles?select=user_id,role,profiles(full_name,phone)`), + // + // PORÉM, VAMOS ASSUMIR QUE A RELAÇÃO ESTÁ REALMENTE QUEBRADA E SIMPLIFICAR A CHAMADA INICIAL: + list_roles: () => api.get(`/rest/v1/user_roles?select=id,user_id,email,role`), + // Se o email também não estiver em 'user_roles', apenas use 'id,user_id,role'. + // O importante é que esta chamada de API NÃO DÊ ERRO 400. + + full_data: (id) => { + const endpoint = `/functions/v1/user-info?user_id=${id}`; + return api.get(endpoint); + }, summary_data: () => api.get(`/auth/v1/user`) } \ No newline at end of file From 0fee84013842319a67c6e70b1aed72ea8b82b109 Mon Sep 17 00:00:00 2001 From: Lucas Rodrigues Date: Wed, 15 Oct 2025 00:08:01 -0300 Subject: [PATCH 4/5] listando os users --- app/manager/usuario/page.tsx | 140 +++++++++++++++++++---------------- package-lock.json | 20 ++--- package.json | 8 +- services/usersApi.mjs | 15 +--- 4 files changed, 92 insertions(+), 91 deletions(-) diff --git a/app/manager/usuario/page.tsx b/app/manager/usuario/page.tsx index 12a4cc4..d126112 100644 --- a/app/manager/usuario/page.tsx +++ b/app/manager/usuario/page.tsx @@ -51,7 +51,8 @@ interface User { interface FlatUser { - id: string; + id: string; + user_id: string; full_name?: string; email: string; phone?: string | null; @@ -59,6 +60,7 @@ interface FlatUser { } + export default function UsersPage() { const router = useRouter(); @@ -73,69 +75,79 @@ export default function UsersPage() { const [selectedRole, setSelectedRole] = useState(""); const fetchUsers = useCallback(async () => { - setLoading(true); - setError(null); - try { + setLoading(true); + setError(null); + try { + const data = await usersService.list_roles(); // já retorna o JSON diretamente + console.log("Resposta da API list_roles:", data); - const { data, error } = await usersService.list_roles(); - if (error) throw error; + if (Array.isArray(data)) { + const mappedUsers: FlatUser[] = data.map((item: any) => ({ + id: item.id || (item.user_id ?? ""), // id da linha ou fallback + user_id: item.user_id || item.id || "", // garante que user_id exista + full_name: item.full_name || "—", + email: item.email || "—", + phone: item.phone ?? "—", + role: item.role || "—", + })); - if (Array.isArray(data)) { - setUsers(data as FlatUser[]); - } else { - console.warn("Formato inesperado recebido:", data); - setUsers([]); - } - } catch (err: any) { - console.error("Erro ao buscar usuários:", err); - setError("Não foi possível carregar os usuários. Tente novamente."); + setUsers(mappedUsers); + } else { + console.warn("Formato inesperado recebido em list_roles:", data); setUsers([]); - } finally { - setLoading(false); } - }, []); + } catch (err: any) { + console.error("Erro ao buscar usuários:", err); + setError("Não foi possível carregar os usuários. Tente novamente."); + setUsers([]); + } finally { + setLoading(false); + } +}, []); + useEffect(() => { fetchUsers(); }, [fetchUsers]); + + + const openDetailsDialog = async (flatUser: FlatUser) => { - setDetailsDialogOpen(true); - setUserDetails(null); - - try { - - const fullUserData: User = await usersService.full_data(flatUser.id); - - - setUserDetails(fullUserData); - - } catch (err: any) { - console.error("Erro ao buscar detalhes do usuário:", err); - - setUserDetails({ - user: { - id: flatUser.id, - email: flatUser.email || "", - created_at: "Erro ao Carregar", - last_sign_in_at: "Erro ao Carregar", - }, - profile: { - full_name: "Erro ao Carregar Detalhes", - phone: "—", - }, - roles: [], - permissions: {}, - } as any); - } - }; + setDetailsDialogOpen(true); + setUserDetails(null); + + try { + console.log("Buscando detalhes do user_id:", flatUser.user_id); + const fullUserData: User = await usersService.full_data(flatUser.user_id); + setUserDetails(fullUserData); + } catch (err: any) { + console.error("Erro ao buscar detalhes do usuário:", err); + setUserDetails({ + user: { + id: flatUser.user_id, + email: flatUser.email || "", + created_at: "Erro ao Carregar", + last_sign_in_at: "Erro ao Carregar", + }, + profile: { + full_name: flatUser.full_name || "Erro ao Carregar Detalhes", + phone: flatUser.phone || "—", + }, + roles: [], + permissions: {}, + } as any); + } +}; - const filteredUsers = selectedRole - ? users.filter((u) => u.role === selectedRole) - : users; + const filteredUsers = selectedRole && selectedRole !== "all" + ? users.filter((u) => u.role === selectedRole) + : users; + + return ( @@ -156,18 +168,19 @@ export default function UsersPage() {
+ + + + + Todos + Admin + Gestor + Médico + Secretaria + Usuário + + +
@@ -211,7 +224,7 @@ export default function UsersPage() { {user.role || "—"}
- +
diff --git a/package-lock.json b/package-lock.json index 61667f8..48539dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55,9 +55,9 @@ "lucide-react": "^0.454.0", "next": "^14.2.33", "next-themes": "^0.4.6", - "react": "^18", + "react": "^18.3.1", "react-day-picker": "^9.8.0", - "react-dom": "^18", + "react-dom": "^18.3.1", "react-hook-form": "^7.60.0", "react-resizable-panels": "^2.1.7", "recharts": "2.15.4", @@ -70,8 +70,8 @@ "devDependencies": { "@tailwindcss/postcss": "^4.1.9", "@types/js-cookie": "^3.0.6", - "@types/node": "^22", - "@types/react": "^18", + "@types/node": "^22.18.10", + "@types/react": "^18.3.26", "@types/react-dom": "^18", "postcss": "^8.5", "tailwindcss": "^4.1.9", @@ -2496,9 +2496,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.18.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.6.tgz", - "integrity": "sha512-r8uszLPpeIWbNKtvWRt/DbVi5zbqZyj1PTmhRMqBMvDnaz1QpmSKujUtJLrqGZeoM8v72MfYggDceY4K1itzWQ==", + "version": "22.18.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.10.tgz", + "integrity": "sha512-anNG/V/Efn/YZY4pRzbACnKxNKoBng2VTFydVu8RRs5hQjikP8CQfaeAV59VFSCzKNp90mXiVXW2QzV56rwMrg==", "dev": true, "license": "MIT", "dependencies": { @@ -2513,9 +2513,9 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "18.3.24", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.24.tgz", - "integrity": "sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A==", + "version": "18.3.26", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.26.tgz", + "integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==", "devOptional": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 950da4f..7236f03 100644 --- a/package.json +++ b/package.json @@ -56,9 +56,9 @@ "lucide-react": "^0.454.0", "next": "^14.2.33", "next-themes": "^0.4.6", - "react": "^18", + "react": "^18.3.1", "react-day-picker": "^9.8.0", - "react-dom": "^18", + "react-dom": "^18.3.1", "react-hook-form": "^7.60.0", "react-resizable-panels": "^2.1.7", "recharts": "2.15.4", @@ -71,8 +71,8 @@ "devDependencies": { "@tailwindcss/postcss": "^4.1.9", "@types/js-cookie": "^3.0.6", - "@types/node": "^22", - "@types/react": "^18", + "@types/node": "^22.18.10", + "@types/react": "^18.3.26", "@types/react-dom": "^18", "postcss": "^8.5", "tailwindcss": "^4.1.9", diff --git a/services/usersApi.mjs b/services/usersApi.mjs index 60be5de..22f44e4 100644 --- a/services/usersApi.mjs +++ b/services/usersApi.mjs @@ -1,20 +1,7 @@ -// services/usersApi.mjs (Versão Corrigida) - import { api } from "./api.mjs"; - export const usersService = { create_user: (data) => api.post(`/functions/v1/create-user`), - - // CORREÇÃO: Voltamos a pedir apenas os campos que sabemos que a view 'user_roles' tem - // (id ou user_id, e role), e usamos o endpoint 'full_data' para obter os detalhes de nome/telefone. - // SE a sua view 'user_roles' contiver uma coluna chamada 'user_id', tente a próxima linha: - // list_roles: () => api.get(`/rest/v1/user_roles?select=user_id,role,profiles(full_name,phone)`), - // - // PORÉM, VAMOS ASSUMIR QUE A RELAÇÃO ESTÁ REALMENTE QUEBRADA E SIMPLIFICAR A CHAMADA INICIAL: - list_roles: () => api.get(`/rest/v1/user_roles?select=id,user_id,email,role`), - // Se o email também não estiver em 'user_roles', apenas use 'id,user_id,role'. - // O importante é que esta chamada de API NÃO DÊ ERRO 400. - + list_roles: () => api.get(`/rest/v1/user_roles`), full_data: (id) => { const endpoint = `/functions/v1/user-info?user_id=${id}`; return api.get(endpoint); From edbe7ee87e0cfb33a5ca69cdd4c59c93d2a3e165 Mon Sep 17 00:00:00 2001 From: pedrosiimoes Date: Wed, 15 Oct 2025 21:42:24 -0300 Subject: [PATCH 5/5] =?UTF-8?q?integra=C3=A7=C3=A3o=20da=20Api=20com=20os?= =?UTF-8?q?=20dashboards=20manager/secretary?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/manager/dashboard/page.tsx | 163 ++++++++++++++----- app/secretary/dashboard/page.tsx | 271 ++++++++++++++++++++++++++----- 2 files changed, 351 insertions(+), 83 deletions(-) diff --git a/app/manager/dashboard/page.tsx b/app/manager/dashboard/page.tsx index 6e1dd10..df56541 100644 --- a/app/manager/dashboard/page.tsx +++ b/app/manager/dashboard/page.tsx @@ -1,41 +1,105 @@ -import ManagerLayout from "@/components/manager-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 ManagerLayout from "@/components/manager-layout"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Calendar, Clock, Plus, User } from "lucide-react"; +import Link from "next/link"; +import React, { useState, useEffect } from "react"; +import { usersService } from "services/usersApi.mjs"; +import { doctorsService } from "services/doctorsApi.mjs"; export default function ManagerDashboard() { + // 🔹 Estados para usuários + const [firstUser, setFirstUser] = useState(null); + const [loadingUser, setLoadingUser] = useState(true); + + // 🔹 Estados para médicos + const [doctors, setDoctors] = useState([]); + const [loadingDoctors, setLoadingDoctors] = useState(true); + + // 🔹 Buscar primeiro usuário + useEffect(() => { + async function fetchFirstUser() { + try { + const data = await usersService.list_roles(); + if (Array.isArray(data) && data.length > 0) { + setFirstUser(data[0]); + } + } catch (error) { + console.error("Erro ao carregar usuário:", error); + } finally { + setLoadingUser(false); + } + } + + fetchFirstUser(); + }, []); + + // 🔹 Buscar 3 primeiros médicos + useEffect(() => { + async function fetchDoctors() { + try { + const data = await doctorsService.list(); // ajuste se seu service tiver outro método + if (Array.isArray(data)) { + setDoctors(data.slice(0, 3)); // pega os 3 primeiros + } + } catch (error) { + console.error("Erro ao carregar médicos:", error); + } finally { + setLoadingDoctors(false); + } + } + + fetchDoctors(); + }, []); + return (
+ {/* Cabeçalho */}

Dashboard

Bem-vindo ao seu portal de consultas médicas

+ {/* Cards principais */}
+ {/* Card 1 */} Relatórios gerenciais -
3
-

2 não lidos, 1 lido

+
0
+

Relatórios disponíveis

+ {/* Card 2 — Gestão de usuários */} Gestão de usuários -
João Marques
-

fez login a 13min

+ {loadingUser ? ( +
Carregando usuário...
+ ) : firstUser ? ( + <> +
{firstUser.full_name || "Sem nome"}
+

+ {firstUser.email || "Sem e-mail cadastrado"} +

+ + ) : ( +
Nenhum usuário encontrado
+ )}
+ {/* Card 3 — Perfil */} Perfil @@ -48,66 +112,79 @@ export default function ManagerDashboard() {
+ {/* Cards secundários */}
+ {/* Card — Ações rápidas */} Ações Rápidas Acesse rapidamente as principais funcionalidades - + - - - - + + + + + + + + {/* Card — Gestão de Médicos */} Gestão de Médicos - Médicos online + Médicos cadastrados recentemente -
-
-
-

Dr. Silva

-

Cardiologia

-
-
-

On-line

-

-
+ {loadingDoctors ? ( +

Carregando médicos...

+ ) : doctors.length === 0 ? ( +

Nenhum médico cadastrado.

+ ) : ( +
+ {doctors.map((doc, index) => ( +
+
+

{doc.full_name || "Sem nome"}

+

+ {doc.specialty || "Sem especialidade"} +

+
+
+

+ {doc.active ? "Ativo" : "Inativo"} +

+
+
+ ))}
-
-
-

Dra. Santos

-

Dermatologia

-
-
-

Off-line

-

Visto as 8:33

-
-
-
+ )}
- ) + ); } diff --git a/app/secretary/dashboard/page.tsx b/app/secretary/dashboard/page.tsx index 7171aa8..e37141c 100644 --- a/app/secretary/dashboard/page.tsx +++ b/app/secretary/dashboard/page.tsx @@ -1,41 +1,207 @@ -import SecretaryLayout from "@/components/secretary-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 SecretaryLayout from "@/components/secretary-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 React, { useState, useEffect } from "react"; +import { patientsService } from "@/services/patientsApi.mjs"; +import { appointmentsService } from "@/services/appointmentsApi.mjs"; export default function SecretaryDashboard() { + // Estados + const [patients, setPatients] = useState([]); + const [loadingPatients, setLoadingPatients] = useState(true); + + const [firstConfirmed, setFirstConfirmed] = useState(null); + const [nextAgendada, setNextAgendada] = useState(null); + const [loadingAppointments, setLoadingAppointments] = useState(true); + + // 🔹 Buscar pacientes + useEffect(() => { + async function fetchPatients() { + try { + const data = await patientsService.list(); + if (Array.isArray(data)) { + setPatients(data.slice(0, 3)); + } + } catch (error) { + console.error("Erro ao carregar pacientes:", error); + } finally { + setLoadingPatients(false); + } + } + fetchPatients(); + }, []); + + // 🔹 Buscar consultas (confirmadas + 1ª do mês) + useEffect(() => { + async function fetchAppointments() { + try { + const hoje = new Date(); + const inicioMes = new Date(hoje.getFullYear(), hoje.getMonth(), 1); + const fimMes = new Date(hoje.getFullYear(), hoje.getMonth() + 1, 0); + + // Mesmo parâmetro de ordenação da página /secretary/appointments + const queryParams = "order=scheduled_at.desc"; + const data = await appointmentsService.search_appointment(queryParams); + + if (!Array.isArray(data) || data.length === 0) { + setFirstConfirmed(null); + setNextAgendada(null); + return; + } + + // 🩵 1️⃣ Consultas confirmadas (para o card “Próxima Consulta Confirmada”) + const confirmadas = data.filter((apt: any) => { + const dataConsulta = new Date(apt.scheduled_at || apt.date); + return apt.status === "confirmed" && dataConsulta >= hoje; + }); + + confirmadas.sort( + (a: any, b: any) => + new Date(a.scheduled_at || a.date).getTime() - + new Date(b.scheduled_at || b.date).getTime() + ); + + setFirstConfirmed(confirmadas[0] || null); + + // 💙 2️⃣ Consultas deste mês — pegar sempre a 1ª (mais próxima) + const consultasMes = data.filter((apt: any) => { + const dataConsulta = new Date(apt.scheduled_at); + return dataConsulta >= inicioMes && dataConsulta <= fimMes; + }); + + if (consultasMes.length > 0) { + consultasMes.sort( + (a: any, b: any) => + new Date(a.scheduled_at).getTime() - + new Date(b.scheduled_at).getTime() + ); + setNextAgendada(consultasMes[0]); + } else { + setNextAgendada(null); + } + } catch (error) { + console.error("Erro ao carregar consultas:", error); + } finally { + setLoadingAppointments(false); + } + } + + fetchAppointments(); + }, []); + return (
+ {/* Cabeçalho */}

Dashboard

Bem-vindo ao seu portal de consultas médicas

+ {/* Cards principais */}
+ {/* Próxima Consulta Confirmada */} - Próxima Consulta + + Próxima Consulta Confirmada + -
15 Jan
-

Dr. Silva - 14:30

+ {loadingAppointments ? ( +
+ Carregando próxima consulta... +
+ ) : firstConfirmed ? ( + <> +
+ {new Date( + firstConfirmed.scheduled_at || firstConfirmed.date + ).toLocaleDateString("pt-BR")} +
+

+ {firstConfirmed.doctor_name + ? `Dr(a). ${firstConfirmed.doctor_name}` + : "Médico não informado"}{" "} + -{" "} + {new Date( + firstConfirmed.scheduled_at + ).toLocaleTimeString("pt-BR", { + hour: "2-digit", + minute: "2-digit", + })} +

+ + ) : ( +
+ Nenhuma consulta confirmada encontrada +
+ )}
+ {/* Consultas Este Mês */} - Consultas Este Mês + + Consultas Este Mês + -
3
-

2 realizadas, 1 agendada

+ {loadingAppointments ? ( +
+ Carregando consultas... +
+ ) : nextAgendada ? ( + <> +
+ {new Date( + nextAgendada.scheduled_at + ).toLocaleDateString("pt-BR", { + day: "2-digit", + month: "2-digit", + year: "numeric", + })}{" "} + às{" "} + {new Date( + nextAgendada.scheduled_at + ).toLocaleTimeString("pt-BR", { + hour: "2-digit", + minute: "2-digit", + })} +
+

+ {nextAgendada.doctor_name + ? `Dr(a). ${nextAgendada.doctor_name}` + : "Médico não informado"} +

+

+ {nextAgendada.patient_name + ? `Paciente: ${nextAgendada.patient_name}` + : ""} +

+ + ) : ( +
+ Nenhuma consulta agendada neste mês +
+ )}
+ {/* Perfil */} Perfil @@ -48,11 +214,15 @@ export default function SecretaryDashboard() {
+ {/* Cards Secundários */}
+ {/* Ações rápidas */} Ações Rápidas - Acesse rapidamente as principais funcionalidades + + Acesse rapidamente as principais funcionalidades + @@ -62,52 +232,73 @@ export default function SecretaryDashboard() { - - - + {/* Pacientes */} - Próximas Consultas - Suas consultas agendadas + Pacientes + + Últimos pacientes cadastrados + -
-
-
-

Dr. Silva

-

Cardiologia

-
-
-

15 Jan

-

14:30

-
+ {loadingPatients ? ( +

+ Carregando pacientes... +

+ ) : patients.length === 0 ? ( +

+ Nenhum paciente cadastrado. +

+ ) : ( +
+ {patients.map((patient, index) => ( +
+
+

+ {patient.full_name || "Sem nome"} +

+

+ {patient.phone_mobile || + patient.phone1 || + "Sem telefone"} +

+
+
+

+ {patient.convenio || "Particular"} +

+
+
+ ))}
-
-
-

Dra. Santos

-

Dermatologia

-
-
-

22 Jan

-

10:00

-
-
-
+ )}
- ) + ); }