571 lines
26 KiB
TypeScript
571 lines
26 KiB
TypeScript
"use client";
|
||
|
||
import { useEffect, useState } from 'react';
|
||
import { Button, Dialog, Input } from '@/components/ui';
|
||
import { Toaster, toast } from 'react-hot-toast';
|
||
import {
|
||
UserPlusIcon,
|
||
TrashIcon,
|
||
XMarkIcon,
|
||
CheckCircleIcon,
|
||
ExclamationTriangleIcon,
|
||
} from '@heroicons/react/24/outline';
|
||
|
||
interface Collaborator {
|
||
id: string;
|
||
email: string;
|
||
name: string;
|
||
agency_role: string;
|
||
created_at: string;
|
||
collaborator_created_at?: string;
|
||
}
|
||
|
||
interface InviteRequest {
|
||
email: string;
|
||
name: string;
|
||
}
|
||
|
||
export default function TeamManagement() {
|
||
const [collaborators, setCollaborators] = useState<Collaborator[]>([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [showInviteDialog, setShowInviteDialog] = useState(false);
|
||
const [showDirectCreateDialog, setShowDirectCreateDialog] = useState(false);
|
||
const [showActionMenu, setShowActionMenu] = useState(false);
|
||
const [inviteForm, setInviteForm] = useState<InviteRequest>({
|
||
email: '',
|
||
name: '',
|
||
});
|
||
const [inviting, setInviting] = useState(false);
|
||
const [tempPassword, setTempPassword] = useState('');
|
||
const [showPasswordDialog, setShowPasswordDialog] = useState(false);
|
||
const [passwordDialogMode, setPasswordDialogMode] = useState<'invite' | 'direct'>('invite');
|
||
const [removingId, setRemovingId] = useState<string | null>(null);
|
||
const [showRemoveDialog, setShowRemoveDialog] = useState(false);
|
||
const [isOwner, setIsOwner] = useState(true);
|
||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||
|
||
// Buscar colaboradores
|
||
const fetchCollaborators = async () => {
|
||
try {
|
||
setLoading(true);
|
||
const token = localStorage.getItem('token');
|
||
const response = await fetch('/api/agency/collaborators', {
|
||
headers: {
|
||
Authorization: `Bearer ${token}`,
|
||
},
|
||
});
|
||
|
||
if (!response.ok) {
|
||
if (response.status === 403) {
|
||
// Usuário não é owner
|
||
setIsOwner(false);
|
||
setErrorMessage('Apenas o dono da agência pode gerenciar colaboradores');
|
||
setCollaborators([]);
|
||
return;
|
||
}
|
||
throw new Error('Erro ao carregar colaboradores');
|
||
}
|
||
|
||
setIsOwner(true);
|
||
setErrorMessage(null);
|
||
const data = await response.json();
|
||
setCollaborators(data || []);
|
||
} catch (error) {
|
||
console.error('Erro ao carregar colaboradores:', error);
|
||
setErrorMessage('Erro ao carregar colaboradores');
|
||
toast.error('Erro ao carregar colaboradores');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
useEffect(() => {
|
||
fetchCollaborators();
|
||
}, []);
|
||
|
||
// Fechar menu de ações ao clicar fora
|
||
useEffect(() => {
|
||
const handleClickOutside = () => setShowActionMenu(false);
|
||
if (showActionMenu) {
|
||
document.addEventListener('click', handleClickOutside);
|
||
return () => document.removeEventListener('click', handleClickOutside);
|
||
}
|
||
}, [showActionMenu]);
|
||
|
||
// Convidar colaborador
|
||
const handleInvite = async () => {
|
||
if (!inviteForm.email || !inviteForm.name) {
|
||
toast.error('Preencha todos os campos');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
setInviting(true);
|
||
const token = localStorage.getItem('token');
|
||
const response = await fetch('/api/agency/collaborators/invite', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
Authorization: `Bearer ${token}`,
|
||
},
|
||
body: JSON.stringify(inviteForm),
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const error = await response.text();
|
||
throw new Error(error || 'Erro ao convidar colaborador');
|
||
}
|
||
|
||
const data = await response.json();
|
||
setTempPassword(data.temporary_password);
|
||
setPasswordDialogMode('invite');
|
||
setShowPasswordDialog(true);
|
||
setShowInviteDialog(false);
|
||
setInviteForm({ email: '', name: '' });
|
||
|
||
// Recarregar colaboradores
|
||
await fetchCollaborators();
|
||
} catch (error) {
|
||
console.error('Erro ao convidar:', error);
|
||
toast.error(error instanceof Error ? error.message : 'Erro ao convidar colaborador');
|
||
} finally {
|
||
setInviting(false);
|
||
}
|
||
};
|
||
|
||
// Criar colaborador diretamente
|
||
const handleDirectCreate = async () => {
|
||
if (!inviteForm.email || !inviteForm.name) {
|
||
toast.error('Preencha todos os campos');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
setInviting(true);
|
||
const token = localStorage.getItem('token');
|
||
const response = await fetch('/api/agency/collaborators/invite', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
Authorization: `Bearer ${token}`,
|
||
},
|
||
body: JSON.stringify(inviteForm),
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const error = await response.text();
|
||
throw new Error(error || 'Erro ao criar colaborador');
|
||
}
|
||
|
||
const data = await response.json();
|
||
setTempPassword(data.temporary_password);
|
||
setPasswordDialogMode('direct');
|
||
setShowPasswordDialog(true);
|
||
setShowDirectCreateDialog(false);
|
||
setInviteForm({ email: '', name: '' });
|
||
|
||
// Recarregar colaboradores
|
||
await fetchCollaborators();
|
||
} catch (error) {
|
||
console.error('Erro ao criar:', error);
|
||
toast.error(error instanceof Error ? error.message : 'Erro ao criar colaborador');
|
||
} finally {
|
||
setInviting(false);
|
||
}
|
||
};
|
||
|
||
// Remover colaborador
|
||
const handleRemove = async () => {
|
||
if (!removingId) return;
|
||
|
||
try {
|
||
const token = localStorage.getItem('token');
|
||
const response = await fetch(`/api/agency/collaborators/remove?id=${removingId}`, {
|
||
method: 'DELETE',
|
||
headers: {
|
||
Authorization: `Bearer ${token}`,
|
||
},
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error('Erro ao remover colaborador');
|
||
}
|
||
|
||
toast.success('Colaborador removido com sucesso');
|
||
setShowRemoveDialog(false);
|
||
setRemovingId(null);
|
||
await fetchCollaborators();
|
||
} catch (error) {
|
||
console.error('Erro ao remover:', error);
|
||
toast.error('Erro ao remover colaborador');
|
||
}
|
||
};
|
||
|
||
const copyPassword = () => {
|
||
navigator.clipboard.writeText(tempPassword);
|
||
toast.success('Senha copiada para a área de transferência');
|
||
};
|
||
|
||
const formatDate = (dateString: string) => {
|
||
try {
|
||
return new Date(dateString).toLocaleDateString('pt-BR');
|
||
} catch {
|
||
return '-';
|
||
}
|
||
};
|
||
|
||
if (loading) {
|
||
return (
|
||
<div className="space-y-4">
|
||
<div className="h-20 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
|
||
<div className="h-20 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<>
|
||
<Toaster position="top-right" />
|
||
|
||
<div className="space-y-6">
|
||
{/* Mensagem de Erro se não for owner */}
|
||
{!isOwner && errorMessage && (
|
||
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 flex gap-3">
|
||
<ExclamationTriangleIcon className="w-5 h-5 text-red-600 dark:text-red-400 flex-shrink-0 mt-0.5" />
|
||
<p className="text-sm text-red-800 dark:text-red-300">
|
||
{errorMessage}
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* Cabeçalho */}
|
||
<div className="flex justify-between items-center">
|
||
<div>
|
||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||
Gerenciamento de Equipe
|
||
</h2>
|
||
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||
Adicione e gerencie colaboradores com acesso ao sistema
|
||
</p>
|
||
</div>
|
||
<div className="relative">
|
||
<Button
|
||
variant="primary"
|
||
onClick={() => setShowActionMenu(!showActionMenu)}
|
||
className="flex items-center gap-2"
|
||
disabled={!isOwner}
|
||
>
|
||
<UserPlusIcon className="w-4 h-4" />
|
||
Adicionar Colaborador
|
||
</Button>
|
||
|
||
{showActionMenu && (
|
||
<div className="absolute right-0 mt-2 w-56 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg z-50">
|
||
<button
|
||
onClick={() => {
|
||
setShowInviteDialog(true);
|
||
setShowActionMenu(false);
|
||
}}
|
||
className="w-full text-left px-4 py-3 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors border-b border-gray-200 dark:border-gray-700"
|
||
>
|
||
<p className="font-medium text-gray-900 dark:text-white text-sm">
|
||
Convidar por Email
|
||
</p>
|
||
<p className="text-xs text-gray-600 dark:text-gray-400 mt-0.5">
|
||
Enviar convite por email com senha temporária
|
||
</p>
|
||
</button>
|
||
<button
|
||
onClick={() => {
|
||
setShowDirectCreateDialog(true);
|
||
setShowActionMenu(false);
|
||
}}
|
||
className="w-full text-left px-4 py-3 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors"
|
||
>
|
||
<p className="font-medium text-gray-900 dark:text-white text-sm">
|
||
Criar sem Convite
|
||
</p>
|
||
<p className="text-xs text-gray-600 dark:text-gray-400 mt-0.5">
|
||
Criar colaborador e copiar senha manualmente
|
||
</p>
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Lista de Colaboradores */}
|
||
{collaborators.length === 0 ? (
|
||
<div className="text-center py-12 bg-gray-50 dark:bg-gray-800/50 rounded-lg border border-dashed border-gray-300 dark:border-gray-600">
|
||
<UserPlusIcon className="w-12 h-12 mx-auto text-gray-400 mb-4" />
|
||
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
||
Nenhum colaborador adicionado ainda
|
||
</p>
|
||
<Button
|
||
variant="secondary"
|
||
onClick={() => setShowInviteDialog(true)}
|
||
>
|
||
Convidar o Primeiro Colaborador
|
||
</Button>
|
||
</div>
|
||
) : (
|
||
<div className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
||
<table className="w-full">
|
||
<thead className="bg-gray-50 dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700">
|
||
<tr>
|
||
<th className="px-6 py-3 text-left text-sm font-semibold text-gray-900 dark:text-white">
|
||
Nome
|
||
</th>
|
||
<th className="px-6 py-3 text-left text-sm font-semibold text-gray-900 dark:text-white">
|
||
Email
|
||
</th>
|
||
<th className="px-6 py-3 text-left text-sm font-semibold text-gray-900 dark:text-white">
|
||
Função
|
||
</th>
|
||
<th className="px-6 py-3 text-left text-sm font-semibold text-gray-900 dark:text-white">
|
||
Data de Adição
|
||
</th>
|
||
<th className="px-6 py-3 text-right text-sm font-semibold text-gray-900 dark:text-white">
|
||
Ações
|
||
</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
|
||
{collaborators.map((collaborator) => (
|
||
<tr
|
||
key={collaborator.id}
|
||
className="hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors"
|
||
>
|
||
<td className="px-6 py-4 text-sm text-gray-900 dark:text-white font-medium">
|
||
{collaborator.name}
|
||
</td>
|
||
<td className="px-6 py-4 text-sm text-gray-600 dark:text-gray-400">
|
||
{collaborator.email}
|
||
</td>
|
||
<td className="px-6 py-4 text-sm">
|
||
<span className={`inline-block px-3 py-1 rounded-full text-xs font-semibold ${collaborator.agency_role === 'owner'
|
||
? 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300'
|
||
: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300'
|
||
}`}>
|
||
{collaborator.agency_role === 'owner' ? 'Dono' : 'Colaborador'}
|
||
</span>
|
||
</td>
|
||
<td className="px-6 py-4 text-sm text-gray-600 dark:text-gray-400">
|
||
{formatDate(collaborator.collaborator_created_at || collaborator.created_at)}
|
||
</td>
|
||
<td className="px-6 py-4 text-right">
|
||
{collaborator.agency_role !== 'owner' && (
|
||
<button
|
||
onClick={() => {
|
||
setRemovingId(collaborator.id);
|
||
setShowRemoveDialog(true);
|
||
}}
|
||
className="text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300 transition-colors"
|
||
>
|
||
<TrashIcon className="w-4 h-4" />
|
||
</button>
|
||
)}
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
)}
|
||
|
||
{/* Informação sobre Permissões */}
|
||
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
|
||
<div className="flex gap-3">
|
||
<ExclamationTriangleIcon className="w-5 h-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
|
||
<div className="text-sm text-blue-800 dark:text-blue-300">
|
||
<p className="font-medium mb-1">Permissões dos Colaboradores:</p>
|
||
<ul className="list-disc list-inside space-y-0.5">
|
||
<li>Podem visualizar leads e clientes</li>
|
||
<li>Não podem editar ou remover dados</li>
|
||
<li>Permissões gerenciadas exclusivamente pelo dono</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Dialog: Convidar Colaborador */}
|
||
<Dialog
|
||
isOpen={showInviteDialog}
|
||
onClose={() => setShowInviteDialog(false)}
|
||
title="Convidar Colaborador"
|
||
>
|
||
<div className="space-y-4">
|
||
<Input
|
||
label="Nome"
|
||
placeholder="Nome completo do colaborador"
|
||
value={inviteForm.name}
|
||
onChange={(e) => setInviteForm({ ...inviteForm, name: e.target.value })}
|
||
disabled={inviting}
|
||
/>
|
||
<Input
|
||
label="Email"
|
||
type="email"
|
||
placeholder="email@exemplo.com"
|
||
value={inviteForm.email}
|
||
onChange={(e) => setInviteForm({ ...inviteForm, email: e.target.value })}
|
||
disabled={inviting}
|
||
/>
|
||
|
||
<div className="flex gap-3 justify-end pt-4">
|
||
<Button
|
||
variant="secondary"
|
||
onClick={() => setShowInviteDialog(false)}
|
||
disabled={inviting}
|
||
>
|
||
Cancelar
|
||
</Button>
|
||
<Button
|
||
variant="primary"
|
||
onClick={handleInvite}
|
||
disabled={inviting || !inviteForm.email || !inviteForm.name}
|
||
>
|
||
{inviting ? 'Convidando...' : 'Convidar'}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</Dialog>
|
||
|
||
{/* Dialog: Senha Temporária */}
|
||
<Dialog
|
||
isOpen={showPasswordDialog}
|
||
onClose={() => setShowPasswordDialog(false)}
|
||
title={passwordDialogMode === 'invite' ? 'Colaborador Convidado com Sucesso' : 'Colaborador Criado com Sucesso'}
|
||
>
|
||
<div className="space-y-4">
|
||
<div className="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg p-4 flex gap-3">
|
||
<CheckCircleIcon className="w-5 h-5 text-green-600 dark:text-green-400 flex-shrink-0" />
|
||
<p className="text-sm text-green-800 dark:text-green-300">
|
||
{passwordDialogMode === 'invite'
|
||
? 'Colaborador criado com sucesso! Um email com a senha temporária foi enviado.'
|
||
: 'Colaborador criado com sucesso! Copie a senha abaixo e compartilhe com segurança.'}
|
||
</p>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<label className="block text-sm font-medium text-gray-900 dark:text-white">
|
||
Senha Temporária
|
||
</label>
|
||
<div className="flex gap-2">
|
||
<input
|
||
type="text"
|
||
value={tempPassword}
|
||
readOnly
|
||
className="flex-1 px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-white font-mono text-sm"
|
||
/>
|
||
<Button
|
||
variant="secondary"
|
||
onClick={copyPassword}
|
||
className="px-4"
|
||
>
|
||
Copiar
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4 text-sm text-yellow-800 dark:text-yellow-300">
|
||
⚠️ {passwordDialogMode === 'invite'
|
||
? 'O colaborador deverá alterar a senha no primeiro acesso.'
|
||
: 'Compartilhe esta senha com segurança. O colaborador deverá alterá-la no primeiro acesso.'}
|
||
</div>
|
||
|
||
<div className="flex justify-end pt-4">
|
||
<Button
|
||
variant="primary"
|
||
onClick={() => setShowPasswordDialog(false)}
|
||
>
|
||
OK
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</Dialog>
|
||
|
||
{/* Dialog: Criar Colaborador Direto */}
|
||
<Dialog
|
||
isOpen={showDirectCreateDialog}
|
||
onClose={() => setShowDirectCreateDialog(false)}
|
||
title="Criar Colaborador (Sem Email)"
|
||
>
|
||
<div className="space-y-4">
|
||
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4 flex gap-3">
|
||
<ExclamationTriangleIcon className="w-5 h-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
|
||
<p className="text-sm text-blue-800 dark:text-blue-300">
|
||
O colaborador será criado imediatamente. Você receberá a senha para compartilhar manualmente.
|
||
</p>
|
||
</div>
|
||
|
||
<Input
|
||
label="Nome"
|
||
placeholder="Nome completo do colaborador"
|
||
value={inviteForm.name}
|
||
onChange={(e) => setInviteForm({ ...inviteForm, name: e.target.value })}
|
||
disabled={inviting}
|
||
/>
|
||
<Input
|
||
label="Email"
|
||
type="email"
|
||
placeholder="email@exemplo.com"
|
||
value={inviteForm.email}
|
||
onChange={(e) => setInviteForm({ ...inviteForm, email: e.target.value })}
|
||
disabled={inviting}
|
||
/>
|
||
|
||
<div className="flex gap-3 justify-end pt-4">
|
||
<Button
|
||
variant="secondary"
|
||
onClick={() => setShowDirectCreateDialog(false)}
|
||
disabled={inviting}
|
||
>
|
||
Cancelar
|
||
</Button>
|
||
<Button
|
||
variant="primary"
|
||
onClick={handleDirectCreate}
|
||
disabled={inviting || !inviteForm.email || !inviteForm.name}
|
||
>
|
||
{inviting ? 'Criando...' : 'Criar Colaborador'}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</Dialog>
|
||
|
||
{/* Dialog: Remover Colaborador */}
|
||
<Dialog
|
||
isOpen={showRemoveDialog}
|
||
onClose={() => setShowRemoveDialog(false)}
|
||
title="Remover Colaborador"
|
||
>
|
||
<div className="space-y-4">
|
||
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 flex gap-3">
|
||
<ExclamationTriangleIcon className="w-5 h-5 text-red-600 dark:text-red-400 flex-shrink-0" />
|
||
<p className="text-sm text-red-800 dark:text-red-300">
|
||
Tem certeza que deseja remover este colaborador? Ele perderá o acesso ao sistema imediatamente.
|
||
</p>
|
||
</div>
|
||
|
||
<div className="flex gap-3 justify-end pt-4">
|
||
<Button
|
||
variant="secondary"
|
||
onClick={() => setShowRemoveDialog(false)}
|
||
>
|
||
Cancelar
|
||
</Button>
|
||
<Button
|
||
variant="primary"
|
||
onClick={handleRemove}
|
||
className="bg-red-600 hover:bg-red-700"
|
||
>
|
||
Remover
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</Dialog>
|
||
</>
|
||
);
|
||
}
|