feat: redesign superadmin agencies list, implement flat design, add date filters, and fix UI bugs
This commit is contained in:
@@ -0,0 +1,302 @@
|
||||
'use client';
|
||||
|
||||
import { Fragment, useState } from 'react';
|
||||
import { Dialog, Transition, Tab } from '@headlessui/react';
|
||||
import {
|
||||
XMarkIcon,
|
||||
BuildingOfficeIcon,
|
||||
MapPinIcon,
|
||||
UserIcon,
|
||||
CheckCircleIcon
|
||||
} from '@heroicons/react/24/outline';
|
||||
|
||||
interface CreateAgencyModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
function classNames(...classes: string[]) {
|
||||
return classes.filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
export default function CreateAgencyModal({ isOpen, onClose, onSuccess }: CreateAgencyModalProps) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
// Agência
|
||||
agencyName: '',
|
||||
subdomain: '',
|
||||
cnpj: '',
|
||||
razaoSocial: '',
|
||||
description: '',
|
||||
website: '',
|
||||
industry: '',
|
||||
phone: '',
|
||||
teamSize: '',
|
||||
|
||||
// Endereço
|
||||
cep: '',
|
||||
state: '',
|
||||
city: '',
|
||||
neighborhood: '',
|
||||
street: '',
|
||||
number: '',
|
||||
complement: '',
|
||||
|
||||
// Admin
|
||||
adminEmail: '',
|
||||
adminPassword: '',
|
||||
adminName: '',
|
||||
});
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[e.target.name]: e.target.value
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/agencies/register', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
body: JSON.stringify(formData),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.text();
|
||||
throw new Error(errorData || 'Erro ao criar agência');
|
||||
}
|
||||
|
||||
onSuccess();
|
||||
onClose();
|
||||
// Reset form?
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const tabs = [
|
||||
{ name: 'Dados Gerais', icon: BuildingOfficeIcon },
|
||||
{ name: 'Endereço', icon: MapPinIcon },
|
||||
{ name: 'Administrador', icon: UserIcon },
|
||||
];
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-50" onClose={onClose}>
|
||||
<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 hidden pr-4 pt-4 sm:block">
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-md bg-white dark:bg-zinc-900 text-zinc-400 hover:text-zinc-500 focus:outline-none"
|
||||
onClick={onClose}
|
||||
>
|
||||
<span className="sr-only">Fechar</span>
|
||||
<XMarkIcon className="h-6 w-6" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-6 sm:p-8">
|
||||
<div className="sm:flex sm:items-start mb-6">
|
||||
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-zinc-100 dark:bg-zinc-800 sm:mx-0 sm:h-10 sm:w-10">
|
||||
<BuildingOfficeIcon className="h-6 w-6 text-[var(--brand-color)]" aria-hidden="true" />
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
|
||||
<Dialog.Title as="h3" className="text-xl font-semibold leading-6 text-zinc-900 dark:text-white">
|
||||
Nova Agência
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-zinc-500 dark:text-zinc-400">
|
||||
Preencha os dados abaixo para cadastrar uma nova agência parceira.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mb-6 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg text-sm text-red-800 dark:text-red-300">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Tab.Group>
|
||||
<Tab.List className="flex space-x-1 rounded-xl bg-zinc-100 dark:bg-zinc-800/50 p-1 mb-6">
|
||||
{tabs.map((tab) => (
|
||||
<Tab
|
||||
key={tab.name}
|
||||
className={({ selected }) =>
|
||||
classNames(
|
||||
'w-full rounded-lg py-2.5 text-sm font-medium leading-5 transition-all duration-200',
|
||||
'ring-white ring-opacity-60 ring-offset-2 ring-offset-[var(--brand-color)] focus:outline-none focus:ring-2',
|
||||
selected
|
||||
? 'bg-white dark:bg-zinc-800 text-[var(--brand-color)] shadow'
|
||||
: 'text-zinc-500 hover:bg-white/[0.12] hover:text-zinc-700 dark:hover:text-zinc-300'
|
||||
)
|
||||
}
|
||||
>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<tab.icon className="w-4 h-4" />
|
||||
{tab.name}
|
||||
</div>
|
||||
</Tab>
|
||||
))}
|
||||
</Tab.List>
|
||||
<Tab.Panels>
|
||||
{/* Dados Gerais */}
|
||||
<Tab.Panel className="space-y-4 focus:outline-none">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Input label="Nome da Agência *" name="agencyName" value={formData.agencyName} onChange={handleChange} required />
|
||||
<Input label="Subdomínio *" name="subdomain" value={formData.subdomain} onChange={handleChange} required prefix="http://" suffix=".aggios.app" />
|
||||
<Input label="CNPJ" name="cnpj" value={formData.cnpj} onChange={handleChange} />
|
||||
<Input label="Razão Social" name="razaoSocial" value={formData.razaoSocial} onChange={handleChange} />
|
||||
<Input label="Telefone" name="phone" value={formData.phone} onChange={handleChange} />
|
||||
<Input label="Website" name="website" value={formData.website} onChange={handleChange} />
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-1">Descrição</label>
|
||||
<textarea
|
||||
name="description"
|
||||
rows={3}
|
||||
className="w-full rounded-lg border border-zinc-200 dark:border-zinc-700 bg-white dark:bg-zinc-800/50 px-3 py-2 text-sm text-zinc-900 dark:text-white focus:border-[var(--brand-color)] focus:ring-1 focus:ring-[var(--brand-color)] outline-none transition-all"
|
||||
value={formData.description}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Tab.Panel>
|
||||
|
||||
{/* Endereço */}
|
||||
<Tab.Panel className="space-y-4 focus:outline-none">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Input label="CEP" name="cep" value={formData.cep} onChange={handleChange} />
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Input label="Estado" name="state" value={formData.state} onChange={handleChange} />
|
||||
<Input label="Cidade" name="city" value={formData.city} onChange={handleChange} />
|
||||
</div>
|
||||
<Input label="Bairro" name="neighborhood" value={formData.neighborhood} onChange={handleChange} />
|
||||
<Input label="Rua" name="street" value={formData.street} onChange={handleChange} />
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Input label="Número" name="number" value={formData.number} onChange={handleChange} />
|
||||
<Input label="Complemento" name="complement" value={formData.complement} onChange={handleChange} />
|
||||
</div>
|
||||
</div>
|
||||
</Tab.Panel>
|
||||
|
||||
{/* Administrador */}
|
||||
<Tab.Panel className="space-y-4 focus:outline-none">
|
||||
<div className="bg-zinc-50 dark:bg-zinc-800/50 p-4 rounded-lg mb-4 border border-zinc-100 dark:border-zinc-800">
|
||||
<p className="text-sm text-zinc-600 dark:text-zinc-400 flex items-center gap-2">
|
||||
<UserIcon className="w-4 h-4 text-[var(--brand-color)]" />
|
||||
Este usuário será o administrador principal da agência.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
<Input label="Nome Completo *" name="adminName" value={formData.adminName} onChange={handleChange} required />
|
||||
<Input label="E-mail *" name="adminEmail" type="email" value={formData.adminEmail} onChange={handleChange} required />
|
||||
<Input label="Senha *" name="adminPassword" type="password" value={formData.adminPassword} onChange={handleChange} required />
|
||||
</div>
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
|
||||
<div className="mt-8 flex items-center justify-end gap-3 border-t border-zinc-100 dark:border-zinc-800 pt-6">
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-lg px-4 py-2 text-sm font-medium text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors"
|
||||
onClick={onClose}
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="inline-flex justify-center rounded-lg px-4 py-2 text-sm font-medium text-white shadow-sm hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-[var(--brand-color)] focus:ring-offset-2 disabled:opacity-50 transition-all"
|
||||
style={{ background: 'var(--gradient)' }}
|
||||
>
|
||||
{loading ? 'Criando...' : 'Criar Agência'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
}
|
||||
|
||||
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
label: string;
|
||||
prefix?: string;
|
||||
suffix?: string;
|
||||
}
|
||||
|
||||
function Input({ label, prefix, suffix, className, ...props }: InputProps) {
|
||||
return (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-1">
|
||||
{label}
|
||||
</label>
|
||||
<div className="relative flex rounded-lg shadow-sm">
|
||||
{prefix && (
|
||||
<span className="inline-flex items-center rounded-l-lg border border-r-0 border-zinc-200 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-800 px-3 text-zinc-500 sm:text-sm">
|
||||
{prefix}
|
||||
</span>
|
||||
)}
|
||||
<input
|
||||
className={classNames(
|
||||
"block w-full border-zinc-200 dark:border-zinc-700 bg-white dark:bg-zinc-800/50 text-zinc-900 dark:text-white focus:border-[var(--brand-color)] focus:ring-1 focus:ring-[var(--brand-color)] sm:text-sm outline-none transition-all py-2 px-3",
|
||||
prefix ? "rounded-none" : "rounded-l-lg",
|
||||
suffix ? "rounded-none" : "rounded-r-lg",
|
||||
!prefix && !suffix ? "rounded-lg" : "",
|
||||
className || ""
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
{suffix && (
|
||||
<span className="inline-flex items-center rounded-r-lg border border-l-0 border-zinc-200 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-800 px-3 text-zinc-500 sm:text-sm">
|
||||
{suffix}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user