BOD-84 Added base calculations for schedule load

This commit is contained in:
Patrick Fic
2020-05-08 15:33:08 -07:00
parent 3d74a4106e
commit 6096fce7a6
17 changed files with 456 additions and 122 deletions

View File

@@ -0,0 +1,29 @@
import moment from "moment";
export function getRange(dateParam, viewParam) {
let start, end;
let date = dateParam || new Date();
let view = viewParam || "week";
// if view is day: from moment(date).startOf('day') to moment(date).endOf('day');
if (view === "day") {
start = moment(date).startOf("day");
end = moment(date).endOf("day");
}
// if view is week: from moment(date).startOf('isoWeek') to moment(date).endOf('isoWeek');
else if (view === "week") {
start = moment(date).startOf("week");
end = moment(date).endOf("week");
}
//if view is month: from moment(date).startOf('month').subtract(7, 'days') to moment(date).endOf('month').add(7, 'days'); i do additional 7 days math because you can see adjacent weeks on month view (that is the way how i generate my recurrent events for the Big Calendar, but if you need only start-end of month - just remove that math);
else if (view === "month") {
start = moment(date).startOf("month").subtract(7, "days");
end = moment(date).endOf("month").add(7, "days");
}
// if view is agenda: from moment(date).startOf('day') to moment(date).endOf('day').add(1, 'month');
else if (view === "agenda") {
start = moment(date).startOf("day");
end = moment(date).endOf("day").add(1, "month");
}
return { start, end };
}

View File

@@ -1,9 +1,9 @@
import { Progress } from "antd";
import moment from "moment";
import queryString from "query-string";
import React from "react";
import { Calendar, momentLocalizer } from "react-big-calendar";
import { useHistory, useLocation } from "react-router-dom";
import DateCellWrapper from "../schedule-datecellwrapper/schedule-datecellwrapper.component";
import Event from "../schedule-event/schedule-event.container";
//import "react-big-calendar/lib/css/react-big-calendar.css";
import "./schedule-calendar.styles.scss";
@@ -14,6 +14,7 @@ export default function ScheduleCalendarWrapperComponent({
data,
refetch,
defaultView,
setDateRangeCallback,
...otherProps
}) {
const search = queryString.parse(useLocation().search);
@@ -22,12 +23,19 @@ export default function ScheduleCalendarWrapperComponent({
return (
<Calendar
events={data}
defaultView={defaultView}
defaultView={search.view || defaultView || "week"}
date={new Date(search.date || Date.now())}
onNavigate={(date, view, action) => {
search.date = date.toISOString().substr(0, 10);
history.push({ search: queryString.stringify(search) });
}}
onRangeChange={(start, end) => {
if (setDateRangeCallback) setDateRangeCallback({ start, end });
}}
onView={(view) => {
search.view = view;
history.push({ search: queryString.stringify(search) });
}}
step={30}
showMultiDayTimes
localizer={localizer}
@@ -35,7 +43,15 @@ export default function ScheduleCalendarWrapperComponent({
max={new Date("2020-01-01T20:00:00")}
components={{
event: (e) => Event({ event: e.event, refetch: refetch }),
dateCellWrapper: DateCellWrapper,
header: (props) => {
return (
<span>
<div>{props.label}</div>
<Progress style={{ flex: "1" }} percent={77} showInfo={false} />
</span>
);
},
//dateCellWrapper: DateCellWrapper,
}}
{...otherProps}
/>

View File

@@ -8,16 +8,15 @@ import { setModalContext } from "../../redux/modals/modals.actions";
import ScheduleCalendarWrapperComponent from "../schedule-calendar-wrapper/scheduler-calendar-wrapper.component";
import ScheduleModal from "../schedule-job-modal/schedule-job-modal.container";
const mapDispatchToProps = dispatch => ({
setScheduleContext: context =>
dispatch(setModalContext({ context: context, modal: "schedule" }))
const mapDispatchToProps = (dispatch) => ({
setScheduleContext: (context) =>
dispatch(setModalContext({ context: context, modal: "schedule" })),
});
export function ScheduleCalendarComponent({
data,
refetch,
setScheduleContext
setScheduleContext,
}) {
const { t } = useTranslation();
@@ -36,8 +35,8 @@ export function ScheduleCalendarComponent({
setScheduleContext({
actions: { refetch: refetch },
context: {
jobId: null
}
jobId: null,
},
});
}}
>
@@ -46,11 +45,7 @@ export function ScheduleCalendarComponent({
<ScheduleModal />
<ScheduleCalendarWrapperComponent
data={data}
defaultView="week"
refetch={refetch}
/>
<ScheduleCalendarWrapperComponent data={data} refetch={refetch} />
</div>
);
}

View File

@@ -1,13 +1,33 @@
import { useQuery } from "@apollo/react-hooks";
import queryString from "query-string";
import React from "react";
import { useLocation } from "react-router-dom";
import { QUERY_ALL_ACTIVE_APPOINTMENTS } from "../../graphql/appointments.queries";
import AlertComponent from "../alert/alert.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import { getRange } from "../schedule-calendar-wrapper/schedule-calendar-util";
import ScheduleCalendarComponent from "./schedule-calendar.component";
import { calculateScheduleLoad } from "../../redux/application/application.actions";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
});
const mapDispatchToProps = (dispatch) => ({
calculateScheduleLoad: (endDate) => dispatch(calculateScheduleLoad(endDate)),
});
export default function ScheduleCalendarContainer() {
export function ScheduleCalendarContainer({ calculateScheduleLoad }) {
const search = queryString.parse(useLocation().search);
const { date, view } = search;
const range = getRange(date, view);
const { loading, error, data, refetch } = useQuery(
QUERY_ALL_ACTIVE_APPOINTMENTS
QUERY_ALL_ACTIVE_APPOINTMENTS,
{
variables: { start: range.start.toDate(), end: range.end.toDate() },
skip: !!!range.start || !!!range.end,
}
);
if (loading) return <LoadingSpinner />;
@@ -22,10 +42,32 @@ export default function ScheduleCalendarContainer() {
);
});
console.log("ScheduleCalendarContainer -> normalizedData", normalizedData);
normalizedData.push({
id: 1,
title: "All day event",
allDay: true,
isintake: false,
start: new Date(),
end: new Date(),
});
return (
<ScheduleCalendarComponent
refetch={refetch}
data={data ? normalizedData : null}
/>
<span>
<button
onClick={() => {
calculateScheduleLoad(range.end);
}}
>
Calc Load
</button>
<ScheduleCalendarComponent
refetch={refetch}
data={data ? normalizedData : null}
/>
</span>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(ScheduleCalendarContainer);

View File

@@ -7,7 +7,7 @@ export default function ScheduleDateCellWrapper(dateCellWrapperProps) {
display: "flex",
flex: 1,
borderLeft: "1px solid #DDD",
backgroundColor: "#fff"
backgroundColor: "#fff",
};
return (
<div style={style}>

View File

@@ -1,5 +1,5 @@
import React from "react";
import { Popover, Button } from "antd";
import { Popover, Button, Progress } from "antd";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import PhoneFormatter from "../../utils/PhoneFormatter";
import { Link } from "react-router-dom";
@@ -13,30 +13,30 @@ export default function ScheduleEventComponent({ event, handleCancel }) {
<strong>{event.title}</strong>
) : (
<div>
<strong>{`${(event.job && event.job.ownr_fn) || ""} ${(event.job &&
event.job.ownr_ln) ||
""}`}</strong>
<strong>{`${(event.job && event.job.ownr_fn) || ""} ${
(event.job && event.job.ownr_ln) || ""
}`}</strong>
<span style={{ margin: 4 }}>
{`${(event.job && event.job.v_model_yr) ||
""} ${(event.job && event.job.v_make_desc) ||
""} ${(event.job && event.job.v_model_desc) || ""}`}
{`${(event.job && event.job.v_model_yr) || ""} ${
(event.job && event.job.v_make_desc) || ""
} ${(event.job && event.job.v_model_desc) || ""}`}
</span>
</div>
)}
{event.job ? (
<div>
<div>{`${t("jobs.fields.ro_number")}: ${(event.job &&
event.job.ro_number) ||
""}`}</div>
<div>{`${t("jobs.fields.ro_number")}: ${
(event.job && event.job.ro_number) || ""
}`}</div>
<div>
{t("jobs.fields.clm_total")}:
<CurrencyFormatter>
{(event.job && event.job.clm_total) || ""}
</CurrencyFormatter>
</div>
<div>{`${t("jobs.fields.clm_no")}: ${(event.job &&
event.job.clm_no) ||
""}`}</div>
<div>{`${t("jobs.fields.clm_no")}: ${
(event.job && event.job.clm_no) || ""
}`}</div>
<div>
{t("jobs.fields.ownr_ea")}:{(event.job && event.job.ownr_ea) || ""}
</div>
@@ -76,26 +76,31 @@ export default function ScheduleEventComponent({ event, handleCancel }) {
</div>
);
const RegularEvent = event.isintake ? (
<div>
<strong>{`${(event.job && event.job.ownr_fn) || ""} ${
(event.job && event.job.ownr_ln) || ""
}`}</strong>
<span style={{ margin: 4 }}>
{`${(event.job && event.job.v_model_yr) || ""} ${
(event.job && event.job.v_make_desc) || ""
} ${(event.job && event.job.v_model_desc) || ""}`}
</span>
</div>
) : (
<div>
<strong>{`${event.title || ""}`}</strong>
</div>
);
const Load = (
<div className="load">
<Progress percent={77} showInfo={false} />
</div>
);
return (
<Popover content={popoverContent}>
<div>
{event.isintake ? (
<div>
<strong>{`${(event.job && event.job.ownr_fn) || ""} ${(event.job &&
event.job.ownr_ln) ||
""}`}</strong>
<span style={{ margin: 4 }}>
{`${(event.job && event.job.v_model_yr) ||
""} ${(event.job && event.job.v_make_desc) ||
""} ${(event.job && event.job.v_model_desc) || ""}`}
</span>
</div>
) : (
<div>
<strong>{`${event.title || ""}`}</strong>
</div>
)}
</div>
<div>{event.allDay ? Load : RegularEvent}</div>
</Popover>
);
}

View File

@@ -6,7 +6,7 @@ import { useTranslation } from "react-i18next";
import { DateTimeFormatter } from "../../utils/DateFormatter";
export default function ScheduleExistingAppointmentsList({
existingAppointments
existingAppointments,
}) {
const { t } = useTranslation();
if (existingAppointments.loading) return <LoadingSpinner />;
@@ -22,22 +22,26 @@ export default function ScheduleExistingAppointmentsList({
<div>
{t("appointments.labels.priorappointments")}
<Timeline>
{existingAppointments.data.appointments.map(item => {
return (
<Timeline.Item
key={item.id}
color={item.canceled ? "red" : item.arrived ? "green" : "blue"}
>
{item.canceled
? t("appointments.labels.cancelledappointment")
: item.arrived
? t("appointments.labels.arrivedon")
: t("appointments.labels.scheduledfor")}
{existingAppointments.data
? existingAppointments.data.appointments.map((item) => {
return (
<Timeline.Item
key={item.id}
color={
item.canceled ? "red" : item.arrived ? "green" : "blue"
}
>
{item.canceled
? t("appointments.labels.cancelledappointment")
: item.arrived
? t("appointments.labels.arrivedon")
: t("appointments.labels.scheduledfor")}
<DateTimeFormatter>{item.start}</DateTimeFormatter>
</Timeline.Item>
);
})}
<DateTimeFormatter>{item.start}</DateTimeFormatter>
</Timeline.Item>
);
})
: null}
</Timeline>
</div>
);

View File

@@ -48,7 +48,7 @@ export function ScheduleJobModalContainer({
const existingAppointments = useQuery(QUERY_APPOINTMENTS_BY_JOBID, {
variables: { jobid: jobId },
fetchPolicy: "network-only",
skip: !visible,
skip: !visible || !!!jobId,
});
//TODO Customize the amount of minutes it will add.

View File

@@ -1,8 +1,17 @@
import gql from "graphql-tag";
export const QUERY_ALL_ACTIVE_APPOINTMENTS = gql`
query QUERY_ALL_ACTIVE_APPOINTMENTS {
appointments(where: { canceled: { _eq: false } }) {
query QUERY_ALL_ACTIVE_APPOINTMENTS(
$start: timestamptz!
$end: timestamptz!
) {
appointments(
where: {
canceled: { _eq: false }
end: { _lte: $end }
start: { _gte: $start }
}
) {
start
id
end
@@ -11,18 +20,16 @@ export const QUERY_ALL_ACTIVE_APPOINTMENTS = gql`
job {
ro_number
ownr_ln
ownr_co_nm
ownr_fn
ownr_ph1
ownr_ea
clm_total
id
clm_no
vehicle {
id
v_model_yr
v_make_desc
v_model_desc
}
v_model_yr
v_make_desc
v_model_desc
}
}
}
@@ -94,3 +101,53 @@ export const QUERY_APPOINTMENTS_BY_JOBID = gql`
}
}
`;
export const QUERY_SCHEDULE_LOAD_DATA = gql`
query QUERY_SCHEDULE_LOAD_DATA($start: timestamptz!, $end: timestamptz!) {
productionview_aggregate {
aggregate {
sum {
larhrs
labhrs
}
}
}
compJobs: jobs(
where: { scheduled_completion: { _gte: $start, _lte: $end } }
) {
id
scheduled_completion
labhrs: joblines_aggregate(where: { mod_lbr_ty: { _eq: "LAB" } }) {
aggregate {
sum {
mod_lb_hrs
}
}
}
larhrs: joblines_aggregate(where: { mod_lbr_ty: { _eq: "LAR" } }) {
aggregate {
sum {
mod_lb_hrs
}
}
}
}
arrJobs: jobs(where: { scheduled_in: { _gte: $start, _lte: $end } }) {
id
scheduled_in
labhrs: joblines_aggregate(where: { mod_lbr_ty: { _eq: "LAB" } }) {
aggregate {
sum {
mod_lb_hrs
}
}
}
larhrs: joblines_aggregate(where: { mod_lbr_ty: { _eq: "LAR" } }) {
aggregate {
sum {
mod_lb_hrs
}
}
}
}
}
`;

View File

@@ -12,4 +12,9 @@ export const endLoading = (options) => ({
export const setBreadcrumbs = (breadcrumbs) => ({
type: ApplicationActionTypes.SET_BREAD_CRUMBS,
payload: breadcrumbs,
});
});
export const calculateScheduleLoad = (rangeEnd) => ({
type: ApplicationActionTypes.CALCULATE_SCHEDULE_LOAD,
payload: rangeEnd,
});

View File

@@ -3,10 +3,43 @@ import ApplicationActionTypes from "./application.types";
const INITIAL_STATE = {
loading: false,
breadcrumbs: [],
scheduleLoad: {
load: [],
calculating: false,
error: null,
},
};
const applicationReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case ApplicationActionTypes.SET_BREAD_CRUMBS:
return {
...state,
breadcrumbs: action.payload,
};
case ApplicationActionTypes.CALCULATE_SCHEDULE_LOAD:
return {
...state,
scheduleLoad: { ...state.scheduleLoad, calculating: true, error: null },
};
case ApplicationActionTypes.CALCULATE_SCHEDULE_LOAD_SUCCESS:
return {
...state,
scheduleLoad: {
...state.scheduleLoad,
load: action.payload,
calculating: false,
},
};
case ApplicationActionTypes.CALCULATE_SCHEDULE_LOAD_FAILURE:
return {
...state,
scheduleLoad: {
...state.scheduleLoad,
calculating: false,
error: action.payload,
},
};
case ApplicationActionTypes.START_LOADING:
return {
...state,
@@ -17,11 +50,7 @@ const applicationReducer = (state = INITIAL_STATE, action) => {
...state,
loading: false,
};
case ApplicationActionTypes.SET_BREAD_CRUMBS:
return {
...state,
breadcrumbs: action.payload,
};
default:
return state;
}

View File

@@ -1,47 +1,87 @@
import { all } from "redux-saga/effects";
import { all, takeLatest, call } from "redux-saga/effects";
import ApplicationActionTypes from "./application.types";
import { client } from "../../App/App.container";
import { QUERY_SCHEDULE_LOAD_DATA } from "../../graphql/appointments.queries";
import moment from "moment";
// export function* onSendEmail() {
// yield takeLatest(EmailActionTypes.SEND_EMAIL, sendEmail);
// }
// export function* sendEmail(payload) {
// try {
// console.log("Sending thta email", payload);
// axios.post("/sendemail", payload).then(response => {
// console.log(JSON.stringify(response));
// put(sendEmailSuccess());
// });
// } catch (error) {
// console.log("Error in sendEmail saga.");
// yield put(sendEmailFailure(error.message));
// }
// }
export function* onCalculateScheduleLoad() {
yield takeLatest(
ApplicationActionTypes.CALCULATE_SCHEDULE_LOAD,
calculateScheduleLoad
);
}
export function* calculateScheduleLoad({ payload: end }) {
const today = moment(new Date()).startOf("day");
try {
const result = yield client.query({
query: QUERY_SCHEDULE_LOAD_DATA,
variables: {
start: today,
end: end,
},
});
// export function* onSendEmailSuccess() {
// yield takeLatest(EmailActionTypes.SEND_EMAIL_SUCCESS, sendEmailSuccessSaga);
// }
// export function* sendEmailSuccessSaga() {
// try {
// console.log("Send email success.");
// } catch (error) {
// console.log("Error in sendEmailSuccess saga.");
// yield put(sendEmailFailure(error.message));
// }
// }
const productionHoursTotal =
result.data.productionview_aggregate.aggregate.sum.larhrs +
result.data.productionview_aggregate.aggregate.sum.labhrs;
// export function* onRenderTemplate() {
// yield takeLatest(ApplicationActionTypes.RENDER_TEMPLATE, renderTemplate);
// }
// export function* renderTemplate({ payload: template }) {
// try {
// yield console.log("Render Template Saga => template", template);
// } catch (error) {
// console.log("Error in sendEmailFailure saga.", error.message);
// }
// }
const range = Math.round(moment.duration(end.diff(today)).asDays());
let load = {};
//Start from today and go until the range.
// for (var day = 0; day < range; day++) {
// let workingDate = today.add(day, "days");
// console.log(
// "function*calculateScheduleLoad -> workingDate",
// workingDate.toLocaleString()
// );
// }
const { arrJobs, compJobs } = result.data;
arrJobs.forEach((item) => {
const itemDate = moment(item.scheduled_in).toISOString().substr(0, 10);
if (!!load[itemDate]) {
load[itemDate].hoursIn =
(load[itemDate].hoursIn || 0) +
item.labhrs.aggregate.sum.mod_lb_hrs +
item.larhrs.aggregate.sum.mod_lb_hrs;
} else {
load[itemDate] = {
hoursIn:
item.labhrs.aggregate.sum.mod_lb_hrs +
item.larhrs.aggregate.sum.mod_lb_hrs,
};
}
});
compJobs.forEach((item) => {
const itemDate = moment(item.scheduled_completion)
.toISOString()
.substr(0, 10);
if (!!load[itemDate]) {
load[itemDate].hoursOut =
(load[itemDate].hoursOut || 0) +
item.labhrs.aggregate.sum.mod_lb_hrs +
item.larhrs.aggregate.sum.mod_lb_hrs;
} else {
load[itemDate] = {
hoursOut:
item.labhrs.aggregate.sum.mod_lb_hrs +
item.larhrs.aggregate.sum.mod_lb_hrs,
};
}
});
console.log("load", load);
} catch (error) {
//console.log("Error in sendEmailFailure saga.", error.message);
console.log("error", error);
}
}
export function* applicationSagas() {
yield all([
// call(onSendEmail),
call(onCalculateScheduleLoad),
// call(onSendEmailFailure),
// call(onSendEmailSuccess)
//call(onRenderTemplate),

View File

@@ -11,3 +11,13 @@ export const selectBreadcrumbs = createSelector(
[selectApplication],
(application) => application.breadcrumbs
);
export const selectScheduleLoad = createSelector(
[selectApplication],
(application) => application.scheduleLoad.load
);
export const selectScheduleLoadCalculating = createSelector(
[selectApplication],
(application) => application.scheduleLoad.calculating
);

View File

@@ -2,5 +2,8 @@ const ApplicationActionTypes = {
START_LOADING: "START_LOADING",
END_LOADING: "END_LOADING",
SET_BREAD_CRUMBS: "SET_BREAD_CRUMBS",
CALCULATE_SCHEDULE_LOAD: "CALCULATE_SCHEDULE_LOAD",
CALCULATE_SCHEDULE_LOAD_SUCCESS: "CALCULATE_SCHEDULE_LOAD_SUCCESS",
CALCULATE_SCHEDULE_LOAD_FAILURE: "CALCULATE_SCHEDULE_LOAD_FAILURE",
};
export default ApplicationActionTypes;

View File

@@ -0,0 +1,49 @@
- args:
role: user
table:
name: productionview
schema: public
type: drop_select_permission
- args:
permission:
allow_aggregations: false
columns:
- id
- status
- ro_number
- est_number
- ownr_fn
- ownr_ln
- v_model_yr
- v_model_desc
- clm_no
- v_make_desc
- v_color
- plate_no
- actual_in
- scheduled_completion
- scheduled_delivery
- ins_co_nm
- clm_total
- ownr_ph1
- special_coverage_policy
- production_vars
- labhrs
- larhrs
- shopid
- partcount
computed_fields: []
filter:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
role: user
table:
name: productionview
schema: public
type: create_select_permission

View File

@@ -0,0 +1,49 @@
- args:
role: user
table:
name: productionview
schema: public
type: drop_select_permission
- args:
permission:
allow_aggregations: true
columns:
- id
- status
- ro_number
- est_number
- ownr_fn
- ownr_ln
- v_model_yr
- v_model_desc
- clm_no
- v_make_desc
- v_color
- plate_no
- actual_in
- scheduled_completion
- scheduled_delivery
- ins_co_nm
- clm_total
- ownr_ph1
- special_coverage_policy
- production_vars
- labhrs
- larhrs
- shopid
- partcount
computed_fields: []
filter:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
role: user
table:
name: productionview
schema: public
type: create_select_permission

View File

@@ -2978,6 +2978,7 @@ tables:
_eq: X-Hasura-User-Id
- active:
_eq: true
allow_aggregations: true
- table:
schema: public
name: templates