chore: update frontend configuration

This commit is contained in:
Matthieu
2025-09-26 11:29:47 +02:00
parent b7caa4f552
commit a78938a4d1
64 changed files with 5790 additions and 5129 deletions

View File

@@ -2,7 +2,7 @@
<div class="space-y-4">
<!-- Root Components -->
<div v-for="component in components" :key="component.id" class="border border-gray-200 rounded-lg p-4">
<ComponentItem
<ComponentItem
:component="component"
:is-edit-mode="isEditMode"
:collapse-all="collapseAll"
@@ -43,13 +43,13 @@ defineProps({
},
componentModelOptionsProvider: {
type: Function,
default: () => [],
default: () => []
},
pieceModelOptionsProvider: {
type: Function,
default: () => [],
},
default: () => []
}
})
defineEmits(['update', 'edit-piece', 'assign-model', 'assign-piece-model', 'custom-field-update', 'create-model-from-component'])
</script>
</script>

View File

@@ -13,15 +13,17 @@
type="button"
class="btn btn-ghost btn-sm btn-circle shrink-0 transition-transform"
:class="{ 'rotate-90': !isCollapsed }"
@click="toggleCollapse"
:aria-expanded="!isCollapsed"
:title="isCollapsed ? 'Déplier les détails du composant' : 'Replier les détails du composant'"
@click="toggleCollapse"
>
<IconLucideChevronRight class="w-5 h-5 transition-transform" aria-hidden="true" />
<span class="sr-only">{{ isCollapsed ? 'Déplier' : 'Replier' }} le composant</span>
</button>
<div class="flex-1">
<h3 class="text-lg font-semibold">{{ component.name }}</h3>
<h3 class="text-lg font-semibold">
{{ component.name }}
</h3>
<div class="flex flex-wrap gap-2 mt-2">
<span v-if="component.reference" class="badge badge-outline badge-sm">{{ component.reference }}</span>
<span v-if="component.constructeur" class="badge badge-outline badge-sm">{{ component.constructeur?.name }}</span>
@@ -55,8 +57,10 @@
type="text"
class="input input-bordered input-sm"
@blur="updateComponent"
/>
<div v-else class="input input-bordered input-sm bg-base-200">{{ component.name }}</div>
>
<div v-else class="input input-bordered input-sm bg-base-200">
{{ component.name }}
</div>
</div>
<div class="form-control">
<label class="label"><span class="label-text font-medium">Référence</span></label>
@@ -66,8 +70,10 @@
type="text"
class="input input-bordered input-sm"
@blur="updateComponent"
/>
<div v-else class="input input-bordered input-sm bg-base-200">{{ component.reference || 'Non définie' }}</div>
>
<div v-else class="input input-bordered input-sm bg-base-200">
{{ component.reference || 'Non définie' }}
</div>
</div>
<div class="form-control">
<label class="label"><span class="label-text font-medium">Prix</span></label>
@@ -78,8 +84,10 @@
step="0.01"
class="input input-bordered input-sm"
@blur="updateComponent"
/>
<div v-else class="input input-bordered input-sm bg-base-200">{{ component.prix ? `${component.prix}` : 'Non défini' }}</div>
>
<div v-else class="input input-bordered input-sm bg-base-200">
{{ component.prix ? `${component.prix}` : 'Non défini' }}
</div>
</div>
<div class="form-control">
<label class="label"><span class="label-text font-medium">Constructeur</span></label>
@@ -87,7 +95,7 @@
v-if="isEditMode"
class="w-full"
:model-value="component.constructeurId || component.constructeur?.id || null"
@update:modelValue="handleConstructeurChange"
@update:model-value="handleConstructeurChange"
/>
<div v-else class="input input-bordered input-sm bg-base-200">
<div class="flex flex-col">
@@ -117,7 +125,9 @@
class="select select-bordered select-sm"
@change="assignComponentModel($event.target.value)"
>
<option value="">Définir manuellement</option>
<option value="">
Définir manuellement
</option>
<option
v-for="model in componentModelOptionsList"
:key="model.id"
@@ -141,7 +151,9 @@
<!-- Custom Fields Display - Editable or Read-only -->
<div v-if="component.customFields && component.customFields.length > 0" class="mt-4 pt-4 border-t border-gray-200">
<h4 class="font-semibold text-sm text-gray-700 mb-3">Champs personnalisés</h4>
<h4 class="font-semibold text-sm text-gray-700 mb-3">
Champs personnalisés
</h4>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div v-for="field in component.customFields" :key="field.id" class="form-control">
<label class="label">
@@ -156,7 +168,7 @@
class="input input-bordered input-sm"
:required="field.required"
@blur="updateComponentCustomField(field)"
/>
>
<input
v-else-if="field.type === 'number'"
v-model="field.value"
@@ -164,7 +176,7 @@
class="input input-bordered input-sm"
:required="field.required"
@blur="updateComponentCustomField(field)"
/>
>
<select
v-else-if="field.type === 'select'"
v-model="field.value"
@@ -172,8 +184,12 @@
:required="field.required"
@change="updateComponentCustomField(field)"
>
<option value="">Sélectionner...</option>
<option v-for="option in field.options" :key="option" :value="option">{{ option }}</option>
<option value="">
Sélectionner...
</option>
<option v-for="option in field.options" :key="option" :value="option">
{{ option }}
</option>
</select>
<div v-else-if="field.type === 'boolean'" class="flex items-center gap-2">
<input
@@ -183,7 +199,7 @@
true-value="true"
false-value="false"
@change="updateComponentCustomField(field)"
/>
>
<span class="text-sm">{{ field.value === 'true' ? 'Oui' : 'Non' }}</span>
</div>
<input
@@ -193,10 +209,12 @@
class="input input-bordered input-sm"
:required="field.required"
@blur="updateComponentCustomField(field)"
/>
>
</template>
<template v-else>
<div class="input input-bordered input-sm bg-base-200">{{ field.value || 'Non défini' }}</div>
<div class="input input-bordered input-sm bg-base-200">
{{ field.value || 'Non défini' }}
</div>
</template>
</div>
</div>
@@ -204,13 +222,17 @@
<div class="mt-4 pt-4 border-t border-gray-200 space-y-3">
<div class="flex items-center justify-between">
<h4 class="font-semibold text-sm text-gray-700">Documents</h4>
<h4 class="font-semibold text-sm text-gray-700">
Documents
</h4>
<span v-if="isEditMode && selectedFiles.length" class="badge badge-outline">
{{ selectedFiles.length }} fichier{{ selectedFiles.length > 1 ? 's' : '' }} sélectionné{{ selectedFiles.length > 1 ? 's' : '' }}
</span>
</div>
<p v-if="loadingDocuments" class="text-xs text-gray-500">Chargement des documents...</p>
<p v-if="loadingDocuments" class="text-xs text-gray-500">
Chargement des documents...
</p>
<DocumentUpload
v-if="isEditMode"
@@ -235,7 +257,9 @@
/>
</span>
<div>
<div class="font-medium">{{ document.name }}</div>
<div class="font-medium">
{{ document.name }}
</div>
<div class="text-xs text-gray-500">
{{ document.mimeType || 'Inconnu' }} {{ formatSize(document.size) }}
</div>
@@ -266,12 +290,16 @@
</div>
</div>
</div>
<p v-else-if="!loadingDocuments" class="text-xs text-gray-500">Aucun document lié à ce composant.</p>
<p v-else-if="!loadingDocuments" class="text-xs text-gray-500">
Aucun document lié à ce composant.
</p>
</div>
<!-- Component Pieces -->
<div v-if="component.pieces && component.pieces.length > 0" class="space-y-2">
<h4 class="font-semibold text-gray-700">Pièces du composant</h4>
<h4 class="font-semibold text-gray-700">
Pièces du composant
</h4>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
<PieceItem
v-for="piece in component.pieces"
@@ -289,7 +317,9 @@
<!-- Sub Components -->
<div v-if="component.subComponents && component.subComponents.length > 0" class="space-y-3">
<h4 class="font-semibold text-gray-700">Sous-composants</h4>
<h4 class="font-semibold text-gray-700">
Sous-composants
</h4>
<div class="space-y-3 pl-4 border-l-2 border-gray-200">
<ComponentItem
v-for="subComponent in component.subComponents"
@@ -326,32 +356,32 @@ import IconLucideChevronRight from '~icons/lucide/chevron-right'
const props = defineProps({
component: {
type: Object,
required: true,
required: true
},
isEditMode: {
type: Boolean,
default: false,
default: false
},
collapseAll: {
type: Boolean,
default: true,
default: true
},
toggleToken: {
type: Number,
default: 0,
default: 0
},
componentModelOptions: {
type: Array,
default: () => [],
default: () => []
},
componentModelOptionsProvider: {
type: Function,
default: () => [],
default: () => []
},
pieceModelOptionsProvider: {
type: Function,
default: () => [],
},
default: () => []
}
})
const emit = defineEmits([
@@ -360,7 +390,7 @@ const emit = defineEmits([
'custom-field-update',
'assign-model',
'assign-piece-model',
'create-model-from-component',
'create-model-from-component'
])
const isCollapsed = ref(true)
@@ -369,7 +399,7 @@ const uploadingDocuments = ref(false)
const loadingDocuments = ref(false)
const documentsLoaded = ref(!!(props.component.documents && props.component.documents.length))
const componentDocuments = computed(() => props.component.documents || [])
const documentIcon = (doc) => getFileIcon({ name: doc.filename || doc.name, mime: doc.mimeType })
const documentIcon = doc => getFileIcon({ name: doc.filename || doc.name, mime: doc.mimeType })
const previewDocument = ref(null)
const previewVisible = ref(false)
@@ -395,14 +425,14 @@ watch(
ensureDocumentsLoaded()
}
},
{ immediate: true },
{ immediate: true }
)
watch(
() => props.component.documents,
(docs) => {
documentsLoaded.value = !!(docs && docs.length)
},
}
)
const toggleCollapse = () => {
@@ -444,7 +474,7 @@ const assignComponentModel = (value) => {
componentId: props.component.id,
composantModelId: value || null,
previousModelId,
previousModel,
previousModel
})
}
@@ -453,7 +483,7 @@ const emitAssignPieceModel = (payload) => {
}
const ensureDocumentsLoaded = async () => {
if (documentsLoaded.value || !props.component?.id) return
if (documentsLoaded.value || !props.component?.id) { return }
await refreshDocuments()
}
@@ -471,15 +501,15 @@ const refreshDocuments = async () => {
}
const handleFilesAdded = async (files) => {
if (!files.length || !props.component?.id) return
if (!files.length || !props.component?.id) { return }
uploadingDocuments.value = true
try {
const result = await uploadDocuments(
{
files,
context: { composantId: props.component.id },
context: { composantId: props.component.id }
},
{ updateStore: false },
{ updateStore: false }
)
if (result.success) {
@@ -494,15 +524,15 @@ const handleFilesAdded = async (files) => {
}
const removeDocument = async (documentId) => {
if (!documentId) return
if (!documentId) { return }
const result = await deleteDocument(documentId, { updateStore: false })
if (result.success) {
props.component.documents = (props.component.documents || []).filter((doc) => doc.id !== documentId)
props.component.documents = (props.component.documents || []).filter(doc => doc.id !== documentId)
}
}
const downloadDocument = (doc) => {
if (!doc?.path) return
if (!doc?.path) { return }
if (doc.path.startsWith('data:')) {
const link = document.createElement('a')
@@ -516,7 +546,7 @@ const downloadDocument = (doc) => {
}
const openPreview = (doc) => {
if (!canPreviewDocument(doc)) return
if (!canPreviewDocument(doc)) { return }
previewDocument.value = doc
previewVisible.value = true
}
@@ -527,8 +557,8 @@ const closePreview = () => {
}
const formatSize = (size) => {
if (size === undefined || size === null) return '—'
if (size === 0) return '0 B'
if (size === undefined || size === null) { return '—' }
if (size === 0) { return '0 B' }
const units = ['B', 'KB', 'MB', 'GB']
const index = Math.min(units.length - 1, Math.floor(Math.log(size) / Math.log(1024)))
const formatted = size / Math.pow(1024, index)

View File

@@ -70,52 +70,43 @@
>
<div class="flex items-start justify-between gap-2">
<div class="flex-1 space-y-3">
<div class="grid grid-cols-1 md:grid-cols-3 gap-2">
<input
v-model="piece.name"
type="text"
class="input input-bordered input-xs"
placeholder="Nom de la pièce"
/>
<input
v-model="piece.reference"
type="text"
class="input input-bordered input-xs"
placeholder="Référence"
/>
<input
v-model.number="piece.quantity"
type="number"
min="0"
step="1"
class="input input-bordered input-xs"
placeholder="Quantité"
/>
</div>
<div class="form-control">
<label class="label"><span class="label-text">Famille de pièce</span></label>
<div>
<input
:list="`component-piece-type-options-${index}`"
v-model="piece.typePieceLabel"
type="search"
autocomplete="off"
class="input input-bordered input-xs"
placeholder="Rechercher une famille"
@change="handlePieceTypeChange(piece)"
@blur="handlePieceTypeChange(piece)"
/>
<datalist :id="`component-piece-type-options-${index}`">
<option
v-for="type in availablePieceTypes"
:key="type.id"
:value="formatPieceTypeOption(type)"
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<div class="form-control">
<label class="label"><span class="label-text">Famille de pièce</span></label>
<div>
<input
:list="`component-piece-type-options-${index}`"
v-model="piece.typePieceLabel"
type="search"
autocomplete="off"
class="input input-bordered input-xs"
placeholder="Sélectionner une famille"
@change="handlePieceTypeChange(piece)"
@blur="handlePieceTypeChange(piece)"
/>
</datalist>
<datalist :id="`component-piece-type-options-${index}`">
<option
v-for="type in availablePieceTypes"
:key="type.id"
:value="formatPieceTypeOption(type)"
/>
</datalist>
</div>
<p class="mt-1 text-[11px] text-gray-500">
{{ piece.typePieceId ? `Sélection : ${getPieceTypeLabel(piece.typePieceId) || 'Inconnue'}` : 'Aucune famille sélectionnée' }}
</p>
</div>
<div class="form-control">
<label class="label"><span class="label-text">Quantité (optionnel)</span></label>
<input
v-model.number="piece.quantity"
type="number"
min="0"
step="1"
class="input input-bordered input-xs"
placeholder="Quantité"
/>
</div>
<p class="mt-1 text-[11px] text-gray-500">
{{ piece.typePieceId ? `Sélection : ${getPieceTypeLabel(piece.typePieceId) || 'Inconnue'}` : 'Aucune famille sélectionnée' }}
</p>
</div>
</div>
<button type="button" class="btn btn-error btn-xs btn-square" @click="removePiece(index)">
@@ -144,6 +135,7 @@
:node="subComponent"
:depth="0"
:piece-types="availablePieceTypes"
:component-types="availableComponentTypes"
@remove="removeSubComponent(index)"
/>
</div>
@@ -162,6 +154,7 @@ import {
normalizeStructureForSave,
} from '~/shared/modelUtils'
import { usePieceTypes } from '~/composables/usePieceTypes'
import { useComponentTypes } from '~/composables/useComponentTypes'
defineOptions({ name: 'ComponentModelStructureEditor' })
@@ -207,18 +200,25 @@ watch(
{ deep: true }
)
type PieceTypeOption = {
type ModelTypeOption = {
id: string
name: string
code?: string | null
}
const { pieceTypes, loadPieceTypes } = usePieceTypes()
const formatModelTypeOption = (type: ModelTypeOption | undefined | null) => {
if (!type) return ''
return type.code ? `${type.name} (${type.code})` : type.name
}
const availablePieceTypes = computed<PieceTypeOption[]>(() => pieceTypes.value ?? [])
const { pieceTypes, loadPieceTypes } = usePieceTypes()
const { componentTypes, loadComponentTypes } = useComponentTypes()
const availablePieceTypes = computed<ModelTypeOption[]>(() => pieceTypes.value ?? [])
const availableComponentTypes = computed<ModelTypeOption[]>(() => componentTypes.value ?? [])
const pieceTypeMap = computed(() => {
const map = new Map<string, PieceTypeOption>()
const map = new Map<string, ModelTypeOption>()
availablePieceTypes.value.forEach((type) => {
if (type && typeof type.id === 'string') {
map.set(type.id, type)
@@ -227,10 +227,18 @@ const pieceTypeMap = computed(() => {
return map
})
const formatPieceTypeOption = (type: PieceTypeOption | undefined | null) => {
if (!type) return ''
return type.code ? `${type.name} (${type.code})` : type.name
}
const componentTypeMap = computed(() => {
const map = new Map<string, ModelTypeOption>()
availableComponentTypes.value.forEach((type) => {
if (type && typeof type.id === 'string') {
map.set(type.id, type)
}
})
return map
})
const formatPieceTypeOption = (type: ModelTypeOption | undefined | null) => formatModelTypeOption(type)
const formatComponentTypeOption = (type: ModelTypeOption | undefined | null) => formatModelTypeOption(type)
const resolvePieceType = (input: string) => {
const normalized = input.trim().toLowerCase()
@@ -251,6 +259,25 @@ const resolvePieceType = (input: string) => {
)
}
const resolveComponentType = (input: string) => {
const normalized = input.trim().toLowerCase()
if (!normalized) {
return null
}
return (
availableComponentTypes.value.find((type) => {
const formatted = formatComponentTypeOption(type).toLowerCase()
const name = (type?.name ?? '').toLowerCase()
const code = (type?.code ?? '').toLowerCase()
return (
formatted === normalized
|| name === normalized
|| (!!code && code === normalized)
)
}) ?? null
)
}
const getPieceTypeLabel = (id?: string) => {
if (!id) return ''
const option = pieceTypeMap.value.get(id)
@@ -263,12 +290,21 @@ const updatePieceTypeLabel = (piece: any) => {
}
if (piece.typePieceId) {
const option = pieceTypeMap.value.get(piece.typePieceId)
piece.typePieceLabel = option ? formatPieceTypeOption(option) : piece.typePieceLabel || ''
if (option) {
piece.typePieceLabel = formatPieceTypeOption(option)
piece.name = option.name || formatPieceTypeOption(option)
} else if (!piece.typePieceLabel) {
piece.name = ''
}
} else if (piece.typePieceLabel) {
const match = resolvePieceType(piece.typePieceLabel)
if (match) {
piece.typePieceId = match.id
piece.typePieceLabel = formatPieceTypeOption(match)
piece.name = match.name || formatPieceTypeOption(match)
} else {
piece.typePieceLabel = ''
piece.name = ''
}
}
}
@@ -281,14 +317,18 @@ const handlePieceTypeChange = (piece: any) => {
if (!value) {
piece.typePieceId = ''
piece.typePieceLabel = ''
piece.name = ''
return
}
const match = resolvePieceType(value)
if (match) {
piece.typePieceId = match.id
piece.typePieceLabel = formatPieceTypeOption(match)
piece.name = match.name || formatPieceTypeOption(match)
} else {
piece.typePieceId = ''
piece.typePieceLabel = ''
piece.name = ''
}
}
@@ -304,37 +344,84 @@ const applyPieceLabels = (pieces?: any[]) => {
if (match) {
piece.typePieceId = match.id
piece.typePieceLabel = formatPieceTypeOption(match)
piece.name = match.name || formatPieceTypeOption(match)
} else {
piece.typePieceLabel = ''
piece.name = ''
}
} else if (!piece?.name) {
piece.name = ''
}
})
}
const applyComponentTypeLabel = (component: any) => {
if (!component) {
return
}
if (component.typeComposantId) {
const option = componentTypeMap.value.get(component.typeComposantId)
if (option) {
component.typeComposantLabel = formatComponentTypeOption(option)
component.name = option.name || formatComponentTypeOption(option)
} else if (!component.typeComposantLabel) {
component.name = ''
}
} else if (component.typeComposantLabel) {
const match = resolveComponentType(component.typeComposantLabel)
if (match) {
component.typeComposantId = match.id
component.typeComposantLabel = formatComponentTypeOption(match)
component.name = match.name || formatComponentTypeOption(match)
} else {
component.typeComposantLabel = ''
component.name = ''
}
}
}
const traverseSubComponents = (components?: any[]) => {
if (!Array.isArray(components)) {
return
}
components.forEach((component) => {
applyComponentTypeLabel(component)
applyPieceLabels(component?.pieces)
traverseSubComponents(component?.subComponents)
})
}
const syncAllPieceTypeLabels = () => {
const syncAllTypeLabels = () => {
applyPieceLabels(localStructure.pieces)
traverseSubComponents(localStructure.subComponents)
}
onMounted(async () => {
const loaders: Promise<any>[] = []
if (!availablePieceTypes.value.length) {
await loadPieceTypes()
loaders.push(loadPieceTypes())
}
syncAllPieceTypeLabels()
if (!availableComponentTypes.value.length) {
loaders.push(loadComponentTypes())
}
if (loaders.length) {
await Promise.all(loaders)
}
syncAllTypeLabels()
})
watch(
() => availablePieceTypes.value,
() => {
syncAllPieceTypeLabels()
syncAllTypeLabels()
},
{ deep: true }
)
watch(
() => availableComponentTypes.value,
() => {
syncAllTypeLabels()
},
{ deep: true }
)
@@ -365,7 +452,6 @@ const addPiece = () => {
ensureArray('pieces')
localStructure.pieces.push({
name: '',
reference: '',
quantity: undefined,
typePieceId: '',
typePieceLabel: '',
@@ -383,6 +469,8 @@ const addSubComponent = () => {
name: '',
description: '',
quantity: undefined,
typeComposantId: '',
typeComposantLabel: '',
customFields: [],
pieces: [],
subComponents: [],

View File

@@ -4,13 +4,13 @@
<div class="flex items-center gap-2">
<div class="relative flex-1">
<input
type="text"
v-model="searchTerm"
type="text"
class="input input-bordered w-full pr-10"
:placeholder="placeholder"
@focus="openDropdown = true; ensureOptionsLoaded()"
@input="onSearch"
/>
>
<button
type="button"
class="absolute right-2 top-1/2 -translate-y-1/2 btn btn-ghost btn-xs"
@@ -57,11 +57,13 @@
<dialog class="modal" :class="{ 'modal-open': openCreateModal }">
<div class="modal-box">
<h3 class="font-bold text-lg mb-4">Nouveau constructeur</h3>
<h3 class="font-bold text-lg mb-4">
Nouveau constructeur
</h3>
<form @submit.prevent="handleCreate">
<div class="form-control mb-3">
<label class="label"><span class="label-text">Nom</span></label>
<input v-model="createForm.name" type="text" class="input input-bordered" required />
<input v-model="createForm.name" type="text" class="input input-bordered" required>
</div>
<FieldEmail
v-model="createForm.email"
@@ -78,9 +80,11 @@
/>
<div class="modal-action">
<button type="button" class="btn" @click="closeCreateModal">Annuler</button>
<button type="button" class="btn" @click="closeCreateModal">
Annuler
</button>
<button type="submit" class="btn btn-primary" :disabled="creating">
<span v-if="creating" class="loading loading-spinner loading-xs mr-2"></span>
<span v-if="creating" class="loading loading-spinner loading-xs mr-2" />
Créer
</button>
</div>
@@ -100,16 +104,16 @@ import IconLucideChevronsUpDown from '~icons/lucide/chevrons-up-down'
const props = defineProps({
modelValue: {
type: String,
default: null,
default: null
},
label: {
type: String,
default: '',
default: ''
},
placeholder: {
type: String,
default: 'Sélectionner ou créer un constructeur...',
},
default: 'Sélectionner ou créer un constructeur...'
}
})
const emit = defineEmits(['update:modelValue'])
@@ -131,7 +135,7 @@ const applyOptions = (items = []) => {
if (selectedId && !limited.some(item => item.id === selectedId)) {
const selected = cloned.find(item => item.id === selectedId)
if (selected) {
if (limited.length >= 10) limited.pop()
if (limited.length >= 10) { limited.pop() }
limited.unshift(selected)
}
}
@@ -142,7 +146,7 @@ const applyOptions = (items = []) => {
const createForm = ref({
name: '',
email: '',
phone: '',
phone: ''
})
const selectedConstructeur = computed(() =>
@@ -171,8 +175,8 @@ const ensureOptionsLoaded = async (force = false) => {
applyOptions(constructeurs.value)
return
}
if (!force && searchTerm.value === lastSearchTerm && options.value.length) return
if (options.value.length && !force) return
if (!force && searchTerm.value === lastSearchTerm && options.value.length) { return }
if (options.value.length && !force) { return }
const result = await searchConstructeurs(searchTerm.value)
if (result.success) {
applyOptions(result.data || [])
@@ -189,7 +193,7 @@ const onSearch = () => {
lastSearchTerm = ''
return
}
if (searchTerm.value === lastSearchTerm) return
if (searchTerm.value === lastSearchTerm) { return }
const result = await searchConstructeurs(searchTerm.value)
if (result.success) {
applyOptions(result.data || [])
@@ -212,8 +216,8 @@ const closeCreateModal = () => {
const handleCreate = async () => {
creating.value = true
const payload = { ...createForm.value }
if (!payload.phone) delete payload.phone
if (!payload.email) delete payload.email
if (!payload.phone) { delete payload.phone }
if (!payload.email) { delete payload.email }
const result = await createConstructeur(payload)
creating.value = false
if (result.success) {

View File

@@ -1,9 +1,11 @@
<template>
<div v-if="customFields && customFields.length > 0" class="space-y-4">
<h4 class="font-semibold text-gray-700 mb-3">Champs personnalisés</h4>
<h4 class="font-semibold text-gray-700 mb-3">
Champs personnalisés
</h4>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div
v-for="field in customFields"
<div
v-for="field in customFields"
:key="field.id"
class="form-control"
>
@@ -11,66 +13,68 @@
<span class="label-text">{{ field.name }}</span>
<span v-if="field.required" class="label-text-alt text-error">*</span>
</label>
<!-- Champ de type TEXT -->
<input
<input
v-if="field.type === 'text'"
v-model="fieldValues[field.id]"
type="text"
class="input input-bordered input-sm"
:required="field.required"
@blur="updateCustomFieldValue(field.id)"
/>
>
<!-- Champ de type NUMBER -->
<input
<input
v-else-if="field.type === 'number'"
v-model="fieldValues[field.id]"
type="number"
class="input input-bordered input-sm"
:required="field.required"
@blur="updateCustomFieldValue(field.id)"
/>
>
<!-- Champ de type SELECT -->
<select
<select
v-else-if="field.type === 'select'"
v-model="fieldValues[field.id]"
class="select select-bordered select-sm"
:required="field.required"
@change="updateCustomFieldValue(field.id)"
>
<option value="">Sélectionner...</option>
<option
v-for="option in field.options"
:key="option"
<option value="">
Sélectionner...
</option>
<option
v-for="option in field.options"
:key="option"
:value="option"
>
{{ option }}
</option>
</select>
<!-- Champ de type BOOLEAN -->
<div v-else-if="field.type === 'boolean'" class="flex items-center gap-2">
<input
<input
v-model="fieldValues[field.id]"
type="checkbox"
class="checkbox checkbox-sm"
:checked="fieldValues[field.id] === 'true'"
@change="updateCustomFieldValue(field.id)"
/>
>
<span class="text-sm">{{ fieldValues[field.id] === 'true' ? 'Oui' : 'Non' }}</span>
</div>
<!-- Champ de type DATE -->
<input
<input
v-else-if="field.type === 'date'"
v-model="fieldValues[field.id]"
type="date"
class="input input-bordered input-sm"
:required="field.required"
@blur="updateCustomFieldValue(field.id)"
/>
>
</div>
</div>
</div>
@@ -91,7 +95,7 @@ const props = defineProps({
entityType: {
type: String,
required: true, // 'machine', 'composant', 'piece'
validator: (value) => ['machine', 'composant', 'piece'].includes(value)
validator: value => ['machine', 'composant', 'piece'].includes(value)
}
})
@@ -102,7 +106,7 @@ const fieldValues = reactive({})
// Initialiser les valeurs sans appliquer de valeur par défaut implicite
const initializeFieldValues = () => {
props.customFields.forEach(field => {
props.customFields.forEach((field) => {
if (!(field.id in fieldValues)) {
fieldValues[field.id] = field.value ?? ''
}
@@ -129,4 +133,4 @@ watch(() => props.customFields, () => {
onMounted(() => {
initializeFieldValues()
})
</script>
</script>

View File

@@ -1,23 +1,25 @@
<template>
<div class="modal" :class="{ 'modal-open': isOpen }">
<div class="modal-box">
<h3 class="font-bold text-lg mb-4">Paramètres d'affichage</h3>
<h3 class="font-bold text-lg mb-4">
Paramètres d'affichage
</h3>
<!-- Contrôle du zoom -->
<div class="form-control mb-4">
<label class="label">
<span class="label-text">Taille du texte</span>
<span class="label-text-alt">{{ zoomLevel }}%</span>
</label>
<input
type="range"
min="80"
max="150"
<input
type="range"
min="80"
max="150"
step="10"
:value="zoomLevel"
:value="zoomLevel"
class="range range-primary"
@input="updateZoom"
class="range range-primary"
/>
>
<div class="w-full flex justify-between text-xs px-2 mt-1">
<span>80%</span>
<span>100%</span>
@@ -31,24 +33,24 @@
<span class="label-text">Densité de l'interface</span>
</label>
<div class="flex gap-2">
<button
@click="setDensity('compact')"
<button
class="btn btn-sm"
:class="density === 'compact' ? 'btn-primary' : 'btn-outline'"
@click="setDensity('compact')"
>
Compacte
</button>
<button
@click="setDensity('comfortable')"
<button
class="btn btn-sm"
:class="density === 'comfortable' ? 'btn-primary' : 'btn-outline'"
@click="setDensity('comfortable')"
>
Confortable
</button>
<button
@click="setDensity('spacious')"
<button
class="btn btn-sm"
:class="density === 'spacious' ? 'btn-primary' : 'btn-outline'"
@click="setDensity('spacious')"
>
Espacée
</button>
@@ -61,17 +63,17 @@
<span class="label-text">Contraste</span>
</label>
<div class="flex gap-2">
<button
@click="setContrast('normal')"
<button
class="btn btn-sm"
:class="contrast === 'normal' ? 'btn-primary' : 'btn-outline'"
@click="setContrast('normal')"
>
Normal
</button>
<button
@click="setContrast('high')"
<button
class="btn btn-sm"
:class="contrast === 'high' ? 'btn-primary' : 'btn-outline'"
@click="setContrast('high')"
>
Élevé
</button>
@@ -80,9 +82,9 @@
<!-- Réinitialiser -->
<div class="form-control">
<button
@click="resetSettings"
<button
class="btn btn-outline btn-sm"
@click="resetSettings"
>
Réinitialiser les paramètres
</button>
@@ -90,7 +92,9 @@
<!-- Actions -->
<div class="modal-action">
<button @click="closeModal" class="btn btn-primary">Fermer</button>
<button class="btn btn-primary" @click="closeModal">
Fermer
</button>
</div>
</div>
</div>
@@ -118,11 +122,11 @@ onMounted(() => {
const savedZoom = localStorage.getItem('display-zoom')
const savedDensity = localStorage.getItem('display-density')
const savedContrast = localStorage.getItem('display-contrast')
if (savedZoom) zoomLevel.value = parseInt(savedZoom)
if (savedDensity) density.value = savedDensity
if (savedContrast) contrast.value = savedContrast
if (savedZoom) { zoomLevel.value = parseInt(savedZoom) }
if (savedDensity) { density.value = savedDensity }
if (savedContrast) { contrast.value = savedContrast }
applySettings()
})
@@ -146,10 +150,10 @@ const applySettings = () => {
localStorage.setItem('display-zoom', zoomLevel.value.toString())
localStorage.setItem('display-density', density.value)
localStorage.setItem('display-contrast', contrast.value)
// Appliquer les styles
const root = document.documentElement
// Zoom - exclure complètement le modal des paramètres
const modal = document.querySelector('.modal')
if (modal) {
@@ -157,27 +161,27 @@ const applySettings = () => {
modal.style.fontSize = '100%'
modal.style.transform = 'none'
modal.style.scale = '1'
// Appliquer aux enfants du modal
const modalElements = modal.querySelectorAll('*')
modalElements.forEach(element => {
modalElements.forEach((element) => {
element.style.fontSize = 'inherit'
element.style.transform = 'none'
element.style.scale = '1'
})
}
// Appliquer le zoom au reste de la page (sauf le modal)
root.style.fontSize = `${zoomLevel.value}%`
// Densité - utiliser les classes DaisyUI
root.classList.remove('density-compact', 'density-comfortable', 'density-spacious')
root.classList.add(`density-${density.value}`)
// Contraste - utiliser les classes DaisyUI
root.classList.remove('contrast-normal', 'contrast-high')
root.classList.add(`contrast-${contrast.value}`)
// Émettre les changements
emit('update-settings', {
zoom: zoomLevel.value,
@@ -200,4 +204,4 @@ const closeModal = () => {
<style scoped>
/* Les styles sont maintenant gérés par DaisyUI et le CSS global */
</style>
</style>

View File

@@ -8,7 +8,9 @@
<div class="w-full max-w-[1600px] h-full max-h-[94vh] bg-base-100 rounded-2xl shadow-2xl flex flex-col overflow-hidden">
<header class="flex items-start justify-between gap-4 p-6 border-b border-base-200">
<div class="min-w-0">
<h3 class="font-bold text-xl truncate">Prévisualisation</h3>
<h3 class="font-bold text-xl truncate">
Prévisualisation
</h3>
<p class="text-sm text-gray-500 truncate">
{{ document?.name || document?.filename }}<span v-if="documentDescription"> {{ documentDescription }}</span>
</p>
@@ -21,7 +23,7 @@
<section class="flex-1 bg-base-200/40 px-6 py-5 overflow-hidden">
<div class="h-full w-full rounded-xl border border-base-300 bg-base-100 flex items-center justify-center overflow-hidden">
<template v-if="previewType === 'image'">
<img :src="document?.path" alt="preview" class="max-h-full max-w-full object-contain" />
<img :src="document?.path" alt="preview" class="max-h-full max-w-full object-contain">
</template>
<template v-else-if="previewType === 'pdf'">
@@ -30,21 +32,21 @@
class="w-full h-full bg-white"
frameborder="0"
title="Aperçu PDF"
></iframe>
/>
</template>
<template v-else-if="previewType === 'audio'">
<audio :src="document?.path" controls class="w-full"></audio>
<audio :src="document?.path" controls class="w-full" />
</template>
<template v-else-if="previewType === 'video'">
<video :src="document?.path" controls class="w-full h-full bg-black"></video>
<video :src="document?.path" controls class="w-full h-full bg-black" />
</template>
<template v-else-if="previewType === 'text'">
<div class="w-full h-full overflow-auto">
<div v-if="textLoading" class="flex items-center justify-center py-10 text-sm text-gray-500">
<span class="loading loading-spinner loading-md mr-2"></span>
<span class="loading loading-spinner loading-md mr-2" />
Chargement du document...
</div>
<div v-else-if="textError" class="alert alert-error text-sm">
@@ -65,7 +67,9 @@
</section>
<footer class="border-t border-base-200 px-6 py-4 flex flex-wrap gap-2 justify-end bg-base-100">
<button type="button" class="btn" @click="close">Fermer</button>
<button type="button" class="btn" @click="close">
Fermer
</button>
<button type="button" class="btn btn-primary" @click="download">
Télécharger
</button>
@@ -82,12 +86,12 @@ import { getPreviewType, describeDocument } from '~/utils/documentPreview'
const props = defineProps({
document: {
type: Object,
default: null,
default: null
},
visible: {
type: Boolean,
default: false,
},
default: false
}
})
const emit = defineEmits(['close'])
@@ -106,8 +110,8 @@ watch(
textError.value = ''
textLoading.value = false
if (!doc) return
if (getPreviewType(doc) !== 'text') return
if (!doc) { return }
if (getPreviewType(doc) !== 'text') { return }
try {
textLoading.value = true
@@ -142,7 +146,7 @@ const close = () => {
}
const download = () => {
if (!props.document?.path) return
if (!props.document?.path) { return }
const link = document.createElement('a')
link.href = props.document.path
link.download = props.document.filename || props.document.name || 'document'

View File

@@ -10,8 +10,12 @@
<IconLucideCloudUpload class="w-10 h-10 text-primary" aria-hidden="true" />
<div>
<h3 class="font-semibold">{{ title }}</h3>
<p class="text-sm text-gray-500">{{ subtitle }}</p>
<h3 class="font-semibold">
{{ title }}
</h3>
<p class="text-sm text-gray-500">
{{ subtitle }}
</p>
</div>
<div class="flex flex-wrap justify-center gap-2">
@@ -28,7 +32,7 @@
:accept="accept"
:multiple="multiple"
@change="onFileChange"
/>
>
<ul v-if="selectedFiles.length" class="mt-4 w-full space-y-2 text-left">
<li v-for="file in selectedFiles" :key="file.name" class="flex items-center justify-between text-sm">
@@ -62,28 +66,28 @@ import IconLucideCloudUpload from '~icons/lucide/cloud-upload'
const props = defineProps({
title: {
type: String,
default: 'Ajouter des documents',
default: 'Ajouter des documents'
},
subtitle: {
type: String,
default: 'Formats acceptés : PDF, images, textes…',
default: 'Formats acceptés : PDF, images, textes…'
},
accept: {
type: String,
default: '',
default: ''
},
multiple: {
type: Boolean,
default: true,
default: true
},
modelValue: {
type: Array,
default: () => [],
default: () => []
},
maxFileSizeMb: {
type: Number,
default: 200,
},
default: 200
}
})
const emit = defineEmits(['update:modelValue', 'files-added'])
@@ -166,7 +170,7 @@ const removeFile = (fileToRemove) => {
}
const formatSize = (size) => {
if (!size) return '0 B'
if (!size) { return '0 B' }
const units = ['B', 'KB', 'MB', 'GB']
const index = Math.floor(Math.log(size) / Math.log(1024))
const formatted = size / Math.pow(1024, index)

View File

@@ -1,8 +1,10 @@
<template>
<dialog class="modal" :class="{ 'modal-open': open }">
<div class="modal-box max-w-3xl">
<form method="dialog" class="modal-close" @submit.prevent></form>
<h3 class="font-bold text-lg mb-2">Préparer l'impression</h3>
<form method="dialog" class="modal-close" @submit.prevent />
<h3 class="font-bold text-lg mb-2">
Préparer l'impression
</h3>
<p class="text-sm text-base-content/70 mb-4">
Choisissez les sections à inclure avant de lancer l'impression.
</p>
@@ -23,10 +25,10 @@
</h4>
<label class="flex items-start gap-3">
<input
v-model="selection.machine.info"
type="checkbox"
class="checkbox checkbox-primary mt-1"
v-model="selection.machine.info"
/>
>
<div>
<p class="font-medium">Informations générales</p>
<p class="text-xs text-base-content/60">
@@ -36,10 +38,10 @@
</label>
<label class="flex items-start gap-3">
<input
v-model="selection.machine.customFields"
type="checkbox"
class="checkbox checkbox-primary mt-1"
v-model="selection.machine.customFields"
/>
>
<div>
<p class="font-medium">Champs personnalisés</p>
<p class="text-xs text-base-content/60">
@@ -49,10 +51,10 @@
</label>
<label class="flex items-start gap-3">
<input
v-model="selection.machine.documents"
type="checkbox"
class="checkbox checkbox-primary mt-1"
v-model="selection.machine.documents"
/>
>
<div>
<p class="font-medium">Documents</p>
<p class="text-xs text-base-content/60">
@@ -62,7 +64,7 @@
</label>
</section>
<section class="bg-base-200/30 rounded-xl p-4 space-y-3" v-if="hasComponents">
<section v-if="hasComponents" class="bg-base-200/30 rounded-xl p-4 space-y-3">
<h4 class="font-semibold text-sm uppercase tracking-wide text-base-content/70">
Composants & pièces
</h4>
@@ -76,7 +78,7 @@
</div>
</section>
<section class="bg-base-200/30 rounded-xl p-4 space-y-3" v-if="hasPieces">
<section v-if="hasPieces" class="bg-base-200/30 rounded-xl p-4 space-y-3">
<h4 class="font-semibold text-sm uppercase tracking-wide text-base-content/70">
Pièces indépendantes
</h4>
@@ -87,10 +89,10 @@
class="flex items-start gap-3"
>
<input
v-model="selection.pieces[piece.id]"
type="checkbox"
class="checkbox checkbox-secondary mt-1"
v-model="selection.pieces[piece.id]"
/>
>
<div>
<p class="font-medium">{{ piece.name }}</p>
<p class="text-xs text-base-content/60">
@@ -122,7 +124,7 @@ const props = defineProps({
open: { type: Boolean, default: false },
selection: { type: Object, required: true },
components: { type: Array, default: () => [] },
pieces: { type: Array, default: () => [] },
pieces: { type: Array, default: () => [] }
})
const emit = defineEmits(['close', 'confirm', 'select-all', 'deselect-all'])

View File

@@ -2,10 +2,10 @@
<div class="rounded-lg border border-base-300 bg-base-100/80 p-3 space-y-3">
<label class="flex items-start gap-3">
<input
v-model="selection.components[component.id]"
type="checkbox"
class="checkbox checkbox-primary mt-1"
v-model="selection.components[component.id]"
/>
>
<div class="flex-1">
<p class="font-medium">{{ component.name }}</p>
<p v-if="component.reference" class="text-xs text-base-content/60">
@@ -24,10 +24,10 @@
class="flex items-start gap-3"
>
<input
v-model="selection.pieces[piece.id]"
type="checkbox"
class="checkbox checkbox-secondary mt-1"
v-model="selection.pieces[piece.id]"
/>
>
<div>
<p class="font-medium">{{ piece.name }}</p>
<p class="text-xs text-base-content/60">
@@ -55,7 +55,7 @@ defineOptions({ name: 'MachinePrintSelectionNode' })
const props = defineProps({
component: { type: Object, required: true },
selection: { type: Object, required: true },
selection: { type: Object, required: true }
})
const childComponents = computed(() => props.component.subComponents || [])

View File

@@ -1,16 +1,18 @@
<template>
<div class="space-y-4">
<div class="flex flex-wrap items-center gap-2 text-sm text-gray-600">
<span class="badge badge-outline badge-sm" v-if="stats.customFields">{{ stats.customFields }} champ(s)</span>
<span class="badge badge-outline badge-sm" v-if="stats.pieces">{{ stats.pieces }} pièce(s)</span>
<span class="badge badge-outline badge-sm" v-if="stats.subComponents">{{ stats.subComponents }} sous-composant(s)</span>
<span v-if="stats.customFields" class="badge badge-outline badge-sm">{{ stats.customFields }} champ(s)</span>
<span v-if="stats.pieces" class="badge badge-outline badge-sm">{{ stats.pieces }} pièce(s)</span>
<span v-if="stats.subComponents" class="badge badge-outline badge-sm">{{ stats.subComponents }} sous-composant(s)</span>
<span v-if="!stats.customFields && !stats.pieces && !stats.subComponents" class="text-xs text-gray-500">
Structure vide
</span>
</div>
<details class="collapse collapse-arrow bg-base-200">
<summary class="collapse-title text-sm font-medium">Voir la structure JSON</summary>
<summary class="collapse-title text-sm font-medium">
Voir la structure JSON
</summary>
<div class="collapse-content">
<pre class="mockup-code whitespace-pre-wrap text-xs bg-base-300 p-4 rounded">
<code>{{ formatted }}</code>
@@ -27,8 +29,8 @@ import { computeStructureStats } from '~/shared/modelUtils'
const props = defineProps({
structure: {
type: Object,
default: () => ({}),
},
default: () => ({})
}
})
const stats = computed(() => computeStructureStats(props.structure))

View File

@@ -5,7 +5,9 @@
<component :is="headingTag" v-if="title" class="text-4xl font-bold">
{{ title }}
</component>
<p v-if="subtitle" class="text-sm opacity-90">{{ subtitle }}</p>
<p v-if="subtitle" class="text-sm opacity-90">
{{ subtitle }}
</p>
<slot />
</div>
</div>
@@ -18,41 +20,41 @@ import { computed } from 'vue'
const props = defineProps({
title: {
type: String,
default: '',
default: ''
},
subtitle: {
type: String,
default: '',
default: ''
},
gradientFrom: {
type: String,
default: 'from-primary',
default: 'from-primary'
},
gradientTo: {
type: String,
default: 'to-secondary',
default: 'to-secondary'
},
minHeight: {
type: String,
default: 'min-h-[25vh]',
default: 'min-h-[25vh]'
},
maxWidth: {
type: String,
default: 'max-w-xl',
default: 'max-w-xl'
},
rounded: {
type: Boolean,
default: false,
default: false
},
alignment: {
type: String,
default: 'center',
validator: (value) => ['center', 'start', 'end'].includes(value),
validator: value => ['center', 'start', 'end'].includes(value)
},
headingTag: {
type: String,
default: 'h1',
},
default: 'h1'
}
})
const sectionClasses = computed(() => {

View File

@@ -12,14 +12,14 @@
class="w-4 h-4 text-purple-500"
aria-hidden="true"
/>
<input
<input
v-if="isEditMode"
:id="`piece-name-${piece.id}`"
v-model="pieceData.name"
type="text"
v-model="pieceData.name"
type="text"
class="font-semibold text-lg input input-sm input-bordered"
@blur="updatePiece"
/>
>
<div v-else class="font-semibold text-lg input input-sm input-bordered bg-base-200">
{{ pieceData.name }}
</div>
@@ -45,19 +45,19 @@
<div class="space-y-2 text-sm">
<div>
<span class="font-medium">Référence:</span>
<input
<span class="font-medium">Référence:</span>
<input
v-if="isEditMode"
:id="`piece-reference-${piece.id}`"
v-model="pieceData.reference"
type="text"
v-model="pieceData.reference"
type="text"
class="input input-sm input-bordered ml-2"
@blur="updatePiece"
/>
>
<span v-else class="ml-2">{{ pieceData.reference || 'Non définie' }}</span>
</div>
<div>
<span class="font-medium">Constructeur:</span>
<span class="font-medium">Constructeur:</span>
<span v-if="!isEditMode" class="ml-2">
<span class="font-medium">{{ piece.constructeur?.name || 'Non défini' }}</span>
<span v-if="piece.constructeur" class="block text-xs text-gray-500">
@@ -68,20 +68,20 @@
v-else
class="w-full"
:model-value="piece.constructeurId || piece.constructeur?.id || null"
@update:modelValue="handleConstructeurChange"
@update:model-value="handleConstructeurChange"
/>
</div>
<div>
<span class="font-medium">Prix:</span>
<input
<span class="font-medium">Prix:</span>
<input
v-if="isEditMode"
:id="`piece-prix-${piece.id}`"
v-model="pieceData.prix"
type="number"
v-model="pieceData.prix"
type="number"
step="0.01"
class="input input-sm input-bordered ml-2"
@blur="updatePiece"
/>
>
<span v-else class="ml-2">{{ pieceData.prix ? `${pieceData.prix}` : 'Non défini' }}</span>
</div>
</div>
@@ -90,7 +90,7 @@
v-if="isEditMode && piece.typeMachinePieceRequirement"
class="mt-3"
>
<label class="label">
<label class="label">
<span class="label-text text-sm font-medium">Modèle de pièce</span>
<span class="label-text-alt text-xs">
{{ piece.typeMachinePieceRequirement.label || piece.typeMachinePieceRequirement.typePiece?.name || 'Groupe' }}
@@ -101,7 +101,9 @@
class="select select-bordered select-sm w-full"
@change="assignPieceModel($event.target.value)"
>
<option value="">Définir manuellement</option>
<option value="">
Définir manuellement
</option>
<option
v-for="model in pieceModelOptions"
:key="model.id"
@@ -114,10 +116,12 @@
<!-- Champs personnalisés de la pièce -->
<div v-if="piece.customFieldValues && piece.customFieldValues.length > 0" class="mt-4 pt-4 border-t border-gray-200">
<h5 class="text-sm font-medium text-gray-700 mb-3">Champs personnalisés</h5>
<h5 class="text-sm font-medium text-gray-700 mb-3">
Champs personnalisés
</h5>
<div class="space-y-3">
<div
v-for="fieldValue in piece.customFieldValues"
<div
v-for="fieldValue in piece.customFieldValues"
:key="fieldValue.id"
class="form-control"
>
@@ -125,75 +129,77 @@
<span class="label-text text-sm">{{ fieldValue.customField.name }}</span>
<span v-if="fieldValue.customField.required" class="label-text-alt text-error">*</span>
</label>
<!-- Mode édition -->
<template v-if="isEditMode">
<!-- Champ de type TEXT -->
<input
<input
v-if="fieldValue.customField.type === 'text'"
:value="fieldValue.value"
@input="setCustomFieldValue(fieldValue.id, $event.target.value)"
type="text"
class="input input-bordered input-sm"
:required="fieldValue.customField.required"
@input="setCustomFieldValue(fieldValue.id, $event.target.value)"
@blur="updateCustomFieldValue(fieldValue.id)"
/>
>
<!-- Champ de type NUMBER -->
<input
<input
v-else-if="fieldValue.customField.type === 'number'"
:value="fieldValue.value"
@input="setCustomFieldValue(fieldValue.id, $event.target.value)"
type="number"
class="input input-bordered input-sm"
:required="fieldValue.customField.required"
@blur="updateCustomFieldValue(fieldValue.id)"
/>
<!-- Champ de type SELECT -->
<select
v-else-if="fieldValue.customField.type === 'select'"
:value="fieldValue.value"
@change="setCustomFieldValue(fieldValue.id, $event.target.value)"
class="select select-bordered select-sm"
:required="fieldValue.customField.required"
@input="setCustomFieldValue(fieldValue.id, $event.target.value)"
@blur="updateCustomFieldValue(fieldValue.id)"
>
<option value="">Sélectionner...</option>
<option
v-for="option in fieldValue.customField.options"
:key="option"
<!-- Champ de type SELECT -->
<select
v-else-if="fieldValue.customField.type === 'select'"
:value="fieldValue.value"
class="select select-bordered select-sm"
:required="fieldValue.customField.required"
@change="setCustomFieldValue(fieldValue.id, $event.target.value)"
@blur="updateCustomFieldValue(fieldValue.id)"
>
<option value="">
Sélectionner...
</option>
<option
v-for="option in fieldValue.customField.options"
:key="option"
:value="option"
>
{{ option }}
</option>
</select>
<!-- Champ de type BOOLEAN -->
<div v-else-if="fieldValue.customField.type === 'boolean'" class="flex items-center gap-2">
<input
<input
:value="fieldValue.value"
@change="setCustomFieldValue(fieldValue.id, $event.target.checked ? 'true' : 'false')"
type="checkbox"
class="checkbox checkbox-sm"
:checked="fieldValue.value === 'true'"
@change="setCustomFieldValue(fieldValue.id, $event.target.checked ? 'true' : 'false')"
@blur="updateCustomFieldValue(fieldValue.id)"
/>
>
<span class="text-sm">{{ fieldValue.value === 'true' ? 'Oui' : 'Non' }}</span>
</div>
<!-- Champ de type DATE -->
<input
<input
v-else-if="fieldValue.customField.type === 'date'"
:value="fieldValue.value"
@input="setCustomFieldValue(fieldValue.id, $event.target.value)"
type="date"
class="input input-bordered input-sm"
:required="fieldValue.customField.required"
@input="setCustomFieldValue(fieldValue.id, $event.target.value)"
@blur="updateCustomFieldValue(fieldValue.id)"
/>
>
</template>
<!-- Mode lecture seule -->
<template v-else>
<div class="input input-bordered input-sm bg-base-200">
@@ -206,13 +212,17 @@
<div class="mt-4 pt-4 border-t border-gray-200 space-y-3">
<div class="flex items-center justify-between">
<h5 class="text-sm font-medium text-gray-700">Documents</h5>
<h5 class="text-sm font-medium text-gray-700">
Documents
</h5>
<span v-if="isEditMode && selectedFiles.length" class="badge badge-outline">
{{ selectedFiles.length }} fichier{{ selectedFiles.length > 1 ? 's' : '' }} sélectionné{{ selectedFiles.length > 1 ? 's' : '' }}
</span>
</div>
<p v-if="loadingDocuments" class="text-xs text-gray-500">Chargement des documents...</p>
<p v-if="loadingDocuments" class="text-xs text-gray-500">
Chargement des documents...
</p>
<DocumentUpload
v-if="isEditMode"
@@ -237,7 +247,9 @@
/>
</span>
<div>
<div class="font-medium">{{ document.name }}</div>
<div class="font-medium">
{{ document.name }}
</div>
<div class="text-xs text-gray-500">
{{ document.mimeType || 'Inconnu' }} {{ formatSize(document.size) }}
</div>
@@ -268,13 +280,16 @@
</div>
</div>
</div>
<p v-else-if="!loadingDocuments" class="text-xs text-gray-500">Aucun document lié à cette pièce.</p>
<p v-else-if="!loadingDocuments" class="text-xs text-gray-500">
Aucun document lié à cette pièce.
</p>
</div>
</div>
</template>
<script setup>
import { reactive, onMounted, watch, ref, computed } from 'vue'
import ConstructeurSelect from './ConstructeurSelect.vue'
import { useCustomFields } from '~/composables/useCustomFields'
import { useToast } from '~/composables/useToast'
import { useDocuments } from '~/composables/useDocuments'
@@ -282,22 +297,21 @@ import { getFileIcon } from '~/utils/fileIcons'
import { canPreviewDocument } from '~/utils/documentPreview'
import DocumentUpload from '~/components/DocumentUpload.vue'
import DocumentPreviewModal from '~/components/DocumentPreviewModal.vue'
import ConstructeurSelect from './ConstructeurSelect.vue'
import IconLucidePackage from '~icons/lucide/package'
const props = defineProps({
piece: {
type: Object,
required: true,
required: true
},
isEditMode: {
type: Boolean,
default: false,
default: false
},
pieceModelOptions: {
type: Array,
default: () => [],
},
default: () => []
}
})
const emit = defineEmits(['update', 'edit', 'custom-field-update', 'assign-model'])
@@ -314,7 +328,7 @@ const uploadingDocuments = ref(false)
const loadingDocuments = ref(false)
const documentsLoaded = ref(!!(props.piece.documents && props.piece.documents.length))
const pieceDocuments = computed(() => props.piece.documents || [])
const documentIcon = (doc) => getFileIcon({ name: doc.filename || doc.name, mime: doc.mimeType })
const documentIcon = doc => getFileIcon({ name: doc.filename || doc.name, mime: doc.mimeType })
const previewDocument = ref(null)
const previewVisible = ref(false)
const selectedPieceModelId = computed(() => props.piece.pieceModelId || props.piece.pieceModel?.id || '')
@@ -328,7 +342,7 @@ const handleConstructeurChange = (value) => {
const { uploadDocuments, deleteDocument, loadDocumentsByPiece } = useDocuments()
const refreshDocuments = async () => {
if (!props.piece?.id) return
if (!props.piece?.id) { return }
loadingDocuments.value = true
try {
const result = await loadDocumentsByPiece(props.piece.id, { updateStore: false })
@@ -342,7 +356,7 @@ const refreshDocuments = async () => {
}
const handleFilesAdded = async (files) => {
if (!files.length || !props.piece?.id) return
if (!files.length || !props.piece?.id) { return }
uploadingDocuments.value = true
try {
const result = await uploadDocuments(
@@ -365,7 +379,7 @@ const handleFilesAdded = async (files) => {
}
const removeDocument = async (documentId) => {
if (!documentId) return
if (!documentId) { return }
const result = await deleteDocument(documentId, { updateStore: false })
if (result.success) {
props.piece.documents = (props.piece.documents || []).filter(doc => doc.id !== documentId)
@@ -373,7 +387,7 @@ const removeDocument = async (documentId) => {
}
const downloadDocument = (doc) => {
if (!doc?.path) return
if (!doc?.path) { return }
if (doc.path.startsWith('data:')) {
const link = document.createElement('a')
@@ -387,7 +401,7 @@ const downloadDocument = (doc) => {
}
const openPreview = (doc) => {
if (!canPreviewDocument(doc)) return
if (!canPreviewDocument(doc)) { return }
previewDocument.value = doc
previewVisible.value = true
}
@@ -398,8 +412,8 @@ const closePreview = () => {
}
const formatSize = (size) => {
if (size === undefined || size === null) return '—'
if (size === 0) return '0 B'
if (size === undefined || size === null) { return '—' }
if (size === 0) { return '0 B' }
const units = ['B', 'KB', 'MB', 'GB']
const index = Math.min(units.length - 1, Math.floor(Math.log(size) / Math.log(1024)))
const formatted = size / Math.pow(1024, index)
@@ -413,7 +427,6 @@ watch(
}
)
// Méthodes pour gérer les champs personnalisés
const setCustomFieldValue = (fieldValueId, value) => {
const fieldValue = props.piece.customFieldValues?.find(fv => fv.id === fieldValueId)
@@ -428,7 +441,7 @@ const updatePiece = () => {
...props.piece,
...pieceData,
prix: prixValue && prixValue !== '' ? parseFloat(prixValue) : null,
constructeurId: props.piece.constructeurId || null,
constructeurId: props.piece.constructeurId || null
})
}
@@ -443,7 +456,7 @@ const assignPieceModel = (value) => {
pieceId: props.piece.id,
pieceModelId: value || null,
previousModelId,
previousModel,
previousModel
})
}
@@ -452,7 +465,7 @@ const updateCustomFieldValue = async (fieldValueId) => {
if (fieldValue) {
const { updateCustomFieldValue } = useCustomFields()
const { showSuccess, showError } = useToast()
const result = await updateCustomFieldValue(fieldValueId, { value: fieldValue.value })
if (result.success) {
showSuccess(`Champ "${fieldValue.customField.name}" mis à jour avec succès`)
@@ -473,7 +486,7 @@ watch(
pieceData.name = props.piece.name || ''
pieceData.reference = props.piece.reference || ''
pieceData.prix = props.piece.prix || ''
},
}
)
onMounted(() => {
@@ -481,7 +494,7 @@ onMounted(() => {
pieceData.name = props.piece.name || ''
pieceData.reference = props.piece.reference || ''
pieceData.prix = props.piece.prix || ''
// Debug: vérifier si les champs personnalisés sont présents
console.log('PieceItem - piece:', props.piece)
console.log('PieceItem - customFieldValues:', props.piece.customFieldValues)
@@ -490,4 +503,4 @@ onMounted(() => {
refreshDocuments()
}
})
</script>
</script>

View File

@@ -2,7 +2,9 @@
<div class="space-y-4">
<section class="space-y-3">
<div class="flex items-center justify-between">
<h3 class="text-sm font-semibold">Champs personnalisés</h3>
<h3 class="text-sm font-semibold">
Champs personnalisés
</h3>
<button type="button" class="btn btn-outline btn-xs" @click="addField">
<IconLucidePlus class="w-3 h-3 mr-2" aria-hidden="true" />
Ajouter
@@ -27,18 +29,28 @@
type="text"
class="input input-bordered input-xs"
placeholder="Nom du champ"
/>
>
<select v-model="field.type" class="select select-bordered select-xs">
<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>
<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" />
<input v-model="field.required" type="checkbox" class="checkbox checkbox-xs">
Obligatoire
</div>
@@ -47,7 +59,7 @@
v-model="field.optionsText"
class="textarea textarea-bordered textarea-xs h-20"
placeholder="Option 1&#10;Option 2"
></textarea>
/>
</div>
<button
type="button"
@@ -71,13 +83,13 @@ import IconLucideTrash from '~icons/lucide/trash'
const props = defineProps({
modelValue: {
type: Object,
default: () => ({ customFields: [] }),
},
default: () => ({ customFields: [] })
}
})
const emit = defineEmits(['update:modelValue'])
const ensureArray = (value) => (Array.isArray(value) ? value : [])
const ensureArray = value => (Array.isArray(value) ? value : [])
const clone = (input, fallback = {}) => {
try {
@@ -104,24 +116,24 @@ const toEditorField = (input = {}) => ({
? input.options.join('\n')
: typeof input.optionsText === 'string'
? input.optionsText
: '',
: ''
})
const hydrateFields = (structure = {}) => ensureArray(structure.customFields).map(toEditorField)
const localState = reactive({
fields: hydrateFields(props.modelValue),
fields: hydrateFields(props.modelValue)
})
const extraState = reactive({
rest: clone(extractRest(props.modelValue)),
rest: clone(extractRest(props.modelValue))
})
const localFields = computed({
get: () => localState.fields,
set: (value) => {
localState.fields = ensureArray(value).map(toEditorField)
},
}
})
const normalizeFields = (fields) => {
@@ -139,8 +151,8 @@ const normalizeFields = (fields) => {
const raw = typeof field.optionsText === 'string' ? field.optionsText : ''
const parsed = raw
.split(/\r?\n/)
.map((option) => option.trim())
.filter((option) => option.length > 0)
.map(option => option.trim())
.filter(option => option.length > 0)
options = parsed.length > 0 ? parsed : undefined
}
@@ -155,14 +167,14 @@ const normalizeFields = (fields) => {
let lastEmitted = JSON.stringify({
...clone(extraState.rest, {}),
customFields: normalizeFields(props.modelValue?.customFields),
customFields: normalizeFields(props.modelValue?.customFields)
})
const emitUpdate = () => {
const customFields = normalizeFields(localFields.value)
const payload = {
...clone(extraState.rest, {}),
customFields,
customFields
}
const serialized = JSON.stringify(payload)
if (serialized !== lastEmitted) {
@@ -178,7 +190,7 @@ watch(
extraState.rest = clone(extractRest(value), {})
lastEmitted = JSON.stringify({
...clone(extraState.rest, {}),
customFields: normalizeFields(value?.customFields),
customFields: normalizeFields(value?.customFields)
})
},
{ deep: true }

View File

@@ -10,12 +10,28 @@
aria-hidden="true"
/>
</button>
<input
v-model="node.name"
type="text"
class="input input-sm input-bordered w-full"
placeholder="Nom du sous-composant"
/>
<div class="flex-1">
<input
:list="componentTypeListId"
v-model="node.typeComposantLabel"
type="search"
autocomplete="off"
class="input input-sm input-bordered w-full"
placeholder="Sélectionner une famille de composant"
@change="handleComponentTypeChange(node)"
@blur="handleComponentTypeChange(node)"
/>
<datalist :id="componentTypeListId">
<option
v-for="type in componentTypes"
:key="type.id"
:value="formatComponentTypeOption(type)"
/>
</datalist>
<p class="mt-1 text-[11px] text-gray-500">
{{ node.typeComposantId ? `Sélection : ${getComponentTypeLabel(node.typeComposantId) || 'Inconnue'}` : 'Aucune famille sélectionnée' }}
</p>
</div>
</div>
<span v-if="!expanded && node.description" class="text-xs text-gray-500 truncate">
{{ node.description }}
@@ -112,52 +128,43 @@
>
<div class="flex items-start justify-between gap-2">
<div class="flex-1 space-y-3">
<div class="grid grid-cols-1 md:grid-cols-3 gap-2">
<input
v-model="piece.name"
type="text"
class="input input-bordered input-xs"
placeholder="Nom de la pièce"
/>
<input
v-model="piece.reference"
type="text"
class="input input-bordered input-xs"
placeholder="Référence"
/>
<input
v-model.number="piece.quantity"
type="number"
min="0"
step="1"
class="input input-bordered input-xs"
placeholder="Quantité"
/>
</div>
<div class="form-control">
<label class="label"><span class="label-text">Famille de pièce</span></label>
<div>
<input
:list="getPieceTypeListId(pieceIndex)"
v-model="piece.typePieceLabel"
type="search"
autocomplete="off"
class="input input-bordered input-xs"
placeholder="Rechercher une famille"
@change="handlePieceTypeChange(piece)"
@blur="handlePieceTypeChange(piece)"
/>
<datalist :id="getPieceTypeListId(pieceIndex)">
<option
v-for="type in pieceTypes"
:key="type.id"
:value="formatPieceTypeOption(type)"
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<div class="form-control">
<label class="label"><span class="label-text">Famille de pièce</span></label>
<div>
<input
:list="getPieceTypeListId(pieceIndex)"
v-model="piece.typePieceLabel"
type="search"
autocomplete="off"
class="input input-bordered input-xs"
placeholder="Sélectionner une famille"
@change="handlePieceTypeChange(piece)"
@blur="handlePieceTypeChange(piece)"
/>
</datalist>
<datalist :id="getPieceTypeListId(pieceIndex)">
<option
v-for="type in pieceTypes"
:key="type.id"
:value="formatPieceTypeOption(type)"
/>
</datalist>
</div>
<p class="mt-1 text-[11px] text-gray-500">
{{ piece.typePieceId ? `Sélection : ${getPieceTypeLabel(piece.typePieceId) || 'Inconnue'}` : 'Aucune famille sélectionnée' }}
</p>
</div>
<div class="form-control">
<label class="label"><span class="label-text">Quantité (optionnel)</span></label>
<input
v-model.number="piece.quantity"
type="number"
min="0"
step="1"
class="input input-bordered input-xs"
placeholder="Quantité"
/>
</div>
<p class="mt-1 text-[11px] text-gray-500">
{{ piece.typePieceId ? `Sélection : ${getPieceTypeLabel(piece.typePieceId) || 'Inconnue'}` : 'Aucune famille sélectionnée' }}
</p>
</div>
</div>
<button type="button" class="btn btn-error btn-xs btn-square" @click="removePiece(pieceIndex)">
@@ -188,6 +195,7 @@
:node="sub"
:depth="depth + 1"
:piece-types="pieceTypes"
:component-types="componentTypes"
@remove="removeSubComponent(index)"
/>
</div>
@@ -197,14 +205,14 @@
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { computed, ref, watch, getCurrentInstance } from 'vue'
import IconLucideChevronRight from '~icons/lucide/chevron-right'
import IconLucidePlus from '~icons/lucide/plus'
import IconLucideTrash from '~icons/lucide/trash'
defineOptions({ name: 'StructureSubComponentEditor' })
type PieceTypeOption = {
type ModelTypeOption = {
id: string
name: string
code?: string | null
@@ -213,17 +221,30 @@ type PieceTypeOption = {
const props = withDefaults(defineProps<{
node: Record<string, any>
depth?: number
pieceTypes?: PieceTypeOption[]
pieceTypes?: ModelTypeOption[]
componentTypes?: ModelTypeOption[]
}>(), {
depth: 0,
pieceTypes: () => [],
componentTypes: () => [],
})
const emit = defineEmits(['remove'])
const pieceTypes = computed(() => props.pieceTypes ?? [])
const componentTypes = computed(() => props.componentTypes ?? [])
const instance = getCurrentInstance()
const componentTypeListId = `sub-component-type-options-${instance?.uid ?? 0}`
const formatModelTypeOption = (type: ModelTypeOption | undefined | null) => {
if (!type) return ''
return type.code ? `${type.name} (${type.code})` : type.name
}
const pieceTypeMap = computed(() => {
const map = new Map<string, PieceTypeOption>()
;(props.pieceTypes ?? []).forEach((type) => {
const map = new Map<string, ModelTypeOption>()
pieceTypes.value.forEach((type) => {
if (type && typeof type.id === 'string') {
map.set(type.id, type)
}
@@ -231,10 +252,18 @@ const pieceTypeMap = computed(() => {
return map
})
const formatPieceTypeOption = (type: PieceTypeOption | undefined | null) => {
if (!type) return ''
return type.code ? `${type.name} (${type.code})` : type.name
}
const componentTypeMap = computed(() => {
const map = new Map<string, ModelTypeOption>()
componentTypes.value.forEach((type) => {
if (type && typeof type.id === 'string') {
map.set(type.id, type)
}
})
return map
})
const formatPieceTypeOption = (type: ModelTypeOption | undefined | null) => formatModelTypeOption(type)
const formatComponentTypeOption = (type: ModelTypeOption | undefined | null) => formatModelTypeOption(type)
const resolvePieceType = (input: string) => {
const normalized = input.trim().toLowerCase()
@@ -242,7 +271,7 @@ const resolvePieceType = (input: string) => {
return null
}
return (
(props.pieceTypes ?? []).find((type) => {
pieceTypes.value.find((type) => {
const formatted = formatPieceTypeOption(type).toLowerCase()
const name = (type?.name ?? '').toLowerCase()
const code = (type?.code ?? '').toLowerCase()
@@ -255,24 +284,58 @@ const resolvePieceType = (input: string) => {
)
}
const resolveComponentType = (input: string) => {
const normalized = input.trim().toLowerCase()
if (!normalized) {
return null
}
return (
componentTypes.value.find((type) => {
const formatted = formatComponentTypeOption(type).toLowerCase()
const name = (type?.name ?? '').toLowerCase()
const code = (type?.code ?? '').toLowerCase()
return (
formatted === normalized
|| name === normalized
|| (!!code && code === normalized)
)
}) ?? null
)
}
const getPieceTypeLabel = (id?: string) => {
if (!id) return ''
const option = pieceTypeMap.value.get(id)
return formatPieceTypeOption(option)
}
const getComponentTypeLabel = (id?: string) => {
if (!id) return ''
const option = componentTypeMap.value.get(id)
return formatComponentTypeOption(option)
}
const updatePieceTypeLabel = (piece: any) => {
if (!piece) {
return
}
if (piece.typePieceId) {
const option = pieceTypeMap.value.get(piece.typePieceId)
piece.typePieceLabel = option ? formatPieceTypeOption(option) : piece.typePieceLabel || ''
if (option) {
piece.typePieceLabel = formatPieceTypeOption(option)
piece.name = option.name || formatPieceTypeOption(option)
} else if (!piece.typePieceLabel) {
piece.name = ''
}
} else if (piece.typePieceLabel) {
const match = resolvePieceType(piece.typePieceLabel)
if (match) {
piece.typePieceId = match.id
piece.typePieceLabel = formatPieceTypeOption(match)
piece.name = match.name || formatPieceTypeOption(match)
} else {
piece.typePieceLabel = ''
piece.name = ''
}
}
}
@@ -285,14 +348,18 @@ const handlePieceTypeChange = (piece: any) => {
if (!value) {
piece.typePieceId = ''
piece.typePieceLabel = ''
piece.name = ''
return
}
const match = resolvePieceType(value)
if (match) {
piece.typePieceId = match.id
piece.typePieceLabel = formatPieceTypeOption(match)
piece.name = match.name || formatPieceTypeOption(match)
} else {
piece.typePieceId = ''
piece.typePieceLabel = ''
piece.name = ''
}
}
@@ -310,38 +377,96 @@ const applyPieceLabels = (pieces?: any[]) => {
if (match) {
piece.typePieceId = match.id
piece.typePieceLabel = formatPieceTypeOption(match)
piece.name = match.name || formatPieceTypeOption(match)
} else {
piece.typePieceLabel = ''
piece.name = ''
}
} else if (!piece?.name) {
piece.name = ''
}
})
}
const applyComponentTypeLabel = (component: any) => {
if (!component) {
return
}
if (component.typeComposantId) {
const option = componentTypeMap.value.get(component.typeComposantId)
if (option) {
component.typeComposantLabel = formatComponentTypeOption(option)
component.name = option.name || formatComponentTypeOption(option)
} else if (!component.typeComposantLabel) {
component.name = ''
}
} else if (component.typeComposantLabel) {
const match = resolveComponentType(component.typeComposantLabel)
if (match) {
component.typeComposantId = match.id
component.typeComposantLabel = formatComponentTypeOption(match)
component.name = match.name || formatComponentTypeOption(match)
} else {
component.typeComposantLabel = ''
component.name = ''
}
}
}
const handleComponentTypeChange = (component: any) => {
if (!component) {
return
}
const value = typeof component.typeComposantLabel === 'string'
? component.typeComposantLabel.trim()
: ''
if (!value) {
component.typeComposantId = ''
component.typeComposantLabel = ''
component.name = ''
return
}
const match = resolveComponentType(value)
if (match) {
component.typeComposantId = match.id
component.typeComposantLabel = formatComponentTypeOption(match)
component.name = match.name || formatComponentTypeOption(match)
} else {
component.typeComposantId = ''
component.typeComposantLabel = ''
component.name = ''
}
}
const traverseSubComponents = (components?: any[]) => {
if (!Array.isArray(components)) {
return
}
components.forEach((component) => {
applyComponentTypeLabel(component)
applyPieceLabels(component?.pieces)
traverseSubComponents(component?.subComponents)
})
}
const syncPieceTypeLabels = () => {
const syncTypeLabels = () => {
applyComponentTypeLabel(props.node)
applyPieceLabels(props.node?.pieces)
traverseSubComponents(props.node?.subComponents)
}
watch(
() => props.pieceTypes,
() => {
syncPieceTypeLabels()
},
{ deep: true, immediate: true }
)
watch(pieceTypes, () => {
syncTypeLabels()
}, { deep: true, immediate: true })
watch(componentTypes, () => {
syncTypeLabels()
}, { deep: true, immediate: true })
watch(
() => props.node,
() => {
syncPieceTypeLabels()
syncTypeLabels()
},
{ deep: true }
)
@@ -379,7 +504,6 @@ const addPiece = () => {
ensureArray('pieces')
props.node.pieces.push({
name: '',
reference: '',
quantity: undefined,
typePieceId: '',
typePieceLabel: '',
@@ -397,6 +521,8 @@ const addSubComponent = () => {
name: '',
description: '',
quantity: undefined,
typeComposantId: '',
typeComposantLabel: '',
customFields: [],
pieces: [],
subComponents: [],

View File

@@ -38,16 +38,16 @@
aria-hidden="true"
/>
</div>
<!-- Message -->
<div class="flex-1">
<span class="text-sm font-medium">{{ toast.message }}</span>
</div>
<!-- Close button -->
<button
@click="removeToast(toast.id)"
class="btn btn-ghost btn-xs"
@click="removeToast(toast.id)"
>
<IconLucideX class="w-4 h-4" aria-hidden="true" />
</button>
@@ -102,4 +102,4 @@ const getToastClasses = (type) => {
.toast-move {
transition: transform 0.3s ease;
}
</style>
</style>

View File

@@ -1,7 +1,7 @@
<template>
<div class="space-y-4">
<div class="flex justify-end">
<button
<button
type="button"
class="btn btn-outline btn-sm"
@click="toggleAllComponents"
@@ -10,157 +10,171 @@
class="w-4 h-4 mr-2"
aria-hidden="true"
/>
</button>
<h5 class="text-sm font-medium">Nouveau composant {{ index + 1 }}</h5>
<span v-if="!isComponentExpanded(index)" class="text-xs text-gray-500 truncate max-w-[160px]">
{{ component.name || 'Sans nom' }}
</span>
</div>
<button
type="button"
@click="removeComponent(index)"
class="btn btn-square btn-error btn-sm"
>
<IconLucideChevronRight
class="w-4 h-4"
aria-hidden="true"
/>
</button>
<span class="text-xs font-medium">Champ personnalisé {{ fieldIndex + 1 }}</span>
<span v-if="!isComponentCustomFieldExpanded(index, fieldIndex)" class="text-[10px] text-gray-500 truncate max-w-[120px]">
{{ field.name || 'Sans nom' }}
</span>
</div>
<button
type="button"
@click="removeComponentCustomField(index, fieldIndex)"
class="btn btn-square btn-error btn-xs"
>
<IconLucideChevronRight
class="w-3 h-3"
aria-hidden="true"
/>
</button>
<IconLucideChevronRight
class="w-3 h-3 text-red-500"
aria-hidden="true"
/>
</button>
<span class="text-xs">Champ {{ fieldIndex + 1 }}</span>
<span v-if="!isComponentPieceCustomFieldExpanded(index, pieceIndex, fieldIndex)" class="text-[10px] text-gray-500 truncate max-w-[100px]">
{{ field.name || 'Sans nom' }}
</span>
</div>
<button
type="button"
@click="removeComponentPieceCustomField(index, pieceIndex, fieldIndex)"
class="btn btn-square btn-error btn-xs"
>
<IconLucideChevronRight
class="w-2 h-2"
aria-hidden="true"
/>
</button>
<IconLucideChevronRight
class="w-3 h-3 text-green-500"
aria-hidden="true"
/>
</button>
<span class="text-xs">Champ {{ fieldIndex + 1 }}</span>
<span v-if="!isSubComponentCustomFieldExpanded(index, subIndex, fieldIndex)" class="text-[10px] text-gray-500 truncate max-w-[100px]">
{{ field.name || 'Sans nom' }}
</span>
</div>
<button
type="button"
@click="removeSubComponentCustomField(index, subIndex, fieldIndex)"
class="btn btn-square btn-error btn-xs"
>
<IconLucideChevronRight
class="w-2 h-2"
aria-hidden="true"
/>
</button>
<IconLucideChevronRight
class="w-2 h-2 text-red-500"
aria-hidden="true"
/>
</button>
<span class="text-xs">Champ {{ fieldIndex + 1 }}</span>
<span v-if="!isSubComponentPieceCustomFieldExpanded(index, subIndex, pieceIndex, fieldIndex)" class="text-[10px] text-gray-500 truncate max-w-[100px]">
{{ field.name || 'Sans nom' }}
</span>
</div>
<button
type="button"
@click="removeSubComponentPieceCustomField(index, subIndex, pieceIndex, fieldIndex)"
class="btn btn-square btn-error btn-xs"
>
<IconLucideX
class="w-2 h-2"
aria-hidden="true"
/>
</button>
</div>
<div v-if="isSubComponentPieceCustomFieldExpanded(index, subIndex, pieceIndex, fieldIndex)" class="grid grid-cols-2 gap-1">
<input
v-model="field.name"
type="text"
placeholder="Nom"
class="input input-bordered input-xs"
/>
<select v-model="field.type" class="select select-bordered select-xs">
<option value="">Type</option>
<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 v-if="isSubComponentPieceCustomFieldExpanded(index, subIndex, pieceIndex, fieldIndex)" class="mt-1">
<label class="flex items-center gap-1 text-xs">
<input
v-model="field.required"
type="checkbox"
class="checkbox checkbox-xs"
/>
Obligatoire
</label>
</div>
<div v-if="isSubComponentPieceCustomFieldExpanded(index, subIndex, pieceIndex, fieldIndex) && field.type === 'select'" class="mt-1">
<textarea
v-model="field.optionsText"
placeholder="Option 1&#10;Option 2&#10;Option 3"
class="textarea textarea-bordered textarea-xs w-full h-10"
@input="updateSubComponentPieceFieldOptions(index, subIndex, pieceIndex, fieldIndex)"
></textarea>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</button>
<h5 class="text-sm font-medium">
Nouveau composant {{ index + 1 }}
</h5>
<span v-if="!isComponentExpanded(index)" class="text-xs text-gray-500 truncate max-w-[160px]">
{{ component.name || 'Sans nom' }}
</span>
</div>
<button
type="button"
@click="addComponent"
class="btn btn-outline btn-sm"
<button
type="button"
class="btn btn-square btn-error btn-sm"
@click="removeComponent(index)"
>
<IconLucidePlus
class="w-4 h-4 mr-2"
<IconLucideChevronRight
class="w-4 h-4"
aria-hidden="true"
/>
Ajouter un composant
</button>
<span class="text-xs font-medium">Champ personnalisé {{ fieldIndex + 1 }}</span>
<span v-if="!isComponentCustomFieldExpanded(index, fieldIndex)" class="text-[10px] text-gray-500 truncate max-w-[120px]">
{{ field.name || 'Sans nom' }}
</span>
</div>
<button
type="button"
class="btn btn-square btn-error btn-xs"
@click="removeComponentCustomField(index, fieldIndex)"
>
<IconLucideChevronRight
class="w-3 h-3"
aria-hidden="true"
/>
</button>
<IconLucideChevronRight
class="w-3 h-3 text-red-500"
aria-hidden="true"
/>
</button>
<span class="text-xs">Champ {{ fieldIndex + 1 }}</span>
<span v-if="!isComponentPieceCustomFieldExpanded(index, pieceIndex, fieldIndex)" class="text-[10px] text-gray-500 truncate max-w-[100px]">
{{ field.name || 'Sans nom' }}
</span>
</div>
<button
type="button"
class="btn btn-square btn-error btn-xs"
@click="removeComponentPieceCustomField(index, pieceIndex, fieldIndex)"
>
<IconLucideChevronRight
class="w-2 h-2"
aria-hidden="true"
/>
</button>
<IconLucideChevronRight
class="w-3 h-3 text-green-500"
aria-hidden="true"
/>
</button>
<span class="text-xs">Champ {{ fieldIndex + 1 }}</span>
<span v-if="!isSubComponentCustomFieldExpanded(index, subIndex, fieldIndex)" class="text-[10px] text-gray-500 truncate max-w-[100px]">
{{ field.name || 'Sans nom' }}
</span>
</div>
<button
type="button"
class="btn btn-square btn-error btn-xs"
@click="removeSubComponentCustomField(index, subIndex, fieldIndex)"
>
<IconLucideChevronRight
class="w-2 h-2"
aria-hidden="true"
/>
</button>
<IconLucideChevronRight
class="w-2 h-2 text-red-500"
aria-hidden="true"
/>
</button>
<span class="text-xs">Champ {{ fieldIndex + 1 }}</span>
<span v-if="!isSubComponentPieceCustomFieldExpanded(index, subIndex, pieceIndex, fieldIndex)" class="text-[10px] text-gray-500 truncate max-w-[100px]">
{{ field.name || 'Sans nom' }}
</span>
</div>
<button
type="button"
class="btn btn-square btn-error btn-xs"
@click="removeSubComponentPieceCustomField(index, subIndex, pieceIndex, fieldIndex)"
>
<IconLucideX
class="w-2 h-2"
aria-hidden="true"
/>
</button>
</div>
<div v-if="isSubComponentPieceCustomFieldExpanded(index, subIndex, pieceIndex, fieldIndex)" class="grid grid-cols-2 gap-1">
<input
v-model="field.name"
type="text"
placeholder="Nom"
class="input input-bordered input-xs"
>
<select v-model="field.type" class="select select-bordered select-xs">
<option value="">
Type
</option>
<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 v-if="isSubComponentPieceCustomFieldExpanded(index, subIndex, pieceIndex, fieldIndex)" class="mt-1">
<label class="flex items-center gap-1 text-xs">
<input
v-model="field.required"
type="checkbox"
class="checkbox checkbox-xs"
>
Obligatoire
</label>
</div>
<div v-if="isSubComponentPieceCustomFieldExpanded(index, subIndex, pieceIndex, fieldIndex) && field.type === 'select'" class="mt-1">
<textarea
v-model="field.optionsText"
placeholder="Option 1&#10;Option 2&#10;Option 3"
class="textarea textarea-bordered textarea-xs w-full h-10"
@input="updateSubComponentPieceFieldOptions(index, subIndex, pieceIndex, fieldIndex)"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<button
type="button"
class="btn btn-outline btn-sm"
@click="addComponent"
>
<IconLucidePlus
class="w-4 h-4 mr-2"
aria-hidden="true"
/>
Ajouter un composant
</button>
</div>
</template>
@@ -381,7 +395,7 @@ const reorderStore = (store) => {
.forEach((key, position) => {
reordered[position] = store[key]
})
Object.keys(store).forEach((key) => delete store[key])
Object.keys(store).forEach(key => delete store[key])
Object.assign(store, reordered)
}
@@ -394,19 +408,19 @@ const reorderNestedStore = (store, componentIndex) => {
.forEach((key, position) => {
reordered[position] = entries[key]
})
Object.keys(entries).forEach((key) => delete entries[key])
Object.keys(entries).forEach(key => delete entries[key])
Object.assign(entries, reordered)
}
const clearExpansionState = () => {
expandedComponents.value = []
Object.keys(expandedComponentCustomFields).forEach((key) => delete expandedComponentCustomFields[key])
Object.keys(expandedComponentPieces).forEach((key) => delete expandedComponentPieces[key])
Object.keys(expandedComponentPieceCustomFields).forEach((key) => delete expandedComponentPieceCustomFields[key])
Object.keys(expandedSubComponents).forEach((key) => delete expandedSubComponents[key])
Object.keys(expandedSubComponentCustomFields).forEach((key) => delete expandedSubComponentCustomFields[key])
Object.keys(expandedSubComponentPieces).forEach((key) => delete expandedSubComponentPieces[key])
Object.keys(expandedSubComponentPieceCustomFields).forEach((key) => delete expandedSubComponentPieceCustomFields[key])
Object.keys(expandedComponentCustomFields).forEach(key => delete expandedComponentCustomFields[key])
Object.keys(expandedComponentPieces).forEach(key => delete expandedComponentPieces[key])
Object.keys(expandedComponentPieceCustomFields).forEach(key => delete expandedComponentPieceCustomFields[key])
Object.keys(expandedSubComponents).forEach(key => delete expandedSubComponents[key])
Object.keys(expandedSubComponentCustomFields).forEach(key => delete expandedSubComponentCustomFields[key])
Object.keys(expandedSubComponentPieces).forEach(key => delete expandedSubComponentPieces[key])
Object.keys(expandedSubComponentPieceCustomFields).forEach(key => delete expandedSubComponentPieceCustomFields[key])
}
const setAllExpanded = (value) => {
@@ -625,7 +639,7 @@ const removeSubComponent = (componentIndex, subIndex) => {
.forEach((key, position) => {
reordered[position] = pieceFieldEntries[key]
})
Object.keys(pieceFieldEntries).forEach((key) => delete pieceFieldEntries[key])
Object.keys(pieceFieldEntries).forEach(key => delete pieceFieldEntries[key])
Object.assign(pieceFieldEntries, reordered)
}
@@ -687,7 +701,7 @@ const removeSubComponentPiece = (componentIndex, subIndex, pieceIndex) => {
.forEach((key, position) => {
reordered[position] = store[key]
})
Object.keys(store || {}).forEach((key) => delete store[key])
Object.keys(store || {}).forEach(key => delete store[key])
Object.assign(store || {}, reordered)
}
}

View File

@@ -2,7 +2,7 @@
<div class="card bg-base-100 shadow-lg">
<div class="card-body">
<div class="card-actions justify-end">
<button type="button" @click="$emit('reset')" class="btn btn-outline">
<button type="button" class="btn btn-outline" @click="$emit('reset')">
Réinitialiser
</button>
<button type="submit" class="btn btn-primary" :disabled="saving">
@@ -26,8 +26,8 @@ import IconLucideCheck from '~icons/lucide/check'
defineProps({
saving: {
type: Boolean,
default: false,
},
default: false
}
})
defineEmits(['reset'])

View File

@@ -1,7 +1,9 @@
<template>
<div class="card bg-base-100 shadow-lg">
<div class="card-body">
<h3 class="card-title text-lg mb-4">Informations de base</h3>
<h3 class="card-title text-lg mb-4">
Informations de base
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-control">
@@ -15,7 +17,7 @@
placeholder="Nom du type de machine"
class="input input-bordered"
required
/>
>
</div>
<div class="form-control">
@@ -27,7 +29,7 @@
type="text"
placeholder="Catégorie du type"
class="input input-bordered"
/>
>
</div>
<div class="form-control md:col-span-2">
@@ -38,7 +40,7 @@
v-model="descriptionModel"
placeholder="Description du type de machine"
class="textarea textarea-bordered h-24"
></textarea>
/>
</div>
<div class="form-control">
@@ -50,7 +52,7 @@
type="text"
placeholder="ex: Mensuelle, Trimestrielle"
class="input input-bordered"
/>
>
</div>
</div>
</div>
@@ -63,41 +65,41 @@ import { computed } from 'vue'
const props = defineProps({
name: {
type: String,
default: '',
default: ''
},
category: {
type: String,
default: '',
default: ''
},
description: {
type: String,
default: '',
default: ''
},
maintenanceFrequency: {
type: String,
default: '',
},
default: ''
}
})
const emit = defineEmits(['update:name', 'update:category', 'update:description', 'update:maintenanceFrequency'])
const nameModel = computed({
get: () => props.name,
set: (value) => emit('update:name', value),
set: value => emit('update:name', value)
})
const categoryModel = computed({
get: () => props.category,
set: (value) => emit('update:category', value),
set: value => emit('update:category', value)
})
const descriptionModel = computed({
get: () => props.description,
set: (value) => emit('update:description', value),
set: value => emit('update:description', value)
})
const maintenanceModel = computed({
get: () => props.maintenanceFrequency,
set: (value) => emit('update:maintenanceFrequency', value),
set: value => emit('update:maintenanceFrequency', value)
})
</script>

View File

@@ -14,7 +14,9 @@
aria-hidden="true"
/>
</button>
<h3 class="card-title text-lg">Champs personnalisés du type</h3>
<h3 class="card-title text-lg">
Champs personnalisés du type
</h3>
<span class="badge badge-primary">{{ fields.length }}</span>
</div>
</div>
@@ -30,8 +32,8 @@
<button
type="button"
class="btn btn-ghost btn-xs p-1"
@click="toggleField(fieldIndex)"
title="Plier / déplier le champ"
@click="toggleField(fieldIndex)"
>
<IconLucideChevronRight
class="w-4 h-4 transition-transform duration-200"
@@ -40,16 +42,18 @@
/>
</button>
<IconLucideListChecks class="w-4 h-4 text-blue-500" aria-hidden="true" />
<h5 class="text-sm font-medium">Champ personnalisé {{ fieldIndex + 1 }}</h5>
<h5 class="text-sm font-medium">
Champ personnalisé {{ fieldIndex + 1 }}
</h5>
<span v-if="!isFieldExpanded(fieldIndex)" class="text-xs text-gray-500 truncate max-w-[160px]">
{{ field.name || 'Sans nom' }}
</span>
</div>
<button
type="button"
@click="removeField(fieldIndex)"
class="btn btn-square btn-error btn-sm"
title="Supprimer ce champ"
@click="removeField(fieldIndex)"
>
<IconLucideX class="w-4 h-4" aria-hidden="true" />
</button>
@@ -68,7 +72,7 @@
class="input input-bordered input-sm"
required
@input="updateField(fieldIndex, { name: $event.target.value })"
/>
>
</div>
<div class="form-control">
@@ -82,12 +86,24 @@
:value="field.type"
@change="updateField(fieldIndex, { type: $event.target.value })"
>
<option value="">Sélectionner un type</option>
<option value="text">Texte</option>
<option value="number">Nombre</option>
<option value="select">Liste déroulante</option>
<option value="boolean">Oui/Non</option>
<option value="date">Date</option>
<option value="">
Sélectionner un type
</option>
<option value="text">
Texte
</option>
<option value="number">
Nombre
</option>
<option value="select">
Liste déroulante
</option>
<option value="boolean">
Oui/Non
</option>
<option value="date">
Date
</option>
</select>
</div>
</div>
@@ -99,7 +115,7 @@
class="checkbox checkbox-sm"
:checked="field.required"
@change="updateField(fieldIndex, { required: $event.target.checked })"
/>
>
<span class="text-sm">Champ obligatoire</span>
</div>
</div>
@@ -117,19 +133,19 @@
placeholder="Option 1&#10;Option 2&#10;Option 3"
class="textarea textarea-bordered textarea-sm w-full h-20"
@input="updateOptions(fieldIndex, $event.target.value)"
></textarea>
/>
</div>
</div>
<div class="flex justify-end">
<button type="button" @click="addField" class="btn btn-primary btn-sm">
<button type="button" class="btn btn-primary btn-sm" @click="addField">
<IconLucidePlus class="w-4 h-4 mr-2" aria-hidden="true" />
Ajouter un champ
</button>
</div>
</div>
<div v-else class="flex justify-end">
<button type="button" @click="addField" class="btn btn-primary btn-sm">
<button type="button" class="btn btn-primary btn-sm" @click="addField">
<IconLucidePlus class="w-4 h-4 mr-2" aria-hidden="true" />
Ajouter un champ
</button>
@@ -148,23 +164,23 @@ import IconLucidePlus from '~icons/lucide/plus'
const props = defineProps({
modelValue: {
type: Array,
default: () => [],
default: () => []
},
allExpanded: {
type: Boolean,
default: false,
default: false
},
expandAllTrigger: {
type: Number,
default: 0,
},
default: 0
}
})
const emit = defineEmits(['update:modelValue'])
const fields = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value),
set: value => emit('update:modelValue', value)
})
const expanded = ref(false)
@@ -213,8 +229,8 @@ const addField = () => {
name: '',
type: '',
required: false,
optionsText: '',
},
optionsText: ''
}
]
expandedFields.value.push(true)
expanded.value = true
@@ -231,7 +247,7 @@ const updateField = (index, patch) => {
const updateOptions = (index, value) => {
updateField(index, {
optionsText: value.replace(/\r\n/g, '\n').replace(/\r/g, '\n'),
optionsText: value.replace(/\r\n/g, '\n').replace(/\r/g, '\n')
})
}
</script>

View File

@@ -42,17 +42,17 @@ import TypeEditPieceRequirementsSection from '~/components/TypeEditPieceRequirem
const props = defineProps({
modelValue: {
type: Object,
required: true,
required: true
},
saving: {
type: Boolean,
default: false,
},
default: false
}
})
const emit = defineEmits(['update:modelValue', 'submit'])
const deepClone = (value) => JSON.parse(JSON.stringify(value))
const deepClone = value => JSON.parse(JSON.stringify(value))
const createDefaultForm = (source = {}) => ({
name: source.name || '',
@@ -61,7 +61,7 @@ const createDefaultForm = (source = {}) => ({
maintenanceFrequency: source.maintenanceFrequency || '',
customFields: deepClone(source.customFields || []),
componentRequirements: deepClone(source.componentRequirements || []),
pieceRequirements: deepClone(source.pieceRequirements || []),
pieceRequirements: deepClone(source.pieceRequirements || [])
})
const formData = reactive(createDefaultForm(props.modelValue))
@@ -69,7 +69,7 @@ const allExpanded = ref(false)
const expandAllTrigger = ref(0)
let syncingFromParent = false
const toPlainObject = (value) => JSON.parse(JSON.stringify(value))
const toPlainObject = value => JSON.parse(JSON.stringify(value))
const lastSnapshot = ref(toPlainObject(createDefaultForm(props.modelValue)))
watch(
@@ -91,7 +91,7 @@ watch(
watch(
formData,
(value) => {
if (syncingFromParent) return
if (syncingFromParent) { return }
const normalized = createDefaultForm(value)
if (JSON.stringify(normalized) === JSON.stringify(lastSnapshot.value)) {
return

View File

@@ -15,8 +15,8 @@ import IconLucidePlus from '~icons/lucide/plus'
defineProps({
allExpanded: {
type: Boolean,
default: false,
},
default: false
}
})
defineEmits(['toggle'])

View File

@@ -1,13 +1,17 @@
<template>
<div class="alert alert-info mb-6">
<div>
<h3 class="font-bold">Type existant</h3>
<h3 class="font-bold">
Type existant
</h3>
<div class="text-sm">
<p><strong>Catégorie:</strong> {{ type.category || 'Non définie' }}</p>
<p><strong>Maintenance:</strong> {{ type.maintenanceFrequency || 'Non définie' }}</p>
<p><strong>Familles de composants:</strong> {{ type.componentRequirements?.length || 0 }}</p>
<p><strong>Groupes de pièces:</strong> {{ type.pieceRequirements?.length || 0 }}</p>
<p v-if="type.description"><strong>Description:</strong> {{ type.description }}</p>
<p v-if="type.description">
<strong>Description:</strong> {{ type.description }}
</p>
</div>
</div>
</div>
@@ -20,4 +24,4 @@ defineProps({
required: true
}
})
</script>
</script>

View File

@@ -2,8 +2,12 @@
<div class="card bg-base-100 shadow-lg hover:shadow-xl transition-shadow">
<div class="card-body">
<div class="flex items-center justify-between mb-4">
<h3 class="card-title text-lg">{{ site.name }}</h3>
<div class="badge badge-primary badge-sm">{{ machineCount }} machines</div>
<h3 class="card-title text-lg">
{{ site.name }}
</h3>
<div class="badge badge-primary badge-sm">
{{ machineCount }} machines
</div>
</div>
<div class="space-y-3 text-sm">
@@ -20,7 +24,7 @@
<div class="flex items-start gap-2 text-gray-600">
<IconLucideMapPin class="w-4 h-4 text-accent mt-1" aria-hidden="true" />
<span>
{{ site.contactAddress }}<br />
{{ site.contactAddress }}<br>
{{ site.contactPostalCode }} {{ site.contactCity }}
</span>
</div>

View File

@@ -5,7 +5,7 @@
Modifier le site
<span v-if="siteName" class="block text-sm font-normal text-gray-500">{{ siteName }}</span>
</h3>
<form @submit.prevent="emit('submit')" class="space-y-4">
<form class="space-y-4" @submit.prevent="emit('submit')">
<div class="form-control">
<label class="label">
<span class="label-text">Nom du site</span>
@@ -16,7 +16,7 @@
placeholder="Nom du site"
class="input input-bordered"
required
/>
>
</div>
<SiteContactFormFields :form="props.form" />
@@ -24,8 +24,12 @@
<div class="border-t border-base-200 pt-4 space-y-4">
<div class="flex items-center justify-between">
<div>
<h4 class="font-semibold text-sm">Documents liés</h4>
<p class="text-xs text-gray-500">Ajoutez des documents (PDF, images...) relatifs à ce site.</p>
<h4 class="font-semibold text-sm">
Documents liés
</h4>
<p class="text-xs text-gray-500">
Ajoutez des documents (PDF, images...) relatifs à ce site.
</p>
</div>
<span v-if="selectedFilesModel.length" class="badge badge-outline">
{{ selectedFilesModel.length }} fichier{{ selectedFilesModel.length > 1 ? 's' : '' }} prêt{{ selectedFilesModel.length > 1 ? 's' : '' }} à être ajouté
@@ -39,7 +43,9 @@
/>
<div v-if="documents.length" class="space-y-3">
<h5 class="text-sm font-medium">Documents existants</h5>
<h5 class="text-sm font-medium">
Documents existants
</h5>
<div class="space-y-2 max-h-48 overflow-y-auto pr-1">
<div
v-for="document in documents"
@@ -51,7 +57,9 @@
<component :is="documentIcon(document).component" class="h-6 w-6" aria-hidden="true" />
</span>
<div>
<div class="font-medium">{{ document.name }}</div>
<div class="font-medium">
{{ document.name }}
</div>
<div class="text-xs text-gray-500">
{{ document.mimeType || 'Inconnu' }} {{ formatSize(document.size) }}
</div>
@@ -84,7 +92,7 @@
Annuler
</button>
<button type="submit" class="btn btn-primary" :disabled="uploadingDocuments">
<span v-if="uploadingDocuments" class="loading loading-spinner loading-xs mr-2"></span>
<span v-if="uploadingDocuments" class="loading loading-spinner loading-xs mr-2" />
Enregistrer
</button>
</div>