Consolidate create and edit pages into single create pages with edit mode support. Remove obsolete catalog pages, history composables, and fix remaining code review issues. Include migration to relink orphaned custom fields. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
364 lines
14 KiB
Vue
364 lines
14 KiB
Vue
<template>
|
|
<div>
|
|
<main class="container mx-auto px-6 py-8">
|
|
<!-- Loading State -->
|
|
<div v-if="d.loading.value" class="flex justify-center items-center py-16">
|
|
<span class="loading loading-spinner loading-lg text-primary"></span>
|
|
</div>
|
|
|
|
<!-- Machine Details -->
|
|
<div v-else-if="d.machine.value" ref="d.printAreaRef" class="space-y-8">
|
|
<DocumentPreviewModal
|
|
:document="d.previewDocument.value"
|
|
:visible="d.previewVisible.value"
|
|
:documents="d.machineDocumentsList.value"
|
|
@close="d.closePreview"
|
|
/>
|
|
|
|
<!-- Header with actions -->
|
|
<MachineDetailHeader
|
|
:title="d.machine.value.name"
|
|
:description="d.machine.value.description"
|
|
:site-name="d.machine.value.site?.name"
|
|
:site-color="d.machine.value.site?.color"
|
|
:reference="d.machine.value.reference"
|
|
:is-edit-mode="d.isEditMode.value"
|
|
@toggle-edit="d.toggleEditMode"
|
|
@open-print="d.openPrintModal"
|
|
/>
|
|
|
|
<!-- Tabbed content -->
|
|
<EntityTabs v-model="activeTab" :tabs="machineTabs" aria-label="Sections machine">
|
|
<template #tab-general>
|
|
<div class="space-y-8">
|
|
<MachineInfoCard
|
|
ref="machineInfoCardRef"
|
|
:is-edit-mode="d.isEditMode.value"
|
|
:machine-name="d.machineName.value"
|
|
:machine-reference="d.machineReference.value"
|
|
:machine-site-id="d.machineSiteId.value"
|
|
:machine-site-name="d.machine.value?.site?.name ?? ''"
|
|
:sites="d.sites.value"
|
|
:machine-constructeur-ids="d.machineConstructeurIds.value"
|
|
:machine-constructeurs-display="d.machineConstructeursDisplay.value"
|
|
:has-machine-constructeur="d.hasMachineConstructeur.value"
|
|
:constructeur-links="d.constructeurLinks.value"
|
|
:visible-custom-fields="d.visibleMachineCustomFields.value"
|
|
:get-machine-field-id="d.getMachineFieldId"
|
|
:machine-id="machineId"
|
|
:machine-custom-field-defs="d.machine.value?.customFields ?? []"
|
|
@update:machine-name="d.machineName.value = $event"
|
|
@update:machine-reference="d.machineReference.value = $event"
|
|
@update:machine-site-id="d.machineSiteId.value = $event"
|
|
@update:constructeur-ids="d.handleMachineConstructeurChange"
|
|
@update:constructeur-links="d.constructeurLinks.value = $event"
|
|
@remove-constructeur-link="handleRemoveConstructeurLink"
|
|
@set-custom-field-value="d.setMachineCustomFieldValue"
|
|
@custom-fields-saved="() => { if (!isSavingMachine) { d.loadMachineData(); refreshVersions() } }"
|
|
/>
|
|
<MachineProductsCard
|
|
v-if="d.isEditMode.value || d.machineDirectProducts.value.length > 0"
|
|
:products="d.machineDirectProducts.value"
|
|
:is-edit-mode="d.isEditMode.value"
|
|
@add-product="openAddModal('product')"
|
|
@remove-product="confirmRemoveProduct"
|
|
@fill-entity="(linkId, typeId) => handleFillEntity(linkId, 'product', typeId)"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<template #tab-structure>
|
|
<div class="space-y-8">
|
|
<MachineComponentsCard
|
|
v-if="d.isEditMode.value || d.components.value.length > 0"
|
|
:components="d.components.value"
|
|
:is-edit-mode="d.isEditMode.value"
|
|
:collapsed="d.componentsCollapsed.value"
|
|
:collapse-toggle-token="d.collapseToggleToken.value"
|
|
@toggle-collapse="d.toggleAllComponents"
|
|
@update-component="d.updateComponent"
|
|
@edit-piece="d.updatePieceFromComponent"
|
|
@custom-field-update="d.handleCustomFieldUpdate"
|
|
@add-component="openAddModal('component')"
|
|
@remove-component="confirmRemoveComponent"
|
|
@fill-entity="(linkId, typeId) => handleFillEntity(linkId, 'component', typeId)"
|
|
/>
|
|
<MachinePiecesCard
|
|
v-if="d.isEditMode.value || d.machinePieces.value.length > 0"
|
|
:pieces="d.machinePieces.value"
|
|
:is-edit-mode="d.isEditMode.value"
|
|
:collapsed="d.piecesCollapsed.value"
|
|
:collapse-toggle-token="d.pieceCollapseToggleToken.value"
|
|
@update-piece="d.updatePieceInfo"
|
|
@edit-piece="d.editPiece"
|
|
@custom-field-update="d.handleCustomFieldUpdate"
|
|
@add-piece="openAddModal('piece')"
|
|
@remove-piece="confirmRemovePiece"
|
|
@fill-entity="(linkId, typeId) => handleFillEntity(linkId, 'piece', typeId)"
|
|
@toggle-collapse="d.toggleAllPieces"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<template #tab-documents>
|
|
<MachineDocumentsCard
|
|
v-if="d.isEditMode.value || d.machineDocumentsList.value.length > 0"
|
|
:documents="d.machineDocumentsList.value"
|
|
:is-edit-mode="d.isEditMode.value"
|
|
:uploading="d.machineDocumentsUploading.value"
|
|
:files="d.machineDocumentFiles.value"
|
|
@update:files="d.machineDocumentFiles.value = $event"
|
|
@files-added="d.handleMachineFilesAdded"
|
|
@preview="d.openPreview"
|
|
@download="d.downloadDocument"
|
|
@remove="confirmRemoveDocument"
|
|
/>
|
|
</template>
|
|
|
|
<template #tab-history>
|
|
<div class="space-y-8">
|
|
<EntityHistorySection
|
|
:entries="history"
|
|
:loading="historyLoading"
|
|
:error="historyError"
|
|
:field-labels="historyFieldLabels"
|
|
/>
|
|
<EntityVersionList
|
|
ref="versionListRef"
|
|
entity-type="machine"
|
|
:entity-id="String(machineId)"
|
|
:field-labels="historyFieldLabels"
|
|
:refresh-key="versionRefreshKey"
|
|
@restored="d.loadMachineData()"
|
|
/>
|
|
<CommentSection
|
|
entity-type="machine"
|
|
:entity-id="String(machineId)"
|
|
:entity-name="d.machine.value?.name"
|
|
show-resolved
|
|
/>
|
|
</div>
|
|
</template>
|
|
</EntityTabs>
|
|
|
|
<!-- Add Entity Modal -->
|
|
<AddEntityToMachineModal
|
|
:open="addModalOpen"
|
|
:entity-kind="addModalKind"
|
|
:prefill-type-id="fillTypeId"
|
|
@close="addModalOpen = false; fillLinkId = ''; fillTypeId = ''"
|
|
@confirm="handleAddEntity"
|
|
/>
|
|
|
|
<!-- Save / Cancel buttons -->
|
|
<div v-if="d.isEditMode.value" class="flex flex-col gap-3 md:flex-row md:justify-end">
|
|
<button
|
|
type="button"
|
|
class="btn btn-ghost"
|
|
:class="{ 'btn-disabled': d.saving.value }"
|
|
@click="d.cancelEdition()"
|
|
>
|
|
Annuler
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="btn btn-primary"
|
|
:disabled="!d.canSubmit.value"
|
|
@click="submitMachineEdition"
|
|
>
|
|
<span v-if="d.saving.value" class="loading loading-spinner loading-sm mr-2" />
|
|
Enregistrer les modifications
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Error State -->
|
|
<EmptyState
|
|
v-else
|
|
:icon="IconLucideAlertTriangle"
|
|
title="Machine non trouvée"
|
|
:description="`La machine avec l'ID « ${machineId} » n'existe pas ou a été supprimée.`"
|
|
action-label="Retour aux machines"
|
|
@action="$router.back()"
|
|
/>
|
|
</main>
|
|
|
|
<MachinePrintSelectionModal
|
|
:open="d.printModalOpen.value"
|
|
:selection="d.printSelection"
|
|
:components="d.components.value"
|
|
:pieces="d.machinePieces.value"
|
|
@close="d.closePrintModal"
|
|
@confirm="d.handlePrintConfirm"
|
|
@select-all="d.setAllPrintSelection(true)"
|
|
@deselect-all="d.setAllPrintSelection(false)"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, ref, watch, onMounted } from 'vue'
|
|
import { useRoute } from 'vue-router'
|
|
import { useMachineDetailData } from '~/composables/useMachineDetailData'
|
|
import { useEntityHistory } from '~/composables/useEntityHistory'
|
|
import DocumentPreviewModal from '~/components/DocumentPreviewModal.vue'
|
|
import MachinePrintSelectionModal from '~/components/MachinePrintSelectionModal.vue'
|
|
import MachineDetailHeader from '~/components/machine/MachineDetailHeader.vue'
|
|
import MachineInfoCard from '~/components/machine/MachineInfoCard.vue'
|
|
import MachineDocumentsCard from '~/components/machine/MachineDocumentsCard.vue'
|
|
import MachineProductsCard from '~/components/machine/MachineProductsCard.vue'
|
|
import MachineComponentsCard from '~/components/machine/MachineComponentsCard.vue'
|
|
import MachinePiecesCard from '~/components/machine/MachinePiecesCard.vue'
|
|
import AddEntityToMachineModal from '~/components/machine/AddEntityToMachineModal.vue'
|
|
import EntityHistorySection from '~/components/common/EntityHistorySection.vue'
|
|
import EntityVersionList from '~/components/common/EntityVersionList.vue'
|
|
import IconLucideAlertTriangle from '~icons/lucide/alert-triangle'
|
|
|
|
const route = useRoute()
|
|
const machineId = route.params.id
|
|
const { canEdit } = usePermissions()
|
|
|
|
if (!machineId) {
|
|
console.error('ID de machine manquant')
|
|
}
|
|
|
|
const d = useMachineDetailData(machineId)
|
|
const machineInfoCardRef = ref<{ saveFieldDefinitions?: () => Promise<void> } | null>(null)
|
|
const versionRefreshKey = ref(0)
|
|
const refreshVersions = () => { versionRefreshKey.value++ }
|
|
const isSavingMachine = ref(false)
|
|
const { confirm: confirmDialog } = useConfirm()
|
|
|
|
const versionListRef = ref<InstanceType<typeof EntityVersionList> | null>(null)
|
|
const activeTab = ref((route.query.tab as string) || 'general')
|
|
watch(activeTab, (val) => {
|
|
navigateTo({ query: { ...route.query, tab: val } }, { replace: true })
|
|
})
|
|
|
|
const machineTabs = computed(() => [
|
|
{ key: 'general', label: 'Général' },
|
|
{ key: 'structure', label: 'Structure', count: d.components.value.length + d.machinePieces.value.length },
|
|
{ key: 'documents', label: 'Documents', count: d.machineDocumentsList.value.length },
|
|
{ key: 'history', label: 'Historique' },
|
|
])
|
|
|
|
const {
|
|
history,
|
|
loading: historyLoading,
|
|
error: historyError,
|
|
loadHistory,
|
|
} = useEntityHistory('machine')
|
|
|
|
const historyFieldLabels = {
|
|
name: 'Nom',
|
|
reference: 'Référence',
|
|
prix: 'Prix',
|
|
site: 'Site',
|
|
constructeurIds: 'Fournisseurs',
|
|
addedComponent: 'Composant ajouté',
|
|
removedComponent: 'Composant supprimé',
|
|
addedPiece: 'Pièce ajoutée',
|
|
removedPiece: 'Pièce supprimée',
|
|
addedProduct: 'Produit ajouté',
|
|
removedProduct: 'Produit supprimé',
|
|
componentLinks: 'Composants liés',
|
|
pieceLinks: 'Pièces liées',
|
|
productLinks: 'Produits liés',
|
|
}
|
|
|
|
const addModalOpen = ref(false)
|
|
const addModalKind = ref('component')
|
|
const fillLinkId = ref('')
|
|
const fillTypeId = ref('')
|
|
|
|
const openAddModal = (kind) => {
|
|
addModalKind.value = kind
|
|
addModalOpen.value = true
|
|
}
|
|
|
|
const handleRemoveConstructeurLink = (constructeurId) => {
|
|
const ids = d.machineConstructeurIds.value.filter(id => id !== constructeurId)
|
|
d.handleMachineConstructeurChange(ids)
|
|
}
|
|
|
|
const handleAddEntity = async (payload) => {
|
|
const { entityId, modelTypeId } = payload
|
|
|
|
if (fillLinkId.value) {
|
|
await d.fillEntityLink(fillLinkId.value, entityId, addModalKind.value)
|
|
fillLinkId.value = ''
|
|
fillTypeId.value = ''
|
|
} else if (entityId) {
|
|
if (addModalKind.value === 'component') {
|
|
await d.addComponentLink(entityId)
|
|
} else if (addModalKind.value === 'piece') {
|
|
await d.addPieceLink(entityId)
|
|
} else {
|
|
await d.addProductLink(entityId)
|
|
}
|
|
} else {
|
|
if (addModalKind.value === 'component') {
|
|
await d.addComponentLinkCategoryOnly(modelTypeId)
|
|
} else if (addModalKind.value === 'piece') {
|
|
await d.addPieceLinkCategoryOnly(modelTypeId)
|
|
} else {
|
|
await d.addProductLinkCategoryOnly(modelTypeId)
|
|
}
|
|
}
|
|
refreshVersions()
|
|
}
|
|
|
|
const handleFillEntity = (linkId: string, entityKind: string, modelTypeId: string) => {
|
|
fillLinkId.value = linkId
|
|
fillTypeId.value = modelTypeId
|
|
addModalKind.value = entityKind
|
|
addModalOpen.value = true
|
|
}
|
|
|
|
const submitMachineEdition = async () => {
|
|
isSavingMachine.value = true
|
|
try {
|
|
if (machineInfoCardRef.value?.saveFieldDefinitions) {
|
|
await machineInfoCardRef.value.saveFieldDefinitions()
|
|
}
|
|
await d.submitEdition()
|
|
refreshVersions()
|
|
} finally {
|
|
isSavingMachine.value = false
|
|
}
|
|
}
|
|
|
|
const confirmRemoveProduct = async (id: string) => {
|
|
if (!await confirmDialog({ title: 'Retirer ce produit ?', message: 'Le produit sera dissocié de la machine.', confirmText: 'Retirer', dangerous: true })) return
|
|
await d.removeProductLink(id)
|
|
refreshVersions()
|
|
}
|
|
|
|
const confirmRemoveComponent = async (id: string) => {
|
|
if (!await confirmDialog({ title: 'Retirer ce composant ?', message: 'Le composant sera dissocié de la machine.', confirmText: 'Retirer', dangerous: true })) return
|
|
await d.removeComponentLink(id)
|
|
refreshVersions()
|
|
}
|
|
|
|
const confirmRemovePiece = async (id: string) => {
|
|
if (!await confirmDialog({ title: 'Retirer cette pièce ?', message: 'La pièce sera dissociée de la machine.', confirmText: 'Retirer', dangerous: true })) return
|
|
await d.removePieceLink(id)
|
|
refreshVersions()
|
|
}
|
|
|
|
const confirmRemoveDocument = async (id: string) => {
|
|
if (!await confirmDialog({ title: 'Supprimer ce document ?', message: 'Le fichier sera supprimé définitivement.', confirmText: 'Supprimer', dangerous: true })) return
|
|
await d.removeMachineDocument(id)
|
|
}
|
|
|
|
onMounted(() => {
|
|
d.loadMachineData()
|
|
d.loadInitialData()
|
|
loadHistory(String(machineId)).catch(() => {})
|
|
|
|
if (route.query.edit === 'true' && canEdit.value) {
|
|
d.isEditMode.value = true
|
|
}
|
|
})
|
|
</script>
|