Assessor_2.0/src/features/project-import/DatahubHistoryOverview.tsx

131 lines
7.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
</>;
}