Files
aggios.app/front-end-agency/app/(agency)/configuracoes/page.tsx
Erik Silva 2f1cf2bb2a v1.4: Segurança multi-tenant, file serving via API e UX humanizada
-  Validação cross-tenant no login e rotas protegidas
-  File serving via /api/files/{bucket}/{path} (eliminação DNS)
-  Mensagens de erro humanizadas inline (sem pop-ups)
-  Middleware tenant detection via headers customizados
-  Upload de logos retorna URLs via API
-  README atualizado com changelog v1.4 completo
2025-12-13 15:05:51 -03:00

1175 lines
66 KiB
TypeScript

"use client";
import { useState, useEffect } from 'react';
import { Tab } from '@headlessui/react';
import { Button, Dialog, Input } from '@/components/ui';
import { Toaster, toast } from 'react-hot-toast';
import {
BuildingOfficeIcon,
PhotoIcon,
UserGroupIcon,
ShieldCheckIcon,
BellIcon,
ArrowUpTrayIcon,
TrashIcon,
CheckCircleIcon,
} from '@heroicons/react/24/outline';
const tabs = [
{ name: 'Dados da Agência', icon: BuildingOfficeIcon },
{ name: 'Logo e Marca', icon: PhotoIcon },
{ name: 'Equipe', icon: UserGroupIcon },
{ name: 'Segurança', icon: ShieldCheckIcon },
{ name: 'Notificações', icon: BellIcon },
];
const parseAddressParts = (address: string) => {
if (!address) return { street: '', number: '', complement: '' };
const [streetPart, rest] = address.split(',', 2).map(part => part.trim());
if (!rest) return { street: streetPart, number: '', complement: '' };
const [numberPart, complementPart] = rest.split('-', 2).map(part => part.trim());
return {
street: streetPart,
number: numberPart || '',
complement: complementPart || '',
};
};
export default function ConfiguracoesPage() {
const [selectedTab, setSelectedTab] = useState(0);
const [showSuccessDialog, setShowSuccessDialog] = useState(false);
const [successMessage, setSuccessMessage] = useState('');
const [showSupportDialog, setShowSupportDialog] = useState(false);
const [supportMessage, setSupportMessage] = useState('Para alterar estes dados, contate o suporte.');
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [loadingCep, setLoadingCep] = useState(false);
const [uploadingLogo, setUploadingLogo] = useState(false);
const [logoPreview, setLogoPreview] = useState<string | null>(null);
const [logoHorizontalPreview, setLogoHorizontalPreview] = useState<string | null>(null);
// Dados da agência (buscados da API)
const [agencyData, setAgencyData] = useState({
name: '',
cnpj: '',
email: '',
phone: '',
website: '',
address: '',
street: '',
number: '',
complement: '',
neighborhood: '',
city: '',
state: '',
zip: '',
razaoSocial: '',
description: '',
industry: '',
teamSize: '',
logoUrl: '',
logoHorizontalUrl: '',
primaryColor: '#ff3a05',
secondaryColor: '#ff0080',
});
// Live Preview da Cor Primária
useEffect(() => {
if (agencyData.primaryColor) {
const root = document.documentElement;
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 primaryRgb = hexToRgb(agencyData.primaryColor);
if (primaryRgb) {
root.style.setProperty('--brand-rgb', primaryRgb);
root.style.setProperty('--brand-strong-rgb', primaryRgb);
root.style.setProperty('--brand-hover-rgb', primaryRgb);
}
root.style.setProperty('--brand-color', agencyData.primaryColor);
root.style.setProperty('--gradient', `linear-gradient(135deg, ${agencyData.primaryColor}, ${agencyData.primaryColor})`);
}
}, [agencyData.primaryColor]);
// Dados para alteração de senha
const [passwordData, setPasswordData] = useState({
currentPassword: '',
newPassword: '',
confirmPassword: '',
});
useEffect(() => {
const fetchAgencyData = async () => {
try {
setLoading(true);
const token = localStorage.getItem('token');
const userData = localStorage.getItem('user');
if (!token || !userData) {
console.error('Usuário não autenticado');
setLoading(false);
return;
}
// Buscar dados da API
const response = await fetch('/api/agency/profile', {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
if (response.ok) {
const data = await response.json();
console.log('DEBUG: API response data:', data);
console.log('DEBUG: logo_url:', data.logo_url);
console.log('DEBUG: logo_horizontal_url:', data.logo_horizontal_url);
const parsedAddress = parseAddressParts(data.address || '');
setAgencyData({
name: data.name || '',
cnpj: data.cnpj || '',
email: data.email || '',
phone: data.phone || '',
website: data.website || '',
address: data.address || '',
street: parsedAddress.street,
number: data.number || parsedAddress.number,
complement: data.complement || parsedAddress.complement,
neighborhood: data.neighborhood || '',
city: data.city || '',
state: data.state || '',
zip: data.zip || '',
razaoSocial: data.razao_social || '',
description: data.description || '',
industry: data.industry || '',
teamSize: data.team_size || '',
logoUrl: data.logo_url || '',
logoHorizontalUrl: data.logo_horizontal_url || '',
primaryColor: data.primary_color || '#ff3a05',
secondaryColor: data.secondary_color || '#ff0080',
});
// Set logo previews
console.log('DEBUG: Setting previews...');
if (data.logo_url) {
console.log('DEBUG: Setting logoPreview to:', data.logo_url);
setLogoPreview(data.logo_url);
}
if (data.logo_horizontal_url) {
console.log('DEBUG: Setting logoHorizontalPreview to:', data.logo_horizontal_url);
setLogoHorizontalPreview(data.logo_horizontal_url);
}
} else {
console.error('Erro ao buscar dados:', response.status);
// Fallback para localStorage se API falhar
const savedData = localStorage.getItem('cadastroData');
if (savedData) {
const data = JSON.parse(savedData);
const user = JSON.parse(userData);
setAgencyData({
name: data.formData?.companyName || '',
cnpj: data.formData?.cnpj || '',
email: data.formData?.email || user.email || '',
phone: data.contacts?.[0]?.phone || '',
website: data.formData?.website || '',
address: `${data.cepData?.logradouro || ''}, ${data.formData?.number || ''}`,
street: data.cepData?.logradouro || '',
number: data.formData?.number || '',
complement: data.formData?.complement || '',
neighborhood: data.cepData?.bairro || '',
city: data.cepData?.localidade || '',
state: data.cepData?.uf || '',
zip: data.formData?.cep || '',
razaoSocial: data.cnpjData?.razaoSocial || '',
description: data.formData?.description || '',
industry: data.formData?.industry || '',
teamSize: data.formData?.teamSize || '',
logoUrl: '',
logoHorizontalUrl: '',
primaryColor: '#ff3a05',
secondaryColor: '#ff0080',
});
}
}
} catch (error) {
console.error('Erro ao buscar dados da agência:', error);
setSuccessMessage('Erro ao carregar dados da agência.');
setShowSuccessDialog(true);
} finally {
setLoading(false);
}
};
fetchAgencyData();
}, []);
const handleLogoUpload = async (file: File, type: 'logo' | 'horizontal') => {
if (!file) return;
// Validar tipo de arquivo
if (!file.type.startsWith('image/')) {
toast.error('Por favor, selecione uma imagem válida');
return;
}
// Validar tamanho (2MB)
if (file.size > 2 * 1024 * 1024) {
toast.error('A imagem deve ter no máximo 2MB');
return;
}
setUploadingLogo(true);
try {
const token = localStorage.getItem('token');
if (!token) {
toast.error('Você precisa estar autenticado');
return;
}
const formData = new FormData();
formData.append('logo', file);
formData.append('type', type);
const response = await fetch('/api/agency/logo', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
},
body: formData,
});
if (response.ok) {
const data = await response.json();
const logoUrl = data.logo_url || data.url;
if (type === 'logo') {
setAgencyData(prev => ({ ...prev, logoUrl }));
setLogoPreview(logoUrl);
// Salvar no localStorage para uso do favicon
localStorage.setItem('agency-logo-url', logoUrl);
} else {
setAgencyData(prev => ({ ...prev, logoHorizontalUrl: logoUrl }));
setLogoHorizontalPreview(logoUrl);
}
// Disparar evento para atualizar branding em tempo real
if (typeof window !== 'undefined') {
window.dispatchEvent(new Event('branding-update'));
}
toast.success('Logo enviado com sucesso!');
} else {
const errorData = await response.json().catch(() => ({}));
console.error('Upload error:', errorData);
toast.error(errorData.error || 'Erro ao enviar logo. Tente novamente.');
}
} catch (error) {
console.error('Erro ao fazer upload:', error);
toast.error('Erro ao enviar logo. Verifique sua conexão.');
} finally {
setUploadingLogo(false);
}
};
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>, type: 'logo' | 'horizontal') => {
const file = e.target.files?.[0];
if (file) {
// Create preview immediately
const reader = new FileReader();
reader.onloadend = () => {
const previewUrl = reader.result as string;
if (type === 'logo') {
setLogoPreview(previewUrl);
} else {
setLogoHorizontalPreview(previewUrl);
}
};
reader.readAsDataURL(file);
// Upload file
handleLogoUpload(file, type);
}
};
const formatCep = (value: string) => {
const numbers = value.replace(/\D/g, '');
return numbers.replace(/(\d{5})(\d{0,3})/, '$1-$2').substring(0, 9);
};
const fetchCepData = async (cep: string) => {
const numbers = cep.replace(/\D/g, '');
if (numbers.length !== 8) return;
setLoadingCep(true);
try {
const response = await fetch(`https://viacep.com.br/ws/${numbers}/json/`);
if (!response.ok) {
toast.error('Não foi possível consultar o CEP agora.');
return;
}
const data = await response.json();
if (data?.erro) {
toast.error('CEP não encontrado. Verifique o número.');
setAgencyData(prev => ({
...prev,
address: '',
street: '',
neighborhood: '',
city: '',
state: '',
}));
return;
}
const formattedCep = formatCep(cep);
const nextAddress = data.logradouro || '';
const nextNeighborhood = data.bairro || '';
const nextCity = data.localidade || '';
const nextState = data.uf || '';
setAgencyData(prev => ({
...prev,
zip: formattedCep,
address: nextAddress,
street: nextAddress,
neighborhood: nextNeighborhood,
city: nextCity,
state: nextState,
}));
toast.success('Endereço preenchido pelo CEP');
} catch (error) {
console.error('Erro ao buscar CEP:', error);
toast.error('Erro ao buscar CEP. Tente novamente.');
} finally {
setLoadingCep(false);
}
};
const handleSaveAgency = async () => {
setSaving(true);
try {
const token = localStorage.getItem('token');
if (!token) {
setSuccessMessage('Você precisa estar autenticado.');
setShowSuccessDialog(true);
setSaving(false);
return;
}
const composedAddress = agencyData.street
? `${agencyData.street}${agencyData.number ? `, ${agencyData.number}` : ''}${agencyData.complement ? ` - ${agencyData.complement}` : ''}`
: agencyData.address;
const response = await fetch('/api/agency/profile', {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: agencyData.name,
cnpj: agencyData.cnpj,
email: agencyData.email,
phone: agencyData.phone,
website: agencyData.website,
address: composedAddress,
neighborhood: agencyData.neighborhood,
number: agencyData.number,
complement: agencyData.complement,
city: agencyData.city,
state: agencyData.state,
zip: agencyData.zip,
razao_social: agencyData.razaoSocial,
description: agencyData.description,
industry: agencyData.industry,
team_size: agencyData.teamSize,
primary_color: agencyData.primaryColor,
secondary_color: agencyData.secondaryColor,
}),
});
if (response.ok) {
setSuccessMessage('Dados da agência salvos com sucesso!');
// Atualiza localStorage imediatamente para persistência instantânea
localStorage.setItem('agency-primary-color', agencyData.primaryColor);
localStorage.setItem('agency-secondary-color', agencyData.secondaryColor);
if (agencyData.logoUrl) localStorage.setItem('agency-logo-url', agencyData.logoUrl);
if (agencyData.logoHorizontalUrl) localStorage.setItem('agency-logo-horizontal-url', agencyData.logoHorizontalUrl);
// Disparar evento para atualizar o tema em tempo real
window.dispatchEvent(new Event('branding-update'));
} else {
setSuccessMessage('Erro ao salvar dados. Tente novamente.');
}
} catch (error) {
console.error('Erro ao salvar:', error);
setSuccessMessage('Erro ao salvar dados. Verifique sua conexão.');
} finally {
setSaving(false);
}
setShowSuccessDialog(true);
};
const handleChangePassword = async () => {
// Validações
if (!passwordData.currentPassword) {
setSuccessMessage('Por favor, informe sua senha atual.');
setShowSuccessDialog(true);
return;
}
if (!passwordData.newPassword || passwordData.newPassword.length < 8) {
setSuccessMessage('A nova senha deve ter pelo menos 8 caracteres.');
setShowSuccessDialog(true);
return;
}
if (passwordData.newPassword !== passwordData.confirmPassword) {
setSuccessMessage('As senhas não coincidem.');
setShowSuccessDialog(true);
return;
}
try {
const token = localStorage.getItem('token');
if (!token) {
setSuccessMessage('Você precisa estar autenticado.');
setShowSuccessDialog(true);
return;
}
const response = await fetch('/api/auth/change-password', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
currentPassword: passwordData.currentPassword,
newPassword: passwordData.newPassword,
}),
});
if (response.ok) {
setPasswordData({ currentPassword: '', newPassword: '', confirmPassword: '' });
setSuccessMessage('Senha alterada com sucesso!');
} else {
const error = await response.text();
setSuccessMessage(error || 'Erro ao alterar senha. Verifique sua senha atual.');
}
} catch (error) {
console.error('Erro ao alterar senha:', error);
setSuccessMessage('Erro ao alterar senha. Verifique sua conexão.');
}
setShowSuccessDialog(true);
};
return (
<div className="p-6 max-w-7xl mx-auto">
<Toaster position="top-center" />
{/* Header */}
<div className="mb-8">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">
Configurações
</h1>
<p className="text-gray-600 dark:text-gray-400">
Gerencie as configurações da sua agência
</p>
</div>
{/* Loading State */}
{loading ? (
<div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-12 w-12 border-b border-gray-900 dark:border-gray-100"></div>
</div>
) : (
<>
{/* Tabs */}
<Tab.Group selectedIndex={selectedTab} onChange={setSelectedTab}>
<Tab.List className="flex space-x-1 rounded-xl bg-gray-100 dark:bg-gray-800 p-1 mb-8">
{tabs.map((tab) => {
const Icon = tab.icon;
return (
<Tab
key={tab.name}
className={({ selected }) =>
`w-full flex items-center justify-center space-x-2 rounded-lg py-2.5 text-sm font-medium leading-5 transition-all
${selected
? 'bg-white dark:bg-gray-900 text-gray-900 dark:text-white shadow'
: 'text-gray-600 dark:text-gray-400 hover:bg-white/50 dark:hover:bg-gray-700/50 hover:text-gray-900 dark:hover:text-white'
}`
}
>
<Icon className="w-5 h-5" />
<span className="hidden sm:inline">{tab.name}</span>
</Tab>
);
})}
</Tab.List>
<Tab.Panels>
{/* Tab 1: Dados da Agência */}
<Tab.Panel className="space-y-6">
{/* Card: Informações da Agência */}
<div className="rounded-xl bg-white dark:bg-gray-800 p-6 border border-gray-200 dark:border-gray-700">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-6">
Informações da Agência
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<Input
label="Nome da Agência"
value={agencyData.name}
onChange={(e) => setAgencyData({ ...agencyData, name: e.target.value })}
placeholder="Ex: Minha Agência"
/>
</div>
<div>
<Input
label="Razão Social"
value={agencyData.razaoSocial}
onChange={(e) => setAgencyData({ ...agencyData, razaoSocial: e.target.value })}
placeholder="Razão Social Ltda"
/>
</div>
<div>
<Input
label="CNPJ"
value={agencyData.cnpj}
readOnly
onClick={() => {
setSupportMessage('Para alterar CNPJ, contate o suporte.');
setShowSupportDialog(true);
}}
className="cursor-pointer bg-gray-50 dark:bg-gray-800"
helperText="Alteração via suporte"
/>
</div>
<div>
<Input
label="E-mail (acesso)"
type="email"
value={agencyData.email}
readOnly
onClick={() => {
setSupportMessage('Para alterar o e-mail de acesso, contate o suporte.');
setShowSupportDialog(true);
}}
className="cursor-pointer bg-gray-50 dark:bg-gray-800"
helperText="Alteração via suporte"
/>
</div>
<div>
<Input
label="Telefone / WhatsApp"
type="tel"
value={agencyData.phone}
onChange={(e) => setAgencyData({ ...agencyData, phone: e.target.value })}
placeholder="(00) 00000-0000"
/>
</div>
<div className="md:col-span-2">
<Input
label="Website"
type="url"
value={agencyData.website}
onChange={(e) => setAgencyData({ ...agencyData, website: e.target.value })}
placeholder="https://www.suaagencia.com.br"
leftIcon="ri-global-line"
/>
</div>
<div>
<Input
label="Segmento / Indústria"
value={agencyData.industry}
onChange={(e) => setAgencyData({ ...agencyData, industry: e.target.value })}
placeholder="Ex: Marketing Digital"
/>
</div>
<div>
<Input
label="Tamanho da Equipe"
value={agencyData.teamSize}
onChange={(e) => setAgencyData({ ...agencyData, teamSize: e.target.value })}
placeholder="Ex: 10-50 funcionários"
/>
</div>
</div>
</div>
{/* Card: Contato e Localização */}
<div className="rounded-xl bg-white dark:bg-gray-800 p-6 border border-gray-200 dark:border-gray-700">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-6">
Contato e Localização
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<Input
label="CEP"
value={agencyData.zip}
onChange={(e) => {
const formatted = formatCep(e.target.value);
setAgencyData(prev => ({ ...prev, zip: formatted }));
const numbers = formatted.replace(/\D/g, '');
if (numbers.length === 8) {
fetchCepData(formatted);
}
if (numbers.length === 0) {
setAgencyData(prev => ({
...prev,
address: '',
street: '',
neighborhood: '',
city: '',
state: '',
}));
}
}}
placeholder="00000-000"
rightIcon={loadingCep ? "ri-loader-4-line animate-spin" : undefined}
/>
</div>
<div>
<Input
label="Estado"
value={agencyData.state}
readOnly
className="bg-gray-50 dark:bg-gray-800 cursor-not-allowed"
/>
</div>
<div>
<Input
label="Cidade"
value={agencyData.city}
readOnly
className="bg-gray-50 dark:bg-gray-800 cursor-not-allowed"
/>
</div>
<div>
<Input
label="Bairro"
value={agencyData.neighborhood}
readOnly
className="bg-gray-50 dark:bg-gray-800 cursor-not-allowed"
/>
</div>
<div className="md:col-span-2">
<Input
label="Rua/Avenida"
value={agencyData.street}
readOnly
className="bg-gray-50 dark:bg-gray-800 cursor-not-allowed"
/>
</div>
<div>
<Input
label="Número"
value={agencyData.number}
onChange={(e) => setAgencyData({ ...agencyData, number: e.target.value })}
placeholder="123"
/>
</div>
<div>
<Input
label="Complemento (opcional)"
value={agencyData.complement}
onChange={(e) => setAgencyData({ ...agencyData, complement: e.target.value })}
placeholder="Apto 101, Bloco B"
/>
</div>
</div>
<div className="mt-6 flex justify-end">
<Button
onClick={handleSaveAgency}
variant="primary"
size="lg"
>
Salvar Alterações
</Button>
</div>
</div>
</Tab.Panel>
{/* Tab 2: Logo e Marca */}
<Tab.Panel className="space-y-6">
<div className="rounded-xl bg-white dark:bg-gray-800 p-6 border border-gray-200 dark:border-gray-700">
<div className="mb-6">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
Logo e Identidade Visual
</h2>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
Configure os logos da sua agência que serão exibidos no sistema
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Logo Principal */}
<div className="space-y-4">
<div>
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-1">
Logo Principal (Quadrado)
</h3>
<p className="text-xs text-gray-500 dark:text-gray-400">
Usado no menu lateral e ícones do sistema
</p>
</div>
<div className="relative">
{logoPreview ? (
<div className="space-y-3">
<div className="relative border-2 border-gray-200 dark:border-gray-700 rounded-xl p-8 bg-linear-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800">
<div className="flex items-center justify-center min-h-[140px]">
<img
src={logoPreview}
alt="Logo Principal"
className="max-h-36 max-w-full object-contain drop-shadow-lg"
/>
</div>
{agencyData.logoUrl && (
<div className="absolute top-3 right-3">
<div className="bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 px-2 py-1 rounded-md flex items-center gap-1 text-xs font-medium">
<CheckCircleIcon className="w-3.5 h-3.5" />
Salvo
</div>
</div>
)}
</div>
<div className="flex gap-2">
<label className="flex-1 cursor-pointer">
<input
type="file"
accept="image/png,image/jpeg,image/jpg,image/svg+xml"
onChange={(e) => handleFileSelect(e, 'logo')}
className="hidden"
disabled={uploadingLogo}
/>
<Button
variant="primary"
size="md"
className="w-full pointer-events-none"
disabled={uploadingLogo}
isLoading={uploadingLogo}
type="button"
>
<ArrowUpTrayIcon className="w-4 h-4 mr-2" />
{uploadingLogo ? 'Enviando...' : 'Trocar Logo'}
</Button>
</label>
<Button
variant="outline"
size="md"
onClick={() => {
setLogoPreview(null);
setAgencyData(prev => ({ ...prev, logoUrl: '' }));
}}
disabled={uploadingLogo}
className="border-red-200 dark:border-red-900 text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20"
>
<TrashIcon className="w-4 h-4 mr-2" />
Remover
</Button>
</div>
</div>
) : (
<label className="block cursor-pointer">
<input
type="file"
accept="image/png,image/jpeg,image/jpg,image/svg+xml"
onChange={(e) => handleFileSelect(e, 'logo')}
className="hidden"
disabled={uploadingLogo}
/>
<div className="border-2 border-dashed border-gray-200 dark:border-gray-600 rounded-xl p-8 text-center hover:border-gray-400 dark:hover:border-gray-500 transition-colors bg-gray-50/50 dark:bg-gray-900/50">
<div className="flex flex-col items-center">
<div className="w-16 h-16 rounded-full bg-gray-100 dark:bg-gray-800 flex items-center justify-center mb-4">
<PhotoIcon className="w-8 h-8 text-gray-400 dark:text-gray-500" />
</div>
<p className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Clique para fazer upload
</p>
<p className="text-xs text-gray-500 dark:text-gray-400 mb-4">
ou arraste e solte aqui
</p>
<Button
variant="primary"
size="md"
disabled={uploadingLogo}
isLoading={uploadingLogo}
type="button"
className="pointer-events-none"
>
<ArrowUpTrayIcon className="w-4 h-4 mr-2" />
{uploadingLogo ? 'Enviando...' : 'Selecionar Arquivo'}
</Button>
<p className="text-xs text-gray-400 dark:text-gray-500 mt-3">
PNG, JPG ou SVG Máx. 2MB
</p>
</div>
</div>
</label>
)}
</div>
</div>
{/* Logo Horizontal */}
<div className="space-y-4">
<div>
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-1">
Logo Horizontal (Opcional)
</h3>
<p className="text-xs text-gray-500 dark:text-gray-400">
Usado no cabeçalho e emails
</p>
</div>
<div className="relative">
{logoHorizontalPreview ? (
<div className="space-y-3">
<div className="relative border-2 border-gray-200 dark:border-gray-700 rounded-xl p-8 bg-linear-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800">
<div className="flex items-center justify-center min-h-[140px]">
<img
src={logoHorizontalPreview}
alt="Logo Horizontal"
className="max-h-36 max-w-full object-contain drop-shadow-lg"
/>
</div>
{agencyData.logoHorizontalUrl && (
<div className="absolute top-3 right-3">
<div className="bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 px-2 py-1 rounded-md flex items-center gap-1 text-xs font-medium">
<CheckCircleIcon className="w-3.5 h-3.5" />
Salvo
</div>
</div>
)}
</div>
<div className="flex gap-2">
<label className="flex-1 cursor-pointer">
<input
type="file"
accept="image/png,image/jpeg,image/jpg,image/svg+xml"
onChange={(e) => handleFileSelect(e, 'horizontal')}
className="hidden"
disabled={uploadingLogo}
/>
<Button
variant="primary"
size="md"
className="w-full pointer-events-none"
disabled={uploadingLogo}
isLoading={uploadingLogo}
type="button"
>
<ArrowUpTrayIcon className="w-4 h-4 mr-2" />
{uploadingLogo ? 'Enviando...' : 'Trocar Logo'}
</Button>
</label>
<Button
variant="outline"
size="md"
onClick={() => {
setLogoHorizontalPreview(null);
setAgencyData(prev => ({ ...prev, logoHorizontalUrl: '' }));
}}
disabled={uploadingLogo}
className="border-red-200 dark:border-red-900 text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20"
>
<TrashIcon className="w-4 h-4 mr-2" />
Remover
</Button>
</div>
</div>
) : (
<label className="block cursor-pointer">
<input
type="file"
accept="image/png,image/jpeg,image/jpg,image/svg+xml"
onChange={(e) => handleFileSelect(e, 'horizontal')}
className="hidden"
disabled={uploadingLogo}
/>
<div className="border-2 border-dashed border-gray-200 dark:border-gray-600 rounded-xl p-8 text-center hover:border-gray-400 dark:hover:border-gray-500 transition-colors bg-gray-50/50 dark:bg-gray-900/50">
<div className="flex flex-col items-center">
<div className="w-16 h-16 rounded-full bg-gray-100 dark:bg-gray-800 flex items-center justify-center mb-4">
<PhotoIcon className="w-8 h-8 text-gray-400 dark:text-gray-500" />
</div>
<p className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Clique para fazer upload
</p>
<p className="text-xs text-gray-500 dark:text-gray-400 mb-4">
ou arraste e solte aqui
</p>
<Button
variant="primary"
size="md"
disabled={uploadingLogo}
isLoading={uploadingLogo}
type="button"
className="pointer-events-none"
>
<ArrowUpTrayIcon className="w-4 h-4 mr-2" />
{uploadingLogo ? 'Enviando...' : 'Selecionar Arquivo'}
</Button>
<p className="text-xs text-gray-400 dark:text-gray-500 mt-3">
PNG, JPG ou SVG Máx. 2MB
</p>
</div>
</div>
</label>
)}
</div>
</div>
{/* Cores da Marca */}
<div className="pt-6 border-t border-gray-200 dark:border-gray-700">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Personalização de Cores
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<div className="flex items-end gap-3">
<div className="relative w-[50px] h-[42px] rounded-lg overflow-hidden border border-gray-200 dark:border-gray-700 shadow-sm shrink-0 mb-[2px]">
<input
type="color"
value={agencyData.primaryColor}
onChange={(e) => setAgencyData({ ...agencyData, primaryColor: e.target.value })}
className="absolute -top-2 -left-2 w-24 h-24 cursor-pointer p-0 border-0"
/>
</div>
<div className="flex-1">
<Input
label="Cor Primária"
type="text"
value={agencyData.primaryColor}
onChange={(e) => setAgencyData({ ...agencyData, primaryColor: e.target.value })}
className="uppercase"
maxLength={7}
/>
</div>
</div>
</div>
<div>
<div className="flex items-end gap-3">
<div className="relative w-[50px] h-[42px] rounded-lg overflow-hidden border border-gray-200 dark:border-gray-700 shadow-sm shrink-0 mb-[2px]">
<input
type="color"
value={agencyData.secondaryColor}
onChange={(e) => setAgencyData({ ...agencyData, secondaryColor: e.target.value })}
className="absolute -top-2 -left-2 w-24 h-24 cursor-pointer p-0 border-0"
/>
</div>
<div className="flex-1">
<Input
label="Cor Secundária"
type="text"
value={agencyData.secondaryColor}
onChange={(e) => setAgencyData({ ...agencyData, secondaryColor: e.target.value })}
className="uppercase"
maxLength={7}
/>
</div>
</div>
</div>
</div>
<div className="mt-6 flex justify-end">
<Button
onClick={handleSaveAgency}
variant="primary"
isLoading={saving}
style={{ backgroundColor: agencyData.primaryColor }}
>
Salvar Cores
</Button>
</div>
</div>
</div>
{/* Info adicional */}
<div className="mt-6 p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
<p className="text-sm text-blue-800 dark:text-blue-300">
<strong>Dica:</strong> Para melhores resultados, use imagens de alta qualidade em formato PNG com fundo transparente.
</p>
</div>
</div>
</Tab.Panel>
{/* Tab 3: Equipe */}
<Tab.Panel className="rounded-xl bg-white dark:bg-gray-800 p-6 border border-gray-200 dark:border-gray-700">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-6">
Gerenciamento de Equipe
</h2>
<div className="text-center py-12">
<UserGroupIcon className="w-16 h-16 mx-auto text-gray-300 dark:text-gray-600 mb-4" />
<p className="text-gray-600 dark:text-gray-400 mb-4">
Em breve: gerenciamento completo de usuários e permissões
</p>
<Button variant="primary">
Convidar Membro
</Button>
</div>
</Tab.Panel>
{/* Tab 3: Segurança */}
<Tab.Panel className="rounded-xl bg-white dark:bg-gray-800 p-6 border border-gray-200 dark:border-gray-700">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-6">
Segurança e Privacidade
</h2>
{/* Alteração de Senha */}
<div className="max-w-2xl">
<h3 className="text-md font-medium text-gray-900 dark:text-white mb-4">
Alterar Senha
</h3>
<div className="space-y-4">
<div>
<Input
label="Senha Atual"
type="password"
value={passwordData.currentPassword}
onChange={(e) => setPasswordData({ ...passwordData, currentPassword: e.target.value })}
placeholder="Digite sua senha atual"
/>
</div>
<div>
<Input
label="Nova Senha"
type="password"
value={passwordData.newPassword}
onChange={(e) => setPasswordData({ ...passwordData, newPassword: e.target.value })}
placeholder="Digite a nova senha (mínimo 8 caracteres)"
/>
</div>
<div>
<Input
label="Confirmar Nova Senha"
type="password"
value={passwordData.confirmPassword}
onChange={(e) => setPasswordData({ ...passwordData, confirmPassword: e.target.value })}
placeholder="Digite a nova senha novamente"
/>
</div>
<div className="pt-4">
<Button
onClick={handleChangePassword}
variant="primary"
>
Alterar Senha
</Button>
</div>
</div>
{/* Recursos Futuros */}
<div className="mt-8 pt-8 border-t border-gray-200 dark:border-gray-700">
<h3 className="text-md font-medium text-gray-900 dark:text-white mb-4">
Recursos em Desenvolvimento
</h3>
<div className="space-y-3 text-sm text-gray-600 dark:text-gray-400">
<div className="flex items-center space-x-2">
<ShieldCheckIcon className="w-5 h-5" />
<span>Autenticação em duas etapas (2FA)</span>
</div>
<div className="flex items-center space-x-2">
<ShieldCheckIcon className="w-5 h-5" />
<span>Histórico de acessos</span>
</div>
<div className="flex items-center space-x-2">
<ShieldCheckIcon className="w-5 h-5" />
<span>Dispositivos conectados</span>
</div>
</div>
</div>
</div>
</Tab.Panel>
{/* Tab 4: Notificações */}
<Tab.Panel className="rounded-xl bg-white dark:bg-gray-800 p-6 border border-gray-200 dark:border-gray-700">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-6">
Preferências de Notificações
</h2>
<div className="text-center py-12">
<BellIcon className="w-16 h-16 mx-auto text-gray-300 dark:text-gray-600 mb-4" />
<p className="text-gray-600 dark:text-gray-400">
Em breve: configuração de notificações por e-mail, push e mais
</p>
</div>
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
</>
)}
{/* Dialog de Sucesso */}
<Dialog
isOpen={showSuccessDialog}
onClose={() => setShowSuccessDialog(false)}
title="Sucesso"
size="sm"
>
<Dialog.Body>
<p className="text-center py-4">{successMessage}</p>
</Dialog.Body>
<Dialog.Footer>
<Button
onClick={() => setShowSuccessDialog(false)}
variant="primary"
>
OK
</Button>
</Dialog.Footer>
</Dialog>
{/* Dialog de Suporte */}
<Dialog
isOpen={showSupportDialog}
onClose={() => setShowSupportDialog(false)}
title="Contatar suporte"
>
<Dialog.Body>
<p className="text-sm text-gray-700 dark:text-gray-200">{supportMessage}</p>
<p className="mt-3 text-sm text-gray-500">Envie um e-mail para suporte@aggios.app ou abra um chamado para ajuste desses dados.</p>
</Dialog.Body>
<Dialog.Footer>
<Button
onClick={() => setShowSupportDialog(false)}
variant="primary"
>
Fechar
</Button>
</Dialog.Footer>
</Dialog>
</div>
);
}