diff --git a/client/src/components/bill-form/bill-form.component.jsx b/client/src/components/bill-form/bill-form.component.jsx index 44b8cd815..ba9f937ba 100644 --- a/client/src/components/bill-form/bill-form.component.jsx +++ b/client/src/components/bill-form/bill-form.component.jsx @@ -79,6 +79,20 @@ export function BillFormComponent({ }); }; + const handleFederalTaxExemptSwitchToggle = (checked) => { + // Early gate + if (!checked) return; + const values = form.getFieldsValue("billlines"); + // Gate bill lines + if (!values?.billlines?.length) return; + + const billlines = values.billlines.map((b) => { + b.applicable_taxes.federal = false; + return b; + }); + form.setFieldsValue({ billlines }); + }; + useEffect(() => { if (job) form.validateFields(["is_credit_memo"]); }, [job, form]); @@ -387,7 +401,16 @@ export function BillFormComponent({ > - + {bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid ? ( + + + + ) : null} + {() => { const values = form.getFieldsValue([ "billlines", @@ -405,7 +428,7 @@ export function BillFormComponent({ totals = CalculateBillTotal(values); if (!!totals) return ( -
+
+ ({ + setEmailOptions: (e) => dispatch(setEmailOptions(e)), +}); + +export function ChatPrintButton({ conversation }) { + const [loading, setLoading] = useState(false); + + return ( + + { + setLoading(true); + GenerateDocument( + { + name: TemplateList("messaging").conversation_list.key, + variables: { id: conversation.id }, + }, + { + subject: TemplateList("messaging").conversation_list.subject, + }, + "p", + conversation.id + ); + setLoading(false); + }} + /> + { + setLoading(true); + GenerateDocument( + { + name: TemplateList("messaging").conversation_list.key, + variables: { id: conversation.id }, + }, + { + subject: TemplateList("messaging").conversation_list.subject, + }, + "e", + conversation.id + ); + setLoading(false); + }} + /> + {loading && } + + ); +} +export default connect(mapStateToProps, mapDispatchToProps)(ChatPrintButton); diff --git a/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.component.jsx b/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.component.jsx index ae3ba9a7b..bf0b049fb 100644 --- a/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.component.jsx +++ b/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.component.jsx @@ -108,6 +108,14 @@ export function JobsDetailHeaderActions({ }, }, }); + insertAuditTrail({ + jobid: job.id, + operation: AuditTrailMapping.alertToggle( + !!job.production_vars && !!job.production_vars.alert + ? !job.production_vars.alert + : true + ), + }); }; const handleSuspend = (e) => { diff --git a/client/src/components/production-list-columns/production-list-columns.alert.component.jsx b/client/src/components/production-list-columns/production-list-columns.alert.component.jsx index 8823afb46..8bad135ca 100644 --- a/client/src/components/production-list-columns/production-list-columns.alert.component.jsx +++ b/client/src/components/production-list-columns/production-list-columns.alert.component.jsx @@ -1,12 +1,23 @@ import { ExclamationCircleFilled } from "@ant-design/icons"; +import { useMutation } from "@apollo/client"; import { Dropdown, Menu } from "antd"; import React from "react"; import { useTranslation } from "react-i18next"; -import { useMutation } from "@apollo/client"; -import { UPDATE_JOB } from "../../graphql/jobs.queries"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; import { logImEXEvent } from "../../firebase/firebase.utils"; +import { UPDATE_JOB } from "../../graphql/jobs.queries"; +import { insertAuditTrail } from "../../redux/application/application.actions"; +import AuditTrailMapping from "../../utils/AuditTrailMappings"; -export default function ProductionListColumnAlert({ record }) { +const mapStateToProps = createStructuredSelector({}); + +const mapDispatchToProps = (dispatch) => ({ + insertAuditTrail: ({ jobid, operation }) => + dispatch(insertAuditTrail({ jobid, operation })), +}); + +export function ProductionListColumnAlert({ record, insertAuditTrail }) { const { t } = useTranslation(); const [updateAlert] = useMutation(UPDATE_JOB); @@ -27,6 +38,14 @@ export default function ProductionListColumnAlert({ record }) { }, }, }, + }); + insertAuditTrail({ + jobid: record.id, + operation: AuditTrailMapping.alertToggle( + !!record.production_vars && !!record.production_vars.alert + ? !record.production_vars.alert + : true + ), }).then(() => { if (record.refetch) record.refetch(); }); @@ -58,3 +77,8 @@ export default function ProductionListColumnAlert({ record }) { ); } + +export default connect( + mapStateToProps, + mapDispatchToProps +)(ProductionListColumnAlert); diff --git a/client/src/components/rbac-wrapper/rbac-defaults.js b/client/src/components/rbac-wrapper/rbac-defaults.js index 24a564872..a01dd54c2 100644 --- a/client/src/components/rbac-wrapper/rbac-defaults.js +++ b/client/src/components/rbac-wrapper/rbac-defaults.js @@ -55,10 +55,11 @@ const ret = { "shiftclock:view": 2, "shop:config": 4, - "shop:rbac": 5, - "shop:vendors": 2, "shop:dashboard": 3, + "shop:rbac": 5, + "shop:reportcenter": 2, "shop:templates": 4, + "shop:vendors": 2, "temporarydocs:view": 2, diff --git a/client/src/components/report-center-modal/report-center-modal.container.jsx b/client/src/components/report-center-modal/report-center-modal.container.jsx index f0d361785..84fe65560 100644 --- a/client/src/components/report-center-modal/report-center-modal.container.jsx +++ b/client/src/components/report-center-modal/report-center-modal.container.jsx @@ -5,6 +5,7 @@ import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { toggleModalVisible } from "../../redux/modals/modals.actions"; import { selectReportCenter } from "../../redux/modals/modals.selectors"; +import RbacWrapperComponent from "../rbac-wrapper/rbac-wrapper.component"; import ReportCenterModalComponent from "./report-center-modal.component"; const mapStateToProps = createStructuredSelector({ @@ -33,7 +34,9 @@ export function ReportCenterModalContainer({ destroyOnClose width="80%" > - + + + ); } diff --git a/client/src/components/scoreboard-timetickets-stats/scoreboard-timetickets.component.jsx b/client/src/components/scoreboard-timetickets-stats/scoreboard-timetickets.component.jsx index 0117279d5..af4f28695 100644 --- a/client/src/components/scoreboard-timetickets-stats/scoreboard-timetickets.component.jsx +++ b/client/src/components/scoreboard-timetickets-stats/scoreboard-timetickets.component.jsx @@ -29,7 +29,7 @@ export default connect( export function ScoreboardTimeTicketsStats({ bodyshop }) { const { t } = useTranslation(); - const startDate = moment().startOf("month") + const startDate = moment().startOf("month"); const endDate = moment().endOf("month"); const fixedPeriods = useMemo(() => { @@ -84,6 +84,8 @@ export function ScoreboardTimeTicketsStats({ bodyshop }) { end: endDate.format("YYYY-MM-DD"), fixedStart: fixedPeriods.start.format("YYYY-MM-DD"), fixedEnd: fixedPeriods.end.format("YYYY-MM-DD"), + jobStart: startDate, + jobEnd: endDate, }, fetchPolicy: "network-only", nextFetchPolicy: "network-only", @@ -340,11 +342,21 @@ export function ScoreboardTimeTicketsStats({ bodyshop }) { larData.push({ ...r, ...lar }); }); + const jobData = {}; + data.jobs.forEach((job) => { + job.tthrs = job.joblines.reduce((acc, val) => acc + val.mod_lb_hrs, 0); + }); + jobData.tthrs = data.jobs + .reduce((acc, val) => acc + val.tthrs, 0) + .toFixed(1); + jobData.count = data.jobs.length.toFixed(0); + return { fixed: ret, combinedData: combinedData, labData: labData, larData: larData, + jobData: jobData, }; }, [fixedPeriods, data, bodyshop]); @@ -356,7 +368,10 @@ export function ScoreboardTimeTicketsStats({ bodyshop }) { - + {/* This Month */} - + @@ -482,7 +482,7 @@ export function ScoreboardTicketsStats({ data, bodyshop }) { {/* Last Month */} - + @@ -556,7 +556,7 @@ export function ScoreboardTicketsStats({ data, bodyshop }) { {/* Efficiency Over Period */} - + + + + + + + + + + + + {t("scoreboard.labels.totalhrs")} + + } + value={jobData.tthrs} + valueStyle={{ + fontSize: statisticSize, + fontWeight: statisticWeight, + }} + /> + + + + {/* Disclaimer */} diff --git a/client/src/components/scoreboard-timetickets/scoreboard-timetickets.component.jsx b/client/src/components/scoreboard-timetickets/scoreboard-timetickets.component.jsx index 0fc84af6d..9fdb384d1 100644 --- a/client/src/components/scoreboard-timetickets/scoreboard-timetickets.component.jsx +++ b/client/src/components/scoreboard-timetickets/scoreboard-timetickets.component.jsx @@ -65,6 +65,8 @@ export default function ScoreboardTimeTickets() { end: endDate.format("YYYY-MM-DD"), fixedStart: fixedPeriods.start.format("YYYY-MM-DD"), fixedEnd: fixedPeriods.end.format("YYYY-MM-DD"), + jobStart: startDate, + jobEnd: endDate, }, fetchPolicy: "network-only", nextFetchPolicy: "network-only", diff --git a/client/src/components/shop-info/shop-info.rbac.component.jsx b/client/src/components/shop-info/shop-info.rbac.component.jsx index fe4f80f31..e4152fb88 100644 --- a/client/src/components/shop-info/shop-info.rbac.component.jsx +++ b/client/src/components/shop-info/shop-info.rbac.component.jsx @@ -28,18 +28,6 @@ export function ShopInfoRbacComponent({ form, bodyshop }) { return ( - - - + + + + + + + + + + + + @@ -173,26 +209,38 @@ export function ShopInfoRbacComponent({ form, bodyshop }) { + + + @@ -208,30 +256,6 @@ export function ShopInfoRbacComponent({ form, bodyshop }) { > - - - - - - - - - @@ -280,6 +292,18 @@ export function ShopInfoRbacComponent({ form, bodyshop }) { > + + + + + + + + + + + + + + + @@ -329,74 +401,14 @@ export function ShopInfoRbacComponent({ form, bodyshop }) { - - - - - - - - - - - - - - - @@ -412,18 +424,6 @@ export function ShopInfoRbacComponent({ form, bodyshop }) { > - - - + + + + + + - - - - - - @@ -556,18 +556,6 @@ export function ShopInfoRbacComponent({ form, bodyshop }) { > - - - + + + + + + + + + + + + + + + - - - - - - - - - {Simple_Inventory.treatment === "on" && ( <> ({ + insertAuditTrail: ({ jobid, operation }) => + dispatch(insertAuditTrail({ jobid, operation })), +}); + +export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail, }) { const { t } = useTranslation(); const [form] = Form.useForm(); const client = useApolloClient(); @@ -110,6 +118,10 @@ export function JobsCloseComponent({ job, bodyshop, jobRO }) { notification["success"]({ message: t("jobs.successes.closed"), }); + insertAuditTrail({ + jobid: job.id, + operation: AuditTrailMapping.jobinvoiced(), + }); // history.push(`/manage/jobs/${job.id}`); } else { setLoading(false); @@ -527,4 +539,4 @@ export function JobsCloseComponent({ job, bodyshop, jobRO }) {
); } -export default connect(mapStateToProps, null)(JobsCloseComponent); +export default connect(mapStateToProps, mapDispatchToProps)(JobsCloseComponent); diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index bbac4cc7b..b96b555db 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -103,6 +103,7 @@ "admin_jobmarkforreexport": "ADMIN: Job marked for re-export.", "admin_jobuninvoice": "ADMIN: Job has been uninvoiced.", "admin_jobunvoid": "ADMIN: Job has been unvoided.", + "alerttoggle": "Alert Toggle set to {{status}}", "appointmentcancel": "Appointment canceled. Lost Reason: {{lost_sale_reason}}.", "appointmentinsert": "Appointment created. Appointment Date: {{start}}.", "billposted": "Bill with invoice number {{invoice_number}} posted.", @@ -111,6 +112,7 @@ "jobassignmentchange": "Employee {{name}} assigned to {{operation}}", "jobassignmentremoved": "Employee assignment removed for {{operation}}", "jobchecklist": "Checklist type \"{{type}}\" completed. In production set to {{inproduction}}. Status set to {{status}}.", + "jobinvoiced": "Job has been invoiced.", "jobconverted": "Job converted and assigned number {{ro_number}}.", "jobfieldchanged": "Job field $t(jobs.fields.{{field}}) changed to {{value}}.", "jobimported": "Job imported.", @@ -203,6 +205,7 @@ "entered_total": "Total of Entered Lines", "enteringcreditmemo": "You are entering a credit memo. Please ensure you are also entering positive values.", "federal_tax": "Federal Tax", + "federal_tax_exempt": "Federal Tax Exempt?", "generatepartslabel": "Generate Parts Labels after Saving?", "iouexists": "An IOU exists that is associated to this RO.", "local_tax": "Local Tax", @@ -447,6 +450,7 @@ "config": "Shop -> Config", "dashboard": "Shop -> Dashboard", "rbac": "Shop -> RBAC", + "reportcenter": "Shop -> Report Center", "templates": "Shop -> Templates", "vendors": "Shop -> Vendors" }, @@ -2044,6 +2048,9 @@ "sentby": "Sent by {{by}} at {{time}}", "typeamessage": "Send a message...", "unarchive": "Unarchive" + }, + "render": { + "conversation_list": "Conversation List" } }, "notes": { @@ -2695,6 +2702,7 @@ "efficiencyoverperiod": "Efficiency over Selected Dates", "entries": "Scoreboard Entries", "jobs": "Jobs", + "jobscompletednotinvoiced": "Completed Not Invoiced", "lastmonth": "Last Month", "lastweek": "Last Week", "monthlytarget": "Monthly", @@ -2709,6 +2717,7 @@ "timetickets": "Time Tickets", "timeticketsemployee": "Time Tickets by Employee", "todateactual": "Actual (MTD)", + "totalhrs": "Total Hours", "totaloverperiod": "Total over Selected Dates", "weeklyactual": "Actual (W)", "weeklytarget": "Weekly", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index 33cf621da..f68cf4a41 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -103,6 +103,7 @@ "admin_jobmarkforreexport": "", "admin_jobuninvoice": "", "admin_jobunvoid": "", + "alerttoggle": "", "appointmentcancel": "", "appointmentinsert": "", "billposted": "", @@ -111,6 +112,7 @@ "jobassignmentchange": "", "jobassignmentremoved": "", "jobchecklist": "", + "jobinvoiced": "", "jobconverted": "", "jobfieldchanged": "", "jobimported": "", @@ -203,6 +205,7 @@ "entered_total": "", "enteringcreditmemo": "", "federal_tax": "", + "federal_tax_exempt": "", "generatepartslabel": "", "iouexists": "", "local_tax": "", @@ -447,6 +450,7 @@ "config": "", "dashboard": "", "rbac": "", + "reportcenter": "", "templates": "", "vendors": "" }, @@ -2044,6 +2048,9 @@ "sentby": "", "typeamessage": "Enviar un mensaje...", "unarchive": "" + }, + "render": { + "conversation_list": "" } }, "notes": { @@ -2695,6 +2702,7 @@ "efficiencyoverperiod": "", "entries": "", "jobs": "", + "jobscompletednotinvoiced": "", "lastmonth": "", "lastweek": "", "monthlytarget": "", @@ -2709,6 +2717,7 @@ "timetickets": "", "timeticketsemployee": "", "todateactual": "", + "totalhrs": "", "totaloverperiod": "", "weeklyactual": "", "weeklytarget": "", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index 3b5251fa7..d0e44b704 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -103,6 +103,7 @@ "admin_jobmarkforreexport": "", "admin_jobuninvoice": "", "admin_jobunvoid": "", + "alerttoggle": "", "appointmentcancel": "", "appointmentinsert": "", "billposted": "", @@ -111,6 +112,7 @@ "jobassignmentchange": "", "jobassignmentremoved": "", "jobchecklist": "", + "jobinvoiced": "", "jobconverted": "", "jobfieldchanged": "", "jobimported": "", @@ -203,6 +205,7 @@ "entered_total": "", "enteringcreditmemo": "", "federal_tax": "", + "federal_tax_exempt": "", "generatepartslabel": "", "iouexists": "", "local_tax": "", @@ -447,6 +450,7 @@ "config": "", "dashboard": "", "rbac": "", + "reportcenter": "", "templates": "", "vendors": "" }, @@ -2044,6 +2048,9 @@ "sentby": "", "typeamessage": "Envoyer un message...", "unarchive": "" + }, + "render": { + "conversation_list": "" } }, "notes": { @@ -2695,6 +2702,7 @@ "efficiencyoverperiod": "", "entries": "", "jobs": "", + "jobscompletednotinvoiced": "", "lastmonth": "", "lastweek": "", "monthlytarget": "", @@ -2709,6 +2717,7 @@ "timetickets": "", "timeticketsemployee": "", "todateactual": "", + "totalhrs": "", "totaloverperiod": "", "weeklyactual": "", "weeklytarget": "", diff --git a/client/src/utils/AuditTrailMappings.js b/client/src/utils/AuditTrailMappings.js index afa0de1e5..d7098fa2d 100644 --- a/client/src/utils/AuditTrailMappings.js +++ b/client/src/utils/AuditTrailMappings.js @@ -1,6 +1,7 @@ import i18n from "i18next"; const AuditTrailMapping = { + alertToggle: (status) => i18n.t("audit_trail.messages.alerttoggle", { status }), appointmentcancel: (lost_sale_reason) => i18n.t("audit_trail.messages.appointmentcancel", { lost_sale_reason }), appointmentinsert: (start) => @@ -11,6 +12,8 @@ const AuditTrailMapping = { "ADMIN: " + i18n.t("audit_trail.messages.jobstatuschange", { status }), jobsupplement: () => i18n.t("audit_trail.messages.jobsupplement"), jobimported: () => i18n.t("audit_trail.messages.jobimported"), + jobinvoiced: () => + i18n.t("audit_trail.messages.jobinvoiced"), jobconverted: (ro_number) => i18n.t("audit_trail.messages.jobconverted", { ro_number }), jobfieldchange: (field, value) => diff --git a/client/src/utils/TemplateConstants.js b/client/src/utils/TemplateConstants.js index c5e10d712..365b2431e 100644 --- a/client/src/utils/TemplateConstants.js +++ b/client/src/utils/TemplateConstants.js @@ -2102,6 +2102,17 @@ export const TemplateList = (type, context) => { // }, } : {}), + ...(!type || type === "messaging" + ? { + conversation_list: { + title: i18n.t("messaging.render.conversation_list"), + description: "", + subject: i18n.t("messaging.render.conversation_list"), + key: "conversation_list", + disabled: false, + }, + } + : {}), ...(!type || type === "vendor" ? { purchases_by_vendor_detailed: { diff --git a/server/firebase/firebase-handler.js b/server/firebase/firebase-handler.js index 7221649ec..e203bcbf3 100644 --- a/server/firebase/firebase-handler.js +++ b/server/firebase/firebase-handler.js @@ -50,7 +50,7 @@ exports.createUser = async (req, res) => { `, { user: { - email, + email: email.toLowerCase(), authid: userRecord.uid, associations: { data: [{ shopid, authlevel, active: true }],