Additional Changes for calculations of RPS %.

This commit is contained in:
Patrick Fic
2020-10-16 15:14:25 -07:00
parent c94f525a3e
commit 584f43bc4e
27 changed files with 504 additions and 115 deletions

View File

@@ -184,76 +184,128 @@ async function DecodeTtlFile(extensionlessFilePath) {
async function DecodeLinFile(extensionlessFilePath) {
let dbf = await DBFFile.open(`${extensionlessFilePath}.LIN`);
let records = await dbf.readRecords();
let joblines = records.map((record) =>
_.transform(
_.pick(record, [
"LINE_NO",
"LINE_IND",
// "LINE_REF",
// "TRAN_CODE",
"DB_REF",
"UNQ_SEQ",
// "WHO_PAYS",
"LINE_DESC",
"PART_TYPE",
// "PART_DESCJ",
"GLASS_FLAG",
"OEM_PARTNO",
// "PRICE_INC",
// "ALT_PART_I",
// "TAX_PART",
"DB_PRICE",
"ACT_PRICE",
// "PRICE_J",
// "CERT_PART",
"PART_QTY",
// "ALT_CO_ID",
// "ALT_PARTNO",
// "ALT_OVERRD",
// "ALT_PARTM",
// "PRT_DSMK_P",
// "PRT_DSMK_M",
// "MOD_LBR_TY",
// "DB_HRS",
// "MOD_LB_HRS",
// "LBR_INC",
// "LBR_OP",
// "LBR_HRS_J",
// "LBR_TYP_J",
// "LBR_OP_J",
// "PAINT_STG",
// "PAINT_TONE",
// "LBR_TAX",
// "LBR_AMT",
// "MISC_AMT",
// "MISC_SUBLT",
// "MISC_TAX",
// "BETT_TYPE",
// "BETT_PCTG",
// "BETT_AMT",
// "BETT_TAX",
]),
function (result, val, key) {
//Required because unq_seq gets pulled as a numeric instaed of a string.
console.log("key", key);
if (key === "UNQ_SEQ") {
return (result[key.toLowerCase()] = val.toString());
}
return (result[key.toLowerCase()] = val);
}
)
);
let joblines = records
.map((record) => {
console.log(
"object",
_.pick(record, [
"LINE_NO",
"LINE_IND",
// "LINE_REF",
// "TRAN_CODE",
"DB_REF",
"UNQ_SEQ",
// "WHO_PAYS",
"LINE_DESC",
"PART_TYPE",
// "PART_DESCJ",
"GLASS_FLAG",
"OEM_PARTNO",
// "PRICE_INC",
// "ALT_PART_I",
// "TAX_PART",
"DB_PRICE",
"ACT_PRICE",
// "PRICE_J",
// "CERT_PART",
"PART_QTY",
// "ALT_CO_ID",
// "ALT_PARTNO",
// "ALT_OVERRD",
// "ALT_PARTM",
// "PRT_DSMK_P",
// "PRT_DSMK_M",
// "MOD_LBR_TY",
// "DB_HRS",
// "MOD_LB_HRS",
// "LBR_INC",
// "LBR_OP",
// "LBR_HRS_J",
// "LBR_TYP_J",
// "LBR_OP_J",
// "PAINT_STG",
// "PAINT_TONE",
// "LBR_TAX",
// "LBR_AMT",
// "MISC_AMT",
// "MISC_SUBLT",
// "MISC_TAX",
// "BETT_TYPE",
// "BETT_PCTG",
// "BETT_AMT",
// "BETT_TAX",
])
);
return _.transform(
_.pick(record, [
"LINE_NO",
"LINE_IND",
// "LINE_REF",
// "TRAN_CODE",
"DB_REF",
"UNQ_SEQ",
// "WHO_PAYS",
"LINE_DESC",
"PART_TYPE",
// "PART_DESCJ",
const m = joblines
"OEM_PARTNO",
// "PRICE_INC",
// "ALT_PART_I",
// "TAX_PART",
"DB_PRICE",
"ACT_PRICE",
// "PRICE_J",
// "CERT_PART",
"PART_QTY",
// "ALT_CO_ID",
// "ALT_PARTNO",
// "ALT_OVERRD",
// "ALT_PARTM",
// "PRT_DSMK_P",
// "PRT_DSMK_M",
// "MOD_LBR_TY",
// "DB_HRS",
// "MOD_LB_HRS",
// "LBR_INC",
// "LBR_OP",
// "LBR_HRS_J",
// "LBR_TYP_J",
// "LBR_OP_J",
// "PAINT_STG",
// "PAINT_TONE",
// "LBR_TAX",
// "LBR_AMT",
// "MISC_AMT",
// "MISC_SUBLT",
// "MISC_TAX",
// "BETT_TYPE",
// "BETT_PCTG",
// "BETT_AMT",
// "BETT_TAX",
"GLASS_FLAG",
]),
function (result, val, key) {
//Required because unq_seq gets pulled as a numeric instaed of a string.
console.log("key", key);
if (key === "UNQ_SEQ") {
return (result[key.toLowerCase()] = val.toString());
}
return (result[key.toLowerCase()] = val);
}
);
})
.filter(
(jobline) =>
jobline.PART_TYPE &&
jobline.part_type &&
!jobline.db_ref.startsWith("900") &&
(jobline.part_type && jobline.part_type.toUpperCase()) !== "PAG" &&
jobline.part_type.toUpperCase() !== "PAG" &&
jobline.part_type.toUpperCase() !== "PAS" &&
jobline.part_type.toUpperCase() !== "PASL" &&
jobline.part_type.toUpperCase() !== "PAE" &&
jobline.glass_flag === false
)
.map((jobline) => {
console.log("Massagejobline", jobline);
if (
(jobline.db_price === null || jobline.db_price === 0) &&
!!jobline.act_price &&
@@ -285,7 +337,6 @@ async function DecodeLinFile(extensionlessFilePath) {
}
delete jobline.glass_flag;
console.log(jobline);
return jobline;
});

View File

@@ -3,7 +3,6 @@ const ipcTypes = require("../../src/ipc.types");
const path = require("path");
const { DecodeEstimate } = require("../decoder/decoder");
const { BrowserWindow } = require("electron");
const _ = require("lodash");
const { store } = require("../electron-store");
const {
NewNotification,
@@ -26,7 +25,7 @@ async function StartWatcher() {
if (watcher) {
try {
console.log("Trying to close watcher - it already existed.111");
console.log("Trying to close watcher - it already existed.");
await watcher.close();
console.log("Watcher closed successfully!");

View File

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

View File

@@ -0,0 +1,23 @@
- args:
cascade: false
read_only: false
sql: CREATE EXTENSION IF NOT EXISTS pgcrypto;
type: run_sql
- args:
cascade: false
read_only: false
sql: "CREATE TABLE \"public\".\"targets\"(\"id\" uuid NOT NULL DEFAULT gen_random_uuid(),
\"updated_at\" timestamptz NOT NULL DEFAULT now(), \"created_at\" timestamptz
NOT NULL DEFAULT now(), \"label\" text NOT NULL, \"config\" jsonb NOT NULL DEFAULT
jsonb_build_object(), \"start_date\" date NOT NULL, \"end_date\" date, PRIMARY
KEY (\"id\") );\nCREATE OR REPLACE FUNCTION \"public\".\"set_current_timestamp_updated_at\"()\nRETURNS
TRIGGER AS $$\nDECLARE\n _new record;\nBEGIN\n _new := NEW;\n _new.\"updated_at\"
= NOW();\n RETURN _new;\nEND;\n$$ LANGUAGE plpgsql;\nCREATE TRIGGER \"set_public_targets_updated_at\"\nBEFORE
UPDATE ON \"public\".\"targets\"\nFOR EACH ROW\nEXECUTE PROCEDURE \"public\".\"set_current_timestamp_updated_at\"();\nCOMMENT
ON TRIGGER \"set_public_targets_updated_at\" ON \"public\".\"targets\" \nIS
'trigger to set value of column \"updated_at\" to current timestamp on row update';"
type: run_sql
- args:
name: targets
schema: public
type: add_existing_table_or_view

View File

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

View File

@@ -0,0 +1,20 @@
- args:
permission:
allow_aggregations: false
backend_only: false
columns:
- id
- updated_at
- created_at
- label
- config
- start_date
- end_date
computed_fields: []
filter: {}
limit: null
role: user
table:
name: targets
schema: public
type: create_select_permission

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,25 @@
- args:
role: user
table:
name: bodyshops
schema: public
type: drop_select_permission
- args:
permission:
allow_aggregations: false
columns:
- id
- created_at
- updated_at
- shopname
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,26 @@
- args:
role: user
table:
name: bodyshops
schema: public
type: drop_select_permission
- args:
permission:
allow_aggregations: false
columns:
- 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,6 @@
- args:
role: user
table:
name: bodyshops
schema: public
type: drop_update_permission

View File

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

View File

@@ -42,15 +42,27 @@ tables:
- role: user
permission:
columns:
- id
- created_at
- updated_at
- id
- shopname
- targets
- updated_at
filter:
associations:
user:
authid:
_eq: X-Hasura-User-Id
update_permissions:
- role: user
permission:
columns:
- targets
filter:
associations:
user:
authid:
_eq: X-Hasura-User-Id
check: null
- table:
schema: public
name: joblines

View File

@@ -0,0 +1,52 @@
import React, { useMemo } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../../redux/user/user.selectors";
import "./price-diff-pc-formatter.styles.scss";
import { AlertFilled } from "@ant-design/icons";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export function PriceDiffPcFormatterAtom({
bodyshop,
price_diff_pc,
group,
v_age,
}) {
const metTarget = useMemo(() => {
const targetsForGroup = bodyshop.targets[group];
if (!targetsForGroup) return 0;
const targetPc = targetsForGroup.filter(
(t) => t.ageGte <= v_age && (t.ageLt ? t.ageLt > v_age : true)
);
if (targetPc.length === 0) return false;
else if (targetPc.length === 1) return price_diff_pc >= targetPc[0].target;
else {
alert("Multiple targets match.");
return false;
}
}, [bodyshop, group, price_diff_pc, v_age]);
return (
<div
style={{
color: metTarget ? "green" : "red",
display: "flex",
alignItems: "center",
}}
>
{(price_diff_pc * 100).toFixed(1)}%
{price_diff_pc === 1 ? (
<AlertFilled style={{ color: "tomato" }} className="blink_me" />
) : null}
</div>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(PriceDiffPcFormatterAtom);

View File

@@ -0,0 +1,9 @@
.blink_me {
animation: blinker 1s linear infinite;
}
@keyframes blinker {
50% {
opacity: 0;
}
}

View File

@@ -0,0 +1,39 @@
import { Statistic } from "antd";
import React, { useMemo } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export function PriceDiffPcFormatterAtom({ bodyshop, group, v_age }) {
const metTarget = useMemo(() => {
const targetsForGroup = bodyshop.targets[group];
if (!targetsForGroup) return 0;
const targetPc = targetsForGroup.filter(
(t) => t.ageGte <= v_age && (t.ageLt ? t.ageLt > v_age : true)
);
if (targetPc.length === 0) return 0;
else if (targetPc.length === 1) return targetPc[0].target;
else {
return 0;
}
}, [bodyshop, group, v_age]);
return (
<Statistic
title="Target RPS %"
value={(metTarget * 100).toFixed(1)}
suffix="%"
/>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(PriceDiffPcFormatterAtom);

View File

@@ -1,4 +1,4 @@
import { Descriptions, Skeleton } from "antd";
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";
@@ -10,24 +10,17 @@ export default function JobsDetailDescriptionMolecule({ loading, job }) {
return (
<div>
<Descriptions
title={`${job.clm_no}${job.ins_co_nm ? ` | ${job.ins_co_nm}` : ""}`}
bordered
layout="vertical"
column={{ xxl: 5, xl: 4, lg: 3, md: 3, sm: 2, xs: 1 }}
>
<Descriptions.Item label="Claim No.">{job.clm_no}</Descriptions.Item>
<Descriptions.Item label="Ins Co. Nm.">
{job.ins_co_nm}
</Descriptions.Item>
<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="Claim Total">
<CurrencyFormatterAtom>{job.clm_total}</CurrencyFormatterAtom>
</Descriptions.Item>
<Descriptions.Item label="Group">{job.group}</Descriptions.Item>
<Descriptions.Item label="Age">{job.v_age}</Descriptions.Item>
</Descriptions>
<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="Claim Total">
<CurrencyFormatterAtom>{job.clm_total}</CurrencyFormatterAtom>
</Descriptions.Item>
<Descriptions.Item label="Group">{job.group}</Descriptions.Item>
<Descriptions.Item label="Age">{job.v_age}</Descriptions.Item>
</Descriptions>
</PageHeader>
</div>
);
}

View File

@@ -1,8 +1,10 @@
import { Table } from "antd";
import React from "react";
import CurrencyFormatterAtom from "../../atoms/currency-formatter/currency-formatter.atom";
import PriceDiffPcFormatterAtom from "../../atoms/price-diff-pc-formatter/price-diff-pc-formatter.atom";
export default function JobLinesTableMolecule({ loading, jobLines }) {
export default function JobLinesTableMolecule({ loading, job }) {
const { joblines } = job;
const columns = [
{
title: "#",
@@ -48,9 +50,26 @@ export default function JobLinesTableMolecule({ loading, jobLines }) {
),
},
{
title: "Qty.",
dataIndex: "part_qty",
key: "part_qty",
title: "Price Diff.",
dataIndex: "price_diff",
key: "price_diff",
render: (text, record) => (
<CurrencyFormatterAtom>{record.price_diff}</CurrencyFormatterAtom>
),
},
{
title: "Price Diff. %",
dataIndex: "price_diff_pc",
key: "price_diff_pc",
render: (text, record) => (
<PriceDiffPcFormatterAtom
price_diff_pc={record.price_diff_pc}
v_age={job.v_age}
group={job.group}
/>
),
},
];
@@ -62,7 +81,7 @@ export default function JobLinesTableMolecule({ loading, jobLines }) {
loading={loading}
size="small"
pagination={false}
dataSource={jobLines}
dataSource={joblines}
scroll={{
x: true,
//y: "40rem"

View File

@@ -0,0 +1,52 @@
import { Skeleton, Space, Statistic } from "antd";
import React, { useMemo } from "react";
import ErrorResultAtom from "../../atoms/error-result/error-result.atom";
import TargetPriceDiffPcAtom from "../../atoms/target-price-diff/target-price-diff-pc.atom";
import _ from "lodash";
import Dinero from "dinero.js";
export default function JobsTargetsStatsMolecule({ loading, job }) {
const currentRpsPc = useMemo(() => {
if (!job) {
return 0;
}
return (
(_.sum(job.joblines.map((jl) => jl.price_diff_pc)) /
job.joblines.length) *
100
).toFixed(1);
}, [job]);
const currentRpsDollars = useMemo(() => {
if (!job) {
return 0;
}
return job.joblines.reduce((acc, val) => {
console.log("val.price_diff :>> ", val.price_diff);
if (val.price_diff > 0) {
return acc.add(
Dinero({ amount: Math.round((val.price_diff || 0) * 100) })
);
} else {
return acc;
}
}, Dinero());
}, [job]);
if (loading) return <Skeleton active />;
if (!job) return <ErrorResultAtom title="Error displaying job data." />;
return (
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-around",
}}
>
<TargetPriceDiffPcAtom v_age={job.v_age} group={job.group} />
<Statistic title="Current RPS %" value={currentRpsPc} suffix="%" />
<Statistic title="Current RPS $" value={currentRpsDollars.toFormat()} />
</div>
);
}

View File

@@ -8,6 +8,7 @@ import { selectSelectedJobId } from "../../../redux/application/application.sele
import ErrorResultAtom from "../../atoms/error-result/error-result.atom";
import JobsDetailDescriptionMolecule from "../../molecules/jobs-detail-description/jobs-detail-description.molecule";
import JobsLinesTableMolecule from "../../molecules/jobs-lines-table/jobs-lines-table.molecule";
import JobsTargetsStatsMolecule from "../../molecules/jobs-targets-stats/jobs-targets-stats.molecule";
import "./jobs-detail.organism.styles.scss";
const mapStateToProps = createStructuredSelector({
@@ -32,17 +33,21 @@ export function JobsDetailOrganism({ selectedJobId }) {
errorMessage={JSON.stringify(error)}
/>
);
return (
<div className="jobs-detail-container">
<JobsDetailDescriptionMolecule
loading={loading}
job={data ? data.jobs_by_pk : null}
/>
<JobsTargetsStatsMolecule
loading={loading}
job={data ? data.jobs_by_pk : null}
/>
<JobsLinesTableMolecule
loading={loading}
jobLines={data ? data.jobs_by_pk.joblines : []}
job={data ? data.jobs_by_pk : {}}
/>
{selectedJobId}
</div>
);
}

View File

@@ -1,6 +1,6 @@
import { SyncOutlined } from "@ant-design/icons";
import { useQuery } from "@apollo/client";
import { Button, List, Space, Spin, Typography } from "antd";
import { Button, Divider, List, Space, Spin } from "antd";
import React, { useState } from "react";
import InfiniteScroll from "react-infinite-scroller";
import { connect } from "react-redux";
@@ -87,6 +87,7 @@ export function JobsTableOrganism({ selectedJobId, setSelectedJobId }) {
useWindow={false}
>
<List
bordered
dataSource={data ? data.jobs : []}
renderItem={(item) => (
<List.Item
@@ -101,22 +102,24 @@ export function JobsTableOrganism({ selectedJobId, setSelectedJobId }) {
: ""
}`}
>
<div style={{ display: "flex" }}>
<Typography.Title level={4} style={{ flex: 1 }}>
{`${item.clm_no}${
item.ins_co_nm ? ` | ${item.ins_co_nm}` : ""
}`}
</Typography.Title>
<span className="job-list-last-updated-time">
<div
style={{
display: "flex",
justifyContent: "space-between",
}}
>
<strong>{item.clm_no || "No Claim Number"}</strong>
<span style={{ fontStyle: "italic" }}>
<TimeAgoFormatter>{item.updated_at}</TimeAgoFormatter>
</span>
</div>
<Space>
<span>{`${item.ownr_fn} ${item.ownr_ln}`}</span>
<span>
{`${item.v_model_yr} ${item.v_makedesc} ${item.v_model} ${item.v_vin}`}
</span>
</Space>
<div>{item.ins_co_nm || "No Insurance Co."}</div>
<div>{`${item.ownr_fn} ${item.ownr_ln}`}</div>
<div>
{`${item.v_model_yr} ${item.v_makedesc} ${item.v_model} ${item.v_vin}`}
</div>
</div>
</List.Item>
)}

View File

@@ -2,8 +2,8 @@ import { Col, Row } from "antd";
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import JobsListOrganism from "../../organisms/jobs-list/jobs-list.organism";
import JobsDetailOrganism from "../../organisms/jobs-detail/jobs-detail.organism";
import JobsListOrganism from "../../organisms/jobs-list/jobs-list.organism";
const mapStateToProps = createStructuredSelector({});
const mapDispatchToProps = (dispatch) => ({});
@@ -12,11 +12,10 @@ export function JobsPage() {
return (
<div style={{ height: "100%" }}>
<Row gutter={[16, 16]} style={{ height: "100%" }}>
<Col span={10} style={{ height: "100%" }}>
<Col span={6} style={{ height: "100%" }}>
<JobsListOrganism />
</Col>
<Col span={14} style={{ height: "100%" }}>
<Col span={18} style={{ height: "100%" }}>
<JobsDetailOrganism />
</Col>
</Row>

View File

@@ -4,6 +4,7 @@ export const QUERY_BODYSHOP = gql`
bodyshops {
id
shopname
targets
}
}
`;

View File

@@ -1,9 +1,16 @@
import gql from "graphql-tag";
export const QUERY_GROUPS_BY_MAKE_TYPE = gql`
query QUERY_GROUPS_BY_MAKE_TYPE($make: String!, $type: String) {
query QUERY_GROUPS_BY_MAKE_TYPE(
$make: String!
$type: String
$isNull: Boolean
) {
veh_groups(
where: { _and: { make: { _eq: $make } }, type: { _eq: $type } }
where: {
_and: { make: { _eq: $make } }
type: { _eq: $type, _is_null: $isNull }
}
) {
id
group

View File

@@ -166,7 +166,11 @@ const DetermineVehicleGroup = async (job) => {
query: QUERY_GROUPS_BY_MAKE_TYPE,
variables: {
make: job.v_makedesc.toUpperCase(),
type: job.v_type ? job.v_type.toUpperCase() : null,
type:
job.v_type && job.v_type.toUpperCase() !== "PC"
? job.v_type.toUpperCase()
: null,
isNull: job.v_type && job.v_type.toUpperCase() !== "PC" ? false : true,
},
});