Add dashboard functionality: Integrate dashboardRouter and UserDashboard model, enabling user-specific dashboard configurations. Update LoggedInView to support dynamic widget management, including adding, removing, and saving widget configurations, enhancing user experience and interactivity.
This commit is contained in:
218
frontend/src/components/DashboardWidget.vue
Normal file
218
frontend/src/components/DashboardWidget.vue
Normal file
@@ -0,0 +1,218 @@
|
||||
<template>
|
||||
<div
|
||||
class="dashboard-widget"
|
||||
:class="{ 'is-dragging': isDragging }"
|
||||
:data-widget-id="widgetId"
|
||||
>
|
||||
<header class="dashboard-widget__titlebar">
|
||||
<span class="dashboard-widget__drag-handle" title="Verschieben" draggable="true" @dragstart="onDragStart" @dragend="onDragEnd">⋮⋮</span>
|
||||
<span class="dashboard-widget__title">{{ title }}</span>
|
||||
<slot name="title-actions"></slot>
|
||||
</header>
|
||||
<div class="dashboard-widget__frame">
|
||||
<div v-if="loading" class="dashboard-widget__state">Laden…</div>
|
||||
<div v-else-if="error" class="dashboard-widget__state dashboard-widget__error">{{ error }}</div>
|
||||
<div v-else class="dashboard-widget__body">
|
||||
<slot :data="data">
|
||||
<template v-if="dataList.length">
|
||||
<ul class="dashboard-widget__list">
|
||||
<li v-for="(item, i) in dataList" :key="i" class="dashboard-widget__list-item">
|
||||
<span v-if="item.datum" class="dashboard-widget__date">{{ formatDatum(item.datum) }}</span>
|
||||
<span v-if="item.titel" class="dashboard-widget__title-text">{{ item.titel }}</span>
|
||||
<span v-else-if="item.label" class="dashboard-widget__title-text">{{ item.label }}</span>
|
||||
<p v-if="item.beschreibung" class="dashboard-widget__desc">{{ item.beschreibung }}</p>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
<template v-else>{{ defaultContent }}</template>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import apiClient from '@/utils/axios.js';
|
||||
|
||||
export default {
|
||||
name: 'DashboardWidget',
|
||||
props: {
|
||||
widgetId: { type: String, required: true },
|
||||
title: { type: String, required: true },
|
||||
endpoint: { type: String, required: true },
|
||||
/** Wenn true, wird nicht automatisch geladen (z. B. bei Bearbeitung). */
|
||||
pauseFetch: { type: Boolean, default: false }
|
||||
},
|
||||
emits: ['drag-start', 'drag-end'],
|
||||
data() {
|
||||
return {
|
||||
data: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
isDragging: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
dataList() {
|
||||
if (!Array.isArray(this.data) || this.data.length === 0) return [];
|
||||
const first = this.data[0];
|
||||
if (first !== null && typeof first === 'object') return this.data;
|
||||
return [];
|
||||
},
|
||||
defaultContent() {
|
||||
if (this.data == null) return '';
|
||||
if (Array.isArray(this.data)) {
|
||||
return this.data.length === 0 ? 'Keine Einträge' : `(${this.data.length} Einträge)`;
|
||||
}
|
||||
if (typeof this.data === 'object') {
|
||||
const keys = Object.keys(this.data);
|
||||
return keys.length === 0 ? '—' : `(${keys.length} Felder)`;
|
||||
}
|
||||
return String(this.data);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
endpoint: { handler: 'fetchData', immediate: false },
|
||||
pauseFetch: { handler(now) { if (!now) this.fetchData(); } }
|
||||
},
|
||||
mounted() {
|
||||
if (!this.pauseFetch && this.endpoint) this.fetchData();
|
||||
},
|
||||
methods: {
|
||||
async fetchData() {
|
||||
if (!this.endpoint || this.pauseFetch) return;
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
try {
|
||||
const path = this.endpoint.startsWith('/') ? this.endpoint : `/${this.endpoint}`;
|
||||
const url = path.startsWith('/api') ? path : `/api${path}`;
|
||||
const res = await apiClient.get(url);
|
||||
this.data = res.data;
|
||||
} catch (e) {
|
||||
this.error = e.response?.data?.message || e.message || 'Fehler beim Laden';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
onDragStart(e) {
|
||||
this.isDragging = true;
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
e.dataTransfer.setData('text/plain', this.widgetId);
|
||||
e.dataTransfer.setData('application/x-widget-id', this.widgetId);
|
||||
this.$emit('drag-start', { widgetId: this.widgetId, event: e });
|
||||
},
|
||||
onDragEnd() {
|
||||
this.isDragging = false;
|
||||
this.$emit('drag-end');
|
||||
},
|
||||
formatDatum(dateStr) {
|
||||
if (!dateStr) return '';
|
||||
const d = new Date(dateStr);
|
||||
if (Number.isNaN(d.getTime())) return String(dateStr);
|
||||
return d.toLocaleDateString('de-DE', { weekday: 'short', day: 'numeric', month: 'short', year: 'numeric' });
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dashboard-widget {
|
||||
min-height: 200px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--dashboard-widget-bg, #fff);
|
||||
border: 1px solid var(--dashboard-widget-border, #dee2e6);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
|
||||
overflow: hidden;
|
||||
}
|
||||
.dashboard-widget.is-dragging {
|
||||
opacity: 0.7;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.dashboard-widget__titlebar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
background: var(--dashboard-widget-title-bg, #f1f3f5);
|
||||
border-bottom: 1px solid var(--dashboard-widget-border, #dee2e6);
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
color: var(--dashboard-widget-title-color, #333);
|
||||
}
|
||||
|
||||
.dashboard-widget__drag-handle {
|
||||
cursor: grab;
|
||||
color: #868e96;
|
||||
user-select: none;
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
}
|
||||
.dashboard-widget__drag-handle:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.dashboard-widget__title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dashboard-widget__frame {
|
||||
flex: 1;
|
||||
min-height: 160px;
|
||||
padding: 12px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.dashboard-widget__state {
|
||||
color: #666;
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
}
|
||||
.dashboard-widget__error {
|
||||
color: #c92a2a;
|
||||
}
|
||||
|
||||
.dashboard-widget__body {
|
||||
font-size: 0.9rem;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.dashboard-widget__list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.dashboard-widget__list-item {
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
.dashboard-widget__list-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.dashboard-widget__date {
|
||||
display: block;
|
||||
font-size: 0.8rem;
|
||||
color: #666;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.dashboard-widget__title-text {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.dashboard-widget__desc {
|
||||
margin: 4px 0 0 0;
|
||||
font-size: 0.85rem;
|
||||
color: #555;
|
||||
line-height: 1.4;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user