WIP for Reporting. Pulled out calculations to utility functions.
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
import { Skeleton, Statistic } from "antd";
|
||||
import Dinero from "dinero.js";
|
||||
import React, { useMemo } from "react";
|
||||
import React, { useCallback } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectSelectedJobTargetPc } from "../../../redux/application/application.selectors";
|
||||
import {
|
||||
CalculateJobRpsDollars,
|
||||
CalculateJobRpsPc,
|
||||
} from "../../../util/CalculateJobRps";
|
||||
import ErrorResultAtom from "../../atoms/error-result/error-result.atom";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
@@ -22,35 +25,12 @@ export function JobsTargetsStatsMolecule({
|
||||
job,
|
||||
selectedJobTargetPc,
|
||||
}) {
|
||||
const currentRpsDollars = useMemo(() => {
|
||||
if (!job) {
|
||||
return 0;
|
||||
}
|
||||
return job.joblines
|
||||
.filter((j) => !j.ignore)
|
||||
.reduce((acc, val) => {
|
||||
if (val.price_diff > 0) {
|
||||
return acc.add(
|
||||
Dinero({ amount: Math.round((val.price_diff || 0) * 100) })
|
||||
);
|
||||
} else {
|
||||
return acc;
|
||||
}
|
||||
}, Dinero());
|
||||
}, [job]);
|
||||
const currentRpsDollars = useCallback(CalculateJobRpsDollars(job), [job]);
|
||||
|
||||
const currentRpsPc = useMemo(() => {
|
||||
//TODO Redo this to do total of db price - act price / db price
|
||||
if (!job) {
|
||||
return 0;
|
||||
}
|
||||
const dbPriceSum = job.joblines
|
||||
.filter((j) => !j.ignore)
|
||||
.reduce((acc, val) => {
|
||||
return acc + val.db_price;
|
||||
}, 0);
|
||||
return (currentRpsDollars.getAmount() / dbPriceSum).toFixed(1);
|
||||
}, [job, currentRpsDollars]);
|
||||
const currentRpsPc = useCallback(CalculateJobRpsPc(job, currentRpsDollars), [
|
||||
job,
|
||||
currentRpsDollars,
|
||||
]);
|
||||
|
||||
if (loading) return <Skeleton active />;
|
||||
if (!job) return <ErrorResultAtom title="Error displaying job data." />;
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Button, DatePicker, Form } from "antd";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { queryReportingData } from "../../../redux/reporting/reporting.actions";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
queryReportingData: (dates) => dispatch(queryReportingData(dates)),
|
||||
});
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ReportingDatesMolecule);
|
||||
|
||||
export function ReportingDatesMolecule({ queryReportingData }) {
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const handleFinish = (values) => {
|
||||
console.log("values", values);
|
||||
queryReportingData({
|
||||
startDate: values.dateRange[0],
|
||||
endDate: values.dateRange[1],
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Form form={form} onFinish={handleFinish}>
|
||||
<div style={{ display: "flex" }}>
|
||||
<Form.Item
|
||||
label="Close Date Between"
|
||||
name="dateRange"
|
||||
rules={[{ type: "array", required: true }]}
|
||||
>
|
||||
<DatePicker.RangePicker />
|
||||
</Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
Run Search
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { UserOutlined } from "@ant-design/icons";
|
||||
import { LogoutOutlined } from "@ant-design/icons";
|
||||
import { Menu } from "antd";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
@@ -11,7 +11,7 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
export function SiderSignOut({ signOutStart, ...restProps }) {
|
||||
return (
|
||||
<Menu.Item
|
||||
icon={<UserOutlined />}
|
||||
icon={<LogoutOutlined />}
|
||||
{...restProps}
|
||||
onClick={() => signOutStart()}
|
||||
>
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
PieChartOutlined,
|
||||
SettingFilled,
|
||||
CloseOutlined,
|
||||
BarChartOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { Menu } from "antd";
|
||||
import React from "react";
|
||||
@@ -19,6 +20,9 @@ export default function SiderMenuOrganism() {
|
||||
<Menu.Item key="/" icon={<PieChartOutlined />}>
|
||||
<Link to="/">Jobs</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="/reporting" icon={<BarChartOutlined />}>
|
||||
<Link to="/reporting">Reporting</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="/settings" icon={<SettingFilled />}>
|
||||
<Link to="/settings">Settings</Link>
|
||||
</Menu.Item>
|
||||
|
||||
10
src/components/pages/reporting/reporting.page.jsx
Normal file
10
src/components/pages/reporting/reporting.page.jsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from "react";
|
||||
import ReportingDatesMolecule from "../../molecules/reporting-dates/reporting-dates.molecule";
|
||||
|
||||
export default function ReportingPage() {
|
||||
return (
|
||||
<div>
|
||||
<ReportingDatesMolecule />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,11 +3,13 @@ import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { Route, Switch } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import SiderMenuOrganism from "../../organisms/sider-menu/sider-menu.organism";
|
||||
import Jobs from "../jobs/jobs.page";
|
||||
import SettingsPage from "../settings/settings.page";
|
||||
import { selectBodyshop } from "../../../redux/user/user.selectors";
|
||||
import ErrorResultAtom from "../../atoms/error-result/error-result.atom";
|
||||
import SiderMenuOrganism from "../../organisms/sider-menu/sider-menu.organism";
|
||||
import Jobs from "../jobs/jobs.page";
|
||||
import ReportingPage from "../reporting/reporting.page";
|
||||
import SettingsPage from "../settings/settings.page";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop });
|
||||
const mapDispatchToProps = (dispatch) => ({});
|
||||
|
||||
@@ -32,6 +34,7 @@ export function RoutesPage({ bodyshop }) {
|
||||
<Layout.Content style={{ margin: "1rem", height: "100%" }}>
|
||||
<Switch>
|
||||
<Route exact path="/settings" component={SettingsPage} />
|
||||
<Route exact path="/reporting" component={ReportingPage} />
|
||||
<Route path="/" component={Jobs} />
|
||||
</Switch>
|
||||
</Layout.Content>
|
||||
|
||||
@@ -28,11 +28,3 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// <Button
|
||||
// onClick={() => {
|
||||
// ipcRenderer.send(ipcTypes.default.filewatcher.start);
|
||||
// }}
|
||||
// >
|
||||
// Start Watcher
|
||||
// </Button>
|
||||
|
||||
47
src/graphql/reporting.queries.js
Normal file
47
src/graphql/reporting.queries.js
Normal file
@@ -0,0 +1,47 @@
|
||||
import gql from "graphql-tag";
|
||||
|
||||
export const REPORTING_GET_JOBS = gql`
|
||||
query REPORTING_GET_JOBS($startDate: date, $endDate: date) {
|
||||
jobs(
|
||||
where: {
|
||||
_and: [
|
||||
{ close_date: { _gte: $startDate } }
|
||||
{ close_date: { _lte: $endDate } }
|
||||
{ close_date: { _is_null: false } }
|
||||
]
|
||||
}
|
||||
) {
|
||||
ownr_ln
|
||||
ownr_fn
|
||||
ins_co_nm
|
||||
group
|
||||
clm_total
|
||||
clm_no
|
||||
close_date
|
||||
id
|
||||
loss_date
|
||||
updated_at
|
||||
v_age
|
||||
v_makedesc
|
||||
v_model
|
||||
v_model_yr
|
||||
joblines {
|
||||
act_price
|
||||
db_price
|
||||
part_qty
|
||||
part_type
|
||||
price_diff
|
||||
price_diff_pc
|
||||
updated_at
|
||||
oem_partno
|
||||
line_no
|
||||
line_ind
|
||||
line_desc
|
||||
ignore
|
||||
id
|
||||
db_ref
|
||||
unq_seq
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -1,4 +1,5 @@
|
||||
import { all, call, takeLatest, select, put } from "redux-saga/effects";
|
||||
import GetJobTarget from "../../util/GetJobTarget";
|
||||
import { setSelectedJobTargetPcSuccess } from "./application.actions";
|
||||
import ApplicationActionTypes from "./application.types";
|
||||
|
||||
@@ -12,17 +13,18 @@ export function* CalculateTarget({ payload }) {
|
||||
const { group, v_age } = payload;
|
||||
const targets = yield select((state) => state.user.bodyshop.targets);
|
||||
|
||||
const targetsForGroup = targets.filter((t) => t.group === 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) yield put(setSelectedJobTargetPcSuccess(100));
|
||||
else if (targetPc.length === 1)
|
||||
yield put(setSelectedJobTargetPcSuccess(targetPc[0].target));
|
||||
else {
|
||||
yield put(setSelectedJobTargetPcSuccess(100));
|
||||
}
|
||||
yield put(setSelectedJobTargetPcSuccess(GetJobTarget(group, v_age, targets)));
|
||||
// const targetsForGroup = targets.filter((t) => t.group === 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) yield put(setSelectedJobTargetPcSuccess(100));
|
||||
// else if (targetPc.length === 1)
|
||||
// yield put(setSelectedJobTargetPcSuccess(targetPc[0].target));
|
||||
// else {
|
||||
// yield put(setSelectedJobTargetPcSuccess(100));
|
||||
// }
|
||||
}
|
||||
|
||||
export function* applicationSagas() {
|
||||
|
||||
24
src/redux/reporting/reporting.actions.js
Normal file
24
src/redux/reporting/reporting.actions.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import ReportingActionTypes from "./reporting.types";
|
||||
|
||||
export const queryReportingData = ({ startDate, endDate }) => ({
|
||||
type: ReportingActionTypes.QUERY_REPORTING_DATA,
|
||||
payload: { startDate, endDate },
|
||||
});
|
||||
|
||||
export const setReportingData = (data) => ({
|
||||
type: ReportingActionTypes.SET_REPORTING_DATA,
|
||||
payload: data,
|
||||
});
|
||||
|
||||
export const calculateScorecard = (data) => ({
|
||||
type: ReportingActionTypes.CALCULATE_SCORE_CARD,
|
||||
payload: data,
|
||||
});
|
||||
export const setScoreCard = (data) => ({
|
||||
type: ReportingActionTypes.SET_SCORE_CARD,
|
||||
payload: data,
|
||||
});
|
||||
export const setReportingError = (data) => ({
|
||||
type: ReportingActionTypes.SET_REPORTING_ERROR,
|
||||
payload: data,
|
||||
});
|
||||
23
src/redux/reporting/reporting.reducer.js
Normal file
23
src/redux/reporting/reporting.reducer.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import ReportingActionTypes from "./reporting.types";
|
||||
const INITIAL_STATE = {
|
||||
dates: { startDate: null, endDate: null },
|
||||
data: [],
|
||||
scoreCard: null,
|
||||
error: null,
|
||||
loading: false,
|
||||
};
|
||||
|
||||
const applicationReducer = (state = INITIAL_STATE, action) => {
|
||||
switch (action.type) {
|
||||
case ReportingActionTypes.QUERY_REPORTING_DATA:
|
||||
return { ...state, loading: true, dates: action.payload };
|
||||
case ReportingActionTypes.SET_REPORTING_DATA:
|
||||
return { ...state, data: action.payload };
|
||||
case ReportingActionTypes.SET_SCORE_CARD:
|
||||
return { ...state, loading: false, scoreCard: action.payload };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default applicationReducer;
|
||||
57
src/redux/reporting/reporting.sagas.js
Normal file
57
src/redux/reporting/reporting.sagas.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import { all, call, takeLatest, select, put } from "redux-saga/effects";
|
||||
import { calculateScorecard, setReportingData } from "./reporting.actions";
|
||||
import ReportingApplicationTypes from "./reporting.types";
|
||||
import client from "../../graphql/GraphQLClient";
|
||||
import { REPORTING_GET_JOBS } from "../../graphql/reporting.queries";
|
||||
const { log } = window;
|
||||
|
||||
export function* onQueryReportData() {
|
||||
yield takeLatest(
|
||||
ReportingApplicationTypes.QUERY_REPORTING_DATA,
|
||||
queryReportingData
|
||||
);
|
||||
}
|
||||
export function* queryReportingData({ payload: { startDate, endDate } }) {
|
||||
const result = yield client.query({
|
||||
query: REPORTING_GET_JOBS,
|
||||
variables: { startDate, endDate },
|
||||
});
|
||||
if (result.errors) {
|
||||
log.error("Error fetching report data.", result.errors);
|
||||
yield put(setReportingData(null));
|
||||
} else {
|
||||
yield put(setReportingData(result.data.jobs));
|
||||
}
|
||||
}
|
||||
|
||||
export function* onSetReportData() {
|
||||
yield takeLatest(
|
||||
ReportingApplicationTypes.SET_REPORTING_DATA,
|
||||
handleSetReportData
|
||||
);
|
||||
}
|
||||
export function* handleSetReportData({ payload: jobs }) {
|
||||
yield put(calculateScorecard(jobs));
|
||||
}
|
||||
|
||||
export function* onCalculateScoreCard() {
|
||||
yield takeLatest(
|
||||
ReportingApplicationTypes.CALCULATE_SCORE_CARD,
|
||||
handleCalculateScoreCard
|
||||
);
|
||||
}
|
||||
export function* handleCalculateScoreCard({ payload: jobs }) {
|
||||
console.log("jobs", jobs);
|
||||
// yield put(calculateScorecard(jobs));
|
||||
|
||||
//Get the RPS on a per job basis.
|
||||
|
||||
}
|
||||
|
||||
export function* reportingSagas() {
|
||||
yield all([
|
||||
call(onQueryReportData),
|
||||
call(onSetReportData),
|
||||
call(onCalculateScoreCard),
|
||||
]);
|
||||
}
|
||||
49
src/redux/reporting/reporting.selectors.js
Normal file
49
src/redux/reporting/reporting.selectors.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import { createSelector } from "reselect";
|
||||
|
||||
const selectReporting = (state) => state.reporting;
|
||||
|
||||
export const selectReportLoading = createSelector(
|
||||
[selectReporting],
|
||||
(reporting) => reporting.loading
|
||||
);
|
||||
export const selectDates = createSelector(
|
||||
[selectReporting],
|
||||
(reporting) => reporting.dates
|
||||
);
|
||||
export const selectScorecard = createSelector(
|
||||
[selectReporting],
|
||||
(reporting) => reporting.scoreCard
|
||||
);
|
||||
export const selectReportingError = createSelector(
|
||||
[selectReporting],
|
||||
(reporting) => reporting.error
|
||||
);
|
||||
export const selectReportData = createSelector(
|
||||
[selectReporting],
|
||||
(reporting) => reporting.data
|
||||
);
|
||||
|
||||
// export const selectWatchedPaths = createSelector(
|
||||
// [selectReporting],
|
||||
// (application) => application.watchedPaths
|
||||
// );
|
||||
|
||||
// export const selectWatcherError = createSelector(
|
||||
// [selectReporting],
|
||||
// (application) => application.watcherError
|
||||
// );
|
||||
|
||||
// export const selectSelectedJobId = createSelector(
|
||||
// [selectReporting],
|
||||
// (application) => application.selectedJobId
|
||||
// );
|
||||
|
||||
// export const selectSelectedJobTargetPc = createSelector(
|
||||
// [selectReporting],
|
||||
// (application) => application.selectedJobTargetPc
|
||||
// );
|
||||
|
||||
// export const selectSettings = createSelector(
|
||||
// [selectReporting],
|
||||
// (application) => application.settings
|
||||
// );
|
||||
8
src/redux/reporting/reporting.types.js
Normal file
8
src/redux/reporting/reporting.types.js
Normal file
@@ -0,0 +1,8 @@
|
||||
const ReportingActionTypes = {
|
||||
QUERY_REPORTING_DATA: "QUERY_REPORTING_DATA",
|
||||
CALCULATE_SCORE_CARD: "CALCULATE_SCORE_CARD",
|
||||
SET_REPORTING_DATA: "SET_REPORTING_DATA",
|
||||
SET_SCORE_CARD: "SET_SCORE_CARD",
|
||||
SET_REPORTING_ERROR: "SET_REPORTING_ERROR",
|
||||
};
|
||||
export default ReportingActionTypes;
|
||||
@@ -3,16 +3,18 @@ import { persistReducer } from "redux-persist";
|
||||
import storage from "redux-persist/lib/storage";
|
||||
import applicationReducer from "./application/application.reducer";
|
||||
import userReducer from "./user/user.reducer";
|
||||
import reportingReducer from "./reporting/reporting.reducer";
|
||||
|
||||
const persistConfig = {
|
||||
key: "root",
|
||||
storage,
|
||||
blacklist: ["application", "user"],
|
||||
blacklist: ["application", "user", "reporting"],
|
||||
};
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
application: applicationReducer,
|
||||
user: userReducer,
|
||||
reporting: reportingReducer,
|
||||
});
|
||||
|
||||
export default persistReducer(persistConfig, rootReducer);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { all, call } from "redux-saga/effects";
|
||||
import { applicationSagas } from "./application/application.sagas";
|
||||
import { userSagas } from "./user/user.sagas";
|
||||
|
||||
import { reportingSagas } from "./reporting/reporting.sagas";
|
||||
export default function* rootSaga() {
|
||||
yield all([call(applicationSagas), call(userSagas)]);
|
||||
yield all([call(applicationSagas), call(userSagas), call(reportingSagas)]);
|
||||
}
|
||||
|
||||
31
src/util/CalculateJobRps.js
Normal file
31
src/util/CalculateJobRps.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import Dinero from "dinero.js";
|
||||
|
||||
export function CalculateJobRpsDollars(job) {
|
||||
if (!job) {
|
||||
return 0;
|
||||
}
|
||||
return job.joblines
|
||||
.filter((j) => !j.ignore)
|
||||
.reduce((acc, val) => {
|
||||
if (val.price_diff > 0) {
|
||||
return acc.add(
|
||||
Dinero({ amount: Math.round((val.price_diff || 0) * 100) })
|
||||
);
|
||||
} else {
|
||||
return acc;
|
||||
}
|
||||
}, Dinero());
|
||||
}
|
||||
|
||||
export function CalculateJobRpsPc(job, currentRpsDollars) {
|
||||
//TODO Redo this to do total of db price - act price / db price
|
||||
if (!job) {
|
||||
return 0;
|
||||
}
|
||||
const dbPriceSum = job.joblines
|
||||
.filter((j) => !j.ignore)
|
||||
.reduce((acc, val) => {
|
||||
return acc + val.db_price;
|
||||
}, 0);
|
||||
return (currentRpsDollars.getAmount() / dbPriceSum).toFixed(1);
|
||||
}
|
||||
12
src/util/GetJobTarget.js
Normal file
12
src/util/GetJobTarget.js
Normal file
@@ -0,0 +1,12 @@
|
||||
export default function GetJobTarget(group, v_age, targets) {
|
||||
const targetsForGroup = targets.filter((t) => t.group === 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 100;
|
||||
else if (targetPc.length === 1) return targetPc[0].target;
|
||||
else {
|
||||
return 100;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user