New report system and views

This commit is contained in:
Simon Pocrnjič
2026-01-02 12:32:20 +01:00
parent 9fc5b54b8a
commit 703b52ff59
67 changed files with 8255 additions and 2794 deletions
@@ -2,6 +2,7 @@
import { computed, ref, watch } from "vue";
import SkeletonTable from "@/Components/Skeleton/SkeletonTable.vue";
import EmptyState from "@/Components/EmptyState.vue";
import DataTablePaginationClient from "@/Components/DataTable/DataTablePaginationClient.vue";
import {
Table,
TableHeader,
@@ -10,6 +11,8 @@ import {
TableRow,
TableCell,
} from "@/Components/ui/table";
import { Button } from "../ui/button";
import { ArrowDownNarrowWide, ArrowUpWideNarrowIcon } from "lucide-vue-next";
const props = defineProps({
columns: { type: Array, required: true }, // [{ key, label, sortable?, align?, class?, formatter? }]
@@ -29,6 +32,7 @@ const props = defineProps({
rowKey: { type: [String, Function], default: "id" },
showToolbar: { type: Boolean, default: true },
// Pagination UX options
showPagination: { type: Boolean, default: true },
showPageStats: { type: Boolean, default: true },
showGoto: { type: Boolean, default: true },
maxPageLinks: { type: Number, default: 5 }, // odd number preferred
@@ -139,29 +143,6 @@ const pageRows = computed(() => sortedRows.value.slice(startIndex.value, endInde
const showingFrom = computed(() => (total.value === 0 ? 0 : startIndex.value + 1));
const showingTo = computed(() => (total.value === 0 ? 0 : endIndex.value));
const gotoInput = ref("");
function goToPageInput() {
const raw = String(gotoInput.value || "").trim();
const n = Number(raw);
if (!Number.isFinite(n)) return;
const target = Math.max(1, Math.min(lastPage.value, Math.floor(n)));
if (target !== currentPage.value) setPage(target);
gotoInput.value = "";
}
const visiblePages = computed(() => {
const pages = [];
const count = lastPage.value;
if (count <= 1) return [1];
const windowSize = Math.max(3, props.maxPageLinks);
const half = Math.floor(windowSize / 2);
let start = Math.max(1, currentPage.value - half);
let end = Math.min(count, start + windowSize - 1);
start = Math.max(1, Math.min(start, end - windowSize + 1));
for (let p = start; p <= end; p++) pages.push(p);
return pages;
});
function setPage(p) {
emit("update:page", Math.min(Math.max(1, p), lastPage.value));
}
@@ -196,28 +177,26 @@ function setPageSize(ps) {
</div>
</div>
<div
class="relative overflow-x-auto rounded-lg border border-gray-200 bg-white shadow-sm"
>
<Table class="text-sm">
<TableHeader
class="sticky top-0 z-10 bg-gray-50/90 backdrop-blur border-b border-gray-200 shadow-sm"
>
<div>
<Table class="border-t">
<TableHeader>
<TableRow class="border-b">
<TableHead v-for="col in columns" :key="col.key" :class="col.class">
<button
<Button
v-if="col.sortable"
type="button"
class="inline-flex items-center gap-1 hover:text-indigo-600"
variant="ghost"
class="text-left gap-1 p-1"
@click="toggleSort(col)"
:aria-sort="sort?.key === col.key ? sort.direction || 'none' : 'none'"
>
<span class="uppercase">{{ col.label }}</span>
<span v-if="sort?.key === col.key && sort.direction === 'asc'"></span>
<span v-if="sort?.key === col.key && sort.direction === 'asc'"
><ArrowDownNarrowWide
/></span>
<span v-else-if="sort?.key === col.key && sort.direction === 'desc'"
></span
>
</button>
><ArrowUpWideNarrowIcon
/></span>
</Button>
<span v-else>{{ col.label }}</span>
</TableHead>
<TableHead v-if="$slots.actions" class="w-px">&nbsp;</TableHead>
@@ -232,11 +211,7 @@ function setPageSize(ps) {
@click="$emit('row:click', row)"
class="cursor-default hover:bg-gray-50/50"
>
<TableCell
v-for="col in columns"
:key="col.key"
:class="col.class"
>
<TableCell v-for="col in columns" :key="col.key" :class="col.class">
<template v-if="$slots['cell-' + col.key]">
<slot
:name="'cell-' + col.key"
@@ -275,10 +250,7 @@ function setPageSize(ps) {
<TableRow>
<TableCell :colspan="columns.length + ($slots.actions ? 1 : 0)">
<slot name="empty">
<EmptyState
:title="emptyText"
size="sm"
/>
<EmptyState :title="emptyText" size="sm" />
</slot>
</TableCell>
</TableRow>
@@ -286,112 +258,19 @@ function setPageSize(ps) {
</TableBody>
</Table>
</div>
<nav
v-if="showPagination"
class="mt-3 flex flex-wrap items-center justify-between gap-3 text-sm text-gray-700"
aria-label="Pagination"
>
<div v-if="showPageStats">
<span v-if="total > 0"
>Prikazano: {{ showingFrom }}{{ showingTo }} od {{ total }}</span
>
<span v-else>Ni zadetkov</span>
</div>
<div class="flex items-center gap-1">
<!-- First -->
<button
class="px-2 py-1 rounded border border-gray-300 hover:bg-gray-50 disabled:opacity-50"
:disabled="currentPage <= 1"
@click="setPage(1)"
aria-label="Prva stran"
>
««
</button>
<!-- Prev -->
<button
class="px-2 py-1 rounded border border-gray-300 hover:bg-gray-50 disabled:opacity-50"
:disabled="currentPage <= 1"
@click="setPage(currentPage - 1)"
aria-label="Prejšnja stran"
>
«
</button>
<!-- Leading ellipsis / first page when window doesn't include 1 -->
<button
v-if="visiblePages[0] > 1"
class="px-2 py-1 rounded border border-gray-300 hover:bg-gray-50"
@click="setPage(1)"
>
1
</button>
<span v-if="visiblePages[0] > 2" class="px-1">…</span>
<!-- Page numbers -->
<button
v-for="p in visiblePages"
:key="p"
class="px-3 py-1 rounded border transition-colors"
:class="
p === currentPage
? 'border-indigo-600 bg-indigo-600 text-white'
: 'border-gray-300 hover:bg-gray-50'
"
:aria-current="p === currentPage ? 'page' : undefined"
@click="setPage(p)"
>
{{ p }}
</button>
<!-- Trailing ellipsis / last page when window doesn't include last -->
<span v-if="visiblePages[visiblePages.length - 1] < lastPage - 1" class="px-1"
></span
>
<button
v-if="visiblePages[visiblePages.length - 1] < lastPage"
class="px-2 py-1 rounded border border-gray-300 hover:bg-gray-50"
@click="setPage(lastPage)"
>
{{ lastPage }}
</button>
<!-- Next -->
<button
class="px-2 py-1 rounded border border-gray-300 hover:bg-gray-50 disabled:opacity-50"
:disabled="currentPage >= lastPage"
@click="setPage(currentPage + 1)"
aria-label="Naslednja stran"
>
»
</button>
<!-- Last -->
<button
class="px-2 py-1 rounded border border-gray-300 hover:bg-gray-50 disabled:opacity-50"
:disabled="currentPage >= lastPage"
@click="setPage(lastPage)"
aria-label="Zadnja stran"
>
»»
</button>
<!-- Goto page -->
<div v-if="showGoto" class="ms-2 flex items-center gap-1">
<input
v-model="gotoInput"
type="number"
min="1"
:max="lastPage"
inputmode="numeric"
class="w-16 rounded border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 text-sm"
:placeholder="String(currentPage)"
aria-label="Pojdi na stran"
@keyup.enter="goToPageInput"
@blur="goToPageInput"
/>
<span class="text-gray-500">/ {{ lastPage }}</span>
</div>
</div>
</nav>
<div class="px-2 py-4 border-t">
<DataTablePaginationClient
v-if="showPagination"
:current-page="currentPage"
:last-page="lastPage"
:total="total"
:showing-from="showingFrom"
:showing-to="showingTo"
:show-page-stats="showPageStats"
:show-goto="showGoto"
:max-page-links="maxPageLinks"
@update:page="setPage"
/>
</div>
</div>
</template>