diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index 3a852e2e8..6c65bd587 100644 --- a/bodyshop_translations.babel +++ b/bodyshop_translations.babel @@ -40880,6 +40880,48 @@ + + gsr_by_atp + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + gsr_by_category + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + gsr_by_csr false diff --git a/client/src/components/schedule-calendar-wrapper/schedule-calendar-header-graph.component.js b/client/src/components/schedule-calendar-wrapper/schedule-calendar-header-graph.component.js index 4d8887128..82d1c9281 100644 --- a/client/src/components/schedule-calendar-wrapper/schedule-calendar-header-graph.component.js +++ b/client/src/components/schedule-calendar-wrapper/schedule-calendar-header-graph.component.js @@ -22,10 +22,6 @@ const mapDispatchToProps = (dispatch) => ({ }); export function ScheduleCalendarHeaderGraph({ bodyshop, loadData }) { - console.log( - "🚀 ~ file: schedule-calendar-header-graph.component.js:23 ~ ScheduleCalendarHeaderGraph ~ loadData", - loadData - ); const { ssbuckets } = bodyshop; const { t } = useTranslation(); const data = useMemo(() => { diff --git a/client/src/components/schedule-calendar-wrapper/schedule-calendar-header.component.js b/client/src/components/schedule-calendar-wrapper/schedule-calendar-header.component.js index e312ebe03..732bcc2c2 100644 --- a/client/src/components/schedule-calendar-wrapper/schedule-calendar-header.component.js +++ b/client/src/components/schedule-calendar-wrapper/schedule-calendar-header.component.js @@ -66,8 +66,8 @@ export function ScheduleCalendarHeaderComponent({
e.stopPropagation()}> - {loadData && loadData.jobsOut ? ( - loadData.jobsOut.map((j) => ( + {loadData && loadData.allJobsOut ? ( + loadData.allJobsOut.map((j) => (
{j.ro_number} @@ -102,11 +102,12 @@ export function ScheduleCalendarHeaderComponent({
e.stopPropagation()}> - {loadData && loadData.jobsIn ? ( - loadData.jobsIn.map((j) => ( + {loadData && loadData.allJobsIn ? ( + loadData.allJobsIn.map((j) => (
{j.ro_number} + {j.status} @@ -142,7 +143,7 @@ export function ScheduleCalendarHeaderComponent({ title={t("appointments.labels.arrivingjobs")} > - {(loadData.hoursIn || 0) && loadData.hoursIn.toFixed(2)} + {(loadData.allHoursIn || 0) && loadData.allHoursIn.toFixed(2)} - {(loadData.hoursOut || 0) && loadData.hoursOut.toFixed(2)} + {(loadData.allHoursOut || 0) && loadData.allHoursOut.toFixed(2)} diff --git a/client/src/graphql/appointments.queries.js b/client/src/graphql/appointments.queries.js index 6a95864d8..01399b81a 100644 --- a/client/src/graphql/appointments.queries.js +++ b/client/src/graphql/appointments.queries.js @@ -294,6 +294,12 @@ export const QUERY_SCHEDULE_LOAD_DATA = gql` where: { inproduction: { _eq: true }, suspended: { _eq: false } } ) { id + actual_in + scheduled_in + actual_completion + scheduled_completion + inproduction + ro_number labhrs: joblines_aggregate( where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } } ) { @@ -327,12 +333,15 @@ export const QUERY_SCHEDULE_LOAD_DATA = gql` } ) { id + status ro_number scheduled_completion actual_completion + scheduled_in ownr_fn ownr_ln ownr_co_nm + inproduction labhrs: joblines_aggregate( where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } } ) { @@ -360,11 +369,16 @@ export const QUERY_SCHEDULE_LOAD_DATA = gql` ) { id scheduled_in + actual_in + scheduled_completion ro_number ownr_fn ownr_ln ownr_co_nm alt_transport + actual_completion + inproduction + status labhrs: joblines_aggregate( where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } } ) { diff --git a/client/src/redux/application/application.sagas.js b/client/src/redux/application/application.sagas.js index a768b3b93..e2dc607df 100644 --- a/client/src/redux/application/application.sagas.js +++ b/client/src/redux/application/application.sagas.js @@ -37,7 +37,7 @@ export function* calculateScheduleLoad({ payload: end }) { productionTotal: {}, productionHours: 0, }; - + let problemJobs = []; //Set the current load. buckets.forEach((bucket) => { load.productionTotal[bucket.id] = { count: 0, label: bucket.label }; @@ -45,6 +45,32 @@ export function* calculateScheduleLoad({ payload: end }) { prodJobs.forEach((item) => { //Add all of the jobs currently in production to the buckets so that we have a starting point. + if ( + !item.actual_completion && + moment(item.scheduled_completion).isBefore(moment().startOf("day")) + ) { + problemJobs.push({ + ...item, + code: "Job was scheduled to go, but it has not been completed. Update the scheduled completion date to correct projections", + }); + } + + if ( + item.actual_completion && + moment(item.actual_completion).isBefore(moment().startOf("day")) + ) { + problemJobs.push({ + ...item, + code: "Job is already marked as completed, but it is still in production. This job should be removed from production", + }); + } + if (!(item.actual_completion || item.scheduled_completion)) { + problemJobs.push({ + ...item, + code: "Job does not have a scheduled or actual completion date. Update the scheduled or actual completion dates to correct projections", + }); + } + const bucketId = CheckJobBucket(buckets, item); load.productionHours = load.productionHours + @@ -59,77 +85,119 @@ export function* calculateScheduleLoad({ payload: end }) { }); arrJobs.forEach((item) => { - if (!item.scheduled_in) + if (!item.scheduled_in) { console.log("JOB HAS NO SCHEDULED IN DATE.", item); - const itemDate = moment(item.scheduled_in).format("yyyy-MM-DD"); + problemJobs.push({ + ...item, + code: "Job has no scheduled in date", + }); + } + if ( + item.actual_completion && + moment(item.actual_completion).isAfter(moment()) + ) { + problemJobs.push({ + ...item, + code: "Job has an actual completion date set in the future", + }); + } + if (item.actual_completion && item.inproduction) { + problemJobs.push({ + ...item, + code: "Job has an actual completion date but it is still marked in production", + }); + } + + const itemDate = moment(item.actual_in || item.scheduled_in).format( + "yyyy-MM-DD" + ); + + const AddJobForSchedulingCalc = + !item.inproduction && + !moment(item.actual_completion || item.scheduled_completion).isSame( + moment(item.actual_in || item.scheduled_in), + "day" + ); + if (!!load[itemDate]) { - load[itemDate].hoursIn = - (load[itemDate].hoursIn || 0) + + load[itemDate].allHoursIn = + (load[itemDate].allHoursIn || 0) + item.labhrs.aggregate.sum.mod_lb_hrs + item.larhrs.aggregate.sum.mod_lb_hrs; - load[itemDate].jobsIn.push(item); + + //If the job hasn't already arrived, add it to the jobs in list. + // Make sure it also hasn't already been completed, or isn't an in and out job. + //This prevents the duplicate counting. + load[itemDate].allJobsIn.push(item); + if (AddJobForSchedulingCalc) { + load[itemDate].jobsIn.push(item); + load[itemDate].hoursIn = + (load[itemDate].hoursIn || 0) + + item.labhrs.aggregate.sum.mod_lb_hrs + + item.larhrs.aggregate.sum.mod_lb_hrs; + } } else { load[itemDate] = { - jobsIn: [item], + allJobsIn: [item], + jobsIn: AddJobForSchedulingCalc ? [item] : [], //Same as above, only add it if it isn't already in production. jobsOut: [], - hoursIn: + allJobsOut: [], + allHoursIn: item.labhrs.aggregate.sum.mod_lb_hrs + item.larhrs.aggregate.sum.mod_lb_hrs, + hoursIn: AddJobForSchedulingCalc + ? item.labhrs.aggregate.sum.mod_lb_hrs + + item.larhrs.aggregate.sum.mod_lb_hrs + : 0, }; } }); - let problemJobs = []; compJobs.forEach((item) => { if (!(item.actual_completion || item.scheduled_completion)) - console.log("JOB HAS NO COMPLETION DATE.", item); + console.warn("JOB HAS NO COMPLETION DATE.", item); const inProdJobs = prodJobs.find((p) => p.id === item.id); const inArrJobs = arrJobs.find((p) => p.id === item.id); - if (!(inProdJobs || inArrJobs)) { - //Job isn't found in production or coming in. - //is it going today or scheduled to go today? - if ( - moment(item.actual_completion || item.scheduled_completion).isSame( - moment(), - "day" - ) - ) { - console.log("Job is going today anyways, ignore it.", item); - return; - } - - if ( - moment(item.actual_completion || item.scheduled_completion).isBefore( - moment(), - "day" - ) - ) { - console.log("Job should have already gone. Ignoring it.", item); - return; - } - - 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 AddJobForSchedulingCalc = + item.inproduction && + !moment(item.actual_completion || item.scheduled_completion).isSame( + moment(item.scheduled_in), + "day" + ) && + (inProdJobs || inArrJobs); const itemDate = moment( item.actual_completion || item.scheduled_completion ).format("yyyy-MM-DD"); + //Skip it, it's already completed. + if (!!load[itemDate]) { - load[itemDate].hoursOut = - (load[itemDate].hoursOut || 0) + + load[itemDate].allHoursOut = + (load[itemDate].allHoursOut || 0) + item.labhrs.aggregate.sum.mod_lb_hrs + item.larhrs.aggregate.sum.mod_lb_hrs; - load[itemDate].jobsOut.push(item); + //Add only the jobs that are still in production to get rid of. + //If it's not in production, we'd subtract unnecessarily. + load[itemDate].allJobsOut.push(item); + + if (AddJobForSchedulingCalc) { + load[itemDate].jobsOut.push(item); + load[itemDate].hoursOut = + (load[itemDate].hoursOut || 0) + + item.labhrs.aggregate.sum.mod_lb_hrs + + item.larhrs.aggregate.sum.mod_lb_hrs; + } } else { load[itemDate] = { - jobsOut: [item], - hoursOut: + allJobsOut: [item], + jobsOut: AddJobForSchedulingCalc ? [item] : [], //Same as above. + hoursOut: AddJobForSchedulingCalc + ? item.labhrs.aggregate.sum.mod_lb_hrs + + item.larhrs.aggregate.sum.mod_lb_hrs + : 0, + allHoursOut: item.labhrs.aggregate.sum.mod_lb_hrs + item.larhrs.aggregate.sum.mod_lb_hrs, }; @@ -137,6 +205,7 @@ export function* calculateScheduleLoad({ payload: end }) { }); //Propagate the expected load to each day. + 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"); @@ -146,6 +215,7 @@ export function* calculateScheduleLoad({ payload: end }) { if (!!!load[current]) { load[current] = {}; } + if (day === 0) { //Starting on day 1. The load is current. load[current].expectedLoad = CalculateLoad( diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index d8e7bc391..23f24f79d 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -49,7 +49,7 @@ "blocked": "Blocked", "cancelledappointment": "Canceled appointment for: ", "completingjobs": "Completing Jobs", - "dataconsistency": "{{ro_number}} has a data consistency issue. It has been excluded for scheduling purposes. CODE: {{code}}.", + "dataconsistency": "{{ro_number}} has a data consistency issue. It may have been excluded for scheduling purposes. CODE: {{code}}.", "expectedjobs": "Expected Jobs in Production: ", "expectedprodhrs": "Expected Production Hours:", "history": "History", @@ -2425,6 +2425,8 @@ "export_payables": "Export Log - Payables", "export_payments": "Export Log - Payments", "export_receivables": "Export Log - Receivables", + "gsr_by_atp": "Gross Sales by ATP", + "gsr_by_category": "Gross Sales by Category", "gsr_by_csr": "Gross Sales by CSR", "gsr_by_delivery_date": "Gross Sales by Delivery Date", "gsr_by_estimator": "Gross Sales by Estimator", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index 359d844a2..10385ab2e 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -2425,6 +2425,8 @@ "export_payables": "", "export_payments": "", "export_receivables": "", + "gsr_by_atp": "", + "gsr_by_category": "", "gsr_by_csr": "", "gsr_by_delivery_date": "", "gsr_by_estimator": "", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index 78f2d8777..ee9ffb395 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -2425,6 +2425,8 @@ "export_payables": "", "export_payments": "", "export_receivables": "", + "gsr_by_atp": "", + "gsr_by_category": "", "gsr_by_csr": "", "gsr_by_delivery_date": "", "gsr_by_estimator": "", diff --git a/client/src/utils/TemplateConstants.js b/client/src/utils/TemplateConstants.js index a53040f0f..8869a5b2c 100644 --- a/client/src/utils/TemplateConstants.js +++ b/client/src/utils/TemplateConstants.js @@ -1343,6 +1343,32 @@ export const TemplateList = (type, context) => { }, group: "sales", }, + gsr_by_category: { + title: i18n.t("reportcenter.templates.gsr_by_category"), + description: "", + subject: i18n.t("reportcenter.templates.gsr_by_category"), + key: "gsr_by_category", + //idtype: "vendor", + disabled: false, + rangeFilter: { + object: i18n.t("reportcenter.labels.objects.jobs"), + field: i18n.t("jobs.fields.date_invoiced"), + }, + group: "sales", + }, + gsr_by_atp: { + title: i18n.t("reportcenter.templates.gsr_by_atp"), + description: "", + subject: i18n.t("reportcenter.templates.gsr_by_atp"), + key: "gsr_by_atp", + //idtype: "vendor", + disabled: false, + rangeFilter: { + object: i18n.t("reportcenter.labels.objects.jobs"), + field: i18n.t("jobs.fields.date_invoiced"), + }, + group: "sales", + }, gsr_labor_only: { title: i18n.t("reportcenter.templates.gsr_labor_only"), description: "", diff --git a/server/email/sendemail.js b/server/email/sendemail.js index 68c57a38d..28c0adf5d 100644 --- a/server/email/sendemail.js +++ b/server/email/sendemail.js @@ -23,6 +23,7 @@ let transporter = nodemailer.createTransport({ }); exports.sendServerEmail = async function ({ subject, text }) { + if (process.env.NODE_ENV === undefined) return; try { transporter.sendMail( {