feat(admin): add direct client onboarding modal and management directly in app.4ventures.es
This commit is contained in:
46
src/app/api/admin/create-client/route.ts
Normal file
46
src/app/api/admin/create-client/route.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createClient } from '@/lib/supabase/server';
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const supabase = await createClient();
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'No autorizado. Inicia sesión.' }, { status: 401 });
|
||||
}
|
||||
|
||||
const userRole = user.app_metadata?.role || user.user_metadata?.role || 'client';
|
||||
if (userRole !== 'admin' && userRole !== 'team') {
|
||||
return NextResponse.json({ error: 'Permisos insuficientes. Solo administradores pueden crear clientes.' }, { status: 403 });
|
||||
}
|
||||
|
||||
const body = await req.json();
|
||||
const { companyName, slug, email, password, role } = body;
|
||||
|
||||
if (!companyName || !slug || !email || !password) {
|
||||
return NextResponse.json({ error: 'Todos los campos son obligatorios.' }, { status: 400 });
|
||||
}
|
||||
|
||||
const sanitizedSlug = slug.toLowerCase().trim().replace(/[^a-z0-9-]/g, '-');
|
||||
const targetRole = role === 'admin' || role === 'team' ? role : 'client';
|
||||
|
||||
const { data, error } = await supabase.rpc('provision_client', {
|
||||
p_company_name: companyName.trim(),
|
||||
p_slug: sanitizedSlug,
|
||||
p_email: email.toLowerCase().trim(),
|
||||
p_password: password,
|
||||
p_role: targetRole,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error('Error in provision_client RPC:', error);
|
||||
return NextResponse.json({ error: error.message || 'Error al provisionar cliente en base de datos.' }, { status: 500 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true, client: data });
|
||||
} catch (err: any) {
|
||||
console.error('Server error creating client:', err);
|
||||
return NextResponse.json({ error: err?.message || 'Error interno del servidor.' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Building2, DollarSign, Users, TrendingUp, Layers } from 'lucide-react';
|
||||
import ClientManagement from '@/components/ClientManagement';
|
||||
|
||||
export const revalidate = 0;
|
||||
|
||||
@@ -101,50 +102,8 @@ export default async function AgencyPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tenants Table */}
|
||||
<div className="bg-[#111827] border border-gray-800 rounded-xl overflow-hidden">
|
||||
<div className="p-5 border-b border-gray-800">
|
||||
<h3 className="text-base font-bold text-white">Cartera de Clientes Activos</h3>
|
||||
<p className="text-xs text-gray-400 mt-0.5">
|
||||
Cada cliente cuenta con aislamiento nativo Row Level Security (RLS) en Postgres.
|
||||
</p>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left text-sm text-gray-300">
|
||||
<thead className="bg-gray-900/50 text-xs uppercase text-gray-400 border-b border-gray-800">
|
||||
<tr>
|
||||
<th className="px-5 py-3">Nombre del Cliente</th>
|
||||
<th className="px-5 py-3">Tenant Slug</th>
|
||||
<th className="px-5 py-3">UUID</th>
|
||||
<th className="px-5 py-3">Acceso Portal</th>
|
||||
<th className="px-5 py-3">Estado</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-800/60">
|
||||
{tenants.map((t) => (
|
||||
<tr key={t.id} className="hover:bg-gray-800/30 transition-colors">
|
||||
<td className="px-5 py-3.5 font-semibold text-white">{t.name}</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-gray-400 font-mono">{t.id}</td>
|
||||
<td className="px-5 py-3.5 text-xs">
|
||||
<a
|
||||
href={`/dashboard/kpis?tenant=${t.slug}`}
|
||||
className="text-brand-400 hover:underline inline-flex items-center gap-1"
|
||||
>
|
||||
Abrir Portal ↗
|
||||
</a>
|
||||
</td>
|
||||
<td className="px-5 py-3.5 text-xs">
|
||||
<span className="bg-emerald-950/80 text-emerald-400 px-2 py-0.5 rounded-full border border-emerald-800/40 text-[10px] font-bold uppercase">
|
||||
Activo
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{/* Interactive Tenants Management with Onboarding Form */}
|
||||
<ClientManagement initialTenants={tenants} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
442
src/components/ClientManagement.tsx
Normal file
442
src/components/ClientManagement.tsx
Normal file
@@ -0,0 +1,442 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Plus,
|
||||
Key,
|
||||
Copy,
|
||||
Check,
|
||||
X,
|
||||
ExternalLink,
|
||||
ShieldCheck,
|
||||
Building2,
|
||||
Mail,
|
||||
Lock,
|
||||
Sparkles,
|
||||
Loader2,
|
||||
CheckCircle2
|
||||
} from 'lucide-react';
|
||||
|
||||
interface Tenant {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
interface ClientManagementProps {
|
||||
initialTenants: Tenant[];
|
||||
}
|
||||
|
||||
export default function ClientManagement({ initialTenants }: ClientManagementProps) {
|
||||
const [tenants, setTenants] = useState<Tenant[]>(initialTenants);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [errorMsg, setErrorMsg] = useState<string | null>(null);
|
||||
|
||||
// Form state
|
||||
const [companyName, setCompanyName] = useState('');
|
||||
const [slug, setSlug] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [role, setRole] = useState<'client' | 'team' | 'admin'>('client');
|
||||
const [autoSlug, setAutoSlug] = useState(true);
|
||||
|
||||
// Created access card state
|
||||
const [createdClient, setCreatedClient] = useState<{
|
||||
company: string;
|
||||
slug: string;
|
||||
email: string;
|
||||
password: string;
|
||||
role: string;
|
||||
} | null>(null);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
// Auto slug generation from company name
|
||||
const handleNameChange = (val: string) => {
|
||||
setCompanyName(val);
|
||||
if (autoSlug) {
|
||||
const generated = val
|
||||
.toLowerCase()
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/(^-|-$)+/g, '');
|
||||
setSlug(generated);
|
||||
}
|
||||
};
|
||||
|
||||
const generateRandomPassword = () => {
|
||||
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789!@#$%&*';
|
||||
let pw = '';
|
||||
for (let i = 0; i < 14; i++) {
|
||||
pw += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
setPassword(pw);
|
||||
};
|
||||
|
||||
const handleCreate = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setErrorMsg(null);
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/admin/create-client', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
companyName,
|
||||
slug,
|
||||
email,
|
||||
password,
|
||||
role
|
||||
})
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok || data.error) {
|
||||
throw new Error(data.error || 'Error al crear cliente');
|
||||
}
|
||||
|
||||
// Add to local state if not exists
|
||||
const newTenant: Tenant = {
|
||||
id: data.client.tenant_id,
|
||||
name: companyName,
|
||||
slug: slug,
|
||||
created_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
setTenants(prev => {
|
||||
const filtered = prev.filter(t => t.slug !== slug);
|
||||
return [...filtered, newTenant];
|
||||
});
|
||||
|
||||
setCreatedClient({
|
||||
company: companyName,
|
||||
slug: slug,
|
||||
email: email,
|
||||
password: password,
|
||||
role: role
|
||||
});
|
||||
|
||||
// Clear input fields
|
||||
setCompanyName('');
|
||||
setSlug('');
|
||||
setEmail('');
|
||||
setPassword('');
|
||||
} catch (err: any) {
|
||||
setErrorMsg(err.message || 'Error inesperado');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const copyCredentials = () => {
|
||||
if (!createdClient) return;
|
||||
const text = `🔐 *Acceso a tu Centro de Mando — 4Ventures*
|
||||
Empresa: ${createdClient.company}
|
||||
Portal Web: https://app.4ventures.es/login
|
||||
Usuario: ${createdClient.email}
|
||||
Contraseña: ${createdClient.password}
|
||||
Rol: ${createdClient.role === 'client' ? 'Cliente' : 'Agencia'}
|
||||
|
||||
Este portal cuenta con aislamiento de datos cifrado y métricas en tiempo real.`;
|
||||
|
||||
navigator.clipboard.writeText(text);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2500);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-[#111827] border border-gray-800 rounded-xl overflow-hidden shadow-xl">
|
||||
{/* Table Header with Action Button */}
|
||||
<div className="p-5 border-b border-gray-800 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h3 className="text-base font-bold text-white flex items-center gap-2">
|
||||
<Building2 className="w-4 h-4 text-brand-400" />
|
||||
Cartera de Clientes Activos ({tenants.length})
|
||||
</h3>
|
||||
<p className="text-xs text-gray-400 mt-0.5">
|
||||
Cada cliente cuenta con aislamiento nativo Row Level Security (RLS) en Postgres.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsOpen(true);
|
||||
setCreatedClient(null);
|
||||
setErrorMsg(null);
|
||||
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"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
Dar de Alta Cliente
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Tenants Table */}
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left text-sm text-gray-300">
|
||||
<thead className="bg-gray-900/50 text-xs uppercase text-gray-400 border-b border-gray-800">
|
||||
<tr>
|
||||
<th className="px-5 py-3">Nombre del Cliente</th>
|
||||
<th className="px-5 py-3">Tenant Slug</th>
|
||||
<th className="px-5 py-3">UUID</th>
|
||||
<th className="px-5 py-3">Acceso Portal</th>
|
||||
<th className="px-5 py-3">Estado</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-800/60">
|
||||
{tenants.map((t) => (
|
||||
<tr key={t.id} className="hover:bg-gray-800/30 transition-colors">
|
||||
<td className="px-5 py-3.5 font-semibold text-white flex items-center gap-2">
|
||||
<span className="w-2 h-2 rounded-full bg-emerald-400" />
|
||||
{t.name}
|
||||
</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-gray-400 font-mono">{t.id}</td>
|
||||
<td className="px-5 py-3.5 text-xs">
|
||||
<a
|
||||
href={`/dashboard/kpis?tenant=${t.slug}`}
|
||||
className="text-brand-400 hover:underline inline-flex items-center gap-1"
|
||||
>
|
||||
Abrir Portal <ExternalLink className="w-3 h-3" />
|
||||
</a>
|
||||
</td>
|
||||
<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">
|
||||
Activo
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Modal: Onboard New Client */}
|
||||
{isOpen && (
|
||||
<div className="fixed inset-0 z-50 bg-black/70 backdrop-blur-sm flex items-center justify-center p-4">
|
||||
<div className="bg-[#0D121F] border border-gray-800 rounded-2xl w-full max-w-lg overflow-hidden shadow-2xl relative animate-in fade-in zoom-in duration-200">
|
||||
{/* Modal Header */}
|
||||
<div className="p-6 border-b border-gray-800 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<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">
|
||||
<Building2 className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-white">Alta de Nuevo Cliente</h3>
|
||||
<p className="text-xs text-gray-400">Crea el portal corporativo y activa sus credenciales de acceso</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="text-gray-400 hover:text-white p-1 rounded-lg hover:bg-gray-800/60 transition-colors"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Modal Body */}
|
||||
<div className="p-6">
|
||||
{createdClient ? (
|
||||
/* Success Screen with Credentials */
|
||||
<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">
|
||||
<CheckCircle2 className="w-5 h-5 text-emerald-400 shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-emerald-300">¡Cliente Creado con Éxito!</h4>
|
||||
<p className="text-xs text-emerald-400/80 mt-0.5">
|
||||
El tenant ha sido configurado en Postgres con aislamiento RLS y el usuario ya puede iniciar sesión.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-900/80 rounded-xl p-4 border border-gray-800 space-y-2.5 font-mono text-xs">
|
||||
<div className="flex justify-between border-b border-gray-800 pb-2">
|
||||
<span className="text-gray-400">Empresa:</span>
|
||||
<span className="text-white font-semibold">{createdClient.company}</span>
|
||||
</div>
|
||||
<div className="flex justify-between border-b border-gray-800 pb-2">
|
||||
<span className="text-gray-400">URL Acceso:</span>
|
||||
<span className="text-brand-400">https://app.4ventures.es/login</span>
|
||||
</div>
|
||||
<div className="flex justify-between border-b border-gray-800 pb-2">
|
||||
<span className="text-gray-400">Email:</span>
|
||||
<span className="text-white">{createdClient.email}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-gray-400">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">
|
||||
{createdClient.password}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
type="button"
|
||||
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"
|
||||
>
|
||||
{copied ? <Check className="w-4 h-4 text-emerald-300" /> : <Copy className="w-4 h-4" />}
|
||||
<span>{copied ? '¡Copiado al Portapapeles!' : 'Copiar Credenciales para el Cliente'}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setCreatedClient(null);
|
||||
generateRandomPassword();
|
||||
}}
|
||||
className="py-2.5 px-4 rounded-xl bg-gray-800 hover:bg-gray-700 text-gray-200 text-xs font-semibold transition-all cursor-pointer"
|
||||
>
|
||||
Crear Otro
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
/* Creation Form */
|
||||
<form onSubmit={handleCreate} className="space-y-4">
|
||||
{errorMsg && (
|
||||
<div className="p-3 rounded-lg bg-red-500/10 border border-red-500/30 text-red-400 text-xs">
|
||||
{errorMsg}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-300 mb-1">
|
||||
Nombre Comercial de la Empresa / Cliente
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Building2 className="w-4 h-4 text-gray-400 absolute left-3 top-3" />
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
placeholder="Ej: Clínica Dental Sonrisas"
|
||||
value={companyName}
|
||||
onChange={(e) => handleNameChange(e.target.value)}
|
||||
className="w-full pl-9 pr-3 py-2 bg-gray-900 border border-gray-800 rounded-lg text-sm text-white placeholder-gray-500 focus:outline-none focus:border-brand-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<label className="text-xs font-medium text-gray-300">
|
||||
Slug del Tenant (identificador para subdominio y URL)
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setAutoSlug(!autoSlug)}
|
||||
className="text-[10px] text-brand-400 hover:underline cursor-pointer"
|
||||
>
|
||||
{autoSlug ? 'Modo manual' : 'Modo auto'}
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
placeholder="ej: clinica-sonrisas"
|
||||
value={slug}
|
||||
onChange={(e) => {
|
||||
setAutoSlug(false);
|
||||
setSlug(e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, ''));
|
||||
}}
|
||||
className="w-full px-3 py-2 bg-gray-900 border border-gray-800 rounded-lg text-sm font-mono text-brand-400 placeholder-gray-500 focus:outline-none focus:border-brand-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-300 mb-1">
|
||||
Correo Electrónico de Acceso
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Mail className="w-4 h-4 text-gray-400 absolute left-3 top-3" />
|
||||
<input
|
||||
type="email"
|
||||
required
|
||||
placeholder="director@cliente.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="w-full pl-9 pr-3 py-2 bg-gray-900 border border-gray-800 rounded-lg text-sm text-white placeholder-gray-500 focus:outline-none focus:border-brand-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<label className="text-xs font-medium text-gray-300">
|
||||
Contraseña Inicial
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={generateRandomPassword}
|
||||
className="text-[10px] text-brand-400 hover:underline flex items-center gap-1 cursor-pointer"
|
||||
>
|
||||
<Sparkles className="w-3 h-3" /> Generar aleatoria
|
||||
</button>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Lock className="w-4 h-4 text-gray-400 absolute left-3 top-3" />
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full pl-9 pr-3 py-2 bg-gray-900 border border-gray-800 rounded-lg text-sm font-mono text-white placeholder-gray-500 focus:outline-none focus:border-brand-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-300 mb-1">
|
||||
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-800 rounded-lg text-sm text-white focus:outline-none focus:border-brand-500"
|
||||
>
|
||||
<option value="client">Cliente (Acceso exclusivo a su portal y métricas)</option>
|
||||
<option value="team">Equipo Agencia (Acceso a toda la cartera)</option>
|
||||
<option value="admin">Administrador 4Ventures (Control total)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="pt-3 flex items-center justify-end gap-3 border-t border-gray-800">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="px-4 py-2 rounded-lg text-xs font-semibold text-gray-400 hover:text-white hover:bg-gray-800 transition-all cursor-pointer"
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
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"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
<span>Provisionando...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Key className="w-4 h-4" />
|
||||
<span>Crear y Activar Acceso</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user