From f8f5f8214af85a354bec84f90753e924256b56a6 Mon Sep 17 00:00:00 2001 From: Gabriel Lira Figueira Date: Wed, 5 Nov 2025 01:35:44 -0300 Subject: [PATCH 1/4] =?UTF-8?q?feat(admin,=20patient):=20implementa=20cria?= =?UTF-8?q?=C3=A7=C3=A3o=20condicional=20e=20corrige=20layouts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refatora o formulário de criação de usuários no painel do manager para lidar com a lógica de múltiplos endpoints, diferenciando a criação de médicos das demais roles. - Adiciona campos condicionais para CRM e especialidade na UI. - Implementa a chamada ao endpoint `/functions/v1/create-doctor` para a role "medico". - Ajusta o payload para o endpoint `/create-user-with-password` para as outras roles. fix(patient): corrige renderização duplicada do layout nas páginas de agendamento e consultas, removendo o wrapper redundante do `PatientLayout`. refactor(services): ajusta os serviços `doctorsApi` e `usersApi` para alinhar com os schemas de dados corretos da API. --- app/manager/usuario/novo/page.tsx | 100 ++++++++++++++++++++++++++---- app/patient/appointments/page.tsx | 12 +++- app/patient/schedule/page.tsx | 6 +- components/patient-layout.tsx | 2 +- services/doctorsApi.mjs | 7 ++- services/usersApi.mjs | 3 +- 6 files changed, 106 insertions(+), 24 deletions(-) diff --git a/app/manager/usuario/novo/page.tsx b/app/manager/usuario/novo/page.tsx index 77dae32..1e63d72 100644 --- a/app/manager/usuario/novo/page.tsx +++ b/app/manager/usuario/novo/page.tsx @@ -1,3 +1,5 @@ +// /app/manager/usuario/novo/page.tsx + "use client"; import { useState } from "react"; @@ -9,7 +11,8 @@ import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Save, Loader2, Pause } from "lucide-react"; import ManagerLayout from "@/components/manager-layout"; -import { usersService } from "services/usersApi.mjs"; +import { usersService } from "@/services/usersApi.mjs"; +import { doctorsService } from "@/services/doctorsApi.mjs"; // Importação adicionada import { login } from "services/api.mjs"; interface UserFormData { @@ -20,6 +23,10 @@ interface UserFormData { senha: string; confirmarSenha: string; cpf: string; + // Novos campos para Médico + crm: string; + crm_uf: string; + specialty: string; } const defaultFormData: UserFormData = { @@ -30,6 +37,10 @@ const defaultFormData: UserFormData = { senha: "", confirmarSenha: "", cpf: "", + // Valores iniciais para campos de Médico + crm: "", + crm_uf: "", + specialty: "", }; const cleanNumber = (value: string): string => value.replace(/\D/g, ""); @@ -47,7 +58,13 @@ export default function NovoUsuarioPage() { const [error, setError] = useState(null); const handleInputChange = (key: keyof UserFormData, value: string) => { - const updatedValue = key === "telefone" ? formatPhone(value) : value; + let updatedValue = value; + if (key === "telefone") { + updatedValue = formatPhone(value); + } else if (key === "crm_uf") { + // Converte UF para maiúsculas + updatedValue = value.toUpperCase(); + } setFormData((prev) => ({ ...prev, [key]: updatedValue })); }; @@ -65,22 +82,56 @@ export default function NovoUsuarioPage() { return; } + // Validação adicional para Médico + if (formData.papel === "medico") { + if (!formData.crm || !formData.crm_uf) { + setError("Para a função 'Médico', o CRM e a UF do CRM são obrigatórios."); + return; + } + } + setIsSaving(true); try { - const payload = { - full_name: formData.nomeCompleto, - email: formData.email.trim().toLowerCase(), - phone: formData.telefone || null, - role: formData.papel, - password: formData.senha, - cpf: formData.cpf, - }; + if (formData.papel === "medico") { + // Lógica para criação de Médico + const doctorPayload = { + email: formData.email.trim().toLowerCase(), + full_name: formData.nomeCompleto, + cpf: formData.cpf, + crm: formData.crm, + crm_uf: formData.crm_uf, + specialty: formData.specialty || null, + phone_mobile: formData.telefone || null, // Usando phone_mobile conforme o schema + }; - console.log("📤 Enviando payload:"); - console.log(payload); + console.log("📤 Enviando payload para Médico:"); + console.log(doctorPayload); - await usersService.create_user(payload); + // Chamada ao endpoint específico para criação de médico + await doctorsService.create(doctorPayload); + + } else { + // Lógica para criação de Outras Roles + const isPatient = formData.papel === "paciente"; + + const userPayload = { + email: formData.email.trim().toLowerCase(), + password: formData.senha, + full_name: formData.nomeCompleto, + phone: formData.telefone || null, + role: formData.papel, + cpf: formData.cpf, + create_patient_record: isPatient, // true se a role for 'paciente' + phone_mobile: isPatient ? formData.telefone || null : undefined, // Enviar phone_mobile se for paciente + }; + + console.log("📤 Enviando payload para Usuário Comum:"); + console.log(userPayload); + + // Chamada ao endpoint padrão para criação de usuário + await usersService.create_user(userPayload); + } router.push("/manager/usuario"); } catch (e: any) { @@ -91,6 +142,8 @@ export default function NovoUsuarioPage() { } }; + const isMedico = formData.papel === "medico"; + return (
@@ -140,6 +193,27 @@ export default function NovoUsuarioPage() {
+ {/* Campos Condicionais para Médico */} + {isMedico && ( + <> +
+ + handleInputChange("crm", e.target.value)} placeholder="Número do CRM" required /> +
+ +
+ + handleInputChange("crm_uf", e.target.value)} placeholder="Ex: SP" maxLength={2} required /> +
+ +
+ + handleInputChange("specialty", e.target.value)} placeholder="Ex: Cardiologia" /> +
+ + )} + {/* Fim dos Campos Condicionais */} +
handleInputChange("senha", e.target.value)} placeholder="Mínimo 8 caracteres" minLength={8} required /> diff --git a/app/patient/appointments/page.tsx b/app/patient/appointments/page.tsx index 78f2d8b..5a56fc2 100644 --- a/app/patient/appointments/page.tsx +++ b/app/patient/appointments/page.tsx @@ -182,8 +182,8 @@ export default function PatientAppointments() {
-

Minhas Consultas

-

Veja, reagende ou cancele suas consultas

+

Minhas Consultas

+

Veja, reagende ou cancele suas consultas

@@ -244,7 +244,13 @@ export default function PatientAppointments() { )) ) : ( -

Você ainda não possui consultas agendadas.

+ + + Nenhuma Consulta Encontrada + + Você ainda não possui consultas agendadas. Use o menu "Agendar Consulta" para começar. + + )}
diff --git a/app/patient/schedule/page.tsx b/app/patient/schedule/page.tsx index f6a4f0f..448692d 100644 --- a/app/patient/schedule/page.tsx +++ b/app/patient/schedule/page.tsx @@ -134,8 +134,8 @@ export default function ScheduleAppointment() { {/* Médico */}
- + @@ -168,7 +168,7 @@ export default function ScheduleAppointment() {
diff --git a/services/doctorsApi.mjs b/services/doctorsApi.mjs index 5d918a0..45fd58d 100644 --- a/services/doctorsApi.mjs +++ b/services/doctorsApi.mjs @@ -3,7 +3,10 @@ import { api } from "./api.mjs"; export const doctorsService = { list: () => api.get("/rest/v1/doctors"), getById: (id) => api.get(`/rest/v1/doctors?id=eq.${id}`).then(data => data[0]), - create: (data) => api.post("/functions/v1/create-doctor", data), + async create(data) { + // Esta é a função usada no page.tsx para criar médicos + return await api.post("/functions/v1/create-doctor", data); + }, update: (id, data) => api.patch(`/rest/v1/doctors?id=eq.${id}`, data), delete: (id) => api.delete(`/rest/v1/doctors?id=eq.${id}`), -}; \ No newline at end of file +}; diff --git a/services/usersApi.mjs b/services/usersApi.mjs index e9b84be..396ea16 100644 --- a/services/usersApi.mjs +++ b/services/usersApi.mjs @@ -1,5 +1,3 @@ -// SUBSTITUA O OBJETO INTEIRO EM services/usersApi.mjs - import { api } from "./api.mjs"; export const usersService = { @@ -19,6 +17,7 @@ export const usersService = { }, async create_user(data) { + // Esta é a função usada no page.tsx para criar usuários que não são médicos return await api.post(`/functions/v1/create-user-with-password`, data); }, From f8d88943bb229d30940bcc71679de631325b2e31 Mon Sep 17 00:00:00 2001 From: Gabriel Lira Figueira Date: Wed, 5 Nov 2025 01:44:32 -0300 Subject: [PATCH 2/4] =?UTF-8?q?refactor(manager):=20unifica=20fluxo=20de?= =?UTF-8?q?=20cria=C3=A7=C3=A3o=20de=20m=C3=A9dico=20e=20usu=C3=A1rio?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove o componente de formulário de criação de médico (`/manager/home/novo`) que estava duplicado e desatualizado. O botão "Novo Usuário" na página de gerenciamento de médicos (`/manager/home`) foi redirecionado para o formulário genérico e aprimorado em `/manager/usuario/novo`. Essa alteração centraliza toda a lógica de criação de usuários em um único componente, aproveitando a UI condicional já implementada para a role "medico" e simplificando a manutenção do código. --- app/manager/home/novo/page.tsx | 534 --------------------------------- app/manager/home/page.tsx | 4 +- 2 files changed, 2 insertions(+), 536 deletions(-) delete mode 100644 app/manager/home/novo/page.tsx diff --git a/app/manager/home/novo/page.tsx b/app/manager/home/novo/page.tsx deleted file mode 100644 index 1912432..0000000 --- a/app/manager/home/novo/page.tsx +++ /dev/null @@ -1,534 +0,0 @@ -"use client" - -import { useState } from "react" -import { useRouter } from "next/navigation" -import Link from "next/link" -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { Label } from "@/components/ui/label" -import { Textarea } from "@/components/ui/textarea" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" -import { Checkbox } from "@/components/ui/checkbox" -import { Upload, X, ChevronDown, Save, Loader2 } from "lucide-react" -import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible" -import ManagerLayout from "@/components/manager-layout" -import { doctorsService } from "services/doctorsApi.mjs"; - - -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 { - - nomeCompleto: string; - crm: string; - crmEstado: string; - cpf: string; - email: string; - especialidade: string; - telefoneCelular: string; - telefone2: string; - cep: string; - endereco: string; - numero: string; - complemento: string; - bairro: string; - cidade: string; - estado: string; - dataNascimento: string; - rg: string; - ativo: boolean; - observacoes: string; - anexos: { id: number, name: string }[]; -} - - -const apiMap: { [K in keyof DoctorFormData]: string | null } = { - nomeCompleto: 'full_name', - crm: 'crm', - crmEstado: 'crm_uf', - cpf: 'cpf', - email: 'email', - - especialidade: 'specialty', - telefoneCelular: 'phone_mobile', - telefone2: 'phone2', - cep: 'cep', - endereco: 'street', - numero: 'number', - complemento: 'complement', - bairro: 'neighborhood', - cidade: 'city', - estado: 'state', - dataNascimento: 'birth_date', - rg: 'rg', - ativo: 'active', - - observacoes: null, - anexos: null, -}; - - -const defaultFormData: DoctorFormData = { - nomeCompleto: '', crm: '', crmEstado: '', cpf: '', email: '', - especialidade: '', telefoneCelular: '', telefone2: '', cep: '', - endereco: '', numero: '', complemento: '', bairro: '', cidade: '', estado: '', - dataNascimento: '', rg: '', ativo: true, - observacoes: '', anexos: [], -}; - - - - -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 NovoMedicoPage() { - const router = useRouter(); - const [formData, setFormData] = useState(defaultFormData); - const [isSaving, setIsSaving] = useState(false); - const [error, setError] = useState(null); - const [anexosOpen, setAnexosOpen] = useState(false); - - - const handleInputChange = (key: keyof DoctorFormData, value: string | boolean | { id: number, name: string }[]) => { - - - 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 adicionarAnexo = () => { - const newId = Date.now(); - handleInputChange('anexos', [...formData.anexos, { id: newId, name: `Documento ${formData.anexos.length + 1}` }]); - } - - const removerAnexo = (id: number) => { - handleInputChange('anexos', formData.anexos.filter((anexo) => anexo.id !== id)); - } - - - const requiredFields = [ - { key: 'nomeCompleto', name: 'Nome Completo' }, - { key: 'crm', name: 'CRM' }, - { key: 'crmEstado', name: 'UF do CRM' }, - { key: 'cpf', name: 'CPF' }, - { key: 'email', name: 'E-mail' }, - ] as const; - - - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setError(null); - setIsSaving(true); - - - for (const field of requiredFields) { - let valueToCheck = formData[field.key]; - - - if (!valueToCheck || String(valueToCheck).trim() === '') { - setError(`O campo obrigatório "${field.name}" deve ser preenchido.`); - setIsSaving(false); - return; - } - } - - const finalPayload: { [key: string]: any } = {}; - const formKeys = Object.keys(formData) as Array; - - - formKeys.forEach((key) => { - const apiFieldName = apiMap[key]; - - if (!apiFieldName) return; - - let value = formData[key]; - - if (typeof value === 'string') { - let trimmedValue = value.trim(); - - - const isOptional = !requiredFields.some(f => f.key === key); - - if (isOptional && trimmedValue === '') { - finalPayload[apiFieldName] = null; - return; - } - - - if (key === 'crmEstado' || key === 'estado') { - trimmedValue = trimmedValue.toUpperCase(); - } - - value = trimmedValue; - } - - finalPayload[apiFieldName] = value; - }); - - - try { - - const response = await doctorsService.create(finalPayload); - router.push("/manager/home"); - } catch (e: any) { - console.error("Erro ao salvar o médico:", e); - - let detailedError = `Erro na requisição. Verifique se o **CRM** ou **CPF** já existem ou se as **Máscaras/Datas** estão incorretas.`; - - - 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."; - } 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 cadastrar. Detalhes: ${detailedError}`); - } finally { - setIsSaving(false); - } - }; - - return ( - -
-
-
-

Novo Médico

-

- Preencha os dados do novo médico para cadastro. -

-
- - - -
- -
- - {error && ( -
-

Erro no Cadastro:

-

{error}

-
- )} - - -
-

- Dados Principais e Pessoais -

- - -
-
- - handleInputChange("nomeCompleto", e.target.value)} - placeholder="Nome do Médico" - required - /> -
-
- - handleInputChange("crm", e.target.value)} - placeholder="Ex: 123456" - required - /> -
-
- - -
-
- - -
-
- - handleInputChange("especialidade", e.target.value)} - placeholder="Ex: Cardiologia" - /> -
-
- - handleInputChange("cpf", e.target.value)} - placeholder="000.000.000-00" - maxLength={14} - required - /> -
-
- - handleInputChange("rg", e.target.value)} - placeholder="00.000.000-0" - /> -
-
- - -
-
- - handleInputChange("email", e.target.value)} - placeholder="exemplo@dominio.com" - required - /> -
-
- - handleInputChange("dataNascimento", e.target.value)} - /> -
-
-
- - -
-

- Contato e Endereço -

- - -
-
- - handleInputChange("telefoneCelular", e.target.value)} - placeholder="(00) 00000-0000" - maxLength={15} - /> -
-
- - handleInputChange("telefone2", e.target.value)} - placeholder="(00) 00000-0000" - maxLength={15} - /> -
-
-
- handleInputChange("ativo", checked === true)} - /> - -
-
-
- - -
-
- - handleInputChange("cep", e.target.value)} - placeholder="00000-000" - maxLength={9} - /> -
-
- - handleInputChange("endereco", e.target.value)} - placeholder="Rua, Avenida, etc." - /> -
-
-
-
- - handleInputChange("numero", e.target.value)} - placeholder="123" - /> -
-
- - handleInputChange("complemento", e.target.value)} - placeholder="Apto, Bloco, etc." - /> -
-
-
-
- - handleInputChange("bairro", e.target.value)} - placeholder="Bairro" - /> -
-
- - handleInputChange("estado", e.target.value)} - placeholder="SP" - /> -
-
- - handleInputChange("cidade", e.target.value)} - placeholder="São Paulo" - /> -
-
-
- - -
-

- Outras Informações (Internas) -

- -
-
- -