fix: Mise a jour ui simple

- supp champs emplacement
- augmenter espace en label et input
sup btn compléter champs perso
This commit is contained in:
Matthieu
2025-09-29 15:36:14 +02:00
parent 43b615ac3e
commit c489f093ed
4 changed files with 158 additions and 305 deletions

View File

@@ -60,7 +60,7 @@
</NuxtLink>
</li>
<li
class="dropdown dropdown-hover"
class="dropdown"
:class="{ 'dropdown-open': openDropdown === 'pieces-mobile' }"
@mouseenter="setDropdown('pieces-mobile')"
@mouseleave="scheduleDropdownClose('pieces-mobile')"
@@ -71,6 +71,9 @@
tabindex="0"
role="button"
class="rounded-md px-2 py-1 transition-colors cursor-pointer"
@click="toggleDropdown('pieces-mobile')"
@keydown.enter.prevent="toggleDropdown('pieces-mobile')"
@keydown.space.prevent="toggleDropdown('pieces-mobile')"
:class="
isActive('/pieces-catalog') || isActive('/piece-category')
? 'bg-primary text-primary-content font-semibold shadow-sm'
@@ -112,7 +115,7 @@
</ul>
</li>
<li
class="dropdown dropdown-hover"
class="dropdown"
:class="{ 'dropdown-open': openDropdown === 'component-mobile' }"
@mouseenter="setDropdown('component-mobile')"
@mouseleave="scheduleDropdownClose('component-mobile')"
@@ -123,6 +126,9 @@
tabindex="0"
role="button"
class="rounded-md px-2 py-1 transition-colors cursor-pointer"
@click="toggleDropdown('component-mobile')"
@keydown.enter.prevent="toggleDropdown('component-mobile')"
@keydown.space.prevent="toggleDropdown('component-mobile')"
:class="
isActive('/component-catalog') ||
isActive('/component-category')
@@ -165,7 +171,7 @@
</ul>
</li>
<li
class="dropdown dropdown-hover"
class="dropdown"
:class="{ 'dropdown-open': openDropdown === 'resources-mobile' }"
@mouseenter="setDropdown('resources-mobile')"
@mouseleave="scheduleDropdownClose('resources-mobile')"
@@ -176,6 +182,9 @@
tabindex="0"
role="button"
class="rounded-md px-2 py-1 transition-colors cursor-pointer"
@click="toggleDropdown('resources-mobile')"
@keydown.enter.prevent="toggleDropdown('resources-mobile')"
@keydown.space.prevent="toggleDropdown('resources-mobile')"
:class="
isActive('/sites') ||
isActive('/documents') ||
@@ -288,20 +297,23 @@
</NuxtLink>
</li>
<li
class="dropdown dropdown-hover"
class="dropdown"
:class="{ 'dropdown-open': openDropdown === 'pieces-desktop' }"
@mouseenter="setDropdown('pieces-desktop')"
@mouseleave="scheduleDropdownClose('pieces-desktop')"
@focusin="setDropdown('pieces-desktop')"
@focusout="scheduleDropdownClose('pieces-desktop')"
>
<div
tabindex="0"
role="button"
class="transition-colors px-3 py-2 rounded-md inline-flex items-center gap-1 cursor-pointer"
:class="
isActive('/pieces-catalog') || isActive('/piece-category')
? 'bg-primary text-primary-content font-semibold shadow-sm'
<div
tabindex="0"
role="button"
class="transition-colors px-3 py-2 rounded-md inline-flex items-center gap-1 cursor-pointer"
@click="toggleDropdown('pieces-desktop')"
@keydown.enter.prevent="toggleDropdown('pieces-desktop')"
@keydown.space.prevent="toggleDropdown('pieces-desktop')"
:class="
isActive('/pieces-catalog') || isActive('/piece-category')
? 'bg-primary text-primary-content font-semibold shadow-sm'
: 'text-base-content hover:bg-primary/10 hover:text-primary'
"
>
@@ -340,20 +352,23 @@
</ul>
</li>
<li
class="dropdown dropdown-hover"
class="dropdown"
:class="{ 'dropdown-open': openDropdown === 'component-desktop' }"
@mouseenter="setDropdown('component-desktop')"
@mouseleave="scheduleDropdownClose('component-desktop')"
@focusin="setDropdown('component-desktop')"
@focusout="scheduleDropdownClose('component-desktop')"
>
<div
tabindex="0"
role="button"
class="transition-colors px-3 py-2 rounded-md inline-flex items-center gap-1 cursor-pointer"
:class="
isActive('/component-category') ||
isActive('/component-catalog')
<div
tabindex="0"
role="button"
class="transition-colors px-3 py-2 rounded-md inline-flex items-center gap-1 cursor-pointer"
@click="toggleDropdown('component-desktop')"
@keydown.enter.prevent="toggleDropdown('component-desktop')"
@keydown.space.prevent="toggleDropdown('component-desktop')"
:class="
isActive('/component-category') ||
isActive('/component-catalog')
? 'bg-primary text-primary-content font-semibold shadow-sm'
: 'text-base-content hover:bg-primary/10 hover:text-primary'
"
@@ -393,20 +408,23 @@
</ul>
</li>
<li
class="dropdown dropdown-hover"
class="dropdown"
:class="{ 'dropdown-open': openDropdown === 'resources-desktop' }"
@mouseenter="setDropdown('resources-desktop')"
@mouseleave="scheduleDropdownClose('resources-desktop')"
@focusin="setDropdown('resources-desktop')"
@focusout="scheduleDropdownClose('resources-desktop')"
>
<div
tabindex="0"
role="button"
class="transition-colors px-3 py-2 rounded-md inline-flex items-center gap-1 cursor-pointer"
:class="
isActive('/sites') ||
isActive('/documents') ||
<div
tabindex="0"
role="button"
class="transition-colors px-3 py-2 rounded-md inline-flex items-center gap-1 cursor-pointer"
@click="toggleDropdown('resources-desktop')"
@keydown.enter.prevent="toggleDropdown('resources-desktop')"
@keydown.space.prevent="toggleDropdown('resources-desktop')"
:class="
isActive('/sites') ||
isActive('/documents') ||
isActive('/constructeurs')
? 'bg-primary text-primary-content font-semibold shadow-sm'
: 'text-base-content hover:bg-primary/10 hover:text-primary'
@@ -620,6 +638,22 @@ const scheduleDropdownClose = (name) => {
}, 200);
};
const closeDropdownNow = () => {
if (dropdownCloseTimer) {
clearTimeout(dropdownCloseTimer);
dropdownCloseTimer = null;
}
openDropdown.value = null;
};
const toggleDropdown = (name) => {
if (openDropdown.value === name) {
closeDropdownNow();
return;
}
setDropdown(name);
};
const activeProfileLabel = computed(() => {
if (!activeProfile.value) {
return "Profil inconnu";

View File

@@ -278,20 +278,73 @@
}
/* Espacement adaptatif */
.p-1 { padding: var(--spacing-xs); }
.p-2 { padding: var(--spacing-sm); }
.p-3 { padding: var(--spacing-md); }
.p-4 { padding: var(--spacing-lg); }
.p-5 { padding: var(--spacing-xl); }
.p-1 {
padding: var(--spacing-xs);
}
.p-2 {
padding: var(--spacing-sm);
}
.p-3 {
padding: var(--spacing-md);
}
.p-4 {
padding: var(--spacing-lg);
}
.p-5 {
padding: var(--spacing-xl);
}
.m-1 { margin: var(--spacing-xs); }
.m-2 { margin: var(--spacing-sm); }
.m-3 { margin: var(--spacing-md); }
.m-4 { margin: var(--spacing-lg); }
.m-5 { margin: var(--spacing-xl); }
.m-1 {
margin: var(--spacing-xs);
}
.m-2 {
margin: var(--spacing-sm);
}
.m-3 {
margin: var(--spacing-md);
}
.m-4 {
margin: var(--spacing-lg);
}
.m-5 {
margin: var(--spacing-xl);
}
.gap-1 { gap: var(--spacing-xs); }
.gap-2 { gap: var(--spacing-sm); }
.gap-3 { gap: var(--spacing-md); }
.gap-4 { gap: var(--spacing-lg); }
.gap-5 { gap: var(--spacing-xl); }
.gap-1 {
gap: var(--spacing-xs);
}
.gap-2 {
gap: var(--spacing-sm);
}
.gap-3 {
gap: var(--spacing-md);
}
.gap-4 {
gap: var(--spacing-lg);
}
.gap-5 {
gap: var(--spacing-xl);
}
@layer components {
.form-control .label {
@apply mb-2;
padding-bottom: 0;
margin-right: 15px;
}
.form-control .label + * {
margin-top: var(--spacing-xs);
}
}
@layer base {
label + input,
label + select,
label + textarea,
label + .input,
label + .select,
label + .textarea {
margin-top: var(--spacing-xs);
}
}

View File

@@ -15,7 +15,9 @@
<!-- Header with actions -->
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<h1 class="text-3xl font-bold">Détails de la machine</h1>
<h1 class="text-3xl font-bold">
{{ machineViewTitle }}
</h1>
<div class="flex items-center gap-2 print:hidden" data-print-hide>
<button
@click="toggleEditMode"
@@ -43,20 +45,6 @@
>
Modifier les éléments du squelette
</button>
<button
type="button"
class="btn btn-outline"
:class="{ 'btn-primary': hasMissingRequiredCustomFields }"
:disabled="completingCustomFields"
@click="handleCompleteCustomFieldsClick"
>
<span
v-if="completingCustomFields"
class="loading loading-spinner loading-sm mr-2"
aria-hidden="true"
/>
Compléter les champs personnalisés
</button>
<button
v-if="!isEditMode"
@click="openPrintModal"
@@ -98,7 +86,9 @@
<div class="card-body space-y-6">
<div class="flex flex-wrap items-start justify-between gap-3">
<div>
<h2 class="card-title">Modifier les éléments du squelette</h2>
<h2 class="card-title">
{{ skeletonEditorTitle }}
</h2>
<p class="text-sm text-gray-500">
Sélectionnez les composants et pièces à associer à cette machine selon les exigences du type.
</p>
@@ -874,7 +864,6 @@ if (!machineId) {
const {
updateMachine: updateMachineApi,
reconfigureSkeleton: reconfigureMachineSkeleton,
addMissingCustomFields: addMissingCustomFieldsApi,
} = useMachines()
const {
getComposantsByMachine,
@@ -911,7 +900,6 @@ const loading = ref(true)
const machine = ref(null)
const components = ref([])
const pieces = ref([])
const completingCustomFields = ref(false)
const printAreaRef = ref(null)
const { constructeurs, loadConstructeurs } = useConstructeurs()
@@ -926,9 +914,6 @@ const machineConstructeurDisplay = computed(() => {
return constructeurs.value.find(item => item.id === id) || machine.value?.constructeur || null
})
// Valeurs des champs personnalisés de la machine
const machineCustomFieldValues = reactive({})
const machineDocumentFiles = ref([])
const machineDocumentsUploading = ref(false)
const machineDocumentsLoaded = ref(false)
@@ -1284,7 +1269,6 @@ const applySkeletonReconfigurationResult = async (data) => {
}
await ensureModelsForExistingEntities()
await autoCompleteMissingCustomFieldsIfNeeded()
}
const saveSkeletonConfiguration = async () => {
@@ -1369,6 +1353,16 @@ const closeSaveComponentModelModal = () => {
const isEditMode = ref(false)
const debug = ref(false) // Ajout de debug pour afficher les infos de debug
const machineViewTitle = computed(() =>
isEditMode.value ? 'Modification de la machine' : 'Détails de la machine'
)
const skeletonEditorTitle = computed(() =>
skeletonEditor.open
? 'Modification des éléments du squelette'
: 'Éléments du squelette'
)
// Gestion du pliage global des composants
const componentsCollapsed = ref(true)
const collapseToggleToken = ref(0)
@@ -1423,26 +1417,6 @@ const flattenComponents = (list = []) => {
const flattenedComponents = computed(() => flattenComponents(components.value))
const hasMissingRequiredCustomFields = computed(() => {
if (!machine.value) {
return false
}
if (hasMissingRequiredCustomFieldsInMachine(machine.value)) {
return true
}
if (components.value.some(component => hasMissingRequiredCustomFieldsInComponent(component))) {
return true
}
if (pieces.value.some(piece => hasMissingRequiredCustomFieldsInPiece(piece))) {
return true
}
return false
})
const preloadModelsForTypeMachine = async (typeMachine) => {
if (!typeMachine) return
const componentTypeIds = new Set(
@@ -1846,134 +1820,6 @@ const transformComponentCustomFields = (componentsData) => {
});
};
function getCustomFieldIdentifier(field) {
if (!field) {
return null
}
return field.customFieldId ?? field.customField?.id ?? field.id ?? null
}
function normalizeCustomFieldEntries(entries = []) {
return entries
.map(entry => ({
id: getCustomFieldIdentifier(entry),
required: entry?.customField?.required ?? entry?.required ?? false,
value: entry?.value ?? null,
}))
.filter(entry => entry.id !== null)
}
function collectCustomFieldDefinitions(...sources) {
return sources.flatMap(source => (Array.isArray(source) ? source : []))
}
function isEmptyCustomFieldValue(value) {
if (value === null || value === undefined) {
return true
}
if (typeof value === 'string') {
return value.trim() === ''
}
return false
}
function hasMissingCustomFieldValues(values = []) {
return values.some(entry => entry.required && isEmptyCustomFieldValue(entry.value))
}
function hasMissingCustomFieldDefinitions(definitions = [], values = []) {
if (!definitions.length) {
return false
}
const valueIds = new Set(values.map(entry => entry.id))
return definitions.some(definition => {
if (!definition?.required) {
return false
}
const id = getCustomFieldIdentifier(definition)
return id !== null && !valueIds.has(id)
})
}
function hasMissingRequiredCustomFieldsInMachine(machineData) {
if (!machineData) {
return false
}
const values = normalizeCustomFieldEntries(machineData.customFieldValues || [])
const definitions = collectCustomFieldDefinitions(machineData.typeMachine?.customFields)
return (
hasMissingCustomFieldDefinitions(definitions, values)
|| hasMissingCustomFieldValues(values)
)
}
function hasMissingRequiredCustomFieldsInComponent(component) {
if (!component) {
return false
}
const sourceValues = component.customFields?.length
? component.customFields
: component.customFieldValues || []
const values = normalizeCustomFieldEntries(sourceValues)
const definitions = collectCustomFieldDefinitions(
component.typeComposant?.customFields,
component.composantModel?.customFields,
component.typeMachineComponentRequirement?.customFields,
)
if (
hasMissingCustomFieldDefinitions(definitions, values)
|| hasMissingCustomFieldValues(values)
) {
return true
}
if (
Array.isArray(component.pieces)
&& component.pieces.some(piece => hasMissingRequiredCustomFieldsInPiece(piece))
) {
return true
}
if (
Array.isArray(component.subComponents)
&& component.subComponents.some(sub => hasMissingRequiredCustomFieldsInComponent(sub))
) {
return true
}
return false
}
function hasMissingRequiredCustomFieldsInPiece(piece) {
if (!piece) {
return false
}
const sourceValues = piece.customFields?.length
? piece.customFields
: piece.customFieldValues || []
const values = normalizeCustomFieldEntries(sourceValues)
const definitions = collectCustomFieldDefinitions(
piece.typePiece?.customFields,
piece.pieceModel?.customFields,
piece.typeMachinePieceRequirement?.customFields,
)
return (
hasMissingCustomFieldDefinitions(definitions, values)
|| hasMissingCustomFieldValues(values)
)
}
function mergePieceLists(existing = [], updates = []) {
if (!existing.length) {
return updates
@@ -2036,91 +1882,6 @@ function mergeComponentTrees(existing = [], updates = []) {
return merged
}
async function applyCustomFieldsCompletionResult(payload) {
if (!payload) {
return
}
const updatedMachine = payload.machine || payload
if (machine.value && updatedMachine?.customFieldValues) {
machine.value = {
...machine.value,
customFieldValues: updatedMachine.customFieldValues,
}
}
const updatedComponentsRaw = payload.components ?? updatedMachine?.components ?? null
if (Array.isArray(updatedComponentsRaw)) {
const transformedComponents = transformComponentCustomFields(updatedComponentsRaw)
components.value = mergeComponentTrees(components.value, transformedComponents)
}
const updatedPiecesRaw = payload.pieces ?? updatedMachine?.pieces ?? null
if (Array.isArray(updatedPiecesRaw)) {
const transformedPieces = transformCustomFields(updatedPiecesRaw)
pieces.value = mergePieceLists(pieces.value, transformedPieces)
}
await ensureModelsForExistingEntities()
}
async function completeMissingCustomFields({ silent = false, checkMissing = true } = {}) {
if (!machine.value?.id) {
const error = 'Machine introuvable'
if (!silent) {
toast.showError(error)
}
return { success: false, error }
}
if (checkMissing && !hasMissingRequiredCustomFields.value) {
return { success: true, skipped: true }
}
if (completingCustomFields.value) {
if (!silent) {
toast.showInfo('Complétion des champs personnalisés déjà en cours')
}
return { success: false, error: 'Complétion déjà en cours' }
}
completingCustomFields.value = true
try {
const result = await addMissingCustomFieldsApi(machine.value.id, { showToast: false })
if (result.success) {
await applyCustomFieldsCompletionResult(result.data)
if (!silent) {
toast.showSuccess('Champs personnalisés complétés avec succès')
}
} else if (!silent) {
toast.showError(result.error || 'Impossible de compléter les champs personnalisés')
}
return result
} catch (error) {
console.error('Erreur lors de la complétion des champs personnalisés:', error)
if (!silent) {
toast.showError('Impossible de compléter les champs personnalisés')
}
return { success: false, error: error?.message || 'Erreur inconnue' }
} finally {
completingCustomFields.value = false
}
}
async function autoCompleteMissingCustomFieldsIfNeeded() {
await nextTick()
if (completingCustomFields.value) {
return
}
if (!machine.value?.id) {
return
}
if (!hasMissingRequiredCustomFields.value) {
return
}
await completeMissingCustomFields({ silent: true, checkMissing: true })
}
// Methods
const loadMachineData = async () => {
loading.value = true
@@ -2196,8 +1957,6 @@ const loadMachineData = async () => {
console.log('Aucune pièce trouvée dans la réponse de la machine')
}
await autoCompleteMissingCustomFieldsIfNeeded()
if (!machineDocumentsLoaded.value) {
await refreshMachineDocuments()
}
@@ -2454,10 +2213,6 @@ const updatePieceCustomField = async (fieldUpdate) => {
}
}
const handleCompleteCustomFieldsClick = async () => {
await completeMissingCustomFields({ silent: false, checkMissing: false })
}
const editComponent = (component) => {
// TODO: Implement edit modal
console.log('Edit component:', component)

View File

@@ -14,7 +14,7 @@
<div class="card-body">
<div class="flex items-center justify-between mb-6">
<h2 class="card-title text-2xl">
Modifier : {{ type.name }}
{{ typePageTitle }}
</h2>
<div class="flex gap-2">
<NuxtLink :to="`/type/edit/${type.id}`" class="btn btn-secondary">
@@ -115,7 +115,7 @@
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { ref, computed, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { useMachineTypesApi } from '~/composables/useMachineTypesApi'
import { useToast } from '~/composables/useToast'
@@ -128,6 +128,17 @@ const { showError } = useToast()
const type = ref(null)
const loading = ref(true)
const isEditView = computed(() => {
const editQuery = Array.isArray(route.query.edit) ? route.query.edit[0] : route.query.edit
const modeQuery = Array.isArray(route.query.mode) ? route.query.mode[0] : route.query.mode
return editQuery === 'true' || editQuery === '1' || modeQuery === 'edit'
})
const typePageTitle = computed(() => {
const currentName = type.value?.name || 'Type de squelette'
return isEditView.value ? `Modification : ${currentName}` : currentName
})
const componentRequirementCount = computed(() => type.value?.componentRequirements?.length || 0)
const pieceRequirementCount = computed(() => type.value?.pieceRequirements?.length || 0)