Files
aggios.app/front-end-dash.aggios.app/components/plans/CreatePlanModal.tsx
2025-12-17 13:36:23 -03:00

617 lines
41 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import { Fragment, useState } from 'react';
import { Dialog, Transition, Tab } from '@headlessui/react';
import {
XMarkIcon,
SparklesIcon,
PlusIcon,
MinusIcon,
} from '@heroicons/react/24/outline';
interface CreatePlanModalProps {
isOpen: boolean;
onClose: () => void;
onSuccess: (plan: any) => void;
}
interface CreatePlanForm {
name: string;
slug: string;
description: string;
min_users: number;
max_users: number;
monthly_price: string;
annual_price: string;
discount_months: number;
features: string;
differentiators: string;
storage_gb: number;
is_active: boolean;
}
export default function CreatePlanModal({ isOpen, onClose, onSuccess }: CreatePlanModalProps) {
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [formData, setFormData] = useState<CreatePlanForm>({
name: '',
slug: '',
description: '',
min_users: 1,
max_users: 30,
monthly_price: '',
annual_price: '',
discount_months: 2,
features: '',
differentiators: '',
storage_gb: 10,
is_active: true,
});
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value, type } = e.target;
if (type === 'checkbox') {
setFormData(prev => ({
...prev,
[name]: (e.target as HTMLInputElement).checked,
}));
} else if (type === 'number') {
const numValue = parseFloat(value) || 0;
setFormData(prev => {
const newData = {
...prev,
[name]: numValue,
};
// Calcular preço anual automaticamente quando mensal ou discount_months muda
if ((name === 'monthly_price' || name === 'discount_months')) {
const monthlyPrice = name === 'monthly_price' ? numValue : parseFloat(prev.monthly_price) || 0;
const discountMonths = name === 'discount_months' ? numValue : prev.discount_months;
if (monthlyPrice > 0 && discountMonths >= 0) {
// Calcula: (12 meses - meses de desconto) * preço mensal
const monthsToPay = Math.max(0, 12 - discountMonths);
const annualWithDiscount = (monthlyPrice * monthsToPay).toFixed(2);
newData.annual_price = annualWithDiscount;
}
}
return newData;
});
} else {
setFormData(prev => ({
...prev,
[name]: value,
}));
}
};
const incrementValue = (field: 'min_users' | 'max_users' | 'storage_gb' | 'discount_months', step: number = 1) => {
setFormData(prev => {
const newValue = prev[field] + step;
const newData = {
...prev,
[field]: newValue,
};
// Recalcular preço anual se mudou discount_months
if (field === 'discount_months') {
const monthlyPrice = parseFloat(prev.monthly_price) || 0;
if (monthlyPrice > 0 && newValue >= 0) {
const monthsToPay = Math.max(0, 12 - newValue);
newData.annual_price = (monthlyPrice * monthsToPay).toFixed(2);
}
}
return newData;
});
};
const decrementValue = (field: 'min_users' | 'max_users' | 'storage_gb' | 'discount_months', step: number = 1, min: number = 0) => {
setFormData(prev => {
const newValue = Math.max(min, prev[field] - step);
const newData = {
...prev,
[field]: newValue,
};
// Recalcular preço anual se mudou discount_months
if (field === 'discount_months') {
const monthlyPrice = parseFloat(prev.monthly_price) || 0;
if (monthlyPrice > 0 && newValue >= 0) {
const monthsToPay = Math.max(0, 12 - newValue);
newData.annual_price = (monthlyPrice * monthsToPay).toFixed(2);
}
}
return newData;
});
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError('');
try {
if (!formData.name || !formData.slug) {
setError('Nome e Slug são obrigatórios');
setLoading(false);
return;
}
const token = localStorage.getItem('token');
const features = formData.features
.split(',')
.map(f => f.trim())
.filter(f => f.length > 0);
const differentiators = formData.differentiators
.split(',')
.map(d => d.trim())
.filter(d => d.length > 0);
const payload = {
name: formData.name,
slug: formData.slug,
description: formData.description,
min_users: formData.min_users,
max_users: formData.max_users,
monthly_price: formData.monthly_price ? parseFloat(formData.monthly_price) : null,
annual_price: formData.annual_price ? parseFloat(formData.annual_price) : null,
features,
differentiators,
storage_gb: formData.storage_gb,
is_active: formData.is_active,
};
console.log('Enviando payload:', payload);
const response = await fetch('/api/admin/plans', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
});
if (!response.ok) {
const errorData = await response.json();
console.error('Erro da API:', errorData);
throw new Error(errorData.message || 'Erro ao criar plano');
}
const data = await response.json();
console.log('Plano criado:', data);
onSuccess(data.plan);
handleClose();
} catch (err: any) {
console.error('Erro ao criar plano:', err);
setError(err.message || 'Erro desconhecido ao criar plano');
} finally {
setLoading(false);
}
};
const handleClose = () => {
if (!loading) {
setError('');
setFormData({
name: '',
slug: '',
description: '',
min_users: 1,
max_users: 30,
monthly_price: '',
annual_price: '',
discount_months: 2,
features: '',
differentiators: '',
storage_gb: 10,
is_active: true,
});
onClose();
}
};
return (
<Transition.Root show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-50" onClose={handleClose}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-zinc-900/40 backdrop-blur-sm transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform overflow-hidden rounded-2xl bg-white dark:bg-zinc-900 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-3xl border border-zinc-200 dark:border-zinc-800">
<div className="absolute right-0 top-0 pr-6 pt-6">
<button
type="button"
className="rounded-lg p-1.5 text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors"
onClick={handleClose}
disabled={loading}
>
<XMarkIcon className="h-5 w-5" />
</button>
</div>
<div className="p-6 sm:p-8">
{/* Header */}
<div className="flex items-start gap-4 mb-6">
<div
className="flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-xl shadow-lg"
style={{ background: 'var(--gradient)' }}
>
<SparklesIcon className="h-6 w-6 text-white" />
</div>
<div>
<Dialog.Title className="text-xl font-bold text-zinc-900 dark:text-white">
Criar Novo Plano
</Dialog.Title>
<p className="mt-1 text-sm text-zinc-500 dark:text-zinc-400">
Configure um novo plano de assinatura para as agências.
</p>
</div>
</div>
{/* Error Message */}
{error && (
<div className="mb-6 rounded-lg bg-red-50 dark:bg-red-900/20 p-4 border border-red-200 dark:border-red-800">
<p className="text-sm font-medium text-red-800 dark:text-red-400">{error}</p>
</div>
)}
<form onSubmit={handleSubmit}>
<Tab.Group>
<Tab.List className="flex gap-2 p-1 bg-zinc-100 dark:bg-zinc-800 rounded-lg mb-6">
{['Dados Básicos', 'Usuários', 'Preços', 'Avançado'].map((tab) => (
<Tab
key={tab}
className={({ selected }) =>
`flex-1 px-4 py-2.5 text-sm font-medium rounded-lg transition-all focus:outline-none ${selected
? 'bg-white dark:bg-zinc-900 text-zinc-900 dark:text-white shadow-sm'
: 'text-zinc-600 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-white'
}`
}
>
{tab}
</Tab>
))}
</Tab.List>
<Tab.Panels className="space-y-4">
{/* Tab 1: Dados Básicos */}
<Tab.Panel className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-2">
Nome do Plano *
</label>
<input
type="text"
name="name"
value={formData.name}
onChange={handleChange}
placeholder="Ex: Ignição"
className="w-full px-3 py-2.5 border border-zinc-200 dark:border-zinc-700 rounded-lg bg-white dark:bg-zinc-900 text-zinc-900 dark:text-white placeholder-zinc-400 focus:outline-none focus:ring-2 focus:ring-[var(--brand-color)] focus:border-transparent transition-all"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-2">
Slug *
</label>
<input
type="text"
name="slug"
value={formData.slug}
onChange={handleChange}
placeholder="Ex: ignition"
className="w-full px-3 py-2.5 border border-zinc-200 dark:border-zinc-700 rounded-lg bg-white dark:bg-zinc-900 text-zinc-900 dark:text-white placeholder-zinc-400 focus:outline-none focus:ring-2 focus:ring-[var(--brand-color)] focus:border-transparent transition-all"
required
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-2">
Descrição
</label>
<textarea
name="description"
value={formData.description}
onChange={handleChange}
placeholder="Descrição breve do plano"
rows={3}
className="w-full px-3 py-2.5 border border-zinc-200 dark:border-zinc-700 rounded-lg bg-white dark:bg-zinc-900 text-zinc-900 dark:text-white placeholder-zinc-400 focus:outline-none focus:ring-2 focus:ring-[var(--brand-color)] focus:border-transparent resize-none transition-all"
/>
</div>
<div className="flex items-center pt-2">
<input
type="checkbox"
name="is_active"
checked={formData.is_active}
onChange={handleChange}
className="h-4 w-4 rounded border-zinc-300 dark:border-zinc-600 focus:ring-2 focus:ring-[var(--brand-color)]"
style={{ accentColor: 'var(--brand-color)' }}
/>
<label className="ml-3 text-sm font-medium text-zinc-700 dark:text-zinc-300">
Plano Ativo
</label>
</div>
</Tab.Panel>
{/* Tab 2: Usuários */}
<Tab.Panel className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-2">
Mínimo de Usuários
</label>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => decrementValue('min_users', 1, 1)}
className="p-2 rounded-lg border border-zinc-200 dark:border-zinc-700 hover:bg-zinc-50 dark:hover:bg-zinc-800 transition-colors"
>
<MinusIcon className="w-4 h-4 text-zinc-600 dark:text-zinc-400" />
</button>
<input
type="number"
name="min_users"
value={formData.min_users}
onChange={handleChange}
min="1"
className="flex-1 px-3 py-2.5 border border-zinc-200 dark:border-zinc-700 rounded-lg bg-white dark:bg-zinc-900 text-zinc-900 dark:text-white text-center font-medium focus:outline-none focus:ring-2 focus:ring-[var(--brand-color)] focus:border-transparent transition-all"
/>
<button
type="button"
onClick={() => incrementValue('min_users', 1)}
className="p-2 rounded-lg border border-zinc-200 dark:border-zinc-700 hover:bg-zinc-50 dark:hover:bg-zinc-800 transition-colors"
>
<PlusIcon className="w-4 h-4 text-zinc-600 dark:text-zinc-400" />
</button>
</div>
</div>
<div>
<label className="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-2">
Máximo de Usuários
</label>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => decrementValue('max_users', 5, -1)}
className="p-2 rounded-lg border border-zinc-200 dark:border-zinc-700 hover:bg-zinc-50 dark:hover:bg-zinc-800 transition-colors"
>
<MinusIcon className="w-4 h-4 text-zinc-600 dark:text-zinc-400" />
</button>
<input
type="number"
name="max_users"
value={formData.max_users}
onChange={handleChange}
className="flex-1 px-3 py-2.5 border border-zinc-200 dark:border-zinc-700 rounded-lg bg-white dark:bg-zinc-900 text-zinc-900 dark:text-white text-center font-medium focus:outline-none focus:ring-2 focus:ring-[var(--brand-color)] focus:border-transparent transition-all"
/>
<button
type="button"
onClick={() => incrementValue('max_users', 5)}
className="p-2 rounded-lg border border-zinc-200 dark:border-zinc-700 hover:bg-zinc-50 dark:hover:bg-zinc-800 transition-colors"
>
<PlusIcon className="w-4 h-4 text-zinc-600 dark:text-zinc-400" />
</button>
</div>
<p className="mt-2 text-xs text-zinc-500 dark:text-zinc-400">
Use -1 para ilimitado
</p>
</div>
</div>
</Tab.Panel>
{/* Tab 3: Preços */}
<Tab.Panel className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-2">
Preço Mensal (R$) *
</label>
<div className="relative">
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-zinc-500">R$</span>
<input
type="number"
name="monthly_price"
value={formData.monthly_price}
onChange={handleChange}
placeholder="199.99"
step="0.01"
className="w-full pl-10 pr-3 py-2.5 border border-zinc-200 dark:border-zinc-700 rounded-lg bg-white dark:bg-zinc-900 text-zinc-900 dark:text-white placeholder-zinc-400 focus:outline-none focus:ring-2 focus:ring-[var(--brand-color)] focus:border-transparent transition-all"
/>
</div>
<p className="mt-2 text-xs text-zinc-500 dark:text-zinc-400">
Digite o preço mensal
</p>
</div>
<div>
<label className="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-2">
Meses Grátis (Desconto)
</label>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => decrementValue('discount_months', 1, 0)}
className="p-2 rounded-lg border border-zinc-200 dark:border-zinc-700 hover:bg-zinc-50 dark:hover:bg-zinc-800 transition-colors"
>
<MinusIcon className="w-4 h-4 text-zinc-600 dark:text-zinc-400" />
</button>
<input
type="number"
name="discount_months"
value={formData.discount_months}
onChange={handleChange}
min="0"
max="11"
className="flex-1 px-3 py-2.5 border border-zinc-200 dark:border-zinc-700 rounded-lg bg-white dark:bg-zinc-900 text-zinc-900 dark:text-white text-center font-medium focus:outline-none focus:ring-2 focus:ring-[var(--brand-color)] focus:border-transparent transition-all"
/>
<button
type="button"
onClick={() => incrementValue('discount_months', 1)}
className="p-2 rounded-lg border border-zinc-200 dark:border-zinc-700 hover:bg-zinc-50 dark:hover:bg-zinc-800 transition-colors"
>
<PlusIcon className="w-4 h-4 text-zinc-600 dark:text-zinc-400" />
</button>
</div>
<p className="mt-2 text-xs text-zinc-500 dark:text-zinc-400">
Ex: 2 = cliente paga 10 meses
</p>
</div>
</div>
<div>
<label className="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-2">
Preço Anual (R$) <span className="text-emerald-600 dark:text-emerald-400"> Auto</span>
</label>
<div className="relative">
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-zinc-500">R$</span>
<input
type="number"
name="annual_price"
value={formData.annual_price}
onChange={handleChange}
placeholder="Calculado automaticamente"
step="0.01"
className="w-full pl-10 pr-3 py-2.5 border border-zinc-200 dark:border-zinc-700 rounded-lg bg-white dark:bg-zinc-900 text-zinc-900 dark:text-white placeholder-zinc-400 focus:outline-none focus:ring-2 focus:ring-[var(--brand-color)] focus:border-transparent transition-all"
/>
</div>
{formData.monthly_price && formData.annual_price && formData.discount_months > 0 && (
<p className="mt-2 text-xs font-medium text-emerald-600 dark:text-emerald-400">
💰 Cliente paga {12 - formData.discount_months} meses e ganha {formData.discount_months} mês(es) grátis!
</p>
)}
</div>
<div className="p-4 bg-gradient-to-r from-emerald-50 to-blue-50 dark:from-emerald-900/20 dark:to-blue-900/20 rounded-lg border border-emerald-200 dark:border-emerald-800">
<p className="text-sm font-medium text-emerald-900 dark:text-emerald-100 mb-1">
🎯 Cálculo Automático de Desconto
</p>
<p className="text-xs text-emerald-700 dark:text-emerald-300">
Configure quantos meses de desconto deseja oferecer. O preço anual será calculado automaticamente: <strong>Preço Mensal × (12 - Meses Grátis)</strong>. Ideal para promoções sazonais!
</p>
</div>
</Tab.Panel>
{/* Tab 4: Avançado */}
<Tab.Panel className="space-y-4">
<div>
<label className="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-2">
Armazenamento (GB)
</label>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => decrementValue('storage_gb', 1, 1)}
className="p-2 rounded-lg border border-zinc-200 dark:border-zinc-700 hover:bg-zinc-50 dark:hover:bg-zinc-800 transition-colors"
>
<MinusIcon className="w-4 h-4 text-zinc-600 dark:text-zinc-400" />
</button>
<input
type="number"
name="storage_gb"
value={formData.storage_gb}
onChange={handleChange}
min="1"
className="flex-1 px-3 py-2.5 border border-zinc-200 dark:border-zinc-700 rounded-lg bg-white dark:bg-zinc-900 text-zinc-900 dark:text-white text-center font-medium focus:outline-none focus:ring-2 focus:ring-[var(--brand-color)] focus:border-transparent transition-all"
/>
<button
type="button"
onClick={() => incrementValue('storage_gb', 1)}
className="p-2 rounded-lg border border-zinc-200 dark:border-zinc-700 hover:bg-zinc-50 dark:hover:bg-zinc-800 transition-colors"
>
<PlusIcon className="w-4 h-4 text-zinc-600 dark:text-zinc-400" />
</button>
</div>
<p className="mt-2 text-xs text-zinc-500 dark:text-zinc-400">
Incrementos de 1 GB
</p>
</div>
<div>
<label className="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-2">
Recursos <span className="text-xs text-zinc-500">(separados por vírgula)</span>
</label>
<textarea
name="features"
value={formData.features}
onChange={handleChange}
placeholder="CRM, ERP, Projetos, Helpdesk, Pagamentos"
rows={3}
className="w-full px-3 py-2.5 border border-zinc-200 dark:border-zinc-700 rounded-lg bg-white dark:bg-zinc-900 text-zinc-900 dark:text-white placeholder-zinc-400 focus:outline-none focus:ring-2 focus:ring-[var(--brand-color)] focus:border-transparent resize-none transition-all"
/>
</div>
<div>
<label className="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-2">
Diferenciais <span className="text-xs text-zinc-500">(separados por vírgula)</span>
</label>
<textarea
name="differentiators"
value={formData.differentiators}
onChange={handleChange}
placeholder="Suporte prioritário, Gerente dedicado, API avançada"
rows={3}
className="w-full px-3 py-2.5 border border-zinc-200 dark:border-zinc-700 rounded-lg bg-white dark:bg-zinc-900 text-zinc-900 dark:text-white placeholder-zinc-400 focus:outline-none focus:ring-2 focus:ring-[var(--brand-color)] focus:border-transparent resize-none transition-all"
/>
</div>
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
{/* Buttons */}
<div className="mt-8 pt-6 border-t border-zinc-200 dark:border-zinc-700 flex gap-3">
<button
type="button"
onClick={handleClose}
disabled={loading}
className="flex-1 px-4 py-2.5 border border-zinc-200 dark:border-zinc-700 text-zinc-700 dark:text-zinc-300 font-medium rounded-lg hover:bg-zinc-50 dark:hover:bg-zinc-800 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
Cancelar
</button>
<button
type="submit"
disabled={loading}
className="flex-1 px-4 py-2.5 text-white font-medium rounded-lg disabled:opacity-50 disabled:cursor-not-allowed transition-all shadow-lg hover:shadow-xl"
style={{ background: loading ? '#999' : 'var(--gradient)' }}
>
{loading ? 'Criando...' : 'Criar Plano'}
</button>
</div>
</form>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
}