Compare commits
10 Commits
v1.9.0
...
5912216a89
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5912216a89 | ||
|
|
139ba183de | ||
|
|
9fef009610 | ||
|
|
4a3bceffa1 | ||
|
|
50d8dde6d5 | ||
|
|
9b40f9f2c7 | ||
|
|
721963449b | ||
|
|
22ba9a8d05 | ||
|
|
695d56a6d3 | ||
|
|
5c31045e83 |
@@ -215,14 +215,14 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Component Pieces -->
|
<!-- Component Pieces (real MachinePieceLinks) -->
|
||||||
<div v-if="component.pieces && component.pieces.length > 0" class="space-y-2">
|
<div v-if="linkedPieces.length > 0" class="space-y-2">
|
||||||
<p class="text-xs font-semibold text-base-content/50 uppercase tracking-wide">
|
<p class="text-xs font-semibold text-base-content/50 uppercase tracking-wide">
|
||||||
Pièces du composant
|
Pièces du composant
|
||||||
</p>
|
</p>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<PieceItem
|
<PieceItem
|
||||||
v-for="piece in component.pieces"
|
v-for="piece in linkedPieces"
|
||||||
:key="piece.id"
|
:key="piece.id"
|
||||||
:piece="piece"
|
:piece="piece"
|
||||||
:is-edit-mode="isEditMode"
|
:is-edit-mode="isEditMode"
|
||||||
@@ -233,6 +233,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Structure pieces (read-only, from composant definition) -->
|
||||||
|
<div v-if="structurePieces.length > 0" class="space-y-2">
|
||||||
|
<p class="text-xs font-semibold text-base-content/50 uppercase tracking-wide">
|
||||||
|
Pièces incluses par défaut
|
||||||
|
</p>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<PieceItem
|
||||||
|
v-for="piece in structurePieces"
|
||||||
|
:key="piece.id"
|
||||||
|
:piece="piece"
|
||||||
|
:is-edit-mode="false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Sub Components -->
|
<!-- Sub Components -->
|
||||||
<div v-if="childComponents.length > 0" class="space-y-2">
|
<div v-if="childComponents.length > 0" class="space-y-2">
|
||||||
<p class="text-xs font-semibold text-base-content/50 uppercase tracking-wide">
|
<p class="text-xs font-semibold text-base-content/50 uppercase tracking-wide">
|
||||||
@@ -341,6 +356,14 @@ const childComponents = computed(() => {
|
|||||||
return Array.isArray(list) ? list : []
|
return Array.isArray(list) ? list : []
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// --- Pieces split: real links vs structure definitions ---
|
||||||
|
const allPieces = computed(() => {
|
||||||
|
const list = props.component.pieces
|
||||||
|
return Array.isArray(list) ? list : []
|
||||||
|
})
|
||||||
|
const linkedPieces = computed(() => allPieces.value.filter((p) => !p._structurePiece))
|
||||||
|
const structurePieces = computed(() => allPieces.value.filter((p) => p._structurePiece))
|
||||||
|
|
||||||
// --- Constructeurs ---
|
// --- Constructeurs ---
|
||||||
const { constructeurs } = useConstructeurs()
|
const { constructeurs } = useConstructeurs()
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,12 @@
|
|||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<h3 class="text-lg font-semibold">
|
<h3 class="text-lg font-semibold">
|
||||||
{{ pieceData.name }}
|
{{ pieceData.name }}
|
||||||
|
<span
|
||||||
|
v-if="displayQuantity > 1"
|
||||||
|
class="text-sm font-normal text-base-content/60 ml-1"
|
||||||
|
>
|
||||||
|
×{{ displayQuantity }}
|
||||||
|
</span>
|
||||||
</h3>
|
</h3>
|
||||||
<div class="flex flex-wrap gap-2 mt-2">
|
<div class="flex flex-wrap gap-2 mt-2">
|
||||||
<span v-if="piece.parentComponentName" class="badge badge-ghost badge-sm">
|
<span v-if="piece.parentComponentName" class="badge badge-ghost badge-sm">
|
||||||
@@ -63,6 +69,23 @@
|
|||||||
<div v-show="!isCollapsed" class="space-y-4">
|
<div v-show="!isCollapsed" class="space-y-4">
|
||||||
<div class="p-4 bg-base-100 border border-base-200 rounded-lg">
|
<div class="p-4 bg-base-100 border border-base-200 rounded-lg">
|
||||||
<div class="space-y-2 text-sm">
|
<div class="space-y-2 text-sm">
|
||||||
|
<div v-if="isEditMode" class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text text-sm">Quantité</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
v-model.number="pieceData.quantity"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
step="1"
|
||||||
|
class="input input-bordered input-sm md:input-md w-24"
|
||||||
|
@blur="updatePiece"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="displayQuantity > 1">
|
||||||
|
<span class="font-medium">Quantité:</span>
|
||||||
|
<span class="ml-2">{{ displayQuantity }}</span>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span class="font-medium">Référence:</span>
|
<span class="font-medium">Référence:</span>
|
||||||
<input
|
<input
|
||||||
@@ -272,6 +295,11 @@ const pieceData = reactive({
|
|||||||
reference: props.piece.reference || '',
|
reference: props.piece.reference || '',
|
||||||
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,
|
||||||
|
})
|
||||||
|
|
||||||
|
const displayQuantity = computed(() => {
|
||||||
|
return pieceData.quantity ?? 1
|
||||||
})
|
})
|
||||||
|
|
||||||
// --- Products ---
|
// --- Products ---
|
||||||
@@ -432,13 +460,14 @@ const updatePiece = () => {
|
|||||||
let parsedPrice = null
|
let parsedPrice = null
|
||||||
if (prixValue !== null && prixValue !== undefined && String(prixValue).trim().length > 0) {
|
if (prixValue !== null && prixValue !== undefined && String(prixValue).trim().length > 0) {
|
||||||
const numeric = Number(prixValue)
|
const numeric = Number(prixValue)
|
||||||
if (!Number.isNaN(numeric)) parsedPrice = numeric
|
if (!Number.isNaN(numeric)) parsedPrice = String(numeric)
|
||||||
}
|
}
|
||||||
const product = selectedProduct.value ? { ...selectedProduct.value } : null
|
const product = selectedProduct.value ? { ...selectedProduct.value } : null
|
||||||
emit('update', {
|
emit('update', {
|
||||||
...props.piece,
|
...props.piece,
|
||||||
...pieceData,
|
...pieceData,
|
||||||
prix: parsedPrice,
|
prix: parsedPrice,
|
||||||
|
quantity: pieceData.quantity ?? 1,
|
||||||
productId: pieceData.productId || null,
|
productId: pieceData.productId || null,
|
||||||
product,
|
product,
|
||||||
constructeurIds: pieceConstructeurIds.value,
|
constructeurIds: pieceConstructeurIds.value,
|
||||||
@@ -478,11 +507,12 @@ watch(
|
|||||||
)
|
)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => [props.piece.name, props.piece.reference, props.piece.prix],
|
() => [props.piece.name, props.piece.reference, props.piece.prix, props.piece.quantity],
|
||||||
() => {
|
() => {
|
||||||
pieceData.name = props.piece.name || ''
|
pieceData.name = props.piece.name || ''
|
||||||
pieceData.reference = props.piece.reference || ''
|
pieceData.reference = props.piece.reference || ''
|
||||||
pieceData.prix = props.piece.prix || ''
|
pieceData.prix = props.piece.prix || ''
|
||||||
|
pieceData.quantity = props.piece.quantity ?? 1
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -490,6 +520,7 @@ onMounted(() => {
|
|||||||
pieceData.name = props.piece.name || ''
|
pieceData.name = props.piece.name || ''
|
||||||
pieceData.reference = props.piece.reference || ''
|
pieceData.reference = props.piece.reference || ''
|
||||||
pieceData.prix = props.piece.prix || ''
|
pieceData.prix = props.piece.prix || ''
|
||||||
|
pieceData.quantity = props.piece.quantity ?? 1
|
||||||
loadProducts().catch(() => {})
|
loadProducts().catch(() => {})
|
||||||
if (pieceData.productId) ensureProductLoaded(pieceData.productId)
|
if (pieceData.productId) ensureProductLoaded(pieceData.productId)
|
||||||
if (!props.piece.documents?.length) refreshDocuments()
|
if (!props.piece.documents?.length) refreshDocuments()
|
||||||
|
|||||||
@@ -280,6 +280,18 @@
|
|||||||
{{ piece.typePieceId ? `Sélection : ${getPieceTypeLabel(piece.typePieceId) || 'Inconnue'}` : 'Aucune famille sélectionnée' }}
|
{{ piece.typePieceId ? `Sélection : ${getPieceTypeLabel(piece.typePieceId) || 'Inconnue'}` : 'Aucune famille sélectionnée' }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label py-1"><span class="label-text text-xs">Quantité</span></label>
|
||||||
|
<input
|
||||||
|
v-model.number="piece.quantity"
|
||||||
|
type="number"
|
||||||
|
:min="1"
|
||||||
|
step="1"
|
||||||
|
placeholder="Qté"
|
||||||
|
class="input input-bordered input-sm md:input-md w-20"
|
||||||
|
@input="piece.quantity = Math.max(1, piece.quantity || 1)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button v-if="!isPieceLocked(index)" type="button" class="btn btn-error btn-xs btn-square" @click="removePiece(index)">
|
<button v-if="!isPieceLocked(index)" type="button" class="btn btn-error btn-xs btn-square" @click="removePiece(index)">
|
||||||
<IconLucideTrash class="w-4 h-4" aria-hidden="true" />
|
<IconLucideTrash class="w-4 h-4" aria-hidden="true" />
|
||||||
|
|||||||
@@ -72,23 +72,23 @@
|
|||||||
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="input input-bordered bg-base-200">
|
<div v-else class="border border-base-300 rounded-btn bg-base-200 px-4 py-2 min-h-12 flex items-center">
|
||||||
<div v-if="machineConstructeursDisplay.length" class="space-y-1">
|
<div v-if="machineConstructeursDisplay.length" class="flex flex-wrap gap-2">
|
||||||
<div
|
<span
|
||||||
v-for="constructeur in machineConstructeursDisplay"
|
v-for="constructeur in machineConstructeursDisplay"
|
||||||
:key="constructeur.id"
|
:key="constructeur.id"
|
||||||
class="flex flex-col"
|
class="badge badge-ghost gap-1"
|
||||||
>
|
>
|
||||||
<span class="font-medium">{{ constructeur.name }}</span>
|
{{ constructeur.name }}
|
||||||
<span
|
<span
|
||||||
v-if="formatConstructeurContactSummary(constructeur)"
|
v-if="formatConstructeurContactSummary(constructeur)"
|
||||||
class="text-xs text-base-content/50"
|
class="text-xs opacity-60"
|
||||||
>
|
>
|
||||||
{{ formatConstructeurContactSummary(constructeur) }}
|
· {{ formatConstructeurContactSummary(constructeur) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span v-else class="font-medium">Non défini</span>
|
<span v-else class="text-base-content/50">Non défini</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -28,10 +28,11 @@
|
|||||||
<div v-for="piece in pieces" :key="piece.id">
|
<div v-for="piece in pieces" :key="piece.id">
|
||||||
<PieceItem
|
<PieceItem
|
||||||
:piece="piece"
|
:piece="piece"
|
||||||
:is-edit-mode="false"
|
:is-edit-mode="isEditMode"
|
||||||
:show-delete="isEditMode"
|
:show-delete="isEditMode"
|
||||||
:collapse-all="collapsed"
|
:collapse-all="collapsed"
|
||||||
:toggle-token="collapseToggleToken"
|
:toggle-token="collapseToggleToken"
|
||||||
|
@update="$emit('update-piece', $event)"
|
||||||
@edit="$emit('edit-piece', $event)"
|
@edit="$emit('edit-piece', $event)"
|
||||||
@delete="$emit('remove-piece', piece.linkId || piece.id)"
|
@delete="$emit('remove-piece', piece.linkId || piece.id)"
|
||||||
/>
|
/>
|
||||||
@@ -62,6 +63,7 @@ defineProps<{
|
|||||||
|
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
'toggle-collapse': []
|
'toggle-collapse': []
|
||||||
|
'update-piece': [piece: any]
|
||||||
'edit-piece': [piece: any]
|
'edit-piece': [piece: any]
|
||||||
'add-piece': []
|
'add-piece': []
|
||||||
'remove-piece': [linkId: string]
|
'remove-piece': [linkId: string]
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useProductTypes } from '~/composables/useProductTypes'
|
|||||||
import { usePieces } from '~/composables/usePieces'
|
import { usePieces } from '~/composables/usePieces'
|
||||||
import { useProducts } from '~/composables/useProducts'
|
import { useProducts } from '~/composables/useProducts'
|
||||||
import { useCustomFields } from '~/composables/useCustomFields'
|
import { useCustomFields } from '~/composables/useCustomFields'
|
||||||
|
import type { SelectionEntry } from '~/shared/utils/structureSelectionUtils'
|
||||||
import { useApi } from '~/composables/useApi'
|
import { useApi } from '~/composables/useApi'
|
||||||
import { useToast } from '~/composables/useToast'
|
import { useToast } from '~/composables/useToast'
|
||||||
import { extractRelationId } from '~/shared/apiRelations'
|
import { extractRelationId } from '~/shared/apiRelations'
|
||||||
@@ -53,7 +54,7 @@ const historyFieldLabels: Record<string, string> = {
|
|||||||
export function useComponentEdit(componentId: string) {
|
export function useComponentEdit(componentId: string) {
|
||||||
const { canEdit } = usePermissions()
|
const { canEdit } = usePermissions()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { get } = useApi()
|
const { get, patch } = useApi()
|
||||||
const { componentTypes, loadComponentTypes } = useComponentTypes()
|
const { componentTypes, loadComponentTypes } = useComponentTypes()
|
||||||
const { pieceTypes, loadPieceTypes } = usePieceTypes()
|
const { pieceTypes, loadPieceTypes } = usePieceTypes()
|
||||||
const { productTypes, loadProductTypes } = useProductTypes()
|
const { productTypes, loadProductTypes } = useProductTypes()
|
||||||
@@ -269,6 +270,21 @@ export function useComponentEdit(componentId: string) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const saveSlotQuantity = async (entry: SelectionEntry) => {
|
||||||
|
const slotId = entry.slotId
|
||||||
|
const quantity = typeof entry._definition?.quantity === 'number'
|
||||||
|
? Math.max(1, entry._definition.quantity)
|
||||||
|
: null
|
||||||
|
if (!slotId || quantity === null) return
|
||||||
|
try {
|
||||||
|
await patch(`/composant-piece-slots/${slotId}`, { quantity })
|
||||||
|
toast.showSuccess('Quantité mise à jour')
|
||||||
|
}
|
||||||
|
catch (error: any) {
|
||||||
|
toast.showError(error?.message || 'Erreur lors de la mise à jour de la quantité')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const submitEdition = async () => {
|
const submitEdition = async () => {
|
||||||
if (!component.value) {
|
if (!component.value) {
|
||||||
return
|
return
|
||||||
@@ -453,6 +469,7 @@ export function useComponentEdit(componentId: string) {
|
|||||||
handleFilesAdded,
|
handleFilesAdded,
|
||||||
refreshDocuments,
|
refreshDocuments,
|
||||||
submitEdition,
|
submitEdition,
|
||||||
|
saveSlotQuantity,
|
||||||
resolvePieceLabel,
|
resolvePieceLabel,
|
||||||
resolveProductLabel,
|
resolveProductLabel,
|
||||||
resolveSubcomponentLabel,
|
resolveSubcomponentLabel,
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export function useEntityDocuments(deps: EntityDocumentsDeps) {
|
|||||||
// CRUD operations
|
// CRUD operations
|
||||||
const refreshDocuments = async () => {
|
const refreshDocuments = async () => {
|
||||||
const e = entity()
|
const e = entity()
|
||||||
if (!e?.id) return
|
if (!e?.id || e._structurePiece) return
|
||||||
loadingDocuments.value = true
|
loadingDocuments.value = true
|
||||||
try {
|
try {
|
||||||
const result: any = await loadDocumentsFn(e.id, { updateStore: false })
|
const result: any = await loadDocumentsFn(e.id, { updateStore: false })
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export function useMachineDetailData(machineId: string) {
|
|||||||
const { componentTypes, loadComponentTypes } = useComponentTypes()
|
const { componentTypes, loadComponentTypes } = useComponentTypes()
|
||||||
const { pieceTypes, loadPieceTypes } = usePieceTypes()
|
const { pieceTypes, loadPieceTypes } = usePieceTypes()
|
||||||
const { upsertCustomFieldValue } = useCustomFields()
|
const { upsertCustomFieldValue } = useCustomFields()
|
||||||
const { get } = useApi()
|
const { get, patch: apiPatch } = useApi()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const { constructeurs, loadConstructeurs } = useConstructeurs()
|
const { constructeurs, loadConstructeurs } = useConstructeurs()
|
||||||
const { sites, loadSites } = useSites()
|
const { sites, loadSites } = useSites()
|
||||||
@@ -274,6 +274,7 @@ export function useMachineDetailData(machineId: string) {
|
|||||||
updateMachineApi,
|
updateMachineApi,
|
||||||
updateComposantApi: updateComposantApi,
|
updateComposantApi: updateComposantApi,
|
||||||
updatePieceApi,
|
updatePieceApi,
|
||||||
|
apiPatch,
|
||||||
toast,
|
toast,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export interface UseMachineDetailUpdatesDeps {
|
|||||||
updateMachineApi: (id: string, data: any) => Promise<unknown>
|
updateMachineApi: (id: string, data: any) => Promise<unknown>
|
||||||
updateComposantApi: (id: string, data: any) => Promise<unknown>
|
updateComposantApi: (id: string, data: any) => Promise<unknown>
|
||||||
updatePieceApi: (id: string, data: any) => Promise<unknown>
|
updatePieceApi: (id: string, data: any) => Promise<unknown>
|
||||||
|
apiPatch: (endpoint: string, data?: unknown) => Promise<any>
|
||||||
toast: { showInfo: (msg: string) => void }
|
toast: { showInfo: (msg: string) => void }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,6 +54,7 @@ export function useMachineDetailUpdates(deps: UseMachineDetailUpdatesDeps) {
|
|||||||
updateMachineApi,
|
updateMachineApi,
|
||||||
updateComposantApi,
|
updateComposantApi,
|
||||||
updatePieceApi,
|
updatePieceApi,
|
||||||
|
apiPatch,
|
||||||
toast,
|
toast,
|
||||||
} = deps
|
} = deps
|
||||||
|
|
||||||
@@ -108,18 +110,18 @@ export function useMachineDetailUpdates(deps: UseMachineDetailUpdatesDeps) {
|
|||||||
const productId = updatedComponent.productId
|
const productId = updatedComponent.productId
|
||||||
? String(updatedComponent.productId)
|
? String(updatedComponent.productId)
|
||||||
: null
|
: null
|
||||||
const prix =
|
const prixStr =
|
||||||
updatedComponent.prix !== null &&
|
updatedComponent.prix !== null &&
|
||||||
updatedComponent.prix !== undefined &&
|
updatedComponent.prix !== undefined &&
|
||||||
String(updatedComponent.prix).trim() !== ''
|
String(updatedComponent.prix).trim() !== ''
|
||||||
? Number(updatedComponent.prix)
|
? String(updatedComponent.prix)
|
||||||
: null
|
: null
|
||||||
|
|
||||||
const result: any = await updateComposantApi(updatedComponent.id as string, {
|
const result: any = await updateComposantApi(updatedComponent.id as string, {
|
||||||
name: updatedComponent.name,
|
name: updatedComponent.name,
|
||||||
reference: updatedComponent.reference,
|
reference: updatedComponent.reference,
|
||||||
constructeurIds: cIds,
|
constructeurIds: cIds,
|
||||||
prix: Number.isNaN(prix) ? null : prix,
|
prix: prixStr,
|
||||||
productId,
|
productId,
|
||||||
} as any)
|
} as any)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
@@ -138,18 +140,18 @@ export function useMachineDetailUpdates(deps: UseMachineDetailUpdatesDeps) {
|
|||||||
updatedPiece.constructeur,
|
updatedPiece.constructeur,
|
||||||
)
|
)
|
||||||
const productId = updatedPiece.productId ? String(updatedPiece.productId) : null
|
const productId = updatedPiece.productId ? String(updatedPiece.productId) : null
|
||||||
const prix =
|
const prixStr =
|
||||||
updatedPiece.prix !== null &&
|
updatedPiece.prix !== null &&
|
||||||
updatedPiece.prix !== undefined &&
|
updatedPiece.prix !== undefined &&
|
||||||
String(updatedPiece.prix).trim() !== ''
|
String(updatedPiece.prix).trim() !== ''
|
||||||
? Number(updatedPiece.prix)
|
? String(updatedPiece.prix)
|
||||||
: null
|
: null
|
||||||
|
|
||||||
const result: any = await updatePieceApi(updatedPiece.id as string, {
|
const result: any = await updatePieceApi(updatedPiece.id as string, {
|
||||||
name: updatedPiece.name,
|
name: updatedPiece.name,
|
||||||
reference: updatedPiece.reference,
|
reference: updatedPiece.reference || null,
|
||||||
constructeurIds: cIds,
|
constructeurIds: cIds,
|
||||||
prix: Number.isNaN(prix) ? null : prix,
|
prix: prixStr,
|
||||||
productId,
|
productId,
|
||||||
} as any)
|
} as any)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
@@ -179,6 +181,13 @@ export function useMachineDetailUpdates(deps: UseMachineDetailUpdatesDeps) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update slot quantity if this is a composant structure piece
|
||||||
|
const slotId = updatedPiece.slotId as string | null
|
||||||
|
const quantity = typeof updatedPiece.quantity === 'number' ? Math.max(1, updatedPiece.quantity) : null
|
||||||
|
if (slotId && quantity !== null) {
|
||||||
|
await apiPatch(`/composant-piece-slots/${slotId}`, { quantity })
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erreur lors de la mise à jour de la pièce:', error)
|
console.error('Erreur lors de la mise à jour de la pièce:', error)
|
||||||
}
|
}
|
||||||
@@ -187,6 +196,13 @@ export function useMachineDetailUpdates(deps: UseMachineDetailUpdatesDeps) {
|
|||||||
const updatePieceInfo = async (updatedPiece: AnyRecord) => {
|
const updatePieceInfo = async (updatedPiece: AnyRecord) => {
|
||||||
try {
|
try {
|
||||||
await _buildAndUpdatePiece(updatedPiece)
|
await _buildAndUpdatePiece(updatedPiece)
|
||||||
|
|
||||||
|
// Update link quantity if this is a direct machine piece
|
||||||
|
const linkId = updatedPiece.linkId || updatedPiece.machinePieceLinkId
|
||||||
|
const quantity = typeof updatedPiece.quantity === 'number' ? Math.max(1, updatedPiece.quantity) : null
|
||||||
|
if (linkId && quantity !== null) {
|
||||||
|
await apiPatch(`/machine_piece_links/${linkId}`, { quantity })
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erreur lors de la mise à jour de la pièce:', error)
|
console.error('Erreur lors de la mise à jour de la pièce:', error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -181,6 +181,7 @@ export const buildMachineHierarchyFromLinks = (
|
|||||||
parentLinkId: resolveIdentifier(link.parentLinkId, link.parentMachinePieceLinkId, appliedPiece.parentLinkId),
|
parentLinkId: resolveIdentifier(link.parentLinkId, link.parentMachinePieceLinkId, appliedPiece.parentLinkId),
|
||||||
parentPieceLinkId: resolveIdentifier(link.parentPieceLinkId, appliedPiece.parentPieceLinkId),
|
parentPieceLinkId: resolveIdentifier(link.parentPieceLinkId, appliedPiece.parentPieceLinkId),
|
||||||
parentPieceId: resolveIdentifier(appliedPiece.parentPieceId, link.parentPieceId),
|
parentPieceId: resolveIdentifier(appliedPiece.parentPieceId, link.parentPieceId),
|
||||||
|
quantity: typeof link.quantity === 'number' ? link.quantity : 1,
|
||||||
definition: appliedPiece.definition || originalPiece?.definition || {},
|
definition: appliedPiece.definition || originalPiece?.definition || {},
|
||||||
customFields: appliedPiece.customFields || [],
|
customFields: appliedPiece.customFields || [],
|
||||||
}
|
}
|
||||||
@@ -214,10 +215,39 @@ export const buildMachineHierarchyFromLinks = (
|
|||||||
|
|
||||||
const componentName = (compOverrides?.name || appliedComponent.name || (appliedComponent.definition as AnyRecord)?.alias || (appliedComponent.definition as AnyRecord)?.name || originalComponent?.name || 'Composant') as string
|
const componentName = (compOverrides?.name || appliedComponent.name || (appliedComponent.definition as AnyRecord)?.alias || (appliedComponent.definition as AnyRecord)?.name || originalComponent?.name || 'Composant') as string
|
||||||
|
|
||||||
const pieces = Array.isArray(link.pieceLinks)
|
const linkedPieces = Array.isArray(link.pieceLinks)
|
||||||
? (link.pieceLinks as AnyRecord[]).map((pl) => createPieceNode(pl, componentName)).filter(Boolean) as AnyRecord[]
|
? (link.pieceLinks as AnyRecord[]).map((pl) => createPieceNode(pl, componentName)).filter(Boolean) as AnyRecord[]
|
||||||
: []
|
: []
|
||||||
|
|
||||||
|
// If no linked pieces exist, build read-only entries from the composant's structure
|
||||||
|
const structurePieceDefs = (!linkedPieces.length && appliedComponent.structure && typeof appliedComponent.structure === 'object')
|
||||||
|
? (Array.isArray((appliedComponent.structure as AnyRecord).pieces) ? (appliedComponent.structure as AnyRecord).pieces as AnyRecord[] : [])
|
||||||
|
: []
|
||||||
|
const structurePieces = structurePieceDefs.map((def, index) => {
|
||||||
|
const definition = (def.definition && typeof def.definition === 'object' ? def.definition : def) as AnyRecord
|
||||||
|
const resolved = (def.resolvedPiece && typeof def.resolvedPiece === 'object' ? def.resolvedPiece : null) as AnyRecord | null
|
||||||
|
const quantity = typeof definition.quantity === 'number' ? definition.quantity : (typeof def.quantity === 'number' ? def.quantity : 1)
|
||||||
|
return {
|
||||||
|
...(resolved || {}),
|
||||||
|
id: resolved?.id || `structure-piece-${composantId}-${index}`,
|
||||||
|
pieceId: resolved?.id || null,
|
||||||
|
name: resolved?.name || definition.role || definition.name || def.role || def.name || `Pièce ${index + 1}`,
|
||||||
|
reference: resolved?.reference || definition.reference || def.reference || null,
|
||||||
|
prix: resolved?.prix ?? null,
|
||||||
|
constructeurs: resolved?.constructeurs || [],
|
||||||
|
documents: [],
|
||||||
|
quantity,
|
||||||
|
slotId: def.slotId || definition.slotId || null,
|
||||||
|
typePieceId: resolved?.typePieceId || definition.typePieceId || def.typePieceId || null,
|
||||||
|
typePiece: resolved?.typePiece || null,
|
||||||
|
parentComponentLinkId: machineComponentLinkId,
|
||||||
|
parentComponentName: componentName,
|
||||||
|
_structurePiece: true,
|
||||||
|
}
|
||||||
|
}) as AnyRecord[]
|
||||||
|
|
||||||
|
const pieces = linkedPieces.length ? linkedPieces : structurePieces
|
||||||
|
|
||||||
const subComponents = Array.isArray(link.childLinks)
|
const subComponents = Array.isArray(link.childLinks)
|
||||||
? (link.childLinks as AnyRecord[]).map(createComponentNode).filter(Boolean) as AnyRecord[]
|
? (link.childLinks as AnyRecord[]).map(createComponentNode).filter(Boolean) as AnyRecord[]
|
||||||
: []
|
: []
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ export function useStructureNodeCrud(props: StructureNodeCrudDeps) {
|
|||||||
reference: '',
|
reference: '',
|
||||||
familyCode: '',
|
familyCode: '',
|
||||||
role: '',
|
role: '',
|
||||||
|
quantity: 1,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -166,9 +166,21 @@
|
|||||||
<div v-if="structureSelections.pieces.length" class="space-y-2">
|
<div v-if="structureSelections.pieces.length" class="space-y-2">
|
||||||
<h3 class="font-semibold text-sm text-base-content">Pièces choisies</h3>
|
<h3 class="font-semibold text-sm text-base-content">Pièces choisies</h3>
|
||||||
<ul class="list-disc list-inside space-y-1 text-sm">
|
<ul class="list-disc list-inside space-y-1 text-sm">
|
||||||
<li v-for="entry in structureSelections.pieces" :key="`selected-piece-${entry.path}-${entry.id}`">
|
<li v-for="entry in structureSelections.pieces" :key="`selected-piece-${entry.path}-${entry.id}`" class="flex items-center gap-2">
|
||||||
<span class="font-medium">{{ entry.resolvedName }}</span>
|
<span class="font-medium">{{ entry.resolvedName }}</span>
|
||||||
|
<span v-if="(entry.quantity ?? 1) > 1" class="text-sm text-base-content/60">×{{ entry.quantity }}</span>
|
||||||
<span class="text-xs text-base-content/70"> — {{ entry.requirementLabel }}</span>
|
<span class="text-xs text-base-content/70"> — {{ entry.requirementLabel }}</span>
|
||||||
|
<input
|
||||||
|
v-if="canEdit && entry._definition"
|
||||||
|
v-model.number="entry._definition.quantity"
|
||||||
|
type="number"
|
||||||
|
:min="1"
|
||||||
|
step="1"
|
||||||
|
placeholder="Qté"
|
||||||
|
class="input input-bordered input-xs w-16 ml-auto"
|
||||||
|
@input="entry._definition.quantity = Math.max(1, entry._definition.quantity || 1)"
|
||||||
|
@blur="saveSlotQuantity(entry)"
|
||||||
|
/>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -308,6 +320,7 @@ const {
|
|||||||
removeDocument,
|
removeDocument,
|
||||||
handleFilesAdded,
|
handleFilesAdded,
|
||||||
submitEdition,
|
submitEdition,
|
||||||
|
saveSlotQuantity,
|
||||||
resolvePieceLabel,
|
resolvePieceLabel,
|
||||||
resolveProductLabel,
|
resolveProductLabel,
|
||||||
resolveSubcomponentLabel,
|
resolveSubcomponentLabel,
|
||||||
|
|||||||
@@ -390,7 +390,7 @@ const loadProductType = async () => {
|
|||||||
// Try using the expanded typeProduct from entity response first
|
// Try using the expanded typeProduct from entity response first
|
||||||
const embedded = product.value?.typeProduct
|
const embedded = product.value?.typeProduct
|
||||||
if (embedded && typeof embedded === 'object' && embedded.id) {
|
if (embedded && typeof embedded === 'object' && embedded.id) {
|
||||||
const embeddedStructure = embedded.structure ?? embedded.productSkeleton ?? null
|
const embeddedStructure = embedded.structure ?? null
|
||||||
if (embeddedStructure) {
|
if (embeddedStructure) {
|
||||||
productType.value = embedded
|
productType.value = embedded
|
||||||
structure.value = normalizeProductStructureForSave(embeddedStructure)
|
structure.value = normalizeProductStructureForSave(embeddedStructure)
|
||||||
@@ -406,7 +406,7 @@ const loadProductType = async () => {
|
|||||||
try {
|
try {
|
||||||
const type = await getModelType(product.value.typeProductId)
|
const type = await getModelType(product.value.typeProductId)
|
||||||
productType.value = type
|
productType.value = type
|
||||||
structure.value = normalizeProductStructureForSave(type?.structure ?? type?.productSkeleton ?? null)
|
structure.value = normalizeProductStructureForSave(type?.structure ?? null)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erreur lors du chargement du type de produit:', error)
|
console.error('Erreur lors du chargement du type de produit:', error)
|
||||||
productType.value = embedded ?? null
|
productType.value = embedded ?? null
|
||||||
|
|||||||
@@ -46,9 +46,6 @@ export interface ModelType extends BaseModelTypePayload {
|
|||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
category: ModelCategory;
|
category: ModelCategory;
|
||||||
structure: ModelTypeStructure;
|
structure: ModelTypeStructure;
|
||||||
componentSkeleton?: ComponentModelStructure | null;
|
|
||||||
pieceSkeleton?: PieceModelStructure | null;
|
|
||||||
productSkeleton?: ProductModelStructure | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ModelTypeListParams {
|
export interface ModelTypeListParams {
|
||||||
@@ -86,42 +83,9 @@ const normalizeModelType = (item: any): ModelType => {
|
|||||||
if (!item || typeof item !== 'object') {
|
if (!item || typeof item !== 'object') {
|
||||||
return item as ModelType;
|
return item as ModelType;
|
||||||
}
|
}
|
||||||
if (!item.structure) {
|
|
||||||
if (item.category === 'COMPONENT' && item.componentSkeleton) {
|
|
||||||
item.structure = item.componentSkeleton;
|
|
||||||
} else if (item.category === 'PIECE' && item.pieceSkeleton) {
|
|
||||||
item.structure = item.pieceSkeleton;
|
|
||||||
} else if (item.category === 'PRODUCT' && item.productSkeleton) {
|
|
||||||
item.structure = item.productSkeleton;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return item as ModelType;
|
return item as ModelType;
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStructureToSkeleton = <T extends Record<string, any>>(payload: T): T => {
|
|
||||||
if (!payload || typeof payload !== 'object') {
|
|
||||||
return payload;
|
|
||||||
}
|
|
||||||
if (!('structure' in payload)) {
|
|
||||||
return payload;
|
|
||||||
}
|
|
||||||
const structure = (payload as any).structure;
|
|
||||||
if (!structure) {
|
|
||||||
return payload;
|
|
||||||
}
|
|
||||||
const category = (payload as any).category;
|
|
||||||
const next = { ...payload } as Record<string, any>;
|
|
||||||
if (category === 'COMPONENT') {
|
|
||||||
next.componentSkeleton = structure;
|
|
||||||
} else if (category === 'PIECE') {
|
|
||||||
next.pieceSkeleton = structure;
|
|
||||||
} else if (category === 'PRODUCT') {
|
|
||||||
next.productSkeleton = structure;
|
|
||||||
}
|
|
||||||
delete next.structure;
|
|
||||||
return next as T;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function listModelTypes(params: ModelTypeListParams = {}, opts: { signal?: AbortSignal } = {}) {
|
export async function listModelTypes(params: ModelTypeListParams = {}, opts: { signal?: AbortSignal } = {}) {
|
||||||
const requestFetch = useRequestFetch();
|
const requestFetch = useRequestFetch();
|
||||||
const query: Record<string, string | number> = {};
|
const query: Record<string, string | number> = {};
|
||||||
@@ -178,28 +142,26 @@ export async function listModelTypes(params: ModelTypeListParams = {}, opts: { s
|
|||||||
|
|
||||||
export function createModelType(payload: ModelTypePayload, opts: { signal?: AbortSignal } = {}) {
|
export function createModelType(payload: ModelTypePayload, opts: { signal?: AbortSignal } = {}) {
|
||||||
const requestFetch = useRequestFetch();
|
const requestFetch = useRequestFetch();
|
||||||
const mappedPayload = mapStructureToSkeleton(payload);
|
|
||||||
return requestFetch<ModelType>(ENDPOINT, createOptions({
|
return requestFetch<ModelType>(ENDPOINT, createOptions({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/ld+json',
|
'Content-Type': 'application/ld+json',
|
||||||
Accept: 'application/ld+json',
|
Accept: 'application/ld+json',
|
||||||
},
|
},
|
||||||
body: mappedPayload,
|
body: payload,
|
||||||
signal: opts.signal,
|
signal: opts.signal,
|
||||||
})).then(normalizeModelType);
|
})).then(normalizeModelType);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateModelType(id: string, payload: Partial<ModelTypePayload>, opts: { signal?: AbortSignal } = {}) {
|
export function updateModelType(id: string, payload: Partial<ModelTypePayload>, opts: { signal?: AbortSignal } = {}) {
|
||||||
const requestFetch = useRequestFetch();
|
const requestFetch = useRequestFetch();
|
||||||
const mappedPayload = mapStructureToSkeleton(payload);
|
|
||||||
return requestFetch<ModelType>(`${ENDPOINT}/${id}`, createOptions({
|
return requestFetch<ModelType>(`${ENDPOINT}/${id}`, createOptions({
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/merge-patch+json',
|
'Content-Type': 'application/merge-patch+json',
|
||||||
Accept: 'application/ld+json',
|
Accept: 'application/ld+json',
|
||||||
},
|
},
|
||||||
body: mappedPayload,
|
body: payload,
|
||||||
signal: opts.signal,
|
signal: opts.signal,
|
||||||
})).then(normalizeModelType);
|
})).then(normalizeModelType);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -175,6 +175,9 @@ export const normalizeStructureForSave = (input: any): any => {
|
|||||||
if (piece.reference) {
|
if (piece.reference) {
|
||||||
payload.reference = piece.reference
|
payload.reference = piece.reference
|
||||||
}
|
}
|
||||||
|
if ((piece as any).quantity !== undefined && (piece as any).quantity >= 1) {
|
||||||
|
payload.quantity = (piece as any).quantity
|
||||||
|
}
|
||||||
return payload
|
return payload
|
||||||
}) as any
|
}) as any
|
||||||
|
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ export const hydratePieces = (pieces: any[]): ComponentModelPiece[] => {
|
|||||||
reference: piece?.reference ?? '',
|
reference: piece?.reference ?? '',
|
||||||
familyCode: piece?.familyCode ?? piece?.typePiece?.code ?? '',
|
familyCode: piece?.familyCode ?? piece?.typePiece?.code ?? '',
|
||||||
role: piece?.role ?? '',
|
role: piece?.role ?? '',
|
||||||
|
...(piece?.quantity !== undefined && piece.quantity >= 1 ? { quantity: piece.quantity } : {}),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,6 +176,7 @@ export const mapComponentPieces = (pieces: any[]): ComponentModelPiece[] => {
|
|||||||
typePieceLabel: piece?.typePieceLabel ?? piece?.typePiece?.name ?? '',
|
typePieceLabel: piece?.typePieceLabel ?? piece?.typePiece?.name ?? '',
|
||||||
familyCode: piece?.familyCode ?? piece?.typePiece?.code ?? '',
|
familyCode: piece?.familyCode ?? piece?.typePiece?.code ?? '',
|
||||||
role: piece?.role ?? '',
|
role: piece?.role ?? '',
|
||||||
|
...(piece?.quantity !== undefined && piece.quantity >= 1 ? { quantity: piece.quantity } : {}),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -162,6 +162,8 @@ export const sanitizePieces = (pieces: any[]): ComponentModelPiece[] => {
|
|||||||
const rawRole = typeof piece?.role === 'string' ? piece.role.trim() : ''
|
const rawRole = typeof piece?.role === 'string' ? piece.role.trim() : ''
|
||||||
const role = rawRole.length > 0 ? rawRole : undefined
|
const role = rawRole.length > 0 ? rawRole : undefined
|
||||||
|
|
||||||
|
const quantity = typeof piece?.quantity === 'number' && piece.quantity >= 1 ? piece.quantity : undefined
|
||||||
|
|
||||||
if (!typePieceId && !typePieceLabel && !reference && !familyCode) {
|
if (!typePieceId && !typePieceLabel && !reference && !familyCode) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -182,6 +184,9 @@ export const sanitizePieces = (pieces: any[]): ComponentModelPiece[] => {
|
|||||||
if (typePieceLabel) {
|
if (typePieceLabel) {
|
||||||
result.typePieceLabel = typePieceLabel
|
result.typePieceLabel = typePieceLabel
|
||||||
}
|
}
|
||||||
|
if (quantity !== undefined) {
|
||||||
|
result.quantity = quantity
|
||||||
|
}
|
||||||
return result
|
return result
|
||||||
})
|
})
|
||||||
.filter((piece): piece is ComponentModelPiece => !!piece)
|
.filter((piece): piece is ComponentModelPiece => !!piece)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export interface DefinitionOverridePayload {
|
|||||||
name?: string
|
name?: string
|
||||||
reference?: string
|
reference?: string
|
||||||
constructeurIds?: string[]
|
constructeurIds?: string[]
|
||||||
prix?: number
|
prix?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const sanitizeDefinitionOverrides = (definition: any): DefinitionOverridePayload | null => {
|
export const sanitizeDefinitionOverrides = (definition: any): DefinitionOverridePayload | null => {
|
||||||
@@ -41,7 +41,7 @@ export const sanitizeDefinitionOverrides = (definition: any): DefinitionOverride
|
|||||||
if (definition.prix !== undefined && definition.prix !== null && definition.prix !== '') {
|
if (definition.prix !== undefined && definition.prix !== null && definition.prix !== '') {
|
||||||
const parsed = Number(definition.prix)
|
const parsed = Number(definition.prix)
|
||||||
if (!Number.isNaN(parsed)) {
|
if (!Number.isNaN(parsed)) {
|
||||||
payload.prix = parsed
|
payload.prix = String(parsed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export interface ComponentModelPiece {
|
|||||||
reference?: string
|
reference?: string
|
||||||
familyCode?: string
|
familyCode?: string
|
||||||
role?: string
|
role?: string
|
||||||
|
quantity?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ComponentModelProduct {
|
export interface ComponentModelProduct {
|
||||||
@@ -156,6 +157,7 @@ const validatePiece = (
|
|||||||
const reference = ensureString(value.reference)
|
const reference = ensureString(value.reference)
|
||||||
const familyCode = ensureString(value.familyCode)
|
const familyCode = ensureString(value.familyCode)
|
||||||
const role = ensureString(value.role)
|
const role = ensureString(value.role)
|
||||||
|
const quantity = typeof value.quantity === 'number' && value.quantity >= 1 ? value.quantity : undefined
|
||||||
|
|
||||||
if (!typePieceId && !typePieceLabel && !reference && !familyCode) {
|
if (!typePieceId && !typePieceLabel && !reference && !familyCode) {
|
||||||
issues.push(`${path}: au moins un identifiant, une famille ou une référence de pièce est requis`)
|
issues.push(`${path}: au moins un identifiant, une famille ou une référence de pièce est requis`)
|
||||||
@@ -168,6 +170,7 @@ const validatePiece = (
|
|||||||
...(reference ? { reference } : {}),
|
...(reference ? { reference } : {}),
|
||||||
...(familyCode ? { familyCode } : {}),
|
...(familyCode ? { familyCode } : {}),
|
||||||
...(role ? { role } : {}),
|
...(role ? { role } : {}),
|
||||||
|
...(quantity ? { quantity } : {}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -225,7 +225,10 @@ export const buildCustomFieldInputs = (
|
|||||||
if (fieldName) mapByName.set(fieldName, entry)
|
if (fieldName) mapByName.set(fieldName, entry)
|
||||||
})
|
})
|
||||||
|
|
||||||
return definitions
|
const matchedIds = new Set<string>()
|
||||||
|
const matchedNames = new Set<string>()
|
||||||
|
|
||||||
|
const result = definitions
|
||||||
.map((definition) => {
|
.map((definition) => {
|
||||||
const definitionId = definition.customFieldId || definition.id || null
|
const definitionId = definition.customFieldId || definition.id || null
|
||||||
const matched = (definitionId ? mapById.get(definitionId) : null) || mapByName.get(definition.name)
|
const matched = (definitionId ? mapById.get(definitionId) : null) || mapByName.get(definition.name)
|
||||||
@@ -239,6 +242,11 @@ export const buildCustomFieldInputs = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const matchedFieldId = matched.customField?.id || matched.customFieldId || null
|
||||||
|
if (matchedFieldId) matchedIds.add(matchedFieldId)
|
||||||
|
const matchedFieldName = matched.customField?.name || matched.name || null
|
||||||
|
if (matchedFieldName) matchedNames.add(matchedFieldName)
|
||||||
|
|
||||||
const resolvedValue = extractStoredCustomFieldValue(matched)
|
const resolvedValue = extractStoredCustomFieldValue(matched)
|
||||||
return {
|
return {
|
||||||
...definition,
|
...definition,
|
||||||
@@ -253,7 +261,36 @@ export const buildCustomFieldInputs = (
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.sort((a, b) => (a.orderIndex ?? 0) - (b.orderIndex ?? 0))
|
|
||||||
|
// Include values with embedded definitions that didn't match any structure definition
|
||||||
|
valueList.forEach((entry, index) => {
|
||||||
|
if (!entry || typeof entry !== 'object') return
|
||||||
|
const cf = entry.customField
|
||||||
|
if (!cf || typeof cf !== 'object') return
|
||||||
|
const fieldId = cf.id || entry.customFieldId || null
|
||||||
|
const fieldName = cf.name || entry.name || null
|
||||||
|
if (fieldId && matchedIds.has(fieldId)) return
|
||||||
|
if (fieldName && matchedNames.has(fieldName)) return
|
||||||
|
|
||||||
|
const name = resolveFieldName(cf)
|
||||||
|
if (!name) return
|
||||||
|
|
||||||
|
const type = resolveFieldType(cf)
|
||||||
|
const resolvedValue = extractStoredCustomFieldValue(entry)
|
||||||
|
result.push({
|
||||||
|
id: fieldId,
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
required: resolveRequiredFlag(cf),
|
||||||
|
options: resolveOptions(cf),
|
||||||
|
value: formatDefaultValue(type, resolvedValue),
|
||||||
|
customFieldId: fieldId,
|
||||||
|
customFieldValueId: entry.id ?? null,
|
||||||
|
orderIndex: typeof cf.orderIndex === 'number' ? cf.orderIndex : definitions.length + index,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return result.sort((a, b) => (a.orderIndex ?? 0) - (b.orderIndex ?? 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -196,34 +196,19 @@ export const getProductDisplay = (
|
|||||||
|
|
||||||
const structuralCandidates = [
|
const structuralCandidates = [
|
||||||
source.products,
|
source.products,
|
||||||
source.productSkeleton,
|
|
||||||
(source.definition as AnyRecord)?.products,
|
(source.definition as AnyRecord)?.products,
|
||||||
(source.definition as AnyRecord)?.productSkeleton,
|
|
||||||
((source.definition as AnyRecord)?.structure as AnyRecord)?.products,
|
((source.definition as AnyRecord)?.structure as AnyRecord)?.products,
|
||||||
((source.definition as AnyRecord)?.structure as AnyRecord)?.productSkeleton,
|
|
||||||
(source.structure as AnyRecord)?.products,
|
(source.structure as AnyRecord)?.products,
|
||||||
(source.structure as AnyRecord)?.productSkeleton,
|
|
||||||
(source.requirement as AnyRecord)?.products,
|
(source.requirement as AnyRecord)?.products,
|
||||||
(source.requirement as AnyRecord)?.productSkeleton,
|
|
||||||
((source.requirement as AnyRecord)?.structure as AnyRecord)?.products,
|
((source.requirement as AnyRecord)?.structure as AnyRecord)?.products,
|
||||||
((source.requirement as AnyRecord)?.structure as AnyRecord)?.productSkeleton,
|
|
||||||
((source.requirement as AnyRecord)?.componentSkeleton as AnyRecord)?.products,
|
|
||||||
(source.typeComposant as AnyRecord)?.products,
|
(source.typeComposant as AnyRecord)?.products,
|
||||||
(source.typeComposant as AnyRecord)?.productSkeleton,
|
|
||||||
((source.typeComposant as AnyRecord)?.structure as AnyRecord)?.products,
|
((source.typeComposant as AnyRecord)?.structure as AnyRecord)?.products,
|
||||||
((source.typeComposant as AnyRecord)?.structure as AnyRecord)?.productSkeleton,
|
|
||||||
(source.originalComposant as AnyRecord)?.products,
|
(source.originalComposant as AnyRecord)?.products,
|
||||||
(source.originalComposant as AnyRecord)?.productSkeleton,
|
|
||||||
((source.originalComposant as AnyRecord)?.definition as AnyRecord)?.products,
|
((source.originalComposant as AnyRecord)?.definition as AnyRecord)?.products,
|
||||||
((source.originalComposant as AnyRecord)?.definition as AnyRecord)?.productSkeleton,
|
|
||||||
(((source.originalComposant as AnyRecord)?.definition as AnyRecord)?.structure as AnyRecord)?.products,
|
(((source.originalComposant as AnyRecord)?.definition as AnyRecord)?.structure as AnyRecord)?.products,
|
||||||
(((source.originalComposant as AnyRecord)?.definition as AnyRecord)?.structure as AnyRecord)?.productSkeleton,
|
|
||||||
(source.originalComponent as AnyRecord)?.products,
|
(source.originalComponent as AnyRecord)?.products,
|
||||||
(source.originalComponent as AnyRecord)?.productSkeleton,
|
|
||||||
((source.originalComponent as AnyRecord)?.definition as AnyRecord)?.products,
|
((source.originalComponent as AnyRecord)?.definition as AnyRecord)?.products,
|
||||||
((source.originalComponent as AnyRecord)?.definition as AnyRecord)?.productSkeleton,
|
|
||||||
(((source.originalComponent as AnyRecord)?.definition as AnyRecord)?.structure as AnyRecord)?.products,
|
(((source.originalComponent as AnyRecord)?.definition as AnyRecord)?.structure as AnyRecord)?.products,
|
||||||
(((source.originalComponent as AnyRecord)?.definition as AnyRecord)?.structure as AnyRecord)?.productSkeleton,
|
|
||||||
]
|
]
|
||||||
|
|
||||||
const structuralProducts = structuralCandidates
|
const structuralProducts = structuralCandidates
|
||||||
|
|||||||
@@ -176,6 +176,7 @@ export function sanitizePieceDefinition(definition: ComponentModelPiece) {
|
|||||||
typePieceLabel: definition.typePieceLabel ?? null,
|
typePieceLabel: definition.typePieceLabel ?? null,
|
||||||
reference: definition.reference ?? null,
|
reference: definition.reference ?? null,
|
||||||
familyCode: (definition as any).familyCode ?? null,
|
familyCode: (definition as any).familyCode ?? null,
|
||||||
|
quantity: typeof (definition as any).quantity === 'number' ? (definition as any).quantity : null,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ export type SelectionEntry = {
|
|||||||
path: string
|
path: string
|
||||||
requirementLabel: string
|
requirementLabel: string
|
||||||
resolvedName: string
|
resolvedName: string
|
||||||
|
quantity?: number
|
||||||
|
slotId?: string
|
||||||
|
_definition?: Record<string, any>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type StructureSelectionResult = {
|
export type StructureSelectionResult = {
|
||||||
@@ -59,6 +62,9 @@ export function collectStructureSelections(
|
|||||||
path: isNonEmptyString(entry?.path) ? entry.path : `${nodePath}:piece-${index + 1}`,
|
path: isNonEmptyString(entry?.path) ? entry.path : `${nodePath}:piece-${index + 1}`,
|
||||||
requirementLabel: resolvers.resolvePieceLabel(definition),
|
requirementLabel: resolvers.resolvePieceLabel(definition),
|
||||||
resolvedName: catalogPiece?.name || selectedId,
|
resolvedName: catalogPiece?.name || selectedId,
|
||||||
|
quantity: typeof definition?.quantity === 'number' ? definition.quantity : undefined,
|
||||||
|
slotId: isNonEmptyString(entry?.slotId) ? entry.slotId : undefined,
|
||||||
|
_definition: definition,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -50,60 +50,55 @@ beforeEach(() => {
|
|||||||
// normalizeModelType (tested via getModelType which calls .then(normalizeModelType))
|
// normalizeModelType (tested via getModelType which calls .then(normalizeModelType))
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
describe('normalizeModelType (via getModelType)', () => {
|
describe('normalizeModelType (via getModelType)', () => {
|
||||||
it('maps componentSkeleton to structure for COMPONENT', async () => {
|
it('returns structure as-is for COMPONENT', async () => {
|
||||||
const skeleton = { customFields: [{ name: 'Weight' }] }
|
const structure = { customFields: [{ name: 'Weight' }] }
|
||||||
mockFetch.mockResolvedValue(fakeModelType({
|
mockFetch.mockResolvedValue(fakeModelType({
|
||||||
category: 'COMPONENT',
|
category: 'COMPONENT',
|
||||||
structure: null,
|
structure: structure as any,
|
||||||
componentSkeleton: skeleton as any,
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const result = await getModelType('mt-1')
|
const result = await getModelType('mt-1')
|
||||||
expect(result.structure).toEqual(skeleton)
|
expect(result.structure).toEqual(structure)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('maps pieceSkeleton to structure for PIECE', async () => {
|
it('returns structure as-is for PIECE', async () => {
|
||||||
const skeleton = { customFields: [{ name: 'Size' }] }
|
const structure = { customFields: [{ name: 'Size' }] }
|
||||||
mockFetch.mockResolvedValue(fakeModelType({
|
mockFetch.mockResolvedValue(fakeModelType({
|
||||||
category: 'PIECE',
|
category: 'PIECE',
|
||||||
structure: null,
|
structure: structure as any,
|
||||||
pieceSkeleton: skeleton as any,
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const result = await getModelType('mt-1')
|
const result = await getModelType('mt-1')
|
||||||
expect(result.structure).toEqual(skeleton)
|
expect(result.structure).toEqual(structure)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('maps productSkeleton to structure for PRODUCT', async () => {
|
it('returns structure as-is for PRODUCT', async () => {
|
||||||
const skeleton = { customFields: [{ name: 'Brand' }] }
|
const structure = { customFields: [{ name: 'Brand' }] }
|
||||||
mockFetch.mockResolvedValue(fakeModelType({
|
mockFetch.mockResolvedValue(fakeModelType({
|
||||||
category: 'PRODUCT',
|
category: 'PRODUCT',
|
||||||
structure: null,
|
structure: structure as any,
|
||||||
productSkeleton: skeleton as any,
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const result = await getModelType('mt-1')
|
const result = await getModelType('mt-1')
|
||||||
expect(result.structure).toEqual(skeleton)
|
expect(result.structure).toEqual(structure)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('does not override existing structure', async () => {
|
it('preserves null structure', async () => {
|
||||||
const existing = { customFields: [{ name: 'Existing' }] }
|
|
||||||
mockFetch.mockResolvedValue(fakeModelType({
|
mockFetch.mockResolvedValue(fakeModelType({
|
||||||
category: 'COMPONENT',
|
category: 'COMPONENT',
|
||||||
structure: existing as any,
|
structure: null,
|
||||||
componentSkeleton: { customFields: [{ name: 'Skeleton' }] } as any,
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const result = await getModelType('mt-1')
|
const result = await getModelType('mt-1')
|
||||||
expect(result.structure).toEqual(existing)
|
expect(result.structure).toBeNull()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// createModelType — maps structure to skeleton
|
// createModelType — sends structure directly
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
describe('createModelType', () => {
|
describe('createModelType', () => {
|
||||||
it('sends POST with componentSkeleton for COMPONENT', async () => {
|
it('sends POST with structure for COMPONENT', async () => {
|
||||||
const structure = { customFields: [] }
|
const structure = { customFields: [] }
|
||||||
mockFetch.mockResolvedValue(fakeModelType())
|
mockFetch.mockResolvedValue(fakeModelType())
|
||||||
|
|
||||||
@@ -120,11 +115,10 @@ describe('createModelType', () => {
|
|||||||
const [endpoint, options] = mockFetch.mock.calls[0]
|
const [endpoint, options] = mockFetch.mock.calls[0]
|
||||||
expect(endpoint).toBe('/model_types')
|
expect(endpoint).toBe('/model_types')
|
||||||
expect(options.method).toBe('POST')
|
expect(options.method).toBe('POST')
|
||||||
expect(options.body.componentSkeleton).toEqual(structure)
|
expect(options.body.structure).toEqual(structure)
|
||||||
expect(options.body.structure).toBeUndefined()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('sends POST with pieceSkeleton for PIECE', async () => {
|
it('sends POST with structure for PIECE', async () => {
|
||||||
const structure = { customFields: [], products: [] }
|
const structure = { customFields: [], products: [] }
|
||||||
mockFetch.mockResolvedValue(fakeModelType({ category: 'PIECE' }))
|
mockFetch.mockResolvedValue(fakeModelType({ category: 'PIECE' }))
|
||||||
|
|
||||||
@@ -136,11 +130,10 @@ describe('createModelType', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const [, options] = mockFetch.mock.calls[0]
|
const [, options] = mockFetch.mock.calls[0]
|
||||||
expect(options.body.pieceSkeleton).toEqual(structure)
|
expect(options.body.structure).toEqual(structure)
|
||||||
expect(options.body.structure).toBeUndefined()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('sends POST with productSkeleton for PRODUCT', async () => {
|
it('sends POST with structure for PRODUCT', async () => {
|
||||||
const structure = { customFields: [] }
|
const structure = { customFields: [] }
|
||||||
mockFetch.mockResolvedValue(fakeModelType({ category: 'PRODUCT' }))
|
mockFetch.mockResolvedValue(fakeModelType({ category: 'PRODUCT' }))
|
||||||
|
|
||||||
@@ -152,15 +145,15 @@ describe('createModelType', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const [, options] = mockFetch.mock.calls[0]
|
const [, options] = mockFetch.mock.calls[0]
|
||||||
expect(options.body.productSkeleton).toEqual(structure)
|
expect(options.body.structure).toEqual(structure)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// updateModelType — maps structure to skeleton
|
// updateModelType — sends structure directly
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
describe('updateModelType', () => {
|
describe('updateModelType', () => {
|
||||||
it('sends PATCH with correct endpoint and skeleton', async () => {
|
it('sends PATCH with correct endpoint and structure', async () => {
|
||||||
const structure = { customFields: [{ name: 'Updated' }] }
|
const structure = { customFields: [{ name: 'Updated' }] }
|
||||||
mockFetch.mockResolvedValue(fakeModelType())
|
mockFetch.mockResolvedValue(fakeModelType())
|
||||||
|
|
||||||
@@ -176,10 +169,10 @@ describe('updateModelType', () => {
|
|||||||
expect(endpoint).toBe('/model_types/mt-1')
|
expect(endpoint).toBe('/model_types/mt-1')
|
||||||
expect(options.method).toBe('PATCH')
|
expect(options.method).toBe('PATCH')
|
||||||
expect(options.headers['Content-Type']).toBe('application/merge-patch+json')
|
expect(options.headers['Content-Type']).toBe('application/merge-patch+json')
|
||||||
expect(options.body.componentSkeleton).toEqual(structure)
|
expect(options.body.structure).toEqual(structure)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('sends payload without skeleton when no structure', async () => {
|
it('sends payload without structure when not provided', async () => {
|
||||||
mockFetch.mockResolvedValue(fakeModelType())
|
mockFetch.mockResolvedValue(fakeModelType())
|
||||||
|
|
||||||
await updateModelType('mt-1', {
|
await updateModelType('mt-1', {
|
||||||
@@ -189,7 +182,7 @@ describe('updateModelType', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const [, options] = mockFetch.mock.calls[0]
|
const [, options] = mockFetch.mock.calls[0]
|
||||||
expect(options.body.componentSkeleton).toBeUndefined()
|
expect(options.body.structure).toBeUndefined()
|
||||||
expect(options.body.name).toBe('Just Name')
|
expect(options.body.name).toBe('Just Name')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user