Begin refactoring smart schedule calculations.

This commit is contained in:
Patrick Fic
2020-10-08 13:42:56 -07:00
parent 630e8a32ed
commit 020bec3fa2
7 changed files with 185 additions and 74 deletions

View File

@@ -0,0 +1,68 @@
import { Popover } from "antd";
import React from "react";
import { connect } from "react-redux";
import {
PolarAngleAxis,
PolarGrid,
PolarRadiusAxis,
Radar,
RadarChart,
} from "recharts";
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 ScheduleCalendarHeaderGraph({ bodyshop, loadData }) {
const { ssbuckets } = bodyshop;
const data = Object.keys(loadData.expectedLoad).map((key) => {
const metadataBucket = ssbuckets.filter((b) => b.id === key)[0];
return {
bucket: loadData.expectedLoad[key].label,
current: loadData.expectedLoad[key].count,
target: metadataBucket && metadataBucket.target,
};
});
//d console.log("data", data);
const popContent = (
<div>
<RadarChart
cx={300}
cy={250}
outerRadius={150}
width={600}
height={500}
data={data}
>
<PolarGrid />
<PolarAngleAxis dataKey="bucket" />
<PolarRadiusAxis />
<Radar
name="Current"
dataKey="current"
stroke="#8884d8"
fill="#8884d8"
fillOpacity={0.6}
/>
</RadarChart>
</div>
);
return (
<Popover trigger="hover" placement="bottom" content={popContent}>
G
</Popover>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(ScheduleCalendarHeaderGraph);

View File

@@ -1,5 +1,5 @@
import Icon from "@ant-design/icons";
import { Popover, Statistic } from "antd";
import { Popover } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { MdFileDownload, MdFileUpload } from "react-icons/md";
@@ -8,23 +8,21 @@ import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import {
selectScheduleLoad,
selectScheduleLoadCalculating,
selectScheduleLoadCalculating
} from "../../redux/application/application.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
import ScheduleBlockDay from "../schedule-block-day/schedule-block-day.component";
import ScheduleCalendarHeaderGraph from "./schedule-calendar-header-graph.component";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
bodyshop: selectBodyshop,
load: selectScheduleLoad,
calculating: selectScheduleLoadCalculating,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
const mapDispatchToProps = (dispatch) => ({});
export function ScheduleCalendarHeaderComponent({
bodyshop,
@@ -122,23 +120,7 @@ export function ScheduleCalendarHeaderComponent({
<Icon component={MdFileUpload} style={{ color: "red" }} />
{(loadData.hoursOut || 0) && loadData.hoursOut.toFixed(2)}
</Popover>
<Statistic
value={(
((loadData.expectedLoad || 0) / bodyshop.prodtargethrs) *
100
).toFixed(1)}
suffix={"%"}
precision={0}
valueStyle={{
color:
Math.abs(
100 -
((loadData.expectedLoad || 0) / bodyshop.prodtargethrs) * 100
) <= 10
? "green"
: "red",
}}
/>
<ScheduleCalendarHeaderGraph loadData={loadData} />
</div>
) : null;

View File

@@ -102,7 +102,7 @@ export default function ShopInfoSchedulingComponent({ form }) {
key={field.key}
style={{ padding: 0, margin: 2 }}
>
<div className="imex-flex-row">
<LayoutFormRow>
<Form.Item
label={t("bodyshop.fields.ssbuckets.id")}
key={`${index}id`}
@@ -165,17 +165,19 @@ export default function ShopInfoSchedulingComponent({ form }) {
>
<InputNumber />
</Form.Item>
<DeleteFilled
onClick={() => {
remove(field.name);
}}
/>
<FormListMoveArrows
move={move}
index={index}
total={fields.length}
/>
</div>
<div>
<DeleteFilled
onClick={() => {
remove(field.name);
}}
/>
<FormListMoveArrows
move={move}
index={index}
total={fields.length}
/>
</div>
</LayoutFormRow>
</Form.Item>
))}
<Form.Item>

4
client/src/debug.log Normal file
View File

@@ -0,0 +1,4 @@
[1008/110112.528:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3)
[1008/114603.993:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3)
[1008/121110.259:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3)
[1008/122424.146:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3)

View File

@@ -166,32 +166,23 @@ export const QUERY_APPOINTMENTS_BY_JOBID = gql`
`;
export const QUERY_SCHEDULE_LOAD_DATA = gql`
query QUERY_SCHEDULE_LOAD_DATA($start: timestamptz!, $end: timestamptz!) {
labhrs: joblines_aggregate(
where: {
mod_lbr_ty: { _eq: "LAB" }
job: { inproduction: { _eq: true } }
prodJobs: jobs(where: { inproduction: { _eq: true } }) {
id
labhrs: joblines_aggregate(where: { mod_lbr_ty: { _eq: "LAB" } }) {
aggregate {
sum {
mod_lb_hrs
}
}
}
) {
aggregate {
sum {
mod_lb_hrs
larhrs: joblines_aggregate(where: { mod_lbr_ty: { _eq: "LAR" } }) {
aggregate {
sum {
mod_lb_hrs
}
}
}
}
larhrs: joblines_aggregate(
where: {
mod_lbr_ty: { _eq: "LAR" }
job: { inproduction: { _eq: true } }
}
) {
aggregate {
sum {
mod_lb_hrs
}
}
}
compJobs: jobs(
where: { scheduled_completion: { _gte: $start, _lte: $end } }
) {

View File

@@ -1,12 +1,13 @@
import { all, takeLatest, call, put } from "redux-saga/effects";
import ApplicationActionTypes from "./application.types";
import client from "../../utils/GraphQLClient";
import moment from "moment";
import { all, call, put, select, takeLatest } from "redux-saga/effects";
import { QUERY_SCHEDULE_LOAD_DATA } from "../../graphql/appointments.queries";
import client from "../../utils/GraphQLClient";
import { CalculateLoad, CheckJobBucket } from "../../utils/SSSUtils";
import {
scheduleLoadFailure,
scheduleLoadSuccess,
} from "./application.actions";
import moment from "moment";
import ApplicationActionTypes from "./application.types";
export function* onCalculateScheduleLoad() {
yield takeLatest(
@@ -17,6 +18,9 @@ export function* onCalculateScheduleLoad() {
export function* calculateScheduleLoad({ payload: end }) {
//REMINDER: Moment.js is not immutable. Today WILL change when adjusted.
const today = moment(new Date()).startOf("day");
const state = yield select();
const buckets = state.user.bodyshop.ssbuckets;
try {
const result = yield client.query({
query: QUERY_SCHEDULE_LOAD_DATA,
@@ -26,14 +30,26 @@ export function* calculateScheduleLoad({ payload: end }) {
},
fetchPolicy: "network-only",
});
const { prodJobs, arrJobs, compJobs } = result.data;
let load = {
productionHoursTotal:
result.data.larhrs.aggregate.sum.mod_lb_hrs +
result.data.labhrs.aggregate.sum.mod_lb_hrs,
const load = {
productionTotal: {},
};
const { arrJobs, compJobs } = result.data;
//Set the current load.
buckets.forEach((bucket) => {
load.productionTotal[bucket.id] = { count: 0, label: bucket.label };
});
prodJobs.forEach((item) => {
const bucketId = CheckJobBucket(buckets, item);
if (bucketId) {
load.productionTotal[bucketId].count =
load.productionTotal[bucketId].count + 1;
} else {
console.log("Uh oh, this job doesn't fit in a bucket!", item);
}
});
arrJobs.forEach((item) => {
const itemDate = moment(item.scheduled_in).format("yyyy-MM-DD");
@@ -84,17 +100,23 @@ export function* calculateScheduleLoad({ payload: end }) {
}
if (day === 0) {
//Starting on day 1. The load is current.
load[current].expectedLoad =
load.productionHoursTotal +
(load[current].hoursIn || 0) -
(load[current].hoursOut || 0);
load[current].expectedLoad = CalculateLoad(
load.productionTotal,
buckets,
load[current].jobsIn || [],
load[current].jobsOut || []
);
} else {
load[current].expectedLoad =
load[prev].expectedLoad +
(load[current].hoursIn || 0) -
(load[current].hoursOut || 0);
load[current].expectedLoad = CalculateLoad(
load[prev].expectedLoad,
buckets,
load[current].jobsIn || [],
load[current].jobsOut || []
);
}
console.log(load);
}
yield put(scheduleLoadSuccess(load));
} catch (error) {
//console.log("Error in sendEmailFailure saga.", error.message);

View File

@@ -0,0 +1,42 @@
export const CheckJobBucket = (buckets, job) => {
const jobHours =
job.labhrs.aggregate.sum.mod_lb_hrs + job.larhrs.aggregate.sum.mod_lb_hrs;
const matchingBucket = buckets.filter(
(b) => b.gte <= jobHours && b.lt > jobHours
);
return matchingBucket[0] && matchingBucket[0].id;
};
export const CalculateLoad = (currentLoad, buckets, jobsIn, jobsOut) => {
//Add the jobs coming
const newLoad = { ...currentLoad };
jobsIn.forEach((job) => {
const bucketId = CheckJobBucket(buckets, job);
if (bucketId) {
newLoad[bucketId].count = newLoad[bucketId].count + 1;
} else {
console.log(
"[Util Arr Job]Uh oh, this job doesn't fit in a bucket!",
job
);
}
});
jobsOut.forEach((job) => {
const bucketId = CheckJobBucket(buckets, job);
if (bucketId) {
newLoad[bucketId].count = newLoad[bucketId].count - 1;
} else {
console.log(
"[Util Arr Job]Uh oh, this job doesn't fit in a bucket!",
job
);
}
});
console.log("newLoad", newLoad);
return newLoad;
};