Start UI Change

This commit is contained in:
2026-03-21 21:13:22 +01:00
parent cfd84b773f
commit b009ac845f
65 changed files with 2837 additions and 2114 deletions

View File

@@ -1,7 +1,9 @@
export default defineAppConfig({ export default defineAppConfig({
ui: { ui: {
colors: {
primary: 'green', primary: 'green',
gray: 'slate', neutral: 'slate'
},
tooltip: { tooltip: {
background: '!bg-background' background: '!bg-background'
}, },

View File

@@ -0,0 +1,26 @@
@import "tailwindcss";
@import "@nuxt/ui-pro";
@theme static {
--font-sans: "SF Pro Text", "SF Pro Display", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--font-mono: "SF Mono", "Cascadia Code", "JetBrains Mono", Consolas, "Liberation Mono", Menlo, monospace;
--color-green-50: #f4fbf2;
--color-green-100: #e7f7e1;
--color-green-200: #cdeec4;
--color-green-300: #a6e095;
--color-green-400: #69c350;
--color-green-500: #53ad3a;
--color-green-600: #418e2b;
--color-green-700: #357025;
--color-green-800: #2d5922;
--color-green-900: #254a1d;
--color-green-950: #10280b;
}
:root {
--ui-container: 90rem;
}
body {
font-family: var(--font-sans);
}

View File

@@ -38,7 +38,7 @@ const emitConfirm = () => {
> >
Archivieren Archivieren
</UButton> </UButton>
<UModal v-model="showModal"> <UModal v-model:open="showModal">
<UCard> <UCard>
<template #header> <template #header>
<span class="text-md font-bold">Archivieren bestätigen</span> <span class="text-md font-bold">Archivieren bestätigen</span>

View File

@@ -140,7 +140,7 @@ loadAccounts()
</InputGroup> </InputGroup>
</div> </div>
<UModal v-model="showCreate"> <UModal v-model:open="showCreate">
<UCard> <UCard>
<template #header>Neue Bankverbindung erstellen</template> <template #header>Neue Bankverbindung erstellen</template>
<div class="space-y-3"> <div class="space-y-3">

View File

@@ -31,7 +31,8 @@ const emitConfirm = () => {
> >
<slot name="button"></slot> <slot name="button"></slot>
</UButton> </UButton>
<UModal v-model="showModal"> <UModal v-model:open="showModal">
<template #content>
<UCard> <UCard>
<template #header> <template #header>
<slot name="header"></slot> <slot name="header"></slot>
@@ -58,6 +59,7 @@ const emitConfirm = () => {
</div> </div>
</template> </template>
</UCard> </UCard>
</template>
</UModal> </UModal>
</template> </template>

View File

@@ -227,9 +227,14 @@ defineShortcuts({
width: 4px; width: 4px;
} }
.custom-scrollbar::-webkit-scrollbar-track { .custom-scrollbar::-webkit-scrollbar-track {
@apply bg-transparent; background: transparent;
} }
.custom-scrollbar::-webkit-scrollbar-thumb { .custom-scrollbar::-webkit-scrollbar-thumb {
@apply bg-gray-200 dark:bg-gray-700 rounded-full; background: #e5e7eb;
border-radius: 9999px;
}
:global(.dark) .custom-scrollbar::-webkit-scrollbar-thumb {
background: #374151;
} }
</style> </style>

View File

@@ -156,6 +156,7 @@ const moveFile = async () => {
<template> <template>
<UModal fullscreen > <UModal fullscreen >
<template #content>
<UCard :ui="{ body: { base: 'flex-1' }, ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }" class="h-full"> <UCard :ui="{ body: { base: 'flex-1' }, ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }" class="h-full">
<template #header> <template #header>
<div class="flex flex-row justify-between"> <div class="flex flex-row justify-between">
@@ -352,6 +353,7 @@ const moveFile = async () => {
</div> </div>
</div> </div>
</UCard> </UCard>
</template>
</UModal> </UModal>
</template> </template>

View File

@@ -78,6 +78,7 @@ const fileNames = computed(() => {
<template> <template>
<UModal> <UModal>
<template #content>
<div ref="dropZoneRef" class="relative h-full flex flex-col"> <div ref="dropZoneRef" class="relative h-full flex flex-col">
<div <div
@@ -153,6 +154,7 @@ const fileNames = computed(() => {
</template> </template>
</UCard> </UCard>
</div> </div>
</template>
</UModal> </UModal>
</template> </template>

View File

@@ -191,14 +191,14 @@ const filteredRows = computed(() => {
<EntityTableMobile <EntityTableMobile
v-if="platform === 'mobile'" v-if="platform === 'mobile'"
:type="props.type" :type="props.type"
:columns="columns" :columns="normalizeTableColumns(columns)"
:rows="filteredRows" :rows="filteredRows"
/> />
<EntityTable <EntityTable
v-else v-else
@sort="(i) => emit('sort',i)" @sort="(i) => emit('sort',i)"
:type="props.type" :type="props.type"
:columns="columns" :columns="normalizeTableColumns(columns)"
:rows="filteredRows" :rows="filteredRows"
:loading="props.loading" :loading="props.loading"
/> />

View File

@@ -114,7 +114,7 @@ setup()
<div class="scroll" style="height: 70vh"> <div class="scroll" style="height: 70vh">
<EntityTable <EntityTable
:type="type" :type="type"
:columns="columns" :columns="normalizeTableColumns(columns)"
:rows="props.item[type]" :rows="props.item[type]"
style style
/> />

View File

@@ -249,7 +249,7 @@ const selectItem = (item) => {
</Toolbar> </Toolbar>
<UTable <UTable
:rows="props.item.createddocuments.filter(i => !i.archived)" :rows="props.item.createddocuments.filter(i => !i.archived)"
:columns="columns" :columns="normalizeTableColumns(columns)"
class="w-full" class="w-full"
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }" :ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
@select="selectItem" @select="selectItem"

View File

@@ -94,7 +94,7 @@ function isImage(file) {
</UCard> </UCard>
<!-- 📱 PDF / IMG Viewer Slideover --> <!-- 📱 PDF / IMG Viewer Slideover -->
<UModal v-model="showViewer" side="bottom" class="h-[100dvh]" fullscreen> <UModal v-model:open="showViewer" side="bottom" class="h-[100dvh]" fullscreen>
<!-- Header --> <!-- Header -->
<div class="p-4 border-b flex justify-between items-center flex-shrink-0"> <div class="p-4 border-b flex justify-between items-center flex-shrink-0">
<h3 class="font-bold truncate max-w-[70vw]">{{ activeFile?.path?.split("/").pop() }}</h3> <h3 class="font-bold truncate max-w-[70vw]">{{ activeFile?.path?.split("/").pop() }}</h3>

View File

@@ -78,7 +78,7 @@ const renderedAllocations = computed(() => {
<UTable <UTable
v-if="props.item.statementallocations" v-if="props.item.statementallocations"
:rows="renderedAllocations" :rows="renderedAllocations"
:columns="[{key:'amount', label:'Betrag'},{key:'date', label:'Datum'},{key:'partner', label:'Partner'},{key:'description', label:'Beschreibung'}]" :columns="normalizeTableColumns([{key:'amount', label:'Betrag'},{key:'date', label:'Datum'},{key:'partner', label:'Partner'},{key:'description', label:'Beschreibung'}])"
@select="(i) => selectAllocation(i)" @select="(i) => selectAllocation(i)"
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Buchungen anzuzeigen' }" :empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Buchungen anzuzeigen' }"
> >

View File

@@ -95,26 +95,26 @@ const changeActivePhase = async (key) => {
<UAccordion <UAccordion
:items="renderedPhases" :items="renderedPhases"
> >
<template #default="{item,index,open}"> <template #default="slotProps">
<UButton <UButton
variant="ghost" variant="ghost"
:color="item.active ? 'primary' : 'white'" :color="slotProps.item.active ? 'primary' : 'white'"
class="mb-1" class="mb-1"
:disabled="true" :disabled="true"
> >
<template #leading> <template #leading>
<div class="w-6 h-6 flex items-center justify-center -my-1"> <div class="w-6 h-6 flex items-center justify-center -my-1">
<UIcon :name="item.icon" class="w-4 h-4 " /> <UIcon :name="slotProps.item.icon" class="w-4 h-4 " />
</div> </div>
</template> </template>
<span class="truncate"> {{item.label}}</span> <span class="truncate"> {{ slotProps.item.label }}</span>
<template #trailing> <template #trailing>
<UIcon <UIcon
name="i-heroicons-chevron-right-20-solid" name="i-heroicons-chevron-right-20-solid"
class="w-5 h-5 ms-auto transform transition-transform duration-200" class="w-5 h-5 ms-auto transform transition-transform duration-200"
:class="[open && 'rotate-90']" :class="[slotProps?.open && 'rotate-90']"
/> />
</template> </template>

View File

@@ -67,7 +67,7 @@ const columns = [
<UCard class="mt-5"> <UCard class="mt-5">
<UTable <UTable
class="mt-3" class="mt-3"
:columns="columns" :columns="normalizeTableColumns(columns)"
:rows="props.item.times" :rows="props.item.times"
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Noch keine Einträge' }" :empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Noch keine Einträge' }"
> >

View File

@@ -62,6 +62,7 @@
column: dataType.sortColumn || "date", column: dataType.sortColumn || "date",
direction: 'desc' direction: 'desc'
}) })
const normalizedColumns = computed(() => normalizeTableColumns(props.columns))
</script> </script>
@@ -74,7 +75,7 @@
@update:sort="emit('sort',{sort_column: sort.column, sort_direction: sort.direction})" @update:sort="emit('sort',{sort_column: sort.column, sort_direction: sort.direction})"
v-if="dataType && columns" v-if="dataType && columns"
:rows="props.rows" :rows="props.rows"
:columns="props.columns" :columns="normalizedColumns"
class="w-full" class="w-full"
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }" :ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
@select="(i) => router.push(getShowRoute(type, i.id))" @select="(i) => router.push(getShowRoute(type, i.id))"

View File

@@ -55,7 +55,8 @@ setup()
</script> </script>
<template> <template>
<UModal v-model="showMessageModal" prevent-close> <UModal v-model:open="showMessageModal" prevent-close>
<template #content>
<UCard> <UCard>
<template #header> <template #header>
<span class="font-bold">{{messageToShow.title}}</span> <span class="font-bold">{{messageToShow.title}}</span>
@@ -66,6 +67,7 @@ setup()
@click="markMessageAsRead" @click="markMessageAsRead"
>Gelesen</UButton> >Gelesen</UButton>
</UCard> </UCard>
</template>
</UModal> </UModal>
<!-- <UCard <!-- <UCard
@@ -79,7 +81,7 @@ setup()
variant="ghost" variant="ghost"
@click="showMessage(globalMessages[0])" @click="showMessage(globalMessages[0])"
/> />
<UModal v-model="showMessageModal"> <UModal v-model:open="showMessageModal">
<UCard> <UCard>
<template #header> <template #header>
<span class="font-bold">{{messageToShow.title}}</span> <span class="font-bold">{{messageToShow.title}}</span>

View File

@@ -125,6 +125,7 @@ function onSelect (option) {
<UModal <UModal
v-model="showCommandPalette" v-model="showCommandPalette"
> >
<template #content>
<UCommandPalette <UCommandPalette
v-model="selectedCommand" v-model="selectedCommand"
:groups="groups" :groups="groups"
@@ -132,6 +133,7 @@ function onSelect (option) {
@update:model-value="onSelect" @update:model-value="onSelect"
ref="commandPaletteRef" ref="commandPaletteRef"
/> />
</template>
</UModal> </UModal>
</template> </template>

View File

@@ -2,9 +2,16 @@
import dayjs from 'dayjs' import dayjs from 'dayjs'
const { isHelpSlideoverOpen } = useDashboard() const { isHelpSlideoverOpen } = useDashboard()
const { metaSymbol } = useShortcuts()
const { entries, pending, error, seenState, refresh, markAsSeen } = useChangelog() const { entries, pending, error, seenState, refresh, markAsSeen } = useChangelog()
const metaSymbol = computed(() => {
if (import.meta.server) {
return 'Ctrl'
}
return /Mac|iPhone|iPad|iPod/i.test(navigator.platform) ? '⌘' : 'Ctrl'
})
const shortcuts = ref(false) const shortcuts = ref(false)
const query = ref('') const query = ref('')
const toast = useToast() const toast = useToast()
@@ -154,7 +161,7 @@ watch(isHelpSlideoverOpen, async (isOpen) => {
</script> </script>
<template> <template>
<UDashboardSlideover v-model="isHelpSlideoverOpen"> <USlideover v-model:open="isHelpSlideoverOpen" side="right">
<template #title> <template #title>
<UButton <UButton
v-if="shortcuts" v-if="shortcuts"
@@ -168,6 +175,7 @@ watch(isHelpSlideoverOpen, async (isOpen) => {
{{ shortcuts ? 'Shortcuts' : 'Hilfe & Information' }} {{ shortcuts ? 'Shortcuts' : 'Hilfe & Information' }}
</template> </template>
<template #body>
<div v-if="shortcuts" class="space-y-6"> <div v-if="shortcuts" class="space-y-6">
<UInput v-model="query" icon="i-heroicons-magnifying-glass" placeholder="Search..." autofocus color="gray" /> <UInput v-model="query" icon="i-heroicons-magnifying-glass" placeholder="Search..." autofocus color="gray" />
@@ -305,5 +313,6 @@ watch(isHelpSlideoverOpen, async (isOpen) => {
</UForm> </UForm>
</div> </div>
<UProgress class="mt-5" animation="carousel" v-else/>--> <UProgress class="mt-5" animation="carousel" v-else/>-->
</UDashboardSlideover> </template>
</USlideover>
</template> </template>

View File

@@ -79,6 +79,7 @@ const renderText = (text) => {
v-model="showAddHistoryItemModal" v-model="showAddHistoryItemModal"
> >
<template #content>
<UCard class="h-full"> <UCard class="h-full">
<template #header> <template #header>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
@@ -96,8 +97,8 @@ const renderText = (text) => {
v-model="addHistoryItemData.text" v-model="addHistoryItemData.text"
@keyup.meta.enter="addHistoryItem" @keyup.meta.enter="addHistoryItem"
/> />
<!-- TODO: Add Dropdown and Checking for Usernames --> <!-- TODO: Add Dropdown and Checking for Usernames -->
<!-- <template #help> <!-- <template #help>
<UKbd>{{metaSymbol}}</UKbd> <UKbd>Enter</UKbd> Speichern <UKbd>{{metaSymbol}}</UKbd> <UKbd>Enter</UKbd> Speichern
</template>--> </template>-->
@@ -108,6 +109,7 @@ const renderText = (text) => {
<UButton @click="addHistoryItem">Speichern</UButton> <UButton @click="addHistoryItem">Speichern</UButton>
</template> </template>
</UCard> </UCard>
</template>
</UModal> </UModal>
<Toolbar <Toolbar
v-if="!props.renderHeadline && props.elementId && props.type" v-if="!props.renderHeadline && props.elementId && props.type"

View File

@@ -90,6 +90,7 @@ watch(() => labelPrinter.connected, (connected) => {
<template> <template>
<UModal :ui="{ width: 'sm:max-w-5xl' }"> <UModal :ui="{ width: 'sm:max-w-5xl' }">
<template #content>
<UCard class="w-[92vw] max-w-5xl"> <UCard class="w-[92vw] max-w-5xl">
<template #header> <template #header>
@@ -134,5 +135,6 @@ watch(() => labelPrinter.connected, (connected) => {
</template> </template>
</UCard> </UCard>
</template>
</UModal> </UModal>
</template> </template>

View File

@@ -18,7 +18,8 @@ const handleClick = async () => {
<template> <template>
<!-- Printer Button --> <!-- Printer Button -->
<UModal v-model="showPrinterInfo"> <UModal v-model:open="showPrinterInfo">
<template #content>
<UCard> <UCard>
<template #header> <template #header>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
@@ -33,6 +34,7 @@ const handleClick = async () => {
<p>Hardware Version: {{labelPrinter.info.hardwareVersion}}</p> <p>Hardware Version: {{labelPrinter.info.hardwareVersion}}</p>
<p>Software Version: {{labelPrinter.info.softwareVersion}}</p> <p>Software Version: {{labelPrinter.info.softwareVersion}}</p>
</UCard> </UCard>
</template>
</UModal> </UModal>
<UButton <UButton

View File

@@ -1,10 +1,14 @@
<script setup> <script setup>
const props = defineProps({
collapsed: {
type: Boolean,
default: false
}
})
const route = useRoute() const route = useRoute()
const auth = useAuthStore() const auth = useAuthStore()
const { has } = usePermission() const { has } = usePermission()
// Lokaler State für den Taschenrechner
const showCalculator = ref(false)
const tenantExtraModules = computed(() => { const tenantExtraModules = computed(() => {
const modules = auth.activeTenantData?.extraModules const modules = auth.activeTenantData?.extraModules
return Array.isArray(modules) ? modules : [] return Array.isArray(modules) ? modules : []
@@ -19,6 +23,17 @@ const isAdmin = computed(() => Boolean(auth.user?.is_admin))
const tenantFeatures = computed(() => auth.activeTenantData?.features || {}) const tenantFeatures = computed(() => auth.activeTenantData?.features || {})
const featureEnabled = (key) => tenantFeatures.value?.[key] !== false const featureEnabled = (key) => tenantFeatures.value?.[key] !== false
const visibleItems = (items) => items.filter(item => item && !item.disabled) const visibleItems = (items) => items.filter(item => item && !item.disabled)
const isRouteActive = (to) => {
if (!to) {
return false
}
if (to === '/') {
return route.path === '/'
}
return route.path === to || route.path.startsWith(`${to}/`)
}
const links = computed(() => { const links = computed(() => {
const organisationChildren = [ const organisationChildren = [
@@ -284,15 +299,13 @@ const links = computed(() => {
label: pin.label, label: pin.label,
to: pin.link, to: pin.link,
icon: pin.icon, icon: pin.icon,
target: "_blank", target: "_blank"
pinned: true
} }
} else if (pin.type === "standardEntity") { } else if (pin.type === "standardEntity") {
return { return {
label: pin.label, label: pin.label,
to: pin.datatype === "tasks" ? `/tasks/show/${pin.id}` : `/standardEntity/${pin.datatype}/show/${pin.id}`, to: pin.datatype === "tasks" ? `/tasks/show/${pin.id}` : `/standardEntity/${pin.datatype}/show/${pin.id}`,
icon: pin.icon, icon: pin.icon
pinned: true
} }
} }
}), }),
@@ -382,81 +395,80 @@ const links = computed(() => {
]) ])
}) })
const accordionItems = computed(() => const navItems = computed(() =>
links.value.filter(item => Array.isArray(item.children) && item.children.length > 0) links.value
) .filter(Boolean)
.map((item, index) => {
const children = Array.isArray(item.children)
? item.children.map((child, childIndex) => ({
...child,
value: child.id || child.label || `${index}-${childIndex}`,
active: isRouteActive(child.to)
}))
: undefined
const buttonItems = computed(() => const active = item.active || isRouteActive(item.to) || Boolean(children?.some(child => child.active))
links.value.filter(item => !item.children || item.children.length === 0)
return {
...item,
children,
value: item.id || item.label || String(index),
defaultOpen: item.defaultOpen || active,
active,
tooltip: true,
popover: true,
trailingIcon: children?.length ? undefined : ''
}
})
) )
</script> </script>
<template> <template>
<div class="flex flex-col gap-1"> <UNavigationMenu
<UButton :items="navItems"
v-for="item in buttonItems" orientation="vertical"
:key="item.label" :collapsed="props.collapsed"
variant="ghost" tooltip
:color="(item.to && route.path === item.to) ? 'primary' : (item.pinned ? 'amber' : 'gray')" popover
:icon="item.pinned ? 'i-heroicons-star' : item.icon" color="neutral"
highlight
highlight-color="primary"
class="w-full" class="w-full"
:to="item.to" :ui="{
:target="item.target" root: 'w-full',
@click="item.click ? item.click() : null" list: 'space-y-1',
link: 'min-w-0 rounded-lg px-2.5 py-2',
linkLeadingIcon: 'size-5 shrink-0',
linkLabel: 'truncate',
childList: 'ms-0 space-y-1 border-l border-default ps-3',
childLink: 'min-w-0 rounded-lg px-2 py-1.5',
childLinkLabel: 'truncate'
}"
> >
<template #item-leading="{ item, active }">
<UIcon <UIcon
v-if="item.pinned" v-if="item.icon"
:name="item.icon" :name="item.icon"
class="w-5 h-5 me-2" class="size-5 shrink-0"
:class="active ? 'text-primary' : 'text-muted'"
/> />
{{ item.label }} </template>
</UButton>
</div>
<UDivider class="my-2"/> <template #item-trailing="{ item, active }">
<UBadge
<UAccordion v-if="item.badge && !props.collapsed"
:items="accordionItems" color="primary"
:multiple="false" variant="soft"
class="mt-2" size="xs"
> >
<template #default="{ item, open }"> {{ item.badge }}
<UButton </UBadge>
variant="ghost"
:color="(item.children?.some(c => route.path.includes(c.to))) ? 'primary' : 'gray'"
:icon="item.icon"
class="w-full"
>
{{ item.label }}
<template #trailing>
<UIcon <UIcon
name="i-heroicons-chevron-right-20-solid" v-else-if="item.children?.length"
class="w-5 h-5 ms-auto transform transition-transform duration-200" name="i-heroicons-chevron-down-20-solid"
:class="[open && 'rotate-90']" class="size-4 shrink-0 transition-transform"
:class="active ? 'text-primary' : 'text-muted'"
/> />
</template> </template>
</UButton> </UNavigationMenu>
</template>
<template #item="{ item }">
<div class="flex flex-col">
<UButton
v-for="child in item.children"
:key="child.label"
variant="ghost"
:color="child.to === route.path ? 'primary' : 'gray'"
:icon="child.icon"
class="ml-4"
:to="child.to"
:target="child.target"
:disabled="child.disabled"
@click="child.click ? child.click() : null"
>
{{ child.label }}
</UButton>
</div>
</template>
</UAccordion>
<Calculator v-if="showCalculator" v-model="showCalculator"/>
</template> </template>

View File

@@ -36,7 +36,8 @@ const setNotificationAsRead = async (notification) => {
</script> </script>
<template> <template>
<UDashboardSlideover v-model="isNotificationsSlideoverOpen" title="Benachrichtigungen"> <USlideover v-model:open="isNotificationsSlideoverOpen" title="Benachrichtigungen" side="right">
<template #body>
<NuxtLink <NuxtLink
v-for="notification in notifications" v-for="notification in notifications"
:key="notification.id" :key="notification.id"
@@ -59,5 +60,6 @@ const setNotificationAsRead = async (notification) => {
</p> </p>
</div> </div>
</NuxtLink> </NuxtLink>
</UDashboardSlideover> </template>
</USlideover>
</template> </template>

View File

@@ -16,7 +16,8 @@ const onLogout = async () => {
</script> </script>
<template> <template>
<UModal v-model="auth.sessionWarningVisible" prevent-close> <UModal v-model:open="auth.sessionWarningVisible" prevent-close>
<template #content>
<UCard> <UCard>
<template #header> <template #header>
<h3 class="text-lg font-semibold">Sitzung läuft bald ab</h3> <h3 class="text-lg font-semibold">Sitzung läuft bald ab</h3>
@@ -39,5 +40,6 @@ const onLogout = async () => {
</div> </div>
</template> </template>
</UCard> </UCard>
</template>
</UModal> </UModal>
</template> </template>

View File

@@ -124,7 +124,7 @@ async function onSubmit(event: FormSubmitEvent<any>) {
</script> </script>
<template> <template>
<UModal v-model="isOpen"> <UModal v-model:open="isOpen">
<UCard :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }"> <UCard :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
<template #header> <template #header>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">

View File

@@ -61,6 +61,7 @@ setupPage()
<template> <template>
<UModal :fullscreen="props.mode === 'show'"> <UModal :fullscreen="props.mode === 'show'">
<template #content>
<EntityShow <EntityShow
v-if="loaded && props.mode === 'show'" v-if="loaded && props.mode === 'show'"
:type="props.type" :type="props.type"
@@ -78,7 +79,7 @@ setupPage()
:createQuery="props.createQuery" :createQuery="props.createQuery"
:mode="props.mode" :mode="props.mode"
/> />
<!-- <EntityList <!-- <EntityList
v-else-if="loaded && props.mode === 'list'" v-else-if="loaded && props.mode === 'list'"
:type="props.type" :type="props.type"
:items="items" :items="items"
@@ -88,6 +89,7 @@ setupPage()
animation="carousel" animation="carousel"
class="p-5 mt-10" class="p-5 mt-10"
/> />
</template>
</UModal> </UModal>
</template> </template>

View File

@@ -1,27 +1,59 @@
<script setup> <script setup>
const auth = useAuthStore() const auth = useAuthStore()
const selectedTenant = ref(auth.user.tenant_id) const activeTenantName = computed(() => {
return auth.activeTenantData?.name || auth.tenants?.find((tenant) => tenant.id === auth.activeTenant)?.name || 'Mandant waehlen'
})
const tenantInitials = computed(() => {
return activeTenantName.value
.split(' ')
.filter(Boolean)
.slice(0, 2)
.map((part) => part[0]?.toUpperCase() || '')
.join('') || 'M'
})
const tenantItems = computed(() => [
auth.tenants.map((tenant) => ({
label: tenant.name,
icon: tenant.id === auth.activeTenant ? 'i-heroicons-check' : undefined,
disabled: Boolean(tenant.locked),
onSelect: async (event) => {
if (tenant.locked || tenant.id === auth.activeTenant) {
event?.preventDefault?.()
return
}
await auth.switchTenant(tenant.id)
}
}))
])
</script> </script>
<template> <template>
<USelectMenu <USelectMenu
:options="auth.tenants" :items="tenantItems"
value-attribute="id" :content="{ align: 'start', side: 'bottom', sideOffset: 6 }"
class="w-40" :ui="{ content: 'w-[var(--reka-dropdown-menu-trigger-width)] max-w-[var(--reka-dropdown-menu-trigger-width)]' }"
@change="auth.switchTenant(selectedTenant)" class="block w-40"
v-model="selectedTenant" :avatar="{
alt: activeTenantName,
text: tenantInitials,
loading: 'lazy'
}"
> >
<UButton color="gray" variant="ghost" :class="[open && 'bg-gray-50 dark:bg-gray-800']" class="w-full"> <template #default="{ open }">
<UAvatar :alt="auth.activeTenantData?.name" size="md" /> <UButton
color="gray"
<span class="truncate text-gray-900 dark:text-white font-semibold">{{auth.tenants.find(i => auth.activeTenant === i.id).name}}</span> variant="ghost"
class="w-full min-w-0 max-w-full justify-start gap-2 rounded-lg px-2.5 py-2 text-left"
:class="[open && 'bg-gray-100 dark:bg-gray-800']"
>
<span class="min-w-0 flex-1 truncate font-medium text-gray-900 dark:text-white">
{{ activeTenantName }}
</span>
</UButton> </UButton>
<template #option="{option}">
{{option.name}}
</template> </template>
</USelectMenu> </USelectMenu>
</template> </template>

View File

@@ -1,8 +1,6 @@
<script setup> <script setup>
const { isHelpSlideoverOpen } = useDashboard() const { isHelpSlideoverOpen } = useDashboard()
const { isDashboardSearchModalOpen } = useUIState()
const { metaSymbol } = useShortcuts()
const auth = useAuthStore() const auth = useAuthStore()
const items = computed(() => [ const items = computed(() => [
@@ -31,8 +29,8 @@ const items = computed(() => [
<template> <template>
<UDropdown mode="hover" :items="items" :ui="{ width: 'w-full', item: { disabled: 'cursor-text select-text' } }" :popper="{ strategy: 'absolute', placement: 'top' }" class="w-full"> <UDropdown mode="hover" :items="items" :ui="{ width: 'w-full', item: { disabled: 'cursor-text select-text' } }" :popper="{ strategy: 'absolute', placement: 'top' }" class="w-full">
<template #default="{ open }"> <template #default="slotProps">
<UButton color="gray" variant="ghost" class="w-full" :label="auth.user.email" :class="[open && 'bg-gray-50 dark:bg-gray-800']"> <UButton color="gray" variant="ghost" class="w-full" :label="auth.user.email" :class="[slotProps?.open && 'bg-gray-50 dark:bg-gray-800']">
<!-- <template #leading> <!-- <template #leading>
<UAvatar :alt="auth.user.email" size="xs" /> <UAvatar :alt="auth.user.email" size="xs" />
</template>--> </template>-->

View File

@@ -67,6 +67,7 @@ const startImport = () => {
<template> <template>
<UModal :fullscreen="false"> <UModal :fullscreen="false">
<template #content>
<UCard> <UCard>
<template #header> <template #header>
Erstelltes Dokument Kopieren Erstelltes Dokument Kopieren
@@ -101,6 +102,7 @@ const startImport = () => {
</template> </template>
</UCard> </UCard>
</template>
</UModal> </UModal>
</template> </template>

View File

@@ -24,7 +24,7 @@ setupPage()
<UTable <UTable
v-if="openTasks.length > 0" v-if="openTasks.length > 0"
:rows="openTasks" :rows="openTasks"
:columns="[{key:'name',label:'Name'},{key:'categorie',label:'Kategorie'}]" :columns="normalizeTableColumns([{key:'name',label:'Name'},{key:'categorie',label:'Kategorie'}])"
@select="(i) => router.push(`/tasks/show/${i.id}`)" @select="(i) => router.push(`/tasks/show/${i.id}`)"
/> />
<div v-else> <div v-else>

View File

@@ -53,7 +53,10 @@ const emit = defineEmits(["click"])
<style scoped> <style scoped>
/* FAB Basis */ /* FAB Basis */
.fab-base { .fab-base {
@apply rounded-full px-5 py-4 text-lg font-semibold; border-radius: 9999px;
padding: 1rem 1.25rem;
font-size: 1.125rem;
font-weight: 600;
/* Wenn nur ein Icon vorhanden ist → runder Kreis */ /* Wenn nur ein Icon vorhanden ist → runder Kreis */
/* Wenn Label + Icon → Extended FAB */ /* Wenn Label + Icon → Extended FAB */
@@ -61,6 +64,12 @@ const emit = defineEmits(["click"])
/* Optional: Auto-Kreisen wenn kein Label */ /* Optional: Auto-Kreisen wenn kein Label */
#fab:not([label]) { #fab:not([label]) {
@apply w-14 h-14 p-0 flex items-center justify-center text-2xl; width: 3.5rem;
height: 3.5rem;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
} }
</style> </style>

View File

@@ -206,16 +206,62 @@ const addVideo = () => {
<style scoped> <style scoped>
/* Toolbar & Buttons */ /* Toolbar & Buttons */
.toolbar-btn { .toolbar-btn {
@apply p-1.5 rounded text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors text-sm font-medium min-w-[28px] h-[28px] flex items-center justify-center; padding: 0.375rem;
border-radius: 0.25rem;
color: #4b5563;
transition: background-color 0.2s ease, color 0.2s ease;
font-size: 0.875rem;
font-weight: 500;
min-width: 28px;
height: 28px;
display: inline-flex;
align-items: center;
justify-content: center;
} }
.toolbar-btn.is-active { .toolbar-btn.is-active {
@apply bg-gray-200 dark:bg-gray-600 text-black dark:text-white shadow-inner; background: #e5e7eb;
color: #000;
box-shadow: inset 0 2px 4px 0 rgb(0 0 0 / 0.05);
} }
.bubble-btn { .bubble-btn {
@apply px-2 py-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors text-sm min-w-[24px] h-[24px] flex items-center justify-center text-gray-700 dark:text-gray-200; padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
transition: background-color 0.2s ease, color 0.2s ease;
font-size: 0.875rem;
min-width: 24px;
height: 24px;
display: inline-flex;
align-items: center;
justify-content: center;
color: #374151;
} }
.bubble-btn.is-active { .bubble-btn.is-active {
@apply bg-gray-200 dark:bg-gray-600 text-black dark:text-white; background: #e5e7eb;
color: #000;
}
.toolbar-btn:hover,
.bubble-btn:hover {
background: #f3f4f6;
}
:global(.dark) .toolbar-btn {
color: #d1d5db;
}
:global(.dark) .toolbar-btn:hover,
:global(.dark) .bubble-btn:hover {
background: #374151;
}
:global(.dark) .toolbar-btn.is-active,
:global(.dark) .bubble-btn.is-active {
background: #4b5563;
color: #fff;
}
:global(.dark) .bubble-btn {
color: #e5e7eb;
} }
/* GLOBAL EDITOR STYLES */ /* GLOBAL EDITOR STYLES */
@@ -235,20 +281,48 @@ const addVideo = () => {
/* MENTION */ /* MENTION */
.wiki-mention { .wiki-mention {
/* Pill-Shape, grau/neutral statt knallig blau */ /* Pill-Shape, grau/neutral statt knallig blau */
@apply bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-200 px-1.5 py-0.5 rounded-md text-sm font-medium no-underline inline-block mx-0.5 align-middle border border-gray-200 dark:border-gray-700; background: #f3f4f6;
color: #374151;
padding: 0.125rem 0.375rem;
border-radius: 0.375rem;
font-size: 0.875rem;
font-weight: 500;
text-decoration: none;
display: inline-block;
margin-inline: 0.125rem;
vertical-align: middle;
border: 1px solid #e5e7eb;
box-decoration-break: clone; box-decoration-break: clone;
} }
.wiki-mention::before { .wiki-mention::before {
@apply text-gray-400 dark:text-gray-500 mr-0.5; color: #9ca3af;
margin-right: 0.125rem;
} }
.wiki-mention:hover { .wiki-mention:hover {
@apply bg-primary-50 dark:bg-primary-900/30 border-primary-200 dark:border-primary-800 text-primary-700 dark:text-primary-400; background: #eefbf0;
border-color: #bbf7d0;
color: #15803d;
cursor: pointer; cursor: pointer;
} }
:global(.dark) .wiki-mention {
background: #1f2937;
color: #e5e7eb;
border-color: #374151;
}
:global(.dark) .wiki-mention::before {
color: #6b7280;
}
:global(.dark) .wiki-mention:hover {
background: rgb(20 83 45 / 0.3);
border-color: #166534;
color: #4ade80;
}
/* TABLE */ /* TABLE */
table { width: 100% !important; border-collapse: collapse; table-layout: fixed; margin: 1rem 0; } table { width: 100% !important; border-collapse: collapse; table-layout: fixed; margin: 1rem 0; }
th, td { border: 1px solid #d1d5db; padding: 0.5rem; vertical-align: top; box-sizing: border-box; min-width: 1em; } th, td { border: 1px solid #d1d5db; padding: 0.5rem; vertical-align: top; box-sizing: border-box; min-width: 1em; }
@@ -258,7 +332,7 @@ const addVideo = () => {
.column-resize-handle { background-color: #3b82f6; width: 4px; } .column-resize-handle { background-color: #3b82f6; width: 4px; }
/* CODE */ /* CODE */
pre { background: #0d1117; color: #c9d1d9; font-family: 'JetBrains Mono', monospace; padding: 0.75rem 1rem; border-radius: 0.5rem; margin: 1rem 0; overflow-x: auto; } pre { background: #0d1117; color: #c9d1d9; font-family: var(--font-mono); padding: 0.75rem 1rem; border-radius: 0.5rem; margin: 1rem 0; overflow-x: auto; }
code { color: inherit; padding: 0; background: none; font-size: 0.9rem; } code { color: inherit; padding: 0; background: none; font-size: 0.9rem; }
/* IMG */ /* IMG */

View File

@@ -98,7 +98,7 @@
</div> </div>
</div> </div>
<UModal v-model="isCreateModalOpen"> <UModal v-model:open="isCreateModalOpen">
<div class="p-5"> <div class="p-5">
<h3 class="font-bold mb-4">Neue Seite</h3> <h3 class="font-bold mb-4">Neue Seite</h3>
<form @submit.prevent="createPage"> <form @submit.prevent="createPage">

View File

@@ -0,0 +1,28 @@
type LegacyTableColumn = {
id?: string
key?: string
label?: unknown
header?: unknown
accessorKey?: string
[key: string]: unknown
}
export const normalizeTableColumns = (columns: LegacyTableColumn[] = []) => {
return columns.map((column, index) => {
const accessorKey = typeof column.accessorKey === 'string'
? column.accessorKey
: typeof column.key === 'string'
? column.key
: undefined
const header = column.header ?? column.label ?? accessorKey ?? `column_${index}`
const id = column.id ?? accessorKey ?? (typeof header === 'string' ? header : `column_${index}`)
return {
...column,
id,
accessorKey,
header
}
})
}

View File

@@ -240,26 +240,42 @@ onMounted(() => {
</UCard> </UCard>
</UContainer> </UContainer>
</div> </div>
<UDashboardLayout class="safearea" v-else> <div class="safearea flex min-h-screen w-full flex-col overflow-hidden" v-else>
<UDashboardPanel :width="250" :resizable="{ min: 200, max: 300 }" collapsible> <!-- <div
<UDashboardNavbar style="margin-top: env(safe-area-inset-top, 10px) !important;" class="border-b border-default bg-default px-3 py-2"
:class="['!border-transparent']" :ui="{ left: 'flex-1' }"> style="padding-top: max(env(safe-area-inset-top, 0px), 0.5rem);"
<template #left> >
<TenantDropdown class="min-w-0 w-full max-w-sm" />
</div>-->
<UDashboardGroup class="flex min-h-0 flex-1 flex-col overflow-hidden">
<UDashboardSidebar
id="sidebar"
collapsible
resizable
:default-size="18"
:min-size="14"
:max-size="24"
class="shrink-0 border-r border-default bg-default"
>
<template #header>
<TenantDropdown class="w-full"/> <TenantDropdown class="w-full"/>
</template> </template>
</UDashboardNavbar>
<UDashboardSidebar id="sidebar"> <template #default="{ collapsed }">
<MainNav :collapsed="collapsed" />
<MainNav/> </template>
<div class="flex-1"/>
<template #footer>
<template #footer="{ collapsed }">
<div class="flex flex-col gap-3"> <div class="flex flex-col gap-3">
<div class="flex items-center gap-2">
<UColorModeToggle :class="[collapsed ? 'mx-auto' : 'ml-3']"/>
<UDashboardSidebarCollapse v-if="collapsed" class="mx-auto" />
</div>
<UColorModeToggle class="ml-3"/>
<LabelPrinterButton class="w-full"/> <LabelPrinterButton class="w-full"/>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
@@ -272,10 +288,10 @@ onMounted(() => {
:icon="item.icon" :icon="item.icon"
@click="item.click ? item.click() : null" @click="item.click ? item.click() : null"
> >
{{ item.label }} <span v-if="!collapsed">{{ item.label }}</span>
<template #trailing> <template #trailing>
<UBadge v-if="item.badge" color="primary" variant="solid" size="xs"> <UBadge v-if="!collapsed && item.badge" color="primary" variant="solid" size="xs">
{{ item.badge }} {{ item.badge }}
</UBadge> </UBadge>
</template> </template>
@@ -287,19 +303,18 @@ onMounted(() => {
</div> </div>
</template> </template>
</UDashboardSidebar> </UDashboardSidebar>
</UDashboardPanel>
<UDashboardPage>
<UDashboardPanel grow> <div class="flex min-w-0 flex-1 flex-col overflow-hidden">
<slot/> <slot/>
</UDashboardPanel>
</UDashboardPage> </div>
</UDashboardGroup>
<HelpSlideover/> <HelpSlideover/>
<Calculator v-if="calculatorStore.isOpen"/> <Calculator v-if="calculatorStore.isOpen"/>
</div>
</UDashboardLayout>
</div> </div>
<div <div

View File

@@ -7,7 +7,7 @@ export default defineNuxtConfig({
} }
}, },
modules: ['@vite-pwa/nuxt','@pinia/nuxt', '@nuxt/ui', "nuxt-editorjs", '@nuxtjs/fontaine', 'nuxt-viewport', '@nuxtjs/leaflet', '@vueuse/nuxt'], modules: ['@vite-pwa/nuxt','@pinia/nuxt', '@nuxt/ui-pro', "nuxt-editorjs", '@nuxtjs/fontaine', 'nuxt-viewport', '@nuxtjs/leaflet', '@vueuse/nuxt'],
ssr: false, ssr: false,
@@ -15,14 +15,12 @@ export default defineNuxtConfig({
dirs: ['stores'] dirs: ['stores']
}, },
extends: [
'@nuxt/ui-pro'
],
components: [{ components: [{
path: '~/components' path: '~/components'
}], }],
css: ['~/assets/css/main.css'],
build: { build: {
transpile: ['@vuepic/vue-datepicker','@tiptap/vue-3','@tiptap/extension-code-block-lowlight', transpile: ['@vuepic/vue-datepicker','@tiptap/vue-3','@tiptap/extension-code-block-lowlight',
'lowlight',] 'lowlight',]
@@ -74,10 +72,6 @@ export default defineNuxtConfig({
}, },
}, },
ui: {
icons: ['heroicons', 'mdi', 'simple-icons']
},
colorMode: { colorMode: {
preference: 'system' preference: 'system'
}, },

File diff suppressed because it is too large Load Diff

View File

@@ -37,7 +37,8 @@
"@fullcalendar/vue3": "^6.1.10", "@fullcalendar/vue3": "^6.1.10",
"@iconify/json": "^2.2.171", "@iconify/json": "^2.2.171",
"@mmote/niimbluelib": "^0.0.1-alpha.29", "@mmote/niimbluelib": "^0.0.1-alpha.29",
"@nuxt/ui-pro": "^1.6.0", "@nuxt/ui": "^3.3.7",
"@nuxt/ui-pro": "^3.3.7",
"@nuxtjs/fontaine": "^0.4.1", "@nuxtjs/fontaine": "^0.4.1",
"@nuxtjs/google-fonts": "^3.1.0", "@nuxtjs/google-fonts": "^3.1.0",
"@nuxtjs/strapi": "^1.9.3", "@nuxtjs/strapi": "^1.9.3",

View File

@@ -253,7 +253,7 @@ onMounted(loadData)
</template> </template>
<UTable <UTable
:columns="columns" :columns="normalizeTableColumns(columns)"
:rows="periods" :rows="periods"
:loading="loading" :loading="loading"
:empty-state="{ icon: 'i-heroicons-calculator', label: 'Keine Daten für die USt-Auswertung vorhanden' }" :empty-state="{ icon: 'i-heroicons-calculator', label: 'Keine Daten für die USt-Auswertung vorhanden' }"

View File

@@ -195,7 +195,7 @@ setupPage()
</UDashboardToolbar> </UDashboardToolbar>
<UTable <UTable
:rows="filteredRows" :rows="filteredRows"
:columns="columns" :columns="normalizeTableColumns(columns)"
class="w-full" class="w-full"
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }" :ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
@select="(i) => router.push(`/accounts/show/${i.id}`)" @select="(i) => router.push(`/accounts/show/${i.id}`)"

View File

@@ -138,7 +138,7 @@ const saldo = computed(() => {
<UTable <UTable
v-if="statementallocations" v-if="statementallocations"
:rows="renderedAllocations" :rows="renderedAllocations"
:columns="[{key:'amount', label:'Betrag'},{key:'date', label:'Datum'},{key:'partner', label:'Partner'},{key:'description', label:'Beschreibung'}]" :columns="normalizeTableColumns([{key:'amount', label:'Betrag'},{key:'date', label:'Datum'},{key:'partner', label:'Partner'},{key:'description', label:'Beschreibung'}])"
@select="(i) => selectAllocation(i)" @select="(i) => selectAllocation(i)"
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Buchungen anzuzeigen' }" :empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Buchungen anzuzeigen' }"
> >

View File

@@ -601,7 +601,7 @@ onMounted(() => {
</div> </div>
<PageLeaveGuard :when="isSyncing"/> <PageLeaveGuard :when="isSyncing"/>
<UModal v-model="suggestionsModalOpen" :ui="{ width: 'sm:max-w-6xl' }"> <UModal v-model:open="suggestionsModalOpen" :ui="{ width: 'sm:max-w-6xl' }">
<UCard> <UCard>
<template #header> <template #header>
<div class="flex items-center justify-between gap-3"> <div class="flex items-center justify-between gap-3">

View File

@@ -2453,7 +2453,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
icon="i-heroicons-magnifying-glass" icon="i-heroicons-magnifying-glass"
@click="showProductSelectionModal = true" @click="showProductSelectionModal = true"
/> />
<UModal v-model="showProductSelectionModal"> <UModal v-model:open="showProductSelectionModal">
<UCard> <UCard>
<template #header> <template #header>
Artikel Auswählen Artikel Auswählen
@@ -2472,11 +2472,11 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
</InputGroup> </InputGroup>
<UTable <UTable
:rows="selectedProductcategorie !== 'not set' ? products.filter(i => i.productcategories.includes(selectedProductcategorie)) : products.filter(i => i.productcategories.length === 0)" :rows="selectedProductcategorie !== 'not set' ? products.filter(i => i.productcategories.includes(selectedProductcategorie)) : products.filter(i => i.productcategories.length === 0)"
:columns="[ :columns="normalizeTableColumns([
{key: 'name',label:'Name'}, {key: 'name',label:'Name'},
{key: 'manufacturer',label:'Hersteller'}, {key: 'manufacturer',label:'Hersteller'},
{key: 'articleNumber',label:'Artikelnummer'}, {key: 'articleNumber',label:'Artikelnummer'},
]" ])"
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Artikel anzuzeigen' }" :empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Artikel anzuzeigen' }"
@select=" (i) => { @select=" (i) => {
row.product = i.id row.product = i.id
@@ -2525,7 +2525,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
icon="i-heroicons-magnifying-glass" icon="i-heroicons-magnifying-glass"
@click="showServiceSelectionModal = true" @click="showServiceSelectionModal = true"
/> />
<UModal v-model="showServiceSelectionModal"> <UModal v-model:open="showServiceSelectionModal">
<UCard> <UCard>
<template #header> <template #header>
Leistung Auswählen Leistung Auswählen
@@ -2544,11 +2544,11 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
</InputGroup> </InputGroup>
<UTable <UTable
:rows="selectedServicecategorie !== 'not set' ? services.filter(i => i.servicecategories.includes(selectedServicecategorie)) : services.filter(i => i.servicecategories.length === 0)" :rows="selectedServicecategorie !== 'not set' ? services.filter(i => i.servicecategories.includes(selectedServicecategorie)) : services.filter(i => i.servicecategories.length === 0)"
:columns="[ :columns="normalizeTableColumns([
{key: 'name',label:'Name'}, {key: 'name',label:'Name'},
{key: 'serviceNumber',label:'Leistungsnummer'}, {key: 'serviceNumber',label:'Leistungsnummer'},
{key: 'sellingPrice',label:'Verkaufspreis'}, {key: 'sellingPrice',label:'Verkaufspreis'},
]" ])"
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Leistungen anzuzeigen' }" :empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Leistungen anzuzeigen' }"
@select=" (i) => { @select=" (i) => {
row.service = i.id row.service = i.id
@@ -2670,7 +2670,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
v-if="row.agriculture" v-if="row.agriculture"
@click="row.showEditDiesel = true" @click="row.showEditDiesel = true"
/> />
<UModal v-model="row.showEdit"> <UModal v-model:open="row.showEdit">
<UCard> <UCard>
<!-- <template #header> <!-- <template #header>
Zeile bearbeiten Zeile bearbeiten
@@ -2842,7 +2842,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
</UModal> </UModal>
<UModal v-model="row.showEditDiesel"> <UModal v-model:open="row.showEditDiesel">
<UCard> <UCard>
<template #header> <template #header>
Dieselverbrauch bearbeiten Dieselverbrauch bearbeiten

View File

@@ -58,7 +58,7 @@
<template #item="{item}"> <template #item="{item}">
<div style="height: 80vh; overflow-y: scroll"> <div style="height: 80vh; overflow-y: scroll">
<UTable <UTable
:columns="getColumnsForTab(item.key)" :columns="normalizeTableColumns(getColumnsForTab(item.key))"
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Belege anzuzeigen' }" :empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Belege anzuzeigen' }"
:rows="getRowsForTab(item.key)" :rows="getRowsForTab(item.key)"
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }" :ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"

View File

@@ -94,7 +94,7 @@
<UTable <UTable
:rows="filteredRows" :rows="filteredRows"
:columns="columns" :columns="normalizeTableColumns(columns)"
class="w-full" class="w-full"
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }" :ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
@select="(row) => router.push(`/createDocument/edit/${row.id}`)" @select="(row) => router.push(`/createDocument/edit/${row.id}`)"
@@ -139,7 +139,7 @@
</template> </template>
</UTable> </UTable>
<UModal v-model="showExecutionModal" :ui="{ width: 'sm:max-w-4xl' }"> <UModal v-model:open="showExecutionModal" :ui="{ width: 'sm:max-w-4xl' }">
<UCard> <UCard>
<template #header> <template #header>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
@@ -205,7 +205,7 @@
<UTable <UTable
v-model="selectedExecutionRows" v-model="selectedExecutionRows"
:rows="filteredExecutionList" :rows="filteredExecutionList"
:columns="executionColumns" :columns="normalizeTableColumns(executionColumns)"
:ui="{ th: { base: 'whitespace-nowrap' } }" :ui="{ th: { base: 'whitespace-nowrap' } }"
> >
<template #partner-data="{row}"> <template #partner-data="{row}">
@@ -247,7 +247,7 @@
</UCard> </UCard>
</UModal> </UModal>
<USlideover v-model="showExecutionsSlideover" :ui="{ width: 'w-screen max-w-md' }"> <USlideover v-model:open="showExecutionsSlideover" :ui="{ width: 'w-screen max-w-md' }">
<UCard class="flex flex-col flex-1" :ui="{ body: { base: 'flex-1' }, ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }"> <UCard class="flex flex-col flex-1" :ui="{ body: { base: 'flex-1' }, ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
<template #header> <template #header>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">

View File

@@ -111,14 +111,14 @@ const createExport = async () => {
<UTable <UTable
:rows="filteredExports" :rows="filteredExports"
:columns="[ :columns="normalizeTableColumns([
{ key: 'created_at', label: 'Erstellt am' }, { key: 'created_at', label: 'Erstellt am' },
{ key: 'start_date', label: 'Start' }, { key: 'start_date', label: 'Start' },
{ key: 'end_date', label: 'Ende' }, { key: 'end_date', label: 'Ende' },
{ key: 'valid_until', label: 'Gültig bis' }, { key: 'valid_until', label: 'Gültig bis' },
{ key: 'type', label: 'Typ' }, { key: 'type', label: 'Typ' },
{ key: 'download', label: 'Download' }, { key: 'download', label: 'Download' },
]" ])"
> >
<template #created_at-data="{row}"> <template #created_at-data="{row}">
{{dayjs(row.created_at).format("DD.MM.YYYY HH:mm")}} {{dayjs(row.created_at).format("DD.MM.YYYY HH:mm")}}
@@ -137,7 +137,7 @@ const createExport = async () => {
</template> </template>
</UTable> </UTable>
<UModal v-model="showCreateExportModal"> <UModal v-model:open="showCreateExportModal">
<UCard> <UCard>
<template #header> <template #header>
Export erstellen Export erstellen

View File

@@ -453,7 +453,7 @@ const syncdokubox = async () => {
</div> </div>
</UDashboardPanelContent> </UDashboardPanelContent>
<UModal v-model="createFolderModalOpen"> <UModal v-model:open="createFolderModalOpen">
<UCard> <UCard>
<template #header><h3 class="font-bold">Ordner erstellen</h3></template> <template #header><h3 class="font-bold">Ordner erstellen</h3></template>
@@ -494,7 +494,7 @@ const syncdokubox = async () => {
</UCard> </UCard>
</UModal> </UModal>
<UModal v-model="renameModalOpen"> <UModal v-model:open="renameModalOpen">
<UCard> <UCard>
<template #header><h3 class="font-bold">Umbenennen</h3></template> <template #header><h3 class="font-bold">Umbenennen</h3></template>
<UFormGroup label="Neuer Name"> <UFormGroup label="Neuer Name">

View File

@@ -251,7 +251,7 @@ const selectIncomingInvoice = (invoice) => {
sort-mode="manual" sort-mode="manual"
@update:sort="setupPage" @update:sort="setupPage"
:rows="filteredRows.filter(i => item.label === 'Gebucht' ? i.state === 'Gebucht' : i.state !== 'Gebucht' )" :rows="filteredRows.filter(i => item.label === 'Gebucht' ? i.state === 'Gebucht' : i.state !== 'Gebucht' )"
:columns="columns" :columns="normalizeTableColumns(columns)"
class="w-full" class="w-full"
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }" :ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
@select="(i) => selectIncomingInvoice(i) " @select="(i) => selectIncomingInvoice(i) "

View File

@@ -382,8 +382,7 @@ onBeforeUnmount(() => {
</template> </template>
</UDashboardNavbar> </UDashboardNavbar>
<UDashboardPanelContent> <div v-if="visibleWidgets.length > 0" ref="gridElement" class="grid-stack dashboard-grid overflow-y-auto">
<div v-if="visibleWidgets.length > 0" ref="gridElement" class="grid-stack dashboard-grid">
<div <div
v-for="widget in visibleWidgets" v-for="widget in visibleWidgets"
:key="widget.id" :key="widget.id"
@@ -449,9 +448,9 @@ onBeforeUnmount(() => {
Karte hinzufügen Karte hinzufügen
</UButton> </UButton>
</div> </div>
</UDashboardPanelContent>
<UModal v-model="manageCardsOpen"> <UModal v-model:open="manageCardsOpen">
<template #content>
<UCard> <UCard>
<template #header> <template #header>
<div class="flex items-center justify-between gap-3"> <div class="flex items-center justify-between gap-3">
@@ -500,6 +499,7 @@ onBeforeUnmount(() => {
</div> </div>
</div> </div>
</UCard> </UCard>
</template>
</UModal> </UModal>
</div> </div>
</template> </template>

View File

@@ -453,7 +453,7 @@ onMounted(() => {
/> />
</UDashboardPanelContent> </UDashboardPanelContent>
<UModal v-model="isAbsenceModalOpen"> <UModal v-model:open="isAbsenceModalOpen">
<UCard :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }"> <UCard :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
<template #header> <template #header>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">

View File

@@ -247,7 +247,7 @@ const addPhase = () => {
{{ button.label }} {{ button.label }}
</UButton> </UButton>
<UModal v-model="openQuickActionModal"> <UModal v-model:open="openQuickActionModal">
<UCard> <UCard>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white"> <h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">

View File

@@ -88,7 +88,7 @@ const filteredRows = computed(() => {
<UTable <UTable
:rows="filteredRows" :rows="filteredRows"
:columns="columns" :columns="normalizeTableColumns(columns)"
class="w-full" class="w-full"
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }" :ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
@select="(i) => router.push(`/projecttypes/show/${i.id}`) " @select="(i) => router.push(`/projecttypes/show/${i.id}`) "

View File

@@ -105,12 +105,14 @@ const userTableColumns = [
{ key: "tenant_count", label: "Tenants" }, { key: "tenant_count", label: "Tenants" },
{ key: "is_admin", label: "Admin" }, { key: "is_admin", label: "Admin" },
] ]
const normalizedUserTableColumns = normalizeTableColumns(userTableColumns)
const tenantTableColumns = [ const tenantTableColumns = [
{ key: "name", label: "Tenant" }, { key: "name", label: "Tenant" },
{ key: "short", label: "Kürzel" }, { key: "short", label: "Kürzel" },
{ key: "user_count", label: "Benutzer" }, { key: "user_count", label: "Benutzer" },
] ]
const normalizedTenantTableColumns = normalizeTableColumns(tenantTableColumns)
const userTableRows = computed(() => const userTableRows = computed(() =>
sortedUsers.value.map((user) => ({ sortedUsers.value.map((user) => ({
@@ -491,7 +493,7 @@ onMounted(async () => {
<UTable <UTable
v-if="!loading" v-if="!loading"
:rows="userTableRows" :rows="userTableRows"
:columns="userTableColumns" :columns="normalizedUserTableColumns"
@select="selectUser" @select="selectUser"
/> />
@@ -673,7 +675,7 @@ onMounted(async () => {
<UTable <UTable
v-if="!loading" v-if="!loading"
:rows="tenantTableRows" :rows="tenantTableRows"
:columns="tenantTableColumns" :columns="normalizedTenantTableColumns"
@select="selectTenant" @select="selectTenant"
/> />
@@ -740,7 +742,7 @@ onMounted(async () => {
</UTabs> </UTabs>
</UDashboardPanelContent> </UDashboardPanelContent>
<UModal v-model="createUserModalOpen"> <UModal v-model:open="createUserModalOpen">
<UCard> <UCard>
<template #header> <template #header>
<div class="text-lg font-semibold">Benutzer anlegen</div> <div class="text-lg font-semibold">Benutzer anlegen</div>
@@ -797,7 +799,7 @@ onMounted(async () => {
</UCard> </UCard>
</UModal> </UModal>
<UModal v-model="createTenantModalOpen"> <UModal v-model:open="createTenantModalOpen">
<UCard> <UCard>
<template #header> <template #header>
<div class="text-lg font-semibold">Tenant anlegen</div> <div class="text-lg font-semibold">Tenant anlegen</div>

View File

@@ -151,7 +151,7 @@ setupPage()
</template> </template>
</UDashboardNavbar> </UDashboardNavbar>
<UModal v-model="showReqData"> <UModal v-model:open="showReqData">
<UCard> <UCard>
<template #header> <template #header>
Verfügbare Bankkonten Verfügbare Bankkonten
@@ -181,7 +181,7 @@ setupPage()
<UTable <UTable
:rows="bankaccounts" :rows="bankaccounts"
:columns="[ :columns="normalizeTableColumns([
{ {
key: 'expired', key: 'expired',
label: 'Aktiv' label: 'Aktiv'
@@ -198,7 +198,7 @@ setupPage()
key: 'balance', key: 'balance',
label: 'Saldo' label: 'Saldo'
}, },
]" ])"
> >
<template #expired-data="{row}"> <template #expired-data="{row}">
<span v-if="row.expired" class="text-rose-600">Ausgelaufen</span> <span v-if="row.expired" class="text-rose-600">Ausgelaufen</span>

View File

@@ -34,6 +34,7 @@ const columns = computed(() => templateColumns.filter((column) => selectedColumn
<UModal <UModal
v-model="showEmailAddressModal" v-model="showEmailAddressModal"
> >
<template #content>
<UCard> <UCard>
<template #header> <template #header>
E-Mail Adresse E-Mail Adresse
@@ -65,6 +66,7 @@ const columns = computed(() => templateColumns.filter((column) => selectedColumn
</UButton> </UButton>
</template> </template>
</UCard> </UCard>
</template>
</UModal> </UModal>
<UDashboardNavbar title="E-Mail Konten"> <UDashboardNavbar title="E-Mail Konten">
@@ -81,7 +83,7 @@ const columns = computed(() => templateColumns.filter((column) => selectedColumn
</UDashboardNavbar> </UDashboardNavbar>
<UTable <UTable
:rows="items" :rows="items"
:columns="columns" :columns="normalizeTableColumns(columns)"
class="w-full" class="w-full"
@select="(i) => navigateTo(`/settings/emailaccounts/edit/${i.id}`)" @select="(i) => navigateTo(`/settings/emailaccounts/edit/${i.id}`)"
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }" :ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"

View File

@@ -164,13 +164,13 @@ const getDocLabel = (type) => {
:loading="loading" :loading="loading"
v-model:expand="expand" v-model:expand="expand"
:empty-state="{ icon: 'i-heroicons-document-text', label: 'Keine Textvorlagen gefunden' }" :empty-state="{ icon: 'i-heroicons-document-text', label: 'Keine Textvorlagen gefunden' }"
:columns="[ :columns="normalizeTableColumns([
{ key: 'name', label: 'Bezeichnung' }, { key: 'name', label: 'Bezeichnung' },
{ key: 'documentType', label: 'Verwendung' }, { key: 'documentType', label: 'Verwendung' },
{ key: 'pos', label: 'Position' }, { key: 'pos', label: 'Position' },
{ key: 'default', label: 'Standard' }, { key: 'default', label: 'Standard' },
{ key: 'actions', label: '' } { key: 'actions', label: '' }
]" ])"
> >
<template #name-data="{ row }"> <template #name-data="{ row }">
<span class="font-medium text-gray-900 dark:text-white">{{ row.name }}</span> <span class="font-medium text-gray-900 dark:text-white">{{ row.name }}</span>
@@ -236,7 +236,7 @@ const getDocLabel = (type) => {
</UTable> </UTable>
</UDashboardPanelContent> </UDashboardPanelContent>
<UModal v-model="editTemplateModalOpen" :ui="{ width: 'sm:max-w-4xl' }"> <UModal v-model:open="editTemplateModalOpen" :ui="{ width: 'sm:max-w-4xl' }">
<UCard> <UCard>
<template #header> <template #header>
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">

View File

@@ -40,7 +40,7 @@
</UDashboardNavbar> </UDashboardNavbar>
<UTable <UTable
:rows="items" :rows="items"
:columns="columns" :columns="normalizeTableColumns(columns)"
@select="(i) => navigateTo(`/staff/profiles/${i.id}`)" @select="(i) => navigateTo(`/staff/profiles/${i.id}`)"
> >

View File

@@ -296,13 +296,13 @@ await setupPage()
v-if="workingTimeInfo" v-if="workingTimeInfo"
:rows="workingTimeInfo.spans" :rows="workingTimeInfo.spans"
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Anwesenheiten' }" :empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Anwesenheiten' }"
:columns="[ :columns="normalizeTableColumns([
{ key: 'status', label: 'Status' }, { key: 'status', label: 'Status' },
{ key: 'startedAt', label: 'Start' }, { key: 'startedAt', label: 'Start' },
{ key: 'endedAt', label: 'Ende' }, { key: 'endedAt', label: 'Ende' },
{ key: 'duration', label: 'Dauer' }, { key: 'duration', label: 'Dauer' },
{ key: 'type', label: 'Typ' } { key: 'type', label: 'Typ' }
]" ])"
@select="(row) => router.push(`/workingtimes/edit/${row.sourceEventIds[0]}`)" @select="(row) => router.push(`/workingtimes/edit/${row.sourceEventIds[0]}`)"
> >
<template #status-data="{row}"> <template #status-data="{row}">

View File

@@ -228,7 +228,7 @@ onMounted(async () => {
<UCard v-if="view === 'list'" :ui="{ body: { padding: 'p-0 sm:p-0' } }"> <UCard v-if="view === 'list'" :ui="{ body: { padding: 'p-0 sm:p-0' } }">
<UTable <UTable
:rows="entries" :rows="entries"
:columns="[ :columns="normalizeTableColumns([
{ key: 'actions', label: 'Aktionen', class: 'w-32' }, { key: 'actions', label: 'Aktionen', class: 'w-32' },
{ key: 'state', label: 'Status' }, { key: 'state', label: 'Status' },
{ key: 'started_at', label: 'Start' }, { key: 'started_at', label: 'Start' },
@@ -236,7 +236,7 @@ onMounted(async () => {
{ key: 'duration_minutes', label: 'Dauer' }, { key: 'duration_minutes', label: 'Dauer' },
{ key: 'type', label: 'Typ' }, { key: 'type', label: 'Typ' },
{ key: 'description', label: 'Beschreibung' }, { key: 'description', label: 'Beschreibung' },
]" ])"
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Zeiten anzuzeigen' }" :empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Zeiten anzuzeigen' }"
> >
<template #state-data="{ row }"> <template #state-data="{ row }">
@@ -444,7 +444,7 @@ onMounted(async () => {
:default-user-id="selectedUser" :default-user-id="selectedUser"
/> />
<UModal v-model="showRejectModal"> <UModal v-model:open="showRejectModal">
<UCard :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }"> <UCard :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
<template #header> <template #header>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">

View File

@@ -388,7 +388,7 @@ const handleFilterChange = async (action,column) => {
@update:sort="setupPage" @update:sort="setupPage"
v-if="dataType && columns && items.length > 0 && !loading" v-if="dataType && columns && items.length > 0 && !loading"
:rows="items" :rows="items"
:columns="columns" :columns="normalizeTableColumns(columns)"
class="w-full" class="w-full"
style="height: 85dvh" style="height: 85dvh"
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }" :ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
@@ -428,7 +428,7 @@ const handleFilterChange = async (action,column) => {
<template #empty> <template #empty>
Keine Einträge in der Spalte {{column.label}} Keine Einträge in der Spalte {{column.label}}
</template> </template>
<template #default="{open}"> <template #default="slotProps">
<UButton <UButton
:disabled="!columnsToFilter[column.key]?.length > 0" :disabled="!columnsToFilter[column.key]?.length > 0"
:variant="columnsToFilter[column.key]?.length !== itemsMeta.distinctValues?.[column.key]?.length ? 'outline' : 'solid'" :variant="columnsToFilter[column.key]?.length !== itemsMeta.distinctValues?.[column.key]?.length ? 'outline' : 'solid'"
@@ -436,7 +436,7 @@ const handleFilterChange = async (action,column) => {
> >
<span class="truncate">{{ column.label }}</span> <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']" /> <UIcon name="i-heroicons-chevron-right-20-solid" class="w-5 h-5 transition-transform text-gray-400 dark:text-gray-500" :class="[slotProps?.open && 'transform rotate-90']" />
</UButton> </UButton>
</template> </template>

View File

@@ -65,7 +65,7 @@ const addMessage = async () => {
> >
+ Eintrag + Eintrag
</UButton> </UButton>
<UModal v-model="showAddEntryModal"> <UModal v-model:open="showAddEntryModal">
<UCard> <UCard>
<template #header> <template #header>
Eintrag hinzufügen Eintrag hinzufügen

View File

@@ -75,7 +75,7 @@ const filteredRows = computed(() => {
:rows="filteredRows" :rows="filteredRows"
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: `Keine Tickets anzuzeigen` }" :empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: `Keine Tickets anzuzeigen` }"
@select="(i) => router.push(`/support/${i.id}`)" @select="(i) => router.push(`/support/${i.id}`)"
:columns="[{key:'created_at',label:'Datum'}, ...profileStore.currentTenant === 5 ? [{key:'tenant',label:'Tenant'}] : [],{key:'status',label:'Status'},{key:'title',label:'Titel'},{key:'created_by',label:'Ersteller'},{key:'ticketmessages',label:'Nachrichten'}]" :columns="normalizeTableColumns([{key:'created_at',label:'Datum'}, ...profileStore.currentTenant === 5 ? [{key:'tenant',label:'Tenant'}] : [],{key:'status',label:'Status'},{key:'title',label:'Titel'},{key:'created_by',label:'Ersteller'},{key:'ticketmessages',label:'Nachrichten'}])"
> >
<template #tenant-data="{ row }"> <template #tenant-data="{ row }">
{{row.tenant.name}} {{row.tenant.name}}

View File

@@ -88,6 +88,7 @@ const listColumns = [
{ key: "customer", label: "Kunde" }, { key: "customer", label: "Kunde" },
{ key: "plant", label: "Objekt" } { key: "plant", label: "Objekt" }
] ]
const normalizedListColumns = normalizeTableColumns(listColumns)
function getEmptyTask() { function getEmptyTask() {
return { return {
@@ -458,7 +459,7 @@ onMounted(async () => {
<UTable <UTable
v-else-if="filteredTasks.length" v-else-if="filteredTasks.length"
:rows="filteredTasks" :rows="filteredTasks"
:columns="listColumns" :columns="normalizedListColumns"
@select="(task) => openTaskViaRoute(task)" @select="(task) => openTaskViaRoute(task)"
> >
<template #actions-data="{ row }"> <template #actions-data="{ row }">
@@ -497,7 +498,7 @@ onMounted(async () => {
/> />
</UDashboardPanelContent> </UDashboardPanelContent>
<UModal v-model="isModalOpen" :prevent-close="saving || deleting"> <UModal v-model:open="isModalOpen" :prevent-close="saving || deleting">
<UCard> <UCard>
<template #header> <template #header>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">

View File

@@ -91,7 +91,7 @@
</div> </div>
</main> </main>
<UModal v-model="isModalOpen"> <UModal v-model:open="isModalOpen">
<UCard :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }"> <UCard :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
<template #header> <template #header>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">