994 lines
28 KiB
Vue
994 lines
28 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="displayedCustomFields.length"
|
|
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="(field, index) in displayedCustomFields"
|
|
:key="resolveFieldKey(field, index)"
|
|
class="form-control"
|
|
>
|
|
<label class="label">
|
|
<span class="label-text text-sm">{{
|
|
resolveFieldName(field)
|
|
}}</span>
|
|
<span
|
|
v-if="resolveFieldRequired(field)"
|
|
class="label-text-alt text-error"
|
|
>*</span
|
|
>
|
|
</label>
|
|
|
|
<!-- Mode édition -->
|
|
<template v-if="isEditMode && !resolveFieldReadOnly(field)">
|
|
<!-- Champ de type TEXT -->
|
|
<input
|
|
v-if="resolveFieldType(field) === 'text'"
|
|
:value="field.value ?? ''"
|
|
type="text"
|
|
class="input input-bordered input-sm"
|
|
:required="resolveFieldRequired(field)"
|
|
@input="
|
|
setCustomFieldValue(
|
|
resolveFieldId(field),
|
|
$event.target.value,
|
|
field
|
|
)
|
|
"
|
|
@blur="updateCustomFieldValue(resolveFieldId(field), field)"
|
|
/>
|
|
|
|
<!-- Champ de type NUMBER -->
|
|
<input
|
|
v-else-if="resolveFieldType(field) === 'number'"
|
|
:value="field.value ?? ''"
|
|
type="number"
|
|
class="input input-bordered input-sm"
|
|
:required="resolveFieldRequired(field)"
|
|
@input="
|
|
setCustomFieldValue(
|
|
resolveFieldId(field),
|
|
$event.target.value,
|
|
field
|
|
)
|
|
"
|
|
@blur="updateCustomFieldValue(resolveFieldId(field), field)"
|
|
/>
|
|
|
|
<!-- Champ de type SELECT -->
|
|
<select
|
|
v-else-if="resolveFieldType(field) === 'select'"
|
|
:value="field.value ?? ''"
|
|
class="select select-bordered select-sm"
|
|
:required="resolveFieldRequired(field)"
|
|
@change="
|
|
(event) =>
|
|
setCustomFieldValue(
|
|
resolveFieldId(field),
|
|
event.target.value,
|
|
field
|
|
)
|
|
"
|
|
@blur="updateCustomFieldValue(resolveFieldId(field), field)"
|
|
>
|
|
<option value="">Sélectionner...</option>
|
|
<option
|
|
v-for="option in resolveFieldOptions(field)"
|
|
:key="option"
|
|
:value="option"
|
|
>
|
|
{{ option }}
|
|
</option>
|
|
</select>
|
|
|
|
<!-- Champ de type BOOLEAN -->
|
|
<div
|
|
v-else-if="resolveFieldType(field) === 'boolean'"
|
|
class="flex items-center gap-2"
|
|
>
|
|
<input
|
|
:value="field.value ?? ''"
|
|
type="checkbox"
|
|
class="checkbox checkbox-sm"
|
|
:checked="String(field.value).toLowerCase() === 'true'"
|
|
@change="
|
|
setCustomFieldValue(
|
|
resolveFieldId(field),
|
|
$event.target.checked ? 'true' : 'false',
|
|
field
|
|
)
|
|
"
|
|
@blur="updateCustomFieldValue(resolveFieldId(field), field)"
|
|
/>
|
|
<span class="text-sm">{{
|
|
String(field.value).toLowerCase() === "true" ? "Oui" : "Non"
|
|
}}</span>
|
|
</div>
|
|
|
|
<!-- Champ de type DATE -->
|
|
<input
|
|
v-else-if="resolveFieldType(field) === 'date'"
|
|
:value="field.value ?? ''"
|
|
type="date"
|
|
class="input input-bordered input-sm"
|
|
:required="resolveFieldRequired(field)"
|
|
@input="
|
|
setCustomFieldValue(
|
|
resolveFieldId(field),
|
|
$event.target.value,
|
|
field
|
|
)
|
|
"
|
|
@blur="updateCustomFieldValue(resolveFieldId(field), field)"
|
|
/>
|
|
</template>
|
|
|
|
<!-- Mode lecture seule -->
|
|
<template v-else>
|
|
<div class="input input-bordered input-sm bg-base-200">
|
|
{{ formatFieldDisplayValue(field) }}
|
|
</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 || []);
|
|
function fieldKeyFromNameAndType(name, type) {
|
|
const normalizedName = typeof name === 'string' ? name : '';
|
|
const normalizedType = typeof type === 'string' ? type : '';
|
|
return normalizedName ? `${normalizedName}::${normalizedType}` : null;
|
|
}
|
|
|
|
function mergeFieldDefinitionsWithValues(definitions, values) {
|
|
const definitionList = Array.isArray(definitions) ? definitions : [];
|
|
const valueList = Array.isArray(values) ? values : [];
|
|
|
|
const valueMap = new Map();
|
|
valueList.forEach((entry) => {
|
|
if (!entry || typeof entry !== 'object') {
|
|
return;
|
|
}
|
|
const fieldId = entry.customField?.id ?? entry.customFieldId ?? null;
|
|
if (fieldId) {
|
|
valueMap.set(fieldId, entry);
|
|
}
|
|
const nameKey = fieldKeyFromNameAndType(
|
|
entry.customField?.name,
|
|
entry.customField?.type,
|
|
);
|
|
if (nameKey) {
|
|
valueMap.set(nameKey, entry);
|
|
}
|
|
});
|
|
|
|
const merged = definitionList.map((field) => {
|
|
if (!field || typeof field !== 'object') {
|
|
return field;
|
|
}
|
|
|
|
const fieldId = resolveCustomFieldId(field);
|
|
const nameKey = fieldKeyFromNameAndType(
|
|
resolveFieldName(field),
|
|
resolveFieldType(field),
|
|
);
|
|
|
|
const matchedValue =
|
|
(fieldId ? valueMap.get(fieldId) : undefined) ??
|
|
(nameKey ? valueMap.get(nameKey) : undefined);
|
|
|
|
if (!matchedValue) {
|
|
return {
|
|
...field,
|
|
value: field?.value ?? '',
|
|
};
|
|
}
|
|
|
|
return {
|
|
...field,
|
|
customFieldValueId: matchedValue.id ?? field.customFieldValueId ?? null,
|
|
customFieldId:
|
|
matchedValue.customField?.id ??
|
|
matchedValue.customFieldId ??
|
|
fieldId ??
|
|
null,
|
|
customField: matchedValue.customField ?? field.customField ?? null,
|
|
value: matchedValue.value ?? field.value ?? '',
|
|
};
|
|
});
|
|
|
|
valueList.forEach((entry) => {
|
|
if (!entry || typeof entry !== 'object') {
|
|
return;
|
|
}
|
|
|
|
const fieldId = entry.customField?.id ?? entry.customFieldId ?? null;
|
|
const nameKey = fieldKeyFromNameAndType(
|
|
entry.customField?.name,
|
|
entry.customField?.type,
|
|
);
|
|
|
|
const exists = merged.some((field) => {
|
|
if (!field || typeof field !== 'object') {
|
|
return false;
|
|
}
|
|
if (field.customFieldValueId && field.customFieldValueId === entry.id) {
|
|
return true;
|
|
}
|
|
const existingId = resolveCustomFieldId(field);
|
|
if (fieldId && existingId && existingId === fieldId) {
|
|
return true;
|
|
}
|
|
if (!fieldId && nameKey) {
|
|
return (
|
|
fieldKeyFromNameAndType(
|
|
resolveFieldName(field),
|
|
resolveFieldType(field),
|
|
) === nameKey
|
|
);
|
|
}
|
|
return false;
|
|
});
|
|
|
|
if (!exists) {
|
|
merged.push({
|
|
customFieldValueId: entry.id ?? null,
|
|
customFieldId: fieldId,
|
|
name: entry.customField?.name ?? '',
|
|
type: entry.customField?.type ?? 'text',
|
|
required: entry.customField?.required ?? false,
|
|
options: entry.customField?.options ?? [],
|
|
value: entry.value ?? '',
|
|
customField: entry.customField ?? null,
|
|
});
|
|
}
|
|
});
|
|
|
|
return merged;
|
|
}
|
|
|
|
const displayedCustomFields = computed(() =>
|
|
mergeFieldDefinitionsWithValues(
|
|
props.piece.customFields,
|
|
props.piece.customFieldValues,
|
|
),
|
|
);
|
|
|
|
const candidateCustomFields = computed(() => {
|
|
const sources = [
|
|
props.piece.customFieldValues?.map((value) => value?.customField),
|
|
props.piece.typePiece?.customFields,
|
|
props.piece.typeMachinePieceRequirement?.typePiece?.customFields,
|
|
props.piece.typeMachinePieceRequirement?.customFields,
|
|
props.piece.pieceModel?.customFields,
|
|
props.piece.customFields,
|
|
];
|
|
|
|
const map = new Map();
|
|
sources.forEach((collection) => {
|
|
if (!Array.isArray(collection)) {
|
|
return;
|
|
}
|
|
collection.forEach((item) => {
|
|
if (!item || typeof item !== 'object') {
|
|
return;
|
|
}
|
|
const id = item.id || item.customFieldId;
|
|
const name = typeof item.name === 'string' ? item.name : null;
|
|
const key = id || (name ? `${name}::${item.type ?? ''}` : null);
|
|
if (!key || map.has(key)) {
|
|
return;
|
|
}
|
|
map.set(key, item);
|
|
});
|
|
});
|
|
|
|
return Array.from(map.values());
|
|
});
|
|
|
|
const handleConstructeurChange = (value) => {
|
|
props.piece.constructeurId = value;
|
|
updatePiece();
|
|
};
|
|
|
|
const { uploadDocuments, deleteDocument, loadDocumentsByPiece } =
|
|
useDocuments();
|
|
const {
|
|
updateCustomFieldValue: updateCustomFieldValueApi,
|
|
upsertCustomFieldValue,
|
|
} = useCustomFields();
|
|
const { showSuccess, showError } = useToast();
|
|
|
|
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
|
|
function resolveFieldKey(field, index) {
|
|
return field?.id ??
|
|
field?.customFieldValueId ??
|
|
field?.customFieldId ??
|
|
field?.name ??
|
|
`field-${index}`
|
|
}
|
|
|
|
function resolveFieldId(field) {
|
|
return field?.customFieldValueId ?? null
|
|
}
|
|
|
|
function resolveFieldName(field) {
|
|
return field?.name ?? 'Champ'
|
|
}
|
|
|
|
function resolveFieldType(field) {
|
|
return field?.type ?? 'text'
|
|
}
|
|
|
|
function resolveFieldOptions(field) {
|
|
return field?.options ?? []
|
|
}
|
|
|
|
function resolveFieldRequired(field) {
|
|
return !!field?.required
|
|
}
|
|
|
|
function resolveFieldReadOnly(field) {
|
|
return !!field?.readOnly
|
|
}
|
|
|
|
function buildCustomFieldMetadata(field) {
|
|
return {
|
|
customFieldName: resolveFieldName(field),
|
|
customFieldType: resolveFieldType(field),
|
|
customFieldRequired: resolveFieldRequired(field),
|
|
customFieldOptions: resolveFieldOptions(field)
|
|
}
|
|
}
|
|
|
|
function resolveCustomFieldId(field) {
|
|
return field?.customFieldId ?? field?.id ?? field?.customField?.id ?? null;
|
|
}
|
|
|
|
function ensureCustomFieldId(field) {
|
|
const existingId = resolveCustomFieldId(field);
|
|
if (existingId) {
|
|
return existingId;
|
|
}
|
|
|
|
const name = resolveFieldName(field);
|
|
if (!name || name === "Champ") {
|
|
return null;
|
|
}
|
|
|
|
const matches = candidateCustomFields.value.filter((candidate) => {
|
|
if (!candidate || typeof candidate !== "object") {
|
|
return false;
|
|
}
|
|
const candidateId = candidate.id || candidate.customFieldId;
|
|
if (candidateId && (candidateId === field?.id || candidateId === field?.customFieldId)) {
|
|
return true;
|
|
}
|
|
return typeof candidate.name === "string" && candidate.name === name;
|
|
});
|
|
|
|
if (matches.length) {
|
|
const withId = matches.find((candidate) => candidate?.id || candidate?.customFieldId) || matches[0];
|
|
const id = withId?.id || withId?.customFieldId || null;
|
|
if (id) {
|
|
field.customFieldId = id;
|
|
}
|
|
if (!field.customField && typeof withId === "object") {
|
|
field.customField = withId;
|
|
}
|
|
return id;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
watch(
|
|
candidateCustomFields,
|
|
() => {
|
|
const fields = displayedCustomFields.value || []
|
|
fields.forEach((field) => field && ensureCustomFieldId(field))
|
|
},
|
|
{ immediate: true, deep: true }
|
|
)
|
|
|
|
watch(
|
|
displayedCustomFields,
|
|
(fields) => {
|
|
(fields || []).forEach((field) => field && ensureCustomFieldId(field))
|
|
},
|
|
{ immediate: true, deep: true }
|
|
)
|
|
|
|
const formatFieldDisplayValue = (field) => {
|
|
const type = resolveFieldType(field);
|
|
const rawValue = field?.value ?? "";
|
|
if (type === "boolean") {
|
|
const normalized = String(rawValue).toLowerCase();
|
|
if (normalized === "true") return "Oui";
|
|
if (normalized === "false") return "Non";
|
|
}
|
|
return rawValue || "Non défini";
|
|
};
|
|
|
|
const setCustomFieldValue = (fieldValueId, value, field) => {
|
|
if (resolveFieldReadOnly(field)) {
|
|
return;
|
|
}
|
|
if (field && typeof field === "object") {
|
|
field.value = value;
|
|
}
|
|
if (!fieldValueId) {
|
|
return;
|
|
}
|
|
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, field) => {
|
|
if (!field || resolveFieldReadOnly(field)) {
|
|
return;
|
|
}
|
|
|
|
const fieldValue = fieldValueId
|
|
? props.piece.customFieldValues?.find((fv) => fv.id === fieldValueId)
|
|
: undefined;
|
|
|
|
if (fieldValue) {
|
|
const result = await updateCustomFieldValueApi(fieldValueId, {
|
|
value: fieldValue.value,
|
|
});
|
|
if (result.success) {
|
|
if (fieldValue.customField?.id) {
|
|
field.customFieldId = fieldValue.customField.id;
|
|
field.customField = fieldValue.customField;
|
|
}
|
|
showSuccess(
|
|
`Champ "${fieldValue.customField.name}" mis à jour avec succès`
|
|
);
|
|
emit("custom-field-update", {
|
|
fieldId: fieldValue.customField?.id ?? ensureCustomFieldId(field),
|
|
pieceId: props.piece.id,
|
|
value: fieldValue.value,
|
|
});
|
|
} else {
|
|
showError(
|
|
`Erreur lors de la mise à jour du champ "${fieldValue.customField.name}"`
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
const customFieldId = ensureCustomFieldId(field);
|
|
const fieldName = resolveFieldName(field);
|
|
if (!props.piece?.id) {
|
|
showError("Impossible de créer la valeur pour ce champ de pièce");
|
|
return;
|
|
}
|
|
|
|
if (!customFieldId && (!fieldName || fieldName === "Champ")) {
|
|
showError("Impossible de créer la valeur pour ce champ de pièce");
|
|
return;
|
|
}
|
|
|
|
const metadata = customFieldId ? undefined : buildCustomFieldMetadata(field);
|
|
|
|
const result = await upsertCustomFieldValue(
|
|
customFieldId,
|
|
"piece",
|
|
props.piece.id,
|
|
field.value ?? "",
|
|
metadata
|
|
);
|
|
|
|
if (result.success) {
|
|
const created = result.data;
|
|
if (created?.id) {
|
|
field.customFieldValueId = created.id;
|
|
field.value = created.value ?? field.value ?? "";
|
|
if (created.customField?.id) {
|
|
field.customFieldId = created.customField.id;
|
|
field.customField = created.customField;
|
|
}
|
|
|
|
if (Array.isArray(props.piece.customFieldValues)) {
|
|
const index = props.piece.customFieldValues.findIndex(
|
|
(value) => value.id === created.id
|
|
);
|
|
if (index !== -1) {
|
|
props.piece.customFieldValues.splice(index, 1, created);
|
|
} else {
|
|
props.piece.customFieldValues.push(created);
|
|
}
|
|
} else {
|
|
props.piece.customFieldValues = [created];
|
|
}
|
|
}
|
|
|
|
showSuccess(`Champ "${resolveFieldName(field)}" créé avec succès`);
|
|
emit("custom-field-update", {
|
|
fieldId:
|
|
created?.customField?.id ??
|
|
created?.customFieldId ??
|
|
ensureCustomFieldId(field),
|
|
pieceId: props.piece.id,
|
|
value: field.value ?? "",
|
|
});
|
|
|
|
const definitions = Array.isArray(props.piece.customFields)
|
|
? [...props.piece.customFields]
|
|
: [];
|
|
const fieldIdentifier = ensureCustomFieldId(field);
|
|
const existingIndex = definitions.findIndex((definition) => {
|
|
const definitionId = ensureCustomFieldId(definition);
|
|
if (fieldIdentifier && definitionId) {
|
|
return definitionId === fieldIdentifier;
|
|
}
|
|
return definition?.name === resolveFieldName(field);
|
|
});
|
|
|
|
const updatedDefinition = {
|
|
...(existingIndex !== -1 ? definitions[existingIndex] : {}),
|
|
customFieldValueId: field.customFieldValueId,
|
|
customFieldId: fieldIdentifier,
|
|
name: resolveFieldName(field),
|
|
type: resolveFieldType(field),
|
|
required: resolveFieldRequired(field),
|
|
options: resolveFieldOptions(field),
|
|
value: field.value ?? "",
|
|
customField: field.customField ?? null,
|
|
};
|
|
|
|
if (existingIndex !== -1) {
|
|
definitions.splice(existingIndex, 1, updatedDefinition);
|
|
} else {
|
|
definitions.push(updatedDefinition);
|
|
}
|
|
|
|
props.piece.customFields = definitions;
|
|
} else {
|
|
showError(
|
|
`Erreur lors de la sauvegarde du champ "${resolveFieldName(field)}"`
|
|
);
|
|
}
|
|
};
|
|
|
|
// Surveiller les changements dans les champs personnalisés
|
|
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 || "";
|
|
if (!documentsLoaded.value) {
|
|
refreshDocuments();
|
|
}
|
|
});
|
|
</script>
|