@movalib/movalib-commons 1.68.32 → 1.68.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/FEATURE_minimize-rdv-dialog.md +74 -0
- package/.claude/MOVA-393-export-fec.md +392 -0
- package/.claude/POSTAL_CODE_AUTOCOMPLETE.md +78 -0
- package/.claude/PROMPT_fix-404-navigation.md +237 -0
- package/.claude/PROMPT_version-refresh-cleanup.md +157 -0
- package/.claude/REPAIR_ORDER_FEATURE.md +416 -0
- package/.claude/SESSION_INVENTORY_MODULE.md +75 -0
- package/.claude/TICKET_PERF_MY_SERVICES.md +67 -0
- package/.claude/feature-forfaits-creation.md +263 -0
- package/.claude/recaptcha-enterprise-setup.md +159 -0
- package/.claude/settings.json +8 -0
- package/.claude/settings.local.json +12 -0
- package/dist/index.d.ts +1 -0
- package/dist/src/models/Customer.d.ts +2 -0
- package/dist/src/models/EmailVerification.d.ts +13 -0
- package/dist/src/models/EmailVerification.js +2 -0
- package/index.ts +1 -0
- package/package.json +1 -1
- package/src/models/Customer.ts +3 -1
- package/src/models/EmailVerification.ts +13 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Feature : Minimiser la dialog de création de RDV (MyCalendarEventDialog)
|
|
2
|
+
|
|
3
|
+
## Contexte
|
|
4
|
+
|
|
5
|
+
L'application movalib-pro (React/TypeScript + MUI) possède une grosse dialog de création/édition de rendez-vous : `MyCalendarEventDialog`. Actuellement, si l'utilisateur ferme la dialog en cours de saisie, tout est perdu.
|
|
6
|
+
|
|
7
|
+
## Objectif
|
|
8
|
+
|
|
9
|
+
Permettre de **minimiser** la dialog de création de RDV (façon Gmail compose / fenêtres Windows) pour la reprendre plus tard, sans perdre la saisie en cours. Les dialogs minimisées apparaissent sous forme de chips/onglets dans une barre fixée en bas à droite de l'écran.
|
|
10
|
+
|
|
11
|
+
## Règles métier
|
|
12
|
+
|
|
13
|
+
1. **Uniquement en mode CRÉATION.** `MyCalendarEventDialog` peut aussi s'ouvrir en mode édition : dans ce cas, PAS de bouton minimiser, comportement actuel inchangé.
|
|
14
|
+
2. **Maximum 3 brouillons minimisés simultanément.** Si l'utilisateur tente d'ouvrir une 4e création alors que 3 brouillons existent déjà : bloquer l'ouverture et afficher un snackbar d'information (ex: "Vous avez atteint la limite de 3 brouillons de rendez-vous. Fermez ou finalisez un brouillon existant."). Utiliser le système de snackbar existant du projet.
|
|
15
|
+
3. **Une seule dialog visible à la fois.** Restaurer un brouillon alors qu'une dialog est ouverte → minimiser automatiquement la dialog courante (si en mode création) avant de restaurer l'autre. Si la dialog courante est en mode édition, bloquer avec snackbar.
|
|
16
|
+
4. **Label du chip minimisé** : nom du client si saisi, sinon plaque d'immatriculation si saisie, sinon "Brouillon RDV". Mettre à jour le label au moment de la minimisation (pas besoin de temps réel).
|
|
17
|
+
5. **Persistance au refresh (F5)** : les brouillons doivent survivre à un rechargement de page via `localStorage` (voir section Persistance).
|
|
18
|
+
6. La **fermeture définitive** (croix / annuler / création réussie du RDV) supprime le brouillon (state + localStorage).
|
|
19
|
+
|
|
20
|
+
## Architecture attendue
|
|
21
|
+
|
|
22
|
+
### Stratégie : composant gardé monté + snapshot localStorage
|
|
23
|
+
|
|
24
|
+
- **En session** : quand une dialog est minimisée, NE PAS démonter le composant. Le cacher visuellement (wrapper avec `display: none`, ou garder la Dialog MUI avec `open=false` selon ce qui préserve le mieux le state — vérifier que MUI ne démonte pas le contenu : utiliser `keepMounted` sur la Dialog si nécessaire). Le state interne (Formik, refs, etc.) est ainsi préservé sans sérialisation.
|
|
25
|
+
- **Pour le refresh** : en complément, sauvegarder un snapshot sérialisable des valeurs du formulaire dans `localStorage` à chaque minimisation (et idéalement de façon debounced pendant la saisie si simple à faire, sinon uniquement à la minimisation).
|
|
26
|
+
|
|
27
|
+
### Composants / fichiers à créer
|
|
28
|
+
|
|
29
|
+
1. **`MinimizedEventDialogsContext`** (Context + Provider + hook `useMinimizedEventDialogs`)
|
|
30
|
+
- State : liste des brouillons `{ id: string, label: string, createdAt: number }`, `activeDraftId: string | null`
|
|
31
|
+
- Actions : `openNewDraft()` (retourne null + snackbar si limite atteinte), `minimizeDraft(id, label, formSnapshot)`, `restoreDraft(id)`, `closeDraft(id)`
|
|
32
|
+
- Gère la synchronisation avec localStorage.
|
|
33
|
+
|
|
34
|
+
2. **`MinimizedDialogsBar`** — barre fixe en bas à droite (`position: fixed; bottom; right; zIndex` au-dessus du contenu mais sous les dialogs/snackbars MUI). Affiche un chip MUI par brouillon :
|
|
35
|
+
- Clic sur le chip → restaure le brouillon
|
|
36
|
+
- Petite croix sur le chip (`onDelete` du Chip MUI) → fermeture définitive du brouillon, avec dialog de confirmation ("Supprimer ce brouillon ?") via le système de confirmation existant du projet
|
|
37
|
+
- La barre n'est pas rendue si aucun brouillon.
|
|
38
|
+
|
|
39
|
+
3. **Intégration dans `MyCalendarEventDialog`** :
|
|
40
|
+
- Ajouter un **IconButton "Réduire"** (icône `Minimize` ou `Remove` de @mui/icons-material) dans la **toolbar existante de la dialog, juste à côté du bouton close**, uniquement en mode création. Respecter le style existant de la toolbar (taille, couleur des icons).
|
|
41
|
+
- Au clic : calculer le label (client > plaque > "Brouillon RDV"), snapshot des valeurs du formulaire, appeler `minimizeDraft`.
|
|
42
|
+
- Chaque instance de dialog en création reçoit un `draftId` (uuid généré à l'ouverture).
|
|
43
|
+
|
|
44
|
+
4. **Rendu des instances** : le composant parent qui monte `MyCalendarEventDialog` (probablement le composant calendrier) doit pouvoir rendre jusqu'à 3 instances simultanées (une par brouillon), chacune cachée ou visible selon `activeDraftId`. Adapter le point de montage actuel en conséquence (ex: mapper sur la liste des brouillons du context). Analyser d'abord comment la dialog est actuellement ouverte (state local ? context existant type DocumentEditorContext ?) et s'intégrer proprement dans le pattern existant — modification chirurgicale, pas de refactoring massif.
|
|
45
|
+
|
|
46
|
+
### Persistance localStorage
|
|
47
|
+
|
|
48
|
+
- Clé : `movalib.rdvDrafts` (ou préfixe cohérent avec les conventions existantes du projet — vérifier s'il y a déjà des clés localStorage et suivre le même pattern).
|
|
49
|
+
- Contenu : `[{ id, label, createdAt, formValues }]` où `formValues` est un snapshot JSON-sérialisable des champs du formulaire.
|
|
50
|
+
- **Attention aux types non sérialisables** : dates (stocker en ISO string, reconvertir au restore), fichiers/pièces jointes (NE PAS persister — accepter la perte au refresh, éventuellement le signaler), instances de classes (class-transformer : utiliser `instanceToPlain`/`plainToInstance` si le form utilise des modèles de classe).
|
|
51
|
+
- Au chargement de l'app : lire localStorage, recréer les entrées de brouillons dans le context (dialogs montées cachées, formulaire réhydraté avec les snapshots).
|
|
52
|
+
- Prévoir une expiration simple : purger les brouillons de plus de 7 jours au chargement.
|
|
53
|
+
- Versionner le schéma (`{ version: 1, drafts: [...] }`) : si le schéma ne matche pas, purger silencieusement (pas de crash).
|
|
54
|
+
|
|
55
|
+
## Étapes suggérées
|
|
56
|
+
|
|
57
|
+
1. **Explorer** : localiser `MyCalendarEventDialog`, comprendre comment elle est ouverte (props, state parent, mode création vs édition), identifier le système de formulaire (Formik ?), le système snackbar et le système de dialog de confirmation existants.
|
|
58
|
+
2. Créer le context + provider, brancher au niveau racine approprié (au même niveau que les providers existants).
|
|
59
|
+
3. Créer `MinimizedDialogsBar`.
|
|
60
|
+
4. Adapter le montage de `MyCalendarEventDialog` pour supporter les instances multiples cachées + le bouton Réduire.
|
|
61
|
+
5. Ajouter la persistance localStorage (snapshot + réhydratation).
|
|
62
|
+
6. Vérifier : build TypeScript OK, pas de régression sur le mode édition, la limite de 3 fonctionne, le F5 restaure bien.
|
|
63
|
+
|
|
64
|
+
## Contraintes
|
|
65
|
+
|
|
66
|
+
- Respecter les conventions existantes du projet (MUI theming, structure des dossiers, i18n si les libellés passent par un système de traduction — vérifier).
|
|
67
|
+
- Modifications minimales et chirurgicales sur `MyCalendarEventDialog` : la logique de minimisation doit vivre au maximum dans le context et le wrapper, pas dans le corps de la dialog.
|
|
68
|
+
- Aucun changement de comportement pour le mode édition.
|
|
69
|
+
- Tous les libellés en français.
|
|
70
|
+
|
|
71
|
+
## Questions à poser si ambigu
|
|
72
|
+
|
|
73
|
+
- Si le formulaire n'utilise pas Formik ou si le state est éclaté dans plusieurs sous-composants, proposer la stratégie de snapshot avant d'implémenter.
|
|
74
|
+
- Si `MyCalendarEventDialog` est montée via un portal/context existant, montrer le plan d'intégration avant de modifier.
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
# MOVA-393 — Export FEC incrémentiel (écritures comptables)
|
|
2
|
+
|
|
3
|
+
## But de la tâche
|
|
4
|
+
|
|
5
|
+
Implémenter, dans **movalib-pro**, un écran de **gestion des exports comptables (FEC)**
|
|
6
|
+
accessible depuis le module **Facturation**. L'utilisateur doit pouvoir :
|
|
7
|
+
|
|
8
|
+
- voir le **nombre d'écritures en attente d'export** (WIP) ;
|
|
9
|
+
- **générer un export incrémentiel** à tout moment (seules les écritures non encore consommées) ;
|
|
10
|
+
- **suivre l'avancement** de la génération (et voir un échec) ;
|
|
11
|
+
- **télécharger** les exports disponibles ;
|
|
12
|
+
- **identifier** les exports déjà téléchargés ;
|
|
13
|
+
- **annuler** le dernier export généré s'il n'a pas encore été téléchargé ;
|
|
14
|
+
- **consulter tout l'historique**, avec pagination (potentiellement volumineux).
|
|
15
|
+
|
|
16
|
+
> ⚠️ **La maquette HTML fournie n'est pas à reprendre telle quelle.** Elle sert
|
|
17
|
+
> **uniquement de référence pour l'ordre et la présence des blocs**. Le rendu doit
|
|
18
|
+
> respecter la **DA (design system) réelle de movalib-pro** : MUI v6, thème existant
|
|
19
|
+
> (`src/theme`), composants et patterns déjà en place. Aucune couleur / police / valeur
|
|
20
|
+
> en dur issue de la maquette. Voir la section « Correspondance maquette → DA ».
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Emplacements des fichiers à créer
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
src/pages/billing/AccountingExports.tsx # page principale (l'écran)
|
|
28
|
+
src/pages/billing/components/ # sous-composants de l'écran (voir plus bas)
|
|
29
|
+
src/models/Billing/AccountingExport.ts # modèles class-transformer
|
|
30
|
+
src/services/Billing/AccountingExportService.ts # service + hooks React Query
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Créer les dossiers `src/models/Billing` et `src/services/Billing` s'ils n'existent pas.
|
|
34
|
+
Router : ajouter la route de la page dans `src/App.tsx` (suivre exactement le pattern des
|
|
35
|
+
autres pages `billing/*` déjà importées là — ex. `BankTransactions`, `Stocks`).
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Contraintes projet (rappel — ne pas dévier)
|
|
40
|
+
|
|
41
|
+
- **Composants fonctionnels uniquement**, hooks. Pas de class component React.
|
|
42
|
+
- **Data fetching = React Query v5** (`useQuery` / `useInfiniteQuery` / `useMutation`).
|
|
43
|
+
Jamais `useEffect` + fetch manuel.
|
|
44
|
+
- **Modèles = class-transformer** (`@Expose()`, `@Type()`, `plainToInstance(..., { excludeExtraneousValues: true })`),
|
|
45
|
+
méthodes statiques `JsonToObject` / `JsonToObjects`. Reproduire à l'identique le pattern
|
|
46
|
+
de `src/models/SupplierOrder.ts`.
|
|
47
|
+
- **Appels API via `request`** importé de `@movalib/movalib-commons` (avec `APIMethod`,
|
|
48
|
+
`APIResponse`), `API_BASE_URL` depuis `src/helpers/Constants`. Suivre `SupplierOrderService`
|
|
49
|
+
/ `DataBoxService` comme référence.
|
|
50
|
+
- **Styles = MUI `sx` prop ou `styled()`**. Aucun CSS inline, aucun fichier `.css`, aucune
|
|
51
|
+
variable CSS de la maquette.
|
|
52
|
+
- **Dates = date-fns v2** (`format`, locale `fr`). Pas de moment (sauf code existant).
|
|
53
|
+
- **Feedback utilisateur = `useSnackbar` / `showSnackbar(message, severity)`** (pattern existant).
|
|
54
|
+
- **Chips de statut** : suivre le pattern `QuoteStatusChip` (variantes palette `light` / `dark`).
|
|
55
|
+
- Pas de `any` non justifié, pas de `console.log`, pas de props drilling > 2 niveaux.
|
|
56
|
+
- Ne pas réécrire de fichiers hors périmètre ; proposer les diffs pour `App.tsx`.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## API backend
|
|
61
|
+
|
|
62
|
+
Base : `${API_BASE_URL}/accounting/garage/{garageId}/...`
|
|
63
|
+
`garageId` provient du garage courant (même mécanisme que les autres pages du module —
|
|
64
|
+
récupérer via le contexte / `useFetchAdministratedGarages` comme ailleurs, ne pas hardcoder `20`).
|
|
65
|
+
|
|
66
|
+
| Action | Méthode & URL |
|
|
67
|
+
|---|---|
|
|
68
|
+
| Liste paginée des exports | `GET /exports?page={p}&size={s}` |
|
|
69
|
+
| Écritures en attente (WIP) | `GET /exports/pending` *(à créer côté API — voir note)* |
|
|
70
|
+
| Générer un export | `POST /exports?until={yyyy-MM-dd}` |
|
|
71
|
+
| État d'un export | `GET /exports/{exportId}` |
|
|
72
|
+
| Annuler le dernier export non téléchargé | `DELETE /exports/{exportId}` |
|
|
73
|
+
| Déclencher un téléchargement | `POST /exports/{exportId}/download` |
|
|
74
|
+
| État d'un téléchargement | `GET /sinao-downloads/{downloadId}` |
|
|
75
|
+
|
|
76
|
+
> **Note pagination / pending :** aujourd'hui `GET /exports` **et** `GET /exports/pending`
|
|
77
|
+
> ne sont pas encore livrés côté back sous cette forme. Deux formes de réponse ont été
|
|
78
|
+
> observées dans les échantillons (voir plus bas) :
|
|
79
|
+
> 1. une réponse `Page<Export>` Spring « nue » (`{ content, pageable, totalPages, ... }`) ;
|
|
80
|
+
> 2. une réponse enveloppée `{ exports: Page<Export>, pendingEntriesCount: number }`.
|
|
81
|
+
>
|
|
82
|
+
> **Implémenter le service de façon défensive** : parser `pendingEntriesCount` depuis la
|
|
83
|
+
> réponse liste **si présente**, sinon consommer `GET /exports/pending`. Isoler ce choix
|
|
84
|
+
> dans le service pour qu'un seul endroit change quand l'API sera figée. Documenter par un
|
|
85
|
+
> commentaire l'hypothèse retenue.
|
|
86
|
+
|
|
87
|
+
### Formes de réponse (échantillons réels)
|
|
88
|
+
|
|
89
|
+
**Génération — `POST /exports?until=...`** (statut initial `generating`) :
|
|
90
|
+
```json
|
|
91
|
+
{ "exportId": 250, "periodStart": "2026-05-01", "periodEnd": "2026-05-16", "status": "generating" }
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**État d'un export — `GET /exports/{id}`** :
|
|
95
|
+
```json
|
|
96
|
+
{
|
|
97
|
+
"id": 250,
|
|
98
|
+
"periodStart": "2026-05-01",
|
|
99
|
+
"periodEnd": "2026-05-16",
|
|
100
|
+
"status": "generated",
|
|
101
|
+
"sinaoEntriesCount": 23,
|
|
102
|
+
"sinaoZipUrl": null,
|
|
103
|
+
"downloadedAt": null,
|
|
104
|
+
"transmissions": [],
|
|
105
|
+
"canTransmit": ["pennylane"],
|
|
106
|
+
"createdAt": "2026-09-02T16:20:30.741872Z"
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Liste — `GET /exports` (forme 1, Page nue)** :
|
|
111
|
+
```json
|
|
112
|
+
{
|
|
113
|
+
"content": [ { "id": 250, "periodStart": "...", "periodEnd": "...", "status": "generated",
|
|
114
|
+
"sinaoEntriesCount": 23, "sinaoZipUrl": "https://...", "downloadedAt": "2026-09-02T16:21:54Z",
|
|
115
|
+
"transmissions": [], "canTransmit": ["pennylane"], "createdAt": "..." } ],
|
|
116
|
+
"totalElements": 1, "totalPages": 1, "number": 0, "size": 12, "first": true, "last": true, "empty": false
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**Liste — `GET /exports` (forme 2, enveloppée + WIP)** :
|
|
121
|
+
```json
|
|
122
|
+
{ "exports": { "content": [ ...Export ], "totalPages": 1, "totalElements": 2, "number": 0, "size": 12, ... },
|
|
123
|
+
"pendingEntriesCount": 20 }
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**État d'un téléchargement — `GET /sinao-downloads/{id}`** (⚠️ snake_case ici) :
|
|
127
|
+
```json
|
|
128
|
+
{
|
|
129
|
+
"id": 80,
|
|
130
|
+
"status": "finished",
|
|
131
|
+
"entries_count": 9,
|
|
132
|
+
"file_url": "https://sinao-prod-storage.s3.eu-west-3.amazonaws.com/.../80.zip?...",
|
|
133
|
+
"period_start": "2026-05-14T22:00:00.000000Z",
|
|
134
|
+
"period_end": "2026-05-26T22:00:00.000000Z",
|
|
135
|
+
"error_message": null,
|
|
136
|
+
"info_message": null
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
> ⚠️ Incohérence de casing entre endpoints : `/exports/*` renvoie du **camelCase**,
|
|
141
|
+
> `/sinao-downloads/*` du **snake_case**. Gérer via les `@Expose({ name: ... })` dans deux
|
|
142
|
+
> classes distinctes.
|
|
143
|
+
|
|
144
|
+
### Statuts (`Export.status`)
|
|
145
|
+
|
|
146
|
+
Valeurs observées : `generating`, `generated`. Prévoir aussi un statut d'échec
|
|
147
|
+
(`failed` / `error` — à confirmer côté back ; mapper de façon tolérante et logguer un warn
|
|
148
|
+
si valeur inconnue plutôt que crash). Modéliser en `enum` :
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
export enum AccountingExportStatus {
|
|
152
|
+
GENERATING = "generating",
|
|
153
|
+
GENERATED = "generated",
|
|
154
|
+
FAILED = "failed", // à confirmer avec le back
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
État de téléchargement (`SinaoDownload.status`) : `pending` / `processing` / `finished` /
|
|
159
|
+
`failed` (observé : `finished`). Même prudence.
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Modèles à créer — `src/models/Billing/AccountingExport.ts`
|
|
164
|
+
|
|
165
|
+
Suivre **exactement** le pattern class-transformer de `SupplierOrder`.
|
|
166
|
+
|
|
167
|
+
- `AccountingExport`
|
|
168
|
+
- `@Expose() id!: number`
|
|
169
|
+
- `@Expose() periodStart!: string` / `@Expose() periodEnd!: string`
|
|
170
|
+
- `@Expose() status!: AccountingExportStatus`
|
|
171
|
+
- `@Expose() sinaoEntriesCount?: number | null`
|
|
172
|
+
- `@Expose() sinaoZipUrl?: string | null`
|
|
173
|
+
- `@Expose() downloadedAt?: string | null`
|
|
174
|
+
- `@Expose() createdAt?: string`
|
|
175
|
+
- `@Expose() transmissions?: unknown[]` (typer plus finement si le back documente)
|
|
176
|
+
- `@Expose() canTransmit?: string[]`
|
|
177
|
+
- statiques `JsonToObject` / `JsonToObjects`
|
|
178
|
+
- helpers dérivés (getters ou fonctions pures exportées), utilisés par l'UI :
|
|
179
|
+
- `isDownloaded` = `downloadedAt != null`
|
|
180
|
+
- `isGenerating` = `status === GENERATING`
|
|
181
|
+
- `isFailed` = `status === FAILED`
|
|
182
|
+
- `isAvailable` = `status === GENERATED && downloadedAt == null`
|
|
183
|
+
|
|
184
|
+
- `SinaoDownload` (mapping snake_case)
|
|
185
|
+
- `@Expose() id!: number`
|
|
186
|
+
- `@Expose() status!: SinaoDownloadStatus`
|
|
187
|
+
- `@Expose({ name: "entries_count" }) entriesCount?: number`
|
|
188
|
+
- `@Expose({ name: "file_url" }) fileUrl?: string | null`
|
|
189
|
+
- `@Expose({ name: "period_start" }) periodStart?: string`
|
|
190
|
+
- `@Expose({ name: "period_end" }) periodEnd?: string`
|
|
191
|
+
- `@Expose({ name: "error_message" }) errorMessage?: string | null`
|
|
192
|
+
- `@Expose({ name: "info_message" }) infoMessage?: string | null`
|
|
193
|
+
- statiques `JsonToObject`
|
|
194
|
+
|
|
195
|
+
- `PendingEntries` (si `GET /exports/pending` renvoie un objet) : `@Expose() count!: number`
|
|
196
|
+
(adapter au vrai payload ; à défaut typer `{ count: number }`).
|
|
197
|
+
|
|
198
|
+
- Type d'aide pour la liste paginée (générique Spring) — soit réutiliser un type `Page<T>`
|
|
199
|
+
déjà présent dans le projet **s'il existe**, sinon en définir un local :
|
|
200
|
+
```ts
|
|
201
|
+
export type Page<T> = {
|
|
202
|
+
content: T[];
|
|
203
|
+
totalPages: number;
|
|
204
|
+
totalElements: number;
|
|
205
|
+
number: number; // index de page courant
|
|
206
|
+
size: number;
|
|
207
|
+
first: boolean;
|
|
208
|
+
last: boolean;
|
|
209
|
+
empty: boolean;
|
|
210
|
+
};
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Service à créer — `src/services/Billing/AccountingExportService.ts`
|
|
216
|
+
|
|
217
|
+
Clé de cache : `export const ACCOUNTING_EXPORT = "ACCOUNTING_EXPORT";`
|
|
218
|
+
|
|
219
|
+
Hooks / fonctions à exposer (adapter la signature au pattern maison `useFetch...`) :
|
|
220
|
+
|
|
221
|
+
1. **`useFetchPendingEntriesCount(garageId)`** → `useQuery`
|
|
222
|
+
- `GET /exports/pending` (ou lire `pendingEntriesCount` de la liste selon la note API).
|
|
223
|
+
- Renvoie un `number`. `queryKey: [ACCOUNTING_EXPORT, "pending", garageId]`.
|
|
224
|
+
|
|
225
|
+
2. **`useFetchExports(garageId, { page, size })`** → `useQuery`
|
|
226
|
+
- `GET /exports?page=&size=`. `size` par défaut = **12** (cf. échantillons).
|
|
227
|
+
- Renvoie un `Page<AccountingExport>`. Gérer les deux formes de payload (nue / enveloppée).
|
|
228
|
+
- `queryKey: [ACCOUNTING_EXPORT, "list", garageId, page, size]`.
|
|
229
|
+
|
|
230
|
+
3. **`useFetchExportsInfinite(garageId, { size })`** → `useInfiniteQuery`
|
|
231
|
+
- Pour l'historique « Voir tout ». `getNextPageParam` basé sur `totalPages` (cf. pattern
|
|
232
|
+
`useFetchQuoteByState` déjà utilisé dans le projet). Aplatir via `data.pages.flatMap`.
|
|
233
|
+
- À utiliser pour la liste « Déjà téléchargés » quand on déplie l'historique complet.
|
|
234
|
+
|
|
235
|
+
4. **`useFetchExport(garageId, exportId, options?)`** → `useQuery`
|
|
236
|
+
- `GET /exports/{id}`. Sert au **polling** : activer `refetchInterval` tant que
|
|
237
|
+
`status === generating` (ex. 2000 ms), le couper sinon. `enabled` sur `!!exportId`.
|
|
238
|
+
|
|
239
|
+
5. **`useGenerateExport(garageId)`** → `useMutation`
|
|
240
|
+
- `POST /exports?until={yyyy-MM-dd}`. Par défaut `until = today` (format date-fns
|
|
241
|
+
`yyyy-MM-dd`). Sur succès : invalider `pending` + `list`, retourner l'`AccountingExport`
|
|
242
|
+
initial (`generating`) pour amorcer le polling.
|
|
243
|
+
|
|
244
|
+
6. **`useCancelExport(garageId)`** → `useMutation`
|
|
245
|
+
- `DELETE /exports/{id}`. Sur succès : invalider `pending` + `list`.
|
|
246
|
+
- Rappel métier : seul le **dernier** export **non téléchargé** est annulable — l'UI ne
|
|
247
|
+
doit proposer « Annuler » que dans ce cas, mais gérer aussi un refus back (409/403) via snackbar.
|
|
248
|
+
|
|
249
|
+
7. **`useDownloadExport(garageId)`** → `useMutation`
|
|
250
|
+
- `POST /exports/{id}/download` → renvoie (ou permet de récupérer) un `downloadId`
|
|
251
|
+
(`sinao-download`). Puis suivre l'état.
|
|
252
|
+
|
|
253
|
+
8. **`useFetchSinaoDownload(garageId, downloadId, options?)`** → `useQuery`
|
|
254
|
+
- `GET /sinao-downloads/{id}`, polling tant que `status` ∈ {pending, processing}.
|
|
255
|
+
- Quand `finished` + `file_url` : déclencher le téléchargement effectif du fichier
|
|
256
|
+
(réutiliser le helper de download authentifié déjà présent dans le projet — même
|
|
257
|
+
mécanisme que le download PDF factures : `readCookie("movalibProToken")`, blob, `<a>`
|
|
258
|
+
programmatique. **Ne pas réinventer** : chercher le helper existant et le réutiliser).
|
|
259
|
+
|
|
260
|
+
9. **`invalidateAccountingExports(queryClient)`** → `invalidateQueries({ queryKey: [ACCOUNTING_EXPORT], exact: false })`.
|
|
261
|
+
|
|
262
|
+
Toutes les fonctions passent par `request({ url, method, body? })`, puis
|
|
263
|
+
`AccountingExport.JsonToObject(res)` / `.JsonToObjects(res.content)` selon le cas.
|
|
264
|
+
Gérer les erreurs avec `.catch` → `throw new Error(...)` remonté aux mutations pour le snackbar.
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## Écran — `src/pages/billing/AccountingExports.tsx`
|
|
269
|
+
|
|
270
|
+
**Ordre des blocs à respecter (issu de la maquette) — du haut vers le bas :**
|
|
271
|
+
|
|
272
|
+
1. **En-tête** : titre « Écritures comptables » + nom du garage (dans le style d'en-tête
|
|
273
|
+
des autres pages du module, pas la mise en forme de la maquette).
|
|
274
|
+
|
|
275
|
+
2. **Bloc générateur** (carte MUI, style thème) :
|
|
276
|
+
- Grand **compteur** = nombre d'écritures en attente (`pending`) + libellé
|
|
277
|
+
« pièces prêtes à exporter » (accord singulier/pluriel).
|
|
278
|
+
- Ligne d'info secondaire : « Saisies depuis votre dernière génération, le {date du dernier export} »
|
|
279
|
+
(formatée date-fns/fr). Si aucun export précédent, adapter le texte.
|
|
280
|
+
- **Bouton principal « Générer l'export FEC »** (couleur primaire du thème) :
|
|
281
|
+
- actif si `pending > 0` et pas de génération en cours ;
|
|
282
|
+
- **désactivé** avec libellé « Aucune écriture à exporter » si `pending === 0` ;
|
|
283
|
+
- **désactivé** avec libellé « Génération en cours… » pendant un `generating`.
|
|
284
|
+
- **Zone de progression** (visible seulement pendant `generating`) : un indicateur de
|
|
285
|
+
progression MUI (`LinearProgress` — indéterminé, car l'API n'expose pas de %) + texte
|
|
286
|
+
d'état. À la fin : message « Export prêt — à télécharger ci-dessous » (style thème,
|
|
287
|
+
pas la pastille jaune de la maquette).
|
|
288
|
+
- **État « génération déjà lancée au chargement »** : si, au montage, un export est déjà
|
|
289
|
+
`generating` (présent en tête de liste), afficher directement l'état en cours + bouton
|
|
290
|
+
désactivé, et démarrer le polling.
|
|
291
|
+
|
|
292
|
+
3. **Bloc « À télécharger »** (exports `isAvailable` = generated & non téléchargés) :
|
|
293
|
+
- En-tête de section « {n} fichier(s) à télécharger ».
|
|
294
|
+
- Une ligne par export : date (`createdAt`), détail « {sinaoEntriesCount} écritures »,
|
|
295
|
+
bouton **Télécharger**.
|
|
296
|
+
- Sur **le tout premier** (le plus récent) et **seulement s'il est le dernier généré non
|
|
297
|
+
téléchargé** : bouton **Annuler** (avec confirmation — voir plus bas).
|
|
298
|
+
- Ligne nouvellement générée : léger highlight transitoire (via `sx`/thème), optionnel.
|
|
299
|
+
- **État « échec »** : si un export est `failed`, l'afficher en ligne d'erreur (couleur
|
|
300
|
+
d'erreur du thème) avec message « La génération a échoué. Aucune écriture n'a été
|
|
301
|
+
consommée, vous pouvez relancer. » + bouton **Relancer** (= re-générer).
|
|
302
|
+
|
|
303
|
+
4. **État « Tout récupéré »** : si aucun export « à télécharger » → petit bloc centré
|
|
304
|
+
« Tous vos fichiers ont été récupérés. » (icône check du thème).
|
|
305
|
+
|
|
306
|
+
5. **Section « Déjà téléchargés »** (exports `isDownloaded`) :
|
|
307
|
+
- Liste : icône check, résumé « {date} · {n} écritures », mention « récupéré le {downloadedAt} »,
|
|
308
|
+
bouton discret **Télécharger** (re-download).
|
|
309
|
+
- **Pagination** : afficher d'abord la première page (size 12), puis bouton
|
|
310
|
+
**« Voir tout l'historique »** qui bascule sur `useFetchExportsInfinite` (ou incrémente
|
|
311
|
+
la page) — l'historique peut être volumineux, ne jamais tout charger d'un coup.
|
|
312
|
+
|
|
313
|
+
**Cas limites à couvrir explicitement (critères d'acceptation) :**
|
|
314
|
+
- `pending === 0` → compteur 0 + bouton désactivé « Aucune écriture à exporter ».
|
|
315
|
+
- Génération en cours → progression + bouton désactivé + polling.
|
|
316
|
+
- Échec de génération → ligne d'erreur + relance possible.
|
|
317
|
+
- Annulation impossible (un export plus récent existe) → **ne pas** afficher « Annuler » sur
|
|
318
|
+
les exports concernés ; si le back refuse une annulation, snackbar explicite.
|
|
319
|
+
- Historique volumineux → pagination / infinite scroll.
|
|
320
|
+
- Aucun doublon entre exports successifs : garanti côté back (incrémental) ; l'UI se contente
|
|
321
|
+
de ne jamais relancer deux générations en parallèle (désactiver le bouton pendant le WIP).
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
## Sous-composants suggérés (`src/pages/billing/components/`)
|
|
326
|
+
|
|
327
|
+
Découper pour rester lisible (chaque fichier ≈ une responsabilité) :
|
|
328
|
+
|
|
329
|
+
- `ExportGenerator.tsx` — carte compteur + bouton générer + progression.
|
|
330
|
+
- `ExportAvailableList.tsx` — liste « à télécharger » (+ gestion Annuler/Relancer).
|
|
331
|
+
- `ExportDownloadedList.tsx` — liste « déjà téléchargés » + pagination / voir tout.
|
|
332
|
+
- `ExportRow.tsx` — ligne d'export réutilisable (variantes : available / downloaded / error).
|
|
333
|
+
- `CancelExportConfirm.tsx` — confirmation d'annulation (dialog MUI ou confirm inline selon
|
|
334
|
+
le pattern de confirmation déjà utilisé dans movalib-pro — réutiliser l'existant).
|
|
335
|
+
- `AccountingExportStatusChip.tsx` — chip de statut façon `QuoteStatusChip`.
|
|
336
|
+
|
|
337
|
+
Le téléchargement effectif du ZIP réutilise le helper de download authentifié existant
|
|
338
|
+
(cf. download PDF factures). Chercher et réutiliser, ne pas dupliquer.
|
|
339
|
+
|
|
340
|
+
---
|
|
341
|
+
|
|
342
|
+
## Confirmation d'annulation
|
|
343
|
+
|
|
344
|
+
Au clic sur « Annuler » : demander confirmation avec un message du type
|
|
345
|
+
« Annuler l'export du {date} ? Ses {n} écritures repartiront dans le prochain export. »
|
|
346
|
+
→ boutons « Conserver » / « Annuler l'export ». Sur confirmation : `useCancelExport`,
|
|
347
|
+
puis invalider `pending` + `list` (le compteur WIP doit augmenter du nombre d'écritures
|
|
348
|
+
restituées). Utiliser le composant/dialog de confirmation déjà en place dans le projet.
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
## Correspondance maquette → DA movalib-pro
|
|
353
|
+
|
|
354
|
+
| Élément maquette | À remplacer par |
|
|
355
|
+
|---|---|
|
|
356
|
+
| Police Outfit, variables `--vert/--jaune/--rose/--noir` | Thème MUI (`theme.palette.*`), typo du thème |
|
|
357
|
+
| `.bouton--principal` vert | `Button variant="contained"` couleur primaire du thème |
|
|
358
|
+
| `.bouton--telecharger` noir | `Button variant="contained"` selon thème (secondaire/neutre) |
|
|
359
|
+
| `.bouton--annuler` / `--discret` | `Button variant="outlined"` / `variant="text"` |
|
|
360
|
+
| Bande jaune « à télécharger » | En-tête de section MUI (couleur d'accent du thème) |
|
|
361
|
+
| Pastille jaune « Export prêt » | `Alert`/texte info du thème |
|
|
362
|
+
| `.piste` + `.jauge` custom | `LinearProgress` MUI (indéterminé) |
|
|
363
|
+
| Puce rose d'erreur / `.ligne--erreur` | Couleur `error` du thème + chip/texte d'erreur |
|
|
364
|
+
| Icônes SVG inline | Icônes MUI (`@mui/icons-material`) équivalentes |
|
|
365
|
+
| Highlights `--neuve` / `--recuperee` | `sx` avec transition légère du thème (optionnel) |
|
|
366
|
+
|
|
367
|
+
Le **JS de la maquette est illustratif** (état simulé) : ne pas le porter. La logique réelle
|
|
368
|
+
vient des hooks React Query ci-dessus.
|
|
369
|
+
|
|
370
|
+
---
|
|
371
|
+
|
|
372
|
+
## Definition of Done
|
|
373
|
+
|
|
374
|
+
- [ ] Route ajoutée dans `App.tsx` (pattern des pages `billing/*`), accès depuis Facturation.
|
|
375
|
+
- [ ] Modèles `AccountingExport` / `SinaoDownload` en class-transformer, casing géré.
|
|
376
|
+
- [ ] Service complet (pending, liste paginée + infinite, état export, générer, annuler,
|
|
377
|
+
download + suivi sinao-download, invalidation).
|
|
378
|
+
- [ ] Écran conforme à l'ordre des blocs, tous les états couverts (pending 0, generating,
|
|
379
|
+
failed, available, downloaded, annulation impossible, historique paginé).
|
|
380
|
+
- [ ] DA respectée (MUI + thème, aucune valeur en dur de la maquette).
|
|
381
|
+
- [ ] Polling génération + polling download, coupés dès l'état terminal.
|
|
382
|
+
- [ ] Aucune génération en double possible (bouton verrouillé pendant le WIP).
|
|
383
|
+
- [ ] `tsc` sans erreur, pas de `any` non justifié, pas de `console.log`.
|
|
384
|
+
- [ ] `garageId` dynamique (pas de `20` en dur).
|
|
385
|
+
|
|
386
|
+
## Questions ouvertes à confirmer côté back (à signaler, ne pas bloquer)
|
|
387
|
+
|
|
388
|
+
- Forme définitive de `GET /exports` (nue vs enveloppée + `pendingEntriesCount`).
|
|
389
|
+
- Payload exact de `GET /exports/pending`.
|
|
390
|
+
- Valeur exacte du statut d'échec (`failed` ? `error` ?).
|
|
391
|
+
- Réponse de `POST /exports/{id}/download` : renvoie-t-elle directement le `downloadId` /
|
|
392
|
+
l'URL, ou faut-il enchaîner sur `GET /sinao-downloads/{id}` avec un id connu autrement ?
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
Tous préfixés /api/billing/{garageId}/packages
|
|
2
|
+
|
|
3
|
+
┌─────────┬──────────────────────────────────────────────┬─────────────────────────────────────┐
|
|
4
|
+
│ Méthode │ URL │ Description │
|
|
5
|
+
├─────────┼──────────────────────────────────────────────┼─────────────────────────────────────┤
|
|
6
|
+
│ GET │ /{garageId}/packages?search=&page=0&limit=20 │ Liste paginée des forfaits │
|
|
7
|
+
├─────────┼──────────────────────────────────────────────┼─────────────────────────────────────┤
|
|
8
|
+
│ GET │ /{garageId}/packages/{packageId} │ Détail d'un forfait avec composants │
|
|
9
|
+
├─────────┼──────────────────────────────────────────────┼─────────────────────────────────────┤
|
|
10
|
+
│ POST │ /{garageId}/packages │ Création d'un forfait │
|
|
11
|
+
├─────────┼──────────────────────────────────────────────┼─────────────────────────────────────┤
|
|
12
|
+
│ PATCH │ /{garageId}/packages/{packageId} │ Modification d'un forfait │
|
|
13
|
+
├─────────┼──────────────────────────────────────────────┼─────────────────────────────────────┤
|
|
14
|
+
│ DELETE │ /{garageId}/packages/{packageId} │ Suppression d'un forfait │
|
|
15
|
+
└─────────┴──────────────────────────────────────────────┴─────────────────────────────────────┘
|
|
16
|
+
|
|
17
|
+
Exemple body POST/PATCH :
|
|
18
|
+
{
|
|
19
|
+
"reference": "FORFAIT-VIDANGE-01",
|
|
20
|
+
"label": "Forfait vidange complète",
|
|
21
|
+
"amountCents": 8000,
|
|
22
|
+
"amountInclVatCents": 9600,
|
|
23
|
+
"vatRate": 20.00,
|
|
24
|
+
"accountId": 42,
|
|
25
|
+
"components": [
|
|
26
|
+
{
|
|
27
|
+
"catalogId": 101,
|
|
28
|
+
"reference": "HUI-MOT-5W30",
|
|
29
|
+
"label": "Huile moteur 5W30 - 5L",
|
|
30
|
+
"quantity": 1,
|
|
31
|
+
"amountCents": 3500,
|
|
32
|
+
"amountInclVatCents": 4200,
|
|
33
|
+
"vatRate": 20.00,
|
|
34
|
+
"index": 0
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"catalogId": 102,
|
|
38
|
+
"reference": "FIL-HUI-01",
|
|
39
|
+
"label": "Filtre à huile",
|
|
40
|
+
"quantity": 1,
|
|
41
|
+
"amountCents": 1000,
|
|
42
|
+
"amountInclVatCents": 1200,
|
|
43
|
+
"vatRate": 20.00,
|
|
44
|
+
"index": 1
|
|
45
|
+
}
|
|
46
|
+
],
|
|
47
|
+
"labor": {
|
|
48
|
+
"serviceId": 55,
|
|
49
|
+
"reference": "MO-VIDANGE",
|
|
50
|
+
"label": "Main d'œuvre vidange",
|
|
51
|
+
"quantity": 1,
|
|
52
|
+
"amountCents": 3500,
|
|
53
|
+
"amountInclVatCents": 4200,
|
|
54
|
+
"vatRate": 20.00
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
Tous les montants sont en centimes (8000 = 80,00 €). La somme Σ composants TTC + labor TTC doit être égale à amountInclVatCents — le back valide cette cohérence.
|
|
59
|
+
|
|
60
|
+
Modifications sur les appels existants updateInvoice / updateQuote
|
|
61
|
+
|
|
62
|
+
Quand un forfait est ajouté sur un devis ou une facture, il faut passer les IDs des forfaits concernés en query params sur les endpoints POST /{garageId}/invoices/{invoiceId} et POST
|
|
63
|
+
/{garageId}/quotes/{quoteId} :
|
|
64
|
+
|
|
65
|
+
POST /api/billing/12/invoices/456?packageIds[0]=7&packageIds[1]=12
|
|
66
|
+
|
|
67
|
+
Ces IDs servent à alimenter la table analytique document_packages. À chaque appel, le back repart de zéro et recrée les associations — pas d'état à maintenir côté front, envoyer simplement la liste
|
|
68
|
+
complète des forfaits présents sur le document à ce moment-là. Si le document n'a plus de forfait, ne pas envoyer le paramètre (ou envoyer une liste vide).
|
|
69
|
+
|
|
70
|
+
Nouvelles propriétés sur les lignes du document
|
|
71
|
+
|
|
72
|
+
Quand le front crée une ligne de type commentaire correspondant à un composant de forfait, il doit stocker et renvoyer l'UUID du PackageComponent associé. Ce champ est retourné dans la réponse GET du
|
|
73
|
+
forfait (components[].id).
|
|
74
|
+
|
|
75
|
+
Ce packageComponentId est ce qui permet au back de déclencher la sortie de stock correcte au moment de la finalisation de la facture, en ignorant la quantité Sinao et en utilisant celle définie dans
|
|
76
|
+
le forfait.
|
|
77
|
+
|
|
78
|
+
⚠️ Si l'utilisateur supprime une ligne COMPONENT du document avant de finaliser, ne pas envoyer le packageComponentId → pas de sortie de stock pour ce composant (comportement attendu).
|