131 lines
7.4 KiB
TypeScript
131 lines
7.4 KiB
TypeScript
import type { GpoBackupStatusRecord, ProjectAssessmentHistoryResponse } from '../../api/assessor-api-contract';
|
||
import type { AssessmentProject, ImportFileRecord } from '../../domain/canonical-model';
|
||
import { groupProjectImportFiles } from './datahub-import-groups';
|
||
import { summarizeClientProcessingReadiness } from './datahub-processing-readiness';
|
||
import type { DatahubTimeslotResultMetrics } from './datahub-scan-analysis';
|
||
import { DonutMetricCard } from '../../components/DonutMetricCard/DonutMetricCard';
|
||
import type { TimeslotOpenReadiness } from './datahub-open-readiness';
|
||
import { PanelLoadingState } from '../../components/PanelLoadingState/PanelLoadingState';
|
||
|
||
export interface DatahubTimeslotSelection {
|
||
projectId: string;
|
||
slots: Array<{ id: string; runIds: string[]; assessmentIds: string[]; effectiveAt: string }>;
|
||
}
|
||
|
||
interface DatahubHistoryOverviewProps {
|
||
projects: AssessmentProject[];
|
||
projectData: Record<string, ImportFileRecord[]>;
|
||
projectHistory: Record<string, ProjectAssessmentHistoryResponse>;
|
||
projectGpoData: Record<string, GpoBackupStatusRecord[]>;
|
||
projectOpenReadiness?: Record<string, Record<string, TimeslotOpenReadiness>>;
|
||
selection: DatahubTimeslotSelection | null;
|
||
timeslotMetrics: DatahubTimeslotResultMetrics | null;
|
||
loading: boolean;
|
||
comparisonBusy: boolean;
|
||
comparisonError: string | null;
|
||
onCreate(): void;
|
||
onShow(project: AssessmentProject): void;
|
||
onOpen(project: AssessmentProject, scanRunIds: string[], assessmentIds?: string[]): void;
|
||
onSelectProject(projectId: string): void;
|
||
}
|
||
|
||
function formatDate(value: string): string {
|
||
const date = new Date(value);
|
||
return Number.isNaN(date.getTime()) ? '—' : date.toLocaleDateString('de-DE');
|
||
}
|
||
|
||
function resultPercentage(value: number, total: number): number {
|
||
return total > 0 ? Math.min(100, Math.max(0, (value / total) * 100)) : 0;
|
||
}
|
||
|
||
export function DatahubHistoryOverview({
|
||
projects,
|
||
projectData,
|
||
projectHistory,
|
||
projectGpoData,
|
||
projectOpenReadiness = {},
|
||
selection,
|
||
timeslotMetrics,
|
||
loading,
|
||
comparisonBusy,
|
||
comparisonError,
|
||
onCreate,
|
||
onShow,
|
||
onOpen,
|
||
onSelectProject
|
||
}: DatahubHistoryOverviewProps) {
|
||
const resultMetrics = timeslotMetrics ?? { all: null, deviation: null, passed: null, notApplicable: null, notRun: null, error: null };
|
||
const pendingLabel = loading || comparisonBusy ? 'Loading…'
|
||
: comparisonError ? 'Unavailable' : selection ? 'No results' : 'Select client';
|
||
|
||
return <>
|
||
<section className="datahub-rack-module datahub-status-module" aria-label="Selected client result status" aria-busy={loading || comparisonBusy}>
|
||
<div className="datahub-reference-controls donut-metric-rack">
|
||
<DonutMetricCard className="datahub-reference-knob" label="Results All" value={resultMetrics.all} percentage={resultMetrics.all !== null ? 100 : 0} tone="all" pendingLabel={pendingLabel}/>
|
||
<DonutMetricCard className="datahub-reference-knob" label="Deviation" value={resultMetrics.deviation} percentage={resultPercentage(resultMetrics.deviation ?? 0, resultMetrics.all ?? 0)} tone="deviation" pendingLabel={pendingLabel}/>
|
||
<DonutMetricCard className="datahub-reference-knob" label="Passed" value={resultMetrics.passed} percentage={resultPercentage(resultMetrics.passed ?? 0, resultMetrics.all ?? 0)} tone="passed" pendingLabel={pendingLabel}/>
|
||
<DonutMetricCard className="datahub-reference-knob" label="Not Applicable" value={resultMetrics.notApplicable} percentage={resultPercentage(resultMetrics.notApplicable ?? 0, resultMetrics.all ?? 0)} tone="not-applicable" pendingLabel={pendingLabel}/>
|
||
<DonutMetricCard className="datahub-reference-knob" label="Not Run" value={resultMetrics.notRun} percentage={resultPercentage(resultMetrics.notRun ?? 0, resultMetrics.all ?? 0)} tone="not-run" pendingLabel={pendingLabel}/>
|
||
<DonutMetricCard className="datahub-reference-knob" label="Error" value={resultMetrics.error} percentage={resultPercentage(resultMetrics.error ?? 0, resultMetrics.all ?? 0)} tone="error" pendingLabel={pendingLabel}/>
|
||
</div>
|
||
{comparisonError && <em className="datahub-comparison-error" role="alert">{comparisonError}</em>}
|
||
</section>
|
||
|
||
<section className="datahub-rack-module datahub-tenants-module" aria-label="Mandanten">
|
||
{loading ? <PanelLoadingState message="Loading clients …" compact/> : (
|
||
<div className="datahub-tenant-card-rack">
|
||
{projects.map(project => {
|
||
const history = projectHistory[project.id];
|
||
const groups = groupProjectImportFiles(
|
||
projectData[project.id] ?? [],
|
||
history?.assessments,
|
||
history?.scanRuns,
|
||
projectGpoData[project.id] ?? []
|
||
);
|
||
const latest = groups[0];
|
||
const latestOpenable = groups.find(group => projectOpenReadiness[project.id]?.[group.id]?.canOpen);
|
||
const activeProcessingRunIds = groups.flatMap(group => group.scanRunIds);
|
||
const processingSummary = summarizeClientProcessingReadiness(
|
||
projectData[project.id] ?? [],
|
||
activeProcessingRunIds,
|
||
projectGpoData[project.id] ?? []
|
||
);
|
||
const selected = selection?.projectId === project.id;
|
||
return <article className={`donut-metric-card donut-metric-card--all datahub-tenant-card${selected ? ' is-selected' : ''}`} key={project.id}>
|
||
{processingSummary.state !== 'no-data' && <span
|
||
className="datahub-tenant-processing-dot"
|
||
data-status={processingSummary.state}
|
||
role="img"
|
||
aria-label={processingSummary.label}
|
||
title={processingSummary.label}
|
||
/>}
|
||
<button
|
||
className="datahub-tenant-select"
|
||
type="button"
|
||
aria-label={`Mandant ${project.name} auswählen`}
|
||
aria-pressed={selected}
|
||
onClick={() => onSelectProject(project.id)}
|
||
>
|
||
<span className="datahub-tenant-name">{project.name}</span>
|
||
<span className="datahub-tenant-summary"><strong>{groups.length.toLocaleString('de-DE')}</strong><small>{groups.length === 1 ? 'Timeslot' : 'Timeslots'}</small></span>
|
||
<span className="datahub-tenant-latest">{latest ? `Latest ${formatDate(latest.fileDate)}` : 'No scan data yet'}</span>
|
||
</button>
|
||
{(selected || processingSummary.state === 'update' || processingSummary.state === 'review' || processingSummary.state === 'error') && <button
|
||
className="btn datahub-tenant-show"
|
||
data-status={processingSummary.state}
|
||
type="button"
|
||
title={['update', 'review', 'error'].includes(processingSummary.state) ? processingSummary.label : 'Manage client'}
|
||
onClick={() => onShow(project)}
|
||
>Manage</button>}
|
||
<button className="btn primary datahub-tenant-open" type="button" disabled={!latestOpenable} title={latestOpenable ? 'Open latest timeslot' : 'No scan data available'} onClick={() => latestOpenable && onOpen(project, latestOpenable.scanRunIds, latestOpenable.assessmentIds)}>Open</button>
|
||
</article>;
|
||
})}
|
||
<button className="donut-metric-card donut-metric-card--all datahub-tenant-card datahub-tenant-add" type="button" onClick={onCreate} aria-label="Mandant anlegen">
|
||
<span aria-hidden="true">+</span>
|
||
</button>
|
||
</div>
|
||
)}
|
||
</section>
|
||
</>;
|
||
}
|