diff --git a/electron/electron-store.js b/electron/electron-store.js index 39aa05f..8428dca 100644 --- a/electron/electron-store.js +++ b/electron/electron-store.js @@ -9,6 +9,7 @@ const storeOptions = { enableNotifications: true, filePaths: [], accepted_ins_co: [], + darkMode: false, runWatcherOnStartup: true, polling: { enabled: false, diff --git a/src/App/App.jsx b/src/App/App.jsx index 53e7531..8f000a0 100644 --- a/src/App/App.jsx +++ b/src/App/App.jsx @@ -41,11 +41,32 @@ function AntdFeedbackBridge() { return null; } +function AppShell({ currentUser }) { + const { token } = theme.useToken(); + + return ( +
+ {currentUser.authorized ? : } +
+ ); +} + export function App({ currentUser, checkUserSession, darkMode }) { useEffect(() => { checkUserSession(); }, [checkUserSession]); + useEffect(() => { + ipcRenderer.send(ipcTypes.store.get, "darkMode"); + }, []); + useEffect(() => { if (currentUser && currentUser.email) { ipcRenderer.send(ipcTypes.app.toMain.setUserName, currentUser.email); @@ -75,7 +96,7 @@ export function App({ currentUser, checkUserSession, darkMode }) { > -
{currentUser.authorized ? : }
+
diff --git a/src/App/App.styles.scss b/src/App/App.styles.scss index d1b15e6..072d510 100644 --- a/src/App/App.styles.scss +++ b/src/App/App.styles.scss @@ -47,23 +47,35 @@ body { overflow: hidden; white-space: nowrap; } -// ::-webkit-scrollbar-track { -// -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); -// border-radius: 0.2rem; -// background-color: #f5f5f5; -// } -// ::-webkit-scrollbar { -// width: 0.25rem; -// max-height: 0.25rem; -// background-color: #f5f5f5; -// } +.imex-app-shell { + scrollbar-color: var(--imex-scrollbar-thumb) var(--imex-scrollbar-track); + scrollbar-width: thin; +} + +.imex-app-shell * { + scrollbar-color: var(--imex-scrollbar-thumb) var(--imex-scrollbar-track); +} + +.imex-app-shell ::-webkit-scrollbar { + width: 12px; + height: 12px; +} + +.imex-app-shell ::-webkit-scrollbar-track { + background-color: var(--imex-scrollbar-track); +} + +.imex-app-shell ::-webkit-scrollbar-thumb { + background-color: var(--imex-scrollbar-thumb); + border: 3px solid var(--imex-scrollbar-track); + border-radius: 999px; +} + +.imex-app-shell ::-webkit-scrollbar-thumb:hover { + background-color: var(--imex-scrollbar-thumb-hover); +} -// ::-webkit-scrollbar-thumb { -// border-radius: 0.2rem; -// -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); -// background-color: #188fff; -// } .jobs-list-container { height: 100%; display: flex; diff --git a/src/components/molecules/estimate-scruber-results/estimate-scrubber-results.molecule.jsx b/src/components/molecules/estimate-scruber-results/estimate-scrubber-results.molecule.jsx index 236251c..dee86fe 100644 --- a/src/components/molecules/estimate-scruber-results/estimate-scrubber-results.molecule.jsx +++ b/src/components/molecules/estimate-scruber-results/estimate-scrubber-results.molecule.jsx @@ -1,310 +1,343 @@ -import { InfoCircleOutlined, LinkOutlined, SearchOutlined } from "@ant-design/icons"; -import { Badge, Button, Collapse, Input, Result, Space, Table, Tag, Typography } from "antd"; +import {FilePdfOutlined, FlagOutlined, LinkOutlined, SearchOutlined} from "@ant-design/icons"; +import {Badge, Button, Collapse, Input, Result, Space, Table, Tag, Typography} from "antd"; import _ from "lodash"; -import { useState } from "react"; -import { connect } from "react-redux"; -import { createStructuredSelector } from "reselect"; -import { selectEsResults } from "../../../redux/application/application.selectors"; -import { selectBodyshop } from "../../../redux/user/user.selectors"; +import {useState} from "react"; +import {connect} from "react-redux"; +import {createStructuredSelector} from "reselect"; +import {selectEsResults} from "../../../redux/application/application.selectors"; +import {selectBodyshop} from "../../../redux/user/user.selectors"; import EstimateScrubberButton from "../estimate-scrubber-button/estimate-scrubber-button.molecule"; const mapStateToProps = createStructuredSelector({ - //currentUser: selectCurrentUser - bodyshop: selectBodyshop, - esResults: selectEsResults + //currentUser: selectCurrentUser + bodyshop: selectBodyshop, + esResults: selectEsResults }); const mapDispatchToProps = (dispatch) => ({ - //setUserLanguage: language => dispatch(setUserLanguage(language)) + //setUserLanguage: language => dispatch(setUserLanguage(language)) }); export default connect(mapStateToProps, mapDispatchToProps)(EstimateScrubberResults); -const { Title, Text, Link } = Typography; +const {Title, Text, Link} = Typography; + +function buildVinLookupUrl(vin) { + if (!vin) return null; + + return `https://www.vtechtoolkit.com/ES_NHTSA.aspx?Vin=${encodeURIComponent(vin)}`; +} + +function buildCpsUrl(bodyshop) { + const apiKey = bodyshop?.es_api_key; + if (bodyshop?.ins_rule_set !== "MPI" || !apiKey) return null; + + return `https://www.EstimateScrubber.com/ATAM.aspx?apiKey=${encodeURIComponent(apiKey)}`; +} function getOrderedListParts(text) { - if (typeof text !== "string") return null; + if (typeof text !== "string") return null; - const matches = []; - const markerPattern = /(\d{1,2})\.\s+/g; - let match; + const matches = []; + const markerPattern = /(\d{1,2})\.\s+/g; + let match; - while ((match = markerPattern.exec(text)) !== null) { - const markerStart = match.index; - const previousCharacter = text[markerStart - 1]; + while ((match = markerPattern.exec(text)) !== null) { + const markerStart = match.index; + const previousCharacter = text[markerStart - 1]; - if (markerStart > 0 && !/\s/.test(previousCharacter)) continue; + if (markerStart > 0 && !/\s/.test(previousCharacter)) continue; - matches.push({ - number: Number(match[1]), - markerStart, - contentStart: markerPattern.lastIndex - }); - } - - if (matches.length < 2) return null; - - const listStartIndex = matches.findIndex((item, index) => { - const nextItem = matches[index + 1]; - return item.number === 1 && nextItem?.number === 2; - }); - - if (listStartIndex === -1) return null; - - const listMatches = matches.slice(listStartIndex); - const sequentialEndIndex = listMatches.findIndex((item, index) => item.number !== index + 1); - const orderedMatches = sequentialEndIndex === -1 ? listMatches : listMatches.slice(0, sequentialEndIndex); - const orderedListEnd = sequentialEndIndex === -1 ? text.length : listMatches[sequentialEndIndex].markerStart; - - if (orderedMatches.length < 2) return null; - - const items = orderedMatches - .map((item, index) => { - const nextMarkerStart = orderedMatches[index + 1]?.markerStart ?? orderedListEnd; - return text.slice(item.contentStart, nextMarkerStart).trim(); - }) - .filter(Boolean); - - if (items.length < 2) return null; - - return { - prefix: text.slice(0, orderedMatches[0].markerStart).trim(), - items - }; -} - -function ScrubberDescription({ text }) { - const orderedListParts = getOrderedListParts(text); - - if (!orderedListParts) return {text}; - - return ( -
- {orderedListParts.prefix && {orderedListParts.prefix}} -
    - {orderedListParts.items.map((item, index) => ( -
  1. - {item} -
  2. - ))} -
-
- ); -} - -export function EstimateScrubberResults({ bodyshop, jobid, job, esResults }) { - const [searchText, setSearchText] = useState(""); - const buttonDisabled = job?.g_bett_amt == null; - - // Filter items based on search text - const itemsWithKeys = esResults?.items - ? esResults.items.map((item, itemIndex) => ({ - ...item, - scrubberRowKey: [item.SubCategory, item.L, item.R, item.Anchor, itemIndex].filter(Boolean).join("|") - })) - : []; - - const filteredItems = itemsWithKeys.length - ? itemsWithKeys.filter((item) => { - if (!searchText.trim()) return true; - const searchLower = searchText.toLowerCase(); - return ( - (item.L && item.L.toLowerCase().includes(searchLower)) || - (item.R && item.R.toLowerCase().includes(searchLower)) - ); - }) - : []; - - // Group filtered items by category - const groupedItems = filteredItems.length - ? _.groupBy( - filteredItems.filter((item) => item.SubCategory !== "In Main Display Group - Table Level"), - "SubCategory" - ) - : {}; - - // Define category colors and priorities - const categoryConfig = { - "Administrative Items": { color: "blue", priority: 1, icon: "📎" }, - "Rates Issues": { color: "blue", priority: 2, icon: "💵" }, - "MPI Guidelines Items": { color: "blue", priority: 3, icon: "📋" }, - "Estimator Recommendations": { color: "blue", priority: 4, icon: "✅" }, - "Estimate Parts Found": { color: "blue", priority: 5, icon: "🔧" }, - "All Parts Found": { color: "blue", priority: 6, icon: "🔧" } - }; - - // Sort categories by priority - const sortedCategories = Object.keys(groupedItems).sort((a, b) => { - const priorityA = categoryConfig[a]?.priority || 999; - const priorityB = categoryConfig[b]?.priority || 999; - return priorityA - priorityB; - }); - - // Define table columns - const columns = [ - { - title: "Item", - dataIndex: "L", - key: "item", - width: "25%", - render: (text, record) => ( - - {text} - {record.LinkText && record.Anchor && ( - - Learn more - - )} - - ) - }, - { - title: "Description", - dataIndex: "R", - key: "description", - width: "75%", - render: (text) => + matches.push({ + number: Number(match[1]), + markerStart, + contentStart: markerPattern.lastIndex + }); } - ]; - const collapseItems = sortedCategories.map((category) => { - const items = groupedItems[category]; - const config = categoryConfig[category] || { color: "default", icon: "📄" }; + if (matches.length < 2) return null; + + const listStartIndex = matches.findIndex((item, index) => { + const nextItem = matches[index + 1]; + return item.number === 1 && nextItem?.number === 2; + }); + + if (listStartIndex === -1) return null; + + const listMatches = matches.slice(listStartIndex); + const sequentialEndIndex = listMatches.findIndex((item, index) => item.number !== index + 1); + const orderedMatches = sequentialEndIndex === -1 ? listMatches : listMatches.slice(0, sequentialEndIndex); + const orderedListEnd = sequentialEndIndex === -1 ? text.length : listMatches[sequentialEndIndex].markerStart; + + if (orderedMatches.length < 2) return null; + + const items = orderedMatches + .map((item, index) => { + const nextMarkerStart = orderedMatches[index + 1]?.markerStart ?? orderedListEnd; + return text.slice(item.contentStart, nextMarkerStart).trim(); + }) + .filter(Boolean); + + if (items.length < 2) return null; return { - key: category, - label: ( - - {/* {config.icon} */} - {category} - - - ), - children: ( - - ) + prefix: text.slice(0, orderedMatches[0].markerStart).trim(), + items }; - }); - - if (!esResults?.items?.length || job?.id !== esResults?.jobid) { - return ( - } - /> - - //
- // - // - // Estimate not yet scrubbed. - // - // Run the estimate scrubber to see results here. - // - //
- ); - } - - // Show empty search results state - if (searchText && !filteredItems.length) { - return ( -
- } - value={searchText} - onChange={(e) => setSearchText(e.target.value)} - allowClear - style={{ maxWidth: 400, marginBottom: "24px" }} - /> -
- - - No items match your search - - Try different keywords or clear the search to see all items. -
-
- ); - } - - return ( -
- {Object.keys(groupedItems).length === 0 ? ( - - ) : ( - - - } - value={searchText} - onChange={(e) => setSearchText(e.target.value)} - allowClear - style={{ maxWidth: 400 }} - /> - {searchText && ( -
- - Showing {filteredItems.length} of {esResults.items.length} items - -
- )} - - - -
- - {Object.entries(groupedItems).map(([category, items]) => { - const config = categoryConfig[category] || { color: "default" }; - return ( - - {category}: {items.length} - - ); - })} - - - -
- )} -
- ); +} + +function ScrubberDescription({text}) { + const orderedListParts = getOrderedListParts(text); + + if (!orderedListParts) return {text}; + + return ( +
+ {orderedListParts.prefix && {orderedListParts.prefix}} +
    + {orderedListParts.items.map((item, index) => ( +
  1. + {item} +
  2. + ))} +
+
+ ); +} + +export function EstimateScrubberResults({bodyshop, jobid, job, esResults}) { + const [searchText, setSearchText] = useState(""); + const buttonDisabled = job?.g_bett_amt == null; + const vinLookupUrl = buildVinLookupUrl(job?.v_vin); + const cpsUrl = buildCpsUrl(bodyshop); + + // Filter items based on search text + const itemsWithKeys = esResults?.items + ? esResults.items.map((item, itemIndex) => ({ + ...item, + scrubberRowKey: [item.SubCategory, item.L, item.R, item.Anchor, itemIndex].filter(Boolean).join("|") + })) + : []; + + const filteredItems = itemsWithKeys.length + ? itemsWithKeys.filter((item) => { + if (!searchText.trim()) return true; + const searchLower = searchText.toLowerCase(); + return ( + (item.L && item.L.toLowerCase().includes(searchLower)) || + (item.R && item.R.toLowerCase().includes(searchLower)) + ); + }) + : []; + + // Group filtered items by category + const groupedItems = filteredItems.length + ? _.groupBy( + filteredItems.filter((item) => item.SubCategory !== "In Main Display Group - Table Level"), + "SubCategory" + ) + : {}; + + // Define category colors and priorities + const categoryConfig = { + "Administrative Items": {color: "blue", priority: 1, icon: "📎"}, + "Rates Issues": {color: "blue", priority: 2, icon: "💵"}, + "MPI Guidelines Items": {color: "blue", priority: 3, icon: "📋"}, + "Estimator Recommendations": {color: "blue", priority: 4, icon: "✅"}, + "Estimate Parts Found": {color: "blue", priority: 5, icon: "🔧"}, + "All Parts Found": {color: "blue", priority: 6, icon: "🔧"} + }; + + // Sort categories by priority + const sortedCategories = Object.keys(groupedItems).sort((a, b) => { + const priorityA = categoryConfig[a]?.priority || 999; + const priorityB = categoryConfig[b]?.priority || 999; + return priorityA - priorityB; + }); + + // Define table columns + const columns = [ + { + title: "Item", + dataIndex: "L", + key: "item", + width: "25%", + render: (text, record) => ( + + {text} + {record.LinkText && record.Anchor && ( + + Learn more + + )} + + ) + }, + { + title: "Description", + dataIndex: "R", + key: "description", + width: "75%", + render: (text) => + } + ]; + + const collapseItems = sortedCategories.map((category) => { + const items = groupedItems[category]; + const config = categoryConfig[category] || {color: "default", icon: "📄"}; + + return { + key: category, + label: ( + + {/* {config.icon} */} + {category} + + + ), + children: ( +
+ ) + }; + }); + + if (!esResults?.items?.length || job?.id !== esResults?.jobid) { + return ( + } + /> + + //
+ // + // + // Estimate not yet scrubbed. + // + // Run the estimate scrubber to see results here. + // + //
+ ); + } + + // Show empty search results state + if (searchText && !filteredItems.length) { + return ( +
+ } + value={searchText} + onChange={(e) => setSearchText(e.target.value)} + allowClear + style={{maxWidth: 400, marginBottom: "24px"}} + /> +
+ + + No items match your search + + Try different keywords or clear the search to see all items. +
+
+ ); + } + + return ( +
+ {Object.keys(groupedItems).length === 0 ? ( + + ) : ( + + + } + value={searchText} + onChange={(e) => setSearchText(e.target.value)} + allowClear + style={{maxWidth: 400}} + /> + {searchText && ( +
+ + Showing {filteredItems.length} of {esResults.items.length} items + +
+ )} + + + + {cpsUrl && ( + + )} + +
+ + {Object.entries(groupedItems).map(([category, items]) => { + const config = categoryConfig[category] || {color: "default"}; + return ( + + {category}: {items.length} + + ); + })} + + + +
+ )} +
+ ); } diff --git a/src/components/organisms/sider-menu/sider-menu.organism.jsx b/src/components/organisms/sider-menu/sider-menu.organism.jsx index ddac342..04a6ff1 100644 --- a/src/components/organisms/sider-menu/sider-menu.organism.jsx +++ b/src/components/organisms/sider-menu/sider-menu.organism.jsx @@ -9,102 +9,103 @@ import { SettingFilled, SunOutlined } from "@ant-design/icons"; -import { Menu } from "antd"; +import {Menu} from "antd"; import React from "react"; -import { Link, useLocation } from "react-router-dom"; -import { createStructuredSelector } from "reselect"; +import {Link, useLocation} from "react-router-dom"; +import {createStructuredSelector} from "reselect"; import ipcTypes from "../../../ipc.types"; -import { selectDarkMode } from "../../../redux/user/user.selectors"; -import SiderSignOut from "../../molecules/sider-sign-out/sider-sign-out.molecule"; -import { connect } from "react-redux"; -import { signOutStart, toggleDarkMode } from "../../../redux/user/user.actions"; -const { ipcRenderer } = window; +import {selectDarkMode} from "../../../redux/user/user.selectors"; +import {connect} from "react-redux"; +import {signOutStart, toggleDarkMode} from "../../../redux/user/user.actions"; + +const {ipcRenderer} = window; const mapStateToProps = createStructuredSelector({ - darkMode: selectDarkMode + darkMode: selectDarkMode }); const mapDispatchToProps = (dispatch) => ({ - toggleDarkMode: () => dispatch(toggleDarkMode()), - signOutStart: () => dispatch(signOutStart()) + toggleDarkMode: () => dispatch(toggleDarkMode()), + signOutStart: () => dispatch(signOutStart()) }); -export function SiderMenuOrganism({ darkMode, toggleDarkMode, signOutStart }) { - const { pathname } = useLocation(); +export function SiderMenuOrganism({darkMode, toggleDarkMode, signOutStart}) { + const {pathname} = useLocation(); - return ( - { - switch (e.key) { - case "darkmode": - toggleDarkMode(); - break; - case "quit": - ipcRenderer.send(ipcTypes.quit); - break; - case "signout": - signOutStart(); - break; - default: - break; - } - }} - mode="inline" - items={[ - { - key: "/", - icon: , - label: Jobs - }, - { - key: "/scan", - icon: , - label: File Scan - }, - { - key: "/reporting", - icon: , - label: Reporting - }, - { - key: "/audit", - icon: , - label: Audit - }, - { - key: "/settings", - icon: , - label: Settings - }, - { type: "divider" }, - { - key: "signout", - icon: , - label: "Sign out" - }, - { - key: "quit", - icon: , - label: "Quit" - }, - { - key: "darkmode", - icon: darkMode ? : , - label: darkMode ? "Light Mode" : "Dark Mode" - } - // ...(process.env.NODE_ENV !== "production" - // ? [ - // { - // key: "/admin", - // icon: , - // label: ADMIN - // } - // ] - // : []) - ]} - /> - ); + return ( + { + switch (e.key) { + case "darkmode": + ipcRenderer.send(ipcTypes.store.set, {darkMode: !darkMode}); + toggleDarkMode(); + break; + case "quit": + ipcRenderer.send(ipcTypes.quit); + break; + case "signout": + signOutStart(); + break; + default: + break; + } + }} + mode="inline" + items={[ + { + key: "/", + icon: , + label: Jobs + }, + { + key: "/scan", + icon: , + label: File Scan + }, + { + key: "/reporting", + icon: , + label: Reporting + }, + { + key: "/audit", + icon: , + label: Audit + }, + { + key: "/settings", + icon: , + label: Settings + }, + {type: "divider"}, + { + key: "signout", + icon: , + label: "Sign out" + }, + { + key: "quit", + icon: , + label: "Quit" + }, + { + key: "darkmode", + icon: darkMode ? : , + label: darkMode ? "Light Mode" : "Dark Mode" + } + // ...(process.env.NODE_ENV !== "production" + // ? [ + // { + // key: "/admin", + // icon: , + // label: ADMIN + // } + // ] + // : []) + ]} + /> + ); } export default connect(mapStateToProps, mapDispatchToProps)(SiderMenuOrganism); diff --git a/src/components/pages/routes/routes.page.jsx b/src/components/pages/routes/routes.page.jsx index 73cd3a8..966bab8 100644 --- a/src/components/pages/routes/routes.page.jsx +++ b/src/components/pages/routes/routes.page.jsx @@ -1,10 +1,9 @@ -import { Alert, Layout } from "antd"; +import {Alert, Layout} from "antd"; import React from "react"; -import { connect } from "react-redux"; -import { Routes } from "react-router-dom"; -import { Route } from "react-router-dom"; -import { createStructuredSelector } from "reselect"; -import { selectBodyshop, selectDarkMode } from "../../../redux/user/user.selectors"; +import {connect} from "react-redux"; +import {Route, Routes} from "react-router-dom"; +import {createStructuredSelector} from "reselect"; +import {selectBodyshop, selectDarkMode} from "../../../redux/user/user.selectors"; import ErrorResultAtom from "../../atoms/error-result/error-result.atom"; import ReleaseNotes from "../../molecules/release-notes/release-notes.molecule"; import NotificationModalOrganism from "../../organisms/notification-modal/notification-modal.organism"; @@ -17,47 +16,48 @@ import SettingsPage from "../settings/settings.page"; import AuditPage from "../audit/audit.page"; import AdminPage from "../admin/admin.page"; -const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, darkMode: selectDarkMode }); +const mapStateToProps = createStructuredSelector({bodyshop: selectBodyshop, darkMode: selectDarkMode}); const mapDispatchToProps = (dispatch) => ({}); -export function RoutesPage({ bodyshop, darkMode }) { - if (bodyshop === false) - return ( - - ); - - return ( - - - - - - - - {bodyshop?.ins_rule_set === "SGI" && ( - - )} - - } /> - } /> - } /> - } /> - } /> - } /> - - - - - - - ); + ); + + return ( + + + + + + + + {bodyshop?.ins_rule_set === "SGI" && ( + + )} + + }/> + }/> + }/> + }/> + }/> + }/> + + + + + + + ); } + export default connect(mapStateToProps, mapDispatchToProps)(RoutesPage); diff --git a/src/components/pages/settings/settings.page.jsx b/src/components/pages/settings/settings.page.jsx index 7793e43..6c69794 100644 --- a/src/components/pages/settings/settings.page.jsx +++ b/src/components/pages/settings/settings.page.jsx @@ -1,4 +1,4 @@ -import { Card, Col, Row, Space, Typography } from "antd"; +import { Card, Space, Typography, theme } from "antd"; import React, { useEffect } from "react"; import ipcTypes from "../../../ipc.types"; import NotificationsToggleAtom from "../../atoms/notifications-toggle/notifications-toggle.atom"; @@ -11,48 +11,63 @@ import "./settings.page.styles.scss"; const { ipcRenderer } = window; export default function SettingsPage() { + const { token } = theme.useToken(); + useEffect(() => { ipcRenderer.send(ipcTypes.store.getAll); }, []); return ( -
+
- Settings - - Manage watcher behavior, notifications, and shop defaults in one compact workspace. - +
+ Settings + + Manage watcher behavior, notifications, and shop defaults in one compact workspace. + +
+
+ +
- -
+ + + + +
- -
- - - - - - Automation - - Fine-tune polling, startup behavior, and desktop notifications. - - - - - - - - - - - - - - + + Automation + + Fine-tune polling, startup behavior, and desktop notifications. + + + + + + + + + ); } diff --git a/src/components/pages/settings/settings.page.styles.scss b/src/components/pages/settings/settings.page.styles.scss index 79afbf3..0da1df9 100644 --- a/src/components/pages/settings/settings.page.styles.scss +++ b/src/components/pages/settings/settings.page.styles.scss @@ -8,11 +8,20 @@ } .settings-page__hero { + display: flex; + align-items: center; + justify-content: space-between; + gap: 24px; padding: 18px 24px; border-radius: 18px; - background: linear-gradient(135deg, #ffffff 0%, #f5f9ff 55%, #eef4ff 100%); - border: 1px solid #dbe7ff; - box-shadow: 0 10px 24px rgba(23, 43, 77, 0.06); + background: linear-gradient( + 135deg, + var(--settings-hero-bg-start, #ffffff) 0%, + var(--settings-hero-bg-mid, #f5f9ff) 55%, + var(--settings-hero-bg-end, #eef4ff) 100% + ); + border: 1px solid var(--settings-border, #dbe7ff); + box-shadow: var(--settings-shadow, 0 10px 24px rgba(23, 43, 77, 0.06)); .ant-typography { margin-bottom: 0; @@ -23,18 +32,47 @@ } } -.settings-page__side-stack { +.settings-page__hero-copy { + flex: 1 1 360px; + min-width: 0; +} + +.settings-page__hero-status { + flex: 0 1 520px; + min-width: 360px; +} + +.settings-page__hero-status .settings-watcher-manager { + gap: 8px; +} + +.settings-page__hero-status .settings-watcher-manager__content { + justify-content: flex-end; +} + +.settings-page__content-stack { width: 100%; } -.settings-page__side-stack > .ant-space-item, +.settings-page__content-stack > .ant-space-item, .settings-page__controls > .ant-space-item { width: 100%; } +.settings-page__secondary-grid { + display: grid; + grid-template-columns: minmax(420px, 0.38fr) minmax(0, 1fr); + gap: 16px; + width: 100%; +} + +.settings-page__secondary-grid .settings-page__card { + height: 100%; +} + .settings-page__card { border-radius: 18px; - box-shadow: 0 10px 24px rgba(15, 23, 42, 0.06); + box-shadow: var(--settings-shadow, 0 10px 24px rgba(15, 23, 42, 0.06)); .ant-card-body { padding: 18px 20px; @@ -64,8 +102,8 @@ .settings-page__controls > .ant-space-item > * { padding: 10px 12px; border-radius: 12px; - background: #fafcff; - border: 1px solid #edf2ff; + background: var(--settings-control-bg, #fafcff); + border: 1px solid var(--settings-border, #edf2ff); } .settings-page__controls > .ant-space-item > * > div { @@ -105,9 +143,9 @@ .settings-filepaths__list { overflow: hidden; - border: 1px solid #edf2ff; + border: 1px solid var(--settings-border, #edf2ff); border-radius: 14px; - background: #fafcff; + background: var(--settings-control-bg, #fafcff); } .settings-filepaths__list .ant-list-item { @@ -155,15 +193,19 @@ } .settings-watcher-status--started { - color: #389e0d; - background: #f6ffed; - border-color: #b7eb8f; + color: var(--settings-success-text, #389e0d); + background: var(--settings-success-bg, #f6ffed); + border-color: var(--settings-success-border, #b7eb8f); } .settings-watcher-status--stopped { - color: #cf1322; - background: #fff1f0; - border-color: #ffa39e; + color: var(--settings-error-text, #cf1322); + background: var(--settings-error-bg, #fff1f0); + border-color: var(--settings-error-border, #ffa39e); +} + +.settings-watcher-status .ant-typography { + color: inherit; } .settings-watcher-status__error { @@ -176,7 +218,23 @@ padding-bottom: 16px; } - .settings-page__hero, + .settings-page__hero { + align-items: stretch; + flex-direction: column; + gap: 16px; + padding: 20px; + } + + .settings-page__hero-status { + flex: 1 1 auto; + min-width: 0; + width: 100%; + } + + .settings-page__secondary-grid { + grid-template-columns: 1fr; + } + .settings-page__card .ant-card-body { padding: 20px; } diff --git a/src/ipc/ipc-renderer-handler.js b/src/ipc/ipc-renderer-handler.js index d70ada6..0b1debd 100644 --- a/src/ipc/ipc-renderer-handler.js +++ b/src/ipc/ipc-renderer-handler.js @@ -1,4 +1,4 @@ -import { antdNotification as notification } from "../util/antdFeedback"; +import {antdNotification as notification} from "../util/antdFeedback"; import ipcTypes from "../ipc.types"; import { setReleaseNotes, @@ -9,87 +9,91 @@ import { setWatchedPaths, setWatcherStatus } from "../redux/application/application.actions"; -import { calculateAudit, setAuditError } from "../redux/reporting/reporting.actions"; -import { setScanEstimateList } from "../redux/scan/scan.actions"; -import { store } from "../redux/store"; -import { signOutStart } from "../redux/user/user.actions"; -import { GetR4PDateWithClaim, UpsertEstimate } from "./ipc-estimate-utils"; -const { ipcRenderer } = window; +import {calculateAudit, setAuditError} from "../redux/reporting/reporting.actions"; +import {setScanEstimateList} from "../redux/scan/scan.actions"; +import {store} from "../redux/store"; +import {setDarkMode, signOutStart} from "../redux/user/user.actions"; +import {GetR4PDateWithClaim, UpsertEstimate} from "./ipc-estimate-utils"; + +const {ipcRenderer} = window; console.log("----Initializing IPC Listeners in React App."); ipcRenderer.on("test-toRenderer", (event, obj) => { - console.log("test-toRenderer", obj); + console.log("test-toRenderer", obj); }); ipcRenderer.on(ipcTypes.fileWatcher.toRenderer.filepathsList, (event, obj) => { - store.dispatch(setWatchedPaths(obj)); + store.dispatch(setWatchedPaths(obj)); }); //Filewatcher Status Section ipcRenderer.on(ipcTypes.fileWatcher.toRenderer.startSuccess, (event, obj) => { - store.dispatch(setWatcherStatus("Started")); + store.dispatch(setWatcherStatus("Started")); }); ipcRenderer.on(ipcTypes.fileWatcher.toRenderer.stopSuccess, (event, obj) => { - store.dispatch(setWatcherStatus("Stopped")); + store.dispatch(setWatcherStatus("Stopped")); }); ipcRenderer.on(ipcTypes.fileWatcher.toRenderer.error, (event, obj) => { - store.dispatch(setWatcherStatus(obj)); + store.dispatch(setWatcherStatus(obj)); }); //Estimate Section -ipcRenderer.on(ipcTypes.estimate.toRenderer.getCloseDate, async (event, { filepath, clm_no }) => { - const close_date = await GetR4PDateWithClaim(clm_no); - ipcRenderer.send(ipcTypes.app.toMain.importJob, { - filepath, - close_date - }); +ipcRenderer.on(ipcTypes.estimate.toRenderer.getCloseDate, async (event, {filepath, clm_no}) => { + const close_date = await GetR4PDateWithClaim(clm_no); + ipcRenderer.send(ipcTypes.app.toMain.importJob, { + filepath, + close_date + }); }); ipcRenderer.on(ipcTypes.estimate.toRenderer.estimateDecodeSuccess, async (event, obj) => { - await UpsertEstimate(obj); + await UpsertEstimate(obj); }); ipcRenderer.on(ipcTypes.store.response, (event, obj) => { - store.dispatch(setSettings(obj)); + store.dispatch(setSettings(obj)); + if (Object.prototype.hasOwnProperty.call(obj, "darkMode")) { + store.dispatch(setDarkMode(obj.darkMode)); + } }); //FileScan Section ipcRenderer.on(ipcTypes.fileScan.toRenderer.scanFilePathsResponse, async (event, listOfEstimates) => { - store.dispatch(setScanEstimateList(listOfEstimates)); + store.dispatch(setScanEstimateList(listOfEstimates)); }); ipcRenderer.on(ipcTypes.app.toRenderer.updateAvailable, async (event, updateInfo) => { - store.dispatch(setUpdateAvailable(updateInfo)); + store.dispatch(setUpdateAvailable(updateInfo)); }); ipcRenderer.on(ipcTypes.app.toRenderer.downloadProgress, async (event, progress) => { - store.dispatch(setUpdateProgress(progress)); + store.dispatch(setUpdateProgress(progress)); }); ipcRenderer.on(ipcTypes.app.toRenderer.signOut, async (event, progress) => { - store.dispatch(signOutStart()); + store.dispatch(signOutStart()); }); ipcRenderer.on(ipcTypes.app.toRenderer.setReleaseNotes, async (event, releaseNotes) => { - store.dispatch(setReleaseNotes(releaseNotes)); + store.dispatch(setReleaseNotes(releaseNotes)); }); ipcRenderer.on(ipcTypes.app.toRenderer.appVersion, async (event, appversion) => { - window.$crisp.push(["set", "session:data", [[["rps-version", appversion]]]]); + window.$crisp.push(["set", "session:data", [[["rps-version", appversion]]]]); }); //Handle Autdit ipcRenderer.on(ipcTypes.audit.toRenderer.auditClaimsArray, async (event, claimsArray) => { - store.dispatch(calculateAudit(claimsArray)); + store.dispatch(calculateAudit(claimsArray)); }); ipcRenderer.on(ipcTypes.audit.toRenderer.auditError, async (event, error) => { - store.dispatch(setAuditError(error)); + store.dispatch(setAuditError(error)); }); ipcRenderer.on(ipcTypes.app.toRenderer.scrubResults, async (event, results) => { - store.dispatch(setScrubResults(results)); + store.dispatch(setScrubResults(results)); }); -ipcRenderer.on(ipcTypes.app.toRenderer.scrubError, async (event, { message }) => { - notification.open({ type: "error", message: "Estimate Scrubber Error", description: message }); +ipcRenderer.on(ipcTypes.app.toRenderer.scrubError, async (event, {message}) => { + notification.open({type: "error", message: "Estimate Scrubber Error", description: message}); }); diff --git a/src/redux/user/user.actions.js b/src/redux/user/user.actions.js index 2aeb89d..42d23fe 100644 --- a/src/redux/user/user.actions.js +++ b/src/redux/user/user.actions.js @@ -97,6 +97,11 @@ export const checkForNotification = () => ({ type: UserActionTypes.CHECK_FOR_NOTIFICATION }); +export const setDarkMode = (darkMode) => ({ + type: UserActionTypes.SET_DARK_MODE, + payload: darkMode +}); + export const toggleDarkMode = () => ({ type: UserActionTypes.TOGGLE_DARK_MODE }); diff --git a/src/redux/user/user.reducer.js b/src/redux/user/user.reducer.js index c2ea19f..9034ff0 100644 --- a/src/redux/user/user.reducer.js +++ b/src/redux/user/user.reducer.js @@ -82,6 +82,11 @@ const userReducer = (state = INITIAL_STATE, action) => { ...action.payload //Spread current user details in. } }; + case UserActionTypes.SET_DARK_MODE: + return { + ...state, + darkMode: action.payload + }; case UserActionTypes.TOGGLE_DARK_MODE: return { ...state, diff --git a/src/redux/user/user.types.js b/src/redux/user/user.types.js index a8e173c..7d7324d 100644 --- a/src/redux/user/user.types.js +++ b/src/redux/user/user.types.js @@ -30,6 +30,7 @@ const UserActionTypes = { CHECK_FOR_NOTIFICATION: "CHECK_FOR_NOTIFICATION", SET_NOTIFICATIONS: "SET_NOTIFICATIONS", SET_TARGETS: "SET_TARGETS", + SET_DARK_MODE: "SET_DARK_MODE", TOGGLE_DARK_MODE: "TOGGLE_DARK_MODE" }; export default UserActionTypes;