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

135 lines
8.1 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, withReferenceReadiness, type DatahubReferenceReadiness } from './datahub-processing-readiness';
import type { DatahubTimeslotResultMetrics } from './datahub-scan-analysis';
import { DonutMetricCard } from '../../components/DonutMetricCard/DonutMetricCard';
import { assessedResultPercentages } from '../../components/DonutMetricCard/assessed-result-percentages';
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>>;
projectReferenceReadiness?: Record<string, DatahubReferenceReadiness>;
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 = {},
projectReferenceReadiness = {},
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 assessmentPercentages = assessedResultPercentages(resultMetrics.passed ?? 0, resultMetrics.deviation ?? 0);
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} percentageBasisLabel="Of all records" tone="all" pendingLabel={pendingLabel}/>
<DonutMetricCard className="datahub-reference-knob" label="Deviation" value={resultMetrics.deviation} percentage={assessmentPercentages.deviation} percentageBasisLabel="Of assessed checks" tone="deviation" pendingLabel={pendingLabel}/>
<DonutMetricCard className="datahub-reference-knob" label="Passed" value={resultMetrics.passed} percentage={assessmentPercentages.passed} percentageBasisLabel="Of assessed checks" tone="passed" pendingLabel={pendingLabel}/>
<DonutMetricCard className="datahub-reference-knob" label="Not Applicable" value={resultMetrics.notApplicable} percentage={resultPercentage(resultMetrics.notApplicable ?? 0, resultMetrics.all ?? 0)} percentageBasisLabel="Of all records" tone="not-applicable" pendingLabel={pendingLabel}/>
<DonutMetricCard className="datahub-reference-knob" label="Not Run" value={resultMetrics.notRun} percentage={resultPercentage(resultMetrics.notRun ?? 0, resultMetrics.all ?? 0)} percentageBasisLabel="Of all records" tone="not-run" pendingLabel={pendingLabel}/>
<DonutMetricCard className="datahub-reference-knob" label="Error" value={resultMetrics.error} percentage={resultPercentage(resultMetrics.error ?? 0, resultMetrics.all ?? 0)} percentageBasisLabel="Of all records" 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 = withReferenceReadiness(summarizeClientProcessingReadiness(
projectData[project.id] ?? [],
activeProcessingRunIds,
projectGpoData[project.id] ?? []
), projectReferenceReadiness[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 === 'reference-update' || processingSummary.state === 'review' || processingSummary.state === 'error') && <button
className="btn datahub-tenant-show"
data-status={processingSummary.state}
type="button"
title={['update', 'reference-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>
</>;
}