617 lines
41 KiB
TypeScript
617 lines
41 KiB
TypeScript
'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>
|
||
);
|
||
}
|