import { useCallback, useEffect, useMemo, useState } from "react"; import { fetchPlans, fetchPlatforms, sendPlanDecision } from "../../api/tguApi"; import type { TguPlanDecision, TguPlatform } from "../../model/tguTypes"; import type { TguActiveTab, TguPlanUi, TimelineRange } from "../../model/timelineTypes"; import { TguEditorTab } from "../tgu-editor/TguEditorTab"; import { Tgu2DMapTab } from "../tgu-map-2d/Tgu2DMapTab"; import { TguRequestsTab } from "../tgu-requests/TguRequestsTab"; import { TguPlanDetails } from "./TguPlanDetails"; import { TguPlanningLayout } from "./TguPlanningLayout"; import { TguSidebar } from "./TguSidebar"; import { TguTimeline } from "./TguTimeline"; import { TguToolbar } from "./TguToolbar"; import { buildPlatformsForInterval, buildTimelineRows, mapPlans } from "./tguTimelineMapper"; const DAY_MS = 24 * 60 * 60 * 1000; export function TguPlanningPage() { const initialRange = useMemo(defaultRange, []); const [fromValue, setFromValue] = useState(formatDateTimeLocal(initialRange.fromMs)); const [toValue, setToValue] = useState(formatDateTimeLocal(initialRange.toMs)); const [appliedRange, setAppliedRange] = useState(initialRange); const [rawPlatforms, setRawPlatforms] = useState([]); const [plans, setPlans] = useState([]); const [loading, setLoading] = useState(false); const [pageError, setPageError] = useState(); const [platformLoadFailed, setPlatformLoadFailed] = useState(false); const [search, setSearch] = useState(""); const [selectedSpacecraftId, setSelectedSpacecraftId] = useState(); const [selectedPlanId, setSelectedPlanId] = useState(); const [activeTab, setActiveTab] = useState("timeline"); const [decisionInFlight, setDecisionInFlight] = useState(false); const [decisionNotice, setDecisionNotice] = useState(); const [decisionError, setDecisionError] = useState(); const parsedInputRange = useMemo( () => ({ fromMs: new Date(fromValue).getTime(), toMs: new Date(toValue).getTime() }), [fromValue, toValue] ); const invalidRange = !Number.isFinite(parsedInputRange.fromMs) || !Number.isFinite(parsedInputRange.toMs) || parsedInputRange.fromMs >= parsedInputRange.toMs; const loadData = useCallback( async (range: TimelineRange) => { setLoading(true); setPageError(undefined); setDecisionError(undefined); const historyDays = historyDaysForRange(range); const [platformResult, planResult] = await Promise.allSettled([ fetchPlatforms(), fetchPlans(historyDays) ]); if (platformResult.status === "rejected") { setPlatformLoadFailed(true); } else { setPlatformLoadFailed(false); setRawPlatforms(platformResult.value); } if (planResult.status === "rejected") { setPlans([]); setPageError(planResult.reason instanceof Error ? planResult.reason.message : "Не удалось загрузить планы."); } else { const platforms = platformResult.status === "fulfilled" ? platformResult.value : []; setPlans(mapPlans(planResult.value, platforms)); } setLoading(false); }, [] ); useEffect(() => { void loadData(appliedRange); }, [appliedRange, loadData]); const platforms = useMemo( () => buildPlatformsForInterval(rawPlatforms, plans, appliedRange), [rawPlatforms, plans, appliedRange] ); const rows = useMemo( () => buildTimelineRows(plans, platforms, appliedRange, selectedSpacecraftId), [plans, platforms, appliedRange, selectedSpacecraftId] ); const selectedPlan = useMemo(() => { const plan = plans.find((item) => item.planId === selectedPlanId); if (!plan) return undefined; const platform = platforms.find((item) => item.spacecraftId === plan.spacecraftId); return { ...plan, platform }; }, [plans, platforms, selectedPlanId]); const applyRange = () => { setDecisionNotice(undefined); if (invalidRange) { setPageError("Некорректный интервал: from должен быть меньше to."); return; } setPageError(undefined); setAppliedRange(parsedInputRange); }; const quickRange = (hours: number) => { const fromMs = startOfCurrentDayMs(); const toMs = fromMs + hours * 60 * 60 * 1000; setFromValue(formatDateTimeLocal(fromMs)); setToValue(formatDateTimeLocal(toMs)); setAppliedRange({ fromMs, toMs }); setPageError(undefined); setDecisionNotice(undefined); }; const handleDecision = async (planId: string, decision: TguPlanDecision, reason?: string) => { setDecisionInFlight(true); setDecisionError(undefined); setDecisionNotice(undefined); try { await sendPlanDecision(planId, decision, reason); setDecisionNotice( decision === "REJECTED" ? "Решение REJECTED отправлено. План может вернуться в PLANNED для повторной выдачи." : "Решение ACCEPTED отправлено." ); await loadData(appliedRange); } catch (error) { setDecisionError(error instanceof Error ? error.message : "Не удалось отправить решение."); } finally { setDecisionInFlight(false); } }; return ( } > {activeTab === "timeline" ? (
{ setSelectedSpacecraftId(spacecraftId); setSelectedPlanId(undefined); setDecisionNotice(undefined); }} />
{ setSelectedPlanId(planId); const plan = plans.find((item) => item.planId === planId); if (plan) { setSelectedSpacecraftId(plan.spacecraftId); } }} />
setSelectedPlanId(undefined)} />
) : activeTab === "map" ? ( ) : activeTab === "requests" ? ( ) : ( )}
); } function defaultRange(): TimelineRange { const fromMs = startOfCurrentDayMs(); return { fromMs, toMs: fromMs + DAY_MS }; } function startOfCurrentDayMs(): number { const now = new Date(); return new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime(); } function formatDateTimeLocal(ms: number): string { const date = new Date(ms); const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); const hours = String(date.getHours()).padStart(2, "0"); const minutes = String(date.getMinutes()).padStart(2, "0"); return `${year}-${month}-${day}T${hours}:${minutes}`; } function historyDaysForRange(range: TimelineRange): number { const daysBack = Math.ceil(Math.max(0, Date.now() - range.fromMs) / DAY_MS); return Math.max(1, daysBack + 1); }