@@ -323,7 +328,6 @@ import {
parseConstructeurLinksFromApi,
} from '~/shared/constructeurUtils'
import {
- formatSize,
shouldInlinePdf,
documentPreviewSrc,
documentIcon,
@@ -332,9 +336,12 @@ import {
import { useEntityDocuments } from '~/composables/useEntityDocuments'
import { useEntityProductDisplay } from '~/composables/useEntityProductDisplay'
import { useEntityCustomFields } from '~/composables/useEntityCustomFields'
-import { mergeFieldDefinitionsWithValues, dedupeMergedFields } from '~/shared/utils/entityCustomFieldLogic'
-import { useCustomFields } from '~/composables/useCustomFields'
-import { useToast } from '~/composables/useToast'
+import {
+ mergeFieldDefinitionsWithValues,
+ dedupeMergedFields,
+ resolveCustomFieldId,
+ resolveFieldId,
+} from '~/shared/utils/entityCustomFieldLogic'
const props = defineProps({
component: { type: Object, required: true },
@@ -374,9 +381,6 @@ const {
updateCustomField: updateComponentCustomField,
} = useEntityCustomFields({ entity: () => props.component, entityType: 'composant' })
-const { upsertCustomFieldValue } = useCustomFields()
-const { showSuccess, showError } = useToast()
-
const mergedContextFields = computed(() => {
const definitions = props.component?.contextCustomFields ?? []
const values = props.component?.contextCustomFieldValues ?? []
@@ -386,26 +390,23 @@ const mergedContextFields = computed(() => {
)
})
-const updateContextCustomField = async (field) => {
+const queueContextCustomFieldUpdate = (field, value) => {
const linkId = props.component?.linkId
if (!linkId || !field) return
- const customFieldId = field.customFieldId || field.customField?.id
- if (!customFieldId) return
+ const customFieldId = resolveCustomFieldId(field)
+ const customFieldValueId = resolveFieldId(field)
+ if (!customFieldId && !customFieldValueId) return
- const result = await upsertCustomFieldValue(
- customFieldId,
- 'machineComponentLink',
- linkId,
- field.value ?? '',
- )
-
- if (result.success) {
- showSuccess(`Champ contextuel "${field.name || field.customField?.name}" mis à jour`)
- }
- else {
- showError(`Erreur lors de la mise à jour du champ contextuel`)
- }
+ field.value = value
+ emit('custom-field-update', {
+ entityType: 'machineComponentLink',
+ entityId: linkId,
+ fieldId: customFieldId,
+ customFieldValueId,
+ value: value ?? '',
+ fieldName: field.name || field.customField?.name || 'Champ contextuel',
+ })
}
// --- Document edit modal ---
diff --git a/frontend/app/components/PieceItem.vue b/frontend/app/components/PieceItem.vue
index e79ba80..c715deb 100644
--- a/frontend/app/components/PieceItem.vue
+++ b/frontend/app/components/PieceItem.vue
@@ -241,22 +241,27 @@
-
-
-
- Champs contextuels
-
-
-
+
+
+ Champs personnalisés machine
+
+
+
@@ -316,13 +321,14 @@ import {
} from '~/shared/constructeurUtils'
import {
resolveFieldId,
- resolveFieldReadOnly, mergeFieldDefinitionsWithValues, dedupeMergedFields
+ resolveFieldReadOnly,
+ resolveCustomFieldId,
+ mergeFieldDefinitionsWithValues,
+ dedupeMergedFields,
} from '~/shared/utils/entityCustomFieldLogic'
import { useEntityDocuments } from '~/composables/useEntityDocuments'
import { useEntityProductDisplay } from '~/composables/useEntityProductDisplay'
import { useEntityCustomFields } from '~/composables/useEntityCustomFields'
-import { useCustomFields } from '~/composables/useCustomFields'
-import { useToast } from '~/composables/useToast'
const props = defineProps({
piece: { type: Object, required: true },
@@ -390,9 +396,6 @@ const {
updateCustomField,
} = useEntityCustomFields({ entity: () => props.piece, entityType: 'piece' })
-const { upsertCustomFieldValue } = useCustomFields()
-const { showSuccess, showError } = useToast()
-
const mergedContextFields = computed(() => {
const definitions = props.piece?.contextCustomFields ?? []
const values = props.piece?.contextCustomFieldValues ?? []
@@ -402,26 +405,23 @@ const mergedContextFields = computed(() => {
)
})
-const updateContextCustomField = async (field) => {
+const queueContextCustomFieldUpdate = (field, value) => {
const linkId = props.piece?.linkId
if (!linkId || !field) return
- const customFieldId = field.customFieldId || field.customField?.id
- if (!customFieldId) return
+ const customFieldId = resolveCustomFieldId(field)
+ const customFieldValueId = resolveFieldId(field)
+ if (!customFieldId && !customFieldValueId) return
- const result = await upsertCustomFieldValue(
- customFieldId,
- 'machinePieceLink',
- linkId,
- field.value ?? '',
- )
-
- if (result.success) {
- showSuccess(`Champ contextuel "${field.name || field.customField?.name}" mis à jour`)
- }
- else {
- showError(`Erreur lors de la mise à jour du champ contextuel`)
- }
+ field.value = value
+ emit('custom-field-update', {
+ entityType: 'machinePieceLink',
+ entityId: linkId,
+ fieldId: customFieldId,
+ customFieldValueId,
+ value: value ?? '',
+ fieldName: field.name || field.customField?.name || 'Champ contextuel',
+ })
}
// --- Document edit modal ---
diff --git a/frontend/app/components/common/CustomFieldDisplay.vue b/frontend/app/components/common/CustomFieldDisplay.vue
index 30e04c7..5dcab65 100644
--- a/frontend/app/components/common/CustomFieldDisplay.vue
+++ b/frontend/app/components/common/CustomFieldDisplay.vue
@@ -1,10 +1,10 @@
-
- Champs personnalisés
+
+ {{ title }}
-
+
()
const emit = defineEmits<{
@@ -155,6 +160,20 @@ const layoutClass = computed(() =>
: 'space-y-3',
)
+const title = computed(() => props.title ?? 'Champs personnalisés')
+const showHeader = computed(() => props.showHeader ?? true)
+const containerClass = computed(() =>
+ props.withTopBorder === false
+ ? ''
+ : 'mt-4 pt-4 border-t border-base-200',
+)
+const editable = computed(() => props.editable ?? true)
+const emitBlur = computed(() => props.emitBlur ?? true)
+
+function isFieldEditable(field: any) {
+ return props.isEditMode && editable.value && !resolveFieldReadOnly(field)
+}
+
function onInput(field: any, value: string) {
field.value = value
emit('field-input', field, value)
@@ -164,10 +183,14 @@ function onBooleanChange(field: any, checked: boolean) {
const value = checked ? 'true' : 'false'
field.value = value
emit('field-input', field, value)
- emit('field-blur', field)
+ if (emitBlur.value) {
+ emit('field-blur', field)
+ }
}
function onBlur(field: any) {
- emit('field-blur', field)
+ if (emitBlur.value) {
+ emit('field-blur', field)
+ }
}
diff --git a/frontend/app/components/machine/MachineComponentsCard.vue b/frontend/app/components/machine/MachineComponentsCard.vue
index bdaddef..097a581 100644
--- a/frontend/app/components/machine/MachineComponentsCard.vue
+++ b/frontend/app/components/machine/MachineComponentsCard.vue
@@ -28,11 +28,13 @@
$emit('fill-entity', linkId, typeId)"
/>
diff --git a/frontend/app/components/machine/MachinePiecesCard.vue b/frontend/app/components/machine/MachinePiecesCard.vue
index 4f469e7..68b157b 100644
--- a/frontend/app/components/machine/MachinePiecesCard.vue
+++ b/frontend/app/components/machine/MachinePiecesCard.vue
@@ -34,6 +34,7 @@
:toggle-token="collapseToggleToken"
@update="$emit('update-piece', $event)"
@edit="$emit('edit-piece', $event)"
+ @custom-field-update="$emit('custom-field-update', $event)"
@delete="$emit('remove-piece', piece.linkId || piece.id)"
@fill-entity="(linkId, typeId) => $emit('fill-entity', linkId, typeId)"
/>
@@ -66,6 +67,7 @@ defineEmits<{
'toggle-collapse': []
'update-piece': [piece: any]
'edit-piece': [piece: any]
+ 'custom-field-update': [fieldUpdate: any]
'add-piece': []
'remove-piece': [linkId: string]
'fill-entity': [linkId: string, modelTypeId: string]
diff --git a/frontend/app/composables/useMachineDetailCustomFields.ts b/frontend/app/composables/useMachineDetailCustomFields.ts
index fa89b07..5e31340 100644
--- a/frontend/app/composables/useMachineDetailCustomFields.ts
+++ b/frontend/app/composables/useMachineDetailCustomFields.ts
@@ -44,6 +44,7 @@ export function useMachineDetailCustomFields(deps: MachineDetailCustomFieldsDeps
// ---------------------------------------------------------------------------
const machineCustomFields = ref([])
+ const pendingContextFieldUpdates = ref([])
// ---------------------------------------------------------------------------
// Computed
@@ -380,6 +381,83 @@ export function useMachineDetailCustomFields(deps: MachineDetailCustomFieldsDeps
}
}
+ const handleCustomFieldUpdate = async (fieldUpdate: AnyRecord) => {
+ if (fieldUpdate?.entityType && fieldUpdate?.entityId) {
+ queueContextFieldUpdate(fieldUpdate)
+ return
+ }
+
+ await updatePieceCustomField(fieldUpdate)
+ }
+
+ const queueContextFieldUpdate = (fieldUpdate: AnyRecord) => {
+ const entityType = fieldUpdate.entityType as string | undefined
+ const entityId = fieldUpdate.entityId as string | undefined
+ const fieldId = fieldUpdate.fieldId as string | undefined
+ const customFieldValueId = fieldUpdate.customFieldValueId as string | undefined
+
+ if (!entityType || !entityId || (!fieldId && !customFieldValueId)) return
+
+ const nextUpdate = {
+ entityType,
+ entityId,
+ fieldId,
+ customFieldValueId,
+ value: fieldUpdate.value ?? '',
+ fieldName: fieldUpdate.fieldName ?? 'Champ contextuel',
+ }
+
+ const existingIndex = pendingContextFieldUpdates.value.findIndex(
+ (item) =>
+ item.entityType === entityType &&
+ item.entityId === entityId &&
+ item.fieldId === fieldId &&
+ item.customFieldValueId === customFieldValueId,
+ )
+
+ if (existingIndex >= 0) {
+ pendingContextFieldUpdates.value[existingIndex] = nextUpdate
+ return
+ }
+
+ pendingContextFieldUpdates.value.push(nextUpdate)
+ }
+
+ const clearPendingContextFieldUpdates = () => {
+ pendingContextFieldUpdates.value = []
+ }
+
+ const saveAllContextCustomFields = async () => {
+ const updates = pendingContextFieldUpdates.value.slice()
+ if (!updates.length) return
+
+ try {
+ for (const update of updates) {
+ if (update.customFieldValueId) {
+ await updateCustomFieldValueApi(update.customFieldValueId as string, {
+ value: update.value ?? '',
+ } as any)
+ continue
+ }
+
+ if (!update.fieldId) {
+ continue
+ }
+
+ await upsertCustomFieldValue(
+ update.fieldId as string,
+ update.entityType as string,
+ update.entityId as string,
+ update.value ?? '',
+ )
+ }
+ clearPendingContextFieldUpdates()
+ } catch (error) {
+ console.error('Erreur lors de la sauvegarde batch des champs contextuels:', error)
+ throw error
+ }
+ }
+
const saveAllMachineCustomFields = async () => {
if (!machine.value) return
@@ -435,6 +513,7 @@ export function useMachineDetailCustomFields(deps: MachineDetailCustomFieldsDeps
return {
// State
machineCustomFields,
+ pendingContextFieldUpdates,
// Computed
visibleMachineCustomFields,
@@ -448,6 +527,10 @@ export function useMachineDetailCustomFields(deps: MachineDetailCustomFieldsDeps
setMachineCustomFieldValue,
updateMachineCustomField,
updatePieceCustomField,
+ handleCustomFieldUpdate,
+ queueContextFieldUpdate,
+ clearPendingContextFieldUpdates,
saveAllMachineCustomFields,
+ saveAllContextCustomFields,
}
}
diff --git a/frontend/app/composables/useMachineDetailData.ts b/frontend/app/composables/useMachineDetailData.ts
index 0fe4eed..83c387c 100644
--- a/frontend/app/composables/useMachineDetailData.ts
+++ b/frontend/app/composables/useMachineDetailData.ts
@@ -151,13 +151,18 @@ export function useMachineDetailData(machineId: string) {
const {
machineCustomFields,
visibleMachineCustomFields,
+ pendingContextFieldUpdates,
transformCustomFields,
transformComponentCustomFields,
syncMachineCustomFields,
setMachineCustomFieldValue,
updateMachineCustomField,
updatePieceCustomField,
+ handleCustomFieldUpdate,
+ queueContextFieldUpdate,
+ clearPendingContextFieldUpdates,
saveAllMachineCustomFields,
+ saveAllContextCustomFields,
} = useMachineDetailCustomFields({
machine,
isEditMode,
@@ -333,10 +338,13 @@ export function useMachineDetailData(machineId: string) {
// 2. Save all custom field values
await saveAllMachineCustomFields()
- // 3. Reload machine data to get fresh state
+ // 3. Save contextual custom field values queued from piece/component inputs
+ await saveAllContextCustomFields()
+
+ // 4. Reload machine data to get fresh state
await loadMachineData()
- // 4. Exit edit mode
+ // 5. Exit edit mode
isEditMode.value = false
toast.showSuccess('Machine mise à jour avec succès')
} catch (error) {
@@ -350,6 +358,7 @@ export function useMachineDetailData(machineId: string) {
const cancelEdition = () => {
initMachineFields()
syncMachineCustomFields()
+ clearPendingContextFieldUpdates()
constructeurLinks.value = originalConstructeurLinks.value.map(l => ({ ...l }))
machineConstructeurIds.value = constructeurIdsFromLinks(constructeurLinks.value)
isEditMode.value = false
@@ -486,7 +495,7 @@ export function useMachineDetailData(machineId: string) {
// UI state
machineDocumentFiles, machineDocumentsUploading, machineDocumentsLoaded,
- machineCustomFields, previewDocument, previewVisible,
+ machineCustomFields, pendingContextFieldUpdates, previewDocument, previewVisible,
isEditMode, debug,
componentsCollapsed, collapseToggleToken, piecesCollapsed, pieceCollapseToggleToken,
@@ -499,7 +508,8 @@ export function useMachineDetailData(machineId: string) {
findProductById, resolveProductReference, getProductDisplay,
initMachineFields, getMachineFieldId,
syncMachineCustomFields, setMachineCustomFieldValue,
- updateMachineCustomField, updatePieceCustomField,
+ updateMachineCustomField, updatePieceCustomField, handleCustomFieldUpdate,
+ queueContextFieldUpdate, clearPendingContextFieldUpdates, saveAllContextCustomFields,
refreshMachineDocuments, handleMachineFilesAdded, removeMachineDocument,
openPreview, closePreview,
updateMachineInfo, updateComponent, updatePieceFromComponent,
diff --git a/frontend/app/composables/useMachineDetailUpdates.ts b/frontend/app/composables/useMachineDetailUpdates.ts
index 8a43a36..fd14011 100644
--- a/frontend/app/composables/useMachineDetailUpdates.ts
+++ b/frontend/app/composables/useMachineDetailUpdates.ts
@@ -73,11 +73,11 @@ export function useMachineDetailUpdates(deps: UseMachineDetailUpdatesDeps) {
const updateMachineInfo = async () => {
if (!machine.value) return
try {
- const result: any = await updateMachineApi(machine.value.id as string, {
- name: machineName.value,
- reference: machineReference.value,
- siteId: machineSiteId.value || undefined,
- } as any)
+ const payload: Record = {}
+ if (machineName.value !== machine.value.name) payload.name = machineName.value
+ if (machineReference.value !== machine.value.reference) payload.reference = machineReference.value
+ if ((machineSiteId.value || undefined) !== ((machine.value.siteId as string) || (machine.value.site as any)?.id || undefined)) payload.siteId = machineSiteId.value || undefined
+ const result: any = await updateMachineApi(machine.value.id as string, payload as any)
if (result.success) {
const machinePayload =
result.data?.machine && typeof result.data.machine === 'object'
diff --git a/frontend/app/composables/useMachineHierarchy.ts b/frontend/app/composables/useMachineHierarchy.ts
index df8064c..2846461 100644
--- a/frontend/app/composables/useMachineHierarchy.ts
+++ b/frontend/app/composables/useMachineHierarchy.ts
@@ -208,6 +208,8 @@ export const buildMachineHierarchyFromLinks = (
quantity: typeof link.quantity === 'number' ? link.quantity : 1,
definition: appliedPiece.definition || originalPiece?.definition || {},
customFields: appliedPiece.customFields || [],
+ contextCustomFields: link.contextCustomFields || [],
+ contextCustomFieldValues: link.contextCustomFieldValues || [],
}
const resolvedProductId = resolveIdentifier(appliedPiece.productId, (appliedPiece.product as AnyRecord)?.id, link.productId, (link.product as AnyRecord)?.id, originalPiece?.productId, (originalPiece?.product as AnyRecord)?.id)
@@ -335,6 +337,8 @@ export const buildMachineHierarchyFromLinks = (
parentComposantId: resolveIdentifier(appliedComponent.parentComposantId, link.parentComponentId),
definition: appliedComponent.definition || originalComponent?.definition || {},
customFields: appliedComponent.customFields || [],
+ contextCustomFields: link.contextCustomFields || [],
+ contextCustomFieldValues: link.contextCustomFieldValues || [],
pieces,
subComponents,
subcomponents: subComponents,
diff --git a/frontend/app/pages/machine/[id].vue b/frontend/app/pages/machine/[id].vue
index 0c01038..b92c4d5 100644
--- a/frontend/app/pages/machine/[id].vue
+++ b/frontend/app/pages/machine/[id].vue
@@ -113,7 +113,7 @@
@toggle-collapse="d.toggleAllComponents"
@update-component="d.updateComponent"
@edit-piece="d.updatePieceFromComponent"
- @custom-field-update="d.updatePieceCustomField"
+ @custom-field-update="d.handleCustomFieldUpdate"
@add-component="openAddModal('component')"
@remove-component="async (id) => { await d.removeComponentLink(id); refreshVersions() }"
@fill-entity="(linkId, typeId) => handleFillEntity(linkId, 'component', typeId)"
@@ -128,7 +128,7 @@
:collapse-toggle-token="d.pieceCollapseToggleToken.value"
@update-piece="d.updatePieceInfo"
@edit-piece="d.editPiece"
- @custom-field-update="d.updatePieceCustomField"
+ @custom-field-update="d.handleCustomFieldUpdate"
@add-piece="openAddModal('piece')"
@remove-piece="async (id) => { await d.removePieceLink(id); refreshVersions() }"
@fill-entity="(linkId, typeId) => handleFillEntity(linkId, 'piece', typeId)"
diff --git a/frontend/app/shared/model/componentStructure.ts b/frontend/app/shared/model/componentStructure.ts
index 1885345..c2915ab 100644
--- a/frontend/app/shared/model/componentStructure.ts
+++ b/frontend/app/shared/model/componentStructure.ts
@@ -98,6 +98,7 @@ export const normalizeStructureForEditor = (input: any): ComponentModelStructure
name: field.name,
type: field.type,
required: field.required,
+ machineContextOnly: !!field.machineContextOnly,
options,
defaultValue,
optionsText,
diff --git a/src/Controller/MaintenanceController.php b/src/Controller/MaintenanceController.php
index 87e9cee..19afa71 100644
--- a/src/Controller/MaintenanceController.php
+++ b/src/Controller/MaintenanceController.php
@@ -53,6 +53,6 @@ final class MaintenanceController extends AbstractController
private function flagPath(): string
{
- return $this->kernel->getProjectDir() . '/var/maintenance';
+ return $this->kernel->getProjectDir().'/var/maintenance';
}
}
diff --git a/src/Entity/Machine.php b/src/Entity/Machine.php
index 1696dc7..cd29b8f 100644
--- a/src/Entity/Machine.php
+++ b/src/Entity/Machine.php
@@ -25,7 +25,7 @@ use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity(repositoryClass: MachineRepository::class)]
#[ORM\Table(name: 'machines')]
#[ORM\HasLifecycleCallbacks]
-#[UniqueEntity(fields: ['reference'], message: 'Une machine avec cette référence existe déjà.')]
+#[UniqueEntity(fields: ['reference'], message: 'Une machine avec cette référence existe déjà.', ignoreNull: true)]
#[ApiResource(
description: 'Machines industrielles rattachées à un site. Chaque machine possède une structure hiérarchique de composants, pièces et produits, ainsi que des champs personnalisés et des documents.',
operations: [
@@ -150,7 +150,7 @@ class Machine
public function setReference(?string $reference): static
{
- $this->reference = $reference;
+ $this->reference = (null !== $reference && '' !== trim($reference)) ? $reference : null;
return $this;
}
diff --git a/src/EventListener/MaintenanceModeListener.php b/src/EventListener/MaintenanceModeListener.php
index 4d041c7..1932fca 100644
--- a/src/EventListener/MaintenanceModeListener.php
+++ b/src/EventListener/MaintenanceModeListener.php
@@ -24,14 +24,14 @@ final class MaintenanceModeListener
return;
}
- $flagFile = $this->kernel->getProjectDir() . '/var/maintenance';
+ $flagFile = $this->kernel->getProjectDir().'/var/maintenance';
if (!file_exists($flagFile)) {
return;
}
$request = $event->getRequest();
- $path = $request->getPathInfo();
+ $path = $request->getPathInfo();
// Always allow maintenance status endpoint and session endpoints
if (str_starts_with($path, '/api/admin/maintenance')