feature/IO-3725-RPS-Changes - Styling / Layout fixes (Dark mode / Settings page) / Add VIN Lookup
This commit is contained in:
@@ -9,6 +9,7 @@ const storeOptions = {
|
||||
enableNotifications: true,
|
||||
filePaths: [],
|
||||
accepted_ins_co: [],
|
||||
darkMode: false,
|
||||
runWatcherOnStartup: true,
|
||||
polling: {
|
||||
enabled: false,
|
||||
|
||||
@@ -41,11 +41,32 @@ function AntdFeedbackBridge() {
|
||||
return null;
|
||||
}
|
||||
|
||||
function AppShell({ currentUser }) {
|
||||
const { token } = theme.useToken();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="imex-app-shell"
|
||||
style={{
|
||||
"--imex-scrollbar-track": token.colorBgContainer,
|
||||
"--imex-scrollbar-thumb": token.colorFillSecondary,
|
||||
"--imex-scrollbar-thumb-hover": token.colorFill
|
||||
}}
|
||||
>
|
||||
{currentUser.authorized ? <RoutesPage /> : <SignInPage />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 }) {
|
||||
>
|
||||
<AntdApp>
|
||||
<AntdFeedbackBridge />
|
||||
<div>{currentUser.authorized ? <RoutesPage /> : <SignInPage />}</div>
|
||||
<AppShell currentUser={currentUser} />
|
||||
</AntdApp>
|
||||
</ConfigProvider>
|
||||
</ApolloProvider>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>{text}</Text>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{orderedListParts.prefix && <Text>{orderedListParts.prefix}</Text>}
|
||||
<ol
|
||||
style={{
|
||||
margin: orderedListParts.prefix ? "8px 0 0 20px" : "0 0 0 20px",
|
||||
paddingLeft: 16
|
||||
}}
|
||||
>
|
||||
{orderedListParts.items.map((item, index) => (
|
||||
<li key={`${index}-${item}`}>
|
||||
<Text>{item}</Text>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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) => (
|
||||
<Space direction="vertical" size="small">
|
||||
<Text strong>{text}</Text>
|
||||
{record.LinkText && record.Anchor && (
|
||||
<Link href={record.Anchor} target="_blank" rel="noopener noreferrer">
|
||||
<LinkOutlined /> Learn more
|
||||
</Link>
|
||||
)}
|
||||
</Space>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "Description",
|
||||
dataIndex: "R",
|
||||
key: "description",
|
||||
width: "75%",
|
||||
render: (text) => <ScrubberDescription text={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: (
|
||||
<Space>
|
||||
{/* <span style={{ fontSize: "18px" }}>{config.icon}</span> */}
|
||||
<Text strong>{category}</Text>
|
||||
<Badge
|
||||
count={items.length}
|
||||
style={{
|
||||
backgroundColor: config.color === "blue" ? "#1890ff" : config.color === "orange" ? "#fa8c16" : "#52c41a"
|
||||
}}
|
||||
/>
|
||||
</Space>
|
||||
),
|
||||
children: (
|
||||
<Table
|
||||
dataSource={items}
|
||||
columns={columns}
|
||||
pagination={false}
|
||||
size="middle"
|
||||
rowKey="scrubberRowKey"
|
||||
style={{ marginTop: "12px" }}
|
||||
scroll={{}}
|
||||
/>
|
||||
)
|
||||
prefix: text.slice(0, orderedMatches[0].markerStart).trim(),
|
||||
items
|
||||
};
|
||||
});
|
||||
|
||||
if (!esResults?.items?.length || job?.id !== esResults?.jobid) {
|
||||
return (
|
||||
<Result
|
||||
status={buttonDisabled ? "error" : "info"}
|
||||
title={buttonDisabled ? "Unable to scrub." : "Estimate Not Scrubbed"}
|
||||
subTitle={
|
||||
buttonDisabled
|
||||
? "Additional information is required to scrub this estimate. Please reimport the estimate to enable scrubbing."
|
||||
: "Run the estimate scrubber to see results here."
|
||||
}
|
||||
extra={<EstimateScrubberButton key="es" jobid={job ? job.id : null} job={job ? job : null} />}
|
||||
/>
|
||||
|
||||
// <div style={{ padding: "24px", textAlign: "center" }}>
|
||||
// <InfoCircleOutlined style={{ fontSize: "48px", color: "#d9d9d9", marginBottom: "16px" }} />
|
||||
// <Title level={4} type="secondary">
|
||||
// Estimate not yet scrubbed.
|
||||
// </Title>
|
||||
// <Text type="secondary">Run the estimate scrubber to see results here.</Text>
|
||||
// <EstimateScrubberButton key="es" jobid={job ? job.id : null} job={job ? job : null} />
|
||||
// </div>
|
||||
);
|
||||
}
|
||||
|
||||
// Show empty search results state
|
||||
if (searchText && !filteredItems.length) {
|
||||
return (
|
||||
<div style={{ padding: "16px" }}>
|
||||
<Input
|
||||
placeholder="Search..."
|
||||
prefix={<SearchOutlined />}
|
||||
value={searchText}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
allowClear
|
||||
style={{ maxWidth: 400, marginBottom: "24px" }}
|
||||
/>
|
||||
<div style={{ padding: "24px", textAlign: "center" }}>
|
||||
<SearchOutlined style={{ fontSize: "48px", color: "#d9d9d9", marginBottom: "16px" }} />
|
||||
<Title level={4} type="secondary">
|
||||
No items match your search
|
||||
</Title>
|
||||
<Text type="secondary">Try different keywords or clear the search to see all items.</Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: "16px" }}>
|
||||
{Object.keys(groupedItems).length === 0 ? (
|
||||
<Result title="Estimate Scrubber did not find any issues." status="success" />
|
||||
) : (
|
||||
<Space direction="vertical" size="large" style={{ width: "100%" }}>
|
||||
<Space>
|
||||
<Input
|
||||
placeholder="Search"
|
||||
prefix={<SearchOutlined />}
|
||||
value={searchText}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
allowClear
|
||||
style={{ maxWidth: 400 }}
|
||||
/>
|
||||
{searchText && (
|
||||
<div style={{ marginTop: "8px" }}>
|
||||
<Text type="secondary">
|
||||
Showing {filteredItems.length} of {esResults.items.length} items
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
style={{ float: "right" }}
|
||||
type="primary"
|
||||
href={esResults?.pdfUrl}
|
||||
target="_blank"
|
||||
disabled={!esResults?.pdfUrl}
|
||||
>
|
||||
View PDF
|
||||
</Button>
|
||||
<Button
|
||||
style={{ float: "right" }}
|
||||
type="link"
|
||||
href={esResults?.reportIssueUrl}
|
||||
target="_blank"
|
||||
disabled={!esResults?.reportIssueUrl}
|
||||
>
|
||||
Report Scrubbing Issue
|
||||
</Button>
|
||||
</Space>
|
||||
<Space wrap size={"small"}>
|
||||
{Object.entries(groupedItems).map(([category, items]) => {
|
||||
const config = categoryConfig[category] || { color: "default" };
|
||||
return (
|
||||
<Tag key={category} color={config.color}>
|
||||
{category}: {items.length}
|
||||
</Tag>
|
||||
);
|
||||
})}
|
||||
</Space>
|
||||
|
||||
<Collapse defaultActiveKey={sortedCategories} expandIconPosition="end" size="large" items={collapseItems} />
|
||||
</Space>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ScrubberDescription({text}) {
|
||||
const orderedListParts = getOrderedListParts(text);
|
||||
|
||||
if (!orderedListParts) return <Text>{text}</Text>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{orderedListParts.prefix && <Text>{orderedListParts.prefix}</Text>}
|
||||
<ol
|
||||
style={{
|
||||
margin: orderedListParts.prefix ? "8px 0 0 20px" : "0 0 0 20px",
|
||||
paddingLeft: 16
|
||||
}}
|
||||
>
|
||||
{orderedListParts.items.map((item, index) => (
|
||||
<li key={`${index}-${item}`}>
|
||||
<Text>{item}</Text>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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) => (
|
||||
<Space direction="vertical" size="small">
|
||||
<Text strong>{text}</Text>
|
||||
{record.LinkText && record.Anchor && (
|
||||
<Link href={record.Anchor} target="_blank" rel="noopener noreferrer">
|
||||
<LinkOutlined/> Learn more
|
||||
</Link>
|
||||
)}
|
||||
</Space>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "Description",
|
||||
dataIndex: "R",
|
||||
key: "description",
|
||||
width: "75%",
|
||||
render: (text) => <ScrubberDescription text={text}/>
|
||||
}
|
||||
];
|
||||
|
||||
const collapseItems = sortedCategories.map((category) => {
|
||||
const items = groupedItems[category];
|
||||
const config = categoryConfig[category] || {color: "default", icon: "📄"};
|
||||
|
||||
return {
|
||||
key: category,
|
||||
label: (
|
||||
<Space>
|
||||
{/* <span style={{ fontSize: "18px" }}>{config.icon}</span> */}
|
||||
<Text strong>{category}</Text>
|
||||
<Badge
|
||||
count={items.length}
|
||||
style={{
|
||||
backgroundColor: config.color === "blue" ? "#1890ff" : config.color === "orange" ? "#fa8c16" : "#52c41a"
|
||||
}}
|
||||
/>
|
||||
</Space>
|
||||
),
|
||||
children: (
|
||||
<Table
|
||||
dataSource={items}
|
||||
columns={columns}
|
||||
pagination={false}
|
||||
size="middle"
|
||||
rowKey="scrubberRowKey"
|
||||
style={{marginTop: "12px"}}
|
||||
scroll={{}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
});
|
||||
|
||||
if (!esResults?.items?.length || job?.id !== esResults?.jobid) {
|
||||
return (
|
||||
<Result
|
||||
status={buttonDisabled ? "error" : "info"}
|
||||
title={buttonDisabled ? "Unable to scrub." : "Estimate Not Scrubbed"}
|
||||
subTitle={
|
||||
buttonDisabled
|
||||
? "Additional information is required to scrub this estimate. Please reimport the estimate to enable scrubbing."
|
||||
: "Run the estimate scrubber to see results here."
|
||||
}
|
||||
extra={<EstimateScrubberButton key="es" jobid={job ? job.id : null} job={job ? job : null}/>}
|
||||
/>
|
||||
|
||||
// <div style={{ padding: "24px", textAlign: "center" }}>
|
||||
// <InfoCircleOutlined style={{ fontSize: "48px", color: "#d9d9d9", marginBottom: "16px" }} />
|
||||
// <Title level={4} type="secondary">
|
||||
// Estimate not yet scrubbed.
|
||||
// </Title>
|
||||
// <Text type="secondary">Run the estimate scrubber to see results here.</Text>
|
||||
// <EstimateScrubberButton key="es" jobid={job ? job.id : null} job={job ? job : null} />
|
||||
// </div>
|
||||
);
|
||||
}
|
||||
|
||||
// Show empty search results state
|
||||
if (searchText && !filteredItems.length) {
|
||||
return (
|
||||
<div style={{padding: "16px"}}>
|
||||
<Input
|
||||
placeholder="Search..."
|
||||
prefix={<SearchOutlined/>}
|
||||
value={searchText}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
allowClear
|
||||
style={{maxWidth: 400, marginBottom: "24px"}}
|
||||
/>
|
||||
<div style={{padding: "24px", textAlign: "center"}}>
|
||||
<SearchOutlined style={{fontSize: "48px", color: "#d9d9d9", marginBottom: "16px"}}/>
|
||||
<Title level={4} type="secondary">
|
||||
No items match your search
|
||||
</Title>
|
||||
<Text type="secondary">Try different keywords or clear the search to see all items.</Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{padding: "16px"}}>
|
||||
{Object.keys(groupedItems).length === 0 ? (
|
||||
<Result title="Estimate Scrubber did not find any issues." status="success"/>
|
||||
) : (
|
||||
<Space direction="vertical" size="large" style={{width: "100%"}}>
|
||||
<Space wrap>
|
||||
<Input
|
||||
placeholder="Search"
|
||||
prefix={<SearchOutlined/>}
|
||||
value={searchText}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
allowClear
|
||||
style={{maxWidth: 400}}
|
||||
/>
|
||||
{searchText && (
|
||||
<div style={{marginTop: "8px"}}>
|
||||
<Text type="secondary">
|
||||
Showing {filteredItems.length} of {esResults.items.length} items
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
style={{float: "right"}}
|
||||
type="primary"
|
||||
href={esResults?.pdfUrl}
|
||||
target="_blank"
|
||||
disabled={!esResults?.pdfUrl}
|
||||
icon={<FilePdfOutlined/>}
|
||||
>
|
||||
View PDF
|
||||
</Button>
|
||||
<Button
|
||||
style={{float: "right"}}
|
||||
href={vinLookupUrl}
|
||||
target="_blank"
|
||||
disabled={!vinLookupUrl}
|
||||
icon={<SearchOutlined/>}
|
||||
>
|
||||
VIN Lookup
|
||||
</Button>
|
||||
{cpsUrl && (
|
||||
<Button style={{float: "right"}} href={cpsUrl} target="_blank" icon={<LinkOutlined/>}>
|
||||
CPS
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
style={{float: "right"}}
|
||||
type="primary"
|
||||
danger
|
||||
href={esResults?.reportIssueUrl}
|
||||
target="_blank"
|
||||
disabled={!esResults?.reportIssueUrl}
|
||||
icon={<FlagOutlined/>}
|
||||
>
|
||||
Report Scrubbing Issue
|
||||
</Button>
|
||||
</Space>
|
||||
<Space wrap size={"small"}>
|
||||
{Object.entries(groupedItems).map(([category, items]) => {
|
||||
const config = categoryConfig[category] || {color: "default"};
|
||||
return (
|
||||
<Tag key={category} color={config.color}>
|
||||
{category}: {items.length}
|
||||
</Tag>
|
||||
);
|
||||
})}
|
||||
</Space>
|
||||
|
||||
<Collapse defaultActiveKey={sortedCategories} expandIconPosition="end" size="large"
|
||||
items={collapseItems}/>
|
||||
</Space>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<Menu
|
||||
defaultSelectedKeys={[`/`]}
|
||||
selectedKeys={[pathname]}
|
||||
onClick={(e) => {
|
||||
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: <PieChartOutlined />,
|
||||
label: <Link to="/">Jobs</Link>
|
||||
},
|
||||
{
|
||||
key: "/scan",
|
||||
icon: <FileAddFilled />,
|
||||
label: <Link to="/scan">File Scan</Link>
|
||||
},
|
||||
{
|
||||
key: "/reporting",
|
||||
icon: <BarChartOutlined />,
|
||||
label: <Link to="/reporting">Reporting</Link>
|
||||
},
|
||||
{
|
||||
key: "/audit",
|
||||
icon: <AuditOutlined />,
|
||||
label: <Link to="/audit">Audit</Link>
|
||||
},
|
||||
{
|
||||
key: "/settings",
|
||||
icon: <SettingFilled />,
|
||||
label: <Link to="/settings">Settings</Link>
|
||||
},
|
||||
{ type: "divider" },
|
||||
{
|
||||
key: "signout",
|
||||
icon: <LogoutOutlined style={{ color: "tomato" }} />,
|
||||
label: "Sign out"
|
||||
},
|
||||
{
|
||||
key: "quit",
|
||||
icon: <CloseOutlined style={{ color: "tomato" }} />,
|
||||
label: "Quit"
|
||||
},
|
||||
{
|
||||
key: "darkmode",
|
||||
icon: darkMode ? <SunOutlined /> : <MoonOutlined />,
|
||||
label: darkMode ? "Light Mode" : "Dark Mode"
|
||||
}
|
||||
// ...(process.env.NODE_ENV !== "production"
|
||||
// ? [
|
||||
// {
|
||||
// key: "/admin",
|
||||
// icon: <AlertOutlined />,
|
||||
// label: <Link to="/admin">ADMIN</Link>
|
||||
// }
|
||||
// ]
|
||||
// : [])
|
||||
]}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<Menu
|
||||
defaultSelectedKeys={[`/`]}
|
||||
selectedKeys={[pathname]}
|
||||
onClick={(e) => {
|
||||
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: <PieChartOutlined/>,
|
||||
label: <Link to="/">Jobs</Link>
|
||||
},
|
||||
{
|
||||
key: "/scan",
|
||||
icon: <FileAddFilled/>,
|
||||
label: <Link to="/scan">File Scan</Link>
|
||||
},
|
||||
{
|
||||
key: "/reporting",
|
||||
icon: <BarChartOutlined/>,
|
||||
label: <Link to="/reporting">Reporting</Link>
|
||||
},
|
||||
{
|
||||
key: "/audit",
|
||||
icon: <AuditOutlined/>,
|
||||
label: <Link to="/audit">Audit</Link>
|
||||
},
|
||||
{
|
||||
key: "/settings",
|
||||
icon: <SettingFilled/>,
|
||||
label: <Link to="/settings">Settings</Link>
|
||||
},
|
||||
{type: "divider"},
|
||||
{
|
||||
key: "signout",
|
||||
icon: <LogoutOutlined style={{color: "tomato"}}/>,
|
||||
label: "Sign out"
|
||||
},
|
||||
{
|
||||
key: "quit",
|
||||
icon: <CloseOutlined style={{color: "tomato"}}/>,
|
||||
label: "Quit"
|
||||
},
|
||||
{
|
||||
key: "darkmode",
|
||||
icon: darkMode ? <SunOutlined/> : <MoonOutlined/>,
|
||||
label: darkMode ? "Light Mode" : "Dark Mode"
|
||||
}
|
||||
// ...(process.env.NODE_ENV !== "production"
|
||||
// ? [
|
||||
// {
|
||||
// key: "/admin",
|
||||
// icon: <AlertOutlined />,
|
||||
// label: <Link to="/admin">ADMIN</Link>
|
||||
// }
|
||||
// ]
|
||||
// : [])
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(SiderMenuOrganism);
|
||||
|
||||
@@ -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 (
|
||||
<ErrorResultAtom
|
||||
title="No shop access"
|
||||
errorMessage="You do not currently have access to any shop. Please reach out to technical support."
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Layout style={{ height: "100vh" }} hasSider>
|
||||
<Layout.Sider style={{ background: !darkMode && "#fff" }} collapsible defaultCollapsed="true">
|
||||
<SiderMenuOrganism />
|
||||
</Layout.Sider>
|
||||
<Layout style={{}}>
|
||||
<Layout.Content style={{ marginLeft: "1rem", paddingTop: "1rem", height: "100%", boxSizing: "border-box" }}>
|
||||
<NotificationModalOrganism />
|
||||
{bodyshop?.ins_rule_set === "SGI" && (
|
||||
<Alert
|
||||
message="SGI has not yet released eligibility rules. MPI eligibility rules have been applied and may be incorrect."
|
||||
type="warning"
|
||||
showIcon
|
||||
style={{ marginBottom: "1rem" }}
|
||||
export function RoutesPage({bodyshop, darkMode}) {
|
||||
if (bodyshop === false)
|
||||
return (
|
||||
<ErrorResultAtom
|
||||
title="No shop access"
|
||||
errorMessage="You do not currently have access to any shop. Please reach out to technical support."
|
||||
/>
|
||||
)}
|
||||
<Routes>
|
||||
<Route exact path="/settings" element={<SettingsPage />} />
|
||||
<Route exact path="/reporting" element={<ReportingPage />} />
|
||||
<Route exact path="/audit" element={<AuditPage />} />
|
||||
<Route exact path="/scan" element={<ScanPage />} />
|
||||
<Route exact path="/admin" element={<AdminPage />} />
|
||||
<Route exact path="/" element={<JobsPage />} />
|
||||
</Routes>
|
||||
</Layout.Content>
|
||||
<ReleaseNotes />
|
||||
<UpdateManagerOrganism />
|
||||
</Layout>
|
||||
</Layout>
|
||||
);
|
||||
);
|
||||
|
||||
return (
|
||||
<Layout style={{height: "100vh", overflow: "hidden"}} hasSider>
|
||||
<Layout.Sider style={{background: !darkMode && "#fff"}} collapsible defaultCollapsed="true">
|
||||
<SiderMenuOrganism/>
|
||||
</Layout.Sider>
|
||||
<Layout style={{minWidth: 0, overflow: "hidden"}}>
|
||||
<Layout.Content style={{padding: "1rem", height: "100%", boxSizing: "border-box", overflowX: "hidden"}}>
|
||||
<NotificationModalOrganism/>
|
||||
{bodyshop?.ins_rule_set === "SGI" && (
|
||||
<Alert
|
||||
message="SGI has not yet released eligibility rules. MPI eligibility rules have been applied and may be incorrect."
|
||||
type="warning"
|
||||
showIcon
|
||||
style={{marginBottom: "1rem"}}
|
||||
/>
|
||||
)}
|
||||
<Routes>
|
||||
<Route exact path="/settings" element={<SettingsPage/>}/>
|
||||
<Route exact path="/reporting" element={<ReportingPage/>}/>
|
||||
<Route exact path="/audit" element={<AuditPage/>}/>
|
||||
<Route exact path="/scan" element={<ScanPage/>}/>
|
||||
<Route exact path="/admin" element={<AdminPage/>}/>
|
||||
<Route exact path="/" element={<JobsPage/>}/>
|
||||
</Routes>
|
||||
</Layout.Content>
|
||||
<ReleaseNotes/>
|
||||
<UpdateManagerOrganism/>
|
||||
</Layout>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(RoutesPage);
|
||||
|
||||
@@ -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 (
|
||||
<div className="settings-page-container">
|
||||
<div
|
||||
className="settings-page-container"
|
||||
style={{
|
||||
"--settings-hero-bg-start": token.colorBgContainer,
|
||||
"--settings-hero-bg-mid": token.colorFillQuaternary,
|
||||
"--settings-hero-bg-end": token.colorFillTertiary,
|
||||
"--settings-border": token.colorBorderSecondary,
|
||||
"--settings-shadow": token.boxShadowTertiary,
|
||||
"--settings-control-bg": token.colorFillQuaternary,
|
||||
"--settings-success-bg": token.colorSuccessBg,
|
||||
"--settings-success-border": token.colorSuccessBorder,
|
||||
"--settings-success-text": token.colorSuccessText,
|
||||
"--settings-error-bg": token.colorErrorBg,
|
||||
"--settings-error-border": token.colorErrorBorder,
|
||||
"--settings-error-text": token.colorErrorText
|
||||
}}
|
||||
>
|
||||
<div className="settings-page__hero">
|
||||
<Typography.Title level={3}>Settings</Typography.Title>
|
||||
<Typography.Paragraph type="secondary">
|
||||
Manage watcher behavior, notifications, and shop defaults in one compact workspace.
|
||||
</Typography.Paragraph>
|
||||
<div className="settings-page__hero-copy">
|
||||
<Typography.Title level={3}>Settings</Typography.Title>
|
||||
<Typography.Paragraph type="secondary">
|
||||
Manage watcher behavior, notifications, and shop defaults in one compact workspace.
|
||||
</Typography.Paragraph>
|
||||
</div>
|
||||
<div className="settings-page__hero-status">
|
||||
<WatcherManagerOrganism />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Row gutter={[16, 16]} align="top">
|
||||
<Col xs={24} xl={16}>
|
||||
<Space direction="vertical" size={16} className="settings-page__content-stack">
|
||||
<Card variant="borderless" className="settings-page__card settings-page__card--shop">
|
||||
<ShopSettingsOrganism />
|
||||
</Card>
|
||||
<div className="settings-page__secondary-grid">
|
||||
<Card variant="borderless" className="settings-page__card settings-page__card--filepaths">
|
||||
<FilePathsListOrganism />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} xl={8}>
|
||||
<Space direction="vertical" size={16} className="settings-page__side-stack">
|
||||
<Card variant="borderless" className="settings-page__card">
|
||||
<WatcherManagerOrganism />
|
||||
</Card>
|
||||
<Card variant="borderless" className="settings-page__card settings-page__card--automation">
|
||||
<Typography.Title level={5}>Automation</Typography.Title>
|
||||
<Typography.Paragraph type="secondary">
|
||||
Fine-tune polling, startup behavior, and desktop notifications.
|
||||
</Typography.Paragraph>
|
||||
<Space direction="vertical" size={10} className="settings-page__controls">
|
||||
<WatcherPollingMolecule />
|
||||
<NotificationsToggleAtom />
|
||||
<WatcherStartupAtom />
|
||||
</Space>
|
||||
</Card>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Card variant="borderless" className="settings-page__card settings-page__card--shop">
|
||||
<ShopSettingsOrganism />
|
||||
</Card>
|
||||
<Card variant="borderless" className="settings-page__card settings-page__card--automation">
|
||||
<Typography.Title level={3}>Automation</Typography.Title>
|
||||
<Typography.Paragraph type="secondary">
|
||||
Fine-tune polling, startup behavior, and desktop notifications.
|
||||
</Typography.Paragraph>
|
||||
<Space direction="vertical" size={10} className="settings-page__controls">
|
||||
<WatcherPollingMolecule />
|
||||
<NotificationsToggleAtom />
|
||||
<WatcherStartupAtom />
|
||||
</Space>
|
||||
</Card>
|
||||
</div>
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user