137e14b3d1
- Kickoff: centered "21:00" above flags (no 'Uhr' suffix) - Countdown: always rendered as badge (was unstyled span for <60min) - DevPanel: added close button (✕) in panel header for reliable closing Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
250 lines
8.5 KiB
TypeScript
250 lines
8.5 KiB
TypeScript
import { useState } from 'react';
|
||
import { Match } from '../api/client';
|
||
import styles from './DevPanel.module.css';
|
||
|
||
const DEV_USERS = [
|
||
{ id: 1, name: 'Ronny M.', role: 'Editor' },
|
||
{ id: 2, name: 'Max M.', role: 'Viewer' },
|
||
{ id: 3, name: 'Anna S.', role: 'Viewer' },
|
||
];
|
||
|
||
const TIME_PRESETS = [
|
||
{ label: 'In 2 Std.', minutes: 120 },
|
||
{ label: 'In 10 Min.', minutes: 10 },
|
||
{ label: 'Jetzt +1 Min.', minutes: 1 },
|
||
{ label: 'Läuft (−30)', minutes: -30 },
|
||
{ label: 'Beendet (−120)', minutes: -120 },
|
||
];
|
||
|
||
const STATUS_PRESETS = [
|
||
{ label: 'TIMED', status: 'TIMED', scoreHome: null, scoreAway: null },
|
||
{ label: 'LIVE', status: 'IN_PLAY', scoreHome: 0, scoreAway: 0 },
|
||
{ label: 'Pause', status: 'PAUSED', scoreHome: 1, scoreAway: 0 },
|
||
{ label: '2:1 Fertig', status: 'FINISHED', scoreHome: 2, scoreAway: 1 },
|
||
{ label: '0:0 Fertig', status: 'FINISHED', scoreHome: 0, scoreAway: 0 },
|
||
];
|
||
|
||
interface Props {
|
||
currentUser: number;
|
||
onUserChange: (userId: number) => void;
|
||
matches: Match[];
|
||
onRefresh: () => void;
|
||
}
|
||
|
||
export default function DevPanel({ currentUser, onUserChange, matches, onRefresh }: Props) {
|
||
const [open, setOpen] = useState(false);
|
||
const [selectedMatch, setSelectedMatch] = useState<number | ''>('');
|
||
const [busy, setBusy] = useState(false);
|
||
const [log, setLog] = useState<string[]>([]);
|
||
|
||
function addLog(msg: string) {
|
||
setLog(prev => [`${new Date().toLocaleTimeString('de-DE')} ${msg}`, ...prev].slice(0, 8));
|
||
}
|
||
|
||
async function applyTime(minutes: number) {
|
||
if (!selectedMatch) return;
|
||
setBusy(true);
|
||
try {
|
||
await fetch(`/api/dev/match/${selectedMatch}/set-time`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ minutesFromNow: minutes }),
|
||
});
|
||
addLog(`✓ Spiel #${selectedMatch}: Zeit → ${minutes > 0 ? `+${minutes}` : minutes} Min.`);
|
||
onRefresh();
|
||
} catch (e) {
|
||
addLog(`✗ Fehler: ${(e as Error).message}`);
|
||
} finally {
|
||
setBusy(false);
|
||
}
|
||
}
|
||
|
||
async function applyStatus(status: string, scoreHome: number | null, scoreAway: number | null) {
|
||
if (!selectedMatch) return;
|
||
setBusy(true);
|
||
try {
|
||
await fetch(`/api/dev/match/${selectedMatch}/set-status`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ status, scoreHome, scoreAway }),
|
||
});
|
||
addLog(`✓ Spiel #${selectedMatch}: Status → ${status}`);
|
||
onRefresh();
|
||
} catch (e) {
|
||
addLog(`✗ Fehler: ${(e as Error).message}`);
|
||
} finally {
|
||
setBusy(false);
|
||
}
|
||
}
|
||
|
||
async function resetTips() {
|
||
setBusy(true);
|
||
try {
|
||
await fetch('/api/dev/reset-tips', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ userId: `dev-user-00${currentUser}` }),
|
||
});
|
||
addLog(`✓ Tipps von User ${currentUser} gelöscht`);
|
||
onRefresh();
|
||
} catch (e) {
|
||
addLog(`✗ Fehler: ${(e as Error).message}`);
|
||
} finally {
|
||
setBusy(false);
|
||
}
|
||
}
|
||
|
||
async function resetMatch(all: boolean) {
|
||
setBusy(true);
|
||
try {
|
||
const body = all ? {} : { matchId: selectedMatch };
|
||
const res = await fetch('/api/dev/reset-match', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(body),
|
||
});
|
||
const data = await res.json() as { count?: number; matchId?: number };
|
||
if (all) {
|
||
addLog(`✓ ${data.count ?? 0} Spiele zurückgesetzt (TIMED)`);
|
||
} else {
|
||
addLog(`✓ Spiel #${selectedMatch} zurückgesetzt (TIMED)`);
|
||
}
|
||
onRefresh();
|
||
} catch (e) {
|
||
addLog(`✗ Fehler: ${(e as Error).message}`);
|
||
} finally {
|
||
setBusy(false);
|
||
}
|
||
}
|
||
|
||
// Nur erste 20 Spiele zur Auswahl anbieten
|
||
const selectableMatches = matches.slice(0, 20);
|
||
|
||
return (
|
||
<div className={styles.wrap}>
|
||
{/* Toggle Button */}
|
||
<button className={styles.toggleBtn} onClick={() => setOpen(o => !o)}>
|
||
{open ? '✕' : '🧪'} {!open && <span className={styles.toggleLabel}>Dev</span>}
|
||
</button>
|
||
|
||
{open && (
|
||
<div className={styles.panel}>
|
||
<div className={styles.panelHeader}>
|
||
<span className={styles.panelTitle}>🧪 Simulations-Modus</span>
|
||
<button className={styles.closeBtn} onClick={() => setOpen(false)}>✕</button>
|
||
</div>
|
||
|
||
{/* User Switcher */}
|
||
<section className={styles.section}>
|
||
<div className={styles.sectionLabel}>Aktiver User</div>
|
||
<div className={styles.userButtons}>
|
||
{DEV_USERS.map(u => (
|
||
<button
|
||
key={u.id}
|
||
className={`${styles.userBtn} ${currentUser === u.id ? styles.userBtnActive : ''}`}
|
||
onClick={() => {
|
||
onUserChange(u.id);
|
||
addLog(`→ Wechsel zu User ${u.id}: ${u.name}`);
|
||
}}
|
||
>
|
||
<span className={styles.userInitial}>{u.name.charAt(0)}</span>
|
||
<span className={styles.userName}>{u.name}</span>
|
||
<span className={styles.userRole}>{u.role}</span>
|
||
</button>
|
||
))}
|
||
</div>
|
||
</section>
|
||
|
||
{/* Match Selector */}
|
||
<section className={styles.section}>
|
||
<div className={styles.sectionLabel}>Spiel auswählen</div>
|
||
<select
|
||
className={styles.select}
|
||
value={selectedMatch}
|
||
onChange={e => setSelectedMatch(e.target.value ? parseInt(e.target.value) : '')}
|
||
>
|
||
<option value="">— Spiel wählen —</option>
|
||
{selectableMatches.map(m => (
|
||
<option key={m.id} value={m.id}>
|
||
#{m.id} {m.homeTeam.shortName} vs {m.awayTeam.shortName}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</section>
|
||
|
||
{/* Zeit-Manipulation */}
|
||
<section className={`${styles.section} ${!selectedMatch ? styles.sectionDisabled : ''}`}>
|
||
<div className={styles.sectionLabel}>Anstoßzeit setzen</div>
|
||
<div className={styles.presetGrid}>
|
||
{TIME_PRESETS.map(p => (
|
||
<button
|
||
key={p.label}
|
||
className={styles.presetBtn}
|
||
onClick={() => applyTime(p.minutes)}
|
||
disabled={!selectedMatch || busy}
|
||
>
|
||
{p.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</section>
|
||
|
||
{/* Status-Manipulation */}
|
||
<section className={`${styles.section} ${!selectedMatch ? styles.sectionDisabled : ''}`}>
|
||
<div className={styles.sectionLabel}>Status setzen</div>
|
||
<div className={styles.presetGrid}>
|
||
{STATUS_PRESETS.map(p => (
|
||
<button
|
||
key={p.label}
|
||
className={`${styles.presetBtn} ${p.status === 'FINISHED' ? styles.presetBtnDanger : p.status === 'IN_PLAY' ? styles.presetBtnLive : ''}`}
|
||
onClick={() => applyStatus(p.status, p.scoreHome, p.scoreAway)}
|
||
disabled={!selectedMatch || busy}
|
||
>
|
||
{p.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</section>
|
||
|
||
{/* Reset */}
|
||
<section className={styles.section}>
|
||
<div className={styles.sectionLabel}>Zurücksetzen</div>
|
||
<div className={styles.resetGrid}>
|
||
<button
|
||
className={styles.resetBtn}
|
||
onClick={() => resetMatch(false)}
|
||
disabled={!selectedMatch || busy}
|
||
title="Ausgewähltes Spiel auf TIMED zurücksetzen"
|
||
>
|
||
↺ Spiel zurücksetzen
|
||
</button>
|
||
<button
|
||
className={`${styles.resetBtn} ${styles.resetBtnAll}`}
|
||
onClick={() => resetMatch(true)}
|
||
disabled={busy}
|
||
title="Alle laufenden/beendeten Spiele zurücksetzen"
|
||
>
|
||
↺ Alle Spiele
|
||
</button>
|
||
<button
|
||
className={`${styles.resetBtn} ${styles.resetBtnTips}`}
|
||
onClick={resetTips}
|
||
disabled={busy}
|
||
>
|
||
🗑 Tipps löschen
|
||
</button>
|
||
</div>
|
||
</section>
|
||
|
||
{/* Log */}
|
||
{log.length > 0 && (
|
||
<div className={styles.log}>
|
||
{log.map((l, i) => <div key={i} className={styles.logLine}>{l}</div>)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|