"use client"; import React, { FormEvent, useCallback, useEffect, useState, } from "react"; import type { ProjectHistoryStateDto, ProjectRevisionPageDto, ProjectRevisionSummaryDto, ProjectSnapshotMetadataDto, } from "../types"; import { createNamedProjectSnapshot, getProjectHistory, listProjectRevisions, listProjectSnapshots, redoProjectCommand, restoreProjectSnapshot, undoProjectCommand, } from "../utils/api"; import { getProjectRevisionDescription, getProjectRevisionSourceLabel, getProjectSnapshotKindLabel, mergeProjectRevisionPages, } from "../utils/project-version-history"; interface ProjectVersionHistoryProps { projectId: string; currentRevision: number; onProjectStateChange: () => void; } const revisionPageSize = 10; export function ProjectVersionHistory({ projectId, currentRevision, onProjectStateChange, }: ProjectVersionHistoryProps) { const [history, setHistory] = useState(null); const [snapshots, setSnapshots] = useState( [] ); const [revisions, setRevisions] = useState( [] ); const [nextBeforeRevision, setNextBeforeRevision] = useState< number | null >(null); const [snapshotName, setSnapshotName] = useState(""); const [snapshotDescription, setSnapshotDescription] = useState(""); const [restoreCandidate, setRestoreCandidate] = useState(null); const [isOpen, setIsOpen] = useState(false); const [isLoading, setIsLoading] = useState(true); const [isHistoryLoading, setIsHistoryLoading] = useState(true); const [isBusy, setIsBusy] = useState(false); const [error, setError] = useState(null); const loadHistory = useCallback(async () => { setIsHistoryLoading(true); try { setHistory(await getProjectHistory(projectId)); } catch (loadError) { setError( getVersionHistoryError( loadError, "Rückgängig-Status konnte nicht geladen werden." ) ); } finally { setIsHistoryLoading(false); } }, [projectId]); const loadOverview = useCallback(async () => { setIsLoading(true); setError(null); try { const [loadedSnapshots, revisionPage] = await Promise.all([ listProjectSnapshots(projectId), listProjectRevisions(projectId, { limit: revisionPageSize }), ]); setSnapshots(loadedSnapshots); setRevisions(revisionPage.revisions); setNextBeforeRevision(revisionPage.nextBeforeRevision); } catch (loadError) { setError( getVersionHistoryError( loadError, "Versionshistorie konnte nicht geladen werden." ) ); } finally { setIsLoading(false); } }, [projectId]); useEffect(() => { void loadHistory(); }, [currentRevision, loadHistory]); useEffect(() => { if (isOpen) { void loadOverview(); } }, [loadOverview, currentRevision, isOpen]); async function handleCreateSnapshot(event: FormEvent) { event.preventDefault(); const name = snapshotName.trim(); if (!name) { return; } setIsBusy(true); setError(null); try { const created = await createNamedProjectSnapshot( projectId, currentRevision, name, snapshotDescription.trim() || undefined ); setSnapshots((current) => [created, ...current]); setSnapshotName(""); setSnapshotDescription(""); } catch (createError) { setError( getVersionHistoryError( createError, "Projektstand konnte nicht gespeichert werden." ) ); } finally { setIsBusy(false); } } async function handleLoadOlderRevisions() { if (nextBeforeRevision === null) { return; } setIsBusy(true); setError(null); try { const page: ProjectRevisionPageDto = await listProjectRevisions( projectId, { limit: revisionPageSize, beforeRevision: nextBeforeRevision, } ); setRevisions((current) => mergeProjectRevisionPages(current, page.revisions) ); setNextBeforeRevision(page.nextBeforeRevision); } catch (loadError) { setError( getVersionHistoryError( loadError, "Ältere Revisionen konnten nicht geladen werden." ) ); } finally { setIsBusy(false); } } async function handleRestoreSnapshot() { if (!restoreCandidate) { return; } setIsBusy(true); setError(null); try { await restoreProjectSnapshot( projectId, restoreCandidate.id, currentRevision ); setRestoreCandidate(null); setIsBusy(false); onProjectStateChange(); } catch (restoreError) { setError( getVersionHistoryError( restoreError, "Projektstand konnte nicht wiederhergestellt werden." ) ); setRestoreCandidate(null); setIsBusy(false); } } async function handleHistoryCommand(direction: "undo" | "redo") { setIsBusy(true); setError(null); try { const result = direction === "undo" ? await undoProjectCommand(projectId, currentRevision) : await redoProjectCommand(projectId, currentRevision); setHistory(result.history); onProjectStateChange(); } catch (historyError) { setError( getVersionHistoryError( historyError, direction === "undo" ? "Die letzte Änderung konnte nicht rückgängig gemacht werden." : "Die letzte Änderung konnte nicht wiederholt werden." ) ); } finally { setIsBusy(false); } } const historyIsCurrent = history?.currentRevision === currentRevision; return (
Versionen und Sicherungspunkte
Aktuelle Revision {currentRevision}
{error ? (
{error}
) : null} {isOpen ? ( <>

Neuen Sicherungspunkt anlegen

Ein Sicherungspunkt speichert den vollständigen Projektstand, ohne die Revision oder Rückgängig-Historie zu verändern.

setSnapshotName(event.target.value)} />
setSnapshotDescription(event.target.value) } />

Gespeicherte Sicherungspunkte

{isLoading ? (

Sicherungspunkte werden geladen …

) : snapshots.length ? (
{snapshots.map((snapshot) => ( ))}
Name Projektstand Erstellt Aktion
{snapshot.name} {getProjectSnapshotKindLabel(snapshot.kind)} {snapshot.description ? (
{snapshot.description}
) : null}
Revision {snapshot.sourceRevision} {formatDateTime(snapshot.createdAtIso)}
) : (

Noch keine Sicherungspunkte vorhanden.

)} {restoreCandidate ? (

„{restoreCandidate.name}“ wirklich wiederherstellen?

Verteiler, Stromkreise, Projektgeräte, Räume und Projekteinstellungen werden auf Revision{" "} {restoreCandidate.sourceRevision} zurückgesetzt. Der aktuelle Stand bleibt als neue rückgängig machbare Revision erhalten.

) : null}

Letzte Änderungen

{isLoading ? (

Historie wird geladen …

) : revisions.length ? ( <>
{revisions.map((revision) => ( ))}
Revision Zeitpunkt Art Änderung
{revision.revisionNumber} {formatDateTime(revision.createdAtIso)} {getProjectRevisionSourceLabel(revision.source)} {getProjectRevisionDescription(revision)}
{nextBeforeRevision !== null ? ( ) : null} ) : (

Für dieses Projekt wurden noch keine versionierten Änderungen gespeichert.

)}
) : null}
); } function formatDateTime(value: string) { const date = new Date(value); if (Number.isNaN(date.getTime())) { return value; } return new Intl.DateTimeFormat("de-DE", { dateStyle: "medium", timeStyle: "short", }).format(date); } function getVersionHistoryError(error: unknown, fallback: string) { const details = error instanceof Error ? error.message : ""; if (details.includes("PROJECT_SNAPSHOT_NAME_CONFLICT")) { return "Ein Sicherungspunkt mit diesem Namen ist bereits vorhanden."; } if ( details.includes("PROJECT_REVISION_CONFLICT") || details.includes("PROJECT_STATE_CONFLICT") ) { return "Das Projekt wurde inzwischen geändert. Bitte lade die Seite neu und versuche es erneut."; } return fallback; }