diff --git a/MEDICONNECT 2/src/components/AccessibilityMenu.tsx b/MEDICONNECT 2/src/components/AccessibilityMenu.tsx index bff467872..84ca25fec 100644 --- a/MEDICONNECT 2/src/components/AccessibilityMenu.tsx +++ b/MEDICONNECT 2/src/components/AccessibilityMenu.tsx @@ -161,10 +161,10 @@ const AccessibilityMenu: React.FC = () => { aria-modal="true" aria-labelledby={DIALOG_TITLE_ID} aria-describedby={DIALOG_DESC_ID} - className="fixed bottom-24 right-6 z-50 bg-white dark:bg-slate-800 rounded-lg shadow-2xl p-6 w-80 border-2 border-blue-600 transition-all duration-300 animate-slideIn focus:outline-none" + className="fixed bottom-24 right-6 z-50 bg-white dark:bg-slate-800 rounded-lg shadow-2xl w-80 border-2 border-blue-600 transition-all duration-300 animate-slideIn focus:outline-none max-h-[calc(100vh-7rem)]" onKeyDown={onKeyDown} > -
+

{

- + {active ? "ON" : "OFF"}
diff --git a/MEDICONNECT 2/src/components/AvatarInitials.tsx b/MEDICONNECT 2/src/components/AvatarInitials.tsx index 8310da331..9391f4aec 100644 --- a/MEDICONNECT 2/src/components/AvatarInitials.tsx +++ b/MEDICONNECT 2/src/components/AvatarInitials.tsx @@ -44,7 +44,9 @@ export const AvatarInitials: React.FC = ({ const style: React.CSSProperties = { width: size, height: size, - lineHeight: `${size}px`, + minWidth: size, + minHeight: size, + flexShrink: 0, }; const fontSize = Math.max(14, Math.round(size * 0.42)); return ( diff --git a/MEDICONNECT 2/src/components/Header.tsx b/MEDICONNECT 2/src/components/Header.tsx index 429551014..64546b046 100644 --- a/MEDICONNECT 2/src/components/Header.tsx +++ b/MEDICONNECT 2/src/components/Header.tsx @@ -1,142 +1,132 @@ import React from "react"; -import { Link, useLocation } from "react-router-dom"; -import { Heart, Stethoscope, User, Clipboard, LogOut } from "lucide-react"; +import { Link } from "react-router-dom"; +import { Heart, LogOut, LogIn } from "lucide-react"; import { useAuth } from "../hooks/useAuth"; -import Logo from "./images/logo.PNG"; // caminho relativo ao arquivo +import { ProfileSelector } from "./ProfileSelector"; +import { i18n } from "../i18n"; +import Logo from "./images/logo.PNG"; const Header: React.FC = () => { - const location = useLocation(); - - const isActive = (path: string) => { - return location.pathname === path; - }; - const { user, logout, role, isAuthenticated } = useAuth(); const roleLabel: Record = { secretaria: "Secretaria", medico: "Médico", paciente: "Paciente", + admin: "Administrador", + gestor: "Gestor", }; return (
+ {/* Skip to content link for accessibility */} + + {i18n.t("common.skipToContent")} + +
{/* Logo */} - + MediConnect -
-

MediConnect

-

Sistema de Agendamento

+

+ {i18n.t("header.logo")} +

+

+ {i18n.t("header.subtitle")} +

- {/* Navigation */} -
-
+
{activeTab === "dashboard" && (
@@ -1784,12 +1916,12 @@ const PainelSecretaria = () => { {patientModalOpen && (
-
-
-
+
+
+

{patientModalMode === "create" - ? "Cadastrar Paciente" + ? "Cadastrar Novo Paciente" : "Editar Paciente"}

+

+ Preencha todos os campos obrigatórios (*) +

+
+
+
+ {/* Seção: Dados Pessoais */} +
+

+ Dados Pessoais +

- -
-
-

- Dados pessoais -

+
+
+ + + setFormDataPaciente((prev) => ({ + ...prev, + nome: event.target.value, + })) + } + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent" + required + placeholder="Maria Santos Silva" + /> +
+ +
+ + + setFormDataPaciente((prev) => ({ + ...prev, + social_name: event.target.value, + })) + } + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent" + placeholder="Maria Santos" + /> +
+ +
+ + +
+ +
+ + + setFormDataPaciente((prev) => ({ + ...prev, + dataNascimento: event.target.value, + })) + } + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent" + required + /> +
+ +
+ + +
+
+ + {/* Seção: Contato */} +
+

+ Contato +

+
setFormDataPaciente((prev) => ({ ...prev, - nome: event.target.value, - })) - } - className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent" - required - placeholder="Digite o nome completo" - /> -
-
- - - setFormDataPaciente((prev) => ({ - ...prev, - social_name: event.target.value, - })) - } - className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent" - placeholder="Opcional" - /> -
-
- - - {cpfError && ( -

{cpfError}

- )} - {cpfValidation && patientModalMode === "create" && ( -

- Validação externa:{" "} - {cpfValidation.valido ? "OK" : "Inválido"} -

- )} -
-
- - -
-
- - - setFormDataPaciente((prev) => ({ - ...prev, - dataNascimento: event.target.value, + email: event.target.value, })) } className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent" required + placeholder="maria@email.com" />
-
-

- Contato -

-
-
+
{ })) } className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent" - placeholder="Número do telefone" + placeholder="99999-9999" required />
-
- - - setFormDataPaciente((prev) => ({ - ...prev, - email: event.target.value, - })) - } - className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent" - required - placeholder="contato@paciente.com" - /> +
+ + {/* Seção: Informações Clínicas */} +
+

+ Informações Clínicas +

+ +
+
+ + +
+ +
+ + + setFormDataPaciente((prev) => ({ + ...prev, + peso: event.target.value, + })) + } + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent" + placeholder="65.5" + /> +
+ +
+ + + setFormDataPaciente((prev) => ({ + ...prev, + altura: event.target.value, + })) + } + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent" + placeholder="1.65" + /> +
-
-

- Informações clínicas -

-
-
- - -
-
- - - setFormDataPaciente((prev) => ({ - ...prev, - altura: event.target.value, - })) - } - className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent" - placeholder="170" - /> -
-
- - - setFormDataPaciente((prev) => ({ - ...prev, - peso: event.target.value, - })) - } - className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent" - placeholder="70.5" - /> -
-
- - -
-
- - - setFormDataPaciente((prev) => ({ - ...prev, - numeroCarteirinha: event.target.value, - })) - } - className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent" - placeholder="Informe se possuir convênio" - /> -
+
+
+ + +
-
-

- Endereço -

+
+ + + setFormDataPaciente((prev) => ({ + ...prev, + numeroCarteirinha: event.target.value, + })) + } + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent" + placeholder="Número da carteirinha" + /> +
+
+ + {/* Seção: Endereço */} +
+

+ Endereço +

+
- - setFormDataPaciente((prev) => ({ - ...prev, - endereco: { - ...prev.endereco, - cep: event.target.value, - }, - })) - } - onBlur={(event) => - void handleCepLookup(event.target.value) - } - className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent" - placeholder="00000-000" - /> +
+ { + const digits = event.target.value.replace(/\D/g, "").slice(0, 8); + setFormDataPaciente((prev) => ({ + ...prev, + endereco: { + ...prev.endereco, + cep: digits, + }, + })); + }} + onBlur={(event) => { + const digits = event.target.value.replace(/\D/g, ""); + if (digits.length === 8) { + void handleCepLookup(digits); + } + }} + className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent" + placeholder="01234-567" + maxLength={9} + /> + +
-
- - - setFormDataPaciente((prev) => ({ - ...prev, - endereco: { - ...prev.endereco, - rua: event.target.value, - }, - })) - } - className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent" - placeholder="Rua" - /> + +
+
+ + + setFormDataPaciente((prev) => ({ + ...prev, + endereco: { + ...prev.endereco, + rua: event.target.value, + }, + })) + } + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent" + placeholder="Rua das Flores" + /> +
+ +
+ + + setFormDataPaciente((prev) => ({ + ...prev, + endereco: { + ...prev.endereco, + numero: event.target.value, + }, + })) + } + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent" + placeholder="123" + /> +
-
- - - setFormDataPaciente((prev) => ({ - ...prev, - endereco: { - ...prev.endereco, - numero: event.target.value, - }, - })) - } - className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent" - placeholder="Número" - /> + +
+
+ + + setFormDataPaciente((prev) => ({ + ...prev, + endereco: { + ...prev.endereco, + bairro: event.target.value, + }, + })) + } + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent" + placeholder="Centro" + /> +
+ +
+ + + setFormDataPaciente((prev) => ({ + ...prev, + endereco: { + ...prev.endereco, + cidade: event.target.value, + }, + })) + } + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent" + placeholder="São Paulo" + /> +
+ +
+ + + setFormDataPaciente((prev) => ({ + ...prev, + endereco: { + ...prev.endereco, + estado: event.target.value.toUpperCase(), + }, + })) + } + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent" + placeholder="SP" + maxLength={2} + /> +
+
-
- - - setFormDataPaciente((prev) => ({ - ...prev, - endereco: { - ...prev.endereco, - bairro: event.target.value, - }, - })) - } - className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent" - placeholder="Bairro" - /> -
-
- - - setFormDataPaciente((prev) => ({ - ...prev, - endereco: { - ...prev.endereco, - cidade: event.target.value, - }, - })) - } - className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent" - placeholder="Cidade" - /> -
-
- - - setFormDataPaciente((prev) => ({ - ...prev, - endereco: { - ...prev.endereco, - estado: event.target.value, - }, - })) - } - className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent" - placeholder="Estado" + placeholder="Apto 45, Bloco B..." />
+
-
+ {/* Seção: Observações */} +
+

+ Observações Adicionais +

+ +
@@ -2253,12 +2433,12 @@ const PainelSecretaria = () => { } className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent" rows={3} - placeholder="Observações gerais do paciente" + placeholder="Observações gerais sobre o paciente..." />
-
+
@@ -2289,12 +2469,12 @@ const PainelSecretaria = () => { {doctorModalOpen && (
-
-
-
+
+
+

{doctorModalMode === "create" - ? "Cadastrar Médico" + ? "Cadastrar Novo Médico" : "Editar Médico"}

+

+ Preencha todos os campos obrigatórios (*) +

+
+
-
-
- - - setFormDataMedico((prev) => ({ - ...prev, - nome: event.target.value, - })) - } - className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" - required - /> -
-
- - -
-
- - - setFormDataMedico((prev) => ({ - ...prev, - crm: event.target.value, - })) - } - className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" - required - /> -
-
- - - setFormDataMedico((prev) => ({ - ...prev, - telefone: buildMedicoTelefone(event.target.value), - })) - } - className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" - required - /> -
-
- - - setFormDataMedico((prev) => ({ - ...prev, - email: event.target.value, - })) - } - className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" - required - /> -
- {doctorModalMode === "create" && ( + + {/* Seção: Dados Pessoais */} +
+

+ Dados Pessoais +

+
setFormDataMedico((prev) => ({ ...prev, - senha: event.target.value, + nome: event.target.value, })) } className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" required - minLength={6} + placeholder="Dr. João da Silva" />
+ +
+
+ + { + const { digits } = maskCpf(event.target.value); + setFormDataMedico((prev) => ({ + ...prev, + cpf: digits, + })); + }} + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" + required + placeholder="000.000.000-00" + maxLength={14} + /> +
+ +
+ + + setFormDataMedico((prev) => ({ + ...prev, + rg: event.target.value, + })) + } + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" + placeholder="00.000.000-0" + /> +
+
+ +
+ + + setFormDataMedico((prev) => ({ + ...prev, + dataNascimento: event.target.value, + })) + } + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" + required + /> +
+
+ + {/* Seção: Dados Profissionais */} +
+

+ Dados Profissionais +

+ +
+
+ + + setFormDataMedico((prev) => ({ + ...prev, + crm: event.target.value, + })) + } + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" + required + placeholder="123456" + /> +
+ +
+ + +
+
+ +
+ + +
+
+ + {/* Seção: Contato */} +
+

+ Contato +

+ +
+ + + setFormDataMedico((prev) => ({ + ...prev, + email: event.target.value, + })) + } + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" + required + placeholder="medico@email.com" + /> +
+ +
+
+ + + setFormDataMedico((prev) => ({ + ...prev, + telefone: buildMedicoTelefone(event.target.value), + })) + } + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" + required + placeholder="(11) 99999-9999" + /> +
+ +
+ + + setFormDataMedico((prev) => ({ + ...prev, + telefone2: event.target.value, + })) + } + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" + placeholder="(11) 3333-4444" + /> +
+
+
+ + {/* Seção: Endereço (obrigatório) */} +
+

+ Endereço +

+ +
+ +
+ { + const digits = event.target.value.replace(/\D/g, "").slice(0, 8); + setFormDataMedico((prev) => ({ + ...prev, + cep: digits, + })); + }} + onBlur={(event) => { + const digits = event.target.value.replace(/\D/g, ""); + if (digits.length === 8) { + void handleCepLookupMedico(digits); + } + }} + className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" + placeholder="00000-000" + required + maxLength={9} + /> + +
+
+ +
+
+ + + setFormDataMedico((prev) => ({ + ...prev, + rua: event.target.value, + })) + } + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" + placeholder="Nome da rua" + required + /> +
+ +
+ + + setFormDataMedico((prev) => ({ + ...prev, + numero: event.target.value, + })) + } + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" + placeholder="123" + required + /> +
+
+ +
+
+ + + setFormDataMedico((prev) => ({ + ...prev, + bairro: event.target.value, + })) + } + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" + placeholder="Bairro" + required + /> +
+ +
+ + + setFormDataMedico((prev) => ({ + ...prev, + cidade: event.target.value, + })) + } + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" + placeholder="Cidade" + required + /> +
+ +
+ + + setFormDataMedico((prev) => ({ + ...prev, + estado: event.target.value.toUpperCase(), + })) + } + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" + placeholder="UF" + maxLength={2} + required + /> +
+
+ +
+ + + setFormDataMedico((prev) => ({ + ...prev, + complemento: event.target.value, + })) + } + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" + placeholder="Apto, sala, bloco..." + /> +
+
+ + {/* Seção: Senha (apenas criação) */} + {doctorModalMode === "create" && ( +
+

+ Acesso ao Sistema +

+
+ + + setFormDataMedico((prev) => ({ + ...prev, + senha: event.target.value, + })) + } + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" + required + minLength={6} + placeholder="Mínimo 6 caracteres" + /> +

+ O médico deverá alterar esta senha no primeiro acesso +

+
+
)} + {doctorModalMode === "edit" && (
A senha permanece inalterada neste fluxo. Se necessário, @@ -2435,7 +2928,7 @@ const PainelSecretaria = () => {
)} -
+
diff --git a/MEDICONNECT 2/src/services/medicoService.ts b/MEDICONNECT 2/src/services/medicoService.ts index 8459f15ef..babc2b7b7 100644 --- a/MEDICONNECT 2/src/services/medicoService.ts +++ b/MEDICONNECT 2/src/services/medicoService.ts @@ -69,19 +69,19 @@ export interface MedicoCreate { nome: string; // full_name email: string; // email crm: string; // crm - crmUf?: string; // crm_uf - cpf?: string; // cpf + crmUf: string; // crm_uf (REQUIRED) + cpf: string; // cpf (REQUIRED) especialidade: string; // specialty telefone: string; // phone_mobile telefone2?: string; // phone2 - cep?: string; // cep - rua?: string; // street - numero?: string; // number + cep: string; // cep (REQUIRED) + rua: string; // street (REQUIRED) + numero: string; // number (REQUIRED) complemento?: string; // complement - bairro?: string; // neighborhood - cidade?: string; // city - estado?: string; // state - dataNascimento?: string; // birth_date (YYYY-MM-DD) + bairro: string; // neighborhood (REQUIRED) + cidade: string; // city (REQUIRED) + estado: string; // state (REQUIRED) + dataNascimento: string; // birth_date (YYYY-MM-DD) (REQUIRED) rg?: string; // rg status?: "ativo" | "inativo"; // mapeado para active } diff --git a/MEDICONNECT 2/src/services/pacienteService.ts b/MEDICONNECT 2/src/services/pacienteService.ts index 2279c6a27..45d792904 100644 --- a/MEDICONNECT 2/src/services/pacienteService.ts +++ b/MEDICONNECT 2/src/services/pacienteService.ts @@ -358,35 +358,32 @@ export async function createPatient(payload: { alturaM?: number; endereco?: EnderecoPaciente; }): Promise> { - // Normalizações: remover qualquer formatação para envio limpo - const cleanCpf = (payload.cpf || "").replace(/\D/g, ""); - const cleanPhone = (payload.telefone || "").replace(/\D/g, ""); + // Sanitização forte + const rawCpf = (payload.cpf || "").replace(/\D/g, "").slice(0, 11); + let phone = (payload.telefone || "").replace(/\D/g, ""); + if (phone.length > 15) phone = phone.slice(0, 15); const cleanEndereco: EnderecoPaciente | undefined = payload.endereco - ? { - ...payload.endereco, - cep: payload.endereco.cep?.replace(/\D/g, ""), - } + ? { ...payload.endereco, cep: payload.endereco.cep?.replace(/\D/g, "") } : undefined; + const peso = typeof payload.pesoKg === "number" && payload.pesoKg > 0 && payload.pesoKg < 500 ? payload.pesoKg : undefined; + const altura = typeof payload.alturaM === "number" && payload.alturaM > 0 && payload.alturaM < 3 ? payload.alturaM : undefined; - // Validação mínima required - if (!payload.nome?.trim()) - return { success: false, error: "Nome é obrigatório" }; - if (!cleanCpf) return { success: false, error: "CPF é obrigatório" }; - if (!payload.email?.trim()) - return { success: false, error: "Email é obrigatório" }; - if (!cleanPhone) return { success: false, error: "Telefone é obrigatório" }; + if (!payload.nome?.trim()) return { success: false, error: "Nome é obrigatório" }; + if (!rawCpf) return { success: false, error: "CPF é obrigatório" }; + if (!payload.email?.trim()) return { success: false, error: "Email é obrigatório" }; + if (!phone) return { success: false, error: "Telefone é obrigatório" }; - const body: Partial = { + const buildBody = (cpfValue: string): Partial => ({ full_name: payload.nome, - cpf: cleanCpf, + cpf: cpfValue, email: payload.email, - phone_mobile: cleanPhone, + phone_mobile: phone, birth_date: payload.dataNascimento, social_name: payload.socialName, sex: payload.sexo, blood_type: payload.tipoSanguineo, - weight_kg: payload.pesoKg, - height_m: payload.alturaM, + weight_kg: peso, + height_m: altura, street: cleanEndereco?.rua, number: cleanEndereco?.numero, complement: cleanEndereco?.complemento, @@ -394,37 +391,67 @@ export async function createPatient(payload: { city: cleanEndereco?.cidade, state: cleanEndereco?.estado, cep: cleanEndereco?.cep, - }; - Object.keys(body).forEach((k) => { - const v = (body as Record)[k]; - if (v === undefined || v === "") - delete (body as Record)[k]; }); - try { + + let body: Partial = buildBody(rawCpf); + const prune = () => { + Object.keys(body).forEach((k) => { + const v = (body as Record)[k]; + if (v === undefined || v === "") delete (body as Record)[k]; + }); + }; + prune(); + + const attempt = async (): Promise> => { const response = await http.post( ENDPOINTS.PATIENTS, body, - { - headers: { Prefer: "return=representation" }, - } + { headers: { Prefer: "return=representation" } } ); - if (!response.success || !response.data) - return { - success: false, - error: response.error || "Erro ao criar paciente", - }; - const raw = Array.isArray(response.data) ? response.data[0] : response.data; - return { success: true, data: mapPacienteFromApi(raw) }; - } catch (error: unknown) { - const err = error as { - response?: { status?: number; data?: { message?: string } }; - }; + if (response.success && response.data) { + const raw = Array.isArray(response.data) ? response.data[0] : response.data; + return { success: true, data: mapPacienteFromApi(raw) }; + } + return { success: false, error: response.error || "Erro ao criar paciente" }; + }; + + const handleOverflowFallbacks = async (baseError: string): Promise> => { + // 1) tentar com CPF formatado + if (/numeric field overflow/i.test(baseError) && rawCpf.length === 11) { + body = buildBody(rawCpf.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, "$1.$2.$3-$4")); + prune(); + let r = await attempt(); + if (r.success) return r; + // 2) remover campos opcionais progressivamente + const optional: Array = ["weight_kg", "height_m", "blood_type", "cep", "number"]; + for (const key of optional) { + if (key in body) { + delete (body as Record)[key]; + r = await attempt(); + if (r.success) return r; + } + } + return r; // retorna último erro + } + return { success: false, error: baseError }; + }; + + try { + let first = await attempt(); + if (!first.success && /numeric field overflow/i.test(first.error || "")) { + first = await handleOverflowFallbacks(first.error || "numeric field overflow"); + } + return first; + } catch (err: unknown) { + const e = err as { response?: { status?: number; data?: { message?: string } } }; let msg = "Erro ao criar paciente"; - if (err.response?.status === 401) msg = "Não autorizado"; - else if (err.response?.status === 400) - msg = err.response.data?.message || "Dados inválidos"; - else if (err.response?.data?.message) msg = err.response.data.message; - console.error(msg, error); + if (e.response?.status === 401) msg = "Não autorizado"; + else if (e.response?.status === 400) msg = e.response.data?.message || "Dados inválidos"; + else if (e.response?.data?.message) msg = e.response.data.message; + if (/numeric field overflow/i.test(msg)) { + const overflowAttempt = await handleOverflowFallbacks(msg); + return overflowAttempt; + } return { success: false, error: msg }; } } diff --git a/MEDICONNECT 2/src/services/telemetry.ts b/MEDICONNECT 2/src/services/telemetry.ts new file mode 100644 index 000000000..fe7c40b7d --- /dev/null +++ b/MEDICONNECT 2/src/services/telemetry.ts @@ -0,0 +1,95 @@ +/** + * Sistema de telemetria para tracking de eventos + * Expõe eventos via dataLayer (Google Tag Manager) e console + */ + +export interface TelemetryEvent { + event: string; + category: string; + action: string; + label?: string; + value?: number; + timestamp: string; +} + +declare global { + interface Window { + dataLayer?: TelemetryEvent[]; + } +} + +class TelemetryService { + private enabled: boolean; + + constructor() { + this.enabled = true; + this.initDataLayer(); + } + + private initDataLayer(): void { + if (typeof window !== "undefined" && !window.dataLayer) { + window.dataLayer = []; + } + } + + public trackEvent( + category: string, + action: string, + label?: string, + value?: number + ): void { + if (!this.enabled) return; + + const event: TelemetryEvent = { + event: "custom_event", + category, + action, + label, + value, + timestamp: new Date().toISOString(), + }; + + // Push para dataLayer (GTM) + if (typeof window !== "undefined" && window.dataLayer) { + window.dataLayer.push(event); + } + + // Log no console (desenvolvimento) + if (import.meta.env.DEV) { + console.log("[Telemetry]", event); + } + } + + public trackCTA(ctaName: string, destination: string): void { + this.trackEvent("CTA", "click", `${ctaName} -> ${destination}`); + } + + public trackProfileChange( + fromProfile: string | null, + toProfile: string + ): void { + this.trackEvent( + "Profile", + "change", + `${fromProfile || "none"} -> ${toProfile}` + ); + } + + public trackNavigation(from: string, to: string): void { + this.trackEvent("Navigation", "page_view", `${from} -> ${to}`); + } + + public trackError(errorType: string, errorMessage: string): void { + this.trackEvent("Error", errorType, errorMessage); + } + + public disable(): void { + this.enabled = false; + } + + public enable(): void { + this.enabled = true; + } +} + +export const telemetry = new TelemetryService();