448 lines
19 KiB
TypeScript
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>
|
|
);
|
|
}
|