Merged in release/2022-01-07 (pull request #333)

release/2022-01-07

Approved-by: Patrick Fic
This commit is contained in:
Patrick Fic
2022-01-03 18:49:44 +00:00
25 changed files with 449 additions and 127 deletions

View File

@@ -1,4 +1,4 @@
<babeledit_project version="1.2" be_version="2.7.1"> <babeledit_project be_version="2.7.1" version="1.2">
<!-- <!--
BabelEdit project file BabelEdit project file
@@ -21431,6 +21431,27 @@
</concept_node> </concept_node>
</children> </children>
</folder_node> </folder_node>
<concept_node>
<name>invoice_final_note</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>kmin</name> <name>kmin</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -33986,6 +34007,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>folder_label_multiple</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>glass_express_checklist</name> <name>glass_express_checklist</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>

View File

@@ -117,9 +117,9 @@
"devDependencies": { "devDependencies": {
"@sentry/webpack-plugin": "^1.18.3", "@sentry/webpack-plugin": "^1.18.3",
"@testing-library/cypress": "^8.0.2", "@testing-library/cypress": "^8.0.2",
"react-error-overlay": "6.0.9",
"cypress": "^9.1.1", "cypress": "^9.1.1",
"eslint-plugin-cypress": "^2.12.1", "eslint-plugin-cypress": "^2.12.1",
"react-error-overlay": "6.0.9",
"redux-logger": "^3.0.6", "redux-logger": "^3.0.6",
"source-map-explorer": "^2.5.2" "source-map-explorer": "^2.5.2"
} }

View File

@@ -43,7 +43,8 @@ export function PrintCenterJobsLabels({ bodyshop, jobId }) {
context: restVals, context: restVals,
}, },
{}, {},
"p" "p",
jobId
); );
setLoading(false); setLoading(false);
setIsModalVisible(false); setIsModalVisible(false);
@@ -61,10 +62,10 @@ export function PrintCenterJobsLabels({ bodyshop, jobId }) {
label={t("printcenter.jobs.labels.position")} label={t("printcenter.jobs.labels.position")}
name="position" name="position"
> >
<InputNumber min={0} precision={0} /> <InputNumber min={1} precision={0} />
</Form.Item> </Form.Item>
<Form.Item label={t("printcenter.jobs.labels.count")} name="count"> <Form.Item label={t("printcenter.jobs.labels.count")} name="count">
<InputNumber min={0} precision={0} /> <InputNumber min={1} precision={0} max={99} />
</Form.Item> </Form.Item>
<Button type="primary" loading={loading} onClick={handleOk}> <Button type="primary" loading={loading} onClick={handleOk}>
{t("general.actions.print")} {t("general.actions.print")}

View File

@@ -10,13 +10,17 @@ import Event from "../job-at-change/schedule-event.container";
import HeaderComponent from "./schedule-calendar-header.component"; import HeaderComponent from "./schedule-calendar-header.component";
import "./schedule-calendar.styles.scss"; import "./schedule-calendar.styles.scss";
import JobDetailCards from "../job-detail-cards/job-detail-cards.component"; import JobDetailCards from "../job-detail-cards/job-detail-cards.component";
import { selectProblemJobs } from "../../redux/application/application.selectors";
import { Alert } from "antd";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
problemJobs: selectProblemJobs,
}); });
const localizer = momentLocalizer(moment); const localizer = momentLocalizer(moment);
export function ScheduleCalendarWrapperComponent({ export function ScheduleCalendarWrapperComponent({
bodyshop, bodyshop,
problemJobs,
data, data,
refetch, refetch,
defaultView, defaultView,
@@ -48,6 +52,15 @@ export function ScheduleCalendarWrapperComponent({
return ( return (
<> <>
<JobDetailCards /> <JobDetailCards />
{problemJobs &&
problemJobs.map((problem) => (
<Alert
key={problem.id}
type="error"
message={`${problem.ro_number} has a data consistency issue. It has been exlcuded for scheduling purposes. CODE: ${problem.code}`}
/>
))}
<Calendar <Calendar
events={data} events={data}
defaultView={search.view || defaultView || "week"} defaultView={search.view || defaultView || "week"}

View File

@@ -5,6 +5,7 @@ import ScheduleCalendarWrapperComponent from "../schedule-calendar-wrapper/sched
import ScheduleModal from "../schedule-job-modal/schedule-job-modal.container"; import ScheduleModal from "../schedule-job-modal/schedule-job-modal.container";
//import ScheduleManualEvent from "../schedule-manual-event/schedule-manual-event.component"; //import ScheduleManualEvent from "../schedule-manual-event/schedule-manual-event.component";
import ScheduleProductionList from "../schedule-production-list/schedule-production-list.component"; import ScheduleProductionList from "../schedule-production-list/schedule-production-list.component";
import ScheduleVerifyIntegrity from "../schedule-verify-integrity/schedule-verify-integrity.component";
export default function ScheduleCalendarComponent({ data, refetch }) { export default function ScheduleCalendarComponent({ data, refetch }) {
return ( return (
@@ -15,6 +16,7 @@ export default function ScheduleCalendarComponent({ data, refetch }) {
<PageHeader <PageHeader
extra={ extra={
<Space wrap> <Space wrap>
<ScheduleVerifyIntegrity />
<Button <Button
onClick={() => { onClick={() => {
refetch(); refetch();

View File

@@ -5,8 +5,9 @@ import {
Input, Input,
Row, Row,
Select, Select,
Space, Switch, Space,
Typography Switch,
Typography,
} from "antd"; } from "antd";
import axios from "axios"; import axios from "axios";
import moment from "moment"; import moment from "moment";
@@ -75,10 +76,14 @@ export function ScheduleJobModalComponent({
<Row gutter={[16, 16]}> <Row gutter={[16, 16]}>
<Col span={12}> <Col span={12}>
<Space> <Space>
<Typography.Title level={3}>{lbrHrsData?.jobs_by_pk?.ro_number}</Typography.Title> <Typography.Title level={3}>
<Typography.Title {lbrHrsData?.jobs_by_pk?.ro_number}
level={4} </Typography.Title>
>{`B/R Hrs:${lbrHrsData?.jobs_by_pk.labhrs?.aggregate.sum.mod_lb_hrs}/${lbrHrsData?.jobs_by_pk.larhrs?.aggregate.sum.mod_lb_hrs}`}</Typography.Title> <Typography.Title level={4}>{`B/R Hrs:${
lbrHrsData?.jobs_by_pk.labhrs?.aggregate?.sum?.mod_lb_hrs || 0
}/${
lbrHrsData?.jobs_by_pk.larhrs?.aggregate?.sum?.mod_lb_hrs || 0
}`}</Typography.Title>
</Space> </Space>
<LayoutFormRow grow> <LayoutFormRow grow>
<Form.Item <Form.Item

View File

@@ -0,0 +1,60 @@
import { useApolloClient } from "@apollo/client";
import { Button } from "antd";
import moment from "moment";
import React, { useState } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { QUERY_SCHEDULE_LOAD_DATA } from "../../graphql/appointments.queries";
import { selectCurrentUser } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(ScheduleVerifyIntegrity);
export function ScheduleVerifyIntegrity({ currentUser }) {
const [loading, setLoading] = useState(false);
const client = useApolloClient();
const handleVerify = async () => {
setLoading(true);
const {
data: { arrJobs, compJobs, prodJobs },
} = await client.query({
query: QUERY_SCHEDULE_LOAD_DATA,
variables: { start: moment(), end: moment().add(180, "days") },
});
//check that the leaving jobs are either in the arriving list, or in production.
const issues = [];
compJobs.forEach((j) => {
const inProdJobs = prodJobs.find((p) => p.id === j.id);
const inArrJobs = arrJobs.find((p) => p.id === j.id);
if (!(inProdJobs || inArrJobs)) {
// NOT FOUND!
issues.push(j);
}
});
console.log(
"The following completing jobs are not in production, or are arriving within the next 180 days. ",
issues
);
setLoading(false);
};
if (currentUser.email === "patrick@imex.prod")
return (
<Button loading={loading} onClick={handleVerify}>
Developer Use Only - Verify Schedule Integrity
</Button>
);
else return null;
}

View File

@@ -234,11 +234,17 @@ export const QUERY_SCHEDULE_LOAD_DATA = gql`
} }
} }
compJobs: jobs( compJobs: jobs(
where: { scheduled_completion: { _gte: $start, _lte: $end } } where: {
_or: [
{ scheduled_completion: { _gte: $start, _lte: $end } }
{ actual_completion: { _gte: $start, _lte: $end } }
]
}
) { ) {
id id
ro_number ro_number
scheduled_completion scheduled_completion
actual_completion
labhrs: joblines_aggregate( labhrs: joblines_aggregate(
where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } } where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }
) { ) {

View File

@@ -1816,6 +1816,7 @@ export const QUERY_JOB_CLOSE_DETAILS = gql`
jobs_by_pk(id: $id) { jobs_by_pk(id: $id) {
ro_number ro_number
invoice_allocation invoice_allocation
invoice_final_note
ins_co_id ins_co_id
dms_allocation dms_allocation
id id

View File

@@ -63,6 +63,7 @@ export function JobsCloseComponent({ job, bodyshop, jobRO }) {
kmin: values.kmin, kmin: values.kmin,
kmout: values.kmout, kmout: values.kmout,
dms_allocation: values.dms_allocation, dms_allocation: values.dms_allocation,
invoice_final_note: values.invoice_final_note,
}, },
}, },
refetchQueries: ["QUERY_JOB_CLOSE_DETAILS"], refetchQueries: ["QUERY_JOB_CLOSE_DETAILS"],
@@ -122,6 +123,7 @@ export function JobsCloseComponent({ job, bodyshop, jobRO }) {
kmin: job.kmin, kmin: job.kmin,
kmout: job.kmout, kmout: job.kmout,
dms_allocation: job.dms_allocation, dms_allocation: job.dms_allocation,
invoice_final_note: job.invoice_final_note,
}} }}
scrollToFirstError scrollToFirstError
> >
@@ -271,6 +273,12 @@ export function JobsCloseComponent({ job, bodyshop, jobRO }) {
<Input disabled /> <Input disabled />
</Form.Item> </Form.Item>
)} )}
<Form.Item
label={t("jobs.fields.invoice_final_note")}
name="invoice_final_note"
>
<Input.TextArea disabled={jobRO} />
</Form.Item>
</LayoutFormRow> </LayoutFormRow>
<Divider /> <Divider />
<JobsCloseLines job={job} /> <JobsCloseLines job={job} />

View File

@@ -58,3 +58,7 @@ export const insertAuditTrail = ({ jobid, billid, operation }) => ({
type: ApplicationActionTypes.INSERT_AUDIT_TRAIL, type: ApplicationActionTypes.INSERT_AUDIT_TRAIL,
payload: { jobid, billid, operation }, payload: { jobid, billid, operation },
}); });
export const setProblemJobs = (problemJobs) => ({
type: ApplicationActionTypes.SET_PROBLEM_JOBS,
payload: problemJobs,
});

View File

@@ -6,6 +6,7 @@ const INITIAL_STATE = {
breadcrumbs: [], breadcrumbs: [],
recentItems: [], recentItems: [],
selectedHeader: "home", selectedHeader: "home",
problemJobs: [],
scheduleLoad: { scheduleLoad: {
load: {}, load: {},
calculating: false, calculating: false,
@@ -40,6 +41,7 @@ const applicationReducer = (state = INITIAL_STATE, action) => {
case ApplicationActionTypes.CALCULATE_SCHEDULE_LOAD: case ApplicationActionTypes.CALCULATE_SCHEDULE_LOAD:
return { return {
...state, ...state,
problemJobs: [],
scheduleLoad: { ...state.scheduleLoad, calculating: true, error: null }, scheduleLoad: { ...state.scheduleLoad, calculating: true, error: null },
}; };
case ApplicationActionTypes.CALCULATE_SCHEDULE_LOAD_SUCCESS: case ApplicationActionTypes.CALCULATE_SCHEDULE_LOAD_SUCCESS:
@@ -76,7 +78,9 @@ const applicationReducer = (state = INITIAL_STATE, action) => {
case ApplicationActionTypes.SET_PARTNER_VERSION: case ApplicationActionTypes.SET_PARTNER_VERSION:
return { ...state, partnerVersion: action.payload }; return { ...state, partnerVersion: action.payload };
case ApplicationActionTypes.SET_PROBLEM_JOBS: {
return { ...state, problemJobs: action.payload };
}
default: default:
return state; return state;
} }

View File

@@ -7,6 +7,7 @@ import { CalculateLoad, CheckJobBucket } from "../../utils/SSSUtils";
import { import {
scheduleLoadFailure, scheduleLoadFailure,
scheduleLoadSuccess, scheduleLoadSuccess,
setProblemJobs,
} from "./application.actions"; } from "./application.actions";
import ApplicationActionTypes from "./application.types"; import ApplicationActionTypes from "./application.types";
@@ -53,6 +54,8 @@ export function* calculateScheduleLoad({ payload: end }) {
}); });
arrJobs.forEach((item) => { arrJobs.forEach((item) => {
if (!item.scheduled_in)
console.log("JOB HAS NO SCHEDULED IN DATE.", item);
const itemDate = moment(item.scheduled_in).format("yyyy-MM-DD"); const itemDate = moment(item.scheduled_in).format("yyyy-MM-DD");
if (!!load[itemDate]) { if (!!load[itemDate]) {
load[itemDate].hoursIn = load[itemDate].hoursIn =
@@ -71,7 +74,26 @@ export function* calculateScheduleLoad({ payload: end }) {
} }
}); });
let problemJobs = [];
compJobs.forEach((item) => { compJobs.forEach((item) => {
if (!item.scheduled_completion)
console.log("JOB HAS NO SCHEDULED COMPLETION DATE.", item);
const inProdJobs = prodJobs.find((p) => p.id === item.id);
const inArrJobs = arrJobs.find((p) => p.id === item.id);
if (
!(inProdJobs || inArrJobs) &&
!moment(item.scheduled_completion).isSame(moment(), "day")
) {
// NOT FOUND!
problemJobs.push({
...item,
code: "Job is scheduled for completion, but it is not marked in production nor is it an arriving job in this period. Check the scheduled in and completion dates",
});
return;
}
const itemDate = moment(item.scheduled_completion).format("yyyy-MM-DD"); const itemDate = moment(item.scheduled_completion).format("yyyy-MM-DD");
if (!!load[itemDate]) { if (!!load[itemDate]) {
load[itemDate].hoursOut = load[itemDate].hoursOut =
@@ -116,7 +138,7 @@ export function* calculateScheduleLoad({ payload: end }) {
); );
} }
} }
yield put(setProblemJobs(problemJobs));
yield put(scheduleLoadSuccess(load)); yield put(scheduleLoadSuccess(load));
} catch (error) { } catch (error) {
yield put(scheduleLoadFailure(error)); yield put(scheduleLoadFailure(error));

View File

@@ -44,3 +44,7 @@ export const selectOnline = createSelector(
[selectApplication], [selectApplication],
(application) => application.online (application) => application.online
); );
export const selectProblemJobs = createSelector(
[selectApplication],
(application) => application.problemJobs
);

View File

@@ -11,5 +11,6 @@ const ApplicationActionTypes = {
SET_PARTNER_VERSION: "SET_PARTNER_VERSION", SET_PARTNER_VERSION: "SET_PARTNER_VERSION",
SET_ONLINE_STATUS: "SET_ONLINE_STATUS", SET_ONLINE_STATUS: "SET_ONLINE_STATUS",
INSERT_AUDIT_TRAIL: "INSERT_AUDIT_TRAIL", INSERT_AUDIT_TRAIL: "INSERT_AUDIT_TRAIL",
SET_PROBLEM_JOBS: "SET_PROBLEM_JOBS",
}; };
export default ApplicationActionTypes; export default ApplicationActionTypes;

View File

@@ -1298,6 +1298,7 @@
"required": "Required?", "required": "Required?",
"type": "Type" "type": "Type"
}, },
"invoice_final_note": "Note to Display on Final Invoice",
"kmin": "Mileage In", "kmin": "Mileage In",
"kmout": "Mileage Out", "kmout": "Mileage Out",
"la1": "LA1", "la1": "LA1",
@@ -2028,6 +2029,7 @@
"filing_coversheet_portrait": "Filing Coversheet (Portrait)", "filing_coversheet_portrait": "Filing Coversheet (Portrait)",
"final_invoice": "Final Invoice", "final_invoice": "Final Invoice",
"fippa_authorization": "FIPPA Authorization", "fippa_authorization": "FIPPA Authorization",
"folder_label_multiple": "Folder Label Multiple",
"glass_express_checklist": "Glass Express Checklist", "glass_express_checklist": "Glass Express Checklist",
"guarantee": "Repair Guarantee", "guarantee": "Repair Guarantee",
"individual_job_note": "Job Note RO # {{ro_number}}", "individual_job_note": "Job Note RO # {{ro_number}}",
@@ -2097,7 +2099,7 @@
}, },
"subjects": { "subjects": {
"jobs": { "jobs": {
"parts_order": "Parts Order PO: {{ro_number}}" "parts_order": "Parts Order PO: {{ro_number}} - {{name}}"
} }
}, },
"vendors": { "vendors": {

View File

@@ -1298,6 +1298,7 @@
"required": "", "required": "",
"type": "" "type": ""
}, },
"invoice_final_note": "",
"kmin": "Kilometraje en", "kmin": "Kilometraje en",
"kmout": "Kilometraje", "kmout": "Kilometraje",
"la1": "", "la1": "",
@@ -2028,6 +2029,7 @@
"filing_coversheet_portrait": "", "filing_coversheet_portrait": "",
"final_invoice": "", "final_invoice": "",
"fippa_authorization": "", "fippa_authorization": "",
"folder_label_multiple": "",
"glass_express_checklist": "", "glass_express_checklist": "",
"guarantee": "", "guarantee": "",
"individual_job_note": "", "individual_job_note": "",

View File

@@ -1298,6 +1298,7 @@
"required": "", "required": "",
"type": "" "type": ""
}, },
"invoice_final_note": "",
"kmin": "Kilométrage en", "kmin": "Kilométrage en",
"kmout": "Kilométrage hors", "kmout": "Kilométrage hors",
"la1": "", "la1": "",
@@ -2028,6 +2029,7 @@
"filing_coversheet_portrait": "", "filing_coversheet_portrait": "",
"final_invoice": "", "final_invoice": "",
"fippa_authorization": "", "fippa_authorization": "",
"folder_label_multiple": "",
"glass_express_checklist": "", "glass_express_checklist": "",
"guarantee": "", "guarantee": "",
"individual_job_note": "", "individual_job_note": "",

View File

@@ -30,6 +30,9 @@ export const CalculateLoad = (currentLoad, buckets, jobsIn, jobsOut) => {
const bucketId = CheckJobBucket(buckets, job); const bucketId = CheckJobBucket(buckets, job);
if (bucketId) { if (bucketId) {
newLoad[bucketId].count = newLoad[bucketId].count - 1; newLoad[bucketId].count = newLoad[bucketId].count - 1;
if (newLoad[bucketId].count < 0) {
console.log("***ERROR: NEGATIVE LOAD", bucketId, job);
}
} else { } else {
console.log( console.log(
"[Util Out Job]Uh oh, this job doesn't fit in a bucket!", "[Util Out Job]Uh oh, this job doesn't fit in a bucket!",

View File

@@ -509,6 +509,11 @@ export const TemplateList = (type, context) => {
key: "parts_order", key: "parts_order",
subject: i18n.t("printcenter.subjects.jobs.parts_order", { subject: i18n.t("printcenter.subjects.jobs.parts_order", {
ro_number: context && context.job && context.job.ro_number, ro_number: context && context.job && context.job.ro_number,
name: `${(context && context.job && context.job.ownr_fn) || ""} ${
(context && context.job && context.job.ownr_ln) || ""
} ${
(context && context.job && context.job.ownr_co_nm) || ""
}`.trim(),
}), }),
disabled: false, disabled: false,
}, },

View File

@@ -2694,6 +2694,7 @@
- intakechecklist - intakechecklist
- invoice_allocation - invoice_allocation
- invoice_date - invoice_date
- invoice_final_note
- iouparent - iouparent
- job_totals - job_totals
- kanbanparent - kanbanparent
@@ -2948,6 +2949,7 @@
- intakechecklist - intakechecklist
- invoice_allocation - invoice_allocation
- invoice_date - invoice_date
- invoice_final_note
- iouparent - iouparent
- job_totals - job_totals
- kanbanparent - kanbanparent
@@ -3212,6 +3214,7 @@
- intakechecklist - intakechecklist
- invoice_allocation - invoice_allocation
- invoice_date - invoice_date
- invoice_final_note
- iouparent - iouparent
- job_totals - job_totals
- kanbanparent - kanbanparent

View File

@@ -0,0 +1,4 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- alter table "public"."jobs" add column "invoice_final_note" Text
-- null;

View File

@@ -0,0 +1,2 @@
alter table "public"."jobs" add column "invoice_final_note" Text
null;

View File

@@ -454,15 +454,14 @@ exports.QUERY_PAYMENTS_FOR_EXPORT = `
} }
`; `;
exports.QUERY_UPCOMING_APPOINTMENTS = ` exports.QUERY_UPCOMING_APPOINTMENTS = `query QUERY_UPCOMING_APPOINTMENTS($now: timestamptz!, $jobId: uuid!) {
query QUERY_UPCOMING_APPOINTMENTS($now: timestamptz!, $jobId: uuid!) {
jobs_by_pk(id: $jobId) { jobs_by_pk(id: $jobId) {
bodyshop { bodyshop {
ssbuckets ssbuckets
target_touchtime target_touchtime
workingdays workingdays
} }
jobhrs: joblines_aggregate(where: { removed: { _eq: false } }) { jobhrs: joblines_aggregate(where: {removed: {_eq: false}}) {
aggregate { aggregate {
sum { sum {
mod_lb_hrs mod_lb_hrs
@@ -470,40 +469,60 @@ query QUERY_UPCOMING_APPOINTMENTS($now: timestamptz!, $jobId: uuid!) {
} }
} }
} }
appointments(where: {_and: {canceled: {_eq: false}, start: {_gte: $now}}}) { blockedDays: appointments(where: {_and: {canceled: {_eq: false}, block: {_eq: true}, start: {_gte: $now}}}) {
start start
isintake
id
block block
job { }
joblines_aggregate(where: { removed: { _eq: false } }) { arrJobs: jobs(where: {scheduled_in: {_gte: $now}}) {
aggregate { id
sum { scheduled_in
mod_lb_hrs ro_number
} labhrs: joblines_aggregate(where: {mod_lbr_ty: {_neq: "LAR"}, removed: {_eq: false}}) {
aggregate {
sum {
mod_lb_hrs
}
}
}
larhrs: joblines_aggregate(where: {mod_lbr_ty: {_eq: "LAR"}, removed: {_eq: false}}) {
aggregate {
sum {
mod_lb_hrs
} }
} }
} }
} }
jobs(where: {inproduction: {_eq: true}}) { compJobs: jobs(where: {_or: [{scheduled_completion: {_gte: $now}}, {actual_completion: {_gte: $now}}]}) {
id
ro_number
scheduled_completion
actual_completion
labhrs: joblines_aggregate(where: {mod_lbr_ty: {_neq: "LAR"}, removed: {_eq: false}}) {
aggregate {
sum {
mod_lb_hrs
}
}
}
larhrs: joblines_aggregate(where: {mod_lbr_ty: {_eq: "LAR"}, removed: {_eq: false}}) {
aggregate {
sum {
mod_lb_hrs
}
}
}
}
prodJobs: jobs(where: {inproduction: {_eq: true}}) {
id id
scheduled_completion scheduled_completion
labhrs: joblines_aggregate( labhrs: joblines_aggregate(where: {_and: [{mod_lbr_ty: {_neq: "LAR"}}, {removed: {_eq: false}}]}) {
where: {
_and: [{ mod_lbr_ty: { _neq: "LAR" } }, { removed: { _eq: false } }]
}
) {
aggregate { aggregate {
sum { sum {
mod_lb_hrs mod_lb_hrs
} }
} }
} }
larhrs: joblines_aggregate( larhrs: joblines_aggregate(where: {_and: [{mod_lbr_ty: {_eq: "LAR"}}, {removed: {_eq: false}}]}) {
where: {
_and: [{ mod_lbr_ty: { _eq: "LAR" } }, { removed: { _eq: false } }]
}
) {
aggregate { aggregate {
sum { sum {
mod_lb_hrs mod_lb_hrs
@@ -512,6 +531,9 @@ query QUERY_UPCOMING_APPOINTMENTS($now: timestamptz!, $jobId: uuid!) {
} }
} }
} }
`; `;
exports.QUERY_EMPLOYEE_PIN = `query QUERY_EMPLOYEE_PIN($shopId: uuid!, $employeeId: String!) { exports.QUERY_EMPLOYEE_PIN = `query QUERY_EMPLOYEE_PIN($shopId: uuid!, $employeeId: String!) {

View File

@@ -4,6 +4,7 @@ const queries = require("../graphql-client/queries");
const Dinero = require("dinero.js"); const Dinero = require("dinero.js");
const moment = require("moment"); const moment = require("moment");
const logger = require("../utils/logger"); const logger = require("../utils/logger");
const _ = require("lodash");
require("dotenv").config({ require("dotenv").config({
path: path.resolve( path: path.resolve(
process.cwd(), process.cwd(),
@@ -30,7 +31,7 @@ exports.job = async (req, res) => {
jobId: jobId, jobId: jobId,
}); });
const { appointments, jobs } = result; const { jobs_by_pk, blockedDays, prodJobs, arrJobs, compJobs } = result;
const { ssbuckets, workingdays } = result.jobs_by_pk.bodyshop; const { ssbuckets, workingdays } = result.jobs_by_pk.bodyshop;
const jobHrs = result.jobs_by_pk.jobhrs.aggregate.sum.mod_lb_hrs; const jobHrs = result.jobs_by_pk.jobhrs.aggregate.sum.mod_lb_hrs;
@@ -39,105 +40,164 @@ exports.job = async (req, res) => {
bucket.gte <= jobHrs && (!!bucket.lt ? bucket.lt > jobHrs : true) bucket.gte <= jobHrs && (!!bucket.lt ? bucket.lt > jobHrs : true)
)[0]; )[0];
const bucketMatrix = {}; const load = {
const yesterday = moment().subtract(1, "day"); productionTotal: {},
//Get latest date + add 5 days to allow for back end adding.. };
//Set the current load.
ssbuckets.forEach((bucket) => {
load.productionTotal[bucket.id] = { count: 0, label: bucket.label };
});
const totalMatrixDays = moment const filteredProdJobsList = prodJobs.filter(
.max([ (j) => JobBucket.id === CheckJobBucket(ssbuckets, j)
...appointments.map((a) => moment(a.start)), );
...jobs
.map((p) => moment(p.scheduled_completion))
.filter((p) => p.isValid() && p.isAfter(yesterday)),
])
.add("5", "days")
.diff(moment(), "days");
//Initialize the bucket matrix filteredProdJobsList.forEach((item) => {
for (var i = 0; i < totalMatrixDays; i++) { //Add all of the jobs currently in production to the buckets so that we have a starting point.
const theDate = moment().add(i, "days").format("yyyy-MM-DD"); const bucketId = CheckJobBucket(ssbuckets, item);
//Only need to create a matrix for jobs of the same bucket. if (bucketId) {
bucketMatrix[theDate] = { in: 0, out: 0 }; load.productionTotal[bucketId].count =
load.productionTotal[bucketId].count + 1;
// ssbuckets.forEach((bucket) => {
// bucketMatrix[theDate] = {
// ...bucketMatrix[theDate],
// [bucket.id]: { in: 0, out: 0 },
// };
// });
}
//Populate the jobs scheduled to come in.
appointments.forEach((appointment) => {
if (!appointment.block) {
const jobHrs =
appointment.job.joblines_aggregate.aggregate.sum.mod_lb_hrs;
//Is the job in the same bucket?
const appointmentBucket = ssbuckets.filter(
(bucket) =>
bucket.gte <= jobHrs && (!!bucket.lt ? bucket.lt > jobHrs : true)
)[0];
if (appointmentBucket.id === JobBucket.id) {
//Theyre the same classification. Add it to the matrix.
const appDate = moment(appointment.start).format("yyyy-MM-DD");
bucketMatrix[appDate] = {
...bucketMatrix[appDate],
in: bucketMatrix[appDate].in + 1,
};
}
} else { } else {
//remove the date from the possible list. console.log("Uh oh, this job doesn't fit in a bucket!", item);
const appDate = moment(appointment.start).format("yyyy-MM-DD"); }
bucketMatrix[appDate] = { });
...bucketMatrix[appDate],
blocked: true, const filteredArrJobs = arrJobs.filter(
(j) => JobBucket.id === CheckJobBucket(ssbuckets, j)
);
filteredArrJobs.forEach((item) => {
const itemDate = moment(item.scheduled_in).format("yyyy-MM-DD");
if (!!load[itemDate]) {
load[itemDate].hoursIn =
(load[itemDate].hoursIn || 0) +
item.labhrs.aggregate.sum.mod_lb_hrs +
item.larhrs.aggregate.sum.mod_lb_hrs;
load[itemDate].jobsIn.push(item);
} else {
load[itemDate] = {
jobsIn: [item],
jobsOut: [],
hoursIn:
item.labhrs.aggregate.sum.mod_lb_hrs +
item.larhrs.aggregate.sum.mod_lb_hrs,
}; };
} }
}); });
//Populate the jobs that are leaving today. //Get the completing jobs.
const todayIsoString = moment().format("yyyy-MM-DD"); let problemJobs = [];
jobs.forEach((pjob) => { const filteredCompJobs = compJobs.filter(
const jobHrs = (j) => JobBucket.id === CheckJobBucket(ssbuckets, j)
pjob.larhrs.aggregate.sum.mod_lb_hrs + );
pjob.labhrs.aggregate.sum.mod_lb_hrs;
//Is the job in the same bucket?
const pjobBucket = ssbuckets.filter(
(bucket) =>
bucket.gte <= jobHrs && (!!bucket.lt ? bucket.lt > jobHrs : true)
)[0];
if (pjobBucket.id === JobBucket.id) {
//Theyre the same classification. Add it to the matrix.
const compDate = moment(pjob.scheduled_completion);
//Is the schedule completion behind today? If so, use today as it.
let dateToUse;
dateToUse = compDate.isValid()
? moment().diff(compDate, "days") < 0
? compDate.format("yyyy-MM-DD")
: todayIsoString
: todayIsoString;
bucketMatrix[dateToUse] = { filteredCompJobs.forEach((item) => {
...bucketMatrix[dateToUse], const inProdJobs = filteredProdJobsList.find((p) => p.id === item.id);
out: (bucketMatrix[dateToUse].out || 0) + 1, const inArrJobs = filteredArrJobs.find((p) => p.id === item.id);
};
}
});
//Propose the first 5 dates where we are below target.
const possibleDates = [];
const bucketMatrixKeys = Object.keys(bucketMatrix);
bucketMatrixKeys.forEach((bmkey) => {
const isShopOpen =
workingdays[dayOfWeekMapper(moment(bmkey).day())] &&
!bucketMatrix[bmkey].blocked;
if ( if (
JobBucket.target > bucketMatrix[bmkey].in - bucketMatrix[bmkey].out && !(inProdJobs || inArrJobs) &&
!moment(item.actual_completion || item.scheduled_completion).isSame(
moment(),
"day"
)
) {
// NOT FOUND!
console.log("PROBLEM JOB", item);
problemJobs.push({
...item,
code: "Job is scheduled for completion, but it is not marked in production nor is it an arriving job in this period. Check the scheduled in and completion dates",
});
return;
} else {
const itemDate = moment(
item.actual_completion || item.scheduled_completion
).format("yyyy-MM-DD");
if (!!load[itemDate]) {
load[itemDate].hoursOut =
(load[itemDate].hoursOut || 0) +
item.labhrs.aggregate.sum.mod_lb_hrs +
item.larhrs.aggregate.sum.mod_lb_hrs;
load[itemDate].jobsOut.push(item);
} else {
load[itemDate] = {
jobsOut: [item],
hoursOut:
item.labhrs.aggregate.sum.mod_lb_hrs +
item.larhrs.aggregate.sum.mod_lb_hrs,
};
}
}
});
//Propagate the expected load to each day.
const yesterday = moment().subtract(1, "day");
const today = moment().startOf("day");
const end = moment.max([
...filteredArrJobs.map((a) => moment(a.scheduled_in)),
...filteredCompJobs
.map((p) => moment(p.actual_completion || p.scheduled_completion))
.filter((p) => p.isValid() && p.isAfter(yesterday)),
]);
const range = Math.round(moment.duration(end.diff(today)).asDays());
for (var day = 0; day < range; day++) {
const current = moment(today).add(day, "days").format("yyyy-MM-DD");
const prev = moment(today)
.add(day - 1, "days")
.format("yyyy-MM-DD");
if (!!!load[current]) {
load[current] = {};
}
if (day === 0) {
//Starting on day 1. The load is current.
load[current].expectedLoad = CalculateLoad(
load.productionTotal,
ssbuckets,
load[current].jobsIn || [],
load[current].jobsOut || []
);
} else {
load[current].expectedLoad = CalculateLoad(
load[prev].expectedLoad,
ssbuckets,
load[current].jobsIn || [],
load[current].jobsOut || []
);
}
}
//Add in all of the blocked days.
blockedDays.forEach((b) => {
//Find it in the load, set it as blocked.
const startIsoFormat = moment(b.start).format("YYYY-MM-DD");
if (load[startIsoFormat]) load[startIsoFormat].blocked = true;
else {
load[startIsoFormat] = { blocked: true };
}
});
// //Propose the first 5 dates where we are below target.
const possibleDates = [];
delete load.productionTotal;
const loadKeys = Object.keys(load).sort((a, b) =>
moment(a).isAfter(moment(b)) ? 1 : -1
);
loadKeys.forEach((loadKey) => {
const isShopOpen =
(workingdays[dayOfWeekMapper(moment(loadKey).day())] || false) &&
!load[loadKey].blocked;
if (
load[loadKey].expectedLoad &&
load[loadKey].expectedLoad[JobBucket.id] &&
JobBucket.target > load[loadKey].expectedLoad[JobBucket.id].count &&
isShopOpen isShopOpen
) )
possibleDates.push(new Date(bmkey).toISOString().substr(0, 10)); possibleDates.push(new Date(loadKey).toISOString().substr(0, 10));
}); });
if (possibleDates.length < 6) { if (possibleDates.length < 6) {
@@ -147,7 +207,7 @@ exports.job = async (req, res) => {
} }
} catch (error) { } catch (error) {
logger.log("smart-scheduling-error", "ERROR", req.user.email, jobId, { logger.log("smart-scheduling-error", "ERROR", req.user.email, jobId, {
error: JSON.stringify(error), error,
}); });
res.status(400).send(error); res.status(400).send(error);
} }
@@ -171,3 +231,47 @@ const dayOfWeekMapper = (numberOfDay) => {
return "saturday"; return "saturday";
} }
}; };
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 ? b.lt > jobHours : true
);
return matchingBucket[0] && matchingBucket[0].id;
};
const CalculateLoad = (currentLoad, buckets, jobsIn, jobsOut) => {
//Add the jobs coming
const newLoad = _.cloneDeep(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;
if (newLoad[bucketId].count < 0) {
console.log("***ERROR: NEGATIVE LOAD Bucket =>", bucketId, job);
}
} else {
console.log(
"[Util Out Job]Uh oh, this job doesn't fit in a bucket!",
job
);
}
});
return newLoad;
};