Files

9.1 KiB

name, description
name description
create-module Scaffold a new Coltura module (backend + frontend) and optionally wire its entries into the sidebar config. Use when the user asks to create, add, scaffold, or generate a new module — e.g., "crée un module Paie", "add a Pointage module", "ajoute un module RH". The backend is the source of truth for activation and sidebar layout; the frontend scans modules automatically.

Create a new Coltura module

Scaffolds a new module across backend and frontend following Coltura's modular monolith DDD architecture.

Architecture reminder — read before acting

The module system has two concerns that are decoupled:

  1. Module = code + routes + pages. A module is a bounded context that owns feature code. Declared in config/modules.php (backend) and scanned automatically on frontend via nuxt.config.ts (any directory under frontend/modules/ becomes a Nuxt layer).
  2. Sidebar = navigation layout. Defined in config/sidebar.php. Each sidebar item references the module that owns it via the module key. Items whose module is not active are filtered out by the backend. You can freely move a sidebar item from one section to another without touching module code.

Consequences:

  • Frontend never hardcodes module metadata — no .module.ts, no modules-loader.ts, no manual registration.
  • Frontend never hardcodes the sidebar — it fetches GET /api/sidebar.
  • To add a new module in the sidebar: edit config/sidebar.php.
  • To move an item between sections: edit config/sidebar.php.
  • To disable a module: remove/comment it from config/modules.php. Its sidebar items are auto-filtered AND its routes land in disabledRoutes, which the frontend middleware modules.global.ts uses to redirect any direct URL access back to /.

The /api/sidebar response shape is:

{
    "sections": [{ "label": "...", "icon": "...", "items": [...] }],
    "disabledRoutes": ["/commercial", "/commercial/orders"]
}

Any item whose module key matches an inactive module is moved from sections into disabledRoutes automatically by SidebarProvider.

When to use

User wants to create a new vendable module. Example triggers:

  • "Crée un module Paie"
  • "Ajoute un module Pointage"
  • "Scaffold a new Stock module"

Prerequisites — gather before acting

Ask the user (via AskUserQuestion if not already provided):

  1. Module name (French, human readable, e.g., "Paie", "Gestion RH", "Pointage"). Derive:
    • PascalCase for backend folders/classes: Paie, GestionRh, Pointage
    • kebab-case for frontend folders/routes: paie, gestion-rh, pointage
    • camelCase for i18n keys: paie, gestionRh, pointage
    • snake_case for module ID: paie, gestion_rh, pointage
  2. Icon — an mdi icon identifier for the sidebar section (e.g., mdi:cash-multiple). If not given, suggest a relevant one.
  3. Should it appear in the sidebar immediately? If yes, ask which section (new one or existing) and what nav items (label + route).

Don't guess these — ask.

Placeholders used below

  • {Pascal} = PascalCase (e.g., Paie)
  • {kebab} = kebab-case (e.g., paie)
  • {camel} = camelCase i18n slug (e.g., paie, gestionRh)
  • {id} = module ID for backend + config (same as {camel} for single-word names, snake_case otherwise)
  • {label} = French display label (e.g., Paie)
  • {icon} = mdi icon string (e.g., mdi:cash-multiple)

Files to create/edit

Backend — module declaration

1. src/Module/{Pascal}/{Pascal}Module.php

<?php

declare(strict_types=1);

namespace App\Module\{Pascal};

final class {Pascal}Module
{
    public const string ID = '{id}';
    public const string LABEL = '{label}';
    public const bool REQUIRED = false;
}

2. Edit config/modules.php — append the module class:

return [
    \App\Module\Core\CoreModule::class,
    // ... existing modules
    \App\Module\{Pascal}\{Pascal}Module::class,
];

Backend — sidebar config (only if the user wants sidebar entries)

3. Edit config/sidebar.php — either add items to an existing section or append a new section:

[
    'label' => 'sidebar.{camel}.section',
    'icon'  => '{icon}',
    'items' => [
        [
            'label'  => 'sidebar.{camel}.overview',
            'to'     => '/{kebab}',
            'icon'   => '{icon}',
            'module' => '{id}',
        ],
        // more items from user input...
    ],
],

Every item must have a module key matching an active module ID — otherwise the backend will filter it out.

Frontend — module code (auto-detected)

4a. frontend/modules/{kebab}/nuxt.config.ts — required for Nuxt to treat the folder as a layer. Content:

export default defineNuxtConfig({})

4b. frontend/modules/{kebab}/pages/{kebab}.vue — placeholder page. The filename becomes the route /{kebab}.

<template>
    <div>
        <h1 class="text-xl font-bold text-primary-500 sm:text-2xl">{{ $t('{camel}.title') }}</h1>
        <p class="mt-4 text-neutral-500">{{ $t('{camel}.welcome') }}</p>
    </div>
</template>

<script setup lang="ts">
const { t } = useI18n()

useHead({ title: t('{camel}.title') })
</script>

Create one page file per nav item the user specified. Derive filenames from the to paths (e.g., /paie/bulletinspages/paie/bulletins.vue).

Do NOT create any of these — they no longer exist in this architecture:

  • frontend/modules/{kebab}/{kebab}.module.ts
  • frontend/plugins/modules-loader.ts
  • Any extends entry in nuxt.config.ts — modules are auto-detected from frontend/modules/*/.

Frontend — translations

5. Edit frontend/i18n/locales/fr.json — add the sidebar keys and page content keys:

{
    "sidebar": {
        "{camel}": {
            "section": "{label}",
            "overview": "Vue d'ensemble"
        }
    },
    "{camel}": {
        "title": "{label}",
        "welcome": "Module {label}"
    }
}

Every label in the new config/sidebar.php entries must have a matching key here, and every page's $t('{camel}.*') calls must match too.

Implementation steps

Execute in this exact order:

  1. Clarify inputs — use AskUserQuestion for name, icon, and sidebar decisions.
  2. Derive naming — confirm Pascal/kebab/camel/id are sensible (e.g., "Gestion RH" → GestionRh/gestion-rh/gestionRh/gestion_rh).
  3. Read current state — Read config/modules.php, config/sidebar.php, frontend/i18n/locales/fr.json so you know what exists before editing.
  4. Backend: declare the module — create {Pascal}Module.php, edit config/modules.php.
  5. Frontend: create pages — create the placeholder .vue files under frontend/modules/{kebab}/pages/.
  6. Backend: sidebar — if the user wants sidebar entries, edit config/sidebar.php.
  7. Frontend: translations — edit frontend/i18n/locales/fr.json.
  8. Verify — run:
    • docker exec -t -u root php-coltura-fpm chown -R www-data:www-data /var/www/html/var (avoid permission issues)
    • docker exec -t -u www-data php-coltura-fpm php bin/console cache:clear (validates backend)
    • cd frontend && npx nuxi prepare (validates Nuxt auto-detection of the new layer)
  9. Report — list files created, the route(s) to test, and the sidebar items added.

Rules — do not violate

  • Never create .module.ts files — the old ModuleDefinition / registerModule pattern is gone. Activation and sidebar are 100% backend-driven.
  • Never edit extends in nuxt.config.ts — it auto-scans frontend/modules/*/. Adding entries manually causes duplicates.
  • Never create frontend/plugins/modules-loader.ts — it was deleted on purpose.
  • All sidebar labels are i18n keys, not raw text — the layout calls t() on every label. Raw text will display as-is and look broken.
  • Every sidebar item needs a module key — if omitted, the backend filters it out silently.
  • Backend module must be in config/modules.php — otherwise /api/sidebar will hide its items even though the sidebar config references them.
  • No DDD scaffolding unless asked — only create the {Pascal}Module.php initially. Don't create empty Domain/, Application/, Infrastructure/ folders. Real domain code comes when the module gets features.
  • Don't create a page per nav item blindly — if the user didn't specify, create only the root /{kebab} page and ask whether they want stubs for subpages.
  • Don't use make cache-clear — it may hit permission issues. Use the docker commands directly (see Verify step).

Naming derivation examples

User input Pascal kebab camel id
Paie Paie paie paie paie
Pointage Pointage pointage pointage pointage
Gestion RH GestionRh gestion-rh gestionRh gestion_rh
Stock & Inventaire StockInventaire stock-inventaire stockInventaire stock_inventaire
CRM Crm crm crm crm

For accented characters (é, è, à, ç...): strip accents in folder/identifier names (ForêtForet/foret), keep them in the French label.