13 Commits

Author SHA1 Message Date
Matthieu
ac860d3165 fix(constructeurs) : always send constructeurs array in PATCH payload 2026-03-23 13:52:39 +01:00
Matthieu
8176635eb8 fix(machine) : use linkId instead of composantId when deleting a component from machine
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 13:35:43 +01:00
Matthieu
a730a18794 fix(creation) : redirect to edit page after creating composant, piece, or product
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 12:26:18 +01:00
Matthieu
40d0753637 fix(model-types) : extract error field from 409 response for user-friendly messages
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 11:47:51 +01:00
Matthieu
db630e315b fix(custom-fields) : preserve CustomField ID in piece structure payload
Prevents data loss when saving ModelType: the frontend now sends existing
CustomField IDs so the backend can match them instead of deleting and recreating.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 19:07:37 +01:00
Matthieu
53530dc16d fix(piece-edit) : stay on page after saving piece
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 16:12:15 +01:00
Matthieu
974b74ee9f fix(SearchSelect) : render option-description slot even without optionDescription prop
The v-if on resolveDescription() was hiding the entire slot when
optionDescription prop was not provided. Now checks for slot presence
first, allowing custom formatDescription in PieceSelect/ProductSelect/
ComposantSelect to render properly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 15:54:24 +01:00
Matthieu
ab05ce589d fix(ui) : show type name and ref in slot selects, stay on page after save
- PieceSelect, ProductSelect, ComposantSelect: show type name and
  "Ref." prefix in dropdown descriptions (matching create page format)
- Category edit pages (component, piece, product): stay on page after
  successful save instead of navigating back to list
- Component and product edit pages: same — stay on page after save

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 15:52:02 +01:00
Matthieu
ce3f081a0a refactor(category) : remove quantity field from category structure editor
Quantity is now managed per-component on the component edit page,
not at the category level.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 13:52:49 +01:00
Matthieu
63fba4138e perf(component-edit) : remove redundant full-catalog loads on mount
The 3 loadPieces/loadProducts/loadComposants(200) calls on mount were
redundant since select components now load filtered data server-side.
Removing them eliminates ~3 heavy API calls + constructeur resolution
per page load.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 13:50:34 +01:00
Matthieu
d58a8c2479 feat(component-edit) : add inline quantity input for piece slots
Quantity can now be edited directly on the component edit page next to
each piece selector, instead of only being defined in the category.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 13:34:51 +01:00
Matthieu
81f7b1a9ac feat(component-edit) : add link to category edit page from component editor
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 13:32:54 +01:00
Matthieu
9e303426a7 fix(slots) : filter slot select options server-side instead of client-side
PieceSelect, ProductSelect and ComposantSelect were loading up to 200
items then filtering client-side by typeId. If the matching items were
not in the first 200, the dropdown appeared empty.

Now each select component uses API Platform filters (typePiece,
typeProduct, typeComposant) to fetch only relevant items server-side,
with local state to avoid overwriting the global catalog cache.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 11:59:51 +01:00
24 changed files with 235 additions and 191 deletions

View File

@@ -25,7 +25,7 @@
</template>
<script setup lang="ts">
import { computed, onMounted, watch } from 'vue'
import { computed, onMounted, ref, watch } from 'vue'
import SearchSelect from '~/components/common/SearchSelect.vue'
import { useComposants } from '~/composables/useComposants'
@@ -52,43 +52,39 @@ const emit = defineEmits<{
(e: 'update:modelValue', value: string | null): void
}>()
const { composants, loading, loadComposants } = useComposants()
const { loading: globalLoading, loadComposants } = useComposants()
const composantOptions = computed(() => {
const baseOptions = Array.isArray(composants.value) ? composants.value : []
if (!props.typeComposantId) {
return baseOptions
const localComposants = ref<any[]>([])
const localLoading = ref(false)
const loading = computed(() => localLoading.value || globalLoading.value)
const composantOptions = computed(() => localComposants.value)
const loadFilteredComposants = async () => {
if (!props.typeComposantId) return
localLoading.value = true
try {
const result = await loadComposants({ typeComposantId: props.typeComposantId, itemsPerPage: 500, force: true })
if (result.success && result.data?.items) {
localComposants.value = result.data.items
}
}
const allowedTypeId = String(props.typeComposantId)
return baseOptions.filter((composant: any) => {
const typeId =
composant?.typeComposantId ||
composant?.typeComposant?.id ||
null
return typeId ? String(typeId) === allowedTypeId : false
})
})
catch (error: unknown) {
console.error('Erreur lors du chargement des composants:', error)
}
finally {
localLoading.value = false
}
}
onMounted(() => {
if (composantOptions.value.length === 0) {
loadComposants({ itemsPerPage: 200 }).catch((error: unknown) => {
console.error('Erreur lors du chargement des composants:', error)
})
}
loadFilteredComposants()
})
watch(
() => props.modelValue,
(value) => {
if (typeof value === 'string' && value) {
const exists = composantOptions.value.some((c: any) => c.id === value)
if (!exists && !loading.value) {
loadComposants({ itemsPerPage: 200, force: true }).catch((error: unknown) => {
console.error('Erreur lors du chargement des composants:', error)
})
}
}
() => props.typeComposantId,
() => {
loadFilteredComposants()
},
)
@@ -102,8 +98,12 @@ const updateValue = (value: string | number | null | undefined) => {
const formatDescription = (option: any) => {
const parts: string[] = []
const typeName = option?.typeComposant?.name
if (typeName) {
parts.push(typeName)
}
if (option?.reference) {
parts.push(option.reference)
parts.push(`Ref. ${option.reference}`)
}
if (option?.prix !== undefined && option.prix !== null) {
const price = Number(option.prix)

View File

@@ -25,7 +25,7 @@
</template>
<script setup lang="ts">
import { computed, onMounted, watch } from 'vue'
import { computed, onMounted, ref, watch } from 'vue'
import SearchSelect from '~/components/common/SearchSelect.vue'
import { usePieces } from '~/composables/usePieces'
@@ -52,43 +52,39 @@ const emit = defineEmits<{
(e: 'update:modelValue', value: string | null): void
}>()
const { pieces, loading, loadPieces } = usePieces()
const { loading: globalLoading, loadPieces } = usePieces()
const pieceOptions = computed(() => {
const baseOptions = Array.isArray(pieces.value) ? pieces.value : []
if (!props.typePieceId) {
return baseOptions
const localPieces = ref<any[]>([])
const localLoading = ref(false)
const loading = computed(() => localLoading.value || globalLoading.value)
const pieceOptions = computed(() => localPieces.value)
const loadFilteredPieces = async () => {
if (!props.typePieceId) return
localLoading.value = true
try {
const result = await loadPieces({ typePieceId: props.typePieceId, itemsPerPage: 500, force: true })
if (result.success && result.data?.items) {
localPieces.value = result.data.items
}
}
const allowedTypeId = String(props.typePieceId)
return baseOptions.filter((piece: any) => {
const typeId =
piece?.typePieceId ||
piece?.typePiece?.id ||
null
return typeId ? String(typeId) === allowedTypeId : false
})
})
catch (error: unknown) {
console.error('Erreur lors du chargement des pièces:', error)
}
finally {
localLoading.value = false
}
}
onMounted(() => {
if (pieceOptions.value.length === 0) {
loadPieces({ itemsPerPage: 200 }).catch((error: unknown) => {
console.error('Erreur lors du chargement des pièces:', error)
})
}
loadFilteredPieces()
})
watch(
() => props.modelValue,
(value) => {
if (typeof value === 'string' && value) {
const exists = pieceOptions.value.some((piece: any) => piece.id === value)
if (!exists && !loading.value) {
loadPieces({ itemsPerPage: 200, force: true }).catch((error: unknown) => {
console.error('Erreur lors du chargement des pièces:', error)
})
}
}
() => props.typePieceId,
() => {
loadFilteredPieces()
},
)
@@ -102,8 +98,12 @@ const updateValue = (value: string | number | null | undefined) => {
const formatDescription = (option: any) => {
const parts: string[] = []
const typeName = option?.typePiece?.name
if (typeName) {
parts.push(typeName)
}
if (option?.reference) {
parts.push(option.reference)
parts.push(`Ref. ${option.reference}`)
}
if (option?.prix !== undefined && option.prix !== null) {
const price = Number(option.prix)

View File

@@ -25,7 +25,7 @@
</template>
<script setup lang="ts">
import { computed, onMounted, watch } from 'vue'
import { computed, onMounted, ref, watch } from 'vue'
import SearchSelect from '~/components/common/SearchSelect.vue'
import { useProducts } from '~/composables/useProducts'
@@ -52,43 +52,39 @@ const emit = defineEmits<{
(e: 'update:modelValue', value: string | null): void
}>()
const { products, loading, loadProducts } = useProducts()
const { loading: globalLoading, loadProducts } = useProducts()
const productOptions = computed(() => {
const baseOptions = Array.isArray(products.value) ? products.value : []
if (!props.typeProductId) {
return baseOptions
const localProducts = ref<any[]>([])
const localLoading = ref(false)
const loading = computed(() => localLoading.value || globalLoading.value)
const productOptions = computed(() => localProducts.value)
const loadFilteredProducts = async () => {
if (!props.typeProductId) return
localLoading.value = true
try {
const result = await loadProducts({ typeProductId: props.typeProductId, itemsPerPage: 500, force: true })
if (result.success && result.data?.items) {
localProducts.value = result.data.items
}
}
const allowedTypeId = String(props.typeProductId)
return baseOptions.filter((product) => {
const typeId =
product?.typeProductId ||
product?.typeProduct?.id ||
null
return typeId ? String(typeId) === allowedTypeId : false
})
})
catch (error: unknown) {
console.error('Erreur lors du chargement des produits:', error)
}
finally {
localLoading.value = false
}
}
onMounted(() => {
if (productOptions.value.length === 0) {
loadProducts().catch((error) => {
console.error('Erreur lors du chargement des produits:', error)
})
}
loadFilteredProducts()
})
watch(
() => props.modelValue,
(value) => {
if (typeof value === 'string' && value) {
const exists = productOptions.value.some((product) => product.id === value)
if (!exists && !loading.value) {
loadProducts({ force: true }).catch((error) => {
console.error('Erreur lors du chargement des produits:', error)
})
}
}
() => props.typeProductId,
() => {
loadFilteredProducts()
},
)
@@ -102,8 +98,12 @@ const updateValue = (value: string | number | null | undefined) => {
const formatDescription = (option: any) => {
const parts: string[] = []
const typeName = option?.typeProduct?.name
if (typeName) {
parts.push(typeName)
}
if (option?.reference) {
parts.push(option.reference)
parts.push(`Ref. ${option.reference}`)
}
if (option?.supplierPrice !== undefined && option.supplierPrice !== null) {
const price = Number(option.supplierPrice)

View File

@@ -258,18 +258,7 @@
{{ piece.typePieceId ? `Sélection : ${getPieceTypeLabel(piece.typePieceId) || 'Inconnue'}` : 'Aucune famille sélectionnée' }}
</p>
</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>
<!-- Quantity is set per-component on the component edit page -->
</div>
<button type="button" class="btn btn-error btn-xs btn-square" @click="removePiece(index)">
<IconLucideTrash class="w-4 h-4" aria-hidden="true" />

View File

@@ -69,7 +69,7 @@
{{ resolveLabel(option) }}
</slot>
</span>
<span v-if="resolveDescription(option)" class="text-xs text-base-content/50">
<span v-if="$slots['option-description'] || resolveDescription(option)" class="text-xs text-base-content/50">
<slot name="option-description" :option="option">
{{ resolveDescription(option) }}
</slot>

View File

@@ -344,7 +344,7 @@ export function useComponentCreate() {
selectedDocuments.value = []
}
toast.showSuccess('Composant créé avec succès')
await router.push('/component-catalog')
await router.push(`/component/${createdComponent.id}/edit`)
}
else if (result.error) {
toast.showError(result.error)

View File

@@ -7,7 +7,6 @@ import { useProductTypes } from '~/composables/useProductTypes'
import { usePieces } from '~/composables/usePieces'
import { useProducts } from '~/composables/useProducts'
import { useCustomFields } from '~/composables/useCustomFields'
import type { SelectionEntry } from '~/shared/utils/structureSelectionUtils'
import { useApi } from '~/composables/useApi'
import { useToast } from '~/composables/useToast'
import { extractRelationId } from '~/shared/apiRelations'
@@ -58,9 +57,9 @@ export function useComponentEdit(componentId: string) {
const { componentTypes, loadComponentTypes } = useComponentTypes()
const { pieceTypes, loadPieceTypes } = usePieceTypes()
const { productTypes, loadProductTypes } = useProductTypes()
const { updateComposant, loadComposants, composants: componentCatalogRef } = useComposants()
const { pieces, loadPieces } = usePieces()
const { products, loadProducts } = useProducts()
const { updateComposant, composants: componentCatalogRef } = useComposants()
const { pieces } = usePieces()
const { products } = useProducts()
const { ensureConstructeurs } = useConstructeurs()
const { upsertCustomFieldValue, updateCustomFieldValue } = useCustomFields()
const toast = useToast()
@@ -348,19 +347,17 @@ 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 })
const saveSlotQuantity = async (slotId: string, quantity: number) => {
if (!slotId || quantity < 1) return
const result = await patch(`/composant-piece-slots/${slotId}`, { quantity: Math.max(1, quantity) })
if (result.success) {
const structure = component.value?.structure
if (structure?.pieces) {
const slot = (structure.pieces as any[]).find((s: any) => s.slotId === slotId)
if (slot) slot.quantity = 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 () => {
@@ -406,7 +403,7 @@ export function useComponentEdit(componentId: string) {
],
{ customFieldInputs, upsertCustomFieldValue, updateCustomFieldValue, toast },
)
await router.push('/component-catalog')
toast.showSuccess('Composant mis à jour avec succès.')
}
}
catch (error: any) {
@@ -500,13 +497,6 @@ export function useComponentEdit(componentId: string) {
fetchComponent(),
])
loading.value = false
// Load catalogs for slot selectors (force: true to bypass cache from list pages that load fewer items)
Promise.allSettled([
loadPieces({ itemsPerPage: 200, force: true }),
loadProducts({ itemsPerPage: 200, force: true }),
loadComposants({ itemsPerPage: 200, force: true }),
]).catch(() => {})
})
return {

View File

@@ -42,6 +42,7 @@ interface LoadComposantsOptions {
orderBy?: string
orderDir?: 'asc' | 'desc'
typeName?: string
typeComposantId?: string
force?: boolean
}
@@ -109,17 +110,18 @@ export function useComposants() {
orderBy = 'name',
orderDir = 'asc',
typeName,
typeComposantId,
force = false,
} = options
if (!force && loaded.value && !search && !typeName && page === 1) {
if (!force && loaded.value && !search && !typeName && !typeComposantId && page === 1) {
return {
success: true,
data: { items: composants.value, total: total.value, page, itemsPerPage },
}
}
if (loading.value) {
if (!typeComposantId && loading.value) {
return {
success: true,
data: { items: composants.value, total: total.value, page, itemsPerPage },
@@ -128,7 +130,6 @@ export function useComposants() {
loading.value = true
try {
const params = new URLSearchParams()
params.set('itemsPerPage', String(itemsPerPage))
params.set('page', String(page))
@@ -141,20 +142,29 @@ export function useComposants() {
params.set('typeComposant.name', typeName.trim())
}
if (typeComposantId) {
params.set('typeComposant', typeComposantId)
}
params.set(`order[${orderBy}]`, orderDir)
const result = await get(`/composants?${params.toString()}`)
if (result.success) {
const items = extractCollection(result.data)
const enrichedItems = await Promise.all(items.map((item) => withResolvedConstructeurs(item)))
composants.value = enrichedItems
total.value = extractTotal(result.data, items.length)
loaded.value = true
const resultTotal = extractTotal(result.data, items.length)
if (!typeComposantId) {
composants.value = enrichedItems
total.value = resultTotal
loaded.value = true
}
return {
success: true,
data: {
items: enrichedItems,
total: total.value,
total: resultTotal,
page,
itemsPerPage,
},

View File

@@ -127,8 +127,8 @@ export function useEntityTypes(config: EntityTypeConfig) {
showSuccess(`Type de ${label} "${data.name}" créé`)
return { success: true, data: normalized }
} catch (error) {
const err = error as Error & { data?: { message?: string }; message?: string }
const raw = err?.data?.message || err?.message
const err = error as Error & { data?: { error?: string; message?: string }; message?: string }
const raw = err?.data?.error || err?.data?.message || err?.message
const message = humanizeError(raw)
showError(`Impossible de créer le type de ${label} : ${message}`)
return { success: false, error: message }
@@ -153,8 +153,8 @@ export function useEntityTypes(config: EntityTypeConfig) {
showSuccess(`Type de ${label} "${data.name}" mis à jour`)
return { success: true, data: normalized }
} catch (error) {
const err = error as Error & { data?: { message?: string }; message?: string }
const raw = err?.data?.message || err?.message
const err = error as Error & { data?: { error?: string; message?: string }; message?: string }
const raw = err?.data?.error || err?.data?.message || err?.message
const message = humanizeError(raw)
showError(`Impossible de mettre à jour le type de ${label} : ${message}`)
return { success: false, error: message }
@@ -171,8 +171,8 @@ export function useEntityTypes(config: EntityTypeConfig) {
showSuccess(`Type de ${label} supprimé`)
return { success: true }
} catch (error) {
const err = error as Error & { data?: { message?: string }; message?: string }
const raw = err?.data?.message || err?.message
const err = error as Error & { data?: { error?: string; message?: string }; message?: string }
const raw = err?.data?.error || err?.data?.message || err?.message
const message = humanizeError(raw)
showError(`Impossible de supprimer le type de ${label} : ${message}`)
return { success: false, error: message }

View File

@@ -273,6 +273,7 @@ export const buildMachineHierarchyFromLinks = (
originalComposant: originalComponent,
machineComponentLink: link,
machineComponentLinkId,
linkId: machineComponentLinkId,
componentLinkId: machineComponentLinkId,
parentComponentLinkId: resolveIdentifier(link.parentComponentLinkId, link.parentLinkId, link.parentMachineComponentLinkId, appliedComponent.parentComponentLinkId),
parentComposantId: resolveIdentifier(appliedComponent.parentComposantId, link.parentComponentId),

View File

@@ -412,7 +412,7 @@ export function usePieceEdit(pieceId: string) {
],
{ customFieldInputs, upsertCustomFieldValue, updateCustomFieldValue, toast },
)
await router.push('/pieces-catalog')
toast.showSuccess('Pièce mise à jour avec succès.')
}
}
catch (error: any) {

View File

@@ -158,6 +158,13 @@ const buildPayload = (
orderIndex: index,
}
if (field.id) {
payload.id = field.id
}
if (field.customFieldId) {
payload.customFieldId = field.customFieldId
}
if (type === 'select') {
const options = normalizeLineEndings(field.optionsText)
.split('\n')

View File

@@ -43,6 +43,7 @@ interface LoadPiecesOptions {
orderBy?: string
orderDir?: 'asc' | 'desc'
typeName?: string
typePieceId?: string
force?: boolean
}
@@ -119,17 +120,20 @@ export function usePieces() {
orderBy = 'name',
orderDir = 'asc',
typeName,
typePieceId,
force = false,
} = options
if (!force && loaded.value && !search && !typeName && page === 1) {
// Only use cache for unfiltered full-catalog loads
if (!force && loaded.value && !search && !typeName && !typePieceId && page === 1) {
return {
success: true,
data: { items: pieces.value, total: total.value, page, itemsPerPage },
}
}
if (loading.value) {
// For filtered queries, don't block on global loading state
if (!typePieceId && loading.value) {
return {
success: true,
data: { items: pieces.value, total: total.value, page, itemsPerPage },
@@ -138,7 +142,6 @@ export function usePieces() {
loading.value = true
try {
const params = new URLSearchParams()
params.set('itemsPerPage', String(itemsPerPage))
params.set('page', String(page))
@@ -151,20 +154,30 @@ export function usePieces() {
params.set('typePiece.name', typeName.trim())
}
if (typePieceId) {
params.set('typePiece', typePieceId)
}
params.set(`order[${orderBy}]`, orderDir)
const result = await get(`/pieces?${params.toString()}`)
if (result.success) {
const items = extractCollection(result.data)
const enrichedItems = await Promise.all(items.map((item) => withResolvedConstructeurs(item)))
pieces.value = enrichedItems
total.value = extractTotal(result.data, items.length)
loaded.value = true
const resultTotal = extractTotal(result.data, items.length)
// Only update global cache for unfiltered queries
if (!typePieceId) {
pieces.value = enrichedItems
total.value = resultTotal
loaded.value = true
}
return {
success: true,
data: {
items: enrichedItems,
total: total.value,
total: resultTotal,
page,
itemsPerPage,
},

View File

@@ -41,6 +41,7 @@ interface LoadProductsOptions {
orderBy?: string
orderDir?: 'asc' | 'desc'
typeName?: string
typeProductId?: string
force?: boolean
}
@@ -118,17 +119,18 @@ export function useProducts() {
orderBy = 'name',
orderDir = 'asc',
typeName,
typeProductId,
force = false,
} = options
if (!force && loaded.value && !search && !typeName && page === 1) {
if (!force && loaded.value && !search && !typeName && !typeProductId && page === 1) {
return {
success: true,
data: { items: products.value, total: total.value, page, itemsPerPage },
}
}
if (loading.value) {
if (!typeProductId && loading.value) {
return {
success: true,
data: { items: products.value, total: total.value, page, itemsPerPage },
@@ -150,20 +152,29 @@ export function useProducts() {
params.set('typeProduct.name', typeName.trim())
}
if (typeProductId) {
params.set('typeProduct', typeProductId)
}
params.set(`order[${orderBy}]`, orderDir)
const result = await get(`/products?${params.toString()}`)
if (result.success) {
const items = extractCollection(result.data)
const enrichedItems = await Promise.all(items.map((item) => withResolvedConstructeurs(item)))
products.value = enrichedItems
total.value = extractTotal(result.data, items.length)
loaded.value = true
const resultTotal = extractTotal(result.data, items.length)
if (!typeProductId) {
products.value = enrichedItems
total.value = resultTotal
loaded.value = true
}
return {
success: true,
data: {
items: enrichedItems,
total: total.value,
total: resultTotal,
page,
itemsPerPage,
},

View File

@@ -158,7 +158,6 @@ const handleSubmit = async (payload: Parameters<typeof updateModelType>[1]) => {
await syncExecute(id, { confirmDeletions: false, confirmTypeChanges: false })
await loadComponentTypes({ force: true })
showSuccess('Catégorie de composant mise à jour avec succès.')
await navigateBackToList()
}
} catch (error) {
showError(normalizeError(error))
@@ -183,7 +182,6 @@ const handleSyncConfirm = async () => {
})
await loadComponentTypes({ force: true })
showSuccess('Catégorie de composant mise à jour avec succès.')
await navigateBackToList()
} catch (error) {
showError(normalizeError(error))
} finally {

View File

@@ -45,20 +45,33 @@
<label class="label">
<span class="label-text">Catégorie de composant</span>
</label>
<select
v-model="selectedTypeId"
class="select select-bordered select-sm md:select-md"
disabled
>
<option value="">Sélectionner une catégorie</option>
<option
v-for="type in componentTypeList"
:key="type.id"
:value="type.id"
<div class="flex items-center gap-2">
<select
v-model="selectedTypeId"
class="select select-bordered select-sm md:select-md flex-1"
disabled
>
{{ type.name }}
</option>
</select>
<option value="">Sélectionner une catégorie</option>
<option
v-for="type in componentTypeList"
:key="type.id"
:value="type.id"
>
{{ type.name }}
</option>
</select>
<NuxtLink
v-if="selectedTypeId"
:to="`/component-category/${selectedTypeId}/edit`"
class="btn btn-ghost btn-sm"
title="Voir la catégorie"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path d="M11 3a1 1 0 100 2h2.586l-6.293 6.293a1 1 0 101.414 1.414L15 6.414V9a1 1 0 102 0V4a1 1 0 00-1-1h-5z" />
<path d="M5 5a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002-2v-3a1 1 0 10-2 0v3H5V7h3a1 1 0 000-2H5z" />
</svg>
</NuxtLink>
</div>
<p class="text-xs text-base-content/60 mt-1">
La catégorie d'origine ne peut pas être modifiée depuis cette page.
</p>
@@ -173,12 +186,27 @@
<label class="label">
<span class="label-text text-xs font-medium">{{ slot.label }}</span>
</label>
<PieceSelect
:model-value="slot.selectedPieceId"
:disabled="!canEdit || saving"
:type-piece-id="slot.typePieceId"
@update:model-value="(value) => savePieceSlotSelection(slot.slotId, value)"
/>
<div class="flex items-start gap-2">
<div class="flex-1">
<PieceSelect
:model-value="slot.selectedPieceId"
:disabled="!canEdit || saving"
:type-piece-id="slot.typePieceId"
@update:model-value="(value) => savePieceSlotSelection(slot.slotId, value)"
/>
</div>
<div class="w-20 shrink-0">
<input
type="number"
:value="slot.quantity"
min="1"
class="input input-bordered input-sm w-full text-center"
:disabled="!canEdit || saving"
title="Quantité"
@change="(e) => saveSlotQuantity(slot.slotId, Number((e.target as HTMLInputElement).value))"
>
</div>
</div>
</div>
</div>
</div>

View File

@@ -156,7 +156,6 @@ const handleSubmit = async (payload: Parameters<typeof updateModelType>[1]) => {
await syncExecute(id, { confirmDeletions: false, confirmTypeChanges: false })
await loadPieceTypes({ force: true })
showSuccess('Catégorie de pièce mise à jour avec succès.')
await navigateBackToList()
}
} catch (error) {
showError(normalizeError(error))
@@ -181,7 +180,6 @@ const handleSyncConfirm = async () => {
})
await loadPieceTypes({ force: true })
showSuccess('Catégorie de pièce mise à jour avec succès.')
await navigateBackToList()
} catch (error) {
showError(normalizeError(error))
} finally {

View File

@@ -466,7 +466,7 @@ const submitCreation = async () => {
selectedDocuments.value = []
}
toast.showSuccess('Pièce créée avec succès')
await router.push('/pieces-catalog')
await router.push(`/pieces/${createdPiece.id}/edit`)
} else if (result.error) {
toast.showError(result.error)
}

View File

@@ -156,7 +156,6 @@ const handleSubmit = async (payload: Parameters<typeof updateModelType>[1]) => {
await syncExecute(id, { confirmDeletions: false, confirmTypeChanges: false })
await loadProductTypes({ force: true })
showSuccess('Catégorie de produit mise à jour avec succès.')
await navigateBackToList()
}
} catch (error) {
showError(normalizeError(error))
@@ -181,7 +180,6 @@ const handleSyncConfirm = async () => {
})
await loadProductTypes({ force: true })
showSuccess('Catégorie de produit mise à jour avec succès.')
await navigateBackToList()
} catch (error) {
showError(normalizeError(error))
} finally {

View File

@@ -482,7 +482,6 @@ const submitEdition = async () => {
return
}
toast.showSuccess('Produit mis à jour avec succès')
await router.push('/product-catalog')
}
} catch (error: any) {
toast.showError(humanizeError(error?.message) || 'Impossible de mettre à jour le produit')

View File

@@ -352,7 +352,7 @@ const submitCreation = async () => {
}
}
toast.showSuccess('Produit créé avec succès')
await router.push('/product-catalog')
await router.push(`/product/${productId}/edit`)
}
} catch (error: any) {
toast.showError(error?.message || 'Erreur lors de la création du produit')

View File

@@ -155,9 +155,7 @@ export const buildConstructeurRequestPayload = <T extends Record<string, any>>(
delete next.constructeurs;
delete next.constructeurIds;
if (ids.length) {
next.constructeurs = ids.map((id) => `/api/constructeurs/${id}`);
}
next.constructeurs = ids.map((id) => `/api/constructeurs/${id}`);
return next as T & { constructeurs?: string[] };
};

View File

@@ -125,6 +125,8 @@ const hydratePieceCustomFields = (fields: any[]): PieceModelStructureEditorField
? field.options.join('\n')
: '',
orderIndex: typeof field?.orderIndex === 'number' ? field.orderIndex : index,
...(field?.id ? { id: field.id } : {}),
...(field?.customFieldId ? { customFieldId: field.customFieldId } : {}),
}))
}

View File

@@ -61,6 +61,8 @@ export interface PieceModelCustomField {
key?: string
value?: unknown
defaultValue?: string | null
id?: string
customFieldId?: string
}
export interface PieceModelProduct {