Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6744542f84 | ||
| 3e0e9d5270 | |||
|
|
4e0efc11ba | ||
| 9fc88df3ff | |||
|
|
041a04f0e9 | ||
| d089cd4873 | |||
|
|
b304cf6684 | ||
| 0fe7f3131e | |||
| a6bbcaf6d1 | |||
| 9f2e1da6ec | |||
|
|
7962576eec | ||
| 7d98c1598c |
175
composer.lock
generated
175
composer.lock
generated
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "2db01f705a09cf38007a2baa3b078e49",
|
"content-hash": "f94dc3c05e9ba6be99c510aad3d17182",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "api-platform/doctrine-common",
|
"name": "api-platform/doctrine-common",
|
||||||
@@ -5341,6 +5341,92 @@
|
|||||||
],
|
],
|
||||||
"time": "2026-03-04T16:39:24+00:00"
|
"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",
|
"name": "symfony/options-resolver",
|
||||||
"version": "v8.0.0",
|
"version": "v8.0.0",
|
||||||
@@ -5567,6 +5653,93 @@
|
|||||||
],
|
],
|
||||||
"time": "2025-06-27T09:58:17+00:00"
|
"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",
|
"name": "symfony/polyfill-intl-normalizer",
|
||||||
"version": "v1.33.0",
|
"version": "v1.33.0",
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
parameters:
|
parameters:
|
||||||
app.version: '1.9.19'
|
app.version: '1.9.24'
|
||||||
|
|||||||
@@ -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<{
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -99,11 +99,7 @@
|
|||||||
v-else
|
v-else
|
||||||
class="space-y-3 rounded-lg border border-base-300 p-4"
|
class="space-y-3 rounded-lg border border-base-300 p-4"
|
||||||
>
|
>
|
||||||
<p class="text-sm text-base-content/70">
|
<PieceModelStructureEditor v-model="productStructure" hide-products />
|
||||||
Aperçu :
|
|
||||||
<span class="font-medium text-base-content">{{ productStructurePreview }}</span>
|
|
||||||
</p>
|
|
||||||
<PieceModelStructureEditor v-model="productStructure" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</section>
|
</section>
|
||||||
@@ -194,15 +190,16 @@ const form = reactive<ModelTypePayload & { referenceFormula?: string | null }>({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const formulaBuilderCustomFields = computed(() => {
|
const formulaBuilderCustomFields = computed(() => {
|
||||||
|
let fields: any[] = []
|
||||||
if (form.category === 'PIECE') {
|
if (form.category === 'PIECE') {
|
||||||
const fields = pieceStructure.value?.customFields
|
const raw = pieceStructure.value?.customFields
|
||||||
return Array.isArray(fields) ? fields : []
|
fields = Array.isArray(raw) ? raw : []
|
||||||
}
|
}
|
||||||
if (form.category === 'COMPONENT') {
|
else if (form.category === 'COMPONENT') {
|
||||||
const fields = componentStructure.value?.customFields
|
const raw = componentStructure.value?.customFields
|
||||||
return Array.isArray(fields) ? fields : []
|
fields = Array.isArray(raw) ? raw : []
|
||||||
}
|
}
|
||||||
return []
|
return fields.filter((f: any) => !f.machineContextOnly)
|
||||||
})
|
})
|
||||||
|
|
||||||
const extractFormulaFields = (formula: string | null | undefined): string[] => {
|
const extractFormulaFields = (formula: string | null | undefined): string[] => {
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ import {
|
|||||||
import {
|
import {
|
||||||
hasAssignments,
|
hasAssignments,
|
||||||
initializeStructureAssignments,
|
initializeStructureAssignments,
|
||||||
isAssignmentNodeComplete,
|
|
||||||
serializeStructureAssignments,
|
serializeStructureAssignments,
|
||||||
} from '~/shared/utils/structureAssignmentHelpers'
|
} from '~/shared/utils/structureAssignmentHelpers'
|
||||||
import type { ComponentModelStructure } from '~/shared/types/inventory'
|
import type { ComponentModelStructure } from '~/shared/types/inventory'
|
||||||
@@ -152,24 +151,14 @@ export function useComponentCreate() {
|
|||||||
values: computed(() => []),
|
values: computed(() => []),
|
||||||
entityType: 'composant',
|
entityType: 'composant',
|
||||||
entityId: createdComponentId,
|
entityId: createdComponentId,
|
||||||
|
context: 'standalone',
|
||||||
})
|
})
|
||||||
|
|
||||||
const structureHasRequirements = computed(() =>
|
const structureHasRequirements = computed(() =>
|
||||||
hasAssignments(structureAssignments.value),
|
hasAssignments(structureAssignments.value),
|
||||||
)
|
)
|
||||||
|
|
||||||
const structureSelectionsComplete = computed(() => {
|
const structureSelectionsComplete = computed(() => true)
|
||||||
if (!structureHasRequirements.value) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if (structureDataLoading.value) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (!structureAssignments.value) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return isAssignmentNodeComplete(structureAssignments.value, true)
|
|
||||||
})
|
|
||||||
|
|
||||||
const canSubmit = computed(() => Boolean(
|
const canSubmit = computed(() => Boolean(
|
||||||
canEdit.value
|
canEdit.value
|
||||||
@@ -307,11 +296,6 @@ export function useComponentCreate() {
|
|||||||
payload.productId = rootProductSelection.selectedProductId.trim()
|
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
|
const serializedStructure = structureHasRequirements.value
|
||||||
? serializeStructureAssignments(structureAssignments.value)
|
? serializeStructureAssignments(structureAssignments.value)
|
||||||
: null
|
: null
|
||||||
@@ -414,6 +398,7 @@ export function useComponentCreate() {
|
|||||||
structureSelectionsComplete,
|
structureSelectionsComplete,
|
||||||
canEdit,
|
canEdit,
|
||||||
canSubmit,
|
canSubmit,
|
||||||
|
requiredCustomFieldsFilled,
|
||||||
|
|
||||||
// Functions
|
// Functions
|
||||||
typeOptionLabel,
|
typeOptionLabel,
|
||||||
|
|||||||
@@ -209,6 +209,7 @@ export function useComponentEdit(componentId: string) {
|
|||||||
values: computed(() => component.value?.customFieldValues ?? []),
|
values: computed(() => component.value?.customFieldValues ?? []),
|
||||||
entityType: 'composant',
|
entityType: 'composant',
|
||||||
entityId: computed(() => component.value?.id ?? null),
|
entityId: computed(() => component.value?.id ?? null),
|
||||||
|
context: 'standalone',
|
||||||
onValueCreated: (newValue) => {
|
onValueCreated: (newValue) => {
|
||||||
if (component.value && Array.isArray(component.value.customFieldValues)) {
|
if (component.value && Array.isArray(component.value.customFieldValues)) {
|
||||||
component.value.customFieldValues.push(newValue)
|
component.value.customFieldValues.push(newValue)
|
||||||
@@ -556,6 +557,7 @@ export function useComponentEdit(componentId: string) {
|
|||||||
originalConstructeurLinks,
|
originalConstructeurLinks,
|
||||||
constructeurIdsFromForm,
|
constructeurIdsFromForm,
|
||||||
customFieldInputs,
|
customFieldInputs,
|
||||||
|
requiredCustomFieldsFilled,
|
||||||
historyFieldLabels,
|
historyFieldLabels,
|
||||||
|
|
||||||
// Computed
|
// Computed
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import {
|
|||||||
buildProductRequirementDescriptions,
|
buildProductRequirementDescriptions,
|
||||||
buildProductRequirementEntries,
|
buildProductRequirementEntries,
|
||||||
resizeProductSelections,
|
resizeProductSelections,
|
||||||
areProductSelectionsFilled,
|
|
||||||
applyProductSelection,
|
applyProductSelection,
|
||||||
collectNormalizedProductIds,
|
collectNormalizedProductIds,
|
||||||
} from '~/shared/utils/pieceProductSelectionUtils'
|
} from '~/shared/utils/pieceProductSelectionUtils'
|
||||||
@@ -99,6 +98,7 @@ export function usePieceEdit(pieceId: string) {
|
|||||||
values: computed(() => piece.value?.customFieldValues ?? []),
|
values: computed(() => piece.value?.customFieldValues ?? []),
|
||||||
entityType: 'piece',
|
entityType: 'piece',
|
||||||
entityId: computed(() => piece.value?.id ?? null),
|
entityId: computed(() => piece.value?.id ?? null),
|
||||||
|
context: 'standalone',
|
||||||
onValueCreated: (newValue) => {
|
onValueCreated: (newValue) => {
|
||||||
if (piece.value && Array.isArray(piece.value.customFieldValues)) {
|
if (piece.value && Array.isArray(piece.value.customFieldValues)) {
|
||||||
piece.value.customFieldValues.push(newValue)
|
piece.value.customFieldValues.push(newValue)
|
||||||
@@ -198,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)
|
||||||
@@ -354,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
|
||||||
@@ -435,6 +424,7 @@ export function usePieceEdit(pieceId: string) {
|
|||||||
constructeurIdsFromForm,
|
constructeurIdsFromForm,
|
||||||
productSelections,
|
productSelections,
|
||||||
customFieldInputs,
|
customFieldInputs,
|
||||||
|
requiredCustomFieldsFilled,
|
||||||
canEdit,
|
canEdit,
|
||||||
|
|
||||||
// Computed
|
// Computed
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -218,6 +218,9 @@
|
|||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
<CustomFieldInputGrid :fields="customFieldInputs" :disabled="!canEdit || submitting" />
|
<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>
|
</div>
|
||||||
<EmptyState
|
<EmptyState
|
||||||
v-else
|
v-else
|
||||||
@@ -237,6 +240,9 @@
|
|||||||
Créer la pièce
|
Créer la pièce
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
@@ -267,7 +273,6 @@ import {
|
|||||||
buildProductRequirementDescriptions,
|
buildProductRequirementDescriptions,
|
||||||
buildProductRequirementEntries,
|
buildProductRequirementEntries,
|
||||||
resizeProductSelections,
|
resizeProductSelections,
|
||||||
areProductSelectionsFilled,
|
|
||||||
applyProductSelection,
|
applyProductSelection,
|
||||||
collectNormalizedProductIds,
|
collectNormalizedProductIds,
|
||||||
} from '~/shared/utils/pieceProductSelectionUtils'
|
} from '~/shared/utils/pieceProductSelectionUtils'
|
||||||
@@ -311,7 +316,9 @@ const { fields: customFieldInputs, requiredFilled: requiredCustomFieldsFilled, s
|
|||||||
values: [] as any[],
|
values: [] as any[],
|
||||||
entityType: 'piece' as CustomFieldEntityType,
|
entityType: 'piece' as CustomFieldEntityType,
|
||||||
entityId: createdEntityId,
|
entityId: createdEntityId,
|
||||||
|
context: 'standalone',
|
||||||
})
|
})
|
||||||
|
const hasRequiredCustomFields = computed(() => customFieldInputs.value.some(f => f.required))
|
||||||
const selectedDocuments = ref<File[]>([])
|
const selectedDocuments = ref<File[]>([])
|
||||||
const uploadingDocuments = ref(false)
|
const uploadingDocuments = ref(false)
|
||||||
|
|
||||||
@@ -371,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)
|
||||||
@@ -436,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,
|
||||||
|
|||||||
@@ -274,6 +274,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">
|
||||||
@@ -338,7 +341,7 @@
|
|||||||
Enregistrer les modifications
|
Enregistrer les modifications
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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.
|
Merci de renseigner tous les champs personnalisés obligatoires.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -409,6 +412,7 @@ const {
|
|||||||
values: cfValues,
|
values: cfValues,
|
||||||
entityType: 'product' as CustomFieldEntityType,
|
entityType: 'product' as CustomFieldEntityType,
|
||||||
entityId,
|
entityId,
|
||||||
|
context: 'standalone',
|
||||||
})
|
})
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
@@ -446,7 +450,7 @@ const editionForm = reactive({
|
|||||||
supplierPrice: '' as string,
|
supplierPrice: '' as string,
|
||||||
})
|
})
|
||||||
|
|
||||||
// requiredCustomFieldsFilled comes from useCustomFieldInputs composable
|
const hasRequiredCustomFields = computed(() => customFieldInputs.value.some(f => f.required))
|
||||||
|
|
||||||
const canSubmit = computed(() =>
|
const canSubmit = computed(() =>
|
||||||
Boolean(canEdit.value && product.value && editionForm.name.trim().length >= 2 && requiredCustomFieldsFilled.value && !saving.value),
|
Boolean(canEdit.value && product.value && editionForm.name.trim().length >= 2 && requiredCustomFieldsFilled.value && !saving.value),
|
||||||
|
|||||||
@@ -158,6 +158,9 @@
|
|||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
<CustomFieldInputGrid :fields="customFieldInputs" :disabled="!canEdit || submitting" />
|
<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>
|
</div>
|
||||||
<EmptyState
|
<EmptyState
|
||||||
v-else
|
v-else
|
||||||
@@ -177,7 +180,7 @@
|
|||||||
Créer le produit
|
Créer le produit
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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.
|
Merci de renseigner tous les champs personnalisés obligatoires.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -241,7 +244,9 @@ const { fields: customFieldInputs, requiredFilled: requiredCustomFieldsFilled, s
|
|||||||
values: [] as any[],
|
values: [] as any[],
|
||||||
entityType: 'product' as CustomFieldEntityType,
|
entityType: 'product' as CustomFieldEntityType,
|
||||||
entityId: createdEntityId,
|
entityId: createdEntityId,
|
||||||
|
context: 'standalone',
|
||||||
})
|
})
|
||||||
|
const hasRequiredCustomFields = computed(() => customFieldInputs.value.some(f => f.required))
|
||||||
|
|
||||||
const productTypeList = computed<ProductCatalogType[]>(() =>
|
const productTypeList = computed<ProductCatalogType[]>(() =>
|
||||||
(productTypes.value || []) as ProductCatalogType[],
|
(productTypes.value || []) as ProductCatalogType[],
|
||||||
|
|||||||
Reference in New Issue
Block a user