Rework CSS theme (app.css), navbar layout, dashboard page, machine detail, catalog pages, and various form/display components for better consistency and mobile responsiveness. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
437 lines
16 KiB
Vue
437 lines
16 KiB
Vue
<template>
|
|
<div class="space-y-4">
|
|
<DocumentPreviewModal
|
|
:document="previewDocument"
|
|
:visible="previewVisible"
|
|
:documents="componentDocuments"
|
|
@close="closePreview"
|
|
/>
|
|
|
|
<!-- Component Header -->
|
|
<div class="flex items-start justify-between p-4 bg-base-200 rounded-lg">
|
|
<div class="flex items-start gap-3 w-full">
|
|
<button
|
|
type="button"
|
|
class="btn btn-ghost btn-sm btn-circle shrink-0 transition-transform"
|
|
:class="{ 'rotate-90': !isCollapsed }"
|
|
: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>
|
|
<div class="flex flex-wrap gap-2 mt-2">
|
|
<span v-if="component.reference" class="badge badge-outline badge-sm">{{ component.reference }}</span>
|
|
<template v-if="componentConstructeursDisplay.length">
|
|
<span
|
|
v-for="constructeur in componentConstructeursDisplay"
|
|
:key="constructeur.id"
|
|
class="badge badge-outline badge-sm"
|
|
>
|
|
{{ constructeur.name }}
|
|
</span>
|
|
</template>
|
|
<span v-if="component.prix" class="badge badge-primary badge-sm">{{ component.prix }}€</span>
|
|
<span
|
|
v-if="displayProductName"
|
|
class="badge badge-info badge-sm"
|
|
>
|
|
Produit : {{ displayProductName }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-show="!isCollapsed" class="space-y-4">
|
|
<!-- Component Info Display - Editable or Read-only -->
|
|
<div class="p-4 bg-base-100 border border-base-200 rounded-lg">
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text font-medium">Nom</span></label>
|
|
<input
|
|
v-if="isEditMode"
|
|
v-model="component.name"
|
|
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>
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text font-medium">Référence</span></label>
|
|
<input
|
|
v-if="isEditMode"
|
|
v-model="component.reference"
|
|
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>
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text font-medium">Prix</span></label>
|
|
<input
|
|
v-if="isEditMode"
|
|
v-model="component.prix"
|
|
type="number"
|
|
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>
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text font-medium">Fournisseur</span></label>
|
|
<ConstructeurSelect
|
|
v-if="isEditMode"
|
|
class="w-full"
|
|
:model-value="componentConstructeurIds"
|
|
:initial-options="componentConstructeursDisplay"
|
|
@update:model-value="handleConstructeurChange"
|
|
/>
|
|
<div v-else class="input input-bordered input-sm bg-base-200">
|
|
<div v-if="componentConstructeursDisplay.length" class="space-y-1">
|
|
<div
|
|
v-for="constructeur in componentConstructeursDisplay"
|
|
:key="constructeur.id"
|
|
class="flex flex-col"
|
|
>
|
|
<span class="font-medium">{{ constructeur.name }}</span>
|
|
<span
|
|
v-if="formatConstructeurContact(constructeur)"
|
|
class="text-xs text-base-content/50"
|
|
>
|
|
{{ formatConstructeurContact(constructeur) }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<span v-else class="font-medium">Non défini</span>
|
|
</div>
|
|
</div>
|
|
<div class="form-control md:col-span-2">
|
|
<label class="label">
|
|
<span class="label-text font-medium">Produit catalogue</span>
|
|
</label>
|
|
<div class="input input-bordered input-sm bg-base-200 min-h-[2.75rem] flex flex-col justify-center space-y-1">
|
|
<template v-if="displayProduct">
|
|
<span class="font-semibold text-base-content">
|
|
{{ displayProductName || 'Produit catalogue' }}
|
|
</span>
|
|
<span
|
|
v-for="info in productInfoRows"
|
|
:key="info.label"
|
|
class="text-xs text-base-content/70"
|
|
>
|
|
{{ info.label }} : {{ info.value }}
|
|
</span>
|
|
<NuxtLink
|
|
v-if="component.product?.id"
|
|
:to="`/product/${component.product.id}/edit`"
|
|
class="link link-primary text-xs"
|
|
>
|
|
Ouvrir la fiche produit
|
|
</NuxtLink>
|
|
</template>
|
|
<span v-else class="font-medium">Non défini</span>
|
|
</div>
|
|
<div
|
|
v-if="productDocuments.length"
|
|
class="mt-2 space-y-2 rounded-md border border-base-200 bg-base-100 p-3 text-xs"
|
|
>
|
|
<h4 class="font-medium text-base-content">
|
|
Documents du produit
|
|
</h4>
|
|
<div
|
|
v-for="document in productDocuments"
|
|
:key="document.id || document.path || document.name"
|
|
class="flex items-center justify-between gap-3 rounded border border-base-200 bg-base-100 px-3 py-2"
|
|
>
|
|
<div class="flex items-center gap-3">
|
|
<div
|
|
class="flex-shrink-0 overflow-hidden rounded-md border border-base-200 bg-base-200/70 flex items-center justify-center h-12 w-10"
|
|
>
|
|
<img
|
|
v-if="isImageDocument(document) && (document.fileUrl || document.path)"
|
|
:src="document.fileUrl || document.path"
|
|
class="h-full w-full object-cover"
|
|
:alt="`Aperçu de ${document.name}`"
|
|
>
|
|
<iframe
|
|
v-else-if="shouldInlinePdf(document)"
|
|
:src="documentPreviewSrc(document)"
|
|
class="h-full w-full border-0 bg-white"
|
|
title="Aperçu PDF"
|
|
/>
|
|
<component
|
|
v-else
|
|
:is="documentIcon(document).component"
|
|
class="h-6 w-6"
|
|
:class="documentIcon(document).colorClass"
|
|
aria-hidden="true"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<div class="font-medium text-base-content">
|
|
{{ document.name }}
|
|
</div>
|
|
<div class="text-xs text-base-content/70">
|
|
{{ document.mimeType || 'Inconnu' }} • {{ formatSize(document.size) }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center gap-2 text-xs">
|
|
<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>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- Custom Fields Display - Editable or Read-only -->
|
|
<CustomFieldDisplay
|
|
:fields="displayedCustomFields"
|
|
:is-edit-mode="isEditMode"
|
|
:columns="2"
|
|
@field-blur="updateComponentCustomField"
|
|
/>
|
|
|
|
<div class="mt-4 pt-4 border-t border-base-200 space-y-3">
|
|
<div class="flex items-center justify-between">
|
|
<h4 class="font-semibold text-sm text-base-content/80">
|
|
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-base-content/50">
|
|
Chargement des documents...
|
|
</p>
|
|
|
|
<DocumentUpload
|
|
v-if="isEditMode"
|
|
v-model="selectedFiles"
|
|
title="Déposer des fichiers pour ce composant"
|
|
subtitle="Formats acceptés : PDF, images, documents..."
|
|
@files-added="handleFilesAdded"
|
|
/>
|
|
|
|
<DocumentListInline
|
|
:documents="componentDocuments"
|
|
:can-delete="isEditMode"
|
|
:delete-disabled="uploadingDocuments"
|
|
empty-text="Aucun document lié à ce composant."
|
|
@preview="openPreview"
|
|
@delete="removeDocument"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Component Pieces -->
|
|
<div v-if="component.pieces && component.pieces.length > 0" class="space-y-2">
|
|
<h4 class="font-semibold text-base-content/80">
|
|
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"
|
|
:key="piece.id"
|
|
:piece="piece"
|
|
:is-edit-mode="isEditMode"
|
|
@update="updatePiece"
|
|
@edit="editPiece"
|
|
@custom-field-update="updatePieceCustomField"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Sub Components -->
|
|
<div v-if="childComponents.length > 0" class="space-y-3">
|
|
<h4 class="font-semibold text-base-content/80">
|
|
Sous-composants
|
|
</h4>
|
|
<div class="space-y-3 pl-4 border-l-2 border-base-200">
|
|
<ComponentItem
|
|
v-for="subComponent in childComponents"
|
|
:key="subComponent.id"
|
|
:component="subComponent"
|
|
:is-edit-mode="isEditMode"
|
|
:collapse-all="collapseAll"
|
|
:toggle-token="toggleToken"
|
|
@update="$emit('update', $event)"
|
|
@edit-piece="$emit('edit-piece', $event)"
|
|
@custom-field-update="$emit('custom-field-update', $event)"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, watch, computed } from 'vue'
|
|
import PieceItem from './PieceItem.vue'
|
|
import DocumentUpload from './DocumentUpload.vue'
|
|
import ConstructeurSelect from './ConstructeurSelect.vue'
|
|
import DocumentPreviewModal from '~/components/DocumentPreviewModal.vue'
|
|
import IconLucideChevronRight from '~icons/lucide/chevron-right'
|
|
import { canPreviewDocument, isImageDocument } from '~/utils/documentPreview'
|
|
import { useConstructeurs } from '~/composables/useConstructeurs'
|
|
import {
|
|
formatConstructeurContact as formatConstructeurContactSummary,
|
|
resolveConstructeurs,
|
|
uniqueConstructeurIds,
|
|
} from '~/shared/constructeurUtils'
|
|
import {
|
|
formatSize,
|
|
shouldInlinePdf,
|
|
documentPreviewSrc,
|
|
documentIcon,
|
|
downloadDocument,
|
|
} from '~/shared/utils/documentDisplayUtils'
|
|
import { useEntityDocuments } from '~/composables/useEntityDocuments'
|
|
import { useEntityProductDisplay } from '~/composables/useEntityProductDisplay'
|
|
import { useEntityCustomFields } from '~/composables/useEntityCustomFields'
|
|
|
|
const props = defineProps({
|
|
component: { type: Object, required: true },
|
|
isEditMode: { type: Boolean, default: false },
|
|
collapseAll: { type: Boolean, default: true },
|
|
toggleToken: { type: Number, default: 0 },
|
|
})
|
|
|
|
const emit = defineEmits(['update', 'edit-piece', 'custom-field-update'])
|
|
|
|
// --- Shared composables ---
|
|
const {
|
|
documents: componentDocuments,
|
|
selectedFiles,
|
|
uploadingDocuments,
|
|
loadingDocuments,
|
|
previewDocument,
|
|
previewVisible,
|
|
openPreview,
|
|
closePreview,
|
|
ensureDocumentsLoaded,
|
|
handleFilesAdded,
|
|
removeDocument,
|
|
} = useEntityDocuments({ entity: () => props.component, entityType: 'composant' })
|
|
|
|
const {
|
|
displayProduct,
|
|
displayProductName,
|
|
productInfoRows,
|
|
productDocuments,
|
|
} = useEntityProductDisplay({ entity: () => props.component })
|
|
|
|
const {
|
|
displayedCustomFields,
|
|
updateCustomField: updateComponentCustomField,
|
|
} = useEntityCustomFields({ entity: () => props.component, entityType: 'composant' })
|
|
|
|
// --- Collapse state ---
|
|
const isCollapsed = ref(true)
|
|
|
|
watch(
|
|
() => props.toggleToken,
|
|
() => {
|
|
isCollapsed.value = props.collapseAll
|
|
if (!isCollapsed.value) ensureDocumentsLoaded()
|
|
},
|
|
{ immediate: true },
|
|
)
|
|
|
|
const toggleCollapse = () => {
|
|
isCollapsed.value = !isCollapsed.value
|
|
if (!isCollapsed.value) ensureDocumentsLoaded()
|
|
}
|
|
|
|
// --- Child components ---
|
|
const childComponents = computed(() => {
|
|
const list = props.component.subcomponents || props.component.subComponents || []
|
|
return Array.isArray(list) ? list : []
|
|
})
|
|
|
|
// --- Constructeurs ---
|
|
const { constructeurs } = useConstructeurs()
|
|
|
|
const componentConstructeurIds = computed(() =>
|
|
uniqueConstructeurIds(
|
|
props.component,
|
|
Array.isArray(props.component.constructeurs) ? props.component.constructeurs : [],
|
|
props.component.constructeur ? [props.component.constructeur] : [],
|
|
),
|
|
)
|
|
|
|
const componentConstructeursDisplay = computed(() =>
|
|
resolveConstructeurs(
|
|
componentConstructeurIds.value,
|
|
Array.isArray(props.component.constructeurs) ? props.component.constructeurs : [],
|
|
props.component.constructeur ? [props.component.constructeur] : [],
|
|
constructeurs.value,
|
|
),
|
|
)
|
|
|
|
const formatConstructeurContact = (constructeur) =>
|
|
formatConstructeurContactSummary(constructeur)
|
|
|
|
const handleConstructeurChange = async (value) => {
|
|
const ids = uniqueConstructeurIds(value)
|
|
props.component.constructeurIds = [...ids]
|
|
props.component.constructeurId = null
|
|
props.component.constructeur = null
|
|
props.component.constructeurs = resolveConstructeurs(
|
|
ids,
|
|
constructeurs.value,
|
|
Array.isArray(props.component.constructeurs) ? props.component.constructeurs : [],
|
|
)
|
|
await updateComponent()
|
|
}
|
|
|
|
// --- Update / Event forwarding ---
|
|
const updateComponent = () => {
|
|
emit('update', {
|
|
...props.component,
|
|
constructeurIds: componentConstructeurIds.value,
|
|
})
|
|
}
|
|
|
|
const updatePiece = (updatedPiece) => {
|
|
emit('edit-piece', updatedPiece)
|
|
}
|
|
|
|
const editPiece = (piece) => {
|
|
emit('edit-piece', piece)
|
|
}
|
|
|
|
const updatePieceCustomField = (fieldUpdate) => {
|
|
emit('custom-field-update', fieldUpdate)
|
|
emit('edit-piece', { ...fieldUpdate, type: 'custom-field-update' })
|
|
}
|
|
</script>
|