import React, { useState, useEffect } from 'react' import type { Process, Server } from '../api/baserow' const SERVER_ICON = ( ) type ViewMode = '2H' | '6H' | 'Arbeitszeit' | 'Nachts' | 'Tag' | 'Woche' | 'Monat' function getViewBounds(mode: ViewMode, referenceDate: Date): { start: Date, end: Date, spanHours: number } { const d = new Date(referenceDate) let start = new Date(d) let end = new Date(d) let spanHours = 0 if (mode === '2H') { start = new Date(d.getTime() - 1 * 3600_000) end = new Date(d.getTime() + 1 * 3600_000) spanHours = 2 } else if (mode === '6H') { start = new Date(d.getTime() - 3 * 3600_000) end = new Date(d.getTime() + 3 * 3600_000) spanHours = 6 } else if (mode === 'Arbeitszeit') { start.setHours(6, 0, 0, 0) end.setHours(18, 0, 0, 0) spanHours = 12 } else if (mode === 'Nachts') { if (d.getHours() < 6) d.setDate(d.getDate() - 1) start = new Date(d) start.setHours(18, 0, 0, 0) end = new Date(start.getTime() + 12 * 3600_000) spanHours = 12 } else if (mode === 'Tag') { start.setHours(0, 0, 0, 0) end = new Date(start.getTime() + 24 * 3600_000) spanHours = 24 } else if (mode === 'Woche') { const day = start.getDay() const diff = start.getDate() - day + (day === 0 ? -6 : 1) start.setDate(diff) start.setHours(0, 0, 0, 0) end = new Date(start.getTime() + 7 * 24 * 3600_000) spanHours = 168 } else if (mode === 'Monat') { start.setDate(1) start.setHours(0, 0, 0, 0) end = new Date(start.getTime()) end.setMonth(end.getMonth() + 1) spanHours = (end.getTime() - start.getTime()) / 3600_000 } return { start, end, spanHours } } function parseTime(tStr: string | null | undefined): [number, number, number] { if (!tStr) return [12, 0, 0] const [h, m, s] = tStr.split(':').map(Number) return [isNaN(h) ? 12 : h, isNaN(m) ? 0 : m, isNaN(s) ? 0 : s] } export default function GanttChart({ processes, servers, onRunningChange }: { processes: Process[], servers?: Server[], onRunningChange?: (count: number) => void }) { const [view, setView] = useState('Tag') const [now, setNow] = useState(new Date()) useEffect(() => { const interval = setInterval(() => setNow(new Date()), 60_000) return () => clearInterval(interval) }, []) // Calculate Running Processes whenever now or processes change useEffect(() => { if (!onRunningChange) return let count = 0 const nowMs = now.getTime() for (const proc of processes) { if (proc.Status?.value !== 'aktiv') continue // Minimal version of renderBlocks logic to find if any block overlaps 'now' const rep = proc.Wiederholung?.value let isRunning = false // We only need to check today's blocks for 'now' const iter = new Date(now) iter.setHours(0, 0, 0, 0) const check = (stMs: number, durSecs: number) => { const edMs = stMs + (durSecs * 1000) return nowMs >= stMs && nowMs <= edMs } const dur = parseInt(proc.Dauer || '0') || 60 const dayStrings = ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'] const currentDayStr = dayStrings[iter.getDay()] // Weekday filter if (proc.Wochentage && proc.Wochentage.trim() !== '') { if (!proc.Wochentage.includes(currentDayStr) && rep !== 'WOCHENENDE' && rep !== 'WERKTAGE') { continue } } if (rep === 'TAEGLICH') { const [h, m, s] = parseTime(proc.Start) const d = new Date(iter) d.setHours(h, m, s) if (check(d.getTime(), dur)) isRunning = true } else if (rep === 'STUENDLICH') { const d = new Date(iter) d.setHours(now.getHours(), proc['Start Minute'] || 0, 0) if (check(d.getTime(), dur)) isRunning = true } else if (rep === 'WOECHENTLICH') { const [h, m, s] = parseTime(proc.Start) const d = new Date(iter) d.setHours(h, m, s) if (check(d.getTime(), dur)) isRunning = true } else if (rep === 'WERKTAGE') { const day = iter.getDay() if (day >= 1 && day <= 5) { const [h, m, s] = parseTime(proc.Start) const d = new Date(iter) d.setHours(h, m, s) if (check(d.getTime(), dur)) isRunning = true } } else if (rep === 'WOCHENENDE') { const day = iter.getDay() if (day === 0 || day === 6) { const [h, m, s] = parseTime(proc.Start) const d = new Date(iter) d.setHours(h, m, s) if (check(d.getTime(), dur)) isRunning = true } } else if (rep === 'INTERVALL') { const [fh, fm, fs] = parseTime(proc['Erste Ausführung']) const [wh, wm, ws] = parseTime(proc['Ausführung bis'] || '23:59:59') const interval = Math.max(1, parseInt(proc['Intervall zwischen'] || '0') || 60) const dayStart = new Date(iter) dayStart.setHours(fh, fm, fs) const dayEnd = new Date(iter) dayEnd.setHours(wh, wm, ws) // Find if any interval in this day contains 'now' if (nowMs >= dayStart.getTime() && nowMs <= dayEnd.getTime()) { const diffSeconds = (nowMs - dayStart.getTime()) / 1000 const remainder = diffSeconds % interval if (remainder <= dur) isRunning = true } } else if (rep === 'MONATLICH_FESTER_TAG') { if (iter.getDate() === 1) { const [h, m, s] = parseTime(proc.Start) const d = new Date(iter) d.setHours(h, m, s) if (check(d.getTime(), dur)) isRunning = true } } if (isRunning) count++ } onRunningChange(count) }, [now, processes, onRunningChange]) const { start: viewStart, end: viewEnd, spanHours } = getViewBounds(view, now) const spanMs = viewEnd.getTime() - viewStart.getTime() // Generate Hour Markers const markers = [] let mkIter = new Date(viewStart) // Dynamic Snapping if (view === '2H') { mkIter.setMinutes(Math.floor(mkIter.getMinutes() / 15) * 15, 0, 0) } else if (view === '6H') { mkIter.setMinutes(Math.floor(mkIter.getMinutes() / 30) * 30, 0, 0) } else if (view === 'Monat') { mkIter.setHours(0, 0, 0, 0) } else if (view === 'Woche') { mkIter.setHours(Math.floor(mkIter.getHours() / 6) * 6, 0, 0, 0) } else { mkIter.setMinutes(0, 0, 0) } while (mkIter <= viewEnd) { const pct = ((mkIter.getTime() - viewStart.getTime()) / spanMs) * 100 let label = '' if (view === 'Monat') { label = mkIter.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' }) } else if (view === 'Woche') { if (mkIter.getHours() === 0) { label = mkIter.toLocaleDateString('de-DE', { weekday: 'short', day: '2-digit', month: '2-digit' }) } else { label = mkIter.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }) } } else { label = mkIter.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }) } if (pct >= 0 && pct <= 100) { markers.push({ key: mkIter.getTime(), pct, label }) } // Dynamic Increment if (view === '2H') mkIter.setMinutes(mkIter.getMinutes() + 15) else if (view === '6H') mkIter.setMinutes(mkIter.getMinutes() + 30) else if (view === 'Woche') mkIter.setHours(mkIter.getHours() + 6) else if (view === 'Monat') mkIter.setDate(mkIter.getDate() + 1) else mkIter.setHours(mkIter.getHours() + 1) } // Now line const nowPct = ((now.getTime() - viewStart.getTime()) / spanMs) * 100 const isNowVisible = nowPct >= 0 && nowPct <= 100 // Group processes by server const serverGroups = new Map() for (const p of processes) { // Only render active processes if (p.Status?.value && p.Status.value !== 'aktiv') continue // APPLY VISIBILITY MAX VIEW FILTER (CRITICAL MEMORY PRESERVATION) const maxView = p.Sichtbarkeit?.value || 'Immer' if (maxView === 'Max 6 Stunden') { if (view === 'Tag' || view === 'Woche' || view === 'Monat' || view === 'Arbeitszeit' || view === 'Nachts') continue } else if (maxView === 'Max 1 Tag') { if (view === 'Woche' || view === 'Monat') continue } const sName = p.Server?.[0]?.value || 'Ohne Server' if (!serverGroups.has(sName)) serverGroups.set(sName, []) serverGroups.get(sName)!.push(p) } const renderBlocksForProcess = (proc: Process) => { const rawBlocks: { stMs: number; edMs: number }[] = [] const rep = proc.Wiederholung?.value let iter = new Date(viewStart) iter.setHours(0, 0, 0, 0) // Add one day padding iter.setDate(iter.getDate() - 1) const iterEnd = new Date(viewEnd) iterEnd.setDate(iterEnd.getDate() + 1) const pushRawBlock = (startTimeStr: Date, durationSecs: number) => { const stMs = startTimeStr.getTime() const edMs = stMs + (durationSecs * 1000) if (edMs < viewStart.getTime() || stMs > viewEnd.getTime()) return rawBlocks.push({ stMs, edMs }) } while (iter < iterEnd) { // Apply weekday filter globally const dayStrings = ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'] const currentDayStr = dayStrings[iter.getDay()] if (proc.Wochentage && proc.Wochentage.trim() !== '') { if (!proc.Wochentage.includes(currentDayStr) && rep !== 'WOCHENENDE' && rep !== 'WERKTAGE') { iter.setDate(iter.getDate() + 1) continue } } if (rep === 'TAEGLICH') { const [h, m, s] = parseTime(proc.Start) const d = new Date(iter) d.setHours(h, m, s) pushRawBlock(d, parseInt(proc.Dauer || '0') || 60) } else if (rep === 'STUENDLICH') { for (let hh = 0; hh < 24; hh++) { const d = new Date(iter) d.setHours(hh, proc['Start Minute'] || 0, 0) pushRawBlock(d, Math.max(1, parseInt(proc.Dauer || '0') || 60)) } } else if (rep === 'WOECHENTLICH') { const [h, m, s] = parseTime(proc.Start) const d = new Date(iter) d.setHours(h, m, s) pushRawBlock(d, parseInt(proc.Dauer || '0') || 60) } else if (rep === 'WERKTAGE') { const day = iter.getDay() if (day >= 1 && day <= 5) { const [h, m, s] = parseTime(proc.Start) const d = new Date(iter) d.setHours(h, m, s) pushRawBlock(d, parseInt(proc.Dauer || '0') || 60) } } else if (rep === 'WOCHENENDE') { const day = iter.getDay() if (day === 0 || day === 6) { const [h, m, s] = parseTime(proc.Start) const d = new Date(iter) d.setHours(h, m, s) pushRawBlock(d, parseInt(proc.Dauer || '0') || 60) } } else if (rep === 'INTERVALL') { const [fh, fm, fs] = parseTime(proc['Erste Ausführung']) const [wh, wm, ws] = parseTime(proc['Ausführung bis'] || '23:59:59') const interval = Math.max(1, parseInt(proc['Intervall zwischen'] || '0') || 60) const dur = parseInt(proc.Dauer || '0') || 60 const dayStart = new Date(iter) dayStart.setHours(fh, fm, fs) const dayEnd = new Date(iter) dayEnd.setHours(wh, wm, ws) for (let t = dayStart.getTime(); t <= dayEnd.getTime(); t += interval * 1000) { pushRawBlock(new Date(t), dur) } } else if (rep === 'MONATLICH_FESTER_TAG') { if (iter.getDate() === 1) { const [h, m, s] = parseTime(proc.Start) const d = new Date(iter) d.setHours(h, m, s) pushRawBlock(d, parseInt(proc.Dauer || '0') || 60) } } else { const [h, m, s] = parseTime(proc.Start || '12:00:00') const d = new Date(iter) d.setHours(h, m, s) pushRawBlock(d, parseInt(proc.Dauer || '0') || 30) } iter.setDate(iter.getDate() + 1) } // Sort blocks by start time rawBlocks.sort((a, b) => a.stMs - b.stMs) // Merge blocks that are overlapping or too close to render individually (e.g. gap < 0.2% of screen) const mergedBlocks: { stMs: number; edMs: number }[] = [] // 0.2% of total span ms is threshold for merging const mergeThreshold = spanMs * 0.002 for (const b of rawBlocks) { if (mergedBlocks.length === 0) { mergedBlocks.push({ ...b }) } else { const prev = mergedBlocks[mergedBlocks.length - 1] // If start is before or overlaps with prev.end + visual threshold if (b.stMs <= prev.edMs + mergeThreshold) { prev.edMs = Math.max(prev.edMs, b.edMs) } else { mergedBlocks.push({ ...b }) } } } // Now map them to standard JSX layout return mergedBlocks.map(b => { const left = Math.max(0, ((b.stMs - viewStart.getTime()) / spanMs) * 100) const rightBounds = Math.min(viewEnd.getTime(), b.edMs) let width = ((rightBounds - Math.max(viewStart.getTime(), b.stMs)) / spanMs) * 100 if (width < 0.2) width = 0.2 return (
{width > 5 && {proc.Name}}
) }) } return (
{/* Zoom controls */}

Timeline Setup

{(['2H', '6H', 'Arbeitszeit', 'Nachts', 'Tag', 'Woche', 'Monat'] as ViewMode[]).map(v => ( ))}
{/* Header markers */}
{markers.map(m => (
{m.label}
))}
{/* Chart body */}
{isNowVisible && (
)} {Array.from(serverGroups.entries()) .sort(([sNameA], [sNameB]) => { if (!servers) return 0 const sA = servers.find(s => s.Name === sNameA) const sB = servers.find(s => s.Name === sNameB) return (sA?.Sortierung ?? 9999) - (sB?.Sortierung ?? 9999) }) .map(([serverName, procs]) => { const sortedProcs = [...procs].sort((a, b) => (a.Sortierung ?? 9999) - (b.Sortierung ?? 9999)) return (
{SERVER_ICON} {serverName}
{sortedProcs.length > 0 && (
{sortedProcs.map(p => ( {p.Name} ))}
)}
{sortedProcs.map(p => ( {renderBlocksForProcess(p)} ))}
)})} {serverGroups.size === 0 && (
Keine aktiven Prozesse in dieser Ansicht sichtbar.
)}
) }