feat(ui) : add UsedInSection showing reverse entity relationships on detail pages
This commit is contained in:
49
frontend/app/components/common/UsedInSection.vue
Normal file
49
frontend/app/components/common/UsedInSection.vue
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="!loading && totalCount > 0" class="space-y-3 rounded-lg border border-base-200 bg-base-200/30 p-4">
|
||||||
|
<h3 class="font-semibold text-base-content">Utilisé dans</h3>
|
||||||
|
|
||||||
|
<div v-if="data.machines.length" class="space-y-1">
|
||||||
|
<p class="text-xs font-medium text-base-content/60 uppercase tracking-wide">Machines</p>
|
||||||
|
<div v-for="m in data.machines" :key="m.id" class="flex items-center gap-2 text-sm">
|
||||||
|
<NuxtLink :to="`/machine/${m.id}`" class="hover:underline hover:text-primary transition-colors font-medium">
|
||||||
|
{{ m.name }}
|
||||||
|
</NuxtLink>
|
||||||
|
<span v-if="m.site?.name" class="badge badge-ghost badge-xs">{{ m.site.name }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="data.composants.length" class="space-y-1">
|
||||||
|
<p class="text-xs font-medium text-base-content/60 uppercase tracking-wide">Composants</p>
|
||||||
|
<div v-for="c in data.composants" :key="c.id" class="text-sm">
|
||||||
|
<NuxtLink :to="`/component/${c.id}`" class="hover:underline hover:text-primary transition-colors font-medium">
|
||||||
|
{{ c.name }}
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="data.pieces.length" class="space-y-1">
|
||||||
|
<p class="text-xs font-medium text-base-content/60 uppercase tracking-wide">Pièces</p>
|
||||||
|
<div v-for="p in data.pieces" :key="p.id" class="text-sm">
|
||||||
|
<NuxtLink :to="`/piece/${p.id}`" class="hover:underline hover:text-primary transition-colors font-medium">
|
||||||
|
{{ p.name }}
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="loading" class="flex justify-center py-4">
|
||||||
|
<span class="loading loading-spinner loading-sm" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const props = defineProps<{
|
||||||
|
entityType: string
|
||||||
|
entityId: string | null
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { data, loading, totalCount } = useUsedIn(
|
||||||
|
computed(() => props.entityType),
|
||||||
|
computed(() => props.entityId),
|
||||||
|
)
|
||||||
|
</script>
|
||||||
52
frontend/app/composables/useUsedIn.ts
Normal file
52
frontend/app/composables/useUsedIn.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import type { Ref } from 'vue'
|
||||||
|
|
||||||
|
interface UsedInMachine {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
site?: { id: string; name: string } | null
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UsedInEntity {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UsedInData {
|
||||||
|
machines: UsedInMachine[]
|
||||||
|
composants: UsedInEntity[]
|
||||||
|
pieces: UsedInEntity[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUsedIn(entityType: Ref<string>, entityId: Ref<string | null>) {
|
||||||
|
const data = ref<UsedInData>({ machines: [], composants: [], pieces: [] })
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
const load = async () => {
|
||||||
|
if (!entityId.value) return
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const result = await api.get(`/${entityType.value}/${entityId.value}/used-in`)
|
||||||
|
if (result.success && result.data) {
|
||||||
|
data.value = {
|
||||||
|
machines: result.data.machines || [],
|
||||||
|
composants: result.data.composants || [],
|
||||||
|
pieces: result.data.pieces || [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalCount = computed(() =>
|
||||||
|
data.value.machines.length + data.value.composants.length + data.value.pieces.length
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(entityId, (val) => {
|
||||||
|
if (val) load()
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
return { data, loading, totalCount, load }
|
||||||
|
}
|
||||||
@@ -198,6 +198,8 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<CommonUsedInSection entity-type="composants" :entity-id="component?.id ?? null" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -222,6 +222,8 @@
|
|||||||
:preview-badge="formatPieceStructurePreview(resolvedStructure)"
|
:preview-badge="formatPieceStructurePreview(resolvedStructure)"
|
||||||
variant="piece"
|
variant="piece"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<CommonUsedInSection entity-type="pieces" :entity-id="piece?.id ?? null" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -198,6 +198,8 @@
|
|||||||
<span class="badge badge-outline">{{ structurePreview }}</span>
|
<span class="badge badge-outline">{{ structurePreview }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<CommonUsedInSection entity-type="products" :entity-id="product?.id ?? null" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user