Compare commits

...

10 Commits

Author SHA1 Message Date
gitea-actions
4e0efc11ba chore : bump version to v1.9.23
All checks were successful
Auto Tag Develop / tag (push) Successful in 9s
Build & Push Docker Image / build (push) Successful in 38s
2026-04-06 15:18:20 +00:00
9fc88df3ff fix(piece) : rendre les slots produit optionnels en création et édition
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>
2026-04-06 17:18:10 +02:00
gitea-actions
041a04f0e9 chore : bump version to v1.9.22
All checks were successful
Auto Tag Develop / tag (push) Successful in 8s
Build & Push Docker Image / build (push) Successful in 36s
2026-04-06 15:15:37 +00:00
d089cd4873 fix(model-type) : masquer uniquement les produits, garder les champs perso
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
Ajoute une prop hideProducts au PieceModelStructureEditor pour masquer
la section « Produits inclus par défaut » sans retirer les champs
personnalisés. Utilisé pour les catégories PRODUCT.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 17:15:26 +02:00
gitea-actions
b304cf6684 chore : bump version to v1.9.21
All checks were successful
Auto Tag Develop / tag (push) Successful in 8s
Build & Push Docker Image / build (push) Successful in 36s
2026-04-06 15:12:40 +00:00
0fe7f3131e fix(model-type) : retirer l'éditeur de structure produit inutilisé
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
Le PieceModelStructureEditor affiché pour les catégories PRODUCT ne
fonctionnait plus et n'est plus utilisé.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 17:12:29 +02:00
a6bbcaf6d1 fix(custom-fields) : masquer les champs machineContextOnly hors vue machine
Ajoute context: 'standalone' aux appels useCustomFieldInputs dans les
vues composant, pièce et produit (création et édition) pour filtrer
les champs perso réservés au contexte machine.

Exclut également ces champs de la formule de référence automatique
dans le ReferenceFormulaBuilder des catégories.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 17:12:29 +02:00
9f2e1da6ec fix(composant) : rendre les slots de structure optionnels à la création
Les emplacements pièces, produits et sous-composants du squelette ne
bloquent plus la soumission du formulaire de création de composant.
Les slots vides restent visibles en consultation avec l'indicateur rouge
« manquant » et peuvent être remplis ultérieurement en édition.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 17:12:29 +02:00
gitea-actions
7962576eec chore : bump version to v1.9.20
All checks were successful
Auto Tag Develop / tag (push) Successful in 8s
Build & Push Docker Image / build (push) Successful in 37s
2026-04-06 14:54:20 +00:00
7d98c1598c fix(deps) : update composer.lock with symfony/mime
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 16:54:09 +02:00
11 changed files with 225 additions and 63 deletions

175
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "2db01f705a09cf38007a2baa3b078e49",
"content-hash": "f94dc3c05e9ba6be99c510aad3d17182",
"packages": [
{
"name": "api-platform/doctrine-common",
@@ -5341,6 +5341,92 @@
],
"time": "2026-03-04T16:39:24+00:00"
},
{
"name": "symfony/mime",
"version": "v8.0.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/mime.git",
"reference": "ddff21f14c7ce04b98101b399a9463dce8b0ce66"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/mime/zipball/ddff21f14c7ce04b98101b399a9463dce8b0ce66",
"reference": "ddff21f14c7ce04b98101b399a9463dce8b0ce66",
"shasum": ""
},
"require": {
"php": ">=8.4",
"symfony/polyfill-intl-idn": "^1.10",
"symfony/polyfill-mbstring": "^1.0"
},
"conflict": {
"egulias/email-validator": "~3.0.0",
"phpdocumentor/reflection-docblock": "<5.2|>=7",
"phpdocumentor/type-resolver": "<1.5.1"
},
"require-dev": {
"egulias/email-validator": "^2.1.10|^3.1|^4",
"league/html-to-markdown": "^5.0",
"phpdocumentor/reflection-docblock": "^5.2|^6.0",
"symfony/dependency-injection": "^7.4|^8.0",
"symfony/process": "^7.4|^8.0",
"symfony/property-access": "^7.4|^8.0",
"symfony/property-info": "^7.4|^8.0",
"symfony/serializer": "^7.4|^8.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Mime\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Allows manipulating MIME messages",
"homepage": "https://symfony.com",
"keywords": [
"mime",
"mime-type"
],
"support": {
"source": "https://github.com/symfony/mime/tree/v8.0.8"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-03-30T15:14:47+00:00"
},
{
"name": "symfony/options-resolver",
"version": "v8.0.0",
@@ -5567,6 +5653,93 @@
],
"time": "2025-06-27T09:58:17+00:00"
},
{
"name": "symfony/polyfill-intl-idn",
"version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-idn.git",
"reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3",
"reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3",
"shasum": ""
},
"require": {
"php": ">=7.2",
"symfony/polyfill-intl-normalizer": "^1.10"
},
"suggest": {
"ext-intl": "For best performance"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Intl\\Idn\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Laurent Bassin",
"email": "laurent@bassin.info"
},
{
"name": "Trevor Rowbotham",
"email": "trevor.rowbotham@pm.me"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"idn",
"intl",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.33.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-09-10T14:38:51+00:00"
},
{
"name": "symfony/polyfill-intl-normalizer",
"version": "v1.33.0",

View File

@@ -1,2 +1,2 @@
parameters:
app.version: '1.9.19'
app.version: '1.9.23'

View File

@@ -1,6 +1,6 @@
<template>
<div class="space-y-6">
<section class="space-y-3">
<section v-if="!hideProducts" class="space-y-3">
<header>
<h3 class="text-sm font-semibold">
Produits inclus par défaut
@@ -166,6 +166,7 @@ defineOptions({ name: 'PieceModelStructureEditor' })
const props = defineProps<{
modelValue?: PieceModelStructure | null
hideProducts?: boolean
}>()
const emit = defineEmits<{

View File

@@ -99,11 +99,7 @@
v-else
class="space-y-3 rounded-lg border border-base-300 p-4"
>
<p class="text-sm text-base-content/70">
Aperçu :
<span class="font-medium text-base-content">{{ productStructurePreview }}</span>
</p>
<PieceModelStructureEditor v-model="productStructure" />
<PieceModelStructureEditor v-model="productStructure" hide-products />
</div>
</template>
</section>
@@ -194,15 +190,16 @@ const form = reactive<ModelTypePayload & { referenceFormula?: string | null }>({
})
const formulaBuilderCustomFields = computed(() => {
let fields: any[] = []
if (form.category === 'PIECE') {
const fields = pieceStructure.value?.customFields
return Array.isArray(fields) ? fields : []
const raw = pieceStructure.value?.customFields
fields = Array.isArray(raw) ? raw : []
}
if (form.category === 'COMPONENT') {
const fields = componentStructure.value?.customFields
return Array.isArray(fields) ? fields : []
else if (form.category === 'COMPONENT') {
const raw = componentStructure.value?.customFields
fields = Array.isArray(raw) ? raw : []
}
return []
return fields.filter((f: any) => !f.machineContextOnly)
})
const extractFormulaFields = (formula: string | null | undefined): string[] => {

View File

@@ -34,7 +34,6 @@ import {
import {
hasAssignments,
initializeStructureAssignments,
isAssignmentNodeComplete,
serializeStructureAssignments,
} from '~/shared/utils/structureAssignmentHelpers'
import type { ComponentModelStructure } from '~/shared/types/inventory'
@@ -152,24 +151,14 @@ export function useComponentCreate() {
values: computed(() => []),
entityType: 'composant',
entityId: createdComponentId,
context: 'standalone',
})
const structureHasRequirements = computed(() =>
hasAssignments(structureAssignments.value),
)
const structureSelectionsComplete = computed(() => {
if (!structureHasRequirements.value) {
return true
}
if (structureDataLoading.value) {
return false
}
if (!structureAssignments.value) {
return false
}
return isAssignmentNodeComplete(structureAssignments.value, true)
})
const structureSelectionsComplete = computed(() => true)
const canSubmit = computed(() => Boolean(
canEdit.value
@@ -307,11 +296,6 @@ export function useComponentCreate() {
payload.productId = rootProductSelection.selectedProductId.trim()
}
if (structureHasRequirements.value && !structureSelectionsComplete.value) {
toast.showError('Complétez la sélection des pièces, produits et sous-composants.')
return
}
const serializedStructure = structureHasRequirements.value
? serializeStructureAssignments(structureAssignments.value)
: null
@@ -414,6 +398,7 @@ export function useComponentCreate() {
structureSelectionsComplete,
canEdit,
canSubmit,
requiredCustomFieldsFilled,
// Functions
typeOptionLabel,

View File

@@ -209,6 +209,7 @@ export function useComponentEdit(componentId: string) {
values: computed(() => component.value?.customFieldValues ?? []),
entityType: 'composant',
entityId: computed(() => component.value?.id ?? null),
context: 'standalone',
onValueCreated: (newValue) => {
if (component.value && Array.isArray(component.value.customFieldValues)) {
component.value.customFieldValues.push(newValue)
@@ -556,6 +557,7 @@ export function useComponentEdit(componentId: string) {
originalConstructeurLinks,
constructeurIdsFromForm,
customFieldInputs,
requiredCustomFieldsFilled,
historyFieldLabels,
// Computed

View File

@@ -20,7 +20,6 @@ import {
buildProductRequirementDescriptions,
buildProductRequirementEntries,
resizeProductSelections,
areProductSelectionsFilled,
applyProductSelection,
collectNormalizedProductIds,
} from '~/shared/utils/pieceProductSelectionUtils'
@@ -99,6 +98,7 @@ export function usePieceEdit(pieceId: string) {
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)
@@ -198,13 +198,7 @@ export function usePieceEdit(pieceId: string) {
buildProductRequirementEntries(structureProducts.value, 'piece-product-requirement'),
)
const productSelectionsFilled = computed(() =>
areProductSelectionsFilled(
requiresProductSelection.value,
productRequirementEntries.value,
productSelections.value,
),
)
const productSelectionsFilled = computed(() => true)
const setProductSelection = (index: number, value: string | null) => {
productSelections.value = applyProductSelection(productSelections.value, index, value)
@@ -354,11 +348,6 @@ export function usePieceEdit(pieceId: string) {
return
}
if (!productSelectionsFilled.value) {
toast.showError('Sélectionnez un produit conforme au squelette.')
return
}
const rawPrice = typeof editionForm.prix === 'string'
? editionForm.prix.trim()
: editionForm.prix === null || editionForm.prix === undefined
@@ -435,6 +424,7 @@ export function usePieceEdit(pieceId: string) {
constructeurIdsFromForm,
productSelections,
customFieldInputs,
requiredCustomFieldsFilled,
canEdit,
// Computed

View File

@@ -261,7 +261,7 @@
:model-value="productSelections[entry.index] || null"
:disabled="!canEdit || saving"
:type-product-id="entry.typeProductId"
helper-text="Un produit valide est requis pour cette pièce."
helper-text="Sélectionnez un produit (optionnel)."
@update:model-value="(value) => setProductSelection(entry.index, value)"
/>
</div>
@@ -359,6 +359,9 @@
</header>
<template v-if="isEditMode">
<CustomFieldInputGrid :fields="customFieldInputs" :disabled="!canEdit || saving" />
<p v-if="hasRequiredCustomFields && !requiredCustomFieldsFilled" class="text-xs text-warning">
Certains champs personnalisés sont obligatoires. Veuillez les renseigner avant de valider.
</p>
</template>
<template v-else>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
@@ -420,6 +423,9 @@
Enregistrer les modifications
</button>
</div>
<p v-if="isEditMode && hasRequiredCustomFields && !requiredCustomFieldsFilled" class="text-xs text-error text-right">
Merci de renseigner tous les champs personnalisés obligatoires.
</p>
</div>
</section>
</main>
@@ -460,6 +466,7 @@ const {
constructeurLinks,
productSelections,
customFieldInputs,
requiredCustomFieldsFilled,
pieceTypeList,
selectedType,
resolvedStructure,
@@ -481,6 +488,8 @@ const {
formatPieceStructurePreview,
} = usePieceEdit(String(route.params.id))
const hasRequiredCustomFields = computed(() => customFieldInputs.value.some(f => f.required))
const entityTabs = computed(() => [
{ key: 'general', label: 'Général' },
{ key: 'products', label: 'Produits liés', count: structureProducts.value.length },

View File

@@ -168,7 +168,7 @@
:model-value="productSelections[entry.index] || null"
:disabled="!canEdit || submitting || !selectedType"
:type-product-id="entry.typeProductId"
helper-text="Un produit est requis pour cette pièce."
helper-text="Sélectionnez un produit (optionnel)."
@update:model-value="(value) => setProductSelection(entry.index, value)"
/>
</div>
@@ -218,6 +218,9 @@
</p>
</header>
<CustomFieldInputGrid :fields="customFieldInputs" :disabled="!canEdit || submitting" />
<p v-if="hasRequiredCustomFields && !requiredCustomFieldsFilled" class="text-xs text-warning">
Certains champs personnalisés sont obligatoires. Veuillez les renseigner avant de valider.
</p>
</div>
<EmptyState
v-else
@@ -237,6 +240,9 @@
Créer la pièce
</button>
</div>
<p v-if="selectedType && hasRequiredCustomFields && !requiredCustomFieldsFilled" class="text-xs text-error text-right">
Merci de renseigner tous les champs personnalisés obligatoires avant de créer la pièce.
</p>
</div>
</section>
</main>
@@ -267,7 +273,6 @@ import {
buildProductRequirementDescriptions,
buildProductRequirementEntries,
resizeProductSelections,
areProductSelectionsFilled,
applyProductSelection,
collectNormalizedProductIds,
} from '~/shared/utils/pieceProductSelectionUtils'
@@ -311,7 +316,9 @@ const { fields: customFieldInputs, requiredFilled: requiredCustomFieldsFilled, s
values: [] as any[],
entityType: 'piece' as CustomFieldEntityType,
entityId: createdEntityId,
context: 'standalone',
})
const hasRequiredCustomFields = computed(() => customFieldInputs.value.some(f => f.required))
const selectedDocuments = ref<File[]>([])
const uploadingDocuments = ref(false)
@@ -371,13 +378,7 @@ const productRequirementEntries = computed(() =>
buildProductRequirementEntries(structureProducts.value, 'piece-create-product-requirement'),
)
const productSelectionsFilled = computed(() =>
areProductSelectionsFilled(
requiresProductSelection.value,
productRequirementEntries.value,
productSelections.value,
),
)
const productSelectionsFilled = computed(() => true)
const setProductSelection = (index: number, value: string | null) => {
productSelections.value = applyProductSelection(productSelections.value, index, value)
@@ -436,11 +437,6 @@ const submitCreation = async () => {
return
}
if (!productSelectionsFilled.value) {
toast.showError('Sélectionnez un produit conforme au squelette.')
return
}
const payload: Record<string, any> = {
name: creationForm.name.trim(),
typePieceId: selectedType.value.id,

View File

@@ -274,6 +274,9 @@
</header>
<template v-if="isEditMode">
<CustomFieldInputGrid :fields="customFieldInputs" :disabled="!canEdit || saving" />
<p v-if="hasRequiredCustomFields && !requiredCustomFieldsFilled" class="text-xs text-warning">
Certains champs personnalisés sont obligatoires. Veuillez les renseigner avant de valider.
</p>
</template>
<template v-else>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
@@ -338,7 +341,7 @@
Enregistrer les modifications
</button>
</div>
<p v-if="isEditMode && product && !requiredCustomFieldsFilled" class="text-xs text-error text-right">
<p v-if="isEditMode && hasRequiredCustomFields && !requiredCustomFieldsFilled" class="text-xs text-error text-right">
Merci de renseigner tous les champs personnalisés obligatoires.
</p>
</div>
@@ -409,6 +412,7 @@ const {
values: cfValues,
entityType: 'product' as CustomFieldEntityType,
entityId,
context: 'standalone',
})
const loading = ref(true)
const saving = ref(false)
@@ -446,7 +450,7 @@ const editionForm = reactive({
supplierPrice: '' as string,
})
// requiredCustomFieldsFilled comes from useCustomFieldInputs composable
const hasRequiredCustomFields = computed(() => customFieldInputs.value.some(f => f.required))
const canSubmit = computed(() =>
Boolean(canEdit.value && product.value && editionForm.name.trim().length >= 2 && requiredCustomFieldsFilled.value && !saving.value),

View File

@@ -158,6 +158,9 @@
</p>
</header>
<CustomFieldInputGrid :fields="customFieldInputs" :disabled="!canEdit || submitting" />
<p v-if="hasRequiredCustomFields && !requiredCustomFieldsFilled" class="text-xs text-warning">
Certains champs personnalisés sont obligatoires. Veuillez les renseigner avant de valider.
</p>
</div>
<EmptyState
v-else
@@ -177,7 +180,7 @@
Créer le produit
</button>
</div>
<p v-if="selectedType && !requiredCustomFieldsFilled" class="text-xs text-error text-right">
<p v-if="selectedType && hasRequiredCustomFields && !requiredCustomFieldsFilled" class="text-xs text-error text-right">
Merci de renseigner tous les champs personnalisés obligatoires.
</p>
</div>
@@ -241,7 +244,9 @@ const { fields: customFieldInputs, requiredFilled: requiredCustomFieldsFilled, s
values: [] as any[],
entityType: 'product' as CustomFieldEntityType,
entityId: createdEntityId,
context: 'standalone',
})
const hasRequiredCustomFields = computed(() => customFieldInputs.value.some(f => f.required))
const productTypeList = computed<ProductCatalogType[]>(() =>
(productTypes.value || []) as ProductCatalogType[],