refactor(frontend) : extract assignment fetch logic into composable
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -134,76 +134,24 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
import SearchSelect from '~/components/common/SearchSelect.vue';
|
||||
import { useApi } from '~/composables/useApi';
|
||||
import { extractCollection } from '~/shared/utils/apiHelpers';
|
||||
import { useStructureAssignmentFetch } from '~/composables/useStructureAssignmentFetch';
|
||||
import type {
|
||||
ComponentModelPiece,
|
||||
ComponentModelProduct,
|
||||
ComponentModelStructureNode,
|
||||
} from '~/shared/types/inventory';
|
||||
ComponentOption,
|
||||
PieceOption,
|
||||
ProductOption,
|
||||
} from '~/composables/useStructureAssignmentFetch';
|
||||
|
||||
interface ComponentOption {
|
||||
id: string;
|
||||
name?: string | null;
|
||||
reference?: string | null;
|
||||
typeComposantId?: string | null;
|
||||
typeComposant?: {
|
||||
id: string;
|
||||
name?: string | null;
|
||||
code?: string | null;
|
||||
} | null;
|
||||
}
|
||||
|
||||
interface PieceOption {
|
||||
id: string;
|
||||
name?: string | null;
|
||||
reference?: string | null;
|
||||
typePieceId?: string | null;
|
||||
typePiece?: {
|
||||
id: string;
|
||||
name?: string | null;
|
||||
code?: string | null;
|
||||
} | null;
|
||||
}
|
||||
|
||||
interface ProductOption {
|
||||
id: string;
|
||||
name?: string | null;
|
||||
reference?: string | null;
|
||||
typeProductId?: string | null;
|
||||
typeProduct?: {
|
||||
id: string;
|
||||
name?: string | null;
|
||||
code?: string | null;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface StructurePieceAssignment {
|
||||
path: string;
|
||||
definition: ComponentModelPiece;
|
||||
selectedPieceId: string;
|
||||
}
|
||||
|
||||
export interface StructureProductAssignment {
|
||||
path: string;
|
||||
definition: ComponentModelProduct;
|
||||
selectedProductId: string;
|
||||
}
|
||||
|
||||
export interface StructureAssignmentNode {
|
||||
path: string;
|
||||
definition: ComponentModelStructureNode;
|
||||
selectedComponentId: string;
|
||||
pieces: StructurePieceAssignment[];
|
||||
products: StructureProductAssignment[];
|
||||
subcomponents: StructureAssignmentNode[];
|
||||
}
|
||||
export type {
|
||||
StructureAssignmentNode,
|
||||
StructurePieceAssignment,
|
||||
StructureProductAssignment,
|
||||
} from '~/composables/useStructureAssignmentFetch';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
assignment: StructureAssignmentNode;
|
||||
assignment: import('~/composables/useStructureAssignmentFetch').StructureAssignmentNode;
|
||||
pieces: PieceOption[] | null;
|
||||
products: ProductOption[] | null;
|
||||
components: ComponentOption[] | null;
|
||||
@@ -236,331 +184,46 @@ const wrapperClass = computed(() =>
|
||||
depth.value === 0 ? 'space-y-6' : 'space-y-6 border-l border-base-300 pl-4',
|
||||
);
|
||||
|
||||
const { get } = useApi();
|
||||
const pieceOptionsByPath = ref<Record<string, PieceOption[]>>({});
|
||||
const productOptionsByPath = ref<Record<string, ProductOption[]>>({});
|
||||
const componentOptionsByPath = ref<Record<string, ComponentOption[]>>({});
|
||||
const pieceLoadingByPath = ref<Record<string, boolean>>({});
|
||||
const productLoadingByPath = ref<Record<string, boolean>>({});
|
||||
const componentLoadingByPath = ref<Record<string, boolean>>({});
|
||||
|
||||
const setLoading = (target: Record<string, boolean>, key: string, value: boolean) => {
|
||||
target[key] = value;
|
||||
};
|
||||
|
||||
const componentOptions = computed(() => {
|
||||
if (isRoot.value) {
|
||||
return [];
|
||||
}
|
||||
const cached = componentOptionsByPath.value[props.assignment.path];
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const definition = props.assignment.definition || {};
|
||||
const requiredTypeId =
|
||||
definition.typeComposantId || definition.modelId || null;
|
||||
const requiredFamilyCode = definition.familyCode || null;
|
||||
|
||||
return (props.components || []).filter((component) => {
|
||||
if (!component || typeof component !== 'object') {
|
||||
return false;
|
||||
}
|
||||
if (requiredTypeId) {
|
||||
return component.typeComposantId === requiredTypeId;
|
||||
}
|
||||
if (requiredFamilyCode) {
|
||||
return (
|
||||
component.typeComposant?.code === requiredFamilyCode ||
|
||||
component.typeComposantId === requiredFamilyCode
|
||||
);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
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 componentOptionLabel = (component?: ComponentOption | null) => {
|
||||
if (!component) {
|
||||
return 'Composant sans nom';
|
||||
}
|
||||
return component.name || 'Composant sans nom';
|
||||
};
|
||||
|
||||
const componentOptionDescription = (component?: ComponentOption | null) => {
|
||||
if (!component) {
|
||||
const normalizeSelectionValue = (value: unknown) => {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return '';
|
||||
}
|
||||
const parts: string[] = [];
|
||||
const typeLabel =
|
||||
component.typeComposant?.name || component.typeComposant?.code || null;
|
||||
if (typeLabel) {
|
||||
parts.push(typeLabel);
|
||||
if (typeof value === 'string') {
|
||||
return value;
|
||||
}
|
||||
if (component.reference) {
|
||||
parts.push(`Ref. ${component.reference}`);
|
||||
if (typeof value === 'number') {
|
||||
return String(value);
|
||||
}
|
||||
return parts.join(' • ');
|
||||
};
|
||||
|
||||
const typeIri = (id: string) => `/api/model_types/${id}`;
|
||||
const primedPiecePaths = new Set<string>();
|
||||
const primedProductPaths = new Set<string>();
|
||||
const primedComponentPaths = new Set<string>();
|
||||
|
||||
const fetchComponentOptions = async (term = '') => {
|
||||
if (isRoot.value) {
|
||||
return;
|
||||
}
|
||||
const key = props.assignment.path;
|
||||
if (componentLoadingByPath.value[key]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const definition = props.assignment.definition || {};
|
||||
const requiredTypeId =
|
||||
definition.typeComposantId || definition.modelId || definition.typeComposant?.id || null;
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.set('itemsPerPage', '50');
|
||||
if (term.trim()) {
|
||||
params.set('name', term.trim());
|
||||
}
|
||||
if (requiredTypeId) {
|
||||
params.set('typeComposant', typeIri(requiredTypeId));
|
||||
}
|
||||
|
||||
setLoading(componentLoadingByPath.value, key, true);
|
||||
try {
|
||||
const result = await get(`/composants?${params.toString()}`);
|
||||
if (result.success) {
|
||||
componentOptionsByPath.value[key] = extractCollection(result.data);
|
||||
}
|
||||
} finally {
|
||||
setLoading(componentLoadingByPath.value, key, false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchPieceOptions = async (assignment: StructurePieceAssignment, term = '') => {
|
||||
const key = assignment.path;
|
||||
if (pieceLoadingByPath.value[key]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const definition = assignment.definition || {};
|
||||
const requiredTypeId =
|
||||
definition.typePieceId || definition.typePiece?.id || null;
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.set('itemsPerPage', '50');
|
||||
if (term.trim()) {
|
||||
params.set('name', term.trim());
|
||||
}
|
||||
if (requiredTypeId) {
|
||||
params.set('typePiece', typeIri(requiredTypeId));
|
||||
}
|
||||
|
||||
setLoading(pieceLoadingByPath.value, key, true);
|
||||
try {
|
||||
const result = await get(`/pieces?${params.toString()}`);
|
||||
if (result.success) {
|
||||
pieceOptionsByPath.value[key] = extractCollection(result.data);
|
||||
}
|
||||
} finally {
|
||||
setLoading(pieceLoadingByPath.value, key, false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchProductOptions = async (assignment: StructureProductAssignment, term = '') => {
|
||||
const key = assignment.path;
|
||||
if (productLoadingByPath.value[key]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const definition = assignment.definition || {};
|
||||
const requiredTypeId =
|
||||
definition.typeProductId || definition.typeProduct?.id || null;
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.set('itemsPerPage', '50');
|
||||
if (term.trim()) {
|
||||
params.set('name', term.trim());
|
||||
}
|
||||
if (requiredTypeId) {
|
||||
params.set('typeProduct', typeIri(requiredTypeId));
|
||||
}
|
||||
|
||||
setLoading(productLoadingByPath.value, key, true);
|
||||
try {
|
||||
const result = await get(`/products?${params.toString()}`);
|
||||
if (result.success) {
|
||||
productOptionsByPath.value[key] = extractCollection(result.data);
|
||||
}
|
||||
} finally {
|
||||
setLoading(productLoadingByPath.value, key, false);
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
componentOptions,
|
||||
(options) => {
|
||||
if (isRoot.value) {
|
||||
return;
|
||||
}
|
||||
const hasMatch = options.some(
|
||||
(component) => component.id === props.assignment.selectedComponentId,
|
||||
);
|
||||
if (!hasMatch) {
|
||||
props.assignment.selectedComponentId = '';
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const describePieceRequirement = (assignment: StructurePieceAssignment) => {
|
||||
const definition = assignment.definition;
|
||||
const parts: string[] = [];
|
||||
const addPart = (value?: string | null) => {
|
||||
const trimmed = typeof value === 'string' ? value.trim() : '';
|
||||
if (trimmed && !parts.includes(trimmed)) {
|
||||
parts.push(trimmed);
|
||||
}
|
||||
};
|
||||
|
||||
const options = getPieceOptions(assignment);
|
||||
const fallbackPiece = options[0] || null;
|
||||
const fallbackType = fallbackPiece?.typePiece || null;
|
||||
|
||||
addPart(definition.role);
|
||||
const explicitLabel =
|
||||
definition.typePieceLabel ||
|
||||
definition.typePiece?.name ||
|
||||
(definition.typePieceId ? props.pieceTypeLabelMap[definition.typePieceId] : null) ||
|
||||
fallbackType?.name;
|
||||
addPart(explicitLabel);
|
||||
|
||||
const family =
|
||||
definition.familyCode ||
|
||||
definition.typePiece?.code ||
|
||||
fallbackType?.code ||
|
||||
null;
|
||||
if (family) {
|
||||
addPart(`Famille ${family}`);
|
||||
}
|
||||
|
||||
if (parts.length === 0) {
|
||||
addPart(fallbackType?.name);
|
||||
if (fallbackType?.code) {
|
||||
addPart(`Famille ${fallbackType.code}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (parts.length === 0 && definition.typePieceId) {
|
||||
addPart(`#${definition.typePieceId}`);
|
||||
}
|
||||
|
||||
return parts.length ? parts.join(' • ') : 'Pièce du squelette';
|
||||
};
|
||||
|
||||
const getProductOptions = (assignment: StructureProductAssignment) => {
|
||||
const cached = productOptionsByPath.value[assignment.path];
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const definition = assignment.definition;
|
||||
const requiredTypeId =
|
||||
definition.typeProductId ||
|
||||
definition.typeProduct?.id ||
|
||||
definition.familyCode ||
|
||||
null;
|
||||
|
||||
return (props.products || []).filter((product) => {
|
||||
if (!product || typeof product !== 'object') {
|
||||
return false;
|
||||
}
|
||||
if (!requiredTypeId) {
|
||||
return true;
|
||||
}
|
||||
if (definition.typeProductId || definition.typeProduct?.id) {
|
||||
return (
|
||||
product.typeProductId === requiredTypeId ||
|
||||
product.typeProduct?.id === requiredTypeId
|
||||
);
|
||||
}
|
||||
if (definition.familyCode) {
|
||||
return (
|
||||
product.typeProduct?.code === requiredTypeId ||
|
||||
product.typeProductId === requiredTypeId
|
||||
);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
const productOptionLabel = (product?: ProductOption | null) => {
|
||||
if (!product) {
|
||||
return 'Produit';
|
||||
}
|
||||
return product.name || product.reference || 'Produit';
|
||||
};
|
||||
|
||||
const productOptionDescription = (product?: ProductOption | null) => {
|
||||
if (!product) {
|
||||
return '';
|
||||
}
|
||||
const parts: string[] = [];
|
||||
const typeLabel =
|
||||
product.typeProduct?.name || product.typeProduct?.code || null;
|
||||
if (typeLabel) {
|
||||
parts.push(typeLabel);
|
||||
}
|
||||
if (product.reference) {
|
||||
parts.push(`Ref. ${product.reference}`);
|
||||
}
|
||||
return parts.join(' • ');
|
||||
};
|
||||
|
||||
const describeProductRequirement = (assignment: StructureProductAssignment) => {
|
||||
const definition = assignment.definition;
|
||||
const parts: string[] = [];
|
||||
const addPart = (value?: string | null) => {
|
||||
const trimmed = typeof value === 'string' ? value.trim() : '';
|
||||
if (trimmed && !parts.includes(trimmed)) {
|
||||
parts.push(trimmed);
|
||||
}
|
||||
};
|
||||
|
||||
const options = getProductOptions(assignment);
|
||||
const fallbackProduct = options[0] || null;
|
||||
const fallbackType = fallbackProduct?.typeProduct || null;
|
||||
|
||||
addPart(definition.role);
|
||||
const explicitLabel =
|
||||
definition.typeProductLabel ||
|
||||
definition.typeProduct?.name ||
|
||||
(definition.typeProductId ? props.productTypeLabelMap[definition.typeProductId] : null) ||
|
||||
fallbackType?.name;
|
||||
addPart(explicitLabel);
|
||||
|
||||
const family =
|
||||
definition.familyCode ||
|
||||
definition.typeProduct?.code ||
|
||||
fallbackType?.code ||
|
||||
null;
|
||||
if (family) {
|
||||
addPart(`Famille ${family}`);
|
||||
}
|
||||
|
||||
if (parts.length === 0) {
|
||||
addPart(fallbackType?.name);
|
||||
if (fallbackType?.code) {
|
||||
addPart(`Famille ${fallbackType.code}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (parts.length === 0 && definition.typeProductId) {
|
||||
addPart(`#${definition.typeProductId}`);
|
||||
}
|
||||
|
||||
return parts.length ? parts.join(' • ') : 'Produit du squelette';
|
||||
return '';
|
||||
};
|
||||
|
||||
const requirementLabel = computed(() => {
|
||||
@@ -584,139 +247,13 @@ const requirementLabel = computed(() => {
|
||||
const requirementDescription = computed(() => {
|
||||
const definition = props.assignment.definition || {};
|
||||
const family =
|
||||
definition.typeComposantLabel ||
|
||||
(definition.typeComposantId ? props.componentTypeLabelMap[definition.typeComposantId] : null) ||
|
||||
definition.typeComposant?.name ||
|
||||
definition.familyCode;
|
||||
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.';
|
||||
});
|
||||
|
||||
const getPieceOptions = (assignment: StructurePieceAssignment) => {
|
||||
const cached = pieceOptionsByPath.value[assignment.path];
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const definition = assignment.definition;
|
||||
const requiredTypeId =
|
||||
definition.typePieceId ||
|
||||
definition.typePiece?.id ||
|
||||
definition.familyCode ||
|
||||
null;
|
||||
|
||||
return (props.pieces || []).filter((piece) => {
|
||||
if (!piece || typeof piece !== 'object') {
|
||||
return false;
|
||||
}
|
||||
if (!requiredTypeId) {
|
||||
return true;
|
||||
}
|
||||
if (definition.typePieceId || definition.typePiece?.id) {
|
||||
return (
|
||||
piece.typePieceId === requiredTypeId ||
|
||||
piece.typePiece?.id === requiredTypeId
|
||||
);
|
||||
}
|
||||
if (definition.familyCode) {
|
||||
return (
|
||||
piece.typePiece?.code === requiredTypeId ||
|
||||
piece.typePieceId === requiredTypeId
|
||||
);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
const pieceOptionLabel = (piece?: PieceOption | null) => {
|
||||
if (!piece) {
|
||||
return 'Pièce';
|
||||
}
|
||||
return piece.name || 'Pièce';
|
||||
};
|
||||
|
||||
const pieceOptionDescription = (piece?: PieceOption | null) => {
|
||||
if (!piece) {
|
||||
return '';
|
||||
}
|
||||
const parts: string[] = [];
|
||||
const typeLabel =
|
||||
piece.typePiece?.name || piece.typePiece?.code || null;
|
||||
if (typeLabel) {
|
||||
parts.push(typeLabel);
|
||||
}
|
||||
if (piece.reference) {
|
||||
parts.push(`Ref. ${piece.reference}`);
|
||||
}
|
||||
return parts.join(' • ');
|
||||
};
|
||||
|
||||
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 '';
|
||||
};
|
||||
|
||||
watch(
|
||||
() => [props.pieces, props.assignment.pieces],
|
||||
() => {
|
||||
for (const pieceAssignment of props.assignment.pieces) {
|
||||
const options = getPieceOptions(pieceAssignment);
|
||||
if (
|
||||
pieceAssignment.selectedPieceId &&
|
||||
!options.some((piece) => piece.id === pieceAssignment.selectedPieceId)
|
||||
) {
|
||||
pieceAssignment.selectedPieceId = '';
|
||||
}
|
||||
if (!primedPiecePaths.has(pieceAssignment.path) && !pieceOptionsByPath.value[pieceAssignment.path]) {
|
||||
primedPiecePaths.add(pieceAssignment.path);
|
||||
fetchPieceOptions(pieceAssignment).catch(() => {});
|
||||
}
|
||||
}
|
||||
},
|
||||
{ deep: true, immediate: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
() => [props.products, props.assignment.products],
|
||||
() => {
|
||||
for (const productAssignment of props.assignment.products) {
|
||||
const options = getProductOptions(productAssignment);
|
||||
if (
|
||||
productAssignment.selectedProductId &&
|
||||
!options.some((product) => product.id === productAssignment.selectedProductId)
|
||||
) {
|
||||
productAssignment.selectedProductId = '';
|
||||
}
|
||||
if (!primedProductPaths.has(productAssignment.path) && !productOptionsByPath.value[productAssignment.path]) {
|
||||
primedProductPaths.add(productAssignment.path);
|
||||
fetchProductOptions(productAssignment).catch(() => {});
|
||||
}
|
||||
}
|
||||
},
|
||||
{ deep: true, immediate: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.assignment.definition,
|
||||
() => {
|
||||
if (isRoot.value) {
|
||||
return;
|
||||
}
|
||||
const key = props.assignment.path;
|
||||
if (!primedComponentPaths.has(key) && !componentOptionsByPath.value[key]) {
|
||||
primedComponentPaths.add(key);
|
||||
fetchComponentOptions().catch(() => {});
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user