feat(dashboard): enhance widget availability and initialization
All checks were successful
Deploy to production / deploy (push) Successful in 3m2s

- Updated `getAvailableWidgets` method in `DashboardService` to merge default widget types with database entries, ensuring immediate visibility of new widgets post-deployment.
- Introduced `DASHBOARD_WIDGET_TYPE_DEFAULTS` in `initializeWidgetTypes` for canonical widget types, facilitating API merging when database entries are absent.
- Modified `StatusBar.vue` to utilize a computed property for quick access children, improving menu item handling and visibility based on user context.
- Enhanced `LoggedInView.vue` to dynamically return localized widget labels, improving user experience with accurate translations.
This commit is contained in:
Torsten Schulz (local)
2026-04-02 15:19:08 +02:00
parent 5fcd55be43
commit 49713d957d
5 changed files with 95 additions and 20 deletions

View File

@@ -1,24 +1,62 @@
import BaseService from './BaseService.js';
import UserDashboard from '../models/community/user_dashboard.js';
import WidgetType from '../models/type/widget_type.js';
import { DASHBOARD_WIDGET_TYPE_DEFAULTS } from '../utils/initializeWidgetTypes.js';
class DashboardService extends BaseService {
/**
* Liste aller möglichen (verfügbaren) Widget-Typen.
* @returns {Promise<Array<{ id: number, label: string, endpoint: string, description: string|null, orderId: number }>>}
* Merge: Code-Defaults (immer) + DB-Zeilen; fehlende Defaults erscheinen mit synthetischer id,
* damit neue Widgets nach Deploy sofort unter „Widget hinzufügen“ sichtbar sind.
* @returns {Promise<Array<{ id: number|string, label: string, endpoint: string, description: string|null, orderId: number }>>}
*/
async getAvailableWidgets() {
const rows = await WidgetType.findAll({
order: [['orderId', 'ASC'], ['id', 'ASC']],
attributes: ['id', 'label', 'endpoint', 'description', 'orderId']
});
return rows.map(r => ({
id: r.id,
label: r.label,
endpoint: r.endpoint,
description: r.description ?? null,
orderId: r.orderId
}));
const dbByEndpoint = new Map(rows.map((r) => [r.endpoint, r]));
const defaultEndpoints = new Set(DASHBOARD_WIDGET_TYPE_DEFAULTS.map((d) => d.endpoint));
const sortedDefaults = [...DASHBOARD_WIDGET_TYPE_DEFAULTS].sort(
(a, b) => (a.orderId || 0) - (b.orderId || 0)
);
const ordered = [];
for (const def of sortedDefaults) {
const existing = dbByEndpoint.get(def.endpoint);
if (existing) {
ordered.push({
id: existing.id,
label: existing.label,
endpoint: existing.endpoint,
description: existing.description ?? null,
orderId: existing.orderId
});
} else {
ordered.push({
id: `ep:${def.endpoint}`,
label: def.label,
endpoint: def.endpoint,
description: def.description ?? null,
orderId: def.orderId ?? 0
});
}
}
for (const r of rows) {
if (!defaultEndpoints.has(r.endpoint)) {
ordered.push({
id: r.id,
label: r.label,
endpoint: r.endpoint,
description: r.description ?? null,
orderId: r.orderId
});
}
}
return ordered;
}
/**

View File

@@ -0,0 +1,22 @@
-- Dashboard-Widget-Typen (type.widget_type)
-- Für Produktion ohne initializeWidgetTypes / ohne Deploy-Sync.
-- Idempotent: pro endpoint nur einfügen, wenn noch keine Zeile existiert.
--
-- Muss mit backend/utils/initializeWidgetTypes.js (DASHBOARD_WIDGET_TYPE_DEFAULTS)
-- übereinstimmen bei neuen Widgets beides anpassen.
INSERT INTO type.widget_type (label, endpoint, description, order_id)
SELECT v.label, v.endpoint, v.description, v.order_id
FROM (
VALUES
('Termine'::text, '/api/termine'::text, 'Bevorstehende Termine'::text, 1),
('Falukant', '/api/falukant/dashboard-widget', 'Charakter, Geld, Nachrichten, Kinder', 2),
('News', '/api/news?language=de&category=top', 'Nachrichten (newsdata.io), Counter für Pagination', 3),
('Geburtstage', '/api/calendar/widget/birthdays', 'Nächste Geburtstage von Freunden', 4),
('Nächste Termine', '/api/calendar/widget/upcoming', 'Anstehende Kalendertermine', 5),
('Kalender', '/api/calendar/widget/mini', 'Mini-Kalenderansicht', 6),
('Sprachkurse', '/api/vocab/dashboard-widget', 'Vokabelkurse, aktuelle Lektion, Sprung zur Lektion', 7)
) AS v(label, endpoint, description, order_id)
WHERE NOT EXISTS (
SELECT 1 FROM type.widget_type wt WHERE wt.endpoint = v.endpoint
);

View File

@@ -4,7 +4,8 @@ import WidgetType from '../models/type/widget_type.js';
* Default rows for type.widget_type. Labels/descriptions are German for DB/admin readability;
* the web app maps known `endpoint` values to localized strings (see LoggedInView / home.dashboard.widgetLabels).
*/
const DEFAULT_WIDGET_TYPES = [
/** Kanonische Widget-Typen (auch für API-Merge, falls DB noch keine Zeile hat) */
export const DASHBOARD_WIDGET_TYPE_DEFAULTS = [
{ label: 'Termine', endpoint: '/api/termine', description: 'Bevorstehende Termine', orderId: 1 },
{ label: 'Falukant', endpoint: '/api/falukant/dashboard-widget', description: 'Charakter, Geld, Nachrichten, Kinder', orderId: 2 },
{ label: 'News', endpoint: '/api/news?language=de&category=top', description: 'Nachrichten (newsdata.io), Counter für Pagination', orderId: 3 },
@@ -17,9 +18,12 @@ const DEFAULT_WIDGET_TYPES = [
/**
* Stellt die Standard-Widget-Typen in type.widget_type bereit.
* Idempotent: vorhandene Einträge werden nicht verändert.
*
* Produktion ohne Sync/Init: manuell backend/sql/seed_dashboard_widget_types.sql ausführen
* (muss dieselben Endpoints wie DASHBOARD_WIDGET_TYPE_DEFAULTS enthalten).
*/
const initializeWidgetTypes = async () => {
for (const row of DEFAULT_WIDGET_TYPES) {
for (const row of DASHBOARD_WIDGET_TYPE_DEFAULTS) {
await WidgetType.findOrCreate({
where: { endpoint: row.endpoint },
defaults: {