Compare commits

...

6 Commits

Author SHA1 Message Date
gitea-actions
191e071957 chore : bump version to v1.9.25
All checks were successful
Auto Tag Develop / tag (push) Successful in 8s
Build & Push Docker Image / build (push) Successful in 35s
2026-04-06 16:54:32 +00:00
f964df76b9 feat(custom-fields) : messages warning champs obligatoires + commandes make frontend
All checks were successful
Auto Tag Develop / tag (push) Successful in 10s
Ajoute des messages visuels (warning + error) quand des champs perso
obligatoires ne sont pas renseignés sur les pages composant (création
et édition). Ajoute make test-front et make test-front-watch au Makefile.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 18:54:22 +02:00
gitea-actions
6744542f84 chore : bump version to v1.9.24
All checks were successful
Auto Tag Develop / tag (push) Successful in 8s
Build & Push Docker Image / build (push) Successful in 37s
2026-04-06 15:23:07 +00:00
3e0e9d5270 feat(categories) : aligner design catégories sur catalogues
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
- Ajoute colonne createdAt triable dans la datatable des catégories
- Retire le bouton « Créer » de la vue catégorie (ManagementView)
- Retire l'action « Convertir » de toutes les catégories
- Le bouton « Ajouter » des pages catalogue switch selon l'onglet
  actif : crée un item (catalogue) ou une catégorie (catégories)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 17:22:57 +02:00
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
11 changed files with 51 additions and 92 deletions

View File

@@ -1,2 +1,2 @@
parameters:
app.version: '1.9.22'
app.version: '1.9.25'

View File

@@ -57,16 +57,6 @@
/>
</label>
<button
v-if="canEdit"
type="button"
class="btn btn-primary btn-sm"
:disabled="loading"
@click="openCreatePage"
>
<IconLucidePlus class="w-4 h-4" aria-hidden="true" />
Créer
</button>
</template>
<template #cell-name="{ row }">
@@ -78,19 +68,15 @@
<span v-else class="text-base-content/50"></span>
</template>
<template #cell-createdAt="{ row }">
<span class="whitespace-nowrap">{{ formatDate(row.createdAt) }}</span>
</template>
<template #cell-actions="{ row }">
<div class="flex justify-end gap-2">
<button type="button" class="btn btn-ghost btn-xs" @click="openRelatedModal(row)">
Liés
</button>
<button
v-if="canEdit && showConvertButton"
type="button"
class="btn btn-ghost btn-xs text-warning"
@click="openConversionModal(row)"
>
Convertir
</button>
<button type="button" class="btn btn-ghost btn-xs" @click="openEditPage(row)">
Éditer
</button>
@@ -101,13 +87,6 @@
</template>
</DataTable>
<ConversionModal
:open="conversionModalOpen"
:model-type="conversionTarget"
@close="closeConversionModal"
@converted="onConverted"
/>
<RelatedItemsModal
:open="relatedModalOpen"
:model-type="relatedType"
@@ -121,7 +100,6 @@
import { computed, onBeforeUnmount, onMounted, ref, watch, type Ref } from 'vue'
import { useHead, useRouter } from '#imports'
import DataTable from '~/components/common/DataTable.vue'
import ConversionModal from '~/components/model-types/ConversionModal.vue'
import { useUrlState } from '~/composables/useUrlState'
import type { DataTableSort } from '~/shared/types/dataTable'
import {
@@ -135,7 +113,7 @@ import { useToast } from '~/composables/useToast'
import { humanizeError } from '~/shared/utils/errorMessages'
import { invalidateEntityTypeCache } from '~/composables/useEntityTypes'
import IconLucideSearch from '~icons/lucide/search'
import IconLucidePlus from '~icons/lucide/plus'
import { formatFrenchDate } from '~/utils/date'
const DEFAULT_DESCRIPTION
= 'Gérez les catégories utilisées pour structurer les catalogues de composants, de pièces et de produits. Ajoutez, modifiez ou supprimez des entrées avec tri, recherche et pagination.'
@@ -199,12 +177,11 @@ useHead(() => ({ title: headingText.value }))
const columns = [
{ key: 'name', label: 'Nom', sortable: true },
{ key: 'notes', label: 'Notes' },
{ key: 'createdAt', label: 'Date', sortable: true },
{ key: 'actions', label: 'Actions', align: 'right' as const, width: 'w-48' },
]
const showConvertButton = computed(() =>
selectedCategory.value === 'PIECE' || selectedCategory.value === 'COMPONENT',
)
const formatDate = formatFrenchDate
const categories: Array<{ label: string, value: ModelCategory }> = [
{ label: 'Composants', value: 'COMPONENT' },
@@ -339,13 +316,6 @@ const resolveCategoryBasePath = (category: ModelCategory) => {
return '/product-category'
}
const openCreatePage = () => {
const basePath = resolveCategoryBasePath(selectedCategory.value)
router.push(`${basePath}/new`).catch(() => {
showError('Navigation impossible vers la page de création.')
})
}
const openEditPage = (item: ModelType) => {
const category = item.category ?? selectedCategory.value
const basePath = resolveCategoryBasePath(category)
@@ -400,26 +370,6 @@ const openRelatedEdit = (entry: { id: string }) => {
})
}
const conversionModalOpen = ref(false)
const conversionTarget = ref<ModelType | null>(null)
const openConversionModal = (item: ModelType) => {
conversionTarget.value = item
conversionModalOpen.value = true
}
const closeConversionModal = () => {
conversionModalOpen.value = false
}
const onConverted = () => {
conversionModalOpen.value = false
invalidateEntityTypeCache('PIECE')
invalidateEntityTypeCache('COMPONENT')
showSuccess('Catégorie convertie avec succès.')
doRefresh()
}
watch(
() => searchInput.value,
(value) => {

View File

@@ -20,7 +20,6 @@ import {
buildProductRequirementDescriptions,
buildProductRequirementEntries,
resizeProductSelections,
areProductSelectionsFilled,
applyProductSelection,
collectNormalizedProductIds,
} from '~/shared/utils/pieceProductSelectionUtils'
@@ -199,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)
@@ -355,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

View File

@@ -5,8 +5,8 @@
<h1 class="text-3xl font-bold tracking-tight">Composants</h1>
<p class="text-sm text-base-content/70">Catalogue et catégories de composants.</p>
</div>
<NuxtLink v-if="canEdit" to="/component/create" class="btn btn-primary btn-sm md:btn-md">
Ajouter un composant
<NuxtLink v-if="canEdit" :to="activeTab === 'categories' ? '/component-category/new' : '/component/create'" class="btn btn-primary btn-sm md:btn-md">
{{ activeTab === 'categories' ? 'Ajouter une catégorie' : 'Ajouter un composant' }}
</NuxtLink>
</div>

View File

@@ -5,8 +5,8 @@
<h1 class="text-3xl font-bold tracking-tight">Pièces</h1>
<p class="text-sm text-base-content/70">Catalogue et catégories de pièces.</p>
</div>
<NuxtLink v-if="canEdit" to="/pieces/create" class="btn btn-primary btn-sm md:btn-md">
Ajouter une pièce
<NuxtLink v-if="canEdit" :to="activeTab === 'categories' ? '/piece-category/new' : '/pieces/create'" class="btn btn-primary btn-sm md:btn-md">
{{ activeTab === 'categories' ? 'Ajouter une catégorie' : 'Ajouter une pièce' }}
</NuxtLink>
</div>

View File

@@ -5,8 +5,8 @@
<h1 class="text-3xl font-bold tracking-tight">Produits</h1>
<p class="text-sm text-base-content/70">Catalogue et catégories de produits.</p>
</div>
<NuxtLink v-if="canEdit" to="/product/create" class="btn btn-primary btn-sm md:btn-md">
Ajouter un produit
<NuxtLink v-if="canEdit" :to="activeTab === 'categories' ? '/product-category/new' : '/product/create'" class="btn btn-primary btn-sm md:btn-md">
{{ activeTab === 'categories' ? 'Ajouter une catégorie' : 'Ajouter un produit' }}
</NuxtLink>
</div>

View File

@@ -408,6 +408,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">
@@ -468,6 +471,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>
@@ -511,6 +517,7 @@ const {
constructeurLinks,
constructeurIdsFromForm,
customFieldInputs,
requiredCustomFieldsFilled,
historyFieldLabels,
canSubmit,
componentTypeList,
@@ -538,6 +545,8 @@ const {
formatStructurePreview,
} = useComponentEdit(String(route.params.id))
const hasRequiredCustomFields = computed(() => customFieldInputs.value.some(f => f.required))
const submitEdition = async () => {
await _submitEdition()
if (!saving.value) {

View File

@@ -223,6 +223,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
@@ -242,6 +245,9 @@
Créer le composant
</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 le composant.
</p>
</div>
</section>
</main>
@@ -290,8 +296,11 @@ const {
resolveProductLabel,
resolveSubcomponentLabel,
submitCreation,
requiredCustomFieldsFilled,
} = useComponentCreate()
const hasRequiredCustomFields = computed(() => customFieldInputs.value.some(f => f.required))
const entityTabs = computed(() => [
{ key: 'general', label: 'Général' },
{ key: 'structure', label: 'Structure' },

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>
@@ -273,7 +273,6 @@ import {
buildProductRequirementDescriptions,
buildProductRequirementEntries,
resizeProductSelections,
areProductSelectionsFilled,
applyProductSelection,
collectNormalizedProductIds,
} from '~/shared/utils/pieceProductSelectionUtils'
@@ -379,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)
@@ -444,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

@@ -127,6 +127,12 @@ php-cs-fixer-allow-risky:
test:
$(EXEC_PHP) php -d memory_limit="512M" vendor/bin/phpunit $(FILES)
test-front:
cd frontend && npx vitest run $(FILES)
test-front-watch:
cd frontend && npx vitest --watch $(FILES)
test-setup:
$(SYMFONY_CONSOLE) doctrine:database:create --if-not-exists --env=test
$(SYMFONY_CONSOLE) doctrine:schema:update --force --env=test