Neues Dashboard mit selbstwählbaren und verschiebbaren Cards
This commit is contained in:
17
frontend/package-lock.json
generated
17
frontend/package-lock.json
generated
@@ -68,6 +68,7 @@
|
||||
"date-fns": "^3.6.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"fast-sort": "^3.4.1",
|
||||
"gridstack": "^12.4.2",
|
||||
"handlebars": "^4.7.8",
|
||||
"image-js": "^1.1.0",
|
||||
"leaflet": "^1.9.4",
|
||||
@@ -12114,6 +12115,22 @@
|
||||
"node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/gridstack": {
|
||||
"version": "12.4.2",
|
||||
"resolved": "https://registry.npmjs.org/gridstack/-/gridstack-12.4.2.tgz",
|
||||
"integrity": "sha512-aXbJrQpi3LwpYXYOr4UriPM5uc/dPcjK01SdOE5PDpx2vi8tnLhU7yBg/1i4T59UhNkG/RBfabdFUObuN+gMnw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "paypal",
|
||||
"url": "https://www.paypal.me/alaind831"
|
||||
},
|
||||
{
|
||||
"type": "venmo",
|
||||
"url": "https://www.venmo.com/adumesny"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/gzip-size": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-7.0.0.tgz",
|
||||
|
||||
@@ -81,6 +81,7 @@
|
||||
"date-fns": "^3.6.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"fast-sort": "^3.4.1",
|
||||
"gridstack": "^12.4.2",
|
||||
"handlebars": "^4.7.8",
|
||||
"image-js": "^1.1.0",
|
||||
"leaflet": "^1.9.4",
|
||||
|
||||
564
frontend/pages/index.client.vue
Normal file
564
frontend/pages/index.client.vue
Normal file
@@ -0,0 +1,564 @@
|
||||
<script setup>
|
||||
import { setPageLayout } from "#app"
|
||||
import "gridstack/dist/gridstack.min.css"
|
||||
|
||||
import DisplayIncomeAndExpenditure from "~/components/displayIncomeAndExpenditure.vue"
|
||||
import DisplayOpenBalances from "~/components/displayOpenBalances.vue"
|
||||
import DisplayBankaccounts from "~/components/displayBankaccounts.vue"
|
||||
import DisplayProjectsInPhases from "~/components/displayProjectsInPhases.vue"
|
||||
import DisplayOpenTasks from "~/components/displayOpenTasks.vue"
|
||||
|
||||
setPageLayout("default")
|
||||
|
||||
const toast = useToast()
|
||||
const tempStore = useTempStore()
|
||||
|
||||
const gridElement = ref(null)
|
||||
const grid = shallowRef(null)
|
||||
const isEditMode = ref(false)
|
||||
const manageCardsOpen = ref(false)
|
||||
const widgets = ref([])
|
||||
|
||||
let gridStackClass = null
|
||||
let persistTimeout = null
|
||||
const isSyncingGrid = ref(false)
|
||||
|
||||
const DASHBOARD_WIDGETS = [
|
||||
{
|
||||
id: "income-expense",
|
||||
title: "Einnahmen und Ausgaben",
|
||||
description: "Umsatz- und Ausgabenentwicklung",
|
||||
component: markRaw(DisplayIncomeAndExpenditure),
|
||||
defaultLayout: { x: 0, y: 0, w: 12, h: 4 },
|
||||
minW: 4,
|
||||
minH: 4
|
||||
},
|
||||
{
|
||||
id: "open-balances",
|
||||
title: "Buchhaltung",
|
||||
description: "Offene Rechnungen und Entwurfsstatus",
|
||||
component: markRaw(DisplayOpenBalances),
|
||||
defaultLayout: { x: 0, y: 4, w: 4, h: 3 },
|
||||
minW: 3,
|
||||
minH: 3
|
||||
},
|
||||
{
|
||||
id: "bankaccounts",
|
||||
title: "Bank",
|
||||
description: "Kontostand und offene Zuordnungen",
|
||||
component: markRaw(DisplayBankaccounts),
|
||||
defaultLayout: { x: 4, y: 4, w: 4, h: 3 },
|
||||
minW: 3,
|
||||
minH: 3
|
||||
},
|
||||
{
|
||||
id: "projects",
|
||||
title: "Projekte",
|
||||
description: "Aktuelle Projektphasen",
|
||||
component: markRaw(DisplayProjectsInPhases),
|
||||
defaultLayout: { x: 8, y: 4, w: 4, h: 3 },
|
||||
minW: 3,
|
||||
minH: 3
|
||||
},
|
||||
{
|
||||
id: "tasks",
|
||||
title: "Aufgaben",
|
||||
description: "Offene Aufgaben des aktuellen Nutzers",
|
||||
component: markRaw(DisplayOpenTasks),
|
||||
defaultLayout: { x: 0, y: 7, w: 4, h: 3 },
|
||||
minW: 3,
|
||||
minH: 3
|
||||
}
|
||||
]
|
||||
|
||||
const widgetDefinitions = Object.fromEntries(DASHBOARD_WIDGETS.map((widget) => [widget.id, widget]))
|
||||
|
||||
function normalizeNumber(value, fallback) {
|
||||
const parsed = Number(value)
|
||||
return Number.isFinite(parsed) ? parsed : fallback
|
||||
}
|
||||
|
||||
function serializeWidgets(input) {
|
||||
return input.map((widget) => ({
|
||||
id: widget.id,
|
||||
x: widget.x,
|
||||
y: widget.y,
|
||||
w: widget.w,
|
||||
h: widget.h,
|
||||
visible: widget.visible
|
||||
}))
|
||||
}
|
||||
|
||||
function normalizeDashboardWidgets(storedWidgets) {
|
||||
const storedById = new Map(
|
||||
(Array.isArray(storedWidgets) ? storedWidgets : [])
|
||||
.filter((widget) => widget?.id && widgetDefinitions[widget.id])
|
||||
.map((widget) => [widget.id, widget])
|
||||
)
|
||||
|
||||
return DASHBOARD_WIDGETS.map((definition) => {
|
||||
const stored = storedById.get(definition.id) || {}
|
||||
|
||||
return {
|
||||
id: definition.id,
|
||||
x: normalizeNumber(stored.x, definition.defaultLayout.x),
|
||||
y: normalizeNumber(stored.y, definition.defaultLayout.y),
|
||||
w: normalizeNumber(stored.w, definition.defaultLayout.w),
|
||||
h: normalizeNumber(stored.h, definition.defaultLayout.h),
|
||||
visible: typeof stored.visible === "boolean" ? stored.visible : true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function widgetsSignature(input) {
|
||||
return JSON.stringify(serializeWidgets(input))
|
||||
}
|
||||
|
||||
function getWidgetLayout(id) {
|
||||
return widgets.value.find((widget) => widget.id === id)
|
||||
}
|
||||
|
||||
function updateWidget(id, patch) {
|
||||
widgets.value = widgets.value.map((widget) => {
|
||||
if (widget.id !== id) return widget
|
||||
return { ...widget, ...patch }
|
||||
})
|
||||
}
|
||||
|
||||
function persistWidgets() {
|
||||
tempStore.modifySettings("dashboard", {
|
||||
version: 1,
|
||||
widgets: serializeWidgets(widgets.value)
|
||||
})
|
||||
}
|
||||
|
||||
function schedulePersistWidgets() {
|
||||
clearTimeout(persistTimeout)
|
||||
persistTimeout = setTimeout(() => {
|
||||
persistWidgets()
|
||||
}, 120)
|
||||
}
|
||||
|
||||
async function ensureGridStack() {
|
||||
if (gridStackClass) return gridStackClass
|
||||
const gridStackModule = await import("gridstack")
|
||||
gridStackClass = gridStackModule.GridStack
|
||||
return gridStackClass
|
||||
}
|
||||
|
||||
function gridOptionsFor(widget) {
|
||||
const definition = widgetDefinitions[widget.id]
|
||||
|
||||
return {
|
||||
x: widget.x,
|
||||
y: widget.y,
|
||||
w: widget.w,
|
||||
h: widget.h,
|
||||
minW: definition.minW,
|
||||
minH: definition.minH,
|
||||
id: widget.id
|
||||
}
|
||||
}
|
||||
|
||||
function handleGridChange(_event, items = []) {
|
||||
if (isSyncingGrid.value || !isEditMode.value) return
|
||||
|
||||
let hasChanges = false
|
||||
|
||||
items.forEach((item) => {
|
||||
const widgetId = item.el?.dataset?.widgetId || item.id
|
||||
if (!widgetId) return
|
||||
|
||||
const current = getWidgetLayout(widgetId)
|
||||
if (!current) return
|
||||
|
||||
const nextPatch = {
|
||||
x: normalizeNumber(item.x, current.x),
|
||||
y: normalizeNumber(item.y, current.y),
|
||||
w: normalizeNumber(item.w, current.w),
|
||||
h: normalizeNumber(item.h, current.h)
|
||||
}
|
||||
|
||||
if (current.x === nextPatch.x && current.y === nextPatch.y && current.w === nextPatch.w && current.h === nextPatch.h) {
|
||||
return
|
||||
}
|
||||
|
||||
updateWidget(widgetId, nextPatch)
|
||||
hasChanges = true
|
||||
})
|
||||
|
||||
if (hasChanges) schedulePersistWidgets()
|
||||
}
|
||||
|
||||
function syncGridInteractivity() {
|
||||
if (!grid.value) return
|
||||
grid.value.enableMove(isEditMode.value)
|
||||
grid.value.enableResize(isEditMode.value)
|
||||
}
|
||||
|
||||
async function syncGridWithDom() {
|
||||
if (!import.meta.client) return
|
||||
|
||||
await nextTick()
|
||||
|
||||
if (!gridElement.value) return
|
||||
|
||||
const GridStack = await ensureGridStack()
|
||||
|
||||
if (!grid.value) {
|
||||
isSyncingGrid.value = true
|
||||
grid.value = GridStack.init(
|
||||
{
|
||||
column: 12,
|
||||
float: true,
|
||||
margin: 16,
|
||||
cellHeight: 96,
|
||||
disableOneColumnMode: false,
|
||||
handle: ".dashboard-widget-drag-handle",
|
||||
resizable: {
|
||||
handles: "all"
|
||||
}
|
||||
},
|
||||
gridElement.value
|
||||
)
|
||||
grid.value.on("change", handleGridChange)
|
||||
syncGridInteractivity()
|
||||
isSyncingGrid.value = false
|
||||
}
|
||||
|
||||
const visibleIds = new Set(visibleWidgets.value.map((widget) => widget.id))
|
||||
|
||||
isSyncingGrid.value = true
|
||||
grid.value.batchUpdate()
|
||||
|
||||
;[...grid.value.engine.nodes].forEach((node) => {
|
||||
const widgetId = node.el?.dataset?.widgetId || node.id
|
||||
if (!node.el || !node.el.isConnected || !visibleIds.has(widgetId)) {
|
||||
if (node.el) grid.value.removeWidget(node.el, false, false)
|
||||
}
|
||||
})
|
||||
|
||||
const domWidgets = [...gridElement.value.querySelectorAll(".grid-stack-item")]
|
||||
|
||||
domWidgets.forEach((element) => {
|
||||
const widget = visibleWidgets.value.find((item) => item.id === element.dataset.widgetId)
|
||||
if (!widget) return
|
||||
|
||||
if (!element.gridstackNode) {
|
||||
grid.value.makeWidget(element)
|
||||
}
|
||||
|
||||
grid.value.update(element, gridOptionsFor(widget))
|
||||
})
|
||||
|
||||
grid.value.batchUpdate(false)
|
||||
isSyncingGrid.value = false
|
||||
}
|
||||
|
||||
function addWidget(id) {
|
||||
const widget = getWidgetLayout(id)
|
||||
if (!widget || widget.visible) return
|
||||
|
||||
updateWidget(id, { visible: true })
|
||||
persistWidgets()
|
||||
}
|
||||
|
||||
function toggleEditMode() {
|
||||
isEditMode.value = !isEditMode.value
|
||||
if (!isEditMode.value) manageCardsOpen.value = false
|
||||
}
|
||||
|
||||
function removeWidget(id) {
|
||||
if (visibleWidgets.value.length <= 1) {
|
||||
toast.add({
|
||||
title: "Letzte Karte",
|
||||
description: "Mindestens eine Dashboard-Karte sollte sichtbar bleiben.",
|
||||
color: "orange"
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
updateWidget(id, { visible: false })
|
||||
persistWidgets()
|
||||
}
|
||||
|
||||
function resetDashboard() {
|
||||
widgets.value = normalizeDashboardWidgets()
|
||||
persistWidgets()
|
||||
}
|
||||
|
||||
const visibleWidgets = computed(() =>
|
||||
widgets.value
|
||||
.filter((widget) => widget.visible)
|
||||
.map((widget) => ({
|
||||
...widgetDefinitions[widget.id],
|
||||
...widget
|
||||
}))
|
||||
)
|
||||
|
||||
const hiddenWidgets = computed(() =>
|
||||
DASHBOARD_WIDGETS.filter((definition) => !getWidgetLayout(definition.id)?.visible)
|
||||
)
|
||||
|
||||
watch(
|
||||
() => tempStore.settings?.dashboard,
|
||||
(storedDashboard) => {
|
||||
const nextWidgets = normalizeDashboardWidgets(storedDashboard?.widgets)
|
||||
if (widgetsSignature(nextWidgets) === widgetsSignature(widgets.value)) return
|
||||
widgets.value = nextWidgets
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
visibleWidgets,
|
||||
async () => {
|
||||
await syncGridWithDom()
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
watch(isEditMode, () => {
|
||||
syncGridInteractivity()
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
if (!widgets.value.length) widgets.value = normalizeDashboardWidgets()
|
||||
await syncGridWithDom()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearTimeout(persistTimeout)
|
||||
if (grid.value) {
|
||||
grid.value.off("change", handleGridChange)
|
||||
grid.value.destroy(false)
|
||||
grid.value = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<UDashboardNavbar title="Home">
|
||||
<template #right>
|
||||
<div class="flex items-center gap-2">
|
||||
<UButton
|
||||
:icon="isEditMode ? 'i-heroicons-check' : 'i-heroicons-pencil-square'"
|
||||
:color="isEditMode ? 'primary' : 'gray'"
|
||||
:variant="isEditMode ? 'solid' : 'ghost'"
|
||||
@click="toggleEditMode"
|
||||
>
|
||||
{{ isEditMode ? "Bearbeitung beenden" : "Dashboard bearbeiten" }}
|
||||
</UButton>
|
||||
<UButton
|
||||
v-if="isEditMode && hiddenWidgets.length > 0"
|
||||
icon="i-heroicons-plus"
|
||||
color="white"
|
||||
variant="soft"
|
||||
@click="manageCardsOpen = true"
|
||||
>
|
||||
Karte hinzufügen
|
||||
</UButton>
|
||||
<UButton
|
||||
v-if="isEditMode"
|
||||
icon="i-heroicons-squares-2x2"
|
||||
color="gray"
|
||||
variant="ghost"
|
||||
@click="manageCardsOpen = true"
|
||||
>
|
||||
Karten verwalten
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
</UDashboardNavbar>
|
||||
|
||||
<div v-if="visibleWidgets.length > 0" ref="gridElement" class="grid-stack dashboard-grid overflow-y-auto h-80">
|
||||
<div
|
||||
v-for="widget in visibleWidgets"
|
||||
:key="widget.id"
|
||||
:class="['grid-stack-item', isEditMode ? 'dashboard-widget-editing' : '']"
|
||||
:data-widget-id="widget.id"
|
||||
:gs-x="widget.x"
|
||||
:gs-y="widget.y"
|
||||
:gs-w="widget.w"
|
||||
:gs-h="widget.h"
|
||||
:gs-min-w="widget.minW"
|
||||
:gs-min-h="widget.minH"
|
||||
>
|
||||
<div class="grid-stack-item-content dashboard-grid-item">
|
||||
<div class="dashboard-widget-card">
|
||||
<div class="dashboard-widget-header">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="min-w-0">
|
||||
<div :class="['dashboard-widget-drag-handle font-semibold text-gray-900 dark:text-white', isEditMode ? 'cursor-move' : 'cursor-default']">
|
||||
{{ widget.title }}
|
||||
</div>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ widget.description }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="dashboard-widget-header-actions">
|
||||
<div :id="`dashboard-widget-header-actions-${widget.id}`" class="dashboard-widget-header-target" />
|
||||
<UButtonGroup v-if="isEditMode" size="xs">
|
||||
<UButton
|
||||
color="gray"
|
||||
variant="soft"
|
||||
icon="i-heroicons-arrows-pointing-out"
|
||||
class="dashboard-widget-drag-handle"
|
||||
/>
|
||||
<UButton
|
||||
color="gray"
|
||||
variant="soft"
|
||||
icon="i-heroicons-x-mark"
|
||||
:disabled="visibleWidgets.length <= 1"
|
||||
@click="removeWidget(widget.id)"
|
||||
/>
|
||||
</UButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-widget-body">
|
||||
<component
|
||||
:is="widget.component"
|
||||
v-bind="widget.id === 'income-expense'
|
||||
? { headerTarget: `#dashboard-widget-header-actions-${widget.id}` }
|
||||
: {}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="rounded-xl border border-dashed border-gray-300 bg-white p-10 text-center dark:border-gray-700 dark:bg-gray-900">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Es sind aktuell keine Dashboard-Karten sichtbar.
|
||||
</p>
|
||||
<UButton v-if="isEditMode" class="mt-4" icon="i-heroicons-plus" @click="manageCardsOpen = true">
|
||||
Karte hinzufügen
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<UModal v-model="manageCardsOpen">
|
||||
<UCard>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<h2 class="font-semibold">Dashboard-Karten</h2>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Karten ein- oder ausblenden und bei Bedarf auf das Standardlayout zurücksetzen.
|
||||
</p>
|
||||
</div>
|
||||
<UButton color="gray" variant="ghost" icon="i-heroicons-arrow-path" @click="resetDashboard">
|
||||
Zurücksetzen
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div
|
||||
v-for="definition in DASHBOARD_WIDGETS"
|
||||
:key="definition.id"
|
||||
class="flex items-center justify-between gap-3 rounded-lg border border-gray-200 px-4 py-3 dark:border-gray-800"
|
||||
>
|
||||
<div>
|
||||
<p class="font-medium text-gray-900 dark:text-white">{{ definition.title }}</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">{{ definition.description }}</p>
|
||||
</div>
|
||||
|
||||
<UButton
|
||||
v-if="getWidgetLayout(definition.id)?.visible"
|
||||
color="gray"
|
||||
variant="soft"
|
||||
icon="i-heroicons-minus"
|
||||
:disabled="visibleWidgets.length <= 1"
|
||||
@click="removeWidget(definition.id)"
|
||||
>
|
||||
Entfernen
|
||||
</UButton>
|
||||
<UButton
|
||||
v-else
|
||||
color="primary"
|
||||
variant="soft"
|
||||
icon="i-heroicons-plus"
|
||||
@click="addWidget(definition.id)"
|
||||
>
|
||||
Hinzufügen
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</UCard>
|
||||
</UModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dashboard-grid {
|
||||
min-height: 60vh;
|
||||
}
|
||||
|
||||
.dashboard-grid-item {
|
||||
height: 100%;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.dashboard-widget-card {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid rgb(229 231 235);
|
||||
background: white;
|
||||
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||
}
|
||||
|
||||
.dashboard-widget-header {
|
||||
flex: 0 0 auto;
|
||||
border-bottom: 1px solid rgb(229 231 235);
|
||||
padding: 1rem 1rem 0.875rem;
|
||||
}
|
||||
|
||||
.dashboard-widget-header-actions {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.dashboard-widget-header-target {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.dashboard-widget-body {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
:deep(.grid-stack-item-content) {
|
||||
inset: 0;
|
||||
background: transparent;
|
||||
overflow-y: hidden !important;
|
||||
}
|
||||
|
||||
:deep(.grid-stack-item:not(.dashboard-widget-editing) .ui-resizable-handle) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:deep(.dark .dashboard-widget-card) {
|
||||
border-color: rgb(31 41 55);
|
||||
background: rgb(17 24 39);
|
||||
}
|
||||
|
||||
:deep(.dark .dashboard-widget-header) {
|
||||
border-color: rgb(31 41 55);
|
||||
}
|
||||
</style>
|
||||
@@ -1,92 +0,0 @@
|
||||
<template>
|
||||
<UDashboardNavbar title="Home">
|
||||
<template #right>
|
||||
<!-- <UTooltip text="Notifications" :shortcuts="['N']">
|
||||
<UButton color="gray" variant="ghost" square @click="isNotificationsSlideoverOpen = true">
|
||||
<UChip :show="unreadMessages" color="primary" inset>
|
||||
<UIcon name="i-heroicons-bell" class="w-5 h-5" />
|
||||
</UChip>
|
||||
</UButton>
|
||||
</UTooltip>-->
|
||||
</template>
|
||||
</UDashboardNavbar>
|
||||
|
||||
<UDashboardPanelContent>
|
||||
<div class="mb-5">
|
||||
<UDashboardCard
|
||||
title="Einnahmen und Ausgaben"
|
||||
class="mt-3"
|
||||
>
|
||||
<display-income-and-expenditure/>
|
||||
</UDashboardCard>
|
||||
</div>
|
||||
<UPageGrid>
|
||||
<UDashboardCard
|
||||
title="Buchhaltung"
|
||||
>
|
||||
<display-open-balances/>
|
||||
</UDashboardCard>
|
||||
<UDashboardCard
|
||||
title="Bank"
|
||||
>
|
||||
<display-bankaccounts/>
|
||||
</UDashboardCard>
|
||||
<UDashboardCard
|
||||
title="Projekte"
|
||||
>
|
||||
<display-projects-in-phases/>
|
||||
</UDashboardCard>
|
||||
<!--<UDashboardCard
|
||||
title="Anwesende"
|
||||
>
|
||||
<display-present-profiles/>
|
||||
</UDashboardCard>
|
||||
<UDashboardCard
|
||||
title="Projektzeiten"
|
||||
>
|
||||
<display-running-time/>
|
||||
</UDashboardCard>
|
||||
<UDashboardCard
|
||||
title="Anwesenheiten"
|
||||
>
|
||||
<display-running-working-time/>
|
||||
</UDashboardCard>-->
|
||||
<UDashboardCard
|
||||
title="Aufgaben"
|
||||
>
|
||||
<display-open-tasks/>
|
||||
</UDashboardCard>
|
||||
<!-- <UDashboardCard
|
||||
title="Label Test"
|
||||
>
|
||||
<UButton
|
||||
@click="modal.open(LabelPrintModal, {
|
||||
context: {
|
||||
datamatrix: '1234',
|
||||
text: 'FEDEO TEST'
|
||||
}
|
||||
})"
|
||||
icon="i-heroicons-printer"
|
||||
>
|
||||
Label Drucken
|
||||
</UButton>
|
||||
</UDashboardCard>-->
|
||||
</UPageGrid>
|
||||
</UDashboardPanelContent>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
import Nimbot from "~/components/nimbot.vue";
|
||||
import LabelPrintModal from "~/components/LabelPrintModal.vue";
|
||||
|
||||
|
||||
const modal = useModal();
|
||||
|
||||
const { isNotificationsSlideoverOpen } = useDashboard()
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
Reference in New Issue
Block a user