import type { CommandBus } from '@application/handlers/CommandBus'; import type { QueryBus } from '@application/handlers/QueryBus'; import type { UIState } from '@presentation/state/UIState'; import type { DashboardResult } from '@application/queries/GetDashboard'; import type { Enclos } from '@domain/entities/Enclos'; import { MAX_DD } from '@domain/entities/Enclos'; import { GAUGE_DEFS } from '@domain/value-objects/GaugeType'; import { raceColor } from '@domain/value-objects/Race'; import { enclosGlobalState, enclosGaugeCurGl } from '@presentation/helpers/gauge-live'; import { esc, fmtClock } from '@presentation/helpers/format'; import { Toast } from './Toast'; import { ConfirmModal } from './ConfirmModal'; import { UndoManager } from '@presentation/services/UndoManager'; export class Dashboard { private el: HTMLElement | null = null; private lastRenderTime = 0; constructor( private commandBus: CommandBus, private queryBus: QueryBus, private uiState: UIState, ) {} render(container: HTMLElement): void { this.el = document.createElement('div'); this.el.className = 'dash-new'; container.appendChild(this.el); this.renderAll(); } private getData(): DashboardResult { return this.queryBus.execute({ type: 'get-dashboard' }); } private renderAll(): void { if (!this.el) return; const data = this.getData(); // ── KPI Section ────────────────────────────────────────────── const activeDD = data.enclosSummaries.reduce((s, e) => s + e.ddCount, 0); const racesObtained = Object.keys(data.raceBreakdown).length; const kpis = [ { label: 'Total Bébés', value: String(data.totalBabies) }, { label: 'Dragodindes Actives', value: String(activeDD) }, { label: 'Couples Formés', value: String(data.totalCouples) }, { label: 'Taux de Réussite', value: `${data.successRate}%` }, { label: 'Races Obtenues', value: String(racesObtained) }, ]; let kpiHtml = `
Statistiques Globales
`; for (const kpi of kpis) { kpiHtml += `

${esc(kpi.label)}

${esc(kpi.value)}
`; } kpiHtml += `
`; // ── Two-column section ─────────────────────────────────────── let enclosHtml = `
Aperçu — Tous les enclos
`; for (const summary of data.enclosSummaries) { const enc = this.queryBus.execute({ type: 'get-enclos-detail', enclosId: summary.id }); const gs = enc ? enclosGlobalState(enc) : null; const started = !!enc?.timer.startTime; const running = !!enc?.timer.running; const allDone = !!gs?.allDone && started; // Status let statusClass = 'idle'; let statusLabel = 'Inactif'; if (running) { statusClass = 'running'; statusLabel = 'Actif'; } else if (started) { statusClass = 'paused'; statusLabel = 'Pause'; } const cardClass = `dash-enc-card${running ? ' running' : ''}${allDone ? ' done-enc' : ''}`; // Gauge tags — détecte les jauges vides en cours de session const gaugeTags = summary.activeGauges.map(gid => { const def = GAUGE_DEFS[gid as keyof typeof GAUGE_DEFS]; if (!def) return ''; const cssVar = `var(${def.cssVar})`; const curGl = (enc && started) ? enclosGaugeCurGl(enc, gid as any) : (enc?.gaugeLevels[gid as keyof typeof enc.gaugeLevels] ?? 0); const isEmpty = curGl <= 0 && started; if (isEmpty) { return ` ⚠ ${esc(def.label)} `; } return ` ${def.icon} ${esc(def.label)} `; }).join(''); const gaugesRow = summary.activeGauges.length > 0 ? `
${gaugeTags}
` : `
Aucune jauge active
`; // Sous-label capacité max (uniquement quand l'enclos est plein) const capaciteLabel = summary.ddCount >= MAX_DD ? 'Capacit\u00e9 max' : ''; const cdText = started && gs ? (gs.allDone ? '✅' : (!isFinite(gs.globalMax) ? '∞' : fmtClock(gs.globalMax))) : '--:--:--'; const elText = started && gs ? fmtClock(gs.el) : '--:--:--'; // Button style: active = primary button if running and approaching end const btnClass = allDone ? 'dash-enc-btn btn-active' : 'dash-enc-btn'; enclosHtml += `
${esc(summary.name.toUpperCase())}
${esc(statusLabel)}
${summary.ddCount} DD
${capaciteLabel ? `
${capaciteLabel}
` : ''}
hourglass_top Restant : ${cdText}
schedule Écoulé : ${elText}
${gaugesRow}
`; } enclosHtml += `
`; // ── Race progression panel (right col) ─────────────────────── const raceEntries = Object.entries(data.raceBreakdown).sort((a, b) => b[1] - a[1]); const maxCount = raceEntries.length > 0 ? raceEntries[0][1] : 1; let raceHtml = `
Progression des races
`; if (raceEntries.length === 0) { raceHtml += `

Aucune race enregistrée

`; } else { for (const [race, count] of raceEntries) { const pct = maxCount > 0 ? (count / maxCount) * 100 : 0; const col = raceColor(race); raceHtml += `
${esc(race)} ${count}
`; } } raceHtml += `
`; // ── Assemble ────────────────────────────────────────────────── this.el.innerHTML = kpiHtml + `
` + `
${enclosHtml}
` + raceHtml + `
`; this.bindEvents(); } private bindEvents(): void { if (!this.el) return; // "Gérer" buttons — navigate with stopPropagation this.el.querySelectorAll('button[data-enc-id]').forEach(btn => { btn.addEventListener('click', (e) => { e.stopPropagation(); const id = Number(btn.dataset['encId']); if (id) this.uiState.setActiveView(id); }); }); // Card click (excluding button area) this.el.querySelectorAll('.dash-enc-card[id^="dash-enc-"]').forEach(card => { card.addEventListener('click', (e) => { if ((e.target as HTMLElement).closest('button')) return; const id = Number(card.id.replace('dash-enc-', '')); if (id) this.uiState.setActiveView(id); }); }); // Reset stats this.el.querySelector('#dash-reset-btn')?.addEventListener('click', async () => { const ok = await ConfirmModal.show('Réinitialiser les statistiques', 'Toutes les statistiques seront effacées. Continuer ?'); if (!ok) return; const hasSnap = await UndoManager.snapshotCurrent('Reset statistiques'); this.commandBus.execute({ type: 'reset-stats' }); Toast.show('success', 'Statistiques réinitialisées.', hasSnap ? { label: 'Annuler', callback: () => UndoManager.undo() } : undefined); this.renderAll(); }); } update(): void { if (!this.el) return; const now = Date.now(); if (now - this.lastRenderTime >= 1000) { this.lastRenderTime = now; this.renderAll(); } } destroy(): void { this.el?.remove(); this.el = null; } }