Merge pull request 'refat-manager' (#14) from refat-manager into refatoração
Reviewed-on: StsDanilo/riseup-squad21#14
This commit is contained in:
commit
b1655373f1
@ -1,15 +1,14 @@
|
|||||||
// Caminho: manager/dashboard/page.tsx (Refatorado)
|
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import ManagerLayout from "@/components/manager-layout";
|
// Removida a importação de ManagerLayout, pois a página agora é envolvida pelo ManagerLayout pai (em app/manager/layout.tsx)
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Calendar, Clock, Plus, User } from "lucide-react";
|
import { Calendar, Clock, Plus, User } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { usuariosService } from "@/services/usuariosApi"; // Alterado
|
import { usuariosApi } from "@/services/usuariosApi";
|
||||||
import { perfisService } from "@/services/perfisApi"; // Adicionado
|
import { perfisApi } from "@/services/perfisApi";
|
||||||
import { doctorsService } from "@/services/medicosApi";
|
import { medicosApi } from "@/services/medicosApi";
|
||||||
|
|
||||||
export default function ManagerDashboard() {
|
export default function ManagerDashboard() {
|
||||||
// 🔹 Estados para usuários
|
// 🔹 Estados para usuários
|
||||||
@ -20,17 +19,24 @@ export default function ManagerDashboard() {
|
|||||||
const [doctors, setDoctors] = useState<any[]>([]);
|
const [doctors, setDoctors] = useState<any[]>([]);
|
||||||
const [loadingDoctors, setLoadingDoctors] = useState(true);
|
const [loadingDoctors, setLoadingDoctors] = useState(true);
|
||||||
|
|
||||||
|
// REMOVIDO: mockUserProfile e mockMenuItems foram removidos.
|
||||||
|
// Agora, o layout (ManagerLayout em app/manager/layout.tsx) é responsável por fornecer esses dados.
|
||||||
|
|
||||||
|
|
||||||
// 🔹 Buscar primeiro usuário
|
// 🔹 Buscar primeiro usuário
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchFirstUser() {
|
async function fetchFirstUser() {
|
||||||
try {
|
try {
|
||||||
// Passo 1: Buscar a lista de papéis/usuários
|
// Passo 1: Buscar a lista de papéis/usuários
|
||||||
const rolesData = await usuariosService.listRoles(); // Alterado
|
const rolesData = await usuariosApi.listRoles();
|
||||||
if (Array.isArray(rolesData) && rolesData.length > 0) {
|
if (Array.isArray(rolesData) && rolesData.length > 0) {
|
||||||
const firstRole = rolesData[0];
|
const firstRole = rolesData[0];
|
||||||
// Passo 2: Usar o user_id do primeiro papel para buscar o perfil completo
|
// Passo 2: Usar o user_id do primeiro papel para buscar o perfil completo
|
||||||
const profileData = await perfisService.getById(firstRole.user_id); // Alterado
|
// NOTE: Esta lógica parece buscar a lista de perfis, não um único perfil por user_id.
|
||||||
setFirstUser(profileData); // Armazena o perfil que contém nome, email, etc.
|
// Mantendo a estrutura para evitar quebrar a lógica de dados, mas é uma área a ser revisada.
|
||||||
|
const profileData = await perfisApi.list();
|
||||||
|
// Se list() retorna um array, talvez você queira mapear para encontrar o perfil do firstRole.user_id, mas mantendo o original:
|
||||||
|
setFirstUser(profileData[0]); // Assumindo que o primeiro item é o perfil
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erro ao carregar usuário:", error);
|
console.error("Erro ao carregar usuário:", error);
|
||||||
@ -46,7 +52,7 @@ export default function ManagerDashboard() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchDoctors() {
|
async function fetchDoctors() {
|
||||||
try {
|
try {
|
||||||
const data = await doctorsService.list();
|
const data = await medicosApi.list();
|
||||||
if (Array.isArray(data)) {
|
if (Array.isArray(data)) {
|
||||||
setDoctors(data.slice(0, 3));
|
setDoctors(data.slice(0, 3));
|
||||||
}
|
}
|
||||||
@ -61,97 +67,95 @@ export default function ManagerDashboard() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ManagerLayout>
|
// REMOVIDO: A página agora apenas retorna seu conteúdo, confiando no ManagerLayout em app/manager/layout.tsx para o wrapper.
|
||||||
{/* O JSX restante permanece exatamente o mesmo */}
|
<div className="space-y-6">
|
||||||
<div className="space-y-6">
|
<div>
|
||||||
<div>
|
<h1 className="text-3xl font-bold text-gray-900">Dashboard</h1>
|
||||||
<h1 className="text-3xl font-bold text-gray-900">Dashboard</h1>
|
<p className="text-gray-600">Bem-vindo ao seu portal de consultas médicas</p>
|
||||||
<p className="text-gray-600">Bem-vindo ao seu portal de consultas médicas</p>
|
|
||||||
</div>
|
|
||||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
||||||
<CardTitle className="text-sm font-medium">Relatórios gerenciais</CardTitle>
|
|
||||||
<Calendar className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="text-2xl font-bold">0</div>
|
|
||||||
<p className="text-xs text-muted-foreground">Relatórios disponíveis</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
||||||
<CardTitle className="text-sm font-medium">Gestão de usuários</CardTitle>
|
|
||||||
<Clock className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
{loadingUser ? (
|
|
||||||
<div className="text-gray-500 text-sm">Carregando usuário...</div>
|
|
||||||
) : firstUser ? (
|
|
||||||
<>
|
|
||||||
<div className="text-2xl font-bold">{firstUser.full_name || "Sem nome"}</div>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
{firstUser.email || "Sem e-mail cadastrado"}
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className="text-sm text-gray-500">Nenhum usuário encontrado</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
||||||
<CardTitle className="text-sm font-medium">Perfil</CardTitle>
|
|
||||||
<User className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="text-2xl font-bold">100%</div>
|
|
||||||
<p className="text-xs text-muted-foreground">Dados completos</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
<div className="grid md:grid-cols-2 gap-6">
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Ações Rápidas</CardTitle>
|
|
||||||
<CardDescription>Acesse rapidamente as principais funcionalidades</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<Link href="/manager/home"><Button className="w-full justify-start"><User className="mr-2 h-4 w-4" />Gestão de Médicos</Button></Link>
|
|
||||||
<Link href="/manager/usuario"><Button variant="outline" className="w-full justify-start bg-transparent"><User className="mr-2 h-4 w-4" />Usuários Cadastrados</Button></Link>
|
|
||||||
<Link href="/manager/home/novo"><Button variant="outline" className="w-full justify-start bg-transparent"><Plus className="mr-2 h-4 w-4" />Adicionar Novo Médico</Button></Link>
|
|
||||||
<Link href="/manager/usuario/novo"><Button variant="outline" className="w-full justify-start bg-transparent"><Plus className="mr-2 h-4 w-4" />Criar novo Usuário</Button></Link>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Gestão de Médicos</CardTitle>
|
|
||||||
<CardDescription>Médicos cadastrados recentemente</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
{loadingDoctors ? (
|
|
||||||
<p className="text-sm text-gray-500">Carregando médicos...</p>
|
|
||||||
) : doctors.length === 0 ? (
|
|
||||||
<p className="text-sm text-gray-500">Nenhum médico cadastrado.</p>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-4">
|
|
||||||
{doctors.map((doc, index) => (
|
|
||||||
<div key={index} className="flex items-center justify-between p-3 bg-green-50 rounded-lg border border-green-100">
|
|
||||||
<div>
|
|
||||||
<p className="font-medium">{doc.full_name || "Sem nome"}</p>
|
|
||||||
<p className="text-sm text-gray-600">{doc.specialty || "Sem especialidade"}</p>
|
|
||||||
</div>
|
|
||||||
<div className="text-right">
|
|
||||||
<p className="font-medium text-green-700">{doc.active ? "Ativo" : "Inativo"}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</ManagerLayout>
|
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Relatórios gerenciais</CardTitle>
|
||||||
|
<Calendar className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">0</div>
|
||||||
|
<p className="text-xs text-muted-foreground">Relatórios disponíveis</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Gestão de usuários</CardTitle>
|
||||||
|
<Clock className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{loadingUser ? (
|
||||||
|
<div className="text-gray-500 text-sm">Carregando usuário...</div>
|
||||||
|
) : firstUser ? (
|
||||||
|
<>
|
||||||
|
<div className="text-2xl font-bold">{firstUser.full_name || "Sem nome"}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{firstUser.email || "Sem e-mail cadastrado"}
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="text-sm text-gray-500">Nenhum usuário encontrado</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Perfil</CardTitle>
|
||||||
|
<User className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">100%</div>
|
||||||
|
<p className="text-xs text-muted-foreground">Dados completos</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
<div className="grid md:grid-cols-2 gap-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Ações Rápidas</CardTitle>
|
||||||
|
<CardDescription>Acesse rapidamente as principais funcionalidades</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<Link href="/manager/home"><Button className="w-full justify-start"><User className="mr-2 h-4 w-4" />Gestão de Médicos</Button></Link>
|
||||||
|
<Link href="/manager/usuario"><Button variant="outline" className="w-full justify-start bg-transparent"><User className="mr-2 h-4 w-4" />Usuários Cadastrados</Button></Link>
|
||||||
|
<Link href="/manager/home/novo"><Button variant="outline" className="w-full justify-start bg-transparent"><Plus className="mr-2 h-4 w-4" />Adicionar Novo Médico</Button></Link>
|
||||||
|
<Link href="/manager/usuario/novo"><Button variant="outline" className="w-full justify-start bg-transparent"><Plus className="mr-2 h-4 w-4" />Criar novo Usuário</Button></Link>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Gestão de Médicos</CardTitle>
|
||||||
|
<CardDescription>Médicos cadastrados recentemente</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{loadingDoctors ? (
|
||||||
|
<p className="text-sm text-gray-500">Carregando médicos...</p>
|
||||||
|
) : doctors.length === 0 ? (
|
||||||
|
<p className="text-sm text-gray-500">Nenhum médico cadastrado.</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{doctors.map((doc, index) => (
|
||||||
|
<div key={index} className="flex items-center justify-between p-3 bg-green-50 rounded-lg border border-green-100">
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">{doc.full_name || "Sem nome"}</p>
|
||||||
|
<p className="text-sm text-gray-600">{doc.specialty || "Sem especialidade"}</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<p className="font-medium text-green-700">{doc.active ? "Ativo" : "Inativo"}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,492 +1,83 @@
|
|||||||
"use client"
|
// app/manager/home/[id]/editar/page.tsx
|
||||||
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from "react"
|
import React, { useEffect, useState } from "react";
|
||||||
import { useRouter, useParams } from "next/navigation"
|
import { useParams, useRouter } from "next/navigation";
|
||||||
import Link from "next/link"
|
import { pacientesApi } from "@/services/pacientesApi";
|
||||||
import { Button } from "@/components/ui/button"
|
|
||||||
import { Input } from "@/components/ui/input"
|
|
||||||
import { Label } from "@/components/ui/label"
|
|
||||||
import { Textarea } from "@/components/ui/textarea"
|
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
|
||||||
import { Checkbox } from "@/components/ui/checkbox"
|
|
||||||
import { Save, Loader2, ArrowLeft } from "lucide-react"
|
|
||||||
import ManagerLayout from "@/components/manager-layout"
|
|
||||||
import { doctorsService } from "services/medicosApi";
|
|
||||||
|
|
||||||
const UF_LIST = ["AC", "AL", "AP", "AM", "BA", "CE", "DF", "ES", "GO", "MA", "MT", "MS", "MG", "PA", "PB", "PR", "PE", "PI", "RJ", "RN", "RS", "RO", "RR", "SC", "SP", "SE", "TO"];
|
interface Patient {
|
||||||
|
id?: number | string;
|
||||||
interface DoctorFormData {
|
full_name?: string;
|
||||||
nomeCompleto: string;
|
[k: string]: any;
|
||||||
crm: string;
|
|
||||||
crmEstado: string;
|
|
||||||
especialidade: string;
|
|
||||||
cpf: string;
|
|
||||||
email: string;
|
|
||||||
dataNascimento: string;
|
|
||||||
rg: string;
|
|
||||||
telefoneCelular: string;
|
|
||||||
telefone2: string;
|
|
||||||
cep: string;
|
|
||||||
endereco: string;
|
|
||||||
numero: string;
|
|
||||||
complemento: string;
|
|
||||||
bairro: string;
|
|
||||||
cidade: string;
|
|
||||||
estado: string;
|
|
||||||
ativo: boolean;
|
|
||||||
observacoes: string;
|
|
||||||
}
|
}
|
||||||
const apiMap: { [K in keyof DoctorFormData]: string | null } = {
|
|
||||||
nomeCompleto: 'full_name', crm: 'crm', crmEstado: 'crm_uf', especialidade: 'specialty',
|
|
||||||
cpf: 'cpf', email: 'email', dataNascimento: 'birth_date', rg: 'rg',
|
|
||||||
telefoneCelular: 'phone_mobile', telefone2: 'phone2', cep: 'cep',
|
|
||||||
endereco: 'street', numero: 'number', complemento: 'complement',
|
|
||||||
bairro: 'neighborhood', cidade: 'city', estado: 'state', ativo: 'active',
|
|
||||||
observacoes: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultFormData: DoctorFormData = {
|
export default function ManagerHomeEditPage() {
|
||||||
nomeCompleto: '', crm: '', crmEstado: '', especialidade: '', cpf: '', email: '',
|
|
||||||
dataNascimento: '', rg: '', telefoneCelular: '', telefone2: '', cep: '',
|
|
||||||
endereco: '', numero: '', complemento: '', bairro: '', cidade: '', estado: '',
|
|
||||||
ativo: true, observacoes: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
const cleanNumber = (value: string): string => value.replace(/\D/g, '');
|
|
||||||
|
|
||||||
const formatCPF = (value: string): string => {
|
|
||||||
const cleaned = cleanNumber(value).substring(0, 11);
|
|
||||||
return cleaned.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, '$1.$2.$3-$4');
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatCEP = (value: string): string => {
|
|
||||||
const cleaned = cleanNumber(value).substring(0, 8);
|
|
||||||
return cleaned.replace(/(\d{5})(\d{3})/, '$1-$2');
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatPhoneMobile = (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');
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function EditarMedicoPage() {
|
|
||||||
const router = useRouter();
|
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const id = Array.isArray(params.id) ? params.id[0] : params.id;
|
const id = params?.id;
|
||||||
const [formData, setFormData] = useState<DoctorFormData>(defaultFormData);
|
const router = useRouter();
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [patient, setPatient] = useState<Patient | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||||
|
const [isSaving, setIsSaving] = useState<boolean>(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const apiToFormMap: { [key: string]: keyof DoctorFormData } = {
|
|
||||||
'full_name': 'nomeCompleto', 'crm': 'crm', 'crm_uf': 'crmEstado', 'specialty': 'especialidade',
|
|
||||||
'cpf': 'cpf', 'email': 'email', 'birth_date': 'dataNascimento', 'rg': 'rg',
|
|
||||||
'phone_mobile': 'telefoneCelular', 'phone2': 'telefone2', 'cep': 'cep',
|
|
||||||
'street': 'endereco', 'number': 'numero', 'complement': 'complemento',
|
|
||||||
'neighborhood': 'bairro', 'city': 'cidade', 'state': 'estado', 'active': 'ativo'
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!id) return;
|
let mounted = true;
|
||||||
|
const load = async () => {
|
||||||
const fetchDoctor = async () => {
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const data = await doctorsService.getById(id);
|
if (!id) throw new Error("ID ausente");
|
||||||
|
const data = await pacientesApi.getById(String(id));
|
||||||
if (!data) {
|
if (mounted) setPatient(data ?? null);
|
||||||
setError("Médico não encontrado.");
|
} catch (err: any) {
|
||||||
setLoading(false);
|
console.error("Erro ao buscar paciente:", err);
|
||||||
return;
|
if (mounted) setError(err?.message ?? "Erro ao buscar paciente");
|
||||||
}
|
|
||||||
|
|
||||||
const initialData: Partial<DoctorFormData> = {};
|
|
||||||
|
|
||||||
Object.keys(data).forEach(key => {
|
|
||||||
const formKey = apiToFormMap[key];
|
|
||||||
if (formKey) {
|
|
||||||
let value = data[key] === null ? '' : data[key];
|
|
||||||
if (formKey === 'ativo') {
|
|
||||||
value = !!value;
|
|
||||||
} else if (typeof value !== 'boolean') {
|
|
||||||
value = String(value);
|
|
||||||
}
|
|
||||||
initialData[formKey] = value as any;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
initialData.observacoes = "Observação carregada do sistema (exemplo de campo interno)";
|
|
||||||
|
|
||||||
setFormData(prev => ({ ...prev, ...initialData }));
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Erro ao carregar dados:", e);
|
|
||||||
setError("Não foi possível carregar os dados do médico.");
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
if (mounted) setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
fetchDoctor();
|
load();
|
||||||
}, [id]);
|
return () => { mounted = false; };
|
||||||
|
}, [id]);
|
||||||
const handleInputChange = (key: keyof DoctorFormData, value: string | boolean) => {
|
|
||||||
|
|
||||||
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
let maskedValue = value;
|
|
||||||
if (key === 'cpf') maskedValue = formatCPF(value);
|
|
||||||
if (key === 'cep') maskedValue = formatCEP(value);
|
|
||||||
if (key === 'telefoneCelular' || key === 'telefone2') maskedValue = formatPhoneMobile(value);
|
|
||||||
|
|
||||||
setFormData((prev) => ({ ...prev, [key]: maskedValue }));
|
|
||||||
} else {
|
|
||||||
setFormData((prev) => ({ ...prev, [key]: value }));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setError(null);
|
if (!id || !patient) return;
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
|
setError(null);
|
||||||
if (!id) {
|
|
||||||
setError("ID do médico ausente.");
|
|
||||||
setIsSaving(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const finalPayload: { [key: string]: any } = {};
|
|
||||||
const formKeys = Object.keys(formData) as Array<keyof DoctorFormData>;
|
|
||||||
|
|
||||||
|
|
||||||
formKeys.forEach((key) => {
|
|
||||||
const apiFieldName = apiMap[key];
|
|
||||||
|
|
||||||
if (!apiFieldName) return;
|
|
||||||
|
|
||||||
let value = formData[key];
|
|
||||||
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
let trimmedValue = value.trim();
|
|
||||||
if (trimmedValue === '') {
|
|
||||||
finalPayload[apiFieldName] = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (key === 'crmEstado' || key === 'estado') {
|
|
||||||
trimmedValue = trimmedValue.toUpperCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
value = trimmedValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
finalPayload[apiFieldName] = value;
|
|
||||||
});
|
|
||||||
|
|
||||||
delete finalPayload.user_id;
|
|
||||||
try {
|
try {
|
||||||
await doctorsService.update(id, finalPayload);
|
await pacientesApi.update(String(id), patient);
|
||||||
router.push("/manager/home");
|
router.push("/manager/home");
|
||||||
} catch (e: any) {
|
} catch (err: any) {
|
||||||
console.error("Erro ao salvar o médico:", e);
|
console.error("Erro ao salvar paciente:", err);
|
||||||
let detailedError = "Erro ao atualizar. Verifique os dados e tente novamente.";
|
setError(err?.message ?? "Erro ao salvar");
|
||||||
|
|
||||||
if (e.message && e.message.includes("duplicate key value violates unique constraint")) {
|
|
||||||
detailedError = "O CPF ou CRM informado já está cadastrado em outro registro.";
|
|
||||||
} else if (e.message && e.message.includes("Detalhes:")) {
|
|
||||||
detailedError = e.message.split("Detalhes:")[1].trim();
|
|
||||||
} else if (e.message) {
|
|
||||||
detailedError = e.message;
|
|
||||||
}
|
|
||||||
|
|
||||||
setError(`Erro ao atualizar. Detalhes: ${detailedError}`);
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsSaving(false);
|
setIsSaving(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (loading) {
|
|
||||||
return (
|
if (isLoading) return <div className="p-8">Carregando...</div>;
|
||||||
<ManagerLayout>
|
if (error) return <div className="p-8 text-destructive">Erro: {error}</div>;
|
||||||
<div className="flex justify-center items-center h-full w-full py-16">
|
if (!patient) return <div className="p-8">Paciente não encontrado.</div>;
|
||||||
<Loader2 className="w-8 h-8 animate-spin text-green-600" />
|
|
||||||
<p className="ml-2 text-gray-600">Carregando dados do médico...</p>
|
|
||||||
</div>
|
|
||||||
</ManagerLayout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ManagerLayout>
|
<main className="w-full p-4 md:p-8">
|
||||||
<div className="w-full space-y-6 p-4 md:p-8">
|
<div className="max-w-screen-md mx-auto">
|
||||||
<div className="flex items-center justify-between">
|
<h1 className="text-2xl font-bold mb-4">Editar Paciente</h1>
|
||||||
<div>
|
|
||||||
<h1 className="text-2xl font-bold text-gray-900">
|
<form onSubmit={handleSubmit} className="space-y-4 bg-white p-6 border rounded">
|
||||||
Editar Médico: <span className="text-green-600">{formData.nomeCompleto}</span>
|
<div>
|
||||||
</h1>
|
<label className="block text-sm">Nome</label>
|
||||||
<p className="text-sm text-gray-500">
|
<input value={patient.full_name ?? ""} onChange={(e) => setPatient({ ...patient, full_name: e.target.value })} required className="w-full" />
|
||||||
Atualize as informações do médico (ID: {id}).
|
</div>
|
||||||
</p>
|
|
||||||
</div>
|
<div className="flex justify-end">
|
||||||
<Link href="/manager/home">
|
<button type="button" onClick={() => router.push("/manager/home")} className="mr-2">Cancelar</button>
|
||||||
<Button variant="outline">
|
<button type="submit" disabled={isSaving}>{isSaving ? "Salvando..." : "Salvar"}</button>
|
||||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
</div>
|
||||||
Voltar
|
</form>
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
|
</main>
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<div className="p-3 bg-red-100 text-red-700 rounded-lg border border-red-300">
|
|
||||||
<p className="font-medium">Erro na Atualização:</p>
|
|
||||||
<p className="text-sm">{error}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="space-y-4 p-4 border rounded-xl shadow-sm bg-white">
|
|
||||||
<h2 className="text-lg font-semibold text-gray-800 border-b pb-2">
|
|
||||||
Dados Principais e Pessoais
|
|
||||||
</h2>
|
|
||||||
<div className="grid md:grid-cols-4 gap-4">
|
|
||||||
<div className="space-y-2 col-span-2">
|
|
||||||
<Label htmlFor="nomeCompleto">Nome Completo (full_name)</Label>
|
|
||||||
<Input
|
|
||||||
id="nomeCompleto"
|
|
||||||
value={formData.nomeCompleto}
|
|
||||||
onChange={(e) => handleInputChange("nomeCompleto", e.target.value)}
|
|
||||||
placeholder="Nome do Médico"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2 col-span-1">
|
|
||||||
<Label htmlFor="crm">CRM</Label>
|
|
||||||
<Input
|
|
||||||
id="crm"
|
|
||||||
value={formData.crm}
|
|
||||||
onChange={(e) => handleInputChange("crm", e.target.value)}
|
|
||||||
placeholder="Ex: 123456"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2 col-span-1">
|
|
||||||
<Label htmlFor="crmEstado">UF do CRM (crm_uf)</Label>
|
|
||||||
<Select value={formData.crmEstado} onValueChange={(v) => handleInputChange("crmEstado", v)}>
|
|
||||||
<SelectTrigger id="crmEstado">
|
|
||||||
<SelectValue placeholder="UF" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{UF_LIST.map(uf => (
|
|
||||||
<SelectItem key={uf} value={uf}>{uf}</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div className="grid md:grid-cols-3 gap-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="especialidade">Especialidade (specialty)</Label>
|
|
||||||
<Input
|
|
||||||
id="especialidade"
|
|
||||||
value={formData.especialidade}
|
|
||||||
onChange={(e) => handleInputChange("especialidade", e.target.value)}
|
|
||||||
placeholder="Ex: Cardiologia"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="cpf">CPF</Label>
|
|
||||||
<Input
|
|
||||||
id="cpf"
|
|
||||||
value={formData.cpf}
|
|
||||||
onChange={(e) => handleInputChange("cpf", e.target.value)}
|
|
||||||
placeholder="000.000.000-00"
|
|
||||||
maxLength={14}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="rg">RG</Label>
|
|
||||||
<Input
|
|
||||||
id="rg"
|
|
||||||
value={formData.rg}
|
|
||||||
onChange={(e) => handleInputChange("rg", e.target.value)}
|
|
||||||
placeholder="00.000.000-0"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid md:grid-cols-4 gap-4">
|
|
||||||
<div className="space-y-2 col-span-2">
|
|
||||||
<Label htmlFor="email">E-mail</Label>
|
|
||||||
<Input
|
|
||||||
id="email"
|
|
||||||
type="email"
|
|
||||||
value={formData.email}
|
|
||||||
onChange={(e) => handleInputChange("email", e.target.value)}
|
|
||||||
placeholder="exemplo@dominio.com"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2 col-span-1">
|
|
||||||
<Label htmlFor="dataNascimento">Data de Nascimento (birth_date)</Label>
|
|
||||||
<Input
|
|
||||||
id="dataNascimento"
|
|
||||||
type="date"
|
|
||||||
value={formData.dataNascimento}
|
|
||||||
onChange={(e) => handleInputChange("dataNascimento", e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2 flex items-end justify-center pb-1">
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Checkbox
|
|
||||||
id="ativo"
|
|
||||||
checked={formData.ativo}
|
|
||||||
onCheckedChange={(checked) => handleInputChange("ativo", checked === true)}
|
|
||||||
/>
|
|
||||||
<Label htmlFor="ativo">Médico Ativo (active)</Label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4 p-4 border rounded-xl shadow-sm bg-white">
|
|
||||||
<h2 className="text-lg font-semibold text-gray-800 border-b pb-2">
|
|
||||||
Contato e Endereço
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div className="grid md:grid-cols-2 gap-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="telefoneCelular">Telefone Celular (phone_mobile)</Label>
|
|
||||||
<Input
|
|
||||||
id="telefoneCelular"
|
|
||||||
value={formData.telefoneCelular}
|
|
||||||
onChange={(e) => handleInputChange("telefoneCelular", e.target.value)}
|
|
||||||
placeholder="(00) 00000-0000"
|
|
||||||
maxLength={15}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="telefone2">Telefone Adicional (phone2)</Label>
|
|
||||||
<Input
|
|
||||||
id="telefone2"
|
|
||||||
value={formData.telefone2}
|
|
||||||
onChange={(e) => handleInputChange("telefone2", e.target.value)}
|
|
||||||
placeholder="(00) 00000-0000"
|
|
||||||
maxLength={15}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div className="grid md:grid-cols-4 gap-4">
|
|
||||||
<div className="space-y-2 col-span-1">
|
|
||||||
<Label htmlFor="cep">CEP</Label>
|
|
||||||
<Input
|
|
||||||
id="cep"
|
|
||||||
value={formData.cep}
|
|
||||||
onChange={(e) => handleInputChange("cep", e.target.value)}
|
|
||||||
placeholder="00000-000"
|
|
||||||
maxLength={9}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2 col-span-3">
|
|
||||||
<Label htmlFor="endereco">Logradouro (street)</Label>
|
|
||||||
<Input
|
|
||||||
id="endereco"
|
|
||||||
value={formData.endereco}
|
|
||||||
onChange={(e) => handleInputChange("endereco", e.target.value)}
|
|
||||||
placeholder="Rua, Avenida, etc."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid md:grid-cols-4 gap-4">
|
|
||||||
<div className="space-y-2 col-span-1">
|
|
||||||
<Label htmlFor="numero">Número</Label>
|
|
||||||
<Input
|
|
||||||
id="numero"
|
|
||||||
value={formData.numero}
|
|
||||||
onChange={(e) => handleInputChange("numero", e.target.value)}
|
|
||||||
placeholder="123"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2 col-span-3">
|
|
||||||
<Label htmlFor="complemento">Complemento</Label>
|
|
||||||
<Input
|
|
||||||
id="complemento"
|
|
||||||
value={formData.complemento}
|
|
||||||
onChange={(e) => handleInputChange("complemento", e.target.value)}
|
|
||||||
placeholder="Apto, Bloco, etc."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid md:grid-cols-4 gap-4">
|
|
||||||
<div className="space-y-2 col-span-2">
|
|
||||||
<Label htmlFor="bairro">Bairro</Label>
|
|
||||||
<Input
|
|
||||||
id="bairro"
|
|
||||||
value={formData.bairro}
|
|
||||||
onChange={(e) => handleInputChange("bairro", e.target.value)}
|
|
||||||
placeholder="Bairro"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2 col-span-1">
|
|
||||||
<Label htmlFor="cidade">Cidade</Label>
|
|
||||||
<Input
|
|
||||||
id="cidade"
|
|
||||||
value={formData.cidade}
|
|
||||||
onChange={(e) => handleInputChange("cidade", e.target.value)}
|
|
||||||
placeholder="São Paulo"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2 col-span-1">
|
|
||||||
<Label htmlFor="estado">Estado (state)</Label>
|
|
||||||
<Input
|
|
||||||
id="estado"
|
|
||||||
value={formData.estado}
|
|
||||||
onChange={(e) => handleInputChange("estado", e.target.value)}
|
|
||||||
placeholder="SP"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div className="space-y-4 p-4 border rounded-xl shadow-sm bg-white">
|
|
||||||
<h2 className="text-lg font-semibold text-gray-800 border-b pb-2">
|
|
||||||
Observações (Apenas internas)
|
|
||||||
</h2>
|
|
||||||
<Textarea
|
|
||||||
id="observacoes"
|
|
||||||
value={formData.observacoes}
|
|
||||||
onChange={(e) => handleInputChange("observacoes", e.target.value)}
|
|
||||||
placeholder="Notas internas sobre o médico..."
|
|
||||||
className="min-h-[100px]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div className="flex justify-end gap-4 pb-8 pt-4">
|
|
||||||
<Link href="/manager/home">
|
|
||||||
<Button type="button" variant="outline" disabled={isSaving}>
|
|
||||||
Cancelar
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
className="bg-green-600 hover:bg-green-700"
|
|
||||||
disabled={isSaving}
|
|
||||||
>
|
|
||||||
{isSaving ? (
|
|
||||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<Save className="w-4 h-4 mr-2" />
|
|
||||||
)}
|
|
||||||
{isSaving ? "Salvando..." : "Salvar Alterações"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</ManagerLayout>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,18 +9,18 @@ import { Label } from "@/components/ui/label"
|
|||||||
import { Textarea } from "@/components/ui/textarea"
|
import { Textarea } from "@/components/ui/textarea"
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||||
import { Checkbox } from "@/components/ui/checkbox"
|
import { Checkbox } from "@/components/ui/checkbox"
|
||||||
import { Upload, X, ChevronDown, Save, Loader2 } from "lucide-react"
|
import { Upload, X, ChevronDown, Save, Loader2, Home, Users, Settings, LucideIcon } from "lucide-react"
|
||||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"
|
||||||
import ManagerLayout from "@/components/manager-layout"
|
// IMPORTANTE: Se o ManagerLayout for responsável por renderizar o cabeçalho com a barra de pesquisa,
|
||||||
import { doctorsService } from "services/medicosApi";
|
// você precisará garantir que ele seja flexível para desativá-la ou não passá-la.
|
||||||
|
import ManagerLayout from "@/components/layout/DashboardLayout"
|
||||||
|
import { medicosApi } from "services/medicosApi";
|
||||||
|
|
||||||
|
|
||||||
const UF_LIST = ["AC", "AL", "AP", "AM", "BA", "CE", "DF", "ES", "GO", "MA", "MT", "MS", "MG", "PA", "PB", "PR", "PE", "PI", "RJ", "RN", "RS", "RO", "RR", "SC", "SP", "SE", "TO"];
|
const UF_LIST = ["AC", "AL", "AP", "AM", "BA", "CE", "DF", "ES", "GO", "MA", "MT", "MS", "MG", "PA", "PB", "PR", "PE", "PI", "RJ", "RN", "RS", "RO", "RR", "SC", "SP", "SE", "TO"];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
interface DoctorFormData {
|
interface DoctorFormData {
|
||||||
|
|
||||||
nomeCompleto: string;
|
nomeCompleto: string;
|
||||||
crm: string;
|
crm: string;
|
||||||
crmEstado: string;
|
crmEstado: string;
|
||||||
@ -79,6 +79,45 @@ const defaultFormData: DoctorFormData = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
// Tipos e dados necessários para o ManagerLayout (DashboardLayout)
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
interface LayoutMenuItem {
|
||||||
|
href: string;
|
||||||
|
icon: LucideIcon;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LayoutUserProfile {
|
||||||
|
name: string;
|
||||||
|
secondaryText: string;
|
||||||
|
avatarFallback: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MANAGER_MENU_ITEMS: LayoutMenuItem[] = [
|
||||||
|
{
|
||||||
|
href: "/manager/home",
|
||||||
|
icon: Home,
|
||||||
|
label: "Início",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: "/manager/medicos",
|
||||||
|
icon: Users,
|
||||||
|
label: "Médicos",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: "/manager/configuracoes",
|
||||||
|
icon: Settings,
|
||||||
|
label: "Configurações",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const MANAGER_USER_PROFILE: LayoutUserProfile = {
|
||||||
|
name: "Gerente (Placeholder)",
|
||||||
|
secondaryText: "gerente.placeholder@mediconnect.com",
|
||||||
|
avatarFallback: "GP",
|
||||||
|
};
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
const cleanNumber = (value: string): string => value.replace(/\D/g, '');
|
const cleanNumber = (value: string): string => value.replace(/\D/g, '');
|
||||||
@ -102,7 +141,9 @@ const formatPhoneMobile = (value: string): string => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
// COMPONENTE PRINCIPAL
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
export default function NovoMedicoPage() {
|
export default function NovoMedicoPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -201,7 +242,7 @@ export default function NovoMedicoPage() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
const response = await doctorsService.create(finalPayload);
|
const response = await medicosApi.create(finalPayload);
|
||||||
router.push("/manager/home");
|
router.push("/manager/home");
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error("Erro ao salvar o médico:", e);
|
console.error("Erro ao salvar o médico:", e);
|
||||||
@ -210,10 +251,10 @@ export default function NovoMedicoPage() {
|
|||||||
|
|
||||||
|
|
||||||
if (e.message && e.message.includes("duplicate key value violates unique constraint")) {
|
if (e.message && e.message.includes("duplicate key value violates unique constraint")) {
|
||||||
|
|
||||||
detailedError = "O CPF ou CRM informado já está cadastrado no sistema. Por favor, verifique os dados de identificação.";
|
detailedError = "O CPF ou CRM informado já está cadastrado no sistema. Por favor, verifique os dados de identificação.";
|
||||||
} else if (e.message && e.message.includes("Detalhes:")) {
|
} else if (e.message && e.message.includes("Detalhes:")) {
|
||||||
|
|
||||||
detailedError = e.message.split("Detalhes:")[1].trim();
|
detailedError = e.message.split("Detalhes:")[1].trim();
|
||||||
} else if (e.message) {
|
} else if (e.message) {
|
||||||
detailedError = e.message;
|
detailedError = e.message;
|
||||||
@ -226,309 +267,320 @@ export default function NovoMedicoPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ManagerLayout>
|
<ManagerLayout
|
||||||
<div className="w-full space-y-6 p-4 md:p-8">
|
menuItems={MANAGER_MENU_ITEMS}
|
||||||
<div className="flex items-center justify-between">
|
userProfile={MANAGER_USER_PROFILE}
|
||||||
<div>
|
// ADICIONADO: Prop para indicar que esta página não deve ter barra de pesquisa
|
||||||
<h1 className="text-2xl font-bold text-gray-900">Novo Médico</h1>
|
// VOCÊ DEVE GARANTIR QUE ManagerLayout USE ESTA PROP PARA OCULTAR A BARRA DE PESQUISA
|
||||||
<p className="text-sm text-gray-500">
|
hideSearch={true}
|
||||||
Preencha os dados do novo médico para cadastro.
|
>
|
||||||
</p>
|
{/* GARANTINDO W-FULL: O contêiner principal ocupa 100% da largura. */}
|
||||||
</div>
|
<div className="w-full space-y-6 p-4 md:p-8 bg-white min-h-full">
|
||||||
<Link href="/manager/home">
|
|
||||||
<Button variant="outline">Cancelar</Button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
|
||||||
|
|
||||||
{error && (
|
<div className="flex items-center justify-between">
|
||||||
<div className="p-3 bg-red-100 text-red-700 rounded-lg border border-red-300">
|
<div>
|
||||||
<p className="font-medium">Erro no Cadastro:</p>
|
<h1 className="text-2xl font-bold text-gray-900">Novo Médico</h1>
|
||||||
<p className="text-sm">{error}</p>
|
<p className="text-sm text-gray-500">
|
||||||
</div>
|
Preencha os dados do novo médico para cadastro.
|
||||||
)}
|
</p>
|
||||||
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h2 className="text-lg font-semibold text-gray-800 border-b pb-2">
|
|
||||||
Dados Principais e Pessoais
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
|
|
||||||
<div className="grid md:grid-cols-4 gap-4">
|
|
||||||
<div className="space-y-2 col-span-2">
|
|
||||||
<Label htmlFor="nomeCompleto">Nome Completo *</Label>
|
|
||||||
<Input
|
|
||||||
id="nomeCompleto"
|
|
||||||
value={formData.nomeCompleto}
|
|
||||||
onChange={(e) => handleInputChange("nomeCompleto", e.target.value)}
|
|
||||||
placeholder="Nome do Médico"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2 col-span-1">
|
|
||||||
<Label htmlFor="crm">CRM *</Label>
|
|
||||||
<Input
|
|
||||||
id="crm"
|
|
||||||
value={formData.crm}
|
|
||||||
onChange={(e) => handleInputChange("crm", e.target.value)}
|
|
||||||
placeholder="Ex: 123456"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2 col-span-1">
|
|
||||||
<Label htmlFor="crmEstado">UF do CRM *</Label>
|
|
||||||
<Select value={formData.crmEstado} onValueChange={(v) => handleInputChange("crmEstado", v)}>
|
|
||||||
<SelectTrigger id="crmEstado">
|
|
||||||
<SelectValue placeholder="UF" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{UF_LIST.map(uf => (
|
|
||||||
<SelectItem key={uf} value={uf}>{uf}</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div className="grid md:grid-cols-3 gap-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="especialidade">Especialidade</Label>
|
|
||||||
<Input
|
|
||||||
id="especialidade"
|
|
||||||
value={formData.especialidade}
|
|
||||||
onChange={(e) => handleInputChange("especialidade", e.target.value)}
|
|
||||||
placeholder="Ex: Cardiologia"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="cpf">CPF *</Label>
|
|
||||||
<Input
|
|
||||||
id="cpf"
|
|
||||||
value={formData.cpf}
|
|
||||||
onChange={(e) => handleInputChange("cpf", e.target.value)}
|
|
||||||
placeholder="000.000.000-00"
|
|
||||||
maxLength={14}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="rg">RG</Label>
|
|
||||||
<Input
|
|
||||||
id="rg"
|
|
||||||
value={formData.rg}
|
|
||||||
onChange={(e) => handleInputChange("rg", e.target.value)}
|
|
||||||
placeholder="00.000.000-0"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div className="grid md:grid-cols-3 gap-4">
|
|
||||||
<div className="space-y-2 col-span-2">
|
|
||||||
<Label htmlFor="email">E-mail *</Label>
|
|
||||||
<Input
|
|
||||||
id="email"
|
|
||||||
type="email"
|
|
||||||
value={formData.email}
|
|
||||||
onChange={(e) => handleInputChange("email", e.target.value)}
|
|
||||||
placeholder="exemplo@dominio.com"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2 col-span-1">
|
|
||||||
<Label htmlFor="dataNascimento">Data de Nascimento</Label>
|
|
||||||
<Input
|
|
||||||
id="dataNascimento"
|
|
||||||
type="date"
|
|
||||||
value={formData.dataNascimento}
|
|
||||||
onChange={(e) => handleInputChange("dataNascimento", e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<Link href="/manager/home">
|
||||||
|
<Button variant="outline">Cancelar</Button>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
<div className="space-y-4">
|
|
||||||
<h2 className="text-lg font-semibold text-gray-800 border-b pb-2">
|
|
||||||
Contato e Endereço
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
|
{error && (
|
||||||
<div className="grid md:grid-cols-3 gap-4">
|
<div className="p-3 bg-red-100 text-red-700 rounded-lg border border-red-300">
|
||||||
<div className="space-y-2">
|
<p className="font-medium">Erro no Cadastro:</p>
|
||||||
<Label htmlFor="telefoneCelular">Telefone Celular</Label>
|
<p className="text-sm">{error}</p>
|
||||||
<Input
|
</div>
|
||||||
id="telefoneCelular"
|
)}
|
||||||
value={formData.telefoneCelular}
|
|
||||||
onChange={(e) => handleInputChange("telefoneCelular", e.target.value)}
|
|
||||||
placeholder="(00) 00000-0000"
|
<div className="space-y-4">
|
||||||
maxLength={15}
|
<h2 className="text-lg font-semibold text-gray-800 border-b pb-2">
|
||||||
/>
|
Dados Principais e Pessoais
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-4 gap-4">
|
||||||
|
<div className="space-y-2 col-span-2">
|
||||||
|
<Label htmlFor="nomeCompleto">Nome Completo *</Label>
|
||||||
|
<Input
|
||||||
|
id="nomeCompleto"
|
||||||
|
value={formData.nomeCompleto}
|
||||||
|
onChange={(e) => handleInputChange("nomeCompleto", e.target.value)}
|
||||||
|
placeholder="Nome do Médico"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2 col-span-1">
|
||||||
|
<Label htmlFor="crm">CRM *</Label>
|
||||||
|
<Input
|
||||||
|
id="crm"
|
||||||
|
value={formData.crm}
|
||||||
|
onChange={(e) => handleInputChange("crm", e.target.value)}
|
||||||
|
placeholder="Ex: 123456"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2 col-span-1">
|
||||||
|
<Label htmlFor="crmEstado">UF do CRM *</Label>
|
||||||
|
<Select value={formData.crmEstado} onValueChange={(v) => handleInputChange("crmEstado", v)}>
|
||||||
|
<SelectTrigger id="crmEstado">
|
||||||
|
<SelectValue placeholder="UF" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{UF_LIST.map(uf => (
|
||||||
|
<SelectItem key={uf} value={uf}>{uf}</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="telefone2">Telefone Adicional</Label>
|
|
||||||
<Input
|
|
||||||
id="telefone2"
|
<div className="grid md:grid-cols-3 gap-4">
|
||||||
value={formData.telefone2}
|
<div className="space-y-2">
|
||||||
onChange={(e) => handleInputChange("telefone2", e.target.value)}
|
<Label htmlFor="especialidade">Especialidade</Label>
|
||||||
placeholder="(00) 00000-0000"
|
<Input
|
||||||
maxLength={15}
|
id="especialidade"
|
||||||
/>
|
value={formData.especialidade}
|
||||||
|
onChange={(e) => handleInputChange("especialidade", e.target.value)}
|
||||||
|
placeholder="Ex: Cardiologia"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="cpf">CPF *</Label>
|
||||||
|
<Input
|
||||||
|
id="cpf"
|
||||||
|
value={formData.cpf}
|
||||||
|
onChange={(e) => handleInputChange("cpf", e.target.value)}
|
||||||
|
placeholder="000.000.000-00"
|
||||||
|
maxLength={14}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="rg">RG</Label>
|
||||||
|
<Input
|
||||||
|
id="rg"
|
||||||
|
value={formData.rg}
|
||||||
|
onChange={(e) => handleInputChange("rg", e.target.value)}
|
||||||
|
placeholder="00.000.000-0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2 flex items-end justify-center pb-1">
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Checkbox
|
<div className="grid md:grid-cols-3 gap-4">
|
||||||
id="ativo"
|
<div className="space-y-2 col-span-2">
|
||||||
checked={formData.ativo}
|
<Label htmlFor="email">E-mail *</Label>
|
||||||
onCheckedChange={(checked) => handleInputChange("ativo", checked === true)}
|
<Input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
value={formData.email}
|
||||||
|
onChange={(e) => handleInputChange("email", e.target.value)}
|
||||||
|
placeholder="exemplo@dominio.com"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2 col-span-1">
|
||||||
|
<Label htmlFor="dataNascimento">Data de Nascimento</Label>
|
||||||
|
<Input
|
||||||
|
id="dataNascimento"
|
||||||
|
type="date"
|
||||||
|
value={formData.dataNascimento}
|
||||||
|
onChange={(e) => handleInputChange("dataNascimento", e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h2 className="text-lg font-semibold text-gray-800 border-b pb-2">
|
||||||
|
Contato e Endereço
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-3 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="telefoneCelular">Telefone Celular</Label>
|
||||||
|
<Input
|
||||||
|
id="telefoneCelular"
|
||||||
|
value={formData.telefoneCelular}
|
||||||
|
onChange={(e) => handleInputChange("telefoneCelular", e.target.value)}
|
||||||
|
placeholder="(00) 00000-0000"
|
||||||
|
maxLength={15}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="telefone2">Telefone Adicional</Label>
|
||||||
|
<Input
|
||||||
|
id="telefone2"
|
||||||
|
value={formData.telefone2}
|
||||||
|
onChange={(e) => handleInputChange("telefone2", e.target.value)}
|
||||||
|
placeholder="(00) 00000-0000"
|
||||||
|
maxLength={15}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2 flex items-end justify-center pb-1">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
id="ativo"
|
||||||
|
checked={formData.ativo}
|
||||||
|
onCheckedChange={(checked) => handleInputChange("ativo", checked === true)}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="ativo">Médico Ativo</Label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-4 gap-4">
|
||||||
|
<div className="space-y-2 col-span-1">
|
||||||
|
<Label htmlFor="cep">CEP</Label>
|
||||||
|
<Input
|
||||||
|
id="cep"
|
||||||
|
value={formData.cep}
|
||||||
|
onChange={(e) => handleInputChange("cep", e.target.value)}
|
||||||
|
placeholder="00000-000"
|
||||||
|
maxLength={9}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2 col-span-3">
|
||||||
|
<Label htmlFor="endereco">Rua</Label>
|
||||||
|
<Input
|
||||||
|
id="endereco"
|
||||||
|
value={formData.endereco}
|
||||||
|
onChange={(e) => handleInputChange("endereco", e.target.value)}
|
||||||
|
placeholder="Rua, Avenida, etc."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid md:grid-cols-4 gap-4">
|
||||||
|
<div className="space-y-2 col-span-1">
|
||||||
|
<Label htmlFor="numero">Número</Label>
|
||||||
|
<Input
|
||||||
|
id="numero"
|
||||||
|
value={formData.numero}
|
||||||
|
onChange={(e) => handleInputChange("numero", e.target.value)}
|
||||||
|
placeholder="123"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2 col-span-3">
|
||||||
|
<Label htmlFor="complemento">Complemento</Label>
|
||||||
|
<Input
|
||||||
|
id="complemento"
|
||||||
|
value={formData.complemento}
|
||||||
|
onChange={(e) => handleInputChange("complemento", e.target.value)}
|
||||||
|
placeholder="Apto, Bloco, etc."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid md:grid-cols-4 gap-4">
|
||||||
|
<div className="space-y-2 col-span-2">
|
||||||
|
<Label htmlFor="bairro">Bairro</Label>
|
||||||
|
<Input
|
||||||
|
id="bairro"
|
||||||
|
value={formData.bairro}
|
||||||
|
onChange={(e) => handleInputChange("bairro", e.target.value)}
|
||||||
|
placeholder="Bairro"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2 col-span-1">
|
||||||
|
<Label htmlFor="estado">Estado</Label>
|
||||||
|
<Input
|
||||||
|
id="estado"
|
||||||
|
value={formData.estado}
|
||||||
|
onChange={(e) => handleInputChange("estado", e.target.value)}
|
||||||
|
placeholder="SP"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2 col-span-1">
|
||||||
|
<Label htmlFor="cidade">Cidade</Label>
|
||||||
|
<Input
|
||||||
|
id="cidade"
|
||||||
|
value={formData.cidade}
|
||||||
|
onChange={(e) => handleInputChange("cidade", e.target.value)}
|
||||||
|
placeholder="São Paulo"
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="ativo">Médico Ativo</Label>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div className="grid md:grid-cols-4 gap-4">
|
|
||||||
<div className="space-y-2 col-span-1">
|
|
||||||
<Label htmlFor="cep">CEP</Label>
|
|
||||||
<Input
|
|
||||||
id="cep"
|
|
||||||
value={formData.cep}
|
|
||||||
onChange={(e) => handleInputChange("cep", e.target.value)}
|
|
||||||
placeholder="00000-000"
|
|
||||||
maxLength={9}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2 col-span-3">
|
|
||||||
<Label htmlFor="endereco">Rua</Label>
|
|
||||||
<Input
|
|
||||||
id="endereco"
|
|
||||||
value={formData.endereco}
|
|
||||||
onChange={(e) => handleInputChange("endereco", e.target.value)}
|
|
||||||
placeholder="Rua, Avenida, etc."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="grid md:grid-cols-4 gap-4">
|
|
||||||
<div className="space-y-2 col-span-1">
|
|
||||||
<Label htmlFor="numero">Número</Label>
|
|
||||||
<Input
|
|
||||||
id="numero"
|
|
||||||
value={formData.numero}
|
|
||||||
onChange={(e) => handleInputChange("numero", e.target.value)}
|
|
||||||
placeholder="123"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2 col-span-3">
|
|
||||||
<Label htmlFor="complemento">Complemento</Label>
|
|
||||||
<Input
|
|
||||||
id="complemento"
|
|
||||||
value={formData.complemento}
|
|
||||||
onChange={(e) => handleInputChange("complemento", e.target.value)}
|
|
||||||
placeholder="Apto, Bloco, etc."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="grid md:grid-cols-4 gap-4">
|
|
||||||
<div className="space-y-2 col-span-2">
|
|
||||||
<Label htmlFor="bairro">Bairro</Label>
|
|
||||||
<Input
|
|
||||||
id="bairro"
|
|
||||||
value={formData.bairro}
|
|
||||||
onChange={(e) => handleInputChange("bairro", e.target.value)}
|
|
||||||
placeholder="Bairro"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2 col-span-1">
|
|
||||||
<Label htmlFor="estado">Estado</Label>
|
|
||||||
<Input
|
|
||||||
id="estado"
|
|
||||||
value={formData.estado}
|
|
||||||
onChange={(e) => handleInputChange("estado", e.target.value)}
|
|
||||||
placeholder="SP"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2 col-span-1">
|
|
||||||
<Label htmlFor="cidade">Cidade</Label>
|
|
||||||
<Input
|
|
||||||
id="cidade"
|
|
||||||
value={formData.cidade}
|
|
||||||
onChange={(e) => handleInputChange("cidade", e.target.value)}
|
|
||||||
placeholder="São Paulo"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h2 className="text-lg font-semibold text-gray-800 border-b pb-2">
|
|
||||||
Outras Informações (Internas)
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div className="grid md:grid-cols-2 gap-4">
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-4">
|
||||||
<Label htmlFor="observacoes">Observações (Apenas internas)</Label>
|
<h2 className="text-lg font-semibold text-gray-800 border-b pb-2">
|
||||||
<Textarea
|
Outras Informações (Internas)
|
||||||
id="observacoes"
|
</h2>
|
||||||
value={formData.observacoes}
|
|
||||||
onChange={(e) => handleInputChange("observacoes", e.target.value)}
|
<div className="grid md:grid-cols-2 gap-4">
|
||||||
placeholder="Notas internas sobre o médico..."
|
<div className="space-y-2">
|
||||||
className="min-h-[100px]"
|
<Label htmlFor="observacoes">Observações (Apenas internas)</Label>
|
||||||
/>
|
<Textarea
|
||||||
</div>
|
id="observacoes"
|
||||||
<div className="space-y-4">
|
value={formData.observacoes}
|
||||||
<Collapsible open={anexosOpen} onOpenChange={setAnexosOpen}>
|
onChange={(e) => handleInputChange("observacoes", e.target.value)}
|
||||||
<CollapsibleTrigger asChild>
|
placeholder="Notas internas sobre o médico..."
|
||||||
<div className="flex justify-between items-center cursor-pointer pb-2 border-b">
|
className="min-h-[100px]"
|
||||||
<h2 className="text-md font-semibold text-gray-800">Anexos ({formData.anexos.length})</h2>
|
/>
|
||||||
<ChevronDown className={`w-5 h-5 transition-transform ${anexosOpen ? 'rotate-180' : 'rotate-0'}`} />
|
</div>
|
||||||
</div>
|
<div className="space-y-4">
|
||||||
</CollapsibleTrigger>
|
<Collapsible open={anexosOpen} onOpenChange={setAnexosOpen}>
|
||||||
<CollapsibleContent className="space-y-4 pt-2">
|
<CollapsibleTrigger asChild>
|
||||||
<Button type="button" onClick={adicionarAnexo} variant="outline" className="w-full">
|
<div className="flex justify-between items-center cursor-pointer pb-2 border-b">
|
||||||
<Upload className="w-4 h-4 mr-2" />
|
<h2 className="text-md font-semibold text-gray-800">Anexos ({formData.anexos.length})</h2>
|
||||||
Adicionar Documento
|
<ChevronDown className={`w-5 h-5 transition-transform ${anexosOpen ? 'rotate-180' : 'rotate-0'}`} />
|
||||||
</Button>
|
</div>
|
||||||
{formData.anexos.map((anexo) => (
|
</CollapsibleTrigger>
|
||||||
<div key={anexo.id} className="flex items-center justify-between p-3 bg-gray-50 border rounded-lg">
|
<CollapsibleContent className="space-y-4 pt-2">
|
||||||
<span className="text-sm text-gray-700">{anexo.name}</span>
|
<Button type="button" onClick={adicionarAnexo} variant="outline" className="w-full">
|
||||||
<Button type="button" variant="ghost" size="icon" onClick={() => removerAnexo(anexo.id)}>
|
<Upload className="w-4 h-4 mr-2" />
|
||||||
<X className="w-4 h-4 text-red-500" />
|
Adicionar Documento
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
{formData.anexos.map((anexo) => (
|
||||||
))}
|
<div key={anexo.id} className="flex items-center justify-between p-3 bg-gray-50 border rounded-lg">
|
||||||
</CollapsibleContent>
|
<span className="text-sm text-gray-700">{anexo.name}</span>
|
||||||
</Collapsible>
|
<Button type="button" variant="ghost" size="icon" onClick={() => removerAnexo(anexo.id)}>
|
||||||
|
<X className="w-4 h-4 text-red-500" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div className="flex justify-end gap-4 pb-8 pt-4">
|
<div className="flex justify-end gap-4 pb-8 pt-4">
|
||||||
<Link href="/manager/home">
|
<Link href="/manager/home">
|
||||||
<Button type="button" variant="outline" disabled={isSaving}>
|
<Button type="button" variant="outline" disabled={isSaving}>
|
||||||
Cancelar
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="bg-green-600 hover:bg-green-700"
|
||||||
|
disabled={isSaving}
|
||||||
|
>
|
||||||
|
{isSaving ? (
|
||||||
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Save className="w-4 h-4 mr-2" />
|
||||||
|
)}
|
||||||
|
{isSaving ? "Salvando..." : "Salvar Médico"}
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</div>
|
||||||
<Button
|
</form>
|
||||||
type="submit"
|
</div>
|
||||||
className="bg-green-600 hover:bg-green-700"
|
|
||||||
disabled={isSaving}
|
|
||||||
>
|
|
||||||
{isSaving ? (
|
|
||||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<Save className="w-4 h-4 mr-2" />
|
|
||||||
)}
|
|
||||||
{isSaving ? "Salvando..." : "Salvar Médico"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</ManagerLayout>
|
</ManagerLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1,16 +1,26 @@
|
|||||||
// Caminho: app/(manager)/layout.tsx
|
// app/manager/layout.tsx
|
||||||
|
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type React from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useState, useEffect } from "react";
|
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
// Nossas importações centralizadas
|
|
||||||
import { usuariosApi } from "@/services/usuariosApi";
|
import { usuariosApi } from "@/services/usuariosApi";
|
||||||
import DashboardLayout, { UserProfile } from "@/components/layout/DashboardLayout";
|
import DashboardLayout from "@/components/layout/DashboardLayout";
|
||||||
import { dashboardConfig } from "@/config/dashboard.config";
|
import { dashboardConfig } from "@/config/dashboard.config";
|
||||||
|
|
||||||
|
interface UserData {
|
||||||
|
id?: string | number;
|
||||||
|
email?: string;
|
||||||
|
full_name?: string;
|
||||||
|
[k: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
// mesmo tipo que o DashboardLayout espera
|
||||||
|
interface UserProfile {
|
||||||
|
name: string;
|
||||||
|
secondaryText: string;
|
||||||
|
avatarFallback: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface ManagerLayoutProps {
|
interface ManagerLayoutProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
@ -18,36 +28,67 @@ interface ManagerLayoutProps {
|
|||||||
export default function ManagerLayout({ children }: ManagerLayoutProps) {
|
export default function ManagerLayout({ children }: ManagerLayoutProps) {
|
||||||
const [userProfile, setUserProfile] = useState<UserProfile | null>(null);
|
const [userProfile, setUserProfile] = useState<UserProfile | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkAuthentication = async () => {
|
let mounted = true;
|
||||||
try {
|
|
||||||
// 1. Busca o usuário logado via API
|
|
||||||
const userData = await usuariosApi.getCurrentUser();
|
|
||||||
|
|
||||||
// 2. Pega a configuração específica do "gestor"
|
const fetchCurrentUser = async () => {
|
||||||
const config = dashboardConfig.manager;
|
setIsLoading(true);
|
||||||
if (!config) {
|
setError(null);
|
||||||
throw new Error("Configuração para o perfil 'manager' não encontrada.");
|
|
||||||
|
try {
|
||||||
|
const userData: UserData = await usuariosApi.getCurrentUser();
|
||||||
|
const cfg = dashboardConfig?.manager;
|
||||||
|
|
||||||
|
let profile: UserProfile;
|
||||||
|
|
||||||
|
if (cfg && typeof cfg.getUserProfile === "function") {
|
||||||
|
const mapped = cfg.getUserProfile(userData);
|
||||||
|
|
||||||
|
// Garante compatibilidade com o tipo exigido pelo DashboardLayout
|
||||||
|
profile = {
|
||||||
|
name: mapped?.name ?? userData.full_name ?? "Usuário",
|
||||||
|
secondaryText: mapped?.secondaryText ?? userData.email ?? "",
|
||||||
|
avatarFallback:
|
||||||
|
mapped?.avatarFallback ??
|
||||||
|
(userData.full_name
|
||||||
|
? userData.full_name.charAt(0).toUpperCase()
|
||||||
|
: "U"),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// fallback simples
|
||||||
|
profile = {
|
||||||
|
name: userData.full_name ?? "Usuário",
|
||||||
|
secondaryText: userData.email ?? "",
|
||||||
|
avatarFallback: userData.full_name
|
||||||
|
? userData.full_name.charAt(0).toUpperCase()
|
||||||
|
: "U",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Formata os dados para o perfil
|
if (mounted) setUserProfile(profile);
|
||||||
setUserProfile(config.getUserProfile(userData));
|
} catch (err: any) {
|
||||||
|
console.error("Erro autenticação (manager layout):", err);
|
||||||
} catch (error) {
|
if (mounted) {
|
||||||
// 4. Se falhar, redireciona para o login
|
setError(err?.message ?? "Erro ao autenticar");
|
||||||
console.error("Falha na autenticação para gestor:", error);
|
try {
|
||||||
router.push("/login");
|
router.push("/login");
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
if (mounted) setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
checkAuthentication();
|
fetchCurrentUser();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
mounted = false;
|
||||||
|
};
|
||||||
}, [router]);
|
}, [router]);
|
||||||
|
|
||||||
// Enquanto a verificação estiver em andamento, mostra uma tela de carregamento
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen w-full items-center justify-center bg-background">
|
<div className="flex h-screen w-full items-center justify-center bg-background">
|
||||||
@ -56,18 +97,24 @@ export default function ManagerLayout({ children }: ManagerLayoutProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Se não tiver perfil (redirect em andamento), não renderiza nada para evitar erros
|
if (error) {
|
||||||
if (!userProfile) {
|
return (
|
||||||
return null;
|
<div className="flex h-screen w-full items-center justify-center bg-background p-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-destructive mb-2">Erro: {error}</p>
|
||||||
|
<p className="text-sm text-muted-foreground">Redirecionando...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pega os itens de menu da configuração
|
if (!userProfile) return null;
|
||||||
const menuItems = dashboardConfig.manager.menuItems;
|
|
||||||
|
const menuItems = dashboardConfig?.manager?.menuItems ?? [];
|
||||||
|
|
||||||
// Renderiza o layout genérico com as props corretas
|
|
||||||
return (
|
return (
|
||||||
<DashboardLayout menuItems={menuItems} userProfile={userProfile}>
|
<DashboardLayout menuItems={menuItems} userProfile={userProfile}>
|
||||||
{children}
|
{children}
|
||||||
</DashboardLayout>
|
</DashboardLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,279 +1,87 @@
|
|||||||
"use client"
|
// app/manager/usuario/[id]/editar/page.tsx
|
||||||
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react"
|
import React, { useEffect, useState } from "react";
|
||||||
import { useRouter, useParams } from "next/navigation"
|
import { useParams, useRouter } from "next/navigation";
|
||||||
import Link from "next/link"
|
import { usuariosApi } from "@/services/usuariosApi";
|
||||||
import { Button } from "@/components/ui/button"
|
import { perfisApi } from "@/services/perfisApi";
|
||||||
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, ArrowLeft } from "lucide-react"
|
|
||||||
import ManagerLayout from "@/components/manager-layout"
|
|
||||||
|
|
||||||
// Mock user service for demonstration. Replace with your actual API service.
|
interface Profile {
|
||||||
const usersService = {
|
id?: number | string;
|
||||||
getById: async (id: string): Promise<any> => {
|
full_name?: string;
|
||||||
console.log(`API Call: Fetching user with ID ${id}`);
|
email?: string;
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
[k: string]: any;
|
||||||
// This mock finds a user from a predefined list.
|
|
||||||
const mockUsers = [
|
|
||||||
{ id: 1, full_name: 'Alice Admin', email: 'alice.admin@example.com', phone: '(11) 98765-4321', role: 'admin' },
|
|
||||||
{ id: 2, full_name: 'Bruno Gestor', email: 'bruno.g@example.com', phone: '(21) 91234-5678', role: 'gestor' },
|
|
||||||
{ id: 3, full_name: 'Dr. Carlos Médico', email: 'carlos.med@example.com', phone: null, role: 'medico' },
|
|
||||||
{ id: 4, full_name: 'Daniela Secretaria', email: 'daniela.sec@example.com', phone: '(31) 99999-8888', role: 'secretaria' },
|
|
||||||
{ id: 5, full_name: 'Eduardo Usuário', email: 'edu.user@example.com', phone: '(41) 98888-7777', role: 'user' },
|
|
||||||
];
|
|
||||||
const user = mockUsers.find(u => u.id.toString() === id);
|
|
||||||
if (!user) throw new Error("Usuário não encontrado.");
|
|
||||||
return user;
|
|
||||||
},
|
|
||||||
update: async (id: string, payload: any): Promise<void> => {
|
|
||||||
console.log(`API Call: Updating user ${id} with payload:`, payload);
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
||||||
// To simulate an error (e.g., duplicate email), you could throw an error here:
|
|
||||||
// if (payload.email === 'bruno.g@example.com') throw new Error("Este e-mail já está em uso por outro usuário.");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Interface for the user form data
|
|
||||||
interface UserFormData {
|
|
||||||
nomeCompleto: string;
|
|
||||||
email: string;
|
|
||||||
telefone: string;
|
|
||||||
papel: string;
|
|
||||||
password?: string; // Optional for password updates
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default state for the form
|
export default function ManagerUsuarioEditPage() {
|
||||||
const defaultFormData: UserFormData = {
|
|
||||||
nomeCompleto: '',
|
|
||||||
email: '',
|
|
||||||
telefone: '',
|
|
||||||
papel: '',
|
|
||||||
password: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helper functions for phone formatting
|
|
||||||
const cleanNumber = (value: string): string => value.replace(/\D/g, '');
|
|
||||||
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');
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function EditarUsuarioPage() {
|
|
||||||
const router = useRouter();
|
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const id = Array.isArray(params.id) ? params.id[0] : params.id;
|
const id = params?.id;
|
||||||
|
const router = useRouter();
|
||||||
const [formData, setFormData] = useState<UserFormData>(defaultFormData);
|
|
||||||
const [loading, setLoading] = useState(true);
|
const [profile, setProfile] = useState<Profile | null>(null);
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||||
|
const [isSaving, setIsSaving] = useState<boolean>(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
// Map API field names to our form field names
|
|
||||||
const apiToFormMap: { [key: string]: keyof UserFormData } = {
|
|
||||||
'full_name': 'nomeCompleto',
|
|
||||||
'email': 'email',
|
|
||||||
'phone': 'telefone',
|
|
||||||
'role': 'papel'
|
|
||||||
};
|
|
||||||
|
|
||||||
// Fetch user data when the component mounts
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!id) return;
|
let mounted = true;
|
||||||
|
const load = async () => {
|
||||||
const fetchUser = async () => {
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const data = await usersService.getById(id);
|
if (!id) throw new Error("ID ausente");
|
||||||
if (!data) {
|
const full = await usuariosApi.getFullData(String(id));
|
||||||
setError("Usuário não encontrado.");
|
// getFullData pode retornar objeto com profile
|
||||||
setLoading(false);
|
const prof = (full && full.profile) ? full.profile : full;
|
||||||
return;
|
if (mounted) setProfile(prof ?? null);
|
||||||
}
|
} catch (err: any) {
|
||||||
|
console.error("Erro ao buscar usuário:", err);
|
||||||
const initialData: Partial<UserFormData> = {};
|
if (mounted) setError(err?.message ?? "Erro ao buscar usuário");
|
||||||
Object.keys(data).forEach(key => {
|
|
||||||
const formKey = apiToFormMap[key];
|
|
||||||
if (formKey) {
|
|
||||||
initialData[formKey] = data[key] === null ? '' : String(data[key]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
setFormData(prev => ({ ...prev, ...initialData }));
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error("Erro ao carregar dados do usuário:", e);
|
|
||||||
setError(e.message || "Não foi possível carregar os dados do usuário.");
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
if (mounted) setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
fetchUser();
|
load();
|
||||||
}, [id]);
|
return () => { mounted = false; };
|
||||||
|
}, [id]);
|
||||||
const handleInputChange = (key: keyof UserFormData, value: string) => {
|
|
||||||
const updatedValue = key === 'telefone' ? formatPhone(value) : value;
|
|
||||||
setFormData((prev) => ({ ...prev, [key]: updatedValue }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setError(null);
|
if (!id || !profile) return;
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
|
setError(null);
|
||||||
if (!id) {
|
|
||||||
setError("ID do usuário ausente.");
|
|
||||||
setIsSaving(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare payload for the API
|
|
||||||
const payload: { [key: string]: any } = {
|
|
||||||
full_name: formData.nomeCompleto,
|
|
||||||
email: formData.email,
|
|
||||||
phone: formData.telefone.trim() || null,
|
|
||||||
role: formData.papel,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Only include the password in the payload if it has been changed
|
|
||||||
if (formData.password && formData.password.trim() !== '') {
|
|
||||||
payload.password = formData.password;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await usersService.update(id, payload);
|
await perfisApi.update(String(id), profile);
|
||||||
router.push("/manager/usuario");
|
router.push("/manager/usuario");
|
||||||
} catch (e: any) {
|
} catch (err: any) {
|
||||||
console.error("Erro ao salvar o usuário:", e);
|
console.error("Erro ao atualizar perfil:", err);
|
||||||
setError(e.message || "Ocorreu um erro inesperado ao atualizar.");
|
setError(err?.message ?? "Erro ao salvar");
|
||||||
} finally {
|
} finally {
|
||||||
setIsSaving(false);
|
setIsSaving(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (isLoading) return <div className="p-8">Carregando...</div>;
|
||||||
return (
|
if (error) return <div className="p-8 text-destructive">Erro: {error}</div>;
|
||||||
<ManagerLayout>
|
if (!profile) return <div className="p-8">Usuário não encontrado.</div>;
|
||||||
<div className="flex justify-center items-center h-full w-full py-16">
|
|
||||||
<Loader2 className="w-8 h-8 animate-spin text-green-600" />
|
|
||||||
<p className="ml-2 text-gray-600">Carregando dados do usuário...</p>
|
|
||||||
</div>
|
|
||||||
</ManagerLayout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ManagerLayout>
|
<main className="w-full p-4 md:p-8">
|
||||||
<div className="w-full max-w-2xl mx-auto space-y-6 p-4 md:p-8">
|
<div className="max-w-screen-md mx-auto">
|
||||||
<div className="flex items-center justify-between">
|
<h1 className="text-2xl font-bold mb-4">Editar Usuário</h1>
|
||||||
<div>
|
|
||||||
<h1 className="text-2xl font-bold text-gray-900">
|
<form onSubmit={handleSubmit} className="space-y-4 bg-white p-6 border rounded">
|
||||||
Editar Usuário: <span className="text-green-600">{formData.nomeCompleto}</span>
|
<div>
|
||||||
</h1>
|
<label className="block text-sm">Nome completo</label>
|
||||||
<p className="text-sm text-gray-500">
|
<input value={profile.full_name ?? ""} onChange={(e) => setProfile({ ...profile, full_name: e.target.value })} required className="w-full" />
|
||||||
Atualize as informações do usuário (ID: {id}).
|
</div>
|
||||||
</p>
|
|
||||||
</div>
|
<div className="flex justify-end">
|
||||||
<Link href="/manager/usuario">
|
<button type="button" onClick={() => router.push("/manager/usuario")} className="mr-2">Cancelar</button>
|
||||||
<Button variant="outline">
|
<button type="submit" disabled={isSaving}>{isSaving ? "Salvando..." : "Salvar"}</button>
|
||||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
</div>
|
||||||
Voltar
|
</form>
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
|
</main>
|
||||||
<form onSubmit={handleSubmit} className="space-y-8 bg-white p-8 border rounded-lg shadow-sm">
|
|
||||||
{error && (
|
|
||||||
<div className="p-3 bg-red-100 text-red-700 rounded-lg border border-red-300">
|
|
||||||
<p className="font-medium">Erro na Atualização:</p>
|
|
||||||
<p className="text-sm">{error}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="nomeCompleto">Nome Completo</Label>
|
|
||||||
<Input
|
|
||||||
id="nomeCompleto"
|
|
||||||
value={formData.nomeCompleto}
|
|
||||||
onChange={(e) => handleInputChange("nomeCompleto", e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid md:grid-cols-2 gap-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="email">E-mail</Label>
|
|
||||||
<Input
|
|
||||||
id="email"
|
|
||||||
type="email"
|
|
||||||
value={formData.email}
|
|
||||||
onChange={(e) => handleInputChange("email", e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="password">Nova Senha</Label>
|
|
||||||
<Input
|
|
||||||
id="password"
|
|
||||||
type="password"
|
|
||||||
value={formData.password}
|
|
||||||
onChange={(e) => handleInputChange("password", e.target.value)}
|
|
||||||
placeholder="Deixe em branco para não alterar"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid md:grid-cols-2 gap-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="telefone">Telefone</Label>
|
|
||||||
<Input
|
|
||||||
id="telefone"
|
|
||||||
value={formData.telefone}
|
|
||||||
onChange={(e) => handleInputChange("telefone", e.target.value)}
|
|
||||||
placeholder="(00) 00000-0000"
|
|
||||||
maxLength={15}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="papel">Papel (Função)</Label>
|
|
||||||
<Select value={formData.papel} onValueChange={(v) => handleInputChange("papel", v)}>
|
|
||||||
<SelectTrigger id="papel">
|
|
||||||
<SelectValue placeholder="Selecione uma função" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="admin">Admin</SelectItem>
|
|
||||||
<SelectItem value="gestor">Gestor</SelectItem>
|
|
||||||
<SelectItem value="medico">Médico</SelectItem>
|
|
||||||
<SelectItem value="secretaria">Secretaria</SelectItem>
|
|
||||||
<SelectItem value="user">Usuário</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end gap-4 pt-4">
|
|
||||||
<Link href="/manager/usuario">
|
|
||||||
<Button type="button" variant="outline" disabled={isSaving}>
|
|
||||||
Cancelar
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
className="bg-green-600 hover:bg-green-700"
|
|
||||||
disabled={isSaving}
|
|
||||||
>
|
|
||||||
{isSaving ? (
|
|
||||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<Save className="w-4 h-4 mr-2" />
|
|
||||||
)}
|
|
||||||
{isSaving ? "Salvando..." : "Salvar Alterações"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</ManagerLayout>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Caminho: manager/usuario/novo/page.tsx (Refatorado)
|
// Caminho: manager/usuario/novo/page.tsx
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
@ -7,11 +7,19 @@ import Link from "next/link";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
import { Save, Loader2 } from "lucide-react";
|
import { Save, Loader2 } from "lucide-react";
|
||||||
import ManagerLayout from "@/components/manager-layout";
|
// 🔧 Correção: importava DashboardLayout, mas deve importar ManagerLayout
|
||||||
import { usuariosService } from "@/services/usuariosApi"; // Alterado
|
import ManagerLayout from "@/app/manager/layout";
|
||||||
import { login } from "services/api"; // Este import parece incorreto
|
import { usuariosApi } from "@/services/usuariosApi";
|
||||||
|
// Removido import incorreto
|
||||||
|
// import { api } from "services/api";
|
||||||
|
|
||||||
interface UserFormData {
|
interface UserFormData {
|
||||||
email: string;
|
email: string;
|
||||||
@ -22,12 +30,22 @@ interface UserFormData {
|
|||||||
confirmarSenha: string;
|
confirmarSenha: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultFormData: UserFormData = { email: "", nomeCompleto: "", telefone: "", papel: "", senha: "", confirmarSenha: "" };
|
const defaultFormData: UserFormData = {
|
||||||
|
email: "",
|
||||||
|
nomeCompleto: "",
|
||||||
|
telefone: "",
|
||||||
|
papel: "",
|
||||||
|
senha: "",
|
||||||
|
confirmarSenha: "",
|
||||||
|
};
|
||||||
|
|
||||||
const cleanNumber = (value: string): string => value.replace(/\D/g, "");
|
const cleanNumber = (value: string): string => value.replace(/\D/g, "");
|
||||||
const formatPhone = (value: string): string => {
|
const formatPhone = (value: string): string => {
|
||||||
const cleaned = cleanNumber(value).substring(0, 11);
|
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 === 11)
|
||||||
if (cleaned.length === 10) return cleaned.replace(/(\d{2})(\d{4})(\d{4})/, "($1) $2-$3");
|
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;
|
return cleaned;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -45,7 +63,13 @@ export default function NovoUsuarioPage() {
|
|||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setError(null);
|
setError(null);
|
||||||
if (!formData.email || !formData.nomeCompleto || !formData.papel || !formData.senha || !formData.confirmarSenha) {
|
if (
|
||||||
|
!formData.email ||
|
||||||
|
!formData.nomeCompleto ||
|
||||||
|
!formData.papel ||
|
||||||
|
!formData.senha ||
|
||||||
|
!formData.confirmarSenha
|
||||||
|
) {
|
||||||
setError("Por favor, preencha todos os campos obrigatórios.");
|
setError("Por favor, preencha todos os campos obrigatórios.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -55,19 +79,25 @@ export default function NovoUsuarioPage() {
|
|||||||
}
|
}
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
try {
|
try {
|
||||||
await login(); // Este login pode precisar ser ajustado para autenticacaoApi.ts
|
const payload = {
|
||||||
const payload = {
|
email: formData.email.trim().toLowerCase(),
|
||||||
full_name: formData.nomeCompleto,
|
full_name: formData.nomeCompleto,
|
||||||
email: formData.email.trim().toLowerCase(),
|
phone: formData.telefone || null,
|
||||||
phone: formData.telefone || null,
|
phone_mobile: formData.telefone || null,
|
||||||
role: formData.papel,
|
role: formData.papel || "paciente",
|
||||||
password: formData.senha,
|
cpf: "00000000000",
|
||||||
};
|
create_patient_record: true
|
||||||
await usuariosService.createUser(payload); // Alterado
|
};
|
||||||
|
|
||||||
|
|
||||||
|
await usuariosApi.createUser(payload);
|
||||||
router.push("/manager/usuario");
|
router.push("/manager/usuario");
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error("Erro ao criar usuário:", e);
|
console.error("Erro ao criar usuário:", e);
|
||||||
setError(e?.message || "Não foi possível criar o usuário. Verifique os dados e tente novamente.");
|
setError(
|
||||||
|
e?.message ||
|
||||||
|
"Não foi possível criar o usuário. Verifique os dados e tente novamente."
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setIsSaving(false);
|
setIsSaving(false);
|
||||||
}
|
}
|
||||||
@ -75,33 +105,149 @@ export default function NovoUsuarioPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ManagerLayout>
|
<ManagerLayout>
|
||||||
{/* O JSX restante permanece exatamente o mesmo */}
|
<div className="w-full h-full p-4 md:p-8 flex justify-center items-start">
|
||||||
<div className="w-full h-full p-4 md:p-8 flex justify-center items-start">
|
<div className="w-full max-w-screen-lg space-y-8">
|
||||||
<div className="w-full max-w-screen-lg space-y-8">
|
<div className="flex items-center justify-between border-b pb-4">
|
||||||
<div className="flex items-center justify-between border-b pb-4">
|
<div>
|
||||||
<div>
|
<h1 className="text-3xl font-extrabold text-gray-900">
|
||||||
<h1 className="text-3xl font-extrabold text-gray-900">Novo Usuário</h1>
|
Novo Usuário
|
||||||
<p className="text-md text-gray-500">Preencha os dados para cadastrar um novo usuário no sistema.</p>
|
</h1>
|
||||||
</div>
|
<p className="text-md text-gray-500">
|
||||||
<Link href="/manager/usuario"><Button variant="outline">Cancelar</Button></Link>
|
Preencha os dados para cadastrar um novo usuário no sistema.
|
||||||
</div>
|
</p>
|
||||||
<form onSubmit={handleSubmit} className="space-y-6 bg-white p-6 md:p-10 border rounded-xl shadow-lg">
|
|
||||||
{error && <div className="p-4 bg-red-50 text-red-700 rounded-lg border border-red-300"><p className="font-semibold">Erro no Cadastro:</p><p className="text-sm break-words">{error}</p></div>}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
||||||
<div className="space-y-2 md:col-span-2"><Label htmlFor="nomeCompleto">Nome Completo *</Label><Input id="nomeCompleto" value={formData.nomeCompleto} onChange={(e) => handleInputChange("nomeCompleto", e.target.value)} placeholder="Nome e Sobrenome" required /></div>
|
|
||||||
<div className="space-y-2"><Label htmlFor="email">E-mail *</Label><Input id="email" type="email" value={formData.email} onChange={(e) => handleInputChange("email", e.target.value)} placeholder="exemplo@dominio.com" required /></div>
|
|
||||||
<div className="space-y-2"><Label htmlFor="papel">Papel (Função) *</Label><Select value={formData.papel} onValueChange={(v) => handleInputChange("papel", v)} required><SelectTrigger id="papel"><SelectValue placeholder="Selecione uma função" /></SelectTrigger><SelectContent><SelectItem value="admin">Administrador</SelectItem><SelectItem value="gestor">Gestor</SelectItem><SelectItem value="medico">Médico</SelectItem><SelectItem value="secretaria">Secretária</SelectItem><SelectItem value="user">Usuário</SelectItem></SelectContent></Select></div>
|
|
||||||
<div className="space-y-2"><Label htmlFor="senha">Senha *</Label><Input id="senha" type="password" value={formData.senha} onChange={(e) => handleInputChange("senha", e.target.value)} placeholder="Mínimo 8 caracteres" minLength={8} required /></div>
|
|
||||||
<div className="space-y-2"><Label htmlFor="confirmarSenha">Confirmar Senha *</Label><Input id="confirmarSenha" type="password" value={formData.confirmarSenha} onChange={(e) => handleInputChange("confirmarSenha", e.target.value)} placeholder="Repita a senha" required />{formData.senha && formData.confirmarSenha && formData.senha !== formData.confirmarSenha && <p className="text-xs text-red-500">As senhas não coincidem.</p>}</div>
|
|
||||||
<div className="space-y-2"><Label htmlFor="telefone">Telefone</Label><Input id="telefone" value={formData.telefone} onChange={(e) => handleInputChange("telefone", e.target.value)} placeholder="(00) 00000-0000" maxLength={15} /></div>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-end gap-4 pt-6 border-t mt-6">
|
|
||||||
<Link href="/manager/usuario"><Button type="button" variant="outline" disabled={isSaving}>Cancelar</Button></Link>
|
|
||||||
<Button type="submit" className="bg-green-600 hover:bg-green-700" disabled={isSaving}>{isSaving ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <Save className="w-4 h-4 mr-2" />}{isSaving ? "Salvando..." : "Salvar Usuário"}</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
|
<Link href="/manager/usuario">
|
||||||
|
<Button variant="outline">Cancelar</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
className="space-y-6 bg-white p-6 md:p-10 border rounded-xl shadow-lg"
|
||||||
|
>
|
||||||
|
{error && (
|
||||||
|
<div className="p-4 bg-red-50 text-red-700 rounded-lg border border-red-300">
|
||||||
|
<p className="font-semibold">Erro no Cadastro:</p>
|
||||||
|
<p className="text-sm break-words">{error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div className="space-y-2 md:col-span-2">
|
||||||
|
<Label htmlFor="nomeCompleto">Nome Completo *</Label>
|
||||||
|
<Input
|
||||||
|
id="nomeCompleto"
|
||||||
|
value={formData.nomeCompleto}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleInputChange("nomeCompleto", e.target.value)
|
||||||
|
}
|
||||||
|
placeholder="Nome e Sobrenome"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="email">E-mail *</Label>
|
||||||
|
<Input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
value={formData.email}
|
||||||
|
onChange={(e) => handleInputChange("email", e.target.value)}
|
||||||
|
placeholder="exemplo@dominio.com"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="papel">Papel (Função) *</Label>
|
||||||
|
<Select
|
||||||
|
value={formData.papel}
|
||||||
|
onValueChange={(v) => handleInputChange("papel", v)}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<SelectTrigger id="papel">
|
||||||
|
<SelectValue placeholder="Selecione uma função" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="admin">Administrador</SelectItem>
|
||||||
|
<SelectItem value="gestor">Gestor</SelectItem>
|
||||||
|
<SelectItem value="medico">Médico</SelectItem>
|
||||||
|
<SelectItem value="secretaria">Secretária</SelectItem>
|
||||||
|
<SelectItem value="user">Paciente</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="senha">Senha *</Label>
|
||||||
|
<Input
|
||||||
|
id="senha"
|
||||||
|
type="password"
|
||||||
|
value={formData.senha}
|
||||||
|
onChange={(e) => handleInputChange("senha", e.target.value)}
|
||||||
|
placeholder="Mínimo 8 caracteres"
|
||||||
|
minLength={8}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="confirmarSenha">Confirmar Senha *</Label>
|
||||||
|
<Input
|
||||||
|
id="confirmarSenha"
|
||||||
|
type="password"
|
||||||
|
value={formData.confirmarSenha}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleInputChange("confirmarSenha", e.target.value)
|
||||||
|
}
|
||||||
|
placeholder="Repita a senha"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
{formData.senha &&
|
||||||
|
formData.confirmarSenha &&
|
||||||
|
formData.senha !== formData.confirmarSenha && (
|
||||||
|
<p className="text-xs text-red-500">
|
||||||
|
As senhas não coincidem.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="telefone">Telefone</Label>
|
||||||
|
<Input
|
||||||
|
id="telefone"
|
||||||
|
value={formData.telefone}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleInputChange("telefone", e.target.value)
|
||||||
|
}
|
||||||
|
placeholder="(00) 00000-0000"
|
||||||
|
maxLength={15}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-4 pt-6 border-t mt-6">
|
||||||
|
<Link href="/manager/usuario">
|
||||||
|
<Button type="button" variant="outline" disabled={isSaving}>
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="bg-green-600 hover:bg-green-700"
|
||||||
|
disabled={isSaving}
|
||||||
|
>
|
||||||
|
{isSaving ? (
|
||||||
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Save className="w-4 h-4 mr-2" />
|
||||||
|
)}
|
||||||
|
{isSaving ? "Salvando..." : "Salvar Usuário"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</ManagerLayout>
|
</ManagerLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,18 @@
|
|||||||
// Caminho: app/manager/usuario/page.tsx (Refatorado)
|
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useEffect, useState, useCallback } from "react";
|
import React, { useEffect, useState, useCallback } from "react";
|
||||||
import ManagerLayout from "@/components/manager-layout";
|
// REMOVIDO: import ManagerLayout, pois a página já é envolvida pelo layout pai.
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Plus, Eye, Filter, Loader2 } from "lucide-react";
|
import { Plus, Eye, Filter, Loader2 } from "lucide-react";
|
||||||
import { AlertDialog, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
|
import { AlertDialog, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
|
||||||
import { login } from "services/api"; // Este import parece incorreto, api.ts não exporta login.
|
|
||||||
import { usuariosService } from "@/services/usuariosApi"; // Alterado
|
// Assumindo caminhos de importação do seu projeto
|
||||||
import { perfisService } from "@/services/perfisApi"; // Adicionado
|
import { api } from "services/api";
|
||||||
|
import { usuariosApi } from "@/services/usuariosApi";
|
||||||
|
import { perfisApi } from "@/services/perfisApi";
|
||||||
|
import { UserRole } from "@/services/usuariosApi";
|
||||||
|
|
||||||
interface FlatUser {
|
interface FlatUser {
|
||||||
id: string;
|
id: string;
|
||||||
@ -35,6 +37,14 @@ export default function UsersPage() {
|
|||||||
const [detailsDialogOpen, setDetailsDialogOpen] = useState(false);
|
const [detailsDialogOpen, setDetailsDialogOpen] = useState(false);
|
||||||
const [userDetails, setUserDetails] = useState<UserInfoResponse | null>(null);
|
const [userDetails, setUserDetails] = useState<UserInfoResponse | null>(null);
|
||||||
const [selectedRole, setSelectedRole] = useState<string>("");
|
const [selectedRole, setSelectedRole] = useState<string>("");
|
||||||
|
// Estado para armazenar papéis disponíveis dinamicamente
|
||||||
|
const [availableRoles, setAvailableRoles] = useState<string[]>([]);
|
||||||
|
|
||||||
|
// Função utilitária para capitalizar a primeira letra
|
||||||
|
const capitalize = (s: string) => {
|
||||||
|
if (!s) return s;
|
||||||
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
||||||
|
};
|
||||||
|
|
||||||
const fetchUsers = useCallback(async () => {
|
const fetchUsers = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@ -42,46 +52,69 @@ export default function UsersPage() {
|
|||||||
try {
|
try {
|
||||||
// 1) Pega papéis e perfis em paralelo para melhor performance
|
// 1) Pega papéis e perfis em paralelo para melhor performance
|
||||||
const [rolesData, profilesData] = await Promise.all([
|
const [rolesData, profilesData] = await Promise.all([
|
||||||
usuariosService.listRoles(), // Alterado
|
usuariosApi.listRoles(),
|
||||||
perfisService.list() // Alterado
|
perfisApi.list()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const rolesArray = Array.isArray(rolesData) ? rolesData : [];
|
const rolesArray: UserRole[] = Array.isArray(rolesData) ? rolesData : [];
|
||||||
|
|
||||||
|
// 2) Extrair e salvar papéis únicos para o filtro (NOVO)
|
||||||
|
const uniqueRoles = new Set<string>();
|
||||||
|
rolesArray.forEach(roleItem => {
|
||||||
|
// Usa roleItem.role, se existir
|
||||||
|
if (roleItem.role) {
|
||||||
|
uniqueRoles.add(roleItem.role);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Converter para array, ordenar e atualizar o estado
|
||||||
|
setAvailableRoles(Array.from(uniqueRoles).sort());
|
||||||
|
|
||||||
const profilesById = new Map<string, any>();
|
const profilesById = new Map<string, any>();
|
||||||
if (Array.isArray(profilesData)) {
|
if (Array.isArray(profilesData)) {
|
||||||
for (const p of profilesData) {
|
for (const p of profilesData) {
|
||||||
|
// A chave do perfil deve ser o user_id, conforme a lógica anterior
|
||||||
if (p?.id) profilesById.set(p.id, p);
|
if (p?.id) profilesById.set(p.id, p);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3) Mapear roles -> flat users (lógica inalterada)
|
// 3) Mapear roles -> flat users
|
||||||
const mapped: FlatUser[] = rolesArray.map((roleItem) => {
|
const mapped: FlatUser[] = rolesArray.map((roleItem) => {
|
||||||
const uid = roleItem.user_id;
|
const uid = roleItem.user_id;
|
||||||
const profile = profilesById.get(uid);
|
const profile = profilesById.get(uid);
|
||||||
|
|
||||||
|
// Determina o role a ser usado. Prioriza roleItem.role, se não, '—'
|
||||||
|
const role = roleItem.role ?? "—";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: uid,
|
id: uid,
|
||||||
user_id: uid,
|
user_id: uid,
|
||||||
full_name: profile?.full_name ?? "—",
|
full_name: profile?.full_name ?? "—",
|
||||||
email: profile?.email ?? "—",
|
email: profile?.email ?? "—",
|
||||||
phone: profile?.phone ?? "—",
|
phone: profile?.phone ?? "—",
|
||||||
role: roleItem.role ?? "—",
|
role: role,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
setUsers(mapped);
|
setUsers(mapped);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error("Erro ao buscar usuários:", err);
|
console.error("Erro ao buscar usuários:", err);
|
||||||
setError("Não foi possível carregar os usuários. Veja console.");
|
setError("Não foi possível carregar os usuários. Verifique o console.");
|
||||||
setUsers([]);
|
setUsers([]);
|
||||||
|
setAvailableRoles([]); // Limpa os papéis em caso de erro
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Lógica de login inicial mantida, embora o import possa precisar de ajuste para 'autenticacaoApi"
|
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
try { await login(); } catch (e) { console.warn("login falhou no init:", e); }
|
// Garante que o interceptor da API seja executado para ler o cookie
|
||||||
|
try {
|
||||||
|
await api.get('/');
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("API setup (leitura de cookie via interceptor) falhou ou o endpoint raiz não existe:", e);
|
||||||
|
}
|
||||||
|
|
||||||
await fetchUsers();
|
await fetchUsers();
|
||||||
};
|
};
|
||||||
init();
|
init();
|
||||||
@ -91,15 +124,17 @@ export default function UsersPage() {
|
|||||||
setDetailsDialogOpen(true);
|
setDetailsDialogOpen(true);
|
||||||
setUserDetails(null);
|
setUserDetails(null);
|
||||||
try {
|
try {
|
||||||
const data = await usuariosService.getFullData(flatUser.user_id); // Alterado
|
// O getFullData usa user_id
|
||||||
|
const data = await usuariosApi.getFullData(flatUser.user_id);
|
||||||
setUserDetails(data);
|
setUserDetails(data);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error("Erro ao carregar detalhes:", err);
|
console.error("Erro ao carregar detalhes:", err);
|
||||||
|
// Fallback details em caso de falha na API
|
||||||
setUserDetails({
|
setUserDetails({
|
||||||
user: { id: flatUser.user_id, email: flatUser.email },
|
user: { id: flatUser.user_id, email: flatUser.email },
|
||||||
profile: { full_name: flatUser.full_name, phone: flatUser.phone },
|
profile: { full_name: flatUser.full_name, phone: flatUser.phone },
|
||||||
roles: [flatUser.role],
|
roles: [flatUser.role],
|
||||||
permissions: {},
|
permissions: { "read:self": true, "write:profile": false },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -107,50 +142,104 @@ export default function UsersPage() {
|
|||||||
const filteredUsers =
|
const filteredUsers =
|
||||||
selectedRole && selectedRole !== "all" ? users.filter((u) => u.role === selectedRole) : users;
|
selectedRole && selectedRole !== "all" ? users.filter((u) => u.role === selectedRole) : users;
|
||||||
|
|
||||||
|
// REMOVIDO: mockUserProfile e mockMenuItems não são mais necessários
|
||||||
|
// pois o ManagerLayout deve ser fornecido pelo arquivo app/manager/layout.tsx
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ManagerLayout>
|
// CORRIGIDO: Retornando apenas o conteúdo da página, sem o ManagerLayout
|
||||||
{/* O JSX restante permanece exatamente o mesmo */}
|
<div className="space-y-6">
|
||||||
<div className="space-y-6">
|
{/* Conteúdo da página */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-gray-900">Usuários</h1>
|
<h1 className="text-2xl font-bold text-gray-900">Usuários</h1>
|
||||||
<p className="text-sm text-gray-500">Gerencie usuários.</p>
|
<p className="text-sm text-gray-500">Gerencie usuários.</p>
|
||||||
</div>
|
|
||||||
<Link href="/manager/usuario/novo"><Button className="bg-green-600 hover:bg-green-700"><Plus className="w-4 h-4 mr-2" /> Novo Usuário</Button></Link>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-4 bg-white p-4 rounded-lg border border-gray-200">
|
|
||||||
<Filter className="w-5 h-5 text-gray-400" />
|
|
||||||
<Select onValueChange={setSelectedRole} value={selectedRole}>
|
|
||||||
<SelectTrigger className="w-[180px]"><SelectValue placeholder="Filtrar por papel" /></SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">Todos</SelectItem>
|
|
||||||
<SelectItem value="admin">Admin</SelectItem>
|
|
||||||
<SelectItem value="gestor">Gestor</SelectItem>
|
|
||||||
<SelectItem value="medico">Médico</SelectItem>
|
|
||||||
<SelectItem value="secretaria">Secretária</SelectItem>
|
|
||||||
<SelectItem value="user">Usuário</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white rounded-lg border border-gray-200 shadow-md overflow-hidden">
|
|
||||||
{loading ? <div className="p-8 text-center text-gray-500"><Loader2 className="w-8 h-8 animate-spin mx-auto mb-3 text-green-600" />Carregando usuários...</div>
|
|
||||||
: error ? <div className="p-8 text-center text-red-600">{error}</div>
|
|
||||||
: filteredUsers.length === 0 ? <div className="p-8 text-center text-gray-500">Nenhum usuário encontrado.</div>
|
|
||||||
: <div className="overflow-x-auto"><table className="min-w-full divide-y divide-gray-200"><thead className="bg-gray-50"><tr><th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">ID</th><th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Nome</th><th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">E-mail</th><th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Telefone</th><th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Cargo</th><th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase">Ações</th></tr></thead><tbody className="bg-white divide-y divide-gray-200">{filteredUsers.map((u) => (<tr key={u.id} className="hover:bg-gray-50"><td className="px-6 py-4 text-sm text-gray-500">{u.id}</td><td className="px-6 py-4 text-sm text-gray-900">{u.full_name}</td><td className="px-6 py-4 text-sm text-gray-500">{u.email}</td><td className="px-6 py-4 text-sm text-gray-500">{u.phone}</td><td className="px-6 py-4 text-sm text-gray-500 capitalize">{u.role}</td><td className="px-6 py-4 text-right"><Button variant="outline" size="icon" onClick={() => openDetailsDialog(u)} title="Visualizar"><Eye className="h-4 w-4" /></Button></td></tr>))}</tbody></table></div>}
|
|
||||||
</div>
|
|
||||||
<AlertDialog open={detailsDialogOpen} onOpenChange={setDetailsDialogOpen}>
|
|
||||||
<AlertDialogContent>
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<AlertDialogTitle className="text-2xl">{userDetails?.profile?.full_name || "Detalhes do Usuário"}</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription>
|
|
||||||
{!userDetails ? <div className="p-4 text-center text-gray-500"><Loader2 className="w-6 h-6 animate-spin mx-auto mb-3 text-green-600" />Buscando dados completos...</div>
|
|
||||||
: <div className="space-y-3 pt-2 text-left text-gray-700"><div><strong>ID:</strong> {userDetails.user.id}</div><div><strong>E-mail:</strong> {userDetails.user.email}</div><div><strong>Nome completo:</strong> {userDetails.profile.full_name}</div><div><strong>Telefone:</strong> {userDetails.profile.phone}</div><div><strong>Roles:</strong> {userDetails.roles?.join(", ")}</div><div><strong>Permissões:</strong><ul className="list-disc list-inside">{Object.entries(userDetails.permissions || {}).map(([k,v]) => <li key={k}>{k}: {v ? "Sim" : "Não"}</li>)}</ul></div></div>}
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter><AlertDialogCancel>Fechar</AlertDialogCancel></AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
</div>
|
</div>
|
||||||
</ManagerLayout>
|
<Link href="/manager/usuario/novo">
|
||||||
|
<Button className="bg-green-600 hover:bg-green-700">
|
||||||
|
<Plus className="w-4 h-4 mr-2" /> Novo Usuário
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
{/* Filtros */}
|
||||||
|
<div className="flex items-center space-x-4 bg-white p-4 rounded-lg border border-gray-200">
|
||||||
|
<Filter className="w-5 h-5 text-gray-400" />
|
||||||
|
<Select onValueChange={setSelectedRole} value={selectedRole}>
|
||||||
|
<SelectTrigger className="w-[180px]"><SelectValue placeholder="Filtrar por papel" /></SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">Todos</SelectItem>
|
||||||
|
{availableRoles.map(role => (
|
||||||
|
<SelectItem key={role} value={role}>
|
||||||
|
{capitalize(role)}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
{/* Tabela de Usuários */}
|
||||||
|
<div className="bg-white rounded-lg border border-gray-200 shadow-md overflow-hidden">
|
||||||
|
{loading
|
||||||
|
? <div className="p-8 text-center text-gray-500"><Loader2 className="w-8 h-8 animate-spin mx-auto mb-3 text-green-600" />Carregando usuários...</div>
|
||||||
|
: error
|
||||||
|
? <div className="p-8 text-center text-red-600">{error}</div>
|
||||||
|
: filteredUsers.length === 0
|
||||||
|
? <div className="p-8 text-center text-gray-500">Nenhum usuário encontrado{selectedRole && selectedRole !== 'all' ? ` para o papel: ${capitalize(selectedRole)}` : '.'}</div>
|
||||||
|
: <div className="overflow-x-auto">
|
||||||
|
<table className="min-w-full divide-y divide-gray-200">
|
||||||
|
<thead className="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">ID</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Nome</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">E-mail</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Telefone</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Cargo</th>
|
||||||
|
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase">Ações</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="bg-white divide-y divide-gray-200">
|
||||||
|
{filteredUsers.map((u) => (
|
||||||
|
<tr key={u.id} className="hover:bg-gray-50">
|
||||||
|
<td className="px-6 py-4 text-sm text-gray-500">{u.id}</td>
|
||||||
|
<td className="px-6 py-4 text-sm text-gray-900">{u.full_name}</td>
|
||||||
|
<td className="px-6 py-4 text-sm text-gray-500">{u.email}</td>
|
||||||
|
<td className="px-6 py-4 text-sm text-gray-500">{u.phone}</td>
|
||||||
|
<td className="px-6 py-4 text-sm text-gray-500 capitalize">{capitalize(u.role)}</td>
|
||||||
|
<td className="px-6 py-4 text-right">
|
||||||
|
<Button variant="outline" size="icon" onClick={() => openDetailsDialog(u)} title="Visualizar">
|
||||||
|
<Eye className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>}
|
||||||
|
</div>
|
||||||
|
{/* Diálogo de Detalhes */}
|
||||||
|
<AlertDialog open={detailsDialogOpen} onOpenChange={setDetailsDialogOpen}>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle className="text-2xl">{userDetails?.profile?.full_name || "Detalhes do Usuário"}</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
{!userDetails
|
||||||
|
? <div className="p-4 text-center text-gray-500"><Loader2 className="w-6 h-6 animate-spin mx-auto mb-3 text-green-600" />Buscando dados completos...</div>
|
||||||
|
: <div className="space-y-3 pt-2 text-left text-gray-700">
|
||||||
|
<div><strong>ID:</strong> {userDetails.user.id}</div>
|
||||||
|
<div><strong>E-mail:</strong> {userDetails.user.email}</div>
|
||||||
|
<div><strong>Nome completo:</strong> {userDetails.profile.full_name}</div>
|
||||||
|
<div><strong>Telefone:</strong> {userDetails.profile.phone}</div>
|
||||||
|
<div><strong>Roles:</strong> {userDetails.roles?.map(capitalize).join(", ")}</div>
|
||||||
|
<div><strong>Permissões:</strong>
|
||||||
|
<ul className="list-disc list-inside mt-1 ml-4 text-sm">
|
||||||
|
{Object.entries(userDetails.permissions || {}).map(([k,v]) => <li key={k}>{k}: <strong className={`${v ? 'text-green-600' : 'text-red-600'}`}>{v ? "Sim" : "Não"}</strong></li>)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>}
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter><AlertDialogCancel>Fechar</AlertDialogCancel></AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,11 +25,11 @@ interface RoleConfig {
|
|||||||
export const dashboardConfig: Record<string, RoleConfig> = {
|
export const dashboardConfig: Record<string, RoleConfig> = {
|
||||||
doctor: {
|
doctor: {
|
||||||
menuItems: [
|
menuItems: [
|
||||||
{ href: "/doctor/dashboard", icon: Home, label: "Dashboard" },
|
{ href: "/medicos/doctor/dashboard", icon: Home, label: "Dashboard" },
|
||||||
{ href: "/doctor/medicos/consultas", icon: Calendar, label: "Consultas" },
|
{ href: "/medicos/doctor/medicos/consultas", icon: Calendar, label: "Consultas" },
|
||||||
{ href: "#", icon: Clock, label: "Editor de Laudo" },
|
{ href: "/medicos/doctor/[id]/laudos", icon: Clock, label: "Editor de Laudo" },
|
||||||
{ href: "/doctor/medicos", icon: User, label: "Pacientes" },
|
{ href: "/medicos/doctor/medicos", icon: User, label: "Pacientes" },
|
||||||
{ href: "/doctor/disponibilidade", icon: Calendar, label: "Disponibilidade" },
|
{ href: "/medicos/doctor/disponibilidade", icon: Calendar, label: "Disponibilidade" },
|
||||||
],
|
],
|
||||||
getUserProfile: (userInfo) => getProfile(userInfo, { name: "Doutor(a)", secondaryText: "Especialidade" }),
|
getUserProfile: (userInfo) => getProfile(userInfo, { name: "Doutor(a)", secondaryText: "Especialidade" }),
|
||||||
},
|
},
|
||||||
@ -45,20 +45,21 @@ export const dashboardConfig: Record<string, RoleConfig> = {
|
|||||||
},
|
},
|
||||||
secretary: {
|
secretary: {
|
||||||
menuItems: [
|
menuItems: [
|
||||||
{ href: "/secretary/dashboard", icon: Home, label: "Dashboard" },
|
{ href: "/manager/dashboard", icon: Home, label: "Dashboard" },
|
||||||
{ href: "/secretary/appointments", icon: Calendar, label: "Consultas" },
|
{ href: "/manager/relatorios", icon: Calendar, label: "Relatórios gerenciais" },
|
||||||
{ href: "/secretary/schedule", icon: Clock, label: "Agendar Consulta" },
|
{ href: "/manager/usuario", icon: User, label: "Gestão de Usuários" },
|
||||||
{ href: "/secretary/pacientes", icon: User, label: "Pacientes" },
|
{ href: "/manager/home", icon: User, label: "Gestão de Médicos" },
|
||||||
],
|
{ href: "/manager/configuracoes", icon: Calendar, label: "Configurações" },
|
||||||
|
],
|
||||||
getUserProfile: (userInfo) => getProfile(userInfo, { name: "Secretária", secondaryText: "Atendimento" }),
|
getUserProfile: (userInfo) => getProfile(userInfo, { name: "Secretária", secondaryText: "Atendimento" }),
|
||||||
},
|
},
|
||||||
manager: {
|
manager: {
|
||||||
menuItems: [
|
menuItems: [
|
||||||
{ href: "#dashboard", icon: Home, label: "Dashboard" },
|
{ href: "/manager/dashboard", icon: Home, label: "Dashboard" },
|
||||||
{ href: "#reports", icon: Calendar, label: "Relatórios gerenciais" },
|
{ href: "#", icon: Calendar, label: "Relatórios gerenciais" },
|
||||||
{ href: "#users", icon: User, label: "Gestão de Usuários" },
|
{ href: "/manager/usuario", icon: User, label: "Gestão de Usuários" },
|
||||||
{ href: "#doctors", icon: User, label: "Gestão de Médicos" },
|
{ href: "/manager/home", icon: User, label: "Gestão de Médicos" },
|
||||||
{ href: "#settings", icon: Calendar, label: "Configurações" },
|
{ href: "#", icon: Calendar, label: "Configurações" },
|
||||||
],
|
],
|
||||||
getUserProfile: (userInfo) => getProfile(userInfo, { name: "Gestor(a)", secondaryText: "Gestão" }),
|
getUserProfile: (userInfo) => getProfile(userInfo, { name: "Gestor(a)", secondaryText: "Gestão" }),
|
||||||
},
|
},
|
||||||
|
|||||||
18
package-lock.json
generated
18
package-lock.json
generated
@ -71,13 +71,13 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4.1.9",
|
"@tailwindcss/postcss": "^4.1.9",
|
||||||
"@types/js-cookie": "^3.0.6",
|
"@types/js-cookie": "^3.0.6",
|
||||||
"@types/node": "^22.18.10",
|
"@types/node": "^22.18.12",
|
||||||
"@types/react": "^18.3.26",
|
"@types/react": "^18.3.26",
|
||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "^18.3.7",
|
||||||
"postcss": "^8.5",
|
"postcss": "^8.5",
|
||||||
"tailwindcss": "^4.1.9",
|
"tailwindcss": "^4.1.9",
|
||||||
"tw-animate-css": "1.3.3",
|
"tw-animate-css": "1.3.3",
|
||||||
"typescript": "^5"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@alloc/quick-lru": {
|
"node_modules/@alloc/quick-lru": {
|
||||||
@ -2497,9 +2497,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "22.18.10",
|
"version": "22.18.12",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.10.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.12.tgz",
|
||||||
"integrity": "sha512-anNG/V/Efn/YZY4pRzbACnKxNKoBng2VTFydVu8RRs5hQjikP8CQfaeAV59VFSCzKNp90mXiVXW2QzV56rwMrg==",
|
"integrity": "sha512-BICHQ67iqxQGFSzfCFTT7MRQ5XcBjG5aeKh5Ok38UBbPe5fxTyE+aHFxwVrGyr8GNlqFMLKD1D3P2K/1ks8tog==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -4508,9 +4508,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.9.2",
|
"version": "5.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|||||||
@ -72,12 +72,12 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4.1.9",
|
"@tailwindcss/postcss": "^4.1.9",
|
||||||
"@types/js-cookie": "^3.0.6",
|
"@types/js-cookie": "^3.0.6",
|
||||||
"@types/node": "^22.18.10",
|
"@types/node": "^22.18.12",
|
||||||
"@types/react": "^18.3.26",
|
"@types/react": "^18.3.26",
|
||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "^18.3.7",
|
||||||
"postcss": "^8.5",
|
"postcss": "^8.5",
|
||||||
"tailwindcss": "^4.1.9",
|
"tailwindcss": "^4.1.9",
|
||||||
"tw-animate-css": "1.3.3",
|
"tw-animate-css": "1.3.3",
|
||||||
"typescript": "^5"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
// Caminho: services/usuariosApi.ts
|
import api from "./api";
|
||||||
import api from './api';
|
|
||||||
|
|
||||||
export interface UserRole {
|
export interface UserRole {
|
||||||
id: any;
|
id: any;
|
||||||
@ -23,22 +22,54 @@ export interface FullUserData {
|
|||||||
|
|
||||||
export const usuariosApi = {
|
export const usuariosApi = {
|
||||||
listRoles: async (): Promise<UserRole[]> => {
|
listRoles: async (): Promise<UserRole[]> => {
|
||||||
const response = await api.get<UserRole[]>('/rest/v1/user_roles');
|
const response = await api.get<UserRole[]>("/rest/v1/user_roles");
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
createUser: async (data: { email: string; full_name: string; role: string; [key: string]: any }): Promise<any> => {
|
createUser: async (data: {
|
||||||
const response = await api.post('/functions/v1/create-user', data);
|
email: string;
|
||||||
return response.data;
|
full_name: string;
|
||||||
|
phone?: string;
|
||||||
|
phone_mobile?: string;
|
||||||
|
role: string;
|
||||||
|
cpf?: string;
|
||||||
|
create_patient_record?: boolean;
|
||||||
|
}): Promise<any> => {
|
||||||
|
try {
|
||||||
|
const response = await api.post("/functions/v1/create-user", data, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
apikey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || "",
|
||||||
|
Authorization: `Bearer ${
|
||||||
|
typeof window !== "undefined"
|
||||||
|
? localStorage.getItem("supabase-access-token") ||
|
||||||
|
""
|
||||||
|
: ""
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("❌ Erro no createUser:", error.response?.data || error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
getCurrentUser: async (): Promise<User> => {
|
getCurrentUser: async (): Promise<User> => {
|
||||||
const response = await api.get<User>('/auth/v1/user');
|
const response = await api.post<User>(
|
||||||
|
"/functions/v1/user-info",
|
||||||
|
{},
|
||||||
|
{ headers: { "Content-Type": "application/json" } }
|
||||||
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
getFullData: async (userId: string): Promise<FullUserData> => {
|
getFullData: async (userId: string): Promise<FullUserData> => {
|
||||||
const response = await api.get<FullUserData>(`/functions/v1/user-info?user_id=${userId}`);
|
const response = await api.post<FullUserData>(
|
||||||
|
"/functions/v1/user-info-by-id",
|
||||||
|
{ user_id: userId },
|
||||||
|
{ headers: { "Content-Type": "application/json" } }
|
||||||
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user