Files
Inventory_frontend/docs/plans/2026-03-08-reduce-files-under-500-lines.md

21 KiB
Raw Blame History

Reduce Frontend Files Under 500 Lines — Implementation Plan

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: Reduce all 14 frontend files currently over 500 lines to under 500 lines each, without changing any functionality.

Architecture: Extract shared UI sections into reusable components, split large composables/utilities into focused modules, and extract page-level script logic into dedicated composables. Each extraction is a pure refactor — no behavior changes.

Tech Stack: Vue 3 Composition API, TypeScript, Nuxt 4 (auto-imports for composables and components)


Inventory of files to reduce

# File Lines Target strategy
1 composables/useMachineDetailData.ts 1353 Split into 4 focused composables
2 components/StructureNodeEditor.vue 926 Extract type-map + sync logic into composable
3 pages/component/[id]/edit.vue 911 Extract shared component + composable
4 pages/component/create.vue 852 Extract structure assignment helpers
5 pages/pieces/[id]/edit.vue 821 Extract page composable
6 shared/model/componentStructure.ts 794 Split into 3 focused modules
7 components/PieceItem.vue 757 Extract document list + custom fields template
8 components/ComponentStructureAssignmentNode.vue 722 Extract fetch/options logic
9 pages/index.vue 584 Extract modal components
10 components/PieceModelStructureEditor.vue 578 Extract drag-reorder + field logic
11 components/model-types/ManagementView.vue 577 Extract related-items modal
12 components/ComponentItem.vue 573 Extract document list template
13 pages/product/[id]/edit.vue 570 Extract page composable
14 pages/pieces/create.vue 540 Extract product-selection logic

Shared extractions (do these FIRST — they reduce multiple files)

Task 1: Extract DocumentListInline.vue shared component

Rationale: The document list display (thumbnail + name + mimeType + size + Consulter/Télécharger/Supprimer buttons) is duplicated identically in:

  • PieceItem.vue (lines 401-477)
  • ComponentItem.vue (lines 312-379)
  • pages/component/[id]/edit.vue (lines 307-375)
  • pages/pieces/[id]/edit.vue (lines 254-325)
  • pages/product/[id]/edit.vue (lines 165-232)

Files:

  • Create: app/components/common/DocumentListInline.vue
  • Modify: all 5 files above

Step 1: Create DocumentListInline.vue

<template>
  <div v-if="documents.length" class="space-y-2">
    <div
      v-for="document in documents"
      :key="document.id || document.path || document.name"
      class="flex items-center justify-between rounded border border-base-200 bg-base-100 px-3 py-2"
    >
      <div class="flex items-center gap-3 text-sm">
        <div
          class="flex-shrink-0 overflow-hidden rounded-md border border-base-200 bg-base-200/70 flex items-center justify-center"
          :class="documentThumbnailClass(document)"
        >
          <img
            v-if="isImageDocument(document) && (document.fileUrl || document.path)"
            :src="document.fileUrl || document.path"
            class="h-full w-full object-cover"
            :alt="`Aperçu de ${document.name}`"
          >
          <iframe
            v-else-if="shouldInlinePdf(document)"
            :src="documentPreviewSrc(document)"
            class="h-full w-full border-0 bg-white"
            title="Aperçu PDF"
          />
          <component
            v-else
            :is="documentIcon(document).component"
            class="h-6 w-6"
            :class="documentIcon(document).colorClass"
            aria-hidden="true"
          />
        </div>
        <div>
          <div class="font-medium">{{ document.name }}</div>
          <div class="text-xs text-base-content/70">
            {{ document.mimeType || 'Inconnu' }}  {{ formatSize(document.size) }}
          </div>
        </div>
      </div>
      <div class="flex items-center gap-2">
        <button
          type="button"
          class="btn btn-ghost btn-xs"
          :disabled="!canPreviewDocument(document)"
          :title="canPreviewDocument(document) ? 'Consulter le document' : 'Aucun aperçu disponible pour ce type'"
          @click="$emit('preview', document)"
        >
          Consulter
        </button>
        <button type="button" class="btn btn-ghost btn-xs" @click="downloadDocument(document)">
          Télécharger
        </button>
        <button
          v-if="canDelete"
          type="button"
          class="btn btn-error btn-xs"
          :disabled="deleteDisabled"
          @click="$emit('delete', document.id)"
        >
          Supprimer
        </button>
      </div>
    </div>
  </div>
  <p v-else class="text-xs text-base-content/70">
    {{ emptyText }}
  </p>
</template>

<script setup lang="ts">
import { canPreviewDocument, isImageDocument } from '~/utils/documentPreview'
import {
  documentIcon,
  formatSize,
  shouldInlinePdf,
  documentPreviewSrc,
  documentThumbnailClass,
  downloadDocument,
} from '~/shared/utils/documentDisplayUtils'

withDefaults(defineProps<{
  documents: any[]
  canDelete?: boolean
  deleteDisabled?: boolean
  emptyText?: string
}>(), {
  canDelete: false,
  deleteDisabled: false,
  emptyText: 'Aucun document.',
})

defineEmits<{
  (e: 'preview', document: any): void
  (e: 'delete', documentId: string): void
}>()
</script>

Step 2: Run lint

Run: cd Inventory_frontend && npm run lint:fix

Step 3: Replace document list in each of the 5 files

In each file, replace the v-for document list block with:

<DocumentListInline
  :documents="xxxDocuments"
  :can-delete="canEdit || isEditMode"
  :delete-disabled="uploadingDocuments"
  :empty-text="'Aucun document lié à cet élément.'"
  @preview="openPreview"
  @delete="removeDocument"
/>

Remove the now-unused imports (documentIcon, formatSize, shouldInlinePdf, etc.) from each file.

Step 4: Run lint + typecheck

Run: cd Inventory_frontend && npm run lint:fix && npx nuxi typecheck

Step 5: Commit

git add app/components/common/DocumentListInline.vue app/components/PieceItem.vue app/components/ComponentItem.vue app/pages/component/\[id\]/edit.vue app/pages/pieces/\[id\]/edit.vue app/pages/product/\[id\]/edit.vue
git commit -m "refactor(frontend) : extract DocumentListInline shared component"

Expected savings: ~60 lines per file × 5 files = ~300 lines total


Task 2: Extract StructureSkeletonPreview.vue shared component

Rationale: The "Squelette sélectionné" details section (collapsible, shows custom fields / pieces / products / subcomponents) is duplicated in:

  • pages/component/[id]/edit.vue (lines 141-225)
  • pages/component/create.vue (lines 112-189)
  • pages/pieces/[id]/edit.vue (lines 185-216)
  • pages/pieces/create.vue (lines 156-187)

Files:

  • Create: app/components/common/StructureSkeletonPreview.vue
  • Modify: all 4 pages above

Step 1: Create the component

Extract the common <details> collapse + custom fields list + pieces list + products list + subcomponents list into a single component with props:

  • structure — the normalized structure object
  • description — optional description text
  • previewBadge — the badge text (e.g., from formatStructurePreview)
  • pieceTypeLabelMap, productTypeLabelMap — for label resolution (component pages only)
  • variant'component' or 'piece' to control which sections display

Step 2: Replace in each page

Step 3: Run lint + typecheck

Step 4: Commit

git commit -m "refactor(frontend) : extract StructureSkeletonPreview shared component"

Expected savings: ~50 lines per file × 4 files = ~200 lines total


Task 3: Split shared/model/componentStructure.ts (794 lines → 3 files)

Files:

  • Create: app/shared/model/componentStructureSanitize.ts
  • Create: app/shared/model/componentStructureHydrate.ts
  • Modify: app/shared/model/componentStructure.ts (keep only normalize + format + extract)

Step 1: Create componentStructureSanitize.ts

Move these functions (lines 88-362):

  • sanitizeCustomFields
  • sanitizePieces
  • sanitizeProducts
  • sanitizeSubcomponents (make it exported)
  • Helper: extractFieldValueObject, toStringArray

~275 lines → new file

Step 2: Create componentStructureHydrate.ts

Move these functions (lines 364-495, 654-739):

  • hydrateCustomFields
  • hydratePieces
  • hydrateProducts
  • hydrateSubcomponents
  • mapComponentCustomFields
  • mapComponentPieces
  • mapComponentProducts
  • mapSubcomponents

~250 lines → new file

Step 3: Update componentStructure.ts

Keep only:

  • isPlainObject, ModelStructurePreview, defaultStructure, ensureStructureShape, cloneStructure
  • normalizeStructureForEditor, normalizeStructureForSave
  • hydrateStructureForEditor, extractStructureFromComponent
  • computeStructureStats, formatStructurePreview

Import sanitize/hydrate functions from the new files. File should end up ~270 lines.

Step 4: Verify all imports across the codebase still work

Run: cd Inventory_frontend && npx nuxi typecheck

Step 5: Commit

git commit -m "refactor(frontend) : split componentStructure.ts into focused modules"

Task 4: Split composables/useMachineDetailData.ts (1353 lines → 4 composables)

Files:

  • Create: app/composables/useMachineDetailDocuments.ts (~200 lines)
  • Create: app/composables/useMachineDetailCustomFields.ts (~150 lines)
  • Create: app/composables/useMachineDetailHierarchy.ts (~200 lines)
  • Create: app/composables/useMachineDetailProducts.ts (~150 lines)
  • Modify: app/composables/useMachineDetailData.ts (should end up ~400 lines)

Step 1: Identify extraction boundaries

Read the full file and map which functions/refs belong to which domain:

  • Documents: document loading, upload, delete, preview state
  • Custom fields: custom field value management, display logic
  • Hierarchy: machine hierarchy building, component/piece tree resolution
  • Products: product display, resolution, supplier info

Step 2: Extract useMachineDetailDocuments.ts

Move all document-related refs, functions, and watchers. The composable accepts machineId and returns { documents, loadDocuments, uploadDocuments, ... }.

Step 3: Extract useMachineDetailCustomFields.ts

Move custom field resolution, display filtering, and update logic.

Step 4: Extract useMachineDetailHierarchy.ts

Move buildMachineHierarchyFromLinks usage, component/piece tree construction.

Step 5: Extract useMachineDetailProducts.ts

Move product display resolution, supplier info formatting.

Step 6: Update useMachineDetailData.ts

Import and compose the 4 sub-composables. Keep only the orchestration logic (data loading sequence, top-level state).

Step 7: Run lint + typecheck

Step 8: Commit

git commit -m "refactor(frontend) : split useMachineDetailData into focused composables"

Task 5: Extract composable from StructureNodeEditor.vue (926 → <500)

Files:

  • Create: app/composables/useStructureNodeLogic.ts
  • Modify: app/components/StructureNodeEditor.vue

Step 1: Create useStructureNodeLogic.ts

Extract from the <script> section (lines 358-926):

  • Type maps (componentTypeMap, pieceTypeMap, productTypeMap) and label getters
  • Sync functions (syncComponentType, updatePieceTypeLabel, updateProductTypeLabel, syncPieceLabels, syncProductLabels)
  • Handler functions (handleComponentTypeSelect, handlePieceTypeSelect, handleProductTypeSelect)
  • CRUD functions (addCustomField, removeCustomField, addPiece, removePiece, addProduct, removeProduct, addSubComponent, removeSubComponent)
  • Lock state (initialCustomFieldIndices, etc., isCustomFieldLocked, etc.)
  • All watchers

The composable signature:

export function useStructureNodeLogic(props: { node: ..., componentTypes: ..., ... }) {
  // ... all the extracted logic
  return { ... }
}

Step 2: Update StructureNodeEditor.vue

Keep only the <template> (356 lines — already under 500 on its own) + thin <script> that calls the composable.

Step 3: Run lint + typecheck

Step 4: Commit

git commit -m "refactor(frontend) : extract StructureNodeEditor logic into composable"

Task 6: Extract composable from pages/component/[id]/edit.vue (911 → <500)

After Task 1 (DocumentListInline) and Task 2 (StructureSkeletonPreview), this file will be ~750 lines. Still needs ~250 lines extracted.

Files:

  • Create: app/composables/useComponentEdit.ts
  • Modify: app/pages/component/[id]/edit.vue

Step 1: Create useComponentEdit.ts

Extract:

  • All state declarations (refs, reactive)
  • fetchComponent, refreshDocuments, refreshCustomFieldInputs
  • collectStructureSelections function (lines 802-879 — 77 lines alone)
  • submitEdition function
  • All watchers
  • Type label maps and catalog maps

Step 2: Update the page to use the composable

Step 3: Run lint + typecheck

Step 4: Commit

git commit -m "refactor(frontend) : extract component edit page logic into composable"

Task 7: Extract composable from pages/component/create.vue (852 → <500)

After Task 2 (StructureSkeletonPreview), ~800 lines remain.

Files:

  • Create: app/composables/useComponentCreate.ts
  • Modify: app/pages/component/create.vue

Step 1: Create useComponentCreate.ts

Extract:

  • Structure assignment building functions (extractSubcomponents, extractPiecesFromNode, extractProductsFromNode, buildAssignmentNode, initializeStructureAssignments, hasAssignments, isAssignmentNodeComplete)
  • Serialization functions (stripNullish, sanitizeStructureDefinition, sanitizePieceDefinition, sanitizeProductDefinition, serializeStructureAssignments)
  • submitCreation function
  • State management and watchers

Step 2: Update the page

Step 3: Run lint + typecheck

Step 4: Commit

git commit -m "refactor(frontend) : extract component create page logic into composable"

Task 8: Extract composable from pages/pieces/[id]/edit.vue (821 → <500)

After Task 1 and Task 2, ~650 lines remain.

Files:

  • Create: app/composables/usePieceEdit.ts
  • Modify: app/pages/pieces/[id]/edit.vue

Step 1: Create usePieceEdit.ts

Extract:

  • Product selection logic (describeProductRequirement, productRequirementEntries, productSelectionsFilled, ensureProductSelections, setProductSelection)
  • fetchPiece, loadPieceTypeDetailsFromCache, submitEdition
  • State refs and watchers

Step 2: Update the page

Step 3: Run lint + typecheck

Step 4: Commit

git commit -m "refactor(frontend) : extract piece edit page logic into composable"

Task 9: Reduce PieceItem.vue (757 → <500)

After Task 1 (DocumentListInline), ~680 lines remain. The custom field rendering template (lines 236-373) is ~140 lines.

Files:

  • Create: app/components/common/CustomFieldDisplay.vue (~140 lines)
  • Modify: app/components/PieceItem.vue

Step 1: Create CustomFieldDisplay.vue

Extract the custom field edit/display template section. Props: fields, isEditMode, plus events for update and blur.

Step 2: Replace in PieceItem.vue and ComponentItem.vue

Both components have this same custom field display pattern.

Step 3: Run lint + typecheck

Step 4: Commit

git commit -m "refactor(frontend) : extract CustomFieldDisplay shared component"

Expected savings: ~130 lines from PieceItem, ~70 lines from ComponentItem


Task 10: Extract composable from ComponentStructureAssignmentNode.vue (722 → <500)

Files:

  • Create: app/composables/useStructureAssignmentFetch.ts
  • Modify: app/components/ComponentStructureAssignmentNode.vue

Step 1: Create useStructureAssignmentFetch.ts

Extract:

  • All fetch functions (fetchComponentOptions, fetchPieceOptions, fetchProductOptions)
  • Option getters (getPieceOptions, getProductOptions, componentOptions)
  • Loading state maps (pieceLoadingByPath, productLoadingByPath, componentLoadingByPath)
  • Options-by-path state maps
  • Label/description helper functions (describePieceRequirement, describeProductRequirement, etc.)
  • All watchers

Step 2: Update the component

Step 3: Run lint + typecheck

Step 4: Commit

git commit -m "refactor(frontend) : extract assignment fetch logic into composable"

Task 11: Extract modals from pages/index.vue (584 → <500)

Files:

  • Create: app/components/home/AddSiteModal.vue (~50 lines)
  • Create: app/components/home/AddMachineModal.vue (~70 lines)
  • Modify: app/pages/index.vue

Step 1: Extract AddSiteModal.vue

Move lines 261-297 (site modal template + form). Props: open, disabled. Events: close, create.

Step 2: Extract AddMachineModal.vue

Move lines 300-368 (machine modal template + form). Props: open, sites, disabled. Events: close, create.

Step 3: Update index.vue to use the components

Move newSite and newMachine reactive objects into the modals.

Step 4: Run lint + typecheck

Step 5: Commit

git commit -m "refactor(frontend) : extract home page modals into components"

Task 12: Reduce PieceModelStructureEditor.vue (578 → <500)

After Task 9 (if useDragReorder composable is already available), the drag logic is already minimal. The remaining bulk is field/product CRUD.

Files:

  • Create: app/composables/usePieceStructureEditorLogic.ts
  • Modify: app/components/PieceModelStructureEditor.vue

Step 1: Create usePieceStructureEditorLogic.ts

Extract:

  • hydrateFields, hydrateProducts, toEditorField, toEditorProduct
  • buildPayload, serializeStructure, emitUpdate
  • normalizeProductEntry, product type metadata updates
  • Drag state and drag functions

Step 2: Update the component

Step 3: Run lint + typecheck

Step 4: Commit

git commit -m "refactor(frontend) : extract PieceModelStructureEditor logic into composable"

Files:

  • Create: app/components/model-types/RelatedItemsModal.vue (~100 lines)
  • Modify: app/components/model-types/ManagementView.vue

Step 1: Create RelatedItemsModal.vue

Move the related items modal template (lines 109-161) and its logic (relatedModalOpen, relatedItems, relatedLoading, relatedError, loadRelatedItems, etc.).

Props: open, modelType. Events: close, open-edit.

Step 2: Update ManagementView.vue

Step 3: Run lint + typecheck

Step 4: Commit

git commit -m "refactor(frontend) : extract RelatedItemsModal from ManagementView"

Task 14: Reduce ComponentItem.vue (573 → <500)

After Task 1 (DocumentListInline ~60 lines) and Task 9 (CustomFieldDisplay ~70 lines), the file drops to ~443 lines. Already under 500 — no further action needed.


Task 15: Reduce pages/product/[id]/edit.vue (570 → <500)

After Task 1 (DocumentListInline ~60 lines), drops to ~510. Need ~10 more lines extracted.

Files:

  • Modify: app/pages/product/[id]/edit.vue

Step 1: Extract loadProductType and hydrateForm into a small composable or inline

Move loadProductType (lines 462-488) and hydrateForm (lines 490-508) into a shared useProductEdit.ts if beneficial, or just use Task 1 savings which may be enough.

Step 2: Run lint + typecheck

Step 3: Commit (if changes needed)


Task 16: Reduce pages/pieces/create.vue (540 → <500)

After Task 2 (StructureSkeletonPreview ~30 lines), drops to ~510. Need ~10 more lines.

Files:

  • Modify: app/pages/pieces/create.vue

Step 1: Extract product selection logic

The describeProductRequirement, productRequirementDescriptions, productRequirementEntries, productSelectionsFilled, ensureProductSelections, setProductSelection block (lines 343-398, ~55 lines) is duplicated with pages/pieces/[id]/edit.vue.

Extract into app/shared/utils/pieceProductSelectionUtils.ts.

Step 2: Update both piece pages to import from shared utils

Step 3: Run lint + typecheck

Step 4: Commit

git commit -m "refactor(frontend) : extract shared piece product selection utils"

Execution order (dependencies)

  1. Task 1 (DocumentListInline) — no deps, reduces 5 files
  2. Task 2 (StructureSkeletonPreview) — no deps, reduces 4 files
  3. Task 9 (CustomFieldDisplay) — no deps, reduces PieceItem + ComponentItem
  4. Task 3 (componentStructure.ts split) — no deps
  5. Task 4 (useMachineDetailData split) — no deps
  6. Task 5 (StructureNodeEditor) — no deps
  7. Task 10 (ComponentStructureAssignmentNode) — no deps
  8. Task 11 (index.vue modals) — no deps
  9. Task 12 (PieceModelStructureEditor) — no deps
  10. Task 13 (ManagementView) — no deps
  11. Task 16 (pieces/create.vue) — no deps
  12. Task 6 (component/edit) — after Tasks 1, 2
  13. Task 7 (component/create) — after Task 2
  14. Task 8 (pieces/edit) — after Tasks 1, 2
  15. Task 15 (product/edit) — after Task 1
  16. Task 14 (ComponentItem) — verify after Tasks 1, 9

Tasks 1-11 are independent and can be parallelized via subagents (pairs that don't touch the same files).