diff --git a/src/features/admin/actions/statsActions.js b/src/features/admin/actions/statsActions.js index 166d067..f6832a2 100644 --- a/src/features/admin/actions/statsActions.js +++ b/src/features/admin/actions/statsActions.js @@ -1,30 +1,9 @@ /** * Admin Stats Actions - * Server-side actions for core dashboard statistics * - * Usage in your Next.js app: - * - * ```javascript - * // app/(admin)/admin/[...admin]/page.js - * import { protectAdmin } from '@zen/core/features/admin'; - * import { getDashboardStats } from '@zen/core/features/admin/actions'; - * import { AdminPagesClient } from '@zen/core/features/admin/pages'; - * - * export default async function AdminPage({ params }) { - * const { user } = await protectAdmin(); - * - * const statsResult = await getDashboardStats(); - * const dashboardStats = statsResult.success ? statsResult.stats : null; - * - * return ( - * - * ); - * } - * ``` + * @deprecated Utiliser collectAllDashboardData() de serverRegistry à la place. + * Ce fichier est conservé pour la rétrocompatibilité avec les projets qui + * appellent getDashboardStats() directement depuis leur page admin. */ 'use server'; @@ -33,40 +12,16 @@ import { query } from '@zen/core/database'; import { fail } from '@zen/core/shared/logger'; /** - * Get total number of users - * @returns {Promise} - */ -async function getTotalUsersCount() { - try { - const result = await query( - `SELECT COUNT(*) as count FROM zen_auth_users` - ); - return parseInt(result.rows[0].count) || 0; - } catch (error) { - fail(`Error getting users count: ${error.message}`); - return 0; - } -} - -/** - * Get core dashboard statistics - * @returns {Promise} + * @deprecated Utiliser collectAllDashboardData() de serverRegistry à la place. + * @returns {Promise<{ success: boolean, stats?: { totalUsers: number }, error?: string }>} */ export async function getDashboardStats() { try { - const totalUsers = await getTotalUsersCount(); - - return { - success: true, - stats: { - totalUsers, - } - }; + const result = await query(`SELECT COUNT(*) as count FROM zen_auth_users`); + const totalUsers = parseInt(result.rows[0].count) || 0; + return { success: true, stats: { totalUsers } }; } 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' }; } } diff --git a/src/features/admin/components/pages/DashboardPage.js b/src/features/admin/components/pages/DashboardPage.js index 9c50ba7..5e9b1d9 100644 --- a/src/features/admin/components/pages/DashboardPage.js +++ b/src/features/admin/components/pages/DashboardPage.js @@ -1,10 +1,13 @@ 'use client'; -import { StatCard } from '@zen/core/shared/components'; -import { UserMultiple02Icon } from '@zen/core/shared/icons'; +import { getClientWidgets } from '../../dashboard/clientRegistry.js'; +import '../../../dashboard.client.js'; -export default function DashboardPage({ user, stats }) { - const loading = !stats; +// Évalué après tous les imports : les auto-registrations sont complètes +const sortedWidgets = getClientWidgets(); + +export default function DashboardPage({ stats }) { + const loading = stats === null || stats === undefined; return (
@@ -18,14 +21,13 @@ export default function DashboardPage({ user, stats }) {
- + {sortedWidgets.map(({ id, Component }) => ( + + ))}
); diff --git a/src/features/admin/dashboard/clientRegistry.js b/src/features/admin/dashboard/clientRegistry.js new file mode 100644 index 0000000..4b0f806 --- /dev/null +++ b/src/features/admin/dashboard/clientRegistry.js @@ -0,0 +1,30 @@ +'use client'; + +/** + * Dashboard Client Registry — API générique + * + * Les features s'enregistrent ici via registerClientWidget(). + * Ce fichier ne doit jamais importer une feature directement. + * Les features dépendent de ce fichier, pas l'inverse. + */ + +const widgets = []; + +/** + * Enregistre un composant React pour le tableau de bord. + * Appelé en side effect par chaque feature (ex: auth/dashboard.widget.js). + * @param {string} id - Identifiant unique de la feature (doit correspondre à l'id serveur) + * @param {React.ComponentType} Component - Composant avec props { data, loading } + * @param {number} order - Ordre d'affichage (croissant). Utiliser des intervalles de 10. + */ +export function registerClientWidget(id, Component, order) { + widgets.push({ id, Component, order }); +} + +/** + * Retourne tous les widgets enregistrés, triés par order croissant. + * @returns {Array<{ id: string, Component: React.ComponentType, order: number }>} + */ +export function getClientWidgets() { + return [...widgets].sort((a, b) => a.order - b.order); +} diff --git a/src/features/admin/dashboard/registry.js b/src/features/admin/dashboard/registry.js new file mode 100644 index 0000000..af41447 --- /dev/null +++ b/src/features/admin/dashboard/registry.js @@ -0,0 +1,39 @@ +/** + * Dashboard Server Registry — API générique + * + * Les features s'enregistrent ici via registerServerWidget(). + * Ce fichier ne doit jamais importer une feature directement. + * Les features dépendent de ce fichier, pas l'inverse. + */ + +const widgets = new Map(); // id → fetcher + +/** + * Enregistre une fonction de fetch pour le tableau de bord. + * Appelé en side effect par chaque feature (ex: auth/dashboard.server.js). + * @param {string} id - Identifiant unique de la feature + * @param {() => Promise} fetcher - Fonction async retournant des données sérialisables + */ +export function registerServerWidget(id, fetcher) { + widgets.set(id, fetcher); +} + +/** + * Exécute tous les fetchers enregistrés en parallèle. + * Un fetcher qui échoue n'empêche pas les autres (Promise.allSettled). + * @returns {Promise>} Map featureId → données + */ +export async function collectAllDashboardData() { + const results = await Promise.allSettled( + Array.from(widgets.entries()).map(([id, fetcher]) => + fetcher().then(data => ({ id, data })) + ) + ); + + return results.reduce((acc, result) => { + if (result.status === 'fulfilled') { + acc[result.value.id] = result.value.data; + } + return acc; + }, {}); +} diff --git a/src/features/admin/dashboard/serverRegistry.js b/src/features/admin/dashboard/serverRegistry.js new file mode 100644 index 0000000..136d9ed --- /dev/null +++ b/src/features/admin/dashboard/serverRegistry.js @@ -0,0 +1,15 @@ +/** + * Dashboard Server Registry — Point d'entrée + * + * Ré-exporte l'API du registre pur et déclenche le câblage des features + * via un import side-effect de features/dashboard.server.js. + * + * Ne jamais importer une feature directement ici. + * Pour ajouter une feature : éditer src/features/dashboard.server.js. + */ + +export { collectAllDashboardData } from './registry.js'; + +// Side effect : initialise le registre, puis déclenche toutes les auto-registrations +import './registry.js'; +import '../../dashboard.server.js'; diff --git a/src/features/admin/page.js b/src/features/admin/page.js index 107fe36..0238616 100644 --- a/src/features/admin/page.js +++ b/src/features/admin/page.js @@ -8,7 +8,7 @@ import { AdminPagesLayout, AdminPagesClient } from '@zen/core/features/admin/pages'; import { protectAdmin } from '@zen/core/features/admin'; import { buildNavigationSections } from '@zen/core/features/admin/navigation'; -import { getDashboardStats } from '@zen/core/features/admin/actions'; +import { collectAllDashboardData } from './dashboard/serverRegistry.js'; import { logoutAction } from '@zen/core/features/auth/actions'; import { getAppName } from '@zen/core'; @@ -17,8 +17,7 @@ export default async function AdminPage({ params }) { const session = await protectAdmin(); const appName = getAppName(); - const statsResult = await getDashboardStats(); - const dashboardStats = statsResult.success ? statsResult.stats : null; + const dashboardStats = await collectAllDashboardData(); const navigationSections = buildNavigationSections('/'); diff --git a/src/features/auth/dashboard.server.js b/src/features/auth/dashboard.server.js new file mode 100644 index 0000000..d033157 --- /dev/null +++ b/src/features/auth/dashboard.server.js @@ -0,0 +1,22 @@ +/** + * Auth Feature — Contribution serveur au tableau de bord + * + * Ce module s'auto-enregistre auprès du registre admin en side effect. + * Dépendance correcte : auth → admin (feature → core). + */ + +import { registerServerWidget } from '../admin/dashboard/registry.js'; +import { query } from '@zen/core/database'; +import { fail } from '@zen/core/shared/logger'; + +async function getDashboardData() { + try { + const result = await query(`SELECT COUNT(*) as count FROM zen_auth_users`); + return { totalUsers: parseInt(result.rows[0].count) || 0 }; + } catch (error) { + fail(`Auth dashboard data error: ${error.message}`); + return { totalUsers: 0 }; + } +} + +registerServerWidget('auth', getDashboardData); diff --git a/src/features/auth/dashboard.widget.js b/src/features/auth/dashboard.widget.js new file mode 100644 index 0000000..8a4043f --- /dev/null +++ b/src/features/auth/dashboard.widget.js @@ -0,0 +1,33 @@ +'use client'; + +/** + * Auth Feature — Widget client pour le tableau de bord + * + * Ce module s'auto-enregistre auprès du registre admin en side effect. + * Dépendance correcte : auth → admin (feature → core). + * + * Props du composant : + * data — { totalUsers: number } retourné par getDashboardData(), ou null + * loading — true tant que les données serveur ne sont pas disponibles + */ + +import { registerClientWidget } from '../admin/dashboard/clientRegistry.js'; +import { StatCard } from '@zen/core/shared/components'; +import { UserMultiple02Icon } from '@zen/core/shared/icons'; + +function AuthDashboardWidget({ data, loading }) { + return ( + + ); +} + +registerClientWidget('auth', AuthDashboardWidget, 10); + +export default AuthDashboardWidget; diff --git a/src/features/dashboard.client.js b/src/features/dashboard.client.js new file mode 100644 index 0000000..43b83ed --- /dev/null +++ b/src/features/dashboard.client.js @@ -0,0 +1,16 @@ +'use client'; + +/** + * Câblage client des widgets du tableau de bord + * + * Ce fichier est le SEUL à modifier pour ajouter le widget client + * d'une nouvelle feature au tableau de bord. + * + * L'import suffit : chaque module s'enregistre lui-même en side effect + * via registerClientWidget() de admin/dashboard/clientRegistry.js. + * + * Exemple pour une feature 'blog' : + * import './blog/dashboard.widget.js'; + */ + +import './auth/dashboard.widget.js'; diff --git a/src/features/dashboard.server.js b/src/features/dashboard.server.js new file mode 100644 index 0000000..157c381 --- /dev/null +++ b/src/features/dashboard.server.js @@ -0,0 +1,14 @@ +/** + * Câblage serveur des widgets du tableau de bord + * + * Ce fichier est le SEUL à modifier pour ajouter la contribution serveur + * d'une nouvelle feature au tableau de bord. + * + * L'import suffit : chaque module s'enregistre lui-même en side effect + * via registerServerWidget() de admin/dashboard/registry.js. + * + * Exemple pour une feature 'blog' : + * import './blog/dashboard.server.js'; + */ + +import './auth/dashboard.server.js'; diff --git a/tsup.config.js b/tsup.config.js index 6b91882..13d283a 100644 --- a/tsup.config.js +++ b/tsup.config.js @@ -58,6 +58,10 @@ export default defineConfig([ 'src/features/auth/page.js', 'src/features/admin/page.js', 'src/features/admin/navigation.server.js', + 'src/features/admin/dashboard/registry.js', + 'src/features/admin/dashboard/serverRegistry.js', + 'src/features/auth/dashboard.server.js', + 'src/features/dashboard.server.js', ], format: ['esm'], dts: false,