+ );
+ }
- // Enquanto verifica, não renderiza nada ou um loading
- // Para evitar "flash" de conteúdo não autorizado
if (!authorized) {
- return null;
+ return (
+
+
+
+ );
}
return <>{children}>;
diff --git a/front-end-agency/components/auth/LoginBranding.tsx b/front-end-agency/components/auth/LoginBranding.tsx
new file mode 100644
index 0000000..b53717b
--- /dev/null
+++ b/front-end-agency/components/auth/LoginBranding.tsx
@@ -0,0 +1,138 @@
+'use client';
+
+import { useEffect, useState } from 'react';
+
+/**
+ * LoginBranding - Aplica cor primária da agência na página de login
+ * Busca cor do localStorage ou da API se não houver cache
+ */
+export function LoginBranding() {
+ const [mounted, setMounted] = useState(false);
+
+ useEffect(() => {
+ setMounted(true);
+ }, []);
+
+ useEffect(() => {
+ if (!mounted) return;
+
+ const hexToRgb = (hex: string) => {
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
+ return result ? `${parseInt(result[1], 16)} ${parseInt(result[2], 16)} ${parseInt(result[3], 16)}` : null;
+ };
+
+ const applyTheme = (primary: string) => {
+ if (!primary) return;
+
+ const root = document.documentElement;
+ const primaryRgb = hexToRgb(primary);
+
+ root.style.setProperty('--brand-color', primary);
+ root.style.setProperty('--gradient', `linear-gradient(135deg, ${primary}, ${primary})`);
+
+ if (primaryRgb) {
+ root.style.setProperty('--brand-rgb', primaryRgb);
+ root.style.setProperty('--brand-strong-rgb', primaryRgb);
+ root.style.setProperty('--brand-hover-rgb', primaryRgb);
+ }
+ };
+
+ const updateFavicon = (url: string) => {
+ if (typeof window === 'undefined' || typeof document === 'undefined') return;
+
+ try {
+ console.log('🎨 LoginBranding: Atualizando favicon para:', url);
+ const newHref = `${url}${url.includes('?') ? '&' : '?'}v=${Date.now()}`;
+
+ // Buscar TODOS os links de ícone existentes
+ const existingLinks = document.querySelectorAll("link[rel*='icon']");
+
+ if (existingLinks.length > 0) {
+ // Atualizar href de todos os links existentes (SEM REMOVER)
+ existingLinks.forEach(link => {
+ link.setAttribute('href', newHref);
+ });
+ console.log(`✅ ${existingLinks.length} favicons atualizados`);
+ } else {
+ // Criar novo link apenas se não existir nenhum
+ const newLink = document.createElement('link');
+ newLink.rel = 'icon';
+ newLink.type = 'image/x-icon';
+ newLink.href = newHref;
+ document.head.appendChild(newLink);
+ console.log('✅ Novo favicon criado');
+ }
+ } catch (error) {
+ console.error('❌ Erro ao atualizar favicon:', error);
+ }
+ };
+
+ const loadBranding = async () => {
+ if (typeof window === 'undefined') return;
+
+ const hostname = window.location.hostname;
+ const subdomain = hostname.split('.')[0];
+
+ // Para dash.localhost ou localhost sem subdomínio, não buscar
+ if (!subdomain || subdomain === 'localhost' || subdomain === 'www' || subdomain === 'dash') {
+ return;
+ }
+
+ try {
+ // 1. Buscar DIRETO do backend (bypass da rota Next.js que está com problema)
+ console.log('LoginBranding: Buscando cores para:', subdomain);
+ const apiUrl = `/api/tenant/config?subdomain=${subdomain}`;
+ console.log('LoginBranding: URL:', apiUrl);
+
+ const response = await fetch(apiUrl);
+
+ if (response.ok) {
+ const data = await response.json();
+ console.log('LoginBranding: Dados recebidos:', data);
+
+ if (data.primary_color) {
+ applyTheme(data.primary_color);
+ localStorage.setItem('agency-primary-color', data.primary_color);
+ console.log('LoginBranding: Cor aplicada!');
+ }
+
+ if (data.logo_url) {
+ updateFavicon(data.logo_url);
+ localStorage.setItem('agency-logo-url', data.logo_url);
+ console.log('LoginBranding: Favicon aplicado!');
+ }
+ return;
+ } else {
+ console.error('LoginBranding: API retornou:', response.status);
+ }
+
+ // 2. Fallback para cache
+ console.log('LoginBranding: Tentando cache');
+ const cachedPrimary = localStorage.getItem('agency-primary-color');
+ const cachedLogo = localStorage.getItem('agency-logo-url');
+
+ if (cachedPrimary) {
+ applyTheme(cachedPrimary);
+ }
+ if (cachedLogo) {
+ updateFavicon(cachedLogo);
+ }
+ } catch (error) {
+ console.error('LoginBranding: Erro:', error);
+ const cachedPrimary = localStorage.getItem('agency-primary-color');
+ const cachedLogo = localStorage.getItem('agency-logo-url');
+
+ if (cachedPrimary) {
+ applyTheme(cachedPrimary);
+ }
+ if (cachedLogo) {
+ updateFavicon(cachedLogo);
+ }
+ }
+ };
+
+ loadBranding();
+ }, [mounted]);
+
+ return null;
+}
diff --git a/front-end-agency/components/layout/AgencyBranding.tsx b/front-end-agency/components/layout/AgencyBranding.tsx
new file mode 100644
index 0000000..5f44b91
--- /dev/null
+++ b/front-end-agency/components/layout/AgencyBranding.tsx
@@ -0,0 +1,169 @@
+'use client';
+
+import { useEffect, useState } from 'react';
+
+interface AgencyBrandingProps {
+ colors?: {
+ primary: string;
+ secondary: string;
+ } | null;
+}
+
+/**
+ * AgencyBranding - Aplica as cores da agência via CSS Variables
+ * O favicon agora é tratado via Metadata API no layout (server-side)
+ */
+export function AgencyBranding({ colors }: AgencyBrandingProps) {
+ const [mounted, setMounted] = useState(false);
+
+ const [debugInfo, setDebugInfo] = useState('Iniciando...');
+
+ useEffect(() => {
+ setMounted(true);
+ }, []);
+
+ useEffect(() => {
+ if (!mounted) return;
+
+ const hexToRgb = (hex: string) => {
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
+ return result ? `${parseInt(result[1], 16)} ${parseInt(result[2], 16)} ${parseInt(result[3], 16)}` : null;
+ };
+
+ const applyTheme = (primary: string, secondary: string) => {
+ if (!primary || !secondary) return;
+
+ const root = document.documentElement;
+ const primaryRgb = hexToRgb(primary);
+ const secondaryRgb = hexToRgb(secondary);
+
+ const gradient = `linear-gradient(135deg, ${primary}, ${primary})`;
+ const gradientText = `linear-gradient(to right, ${primary}, ${primary})`;
+
+ root.style.setProperty('--gradient', gradient);
+ root.style.setProperty('--gradient-text', gradientText);
+ root.style.setProperty('--gradient-primary', gradient);
+ root.style.setProperty('--color-gradient-brand', gradient);
+
+ root.style.setProperty('--brand-color', primary);
+ root.style.setProperty('--brand-color-strong', secondary);
+
+ if (primaryRgb) root.style.setProperty('--brand-rgb', primaryRgb);
+ if (secondaryRgb) root.style.setProperty('--brand-strong-rgb', secondaryRgb);
+
+ // Salvar no localStorage para cache
+ if (typeof window !== 'undefined') {
+ const hostname = window.location.hostname;
+ const sub = hostname.split('.')[0];
+ if (sub && sub !== 'www') {
+ localStorage.setItem(`agency-theme:${sub}`, gradient);
+ localStorage.setItem('agency-primary-color', primary);
+ localStorage.setItem('agency-secondary-color', secondary);
+ }
+ }
+ };
+
+ const updateFavicon = (url: string) => {
+ if (typeof window === 'undefined' || typeof document === 'undefined') return;
+
+ try {
+ setDebugInfo(`Tentando atualizar favicon: ${url}`);
+ console.log('🎨 AgencyBranding: Atualizando favicon para:', url);
+
+ const newHref = `${url}${url.includes('?') ? '&' : '?'}v=${Date.now()}`;
+
+ // Buscar TODOS os links de ícone existentes
+ const existingLinks = document.querySelectorAll("link[rel*='icon']");
+
+ if (existingLinks.length > 0) {
+ // Atualizar href de todos os links existentes (SEM REMOVER)
+ existingLinks.forEach(link => {
+ link.setAttribute('href', newHref);
+ });
+ setDebugInfo(`Favicon atualizado (${existingLinks.length} links)`);
+ console.log(`✅ ${existingLinks.length} favicons atualizados`);
+ } else {
+ // Criar novo link apenas se não existir nenhum
+ const newLink = document.createElement('link');
+ newLink.rel = 'icon';
+ newLink.type = 'image/x-icon';
+ newLink.href = newHref;
+ document.head.appendChild(newLink);
+ setDebugInfo('Novo favicon criado');
+ console.log('✅ Novo favicon criado');
+ }
+ } catch (error) {
+ setDebugInfo(`Erro: ${error}`);
+ console.error('❌ Erro ao atualizar favicon:', error);
+ }
+ };
+
+ // Se temos cores do servidor, aplicar imediatamente
+ if (colors) {
+ applyTheme(colors.primary, colors.secondary);
+ } else {
+ // Fallback: tentar pegar do cache do localStorage
+ const cachedPrimary = localStorage.getItem('agency-primary-color');
+ const cachedSecondary = localStorage.getItem('agency-secondary-color');
+
+ if (cachedPrimary && cachedSecondary) {
+ applyTheme(cachedPrimary, cachedSecondary);
+ }
+ }
+
+ // Atualizar favicon se houver logo salvo (após montar)
+ const cachedLogo = localStorage.getItem('agency-logo-url');
+ if (cachedLogo) {
+ console.log('🔍 Logo encontrado no cache:', cachedLogo);
+ updateFavicon(cachedLogo);
+ } else {
+ setDebugInfo('Nenhum logo no cache');
+ console.log('⚠️ Nenhum logo encontrado no cache');
+ }
+
+ // Listener para atualizações em tempo real (ex: da página de configurações)
+ const handleUpdate = () => {
+ console.log('🔔 Evento branding-update recebido!');
+ setDebugInfo('Evento branding-update recebido');
+
+ const cachedPrimary = localStorage.getItem('agency-primary-color');
+ const cachedSecondary = localStorage.getItem('agency-secondary-color');
+ const cachedLogo = localStorage.getItem('agency-logo-url');
+
+ if (cachedPrimary && cachedSecondary) {
+ console.log('🎨 Aplicando cores do cache');
+ applyTheme(cachedPrimary, cachedSecondary);
+ }
+
+ if (cachedLogo) {
+ console.log('🖼️ Atualizando favicon do cache:', cachedLogo);
+ updateFavicon(cachedLogo);
+ }
+ };
+
+ window.addEventListener('branding-update', handleUpdate);
+
+ return () => {
+ window.removeEventListener('branding-update', handleUpdate);
+ };
+ }, [mounted, colors]);
+
+ if (!mounted) return null;
+
+ return (
+
{/* Sidebar controla seu próprio estado visual via props */}
setIsExpanded(!isExpanded)}
menuItems={menuItems}
@@ -32,7 +31,9 @@ export const DashboardLayout: React.FC = ({ children, menu
{/* Conteúdo das páginas */}
- {children}
+
+ {children}
+
diff --git a/front-end-agency/components/layout/FaviconUpdater.tsx b/front-end-agency/components/layout/FaviconUpdater.tsx
new file mode 100644
index 0000000..65397a9
--- /dev/null
+++ b/front-end-agency/components/layout/FaviconUpdater.tsx
@@ -0,0 +1,54 @@
+'use client';
+
+import { useEffect, useState } from 'react';
+import { getUser } from '@/lib/auth';
+
+export function FaviconUpdater() {
+ const [mounted, setMounted] = useState(false);
+
+ useEffect(() => {
+ setMounted(true);
+ }, []);
+
+ useEffect(() => {
+ if (!mounted) return;
+
+ const updateFavicon = () => {
+ const user = getUser();
+ if (user?.logoUrl) {
+ // Usar requestAnimationFrame para garantir que o DOM esteja estável após hidratação
+ requestAnimationFrame(() => {
+ const link: HTMLLinkElement = document.querySelector("link[rel*='icon']") || document.createElement('link');
+ link.type = 'image/x-icon';
+ link.rel = 'shortcut icon';
+ link.href = user.logoUrl!;
+ if (!link.parentNode) {
+ document.getElementsByTagName('head')[0].appendChild(link);
+ }
+ });
+ }
+ };
+
+ // Atraso pequeno para garantir que a hidratação terminou
+ const timer = setTimeout(() => {
+ updateFavicon();
+ }, 0);
+
+ // Ouve mudanças no localStorage
+ const handleStorage = () => {
+ requestAnimationFrame(() => updateFavicon());
+ };
+ window.addEventListener('storage', handleStorage);
+
+ // Custom event para atualização interna na mesma aba
+ window.addEventListener('auth-update', handleStorage);
+
+ return () => {
+ clearTimeout(timer);
+ window.removeEventListener('storage', handleStorage);
+ window.removeEventListener('auth-update', handleStorage);
+ };
+ }, [mounted]);
+
+ return null;
+}
diff --git a/front-end-agency/components/layout/SidebarRail.tsx b/front-end-agency/components/layout/SidebarRail.tsx
index 0917ec1..2e19659 100644
--- a/front-end-agency/components/layout/SidebarRail.tsx
+++ b/front-end-agency/components/layout/SidebarRail.tsx
@@ -1,11 +1,12 @@
'use client';
-import React, { useState, useEffect } from 'react';
+import React, { useState, useEffect, useRef } from 'react';
import Link from 'next/link';
import { usePathname, useRouter } from 'next/navigation';
-import { Menu, Transition } from '@headlessui/react';
-import { Fragment } from 'react';
+import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/react';
import { useTheme } from 'next-themes';
+import { getUser, User, getToken, saveAuth } from '@/lib/auth';
+import { API_ENDPOINTS } from '@/lib/api';
import {
ChevronLeftIcon,
ChevronRightIcon,
@@ -30,16 +31,12 @@ export interface MenuItem {
}
interface SidebarRailProps {
- activeTab: string;
- onTabChange: (tab: string) => void;
isExpanded: boolean;
onToggle: () => void;
menuItems: MenuItem[];
}
export const SidebarRail: React.FC = ({
- activeTab,
- onTabChange,
isExpanded,
onToggle,
menuItems,
@@ -48,12 +45,93 @@ export const SidebarRail: React.FC = ({
const router = useRouter();
const { theme, setTheme } = useTheme();
const [mounted, setMounted] = useState(false);
+ const [user, setUser] = useState(null);
const [openSubmenu, setOpenSubmenu] = useState(null);
+ const sidebarRef = useRef(null);
useEffect(() => {
setMounted(true);
+ const currentUser = getUser();
+ setUser(currentUser);
+
+ // Buscar perfil da agência para atualizar logo e nome
+ const fetchProfile = async () => {
+ const token = getToken();
+ if (!token) return;
+
+ try {
+ const res = await fetch(API_ENDPOINTS.agencyProfile, {
+ headers: {
+ 'Authorization': `Bearer ${token}`
+ }
+ });
+
+ if (res.ok) {
+ const data = await res.json();
+ if (currentUser) {
+ const updatedUser = {
+ ...currentUser,
+ company: data.name || currentUser.company,
+ logoUrl: data.logo_url
+ };
+ setUser(updatedUser);
+ saveAuth(token, updatedUser); // Persistir atualização
+
+ // Atualizar localStorage do logo para uso do favicon
+ if (data.logo_url) {
+ console.log('📝 Salvando logo no localStorage:', data.logo_url);
+ localStorage.setItem('agency-logo-url', data.logo_url);
+ window.dispatchEvent(new Event('auth-update')); // Notificar favicon
+ window.dispatchEvent(new Event('branding-update')); // Notificar AgencyBranding
+ }
+ }
+ }
+ } catch (error) {
+ console.error('Error fetching agency profile:', error);
+ }
+ };
+
+ fetchProfile();
+
+ // Listener para atualizar logo em tempo real após upload
+ // REMOVIDO: Causa loop infinito com o dispatchEvent dentro do fetchProfile
+ // O AgencyBranding já cuida de atualizar o favicon/cores
+ // Se precisar atualizar o sidebar após upload, usar um evento específico 'logo-uploaded'
+ /*
+ const handleBrandingUpdate = () => {
+ console.log('SidebarRail: branding-update event received');
+ fetchProfile(); // Re-buscar perfil do backend
+ };
+
+ window.addEventListener('branding-update', handleBrandingUpdate);
+
+ return () => {
+ window.removeEventListener('branding-update', handleBrandingUpdate);
+ };
+ */
}, []);
+ // Fechar submenu ao clicar fora
+ useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ if (sidebarRef.current && !sidebarRef.current.contains(event.target as Node)) {
+ // Verifica se o submenu aberto corresponde à rota atual
+ // Se estivermos navegando dentro do módulo (ex: CRM), o menu deve permanecer fixo
+ const activeItem = menuItems.find(item => item.id === openSubmenu);
+ const isRouteActive = activeItem && activeItem.subItems?.some(sub => pathname === sub.href || pathname.startsWith(sub.href));
+
+ if (!isRouteActive) {
+ setOpenSubmenu(null);
+ }
+ }
+ };
+
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+ }, [openSubmenu, pathname, menuItems]);
+
// Auto-open submenu if active
useEffect(() => {
if (isExpanded && pathname) {
@@ -69,7 +147,7 @@ export const SidebarRail: React.FC = ({
const handleLogout = () => {
localStorage.removeItem('token');
localStorage.removeItem('user');
- router.push('/login');
+ window.location.href = '/login';
};
const toggleTheme = () => {
@@ -79,46 +157,59 @@ export const SidebarRail: React.FC = ({
// Encontrar o item ativo para renderizar o submenu
const activeMenuItem = menuItems.find(item => item.id === openSubmenu);
+ // Lógica de largura do Rail: Se tiver submenu aberto, força recolhimento visual (80px)
+ // Se não, respeita o estado isExpanded
+ const railWidth = isExpanded && !openSubmenu ? 'w-[240px]' : 'w-[80px]';
+ const showLabels = isExpanded && !openSubmenu;
+
return (
-
+
+ {/* Rail Principal (Ícones + Labels Opcionais) */}
{/* Toggle Button - Floating on the border */}
-
- {isExpanded ? (
-
- ) : (
-
- )}
-
+ {/* Só mostra o toggle se não tiver submenu aberto, para evitar confusão */}
+ {!openSubmenu && (
+
+ {isExpanded ? (
+
+ ) : (
+
+ )}
+
+ )}
{/* Header com Logo */}
-
{menuItems.map((item) => (
= ({
icon={item.icon}
href={item.href}
active={pathname === item.href || (item.href !== '/dashboard' && pathname?.startsWith(item.href))}
- onClick={() => {
+ onClick={(e: any) => {
if (item.subItems) {
- setOpenSubmenu(openSubmenu === item.id ? null : item.id);
+ // Se já estiver aberto, fecha e previne navegação (opcional)
+ if (openSubmenu === item.id) {
+ // Se quisermos permitir fechar sem navegar:
+ // e.preventDefault();
+ // setOpenSubmenu(null);
+
+ // Mas se o usuário quer ir para a home do módulo, deixamos navegar.
+ // O useEffect vai reabrir se a rota for do módulo.
+ // Para forçar o fechamento, teríamos que ter lógica mais complexa.
+ // Vamos assumir que clicar no pai sempre leva pra home do pai.
+ // E o useEffect cuida de abrir o menu.
+ // Então NÃO fazemos nada aqui se for abrir.
+ } else {
+ // Se for abrir, deixamos o Link navegar.
+ // O useEffect vai abrir o menu quando a rota mudar.
+ // NÃO setamos o estado aqui para evitar conflito com a navegação.
+ }
} else {
- onTabChange(item.id);
setOpenSubmenu(null);
}
}}
- isExpanded={isExpanded}
+ showLabel={showLabels}
hasSubItems={!!item.subItems}
isOpen={openSubmenu === item.id}
/>
))}
- {/* Separador antes do menu de usuário */}
-
+ {/* Separador */}
+
- {/* User Menu - Footer */}
-