Introduced Paginated Data, Listing to StandardEntitys

This commit is contained in:
2025-10-14 15:13:30 +02:00
parent 2d332f1ded
commit 9658ad621a
5 changed files with 546 additions and 5 deletions

View File

@@ -36,6 +36,83 @@ export const useEntities = (
return data return data
} }
const selectPaginated = async (options: {
select?: string
filters?: Record<string, any>
sort?: { field: string; direction?: 'asc' | 'desc' }[]
page?: number
limit?: number
includeArchived?: boolean
noPagination?: boolean,
search?: string,
searchColumns?: string[],
distinctColumns?: string[],
}): Promise<{ data: any[]; meta: any }> => {
const {
select = '*',
filters = {},
sort = [],
page = 1,
limit = 25,
includeArchived = false,
noPagination = false,
search,
searchColumns = [],
distinctColumns = [],
} = options
const queryParams: Record<string, any> = {
select,
page,
limit,
noPagination: noPagination ? 'true' : undefined
}
// --- 🔍 Search-Parameter (optional) ---
if (search && search.trim().length > 0) {
queryParams.search = search.trim()
}
if (searchColumns.length > 0) queryParams.searchColumns = searchColumns.join(',')
if (distinctColumns.length > 0) queryParams.distinctColumns = distinctColumns.join(',')
// --- Sortierung ---
if (sort.length > 0) {
queryParams.sort = sort
.map(s => `${s.field}:${s.direction || 'asc'}`)
.join(',')
}
// --- Filter ---
for (const [key, value] of Object.entries(filters)) {
if (Array.isArray(value)) {
queryParams[`filter[${key}]`] = value.join(',')
} else {
queryParams[`filter[${key}]`] = value
}
}
const response = await useNuxtApp().$api(`/api/resource/${relation}/paginated`, {
method: 'GET',
params: queryParams
})
if (!response) {
return { data: [], meta: null }
}
let data = response.data || []
const meta = response.queryConfig || {}
// --- Optional: Archivierte ausblenden ---
if (!includeArchived) {
data = data.filter((i: any) => !i.archived)
}
return { data, meta }
}
const selectSpecial = async ( const selectSpecial = async (
select: string = "*", select: string = "*",
sortColumn: string | null = null, sortColumn: string | null = null,
@@ -134,7 +211,7 @@ export const useEntities = (
} }
return {select, create, update, archive, selectSingle, selectSpecial} return {select, create, update, archive, selectSingle, selectSpecial, selectPaginated}
} }

View File

@@ -43,7 +43,12 @@ const setupPage = async (sort_column = null, sort_direction = null) => {
console.log(item.value) console.log(item.value)
} else if (mode.value === "list") { } else if (mode.value === "list") {
//Load Data for List //Load Data for List
items.value = await useEntities(type).select(dataType.supabaseSelectWithInformation, sort_column || dataType.supabaseSortColumn, sort_direction === "asc", true) //items.value = await useEntities(type).select(dataType.supabaseSelectWithInformation, sort_column || dataType.supabaseSortColumn, sort_direction === "asc", true)
items.value = await useEntities(type).select({
filters: {},
sort: [],
page:1
})
} }
loaded.value = true loaded.value = true

View File

@@ -0,0 +1,449 @@
<script setup>
import {useTempStore} from "~/stores/temp.js";
import FloatingActionButton from "~/components/mobile/FloatingActionButton.vue";
import EntityTable from "~/components/EntityTable.vue";
import EntityTableMobile from "~/components/EntityTableMobile.vue";
const { has } = usePermission()
defineShortcuts({
'/': () => {
//console.log(searchinput)
//searchinput.value.focus()
document.getElementById("searchinput").focus()
},
'+': () => {
router.push(`/standardEntity/${type}/create`)
},
'Enter': {
usingInput: true,
handler: () => {
router.push(`/standardEntity/${type}/show/${items.value[selectedItem.value].id}`)
}
},
'arrowdown': () => {
if(selectedItem.value < items.value.length - 1) {
selectedItem.value += 1
} else {
selectedItem.value = 0
}
},
'arrowup': () => {
if(selectedItem.value === 0) {
selectedItem.value = items.value.length - 1
} else {
selectedItem.value -= 1
}
}
})
const router = useRouter()
const route = useRoute()
const dataStore = useDataStore()
const tempStore = useTempStore()
//VARS
const type = route.params.type
const dataType = dataStore.dataTypes[type]
const selectedColumns = ref(tempStore.columns[type] ? tempStore.columns[type] : dataType.templateColumns.filter(i => !i.disabledInTable))
const columns = computed(() => dataType.templateColumns.filter((column) => !column.disabledInTable && selectedColumns.value.find(i => i.key === column.key)))
const searchString = ref(tempStore.searchStrings[type] ||'')
const items = ref([])
const itemsMeta = ref({})
const selectedItem = ref(0)
const loading = ref(true)
const initialSetupDone = ref(false)
const pageLimit = ref(15)
const page = ref(tempStore.pages[type] || 1)
const sort = ref({
column: dataType.supabaseSortColumn || "created_at",
direction: 'desc'
})
const columnsToFilter = ref({})
//Functions
const clearSearchString = () => {
tempStore.clearSearchString(type)
searchString.value = ''
setupPage()
}
const performSearch = async () => {
tempStore.modifySearchString(type,searchString)
setupPage()
}
const changePage = (number) => {
page.value = number
tempStore.modifyPages(type, number)
setupPage()
}
const resetColum = (column) => {
columnsToFilter.value[column] = itemsMeta.value.distinctValues[column]
}
const changeSort = (column) => {
if(sort.value.column === column) {
sort.value.direction = sort.value.direction === 'desc' ? 'asc' : 'desc'
} else {
sort.value.direction = "asc"
sort.value.column = column
}
changePage(1)
}
//SETUP
const setupPage = async () => {
loading.value = true
const filters = {
archived:false
}
Object.keys(columnsToFilter.value).forEach((column) => {
if(columnsToFilter.value[column].length !== itemsMeta.value.distinctValues[column].length) {
filters[column] = columnsToFilter.value[column]
}
})
const {data,meta} = await useEntities(type).selectPaginated({
select: dataType.supabaseSelectWithInformation || "*",
filters: filters,
sort: [{field: sort.value.column, direction: sort.value.direction}],
page: page.value,
limit: pageLimit.value,
search: searchString.value,
searchColumns: columns.value.map((i) => i.key),
distinctColumns: dataType.templateColumns.filter(i => i.distinct).map(i => i.key),
})
items.value = data
itemsMeta.value = meta
if(!initialSetupDone.value){
Object.keys(itemsMeta.value.distinctValues).forEach(distinctValue => {
columnsToFilter.value[distinctValue] = itemsMeta.value.distinctValues[distinctValue]
})
}
loading.value = false
initialSetupDone.value = true
}
setupPage()
</script>
<template>
<!-- <FloatingActionButton
:label="`+ ${dataType.labelSingle}`"
variant="outline"
v-if="platform === 'mobile'"
@click="router.push(`/standardEntity/${type}/create`)"
/>-->
<UDashboardNavbar :title="dataType.label" :badge="itemsMeta.total">
<template #toggle>
<div v-if="platform === 'mobile'"></div>
</template>
<template #right>
<UTooltip :text="`${dataType.label} durchsuchen`">
<UInput
id="searchinput"
v-model="searchString"
icon="i-heroicons-funnel"
autocomplete="off"
placeholder="Suche..."
class="hidden lg:block"
@keydown.esc="$event.target.blur()"
@keyup="performSearch"
@change="performSearch"
>
<template #trailing>
<UKbd value="/" />
</template>
</UInput>
</UTooltip>
<UTooltip text="Suche löschen" v-if="searchString.length > 0">
<UButton
icon="i-heroicons-x-mark"
variant="outline"
color="rose"
@click="clearSearchString()"
/>
</UTooltip>
<UTooltip :text="`${dataType.labelSingle} erstellen`">
<UButton
v-if="platform !== 'mobile' && has(`${type}-create`)/*&& useRole().checkRight(`${type}-create`)*/"
@click="router.push(`/standardEntity/${type}/create`)"
class="ml-3"
>+ {{dataType.labelSingle}}</UButton>
</UTooltip>
</template>
</UDashboardNavbar>
<UDashboardToolbar>
<template #left>
<UTooltip :text="`${dataType.label} pro Seite`">
<USelectMenu
:options="[10,15,25,50,100,250]"
v-model="pageLimit"
@change="setupPage"
/>
</UTooltip>
<!-- <UTooltip text="Erste Seite">
<UButton
variant="outline"
@click="changePage(1)"
icon="i-heroicons-chevron-double-left"
:disabled="page <= 1"
/>
</UTooltip>
<UTooltip text="Eine Seite nach vorne">
<UButton
variant="outline"
@click="changePage(page-1)"
icon="i-heroicons-chevron-left"
:disabled="page <= 1"
/>
</UTooltip>
<UTooltip
v-for="pageNumber in itemsMeta.totalPages"
:text="`Zu Seite ${pageNumber} wechseln`"
>
<UButton
:variant="page === pageNumber ? 'solid' : 'outline'"
@click="changePage(pageNumber)"
>
{{pageNumber}}
</UButton>
</UTooltip>
<UTooltip text="Eine Seite nach hinten">
<UButton
variant="outline"
@click="changePage(page+1)"
icon="i-heroicons-chevron-right"
:disabled="page >= itemsMeta.totalPages"
/>
</UTooltip>
<UTooltip text="Letzte Seite">
<UButton
variant="outline"
@click="changePage(itemsMeta.totalPages)"
icon="i-heroicons-chevron-double-right"
:disabled="page >= itemsMeta.totalPages"
/>
</UTooltip>-->
<UPagination
v-if="initialSetupDone && items.length > 0"
:disabled="loading"
v-model="page"
:page-count="pageLimit"
:total="itemsMeta.total"
@update:modelValue="(i) => changePage(i)"
show-first
show-last
:first-button="{ icon: 'i-heroicons-chevron-double-left', color: 'gray' }"
:last-button="{ icon: 'i-heroicons-chevron-double-right', trailing: true, color: 'gray' }"
/>
</template>
<template #right>
<UTooltip text="Angezeigte Spalten auswählen">
<USelectMenu
v-model="selectedColumns"
icon="i-heroicons-adjustments-horizontal-solid"
:options="dataType.templateColumns.filter(i => !i.disabledInTable)"
multiple
class="hidden lg:block"
by="key"
:color="selectedColumns.length !== dataType.templateColumns.filter(i => !i.disabledInTable).length ? 'primary' : 'white'"
:ui-menu="{ width: 'min-w-max' }"
@change="tempStore.modifyColumns(type,selectedColumns)"
>
<template #label>
Spalten
</template>
</USelectMenu>
</UTooltip>
</template>
</UDashboardToolbar>
<UTable
:loading="loading"
:loading-state="{ icon: 'i-heroicons-arrow-path-20-solid', label: 'Loading...' }"
sort-mode="manual"
v-model:sort="sort"
@update:sort="setupPage"
v-if="dataType && columns && items.length > 0"
:rows="items"
:columns="columns"
class="w-full"
style="height: 85dvh"
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
@select="(i) => router.push(`/standardEntity/${type}/show/${i.id}`) "
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: `Keine ${dataType.label} anzuzeigen` }"
>
<template
v-for="column in dataType.templateColumns.filter(i => !i.disabledInTable)"
v-slot:[`${column.key}-header`]="{row}">
<InputGroup>
<UTooltip v-if="column.sortable">
<UButton
variant="outline"
@click="changeSort(column.key)"
:color="sort.column === column.key ? 'primary' : 'white'"
:icon="sort.column === column.key ? (sort.direction === 'asc' ? 'i-heroicons-arrow-up' : 'i-heroicons-arrow-down') : 'i-heroicons-arrows-up-down'"
>
</UButton>
</UTooltip>
<UTooltip
v-if="column.distinct"
:text="!columnsToFilter[column.key]?.length > 0 ? `Keine Einträge für ${column.label} verfügbar` : `${column.label} Spalte nach Einträgen filtern`"
>
<USelectMenu
:options="itemsMeta?.distinctValues?.[column.key]"
v-model="columnsToFilter[column.key]"
multiple
@change="setupPage"
searchable
searchable-placeholder="Suche..."
:search-attributes="[column.key]"
:ui-menu="{ width: 'min-w-max' }"
clear-search-on-close
>
<template #empty>
Keine Einträge in der Spalte {{column.label}}
</template>
<template #default="{open}">
<UButton
:disabled="!columnsToFilter[column.key]?.length > 0"
:variant="columnsToFilter[column.key]?.length !== itemsMeta.distinctValues?.[column.key]?.length ? 'outline' : 'solid'"
:color="columnsToFilter[column.key]?.length !== itemsMeta.distinctValues?.[column.key]?.length ? 'primary' : 'white'"
>
<span class="truncate">{{ column.label }}</span>
<UIcon name="i-heroicons-chevron-right-20-solid" class="w-5 h-5 transition-transform text-gray-400 dark:text-gray-500" :class="[open && 'transform rotate-90']" />
</UButton>
</template>
</USelectMenu>
</UTooltip>
<UButton
variant="solid"
color="white"
v-else
class="mr-2 truncate"
>{{column.label}}</UButton>
<UTooltip
text="Filter zurücksetzen"
v-if="columnsToFilter[column.key]?.length !== itemsMeta.distinctValues?.[column.key]?.length && column.distinct"
>
<UButton
@click="resetColum(column.key)"
variant="outline"
color="rose"
>
X
</UButton>
</UTooltip>
</InputGroup>
</template>
<template #name-data="{row}">
<span
v-if="row.id === items[selectedItem].id"
class="text-primary-500 font-bold">
<UTooltip
:text="row.name"
>
{{dataType.templateColumns.find(i => i.key === "name").maxLength ? (row.name.length > dataType.templateColumns.find(i => i.key === "name").maxLength ? `${row.name.substring(0,dataType.templateColumns.find(i => i.key === "name").maxLength)}...` : row.name ) : row.name}}
</UTooltip> </span>
<span v-else>
<UTooltip
:text="row.name"
>
{{dataType.templateColumns.find(i => i.key === "name").maxLength ? (row.name.length > dataType.templateColumns.find(i => i.key === "name").maxLength ? `${row.name.substring(0,dataType.templateColumns.find(i => i.key === "name").maxLength)}...` : row.name ) : row.name}}
</UTooltip>
</span>
</template>
<template #fullName-data="{row}">
<span
v-if="row.id === items[selectedItem].id"
class="text-primary-500 font-bold">{{row.fullName}}
</span>
<span v-else>
{{row.fullName}}
</span>
</template>
<template #licensePlate-data="{row}">
<span
v-if="row.id === items[selectedItem].id"
class="text-primary-500 font-bold">{{row.licensePlate}}
</span>
<span v-else>
{{row.licensePlate}}
</span>
</template>
<template
v-for="column in dataType.templateColumns.filter(i => i.key !== 'name' && i.key !== 'fullName' && i.key !== 'licensePlate' && !i.disabledInTable)"
v-slot:[`${column.key}-data`]="{row}">
<component v-if="column.component" :is="column.component" :row="row"></component>
<span v-else-if="row[column.key]">
<UTooltip :text="row[column.key]">
{{row[column.key] ? `${column.maxLength ? (row[column.key].length > column.maxLength ? `${row[column.key].substring(0,column.maxLength)}...` : row[column.key]) : row[column.key]} ${column.unit ? column.unit : ''}`: ''}}
</UTooltip>
</span>
</template>
</UTable>
<UCard
class="w-1/3 mx-auto mt-10"
v-else
>
<div
class="flex flex-col text-center"
>
<UIcon
class="mx-auto w-10 h-10 mb-5"
name="i-heroicons-circle-stack-20-solid"/>
<span class="font-bold">Keine {{dataType.label}} anzuzeigen</span>
</div>
</UCard>
</template>
<style scoped>
</style>

View File

@@ -214,7 +214,8 @@ export const useDataStore = defineStore('data', () => {
}, },
inputColumn: "Allgemeines", inputColumn: "Allgemeines",
sortable: true, sortable: true,
maxLength: 20 maxLength: 20,
distinct: true,
}, { }, {
key: "nameAddition", key: "nameAddition",
label: "Firmenname Zusatz", label: "Firmenname Zusatz",
@@ -226,7 +227,8 @@ export const useDataStore = defineStore('data', () => {
return item.isCompany return item.isCompany
}, },
inputColumn: "Allgemeines", inputColumn: "Allgemeines",
maxLength: 20 maxLength: 20,
distinct: true,
},{ },{
key: "salutation", key: "salutation",
label: "Anrede", label: "Anrede",
@@ -253,7 +255,8 @@ export const useDataStore = defineStore('data', () => {
return !item.isCompany return !item.isCompany
}, },
inputColumn: "Allgemeines", inputColumn: "Allgemeines",
sortable: true sortable: true,
distinct: true
},{ },{
key: "title", key: "title",
label: "Titel", label: "Titel",

View File

@@ -6,6 +6,7 @@ export const useTempStore = defineStore('temp', () => {
const searchStrings = ref({}) const searchStrings = ref({})
const filters = ref({}) const filters = ref({})
const columns = ref({}) const columns = ref({})
const pages = ref({})
function modifySearchString(type,input) { function modifySearchString(type,input) {
searchStrings.value[type] = input searchStrings.value[type] = input
@@ -23,6 +24,10 @@ export const useTempStore = defineStore('temp', () => {
columns.value[type] = input columns.value[type] = input
} }
function modifyPages(type,input) {
pages.value[type] = input
}
return { return {
searchStrings, searchStrings,
@@ -32,6 +37,8 @@ export const useTempStore = defineStore('temp', () => {
modifyFilter, modifyFilter,
columns, columns,
modifyColumns, modifyColumns,
modifyPages,
pages
} }