refactor(catalog) : extract shared delete impact logic and cleanup dead code
Extract duplicated resolveDeleteImpact/buildDeleteMessage into shared utility, remove redundant computed wrappers, fix indentation, and remove dead code. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -93,77 +93,77 @@
|
|||||||
@drop.prevent="onDrop(index)"
|
@drop.prevent="onDrop(index)"
|
||||||
@dragend="onDragEnd"
|
@dragend="onDragEnd"
|
||||||
>
|
>
|
||||||
<div class="flex items-start gap-3">
|
<div class="flex items-start gap-3">
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-ghost btn-xs btn-square cursor-grab active:cursor-grabbing mt-1"
|
|
||||||
title="Réordonner"
|
|
||||||
draggable="false"
|
|
||||||
>
|
|
||||||
<IconLucideGripVertical class="w-4 h-4" aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="flex-1 space-y-2">
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
|
|
||||||
<input
|
|
||||||
v-model="field.name"
|
|
||||||
type="text"
|
|
||||||
class="input input-bordered input-xs"
|
|
||||||
placeholder="Nom du champ"
|
|
||||||
>
|
|
||||||
<select v-model="field.type" class="select select-bordered select-xs" :disabled="isFieldLocked(field)">
|
|
||||||
<option value="text">
|
|
||||||
Texte
|
|
||||||
</option>
|
|
||||||
<option value="number">
|
|
||||||
Nombre
|
|
||||||
</option>
|
|
||||||
<option value="select">
|
|
||||||
Liste
|
|
||||||
</option>
|
|
||||||
<option value="boolean">
|
|
||||||
Oui/Non
|
|
||||||
</option>
|
|
||||||
<option value="date">
|
|
||||||
Date
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-2 text-xs">
|
|
||||||
<input v-model="field.required" type="checkbox" class="checkbox checkbox-xs" :disabled="isFieldLocked(field)">
|
|
||||||
Obligatoire
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<textarea
|
|
||||||
v-if="field.type === 'select'"
|
|
||||||
v-model="field.optionsText"
|
|
||||||
class="textarea textarea-bordered textarea-xs h-20"
|
|
||||||
placeholder="Option 1 Option 2"
|
|
||||||
:disabled="isFieldLocked(field)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
v-if="!isFieldLocked(field)"
|
|
||||||
type="button"
|
|
||||||
class="btn btn-error btn-xs btn-square"
|
|
||||||
@click="removeField(index)"
|
|
||||||
>
|
|
||||||
<IconLucideTrash class="w-4 h-4" aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
<div v-else class="tooltip tooltip-left" data-tip="Ce champ ne peut pas être supprimé car des éléments utilisent cette catégorie">
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-ghost btn-xs btn-square opacity-30 cursor-not-allowed"
|
class="btn btn-ghost btn-xs btn-square cursor-grab active:cursor-grabbing mt-1"
|
||||||
disabled
|
title="Réordonner"
|
||||||
|
draggable="false"
|
||||||
|
>
|
||||||
|
<IconLucideGripVertical class="w-4 h-4" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="flex-1 space-y-2">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||||
|
<input
|
||||||
|
v-model="field.name"
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered input-xs"
|
||||||
|
placeholder="Nom du champ"
|
||||||
|
>
|
||||||
|
<select v-model="field.type" class="select select-bordered select-xs" :disabled="isFieldLocked(field)">
|
||||||
|
<option value="text">
|
||||||
|
Texte
|
||||||
|
</option>
|
||||||
|
<option value="number">
|
||||||
|
Nombre
|
||||||
|
</option>
|
||||||
|
<option value="select">
|
||||||
|
Liste
|
||||||
|
</option>
|
||||||
|
<option value="boolean">
|
||||||
|
Oui/Non
|
||||||
|
</option>
|
||||||
|
<option value="date">
|
||||||
|
Date
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 text-xs">
|
||||||
|
<input v-model="field.required" type="checkbox" class="checkbox checkbox-xs" :disabled="isFieldLocked(field)">
|
||||||
|
Obligatoire
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
v-if="field.type === 'select'"
|
||||||
|
v-model="field.optionsText"
|
||||||
|
class="textarea textarea-bordered textarea-xs h-20"
|
||||||
|
placeholder="Option 1 Option 2"
|
||||||
|
:disabled="isFieldLocked(field)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-if="!isFieldLocked(field)"
|
||||||
|
type="button"
|
||||||
|
class="btn btn-error btn-xs btn-square"
|
||||||
|
@click="removeField(index)"
|
||||||
>
|
>
|
||||||
<IconLucideTrash class="w-4 h-4" aria-hidden="true" />
|
<IconLucideTrash class="w-4 h-4" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
|
<div v-else class="tooltip tooltip-left" data-tip="Ce champ ne peut pas être supprimé car des éléments utilisent cette catégorie">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-ghost btn-xs btn-square opacity-30 cursor-not-allowed"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
<IconLucideTrash class="w-4 h-4" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</li>
|
||||||
</li>
|
</ul>
|
||||||
</ul>
|
|
||||||
<button type="button" class="btn btn-outline btn-xs" @click="addField">
|
<button type="button" class="btn btn-outline btn-xs" @click="addField">
|
||||||
<IconLucidePlus class="w-3 h-3 mr-2" aria-hidden="true" />
|
<IconLucidePlus class="w-3 h-3 mr-2" aria-hidden="true" />
|
||||||
Ajouter
|
Ajouter
|
||||||
|
|||||||
@@ -532,11 +532,6 @@ const getPieceTypeLabel = (id?: string) => {
|
|||||||
return formatModelTypeOption(pieceTypeMap.value.get(id))
|
return formatModelTypeOption(pieceTypeMap.value.get(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
const _getProductTypeLabel = (id?: string) => {
|
|
||||||
if (!id) return ''
|
|
||||||
return formatModelTypeOption(productTypeMap.value.get(id))
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatComponentTypeOption = (type: ModelTypeOption | undefined | null) =>
|
const formatComponentTypeOption = (type: ModelTypeOption | undefined | null) =>
|
||||||
formatModelTypeOption(type)
|
formatModelTypeOption(type)
|
||||||
|
|
||||||
@@ -571,19 +566,6 @@ const syncComponentType = (component: EditableStructureNode) => {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (props.lockType && props.isRoot) {
|
|
||||||
if (props.lockedTypeLabel) {
|
|
||||||
component.typeComposantLabel = props.lockedTypeLabel
|
|
||||||
if (!component.alias || component.alias === component.typeComposantLabel) {
|
|
||||||
component.alias = props.lockedTypeLabel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (component.typeComposantId) {
|
|
||||||
const option = componentTypeMap.value.get(component.typeComposantId)
|
|
||||||
component.familyCode = option?.code ?? component.familyCode
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const id = typeof component.typeComposantId === 'string'
|
const id = typeof component.typeComposantId === 'string'
|
||||||
? component.typeComposantId
|
? component.typeComposantId
|
||||||
: ''
|
: ''
|
||||||
|
|||||||
@@ -127,11 +127,11 @@ import { useComponentTypes } from '~/composables/useComponentTypes'
|
|||||||
import { useDataTable } from '~/composables/useDataTable'
|
import { useDataTable } from '~/composables/useDataTable'
|
||||||
import DocumentThumbnail from '~/components/DocumentThumbnail.vue'
|
import DocumentThumbnail from '~/components/DocumentThumbnail.vue'
|
||||||
import { isImageDocument, isPdfDocument } from '~/utils/documentPreview'
|
import { isImageDocument, isPdfDocument } from '~/utils/documentPreview'
|
||||||
|
import { resolveDeleteImpact, buildDeleteMessage } from '~/shared/utils/deleteImpactUtils'
|
||||||
|
|
||||||
const { canEdit } = usePermissions()
|
const { canEdit } = usePermissions()
|
||||||
const { composants, total, loadComposants, loading: loadingComposantsRef, deleteComposant } = useComposants()
|
const { composants, total, loadComposants, loading: loadingComposants, deleteComposant } = useComposants()
|
||||||
const { componentTypes, loadComponentTypes } = useComponentTypes()
|
const { componentTypes, loadComponentTypes } = useComponentTypes()
|
||||||
const loadingComposants = computed(() => loadingComposantsRef.value)
|
|
||||||
|
|
||||||
const table = useDataTable(
|
const table = useDataTable(
|
||||||
{ fetchData: fetchComposants },
|
{ fetchData: fetchComposants },
|
||||||
@@ -201,27 +201,12 @@ const resolveComponentType = (component: Record<string, any>) => {
|
|||||||
return '—'
|
return '—'
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolveDeleteImpact = (component: Record<string, any>) => {
|
const { confirm } = useConfirm()
|
||||||
const impacts: string[] = []
|
|
||||||
const machineLinks = Array.isArray(component?.machineLinks) ? component.machineLinks.length : component?.machineLinksCount ?? 0
|
|
||||||
const documents = Array.isArray(component?.documents) ? component.documents.length : component?.documentsCount ?? 0
|
|
||||||
const customFields = Array.isArray(component?.customFieldValues) ? component.customFieldValues.length : component?.customFieldValuesCount ?? 0
|
|
||||||
if (machineLinks > 0) impacts.push(`${machineLinks} liaison${machineLinks > 1 ? 's' : ''} machine`)
|
|
||||||
if (documents > 0) impacts.push(`${documents} document${documents > 1 ? 's' : ''}`)
|
|
||||||
if (customFields > 0) impacts.push(`${customFields} valeur${customFields > 1 ? 's' : ''} de champs personnalisés`)
|
|
||||||
return impacts
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDeleteComponent = async (component: Record<string, any>) => {
|
const handleDeleteComponent = async (component: Record<string, any>) => {
|
||||||
const componentName = component?.name || 'ce composant'
|
const componentName = component?.name || 'ce composant'
|
||||||
const impacts = resolveDeleteImpact(component)
|
const message = buildDeleteMessage(componentName, resolveDeleteImpact(component))
|
||||||
const lines = [`Voulez-vous vraiment supprimer « ${componentName} » ?`]
|
const confirmed = await confirm({ title: 'Supprimer le composant', message, dangerous: true })
|
||||||
if (impacts.length) {
|
|
||||||
lines.push(`Cela supprimera également :\n• ${impacts.join('\n• ')}`)
|
|
||||||
}
|
|
||||||
lines.push('Cette action est irréversible.')
|
|
||||||
const { confirm } = useConfirm()
|
|
||||||
const confirmed = await confirm({ title: 'Supprimer le composant', message: lines.join('\n\n'), dangerous: true })
|
|
||||||
if (!confirmed) return
|
if (!confirmed) return
|
||||||
await deleteComposant(component.id)
|
await deleteComposant(component.id)
|
||||||
fetchComposants()
|
fetchComposants()
|
||||||
|
|||||||
@@ -150,11 +150,11 @@ import { usePieceTypes } from '~/composables/usePieceTypes'
|
|||||||
import { useDataTable } from '~/composables/useDataTable'
|
import { useDataTable } from '~/composables/useDataTable'
|
||||||
import DocumentThumbnail from '~/components/DocumentThumbnail.vue'
|
import DocumentThumbnail from '~/components/DocumentThumbnail.vue'
|
||||||
import { isImageDocument, isPdfDocument } from '~/utils/documentPreview'
|
import { isImageDocument, isPdfDocument } from '~/utils/documentPreview'
|
||||||
|
import { resolveDeleteImpact, buildDeleteMessage } from '~/shared/utils/deleteImpactUtils'
|
||||||
|
|
||||||
const { canEdit } = usePermissions()
|
const { canEdit } = usePermissions()
|
||||||
const { pieces, total, loadPieces, loading: loadingPiecesRef, deletePiece } = usePieces()
|
const { pieces, total, loadPieces, loading: loadingPieces, deletePiece } = usePieces()
|
||||||
const { pieceTypes, loadPieceTypes } = usePieceTypes()
|
const { pieceTypes, loadPieceTypes } = usePieceTypes()
|
||||||
const loadingPieces = computed(() => loadingPiecesRef.value)
|
|
||||||
|
|
||||||
const table = useDataTable(
|
const table = useDataTable(
|
||||||
{ fetchData: fetchPieces },
|
{ fetchData: fetchPieces },
|
||||||
@@ -278,27 +278,12 @@ const buildPieceSuppliersDisplay = (piece: Record<string, any>) => {
|
|||||||
return { suppliers, visible, overflow, tooltip: suppliers.length ? suppliers.join(', ') : '' }
|
return { suppliers, visible, overflow, tooltip: suppliers.length ? suppliers.join(', ') : '' }
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolveDeleteImpact = (piece: Record<string, any>) => {
|
const { confirm } = useConfirm()
|
||||||
const impacts: string[] = []
|
|
||||||
const machineLinks = Array.isArray(piece?.machineLinks) ? piece.machineLinks.length : piece?.machineLinksCount ?? 0
|
|
||||||
const documents = Array.isArray(piece?.documents) ? piece.documents.length : piece?.documentsCount ?? 0
|
|
||||||
const customFields = Array.isArray(piece?.customFieldValues) ? piece.customFieldValues.length : piece?.customFieldValuesCount ?? 0
|
|
||||||
if (machineLinks > 0) impacts.push(`${machineLinks} liaison${machineLinks > 1 ? 's' : ''} machine`)
|
|
||||||
if (documents > 0) impacts.push(`${documents} document${documents > 1 ? 's' : ''}`)
|
|
||||||
if (customFields > 0) impacts.push(`${customFields} valeur${customFields > 1 ? 's' : ''} de champs personnalisés`)
|
|
||||||
return impacts
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDeletePiece = async (piece: Record<string, any>) => {
|
const handleDeletePiece = async (piece: Record<string, any>) => {
|
||||||
const pieceName = piece?.name || 'cette pièce'
|
const pieceName = piece?.name || 'cette pièce'
|
||||||
const impacts = resolveDeleteImpact(piece)
|
const message = buildDeleteMessage(pieceName, resolveDeleteImpact(piece))
|
||||||
const lines = [`Voulez-vous vraiment supprimer « ${pieceName} » ?`]
|
const confirmed = await confirm({ title: 'Supprimer la pièce', message, dangerous: true })
|
||||||
if (impacts.length) {
|
|
||||||
lines.push(`Cela supprimera également :\n• ${impacts.join('\n• ')}`)
|
|
||||||
}
|
|
||||||
lines.push('Cette action est irréversible.')
|
|
||||||
const { confirm } = useConfirm()
|
|
||||||
const confirmed = await confirm({ title: 'Supprimer la pièce', message: lines.join('\n\n'), dangerous: true })
|
|
||||||
if (!confirmed) return
|
if (!confirmed) return
|
||||||
await deletePiece(piece.id)
|
await deletePiece(piece.id)
|
||||||
fetchPieces()
|
fetchPieces()
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
v-else
|
v-else
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
:rows="productRows"
|
:rows="productRows"
|
||||||
:loading="loadingProducts"
|
:loading="loading"
|
||||||
:sort="table.sort.value"
|
:sort="table.sort.value"
|
||||||
:pagination="paginationState"
|
:pagination="paginationState"
|
||||||
:column-filters="table.columnFilters.value"
|
:column-filters="table.columnFilters.value"
|
||||||
@@ -148,6 +148,7 @@ import { useToast } from '~/composables/useToast'
|
|||||||
import { useDataTable } from '~/composables/useDataTable'
|
import { useDataTable } from '~/composables/useDataTable'
|
||||||
import DocumentThumbnail from '~/components/DocumentThumbnail.vue'
|
import DocumentThumbnail from '~/components/DocumentThumbnail.vue'
|
||||||
import { isImageDocument, isPdfDocument } from '~/utils/documentPreview'
|
import { isImageDocument, isPdfDocument } from '~/utils/documentPreview'
|
||||||
|
import { resolveDeleteImpact, buildDeleteMessage } from '~/shared/utils/deleteImpactUtils'
|
||||||
|
|
||||||
const { canEdit } = usePermissions()
|
const { canEdit } = usePermissions()
|
||||||
|
|
||||||
@@ -169,7 +170,6 @@ const table = useDataTable(
|
|||||||
{ defaultSort: 'name', defaultDirection: 'asc', defaultPerPage: 20, persistToUrl: true },
|
{ defaultSort: 'name', defaultDirection: 'asc', defaultPerPage: 20, persistToUrl: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
const loadingProducts = computed(() => loading.value)
|
|
||||||
const errorMessage = computed(() => (typeof error.value === 'string' && error.value.length ? error.value : null))
|
const errorMessage = computed(() => (typeof error.value === 'string' && error.value.length ? error.value : null))
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
@@ -296,26 +296,10 @@ const reload = () => fetchProducts()
|
|||||||
|
|
||||||
const { confirm } = useConfirm()
|
const { confirm } = useConfirm()
|
||||||
|
|
||||||
const resolveDeleteImpact = (product: Record<string, any>) => {
|
|
||||||
const impacts: string[] = []
|
|
||||||
const machineLinks = Array.isArray(product?.machineLinks) ? product.machineLinks.length : product?.machineLinksCount ?? 0
|
|
||||||
const documents = Array.isArray(product?.documents) ? product.documents.length : product?.documentsCount ?? 0
|
|
||||||
const customFields = Array.isArray(product?.customFieldValues) ? product.customFieldValues.length : product?.customFieldValuesCount ?? 0
|
|
||||||
if (machineLinks > 0) impacts.push(`${machineLinks} liaison${machineLinks > 1 ? 's' : ''} machine`)
|
|
||||||
if (documents > 0) impacts.push(`${documents} document${documents > 1 ? 's' : ''}`)
|
|
||||||
if (customFields > 0) impacts.push(`${customFields} valeur${customFields > 1 ? 's' : ''} de champs personnalisés`)
|
|
||||||
return impacts
|
|
||||||
}
|
|
||||||
|
|
||||||
const confirmDelete = async (product: Record<string, any>) => {
|
const confirmDelete = async (product: Record<string, any>) => {
|
||||||
const productName = product?.name || 'ce produit'
|
const productName = product?.name || 'ce produit'
|
||||||
const impacts = resolveDeleteImpact(product)
|
const message = buildDeleteMessage(productName, resolveDeleteImpact(product))
|
||||||
const lines = [`Voulez-vous vraiment supprimer « ${productName} » ?`]
|
const confirmed = await confirm({ title: 'Supprimer le produit', message, dangerous: true })
|
||||||
if (impacts.length) {
|
|
||||||
lines.push(`Cela supprimera également :\n• ${impacts.join('\n• ')}`)
|
|
||||||
}
|
|
||||||
lines.push('Cette action est irréversible.')
|
|
||||||
const confirmed = await confirm({ title: 'Supprimer le produit', message: lines.join('\n\n'), dangerous: true })
|
|
||||||
if (!confirmed) return
|
if (!confirmed) return
|
||||||
const result = await deleteProduct(product.id)
|
const result = await deleteProduct(product.id)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
|
|||||||
19
app/shared/utils/deleteImpactUtils.ts
Normal file
19
app/shared/utils/deleteImpactUtils.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
export const resolveDeleteImpact = (entity: Record<string, any>): string[] => {
|
||||||
|
const impacts: string[] = []
|
||||||
|
const machineLinks = Array.isArray(entity?.machineLinks) ? entity.machineLinks.length : entity?.machineLinksCount ?? 0
|
||||||
|
const documents = Array.isArray(entity?.documents) ? entity.documents.length : entity?.documentsCount ?? 0
|
||||||
|
const customFields = Array.isArray(entity?.customFieldValues) ? entity.customFieldValues.length : entity?.customFieldValuesCount ?? 0
|
||||||
|
if (machineLinks > 0) impacts.push(`${machineLinks} liaison${machineLinks > 1 ? 's' : ''} machine`)
|
||||||
|
if (documents > 0) impacts.push(`${documents} document${documents > 1 ? 's' : ''}`)
|
||||||
|
if (customFields > 0) impacts.push(`${customFields} valeur${customFields > 1 ? 's' : ''} de champs personnalisés`)
|
||||||
|
return impacts
|
||||||
|
}
|
||||||
|
|
||||||
|
export const buildDeleteMessage = (entityName: string, impacts: string[]): string => {
|
||||||
|
const lines = [`Voulez-vous vraiment supprimer « ${entityName} » ?`]
|
||||||
|
if (impacts.length) {
|
||||||
|
lines.push(`Cela supprimera également :\n• ${impacts.join('\n• ')}`)
|
||||||
|
}
|
||||||
|
lines.push('Cette action est irréversible.')
|
||||||
|
return lines.join('\n\n')
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user