425 lines
9.7 KiB
Vue
425 lines
9.7 KiB
Vue
<template>
|
|
<div class="home-logged-in">
|
|
<header class="dashboard-header">
|
|
<h1>Willkommen zurück!</h1>
|
|
<p class="dashboard-subtitle">Schön, dass du wieder da bist.</p>
|
|
<div class="dashboard-toolbar">
|
|
<button
|
|
v-if="!editMode"
|
|
type="button"
|
|
class="btn-edit"
|
|
@click="editMode = true"
|
|
>
|
|
Dashboard bearbeiten
|
|
</button>
|
|
<template v-else>
|
|
<div class="widget-add-row">
|
|
<select
|
|
v-model="selectedWidgetTypeId"
|
|
class="widget-type-select"
|
|
@change="onSelectWidgetType"
|
|
>
|
|
<option value="">+ Widget hinzufügen …</option>
|
|
<option
|
|
v-for="wt in widgetTypeOptions"
|
|
:key="wt.id"
|
|
:value="wt.id"
|
|
>
|
|
{{ wt.label }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
<button type="button" class="btn-done" @click="doneEditing">
|
|
Fertig
|
|
</button>
|
|
</template>
|
|
<button type="button" class="logout-btn" @click="handleLogout">
|
|
Logout
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<div
|
|
v-if="loadError"
|
|
class="dashboard-message dashboard-error"
|
|
>
|
|
{{ loadError }}
|
|
</div>
|
|
<div
|
|
v-else
|
|
class="dashboard-grid"
|
|
@dragover.prevent="onGridDragover"
|
|
@drop="onGridDrop"
|
|
>
|
|
<template v-for="(w, index) in widgets" :key="w.id">
|
|
<div
|
|
class="dashboard-grid-cell"
|
|
:class="{ 'drop-target': dragOverIndex === index && draggedIndex !== index }"
|
|
@dragover.prevent="() => setDropTarget(index)"
|
|
@dragleave="clearDropTarget"
|
|
@drop="onDrop(index)"
|
|
>
|
|
<DashboardWidget
|
|
v-if="!editMode"
|
|
:widget-id="w.id"
|
|
:title="w.title"
|
|
:endpoint="w.endpoint"
|
|
:request-counter="widgetRequestCounter(index)"
|
|
@drag-start="() => (draggedIndex = index)"
|
|
@drag-end="() => (draggedIndex = null)"
|
|
/>
|
|
<div v-else class="dashboard-widget-edit">
|
|
<div class="widget-edit-fields">
|
|
<input
|
|
v-model="w.title"
|
|
type="text"
|
|
placeholder="Titel"
|
|
class="widget-edit-input"
|
|
/>
|
|
<input
|
|
v-model="w.endpoint"
|
|
type="text"
|
|
placeholder="Endpoint (z. B. /api/termine)"
|
|
class="widget-edit-input widget-edit-endpoint"
|
|
/>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
class="btn-remove"
|
|
title="Widget entfernen"
|
|
@click="removeWidget(index)"
|
|
>
|
|
Entfernen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<div v-if="widgets.length === 0 && !loading" class="dashboard-empty">
|
|
<p>Noch keine Widgets. Klicke auf „Dashboard bearbeiten“ und dann „+ Widget hinzufügen“.</p>
|
|
</div>
|
|
|
|
<div class="actions">
|
|
<!-- Platz für weitere Aktionen -->
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import { mapActions } from 'vuex';
|
|
import apiClient from '@/utils/axios.js';
|
|
import DashboardWidget from '@/components/DashboardWidget.vue';
|
|
|
|
function generateId() {
|
|
return typeof crypto !== 'undefined' && crypto.randomUUID
|
|
? crypto.randomUUID()
|
|
: `w-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
}
|
|
|
|
export default {
|
|
name: 'HomeLoggedInView',
|
|
components: { DashboardWidget },
|
|
computed: {
|
|
widgetTypeOptions() {
|
|
if (this.availableWidgets.length > 0) return this.availableWidgets;
|
|
return [{ id: 'default', label: 'Termine', endpoint: '/api/termine' }];
|
|
}
|
|
},
|
|
data() {
|
|
return {
|
|
widgets: [],
|
|
availableWidgets: [],
|
|
selectedWidgetTypeId: '',
|
|
loading: true,
|
|
loadError: null,
|
|
editMode: false,
|
|
draggedIndex: null,
|
|
dragOverIndex: null
|
|
};
|
|
},
|
|
mounted() {
|
|
this.loadConfig();
|
|
this.loadAvailableWidgets();
|
|
},
|
|
methods: {
|
|
...mapActions(['logout']),
|
|
/** Counter für EP: wievieltes Widget mit gleichem Endpoint (0, 1, 2, …), damit z. B. News nicht doppelt. */
|
|
widgetRequestCounter(index) {
|
|
const endpoint = this.widgets[index]?.endpoint;
|
|
if (endpoint == null) return undefined;
|
|
let count = 0;
|
|
for (let i = 0; i < index; i++) {
|
|
if (this.widgets[i].endpoint === endpoint) count++;
|
|
}
|
|
return count;
|
|
},
|
|
handleLogout() {
|
|
this.logout();
|
|
},
|
|
async loadAvailableWidgets() {
|
|
try {
|
|
const { data } = await apiClient.get('/api/dashboard/widgets');
|
|
this.availableWidgets = Array.isArray(data) ? data : [];
|
|
} catch (e) {
|
|
this.availableWidgets = [];
|
|
}
|
|
},
|
|
async loadConfig() {
|
|
this.loading = true;
|
|
this.loadError = null;
|
|
try {
|
|
const { data } = await apiClient.get('/api/dashboard/config');
|
|
let list = Array.isArray(data?.widgets) ? [...data.widgets] : [];
|
|
if (list.length === 0) {
|
|
list = [{ id: generateId(), title: 'Termine', endpoint: '/api/termine' }];
|
|
this.widgets = list;
|
|
await this.saveConfig();
|
|
} else {
|
|
this.widgets = list;
|
|
}
|
|
} catch (e) {
|
|
this.loadError = e.response?.data?.error || e.message || 'Dashboard konnte nicht geladen werden.';
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
async saveConfig() {
|
|
try {
|
|
await apiClient.put('/api/dashboard/config', { widgets: this.widgets });
|
|
} catch (e) {
|
|
console.error('Dashboard speichern fehlgeschlagen:', e);
|
|
}
|
|
},
|
|
onSelectWidgetType() {
|
|
const id = this.selectedWidgetTypeId;
|
|
if (!id) return;
|
|
const wt = this.widgetTypeOptions.find(w => String(w.id) === String(id));
|
|
if (wt) {
|
|
this.widgets.push({
|
|
id: generateId(),
|
|
title: wt.label,
|
|
endpoint: wt.endpoint
|
|
});
|
|
this.saveConfig();
|
|
}
|
|
this.selectedWidgetTypeId = '';
|
|
},
|
|
removeWidget(index) {
|
|
this.widgets.splice(index, 1);
|
|
this.saveConfig();
|
|
},
|
|
doneEditing() {
|
|
this.editMode = false;
|
|
this.saveConfig();
|
|
},
|
|
setDropTarget(index) {
|
|
this.dragOverIndex = index;
|
|
},
|
|
clearDropTarget() {
|
|
this.dragOverIndex = null;
|
|
},
|
|
onGridDragover() {
|
|
this.dragOverIndex = this.widgets.length;
|
|
},
|
|
onGridDrop() {
|
|
if (this.draggedIndex == null) return;
|
|
const from = this.draggedIndex;
|
|
const to = this.widgets.length;
|
|
if (from === to || from === to - 1) {
|
|
this.draggedIndex = null;
|
|
this.dragOverIndex = null;
|
|
return;
|
|
}
|
|
const item = this.widgets.splice(from, 1)[0];
|
|
this.widgets.splice(to, 0, item);
|
|
this.draggedIndex = null;
|
|
this.dragOverIndex = null;
|
|
this.saveConfig();
|
|
},
|
|
onDrop(toIndex) {
|
|
if (this.draggedIndex == null) return;
|
|
const from = this.draggedIndex;
|
|
const to = toIndex;
|
|
if (from === to) {
|
|
this.draggedIndex = null;
|
|
this.dragOverIndex = null;
|
|
return;
|
|
}
|
|
const item = this.widgets.splice(from, 1)[0];
|
|
this.widgets.splice(to, 0, item);
|
|
this.draggedIndex = null;
|
|
this.dragOverIndex = null;
|
|
this.saveConfig();
|
|
}
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
.home-logged-in {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
}
|
|
|
|
.dashboard-header {
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.dashboard-header h1 {
|
|
color: #333;
|
|
margin: 0 0 4px 0;
|
|
}
|
|
|
|
.dashboard-subtitle {
|
|
color: #666;
|
|
margin: 0 0 16px 0;
|
|
}
|
|
|
|
.dashboard-toolbar {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.btn-edit,
|
|
.btn-add,
|
|
.btn-done {
|
|
padding: 8px 14px;
|
|
border-radius: 6px;
|
|
border: 1px solid #ced4da;
|
|
background: #fff;
|
|
color: #495057;
|
|
cursor: pointer;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.btn-edit:hover,
|
|
.btn-done:hover {
|
|
background: #f1f3f5;
|
|
}
|
|
|
|
.widget-add-row {
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.widget-type-select {
|
|
padding: 8px 12px;
|
|
border-radius: 6px;
|
|
border: 1px solid #ced4da;
|
|
background: #fff;
|
|
color: #495057;
|
|
font-size: 0.9rem;
|
|
min-width: 180px;
|
|
}
|
|
|
|
.btn-done {
|
|
border-color: #198754;
|
|
color: #198754;
|
|
}
|
|
|
|
.logout-btn {
|
|
margin-left: auto;
|
|
background: #dc3545;
|
|
color: white;
|
|
border: none;
|
|
padding: 8px 16px;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.logout-btn:hover {
|
|
background: #c82333;
|
|
}
|
|
|
|
.dashboard-message {
|
|
padding: 16px;
|
|
border-radius: 8px;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.dashboard-error {
|
|
background: #f8d7da;
|
|
color: #721c24;
|
|
border: 1px solid #f5c6cb;
|
|
}
|
|
|
|
.dashboard-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
|
gap: 20px;
|
|
}
|
|
|
|
.dashboard-grid-cell {
|
|
min-height: 200px;
|
|
}
|
|
|
|
.dashboard-grid-cell.drop-target {
|
|
outline: 2px dashed #0d6efd;
|
|
outline-offset: 4px;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.dashboard-widget-edit {
|
|
min-height: 200px;
|
|
padding: 12px;
|
|
background: #f8f9fa;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 8px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
}
|
|
|
|
.widget-edit-fields {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
|
|
.widget-edit-input {
|
|
padding: 8px 10px;
|
|
border: 1px solid #ced4da;
|
|
border-radius: 4px;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.widget-edit-endpoint {
|
|
font-family: monospace;
|
|
font-size: 0.85rem;
|
|
}
|
|
|
|
.btn-remove {
|
|
align-self: flex-start;
|
|
padding: 6px 12px;
|
|
border: 1px solid #dc3545;
|
|
background: #fff;
|
|
color: #dc3545;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 0.85rem;
|
|
}
|
|
|
|
.btn-remove:hover {
|
|
background: #dc3545;
|
|
color: #fff;
|
|
}
|
|
|
|
.dashboard-empty {
|
|
padding: 32px;
|
|
text-align: center;
|
|
color: #666;
|
|
background: #f8f9fa;
|
|
border-radius: 8px;
|
|
border: 1px dashed #dee2e6;
|
|
}
|
|
|
|
.actions {
|
|
margin-top: 30px;
|
|
}
|
|
</style>
|