Mager updated

This commit is contained in:
Simon Pocrnjič
2025-09-27 17:45:55 +02:00
parent d17e34941b
commit 7227c888d4
74 changed files with 6339 additions and 342 deletions
@@ -1,6 +1,6 @@
<script setup>
import { ref, watch } from 'vue';
import Drawer from './Drawer.vue';
import DialogModal from './DialogModal.vue';
import InputLabel from './InputLabel.vue';
import SectionTitle from './SectionTitle.vue';
import TextInput from './TextInput.vue';
@@ -133,7 +133,7 @@ const callSubmit = () => {
</script>
<template>
<Drawer
<DialogModal
:show="show"
@close="close"
>
@@ -193,5 +193,5 @@ const callSubmit = () => {
</div>
</form>
</template>
</Drawer>
</DialogModal>
</template>
+44 -35
View File
@@ -1,6 +1,6 @@
<script setup>
import { FwbButton, FwbModal, FwbTable, FwbTableBody, FwbTableCell, FwbTableHead, FwbTableHeadCell, FwbTableRow } from 'flowbite-vue';
import Drawer from './Drawer.vue';
import { FwbButton, FwbTable, FwbTableBody, FwbTableCell, FwbTableHead, FwbTableHeadCell, FwbTableRow } from 'flowbite-vue';
import DialogModal from './DialogModal.vue';
import { useForm } from '@inertiajs/vue3';
import { ref } from 'vue';
import TextInput from './TextInput.vue';
@@ -63,7 +63,6 @@ const update = () => {
onSuccess: () => {
closeEditor();
formUpdate.reset();
console.log('ssss')
},
preserveScroll: true
});
@@ -99,38 +98,48 @@ const remove = () => {
</script>
<template>
<div class="relative overflow-x-auto">
<FwbTable hoverable>
<FwbTableHead>
<FwbTableHeadCell v-for="h in header">{{ h.data }}</FwbTableHeadCell>
<FwbTableHeadCell v-if="editor"></FwbTableHeadCell>
<FwbTableHeadCell v-else />
</FwbTableHead>
<FwbTableBody>
<FwbTableRow v-for="(row, key, parent_index) in body" :class="row.options.class" >
<FwbTableCell v-for="col in row.cols">
<a v-if="col.link !== undefined" :class="col.link.css" :href="route(col.link.route, col.link.options)">{{ col.data }}</a>
<span v-else>{{ col.data }}</span>
</FwbTableCell>
<FwbTableCell v-if="editor">
<fwb-button class="mr-1" size="sm" color="default" @click="openEditor(row.options.ref, row.options.editable)" outline>Edit</fwb-button>
<fwb-button size="sm" color="red" @click="showModal(row.options.ref.val, row.options.title)" outline>Remove</fwb-button>
</FwbTableCell>
<FwbTableCell v-else />
</FwbTableRow>
</FwbTableBody>
</FwbTable>
<div>
<!-- Header -->
<div v-if="title || description" class="mb-4">
<h2 v-if="title" class="text-lg font-semibold text-gray-900">{{ title }}</h2>
<p v-if="description" class="mt-1 text-sm text-gray-600">{{ description }}</p>
</div>
<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 supports-[backdrop-filter]:bg-gray-50/80 border-b border-gray-200 shadow-sm">
<FwbTableHeadCell v-for="(h, hIndex) in header" :key="hIndex" class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3 first:pl-6 last:pr-6">{{ h.data }}</FwbTableHeadCell>
<FwbTableHeadCell v-if="editor" class="w-px text-gray-700 py-3"></FwbTableHeadCell>
<FwbTableHeadCell v-else class="w-px text-gray-700 py-3" />
</FwbTableHead>
<FwbTableBody>
<FwbTableRow v-for="(row, key, parent_index) in body" :key="key" :class="row.options.class">
<FwbTableCell v-for="(col, colIndex) in row.cols" :key="colIndex" class="align-middle">
<a v-if="col.link !== undefined" :class="col.link.css" :href="route(col.link.route, col.link.options)">{{ col.data }}</a>
<span v-else>{{ col.data }}</span>
</FwbTableCell>
<FwbTableCell v-if="editor" class="text-right whitespace-nowrap">
<fwb-button class="mr-2" size="sm" color="default" @click="openEditor(row.options.ref, row.options.editable)" outline>Edit</fwb-button>
<fwb-button size="sm" color="red" @click="showModal(row.options.ref.val, row.options.title)" outline>Remove</fwb-button>
</FwbTableCell>
<FwbTableCell v-else />
</FwbTableRow>
</FwbTableBody>
</FwbTable>
<div v-if="!body || body.length === 0" class="p-6 text-center text-sm text-gray-500">No records found.</div>
</div>
</div>
<Drawer
<DialogModal
v-if="editor"
:show="drawerUpdateForm"
@close="drawerUpdateForm = false"
maxWidth="xl"
>
<template #title>Update {{ options.editor_data.title }}</template>
<template #content>
<form @submit.prevent="update">
<div v-for="e in options.editor_data.form.el" class="col-span-6 sm:col-span-4 mb-4">
<form @submit.prevent="update" class="pt-2">
<div v-for="(e, eIndex) in options.editor_data.form.el" :key="eIndex" class="col-span-6 sm:col-span-4 mb-4">
<InputLabel :for="e.id" :value="e.label"/>
<TextInput
v-if="e.type === 'text'"
@@ -138,20 +147,20 @@ const remove = () => {
:ref="e.ref"
type="text"
:autocomplete="e.autocomplete"
class="mt-1 block w-full"
class="mt-1 block w-full text-sm"
v-model="formUpdate[e.bind]"
/>
<select
v-else-if="e.type === 'select'"
class="block w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm"
class="block w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm text-sm"
:id="e.id"
:ref="e.ref"
v-model="formUpdate[e.bind]"
>
<option v-for="op in e.selectOptions" :value="op.val">{{ op.desc }}</option>
<option v-for="(op, opIndex) in e.selectOptions" :key="opIndex" :value="op.val">{{ op.desc }}</option>
</select>
</div>
<div class="flex justify-end mt-4">
<div class="flex justify-end mt-6 gap-3">
<ActionMessage :on="formUpdate.recentlySuccessful" class="me-3">
Saved.
</ActionMessage>
@@ -161,7 +170,7 @@ const remove = () => {
</div>
</form>
</template>
</Drawer>
</DialogModal>
<Modal
v-if="editor"
@@ -170,12 +179,12 @@ const remove = () => {
maxWidth="sm"
>
<form @submit.prevent="remove">
<div class="p-3">
<div class="text-lg text-center py-2 mb-4">
<div class="p-6">
<div class="text-base font-medium text-center py-2 mb-4 text-gray-900">
Remove {{ options.editor_data.title }} <b>{{ modalRemoveTitle }}</b>?
</div>
<div class="flex justify-between">
<div class="flex justify-between items-center">
<SecondaryButton type="button" @click="closeModal">
Cancel
</SecondaryButton>
@@ -0,0 +1,116 @@
<script setup>
import DialogModal from '@/Components/DialogModal.vue'
import PrimaryButton from '@/Components/PrimaryButton.vue'
import SecondaryButton from '@/Components/SecondaryButton.vue'
import ActionMessage from '@/Components/ActionMessage.vue'
import InputLabel from '@/Components/InputLabel.vue'
import TextInput from '@/Components/TextInput.vue'
import { useForm } from '@inertiajs/vue3'
import { ref, watch } from 'vue'
const props = defineProps({
show: { type: Boolean, default: false },
postUrl: { type: String, required: true },
})
const emit = defineEmits(['close', 'uploaded'])
const MAX_SIZE = 25 * 1024 * 1024 // 25MB
const ALLOWED_EXTS = ['doc','docx','pdf','txt','csv','xls','xlsx','jpeg','jpg','png']
const form = useForm({
name: '',
description: '',
file: null,
is_public: false,
})
const localError = ref('')
watch(() => props.show, (v) => {
if (!v) return
localError.value = ''
})
const onFileChange = (e) => {
localError.value = ''
const f = e.target.files?.[0]
if (!f) { form.file = null; return }
const ext = (f.name.split('.').pop() || '').toLowerCase()
if (!ALLOWED_EXTS.includes(ext)) {
localError.value = 'Unsupported file type. Allowed: ' + ALLOWED_EXTS.join(', ')
e.target.value = ''
form.file = null
return
}
if (f.size > MAX_SIZE) {
localError.value = 'File is too large. Maximum size is 25MB.'
e.target.value = ''
form.file = null
return
}
form.file = f
if (!form.name) {
form.name = f.name.replace(/\.[^.]+$/, '')
}
}
const submit = () => {
localError.value = ''
if (!form.file) {
localError.value = 'Please choose a file.'
return
}
const ext = (form.file.name.split('.').pop() || '').toLowerCase()
if (!ALLOWED_EXTS.includes(ext)) {
localError.value = 'Unsupported file type. Allowed: ' + ALLOWED_EXTS.join(', ')
return
}
if (form.file.size > MAX_SIZE) {
localError.value = 'File is too large. Maximum size is 25MB.'
return
}
form.post(props.postUrl, {
forceFormData: true,
onSuccess: () => {
emit('uploaded')
close()
form.reset()
},
})
}
const close = () => emit('close')
</script>
<template>
<DialogModal :show="props.show" @close="close" maxWidth="lg">
<template #title>Dodaj dokument</template>
<template #content>
<div class="space-y-4">
<div>
<InputLabel for="doc_name" value="Name" />
<TextInput id="doc_name" class="mt-1 block w-full" v-model="form.name" />
</div>
<div>
<InputLabel for="doc_desc" value="Description" />
<textarea id="doc_desc" class="mt-1 block w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm" rows="3" v-model="form.description"></textarea>
</div>
<div>
<InputLabel for="doc_file" value="File (max 25MB)" />
<input id="doc_file" type="file" class="mt-1 block w-full" @change="onFileChange" accept=".doc,.docx,.pdf,.txt,.csv,.xls,.xlsx,.jpeg,.jpg,.png" />
<div v-if="localError" class="text-sm text-red-600 mt-1">{{ localError }}</div>
</div>
<label class="inline-flex items-center gap-2 text-sm">
<input type="checkbox" v-model="form.is_public" class="rounded" />
Public
</label>
</div>
</template>
<template #footer>
<div class="flex items-center gap-3">
<ActionMessage :on="form.recentlySuccessful">Uploaded.</ActionMessage>
<SecondaryButton type="button" @click="close">Cancel</SecondaryButton>
<PrimaryButton :disabled="form.processing" @click="submit">Upload</PrimaryButton>
</div>
</template>
</DialogModal>
</template>
@@ -0,0 +1,26 @@
<script setup>
import DialogModal from '@/Components/DialogModal.vue'
import SecondaryButton from '@/Components/SecondaryButton.vue'
const props = defineProps({
show: { type: Boolean, default: false },
src: { type: String, default: '' },
title: { type: String, default: 'Document' }
})
const emit = defineEmits(['close'])
</script>
<template>
<DialogModal :show="props.show" @close="$emit('close')" maxWidth="4xl">
<template #title>{{ props.title }}</template>
<template #content>
<div class="h-[70vh]">
<iframe v-if="props.src" :src="props.src" class="w-full h-full rounded border" />
<div v-else class="text-sm text-gray-500">No document to display.</div>
</div>
</template>
<template #footer>
<SecondaryButton type="button" @click="$emit('close')">Close</SecondaryButton>
</template>
</DialogModal>
</template>
+139
View File
@@ -0,0 +1,139 @@
<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 } from '@fortawesome/free-solid-svg-icons'
import { ref } from 'vue'
const props = defineProps({
documents: { type: Array, default: () => [] },
viewUrlBuilder: { type: Function, default: null },
})
const emit = defineEmits(['view'])
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
}
</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">Name</FwbTableHeadCell>
<FwbTableHeadCell class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3">Type</FwbTableHeadCell>
<FwbTableHeadCell class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3">Size</FwbTableHeadCell>
<FwbTableHeadCell class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3">Added</FwbTableHeadCell>
<FwbTableHeadCell class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3">Drugo</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.original_name || 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 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">
<!-- future actions: download/delete -->
</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>
+3 -3
View File
@@ -1,5 +1,5 @@
<script setup>
import Drawer from '@/Components/Drawer.vue';
import DialogModal from '@/Components/DialogModal.vue';
import InputLabel from '@/Components/InputLabel.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import SectionTitle from '@/Components/SectionTitle.vue';
@@ -62,7 +62,7 @@ const updatePerson = () => {
</script>
<template>
<Drawer
<DialogModal
:show="show"
@close="close"
>
@@ -135,5 +135,5 @@ const updatePerson = () => {
</div>
</form>
</template>
</Drawer>
</DialogModal>
</template>
+3 -3
View File
@@ -1,6 +1,6 @@
<script setup>
import { ref, watch } from 'vue';
import Drawer from './Drawer.vue';
import DialogModal from './DialogModal.vue';
import InputLabel from './InputLabel.vue';
import SectionTitle from './SectionTitle.vue';
import TextInput from './TextInput.vue';
@@ -123,7 +123,7 @@ const submit = () => {
</script>
<template>
<Drawer
<DialogModal
:show="show"
@close="close"
>
@@ -189,5 +189,5 @@ const submit = () => {
</div>
</form>
</template>
</Drawer>
</DialogModal>
</template>