feat(machines) : unicité du nom de machine par site
Auto Tag Develop / tag (push) Successful in 13s

Le nom d'une machine n'est plus unique globalement mais par site :
deux machines peuvent porter le même nom sur des sites différents,
mais le doublon reste interdit sur un même site.

- Machine : contrainte composite (name, siteId) + UniqueEntity (name, site)
- UniqueConstraintSubscriber : message explicite pour uniq_machine_name_site
- Migration : drop index global sur name + create unique index (name, siteid)
- Front : message d'erreur inline explicite à la création (page + modale)
- Tests : 4 scénarios (sites différents / même site / renommage / déplacement)
This commit is contained in:
Matthieu
2026-05-27 15:37:38 +02:00
parent 104942a52b
commit 0fc9daa974
8 changed files with 184 additions and 11 deletions
@@ -5,6 +5,19 @@
Ajouter une nouvelle machine
</h3>
<form @submit.prevent="handleSubmit">
<div v-if="errorMessage" class="alert alert-error mb-4" role="alert">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5 shrink-0"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
</svg>
<span>{{ errorMessage }}</span>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div class="form-control">
<label class="label">
@@ -78,6 +91,7 @@ const props = defineProps<{
sites: Array<{ id: string, name: string }>
disabled: boolean
preselectedSiteId?: string
errorMessage?: string | null
}>()
const emit = defineEmits<{
@@ -8,7 +8,6 @@
import { ref, reactive, onMounted } from 'vue'
import { useMachines } from '~/composables/useMachines'
import { useSites } from '~/composables/useSites'
import { useToast } from '~/composables/useToast'
import { humanizeError } from '~/shared/utils/errorMessages'
export function useMachineCreatePage() {
@@ -18,7 +17,6 @@ export function useMachineCreatePage() {
const { machines, loadMachines, createMachine, cloneMachine } = useMachines()
const { sites, loadSites } = useSites()
const toast = useToast()
// ---------------------------------------------------------------------------
// Local state
@@ -27,6 +25,9 @@ export function useMachineCreatePage() {
const submitting = ref(false)
const loading = ref(true)
/** Persistent error shown inline in the form (e.g. duplicate name on the same site). */
const createError = ref<string | null>(null)
const newMachine = reactive({
name: '',
siteId: '',
@@ -41,8 +42,10 @@ export function useMachineCreatePage() {
const finalizeMachineCreation = async () => {
if (submitting.value) return
createError.value = null
if (!newMachine.name?.trim()) {
toast.showError('Merci de renseigner un nom pour la machine')
createError.value = 'Merci de renseigner un nom pour la machine.'
return
}
@@ -80,10 +83,10 @@ export function useMachineCreatePage() {
await navigateTo('/machines')
}
} else if (result.error) {
toast.showError(`Impossible de créer la machine : ${humanizeError(result.error)}`)
createError.value = humanizeError(result.error)
}
} catch (error: any) {
toast.showError(`Impossible de créer la machine : ${humanizeError(error.message)}`)
createError.value = humanizeError(error.message)
} finally {
submitting.value = false
}
@@ -116,6 +119,7 @@ export function useMachineCreatePage() {
machines,
submitting,
loading,
createError,
// Actions
finalizeMachineCreation,
+18 -3
View File
@@ -116,7 +116,7 @@
<button class="btn btn-primary btn-sm" @click="showAddSiteModal = true">
Ajouter un site
</button>
<button class="btn btn-ghost btn-sm" @click="showAddMachineModal = true">
<button class="btn btn-ghost btn-sm" @click="openAddMachineModal">
Ajouter une machine
</button>
</div>
@@ -282,7 +282,8 @@
:sites="sites"
:disabled="!canEdit"
:preselected-site-id="preselectedSiteId"
@close="showAddMachineModal = false"
:error-message="addMachineError"
@close="closeAddMachineModal"
@create="handleCreateMachine"
/>
</main>
@@ -312,6 +313,7 @@ const { machines, loadMachines, createMachine, deleteMachine } = useMachines()
// Data
const showAddSiteModal = ref(false)
const showAddMachineModal = ref(false)
const addMachineError = ref(null)
const searchTerm = ref('')
const selectedSiteFilter = ref('')
const sortOrder = ref('name-asc')
@@ -449,11 +451,14 @@ const handleCreateSite = async (data) => {
}
const handleCreateMachine = async (data) => {
addMachineError.value = null
const result = await createMachine(data)
if (result.success) {
showAddMachineModal.value = false
await loadMachines()
} else if (result.error) {
addMachineError.value = humanizeError(result.error)
}
}
@@ -498,9 +503,19 @@ const confirmDeleteMachine = async (machine) => {
}
}
const openAddMachineModal = () => {
addMachineError.value = null
showAddMachineModal.value = true
}
const closeAddMachineModal = () => {
addMachineError.value = null
showAddMachineModal.value = false
}
const addMachineToSite = (site) => {
preselectedSiteId.value = site.id
showAddMachineModal.value = true
openAddMachineModal()
}
// Lifecycle
+13
View File
@@ -20,6 +20,19 @@
</div>
<form v-else class="space-y-6" @submit.prevent="c.finalizeMachineCreation">
<div v-if="c.createError" class="alert alert-error" role="alert">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5 shrink-0"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
</svg>
<span>{{ c.createError }}</span>
</div>
<div class="card bg-base-100 shadow-sm">
<div class="card-body space-y-6">
<!-- Basic fields -->