507 lines
17 KiB
Vue
507 lines
17 KiB
Vue
<template>
|
|
<div class="border border-gray-200 rounded-lg p-4">
|
|
<DocumentPreviewModal
|
|
:document="previewDocument"
|
|
:visible="previewVisible"
|
|
@close="closePreview"
|
|
/>
|
|
|
|
<div class="flex items-center justify-between mb-3">
|
|
<div class="flex items-center gap-2">
|
|
<IconLucidePackage
|
|
class="w-4 h-4 text-purple-500"
|
|
aria-hidden="true"
|
|
/>
|
|
<input
|
|
v-if="isEditMode"
|
|
:id="`piece-name-${piece.id}`"
|
|
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>
|
|
</div>
|
|
<div class="flex flex-wrap items-center gap-2 text-xs">
|
|
<span
|
|
v-if="piece.typeMachinePieceRequirement"
|
|
class="badge badge-outline badge-sm"
|
|
>
|
|
Groupe : {{ piece.typeMachinePieceRequirement.label || piece.typeMachinePieceRequirement.typePiece?.name || 'Non défini' }}
|
|
</span>
|
|
<span
|
|
v-if="piece.pieceModel"
|
|
class="badge badge-outline badge-primary badge-sm"
|
|
>
|
|
Modèle : {{ piece.pieceModel.name }}
|
|
</span>
|
|
<span v-if="piece.parentComponentName" class="badge badge-ghost badge-sm">
|
|
Rattachée à {{ piece.parentComponentName }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="space-y-2 text-sm">
|
|
<div>
|
|
<span class="font-medium">Référence:</span>
|
|
<input
|
|
v-if="isEditMode"
|
|
:id="`piece-reference-${piece.id}`"
|
|
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 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">
|
|
{{ [piece.constructeur?.email, piece.constructeur?.phone].filter(Boolean).join(' • ') }}
|
|
</span>
|
|
</span>
|
|
<ConstructeurSelect
|
|
v-else
|
|
class="w-full"
|
|
:model-value="piece.constructeurId || piece.constructeur?.id || null"
|
|
@update:model-value="handleConstructeurChange"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<span class="font-medium">Prix:</span>
|
|
<input
|
|
v-if="isEditMode"
|
|
:id="`piece-prix-${piece.id}`"
|
|
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>
|
|
|
|
<div
|
|
v-if="isEditMode && piece.typeMachinePieceRequirement"
|
|
class="mt-3"
|
|
>
|
|
<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' }}
|
|
</span>
|
|
</label>
|
|
<select
|
|
:value="selectedPieceModelId"
|
|
class="select select-bordered select-sm w-full"
|
|
@change="assignPieceModel($event.target.value)"
|
|
>
|
|
<option value="">
|
|
Définir manuellement
|
|
</option>
|
|
<option
|
|
v-for="model in pieceModelOptions"
|
|
:key="model.id"
|
|
:value="model.id"
|
|
>
|
|
{{ model.name }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- 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>
|
|
<div class="space-y-3">
|
|
<div
|
|
v-for="fieldValue in piece.customFieldValues"
|
|
:key="fieldValue.id"
|
|
class="form-control"
|
|
>
|
|
<label class="label">
|
|
<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
|
|
v-if="fieldValue.customField.type === 'text'"
|
|
:value="fieldValue.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
|
|
v-else-if="fieldValue.customField.type === 'number'"
|
|
:value="fieldValue.value"
|
|
type="number"
|
|
class="input input-bordered input-sm"
|
|
:required="fieldValue.customField.required"
|
|
@input="setCustomFieldValue(fieldValue.id, $event.target.value)"
|
|
@blur="updateCustomFieldValue(fieldValue.id)"
|
|
>
|
|
|
|
<!-- 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
|
|
:value="fieldValue.value"
|
|
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
|
|
v-else-if="fieldValue.customField.type === 'date'"
|
|
:value="fieldValue.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">
|
|
{{ fieldValue.value || 'Non défini' }}
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<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>
|
|
<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>
|
|
|
|
<DocumentUpload
|
|
v-if="isEditMode"
|
|
v-model="selectedFiles"
|
|
title="Déposer des fichiers pour cette pièce"
|
|
subtitle="Formats acceptés : PDF, images, documents..."
|
|
@files-added="handleFilesAdded"
|
|
/>
|
|
|
|
<div v-if="pieceDocuments.length" class="space-y-2">
|
|
<div
|
|
v-for="document in pieceDocuments"
|
|
:key="document.id"
|
|
class="flex items-center justify-between rounded border border-base-200 bg-base-100 px-3 py-2"
|
|
>
|
|
<div class="flex items-center gap-3 text-sm">
|
|
<span class="text-xl" :class="documentIcon(document).colorClass">
|
|
<component
|
|
:is="documentIcon(document).component"
|
|
class="h-6 w-6"
|
|
aria-hidden="true"
|
|
/>
|
|
</span>
|
|
<div>
|
|
<div class="font-medium">
|
|
{{ document.name }}
|
|
</div>
|
|
<div class="text-xs text-gray-500">
|
|
{{ document.mimeType || 'Inconnu' }} • {{ formatSize(document.size) }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<button
|
|
type="button"
|
|
class="btn btn-ghost btn-xs"
|
|
:disabled="!canPreviewDocument(document)"
|
|
:title="canPreviewDocument(document) ? 'Consulter le document' : 'Aucun aperçu disponible pour ce type'"
|
|
@click="openPreview(document)"
|
|
>
|
|
Consulter
|
|
</button>
|
|
<button type="button" class="btn btn-ghost btn-xs" @click="downloadDocument(document)">
|
|
Télécharger
|
|
</button>
|
|
<button
|
|
v-if="isEditMode"
|
|
type="button"
|
|
class="btn btn-error btn-xs"
|
|
:disabled="uploadingDocuments"
|
|
@click="removeDocument(document.id)"
|
|
>
|
|
Supprimer
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<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'
|
|
import { getFileIcon } from '~/utils/fileIcons'
|
|
import { canPreviewDocument } from '~/utils/documentPreview'
|
|
import DocumentUpload from '~/components/DocumentUpload.vue'
|
|
import DocumentPreviewModal from '~/components/DocumentPreviewModal.vue'
|
|
import IconLucidePackage from '~icons/lucide/package'
|
|
|
|
const props = defineProps({
|
|
piece: {
|
|
type: Object,
|
|
required: true
|
|
},
|
|
isEditMode: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
pieceModelOptions: {
|
|
type: Array,
|
|
default: () => []
|
|
}
|
|
})
|
|
|
|
const emit = defineEmits(['update', 'edit', 'custom-field-update', 'assign-model'])
|
|
|
|
// Données locales isolées pour cette pièce
|
|
const pieceData = reactive({
|
|
name: props.piece.name || '',
|
|
reference: props.piece.reference || '',
|
|
prix: props.piece.prix || ''
|
|
})
|
|
|
|
const selectedFiles = ref([])
|
|
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 previewDocument = ref(null)
|
|
const previewVisible = ref(false)
|
|
const selectedPieceModelId = computed(() => props.piece.pieceModelId || props.piece.pieceModel?.id || '')
|
|
const pieceModelOptions = computed(() => props.pieceModelOptions || [])
|
|
|
|
const handleConstructeurChange = (value) => {
|
|
props.piece.constructeurId = value
|
|
updatePiece()
|
|
}
|
|
|
|
const { uploadDocuments, deleteDocument, loadDocumentsByPiece } = useDocuments()
|
|
|
|
const refreshDocuments = async () => {
|
|
if (!props.piece?.id) { return }
|
|
loadingDocuments.value = true
|
|
try {
|
|
const result = await loadDocumentsByPiece(props.piece.id, { updateStore: false })
|
|
if (result.success) {
|
|
props.piece.documents = result.data || []
|
|
documentsLoaded.value = true
|
|
}
|
|
} finally {
|
|
loadingDocuments.value = false
|
|
}
|
|
}
|
|
|
|
const handleFilesAdded = async (files) => {
|
|
if (!files.length || !props.piece?.id) { return }
|
|
uploadingDocuments.value = true
|
|
try {
|
|
const result = await uploadDocuments(
|
|
{
|
|
files,
|
|
context: { pieceId: props.piece.id }
|
|
},
|
|
{ updateStore: false }
|
|
)
|
|
|
|
if (result.success) {
|
|
const newDocs = result.data || []
|
|
props.piece.documents = [...newDocs, ...(props.piece.documents || [])]
|
|
documentsLoaded.value = true
|
|
selectedFiles.value = []
|
|
}
|
|
} finally {
|
|
uploadingDocuments.value = false
|
|
}
|
|
}
|
|
|
|
const removeDocument = async (documentId) => {
|
|
if (!documentId) { return }
|
|
const result = await deleteDocument(documentId, { updateStore: false })
|
|
if (result.success) {
|
|
props.piece.documents = (props.piece.documents || []).filter(doc => doc.id !== documentId)
|
|
}
|
|
}
|
|
|
|
const downloadDocument = (doc) => {
|
|
if (!doc?.path) { return }
|
|
|
|
if (doc.path.startsWith('data:')) {
|
|
const link = document.createElement('a')
|
|
link.href = doc.path
|
|
link.download = doc.filename || doc.name || 'document'
|
|
link.click()
|
|
return
|
|
}
|
|
|
|
window.open(doc.path, '_blank')
|
|
}
|
|
|
|
const openPreview = (doc) => {
|
|
if (!canPreviewDocument(doc)) { return }
|
|
previewDocument.value = doc
|
|
previewVisible.value = true
|
|
}
|
|
|
|
const closePreview = () => {
|
|
previewVisible.value = false
|
|
previewDocument.value = null
|
|
}
|
|
|
|
const formatSize = (size) => {
|
|
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)
|
|
return `${formatted.toFixed(1)} ${units[index]}`
|
|
}
|
|
|
|
watch(
|
|
() => props.piece.documents,
|
|
(docs) => {
|
|
documentsLoaded.value = !!(docs && docs.length)
|
|
}
|
|
)
|
|
|
|
// Méthodes pour gérer les champs personnalisés
|
|
const setCustomFieldValue = (fieldValueId, value) => {
|
|
const fieldValue = props.piece.customFieldValues?.find(fv => fv.id === fieldValueId)
|
|
if (fieldValue) {
|
|
fieldValue.value = value
|
|
}
|
|
}
|
|
|
|
const updatePiece = () => {
|
|
const prixValue = pieceData.prix
|
|
emit('update', {
|
|
...props.piece,
|
|
...pieceData,
|
|
prix: prixValue && prixValue !== '' ? parseFloat(prixValue) : null,
|
|
constructeurId: props.piece.constructeurId || null
|
|
})
|
|
}
|
|
|
|
const assignPieceModel = (value) => {
|
|
const previousModelId = props.piece.pieceModelId || props.piece.pieceModel?.id || null
|
|
const previousModel = props.piece.pieceModel || null
|
|
props.piece.pieceModelId = value || null
|
|
if (!value) {
|
|
props.piece.pieceModel = null
|
|
}
|
|
emit('assign-model', {
|
|
pieceId: props.piece.id,
|
|
pieceModelId: value || null,
|
|
previousModelId,
|
|
previousModel
|
|
})
|
|
}
|
|
|
|
const updateCustomFieldValue = async (fieldValueId) => {
|
|
const fieldValue = props.piece.customFieldValues?.find(fv => fv.id === 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`)
|
|
} else {
|
|
showError(`Erreur lors de la mise à jour du champ "${fieldValue.customField.name}"`)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Surveiller les changements dans les champs personnalisés
|
|
watch(() => props.piece.customFieldValues, () => {
|
|
console.log('PieceItem - customFieldValues updated:', props.piece.customFieldValues)
|
|
}, { deep: true })
|
|
|
|
watch(
|
|
() => [props.piece.name, props.piece.reference, props.piece.prix],
|
|
() => {
|
|
pieceData.name = props.piece.name || ''
|
|
pieceData.reference = props.piece.reference || ''
|
|
pieceData.prix = props.piece.prix || ''
|
|
}
|
|
)
|
|
|
|
onMounted(() => {
|
|
// Initialiser les données avec les props
|
|
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)
|
|
|
|
if (!documentsLoaded.value) {
|
|
refreshDocuments()
|
|
}
|
|
})
|
|
</script>
|