Let agency configure each client's left-nav modules from the ficha.
Checkboxes match the sidebar (incl. Social sub-items), with a live preview and route guard so clients cannot open disabled sections. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -13,6 +13,7 @@ import {
|
|||||||
} from '@/lib/agency';
|
} from '@/lib/agency';
|
||||||
import DashboardShell from '@/components/DashboardShell';
|
import DashboardShell from '@/components/DashboardShell';
|
||||||
import DashboardProviders from '@/components/DashboardProviders';
|
import DashboardProviders from '@/components/DashboardProviders';
|
||||||
|
import ModuleAccessGuard from '@/components/ModuleAccessGuard';
|
||||||
import {
|
import {
|
||||||
DashboardModuleId,
|
DashboardModuleId,
|
||||||
TenantSettings,
|
TenantSettings,
|
||||||
@@ -82,6 +83,10 @@ export default async function DashboardLayout({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardProviders>
|
<DashboardProviders>
|
||||||
|
<ModuleAccessGuard
|
||||||
|
isAgency={isAgency}
|
||||||
|
enabledModules={enabledModules}
|
||||||
|
/>
|
||||||
<DashboardShell
|
<DashboardShell
|
||||||
email={email}
|
email={email}
|
||||||
company={company}
|
company={company}
|
||||||
|
|||||||
@@ -20,13 +20,16 @@ import ClientIntegrationsPanel from '@/components/ClientIntegrationsPanel';
|
|||||||
import ClientCreativeStylePanel from '@/components/ClientCreativeStylePanel';
|
import ClientCreativeStylePanel from '@/components/ClientCreativeStylePanel';
|
||||||
import { PageHeader, SegmentedControl } from '@/components/ui';
|
import { PageHeader, SegmentedControl } from '@/components/ui';
|
||||||
import {
|
import {
|
||||||
DASHBOARD_MODULES,
|
|
||||||
DashboardModuleId,
|
DashboardModuleId,
|
||||||
TenantModuleMap,
|
TenantModuleMap,
|
||||||
TenantSettings,
|
TenantSettings,
|
||||||
|
childDashboardModules,
|
||||||
|
defaultModulesAllOn,
|
||||||
resolveTenantModules,
|
resolveTenantModules,
|
||||||
suggestModulesFromIntegrations,
|
suggestModulesFromIntegrations,
|
||||||
|
topLevelDashboardModules,
|
||||||
} from '@/lib/tenant-modules';
|
} from '@/lib/tenant-modules';
|
||||||
|
import { previewMenuLabels } from '@/components/ModuleAccessGuard';
|
||||||
|
|
||||||
type Tab = 'general' | 'estilo' | 'conexiones' | 'accesos' | 'modulos';
|
type Tab = 'general' | 'estilo' | 'conexiones' | 'accesos' | 'modulos';
|
||||||
|
|
||||||
@@ -308,7 +311,7 @@ export default function ClientFicha({
|
|||||||
{ id: 'estilo', label: 'Estilo Creativo (IA & Ads)' },
|
{ id: 'estilo', label: 'Estilo Creativo (IA & Ads)' },
|
||||||
{ id: 'conexiones', label: 'Conexiones' },
|
{ id: 'conexiones', label: 'Conexiones' },
|
||||||
{ id: 'accesos', label: 'Accesos' },
|
{ id: 'accesos', label: 'Accesos' },
|
||||||
{ id: 'modulos', label: 'Módulos' },
|
{ id: 'modulos', label: 'Menú cliente' },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -523,70 +526,145 @@ export default function ClientFicha({
|
|||||||
|
|
||||||
{tab === 'modulos' && (
|
{tab === 'modulos' && (
|
||||||
<section className="space-y-4 rounded-2xl border border-gray-200 bg-white p-5 shadow-sm">
|
<section className="space-y-4 rounded-2xl border border-gray-200 bg-white p-5 shadow-sm">
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
<div className="flex flex-col lg:flex-row lg:items-start justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2 text-sm font-semibold text-gray-900">
|
<div className="flex items-center gap-2 text-sm font-semibold text-gray-900">
|
||||||
<Shield className="w-4 h-4 text-brand-600" />
|
<Shield className="w-4 h-4 text-brand-600" />
|
||||||
Módulos del dash cliente
|
Menú del portal cliente
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-gray-600 mt-1">
|
<p className="text-xs text-gray-600 mt-1 max-w-xl">
|
||||||
Qué ve el cliente en el menú. Sugerencia según Meta / Google /
|
Marca o desmarca qué ve este cliente en el menú izquierdo. La
|
||||||
RRSS / CRM conectados.
|
agencia sigue viendo todo el menú al operar; el filtro solo
|
||||||
|
aplica a usuarios con rol cliente.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2 shrink-0">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setModules(defaultModulesAllOn())}
|
||||||
|
className="text-xs font-semibold px-3 py-1.5 rounded-lg border border-gray-200 text-gray-700 hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
Marcar todo
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
const off = defaultModulesAllOn();
|
||||||
|
for (const k of Object.keys(off) as DashboardModuleId[]) {
|
||||||
|
off[k] = k === 'kpis';
|
||||||
|
}
|
||||||
|
setModules(off);
|
||||||
|
}}
|
||||||
|
className="text-xs font-semibold px-3 py-1.5 rounded-lg border border-gray-200 text-gray-700 hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
Solo Métricas
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={applySuggested}
|
onClick={applySuggested}
|
||||||
className="inline-flex items-center gap-1.5 text-xs font-semibold px-3 py-1.5 rounded-lg border border-brand-200 text-brand-700 bg-brand-50 hover:bg-brand-100"
|
className="inline-flex items-center gap-1.5 text-xs font-semibold px-3 py-1.5 rounded-lg border border-brand-200 text-brand-700 bg-brand-50 hover:bg-brand-100"
|
||||||
>
|
>
|
||||||
<Sparkles className="w-3.5 h-3.5" />
|
<Sparkles className="w-3.5 h-3.5" />
|
||||||
Autocompletar por conexiones
|
Por conexiones
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ul className="space-y-2">
|
<ul className="space-y-2">
|
||||||
{DASHBOARD_MODULES.map((m) => {
|
{topLevelDashboardModules().map((m) => {
|
||||||
const on = modules[m.id] !== false;
|
const on = modules[m.id] !== false;
|
||||||
|
const children = childDashboardModules(m.id);
|
||||||
return (
|
return (
|
||||||
<li
|
<li key={m.id} className="space-y-1.5">
|
||||||
key={m.id}
|
<label
|
||||||
className={`flex items-center justify-between gap-3 rounded-xl border px-4 py-3 ${
|
className={`flex items-center justify-between gap-3 rounded-xl border px-4 py-3 cursor-pointer ${
|
||||||
on
|
on
|
||||||
? 'border-gray-200 bg-white'
|
? 'border-gray-200 bg-white'
|
||||||
: 'border-gray-100 bg-gray-50 opacity-80'
|
: 'border-gray-100 bg-gray-50 opacity-80'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div>
|
<div className="min-w-0">
|
||||||
<p className="text-sm font-semibold text-gray-900">
|
<p className="text-sm font-semibold text-gray-900">
|
||||||
{m.label}
|
{m.label}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-[11px] text-gray-600">{m.hint}</p>
|
<p className="text-[11px] text-gray-600">{m.hint}</p>
|
||||||
{m.providers.length ? (
|
|
||||||
<p className="text-[10px] text-gray-500 mt-0.5">
|
|
||||||
Suele ir con: {m.providers.join(', ')}
|
|
||||||
</p>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
<button
|
<input
|
||||||
type="button"
|
type="checkbox"
|
||||||
role="switch"
|
className="h-4 w-4 rounded border-gray-300 text-brand-600 focus:ring-brand-500 shrink-0"
|
||||||
aria-checked={on}
|
checked={on}
|
||||||
onClick={() =>
|
onChange={() => {
|
||||||
setModules((prev) => ({ ...prev, [m.id]: !on }))
|
setModules((prev) => {
|
||||||
|
const next = { ...prev, [m.id]: !on };
|
||||||
|
if (on) {
|
||||||
|
for (const c of children) next[c.id] = false;
|
||||||
}
|
}
|
||||||
className={`relative h-7 w-12 rounded-full transition-colors ${
|
return next;
|
||||||
on ? 'bg-brand-500' : 'bg-gray-300'
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
{children.length > 0 && on
|
||||||
|
? children.map((c) => {
|
||||||
|
const childOn = modules[c.id] !== false;
|
||||||
|
return (
|
||||||
|
<label
|
||||||
|
key={c.id}
|
||||||
|
className={`ml-6 flex items-center justify-between gap-3 rounded-xl border px-4 py-2.5 cursor-pointer ${
|
||||||
|
childOn
|
||||||
|
? 'border-gray-200 bg-white'
|
||||||
|
: 'border-gray-100 bg-gray-50 opacity-80'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span
|
<div className="min-w-0">
|
||||||
className={`absolute top-0.5 left-0.5 h-6 w-6 rounded-full bg-white shadow transition-transform ${
|
<p className="text-xs font-semibold text-gray-900">
|
||||||
on ? 'translate-x-5' : ''
|
↳ {c.label}
|
||||||
}`}
|
</p>
|
||||||
|
<p className="text-[10px] text-gray-600">
|
||||||
|
{c.hint}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="h-4 w-4 rounded border-gray-300 text-brand-600 focus:ring-brand-500 shrink-0"
|
||||||
|
checked={childOn}
|
||||||
|
onChange={() =>
|
||||||
|
setModules((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[c.id]: !childOn,
|
||||||
|
}))
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</button>
|
</label>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
: null}
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
<div className="rounded-xl border border-dashed border-brand-200 bg-brand-50/40 px-4 py-3">
|
||||||
|
<p className="text-[11px] font-semibold uppercase tracking-wide text-brand-800 mb-1.5">
|
||||||
|
Vista previa · menú del cliente
|
||||||
|
</p>
|
||||||
|
<ul className="flex flex-wrap gap-1.5">
|
||||||
|
{previewMenuLabels(modules).map((label) => (
|
||||||
|
<li
|
||||||
|
key={label}
|
||||||
|
className="rounded-md border border-brand-100 bg-white px-2 py-0.5 text-[11px] text-gray-800"
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
{!previewMenuLabels(modules).length ? (
|
||||||
|
<p className="text-xs text-amber-700">
|
||||||
|
Ningún módulo activo — el cliente no verá entradas en el menú.
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
@@ -598,7 +676,7 @@ export default function ClientFicha({
|
|||||||
) : (
|
) : (
|
||||||
<Check className="w-4 h-4" />
|
<Check className="w-4 h-4" />
|
||||||
)}
|
)}
|
||||||
Guardar módulos
|
Guardar menú del cliente
|
||||||
</button>
|
</button>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -180,11 +180,13 @@ export default function DashboardShell({
|
|||||||
const dateRange = useDateRange();
|
const dateRange = useDateRange();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
||||||
const visibleNav = NAV_ITEMS.filter((item) => {
|
const moduleOn = (id: DashboardModuleId) => {
|
||||||
if (isAgency) return true;
|
if (isAgency) return true;
|
||||||
if (!enabledModules) return true;
|
if (!enabledModules) return true;
|
||||||
return enabledModules[item.moduleId] !== false;
|
return enabledModules[id] !== false;
|
||||||
});
|
};
|
||||||
|
|
||||||
|
const visibleNav = NAV_ITEMS.filter((item) => moduleOn(item.moduleId));
|
||||||
|
|
||||||
const showClientBrand = Boolean(clientLogoUrl);
|
const showClientBrand = Boolean(clientLogoUrl);
|
||||||
const title = showClientBrand ? brandLabel || company : '4VENTURES';
|
const title = showClientBrand ? brandLabel || company : '4VENTURES';
|
||||||
@@ -249,6 +251,7 @@ export default function DashboardShell({
|
|||||||
<NavLink item={item} onNavigate={close} />
|
<NavLink item={item} onNavigate={close} />
|
||||||
{underSocial ? (
|
{underSocial ? (
|
||||||
<>
|
<>
|
||||||
|
{moduleOn('calendario') ? (
|
||||||
<Link
|
<Link
|
||||||
href="/dashboard/calendario"
|
href="/dashboard/calendario"
|
||||||
onClick={close}
|
onClick={close}
|
||||||
@@ -257,6 +260,8 @@ export default function DashboardShell({
|
|||||||
<CalendarDays className="w-3.5 h-3.5 shrink-0 text-pink-500" />
|
<CalendarDays className="w-3.5 h-3.5 shrink-0 text-pink-500" />
|
||||||
Calendario editorial
|
Calendario editorial
|
||||||
</Link>
|
</Link>
|
||||||
|
) : null}
|
||||||
|
{moduleOn('pipeline') ? (
|
||||||
<Link
|
<Link
|
||||||
href="/dashboard/creatives"
|
href="/dashboard/creatives"
|
||||||
onClick={close}
|
onClick={close}
|
||||||
@@ -265,6 +270,7 @@ export default function DashboardShell({
|
|||||||
<Images className="w-3.5 h-3.5 shrink-0 text-fuchsia-500" />
|
<Images className="w-3.5 h-3.5 shrink-0 text-fuchsia-500" />
|
||||||
Pipeline RRSS
|
Pipeline RRSS
|
||||||
</Link>
|
</Link>
|
||||||
|
) : null}
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
65
src/components/ModuleAccessGuard.tsx
Normal file
65
src/components/ModuleAccessGuard.tsx
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
|
import type { DashboardModuleId } from '@/lib/tenant-modules';
|
||||||
|
import {
|
||||||
|
DASHBOARD_MODULES,
|
||||||
|
isModuleEnabled,
|
||||||
|
topLevelDashboardModules,
|
||||||
|
} from '@/lib/tenant-modules';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirects client users away from dashboard routes whose module is off.
|
||||||
|
* Agency is never blocked (enabledModules is null for agency).
|
||||||
|
*/
|
||||||
|
export default function ModuleAccessGuard({
|
||||||
|
enabledModules,
|
||||||
|
isAgency,
|
||||||
|
}: {
|
||||||
|
enabledModules?: Record<DashboardModuleId, boolean> | null;
|
||||||
|
isAgency: boolean;
|
||||||
|
}) {
|
||||||
|
const pathname = usePathname();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isAgency || !enabledModules || !pathname?.startsWith('/dashboard')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Agency OS / account routes always allowed for agency only — clients don't hit them
|
||||||
|
if (
|
||||||
|
pathname.startsWith('/dashboard/agencia') ||
|
||||||
|
pathname.startsWith('/dashboard/account')
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isModuleEnabled(enabledModules, pathname)) return;
|
||||||
|
|
||||||
|
const fallback =
|
||||||
|
topLevelDashboardModules().find((m) => enabledModules[m.id] !== false)
|
||||||
|
?.href || '/dashboard/kpis';
|
||||||
|
if (fallback !== pathname) {
|
||||||
|
router.replace(fallback);
|
||||||
|
}
|
||||||
|
}, [pathname, enabledModules, isAgency, router]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Labels for preview lists (includes nested). */
|
||||||
|
export function previewMenuLabels(
|
||||||
|
modules: Record<DashboardModuleId, boolean>
|
||||||
|
): string[] {
|
||||||
|
const labels: string[] = [];
|
||||||
|
for (const m of DASHBOARD_MODULES) {
|
||||||
|
if (m.parentId) continue;
|
||||||
|
if (modules[m.id] === false) continue;
|
||||||
|
labels.push(m.label);
|
||||||
|
for (const child of DASHBOARD_MODULES.filter((c) => c.parentId === m.id)) {
|
||||||
|
if (modules[child.id] === false) continue;
|
||||||
|
labels.push(`↳ ${child.label}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return labels;
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ export type DashboardModuleId =
|
|||||||
| 'estrategia'
|
| 'estrategia'
|
||||||
| 'marca'
|
| 'marca'
|
||||||
| 'calendario'
|
| 'calendario'
|
||||||
|
| 'pipeline'
|
||||||
| 'ad-tracker'
|
| 'ad-tracker'
|
||||||
| 'social'
|
| 'social'
|
||||||
| 'organico'
|
| 'organico'
|
||||||
@@ -21,6 +22,8 @@ export type DashboardModuleDef = {
|
|||||||
/** Integration providers that typically unlock this module. */
|
/** Integration providers that typically unlock this module. */
|
||||||
providers: string[];
|
providers: string[];
|
||||||
hint: string;
|
hint: string;
|
||||||
|
/** Shown nested under this parent in the client-menu editor (not a top-level nav row). */
|
||||||
|
parentId?: DashboardModuleId;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DASHBOARD_MODULES: DashboardModuleDef[] = [
|
export const DASHBOARD_MODULES: DashboardModuleDef[] = [
|
||||||
@@ -59,13 +62,6 @@ export const DASHBOARD_MODULES: DashboardModuleDef[] = [
|
|||||||
providers: [],
|
providers: [],
|
||||||
hint: 'Brandbook: voz, oferta, ads, LinkedIn, blog',
|
hint: 'Brandbook: voz, oferta, ads, LinkedIn, blog',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'calendario',
|
|
||||||
href: '/dashboard/calendario',
|
|
||||||
label: 'Calendario Editorial',
|
|
||||||
providers: ['instagram', 'facebook', 'tiktok', 'linkedin'],
|
|
||||||
hint: 'RRSS / contenido',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'ad-tracker',
|
id: 'ad-tracker',
|
||||||
href: '/dashboard/ad-tracker',
|
href: '/dashboard/ad-tracker',
|
||||||
@@ -80,6 +76,22 @@ export const DASHBOARD_MODULES: DashboardModuleDef[] = [
|
|||||||
providers: ['instagram', 'facebook', 'tiktok', 'linkedin'],
|
providers: ['instagram', 'facebook', 'tiktok', 'linkedin'],
|
||||||
hint: 'RRSS orgánicas: perfiles, alcance y contenido',
|
hint: 'RRSS orgánicas: perfiles, alcance y contenido',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'calendario',
|
||||||
|
href: '/dashboard/calendario',
|
||||||
|
label: 'Calendario editorial',
|
||||||
|
providers: ['instagram', 'facebook', 'tiktok', 'linkedin'],
|
||||||
|
hint: 'Submenú de Social · planificación RRSS',
|
||||||
|
parentId: 'social',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pipeline',
|
||||||
|
href: '/dashboard/creatives',
|
||||||
|
label: 'Pipeline RRSS',
|
||||||
|
providers: ['instagram', 'facebook', 'tiktok', 'linkedin', 'google_drive'],
|
||||||
|
hint: 'Submenú de Social · piezas orgánicas a publicar',
|
||||||
|
parentId: 'social',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'organico',
|
id: 'organico',
|
||||||
href: '/dashboard/organico',
|
href: '/dashboard/organico',
|
||||||
@@ -148,9 +160,39 @@ export function suggestModulesFromIntegrations(
|
|||||||
}
|
}
|
||||||
// Always keep Métricas if any paid/analytics, else still on as home
|
// Always keep Métricas if any paid/analytics, else still on as home
|
||||||
if (!out.kpis) out.kpis = true;
|
if (!out.kpis) out.kpis = true;
|
||||||
|
// Nested Social tools follow Social when suggested
|
||||||
|
if (out.social) {
|
||||||
|
if (out.calendario === undefined) out.calendario = true;
|
||||||
|
if (out.pipeline === undefined) out.pipeline = true;
|
||||||
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Top-level nav modules (exclude nested sub-items). */
|
||||||
|
export function topLevelDashboardModules(): DashboardModuleDef[] {
|
||||||
|
return DASHBOARD_MODULES.filter((m) => !m.parentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Nested toggles under a parent (e.g. Social). */
|
||||||
|
export function childDashboardModules(
|
||||||
|
parentId: DashboardModuleId
|
||||||
|
): DashboardModuleDef[] {
|
||||||
|
return DASHBOARD_MODULES.filter((m) => m.parentId === parentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Resolve module id for a dashboard pathname (for access guards). */
|
||||||
|
export function moduleIdForPath(pathname: string): DashboardModuleId | null {
|
||||||
|
const path = pathname.split('?')[0] || pathname;
|
||||||
|
// Longer hrefs first so /dashboard/creatives wins over /dashboard
|
||||||
|
const ranked = [...DASHBOARD_MODULES].sort(
|
||||||
|
(a, b) => b.href.length - a.href.length
|
||||||
|
);
|
||||||
|
for (const m of ranked) {
|
||||||
|
if (path === m.href || path.startsWith(m.href + '/')) return m.id;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
export function resolveTenantModules(
|
export function resolveTenantModules(
|
||||||
settings: TenantSettings | null | undefined,
|
settings: TenantSettings | null | undefined,
|
||||||
integrations: { provider: string; status?: string; enabled?: boolean }[]
|
integrations: { provider: string; status?: string; enabled?: boolean }[]
|
||||||
@@ -172,9 +214,10 @@ export function isModuleEnabled(
|
|||||||
href: string
|
href: string
|
||||||
): boolean {
|
): boolean {
|
||||||
if (!modules) return true;
|
if (!modules) return true;
|
||||||
const def = DASHBOARD_MODULES.find(
|
const id = moduleIdForPath(href);
|
||||||
(m) => href === m.href || href.startsWith(m.href + '/')
|
if (!id) return true;
|
||||||
);
|
if (modules[id] === false) return false;
|
||||||
if (!def) return true;
|
const def = DASHBOARD_MODULES.find((m) => m.id === id);
|
||||||
return modules[def.id] !== false;
|
if (def?.parentId && modules[def.parentId] === false) return false;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user