# Dashboard Vertical Module Guide

This guide defines the implementation standard for dashboard modules migrated under
`src/modules/dashboard`. It complements the project-wide `AGENTS.md`; it does not change
route, validation, security, or response conventions.

`src/modules/dashboard/<module>` is a **transitional interface boundary**, not the final
owner of business models or cross-surface domain logic. The target architecture is
`src/modules/<domain>/domain` plus `src/modules/<domain>/interfaces/dashboard`, as
defined in `FINAL_PROJECT_ARCHITECTURE_BLUEPRINT.md`. Existing dashboard modules remain
stable until their domain-first migration wave is protected by compatibility exports.

## 1. Folder contract

```text
src/modules/dashboard/<module>/
  index.js
  <module>.controller.js
  <module>.service.js
  <module>.repository.js
  <module>.viewModel.js
  <module>.constants.js
  <module>.policy.js
  <module>.query.js       # optional
  <module>.validation.js  # optional for read-only modules
  <module>.uploads.js     # optional when upload rules are module-specific
  <module>.moderation.js  # optional high-risk module-owned workflow/query engine
```

The folder is the public maintenance boundary. Consumers import its `index.js`; they do
not reach into private files from another module.

Dashboard-specific media, moderation, or workflow engines belong to the owning module.
They must not remain in generic horizontal helper folders merely because they predate the
vertical slice. A high-risk engine may be preserved behavior-for-behavior behind a
repository boundary instead of being rewritten during the ownership move.

### Final domain-first destination

When a domain is ready to own all surfaces, its dashboard adapter moves behind:

```text
src/modules/<domain>/
  domain/
    <domain>.model.js
    <domain>.repository.js
    <domain>.service.js
    <domain>.policy.js
    <domain>.constants.js
  interfaces/dashboard/
    <domain>.dashboard.controller.js
    <domain>.dashboard.routes.js
    <domain>.dashboard.validation.js
    <domain>.viewModel.js
    <domain>.uploads.js
  index.js
```

During that move, `src/modules/dashboard/<module>/index.js` remains a compatibility
export until every route, test, and operational consumer uses the domain public index.
EJS and public asset URLs stay in their current locations.

## 2. Layer responsibilities

### Controller

- Owns Express `req`/`res`, validated request extraction, CSRF tokens, redirects,
  render/send calls, and dashboard audit reporting.
- Delegates orchestration to its service and render shaping to its view model.
- Never imports Mongoose models, pagination/query builders, or another module's private
  repository.
- Preserves the existing public handler names used by `adminRoute.js`.

### Service

- Receives plain inputs rather than Express objects.
- Owns use-case ordering, transaction/file lifecycle coordination, and business policy
  decisions.
- Calls only its repository for persistence and its policy/query helpers for rules.
- Never renders EJS, sends HTTP responses, or imports the Express router.

### Repository

- Is the module's only normal owner of Mongoose model access.
- Encapsulates filters, projections, population, write options, and reusable database
  operations.
- Never imports controllers, services, view models, EJS, or HTTP concerns.

### View model

- Produces the exact safe locals consumed by EJS.
- Maps records to presentation flags, status chips, labels, action availability, and
  display fallbacks.
- Never imports models or performs a database read/write.

### Constants, policies, and queries

- Constants wrap existing enums, route/menu identifiers, result codes, and audit keys.
- Policies own reusable status/type/role decisions and action availability.
- Query builders own non-trivial filters and keep soft-delete/status rules out of
  controllers and templates.
- Do not duplicate an enum locally when an authoritative enum already exists.

## 3. Presentation rule

EJS renders decisions; it does not make domain decisions. A template may use:

```ejs
<% if (item.presentation.canDelete) { %>
```

It must not contain:

```ejs
<% if (item.status === 'active') { %>
```

Locale selection, input mechanics, selected-option comparison against values provided by
the view model, and CSS class composition are presentation mechanics rather than domain
policy. Domain status/type/role mapping still belongs in the module policy/view model.

## 4. Import rules

Allowed dependency direction:

```text
adminRoute -> module index -> controller -> service -> repository
                              controller -> viewModel -> policy/constants
                                           service -> policy/query/constants
```

Forbidden:

- repository -> service/controller/viewModel;
- viewModel -> model/repository;
- service -> Express router/application;
- EJS -> server module code;
- cross-module imports of private controller/service/repository files.

Code genuinely reused by three or more modules may live in
`src/modules/dashboard/shared`, but that directory must stay small and domain-neutral.

## 5. Route strategy

`adminRoute.js` remains the central route aggregator until extracting registrations can
be proven not to change ordering. It imports migrated module indices, then binds the same
controller and validation exports.

Every migration must preserve:

- URL, HTTP method, regex/filter ordering, and handler name;
- authentication/authorization order;
- upload-before-CSRF ordering for multipart mutations;
- validators, CSRF, response envelope, redirect target, and EJS locals.

## 6. Adding or migrating a module

1. Inventory current routes, handlers, validation, models, uploads, render locals, and
   tests.
2. Freeze route and middleware order with a contract test.
3. Create the vertical folder and move behavior in small slices.
4. Move model queries to the repository.
5. Move orchestration to the service.
6. Move status/action/display decisions to policy and view model.
7. Export the stable surface through `index.js`.
8. Point `adminRoute.js` to the module index without changing the chain.
9. Remove the old horizontal implementation only after focused tests pass.
10. Run the complete acceptance gate below.

## 7. Required automated tests

- folder/layer existence and module-index exports;
- controller -> service/viewModel and service -> repository direction;
- repository reverse-dependency prohibition;
- view-model model/mutation prohibition;
- vertical `adminRoute.js` imports and stable route/middleware order;
- old horizontal implementation absence;
- constants/policy use and no raw domain comparisons in controller/service/viewModel;
- EJS presentation-boundary checks;
- focused module render, validation, CSRF, and mutation-contract tests;
- full `npm test`, changed-JavaScript `node --check`, and `git diff --check`.

## 8. Browser QA

For require-path-only moves, run authenticated smoke QA for accessible list/create/edit/
show pages. If routes or render contracts change, run the full module matrix including
empty/populated states, delete confirmation, loading, validation, desktop light/dark,
tablet, and mobile.

The acceptance metrics are zero console errors, failed requests, HTTP errors, broken
images, horizontal overflow, clipped text, dark-theme leaks, clipped modals, native
dialogs, and unexpected layout shifts. Store evidence only under the ignored
`public/admin/ui-lab/screenshots/` tree.

## 9. Safety boundary

A dashboard architecture migration does not authorize API/Auth/schema changes, package
installation, seeders/migrations, global RBAC activation, broad UI redesign, or invented
form fields. Risky or undocumented behavior remains explicitly partial instead of being
guessed.
