feat: Refactor dashboard layout with new DashboardSection and DashboardTile components for improved responsiveness and organization
All checks were successful
Deploy Development / deploy (push) Successful in 56s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 14s

This commit is contained in:
Lars 2026-04-05 08:42:07 +02:00
parent d51bfd3daa
commit 422a117026
5 changed files with 390 additions and 167 deletions

View File

@ -371,12 +371,20 @@ body { font-family: var(--font); background: var(--bg); color: var(--text1); -we
margin-bottom: 16px; margin-bottom: 16px;
} }
/* KPI: immer gleich breite Spalten — Mobile 2×2, Desktop 1×4 (kein „einzelne volle Zeile“) */ /*
.dashboard-stat-grid { * Dashboard-Raster (KPI, Nebeneinander-Kacheln): 2 / 4 Spalten.
* StatCard, DashboardTile: span via --tile-sm / --tile-lg (JS clamp).
*/
.dashboard-stat-grid,
.dashboard-tile-grid {
display: grid; display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px; gap: 8px;
margin-bottom: 16px; }
.dashboard-stat-grid--mobile-4col,
.dashboard-tile-grid--mobile-4col {
grid-template-columns: repeat(4, minmax(0, 1fr));
} }
.dashboard-stat-card { .dashboard-stat-card {
@ -385,34 +393,94 @@ body { font-family: var(--font); background: var(--bg); color: var(--text1); -we
padding: 12px 10px; padding: 12px 10px;
border: 1px solid var(--border); border: 1px solid var(--border);
transition: border-color 0.15s; transition: border-color 0.15s;
}
.dashboard-stat-card,
.dashboard-tile {
min-width: 0; min-width: 0;
box-sizing: border-box; box-sizing: border-box;
grid-column: span var(--tile-sm, 1);
} }
.dashboard-summary-row { @media (min-width: 1024px) {
.dashboard-stat-card,
.dashboard-tile {
grid-column: span var(--tile-lg, 1);
}
}
/* ── Dashboard-Abschnitte (Überschrift + Trennlinie) ─ */
.dashboard-section {
margin-bottom: 22px;
}
.dashboard-section:last-child {
margin-bottom: 0;
}
.dashboard-section__header {
display: flex; display: flex;
gap: 8px; align-items: flex-end;
margin-bottom: 16px; justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
padding-bottom: 10px;
margin-bottom: 12px;
border-bottom: 1px solid var(--border);
} }
.dashboard-summary-row > .card { .dashboard-section__headline {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
} }
.dashboard-section__title {
font-size: 13px;
font-weight: 700;
color: var(--text3);
text-transform: uppercase;
letter-spacing: 0.05em;
margin: 0;
}
.dashboard-section__description {
font-size: 12px;
color: var(--text3);
margin: 4px 0 0 0;
line-height: 1.35;
max-width: 640px;
}
.dashboard-section__body {
display: flex;
flex-direction: column;
gap: 12px;
}
.dashboard-section__actions {
flex-shrink: 0;
}
.dashboard-pill-row {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
/* Ernährung/Aktivität: Raster wie KPI; Kacheln per DashboardTile steuerbar */
.dashboard-summary-row.dashboard-tile-grid {
margin-bottom: 0;
}
.dashboard-erholung-grid .dashboard-tile > .card,
.dashboard-summary-row .dashboard-tile > .card {
height: 100%;
}
@media (min-width: 1024px) { @media (min-width: 1024px) {
.dashboard-stat-grid { .dashboard-stat-grid,
.dashboard-tile-grid {
grid-template-columns: repeat(4, minmax(0, 1fr)); grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px; gap: 10px;
} }
.dashboard-summary-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.dashboard-summary-row > .card {
flex: unset;
}
} }

View File

@ -0,0 +1,31 @@
/**
* Abschnitt mit optionalem Kopf (Titel, Beschreibung, Aktionen) und unterem Rand zur optischen Trennung.
*/
export default function DashboardSection({
title,
description,
headerRight,
children,
className = '',
bodyClassName = ''
}) {
const showHeader = title || description || headerRight
return (
<section className={`dashboard-section ${className}`.trim()}>
{showHeader && (
<header className="dashboard-section__header">
<div className="dashboard-section__headline">
{title ? <h2 className="dashboard-section__title">{title}</h2> : null}
{description ? (
<p className="dashboard-section__description">{description}</p>
) : null}
</div>
{headerRight ? (
<div className="dashboard-section__actions">{headerRight}</div>
) : null}
</header>
)}
<div className={`dashboard-section__body ${bodyClassName}`.trim()}>{children}</div>
</section>
)
}

View File

@ -0,0 +1,29 @@
import {
clampTileSpan,
DASHBOARD_TILE_GRID_COLS
} from '../utils/dashboardLayout'
/**
* Kachel im Raster `.dashboard-tile-grid` / `.dashboard-stat-grid`.
* Standard: volle Zeile (Mobile 2/2, Desktop 4/4). Anpassbar via spanMobile / spanDesktop.
*/
export default function DashboardTile({
children,
spanMobile = DASHBOARD_TILE_GRID_COLS.mobile,
spanDesktop = DASHBOARD_TILE_GRID_COLS.desktop,
className = ''
}) {
const sm = clampTileSpan(spanMobile, DASHBOARD_TILE_GRID_COLS.mobile)
const lg = clampTileSpan(spanDesktop, DASHBOARD_TILE_GRID_COLS.desktop)
return (
<div
className={`dashboard-tile ${className}`.trim()}
style={{
'--tile-sm': String(sm),
'--tile-lg': String(lg)
}}
>
{children}
</div>
)
}

View File

@ -1,6 +1,6 @@
import { useState, useEffect, useRef } from 'react' import { useState, useEffect } from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { Check, ChevronRight, Brain } from 'lucide-react' import { Check, Brain } from 'lucide-react'
import { import {
LineChart, Line, XAxis, YAxis, Tooltip, LineChart, Line, XAxis, YAxis, Tooltip,
ResponsiveContainer, CartesianGrid ResponsiveContainer, CartesianGrid
@ -13,10 +13,17 @@ import EmailVerificationBanner from '../components/EmailVerificationBanner'
import TrainingTypeDistribution from '../components/TrainingTypeDistribution' import TrainingTypeDistribution from '../components/TrainingTypeDistribution'
import SleepWidget from '../components/SleepWidget' import SleepWidget from '../components/SleepWidget'
import RestDaysWidget from '../components/RestDaysWidget' import RestDaysWidget from '../components/RestDaysWidget'
import { getInterpretation, getStatusColor, getStatusBg } from '../utils/interpret'
import Markdown from '../utils/Markdown' import Markdown from '../utils/Markdown'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import 'dayjs/locale/de' import 'dayjs/locale/de'
import DashboardSection from '../components/DashboardSection'
import DashboardTile from '../components/DashboardTile'
import {
clampTileSpan,
DASHBOARD_TILE_GRID_COLS,
dashboardStatGridClassName,
dashboardTileGridClassName
} from '../utils/dashboardLayout'
dayjs.locale('de') dayjs.locale('de')
// Helpers // Helpers
@ -144,14 +151,37 @@ function Pill({ label, value, status, sub }) {
} }
// Stat Card // Stat Card
function StatCard({ icon, label, value, unit, delta, deltaGoodWhenNeg=false, sub, onClick, color }) { /**
* KPI-Kachel im Dashboard-Raster (`dashboard-stat-grid` / `dashboard-tile-grid`).
* @param {number} [spanMobile=1] Spaltenbreite unter 1024px (max. = Raster-Spalten mobile)
* @param {number} [spanDesktop=1] Spaltenbreite 1024px (max. 4)
*/
function StatCard({
icon,
label,
value,
unit,
delta,
deltaGoodWhenNeg = false,
sub,
onClick,
color,
spanMobile = 1,
spanDesktop = 1
}) {
const deltaColor = delta==null ? null const deltaColor = delta==null ? null
: (deltaGoodWhenNeg ? delta<0 : delta>0) ? 'var(--accent)' : 'var(--warn)' : (deltaGoodWhenNeg ? delta<0 : delta>0) ? 'var(--accent)' : 'var(--warn)'
const sm = clampTileSpan(spanMobile, DASHBOARD_TILE_GRID_COLS.mobile)
const lg = clampTileSpan(spanDesktop, DASHBOARD_TILE_GRID_COLS.desktop)
return ( return (
<div <div
className="dashboard-stat-card" className="dashboard-stat-card"
onClick={onClick} onClick={onClick}
style={{ cursor: onClick ? 'pointer' : 'default' }} style={{
cursor: onClick ? 'pointer' : 'default',
'--tile-sm': String(sm),
'--tile-lg': String(lg)
}}
onMouseEnter={e=>onClick&&(e.currentTarget.style.borderColor='var(--accent)')} onMouseEnter={e=>onClick&&(e.currentTarget.style.borderColor='var(--accent)')}
onMouseLeave={e=>onClick&&(e.currentTarget.style.borderColor='var(--border)')}> onMouseLeave={e=>onClick&&(e.currentTarget.style.borderColor='var(--border)')}>
<div style={{fontSize:18,marginBottom:4}}>{icon}</div> <div style={{fontSize:18,marginBottom:4}}>{icon}</div>
@ -260,7 +290,6 @@ export default function Dashboard() {
const runPipeline = async () => { const runPipeline = async () => {
setPipelineLoading(true); setPipelineError(null) setPipelineLoading(true); setPipelineError(null)
try { try {
const pid = localStorage.getItem('mitai-jinkendo_active_profile')||''
await api.insightPipeline() await api.insightPipeline()
await load() await load()
} catch(e) { } catch(e) {
@ -268,12 +297,7 @@ export default function Dashboard() {
} finally { setPipelineLoading(false) } } finally { setPipelineLoading(false) }
} }
useEffect(()=>{ useEffect(()=>{ load() },[])
console.log('[Dashboard] Component mounted, loading data...')
load()
},[])
console.log('[Dashboard] Rendering, loading=', loading, 'activeProfile=', activeProfile?.name)
if (loading) return <div className="empty-state"><div className="spinner"/></div> if (loading) return <div className="empty-state"><div className="spinner"/></div>
@ -317,7 +341,11 @@ export default function Dashboard() {
const hasAnyData = latestW||latestCal||nutrition.length>0 const hasAnyData = latestW||latestCal||nutrition.length>0
console.log('[Dashboard] hasAnyData=', hasAnyData, 'latestW=', !!latestW, 'latestCal=', !!latestCal, 'nutrition.length=', nutrition.length) const showNutrSummary = !!(avgKcal || avgProtein)
const showActSummary = actKcal != null
const summaryBoth = showNutrSummary && showActSummary
const summarySpanM = summaryBoth ? 1 : 2
const summarySpanD = summaryBoth ? 2 : 4
return ( return (
<div className="dashboard-page"> <div className="dashboard-page">
@ -349,46 +377,54 @@ export default function Dashboard() {
)} )}
{hasAnyData && <> {hasAnyData && <>
{/* Quick weight entry */} <DashboardSection
<div className="card section-gap"> title="Gewicht heute"
<div style={{display:'flex',alignItems:'center',justifyContent:'space-between',marginBottom:10}}> description="Tageswert erfassen Grundlage für Trends und Ziele."
<div style={{fontWeight:600,fontSize:14}}> Gewicht heute</div> headerRight={
<button style={{background:'none',border:'none',fontSize:12,color:'var(--accent)',cursor:'pointer'}} <button type="button" className="btn btn-secondary"
onClick={()=>nav('/weight')}> style={{ fontSize: 12, padding: '6px 12px' }}
onClick={() => nav('/weight')}>
Alle Einträge Alle Einträge
</button> </button>
}
>
<div className="card section-gap">
<QuickWeight onSaved={load}/>
</div> </div>
<QuickWeight onSaved={load}/> </DashboardSection>
</div>
{/* Key metrics — Mobile: flex-wrap; Desktop: 4-spaltig (RESPONSIVE_UI P3) */} <DashboardSection
<div className="dashboard-stat-grid"> title="Kennzahlen"
<StatCard icon="⚖️" label="Gewicht" value={latestW?.weight??''} unit="kg" description="Aktuelle Messwerte und Ernährungs-Schnitt (7 Tage)."
delta={wDelta} deltaGoodWhenNeg={true} >
sub={latestW ? dayjs(latestW.date).format('DD.MM.') : ''} <div className={dashboardStatGridClassName(DASHBOARD_TILE_GRID_COLS.mobile)}>
onClick={()=>nav('/history')} color="#378ADD"/> <StatCard icon="⚖️" label="Gewicht" value={latestW?.weight??''} unit="kg"
{latestCal?.body_fat_pct && <StatCard icon="🫧" label="Körperfett" value={latestCal.body_fat_pct} unit="%" delta={wDelta} deltaGoodWhenNeg={true}
delta={bfDelta} deltaGoodWhenNeg={true} sub={latestW ? dayjs(latestW.date).format('DD.MM.') : ''}
sub={bfCat?.label} onClick={()=>nav('/history')} color="#378ADD"/>
onClick={()=>nav('/history',{state:{tab:'body'}})} color={bfCat?.color}/>} {latestCal?.body_fat_pct && <StatCard icon="🫧" label="Körperfett" value={latestCal.body_fat_pct} unit="%"
{latestCal?.lean_mass && <StatCard icon="💪" label="Magermasse" value={latestCal.lean_mass} unit="kg" delta={bfDelta} deltaGoodWhenNeg={true}
sub={latestCal.date ? dayjs(latestCal.date).format('DD.MM.') : ''} sub={bfCat?.label}
onClick={()=>nav('/history',{state:{tab:'body'}})}/>} onClick={()=>nav('/history',{state:{tab:'body'}})} color={bfCat?.color}/>}
{avgKcal && <StatCard icon="🍽️" label="Ø Kalorien" value={avgKcal} unit="kcal" {latestCal?.lean_mass && <StatCard icon="💪" label="Magermasse" value={latestCal.lean_mass} unit="kg"
sub="letzte 7 Tage" onClick={()=>nav('/history',{state:{tab:'nutrition'}})} color="#EF9F27"/>} sub={latestCal.date ? dayjs(latestCal.date).format('DD.MM.') : ''}
</div> onClick={()=>nav('/history',{state:{tab:'body'}})}/>}
{avgKcal && <StatCard icon="🍽️" label="Ø Kalorien" value={avgKcal} unit="kcal"
{/* Status pills */} sub="letzte 7 Tage" onClick={()=>nav('/history',{state:{tab:'nutrition'}})} color="#EF9F27"/>}
{pills.length > 0 && (
<div style={{display:'flex',gap:6,flexWrap:'wrap',marginBottom:16}}>
{pills.map((p,i)=><Pill key={i} {...p}/>)}
</div> </div>
)} {pills.length > 0 && (
<div className="dashboard-pill-row">
{pills.map((p,i)=><Pill key={i} {...p}/>)}
</div>
)}
</DashboardSection>
{/* Goals progress */}
{(activeProfile?.goal_weight||activeProfile?.goal_bf_pct) && latestW && ( {(activeProfile?.goal_weight||activeProfile?.goal_bf_pct) && latestW && (
<div className="card section-gap" style={{marginBottom:16}}> <DashboardSection
<div style={{fontWeight:600,fontSize:13,marginBottom:10}}>🎯 Ziele</div> title="Profil-Ziele"
description="Fortschritt zu den Zielwerten in deinem Profil."
>
<div className="card section-gap">
{activeProfile?.goal_weight && latestW && (()=>{ {activeProfile?.goal_weight && latestW && (()=>{
const start = Math.max(...weights.map(w=>w.weight)) const start = Math.max(...weights.map(w=>w.weight))
const curr = latestW.weight const curr = latestW.weight
@ -429,134 +465,167 @@ export default function Dashboard() {
) )
})()} })()}
</div> </div>
</DashboardSection>
)} )}
{/* Combined chart */}
{(weights.length>2||nutrition.length>2) && ( {(weights.length>2||nutrition.length>2) && (
<div className="card section-gap" style={{marginBottom:16}}> <DashboardSection
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:8}}> title="Trends"
<div style={{fontWeight:600,fontSize:13}}>📊 Kalorien + Gewicht (30 Tage)</div> description="Kalorien und Gewicht der letzten 30 Tage."
<button style={{background:'none',border:'none',fontSize:12,color:'var(--accent)',cursor:'pointer'}} headerRight={
<button type="button" className="btn btn-secondary" style={{ fontSize: 12, padding: '6px 12px' }}
onClick={()=>nav('/history',{state:{tab:'body'}})}> onClick={()=>nav('/history',{state:{tab:'body'}})}>
Details Details
</button> </button>
</div> }
<ComboChart weights={weights} nutrition={nutrition}/> >
<div style={{display:'flex',gap:16,justifyContent:'center',marginTop:6,fontSize:10,color:'var(--text3)'}}> <DashboardTile>
<span><span style={{display:'inline-block',width:12,height:2,background:'#EF9F27',verticalAlign:'middle',marginRight:3}}/>Ø Kalorien</span> <div className="card section-gap">
<span><span style={{display:'inline-block',width:12,height:2,background:'#378ADD',verticalAlign:'middle',marginRight:3}}/>Gewicht</span> <ComboChart weights={weights} nutrition={nutrition}/>
</div> <div style={{display:'flex',gap:16,justifyContent:'center',marginTop:6,fontSize:10,color:'var(--text3)'}}>
</div> <span><span style={{display:'inline-block',width:12,height:2,background:'#EF9F27',verticalAlign:'middle',marginRight:3}}/>Ø Kalorien</span>
<span><span style={{display:'inline-block',width:12,height:2,background:'#378ADD',verticalAlign:'middle',marginRight:3}}/>Gewicht</span>
</div>
</div>
</DashboardTile>
</DashboardSection>
)} )}
{/* Activity + Nutrition summary row */} {(showNutrSummary || showActSummary) && (
<div className="dashboard-summary-row"> <DashboardSection
{(avgKcal||avgProtein) && ( title="Ernährung & Aktivität"
<div className="card" style={{flex:1,cursor:'pointer'}} onClick={()=>nav('/history',{state:{tab:'nutrition'}})}> description="Kurzüberblick; volle Verläufe unter Historie."
<div style={{fontWeight:600,fontSize:12,marginBottom:8,color:'var(--text3)'}}>🍽 ERNÄHRUNG (Ø 7T)</div> >
{avgKcal && <div style={{fontSize:16,fontWeight:700,color:'#EF9F27'}}>{avgKcal} kcal</div>} <div className={`dashboard-summary-row ${dashboardTileGridClassName(DASHBOARD_TILE_GRID_COLS.mobile)}`}>
{avgProtein && <div style={{fontSize:13,fontWeight:600, {showNutrSummary && (
color:proteinOk?'var(--accent)':'var(--warn)'}}> <DashboardTile spanMobile={summarySpanM} spanDesktop={summarySpanD}>
{avgProtein}g Protein {proteinOk?'✓':'⚠️'} <div className="card" style={{ cursor: 'pointer', height: '100%' }} onClick={()=>nav('/history',{state:{tab:'nutrition'}})}>
</div>} <div style={{fontWeight:600,fontSize:12,marginBottom:8,color:'var(--text3)'}}>🍽 ERNÄHRUNG (Ø 7T)</div>
<div style={{fontSize:10,color:'var(--text3)',marginTop:4}}> Verlauf Ernährung</div> {avgKcal && <div style={{fontSize:16,fontWeight:700,color:'#EF9F27'}}>{avgKcal} kcal</div>}
{avgProtein && <div style={{fontSize:13,fontWeight:600,
color:proteinOk?'var(--accent)':'var(--warn)'}}>
{avgProtein}g Protein {proteinOk?'✓':'⚠️'}
</div>}
<div style={{fontSize:10,color:'var(--text3)',marginTop:4}}> Verlauf Ernährung</div>
</div>
</DashboardTile>
)}
{showActSummary && (
<DashboardTile spanMobile={summarySpanM} spanDesktop={summarySpanD}>
<div className="card" style={{ cursor: 'pointer', height: '100%' }} onClick={()=>nav('/history',{state:{tab:'activity'}})}>
<div style={{fontWeight:600,fontSize:12,marginBottom:8,color:'var(--text3)'}}>🏋 AKTIVITÄT (7T)</div>
<div style={{fontSize:16,fontWeight:700,color:'#EF9F27'}}>{actKcal} kcal</div>
<div style={{fontSize:13,color:'var(--text2)'}}>{recentAct.length} Trainings</div>
<div style={{fontSize:10,color:'var(--text3)',marginTop:4}}> Verlauf Aktivität</div>
</div>
</DashboardTile>
)}
</div> </div>
)} </DashboardSection>
{actKcal!=null && ( )}
<div className="card" style={{flex:1,cursor:'pointer'}} onClick={()=>nav('/history',{state:{tab:'activity'}})}>
<div style={{fontWeight:600,fontSize:12,marginBottom:8,color:'var(--text3)'}}>🏋 AKTIVITÄT (7T)</div>
<div style={{fontSize:16,fontWeight:700,color:'#EF9F27'}}>{actKcal} kcal</div>
<div style={{fontSize:13,color:'var(--text2)'}}>{recentAct.length} Trainings</div>
<div style={{fontSize:10,color:'var(--text3)',marginTop:4}}> Verlauf Aktivität</div>
</div>
)}
</div>
{/* Sleep Widget */} <DashboardSection
<div style={{marginBottom:16}}> title="Erholung"
<SleepWidget/> description="Schlaf und Ruhetage im Überblick."
</div> >
<div className={`dashboard-erholung-grid ${dashboardTileGridClassName(DASHBOARD_TILE_GRID_COLS.mobile)}`}>
<DashboardTile spanMobile={1} spanDesktop={2}>
<SleepWidget/>
</DashboardTile>
<DashboardTile spanMobile={1} spanDesktop={2}>
<RestDaysWidget/>
</DashboardTile>
</div>
</DashboardSection>
{/* Rest Days Widget */}
<div style={{marginBottom:16}}>
<RestDaysWidget/>
</div>
{/* Training Type Distribution */}
{activities.length > 0 && ( {activities.length > 0 && (
<div className="card section-gap" style={{marginBottom:16}}> <DashboardSection
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:12}}> title="Training"
<div style={{fontWeight:600,fontSize:13}}>🏋 Trainingstyp-Verteilung</div> description="Verteilung der Trainingstypen (28 Tage)."
<button style={{background:'none',border:'none',fontSize:12,color:'var(--accent)',cursor:'pointer'}} headerRight={
<button type="button" className="btn btn-secondary" style={{ fontSize: 12, padding: '6px 12px' }}
onClick={()=>nav('/activity')}> onClick={()=>nav('/activity')}>
Details Details
</button> </button>
</div> }
<TrainingTypeDistribution days={28} /> >
</div> <DashboardTile>
<div className="card section-gap">
<TrainingTypeDistribution days={28} />
</div>
</DashboardTile>
</DashboardSection>
)} )}
{/* Goals Preview */} <DashboardSection
<div className="card section-gap" style={{marginBottom:16,cursor:'pointer'}} title="Ziele & Fokus"
onClick={()=>nav('/goals')}> description="Trainingsmodus, Schwerpunkte und konkrete Ziele für die KI."
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:12}}> headerRight={
<div style={{fontWeight:600,fontSize:13}}>🎯 Ziele</div> <button type="button" className="btn btn-secondary" style={{ fontSize: 12, padding: '6px 12px' }}
<button style={{background:'none',border:'none',fontSize:12,color:'var(--accent)',cursor:'pointer'}} onClick={(e)=>{ e.stopPropagation(); nav('/goals') }}>
onClick={(e)=>{e.stopPropagation();nav('/goals')}}>
Verwalten Verwalten
</button> </button>
</div> }
<div style={{fontSize:12,color:'var(--text2)',padding:'8px 0'}}> >
Definiere deine Trainingsmodus und konkrete Ziele für bessere KI-Analysen <DashboardTile>
</div> <div className="card section-gap" style={{ cursor: 'pointer' }} onClick={()=>nav('/goals')}>
</div> <div style={{fontSize:12,color:'var(--text2)',padding:'8px 0'}}>
Definiere deine Trainingsmodus und konkrete Ziele für bessere KI-Analysen
</div>
</div>
</DashboardTile>
</DashboardSection>
{/* Latest AI insight */} <DashboardSection
<div className="card section-gap"> title="KI-Auswertung"
<div style={{display:'flex',alignItems:'center',justifyContent:'space-between',marginBottom:8}}> description="Mehrstufige Pipeline und letzte Zusammenfassung."
<div style={{fontWeight:600,fontSize:13}}>🤖 KI-Auswertung</div> headerRight={
<button className="btn btn-secondary" style={{fontSize:11,padding:'4px 10px'}} <button type="button" className="btn btn-secondary" style={{ fontSize: 11, padding: '4px 10px' }}
onClick={()=>nav('/analysis')}> onClick={()=>nav('/analysis')}>
<Brain size={11}/> Analysen <Brain size={11}/> Analysen
</button> </button>
</div> }
{/* Pipeline trigger */} >
<button className="btn btn-primary btn-full" style={{marginBottom:10}} <DashboardTile>
onClick={runPipeline} disabled={pipelineLoading}> <div className="card section-gap">
{pipelineLoading <button type="button" className="btn btn-primary btn-full" style={{marginBottom:10}}
? <><div className="spinner" style={{width:13,height:13}}/> Analyse läuft (3 Stufen)</> onClick={runPipeline} disabled={pipelineLoading}>
: <><Brain size={13}/> 🔬 Mehrstufige Analyse starten</>} {pipelineLoading
</button> ? <><div className="spinner" style={{width:13,height:13}}/> Analyse läuft (3 Stufen)</>
{pipelineError && <div style={{fontSize:12,color:'#D85A30',marginBottom:8}}>{pipelineError}</div>} : <><Brain size={13}/> 🔬 Mehrstufige Analyse starten</>}
</button>
{pipelineError && <div style={{fontSize:12,color:'#D85A30',marginBottom:8}}>{pipelineError}</div>}
{latestInsight ? ( {latestInsight ? (
<> <>
<div style={{fontSize:11,color:'var(--text3)',marginBottom:6}}> <div style={{fontSize:11,color:'var(--text3)',marginBottom:6}}>
Letzte Analyse: {dayjs(latestInsight.created).format('DD. MMMM YYYY, HH:mm')} Letzte Analyse: {dayjs(latestInsight.created).format('DD. MMMM YYYY, HH:mm')}
</div> </div>
<div style={{maxHeight: showInsight?'none':120, overflow:'hidden', position:'relative'}}> <div style={{maxHeight: showInsight?'none':120, overflow:'hidden', position:'relative'}}>
<Markdown text={latestInsight.content}/> <Markdown text={latestInsight.content}/>
{!showInsight && ( {!showInsight && (
<div style={{position:'absolute',bottom:0,left:0,right:0,height:40, <div style={{position:'absolute',bottom:0,left:0,right:0,height:40,
background:'linear-gradient(transparent,var(--surface))'}}/> background:'linear-gradient(transparent,var(--surface))'}}/>
)} )}
</div> </div>
<button style={{background:'none',border:'none',cursor:'pointer', <button type="button" style={{background:'none',border:'none',cursor:'pointer',
fontSize:12,color:'var(--accent)',marginTop:6,padding:0}} fontSize:12,color:'var(--accent)',marginTop:6,padding:0}}
onClick={()=>setShowInsight(s=>!s)}> onClick={()=>setShowInsight(s=>!s)}>
{showInsight?'▲ Weniger anzeigen':'▼ Vollständig anzeigen'} {showInsight?'▲ Weniger anzeigen':'▼ Vollständig anzeigen'}
</button> </button>
</> </>
) : ( ) : (
<div style={{fontSize:13,color:'var(--text3)',padding:'8px 0'}}> <div style={{fontSize:13,color:'var(--text3)',padding:'8px 0'}}>
Noch keine KI-Auswertung vorhanden. Noch keine KI-Auswertung vorhanden.
<button className="btn btn-primary" style={{marginTop:8,display:'block',fontSize:12}} <button type="button" className="btn btn-primary" style={{marginTop:8,display:'block',fontSize:12}}
onClick={()=>nav('/analysis')}> onClick={()=>nav('/analysis')}>
Erste Analyse erstellen Erste Analyse erstellen
</button> </button>
</div>
)}
</div> </div>
)} </DashboardTile>
</div> </DashboardSection>
</>} </>}
</div> </div>
) )

View File

@ -0,0 +1,26 @@
/**
* Gemeinsames Raster für Dashboard-Kacheln (Mobile 2 / Desktop 4 Spalten).
* Optional Mobile 4: mobile auf 4 setzen + Klasse dashboard-tile-grid--mobile-4col.
*/
export const DASHBOARD_TILE_GRID_COLS = { mobile: 2, desktop: 4 }
/** @param {number} span @param {number} maxCols */
export function clampTileSpan(span, maxCols) {
const n = Number(span)
if (!Number.isFinite(n)) return 1
return Math.min(maxCols, Math.max(1, Math.round(n)))
}
/** @param {number} [mobileCols] 2 oder 4 */
export function dashboardTileGridClassName(mobileCols = DASHBOARD_TILE_GRID_COLS.mobile) {
let c = 'dashboard-tile-grid'
if (mobileCols === 4) c += ' dashboard-tile-grid--mobile-4col'
return c
}
/** KPI-Raster: dieselben Regeln wie `dashboard-tile-grid`, plus Legacy-Klasse `dashboard-stat-grid`. */
export function dashboardStatGridClassName(mobileCols = DASHBOARD_TILE_GRID_COLS.mobile) {
let c = 'dashboard-stat-grid dashboard-tile-grid'
if (mobileCols === 4) c += ' dashboard-stat-grid--mobile-4col dashboard-tile-grid--mobile-4col'
return c
}