Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
Les sélections de produits liés ne bloquent plus la soumission du formulaire de création ou d'édition de pièce. Les slots vides restent visibles et peuvent être remplis ultérieurement. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
456 lines
14 KiB
TypeScript
456 lines
14 KiB
TypeScript
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
|
import { useRouter } from '#imports'
|
|
import { usePieceTypes } from '~/composables/usePieceTypes'
|
|
import { usePieces } from '~/composables/usePieces'
|
|
import { useApi } from '~/composables/useApi'
|
|
import { useToast } from '~/composables/useToast'
|
|
import { useDocuments } from '~/composables/useDocuments'
|
|
import { useConstructeurs } from '~/composables/useConstructeurs'
|
|
import { useConstructeurLinks } from '~/composables/useConstructeurLinks'
|
|
import { useEntityHistory } from '~/composables/useEntityHistory'
|
|
import { extractRelationId } from '~/shared/apiRelations'
|
|
import { canPreviewDocument } from '~/utils/documentPreview'
|
|
import { formatPieceStructurePreview } from '~/shared/modelUtils'
|
|
import { uniqueConstructeurIds, constructeurIdsFromLinks } from '~/shared/constructeurUtils'
|
|
import type { ConstructeurLinkEntry } from '~/shared/constructeurUtils'
|
|
import type { PieceModelStructure } from '~/shared/types/inventory'
|
|
import type { ModelType } from '~/services/modelTypes'
|
|
import {
|
|
getStructureProducts,
|
|
buildProductRequirementDescriptions,
|
|
buildProductRequirementEntries,
|
|
resizeProductSelections,
|
|
applyProductSelection,
|
|
collectNormalizedProductIds,
|
|
} from '~/shared/utils/pieceProductSelectionUtils'
|
|
import { getModelType } from '~/services/modelTypes'
|
|
import { useCustomFieldInputs, type CustomFieldInput } from '~/composables/useCustomFieldInputs'
|
|
|
|
interface PieceCatalogType extends ModelType {
|
|
structure: PieceModelStructure | null
|
|
customFields?: Array<Record<string, any>>
|
|
}
|
|
|
|
export function usePieceEdit(pieceId: string) {
|
|
const { canEdit } = usePermissions()
|
|
const router = useRouter()
|
|
const { get } = useApi()
|
|
const { pieceTypes, loadPieceTypes } = usePieceTypes()
|
|
const { updatePiece } = usePieces()
|
|
const toast = useToast()
|
|
const { loadDocumentsByPiece, uploadDocuments, deleteDocument } = useDocuments()
|
|
const { ensureConstructeurs } = useConstructeurs()
|
|
const { fetchLinks, syncLinks } = useConstructeurLinks()
|
|
const {
|
|
history,
|
|
loading: historyLoading,
|
|
error: historyError,
|
|
loadHistory,
|
|
} = useEntityHistory('piece')
|
|
|
|
const piece = ref<any | null>(null)
|
|
const loading = ref(true)
|
|
const saving = ref(false)
|
|
const selectedFiles = ref<File[]>([])
|
|
const uploadingDocuments = ref(false)
|
|
const loadingDocuments = ref(false)
|
|
const pieceDocuments = ref<any[]>([])
|
|
const previewDocument = ref<any | null>(null)
|
|
const previewVisible = ref(false)
|
|
|
|
const historyFieldLabels: Record<string, string> = {
|
|
name: 'Nom',
|
|
reference: 'Référence',
|
|
prix: 'Prix',
|
|
typePiece: 'Catégorie',
|
|
product: 'Produit lié',
|
|
productIds: 'Produits liés',
|
|
constructeurIds: 'Fournisseurs',
|
|
}
|
|
|
|
const selectedTypeId = ref<string>('')
|
|
const pieceTypeDetails = ref<any | null>(null)
|
|
const editionForm = reactive({
|
|
name: '' as string,
|
|
description: '' as string,
|
|
reference: '' as string,
|
|
constructeurIds: [] as string[],
|
|
prix: '' as string,
|
|
})
|
|
const constructeurLinks = ref<ConstructeurLinkEntry[]>([])
|
|
const originalConstructeurLinks = ref<ConstructeurLinkEntry[]>([])
|
|
const constructeurIdsFromForm = computed(() => constructeurIdsFromLinks(constructeurLinks.value))
|
|
const productSelections = ref<(string | null)[]>([])
|
|
|
|
// Declared early so useCustomFieldInputs can reference it.
|
|
// selectedType is defined later but is safely accessed inside a computed (lazy evaluation).
|
|
const resolvedStructure = computed<PieceModelStructure | null>(() =>
|
|
pieceTypeDetails.value?.structure ?? null,
|
|
)
|
|
|
|
const {
|
|
fields: customFieldInputs,
|
|
requiredFilled: requiredCustomFieldsFilled,
|
|
saveAll: saveAllCustomFields,
|
|
refresh: refreshCustomFieldInputs,
|
|
} = useCustomFieldInputs({
|
|
definitions: computed(() => resolvedStructure.value?.customFields ?? []),
|
|
values: computed(() => piece.value?.customFieldValues ?? []),
|
|
entityType: 'piece',
|
|
entityId: computed(() => piece.value?.id ?? null),
|
|
context: 'standalone',
|
|
onValueCreated: (newValue) => {
|
|
if (piece.value && Array.isArray(piece.value.customFieldValues)) {
|
|
piece.value.customFieldValues.push(newValue)
|
|
}
|
|
},
|
|
})
|
|
|
|
const openPreview = (doc: any) => {
|
|
if (!doc || !canPreviewDocument(doc)) {
|
|
return
|
|
}
|
|
previewDocument.value = doc
|
|
previewVisible.value = true
|
|
}
|
|
|
|
const closePreview = () => {
|
|
previewVisible.value = false
|
|
previewDocument.value = null
|
|
}
|
|
|
|
const removeDocument = async (documentId: string | number | null | undefined) => {
|
|
if (!documentId) {
|
|
return
|
|
}
|
|
const result = await deleteDocument(documentId, { updateStore: false })
|
|
if (result.success) {
|
|
pieceDocuments.value = pieceDocuments.value.filter((doc) => doc.id !== documentId)
|
|
}
|
|
}
|
|
|
|
const handleFilesAdded = async (files: File[]) => {
|
|
if (!files?.length || !piece.value?.id) {
|
|
return
|
|
}
|
|
uploadingDocuments.value = true
|
|
try {
|
|
const result = await uploadDocuments(
|
|
{
|
|
files,
|
|
context: { pieceId: piece.value.id },
|
|
},
|
|
{ updateStore: false },
|
|
)
|
|
if (result.success) {
|
|
selectedFiles.value = []
|
|
await refreshDocuments()
|
|
}
|
|
}
|
|
finally {
|
|
uploadingDocuments.value = false
|
|
}
|
|
}
|
|
|
|
const refreshDocuments = async () => {
|
|
if (!piece.value?.id) {
|
|
pieceDocuments.value = []
|
|
return
|
|
}
|
|
loadingDocuments.value = true
|
|
try {
|
|
const result = await loadDocumentsByPiece(piece.value.id, { updateStore: false })
|
|
if (result.success) {
|
|
pieceDocuments.value = Array.isArray(result.data) ? result.data : result.data ? [result.data] : []
|
|
}
|
|
}
|
|
finally {
|
|
loadingDocuments.value = false
|
|
}
|
|
}
|
|
|
|
const pieceTypeList = computed<PieceCatalogType[]>(() => (pieceTypes.value || []) as PieceCatalogType[])
|
|
|
|
const selectedType = computed(() => {
|
|
if (!selectedTypeId.value) {
|
|
return null
|
|
}
|
|
return pieceTypeList.value.find((type) => type.id === selectedTypeId.value) ?? null
|
|
})
|
|
|
|
const structureProducts = computed(() =>
|
|
getStructureProducts(resolvedStructure.value),
|
|
)
|
|
|
|
const requiresProductSelection = computed(() => structureProducts.value.length > 0)
|
|
|
|
const productRequirementDescriptions = computed(() =>
|
|
buildProductRequirementDescriptions(structureProducts.value),
|
|
)
|
|
|
|
const ensureProductSelections = (count: number) => {
|
|
productSelections.value = resizeProductSelections(productSelections.value, count)
|
|
}
|
|
|
|
let pendingProductIds: string[] = []
|
|
|
|
const productRequirementEntries = computed(() =>
|
|
buildProductRequirementEntries(structureProducts.value, 'piece-product-requirement'),
|
|
)
|
|
|
|
const productSelectionsFilled = computed(() => true)
|
|
|
|
const setProductSelection = (index: number, value: string | null) => {
|
|
productSelections.value = applyProductSelection(productSelections.value, index, value)
|
|
}
|
|
|
|
watch(structureProducts, (products) => {
|
|
ensureProductSelections(products.length)
|
|
if (!pendingProductIds.length || products.length === 0) {
|
|
return
|
|
}
|
|
const next = Array.from(
|
|
{ length: products.length },
|
|
(_, index) => pendingProductIds[index] ?? null,
|
|
)
|
|
productSelections.value = next
|
|
pendingProductIds = []
|
|
})
|
|
|
|
const canSubmit = computed(() =>
|
|
Boolean(
|
|
canEdit.value
|
|
&& piece.value
|
|
&& editionForm.name
|
|
&& requiredCustomFieldsFilled.value
|
|
&& productSelectionsFilled.value
|
|
&& !saving.value,
|
|
),
|
|
)
|
|
|
|
const fetchPiece = async () => {
|
|
if (!pieceId || typeof pieceId !== 'string') {
|
|
piece.value = null
|
|
pieceDocuments.value = []
|
|
return
|
|
}
|
|
const result = await get(`/pieces/${pieceId}`)
|
|
if (result.success) {
|
|
piece.value = result.data
|
|
pieceDocuments.value = Array.isArray(result.data?.documents) ? result.data.documents : []
|
|
|
|
// The watcher on useCustomFieldInputs will auto-refresh when piece.value changes
|
|
|
|
// Use cached type from loadPieceTypes() instead of separate getModelType() call
|
|
loadPieceTypeDetailsFromCache(result.data)
|
|
|
|
// History is non-blocking — template handles its own loading state
|
|
loadHistory(result.data.id).catch(() => {})
|
|
}
|
|
else {
|
|
piece.value = null
|
|
pieceDocuments.value = []
|
|
}
|
|
}
|
|
|
|
const loadPieceTypeDetailsFromCache = (currentPiece: any) => {
|
|
const typeId = currentPiece?.typePieceId
|
|
|| extractRelationId(currentPiece?.typePiece)
|
|
|| ''
|
|
if (!typeId) {
|
|
pieceTypeDetails.value = null
|
|
return
|
|
}
|
|
// Look up in the already-loaded pieceTypes cache (from loadPieceTypes in onMounted)
|
|
const cachedType = (pieceTypes.value || []).find((t: any) => t.id === typeId) ?? null
|
|
if (cachedType) {
|
|
pieceTypeDetails.value = cachedType
|
|
// useCustomFieldInputs auto-refreshes via its watcher on resolvedStructure
|
|
return
|
|
}
|
|
// Fallback: fetch if not in cache (edge case)
|
|
getModelType(typeId).then((type) => {
|
|
if (type && typeof type === 'object') {
|
|
pieceTypeDetails.value = type
|
|
// useCustomFieldInputs auto-refreshes via its watcher on resolvedStructure
|
|
}
|
|
}).catch(() => {
|
|
pieceTypeDetails.value = null
|
|
})
|
|
}
|
|
|
|
let initialized = false
|
|
|
|
watch(
|
|
[piece, selectedType],
|
|
([currentPiece, _currentType]) => {
|
|
if (!currentPiece || initialized) {
|
|
return
|
|
}
|
|
|
|
const resolvedTypeId = currentPiece.typePieceId
|
|
|| extractRelationId(currentPiece.typePiece)
|
|
|| ''
|
|
if (resolvedTypeId && !currentPiece.typePieceId) {
|
|
currentPiece.typePieceId = resolvedTypeId
|
|
}
|
|
selectedTypeId.value = resolvedTypeId
|
|
|
|
editionForm.name = currentPiece.name || ''
|
|
editionForm.description = currentPiece.description || ''
|
|
editionForm.reference = currentPiece.reference || ''
|
|
// Load constructeur links
|
|
fetchLinks('piece', pieceId).then((links) => {
|
|
constructeurLinks.value = links
|
|
originalConstructeurLinks.value = links.map(l => ({ ...l }))
|
|
editionForm.constructeurIds = constructeurIdsFromLinks(links)
|
|
if (editionForm.constructeurIds.length) {
|
|
void ensureConstructeurs(editionForm.constructeurIds)
|
|
}
|
|
})
|
|
editionForm.prix = currentPiece.prix !== null && currentPiece.prix !== undefined ? String(currentPiece.prix) : ''
|
|
|
|
const existingProductIds = Array.isArray(currentPiece.productIds) && currentPiece.productIds.length
|
|
? currentPiece.productIds.map((id: unknown) => String(id))
|
|
: currentPiece.product?.id || currentPiece.productId
|
|
? [String(currentPiece.product?.id || currentPiece.productId)]
|
|
: []
|
|
pendingProductIds = existingProductIds
|
|
ensureProductSelections(structureProducts.value.length)
|
|
if (existingProductIds.length && structureProducts.value.length) {
|
|
const next = Array.from(
|
|
{ length: structureProducts.value.length },
|
|
(_, index) => existingProductIds[index] ?? null,
|
|
)
|
|
productSelections.value = next
|
|
pendingProductIds = []
|
|
}
|
|
|
|
// useCustomFieldInputs auto-refreshes via its watcher on definitions + values
|
|
|
|
initialized = true
|
|
},
|
|
{ immediate: true },
|
|
)
|
|
|
|
// useCustomFieldInputs auto-refreshes when selectedType changes (via resolvedStructure)
|
|
|
|
watch(resolvedStructure, () => {
|
|
if (!piece.value) {
|
|
return
|
|
}
|
|
ensureProductSelections(structureProducts.value.length)
|
|
// useCustomFieldInputs auto-refreshes via its watcher on definitions
|
|
})
|
|
|
|
const submitEdition = async () => {
|
|
if (!piece.value) {
|
|
return
|
|
}
|
|
|
|
const rawPrice = typeof editionForm.prix === 'string'
|
|
? editionForm.prix.trim()
|
|
: editionForm.prix === null || editionForm.prix === undefined
|
|
? ''
|
|
: String(editionForm.prix).trim()
|
|
|
|
const payload: Record<string, any> = {
|
|
name: editionForm.name.trim(),
|
|
description: editionForm.description.trim() || null,
|
|
}
|
|
|
|
const reference = editionForm.reference.trim()
|
|
payload.reference = reference ? reference : null
|
|
|
|
const normalizedProductIds = collectNormalizedProductIds(
|
|
productRequirementEntries.value,
|
|
productSelections.value,
|
|
)
|
|
|
|
payload.productIds = normalizedProductIds
|
|
payload.productId = normalizedProductIds[0] || null
|
|
|
|
if (rawPrice) {
|
|
const parsed = Number(rawPrice)
|
|
if (!Number.isNaN(parsed)) {
|
|
payload.prix = String(parsed)
|
|
}
|
|
}
|
|
else {
|
|
payload.prix = null
|
|
}
|
|
|
|
saving.value = true
|
|
try {
|
|
const result = await updatePiece(piece.value.id, payload)
|
|
if (result.success && result.data) {
|
|
const failedFields = await saveAllCustomFields()
|
|
if (failedFields.length) {
|
|
toast.showError(`Erreur sur les champs : ${failedFields.join(', ')}`)
|
|
}
|
|
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.')
|
|
}
|
|
}
|
|
catch (error: any) {
|
|
toast.showError(error?.message || 'Erreur lors de la mise à jour de la pièce')
|
|
}
|
|
finally {
|
|
saving.value = false
|
|
}
|
|
}
|
|
|
|
onMounted(async () => {
|
|
await Promise.allSettled([loadPieceTypes(), fetchPiece()])
|
|
loading.value = false
|
|
})
|
|
|
|
return {
|
|
// State
|
|
piece,
|
|
loading,
|
|
saving,
|
|
selectedFiles,
|
|
uploadingDocuments,
|
|
loadingDocuments,
|
|
pieceDocuments,
|
|
previewDocument,
|
|
previewVisible,
|
|
selectedTypeId,
|
|
editionForm,
|
|
constructeurLinks,
|
|
originalConstructeurLinks,
|
|
constructeurIdsFromForm,
|
|
productSelections,
|
|
customFieldInputs,
|
|
requiredCustomFieldsFilled,
|
|
canEdit,
|
|
|
|
// Computed
|
|
pieceTypeList,
|
|
selectedType,
|
|
resolvedStructure,
|
|
structureProducts,
|
|
productRequirementDescriptions,
|
|
productRequirementEntries,
|
|
canSubmit,
|
|
historyFieldLabels,
|
|
|
|
// History
|
|
history,
|
|
historyLoading,
|
|
historyError,
|
|
|
|
// Methods
|
|
openPreview,
|
|
closePreview,
|
|
removeDocument,
|
|
handleFilesAdded,
|
|
setProductSelection,
|
|
submitEdition,
|
|
fetchPiece,
|
|
formatPieceStructurePreview,
|
|
}
|
|
}
|