feat: Refactor PilotVizPage and widget registry for improved layout and rendering
- Replaced the previous widget retrieval method with a new layout-based approach in PilotVizPage. - Introduced a PilotVizAdminCard for layout configuration and management. - Updated widget definitions in the registry to include new components and default order. - Enhanced user feedback for widget visibility with a message when no widgets are active.
This commit is contained in:
parent
932bceb1e1
commit
8c8f595385
76
frontend/src/components/pilot/GoalsSnapshotWidget.jsx
Normal file
76
frontend/src/components/pilot/GoalsSnapshotWidget.jsx
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import { Target } from 'lucide-react'
|
||||||
|
import { api } from '../../utils/api'
|
||||||
|
|
||||||
|
export default function GoalsSnapshotWidget() {
|
||||||
|
const [count, setCount] = useState(null)
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [err, setErr] = useState(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false
|
||||||
|
;(async () => {
|
||||||
|
try {
|
||||||
|
const list = await api.listGoals()
|
||||||
|
if (!cancelled) {
|
||||||
|
setCount(Array.isArray(list) ? list.length : 0)
|
||||||
|
setErr(null)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (!cancelled) {
|
||||||
|
setErr(e.message || 'Laden fehlgeschlagen')
|
||||||
|
setCount(null)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (!cancelled) setLoading(false)
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
return () => {
|
||||||
|
cancelled = true
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div style={{ padding: 16, textAlign: 'center' }}>
|
||||||
|
<div className="spinner" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (err) {
|
||||||
|
return <div style={{ fontSize: 13, color: 'var(--danger)' }}>{err}</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 14, flexWrap: 'wrap' }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 44,
|
||||||
|
height: 44,
|
||||||
|
borderRadius: 12,
|
||||||
|
background: 'var(--accent-light)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Target size={22} color="var(--accent)" />
|
||||||
|
</div>
|
||||||
|
<div style={{ flex: '1 1 200px' }}>
|
||||||
|
<div style={{ fontSize: 15, fontWeight: 600, color: 'var(--text1)' }}>
|
||||||
|
{count === 0
|
||||||
|
? 'Noch keine strategischen Ziele'
|
||||||
|
: `${count} ${count === 1 ? 'Ziel' : 'Ziele'} aktiv`}
|
||||||
|
</div>
|
||||||
|
<p style={{ fontSize: 13, color: 'var(--text2)', margin: '8px 0 0', lineHeight: 1.5 }}>
|
||||||
|
Focus Areas und Fortschritt – wie auf dem Haupt-Dashboard, hier als Pilot-Widget.
|
||||||
|
</p>
|
||||||
|
<Link to="/goals" className="btn btn-secondary" style={{ marginTop: 12, display: 'inline-block', fontSize: 12, padding: '6px 12px', textDecoration: 'none' }}>
|
||||||
|
Zu den Zielen →
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
128
frontend/src/components/pilot/PilotVizAdminCard.jsx
Normal file
128
frontend/src/components/pilot/PilotVizAdminCard.jsx
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
import { Settings2, RotateCcw, ChevronUp, ChevronDown } from 'lucide-react'
|
||||||
|
import { PILOT_WIDGET_DEFS, PILOT_WIDGET_IDS_DEFAULT_ORDER } from '../../pilot/widgetRegistry'
|
||||||
|
import { resetPilotLayout, savePilotLayout } from '../../pilot/pilotLayoutStorage'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pilot: lokale Konfiguration (Ein/Aus, Reihenfolge). Kein Admin-Recht nötig.
|
||||||
|
*/
|
||||||
|
export default function PilotVizAdminCard({ layout, onLayoutChange }) {
|
||||||
|
const persist = (next) => {
|
||||||
|
savePilotLayout(next)
|
||||||
|
onLayoutChange(next)
|
||||||
|
}
|
||||||
|
|
||||||
|
const move = (index, dir) => {
|
||||||
|
const nextOrder = [...layout.order]
|
||||||
|
const j = index + dir
|
||||||
|
if (j < 0 || j >= nextOrder.length) return
|
||||||
|
;[nextOrder[index], nextOrder[j]] = [nextOrder[j], nextOrder[index]]
|
||||||
|
persist({ ...layout, order: nextOrder })
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggle = (id) => {
|
||||||
|
persist({
|
||||||
|
...layout,
|
||||||
|
enabled: { ...layout.enabled, [id]: !layout.enabled[id] },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
onLayoutChange(resetPilotLayout())
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className="card section-gap"
|
||||||
|
style={{
|
||||||
|
borderStyle: 'solid',
|
||||||
|
borderColor: 'var(--accent)',
|
||||||
|
borderWidth: 1,
|
||||||
|
background: 'var(--surface)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="card-title" style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
|
<Settings2 size={18} color="var(--accent)" />
|
||||||
|
Widget-Konfiguration (Pilot)
|
||||||
|
</div>
|
||||||
|
<p style={{ fontSize: 12, color: 'var(--text2)', marginTop: 4, marginBottom: 14, lineHeight: 1.5 }}>
|
||||||
|
Sichtbarkeit und Reihenfolge steuern. Wird nur <strong>lokal in diesem Browser</strong> gespeichert
|
||||||
|
(<code style={{ fontSize: 11 }}>localStorage</code>) – gut zum Ausprobieren vor einer serverseitigen
|
||||||
|
Profil-Konfiguration.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul style={{ listStyle: 'none', margin: 0, padding: 0, display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||||
|
{layout.order.map((id, index) => {
|
||||||
|
const def = PILOT_WIDGET_DEFS[id]
|
||||||
|
if (!def) return null
|
||||||
|
const on = !!layout.enabled[id]
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={id}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 8,
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
padding: '10px 12px',
|
||||||
|
borderRadius: 10,
|
||||||
|
background: on ? 'var(--surface2)' : 'var(--surface)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
opacity: on ? 1 : 0.72,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary"
|
||||||
|
style={{ padding: '4px 8px', minWidth: 36 }}
|
||||||
|
title="Nach oben"
|
||||||
|
disabled={index === 0}
|
||||||
|
onClick={() => move(index, -1)}
|
||||||
|
>
|
||||||
|
<ChevronUp size={16} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary"
|
||||||
|
style={{ padding: '4px 8px', minWidth: 36 }}
|
||||||
|
title="Nach unten"
|
||||||
|
disabled={index === layout.order.length - 1}
|
||||||
|
onClick={() => move(index, 1)}
|
||||||
|
>
|
||||||
|
<ChevronDown size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div style={{ flex: 1, minWidth: 140 }}>
|
||||||
|
<div style={{ fontSize: 13, fontWeight: 600, color: 'var(--text1)' }}>{def.title}</div>
|
||||||
|
<code style={{ fontSize: 10, color: 'var(--text3)' }}>{id}</code>
|
||||||
|
</div>
|
||||||
|
<label
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 8,
|
||||||
|
fontSize: 13,
|
||||||
|
cursor: 'pointer',
|
||||||
|
userSelect: 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input type="checkbox" checked={on} onChange={() => toggle(id)} />
|
||||||
|
sichtbar
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div style={{ marginTop: 14, display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'center' }}>
|
||||||
|
<button type="button" className="btn btn-secondary" onClick={handleReset} style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
|
||||||
|
<RotateCcw size={14} />
|
||||||
|
Standard wiederherstellen
|
||||||
|
</button>
|
||||||
|
<span style={{ fontSize: 11, color: 'var(--text3)' }}>
|
||||||
|
Standard-Reihenfolge: {PILOT_WIDGET_IDS_DEFAULT_ORDER.join(' → ')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
87
frontend/src/components/pilot/WeightKpiWidget.jsx
Normal file
87
frontend/src/components/pilot/WeightKpiWidget.jsx
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { api } from '../../utils/api'
|
||||||
|
|
||||||
|
export default function WeightKpiWidget() {
|
||||||
|
const [stats, setStats] = useState(null)
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [err, setErr] = useState(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false
|
||||||
|
;(async () => {
|
||||||
|
try {
|
||||||
|
const s = await api.weightStats()
|
||||||
|
if (!cancelled) {
|
||||||
|
setStats(s)
|
||||||
|
setErr(null)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (!cancelled) {
|
||||||
|
setErr(e.message || 'Laden fehlgeschlagen')
|
||||||
|
setStats(null)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (!cancelled) setLoading(false)
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
return () => {
|
||||||
|
cancelled = true
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div style={{ padding: 16, textAlign: 'center' }}>
|
||||||
|
<div className="spinner" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (err) {
|
||||||
|
return <div style={{ fontSize: 13, color: 'var(--danger)' }}>{err}</div>
|
||||||
|
}
|
||||||
|
const latest = stats?.latest
|
||||||
|
const prev = stats?.prev
|
||||||
|
if (!latest) {
|
||||||
|
return (
|
||||||
|
<p style={{ fontSize: 13, color: 'var(--text2)', margin: 0 }}>
|
||||||
|
Noch kein Gewicht erfasst.{' '}
|
||||||
|
<Link to="/weight" style={{ color: 'var(--accent)' }}>
|
||||||
|
Zur Eingabe
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const delta =
|
||||||
|
prev && typeof latest.weight === 'number' && typeof prev.weight === 'number'
|
||||||
|
? Math.round((latest.weight - prev.weight) * 10) / 10
|
||||||
|
: null
|
||||||
|
const deltaColor =
|
||||||
|
delta == null ? 'var(--text3)' : delta < 0 ? 'var(--accent)' : delta > 0 ? 'var(--warn)' : 'var(--text3)'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 16, alignItems: 'flex-end' }}>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: 11, color: 'var(--text3)', marginBottom: 4 }}>Aktuelles Gewicht</div>
|
||||||
|
<div style={{ fontSize: 26, fontWeight: 800, color: '#378ADD', lineHeight: 1 }}>
|
||||||
|
{latest.weight}
|
||||||
|
<span style={{ fontSize: 14, fontWeight: 600, color: 'var(--text2)', marginLeft: 4 }}>kg</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: 12, color: 'var(--text3)', marginTop: 6 }}>
|
||||||
|
Stand {dayjs(latest.date).format('DD.MM.YYYY')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{delta != null && (
|
||||||
|
<div style={{ fontSize: 13, fontWeight: 600, color: deltaColor }}>
|
||||||
|
{delta > 0 ? '+' : ''}
|
||||||
|
{delta} kg <span style={{ fontWeight: 400, color: 'var(--text3)' }}>ggü. Vorher</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Link to="/history" className="btn btn-secondary" style={{ fontSize: 12, padding: '6px 12px', textDecoration: 'none' }}>
|
||||||
|
Verlauf →
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,16 @@
|
||||||
|
import { useState } from 'react'
|
||||||
import { FlaskConical } from 'lucide-react'
|
import { FlaskConical } from 'lucide-react'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { getPilotWidgetsInOrder } from '../pilot/widgetRegistry'
|
import { resolvePilotWidgetsForRender } from '../pilot/widgetRegistry'
|
||||||
|
import { loadPilotLayout } from '../pilot/pilotLayoutStorage'
|
||||||
|
import PilotVizAdminCard from '../components/pilot/PilotVizAdminCard'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pilot: Widget-Schicht (Layer 3b) parallel zum produktiven Dashboard.
|
* Pilot: Widget-Schicht (Layer 3b) parallel zum produktiven Dashboard.
|
||||||
* Daten für Referenzwerte: Layer 1 (backend/data_layer/reference_values.py) → API.
|
|
||||||
*/
|
*/
|
||||||
export default function PilotVizPage() {
|
export default function PilotVizPage() {
|
||||||
const widgets = getPilotWidgetsInOrder()
|
const [layout, setLayout] = useState(() => loadPilotLayout())
|
||||||
|
const widgets = resolvePilotWidgetsForRender(layout)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ paddingBottom: 96, textAlign: 'left', maxWidth: 900, margin: '0 auto' }}>
|
<div style={{ paddingBottom: 96, textAlign: 'left', maxWidth: 900, margin: '0 auto' }}>
|
||||||
|
|
@ -24,13 +27,20 @@ export default function PilotVizPage() {
|
||||||
Pilot: Visualisierungs-Module
|
Pilot: Visualisierungs-Module
|
||||||
</h1>
|
</h1>
|
||||||
<p style={{ fontSize: 14, color: 'var(--text2)', lineHeight: 1.6, marginTop: 8 }}>
|
<p style={{ fontSize: 14, color: 'var(--text2)', lineHeight: 1.6, marginTop: 8 }}>
|
||||||
Testumgebung für die zukünftige Widget-Registry. Die produktive Übersicht und der Verlauf bleiben
|
Testumgebung für die Widget-Registry. Konfiguration unten; produktive Übersicht und Verlauf sind unverändert.
|
||||||
unverändert. Referenzwerte-Summary wird über die gleiche API geladen wie in der Erfassungs-UI, angeboten
|
|
||||||
durch den Data Layer (Layer 1).
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{widgets.map((def) => {
|
<PilotVizAdminCard layout={layout} onLayoutChange={setLayout} />
|
||||||
|
|
||||||
|
{widgets.length === 0 ? (
|
||||||
|
<div className="card section-gap" style={{ textAlign: 'center', padding: 24 }}>
|
||||||
|
<p style={{ fontSize: 14, color: 'var(--text2)', margin: 0 }}>
|
||||||
|
Keine Widgets sichtbar. Aktiviere mindestens eines in der <strong>Widget-Konfiguration</strong> oben.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
widgets.map((def) => {
|
||||||
const { Component } = def
|
const { Component } = def
|
||||||
return (
|
return (
|
||||||
<section key={def.id} className="card section-gap" style={{ overflow: 'hidden' }}>
|
<section key={def.id} className="card section-gap" style={{ overflow: 'hidden' }}>
|
||||||
|
|
@ -43,7 +53,8 @@ export default function PilotVizPage() {
|
||||||
<Component />
|
<Component />
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
})}
|
})
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
52
frontend/src/pilot/pilotLayoutStorage.js
Normal file
52
frontend/src/pilot/pilotLayoutStorage.js
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
/**
|
||||||
|
* Pilot-Layout: nur lokal (localStorage), kein Server.
|
||||||
|
* Dient zum Testen von Sichtbarkeit und Reihenfolge vor einer DB-Lösung.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { PILOT_WIDGET_DEFS, PILOT_WIDGET_IDS_DEFAULT_ORDER } from './widgetRegistry'
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'mitai_pilot_viz_layout_v1'
|
||||||
|
|
||||||
|
function defaultLayout() {
|
||||||
|
const order = [...PILOT_WIDGET_IDS_DEFAULT_ORDER]
|
||||||
|
const enabled = Object.fromEntries(order.map((id) => [id, true]))
|
||||||
|
return { version: 1, order, enabled}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeWithRegistry(saved) {
|
||||||
|
if (!saved || typeof saved !== 'object') return defaultLayout()
|
||||||
|
const known = new Set(Object.keys(PILOT_WIDGET_DEFS))
|
||||||
|
let order = Array.isArray(saved.order) ? saved.order.filter((id) => known.has(id)) : []
|
||||||
|
for (const id of PILOT_WIDGET_IDS_DEFAULT_ORDER) {
|
||||||
|
if (!order.includes(id)) order.push(id)
|
||||||
|
}
|
||||||
|
const enabled = { ...defaultLayout().enabled, ...(saved.enabled && typeof saved.enabled === 'object' ? saved.enabled : {}) }
|
||||||
|
for (const id of Object.keys(PILOT_WIDGET_DEFS)) {
|
||||||
|
if (typeof enabled[id] !== 'boolean') enabled[id] = true
|
||||||
|
}
|
||||||
|
return { version: 1, order, enabled }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadPilotLayout() {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(STORAGE_KEY)
|
||||||
|
if (!raw) return defaultLayout()
|
||||||
|
return mergeWithRegistry(JSON.parse(raw))
|
||||||
|
} catch {
|
||||||
|
return defaultLayout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function savePilotLayout(layout) {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(layout))
|
||||||
|
} catch {
|
||||||
|
/* ignore quota */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resetPilotLayout() {
|
||||||
|
const d = defaultLayout()
|
||||||
|
savePilotLayout(d)
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
@ -1,20 +1,38 @@
|
||||||
import ReferenceValuesSummaryWidget from '../components/pilot/ReferenceValuesSummaryWidget'
|
import ReferenceValuesSummaryWidget from '../components/pilot/ReferenceValuesSummaryWidget'
|
||||||
|
import GoalsSnapshotWidget from '../components/pilot/GoalsSnapshotWidget'
|
||||||
|
import WeightKpiWidget from '../components/pilot/WeightKpiWidget'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pilot: konfigurierbare Widget-Reihenfolge (Layer 3b-Vorspiel).
|
* Pilot: Widget-Registry (Layer 3b-Vorspiel).
|
||||||
* Nur für /pilot/viz – produktives Dashboard bleibt unverändert.
|
* Reihenfolge-Default: PILOT_WIDGET_IDS_DEFAULT_ORDER
|
||||||
*/
|
*/
|
||||||
export const PILOT_WIDGET_ORDER = ['reference_values_summary']
|
export const PILOT_WIDGET_IDS_DEFAULT_ORDER = ['weight_kpi', 'goals_teaser', 'reference_values_summary']
|
||||||
|
|
||||||
export const PILOT_WIDGET_DEFS = {
|
export const PILOT_WIDGET_DEFS = {
|
||||||
|
weight_kpi: {
|
||||||
|
id: 'weight_kpi',
|
||||||
|
title: 'Gewicht (KPI)',
|
||||||
|
description: 'Letzter Eintrag und Delta zum vorherigen Messwert (API weight/stats).',
|
||||||
|
Component: WeightKpiWidget,
|
||||||
|
},
|
||||||
|
goals_teaser: {
|
||||||
|
id: 'goals_teaser',
|
||||||
|
title: 'Strategische Ziele',
|
||||||
|
description: 'Anzahl aktiver Ziele, Link zur Ziele-Seite (API goals/list).',
|
||||||
|
Component: GoalsSnapshotWidget,
|
||||||
|
},
|
||||||
reference_values_summary: {
|
reference_values_summary: {
|
||||||
id: 'reference_values_summary',
|
id: 'reference_values_summary',
|
||||||
title: 'Referenzwerte',
|
title: 'Referenzwerte',
|
||||||
description: 'Aktuellste Kennwerte pro Typ (Daten via Layer 1 → bestehende API).',
|
description: 'Aktuellste Kennwerte pro Typ (Layer 1 → /profile-reference-values/summary).',
|
||||||
Component: ReferenceValuesSummaryWidget,
|
Component: ReferenceValuesSummaryWidget,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPilotWidgetsInOrder() {
|
/** Sichtbare Widgets in gespeicherter Reihenfolge (für Render). */
|
||||||
return PILOT_WIDGET_ORDER.map((id) => PILOT_WIDGET_DEFS[id]).filter(Boolean)
|
export function resolvePilotWidgetsForRender(layout) {
|
||||||
|
if (!layout?.order) return []
|
||||||
|
return layout.order
|
||||||
|
.filter((id) => layout.enabled[id] && PILOT_WIDGET_DEFS[id])
|
||||||
|
.map((id) => PILOT_WIDGET_DEFS[id])
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user