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:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
Reference in New Issue
Block a user