refactor : merge Inventory_frontend submodule into frontend/ directory
Merges the full git history of Inventory_frontend into the monorepo under frontend/. Removes the submodule in favor of a unified repo. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
262
frontend/app/components/ComponentStructureAssignmentNode.vue
Normal file
262
frontend/app/components/ComponentStructureAssignmentNode.vue
Normal file
@@ -0,0 +1,262 @@
|
||||
<template>
|
||||
<div :class="wrapperClass">
|
||||
<section v-if="!isRoot" class="rounded-lg border border-base-200 bg-base-100 p-4 space-y-3">
|
||||
<div class="space-y-1">
|
||||
<h4 class="text-sm font-semibold text-base-content">
|
||||
{{ requirementLabel }}
|
||||
</h4>
|
||||
<p class="text-xs text-base-content/70">
|
||||
{{ requirementDescription }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text text-xs">Sélectionner un composant</span>
|
||||
</label>
|
||||
<SearchSelect
|
||||
:model-value="assignment.selectedComponentId || ''"
|
||||
:options="componentOptions"
|
||||
:loading="componentsLoading || componentLoadingByPath[assignment.path]"
|
||||
size="sm"
|
||||
placeholder="Rechercher un composant..."
|
||||
:empty-text="componentOptions.length ? 'Aucun résultat' : 'Aucun composant disponible'"
|
||||
:option-label="componentOptionLabel"
|
||||
:option-description="componentOptionDescription"
|
||||
server-search
|
||||
@search="fetchComponentOptions"
|
||||
@update:modelValue="(value) => { assignment.selectedComponentId = normalizeSelectionValue(value); }"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="assignment.pieces.length" class="rounded-lg border border-dashed border-base-300 bg-base-200/40 p-4 space-y-4">
|
||||
<header class="space-y-1">
|
||||
<h4 class="text-sm font-semibold text-base-content">
|
||||
{{ isRoot ? 'Pièces requises par le squelette' : 'Pièces associées à ce sous-composant' }}
|
||||
</h4>
|
||||
<p class="text-xs text-base-content/70">
|
||||
Sélectionnez les pièces concrètes à associer pour chaque emplacement.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div
|
||||
v-for="pieceAssignment in assignment.pieces"
|
||||
:key="pieceAssignment.path"
|
||||
class="rounded-md border border-base-200 bg-base-100 p-3 space-y-2"
|
||||
>
|
||||
<div class="space-y-1">
|
||||
<p class="text-xs font-medium text-base-content">
|
||||
{{ describePieceRequirement(pieceAssignment) }}
|
||||
</p>
|
||||
<p v-if="!getPieceOptions(pieceAssignment).length" class="text-[11px] text-error">
|
||||
Aucune pièce disponible pour cette famille.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<SearchSelect
|
||||
:model-value="pieceAssignment.selectedPieceId || ''"
|
||||
:options="getPieceOptions(pieceAssignment)"
|
||||
:loading="piecesLoading || pieceLoadingByPath[pieceAssignment.path]"
|
||||
size="xs"
|
||||
placeholder="Rechercher une pièce..."
|
||||
:empty-text="getPieceOptions(pieceAssignment).length ? 'Aucun résultat' : 'Aucune pièce disponible'"
|
||||
:option-label="pieceOptionLabel"
|
||||
:option-description="pieceOptionDescription"
|
||||
server-search
|
||||
@search="(term) => fetchPieceOptions(pieceAssignment, term)"
|
||||
@update:modelValue="(value) => { pieceAssignment.selectedPieceId = normalizeSelectionValue(value); }"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="assignment.products.length" class="rounded-lg border border-dashed border-base-300 bg-base-200/40 p-4 space-y-4">
|
||||
<header class="space-y-1">
|
||||
<h4 class="text-sm font-semibold text-base-content">
|
||||
{{ isRoot ? 'Produits requis par le squelette' : 'Produits associés à ce sous-composant' }}
|
||||
</h4>
|
||||
<p class="text-xs text-base-content/70">
|
||||
Sélectionnez les produits catalogue à lier sur chaque position définie.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div
|
||||
v-for="productAssignment in assignment.products"
|
||||
:key="productAssignment.path"
|
||||
class="rounded-md border border-base-200 bg-base-100 p-3 space-y-2"
|
||||
>
|
||||
<div class="space-y-1">
|
||||
<p class="text-xs font-medium text-base-content">
|
||||
{{ describeProductRequirement(productAssignment) }}
|
||||
</p>
|
||||
<p v-if="!getProductOptions(productAssignment).length" class="text-[11px] text-error">
|
||||
Aucun produit disponible pour cette catégorie.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<SearchSelect
|
||||
:model-value="productAssignment.selectedProductId || ''"
|
||||
:options="getProductOptions(productAssignment)"
|
||||
:loading="productsLoading || productLoadingByPath[productAssignment.path]"
|
||||
size="xs"
|
||||
placeholder="Rechercher un produit..."
|
||||
:empty-text="getProductOptions(productAssignment).length ? 'Aucun résultat' : 'Aucun produit disponible'"
|
||||
:option-label="productOptionLabel"
|
||||
:option-description="productOptionDescription"
|
||||
server-search
|
||||
@search="(term) => fetchProductOptions(productAssignment, term)"
|
||||
@update:modelValue="(value) => { productAssignment.selectedProductId = normalizeSelectionValue(value); }"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="assignment.subcomponents.length" class="space-y-4">
|
||||
<header class="space-y-1">
|
||||
<h4 class="text-sm font-semibold text-base-content">
|
||||
{{ isRoot ? 'Sous-composants définis par le squelette' : 'Sous-composants imbriqués' }}
|
||||
</h4>
|
||||
<p class="text-xs text-base-content/70">
|
||||
Choisissez un composant existant pour chaque sous-niveau requis.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<ComponentStructureAssignmentNode
|
||||
v-for="subAssignment in assignment.subcomponents"
|
||||
:key="subAssignment.path"
|
||||
:assignment="subAssignment"
|
||||
:pieces="pieces"
|
||||
:products="products"
|
||||
:components="components"
|
||||
:components-loading="componentsLoading"
|
||||
:pieces-loading="piecesLoading"
|
||||
:products-loading="productsLoading"
|
||||
:depth="depth + 1"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import SearchSelect from '~/components/common/SearchSelect.vue';
|
||||
import { useStructureAssignmentFetch } from '~/composables/useStructureAssignmentFetch';
|
||||
import type {
|
||||
ComponentOption,
|
||||
PieceOption,
|
||||
ProductOption,
|
||||
} from '~/composables/useStructureAssignmentFetch';
|
||||
|
||||
export type {
|
||||
StructureAssignmentNode,
|
||||
StructurePieceAssignment,
|
||||
StructureProductAssignment,
|
||||
} from '~/composables/useStructureAssignmentFetch';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
assignment: import('~/composables/useStructureAssignmentFetch').StructureAssignmentNode;
|
||||
pieces: PieceOption[] | null;
|
||||
products: ProductOption[] | null;
|
||||
components: ComponentOption[] | null;
|
||||
depth?: number;
|
||||
componentsLoading?: boolean;
|
||||
piecesLoading?: boolean;
|
||||
productsLoading?: boolean;
|
||||
pieceTypeLabelMap?: Record<string, string>;
|
||||
productTypeLabelMap?: Record<string, string>;
|
||||
componentTypeLabelMap?: Record<string, string>;
|
||||
}>(),
|
||||
{
|
||||
depth: 0,
|
||||
pieces: () => [],
|
||||
products: () => [],
|
||||
components: () => [],
|
||||
componentsLoading: false,
|
||||
piecesLoading: false,
|
||||
productsLoading: false,
|
||||
pieceTypeLabelMap: () => ({}),
|
||||
productTypeLabelMap: () => ({}),
|
||||
componentTypeLabelMap: () => ({}),
|
||||
},
|
||||
);
|
||||
|
||||
const depth = computed(() => props.depth ?? 0);
|
||||
const isRoot = computed(() => depth.value === 0);
|
||||
|
||||
const wrapperClass = computed(() =>
|
||||
depth.value === 0 ? 'space-y-6' : 'space-y-6 border-l border-base-300 pl-4',
|
||||
);
|
||||
|
||||
const {
|
||||
pieceLoadingByPath,
|
||||
productLoadingByPath,
|
||||
componentLoadingByPath,
|
||||
componentOptions,
|
||||
componentOptionLabel,
|
||||
componentOptionDescription,
|
||||
fetchComponentOptions,
|
||||
getPieceOptions,
|
||||
pieceOptionLabel,
|
||||
pieceOptionDescription,
|
||||
fetchPieceOptions,
|
||||
describePieceRequirement,
|
||||
getProductOptions,
|
||||
productOptionLabel,
|
||||
productOptionDescription,
|
||||
fetchProductOptions,
|
||||
describeProductRequirement,
|
||||
} = useStructureAssignmentFetch({
|
||||
assignment: props.assignment,
|
||||
pieces: props.pieces,
|
||||
products: props.products,
|
||||
components: props.components,
|
||||
isRoot: () => isRoot.value,
|
||||
pieceTypeLabelMap: props.pieceTypeLabelMap ?? {},
|
||||
productTypeLabelMap: props.productTypeLabelMap ?? {},
|
||||
componentTypeLabelMap: props.componentTypeLabelMap ?? {},
|
||||
});
|
||||
|
||||
const normalizeSelectionValue = (value: unknown) => {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return '';
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === 'number') {
|
||||
return String(value);
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const requirementLabel = computed(() => {
|
||||
const definition = props.assignment.definition || {};
|
||||
const alias = definition.alias || definition.typeComposantLabel;
|
||||
if (alias) {
|
||||
return alias;
|
||||
}
|
||||
if (definition.typeComposantId && props.componentTypeLabelMap[definition.typeComposantId]) {
|
||||
return props.componentTypeLabelMap[definition.typeComposantId];
|
||||
}
|
||||
if (definition.typeComposant?.name) {
|
||||
return definition.typeComposant.name;
|
||||
}
|
||||
if (definition.familyCode) {
|
||||
return `Famille ${definition.familyCode}`;
|
||||
}
|
||||
return 'Sous-composant';
|
||||
});
|
||||
|
||||
const requirementDescription = computed(() => {
|
||||
const definition = props.assignment.definition || {};
|
||||
const family =
|
||||
definition.typeComposantLabel
|
||||
|| (definition.typeComposantId ? props.componentTypeLabelMap[definition.typeComposantId] : null)
|
||||
|| definition.typeComposant?.name
|
||||
|| definition.familyCode;
|
||||
if (family) {
|
||||
return `Doit appartenir à la famille "${family}".`;
|
||||
}
|
||||
return 'Sélectionnez un composant enfant conforme à cette position.';
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user