8 Commits

Author SHA1 Message Date
Matthieu
5ab63e8b27 docs(changelog) : add v1.10.0 release notes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 16:14:08 +01:00
Matthieu
4db832bc8c feat(documents) : add type column, filter, and edit to documents page
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 15:51:26 +01:00
Matthieu
736a8bccf9 feat(documents) : wire DocumentEditModal and type select in all entity pages
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 15:38:30 +01:00
Matthieu
bd69b37524 feat(documents) : add type badge and edit button to DocumentListInline
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 15:33:59 +01:00
Matthieu
e7402dda4d feat(documents) : add DocumentEditModal component
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 15:32:09 +01:00
Matthieu
6b0d2d1b0a feat(documents) : add type select to DocumentUpload component
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 15:32:09 +01:00
Matthieu
7a4a77e3fc feat(documents) : add document type constants and updateDocument method
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 15:31:52 +01:00
Matthieu
2e82e854bf feat(machines) : multi-select site checkboxes, alphabetical sort, OR search param
- Replace site dropdown with inline checkboxes for multi-site filtering
- Sort machines alphabetically (localeCompare fr)
- Switch catalog search from ?name= to ?q= for OR search on name/reference

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 15:15:16 +01:00
17 changed files with 452 additions and 23 deletions

View File

@@ -6,6 +6,12 @@
:documents="componentDocuments"
@close="closePreview"
/>
<DocumentEditModal
:visible="editModalVisible"
:document="editingDocument"
@close="editModalVisible = false"
@updated="handleDocumentUpdated"
/>
<!-- Component Header -->
<div class="flex items-center gap-3 p-3 bg-base-200 rounded-lg cursor-pointer" @click="toggleCollapse">
@@ -208,9 +214,11 @@
<DocumentListInline
:documents="componentDocuments"
:can-delete="isEditMode"
:can-edit="isEditMode"
:delete-disabled="uploadingDocuments"
empty-text="Aucun document lié à ce composant."
@preview="openPreview"
@edit="openEditModal"
@delete="removeDocument"
/>
</div>
@@ -319,6 +327,7 @@ const {
ensureDocumentsLoaded,
handleFilesAdded,
removeDocument,
editDocument,
} = useEntityDocuments({ entity: () => props.component, entityType: 'composant' })
const {
@@ -333,6 +342,21 @@ const {
updateCustomField: updateComponentCustomField,
} = useEntityCustomFields({ entity: () => props.component, entityType: 'composant' })
// --- Document edit modal ---
const editingDocument = ref(null)
const editModalVisible = ref(false)
const openEditModal = (doc) => {
editingDocument.value = doc
editModalVisible.value = true
}
const handleDocumentUpdated = async (data) => {
if (!editingDocument.value?.id) return
await editDocument(editingDocument.value.id, data)
editModalVisible.value = false
editingDocument.value = null
}
// --- Collapse state ---
const isCollapsed = ref(true)

View File

@@ -0,0 +1,90 @@
<template>
<Teleport to="body">
<div v-if="visible" class="modal modal-open" @click.self="$emit('close')">
<div class="modal-box max-w-sm">
<h3 class="font-bold text-lg mb-4">
Modifier le document
</h3>
<div class="space-y-4">
<label class="form-control w-full">
<div class="label">
<span class="label-text">Nom</span>
</div>
<input
v-model="form.name"
type="text"
class="input input-bordered input-sm md:input-md w-full"
>
</label>
<label class="form-control w-full">
<div class="label">
<span class="label-text">Type</span>
</div>
<select
v-model="form.type"
class="select select-bordered select-sm md:select-md w-full"
>
<option v-for="t in DOCUMENT_TYPES" :key="t.value" :value="t.value">
{{ t.label }}
</option>
</select>
</label>
</div>
<div class="modal-action">
<button type="button" class="btn btn-ghost btn-sm md:btn-md" @click="$emit('close')">
Annuler
</button>
<button
type="button"
class="btn btn-primary btn-sm md:btn-md"
:disabled="saving"
@click="save"
>
<span v-if="saving" class="loading loading-spinner loading-xs" />
Sauvegarder
</button>
</div>
</div>
</div>
</Teleport>
</template>
<script setup lang="ts">
import { reactive, watch, ref } from 'vue'
import { DOCUMENT_TYPES } from '~/shared/documentTypes'
import type { Document } from '~/composables/useDocuments'
const props = defineProps<{
visible: boolean
document: Document | null
}>()
const emit = defineEmits<{
(e: 'close'): void
(e: 'updated', data: { name: string; type: string }): void
}>()
const form = reactive({ name: '', type: 'documentation' })
const saving = ref(false)
watch(
() => props.document,
(doc) => {
if (doc) {
form.name = doc.name || ''
form.type = doc.type || 'documentation'
}
},
{ immediate: true },
)
const save = () => {
if (!form.name.trim()) return
saving.value = true
emit('updated', { name: form.name.trim(), type: form.type })
saving.value = false
}
</script>

View File

@@ -34,6 +34,21 @@
@change="onFileChange"
>
<div class="w-full max-w-xs mt-2">
<label class="text-xs font-semibold uppercase tracking-wide text-base-content/70">
Type de document
</label>
<select
class="select select-bordered select-sm w-full mt-1"
:value="documentType"
@change="emit('update:documentType', $event.target.value)"
>
<option v-for="t in DOCUMENT_TYPES" :key="t.value" :value="t.value">
{{ t.label }}
</option>
</select>
</div>
<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">
<div class="flex items-center gap-3">
@@ -69,6 +84,7 @@
<script setup>
import { ref, computed, watch, onBeforeUnmount } from 'vue'
import { useToast } from '~/composables/useToast'
import { DOCUMENT_TYPES } from '~/shared/documentTypes'
import { getFileIcon } from '~/utils/fileIcons'
import IconLucideCloudUpload from '~icons/lucide/cloud-upload'
@@ -96,10 +112,14 @@ const props = defineProps({
maxFileSizeMb: {
type: Number,
default: 200
},
documentType: {
type: String,
default: 'documentation'
}
})
const emit = defineEmits(['update:modelValue', 'files-added'])
const emit = defineEmits(['update:modelValue', 'files-added', 'update:documentType'])
const dragActive = ref(false)
const fileInput = ref(null)

View File

@@ -6,6 +6,12 @@
:documents="pieceDocuments"
@close="closePreview"
/>
<DocumentEditModal
:visible="editModalVisible"
:document="editingDocument"
@close="editModalVisible = false"
@updated="handleDocumentUpdated"
/>
<!-- Piece Header (collapsible, same pattern as ComponentItem) -->
<div class="flex items-start justify-between p-4 bg-base-200 rounded-lg">
@@ -247,9 +253,11 @@
<DocumentListInline
:documents="pieceDocuments"
:can-delete="isEditMode"
:can-edit="isEditMode"
:delete-disabled="uploadingDocuments"
empty-text="Aucun document lié à cette pièce."
@preview="openPreview"
@edit="openEditModal"
@delete="removeDocument"
/>
</div>
@@ -329,6 +337,7 @@ const {
refreshDocuments,
handleFilesAdded,
removeDocument,
editDocument,
} = useEntityDocuments({ entity: () => props.piece, entityType: 'piece' })
const {
@@ -343,6 +352,21 @@ const {
updateCustomField,
} = useEntityCustomFields({ entity: () => props.piece, entityType: 'piece' })
// --- Document edit modal ---
const editingDocument = ref(null)
const editModalVisible = ref(false)
const openEditModal = (doc) => {
editingDocument.value = doc
editModalVisible.value = true
}
const handleDocumentUpdated = async (data) => {
if (!editingDocument.value?.id) return
await editDocument(editingDocument.value.id, data)
editModalVisible.value = false
editingDocument.value = null
}
// --- Collapse state ---
const isCollapsed = ref(true)

View File

@@ -31,8 +31,9 @@
/>
</div>
<div>
<div class="font-medium">
<div class="font-medium flex items-center gap-2">
{{ document.name }}
<span class="badge badge-sm badge-outline">{{ getDocumentTypeLabel(document.type || 'documentation') }}</span>
</div>
<div class="text-xs text-base-content/70">
{{ document.mimeType || 'Inconnu' }} {{ formatSize(document.size) }}
@@ -40,6 +41,15 @@
</div>
</div>
<div class="flex items-center gap-2">
<button
v-if="canEdit"
type="button"
class="btn btn-ghost btn-xs"
title="Modifier"
@click="$emit('edit', document)"
>
Modifier
</button>
<button
type="button"
class="btn btn-ghost btn-xs"
@@ -74,6 +84,7 @@
</template>
<script setup lang="ts">
import { getDocumentTypeLabel } from '~/shared/documentTypes'
import { canPreviewDocument, isImageDocument } from '~/utils/documentPreview'
import {
documentIcon,
@@ -89,10 +100,12 @@ import type { Document } from '~/composables/useDocuments'
withDefaults(defineProps<{
documents: Document[]
canDelete?: boolean
canEdit?: boolean
deleteDisabled?: boolean
emptyText?: string
}>(), {
canDelete: false,
canEdit: false,
deleteDisabled: false,
emptyText: 'Aucun document.',
})
@@ -100,5 +113,6 @@ withDefaults(defineProps<{
defineEmits<{
(e: 'preview', document: Document): void
(e: 'delete', documentId: string): void
(e: 'edit', document: Document): void
}>()
</script>

View File

@@ -135,7 +135,7 @@ export function useComposants() {
params.set('page', String(page))
if (search && search.trim()) {
params.set('name', search.trim())
params.set('q', search.trim())
}
if (typeName && typeName.trim()) {

View File

@@ -11,6 +11,7 @@ export interface Document {
size: number
fileUrl: string
downloadUrl: string
type?: string
/** @deprecated Legacy Base64 data URI — use fileUrl instead */
path?: string
createdAt?: string
@@ -32,6 +33,7 @@ export interface UploadContext {
composantId?: string
productId?: string
pieceId?: string
type?: string
}
export interface DocumentResult {
@@ -47,6 +49,7 @@ interface LoadDocumentsOptions {
orderBy?: string
orderDir?: 'asc' | 'desc'
attachmentFilter?: string
type?: string
force?: boolean
}
@@ -63,7 +66,7 @@ const extractTotal = (payload: unknown, fallbackLength: number): number => {
}
export function useDocuments() {
const { get, postFormData, delete: del } = useApi()
const { get, patch, postFormData, delete: del } = useApi()
const { showError, showSuccess } = useToast()
const loadFromEndpoint = async (
@@ -103,10 +106,11 @@ export function useDocuments() {
orderBy = 'createdAt',
orderDir = 'desc',
attachmentFilter = 'all',
type = 'all',
force = false,
} = options
if (!force && loaded.value && !search && page === 1 && attachmentFilter === 'all') {
if (!force && loaded.value && !search && page === 1 && attachmentFilter === 'all' && type === 'all') {
return { success: true, data: documents.value }
}
@@ -128,6 +132,10 @@ export function useDocuments() {
params.set(`exists[${attachmentFilter}]`, 'true')
}
if (type && type !== 'all') {
params.set('type', type)
}
params.set(`order[${orderBy}]`, orderDir)
const result = await get(`/documents?${params.toString()}`)
@@ -218,6 +226,7 @@ export function useDocuments() {
const formData = new FormData()
formData.append('file', file)
formData.append('name', file.name)
if (context.type) formData.append('type', context.type)
if (context.siteId) formData.append('siteId', context.siteId)
if (context.machineId) formData.append('machineId', context.machineId)
@@ -280,6 +289,33 @@ export function useDocuments() {
}
}
const updateDocument = async (
id: string,
data: { name?: string; type?: string },
): Promise<DocumentResult> => {
loading.value = true
try {
const result = await patch(`/documents/${id}`, data)
if (result.success && result.data) {
const updated = result.data as Document
const index = documents.value.findIndex((doc) => doc.id === id)
if (index !== -1) {
documents.value[index] = { ...documents.value[index], ...updated }
}
showSuccess('Document mis à jour')
return { success: true, data: updated }
}
if (result.error) showError(result.error)
return result as DocumentResult
} catch (error) {
const err = error as Error
showError('Impossible de mettre à jour le document')
return { success: false, error: err.message }
} finally {
loading.value = false
}
}
return {
documents,
total,
@@ -292,6 +328,7 @@ export function useDocuments() {
loadDocumentsByPiece,
loadDocumentsByProduct,
uploadDocuments,
updateDocument,
deleteDocument,
}
}

View File

@@ -17,7 +17,7 @@ export interface EntityDocumentsDeps {
export function useEntityDocuments(deps: EntityDocumentsDeps) {
const { entity, entityType } = deps
const { uploadDocuments, deleteDocument } = useDocuments()
const { uploadDocuments, deleteDocument, updateDocument } = useDocuments()
const loadDocumentsFn = entityType === 'composant'
? useDocuments().loadDocumentsByComponent
@@ -104,6 +104,19 @@ export function useEntityDocuments(deps: EntityDocumentsDeps) {
}
}
const editDocument = async (id: string, data: { name?: string; type?: string }) => {
const result: any = await updateDocument(id, data)
if (result.success) {
const e = entity()
const docs = e.documents || []
const index = docs.findIndex((doc: any) => doc.id === id)
if (index !== -1) {
docs[index] = { ...docs[index], ...data }
}
}
return result
}
return {
documents,
selectedFiles,
@@ -118,5 +131,6 @@ export function useEntityDocuments(deps: EntityDocumentsDeps) {
ensureDocumentsLoaded,
handleFilesAdded,
removeDocument,
editDocument,
}
}

View File

@@ -147,7 +147,7 @@ export function usePieces() {
params.set('page', String(page))
if (search && search.trim()) {
params.set('name', search.trim())
params.set('q', search.trim())
}
if (typeName && typeName.trim()) {

View File

@@ -145,7 +145,7 @@ export function useProducts() {
params.set('page', String(page))
if (search && search.trim()) {
params.set('name', search.trim())
params.set('q', search.trim())
}
if (typeName && typeName.trim()) {

View File

@@ -69,6 +69,29 @@ const badgeClass = (type: ChangeType) => {
}
const releases: Release[] = [
{
version: 'v1.10.0',
date: '2026-03-23',
changes: [
{ type: 'feat', text: 'Serveur MCP (Model Context Protocol) : l\'application expose désormais un serveur MCP permettant l\'intégration avec des assistants IA — outils CRUD complets pour toutes les entités, recherche inventaire, historique, commentaires, champs personnalisés, documents, slots et structure machine' },
{ type: 'feat', text: 'Types de documents : classification des documents par type (Plan, Photo, Fiche technique, Notice, Certificat, Facture, Bon de commande, Autre) avec filtre dédié sur la page documents, sélection du type à l\'upload et possibilité de modifier le type après upload' },
{ type: 'feat', text: 'Filtre sites multi-sélection sur le Parc Machines : remplacement du menu déroulant par des cases à cocher permettant de filtrer sur un ou plusieurs sites simultanément' },
{ type: 'feat', text: 'Tri alphabétique automatique des machines sur le Parc Machines' },
{ type: 'feat', text: 'Recherche par nom OU référence sur les catalogues : la recherche dans les catalogues pièces, composants et produits cherche désormais dans le nom et la référence simultanément (extension Doctrine OR search)' },
{ type: 'feat', text: 'Quantité sur les slots pièces : ajout d\'un champ quantité éditable directement depuis la page d\'édition d\'un composant' },
{ type: 'feat', text: 'Lien rapide vers la catégorie depuis la page d\'édition d\'un composant' },
{ type: 'feat', text: 'Redirection vers la page d\'édition après création d\'un composant, d\'une pièce ou d\'un produit' },
{ type: 'fix', text: 'Correction de la suppression de fournisseurs sur les pièces, composants et produits : la suppression est maintenant persistée correctement' },
{ type: 'fix', text: 'Correction de la création de composants : les sélections de pièces, produits et sous-composants sont maintenant sauvegardées, et les slots squelette sont correctement initialisés' },
{ type: 'fix', text: 'Correction de la perte de données lors de la sauvegarde d\'une catégorie (champs personnalisés et structure)' },
{ type: 'fix', text: 'Correction de la suppression de composants depuis la fiche machine (utilisation du linkId au lieu du composantId)' },
{ type: 'fix', text: 'Amélioration de l\'envoi des fournisseurs en PATCH : le tableau est toujours envoyé pour éviter les pertes' },
{ type: 'fix', text: 'Filtrage serveur des options dans les sélecteurs de slots au lieu du filtrage client' },
{ type: 'fix', text: 'Page d\'édition pièce : rester sur la page après sauvegarde au lieu de rediriger' },
{ type: 'fix', text: 'Messages d\'erreur 409 (conflit) : extraction du champ d\'erreur pour un message compréhensible' },
{ type: 'perf', text: 'Suppression des chargements catalogue redondants sur la page d\'édition composant' },
],
},
{
version: 'v1.9.1',
date: '2026-03-16',

View File

@@ -6,6 +6,12 @@
:documents="componentDocuments"
@close="closePreview"
/>
<DocumentEditModal
:visible="editModalVisible"
:document="editingDocument"
@close="editModalVisible = false"
@updated="handleDocumentUpdated"
/>
<main class="container mx-auto px-6 py-10">
<div v-if="loading" class="flex flex-col items-center gap-4 py-20 text-center">
<span class="loading loading-spinner loading-lg" aria-hidden="true" />
@@ -294,9 +300,11 @@
v-else
:documents="componentDocuments"
:can-delete="canEdit"
:can-edit="true"
:delete-disabled="uploadingDocuments"
empty-text="Aucun document n'est associé à ce composant pour le moment."
@preview="openPreview"
@edit="openEditModal"
@delete="removeDocument"
/>
</div>
@@ -334,10 +342,13 @@
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useRoute } from '#imports'
import { useComponentEdit } from '~/composables/useComponentEdit'
import { useDocuments } from '~/composables/useDocuments'
const route = useRoute()
const { updateDocument } = useDocuments()
const {
component,
@@ -379,4 +390,24 @@ const {
resolveSubcomponentLabel,
formatStructurePreview,
} = useComponentEdit(String(route.params.id))
const editingDocument = ref<any | null>(null)
const editModalVisible = ref(false)
const openEditModal = (doc: any) => {
editingDocument.value = doc
editModalVisible.value = true
}
const handleDocumentUpdated = async (data: { name?: string; type?: string }) => {
if (!editingDocument.value?.id) return
const result = await updateDocument(editingDocument.value.id, data)
if (result.success) {
const idx = componentDocuments.value.findIndex((d: any) => d.id === editingDocument.value?.id)
if (idx !== -1) {
componentDocuments.value[idx] = { ...componentDocuments.value[idx], ...data }
}
}
editModalVisible.value = false
editingDocument.value = null
}
</script>

View File

@@ -7,6 +7,13 @@
@close="closePreview"
/>
<DocumentEditModal
:visible="editModalVisible"
:document="editingDocument"
@close="editModalVisible = false"
@updated="handleDocumentUpdated"
/>
<section class="card bg-base-100 shadow-sm">
<div class="card-body space-y-6">
<DataTable
@@ -55,6 +62,26 @@
<option value="product">Produits</option>
</select>
</div>
<div class="flex items-center gap-2">
<label
class="text-xs font-semibold uppercase tracking-wide text-base-content/70"
for="doc-type-filter"
>
Type
</label>
<select
id="doc-type-filter"
v-model="typeFilter"
class="select select-bordered select-sm"
@change="table.handleFilterChange"
>
<option value="all">Tous</option>
<option v-for="t in DOCUMENT_TYPES" :key="t.value" :value="t.value">
{{ t.label }}
</option>
</select>
</div>
</template>
<template #cell-name="{ row }">
@@ -77,6 +104,10 @@
{{ row.mimeType || 'Inconnu' }}
</template>
<template #cell-type="{ row }">
<span class="badge badge-sm badge-outline">{{ getDocumentTypeLabel(row.type || 'documentation') }}</span>
</template>
<template #cell-size="{ row }">
{{ formatSize(row.size) }}
</template>
@@ -98,6 +129,14 @@
<template #cell-actions="{ row }">
<div class="flex justify-end gap-2">
<button
v-if="canEdit"
class="btn btn-ghost btn-xs"
type="button"
@click="openEditModal(row)"
>
Modifier
</button>
<button
class="btn btn-ghost btn-xs"
type="button"
@@ -123,12 +162,15 @@ import { computed, onMounted, ref, type Ref } from 'vue'
import DataTable from '~/components/common/DataTable.vue'
import { useDocuments } from '~/composables/useDocuments'
import { useDataTable } from '~/composables/useDataTable'
import { usePermissions } from '~/composables/usePermissions'
import { getFileIcon } from '~/utils/fileIcons'
import { canPreviewDocument } from '~/utils/documentPreview'
import { formatFrenchDate } from '~/utils/date'
import { DOCUMENT_TYPES, getDocumentTypeLabel } from '~/shared/documentTypes'
import DocumentPreviewModal from '~/components/DocumentPreviewModal.vue'
const { documents, total, loading, loadDocuments } = useDocuments()
const { documents, total, loading, loadDocuments, updateDocument } = useDocuments()
const { canEdit } = usePermissions()
const table = useDataTable(
{ fetchData: fetchDocuments },
@@ -139,21 +181,26 @@ const table = useDataTable(
persistToUrl: true,
extraParams: {
filter: { default: 'all' },
typeFilter: { default: 'all' },
},
},
)
const attachmentFilter = table.filters.filter as Ref<string>
const typeFilter = table.filters.typeFilter as Ref<string>
const previewDocument = ref<any>(null)
const previewVisible = ref(false)
const editingDocument = ref<any>(null)
const editModalVisible = ref(false)
const documentsOnPage = computed(() => documents.value.length)
const paginationState = table.pagination(total, documentsOnPage)
const columns = [
{ key: 'name', label: 'Nom', sortable: true, sortKey: 'name' },
{ key: 'mimeType', label: 'Type' },
{ key: 'mimeType', label: 'Type MIME' },
{ key: 'type', label: 'Type' },
{ key: 'size', label: 'Taille', sortable: true, sortKey: 'size' },
{ key: 'attachment', label: 'Rattaché à' },
{ key: 'createdAt', label: 'Date', sortable: true, sortKey: 'createdAt' },
@@ -168,6 +215,7 @@ async function fetchDocuments() {
orderBy: table.sortField.value,
orderDir: table.sortDirection.value as 'asc' | 'desc',
attachmentFilter: attachmentFilter.value,
type: typeFilter.value,
force: true,
})
}
@@ -198,6 +246,25 @@ const closePreview = () => {
previewDocument.value = null
}
const openEditModal = (doc: any) => {
editingDocument.value = doc
editModalVisible.value = true
}
const handleDocumentUpdated = async (data: { name: string; type: string }) => {
if (!editingDocument.value?.id) return
const result = await updateDocument(editingDocument.value.id, data)
if (result.success) {
const doc = documents.value.find((d) => d.id === editingDocument.value.id)
if (doc) {
doc.name = data.name
doc.type = data.type
}
}
editModalVisible.value = false
editingDocument.value = null
}
onMounted(() => {
fetchDocuments()
})

View File

@@ -16,16 +16,23 @@
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-control">
<label class="label">
<span class="label-text">Site</span>
<span class="label-text">Sites</span>
</label>
<select v-model="selectedSite" class="select select-bordered">
<option value="">
Tous les sites
</option>
<option v-for="site in sites" :key="site.id" :value="site.id">
{{ site.name }}
</option>
</select>
<div class="flex flex-wrap gap-3">
<label
v-for="site in sites"
:key="site.id"
class="flex items-center gap-2 cursor-pointer"
>
<input
type="checkbox"
class="checkbox checkbox-sm"
:checked="selectedSites.has(site.id)"
@change="selectedSites.has(site.id) ? selectedSites.delete(site.id) : selectedSites.add(site.id)"
>
<span class="text-sm">{{ site.name }}</span>
</label>
</div>
</div>
<div class="form-control">
<label class="label">
@@ -113,7 +120,7 @@
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { ref, reactive, computed, onMounted } from 'vue'
import { useMachines } from '~/composables/useMachines'
import { useSites } from '~/composables/useSites'
import { useToast } from '~/composables/useToast'
@@ -128,7 +135,7 @@ const { machines, loading, loadMachines, deleteMachine } = useMachines()
const { sites, loadSites } = useSites()
const toast = useToast()
const selectedSite = ref('')
const selectedSites = reactive(new Set())
const searchQuery = ref('')
// Enrichir les machines avec les objets site complets
@@ -145,8 +152,8 @@ const enrichedMachines = computed(() => {
const filteredMachines = computed(() => {
let filtered = enrichedMachines.value
if (selectedSite.value) {
filtered = filtered.filter(machine => machine.siteId === selectedSite.value)
if (selectedSites.size > 0) {
filtered = filtered.filter(machine => selectedSites.has(machine.siteId))
}
if (searchQuery.value.trim()) {
@@ -157,6 +164,10 @@ const filteredMachines = computed(() => {
)
}
filtered = [...filtered].sort((a, b) =>
(a.name || '').localeCompare(b.name || '', 'fr')
)
return filtered
})

View File

@@ -6,6 +6,12 @@
:documents="pieceDocuments"
@close="closePreview"
/>
<DocumentEditModal
:visible="editModalVisible"
:document="editingDocument"
@close="editModalVisible = false"
@updated="handleDocumentUpdated"
/>
<main class="container mx-auto px-6 py-10">
<div v-if="loading" class="flex flex-col items-center gap-4 py-20 text-center">
<span class="loading loading-spinner loading-lg" aria-hidden="true" />
@@ -231,9 +237,11 @@
v-else
:documents="pieceDocuments"
:can-delete="canEdit"
:can-edit="true"
:delete-disabled="uploadingDocuments"
empty-text="Aucun document n'est associé à cette pièce pour le moment."
@preview="openPreview"
@edit="openEditModal"
@delete="removeDocument"
/>
</div>
@@ -271,10 +279,13 @@
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useRoute } from '#imports'
import { usePieceEdit } from '~/composables/usePieceEdit'
import { useDocuments } from '~/composables/useDocuments'
const route = useRoute()
const { updateDocument } = useDocuments()
const {
piece,
@@ -310,4 +321,24 @@ const {
submitEdition,
formatPieceStructurePreview,
} = usePieceEdit(String(route.params.id))
const editingDocument = ref<any | null>(null)
const editModalVisible = ref(false)
const openEditModal = (doc: any) => {
editingDocument.value = doc
editModalVisible.value = true
}
const handleDocumentUpdated = async (data: { name?: string; type?: string }) => {
if (!editingDocument.value?.id) return
const result = await updateDocument(editingDocument.value.id, data)
if (result.success) {
const idx = pieceDocuments.value.findIndex((d: any) => d.id === editingDocument.value?.id)
if (idx !== -1) {
pieceDocuments.value[idx] = { ...pieceDocuments.value[idx], ...data }
}
}
editModalVisible.value = false
editingDocument.value = null
}
</script>

View File

@@ -6,6 +6,12 @@
:documents="productDocuments"
@close="closePreview"
/>
<DocumentEditModal
:visible="editModalVisible"
:document="editingDocument"
@close="editModalVisible = false"
@updated="handleDocumentUpdated"
/>
<main class="container mx-auto px-6 py-10">
<div v-if="loading" class="flex flex-col items-center gap-4 py-16 text-center">
<span class="loading loading-spinner loading-lg" aria-hidden="true" />
@@ -167,9 +173,11 @@
v-else
:documents="productDocuments"
:can-delete="canEdit"
:can-edit="true"
:delete-disabled="uploadingDocuments || saving"
empty-text="Aucun document n'est associé à ce produit pour le moment."
@preview="openPreview"
@edit="openEditModal"
@delete="removeDocument"
/>
</div>
@@ -244,6 +252,7 @@ const {
loadDocumentsByProduct,
uploadDocuments: uploadProductDocuments,
deleteDocument: deleteProductDocument,
updateDocument,
} = useDocuments()
const { ensureConstructeurs } = useConstructeurs()
const {
@@ -265,6 +274,8 @@ const loadingDocuments = ref(false)
const productDocuments = ref<any[]>([])
const previewDocument = ref<any | null>(null)
const previewVisible = ref(false)
const editingDocument = ref<any | null>(null)
const editModalVisible = ref(false)
const historyFieldLabels: Record<string, string> = {
name: 'Nom',
@@ -307,6 +318,23 @@ const openPreview = (doc: any) => {
}
const closePreview = () => { previewVisible.value = false; previewDocument.value = null }
const openEditModal = (doc: any) => {
editingDocument.value = doc
editModalVisible.value = true
}
const handleDocumentUpdated = async (data: { name?: string; type?: string }) => {
if (!editingDocument.value?.id) return
const result = await updateDocument(editingDocument.value.id, data)
if (result.success) {
const idx = productDocuments.value.findIndex((d: any) => d.id === editingDocument.value?.id)
if (idx !== -1) {
productDocuments.value[idx] = { ...productDocuments.value[idx], ...data }
}
}
editModalVisible.value = false
editingDocument.value = null
}
const loadProduct = async () => {
const id = route.params.id
if (!id || typeof id !== 'string') {

View File

@@ -0,0 +1,15 @@
export const DOCUMENT_TYPES = [
{ value: 'documentation', label: 'Documentation' },
{ value: 'devis', label: 'Devis' },
{ value: 'facture', label: 'Facture' },
{ value: 'plan', label: 'Plan' },
{ value: 'photo', label: 'Photo' },
{ value: 'autre', label: 'Autre' },
] as const
export type DocumentTypeValue = (typeof DOCUMENT_TYPES)[number]['value']
export const getDocumentTypeLabel = (value: string): string => {
const found = DOCUMENT_TYPES.find((t) => t.value === value)
return found?.label ?? value
}