diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index d11e1ed2d..fe1e7a2a0 100644 --- a/bodyshop_translations.babel +++ b/bodyshop_translations.babel @@ -4598,6 +4598,27 @@ + + md_from_emails + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + md_hour_split @@ -7280,6 +7301,32 @@ + + ss_configuration + + + dailyhrslimit + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + ssbuckets @@ -13205,6 +13252,27 @@ + + from + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + subject false diff --git a/client/src/components/email-overlay/email-overlay.component.jsx b/client/src/components/email-overlay/email-overlay.component.jsx index 6826eba76..baf52d376 100644 --- a/client/src/components/email-overlay/email-overlay.component.jsx +++ b/client/src/components/email-overlay/email-overlay.component.jsx @@ -16,9 +16,14 @@ import EmailDocumentsComponent from "../email-documents/email-documents.componen import _ from "lodash"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; -import { selectBodyshop } from "../../redux/user/user.selectors"; +import { + selectBodyshop, + selectCurrentUser, +} from "../../redux/user/user.selectors"; + const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, + currentUser: selectCurrentUser, }); const mapDispatchToProps = (dispatch) => ({ //setUserLanguage: language => dispatch(setUserLanguage(language)) @@ -28,7 +33,12 @@ export default connect( mapDispatchToProps )(EmailOverlayComponent); -export function EmailOverlayComponent({ form, selectedMediaState, bodyshop }) { +export function EmailOverlayComponent({ + form, + selectedMediaState, + bodyshop, + currentUser, +}) { const { t } = useTranslation(); const handleClick = ({ item, key, keyPath }) => { const email = item.props.value; @@ -51,6 +61,27 @@ export function EmailOverlayComponent({ form, selectedMediaState, bodyshop }) { return (
+ + + diff --git a/client/src/components/email-overlay/email-overlay.container.jsx b/client/src/components/email-overlay/email-overlay.container.jsx index c2dc7d261..9276f35ee 100644 --- a/client/src/components/email-overlay/email-overlay.container.jsx +++ b/client/src/components/email-overlay/email-overlay.container.jsx @@ -56,13 +56,9 @@ export function EmailOverlayContainer({ : bodyshop.shopname, address: EmailSettings.fromAddress, }, - ReplyTo: { - Email: currentUser.validemail ? currentUser.email : bodyshop.email, - Name: currentUser.displayName, - }, }; - const handleFinish = async (values) => { + const handleFinish = async (allValues) => { logImEXEvent("email_send_from_modal"); //const attachments = []; @@ -77,10 +73,15 @@ export function EmailOverlayContainer({ // attachments.push(t); // }); + const { from, ...values } = allValues; setSending(true); try { await axios.post("/sendemail", { ...defaultEmailFrom, + ReplyTo: { + Email: from, + Name: currentUser.displayName, + }, ...values, html: rawHtml, attachments: [ @@ -138,6 +139,7 @@ export function EmailOverlayContainer({ } form.setFieldsValue({ + from: currentUser.validemail ? currentUser.email : bodyshop.email, ...emailConfig.messageOptions, cc: emailConfig.messageOptions.cc && diff --git a/client/src/components/shop-info/shop-info.general.component.jsx b/client/src/components/shop-info/shop-info.general.component.jsx index 29288b9ee..3b2b748a0 100644 --- a/client/src/components/shop-info/shop-info.general.component.jsx +++ b/client/src/components/shop-info/shop-info.general.component.jsx @@ -507,6 +507,18 @@ export default function ShopInfoGeneral({ form }) { > + + + + + {t("bodyshop.labels.workingdays")} diff --git a/client/src/graphql/bodyshop.queries.js b/client/src/graphql/bodyshop.queries.js index f0bd54a21..0ce955c15 100644 --- a/client/src/graphql/bodyshop.queries.js +++ b/client/src/graphql/bodyshop.queries.js @@ -101,6 +101,8 @@ export const QUERY_BODYSHOP = gql` md_filehandlers md_email_cc timezone + ss_configuration + md_from_emails employees { user_email id @@ -199,6 +201,8 @@ export const UPDATE_SHOP = gql` md_filehandlers md_email_cc timezone + ss_configuration + md_from_emails employees { id first_name @@ -266,7 +270,6 @@ export const QUERY_DELIVER_CHECKLIST = gql` ro_number actual_completion actual_delivery - } } `; diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index 255a81a03..0c456db00 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -285,6 +285,7 @@ "md_classes": "Classes", "md_ded_notes": "Deductible Notes", "md_email_cc": "Auto Email CC: $t(printcenter.subjects.jobs.{{template}})", + "md_from_emails": "Additional From Emails", "md_hour_split": { "paint": "Paint Hour Split", "prep": "Prep Hour Split" @@ -455,6 +456,9 @@ "label": "Label", "templates": "Templates" }, + "ss_configuration": { + "dailyhrslimit": "Daily Incoming Hours Limit" + }, "ssbuckets": { "gte": "Greater Than/Equal to (hrs)", "id": "ID", @@ -827,6 +831,7 @@ }, "fields": { "cc": "CC", + "from": "From", "subject": "Subject", "to": "To" }, diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index 336ba07d4..ea698ce2a 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -285,6 +285,7 @@ "md_classes": "", "md_ded_notes": "", "md_email_cc": "", + "md_from_emails": "", "md_hour_split": { "paint": "", "prep": "" @@ -455,6 +456,9 @@ "label": "", "templates": "" }, + "ss_configuration": { + "dailyhrslimit": "" + }, "ssbuckets": { "gte": "", "id": "", @@ -827,6 +831,7 @@ }, "fields": { "cc": "", + "from": "", "subject": "", "to": "" }, diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index 67435a1fc..70f0141b2 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -285,6 +285,7 @@ "md_classes": "", "md_ded_notes": "", "md_email_cc": "", + "md_from_emails": "", "md_hour_split": { "paint": "", "prep": "" @@ -455,6 +456,9 @@ "label": "", "templates": "" }, + "ss_configuration": { + "dailyhrslimit": "" + }, "ssbuckets": { "gte": "", "id": "", @@ -827,6 +831,7 @@ }, "fields": { "cc": "", + "from": "", "subject": "", "to": "" }, diff --git a/client/src/utils/RenderTemplate.js b/client/src/utils/RenderTemplate.js index 7aab1139a..716322937 100644 --- a/client/src/utils/RenderTemplate.js +++ b/client/src/utils/RenderTemplate.js @@ -24,13 +24,34 @@ export default async function RenderTemplate( let { contextData, useShopSpecificTemplate } = await fetchContextData( templateObject ); + const { ignoreCustomMargins } = Templates[templateObject.name]; let reportRequest = { template: { name: useShopSpecificTemplate ? `/${bodyshop.imexshopid}/${templateObject.name}` : `/${templateObject.name}`, - ...(renderAsHtml ? {} : { recipe: "chrome-pdf" }), + ...(renderAsHtml + ? {} + : { + recipe: "chrome-pdf", + ...(!ignoreCustomMargins && { + chrome: { + marginTop: + bodyshop.logo_img_path && + bodyshop.logo_img_path.headerMargin && + bodyshop.logo_img_path.headerMargin > 36 + ? bodyshop.logo_img_path.headerMargin + : "36px", + marginBottom: + bodyshop.logo_img_path && + bodyshop.logo_img_path.footerMargin && + bodyshop.logo_img_path.footerMargin > 36 + ? bodyshop.logo_img_path.footerMargin + : "36px", + }, + }), + }), ...(renderAsExcel ? { recipe: "html-to-xlsx" } : {}), }, data: { @@ -121,7 +142,25 @@ export async function RenderTemplates( name: rootTemplate.useShopSpecificTemplate ? `/${bodyshop.imexshopid}/${rootTemplate.templateObject.name}` : `/${rootTemplate.templateObject.name}`, - ...(renderAsHtml ? {} : { recipe: "chrome-pdf" }), + ...(renderAsHtml + ? {} + : { + recipe: "chrome-pdf", + chrome: { + marginTop: + bodyshop.logo_img_path && + bodyshop.logo_img_path.headerMargin && + bodyshop.logo_img_path.headerMargin > 36 + ? bodyshop.logo_img_path.headerMargin + : "36px", + marginBottom: + bodyshop.logo_img_path && + bodyshop.logo_img_path.footerMargin && + bodyshop.logo_img_path.footerMargin > 36 + ? bodyshop.logo_img_path.footerMargin + : "36px", + }, + }), pdfOperations: templateAndData.map((template) => { return { template: { @@ -151,7 +190,7 @@ export async function RenderTemplates( }; try { - const render = await jsreport.renderAsync(reportRequest); + const render = await jsreport.renderAsync({ reportRequest }); if (!renderAsHtml) { render.download("Speed Print"); } else { diff --git a/client/src/utils/TemplateConstants.js b/client/src/utils/TemplateConstants.js index 6201afada..06f624a4a 100644 --- a/client/src/utils/TemplateConstants.js +++ b/client/src/utils/TemplateConstants.js @@ -371,6 +371,7 @@ export const TemplateList = (type, context) => { subject: i18n.t("printcenter.jobs.parts_label_single"), disabled: false, group: "ro", + ignoreCustomMargins: true, }, envelope_return_address: { title: i18n.t("printcenter.jobs.envelope_return_address"), @@ -379,6 +380,7 @@ export const TemplateList = (type, context) => { key: "envelope_return_address", disabled: false, group: "ro", + ignoreCustomMargins: true, }, sgi_certificate_of_repairs: { title: i18n.t("printcenter.jobs.sgi_certificate_of_repairs"), diff --git a/hasura/metadata/tables.yaml b/hasura/metadata/tables.yaml index c91b048fc..52650b30f 100644 --- a/hasura/metadata/tables.yaml +++ b/hasura/metadata/tables.yaml @@ -834,6 +834,7 @@ - md_email_cc - md_estimators - md_filehandlers + - md_from_emails - md_hour_split - md_ins_cos - md_jobline_presets @@ -860,6 +861,7 @@ - shopname - shoprates - speedprint + - ss_configuration - ssbuckets - state - state_tax_id @@ -915,6 +917,7 @@ - md_email_cc - md_estimators - md_filehandlers + - md_from_emails - md_hour_split - md_ins_cos - md_jobline_presets @@ -938,6 +941,7 @@ - shopname - shoprates - speedprint + - ss_configuration - ssbuckets - state - state_tax_id diff --git a/hasura/migrations/1647876980174_alter_table_public_bodyshops_add_column_ss_configuration/down.sql b/hasura/migrations/1647876980174_alter_table_public_bodyshops_add_column_ss_configuration/down.sql new file mode 100644 index 000000000..fbb2eef6c --- /dev/null +++ b/hasura/migrations/1647876980174_alter_table_public_bodyshops_add_column_ss_configuration/down.sql @@ -0,0 +1,4 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- alter table "public"."bodyshops" add column "ss_configuration" jsonb +-- null default jsonb_build_object(); diff --git a/hasura/migrations/1647876980174_alter_table_public_bodyshops_add_column_ss_configuration/up.sql b/hasura/migrations/1647876980174_alter_table_public_bodyshops_add_column_ss_configuration/up.sql new file mode 100644 index 000000000..c693c6d4e --- /dev/null +++ b/hasura/migrations/1647876980174_alter_table_public_bodyshops_add_column_ss_configuration/up.sql @@ -0,0 +1,2 @@ +alter table "public"."bodyshops" add column "ss_configuration" jsonb + null default jsonb_build_object(); diff --git a/hasura/migrations/1648154167416_alter_table_public_bodyshops_add_column_md_from_emails/down.sql b/hasura/migrations/1648154167416_alter_table_public_bodyshops_add_column_md_from_emails/down.sql new file mode 100644 index 000000000..00a90c958 --- /dev/null +++ b/hasura/migrations/1648154167416_alter_table_public_bodyshops_add_column_md_from_emails/down.sql @@ -0,0 +1,4 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- alter table "public"."bodyshops" add column "md_from_emails" jsonb +-- null default jsonb_build_array(); diff --git a/hasura/migrations/1648154167416_alter_table_public_bodyshops_add_column_md_from_emails/up.sql b/hasura/migrations/1648154167416_alter_table_public_bodyshops_add_column_md_from_emails/up.sql new file mode 100644 index 000000000..108bbd7e8 --- /dev/null +++ b/hasura/migrations/1648154167416_alter_table_public_bodyshops_add_column_md_from_emails/up.sql @@ -0,0 +1,2 @@ +alter table "public"."bodyshops" add column "md_from_emails" jsonb + null default jsonb_build_array(); diff --git a/server/data/autohouse.js b/server/data/autohouse.js index 8e0befe51..76030b5af 100644 --- a/server/data/autohouse.js +++ b/server/data/autohouse.js @@ -576,6 +576,7 @@ const CreateRepairOrderTag = (job, errorCallback) => { SalesTaxTotal: Dinero(job.job_totals.totals.local_tax) .add(Dinero(job.job_totals.totals.state_tax)) .add(Dinero(job.job_totals.totals.federal_tax)) + .add(Dinero(job.job_totals.additional.pvrt)) .toFormat(AHDineroFormat), SalesTaxTotalCost: 0, GrossTotal: Dinero(job.job_totals.totals.total_repairs).toFormat( diff --git a/server/graphql-client/queries.js b/server/graphql-client/queries.js index 306cb06cb..641adbd82 100644 --- a/server/graphql-client/queries.js +++ b/server/graphql-client/queries.js @@ -464,6 +464,7 @@ exports.QUERY_UPCOMING_APPOINTMENTS = `query QUERY_UPCOMING_APPOINTMENTS($now: t jobs_by_pk(id: $jobId) { bodyshop { ssbuckets + ss_configuration target_touchtime workingdays timezone diff --git a/server/scheduling/scheduling-job.js b/server/scheduling/scheduling-job.js index a314022c9..316bc7974 100644 --- a/server/scheduling/scheduling-job.js +++ b/server/scheduling/scheduling-job.js @@ -5,6 +5,7 @@ const Dinero = require("dinero.js"); const moment = require("moment-timezone"); const logger = require("../utils/logger"); const _ = require("lodash"); +const { filter } = require("lodash"); require("dotenv").config({ path: path.resolve( process.cwd(), @@ -32,7 +33,8 @@ exports.job = async (req, res) => { }); const { jobs_by_pk, blockedDays, prodJobs, arrJobs, compJobs } = result; - const { ssbuckets, workingdays, timezone } = result.jobs_by_pk.bodyshop; + const { ssbuckets, workingdays, timezone, ss_configuration } = + result.jobs_by_pk.bodyshop; const jobHrs = result.jobs_by_pk.jobhrs.aggregate.sum.mod_lb_hrs; const JobBucket = ssbuckets.filter( @@ -63,29 +65,68 @@ exports.job = async (req, res) => { } }); - const filteredArrJobs = arrJobs.filter( - (j) => JobBucket.id === CheckJobBucket(ssbuckets, j) - ); + // const filteredArrJobs = arrJobs.filter( + // (j) => JobBucket.id === CheckJobBucket(ssbuckets, j) + // ); + const filteredArrJobs = []; + + // filteredArrJobs.forEach((item) => { + // const itemDate = moment(item.scheduled_in) + // .tz(timezone) + // .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, + // }; + // } + // }); + + arrJobs.forEach((item) => { + let isSameBucket = false; + if (JobBucket.id === CheckJobBucket(ssbuckets, item)) { + filteredArrJobs.push(item); + isSameBucket = true; + } + + let jobHours = + item.labhrs.aggregate.sum.mod_lb_hrs + + item.larhrs.aggregate.sum.mod_lb_hrs; - filteredArrJobs.forEach((item) => { const itemDate = moment(item.scheduled_in) .tz(timezone) .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 { + if (isSameBucket) { + if (!!load[itemDate]) { + load[itemDate].hoursIn = (load[itemDate].hoursIn || 0) + jobHours; + load[itemDate].jobsIn.push(item); + } else { + load[itemDate] = { + jobsIn: [item], + jobsOut: [], + hoursIn: jobHours, + }; + } + } + if (!load[itemDate]) { load[itemDate] = { - jobsIn: [item], + jobsIn: [], jobsOut: [], - hoursIn: - item.labhrs.aggregate.sum.mod_lb_hrs + - item.larhrs.aggregate.sum.mod_lb_hrs, + hoursIn: 0, + hoursInTotal: 0, }; } + load[itemDate].hoursInTotal = + (load[itemDate].hoursInTotal || 0) + jobHours; }); //Get the completing jobs. @@ -214,11 +255,25 @@ exports.job = async (req, res) => { (workingdays[dayOfWeekMapper(moment(loadKey).day())] || false) && !load[loadKey].blocked; + let isUnderDailyTotalLimit = true; + + if ( + ss_configuration && + ss_configuration.dailyhrslimit && + ss_configuration.dailyhrslimit > 0 && + load[loadKey] && + load[loadKey].hoursInTotal && + load[loadKey].hoursInTotal > ss_configuration.dailyhrslimit + ) { + isUnderDailyTotalLimit = false; + } + if ( load[loadKey].expectedLoad && load[loadKey].expectedLoad[JobBucket.id] && JobBucket.target > load[loadKey].expectedLoad[JobBucket.id].count && - isShopOpen + isShopOpen && + isUnderDailyTotalLimit ) possibleDates.push(new Date(loadKey).toISOString().substr(0, 10)); });