Administration

Parcourir l'admin fourni, puis y ajouter une entrée de navigation, une page et un widget.

L'admin est fourni par features/admin et câblé par le projet généré. Cette page fait le tour de l'existant, puis y ajoute une section complète.

Ce qui existe déjà

RoutePage
/admin/dashboardTableau de bord et widgets.
/admin/usersListe, création et invitation d'utilisateurs.
/admin/rolesRôles et permissions.
/admin/profileProfil : informations, photo, sécurité, sessions, apparence.

Le câblage tient en trois fichiers d'une ligne chacun dans le projet, qui ré-exportent le core. Une mise à jour du package fait donc évoluer l'admin sans toucher au projet.

L'anatomie de l'écran

ZoneContenu
Sidebar, 230 pxLogo, navigation, items épinglés en bas.
Barre supérieure, 48 pxFil d'Ariane à gauche, utilisateur à droite.
ContenuPadding 28 à 32 px, fond gris très pâle.

La navigation et le fil d'Ariane sont calculés à partir du registre, pas écrits en dur. Enregistrer une entrée suffit à la faire apparaître aux deux endroits.

Ajouter une section

Créer un fichier d'extensions et l'importer une fois depuis le layout racine. Les enregistrements se font par effet de bord.

// app/lrvo.extensions.js
import './admin/orders/ordersWidget.server.js';
import './admin/orders/ordersWidget.client.js';
import {
  registerNavSection, registerNavItem, registerPage, registerPageMeta,
} from '@lrvo/core/features/admin';
import OrdersPage from './admin/orders/OrdersPage.js';

registerNavSection({ id: 'commerce', title: 'Commerce', icon: 'PackageIcon', order: 30 });

registerNavItem({
  id: 'orders',
  label: 'Commandes',
  icon: 'PackageIcon',
  href: '/admin/orders',
  sectionId: 'commerce',
  permission: 'orders.view',
});

registerPage({ slug: 'orders', Component: OrdersPage, title: 'Commandes' });
registerPageMeta({ slug: 'orders', title: 'Commandes', description: 'Suivi des commandes' });
// app/layout.js
import './lrvo.extensions';

La page

// app/admin/orders/OrdersPage.js
'use client';
import { AdminHeader } from '@lrvo/core/features/admin/components';
import { Button, Table } from '@lrvo/core/shared/components';

export default function OrdersPage() {
  return (
    <div className="flex flex-col gap-6">
      <AdminHeader
        title="Commandes"
        description="Commandes reçues."
        action={<Button href="/admin/orders/new">Créer une commande</Button>}
      />
      <Table columns={columns} rows={rows} />
    </div>
  );
}

AdminHeader donne à la page la même structure que les pages intégrées. Sur une page de formulaire, remplacer action par backHref.

Une colonne de Table sans render reçoit automatiquement le style monospace canonique : inutile de l'envelopper à la main.

Deux enregistrements, deux runtimes

registerPage s'exécute dans le bundle navigateur. Le generateMetadata de Next.js, lui, tourne côté serveur, avant : il ne verra jamais un titre déclaré côté client.

D'où registerPageMeta, appelée depuis un fichier neutre, sans 'use client'. C'est la même logique que la séparation des deux moitiés d'un widget.

Le widget

// app/admin/orders/ordersWidget.server.js
import { registerWidgetFetcher } from '@lrvo/core/features/admin';
import { countOrders } from './orders.server.js';

registerWidgetFetcher('orders', async () => ({ total: await countOrders() }));
// app/admin/orders/ordersWidget.client.js
'use client';
import { registerWidget } from '@lrvo/core/features/admin';
import { StatCard } from '@lrvo/core/shared/components';

function OrdersWidget({ data, loading }) {
  return (
    <StatCard
      title="Commandes"
      value={loading ? '-' : String(data?.total ?? 0)}
      loading={loading}
    />
  );
}

registerWidget({ id: 'orders', Component: OrdersWidget, order: 20, permission: 'orders.view' });

Le fetcher collecte côté serveur, le composant affiche côté client. Les deux partagent l'identifiant orders.

Si le fetcher échoue, data vaut null et loading reste false. Traiter ce cas, sinon le widget affiche un chargement qui ne finit jamais.

La permission

import { registerPermissions } from '@lrvo/core/users';

registerPermissions([
  { key: 'orders.view',   name: 'Voir les commandes',  description: 'Consultation', group_name: 'Commerce' },
  { key: 'orders.manage', name: 'Gérer les commandes', description: 'CRUD',         group_name: 'Commerce' },
]);
npx lrvo-db init

Les permissions apparaissent alors dans /admin/roles, groupées par group_name, et sont attribuées au rôle admin.

Masquer n'est pas protéger. Le champ permission masque une entrée de navigation ou un widget, mais l'API reste appelable directement. La permission doit être déclarée aux deux endroits : sur la route pour la sécurité, sur l'item pour l'ergonomie. Voir Permissions et rôles.

Item actif et fil d'Ariane

L'item actif est celui dont le basePath matche le préfixe le plus long du chemin courant. Le basePath est déduit du href ; le préciser seulement quand cette déduction ne suffit pas.

Sur une sous-route /new ou /edit/:id, le fil d'Ariane ajoute automatiquement « Nouveau » ou « Modification ».

Le devkit

LRVO_DEVKIT=true

Expose /admin/devkit/components et /admin/devkit/icons : galerie des composants partagés et catalogue d'icônes, dans les deux thèmes. Utile pour choisir un composant plutôt que d'en écrire un. Le même flag affiche la tuile Liens rapides du tableau de bord, qui mène à cette documentation. À laisser désactivé en production.

Étape suivante

Cette section vit dans le projet. Pour la réutiliser ailleurs, elle doit devenir un package : Premier module.

Sur cette page