---
title: "Architecture"
weight: 60
---

# Architecture

Cette page décrit l'organisation des fichiers d'une application SmartMaker. Contrairement à un projet React ordinaire, **l'emplacement des fichiers y a des conséquences techniques** : il conditionne la capacité à extraire une fonctionnalité vers une autre application, et un mauvais découpage de la couche base de données provoque une erreur de chargement difficile à diagnostiquer.

## Vue d'ensemble

```
mobile/
├── public/
│   ├── images/              # icones PWA et images statiques
│   └── locales/
│       └── <lang>/
│           └── <feature>.json   # un fichier par langue ET par feature
├── src/
│   ├── api/
│   │   ├── index.js
│   │   └── mapping/
│   │       └── <feature>.js     # mapping backend <-> front, un par feature
│   ├── db/
│   │   ├── index.js             # instanciation de Db
│   │   └── stores/
│   │       └── <feature>/
│   │           ├── indexes.js   # schema Dexie, AUCUN import
│   │           ├── useDb<Feature>.jsx   # hook CRUD metier
│   │           └── index.js     # barrel
│   ├── components/
│   │   ├── app/                 # Router, Provider, Head, Toaster
│   │   ├── layouts/             # mises en page partagees
│   │   ├── global/              # composants transverses de l'application
│   │   └── pages/
│   │       ├── public/<Page>/   # pages non authentifiees
│   │       ├── private/<Page>/  # pages authentifiees
│   │       └── errors/<Page>/
│   ├── global-state/
│   │   └── slices/              # etat UI uniquement
│   ├── hooks/                   # hooks transverses de l'application
│   ├── i18n/
│   │   └── index.js
│   ├── utils/
│   │   ├── constants/
│   │   ├── functions/
│   │   └── maps/
│   │       ├── form.jsx         # type de champ -> composant de saisie
│   │       └── list.jsx         # type de colonne -> composant de cellule
│   ├── assets/                  # fichiers pris en charge par la compilation
│   ├── appConfig.js
│   ├── main.jsx                 # point d'entree
│   └── sw.js                    # Service Worker (mode injectManifest)
├── .env                         # variables d'environnement, non versionne
├── .env.example
├── index.html
├── eslint.config.js
├── package.json
└── vite.config.js
```

Une application générée par SmartBoot arrive avec cette arborescence déjà en place, avec un exemple par emplacement : un store `users` complet (`indexes.js` et `useDbUsers`), un `api/mapping/` commenté, les maps `form.jsx` et `list.jsx`, et cinq namespaces de traduction. Vous la remplissez, vous n'avez pas à la créer.

## Le principe directeur : une fonctionnalité, un dossier

Une fonctionnalité métier (interventions, tâches, devis, inventaire) doit pouvoir être **copiée telle quelle** vers une autre application SmartMaker. Concrètement, elle se répartit sur trois emplacements et trois seulement :

| Emplacement | Contenu |
| --- | --- |
| `db/stores/<feature>/` | schéma Dexie et hook CRUD |
| `api/mapping/<feature>.js` | conversion entre le format backend et le format front |
| `locales/<lang>/<feature>.json` | traductions du namespace de la fonctionnalité |

Le test est simple : copier ces trois éléments vers un autre projet, ajouter la route, et cela doit fonctionner sans autre modification. Si un import "fuit" vers un fichier propre à l'application d'origine, c'est un défaut à corriger.

## La couche données

### Tout le CRUD dans un hook

Chaque fonctionnalité expose un hook `useDb<Feature>` qui porte l'intégralité des accès à la base locale.

```
// src/db/stores/interventions/useDbInterventions.jsx
import { db } from "src/db";

export const useDbInterventions = () => {
  const list = async (filters) => { /* ... */ };
  const get = async (id) => { /* ... */ };
  const create = async (payload) => { /* ... */ };
  const update = async (id, payload) => { /* ... */ };
  const remove = async (id) => { /* ... */ };

  return { list, get, create, update, remove };
};
```

Le hook ne connaît que la classe `Db`, `useGlobalStates` et `useApi`. Rien d'autre du projet hôte.

> [!WARNING]
> Ne mettez jamais d'appel Dexie directement dans un composant de page. C'est l'anti-pattern le plus fréquent, et c'est celui qui rend une fonctionnalité impossible à extraire ensuite.

Le nom du hook suit la forme `useDb<Feature>` : `useDbInterventions`, `useDbTasks`, `useDbProducts`. Les formes `use<Feature>Services` que l'on trouve dans d'anciens projets sont du legacy à renommer.

### Le fichier indexes.js, et pourquoi il est obligatoire

> [!IMPORTANT]
> Le schéma Dexie de chaque fonctionnalité doit vivre dans un fichier `indexes.js` **sans aucun import**, et `db/index.js` doit l'importer directement, sans passer par le barrel.

Sans cette séparation, le graphe d'imports forme un cycle :

```
db/index.js
  -> import { tasksIndexes } from "./stores"        (barrel)
  -> stores/tasks/index.js
       export const tasksIndexes = "..."
       export * from "./useDbTasks"                 <- tire le hook
  -> useDbTasks.jsx
       import { db } from "src/db"                  <- retour au depart
```

À la compilation, ce cycle est partiellement remonté par Vite et produit une erreur `Cannot access 'tasksIndexes' before initialization` au chargement du bundle. Le symptôme est cruel : `npm run build` réussit, puisqu'il ne charge pas le bundle, et l'application plante au démarrage avec une erreur qui semble venir d'ailleurs (authentification cassée, boucle de connexion).

La parade, qui rompt le cycle en faisant de `indexes.js` une feuille du graphe :

```
// src/db/stores/tasks/indexes.js -- ZERO import
export const tasksIndexes = "++id, ref, status, updatedAt";
```

```
// src/db/index.js -- importe les FEUILLES, pas le barrel
import { tasksIndexes } from "./stores/tasks/indexes";
import { projectsIndexes } from "./stores/projects/indexes";

export const db = new Db({
  name: "myapp",
  version: 1,
  stores: { tasks: tasksIndexes, projects: projectsIndexes },
}).db;

export * from "./stores";   // acceptable ici : db est deja construit
```

Le barrel `stores/<feature>/index.js` reste utilisable normalement par les pages et les autres hooks. Seul `db/index.js` doit court-circuiter.

### Où va quelle donnée

| Nature | Emplacement |
| --- | --- |
| donnée métier persistée (interventions, devis, photos) | `Db` (Dexie), via `useDb<Feature>` |
| état d'interface persistant (thème, langue, filtres, dernière page) | `useGlobalStates` (Redux et redux-persist) |
| état d'interface éphémère (formulaire en cours, modale ouverte) | `useState` ou `useStates` local |

> [!WARNING]
> Ne placez jamais une liste d'objets métier dans un slice Redux. Elle doit vivre dans Dexie, et les composants la lisent via le hook de la fonctionnalité.

## Organiser les composants

C'est la question qui revient le plus souvent. La règle de classement tient en une phrase : **on range un composant selon sa portée, pas selon son sujet**.

| Dossier | Ce qu'on y met | Test d'appartenance |
| --- | --- | --- |
| `components/app/` | la plomberie de l'application : `Router`, composition des providers, `Head`, `Toaster` | il n'y en a qu'un seul exemplaire dans l'application |
| `components/layouts/` | mises en page partagées par plusieurs pages | il enveloppe des pages, il n'en est pas une |
| `components/global/` | composants transverses réutilisés par plusieurs pages | il est importé par au moins deux pages |
| `components/pages/<visibilité>/<Page>/` | une page, montée sur une route | il correspond à une entrée du `Router` |

Les pages se rangent ensuite par visibilité : `public/` pour ce qui est accessible sans authentification, `private/` pour le reste, `errors/` pour les pages d'erreur. Ce découpage n'est pas décoratif : il reflète ce que protège le `RouteGuard` et rend immédiatement visible qu'une page est exposée.

### Un composant, un dossier

Chaque composant a son propre dossier avec un `index.jsx`, même s'il est seul. Cela permet d'ajouter plus tard, au même endroit, ses styles, ses sous-composants et ses tests, sans rien déplacer ni corriger un seul import.

```
components/pages/private/InterventionPage/
├── index.jsx          # la page
├── Header/
│   └── index.jsx      # sous-composant propre a cette page
└── LinesTable/
    └── index.jsx
```

> [!TIP]
> Un sous-composant utilisé par une seule page reste **dans le dossier de cette page**. Il ne monte dans `components/global/` que le jour où une deuxième page l'utilise. Remonter par anticipation encombre l'espace commun de composants qui ne sont partagés par personne.

### Une page ne doit rien savoir de l'application

Une page métier doit pouvoir être montée dans le routeur d'une autre application sans modification.

À faire :
- recevoir ses dépendances par les hooks SmartCommon (`useNavigation`, `useApi`, `useGlobalStates`) ou par ses props
- passer les composants de formulaire par la map `utils/maps/form.jsx`, pour que l'application hôte puisse les surcharger par type de champ
- exposer les chemins de navigation dans une fonction, plutôt que d'écrire `navigate('/interventions/123')` en dur

À ne pas faire :
- importer `appConfig` depuis une page métier
- lire un slice Redux propre à l'application hôte

## Traductions : un namespace par fonctionnalité

```
public/locales/
├── fr/
│   ├── common.json
│   ├── interventions.json
│   └── products.json
└── en/
    ├── common.json
    ├── interventions.json
    └── products.json
```

```
const { t } = useTranslation('interventions');
```

Tous les namespaces doivent être déclarés dans la configuration i18next pour être préchargés : c'est indispensable au fonctionnement hors ligne, le precache du Service Worker les embarquant via son motif `**/*.json`.

> [!WARNING]
> Sans `Suspense`, ajoutez `react: { useSuspense: false }` à la configuration i18next. Sinon une page atteinte par navigation directe avant la fin du chargement de son namespace reste figée sur les clés brutes.

Le `keyPrefix` ne doit jamais être écrit en dur dans un hook réutilisable : il se reçoit en paramètre, avec une valeur par défaut.

```
// A eviter
const useIntStatuses = () => useTranslation(undefined, { keyPrefix: 'intStatuses' });

// Preferable
const useIntStatuses = (keyPrefix = 'intStatuses') => useTranslation(undefined, { keyPrefix });
```

## Configuration : aucune valeur en dur

Tout ce qui change d'un projet ou d'un environnement à l'autre passe par une variable d'environnement Vite ou par une prop de provider.

| Variable | Rôle |
| --- | --- |
| `VITE_API_URL` | URL du backend ; forcée en relatif au build de production |
| `VITE_APP_NAME` | nom de l'application |
| `VITE_APP_VERSION` | version et numéro de build, injectés par le Makefile |
| `VITE_LOCALES` | liste des langues, dérivée des sous-dossiers de `public/locales/` |

`appConfig.js` peut centraliser ces valeurs, mais ne doit être importé que depuis la coquille de l'application, jamais depuis une fonctionnalité métier.

## Les fichiers à la racine de mobile/

| Fichier | Rôle |
| --- | --- |
| `.env` | variables d'environnement, ignoré par Git |
| `.env.example` | modèle du `.env`, versionné |
| `index.html` | page dans laquelle React monte l'application ; porte le lien vers le manifest |
| `vite.config.js` | configuration de Vite, y compris celle de la PWA |
| `eslint.config.js` | configuration du lint |
| `package.json` | dépendances et scripts |

## Checklist de relecture

À passer avant d'ouvrir une demande de fusion, ou en reprenant un projet existant :
- [ ] tout le CRUD métier est dans `useDb<Feature>`, aucun appel Dexie dans une page
- [ ] chaque `db/stores/<feature>/` a son `indexes.js` sans import, et `db/index.js` importe les feuilles
- [ ] le mapping backend/front est isolé dans `api/mapping/<feature>.js`
- [ ] aucune donnée métier dans un slice Redux
- [ ] aucune page métier n'importe `appConfig` ni un slice propre à l'application
- [ ] un namespace i18n par fonctionnalité, tous déclarés dans la configuration
- [ ] aucun `fetch` ni `axios` : tout passe par `useApi`
- [ ] aucune URL ni aucun port en dur
- [ ] un composant, un dossier ; les sous-composants restent chez leur page tant qu'ils ne servent qu'à elle

## Voir aussi
- [Composants et pages](/front/composants-et-pages)
- [Stockage de données](/front/stockage-de-donnees)
- [Synchronisation offline](/front/synchronisation)
- [Traductions](/front/traductions)
- [PWA](/front/pwa) - Service Worker et démarrage hors ligne
- [Formation - Bonnes pratiques](/training/module11-bonnes-pratiques)
