Compare commits
7 Commits
a7415964a7
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
958a00c8fc | ||
|
|
e0f761da2b | ||
|
|
80739a4528 | ||
|
|
c5988ec7a6 | ||
|
|
63a56c47ba | ||
|
|
c82c21c0cd | ||
|
|
a339e722a6 |
@@ -35,6 +35,7 @@
|
|||||||
class="text-xs text-base-content/50"
|
class="text-xs text-base-content/50"
|
||||||
>
|
>
|
||||||
{{ constructeur.name }}
|
{{ constructeur.name }}
|
||||||
|
<span v-if="supplierReferenceMap.get(constructeur.id)" class="opacity-70">({{ supplierReferenceMap.get(constructeur.id) }})</span>
|
||||||
</span>
|
</span>
|
||||||
<span v-if="displayProductName" class="badge badge-info badge-xs">
|
<span v-if="displayProductName" class="badge badge-info badge-xs">
|
||||||
{{ displayProductName }}
|
{{ displayProductName }}
|
||||||
@@ -102,6 +103,9 @@
|
|||||||
class="text-base-content"
|
class="text-base-content"
|
||||||
>
|
>
|
||||||
{{ constructeur.name }}
|
{{ constructeur.name }}
|
||||||
|
<span v-if="supplierReferenceMap.get(constructeur.id)" class="text-sm text-base-content/60">
|
||||||
|
— Réf. {{ supplierReferenceMap.get(constructeur.id) }}
|
||||||
|
</span>
|
||||||
<span v-if="formatConstructeurContact(constructeur)" class="text-xs text-base-content/50 block">
|
<span v-if="formatConstructeurContact(constructeur)" class="text-xs text-base-content/50 block">
|
||||||
{{ formatConstructeurContact(constructeur) }}
|
{{ formatConstructeurContact(constructeur) }}
|
||||||
</span>
|
</span>
|
||||||
@@ -292,6 +296,7 @@ import {
|
|||||||
formatConstructeurContact as formatConstructeurContactSummary,
|
formatConstructeurContact as formatConstructeurContactSummary,
|
||||||
resolveConstructeurs,
|
resolveConstructeurs,
|
||||||
uniqueConstructeurIds,
|
uniqueConstructeurIds,
|
||||||
|
parseConstructeurLinksFromApi,
|
||||||
} from '~/shared/constructeurUtils'
|
} from '~/shared/constructeurUtils'
|
||||||
import {
|
import {
|
||||||
formatSize,
|
formatSize,
|
||||||
@@ -391,23 +396,36 @@ const structurePieces = computed(() => allPieces.value.filter((p) => p._structur
|
|||||||
// --- Constructeurs ---
|
// --- Constructeurs ---
|
||||||
const { constructeurs } = useConstructeurs()
|
const { constructeurs } = useConstructeurs()
|
||||||
|
|
||||||
const componentConstructeurIds = computed(() =>
|
const componentConstructeurLinks = computed(() =>
|
||||||
uniqueConstructeurIds(
|
parseConstructeurLinksFromApi(
|
||||||
props.component,
|
|
||||||
Array.isArray(props.component.constructeurs) ? props.component.constructeurs : [],
|
Array.isArray(props.component.constructeurs) ? props.component.constructeurs : [],
|
||||||
props.component.constructeur ? [props.component.constructeur] : [],
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const componentConstructeursDisplay = computed(() =>
|
const supplierReferenceMap = computed(() => {
|
||||||
resolveConstructeurs(
|
const map = new Map()
|
||||||
componentConstructeurIds.value,
|
componentConstructeurLinks.value.forEach(l => {
|
||||||
Array.isArray(props.component.constructeurs) ? props.component.constructeurs : [],
|
if (l.supplierReference) map.set(l.constructeurId, l.supplierReference)
|
||||||
props.component.constructeur ? [props.component.constructeur] : [],
|
})
|
||||||
constructeurs.value,
|
return map
|
||||||
),
|
})
|
||||||
|
|
||||||
|
const componentConstructeurIds = computed(() =>
|
||||||
|
componentConstructeurLinks.value.map(l => l.constructeurId).filter(Boolean),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const componentConstructeursDisplay = computed(() => {
|
||||||
|
// Extract nested constructeur objects from link entries
|
||||||
|
const linkConstructeurs = componentConstructeurLinks.value
|
||||||
|
.filter(l => l.constructeur && l.constructeur.id)
|
||||||
|
.map(l => l.constructeur)
|
||||||
|
return resolveConstructeurs(
|
||||||
|
componentConstructeurIds.value,
|
||||||
|
linkConstructeurs,
|
||||||
|
constructeurs.value,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
const formatConstructeurContact = (constructeur) =>
|
const formatConstructeurContact = (constructeur) =>
|
||||||
formatConstructeurContactSummary(constructeur)
|
formatConstructeurContactSummary(constructeur)
|
||||||
|
|
||||||
|
|||||||
93
app/components/ConstructeurLinksTable.vue
Normal file
93
app/components/ConstructeurLinksTable.vue
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="modelValue.length" class="overflow-x-auto">
|
||||||
|
<table class="table table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Fournisseur</th>
|
||||||
|
<th>Réf. fournisseur</th>
|
||||||
|
<th v-if="!readonly" class="w-10" />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(link, index) in modelValue" :key="link.constructeurId">
|
||||||
|
<td class="font-medium">
|
||||||
|
{{ getConstructeurName(link) }}
|
||||||
|
<div v-if="getConstructeurContact(link)" class="text-xs text-gray-500">
|
||||||
|
{{ getConstructeurContact(link) }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
v-if="!readonly"
|
||||||
|
:value="link.supplierReference || ''"
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered input-sm w-full"
|
||||||
|
placeholder="Réf. fournisseur"
|
||||||
|
@input="updateReference(index, ($event.target as HTMLInputElement).value)"
|
||||||
|
>
|
||||||
|
<span v-else>{{ link.supplierReference || '—' }}</span>
|
||||||
|
</td>
|
||||||
|
<td v-if="!readonly">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-ghost btn-xs text-error"
|
||||||
|
aria-label="Retirer"
|
||||||
|
@click="removeLink(index)"
|
||||||
|
>
|
||||||
|
<IconLucideX class="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { PropType } from 'vue'
|
||||||
|
import type { ConstructeurLinkEntry } from '~/shared/constructeurUtils'
|
||||||
|
import { formatConstructeurContact } from '~/shared/constructeurUtils'
|
||||||
|
import { useConstructeurs } from '~/composables/useConstructeurs'
|
||||||
|
import IconLucideX from '~icons/lucide/x'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: Array as PropType<ConstructeurLinkEntry[]>,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
readonly: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', value: ConstructeurLinkEntry[]): void
|
||||||
|
(e: 'remove', constructeurId: string): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { getConstructeurById } = useConstructeurs()
|
||||||
|
|
||||||
|
const getConstructeurName = (link: ConstructeurLinkEntry): string =>
|
||||||
|
link.constructeur?.name || getConstructeurById(link.constructeurId)?.name || link.constructeurId
|
||||||
|
|
||||||
|
const getConstructeurContact = (link: ConstructeurLinkEntry): string => {
|
||||||
|
const c = link.constructeur || getConstructeurById(link.constructeurId)
|
||||||
|
return formatConstructeurContact(c as any)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateReference = (index: number, value: string) => {
|
||||||
|
const updated = [...props.modelValue]
|
||||||
|
const entry = updated[index]
|
||||||
|
if (!entry) return
|
||||||
|
updated[index] = { ...entry, supplierReference: value || null }
|
||||||
|
emit('update:modelValue', updated)
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeLink = (index: number) => {
|
||||||
|
const removed = props.modelValue[index]
|
||||||
|
const updated = props.modelValue.filter((_, i) => i !== index)
|
||||||
|
emit('update:modelValue', updated)
|
||||||
|
if (removed) emit('remove', removed.constructeurId)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -15,9 +15,9 @@
|
|||||||
<IconLucideEye v-else class="w-5 h-5 mr-2" aria-hidden="true" />
|
<IconLucideEye v-else class="w-5 h-5 mr-2" aria-hidden="true" />
|
||||||
{{ isEditMode ? 'Voir détails' : 'Modifier' }}
|
{{ isEditMode ? 'Voir détails' : 'Modifier' }}
|
||||||
</button>
|
</button>
|
||||||
<NuxtLink :to="backLink" class="btn btn-ghost btn-sm md:btn-md">
|
<button type="button" class="btn btn-ghost btn-sm md:btn-md" @click="goBack">
|
||||||
Retour au catalogue
|
Retour au catalogue
|
||||||
</NuxtLink>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -26,7 +26,9 @@
|
|||||||
import IconLucideSquarePen from '~icons/lucide/square-pen'
|
import IconLucideSquarePen from '~icons/lucide/square-pen'
|
||||||
import IconLucideEye from '~icons/lucide/eye'
|
import IconLucideEye from '~icons/lucide/eye'
|
||||||
|
|
||||||
defineProps<{
|
const router = useRouter()
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
title: string
|
title: string
|
||||||
subtitle?: string
|
subtitle?: string
|
||||||
isEditMode: boolean
|
isEditMode: boolean
|
||||||
@@ -37,4 +39,13 @@ defineProps<{
|
|||||||
defineEmits<{
|
defineEmits<{
|
||||||
'toggle-edit': []
|
'toggle-edit': []
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
function goBack() {
|
||||||
|
if (window.history.length > 1) {
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
navigateTo(props.backLink)
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -42,6 +42,7 @@
|
|||||||
Rattachée à {{ piece.parentComponentName }}
|
Rattachée à {{ piece.parentComponentName }}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="pieceData.reference" class="badge badge-outline badge-sm">{{ pieceData.reference }}</span>
|
<span v-if="pieceData.reference" class="badge badge-outline badge-sm">{{ pieceData.reference }}</span>
|
||||||
|
<span v-if="pieceData.referenceAuto" class="badge badge-secondary badge-sm" title="Référence auto">{{ pieceData.referenceAuto }}</span>
|
||||||
<template v-if="pieceConstructeursDisplay.length">
|
<template v-if="pieceConstructeursDisplay.length">
|
||||||
<span
|
<span
|
||||||
v-for="constructeur in pieceConstructeursDisplay"
|
v-for="constructeur in pieceConstructeursDisplay"
|
||||||
@@ -49,6 +50,9 @@
|
|||||||
class="badge badge-outline badge-sm"
|
class="badge badge-outline badge-sm"
|
||||||
>
|
>
|
||||||
{{ constructeur.name }}
|
{{ constructeur.name }}
|
||||||
|
<span v-if="supplierReferenceMap.get(constructeur.id)" class="text-xs opacity-60 ml-0.5">
|
||||||
|
({{ supplierReferenceMap.get(constructeur.id) }})
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<span v-if="pieceData.prix" class="badge badge-primary badge-sm">{{ pieceData.prix }}€</span>
|
<span v-if="pieceData.prix" class="badge badge-primary badge-sm">{{ pieceData.prix }}€</span>
|
||||||
@@ -106,6 +110,10 @@
|
|||||||
pieceData.reference || "Non définie"
|
pieceData.reference || "Non définie"
|
||||||
}}</span>
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="pieceData.referenceAuto">
|
||||||
|
<span class="font-medium">Référence auto:</span>
|
||||||
|
<span class="ml-2">{{ pieceData.referenceAuto }}</span>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span class="font-medium">Fournisseur:</span>
|
<span class="font-medium">Fournisseur:</span>
|
||||||
<div v-if="!isEditMode" class="ml-2">
|
<div v-if="!isEditMode" class="ml-2">
|
||||||
@@ -117,6 +125,9 @@
|
|||||||
>
|
>
|
||||||
<span class="font-medium">
|
<span class="font-medium">
|
||||||
{{ constructeur.name }}
|
{{ constructeur.name }}
|
||||||
|
<span v-if="supplierReferenceMap.get(constructeur.id)" class="text-sm font-normal text-base-content/60">
|
||||||
|
— Réf. {{ supplierReferenceMap.get(constructeur.id) }}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-if="formatConstructeurContact(constructeur)"
|
v-if="formatConstructeurContact(constructeur)"
|
||||||
@@ -278,6 +289,7 @@ import {
|
|||||||
formatConstructeurContact as formatConstructeurContactSummary,
|
formatConstructeurContact as formatConstructeurContactSummary,
|
||||||
resolveConstructeurs,
|
resolveConstructeurs,
|
||||||
uniqueConstructeurIds,
|
uniqueConstructeurIds,
|
||||||
|
parseConstructeurLinksFromApi,
|
||||||
} from '~/shared/constructeurUtils'
|
} from '~/shared/constructeurUtils'
|
||||||
import {
|
import {
|
||||||
resolveFieldId,
|
resolveFieldId,
|
||||||
@@ -301,6 +313,7 @@ const emit = defineEmits(['update', 'edit', 'custom-field-update', 'delete'])
|
|||||||
const pieceData = reactive({
|
const pieceData = reactive({
|
||||||
name: props.piece.name || '',
|
name: props.piece.name || '',
|
||||||
reference: props.piece.reference || '',
|
reference: props.piece.reference || '',
|
||||||
|
referenceAuto: props.piece.referenceAuto || null,
|
||||||
prix: props.piece.prix || '',
|
prix: props.piece.prix || '',
|
||||||
productId: props.piece.product?.id || props.piece.productId || null,
|
productId: props.piece.product?.id || props.piece.productId || null,
|
||||||
quantity: props.piece.quantity ?? 1,
|
quantity: props.piece.quantity ?? 1,
|
||||||
@@ -387,23 +400,36 @@ const toggleCollapse = () => {
|
|||||||
// --- Constructeurs ---
|
// --- Constructeurs ---
|
||||||
const { constructeurs } = useConstructeurs()
|
const { constructeurs } = useConstructeurs()
|
||||||
|
|
||||||
const pieceConstructeurIds = computed(() =>
|
const pieceConstructeurLinks = computed(() =>
|
||||||
uniqueConstructeurIds(
|
parseConstructeurLinksFromApi(
|
||||||
props.piece,
|
|
||||||
Array.isArray(props.piece.constructeurs) ? props.piece.constructeurs : [],
|
Array.isArray(props.piece.constructeurs) ? props.piece.constructeurs : [],
|
||||||
props.piece.constructeur ? [props.piece.constructeur] : [],
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const pieceConstructeursDisplay = computed(() =>
|
const supplierReferenceMap = computed(() => {
|
||||||
resolveConstructeurs(
|
const map = new Map()
|
||||||
pieceConstructeurIds.value,
|
pieceConstructeurLinks.value.forEach(l => {
|
||||||
Array.isArray(props.piece.constructeurs) ? props.piece.constructeurs : [],
|
if (l.supplierReference) map.set(l.constructeurId, l.supplierReference)
|
||||||
props.piece.constructeur ? [props.piece.constructeur] : [],
|
})
|
||||||
constructeurs.value,
|
return map
|
||||||
),
|
})
|
||||||
|
|
||||||
|
const pieceConstructeurIds = computed(() =>
|
||||||
|
pieceConstructeurLinks.value.map(l => l.constructeurId).filter(Boolean),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const pieceConstructeursDisplay = computed(() => {
|
||||||
|
// Extract nested constructeur objects from link entries
|
||||||
|
const linkConstructeurs = pieceConstructeurLinks.value
|
||||||
|
.filter(l => l.constructeur && l.constructeur.id)
|
||||||
|
.map(l => l.constructeur)
|
||||||
|
return resolveConstructeurs(
|
||||||
|
pieceConstructeurIds.value,
|
||||||
|
linkConstructeurs,
|
||||||
|
constructeurs.value,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
const formatConstructeurContact = (constructeur) =>
|
const formatConstructeurContact = (constructeur) =>
|
||||||
formatConstructeurContactSummary(constructeur)
|
formatConstructeurContactSummary(constructeur)
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,9 @@
|
|||||||
<IconLucidePrinter class="w-5 h-5 mr-2" aria-hidden="true" />
|
<IconLucidePrinter class="w-5 h-5 mr-2" aria-hidden="true" />
|
||||||
Imprimer
|
Imprimer
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" class="btn btn-ghost btn-sm md:btn-md" @click="goBack">
|
||||||
|
Retour aux machines
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -41,6 +44,8 @@ import IconLucideSquarePen from '~icons/lucide/square-pen'
|
|||||||
import IconLucideEye from '~icons/lucide/eye'
|
import IconLucideEye from '~icons/lucide/eye'
|
||||||
import IconLucidePrinter from '~icons/lucide/printer'
|
import IconLucidePrinter from '~icons/lucide/printer'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
title: string
|
title: string
|
||||||
isEditMode: boolean
|
isEditMode: boolean
|
||||||
@@ -50,4 +55,13 @@ defineEmits<{
|
|||||||
'toggle-edit': []
|
'toggle-edit': []
|
||||||
'open-print': []
|
'open-print': []
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
function goBack() {
|
||||||
|
if (window.history.length > 1) {
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
navigateTo('/machines')
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
{{ machineReference }}
|
{{ machineReference }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="isEditMode || hasMachineConstructeur" class="form-control">
|
<div v-if="isEditMode || hasMachineConstructeur" class="form-control md:col-span-2">
|
||||||
<label class="label">
|
<label class="label">
|
||||||
<span class="label-text">Fournisseur</span>
|
<span class="label-text">Fournisseur</span>
|
||||||
</label>
|
</label>
|
||||||
@@ -70,23 +70,15 @@
|
|||||||
placeholder="Rechercher un ou plusieurs fournisseurs..."
|
placeholder="Rechercher un ou plusieurs fournisseurs..."
|
||||||
@update:modelValue="$emit('update:constructeur-ids', $event)"
|
@update:modelValue="$emit('update:constructeur-ids', $event)"
|
||||||
/>
|
/>
|
||||||
<div v-else class="border border-base-300 rounded-btn bg-base-200 px-4 py-2 min-h-12 flex items-center">
|
<ConstructeurLinksTable
|
||||||
<div v-if="machineConstructeursDisplay.length" class="flex flex-wrap gap-2">
|
v-if="constructeurLinks.length"
|
||||||
<span
|
:model-value="constructeurLinks"
|
||||||
v-for="constructeur in machineConstructeursDisplay"
|
:readonly="!isEditMode"
|
||||||
:key="constructeur.id"
|
@update:model-value="$emit('update:constructeur-links', $event)"
|
||||||
class="badge badge-ghost gap-1"
|
@remove="$emit('remove-constructeur-link', $event)"
|
||||||
>
|
/>
|
||||||
{{ constructeur.name }}
|
<div v-else-if="!isEditMode" class="border border-base-300 rounded-btn bg-base-200 px-4 py-2 min-h-12 flex items-center">
|
||||||
<span
|
<span class="text-base-content/50">Non défini</span>
|
||||||
v-if="formatConstructeurContactSummary(constructeur)"
|
|
||||||
class="text-xs opacity-60"
|
|
||||||
>
|
|
||||||
· {{ formatConstructeurContactSummary(constructeur) }}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<span v-else class="text-base-content/50">Non défini</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -188,12 +180,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { watch } from 'vue'
|
import { watch } from 'vue'
|
||||||
import ConstructeurSelect from '~/components/ConstructeurSelect.vue'
|
import ConstructeurSelect from '~/components/ConstructeurSelect.vue'
|
||||||
|
import ConstructeurLinksTable from '~/components/ConstructeurLinksTable.vue'
|
||||||
import MachineCustomFieldDefEditor from '~/components/machine/MachineCustomFieldDefEditor.vue'
|
import MachineCustomFieldDefEditor from '~/components/machine/MachineCustomFieldDefEditor.vue'
|
||||||
import {
|
|
||||||
formatConstructeurContact as formatConstructeurContactSummary,
|
|
||||||
} from '~/shared/constructeurUtils'
|
|
||||||
import { formatCustomFieldValue } from '~/shared/utils/customFieldUtils'
|
import { formatCustomFieldValue } from '~/shared/utils/customFieldUtils'
|
||||||
import { useMachineCustomFieldDefs } from '~/composables/useMachineCustomFieldDefs'
|
import { useMachineCustomFieldDefs } from '~/composables/useMachineCustomFieldDefs'
|
||||||
|
import type { ConstructeurLinkEntry } from '~/shared/constructeurUtils'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
isEditMode: boolean
|
isEditMode: boolean
|
||||||
@@ -205,6 +196,7 @@ const props = defineProps<{
|
|||||||
machineConstructeurIds: string[]
|
machineConstructeurIds: string[]
|
||||||
machineConstructeursDisplay: any[]
|
machineConstructeursDisplay: any[]
|
||||||
hasMachineConstructeur: boolean
|
hasMachineConstructeur: boolean
|
||||||
|
constructeurLinks: ConstructeurLinkEntry[]
|
||||||
visibleCustomFields: any[]
|
visibleCustomFields: any[]
|
||||||
getMachineFieldId: (fieldName: string) => string
|
getMachineFieldId: (fieldName: string) => string
|
||||||
machineId: string
|
machineId: string
|
||||||
@@ -216,6 +208,8 @@ const emit = defineEmits<{
|
|||||||
'update:machine-reference': [value: string]
|
'update:machine-reference': [value: string]
|
||||||
'update:machine-site-id': [value: string]
|
'update:machine-site-id': [value: string]
|
||||||
'update:constructeur-ids': [ids: unknown]
|
'update:constructeur-ids': [ids: unknown]
|
||||||
|
'update:constructeur-links': [links: ConstructeurLinkEntry[]]
|
||||||
|
'remove-constructeur-link': [constructeurId: string]
|
||||||
'set-custom-field-value': [field: any, value: unknown]
|
'set-custom-field-value': [field: any, value: unknown]
|
||||||
'custom-fields-saved': []
|
'custom-fields-saved': []
|
||||||
}>()
|
}>()
|
||||||
|
|||||||
@@ -108,6 +108,13 @@
|
|||||||
</template>
|
</template>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<ReferenceFormulaBuilder
|
||||||
|
v-if="form.category === 'PIECE' || form.category === 'COMPONENT'"
|
||||||
|
v-model="form.referenceFormula"
|
||||||
|
:custom-fields="formulaBuilderCustomFields"
|
||||||
|
:disabled="isReadonly"
|
||||||
|
/>
|
||||||
|
|
||||||
<footer class="flex flex-col gap-3 border-t border-base-300 pt-4 sm:flex-row sm:justify-end">
|
<footer class="flex flex-col gap-3 border-t border-base-300 pt-4 sm:flex-row sm:justify-end">
|
||||||
<button type="button" class="btn btn-ghost" :disabled="saving" @click="emit('cancel')">
|
<button type="button" class="btn btn-ghost" :disabled="saving" @click="emit('cancel')">
|
||||||
Annuler
|
Annuler
|
||||||
@@ -177,14 +184,33 @@ const componentSubcomponentMaxDepth = computed(() =>
|
|||||||
)
|
)
|
||||||
const isReadonly = computed(() => props.readonly === true)
|
const isReadonly = computed(() => props.readonly === true)
|
||||||
|
|
||||||
const form = reactive<ModelTypePayload>({
|
const form = reactive<ModelTypePayload & { referenceFormula?: string | null }>({
|
||||||
name: '',
|
name: '',
|
||||||
code: '',
|
code: '',
|
||||||
category: props.initialCategory,
|
category: props.initialCategory,
|
||||||
notes: '',
|
notes: '',
|
||||||
structure: undefined,
|
structure: undefined,
|
||||||
|
referenceFormula: null,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const formulaBuilderCustomFields = computed(() => {
|
||||||
|
if (form.category === 'PIECE') {
|
||||||
|
const fields = pieceStructure.value?.customFields
|
||||||
|
return Array.isArray(fields) ? fields : []
|
||||||
|
}
|
||||||
|
if (form.category === 'COMPONENT') {
|
||||||
|
const fields = componentStructure.value?.customFields
|
||||||
|
return Array.isArray(fields) ? fields : []
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
})
|
||||||
|
|
||||||
|
const extractFormulaFields = (formula: string | null | undefined): string[] => {
|
||||||
|
if (!formula) return []
|
||||||
|
const matches = [...formula.matchAll(/\{(\w+)\}/g)]
|
||||||
|
return [...new Set(matches.map(m => m[1]).filter((n): n is string => n !== undefined))]
|
||||||
|
}
|
||||||
|
|
||||||
const errors = reactive<{ name?: string }>({})
|
const errors = reactive<{ name?: string }>({})
|
||||||
const nameInput = ref<HTMLInputElement | null>(null)
|
const nameInput = ref<HTMLInputElement | null>(null)
|
||||||
|
|
||||||
@@ -248,6 +274,9 @@ const resetForm = () => {
|
|||||||
|
|
||||||
errors.name = undefined
|
errors.name = undefined
|
||||||
|
|
||||||
|
const incomingAny = incoming as Record<string, unknown>
|
||||||
|
form.referenceFormula = typeof incomingAny.referenceFormula === 'string' ? incomingAny.referenceFormula : null
|
||||||
|
|
||||||
resetStructures(incoming.structure, form.category)
|
resetStructures(incoming.structure, form.category)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,20 +315,28 @@ const handleSubmit = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (form.category === 'COMPONENT') {
|
if (form.category === 'COMPONENT') {
|
||||||
|
const formula = form.referenceFormula || null
|
||||||
|
const requiredFields = extractFormulaFields(formula)
|
||||||
emit('submit', {
|
emit('submit', {
|
||||||
...common,
|
...common,
|
||||||
category: 'COMPONENT',
|
category: 'COMPONENT',
|
||||||
structure: normalizeStructureForSave(cloneStructure(componentStructure.value)),
|
structure: normalizeStructureForSave(cloneStructure(componentStructure.value)),
|
||||||
})
|
referenceFormula: formula,
|
||||||
|
requiredFieldsForReference: requiredFields.length ? requiredFields : null,
|
||||||
|
} as ModelTypePayload)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (form.category === 'PIECE') {
|
if (form.category === 'PIECE') {
|
||||||
|
const formula = form.referenceFormula || null
|
||||||
|
const requiredFields = extractFormulaFields(formula)
|
||||||
emit('submit', {
|
emit('submit', {
|
||||||
...common,
|
...common,
|
||||||
category: 'PIECE',
|
category: 'PIECE',
|
||||||
structure: normalizePieceStructureForSave(clonePieceStructure(pieceStructure.value)),
|
structure: normalizePieceStructureForSave(clonePieceStructure(pieceStructure.value)),
|
||||||
})
|
referenceFormula: formula,
|
||||||
|
requiredFieldsForReference: requiredFields.length ? requiredFields : null,
|
||||||
|
} as ModelTypePayload)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
115
app/components/model-types/ReferenceFormulaBuilder.vue
Normal file
115
app/components/model-types/ReferenceFormulaBuilder.vue
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
<template>
|
||||||
|
<section class="space-y-4">
|
||||||
|
<header>
|
||||||
|
<h3 class="text-lg font-semibold text-base-content">Génération de référence automatique</h3>
|
||||||
|
<p class="mt-1 text-sm text-base-content/70">
|
||||||
|
Cliquez sur un champ pour l'insérer dans la formule. Vous pouvez aussi taper du texte libre (séparateurs, préfixes…).
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="rounded-lg border border-base-300 p-4 space-y-4">
|
||||||
|
<div v-if="fieldNames.length" class="flex flex-wrap gap-2">
|
||||||
|
<button
|
||||||
|
v-for="name in fieldNames"
|
||||||
|
:key="name"
|
||||||
|
type="button"
|
||||||
|
class="btn btn-xs btn-outline btn-primary font-mono"
|
||||||
|
:disabled="disabled"
|
||||||
|
@click="insertField(name)"
|
||||||
|
>
|
||||||
|
{{ name }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p v-else class="text-sm text-base-content/50 italic">
|
||||||
|
Aucun champ personnalisé défini dans la structure.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="label" for="reference-formula">
|
||||||
|
<span class="label-text">Formule</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="reference-formula"
|
||||||
|
ref="inputRef"
|
||||||
|
:value="modelValue"
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered w-full font-mono"
|
||||||
|
placeholder="Ex: SNU {serie}-{diametre}/{type}"
|
||||||
|
:disabled="disabled"
|
||||||
|
@input="emit('update:modelValue', ($event.target as HTMLInputElement).value || null)"
|
||||||
|
/>
|
||||||
|
<p class="mt-1 text-xs text-base-content/60">
|
||||||
|
Laissez vide si ce type n'utilise pas de référence automatique.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="modelValue" class="rounded bg-base-200 px-3 py-2 text-sm">
|
||||||
|
<span class="text-base-content/70">Aperçu :</span>
|
||||||
|
<span class="ml-1 font-mono font-semibold">{{ preview }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, nextTick, ref } from 'vue'
|
||||||
|
|
||||||
|
interface CustomField {
|
||||||
|
name: string
|
||||||
|
type: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: string | null | undefined
|
||||||
|
customFields: CustomField[]
|
||||||
|
disabled?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', value: string | null): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const inputRef = ref<HTMLInputElement | null>(null)
|
||||||
|
|
||||||
|
const fieldNames = computed(() =>
|
||||||
|
props.customFields.map(f => f.name).filter((n): n is string => Boolean(n)),
|
||||||
|
)
|
||||||
|
|
||||||
|
const previewExamples: Record<string, string> = {
|
||||||
|
text: 'VALEUR',
|
||||||
|
number: '123',
|
||||||
|
select: 'OPTION',
|
||||||
|
boolean: 'OUI',
|
||||||
|
date: '2026-01-01',
|
||||||
|
}
|
||||||
|
|
||||||
|
const preview = computed(() => {
|
||||||
|
if (!props.modelValue) return ''
|
||||||
|
const fieldMap = new Map<string, string>()
|
||||||
|
for (const f of props.customFields) {
|
||||||
|
if (f.name) {
|
||||||
|
fieldMap.set(f.name, previewExamples[f.type] ?? 'VALEUR')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return props.modelValue.replace(/\{(\w+)\}/g, (_, name) => fieldMap.get(name) ?? '???')
|
||||||
|
})
|
||||||
|
|
||||||
|
const insertField = (fieldName: string) => {
|
||||||
|
const placeholder = `{${fieldName}}`
|
||||||
|
const input = inputRef.value
|
||||||
|
const current = props.modelValue ?? ''
|
||||||
|
if (!input) {
|
||||||
|
emit('update:modelValue', current + placeholder)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const start = input.selectionStart ?? current.length
|
||||||
|
const end = input.selectionEnd ?? start
|
||||||
|
const updated = current.slice(0, start) + placeholder + current.slice(end)
|
||||||
|
emit('update:modelValue', updated)
|
||||||
|
nextTick(() => {
|
||||||
|
const newPos = start + placeholder.length
|
||||||
|
input.focus()
|
||||||
|
input.setSelectionRange(newPos, newPos)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -26,7 +26,9 @@ import {
|
|||||||
requiredCustomFieldsFilled as _requiredCustomFieldsFilled,
|
requiredCustomFieldsFilled as _requiredCustomFieldsFilled,
|
||||||
saveCustomFieldValues as _saveCustomFieldValues,
|
saveCustomFieldValues as _saveCustomFieldValues,
|
||||||
} from '~/shared/utils/customFieldFormUtils'
|
} from '~/shared/utils/customFieldFormUtils'
|
||||||
import { uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
import { useConstructeurLinks } from '~/composables/useConstructeurLinks'
|
||||||
|
import { uniqueConstructeurIds, constructeurIdsFromLinks } from '~/shared/constructeurUtils'
|
||||||
|
import type { ConstructeurLinkEntry } from '~/shared/constructeurUtils'
|
||||||
import {
|
import {
|
||||||
getStructurePieces,
|
getStructurePieces,
|
||||||
resolvePieceLabel as _resolvePieceLabel,
|
resolvePieceLabel as _resolvePieceLabel,
|
||||||
@@ -77,6 +79,7 @@ export function useComponentCreate() {
|
|||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const { upsertCustomFieldValue, updateCustomFieldValue } = useCustomFields()
|
const { upsertCustomFieldValue, updateCustomFieldValue } = useCustomFields()
|
||||||
const { uploadDocuments } = useDocuments()
|
const { uploadDocuments } = useDocuments()
|
||||||
|
const { syncLinks } = useConstructeurLinks()
|
||||||
const { canEdit } = usePermissions()
|
const { canEdit } = usePermissions()
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
@@ -92,6 +95,8 @@ export function useComponentCreate() {
|
|||||||
constructeurIds: [] as string[],
|
constructeurIds: [] as string[],
|
||||||
prix: '' as string,
|
prix: '' as string,
|
||||||
})
|
})
|
||||||
|
const constructeurLinks = ref<ConstructeurLinkEntry[]>([])
|
||||||
|
const constructeurIdsFromForm = computed(() => constructeurIdsFromLinks(constructeurLinks.value))
|
||||||
const lastSuggestedName = ref('')
|
const lastSuggestedName = ref('')
|
||||||
const customFieldInputs = ref<CustomFieldInput[]>([])
|
const customFieldInputs = ref<CustomFieldInput[]>([])
|
||||||
const structureAssignments = ref<StructureAssignmentNode | null>(null)
|
const structureAssignments = ref<StructureAssignmentNode | null>(null)
|
||||||
@@ -276,9 +281,7 @@ export function useComponentCreate() {
|
|||||||
payload.reference = reference
|
payload.reference = reference
|
||||||
}
|
}
|
||||||
|
|
||||||
if (creationForm.constructeurIds.length) {
|
// constructeurIds are handled via link entities, not in the main payload
|
||||||
payload.constructeurIds = uniqueConstructeurIds(creationForm.constructeurIds)
|
|
||||||
}
|
|
||||||
|
|
||||||
const rawPrice = typeof creationForm.prix === 'string'
|
const rawPrice = typeof creationForm.prix === 'string'
|
||||||
? creationForm.prix.trim()
|
? creationForm.prix.trim()
|
||||||
@@ -343,6 +346,10 @@ export function useComponentCreate() {
|
|||||||
}
|
}
|
||||||
selectedDocuments.value = []
|
selectedDocuments.value = []
|
||||||
}
|
}
|
||||||
|
// Sync constructeur links after creation
|
||||||
|
if (constructeurLinks.value.length) {
|
||||||
|
await syncLinks('composant', createdComponent.id, [], constructeurLinks.value)
|
||||||
|
}
|
||||||
toast.showSuccess('Composant créé avec succès')
|
toast.showSuccess('Composant créé avec succès')
|
||||||
await router.replace(`/component/${createdComponent.id}?edit=true`)
|
await router.replace(`/component/${createdComponent.id}?edit=true`)
|
||||||
}
|
}
|
||||||
@@ -380,6 +387,8 @@ export function useComponentCreate() {
|
|||||||
selectedTypeId,
|
selectedTypeId,
|
||||||
submitting,
|
submitting,
|
||||||
creationForm,
|
creationForm,
|
||||||
|
constructeurLinks,
|
||||||
|
constructeurIdsFromForm,
|
||||||
customFieldInputs,
|
customFieldInputs,
|
||||||
structureAssignments,
|
structureAssignments,
|
||||||
selectedDocuments,
|
selectedDocuments,
|
||||||
|
|||||||
@@ -12,9 +12,11 @@ import { useToast } from '~/composables/useToast'
|
|||||||
import { extractRelationId } from '~/shared/apiRelations'
|
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 { useConstructeurLinks } from '~/composables/useConstructeurLinks'
|
||||||
import { useComponentHistory } from '~/composables/useComponentHistory'
|
import { useComponentHistory } from '~/composables/useComponentHistory'
|
||||||
import { formatStructurePreview, normalizeStructureForEditor } from '~/shared/modelUtils'
|
import { formatStructurePreview, normalizeStructureForEditor } from '~/shared/modelUtils'
|
||||||
import { uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
import { uniqueConstructeurIds, constructeurIdsFromLinks } from '~/shared/constructeurUtils'
|
||||||
|
import type { ConstructeurLinkEntry } from '~/shared/constructeurUtils'
|
||||||
import {
|
import {
|
||||||
getStructurePieces,
|
getStructurePieces,
|
||||||
getStructureProducts,
|
getStructureProducts,
|
||||||
@@ -61,6 +63,7 @@ export function useComponentEdit(componentId: string) {
|
|||||||
const { pieces } = usePieces()
|
const { pieces } = usePieces()
|
||||||
const { products } = useProducts()
|
const { products } = useProducts()
|
||||||
const { ensureConstructeurs } = useConstructeurs()
|
const { ensureConstructeurs } = useConstructeurs()
|
||||||
|
const { fetchLinks, syncLinks } = useConstructeurLinks()
|
||||||
const { upsertCustomFieldValue, updateCustomFieldValue } = useCustomFields()
|
const { upsertCustomFieldValue, updateCustomFieldValue } = useCustomFields()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const { loadDocumentsByComponent, uploadDocuments, deleteDocument } = useDocuments()
|
const { loadDocumentsByComponent, uploadDocuments, deleteDocument } = useDocuments()
|
||||||
@@ -89,6 +92,9 @@ export function useComponentEdit(componentId: string) {
|
|||||||
constructeurIds: [] as string[],
|
constructeurIds: [] as string[],
|
||||||
prix: '' as string,
|
prix: '' as string,
|
||||||
})
|
})
|
||||||
|
const constructeurLinks = ref<ConstructeurLinkEntry[]>([])
|
||||||
|
const originalConstructeurLinks = ref<ConstructeurLinkEntry[]>([])
|
||||||
|
const constructeurIdsFromForm = computed(() => constructeurIdsFromLinks(constructeurLinks.value))
|
||||||
|
|
||||||
const customFieldInputs = ref<CustomFieldInput[]>([])
|
const customFieldInputs = ref<CustomFieldInput[]>([])
|
||||||
const fetchedPieceTypeMap = ref<Record<string, string>>({})
|
const fetchedPieceTypeMap = ref<Record<string, string>>({})
|
||||||
@@ -286,6 +292,7 @@ export function useComponentEdit(componentId: string) {
|
|||||||
slotId: slot.slotId,
|
slotId: slot.slotId,
|
||||||
typePieceId: slot.typePieceId,
|
typePieceId: slot.typePieceId,
|
||||||
selectedPieceId: edits && 'selectedPieceId' in edits ? edits.selectedPieceId : (slot.selectedPieceId ?? null),
|
selectedPieceId: edits && 'selectedPieceId' in edits ? edits.selectedPieceId : (slot.selectedPieceId ?? null),
|
||||||
|
selectedPieceName: slot.selectedPieceName ?? null,
|
||||||
quantity: edits && 'quantity' in edits ? edits.quantity! : (slot.quantity ?? 1),
|
quantity: edits && 'quantity' in edits ? edits.quantity! : (slot.quantity ?? 1),
|
||||||
position: slot.position ?? i,
|
position: slot.position ?? i,
|
||||||
label: pieceTypeLabelMap.value[slot.typePieceId] || `Pièce #${i + 1}`,
|
label: pieceTypeLabelMap.value[slot.typePieceId] || `Pièce #${i + 1}`,
|
||||||
@@ -302,6 +309,7 @@ export function useComponentEdit(componentId: string) {
|
|||||||
slotId: slot.slotId,
|
slotId: slot.slotId,
|
||||||
typeProductId: slot.typeProductId,
|
typeProductId: slot.typeProductId,
|
||||||
selectedProductId: edits && 'selectedProductId' in edits ? edits.selectedProductId : (slot.selectedProductId ?? null),
|
selectedProductId: edits && 'selectedProductId' in edits ? edits.selectedProductId : (slot.selectedProductId ?? null),
|
||||||
|
selectedProductName: slot.selectedProductName ?? null,
|
||||||
familyCode: slot.familyCode,
|
familyCode: slot.familyCode,
|
||||||
position: slot.position ?? i,
|
position: slot.position ?? i,
|
||||||
label: productTypeLabelMap.value[slot.typeProductId] || `Produit #${i + 1}`,
|
label: productTypeLabelMap.value[slot.typeProductId] || `Produit #${i + 1}`,
|
||||||
@@ -318,6 +326,7 @@ export function useComponentEdit(componentId: string) {
|
|||||||
slotId: slot.slotId,
|
slotId: slot.slotId,
|
||||||
typeComposantId: slot.typeComposantId,
|
typeComposantId: slot.typeComposantId,
|
||||||
selectedComponentId: edits && 'selectedComposantId' in edits ? edits.selectedComposantId : (slot.selectedComponentId ?? null),
|
selectedComponentId: edits && 'selectedComposantId' in edits ? edits.selectedComposantId : (slot.selectedComponentId ?? null),
|
||||||
|
selectedComponentName: slot.selectedComponentName ?? null,
|
||||||
alias: slot.alias,
|
alias: slot.alias,
|
||||||
familyCode: slot.familyCode,
|
familyCode: slot.familyCode,
|
||||||
position: slot.position ?? i,
|
position: slot.position ?? i,
|
||||||
@@ -361,7 +370,6 @@ export function useComponentEdit(componentId: string) {
|
|||||||
|
|
||||||
const reference = editionForm.reference.trim()
|
const reference = editionForm.reference.trim()
|
||||||
payload.reference = reference || null
|
payload.reference = reference || null
|
||||||
payload.constructeurIds = uniqueConstructeurIds(editionForm.constructeurIds)
|
|
||||||
|
|
||||||
if (rawPrice) {
|
if (rawPrice) {
|
||||||
const parsed = Number(rawPrice)
|
const parsed = Number(rawPrice)
|
||||||
@@ -434,6 +442,9 @@ export function useComponentEdit(componentId: string) {
|
|||||||
slotEdits.products = {}
|
slotEdits.products = {}
|
||||||
slotEdits.subcomponents = {}
|
slotEdits.subcomponents = {}
|
||||||
|
|
||||||
|
await syncLinks('composant', component.value.id, originalConstructeurLinks.value, constructeurLinks.value)
|
||||||
|
originalConstructeurLinks.value = constructeurLinks.value.map(l => ({ ...l }))
|
||||||
|
|
||||||
toast.showSuccess('Composant mis à jour avec succès.')
|
toast.showSuccess('Composant mis à jour avec succès.')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -468,15 +479,16 @@ export function useComponentEdit(componentId: string) {
|
|||||||
editionForm.name = currentComponent.name || ''
|
editionForm.name = currentComponent.name || ''
|
||||||
editionForm.description = currentComponent.description || ''
|
editionForm.description = currentComponent.description || ''
|
||||||
editionForm.reference = currentComponent.reference || ''
|
editionForm.reference = currentComponent.reference || ''
|
||||||
editionForm.constructeurIds = uniqueConstructeurIds(
|
// Load constructeur links
|
||||||
currentComponent,
|
fetchLinks('composant', componentId).then((links) => {
|
||||||
Array.isArray(currentComponent.constructeurs) ? currentComponent.constructeurs : [],
|
constructeurLinks.value = links
|
||||||
currentComponent.constructeur ? [currentComponent.constructeur] : [],
|
originalConstructeurLinks.value = links.map(l => ({ ...l }))
|
||||||
)
|
editionForm.constructeurIds = constructeurIdsFromLinks(links)
|
||||||
editionForm.prix = currentComponent.prix !== null && currentComponent.prix !== undefined ? String(currentComponent.prix) : ''
|
|
||||||
if (editionForm.constructeurIds.length) {
|
if (editionForm.constructeurIds.length) {
|
||||||
void ensureConstructeurs(editionForm.constructeurIds)
|
void ensureConstructeurs(editionForm.constructeurIds)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
editionForm.prix = currentComponent.prix !== null && currentComponent.prix !== undefined ? String(currentComponent.prix) : ''
|
||||||
|
|
||||||
initialized.value = true
|
initialized.value = true
|
||||||
}
|
}
|
||||||
@@ -543,6 +555,9 @@ export function useComponentEdit(componentId: string) {
|
|||||||
previewVisible,
|
previewVisible,
|
||||||
selectedTypeId,
|
selectedTypeId,
|
||||||
editionForm,
|
editionForm,
|
||||||
|
constructeurLinks,
|
||||||
|
originalConstructeurLinks,
|
||||||
|
constructeurIdsFromForm,
|
||||||
customFieldInputs,
|
customFieldInputs,
|
||||||
historyFieldLabels,
|
historyFieldLabels,
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
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 { buildConstructeurRequestPayload, uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
import { uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
||||||
import { useConstructeurs, type Constructeur } from './useConstructeurs'
|
import { useConstructeurs, type Constructeur } from './useConstructeurs'
|
||||||
import { extractRelationId, normalizeRelationIds } from '~/shared/apiRelations'
|
import { extractRelationId, normalizeRelationIds } from '~/shared/apiRelations'
|
||||||
import { extractCollection } from '~/shared/utils/apiHelpers'
|
import { extractCollection } from '~/shared/utils/apiHelpers'
|
||||||
@@ -182,7 +182,8 @@ export function useComposants() {
|
|||||||
const createComposant = async (composantData: Partial<Composant>): Promise<ComposantSingleResult> => {
|
const createComposant = async (composantData: Partial<Composant>): Promise<ComposantSingleResult> => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const normalizedPayload = normalizeRelationIds(buildConstructeurRequestPayload(composantData))
|
const { constructeurIds, constructeurs, constructeurId, constructeur, ...cleanPayload } = composantData as any
|
||||||
|
const normalizedPayload = normalizeRelationIds(cleanPayload)
|
||||||
const result = await post('/composants', normalizedPayload)
|
const result = await post('/composants', normalizedPayload)
|
||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
const enriched = await withResolvedConstructeurs(result.data as Composant)
|
const enriched = await withResolvedConstructeurs(result.data as Composant)
|
||||||
@@ -209,7 +210,8 @@ export function useComposants() {
|
|||||||
const updateComposantData = async (id: string, composantData: Partial<Composant>): Promise<ComposantSingleResult> => {
|
const updateComposantData = async (id: string, composantData: Partial<Composant>): Promise<ComposantSingleResult> => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const normalizedPayload = normalizeRelationIds(buildConstructeurRequestPayload(composantData))
|
const { constructeurIds, constructeurs, constructeurId, constructeur, ...cleanPayload } = composantData as any
|
||||||
|
const normalizedPayload = normalizeRelationIds(cleanPayload)
|
||||||
const result = await patch(`/composants/${id}`, normalizedPayload)
|
const result = await patch(`/composants/${id}`, normalizedPayload)
|
||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
const updated = await withResolvedConstructeurs(result.data as Composant)
|
const updated = await withResolvedConstructeurs(result.data as Composant)
|
||||||
|
|||||||
103
app/composables/useConstructeurLinks.ts
Normal file
103
app/composables/useConstructeurLinks.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import { useApi } from '~/composables/useApi'
|
||||||
|
import type { ConstructeurLinkEntry } from '~/shared/constructeurUtils'
|
||||||
|
import { extractCollection } from '~/shared/utils/apiHelpers'
|
||||||
|
|
||||||
|
type EntityType = 'machine' | 'piece' | 'composant' | 'product'
|
||||||
|
|
||||||
|
const ENDPOINTS: Record<EntityType, string> = {
|
||||||
|
machine: '/machine_constructeur_links',
|
||||||
|
piece: '/piece_constructeur_links',
|
||||||
|
composant: '/composant_constructeur_links',
|
||||||
|
product: '/product_constructeur_links',
|
||||||
|
}
|
||||||
|
|
||||||
|
const ENTITY_KEYS: Record<EntityType, string> = {
|
||||||
|
machine: 'machine',
|
||||||
|
piece: 'piece',
|
||||||
|
composant: 'composant',
|
||||||
|
product: 'product',
|
||||||
|
}
|
||||||
|
|
||||||
|
const ENTITY_PLURALS: Record<EntityType, string> = {
|
||||||
|
machine: 'machines',
|
||||||
|
piece: 'pieces',
|
||||||
|
composant: 'composants',
|
||||||
|
product: 'products',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useConstructeurLinks() {
|
||||||
|
const { get, post, patch, delete: del } = useApi()
|
||||||
|
|
||||||
|
const fetchLinks = async (
|
||||||
|
entityType: EntityType,
|
||||||
|
entityId: string,
|
||||||
|
): Promise<ConstructeurLinkEntry[]> => {
|
||||||
|
const endpoint = ENDPOINTS[entityType]
|
||||||
|
const key = ENTITY_KEYS[entityType]
|
||||||
|
const plural = ENTITY_PLURALS[entityType]
|
||||||
|
const url = `${endpoint}?${key}=/api/${plural}/${entityId}`
|
||||||
|
const result = await get(url)
|
||||||
|
if (!result.success || !result.data) return []
|
||||||
|
|
||||||
|
const members = extractCollection(result.data)
|
||||||
|
if (!Array.isArray(members)) return []
|
||||||
|
|
||||||
|
return members.map((link: any) => ({
|
||||||
|
linkId: link.id ?? (typeof link['@id'] === 'string' ? link['@id'].split('/').pop() : undefined),
|
||||||
|
constructeurId: typeof link.constructeur === 'string'
|
||||||
|
? link.constructeur.split('/').pop()!
|
||||||
|
: link.constructeur?.id ?? '',
|
||||||
|
constructeur: typeof link.constructeur === 'object' ? link.constructeur : null,
|
||||||
|
supplierReference: link.supplierReference ?? null,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const syncLinks = async (
|
||||||
|
entityType: EntityType,
|
||||||
|
entityId: string,
|
||||||
|
originalLinks: ConstructeurLinkEntry[],
|
||||||
|
formLinks: ConstructeurLinkEntry[],
|
||||||
|
): Promise<void> => {
|
||||||
|
const endpoint = ENDPOINTS[entityType]
|
||||||
|
const key = ENTITY_KEYS[entityType]
|
||||||
|
const plural = ENTITY_PLURALS[entityType]
|
||||||
|
const entityIri = `/api/${plural}/${entityId}`
|
||||||
|
|
||||||
|
const originalMap = new Map(originalLinks.map(l => [l.constructeurId, l]))
|
||||||
|
const formMap = new Map(formLinks.map(l => [l.constructeurId, l]))
|
||||||
|
|
||||||
|
const promises: Promise<any>[] = []
|
||||||
|
|
||||||
|
// Delete removed links
|
||||||
|
for (const [cId, orig] of originalMap) {
|
||||||
|
if (!formMap.has(cId) && orig.linkId) {
|
||||||
|
promises.push(del(`${endpoint}/${orig.linkId}`))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new links
|
||||||
|
for (const [cId, form] of formMap) {
|
||||||
|
if (!originalMap.has(cId)) {
|
||||||
|
promises.push(post(endpoint, {
|
||||||
|
[key]: entityIri,
|
||||||
|
constructeur: `/api/constructeurs/${cId}`,
|
||||||
|
supplierReference: form.supplierReference || null,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Patch modified supplierReference
|
||||||
|
for (const [cId, form] of formMap) {
|
||||||
|
const orig = originalMap.get(cId)
|
||||||
|
if (orig?.linkId && (orig.supplierReference ?? null) !== (form.supplierReference ?? null)) {
|
||||||
|
promises.push(patch(`${endpoint}/${orig.linkId}`, {
|
||||||
|
supplierReference: form.supplierReference || null,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.allSettled(promises)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { fetchLinks, syncLinks }
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ref, computed, type Ref, type ComputedRef } from 'vue'
|
import { ref, computed, watch, type Ref, type ComputedRef } from 'vue'
|
||||||
import { useUrlState } from './useUrlState'
|
import { useUrlState } from './useUrlState'
|
||||||
import type { DataTableSort, DataTablePagination, DataTableColumnFilters, SortDirection } from '~/shared/types/dataTable'
|
import type { DataTableSort, DataTablePagination, DataTableColumnFilters, SortDirection } from '~/shared/types/dataTable'
|
||||||
|
|
||||||
@@ -22,6 +22,8 @@ export interface UseDataTableOptions {
|
|||||||
persistToUrl?: boolean
|
persistToUrl?: boolean
|
||||||
/** Extra URL state params for page-specific filters */
|
/** Extra URL state params for page-specific filters */
|
||||||
extraParams?: Record<string, { default: string | number; type?: 'string' | 'number' }>
|
extraParams?: Record<string, { default: string | number; type?: 'string' | 'number' }>
|
||||||
|
/** Column filter keys to persist in URL (prefixed with `f.` in query string) */
|
||||||
|
columnFilterKeys?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UseDataTableReturn {
|
export interface UseDataTableReturn {
|
||||||
@@ -56,6 +58,7 @@ export function useDataTable(
|
|||||||
searchDebounceMs = 300,
|
searchDebounceMs = 300,
|
||||||
persistToUrl = true,
|
persistToUrl = true,
|
||||||
extraParams = {},
|
extraParams = {},
|
||||||
|
columnFilterKeys = [],
|
||||||
} = options
|
} = options
|
||||||
|
|
||||||
let searchTerm: Ref<string>
|
let searchTerm: Ref<string>
|
||||||
@@ -64,6 +67,7 @@ export function useDataTable(
|
|||||||
let currentPage: Ref<number>
|
let currentPage: Ref<number>
|
||||||
let itemsPerPage: Ref<number>
|
let itemsPerPage: Ref<number>
|
||||||
const filters: Record<string, Ref<string | number>> = {}
|
const filters: Record<string, Ref<string | number>> = {}
|
||||||
|
const columnFilterRefs: Record<string, Ref<string>> = {}
|
||||||
|
|
||||||
if (persistToUrl) {
|
if (persistToUrl) {
|
||||||
const paramDefs: Record<string, { default: string | number; type?: 'string' | 'number'; debounce?: number }> = {
|
const paramDefs: Record<string, { default: string | number; type?: 'string' | 'number'; debounce?: number }> = {
|
||||||
@@ -75,6 +79,10 @@ export function useDataTable(
|
|||||||
...extraParams,
|
...extraParams,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const key of columnFilterKeys) {
|
||||||
|
paramDefs[`f.${key}`] = { default: '', debounce: 300 }
|
||||||
|
}
|
||||||
|
|
||||||
const state = useUrlState(paramDefs, {
|
const state = useUrlState(paramDefs, {
|
||||||
onRestore: () => deps.fetchData(),
|
onRestore: () => deps.fetchData(),
|
||||||
})
|
})
|
||||||
@@ -88,6 +96,10 @@ export function useDataTable(
|
|||||||
for (const key of Object.keys(extraParams)) {
|
for (const key of Object.keys(extraParams)) {
|
||||||
filters[key] = (state as Record<string, Ref<string | number>>)[key]!
|
filters[key] = (state as Record<string, Ref<string | number>>)[key]!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const key of columnFilterKeys) {
|
||||||
|
columnFilterRefs[key] = (state as Record<string, Ref<string>>)[`f.${key}`]!
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
searchTerm = ref('')
|
searchTerm = ref('')
|
||||||
@@ -137,8 +149,31 @@ export function useDataTable(
|
|||||||
deps.fetchData()
|
deps.fetchData()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Column filters
|
// Column filters — seed from URL-persisted refs
|
||||||
const columnFilters = ref<DataTableColumnFilters>({})
|
const initialColumnFilters: DataTableColumnFilters = {}
|
||||||
|
for (const [key, r] of Object.entries(columnFilterRefs)) {
|
||||||
|
if (r.value) initialColumnFilters[key] = r.value
|
||||||
|
}
|
||||||
|
const columnFilters = ref<DataTableColumnFilters>(initialColumnFilters)
|
||||||
|
|
||||||
|
// Sync columnFilters → URL refs
|
||||||
|
if (persistToUrl && columnFilterKeys.length > 0) {
|
||||||
|
watch(columnFilters, (val) => {
|
||||||
|
for (const key of columnFilterKeys) {
|
||||||
|
columnFilterRefs[key]!.value = val[key] || ''
|
||||||
|
}
|
||||||
|
}, { deep: true })
|
||||||
|
|
||||||
|
// Sync URL refs → columnFilters (back/forward navigation)
|
||||||
|
for (const key of columnFilterKeys) {
|
||||||
|
watch(columnFilterRefs[key]!, (urlVal) => {
|
||||||
|
const current = columnFilters.value[key] || ''
|
||||||
|
if (current !== urlVal) {
|
||||||
|
columnFilters.value = { ...columnFilters.value, [key]: urlVal }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleColumnFiltersChange = (newFilters: DataTableColumnFilters) => {
|
const handleColumnFiltersChange = (newFilters: DataTableColumnFilters) => {
|
||||||
columnFilters.value = newFilters
|
columnFilters.value = newFilters
|
||||||
|
|||||||
@@ -21,7 +21,11 @@ import {
|
|||||||
resolveConstructeurs,
|
resolveConstructeurs,
|
||||||
uniqueConstructeurIds,
|
uniqueConstructeurIds,
|
||||||
formatConstructeurContact as formatConstructeurContactSummary,
|
formatConstructeurContact as formatConstructeurContactSummary,
|
||||||
|
parseConstructeurLinksFromApi,
|
||||||
|
constructeurIdsFromLinks,
|
||||||
} from '~/shared/constructeurUtils'
|
} from '~/shared/constructeurUtils'
|
||||||
|
import type { ConstructeurLinkEntry } from '~/shared/constructeurUtils'
|
||||||
|
import { useConstructeurLinks } from '~/composables/useConstructeurLinks'
|
||||||
import { useMachineDetailDocuments } from '~/composables/useMachineDetailDocuments'
|
import { useMachineDetailDocuments } from '~/composables/useMachineDetailDocuments'
|
||||||
import { useMachineDetailCustomFields } from '~/composables/useMachineDetailCustomFields'
|
import { useMachineDetailCustomFields } from '~/composables/useMachineDetailCustomFields'
|
||||||
import { useMachineDetailHierarchy } from '~/composables/useMachineDetailHierarchy'
|
import { useMachineDetailHierarchy } from '~/composables/useMachineDetailHierarchy'
|
||||||
@@ -64,6 +68,11 @@ export function useMachineDetailData(machineId: string) {
|
|||||||
const printAreaRef = ref<HTMLElement | null>(null)
|
const printAreaRef = ref<HTMLElement | null>(null)
|
||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
|
|
||||||
|
// Constructeur links
|
||||||
|
const { fetchLinks, syncLinks } = useConstructeurLinks()
|
||||||
|
const constructeurLinks = ref<ConstructeurLinkEntry[]>([])
|
||||||
|
const originalConstructeurLinks = ref<ConstructeurLinkEntry[]>([])
|
||||||
|
|
||||||
// Machine fields
|
// Machine fields
|
||||||
const machineName = ref('')
|
const machineName = ref('')
|
||||||
const machineReference = ref('')
|
const machineReference = ref('')
|
||||||
@@ -78,20 +87,15 @@ export function useMachineDetailData(machineId: string) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const machineConstructeursDisplay = computed(() => {
|
const machineConstructeursDisplay = computed(() => {
|
||||||
const ids = uniqueConstructeurIds(
|
const ids = machineConstructeurIds.value
|
||||||
machineConstructeurIds.value,
|
if (!ids.length) return [] as any[]
|
||||||
(machine.value as AnyRecord)?.constructeurIds,
|
// Extract nested constructeur objects from link entries as candidate pool
|
||||||
(machine.value as AnyRecord)?.constructeurs,
|
const linkConstructeurs = constructeurLinks.value
|
||||||
(machine.value as AnyRecord)?.constructeur,
|
.filter(l => l.constructeur && l.constructeur.id)
|
||||||
)
|
.map(l => l.constructeur!) as any[]
|
||||||
return resolveConstructeurs(
|
return resolveConstructeurs(
|
||||||
ids,
|
ids,
|
||||||
Array.isArray((machine.value as AnyRecord)?.constructeurs)
|
linkConstructeurs,
|
||||||
? ((machine.value as AnyRecord).constructeurs as any[])
|
|
||||||
: [],
|
|
||||||
(machine.value as AnyRecord)?.constructeur
|
|
||||||
? [(machine.value as AnyRecord).constructeur as any]
|
|
||||||
: [],
|
|
||||||
constructeurs.value as any,
|
constructeurs.value as any,
|
||||||
) as any[]
|
) as any[]
|
||||||
})
|
})
|
||||||
@@ -235,11 +239,12 @@ export function useMachineDetailData(machineId: string) {
|
|||||||
if (machine.value) {
|
if (machine.value) {
|
||||||
machineName.value = (machine.value.name as string) || ''
|
machineName.value = (machine.value.name as string) || ''
|
||||||
machineReference.value = (machine.value.reference as string) || ''
|
machineReference.value = (machine.value.reference as string) || ''
|
||||||
machineConstructeurIds.value = uniqueConstructeurIds(
|
// Parse constructeur links from structure response
|
||||||
machine.value.constructeurIds,
|
const rawLinks = Array.isArray(machine.value.constructeurs) ? machine.value.constructeurs as any[] : []
|
||||||
machine.value.constructeurs,
|
const parsed = parseConstructeurLinksFromApi(rawLinks)
|
||||||
machine.value.constructeur,
|
constructeurLinks.value = parsed
|
||||||
)
|
originalConstructeurLinks.value = parsed.map(l => ({ ...l }))
|
||||||
|
machineConstructeurIds.value = constructeurIdsFromLinks(parsed)
|
||||||
machineSiteId.value = (machine.value.siteId as string) || (machine.value.site as AnyRecord)?.id as string || ''
|
machineSiteId.value = (machine.value.siteId as string) || (machine.value.site as AnyRecord)?.id as string || ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -269,6 +274,8 @@ export function useMachineDetailData(machineId: string) {
|
|||||||
machineReference,
|
machineReference,
|
||||||
machineSiteId,
|
machineSiteId,
|
||||||
machineConstructeurIds,
|
machineConstructeurIds,
|
||||||
|
constructeurLinks,
|
||||||
|
originalConstructeurLinks,
|
||||||
machineDocumentsLoaded,
|
machineDocumentsLoaded,
|
||||||
machineComponentLinks,
|
machineComponentLinks,
|
||||||
machinePieceLinks,
|
machinePieceLinks,
|
||||||
@@ -284,6 +291,7 @@ export function useMachineDetailData(machineId: string) {
|
|||||||
updatePieceApi,
|
updatePieceApi,
|
||||||
apiPatch,
|
apiPatch,
|
||||||
toast,
|
toast,
|
||||||
|
syncLinks,
|
||||||
})
|
})
|
||||||
|
|
||||||
// UI methods
|
// UI methods
|
||||||
@@ -338,6 +346,8 @@ export function useMachineDetailData(machineId: string) {
|
|||||||
const cancelEdition = () => {
|
const cancelEdition = () => {
|
||||||
initMachineFields()
|
initMachineFields()
|
||||||
syncMachineCustomFields()
|
syncMachineCustomFields()
|
||||||
|
constructeurLinks.value = originalConstructeurLinks.value.map(l => ({ ...l }))
|
||||||
|
machineConstructeurIds.value = constructeurIdsFromLinks(constructeurLinks.value)
|
||||||
isEditMode.value = false
|
isEditMode.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -467,6 +477,7 @@ export function useMachineDetailData(machineId: string) {
|
|||||||
// Machine fields
|
// Machine fields
|
||||||
machineName, machineReference, machineSiteId, machineConstructeurIds, machineConstructeurId,
|
machineName, machineReference, machineSiteId, machineConstructeurIds, machineConstructeurId,
|
||||||
machineConstructeursDisplay, machineConstructeurContact, hasMachineConstructeur,
|
machineConstructeursDisplay, machineConstructeurContact, hasMachineConstructeur,
|
||||||
|
constructeurLinks, originalConstructeurLinks,
|
||||||
sites,
|
sites,
|
||||||
|
|
||||||
// UI state
|
// UI state
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Ref } from 'vue'
|
import type { Ref } from 'vue'
|
||||||
import { uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
import { uniqueConstructeurIds, constructeurIdsFromLinks } from '~/shared/constructeurUtils'
|
||||||
|
import type { ConstructeurLinkEntry } from '~/shared/constructeurUtils'
|
||||||
|
|
||||||
type AnyRecord = Record<string, unknown>
|
type AnyRecord = Record<string, unknown>
|
||||||
|
|
||||||
@@ -15,6 +16,8 @@ export interface UseMachineDetailUpdatesDeps {
|
|||||||
machineReference: Ref<string>
|
machineReference: Ref<string>
|
||||||
machineSiteId: Ref<string>
|
machineSiteId: Ref<string>
|
||||||
machineConstructeurIds: Ref<string[]>
|
machineConstructeurIds: Ref<string[]>
|
||||||
|
constructeurLinks: Ref<ConstructeurLinkEntry[]>
|
||||||
|
originalConstructeurLinks: Ref<ConstructeurLinkEntry[]>
|
||||||
machineDocumentsLoaded: Ref<boolean>
|
machineDocumentsLoaded: Ref<boolean>
|
||||||
machineComponentLinks: Ref<AnyRecord[]>
|
machineComponentLinks: Ref<AnyRecord[]>
|
||||||
machinePieceLinks: Ref<AnyRecord[]>
|
machinePieceLinks: Ref<AnyRecord[]>
|
||||||
@@ -35,6 +38,12 @@ export interface UseMachineDetailUpdatesDeps {
|
|||||||
updatePieceApi: (id: string, data: any) => Promise<unknown>
|
updatePieceApi: (id: string, data: any) => Promise<unknown>
|
||||||
apiPatch: (endpoint: string, data?: unknown) => Promise<any>
|
apiPatch: (endpoint: string, data?: unknown) => Promise<any>
|
||||||
toast: { showInfo: (msg: string) => void }
|
toast: { showInfo: (msg: string) => void }
|
||||||
|
syncLinks: (
|
||||||
|
entityType: 'machine' | 'piece' | 'composant' | 'product',
|
||||||
|
entityId: string,
|
||||||
|
originalLinks: ConstructeurLinkEntry[],
|
||||||
|
formLinks: ConstructeurLinkEntry[],
|
||||||
|
) => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useMachineDetailUpdates(deps: UseMachineDetailUpdatesDeps) {
|
export function useMachineDetailUpdates(deps: UseMachineDetailUpdatesDeps) {
|
||||||
@@ -44,6 +53,8 @@ export function useMachineDetailUpdates(deps: UseMachineDetailUpdatesDeps) {
|
|||||||
machineReference,
|
machineReference,
|
||||||
machineSiteId,
|
machineSiteId,
|
||||||
machineConstructeurIds,
|
machineConstructeurIds,
|
||||||
|
constructeurLinks,
|
||||||
|
originalConstructeurLinks,
|
||||||
machineComponentLinks,
|
machineComponentLinks,
|
||||||
machinePieceLinks,
|
machinePieceLinks,
|
||||||
applyMachineLinks,
|
applyMachineLinks,
|
||||||
@@ -56,19 +67,16 @@ export function useMachineDetailUpdates(deps: UseMachineDetailUpdatesDeps) {
|
|||||||
updatePieceApi,
|
updatePieceApi,
|
||||||
apiPatch,
|
apiPatch,
|
||||||
toast,
|
toast,
|
||||||
|
syncLinks,
|
||||||
} = deps
|
} = deps
|
||||||
|
|
||||||
const updateMachineInfo = async () => {
|
const updateMachineInfo = async () => {
|
||||||
if (!machine.value) return
|
if (!machine.value) return
|
||||||
try {
|
try {
|
||||||
const cIds = uniqueConstructeurIds(machineConstructeurIds.value)
|
|
||||||
machineConstructeurIds.value = cIds
|
|
||||||
|
|
||||||
const result: any = await updateMachineApi(machine.value.id as string, {
|
const result: any = await updateMachineApi(machine.value.id as string, {
|
||||||
name: machineName.value,
|
name: machineName.value,
|
||||||
reference: machineReference.value,
|
reference: machineReference.value,
|
||||||
siteId: machineSiteId.value || undefined,
|
siteId: machineSiteId.value || undefined,
|
||||||
constructeurIds: cIds,
|
|
||||||
} as any)
|
} as any)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const machinePayload =
|
const machinePayload =
|
||||||
@@ -82,11 +90,6 @@ export function useMachineDetailUpdates(deps: UseMachineDetailUpdatesDeps) {
|
|||||||
documents: machinePayload.documents || machine.value.documents || [],
|
documents: machinePayload.documents || machine.value.documents || [],
|
||||||
customFieldValues: machinePayload.customFieldValues || machine.value.customFieldValues || [],
|
customFieldValues: machinePayload.customFieldValues || machine.value.customFieldValues || [],
|
||||||
}
|
}
|
||||||
machineConstructeurIds.value = uniqueConstructeurIds(
|
|
||||||
machine.value!.constructeurIds,
|
|
||||||
machine.value!.constructeurs,
|
|
||||||
machine.value!.constructeur,
|
|
||||||
)
|
|
||||||
const linksApplied = applyMachineLinks(result.data)
|
const linksApplied = applyMachineLinks(result.data)
|
||||||
if (linksApplied && machine.value) {
|
if (linksApplied && machine.value) {
|
||||||
machine.value.componentLinks = machineComponentLinks.value
|
machine.value.componentLinks = machineComponentLinks.value
|
||||||
@@ -95,6 +98,9 @@ export function useMachineDetailUpdates(deps: UseMachineDetailUpdatesDeps) {
|
|||||||
loadProductDocuments().catch(() => {})
|
loadProductDocuments().catch(() => {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Sync constructeur links after entity save
|
||||||
|
await syncLinks('machine', machine.value!.id as string, originalConstructeurLinks.value, constructeurLinks.value)
|
||||||
|
originalConstructeurLinks.value = constructeurLinks.value.map(l => ({ ...l }))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erreur lors de la mise à jour de la machine:', error)
|
console.error('Erreur lors de la mise à jour de la machine:', error)
|
||||||
}
|
}
|
||||||
@@ -209,7 +215,13 @@ export function useMachineDetailUpdates(deps: UseMachineDetailUpdatesDeps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleMachineConstructeurChange = (value: unknown) => {
|
const handleMachineConstructeurChange = (value: unknown) => {
|
||||||
machineConstructeurIds.value = uniqueConstructeurIds(value)
|
const newIds = uniqueConstructeurIds(value)
|
||||||
|
machineConstructeurIds.value = newIds
|
||||||
|
// Sync constructeurLinks: keep existing entries, add new ones
|
||||||
|
const existingMap = new Map(constructeurLinks.value.map(l => [l.constructeurId, l]))
|
||||||
|
constructeurLinks.value = newIds.map(id =>
|
||||||
|
existingMap.get(id) ?? { constructeurId: id, supplierReference: null },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const editComponent = () => {
|
const editComponent = () => {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useToast } from './useToast'
|
import { useToast } from './useToast'
|
||||||
import { useApi, type ApiResponse } from './useApi'
|
import { useApi, type ApiResponse } from './useApi'
|
||||||
import { buildConstructeurRequestPayload } from '~/shared/constructeurUtils'
|
|
||||||
import { extractRelationId, normalizeRelationIds } from '~/shared/apiRelations'
|
import { extractRelationId, normalizeRelationIds } from '~/shared/apiRelations'
|
||||||
import { extractCollection } from '~/shared/utils/apiHelpers'
|
import { extractCollection } from '~/shared/utils/apiHelpers'
|
||||||
|
|
||||||
@@ -92,7 +91,7 @@ export function useMachines() {
|
|||||||
const createMachine = async (machineData: Partial<Machine>): Promise<ApiResponse> => {
|
const createMachine = async (machineData: Partial<Machine>): Promise<ApiResponse> => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const normalizedPayload = normalizeRelationIds(buildConstructeurRequestPayload(machineData))
|
const normalizedPayload = normalizeRelationIds(machineData as Record<string, unknown>)
|
||||||
const result = await post('/machines', normalizedPayload)
|
const result = await post('/machines', normalizedPayload)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const createdMachine = normalizeMachineResponse(result.data) ||
|
const createdMachine = normalizeMachineResponse(result.data) ||
|
||||||
@@ -116,7 +115,7 @@ export function useMachines() {
|
|||||||
const updateMachineData = async (id: string, machineData: Partial<Machine>): Promise<ApiResponse> => {
|
const updateMachineData = async (id: string, machineData: Partial<Machine>): Promise<ApiResponse> => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const normalizedPayload = normalizeRelationIds(buildConstructeurRequestPayload(machineData))
|
const normalizedPayload = normalizeRelationIds(machineData as Record<string, unknown>)
|
||||||
const result = await patch(`/machines/${id}`, normalizedPayload)
|
const result = await patch(`/machines/${id}`, normalizedPayload)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const updatedMachine = normalizeMachineResponse(result.data) ||
|
const updatedMachine = normalizeMachineResponse(result.data) ||
|
||||||
|
|||||||
@@ -7,11 +7,13 @@ 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 { useConstructeurLinks } from '~/composables/useConstructeurLinks'
|
||||||
import { usePieceHistory } from '~/composables/usePieceHistory'
|
import { usePieceHistory } from '~/composables/usePieceHistory'
|
||||||
import { extractRelationId } from '~/shared/apiRelations'
|
import { extractRelationId } from '~/shared/apiRelations'
|
||||||
import { canPreviewDocument } from '~/utils/documentPreview'
|
import { canPreviewDocument } from '~/utils/documentPreview'
|
||||||
import { formatPieceStructurePreview } from '~/shared/modelUtils'
|
import { formatPieceStructurePreview } from '~/shared/modelUtils'
|
||||||
import { uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
import { uniqueConstructeurIds, constructeurIdsFromLinks } from '~/shared/constructeurUtils'
|
||||||
|
import type { ConstructeurLinkEntry } from '~/shared/constructeurUtils'
|
||||||
import type { PieceModelStructure } from '~/shared/types/inventory'
|
import type { PieceModelStructure } from '~/shared/types/inventory'
|
||||||
import type { ModelType } from '~/services/modelTypes'
|
import type { ModelType } from '~/services/modelTypes'
|
||||||
import {
|
import {
|
||||||
@@ -46,6 +48,7 @@ export function usePieceEdit(pieceId: string) {
|
|||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const { loadDocumentsByPiece, uploadDocuments, deleteDocument } = useDocuments()
|
const { loadDocumentsByPiece, uploadDocuments, deleteDocument } = useDocuments()
|
||||||
const { ensureConstructeurs } = useConstructeurs()
|
const { ensureConstructeurs } = useConstructeurs()
|
||||||
|
const { fetchLinks, syncLinks } = useConstructeurLinks()
|
||||||
const {
|
const {
|
||||||
history,
|
history,
|
||||||
loading: historyLoading,
|
loading: historyLoading,
|
||||||
@@ -82,6 +85,9 @@ export function usePieceEdit(pieceId: string) {
|
|||||||
constructeurIds: [] as string[],
|
constructeurIds: [] as string[],
|
||||||
prix: '' as string,
|
prix: '' as string,
|
||||||
})
|
})
|
||||||
|
const constructeurLinks = ref<ConstructeurLinkEntry[]>([])
|
||||||
|
const originalConstructeurLinks = ref<ConstructeurLinkEntry[]>([])
|
||||||
|
const constructeurIdsFromForm = computed(() => constructeurIdsFromLinks(constructeurLinks.value))
|
||||||
const productSelections = ref<(string | null)[]>([])
|
const productSelections = ref<(string | null)[]>([])
|
||||||
|
|
||||||
const customFieldInputs = ref<CustomFieldInput[]>([])
|
const customFieldInputs = ref<CustomFieldInput[]>([])
|
||||||
@@ -303,15 +309,16 @@ export function usePieceEdit(pieceId: string) {
|
|||||||
editionForm.name = currentPiece.name || ''
|
editionForm.name = currentPiece.name || ''
|
||||||
editionForm.description = currentPiece.description || ''
|
editionForm.description = currentPiece.description || ''
|
||||||
editionForm.reference = currentPiece.reference || ''
|
editionForm.reference = currentPiece.reference || ''
|
||||||
editionForm.constructeurIds = uniqueConstructeurIds(
|
// Load constructeur links
|
||||||
currentPiece,
|
fetchLinks('piece', pieceId).then((links) => {
|
||||||
Array.isArray(currentPiece.constructeurs) ? currentPiece.constructeurs : [],
|
constructeurLinks.value = links
|
||||||
currentPiece.constructeur ? [currentPiece.constructeur] : [],
|
originalConstructeurLinks.value = links.map(l => ({ ...l }))
|
||||||
)
|
editionForm.constructeurIds = constructeurIdsFromLinks(links)
|
||||||
editionForm.prix = currentPiece.prix !== null && currentPiece.prix !== undefined ? String(currentPiece.prix) : ''
|
|
||||||
if (editionForm.constructeurIds.length) {
|
if (editionForm.constructeurIds.length) {
|
||||||
void ensureConstructeurs(editionForm.constructeurIds)
|
void ensureConstructeurs(editionForm.constructeurIds)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
editionForm.prix = currentPiece.prix !== null && currentPiece.prix !== undefined ? String(currentPiece.prix) : ''
|
||||||
|
|
||||||
const existingProductIds = Array.isArray(currentPiece.productIds) && currentPiece.productIds.length
|
const existingProductIds = Array.isArray(currentPiece.productIds) && currentPiece.productIds.length
|
||||||
? currentPiece.productIds.map((id: unknown) => String(id))
|
? currentPiece.productIds.map((id: unknown) => String(id))
|
||||||
@@ -370,12 +377,9 @@ export function usePieceEdit(pieceId: string) {
|
|||||||
? ''
|
? ''
|
||||||
: String(editionForm.prix).trim()
|
: String(editionForm.prix).trim()
|
||||||
|
|
||||||
const constructeurIds = uniqueConstructeurIds(editionForm.constructeurIds)
|
|
||||||
|
|
||||||
const payload: Record<string, any> = {
|
const payload: Record<string, any> = {
|
||||||
name: editionForm.name.trim(),
|
name: editionForm.name.trim(),
|
||||||
description: editionForm.description.trim() || null,
|
description: editionForm.description.trim() || null,
|
||||||
constructeurIds,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const reference = editionForm.reference.trim()
|
const reference = editionForm.reference.trim()
|
||||||
@@ -412,6 +416,8 @@ export function usePieceEdit(pieceId: string) {
|
|||||||
],
|
],
|
||||||
{ customFieldInputs, upsertCustomFieldValue, updateCustomFieldValue, toast },
|
{ customFieldInputs, upsertCustomFieldValue, updateCustomFieldValue, toast },
|
||||||
)
|
)
|
||||||
|
await syncLinks('piece', piece.value.id, originalConstructeurLinks.value, constructeurLinks.value)
|
||||||
|
originalConstructeurLinks.value = constructeurLinks.value.map(l => ({ ...l }))
|
||||||
toast.showSuccess('Pièce mise à jour avec succès.')
|
toast.showSuccess('Pièce mise à jour avec succès.')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -441,6 +447,9 @@ export function usePieceEdit(pieceId: string) {
|
|||||||
previewVisible,
|
previewVisible,
|
||||||
selectedTypeId,
|
selectedTypeId,
|
||||||
editionForm,
|
editionForm,
|
||||||
|
constructeurLinks,
|
||||||
|
originalConstructeurLinks,
|
||||||
|
constructeurIdsFromForm,
|
||||||
productSelections,
|
productSelections,
|
||||||
customFieldInputs,
|
customFieldInputs,
|
||||||
canEdit,
|
canEdit,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
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 { buildConstructeurRequestPayload, uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
import { uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
||||||
import { useConstructeurs, type Constructeur } from './useConstructeurs'
|
import { useConstructeurs, type Constructeur } from './useConstructeurs'
|
||||||
import { extractRelationId, normalizeRelationIds } from '~/shared/apiRelations'
|
import { extractRelationId, normalizeRelationIds } from '~/shared/apiRelations'
|
||||||
import { extractCollection } from '~/shared/utils/apiHelpers'
|
import { extractCollection } from '~/shared/utils/apiHelpers'
|
||||||
@@ -10,6 +10,7 @@ export interface Piece {
|
|||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
reference?: string | null
|
reference?: string | null
|
||||||
|
referenceAuto?: string | null
|
||||||
description?: string | null
|
description?: string | null
|
||||||
typePieceId?: string | null
|
typePieceId?: string | null
|
||||||
typePiece?: { id: string; name?: string } | null
|
typePiece?: { id: string; name?: string } | null
|
||||||
@@ -195,7 +196,8 @@ export function usePieces() {
|
|||||||
const createPiece = async (pieceData: Partial<Piece>): Promise<PieceSingleResult> => {
|
const createPiece = async (pieceData: Partial<Piece>): Promise<PieceSingleResult> => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const normalizedPayload = normalizeRelationIds(buildConstructeurRequestPayload(pieceData))
|
const { constructeurIds, constructeurs, constructeurId, constructeur, ...cleanPayload } = pieceData as any
|
||||||
|
const normalizedPayload = normalizeRelationIds(cleanPayload)
|
||||||
const result = await post('/pieces', normalizedPayload)
|
const result = await post('/pieces', normalizedPayload)
|
||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
const enriched = await withResolvedConstructeurs(result.data as Piece)
|
const enriched = await withResolvedConstructeurs(result.data as Piece)
|
||||||
@@ -222,7 +224,8 @@ export function usePieces() {
|
|||||||
const updatePieceData = async (id: string, pieceData: Partial<Piece>): Promise<PieceSingleResult> => {
|
const updatePieceData = async (id: string, pieceData: Partial<Piece>): Promise<PieceSingleResult> => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const normalizedPayload = normalizeRelationIds(buildConstructeurRequestPayload(pieceData))
|
const { constructeurIds, constructeurs, constructeurId, constructeur, ...cleanPayload } = pieceData as any
|
||||||
|
const normalizedPayload = normalizeRelationIds(cleanPayload)
|
||||||
const result = await patch(`/pieces/${id}`, normalizedPayload)
|
const result = await patch(`/pieces/${id}`, normalizedPayload)
|
||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
const updated = await withResolvedConstructeurs(result.data as Piece)
|
const updated = await withResolvedConstructeurs(result.data as Piece)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { ref } from 'vue'
|
|||||||
import { useToast } from './useToast'
|
import { useToast } from './useToast'
|
||||||
import { useApi } from './useApi'
|
import { useApi } from './useApi'
|
||||||
import { humanizeError } from '~/shared/utils/errorMessages'
|
import { humanizeError } from '~/shared/utils/errorMessages'
|
||||||
import { buildConstructeurRequestPayload, uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
import { uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
||||||
import { useConstructeurs, type Constructeur } from './useConstructeurs'
|
import { useConstructeurs, type Constructeur } from './useConstructeurs'
|
||||||
import { extractRelationId, normalizeRelationIds } from '~/shared/apiRelations'
|
import { extractRelationId, normalizeRelationIds } from '~/shared/apiRelations'
|
||||||
import { extractCollection } from '~/shared/utils/apiHelpers'
|
import { extractCollection } from '~/shared/utils/apiHelpers'
|
||||||
@@ -196,7 +196,8 @@ export function useProducts() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const createProduct = async (payload: Partial<Product>): Promise<ProductSingleResult> => {
|
const createProduct = async (payload: Partial<Product>): Promise<ProductSingleResult> => {
|
||||||
const normalizedPayload = normalizeRelationIds(buildConstructeurRequestPayload(payload))
|
const { constructeurIds, constructeurs, constructeurId, constructeur, ...cleanPayload } = payload as any
|
||||||
|
const normalizedPayload = normalizeRelationIds(cleanPayload)
|
||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
@@ -225,7 +226,8 @@ export function useProducts() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const updateProduct = async (id: string, payload: Partial<Product>): Promise<ProductSingleResult> => {
|
const updateProduct = async (id: string, payload: Partial<Product>): Promise<ProductSingleResult> => {
|
||||||
const normalizedPayload = normalizeRelationIds(buildConstructeurRequestPayload(payload))
|
const { constructeurIds, constructeurs, constructeurId, constructeur, ...cleanPayload } = payload as any
|
||||||
|
const normalizedPayload = normalizeRelationIds(cleanPayload)
|
||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -72,6 +72,28 @@ const badgeClass = (type: ChangeType) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const releases: Release[] = [
|
const releases: Release[] = [
|
||||||
|
{
|
||||||
|
version: "v1.9.5",
|
||||||
|
date: "2026-03-31",
|
||||||
|
changes: [
|
||||||
|
{
|
||||||
|
type: "feat",
|
||||||
|
text: "Référence automatique des pièces et composants : génération d'une référence technique à partir d'une formule configurable sur la catégorie, recalculée automatiquement à chaque modification des champs personnalisés",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "feat",
|
||||||
|
text: "Formula builder interactif : sélection des champs disponibles par clic (chips) avec insertion à la position du curseur, aperçu live avec valeurs d'exemple, et calcul automatique des champs requis",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "feat",
|
||||||
|
text: "Versioning des entités : numéro de version incrémenté automatiquement à chaque modification, avec historique des versions et possibilité de restaurer une version antérieure",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "feat",
|
||||||
|
text: "Bouton de sauvegarde unique sur la fiche machine : remplacement des sauvegardes automatiques par un bouton explicite, avec affichage des versions sur les liens composants/pièces/produits",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
version: "v1.9.4",
|
version: "v1.9.4",
|
||||||
date: "2026-03-25",
|
date: "2026-03-25",
|
||||||
|
|||||||
@@ -166,6 +166,7 @@ const table = useDataTable(
|
|||||||
status: { default: 'open' },
|
status: { default: 'open' },
|
||||||
entityType: { default: '' },
|
entityType: { default: '' },
|
||||||
},
|
},
|
||||||
|
columnFilterKeys: ['entity'],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ const { componentTypes, loadComponentTypes } = useComponentTypes()
|
|||||||
|
|
||||||
const table = useDataTable(
|
const table = useDataTable(
|
||||||
{ fetchData: fetchComposants },
|
{ fetchData: fetchComposants },
|
||||||
{ defaultSort: 'name', defaultDirection: 'asc', defaultPerPage: 20, persistToUrl: true },
|
{ defaultSort: 'name', defaultDirection: 'asc', defaultPerPage: 20, persistToUrl: true, columnFilterKeys: ['typeComposant'] },
|
||||||
)
|
)
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
|
|||||||
@@ -114,6 +114,8 @@ const loadCategory = async () => {
|
|||||||
category: response.category,
|
category: response.category,
|
||||||
notes: response.notes ?? response.description ?? '',
|
notes: response.notes ?? response.description ?? '',
|
||||||
structure: (response.structure as ComponentModelStructure | null) ?? undefined,
|
structure: (response.structure as ComponentModelStructure | null) ?? undefined,
|
||||||
|
referenceFormula: response.referenceFormula ?? null,
|
||||||
|
requiredFieldsForReference: response.requiredFieldsForReference ?? null,
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -141,6 +141,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ConstructeurLinksTable
|
||||||
|
v-if="constructeurLinks.length"
|
||||||
|
v-model="constructeurLinks"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label">
|
<label class="label">
|
||||||
@@ -350,13 +355,15 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
import { useRoute } from '#imports'
|
import { useRoute } from '#imports'
|
||||||
import { useComponentEdit } from '~/composables/useComponentEdit'
|
import { useComponentEdit } from '~/composables/useComponentEdit'
|
||||||
import { useDocuments } from '~/composables/useDocuments'
|
import { useDocuments } from '~/composables/useDocuments'
|
||||||
|
import { useConstructeurs } from '~/composables/useConstructeurs'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const { updateDocument } = useDocuments()
|
const { updateDocument } = useDocuments()
|
||||||
|
const { getConstructeurById } = useConstructeurs()
|
||||||
const versionRefreshKey = ref(0)
|
const versionRefreshKey = ref(0)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -371,6 +378,8 @@ const {
|
|||||||
previewVisible,
|
previewVisible,
|
||||||
selectedTypeId,
|
selectedTypeId,
|
||||||
editionForm,
|
editionForm,
|
||||||
|
constructeurLinks,
|
||||||
|
constructeurIdsFromForm,
|
||||||
customFieldInputs,
|
customFieldInputs,
|
||||||
historyFieldLabels,
|
historyFieldLabels,
|
||||||
canEdit,
|
canEdit,
|
||||||
@@ -401,6 +410,26 @@ const {
|
|||||||
fetchComponent,
|
fetchComponent,
|
||||||
} = useComponentEdit(String(route.params.id))
|
} = useComponentEdit(String(route.params.id))
|
||||||
|
|
||||||
|
// Sync constructeurIds → constructeurLinks when IDs are added via ConstructeurSelect
|
||||||
|
watch(
|
||||||
|
() => editionForm.constructeurIds,
|
||||||
|
(ids) => {
|
||||||
|
const currentIds = new Set(constructeurLinks.value.map(l => l.constructeurId))
|
||||||
|
for (const id of ids) {
|
||||||
|
if (!currentIds.has(id)) {
|
||||||
|
const resolved = getConstructeurById(id)
|
||||||
|
constructeurLinks.value.push({
|
||||||
|
constructeurId: id,
|
||||||
|
constructeur: resolved ? { id: resolved.id, name: resolved.name } : null,
|
||||||
|
supplierReference: null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Remove links whose ID was removed from the select
|
||||||
|
constructeurLinks.value = constructeurLinks.value.filter(l => ids.includes(l.constructeurId))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
const editingDocument = ref<any | null>(null)
|
const editingDocument = ref<any | null>(null)
|
||||||
const editModalVisible = ref(false)
|
const editModalVisible = ref(false)
|
||||||
|
|
||||||
|
|||||||
@@ -128,7 +128,7 @@
|
|||||||
|
|
||||||
<!-- Référence + Fournisseurs (if value or edit mode) -->
|
<!-- Référence + Fournisseurs (if value or edit mode) -->
|
||||||
<div
|
<div
|
||||||
v-if="isEditMode || component.reference || editionForm.constructeurIds.length"
|
v-if="isEditMode || component.reference || constructeurLinks.length"
|
||||||
class="grid grid-cols-1 gap-4 md:grid-cols-2"
|
class="grid grid-cols-1 gap-4 md:grid-cols-2"
|
||||||
>
|
>
|
||||||
<div v-if="isEditMode || component.reference" class="form-control">
|
<div v-if="isEditMode || component.reference" class="form-control">
|
||||||
@@ -148,7 +148,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="isEditMode || editionForm.constructeurIds.length" class="form-control">
|
<div v-if="isEditMode || constructeurLinks.length" class="form-control">
|
||||||
<label class="label">
|
<label class="label">
|
||||||
<span class="label-text">Fournisseur</span>
|
<span class="label-text">Fournisseur</span>
|
||||||
</label>
|
</label>
|
||||||
@@ -160,18 +160,20 @@
|
|||||||
placeholder="Rechercher un ou plusieurs fournisseurs..."
|
placeholder="Rechercher un ou plusieurs fournisseurs..."
|
||||||
:initial-options="component?.constructeurs || []"
|
:initial-options="component?.constructeurs || []"
|
||||||
/>
|
/>
|
||||||
<div v-else class="flex flex-wrap gap-2">
|
|
||||||
<span
|
|
||||||
v-for="id in editionForm.constructeurIds"
|
|
||||||
:key="id"
|
|
||||||
class="badge badge-outline"
|
|
||||||
>
|
|
||||||
{{ getConstructeurById(id)?.name || id }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Constructeur links table -->
|
||||||
|
<ConstructeurLinksTable
|
||||||
|
v-if="isEditMode && constructeurLinks.length"
|
||||||
|
v-model="constructeurLinks"
|
||||||
|
/>
|
||||||
|
<ConstructeurLinksTable
|
||||||
|
v-else-if="!isEditMode && constructeurLinks.length"
|
||||||
|
:model-value="constructeurLinks"
|
||||||
|
:readonly="true"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Prix (if value or edit mode) -->
|
<!-- Prix (if value or edit mode) -->
|
||||||
<div v-if="isEditMode || component.prix" class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
<div v-if="isEditMode || component.prix" class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
@@ -254,7 +256,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div v-else class="input input-bordered input-sm md:input-md bg-base-200 flex items-center gap-2">
|
<div v-else class="input input-bordered input-sm md:input-md bg-base-200 flex items-center gap-2">
|
||||||
{{ resolvePieceLabel(slot.selectedPieceId) || '— Non sélectionné' }}
|
{{ slot.selectedPieceName || '— Non sélectionné' }}
|
||||||
<span v-if="slot.quantity > 1" class="badge badge-sm">x{{ slot.quantity }}</span>
|
<span v-if="slot.quantity > 1" class="badge badge-sm">x{{ slot.quantity }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -281,7 +283,7 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<div v-else class="input input-bordered input-sm md:input-md bg-base-200 flex items-center">
|
<div v-else class="input input-bordered input-sm md:input-md bg-base-200 flex items-center">
|
||||||
{{ resolveProductLabel(slot.selectedProductId) || '— Non sélectionné' }}
|
{{ slot.selectedProductName || '— Non sélectionné' }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -307,7 +309,7 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<div v-else class="input input-bordered input-sm md:input-md bg-base-200 flex items-center">
|
<div v-else class="input input-bordered input-sm md:input-md bg-base-200 flex items-center">
|
||||||
{{ resolveSubcomponentLabel(slot.selectedComponentId) || '— Non sélectionné' }}
|
{{ slot.selectedComponentName || '— Non sélectionné' }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -435,12 +437,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
import { useRoute } from '#imports'
|
import { useRoute } from '#imports'
|
||||||
import { useComponentEdit } from '~/composables/useComponentEdit'
|
import { useComponentEdit } from '~/composables/useComponentEdit'
|
||||||
import { useDocuments } from '~/composables/useDocuments'
|
import { useDocuments } from '~/composables/useDocuments'
|
||||||
import { usePermissions } from '~/composables/usePermissions'
|
import { usePermissions } from '~/composables/usePermissions'
|
||||||
import { useConstructeurs } from '~/composables/useConstructeurs'
|
import { useConstructeurs } from '~/composables/useConstructeurs'
|
||||||
|
import type { ConstructeurLinkEntry } from '~/shared/constructeurUtils'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const { canEdit } = usePermissions()
|
const { canEdit } = usePermissions()
|
||||||
@@ -461,6 +464,8 @@ const {
|
|||||||
previewVisible,
|
previewVisible,
|
||||||
selectedTypeId,
|
selectedTypeId,
|
||||||
editionForm,
|
editionForm,
|
||||||
|
constructeurLinks,
|
||||||
|
constructeurIdsFromForm,
|
||||||
customFieldInputs,
|
customFieldInputs,
|
||||||
historyFieldLabels,
|
historyFieldLabels,
|
||||||
canSubmit,
|
canSubmit,
|
||||||
@@ -497,6 +502,26 @@ const submitEdition = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sync constructeurIds → constructeurLinks when IDs are added via ConstructeurSelect
|
||||||
|
watch(
|
||||||
|
() => editionForm.constructeurIds,
|
||||||
|
(ids) => {
|
||||||
|
const currentIds = new Set(constructeurLinks.value.map(l => l.constructeurId))
|
||||||
|
for (const id of ids) {
|
||||||
|
if (!currentIds.has(id)) {
|
||||||
|
const resolved = getConstructeurById(id)
|
||||||
|
constructeurLinks.value.push({
|
||||||
|
constructeurId: id,
|
||||||
|
constructeur: resolved ? { id: resolved.id, name: resolved.name } : null,
|
||||||
|
supplierReference: null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Remove links whose ID was removed from the select
|
||||||
|
constructeurLinks.value = constructeurLinks.value.filter(l => ids.includes(l.constructeurId))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
const editingDocument = ref<any | null>(null)
|
const editingDocument = ref<any | null>(null)
|
||||||
const editModalVisible = ref(false)
|
const editModalVisible = ref(false)
|
||||||
|
|
||||||
|
|||||||
@@ -92,6 +92,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ConstructeurLinksTable
|
||||||
|
v-if="constructeurLinks.length"
|
||||||
|
v-model="constructeurLinks"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label">
|
<label class="label">
|
||||||
@@ -215,10 +220,16 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { watch } from 'vue'
|
||||||
|
import { useConstructeurs } from '~/composables/useConstructeurs'
|
||||||
|
|
||||||
|
const { getConstructeurById } = useConstructeurs()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
selectedTypeId,
|
selectedTypeId,
|
||||||
submitting,
|
submitting,
|
||||||
creationForm,
|
creationForm,
|
||||||
|
constructeurLinks,
|
||||||
customFieldInputs,
|
customFieldInputs,
|
||||||
structureAssignments,
|
structureAssignments,
|
||||||
selectedDocuments,
|
selectedDocuments,
|
||||||
@@ -249,4 +260,23 @@ const {
|
|||||||
resolveSubcomponentLabel,
|
resolveSubcomponentLabel,
|
||||||
submitCreation,
|
submitCreation,
|
||||||
} = useComponentCreate()
|
} = useComponentCreate()
|
||||||
|
|
||||||
|
// Sync constructeurIds → constructeurLinks when IDs are added via ConstructeurSelect
|
||||||
|
watch(
|
||||||
|
() => creationForm.constructeurIds,
|
||||||
|
(ids) => {
|
||||||
|
const currentIds = new Set(constructeurLinks.value.map(l => l.constructeurId))
|
||||||
|
for (const id of ids) {
|
||||||
|
if (!currentIds.has(id)) {
|
||||||
|
const resolved = getConstructeurById(id)
|
||||||
|
constructeurLinks.value.push({
|
||||||
|
constructeurId: id,
|
||||||
|
constructeur: resolved ? { id: resolved.id, name: resolved.name } : null,
|
||||||
|
supplierReference: null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
constructeurLinks.value = constructeurLinks.value.filter(l => ids.includes(l.constructeurId))
|
||||||
|
},
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
:machine-constructeur-ids="d.machineConstructeurIds.value"
|
:machine-constructeur-ids="d.machineConstructeurIds.value"
|
||||||
:machine-constructeurs-display="d.machineConstructeursDisplay.value"
|
:machine-constructeurs-display="d.machineConstructeursDisplay.value"
|
||||||
:has-machine-constructeur="d.hasMachineConstructeur.value"
|
:has-machine-constructeur="d.hasMachineConstructeur.value"
|
||||||
|
:constructeur-links="d.constructeurLinks.value"
|
||||||
:visible-custom-fields="d.visibleMachineCustomFields.value"
|
:visible-custom-fields="d.visibleMachineCustomFields.value"
|
||||||
:get-machine-field-id="d.getMachineFieldId"
|
:get-machine-field-id="d.getMachineFieldId"
|
||||||
:machine-id="machineId"
|
:machine-id="machineId"
|
||||||
@@ -72,6 +73,8 @@
|
|||||||
@update:machine-reference="d.machineReference.value = $event"
|
@update:machine-reference="d.machineReference.value = $event"
|
||||||
@update:machine-site-id="d.machineSiteId.value = $event"
|
@update:machine-site-id="d.machineSiteId.value = $event"
|
||||||
@update:constructeur-ids="d.handleMachineConstructeurChange"
|
@update:constructeur-ids="d.handleMachineConstructeurChange"
|
||||||
|
@update:constructeur-links="d.constructeurLinks.value = $event"
|
||||||
|
@remove-constructeur-link="handleRemoveConstructeurLink"
|
||||||
@set-custom-field-value="d.setMachineCustomFieldValue"
|
@set-custom-field-value="d.setMachineCustomFieldValue"
|
||||||
@custom-fields-saved="() => { d.loadMachineData(); refreshVersions() }"
|
@custom-fields-saved="() => { d.loadMachineData(); refreshVersions() }"
|
||||||
/>
|
/>
|
||||||
@@ -196,9 +199,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<h3 class="text-lg font-semibold text-base-content mb-1">Machine non trouvée</h3>
|
<h3 class="text-lg font-semibold text-base-content mb-1">Machine non trouvée</h3>
|
||||||
<p class="text-sm text-base-content/50 mb-6">La machine avec l'ID "{{ machineId }}" n'existe pas ou a été supprimée.</p>
|
<p class="text-sm text-base-content/50 mb-6">La machine avec l'ID "{{ machineId }}" n'existe pas ou a été supprimée.</p>
|
||||||
<NuxtLink to="/machines" class="btn btn-primary">
|
<button type="button" class="btn btn-primary" @click="$router.back()">
|
||||||
Retour aux machines
|
Retour aux machines
|
||||||
</NuxtLink>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
@@ -280,6 +283,11 @@ const openAddModal = (kind) => {
|
|||||||
addModalOpen.value = true
|
addModalOpen.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleRemoveConstructeurLink = (constructeurId) => {
|
||||||
|
const ids = d.machineConstructeurIds.value.filter(id => id !== constructeurId)
|
||||||
|
d.handleMachineConstructeurChange(ids)
|
||||||
|
}
|
||||||
|
|
||||||
const handleAddEntity = async (entityId) => {
|
const handleAddEntity = async (entityId) => {
|
||||||
if (addModalKind.value === 'component') {
|
if (addModalKind.value === 'component') {
|
||||||
await d.addComponentLink(entityId)
|
await d.addComponentLink(entityId)
|
||||||
|
|||||||
@@ -120,11 +120,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, computed, onMounted } from 'vue'
|
import { ref, reactive, computed, onMounted, watch } from 'vue'
|
||||||
import { useMachines } from '~/composables/useMachines'
|
import { useMachines } from '~/composables/useMachines'
|
||||||
import { useSites } from '~/composables/useSites'
|
import { useSites } from '~/composables/useSites'
|
||||||
import { useToast } from '~/composables/useToast'
|
import { useToast } from '~/composables/useToast'
|
||||||
import { humanizeError } from '~/shared/utils/errorMessages'
|
import { humanizeError } from '~/shared/utils/errorMessages'
|
||||||
|
import { useUrlState } from '~/composables/useUrlState'
|
||||||
import IconLucidePlus from '~icons/lucide/plus'
|
import IconLucidePlus from '~icons/lucide/plus'
|
||||||
import IconLucideFactory from '~icons/lucide/factory'
|
import IconLucideFactory from '~icons/lucide/factory'
|
||||||
import IconLucideMapPin from '~icons/lucide/map-pin'
|
import IconLucideMapPin from '~icons/lucide/map-pin'
|
||||||
@@ -135,8 +136,28 @@ const { machines, loading, loadMachines, deleteMachine } = useMachines()
|
|||||||
const { sites, loadSites } = useSites()
|
const { sites, loadSites } = useSites()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
|
const urlState = useUrlState({
|
||||||
|
q: { default: '', debounce: 300 },
|
||||||
|
sites: { default: '' },
|
||||||
|
})
|
||||||
|
|
||||||
|
const searchQuery = urlState.q
|
||||||
const selectedSites = reactive(new Set())
|
const selectedSites = reactive(new Set())
|
||||||
const searchQuery = ref('')
|
|
||||||
|
// Sync URL → selectedSites on load and back/forward
|
||||||
|
watch(urlState.sites, (val) => {
|
||||||
|
selectedSites.clear()
|
||||||
|
if (val) {
|
||||||
|
for (const id of String(val).split(',')) {
|
||||||
|
if (id) selectedSites.add(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
// Sync selectedSites → URL
|
||||||
|
watch(() => [...selectedSites], (ids) => {
|
||||||
|
urlState.sites.value = ids.join(',')
|
||||||
|
})
|
||||||
|
|
||||||
// Enrichir les machines avec les objets site complets
|
// Enrichir les machines avec les objets site complets
|
||||||
const enrichedMachines = computed(() => {
|
const enrichedMachines = computed(() => {
|
||||||
|
|||||||
@@ -112,6 +112,8 @@ const loadCategory = async () => {
|
|||||||
category: response.category,
|
category: response.category,
|
||||||
notes: response.notes ?? response.description ?? '',
|
notes: response.notes ?? response.description ?? '',
|
||||||
structure: (response.structure as PieceModelStructure | null) ?? undefined,
|
structure: (response.structure as PieceModelStructure | null) ?? undefined,
|
||||||
|
referenceFormula: response.referenceFormula ?? null,
|
||||||
|
requiredFieldsForReference: response.requiredFieldsForReference ?? null,
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -113,6 +113,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Référence auto (read-only, shown only if computed) -->
|
||||||
|
<div v-if="piece.referenceAuto" class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text">Référence auto</span>
|
||||||
|
</label>
|
||||||
|
<div class="input input-bordered input-sm md:input-md bg-base-200 flex items-center gap-2">
|
||||||
|
<span class="font-mono font-semibold">{{ piece.referenceAuto }}</span>
|
||||||
|
<span class="badge badge-sm badge-ghost">auto</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Référence + Fournisseurs (if value or edit mode) -->
|
<!-- Référence + Fournisseurs (if value or edit mode) -->
|
||||||
<div
|
<div
|
||||||
v-if="isEditMode || piece.reference || editionForm.constructeurIds.length"
|
v-if="isEditMode || piece.reference || editionForm.constructeurIds.length"
|
||||||
@@ -135,27 +146,29 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="isEditMode || editionForm.constructeurIds.length" class="form-control">
|
<div v-if="isEditMode || constructeurLinks.length" class="form-control">
|
||||||
<label class="label">
|
<label class="label">
|
||||||
<span class="label-text">Fournisseur</span>
|
<span class="label-text">Fournisseur</span>
|
||||||
</label>
|
</label>
|
||||||
|
<template v-if="isEditMode">
|
||||||
<ConstructeurSelect
|
<ConstructeurSelect
|
||||||
v-if="isEditMode"
|
|
||||||
v-model="editionForm.constructeurIds"
|
v-model="editionForm.constructeurIds"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
:disabled="!canEdit || saving"
|
:disabled="!canEdit || saving"
|
||||||
placeholder="Rechercher un ou plusieurs fournisseurs..."
|
placeholder="Rechercher un ou plusieurs fournisseurs..."
|
||||||
:initial-options="piece?.constructeurs || []"
|
:initial-options="piece?.constructeurs || []"
|
||||||
/>
|
/>
|
||||||
<div v-else class="flex flex-wrap gap-2">
|
<ConstructeurLinksTable
|
||||||
<span
|
v-model="constructeurLinks"
|
||||||
v-for="id in editionForm.constructeurIds"
|
class="mt-2"
|
||||||
:key="id"
|
@remove="handleConstructeurRemoved"
|
||||||
class="badge badge-outline"
|
/>
|
||||||
>
|
</template>
|
||||||
{{ getConstructeurById(id)?.name || id }}
|
<ConstructeurLinksTable
|
||||||
</span>
|
v-else
|
||||||
</div>
|
:model-value="constructeurLinks"
|
||||||
|
readonly
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -378,16 +391,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
import { useRoute } from '#imports'
|
import { useRoute } from '#imports'
|
||||||
import { usePieceEdit } from '~/composables/usePieceEdit'
|
import { usePieceEdit } from '~/composables/usePieceEdit'
|
||||||
import { useDocuments } from '~/composables/useDocuments'
|
import { useDocuments } from '~/composables/useDocuments'
|
||||||
import { usePermissions } from '~/composables/usePermissions'
|
import { usePermissions } from '~/composables/usePermissions'
|
||||||
import { useConstructeurs } from '~/composables/useConstructeurs'
|
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const { canEdit } = usePermissions()
|
const { canEdit } = usePermissions()
|
||||||
const { getConstructeurById } = useConstructeurs()
|
|
||||||
const { updateDocument } = useDocuments()
|
const { updateDocument } = useDocuments()
|
||||||
|
|
||||||
const isEditMode = ref(false)
|
const isEditMode = ref(false)
|
||||||
@@ -405,6 +416,7 @@ const {
|
|||||||
previewVisible,
|
previewVisible,
|
||||||
selectedTypeId,
|
selectedTypeId,
|
||||||
editionForm,
|
editionForm,
|
||||||
|
constructeurLinks,
|
||||||
productSelections,
|
productSelections,
|
||||||
customFieldInputs,
|
customFieldInputs,
|
||||||
pieceTypeList,
|
pieceTypeList,
|
||||||
@@ -437,6 +449,18 @@ const submitEdition = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sync ConstructeurSelect changes → constructeurLinks
|
||||||
|
watch(() => editionForm.constructeurIds, (newIds) => {
|
||||||
|
const existing = new Map(constructeurLinks.value.map(l => [l.constructeurId, l]))
|
||||||
|
constructeurLinks.value = newIds.map(id =>
|
||||||
|
existing.get(id) || { constructeurId: id, supplierReference: null },
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleConstructeurRemoved = (constructeurId: string) => {
|
||||||
|
editionForm.constructeurIds = editionForm.constructeurIds.filter(id => id !== constructeurId)
|
||||||
|
}
|
||||||
|
|
||||||
const editingDocument = ref<any | null>(null)
|
const editingDocument = ref<any | null>(null)
|
||||||
const editModalVisible = ref(false)
|
const editModalVisible = ref(false)
|
||||||
|
|
||||||
|
|||||||
@@ -69,6 +69,10 @@
|
|||||||
{{ row.piece.reference || '—' }}
|
{{ row.piece.reference || '—' }}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template #cell-referenceAuto="{ row }">
|
||||||
|
{{ row.piece.referenceAuto || '—' }}
|
||||||
|
</template>
|
||||||
|
|
||||||
<template #cell-description="{ row }">
|
<template #cell-description="{ row }">
|
||||||
<div v-if="row.piece.description" class="group relative">
|
<div v-if="row.piece.description" class="group relative">
|
||||||
<span class="block cursor-help truncate">{{ row.piece.description }}</span>
|
<span class="block cursor-help truncate">{{ row.piece.description }}</span>
|
||||||
@@ -167,13 +171,14 @@ const { pieceTypes, loadPieceTypes } = usePieceTypes()
|
|||||||
|
|
||||||
const table = useDataTable(
|
const table = useDataTable(
|
||||||
{ fetchData: fetchPieces },
|
{ fetchData: fetchPieces },
|
||||||
{ defaultSort: 'name', defaultDirection: 'asc', defaultPerPage: 20, persistToUrl: true },
|
{ defaultSort: 'name', defaultDirection: 'asc', defaultPerPage: 20, persistToUrl: true, columnFilterKeys: ['typePiece'] },
|
||||||
)
|
)
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ key: 'preview', label: 'Aperçu', width: 'w-24' },
|
{ key: 'preview', label: 'Aperçu', width: 'w-24' },
|
||||||
{ key: 'name', label: 'Nom', sortable: true },
|
{ key: 'name', label: 'Nom', sortable: true },
|
||||||
{ key: 'reference', label: 'Référence' },
|
{ key: 'reference', label: 'Référence' },
|
||||||
|
{ key: 'referenceAuto', label: 'Réf. auto' },
|
||||||
{ key: 'description', label: 'Description' },
|
{ key: 'description', label: 'Description' },
|
||||||
{ key: 'suppliers', label: 'Fournisseurs' },
|
{ key: 'suppliers', label: 'Fournisseurs' },
|
||||||
{ key: 'typePiece', label: 'Type de pièce', filterable: true, filterPlaceholder: 'Filtrer…' },
|
{ key: 'typePiece', label: 'Type de pièce', filterable: true, filterPlaceholder: 'Filtrer…' },
|
||||||
|
|||||||
@@ -114,6 +114,19 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="piece?.referenceAuto" class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text">Référence auto</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
:value="piece.referenceAuto"
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered input-sm md:input-md bg-base-200"
|
||||||
|
disabled
|
||||||
|
title="Générée automatiquement à partir du type et des champs personnalisés"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label">
|
<label class="label">
|
||||||
<span class="label-text">Fournisseur</span>
|
<span class="label-text">Fournisseur</span>
|
||||||
|
|||||||
@@ -91,6 +91,10 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<ConstructeurLinksTable
|
||||||
|
v-if="constructeurLinks.length"
|
||||||
|
v-model="constructeurLinks"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
@@ -213,6 +217,7 @@
|
|||||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from '#imports'
|
import { useRoute, useRouter } from '#imports'
|
||||||
import ConstructeurSelect from '~/components/ConstructeurSelect.vue'
|
import ConstructeurSelect from '~/components/ConstructeurSelect.vue'
|
||||||
|
import ConstructeurLinksTable from '~/components/ConstructeurLinksTable.vue'
|
||||||
import DocumentUpload from '~/components/DocumentUpload.vue'
|
import DocumentUpload from '~/components/DocumentUpload.vue'
|
||||||
import ProductSelect from '~/components/ProductSelect.vue'
|
import ProductSelect from '~/components/ProductSelect.vue'
|
||||||
import SearchSelect from '~/components/common/SearchSelect.vue'
|
import SearchSelect from '~/components/common/SearchSelect.vue'
|
||||||
@@ -222,8 +227,11 @@ import { useToast } from '~/composables/useToast'
|
|||||||
import { humanizeError } from '~/shared/utils/errorMessages'
|
import { humanizeError } from '~/shared/utils/errorMessages'
|
||||||
import { useCustomFields } from '~/composables/useCustomFields'
|
import { useCustomFields } from '~/composables/useCustomFields'
|
||||||
import { useDocuments } from '~/composables/useDocuments'
|
import { useDocuments } from '~/composables/useDocuments'
|
||||||
|
import { useConstructeurLinks } from '~/composables/useConstructeurLinks'
|
||||||
|
import { useConstructeurs } from '~/composables/useConstructeurs'
|
||||||
import { formatPieceStructurePreview } from '~/shared/modelUtils'
|
import { formatPieceStructurePreview } from '~/shared/modelUtils'
|
||||||
import { uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
import { constructeurIdsFromLinks } from '~/shared/constructeurUtils'
|
||||||
|
import type { ConstructeurLinkEntry } from '~/shared/constructeurUtils'
|
||||||
import type { PieceModelStructure } from '~/shared/types/inventory'
|
import type { PieceModelStructure } from '~/shared/types/inventory'
|
||||||
import type { ModelType } from '~/services/modelTypes'
|
import type { ModelType } from '~/services/modelTypes'
|
||||||
import {
|
import {
|
||||||
@@ -255,6 +263,8 @@ const { createPiece } = usePieces()
|
|||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const { upsertCustomFieldValue, updateCustomFieldValue } = useCustomFields()
|
const { upsertCustomFieldValue, updateCustomFieldValue } = useCustomFields()
|
||||||
const { uploadDocuments } = useDocuments()
|
const { uploadDocuments } = useDocuments()
|
||||||
|
const { syncLinks } = useConstructeurLinks()
|
||||||
|
const { getConstructeurById } = useConstructeurs()
|
||||||
const { canEdit } = usePermissions()
|
const { canEdit } = usePermissions()
|
||||||
|
|
||||||
const initialTypeId = ref<string>(typeof route.query.typeId === 'string' ? route.query.typeId : '')
|
const initialTypeId = ref<string>(typeof route.query.typeId === 'string' ? route.query.typeId : '')
|
||||||
@@ -267,6 +277,7 @@ const creationForm = reactive({
|
|||||||
constructeurIds: [] as string[],
|
constructeurIds: [] as string[],
|
||||||
prix: '' as string,
|
prix: '' as string,
|
||||||
})
|
})
|
||||||
|
const constructeurLinks = ref<ConstructeurLinkEntry[]>([])
|
||||||
const productSelections = ref<(string | null)[]>([])
|
const productSelections = ref<(string | null)[]>([])
|
||||||
|
|
||||||
const lastSuggestedName = ref('')
|
const lastSuggestedName = ref('')
|
||||||
@@ -380,6 +391,7 @@ const clearCreationForm = () => {
|
|||||||
creationForm.description = ''
|
creationForm.description = ''
|
||||||
creationForm.reference = ''
|
creationForm.reference = ''
|
||||||
creationForm.constructeurIds = []
|
creationForm.constructeurIds = []
|
||||||
|
constructeurLinks.value = []
|
||||||
creationForm.prix = ''
|
creationForm.prix = ''
|
||||||
productSelections.value = []
|
productSelections.value = []
|
||||||
lastSuggestedName.value = ''
|
lastSuggestedName.value = ''
|
||||||
@@ -411,8 +423,6 @@ const submitCreation = async () => {
|
|||||||
payload.reference = reference
|
payload.reference = reference
|
||||||
}
|
}
|
||||||
|
|
||||||
payload.constructeurIds = uniqueConstructeurIds(creationForm.constructeurIds)
|
|
||||||
|
|
||||||
const normalizedProductIds = collectNormalizedProductIds(
|
const normalizedProductIds = collectNormalizedProductIds(
|
||||||
productRequirementEntries.value,
|
productRequirementEntries.value,
|
||||||
productSelections.value,
|
productSelections.value,
|
||||||
@@ -448,6 +458,10 @@ const submitCreation = async () => {
|
|||||||
],
|
],
|
||||||
{ customFieldInputs, upsertCustomFieldValue, updateCustomFieldValue, toast },
|
{ customFieldInputs, upsertCustomFieldValue, updateCustomFieldValue, toast },
|
||||||
)
|
)
|
||||||
|
// Sync constructeur links after creation
|
||||||
|
if (constructeurLinks.value.length) {
|
||||||
|
await syncLinks('piece', createdPiece.id, [], constructeurLinks.value)
|
||||||
|
}
|
||||||
if (selectedDocuments.value.length && createdPiece.id) {
|
if (selectedDocuments.value.length && createdPiece.id) {
|
||||||
uploadingDocuments.value = true
|
uploadingDocuments.value = true
|
||||||
const uploadResult = await uploadDocuments(
|
const uploadResult = await uploadDocuments(
|
||||||
@@ -478,6 +492,26 @@ const submitCreation = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sync constructeurIds → constructeurLinks when IDs are added via ConstructeurSelect
|
||||||
|
watch(
|
||||||
|
() => creationForm.constructeurIds,
|
||||||
|
(ids) => {
|
||||||
|
const currentIds = new Set(constructeurLinks.value.map(l => l.constructeurId))
|
||||||
|
for (const id of ids) {
|
||||||
|
if (!currentIds.has(id)) {
|
||||||
|
const resolved = getConstructeurById(id)
|
||||||
|
constructeurLinks.value.push({
|
||||||
|
constructeurId: id,
|
||||||
|
constructeur: resolved ? { id: resolved.id, name: resolved.name } : null,
|
||||||
|
supplierReference: null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
constructeurLinks.value = constructeurLinks.value.filter(l => ids.includes(l.constructeurId))
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
)
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadPieceTypes()
|
await loadPieceTypes()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ const toast = useToast()
|
|||||||
|
|
||||||
const table = useDataTable(
|
const table = useDataTable(
|
||||||
{ fetchData: fetchProducts },
|
{ fetchData: fetchProducts },
|
||||||
{ defaultSort: 'name', defaultDirection: 'asc', defaultPerPage: 20, persistToUrl: true },
|
{ defaultSort: 'name', defaultDirection: 'asc', defaultPerPage: 20, persistToUrl: true, columnFilterKeys: ['typeProduct'] },
|
||||||
)
|
)
|
||||||
|
|
||||||
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))
|
||||||
|
|||||||
@@ -105,6 +105,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ConstructeurLinksTable
|
||||||
|
v-if="constructeurLinks.length"
|
||||||
|
v-model="constructeurLinks"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label">
|
<label class="label">
|
||||||
@@ -237,9 +242,11 @@ import { useToast } from '~/composables/useToast'
|
|||||||
import { humanizeError } from '~/shared/utils/errorMessages'
|
import { humanizeError } from '~/shared/utils/errorMessages'
|
||||||
import { useDocuments } from '~/composables/useDocuments'
|
import { useDocuments } from '~/composables/useDocuments'
|
||||||
import { useConstructeurs } from '~/composables/useConstructeurs'
|
import { useConstructeurs } from '~/composables/useConstructeurs'
|
||||||
|
import { useConstructeurLinks } from '~/composables/useConstructeurLinks'
|
||||||
import { useProductHistory } from '~/composables/useProductHistory'
|
import { useProductHistory } from '~/composables/useProductHistory'
|
||||||
import { formatProductStructurePreview, normalizeProductStructureForSave } from '~/shared/modelUtils'
|
import { formatProductStructurePreview, normalizeProductStructureForSave } from '~/shared/modelUtils'
|
||||||
import { uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
import { constructeurIdsFromLinks } from '~/shared/constructeurUtils'
|
||||||
|
import type { ConstructeurLinkEntry } from '~/shared/constructeurUtils'
|
||||||
import { getModelType } from '~/services/modelTypes'
|
import { getModelType } from '~/services/modelTypes'
|
||||||
import type { ProductModelStructure } from '~/shared/types/inventory'
|
import type { ProductModelStructure } from '~/shared/types/inventory'
|
||||||
import { canPreviewDocument } from '~/utils/documentPreview'
|
import { canPreviewDocument } from '~/utils/documentPreview'
|
||||||
@@ -263,7 +270,8 @@ const {
|
|||||||
deleteDocument: deleteProductDocument,
|
deleteDocument: deleteProductDocument,
|
||||||
updateDocument,
|
updateDocument,
|
||||||
} = useDocuments()
|
} = useDocuments()
|
||||||
const { ensureConstructeurs } = useConstructeurs()
|
const { ensureConstructeurs, getConstructeurById } = useConstructeurs()
|
||||||
|
const { fetchLinks, syncLinks } = useConstructeurLinks()
|
||||||
const {
|
const {
|
||||||
history,
|
history,
|
||||||
loading: historyLoading,
|
loading: historyLoading,
|
||||||
@@ -286,6 +294,9 @@ const previewVisible = ref(false)
|
|||||||
const editingDocument = ref<any | null>(null)
|
const editingDocument = ref<any | null>(null)
|
||||||
const editModalVisible = ref(false)
|
const editModalVisible = ref(false)
|
||||||
|
|
||||||
|
const constructeurLinks = ref<ConstructeurLinkEntry[]>([])
|
||||||
|
const originalConstructeurLinks = ref<ConstructeurLinkEntry[]>([])
|
||||||
|
|
||||||
const historyFieldLabels: Record<string, string> = {
|
const historyFieldLabels: Record<string, string> = {
|
||||||
name: 'Nom',
|
name: 'Nom',
|
||||||
reference: 'Référence',
|
reference: 'Référence',
|
||||||
@@ -457,18 +468,19 @@ const hydrateForm = () => {
|
|||||||
}
|
}
|
||||||
editionForm.name = product.value.name || ''
|
editionForm.name = product.value.name || ''
|
||||||
editionForm.reference = product.value.reference || ''
|
editionForm.reference = product.value.reference || ''
|
||||||
editionForm.constructeurIds = uniqueConstructeurIds(
|
// Load constructeur links
|
||||||
product.value,
|
fetchLinks('product', String(route.params.id)).then((links) => {
|
||||||
Array.isArray(product.value.constructeurs) ? product.value.constructeurs : [],
|
constructeurLinks.value = links
|
||||||
)
|
originalConstructeurLinks.value = links.map(l => ({ ...l }))
|
||||||
|
editionForm.constructeurIds = constructeurIdsFromLinks(links)
|
||||||
|
if (editionForm.constructeurIds.length) {
|
||||||
|
void ensureConstructeurs(editionForm.constructeurIds)
|
||||||
|
}
|
||||||
|
})
|
||||||
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)
|
||||||
: ''
|
: ''
|
||||||
refreshCustomFieldInputs(structure.value, product.value.customFieldValues)
|
refreshCustomFieldInputs(structure.value, product.value.customFieldValues)
|
||||||
if (editionForm.constructeurIds.length) {
|
|
||||||
// Smart-cached + deduped — fire-and-forget, ConstructeurSelect handles its own loading
|
|
||||||
ensureConstructeurs(editionForm.constructeurIds).catch(() => {})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@@ -486,12 +498,9 @@ const submitEdition = async () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const constructeurIds = uniqueConstructeurIds(editionForm.constructeurIds)
|
|
||||||
|
|
||||||
const payload: Record<string, any> = {
|
const payload: Record<string, any> = {
|
||||||
name: editionForm.name.trim(),
|
name: editionForm.name.trim(),
|
||||||
reference: editionForm.reference.trim() || null,
|
reference: editionForm.reference.trim() || null,
|
||||||
constructeurIds,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const rawPrice = typeof editionForm.supplierPrice === 'string'
|
const rawPrice = typeof editionForm.supplierPrice === 'string'
|
||||||
@@ -518,6 +527,8 @@ const submitEdition = async () => {
|
|||||||
toast.showError(`Impossible d'enregistrer ${failedFields.length} champ(s): ${failedFields.join(', ')}`)
|
toast.showError(`Impossible d'enregistrer ${failedFields.length} champ(s): ${failedFields.join(', ')}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
await syncLinks('product', product.value.id, originalConstructeurLinks.value, constructeurLinks.value)
|
||||||
|
originalConstructeurLinks.value = constructeurLinks.value.map(l => ({ ...l }))
|
||||||
toast.showSuccess('Produit mis à jour avec succès')
|
toast.showSuccess('Produit mis à jour avec succès')
|
||||||
versionRefreshKey.value++
|
versionRefreshKey.value++
|
||||||
}
|
}
|
||||||
@@ -528,6 +539,25 @@ const submitEdition = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sync constructeurIds → constructeurLinks when IDs are added via ConstructeurSelect
|
||||||
|
watch(
|
||||||
|
() => editionForm.constructeurIds,
|
||||||
|
(ids) => {
|
||||||
|
const currentIds = new Set(constructeurLinks.value.map(l => l.constructeurId))
|
||||||
|
for (const id of ids) {
|
||||||
|
if (!currentIds.has(id)) {
|
||||||
|
const resolved = getConstructeurById(id)
|
||||||
|
constructeurLinks.value.push({
|
||||||
|
constructeurId: id,
|
||||||
|
constructeur: resolved ? { id: resolved.id, name: resolved.name } : null,
|
||||||
|
supplierReference: null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
constructeurLinks.value = constructeurLinks.value.filter(l => ids.includes(l.constructeurId))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadProduct()
|
await loadProduct()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -133,6 +133,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Constructeur links table -->
|
||||||
|
<ConstructeurLinksTable
|
||||||
|
v-if="isEditMode && constructeurLinks.length"
|
||||||
|
v-model="constructeurLinks"
|
||||||
|
/>
|
||||||
|
<ConstructeurLinksTable
|
||||||
|
v-else-if="!isEditMode && constructeurLinks.length"
|
||||||
|
:model-value="constructeurLinks"
|
||||||
|
:readonly="true"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Prix fournisseur (if value or edit mode) -->
|
<!-- Prix fournisseur (if value or edit mode) -->
|
||||||
<div v-if="isEditMode || product.supplierPrice" class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
<div v-if="isEditMode || product.supplierPrice" class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
@@ -303,10 +314,12 @@ import { useToast } from '~/composables/useToast'
|
|||||||
import { humanizeError } from '~/shared/utils/errorMessages'
|
import { humanizeError } from '~/shared/utils/errorMessages'
|
||||||
import { useDocuments } from '~/composables/useDocuments'
|
import { useDocuments } from '~/composables/useDocuments'
|
||||||
import { useConstructeurs } from '~/composables/useConstructeurs'
|
import { useConstructeurs } from '~/composables/useConstructeurs'
|
||||||
|
import { useConstructeurLinks } from '~/composables/useConstructeurLinks'
|
||||||
import { useProductHistory } from '~/composables/useProductHistory'
|
import { useProductHistory } from '~/composables/useProductHistory'
|
||||||
import { usePermissions } from '~/composables/usePermissions'
|
import { usePermissions } from '~/composables/usePermissions'
|
||||||
import { formatProductStructurePreview, normalizeProductStructureForSave } from '~/shared/modelUtils'
|
import { formatProductStructurePreview, normalizeProductStructureForSave } from '~/shared/modelUtils'
|
||||||
import { uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
import { constructeurIdsFromLinks } from '~/shared/constructeurUtils'
|
||||||
|
import type { ConstructeurLinkEntry } from '~/shared/constructeurUtils'
|
||||||
import { getModelType } from '~/services/modelTypes'
|
import { getModelType } from '~/services/modelTypes'
|
||||||
import type { ProductModelStructure } from '~/shared/types/inventory'
|
import type { ProductModelStructure } from '~/shared/types/inventory'
|
||||||
import { canPreviewDocument } from '~/utils/documentPreview'
|
import { canPreviewDocument } from '~/utils/documentPreview'
|
||||||
@@ -330,6 +343,7 @@ const {
|
|||||||
updateDocument,
|
updateDocument,
|
||||||
} = useDocuments()
|
} = useDocuments()
|
||||||
const { ensureConstructeurs, getConstructeurById } = useConstructeurs()
|
const { ensureConstructeurs, getConstructeurById } = useConstructeurs()
|
||||||
|
const { fetchLinks, syncLinks } = useConstructeurLinks()
|
||||||
const {
|
const {
|
||||||
history,
|
history,
|
||||||
loading: historyLoading,
|
loading: historyLoading,
|
||||||
@@ -339,6 +353,9 @@ const {
|
|||||||
|
|
||||||
const isEditMode = ref(false)
|
const isEditMode = ref(false)
|
||||||
|
|
||||||
|
const constructeurLinks = ref<ConstructeurLinkEntry[]>([])
|
||||||
|
const originalConstructeurLinks = ref<ConstructeurLinkEntry[]>([])
|
||||||
|
|
||||||
const product = ref<any | null>(null)
|
const product = ref<any | null>(null)
|
||||||
const productType = ref<any | null>(null)
|
const productType = ref<any | null>(null)
|
||||||
const structure = ref<ProductModelStructure | null>(null)
|
const structure = ref<ProductModelStructure | null>(null)
|
||||||
@@ -529,17 +546,19 @@ const hydrateForm = () => {
|
|||||||
}
|
}
|
||||||
editionForm.name = product.value.name || ''
|
editionForm.name = product.value.name || ''
|
||||||
editionForm.reference = product.value.reference || ''
|
editionForm.reference = product.value.reference || ''
|
||||||
editionForm.constructeurIds = uniqueConstructeurIds(
|
// Load constructeur links
|
||||||
product.value,
|
fetchLinks('product', String(route.params.id)).then((links) => {
|
||||||
Array.isArray(product.value.constructeurs) ? product.value.constructeurs : [],
|
constructeurLinks.value = links
|
||||||
)
|
originalConstructeurLinks.value = links.map(l => ({ ...l }))
|
||||||
|
editionForm.constructeurIds = constructeurIdsFromLinks(links)
|
||||||
|
if (editionForm.constructeurIds.length) {
|
||||||
|
void ensureConstructeurs(editionForm.constructeurIds)
|
||||||
|
}
|
||||||
|
})
|
||||||
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)
|
||||||
: ''
|
: ''
|
||||||
refreshCustomFieldInputs(structure.value, product.value.customFieldValues)
|
refreshCustomFieldInputs(structure.value, product.value.customFieldValues)
|
||||||
if (editionForm.constructeurIds.length) {
|
|
||||||
ensureConstructeurs(editionForm.constructeurIds).catch(() => {})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@@ -557,12 +576,9 @@ const submitEdition = async () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const constructeurIds = uniqueConstructeurIds(editionForm.constructeurIds)
|
|
||||||
|
|
||||||
const payload: Record<string, any> = {
|
const payload: Record<string, any> = {
|
||||||
name: editionForm.name.trim(),
|
name: editionForm.name.trim(),
|
||||||
reference: editionForm.reference.trim() || null,
|
reference: editionForm.reference.trim() || null,
|
||||||
constructeurIds,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const rawPrice = typeof editionForm.supplierPrice === 'string'
|
const rawPrice = typeof editionForm.supplierPrice === 'string'
|
||||||
@@ -589,6 +605,8 @@ const submitEdition = async () => {
|
|||||||
toast.showError(`Impossible d'enregistrer ${failedFields.length} champ(s): ${failedFields.join(', ')}`)
|
toast.showError(`Impossible d'enregistrer ${failedFields.length} champ(s): ${failedFields.join(', ')}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
await syncLinks('product', product.value.id, originalConstructeurLinks.value, constructeurLinks.value)
|
||||||
|
originalConstructeurLinks.value = constructeurLinks.value.map(l => ({ ...l }))
|
||||||
toast.showSuccess('Produit mis à jour avec succès')
|
toast.showSuccess('Produit mis à jour avec succès')
|
||||||
await loadProduct()
|
await loadProduct()
|
||||||
isEditMode.value = false
|
isEditMode.value = false
|
||||||
@@ -600,6 +618,25 @@ const submitEdition = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sync constructeurIds → constructeurLinks when IDs are added via ConstructeurSelect
|
||||||
|
watch(
|
||||||
|
() => editionForm.constructeurIds,
|
||||||
|
(ids) => {
|
||||||
|
const currentIds = new Set(constructeurLinks.value.map(l => l.constructeurId))
|
||||||
|
for (const id of ids) {
|
||||||
|
if (!currentIds.has(id)) {
|
||||||
|
const resolved = getConstructeurById(id)
|
||||||
|
constructeurLinks.value.push({
|
||||||
|
constructeurId: id,
|
||||||
|
constructeur: resolved ? { id: resolved.id, name: resolved.name } : null,
|
||||||
|
supplierReference: null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
constructeurLinks.value = constructeurLinks.value.filter(l => ids.includes(l.constructeurId))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadProduct()
|
await loadProduct()
|
||||||
if (route.query.edit === 'true' && canEdit.value) {
|
if (route.query.edit === 'true' && canEdit.value) {
|
||||||
|
|||||||
@@ -79,6 +79,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ConstructeurLinksTable
|
||||||
|
v-if="constructeurLinks.length"
|
||||||
|
v-model="constructeurLinks"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label">
|
<label class="label">
|
||||||
@@ -175,7 +180,10 @@ 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'
|
||||||
import { formatProductStructurePreview, normalizeProductStructureForSave } from '~/shared/modelUtils'
|
import { formatProductStructurePreview, normalizeProductStructureForSave } from '~/shared/modelUtils'
|
||||||
import { uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
import { constructeurIdsFromLinks } from '~/shared/constructeurUtils'
|
||||||
|
import type { ConstructeurLinkEntry } from '~/shared/constructeurUtils'
|
||||||
|
import { useConstructeurLinks } from '~/composables/useConstructeurLinks'
|
||||||
|
import { useConstructeurs } from '~/composables/useConstructeurs'
|
||||||
import type { ProductModelStructure } from '~/shared/types/inventory'
|
import type { ProductModelStructure } from '~/shared/types/inventory'
|
||||||
import type { ModelType } from '~/services/modelTypes'
|
import type { ModelType } from '~/services/modelTypes'
|
||||||
import {
|
import {
|
||||||
@@ -197,6 +205,8 @@ const toast = useToast()
|
|||||||
const { upsertCustomFieldValue } = useCustomFields()
|
const { upsertCustomFieldValue } = useCustomFields()
|
||||||
const { uploadDocuments } = useDocuments()
|
const { uploadDocuments } = useDocuments()
|
||||||
const { canEdit } = usePermissions()
|
const { canEdit } = usePermissions()
|
||||||
|
const { syncLinks } = useConstructeurLinks()
|
||||||
|
const { getConstructeurById } = useConstructeurs()
|
||||||
|
|
||||||
const initialTypeId = ref<string>(typeof route.query.typeId === 'string' ? route.query.typeId : '')
|
const initialTypeId = ref<string>(typeof route.query.typeId === 'string' ? route.query.typeId : '')
|
||||||
const selectedTypeId = ref<string>(initialTypeId.value)
|
const selectedTypeId = ref<string>(initialTypeId.value)
|
||||||
@@ -207,6 +217,7 @@ const creationForm = reactive({
|
|||||||
constructeurIds: [] as string[],
|
constructeurIds: [] as string[],
|
||||||
supplierPrice: '' as string,
|
supplierPrice: '' as string,
|
||||||
})
|
})
|
||||||
|
const constructeurLinks = ref<ConstructeurLinkEntry[]>([])
|
||||||
const selectedDocuments = ref<File[]>([])
|
const selectedDocuments = ref<File[]>([])
|
||||||
const uploadingDocuments = ref(false)
|
const uploadingDocuments = ref(false)
|
||||||
|
|
||||||
@@ -300,8 +311,6 @@ const buildPayload = () => {
|
|||||||
payload.reference = reference
|
payload.reference = reference
|
||||||
}
|
}
|
||||||
|
|
||||||
payload.constructeurIds = uniqueConstructeurIds(creationForm.constructeurIds)
|
|
||||||
|
|
||||||
const rawPrice = typeof creationForm.supplierPrice === 'string'
|
const rawPrice = typeof creationForm.supplierPrice === 'string'
|
||||||
? creationForm.supplierPrice.trim()
|
? creationForm.supplierPrice.trim()
|
||||||
: creationForm.supplierPrice
|
: creationForm.supplierPrice
|
||||||
@@ -333,6 +342,10 @@ const submitCreation = async () => {
|
|||||||
await router.replace(`/product/${result.data.id}?edit=true`)
|
await router.replace(`/product/${result.data.id}?edit=true`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// Sync constructeur links after creation
|
||||||
|
if (constructeurLinks.value.length) {
|
||||||
|
await syncLinks('product', productId, [], constructeurLinks.value)
|
||||||
|
}
|
||||||
if (selectedDocuments.value.length) {
|
if (selectedDocuments.value.length) {
|
||||||
uploadingDocuments.value = true
|
uploadingDocuments.value = true
|
||||||
const uploadResult = await uploadDocuments(
|
const uploadResult = await uploadDocuments(
|
||||||
@@ -395,6 +408,25 @@ const saveCustomFieldValues = async (productId: string) => {
|
|||||||
return failed
|
return failed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sync constructeurIds → constructeurLinks when IDs are added via ConstructeurSelect
|
||||||
|
watch(
|
||||||
|
() => creationForm.constructeurIds,
|
||||||
|
(ids) => {
|
||||||
|
const currentIds = new Set(constructeurLinks.value.map(l => l.constructeurId))
|
||||||
|
for (const id of ids) {
|
||||||
|
if (!currentIds.has(id)) {
|
||||||
|
const resolved = getConstructeurById(id)
|
||||||
|
constructeurLinks.value.push({
|
||||||
|
constructeurId: id,
|
||||||
|
constructeur: resolved ? { id: resolved.id, name: resolved.name } : null,
|
||||||
|
supplierReference: null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
constructeurLinks.value = constructeurLinks.value.filter(l => ids.includes(l.constructeurId))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadProductTypes()
|
await loadProductTypes()
|
||||||
if (selectedTypeId.value && !selectedType.value) {
|
if (selectedTypeId.value && !selectedType.value) {
|
||||||
|
|||||||
@@ -23,11 +23,15 @@ export interface BaseModelTypePayload {
|
|||||||
export interface ComponentModelTypePayload extends BaseModelTypePayload {
|
export interface ComponentModelTypePayload extends BaseModelTypePayload {
|
||||||
category: 'COMPONENT';
|
category: 'COMPONENT';
|
||||||
structure?: ComponentModelStructure | null;
|
structure?: ComponentModelStructure | null;
|
||||||
|
referenceFormula?: string | null;
|
||||||
|
requiredFieldsForReference?: string[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PieceModelTypePayload extends BaseModelTypePayload {
|
export interface PieceModelTypePayload extends BaseModelTypePayload {
|
||||||
category: 'PIECE';
|
category: 'PIECE';
|
||||||
structure?: PieceModelStructure | null;
|
structure?: PieceModelStructure | null;
|
||||||
|
referenceFormula?: string | null;
|
||||||
|
requiredFieldsForReference?: string[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProductModelTypePayload extends BaseModelTypePayload {
|
export interface ProductModelTypePayload extends BaseModelTypePayload {
|
||||||
@@ -46,6 +50,8 @@ export interface ModelType extends BaseModelTypePayload {
|
|||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
category: ModelCategory;
|
category: ModelCategory;
|
||||||
structure: ModelTypeStructure;
|
structure: ModelTypeStructure;
|
||||||
|
referenceFormula?: string | null;
|
||||||
|
requiredFieldsForReference?: string[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ModelTypeListParams {
|
export interface ModelTypeListParams {
|
||||||
|
|||||||
@@ -7,6 +7,32 @@ export interface ConstructeurSummary {
|
|||||||
phone?: string | null;
|
phone?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ConstructeurLinkEntry {
|
||||||
|
linkId?: string;
|
||||||
|
constructeurId: string;
|
||||||
|
constructeur?: ConstructeurSummary | null;
|
||||||
|
supplierReference: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const constructeurIdsFromLinks = (links: ConstructeurLinkEntry[]): string[] =>
|
||||||
|
links.map(l => l.constructeurId).filter(Boolean);
|
||||||
|
|
||||||
|
export const parseConstructeurLinksFromApi = (
|
||||||
|
apiLinks: any[],
|
||||||
|
): ConstructeurLinkEntry[] => {
|
||||||
|
if (!Array.isArray(apiLinks)) return [];
|
||||||
|
return apiLinks
|
||||||
|
.filter(link => link && typeof link === 'object')
|
||||||
|
.map(link => ({
|
||||||
|
linkId: link.id || (typeof link['@id'] === 'string' ? link['@id'].split('/').pop() : undefined),
|
||||||
|
constructeurId: typeof link.constructeur === 'string'
|
||||||
|
? link.constructeur.split('/').pop()!
|
||||||
|
: link.constructeur?.id || '',
|
||||||
|
constructeur: typeof link.constructeur === 'object' ? link.constructeur : null,
|
||||||
|
supplierReference: link.supplierReference ?? null,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
const isObject = (value: unknown): value is Record<string, unknown> =>
|
const isObject = (value: unknown): value is Record<string, unknown> =>
|
||||||
Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
||||||
|
|
||||||
@@ -113,49 +139,3 @@ export const formatConstructeurContact = (
|
|||||||
return [constructeur.email, phone].filter(Boolean).join(' • ');
|
return [constructeur.email, phone].filter(Boolean).join(' • ');
|
||||||
};
|
};
|
||||||
|
|
||||||
export const buildConstructeurRequestPayload = <T extends Record<string, any>>(
|
|
||||||
payload: T,
|
|
||||||
): T & { constructeurs?: string[] } => {
|
|
||||||
const collected = new Set(uniqueConstructeurIds(
|
|
||||||
payload?.constructeurIds,
|
|
||||||
payload?.constructeurId,
|
|
||||||
payload?.constructeur,
|
|
||||||
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>;
|
|
||||||
delete next.constructeurId;
|
|
||||||
delete next.constructeur;
|
|
||||||
delete next.constructeurs;
|
|
||||||
delete next.constructeurIds;
|
|
||||||
|
|
||||||
next.constructeurs = ids.map((id) => `/api/constructeurs/${id}`);
|
|
||||||
|
|
||||||
return next as T & { constructeurs?: string[] };
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -52,10 +52,10 @@ export default defineNuxtConfig({
|
|||||||
appVersion: appVersion,
|
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 || 'false',
|
||||||
enableAnalytics: process.env.NUXT_PUBLIC_ENABLE_ANALYTICS || 'false',
|
enableAnalytics: process.env.NUXT_PUBLIC_ENABLE_ANALYTICS || 'false',
|
||||||
csrfToken: process.env.NUXT_PUBLIC_CSRF_TOKEN || '',
|
csrfToken: process.env.NUXT_PUBLIC_CSRF_TOKEN || '',
|
||||||
logLevel: process.env.NUXT_PUBLIC_LOG_LEVEL || 'debug'
|
logLevel: process.env.NUXT_PUBLIC_LOG_LEVEL || 'warn'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
vite: {
|
vite: {
|
||||||
|
|||||||
Reference in New Issue
Block a user