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:
@@ -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'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user