# Document Types Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Add a `type` enum field to documents (documentation, devis, facture, plan, photo, autre) with classification at upload and inline editing afterward.

**Architecture:** New PHP enum `DocumentType` + column on `documents` table. Migration classifies existing rows by mimeType. Frontend gets a type select at upload, a badge in document lists, and a mini-modal for editing name+type via PATCH.

**Tech Stack:** Symfony 8, API Platform, Doctrine, PHP 8.4 enums, Nuxt 4, Vue 3, DaisyUI 5

---

## File Structure

### Backend (create)
- `src/Enum/DocumentType.php` — PHP backed enum with 6 values
- `migrations/VersionXXX_add_document_type.php` — ALTER TABLE + data classification

### Backend (modify)
- `src/Entity/Document.php` — add `type` column + Patch operation
- `src/State/DocumentUploadProcessor.php` — accept `type` from FormData
- `src/Controller/DocumentQueryController.php` — add `type` to `normalizeDocuments()`

### Frontend (create)
- `frontend/app/shared/documentTypes.ts` — type constants + labels
- `frontend/app/components/DocumentEditModal.vue` — mini-modal for editing name+type

### Frontend (modify)
- `frontend/app/composables/useDocuments.ts` — add `type` to interface + `updateDocument()` method
- `frontend/app/components/DocumentUpload.vue` — add type select
- `frontend/app/components/common/DocumentListInline.vue` — add type badge + edit button
- `frontend/app/composables/useEntityDocuments.ts` — add `updateDocument` delegation
- `frontend/app/pages/documents.vue` — add type column + edit button

---

### Task 1: PHP Enum + Entity Column

**Files:**
- Create: `src/Enum/DocumentType.php`
- Modify: `src/Entity/Document.php:31-54` (API resource), `src/Entity/Document.php:107-113` (add column after site)

- [ ] **Step 1: Create the DocumentType enum**

```php
// src/Enum/DocumentType.php
<?php

declare(strict_types=1);

namespace App\Enum;

enum DocumentType: string
{
    case DOCUMENTATION = 'documentation';
    case DEVIS = 'devis';
    case FACTURE = 'facture';
    case PLAN = 'plan';
    case PHOTO = 'photo';
    case AUTRE = 'autre';
}
```

- [ ] **Step 2: Add type column to Document entity**

In `src/Entity/Document.php`, add after the `$site` property (line ~106):

```php
#[ORM\Column(type: Types::STRING, length: 20, enumType: DocumentType::class)]
#[Groups(['document:list', 'document:read', 'composant:read', 'piece:read', 'product:read'])]
private DocumentType $type = DocumentType::DOCUMENTATION;
```

Add getter/setter:

```php
public function getType(): DocumentType
{
    return $this->type;
}

public function setType(DocumentType $type): static
{
    $this->type = $type;

    return $this;
}
```

Add the import at top: `use App\Enum\DocumentType;`

- [ ] **Step 3: Add Patch operation to Document API resource**

In the `operations` array of `#[ApiResource(...)]`, add after the existing `Put`:

```php
new Patch(security: "is_granted('ROLE_GESTIONNAIRE')"),
```

Add the import: `use ApiPlatform\Metadata\Patch;`

- [ ] **Step 4: Run cs-fixer and verify**

Run: `make php-cs-fixer-allow-risky`

- [ ] **Step 5: Commit**

```bash
git add src/Enum/DocumentType.php src/Entity/Document.php
git commit -m "feat(documents) : add DocumentType enum and type column on entity"
```

---

### Task 2: Migration

**Files:**
- Create: new migration file via Doctrine

- [ ] **Step 1: Generate migration**

Run: `docker exec -u www-data php-inventory-apache php bin/console doctrine:migrations:diff`

This will generate a migration. Then edit it to add the data classification.

- [ ] **Step 2: Edit migration to classify existing documents**

The generated migration will have the `ALTER TABLE` for adding the column. After the column add, append:

```sql
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/%';
```

Use `IF NOT EXISTS` pattern consistent with other migrations:

```php
public function up(Schema $schema): void
{
    $this->addSql("DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'documents' AND column_name = 'type') THEN ALTER TABLE documents ADD COLUMN type VARCHAR(20) NOT NULL DEFAULT 'documentation'; END IF; END $$");
    $this->addSql("UPDATE documents SET type = 'photo' WHERE mimetype LIKE 'image/%'");
    $this->addSql("UPDATE documents SET type = 'autre' WHERE type = 'documentation' AND mimetype NOT LIKE 'application/pdf' AND mimetype NOT LIKE 'image/%'");
}
```

- [ ] **Step 3: Run migration**

Run: `docker exec -u www-data php-inventory-apache php bin/console doctrine:migrations:migrate --no-interaction`

- [ ] **Step 4: Verify data classification**

Run: `docker exec -u www-data php-inventory-apache php bin/console dbal:run-sql "SELECT type, COUNT(*) FROM documents GROUP BY type"`

- [ ] **Step 5: Commit**

```bash
git add migrations/
git commit -m "feat(documents) : add migration for type column with data classification"
```

---

### Task 3: Backend — Upload Processor + Query Controller

**Files:**
- Modify: `src/State/DocumentUploadProcessor.php:66-77`
- Modify: `src/Controller/DocumentQueryController.php:110-127`

- [ ] **Step 1: Accept type in DocumentUploadProcessor**

In `handleMultipartUpload()`, after `$document->setSize((int) $size);` (line ~77), add:

```php
// Document type from form field (default: documentation)
$typeValue = $request->request->get('type', 'documentation');
$docType = DocumentType::tryFrom($typeValue) ?? DocumentType::DOCUMENTATION;
$document->setType($docType);
```

Add import: `use App\Enum\DocumentType;`

- [ ] **Step 2: Add type to DocumentQueryController normalizeDocuments**

In `normalizeDocuments()`, add `'type'` to the returned array after `'productId'`:

```php
'type'        => $document->getType()->value,
```

- [ ] **Step 3: Write test for PATCH type update**

In `tests/Api/Entity/DocumentTest.php`, add:

```php
public function testPatchType(): void
{
    $doc = $this->createDocumentInDb();

    $client = $this->createGestionnaireClient();
    $client->request('PATCH', self::iri('documents', $doc->getId()), [
        'headers' => ['Content-Type' => 'application/merge-patch+json'],
        'json'    => ['type' => 'devis'],
    ]);

    $this->assertResponseIsSuccessful();
    $this->assertJsonContains(['type' => 'devis']);
}

public function testPatchNameAndType(): void
{
    $doc = $this->createDocumentInDb();

    $client = $this->createGestionnaireClient();
    $client->request('PATCH', self::iri('documents', $doc->getId()), [
        'headers' => ['Content-Type' => 'application/merge-patch+json'],
        'json'    => ['name' => 'new-name', 'type' => 'facture'],
    ]);

    $this->assertResponseIsSuccessful();
    $this->assertJsonContains(['name' => 'new-name', 'type' => 'facture']);
}

public function testGetItemIncludesType(): void
{
    $doc = $this->createDocumentInDb();

    $client = $this->createViewerClient();
    $client->request('GET', self::iri('documents', $doc->getId()));

    $this->assertResponseIsSuccessful();
    $this->assertJsonContains(['type' => 'documentation']);
}

public function testViewerCannotPatch(): void
{
    $doc = $this->createDocumentInDb();

    $client = $this->createViewerClient();
    $client->request('PATCH', self::iri('documents', $doc->getId()), [
        'headers' => ['Content-Type' => 'application/merge-patch+json'],
        'json'    => ['type' => 'devis'],
    ]);

    $this->assertResponseStatusCodeSame(403);
}
```

- [ ] **Step 4: Run tests**

Run: `make test FILES=tests/Api/Entity/DocumentTest.php`
Expected: all tests pass

- [ ] **Step 5: Run cs-fixer**

Run: `make php-cs-fixer-allow-risky`

- [ ] **Step 6: Commit**

```bash
git add src/State/DocumentUploadProcessor.php src/Controller/DocumentQueryController.php tests/Api/Entity/DocumentTest.php
git commit -m "feat(documents) : accept type on upload + expose in query controller + PATCH support"
```

---

### Task 4: Frontend — Type Constants + Document Interface

**Files:**
- Create: `frontend/app/shared/documentTypes.ts`
- Modify: `frontend/app/composables/useDocuments.ts:6-27` (Document interface), `useDocuments.ts:205-253` (upload)

- [ ] **Step 1: Create documentTypes.ts**

```typescript
// frontend/app/shared/documentTypes.ts
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']

export const getDocumentTypeLabel = (value: string): string => {
  const found = DOCUMENT_TYPES.find((t) => t.value === value)
  return found?.label ?? value
}
```

- [ ] **Step 2: Add type to Document interface and UploadContext**

In `useDocuments.ts`, add to `Document` interface after `downloadUrl`:

```typescript
type?: string
```

Add to `UploadContext` interface:

```typescript
type?: string
```

- [ ] **Step 3: Add type to uploadDocuments FormData**

In `uploadDocuments()`, after `formData.append('name', file.name)` (line ~220), add:

```typescript
if (context.type) formData.append('type', context.type)
```

- [ ] **Step 4: Add updateDocument method**

In `useDocuments()`, before the `return` block, add:

```typescript
const updateDocument = async (
  id: string,
  data: { name?: string; type?: string },
): Promise<DocumentResult> => {
  loading.value = true
  try {
    const result = await patch(`/documents/${id}`, data)
    if (result.success && result.data) {
      const updated = result.data as Document
      const index = documents.value.findIndex((doc) => doc.id === id)
      if (index !== -1) {
        documents.value[index] = { ...documents.value[index], ...updated }
      }
      showSuccess('Document mis à jour')
      return { success: true, data: updated }
    }
    if (result.error) showError(result.error)
    return result as DocumentResult
  } catch (error) {
    const err = error as Error
    showError('Impossible de mettre à jour le document')
    return { success: false, error: err.message }
  } finally {
    loading.value = false
  }
}
```

Add `patch` to the destructured `useApi()` call at the top of the composable:

```typescript
const { get, patch, postFormData, delete: del } = useApi()
```

Add `updateDocument` to the return object.

- [ ] **Step 5: Run lint**

Run: `cd frontend && npm run lint:fix`

- [ ] **Step 6: Commit frontend**

```bash
cd frontend
git add app/shared/documentTypes.ts app/composables/useDocuments.ts
git commit -m "feat(documents) : add document type constants and updateDocument method"
```

---

### Task 5: Frontend — DocumentUpload Type Select

**Files:**
- Modify: `frontend/app/components/DocumentUpload.vue`

- [ ] **Step 1: Add type prop and select to DocumentUpload**

Add prop:

```javascript
documentType: {
  type: String,
  default: 'documentation'
}
```

Add emit:

```javascript
'update:documentType'
```

Add a select dropdown in the template, before the file list (`<ul>`), after the button area:

```html
<div class="w-full max-w-xs mt-2">
  <label class="text-xs font-semibold uppercase tracking-wide text-base-content/70">
    Type de document
  </label>
  <select
    class="select select-bordered select-sm w-full mt-1"
    :value="documentType"
    @change="$emit('update:documentType', ($event.target as HTMLSelectElement).value)"
  >
    <option v-for="t in documentTypes" :key="t.value" :value="t.value">
      {{ t.label }}
    </option>
  </select>
</div>
```

Import the types:

```javascript
import { DOCUMENT_TYPES } from '~/shared/documentTypes'
const documentTypes = DOCUMENT_TYPES
```

Note: since DocumentUpload uses `<script setup>` without `lang="ts"`, use `@change="$emit('update:documentType', $event.target.value)"` (no cast).

- [ ] **Step 2: Run lint**

Run: `cd frontend && npm run lint:fix`

- [ ] **Step 3: Commit**

```bash
cd frontend
git add app/components/DocumentUpload.vue
git commit -m "feat(documents) : add type select to DocumentUpload component"
```

---

### Task 6: Frontend — DocumentEditModal

**Files:**
- Create: `frontend/app/components/DocumentEditModal.vue`

- [ ] **Step 1: Create DocumentEditModal component**

```vue
<template>
  <Teleport to="body">
    <div v-if="visible" class="modal modal-open" @click.self="$emit('close')">
      <div class="modal-box max-w-sm">
        <h3 class="font-bold text-lg mb-4">
          Modifier le document
        </h3>

        <div class="space-y-4">
          <label class="form-control w-full">
            <div class="label">
              <span class="label-text">Nom</span>
            </div>
            <input
              v-model="form.name"
              type="text"
              class="input input-bordered input-sm md:input-md w-full"
            >
          </label>

          <label class="form-control w-full">
            <div class="label">
              <span class="label-text">Type</span>
            </div>
            <select
              v-model="form.type"
              class="select select-bordered select-sm md:select-md w-full"
            >
              <option v-for="t in DOCUMENT_TYPES" :key="t.value" :value="t.value">
                {{ t.label }}
              </option>
            </select>
          </label>
        </div>

        <div class="modal-action">
          <button type="button" class="btn btn-ghost btn-sm md:btn-md" @click="$emit('close')">
            Annuler
          </button>
          <button
            type="button"
            class="btn btn-primary btn-sm md:btn-md"
            :disabled="saving"
            @click="save"
          >
            <span v-if="saving" class="loading loading-spinner loading-xs" />
            Sauvegarder
          </button>
        </div>
      </div>
    </div>
  </Teleport>
</template>

<script setup lang="ts">
import { reactive, watch, ref } from 'vue'
import { DOCUMENT_TYPES } from '~/shared/documentTypes'
import type { Document } from '~/composables/useDocuments'

const props = defineProps<{
  visible: boolean
  document: Document | null
}>()

const emit = defineEmits<{
  (e: 'close'): void
  (e: 'updated', data: { name: string; type: string }): void
}>()

const form = reactive({ name: '', type: 'documentation' })
const saving = ref(false)

watch(
  () => props.document,
  (doc) => {
    if (doc) {
      form.name = doc.name || ''
      form.type = doc.type || 'documentation'
    }
  },
  { immediate: true },
)

const save = () => {
  if (!form.name.trim()) return
  saving.value = true
  emit('updated', { name: form.name.trim(), type: form.type })
  saving.value = false
}
</script>
```

- [ ] **Step 2: Run lint**

Run: `cd frontend && npm run lint:fix`

- [ ] **Step 3: Commit**

```bash
cd frontend
git add app/components/DocumentEditModal.vue
git commit -m "feat(documents) : add DocumentEditModal component"
```

---

### Task 7: Frontend — DocumentListInline + Type Badge + Edit Button

**Files:**
- Modify: `frontend/app/components/common/DocumentListInline.vue`
- Modify: `frontend/app/composables/useEntityDocuments.ts`

- [ ] **Step 1: Add type badge and edit button to DocumentListInline**

In the template, after the document name `<div>` (line ~33-40), add a badge for the type:

```html
<span class="badge badge-sm badge-outline">{{ getDocumentTypeLabel(document.type || 'documentation') }}</span>
```

In the actions area (line ~42-68), add an edit button before "Consulter":

```html
<button
  v-if="canEdit"
  type="button"
  class="btn btn-ghost btn-xs"
  title="Modifier"
  @click="$emit('edit', document)"
>
  Modifier
</button>
```

Add props:

```typescript
canEdit?: boolean
```

Default: `false`

Add emit:

```typescript
(e: 'edit', document: Document): void
```

Add import:

```typescript
import { getDocumentTypeLabel } from '~/shared/documentTypes'
```

- [ ] **Step 2: Add updateDocument to useEntityDocuments**

In `useEntityDocuments.ts`, add `updateDocument` from useDocuments:

```typescript
const { uploadDocuments, deleteDocument, updateDocument } = useDocuments()
```

Add method:

```typescript
const editDocument = async (id: string, data: { name?: string; type?: string }) => {
  const result: any = await updateDocument(id, data)
  if (result.success) {
    const e = entity()
    const docs = e.documents || []
    const index = docs.findIndex((doc: any) => doc.id === id)
    if (index !== -1) {
      docs[index] = { ...docs[index], ...data }
    }
  }
  return result
}
```

Add `editDocument` to the return object.

- [ ] **Step 3: Run lint**

Run: `cd frontend && npm run lint:fix`

- [ ] **Step 4: Commit**

```bash
cd frontend
git add app/components/common/DocumentListInline.vue app/composables/useEntityDocuments.ts
git commit -m "feat(documents) : add type badge and edit button to DocumentListInline"
```

---

### Task 8: Frontend — Wire Edit Modal in Entity Pages

**Files:**
- Modify: `frontend/app/components/ComponentItem.vue`
- Modify: `frontend/app/components/PieceItem.vue`
- Modify: `frontend/app/pages/pieces/[id]/edit.vue`
- Modify: `frontend/app/pages/component/[id]/edit.vue`
- Modify: `frontend/app/pages/product/[id]/edit.vue`

- [ ] **Step 1: Wire in ComponentItem and PieceItem**

For each of `ComponentItem.vue` and `PieceItem.vue`:

1. Add `editDocument` from the `useEntityDocuments` return
2. Add state refs for the edit modal:
   ```typescript
   const editingDocument = ref<any>(null)
   const editModalVisible = ref(false)
   ```
3. Add handler:
   ```typescript
   const openEditModal = (doc: any) => {
     editingDocument.value = doc
     editModalVisible.value = true
   }
   const handleDocumentUpdated = async (data: { name: string; type: string }) => {
     if (!editingDocument.value?.id) return
     await editDocument(editingDocument.value.id, data)
     editModalVisible.value = false
     editingDocument.value = null
   }
   ```
4. Add `DocumentEditModal` in the template
5. Pass `:can-edit="isEditMode"` and `@edit="openEditModal"` to `DocumentListInline`

- [ ] **Step 2: Wire in edit pages (pieces/edit, component/edit, product/edit)**

Same pattern: add edit modal state, wire `DocumentListInline` with `:can-edit` and `@edit`, add `DocumentEditModal`.

- [ ] **Step 3: Wire type select in upload**

In pages that use `DocumentUpload`, add a `documentType` ref and pass it:
```html
<DocumentUpload
  v-model="selectedFiles"
  v-model:document-type="uploadDocType"
  ...
/>
```

Pass `type: uploadDocType.value` in the upload context when calling `handleFilesAdded` or `uploadDocuments`.

- [ ] **Step 4: Run lint + typecheck**

Run: `cd frontend && npm run lint:fix && npx nuxi typecheck`

- [ ] **Step 5: Commit**

```bash
cd frontend
git add app/components/ app/pages/
git commit -m "feat(documents) : wire DocumentEditModal and type select in all entity pages"
```

---

### Task 9: Frontend — Documents Global Page

**Files:**
- Modify: `frontend/app/pages/documents.vue`

- [ ] **Step 1: Add type column to DataTable**

In the `columns` array, add after `mimeType`:

```typescript
{ key: 'type', label: 'Type' },
```

Add the cell template:

```html
<template #cell-type="{ row }">
  <span class="badge badge-sm badge-outline">{{ getDocumentTypeLabel(row.type || 'documentation') }}</span>
</template>
```

- [ ] **Step 2: Add edit button + modal**

Add an edit button in the `#cell-actions` template slot:

```html
<button
  v-if="canEdit"
  class="btn btn-ghost btn-xs"
  type="button"
  @click="openEditModal(row)"
>
  Modifier
</button>
```

Add `DocumentEditModal` component in the template. Add the edit state + handler logic (same pattern as Task 8). Use `useDocuments().updateDocument` directly.

Import `usePermissions` to derive `canEdit` from the user's role (ROLE_GESTIONNAIRE or above).

- [ ] **Step 3: Add type filter**

Add a type filter select next to the existing "Rattachement" filter:

```html
<label class="text-xs font-semibold uppercase tracking-wide text-base-content/70" for="doc-type-filter">
  Type
</label>
<select
  id="doc-type-filter"
  v-model="typeFilter"
  class="select select-bordered select-sm"
  @change="table.handleFilterChange"
>
  <option value="all">Tous</option>
  <option v-for="t in DOCUMENT_TYPES" :key="t.value" :value="t.value">
    {{ t.label }}
  </option>
</select>
```

Pass `typeFilter` to `fetchDocuments` → `loadDocuments` as a new filter param, and in `useDocuments.loadDocuments` add `params.set('type', typeFilter)` when not `'all'`.

- [ ] **Step 4: Run lint + typecheck**

Run: `cd frontend && npm run lint:fix && npx nuxi typecheck`

- [ ] **Step 5: Commit**

```bash
cd frontend
git add app/pages/documents.vue app/composables/useDocuments.ts
git commit -m "feat(documents) : add type column, filter, and edit to documents page"
```

---

### Task 10: Final — Submodule Pointer + Verification

**Files:**
- Main repo: update submodule pointer

- [ ] **Step 1: Run full backend tests**

Run: `make test`
Expected: all tests pass

- [ ] **Step 2: Run full frontend checks**

Run: `cd frontend && npm run lint:fix && npx nuxi typecheck && npm run build`
Expected: 0 errors

- [ ] **Step 3: Manual verification**

1. Go to `/pieces/{id}/edit` — verify type badge on existing docs, edit modal works
2. Go to `/component/{id}/edit` — same verification
3. Upload a new document — verify type select appears, type is saved
4. Go to `/documents` — verify type column, filter, edit button
5. Check that existing PDFs show "Documentation", images show "Photo", others show "Autre"

- [ ] **Step 4: Commit submodule pointer**

```bash
cd /home/matthieu/dev_malio/Inventory
git add frontend
git commit -m "chore(submodule) : update frontend pointer (document types feature)"
```
