ui-nyla/src/pages/views/redmine/RedmineStatsView.tsx
2026-04-21 18:14:26 +02:00

93 lines
3.6 KiB
TypeScript

/**
* Redmine Statistics View (Phase 2 placeholder).
*
* Will render a ``FormGeneratorReport`` driven by ``getRedmineStatsApi``
* with a ``PeriodPicker`` and tracker-filter. For now: shows the raw
* KPIs so the wiring can be verified against the local mirror.
*/
import React, { useCallback, useEffect, useState } from 'react';
import { useApiRequest } from '../../../hooks/useApi';
import { useInstanceId } from '../../../hooks/useCurrentInstance';
import { useLanguage } from '../../../providers/language/LanguageContext';
import { RedmineStats, getRedmineStatsApi } from '../../../api/redmineApi';
import styles from './RedmineViews.module.css';
export const RedmineStatsView: React.FC = () => {
const { t } = useLanguage();
const { request } = useApiRequest();
const instanceId = useInstanceId();
const [stats, setStats] = useState<RedmineStats | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const _load = useCallback(async () => {
if (!instanceId) return;
setLoading(true);
setError(null);
try {
const result = await getRedmineStatsApi(request, instanceId, { bucket: 'week' });
setStats(result);
} catch (e: any) {
setError(e?.message || t('Fehler beim Laden'));
} finally {
setLoading(false);
}
}, [request, instanceId, t]);
useEffect(() => { _load(); }, [_load]);
if (loading) return <div className={styles.loading}>{t('Statistik wird geladen ...')}</div>;
return (
<div className={styles.page}>
<h2 className={styles.heading}>{t('Redmine -- Statistik')}</h2>
<p className={styles.subheading}>
{t('Aggregiert aus dem lokalen Mirror. PeriodPicker und FormGeneratorReport folgen im naechsten Schritt.')}
</p>
{error && <div className={styles.alertErr}>{error}</div>}
{stats && (
<div className={styles.section}>
<h3 className={styles.sectionTitle}>{t('KPIs (gesamter Mirror)')}</h3>
<div className={styles.kvGrid}>
<div className={styles.kvLabel}>{t('Tickets gesamt')}:</div>
<div className={styles.kvValue}>{stats.kpis.total}</div>
<div className={styles.kvLabel}>{t('Offen')}:</div>
<div className={styles.kvValue}>{stats.kpis.open}</div>
<div className={styles.kvLabel}>{t('Geschlossen')}:</div>
<div className={styles.kvValue}>{stats.kpis.closed}</div>
<div className={styles.kvLabel}>{t('Im Zeitraum erstellt')}:</div>
<div className={styles.kvValue}>{stats.kpis.createdInPeriod}</div>
<div className={styles.kvLabel}>{t('Im Zeitraum geschlossen')}:</div>
<div className={styles.kvValue}>{stats.kpis.closedInPeriod}</div>
<div className={styles.kvLabel}>{t('Orphans (ohne Userstory)')}:</div>
<div className={styles.kvValue}>{stats.kpis.orphans}</div>
</div>
</div>
)}
{stats && stats.statusByTracker.length > 0 && (
<div className={styles.section}>
<h3 className={styles.sectionTitle}>{t('Status pro Tracker')}</h3>
<ul style={{ margin: 0, paddingLeft: '1.25rem', fontSize: '0.85rem' }}>
{stats.statusByTracker.map(entry => (
<li key={`${entry.trackerId}-${entry.trackerName}`}>
<strong>{entry.trackerName}</strong> ({entry.total}):{' '}
{Object.entries(entry.countsByStatus)
.map(([s, n]) => `${s}: ${n}`)
.join(', ')}
</li>
))}
</ul>
</div>
)}
</div>
);
};
export default RedmineStatsView;