feat: Introduce admin dashboard product standard management
- Added new API endpoints for managing the product dashboard standard, including retrieval, update, and deletion functionalities. - Enhanced the DashboardConfigurePage to support admin mode for configuring the product dashboard standard. - Updated the admin navigation to include a link for the product dashboard standard configuration. - Refactored the dashboard layout logic to utilize the new product standard management features. - Bumped app_dashboard version to 1.10.0 to reflect these enhancements and changes.
This commit is contained in:
parent
ff95ef63c7
commit
365ce49c6a
|
|
@ -39,7 +39,7 @@ def lab_default_layout_dict() -> dict[str, Any]:
|
|||
|
||||
|
||||
def product_default_layout_dict() -> dict[str, Any]:
|
||||
"""System-Standard für die Produkt-Übersicht (kein DB-Override)."""
|
||||
"""Code-Fallback für die Produkt-Übersicht; live-Standard ggf. system_config (siehe get_product_default_base_dict)."""
|
||||
on = DEFAULT_PRODUCT_DASHBOARD_WIDGET_IDS
|
||||
return {
|
||||
"version": 1,
|
||||
|
|
|
|||
|
|
@ -58,6 +58,14 @@ def widgets_catalog_payload(profile_id: str, conn) -> dict[str, Any]:
|
|||
}
|
||||
|
||||
|
||||
def widgets_catalog_admin_payload() -> dict[str, Any]:
|
||||
"""Admin: alle Widgets als auswählbar (ohne Feature-Filter)."""
|
||||
return {
|
||||
"catalog_version": 1,
|
||||
"widgets": [_public_row(e, allowed=True) for e in WIDGET_CATALOG],
|
||||
}
|
||||
|
||||
|
||||
def apply_entitlements_to_layout_dict(layout: dict[str, Any], profile_id: str, conn) -> dict[str, Any]:
|
||||
"""
|
||||
Setzt enabled=False für Widgets ohne Berechtigung. Mindestens ein Widget bleibt aktiv (welcome).
|
||||
|
|
|
|||
8
backend/migrations/040_system_config.sql
Normal file
8
backend/migrations/040_system_config.sql
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
-- Globale System-Konfiguration (Key/Value, JSONB)
|
||||
CREATE TABLE IF NOT EXISTS system_config (
|
||||
key VARCHAR(64) PRIMARY KEY,
|
||||
value JSONB NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_system_config_updated_at ON system_config (updated_at);
|
||||
|
|
@ -7,12 +7,21 @@ import os
|
|||
import smtplib
|
||||
from email.mime.text import MIMEText
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Depends
|
||||
|
||||
from db import get_db, get_cursor, r2d
|
||||
from auth import require_admin, hash_pin
|
||||
from models import AdminProfileUpdate
|
||||
from dashboard_layout_schema import DashboardLayoutPayload, product_default_layout_dict
|
||||
from dashboard_widget_entitlements import widgets_catalog_admin_payload
|
||||
from system_dashboard_product_default import (
|
||||
delete_product_default_override,
|
||||
get_product_default_base_dict,
|
||||
get_stored_product_default_validated,
|
||||
upsert_product_default_base,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/api/admin", tags=["admin"])
|
||||
|
||||
|
|
@ -155,3 +164,52 @@ def admin_test_email(data: dict, session: dict=Depends(require_admin)):
|
|||
return {"ok": True, "message": f"Test-E-Mail an {email} gesendet"}
|
||||
except Exception as e:
|
||||
raise HTTPException(500, f"Fehler beim Senden: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/widgets/catalog-full")
|
||||
def admin_widgets_catalog_full(session: dict = Depends(require_admin)):
|
||||
"""Dashboard-Widget-Katalog ohne Feature-Filter (Konfiguration des Produkt-Standards)."""
|
||||
_ = session
|
||||
return widgets_catalog_admin_payload()
|
||||
|
||||
|
||||
@router.get("/dashboard-product-default")
|
||||
def admin_get_dashboard_product_default(session: dict = Depends(require_admin)):
|
||||
"""Aktueller Produkt-Dashboard-Standard (DB oder Code)."""
|
||||
_ = session
|
||||
with get_db() as conn:
|
||||
layout = get_product_default_base_dict(conn)
|
||||
from_database = get_stored_product_default_validated(conn) is not None
|
||||
code_ref = product_default_layout_dict()
|
||||
return {
|
||||
"from_database": from_database,
|
||||
"layout": layout,
|
||||
"code_reference": code_ref,
|
||||
}
|
||||
|
||||
|
||||
@router.put("/dashboard-product-default")
|
||||
def admin_put_dashboard_product_default(
|
||||
body: dict[str, Any],
|
||||
session: dict = Depends(require_admin),
|
||||
):
|
||||
"""System-Standard persistieren (JSON wie Nutzer-Layout v1)."""
|
||||
_ = session
|
||||
try:
|
||||
payload = DashboardLayoutPayload.model_validate(body)
|
||||
except Exception as e:
|
||||
raise HTTPException(422, str(e)) from e
|
||||
stored = payload.to_stored_dict()
|
||||
with get_db() as conn:
|
||||
upsert_product_default_base(conn, stored)
|
||||
return {"ok": True, "layout": stored, "from_database": True}
|
||||
|
||||
|
||||
@router.delete("/dashboard-product-default")
|
||||
def admin_delete_dashboard_product_default(session: dict = Depends(require_admin)):
|
||||
"""DB-Override entfernen; App fällt auf Code-Standard zurück."""
|
||||
_ = session
|
||||
with get_db() as conn:
|
||||
delete_product_default_override(conn)
|
||||
layout = get_product_default_base_dict(conn)
|
||||
return {"ok": True, "layout": layout, "from_database": False}
|
||||
|
|
|
|||
|
|
@ -13,11 +13,11 @@ from dashboard_layout_schema import (
|
|||
DashboardLayoutPayload,
|
||||
coalesce_effective_layout,
|
||||
lab_default_layout_dict,
|
||||
product_default_layout_dict,
|
||||
)
|
||||
from dashboard_widget_entitlements import apply_entitlements_to_layout_dict, widgets_catalog_payload
|
||||
from db import get_cursor, get_db
|
||||
from routers.profiles import get_pid
|
||||
from system_dashboard_product_default import get_product_default_base_dict
|
||||
|
||||
router = APIRouter(prefix="/api/app", tags=["app-dashboard-lab"])
|
||||
|
||||
|
|
@ -51,8 +51,11 @@ def get_dashboard_layout(
|
|||
raw = row["dashboard_layout"] if row else None
|
||||
custom, effective = coalesce_effective_layout(raw)
|
||||
with get_db() as conn:
|
||||
base_product = get_product_default_base_dict(conn)
|
||||
if not custom:
|
||||
effective = base_product
|
||||
effective = apply_entitlements_to_layout_dict(effective, pid, conn)
|
||||
product_adj = apply_entitlements_to_layout_dict(product_default_layout_dict(), pid, conn)
|
||||
product_adj = apply_entitlements_to_layout_dict(base_product, pid, conn)
|
||||
lab_adj = apply_entitlements_to_layout_dict(lab_default_layout_dict(), pid, conn)
|
||||
return {
|
||||
"custom": custom,
|
||||
|
|
@ -107,5 +110,6 @@ def reset_dashboard_layout(
|
|||
)
|
||||
if cur.rowcount == 0:
|
||||
raise HTTPException(404, "Profil nicht gefunden")
|
||||
cleared = apply_entitlements_to_layout_dict(product_default_layout_dict(), pid, conn)
|
||||
base = get_product_default_base_dict(conn)
|
||||
cleared = apply_entitlements_to_layout_dict(base, pid, conn)
|
||||
return {"ok": True, "layout": cleared}
|
||||
|
|
|
|||
77
backend/system_dashboard_product_default.py
Normal file
77
backend/system_dashboard_product_default.py
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
"""
|
||||
Persistenter System-Standard für die Produkt-Übersicht (Dashboard).
|
||||
|
||||
Key in system_config: dashboard_product_default — gültiges DashboardLayoutPayload (JSON).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from psycopg2.extras import Json
|
||||
|
||||
from dashboard_layout_schema import DashboardLayoutPayload, product_default_layout_dict
|
||||
from db import get_cursor
|
||||
|
||||
SYSTEM_CONFIG_KEY_DASHBOARD_PRODUCT_DEFAULT = "dashboard_product_default"
|
||||
|
||||
|
||||
def get_stored_product_default_validated(conn) -> dict[str, Any] | None:
|
||||
"""Gültiges Layout aus DB oder None (fehlt/ungültig)."""
|
||||
cur = get_cursor(conn)
|
||||
cur.execute(
|
||||
"SELECT value FROM system_config WHERE key = %s",
|
||||
(SYSTEM_CONFIG_KEY_DASHBOARD_PRODUCT_DEFAULT,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if not row or row.get("value") is None:
|
||||
return None
|
||||
raw = row["value"]
|
||||
if isinstance(raw, str):
|
||||
import json
|
||||
|
||||
try:
|
||||
raw = json.loads(raw)
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
if not isinstance(raw, dict):
|
||||
return None
|
||||
try:
|
||||
payload = DashboardLayoutPayload.model_validate(
|
||||
{"version": raw.get("version", 1), "widgets": raw.get("widgets", [])}
|
||||
)
|
||||
return payload.to_stored_dict()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def get_product_default_base_dict(conn) -> dict[str, Any]:
|
||||
"""Basis-Layout (ohne Entitlements): DB-Override oder Code-Standard."""
|
||||
stored = get_stored_product_default_validated(conn)
|
||||
if stored is not None:
|
||||
return stored
|
||||
return product_default_layout_dict()
|
||||
|
||||
|
||||
def upsert_product_default_base(conn, layout: dict[str, Any]) -> dict[str, Any]:
|
||||
payload = DashboardLayoutPayload.model_validate(layout)
|
||||
stored = payload.to_stored_dict()
|
||||
cur = get_cursor(conn)
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO system_config (key, value, updated_at)
|
||||
VALUES (%s, %s, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT (key) DO UPDATE SET
|
||||
value = EXCLUDED.value,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
""",
|
||||
(SYSTEM_CONFIG_KEY_DASHBOARD_PRODUCT_DEFAULT, Json(stored)),
|
||||
)
|
||||
return stored
|
||||
|
||||
|
||||
def delete_product_default_override(conn) -> None:
|
||||
cur = get_cursor(conn)
|
||||
cur.execute(
|
||||
"DELETE FROM system_config WHERE key = %s",
|
||||
(SYSTEM_CONFIG_KEY_DASHBOARD_PRODUCT_DEFAULT,),
|
||||
)
|
||||
45
backend/tests/test_system_dashboard_product_default.py
Normal file
45
backend/tests/test_system_dashboard_product_default.py
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
from dashboard_layout_schema import DashboardLayoutPayload, product_default_layout_dict
|
||||
from dashboard_widget_entitlements import widgets_catalog_admin_payload
|
||||
|
||||
|
||||
def test_widgets_catalog_admin_all_allowed():
|
||||
p = widgets_catalog_admin_payload()
|
||||
assert p["catalog_version"] == 1
|
||||
assert len(p["widgets"]) >= 1
|
||||
assert all(w["allowed"] is True for w in p["widgets"])
|
||||
|
||||
|
||||
def test_get_product_default_base_uses_code_when_no_row(monkeypatch):
|
||||
from system_dashboard_product_default import get_product_default_base_dict
|
||||
|
||||
class _Cur:
|
||||
def execute(self, *a, **k):
|
||||
pass
|
||||
|
||||
def fetchone(self):
|
||||
return None
|
||||
|
||||
monkeypatch.setattr("system_dashboard_product_default.get_cursor", lambda _c: _Cur())
|
||||
assert get_product_default_base_dict(object()) == product_default_layout_dict()
|
||||
|
||||
|
||||
def test_get_product_default_base_uses_db_when_valid(monkeypatch):
|
||||
from system_dashboard_product_default import get_product_default_base_dict
|
||||
|
||||
from widget_catalog import ALLOWED_WIDGET_IDS
|
||||
|
||||
small = {
|
||||
"version": 1,
|
||||
"widgets": [{"id": wid, "enabled": wid == "welcome"} for wid in sorted(ALLOWED_WIDGET_IDS)],
|
||||
}
|
||||
DashboardLayoutPayload.model_validate(small)
|
||||
|
||||
class _Cur:
|
||||
def execute(self, *a, **k):
|
||||
pass
|
||||
|
||||
def fetchone(self):
|
||||
return {"value": small}
|
||||
|
||||
monkeypatch.setattr("system_dashboard_product_default.get_cursor", lambda _c: _Cur())
|
||||
assert get_product_default_base_dict(object()) == small
|
||||
|
|
@ -9,7 +9,7 @@ Semantic Versioning: MAJOR.MINOR.PATCH
|
|||
|
||||
APP_VERSION = "0.9n"
|
||||
BUILD_DATE = "2026-04-05"
|
||||
DB_SCHEMA_VERSION = "20260406c" # Migration 039
|
||||
DB_SCHEMA_VERSION = "20260406d" # Migration 040
|
||||
|
||||
MODULE_VERSIONS = {
|
||||
"auth": "1.2.0",
|
||||
|
|
@ -24,20 +24,21 @@ MODULE_VERSIONS = {
|
|||
"photos": "1.0.0",
|
||||
"insights": "1.3.0",
|
||||
"prompts": "1.1.0",
|
||||
"admin": "1.2.0",
|
||||
"admin": "1.3.0", # Dashboard Produkt-Standard (system_config) + catalog-full
|
||||
"stats": "1.0.1",
|
||||
"exportdata": "1.1.0",
|
||||
"importdata": "1.0.0",
|
||||
"membership": "2.1.0",
|
||||
"workflow": "0.6.0", # Phase 4: End Node Template Engine
|
||||
"app_dashboard": "1.9.0", # product vs lab layout defaults; user dashboard configure page API fields
|
||||
"app_dashboard": "1.10.0", # Produkt-Standard aus system_config; Response-Form unverändert
|
||||
}
|
||||
|
||||
CHANGELOG = [
|
||||
{
|
||||
"version": "0.9n",
|
||||
"date": "2026-04-05",
|
||||
"date": "2026-04-06",
|
||||
"changes": [
|
||||
"Admin: Produkt-Dashboard-Systemstandard (Migration 040 system_config, API, UI)",
|
||||
"Phase 4: End Node Template Engine",
|
||||
"workflow_models.py: EndNodeOutputMode enum (AUTO, TEMPLATE)",
|
||||
"workflow_executor.py: execute_end_node() with Jinja2 template rendering",
|
||||
|
|
|
|||
|
|
@ -241,6 +241,7 @@ function AppShell() {
|
|||
<Route path="g/:groupId" element={<AdminGroupHubPage />} />
|
||||
<Route path="users" element={<AdminUsersPage />} />
|
||||
<Route path="system" element={<AdminSystemPage />} />
|
||||
<Route path="dashboard-product-default" element={<DashboardConfigurePage adminMode />} />
|
||||
<Route path="tier-limits" element={<AdminTierLimitsPage/>}/>
|
||||
<Route path="features" element={<AdminFeaturesPage/>}/>
|
||||
<Route path="tiers" element={<AdminTiersPage/>}/>
|
||||
|
|
|
|||
|
|
@ -115,6 +115,11 @@ export const ADMIN_GROUPS = [
|
|||
label: 'SMTP & Metadaten-Export',
|
||||
description: 'SMTP-Status, Test-Mail und Placeholder-Katalog (JSON/ZIP).',
|
||||
},
|
||||
{
|
||||
to: '/admin/dashboard-product-default',
|
||||
label: 'Produkt-Dashboard (Standard)',
|
||||
description: 'Globales Standard-Layout der Startseite (DB oder Code-Fallback).',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
|
|
|||
|
|
@ -32,10 +32,14 @@ function catalogMetaById(catalog) {
|
|||
return Object.fromEntries(catalog.widgets.map((w) => [w.id, w]))
|
||||
}
|
||||
|
||||
export default function DashboardConfigurePage() {
|
||||
/**
|
||||
* @param {{ adminMode?: boolean }} [props]
|
||||
*/
|
||||
export default function DashboardConfigurePage({ adminMode = false } = {}) {
|
||||
ensurePilotLabWidgetsRegistered()
|
||||
|
||||
const [bundle, setBundle] = useState(null)
|
||||
const [adminFromDatabase, setAdminFromDatabase] = useState(null)
|
||||
const [catalog, setCatalog] = useState(null)
|
||||
const [layout, setLayout] = useState(null)
|
||||
const [addPanelOpen, setAddPanelOpen] = useState(false)
|
||||
|
|
@ -103,16 +107,29 @@ export default function DashboardConfigurePage() {
|
|||
const load = useCallback(async () => {
|
||||
setErr(null)
|
||||
try {
|
||||
if (adminMode) {
|
||||
const [cat, d] = await Promise.all([
|
||||
api.adminGetWidgetsCatalogFull(),
|
||||
api.adminGetDashboardProductDefault(),
|
||||
])
|
||||
setCatalog(cat)
|
||||
setBundle({ custom: false, product_default_layout: d.layout })
|
||||
setAdminFromDatabase(!!d.from_database)
|
||||
setChartDaysDraftByWidgetId({})
|
||||
setLayout(normalizeLayoutForEditor(structuredClone(d.layout)))
|
||||
} else {
|
||||
const [cat, b] = await Promise.all([api.getAppWidgetsCatalog(), api.getAppDashboardLayout()])
|
||||
setCatalog(cat)
|
||||
setBundle(b)
|
||||
setAdminFromDatabase(null)
|
||||
setChartDaysDraftByWidgetId({})
|
||||
const base = b.custom ? b.layout : structuredClone(b.product_default_layout)
|
||||
setLayout(normalizeLayoutForEditor(base))
|
||||
}
|
||||
} catch (e) {
|
||||
setErr(formatFastApiDetail(null, e.message))
|
||||
}
|
||||
}, [])
|
||||
}, [adminMode])
|
||||
|
||||
useEffect(() => {
|
||||
load()
|
||||
|
|
@ -138,8 +155,13 @@ export default function DashboardConfigurePage() {
|
|||
setMsg(null)
|
||||
setErr(null)
|
||||
try {
|
||||
if (adminMode) {
|
||||
await api.adminPutDashboardProductDefault(toSave)
|
||||
setMsg('System-Standard für die Übersicht wurde gespeichert.')
|
||||
} else {
|
||||
await api.putAppDashboardLayout(toSave)
|
||||
setMsg('Dein Dashboard wurde gespeichert.')
|
||||
}
|
||||
await load()
|
||||
} catch (e) {
|
||||
setErr(formatFastApiDetail(null, e.message))
|
||||
|
|
@ -149,15 +171,27 @@ export default function DashboardConfigurePage() {
|
|||
}
|
||||
|
||||
const resetToSystem = async () => {
|
||||
if (!window.confirm('Dein individuelles Layout löschen und System-Standard wiederherstellen?')) return
|
||||
const ok = adminMode
|
||||
? window.confirm(
|
||||
'Eintrag in der Datenbank löschen und Layout aus dem Code (widget_catalog) wiederherstellen?'
|
||||
)
|
||||
: window.confirm('Dein individuelles Layout löschen und System-Standard wiederherstellen?')
|
||||
if (!ok) return
|
||||
setBusy(true)
|
||||
setMsg(null)
|
||||
setErr(null)
|
||||
try {
|
||||
if (adminMode) {
|
||||
const r = await api.adminDeleteDashboardProductDefault()
|
||||
setChartDaysDraftByWidgetId({})
|
||||
setLayout(normalizeLayoutForEditor(r.layout))
|
||||
setMsg('Code-Standard wiederhergestellt (kein DB-Override mehr).')
|
||||
} else {
|
||||
const r = await api.resetAppDashboardLayout()
|
||||
setChartDaysDraftByWidgetId({})
|
||||
setLayout(normalizeLayoutForEditor(r.layout))
|
||||
setMsg('Auf System-Standard zurückgesetzt.')
|
||||
}
|
||||
await load()
|
||||
} catch (e) {
|
||||
setErr(formatFastApiDetail(null, e.message))
|
||||
|
|
@ -253,21 +287,36 @@ export default function DashboardConfigurePage() {
|
|||
<div style={{ paddingBottom: 96, textAlign: 'left', maxWidth: 720, margin: '0 auto' }}>
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
<Link
|
||||
to="/settings"
|
||||
to={adminMode ? '/admin/g/system' : '/settings'}
|
||||
className="btn btn-secondary"
|
||||
style={{ display: 'inline-flex', marginBottom: 12, textDecoration: 'none' }}
|
||||
>
|
||||
← Einstellungen
|
||||
{adminMode ? '← Basiseinstellungen (Admin)' : '← Einstellungen'}
|
||||
</Link>
|
||||
<h1 className="page-title" style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<LayoutDashboard size={26} color="var(--accent)" />
|
||||
Übersicht anpassen
|
||||
{adminMode ? 'Produkt-Übersicht: Systemstandard' : 'Übersicht anpassen'}
|
||||
</h1>
|
||||
<p style={{ fontSize: 13, color: 'var(--text2)', lineHeight: 1.6, marginTop: 8 }}>
|
||||
Kacheln für die Startseite sortieren und entfernen. Neue Kacheln über „Kachel hinzufügen“ – mit Suche direkt
|
||||
im eigenen Fenster, ohne langes Scrollen.
|
||||
{adminMode
|
||||
? 'Globales Standard-Dashboard für alle Nutzer ohne eigenes Layout. Gespeichert in der Datenbank; mit „Code-Standard wiederherstellen“ wird der Eintrag entfernt und der Fallback aus dem Code genutzt.'
|
||||
: 'Kacheln für die Startseite sortieren und entfernen. Neue Kacheln über „Kachel hinzufügen“ – mit Suche direkt im eigenen Fenster, ohne langes Scrollen.'}
|
||||
</p>
|
||||
{!bundle?.custom && (
|
||||
{adminMode && adminFromDatabase != null && (
|
||||
<p style={{ fontSize: 12, color: 'var(--accent)', marginTop: 10, lineHeight: 1.5 }}>
|
||||
{adminFromDatabase ? (
|
||||
<>
|
||||
Aktuell gilt ein <strong>gespeicherter Systemstandard</strong> (Datenbank).
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Es liegt <strong>kein DB-Override</strong> vor — es wird der Code-Standard aus dem Widget-Katalog
|
||||
verwendet.
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
{!adminMode && !bundle?.custom && (
|
||||
<p style={{ fontSize: 12, color: 'var(--accent)', marginTop: 10, lineHeight: 1.5 }}>
|
||||
Du bearbeitest gerade das <strong>System-Standardlayout</strong>. Mit „Speichern“ legst du deine persönliche
|
||||
Version ab.
|
||||
|
|
@ -457,10 +506,14 @@ export default function DashboardConfigurePage() {
|
|||
Speichern
|
||||
</button>
|
||||
<button type="button" className="btn btn-secondary" disabled={busy} onClick={resetToSystem}>
|
||||
System-Standard wiederherstellen
|
||||
{adminMode ? 'Code-Standard wiederherstellen' : 'System-Standard wiederherstellen'}
|
||||
</button>
|
||||
<Link to="/" className="btn btn-secondary" style={{ textDecoration: 'none', textAlign: 'center' }}>
|
||||
Zur Übersicht
|
||||
<Link
|
||||
to={adminMode ? '/admin' : '/'}
|
||||
className="btn btn-secondary"
|
||||
style={{ textDecoration: 'none', textAlign: 'center' }}
|
||||
>
|
||||
{adminMode ? 'Admin-Übersicht' : 'Zur Übersicht'}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -76,6 +76,13 @@ export const api = {
|
|||
putAppDashboardLayout: (layout) => req('/app/dashboard-layout', jput(layout)),
|
||||
resetAppDashboardLayout: () => req('/app/dashboard-layout/reset', { method: 'POST' }),
|
||||
|
||||
adminGetWidgetsCatalogFull: () => req('/admin/widgets/catalog-full'),
|
||||
adminGetDashboardProductDefault: () => req('/admin/dashboard-product-default'),
|
||||
adminPutDashboardProductDefault: (layout) =>
|
||||
req('/admin/dashboard-product-default', jput(layout)),
|
||||
adminDeleteDashboardProductDefault: () =>
|
||||
req('/admin/dashboard-product-default', { method: 'DELETE' }),
|
||||
|
||||
// Persönliche Referenzwerte (Profil, historisch)
|
||||
listReferenceValueTypes: () => req('/reference-value-types'),
|
||||
listReferenceValueMetaEnums: () => req('/reference-value-meta/enums'),
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user