Added manual group changing, stbility improvements.

This commit is contained in:
Patrick Fic
2020-10-21 19:36:54 -07:00
parent 34e244783c
commit 3817f8615e
30 changed files with 362 additions and 42 deletions

View File

@@ -23,13 +23,13 @@ async function ImportJob(path) {
NewNotification({
title: "Job Uploaded",
body: "A new job has been uploaded.",
}).show();
});
} else {
log.info(`Ignored job. ${newJob.ERROR}`);
NewNotification({
title: "Job Ignored",
body: newJob.ERROR,
}).show();
});
}
}

View File

@@ -2,6 +2,7 @@ const Store = require("electron-store");
const store = new Store({
defaults: {
enableNotifications: true,
filePaths: [],
accepted_ins_co: [],
polling: {

View File

@@ -17,7 +17,7 @@ async function StartWatcher() {
NewNotification({
title: "RPS Watcher cannot start",
body: "Please set the appropriate file paths and try again.",
}).show();
});
log.warn("Cannot start watcher. No file paths set.");
return [];
}
@@ -87,7 +87,7 @@ function onWatcherReady() {
NewNotification({
title: "RPS Watcher Started",
body: "Newly exported estimates will be automatically uploaded.",
}).show();
});
console.log("Confirmed watched paths:", watcher.getWatched());
}
@@ -99,7 +99,7 @@ async function StopWatcher() {
NewNotification({
title: "RPS Watcher Stopped",
body: "Estimates will not be automatically uploaded.",
}).show();
});
}
exports.StartWatcher = StartWatcher;

View File

@@ -1,11 +1,14 @@
const { Notification } = require("electron");
const path = require("path");
const { store } = require("../electron-store");
function NewNotification(config) {
return Notification({
icon: path.join(__dirname, "../../src/assets/logo512.png"),
...config,
});
const enableNotifications = store.get("enableNotifications");
if (enableNotifications) {
Notification({
icon: path.join(__dirname, "../../src/assets/logo512.png"),
...config,
}).show();
}
}
exports.NewNotification = NewNotification;

View File

@@ -0,0 +1,5 @@
- args:
cascade: false
read_only: false
sql: ALTER TABLE "public"."bodyshops" DROP COLUMN "groups";
type: run_sql

View File

@@ -0,0 +1,6 @@
- args:
cascade: false
read_only: false
sql: ALTER TABLE "public"."bodyshops" ADD COLUMN "groups" jsonb NOT NULL DEFAULT
jsonb_build_array();
type: run_sql

View File

@@ -0,0 +1,27 @@
- args:
role: user
table:
name: bodyshops
schema: public
type: drop_select_permission
- args:
permission:
allow_aggregations: false
columns:
- accepted_ins_co
- created_at
- id
- shopname
- targets
- updated_at
computed_fields: []
filter:
associations:
user:
authid:
_eq: X-Hasura-User-Id
role: user
table:
name: bodyshops
schema: public
type: create_select_permission

View File

@@ -0,0 +1,28 @@
- args:
role: user
table:
name: bodyshops
schema: public
type: drop_select_permission
- args:
permission:
allow_aggregations: false
columns:
- accepted_ins_co
- created_at
- groups
- id
- shopname
- targets
- updated_at
computed_fields: []
filter:
associations:
user:
authid:
_eq: X-Hasura-User-Id
role: user
table:
name: bodyshops
schema: public
type: create_select_permission

View File

@@ -0,0 +1,23 @@
- args:
role: user
table:
name: bodyshops
schema: public
type: drop_update_permission
- args:
permission:
columns:
- accepted_ins_co
- shopname
- targets
filter:
associations:
user:
authid:
_eq: X-Hasura-User-Id
set: {}
role: user
table:
name: bodyshops
schema: public
type: create_update_permission

View File

@@ -0,0 +1,24 @@
- args:
role: user
table:
name: bodyshops
schema: public
type: drop_update_permission
- args:
permission:
columns:
- accepted_ins_co
- groups
- shopname
- targets
filter:
associations:
user:
authid:
_eq: X-Hasura-User-Id
set: {}
role: user
table:
name: bodyshops
schema: public
type: create_update_permission

View File

@@ -0,0 +1,6 @@
- args:
role: user
table:
name: jobs
schema: public
type: drop_delete_permission

View File

@@ -0,0 +1,14 @@
- args:
permission:
backend_only: false
filter:
bodyshop:
associations:
user:
authid:
_eq: X-Hasura-User-Id
role: user
table:
name: jobs
schema: public
type: create_delete_permission

View File

@@ -44,6 +44,7 @@ tables:
columns:
- accepted_ins_co
- created_at
- groups
- id
- shopname
- targets
@@ -58,6 +59,7 @@ tables:
permission:
columns:
- accepted_ins_co
- groups
- shopname
- targets
filter:
@@ -277,6 +279,15 @@ tables:
authid:
_eq: X-Hasura-User-Id
check: null
delete_permissions:
- role: user
permission:
filter:
bodyshop:
associations:
user:
authid:
_eq: X-Hasura-User-Id
- table:
schema: public
name: users

View File

@@ -0,0 +1,39 @@
import { Switch } from "antd";
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import ipcTypes from "../../../ipc.types";
import { selectSettings } from "../../../redux/application/application.selectors";
import DataLabel from "../../atoms/data-label/data-label.atom";
const { ipcRenderer } = window;
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
appSettings: selectSettings,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export function WatcherPollingMolecule({ appSettings }) {
const handleChange = (val) => {
ipcRenderer.send(ipcTypes.default.store.set, {
enableNotifications: val,
});
};
return (
<div>
<DataLabel label="Notifications Enabled?">
<Switch
onChange={handleChange}
checked={appSettings && appSettings.enableNotifications}
/>
</DataLabel>
</div>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(WatcherPollingMolecule);

View File

@@ -1,15 +1,20 @@
export default (part_type) => {
switch (part_type) {
case "PAA":
case "PAL":
case "PAC":
return "A/M";
case "PAE":
return "Exist.";
return "Ex.";
case "PAN":
case "PAP":
return "OEM";
case "PAP":
return "OEMP";
case "PAL":
return "LKQ";
case "PAC":
return "A/M (PAC)";
case "PAR":
return "A/M (PAR)";
default:
return part_type;
}

View File

@@ -12,7 +12,7 @@ export default function TimeAgoFormatter(props) {
}, [m]);
return props.children ? (
<Tooltip placement="top" title={m.format("MM/DD/YYY hh:mm A")}>
<Tooltip placement="top" title={m.format("MM/DD/YYYY hh:mm:ss A")}>
{timestampString}
</Tooltip>
) : null;

View File

@@ -0,0 +1,53 @@
import { DownOutlined, LoadingOutlined } from "@ant-design/icons";
import { useMutation } from "@apollo/client";
import { Dropdown, Menu, message } from "antd";
import React, { useState } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { UPDATE_JOB } from "../../../graphql/jobs.queries";
import { selectBodyshop } from "../../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(JobGroupMolecule);
export function JobGroupMolecule({ bodyshop, jobId, group }) {
const [loading, setLoading] = useState(false);
const [updateJob] = useMutation(UPDATE_JOB);
const handleMenuClick = async (value) => {
setLoading(true);
const result = await updateJob({
variables: { jobId: jobId, job: { group: value.key } },
});
if (!result.errors) {
message.success("Close date updated.");
} else {
message.error("Error updating job.");
}
setLoading(false);
};
const menu = (
<Menu onClick={handleMenuClick}>
{bodyshop.groups.map((g, idx) => (
<Menu.Item key={g}>{g}</Menu.Item>
))}
</Menu>
);
return (
<Dropdown overlay={menu} trigger={["click"]}>
<a href=" #" onClick={(e) => e.preventDefault()}>
{group}
<DownOutlined />
{loading && <LoadingOutlined />}
</a>
</Dropdown>
);
}

View File

@@ -2,8 +2,9 @@ import { Descriptions, PageHeader, Skeleton } from "antd";
import React from "react";
import CurrencyFormatterAtom from "../../atoms/currency-formatter/currency-formatter.atom";
import ErrorResultAtom from "../../atoms/error-result/error-result.atom";
import CloseDateDisplayMolecule from "../close-date-display/close-date-display.molecule";
import TimeAgoFormatter from "../../atoms/time-ago-formatter/time-ago-formatter.atom";
import CloseDateDisplayMolecule from "../close-date-display/close-date-display.molecule";
import JobGroupMolecule from "../job-group/job-group.molecule";
export default function JobsDetailDescriptionMolecule({ loading, job }) {
if (loading) return <Skeleton active />;
@@ -15,11 +16,13 @@ export default function JobsDetailDescriptionMolecule({ loading, job }) {
<PageHeader ghost={false} title={job.clm_no} subTitle={job.ins_co_nm}>
<Descriptions column={{ xxl: 5, xl: 4, lg: 3, md: 3, sm: 2, xs: 1 }}>
<Descriptions.Item label="Owner">{`${job.ownr_fn} ${job.ownr_ln}`}</Descriptions.Item>
<Descriptions.Item label="Vehicle">{`${job.v_model_yr} ${job.v_makedesc} ${job.v_model}`}</Descriptions.Item>
<Descriptions.Item label="Vehicle">{`${job.v_model_yr} ${job.v_makedesc} ${job.v_model} (${job.v_type})`}</Descriptions.Item>
<Descriptions.Item label="Claim Total">
<CurrencyFormatterAtom>{job.clm_total}</CurrencyFormatterAtom>
</Descriptions.Item>
<Descriptions.Item label="Group">{job.group}</Descriptions.Item>
<Descriptions.Item label="Group">
<JobGroupMolecule jobId={job.id} group={job.group} />
</Descriptions.Item>
<Descriptions.Item label="Age">{job.v_age}</Descriptions.Item>
<Descriptions.Item label="Close Date">
<CloseDateDisplayMolecule

View File

@@ -1,5 +1,6 @@
import { Input, Table } from "antd";
import React, { useState } from "react";
import { alphaSort } from "../../../util/sorters";
import CurrencyFormatterAtom from "../../atoms/currency-formatter/currency-formatter.atom";
import IgnoreJobLine from "../../atoms/ignore-job-line/ignore-job-line.atom";
import partTypeConverterAtom from "../../atoms/part-type-converter/part-type-converter.atom";
@@ -14,33 +15,38 @@ export default function JobLinesTableMolecule({ loading, job }) {
title: "#",
dataIndex: "line_no",
key: "line_no",
sorter: (a, b) => a.line_no - b.line_no,
},
{
title: "S#",
dataIndex: "line_ind",
key: "line_ind",
sorter: (a, b) => alphaSort(a.line_ind, b.line_ind),
},
{
title: "Line Description",
dataIndex: "line_desc",
key: "line_desc",
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
},
{
title: "Part Type",
dataIndex: "part_type",
key: "part_type",
sorter: (a, b) => alphaSort(a.part_type, b.part_type),
render: (text, record) => partTypeConverterAtom(text),
},
{
title: "Part Number",
dataIndex: "oem_partno",
key: "oem_partno",
sorter: (a, b) => alphaSort(a.oem_partno, b.oem_partno),
},
{
title: "Database Price",
dataIndex: "db_price",
key: "db_price",
sorter: (a, b) => a.db_price - b.db_price,
render: (text, record) => (
<CurrencyFormatterAtom>{record.db_price}</CurrencyFormatterAtom>
),
@@ -49,7 +55,7 @@ export default function JobLinesTableMolecule({ loading, job }) {
title: "Actual Price",
dataIndex: "act_price",
key: "act_price",
sorter: (a, b) => a.act_price - b.act_price,
render: (text, record) => (
<CurrencyFormatterAtom>{record.act_price}</CurrencyFormatterAtom>
),
@@ -58,7 +64,7 @@ export default function JobLinesTableMolecule({ loading, job }) {
title: "Price Diff.",
dataIndex: "price_diff",
key: "price_diff",
sorter: (a, b) => a.price_diff - b.price_diff,
render: (text, record) => (
<CurrencyFormatterAtom>{record.price_diff}</CurrencyFormatterAtom>
),
@@ -67,7 +73,7 @@ export default function JobLinesTableMolecule({ loading, job }) {
title: "Price Diff. %",
dataIndex: "price_diff_pc",
key: "price_diff_pc",
sorter: (a, b) => a.price_diff_pc - b.price_diff_pc,
render: (text, record) => (
<PriceDiffPcFormatterAtom
price_diff_pc={record.price_diff_pc}

View File

@@ -25,16 +25,18 @@ export function JobsTargetsStatsMolecule({
job,
selectedJobTargetPc,
}) {
const currentRpsDollars = useCallback(CalculateJobRpsDollars(job), [job]);
const { actPriceSum, jobRpsDollars } = useCallback(
CalculateJobRpsDollars(job, true),
[job]
);
const currentRpsPc = useCallback(CalculateJobRpsPc(job, currentRpsDollars), [
job,
currentRpsDollars,
]);
const { dbPriceSum, jobRpsPc } = useCallback(
CalculateJobRpsPc(job, jobRpsDollars, true),
[job, jobRpsDollars]
);
if (loading) return <Skeleton active />;
if (!job) return <ErrorResultAtom title="Error displaying job data." />;
return (
<div
style={{
@@ -53,12 +55,18 @@ export function JobsTargetsStatsMolecule({
<Statistic
title="Current RPS %"
valueStyle={{
color: selectedJobTargetPc > currentRpsPc ? "tomato" : "seagreen",
color: selectedJobTargetPc > (jobRpsPc || 0) ? "tomato" : "seagreen",
}}
value={(currentRpsPc * 100).toFixed(1)}
value={((jobRpsPc || 0) * 100).toFixed(1)}
suffix="%"
/>
<Statistic title="Current RPS $" value={currentRpsDollars.toFormat()} />
<Statistic
title="Target RPS $"
value={actPriceSum.percentage(selectedJobTargetPc * 100).toFormat()}
/>
<Statistic title="Current RPS $" value={jobRpsDollars.toFormat()} />
<Statistic title="DB Price Total" value={dbPriceSum.toFormat()} />
<Statistic title="Actual Price Total" value={actPriceSum.toFormat()} />
</div>
);
}

View File

@@ -55,16 +55,16 @@ export function ReportingTotalsStatsMolecule({ reportingLoading, scoreCard }) {
title="Current RPS %"
valueStyle={{
color:
scoreCard.currentRpsPc <= scoreCard.targetRpsPc
(scoreCard.currentRpsPc || 0) <= (scoreCard.targetRpsPc || 0)
? "tomato"
: "seagreen",
}}
value={(scoreCard.currentRpsPc * 100).toFixed(1)}
value={((scoreCard.currentRpsPc || 0) * 100).toFixed(1)}
suffix="%"
/>
<Statistic
title="Target RPS %"
value={(scoreCard.targetRpsPc * 100).toFixed(1)}
value={((scoreCard.targetRpsPc || 0) * 100).toFixed(1)}
suffix="%"
/>
</div>

View File

@@ -7,6 +7,7 @@ import {
selectScanEstimates,
selectScanLoading,
} from "../../../redux/scan/scan.selectors";
import { alphaSort } from "../../../util/sorters";
import LastScannedAtom from "../../atoms/last-scanned/last-scanned.atom";
import ScanRefreshAtom from "../../atoms/scan-refresh/scan-refresh.atom";
@@ -25,21 +26,25 @@ export function ScanEstimateListMolecule({ scanLoading, estimates }) {
title: "Claim No.",
dataIndex: "clm_no",
key: "clm_no",
sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
},
{
title: "Ins Co.",
dataIndex: "ins_co_nm",
key: "ins_co_nm",
sorter: (a, b) => alphaSort(a.ins_co_nm, b.ins_co_nm),
},
{
title: "First Name",
dataIndex: "ownr_fn",
key: "ownr_fn",
sorter: (a, b) => alphaSort(a.ownr_fn, b.ownr_fn),
},
{
title: "Last Name",
dataIndex: "ownr_ln",
key: "ownr_ln",
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
},
{
title: "Vehicle",
@@ -48,6 +53,11 @@ export function ScanEstimateListMolecule({ scanLoading, estimates }) {
render: (text, record) =>
`${record.v_model_yr} ${record.v_makedesc} ${record.v_model} (${record.v_type})`,
},
{
title: "File Path",
dataIndex: "filepath",
key: "filepath",
},
{
title: "Import",
dataIndex: "import",

View File

@@ -1,10 +1,18 @@
import { Button, Input, Form, Select, InputNumber, Typography } from "antd";
import FormListMoveArrows from "../../atoms/form-list-move-arrows/form-list-move-arrows.atom";
import React from "react";
import LayoutFormRow from "../../atoms/layout-form-row/layout-form-row.atom";
import { DeleteFilled } from "@ant-design/icons";
import { Button, Form, Input, InputNumber, Select, Typography } from "antd";
import React, { useState } from "react";
import FormListMoveArrows from "../../atoms/form-list-move-arrows/form-list-move-arrows.atom";
import LayoutFormRow from "../../atoms/layout-form-row/layout-form-row.atom";
export default function ShopSettingsFormMolecule({ form, saveLoading }) {
const [groupOptions, setGroupOptions] = useState(
form.getFieldValue("groups") || []
);
const handleBlur = () => {
console.log(form.getFieldValue("groups") || []);
setGroupOptions(form.getFieldValue("groups") || []);
};
return (
<div>
<Typography.Title>Shop Settings</Typography.Title>
@@ -40,6 +48,19 @@ export default function ShopSettingsFormMolecule({ form, saveLoading }) {
>
<Select mode="tags" />
</Form.Item>
<Form.Item
name="groups"
label="Available Groupings (Must match below)"
rules={[
{
required: true,
type: "array",
},
]}
>
<Select mode="tags" onBlur={handleBlur} />
</Form.Item>
</LayoutFormRow>
<Typography.Title level={4}>Group Definitions</Typography.Title>
<Form.List name={["targets"]}>
@@ -59,7 +80,13 @@ export default function ShopSettingsFormMolecule({ form, saveLoading }) {
},
]}
>
<Input />
<Select>
{groupOptions.map((item, idx) => (
<Select.Option key={idx} value={item}>
{item}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item

View File

@@ -6,7 +6,7 @@ import { createStructuredSelector } from "reselect";
import { QUERY_JOB_BY_PK } from "../../../graphql/jobs.queries";
import { setSelectedJobTargetPc } from "../../../redux/application/application.actions";
import { selectSelectedJobId } from "../../../redux/application/application.selectors";
import { DeleteJobAtom } from "../../atoms/delete-job/delete-job.atom";
import DeleteJobAtom from "../../atoms/delete-job/delete-job.atom";
import ErrorResultAtom from "../../atoms/error-result/error-result.atom";
import JobsPartsGraphAtom from "../../atoms/jobs-parts-graph/jobs-parts-graph.atom";
import JobsDetailDescriptionMolecule from "../../molecules/jobs-detail-description/jobs-detail-description.molecule";

View File

@@ -1,6 +1,7 @@
import { Col, Row } from "antd";
import React, { useEffect } from "react";
import ipcTypes from "../../../ipc.types";
import NotificationsToggleAtom from "../../atoms/notifications-toggle/notifications-toggle.atom";
import WatcherPollingMolecule from "../../molecules/watcher-polling/watcher-polling.molecule";
import FilePathsListOrganism from "../../organisms/filepaths-list/filepaths-list.organism";
import ShopSettingsOrganism from "../../organisms/shop-settings/shop-settings.organism";
@@ -21,6 +22,7 @@ export default function SettingsPage() {
<Col span={6}>
<WatcherManagerOrganism />
<WatcherPollingMolecule />
<NotificationsToggleAtom />
</Col>
</Row>

View File

@@ -6,9 +6,11 @@ export const QUERY_BODYSHOP = gql`
shopname
targets
accepted_ins_co
groups
}
}
`;
export const UPDATE_SHOP = gql`
mutation UPDATE_SHOP($id: uuid, $shop: bodyshops_set_input!) {
update_bodyshops(where: { id: { _eq: $id } }, _set: $shop) {
@@ -17,6 +19,7 @@ export const UPDATE_SHOP = gql`
shopname
targets
accepted_ins_co
groups
}
}
}

View File

@@ -87,6 +87,7 @@ export const QUERY_JOB_BY_PK = gql`
updated_at
group
v_age
v_type
loss_date
close_date
updated_at

View File

@@ -89,7 +89,10 @@ export const GetSupplementDelta = async (jobId, existingLinesO, newLines) => {
//Found a relevant matching line. Add it to lines to update.
linesToUpdate.push({
id: existingLines[matchingIndex].id,
newData: newLine,
newData: {
...newLine,
ignore: existingLines[matchingIndex].ignore,
},
});
//Splice out item we found for performance.
existingLines.splice(matchingIndex, 1);

View File

@@ -4,7 +4,7 @@ const INITIAL_STATE = {
watchedPaths: [],
watcherError: null,
selectedJobId: null,
selectedJobTargetPc: 100,
selectedJobTargetPc: 0,
settings: {},
};

12
src/util/sorters.js Normal file
View File

@@ -0,0 +1,12 @@
export function alphaSort(a, b) {
let A;
let B;
A = a ? a.toLowerCase() : "";
B = b ? b.toLowerCase() : "";
if (A < B)
//sort string ascending
return -1;
if (A > B) return 1;
return 0; //default return value (no sorting)
}