Migrate all repos into monorepo context folders
Bahn: aisupport, Analyse-O2C-C2S, awesome-bahn-mcp-servers, beam-mcp,
Confluence_Bot, db-planet-mcp-server, O2C-Harness, project-audit,
Projekt-KIQ-HP, teamlandkarte-mcp
Dhive: Jury-Voting
Privat: CV, NoteGraph (NOTE: NoteGraph needs complete redo after consolidation)
Shared: AI-Orchestrator, OrgMyLife, power_skills_and_more
Shared/references: symphony (read-only)
Bahn repos remain available as independent remotes - this monorepo
pulls them in via subtree, the originals are untouched.
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
interface JurorProgress {
|
||||
completed: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays juror voting progress (X/11 teams completed per juror).
|
||||
* Fetches from GET /api/progress. No individual scores shown.
|
||||
*/
|
||||
export default function ProgressMatrix() {
|
||||
const [progress, setProgress] = useState<Record<string, JurorProgress>>({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetchProgress();
|
||||
const interval = setInterval(fetchProgress, 5000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
function fetchProgress() {
|
||||
fetch('/api/progress')
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
setProgress(data);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(() => setLoading(false));
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <div className="progress-loading">Lade Fortschritt...</div>;
|
||||
}
|
||||
|
||||
const entries = Object.entries(progress);
|
||||
const allComplete = entries.length > 0 && entries.every(([, p]) => p.completed === p.total);
|
||||
|
||||
return (
|
||||
<div className="progress-matrix">
|
||||
<h3 className="progress-title">Fortschritt</h3>
|
||||
|
||||
{allComplete && (
|
||||
<div className="progress-complete-badge">
|
||||
✓ Alle Bewertungen abgeschlossen
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="progress-list">
|
||||
{entries.map(([jurorId, { completed, total }]) => {
|
||||
const percent = total > 0 ? (completed / total) * 100 : 0;
|
||||
const isComplete = completed === total;
|
||||
|
||||
return (
|
||||
<div key={jurorId} className="progress-item">
|
||||
<div className="progress-item-header">
|
||||
<span className="progress-juror-name">
|
||||
{jurorId.charAt(0).toUpperCase() + jurorId.slice(1)}
|
||||
</span>
|
||||
<span className={`progress-count ${isComplete ? 'complete' : ''}`}>
|
||||
{completed}/{total}
|
||||
</span>
|
||||
</div>
|
||||
<div className="progress-bar-track">
|
||||
<div
|
||||
className={`progress-bar-fill ${isComplete ? 'complete' : ''}`}
|
||||
style={{ width: `${percent}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { TeamResult } from '../../shared/types';
|
||||
|
||||
interface RankingTableProps {
|
||||
results: TeamResult[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Beamer-optimized ranking table with animated transitions.
|
||||
* Displays ranked teams with scores, highlights top 3.
|
||||
*/
|
||||
export default function RankingTable({ results }: RankingTableProps) {
|
||||
if (results.length === 0) {
|
||||
return (
|
||||
<div className="ranking-empty">
|
||||
<p>Noch keine Bewertungen eingegangen.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const getRankStyle = (rank: number): string => {
|
||||
if (rank === 1) return 'rank-gold';
|
||||
if (rank === 2) return 'rank-silver';
|
||||
if (rank === 3) return 'rank-bronze';
|
||||
return '';
|
||||
};
|
||||
|
||||
const getRankLabel = (rank: number): string => {
|
||||
if (rank === 1) return '🥇';
|
||||
if (rank === 2) return '🥈';
|
||||
if (rank === 3) return '🥉';
|
||||
return `${rank}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="ranking-table">
|
||||
<div className="ranking-header">
|
||||
<span className="ranking-col-rank">Platz</span>
|
||||
<span className="ranking-col-team">Team</span>
|
||||
<span className="ranking-col-total">Gesamt</span>
|
||||
<span className="ranking-col-avg">Ø</span>
|
||||
</div>
|
||||
<div className="ranking-rows">
|
||||
{results.map((result, index) => (
|
||||
<div
|
||||
key={result.teamId}
|
||||
className={`ranking-row glass-panel ${getRankStyle(result.rank)}`}
|
||||
style={{
|
||||
order: index,
|
||||
transition: 'transform 0.6s ease, opacity 0.4s ease',
|
||||
}}
|
||||
>
|
||||
<span className="ranking-col-rank rank-number">
|
||||
{getRankLabel(result.rank)}
|
||||
</span>
|
||||
<span className="ranking-col-team team-name">
|
||||
{result.teamName}
|
||||
</span>
|
||||
<span className="ranking-col-total total-score">
|
||||
{result.total}
|
||||
</span>
|
||||
<span className="ranking-col-avg avg-score">
|
||||
{result.average.toFixed(1)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { useEffect } from 'react';
|
||||
import { TeamResult } from '../../shared/types';
|
||||
|
||||
interface TeamDetailModalProps {
|
||||
team: TeamResult;
|
||||
scaleMax: number;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modal overlay showing per-category bar chart for a selected team.
|
||||
*/
|
||||
export default function TeamDetailModal({ team, scaleMax, onClose }: TeamDetailModalProps) {
|
||||
// Close on Escape key
|
||||
useEffect(() => {
|
||||
const handleKey = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') onClose();
|
||||
};
|
||||
document.addEventListener('keydown', handleKey);
|
||||
return () => document.removeEventListener('keydown', handleKey);
|
||||
}, [onClose]);
|
||||
|
||||
const stats = team.criteriaStats ?? [];
|
||||
|
||||
return (
|
||||
<div className="modal-overlay" onClick={onClose} role="dialog" aria-modal="true" aria-label={`Ergebnisse für ${team.teamName}`}>
|
||||
<div className="modal-content glass-panel" onClick={(e) => e.stopPropagation()}>
|
||||
<header className="modal-header">
|
||||
<h2 className="modal-title">{team.teamName}</h2>
|
||||
<button className="modal-close" onClick={onClose} aria-label="Schließen">✕</button>
|
||||
</header>
|
||||
|
||||
<div className="modal-summary">
|
||||
<span className="modal-stat">
|
||||
<span className="modal-stat-label">Gesamt</span>
|
||||
<span className="modal-stat-value">{team.total}</span>
|
||||
</span>
|
||||
<span className="modal-stat">
|
||||
<span className="modal-stat-label">Ø</span>
|
||||
<span className="modal-stat-value">{team.average.toFixed(1)}</span>
|
||||
</span>
|
||||
<span className="modal-stat">
|
||||
<span className="modal-stat-label">σ</span>
|
||||
<span className="modal-stat-value">±{team.stddev.toFixed(1)}</span>
|
||||
</span>
|
||||
<span className="modal-stat">
|
||||
<span className="modal-stat-label">Juroren</span>
|
||||
<span className="modal-stat-value">{team.jurorCount}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{stats.length > 0 ? (
|
||||
<div className="modal-chart">
|
||||
{stats.map((stat) => (
|
||||
<div key={stat.criterionId} className="chart-row">
|
||||
<span className="chart-label">{stat.criterionName}</span>
|
||||
<div className="chart-bar-container">
|
||||
<div
|
||||
className="chart-bar"
|
||||
style={{ width: `${(stat.mean / scaleMax) * 100}%` }}
|
||||
>
|
||||
<span className="chart-bar-value">{stat.mean.toFixed(1)}</span>
|
||||
</div>
|
||||
{stat.stddev > 0 && (
|
||||
<span className="chart-stddev">±{stat.stddev.toFixed(1)}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="modal-empty">Noch keine Bewertungen für dieses Team.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user