Remodel for Mobile

This commit is contained in:
2025-11-20 12:38:38 +01:00
parent 5f6df7c69d
commit 24f576aeaa
13 changed files with 1226 additions and 595 deletions

View File

@@ -9,7 +9,7 @@ const api = useNuxtApp().$api
const type = route.params.type
const platform = await useCapacitor().getIsPhone() ? "mobile" : "default"
const platform = await useCapacitor().getIsNative() ? "mobile" : "default"
const dataType = dataStore.dataTypes[route.params.type]

View File

@@ -3,9 +3,12 @@ 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";
import {setPageLayout} from "#app";
const { has } = usePermission()
const platformIsNative = useCapacitor().getIsNative()
defineShortcuts({
'/': () => {
//console.log(searchinput)
@@ -67,8 +70,31 @@ const sort = ref({
const columnsToFilter = ref({})
const showMobileFilter = ref(false)
//Functions
function resetMobileFilters() {
if (!itemsMeta.value?.distinctValues) return
Object.keys(itemsMeta.value.distinctValues).forEach(key => {
columnsToFilter.value[key] = [...itemsMeta.value.distinctValues[key]]
})
showMobileFilter.value = false
setupPage()
}
function applyMobileFilters() {
Object.keys(columnsToFilter.value).forEach(key => {
tempStore.modifyFilter(type, key, columnsToFilter.value[key])
})
showMobileFilter.value = false
setupPage()
}
const clearSearchString = () => {
tempStore.clearSearchString(type)
searchString.value = ''
@@ -77,13 +103,14 @@ const clearSearchString = () => {
const performSearch = async () => {
tempStore.modifySearchString(type,searchString)
changePage(1,true)
setupPage()
}
const changePage = (number) => {
const changePage = (number, noSetup = false) => {
page.value = number
tempStore.modifyPages(type, number)
setupPage()
if(!noSetup) setupPage()
}
@@ -99,10 +126,23 @@ const changeSort = (column) => {
changePage(1)
}
const isFiltered = computed(() => {
if (!itemsMeta.value?.distinctValues) return false
return Object.keys(columnsToFilter.value).some(key => {
const allValues = itemsMeta.value.distinctValues[key]
const selected = columnsToFilter.value[key]
if (!allValues || !selected) return false
return selected.length !== allValues.length
})
})
//SETUP
const setupPage = async () => {
loading.value = true
setPageLayout(platformIsNative ? "mobile" : "default")
const filters = {
archived:false
@@ -160,17 +200,11 @@ const handleFilterChange = async (action,column) => {
</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>
<div v-if="platformIsNative"></div>
</template>
<template #right>
<template #right v-if="!platformIsNative">
<UTooltip :text="`${dataType.label} durchsuchen`">
<UInput
id="searchinput"
@@ -211,58 +245,17 @@ const handleFilterChange = async (action,column) => {
</UDashboardNavbar>
<UDashboardToolbar>
<UDashboardToolbar v-if="!platformIsNative">
<template #left>
<UTooltip :text="`${dataType.label} pro Seite`">
<USelectMenu
:options="[10,15,25,50,100,250]"
:options="[{value:10},{value:15, disabled: itemsMeta.total < 15},{value:25, disabled: itemsMeta.total < 25},{value:50, disabled: itemsMeta.total < 50},{value:100, disabled: itemsMeta.total < 100},{value:250, disabled: itemsMeta.total < 250}]"
v-model="pageLimit"
value-attribute="value"
option-attribute="value"
@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"
@@ -299,95 +292,96 @@ const handleFilterChange = async (action,column) => {
</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 && !loading"
: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">
<div v-if="!platformIsNative">
<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 && !loading"
: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="handleFilterChange('change', column.key)"
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="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'"
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>
</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="handleFilterChange('change', column.key)"
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>
<UButton
@click="handleFilterChange('reset',column.key)"
variant="outline"
color="rose"
>
X
</UButton>
</UTooltip>
</USelectMenu>
</UTooltip>
<UButton
variant="solid"
color="white"
v-else
class="mr-2 truncate"
>{{column.label}}</UButton>
</InputGroup>
<UTooltip
text="Filter zurücksetzen"
v-if="columnsToFilter[column.key]?.length !== itemsMeta.distinctValues?.[column.key]?.length && column.distinct"
>
<UButton
@click="handleFilterChange('reset',column.key)"
variant="outline"
color="rose"
>
X
</UButton>
</UTooltip>
</InputGroup>
</template>
<template #name-data="{row}">
</template>
<template #name-data="{row}">
<span
v-if="row.id === items[selectedItem].id"
class="text-primary-500 font-bold">
@@ -396,61 +390,236 @@ const handleFilterChange = async (action,column) => {
>
{{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>
<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}">
</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>
<span v-else>
{{row.fullName}}
</span>
</template>
<template #licensePlate-data="{row}">
</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>
<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]">
</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-if="!loading"
>
<div
class="flex flex-col text-center"
</template>
</UTable>
<UCard
class="w-1/3 mx-auto mt-10"
v-else-if="!loading"
>
<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
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>
<UProgress v-else animation="carousel" class="w-3/4 mx-auto mt-5"></UProgress>
</div>
<div v-else class="relative flex flex-col h-[calc(100dvh-80px)]">
<!-- Mobile Searchbar (sticky top) -->
<div class="p-2 bg-white dark:bg-gray-900 border-b sticky top-0 z-20">
<InputGroup>
<UInput
v-model="searchString"
icon="i-heroicons-magnifying-glass"
placeholder="Suche..."
@keyup="performSearch"
@change="performSearch"
class="w-full"
/>
<UButton
v-if="searchString.length > 0"
icon="i-heroicons-x-mark"
variant="ghost"
color="rose"
@click="clearSearchString()"
/>
<UButton
icon="i-heroicons-funnel"
variant="ghost"
:color="isFiltered ? 'primary' : 'gray'"
@click="showMobileFilter = true"
/>
</InputGroup>
</div>
<!-- Scroll Area -->
<UDashboardPanelContent class="flex-1 overflow-y-auto px-2 py-3 space-y-3 pb-[calc(8vh+env(safe-area-inset-bottom))] mobile-scroll-area">
</UCard>
<UProgress v-else animation="carousel" class="w-3/4 mx-auto mt-5"></UProgress>
<UCard
v-for="item in items"
:key="item.id"
class="p-4 rounded-xl shadow-sm border cursor-pointer active:scale-[0.98] transition"
@click="router.push(`/standardEntity/${type}/show/${item.id}`)"
>
<div class="flex items-center justify-between mb-1">
<p class="text-base font-semibold truncate text-primary-600">
{{
dataType.templateColumns.find(i => i.title)?.key
? item[dataType.templateColumns.find(i => i.title).key]
: null
}}
</p>
<UIcon
name="i-heroicons-chevron-right-20-solid"
class="text-gray-400 w-5 h-5 flex-shrink-0"
/>
</div>
<p class="text-sm text-gray-500 truncate">
{{ dataType.numberRangeHolder ? item[dataType.numberRangeHolder] : null }}
</p>
<div
v-for="secondInfo in dataType.templateColumns.filter(i => i.secondInfo)"
:key="secondInfo.key"
class="text-sm text-gray-400 truncate"
>
{{
(secondInfo.secondInfoKey && item[secondInfo.key])
? item[secondInfo.key][secondInfo.secondInfoKey]
: item[secondInfo.key]
}}
</div>
</UCard>
<div
v-if="!loading && items.length > 0"
class="p-4 bg-white dark:bg-gray-900 border-t flex items-center justify-center mt-4 rounded-xl"
>
<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' }"
/>
</div>
<!-- Empty -->
<UCard
v-if="!loading && items.length === 0"
class="mx-auto mt-10 p-6 text-center"
>
<UIcon name="i-heroicons-circle-stack-20-solid" class="mx-auto w-10 h-10 mb-3"/>
<p class="font-bold">Keine {{ dataType.label }} gefunden</p>
</UCard>
<div v-if="loading" class="mt-5">
<UProgress animation="carousel" class="w-3/4 mx-auto"></UProgress>
</div>
</UDashboardPanelContent>
<!-- Mobile Filter Slideover -->
<USlideover
v-model="showMobileFilter"
side="bottom"
:ui="{ width: '100%', height: 'auto', maxHeight: '90vh' }"
class="pb-[env(safe-area-inset-bottom)]"
>
<!-- Header -->
<div class="p-4 border-b flex items-center justify-between flex-shrink-0">
<h2 class="text-xl font-bold">Filter</h2>
<UButton
icon="i-heroicons-x-mark"
variant="ghost"
@click="showMobileFilter = false"
/>
</div>
<!-- Scrollable content -->
<div class="flex-1 overflow-y-auto p-4 space-y-6">
<div
v-for="column in dataType.templateColumns.filter(c => c.distinct)"
:key="column.key"
class="space-y-2"
>
<p class="font-semibold">{{ column.label }}</p>
<USelectMenu
v-model="columnsToFilter[column.key]"
:options="itemsMeta?.distinctValues?.[column.key]"
multiple
searchable
:search-attributes="[column.key]"
placeholder="Auswählen…"
:ui-menu="{ width: '100%' }"
/>
</div>
</div>
<!-- Footer FIXED in card -->
<div
class="
flex justify-between gap-3
px-4 py-4 border-t flex-shrink-0
bg-white dark:bg-gray-900
rounded-b-2xl
"
>
<UButton
color="rose"
variant="outline"
class="flex-1"
@click="resetMobileFilters"
>
Zurücksetzen
</UButton>
<UButton
color="primary"
class="flex-1"
@click="applyMobileFilters"
>
Anwenden
</UButton>
</div>
</USlideover>
</div>
</template>
<style scoped>