saveinmed/saveinmed-frontend/src/components/Header.tsx
NANDO9322 0a0c344022 feat(geral): implementa fluxo de aprovação, api de endereços e acesso master
Backend:
- Implementa API de Endereços (`POST /enderecos`) e migration da tabela `addresses`.
- Adiciona bloqueio de login para usuários de empresas não verificadas (status `pending`).
- Criação automática do usuário Master (`seedAdmin`) com empresa verificada.
- Adiciona aliases de rota em PT-BR (`/api/v1/empresas` GET/PATCH, `/api/v1/usuarios` PATCH) para compatibilidade com o frontend.
- Atualiza DTOs para suportar campos em português no registro de empresas e atualização de usuários.
- Endpoint `/auth/me` agora retorna `company_name` e flag `superadmin`.
- Ajusta filtro de repositório para listar empresas por status de verificação.

Frontend:
- Nova página `/usuarios-pendentes` com layout padrão e funcionalidade de aprovação.
- Atualiza [Header](cci:1://file:///c:/Projetos/saveinmed/saveinmed-frontend/src/components/Header.tsx:29:0-337:2) para exibir o nome da empresa do usuário logado.
- Serviço `empresaApiService`: correções de mapeamento (`corporate_name` -> `razao_social`) e novos métodos.
- Tipagem atualizada para incluir campos de empresa no [UserData](cci:2://file:///c:/Projetos/saveinmed/saveinmed-frontend/src/types/auth.ts:15:0-30:1).

Fixes:
- Correção de erro 405 (Method Not Allowed) nas rotas de atualização.
- Correção de erro 404 na listagem de pendentes.
- Resolução de campos vazios na listagem de empresas.
2026-01-21 17:20:06 -03:00

340 lines
13 KiB
TypeScript

"use client";
import { useState, useEffect, useRef } from "react";
import Link from "next/link";
import Image from "next/image";
import { useRouter } from "next/navigation";
// import { Models } from "appwrite"; // Removido - não usamos mais Appwrite
import Navbar from "./Navbar";
// import RegistroIncompletoModal from "./RegistroIncompletoModal"; // Removido - agora é controlado pelo dashboard
import CarrinhoCompras from "./CarrinhoCompras";
import LojaVirtualMenu from "./LojaVirtualMenu";
import { useRegistroCompleto } from "@/hooks/useRegistroCompleto";
import {
ShoppingCartIcon,
XMarkIcon,
PlusIcon,
MinusIcon,
UserIcon,
ArrowRightOnRectangleIcon,
BuildingOffice2Icon,
BellIcon,
} from "@heroicons/react/24/outline";
interface HeaderProps {
user?: any | null; // Tipo genérico ao invés de Models.User
title?: string;
subtitle?: string;
showBackButton?: boolean;
}
const Header = ({
user,
title = "SaveInMed",
subtitle = "Plataforma B2B de Medicamentos",
showBackButton = false,
}: HeaderProps) => {
const router = useRouter();
const [isOpen, setIsOpen] = useState(false);
// const [showRegistroModal, setShowRegistroModal] = useState(false); // Removido - agora é controlado pelo dashboard
const dropdownRef = useRef<HTMLDivElement>(null);
const displayUserName =
user?.nome ||
user?.["nome-civil"] ||
user?.["nome-social"] ||
user?.name ||
"Usuário";
const displayCompanyName =
user?.company_name ||
user?.empresa?.["nome-fantasia"] ||
user?.empresa?.["razao-social"] ||
user?.empresa?.nomeFantasia ||
user?.empresa?.razaoSocial ||
user?.empresaData?.["nome-fantasia"] ||
user?.empresaData?.["razao-social"] ||
user?.empresaData?.nomeFantasia ||
user?.empresaData?.razaoSocial ||
(Array.isArray(user?.empresas) ? user.empresas[0] : null) ||
(Array.isArray(user?.empresasDados) ? user.empresasDados[0] : null) ||
"Empresa não informada";
// Hook para verificar se o registro está completo
const { isCompleto, loading, dadosFaltantes } = useRegistroCompleto(user || null);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node)
) {
setIsOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
// // Verificar se deve mostrar o modal de registro incompleto - REMOVIDO
// // Agora essa funcionalidade é controlada pelo dashboard
// useEffect(() => {
// // Funcionalidade movida para o dashboard
// }, [loading, isCompleto, user]);
const handleLogout = async () => {
try {
// 1. Fazer logout no BFF
const token = localStorage.getItem('access_token');
if (token) {
const response = await fetch(`${process.env.NEXT_PUBLIC_BFF_API_URL}/auth/logout`, {
method: 'POST',
headers: {
'accept': '*/*',
'Authorization': `Bearer ${token}`,
},
});
}
// 2. Limpar todos os dados locais
localStorage.removeItem("access_token");
localStorage.removeItem("user");
localStorage.removeItem("registro-incompleto-dismissed");
localStorage.removeItem("completar-registro-usuario-id");
// 3. Redirecionar para a página de login
router.push("/login");
} catch (error) {
console.error("❌ Erro no logout:", error);
// Mesmo com erro, limpar dados locais e redirecionar
localStorage.clear();
router.push("/login");
}
};
return (
<>
<header className="bg-white shadow-sm border-b">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
{/* Logo e navegação */}
<div className="flex items-center min-w-0 -ml-4 sm:-ml-6 lg:-ml-8">
{showBackButton && (
<button
onClick={() => router.push("/dashboard")}
className="text-gray-600 hover:text-gray-900 transition-colors cursor-pointer flex-shrink-0 mr-1"
>
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
</button>
)}
<Link
href="/dashboard"
className="w-16 h-16 sm:w-20 sm:h-20 lg:w-24 lg:h-24 flex items-center justify-center flex-shrink-0"
>
<Image
src="/logo.png"
alt="SaveInMed Logo"
width={96}
height={96}
className="w-16 h-16 sm:w-20 sm:h-20 lg:w-24 lg:h-24 object-contain"
/>
</Link>
<div className="min-w-0 flex-shrink -ml-1">
<h1 className="text-lg sm:text-xl lg:text-2xl font-bold text-gray-900 truncate">
{title}
</h1>
<p className="text-xs text-gray-500 hidden sm:block truncate">
{subtitle}
</p>
</div>
<div className="ml-2 sm:ml-4 flex items-center">
<CarrinhoCompras />
</div>
</div>
{/* Informações do usuário */}
<div className="flex items-center space-x-2 sm:space-x-4 flex-shrink-0">
{/* Menu Loja Virtual */}
<LojaVirtualMenu />
<button
type="button"
className="relative rounded-full p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-50 transition-colors"
aria-label="Notificações"
>
<BellIcon className="w-5 h-5 sm:w-6 sm:h-6" />
</button>
{/* Indicador de cadastro incompleto */}
{!loading && !isCompleto && (
<div className="flex items-center space-x-1 sm:space-x-2">
<div className="w-2 h-2 bg-red-500 rounded-full animate-pulse flex-shrink-0"></div>
<span className="text-xs text-red-600 font-medium hidden md:block whitespace-nowrap">
Cadastro incompleto
</span>
</div>
)}
<div
className="relative"
ref={dropdownRef}
onMouseEnter={() => setIsOpen(true)}
onMouseLeave={() => setIsOpen(false)}
>
<button
onClick={() => setIsOpen(!isOpen)}
className="flex items-center space-x-1 sm:space-x-3 hover:bg-gray-50 rounded-lg p-1 sm:p-2 transition-colors cursor-pointer"
>
<div className="w-6 h-6 sm:w-8 sm:h-8 bg-gradient-to-r from-blue-500 to-green-500 rounded-full flex items-center justify-center flex-shrink-0">
<span className="text-white text-xs sm:text-sm font-semibold">
{displayUserName?.charAt(0).toUpperCase()}
</span>
</div>
<div className="hidden sm:block min-w-0">
<p className="text-sm font-medium text-gray-900 truncate">
{displayUserName}
</p>
<p className="text-xs text-gray-500 truncate">
{displayCompanyName}
</p>
</div>
<svg
className="w-3 h-3 sm:w-4 sm:h-4 text-gray-400 flex-shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 9l-7 7-7-7"
/>
</svg>
</button>
{isOpen && (
<>
{/* Bridge do prevent closing */}
<div className="absolute top-full right-0 w-48 h-2 bg-transparent z-10" />
<ul className="absolute right-0 mt-2 w-48 bg-white rounded-lg shadow-lg z-10 border">
<li>
<div className="px-4 py-3 border-b rounded-t-lg">
<div className="text-[10px] uppercase tracking-wide text-gray-400">
Empresa
</div>
<p className="text-sm font-semibold text-gray-900 truncate">
{displayCompanyName}
</p>
<div className="mt-2 text-[10px] uppercase tracking-wide text-gray-400">
Perfil do usuário
</div>
<p className="text-sm font-semibold text-gray-900 truncate">
{displayUserName}
</p>
</div>
</li>
<li>
<Link
href="/meu-perfil#usuario"
className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center"
>
<UserIcon className="w-4 h-4 mr-2 flex-shrink-0" />
Perfil do usuário
</Link>
</li>
<li>
<Link
href="/meu-perfil#empresa"
className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center"
>
<BuildingOffice2Icon className="w-4 h-4 mr-2 flex-shrink-0" />
Perfil da empresa
</Link>
</li>
<li>
<Link
href="/pedidos"
className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center"
>
<svg
className="w-4 h-4 mr-2 flex-shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z"
/>
</svg>
Meus Pedidos
</Link>
</li>
<li>
<Link
href="/mensagens"
className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center"
>
<svg
className="w-4 h-4 mr-2 flex-shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
/>
</svg>
Mensagens
</Link>
</li>
<li>
<button
onClick={handleLogout}
className="w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-red-50 rounded-b-lg flex items-center"
>
<ArrowRightOnRectangleIcon className="w-4 h-4 mr-2 flex-shrink-0" />
Sair
</button>
</li>
</ul>
</>
)}
</div>
</div>
</div>
{/* Navbar Mobile - Aparece abaixo do header em telas pequenas */}
<div className="lg:hidden border-t border-gray-100 py-2">
<Navbar />
</div>
</div>
</header>
{/* Modal de Registro Incompleto Removido - Agora controlado pelo dashboard */}
</>
);
};
export default Header;