diff --git a/_reference/SampleMetadata.md b/_reference/SampleMetadata.md new file mode 100644 index 000000000..f675a8b17 --- /dev/null +++ b/_reference/SampleMetadata.md @@ -0,0 +1,24 @@ +csiqueestions: +[{"name": "mailing", "type": "checkbox", "label": "Opt into our mailing list?", "required": false}, {"max": 5, "min": 0, "name": "slider", "type": "slider", "label": "Slide me.", "required": false}, {"name": "feedback", "type": "textarea", "label": "Do you have any general feedback you would like to share?", "required": false}, {"name": "overall", "type": "rate", "label": "How would you rate your overall experience with us?", "required": true}] + +md_ro_statuses +{"statuses": ["Open", "Scheduled", "Arrived", "Repair Plan", "Parts", "Body", "Prep", "Paint", "Reassembly", "Sublet", "Detail", "Completed", "Delivered", "Invoiced", "Exported"], "open_statuses": ["Open", "Scheduled", "Arrived", "Repair Plan", "Parts", "Body", "Prep", "Paint", "Reassembly", "Sublet", "Detail"], "default_arrived": "Arrived", "default_exported": "Exported", "default_imported": "Open", "default_invoiced": "Invoiced", "default_completed": "Completed", "default_delivered": "Delivered", "default_scheduled": "Scheduled", "production_statuses": ["Repair Plan", "Parts", "Body", "Prep", "Paint", "Reassembly", "Sublet", "Detail", "Completed"]} + +md_order_status +{"statuses": ["Ordered", "Received", "Canceled", "Backordered", "Returned"], "default_bo": "Backordered", "default_ordered": "Ordered", "default_canceled": "Canceled", "default_received": "Received", "default_returned": "Returned"} + +responsibilitycenters +{"ap": {"name": "AP", "accountdesc": "Pay to Others", "accountitem": "A/P", "accountname": "AP Acc#", "accountnumber": "Accounts Payable"}, "ar": {"name": "AR", "accountdesc": "1100", "accountitem": "A/R", "accountname": "ACCOUNTS RECEIVABLE", "accountnumber": "1100"}, "costs": [{"name": "Aftermarket", "accountdesc": "Aftermarketd", "accountitem": "Aftermarketi", "accountname": "BODY SHOP COST:PARTS:AFTERMARKET", "accountnumber": "Aftermarket"}, {"name": "ATP", "accountdesc": "ATPd", "accountitem": "BODY SHOP_ATP", "accountname": "BODY SHOP COST:ATP", "accountnumber": "ATP"}, {"name": "Body", "accountdesc": "BODY SHOP COST:LABOR", "accountitem": "BODY SHOP_LAB", "accountname": "BODY SHOP COST:LABOR:BODY", "accountnumber": "5001"}, {"name": "Detail", "accountdesc": "Detaild", "accountitem": "Detaili", "accountname": "BODY SHOP COST:LABOR:DETAIL", "accountnumber": "Detail"}, {"name": "Daignostic", "accountdesc": "Daignosticd", "accountitem": "Daignostici", "accountname": "Daignostic", "accountnumber": "Daignostic"}, {"name": "Electrical", "accountdesc": "Electricald", "accountitem": "Electricali", "accountname": "Electrical", "accountnumber": "Electrical"}, {"name": "Chrome", "accountdesc": "Chromed", "accountitem": "Chromei", "accountname": "Chrome", "accountnumber": "Chrome"}, {"name": "Frame", "accountdesc": "Framed", "accountitem": "Framei", "accountname": "BODY SHOP COST:LABOR:Frame", "accountnumber": "Frame"}, {"name": "Mechanical", "accountdesc": "Mechanicald", "accountitem": "Mechanicali", "accountname": "BODY SHOP COST:LABOR:MECHANICAL", "accountnumber": "Mechanical"}, {"name": "Refinish", "accountdesc": "Refinishd", "accountitem": "BODY SHOP_LAR", "accountname": "BODY SHOP COST:LABOR:REFINISH", "accountnumber": "5003"}, {"name": "Structural", "accountdesc": "Structurald", "accountitem": "Structurali", "accountname": "Structural", "accountnumber": "Structural"}, {"name": "Existing", "accountdesc": "Existingd", "accountitem": "Existingi", "accountname": "Existing", "accountnumber": "Existing"}, {"name": "Glass", "accountdesc": "Glassd", "accountitem": "Glassi", "accountname": "BODY SHOP COST:PARTS:Glass", "accountnumber": "Glass"}, {"name": "LKQ", "accountdesc": "LKQd", "accountitem": "LKQi", "accountname": "BODY SHOP COST:PARTS:LKQ", "accountnumber": "LKQ"}, {"name": "OEM", "accountdesc": "OEMd", "accountitem": "OEMi", "accountname": "BODY SHOP COST:PARTS:OEM", "accountnumber": "OEM"}, {"name": "OEM Partial", "accountdesc": "Partiald", "accountitem": "Partial", "accountname": "BODY SHOP COST:PARTS:OEM Partial", "accountnumber": "OEM Partial"}, {"name": "Re-cored", "accountdesc": "coredd", "accountitem": "coredi", "accountname": "Re-cored", "accountnumber": "Re-cored"}, {"name": "Remanufactured", "accountdesc": "Remanufacturedd", "accountitem": "Remanufacturedi", "accountname": "Remanufactured", "accountnumber": "Remanufactured"}, {"name": "Other", "accountdesc": "Otherd", "accountitem": "Otheri", "accountname": "Other", "accountnumber": "Other"}, {"name": "Sublet", "accountdesc": "Subletd", "accountitem": "Subleti", "accountname": "BODY SHOP COST:SUBLET", "accountnumber": "Sublet"}, {"name": "Towing", "accountdesc": "Towingd", "accountitem": "Towingi", "accountname": "BODY SHOP COST:TOWING", "accountnumber": "Towing"}, {"name": "Paint Mat", "accountdesc": "matd", "accountitem": "mati", "accountname": "BODY SHOP COST:PARTS:Materials", "accountnumber": "paint mat"}, {"name": "shop mat", "accountdesc": "shopd", "accountitem": "shopi", "accountname": "BODY SHOP COST:PARTS:Materials", "accountnumber": "shop"}], "taxes": {"local": {"name": "n", "rate": 0, "accountdesc": "n", "accountitem": "n", "accountname": "n", "accountnumber": "n"}, "state": {"name": "PST", "rate": 7, "accountdesc": "Ministry of Finance (BC)", "accountitem": "PST On Sales", "accountname": "PST Payable", "accountnumber": "2220"}, "federal": {"name": "GST", "rate": 5, "accountdesc": "Receiver General - GST", "accountitem": "GST On Sales", "accountname": "GST DUE-NET:GST Collected", "accountnumber": "2200a"}}, "profits": [{"name": "Aftermarket", "accountdesc": "Aftermarketd", "accountitem": "BODY SHOP_PAA", "accountname": "Aftermarket", "accountnumber": "Aftermarket"}, {"name": "ATP", "accountdesc": "ATPd", "accountitem": "BODY SHOP_ATP", "accountname": "ATP", "accountnumber": "ATP"}, {"name": "Body", "accountdesc": "BODY SHOP SALESLABOR:BODY", "accountitem": "BODY SHOP_LAB", "accountname": "BODY SHOP SALES:LABOR:BODY", "accountnumber": "5002"}, {"name": "Detail", "accountdesc": "Detaild", "accountitem": "BODY SHOP_DET", "accountname": "Detail", "accountnumber": "Detail"}, {"name": "Daignostic", "accountdesc": "Daignosticd", "accountitem": "BODY SHOP_LAD", "accountname": "Daignostic", "accountnumber": "Daignostic"}, {"name": "Electrical", "accountdesc": "Electricald", "accountitem": "BODY SHOP_LAE", "accountname": "Electrical", "accountnumber": "Electrical"}, {"name": "Chrome", "accountdesc": "Chromed", "accountitem": "BODY SHOP_PAC", "accountname": "Chrome", "accountnumber": "Chrome"}, {"name": "Frame", "accountdesc": "Framed", "accountitem": "BODY SHOP_LAF", "accountname": "Frame", "accountnumber": "Frame"}, {"name": "Mechanical", "accountdesc": "Mechanicald", "accountitem": "BODY SHOP_LAM", "accountname": "Mechanical", "accountnumber": "Mechanical"}, {"name": "Refinish", "accountdesc": "BODY SHOP SALES:LABOR:REFINISH", "accountitem": "BODY SHOP_LAR", "accountname": "BODY SHOP SALES:LABOR:REFINISH", "accountnumber": "5003"}, {"name": "Structural", "accountdesc": "Structurald", "accountitem": "BODY SHOP_LAS", "accountname": "Structural", "accountnumber": "Structural"}, {"name": "Existing", "accountdesc": "Existingd", "accountitem": "BODY SHOP_PAE", "accountname": "Existing", "accountnumber": "Existing"}, {"name": "Glass", "accountdesc": "Glassd", "accountitem": "BODY SHOP_PAG", "accountname": "Glass", "accountnumber": "Glass"}, {"name": "LKQ", "accountdesc": "LKQd", "accountitem": "BODY SHOP_PAL", "accountname": "LKQ", "accountnumber": "LKQ"}, {"name": "OEM", "accountdesc": "BODY SHOP SALES:PARTS:OEM", "accountitem": "BODY SHOP_PAN", "accountname": "BODY SHOP SALES:PARTS:OEM", "accountnumber": "OEM"}, {"name": "OEM Partial", "accountdesc": "Partiald", "accountitem": "BODY SHOP_PAP", "accountname": "OEM Partial", "accountnumber": "OEM Partial"}, {"name": "Re-cored", "accountdesc": "coredd", "accountitem": "BODY SHOP_PAO", "accountname": "Re-cored", "accountnumber": "Re-cored"}, {"name": "Remanufactured", "accountdesc": "Remanufacturedd", "accountitem": "BODY SHOP_PAO", "accountname": "Remanufactured", "accountnumber": "Remanufactured"}, {"name": "Other", "accountdesc": "Otherd", "accountitem": "BODY SHOP_PAO", "accountname": "Other", "accountnumber": "Other"}, {"name": "Sublet", "accountdesc": "Subletd", "accountitem": "BODY SHOP_PAS", "accountname": "Sublet", "accountnumber": "Sublet"}, {"name": "Towing", "accountdesc": "Towingd", "accountitem": "BODY SHOP_TOW", "accountname": "Towing", "accountnumber": "Towing"}, {"name": "paint", "accountdesc": "paintd", "accountitem": "BODY SHOP_MAPA", "accountname": "paint", "accountnumber": "paint"}, {"name": "shop", "accountdesc": "shopd", "accountitem": "BODY SHOP_MASH", "accountname": "shop", "accountnumber": "shop"}], "defaults": {"costs": {"ATP": "ATP", "LAB": "Body", "LAD": "Daignostic", "LAE": "Electrical", "LAF": "Frame", "LAG": "Glass", "LAM": "Mechanical", "LAR": "Refinish", "LAS": "Structural", "LAU": "Detail", "PAA": "Aftermarket", "PAC": "Chrome", "PAL": "LKQ", "PAM": "Remanufactured", "PAN": "OEM", "PAO": "Other", "PAP": "OEM Partial", "PAR": "Re-cored", "PAS": "Sublet", "TOW": "Towing", "MAPA": "Paint Mat", "MASH": "shop mat"}, "profits": {"ATP": "ATP", "LAB": "Body", "LAD": "Daignostic", "LAE": "Electrical", "LAF": "Frame", "LAG": "Glass", "LAM": "Mechanical", "LAR": "Refinish", "LAS": "Structural", "LAU": "Detail", "PAA": "Aftermarket", "PAC": "Chrome", "PAL": "LKQ", "PAM": "Remanufactured", "PAN": "OEM", "PAO": "Other", "PAP": "OEM Partial", "PAR": "Re-cored", "PAS": "Sublet", "TOW": "Towing", "MAPA": "paint", "MASH": "shop"}}} + +shoprates: {"rate_atp": 8.68} + +productionconfig: {"columnKeys": [{"key": "alert", "width": 57}, {"key": "ro_number", "width": 65}, {"key": "vehicle", "width": 264.1333465576172}, {"key": "clm_no", "width": 127.39999389648438}, {"key": "ownr", "width": 143}, {"key": "labhrs", "width": 48}, {"key": "larhrs", "width": 53.35003662109375}, {"key": "scheduled_completion", "width": 176}, {"key": "status", "width": 101}, {"key": "note", "width": 179}, {"key": "ct", "width": 74}, {"key": "clm_total"}], "tableState": {"sortedInfo": {"order": "descend", "columnKey": "larhrs"}, "filteredInfo": {}}} + +invoicetaxrates +{"local_tax_rate": 0, "state_tax_rate": 7, "federal_tax_rate": 5} + +intakechecklist +{"form": [{"name": "item1", "type": "checkbox", "label": "Checklist Item 1", "required": true}, {"name": "item2", "type": "checkbox", "label": "Checklist Item 2", "required": true}, {"name": "item3", "type": "checkbox", "label": "Checklist Item 3", "required": true}, {"name": "item4", "type": "checkbox", "label": "Checklist Item 4", "required": true}], "templates": ["estimate_detail", "estimate_detail2"]} + +ssbucekts +[{"id": "express", "lt": 10, "gte": 0, "label": "express", "target": 1}, {"id": "small", "lt": 20, "gte": 10, "label": "small", "target": 2}, {"id": "medium", "lt": 30, "gte": 20, "label": "medium", "target": 3}, {"id": "large", "lt": 40, "gte": 30, "label": "large", "target": 4}, {"id": "heavy", "lt": 60, "gte": 40, "label": "heavy", "target": 5}, {"id": "Insane", "gte": 60, "label": "Insane", "target": 1}] \ No newline at end of file diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index 296cfd489..65d954ca0 100644 --- a/bodyshop_translations.babel +++ b/bodyshop_translations.babel @@ -14513,6 +14513,53 @@ parts_orders + + actions + + + backordered + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + receive + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + errors @@ -14689,6 +14736,69 @@ + + oem_partno + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + order_date + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + order_number + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + quantity false @@ -14710,6 +14820,27 @@ + + status + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + diff --git a/client/src/components/invoices-list-table/invoices-list-table.component.jsx b/client/src/components/invoices-list-table/invoices-list-table.component.jsx index b32ea460b..78835a942 100644 --- a/client/src/components/invoices-list-table/invoices-list-table.component.jsx +++ b/client/src/components/invoices-list-table/invoices-list-table.component.jsx @@ -8,6 +8,8 @@ import { setModalContext } from "../../redux/modals/modals.actions"; import CurrencyFormatter from "../../utils/CurrencyFormatter"; import { DateFormatter } from "../../utils/DateFormatter"; import { alphaSort } from "../../utils/sorters"; +import queryString from "query-string"; +import { useLocation } from "react-router-dom"; const mapDispatchToProps = (dispatch) => ({ setPartsOrderContext: (context) => @@ -22,7 +24,6 @@ export function InvoicesListTableComponent({ job, loading, invoicesQuery, - selectedInvoice, handleOnRowClick, setPartsOrderContext, setInvoiceEnterContext, @@ -32,6 +33,8 @@ export function InvoicesListTableComponent({ const [state, setState] = useState({ sortedInfo: {}, }); + const search = queryString.parse(useLocation().search); + const selectedInvoice = search.invoiceid; const invoices = invoicesQuery.data ? invoicesQuery.data.invoices : []; const { refetch } = invoicesQuery; @@ -91,7 +94,8 @@ export function InvoicesListTableComponent({ render: (text, record) => (
+ to={`/manage/invoices?invoiceid=${record.id}&vendorid=${record.vendorid}`} + >
@@ -232,11 +237,11 @@ export function InvoicesListTableComponent({ @@ -246,9 +251,9 @@ export function InvoicesListTableComponent({ return (
( -
+
@@ -260,7 +265,8 @@ export function InvoicesListTableComponent({ job, }, }); - }}> + }} + > {t("jobs.actions.postInvoices")} {" "} -
+
{ @@ -290,7 +297,7 @@ export function InvoicesListTableComponent({ expandedRowRender={rowExpander} pagination={{ position: "top", defaultPageSize: 25 }} columns={columns} - rowKey='id' + rowKey="id" dataSource={invoices} onChange={handleTableChange} expandable={{ 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 1b64c1214..4d4555d69 100644 --- a/client/src/components/job-detail-lines/job-lines.component.jsx +++ b/client/src/components/job-detail-lines/job-lines.component.jsx @@ -1,3 +1,4 @@ +import { SyncOutlined } from "@ant-design/icons"; import { Button, Dropdown, Input, Menu, Table } from "antd"; import React, { useState } from "react"; import { useTranslation } from "react-i18next"; @@ -11,6 +12,8 @@ import AllocationsAssignmentContainer from "../allocations-assignment/allocation import AllocationsBulkAssignmentContainer from "../allocations-bulk-assignment/allocations-bulk-assignment.container"; import AllocationsEmployeeLabelContainer from "../allocations-employee-label/allocations-employee-label.container"; import PartsOrderModalContainer from "../parts-order-modal/parts-order-modal.container"; +import queryString from "query-string"; +import { useHistory, useLocation } from "react-router-dom"; const mapDispatchToProps = (dispatch) => ({ setJobLineEditContext: (context) => @@ -35,6 +38,9 @@ export function JobLinesComponent({ }); const { t } = useTranslation(); + const search = queryString.parse(useLocation().search); + const history = useHistory(); + const columns = [ { title: "#", @@ -213,7 +219,8 @@ export function JobLinesComponent({ actions: { refetch: refetch }, context: record, }); - }}> + }} + > {t("general.actions.edit")} - PAA - PAN - PAL + PAA + PAN + PAL ); @@ -248,16 +255,19 @@ export function JobLinesComponent({
{ return ( -
+
+ @@ -284,10 +295,11 @@ export function JobLinesComponent({ actions: { refetch: refetch }, context: { jobid: jobId }, }); - }}> + }} + > {t("joblines.actions.new")} -
+
{ @@ -300,11 +312,16 @@ export function JobLinesComponent({ ); }} expandedRowRender={(record) => ( -
+
{t("parts_orders.labels.orderhistory")} {record.parts_order_lines.map((item) => (
- {`${item.parts_order.order_number || ""} from `} + + {item.parts_order.order_number || ""} + + - {item.parts_order.vendor.name || ""} diff --git a/client/src/components/jobs-close-export-button/jobs-close-export-button.component.jsx b/client/src/components/jobs-close-export-button/jobs-close-export-button.component.jsx index 6e6af2471..9dabc3c07 100644 --- a/client/src/components/jobs-close-export-button/jobs-close-export-button.component.jsx +++ b/client/src/components/jobs-close-export-button/jobs-close-export-button.component.jsx @@ -44,7 +44,8 @@ export function JobsCloseExportButton({ bodyshop, jobId, disabled }) { let PartnerResponse; try { PartnerResponse = await axios.post( - "http://localhost:1337/qb/", + // "http://localhost:1337/qb/", + "http://b47e67f9cbe3.ngrok.io/qb/", QbXmlResponse.data, { headers: { 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 fb875a8e7..8b4f2d913 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 @@ -21,6 +21,8 @@ const mapDispatchToProps = (dispatch) => ({ dispatch(setModalContext({ context: context, modal: "schedule" })), setInvoiceEnterContext: (context) => dispatch(setModalContext({ context: context, modal: "invoiceEnter" })), + setPaymentContext: (context) => + dispatch(setModalContext({ context: context, modal: "payment" })), }); export function JobsDetailHeaderActions({ @@ -29,12 +31,13 @@ export function JobsDetailHeaderActions({ refetch, setScheduleContext, setInvoiceEnterContext, + setPaymentContext, }) { const { t } = useTranslation(); const client = useApolloClient(); const history = useHistory(); const statusmenu = ( - + { setScheduleContext({ @@ -44,30 +47,43 @@ export function JobsDetailHeaderActions({ job: job, }, }); - }}> + }} + > {t("jobs.actions.schedule")} - - + { + setPaymentContext({ + actions: {}, + context: { jobId: job.id }, + }); + }} + > + {t("menus.header.enterpayment")} + + + }} + > {t("menus.jobsactions.newcccontract")} AddToProduction(client, job.id, refetch)}> + onClick={() => AddToProduction(client, job.id, refetch)} + > {t("jobs.actions.addtoproduction")} - + e.stopPropagation()} onConfirm={() => DuplicateJob( @@ -79,12 +95,13 @@ export function JobsDetailHeaderActions({ } ) } - getPopupContainer={(trigger) => trigger.parentNode}> + getPopupContainer={(trigger) => trigger.parentNode} + > {t("menus.jobsactions.duplicate")} { setInvoiceEnterContext({ actions: { refetch: refetch }, @@ -92,14 +109,16 @@ export function JobsDetailHeaderActions({ job: job, }, }); - }}> + }} + > {t("jobs.actions.postInvoices")} - + + }} + > {t("menus.jobsactions.closejob")} @@ -108,10 +127,11 @@ export function JobsDetailHeaderActions({ ); return ( + key="changestatus" + > diff --git a/client/src/components/jobs-detail-pli/jobs-detail-pli.component.jsx b/client/src/components/jobs-detail-pli/jobs-detail-pli.component.jsx index 03ff41680..c00c08ef7 100644 --- a/client/src/components/jobs-detail-pli/jobs-detail-pli.component.jsx +++ b/client/src/components/jobs-detail-pli/jobs-detail-pli.component.jsx @@ -4,6 +4,7 @@ import AlertComponent from "../alert/alert.component"; import InvoicesListTableComponent from "../invoices-list-table/invoices-list-table.component"; import JobInvoicesTotalsComponent from "../job-invoices-total/job-invoices-total.component"; import PartsOrderModal from "../parts-order-modal/parts-order-modal.container"; +import { PartsOrderListTableComponent } from "../parts-order-list-table/parts-order-list-table.component"; const tableCol = { xs: { span: 24, @@ -25,22 +26,29 @@ const totalsCol = { export default function JobsDetailPliComponent({ job, invoicesQuery, - handleOnRowClick, + handleInvoiceOnRowClick, + handlePartsOrderOnRowClick, selectedInvoice, }) { return (
{invoicesQuery.error ? ( - + ) : null}
+ diff --git a/client/src/components/jobs-detail-pli/jobs-detail-pli.container.jsx b/client/src/components/jobs-detail-pli/jobs-detail-pli.container.jsx index a4b1f191b..ef57ff939 100644 --- a/client/src/components/jobs-detail-pli/jobs-detail-pli.container.jsx +++ b/client/src/components/jobs-detail-pli/jobs-detail-pli.container.jsx @@ -13,7 +13,7 @@ export default function JobsDetailPliContainer({ job }) { const search = queryString.parse(useLocation().search); const history = useHistory(); - const handleOnRowClick = (record) => { + const handleInvoiceOnRowClick = (record) => { if (record) { if (record.id) { search.invoiceid = record.id; @@ -25,12 +25,24 @@ export default function JobsDetailPliContainer({ job }) { } }; + const handlePartsOrderOnRowClick = (record) => { + if (record) { + if (record.id) { + search.partsorderid = record.id; + history.push({ search: queryString.stringify(search) }); + } + } else { + delete search.partsorderid; + history.push({ search: queryString.stringify(search) }); + } + }; + return ( ); } diff --git a/client/src/components/parts-order-line-backorder-button/parts-order-line-backorder-button.component.jsx b/client/src/components/parts-order-line-backorder-button/parts-order-line-backorder-button.component.jsx new file mode 100644 index 000000000..c261d5c7b --- /dev/null +++ b/client/src/components/parts-order-line-backorder-button/parts-order-line-backorder-button.component.jsx @@ -0,0 +1,52 @@ +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Button } from "antd"; +import { useMutation } from "@apollo/react-hooks"; +import { MUTATION_BACKORDER_PART_LINE } from "../../graphql/parts-orders.queries"; +import { selectBodyshop } from "../../redux/user/user.selectors"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; + +const mapStateToProps = createStructuredSelector({ + bodyshop: selectBodyshop, +}); + +export function PartsOrderLineBackorderButton({ + partsOrderStatus, + partsLineId, + jobLineId, + bodyshop, +}) { + const [loading, setLoading] = useState(false); + const [backorderLine] = useMutation(MUTATION_BACKORDER_PART_LINE); + const { t } = useTranslation(); + + const isAlreadyBackordered = + bodyshop.md_order_statuses.default_bo === partsOrderStatus; + + const handleOnClick = async () => { + setLoading(true); + + const result = await backorderLine({ + variables: { + jobLineId, + partsLineId, + status: isAlreadyBackordered + ? bodyshop.md_order_statuses.default_received || "Received*" + : bodyshop.md_order_statuses.default_bo || "Backordered*", + }, + }); + + setLoading(false); + }; + + return ( + + ); +} + +export default connect(mapStateToProps, null)(PartsOrderLineBackorderButton); diff --git a/client/src/components/parts-order-list-table/parts-order-list-table.component.jsx b/client/src/components/parts-order-list-table/parts-order-list-table.component.jsx new file mode 100644 index 000000000..1b5dbd938 --- /dev/null +++ b/client/src/components/parts-order-list-table/parts-order-list-table.component.jsx @@ -0,0 +1,215 @@ +import { SyncOutlined } from "@ant-design/icons"; +import { Button, Input, Table } from "antd"; +import queryString from "query-string"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { useLocation } from "react-router-dom"; +import CurrencyFormatter from "../../utils/CurrencyFormatter"; +import { DateFormatter } from "../../utils/DateFormatter"; +import { alphaSort } from "../../utils/sorters"; +import PartsOrderLineBackorderButton from "../parts-order-line-backorder-button/parts-order-line-backorder-button.component"; + +const mapDispatchToProps = (dispatch) => ({}); + +export function PartsOrderListTableComponent({ + job, + loading, + invoicesQuery, + + handleOnRowClick, +}) { + const { t } = useTranslation(); + const [state, setState] = useState({ + sortedInfo: {}, + }); + const search = queryString.parse(useLocation().search); + const selectedpartsorder = search.partsorderid; + + const parts_orders = invoicesQuery.data + ? invoicesQuery.data.parts_orders + : []; + const { refetch } = invoicesQuery; + const columns = [ + { + title: t("vendors.fields.name"), + dataIndex: "vendorname", + key: "vendorname", + sorter: (a, b) => alphaSort(a.vendor.name, b.vendor.name), + sortOrder: + state.sortedInfo.columnKey === "vendorname" && state.sortedInfo.order, + render: (text, record) => {record.vendor.name}, + }, + { + title: t("parts_orders.fields.order_number"), + dataIndex: "order_number", + key: "order_number", + sorter: (a, b) => alphaSort(a.invoice_number, b.invoice_number), + sortOrder: + state.sortedInfo.columnKey === "invoice_number" && + state.sortedInfo.order, + }, + { + title: t("parts_orders.fields.order_date"), + dataIndex: "order_date", + key: "order_date", + sorter: (a, b) => a.order_date - b.order_date, + sortOrder: + state.sortedInfo.columnKey === "order_date" && state.sortedInfo.order, + render: (text, record) => ( + {record.order_date} + ), + }, + { + title: t("parts_orders.fields.deliver_by"), + dataIndex: "deliver_by", + key: "deliver_by", + sorter: (a, b) => a.deliver_by - b.deliver_by, + sortOrder: + state.sortedInfo.columnKey === "deliver_by" && state.sortedInfo.order, + render: (text, record) => ( + {record.deliver_by} + ), + }, + ]; + + const handleTableChange = (pagination, filters, sorter) => { + setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); + }; + + const rowExpander = (record) => { + const columns = [ + { + title: t("parts_orders.fields.line_desc"), + dataIndex: "line_desc", + key: "line_desc", + sorter: (a, b) => alphaSort(a.line_desc, b.line_desc), + sortOrder: + state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order, + }, + { + title: t("parts_orders.fields.db_price"), + dataIndex: "db_price", + key: "db_price", + sorter: (a, b) => a.db_price - b.db_price, + sortOrder: + state.sortedInfo.columnKey === "db_price" && state.sortedInfo.order, + render: (text, record) => ( + {record.db_price} + ), + }, + { + title: t("parts_orders.fields.act_price"), + dataIndex: "act_price", + key: "act_price", + sorter: (a, b) => a.act_price - b.act_price, + sortOrder: + state.sortedInfo.columnKey === "act_price" && state.sortedInfo.order, + render: (text, record) => ( + {record.act_price} + ), + }, + + { + title: t("parts_orders.fields.oem_partno"), + dataIndex: "oem_partno", + key: "oem_partno", + sorter: (a, b) => alphaSort(a.oem_partno, b.oem_partno), + sortOrder: + state.sortedInfo.columnKey === "oem_partno" && state.sortedInfo.order, + }, + { + title: t("parts_orders.fields.line_remarks"), + dataIndex: "line_remarks", + key: "line_remarks", + }, + { + title: t("parts_orders.fields.status"), + dataIndex: "status", + key: "status", + }, + { + title: t("general.labels.actions"), + dataIndex: "actions", + key: "actions", + render: (text, record) => ( +
+ +
+ ), + }, + ]; + + return ( +
+
+ + ); + }; + + return ( +
( +
+ + +
+ { + e.preventDefault(); + }} + /> +
+
+ )} + scroll={{ x: "50%", y: "40rem" }} + expandedRowRender={rowExpander} + pagination={{ position: "top", defaultPageSize: 25 }} + columns={columns} + rowKey="id" + dataSource={parts_orders} + onChange={handleTableChange} + expandable={{ + expandedRowKeys: [selectedpartsorder], + onExpand: (expanded, record) => { + handleOnRowClick(expanded ? record : null); + }, + }} + rowSelection={{ + onSelect: (record) => { + handleOnRowClick(record); + }, + selectedRowKeys: [selectedpartsorder], + type: "radio", + }} + onRow={(record, rowIndex) => { + return { + onClick: (event) => { + handleOnRowClick(record); + }, // click row + onDoubleClick: (event) => {}, // double click row + onContextMenu: (event) => {}, // right button click row + onMouseEnter: (event) => {}, // mouse enter row + onMouseLeave: (event) => {}, // mouse leave row + }; + }} + /> + ); +} +export default connect(null, mapDispatchToProps)(PartsOrderListTableComponent); diff --git a/client/src/components/payment-modal/payment-modal.container.jsx b/client/src/components/payment-modal/payment-modal.container.jsx index d3a4bd4d9..c6e358c23 100644 --- a/client/src/components/payment-modal/payment-modal.container.jsx +++ b/client/src/components/payment-modal/payment-modal.container.jsx @@ -1,6 +1,6 @@ import { useElements, useStripe, CardElement } from "@stripe/react-stripe-js"; import { Form, Modal, notification } from "antd"; -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; @@ -37,6 +37,7 @@ function InvoiceEnterModalContainer({ const elements = useElements(); const { t } = useTranslation(); const { context, actions, visible } = paymentModal; + const [loading, setLoading] = useState(false); const stripeStateArr = useState({ error: null, @@ -118,6 +119,10 @@ function InvoiceEnterModalContainer({ toggleModalVisible(); }; + useEffect(() => { + if (visible) form.resetFields(); + }, [visible, form]); + return ( { - return id; - //return (data && data.find((x) => x.id === id).ro_number) || null; - }; - const client = useApolloClient(); const handleDragEnd = async (card, source, destination) => { diff --git a/client/src/components/shop-info/shop-info.scheduling.component.jsx b/client/src/components/shop-info/shop-info.scheduling.component.jsx index d40ad4807..bc57ef454 100644 --- a/client/src/components/shop-info/shop-info.scheduling.component.jsx +++ b/client/src/components/shop-info/shop-info.scheduling.component.jsx @@ -1,6 +1,6 @@ import { DeleteFilled } from "@ant-design/icons"; -import { Button, Form, Input, InputNumber, Select, Row, Col } from "antd"; -import React, { useState } from "react"; +import { Button, Col, Form, Input, InputNumber, Row } from "antd"; +import React from "react"; import { useTranslation } from "react-i18next"; //TODO Fix up styles. export default function ShopInfoSchedulingComponent({ form }) { diff --git a/client/src/graphql/invoices.queries.js b/client/src/graphql/invoices.queries.js index 456ef02be..9365c6b20 100644 --- a/client/src/graphql/invoices.queries.js +++ b/client/src/graphql/invoices.queries.js @@ -38,7 +38,32 @@ export const QUERY_ALL_INVOICES_PAGINATED = gql` `; export const QUERY_INVOICES_BY_JOBID = gql` - query QUERY_INVOICES_BY_JOBID($jobid: uuid!) { + query QUERY_PARTS_INVOICES_BY_JOBID($jobid: uuid!) { + parts_orders( + where: { jobid: { _eq: $jobid } } + order_by: { order_date: desc } + ) { + id + vendor { + id + name + } + order_date + deliver_by + parts_order_lines { + id + act_price + db_price + line_desc + oem_partno + status + line_remarks + quantity + job_line_id + } + order_number + user_email + } invoices(where: { jobid: { _eq: $jobid } }, order_by: { date: desc }) { id vendorid diff --git a/client/src/graphql/parts-orders.queries.js b/client/src/graphql/parts-orders.queries.js index 55f68cc11..af326ce5a 100644 --- a/client/src/graphql/parts-orders.queries.js +++ b/client/src/graphql/parts-orders.queries.js @@ -9,3 +9,30 @@ export const INSERT_NEW_PARTS_ORDERS = gql` } } `; + +export const MUTATION_BACKORDER_PART_LINE = gql` + mutation MUTATION_BACKORDER_PART_LINE( + $jobLineId: uuid! + $partsLineId: uuid! + $status: String! + ) { + update_parts_order_lines( + where: { id: { _eq: $partsLineId } } + _set: { status: $status } + ) { + returning { + status + id + } + } + update_joblines( + where: { id: { _eq: $jobLineId } } + _set: { status: $status } + ) { + returning { + status + id + } + } + } +`; diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index 081b7b1c9..9247a8fc8 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -899,6 +899,10 @@ } }, "parts_orders": { + "actions": { + "backordered": "Backordered", + "receive": "Receive" + }, "errors": { "creating": "Error encountered when creating parts order. " }, @@ -910,7 +914,11 @@ "line_desc": "Line Description", "line_remarks": "Remarks", "lineremarks": "Line Remarks", - "quantity": "Qty." + "oem_partno": "Part #", + "order_date": "Order Date", + "order_number": "Order Number", + "quantity": "Qty.", + "status": "Status" }, "labels": { "email": "Send by Email", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index 092fbfad0..3b0a38ba9 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -899,6 +899,10 @@ } }, "parts_orders": { + "actions": { + "backordered": "", + "receive": "" + }, "errors": { "creating": "Se encontró un error al crear el pedido de piezas." }, @@ -910,7 +914,11 @@ "line_desc": "", "line_remarks": "", "lineremarks": "Comentarios de línea", - "quantity": "" + "oem_partno": "", + "order_date": "", + "order_number": "", + "quantity": "", + "status": "" }, "labels": { "email": "Enviar por correo electrónico", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index 8b290ab95..c07a1b2b9 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -899,6 +899,10 @@ } }, "parts_orders": { + "actions": { + "backordered": "", + "receive": "" + }, "errors": { "creating": "Erreur rencontrée lors de la création de la commande de pièces." }, @@ -910,7 +914,11 @@ "line_desc": "", "line_remarks": "", "lineremarks": "Remarques sur la ligne", - "quantity": "" + "oem_partno": "", + "order_date": "", + "order_number": "", + "quantity": "", + "status": "" }, "labels": { "email": "Envoyé par email", diff --git a/client/src/utils/aamva.js b/client/src/utils/aamva.js index 0fd17dfad..0f81b49ec 100644 --- a/client/src/utils/aamva.js +++ b/client/src/utils/aamva.js @@ -36,22 +36,20 @@ data = data.replace(/\s/g, " "); var track = data.match(/(.*?\?)(.*?\?)(.*?\?)/); var res1 = track[1].match( - /(\%)([A-Z]{2})([^\^]{0,13})\^?([^\^]{0,35})\^?([^\^]{0,60})\^?\s*?\?/ + /(%)([A-Z]{2})([^^]{0,13})\^?([^^]{0,35})\^?([^^]{0,60})\^?\s*?\?/ ); var res2 = track[2].match( - /(;)(\d{6})(\d{0,13})(\=)(\d{4})(\d{8})(\d{0,5})\=?\?/ + /(;)(\d{6})(\d{0,13})(=)(\d{4})(\d{8})(\d{0,5})=?\?/ ); var res3 = track[3].match( - /(\#|\%|\+)(\d|\!|\")(\d|\s|.)([0-9A-Z ]{11})([0-9A-Z ]{2})([0-9A-Z ]{10})([0-9A-Z ]{4})([12MF ]{1})([0-9A-Z ]{3})([0-9A-Z ]{3})([0-9A-Z ]{3})([0-9A-Z ]{3})(.*?)\?/ + /(#|%|\+)(\d|!|")(\d|\s|.)([0-9A-Z ]{11})([0-9A-Z ]{2})([0-9A-Z ]{10})([0-9A-Z ]{4})([12MF ]{1})([0-9A-Z ]{3})([0-9A-Z ]{3})([0-9A-Z ]{3})([0-9A-Z ]{3})(.*?)\?/ ); var state = res1[2]; return { state: state, city: res1[3], name: (function () { - var res = res1[4].match( - /([^\$]{0,35})\$?([^\$]{0,35})?\$?([^\$]{0,35})?/ - ); + var res = res1[4].match(/([^$]{0,35})\$?([^$]{0,35})?\$?([^$]{0,35})?/); if (!res) return; return { last: res[1], @@ -90,13 +88,12 @@ switch (Number(res3[8])) { case 1: return "MALE"; - break; + case 2: return "FEMALE"; - break; + default: return "MISSING/INVALID"; - break; } })(), height: res3[9], @@ -452,7 +449,7 @@ var parsedData = {}; var res = data.match(parseRegex); - for (var i = 1; i < res.length; i++) { + for (i = 1; i < res.length; i++) { if (res[i] !== undefined) { parsedData[String(res[i]).substring(0, 3)] = res[i].substring(3).trim(); } @@ -525,10 +522,10 @@ switch (Number(parsedData.DBC)) { case 1: return "MALE"; - break; + case 2: return "FEMALE"; - break; + default: if (parsedData.DBC[0] === "M") { return "MALE"; @@ -536,7 +533,6 @@ return "FEMALE"; } return "MISSING/INVALID"; - break; } })(), height: undefined, diff --git a/server/accounting/accounting-constants.js b/server/accounting/accounting-constants.js index 9d6202c41..39c2bb862 100644 --- a/server/accounting/accounting-constants.js +++ b/server/accounting/accounting-constants.js @@ -1 +1 @@ -exports.DineroQbFormat = "0,0.00"; +exports.DineroQbFormat = "0.00"; diff --git a/server/accounting/qbxml/qbxml-receivables.js b/server/accounting/qbxml/qbxml-receivables.js index ba640572a..91465b298 100644 --- a/server/accounting/qbxml/qbxml-receivables.js +++ b/server/accounting/qbxml/qbxml-receivables.js @@ -72,13 +72,224 @@ exports.default = async (req, res) => { qbxml: generateInvoiceQbxml(jobs_by_pk, bodyshop), }); - res.status(200).json(QbXmlToExecute); + res.status(200).json([{ id: jobId, okStatusCodes: ["0"], qbxml: t }]); } catch (error) { console.log("error", error); res.status(400).send(JSON.stringify(error)); } }; +const t = ` + + + + + + Insurance Corporation of British Co + + 21291 122B AVE + MAPLE RIDGE + BC + + + + + + + CLOVER LANDON #4 + + Insurance Corporation of British Co + + + + + + + RO29 + + Insurance Corporation of British Co:CLOVER LANDON #4 + + + + + + + + Insurance Corporation of British Co:CLOVER LANDON #4:RO29 + + + RO29 + + 21291 122B AVE + MAPLE RIDGE + BC + + BM27914-1-A + + + BODY SHOP_PAA + + Aftermarketd + 1 + 1351.03 + + E + + + + + BODY SHOP_PAN + + BODY SHOP SALES:PARTS:OEM + 1 + 292.45 + + E + + + + + BODY SHOP_ATP + + ATPd + 1 + 144.09 + + E + + + + + BODY SHOP_LAB + + BODY SHOP SALESLABOR:BODY + 1 + 653.35 + + E + + + + + BODY SHOP_LAR + + BODY SHOP SALES:LABOR:REFINISH + 1 + 565.26 + + E + + + + + BODY SHOP_MAPA + + paintd + 1 + 347.66 + + E + + + + + BODY SHOP_MASH + + shopd + 1 + 54.38 + + E + + + + + GST On Sales + + Receiver General - GST + 170.41 + + + + PST On Sales + + Ministry of Finance (BC) + 238.58 + + + + + + +`; + +// exports.default = async (req, res) => { +// const BearerToken = req.headers.authorization; +// const { jobId } = req.body; + +// const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, { +// headers: { +// Authorization: BearerToken, +// }, +// }); + +// try { +// const result = await client +// .setHeaders({ Authorization: BearerToken }) +// .request(queries.QUERY_JOBS_FOR_RECEIVABLES_EXPORT, { id: jobId }); +// const { jobs_by_pk } = result; +// const { bodyshop } = jobs_by_pk; +// const QbXmlToExecute = []; + +// //Is this a two tier, or 3 tier setup? +// const isThreeTier = bodyshop.accountingconfig.tiers === 3; +// const twoTierPref = bodyshop.accountingconfig.twotierpref; + +// if (isThreeTier) { +// QbXmlToExecute.push({ +// id: jobId, +// okStatusCodes: ["0", "3100"], +// qbxml: generateSourceCustomerQbxml(jobs_by_pk, bodyshop), // Create the source customer. +// }); +// } + +// QbXmlToExecute.push({ +// id: jobId, +// okStatusCodes: ["0", "3100"], +// qbxml: generateJobQbxml( +// jobs_by_pk, +// bodyshop, +// isThreeTier, +// 2, +// twoTierPref +// ), +// }); + +// QbXmlToExecute.push({ +// id: jobId, +// okStatusCodes: ["0", "3100"], +// qbxml: generateJobQbxml( +// jobs_by_pk, +// bodyshop, +// isThreeTier, +// 3, +// twoTierPref +// ), +// }); +// //Generate the actual invoice. +// QbXmlToExecute.push({ +// id: jobId, +// okStatusCodes: ["0"], +// qbxml: generateInvoiceQbxml(jobs_by_pk, bodyshop), +// }); + +// res.status(200).json(QbXmlToExecute); +// } catch (error) { +// console.log("error", error); +// res.status(400).send(JSON.stringify(error)); +// } +// }; + const generateSourceCustomerQbxml = (jobs_by_pk, bodyshop) => { const customerQbxmlObj = { QBXML: { @@ -129,7 +340,13 @@ const generateOwnerTier = (jobs_by_pk) => { }`; }; -const generateJobQbxml = (jobs_by_pk, bodyshop, isThreeTier, tierLevel, twoTierPref) => { +const generateJobQbxml = ( + jobs_by_pk, + bodyshop, + isThreeTier, + tierLevel, + twoTierPref +) => { let Name; let ParentRefName;