style(clients): rediseño visual del modal de alta de cliente con inputs claros y selector de roles
This commit is contained in:
@@ -8,13 +8,18 @@ import {
|
|||||||
Check,
|
Check,
|
||||||
X,
|
X,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
ShieldCheck,
|
|
||||||
Building2,
|
Building2,
|
||||||
Mail,
|
Mail,
|
||||||
Lock,
|
Lock,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
Loader2,
|
Loader2,
|
||||||
CheckCircle2
|
CheckCircle2,
|
||||||
|
Eye,
|
||||||
|
EyeOff,
|
||||||
|
User,
|
||||||
|
Users,
|
||||||
|
Shield,
|
||||||
|
Globe
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
interface Tenant {
|
interface Tenant {
|
||||||
@@ -28,6 +33,30 @@ interface ClientManagementProps {
|
|||||||
initialTenants: Tenant[];
|
initialTenants: Tenant[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ROLES = [
|
||||||
|
{
|
||||||
|
id: 'client',
|
||||||
|
label: 'Cliente',
|
||||||
|
badge: 'Recomendado',
|
||||||
|
desc: 'Acceso exclusivo al portal y métricas de su empresa',
|
||||||
|
icon: User,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'team',
|
||||||
|
label: 'Equipo Agencia',
|
||||||
|
badge: 'Colaborador',
|
||||||
|
desc: 'Acceso a toda la cartera de clientes de 4Ventures',
|
||||||
|
icon: Users,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'admin',
|
||||||
|
label: 'Administrador',
|
||||||
|
badge: 'Control Total',
|
||||||
|
desc: 'Permisos completos de configuración y altas',
|
||||||
|
icon: Shield,
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
export default function ClientManagement({ initialTenants }: ClientManagementProps) {
|
export default function ClientManagement({ initialTenants }: ClientManagementProps) {
|
||||||
const [tenants, setTenants] = useState<Tenant[]>(initialTenants);
|
const [tenants, setTenants] = useState<Tenant[]>(initialTenants);
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
@@ -41,6 +70,7 @@ export default function ClientManagement({ initialTenants }: ClientManagementPro
|
|||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [role, setRole] = useState<'client' | 'team' | 'admin'>('client');
|
const [role, setRole] = useState<'client' | 'team' | 'admin'>('client');
|
||||||
const [autoSlug, setAutoSlug] = useState(true);
|
const [autoSlug, setAutoSlug] = useState(true);
|
||||||
|
const [showPassword, setShowPassword] = useState(true);
|
||||||
|
|
||||||
// Created access card state
|
// Created access card state
|
||||||
const [createdClient, setCreatedClient] = useState<{
|
const [createdClient, setCreatedClient] = useState<{
|
||||||
@@ -149,15 +179,17 @@ Este portal cuenta con aislamiento de datos cifrado y métricas en tiempo real.`
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white border border-gray-200 rounded-xl overflow-hidden shadow-xl">
|
<div className="bg-white border border-gray-200 rounded-2xl overflow-hidden shadow-sm">
|
||||||
{/* Table Header with Action Button */}
|
{/* Table Header with Action Button */}
|
||||||
<div className="p-5 border-b border-gray-200 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
<div className="p-5 sm:p-6 border-b border-gray-100 flex flex-col sm:flex-row sm:items-center justify-between gap-4 bg-gradient-to-r from-gray-50/50 via-white to-gray-50/30">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-base font-bold text-gray-900 flex items-center gap-2">
|
<h3 className="text-base font-bold text-gray-900 flex items-center gap-2.5">
|
||||||
<Building2 className="w-4 h-4 text-brand-400" />
|
<div className="w-7 h-7 rounded-lg bg-brand-50 border border-brand-200/60 flex items-center justify-center text-brand-600">
|
||||||
|
<Building2 className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
Cartera de Clientes Activos ({tenants.length})
|
Cartera de Clientes Activos ({tenants.length})
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xs text-gray-400 mt-0.5">
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
Cada cliente cuenta con aislamiento nativo Row Level Security (RLS) en Postgres.
|
Cada cliente cuenta con aislamiento nativo Row Level Security (RLS) en Postgres.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -169,7 +201,7 @@ Este portal cuenta con aislamiento de datos cifrado y métricas en tiempo real.`
|
|||||||
setErrorMsg(null);
|
setErrorMsg(null);
|
||||||
generateRandomPassword();
|
generateRandomPassword();
|
||||||
}}
|
}}
|
||||||
className="inline-flex items-center justify-center gap-2 px-4 py-2 rounded-lg bg-gradient-to-r from-brand-500 to-amber-600 hover:from-brand-600 hover:to-amber-700 text-white text-xs font-semibold shadow-lg shadow-brand-500/20 transition-all cursor-pointer"
|
className="inline-flex items-center justify-center gap-2 px-4 py-2.5 rounded-xl bg-gradient-to-r from-brand-500 via-brand-600 to-amber-600 hover:from-brand-600 hover:to-amber-700 text-white text-xs font-semibold shadow-md shadow-brand-500/20 hover:shadow-lg hover:shadow-brand-500/25 active:scale-[0.99] transition-all cursor-pointer shrink-0"
|
||||||
>
|
>
|
||||||
<Plus className="w-4 h-4" />
|
<Plus className="w-4 h-4" />
|
||||||
Dar de Alta Cliente
|
Dar de Alta Cliente
|
||||||
@@ -179,34 +211,34 @@ Este portal cuenta con aislamiento de datos cifrado y métricas en tiempo real.`
|
|||||||
{/* Tenants Table */}
|
{/* Tenants Table */}
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full text-left text-sm text-gray-600">
|
<table className="w-full text-left text-sm text-gray-600">
|
||||||
<thead className="bg-gray-900/50 text-xs uppercase text-gray-400 border-b border-gray-200">
|
<thead className="bg-gray-50/80 text-[11px] font-semibold uppercase tracking-wider text-gray-500 border-b border-gray-100">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-5 py-3">Nombre del Cliente</th>
|
<th className="px-5 py-3.5">Nombre del Cliente</th>
|
||||||
<th className="px-5 py-3">Tenant Slug</th>
|
<th className="px-5 py-3.5">Tenant Slug</th>
|
||||||
<th className="px-5 py-3">UUID</th>
|
<th className="px-5 py-3.5">UUID</th>
|
||||||
<th className="px-5 py-3">Acceso Portal</th>
|
<th className="px-5 py-3.5">Acceso Portal</th>
|
||||||
<th className="px-5 py-3">Estado</th>
|
<th className="px-5 py-3.5">Estado</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-800/60">
|
<tbody className="divide-y divide-gray-100">
|
||||||
{tenants.map((t) => (
|
{tenants.map((t) => (
|
||||||
<tr key={t.id} className="hover:bg-gray-50/30 transition-colors">
|
<tr key={t.id} className="hover:bg-gray-50/60 transition-colors">
|
||||||
<td className="px-5 py-3.5 font-semibold text-gray-900 flex items-center gap-2">
|
<td className="px-5 py-3.5 font-semibold text-gray-900 flex items-center gap-2.5">
|
||||||
<span className="w-2 h-2 rounded-full bg-emerald-400" />
|
<span className="w-2 h-2 rounded-full bg-emerald-500 ring-4 ring-emerald-50" />
|
||||||
{t.name}
|
{t.name}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-5 py-3.5 text-xs text-brand-400 font-mono">{t.slug}</td>
|
<td className="px-5 py-3.5 text-xs text-brand-600 font-mono font-medium">{t.slug}</td>
|
||||||
<td className="px-5 py-3.5 text-xs text-gray-400 font-mono">{t.id}</td>
|
<td className="px-5 py-3.5 text-xs text-gray-400 font-mono">{t.id}</td>
|
||||||
<td className="px-5 py-3.5 text-xs">
|
<td className="px-5 py-3.5 text-xs">
|
||||||
<a
|
<a
|
||||||
href={`/dashboard/kpis?tenant=${t.slug}`}
|
href={`/dashboard/kpis?tenant=${t.slug}`}
|
||||||
className="text-brand-400 hover:underline inline-flex items-center gap-1"
|
className="text-brand-600 hover:text-brand-700 font-medium hover:underline inline-flex items-center gap-1.5"
|
||||||
>
|
>
|
||||||
Abrir Portal <ExternalLink className="w-3 h-3" />
|
Abrir Portal <ExternalLink className="w-3.5 h-3.5" />
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-5 py-3.5 text-xs">
|
<td className="px-5 py-3.5 text-xs">
|
||||||
<span className="bg-emerald-950/80 text-emerald-400 px-2.5 py-0.5 rounded-full border border-emerald-800/40 text-[10px] font-bold uppercase tracking-wider">
|
<span className="bg-emerald-50 text-emerald-700 border border-emerald-200/70 px-2.5 py-0.5 rounded-full text-[10px] font-bold uppercase tracking-wider">
|
||||||
Activo
|
Activo
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
@@ -218,22 +250,24 @@ Este portal cuenta con aislamiento de datos cifrado y métricas en tiempo real.`
|
|||||||
|
|
||||||
{/* Modal: Onboard New Client */}
|
{/* Modal: Onboard New Client */}
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<div className="fixed inset-0 z-50 bg-black/70 backdrop-blur-sm flex items-center justify-center p-4">
|
<div className="fixed inset-0 z-50 bg-slate-900/40 backdrop-blur-sm flex items-center justify-center p-4 sm:p-6 overflow-y-auto">
|
||||||
<div className="bg-white border border-gray-200 rounded-2xl w-full max-w-lg overflow-hidden shadow-2xl relative animate-in fade-in zoom-in duration-200">
|
<div className="bg-white border border-gray-100 rounded-2xl w-full max-w-xl overflow-hidden shadow-2xl relative my-auto animate-in fade-in zoom-in-95 duration-200">
|
||||||
{/* Modal Header */}
|
{/* Modal Header */}
|
||||||
<div className="p-6 border-b border-gray-200 flex items-center justify-between">
|
<div className="px-6 py-5 border-b border-gray-100 flex items-center justify-between bg-gradient-to-r from-gray-50/60 via-white to-gray-50/30">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3.5">
|
||||||
<div className="w-9 h-9 rounded-xl bg-brand-500/10 border border-brand-500/20 flex items-center justify-center text-brand-400">
|
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-brand-50 to-amber-100/70 border border-brand-200/60 flex items-center justify-center text-brand-600 shadow-xs shrink-0">
|
||||||
<Building2 className="w-5 h-5" />
|
<Building2 className="w-5 h-5" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-bold text-gray-900">Alta de Nuevo Cliente</h3>
|
<h3 className="text-base sm:text-lg font-bold text-gray-900 tracking-tight">Alta de Nuevo Cliente</h3>
|
||||||
<p className="text-xs text-gray-400">Crea el portal corporativo y activa sus credenciales de acceso</p>
|
<p className="text-xs text-gray-500 mt-0.5">Crea el portal corporativo y activa sus credenciales de acceso</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => setIsOpen(false)}
|
onClick={() => setIsOpen(false)}
|
||||||
className="text-gray-600 hover:text-gray-900 p-1 rounded-lg hover:bg-gray-100 transition-colors"
|
className="text-gray-400 hover:text-gray-700 p-2 rounded-xl hover:bg-gray-100 transition-colors cursor-pointer"
|
||||||
|
aria-label="Cerrar modal"
|
||||||
>
|
>
|
||||||
<X className="w-5 h-5" />
|
<X className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
@@ -244,44 +278,51 @@ Este portal cuenta con aislamiento de datos cifrado y métricas en tiempo real.`
|
|||||||
{createdClient ? (
|
{createdClient ? (
|
||||||
/* Success Screen with Credentials */
|
/* Success Screen with Credentials */
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
<div className="p-4 rounded-xl bg-emerald-500/10 border border-emerald-500/30 flex items-start gap-3">
|
<div className="p-4 rounded-xl bg-emerald-50 border border-emerald-200/80 flex items-start gap-3">
|
||||||
<CheckCircle2 className="w-5 h-5 text-emerald-400 shrink-0 mt-0.5" />
|
<CheckCircle2 className="w-5 h-5 text-emerald-600 shrink-0 mt-0.5" />
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-sm font-semibold text-emerald-300">¡Cliente Creado con Éxito!</h4>
|
<h4 className="text-sm font-semibold text-emerald-900">¡Cliente Creado con Éxito!</h4>
|
||||||
<p className="text-xs text-emerald-400/80 mt-0.5">
|
<p className="text-xs text-emerald-700/90 mt-0.5 leading-relaxed">
|
||||||
El tenant ha sido configurado en Postgres con aislamiento RLS y el usuario ya puede iniciar sesión.
|
El tenant ha sido configurado en Postgres con aislamiento RLS y el usuario ya puede iniciar sesión en su centro de mando.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-gray-50 rounded-xl p-4 border border-gray-200 space-y-2.5 font-mono text-xs">
|
<div className="bg-gray-50/80 rounded-xl p-4 border border-gray-200/80 space-y-3 font-mono text-xs">
|
||||||
<div className="flex justify-between border-b border-gray-200 pb-2">
|
<div className="flex justify-between items-center border-b border-gray-200/70 pb-2.5">
|
||||||
<span className="text-gray-400">Empresa:</span>
|
<span className="text-gray-500 font-sans">Empresa:</span>
|
||||||
<span className="text-gray-900 font-semibold">{createdClient.company}</span>
|
<span className="text-gray-900 font-semibold">{createdClient.company}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between border-b border-gray-200 pb-2">
|
<div className="flex justify-between items-center border-b border-gray-200/70 pb-2.5">
|
||||||
<span className="text-gray-400">URL Acceso:</span>
|
<span className="text-gray-500 font-sans">URL de Acceso:</span>
|
||||||
<span className="text-brand-400">https://app.4ventures.es/login</span>
|
<a
|
||||||
|
href="https://app.4ventures.es/login"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="text-brand-600 hover:underline flex items-center gap-1"
|
||||||
|
>
|
||||||
|
https://app.4ventures.es/login <ExternalLink className="w-3 h-3" />
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between border-b border-gray-200 pb-2">
|
<div className="flex justify-between items-center border-b border-gray-200/70 pb-2.5">
|
||||||
<span className="text-gray-400">Email:</span>
|
<span className="text-gray-500 font-sans">Email:</span>
|
||||||
<span className="text-gray-900">{createdClient.email}</span>
|
<span className="text-gray-900 font-medium">{createdClient.email}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-gray-400">Contraseña:</span>
|
<span className="text-gray-500 font-sans">Contraseña:</span>
|
||||||
<span className="text-amber-400 font-bold bg-amber-950/40 px-2 py-0.5 rounded border border-amber-800/40">
|
<span className="text-amber-800 font-bold bg-amber-50 px-2.5 py-1 rounded-lg border border-amber-200 tracking-wider">
|
||||||
{createdClient.password}
|
{createdClient.password}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-3">
|
<div className="flex flex-col sm:flex-row gap-3 pt-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={copyCredentials}
|
onClick={copyCredentials}
|
||||||
className="flex-1 py-2.5 px-4 rounded-xl bg-brand-500 hover:bg-brand-600 text-white text-xs font-semibold flex items-center justify-center gap-2 transition-all cursor-pointer shadow-lg shadow-brand-500/20"
|
className="flex-1 py-2.5 px-4 rounded-xl bg-brand-500 hover:bg-brand-600 text-white text-xs font-semibold flex items-center justify-center gap-2 transition-all cursor-pointer shadow-md shadow-brand-500/20 active:scale-[0.99]"
|
||||||
>
|
>
|
||||||
{copied ? <Check className="w-4 h-4 text-emerald-300" /> : <Copy className="w-4 h-4" />}
|
{copied ? <Check className="w-4 h-4 text-emerald-200" /> : <Copy className="w-4 h-4" />}
|
||||||
<span>{copied ? '¡Copiado al Portapapeles!' : 'Copiar Credenciales para el Cliente'}</span>
|
<span>{copied ? '¡Copiado al Portapapeles!' : 'Copiar Credenciales para el Cliente'}</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@@ -290,7 +331,7 @@ Este portal cuenta con aislamiento de datos cifrado y métricas en tiempo real.`
|
|||||||
setCreatedClient(null);
|
setCreatedClient(null);
|
||||||
generateRandomPassword();
|
generateRandomPassword();
|
||||||
}}
|
}}
|
||||||
className="py-2.5 px-4 rounded-xl bg-gray-100 hover:bg-gray-700 text-gray-800 text-xs font-semibold transition-all cursor-pointer"
|
className="py-2.5 px-4 rounded-xl bg-gray-100 hover:bg-gray-200 text-gray-700 text-xs font-semibold transition-all cursor-pointer text-center"
|
||||||
>
|
>
|
||||||
Crear Otro
|
Crear Otro
|
||||||
</button>
|
</button>
|
||||||
@@ -298,43 +339,49 @@ Este portal cuenta con aislamiento de datos cifrado y métricas en tiempo real.`
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
/* Creation Form */
|
/* Creation Form */
|
||||||
<form onSubmit={handleCreate} className="space-y-4">
|
<form onSubmit={handleCreate} className="space-y-4 sm:space-y-5">
|
||||||
{errorMsg && (
|
{errorMsg && (
|
||||||
<div className="p-3 rounded-lg bg-red-500/10 border border-red-500/30 text-red-400 text-xs">
|
<div className="p-3.5 rounded-xl bg-red-50 border border-red-200 text-red-700 text-xs flex items-center gap-2">
|
||||||
{errorMsg}
|
<X className="w-4 h-4 text-red-500 shrink-0" />
|
||||||
|
<span>{errorMsg}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Nombre del Cliente */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-gray-600 mb-1">
|
<label className="block text-xs font-semibold text-gray-700 mb-1.5">
|
||||||
Nombre Comercial de la Empresa / Cliente
|
Nombre Comercial de la Empresa / Cliente
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative group">
|
||||||
<Building2 className="w-4 h-4 text-gray-400 absolute left-3 top-3" />
|
<Building2 className="w-4 h-4 text-gray-400 absolute left-3.5 top-1/2 -translate-y-1/2 pointer-events-none group-focus-within:text-brand-500 transition-colors" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
placeholder="Ej: Clínica Dental Sonrisas"
|
placeholder="Ej: Noberplast S.L. o Clínica Sonrisas"
|
||||||
value={companyName}
|
value={companyName}
|
||||||
onChange={(e) => handleNameChange(e.target.value)}
|
onChange={(e) => handleNameChange(e.target.value)}
|
||||||
className="w-full pl-9 pr-3 py-2 bg-gray-900 border border-gray-200 rounded-lg text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:border-brand-500"
|
className="w-full pl-10 pr-3.5 py-2.5 bg-gray-50/70 hover:bg-white focus:bg-white border border-gray-200 hover:border-gray-300 focus:border-brand-500 rounded-xl text-sm text-gray-900 placeholder:text-gray-400 focus:outline-none focus:ring-4 focus:ring-brand-500/10 transition-all font-normal"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Slug del Tenant */}
|
||||||
<div>
|
<div>
|
||||||
<div className="flex justify-between items-center mb-1">
|
<div className="flex justify-between items-center mb-1.5">
|
||||||
<label className="text-xs font-medium text-gray-600">
|
<label className="text-xs font-semibold text-gray-700 flex items-center gap-1.5">
|
||||||
Slug del Tenant (identificador para subdominio y URL)
|
Slug del Tenant
|
||||||
|
<span className="text-[11px] font-normal text-gray-400">(identificador URL y subdominio)</span>
|
||||||
</label>
|
</label>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setAutoSlug(!autoSlug)}
|
onClick={() => setAutoSlug(!autoSlug)}
|
||||||
className="text-[10px] text-brand-400 hover:underline cursor-pointer"
|
className="text-[11px] font-medium text-brand-600 hover:text-brand-700 bg-brand-50 hover:bg-brand-100/80 px-2 py-0.5 rounded-full border border-brand-200/50 transition-colors flex items-center gap-1 cursor-pointer"
|
||||||
>
|
>
|
||||||
{autoSlug ? 'Modo manual' : 'Modo auto'}
|
{autoSlug ? '✨ Modo auto' : '✏️ Modo manual'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="relative group">
|
||||||
|
<Globe className="w-4 h-4 text-gray-400 absolute left-3.5 top-1/2 -translate-y-1/2 pointer-events-none group-focus-within:text-brand-500 transition-colors" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
@@ -344,79 +391,125 @@ Este portal cuenta con aislamiento de datos cifrado y métricas en tiempo real.`
|
|||||||
setAutoSlug(false);
|
setAutoSlug(false);
|
||||||
setSlug(e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, ''));
|
setSlug(e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, ''));
|
||||||
}}
|
}}
|
||||||
className="w-full px-3 py-2 bg-gray-900 border border-gray-200 rounded-lg text-sm font-mono text-brand-400 placeholder-gray-500 focus:outline-none focus:border-brand-500"
|
className="w-full pl-10 pr-3.5 py-2.5 bg-gray-50/70 hover:bg-white focus:bg-white border border-gray-200 hover:border-gray-300 focus:border-brand-500 rounded-xl text-sm font-mono text-brand-600 placeholder:text-gray-400 focus:outline-none focus:ring-4 focus:ring-brand-500/10 transition-all font-medium"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<p className="text-[11px] text-gray-400 mt-1 pl-1">
|
||||||
|
Enlace de acceso: <span className="font-mono text-gray-600">app.4ventures.es/dashboard/kpis?tenant={slug || 'cliente'}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Correo Electrónico */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-gray-600 mb-1">
|
<label className="block text-xs font-semibold text-gray-700 mb-1.5">
|
||||||
Correo Electrónico de Acceso
|
Correo Electrónico de Acceso
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative group">
|
||||||
<Mail className="w-4 h-4 text-gray-400 absolute left-3 top-3" />
|
<Mail className="w-4 h-4 text-gray-400 absolute left-3.5 top-1/2 -translate-y-1/2 pointer-events-none group-focus-within:text-brand-500 transition-colors" />
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
required
|
required
|
||||||
placeholder="director@cliente.com"
|
placeholder="director@cliente.com"
|
||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
className="w-full pl-9 pr-3 py-2 bg-gray-900 border border-gray-200 rounded-lg text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:border-brand-500"
|
className="w-full pl-10 pr-3.5 py-2.5 bg-gray-50/70 hover:bg-white focus:bg-white border border-gray-200 hover:border-gray-300 focus:border-brand-500 rounded-xl text-sm text-gray-900 placeholder:text-gray-400 focus:outline-none focus:ring-4 focus:ring-brand-500/10 transition-all font-normal"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Contraseña Inicial */}
|
||||||
<div>
|
<div>
|
||||||
<div className="flex justify-between items-center mb-1">
|
<div className="flex justify-between items-center mb-1.5">
|
||||||
<label className="text-xs font-medium text-gray-600">
|
<label className="text-xs font-semibold text-gray-700">
|
||||||
Contraseña Inicial
|
Contraseña Inicial
|
||||||
</label>
|
</label>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={generateRandomPassword}
|
onClick={generateRandomPassword}
|
||||||
className="text-[10px] text-brand-400 hover:underline flex items-center gap-1 cursor-pointer"
|
className="text-[11px] font-medium text-brand-600 hover:text-brand-700 bg-brand-50 hover:bg-brand-100/80 px-2.5 py-0.5 rounded-lg border border-brand-200/50 transition-colors flex items-center gap-1.5 cursor-pointer"
|
||||||
>
|
>
|
||||||
<Sparkles className="w-3 h-3" /> Generar aleatoria
|
<Sparkles className="w-3 h-3 text-brand-500" /> Generar aleatoria
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative">
|
<div className="relative group">
|
||||||
<Lock className="w-4 h-4 text-gray-400 absolute left-3 top-3" />
|
<Lock className="w-4 h-4 text-gray-400 absolute left-3.5 top-1/2 -translate-y-1/2 pointer-events-none group-focus-within:text-brand-500 transition-colors" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type={showPassword ? 'text' : 'password'}
|
||||||
required
|
required
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
className="w-full pl-9 pr-3 py-2 bg-gray-900 border border-gray-200 rounded-lg text-sm font-mono text-gray-900 placeholder-gray-500 focus:outline-none focus:border-brand-500"
|
className="w-full pl-10 pr-10 py-2.5 bg-gray-50/70 hover:bg-white focus:bg-white border border-gray-200 hover:border-gray-300 focus:border-brand-500 rounded-xl text-sm font-mono text-gray-900 placeholder:text-gray-400 focus:outline-none focus:ring-4 focus:ring-brand-500/10 transition-all font-medium"
|
||||||
/>
|
/>
|
||||||
</div>
|
<button
|
||||||
</div>
|
type="button"
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
<div>
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 p-1 rounded transition-colors"
|
||||||
<label className="block text-xs font-medium text-gray-600 mb-1">
|
title={showPassword ? 'Ocultar contraseña' : 'Ver contraseña'}
|
||||||
Rol de Acceso
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
value={role}
|
|
||||||
onChange={(e: any) => setRole(e.target.value)}
|
|
||||||
className="w-full px-3 py-2 bg-gray-900 border border-gray-200 rounded-lg text-sm text-gray-900 focus:outline-none focus:border-brand-500"
|
|
||||||
>
|
>
|
||||||
<option value="client">Cliente (Acceso exclusivo a su portal y métricas)</option>
|
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||||||
<option value="team">Equipo Agencia (Acceso a toda la cartera)</option>
|
</button>
|
||||||
<option value="admin">Administrador 4Ventures (Control total)</option>
|
</div>
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="pt-3 flex items-center justify-end gap-3 border-t border-gray-200">
|
{/* Rol de Acceso - Tarjetas visuales */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-gray-700 mb-2">
|
||||||
|
Rol de Acceso y Nivel de Permisos
|
||||||
|
</label>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2.5">
|
||||||
|
{ROLES.map((r) => {
|
||||||
|
const isSelected = role === r.id;
|
||||||
|
const IconComponent = r.icon;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
key={r.id}
|
||||||
|
onClick={() => setRole(r.id)}
|
||||||
|
className={`p-3 rounded-xl border text-left transition-all cursor-pointer relative flex flex-col justify-between ${
|
||||||
|
isSelected
|
||||||
|
? 'border-brand-500 bg-brand-50/40 ring-1 ring-brand-500/40 shadow-xs'
|
||||||
|
: 'border-gray-200/90 bg-white hover:border-gray-300 hover:bg-gray-50/50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-1.5">
|
||||||
|
<div className={`w-6 h-6 rounded-lg flex items-center justify-center ${
|
||||||
|
isSelected ? 'bg-brand-500 text-white' : 'bg-gray-100 text-gray-500'
|
||||||
|
}`}>
|
||||||
|
<IconComponent className="w-3.5 h-3.5" />
|
||||||
|
</div>
|
||||||
|
{isSelected && (
|
||||||
|
<span className="w-4 h-4 rounded-full bg-brand-500 text-white flex items-center justify-center text-[10px]">
|
||||||
|
✓
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className={`text-xs font-bold leading-tight ${isSelected ? 'text-brand-900' : 'text-gray-800'}`}>
|
||||||
|
{r.label}
|
||||||
|
</p>
|
||||||
|
<p className="text-[10px] text-gray-500 mt-1 leading-snug">
|
||||||
|
{r.desc}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Botones de acción */}
|
||||||
|
<div className="pt-4 flex items-center justify-end gap-3 border-t border-gray-100">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIsOpen(false)}
|
onClick={() => setIsOpen(false)}
|
||||||
className="px-4 py-2 rounded-lg text-xs font-semibold text-gray-600 hover:text-gray-900 hover:bg-gray-50 transition-all cursor-pointer"
|
className="px-4 py-2.5 rounded-xl text-xs font-semibold text-gray-600 hover:text-gray-900 hover:bg-gray-100 transition-all cursor-pointer"
|
||||||
>
|
>
|
||||||
Cancelar
|
Cancelar
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="px-5 py-2 rounded-lg bg-gradient-to-r from-brand-500 to-amber-600 hover:from-brand-600 hover:to-amber-700 text-white text-xs font-semibold shadow-lg shadow-brand-500/20 flex items-center gap-2 transition-all disabled:opacity-50 cursor-pointer"
|
className="px-5 py-2.5 rounded-xl bg-gradient-to-r from-brand-500 via-brand-600 to-amber-600 hover:from-brand-600 hover:to-amber-700 text-white text-xs font-semibold shadow-md shadow-brand-500/20 hover:shadow-lg hover:shadow-brand-500/25 active:scale-[0.99] flex items-center gap-2 transition-all disabled:opacity-50 cursor-pointer"
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<>
|
<>
|
||||||
|
|||||||
Reference in New Issue
Block a user