diff --git a/server/websockets/sessionUsersWebsocket.ts b/server/websockets/sessionUsersWebsocket.ts index 1c6f36f..c364772 100644 --- a/server/websockets/sessionUsersWebsocket.ts +++ b/server/websockets/sessionUsersWebsocket.ts @@ -363,7 +363,6 @@ export function setupSessionUsersWebsocket(httpServer: HttpServer) { io.to(sessionId).emit('sessionUsersUpdate', users); const overviewIO = getOverviewIO(); if (overviewIO) { - // Force immediate overview data update setTimeout(async () => { try { const { getOverviewData } = await import('./overviewWebsocket.js'); @@ -386,13 +385,12 @@ export function setupSessionUsersWebsocket(httpServer: HttpServer) { if (entry) entry.lastActive = Date.now(); }); - // On disconnect, flush active time to cache socket.on('disconnect', () => { const entry = userActivity.get(userKey); if (entry) { const now = Date.now(); - const activeTime = Math.max(0, now - entry.sessionStart - (5 * 60 * 1000)); - entry.totalActive += activeTime / 60000; + const remainingActiveTime = Math.max(0, (now - entry.lastActive) / 60000 - 0.1); + entry.totalActive += remainingActiveTime; incrementStat(user.userId, 'total_time_controlling_minutes', entry.totalActive); userActivity.delete(userKey); } @@ -439,17 +437,15 @@ export function setupSessionUsersWebsocket(httpServer: HttpServer) { return io; } -// Periodic check (e.g., every minute) to update active time +// Removed incrementStat call to prevent double-counting. Time is now only incremented on disconnect. setInterval(() => { - for (const [userKey, entry] of userActivity.entries()) { - const now = Date.now(); - if (now - entry.lastActive > 5 * 60 * 1000) continue; // Idle, skip - const activeTime = (now - entry.lastActive) / 60000; - entry.totalActive += activeTime; - entry.lastActive = now; - // Increment stat in cache - incrementStat(userKey, 'total_time_controlling_minutes', activeTime); - } + for (const [userKey, entry] of userActivity.entries()) { + const now = Date.now(); + if (now - entry.lastActive > 5 * 60 * 1000) continue; + const activeTime = (now - entry.lastActive) / 60000; + entry.totalActive += activeTime; + entry.lastActive = now; + } }, 60 * 1000); export function getActiveUsers(): typeof activeUsers { diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 62722f5..7caae12 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -313,7 +313,6 @@ export default function Navbar({ sessionId, accessId }: NavbarProps) {

{notification.text}

- {/* New: Hide button */}
{isLegacyMode ? ( - <> - {/* Legacy Mode: Split View*/} -
-
- {/* Departure Airport Section */} - {departureAirport && departureCharts.length > 0 && ( -
-
- -

- Departure - {departureAirport} -

-
-
- {departureCharts.map((chart) => ( - - ))} -
+ isMobile ? ( + mobileView === 'sidebar' ? ( +
+
+
+ + {searchQuery && matchedCategories.size > 0 && ( +
+ Filtered by: {Array.from(matchedCategories).join(', ')} +
+ )}
- )} +
+ {/* Departure Airport Section */} + {departureAirport && filteredDepartureCharts.length > 0 && ( +
+
+ +

+ Departure - {departureAirport} +

+
+
+ {filteredDepartureCharts.map((chart) => ( + + ))} +
+
+ )} - {/* Arrival Airport Section */} - {arrivalAirport && arrivalCharts.length > 0 && ( -
-
- -

- Arrival - {arrivalAirport} -

-
-
- {arrivalCharts.map((chart) => ( - + ))} +
+
+ )} + + {/* Other Airports Section */} + {filteredOtherAirports.length > 0 && ( +
+ - ))} -
+
+ +

+ All Airports +

+
+ {showAllAirports ? ( + + ) : ( + + )} + + {showAllAirports && ( +
+ {filteredOtherAirports.map(({ icao, charts }) => ( +
+
+ {icao} +
+ {charts.map((chart) => ( + + ))} +
+ ))} +
+ )} +
+ )} + + {!departureAirport && + !arrivalAirport && + filteredOtherAirports.length === 0 && ( +
+ No flight information available +
+ )} + {searchQuery && + filteredDepartureCharts.length === 0 && + filteredArrivalCharts.length === 0 && + filteredOtherAirports.length === 0 && ( +
+ No charts match your search +
+ )} +
+
+
+ ) : ( +
+ + {!selectedChart ? ( +
+ Select a chart from the list +
+ ) : chartLoadError ? ( +
+ Chart not available
+ ) : ( + <> +
{ + e.preventDefault(); + if (e.deltaY < 0) handleZoomIn(); + else if (e.deltaY > 0) handleZoomOut(); + }} + style={{ cursor: isChartDragging ? 'grabbing' : 'grab' }} + > + {imageLoading && ( +
+ +
+ )} + Airport Chart e.preventDefault()} + onLoad={(e) => { + setChartLoadError(false); + setImageLoading(false); + setImageSize({ + width: (e.target as HTMLImageElement).naturalWidth, + height: (e.target as HTMLImageElement) + .naturalHeight, + }); + }} + onError={() => { + setChartLoadError(true); + setImageLoading(false); + }} + /> +
+ {chartsToUse.find((c) => c.path === selectedChart) + ?.credits && ( +
+ Chart created by{' '} + { + chartsToUse.find((c) => c.path === selectedChart) + ?.credits + } +
+ )} +
+ Redistribution of this chart is prohibited. +
+ )} +
+ ) + ) : ( + <> +
+
+ + {searchQuery && matchedCategories.size > 0 && ( +
+ Filtered by: {Array.from(matchedCategories).join(', ')} +
+ )} +
+
+ {/* Departure Airport Section */} + {departureAirport && filteredDepartureCharts.length > 0 && ( +
+
+ +

+ Departure - {departureAirport} +

+
+
+ {filteredDepartureCharts.map((chart) => ( + + ))} +
+
+ )} - {/* Other Airports Section */} - {otherAirports.length > 0 && ( -
- - {showAllAirports && ( -
- {otherAirports.map((icao) => { - const airportCharts = getChartsForAirport(icao); - if (airportCharts.length === 0) return null; +
+ {filteredArrivalCharts.map((chart) => ( + + ))} +
+
+ )} - return ( + {/* Other Airports Section */} + {filteredOtherAirports.length > 0 && ( +
+ + {showAllAirports && ( +
+ {filteredOtherAirports.map(({ icao, charts }) => (
{icao}
- {airportCharts.map((chart) => ( + {charts.map((chart) => ( ))}
- ); - })} + ))} +
+ )} +
+ )} + + {!departureAirport && + !arrivalAirport && + filteredOtherAirports.length === 0 && ( +
+ No flight information available +
+ )} + {searchQuery && + filteredDepartureCharts.length === 0 && + filteredArrivalCharts.length === 0 && + filteredOtherAirports.length === 0 && ( +
+ No charts match your search
)} +
+
+
+ {!selectedChart ? ( +
+ Select a chart from the list
- )} - - {!departureAirport && !arrivalAirport && ( -
- No flight information available + ) : chartLoadError ? ( +
+ Chart not available
- )} -
-
-
- {!selectedChart ? ( -
- Select a chart from the list -
- ) : chartLoadError ? ( -
- Chart not available -
- ) : ( - <> -
{ - e.preventDefault(); - if (e.deltaY < 0) handleZoomIn(); - else if (e.deltaY > 0) handleZoomOut(); - }} - style={{ cursor: isChartDragging ? 'grabbing' : 'grab' }} - > - {imageLoading && ( -
- + ) : ( + <> +
{ + e.preventDefault(); + if (e.deltaY < 0) handleZoomIn(); + else if (e.deltaY > 0) handleZoomOut(); + }} + style={{ cursor: isChartDragging ? 'grabbing' : 'grab' }} + > + {imageLoading && ( +
+ +
+ )} + Airport Chart e.preventDefault()} + onLoad={(e) => { + setChartLoadError(false); + setImageLoading(false); + setImageSize({ + width: (e.target as HTMLImageElement).naturalWidth, + height: (e.target as HTMLImageElement) + .naturalHeight, + }); + }} + onError={() => { + setChartLoadError(true); + setImageLoading(false); + }} + /> +
+ {chartsToUse.find((c) => c.path === selectedChart) + ?.credits && ( +
+ Chart created by{' '} + { + chartsToUse.find((c) => c.path === selectedChart) + ?.credits + }
)} - Airport Chart e.preventDefault()} - onLoad={(e) => { - setChartLoadError(false); - setImageLoading(false); - setImageSize({ - width: (e.target as HTMLImageElement).naturalWidth, - height: (e.target as HTMLImageElement).naturalHeight, - }); - }} - onError={() => { - setChartLoadError(true); - setImageLoading(false); - }} - /> -
- {chartsToUse.find((c) => c.path === selectedChart)?.credits && ( -
- Chart created by{' '} - {chartsToUse.find((c) => c.path === selectedChart)?.credits} +
+ Redistribution of this chart is prohibited.
- )} -
- Redistribution of this chart is prohibited. -
- - )} -
- + + )} +
+ + ) ) : ( - /* List Mode*/ + /* List Mode*/
{!selectedChart ? (
@@ -444,7 +851,8 @@ export default function AcarsChartDrawer({ setImageLoading(false); setImageSize({ width: (e.target as HTMLImageElement).naturalWidth, - height: (e.target as HTMLImageElement).naturalHeight, + height: (e.target as HTMLImageElement) + .naturalHeight, }); }} onError={() => { @@ -453,10 +861,14 @@ export default function AcarsChartDrawer({ }} />
- {chartsToUse.find((c) => c.path === selectedChart)?.credits && ( + {chartsToUse.find((c) => c.path === selectedChart) + ?.credits && (
Chart created by{' '} - {chartsToUse.find((c) => c.path === selectedChart)?.credits} + { + chartsToUse.find((c) => c.path === selectedChart) + ?.credits + }
)}
diff --git a/src/components/buttons/UserButton.tsx b/src/components/buttons/UserButton.tsx index e59042c..2ab34d9 100644 --- a/src/components/buttons/UserButton.tsx +++ b/src/components/buttons/UserButton.tsx @@ -1,338 +1,304 @@ import { useState, useRef, useEffect } from 'react'; import { - User, - LogOut, - Settings, - ChevronDown, - List, - LayoutDashboard, - Notebook, + User, + LogOut, + Settings, + ChevronDown, + List, + LayoutDashboard, + Notebook, } from 'lucide-react'; -import { Link } from 'react-router-dom'; import { useAuth } from '../../hooks/auth/useAuth'; import ProtectedRoute from '../ProtectedRoute'; interface CustomUserButtonProps { - className?: string; - isMobile?: boolean; - onAction?: () => void; + className?: string; + isMobile?: boolean; + onAction?: () => void; } export default function CustomUserButton({ - className = '', - isMobile = false, - onAction, + className = '', + isMobile = false, + onAction, }: CustomUserButtonProps) { - const { user, isLoading, logout } = useAuth(); - const [isDropdownOpen, setIsDropdownOpen] = useState(false); - const dropdownRef = useRef(null); + const { user, isLoading, logout } = useAuth(); + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const dropdownRef = useRef(null); - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if ( - dropdownRef.current && - !dropdownRef.current.contains(event.target as Node) - ) { - setIsDropdownOpen(false); - } - }; - - if (isDropdownOpen) { - document.addEventListener('mousedown', handleClickOutside); - } - - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, [isDropdownOpen]); - - const handleAction = (callback?: () => void) => { - if (onAction) onAction(); - if (callback) callback(); + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + dropdownRef.current && + !dropdownRef.current.contains(event.target as Node) + ) { setIsDropdownOpen(false); + } }; - if (isLoading) { - return ( -
-
-
- ); + if (isDropdownOpen) { + document.addEventListener('mousedown', handleClickOutside); } - if (!user) { - const baseClasses = isMobile - ? 'w-full bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 text-white px-6 py-3 rounded-xl font-medium transition-all duration-300 shadow-lg hover:shadow-xl' - : 'bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 text-white px-6 py-2 rounded-full font-medium transition-all duration-300 shadow-lg hover:shadow-xl'; + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [isDropdownOpen]); - return ( - - ); - } + const handleAction = (callback?: () => void) => { + if (onAction) onAction(); + if (callback) callback(); + setIsDropdownOpen(false); + }; - if (isMobile) { - return ( -
- handleAction()} - className="flex items-center space-x-3 px-4 py-3 bg-zinc-800/60 rounded-xl border border-zinc-700/50 hover:bg-zinc-700/60 transition-colors" - > - {user.avatar ? ( - {user.username} - ) : ( -
- -
- )} -
-

- {user.username} -

- {user.isAdmin && ( - - Admin - - )} -
- + if (isLoading) { + return ( +
+
+
+ ); + } -
- + if (!user) { + const baseClasses = isMobile + ? 'w-full bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 text-white px-6 py-3 rounded-xl font-medium transition-all duration-300 shadow-lg hover:shadow-xl' + : 'bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 text-white px-6 py-2 rounded-full font-medium transition-all duration-300 shadow-lg hover:shadow-xl'; - + return ( + + ); + } + + if (isMobile) { + return ( +
+
+ {user.avatar ? ( + {user.username} + ) : ( +
+ +
+ )} +
+

{user.username}

+ {user.isAdmin && ( + + Admin + + )} +
+
- +
+ + + + + + + + + {(user.isAdmin || + (user.rolePermissions && user.rolePermissions.admin)) && ( + + + + )} - + +
+
+ ); + } - {(user.isAdmin || - (user.rolePermissions && - user.rolePermissions.admin)) && ( - - - - )} + return ( +
+ - + {isDropdownOpen && ( +
+
+
+ {user.avatar ? ( + {user.username} + ) : ( +
+
+ )} +
+

+ {user.username} +

+ {user.isAdmin && ( +
+ Administrator +
+ )} +
- ); - } +
- return ( -
+
- {isDropdownOpen && ( -
-
-
- {user.avatar ? ( - {user.username} - ) : ( -
- -
- )} -
-

- {user.username} -

- {user.isAdmin && ( -
- Administrator -
- )} -
-
-
- -
- - - + - + - -
- {(user.isAdmin || - (user.rolePermissions && - user.rolePermissions.admin)) && ( - -
- -
-
- )} -
- -
-
- )} + +
+ {(user.isAdmin || + (user.rolePermissions && user.rolePermissions.admin)) && ( + +
+ +
+
+ )} +
+ +
- ); + )} +
+ ); } diff --git a/src/components/tables/DepartureTable.tsx b/src/components/tables/DepartureTable.tsx index 3df05f4..f89b48f 100644 --- a/src/components/tables/DepartureTable.tsx +++ b/src/components/tables/DepartureTable.tsx @@ -77,6 +77,15 @@ export default function DepartureTable({ const [remarkValues, setRemarkValues] = useState< Record >({}); + const [callsignValues, setCallsignValues] = useState< + Record + >({}); + const [standValues, setStandValues] = useState< + Record + >({}); + const [squawkValues, setSquawkValues] = useState< + Record + >({}); const debounceTimeouts = useRef>({}); const debouncedHandleRemarkChange = useCallback( @@ -97,6 +106,60 @@ export default function DepartureTable({ [onFlightChange] ); + const debouncedHandleCallsignChange = useCallback( + (flightId: string | number, callsign: string) => { + setCallsignValues((prev) => ({ ...prev, [flightId]: callsign })); + + if (debounceTimeouts.current[flightId]) { + clearTimeout(debounceTimeouts.current[flightId]); + } + + debounceTimeouts.current[flightId] = setTimeout(() => { + if (onFlightChange) { + onFlightChange(flightId, { callsign }); + } + delete debounceTimeouts.current[flightId]; + }, 500); + }, + [onFlightChange] + ); + + const debouncedHandleStandChange = useCallback( + (flightId: string | number, stand: string) => { + setStandValues((prev) => ({ ...prev, [flightId]: stand })); + + if (debounceTimeouts.current[flightId]) { + clearTimeout(debounceTimeouts.current[flightId]); + } + + debounceTimeouts.current[flightId] = setTimeout(() => { + if (onFlightChange) { + onFlightChange(flightId, { stand }); + } + delete debounceTimeouts.current[flightId]; + }, 500); + }, + [onFlightChange] + ); + + const debouncedHandleSquawkChange = useCallback( + (flightId: string | number, squawk: string) => { + setSquawkValues((prev) => ({ ...prev, [flightId]: squawk })); + + if (debounceTimeouts.current[flightId]) { + clearTimeout(debounceTimeouts.current[flightId]); + } + + debounceTimeouts.current[flightId] = setTimeout(() => { + if (onFlightChange) { + onFlightChange(flightId, { squawk }); + } + delete debounceTimeouts.current[flightId]; + }, 500); + }, + [onFlightChange] + ); + const handleHideFlight = async (flightId: string | number) => { if (onFlightChange) { onFlightChange(flightId, { hidden: true }); @@ -124,27 +187,6 @@ export default function DepartureTable({ debouncedHandleRemarkChange(flightId, remark); }; - const handleCallsignChange = ( - flightId: string | number, - callsign: string - ) => { - if (onFlightChange) { - onFlightChange(flightId, { callsign }); - } - }; - - const handleStandChange = (flightId: string | number, stand: string) => { - if (onFlightChange) { - onFlightChange(flightId, { stand }); - } - }; - - const handleSquawkChange = (flightId: string | number, squawk: string) => { - if (onFlightChange) { - onFlightChange(flightId, { squawk }); - } - }; - const handleArrivalChange = (flightId: string | number, arrival: string) => { if (onFlightChange) { onFlightChange(flightId, { arrival }); @@ -242,12 +284,12 @@ export default function DepartureTable({ const handleRegenerateSquawk = (flightId: string | number) => { const newSquawk = generateRandomSquawk(); + setSquawkValues((prev) => ({ ...prev, [flightId]: newSquawk })); if (onFlightChange) { onFlightChange(flightId, { squawk: newSquawk }); } }; - // when rendering mobile variant if (isMobile) { return ( <> @@ -402,9 +444,11 @@ export default function DepartureTable({ {departureColumns.callsign !== false && ( - handleCallsignChange(flight.id, value) + debouncedHandleCallsignChange(flight.id, value) } className="bg-transparent border-none focus:bg-gray-800 px-1 rounded text-white" placeholder="-" @@ -426,9 +470,9 @@ export default function DepartureTable({ {departureColumns.stand !== false && ( - handleStandChange(flight.id, value) + debouncedHandleStandChange(flight.id, value) } className="bg-transparent border-none focus:bg-gray-800 px-1 rounded text-white" placeholder="-" @@ -531,9 +575,11 @@ export default function DepartureTable({
- handleSquawkChange(flight.id, value) + debouncedHandleSquawkChange(flight.id, value) } className="bg-transparent border-none focus:bg-gray-800 px-1 rounded text-white w-full min-w-0" placeholder="-" @@ -571,7 +617,7 @@ export default function DepartureTable({ flight.id, !isClearanceChecked(flight.clearance) ) - } // or: onToggleClearance(flight.id) + } label="" checkedClass="bg-green-600 border-green-600" flashing={isFlashing} diff --git a/src/components/tools/ChatSidebar.tsx b/src/components/tools/ChatSidebar.tsx index ef06cce..3f12b4f 100644 --- a/src/components/tools/ChatSidebar.tsx +++ b/src/components/tools/ChatSidebar.tsx @@ -190,12 +190,6 @@ export default function ChatSidebar({ Session Chat -
-
- - {activeChatUsers.length} online - -
diff --git a/src/pages/ACARS.tsx b/src/pages/ACARS.tsx index 3fd76c3..0291a17 100644 --- a/src/pages/ACARS.tsx +++ b/src/pages/ACARS.tsx @@ -3,7 +3,14 @@ import { useParams, useSearchParams, useNavigate } from 'react-router-dom'; import Navbar from '../components/Navbar'; import Loader from '../components/common/Loader'; import Button from '../components/common/Button'; -import { PanelLeftClose, PanelLeftOpen, Map, PlaneTakeoff, MapPinned, PlusCircle } from 'lucide-react'; +import { + PanelLeftClose, + PanelLeftOpen, + Map, + PlaneTakeoff, + MapPinned, + PlusCircle, +} from 'lucide-react'; import { useData } from '../hooks/data/useData'; import { useSettings } from '../hooks/settings/useSettings'; import { createFlightsSocket } from '../sockets/flightsSocket'; @@ -72,8 +79,13 @@ export default function ACARS() { const savedNotes = localStorage.getItem(storageKey); const savedTimestamp = localStorage.getItem(timestampKey); const TWELVE_HOURS = 12 * 60 * 60 * 1000; - const parsedTimestamp = savedTimestamp ? parseInt(savedTimestamp, 10) : null; - const isExpired = parsedTimestamp && !isNaN(parsedTimestamp) && (Date.now() - parsedTimestamp) > TWELVE_HOURS; + const parsedTimestamp = savedTimestamp + ? parseInt(savedTimestamp, 10) + : null; + const isExpired = + parsedTimestamp && + !isNaN(parsedTimestamp) && + Date.now() - parsedTimestamp > TWELVE_HOURS; if (savedNotes && !isExpired) { setNotes(savedNotes); @@ -555,61 +567,49 @@ NOTES:
-
+

- {flight?.callsign - ? `${flight.callsign} - ACARS Terminal` - : 'ACARS Terminal'} + {flight?.callsign ? `${flight.callsign}` : 'ACARS Terminal'}

-
- - + - +
diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index afba53c..50b8d5a 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -610,63 +610,58 @@ export default function Home() { {/* CTA Section - PFControl v2 */} -
-
-
-
-

+
+
+
+
+
+
+

+ PFControl +

+

+ The next-generation flight strip platform built for real-time + coordination between air traffic controllers. +

+
    +
  • + + + Enhanced real-time collaboration between controllers + +
  • +
  • + + Redesigned interface for improved usability +
  • +
  • + + Advanced flight strip management system +
  • +
+
+

-
-

- The next-generation flight strip platform built for real-time - coordination between air traffic controllers. -

-
    -
  • - - - Enhanced real-time collaboration between controllers - -
  • -
  • - - Redesigned interface for improved usability -
  • -
  • - - Advanced flight strip management system -
  • -
-
- -
-
- -
-
-
- -
-
+ Try the Latest Version Now +