93 lines
3.6 KiB
TypeScript
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;
|