diff --git a/app/components/model-types/ManagementView.vue b/app/components/model-types/ManagementView.vue index e179b22..c88b553 100644 --- a/app/components/model-types/ManagementView.vue +++ b/app/components/model-types/ManagementView.vue @@ -29,10 +29,65 @@ :total="total" :limit="limit" :offset="offset" + @related="openRelatedModal" @edit="openEditPage" @delete="confirmDelete" @update:offset="onOffsetChange" /> + + + + @@ -41,6 +96,7 @@ import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue"; import { useHead, useRouter } from "#imports"; import ModelTypesToolbar from "~/components/model-types/Toolbar.vue"; import ModelTypesTable from "~/components/model-types/Table.vue"; +import { useApi } from "~/composables/useApi"; import { deleteModelType, listModelTypes, @@ -82,6 +138,7 @@ let activeController: AbortController | null = null; const router = useRouter(); const { showError, showSuccess } = useToast(); +const { get } = useApi(); const headingText = computed(() => props.heading); const descriptionText = computed( @@ -257,6 +314,165 @@ const confirmDelete = async (item: ModelType) => { } }; +type RelatedEntry = { + id: string; + name: string; + reference?: string | null; +}; + +const relatedModalOpen = ref(false); +const relatedLoading = ref(false); +const relatedError = ref(null); +const relatedItems = ref([]); +const relatedType = ref(null); + +const relatedCategoryLabels: Record< + ModelCategory, + { plural: string; singular: string } +> = { + COMPONENT: { plural: "composants", singular: "composant" }, + PIECE: { plural: "pièces", singular: "pièce" }, + PRODUCT: { plural: "produits", singular: "produit" }, +}; + +const relatedModalTitle = computed(() => { + const current = relatedType.value; + if (!current) { + return "Éléments liés"; + } + return `Éléments liés à « ${current.name} »`; +}); + +const relatedModalSubtitle = computed(() => { + const current = relatedType.value; + if (!current) { + return ""; + } + const labels = + relatedCategoryLabels[current.category] ?? relatedCategoryLabels.COMPONENT; + const count = relatedItems.value.length; + if (relatedLoading.value) { + return `Chargement des ${labels.plural}…`; + } + if (count === 0) { + return `Aucun ${labels.singular} lié.`; + } + if (count === 1) { + return `1 ${labels.singular} lié.`; + } + return `${count} ${labels.plural} liés.`; +}); + +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?.items)) { + return payload.items; + } + return []; +}; + +const buildModelTypeIri = (id: string) => `/api/model_types/${id}`; + +const resolveRelatedConfig = (category: ModelCategory) => { + if (category === "COMPONENT") { + return { endpoint: "/composants", filterKey: "typeComposant" }; + } + if (category === "PIECE") { + return { endpoint: "/pieces", filterKey: "typePiece" }; + } + return { endpoint: "/products", filterKey: "typeProduct" }; +}; + +const resolveRelatedEditBasePath = (category: ModelCategory) => { + if (category === "COMPONENT") { + return "/component"; + } + if (category === "PIECE") { + return "/pieces"; + } + return "/product"; +}; + +const mapRelatedEntry = (item: any): RelatedEntry | null => { + if (!item || typeof item !== "object" || typeof item.id !== "string") { + return null; + } + const name = + typeof item.name === "string" && item.name.trim() + ? item.name + : "Sans nom"; + const reference = + typeof item.reference === "string" && item.reference.trim() + ? item.reference + : typeof item.code === "string" && item.code.trim() + ? item.code + : null; + return { + id: item.id, + name, + reference, + }; +}; + +const loadRelatedItems = async (item: ModelType) => { + const { endpoint, filterKey } = resolveRelatedConfig(item.category); + const params = new URLSearchParams(); + params.set("itemsPerPage", "200"); + params.set(filterKey, buildModelTypeIri(item.id)); + params.set("order[name]", "asc"); + + relatedLoading.value = true; + relatedError.value = null; + relatedItems.value = []; + + try { + const result = await get(`${endpoint}?${params.toString()}`); + if (!result.success) { + relatedError.value = + result.error ?? "Impossible de charger les éléments liés."; + return; + } + const collection = extractCollection(result.data); + relatedItems.value = collection + .map(mapRelatedEntry) + .filter((entry): entry is RelatedEntry => Boolean(entry)); + } catch (error) { + relatedError.value = extractErrorMessage(error); + } finally { + relatedLoading.value = false; + } +}; + +const openRelatedModal = (item: ModelType) => { + relatedType.value = item; + relatedModalOpen.value = true; + void loadRelatedItems(item); +}; + +const openRelatedEdit = (entry: RelatedEntry) => { + const current = relatedType.value; + if (!current) { + return; + } + const basePath = resolveRelatedEditBasePath(current.category); + relatedModalOpen.value = false; + router.push(`${basePath}/${entry.id}/edit`).catch(() => { + showError("Navigation impossible vers la fiche d'édition."); + }); +}; + +const closeRelatedModal = () => { + relatedModalOpen.value = false; +}; + watch( () => searchInput.value, (value) => { diff --git a/app/components/model-types/ModelTypeForm.vue b/app/components/model-types/ModelTypeForm.vue index adfb92f..84cb4b4 100644 --- a/app/components/model-types/ModelTypeForm.vue +++ b/app/components/model-types/ModelTypeForm.vue @@ -108,6 +108,15 @@ + +