Compare commits

...

6 Commits

Author SHA1 Message Date
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
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
10 changed files with 35 additions and 93 deletions

View File

@@ -1,2 +1,2 @@
parameters: parameters:
app.version: '1.9.21' app.version: '1.9.24'

View File

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

View File

@@ -57,16 +57,6 @@
/> />
</label> </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>
<template #cell-name="{ row }"> <template #cell-name="{ row }">
@@ -78,19 +68,15 @@
<span v-else class="text-base-content/50"></span> <span v-else class="text-base-content/50"></span>
</template> </template>
<template #cell-createdAt="{ row }">
<span class="whitespace-nowrap">{{ formatDate(row.createdAt) }}</span>
</template>
<template #cell-actions="{ row }"> <template #cell-actions="{ row }">
<div class="flex justify-end gap-2"> <div class="flex justify-end gap-2">
<button type="button" class="btn btn-ghost btn-xs" @click="openRelatedModal(row)"> <button type="button" class="btn btn-ghost btn-xs" @click="openRelatedModal(row)">
Liés Liés
</button> </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)"> <button type="button" class="btn btn-ghost btn-xs" @click="openEditPage(row)">
Éditer Éditer
</button> </button>
@@ -101,13 +87,6 @@
</template> </template>
</DataTable> </DataTable>
<ConversionModal
:open="conversionModalOpen"
:model-type="conversionTarget"
@close="closeConversionModal"
@converted="onConverted"
/>
<RelatedItemsModal <RelatedItemsModal
:open="relatedModalOpen" :open="relatedModalOpen"
:model-type="relatedType" :model-type="relatedType"
@@ -121,7 +100,6 @@
import { computed, onBeforeUnmount, onMounted, ref, watch, type Ref } from 'vue' import { computed, onBeforeUnmount, onMounted, ref, watch, type Ref } from 'vue'
import { useHead, useRouter } from '#imports' import { useHead, useRouter } from '#imports'
import DataTable from '~/components/common/DataTable.vue' import DataTable from '~/components/common/DataTable.vue'
import ConversionModal from '~/components/model-types/ConversionModal.vue'
import { useUrlState } from '~/composables/useUrlState' import { useUrlState } from '~/composables/useUrlState'
import type { DataTableSort } from '~/shared/types/dataTable' import type { DataTableSort } from '~/shared/types/dataTable'
import { import {
@@ -135,7 +113,7 @@ import { useToast } from '~/composables/useToast'
import { humanizeError } from '~/shared/utils/errorMessages' import { humanizeError } from '~/shared/utils/errorMessages'
import { invalidateEntityTypeCache } from '~/composables/useEntityTypes' import { invalidateEntityTypeCache } from '~/composables/useEntityTypes'
import IconLucideSearch from '~icons/lucide/search' import IconLucideSearch from '~icons/lucide/search'
import IconLucidePlus from '~icons/lucide/plus' import { formatFrenchDate } from '~/utils/date'
const DEFAULT_DESCRIPTION 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.' = '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 = [ const columns = [
{ key: 'name', label: 'Nom', sortable: true }, { key: 'name', label: 'Nom', sortable: true },
{ key: 'notes', label: 'Notes' }, { key: 'notes', label: 'Notes' },
{ key: 'createdAt', label: 'Date', sortable: true },
{ key: 'actions', label: 'Actions', align: 'right' as const, width: 'w-48' }, { key: 'actions', label: 'Actions', align: 'right' as const, width: 'w-48' },
] ]
const showConvertButton = computed(() => const formatDate = formatFrenchDate
selectedCategory.value === 'PIECE' || selectedCategory.value === 'COMPONENT',
)
const categories: Array<{ label: string, value: ModelCategory }> = [ const categories: Array<{ label: string, value: ModelCategory }> = [
{ label: 'Composants', value: 'COMPONENT' }, { label: 'Composants', value: 'COMPONENT' },
@@ -339,13 +316,6 @@ const resolveCategoryBasePath = (category: ModelCategory) => {
return '/product-category' 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 openEditPage = (item: ModelType) => {
const category = item.category ?? selectedCategory.value const category = item.category ?? selectedCategory.value
const basePath = resolveCategoryBasePath(category) 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( watch(
() => searchInput.value, () => searchInput.value,
(value) => { (value) => {

View File

@@ -95,6 +95,12 @@
<PieceModelStructureEditor v-model="pieceStructure" /> <PieceModelStructureEditor v-model="pieceStructure" />
</div> </div>
<div
v-else
class="space-y-3 rounded-lg border border-base-300 p-4"
>
<PieceModelStructureEditor v-model="productStructure" hide-products />
</div>
</template> </template>
</section> </section>

View File

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

View File

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

View File

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

View File

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

View File

@@ -261,7 +261,7 @@
:model-value="productSelections[entry.index] || null" :model-value="productSelections[entry.index] || null"
:disabled="!canEdit || saving" :disabled="!canEdit || saving"
:type-product-id="entry.typeProductId" :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)" @update:model-value="(value) => setProductSelection(entry.index, value)"
/> />
</div> </div>
@@ -359,6 +359,9 @@
</header> </header>
<template v-if="isEditMode"> <template v-if="isEditMode">
<CustomFieldInputGrid :fields="customFieldInputs" :disabled="!canEdit || saving" /> <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>
<template v-else> <template v-else>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2"> <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
@@ -420,6 +423,9 @@
Enregistrer les modifications Enregistrer les modifications
</button> </button>
</div> </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> </div>
</section> </section>
</main> </main>
@@ -460,6 +466,7 @@ const {
constructeurLinks, constructeurLinks,
productSelections, productSelections,
customFieldInputs, customFieldInputs,
requiredCustomFieldsFilled,
pieceTypeList, pieceTypeList,
selectedType, selectedType,
resolvedStructure, resolvedStructure,
@@ -481,6 +488,8 @@ const {
formatPieceStructurePreview, formatPieceStructurePreview,
} = usePieceEdit(String(route.params.id)) } = usePieceEdit(String(route.params.id))
const hasRequiredCustomFields = computed(() => customFieldInputs.value.some(f => f.required))
const entityTabs = computed(() => [ const entityTabs = computed(() => [
{ key: 'general', label: 'Général' }, { key: 'general', label: 'Général' },
{ key: 'products', label: 'Produits liés', count: structureProducts.value.length }, { key: 'products', label: 'Produits liés', count: structureProducts.value.length },

View File

@@ -168,7 +168,7 @@
:model-value="productSelections[entry.index] || null" :model-value="productSelections[entry.index] || null"
:disabled="!canEdit || submitting || !selectedType" :disabled="!canEdit || submitting || !selectedType"
:type-product-id="entry.typeProductId" :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)" @update:model-value="(value) => setProductSelection(entry.index, value)"
/> />
</div> </div>
@@ -273,7 +273,6 @@ import {
buildProductRequirementDescriptions, buildProductRequirementDescriptions,
buildProductRequirementEntries, buildProductRequirementEntries,
resizeProductSelections, resizeProductSelections,
areProductSelectionsFilled,
applyProductSelection, applyProductSelection,
collectNormalizedProductIds, collectNormalizedProductIds,
} from '~/shared/utils/pieceProductSelectionUtils' } from '~/shared/utils/pieceProductSelectionUtils'
@@ -379,13 +378,7 @@ const productRequirementEntries = computed(() =>
buildProductRequirementEntries(structureProducts.value, 'piece-create-product-requirement'), buildProductRequirementEntries(structureProducts.value, 'piece-create-product-requirement'),
) )
const productSelectionsFilled = computed(() => const productSelectionsFilled = computed(() => true)
areProductSelectionsFilled(
requiresProductSelection.value,
productRequirementEntries.value,
productSelections.value,
),
)
const setProductSelection = (index: number, value: string | null) => { const setProductSelection = (index: number, value: string | null) => {
productSelections.value = applyProductSelection(productSelections.value, index, value) productSelections.value = applyProductSelection(productSelections.value, index, value)
@@ -444,11 +437,6 @@ const submitCreation = async () => {
return return
} }
if (!productSelectionsFilled.value) {
toast.showError('Sélectionnez un produit conforme au squelette.')
return
}
const payload: Record<string, any> = { const payload: Record<string, any> = {
name: creationForm.name.trim(), name: creationForm.name.trim(),
typePieceId: selectedType.value.id, typePieceId: selectedType.value.id,