diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index abc4f8118..2530057aa 100644 --- a/bodyshop_translations.babel +++ b/bodyshop_translations.babel @@ -11040,6 +11040,27 @@ + + uploading + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + usage false @@ -11087,6 +11108,27 @@ + + edituploaded + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + insert false @@ -11907,6 +11949,37 @@ + + exportlogs + + + fields + + + createdat + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + + + general @@ -12377,6 +12450,32 @@ + + errors + + + notfound + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + itemtypes @@ -29164,6 +29263,48 @@ + + purchases_by_ro_detail + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + purchases_by_ro_summary + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + qc_sheet false @@ -29185,6 +29326,27 @@ + + ro_totals + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + ro_with_description false @@ -30675,6 +30837,27 @@ + + exportlogs + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + jobs false @@ -30766,6 +30949,27 @@ templates + + attendance + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + attendance_employee false @@ -30787,6 +30991,27 @@ + + attendance_summary + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + credits_not_received_date false @@ -30850,6 +31075,90 @@ + + export_payables + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + export_payments + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + export_receivables + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + gsr_by_delivery_date + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + gsr_by_estimator false @@ -30871,6 +31180,27 @@ + + gsr_by_exported_date + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + gsr_by_make false @@ -30892,6 +31222,69 @@ + + gsr_by_referral + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + gsr_by_ro + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + gsr_by_source + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + gsr_labor_only false @@ -31186,6 +31579,90 @@ + + open_orders_estimator + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + open_orders_source + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + parts_backorder + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + payments_by_date + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + payments_by_date_type false @@ -31417,6 +31894,27 @@ + + thank_you_date + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + timetickets false @@ -31480,6 +31978,27 @@ + + unclaimed_hrs + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + void_ros false @@ -31501,6 +32020,48 @@ + + work_in_progress_labour + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + work_in_progress_payables + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + diff --git a/client/package.json b/client/package.json index 57f1b5afd..d7d61881b 100644 --- a/client/package.json +++ b/client/package.json @@ -29,6 +29,7 @@ "jsreport-browser-client-dist": "^1.3.0", "libphonenumber-js": "^1.9.17", "logrocket": "^1.2.0", + "markerjs2": "^2.8.1", "moment-business-days": "^1.2.0", "phone": "^2.4.21", "preval.macro": "^5.0.0", diff --git a/client/src/App/App.jsx b/client/src/App/App.jsx index 89d96e27c..fbaedfd0a 100644 --- a/client/src/App/App.jsx +++ b/client/src/App/App.jsx @@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { Route, Switch } from "react-router-dom"; import { createStructuredSelector } from "reselect"; +import DocumentEditorContainer from "../components/document-editor/document-editor.container"; import ErrorBoundary from "../components/error-boundary/error-boundary.component"; //Component Imports import LoadingSpinner from "../components/loading-spinner/loading-spinner.component"; @@ -39,8 +40,12 @@ const mapDispatchToProps = (dispatch) => ({ export function App({ checkUserSession, currentUser, online, setOnline }) { useEffect(() => { + if (!navigator.onLine) { + setOnline(false); + } + checkUserSession(); - }, [checkUserSession]); + }, [checkUserSession, setOnline]); //const b = Grid.useBreakpoint(); // console.log("Breakpoints:", b); @@ -118,6 +123,13 @@ export function App({ checkUserSession, currentUser, online, setOnline }) { component={TechPageContainer} /> + + + ); diff --git a/client/src/components/accounting-payments-table/accounting-payments-table.component.jsx b/client/src/components/accounting-payments-table/accounting-payments-table.component.jsx index 35308356e..f2a72dd92 100644 --- a/client/src/components/accounting-payments-table/accounting-payments-table.component.jsx +++ b/client/src/components/accounting-payments-table/accounting-payments-table.component.jsx @@ -5,7 +5,7 @@ import { Link } from "react-router-dom"; import { logImEXEvent } from "../../firebase/firebase.utils"; import CurrencyFormatter from "../../utils/CurrencyFormatter"; import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter"; -import { alphaSort } from "../../utils/sorters"; +import { alphaSort, dateSort } from "../../utils/sorters"; import PaymentExportButton from "../payment-export-button/payment-export-button.component"; import PaymentsExportAllButton from "../payments-export-all-button/payments-export-all-button.component"; @@ -41,19 +41,12 @@ export default function AccountingPayablesTableComponent({ title: t("payments.fields.date"), dataIndex: "date", key: "date", - sorter: (a, b) => alphaSort(a.date, b.date), + sorter: (a, b) => dateSort(a.date, b.date), sortOrder: state.sortedInfo.columnKey === "date" && state.sortedInfo.order, render: (text, record) => {record.date}, }, - { - title: t("payments.fields.date"), - dataIndex: "date", - key: "date", - sorter: (a, b) => alphaSort(a.date, b.date), - sortOrder: - state.sortedInfo.columnKey === "date" && state.sortedInfo.order, - }, + { title: t("jobs.fields.owner"), dataIndex: "owner", diff --git a/client/src/components/document-editor/document-editor.component.jsx b/client/src/components/document-editor/document-editor.component.jsx new file mode 100644 index 000000000..cb12c1aec --- /dev/null +++ b/client/src/components/document-editor/document-editor.component.jsx @@ -0,0 +1,113 @@ +//import "tui-image-editor/dist/tui-image-editor.css"; +import { Result } from "antd"; +import * as markerjs2 from "markerjs2"; +import React, { useCallback, useEffect, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { + selectBodyshop, + selectCurrentUser, +} from "../../redux/user/user.selectors"; +import { handleUpload } from "../documents-upload/documents-upload.utility"; +import { GenerateSrcUrl } from "../jobs-documents-gallery/job-documents.utility"; +import LoadingSpinner from "../loading-spinner/loading-spinner.component"; + +const mapStateToProps = createStructuredSelector({ + currentUser: selectCurrentUser, + bodyshop: selectBodyshop, +}); +const mapDispatchToProps = (dispatch) => ({ + //setUserLanguage: language => dispatch(setUserLanguage(language)) +}); + +export function DocumentEditorComponent({ currentUser, bodyshop, document }) { + const imgRef = useRef(null); + const [loading, setLoading] = useState(false); + const [uploaded, setuploaded] = useState(false); + const markerArea = useRef(null); + const { t } = useTranslation(); + + const triggerUpload = useCallback( + async (dataUrl) => { + setLoading(true); + handleUpload( + { + filename: `${document.key.split("/").pop()}-${Date.now()}.jpg`, + file: await b64toBlob(dataUrl), + onSuccess: () => { + setLoading(false); + setuploaded(true); + }, + onError: () => setLoading(false), + }, + { + bodyshop: bodyshop, + uploaded_by: currentUser.email, + jobId: document.jobid, + //billId: billId, + tagsArray: ["edited"], + //callback: callbackAfterUpload, + } + ); + }, + [bodyshop, currentUser, document] + ); + + useEffect(() => { + if (imgRef.current !== null) { + // create a marker.js MarkerArea + markerArea.current = new markerjs2.MarkerArea(imgRef.current); + console.log(`markerArea.current`, markerArea.current); + // attach an event handler to assign annotated image back to our image element + markerArea.current.addCloseEventListener((closeEvent) => { + console.log("Close Event", closeEvent); + }); + + markerArea.current.addRenderEventListener((dataUrl) => { + imgRef.current.src = dataUrl; + markerArea.current.close(); + triggerUpload(dataUrl); + }); + // launch marker.js + + markerArea.current.renderAtNaturalSize = true; + markerArea.current.renderImageType = "image/jpeg"; + markerArea.current.renderImageQuality = 1; + //markerArea.current.settings.displayMode = "inline"; + markerArea.current.show(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [triggerUpload]); + + async function b64toBlob(url) { + const res = await fetch(url); + return await res.blob(); + } + + return ( +
+ {!loading && !uploaded && ( + sample + )} + {loading && } + {uploaded && ( + + )} +
+ ); +} + +export default connect( + mapStateToProps, + mapDispatchToProps +)(DocumentEditorComponent); diff --git a/client/src/components/document-editor/document-editor.container.jsx b/client/src/components/document-editor/document-editor.container.jsx new file mode 100644 index 000000000..3819f27f8 --- /dev/null +++ b/client/src/components/document-editor/document-editor.container.jsx @@ -0,0 +1,59 @@ +import { useQuery } from "@apollo/client"; +import { Result } from "antd"; +import queryString from "query-string"; +import React, { useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { useLocation } from "react-router"; +import { QUERY_BODYSHOP } from "../../graphql/bodyshop.queries"; +import { GET_DOCUMENT_BY_PK } from "../../graphql/documents.queries"; +import { setBodyshop } from "../../redux/user/user.actions"; +import AlertComponent from "../alert/alert.component"; +import LoadingSpinner from "../loading-spinner/loading-spinner.component"; +import DocumentEditor from "./document-editor.component"; + +const mapDispatchToProps = (dispatch) => ({ + setBodyshop: (bs) => dispatch(setBodyshop(bs)), +}); + +export default connect(null, mapDispatchToProps)(DocumentEditorContainer); + +export function DocumentEditorContainer({ setBodyshop }) { + //Get the image details for the image to be saved. + //Get the document id from the search string. + const { documentId } = queryString.parse(useLocation().search); + const { t } = useTranslation(); + const { + loading: loadingShop, + error: errorShop, + data: dataShop, + } = useQuery(QUERY_BODYSHOP, { + fetchPolicy: "network-only", + }); + + useEffect(() => { + if (dataShop) setBodyshop(dataShop.bodyshops[0]); + }, [dataShop, setBodyshop]); + + const { loading, error, data } = useQuery(GET_DOCUMENT_BY_PK, { + variables: { documentId }, + skip: !documentId, + }); + + if (loading || loadingShop) return ; + if (error || errorShop) + return ( + + ); + + if (!data || !data.documents_by_pk) + return ; + return ( +
+ +
+ ); +} diff --git a/client/src/components/documents-upload/documents-upload.utility.js b/client/src/components/documents-upload/documents-upload.utility.js index 5856d985e..72893ef88 100644 --- a/client/src/components/documents-upload/documents-upload.utility.js +++ b/client/src/components/documents-upload/documents-upload.utility.js @@ -21,8 +21,10 @@ export const handleUpload = (ev, context) => { const { onError, onSuccess, onProgress } = ev; const { bodyshop, jobId } = context; - let key = `${bodyshop.id}/${jobId}/${ev.file.name.replace(/\.[^/.]+$/, "")}`; - let extension = ev.file.name.split(".").pop(); + const fileName = ev.file.name || ev.filename; + + let key = `${bodyshop.id}/${jobId}/${fileName.replace(/\.[^/.]+$/, "")}`; + let extension = fileName.split(".").pop(); uploadToCloudinary( key, extension, diff --git a/client/src/components/job-detail-lines/job-lines.component.jsx b/client/src/components/job-detail-lines/job-lines.component.jsx index cd88da672..67e8381c9 100644 --- a/client/src/components/job-detail-lines/job-lines.component.jsx +++ b/client/src/components/job-detail-lines/job-lines.component.jsx @@ -22,6 +22,7 @@ import { createStructuredSelector } from "reselect"; import { DELETE_JOB_LINE_BY_PK } from "../../graphql/jobs-lines.queries"; import { selectJobReadOnly } from "../../redux/application/application.selectors"; import { setModalContext } from "../../redux/modals/modals.actions"; +import { selectTechnician } from "../../redux/tech/tech.selectors"; import { onlyUnique } from "../../utils/arrayHelper"; import CurrencyFormatter from "../../utils/CurrencyFormatter"; import { alphaSort } from "../../utils/sorters"; @@ -37,6 +38,7 @@ import PartsOrderModalContainer from "../parts-order-modal/parts-order-modal.con const mapStateToProps = createStructuredSelector({ //currentUser: selectCurrentUser jobRO: selectJobReadOnly, + technician: selectTechnician, }); const mapDispatchToProps = (dispatch) => ({ @@ -48,6 +50,7 @@ const mapDispatchToProps = (dispatch) => ({ export function JobLinesComponent({ jobRO, + technician, setPartsOrderContext, loading, refetch, @@ -364,7 +367,8 @@ export function JobLinesComponent({ disabled={ (job && !job.converted) || (selectedLines.length > 0 ? false : true) || - jobRO + jobRO || + technician } onClick={() => { setPartsOrderContext({ @@ -399,7 +403,7 @@ export function JobLinesComponent({ { + console.log(`Clicked`); + const newWindow = window.open( + `${window.location.protocol}//${window.location.host}/edit?documentId=${galleryImages.images[index].id}`, + "_blank", + "noopener,noreferrer" + ); + if (newWindow) newWindow.opener = null; + }} + > + + , + ]} onClickImage={(props) => { window.open( props.target.src, diff --git a/client/src/graphql/documents.queries.js b/client/src/graphql/documents.queries.js index d0c11f914..f9cc8877b 100644 --- a/client/src/graphql/documents.queries.js +++ b/client/src/graphql/documents.queries.js @@ -1,5 +1,20 @@ import { gql } from "@apollo/client"; +export const GET_DOCUMENT_BY_PK = gql` + query GET_DOCUMENT_BY_PK($documentId: uuid!) { + documents_by_pk(id: $documentId) { + id + name + key + type + size + takenat + extension + jobid + } + } +`; + export const GET_DOCUMENTS_BY_JOB = gql` query GET_DOCUMENTS_BY_JOB($jobId: uuid!) { jobs_by_pk(id: $jobId) { diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index 164bdd128..1d04f21d0 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -6,7 +6,7 @@ }, "errors": { "deleting": "Error encountered while deleting allocation. {{message}}", - "saving": "Error while allocating. {{message}}", + "saving": "Error while allocating. {{message}}", "validation": "Please ensure all fields are entered correctly. " }, "fields": { @@ -701,10 +701,12 @@ "upload": "Upload", "upload_limitexceeded": "Uploading all selected documents will exceed the job storage limit for your shop. ", "upload_limitexceeded_title": "Unable to upload document(s)", + "uploading": "Uploading...", "usage": "of job storage used. ({{used}} / {{total}})" }, "successes": { "delete": "Document(s) deleted successfully.", + "edituploaded": "Edited document uploaded successfully. Please close this window and refresh the documents list.", "insert": "Uploaded document successfully. ", "updated": "Document updated successfully. " } @@ -767,6 +769,11 @@ "unique_employee_number": "You must enter a unique employee number." } }, + "exportlogs": { + "fields": { + "createdat": "Created At" + } + }, "general": { "actions": { "add": "Add", @@ -792,6 +799,9 @@ "submitticket": "Submit a Support Ticket", "view": "View" }, + "errors": { + "notfound": "No record was found." + }, "itemtypes": { "contract": "CC Contract", "courtesycar": "Courtesy Car", @@ -1742,7 +1752,10 @@ "payment_receipt": "Payment Receipt", "payment_request": "Payment Request", "payments_by_job": "Job Payments", + "purchases_by_ro_detail": "Purchases - Detail", + "purchases_by_ro_summary": "Purchases - Summary", "qc_sheet": "Quality Control Sheet", + "ro_totals": "RO Totals", "ro_with_description": "RO Summary with Descriptions", "supplement_request": "Supplement Request", "thank_you_ro": "Thank You Letter", @@ -1843,6 +1856,7 @@ "objects": { "appointments": "Appointments", "bills": "Bills", + "exportlogs": "Export Logs", "jobs": "Jobs", "payments": "Payments", "timetickets": "Timetickets" @@ -1850,12 +1864,22 @@ "vendor": "Vendor" }, "templates": { + "attendance": "Attendance (All Employees)", "attendance_employee": "Employee Attendance", + "attendance_summary": "Attendance Summary (All Employees)", "credits_not_received_date": "Credits not Received by Date", "estimator_detail": "Jobs by Estimator (Detail)", "estimator_summary": "Jobs by Estimator (Summary)", + "export_payables": "Export Log - Payables", + "export_payments": "Export Log - Payments", + "export_receivables": "Export Log - Receivables", + "gsr_by_delivery_date": "Gross Sales by Delivery Date", "gsr_by_estimator": "Gross Sales by Estimator", + "gsr_by_exported_date": "Gross Sales by Export Date", "gsr_by_make": "Gross Sales by Vehicle Make", + "gsr_by_referral": "Gross Sales by Referral Source", + "gsr_by_ro": "Gross Sales by RO", + "gsr_by_source": "Gross Sales by Insurance Company'", "gsr_labor_only": "Gross Sales - Labor Only", "hours_sold_detail_closed": "Hours Sold Detail - Closed", "hours_sold_detail_closed_source": "Hours Sold Detail - Closed by Source", @@ -1870,6 +1894,10 @@ "job_costing_ro_estimator": "Job Costing by Estimator", "job_costing_ro_source": "Job Costing by RO Source", "open_orders": "Open Orders by Date", + "open_orders_estimator": "Open Orders by Estimator", + "open_orders_source": "Open Orders by Insurance Company", + "parts_backorder": "Backordered Parts", + "payments_by_date": "Payments by Date", "payments_by_date_type": "Payments by Date Range", "purchases_by_cost_center_detail": "Purchases by Cost Center (Detail)", "purchases_by_cost_center_summary": "Purchases by Cost Center (Summary)", @@ -1881,10 +1909,14 @@ "purchases_grouped_by_vendor_summary": "Purchases Grouped by Vendor - Summary", "schedule": "Appointment Schedule", "supplement_ratio_source": "Supplement Ratio by Source", + "thank_you_date": "Thank You Letters", "timetickets": "Time Tickets", "timetickets_employee": "Employee Time Tickets", "timetickets_summary": "Time Tickets Summary", - "void_ros": "Void ROs" + "unclaimed_hrs": "Unclaimed Hours", + "void_ros": "Void ROs", + "work_in_progress_labour": "Work in Progress - Labor", + "work_in_progress_payables": "Work in Progress - Payables" } }, "scoreboard": { diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index 4fae2a134..e38774c9a 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -701,10 +701,12 @@ "upload": "Subir", "upload_limitexceeded": "", "upload_limitexceeded_title": "", + "uploading": "", "usage": "" }, "successes": { "delete": "Documento eliminado con éxito.", + "edituploaded": "", "insert": "Documento cargado con éxito.", "updated": "" } @@ -767,6 +769,11 @@ "unique_employee_number": "" } }, + "exportlogs": { + "fields": { + "createdat": "" + } + }, "general": { "actions": { "add": "", @@ -792,6 +799,9 @@ "submitticket": "", "view": "" }, + "errors": { + "notfound": "" + }, "itemtypes": { "contract": "", "courtesycar": "", @@ -1742,7 +1752,10 @@ "payment_receipt": "", "payment_request": "", "payments_by_job": "", + "purchases_by_ro_detail": "", + "purchases_by_ro_summary": "", "qc_sheet": "", + "ro_totals": "", "ro_with_description": "", "supplement_request": "", "thank_you_ro": "", @@ -1843,6 +1856,7 @@ "objects": { "appointments": "", "bills": "", + "exportlogs": "", "jobs": "", "payments": "", "timetickets": "" @@ -1850,12 +1864,22 @@ "vendor": "" }, "templates": { + "attendance": "", "attendance_employee": "", + "attendance_summary": "", "credits_not_received_date": "", "estimator_detail": "", "estimator_summary": "", + "export_payables": "", + "export_payments": "", + "export_receivables": "", + "gsr_by_delivery_date": "", "gsr_by_estimator": "", + "gsr_by_exported_date": "", "gsr_by_make": "", + "gsr_by_referral": "", + "gsr_by_ro": "", + "gsr_by_source": "", "gsr_labor_only": "", "hours_sold_detail_closed": "", "hours_sold_detail_closed_source": "", @@ -1870,6 +1894,10 @@ "job_costing_ro_estimator": "", "job_costing_ro_source": "", "open_orders": "", + "open_orders_estimator": "", + "open_orders_source": "", + "parts_backorder": "", + "payments_by_date": "", "payments_by_date_type": "", "purchases_by_cost_center_detail": "", "purchases_by_cost_center_summary": "", @@ -1881,10 +1909,14 @@ "purchases_grouped_by_vendor_summary": "", "schedule": "", "supplement_ratio_source": "", + "thank_you_date": "", "timetickets": "", "timetickets_employee": "", "timetickets_summary": "", - "void_ros": "" + "unclaimed_hrs": "", + "void_ros": "", + "work_in_progress_labour": "", + "work_in_progress_payables": "" } }, "scoreboard": { diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index 63fde29b6..22362a95d 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -701,10 +701,12 @@ "upload": "Télécharger", "upload_limitexceeded": "", "upload_limitexceeded_title": "", + "uploading": "", "usage": "" }, "successes": { "delete": "Le document a bien été supprimé.", + "edituploaded": "", "insert": "Document téléchargé avec succès.", "updated": "" } @@ -767,6 +769,11 @@ "unique_employee_number": "" } }, + "exportlogs": { + "fields": { + "createdat": "" + } + }, "general": { "actions": { "add": "", @@ -792,6 +799,9 @@ "submitticket": "", "view": "" }, + "errors": { + "notfound": "" + }, "itemtypes": { "contract": "", "courtesycar": "", @@ -1742,7 +1752,10 @@ "payment_receipt": "", "payment_request": "", "payments_by_job": "", + "purchases_by_ro_detail": "", + "purchases_by_ro_summary": "", "qc_sheet": "", + "ro_totals": "", "ro_with_description": "", "supplement_request": "", "thank_you_ro": "", @@ -1843,6 +1856,7 @@ "objects": { "appointments": "", "bills": "", + "exportlogs": "", "jobs": "", "payments": "", "timetickets": "" @@ -1850,12 +1864,22 @@ "vendor": "" }, "templates": { + "attendance": "", "attendance_employee": "", + "attendance_summary": "", "credits_not_received_date": "", "estimator_detail": "", "estimator_summary": "", + "export_payables": "", + "export_payments": "", + "export_receivables": "", + "gsr_by_delivery_date": "", "gsr_by_estimator": "", + "gsr_by_exported_date": "", "gsr_by_make": "", + "gsr_by_referral": "", + "gsr_by_ro": "", + "gsr_by_source": "", "gsr_labor_only": "", "hours_sold_detail_closed": "", "hours_sold_detail_closed_source": "", @@ -1870,6 +1894,10 @@ "job_costing_ro_estimator": "", "job_costing_ro_source": "", "open_orders": "", + "open_orders_estimator": "", + "open_orders_source": "", + "parts_backorder": "", + "payments_by_date": "", "payments_by_date_type": "", "purchases_by_cost_center_detail": "", "purchases_by_cost_center_summary": "", @@ -1881,10 +1909,14 @@ "purchases_grouped_by_vendor_summary": "", "schedule": "", "supplement_ratio_source": "", + "thank_you_date": "", "timetickets": "", "timetickets_employee": "", "timetickets_summary": "", - "void_ros": "" + "unclaimed_hrs": "", + "void_ros": "", + "work_in_progress_labour": "", + "work_in_progress_payables": "" } }, "scoreboard": { diff --git a/client/src/utils/TemplateConstants.js b/client/src/utils/TemplateConstants.js index 0b063e6e1..403ea7d0d 100644 --- a/client/src/utils/TemplateConstants.js +++ b/client/src/utils/TemplateConstants.js @@ -230,6 +230,14 @@ export const TemplateList = (type, context) => { disabled: false, group: "financial", }, + ro_totals: { + title: i18n.t("printcenter.jobs.ro_totals"), + description: "All Jobs Notes", + subject: i18n.t("printcenter.jobs.ro_totals"), + key: "ro_totals", + disabled: false, + group: "financial", + }, job_costing_ro: { title: i18n.t("printcenter.jobs.job_costing_ro"), description: "All Jobs Notes", @@ -238,6 +246,22 @@ export const TemplateList = (type, context) => { disabled: false, group: "financial", }, + purchases_by_ro_detail: { + title: i18n.t("printcenter.jobs.purchases_by_ro_detail"), + description: "All Jobs Notes", + subject: i18n.t("printcenter.jobs.purchases_by_ro_detail"), + key: "purchases_by_ro_detail", + disabled: false, + group: "financial", + }, + purchases_by_ro_summary: { + title: i18n.t("printcenter.jobs.purchases_by_ro_summary"), + description: "All Jobs Notes", + subject: i18n.t("printcenter.jobs.purchases_by_ro_summary"), + key: "purchases_by_ro_summary", + disabled: false, + group: "financial", + }, filing_coversheet_portrait: { title: i18n.t("printcenter.jobs.filing_coversheet_portrait"), description: "All Jobs Notes", @@ -361,6 +385,16 @@ export const TemplateList = (type, context) => { ...(!type || type === "csi" ? {} : {}), ...(!type || type === "report_center" ? { + payments_by_date: { + title: i18n.t("reportcenter.templates.payments_by_date"), + subject: i18n.t("reportcenter.templates.payments_by_date"), + key: "payments_by_date", + disabled: false, + rangeFilter: { + object: i18n.t("reportcenter.labels.objects.payments"), + field: i18n.t("payments.fields.date"), + }, + }, payments_by_date_type: { title: i18n.t("reportcenter.templates.payments_by_date_type"), subject: i18n.t("reportcenter.templates.payments_by_date_type"), @@ -521,7 +555,28 @@ export const TemplateList = (type, context) => { idtype: "employee", disabled: false, }, + attendance: { + title: i18n.t("reportcenter.templates.attendance"), + subject: i18n.t("reportcenter.templates.attendance"), + key: "attendance", + disabled: false, + rangeFilter: { + object: i18n.t("reportcenter.labels.objects.timetickets"), + field: i18n.t("timetickets.fields.date"), + }, + }, + attendance_summary: { + title: i18n.t("reportcenter.templates.attendance_summary"), + subject: i18n.t("reportcenter.templates.attendance_summary"), + key: "attendance_summary", + + disabled: false, + rangeFilter: { + object: i18n.t("reportcenter.labels.objects.timetickets"), + field: i18n.t("timetickets.fields.date"), + }, + }, attendance_employee: { title: i18n.t("reportcenter.templates.attendance_employee"), subject: i18n.t("reportcenter.templates.attendance_employee"), @@ -789,6 +844,67 @@ export const TemplateList = (type, context) => { field: i18n.t("jobs.fields.date_invoiced"), }, }, + gsr_by_delivery_date: { + title: i18n.t("reportcenter.templates.gsr_by_delivery_date"), + description: "", + subject: i18n.t("reportcenter.templates.gsr_by_delivery_date"), + key: "gsr_by_delivery_date", + //idtype: "vendor", + disabled: false, + rangeFilter: { + object: i18n.t("reportcenter.labels.objects.jobs"), + field: i18n.t("jobs.fields.actual_delivery"), + }, + }, + gsr_by_referral: { + title: i18n.t("reportcenter.templates.gsr_by_referral"), + description: "", + subject: i18n.t("reportcenter.templates.gsr_by_referral"), + key: "gsr_by_referral", + //idtype: "vendor", + disabled: false, + rangeFilter: { + object: i18n.t("reportcenter.labels.objects.jobs"), + field: i18n.t("jobs.fields.date_invoiced"), + }, + }, + gsr_by_ro: { + title: i18n.t("reportcenter.templates.gsr_by_ro"), + description: "", + subject: i18n.t("reportcenter.templates.gsr_by_ro"), + key: "gsr_by_ro", + //idtype: "vendor", + disabled: false, + rangeFilter: { + object: i18n.t("reportcenter.labels.objects.jobs"), + field: i18n.t("jobs.fields.date_invoiced"), + }, + }, + gsr_by_source: { + title: i18n.t("reportcenter.templates.gsr_by_source"), + description: "", + subject: i18n.t("reportcenter.templates.gsr_by_source"), + key: "gsr_by_source", + //idtype: "vendor", + disabled: false, + rangeFilter: { + object: i18n.t("reportcenter.labels.objects.jobs"), + field: i18n.t("jobs.fields.date_invoiced"), + }, + }, + gsr_by_exported_date: { + title: i18n.t("reportcenter.templates.gsr_by_exported_date"), + description: "", + subject: i18n.t("reportcenter.templates.gsr_by_exported_date"), + key: "gsr_by_exported_date", + //idtype: "vendor", + disabled: false, + rangeFilter: { + object: i18n.t("reportcenter.labels.objects.jobs"), + field: i18n.t("jobs.fields.date_exported"), + }, + }, + gsr_by_estimator: { title: i18n.t("reportcenter.templates.gsr_by_estimator"), description: "", @@ -825,6 +941,128 @@ export const TemplateList = (type, context) => { field: i18n.t("jobs.fields.date_open"), }, }, + open_orders_estimator: { + title: i18n.t("reportcenter.templates.open_orders_estimator"), + description: "", + subject: i18n.t("reportcenter.templates.open_orders_estimator"), + key: "open_orders_estimator", + //idtype: "vendor", + disabled: false, + rangeFilter: { + object: i18n.t("reportcenter.labels.objects.jobs"), + field: i18n.t("jobs.fields.date_open"), + }, + }, + open_orders_source: { + title: i18n.t("reportcenter.templates.open_orders_source"), + description: "", + subject: i18n.t("reportcenter.templates.open_orders_source"), + key: "open_orders_source", + //idtype: "vendor", + disabled: false, + rangeFilter: { + object: i18n.t("reportcenter.labels.objects.jobs"), + field: i18n.t("jobs.fields.date_open"), + }, + }, + export_payables: { + title: i18n.t("reportcenter.templates.export_payables"), + description: "", + subject: i18n.t("reportcenter.templates.export_payables"), + key: "export_payables", + //idtype: "vendor", + disabled: false, + rangeFilter: { + object: i18n.t("reportcenter.labels.objects.exportlogs"), + field: i18n.t("exportlogs.fields.createdat"), + }, + export_payments: { + title: i18n.t("reportcenter.templates.export_payments"), + description: "", + subject: i18n.t("reportcenter.templates.export_payments"), + key: "export_payments", + //idtype: "vendor", + disabled: false, + rangeFilter: { + object: i18n.t("reportcenter.labels.objects.exportlogs"), + field: i18n.t("exportlogs.fields.createdat"), + }, + }, + export_receivables: { + title: i18n.t("reportcenter.templates.export_receivables"), + description: "", + subject: i18n.t("reportcenter.templates.export_receivables"), + key: "export_receivables", + //idtype: "vendor", + disabled: false, + rangeFilter: { + object: i18n.t("reportcenter.labels.objects.exportlogs"), + field: i18n.t("exportlogs.fields.createdat"), + }, + }, + parts_backorder: { + title: i18n.t("reportcenter.templates.parts_backorder"), + description: "", + subject: i18n.t("reportcenter.templates.parts_backorder"), + key: "parts_backorder", + //idtype: "vendor", + disabled: false, + rangeFilter: { + object: i18n.t("reportcenter.labels.objects.jobs"), + field: i18n.t("jobs.fields.actual_in"), + }, + }, + thank_you_date: { + title: i18n.t("reportcenter.templates.thank_you_date"), + description: "", + subject: i18n.t("reportcenter.templates.thank_you_date"), + key: "thank_you_date", + //idtype: "vendor", + disabled: false, + rangeFilter: { + object: i18n.t("reportcenter.labels.objects.jobs"), + field: i18n.t("jobs.fields.date_invoiced"), + }, + }, + unclaimed_hrs: { + title: i18n.t("reportcenter.templates.unclaimed_hrs"), + description: "", + subject: i18n.t("reportcenter.templates.unclaimed_hrs"), + key: "unclaimed_hrs", + //idtype: "vendor", + disabled: false, + rangeFilter: { + object: i18n.t("reportcenter.labels.objects.jobs"), + field: i18n.t("jobs.fields.date_open"), + }, + }, + work_in_progress_labour: { + title: i18n.t("reportcenter.templates.work_in_progress_labour"), + description: "", + subject: i18n.t("reportcenter.templates.work_in_progress_labour"), + key: "work_in_progress_labour", + //idtype: "vendor", + disabled: false, + rangeFilter: { + object: i18n.t("reportcenter.labels.objects.jobs"), + field: i18n.t("jobs.fields.date_open"), + }, + }, + work_in_progress_payables: { + title: i18n.t("reportcenter.templates.work_in_progress_payables"), + description: "", + subject: i18n.t( + "reportcenter.templates.work_in_progress_payables" + ), + key: "work_in_progress_payables", + //idtype: "vendor", + disabled: false, + rangeFilter: { + object: i18n.t("reportcenter.labels.objects.jobs"), + field: i18n.t("jobs.fields.date_open"), + }, + }, + }, } : {}), ...(!type || type === "courtesycarcontract" diff --git a/client/yarn.lock b/client/yarn.lock index e6fdbac30..2283b5afb 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -8302,6 +8302,11 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" +markerjs2@^2.8.1: + version "2.8.1" + resolved "https://registry.yarnpkg.com/markerjs2/-/markerjs2-2.8.1.tgz#33c455cc1edd8fa9a5e5b39ed782dcd1b923c917" + integrity sha512-M9AflvjOD5aIcBM0HZWW6u1h/NRdzfq73B9ILv1YehF88PeF0tYT5HIsi9PaSJ6EUOR/vWysZN08f3EyDCJixw== + material-colors@^1.2.1: version "1.2.6" resolved "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz" diff --git a/server/job/job-costing.js b/server/job/job-costing.js index 24d1d6382..3f67bc36a 100644 --- a/server/job/job-costing.js +++ b/server/job/job-costing.js @@ -67,6 +67,12 @@ async function JobCostingMulti(req, res) { gpdollars: Dinero({ amount: 0 }), gppercent: null, gppercentFormatted: null, + totalLaborGp: Dinero({ amount: 0 }), + totalPartsGp: Dinero({ amount: 0 }), + totalLaborGppercent: null, + totalLaborGppercentFormatted: null, + totalPartsGppercent: null, + totalPartsGppercentFormatted: null, }, }; @@ -149,12 +155,39 @@ async function JobCostingMulti(req, res) { multiSummary.summaryData.gpdollars.add( costingData.summaryData.gpdollars ); + + multiSummary.summaryData.totalLaborGp = + multiSummary.summaryData.totalLaborGp.add( + costingData.summaryData.totalLaborGp + ); + multiSummary.summaryData.totalPartsGp = + multiSummary.summaryData.totalPartsGp.add( + costingData.summaryData.totalPartsGp + ); + console.timeEnd(`SummaryOfCostingData-${job.id}`); //Take the summary data & add it to total summary data. }); //For each center, recalculate and toFormat() the values. - multiSummary.summaryData.gpdollars; + + multiSummary.summaryData.totalLaborGppercent = ( + (multiSummary.summaryData.totalLaborGp.getAmount() / + multiSummary.summaryData.totalLaborSales.getAmount()) * + 100 + ).toFixed(2); + multiSummary.summaryData.totalLaborGppercentFormatted = formatGpPercent( + multiSummary.summaryData.totalLaborGppercent + ); + + multiSummary.summaryData.totalPartsGppercent = ( + (multiSummary.summaryData.totalPartsGp.getAmount() / + multiSummary.summaryData.totalPartsSales.getAmount()) * + 100 + ).toFixed(2); + multiSummary.summaryData.totalPartsGppercentFormatted = formatGpPercent( + multiSummary.summaryData.totalPartsGppercent + ); multiSummary.summaryData.gppercent = ( (multiSummary.summaryData.gpdollars.getAmount() / @@ -418,7 +451,14 @@ function GenerateCostingData(job) { totalLaborCost: Dinero({ amount: 0 }), totalPartsCost: Dinero({ amount: 0 }), totalCost: Dinero({ amount: 0 }), + totalLaborGp: Dinero({ amount: 0 }), + totalPartsGp: Dinero({ amount: 0 }), gpdollars: Dinero({ amount: 0 }), + + totalLaborGppercent: null, + totalLaborGppercentFormatted: null, + totalPartsGppercent: null, + totalPartsGppercentFormatted: null, gppercent: null, gppercentFormatted: null, }; @@ -503,6 +543,31 @@ function GenerateCostingData(job) { } //Final summary data massaging. + + summaryData.totalLaborGp = summaryData.totalLaborSales.subtract( + summaryData.totalLaborCost + ); + summaryData.totalLaborGppercent = ( + (summaryData.totalLaborGp.getAmount() / + summaryData.totalLaborSales.getAmount()) * + 100 + ).toFixed(2); + summaryData.totalLaborGppercentFormatted = formatGpPercent( + summaryData.totalLaborGppercent + ); + + summaryData.totalPartsGp = summaryData.totalPartsSales.subtract( + summaryData.totalPartsCost + ); + summaryData.totalPartsGppercent = ( + (summaryData.totalPartsGp.getAmount() / + summaryData.totalPartsSales.getAmount()) * + 100 + ).toFixed(2); + summaryData.totalPartsGppercentFormatted = formatGpPercent( + summaryData.totalPartsGppercent + ); + summaryData.gpdollars = summaryData.totalSales.subtract( summaryData.totalCost ); @@ -510,6 +575,7 @@ function GenerateCostingData(job) { (summaryData.gpdollars.getAmount() / summaryData.totalSales.getAmount()) * 100 ).toFixed(2); + if (isNaN(summaryData.gppercent)) summaryData.gppercentFormatted = 0; else if (!isFinite(summaryData.gppercent)) summaryData.gppercentFormatted = "- ∞";