Teren-app/resources/js/Components/DocumentsTable.vue
2025-09-28 22:36:47 +02:00

199 lines
8.3 KiB
Vue

<script setup>
import { FwbTable, FwbTableHead, FwbTableHeadCell, FwbTableBody, FwbTableRow, FwbTableCell, FwbBadge } from 'flowbite-vue'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { faFilePdf, faFileWord, faFileExcel, faFileLines, faFileImage, faFile, faCircleInfo, faEllipsisVertical, faDownload } from '@fortawesome/free-solid-svg-icons'
import { ref } from 'vue'
import Dropdown from '@/Components/Dropdown.vue'
const props = defineProps({
documents: { type: Array, default: () => [] },
viewUrlBuilder: { type: Function, default: null },
// Optional: build a direct download URL for a document; if not provided, a 'download' event will be emitted
downloadUrlBuilder: { type: Function, default: null },
})
// Derive a human-friendly source for a document: Case or Contract reference
const sourceLabel = (doc) => {
// Server can include optional documentable meta; fall back to type
if (doc.documentable_type?.toLowerCase?.().includes('contract')) {
return doc.contract_reference ? `Pogodba ${doc.contract_reference}` : 'Pogodba'
}
return 'Primer'
}
const emit = defineEmits(['view', 'download'])
const formatSize = (bytes) => {
if (bytes == null) return '-'
const thresh = 1024
if (Math.abs(bytes) < thresh) return bytes + ' B'
const units = ['KB', 'MB', 'GB', 'TB']
let u = -1
do { bytes /= thresh; ++u } while (Math.abs(bytes) >= thresh && u < units.length - 1)
return bytes.toFixed(1) + ' ' + units[u]
}
const extFrom = (doc) => {
let ext = (doc?.extension || '').toLowerCase()
if (!ext && doc?.original_name) {
const parts = String(doc.original_name).toLowerCase().split('.')
if (parts.length > 1) ext = parts.pop()
}
// derive from mime
if (!ext && doc?.mime_type) {
const mime = String(doc.mime_type).toLowerCase()
if (mime.includes('pdf')) ext = 'pdf'
else if (mime.includes('word') || mime.includes('msword') || mime.includes('doc')) ext = 'docx'
else if (mime.includes('excel') || mime.includes('sheet')) ext = 'xlsx'
else if (mime.includes('csv')) ext = 'csv'
else if (mime.startsWith('image/')) ext = 'img'
else if (mime.includes('text')) ext = 'txt'
}
return ext
}
const fileTypeInfo = (doc) => {
const ext = extFrom(doc)
const mime = (doc?.mime_type || '').toLowerCase()
switch (ext) {
case 'pdf':
return { icon: faFilePdf, color: 'text-red-600', label: 'PDF' }
case 'doc':
case 'docx':
return { icon: faFileWord, color: 'text-blue-600', label: (ext || 'DOCX').toUpperCase() }
case 'xls':
case 'xlsx':
return { icon: faFileExcel, color: 'text-green-600', label: (ext || 'XLSX').toUpperCase() }
case 'csv':
// treat CSV as spreadsheet-like
return { icon: faFileExcel, color: 'text-emerald-600', label: 'CSV' }
case 'txt':
return { icon: faFileLines, color: 'text-slate-600', label: 'TXT' }
case 'jpg':
case 'jpeg':
case 'png':
case 'img':
return { icon: faFileImage, color: 'text-fuchsia-600', label: (ext === 'img' ? 'IMG' : (ext || 'IMG').toUpperCase()) }
default:
if (mime.startsWith('image/')) return { icon: faFileImage, color: 'text-fuchsia-600', label: 'IMG' }
return { icon: faFile, color: 'text-gray-600', label: (ext || 'FILE').toUpperCase() }
}
}
const hasDesc = (doc) => {
const d = doc?.description
return typeof d === 'string' && d.trim().length > 0
}
const expandedDescKey = ref(null)
const rowKey = (doc, i) => doc?.uuid ?? i
const toggleDesc = (doc, i) => {
const key = rowKey(doc, i)
expandedDescKey.value = expandedDescKey.value === key ? null : key
}
const resolveDownloadUrl = (doc) => {
if (typeof props.downloadUrlBuilder === 'function') return props.downloadUrlBuilder(doc)
// If no builder provided, parent can handle via emitted event
return null
}
const handleDownload = (doc) => {
const url = resolveDownloadUrl(doc)
if (url) {
// Trigger a navigation to a direct-download endpoint; server should set Content-Disposition: attachment
const a = document.createElement('a')
a.href = url
a.target = '_self'
a.rel = 'noopener'
// In many browsers, simply setting href is enough
a.click()
} else {
emit('download', doc)
}
closeActions()
}
</script>
<template>
<div class="relative overflow-x-auto rounded-lg border border-gray-200 bg-white shadow-sm">
<FwbTable hoverable striped class="text-sm">
<FwbTableHead class="sticky top-0 z-10 bg-gray-50/90 backdrop-blur border-b border-gray-200 shadow-sm">
<FwbTableHeadCell class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3">Naziv</FwbTableHeadCell>
<FwbTableHeadCell class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3">Vrsta</FwbTableHeadCell>
<FwbTableHeadCell class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3">Velikost</FwbTableHeadCell>
<FwbTableHeadCell class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3">Dodano</FwbTableHeadCell>
<FwbTableHeadCell class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3">Vir</FwbTableHeadCell>
<FwbTableHeadCell class="w-px" />
</FwbTableHead>
<FwbTableBody>
<template v-for="(doc, i) in documents" :key="doc.uuid || i">
<FwbTableRow>
<FwbTableCell>
<div class="flex items-center gap-2">
<button type="button" class="text-indigo-600 hover:underline" @click="$emit('view', doc)">{{ doc.name }}</button>
<FwbBadge v-if="doc.is_public" type="green">Public</FwbBadge>
</div>
</FwbTableCell>
<FwbTableCell>
<div class="flex items-center gap-2">
<FontAwesomeIcon :icon="fileTypeInfo(doc).icon" :class="['h-5 w-5', fileTypeInfo(doc).color]" />
<span class="text-gray-700">{{ fileTypeInfo(doc).label }}</span>
</div>
</FwbTableCell>
<FwbTableCell>{{ formatSize(doc.size) }}</FwbTableCell>
<FwbTableCell>{{ new Date(doc.created_at).toLocaleString() }}</FwbTableCell>
<FwbTableCell>
<FwbBadge type="purple">{{ sourceLabel(doc) }}</FwbBadge>
</FwbTableCell>
<FwbTableCell class="text-center">
<button
class="inline-flex items-center justify-center h-8 w-8 rounded-full hover:bg-gray-100 disabled:opacity-40 disabled:cursor-not-allowed"
:disabled="!hasDesc(doc)"
:title="hasDesc(doc) ? 'Pokaži opis' : 'Ni opisa'"
type="button"
@click="toggleDesc(doc, i)"
>
<FontAwesomeIcon :icon="faCircleInfo" class="h-4 w-4 text-gray-700" />
</button>
</FwbTableCell>
<FwbTableCell class="text-right whitespace-nowrap">
<Dropdown align="right" width="48">
<template #trigger>
<button
type="button"
class="inline-flex items-center justify-center h-8 w-8 rounded-full hover:bg-gray-100 focus:outline-none"
:title="'Actions'"
>
<FontAwesomeIcon :icon="faEllipsisVertical" class="h-4 w-4 text-gray-700" />
</button>
</template>
<template #content>
<button
type="button"
class="w-full px-3 py-2 text-left text-sm text-gray-700 hover:bg-gray-50 flex items-center gap-2"
@click="handleDownload(doc)"
>
<FontAwesomeIcon :icon="faDownload" class="h-4 w-4 text-gray-600" />
<span>Download file</span>
</button>
<!-- future actions can be slotted here -->
</template>
</Dropdown>
</FwbTableCell>
</FwbTableRow>
<!-- Expanded description row directly below the item -->
<FwbTableRow :key="'desc-' + (doc.uuid || i)" v-if="expandedDescKey === rowKey(doc, i)">
<FwbTableCell :colspan="6" class="bg-gray-50">
<div class="px-4 py-3 text-sm text-gray-700 whitespace-pre-wrap border-l-2 border-indigo-400">
{{ doc.description }}
</div>
</FwbTableCell>
</FwbTableRow>
</template>
</FwbTableBody>
</FwbTable>
<div v-if="!documents || documents.length === 0" class="p-6 text-center text-sm text-gray-500">No documents.</div>
</div>
</template>