refactor(frontend) : extract assignment fetch logic into composable

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 15:07:40 +01:00
parent 9f9ad80c61
commit e911f169ce
4 changed files with 1322 additions and 513 deletions

View File

@@ -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>

View File

@@ -0,0 +1,366 @@
import { computed, ref, watch } from 'vue'
import { useApi } from '~/composables/useApi'
import { extractCollection } from '~/shared/utils/apiHelpers'
import {
componentOptionDescription,
componentOptionLabel,
describePieceRequirement as _describePieceRequirement,
describeProductRequirement as _describeProductRequirement,
pieceOptionDescription,
pieceOptionLabel,
productOptionDescription,
productOptionLabel,
} from '~/shared/utils/structureAssignmentLabels'
import type {
ComponentOption,
PieceOption,
ProductOption,
StructureAssignmentNode,
StructurePieceAssignment,
StructureProductAssignment,
} from '~/shared/utils/structureAssignmentLabels'
export type {
ComponentOption,
PieceOption,
ProductOption,
StructureAssignmentNode,
StructurePieceAssignment,
StructureProductAssignment,
} from '~/shared/utils/structureAssignmentLabels'
export interface StructureAssignmentFetchDeps {
assignment: StructureAssignmentNode
pieces: PieceOption[] | null
products: ProductOption[] | null
components: ComponentOption[] | null
isRoot: () => boolean
pieceTypeLabelMap: Record<string, string>
productTypeLabelMap: Record<string, string>
componentTypeLabelMap: Record<string, string>
}
export function useStructureAssignmentFetch(deps: StructureAssignmentFetchDeps) {
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 typeIri = (id: string) => `/api/model_types/${id}`
const primedPiecePaths = new Set<string>()
const primedProductPaths = new Set<string>()
const primedComponentPaths = new Set<string>()
// --- Component options ---
const componentOptions = computed(() => {
if (deps.isRoot()) {
return []
}
const cached = componentOptionsByPath.value[deps.assignment.path]
if (cached) {
return cached
}
const definition = deps.assignment.definition || {}
const requiredTypeId =
definition.typeComposantId || definition.modelId || null
const requiredFamilyCode = definition.familyCode || null
return (deps.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 fetchComponentOptions = async (term = '') => {
if (deps.isRoot()) {
return
}
const key = deps.assignment.path
if (componentLoadingByPath.value[key]) {
return
}
const definition = deps.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)
}
}
// --- Piece options ---
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 (deps.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 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 describePieceRequirement = (assignment: StructurePieceAssignment) => {
const options = getPieceOptions(assignment)
return _describePieceRequirement(assignment, options, deps.pieceTypeLabelMap)
}
// --- Product options ---
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 (deps.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 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)
}
}
const describeProductRequirement = (assignment: StructureProductAssignment) => {
const options = getProductOptions(assignment)
return _describeProductRequirement(assignment, options, deps.productTypeLabelMap)
}
// --- Watchers ---
watch(
componentOptions,
(options) => {
if (deps.isRoot()) {
return
}
const hasMatch = options.some(
(component) => component.id === deps.assignment.selectedComponentId,
)
if (!hasMatch) {
deps.assignment.selectedComponentId = ''
}
},
{ immediate: true },
)
watch(
() => [deps.pieces, deps.assignment.pieces],
() => {
for (const pieceAssignment of deps.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(
() => [deps.products, deps.assignment.products],
() => {
for (const productAssignment of deps.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(
() => deps.assignment.definition,
() => {
if (deps.isRoot()) {
return
}
const key = deps.assignment.path
if (!primedComponentPaths.has(key) && !componentOptionsByPath.value[key]) {
primedComponentPaths.add(key)
fetchComponentOptions().catch(() => {})
}
},
{ immediate: true },
)
return {
pieceLoadingByPath,
productLoadingByPath,
componentLoadingByPath,
componentOptions,
componentOptionLabel,
componentOptionDescription,
fetchComponentOptions,
getPieceOptions,
pieceOptionLabel,
pieceOptionDescription,
fetchPieceOptions,
describePieceRequirement,
getProductOptions,
productOptionLabel,
productOptionDescription,
fetchProductOptions,
describeProductRequirement,
}
}

View File

@@ -0,0 +1,259 @@
/**
* Type definitions and pure label/description helpers for structure assignments.
*
* Extracted from composables/useStructureAssignmentFetch.ts to keep files
* under 500 lines. These are stateless utilities that do not depend on Vue
* reactivity or API fetching.
*/
import type {
ComponentModelPiece,
ComponentModelProduct,
ComponentModelStructureNode,
} from '~/shared/types/inventory'
// ---------------------------------------------------------------------------
// Option types
// ---------------------------------------------------------------------------
export interface ComponentOption {
id: string
name?: string | null
reference?: string | null
typeComposantId?: string | null
typeComposant?: {
id: string
name?: string | null
code?: string | null
} | null
}
export interface PieceOption {
id: string
name?: string | null
reference?: string | null
typePieceId?: string | null
typePiece?: {
id: string
name?: string | null
code?: string | null
} | null
}
export interface ProductOption {
id: string
name?: string | null
reference?: string | null
typeProductId?: string | null
typeProduct?: {
id: string
name?: string | null
code?: string | null
} | null
}
// ---------------------------------------------------------------------------
// Assignment node types
// ---------------------------------------------------------------------------
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[]
}
// ---------------------------------------------------------------------------
// Component label helpers
// ---------------------------------------------------------------------------
export const componentOptionLabel = (component?: ComponentOption | null): string => {
if (!component) {
return 'Composant sans nom'
}
return component.name || 'Composant sans nom'
}
export const componentOptionDescription = (component?: ComponentOption | null): string => {
if (!component) {
return ''
}
const parts: string[] = []
const typeLabel =
component.typeComposant?.name || component.typeComposant?.code || null
if (typeLabel) {
parts.push(typeLabel)
}
if (component.reference) {
parts.push(`Ref. ${component.reference}`)
}
return parts.join(' \u2022 ')
}
// ---------------------------------------------------------------------------
// Piece label helpers
// ---------------------------------------------------------------------------
export const pieceOptionLabel = (piece?: PieceOption | null): string => {
if (!piece) {
return 'Pi\u00e8ce'
}
return piece.name || 'Pi\u00e8ce'
}
export const pieceOptionDescription = (piece?: PieceOption | null): string => {
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(' \u2022 ')
}
// ---------------------------------------------------------------------------
// Product label helpers
// ---------------------------------------------------------------------------
export const productOptionLabel = (product?: ProductOption | null): string => {
if (!product) {
return 'Produit'
}
return product.name || product.reference || 'Produit'
}
export const productOptionDescription = (product?: ProductOption | null): string => {
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(' \u2022 ')
}
// ---------------------------------------------------------------------------
// Requirement description helpers
// ---------------------------------------------------------------------------
export const describePieceRequirement = (
assignment: StructurePieceAssignment,
options: PieceOption[],
pieceTypeLabelMap: Record<string, string>,
): string => {
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 fallbackPiece = options[0] || null
const fallbackType = fallbackPiece?.typePiece || null
addPart(definition.role)
const explicitLabel =
definition.typePieceLabel
|| definition.typePiece?.name
|| (definition.typePieceId ? 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(' \u2022 ') : 'Pi\u00e8ce du squelette'
}
export const describeProductRequirement = (
assignment: StructureProductAssignment,
options: ProductOption[],
productTypeLabelMap: Record<string, string>,
): string => {
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 fallbackProduct = options[0] || null
const fallbackType = fallbackProduct?.typeProduct || null
addPart(definition.role)
const explicitLabel =
definition.typeProductLabel
|| definition.typeProduct?.name
|| (definition.typeProductId ? 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(' \u2022 ') : 'Produit du squelette'
}