WIP
This commit is contained in:
138
docs/superpowers/specs/2026-03-23-document-types-design.md
Normal file
138
docs/superpowers/specs/2026-03-23-document-types-design.md
Normal file
@@ -0,0 +1,138 @@
|
||||
# Document Types — Design Spec
|
||||
|
||||
Date: 2026-03-23
|
||||
Status: Approved
|
||||
|
||||
## Goal
|
||||
|
||||
Add a `type` field to documents so users can classify them (documentation, devis, facture, plan, photo, autre). Users can set the type at upload and change it afterward via a mini-modal.
|
||||
|
||||
## Enum Values
|
||||
|
||||
| Value | Label |
|
||||
|-------|-------|
|
||||
| `documentation` | Documentation |
|
||||
| `devis` | Devis |
|
||||
| `facture` | Facture |
|
||||
| `plan` | Plan |
|
||||
| `photo` | Photo |
|
||||
| `autre` | Autre |
|
||||
|
||||
Default: `documentation`
|
||||
|
||||
## Backend
|
||||
|
||||
### 1. PHP Enum
|
||||
|
||||
New file: `src/Enum/DocumentType.php`
|
||||
|
||||
```php
|
||||
enum DocumentType: string
|
||||
{
|
||||
case DOCUMENTATION = 'documentation';
|
||||
case DEVIS = 'devis';
|
||||
case FACTURE = 'facture';
|
||||
case PLAN = 'plan';
|
||||
case PHOTO = 'photo';
|
||||
case AUTRE = 'autre';
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Entity Change — Document.php
|
||||
|
||||
Add column:
|
||||
|
||||
```php
|
||||
#[ORM\Column(type: Types::STRING, length: 20, enumType: DocumentType::class)]
|
||||
#[Groups(['document:list'])]
|
||||
private DocumentType $type = DocumentType::DOCUMENTATION;
|
||||
```
|
||||
|
||||
Add getter/setter:
|
||||
|
||||
```php
|
||||
public function getType(): DocumentType { ... }
|
||||
public function setType(DocumentType $type): static { ... }
|
||||
```
|
||||
|
||||
### 3. API Platform — PATCH operation
|
||||
|
||||
Add a `Patch` operation on Document (ROLE_GESTIONNAIRE) to allow updating `name` and `type`. The existing `Put` already exists but PATCH is more appropriate for partial updates.
|
||||
|
||||
### 4. DocumentUploadProcessor
|
||||
|
||||
Accept optional `type` field from FormData. Validate against enum values, default to `documentation` if absent.
|
||||
|
||||
### 5. Migration
|
||||
|
||||
```sql
|
||||
ALTER TABLE documents ADD COLUMN type VARCHAR(20) NOT NULL DEFAULT 'documentation';
|
||||
|
||||
-- Classify existing documents by mimeType
|
||||
UPDATE documents SET type = 'photo' WHERE mimetype LIKE 'image/%';
|
||||
UPDATE documents SET type = 'autre'
|
||||
WHERE type = 'documentation'
|
||||
AND mimetype NOT LIKE 'application/pdf'
|
||||
AND mimetype NOT LIKE 'image/%';
|
||||
```
|
||||
|
||||
### 6. DocumentQueryController
|
||||
|
||||
Add `type` to the response array in `formatDocument()`.
|
||||
|
||||
## Frontend
|
||||
|
||||
### 1. Type Constants
|
||||
|
||||
New file: `app/shared/documentTypes.ts`
|
||||
|
||||
```typescript
|
||||
export const DOCUMENT_TYPES = [
|
||||
{ value: 'documentation', label: 'Documentation' },
|
||||
{ value: 'devis', label: 'Devis' },
|
||||
{ value: 'facture', label: 'Facture' },
|
||||
{ value: 'plan', label: 'Plan' },
|
||||
{ value: 'photo', label: 'Photo' },
|
||||
{ value: 'autre', label: 'Autre' },
|
||||
] as const
|
||||
|
||||
export type DocumentTypeValue = typeof DOCUMENT_TYPES[number]['value']
|
||||
```
|
||||
|
||||
### 2. DocumentUpload.vue — Type select at upload
|
||||
|
||||
Add a select dropdown (default: `documentation`) in the upload zone. The selected type applies to all files in the current batch. Pass the type through to `uploadDocuments()`.
|
||||
|
||||
### 3. useDocuments composable
|
||||
|
||||
- `uploadDocuments()`: accept `type` in the upload context, append to FormData
|
||||
- New method: `updateDocument(id, { name, type })` — PATCH `/api/documents/{id}` with `application/merge-patch+json`
|
||||
- Add `type` to the `Document` interface
|
||||
|
||||
### 4. DocumentEditModal.vue (new component)
|
||||
|
||||
Mini-modal with:
|
||||
- Input text: document name (pre-filled)
|
||||
- Select: document type (pre-filled)
|
||||
- Buttons: Annuler / Sauvegarder
|
||||
- On save: call `updateDocument()`, emit `updated` event
|
||||
|
||||
### 5. Document list display
|
||||
|
||||
Everywhere documents are listed (machine detail, composant edit, piece edit, product, site):
|
||||
- Show type as a small badge next to the document name
|
||||
- Add a pencil/edit button that opens `DocumentEditModal`
|
||||
- On modal save: refresh the document in local state
|
||||
|
||||
## Migration of existing data
|
||||
|
||||
All existing documents classified by mimeType:
|
||||
- `image/*` → `photo`
|
||||
- `application/pdf` → `documentation`
|
||||
- Everything else → `autre`
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Custom user-defined types (table `document_types`) — can be added later
|
||||
- Filtering documents by type in the UI — can be added later
|
||||
- Bulk type change
|
||||
88
docs/superpowers/specs/2026-03-23-parc-machines-ux-design.md
Normal file
88
docs/superpowers/specs/2026-03-23-parc-machines-ux-design.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# Parc Machines — Améliorations UX
|
||||
|
||||
**Date** : 2026-03-23
|
||||
**Scope** : 3 changements sur le frontend + 1 extension backend
|
||||
|
||||
---
|
||||
|
||||
## 1. Filtre sites multi-sélection par checkboxes
|
||||
|
||||
### Contexte
|
||||
Le filtre site actuel est un `<select>` mono-sélection dans `machines/index.vue`.
|
||||
L'utilisateur veut pouvoir sélectionner plusieurs sites simultanément.
|
||||
|
||||
### Design
|
||||
- Remplacer le `<select>` par une rangée de checkboxes DaisyUI directement visibles dans la barre de filtre.
|
||||
- Chaque site = une checkbox avec le nom du site.
|
||||
- Quand **aucune** checkbox n'est cochée → toutes les machines s'affichent (équivalent "Tous les sites").
|
||||
- Quand **une ou plusieurs** sont cochées → filtre sur ces sites uniquement.
|
||||
|
||||
### Changements techniques
|
||||
**Fichier** : `Inventory_frontend/app/pages/machines/index.vue`
|
||||
|
||||
- **Réactivité** : utiliser `reactive(new Set())` (Vue 3.4+ supporte nativement les mutations `add`/`delete`/`has` sur un Set réactif). Pas de `.value` nécessaire.
|
||||
- **Note** : le fichier utilise `<script setup>` sans `lang="ts"` — ne pas utiliser d'annotations TypeScript comme `Set<string>`.
|
||||
- Template : remplacer le `<select>` par un `div` flex-wrap avec des checkboxes DaisyUI (`checkbox checkbox-sm`) + label pour chaque site.
|
||||
- Computed `filteredMachines` : remplacer `machine.siteId === selectedSite` par `selectedSites.size === 0 || selectedSites.has(machine.siteId)`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Tri alphabétique croissant
|
||||
|
||||
### Contexte
|
||||
Les machines s'affichent dans l'ordre retourné par l'API, sans tri. L'utilisateur veut un tri alphabétique croissant par nom.
|
||||
|
||||
### Design
|
||||
Ajouter un `.sort()` avec `localeCompare('fr')` à la fin du computed `filteredMachines`.
|
||||
|
||||
### Changements techniques
|
||||
**Fichier** : `Inventory_frontend/app/pages/machines/index.vue`
|
||||
|
||||
- Dans le computed `filteredMachines`, ajouter avant le `return` :
|
||||
```js
|
||||
filtered = [...filtered].sort((a, b) =>
|
||||
(a.name || '').localeCompare(b.name || '', 'fr')
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Recherche par référence dans les catalogues (Pièces, Composants, Produits)
|
||||
|
||||
### Contexte
|
||||
Les placeholders des champs de recherche promettent "Nom ou référence…" mais le frontend n'envoie que `?name=xxx` à l'API. Le backend (API Platform SearchFilter) supporte `name` et `reference` en `ipartial`, mais combiner `?name=xxx&reference=xxx` produit un AND (les deux doivent matcher), pas un OR.
|
||||
|
||||
### Design
|
||||
Créer une **Extension Doctrine** (`SearchByNameOrReferenceExtension`) qui intercepte un paramètre `?q=xxx` et ajoute une clause `WHERE name ILIKE %xxx% OR reference ILIKE %xxx%` à la requête. Côté frontend, remplacer `params.set('name', search)` par `params.set('q', search)`.
|
||||
|
||||
### Changements techniques
|
||||
|
||||
**Backend — Nouveau fichier** : `src/Doctrine/SearchByNameOrReferenceExtension.php`
|
||||
- Implémente `QueryCollectionExtensionInterface`
|
||||
- S'applique aux entités `Piece`, `Composant`, `Product`
|
||||
- Lit le paramètre `q` depuis la requête HTTP
|
||||
- Ajoute `LOWER(o.name) LIKE :searchQ OR LOWER(o.reference) LIKE :searchQ` avec paramètre `%{strtolower(q)}%`
|
||||
- **Échappement LIKE** : les caractères `%` et `_` dans l'input utilisateur sont échappés via `addcslashes($q, '%_')` pour éviter des matchs trop larges
|
||||
- **`reference` nullable** : les lignes avec `reference = NULL` ne matcheront pas (comportement SQL standard : `NULL LIKE x` = NULL = false), ce qui est le comportement attendu
|
||||
- **Pas de conflit** avec le `SearchFilter` existant : le paramètre `q` n'est pas enregistré comme propriété de `SearchFilter`, donc il sera ignoré par celui-ci. Les filtres `name` et `reference` restent disponibles pour d'autres usages.
|
||||
|
||||
**Frontend — 3 fichiers** (dans la fonction `loadXxx`, remplacer l'appel `params.set('name', search.trim())`) :
|
||||
- `Inventory_frontend/app/composables/usePieces.ts` → `params.set('q', search.trim())`
|
||||
- `Inventory_frontend/app/composables/useComposants.ts` → idem
|
||||
- `Inventory_frontend/app/composables/useProducts.ts` → idem
|
||||
|
||||
---
|
||||
|
||||
## Fichiers impactés (résumé)
|
||||
|
||||
| Fichier | Changement |
|
||||
|---------|-----------|
|
||||
| `Inventory_frontend/app/pages/machines/index.vue` | Checkboxes sites + tri alphabétique |
|
||||
| `src/Doctrine/SearchByNameOrReferenceExtension.php` | **Nouveau** — Extension Doctrine OR search |
|
||||
| `Inventory_frontend/app/composables/usePieces.ts` | `name` → `q` |
|
||||
| `Inventory_frontend/app/composables/useComposants.ts` | `name` → `q` |
|
||||
| `Inventory_frontend/app/composables/useProducts.ts` | `name` → `q` |
|
||||
|
||||
## Hors scope
|
||||
- La page Parc Machines cherche **déjà** sur nom ET référence côté frontend (filtrage client-side). Pas de changement nécessaire.
|
||||
- Aucun changement de placeholder — ils affichent déjà "Nom ou référence…".
|
||||
@@ -20,7 +20,8 @@ Permettre de consulter l'historique des versions numerotees (v1, v2, v3...) des
|
||||
|
||||
### Restauration
|
||||
- La restauration cree une **nouvelle version** (v+1) — on ne supprime jamais d'historique
|
||||
- L'AuditLog de la restauration a `action = "restore"` et le diff contient `restoredFromVersion: N`
|
||||
- Le service `EntityVersionService::restore()` cree **manuellement** un AuditLog avec `action = "restore"` et le diff contient `restoredFromVersion: N`
|
||||
- Important : le flush du restore declenche les AuditSubscribers, qui produiraient un `update` duplique. Pour eviter cela, l'entite porte un flag transitoire `$skipAudit = true` que les subscribers verifient
|
||||
|
||||
### Controle de squelette (Composant, Piece, Produit uniquement)
|
||||
- Avant restauration, on compare le ModelType actuel avec celui du snapshot
|
||||
@@ -35,10 +36,13 @@ Permettre de consulter l'historique des versions numerotees (v1, v2, v3...) des
|
||||
- **Machine** : site, liens composants/pieces/produits (MachineComponentLink, MachinePieceLink, MachineProductLink)
|
||||
- Les entites manquantes generent des **warnings** affiches a l'utilisateur
|
||||
- Les slots avec des entites supprimees sont restaures **vides** (sans selection)
|
||||
- Pour les custom field values : restauration par `fieldId` + entite parente (pas par ID de la CustomFieldValue elle-meme, car un sync ModelType peut recreer les CFV avec des IDs differents)
|
||||
- Les controles d'integrite utilisent des requetes batch (`findBy(['id' => $ids])`) plutot que des requetes individuelles par slot
|
||||
|
||||
### Machines
|
||||
- Pas de controle de squelette (pas de ModelType) : restauration toujours complete
|
||||
- Controle d'integrite sur le site et les liens machine
|
||||
- Machine n'a pas de champ `description` (contrairement aux autres entites)
|
||||
|
||||
### Permissions
|
||||
- Consulter les versions : `ROLE_VIEWER`
|
||||
@@ -78,7 +82,7 @@ Les Audit Subscribers doivent inclure dans le `snapshot` :
|
||||
"prix": 100.00,
|
||||
"typeComposant": { "id": "cl...", "name": "...", "code": "..." },
|
||||
"product": { "id": "cl...", "name": "..." },
|
||||
"constructeurs": [{ "id": "cl...", "name": "..." }],
|
||||
"constructeurIds": [{ "id": "cl...", "name": "..." }],
|
||||
"customFieldValues": [{ "id": "cl...", "fieldName": "...", "value": "..." }],
|
||||
"pieceSlots": [
|
||||
{ "id": "cl...", "typePieceId": "cl...", "selectedPieceId": "cl...", "quantity": 1, "position": 0 }
|
||||
@@ -103,7 +107,7 @@ Les Audit Subscribers doivent inclure dans le `snapshot` :
|
||||
"prix": 50.00,
|
||||
"typePiece": { "id": "cl...", "name": "...", "code": "..." },
|
||||
"product": { "id": "cl...", "name": "..." },
|
||||
"constructeurs": [{ "id": "cl...", "name": "..." }],
|
||||
"constructeurIds": [{ "id": "cl...", "name": "..." }],
|
||||
"customFieldValues": [{ "id": "cl...", "fieldName": "...", "value": "..." }],
|
||||
"productSlots": [
|
||||
{ "id": "cl...", "typeProductId": "cl...", "selectedProductId": "cl...", "familyCode": "...", "position": 0 }
|
||||
@@ -120,7 +124,7 @@ Les Audit Subscribers doivent inclure dans le `snapshot` :
|
||||
"reference": "...",
|
||||
"supplierPrice": 25.00,
|
||||
"typeProduct": { "id": "cl...", "name": "...", "code": "..." },
|
||||
"constructeurs": [{ "id": "cl...", "name": "..." }],
|
||||
"constructeurIds": [{ "id": "cl...", "name": "..." }],
|
||||
"customFieldValues": [{ "id": "cl...", "fieldName": "...", "value": "..." }],
|
||||
"version": 1
|
||||
}
|
||||
@@ -132,8 +136,9 @@ Les Audit Subscribers doivent inclure dans le `snapshot` :
|
||||
"id": "cl...",
|
||||
"name": "...",
|
||||
"reference": "...",
|
||||
"description": "...",
|
||||
"prix": 1500.00,
|
||||
"site": { "id": "cl...", "name": "..." },
|
||||
"constructeurIds": [{ "id": "cl...", "name": "..." }],
|
||||
"customFieldValues": [{ "id": "cl...", "fieldName": "...", "value": "..." }],
|
||||
"version": 4
|
||||
}
|
||||
@@ -295,6 +300,10 @@ CREATE INDEX IF NOT EXISTS idx_audit_entity_version ON audit_logs (entity_type,
|
||||
|
||||
---
|
||||
|
||||
## Ce qui change (breaking)
|
||||
|
||||
- **Piece snapshot** : le champ legacy `productIds` (ancien JSON) est remplace par `productSlots` (tables normalisees). Les anciens AuditLogs conservent `productIds` dans leur snapshot mais les nouveaux ne l'auront plus. Le restore utilise `productSlots` exclusivement.
|
||||
|
||||
## Ce qui ne change PAS
|
||||
|
||||
- L'onglet/page d'historique existant (`EntityHistoryController`) reste inchange
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
# Références Fournisseur par Item — Design Spec
|
||||
|
||||
**Date :** 2026-03-31
|
||||
**Statut :** Validé
|
||||
|
||||
## Contexte
|
||||
|
||||
Chaque entité (Machine, Pièce, Composant, Produit) a un champ `reference` générique et une relation ManyToMany avec `Constructeur`. Il n'existe aucun moyen de stocker une référence spécifique par fournisseur — si un item est vendu par 3 fournisseurs avec 3 références différentes, on ne peut en stocker qu'une seule.
|
||||
|
||||
## Objectif
|
||||
|
||||
Permettre de stocker une référence fournisseur (`supplierReference`) par couple (item, constructeur). Le champ `reference` existant reste inchangé comme référence interne. Le champ `supplierPrice` sur Product reste inchangé.
|
||||
|
||||
## Design
|
||||
|
||||
### Approche retenue : conversion ManyToMany → entités pivot
|
||||
|
||||
Remplacer les 4 tables de jointure simples (`_MachineConstructeurs`, `_PieceConstructeurs`, `_ComposantConstructeurs`, `_ProductConstructeurs`) par de vraies entités Doctrine Link, suivant le pattern existant (`MachinePieceLink`, `MachineComponentLink`, etc.).
|
||||
|
||||
### Nouvelles entités
|
||||
|
||||
| Entité | Table | FK item | FK constructeur | Champs extra |
|
||||
|--------|-------|---------|-----------------|--------------|
|
||||
| `MachineConstructeurLink` | `machine_constructeur_links` | `machineId` → `Machine` | `constructeurId` → `Constructeur` | `supplierReference` (string 255, nullable) |
|
||||
| `PieceConstructeurLink` | `piece_constructeur_links` | `pieceId` → `Piece` | `constructeurId` → `Constructeur` | `supplierReference` (string 255, nullable) |
|
||||
| `ComposantConstructeurLink` | `composant_constructeur_links` | `composantId` → `Composant` | `constructeurId` → `Constructeur` | `supplierReference` (string 255, nullable) |
|
||||
| `ProductConstructeurLink` | `product_constructeur_links` | `productId` → `Product` | `constructeurId` → `Constructeur` | `supplierReference` (string 255, nullable) |
|
||||
|
||||
### Structure de chaque entité
|
||||
|
||||
Chaque entité suit le pattern `MachinePieceLink` :
|
||||
|
||||
- `CuidEntityTrait` pour l'ID (string, 36 chars)
|
||||
- `#[ORM\HasLifecycleCallbacks]` avec `createdAt` / `updatedAt`
|
||||
- Contrainte unique sur `(item_id, constructeur_id)` via `#[ORM\UniqueConstraint]`
|
||||
- `#[ApiResource]` avec opérations CRUD complètes
|
||||
- Sécurité : `ROLE_VIEWER` pour lecture, `ROLE_GESTIONNAIRE` pour écriture
|
||||
- `ManyToOne` vers l'item (onDelete CASCADE)
|
||||
- `ManyToOne` vers `Constructeur` (onDelete CASCADE)
|
||||
- Champ `supplierReference` (string 255, nullable)
|
||||
|
||||
### Modifications sur les entités existantes
|
||||
|
||||
#### Machine, Pièce, Composant, Produit
|
||||
- Supprimer la propriété `ManyToMany` `constructeurs` et ses getters/setters/add/remove
|
||||
- Ajouter une propriété `OneToMany` `constructeurLinks` vers le Link correspondant
|
||||
- Getter `getConstructeurLinks(): Collection`
|
||||
|
||||
#### Constructeur
|
||||
- Supprimer les 4 propriétés `ManyToMany` (`machines`, `composants`, `pieces`, `products`) et leurs getters/setters
|
||||
- Ajouter 4 propriétés `OneToMany` vers les Links correspondants
|
||||
|
||||
### Migration SQL
|
||||
|
||||
1. Créer les 4 nouvelles tables avec colonnes `id`, `machineId`/`pieceId`/etc., `constructeurId`, `supplierReference`, `createdAt`, `updatedAt`
|
||||
2. Ajouter les contraintes uniques
|
||||
3. Migrer les données des anciennes tables de jointure vers les nouvelles (génération CUID pour chaque ligne, `supplierReference` = NULL)
|
||||
4. Supprimer les anciennes tables de jointure (`_MachineConstructeurs`, `_PieceConstructeurs`, `_ComposantConstructeurs`, `_ProductConstructeurs`)
|
||||
|
||||
### API
|
||||
|
||||
Endpoints API Platform auto-générés pour chaque Link :
|
||||
- `GET /api/machine_constructeur_links` — liste (filtrable par machine, constructeur)
|
||||
- `GET /api/machine_constructeur_links/{id}` — détail
|
||||
- `POST /api/machine_constructeur_links` — créer un lien avec référence
|
||||
- `PATCH /api/machine_constructeur_links/{id}` — modifier la référence
|
||||
- `DELETE /api/machine_constructeur_links/{id}` — supprimer le lien
|
||||
|
||||
Idem pour les 3 autres types.
|
||||
|
||||
### Frontend
|
||||
|
||||
Les pages détail/édition qui affichent les constructeurs devront être adaptées pour :
|
||||
- Afficher la `supplierReference` à côté de chaque constructeur
|
||||
- Permettre l'édition de la référence fournisseur lors de l'ajout/modification d'un constructeur
|
||||
- Utiliser les endpoints `*ConstructeurLink` au lieu de la collection `constructeurs`
|
||||
|
||||
### Hors périmètre
|
||||
|
||||
- Migration de `supplierPrice` de Product vers le Link (explicitement exclu)
|
||||
- Modification du champ `reference` existant sur les entités
|
||||
- Référence auto (`referenceAuto`) sur Pièce/Composant — non impactée
|
||||
Reference in New Issue
Block a user