Compare commits
6 Commits
2e82e854bf
...
4db832bc8c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4db832bc8c | ||
|
|
736a8bccf9 | ||
|
|
bd69b37524 | ||
|
|
e7402dda4d | ||
|
|
6b0d2d1b0a | ||
|
|
7a4a77e3fc |
@@ -6,6 +6,12 @@
|
|||||||
:documents="componentDocuments"
|
:documents="componentDocuments"
|
||||||
@close="closePreview"
|
@close="closePreview"
|
||||||
/>
|
/>
|
||||||
|
<DocumentEditModal
|
||||||
|
:visible="editModalVisible"
|
||||||
|
:document="editingDocument"
|
||||||
|
@close="editModalVisible = false"
|
||||||
|
@updated="handleDocumentUpdated"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Component Header -->
|
<!-- Component Header -->
|
||||||
<div class="flex items-center gap-3 p-3 bg-base-200 rounded-lg cursor-pointer" @click="toggleCollapse">
|
<div class="flex items-center gap-3 p-3 bg-base-200 rounded-lg cursor-pointer" @click="toggleCollapse">
|
||||||
@@ -208,9 +214,11 @@
|
|||||||
<DocumentListInline
|
<DocumentListInline
|
||||||
:documents="componentDocuments"
|
:documents="componentDocuments"
|
||||||
:can-delete="isEditMode"
|
:can-delete="isEditMode"
|
||||||
|
:can-edit="isEditMode"
|
||||||
:delete-disabled="uploadingDocuments"
|
:delete-disabled="uploadingDocuments"
|
||||||
empty-text="Aucun document lié à ce composant."
|
empty-text="Aucun document lié à ce composant."
|
||||||
@preview="openPreview"
|
@preview="openPreview"
|
||||||
|
@edit="openEditModal"
|
||||||
@delete="removeDocument"
|
@delete="removeDocument"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -319,6 +327,7 @@ const {
|
|||||||
ensureDocumentsLoaded,
|
ensureDocumentsLoaded,
|
||||||
handleFilesAdded,
|
handleFilesAdded,
|
||||||
removeDocument,
|
removeDocument,
|
||||||
|
editDocument,
|
||||||
} = useEntityDocuments({ entity: () => props.component, entityType: 'composant' })
|
} = useEntityDocuments({ entity: () => props.component, entityType: 'composant' })
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -333,6 +342,21 @@ const {
|
|||||||
updateCustomField: updateComponentCustomField,
|
updateCustomField: updateComponentCustomField,
|
||||||
} = useEntityCustomFields({ entity: () => props.component, entityType: 'composant' })
|
} = 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 ---
|
// --- Collapse state ---
|
||||||
const isCollapsed = ref(true)
|
const isCollapsed = ref(true)
|
||||||
|
|
||||||
|
|||||||
90
app/components/DocumentEditModal.vue
Normal file
90
app/components/DocumentEditModal.vue
Normal 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>
|
||||||
@@ -34,6 +34,21 @@
|
|||||||
@change="onFileChange"
|
@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">
|
<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">
|
<li v-for="file in selectedFiles" :key="file.name" class="flex items-center justify-between text-sm">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
@@ -69,6 +84,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, watch, onBeforeUnmount } from 'vue'
|
import { ref, computed, watch, onBeforeUnmount } from 'vue'
|
||||||
import { useToast } from '~/composables/useToast'
|
import { useToast } from '~/composables/useToast'
|
||||||
|
import { DOCUMENT_TYPES } from '~/shared/documentTypes'
|
||||||
import { getFileIcon } from '~/utils/fileIcons'
|
import { getFileIcon } from '~/utils/fileIcons'
|
||||||
import IconLucideCloudUpload from '~icons/lucide/cloud-upload'
|
import IconLucideCloudUpload from '~icons/lucide/cloud-upload'
|
||||||
|
|
||||||
@@ -96,10 +112,14 @@ const props = defineProps({
|
|||||||
maxFileSizeMb: {
|
maxFileSizeMb: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 200
|
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 dragActive = ref(false)
|
||||||
const fileInput = ref(null)
|
const fileInput = ref(null)
|
||||||
|
|||||||
@@ -6,6 +6,12 @@
|
|||||||
:documents="pieceDocuments"
|
:documents="pieceDocuments"
|
||||||
@close="closePreview"
|
@close="closePreview"
|
||||||
/>
|
/>
|
||||||
|
<DocumentEditModal
|
||||||
|
:visible="editModalVisible"
|
||||||
|
:document="editingDocument"
|
||||||
|
@close="editModalVisible = false"
|
||||||
|
@updated="handleDocumentUpdated"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Piece Header (collapsible, same pattern as ComponentItem) -->
|
<!-- Piece Header (collapsible, same pattern as ComponentItem) -->
|
||||||
<div class="flex items-start justify-between p-4 bg-base-200 rounded-lg">
|
<div class="flex items-start justify-between p-4 bg-base-200 rounded-lg">
|
||||||
@@ -247,9 +253,11 @@
|
|||||||
<DocumentListInline
|
<DocumentListInline
|
||||||
:documents="pieceDocuments"
|
:documents="pieceDocuments"
|
||||||
:can-delete="isEditMode"
|
:can-delete="isEditMode"
|
||||||
|
:can-edit="isEditMode"
|
||||||
:delete-disabled="uploadingDocuments"
|
:delete-disabled="uploadingDocuments"
|
||||||
empty-text="Aucun document lié à cette pièce."
|
empty-text="Aucun document lié à cette pièce."
|
||||||
@preview="openPreview"
|
@preview="openPreview"
|
||||||
|
@edit="openEditModal"
|
||||||
@delete="removeDocument"
|
@delete="removeDocument"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -329,6 +337,7 @@ const {
|
|||||||
refreshDocuments,
|
refreshDocuments,
|
||||||
handleFilesAdded,
|
handleFilesAdded,
|
||||||
removeDocument,
|
removeDocument,
|
||||||
|
editDocument,
|
||||||
} = useEntityDocuments({ entity: () => props.piece, entityType: 'piece' })
|
} = useEntityDocuments({ entity: () => props.piece, entityType: 'piece' })
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -343,6 +352,21 @@ const {
|
|||||||
updateCustomField,
|
updateCustomField,
|
||||||
} = useEntityCustomFields({ entity: () => props.piece, entityType: 'piece' })
|
} = 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 ---
|
// --- Collapse state ---
|
||||||
const isCollapsed = ref(true)
|
const isCollapsed = ref(true)
|
||||||
|
|
||||||
|
|||||||
@@ -31,8 +31,9 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="font-medium">
|
<div class="font-medium flex items-center gap-2">
|
||||||
{{ document.name }}
|
{{ document.name }}
|
||||||
|
<span class="badge badge-sm badge-outline">{{ getDocumentTypeLabel(document.type || 'documentation') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs text-base-content/70">
|
<div class="text-xs text-base-content/70">
|
||||||
{{ document.mimeType || 'Inconnu' }} • {{ formatSize(document.size) }}
|
{{ document.mimeType || 'Inconnu' }} • {{ formatSize(document.size) }}
|
||||||
@@ -40,6 +41,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-ghost btn-xs"
|
class="btn btn-ghost btn-xs"
|
||||||
@@ -74,6 +84,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { getDocumentTypeLabel } from '~/shared/documentTypes'
|
||||||
import { canPreviewDocument, isImageDocument } from '~/utils/documentPreview'
|
import { canPreviewDocument, isImageDocument } from '~/utils/documentPreview'
|
||||||
import {
|
import {
|
||||||
documentIcon,
|
documentIcon,
|
||||||
@@ -89,10 +100,12 @@ import type { Document } from '~/composables/useDocuments'
|
|||||||
withDefaults(defineProps<{
|
withDefaults(defineProps<{
|
||||||
documents: Document[]
|
documents: Document[]
|
||||||
canDelete?: boolean
|
canDelete?: boolean
|
||||||
|
canEdit?: boolean
|
||||||
deleteDisabled?: boolean
|
deleteDisabled?: boolean
|
||||||
emptyText?: string
|
emptyText?: string
|
||||||
}>(), {
|
}>(), {
|
||||||
canDelete: false,
|
canDelete: false,
|
||||||
|
canEdit: false,
|
||||||
deleteDisabled: false,
|
deleteDisabled: false,
|
||||||
emptyText: 'Aucun document.',
|
emptyText: 'Aucun document.',
|
||||||
})
|
})
|
||||||
@@ -100,5 +113,6 @@ withDefaults(defineProps<{
|
|||||||
defineEmits<{
|
defineEmits<{
|
||||||
(e: 'preview', document: Document): void
|
(e: 'preview', document: Document): void
|
||||||
(e: 'delete', documentId: string): void
|
(e: 'delete', documentId: string): void
|
||||||
|
(e: 'edit', document: Document): void
|
||||||
}>()
|
}>()
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export interface Document {
|
|||||||
size: number
|
size: number
|
||||||
fileUrl: string
|
fileUrl: string
|
||||||
downloadUrl: string
|
downloadUrl: string
|
||||||
|
type?: string
|
||||||
/** @deprecated Legacy Base64 data URI — use fileUrl instead */
|
/** @deprecated Legacy Base64 data URI — use fileUrl instead */
|
||||||
path?: string
|
path?: string
|
||||||
createdAt?: string
|
createdAt?: string
|
||||||
@@ -32,6 +33,7 @@ export interface UploadContext {
|
|||||||
composantId?: string
|
composantId?: string
|
||||||
productId?: string
|
productId?: string
|
||||||
pieceId?: string
|
pieceId?: string
|
||||||
|
type?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DocumentResult {
|
export interface DocumentResult {
|
||||||
@@ -47,6 +49,7 @@ interface LoadDocumentsOptions {
|
|||||||
orderBy?: string
|
orderBy?: string
|
||||||
orderDir?: 'asc' | 'desc'
|
orderDir?: 'asc' | 'desc'
|
||||||
attachmentFilter?: string
|
attachmentFilter?: string
|
||||||
|
type?: string
|
||||||
force?: boolean
|
force?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +66,7 @@ const extractTotal = (payload: unknown, fallbackLength: number): number => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useDocuments() {
|
export function useDocuments() {
|
||||||
const { get, postFormData, delete: del } = useApi()
|
const { get, patch, postFormData, delete: del } = useApi()
|
||||||
const { showError, showSuccess } = useToast()
|
const { showError, showSuccess } = useToast()
|
||||||
|
|
||||||
const loadFromEndpoint = async (
|
const loadFromEndpoint = async (
|
||||||
@@ -103,10 +106,11 @@ export function useDocuments() {
|
|||||||
orderBy = 'createdAt',
|
orderBy = 'createdAt',
|
||||||
orderDir = 'desc',
|
orderDir = 'desc',
|
||||||
attachmentFilter = 'all',
|
attachmentFilter = 'all',
|
||||||
|
type = 'all',
|
||||||
force = false,
|
force = false,
|
||||||
} = options
|
} = 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 }
|
return { success: true, data: documents.value }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,6 +132,10 @@ export function useDocuments() {
|
|||||||
params.set(`exists[${attachmentFilter}]`, 'true')
|
params.set(`exists[${attachmentFilter}]`, 'true')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type && type !== 'all') {
|
||||||
|
params.set('type', type)
|
||||||
|
}
|
||||||
|
|
||||||
params.set(`order[${orderBy}]`, orderDir)
|
params.set(`order[${orderBy}]`, orderDir)
|
||||||
|
|
||||||
const result = await get(`/documents?${params.toString()}`)
|
const result = await get(`/documents?${params.toString()}`)
|
||||||
@@ -218,6 +226,7 @@ export function useDocuments() {
|
|||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('file', file)
|
formData.append('file', file)
|
||||||
formData.append('name', file.name)
|
formData.append('name', file.name)
|
||||||
|
if (context.type) formData.append('type', context.type)
|
||||||
|
|
||||||
if (context.siteId) formData.append('siteId', context.siteId)
|
if (context.siteId) formData.append('siteId', context.siteId)
|
||||||
if (context.machineId) formData.append('machineId', context.machineId)
|
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 {
|
return {
|
||||||
documents,
|
documents,
|
||||||
total,
|
total,
|
||||||
@@ -292,6 +328,7 @@ export function useDocuments() {
|
|||||||
loadDocumentsByPiece,
|
loadDocumentsByPiece,
|
||||||
loadDocumentsByProduct,
|
loadDocumentsByProduct,
|
||||||
uploadDocuments,
|
uploadDocuments,
|
||||||
|
updateDocument,
|
||||||
deleteDocument,
|
deleteDocument,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export interface EntityDocumentsDeps {
|
|||||||
|
|
||||||
export function useEntityDocuments(deps: EntityDocumentsDeps) {
|
export function useEntityDocuments(deps: EntityDocumentsDeps) {
|
||||||
const { entity, entityType } = deps
|
const { entity, entityType } = deps
|
||||||
const { uploadDocuments, deleteDocument } = useDocuments()
|
const { uploadDocuments, deleteDocument, updateDocument } = useDocuments()
|
||||||
|
|
||||||
const loadDocumentsFn = entityType === 'composant'
|
const loadDocumentsFn = entityType === 'composant'
|
||||||
? useDocuments().loadDocumentsByComponent
|
? 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 {
|
return {
|
||||||
documents,
|
documents,
|
||||||
selectedFiles,
|
selectedFiles,
|
||||||
@@ -118,5 +131,6 @@ export function useEntityDocuments(deps: EntityDocumentsDeps) {
|
|||||||
ensureDocumentsLoaded,
|
ensureDocumentsLoaded,
|
||||||
handleFilesAdded,
|
handleFilesAdded,
|
||||||
removeDocument,
|
removeDocument,
|
||||||
|
editDocument,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,12 @@
|
|||||||
:documents="componentDocuments"
|
:documents="componentDocuments"
|
||||||
@close="closePreview"
|
@close="closePreview"
|
||||||
/>
|
/>
|
||||||
|
<DocumentEditModal
|
||||||
|
:visible="editModalVisible"
|
||||||
|
:document="editingDocument"
|
||||||
|
@close="editModalVisible = false"
|
||||||
|
@updated="handleDocumentUpdated"
|
||||||
|
/>
|
||||||
<main class="container mx-auto px-6 py-10">
|
<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">
|
<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" />
|
<span class="loading loading-spinner loading-lg" aria-hidden="true" />
|
||||||
@@ -294,9 +300,11 @@
|
|||||||
v-else
|
v-else
|
||||||
:documents="componentDocuments"
|
:documents="componentDocuments"
|
||||||
:can-delete="canEdit"
|
:can-delete="canEdit"
|
||||||
|
:can-edit="true"
|
||||||
:delete-disabled="uploadingDocuments"
|
:delete-disabled="uploadingDocuments"
|
||||||
empty-text="Aucun document n'est associé à ce composant pour le moment."
|
empty-text="Aucun document n'est associé à ce composant pour le moment."
|
||||||
@preview="openPreview"
|
@preview="openPreview"
|
||||||
|
@edit="openEditModal"
|
||||||
@delete="removeDocument"
|
@delete="removeDocument"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -334,10 +342,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
import { useRoute } from '#imports'
|
import { useRoute } from '#imports'
|
||||||
import { useComponentEdit } from '~/composables/useComponentEdit'
|
import { useComponentEdit } from '~/composables/useComponentEdit'
|
||||||
|
import { useDocuments } from '~/composables/useDocuments'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
const { updateDocument } = useDocuments()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
component,
|
component,
|
||||||
@@ -379,4 +390,24 @@ const {
|
|||||||
resolveSubcomponentLabel,
|
resolveSubcomponentLabel,
|
||||||
formatStructurePreview,
|
formatStructurePreview,
|
||||||
} = useComponentEdit(String(route.params.id))
|
} = 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>
|
</script>
|
||||||
|
|||||||
@@ -7,6 +7,13 @@
|
|||||||
@close="closePreview"
|
@close="closePreview"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<DocumentEditModal
|
||||||
|
:visible="editModalVisible"
|
||||||
|
:document="editingDocument"
|
||||||
|
@close="editModalVisible = false"
|
||||||
|
@updated="handleDocumentUpdated"
|
||||||
|
/>
|
||||||
|
|
||||||
<section class="card bg-base-100 shadow-sm">
|
<section class="card bg-base-100 shadow-sm">
|
||||||
<div class="card-body space-y-6">
|
<div class="card-body space-y-6">
|
||||||
<DataTable
|
<DataTable
|
||||||
@@ -55,6 +62,26 @@
|
|||||||
<option value="product">Produits</option>
|
<option value="product">Produits</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<template #cell-name="{ row }">
|
<template #cell-name="{ row }">
|
||||||
@@ -77,6 +104,10 @@
|
|||||||
{{ row.mimeType || 'Inconnu' }}
|
{{ row.mimeType || 'Inconnu' }}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template #cell-type="{ row }">
|
||||||
|
<span class="badge badge-sm badge-outline">{{ getDocumentTypeLabel(row.type || 'documentation') }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template #cell-size="{ row }">
|
<template #cell-size="{ row }">
|
||||||
{{ formatSize(row.size) }}
|
{{ formatSize(row.size) }}
|
||||||
</template>
|
</template>
|
||||||
@@ -98,6 +129,14 @@
|
|||||||
|
|
||||||
<template #cell-actions="{ row }">
|
<template #cell-actions="{ row }">
|
||||||
<div class="flex justify-end gap-2">
|
<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
|
<button
|
||||||
class="btn btn-ghost btn-xs"
|
class="btn btn-ghost btn-xs"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -123,12 +162,15 @@ import { computed, onMounted, ref, type Ref } from 'vue'
|
|||||||
import DataTable from '~/components/common/DataTable.vue'
|
import DataTable from '~/components/common/DataTable.vue'
|
||||||
import { useDocuments } from '~/composables/useDocuments'
|
import { useDocuments } from '~/composables/useDocuments'
|
||||||
import { useDataTable } from '~/composables/useDataTable'
|
import { useDataTable } from '~/composables/useDataTable'
|
||||||
|
import { usePermissions } from '~/composables/usePermissions'
|
||||||
import { getFileIcon } from '~/utils/fileIcons'
|
import { getFileIcon } from '~/utils/fileIcons'
|
||||||
import { canPreviewDocument } from '~/utils/documentPreview'
|
import { canPreviewDocument } from '~/utils/documentPreview'
|
||||||
import { formatFrenchDate } from '~/utils/date'
|
import { formatFrenchDate } from '~/utils/date'
|
||||||
|
import { DOCUMENT_TYPES, getDocumentTypeLabel } from '~/shared/documentTypes'
|
||||||
import DocumentPreviewModal from '~/components/DocumentPreviewModal.vue'
|
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(
|
const table = useDataTable(
|
||||||
{ fetchData: fetchDocuments },
|
{ fetchData: fetchDocuments },
|
||||||
@@ -139,21 +181,26 @@ const table = useDataTable(
|
|||||||
persistToUrl: true,
|
persistToUrl: true,
|
||||||
extraParams: {
|
extraParams: {
|
||||||
filter: { default: 'all' },
|
filter: { default: 'all' },
|
||||||
|
typeFilter: { default: 'all' },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
const attachmentFilter = table.filters.filter as Ref<string>
|
const attachmentFilter = table.filters.filter as Ref<string>
|
||||||
|
const typeFilter = table.filters.typeFilter as Ref<string>
|
||||||
|
|
||||||
const previewDocument = ref<any>(null)
|
const previewDocument = ref<any>(null)
|
||||||
const previewVisible = ref(false)
|
const previewVisible = ref(false)
|
||||||
|
const editingDocument = ref<any>(null)
|
||||||
|
const editModalVisible = ref(false)
|
||||||
|
|
||||||
const documentsOnPage = computed(() => documents.value.length)
|
const documentsOnPage = computed(() => documents.value.length)
|
||||||
const paginationState = table.pagination(total, documentsOnPage)
|
const paginationState = table.pagination(total, documentsOnPage)
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ key: 'name', label: 'Nom', sortable: true, sortKey: 'name' },
|
{ 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: 'size', label: 'Taille', sortable: true, sortKey: 'size' },
|
||||||
{ key: 'attachment', label: 'Rattaché à' },
|
{ key: 'attachment', label: 'Rattaché à' },
|
||||||
{ key: 'createdAt', label: 'Date', sortable: true, sortKey: 'createdAt' },
|
{ key: 'createdAt', label: 'Date', sortable: true, sortKey: 'createdAt' },
|
||||||
@@ -168,6 +215,7 @@ async function fetchDocuments() {
|
|||||||
orderBy: table.sortField.value,
|
orderBy: table.sortField.value,
|
||||||
orderDir: table.sortDirection.value as 'asc' | 'desc',
|
orderDir: table.sortDirection.value as 'asc' | 'desc',
|
||||||
attachmentFilter: attachmentFilter.value,
|
attachmentFilter: attachmentFilter.value,
|
||||||
|
type: typeFilter.value,
|
||||||
force: true,
|
force: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -198,6 +246,25 @@ const closePreview = () => {
|
|||||||
previewDocument.value = null
|
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(() => {
|
onMounted(() => {
|
||||||
fetchDocuments()
|
fetchDocuments()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,6 +6,12 @@
|
|||||||
:documents="pieceDocuments"
|
:documents="pieceDocuments"
|
||||||
@close="closePreview"
|
@close="closePreview"
|
||||||
/>
|
/>
|
||||||
|
<DocumentEditModal
|
||||||
|
:visible="editModalVisible"
|
||||||
|
:document="editingDocument"
|
||||||
|
@close="editModalVisible = false"
|
||||||
|
@updated="handleDocumentUpdated"
|
||||||
|
/>
|
||||||
<main class="container mx-auto px-6 py-10">
|
<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">
|
<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" />
|
<span class="loading loading-spinner loading-lg" aria-hidden="true" />
|
||||||
@@ -231,9 +237,11 @@
|
|||||||
v-else
|
v-else
|
||||||
:documents="pieceDocuments"
|
:documents="pieceDocuments"
|
||||||
:can-delete="canEdit"
|
:can-delete="canEdit"
|
||||||
|
:can-edit="true"
|
||||||
:delete-disabled="uploadingDocuments"
|
:delete-disabled="uploadingDocuments"
|
||||||
empty-text="Aucun document n'est associé à cette pièce pour le moment."
|
empty-text="Aucun document n'est associé à cette pièce pour le moment."
|
||||||
@preview="openPreview"
|
@preview="openPreview"
|
||||||
|
@edit="openEditModal"
|
||||||
@delete="removeDocument"
|
@delete="removeDocument"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -271,10 +279,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
import { useRoute } from '#imports'
|
import { useRoute } from '#imports'
|
||||||
import { usePieceEdit } from '~/composables/usePieceEdit'
|
import { usePieceEdit } from '~/composables/usePieceEdit'
|
||||||
|
import { useDocuments } from '~/composables/useDocuments'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
const { updateDocument } = useDocuments()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
piece,
|
piece,
|
||||||
@@ -310,4 +321,24 @@ const {
|
|||||||
submitEdition,
|
submitEdition,
|
||||||
formatPieceStructurePreview,
|
formatPieceStructurePreview,
|
||||||
} = usePieceEdit(String(route.params.id))
|
} = 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>
|
</script>
|
||||||
|
|||||||
@@ -6,6 +6,12 @@
|
|||||||
:documents="productDocuments"
|
:documents="productDocuments"
|
||||||
@close="closePreview"
|
@close="closePreview"
|
||||||
/>
|
/>
|
||||||
|
<DocumentEditModal
|
||||||
|
:visible="editModalVisible"
|
||||||
|
:document="editingDocument"
|
||||||
|
@close="editModalVisible = false"
|
||||||
|
@updated="handleDocumentUpdated"
|
||||||
|
/>
|
||||||
<main class="container mx-auto px-6 py-10">
|
<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">
|
<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" />
|
<span class="loading loading-spinner loading-lg" aria-hidden="true" />
|
||||||
@@ -167,9 +173,11 @@
|
|||||||
v-else
|
v-else
|
||||||
:documents="productDocuments"
|
:documents="productDocuments"
|
||||||
:can-delete="canEdit"
|
:can-delete="canEdit"
|
||||||
|
:can-edit="true"
|
||||||
:delete-disabled="uploadingDocuments || saving"
|
:delete-disabled="uploadingDocuments || saving"
|
||||||
empty-text="Aucun document n'est associé à ce produit pour le moment."
|
empty-text="Aucun document n'est associé à ce produit pour le moment."
|
||||||
@preview="openPreview"
|
@preview="openPreview"
|
||||||
|
@edit="openEditModal"
|
||||||
@delete="removeDocument"
|
@delete="removeDocument"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -244,6 +252,7 @@ const {
|
|||||||
loadDocumentsByProduct,
|
loadDocumentsByProduct,
|
||||||
uploadDocuments: uploadProductDocuments,
|
uploadDocuments: uploadProductDocuments,
|
||||||
deleteDocument: deleteProductDocument,
|
deleteDocument: deleteProductDocument,
|
||||||
|
updateDocument,
|
||||||
} = useDocuments()
|
} = useDocuments()
|
||||||
const { ensureConstructeurs } = useConstructeurs()
|
const { ensureConstructeurs } = useConstructeurs()
|
||||||
const {
|
const {
|
||||||
@@ -265,6 +274,8 @@ const loadingDocuments = ref(false)
|
|||||||
const productDocuments = ref<any[]>([])
|
const productDocuments = ref<any[]>([])
|
||||||
const previewDocument = ref<any | null>(null)
|
const previewDocument = ref<any | null>(null)
|
||||||
const previewVisible = ref(false)
|
const previewVisible = ref(false)
|
||||||
|
const editingDocument = ref<any | null>(null)
|
||||||
|
const editModalVisible = ref(false)
|
||||||
|
|
||||||
const historyFieldLabels: Record<string, string> = {
|
const historyFieldLabels: Record<string, string> = {
|
||||||
name: 'Nom',
|
name: 'Nom',
|
||||||
@@ -307,6 +318,23 @@ const openPreview = (doc: any) => {
|
|||||||
}
|
}
|
||||||
const closePreview = () => { previewVisible.value = false; previewDocument.value = null }
|
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 loadProduct = async () => {
|
||||||
const id = route.params.id
|
const id = route.params.id
|
||||||
if (!id || typeof id !== 'string') {
|
if (!id || typeof id !== 'string') {
|
||||||
|
|||||||
15
app/shared/documentTypes.ts
Normal file
15
app/shared/documentTypes.ts
Normal 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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user