chore : merge migration-to-symfony into master for v1.0.0
This commit is contained in:
@@ -17,12 +17,13 @@
|
|||||||
<SearchSelect
|
<SearchSelect
|
||||||
:model-value="assignment.selectedComponentId || ''"
|
:model-value="assignment.selectedComponentId || ''"
|
||||||
:options="componentOptions"
|
:options="componentOptions"
|
||||||
:loading="componentsLoading"
|
:loading="componentsLoading || componentLoadingByPath[assignment.path]"
|
||||||
size="sm"
|
size="sm"
|
||||||
placeholder="Rechercher un composant..."
|
placeholder="Rechercher un composant..."
|
||||||
:empty-text="componentOptions.length ? 'Aucun résultat' : 'Aucun composant disponible'"
|
:empty-text="componentOptions.length ? 'Aucun résultat' : 'Aucun composant disponible'"
|
||||||
:option-label="componentOptionLabel"
|
:option-label="componentOptionLabel"
|
||||||
:option-description="componentOptionDescription"
|
:option-description="componentOptionDescription"
|
||||||
|
@search="fetchComponentOptions"
|
||||||
@update:modelValue="(value) => { assignment.selectedComponentId = normalizeSelectionValue(value); }"
|
@update:modelValue="(value) => { assignment.selectedComponentId = normalizeSelectionValue(value); }"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -45,22 +46,23 @@
|
|||||||
>
|
>
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<p class="text-xs font-medium text-base-content">
|
<p class="text-xs font-medium text-base-content">
|
||||||
{{ describePieceRequirement(pieceAssignment.definition) }}
|
{{ describePieceRequirement(pieceAssignment) }}
|
||||||
</p>
|
</p>
|
||||||
<p v-if="!getPieceOptions(pieceAssignment.definition).length" class="text-[11px] text-error">
|
<p v-if="!getPieceOptions(pieceAssignment).length" class="text-[11px] text-error">
|
||||||
Aucune pièce disponible pour cette famille.
|
Aucune pièce disponible pour cette famille.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SearchSelect
|
<SearchSelect
|
||||||
:model-value="pieceAssignment.selectedPieceId || ''"
|
:model-value="pieceAssignment.selectedPieceId || ''"
|
||||||
:options="getPieceOptions(pieceAssignment.definition)"
|
:options="getPieceOptions(pieceAssignment)"
|
||||||
:loading="piecesLoading"
|
:loading="piecesLoading || pieceLoadingByPath[pieceAssignment.path]"
|
||||||
size="xs"
|
size="xs"
|
||||||
placeholder="Rechercher une pièce..."
|
placeholder="Rechercher une pièce..."
|
||||||
:empty-text="getPieceOptions(pieceAssignment.definition).length ? 'Aucun résultat' : 'Aucune pièce disponible'"
|
:empty-text="getPieceOptions(pieceAssignment).length ? 'Aucun résultat' : 'Aucune pièce disponible'"
|
||||||
:option-label="pieceOptionLabel"
|
:option-label="pieceOptionLabel"
|
||||||
:option-description="pieceOptionDescription"
|
:option-description="pieceOptionDescription"
|
||||||
|
@search="(term) => fetchPieceOptions(pieceAssignment, term)"
|
||||||
@update:modelValue="(value) => { pieceAssignment.selectedPieceId = normalizeSelectionValue(value); }"
|
@update:modelValue="(value) => { pieceAssignment.selectedPieceId = normalizeSelectionValue(value); }"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -83,22 +85,23 @@
|
|||||||
>
|
>
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<p class="text-xs font-medium text-base-content">
|
<p class="text-xs font-medium text-base-content">
|
||||||
{{ describeProductRequirement(productAssignment.definition) }}
|
{{ describeProductRequirement(productAssignment) }}
|
||||||
</p>
|
</p>
|
||||||
<p v-if="!getProductOptions(productAssignment.definition).length" class="text-[11px] text-error">
|
<p v-if="!getProductOptions(productAssignment).length" class="text-[11px] text-error">
|
||||||
Aucun produit disponible pour cette catégorie.
|
Aucun produit disponible pour cette catégorie.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SearchSelect
|
<SearchSelect
|
||||||
:model-value="productAssignment.selectedProductId || ''"
|
:model-value="productAssignment.selectedProductId || ''"
|
||||||
:options="getProductOptions(productAssignment.definition)"
|
:options="getProductOptions(productAssignment)"
|
||||||
:loading="productsLoading"
|
:loading="productsLoading || productLoadingByPath[productAssignment.path]"
|
||||||
size="xs"
|
size="xs"
|
||||||
placeholder="Rechercher un produit..."
|
placeholder="Rechercher un produit..."
|
||||||
:empty-text="getProductOptions(productAssignment.definition).length ? 'Aucun résultat' : 'Aucun produit disponible'"
|
:empty-text="getProductOptions(productAssignment).length ? 'Aucun résultat' : 'Aucun produit disponible'"
|
||||||
:option-label="productOptionLabel"
|
:option-label="productOptionLabel"
|
||||||
:option-description="productOptionDescription"
|
:option-description="productOptionDescription"
|
||||||
|
@search="(term) => fetchProductOptions(productAssignment, term)"
|
||||||
@update:modelValue="(value) => { productAssignment.selectedProductId = normalizeSelectionValue(value); }"
|
@update:modelValue="(value) => { productAssignment.selectedProductId = normalizeSelectionValue(value); }"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -131,8 +134,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import SearchSelect from '~/components/common/SearchSelect.vue';
|
import SearchSelect from '~/components/common/SearchSelect.vue';
|
||||||
|
import { useApi } from '~/composables/useApi';
|
||||||
import type {
|
import type {
|
||||||
ComponentModelPiece,
|
ComponentModelPiece,
|
||||||
ComponentModelProduct,
|
ComponentModelProduct,
|
||||||
@@ -206,6 +210,9 @@ const props = withDefaults(
|
|||||||
componentsLoading?: boolean;
|
componentsLoading?: boolean;
|
||||||
piecesLoading?: boolean;
|
piecesLoading?: boolean;
|
||||||
productsLoading?: boolean;
|
productsLoading?: boolean;
|
||||||
|
pieceTypeLabelMap?: Record<string, string>;
|
||||||
|
productTypeLabelMap?: Record<string, string>;
|
||||||
|
componentTypeLabelMap?: Record<string, string>;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
depth: 0,
|
depth: 0,
|
||||||
@@ -215,6 +222,9 @@ const props = withDefaults(
|
|||||||
componentsLoading: false,
|
componentsLoading: false,
|
||||||
piecesLoading: false,
|
piecesLoading: false,
|
||||||
productsLoading: false,
|
productsLoading: false,
|
||||||
|
pieceTypeLabelMap: () => ({}),
|
||||||
|
productTypeLabelMap: () => ({}),
|
||||||
|
componentTypeLabelMap: () => ({}),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -225,10 +235,42 @@ const wrapperClass = computed(() =>
|
|||||||
depth.value === 0 ? 'space-y-6' : 'space-y-6 border-l border-base-300 pl-4',
|
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 extractCollection = (payload: any): any[] => {
|
||||||
|
if (Array.isArray(payload)) {
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
if (Array.isArray(payload?.member)) {
|
||||||
|
return payload.member;
|
||||||
|
}
|
||||||
|
if (Array.isArray(payload?.['hydra:member'])) {
|
||||||
|
return payload['hydra:member'];
|
||||||
|
}
|
||||||
|
if (Array.isArray(payload?.data)) {
|
||||||
|
return payload.data;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const setLoading = (target: Record<string, boolean>, key: string, value: boolean) => {
|
||||||
|
target[key] = value;
|
||||||
|
};
|
||||||
|
|
||||||
const componentOptions = computed(() => {
|
const componentOptions = computed(() => {
|
||||||
if (isRoot.value) {
|
if (isRoot.value) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
const cached = componentOptionsByPath.value[props.assignment.path];
|
||||||
|
if (cached) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
const definition = props.assignment.definition || {};
|
const definition = props.assignment.definition || {};
|
||||||
const requiredTypeId =
|
const requiredTypeId =
|
||||||
definition.typeComposantId || definition.modelId || null;
|
definition.typeComposantId || definition.modelId || null;
|
||||||
@@ -274,6 +316,104 @@ const componentOptionDescription = (component?: ComponentOption | null) => {
|
|||||||
return parts.join(' • ');
|
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 as any).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 as any).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(
|
watch(
|
||||||
componentOptions,
|
componentOptions,
|
||||||
(options) => {
|
(options) => {
|
||||||
@@ -290,7 +430,8 @@ watch(
|
|||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
const describePieceRequirement = (definition: ComponentModelPiece) => {
|
const describePieceRequirement = (assignment: StructurePieceAssignment) => {
|
||||||
|
const definition = assignment.definition;
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
const addPart = (value?: string | null) => {
|
const addPart = (value?: string | null) => {
|
||||||
const trimmed = typeof value === 'string' ? value.trim() : '';
|
const trimmed = typeof value === 'string' ? value.trim() : '';
|
||||||
@@ -299,16 +440,17 @@ const describePieceRequirement = (definition: ComponentModelPiece) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const options = getPieceOptions(definition);
|
const options = getPieceOptions(assignment);
|
||||||
const fallbackPiece = options[0] || null;
|
const fallbackPiece = options[0] || null;
|
||||||
const fallbackType = fallbackPiece?.typePiece || null;
|
const fallbackType = fallbackPiece?.typePiece || null;
|
||||||
|
|
||||||
addPart(definition.role);
|
addPart(definition.role);
|
||||||
addPart(
|
const explicitLabel =
|
||||||
definition.typePieceLabel ||
|
definition.typePieceLabel ||
|
||||||
(definition as any).typePiece?.name ||
|
(definition as any).typePiece?.name ||
|
||||||
fallbackType?.name,
|
(definition.typePieceId ? props.pieceTypeLabelMap[definition.typePieceId] : null) ||
|
||||||
);
|
fallbackType?.name;
|
||||||
|
addPart(explicitLabel);
|
||||||
|
|
||||||
const family =
|
const family =
|
||||||
definition.familyCode ||
|
definition.familyCode ||
|
||||||
@@ -333,7 +475,12 @@ const describePieceRequirement = (definition: ComponentModelPiece) => {
|
|||||||
return parts.length ? parts.join(' • ') : 'Pièce du squelette';
|
return parts.length ? parts.join(' • ') : 'Pièce du squelette';
|
||||||
};
|
};
|
||||||
|
|
||||||
const getProductOptions = (definition: ComponentModelProduct) => {
|
const getProductOptions = (assignment: StructureProductAssignment) => {
|
||||||
|
const cached = productOptionsByPath.value[assignment.path];
|
||||||
|
if (cached) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
const definition = assignment.definition;
|
||||||
const requiredTypeId =
|
const requiredTypeId =
|
||||||
definition.typeProductId ||
|
definition.typeProductId ||
|
||||||
(definition as any).typeProduct?.id ||
|
(definition as any).typeProduct?.id ||
|
||||||
@@ -386,7 +533,8 @@ const productOptionDescription = (product?: ProductOption | null) => {
|
|||||||
return parts.join(' • ');
|
return parts.join(' • ');
|
||||||
};
|
};
|
||||||
|
|
||||||
const describeProductRequirement = (definition: ComponentModelProduct) => {
|
const describeProductRequirement = (assignment: StructureProductAssignment) => {
|
||||||
|
const definition = assignment.definition;
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
const addPart = (value?: string | null) => {
|
const addPart = (value?: string | null) => {
|
||||||
const trimmed = typeof value === 'string' ? value.trim() : '';
|
const trimmed = typeof value === 'string' ? value.trim() : '';
|
||||||
@@ -395,16 +543,17 @@ const describeProductRequirement = (definition: ComponentModelProduct) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const options = getProductOptions(definition);
|
const options = getProductOptions(assignment);
|
||||||
const fallbackProduct = options[0] || null;
|
const fallbackProduct = options[0] || null;
|
||||||
const fallbackType = fallbackProduct?.typeProduct || null;
|
const fallbackType = fallbackProduct?.typeProduct || null;
|
||||||
|
|
||||||
addPart(definition.role);
|
addPart(definition.role);
|
||||||
addPart(
|
const explicitLabel =
|
||||||
definition.typeProductLabel ||
|
definition.typeProductLabel ||
|
||||||
(definition as any).typeProduct?.name ||
|
(definition as any).typeProduct?.name ||
|
||||||
fallbackType?.name,
|
(definition.typeProductId ? props.productTypeLabelMap[definition.typeProductId] : null) ||
|
||||||
);
|
fallbackType?.name;
|
||||||
|
addPart(explicitLabel);
|
||||||
|
|
||||||
const family =
|
const family =
|
||||||
definition.familyCode ||
|
definition.familyCode ||
|
||||||
@@ -435,6 +584,9 @@ const requirementLabel = computed(() => {
|
|||||||
if (alias) {
|
if (alias) {
|
||||||
return alias;
|
return alias;
|
||||||
}
|
}
|
||||||
|
if (definition.typeComposantId && props.componentTypeLabelMap[definition.typeComposantId]) {
|
||||||
|
return props.componentTypeLabelMap[definition.typeComposantId];
|
||||||
|
}
|
||||||
if (definition.typeComposant?.name) {
|
if (definition.typeComposant?.name) {
|
||||||
return definition.typeComposant.name;
|
return definition.typeComposant.name;
|
||||||
}
|
}
|
||||||
@@ -448,6 +600,7 @@ const requirementDescription = computed(() => {
|
|||||||
const definition = props.assignment.definition || {};
|
const definition = props.assignment.definition || {};
|
||||||
const family =
|
const family =
|
||||||
definition.typeComposantLabel ||
|
definition.typeComposantLabel ||
|
||||||
|
(definition.typeComposantId ? props.componentTypeLabelMap[definition.typeComposantId] : null) ||
|
||||||
definition.typeComposant?.name ||
|
definition.typeComposant?.name ||
|
||||||
definition.familyCode;
|
definition.familyCode;
|
||||||
if (family) {
|
if (family) {
|
||||||
@@ -456,7 +609,12 @@ const requirementDescription = computed(() => {
|
|||||||
return 'Sélectionnez un composant enfant conforme à cette position.';
|
return 'Sélectionnez un composant enfant conforme à cette position.';
|
||||||
});
|
});
|
||||||
|
|
||||||
const getPieceOptions = (definition: ComponentModelPiece) => {
|
const getPieceOptions = (assignment: StructurePieceAssignment) => {
|
||||||
|
const cached = pieceOptionsByPath.value[assignment.path];
|
||||||
|
if (cached) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
const definition = assignment.definition;
|
||||||
const requiredTypeId =
|
const requiredTypeId =
|
||||||
definition.typePieceId ||
|
definition.typePieceId ||
|
||||||
(definition as any).typePiece?.id ||
|
(definition as any).typePiece?.id ||
|
||||||
@@ -526,13 +684,17 @@ watch(
|
|||||||
() => [props.pieces, props.assignment.pieces],
|
() => [props.pieces, props.assignment.pieces],
|
||||||
() => {
|
() => {
|
||||||
for (const pieceAssignment of props.assignment.pieces) {
|
for (const pieceAssignment of props.assignment.pieces) {
|
||||||
const options = getPieceOptions(pieceAssignment.definition);
|
const options = getPieceOptions(pieceAssignment);
|
||||||
if (
|
if (
|
||||||
pieceAssignment.selectedPieceId &&
|
pieceAssignment.selectedPieceId &&
|
||||||
!options.some((piece) => piece.id === pieceAssignment.selectedPieceId)
|
!options.some((piece) => piece.id === pieceAssignment.selectedPieceId)
|
||||||
) {
|
) {
|
||||||
pieceAssignment.selectedPieceId = '';
|
pieceAssignment.selectedPieceId = '';
|
||||||
}
|
}
|
||||||
|
if (!primedPiecePaths.has(pieceAssignment.path) && !pieceOptionsByPath.value[pieceAssignment.path]) {
|
||||||
|
primedPiecePaths.add(pieceAssignment.path);
|
||||||
|
fetchPieceOptions(pieceAssignment).catch(() => {});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ deep: true, immediate: true },
|
{ deep: true, immediate: true },
|
||||||
@@ -542,15 +704,34 @@ watch(
|
|||||||
() => [props.products, props.assignment.products],
|
() => [props.products, props.assignment.products],
|
||||||
() => {
|
() => {
|
||||||
for (const productAssignment of props.assignment.products) {
|
for (const productAssignment of props.assignment.products) {
|
||||||
const options = getProductOptions(productAssignment.definition);
|
const options = getProductOptions(productAssignment);
|
||||||
if (
|
if (
|
||||||
productAssignment.selectedProductId &&
|
productAssignment.selectedProductId &&
|
||||||
!options.some((product) => product.id === productAssignment.selectedProductId)
|
!options.some((product) => product.id === productAssignment.selectedProductId)
|
||||||
) {
|
) {
|
||||||
productAssignment.selectedProductId = '';
|
productAssignment.selectedProductId = '';
|
||||||
}
|
}
|
||||||
|
if (!primedProductPaths.has(productAssignment.path) && !productOptionsByPath.value[productAssignment.path]) {
|
||||||
|
primedProductPaths.add(productAssignment.path);
|
||||||
|
fetchProductOptions(productAssignment).catch(() => {});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ deep: true, immediate: true },
|
{ 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>
|
</script>
|
||||||
|
|||||||
@@ -12,12 +12,6 @@
|
|||||||
loading="lazy"
|
loading="lazy"
|
||||||
decoding="async"
|
decoding="async"
|
||||||
>
|
>
|
||||||
<iframe
|
|
||||||
v-else-if="canRenderPdf"
|
|
||||||
:src="previewSrc"
|
|
||||||
class="h-full w-full border-0 bg-white"
|
|
||||||
title="Aperçu PDF"
|
|
||||||
/>
|
|
||||||
<component
|
<component
|
||||||
v-else
|
v-else
|
||||||
:is="icon.component"
|
:is="icon.component"
|
||||||
@@ -54,8 +48,6 @@ const props = defineProps<{
|
|||||||
alt?: string;
|
alt?: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const PDF_PREVIEW_MAX_BYTES = 5 * 1024 * 1024;
|
|
||||||
|
|
||||||
const normalizedDocument = computed(() => props.document ?? null);
|
const normalizedDocument = computed(() => props.document ?? null);
|
||||||
|
|
||||||
const canRenderImage = computed(() => {
|
const canRenderImage = computed(() => {
|
||||||
@@ -64,14 +56,9 @@ const canRenderImage = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const canRenderPdf = computed(() => {
|
const canRenderPdf = computed(() => {
|
||||||
const doc = normalizedDocument.value;
|
// Rendering many PDF iframes in a list is very heavy for the browser.
|
||||||
if (!doc || !isPdfDocument(doc) || !doc.path) {
|
// We intentionally disable inline PDF previews and fall back to an icon.
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
if (typeof doc.size === 'number' && doc.size > PDF_PREVIEW_MAX_BYTES) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const appendPdfViewerParams = (src: string) => {
|
const appendPdfViewerParams = (src: string) => {
|
||||||
|
|||||||
@@ -62,19 +62,30 @@ const deepClone = value => JSON.parse(JSON.stringify(value))
|
|||||||
|
|
||||||
const createFieldKey = () => `cf-${Math.random().toString(16).slice(2)}-${Date.now()}`
|
const createFieldKey = () => `cf-${Math.random().toString(16).slice(2)}-${Date.now()}`
|
||||||
|
|
||||||
|
const normalizeCustomField = (field = {}, index = 0) => {
|
||||||
|
const clone = deepClone(field)
|
||||||
|
if (clone.type === 'select') {
|
||||||
|
if (typeof clone.optionsText !== 'string' || !clone.optionsText.length) {
|
||||||
|
if (Array.isArray(clone.options)) {
|
||||||
|
clone.optionsText = clone.options.map(option => String(option).trim()).filter(Boolean).join('\n')
|
||||||
|
} else {
|
||||||
|
clone.optionsText = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const currentOrder =
|
||||||
|
typeof clone?.orderIndex === 'number' ? clone.orderIndex : index
|
||||||
|
clone.orderIndex = currentOrder
|
||||||
|
if (typeof clone?.__key !== 'string' || !clone.__key) {
|
||||||
|
clone.__key = createFieldKey()
|
||||||
|
}
|
||||||
|
return clone
|
||||||
|
}
|
||||||
|
|
||||||
const withNormalizedOrder = (items = []) => {
|
const withNormalizedOrder = (items = []) => {
|
||||||
if (!Array.isArray(items)) { return [] }
|
if (!Array.isArray(items)) { return [] }
|
||||||
return items
|
return items
|
||||||
.map((item, index) => {
|
.map((item, index) => normalizeCustomField(item, index))
|
||||||
const clone = deepClone(item)
|
|
||||||
const currentOrder =
|
|
||||||
typeof clone?.orderIndex === 'number' ? clone.orderIndex : index
|
|
||||||
clone.orderIndex = currentOrder
|
|
||||||
if (typeof clone?.__key !== 'string' || !clone.__key) {
|
|
||||||
clone.__key = createFieldKey()
|
|
||||||
}
|
|
||||||
return clone
|
|
||||||
})
|
|
||||||
.sort((a, b) => (a.orderIndex ?? 0) - (b.orderIndex ?? 0))
|
.sort((a, b) => (a.orderIndex ?? 0) - (b.orderIndex ?? 0))
|
||||||
.map((item, index) => ({ ...item, orderIndex: index }))
|
.map((item, index) => ({ ...item, orderIndex: index }))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted } from 'vue'
|
import { computed, onMounted, watch } from 'vue'
|
||||||
import RequirementListEditor from '~/components/common/RequirementListEditor.vue'
|
import RequirementListEditor from '~/components/common/RequirementListEditor.vue'
|
||||||
import { usePieceTypes } from '~/composables/usePieceTypes'
|
import { usePieceTypes } from '~/composables/usePieceTypes'
|
||||||
|
|
||||||
@@ -91,5 +91,11 @@ onMounted(async () => {
|
|||||||
if (!pieceTypes.value.length) {
|
if (!pieceTypes.value.length) {
|
||||||
await loadPieceTypes()
|
await loadPieceTypes()
|
||||||
}
|
}
|
||||||
|
console.log('[PieceRequirementsSection] pieceTypes loaded:', pieceTypes.value.map(t => ({ id: t.id, name: t.name })))
|
||||||
|
console.log('[PieceRequirementsSection] requirements on mount:', props.modelValue.map(r => ({ id: r.id, typePieceId: r.typePieceId })))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(() => props.modelValue, (newVal) => {
|
||||||
|
console.log('[PieceRequirementsSection] requirements updated:', newVal.map(r => ({ id: r.id, typePieceId: r.typePieceId })))
|
||||||
|
}, { deep: true })
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
128
app/components/common/Pagination.vue
Normal file
128
app/components/common/Pagination.vue
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="totalPages > 1" class="flex items-center justify-center gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-ghost"
|
||||||
|
:disabled="currentPage <= 1"
|
||||||
|
@click="goToPage(1)"
|
||||||
|
>
|
||||||
|
<IconLucideChevronFirst class="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-ghost"
|
||||||
|
:disabled="currentPage <= 1"
|
||||||
|
@click="goToPage(currentPage - 1)"
|
||||||
|
>
|
||||||
|
<IconLucideChevronLeft class="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<template v-for="page in visiblePages" :key="page">
|
||||||
|
<span v-if="page === 'ellipsis-start' || page === 'ellipsis-end'" class="px-2">...</span>
|
||||||
|
<button
|
||||||
|
v-else
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm"
|
||||||
|
:class="page === currentPage ? 'btn-primary' : 'btn-ghost'"
|
||||||
|
@click="goToPage(page)"
|
||||||
|
>
|
||||||
|
{{ page }}
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-ghost"
|
||||||
|
:disabled="currentPage >= totalPages"
|
||||||
|
@click="goToPage(currentPage + 1)"
|
||||||
|
>
|
||||||
|
<IconLucideChevronRight class="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-ghost"
|
||||||
|
:disabled="currentPage >= totalPages"
|
||||||
|
@click="goToPage(totalPages)"
|
||||||
|
>
|
||||||
|
<IconLucideChevronLast class="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import IconLucideChevronFirst from '~icons/lucide/chevrons-left'
|
||||||
|
import IconLucideChevronLeft from '~icons/lucide/chevron-left'
|
||||||
|
import IconLucideChevronRight from '~icons/lucide/chevron-right'
|
||||||
|
import IconLucideChevronLast from '~icons/lucide/chevrons-right'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
currentPage: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
totalPages: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
maxVisiblePages: {
|
||||||
|
type: Number,
|
||||||
|
default: 5
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:currentPage'])
|
||||||
|
|
||||||
|
const visiblePages = computed(() => {
|
||||||
|
const pages = []
|
||||||
|
const total = props.totalPages
|
||||||
|
const current = props.currentPage
|
||||||
|
const maxVisible = props.maxVisiblePages
|
||||||
|
|
||||||
|
if (total <= maxVisible + 2) {
|
||||||
|
for (let i = 1; i <= total; i++) {
|
||||||
|
pages.push(i)
|
||||||
|
}
|
||||||
|
return pages
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always show first page
|
||||||
|
pages.push(1)
|
||||||
|
|
||||||
|
const half = Math.floor(maxVisible / 2)
|
||||||
|
let start = Math.max(2, current - half)
|
||||||
|
let end = Math.min(total - 1, current + half)
|
||||||
|
|
||||||
|
// Adjust if near start
|
||||||
|
if (current <= half + 1) {
|
||||||
|
end = maxVisible
|
||||||
|
}
|
||||||
|
// Adjust if near end
|
||||||
|
if (current >= total - half) {
|
||||||
|
start = total - maxVisible + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if (start > 2) {
|
||||||
|
pages.push('ellipsis-start')
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = start; i <= end; i++) {
|
||||||
|
pages.push(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (end < total - 1) {
|
||||||
|
pages.push('ellipsis-end')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always show last page
|
||||||
|
pages.push(total)
|
||||||
|
|
||||||
|
return pages
|
||||||
|
})
|
||||||
|
|
||||||
|
const goToPage = (page) => {
|
||||||
|
if (page >= 1 && page <= props.totalPages && page !== props.currentPage) {
|
||||||
|
emit('update:currentPage', page)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -122,7 +122,7 @@ const props = defineProps({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue'])
|
const emit = defineEmits(['update:modelValue', 'search'])
|
||||||
|
|
||||||
const searchTerm = ref('')
|
const searchTerm = ref('')
|
||||||
const openDropdown = ref(false)
|
const openDropdown = ref(false)
|
||||||
@@ -184,11 +184,13 @@ watch(
|
|||||||
|
|
||||||
watch(
|
watch(
|
||||||
baseOptions,
|
baseOptions,
|
||||||
() => {
|
(newOptions) => {
|
||||||
if (!openDropdown.value) {
|
console.log('[SearchSelect] baseOptions changed, count:', newOptions.length, 'modelValue:', props.modelValue, 'selectedOption:', selectedOption.value?.id)
|
||||||
searchTerm.value = selectedOption.value ? resolveLabel(selectedOption.value) : searchTerm.value
|
if (!openDropdown.value && selectedOption.value) {
|
||||||
|
searchTerm.value = resolveLabel(selectedOption.value)
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
{ deep: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
watch(openDropdown, (isOpen) => {
|
watch(openDropdown, (isOpen) => {
|
||||||
@@ -265,6 +267,7 @@ function handleInput () {
|
|||||||
if (!openDropdown.value) {
|
if (!openDropdown.value) {
|
||||||
openDropdown.value = true
|
openDropdown.value = true
|
||||||
}
|
}
|
||||||
|
emit('search', searchTerm.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeDropdown () {
|
function closeDropdown () {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export function useApi () {
|
|||||||
const apiCall = async (endpoint, options = {}) => {
|
const apiCall = async (endpoint, options = {}) => {
|
||||||
const url = `${API_BASE_URL}${endpoint}`
|
const url = `${API_BASE_URL}${endpoint}`
|
||||||
const defaultOptions = {
|
const defaultOptions = {
|
||||||
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
}
|
}
|
||||||
@@ -23,6 +24,10 @@ export function useApi () {
|
|||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
...defaultOptions,
|
...defaultOptions,
|
||||||
...options,
|
...options,
|
||||||
|
headers: {
|
||||||
|
...defaultOptions.headers,
|
||||||
|
...options.headers
|
||||||
|
},
|
||||||
signal: controller.signal
|
signal: controller.signal
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -32,7 +37,7 @@ export function useApi () {
|
|||||||
let data = null
|
let data = null
|
||||||
if (response.status !== 204) {
|
if (response.status !== 204) {
|
||||||
const contentType = response.headers.get('content-type') || ''
|
const contentType = response.headers.get('content-type') || ''
|
||||||
if (contentType.includes('application/json')) {
|
if (contentType.includes('application/json') || contentType.includes('application/ld+json') || contentType.includes('+json')) {
|
||||||
const text = await response.text()
|
const text = await response.text()
|
||||||
data = text ? JSON.parse(text) : null
|
data = text ? JSON.parse(text) : null
|
||||||
} else {
|
} else {
|
||||||
@@ -69,6 +74,9 @@ export function useApi () {
|
|||||||
const post = async (endpoint, data) => {
|
const post = async (endpoint, data) => {
|
||||||
return apiCall(endpoint, {
|
return apiCall(endpoint, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/ld+json'
|
||||||
|
},
|
||||||
body: JSON.stringify(data)
|
body: JSON.stringify(data)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -76,6 +84,9 @@ export function useApi () {
|
|||||||
const patch = async (endpoint, data) => {
|
const patch = async (endpoint, data) => {
|
||||||
return apiCall(endpoint, {
|
return apiCall(endpoint, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/merge-patch+json'
|
||||||
|
},
|
||||||
body: JSON.stringify(data)
|
body: JSON.stringify(data)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,38 @@ import { useToast } from './useToast'
|
|||||||
import { useApi } from './useApi'
|
import { useApi } from './useApi'
|
||||||
import { buildConstructeurRequestPayload, uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
import { buildConstructeurRequestPayload, uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
||||||
import { useConstructeurs } from './useConstructeurs'
|
import { useConstructeurs } from './useConstructeurs'
|
||||||
|
import { extractRelationId, normalizeRelationIds } from '~/shared/apiRelations'
|
||||||
|
|
||||||
const composants = ref([])
|
const composants = ref([])
|
||||||
|
const total = ref(0)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const extractCollection = (payload) => {
|
||||||
|
if (Array.isArray(payload)) {
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
if (Array.isArray(payload?.member)) {
|
||||||
|
return payload.member
|
||||||
|
}
|
||||||
|
if (Array.isArray(payload?.['hydra:member'])) {
|
||||||
|
return payload['hydra:member']
|
||||||
|
}
|
||||||
|
if (Array.isArray(payload?.data)) {
|
||||||
|
return payload.data
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const extractTotal = (payload, fallbackLength) => {
|
||||||
|
if (typeof payload?.totalItems === 'number') {
|
||||||
|
return payload.totalItems
|
||||||
|
}
|
||||||
|
if (typeof payload?.['hydra:totalItems'] === 'number') {
|
||||||
|
return payload['hydra:totalItems']
|
||||||
|
}
|
||||||
|
return fallbackLength
|
||||||
|
}
|
||||||
|
|
||||||
export function useComposants () {
|
export function useComposants () {
|
||||||
const { showSuccess, showError, showInfo } = useToast()
|
const { showSuccess, showError, showInfo } = useToast()
|
||||||
const { get, post, patch, delete: del } = useApi()
|
const { get, post, patch, delete: del } = useApi()
|
||||||
@@ -16,15 +44,29 @@ export function useComposants () {
|
|||||||
if (!composant || typeof composant !== 'object') {
|
if (!composant || typeof composant !== 'object') {
|
||||||
return composant
|
return composant
|
||||||
}
|
}
|
||||||
|
if (!composant.typeComposantId) {
|
||||||
|
const typeComposantId = extractRelationId(composant.typeComposant)
|
||||||
|
if (typeComposantId) {
|
||||||
|
composant.typeComposantId = typeComposantId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!composant.productId) {
|
||||||
|
const productId = extractRelationId(composant.product)
|
||||||
|
if (productId) {
|
||||||
|
composant.productId = productId
|
||||||
|
}
|
||||||
|
}
|
||||||
const ids = uniqueConstructeurIds(
|
const ids = uniqueConstructeurIds(
|
||||||
composant.constructeurIds,
|
composant.constructeurIds,
|
||||||
composant.constructeurs,
|
composant.constructeurs,
|
||||||
composant.constructeur,
|
composant.constructeur,
|
||||||
)
|
)
|
||||||
const hasConstructeurs =
|
const hasResolvedConstructeurs =
|
||||||
Array.isArray(composant.constructeurs) && composant.constructeurs.length > 0
|
Array.isArray(composant.constructeurs)
|
||||||
|
&& composant.constructeurs.length > 0
|
||||||
|
&& composant.constructeurs.every((item) => item && typeof item === 'object')
|
||||||
|
|
||||||
if (ids.length && !hasConstructeurs) {
|
if (ids.length && !hasResolvedConstructeurs) {
|
||||||
const resolved = await ensureConstructeurs(ids)
|
const resolved = await ensureConstructeurs(ids)
|
||||||
if (resolved.length) {
|
if (resolved.length) {
|
||||||
composant.constructeurs = resolved
|
composant.constructeurs = resolved
|
||||||
@@ -34,30 +76,70 @@ export function useComposants () {
|
|||||||
return composant
|
return composant
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadComposants = async () => {
|
/**
|
||||||
loading.value = true
|
* Load composants with pagination and search support
|
||||||
try {
|
* @param {Object} options - Query options
|
||||||
const result = await get('/composants')
|
* @param {string} [options.search] - Search term for name/reference
|
||||||
if (result.success) {
|
* @param {number} [options.page=1] - Current page (1-based)
|
||||||
const items = Array.isArray(result.data) ? result.data : []
|
* @param {number} [options.itemsPerPage=30] - Items per page
|
||||||
const enrichedItems = await Promise.all(items.map((item) => withResolvedConstructeurs(item)))
|
* @param {string} [options.orderBy='name'] - Field to order by
|
||||||
composants.value = enrichedItems
|
* @param {string} [options.orderDir='asc'] - Order direction (asc/desc)
|
||||||
showInfo(`Chargement de ${composants.value.length} composant(s) réussi`)
|
*/
|
||||||
|
const loadComposants = async (options = {}) => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
search = '',
|
||||||
|
page = 1,
|
||||||
|
itemsPerPage = 30,
|
||||||
|
orderBy = 'name',
|
||||||
|
orderDir = 'asc'
|
||||||
|
} = options
|
||||||
|
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.set('itemsPerPage', String(itemsPerPage))
|
||||||
|
params.set('page', String(page))
|
||||||
|
|
||||||
|
if (search && search.trim()) {
|
||||||
|
params.set('name', search.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
params.set(`order[${orderBy}]`, orderDir)
|
||||||
|
|
||||||
|
const result = await get(`/composants?${params.toString()}`)
|
||||||
|
if (result.success) {
|
||||||
|
const items = extractCollection(result.data)
|
||||||
|
const enrichedItems = await Promise.all(items.map((item) => withResolvedConstructeurs(item)))
|
||||||
|
composants.value = enrichedItems
|
||||||
|
total.value = extractTotal(result.data, items.length)
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
items: enrichedItems,
|
||||||
|
total: total.value,
|
||||||
|
page,
|
||||||
|
itemsPerPage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur lors du chargement des composants:', error)
|
||||||
|
return { success: false, error: error.message }
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('Erreur lors du chargement des composants:', error)
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const createComposant = async (composantData) => {
|
const createComposant = async (composantData) => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const result = await post('/composants', buildConstructeurRequestPayload(composantData))
|
const normalizedPayload = normalizeRelationIds(buildConstructeurRequestPayload(composantData))
|
||||||
|
const result = await post('/composants', normalizedPayload)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const enriched = await withResolvedConstructeurs(result.data)
|
const enriched = await withResolvedConstructeurs(result.data)
|
||||||
composants.value.push(enriched)
|
composants.value.unshift(enriched)
|
||||||
|
total.value += 1
|
||||||
const displayName = result.data?.name
|
const displayName = result.data?.name
|
||||||
|| composantData?.definition?.name
|
|| composantData?.definition?.name
|
||||||
|| composantData?.name
|
|| composantData?.name
|
||||||
@@ -76,7 +158,8 @@ const loadComposants = async () => {
|
|||||||
const updateComposantData = async (id, composantData) => {
|
const updateComposantData = async (id, composantData) => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const result = await patch(`/composants/${id}`, buildConstructeurRequestPayload(composantData))
|
const normalizedPayload = normalizeRelationIds(buildConstructeurRequestPayload(composantData))
|
||||||
|
const result = await patch(`/composants/${id}`, normalizedPayload)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const updated = await withResolvedConstructeurs(result.data)
|
const updated = await withResolvedConstructeurs(result.data)
|
||||||
const index = composants.value.findIndex(comp => comp.id === id)
|
const index = composants.value.findIndex(comp => comp.id === id)
|
||||||
@@ -101,6 +184,7 @@ const loadComposants = async () => {
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
const deletedComposant = composants.value.find(comp => comp.id === id)
|
const deletedComposant = composants.value.find(comp => comp.id === id)
|
||||||
composants.value = composants.value.filter(comp => comp.id !== id)
|
composants.value = composants.value.filter(comp => comp.id !== id)
|
||||||
|
total.value = Math.max(0, total.value - 1)
|
||||||
showSuccess(`Composant "${deletedComposant?.name || 'inconnu'}" supprimé avec succès`)
|
showSuccess(`Composant "${deletedComposant?.name || 'inconnu'}" supprimé avec succès`)
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
@@ -117,6 +201,7 @@ const loadComposants = async () => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
composants,
|
composants,
|
||||||
|
total,
|
||||||
loading,
|
loading,
|
||||||
loadComposants,
|
loadComposants,
|
||||||
createComposant,
|
createComposant,
|
||||||
|
|||||||
@@ -39,6 +39,22 @@ const upsertConstructeurs = (items = []) => {
|
|||||||
const getIndexedConstructeur = (id) =>
|
const getIndexedConstructeur = (id) =>
|
||||||
constructeurs.value.find((item) => item && item.id === id) || null
|
constructeurs.value.find((item) => item && item.id === id) || null
|
||||||
|
|
||||||
|
const extractCollection = (payload) => {
|
||||||
|
if (Array.isArray(payload)) {
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
if (Array.isArray(payload?.member)) {
|
||||||
|
return payload.member
|
||||||
|
}
|
||||||
|
if (Array.isArray(payload?.['hydra:member'])) {
|
||||||
|
return payload['hydra:member']
|
||||||
|
}
|
||||||
|
if (Array.isArray(payload?.data)) {
|
||||||
|
return payload.data
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
const pendingFetches = new Map()
|
const pendingFetches = new Map()
|
||||||
|
|
||||||
export function useConstructeurs () {
|
export function useConstructeurs () {
|
||||||
@@ -51,7 +67,7 @@ export function useConstructeurs () {
|
|||||||
const query = search ? `?search=${encodeURIComponent(search)}` : ''
|
const query = search ? `?search=${encodeURIComponent(search)}` : ''
|
||||||
const result = await get(`/constructeurs${query}`)
|
const result = await get(`/constructeurs${query}`)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const items = Array.isArray(result.data) ? result.data : []
|
const items = extractCollection(result.data)
|
||||||
constructeurs.value = uniqueConstructeurs(items)
|
constructeurs.value = uniqueConstructeurs(items)
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -1,10 +1,27 @@
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useApi } from './useApi'
|
import { useApi } from './useApi'
|
||||||
import { useToast } from './useToast'
|
import { useToast } from './useToast'
|
||||||
|
import { normalizeRelationIds } from '~/shared/apiRelations'
|
||||||
|
|
||||||
const documents = ref([])
|
const documents = ref([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const extractCollection = (payload) => {
|
||||||
|
if (Array.isArray(payload)) {
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
if (Array.isArray(payload?.member)) {
|
||||||
|
return payload.member
|
||||||
|
}
|
||||||
|
if (Array.isArray(payload?.['hydra:member'])) {
|
||||||
|
return payload['hydra:member']
|
||||||
|
}
|
||||||
|
if (Array.isArray(payload?.data)) {
|
||||||
|
return payload.data
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
const fileToBase64 = file =>
|
const fileToBase64 = file =>
|
||||||
new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
const reader = new FileReader()
|
const reader = new FileReader()
|
||||||
@@ -22,7 +39,7 @@ export function useDocuments () {
|
|||||||
try {
|
try {
|
||||||
const result = await get(endpoint)
|
const result = await get(endpoint)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const data = result.data || []
|
const data = extractCollection(result.data)
|
||||||
if (updateStore) {
|
if (updateStore) {
|
||||||
documents.value = data
|
documents.value = data
|
||||||
}
|
}
|
||||||
@@ -80,14 +97,14 @@ export function useDocuments () {
|
|||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const dataUrl = await fileToBase64(file)
|
const dataUrl = await fileToBase64(file)
|
||||||
|
|
||||||
const payload = {
|
const payload = normalizeRelationIds({
|
||||||
name: file.name,
|
name: file.name,
|
||||||
filename: file.name,
|
filename: file.name,
|
||||||
mimeType: file.type || 'application/octet-stream',
|
mimeType: file.type || 'application/octet-stream',
|
||||||
size: file.size,
|
size: file.size,
|
||||||
path: dataUrl,
|
path: dataUrl,
|
||||||
...context
|
...context
|
||||||
}
|
})
|
||||||
|
|
||||||
const result = await post('/documents', payload)
|
const result = await post('/documents', payload)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
|
|||||||
@@ -1,11 +1,40 @@
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useToast } from './useToast'
|
import { useToast } from './useToast'
|
||||||
import { useApi } from './useApi'
|
import { useApi } from './useApi'
|
||||||
|
import { extractRelationId } from '~/shared/apiRelations'
|
||||||
|
|
||||||
const machineTypes = ref([])
|
const machineTypes = ref([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
const normalizeRequirementList = (value) => (Array.isArray(value) ? value : [])
|
const normalizeRequirementList = (value, relationKey) => {
|
||||||
|
if (!Array.isArray(value)) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return value.map((entry, index) => {
|
||||||
|
if (!entry || typeof entry !== 'object') {
|
||||||
|
return entry
|
||||||
|
}
|
||||||
|
const normalized = { ...entry }
|
||||||
|
const relationField = relationKey.replace('Id', '')
|
||||||
|
const relationValue = normalized[relationField]
|
||||||
|
console.log(`[normalizeRequirementList] Entry ${index}:`, {
|
||||||
|
relationKey,
|
||||||
|
relationField,
|
||||||
|
hasRelationKey: !!normalized[relationKey],
|
||||||
|
relationValue,
|
||||||
|
relationValueType: typeof relationValue
|
||||||
|
})
|
||||||
|
if (relationKey && !normalized[relationKey]) {
|
||||||
|
const relationId = extractRelationId(relationValue)
|
||||||
|
console.log(`[normalizeRequirementList] Extracted ID:`, relationId)
|
||||||
|
if (relationId) {
|
||||||
|
normalized[relationKey] = relationId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(`[normalizeRequirementList] Normalized entry:`, normalized)
|
||||||
|
return normalized
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const normalizeMachineType = (type) => {
|
const normalizeMachineType = (type) => {
|
||||||
if (!type || typeof type !== 'object') {
|
if (!type || typeof type !== 'object') {
|
||||||
@@ -13,24 +42,39 @@ const normalizeMachineType = (type) => {
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...type,
|
...type,
|
||||||
componentRequirements: normalizeRequirementList(type.componentRequirements),
|
componentRequirements: normalizeRequirementList(type.componentRequirements, 'typeComposantId'),
|
||||||
pieceRequirements: normalizeRequirementList(type.pieceRequirements),
|
pieceRequirements: normalizeRequirementList(type.pieceRequirements, 'typePieceId'),
|
||||||
productRequirements: normalizeRequirementList(type.productRequirements),
|
productRequirements: normalizeRequirementList(type.productRequirements, 'typeProductId'),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const extractCollection = (payload) => {
|
||||||
|
if (Array.isArray(payload)) {
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
if (Array.isArray(payload?.member)) {
|
||||||
|
return payload.member
|
||||||
|
}
|
||||||
|
if (Array.isArray(payload?.['hydra:member'])) {
|
||||||
|
return payload['hydra:member']
|
||||||
|
}
|
||||||
|
if (Array.isArray(payload?.data)) {
|
||||||
|
return payload.data
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
export function useMachineTypesApi () {
|
export function useMachineTypesApi () {
|
||||||
const { showSuccess, showError, showInfo } = useToast()
|
const { showSuccess, showError, showInfo } = useToast()
|
||||||
const { get, post, patch, delete: del } = useApi()
|
const { get, post, put, delete: del } = useApi()
|
||||||
|
|
||||||
const loadMachineTypes = async () => {
|
const loadMachineTypes = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const result = await get('/types/machines')
|
const result = await get('/type_machines')
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
machineTypes.value = Array.isArray(result.data)
|
const items = extractCollection(result.data)
|
||||||
? result.data.map(normalizeMachineType)
|
machineTypes.value = items.map(normalizeMachineType)
|
||||||
: []
|
|
||||||
showInfo(`Chargement de ${machineTypes.value.length} type(s) de machine réussi`)
|
showInfo(`Chargement de ${machineTypes.value.length} type(s) de machine réussi`)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -43,7 +87,7 @@ export function useMachineTypesApi () {
|
|||||||
const createMachineType = async (typeData) => {
|
const createMachineType = async (typeData) => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const result = await post('/types/machines', typeData)
|
const result = await post('/type_machines', typeData)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
machineTypes.value.push(normalizeMachineType(result.data))
|
machineTypes.value.push(normalizeMachineType(result.data))
|
||||||
showSuccess(`Type de machine "${typeData.name}" créé avec succès`)
|
showSuccess(`Type de machine "${typeData.name}" créé avec succès`)
|
||||||
@@ -60,7 +104,7 @@ export function useMachineTypesApi () {
|
|||||||
const updateMachineType = async (id, typeData) => {
|
const updateMachineType = async (id, typeData) => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const result = await patch(`/types/machines/${id}`, typeData)
|
const result = await put(`/type_machines/${id}`, typeData)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const normalized = normalizeMachineType(result.data)
|
const normalized = normalizeMachineType(result.data)
|
||||||
const index = machineTypes.value.findIndex(type => type.id === id)
|
const index = machineTypes.value.findIndex(type => type.id === id)
|
||||||
@@ -81,7 +125,7 @@ export function useMachineTypesApi () {
|
|||||||
const deleteMachineType = async (id) => {
|
const deleteMachineType = async (id) => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const result = await del(`/types/machines/${id}`)
|
const result = await del(`/type_machines/${id}`)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const deletedType = machineTypes.value.find(type => type.id === id)
|
const deletedType = machineTypes.value.find(type => type.id === id)
|
||||||
machineTypes.value = machineTypes.value.filter(type => type.id !== id)
|
machineTypes.value = machineTypes.value.filter(type => type.id !== id)
|
||||||
@@ -96,19 +140,28 @@ export function useMachineTypesApi () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getMachineTypeById = async (id) => {
|
const getMachineTypeById = async (id, forceRefresh = false) => {
|
||||||
// D'abord chercher dans le cache local
|
// D'abord chercher dans le cache local (sauf si forceRefresh)
|
||||||
const localType = machineTypes.value.find(type => type.id === id)
|
if (!forceRefresh) {
|
||||||
if (localType) {
|
const localType = machineTypes.value.find(type => type.id === id)
|
||||||
return { success: true, data: localType }
|
if (localType) {
|
||||||
|
return { success: true, data: localType }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Si pas trouvé localement, récupérer depuis l'API
|
// Récupérer depuis l'API
|
||||||
try {
|
try {
|
||||||
const result = await get(`/types/machines/${id}`)
|
const result = await get(`/type_machines/${id}`)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
// Ajouter au cache local
|
const normalized = normalizeMachineType(result.data)
|
||||||
machineTypes.value.push(normalizeMachineType(result.data))
|
// Mettre à jour le cache local
|
||||||
|
const index = machineTypes.value.findIndex(type => type.id === id)
|
||||||
|
if (index !== -1) {
|
||||||
|
machineTypes.value[index] = normalized
|
||||||
|
} else {
|
||||||
|
machineTypes.value.push(normalized)
|
||||||
|
}
|
||||||
|
return { success: true, data: normalized }
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { ref } from 'vue'
|
|||||||
import { useToast } from './useToast'
|
import { useToast } from './useToast'
|
||||||
import { useApi } from './useApi'
|
import { useApi } from './useApi'
|
||||||
import { buildConstructeurRequestPayload } from '~/shared/constructeurUtils'
|
import { buildConstructeurRequestPayload } from '~/shared/constructeurUtils'
|
||||||
|
import { extractRelationId, normalizeRelationIds } from '~/shared/apiRelations'
|
||||||
|
|
||||||
const machines = ref([])
|
const machines = ref([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@@ -32,6 +33,20 @@ const normalizeMachineResponse = (payload) => {
|
|||||||
|
|
||||||
const normalized = { ...container }
|
const normalized = { ...container }
|
||||||
|
|
||||||
|
if (!normalized.siteId) {
|
||||||
|
const siteId = extractRelationId(container.site)
|
||||||
|
if (siteId) {
|
||||||
|
normalized.siteId = siteId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!normalized.typeMachineId) {
|
||||||
|
const typeMachineId = extractRelationId(container.typeMachine)
|
||||||
|
if (typeMachineId) {
|
||||||
|
normalized.typeMachineId = typeMachineId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const componentLinks = resolveLinkCollection(payload, ['componentLinks', 'machineComponentLinks']) ??
|
const componentLinks = resolveLinkCollection(payload, ['componentLinks', 'machineComponentLinks']) ??
|
||||||
resolveLinkCollection(container, ['componentLinks', 'machineComponentLinks']) ??
|
resolveLinkCollection(container, ['componentLinks', 'machineComponentLinks']) ??
|
||||||
[]
|
[]
|
||||||
@@ -56,11 +71,15 @@ export function useMachines () {
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
const machineList = Array.isArray(result.data)
|
const machineList = Array.isArray(result.data)
|
||||||
? result.data
|
? result.data
|
||||||
: Array.isArray(result.data?.machines)
|
: Array.isArray(result.data?.member)
|
||||||
? result.data.machines
|
? result.data.member
|
||||||
: Array.isArray(result.data?.data)
|
: Array.isArray(result.data?.['hydra:member'])
|
||||||
? result.data.data
|
? result.data['hydra:member']
|
||||||
: []
|
: Array.isArray(result.data?.machines)
|
||||||
|
? result.data.machines
|
||||||
|
: Array.isArray(result.data?.data)
|
||||||
|
? result.data.data
|
||||||
|
: []
|
||||||
const normalized = machineList
|
const normalized = machineList
|
||||||
.map((item) => normalizeMachineResponse(item))
|
.map((item) => normalizeMachineResponse(item))
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
@@ -77,7 +96,8 @@ export function useMachines () {
|
|||||||
const createMachine = async (machineData) => {
|
const createMachine = async (machineData) => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const result = await post('/machines', buildConstructeurRequestPayload(machineData))
|
const normalizedPayload = normalizeRelationIds(buildConstructeurRequestPayload(machineData))
|
||||||
|
const result = await post('/machines', normalizedPayload)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const createdMachine = normalizeMachineResponse(result.data) ||
|
const createdMachine = normalizeMachineResponse(result.data) ||
|
||||||
normalizeMachineResponse(result.data?.machine) ||
|
normalizeMachineResponse(result.data?.machine) ||
|
||||||
@@ -106,13 +126,14 @@ export function useMachines () {
|
|||||||
// Les composants et pièces seront créés automatiquement
|
// Les composants et pièces seront créés automatiquement
|
||||||
}
|
}
|
||||||
|
|
||||||
return await createMachine(buildConstructeurRequestPayload(machineWithStructure))
|
return await createMachine(machineWithStructure)
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateMachineData = async (id, machineData) => {
|
const updateMachineData = async (id, machineData) => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const result = await patch(`/machines/${id}`, buildConstructeurRequestPayload(machineData))
|
const normalizedPayload = normalizeRelationIds(buildConstructeurRequestPayload(machineData))
|
||||||
|
const result = await patch(`/machines/${id}`, normalizedPayload)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const updatedMachine = normalizeMachineResponse(result.data) ||
|
const updatedMachine = normalizeMachineResponse(result.data) ||
|
||||||
normalizeMachineResponse(result.data?.machine) ||
|
normalizeMachineResponse(result.data?.machine) ||
|
||||||
|
|||||||
53
app/composables/usePersistedSort.ts
Normal file
53
app/composables/usePersistedSort.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import { useCookie } from '#imports'
|
||||||
|
|
||||||
|
type SortCookie = {
|
||||||
|
field?: string
|
||||||
|
direction?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const readSortCookie = (value: unknown): SortCookie | null => {
|
||||||
|
if (!value) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
return value as SortCookie
|
||||||
|
}
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
try {
|
||||||
|
return JSON.parse(value) as SortCookie
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const usePersistedSort = <
|
||||||
|
TField extends string,
|
||||||
|
TDirection extends string,
|
||||||
|
>(
|
||||||
|
key: string,
|
||||||
|
defaults: { field: TField; direction: TDirection },
|
||||||
|
) => {
|
||||||
|
const cookie = useCookie<string | null>(`sort:${key}`, {
|
||||||
|
sameSite: 'lax',
|
||||||
|
})
|
||||||
|
const stored = readSortCookie(cookie.value)
|
||||||
|
const sortField = ref<TField>((stored?.field as TField) || defaults.field)
|
||||||
|
const sortDirection = ref<TDirection>(
|
||||||
|
(stored?.direction as TDirection) || defaults.direction,
|
||||||
|
)
|
||||||
|
|
||||||
|
watch([sortField, sortDirection], () => {
|
||||||
|
cookie.value = JSON.stringify({
|
||||||
|
field: sortField.value,
|
||||||
|
direction: sortDirection.value,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
sortField,
|
||||||
|
sortDirection,
|
||||||
|
}
|
||||||
|
}
|
||||||
34
app/composables/usePersistedValue.ts
Normal file
34
app/composables/usePersistedValue.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import { useCookie } from '#imports'
|
||||||
|
|
||||||
|
const parseValue = <T>(value: unknown, fallback: T): T => {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
try {
|
||||||
|
return JSON.parse(value) as T
|
||||||
|
} catch {
|
||||||
|
return value as unknown as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value as T
|
||||||
|
}
|
||||||
|
|
||||||
|
export const usePersistedValue = <T>(key: string, fallback: T) => {
|
||||||
|
const cookie = useCookie<string | null>(`pref:${key}`, {
|
||||||
|
sameSite: 'lax',
|
||||||
|
})
|
||||||
|
const initial = parseValue(cookie.value, fallback)
|
||||||
|
const state = ref<T>(initial)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
state,
|
||||||
|
(value) => {
|
||||||
|
cookie.value = JSON.stringify(value)
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
return state
|
||||||
|
}
|
||||||
@@ -3,10 +3,38 @@ import { useToast } from './useToast'
|
|||||||
import { useApi } from './useApi'
|
import { useApi } from './useApi'
|
||||||
import { buildConstructeurRequestPayload, uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
import { buildConstructeurRequestPayload, uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
||||||
import { useConstructeurs } from './useConstructeurs'
|
import { useConstructeurs } from './useConstructeurs'
|
||||||
|
import { extractRelationId, normalizeRelationIds } from '~/shared/apiRelations'
|
||||||
|
|
||||||
const pieces = ref([])
|
const pieces = ref([])
|
||||||
|
const total = ref(0)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const extractCollection = (payload) => {
|
||||||
|
if (Array.isArray(payload)) {
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
if (Array.isArray(payload?.member)) {
|
||||||
|
return payload.member
|
||||||
|
}
|
||||||
|
if (Array.isArray(payload?.['hydra:member'])) {
|
||||||
|
return payload['hydra:member']
|
||||||
|
}
|
||||||
|
if (Array.isArray(payload?.data)) {
|
||||||
|
return payload.data
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const extractTotal = (payload, fallbackLength) => {
|
||||||
|
if (typeof payload?.totalItems === 'number') {
|
||||||
|
return payload.totalItems
|
||||||
|
}
|
||||||
|
if (typeof payload?.['hydra:totalItems'] === 'number') {
|
||||||
|
return payload['hydra:totalItems']
|
||||||
|
}
|
||||||
|
return fallbackLength
|
||||||
|
}
|
||||||
|
|
||||||
export function usePieces () {
|
export function usePieces () {
|
||||||
const { showSuccess, showError, showInfo } = useToast()
|
const { showSuccess, showError, showInfo } = useToast()
|
||||||
const { get, post, patch, delete: del } = useApi()
|
const { get, post, patch, delete: del } = useApi()
|
||||||
@@ -16,15 +44,38 @@ export function usePieces () {
|
|||||||
if (!piece || typeof piece !== 'object') {
|
if (!piece || typeof piece !== 'object') {
|
||||||
return piece
|
return piece
|
||||||
}
|
}
|
||||||
|
if (!piece.typePieceId) {
|
||||||
|
const typePieceId = extractRelationId(piece.typePiece)
|
||||||
|
if (typePieceId) {
|
||||||
|
piece.typePieceId = typePieceId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!piece.productId) {
|
||||||
|
const productId = extractRelationId(piece.product)
|
||||||
|
if (productId) {
|
||||||
|
piece.productId = productId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const productIds = Array.isArray(piece.productIds) ? piece.productIds.filter(Boolean) : []
|
||||||
|
if (productIds.length === 0 && piece.productId) {
|
||||||
|
piece.productIds = [piece.productId]
|
||||||
|
} else if (productIds.length > 0) {
|
||||||
|
piece.productIds = productIds.map((id) => String(id))
|
||||||
|
if (!piece.productId) {
|
||||||
|
piece.productId = piece.productIds[0] || null
|
||||||
|
}
|
||||||
|
}
|
||||||
const ids = uniqueConstructeurIds(
|
const ids = uniqueConstructeurIds(
|
||||||
piece.constructeurIds,
|
piece.constructeurIds,
|
||||||
piece.constructeurs,
|
piece.constructeurs,
|
||||||
piece.constructeur,
|
piece.constructeur,
|
||||||
)
|
)
|
||||||
const hasConstructeurs =
|
const hasResolvedConstructeurs =
|
||||||
Array.isArray(piece.constructeurs) && piece.constructeurs.length > 0
|
Array.isArray(piece.constructeurs)
|
||||||
|
&& piece.constructeurs.length > 0
|
||||||
|
&& piece.constructeurs.every((item) => item && typeof item === 'object')
|
||||||
|
|
||||||
if (ids.length && !hasConstructeurs) {
|
if (ids.length && !hasResolvedConstructeurs) {
|
||||||
const resolved = await ensureConstructeurs(ids)
|
const resolved = await ensureConstructeurs(ids)
|
||||||
if (resolved.length) {
|
if (resolved.length) {
|
||||||
piece.constructeurs = resolved
|
piece.constructeurs = resolved
|
||||||
@@ -34,18 +85,58 @@ export function usePieces () {
|
|||||||
return piece
|
return piece
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadPieces = async () => {
|
/**
|
||||||
|
* Load pieces with pagination and search support
|
||||||
|
* @param {Object} options - Query options
|
||||||
|
* @param {string} [options.search] - Search term for name/reference
|
||||||
|
* @param {number} [options.page=1] - Current page (1-based)
|
||||||
|
* @param {number} [options.itemsPerPage=30] - Items per page
|
||||||
|
* @param {string} [options.orderBy='name'] - Field to order by
|
||||||
|
* @param {string} [options.orderDir='asc'] - Order direction (asc/desc)
|
||||||
|
*/
|
||||||
|
const loadPieces = async (options = {}) => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const result = await get('/pieces')
|
const {
|
||||||
|
search = '',
|
||||||
|
page = 1,
|
||||||
|
itemsPerPage = 30,
|
||||||
|
orderBy = 'name',
|
||||||
|
orderDir = 'asc'
|
||||||
|
} = options
|
||||||
|
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.set('itemsPerPage', String(itemsPerPage))
|
||||||
|
params.set('page', String(page))
|
||||||
|
|
||||||
|
if (search && search.trim()) {
|
||||||
|
// API Platform uses property filters
|
||||||
|
params.set('name', search.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
// API Platform OrderFilter syntax: order[field]=direction
|
||||||
|
params.set(`order[${orderBy}]`, orderDir)
|
||||||
|
|
||||||
|
const result = await get(`/pieces?${params.toString()}`)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const items = Array.isArray(result.data) ? result.data : []
|
const items = extractCollection(result.data)
|
||||||
const enrichedItems = await Promise.all(items.map((item) => withResolvedConstructeurs(item)))
|
const enrichedItems = await Promise.all(items.map((item) => withResolvedConstructeurs(item)))
|
||||||
pieces.value = enrichedItems
|
pieces.value = enrichedItems
|
||||||
showInfo(`Chargement de ${pieces.value.length} pièce(s) réussi`)
|
total.value = extractTotal(result.data, items.length)
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
items: enrichedItems,
|
||||||
|
total: total.value,
|
||||||
|
page,
|
||||||
|
itemsPerPage
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return result
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erreur lors du chargement des pièces:', error)
|
console.error('Erreur lors du chargement des pièces:', error)
|
||||||
|
return { success: false, error: error.message }
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
@@ -54,10 +145,12 @@ export function usePieces () {
|
|||||||
const createPiece = async (pieceData) => {
|
const createPiece = async (pieceData) => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const result = await post('/pieces', buildConstructeurRequestPayload(pieceData))
|
const normalizedPayload = normalizeRelationIds(buildConstructeurRequestPayload(pieceData))
|
||||||
|
const result = await post('/pieces', normalizedPayload)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const enriched = await withResolvedConstructeurs(result.data)
|
const enriched = await withResolvedConstructeurs(result.data)
|
||||||
pieces.value.push(enriched)
|
pieces.value.unshift(enriched)
|
||||||
|
total.value += 1
|
||||||
const displayName = result.data?.name
|
const displayName = result.data?.name
|
||||||
|| pieceData?.definition?.name
|
|| pieceData?.definition?.name
|
||||||
|| pieceData?.name
|
|| pieceData?.name
|
||||||
@@ -76,7 +169,8 @@ export function usePieces () {
|
|||||||
const updatePieceData = async (id, pieceData) => {
|
const updatePieceData = async (id, pieceData) => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const result = await patch(`/pieces/${id}`, buildConstructeurRequestPayload(pieceData))
|
const normalizedPayload = normalizeRelationIds(buildConstructeurRequestPayload(pieceData))
|
||||||
|
const result = await patch(`/pieces/${id}`, normalizedPayload)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const updated = await withResolvedConstructeurs(result.data)
|
const updated = await withResolvedConstructeurs(result.data)
|
||||||
const index = pieces.value.findIndex(piece => piece.id === id)
|
const index = pieces.value.findIndex(piece => piece.id === id)
|
||||||
@@ -101,6 +195,7 @@ export function usePieces () {
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
const deletedPiece = pieces.value.find(piece => piece.id === id)
|
const deletedPiece = pieces.value.find(piece => piece.id === id)
|
||||||
pieces.value = pieces.value.filter(piece => piece.id !== id)
|
pieces.value = pieces.value.filter(piece => piece.id !== id)
|
||||||
|
total.value = Math.max(0, total.value - 1)
|
||||||
showSuccess(`Pièce "${deletedPiece?.name || 'inconnu'}" supprimée avec succès`)
|
showSuccess(`Pièce "${deletedPiece?.name || 'inconnu'}" supprimée avec succès`)
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
@@ -117,6 +212,7 @@ export function usePieces () {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
pieces,
|
pieces,
|
||||||
|
total,
|
||||||
loading,
|
loading,
|
||||||
loadPieces,
|
loadPieces,
|
||||||
createPiece,
|
createPiece,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useToast } from './useToast'
|
|||||||
import { useApi } from './useApi'
|
import { useApi } from './useApi'
|
||||||
import { buildConstructeurRequestPayload, uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
import { buildConstructeurRequestPayload, uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
||||||
import { useConstructeurs } from './useConstructeurs'
|
import { useConstructeurs } from './useConstructeurs'
|
||||||
|
import { extractRelationId, normalizeRelationIds } from '~/shared/apiRelations'
|
||||||
|
|
||||||
const products = ref([])
|
const products = ref([])
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
@@ -25,6 +26,32 @@ const replaceInCache = (item) => {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const extractCollection = (payload) => {
|
||||||
|
if (Array.isArray(payload)) {
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
if (Array.isArray(payload?.member)) {
|
||||||
|
return payload.member
|
||||||
|
}
|
||||||
|
if (Array.isArray(payload?.['hydra:member'])) {
|
||||||
|
return payload['hydra:member']
|
||||||
|
}
|
||||||
|
if (Array.isArray(payload?.data)) {
|
||||||
|
return payload.data
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const extractTotal = (payload, fallbackLength) => {
|
||||||
|
if (typeof payload?.totalItems === 'number') {
|
||||||
|
return payload.totalItems
|
||||||
|
}
|
||||||
|
if (typeof payload?.['hydra:totalItems'] === 'number') {
|
||||||
|
return payload['hydra:totalItems']
|
||||||
|
}
|
||||||
|
return fallbackLength
|
||||||
|
}
|
||||||
|
|
||||||
export function useProducts () {
|
export function useProducts () {
|
||||||
const { showError } = useToast()
|
const { showError } = useToast()
|
||||||
const { get, post, patch, delete: del } = useApi()
|
const { get, post, patch, delete: del } = useApi()
|
||||||
@@ -34,19 +61,23 @@ export function useProducts () {
|
|||||||
if (!product || typeof product !== 'object') {
|
if (!product || typeof product !== 'object') {
|
||||||
return product
|
return product
|
||||||
}
|
}
|
||||||
|
if (!product.typeProductId) {
|
||||||
|
const typeProductId = extractRelationId(product.typeProduct)
|
||||||
|
if (typeProductId) {
|
||||||
|
product.typeProductId = typeProductId
|
||||||
|
}
|
||||||
|
}
|
||||||
const ids = uniqueConstructeurIds(
|
const ids = uniqueConstructeurIds(
|
||||||
product.constructeurIds,
|
product.constructeurIds,
|
||||||
product.constructeurs,
|
product.constructeurs,
|
||||||
product.constructeur,
|
product.constructeur,
|
||||||
)
|
)
|
||||||
const hasConstructeurs =
|
const hasResolvedConstructeurs =
|
||||||
Array.isArray(product.constructeurs) && product.constructeurs.length > 0
|
Array.isArray(product.constructeurs)
|
||||||
|
&& product.constructeurs.length > 0
|
||||||
|
&& product.constructeurs.every((item) => item && typeof item === 'object')
|
||||||
|
|
||||||
if (hasConstructeurs && ids.length) {
|
if (ids.length && !hasResolvedConstructeurs) {
|
||||||
product.constructeurIds = ids
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ids.length && !hasConstructeurs) {
|
|
||||||
const resolved = await ensureConstructeurs(ids)
|
const resolved = await ensureConstructeurs(ids)
|
||||||
if (resolved.length) {
|
if (resolved.length) {
|
||||||
product.constructeurs = resolved
|
product.constructeurs = resolved
|
||||||
@@ -56,30 +87,62 @@ export function useProducts () {
|
|||||||
return product
|
return product
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load products with pagination and search support
|
||||||
|
* @param {Object} options - Query options
|
||||||
|
* @param {string} [options.search] - Search term for name/reference
|
||||||
|
* @param {number} [options.page=1] - Current page (1-based)
|
||||||
|
* @param {number} [options.itemsPerPage=30] - Items per page
|
||||||
|
* @param {string} [options.orderBy='name'] - Field to order by
|
||||||
|
* @param {string} [options.orderDir='asc'] - Order direction (asc/desc)
|
||||||
|
* @param {boolean} [options.force=false] - Force reload even if already loaded
|
||||||
|
*/
|
||||||
const loadProducts = async (options = {}) => {
|
const loadProducts = async (options = {}) => {
|
||||||
|
const {
|
||||||
|
search = '',
|
||||||
|
page = 1,
|
||||||
|
itemsPerPage = 30,
|
||||||
|
orderBy = 'name',
|
||||||
|
orderDir = 'asc',
|
||||||
|
force = false
|
||||||
|
} = options
|
||||||
|
|
||||||
if (loading.value) {
|
if (loading.value) {
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: { items: products.value, total: total.value },
|
data: { items: products.value, total: total.value, page, itemsPerPage },
|
||||||
}
|
|
||||||
}
|
|
||||||
if (loaded.value && !options.force) {
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
data: { items: products.value, total: total.value },
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
const result = await get('/products?limit=100')
|
const params = new URLSearchParams()
|
||||||
|
params.set('itemsPerPage', String(itemsPerPage))
|
||||||
|
params.set('page', String(page))
|
||||||
|
|
||||||
|
if (search && search.trim()) {
|
||||||
|
params.set('name', search.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
params.set(`order[${orderBy}]`, orderDir)
|
||||||
|
|
||||||
|
const result = await get(`/products?${params.toString()}`)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const items = Array.isArray(result.data?.items) ? result.data.items : []
|
const items = extractCollection(result.data)
|
||||||
const enrichedItems = await Promise.all(items.map((item) => withResolvedConstructeurs(item)))
|
const enrichedItems = await Promise.all(items.map((item) => withResolvedConstructeurs(item)))
|
||||||
products.value = enrichedItems
|
products.value = enrichedItems
|
||||||
total.value = typeof result.data?.total === 'number' ? result.data.total : items.length
|
total.value = extractTotal(result.data, items.length)
|
||||||
loaded.value = true
|
loaded.value = true
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
items: enrichedItems,
|
||||||
|
total: total.value,
|
||||||
|
page,
|
||||||
|
itemsPerPage
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (result.error) {
|
} else if (result.error) {
|
||||||
error.value = result.error
|
error.value = result.error
|
||||||
showError(`Impossible de charger les produits: ${result.error}`)
|
showError(`Impossible de charger les produits: ${result.error}`)
|
||||||
@@ -97,7 +160,7 @@ export function useProducts () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const createProduct = async (payload) => {
|
const createProduct = async (payload) => {
|
||||||
const normalizedPayload = buildConstructeurRequestPayload(payload)
|
const normalizedPayload = normalizeRelationIds(buildConstructeurRequestPayload(payload))
|
||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
@@ -125,7 +188,7 @@ export function useProducts () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const updateProduct = async (id, payload) => {
|
const updateProduct = async (id, payload) => {
|
||||||
const normalizedPayload = buildConstructeurRequestPayload(payload)
|
const normalizedPayload = normalizeRelationIds(buildConstructeurRequestPayload(payload))
|
||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ import { useState, useRequestHeaders, useRuntimeConfig } from '#imports'
|
|||||||
|
|
||||||
const buildUrl = (path) => {
|
const buildUrl = (path) => {
|
||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
const base = config.public.apiBaseUrl?.replace(/\/$/, '') || ''
|
const baseUrl = process.server
|
||||||
|
? (config.apiBaseUrl || config.public.apiBaseUrl || '')
|
||||||
|
: (config.public.apiBaseUrl || '')
|
||||||
|
const base = baseUrl.replace(/\/$/, '')
|
||||||
return `${base}${path}`
|
return `${base}${path}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export function useProfiles () {
|
|||||||
const fetchProfiles = async () => {
|
const fetchProfiles = async () => {
|
||||||
loadingProfiles.value = true
|
loadingProfiles.value = true
|
||||||
try {
|
try {
|
||||||
profiles.value = await $fetch(buildUrl('/profiles'), {
|
profiles.value = await $fetch(buildUrl('/session/profiles'), {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
headers: getSessionHeaders()
|
headers: getSessionHeaders()
|
||||||
@@ -37,7 +37,7 @@ export function useProfiles () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const createProfile = async ({ firstName, lastName }) => {
|
const createProfile = async ({ firstName, lastName }) => {
|
||||||
const profile = await $fetch(buildUrl('/profiles'), {
|
const profile = await $fetch(buildUrl('/session/profiles'), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
body: { firstName, lastName },
|
body: { firstName, lastName },
|
||||||
@@ -48,7 +48,7 @@ export function useProfiles () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const deleteProfile = async (profileId) => {
|
const deleteProfile = async (profileId) => {
|
||||||
await $fetch(buildUrl(`/profiles/${profileId}`), {
|
await $fetch(buildUrl(`/session/profiles/${profileId}`), {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
headers: getSessionHeaders()
|
headers: getSessionHeaders()
|
||||||
|
|||||||
@@ -13,9 +13,20 @@ export function useSites () {
|
|||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const result = await get('/sites')
|
const result = await get('/sites')
|
||||||
|
console.log('sites api result', result)
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
sites.value = result.data
|
const collection = Array.isArray(result.data)
|
||||||
showInfo(`Chargement de ${sites.value.length} site(s) réussi`)
|
? result.data
|
||||||
|
: Array.isArray(result.data?.member)
|
||||||
|
? result.data.member
|
||||||
|
: Array.isArray(result.data?.['hydra:member'])
|
||||||
|
? result.data['hydra:member']
|
||||||
|
: Array.isArray(result.data?.data)
|
||||||
|
? result.data.data
|
||||||
|
: []
|
||||||
|
sites.value = collection
|
||||||
|
showInfo(`Chargement de ${collection.length} site(s) réussi`)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erreur lors du chargement des sites:', error)
|
console.error('Erreur lors du chargement des sites:', error)
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
type="text"
|
type="text"
|
||||||
class="input input-bordered input-sm w-full mt-1"
|
class="input input-bordered input-sm w-full mt-1"
|
||||||
placeholder="Nom ou référence…"
|
placeholder="Nom ou référence…"
|
||||||
|
@input="debouncedSearch"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
@@ -48,6 +49,7 @@
|
|||||||
id="component-catalog-sort"
|
id="component-catalog-sort"
|
||||||
v-model="sortField"
|
v-model="sortField"
|
||||||
class="select select-bordered select-sm"
|
class="select select-bordered select-sm"
|
||||||
|
@change="handleSortChange"
|
||||||
>
|
>
|
||||||
<option value="name">Nom</option>
|
<option value="name">Nom</option>
|
||||||
<option value="createdAt">Date de création</option>
|
<option value="createdAt">Date de création</option>
|
||||||
@@ -64,14 +66,33 @@
|
|||||||
id="component-catalog-dir"
|
id="component-catalog-dir"
|
||||||
v-model="sortDirection"
|
v-model="sortDirection"
|
||||||
class="select select-bordered select-sm"
|
class="select select-bordered select-sm"
|
||||||
|
@change="handleSortChange"
|
||||||
>
|
>
|
||||||
<option value="asc">Ascendant</option>
|
<option value="asc">Ascendant</option>
|
||||||
<option value="desc">Descendant</option>
|
<option value="desc">Descendant</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<label
|
||||||
|
class="text-xs font-semibold uppercase tracking-wide text-base-content/70"
|
||||||
|
for="component-catalog-per-page"
|
||||||
|
>
|
||||||
|
Par page
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="component-catalog-per-page"
|
||||||
|
v-model.number="itemsPerPage"
|
||||||
|
class="select select-bordered select-sm"
|
||||||
|
@change="handlePerPageChange"
|
||||||
|
>
|
||||||
|
<option :value="20">20</option>
|
||||||
|
<option :value="50">50</option>
|
||||||
|
<option :value="100">100</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-base-content/50 lg:text-right">
|
<p class="text-xs text-base-content/50 lg:text-right">
|
||||||
{{ visibleComposants.length }} / {{ composantsTotal }} résultat{{ visibleComposants.length > 1 ? 's' : '' }}
|
{{ composantsOnPage }} / {{ composantsTotal }} résultat{{ composantsTotal > 1 ? 's' : '' }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -83,54 +104,62 @@
|
|||||||
Aucun composant n'a encore été créé.
|
Aucun composant n'a encore été créé.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p v-else-if="!visibleComposants.length" class="text-sm text-base-content/70">
|
<p v-else-if="!composantsList.length" class="text-sm text-base-content/70">
|
||||||
Aucun composant ne correspond à votre recherche.
|
Aucun composant ne correspond à votre recherche.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div v-else class="overflow-x-auto">
|
<template v-else>
|
||||||
<table class="table table-sm md:table-md">
|
<div class="overflow-x-auto">
|
||||||
<thead>
|
<table class="table table-sm md:table-md">
|
||||||
<tr>
|
<thead>
|
||||||
<th class="w-24">Aperçu</th>
|
<tr>
|
||||||
<th>Nom</th>
|
<th class="w-24">Aperçu</th>
|
||||||
<th>Référence</th>
|
<th>Nom</th>
|
||||||
<th>Type de composant</th>
|
<th>Référence</th>
|
||||||
<th>Actions</th>
|
<th>Type de composant</th>
|
||||||
</tr>
|
<th>Actions</th>
|
||||||
</thead>
|
</tr>
|
||||||
<tbody>
|
</thead>
|
||||||
<tr v-for="component in visibleComposants" :key="component.id">
|
<tbody>
|
||||||
<td class="align-middle">
|
<tr v-for="component in composantsList" :key="component.id">
|
||||||
<DocumentThumbnail
|
<td class="align-middle">
|
||||||
:document="resolvePrimaryDocument(component)"
|
<DocumentThumbnail
|
||||||
:alt="resolvePreviewAlt(component)"
|
:document="resolvePrimaryDocument(component)"
|
||||||
/>
|
:alt="resolvePreviewAlt(component)"
|
||||||
</td>
|
/>
|
||||||
<td>{{ component.name || 'Composant sans nom' }}</td>
|
</td>
|
||||||
<td>{{ component.reference || '—' }}</td>
|
<td>{{ component.name || 'Composant sans nom' }}</td>
|
||||||
<td>{{ resolveComponentType(component) }}</td>
|
<td>{{ component.reference || '—' }}</td>
|
||||||
<td>
|
<td>{{ resolveComponentType(component) }}</td>
|
||||||
<div class="flex items-center gap-2">
|
<td>
|
||||||
<NuxtLink
|
<div class="flex items-center gap-2">
|
||||||
:to="`/component/${component.id}/edit`"
|
<NuxtLink
|
||||||
class="btn btn-ghost btn-xs"
|
:to="`/component/${component.id}/edit`"
|
||||||
>
|
class="btn btn-ghost btn-xs"
|
||||||
Modifier
|
>
|
||||||
</NuxtLink>
|
Modifier
|
||||||
<button
|
</NuxtLink>
|
||||||
type="button"
|
<button
|
||||||
class="btn btn-error btn-xs"
|
type="button"
|
||||||
:disabled="loadingComposants"
|
class="btn btn-error btn-xs"
|
||||||
@click="handleDeleteComponent(component)"
|
:disabled="loadingComposants"
|
||||||
>
|
@click="handleDeleteComponent(component)"
|
||||||
Supprimer
|
>
|
||||||
</button>
|
Supprimer
|
||||||
</div>
|
</button>
|
||||||
</td>
|
</div>
|
||||||
</tr>
|
</td>
|
||||||
</tbody>
|
</tr>
|
||||||
</table>
|
</tbody>
|
||||||
</div>
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Pagination
|
||||||
|
:current-page="currentPage"
|
||||||
|
:total-pages="totalPages"
|
||||||
|
@update:current-page="handlePageChange"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
@@ -140,19 +169,80 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
import { useComposants } from '~/composables/useComposants'
|
import { useComposants } from '~/composables/useComposants'
|
||||||
|
import { useComponentTypes } from '~/composables/useComponentTypes'
|
||||||
import { useToast } from '~/composables/useToast'
|
import { useToast } from '~/composables/useToast'
|
||||||
|
import { usePersistedSort } from '~/composables/usePersistedSort'
|
||||||
import DocumentThumbnail from '~/components/DocumentThumbnail.vue'
|
import DocumentThumbnail from '~/components/DocumentThumbnail.vue'
|
||||||
|
import Pagination from '~/components/common/Pagination.vue'
|
||||||
import { isImageDocument, isPdfDocument } from '~/utils/documentPreview'
|
import { isImageDocument, isPdfDocument } from '~/utils/documentPreview'
|
||||||
|
|
||||||
const { showError } = useToast()
|
const { showError } = useToast()
|
||||||
const { composants, loadComposants, loading: loadingComposantsRef, deleteComposant } = useComposants()
|
const { composants, total, loadComposants, loading: loadingComposantsRef, deleteComposant } = useComposants()
|
||||||
|
const { componentTypes, loadComponentTypes } = useComponentTypes()
|
||||||
const loadingComposants = computed(() => loadingComposantsRef.value)
|
const loadingComposants = computed(() => loadingComposantsRef.value)
|
||||||
const composantsList = computed(() => composants.value || [])
|
|
||||||
const composantsTotal = computed(() => composantsList.value.length)
|
|
||||||
|
|
||||||
|
// Pagination state
|
||||||
|
const currentPage = ref(1)
|
||||||
|
const itemsPerPage = ref(30)
|
||||||
|
const composantsTotal = computed(() => total.value)
|
||||||
|
const composantsOnPage = computed(() => composants.value.length)
|
||||||
|
const totalPages = computed(() => Math.ceil(composantsTotal.value / itemsPerPage.value) || 1)
|
||||||
|
|
||||||
|
// Search state with debounce
|
||||||
const searchTerm = ref('')
|
const searchTerm = ref('')
|
||||||
const sortField = ref<'name' | 'createdAt'>('name')
|
let searchTimeout: ReturnType<typeof setTimeout> | null = null
|
||||||
const sortDirection = ref<'asc' | 'desc'>('asc')
|
|
||||||
|
const debouncedSearch = () => {
|
||||||
|
if (searchTimeout) {
|
||||||
|
clearTimeout(searchTimeout)
|
||||||
|
}
|
||||||
|
searchTimeout = setTimeout(() => {
|
||||||
|
currentPage.value = 1
|
||||||
|
fetchComposants()
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort state
|
||||||
|
const { sortField, sortDirection } = usePersistedSort<'name' | 'createdAt', 'asc' | 'desc'>(
|
||||||
|
'component-catalog',
|
||||||
|
{ field: 'name', direction: 'asc' },
|
||||||
|
)
|
||||||
|
|
||||||
|
// Enrichir les composants avec les types de composants complets
|
||||||
|
const composantsList = computed(() => {
|
||||||
|
return (composants.value || []).map((composant) => {
|
||||||
|
const typeComposant = componentTypes.value.find(t => t.id === composant.typeComposantId)
|
||||||
|
return {
|
||||||
|
...composant,
|
||||||
|
typeComposant: typeComposant || composant.typeComposant || null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const fetchComposants = async () => {
|
||||||
|
await loadComposants({
|
||||||
|
search: searchTerm.value,
|
||||||
|
page: currentPage.value,
|
||||||
|
itemsPerPage: itemsPerPage.value,
|
||||||
|
orderBy: sortField.value,
|
||||||
|
orderDir: sortDirection.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePageChange = (page: number) => {
|
||||||
|
currentPage.value = page
|
||||||
|
fetchComposants()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSortChange = () => {
|
||||||
|
currentPage.value = 1
|
||||||
|
fetchComposants()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePerPageChange = () => {
|
||||||
|
currentPage.value = 1
|
||||||
|
fetchComposants()
|
||||||
|
}
|
||||||
|
|
||||||
const resolvePrimaryDocument = (component: Record<string, any>) => {
|
const resolvePrimaryDocument = (component: Record<string, any>) => {
|
||||||
const documents = Array.isArray(component?.documents) ? component.documents : []
|
const documents = Array.isArray(component?.documents) ? component.documents : []
|
||||||
@@ -215,58 +305,6 @@ const resolveDeleteGuard = (component: Record<string, any>) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolveComparableName = (component: Record<string, any>) => {
|
|
||||||
const toComparable = (value?: string | null) =>
|
|
||||||
(value ?? '').toString().trim().toLowerCase()
|
|
||||||
|
|
||||||
return (
|
|
||||||
toComparable(component?.name) ||
|
|
||||||
toComparable(component?.reference) ||
|
|
||||||
toComparable(component?.id)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const resolveComparableDate = (component: Record<string, any>) => {
|
|
||||||
const raw = component?.createdAt ?? component?.created_at ?? null
|
|
||||||
if (!raw) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
const parsed = new Date(raw).getTime()
|
|
||||||
return Number.isNaN(parsed) ? 0 : parsed
|
|
||||||
}
|
|
||||||
|
|
||||||
const visibleComposants = computed(() => {
|
|
||||||
const term = searchTerm.value.trim().toLowerCase()
|
|
||||||
const source = composantsList.value || []
|
|
||||||
|
|
||||||
const filtered = term
|
|
||||||
? source.filter((component) => {
|
|
||||||
const name = (component?.name || '').toLowerCase()
|
|
||||||
const reference = (component?.reference || '').toLowerCase()
|
|
||||||
return (
|
|
||||||
name.includes(term) ||
|
|
||||||
reference.includes(term)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
: [...source]
|
|
||||||
|
|
||||||
const direction = sortDirection.value === 'asc' ? 1 : -1
|
|
||||||
|
|
||||||
return filtered.sort((a, b) => {
|
|
||||||
if (sortField.value === 'name') {
|
|
||||||
return (
|
|
||||||
resolveComparableName(a).localeCompare(
|
|
||||||
resolveComparableName(b),
|
|
||||||
'fr',
|
|
||||||
{ sensitivity: 'base' }
|
|
||||||
) * direction
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (resolveComparableDate(a) - resolveComparableDate(b)) * direction
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleDeleteComponent = async (component: Record<string, any>) => {
|
const handleDeleteComponent = async (component: Record<string, any>) => {
|
||||||
const { blockingReasons, hasCustomFields } = resolveDeleteGuard(component)
|
const { blockingReasons, hasCustomFields } = resolveDeleteGuard(component)
|
||||||
|
|
||||||
@@ -295,9 +333,14 @@ const handleDeleteComponent = async (component: Record<string, any>) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await deleteComposant(component.id)
|
await deleteComposant(component.id)
|
||||||
|
// Reload current page after deletion
|
||||||
|
fetchComposants()
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadComposants()
|
await Promise.all([
|
||||||
|
fetchComposants(),
|
||||||
|
loadComponentTypes()
|
||||||
|
])
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -176,6 +176,18 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="getStructureProducts(selectedTypeStructure).length" class="space-y-2">
|
||||||
|
<h3 class="font-semibold text-sm text-base-content">Produits imposés</h3>
|
||||||
|
<ul class="list-disc list-inside space-y-1">
|
||||||
|
<li
|
||||||
|
v-for="(product, index) in getStructureProducts(selectedTypeStructure)"
|
||||||
|
:key="product.role || product.typeProductId || product.familyCode || index"
|
||||||
|
>
|
||||||
|
{{ resolveProductLabel(product) }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="getStructureSubcomponents(selectedTypeStructure).length" class="space-y-2">
|
<div v-if="getStructureSubcomponents(selectedTypeStructure).length" class="space-y-2">
|
||||||
<h3 class="font-semibold text-sm text-base-content">Sous-composants</h3>
|
<h3 class="font-semibold text-sm text-base-content">Sous-composants</h3>
|
||||||
<ul class="list-disc list-inside space-y-1">
|
<ul class="list-disc list-inside space-y-1">
|
||||||
@@ -189,7 +201,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p
|
<p
|
||||||
v-if="!getStructureCustomFields(selectedTypeStructure).length && !getStructurePieces(selectedTypeStructure).length && !getStructureSubcomponents(selectedTypeStructure).length"
|
v-if="!getStructureCustomFields(selectedTypeStructure).length && !getStructurePieces(selectedTypeStructure).length && !getStructureProducts(selectedTypeStructure).length && !getStructureSubcomponents(selectedTypeStructure).length"
|
||||||
class="text-xs text-gray-500"
|
class="text-xs text-gray-500"
|
||||||
>
|
>
|
||||||
Ce squelette ne définit pas encore de pièces, sous-composants ou valeurs par défaut.
|
Ce squelette ne définit pas encore de pièces, sous-composants ou valeurs par défaut.
|
||||||
@@ -198,6 +210,50 @@
|
|||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="structureSelections.hasAny"
|
||||||
|
class="space-y-3 rounded-lg border border-base-200 bg-base-200/30 p-4"
|
||||||
|
>
|
||||||
|
<header class="space-y-1">
|
||||||
|
<h2 class="font-semibold text-base-content">Sélections actuelles</h2>
|
||||||
|
<p class="text-xs text-base-content/70">
|
||||||
|
Voici les pièces, produits et sous-composants réellement choisis pour ce composant.
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
|
<div v-if="structureSelections.pieces.length" class="space-y-2">
|
||||||
|
<h3 class="font-semibold text-sm text-base-content">Pièces choisies</h3>
|
||||||
|
<ul class="list-disc list-inside space-y-1 text-sm">
|
||||||
|
<li v-for="entry in structureSelections.pieces" :key="`selected-piece-${entry.path}-${entry.id}`">
|
||||||
|
<span class="font-medium">{{ entry.resolvedName }}</span>
|
||||||
|
<span class="text-xs text-base-content/70"> — {{ entry.requirementLabel }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="structureSelections.products.length" class="space-y-2">
|
||||||
|
<h3 class="font-semibold text-sm text-base-content">Produits choisis</h3>
|
||||||
|
<ul class="list-disc list-inside space-y-1 text-sm">
|
||||||
|
<li v-for="entry in structureSelections.products" :key="`selected-product-${entry.path}-${entry.id}`">
|
||||||
|
<span class="font-medium">{{ entry.resolvedName }}</span>
|
||||||
|
<span class="text-xs text-base-content/70"> — {{ entry.requirementLabel }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="structureSelections.components.length" class="space-y-2">
|
||||||
|
<h3 class="font-semibold text-sm text-base-content">Sous-composants choisis</h3>
|
||||||
|
<ul class="list-disc list-inside space-y-1 text-sm">
|
||||||
|
<li v-for="entry in structureSelections.components" :key="`selected-component-${entry.path}-${entry.id}`">
|
||||||
|
<span class="font-medium">{{ entry.resolvedName }}</span>
|
||||||
|
<span class="text-xs text-base-content/70"> — {{ entry.requirementLabel }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="customFieldInputs.length" class="space-y-4 rounded-lg border border-base-200 bg-base-200/40 p-4">
|
<div v-if="customFieldInputs.length" class="space-y-4 rounded-lg border border-base-200 bg-base-200/40 p-4">
|
||||||
<header class="space-y-1">
|
<header class="space-y-1">
|
||||||
<h2 class="font-semibold text-base-content">Champs personnalisés</h2>
|
<h2 class="font-semibold text-base-content">Champs personnalisés</h2>
|
||||||
@@ -400,9 +456,14 @@ import DocumentUpload from '~/components/DocumentUpload.vue'
|
|||||||
import DocumentPreviewModal from '~/components/DocumentPreviewModal.vue'
|
import DocumentPreviewModal from '~/components/DocumentPreviewModal.vue'
|
||||||
import { useComponentTypes } from '~/composables/useComponentTypes'
|
import { useComponentTypes } from '~/composables/useComponentTypes'
|
||||||
import { useComposants } from '~/composables/useComposants'
|
import { useComposants } from '~/composables/useComposants'
|
||||||
|
import { usePieceTypes } from '~/composables/usePieceTypes'
|
||||||
|
import { useProductTypes } from '~/composables/useProductTypes'
|
||||||
|
import { usePieces } from '~/composables/usePieces'
|
||||||
|
import { useProducts } from '~/composables/useProducts'
|
||||||
import { useCustomFields } from '~/composables/useCustomFields'
|
import { useCustomFields } from '~/composables/useCustomFields'
|
||||||
import { useApi } from '~/composables/useApi'
|
import { useApi } from '~/composables/useApi'
|
||||||
import { useToast } from '~/composables/useToast'
|
import { useToast } from '~/composables/useToast'
|
||||||
|
import { extractRelationId } from '~/shared/apiRelations'
|
||||||
import { useDocuments } from '~/composables/useDocuments'
|
import { useDocuments } from '~/composables/useDocuments'
|
||||||
import { useConstructeurs } from '~/composables/useConstructeurs'
|
import { useConstructeurs } from '~/composables/useConstructeurs'
|
||||||
import { formatStructurePreview, normalizeStructureForEditor } from '~/shared/modelUtils'
|
import { formatStructurePreview, normalizeStructureForEditor } from '~/shared/modelUtils'
|
||||||
@@ -433,9 +494,13 @@ const route = useRoute()
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { get } = useApi()
|
const { get } = useApi()
|
||||||
const { componentTypes, loadComponentTypes } = useComponentTypes()
|
const { componentTypes, loadComponentTypes } = useComponentTypes()
|
||||||
const { updateComposant } = useComposants()
|
const { pieceTypes, loadPieceTypes } = usePieceTypes()
|
||||||
|
const { productTypes, loadProductTypes } = useProductTypes()
|
||||||
|
const { updateComposant, loadComposants, composants: componentCatalogRef } = useComposants()
|
||||||
|
const { pieces, loadPieces } = usePieces()
|
||||||
|
const { products, loadProducts } = useProducts()
|
||||||
const { ensureConstructeurs } = useConstructeurs()
|
const { ensureConstructeurs } = useConstructeurs()
|
||||||
const { upsertCustomFieldValue, updateCustomFieldValue } = useCustomFields()
|
const { upsertCustomFieldValue, updateCustomFieldValue, getCustomFieldValuesByEntity } = useCustomFields()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const { loadDocumentsByComponent, uploadDocuments, deleteDocument } = useDocuments()
|
const { loadDocumentsByComponent, uploadDocuments, deleteDocument } = useDocuments()
|
||||||
|
|
||||||
@@ -499,6 +564,46 @@ const documentPreviewSrc = (document: any) => {
|
|||||||
}
|
}
|
||||||
return document.path
|
return document.path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fetchedPieceTypeMap = ref<Record<string, string>>({})
|
||||||
|
const pieceTypeLabelMap = computed(() => ({
|
||||||
|
...Object.fromEntries(
|
||||||
|
(pieceTypes.value || [])
|
||||||
|
.filter((type: any) => type?.id)
|
||||||
|
.map((type: any) => [type.id, type.name || type.code || '']),
|
||||||
|
),
|
||||||
|
...fetchedPieceTypeMap.value,
|
||||||
|
}))
|
||||||
|
const fetchedProductTypeMap = ref<Record<string, string>>({})
|
||||||
|
const productTypeLabelMap = computed(() => ({
|
||||||
|
...Object.fromEntries(
|
||||||
|
(productTypes.value || [])
|
||||||
|
.filter((type: any) => type?.id)
|
||||||
|
.map((type: any) => [type.id, type.name || type.code || '']),
|
||||||
|
),
|
||||||
|
...fetchedProductTypeMap.value,
|
||||||
|
}))
|
||||||
|
const pieceCatalogMap = computed(() =>
|
||||||
|
new Map(
|
||||||
|
(pieces.value || [])
|
||||||
|
.filter((item: any) => item?.id)
|
||||||
|
.map((item: any) => [String(item.id), item]),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
const productCatalogMap = computed(() =>
|
||||||
|
new Map(
|
||||||
|
(products.value || [])
|
||||||
|
.filter((item: any) => item?.id)
|
||||||
|
.map((item: any) => [String(item.id), item]),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
const componentCatalogMap = computed(() =>
|
||||||
|
new Map(
|
||||||
|
(componentCatalogRef.value || [])
|
||||||
|
.filter((item: any) => item?.id)
|
||||||
|
.map((item: any) => [String(item.id), item]),
|
||||||
|
),
|
||||||
|
)
|
||||||
const documentThumbnailClass = (document: any) => {
|
const documentThumbnailClass = (document: any) => {
|
||||||
if (shouldInlinePdf(document) || (isImageDocument(document) && document?.path)) {
|
if (shouldInlinePdf(document) || (isImageDocument(document) && document?.path)) {
|
||||||
return 'h-24 w-20'
|
return 'h-24 w-20'
|
||||||
@@ -593,6 +698,15 @@ const selectedTypeStructure = computed<ComponentModelStructure | null>(() => {
|
|||||||
return structure ? normalizeStructureForEditor(structure) : null
|
return structure ? normalizeStructureForEditor(structure) : null
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const refreshCustomFieldInputs = (
|
||||||
|
structureOverride?: ComponentModelStructure | null,
|
||||||
|
valuesOverride?: any[] | null,
|
||||||
|
) => {
|
||||||
|
const structure = structureOverride ?? selectedTypeStructure.value ?? null
|
||||||
|
const values = valuesOverride ?? component.value?.customFieldValues ?? null
|
||||||
|
customFieldInputs.value = buildCustomFieldInputs(structure, values)
|
||||||
|
}
|
||||||
|
|
||||||
const requiredCustomFieldsFilled = computed(() =>
|
const requiredCustomFieldsFilled = computed(() =>
|
||||||
customFieldInputs.value.every((field) => {
|
customFieldInputs.value.every((field) => {
|
||||||
if (!field.required) {
|
if (!field.required) {
|
||||||
@@ -636,6 +750,12 @@ const fetchComponent = async () => {
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
component.value = result.data
|
component.value = result.data
|
||||||
componentDocuments.value = Array.isArray(result.data?.documents) ? result.data.documents : []
|
componentDocuments.value = Array.isArray(result.data?.documents) ? result.data.documents : []
|
||||||
|
|
||||||
|
const customValues = await getCustomFieldValuesByEntity('composant', result.data.id)
|
||||||
|
if (customValues.success && Array.isArray(customValues.data)) {
|
||||||
|
component.value.customFieldValues = customValues.data
|
||||||
|
refreshCustomFieldInputs(undefined, customValues.data)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
component.value = null
|
component.value = null
|
||||||
componentDocuments.value = []
|
componentDocuments.value = []
|
||||||
@@ -651,7 +771,13 @@ watch(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedTypeId.value = currentComponent.typeComposantId || ''
|
const resolvedTypeId = currentComponent.typeComposantId
|
||||||
|
|| extractRelationId(currentComponent.typeComposant)
|
||||||
|
|| ''
|
||||||
|
if (resolvedTypeId && !currentComponent.typeComposantId) {
|
||||||
|
currentComponent.typeComposantId = resolvedTypeId
|
||||||
|
}
|
||||||
|
selectedTypeId.value = resolvedTypeId
|
||||||
|
|
||||||
editionForm.name = currentComponent.name || ''
|
editionForm.name = currentComponent.name || ''
|
||||||
editionForm.reference = currentComponent.reference || ''
|
editionForm.reference = currentComponent.reference || ''
|
||||||
@@ -665,10 +791,7 @@ watch(
|
|||||||
void ensureConstructeurs(editionForm.constructeurIds)
|
void ensureConstructeurs(editionForm.constructeurIds)
|
||||||
}
|
}
|
||||||
|
|
||||||
customFieldInputs.value = buildCustomFieldInputs(
|
refreshCustomFieldInputs(currentStructure, currentComponent.customFieldValues)
|
||||||
currentStructure,
|
|
||||||
currentComponent.customFieldValues,
|
|
||||||
)
|
|
||||||
|
|
||||||
initialized = true
|
initialized = true
|
||||||
},
|
},
|
||||||
@@ -679,10 +802,7 @@ watch(selectedTypeStructure, (currentStructure) => {
|
|||||||
if (!component.value) {
|
if (!component.value) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
customFieldInputs.value = buildCustomFieldInputs(
|
refreshCustomFieldInputs(currentStructure, component.value.customFieldValues)
|
||||||
currentStructure,
|
|
||||||
component.value.customFieldValues,
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const submitEdition = async () => {
|
const submitEdition = async () => {
|
||||||
@@ -707,7 +827,7 @@ const submitEdition = async () => {
|
|||||||
if (rawPrice) {
|
if (rawPrice) {
|
||||||
const parsed = Number(rawPrice)
|
const parsed = Number(rawPrice)
|
||||||
if (!Number.isNaN(parsed)) {
|
if (!Number.isNaN(parsed)) {
|
||||||
payload.prix = parsed
|
payload.prix = String(parsed)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
payload.prix = null
|
payload.prix = null
|
||||||
@@ -990,6 +1110,10 @@ const getStructurePieces = (structure: ComponentModelStructure | null) => {
|
|||||||
return Array.isArray(structure?.pieces) ? structure.pieces : []
|
return Array.isArray(structure?.pieces) ? structure.pieces : []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getStructureProducts = (structure: ComponentModelStructure | null) => {
|
||||||
|
return Array.isArray(structure?.products) ? structure.products : []
|
||||||
|
}
|
||||||
|
|
||||||
const getStructureSubcomponents = (structure: ComponentModelStructure | null) => {
|
const getStructureSubcomponents = (structure: ComponentModelStructure | null) => {
|
||||||
if (Array.isArray(structure?.subcomponents)) {
|
if (Array.isArray(structure?.subcomponents)) {
|
||||||
return structure.subcomponents
|
return structure.subcomponents
|
||||||
@@ -998,6 +1122,9 @@ const getStructureSubcomponents = (structure: ComponentModelStructure | null) =>
|
|||||||
return Array.isArray(legacy) ? legacy : []
|
return Array.isArray(legacy) ? legacy : []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isNonEmptyString = (value: unknown): value is string =>
|
||||||
|
typeof value === 'string' && value.trim().length > 0
|
||||||
|
|
||||||
const resolvePieceLabel = (piece: Record<string, any>) => {
|
const resolvePieceLabel = (piece: Record<string, any>) => {
|
||||||
const parts: string[] = []
|
const parts: string[] = []
|
||||||
if (piece.role) {
|
if (piece.role) {
|
||||||
@@ -1007,6 +1134,8 @@ const resolvePieceLabel = (piece: Record<string, any>) => {
|
|||||||
parts.push(piece.typePiece.name)
|
parts.push(piece.typePiece.name)
|
||||||
} else if (piece.typePieceLabel) {
|
} else if (piece.typePieceLabel) {
|
||||||
parts.push(piece.typePieceLabel)
|
parts.push(piece.typePieceLabel)
|
||||||
|
} else if (piece.typePieceId && pieceTypeLabelMap.value[piece.typePieceId]) {
|
||||||
|
parts.push(pieceTypeLabelMap.value[piece.typePieceId])
|
||||||
} else if (piece.typePiece?.code) {
|
} else if (piece.typePiece?.code) {
|
||||||
parts.push(`Famille ${piece.typePiece.code}`)
|
parts.push(`Famille ${piece.typePiece.code}`)
|
||||||
} else if (piece.familyCode) {
|
} else if (piece.familyCode) {
|
||||||
@@ -1017,6 +1146,91 @@ const resolvePieceLabel = (piece: Record<string, any>) => {
|
|||||||
return parts.length ? parts.join(' • ') : 'Pièce'
|
return parts.length ? parts.join(' • ') : 'Pièce'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fetchPieceTypeNames = async (ids: string[]) => {
|
||||||
|
const missing = ids.filter((id) => id && !pieceTypeLabelMap.value[id])
|
||||||
|
if (!missing.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const results = await Promise.allSettled(
|
||||||
|
missing.map((id) => get(`/model_types/${id}`)),
|
||||||
|
)
|
||||||
|
const next = { ...fetchedPieceTypeMap.value }
|
||||||
|
results.forEach((result, index) => {
|
||||||
|
if (result.status !== 'fulfilled') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const data = result.value?.data
|
||||||
|
const name = data?.name || data?.code
|
||||||
|
if (name) {
|
||||||
|
next[missing[index]] = name
|
||||||
|
}
|
||||||
|
})
|
||||||
|
fetchedPieceTypeMap.value = next
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolveProductLabel = (product: Record<string, any>) => {
|
||||||
|
const parts: string[] = []
|
||||||
|
if (product.role) {
|
||||||
|
parts.push(product.role)
|
||||||
|
}
|
||||||
|
if (product.typeProduct?.name) {
|
||||||
|
parts.push(product.typeProduct.name)
|
||||||
|
} else if (product.typeProductLabel) {
|
||||||
|
parts.push(product.typeProductLabel)
|
||||||
|
} else if (product.typeProductId && productTypeLabelMap.value[product.typeProductId]) {
|
||||||
|
parts.push(productTypeLabelMap.value[product.typeProductId])
|
||||||
|
} else if (product.typeProduct?.code) {
|
||||||
|
parts.push(`Catégorie ${product.typeProduct.code}`)
|
||||||
|
} else if (product.familyCode) {
|
||||||
|
parts.push(`Catégorie ${product.familyCode}`)
|
||||||
|
} else if (product.typeProductId) {
|
||||||
|
parts.push(`#${product.typeProductId}`)
|
||||||
|
}
|
||||||
|
return parts.length ? parts.join(' • ') : 'Produit'
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchProductTypeNames = async (ids: string[]) => {
|
||||||
|
const missing = ids.filter((id) => id && !productTypeLabelMap.value[id])
|
||||||
|
if (!missing.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const results = await Promise.allSettled(
|
||||||
|
missing.map((id) => get(`/model_types/${id}`)),
|
||||||
|
)
|
||||||
|
const next = { ...fetchedProductTypeMap.value }
|
||||||
|
results.forEach((result, index) => {
|
||||||
|
if (result.status !== 'fulfilled') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const data = result.value?.data
|
||||||
|
const name = data?.name || data?.code
|
||||||
|
if (name) {
|
||||||
|
next[missing[index]] = name
|
||||||
|
}
|
||||||
|
})
|
||||||
|
fetchedProductTypeMap.value = next
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
selectedTypeStructure,
|
||||||
|
(structure) => {
|
||||||
|
const pieceIds = getStructurePieces(structure)
|
||||||
|
.map((piece: any) => piece?.typePieceId)
|
||||||
|
.filter((id: any): id is string => typeof id === 'string' && id.trim().length > 0)
|
||||||
|
if (pieceIds.length) {
|
||||||
|
fetchPieceTypeNames(Array.from(new Set(pieceIds))).catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
const productIds = getStructureProducts(structure)
|
||||||
|
.map((product: any) => product?.typeProductId)
|
||||||
|
.filter((id: any): id is string => typeof id === 'string' && id.trim().length > 0)
|
||||||
|
if (productIds.length) {
|
||||||
|
fetchProductTypeNames(Array.from(new Set(productIds))).catch(() => {})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
const resolveSubcomponentLabel = (node: Record<string, any>) => {
|
const resolveSubcomponentLabel = (node: Record<string, any>) => {
|
||||||
const parts: string[] = []
|
const parts: string[] = []
|
||||||
if (node.alias) {
|
if (node.alias) {
|
||||||
@@ -1043,6 +1257,104 @@ const resolveSubcomponentLabel = (node: Record<string, any>) => {
|
|||||||
return parts.length ? parts.join(' • ') : 'Sous-composant'
|
return parts.length ? parts.join(' • ') : 'Sous-composant'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SelectionEntry = {
|
||||||
|
id: string
|
||||||
|
path: string
|
||||||
|
requirementLabel: string
|
||||||
|
resolvedName: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const collectStructureSelections = (root: any): {
|
||||||
|
pieces: SelectionEntry[]
|
||||||
|
products: SelectionEntry[]
|
||||||
|
components: SelectionEntry[]
|
||||||
|
} => {
|
||||||
|
const piecesSelected: SelectionEntry[] = []
|
||||||
|
const productsSelected: SelectionEntry[] = []
|
||||||
|
const componentsSelected: SelectionEntry[] = []
|
||||||
|
|
||||||
|
if (!root || typeof root !== 'object') {
|
||||||
|
return { pieces: piecesSelected, products: productsSelected, components: componentsSelected }
|
||||||
|
}
|
||||||
|
|
||||||
|
const visitNode = (node: any, fallbackPath = 'racine') => {
|
||||||
|
if (!node || typeof node !== 'object') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodePath = isNonEmptyString(node.path) ? node.path : fallbackPath
|
||||||
|
|
||||||
|
const nodePieces = Array.isArray(node.pieces) ? node.pieces : []
|
||||||
|
nodePieces.forEach((entry: any, index: number) => {
|
||||||
|
const selectedId = entry?.selectedPieceId
|
||||||
|
if (!isNonEmptyString(selectedId)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const definition = entry?.definition ?? entry
|
||||||
|
const catalogPiece = pieceCatalogMap.value.get(selectedId)
|
||||||
|
piecesSelected.push({
|
||||||
|
id: selectedId,
|
||||||
|
path: isNonEmptyString(entry?.path) ? entry.path : `${nodePath}:piece-${index + 1}`,
|
||||||
|
requirementLabel: resolvePieceLabel(definition),
|
||||||
|
resolvedName: catalogPiece?.name || selectedId,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const nodeProducts = Array.isArray(node.products) ? node.products : []
|
||||||
|
nodeProducts.forEach((entry: any, index: number) => {
|
||||||
|
const selectedId = entry?.selectedProductId
|
||||||
|
if (!isNonEmptyString(selectedId)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const definition = entry?.definition ?? entry
|
||||||
|
const catalogProduct = productCatalogMap.value.get(selectedId)
|
||||||
|
productsSelected.push({
|
||||||
|
id: selectedId,
|
||||||
|
path: isNonEmptyString(entry?.path) ? entry.path : `${nodePath}:product-${index + 1}`,
|
||||||
|
requirementLabel: resolveProductLabel(definition),
|
||||||
|
resolvedName: catalogProduct?.name || selectedId,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const nodeChildren = Array.isArray(node.subcomponents)
|
||||||
|
? node.subcomponents
|
||||||
|
: Array.isArray(node.subComponents)
|
||||||
|
? node.subComponents
|
||||||
|
: []
|
||||||
|
|
||||||
|
nodeChildren.forEach((child: any, index: number) => {
|
||||||
|
const selectedId = child?.selectedComponentId
|
||||||
|
if (isNonEmptyString(selectedId)) {
|
||||||
|
const definition = child?.definition ?? child
|
||||||
|
const catalogComponent = componentCatalogMap.value.get(selectedId)
|
||||||
|
componentsSelected.push({
|
||||||
|
id: selectedId,
|
||||||
|
path: isNonEmptyString(child?.path) ? child.path : `${nodePath}:subcomponent-${index + 1}`,
|
||||||
|
requirementLabel: resolveSubcomponentLabel(definition),
|
||||||
|
resolvedName: catalogComponent?.name || selectedId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
visitNode(child, isNonEmptyString(child?.path) ? child.path : `${nodePath}:subcomponent-${index + 1}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
visitNode(root, isNonEmptyString(root?.path) ? root.path : 'racine')
|
||||||
|
|
||||||
|
return { pieces: piecesSelected, products: productsSelected, components: componentsSelected }
|
||||||
|
}
|
||||||
|
|
||||||
|
const structureSelections = computed(() => {
|
||||||
|
const selections = collectStructureSelections(component.value?.structure)
|
||||||
|
const total =
|
||||||
|
selections.pieces.length + selections.products.length + selections.components.length
|
||||||
|
return {
|
||||||
|
...selections,
|
||||||
|
total,
|
||||||
|
hasAny: total > 0,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const buildCustomFieldMetadata = (field: CustomFieldInput) => ({
|
const buildCustomFieldMetadata = (field: CustomFieldInput) => ({
|
||||||
customFieldName: field.name,
|
customFieldName: field.name,
|
||||||
customFieldType: field.type,
|
customFieldType: field.type,
|
||||||
@@ -1142,7 +1454,15 @@ const saveCustomFieldValues = async (updatedComponent: any) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await Promise.allSettled([loadComponentTypes(), fetchComponent()])
|
await Promise.allSettled([
|
||||||
|
loadComponentTypes(),
|
||||||
|
loadPieceTypes(),
|
||||||
|
loadProductTypes(),
|
||||||
|
loadPieces({ itemsPerPage: 500 }),
|
||||||
|
loadProducts({ itemsPerPage: 500, force: true }),
|
||||||
|
loadComposants({ itemsPerPage: 500 }),
|
||||||
|
fetchComponent(),
|
||||||
|
])
|
||||||
loading.value = false
|
loading.value = false
|
||||||
if (component.value?.id) {
|
if (component.value?.id) {
|
||||||
await refreshDocuments()
|
await refreshDocuments()
|
||||||
|
|||||||
@@ -212,6 +212,9 @@
|
|||||||
:pieces-loading="piecesLoading"
|
:pieces-loading="piecesLoading"
|
||||||
:products-loading="productsLoading"
|
:products-loading="productsLoading"
|
||||||
:components-loading="componentsLoading"
|
:components-loading="componentsLoading"
|
||||||
|
:piece-type-label-map="pieceTypeLabelMap"
|
||||||
|
:product-type-label-map="productTypeLabelMap"
|
||||||
|
:component-type-label-map="componentTypeLabelMap"
|
||||||
/>
|
/>
|
||||||
<p v-else class="text-xs text-error">
|
<p v-else class="text-xs text-error">
|
||||||
Impossible de générer les emplacements définis par le squelette.
|
Impossible de générer les emplacements définis par le squelette.
|
||||||
@@ -349,7 +352,10 @@ import SearchSelect from '~/components/common/SearchSelect.vue'
|
|||||||
import { useComponentTypes } from '~/composables/useComponentTypes'
|
import { useComponentTypes } from '~/composables/useComponentTypes'
|
||||||
import { useComposants } from '~/composables/useComposants'
|
import { useComposants } from '~/composables/useComposants'
|
||||||
import { usePieces } from '~/composables/usePieces'
|
import { usePieces } from '~/composables/usePieces'
|
||||||
|
import { usePieceTypes } from '~/composables/usePieceTypes'
|
||||||
import { useProducts } from '~/composables/useProducts'
|
import { useProducts } from '~/composables/useProducts'
|
||||||
|
import { useProductTypes } from '~/composables/useProductTypes'
|
||||||
|
import { useApi } from '~/composables/useApi'
|
||||||
import { useToast } from '~/composables/useToast'
|
import { useToast } from '~/composables/useToast'
|
||||||
import { useCustomFields } from '~/composables/useCustomFields'
|
import { useCustomFields } from '~/composables/useCustomFields'
|
||||||
import { useDocuments } from '~/composables/useDocuments'
|
import { useDocuments } from '~/composables/useDocuments'
|
||||||
@@ -370,22 +376,22 @@ interface ComponentCatalogType extends ModelType {
|
|||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const { get } = useApi()
|
||||||
|
|
||||||
const { componentTypes, loadComponentTypes, loadingComponentTypes } = useComponentTypes()
|
const { componentTypes, loadComponentTypes, loadingComponentTypes } = useComponentTypes()
|
||||||
|
const { pieceTypes, loadPieceTypes } = usePieceTypes()
|
||||||
|
const { productTypes, loadProductTypes } = useProductTypes()
|
||||||
const {
|
const {
|
||||||
createComposant,
|
createComposant,
|
||||||
composants: componentCatalogRef,
|
composants: componentCatalogRef,
|
||||||
loadComposants,
|
|
||||||
loading: componentsLoading,
|
loading: componentsLoading,
|
||||||
} = useComposants()
|
} = useComposants()
|
||||||
const {
|
const {
|
||||||
pieces: pieceCatalogRef,
|
pieces: pieceCatalogRef,
|
||||||
loadPieces,
|
|
||||||
loading: piecesLoading,
|
loading: piecesLoading,
|
||||||
} = usePieces()
|
} = usePieces()
|
||||||
const {
|
const {
|
||||||
products: productCatalogRef,
|
products: productCatalogRef,
|
||||||
loadProducts,
|
|
||||||
loading: productsLoading,
|
loading: productsLoading,
|
||||||
} = useProducts()
|
} = useProducts()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
@@ -414,6 +420,30 @@ const structureDataLoading = computed(
|
|||||||
() => piecesLoading.value || componentsLoading.value || productsLoading.value,
|
() => piecesLoading.value || componentsLoading.value || productsLoading.value,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const fetchedPieceTypeMap = ref<Record<string, string>>({})
|
||||||
|
const pieceTypeLabelMap = computed(() => ({
|
||||||
|
...Object.fromEntries(
|
||||||
|
(pieceTypes.value || [])
|
||||||
|
.filter((type: any) => type?.id)
|
||||||
|
.map((type: any) => [type.id, type.name || type.code || '']),
|
||||||
|
),
|
||||||
|
...fetchedPieceTypeMap.value,
|
||||||
|
}))
|
||||||
|
const productTypeLabelMap = computed(() =>
|
||||||
|
Object.fromEntries(
|
||||||
|
(productTypes.value || [])
|
||||||
|
.filter((type: any) => type?.id)
|
||||||
|
.map((type: any) => [type.id, type.name || type.code || '']),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
const componentTypeLabelMap = computed(() =>
|
||||||
|
Object.fromEntries(
|
||||||
|
(componentTypes.value || [])
|
||||||
|
.filter((type: any) => type?.id)
|
||||||
|
.map((type: any) => [type.id, type.name || type.code || '']),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => route.query.typeId,
|
() => route.query.typeId,
|
||||||
(value) => {
|
(value) => {
|
||||||
@@ -778,6 +808,8 @@ const resolvePieceLabel = (piece: Record<string, any>) => {
|
|||||||
parts.push(piece.typePiece.name)
|
parts.push(piece.typePiece.name)
|
||||||
} else if (piece.typePieceLabel) {
|
} else if (piece.typePieceLabel) {
|
||||||
parts.push(piece.typePieceLabel)
|
parts.push(piece.typePieceLabel)
|
||||||
|
} else if (piece.typePieceId && pieceTypeLabelMap.value[piece.typePieceId]) {
|
||||||
|
parts.push(pieceTypeLabelMap.value[piece.typePieceId])
|
||||||
} else if (piece.typePiece?.code) {
|
} else if (piece.typePiece?.code) {
|
||||||
parts.push(`Famille ${piece.typePiece.code}`)
|
parts.push(`Famille ${piece.typePiece.code}`)
|
||||||
} else if (piece.familyCode) {
|
} else if (piece.familyCode) {
|
||||||
@@ -788,6 +820,42 @@ const resolvePieceLabel = (piece: Record<string, any>) => {
|
|||||||
return parts.length ? parts.join(' • ') : 'Pièce'
|
return parts.length ? parts.join(' • ') : 'Pièce'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fetchPieceTypeNames = async (ids: string[]) => {
|
||||||
|
const missing = ids.filter((id) => id && !pieceTypeLabelMap.value[id])
|
||||||
|
if (!missing.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const results = await Promise.allSettled(
|
||||||
|
missing.map((id) => get(`/model_types/${id}`)),
|
||||||
|
)
|
||||||
|
const next = { ...fetchedPieceTypeMap.value }
|
||||||
|
results.forEach((result, index) => {
|
||||||
|
if (result.status !== 'fulfilled') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const data = result.value?.data
|
||||||
|
const name = data?.name || data?.code
|
||||||
|
if (name) {
|
||||||
|
next[missing[index]] = name
|
||||||
|
}
|
||||||
|
})
|
||||||
|
fetchedPieceTypeMap.value = next
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
selectedTypeStructure,
|
||||||
|
(structure) => {
|
||||||
|
const ids = getStructurePieces(structure)
|
||||||
|
.map((piece: any) => piece?.typePieceId)
|
||||||
|
.filter((id: any): id is string => typeof id === 'string' && id.trim().length > 0)
|
||||||
|
if (!ids.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fetchPieceTypeNames(Array.from(new Set(ids))).catch(() => {})
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
const resolveProductLabel = (product: Record<string, any>) => {
|
const resolveProductLabel = (product: Record<string, any>) => {
|
||||||
const parts: string[] = []
|
const parts: string[] = []
|
||||||
if (product.role) {
|
if (product.role) {
|
||||||
@@ -870,7 +938,7 @@ const submitCreation = async () => {
|
|||||||
if (rawPrice) {
|
if (rawPrice) {
|
||||||
const parsed = Number(rawPrice)
|
const parsed = Number(rawPrice)
|
||||||
if (!Number.isNaN(parsed)) {
|
if (!Number.isNaN(parsed)) {
|
||||||
payload.prix = parsed
|
payload.prix = String(parsed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -934,9 +1002,8 @@ const submitCreation = async () => {
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await Promise.allSettled([
|
await Promise.allSettled([
|
||||||
loadComponentTypes(),
|
loadComponentTypes(),
|
||||||
loadPieces(),
|
loadPieceTypes(),
|
||||||
loadComposants(),
|
loadProductTypes(),
|
||||||
loadProducts(),
|
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ import FieldEmail from '~/components/form/FieldEmail.vue'
|
|||||||
import FieldPhone from '~/components/form/FieldPhone.vue'
|
import FieldPhone from '~/components/form/FieldPhone.vue'
|
||||||
import { useConstructeurs } from '~/composables/useConstructeurs'
|
import { useConstructeurs } from '~/composables/useConstructeurs'
|
||||||
import { useToast } from '~/composables/useToast'
|
import { useToast } from '~/composables/useToast'
|
||||||
|
import { usePersistedValue } from '~/composables/usePersistedValue'
|
||||||
import { formatPhone } from '~/utils/formatters/phone'
|
import { formatPhone } from '~/utils/formatters/phone'
|
||||||
import IconLucidePlus from '~icons/lucide/plus'
|
import IconLucidePlus from '~icons/lucide/plus'
|
||||||
|
|
||||||
@@ -129,7 +130,7 @@ const { constructeurs, loading, searchConstructeurs, createConstructeur, updateC
|
|||||||
const { showError, showSuccess } = useToast()
|
const { showError, showSuccess } = useToast()
|
||||||
|
|
||||||
const searchTerm = ref('')
|
const searchTerm = ref('')
|
||||||
const sortKey = ref('name')
|
const sortKey = usePersistedValue('constructeurs-sort', 'name')
|
||||||
const modalOpen = ref(false)
|
const modalOpen = ref(false)
|
||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
const editingConstructeur = ref(null)
|
const editingConstructeur = ref(null)
|
||||||
|
|||||||
@@ -472,10 +472,11 @@ import IconLucideChevronDown from '~icons/lucide/chevron-down'
|
|||||||
import IconLucideSettings2 from '~icons/lucide/settings-2'
|
import IconLucideSettings2 from '~icons/lucide/settings-2'
|
||||||
import IconLucideTag from '~icons/lucide/tag'
|
import IconLucideTag from '~icons/lucide/tag'
|
||||||
import { formatPhone } from '~/utils/formatters/phone'
|
import { formatPhone } from '~/utils/formatters/phone'
|
||||||
|
import { extractRelationId } from '~/shared/apiRelations'
|
||||||
|
|
||||||
const { sites, loading, loadSites, createSite } = useSites()
|
const { sites, loading, loadSites, createSite } = useSites()
|
||||||
const { machineTypes, loadMachineTypes } = useMachineTypesApi()
|
const { machineTypes, loadMachineTypes } = useMachineTypesApi()
|
||||||
const { createMachineFromType, deleteMachine } = useMachines()
|
const { machines, loadMachines, createMachineFromType, deleteMachine } = useMachines()
|
||||||
|
|
||||||
// Data
|
// Data
|
||||||
const showAddSiteModal = ref(false)
|
const showAddSiteModal = ref(false)
|
||||||
@@ -517,8 +518,50 @@ const categories = computed(() => {
|
|||||||
return Array.from(cats)
|
return Array.from(cats)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const machinesWithType = computed(() => {
|
||||||
|
return machines.value.map((machine) => {
|
||||||
|
const resolvedTypeMachineId = machine.typeMachineId || extractRelationId(machine.typeMachine)
|
||||||
|
const resolvedTypeMachine = resolvedTypeMachineId
|
||||||
|
? machineTypes.value.find(type => type.id === resolvedTypeMachineId) || null
|
||||||
|
: null
|
||||||
|
|
||||||
|
return {
|
||||||
|
...machine,
|
||||||
|
typeMachineId: resolvedTypeMachineId || machine.typeMachineId,
|
||||||
|
typeMachine:
|
||||||
|
machine.typeMachine && typeof machine.typeMachine === 'object'
|
||||||
|
? machine.typeMachine
|
||||||
|
: resolvedTypeMachine
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const machinesBySiteId = computed(() => {
|
||||||
|
const map = new Map()
|
||||||
|
|
||||||
|
machinesWithType.value.forEach((machine) => {
|
||||||
|
const siteId = machine.siteId || extractRelationId(machine.site)
|
||||||
|
if (!siteId) { return }
|
||||||
|
|
||||||
|
if (!map.has(siteId)) {
|
||||||
|
map.set(siteId, [])
|
||||||
|
}
|
||||||
|
|
||||||
|
map.get(siteId).push(machine)
|
||||||
|
})
|
||||||
|
|
||||||
|
return map
|
||||||
|
})
|
||||||
|
|
||||||
|
const sitesWithMachines = computed(() => {
|
||||||
|
return sites.value.map((site) => ({
|
||||||
|
...site,
|
||||||
|
machines: machinesBySiteId.value.get(site.id) || []
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
const totalMachines = computed(() => {
|
const totalMachines = computed(() => {
|
||||||
return sites.value.reduce((total, site) => {
|
return sitesWithMachines.value.reduce((total, site) => {
|
||||||
return total + (site.machines?.length || 0)
|
return total + (site.machines?.length || 0)
|
||||||
}, 0)
|
}, 0)
|
||||||
})
|
})
|
||||||
@@ -532,7 +575,7 @@ const formatPhoneDisplay = (value) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const filteredSites = computed(() => {
|
const filteredSites = computed(() => {
|
||||||
let filtered = sites.value
|
let filtered = sitesWithMachines.value
|
||||||
|
|
||||||
// Filtrer par terme de recherche
|
// Filtrer par terme de recherche
|
||||||
if (searchTerm.value) {
|
if (searchTerm.value) {
|
||||||
@@ -551,9 +594,11 @@ const filteredSites = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const machineMatches = site.machines?.some(
|
const machineMatches = site.machines?.some(
|
||||||
machine =>
|
machine => {
|
||||||
machine.name.toLowerCase().includes(lowerTerm) ||
|
const name = (machine.name || '').toLowerCase()
|
||||||
machine.reference?.toLowerCase().includes(lowerTerm)
|
const reference = (machine.reference || '').toLowerCase()
|
||||||
|
return name.includes(lowerTerm) || reference.includes(lowerTerm)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
return siteMatches || machineMatches
|
return siteMatches || machineMatches
|
||||||
@@ -637,6 +682,7 @@ const handleCreateMachine = async () => {
|
|||||||
newMachine.typeMachineId = ''
|
newMachine.typeMachineId = ''
|
||||||
newMachine.reference = ''
|
newMachine.reference = ''
|
||||||
showAddMachineModal.value = false
|
showAddMachineModal.value = false
|
||||||
|
await loadMachines()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -671,6 +717,7 @@ const confirmDeleteMachine = async (machine) => {
|
|||||||
const result = await deleteMachine(machine.id)
|
const result = await deleteMachine(machine.id)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
showSuccess(`Machine "${machine.name}" supprimée avec succès`)
|
showSuccess(`Machine "${machine.name}" supprimée avec succès`)
|
||||||
|
await loadMachines()
|
||||||
} else {
|
} else {
|
||||||
showError(`Erreur lors de la suppression: ${result.error}`)
|
showError(`Erreur lors de la suppression: ${result.error}`)
|
||||||
}
|
}
|
||||||
@@ -698,6 +745,6 @@ const getCategoryBadgeClass = (category) => {
|
|||||||
|
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await Promise.all([loadSites(), loadMachineTypes()])
|
await Promise.all([loadSites(), loadMachineTypes(), loadMachines()])
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -86,6 +86,7 @@
|
|||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { useMachineTypesApi } from '~/composables/useMachineTypesApi'
|
import { useMachineTypesApi } from '~/composables/useMachineTypesApi'
|
||||||
import { useToast } from '~/composables/useToast'
|
import { useToast } from '~/composables/useToast'
|
||||||
|
import { extractRelationId } from '~/shared/apiRelations'
|
||||||
import IconLucidePlus from '~icons/lucide/plus'
|
import IconLucidePlus from '~icons/lucide/plus'
|
||||||
import IconLucideClipboardList from '~icons/lucide/clipboard-list'
|
import IconLucideClipboardList from '~icons/lucide/clipboard-list'
|
||||||
import IconLucideList from '~icons/lucide/list'
|
import IconLucideList from '~icons/lucide/list'
|
||||||
@@ -142,6 +143,20 @@ const parseOptions = (field = {}) => {
|
|||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toModelTypeIri = (value) => {
|
||||||
|
if (!value) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
if (typeof value === 'string' && value.startsWith('/api/model_types/')) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
const relationId = extractRelationId(value)
|
||||||
|
if (relationId) {
|
||||||
|
return `/api/model_types/${relationId}`
|
||||||
|
}
|
||||||
|
return typeof value === 'string' ? `/api/model_types/${value}` : undefined
|
||||||
|
}
|
||||||
|
|
||||||
const normalizeCustomFields = (fields = []) =>
|
const normalizeCustomFields = (fields = []) =>
|
||||||
fields
|
fields
|
||||||
.filter(field => field?.name && field.name.trim() !== '')
|
.filter(field => field?.name && field.name.trim() !== '')
|
||||||
@@ -165,9 +180,9 @@ const toIntegerOrNull = (value, fallback = null) => {
|
|||||||
|
|
||||||
const normalizeComponentRequirements = (requirements = []) =>
|
const normalizeComponentRequirements = (requirements = []) =>
|
||||||
requirements
|
requirements
|
||||||
.filter(req => req?.typeComposantId)
|
.filter(req => req?.typeComposantId || req?.typeComposant)
|
||||||
.map((req, index) => ({
|
.map((req, index) => ({
|
||||||
typeComposantId: req.typeComposantId,
|
typeComposant: toModelTypeIri(req.typeComposantId || req.typeComposant),
|
||||||
label: req.label?.trim() ? req.label.trim() : undefined,
|
label: req.label?.trim() ? req.label.trim() : undefined,
|
||||||
minCount: toIntegerOrNull(req.minCount, 1),
|
minCount: toIntegerOrNull(req.minCount, 1),
|
||||||
maxCount: toIntegerOrNull(req.maxCount, null),
|
maxCount: toIntegerOrNull(req.maxCount, null),
|
||||||
@@ -180,9 +195,9 @@ const normalizeComponentRequirements = (requirements = []) =>
|
|||||||
|
|
||||||
const normalizePieceRequirements = (requirements = []) =>
|
const normalizePieceRequirements = (requirements = []) =>
|
||||||
requirements
|
requirements
|
||||||
.filter(req => req?.typePieceId)
|
.filter(req => req?.typePieceId || req?.typePiece)
|
||||||
.map((req, index) => ({
|
.map((req, index) => ({
|
||||||
typePieceId: req.typePieceId,
|
typePiece: toModelTypeIri(req.typePieceId || req.typePiece),
|
||||||
label: req.label?.trim() ? req.label.trim() : undefined,
|
label: req.label?.trim() ? req.label.trim() : undefined,
|
||||||
minCount: toIntegerOrNull(req.minCount, 0),
|
minCount: toIntegerOrNull(req.minCount, 0),
|
||||||
maxCount: toIntegerOrNull(req.maxCount, null),
|
maxCount: toIntegerOrNull(req.maxCount, null),
|
||||||
@@ -195,9 +210,9 @@ const normalizePieceRequirements = (requirements = []) =>
|
|||||||
|
|
||||||
const normalizeProductRequirements = (requirements = []) =>
|
const normalizeProductRequirements = (requirements = []) =>
|
||||||
requirements
|
requirements
|
||||||
.filter(req => req?.typeProductId)
|
.filter(req => req?.typeProductId || req?.typeProduct)
|
||||||
.map((req, index) => ({
|
.map((req, index) => ({
|
||||||
typeProductId: req.typeProductId,
|
typeProduct: toModelTypeIri(req.typeProductId || req.typeProduct),
|
||||||
label: req.label?.trim() ? req.label.trim() : undefined,
|
label: req.label?.trim() ? req.label.trim() : undefined,
|
||||||
minCount: toIntegerOrNull(req.minCount, 0),
|
minCount: toIntegerOrNull(req.minCount, 0),
|
||||||
maxCount: toIntegerOrNull(req.maxCount, null),
|
maxCount: toIntegerOrNull(req.maxCount, null),
|
||||||
|
|||||||
@@ -3921,7 +3921,7 @@ const applyMachineLinks = (source) => {
|
|||||||
const loadMachineData = async () => {
|
const loadMachineData = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const machineResult = await get(`/machines/${machineId}`)
|
const machineResult = await get(`/machines/${machineId}/skeleton`)
|
||||||
|
|
||||||
if (!machineResult.success) {
|
if (!machineResult.success) {
|
||||||
console.error('Machine non trouvée:', machineId, machineResult.error)
|
console.error('Machine non trouvée:', machineId, machineResult.error)
|
||||||
|
|||||||
@@ -163,8 +163,21 @@ const categories = computed(() => {
|
|||||||
return Array.from(cats)
|
return Array.from(cats)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Enrichir les machines avec les objets site et typeMachine complets
|
||||||
|
const enrichedMachines = computed(() => {
|
||||||
|
return machines.value.map((machine) => {
|
||||||
|
const site = sites.value.find(s => s.id === machine.siteId)
|
||||||
|
const typeMachine = machineTypes.value.find(t => t.id === machine.typeMachineId)
|
||||||
|
return {
|
||||||
|
...machine,
|
||||||
|
site: site || null,
|
||||||
|
typeMachine: typeMachine || null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
const filteredMachines = computed(() => {
|
const filteredMachines = computed(() => {
|
||||||
let filtered = machines.value
|
let filtered = enrichedMachines.value
|
||||||
|
|
||||||
if (selectedSite.value) {
|
if (selectedSite.value) {
|
||||||
filtered = filtered.filter(machine => machine.siteId === selectedSite.value)
|
filtered = filtered.filter(machine => machine.siteId === selectedSite.value)
|
||||||
|
|||||||
@@ -273,18 +273,19 @@
|
|||||||
</label>
|
</label>
|
||||||
<SearchSelect
|
<SearchSelect
|
||||||
:model-value="entry.pieceId || ''"
|
:model-value="entry.pieceId || ''"
|
||||||
:options="getPieceOptions(requirement, entry)"
|
:options="getPieceOptions(requirement, entry, entryIndex)"
|
||||||
:loading="piecesLoading"
|
:loading="piecesLoading || pieceLoadingByKey[getPieceKey(requirement, entryIndex)]"
|
||||||
size="sm"
|
size="sm"
|
||||||
placeholder="Rechercher une pièce…"
|
placeholder="Rechercher une pièce…"
|
||||||
empty-text="Aucune pièce disponible"
|
empty-text="Aucune pièce disponible"
|
||||||
:option-label="pieceOptionLabel"
|
:option-label="pieceOptionLabel"
|
||||||
:option-description="pieceOptionDescription"
|
:option-description="pieceOptionDescription"
|
||||||
|
@search="(term) => fetchPieceOptions(requirement, entryIndex, term)"
|
||||||
@update:modelValue="setPieceRequirementPiece(requirement, entryIndex, $event || '')"
|
@update:modelValue="setPieceRequirementPiece(requirement, entryIndex, $event || '')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p
|
<p
|
||||||
v-if="getPieceOptions(requirement, entry).length === 0"
|
v-if="getPieceOptions(requirement, entry, entryIndex).length === 0"
|
||||||
class="text-xs text-error"
|
class="text-xs text-error"
|
||||||
>
|
>
|
||||||
Aucune pièce disponible pour cette famille.
|
Aucune pièce disponible pour cette famille.
|
||||||
@@ -743,6 +744,7 @@ import { useMachineTypesApi } from '~/composables/useMachineTypesApi'
|
|||||||
import { useComposants } from '~/composables/useComposants'
|
import { useComposants } from '~/composables/useComposants'
|
||||||
import { usePieces } from '~/composables/usePieces'
|
import { usePieces } from '~/composables/usePieces'
|
||||||
import { useProducts } from '~/composables/useProducts'
|
import { useProducts } from '~/composables/useProducts'
|
||||||
|
import { useApi } from '~/composables/useApi'
|
||||||
import { useToast } from '~/composables/useToast'
|
import { useToast } from '~/composables/useToast'
|
||||||
import { sanitizeDefinitionOverrides } from '~/shared/modelUtils'
|
import { sanitizeDefinitionOverrides } from '~/shared/modelUtils'
|
||||||
import SearchSelect from '~/components/common/SearchSelect.vue'
|
import SearchSelect from '~/components/common/SearchSelect.vue'
|
||||||
@@ -754,12 +756,13 @@ import IconLucideAlertTriangle from '~icons/lucide/alert-triangle'
|
|||||||
import IconLucideCheckCircle2 from '~icons/lucide/check-circle-2'
|
import IconLucideCheckCircle2 from '~icons/lucide/check-circle-2'
|
||||||
import IconLucideCircle from '~icons/lucide/circle'
|
import IconLucideCircle from '~icons/lucide/circle'
|
||||||
|
|
||||||
const { createMachine, createMachineFromType } = useMachines()
|
const { createMachine, createMachineFromType, reconfigureSkeleton } = useMachines()
|
||||||
const { sites, loadSites } = useSites()
|
const { sites, loadSites } = useSites()
|
||||||
const { machineTypes, loadMachineTypes, loading: machineTypesLoading } = useMachineTypesApi()
|
const { machineTypes, loadMachineTypes, loading: machineTypesLoading } = useMachineTypesApi()
|
||||||
const { composants, loadComposants, loading: composantsLoading } = useComposants()
|
const { composants, loadComposants, loading: composantsLoading } = useComposants()
|
||||||
const { pieces, loadPieces, loading: piecesLoading } = usePieces()
|
const { pieces, loadPieces, loading: piecesLoading } = usePieces()
|
||||||
const { products, loadProducts, loading: productsLoading } = useProducts()
|
const { products, loadProducts, loading: productsLoading } = useProducts()
|
||||||
|
const { get } = useApi()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
const submitting = ref(false)
|
const submitting = ref(false)
|
||||||
@@ -842,6 +845,85 @@ const productById = computed(() => {
|
|||||||
return map
|
return map
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const pieceOptionsByKey = ref({})
|
||||||
|
const pieceLoadingByKey = ref({})
|
||||||
|
|
||||||
|
const extractCollection = (payload) => {
|
||||||
|
if (Array.isArray(payload)) {
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
if (Array.isArray(payload?.member)) {
|
||||||
|
return payload.member
|
||||||
|
}
|
||||||
|
if (Array.isArray(payload?.['hydra:member'])) {
|
||||||
|
return payload['hydra:member']
|
||||||
|
}
|
||||||
|
if (Array.isArray(payload?.data)) {
|
||||||
|
return payload.data
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPieceKey = (requirement, entryIndex) => `${requirement?.id || 'req'}:${entryIndex}`
|
||||||
|
|
||||||
|
const findPieceInCachedOptions = (id) => {
|
||||||
|
if (!id) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const buckets = Object.values(pieceOptionsByKey.value || {})
|
||||||
|
for (const bucket of buckets) {
|
||||||
|
if (!Array.isArray(bucket)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const found = bucket.find((piece) => piece?.id === id)
|
||||||
|
if (found) {
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const cachePieceIfMissing = (piece) => {
|
||||||
|
if (!piece?.id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (pieceById.value.has(piece.id)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const current = Array.isArray(pieces.value) ? pieces.value : []
|
||||||
|
pieces.value = [...current, piece]
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchPieceOptions = async (requirement, entryIndex, term = '') => {
|
||||||
|
const key = getPieceKey(requirement, entryIndex)
|
||||||
|
if (pieceLoadingByKey.value[key]) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const requirementTypeId = requirement?.typePieceId || requirement?.typePiece?.id || null
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.set('itemsPerPage', '50')
|
||||||
|
if (term && term.trim()) {
|
||||||
|
params.set('name', term.trim())
|
||||||
|
}
|
||||||
|
if (requirementTypeId) {
|
||||||
|
params.set('typePiece', `/api/model_types/${requirementTypeId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
pieceLoadingByKey.value = { ...pieceLoadingByKey.value, [key]: true }
|
||||||
|
try {
|
||||||
|
const result = await get(`/pieces?${params.toString()}`)
|
||||||
|
if (result.success) {
|
||||||
|
pieceOptionsByKey.value = {
|
||||||
|
...pieceOptionsByKey.value,
|
||||||
|
[key]: extractCollection(result.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
pieceLoadingByKey.value = { ...pieceLoadingByKey.value, [key]: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const isPlainObject = value => value !== null && typeof value === 'object' && !Array.isArray(value)
|
const isPlainObject = value => value !== null && typeof value === 'object' && !Array.isArray(value)
|
||||||
|
|
||||||
const toTrimmedString = (value) => {
|
const toTrimmedString = (value) => {
|
||||||
@@ -1077,7 +1159,12 @@ const getComponentOptions = (requirement, currentEntry) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPieceOptions = (requirement, currentEntry) => {
|
const getPieceOptions = (requirement, currentEntry, entryIndex) => {
|
||||||
|
const key = getPieceKey(requirement, entryIndex)
|
||||||
|
const cached = pieceOptionsByKey.value[key]
|
||||||
|
if (cached) {
|
||||||
|
return cached
|
||||||
|
}
|
||||||
const requirementTypeId = requirement?.typePieceId || requirement?.typePiece?.id || null
|
const requirementTypeId = requirement?.typePieceId || requirement?.typePiece?.id || null
|
||||||
const usedIds = new Set(
|
const usedIds = new Set(
|
||||||
selectedPieceIds.value.filter((id) => id && (!currentEntry || id !== currentEntry.pieceId)),
|
selectedPieceIds.value.filter((id) => id && (!currentEntry || id !== currentEntry.pieceId)),
|
||||||
@@ -1241,8 +1328,11 @@ const setPieceRequirementPiece = (requirement, index, pieceId) => {
|
|||||||
if (!entry) return
|
if (!entry) return
|
||||||
entry.pieceId = pieceId || null
|
entry.pieceId = pieceId || null
|
||||||
if (pieceId) {
|
if (pieceId) {
|
||||||
const piece = findPieceById(pieceId)
|
const piece = findPieceById(pieceId) || findPieceInCachedOptions(pieceId)
|
||||||
entry.typePieceId = piece?.typePieceId || requirement?.typePieceId || null
|
entry.typePieceId = piece?.typePieceId || requirement?.typePieceId || null
|
||||||
|
if (piece) {
|
||||||
|
cachePieceIfMissing(piece)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
entry.typePieceId = requirement?.typePieceId || null
|
entry.typePieceId = requirement?.typePieceId || null
|
||||||
}
|
}
|
||||||
@@ -1259,7 +1349,7 @@ const findPieceById = (id) => {
|
|||||||
if (!id) {
|
if (!id) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return pieceById.value.get(id) || null
|
return pieceById.value.get(id) || findPieceInCachedOptions(id) || null
|
||||||
}
|
}
|
||||||
|
|
||||||
const findProductById = (id) => {
|
const findProductById = (id) => {
|
||||||
@@ -1519,6 +1609,7 @@ const addPieceSelectionEntry = (requirement) => {
|
|||||||
...entries,
|
...entries,
|
||||||
createPieceSelectionEntry(requirement),
|
createPieceSelectionEntry(requirement),
|
||||||
]
|
]
|
||||||
|
fetchPieceOptions(requirement, entries.length).catch(() => {})
|
||||||
}
|
}
|
||||||
|
|
||||||
const removePieceSelectionEntry = (requirementId, index) => {
|
const removePieceSelectionEntry = (requirementId, index) => {
|
||||||
@@ -2096,6 +2187,9 @@ const initializeRequirementSelections = (type) => {
|
|||||||
const initialCount = Math.max(min, requirement.required ? 1 : 0)
|
const initialCount = Math.max(min, requirement.required ? 1 : 0)
|
||||||
if (initialCount > 0) {
|
if (initialCount > 0) {
|
||||||
pieceRequirementSelections[requirement.id] = Array.from({ length: initialCount }, () => createPieceSelectionEntry(requirement))
|
pieceRequirementSelections[requirement.id] = Array.from({ length: initialCount }, () => createPieceSelectionEntry(requirement))
|
||||||
|
pieceRequirementSelections[requirement.id].forEach((_, index) => {
|
||||||
|
fetchPieceOptions(requirement, index).catch(() => {})
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
pieceRequirementSelections[requirement.id] = []
|
pieceRequirementSelections[requirement.id] = []
|
||||||
}
|
}
|
||||||
@@ -2158,22 +2252,22 @@ const finalizeMachineCreation = async () => {
|
|||||||
productLinks = validationResult.productLinks
|
productLinks = validationResult.productLinks
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = {
|
|
||||||
...baseMachineData,
|
|
||||||
...(hasRequirements
|
|
||||||
? {
|
|
||||||
componentLinks,
|
|
||||||
pieceLinks,
|
|
||||||
productLinks
|
|
||||||
}
|
|
||||||
: {})
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = hasRequirements
|
const result = hasRequirements
|
||||||
? await createMachine(payload)
|
? await createMachine(baseMachineData)
|
||||||
: await createMachineFromType(baseMachineData, type)
|
: await createMachineFromType(baseMachineData, type)
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
|
if (hasRequirements && result.data?.id) {
|
||||||
|
const skeletonResult = await reconfigureSkeleton(result.data.id, {
|
||||||
|
componentLinks,
|
||||||
|
pieceLinks,
|
||||||
|
productLinks,
|
||||||
|
})
|
||||||
|
if (!skeletonResult.success) {
|
||||||
|
toast.showError(skeletonResult.error || 'Impossible d\'enregistrer les pièces/composants')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
newMachine.name = ''
|
newMachine.name = ''
|
||||||
newMachine.siteId = ''
|
newMachine.siteId = ''
|
||||||
newMachine.typeMachineId = ''
|
newMachine.typeMachineId = ''
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
type="text"
|
type="text"
|
||||||
class="input input-bordered input-sm w-full mt-1"
|
class="input input-bordered input-sm w-full mt-1"
|
||||||
placeholder="Nom ou référence…"
|
placeholder="Nom ou référence…"
|
||||||
|
@input="debouncedSearch"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
@@ -47,6 +48,7 @@
|
|||||||
id="piece-catalog-sort"
|
id="piece-catalog-sort"
|
||||||
v-model="sortField"
|
v-model="sortField"
|
||||||
class="select select-bordered select-sm"
|
class="select select-bordered select-sm"
|
||||||
|
@change="handleSortChange"
|
||||||
>
|
>
|
||||||
<option value="name">Nom</option>
|
<option value="name">Nom</option>
|
||||||
<option value="createdAt">Date de création</option>
|
<option value="createdAt">Date de création</option>
|
||||||
@@ -63,14 +65,33 @@
|
|||||||
id="piece-catalog-dir"
|
id="piece-catalog-dir"
|
||||||
v-model="sortDirection"
|
v-model="sortDirection"
|
||||||
class="select select-bordered select-sm"
|
class="select select-bordered select-sm"
|
||||||
|
@change="handleSortChange"
|
||||||
>
|
>
|
||||||
<option value="asc">Ascendant</option>
|
<option value="asc">Ascendant</option>
|
||||||
<option value="desc">Descendant</option>
|
<option value="desc">Descendant</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<label
|
||||||
|
class="text-xs font-semibold uppercase tracking-wide text-base-content/70"
|
||||||
|
for="piece-catalog-per-page"
|
||||||
|
>
|
||||||
|
Par page
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="piece-catalog-per-page"
|
||||||
|
v-model.number="itemsPerPage"
|
||||||
|
class="select select-bordered select-sm"
|
||||||
|
@change="handlePerPageChange"
|
||||||
|
>
|
||||||
|
<option :value="20">20</option>
|
||||||
|
<option :value="50">50</option>
|
||||||
|
<option :value="100">100</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-base-content/50 lg:text-right">
|
<p class="text-xs text-base-content/50 lg:text-right">
|
||||||
{{ visiblePieces.length }} / {{ piecesTotal }} résultat{{ visiblePieces.length > 1 ? 's' : '' }}
|
{{ piecesOnPage }} / {{ piecesTotal }} résultat{{ piecesTotal > 1 ? 's' : '' }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -82,77 +103,85 @@
|
|||||||
Aucune pièce n'a encore été créée.
|
Aucune pièce n'a encore été créée.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p v-else-if="!visiblePieces.length" class="text-sm text-base-content/70">
|
<p v-else-if="!piecesList.length" class="text-sm text-base-content/70">
|
||||||
Aucune pièce ne correspond à votre recherche.
|
Aucune pièce ne correspond à votre recherche.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div v-else class="overflow-x-auto">
|
<template v-else>
|
||||||
<table class="table table-sm md:table-md">
|
<div class="overflow-x-auto">
|
||||||
<thead>
|
<table class="table table-sm md:table-md">
|
||||||
<tr>
|
<thead>
|
||||||
<th class="w-24">Aperçu</th>
|
<tr>
|
||||||
<th>Nom</th>
|
<th class="w-24">Aperçu</th>
|
||||||
<th>Référence</th>
|
<th>Nom</th>
|
||||||
<th>Fournisseurs</th>
|
<th>Référence</th>
|
||||||
<th>Type de pièce</th>
|
<th>Fournisseurs</th>
|
||||||
<th>Actions</th>
|
<th>Type de pièce</th>
|
||||||
</tr>
|
<th>Actions</th>
|
||||||
</thead>
|
</tr>
|
||||||
<tbody>
|
</thead>
|
||||||
<tr v-for="row in pieceRows" :key="row.piece.id">
|
<tbody>
|
||||||
<td class="align-middle">
|
<tr v-for="row in pieceRows" :key="row.piece.id">
|
||||||
<DocumentThumbnail
|
<td class="align-middle">
|
||||||
:document="resolvePrimaryDocument(row.piece)"
|
<DocumentThumbnail
|
||||||
:alt="resolvePreviewAlt(row.piece)"
|
:document="resolvePrimaryDocument(row.piece)"
|
||||||
/>
|
:alt="resolvePreviewAlt(row.piece)"
|
||||||
</td>
|
/>
|
||||||
<td>{{ row.piece.name || 'Pièce sans nom' }}</td>
|
</td>
|
||||||
<td>{{ row.piece.reference || '—' }}</td>
|
<td>{{ row.piece.name || 'Pièce sans nom' }}</td>
|
||||||
<td>
|
<td>{{ row.piece.reference || '—' }}</td>
|
||||||
<div
|
<td>
|
||||||
v-if="row.suppliers.visible.length"
|
<div
|
||||||
class="flex max-w-[14rem] flex-wrap items-center gap-1"
|
v-if="row.suppliers.visible.length"
|
||||||
:title="row.suppliers.tooltip"
|
class="flex max-w-[14rem] flex-wrap items-center gap-1"
|
||||||
>
|
:title="row.suppliers.tooltip"
|
||||||
<span
|
|
||||||
v-for="supplier in row.suppliers.visible"
|
|
||||||
:key="supplier"
|
|
||||||
class="badge badge-ghost badge-sm whitespace-nowrap"
|
|
||||||
>
|
>
|
||||||
{{ supplier }}
|
<span
|
||||||
</span>
|
v-for="supplier in row.suppliers.visible"
|
||||||
<span
|
:key="supplier"
|
||||||
v-if="row.suppliers.overflow"
|
class="badge badge-ghost badge-sm whitespace-nowrap"
|
||||||
class="badge badge-outline badge-sm"
|
>
|
||||||
>
|
{{ supplier }}
|
||||||
+{{ row.suppliers.overflow }}
|
</span>
|
||||||
</span>
|
<span
|
||||||
</div>
|
v-if="row.suppliers.overflow"
|
||||||
<span v-else>—</span>
|
class="badge badge-outline badge-sm"
|
||||||
</td>
|
>
|
||||||
<td>{{ resolvePieceType(row.piece) }}</td>
|
+{{ row.suppliers.overflow }}
|
||||||
<td>
|
</span>
|
||||||
<div class="flex items-center gap-2">
|
</div>
|
||||||
<NuxtLink
|
<span v-else>—</span>
|
||||||
:to="`/pieces/${row.piece.id}/edit`"
|
</td>
|
||||||
class="btn btn-ghost btn-xs"
|
<td>{{ resolvePieceType(row.piece) }}</td>
|
||||||
>
|
<td>
|
||||||
Modifier
|
<div class="flex items-center gap-2">
|
||||||
</NuxtLink>
|
<NuxtLink
|
||||||
<button
|
:to="`/pieces/${row.piece.id}/edit`"
|
||||||
type="button"
|
class="btn btn-ghost btn-xs"
|
||||||
class="btn btn-error btn-xs"
|
>
|
||||||
:disabled="loadingPieces"
|
Modifier
|
||||||
@click="handleDeletePiece(row.piece)"
|
</NuxtLink>
|
||||||
>
|
<button
|
||||||
Supprimer
|
type="button"
|
||||||
</button>
|
class="btn btn-error btn-xs"
|
||||||
</div>
|
:disabled="loadingPieces"
|
||||||
</td>
|
@click="handleDeletePiece(row.piece)"
|
||||||
</tr>
|
>
|
||||||
</tbody>
|
Supprimer
|
||||||
</table>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Pagination
|
||||||
|
:current-page="currentPage"
|
||||||
|
:total-pages="totalPages"
|
||||||
|
@update:current-page="handlePageChange"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
@@ -160,21 +189,82 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
import { usePieces } from '~/composables/usePieces'
|
import { usePieces } from '~/composables/usePieces'
|
||||||
|
import { usePieceTypes } from '~/composables/usePieceTypes'
|
||||||
import { useToast } from '~/composables/useToast'
|
import { useToast } from '~/composables/useToast'
|
||||||
|
import { usePersistedSort } from '~/composables/usePersistedSort'
|
||||||
import DocumentThumbnail from '~/components/DocumentThumbnail.vue'
|
import DocumentThumbnail from '~/components/DocumentThumbnail.vue'
|
||||||
|
import Pagination from '~/components/common/Pagination.vue'
|
||||||
import { isImageDocument, isPdfDocument } from '~/utils/documentPreview'
|
import { isImageDocument, isPdfDocument } from '~/utils/documentPreview'
|
||||||
|
|
||||||
const { showError } = useToast()
|
const { showError } = useToast()
|
||||||
const { pieces, loadPieces, loading: loadingPiecesRef, deletePiece } = usePieces()
|
const { pieces, total, loadPieces, loading: loadingPiecesRef, deletePiece } = usePieces()
|
||||||
|
const { pieceTypes, loadPieceTypes } = usePieceTypes()
|
||||||
const loadingPieces = computed(() => loadingPiecesRef.value)
|
const loadingPieces = computed(() => loadingPiecesRef.value)
|
||||||
const piecesList = computed(() => pieces.value || [])
|
|
||||||
const piecesTotal = computed(() => piecesList.value.length)
|
|
||||||
|
|
||||||
|
// Pagination state
|
||||||
|
const currentPage = ref(1)
|
||||||
|
const itemsPerPage = ref(30)
|
||||||
|
const piecesTotal = computed(() => total.value)
|
||||||
|
const piecesOnPage = computed(() => pieces.value.length)
|
||||||
|
const totalPages = computed(() => Math.ceil(piecesTotal.value / itemsPerPage.value) || 1)
|
||||||
|
|
||||||
|
// Search state with debounce
|
||||||
const searchTerm = ref('')
|
const searchTerm = ref('')
|
||||||
const sortField = ref<'name' | 'createdAt'>('name')
|
let searchTimeout: ReturnType<typeof setTimeout> | null = null
|
||||||
const sortDirection = ref<'asc' | 'desc'>('asc')
|
|
||||||
|
const debouncedSearch = () => {
|
||||||
|
if (searchTimeout) {
|
||||||
|
clearTimeout(searchTimeout)
|
||||||
|
}
|
||||||
|
searchTimeout = setTimeout(() => {
|
||||||
|
currentPage.value = 1
|
||||||
|
fetchPieces()
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort state
|
||||||
|
const { sortField, sortDirection } = usePersistedSort<'name' | 'createdAt', 'asc' | 'desc'>(
|
||||||
|
'pieces-catalog',
|
||||||
|
{ field: 'name', direction: 'asc' },
|
||||||
|
)
|
||||||
|
|
||||||
|
// Enrichir les pièces avec les types de pièces complets
|
||||||
|
const piecesList = computed(() => {
|
||||||
|
return (pieces.value || []).map((piece) => {
|
||||||
|
const typePiece = pieceTypes.value.find(t => t.id === piece.typePieceId)
|
||||||
|
return {
|
||||||
|
...piece,
|
||||||
|
typePiece: typePiece || piece.typePiece || null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const fetchPieces = async () => {
|
||||||
|
await loadPieces({
|
||||||
|
search: searchTerm.value,
|
||||||
|
page: currentPage.value,
|
||||||
|
itemsPerPage: itemsPerPage.value,
|
||||||
|
orderBy: sortField.value,
|
||||||
|
orderDir: sortDirection.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePageChange = (page: number) => {
|
||||||
|
currentPage.value = page
|
||||||
|
fetchPieces()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSortChange = () => {
|
||||||
|
currentPage.value = 1
|
||||||
|
fetchPieces()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePerPageChange = () => {
|
||||||
|
currentPage.value = 1
|
||||||
|
fetchPieces()
|
||||||
|
}
|
||||||
|
|
||||||
const resolvePrimaryDocument = (piece: Record<string, any>) => {
|
const resolvePrimaryDocument = (piece: Record<string, any>) => {
|
||||||
const documents = Array.isArray(piece?.documents) ? piece.documents : []
|
const documents = Array.isArray(piece?.documents) ? piece.documents : []
|
||||||
@@ -322,60 +412,8 @@ const resolveDeleteGuard = (piece: Record<string, any>) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolveComparableName = (piece: Record<string, any>) => {
|
|
||||||
const normalise = (value?: string | null) =>
|
|
||||||
(value ?? '').toString().trim().toLowerCase()
|
|
||||||
|
|
||||||
return (
|
|
||||||
normalise(piece?.name) ||
|
|
||||||
normalise(piece?.reference) ||
|
|
||||||
normalise(piece?.id)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const resolveComparableDate = (piece: Record<string, any>) => {
|
|
||||||
const raw = piece?.createdAt ?? piece?.created_at ?? null
|
|
||||||
if (!raw) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
const timestamp = new Date(raw).getTime()
|
|
||||||
return Number.isNaN(timestamp) ? 0 : timestamp
|
|
||||||
}
|
|
||||||
|
|
||||||
const visiblePieces = computed(() => {
|
|
||||||
const term = searchTerm.value.trim().toLowerCase()
|
|
||||||
const source = piecesList.value || []
|
|
||||||
|
|
||||||
const filtered = term
|
|
||||||
? source.filter((piece) => {
|
|
||||||
const name = (piece?.name || '').toLowerCase()
|
|
||||||
const reference = (piece?.reference || '').toLowerCase()
|
|
||||||
return (
|
|
||||||
name.includes(term) ||
|
|
||||||
reference.includes(term)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
: [...source]
|
|
||||||
|
|
||||||
const direction = sortDirection.value === 'asc' ? 1 : -1
|
|
||||||
|
|
||||||
return filtered.sort((a, b) => {
|
|
||||||
if (sortField.value === 'name') {
|
|
||||||
return (
|
|
||||||
resolveComparableName(a).localeCompare(
|
|
||||||
resolveComparableName(b),
|
|
||||||
'fr',
|
|
||||||
{ sensitivity: 'base' }
|
|
||||||
) * direction
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (resolveComparableDate(a) - resolveComparableDate(b)) * direction
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const pieceRows = computed(() =>
|
const pieceRows = computed(() =>
|
||||||
visiblePieces.value.map((piece) => ({
|
piecesList.value.map((piece) => ({
|
||||||
piece,
|
piece,
|
||||||
suppliers: buildPieceSuppliersDisplay(piece),
|
suppliers: buildPieceSuppliersDisplay(piece),
|
||||||
})),
|
})),
|
||||||
@@ -410,9 +448,14 @@ const handleDeletePiece = async (piece: Record<string, any>) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await deletePiece(piece.id)
|
await deletePiece(piece.id)
|
||||||
|
// Reload current page after deletion
|
||||||
|
fetchPieces()
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadPieces()
|
await Promise.all([
|
||||||
|
fetchPieces(),
|
||||||
|
loadPieceTypes()
|
||||||
|
])
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -146,34 +146,48 @@
|
|||||||
<span>{{ description }}</span>
|
<span>{{ description }}</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<ProductSelect
|
<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||||
v-model="editionForm.productId"
|
<div
|
||||||
:disabled="saving"
|
v-for="entry in productRequirementEntries"
|
||||||
:type-product-id="primaryProductRequirement?.typeProductId || null"
|
:key="entry.key"
|
||||||
helper-text="Un produit valide est requis pour cette pièce."
|
class="form-control"
|
||||||
/>
|
>
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text text-xs font-medium">
|
||||||
|
{{ entry.label }}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<ProductSelect
|
||||||
|
:model-value="productSelections[entry.index] || null"
|
||||||
|
:disabled="saving"
|
||||||
|
:type-product-id="entry.typeProductId"
|
||||||
|
helper-text="Un produit valide est requis pour cette pièce."
|
||||||
|
@update:model-value="(value) => setProductSelection(entry.index, value)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="selectedType" class="space-y-3 rounded-lg border border-base-200 bg-base-200/40 p-4">
|
<div v-if="selectedType || resolvedStructure" class="space-y-3 rounded-lg border border-base-200 bg-base-200/40 p-4">
|
||||||
<div class="flex items-center justify-between gap-4">
|
<div class="flex items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="font-semibold text-base-content">Squelette sélectionné</h2>
|
<h2 class="font-semibold text-base-content">Squelette sélectionné</h2>
|
||||||
<p class="text-xs text-base-content/70">
|
<p class="text-xs text-base-content/70">
|
||||||
{{ selectedType.description || 'Ce squelette définit la structure et les champs personnalisés de la pièce.' }}
|
{{ selectedType?.description || 'Ce squelette définit la structure et les champs personnalisés de la pièce.' }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<span class="badge badge-outline">{{ formatPieceStructurePreview(selectedType.structure) }}</span>
|
<span class="badge badge-outline">{{ formatPieceStructurePreview(resolvedStructure) }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<details v-if="selectedType.structure" class="collapse collapse-arrow bg-base-100">
|
<details v-if="resolvedStructure" class="collapse collapse-arrow bg-base-100">
|
||||||
<summary class="collapse-title text-sm font-medium">
|
<summary class="collapse-title text-sm font-medium">
|
||||||
Consulter le détail du squelette
|
Consulter le détail du squelette
|
||||||
</summary>
|
</summary>
|
||||||
<div class="collapse-content space-y-2 text-sm text-base-content/80">
|
<div class="collapse-content space-y-2 text-sm text-base-content/80">
|
||||||
<div v-if="getStructureCustomFields(selectedType.structure).length" class="space-y-1">
|
<div v-if="getStructureCustomFields(resolvedStructure).length" class="space-y-1">
|
||||||
<h3 class="font-semibold text-sm text-base-content">Champs personnalisés</h3>
|
<h3 class="font-semibold text-sm text-base-content">Champs personnalisés</h3>
|
||||||
<ul class="list-disc list-inside space-y-1">
|
<ul class="list-disc list-inside space-y-1">
|
||||||
<li v-for="field in getStructureCustomFields(selectedType.structure)" :key="field.name">
|
<li v-for="field in getStructureCustomFields(resolvedStructure)" :key="field.name">
|
||||||
<span class="font-medium">{{ field.name }}</span>
|
<span class="font-medium">{{ field.name }}</span>
|
||||||
<span v-if="field.value !== undefined && field.value !== null"> : {{ field.value }}</span>
|
<span v-if="field.value !== undefined && field.value !== null"> : {{ field.value }}</span>
|
||||||
</li>
|
</li>
|
||||||
@@ -395,12 +409,14 @@ import { useApi } from '~/composables/useApi'
|
|||||||
import { useToast } from '~/composables/useToast'
|
import { useToast } from '~/composables/useToast'
|
||||||
import { useDocuments } from '~/composables/useDocuments'
|
import { useDocuments } from '~/composables/useDocuments'
|
||||||
import { useConstructeurs } from '~/composables/useConstructeurs'
|
import { useConstructeurs } from '~/composables/useConstructeurs'
|
||||||
|
import { extractRelationId } from '~/shared/apiRelations'
|
||||||
import { getFileIcon } from '~/utils/fileIcons'
|
import { getFileIcon } from '~/utils/fileIcons'
|
||||||
import { canPreviewDocument, isImageDocument, isPdfDocument } from '~/utils/documentPreview'
|
import { canPreviewDocument, isImageDocument, isPdfDocument } from '~/utils/documentPreview'
|
||||||
import { formatPieceStructurePreview } from '~/shared/modelUtils'
|
import { formatPieceStructurePreview } from '~/shared/modelUtils'
|
||||||
import { uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
import { uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
||||||
import type { PieceModelProduct, PieceModelStructure } from '~/shared/types/inventory'
|
import type { PieceModelProduct, PieceModelStructure } from '~/shared/types/inventory'
|
||||||
import type { ModelType } from '~/services/modelTypes'
|
import type { ModelType } from '~/services/modelTypes'
|
||||||
|
import { getModelType } from '~/services/modelTypes'
|
||||||
|
|
||||||
interface PieceCatalogType extends ModelType {
|
interface PieceCatalogType extends ModelType {
|
||||||
structure: PieceModelStructure | null
|
structure: PieceModelStructure | null
|
||||||
@@ -424,7 +440,7 @@ const router = useRouter()
|
|||||||
const { get } = useApi()
|
const { get } = useApi()
|
||||||
const { pieceTypes, loadPieceTypes } = usePieceTypes()
|
const { pieceTypes, loadPieceTypes } = usePieceTypes()
|
||||||
const { updatePiece } = usePieces()
|
const { updatePiece } = usePieces()
|
||||||
const { upsertCustomFieldValue, updateCustomFieldValue } = useCustomFields()
|
const { upsertCustomFieldValue, updateCustomFieldValue, getCustomFieldValuesByEntity } = useCustomFields()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const { loadDocumentsByPiece, uploadDocuments, deleteDocument } = useDocuments()
|
const { loadDocumentsByPiece, uploadDocuments, deleteDocument } = useDocuments()
|
||||||
const { ensureConstructeurs } = useConstructeurs()
|
const { ensureConstructeurs } = useConstructeurs()
|
||||||
@@ -440,17 +456,30 @@ const previewDocument = ref<any | null>(null)
|
|||||||
const previewVisible = ref(false)
|
const previewVisible = ref(false)
|
||||||
|
|
||||||
const selectedTypeId = ref<string>('')
|
const selectedTypeId = ref<string>('')
|
||||||
|
const pieceTypeDetails = ref<any | null>(null)
|
||||||
const editionForm = reactive({
|
const editionForm = reactive({
|
||||||
name: '' as string,
|
name: '' as string,
|
||||||
reference: '' as string,
|
reference: '' as string,
|
||||||
constructeurIds: [] as string[],
|
constructeurIds: [] as string[],
|
||||||
prix: '' as string,
|
prix: '' as string,
|
||||||
productId: null as string | null,
|
|
||||||
})
|
})
|
||||||
|
const productSelections = ref<(string | null)[]>([])
|
||||||
|
|
||||||
const customFieldInputs = ref<CustomFieldInput[]>([])
|
const customFieldInputs = ref<CustomFieldInput[]>([])
|
||||||
const documentIcon = (doc: any) =>
|
const documentIcon = (doc: any) =>
|
||||||
getFileIcon({ name: doc?.filename || doc?.name, mime: doc?.mimeType })
|
getFileIcon({ name: doc?.filename || doc?.name, mime: doc?.mimeType })
|
||||||
|
const resolvedStructure = computed<PieceModelStructure | null>(() =>
|
||||||
|
pieceTypeDetails.value?.structure ?? selectedType.value?.structure ?? null,
|
||||||
|
)
|
||||||
|
|
||||||
|
const refreshCustomFieldInputs = (
|
||||||
|
structureOverride?: PieceModelStructure | null,
|
||||||
|
valuesOverride?: any[] | null,
|
||||||
|
) => {
|
||||||
|
const structure = structureOverride ?? resolvedStructure.value ?? null
|
||||||
|
const values = valuesOverride ?? piece.value?.customFieldValues ?? null
|
||||||
|
customFieldInputs.value = buildCustomFieldInputs(structure, values)
|
||||||
|
}
|
||||||
const formatSize = (size: number | null | undefined) => {
|
const formatSize = (size: number | null | undefined) => {
|
||||||
if (size === null || size === undefined) {
|
if (size === null || size === undefined) {
|
||||||
return '—'
|
return '—'
|
||||||
@@ -577,14 +606,18 @@ const selectedType = computed(() => {
|
|||||||
return pieceTypeList.value.find((type) => type.id === selectedTypeId.value) ?? null
|
return pieceTypeList.value.find((type) => type.id === selectedTypeId.value) ?? null
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const getStructureProducts = (structure: PieceModelStructure | null) =>
|
||||||
|
Array.isArray(structure?.products) ? structure.products : []
|
||||||
|
|
||||||
|
const getStructureCustomFields = (structure: PieceModelStructure | null) =>
|
||||||
|
Array.isArray(structure?.customFields) ? structure.customFields : []
|
||||||
|
|
||||||
const structureProducts = computed(() =>
|
const structureProducts = computed(() =>
|
||||||
getStructureProducts(selectedType.value?.structure ?? null),
|
getStructureProducts(resolvedStructure.value),
|
||||||
)
|
)
|
||||||
|
|
||||||
const requiresProductSelection = computed(() => structureProducts.value.length > 0)
|
const requiresProductSelection = computed(() => structureProducts.value.length > 0)
|
||||||
|
|
||||||
const primaryProductRequirement = computed(() => structureProducts.value[0] ?? null)
|
|
||||||
|
|
||||||
const describeProductRequirement = (requirement: PieceModelProduct, index: number) => {
|
const describeProductRequirement = (requirement: PieceModelProduct, index: number) => {
|
||||||
if (!requirement) {
|
if (!requirement) {
|
||||||
return `Produit ${index + 1}`
|
return `Produit ${index + 1}`
|
||||||
@@ -613,6 +646,50 @@ const productRequirementDescriptions = computed(() =>
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const ensureProductSelections = (count: number) => {
|
||||||
|
const next = Array.from({ length: count }, (_, index) => productSelections.value[index] ?? null)
|
||||||
|
productSelections.value = next
|
||||||
|
}
|
||||||
|
|
||||||
|
let pendingProductIds: string[] = []
|
||||||
|
|
||||||
|
const productRequirementEntries = computed(() =>
|
||||||
|
structureProducts.value.map((requirement, index) => ({
|
||||||
|
index,
|
||||||
|
key: `piece-product-requirement-${index}-${requirement?.typeProductId || 'any'}`,
|
||||||
|
label: describeProductRequirement(requirement, index),
|
||||||
|
typeProductId: requirement?.typeProductId ? String(requirement.typeProductId) : null,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
|
||||||
|
const productSelectionsFilled = computed(() =>
|
||||||
|
!requiresProductSelection.value ||
|
||||||
|
productRequirementEntries.value.every((entry) => {
|
||||||
|
const value = productSelections.value[entry.index]
|
||||||
|
return typeof value === 'string' && value.trim().length > 0
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const setProductSelection = (index: number, value: string | null) => {
|
||||||
|
const normalized = typeof value === 'string' ? value : null
|
||||||
|
const next = [...productSelections.value]
|
||||||
|
next[index] = normalized
|
||||||
|
productSelections.value = next
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(structureProducts, (products) => {
|
||||||
|
ensureProductSelections(products.length)
|
||||||
|
if (!pendingProductIds.length || products.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const next = Array.from(
|
||||||
|
{ length: products.length },
|
||||||
|
(_, index) => pendingProductIds[index] ?? null,
|
||||||
|
)
|
||||||
|
productSelections.value = next
|
||||||
|
pendingProductIds = []
|
||||||
|
})
|
||||||
|
|
||||||
const requiredCustomFieldsFilled = computed(() =>
|
const requiredCustomFieldsFilled = computed(() =>
|
||||||
customFieldInputs.value.every((field) => {
|
customFieldInputs.value.every((field) => {
|
||||||
if (!field.required) {
|
if (!field.required) {
|
||||||
@@ -630,7 +707,7 @@ const canSubmit = computed(() =>
|
|||||||
piece.value &&
|
piece.value &&
|
||||||
editionForm.name &&
|
editionForm.name &&
|
||||||
requiredCustomFieldsFilled.value &&
|
requiredCustomFieldsFilled.value &&
|
||||||
(!requiresProductSelection.value || editionForm.productId) &&
|
productSelectionsFilled.value &&
|
||||||
!saving.value,
|
!saving.value,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -659,12 +736,37 @@ const fetchPiece = async () => {
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
piece.value = result.data
|
piece.value = result.data
|
||||||
pieceDocuments.value = Array.isArray(result.data?.documents) ? result.data.documents : []
|
pieceDocuments.value = Array.isArray(result.data?.documents) ? result.data.documents : []
|
||||||
|
const customValues = await getCustomFieldValuesByEntity('piece', result.data.id)
|
||||||
|
if (customValues.success && Array.isArray(customValues.data)) {
|
||||||
|
piece.value.customFieldValues = customValues.data
|
||||||
|
refreshCustomFieldInputs(undefined, customValues.data)
|
||||||
|
}
|
||||||
|
await loadPieceTypeDetails(result.data)
|
||||||
} else {
|
} else {
|
||||||
piece.value = null
|
piece.value = null
|
||||||
pieceDocuments.value = []
|
pieceDocuments.value = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loadPieceTypeDetails = async (currentPiece: any) => {
|
||||||
|
const typeId = currentPiece?.typePieceId
|
||||||
|
|| extractRelationId(currentPiece?.typePiece)
|
||||||
|
|| ''
|
||||||
|
if (!typeId) {
|
||||||
|
pieceTypeDetails.value = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const type = await getModelType(typeId)
|
||||||
|
if (type && typeof type === 'object') {
|
||||||
|
pieceTypeDetails.value = type
|
||||||
|
refreshCustomFieldInputs(type.structure ?? null, currentPiece?.customFieldValues ?? null)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
pieceTypeDetails.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let initialized = false
|
let initialized = false
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@@ -674,7 +776,13 @@ watch(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedTypeId.value = currentPiece.typePieceId || ''
|
const resolvedTypeId = currentPiece.typePieceId
|
||||||
|
|| extractRelationId(currentPiece.typePiece)
|
||||||
|
|| ''
|
||||||
|
if (resolvedTypeId && !currentPiece.typePieceId) {
|
||||||
|
currentPiece.typePieceId = resolvedTypeId
|
||||||
|
}
|
||||||
|
selectedTypeId.value = resolvedTypeId
|
||||||
|
|
||||||
editionForm.name = currentPiece.name || ''
|
editionForm.name = currentPiece.name || ''
|
||||||
editionForm.reference = currentPiece.reference || ''
|
editionForm.reference = currentPiece.reference || ''
|
||||||
@@ -684,15 +792,27 @@ watch(
|
|||||||
currentPiece.constructeur ? [currentPiece.constructeur] : [],
|
currentPiece.constructeur ? [currentPiece.constructeur] : [],
|
||||||
)
|
)
|
||||||
editionForm.prix = currentPiece.prix !== null && currentPiece.prix !== undefined ? String(currentPiece.prix) : ''
|
editionForm.prix = currentPiece.prix !== null && currentPiece.prix !== undefined ? String(currentPiece.prix) : ''
|
||||||
editionForm.productId = currentPiece.product?.id || currentPiece.productId || null
|
|
||||||
if (editionForm.constructeurIds.length) {
|
if (editionForm.constructeurIds.length) {
|
||||||
void ensureConstructeurs(editionForm.constructeurIds)
|
void ensureConstructeurs(editionForm.constructeurIds)
|
||||||
}
|
}
|
||||||
|
|
||||||
customFieldInputs.value = buildCustomFieldInputs(
|
const existingProductIds = Array.isArray(currentPiece.productIds) && currentPiece.productIds.length
|
||||||
currentType?.structure ?? null,
|
? currentPiece.productIds.map((id: unknown) => String(id))
|
||||||
currentPiece.customFieldValues,
|
: currentPiece.product?.id || currentPiece.productId
|
||||||
)
|
? [String(currentPiece.product?.id || currentPiece.productId)]
|
||||||
|
: []
|
||||||
|
pendingProductIds = existingProductIds
|
||||||
|
ensureProductSelections(structureProducts.value.length)
|
||||||
|
if (existingProductIds.length && structureProducts.value.length) {
|
||||||
|
const next = Array.from(
|
||||||
|
{ length: structureProducts.value.length },
|
||||||
|
(_, index) => existingProductIds[index] ?? null,
|
||||||
|
)
|
||||||
|
productSelections.value = next
|
||||||
|
pendingProductIds = []
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshCustomFieldInputs(currentType?.structure ?? null, currentPiece.customFieldValues)
|
||||||
|
|
||||||
initialized = true
|
initialized = true
|
||||||
},
|
},
|
||||||
@@ -703,10 +823,17 @@ watch(selectedType, (currentType) => {
|
|||||||
if (!piece.value || !currentType) {
|
if (!piece.value || !currentType) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
customFieldInputs.value = buildCustomFieldInputs(
|
if (!pieceTypeDetails.value) {
|
||||||
currentType.structure,
|
refreshCustomFieldInputs(currentType.structure, piece.value.customFieldValues)
|
||||||
piece.value.customFieldValues,
|
}
|
||||||
)
|
})
|
||||||
|
|
||||||
|
watch(resolvedStructure, (currentStructure) => {
|
||||||
|
if (!piece.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ensureProductSelections(structureProducts.value.length)
|
||||||
|
refreshCustomFieldInputs(currentStructure, piece.value.customFieldValues)
|
||||||
})
|
})
|
||||||
|
|
||||||
const submitEdition = async () => {
|
const submitEdition = async () => {
|
||||||
@@ -714,7 +841,7 @@ const submitEdition = async () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (requiresProductSelection.value && !editionForm.productId) {
|
if (!productSelectionsFilled.value) {
|
||||||
toast.showError('Sélectionnez un produit conforme au squelette.')
|
toast.showError('Sélectionnez un produit conforme au squelette.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -735,16 +862,18 @@ const submitEdition = async () => {
|
|||||||
const reference = editionForm.reference.trim()
|
const reference = editionForm.reference.trim()
|
||||||
payload.reference = reference ? reference : null
|
payload.reference = reference ? reference : null
|
||||||
|
|
||||||
const selectedProductId =
|
const normalizedProductIds = productRequirementEntries.value
|
||||||
typeof editionForm.productId === 'string'
|
.map((entry) => productSelections.value[entry.index])
|
||||||
? editionForm.productId.trim()
|
.filter((value): value is string => typeof value === 'string' && value.trim().length > 0)
|
||||||
: ''
|
.map((value) => value.trim())
|
||||||
payload.productId = selectedProductId || null
|
|
||||||
|
payload.productIds = normalizedProductIds
|
||||||
|
payload.productId = normalizedProductIds[0] || null
|
||||||
|
|
||||||
if (rawPrice) {
|
if (rawPrice) {
|
||||||
const parsed = Number(rawPrice)
|
const parsed = Number(rawPrice)
|
||||||
if (!Number.isNaN(parsed)) {
|
if (!Number.isNaN(parsed)) {
|
||||||
payload.prix = parsed
|
payload.prix = String(parsed)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
payload.prix = null
|
payload.prix = null
|
||||||
@@ -932,12 +1061,6 @@ const formatDefaultValue = (type: string, defaultValue: any): string => {
|
|||||||
return String(defaultValue)
|
return String(defaultValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getStructureProducts = (structure: PieceModelStructure | null) =>
|
|
||||||
Array.isArray(structure?.products) ? structure.products : []
|
|
||||||
|
|
||||||
const getStructureCustomFields = (structure: PieceModelStructure | null) =>
|
|
||||||
Array.isArray(structure?.customFields) ? structure.customFields : []
|
|
||||||
|
|
||||||
const buildCustomFieldMetadata = (field: CustomFieldInput) => ({
|
const buildCustomFieldMetadata = (field: CustomFieldInput) => ({
|
||||||
customFieldName: field.name,
|
customFieldName: field.name,
|
||||||
customFieldType: field.type,
|
customFieldType: field.type,
|
||||||
|
|||||||
@@ -118,12 +118,26 @@
|
|||||||
<span>{{ description }}</span>
|
<span>{{ description }}</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<ProductSelect
|
<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||||
v-model="creationForm.productId"
|
<div
|
||||||
:disabled="submitting || !selectedType"
|
v-for="entry in productRequirementEntries"
|
||||||
:type-product-id="primaryProductRequirement?.typeProductId || null"
|
:key="entry.key"
|
||||||
helper-text="Un produit est requis pour cette pièce."
|
class="form-control"
|
||||||
/>
|
>
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text text-xs font-medium">
|
||||||
|
{{ entry.label }}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<ProductSelect
|
||||||
|
:model-value="productSelections[entry.index] || null"
|
||||||
|
:disabled="submitting || !selectedType"
|
||||||
|
:type-product-id="entry.typeProductId"
|
||||||
|
helper-text="Un produit est requis pour cette pièce."
|
||||||
|
@update:model-value="(value) => setProductSelection(entry.index, value)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="selectedType" class="space-y-3 rounded-lg border border-base-200 bg-base-200/40 p-4">
|
<div v-if="selectedType" class="space-y-3 rounded-lg border border-base-200 bg-base-200/40 p-4">
|
||||||
@@ -317,8 +331,8 @@ const creationForm = reactive({
|
|||||||
reference: '' as string,
|
reference: '' as string,
|
||||||
constructeurIds: [] as string[],
|
constructeurIds: [] as string[],
|
||||||
prix: '' as string,
|
prix: '' as string,
|
||||||
productId: null as string | null,
|
|
||||||
})
|
})
|
||||||
|
const productSelections = ref<(string | null)[]>([])
|
||||||
|
|
||||||
const lastSuggestedName = ref('')
|
const lastSuggestedName = ref('')
|
||||||
const customFieldInputs = ref<CustomFieldInput[]>([])
|
const customFieldInputs = ref<CustomFieldInput[]>([])
|
||||||
@@ -364,14 +378,18 @@ const selectedType = computed(() => {
|
|||||||
return pieceTypeList.value.find((type) => type.id === selectedTypeId.value) ?? null
|
return pieceTypeList.value.find((type) => type.id === selectedTypeId.value) ?? null
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const getStructureCustomFields = (structure: PieceModelStructure | null) =>
|
||||||
|
Array.isArray(structure?.customFields) ? structure.customFields : []
|
||||||
|
|
||||||
|
const getStructureProducts = (structure: PieceModelStructure | null) =>
|
||||||
|
Array.isArray(structure?.products) ? structure.products : []
|
||||||
|
|
||||||
const structureProducts = computed(() =>
|
const structureProducts = computed(() =>
|
||||||
getStructureProducts(selectedType.value?.structure ?? null),
|
getStructureProducts(selectedType.value?.structure ?? null),
|
||||||
)
|
)
|
||||||
|
|
||||||
const requiresProductSelection = computed(() => structureProducts.value.length > 0)
|
const requiresProductSelection = computed(() => structureProducts.value.length > 0)
|
||||||
|
|
||||||
const primaryProductRequirement = computed(() => structureProducts.value[0] ?? null)
|
|
||||||
|
|
||||||
const describeProductRequirement = (requirement: PieceModelProduct, index: number) => {
|
const describeProductRequirement = (requirement: PieceModelProduct, index: number) => {
|
||||||
if (!requirement) {
|
if (!requirement) {
|
||||||
return `Produit ${index + 1}`
|
return `Produit ${index + 1}`
|
||||||
@@ -400,6 +418,39 @@ const productRequirementDescriptions = computed(() =>
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const ensureProductSelections = (count: number) => {
|
||||||
|
const next = Array.from({ length: count }, (_, index) => productSelections.value[index] ?? null)
|
||||||
|
productSelections.value = next
|
||||||
|
}
|
||||||
|
|
||||||
|
const productRequirementEntries = computed(() =>
|
||||||
|
structureProducts.value.map((requirement, index) => ({
|
||||||
|
index,
|
||||||
|
key: `piece-create-product-requirement-${index}-${requirement?.typeProductId || 'any'}`,
|
||||||
|
label: describeProductRequirement(requirement, index),
|
||||||
|
typeProductId: requirement?.typeProductId ? String(requirement.typeProductId) : null,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
|
||||||
|
const productSelectionsFilled = computed(() =>
|
||||||
|
!requiresProductSelection.value ||
|
||||||
|
productRequirementEntries.value.every((entry) => {
|
||||||
|
const value = productSelections.value[entry.index]
|
||||||
|
return typeof value === 'string' && value.trim().length > 0
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const setProductSelection = (index: number, value: string | null) => {
|
||||||
|
const normalized = typeof value === 'string' ? value : null
|
||||||
|
const next = [...productSelections.value]
|
||||||
|
next[index] = normalized
|
||||||
|
productSelections.value = next
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(structureProducts, (products) => {
|
||||||
|
ensureProductSelections(products.length)
|
||||||
|
})
|
||||||
|
|
||||||
watch(selectedType, (type) => {
|
watch(selectedType, (type) => {
|
||||||
if (!type) {
|
if (!type) {
|
||||||
clearCreationForm()
|
clearCreationForm()
|
||||||
@@ -411,7 +462,7 @@ watch(selectedType, (type) => {
|
|||||||
}
|
}
|
||||||
lastSuggestedName.value = creationForm.name
|
lastSuggestedName.value = creationForm.name
|
||||||
customFieldInputs.value = normalizeCustomFieldInputs(type.structure)
|
customFieldInputs.value = normalizeCustomFieldInputs(type.structure)
|
||||||
creationForm.productId = null
|
productSelections.value = Array.from({ length: structureProducts.value.length }, () => null)
|
||||||
})
|
})
|
||||||
|
|
||||||
const requiredCustomFieldsFilled = computed(() =>
|
const requiredCustomFieldsFilled = computed(() =>
|
||||||
@@ -431,7 +482,7 @@ const canSubmit = computed(() =>
|
|||||||
selectedType.value &&
|
selectedType.value &&
|
||||||
creationForm.name &&
|
creationForm.name &&
|
||||||
requiredCustomFieldsFilled.value &&
|
requiredCustomFieldsFilled.value &&
|
||||||
(!requiresProductSelection.value || creationForm.productId) &&
|
productSelectionsFilled.value &&
|
||||||
!submitting.value,
|
!submitting.value,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -449,18 +500,12 @@ const toFieldString = (value: unknown): string => {
|
|||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
const getStructureCustomFields = (structure: PieceModelStructure | null) =>
|
|
||||||
Array.isArray(structure?.customFields) ? structure.customFields : []
|
|
||||||
|
|
||||||
const getStructureProducts = (structure: PieceModelStructure | null) =>
|
|
||||||
Array.isArray(structure?.products) ? structure.products : []
|
|
||||||
|
|
||||||
const clearCreationForm = () => {
|
const clearCreationForm = () => {
|
||||||
creationForm.name = ''
|
creationForm.name = ''
|
||||||
creationForm.reference = ''
|
creationForm.reference = ''
|
||||||
creationForm.constructeurIds = []
|
creationForm.constructeurIds = []
|
||||||
creationForm.prix = ''
|
creationForm.prix = ''
|
||||||
creationForm.productId = null
|
productSelections.value = []
|
||||||
lastSuggestedName.value = ''
|
lastSuggestedName.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -470,7 +515,7 @@ const submitCreation = async () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (requiresProductSelection.value && !creationForm.productId) {
|
if (!productSelectionsFilled.value) {
|
||||||
toast.showError('Sélectionnez un produit conforme au squelette.')
|
toast.showError('Sélectionnez un produit conforme au squelette.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -487,12 +532,13 @@ const submitCreation = async () => {
|
|||||||
|
|
||||||
payload.constructeurIds = uniqueConstructeurIds(creationForm.constructeurIds)
|
payload.constructeurIds = uniqueConstructeurIds(creationForm.constructeurIds)
|
||||||
|
|
||||||
const selectedProductId =
|
const normalizedProductIds = productRequirementEntries.value
|
||||||
typeof creationForm.productId === 'string'
|
.map((entry) => productSelections.value[entry.index])
|
||||||
? creationForm.productId.trim()
|
.filter((value): value is string => typeof value === 'string' && value.trim().length > 0)
|
||||||
: ''
|
.map((value) => value.trim())
|
||||||
if (selectedProductId) {
|
if (normalizedProductIds.length) {
|
||||||
payload.productId = selectedProductId
|
payload.productIds = normalizedProductIds
|
||||||
|
payload.productId = normalizedProductIds[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
const rawPrice = typeof creationForm.prix === 'string'
|
const rawPrice = typeof creationForm.prix === 'string'
|
||||||
@@ -504,7 +550,7 @@ const submitCreation = async () => {
|
|||||||
if (rawPrice) {
|
if (rawPrice) {
|
||||||
const parsed = Number(rawPrice)
|
const parsed = Number(rawPrice)
|
||||||
if (!Number.isNaN(parsed)) {
|
if (!Number.isNaN(parsed)) {
|
||||||
payload.prix = parsed
|
payload.prix = String(parsed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -164,7 +164,9 @@
|
|||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
import { useHead } from '#imports'
|
import { useHead } from '#imports'
|
||||||
import { useProducts } from '~/composables/useProducts'
|
import { useProducts } from '~/composables/useProducts'
|
||||||
|
import { useProductTypes } from '~/composables/useProductTypes'
|
||||||
import { useToast } from '~/composables/useToast'
|
import { useToast } from '~/composables/useToast'
|
||||||
|
import { usePersistedSort } from '~/composables/usePersistedSort'
|
||||||
import DocumentThumbnail from '~/components/DocumentThumbnail.vue'
|
import DocumentThumbnail from '~/components/DocumentThumbnail.vue'
|
||||||
import { isImageDocument, isPdfDocument } from '~/utils/documentPreview'
|
import { isImageDocument, isPdfDocument } from '~/utils/documentPreview'
|
||||||
|
|
||||||
@@ -181,13 +183,25 @@ const {
|
|||||||
loadProducts,
|
loadProducts,
|
||||||
deleteProduct,
|
deleteProduct,
|
||||||
} = useProducts()
|
} = useProducts()
|
||||||
|
const { productTypes, loadProductTypes } = useProductTypes()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
const searchTerm = ref('')
|
const searchTerm = ref('')
|
||||||
const sortField = ref<'name' | 'createdAt'>('name')
|
const { sortField, sortDirection } = usePersistedSort<'name' | 'createdAt', 'asc' | 'desc'>(
|
||||||
const sortDirection = ref<'asc' | 'desc'>('asc')
|
'product-catalog',
|
||||||
|
{ field: 'name', direction: 'asc' },
|
||||||
|
)
|
||||||
|
|
||||||
const normalizedProducts = computed(() => (Array.isArray(products.value) ? products.value : []))
|
// Enrichir les produits avec les types de produits complets
|
||||||
|
const normalizedProducts = computed(() => {
|
||||||
|
return (Array.isArray(products.value) ? products.value : []).map((product) => {
|
||||||
|
const typeProduct = productTypes.value.find(t => t.id === product.typeProductId)
|
||||||
|
return {
|
||||||
|
...product,
|
||||||
|
typeProduct: typeProduct || product.typeProduct || null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
const hasLoaded = computed(() => loaded.value)
|
const hasLoaded = computed(() => loaded.value)
|
||||||
const errorMessage = computed(() => (typeof error.value === 'string' && error.value.length ? error.value : null))
|
const errorMessage = computed(() => (typeof error.value === 'string' && error.value.length ? error.value : null))
|
||||||
|
|
||||||
@@ -383,6 +397,9 @@ const confirmDelete = async (product: Record<string, any>) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadProducts()
|
await Promise.all([
|
||||||
|
loadProducts(),
|
||||||
|
loadProductTypes()
|
||||||
|
])
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -352,7 +352,7 @@ const route = useRoute()
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const { getProduct, updateProduct } = useProducts()
|
const { getProduct, updateProduct } = useProducts()
|
||||||
const { upsertCustomFieldValue, updateCustomFieldValue } = useCustomFields()
|
const { upsertCustomFieldValue, updateCustomFieldValue, getCustomFieldValuesByEntity } = useCustomFields()
|
||||||
const {
|
const {
|
||||||
loadDocumentsByProduct,
|
loadDocumentsByProduct,
|
||||||
uploadDocuments: uploadProductDocuments,
|
uploadDocuments: uploadProductDocuments,
|
||||||
@@ -373,6 +373,15 @@ const productDocuments = ref<any[]>([])
|
|||||||
const previewDocument = ref<any | null>(null)
|
const previewDocument = ref<any | null>(null)
|
||||||
const previewVisible = ref(false)
|
const previewVisible = ref(false)
|
||||||
|
|
||||||
|
const refreshCustomFieldInputs = (
|
||||||
|
structureOverride?: ProductModelStructure | null,
|
||||||
|
valuesOverride?: any[] | null,
|
||||||
|
) => {
|
||||||
|
const nextStructure = structureOverride ?? structure.value ?? null
|
||||||
|
const nextValues = valuesOverride ?? product.value?.customFieldValues ?? null
|
||||||
|
customFieldInputs.value = buildCustomFieldInputs(nextStructure, nextValues)
|
||||||
|
}
|
||||||
|
|
||||||
const editionForm = reactive({
|
const editionForm = reactive({
|
||||||
name: '' as string,
|
name: '' as string,
|
||||||
reference: '' as string,
|
reference: '' as string,
|
||||||
@@ -493,6 +502,11 @@ const loadProduct = async () => {
|
|||||||
product.value = result.data
|
product.value = result.data
|
||||||
productDocuments.value = Array.isArray(result.data?.documents) ? result.data.documents : []
|
productDocuments.value = Array.isArray(result.data?.documents) ? result.data.documents : []
|
||||||
await loadProductType()
|
await loadProductType()
|
||||||
|
const customValues = await getCustomFieldValuesByEntity('product', result.data.id)
|
||||||
|
if (customValues.success && Array.isArray(customValues.data)) {
|
||||||
|
product.value.customFieldValues = customValues.data
|
||||||
|
refreshCustomFieldInputs(undefined, customValues.data)
|
||||||
|
}
|
||||||
await hydrateForm()
|
await hydrateForm()
|
||||||
await refreshDocuments()
|
await refreshDocuments()
|
||||||
} else {
|
} else {
|
||||||
@@ -582,7 +596,7 @@ const hydrateForm = async () => {
|
|||||||
editionForm.supplierPrice = product.value.supplierPrice !== null && product.value.supplierPrice !== undefined
|
editionForm.supplierPrice = product.value.supplierPrice !== null && product.value.supplierPrice !== undefined
|
||||||
? String(product.value.supplierPrice)
|
? String(product.value.supplierPrice)
|
||||||
: ''
|
: ''
|
||||||
customFieldInputs.value = buildCustomFieldInputs(structure.value, product.value.customFieldValues)
|
refreshCustomFieldInputs(structure.value, product.value.customFieldValues)
|
||||||
if (editionForm.constructeurIds.length) {
|
if (editionForm.constructeurIds.length) {
|
||||||
await ensureConstructeurs(editionForm.constructeurIds)
|
await ensureConstructeurs(editionForm.constructeurIds)
|
||||||
}
|
}
|
||||||
@@ -693,13 +707,11 @@ const submitEdition = async () => {
|
|||||||
|
|
||||||
const rawPrice = typeof editionForm.supplierPrice === 'string'
|
const rawPrice = typeof editionForm.supplierPrice === 'string'
|
||||||
? editionForm.supplierPrice.trim()
|
? editionForm.supplierPrice.trim()
|
||||||
: editionForm.supplierPrice === null || editionForm.supplierPrice === undefined
|
: editionForm.supplierPrice
|
||||||
? ''
|
payload.supplierPrice = rawPrice !== '' && rawPrice !== null && rawPrice !== undefined
|
||||||
: String(editionForm.supplierPrice).trim()
|
|
||||||
payload.supplierPrice = rawPrice
|
|
||||||
? Number.isNaN(Number(rawPrice))
|
? Number.isNaN(Number(rawPrice))
|
||||||
? null
|
? null
|
||||||
: Number(rawPrice)
|
: String(Number(rawPrice))
|
||||||
: null
|
: null
|
||||||
|
|
||||||
saving.value = true
|
saving.value = true
|
||||||
@@ -734,20 +746,29 @@ const saveCustomFieldValues = async (productId: string) => {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!field.customFieldId) {
|
const metadata = field.customFieldId
|
||||||
continue
|
? undefined
|
||||||
}
|
: { customFieldName: field.name, customFieldType: field.type, customFieldRequired: field.required }
|
||||||
|
|
||||||
const result = await upsertCustomFieldValue(
|
const result = await upsertCustomFieldValue(
|
||||||
field.customFieldId,
|
field.customFieldId,
|
||||||
'product',
|
'product',
|
||||||
productId,
|
productId,
|
||||||
String(value ?? ''),
|
String(value ?? ''),
|
||||||
{ customFieldName: field.name, customFieldType: field.type, customFieldRequired: field.required },
|
metadata,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
failed.push(field.name)
|
failed.push(field.name)
|
||||||
|
} else {
|
||||||
|
const createdValue = result.data
|
||||||
|
if (createdValue?.id) {
|
||||||
|
field.customFieldValueId = createdValue.id
|
||||||
|
}
|
||||||
|
const resolvedId = createdValue?.customField?.id || field.customFieldId
|
||||||
|
if (resolvedId) {
|
||||||
|
field.customFieldId = resolvedId
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return failed
|
return failed
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<main class="mx-auto flex w-full max-w-4xl flex-col gap-8 px-4 py-8 sm:px-6 lg:px-8">
|
<main class="mx-auto flex w-full max-w-4xl flex-col gap-8 px-4 py-8 sm:px-6 lg:px-8">
|
||||||
<header class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
<header class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<h1 class="text-3xl font-semibold text-base-content">Nouveau produit</h1>
|
<h1 class="text-3xl font-semibold text-base-content">Nouveau produit</h1>
|
||||||
@@ -231,7 +231,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -427,13 +427,11 @@ const buildPayload = () => {
|
|||||||
|
|
||||||
const rawPrice = typeof creationForm.supplierPrice === 'string'
|
const rawPrice = typeof creationForm.supplierPrice === 'string'
|
||||||
? creationForm.supplierPrice.trim()
|
? creationForm.supplierPrice.trim()
|
||||||
: creationForm.supplierPrice === null || creationForm.supplierPrice === undefined
|
: creationForm.supplierPrice
|
||||||
? ''
|
if (rawPrice !== '' && rawPrice !== null && rawPrice !== undefined) {
|
||||||
: String(creationForm.supplierPrice).trim()
|
|
||||||
if (rawPrice) {
|
|
||||||
const parsed = Number(rawPrice)
|
const parsed = Number(rawPrice)
|
||||||
if (!Number.isNaN(parsed)) {
|
if (!Number.isNaN(parsed)) {
|
||||||
payload.supplierPrice = parsed
|
payload.supplierPrice = String(parsed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -490,19 +488,31 @@ const submitCreation = async () => {
|
|||||||
const saveCustomFieldValues = async (productId: string) => {
|
const saveCustomFieldValues = async (productId: string) => {
|
||||||
const failed: string[] = []
|
const failed: string[] = []
|
||||||
for (const field of customFieldInputs.value) {
|
for (const field of customFieldInputs.value) {
|
||||||
if (!field.customFieldId || !field.name) {
|
if (!field.name) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const value = field.value ?? ''
|
const value = field.value ?? ''
|
||||||
|
const metadata = field.customFieldId
|
||||||
|
? undefined
|
||||||
|
: { customFieldName: field.name, customFieldType: field.type, customFieldRequired: field.required }
|
||||||
const result = await upsertCustomFieldValue(
|
const result = await upsertCustomFieldValue(
|
||||||
field.customFieldId,
|
field.customFieldId,
|
||||||
'product',
|
'product',
|
||||||
productId,
|
productId,
|
||||||
String(value ?? ''),
|
String(value ?? ''),
|
||||||
{ customFieldName: field.name, customFieldType: field.type, customFieldRequired: field.required },
|
metadata,
|
||||||
)
|
)
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
failed.push(field.name)
|
failed.push(field.name)
|
||||||
|
} else {
|
||||||
|
const createdValue = result.data
|
||||||
|
if (createdValue?.id) {
|
||||||
|
field.customFieldValueId = createdValue.id
|
||||||
|
}
|
||||||
|
const resolvedId = createdValue?.customField?.id || field.customFieldId
|
||||||
|
if (resolvedId) {
|
||||||
|
field.customFieldId = resolvedId
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return failed
|
return failed
|
||||||
|
|||||||
@@ -187,6 +187,14 @@ onMounted(async () => {
|
|||||||
const typeId = route.params.id
|
const typeId = route.params.id
|
||||||
console.log('=== TYPE DETAIL PAGE LOADING ===')
|
console.log('=== TYPE DETAIL PAGE LOADING ===')
|
||||||
console.log('Loading type with ID:', typeId)
|
console.log('Loading type with ID:', typeId)
|
||||||
|
console.log('Full route params:', route.params)
|
||||||
|
|
||||||
|
if (!typeId) {
|
||||||
|
console.error('No type ID provided in route')
|
||||||
|
showError('Aucun identifiant de type fourni')
|
||||||
|
loading.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const result = await getMachineTypeById(typeId)
|
const result = await getMachineTypeById(typeId)
|
||||||
console.log('API Result:', result)
|
console.log('API Result:', result)
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ import { ref, onMounted } from 'vue'
|
|||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { useMachineTypesApi } from '~/composables/useMachineTypesApi'
|
import { useMachineTypesApi } from '~/composables/useMachineTypesApi'
|
||||||
import { useToast } from '~/composables/useToast'
|
import { useToast } from '~/composables/useToast'
|
||||||
|
import { extractRelationId } from '~/shared/apiRelations'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -90,6 +91,20 @@ const parseOptions = (field = {}) => {
|
|||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toModelTypeIri = (value) => {
|
||||||
|
if (!value) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
if (typeof value === 'string' && value.startsWith('/api/model_types/')) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
const relationId = extractRelationId(value)
|
||||||
|
if (relationId) {
|
||||||
|
return `/api/model_types/${relationId}`
|
||||||
|
}
|
||||||
|
return typeof value === 'string' ? `/api/model_types/${value}` : undefined
|
||||||
|
}
|
||||||
|
|
||||||
const normalizeCustomFields = (fields = []) =>
|
const normalizeCustomFields = (fields = []) =>
|
||||||
fields
|
fields
|
||||||
.filter(field => field?.name && field.name.trim() !== '')
|
.filter(field => field?.name && field.name.trim() !== '')
|
||||||
@@ -113,9 +128,9 @@ const toIntegerOrNull = (value, fallback = null) => {
|
|||||||
|
|
||||||
const normalizeComponentRequirements = (requirements = []) =>
|
const normalizeComponentRequirements = (requirements = []) =>
|
||||||
requirements
|
requirements
|
||||||
.filter(req => req?.typeComposantId)
|
.filter(req => req?.typeComposantId || req?.typeComposant)
|
||||||
.map((req, index) => ({
|
.map((req, index) => ({
|
||||||
typeComposantId: req.typeComposantId,
|
typeComposant: toModelTypeIri(req.typeComposantId || req.typeComposant),
|
||||||
label: req.label?.trim() ? req.label.trim() : undefined,
|
label: req.label?.trim() ? req.label.trim() : undefined,
|
||||||
minCount: toIntegerOrNull(req.minCount, 1),
|
minCount: toIntegerOrNull(req.minCount, 1),
|
||||||
maxCount: toIntegerOrNull(req.maxCount, null),
|
maxCount: toIntegerOrNull(req.maxCount, null),
|
||||||
@@ -128,9 +143,9 @@ const normalizeComponentRequirements = (requirements = []) =>
|
|||||||
|
|
||||||
const normalizePieceRequirements = (requirements = []) =>
|
const normalizePieceRequirements = (requirements = []) =>
|
||||||
requirements
|
requirements
|
||||||
.filter(req => req?.typePieceId)
|
.filter(req => req?.typePieceId || req?.typePiece)
|
||||||
.map((req, index) => ({
|
.map((req, index) => ({
|
||||||
typePieceId: req.typePieceId,
|
typePiece: toModelTypeIri(req.typePieceId || req.typePiece),
|
||||||
label: req.label?.trim() ? req.label.trim() : undefined,
|
label: req.label?.trim() ? req.label.trim() : undefined,
|
||||||
minCount: toIntegerOrNull(req.minCount, 0),
|
minCount: toIntegerOrNull(req.minCount, 0),
|
||||||
maxCount: toIntegerOrNull(req.maxCount, null),
|
maxCount: toIntegerOrNull(req.maxCount, null),
|
||||||
@@ -143,9 +158,9 @@ const normalizePieceRequirements = (requirements = []) =>
|
|||||||
|
|
||||||
const normalizeProductRequirements = (requirements = []) =>
|
const normalizeProductRequirements = (requirements = []) =>
|
||||||
requirements
|
requirements
|
||||||
.filter(req => req?.typeProductId)
|
.filter(req => req?.typeProductId || req?.typeProduct)
|
||||||
.map((req, index) => ({
|
.map((req, index) => ({
|
||||||
typeProductId: req.typeProductId,
|
typeProduct: toModelTypeIri(req.typeProductId || req.typeProduct),
|
||||||
label: req.label?.trim() ? req.label.trim() : undefined,
|
label: req.label?.trim() ? req.label.trim() : undefined,
|
||||||
minCount: toIntegerOrNull(req.minCount, 0),
|
minCount: toIntegerOrNull(req.minCount, 0),
|
||||||
maxCount: toIntegerOrNull(req.maxCount, null),
|
maxCount: toIntegerOrNull(req.maxCount, null),
|
||||||
@@ -194,7 +209,7 @@ onMounted(async () => {
|
|||||||
console.log('=== EDIT TYPE PAGE LOADING ===')
|
console.log('=== EDIT TYPE PAGE LOADING ===')
|
||||||
console.log('Loading type with ID:', typeId)
|
console.log('Loading type with ID:', typeId)
|
||||||
|
|
||||||
const result = await getMachineTypeById(typeId)
|
const result = await getMachineTypeById(typeId, true)
|
||||||
console.log('API Result:', result)
|
console.log('API Result:', result)
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ export interface ModelType extends BaseModelTypePayload {
|
|||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
category: ModelCategory;
|
category: ModelCategory;
|
||||||
structure: ModelTypeStructure;
|
structure: ModelTypeStructure;
|
||||||
|
componentSkeleton?: ComponentModelStructure | null;
|
||||||
|
pieceSkeleton?: PieceModelStructure | null;
|
||||||
|
productSkeleton?: ProductModelStructure | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ModelTypeListParams {
|
export interface ModelTypeListParams {
|
||||||
@@ -65,7 +68,7 @@ export interface ModelTypeListResponse {
|
|||||||
limit: number;
|
limit: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ENDPOINT = '/api/model-types';
|
const ENDPOINT = '/model_types';
|
||||||
|
|
||||||
function resolveBaseUrl() {
|
function resolveBaseUrl() {
|
||||||
const runtimeConfig = useRuntimeConfig();
|
const runtimeConfig = useRuntimeConfig();
|
||||||
@@ -80,7 +83,47 @@ function createOptions<T>(options: FetchOptions<T> = {}) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listModelTypes(params: ModelTypeListParams = {}, opts: { signal?: AbortSignal } = {}) {
|
const normalizeModelType = (item: any): ModelType => {
|
||||||
|
if (!item || typeof item !== 'object') {
|
||||||
|
return item as ModelType;
|
||||||
|
}
|
||||||
|
if (!item.structure) {
|
||||||
|
if (item.category === 'COMPONENT' && item.componentSkeleton) {
|
||||||
|
item.structure = item.componentSkeleton;
|
||||||
|
} else if (item.category === 'PIECE' && item.pieceSkeleton) {
|
||||||
|
item.structure = item.pieceSkeleton;
|
||||||
|
} else if (item.category === 'PRODUCT' && item.productSkeleton) {
|
||||||
|
item.structure = item.productSkeleton;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return item as ModelType;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStructureToSkeleton = <T extends Record<string, any>>(payload: T): T => {
|
||||||
|
if (!payload || typeof payload !== 'object') {
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
if (!('structure' in payload)) {
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
const structure = (payload as any).structure;
|
||||||
|
if (!structure) {
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
const category = (payload as any).category;
|
||||||
|
const next = { ...payload } as Record<string, any>;
|
||||||
|
if (category === 'COMPONENT') {
|
||||||
|
next.componentSkeleton = structure;
|
||||||
|
} else if (category === 'PIECE') {
|
||||||
|
next.pieceSkeleton = structure;
|
||||||
|
} else if (category === 'PRODUCT') {
|
||||||
|
next.productSkeleton = structure;
|
||||||
|
}
|
||||||
|
delete next.structure;
|
||||||
|
return next as T;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function listModelTypes(params: ModelTypeListParams = {}, opts: { signal?: AbortSignal } = {}) {
|
||||||
const requestFetch = useRequestFetch();
|
const requestFetch = useRequestFetch();
|
||||||
const query: Record<string, string | number> = {};
|
const query: Record<string, string | number> = {};
|
||||||
|
|
||||||
@@ -96,36 +139,84 @@ export function listModelTypes(params: ModelTypeListParams = {}, opts: { signal?
|
|||||||
if (params.dir) {
|
if (params.dir) {
|
||||||
query.dir = params.dir;
|
query.dir = params.dir;
|
||||||
}
|
}
|
||||||
if (typeof params.limit === 'number') {
|
const hasCategoryFilter = Boolean(params.category);
|
||||||
query.limit = params.limit;
|
const effectiveLimit = typeof params.limit === 'number' ? params.limit : undefined;
|
||||||
}
|
const effectiveOffset = typeof params.offset === 'number' ? params.offset : 0;
|
||||||
if (typeof params.offset === 'number') {
|
|
||||||
query.offset = params.offset;
|
if (hasCategoryFilter) {
|
||||||
|
// Fetch enough items to allow client-side category filtering + pagination.
|
||||||
|
query.itemsPerPage = Math.max(effectiveLimit ?? 200, 200);
|
||||||
|
query.offset = 0;
|
||||||
|
} else {
|
||||||
|
if (typeof params.limit === 'number') {
|
||||||
|
query.itemsPerPage = params.limit;
|
||||||
|
}
|
||||||
|
if (typeof params.offset === 'number') {
|
||||||
|
query.offset = params.offset;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return requestFetch<ModelTypeListResponse>(ENDPOINT, createOptions({
|
const payload = await requestFetch<Record<string, any>>(ENDPOINT, createOptions({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
query,
|
query,
|
||||||
signal: opts.signal,
|
signal: opts.signal,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const rawItems = Array.isArray(payload?.member)
|
||||||
|
? payload.member
|
||||||
|
: Array.isArray(payload?.['hydra:member'])
|
||||||
|
? payload['hydra:member']
|
||||||
|
: Array.isArray(payload?.items)
|
||||||
|
? payload.items
|
||||||
|
: [];
|
||||||
|
const filteredItems = params.category
|
||||||
|
? rawItems.filter((item: any) => item?.category === params.category)
|
||||||
|
: rawItems;
|
||||||
|
const total = params.category
|
||||||
|
? filteredItems.length
|
||||||
|
: typeof payload?.totalItems === 'number'
|
||||||
|
? payload.totalItems
|
||||||
|
: Array.isArray(payload?.items)
|
||||||
|
? payload.items.length
|
||||||
|
: rawItems.length;
|
||||||
|
const items = (params.category && typeof effectiveLimit === 'number'
|
||||||
|
? filteredItems.slice(effectiveOffset, effectiveOffset + effectiveLimit)
|
||||||
|
: filteredItems).map(normalizeModelType);
|
||||||
|
|
||||||
|
return {
|
||||||
|
items,
|
||||||
|
total,
|
||||||
|
offset: effectiveOffset,
|
||||||
|
limit: typeof effectiveLimit === 'number' ? effectiveLimit : items.length,
|
||||||
|
} satisfies ModelTypeListResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createModelType(payload: ModelTypePayload, opts: { signal?: AbortSignal } = {}) {
|
export function createModelType(payload: ModelTypePayload, opts: { signal?: AbortSignal } = {}) {
|
||||||
const requestFetch = useRequestFetch();
|
const requestFetch = useRequestFetch();
|
||||||
|
const mappedPayload = mapStructureToSkeleton(payload);
|
||||||
return requestFetch<ModelType>(ENDPOINT, createOptions({
|
return requestFetch<ModelType>(ENDPOINT, createOptions({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: payload,
|
headers: {
|
||||||
|
'Content-Type': 'application/ld+json',
|
||||||
|
Accept: 'application/ld+json',
|
||||||
|
},
|
||||||
|
body: mappedPayload,
|
||||||
signal: opts.signal,
|
signal: opts.signal,
|
||||||
}));
|
})).then(normalizeModelType);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateModelType(id: string, payload: Partial<ModelTypePayload>, opts: { signal?: AbortSignal } = {}) {
|
export function updateModelType(id: string, payload: Partial<ModelTypePayload>, opts: { signal?: AbortSignal } = {}) {
|
||||||
const requestFetch = useRequestFetch();
|
const requestFetch = useRequestFetch();
|
||||||
|
const mappedPayload = mapStructureToSkeleton(payload);
|
||||||
return requestFetch<ModelType>(`${ENDPOINT}/${id}`, createOptions({
|
return requestFetch<ModelType>(`${ENDPOINT}/${id}`, createOptions({
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
body: payload,
|
headers: {
|
||||||
|
'Content-Type': 'application/merge-patch+json',
|
||||||
|
Accept: 'application/ld+json',
|
||||||
|
},
|
||||||
|
body: mappedPayload,
|
||||||
signal: opts.signal,
|
signal: opts.signal,
|
||||||
}));
|
})).then(normalizeModelType);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteModelType(id: string, opts: { signal?: AbortSignal } = {}) {
|
export function deleteModelType(id: string, opts: { signal?: AbortSignal } = {}) {
|
||||||
@@ -141,5 +232,5 @@ export function getModelType(id: string, opts: { signal?: AbortSignal } = {}) {
|
|||||||
return requestFetch<ModelType>(`${ENDPOINT}/${id}`, createOptions({
|
return requestFetch<ModelType>(`${ENDPOINT}/${id}`, createOptions({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
signal: opts.signal,
|
signal: opts.signal,
|
||||||
}));
|
})).then(normalizeModelType);
|
||||||
}
|
}
|
||||||
|
|||||||
57
app/shared/apiRelations.ts
Normal file
57
app/shared/apiRelations.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
export const RELATION_ID_MAP: Record<string, { key: string; path: string }> = {
|
||||||
|
siteId: { key: 'site', path: 'sites' },
|
||||||
|
machineId: { key: 'machine', path: 'machines' },
|
||||||
|
composantId: { key: 'composant', path: 'composants' },
|
||||||
|
pieceId: { key: 'piece', path: 'pieces' },
|
||||||
|
productId: { key: 'product', path: 'products' },
|
||||||
|
typeMachineId: { key: 'typeMachine', path: 'type_machines' },
|
||||||
|
typeComposantId: { key: 'typeComposant', path: 'model_types' },
|
||||||
|
typePieceId: { key: 'typePiece', path: 'model_types' },
|
||||||
|
typeProductId: { key: 'typeProduct', path: 'model_types' },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const toIri = (path: string, id: string): string => `/api/${path}/${id}`;
|
||||||
|
|
||||||
|
export const extractRelationId = (value: unknown): string | null => {
|
||||||
|
if (!value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (trimmed.includes('/')) {
|
||||||
|
const parts = trimmed.split('/').filter(Boolean);
|
||||||
|
return parts.length ? parts[parts.length - 1] : null;
|
||||||
|
}
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
if (typeof value === 'object' && 'id' in (value as Record<string, any>)) {
|
||||||
|
const id = (value as Record<string, any>).id;
|
||||||
|
return typeof id === 'string' ? id : null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const normalizeRelationIds = <T extends Record<string, any>>(payload: T): T => {
|
||||||
|
if (!payload || typeof payload !== 'object') {
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
const next: Record<string, any> = { ...payload };
|
||||||
|
Object.entries(RELATION_ID_MAP).forEach(([sourceKey, config]) => {
|
||||||
|
const raw = next[sourceKey];
|
||||||
|
if (typeof raw !== 'string') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const trimmed = raw.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
next[config.key] = toIri(config.path, trimmed);
|
||||||
|
delete next[sourceKey];
|
||||||
|
});
|
||||||
|
|
||||||
|
return next as T;
|
||||||
|
};
|
||||||
@@ -15,7 +15,14 @@ const toStringId = (value: unknown): string | null => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const trimmed = value.trim();
|
const trimmed = value.trim();
|
||||||
return trimmed.length > 0 ? trimmed : null;
|
if (!trimmed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (trimmed.includes('/')) {
|
||||||
|
const parts = trimmed.split('/').filter(Boolean);
|
||||||
|
return parts.length ? parts[parts.length - 1] : null;
|
||||||
|
}
|
||||||
|
return trimmed;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const uniqueConstructeurIds = (...sources: unknown[]): string[] => {
|
export const uniqueConstructeurIds = (...sources: unknown[]): string[] => {
|
||||||
@@ -53,7 +60,9 @@ export const uniqueConstructeurIds = (...sources: unknown[]): string[] => {
|
|||||||
if (value.constructeur) {
|
if (value.constructeur) {
|
||||||
explore(value.constructeur);
|
explore(value.constructeur);
|
||||||
}
|
}
|
||||||
if (typeof value.id === 'string') {
|
// Only extract ID if this looks like a constructeur object (has @type or recognizable fields)
|
||||||
|
// Don't extract ID from component/piece/product objects that happen to be passed in
|
||||||
|
if (typeof value.id === 'string' && !value.name && !value.typeComposant && !value.typePiece && !value.typeProduct) {
|
||||||
pushId(value.id);
|
pushId(value.id);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -107,19 +116,49 @@ export const formatConstructeurContact = (
|
|||||||
|
|
||||||
export const buildConstructeurRequestPayload = <T extends Record<string, any>>(
|
export const buildConstructeurRequestPayload = <T extends Record<string, any>>(
|
||||||
payload: T,
|
payload: T,
|
||||||
): T & { constructeurIds: string[] } => {
|
): T & { constructeurs?: string[] } => {
|
||||||
const ids = uniqueConstructeurIds(
|
const collected = new Set(uniqueConstructeurIds(
|
||||||
payload?.constructeurIds,
|
payload?.constructeurIds,
|
||||||
payload?.constructeurId,
|
payload?.constructeurId,
|
||||||
payload?.constructeur,
|
payload?.constructeur,
|
||||||
payload?.constructeurs,
|
payload?.constructeurs,
|
||||||
);
|
));
|
||||||
|
|
||||||
|
if (!collected.size) {
|
||||||
|
const fallbackLists = [
|
||||||
|
payload?.constructeurIds,
|
||||||
|
payload?.constructeurs,
|
||||||
|
];
|
||||||
|
fallbackLists.forEach((list) => {
|
||||||
|
if (!Array.isArray(list)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
list.forEach((item) => {
|
||||||
|
if (typeof item === 'string') {
|
||||||
|
const id = toStringId(item);
|
||||||
|
if (id) {
|
||||||
|
collected.add(id);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isObject(item) && typeof item.id === 'string') {
|
||||||
|
collected.add(item.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const ids = Array.from(collected);
|
||||||
|
|
||||||
const next = { ...payload } as Record<string, any>;
|
const next = { ...payload } as Record<string, any>;
|
||||||
next.constructeurIds = ids;
|
|
||||||
delete next.constructeurId;
|
delete next.constructeurId;
|
||||||
delete next.constructeur;
|
delete next.constructeur;
|
||||||
delete next.constructeurs;
|
delete next.constructeurs;
|
||||||
|
delete next.constructeurIds;
|
||||||
|
|
||||||
return next as T & { constructeurIds: string[] };
|
if (ids.length) {
|
||||||
|
next.constructeurs = ids.map((id) => `/api/constructeurs/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return next as T & { constructeurs?: string[] };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,28 @@
|
|||||||
import tailwindcss from '@tailwindcss/vite'
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
|
import { readFileSync } from 'node:fs'
|
||||||
|
import { dirname, resolve } from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
|
// Lire la version depuis le fichier VERSION à la racine du projet parent
|
||||||
|
const getAppVersion = (): string => {
|
||||||
|
try {
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||||
|
const versionPath = resolve(__dirname, '..', 'VERSION')
|
||||||
|
return readFileSync(versionPath, 'utf-8').trim()
|
||||||
|
} catch {
|
||||||
|
return '0.0.0'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const appVersion = process.env.NUXT_PUBLIC_APP_VERSION || getAppVersion()
|
||||||
|
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
compatibilityDate: '2025-07-15',
|
compatibilityDate: '2025-07-15',
|
||||||
|
ssr: false, // Désactive le SSR pour un mode SPA pur (Client-Side Rendering uniquement)
|
||||||
devtools: { enabled: true },
|
devtools: { enabled: true },
|
||||||
devServer: {
|
devServer: {
|
||||||
port: 3001
|
host: '0.0.0.0',
|
||||||
|
port: 3000
|
||||||
},
|
},
|
||||||
modules: [
|
modules: [
|
||||||
[
|
[
|
||||||
@@ -18,11 +37,14 @@ export default defineNuxtConfig({
|
|||||||
]
|
]
|
||||||
],
|
],
|
||||||
runtimeConfig: {
|
runtimeConfig: {
|
||||||
|
apiBaseUrl: process.env.NUXT_API_BASE_URL
|
||||||
|
|| process.env.NUXT_PUBLIC_API_BASE_URL
|
||||||
|
|| 'http://localhost/api',
|
||||||
public: {
|
public: {
|
||||||
apiBaseUrl: process.env.NUXT_PUBLIC_API_BASE_URL || 'http://localhost:3000',
|
apiBaseUrl: process.env.NUXT_PUBLIC_API_BASE_URL || 'http://localhost:8081/api',
|
||||||
appUrl: process.env.NUXT_PUBLIC_APP_URL || 'http://localhost:3001',
|
appUrl: process.env.NUXT_PUBLIC_APP_URL || 'http://localhost:3001',
|
||||||
appName: process.env.NUXT_PUBLIC_APP_NAME || 'Inventory Management System',
|
appName: process.env.NUXT_PUBLIC_APP_NAME || 'Inventory Management System',
|
||||||
appVersion: process.env.NUXT_PUBLIC_APP_VERSION || '0.1.0',
|
appVersion: appVersion,
|
||||||
apiTimeout: process.env.NUXT_PUBLIC_API_TIMEOUT || '30000',
|
apiTimeout: process.env.NUXT_PUBLIC_API_TIMEOUT || '30000',
|
||||||
requestTimeout: process.env.NUXT_PUBLIC_REQUEST_TIMEOUT || '10000',
|
requestTimeout: process.env.NUXT_PUBLIC_REQUEST_TIMEOUT || '10000',
|
||||||
enableDebug: process.env.NUXT_PUBLIC_ENABLE_DEBUG || 'true',
|
enableDebug: process.env.NUXT_PUBLIC_ENABLE_DEBUG || 'true',
|
||||||
|
|||||||
3
package-lock.json
generated
3
package-lock.json
generated
@@ -3754,7 +3754,6 @@
|
|||||||
"integrity": "sha512-UG8hdElzuBDzIbjG1QDwnYH0MQ73YLXDFHgZzB4Zh/YJfnw8XNsloVtytqzx0I2Qky9THSdpTmi8Vjn/pf/Lew==",
|
"integrity": "sha512-UG8hdElzuBDzIbjG1QDwnYH0MQ73YLXDFHgZzB4Zh/YJfnw8XNsloVtytqzx0I2Qky9THSdpTmi8Vjn/pf/Lew==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.9.0",
|
"@eslint-community/eslint-utils": "^4.9.0",
|
||||||
"@typescript-eslint/types": "^8.44.0",
|
"@typescript-eslint/types": "^8.44.0",
|
||||||
@@ -11673,7 +11672,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.2.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.2.tgz",
|
||||||
"integrity": "sha512-I25/2QgoROE1vYV+NQ1En9T9UFB9Cmfm2CJ83zZOlaDpvz29wGQSZXWKw7MiNXau7wYgB/T9fVIdIuEQ+KbiiA==",
|
"integrity": "sha512-I25/2QgoROE1vYV+NQ1En9T9UFB9Cmfm2CJ83zZOlaDpvz29wGQSZXWKw7MiNXau7wYgB/T9fVIdIuEQ+KbiiA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/estree": "1.0.8"
|
"@types/estree": "1.0.8"
|
||||||
},
|
},
|
||||||
@@ -12483,6 +12481,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz",
|
"resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz",
|
||||||
"integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==",
|
"integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==",
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/source-map": "^0.3.3",
|
"@jridgewell/source-map": "^0.3.3",
|
||||||
"acorn": "^8.15.0",
|
"acorn": "^8.15.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user