5 Commits

Author SHA1 Message Date
Matthieu
592beb0fa7 fix(ui) : move add buttons below last element in structure editors
Place "Ajouter" buttons after the items list instead of in the section
header, so they always appear below the last added element.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 11:28:07 +01:00
Matthieu
e732585e63 fix(catalog) : add delete impact confirmation to product catalog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 11:06:06 +01:00
Matthieu
f1cc21c31b docs(changelog) : add delete confirmation dialog entry
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 10:59:43 +01:00
Matthieu
6c2f84dd3a fix(catalog) : replace blocking delete guard with confirmation dialog
Show cascade-delete impact (documents, machine links, custom fields)
in a confirmation modal instead of blocking deletion entirely.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 10:58:41 +01:00
Matthieu
032b3b33c9 docs(changelog) : add v1.8.1 release notes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 17:39:01 +01:00
9 changed files with 169 additions and 160 deletions

View File

@@ -1,19 +1,13 @@
<template> <template>
<div class="space-y-6"> <div class="space-y-6">
<section class="space-y-3"> <section class="space-y-3">
<header class="flex items-center justify-between"> <header>
<div> <h3 class="text-sm font-semibold">
<h3 class="text-sm font-semibold"> Produits inclus par défaut
Produits inclus par défaut </h3>
</h3> <p class="text-xs text-base-content/70">
<p class="text-xs text-base-content/70"> Ces produits s'afficheront lors de la création d'une pièce basée sur cette catégorie.
Ces produits s'afficheront lors de la création d'une pièce basée sur cette catégorie. </p>
</p>
</div>
<button v-if="!restrictedMode" type="button" class="btn btn-outline btn-xs" @click="addProduct">
<IconLucidePlus class="w-3 h-3 mr-2" aria-hidden="true" />
Ajouter
</button>
</header> </header>
<p v-if="!products.length" class="text-xs text-gray-500"> <p v-if="!products.length" class="text-xs text-gray-500">
@@ -71,18 +65,16 @@
</div> </div>
</li> </li>
</ul> </ul>
<button v-if="!restrictedMode" type="button" class="btn btn-outline btn-xs" @click="addProduct">
<IconLucidePlus class="w-3 h-3 mr-2" aria-hidden="true" />
Ajouter
</button>
</section> </section>
<section class="space-y-3"> <section class="space-y-3">
<header class="flex items-center justify-between"> <h3 class="text-sm font-semibold">
<h3 class="text-sm font-semibold"> Champs personnalisés
Champs personnalisés </h3>
</h3>
<button type="button" class="btn btn-outline btn-xs" @click="addField">
<IconLucidePlus class="w-3 h-3 mr-2" aria-hidden="true" />
Ajouter
</button>
</header>
<p v-if="!fields.length" class="text-xs text-gray-500"> <p v-if="!fields.length" class="text-xs text-gray-500">
Aucun champ personnalisé n'a encore été défini. Aucun champ personnalisé n'a encore été défini.
@@ -172,6 +164,10 @@
</div> </div>
</li> </li>
</ul> </ul>
<button type="button" class="btn btn-outline btn-xs" @click="addField">
<IconLucidePlus class="w-3 h-3 mr-2" aria-hidden="true" />
Ajouter
</button>
</section> </section>
</div> </div>
</template> </template>

View File

@@ -70,15 +70,9 @@
<div class="px-4 py-4 space-y-5"> <div class="px-4 py-4 space-y-5">
<section v-if="isRoot" class="space-y-3"> <section v-if="isRoot" class="space-y-3">
<div class="flex items-center justify-between gap-2"> <h4 :class="headingClass">
<h4 :class="headingClass"> {{ isRoot ? 'Champs personnalisés du composant' : 'Champs personnalisés' }}
{{ isRoot ? 'Champs personnalisés du composant' : 'Champs personnalisés' }} </h4>
</h4>
<button type="button" class="btn btn-outline btn-xs" @click="addCustomField">
<IconLucidePlus class="w-3 h-3 mr-2" aria-hidden="true" />
Ajouter
</button>
</div>
<p v-if="!(node.customFields?.length)" class="text-xs text-gray-500"> <p v-if="!(node.customFields?.length)" class="text-xs text-gray-500">
Aucun champ n'a encore été défini. Aucun champ n'a encore été défini.
</p> </p>
@@ -155,18 +149,16 @@
</div> </div>
</div> </div>
</div> </div>
<button type="button" class="btn btn-outline btn-xs" @click="addCustomField">
<IconLucidePlus class="w-3 h-3 mr-2" aria-hidden="true" />
Ajouter
</button>
</section> </section>
<section v-if="isRoot" class="space-y-3"> <section v-if="isRoot" class="space-y-3">
<div class="flex items-center justify-between gap-2"> <h4 :class="headingClass">
<h4 :class="headingClass"> {{ isRoot ? 'Produits inclus par défaut' : 'Produits' }}
{{ isRoot ? 'Produits inclus par défaut' : 'Produits' }} </h4>
</h4>
<button v-if="!restrictedMode" type="button" class="btn btn-outline btn-xs" @click="addProduct">
<IconLucidePlus class="w-3 h-3 mr-2" aria-hidden="true" />
Ajouter
</button>
</div>
<p v-if="!(node.products?.length)" class="text-xs text-gray-500"> <p v-if="!(node.products?.length)" class="text-xs text-gray-500">
Aucun produit défini. Aucun produit défini.
</p> </p>
@@ -228,18 +220,16 @@
</div> </div>
</div> </div>
</div> </div>
<button v-if="!restrictedMode" type="button" class="btn btn-outline btn-xs" @click="addProduct">
<IconLucidePlus class="w-3 h-3 mr-2" aria-hidden="true" />
Ajouter
</button>
</section> </section>
<section v-if="isRoot" class="space-y-3"> <section v-if="isRoot" class="space-y-3">
<div class="flex items-center justify-between gap-2"> <h4 :class="headingClass">
<h4 :class="headingClass"> {{ isRoot ? 'Pièces incluses par défaut' : 'Pièces' }}
{{ isRoot ? 'Pièces incluses par défaut' : 'Pièces' }} </h4>
</h4>
<button v-if="!restrictedMode" type="button" class="btn btn-outline btn-xs" @click="addPiece">
<IconLucidePlus class="w-3 h-3 mr-2" aria-hidden="true" />
Ajouter
</button>
</div>
<p v-if="!(node.pieces?.length)" class="text-xs text-gray-500"> <p v-if="!(node.pieces?.length)" class="text-xs text-gray-500">
Aucune pièce définie. Aucune pièce définie.
</p> </p>
@@ -302,21 +292,14 @@
</div> </div>
</div> </div>
</div> </div>
<button v-if="!restrictedMode" type="button" class="btn btn-outline btn-xs" @click="addPiece">
<IconLucidePlus class="w-3 h-3 mr-2" aria-hidden="true" />
Ajouter
</button>
</section> </section>
<section v-if="canManageSubcomponents || hasSubcomponents" class="space-y-3"> <section v-if="canManageSubcomponents || hasSubcomponents" class="space-y-3">
<div class="flex items-center justify-between gap-2"> <h4 :class="headingClass">Sous-composants</h4>
<h4 :class="headingClass">Sous-composants</h4>
<button
v-if="canManageSubcomponents && !restrictedMode"
type="button"
class="btn btn-outline btn-xs"
@click="addSubComponent"
>
<IconLucidePlus class="w-3 h-3 mr-2" aria-hidden="true" />
Ajouter
</button>
</div>
<p v-if="!isRoot && canManageSubcomponents" class="text-[11px] text-gray-500"> <p v-if="!isRoot && canManageSubcomponents" class="text-[11px] text-gray-500">
Sélectionnez uniquement la famille de ce sous-composant ; il sera configuré via son propre modèle. Sélectionnez uniquement la famille de ce sous-composant ; il sera configuré via son propre modèle.
</p> </p>
@@ -357,6 +340,15 @@
/> />
</div> </div>
</div> </div>
<button
v-if="canManageSubcomponents && !restrictedMode"
type="button"
class="btn btn-outline btn-xs"
@click="addSubComponent"
>
<IconLucidePlus class="w-3 h-3 mr-2" aria-hidden="true" />
Ajouter
</button>
</section> </section>
</div> </div>
</div> </div>

View File

@@ -3,31 +3,21 @@
<div class="card-body"> <div class="card-body">
<div class="flex justify-between items-center mb-4"> <div class="flex justify-between items-center mb-4">
<h2 class="card-title">Composants</h2> <h2 class="card-title">Composants</h2>
<div class="flex items-center gap-2"> <button
<button type="button"
v-if="isEditMode" class="btn btn-ghost btn-sm gap-2"
type="button" @click="$emit('toggle-collapse')"
class="btn btn-sm md:btn-md btn-primary" :title="collapsed ? 'Déplier tous les composants' : 'Replier tous les composants'"
@click="$emit('add-component')" >
> <IconLucideChevronRight
Ajouter un composant class="w-5 h-5 transition-transform"
</button> :class="collapsed ? 'rotate-0' : 'rotate-90'"
<button aria-hidden="true"
type="button" />
class="btn btn-ghost btn-sm gap-2" <span class="text-sm">
@click="$emit('toggle-collapse')" {{ collapsed ? 'Tout déplier' : 'Tout replier' }}
:title="collapsed ? 'Déplier tous les composants' : 'Replier tous les composants'" </span>
> </button>
<IconLucideChevronRight
class="w-5 h-5 transition-transform"
:class="collapsed ? 'rotate-0' : 'rotate-90'"
aria-hidden="true"
/>
<span class="text-sm">
{{ collapsed ? 'Tout déplier' : 'Tout replier' }}
</span>
</button>
</div>
</div> </div>
<div v-if="components.length === 0" class="text-sm text-gray-500 py-4"> <div v-if="components.length === 0" class="text-sm text-gray-500 py-4">
@@ -54,6 +44,15 @@
/> />
</div> </div>
</div> </div>
<button
v-if="isEditMode"
type="button"
class="btn btn-sm md:btn-md btn-primary"
@click="$emit('add-component')"
>
Ajouter un composant
</button>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -3,31 +3,21 @@
<div class="card-body"> <div class="card-body">
<div class="flex justify-between items-center mb-4"> <div class="flex justify-between items-center mb-4">
<h2 class="card-title">Pièces de la machine</h2> <h2 class="card-title">Pièces de la machine</h2>
<div class="flex items-center gap-2"> <button
<button type="button"
v-if="isEditMode" class="btn btn-ghost btn-sm gap-2"
type="button" @click="$emit('toggle-collapse')"
class="btn btn-sm md:btn-md btn-primary" :title="collapsed ? 'Déplier toutes les pièces' : 'Replier toutes les pièces'"
@click="$emit('add-piece')" >
> <IconLucideChevronRight
Ajouter une pièce class="w-5 h-5 transition-transform"
</button> :class="collapsed ? 'rotate-0' : 'rotate-90'"
<button aria-hidden="true"
type="button" />
class="btn btn-ghost btn-sm gap-2" <span class="text-sm">
@click="$emit('toggle-collapse')" {{ collapsed ? 'Tout déplier' : 'Tout replier' }}
:title="collapsed ? 'Déplier toutes les pièces' : 'Replier toutes les pièces'" </span>
> </button>
<IconLucideChevronRight
class="w-5 h-5 transition-transform"
:class="collapsed ? 'rotate-0' : 'rotate-90'"
aria-hidden="true"
/>
<span class="text-sm">
{{ collapsed ? 'Tout déplier' : 'Tout replier' }}
</span>
</button>
</div>
</div> </div>
<div v-if="pieces.length === 0" class="text-sm text-gray-500 py-4"> <div v-if="pieces.length === 0" class="text-sm text-gray-500 py-4">
@@ -54,6 +44,15 @@
/> />
</div> </div>
</div> </div>
<button
v-if="isEditMode"
type="button"
class="btn btn-sm md:btn-md btn-primary"
@click="$emit('add-piece')"
>
Ajouter une pièce
</button>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -15,19 +15,9 @@
Produits sélectionnés directement pour cette machine. Produits sélectionnés directement pour cette machine.
</p> </p>
</div> </div>
<div class="flex items-center gap-2"> <span class="badge badge-outline" v-if="products.length">
<button {{ products.length }} produit{{ products.length > 1 ? 's' : '' }}
v-if="isEditMode" </span>
type="button"
class="btn btn-sm md:btn-md btn-primary"
@click="$emit('add-product')"
>
Ajouter un produit
</button>
<span class="badge badge-outline" v-if="products.length">
{{ products.length }} produit{{ products.length > 1 ? 's' : '' }}
</span>
</div>
</div> </div>
<div v-if="products.length" class="space-y-3"> <div v-if="products.length" class="space-y-3">
@@ -117,6 +107,15 @@
<p v-else class="text-xs text-gray-500"> <p v-else class="text-xs text-gray-500">
Aucun produit n'a été associé directement à cette machine. Aucun produit n'a été associé directement à cette machine.
</p> </p>
<button
v-if="isEditMode"
type="button"
class="btn btn-sm md:btn-md btn-primary"
@click="$emit('add-product')"
>
Ajouter un produit
</button>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -69,6 +69,22 @@ const badgeClass = (type: ChangeType) => {
} }
const releases: Release[] = [ const releases: Release[] = [
{
version: 'v1.8.1',
date: '2026-03-05',
changes: [
{ type: 'feat', text: 'Composant DataTable générique avec tri, recherche, pagination et filtres server-side toutes les pages catalogue migrées vers ce composant partagé' },
{ type: 'feat', text: 'Messages d\'erreur humanisés : les erreurs backend sont traduites en messages compréhensibles pour l\'utilisateur final' },
{ type: 'feat', text: 'Modal d\'ajout d\'entités aux machines : ajout direct de composants, pièces et produits depuis la fiche machine' },
{ type: 'feat', text: 'Filtres SearchFilter ipartial sur les noms de types de modèles et commentaires côté API' },
{ type: 'feat', text: 'Suppression du système TypeMachine (squelettes machines) : les champs personnalisés sont désormais liés directement à chaque machine' },
{ type: 'feat', text: 'Simplification de la création de machines : plus besoin de sélectionner un squelette, ajout direct des entités' },
{ type: 'fix', text: 'Suppression catalogue pièces/composants : confirmation avec liste des éléments supprimés en cascade (documents, liaisons machine, champs personnalisés) au lieu de bloquer la suppression' },
{ type: 'fix', text: 'Affichage des catégories sur les pages d\'édition (produit, composant, pièce) : correction de « Catégorie inconnue » causée par un import obsolète dans ModelType' },
{ type: 'fix', text: 'Recherche insensible à la casse sur les commentaires et documents (partial → ipartial)' },
{ type: 'chore', text: 'Suppression des pages squelettes machines (/machine-skeleton, /type) et composants associés' },
],
},
{ {
version: 'v1.8.0', version: 'v1.8.0',
date: '2026-03-03', date: '2026-03-03',

View File

@@ -124,13 +124,11 @@ import { computed, onMounted } from 'vue'
import DataTable from '~/components/common/DataTable.vue' import DataTable from '~/components/common/DataTable.vue'
import { useComposants } from '~/composables/useComposants' import { useComposants } from '~/composables/useComposants'
import { useComponentTypes } from '~/composables/useComponentTypes' import { useComponentTypes } from '~/composables/useComponentTypes'
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'
const { canEdit } = usePermissions() const { canEdit } = usePermissions()
const { showError } = useToast()
const { composants, total, loadComposants, loading: loadingComposantsRef, deleteComposant } = useComposants() const { composants, total, loadComposants, loading: loadingComposantsRef, deleteComposant } = useComposants()
const { componentTypes, loadComponentTypes } = useComponentTypes() const { componentTypes, loadComponentTypes } = useComponentTypes()
const loadingComposants = computed(() => loadingComposantsRef.value) const loadingComposants = computed(() => loadingComposantsRef.value)
@@ -203,29 +201,27 @@ const resolveComponentType = (component: Record<string, any>) => {
return '—' return '—'
} }
const resolveDeleteGuard = (component: Record<string, any>) => { const resolveDeleteImpact = (component: Record<string, any>) => {
const blockingReasons: string[] = [] const impacts: string[] = []
const machineLinks = Array.isArray(component?.machineLinks) ? component.machineLinks.length : component?.machineLinksCount ?? 0 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 documents = Array.isArray(component?.documents) ? component.documents.length : component?.documentsCount ?? 0
const customFields = Array.isArray(component?.customFieldValues) ? component.customFieldValues.length : component?.customFieldValuesCount ?? 0 const customFields = Array.isArray(component?.customFieldValues) ? component.customFieldValues.length : component?.customFieldValuesCount ?? 0
if (machineLinks > 0) blockingReasons.push(`${machineLinks} liaison${machineLinks > 1 ? 's' : ''} machine`) if (machineLinks > 0) impacts.push(`${machineLinks} liaison${machineLinks > 1 ? 's' : ''} machine`)
if (documents > 0) blockingReasons.push(`${documents} document${documents > 1 ? 's' : ''}`) if (documents > 0) impacts.push(`${documents} document${documents > 1 ? 's' : ''}`)
return { blockingReasons, hasCustomFields: customFields > 0 } 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 { blockingReasons, hasCustomFields } = resolveDeleteGuard(component)
if (blockingReasons.length) {
showError(`Impossible de supprimer ce composant car il possède encore: ${blockingReasons.join(', ')}. Supprimez ou détachez ces éléments avant de réessayer.`)
return
}
const componentName = component?.name || 'ce composant' const componentName = component?.name || 'ce composant'
const confirmLines = [`Voulez-vous vraiment supprimer ${componentName} ?`] const impacts = resolveDeleteImpact(component)
if (hasCustomFields) { const lines = [`Voulez-vous vraiment supprimer « ${componentName} » ?`]
confirmLines.push('Les valeurs de champs personnalisés associées seront également supprimées.') if (impacts.length) {
lines.push(`Cela supprimera également :\n• ${impacts.join('\n• ')}`)
} }
lines.push('Cette action est irréversible.')
const { confirm } = useConfirm() const { confirm } = useConfirm()
const confirmed = await confirm({ message: confirmLines.join('\n\n') }) 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()

View File

@@ -147,13 +147,11 @@ import { computed, onMounted } from 'vue'
import DataTable from '~/components/common/DataTable.vue' import DataTable from '~/components/common/DataTable.vue'
import { usePieces } from '~/composables/usePieces' import { usePieces } from '~/composables/usePieces'
import { usePieceTypes } from '~/composables/usePieceTypes' import { usePieceTypes } from '~/composables/usePieceTypes'
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'
const { canEdit } = usePermissions() const { canEdit } = usePermissions()
const { showError } = useToast()
const { pieces, total, loadPieces, loading: loadingPiecesRef, deletePiece } = usePieces() const { pieces, total, loadPieces, loading: loadingPiecesRef, deletePiece } = usePieces()
const { pieceTypes, loadPieceTypes } = usePieceTypes() const { pieceTypes, loadPieceTypes } = usePieceTypes()
const loadingPieces = computed(() => loadingPiecesRef.value) const loadingPieces = computed(() => loadingPiecesRef.value)
@@ -280,29 +278,27 @@ 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 resolveDeleteGuard = (piece: Record<string, any>) => { const resolveDeleteImpact = (piece: Record<string, any>) => {
const blockingReasons: string[] = [] const impacts: string[] = []
const machineLinks = Array.isArray(piece?.machineLinks) ? piece.machineLinks.length : piece?.machineLinksCount ?? 0 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 documents = Array.isArray(piece?.documents) ? piece.documents.length : piece?.documentsCount ?? 0
const customFields = Array.isArray(piece?.customFieldValues) ? piece.customFieldValues.length : piece?.customFieldValuesCount ?? 0 const customFields = Array.isArray(piece?.customFieldValues) ? piece.customFieldValues.length : piece?.customFieldValuesCount ?? 0
if (machineLinks > 0) blockingReasons.push(`${machineLinks} liaison${machineLinks > 1 ? 's' : ''} machine`) if (machineLinks > 0) impacts.push(`${machineLinks} liaison${machineLinks > 1 ? 's' : ''} machine`)
if (documents > 0) blockingReasons.push(`${documents} document${documents > 1 ? 's' : ''}`) if (documents > 0) impacts.push(`${documents} document${documents > 1 ? 's' : ''}`)
return { blockingReasons, hasCustomFields: customFields > 0 } 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 { blockingReasons, hasCustomFields } = resolveDeleteGuard(piece)
if (blockingReasons.length) {
showError(`Impossible de supprimer cette pièce car elle possède encore: ${blockingReasons.join(', ')}. Supprimez ou détachez ces éléments avant de réessayer.`)
return
}
const pieceName = piece?.name || 'cette pièce' const pieceName = piece?.name || 'cette pièce'
const confirmLines = [`Voulez-vous vraiment supprimer ${pieceName} ?`] const impacts = resolveDeleteImpact(piece)
if (hasCustomFields) { const lines = [`Voulez-vous vraiment supprimer « ${pieceName} » ?`]
confirmLines.push('Les valeurs de champs personnalisés associées seront également supprimées.') if (impacts.length) {
lines.push(`Cela supprimera également :\n• ${impacts.join('\n• ')}`)
} }
lines.push('Cette action est irréversible.')
const { confirm } = useConfirm() const { confirm } = useConfirm()
const confirmed = await confirm({ message: confirmLines.join('\n\n') }) 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()

View File

@@ -296,14 +296,30 @@ 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 confirmed = await confirm({ const productName = product?.name || 'ce produit'
message: `Voulez-vous vraiment supprimer le produit "${product.name}" ?\n\nCette action est irréversible.`, const impacts = resolveDeleteImpact(product)
}) const lines = [`Voulez-vous vraiment supprimer « ${productName} » ?`]
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) {
toast.showSuccess(`Produit "${product.name}" supprimé`) toast.showSuccess(`Produit "${productName}" supprimé`)
} }
} }