chore: update frontend configuration
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'])
|
||||
|
||||
@@ -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 || [])
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 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 }
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 Option 2 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 Option 2 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'])
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 Option 2 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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -15,8 +15,8 @@ import IconLucidePlus from '~icons/lucide/plus'
|
||||
defineProps({
|
||||
allExpanded: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
defineEmits(['toggle'])
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user