Merges the full git history of Inventory_frontend into the monorepo under frontend/. Removes the submodule in favor of a unified repo. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
272 lines
9.3 KiB
Vue
272 lines
9.3 KiB
Vue
<template>
|
|
<main class="container mx-auto px-6 py-8 space-y-8">
|
|
<DocumentPreviewModal
|
|
:document="previewDocument"
|
|
:visible="previewVisible"
|
|
:documents="documents"
|
|
@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
|
|
:columns="columns"
|
|
:rows="documents"
|
|
:loading="loading"
|
|
:sort="table.sort.value"
|
|
:pagination="paginationState"
|
|
:show-per-page="true"
|
|
empty-message="Aucun document n'a encore été ajouté."
|
|
no-results-message="Aucun document ne correspond à votre recherche."
|
|
@sort="table.handleSort"
|
|
@update:current-page="table.handlePageChange"
|
|
@update:per-page="table.handlePerPageChange"
|
|
>
|
|
<template #toolbar>
|
|
<label class="w-full sm:w-72">
|
|
<span class="text-xs font-semibold uppercase tracking-wide text-base-content/70">Recherche</span>
|
|
<input
|
|
v-model="table.searchTerm.value"
|
|
type="text"
|
|
class="input input-bordered input-sm w-full mt-1"
|
|
placeholder="Nom du document..."
|
|
@input="table.debouncedSearch"
|
|
/>
|
|
</label>
|
|
|
|
<div class="flex items-center gap-2">
|
|
<label
|
|
class="text-xs font-semibold uppercase tracking-wide text-base-content/70"
|
|
for="doc-filter"
|
|
>
|
|
Rattachement
|
|
</label>
|
|
<select
|
|
id="doc-filter"
|
|
v-model="attachmentFilter"
|
|
class="select select-bordered select-sm"
|
|
@change="table.handleFilterChange"
|
|
>
|
|
<option value="all">Tous</option>
|
|
<option value="site">Sites</option>
|
|
<option value="machine">Machines</option>
|
|
<option value="composant">Composants</option>
|
|
<option value="piece">Pièces</option>
|
|
<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 }">
|
|
<div class="flex items-center gap-3">
|
|
<span class="text-xl" :class="documentIcon(row).colorClass">
|
|
<component
|
|
:is="documentIcon(row).component"
|
|
class="h-6 w-6"
|
|
aria-hidden="true"
|
|
/>
|
|
</span>
|
|
<div>
|
|
<div class="font-semibold">{{ row.name }}</div>
|
|
<div class="text-xs text-base-content/50">{{ row.filename }}</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<template #cell-mimeType="{ row }">
|
|
{{ 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>
|
|
|
|
<template #cell-attachment="{ row }">
|
|
<div class="flex flex-col text-xs">
|
|
<span v-if="row.site">Site · {{ row.site.name }}</span>
|
|
<span v-else-if="row.machine">Machine · {{ row.machine.name }}</span>
|
|
<span v-else-if="row.composant">Composant · {{ row.composant.name }}</span>
|
|
<span v-else-if="row.piece">Pièce · {{ row.piece.name }}</span>
|
|
<span v-else-if="row.product">Produit · {{ row.product.name }}</span>
|
|
<span v-else class="text-base-content/30">Non défini</span>
|
|
</div>
|
|
</template>
|
|
|
|
<template #cell-createdAt="{ row }">
|
|
{{ formatFrenchDate(row.createdAt) }}
|
|
</template>
|
|
|
|
<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"
|
|
:disabled="!canPreviewDocument(row)"
|
|
:title="canPreviewDocument(row) ? 'Consulter le document' : 'Aucun aper\u00E7u disponible pour ce type'"
|
|
@click="openPreview(row)"
|
|
>
|
|
Consulter
|
|
</button>
|
|
<button class="btn btn-ghost btn-xs" type="button" @click="downloadDocument(row)">
|
|
Télécharger
|
|
</button>
|
|
</div>
|
|
</template>
|
|
</DataTable>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
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, updateDocument } = useDocuments()
|
|
const { canEdit } = usePermissions()
|
|
|
|
const table = useDataTable(
|
|
{ fetchData: fetchDocuments },
|
|
{
|
|
defaultSort: 'createdAt',
|
|
defaultDirection: 'desc',
|
|
defaultPerPage: 30,
|
|
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 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' },
|
|
{ key: 'actions', label: 'Actions', align: 'right' as const },
|
|
]
|
|
|
|
async function fetchDocuments() {
|
|
await loadDocuments({
|
|
search: table.searchTerm.value,
|
|
page: table.currentPage.value,
|
|
itemsPerPage: table.itemsPerPage.value,
|
|
orderBy: table.sortField.value,
|
|
orderDir: table.sortDirection.value as 'asc' | 'desc',
|
|
attachmentFilter: attachmentFilter.value,
|
|
type: typeFilter.value,
|
|
force: true,
|
|
})
|
|
}
|
|
|
|
const formatSize = (size: number | undefined | null) => {
|
|
if (size === undefined || size === null) return '\u2014'
|
|
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]}`
|
|
}
|
|
|
|
const documentIcon = (doc: any) => getFileIcon({ name: doc.filename || doc.name, mime: doc.mimeType })
|
|
|
|
const downloadDocument = (doc: any) => {
|
|
if (doc?.downloadUrl) window.open(doc.downloadUrl, '_blank')
|
|
}
|
|
|
|
const openPreview = (doc: any) => {
|
|
if (!canPreviewDocument(doc)) return
|
|
previewDocument.value = doc
|
|
previewVisible.value = true
|
|
}
|
|
|
|
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 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()
|
|
})
|
|
</script>
|