381 lines
9.9 KiB
Vue
381 lines
9.9 KiB
Vue
<template>
|
|
<div class="document-upload">
|
|
<div class="flex justify-content-between align-items-center mb-3">
|
|
<h3 v-if="title">{{ title }}</h3>
|
|
<Button
|
|
v-if="canUpload"
|
|
label="Dokument hochladen"
|
|
icon="pi pi-upload"
|
|
@click="showUploadDialog = true"
|
|
severity="success"
|
|
class="ml-auto"
|
|
outlined
|
|
/>
|
|
</div>
|
|
|
|
<!-- Documents List -->
|
|
<DataTable
|
|
:value="documents"
|
|
:loading="loading"
|
|
stripedRows
|
|
size="small"
|
|
class="p-datatable-sm"
|
|
>
|
|
<Column field="originalFilename" header="Dateiname">
|
|
<template #body="{ data }">
|
|
<div class="flex align-items-center gap-2">
|
|
<i :class="getFileIcon(data.mimeType)" class="text-xl"></i>
|
|
<span>{{ data.originalFilename }}</span>
|
|
</div>
|
|
</template>
|
|
</Column>
|
|
<Column field="description" header="Beschreibung"></Column>
|
|
<Column field="size" header="Größe">
|
|
<template #body="{ data }">
|
|
{{ formatFileSize(data.size) }}
|
|
</template>
|
|
</Column>
|
|
<Column field="uploadedAt" header="Hochgeladen am">
|
|
<template #body="{ data }">
|
|
{{ formatDate(data.uploadedAt) }}
|
|
</template>
|
|
</Column>
|
|
<Column field="uploadedBy" header="Hochgeladen von">
|
|
<template #body="{ data }">
|
|
{{ data.uploadedBy ? `${data.uploadedBy.firstName} ${data.uploadedBy.lastName}` : 'Unbekannt' }}
|
|
</template>
|
|
</Column>
|
|
<Column header="Aktionen" style="width: 120px">
|
|
<template #body="{ data }">
|
|
<div class="flex gap-2">
|
|
<Button
|
|
icon="pi pi-download"
|
|
severity="info"
|
|
size="small"
|
|
text
|
|
rounded
|
|
@click="downloadDocument(data)"
|
|
v-tooltip.top="'Herunterladen'"
|
|
/>
|
|
<Button
|
|
v-if="canDelete"
|
|
icon="pi pi-trash"
|
|
severity="danger"
|
|
size="small"
|
|
text
|
|
rounded
|
|
@click="confirmDeleteDocument(data)"
|
|
v-tooltip.top="'Löschen'"
|
|
/>
|
|
</div>
|
|
</template>
|
|
</Column>
|
|
</DataTable>
|
|
|
|
<!-- Upload Dialog -->
|
|
<Dialog
|
|
v-model:visible="showUploadDialog"
|
|
:modal="true"
|
|
header="Dokument hochladen"
|
|
:style="{ width: '600px' }"
|
|
>
|
|
<div class="flex flex-column gap-3">
|
|
<div class="field">
|
|
<label for="file">Datei auswählen</label>
|
|
<FileUpload
|
|
mode="basic"
|
|
name="file"
|
|
accept="image/*,.pdf,.csv,.xls,.xlsx,.doc,.docx"
|
|
:maxFileSize="10000000"
|
|
@select="onFileSelect"
|
|
:auto="false"
|
|
chooseLabel="Datei wählen"
|
|
/>
|
|
<small v-if="selectedFile" class="text-500">
|
|
{{ selectedFile.name }} ({{ formatFileSize(selectedFile.size) }})
|
|
</small>
|
|
<small class="block text-500 mt-2">
|
|
Erlaubt: Bilder (JPG, PNG, GIF, WebP, SVG), PDF, Excel (XLS, XLSX), CSV, Word (DOC, DOCX) - Max. 10 MB
|
|
</small>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label for="description">Beschreibung (optional)</label>
|
|
<Textarea
|
|
id="description"
|
|
v-model="uploadDescription"
|
|
rows="3"
|
|
class="w-full"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<template #footer>
|
|
<Button label="Abbrechen" severity="secondary" @click="closeUploadDialog" />
|
|
<Button
|
|
label="Hochladen"
|
|
icon="pi pi-upload"
|
|
@click="uploadDocument"
|
|
:disabled="!selectedFile"
|
|
:loading="uploading"
|
|
/>
|
|
</template>
|
|
</Dialog>
|
|
|
|
<!-- Delete Confirmation -->
|
|
<Dialog
|
|
v-model:visible="showDeleteDialog"
|
|
:modal="true"
|
|
header="Dokument löschen"
|
|
:style="{ width: '450px' }"
|
|
>
|
|
<p>Möchten Sie das Dokument "{{ documentToDelete?.originalFilename }}" wirklich löschen?</p>
|
|
<template #footer>
|
|
<Button label="Abbrechen" severity="secondary" @click="showDeleteDialog = false" />
|
|
<Button
|
|
label="Löschen"
|
|
severity="danger"
|
|
@click="deleteDocument"
|
|
:loading="deleting"
|
|
/>
|
|
</template>
|
|
</Dialog>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted, computed } from 'vue'
|
|
import DataTable from 'primevue/datatable'
|
|
import Column from 'primevue/column'
|
|
import Button from 'primevue/button'
|
|
import Dialog from 'primevue/dialog'
|
|
import FileUpload from 'primevue/fileupload'
|
|
import Textarea from 'primevue/textarea'
|
|
import { useToast } from 'primevue/usetoast'
|
|
|
|
const props = defineProps({
|
|
entityType: {
|
|
type: String,
|
|
required: true
|
|
},
|
|
entityId: {
|
|
type: Number,
|
|
required: true
|
|
},
|
|
title: {
|
|
type: String,
|
|
default: 'Dokumente'
|
|
},
|
|
canUpload: {
|
|
type: Boolean,
|
|
default: true
|
|
},
|
|
canDelete: {
|
|
type: Boolean,
|
|
default: true
|
|
}
|
|
})
|
|
|
|
const toast = useToast()
|
|
const documents = ref([])
|
|
const loading = ref(false)
|
|
const showUploadDialog = ref(false)
|
|
const showDeleteDialog = ref(false)
|
|
const selectedFile = ref(null)
|
|
const uploadDescription = ref('')
|
|
const uploading = ref(false)
|
|
const deleting = ref(false)
|
|
const documentToDelete = ref(null)
|
|
|
|
onMounted(() => {
|
|
loadDocuments()
|
|
})
|
|
|
|
async function loadDocuments() {
|
|
loading.value = true
|
|
try {
|
|
const response = await fetch(
|
|
`/api/documents?relatedEntity=${props.entityType}&relatedEntityId=${props.entityId}`
|
|
)
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`)
|
|
}
|
|
|
|
const data = await response.json()
|
|
console.log('Documents API response:', data)
|
|
|
|
// Handle API Platform response
|
|
const items = data['hydra:member'] || data.member || data
|
|
|
|
// Ensure it's an array and not an object
|
|
if (Array.isArray(items)) {
|
|
documents.value = items
|
|
} else {
|
|
console.error('API returned non-array:', items)
|
|
documents.value = []
|
|
}
|
|
} catch (error) {
|
|
console.error('Fehler beim Laden der Dokumente:', error)
|
|
documents.value = []
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: 'Fehler',
|
|
detail: 'Dokumente konnten nicht geladen werden',
|
|
life: 3000
|
|
})
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function onFileSelect(event) {
|
|
selectedFile.value = event.files[0]
|
|
}
|
|
|
|
async function uploadDocument() {
|
|
if (!selectedFile.value) return
|
|
|
|
uploading.value = true
|
|
try {
|
|
const formData = new FormData()
|
|
formData.append('file', selectedFile.value)
|
|
formData.append('relatedEntity', props.entityType)
|
|
formData.append('relatedEntityId', props.entityId)
|
|
if (uploadDescription.value) {
|
|
formData.append('description', uploadDescription.value)
|
|
}
|
|
|
|
const response = await fetch('/api/documents/upload', {
|
|
method: 'POST',
|
|
body: formData
|
|
})
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Upload fehlgeschlagen')
|
|
}
|
|
|
|
toast.add({
|
|
severity: 'success',
|
|
summary: 'Erfolg',
|
|
detail: 'Dokument wurde hochgeladen',
|
|
life: 3000
|
|
})
|
|
|
|
closeUploadDialog()
|
|
loadDocuments()
|
|
} catch (error) {
|
|
console.error('Fehler beim Upload:', error)
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: 'Fehler',
|
|
detail: 'Dokument konnte nicht hochgeladen werden',
|
|
life: 3000
|
|
})
|
|
} finally {
|
|
uploading.value = false
|
|
}
|
|
}
|
|
|
|
function closeUploadDialog() {
|
|
showUploadDialog.value = false
|
|
selectedFile.value = null
|
|
uploadDescription.value = ''
|
|
}
|
|
|
|
async function downloadDocument(document) {
|
|
try {
|
|
// Open document in new window instead of downloading
|
|
window.open(`/api/documents/${document.id}/download`, '_blank')
|
|
} catch (error) {
|
|
console.error('Fehler beim Öffnen:', error)
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: 'Fehler',
|
|
detail: 'Dokument konnte nicht geöffnet werden',
|
|
life: 3000
|
|
})
|
|
}
|
|
}
|
|
|
|
function confirmDeleteDocument(document) {
|
|
documentToDelete.value = document
|
|
showDeleteDialog.value = true
|
|
}
|
|
|
|
async function deleteDocument() {
|
|
if (!documentToDelete.value) return
|
|
|
|
deleting.value = true
|
|
try {
|
|
const response = await fetch(`/api/documents/${documentToDelete.value.id}`, {
|
|
method: 'DELETE'
|
|
})
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Löschen fehlgeschlagen')
|
|
}
|
|
|
|
toast.add({
|
|
severity: 'success',
|
|
summary: 'Erfolg',
|
|
detail: 'Dokument wurde gelöscht',
|
|
life: 3000
|
|
})
|
|
|
|
showDeleteDialog.value = false
|
|
documentToDelete.value = null
|
|
loadDocuments()
|
|
} catch (error) {
|
|
console.error('Fehler beim Löschen:', error)
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: 'Fehler',
|
|
detail: 'Dokument konnte nicht gelöscht werden',
|
|
life: 3000
|
|
})
|
|
} finally {
|
|
deleting.value = false
|
|
}
|
|
}
|
|
|
|
function formatFileSize(bytes) {
|
|
if (bytes === 0) return '0 Bytes'
|
|
const k = 1024
|
|
const sizes = ['Bytes', 'KB', 'MB', 'GB']
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]
|
|
}
|
|
|
|
function formatDate(dateString) {
|
|
const date = new Date(dateString)
|
|
return date.toLocaleDateString('de-DE', {
|
|
day: '2-digit',
|
|
month: '2-digit',
|
|
year: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
})
|
|
}
|
|
|
|
function getFileIcon(mimeType) {
|
|
if (!mimeType) return 'pi pi-file'
|
|
|
|
if (mimeType.includes('pdf')) return 'pi pi-file-pdf'
|
|
if (mimeType.includes('word') || mimeType.includes('document')) return 'pi pi-file-word'
|
|
if (mimeType.includes('excel') || mimeType.includes('spreadsheet')) return 'pi pi-file-excel'
|
|
if (mimeType.includes('image')) return 'pi pi-image'
|
|
if (mimeType.includes('video')) return 'pi pi-video'
|
|
if (mimeType.includes('audio')) return 'pi pi-volume-up'
|
|
if (mimeType.includes('zip') || mimeType.includes('compressed')) return 'pi pi-file-import'
|
|
|
|
return 'pi pi-file'
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.document-upload {
|
|
width: 100%;
|
|
}
|
|
</style>
|