diff --git a/app/src/app.tsx b/app/src/app.tsx index f5e19b0..af3284b 100644 --- a/app/src/app.tsx +++ b/app/src/app.tsx @@ -4,23 +4,23 @@ import Dashboard from "./pages/Dashboard"; import Project from "./pages/Project"; import UserAuth from "./pages/UserAuth"; import ToastProvider from "./components/templates/ToastProvider"; -import "./styles/main.scss" +import "./styles/main.scss"; +import { LoggerProvider } from "./components/ui/log/LoggerContext"; const App: React.FC = () => { return ( - - - - } - /> - } /> - } /> - - - + + + + + } /> + } /> + } /> + + + + ); }; -export default App; +export default App; \ No newline at end of file diff --git a/app/src/components/icons/ExportCommonIcons.tsx b/app/src/components/icons/ExportCommonIcons.tsx index 8e03fd0..67d7211 100644 --- a/app/src/components/icons/ExportCommonIcons.tsx +++ b/app/src/components/icons/ExportCommonIcons.tsx @@ -785,21 +785,135 @@ export const SpeedIcon = () => { ); }; -// export const DublicateIcon = () => { -// return ( -// -// -// -// ); -// }; +export const LogListIcon = () => { + return ( + + + + ); +}; + +export const LogTickIcon = () => { + return ( + + + + + + + + + + + ); +}; + +export const LogInfoIcon = () => { + return ( + + + + + + + ); +}; + +export const WarningIcon = () => { + return ( + + + + + + + ); +}; + +export const ErrorIcon = () => { + return ( + + + + ); +}; diff --git a/app/src/components/ui/analysis/ThroughputSummary.tsx b/app/src/components/ui/analysis/ThroughputSummary.tsx index b92a82d..e9946ae 100644 --- a/app/src/components/ui/analysis/ThroughputSummary.tsx +++ b/app/src/components/ui/analysis/ThroughputSummary.tsx @@ -8,6 +8,7 @@ import { PointElement, } from "chart.js"; import { PowerIcon, ThroughputSummaryIcon } from "../../icons/analysis"; +import { getAvatarColor } from "../../../modules/collaboration/functions/getAvatarColor"; ChartJS.register(LineElement, CategoryScale, LinearScale, PointElement); @@ -142,13 +143,13 @@ const ThroughputSummary = () => {
{/* Dynamically create progress bars based on shiftUtilization array */} - {shiftUtilization.map((shift) => ( + {shiftUtilization.map((shift, index) => (
))} @@ -156,11 +157,11 @@ const ThroughputSummary = () => {
{/* Dynamically create shift indicators with random colors */} - {shiftUtilization.map((shift) => ( + {shiftUtilization.map((shift, index) => (
diff --git a/app/src/components/ui/footer/Footer.tsx b/app/src/components/ui/footer/Footer.tsx new file mode 100644 index 0000000..93da68c --- /dev/null +++ b/app/src/components/ui/footer/Footer.tsx @@ -0,0 +1,44 @@ +import React from "react"; +import { HelpIcon } from "../../icons/DashboardIcon"; +import { useLogger } from "../log/LoggerContext"; + +const Footer = () => { + const { logs, setIsLogListVisible } = useLogger(); + + const lastLog = logs.length > 0 ? logs[logs.length - 1] : null; + + return ( +
+
+
+
+
Selection
+
+
+
+
Rotate/Zoom
+
+
+
+
Pan/Context Menu
+
+
+ +
+
setIsLogListVisible(true)}> + {lastLog + ? `[${lastLog.type.toUpperCase()}] ${lastLog.message}` + : "No logs yet."} +
+
+ V 0.01 +
+ +
+
+
+
+ ); +}; + +export default Footer; diff --git a/app/src/components/ui/log/LogList.tsx b/app/src/components/ui/log/LogList.tsx new file mode 100644 index 0000000..4a43933 --- /dev/null +++ b/app/src/components/ui/log/LogList.tsx @@ -0,0 +1,87 @@ +// LogList.tsx +import React, { useState } from "react"; +import { + LogListIcon, + TickIcon, + LogInfoIcon, + WarningIcon, + ErrorIcon, +} from "../../icons/ExportCommonIcons"; // Adjust path as needed +import { useLogger } from "./LoggerContext"; + +const LogList: React.FC = () => { + const { logs, clear, setIsLogListVisible } = useLogger(); + const [selectedTab, setSelectedTab] = useState< + "all" | "info" | "warning" | "error" | "log" + >("all"); + + const getLogIcon = (type: string) => { + switch (type) { + case "info": + return ; + case "error": + return ; + case "warning": + return ; + case "log": + default: + return ; + } + }; + + const formatTimestamp = (date: Date) => new Date(date).toLocaleTimeString(); + + const filteredLogs = + selectedTab === "all" + ? [...logs].reverse() + : [...logs].filter((log) => log.type === selectedTab).reverse(); + + return ( +
+
+
+
+
+ +
+
Log List
+
+
setIsLogListVisible(false)}> + X +
+
+ + {/* Tabs */} +
+ {["all", "info", "warning", "error"].map((type) => ( +
setSelectedTab(type as any)} + > + {`${type.charAt(0).toUpperCase() + type.slice(1)} Logs`} +
+ ))} +
+ + {/* Log Entries */} +
+ {filteredLogs.map((log) => ( +
+
+ +
+
{getLogIcon(log.type)}
+
+ [{formatTimestamp(log.timestamp)}] [{log.type.toUpperCase()}]{" "} + {log.message} +
+
+ ))} +
+
+
+ ); +}; + +export default LogList; diff --git a/app/src/components/ui/log/LoggerContext.tsx b/app/src/components/ui/log/LoggerContext.tsx new file mode 100644 index 0000000..f323b93 --- /dev/null +++ b/app/src/components/ui/log/LoggerContext.tsx @@ -0,0 +1,77 @@ +import React, { createContext, useContext, useState, useCallback } from "react"; + +export type LogType = "log" | "info" | "warning" | "error"; + +export interface LogEntry { + id: string; + type: LogType; + message: string; + timestamp: Date; +} + +interface LoggerContextValue { + logs: LogEntry[]; + setLogs: React.Dispatch>; + isLogListVisible: boolean; + setIsLogListVisible: React.Dispatch>; + log: (message: string) => void; + info: (message: string) => void; + warn: (message: string) => void; + error: (message: string) => void; + clear: () => void; +} + +const LoggerContext = createContext(undefined); + +export const LoggerProvider: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => { + const [logs, setLogs] = useState([]); + const [isLogListVisible, setIsLogListVisible] = useState(false); + + const generateId = useCallback( + () => Math.random().toString(36).substring(2, 9), + [] + ); + + const addLog = useCallback( + (type: LogType, message: string) => { + const newLog: LogEntry = { + id: generateId(), + type, + message, + timestamp: new Date(), + }; + setLogs((prevLogs) => [...prevLogs, newLog]); + }, + [generateId] + ); + + const loggerMethods: LoggerContextValue = { + logs, + setLogs, + isLogListVisible, + setIsLogListVisible, + log: (message: string) => addLog("log", message), + info: (message: string) => addLog("info", message), + warn: (message: string) => addLog("warning", message), + error: (message: string) => addLog("error", message), + clear: () => { + setLogs([]); + }, + }; + + return ( + + {children} + + ); +}; + +export const useLogger = () => { + const context = useContext(LoggerContext); + if (!context) { + throw new Error("useLogger must be used within a LoggerProvider"); + } + return context; +}; diff --git a/app/src/components/ui/log/logger.ts b/app/src/components/ui/log/logger.ts new file mode 100644 index 0000000..1781e26 --- /dev/null +++ b/app/src/components/ui/log/logger.ts @@ -0,0 +1,71 @@ +type LogType = 'log' | 'info' | 'warning' | 'error'; + +interface LogEntry { + type: LogType; + message: string; + timestamp: Date; + context?: string; +} + +class Logger { + private static instance: Logger; + private logs: LogEntry[] = []; + private subscribers: Array<(log: LogEntry) => void> = []; + + private constructor() {} + + public static getInstance(): Logger { + if (!Logger.instance) { + Logger.instance = new Logger(); + } + return Logger.instance; + } + + private notifySubscribers(log: LogEntry) { + this.subscribers.forEach(callback => callback(log)); + } + + private addLog(type: LogType, message: string, context?: string) { + const log: LogEntry = { type, message, timestamp: new Date(), context }; + this.logs.push(log); + this.notifySubscribers(log); + + if (process.env.NODE_ENV === 'development') { + const logMessage = context ? `[${context}] ${message}` : message; + console[type === 'warning' ? 'warn' : type](logMessage); + } + } + + public log(message: string, context?: string) { + this.addLog('log', message, context); + } + + public info(message: string, context?: string) { + this.addLog('info', message, context); + } + + public warning(message: string, context?: string) { + this.addLog('warning', message, context); + } + + public error(message: string, context?: string) { + this.addLog('error', message, context); + } + + public getLogs(): LogEntry[] { + return [...this.logs]; + } + + public clear() { + this.logs = []; + } + + public subscribe(callback: (log: LogEntry) => void) { + this.subscribers.push(callback); + return () => { + this.subscribers = this.subscribers.filter(sub => sub !== callback); + }; + } +} + +export const logger = Logger.getInstance(); diff --git a/app/src/components/ui/simulation/simulationPlayer.tsx b/app/src/components/ui/simulation/simulationPlayer.tsx index 86c5372..6ab6738 100644 --- a/app/src/components/ui/simulation/simulationPlayer.tsx +++ b/app/src/components/ui/simulation/simulationPlayer.tsx @@ -16,6 +16,7 @@ import { SpeedIcon, StartIcon, } from "../../icons/ExportCommonIcons"; +import { getAvatarColor } from "../../../modules/collaboration/functions/getAvatarColor"; const SimulationPlayer: React.FC = () => { const { speed, setSpeed } = useAnimationPlaySpeed(); @@ -110,15 +111,6 @@ const SimulationPlayer: React.FC = () => { return color; }; - // Store colors for each process item - const [processColors, setProcessColors] = useState([]); - - // Generate colors on mount or when process changes - useEffect(() => { - const generatedColors = process.map(() => getRandomColor()); - setProcessColors(generatedColors); - }, []); - const intervals = [10, 20, 30, 40, 50, 60]; // in minutes const totalSegments = intervals.length; const progress = 20; // percent (example) @@ -328,6 +320,8 @@ const SimulationPlayer: React.FC = () => {
+
00:00
+
24:00
{ className="process" style={{ width: `${item.completed}%`, - backgroundColor: processColors[index], + backgroundColor: getAvatarColor(index), }} >
))} diff --git a/app/src/pages/Project.tsx b/app/src/pages/Project.tsx index bdefb8b..c3b70e7 100644 --- a/app/src/pages/Project.tsx +++ b/app/src/pages/Project.tsx @@ -25,6 +25,10 @@ import FollowPerson from "../components/templates/FollowPerson"; import ProductionCapacity from "../components/ui/analysis/ProductionCapacity"; import ThroughputSummary from "../components/ui/analysis/ThroughputSummary"; import ROISummary from "../components/ui/analysis/ROISummary"; +import Footer from "../components/ui/footer/Footer"; +import RenderOverlay from "../components/templates/Overlay"; +import LogList from "../components/ui/log/LogList"; +import { useLogger } from "../components/ui/log/LoggerContext"; const Project: React.FC = () => { let navigate = useNavigate(); @@ -58,6 +62,7 @@ const Project: React.FC = () => { const { isPlaying } = usePlayButtonStore(); const { toggleThreeD } = useThreeDStore(); const { selectedUser } = useSelectedUserStore(); + const { isLogListVisible } = useLogger(); return (
@@ -69,7 +74,7 @@ const Project: React.FC = () => {
*/} - {loadingProgress > 0 && } + {/* {loadingProgress > 0 && } */} {!isPlaying && ( <> {toggleThreeD && } @@ -85,7 +90,13 @@ const Project: React.FC = () => { {activeModule !== "market" && } {isPlaying && activeModule === "simulation" && } {/* {} */} + {isLogListVisible && ( + + + + )} {selectedUser && } +
); }; diff --git a/app/src/styles/components/footer/footer.scss b/app/src/styles/components/footer/footer.scss new file mode 100644 index 0000000..a595db0 --- /dev/null +++ b/app/src/styles/components/footer/footer.scss @@ -0,0 +1,56 @@ +.footer-wrapper { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + z-index: 1; + display: flex; + justify-content: space-between; + + .selection-wrapper { + display: flex; + gap: 6px; + + .selector-wrapper { + display: flex; + gap: 6px; + align-items: center; + background: var(--background-color); + padding: 4px 6px; + border-radius: 12px; + color: var(--text-color); + + .selector { + color: var(--text-button-color); + font-weight: 200; + } + } + } + + .logs-wrapper { + display: flex; + gap: 6px; + + .logs-detail, + .version { + border-radius: 12px; + background: var(--background-color); + padding: 4px 6px; + color: var(--text-button-color); + font-weight: 200; + display: flex; + align-items: center; + } + + .logs-detail { + background-color: #fff; + cursor: pointer; + } + + .version { + display: flex; + gap: 6px; + + } + } +} \ No newline at end of file diff --git a/app/src/styles/components/logs/logs.scss b/app/src/styles/components/logs/logs.scss new file mode 100644 index 0000000..99e7548 --- /dev/null +++ b/app/src/styles/components/logs/logs.scss @@ -0,0 +1,75 @@ +.log-list-container { + width: 100vw; + height: 100vh; + background: var(--background-color-secondary); + backdrop-filter: blur(2px); + + .log-list-wrapper { + height: 50%; + min-width: 50%; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 5; + background: var(--background-color); + padding: 14px 12px; + border-radius: 15px; + display: flex; + flex-direction: column; + gap: 12px; + + .log-header { + display: flex; + justify-content: space-between; + + .log-header-wrapper { + display: flex; + align-items: center; + gap: 6px; + } + + .close { + cursor: pointer; + } + } + + .log-nav-wrapper { + display: flex; + gap: 6px; + + .log-nav { + padding: 8px 16px; + border-radius: 19px; + } + + .log-nav.active { + background-color: var(--accent-color); + color: var(--text-button-color); + } + } + + .log-entry-wrapper { + height: 100%; + display: flex; + flex-direction: column; + gap: 4px; + background: var(--background-color); + padding: 18px 10px; + border-radius: 16px; + + .log-entry { + padding: 4px; + border-radius: 4px; + font-size: var(--font-size-small); + display: flex; + align-items: center; + gap: 6px; + + &:nth-child(odd) { + background: var(--background-color); + } + } + } + } +} \ No newline at end of file diff --git a/app/src/styles/components/simulation/simulation.scss b/app/src/styles/components/simulation/simulation.scss index ec53e6f..61655e4 100644 --- a/app/src/styles/components/simulation/simulation.scss +++ b/app/src/styles/components/simulation/simulation.scss @@ -312,6 +312,22 @@ padding: 14px 6px; position: relative; + .timmer { + width: auto; + position: absolute; + bottom: 0; + font-size: var(--font-size-tiny); + } + + .start-displayer { + left: 8px; + } + + .end-displayer { + width: auto; + right: 8px; + } + .process-player { position: absolute; top: 50%; @@ -344,6 +360,11 @@ .simulation-player-container.open { + .start-displayer, + .end-displayer { + display: none; + } + .progresser-wrapper { padding-top: 4px; } diff --git a/app/src/styles/layout/sidebar.scss b/app/src/styles/layout/sidebar.scss index c7f1694..f4ccaa8 100644 --- a/app/src/styles/layout/sidebar.scss +++ b/app/src/styles/layout/sidebar.scss @@ -420,7 +420,7 @@ outline: none; path { stroke: var(--text-button-color); - stroke-width: 1.3; + strokeWidth: 1.3; } } } @@ -686,7 +686,7 @@ path { stroke: var(--accent-color); - stroke-width: 1.5px; + strokeWidth: 1.5px; } &:hover { diff --git a/app/src/styles/main.scss b/app/src/styles/main.scss index 66a60e7..9fe7222 100644 --- a/app/src/styles/main.scss +++ b/app/src/styles/main.scss @@ -27,6 +27,8 @@ @use 'components/confirmationPopUp'; @use 'components/analysis/analysis'; @use 'components/analysis/ROISummary.scss'; +@use 'components/logs/logs'; +@use 'components/footer/footer.scss'; // layout @use 'layout/loading'; diff --git a/app/src/styles/pages/realTimeViz.scss b/app/src/styles/pages/realTimeViz.scss index 7348eff..b5fff7e 100644 --- a/app/src/styles/pages/realTimeViz.scss +++ b/app/src/styles/pages/realTimeViz.scss @@ -416,7 +416,7 @@ path { stroke: var(--text-color); - stroke-width: 2; + strokeWidth: 2; } } @@ -428,7 +428,7 @@ path { stroke: #f65648; - stroke-width: 1.3; + strokeWidth: 1.3; }