diff --git a/client/src/App/App.container.jsx b/client/src/App/App.container.jsx index 6702808d4..42212ca66 100644 --- a/client/src/App/App.container.jsx +++ b/client/src/App/App.container.jsx @@ -2,15 +2,19 @@ import { ApolloProvider } from "@apollo/client"; import { SplitFactoryProvider, useSplitClient } from "@splitsoftware/splitio-react"; import { ConfigProvider } from "antd"; import enLocale from "antd/es/locale/en_US"; -import { useEffect } from "react"; +import { useEffect, useMemo } from "react"; import { useTranslation } from "react-i18next"; -import { useSelector } from "react-redux"; +import { connect, useSelector } from "react-redux"; import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component"; import client from "../utils/GraphQLClient"; import App from "./App"; import * as Sentry from "@sentry/react"; -import themeProvider from "./themeProvider"; +import getTheme from "./themeProvider"; import { CookiesProvider } from "react-cookie"; +import { createStructuredSelector } from "reselect"; +import { selectCurrentUser } from "../redux/user/user.selectors.js"; +import { selectDarkMode } from "../redux/application/application.selectors"; +import { setDarkMode } from "../redux/application/application.actions"; // Base Split configuration const config = { @@ -24,19 +28,54 @@ const config = { function SplitClientProvider({ children }) { const imexshopid = useSelector((state) => state.user.imexshopid); // Access imexshopid from Redux store const splitClient = useSplitClient({ key: imexshopid || "anon" }); // Use imexshopid or fallback to "anon" - useEffect(() => { if (splitClient && imexshopid) { - // Log readiness for debugging; no need for ready() since isReady is available console.log(`Split client initialized with key: ${imexshopid}, isReady: ${splitClient.isReady}`); } }, [splitClient, imexshopid]); - return children; } -function AppContainer() { +const mapDispatchToProps = (dispatch) => ({ + setDarkMode: (isDarkMode) => dispatch(setDarkMode(isDarkMode)) +}); + +const mapStateToProps = createStructuredSelector({ + currentUser: selectCurrentUser +}); + +function AppContainer({ currentUser, setDarkMode }) { const { t } = useTranslation(); + const isDarkMode = useSelector(selectDarkMode); + const theme = useMemo(() => getTheme(isDarkMode), [isDarkMode]); + + // Update data-theme attribute when dark mode changes + useEffect(() => { + document.documentElement.setAttribute("data-theme", isDarkMode ? "dark" : "light"); + return () => document.documentElement.removeAttribute("data-theme"); + }, [isDarkMode]); + + // Sync Redux darkMode with localStorage on user change + useEffect(() => { + if (currentUser?.uid) { + const savedMode = localStorage.getItem(`dark-mode-${currentUser.uid}`); + if (savedMode !== null) { + setDarkMode(JSON.parse(savedMode)); + } else { + setDarkMode(false); // default to light mode + } + } else { + setDarkMode(false); + } + // eslint-disable-next-line + }, [currentUser?.uid]); + + // Persist darkMode to localStorage when it or user changes + useEffect(() => { + if (currentUser?.uid) { + localStorage.setItem(`dark-mode-${currentUser.uid}`, JSON.stringify(isDarkMode)); + } + }, [isDarkMode, currentUser?.uid]); return ( @@ -44,11 +83,10 @@ function AppContainer() { @@ -64,4 +102,4 @@ function AppContainer() { ); } -export default Sentry.withProfiler(AppContainer); +export default Sentry.withProfiler(connect(mapStateToProps, mapDispatchToProps)(AppContainer)); diff --git a/client/src/App/App.styles.scss b/client/src/App/App.styles.scss index 898228880..5ca90f244 100644 --- a/client/src/App/App.styles.scss +++ b/client/src/App/App.styles.scss @@ -1,15 +1,225 @@ -//Global Styles. +:root { + --table-stripe-bg: #f4f4f4; /* Light mode table stripe */ + --menu-divider-color: #74695c; /* Light mode menu divider */ + --menu-submenu-text: rgba(255, 255, 255, 0.65); /* Light mode submenu text */ + --kanban-column-bg: #ddd; /* Light mode kanban column */ + --alert-color: blue; /* Light mode alert */ + --completion-soon-color: rgba(255, 140, 0, 0.8); /* Light mode completion soon */ + --completion-past-color: rgba(255, 0, 0, 0.8); /* Light mode completion past */ + --job-line-manual-color: tomato; /* Light mode job line manual */ + --muted-button-color: lightgray; /* Light mode muted button */ + --muted-button-hover-color: darkgrey; /* Light mode muted button hover */ + --table-border-color: #ddd; /* Light mode table border */ + --table-hover-bg: #f5f5f5; /* Light mode table hover */ + --popover-bg: #fff; /* Light mode popover background */ + --error-text: red; /* Light mode error message */ + --no-jobs-text: #888; /* Light mode no jobs message */ + --message-yours-bg: #eee; /* Light mode yours message background */ + --message-mine-bg-start: #00d0ea; /* Light mode mine message gradient start */ + --message-mine-bg-end: #0085d1; /* Light mode mine message gradient end */ + --message-mine-text: white; /* Light mode mine message text */ + --message-mine-tail-bg: white; /* Light mode mine/yours message tail */ + --system-message-bg: #f5f5f5; /* Light mode system message background */ + --system-message-text: #555; /* Light mode system message text */ + --system-label-text: #888; /* Light mode system label/date text */ + --message-icon-color: whitesmoke; /* Light mode message icon */ + --eula-card-bg: lightgray; /* Light mode eula card background */ + --notification-bg: #fff; /* Light mode notification background */ + --notification-text: rgba(0, 0, 0, 0.85); /* Light mode notification text */ + --notification-border: #d9d9d9; /* Light mode notification border */ + --notification-header-bg: #fafafa; /* Light mode notification header background */ + --notification-header-border: #f0f0f0; /* Light mode notification header border */ + --notification-header-text: rgba(0, 0, 0, 0.85); /* Light mode notification header text */ + --notification-toggle-icon: #1677ff; /* Light mode notification toggle icon */ + --notification-switch-bg: #1677ff; /* Light mode notification switch background */ + --notification-btn-link: #1677ff; /* Light mode notification link button */ + --notification-btn-link-hover: #69b1ff; /* Light mode notification link button hover */ + --notification-btn-link-disabled: rgba(0, 0, 0, 0.25); /* Light mode notification link button disabled */ + --notification-btn-link-active: #0958d9; /* Light mode notification link button active */ + --notification-read-bg: #fff; /* Light mode notification read background */ + --notification-read-text: rgba(0, 0, 0, 0.65); /* Light mode notification read text */ + --notification-unread-bg: #f5f5f5; /* Light mode notification unread background */ + --notification-unread-text: rgba(0, 0, 0, 0.85); /* Light mode notification unread text */ + --notification-item-hover-bg: #fafafa; /* Light mode notification item hover background */ + --notification-ro-number: #1677ff; /* Light mode notification RO number */ + --notification-relative-time: rgba(0, 0, 0, 0.45); /* Light mode notification relative time */ + --alert-bg: #fff1f0; /* Light mode alert background */ + --alert-text: rgba(0, 0, 0, 0.85); /* Light mode alert text */ + --alert-border: #ffa39e; /* Light mode alert border */ + --alert-message: #ff4d4f; /* Light mode alert message */ + --share-badge-bg: #cccccc; /* Light mode share badge background */ + --column-header-bg: #d0d0d0; /* Light mode column header background */ + --footer-bg: #d0d0d0; /* Light mode footer background */ + --tech-icon-color: orangered; /* Light mode tech icon color */ + --clone-border-color: #1890ff; /* Light mode clone border color */ + --event-arrived-bg: rgba(4, 141, 4, 0.4); /* Light mode arrived event background */ + --event-block-bg: rgba(212, 2, 2, 0.6); /* Light mode block event background */ + --event-selected-bg: slategrey; /* Light mode selected event background */ + --task-bg: #fff; /* Light mode task center background */ + --task-text: rgba(0, 0, 0, 0.85); /* Light mode task text */ + --task-border: #d9d9d9; /* Light mode task border */ + --task-header-bg: #fafafa; /* Light mode task header background */ + --task-header-border: #f0f0f0; /* Light mode task header border */ + --task-section-bg: #f5f5f5; /* Light mode task section background */ + --task-section-border: #e8e8e8; /* Light mode task section border */ + --task-row-hover-bg: #f5f5f5; /* Light mode task row hover background */ + --task-row-border: #f0f0f0; /* Light mode task row border */ + --task-ro-number: #1677ff; /* Light mode task RO number */ + --task-due-text: rgba(0, 0, 0, 0.45); /* Light mode task due text */ + --task-button-bg: #1677ff; /* Light mode task button background */ + --task-button-hover-bg: #4096ff; /* Light mode task button hover background */ + --task-button-disabled-bg: #d9d9d9; /* Light mode task button disabled background */ + --task-button-text: white; /* Light mode task button text */ + --task-message-text: rgba(0, 0, 0, 0.45); /* Light mode task message text */ + --mask-bg: rgba(0, 0, 0, 0.05); /* Light mode mask background */ + --board-text-color: #393939; /* Light mode board text color */ + --section-bg: #e3e3e3; /* Light mode section background */ + --detail-text-color: #4d4d4d; /* Light mode detail text color */ + --card-selected-bg: rgba(128, 128, 128, 0.2); /* Light mode selected card background */ + --card-stripe-even-bg: #f0f2f5; /* Light mode even card background */ + --card-stripe-odd-bg: #ffffff; /* Light mode odd card background */ + --bar-border-color: #f0f2f5; /* Light mode bar border and background */ + --tag-wrapper-bg: #f0f2f5; /* Light mode tag wrapper background */ + --tag-wrapper-text: #000; /* Light mode tag wrapper text */ + --preview-bg: lightgray; /* Light mode preview background */ + --preview-border-color: #2196F3; /* Light mode preview border color */ + --event-bg-fallback: #ffffff; /* Light mode event background fallback */ + --card-bg-fallback: #ffffff; /* Light mode card background fallback */ + --card-text-fallback: black; /* Light mode card text fallback */ + --table-row-even-bg: rgb(236, 236, 236); /* Light mode table row even background */ + --status-row-bg-fallback: #ffffff; /* Light mode status row fallback background */ + --reset-link-color: #0000ff; /* Light mode reset link color */ + --error-header-text: tomato; /* Light mode error header text */ + --tooltip-bg: white; /* Light mode tooltip background */ + --tooltip-border: gray; /* Light mode tooltip border */ + --tooltip-text-fallback: black; /* Light mode tooltip text fallback */ + --teams-button-bg: #6264A7; /* Light mode Teams button background */ + --teams-button-border: #6264A7; /* Light mode Teams button border */ + --teams-button-text: #FFFFFF; /* Light mode Teams button text and icon */ + --content-bg: #fff; /* Light mode content background */ + --legend-bg-fallback: #ffffff; /* Light mode legend background fallback */ + --tech-content-bg: #fff; /* Light mode tech content background */ + --today-bg: #ffffff; /* Light mode today background */ + --today-text: #000000; /* Light mode today text */ + --off-range-bg: #f8f8f8; /* Light mode off-range background */ +} + +[data-theme="dark"] { + --table-stripe-bg: #2a2a2a; /* Dark mode table stripe */ + --menu-divider-color: #5c5c5c; /* Dark mode menu divider */ + --menu-submenu-text: rgba(255, 255, 255, 0.85); /* Dark mode submenu text */ + --kanban-column-bg: #333333; /* Dark mode kanban column */ + --alert-color: #4da8ff; /* Dark mode alert */ + --completion-soon-color: #ff8c1a; /* Dark mode completion soon */ + --completion-past-color: #ff4d4f; /* Dark mode completion past */ + --job-line-manual-color: #ff6347; /* Dark mode job line manual */ + --muted-button-color: #666666; /* Dark mode muted button */ + --muted-button-hover-color: #999999; /* Dark mode muted button hover */ + --table-border-color: #5c5c5c; /* Dark mode table border */ + --table-hover-bg: #2a2a2a; /* Dark mode table hover */ + --popover-bg: #2a2a2a; /* Dark mode popover background */ + --error-text: #ff4d4f; /* Dark mode error message */ + --no-jobs-text: #999999; /* Dark mode no jobs message */ + --message-yours-bg: #2a2a2a; /* Dark mode yours message background */ + --message-mine-bg-start: #4da8ff; /* Dark mode mine message gradient start */ + --message-mine-bg-end: #326ade; /* Dark mode mine message gradient end */ + --message-mine-text: #ffffff; /* Dark mode mine message text */ + --message-mine-tail-bg: #1f1f1f; /* Dark mode mine/yours message tail */ + --system-message-bg: #333333; /* Dark mode system message background */ + --system-message-text: #cccccc; /* Dark mode system message text */ + --system-label-text: #999999; /* Dark mode system label/date text */ + --message-icon-color: #cccccc; /* Dark mode message icon */ + --eula-card-bg: #2a2a2a; /* Dark mode eula card background */ + --notification-bg: #2a2a2a; /* Dark mode notification background */ + --notification-text: rgba(255, 255, 255, 0.85); /* Dark mode notification text */ + --notification-border: #5c5c5c; /* Dark mode notification border */ + --notification-header-bg: #333333; /* Dark mode notification header background */ + --notification-header-border: #444444; /* Dark mode notification header border */ + --notification-header-text: rgba(255, 255, 255, 0.85); /* Dark mode notification header text */ + --notification-toggle-icon: #4da8ff; /* Dark mode notification toggle icon */ + --notification-switch-bg: #4da8ff; /* Dark mode notification switch background */ + --notification-btn-link: #4da8ff; /* Dark mode notification link button */ + --notification-btn-link-hover: #80c1ff; /* Dark mode notification link button hover */ + --notification-btn-link-disabled: rgba(255, 255, 255, 0.25); /* Dark mode notification link button disabled */ + --notification-btn-link-active: #2681ff; /* Dark mode notification link button active */ + --notification-read-bg: #2a2a2a; /* Dark mode notification read background */ + --notification-read-text: rgba(255, 255, 255, 0.65); /* Dark mode notification read text */ + --notification-unread-bg: #333333; /* Dark mode notification unread background */ + --notification-unread-text: rgba(255, 255, 255, 0.85); /* Dark mode notification unread text */ + --notification-item-hover-bg: #3a3a3a; /* Dark mode notification item hover background */ + --notification-ro-number: #4da8ff; /* Dark mode notification RO number */ + --notification-relative-time: rgba(255, 255, 255, 0.45); /* Dark mode notification relative time */ + --alert-bg: #3a1a1a; /* Dark mode alert background */ + --alert-text: rgba(255, 255, 255, 0.85); /* Dark mode alert text */ + --alert-border: #ff6666; /* Dark mode alert border */ + --alert-message: #ff6666; /* Dark mode alert message */ + --share-badge-bg: #666666; /* Dark mode share badge background */ + --column-header-bg: #333333; /* Dark mode column header background */ + --footer-bg: #333333; /* Dark mode footer background */ + --tech-icon-color: #ff4500; /* Dark mode tech icon color */ + --clone-border-color: #4da8ff; /* Dark mode clone border color */ + --event-arrived-bg: rgba(4, 141, 4, 0.6); /* Dark mode arrived event background */ + --event-block-bg: rgba(212, 2, 2, 0.8); /* Dark mode block event background */ + --event-selected-bg: #4a5e6e; /* Dark mode selected event background */ + --task-bg: #2a2a2a; /* Dark mode task center background */ + --task-text: rgba(255, 255, 255, 0.85); /* Dark mode task text */ + --task-border: #5c5c5c; /* Dark mode task border */ + --task-header-bg: #333333; /* Dark mode task header background */ + --task-header-border: #444444; /* Dark mode task header border */ + --task-section-bg: #333333; /* Dark mode task section background */ + --task-section-border: #444444; /* Dark mode task section border */ + --task-row-hover-bg: #3a3a3a; /* Dark mode task row hover background */ + --task-row-border: #444444; /* Dark mode task row border */ + --task-ro-number: #4da8ff; /* Dark mode task RO number */ + --task-due-text: rgba(255, 255, 255, 0.45); /* Dark mode task due text */ + --task-button-bg: #4da8ff; /* Dark mode task button background */ + --task-button-hover-bg: #80c1ff; /* Dark mode task button hover background */ + --task-button-disabled-bg: #666666; /* Dark mode task button disabled background */ + --task-button-text: #ffffff; /* Dark mode task button text */ + --task-message-text: rgba(255, 255, 255, 0.45); /* Dark mode task message text */ + --mask-bg: rgba(255, 255, 255, 0.05); /* Dark mode mask background */ + --board-text-color: #cccccc; /* Dark mode board text color */ + --section-bg: #333333; /* Dark mode section background */ + --detail-text-color: #bbbbbb; /* Dark mode detail text color */ + --card-selected-bg: rgba(255, 255, 255, 0.1); /* Dark mode selected card background */ + --card-stripe-even-bg: #2a2a2a; /* Dark mode even card background */ + --card-stripe-odd-bg: #1f1f1f; /* Dark mode odd card background */ + --bar-border-color: #2a2a2a; /* Dark mode bar border and background */ + --tag-wrapper-bg: #2a2a2a; /* Dark mode tag wrapper background */ + --tag-wrapper-text: #cccccc; /* Dark mode tag wrapper text */ + --preview-bg: #2a2a2a; /* Dark mode preview background */ + --preview-border-color: #4da8ff; /* Dark mode preview border color */ + --event-bg-fallback: #2a2a2a; /* Dark mode event background fallback */ + --card-bg-fallback: #2a2a2a; /* Dark mode card background fallback */ + --card-text-fallback: #cccccc; /* Dark mode card text fallback */ + --table-row-even-bg: #2a2a2a; /* Dark mode table row even background */ + --status-row-bg-fallback: #1f1f1f; /* Dark mode status row fallback background */ + --reset-link-color: #4da8ff; /* Dark mode reset link color */ + --error-header-text: #ff6347; /* Dark mode error header text */ + --tooltip-bg: #2a2a2a; /* Dark mode tooltip background */ + --tooltip-border: #5c5c5c; /* Dark mode tooltip border */ + --tooltip-text-fallback: #cccccc; /* Dark mode tooltip text fallback */ + --teams-button-bg: #7b7dc4; /* Dark mode Teams button background */ + --teams-button-border: #7b7dc4; /* Dark mode Teams button border */ + --teams-button-text: #ffffff; /* Dark mode Teams button text and icon */ + --content-bg: #2a2a2a; /* Dark mode content background */ + --legend-bg-fallback: #2a2a2a; /* Dark mode legend background fallback */ + --tech-content-bg: #2a2a2a; /* Dark mode tech content background */ + --today-bg: #4a5e6e; /* Dark mode today background */ + --today-text: #ffffff; /* Dark mode today text */ + --off-range-bg: #333333; /* Dark mode off-range background */ +} + +// Global Styles @import "react-big-calendar/lib/sass/styles"; .ant-menu-item-divider { - border-bottom: 1px solid #74695c !important; + border-bottom: 1px solid var(--menu-divider-color) !important; } -// TODO: This was added because the newest release of ant was making the text color and the background color the same on a selected header -// Tried all available tokens (https://ant.design/components/menu?locale=en-US) and even reverted all our custom styles, to no avail -// This should be kept an eye on, especially if implementing DARK MODE +// Note: Monitor this in dark mode to ensure text visibility .ant-menu-submenu-title { - color: rgba(255, 255, 255, 0.65) !important; + color: var(--menu-submenu-text) !important; } .imex-table-header { @@ -46,7 +256,7 @@ } .ellipses { - display: inline-block; /* for em, a, span, etc (inline by default) */ + display: inline-block; text-overflow: ellipsis; width: calc(95%); overflow: hidden; @@ -60,23 +270,24 @@ } } -// ::-webkit-scrollbar-track { -// -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); -// border-radius: 0.2rem; -// background-color: #f5f5f5; -// } +// Scrollbar styles (uncomment if needed, updated for dark mode) +::-webkit-scrollbar-track { + -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); + border-radius: 0.2rem; + background-color: var(--table-stripe-bg); +} -// ::-webkit-scrollbar { -// width: 0.25rem; -// max-height: 0.25rem; -// background-color: #f5f5f5; -// } +::-webkit-scrollbar { + width: 0.25rem; + max-height: 0.25rem; + background-color: var(--table-stripe-bg); +} -// ::-webkit-scrollbar-thumb { -// border-radius: 0.2rem; -// -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); -// background-color: #188fff; -// } +::-webkit-scrollbar-thumb { + border-radius: 0.2rem; + -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); + background-color: var(--alert-color); +} .ant-input-number-input, .ant-input-number, @@ -88,28 +299,27 @@ .production-alert { animation: alertBlinker 1s linear infinite; - color: blue; + color: var(--alert-color); } @keyframes alertBlinker { 50% { - color: red; + color: var(--completion-past-color); opacity: 100; - //opacity: 0; } } .blue { - color: blue; + color: var(--alert-color); } .production-completion-soon { - color: rgba(255, 140, 0, 0.8); + color: var(--completion-soon-color); font-weight: bold; } .production-completion-past { - color: rgba(255, 0, 0, 0.8); + color: var(--completion-past-color); font-weight: bold; } @@ -139,7 +349,7 @@ } .react-kanban-column { - background-color: #ddd !important; + background-color: var(--kanban-column-bg) !important; } .production-list-table { @@ -151,18 +361,18 @@ .ReactGridGallery_tile-icon-bar { div { svg { - fill: #1890ff; + fill: var(--alert-color); } } } .job-line-manual { - color: tomato; + color: var(--job-line-manual-color); font-style: italic; } .ant-table-tbody > tr.ant-table-row:nth-child(2n) > td { - background-color: #f4f4f4; + background-color: var(--table-stripe-bg); } .rowWithColor > td { @@ -170,15 +380,15 @@ } .muted-button { - color: lightgray; + color: var(--muted-button-color); border: none; background: none; cursor: pointer; - font-size: 16px; /* Adjust as needed */ + font-size: 16px; } .muted-button:hover { - color: darkgrey; + color: var(--muted-button-hover-color); } .notification-alert-unordered-list { @@ -194,3 +404,19 @@ .content-container { padding: 1rem; } + +// Override react-big-calendar styles for dark mode only +[data-theme="dark"] { + .rbc-today { + background-color: var(--today-bg); + color: var(--today-text); + } + + .rbc-off-range { + background-color: var(--off-range-bg); + } + + .rbc-day-bg.rbc-today { + background-color: var(--today-bg); + } +} diff --git a/client/src/App/themeProvider.js b/client/src/App/themeProvider.js index 5aaaef4cf..06ea72e18 100644 --- a/client/src/App/themeProvider.js +++ b/client/src/App/themeProvider.js @@ -4,36 +4,42 @@ import InstanceRenderMgr from "../utils/instanceRenderMgr"; const { defaultAlgorithm, darkAlgorithm } = theme; -let isDarkMode = false; - /** * Default theme * @type {{components: {Menu: {itemDividerBorderColor: string}}}} */ -const defaultTheme = { +const defaultTheme = (isDarkMode) => ({ components: { Table: { - rowHoverBg: "#e7f3ff", - rowSelectedBg: "#e6f7ff", + rowHoverBg: isDarkMode ? "#2a2a2a" : "#e7f3ff", + rowSelectedBg: isDarkMode ? "#333333" : "#e6f7ff", headerSortHoverBg: "transparent" }, Menu: { - darkItemHoverBg: "#1890ff", - itemHoverBg: "#1890ff", - horizontalItemHoverBg: "#1890ff" + darkItemHoverBg: isDarkMode ? "#004a77" : "#1890ff", + itemHoverBg: isDarkMode ? "#004a77" : "#1890ff", + horizontalItemHoverBg: isDarkMode ? "#004a77" : "#1890ff" } }, token: { - colorPrimary: InstanceRenderMgr({ - imex: "#1890ff", - rome: "#326ade" - }), - colorInfo: InstanceRenderMgr({ - imex: "#1890ff", - rome: "#326ade" - }) + colorPrimary: InstanceRenderMgr( + { + imex: isDarkMode ? "#4da8ff" : "#1890ff", + rome: isDarkMode ? "#5b8ce6" : "#326ade" + }, + isDarkMode + ), + colorInfo: InstanceRenderMgr( + { + imex: isDarkMode ? "#4da8ff" : "#1890ff", + rome: isDarkMode ? "#5b8ce6" : "#326ade" + }, + isDarkMode + ), + colorError: isDarkMode ? "#ff4d4f" : "#f5222d", + colorBgBase: isDarkMode ? "#1f1f1f" : "#ffffff" // Align with Ant Design dark mode } -}; +}); /** * Development theme @@ -60,8 +66,9 @@ const prodTheme = {}; const currentTheme = import.meta.env.DEV ? devTheme : prodTheme; -const finaltheme = { +const getTheme = (isDarkMode) => ({ algorithm: isDarkMode ? darkAlgorithm : defaultAlgorithm, ...defaultsDeep(currentTheme, defaultTheme) -}; -export default finaltheme; +}); + +export default getTheme; diff --git a/client/src/components/bill-cm-returns-table/bill-cm-returns-table.styles.scss b/client/src/components/bill-cm-returns-table/bill-cm-returns-table.styles.scss index 743ef7ac0..8f1c19835 100644 --- a/client/src/components/bill-cm-returns-table/bill-cm-returns-table.styles.scss +++ b/client/src/components/bill-cm-returns-table/bill-cm-returns-table.styles.scss @@ -6,7 +6,7 @@ td { padding: 8px; text-align: left; - border-bottom: 1px solid #ddd; + border-bottom: 1px solid var(--table-border-color); .ant-form-item { margin-bottom: 0px !important; @@ -14,6 +14,6 @@ } tr:hover { - background-color: #f5f5f5; + background-color: var(--table-hover-bg); } } diff --git a/client/src/components/bill-inventory-table/bill-inventory-table.styles.scss b/client/src/components/bill-inventory-table/bill-inventory-table.styles.scss index 173714f41..d06ad316a 100644 --- a/client/src/components/bill-inventory-table/bill-inventory-table.styles.scss +++ b/client/src/components/bill-inventory-table/bill-inventory-table.styles.scss @@ -6,7 +6,7 @@ td { padding: 8px; text-align: left; - border-bottom: 1px solid #ddd; + border-bottom: 1px solid var(--table-border-color); .ant-form-item { margin-bottom: 0px !important; @@ -14,6 +14,6 @@ } tr:hover { - background-color: #f5f5f5; + background-color: var(--table-hover-bg); } } diff --git a/client/src/components/chat-conversation-list/chat-conversation-list.component.jsx b/client/src/components/chat-conversation-list/chat-conversation-list.component.jsx index e5fe4b9ca..72f8be33c 100644 --- a/client/src/components/chat-conversation-list/chat-conversation-list.component.jsx +++ b/client/src/components/chat-conversation-list/chat-conversation-list.component.jsx @@ -29,9 +29,7 @@ const mapDispatchToProps = (dispatch) => ({ function ChatConversationListComponent({ conversationList, selectedConversation, setSelectedConversation, bodyshop }) { const { t } = useTranslation(); const [, forceUpdate] = useState(false); - const phoneNumbers = conversationList.map((item) => phone(item.phone_num, "CA").phoneNumber.replace(/^\+1/, "")); - const { data: optOutData } = useQuery(GET_PHONE_NUMBER_OPT_OUTS, { variables: { bodyshopid: bodyshop.id, @@ -64,15 +62,12 @@ function ChatConversationListComponent({ conversationList, selectedConversation, const item = sortedConversationList[index]; const normalizedPhone = phone(item.phone_num, "CA").phoneNumber.replace(/^\+1/, ""); const hasOptOutEntry = optOutMap.has(normalizedPhone); - const cardContentRight = {item.updated_at}; const cardContentLeft = item.job_conversations.length > 0 ? item.job_conversations.map((j, idx) => {j.job.ro_number}) : null; - const names = <>{_.uniq(item.job_conversations.map((j, idx) => OwnerNameDisplayFunction(j.job)))}; - const cardTitle = ( <> {item.label && {item.label}} @@ -85,7 +80,6 @@ function ChatConversationListComponent({ conversationList, selectedConversation, )} ); - const cardExtra = ( <> @@ -98,11 +92,10 @@ function ChatConversationListComponent({ conversationList, selectedConversation, )} ); - const getCardStyle = () => item.id === selectedConversation - ? { backgroundColor: "rgba(128, 128, 128, 0.2)" } - : { backgroundColor: index % 2 === 0 ? "#f0f2f5" : "#ffffff" }; + ? { backgroundColor: "var(--card-selected-bg)" } + : { backgroundColor: index % 2 === 0 ? "var(--card-stripe-even-bg)" : "var(--card-stripe-odd-bg)" }; return ( ; const extra = `${t("job_lifecycle.content.calculated_based_on")} ${lifecycleData.jobs} ${t("job_lifecycle.content.jobs_in_since")} ${fortyFiveDaysAgo()}`; @@ -88,7 +87,7 @@ export default function JobLifecycleDashboardComponent({ data, bodyshop, ...card borderRadius: "5px", borderWidth: "5px", borderStyle: "solid", - borderColor: "#f0f2f5", + borderColor: "var(--bar-border-color)", margin: 0, padding: 0 }} @@ -107,12 +106,10 @@ export default function JobLifecycleDashboardComponent({ data, bodyshop, ...card alignItems: "center", margin: 0, padding: 0, - - borderTop: "1px solid #f0f2f5", - borderBottom: "1px solid #f0f2f5", - borderLeft: isFirst ? "1px solid #f0f2f5" : undefined, - borderRight: isLast ? "1px solid #f0f2f5" : undefined, - + borderTop: "1px solid var(--bar-border-color)", + borderBottom: "1px solid var(--bar-border-color)", + borderLeft: isFirst ? "1px solid var(--bar-border-color)" : undefined, + borderRight: isLast ? "1px solid var(--bar-border-color)" : undefined, backgroundColor: key.color, width: `${key.percentage}%` }} @@ -124,7 +121,7 @@ export default function JobLifecycleDashboardComponent({ data, bodyshop, ...card
{key.roundedPercentage}
({ + +const mapDispatchToProps = () => ({ //setUserLanguage: language => dispatch(setUserLanguage(language)) }); + export default connect(mapStateToProps, mapDispatchToProps)(EmailOverlayComponent); export function EmailOverlayComponent({ emailConfig, form, selectedMediaState, bodyshop, currentUser }) { const { t } = useTranslation(); - const handleClick = ({ item, key, keyPath }) => { + + const handleClick = ({ item }) => { const email = item.props.value; form.setFieldsValue({ to: _.uniq([...form.getFieldValue("to"), ...(typeof email === "string" ? [email] : email)]) }); }; - const handle_CC_Click = ({ item, key, keyPath }) => { + + const handle_CC_Click = ({ item }) => { const email = item.props.value; form.setFieldsValue({ cc: _.uniq([...(form.getFieldValue("cc") || ""), ...(typeof email === "string" ? [email] : email)]) @@ -52,6 +55,7 @@ export function EmailOverlayComponent({ emailConfig, form, selectedMediaState, b ], onClick: handleClick }; + const menuCC = { items: [ ...bodyshop.employees @@ -136,26 +140,22 @@ export function EmailOverlayComponent({ emailConfig, form, selectedMediaState, b > - {t("emails.labels.preview")} {bodyshop.attach_pdf_to_email && {t("emails.labels.pdfcopywillbeattached")}} - {() => { return (
); }} - ({ + () => ({ validator(rule, value) { const totalSize = value.reduce((acc, val) => (acc = acc + val.size), 0); - const limit = 10485760 - new Blob([form.getFieldValue("html")]).size; - if (totalSize > limit) { return Promise.reject(t("general.errors.sizelimit")); } diff --git a/client/src/components/eula/eula.styles.scss b/client/src/components/eula/eula.styles.scss index 1773ffa7a..d3d0fd257 100644 --- a/client/src/components/eula/eula.styles.scss +++ b/client/src/components/eula/eula.styles.scss @@ -5,7 +5,7 @@ .eula-markdown-card { max-height: 50vh; overflow-y: auto; - background-color: lightgray; + background-color: var(--eula-card-bg); } .eula-markdown-div { diff --git a/client/src/components/form-items-formatted/read-only-form-item.component.jsx b/client/src/components/form-items-formatted/read-only-form-item.component.jsx index 65333883a..786d6a1a0 100644 --- a/client/src/components/form-items-formatted/read-only-form-item.component.jsx +++ b/client/src/components/form-items-formatted/read-only-form-item.component.jsx @@ -1,5 +1,4 @@ import Dinero from "dinero.js"; -import { forwardRef } from "react"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { selectBodyshop } from "../../redux/user/user.selectors"; @@ -28,4 +27,4 @@ const ReadOnlyFormItem = ({ bodyshop, value, type = "text" }) => { } }; -export default connect(mapStateToProps, mapDispatchToProps)(forwardRef(ReadOnlyFormItem)); +export default connect(mapStateToProps, mapDispatchToProps)(ReadOnlyFormItem); diff --git a/client/src/components/header/buildLeftMenuItems.jsx b/client/src/components/header/buildLeftMenuItems.jsx index 166779572..e742d33d0 100644 --- a/client/src/components/header/buildLeftMenuItems.jsx +++ b/client/src/components/header/buildLeftMenuItems.jsx @@ -25,7 +25,7 @@ import { UsergroupAddOutlined, UserOutlined } from "@ant-design/icons"; -import { FaCalendarAlt, FaCarCrash, FaTasks } from "react-icons/fa"; +import { FaCalendarAlt, FaCarCrash, FaMoon, FaSun, FaTasks } from "react-icons/fa"; import { BsKanban } from "react-icons/bs"; import { FiLogOut } from "react-icons/fi"; import { GiPlayerTime, GiSettingsKnobs } from "react-icons/gi"; @@ -33,6 +33,7 @@ import { RiSurveyLine } from "react-icons/ri"; import { IoBusinessOutline } from "react-icons/io5"; import InstanceRenderManager from "../../utils/instanceRenderMgr"; import LockWrapper from "../../components/lock-wrapper/lock-wrapper.component.jsx"; +import { Tooltip } from "antd"; const buildLeftMenuItems = ({ t, @@ -41,7 +42,9 @@ const buildLeftMenuItems = ({ setTaskUpsertContext, setReportCenterContext, signOutStart, - accountingChildren + accountingChildren, + handleDarkModeToggle, + darkMode }) => { return [ { @@ -331,6 +334,15 @@ const buildLeftMenuItems = ({ label: t("user.actions.signout"), onClick: () => signOutStart() }, + { + key: "darkmode-toggle", + id: "header-darkmode-toggle", + label: darkMode ? t("user.actions.light_theme") : t("user.actions.dark_theme"), + icon: ( + {darkMode ? : } + ), + onClick: handleDarkModeToggle + }, { key: "help", id: "header-help", diff --git a/client/src/components/header/header.component.jsx b/client/src/components/header/header.component.jsx index 3312bf9f4..c3fcd1342 100644 --- a/client/src/components/header/header.component.jsx +++ b/client/src/components/header/header.component.jsx @@ -12,7 +12,7 @@ import { createStructuredSelector } from "reselect"; import { TASKS_CENTER_POLL_INTERVAL, useSocket } from "../../contexts/SocketIO/useSocket.js"; import { GET_UNREAD_COUNT } from "../../graphql/notifications.queries.js"; import { QUERY_MY_TASKS_COUNT } from "../../graphql/tasks.queries.js"; -import { selectRecentItems, selectSelectedHeader } from "../../redux/application/application.selectors"; +import { selectDarkMode, selectRecentItems, selectSelectedHeader } from "../../redux/application/application.selectors"; import { setModalContext } from "../../redux/modals/modals.actions"; import { signOutStart } from "../../redux/user/user.actions"; import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors"; @@ -22,13 +22,15 @@ import NotificationCenterContainer from "../notification-center/notification-cen import TaskCenterContainer from "../task-center/task-center.container.jsx"; import buildAccountingChildren from "./buildAccountingChildren.jsx"; import buildLeftMenuItems from "./buildLeftMenuItems.jsx"; +import { toggleDarkMode } from "../../redux/application/application.actions"; // --- Redux mappings --- const mapStateToProps = createStructuredSelector({ currentUser: selectCurrentUser, recentItems: selectRecentItems, selectedHeader: selectSelectedHeader, - bodyshop: selectBodyshop + bodyshop: selectBodyshop, + darkMode: selectDarkMode }); const mapDispatchToProps = (dispatch) => ({ @@ -38,7 +40,8 @@ const mapDispatchToProps = (dispatch) => ({ setReportCenterContext: (context) => dispatch(setModalContext({ context, modal: "reportCenter" })), signOutStart: () => dispatch(signOutStart()), setCardPaymentContext: (context) => dispatch(setModalContext({ context, modal: "cardPayment" })), - setTaskUpsertContext: (context) => dispatch(setModalContext({ context, modal: "taskUpsert" })) + setTaskUpsertContext: (context) => dispatch(setModalContext({ context, modal: "taskUpsert" })), + toggleDarkMode: () => dispatch(toggleDarkMode()) }); // --- Utility Hooks --- @@ -84,22 +87,22 @@ function useIncompleteTaskCount(assignedToId, bodyshopId, isEmployee, isConnecte } // --- Main Component --- -function Header(props) { - const { - handleMenuClick, - currentUser, - bodyshop, - selectedHeader, - signOutStart, - setBillEnterContext, - setTimeTicketContext, - setPaymentContext, - setReportCenterContext, - recentItems, - setCardPaymentContext, - setTaskUpsertContext - } = props; - +function Header({ + handleMenuClick, + currentUser, + bodyshop, + selectedHeader, + signOutStart, + setBillEnterContext, + setTimeTicketContext, + setPaymentContext, + setReportCenterContext, + recentItems, + setCardPaymentContext, + setTaskUpsertContext, + toggleDarkMode, + darkMode +}) { // Feature flags const { treatments: { ImEXPay, DmsAp, Simple_Inventory } @@ -216,6 +219,10 @@ function Header(props) { [handleMenuClick] ); + const handleDarkModeToggle = useCallback(() => { + toggleDarkMode(); + }, [toggleDarkMode]); + // --- Menu Items --- // built externally to keep the component clean, but on this level to prevent unnecessary re-renders @@ -257,9 +264,21 @@ function Header(props) { setTaskUpsertContext, setReportCenterContext, signOutStart, - accountingChildren + accountingChildren, + darkMode, + handleDarkModeToggle }), - [t, bodyshop, recentItems, setTaskUpsertContext, setReportCenterContext, signOutStart, accountingChildren] + [ + t, + bodyshop, + recentItems, + setTaskUpsertContext, + setReportCenterContext, + signOutStart, + accountingChildren, + darkMode, + handleDarkModeToggle + ] ); const rightMenuItems = useMemo(() => { @@ -292,6 +311,7 @@ function Header(props) { ), onClick: handleTaskCenterClick }); + return items; }, [ scenarioNotificationsOn, diff --git a/client/src/components/job-at-change/schedule-event.component.jsx b/client/src/components/job-at-change/schedule-event.component.jsx index 9bdacfe26..b9987b48e 100644 --- a/client/src/components/job-at-change/schedule-event.component.jsx +++ b/client/src/components/job-at-change/schedule-event.component.jsx @@ -36,6 +36,7 @@ import ScheduleEventNote from "./schedule-event.note.component"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop }); + const mapDispatchToProps = (dispatch) => ({ setScheduleContext: (context) => dispatch(setModalContext({ context: context, modal: "schedule" })), openChatByPhone: (phone) => dispatch(openChatByPhone(phone)), @@ -64,7 +65,6 @@ export function ScheduleEventComponent({ const notification = useNotification(); const [form] = Form.useForm(); const [popOverVisible, setPopOverVisible] = useState(false); - const [getJobDetails] = useLazyQuery(GET_JOB_BY_PK_QUICK_INTAKE, { variables: { id: event.job?.id }, onCompleted: (data) => { @@ -83,7 +83,6 @@ export function ScheduleEventComponent({ }); } }, - fetchPolicy: "network-only" }); @@ -115,7 +114,6 @@ export function ScheduleEventComponent({ }); }} /> - @@ -133,7 +131,6 @@ export function ScheduleEventComponent({ } } }); - if (!res.errors) { notification["success"]({ message: t("jobs.successes.converted") @@ -180,7 +177,6 @@ export function ScheduleEventComponent({ - )} - {event.isintake ? (