feat: ficha por cliente en portafolio (conexiones, accesos, logo, módulos)
Cada tenant tiene una ficha con plataformas, invitaciones por email, logo de marca y toggles de módulos visibles en el portal cliente. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -40,9 +40,14 @@ const PLACEHOLDERS: Record<string, string> = {
|
||||
export default function ClientIntegrationsPanel({
|
||||
tenantId,
|
||||
tenantName,
|
||||
embedded = false,
|
||||
onChanged,
|
||||
}: {
|
||||
tenantId: string;
|
||||
tenantName: string;
|
||||
/** Hide page chrome when rendered inside ClientFicha. */
|
||||
embedded?: boolean;
|
||||
onChanged?: () => void;
|
||||
}) {
|
||||
const [integrations, setIntegrations] = useState<Integ[]>([]);
|
||||
const [providers, setProviders] = useState<Provider[]>([]);
|
||||
@@ -80,10 +85,7 @@ export default function ClientIntegrationsPanel({
|
||||
.catch(() => {});
|
||||
}, [tenantId]);
|
||||
|
||||
async function patch(
|
||||
provider: string,
|
||||
body: Record<string, unknown>
|
||||
) {
|
||||
async function patch(provider: string, body: Record<string, unknown>) {
|
||||
setSaving(provider);
|
||||
await fetch('/api/admin/integrations', {
|
||||
method: 'PATCH',
|
||||
@@ -91,6 +93,7 @@ export default function ClientIntegrationsPanel({
|
||||
body: JSON.stringify({ tenantId, provider, ...body }),
|
||||
});
|
||||
await load();
|
||||
onChanged?.();
|
||||
setSaving(null);
|
||||
}
|
||||
|
||||
@@ -116,49 +119,52 @@ export default function ClientIntegrationsPanel({
|
||||
{ id: 'email', title: 'Email / lifecycle' },
|
||||
{ id: 'cms', title: 'Web / e‑commerce' },
|
||||
{ id: 'files', title: 'Archivos' },
|
||||
{ id: 'ops', title: 'Operaciones' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-col sm:flex-row sm:items-start justify-between gap-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<Link
|
||||
href="/dashboard/agencia"
|
||||
className="text-gray-600 hover:text-gray-900"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
</Link>
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 tracking-tight">
|
||||
{tenantName}
|
||||
</h2>
|
||||
<p className="text-sm text-gray-400 mt-0.5">
|
||||
Hub de conexiones · asocia cuentas y decide qué entra a la
|
||||
plataforma
|
||||
</p>
|
||||
{!embedded ? (
|
||||
<div className="flex flex-col sm:flex-row sm:items-start justify-between gap-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<Link
|
||||
href="/dashboard/agencia"
|
||||
className="text-gray-600 hover:text-gray-900"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
</Link>
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 tracking-tight">
|
||||
{tenantName}
|
||||
</h2>
|
||||
<p className="text-sm text-gray-600 mt-0.5">
|
||||
Hub de conexiones · asocia cuentas y decide qué entra a la
|
||||
plataforma
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
disabled={followBusy}
|
||||
onClick={() => void toggleFollow()}
|
||||
className={`inline-flex items-center gap-1.5 text-xs px-3 py-1.5 rounded-lg border ${
|
||||
followed
|
||||
? 'border-emerald-300 text-emerald-700 bg-emerald-50'
|
||||
: 'border-gray-200 text-gray-600'
|
||||
}`}
|
||||
>
|
||||
{followBusy ? (
|
||||
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
||||
) : (
|
||||
<Power className="w-3.5 h-3.5" />
|
||||
)}
|
||||
{followed ? 'Cliente activo en portafolio' : 'Archivado'}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
disabled={followBusy}
|
||||
onClick={() => void toggleFollow()}
|
||||
className={`inline-flex items-center gap-1.5 text-xs px-3 py-1.5 rounded-lg border ${
|
||||
followed
|
||||
? 'border-emerald-500/40 text-emerald-400'
|
||||
: 'border-gray-200 text-gray-400'
|
||||
}`}
|
||||
>
|
||||
{followBusy ? (
|
||||
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
||||
) : (
|
||||
<Power className="w-3.5 h-3.5" />
|
||||
)}
|
||||
{followed ? 'Cliente activo en portafolio' : 'Archivado'}
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{warning && (
|
||||
<p className="text-xs text-amber-400 border border-amber-500/20 rounded-lg px-3 py-2">
|
||||
<p className="text-xs text-amber-900 border border-amber-300 bg-amber-50 rounded-lg px-3 py-2">
|
||||
{warning}
|
||||
</p>
|
||||
)}
|
||||
@@ -168,7 +174,7 @@ export default function ClientIntegrationsPanel({
|
||||
if (!list.length) return null;
|
||||
return (
|
||||
<section key={g.id} className="space-y-2">
|
||||
<h3 className="text-xs font-bold uppercase tracking-wider text-gray-500">
|
||||
<h3 className="text-xs font-bold uppercase tracking-wider text-gray-600">
|
||||
{g.title}
|
||||
</h3>
|
||||
<ul className="space-y-2">
|
||||
@@ -186,7 +192,7 @@ export default function ClientIntegrationsPanel({
|
||||
className={`rounded-xl border p-4 space-y-3 ${
|
||||
enabled
|
||||
? 'border-gray-200 bg-white'
|
||||
: 'border-gray-200/50 bg-[#0d121c] opacity-70'
|
||||
: 'border-gray-200 bg-gray-50 opacity-80'
|
||||
}`}
|
||||
>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
||||
@@ -195,7 +201,7 @@ export default function ClientIntegrationsPanel({
|
||||
{p.label}
|
||||
</p>
|
||||
{folderHint && (
|
||||
<p className="text-[11px] text-brand-400 mt-0.5">
|
||||
<p className="text-[11px] text-brand-700 mt-0.5">
|
||||
Carpeta: {folderHint}
|
||||
</p>
|
||||
)}
|
||||
@@ -206,7 +212,7 @@ export default function ClientIntegrationsPanel({
|
||||
</p>
|
||||
)}
|
||||
{row?.error_message && (
|
||||
<p className="text-[11px] text-red-400 mt-1">
|
||||
<p className="text-[11px] text-red-600 mt-1">
|
||||
{row.error_message}
|
||||
</p>
|
||||
)}
|
||||
@@ -215,13 +221,11 @@ export default function ClientIntegrationsPanel({
|
||||
<button
|
||||
type="button"
|
||||
disabled={saving === p.id}
|
||||
onClick={() =>
|
||||
void patch(p.id, { enabled: !enabled })
|
||||
}
|
||||
className={`text-[11px] px-2.5 py-1 rounded-md border ${
|
||||
onClick={() => void patch(p.id, { enabled: !enabled })}
|
||||
className={`text-[11px] font-semibold px-2.5 py-1 rounded-md border ${
|
||||
enabled
|
||||
? 'border-emerald-500/40 text-emerald-400'
|
||||
: 'border-gray-200 text-gray-500'
|
||||
? 'border-emerald-300 text-emerald-700 bg-emerald-50'
|
||||
: 'border-gray-200 text-gray-600'
|
||||
}`}
|
||||
title="Si está OFF, no cuenta en salud ni se usa"
|
||||
>
|
||||
@@ -239,10 +243,10 @@ export default function ClientIntegrationsPanel({
|
||||
externalAccountId: accountDraft[p.id] || '',
|
||||
})
|
||||
}
|
||||
className={`text-[11px] px-2.5 py-1 rounded-md border ${
|
||||
className={`text-[11px] font-semibold px-2.5 py-1 rounded-md border ${
|
||||
status === 'connected'
|
||||
? 'border-brand-500/40 text-brand-400'
|
||||
: 'border-gray-200 text-gray-400'
|
||||
? 'border-brand-300 text-brand-700 bg-brand-50'
|
||||
: 'border-gray-200 text-gray-600'
|
||||
}`}
|
||||
>
|
||||
{status === 'connected' ? 'Desconectar' : 'Conectar'}
|
||||
@@ -251,7 +255,7 @@ export default function ClientIntegrationsPanel({
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
<input
|
||||
className="flex-1 text-xs rounded-lg bg-gray-900 border border-gray-200 px-3 py-2 text-gray-900 placeholder:text-gray-600"
|
||||
className="flex-1 text-xs rounded-lg bg-white border border-gray-200 px-3 py-2 text-gray-900 placeholder:text-gray-400"
|
||||
placeholder={PLACEHOLDERS[p.id] || 'Account ID'}
|
||||
value={accountDraft[p.id] || ''}
|
||||
disabled={!enabled}
|
||||
@@ -267,11 +271,12 @@ export default function ClientIntegrationsPanel({
|
||||
disabled={saving === p.id || !enabled}
|
||||
onClick={() =>
|
||||
void patch(p.id, {
|
||||
status: status === 'disconnected' ? 'pending' : status,
|
||||
status:
|
||||
status === 'disconnected' ? 'pending' : status,
|
||||
externalAccountId: accountDraft[p.id] || '',
|
||||
})
|
||||
}
|
||||
className="text-xs px-3 py-2 rounded-lg border border-gray-200 text-gray-600 hover:text-gray-900 hover:border-gray-500 inline-flex items-center gap-1"
|
||||
className="text-xs font-semibold px-3 py-2 rounded-lg border border-gray-200 text-gray-700 hover:text-gray-900 hover:bg-gray-50 inline-flex items-center gap-1"
|
||||
>
|
||||
{saving === p.id ? (
|
||||
<Loader2 className="w-3 h-3 animate-spin" />
|
||||
|
||||
Reference in New Issue
Block a user