feat: redesign superadmin agencies list, implement flat design, add date filters, and fix UI bugs
This commit is contained in:
@@ -0,0 +1,447 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { PlusIcon, LinkIcon, PencilSquareIcon, TrashIcon, ClipboardDocumentIcon } from '@heroicons/react/24/outline';
|
||||
import Button from '@/components/ui/Button';
|
||||
import Input from '@/components/ui/Input';
|
||||
import Dialog from '@/components/ui/Dialog';
|
||||
|
||||
interface FormField {
|
||||
name: string;
|
||||
label: string;
|
||||
type: string;
|
||||
required: boolean;
|
||||
order: number;
|
||||
}
|
||||
|
||||
interface SignupTemplate {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
slug: string;
|
||||
form_fields: FormField[];
|
||||
enabled_modules: string[];
|
||||
redirect_url?: string;
|
||||
success_message?: string;
|
||||
custom_logo_url?: string;
|
||||
custom_primary_color?: string;
|
||||
is_active: boolean;
|
||||
usage_count: number;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
const AVAILABLE_FIELDS = [
|
||||
{ name: 'email', label: 'E-mail', type: 'email', required: true },
|
||||
{ name: 'password', label: 'Senha', type: 'password', required: true },
|
||||
{ name: 'subdomain', label: 'Subdomínio', type: 'text', required: true },
|
||||
{ name: 'company_name', label: 'Nome da Empresa', type: 'text', required: false },
|
||||
{ name: 'cnpj', label: 'CNPJ', type: 'text', required: false },
|
||||
{ name: 'phone', label: 'Telefone', type: 'tel', required: false },
|
||||
{ name: 'address', label: 'Endereço', type: 'text', required: false },
|
||||
{ name: 'city', label: 'Cidade', type: 'text', required: false },
|
||||
{ name: 'state', label: 'Estado', type: 'text', required: false },
|
||||
{ name: 'zipcode', label: 'CEP', type: 'text', required: false },
|
||||
];
|
||||
|
||||
const AVAILABLE_MODULES = [
|
||||
'CRM',
|
||||
'ERP',
|
||||
'PROJECTS',
|
||||
'FINANCIAL',
|
||||
'INVENTORY',
|
||||
'HR',
|
||||
];
|
||||
|
||||
export default function SignupTemplatesPage() {
|
||||
const [templates, setTemplates] = useState<SignupTemplate[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [showDialog, setShowDialog] = useState(false);
|
||||
const [editingTemplate, setEditingTemplate] = useState<SignupTemplate | null>(null);
|
||||
|
||||
// Form state
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
description: '',
|
||||
slug: '',
|
||||
redirect_url: '',
|
||||
success_message: '',
|
||||
});
|
||||
const [selectedFields, setSelectedFields] = useState<FormField[]>([]);
|
||||
const [selectedModules, setSelectedModules] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
loadTemplates();
|
||||
}, []);
|
||||
|
||||
const loadTemplates = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
const response = await fetch('/api/admin/signup-templates', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setTemplates(data || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erro ao carregar templates:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFieldToggle = (field: typeof AVAILABLE_FIELDS[0]) => {
|
||||
// Campos obrigatórios não podem ser removidos
|
||||
if (field.required) return;
|
||||
|
||||
setSelectedFields(prev => {
|
||||
const exists = prev.find(f => f.name === field.name);
|
||||
if (exists) {
|
||||
return prev.filter(f => f.name !== field.name);
|
||||
} else {
|
||||
return [...prev, { ...field, order: prev.length + 1 }];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleModuleToggle = (module: string) => {
|
||||
setSelectedModules(prev => {
|
||||
if (prev.includes(module)) {
|
||||
return prev.filter(m => m !== module);
|
||||
} else {
|
||||
return [...prev, module];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
const template = {
|
||||
...formData,
|
||||
form_fields: selectedFields,
|
||||
enabled_modules: selectedModules,
|
||||
is_active: true,
|
||||
};
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
const url = editingTemplate
|
||||
? `/api/admin/signup-templates/${editingTemplate.id}`
|
||||
: '/api/admin/signup-templates';
|
||||
|
||||
const method = editingTemplate ? 'PUT' : 'POST';
|
||||
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(template),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
loadTemplates();
|
||||
handleCloseDialog();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erro ao salvar template:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
if (!confirm('Tem certeza que deseja deletar este template?')) return;
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
const response = await fetch(`/api/admin/signup-templates/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
loadTemplates();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erro ao deletar template:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = (template: SignupTemplate) => {
|
||||
setEditingTemplate(template);
|
||||
setFormData({
|
||||
name: template.name,
|
||||
description: template.description,
|
||||
slug: template.slug,
|
||||
redirect_url: template.redirect_url || '',
|
||||
success_message: template.success_message || '',
|
||||
});
|
||||
setSelectedFields(template.form_fields);
|
||||
setSelectedModules(template.enabled_modules);
|
||||
setShowDialog(true);
|
||||
};
|
||||
|
||||
const handleCloseDialog = () => {
|
||||
setShowDialog(false);
|
||||
setEditingTemplate(null);
|
||||
setFormData({
|
||||
name: '',
|
||||
description: '',
|
||||
slug: '',
|
||||
redirect_url: '',
|
||||
success_message: '',
|
||||
});
|
||||
// Sempre iniciar com os campos obrigatórios selecionados
|
||||
const requiredFields = AVAILABLE_FIELDS.filter(f => f.required).map((f, idx) => ({
|
||||
...f,
|
||||
order: idx + 1
|
||||
}));
|
||||
setSelectedFields(requiredFields);
|
||||
setSelectedModules([]);
|
||||
};
|
||||
|
||||
// Inicializar com campos obrigatórios na primeira renderização
|
||||
useEffect(() => {
|
||||
const requiredFields = AVAILABLE_FIELDS.filter(f => f.required).map((f, idx) => ({
|
||||
...f,
|
||||
order: idx + 1
|
||||
}));
|
||||
if (selectedFields.length === 0) {
|
||||
setSelectedFields(requiredFields);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const copyToClipboard = (slug: string) => {
|
||||
const url = `${window.location.origin}/cadastro/${slug}`;
|
||||
navigator.clipboard.writeText(url);
|
||||
alert('Link copiado para a área de transferência!');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Links de Cadastro</h1>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400 mt-1">
|
||||
Crie links personalizados de cadastro com campos e módulos específicos
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={() => setShowDialog(true)} size="sm">
|
||||
<PlusIcon className="w-4 h-4 mr-2" />
|
||||
Novo Link
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="text-center py-8">
|
||||
<div className="animate-spin rounded-full h-10 w-10 border-b-2 border-gray-900 dark:border-white mx-auto"></div>
|
||||
</div>
|
||||
) : templates.length === 0 ? (
|
||||
<div className="text-center py-8 bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-800">
|
||||
<LinkIcon className="w-10 h-10 text-gray-400 mx-auto mb-3" />
|
||||
<h3 className="text-base font-medium text-gray-900 dark:text-white mb-1">
|
||||
Nenhum link criado
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
|
||||
Crie seu primeiro link de cadastro personalizado
|
||||
</p>
|
||||
<Button onClick={() => setShowDialog(true)} size="sm">
|
||||
<PlusIcon className="w-4 h-4 mr-2" />
|
||||
Criar Primeiro Link
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-3">
|
||||
{templates.map((template) => (
|
||||
<div
|
||||
key={template.id}
|
||||
className="bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-800 p-4"
|
||||
>
|
||||
<div className="flex justify-between items-start mb-3">
|
||||
<div className="flex-1">
|
||||
<h3 className="text-base font-semibold text-gray-900 dark:text-white mb-1">
|
||||
{template.name}
|
||||
</h3>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400 mb-2">
|
||||
{template.description}
|
||||
</p>
|
||||
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<code className="px-2 py-0.5 bg-gray-100 dark:bg-gray-800 rounded text-xs font-mono text-gray-900 dark:text-white">
|
||||
/cadastro/{template.slug}
|
||||
</code>
|
||||
<button
|
||||
onClick={() => copyToClipboard(template.slug)}
|
||||
className="p-1 hover:bg-gray-100 dark:hover:bg-gray-800 rounded"
|
||||
title="Copiar link"
|
||||
>
|
||||
<ClipboardDocumentIcon className="w-4 h-4 text-gray-600 dark:text-gray-400" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2 mb-2">
|
||||
<span className="text-[10px] text-gray-600 dark:text-gray-400">Campos:</span>
|
||||
{template.form_fields.map((field) => (
|
||||
<span
|
||||
key={field.name}
|
||||
className="px-1.5 py-0.5 bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 rounded text-[10px]"
|
||||
>
|
||||
{field.label}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<span className="text-[10px] text-gray-600 dark:text-gray-400">Módulos:</span>
|
||||
{template.enabled_modules.map((module) => (
|
||||
<span
|
||||
key={module}
|
||||
className="px-1.5 py-0.5 bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 rounded text-[10px]"
|
||||
>
|
||||
{module}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 ml-4">
|
||||
<div className="text-right mr-3">
|
||||
<div className="text-xl font-bold text-gray-900 dark:text-white">
|
||||
{template.usage_count}
|
||||
</div>
|
||||
<div className="text-[10px] text-gray-600 dark:text-gray-400">
|
||||
cadastros
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleEdit(template)}
|
||||
className="p-1.5 hover:bg-gray-100 dark:hover:bg-gray-800 rounded"
|
||||
>
|
||||
<PencilSquareIcon className="w-4 h-4 text-gray-600 dark:text-gray-400" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(template.id)}
|
||||
className="p-1.5 hover:bg-red-100 dark:hover:bg-red-900 rounded"
|
||||
>
|
||||
<TrashIcon className="w-4 h-4 text-red-600 dark:text-red-400" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Dialog de Criação/Edição */}
|
||||
<Dialog
|
||||
isOpen={showDialog}
|
||||
onClose={handleCloseDialog}
|
||||
title={editingTemplate ? 'Editar Link de Cadastro' : 'Novo Link de Cadastro'}
|
||||
>
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Input
|
||||
label="Nome do Template"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
required
|
||||
/>
|
||||
<Input
|
||||
label="Slug (URL)"
|
||||
value={formData.slug}
|
||||
onChange={(e) => setFormData({ ...formData, slug: e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, '-') })}
|
||||
required
|
||||
placeholder="ex: crm-rapido"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
label="Descrição"
|
||||
value={formData.description}
|
||||
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
|
||||
Campos do Formulário
|
||||
</label>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{AVAILABLE_FIELDS.map((field) => {
|
||||
const isSelected = selectedFields.some(f => f.name === field.name);
|
||||
const isRequired = field.required;
|
||||
|
||||
return (
|
||||
<label
|
||||
key={field.name}
|
||||
className={`flex items-center gap-2 p-2 rounded border ${isRequired
|
||||
? 'border-purple-300 dark:border-purple-700 bg-purple-50 dark:bg-purple-900/20'
|
||||
: 'border-gray-200 dark:border-gray-700'
|
||||
} ${isRequired
|
||||
? 'cursor-not-allowed'
|
||||
: 'hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer'
|
||||
}`}
|
||||
title={isRequired ? 'Campo obrigatório - não pode ser removido' : ''}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isSelected}
|
||||
onChange={() => handleFieldToggle(field)}
|
||||
disabled={isRequired}
|
||||
className={`rounded ${isRequired ? 'cursor-not-allowed opacity-60' : ''}`}
|
||||
/>
|
||||
<span className="text-sm text-gray-900 dark:text-white">{field.label}</span>
|
||||
{isRequired && (
|
||||
<span className="ml-auto text-xs px-1.5 py-0.5 bg-purple-600 dark:bg-purple-500 text-white rounded font-medium">
|
||||
OBRIGATÓRIO
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<p className="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
Os campos Email, Senha e Subdomínio são obrigatórios e não podem ser removidos
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
|
||||
Módulos Habilitados
|
||||
</label>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{AVAILABLE_MODULES.map((module) => (
|
||||
<label
|
||||
key={module}
|
||||
className="flex items-center gap-2 p-2 rounded border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedModules.includes(module)}
|
||||
onChange={() => handleModuleToggle(module)}
|
||||
className="rounded"
|
||||
/>
|
||||
<span className="text-sm text-gray-900 dark:text-white">{module}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 justify-end pt-4 border-t border-gray-200 dark:border-gray-800">
|
||||
<Button type="button" variant="outline" onClick={handleCloseDialog}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="submit">
|
||||
{editingTemplate ? 'Salvar Alterações' : 'Criar Link'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user