Files

448 lines
19 KiB
TypeScript

"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>
);
}