refactor: remove module system integration from admin and CLI

Removes all module-related logic from the admin dashboard, CLI database
initialization, and AdminPages component:

- Drop `initModules` call from `db init` CLI command and simplify the
  completion message to only reflect core feature tables
- Remove `getModuleDashboardStats` and module page routing from admin
  stats actions and update usage documentation accordingly
- Simplify `AdminPagesClient` to remove module page loading, lazy
  components, and module-specific props (`moduleStats`, `modulePageInfo`,
  `routeInfo`, `enabledModules`)
This commit is contained in:
2026-04-14 19:26:48 -04:00
parent 242ea69664
commit 3131df2b71
9 changed files with 415 additions and 239 deletions
+1 -12
View File
@@ -69,18 +69,7 @@ async function runCLI() {
const { initFeatures } = await import('../features/init.js');
const featuresResult = await initFeatures();
// Module tables are initialized per-module, if present
let modulesResult = { created: [], skipped: [] };
try {
const { initModules } = await import('../modules/init.js');
modulesResult = await initModules();
} catch {
// Modules may not be present in all project setups — silently skip
}
const totalCreated = featuresResult.created.length + modulesResult.created.length;
const totalSkipped = featuresResult.skipped.length + modulesResult.skipped.length;
done(`DB ready — ${totalCreated} tables created, ${totalSkipped} skipped`);
done(`DB ready — ${featuresResult.created.length} tables created, ${featuresResult.skipped.length} skipped`);
break;
}
+11 -19
View File
@@ -1,34 +1,26 @@
/**
* Admin Stats Actions
* Server-side actions for core dashboard statistics
*
* Module-specific stats are handled by each module's dashboard actions.
* See src/modules/{module}/dashboard/statsActions.js
*
*
* Usage in your Next.js app:
*
*
* ```javascript
* // app/(admin)/admin/[...admin]/page.js
* import { protectAdmin } from '@zen/core/admin';
* import { getDashboardStats, getModuleDashboardStats } from '@zen/core/admin/actions';
* import { getDashboardStats } from '@zen/core/admin/actions';
* import { AdminPagesClient } from '@zen/core/admin/pages';
*
*
* export default async function AdminPage({ params }) {
* const { user } = await protectAdmin();
*
* // Fetch core dashboard stats
*
* const statsResult = await getDashboardStats();
* const dashboardStats = statsResult.success ? statsResult.stats : null;
*
* // Fetch module dashboard stats (for dynamic widgets)
* const moduleStats = await getModuleDashboardStats();
*
*
* return (
* <AdminPagesClient
* params={params}
* <AdminPagesClient
* params={params}
* user={user}
* dashboardStats={dashboardStats}
* moduleStats={moduleStats}
* />
* );
* }
@@ -72,9 +64,9 @@ export async function getDashboardStats() {
};
} catch (error) {
fail(`Error getting dashboard stats: ${error.message}`);
return {
success: false,
error: error.message || 'Failed to get dashboard statistics'
return {
success: false,
error: error.message || 'Failed to get dashboard statistics'
};
}
}
+9 -70
View File
@@ -1,85 +1,24 @@
'use client';
/**
* Admin Pages Component
*
* This component handles both core admin pages and module pages.
* Module pages are loaded dynamically on the client where hooks work properly.
*/
import { Suspense } from 'react';
import DashboardPage from './pages/DashboardPage.js';
import UsersPage from './pages/UsersPage.js';
import UserEditPage from './pages/UserEditPage.js';
import ProfilePage from './pages/ProfilePage.js';
import { getModulePageLoader } from '../../../modules/modules.pages.js';
// Loading component for suspense
function PageLoading() {
return (
<div className="flex items-center justify-center min-h-[400px]">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-white"></div>
</div>
);
}
export default function AdminPagesClient({ params, user, dashboardStats = null }) {
const parts = params?.admin || [];
const page = parts[0] || 'dashboard';
export default function AdminPagesClient({
params,
user,
dashboardStats = null,
moduleStats = {},
modulePageInfo = null,
routeInfo = null,
enabledModules = {}
}) {
// If this is a module page, render it with lazy loading
if (modulePageInfo && routeInfo) {
const LazyComponent = getModulePageLoader(modulePageInfo.module, modulePageInfo.path);
if (LazyComponent) {
// Build props for the page
const pageProps = { user };
if (routeInfo.action === 'edit' && routeInfo.id) {
// Add ID props for edit pages (modules may use different prop names)
pageProps.id = routeInfo.id;
pageProps.invoiceId = routeInfo.id;
pageProps.clientId = routeInfo.id;
pageProps.itemId = routeInfo.id;
pageProps.categoryId = routeInfo.id;
pageProps.transactionId = routeInfo.id;
pageProps.recurrenceId = routeInfo.id;
pageProps.templateId = routeInfo.id;
pageProps.postId = routeInfo.id;
}
return (
<Suspense fallback={<PageLoading />}>
<LazyComponent {...pageProps} />
</Suspense>
);
}
if (page === 'users' && parts[1] === 'edit' && parts[2]) {
return <UserEditPage userId={parts[2]} user={user} />;
}
// Determine core page from routeInfo or params
let currentPage = 'dashboard';
if (routeInfo?.path) {
const parts = routeInfo.path.split('/').filter(Boolean);
currentPage = parts[1] || 'dashboard'; // /admin/[page]
} else if (params?.admin) {
currentPage = params.admin[0] || 'dashboard';
}
// Core page components mapping (non-module pages)
const usersPageComponent = routeInfo?.action === 'edit' && routeInfo?.id
? () => <UserEditPage userId={routeInfo.id} user={user} enabledModules={enabledModules} />
: () => <UsersPage user={user} />;
const corePages = {
dashboard: () => <DashboardPage user={user} stats={dashboardStats} moduleStats={moduleStats} enabledModules={enabledModules} />,
users: usersPageComponent,
dashboard: () => <DashboardPage user={user} stats={dashboardStats} />,
users: () => <UsersPage user={user} />,
profile: () => <ProfilePage user={user} />,
};
// Render the appropriate core page or default to dashboard
const CorePageComponent = corePages[currentPage];
return CorePageComponent ? <CorePageComponent /> : <DashboardPage user={user} stats={dashboardStats} moduleStats={moduleStats} enabledModules={enabledModules} />;
const CorePageComponent = corePages[page];
return CorePageComponent ? <CorePageComponent /> : <DashboardPage user={user} stats={dashboardStats} />;
}
@@ -1,33 +1,11 @@
'use client';
/**
* Admin Dashboard Page
* Displays core stats and dynamically loads module dashboard widgets
*/
import { Suspense } from 'react';
import { StatCard } from '../../../../shared/components';
import { UserMultiple02Icon } from '../../../../shared/Icons.js';
import { getModuleDashboardWidgets } from '../../../../modules/modules.pages.js';
/**
* Loading placeholder for widgets
*/
function WidgetLoading() {
return (
<div className="animate-pulse bg-neutral-200 dark:bg-neutral-800 rounded-lg h-32"></div>
);
}
export default function DashboardPage({ user, stats, moduleStats = {}, enabledModules = {} }) {
export default function DashboardPage({ user, stats }) {
const loading = !stats;
// Get only enabled module dashboard widgets
const allModuleWidgets = getModuleDashboardWidgets();
const moduleWidgets = Object.fromEntries(
Object.entries(allModuleWidgets).filter(([moduleName]) => enabledModules[moduleName])
);
return (
<div className="flex flex-col gap-4 sm:gap-6 lg:gap-8">
<div className="flex items-center justify-between">
@@ -40,16 +18,6 @@ export default function DashboardPage({ user, stats, moduleStats = {}, enabledMo
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6">
{/* Module dashboard widgets (dynamically loaded) */}
{Object.entries(moduleWidgets).map(([moduleName, widgets]) => (
widgets.map((Widget, index) => (
<Suspense key={`${moduleName}-widget-${index}`} fallback={<WidgetLoading />}>
<Widget stats={moduleStats[moduleName]} />
</Suspense>
))
))}
{/* Core stats - always shown */}
<StatCard
title="Nombre d'utilisateurs"
value={loading ? '-' : String(stats?.totalUsers || 0)}
@@ -9,20 +9,17 @@ import { useToast } from '@zen/core/toast';
* User Edit Page Component
* Page for editing an existing user (admin only)
*/
const UserEditPage = ({ userId, user, enabledModules = {} }) => {
const UserEditPage = ({ userId, user }) => {
const router = useRouter();
const toast = useToast();
const clientsModuleActive = Boolean(enabledModules?.clients);
const [userData, setUserData] = useState(null);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [clients, setClients] = useState([]);
const [formData, setFormData] = useState({
name: '',
role: 'user',
email_verified: 'false',
client_id: ''
});
const [errors, setErrors] = useState({});
@@ -40,15 +37,6 @@ const UserEditPage = ({ userId, user, enabledModules = {} }) => {
loadUser();
}, [userId]);
useEffect(() => {
if (clientsModuleActive) {
fetch('/zen/api/admin/clients?limit=500', { credentials: 'include' })
.then(res => res.json())
.then(data => data.clients ? setClients(data.clients) : setClients([]))
.catch(() => setClients([]));
}
}, [clientsModuleActive]);
const loadUser = async () => {
try {
setLoading(true);
@@ -64,7 +52,6 @@ const UserEditPage = ({ userId, user, enabledModules = {} }) => {
name: data.user.name || '',
role: data.user.role || 'user',
email_verified: data.user.email_verified ? 'true' : 'false',
client_id: data.linkedClient ? String(data.linkedClient.id) : ''
}));
} else {
toast.error(data.message || 'Utilisateur introuvable');
@@ -107,7 +94,6 @@ const UserEditPage = ({ userId, user, enabledModules = {} }) => {
name: formData.name.trim(),
role: formData.role,
email_verified: formData.email_verified === 'true',
...(clientsModuleActive && { client_id: formData.client_id ? parseInt(formData.client_id, 10) : null })
})
});
const data = await response.json();
@@ -209,21 +195,6 @@ const UserEditPage = ({ userId, user, enabledModules = {} }) => {
onChange={(value) => handleInputChange('email_verified', value)}
options={emailVerifiedOptions}
/>
{clientsModuleActive && (
<Select
label="Client associé"
value={formData.client_id}
onChange={(value) => handleInputChange('client_id', value)}
options={[
{ value: '', label: 'Aucun' },
...clients.map(c => ({
value: String(c.id),
label: [c.client_number, c.company_name || [c.first_name, c.last_name].filter(Boolean).join(' ') || c.email].filter(Boolean).join(' ')
}))
]}
/>
)}
</div>
</div>
</Card>
+4 -14
View File
@@ -1,16 +1,6 @@
/**
* Admin Navigation Builder (Server-Only)
*
* This file imports from the module registry and should ONLY be used on the server.
* It builds the complete navigation including dynamic module navigation.
*
* IMPORTANT: This file is NOT bundled to ensure it shares the same registry instance
* that was populated during module discovery.
*
* IMPORTANT: We import from '@zen/core' (main package) to use the same registry
* instance that was populated during initializeZen(). DO NOT import from
* '@zen/core/core/modules' as that's a separate bundle with its own registry.
*
*
* IMPORTANT: Navigation data must be serializable (no functions/components).
* Icons are passed as string names and resolved on the client.
*/
@@ -18,7 +8,7 @@
/**
* Build complete navigation sections
* @param {string} pathname - Current pathname
* @returns {Array} Complete navigation sections (serializable, icons as strings)
* @returns {Array} Navigation sections (serializable, icons as strings)
*/
export function buildNavigationSections(pathname) {
const coreNavigation = [
@@ -36,7 +26,7 @@ export function buildNavigationSections(pathname) {
]
}
];
const systemNavigation = [
{
id: 'users',
@@ -52,6 +42,6 @@ export function buildNavigationSections(pathname) {
]
}
];
return [...coreNavigation, ...systemNavigation];
}
-37
View File
@@ -12,41 +12,6 @@ import { getDashboardStats } from '@zen/core/admin/actions';
import { logoutAction } from '@zen/core/auth/actions';
import { getAppName } from '@zen/core';
function parseAdminRoute(params) {
const parts = params?.admin || [];
if (parts.length === 0) {
return { path: '/admin/dashboard', action: null, id: null };
}
const corePages = ['dashboard', 'users', 'profile'];
if (corePages.includes(parts[0])) {
if (parts[0] === 'users' && parts[1] === 'edit' && parts[2]) {
return { path: '/admin/users', action: 'edit', id: parts[2] };
}
return { path: `/admin/${parts[0]}`, action: null, id: null };
}
let pathParts = [];
let action = null;
let id = null;
const actionKeywords = ['new', 'create', 'edit'];
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
if (actionKeywords.includes(part)) {
action = part === 'create' ? 'new' : part;
if (action === 'edit' && i + 1 < parts.length) {
id = parts[i + 1];
}
break;
}
pathParts.push(part);
}
return { path: '/admin/' + pathParts.join('/') + (action ? '/' + action : ''), action, id };
}
export default async function AdminPage({ params }) {
const resolvedParams = await params;
const session = await protectAdmin();
@@ -56,7 +21,6 @@ export default async function AdminPage({ params }) {
const dashboardStats = statsResult.success ? statsResult.stats : null;
const navigationSections = buildNavigationSections('/');
const { path, action, id } = parseAdminRoute(resolvedParams);
return (
<AdminPagesLayout
@@ -69,7 +33,6 @@ export default async function AdminPage({ params }) {
params={resolvedParams}
user={session.user}
dashboardStats={dashboardStats}
routeInfo={{ path, action, id }}
/>
</AdminPagesLayout>
);
+1 -24
View File
@@ -1,31 +1,8 @@
'use client';
import { useEffect, useRef } from 'react';
import { ToastProvider, ToastContainer } from '@zen/core/toast';
import { registerExternalModulePages } from '../../modules/modules.pages.js';
/**
* ZenProvider — root client provider for the ZEN CMS.
*
* Pass external module configs via the `modules` prop so their
* admin pages and public pages are available to the client router.
*
* @param {Object} props
* @param {Array} props.modules - External module configs from zen.config.js
* @param {ReactNode} props.children
*/
export function ZenProvider({ modules = [], children }) {
const registered = useRef(false);
if (!registered.current) {
// Register synchronously on first render so pages are available
// before any child component resolves a module route.
if (modules.length > 0) {
registerExternalModulePages(modules);
}
registered.current = true;
}
export function ZenProvider({ children }) {
return (
<ToastProvider>
{children}