Merged in release/2022-09-30 (pull request #590)

Release/2022 09 30
This commit is contained in:
Patrick Fic
2022-09-28 19:07:50 +00:00
8 changed files with 323 additions and 222 deletions

View File

@@ -28335,6 +28335,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>pimraryamountpayable</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<folder_node> <folder_node>
<name>plitooltips</name> <name>plitooltips</name>
<children> <children>
@@ -35325,6 +35346,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>external</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>findermodal</name> <name>findermodal</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>

View File

@@ -7,7 +7,10 @@ import { connect } from "react-redux";
import { useHistory, useLocation, useParams } from "react-router-dom"; import { useHistory, useLocation, useParams } from "react-router-dom";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../../../firebase/firebase.utils"; import { logImEXEvent } from "../../../../firebase/firebase.utils";
import { MARK_LATEST_APPOINTMENT_AS_ARRIVED } from "../../../../graphql/appointments.queries"; import {
MARK_APPOINTMENT_ARRIVED,
MARK_LATEST_APPOINTMENT_ARRIVED,
} from "../../../../graphql/appointments.queries";
import { UPDATE_JOB } from "../../../../graphql/jobs.queries"; import { UPDATE_JOB } from "../../../../graphql/jobs.queries";
import { import {
selectBodyshop, selectBodyshop,
@@ -41,7 +44,8 @@ export function JobChecklistForm({
const { t } = useTranslation(); const { t } = useTranslation();
const [intakeJob] = useMutation(UPDATE_JOB); const [intakeJob] = useMutation(UPDATE_JOB);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [markAptArrived] = useMutation(MARK_LATEST_APPOINTMENT_AS_ARRIVED); const [markAptArrived] = useMutation(MARK_APPOINTMENT_ARRIVED);
const [markLatestAptArrived] = useMutation(MARK_LATEST_APPOINTMENT_ARRIVED);
const [updateOwner] = useMutation(UPDATE_OWNER); const [updateOwner] = useMutation(UPDATE_OWNER);
const { jobId } = useParams(); const { jobId } = useParams();
@@ -125,6 +129,18 @@ export function JobChecklistForm({
variables: { appointmentId: search.appointmentId }, variables: { appointmentId: search.appointmentId },
}); });
if (!!appUpdate.errors) {
notification["error"]({
message: t("checklist.errors.complete", {
error: JSON.stringify(result.errors),
}),
});
}
} else if (type === "intake" && !search.appointmentId) {
const appUpdate = await markLatestAptArrived({
variables: { jobId: jobId },
});
if (!!appUpdate.errors) { if (!!appUpdate.errors) {
notification["error"]({ notification["error"]({
message: t("checklist.errors.complete", { message: t("checklist.errors.complete", {
@@ -133,6 +149,7 @@ export function JobChecklistForm({
}); });
} }
} }
if (type === "intake" && job.owner && job.owner.id) { if (type === "intake" && job.owner && job.owner.id) {
//Updae Owner Allow to Text //Updae Owner Allow to Text
const updateOwnerResult = await updateOwner({ const updateOwnerResult = await updateOwner({
@@ -175,12 +192,7 @@ export function JobChecklistForm({
}); });
} }
}; };
console.log(job, {
removeFromProduction: true,
actual_completion:
job && job.actual_completion && moment(job.actual_completion),
actual_delivery: job && job.actual_delivery && moment(job.actual_delivery),
});
return ( return (
<Card <Card
title={t("checklist.labels.checklist")} title={t("checklist.labels.checklist")}

View File

@@ -1,77 +1,81 @@
import { gql } from "@apollo/client"; import { gql } from "@apollo/client";
export const QUERY_ALL_ACTIVE_APPOINTMENTS = gql` export const QUERY_ALL_ACTIVE_APPOINTMENTS = gql`
query QUERY_ALL_ACTIVE_APPOINTMENTS( query QUERY_ALL_ACTIVE_APPOINTMENTS(
$start: timestamptz! $start: timestamptz!
$end: timestamptz! $end: timestamptz!
$startd: date! $startd: date!
$endd: date! $endd: date!
) {
employee_vacation(
where: { _or: [{ start: { _gte: $startd } },
{ end: { _lte: $endd } },
{_and:[{start:{_lte: $startd}},{end:{_gte:$endd}}]}] }
) { ) {
id employee_vacation(
start where: {
end _or: [
employee { { start: { _gte: $startd } }
{ end: { _lte: $endd } }
{ _and: [{ start: { _lte: $startd } }, { end: { _gte: $endd } }] }
]
}
) {
id id
last_name start
first_name end
employee {
id
last_name
first_name
}
} }
} appointments(
appointments( where: {
where: { canceled: { _eq: false }
canceled: { _eq: false } end: { _lte: $end }
end: { _lte: $end } start: { _gte: $start }
start: { _gte: $start } }
} ) {
) { start
start
id
end
arrived
title
isintake
block
color
note
job {
alt_transport
ro_number
ownr_ln
ownr_co_nm
ownr_fn
ownr_ph1
ownr_ph2
ownr_ea
clm_total
id id
clm_no end
ins_co_nm arrived
v_model_yr title
v_make_desc isintake
v_model_desc block
labhrs: joblines_aggregate( color
where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } } note
) { job {
aggregate { alt_transport
sum { ro_number
mod_lb_hrs ownr_ln
ownr_co_nm
ownr_fn
ownr_ph1
ownr_ph2
ownr_ea
clm_total
id
clm_no
ins_co_nm
v_model_yr
v_make_desc
v_model_desc
labhrs: joblines_aggregate(
where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }
) {
aggregate {
sum {
mod_lb_hrs
}
}
}
larhrs: joblines_aggregate(
where: { mod_lbr_ty: { _eq: "LAR" }, removed: { _eq: false } }
) {
aggregate {
sum {
mod_lb_hrs
}
} }
} }
} }
larhrs: joblines_aggregate(
where: { mod_lbr_ty: { _eq: "LAR" }, removed: { _eq: false } }
) {
aggregate {
sum {
mod_lb_hrs
}
}
}
}
} }
} }
`; `;
@@ -382,8 +386,8 @@ export const QUERY_SCHEDULE_LOAD_DATA = gql`
} }
`; `;
export const MARK_LATEST_APPOINTMENT_AS_ARRIVED = gql` export const MARK_APPOINTMENT_ARRIVED = gql`
mutation MARK_LATEST_APPOINTMENT_AS_ARRIVED($appointmentId: uuid!) { mutation MARK_APPOINTMENT_ARRIVED($appointmentId: uuid!) {
update_appointments( update_appointments(
where: { id: { _eq: $appointmentId } } where: { id: { _eq: $appointmentId } }
_set: { arrived: true } _set: { arrived: true }
@@ -396,3 +400,21 @@ export const MARK_LATEST_APPOINTMENT_AS_ARRIVED = gql`
} }
} }
`; `;
export const MARK_LATEST_APPOINTMENT_ARRIVED = gql`
mutation MARK_LATEST_APPOINTMENT_ARRIVED($jobId: uuid!) {
update_appointments(
where: {
jobid: { _eq: $jobId }
canceled: { _eq: false }
isintake: { _eq: true }
}
_set: { arrived: true }
) {
affected_rows
returning {
id
arrived
}
}
}
`;

View File

@@ -3,6 +3,7 @@ import { useApolloClient, useMutation } from "@apollo/client";
import { import {
Alert, Alert,
Button, Button,
Col,
Divider, Divider,
Form, Form,
Input, Input,
@@ -10,6 +11,7 @@ import {
notification, notification,
PageHeader, PageHeader,
Popconfirm, Popconfirm,
Row,
Select, Select,
Space, Space,
Statistic, Statistic,
@@ -329,148 +331,162 @@ export function JobsCloseComponent({ job, bodyshop, jobRO }) {
</LayoutFormRow> </LayoutFormRow>
<Divider>{t("jobs.labels.multipayers")}</Divider> <Divider>{t("jobs.labels.multipayers")}</Divider>
{Qb_Multi_Ar.treatment === "on" && ( {Qb_Multi_Ar.treatment === "on" && (
<Space> <Row gutter={[16, 16]}>
<Form.List <Col lg={8} md={24}>
name={["qb_multiple_payers"]} <Form.List
rules={[ name={["qb_multiple_payers"]}
({ getFieldValue }) => ({ rules={[
validator(_, value) { ({ getFieldValue }) => ({
let totalAllocated = Dinero(); validator(_, value) {
let totalAllocated = Dinero();
const payers = form.getFieldValue("qb_multiple_payers"); const payers = form.getFieldValue("qb_multiple_payers");
payers && payers &&
payers.forEach((payer) => { payers.forEach((payer) => {
totalAllocated = totalAllocated.add( totalAllocated = totalAllocated.add(
Dinero({ Dinero({
amount: Math.round((payer?.amount || 0) * 100), amount: Math.round((payer?.amount || 0) * 100),
}) })
); );
}); });
const discrep = job.job_totals const discrep = job.job_totals
? Dinero(job.job_totals.totals.total_repairs).subtract( ? Dinero(job.job_totals.totals.total_repairs).subtract(
totalAllocated totalAllocated
)
: Dinero();
return discrep.getAmount() > 0
? Promise.resolve()
: Promise.reject(
new Error(
t("jobs.labels.additionalpayeroverallocation")
) )
); : Dinero();
}, return discrep.getAmount() > 0
}), ? Promise.resolve()
]} : Promise.reject(
> new Error(
{(fields, { add, remove }) => { t("jobs.labels.additionalpayeroverallocation")
return ( )
<div> );
{fields.map((field, index) => ( },
<Form.Item key={field.key}> }),
<Space> ]}
<Form.Item >
label={t("jobs.fields.qb_multiple_payers.name")} {(fields, { add, remove }) => {
key={`${index}name`} return (
name={[field.name, "name"]} <div>
rules={[ {fields.map((field, index) => (
{ <Form.Item key={field.key}>
required: true, <Space>
}, <Form.Item
]} label={t("jobs.fields.qb_multiple_payers.name")}
> key={`${index}name`}
<Select name={[field.name, "name"]}
style={{ minWidth: "12rem" }} rules={[
disabled={jobRO} {
required: true,
},
]}
> >
{bodyshop.md_ins_cos.map((s) => ( <Select
<Select.Option key={s.name} value={s.name}> style={{ minWidth: "12rem" }}
{s.name} disabled={jobRO}
</Select.Option> >
))} {bodyshop.md_ins_cos.map((s) => (
</Select> <Select.Option key={s.name} value={s.name}>
</Form.Item> {s.name}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item <Form.Item
label={t("jobs.fields.qb_multiple_payers.amount")} label={t("jobs.fields.qb_multiple_payers.amount")}
key={`${index}amount`} key={`${index}amount`}
name={[field.name, "amount"]} name={[field.name, "amount"]}
rules={[ rules={[
{ {
required: true, required: true,
}, },
]} ]}
> >
<CurrencyInput min={0} disabled={jobRO} /> <CurrencyInput min={0} disabled={jobRO} />
</Form.Item> </Form.Item>
<DeleteFilled <DeleteFilled
disabled={jobRO} disabled={jobRO}
onClick={() => { onClick={() => {
remove(field.name); remove(field.name);
}} }}
/> />
</Space> </Space>
</Form.Item>
))}
<Form.Item>
<Button
disabled={jobRO}
onClick={() => {
add();
}}
style={{ width: "100%" }}
>
{t("jobs.actions.dms.addpayer")}
</Button>
</Form.Item> </Form.Item>
))} </div>
<Form.Item> );
<Button }}
disabled={jobRO} </Form.List>
onClick={() => { </Col>
add(); <Col lg={16} md={24}>
}} <Form.Item shouldUpdate>
style={{ width: "100%" }} {() => {
> //Perform Calculation to determine discrepancy.
{t("jobs.actions.dms.addpayer")} let totalAllocated = Dinero();
</Button>
</Form.Item>
</div>
);
}}
</Form.List>
<Form.Item shouldUpdate>
{() => {
//Perform Calculation to determine discrepancy.
let totalAllocated = Dinero();
const payers = form.getFieldValue("qb_multiple_payers"); const payers = form.getFieldValue("qb_multiple_payers");
payers && payers &&
payers.forEach((payer) => { payers.forEach((payer) => {
totalAllocated = totalAllocated.add( totalAllocated = totalAllocated.add(
Dinero({ amount: Math.round((payer?.amount || 0) * 100) }) Dinero({
); amount: Math.round((payer?.amount || 0) * 100),
}); })
const discrep = job.job_totals );
? Dinero(job.job_totals.totals.total_repairs).subtract( });
totalAllocated const discrep = job.job_totals
) ? Dinero(job.job_totals.totals.total_repairs).subtract(
: Dinero(); totalAllocated
return ( )
<Space size="large" wrap align="center"> : Dinero();
<Statistic return (
title={t("jobs.labels.total_repairs")} <Space size="large" wrap align="center">
value={(job.job_totals <Statistic
? Dinero(job.job_totals.totals.total_repairs) title={t("jobs.labels.total_cust_payable")}
: Dinero() value={(job.job_totals
).toFormat()} ? Dinero(job.job_totals.totals.custPayable)
/> : Dinero()
<Typography.Title>-</Typography.Title> ).toFormat()}
<Statistic />
title={t("jobs.labels.dms.totalallocated")} <Divider type="vertical" />
value={totalAllocated.toFormat()} <Statistic
/> title={t("jobs.labels.total_repairs")}
<Typography.Title>=</Typography.Title> value={(job.job_totals
<Statistic ? Dinero(job.job_totals.totals.total_repairs)
title={t("jobs.labels.total_cust_payable")} : Dinero()
valueStyle={{ ).toFormat()}
color: discrep.getAmount() > 0 ? "green" : "red", />
}} <Typography.Title>-</Typography.Title>
value={discrep.toFormat()} <Statistic
/> title={t("jobs.labels.dms.totalallocated")}
</Space> value={totalAllocated.toFormat()}
); />
}} <Typography.Title>=</Typography.Title>
</Form.Item> <Statistic
</Space> title={t("jobs.labels.pimraryamountpayable")}
valueStyle={{
color: discrep.getAmount() > 0 ? "green" : "red",
}}
value={discrep.toFormat()}
/>
</Space>
);
}}
</Form.Item>
</Col>
</Row>
)} )}
<Divider /> <Divider />
<JobsCloseLines job={job} /> <JobsCloseLines job={job} />

View File

@@ -1,6 +1,7 @@
import { SyncOutlined } from "@ant-design/icons"; import { SyncOutlined } from "@ant-design/icons";
import { useQuery } from "@apollo/client"; import { useQuery } from "@apollo/client";
import { Button, Card, Input, Space, Table } from "antd"; import { Button, Card, Input, Space, Table } from "antd";
import _ from "lodash";
import queryString from "query-string"; import queryString from "query-string";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -14,7 +15,6 @@ import OwnerNameDisplay from "../../components/owner-name-display/owner-name-dis
import ProductionListColumnComment from "../../components/production-list-columns/production-list-columns.comment.component"; import ProductionListColumnComment from "../../components/production-list-columns/production-list-columns.comment.component";
import { QUERY_PARTS_QUEUE } from "../../graphql/jobs.queries"; import { QUERY_PARTS_QUEUE } from "../../graphql/jobs.queries";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import { onlyUnique } from "../../utils/arrayHelper";
import { DateTimeFormatter, TimeAgoFormatter } from "../../utils/DateFormatter"; import { DateTimeFormatter, TimeAgoFormatter } from "../../utils/DateFormatter";
import { alphaSort, dateSort } from "../../utils/sorters"; import { alphaSort, dateSort } from "../../utils/sorters";
import useLocalStorage from "../../utils/useLocalStorage"; import useLocalStorage from "../../utils/useLocalStorage";
@@ -94,6 +94,14 @@ export function PartsQueuePageComponent({ bodyshop }) {
// searchParams.page = pagination.current; // searchParams.page = pagination.current;
searchParams.sortcolumn = sorter.columnKey; searchParams.sortcolumn = sorter.columnKey;
searchParams.sortorder = sorter.order; searchParams.sortorder = sorter.order;
if (filters.status) {
searchParams.statusFilters = JSON.stringify(
_.flattenDeep(filters.status)
);
} else {
delete searchParams.statusFilters;
}
setFilter(filters); setFilter(filters);
history.push({ search: queryString.stringify(searchParams) }); history.push({ search: queryString.stringify(searchParams) });
}; };
@@ -136,19 +144,14 @@ export function PartsQueuePageComponent({ bodyshop }) {
key: "status", key: "status",
sorter: (a, b) => alphaSort(a.status, b.status), sorter: (a, b) => alphaSort(a.status, b.status),
sortOrder: sortcolumn === "status" && sortorder, sortOrder: sortcolumn === "status" && sortorder,
filteredValue: statusFilters ? JSON.parse(statusFilters) : null,
filters: filters:
(jobs && bodyshop.md_ro_statuses.active_statuses.map((s) => {
jobs return {
.map((j) => j.status) text: s || "No Status*",
.filter(onlyUnique) value: [s],
.map((s) => { };
return { }) || [],
text: s || "No Status*",
value: [s],
};
})) ||
[],
onFilter: (value, record) => value.includes(record.status),
render: (text, record) => { render: (text, record) => {
return record.status || t("general.labels.na"); return record.status || t("general.labels.na");
}, },

View File

@@ -1662,6 +1662,7 @@
"partsfilter": "Parts Only", "partsfilter": "Parts Only",
"partssubletstotal": "Parts & Sublets Total", "partssubletstotal": "Parts & Sublets Total",
"partstotal": "Parts Total (ex. Taxes)", "partstotal": "Parts Total (ex. Taxes)",
"pimraryamountpayable": "Total Primary Payable",
"plitooltips": { "plitooltips": {
"billtotal": "The total amount of all bill lines that have been posted against this RO (not including credits, taxes, or labor adjustments).", "billtotal": "The total amount of all bill lines that have been posted against this RO (not including credits, taxes, or labor adjustments).",
"calculatedcreditsnotreceived": "The calculated credits not received is derived by subtracting the amount of credit memos entered from the <b>retail</b> total of returns created. This does not take into account whether the credit was marked as received. You can find more information <a href=\"https://help.imex.online/en/article/credits-not-received-changes-1jy9snw\" target=\"_blank\">here</a>.", "calculatedcreditsnotreceived": "The calculated credits not received is derived by subtracting the amount of credit memos entered from the <b>retail</b> total of returns created. This does not take into account whether the credit was marked as received. You can find more information <a href=\"https://help.imex.online/en/article/credits-not-received-changes-1jy9snw\" target=\"_blank\">here</a>.",
@@ -2090,6 +2091,7 @@
"customer": "Customer", "customer": "Customer",
"edit": "Edit Payment", "edit": "Edit Payment",
"electronicpayment": "Use Electronic Payment Processing?", "electronicpayment": "Use Electronic Payment Processing?",
"external": "External",
"findermodal": "ICBC Payment Finder", "findermodal": "ICBC Payment Finder",
"insurance": "Insurance", "insurance": "Insurance",
"new": "New Payment", "new": "New Payment",

View File

@@ -1662,6 +1662,7 @@
"partsfilter": "", "partsfilter": "",
"partssubletstotal": "", "partssubletstotal": "",
"partstotal": "", "partstotal": "",
"pimraryamountpayable": "",
"plitooltips": { "plitooltips": {
"billtotal": "", "billtotal": "",
"calculatedcreditsnotreceived": "", "calculatedcreditsnotreceived": "",
@@ -2090,6 +2091,7 @@
"customer": "", "customer": "",
"edit": "", "edit": "",
"electronicpayment": "", "electronicpayment": "",
"external": "",
"findermodal": "", "findermodal": "",
"insurance": "", "insurance": "",
"new": "", "new": "",

View File

@@ -1662,6 +1662,7 @@
"partsfilter": "", "partsfilter": "",
"partssubletstotal": "", "partssubletstotal": "",
"partstotal": "", "partstotal": "",
"pimraryamountpayable": "",
"plitooltips": { "plitooltips": {
"billtotal": "", "billtotal": "",
"calculatedcreditsnotreceived": "", "calculatedcreditsnotreceived": "",
@@ -2090,6 +2091,7 @@
"customer": "", "customer": "",
"edit": "", "edit": "",
"electronicpayment": "", "electronicpayment": "",
"external": "",
"findermodal": "", "findermodal": "",
"insurance": "", "insurance": "",
"new": "", "new": "",