Merge branch 'release/2023-10-20' into rome/test

This commit is contained in:
Patrick Fic
2023-10-20 13:14:29 -07:00
29 changed files with 317 additions and 265 deletions

View File

@@ -38,8 +38,8 @@ const mapStateToProps = createStructuredSelector({
});
const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => dispatch(toggleModalVisible("billEnter")),
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
insertAuditTrail: ({ jobid, billid, operation }) =>
dispatch(insertAuditTrail({ jobid, billid, operation })),
});
const Templates = TemplateList("job_special");
@@ -167,7 +167,7 @@ function BillEnterModalContainer({
},
],
},
refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID"],
refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID", "GET_JOB_BY_PK"],
});
await Promise.all(
@@ -354,7 +354,9 @@ function BillEnterModalContainer({
insertAuditTrail({
jobid: values.jobid,
billid: billId,
operation: AuditTrailMapping.billposted(remainingValues.invoice_number),
operation: AuditTrailMapping.billposted(
r1.data.insert_bills.returning[0].invoice_number
),
});
if (enterAgain) {

View File

@@ -8,15 +8,23 @@ import PhoneNumberFormatter from "../../utils/PhoneFormatter";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { searchingForConversation } from "../../redux/messaging/messaging.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
searchingForConversation: searchingForConversation,
});
const mapDispatchToProps = (dispatch) => ({
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
});
export function ChatOpenButton({ bodyshop, phone, jobid, openChatByPhone }) {
export function ChatOpenButton({
bodyshop,
searchingForConversation,
phone,
jobid,
openChatByPhone,
}) {
const { t } = useTranslation();
if (!phone) return <></>;
@@ -29,7 +37,7 @@ export function ChatOpenButton({ bodyshop, phone, jobid, openChatByPhone }) {
onClick={(e) => {
e.stopPropagation();
const p = parsePhoneNumber(phone, "CA");
if (searchingForConversation) return; //This is to prevent finding the same thing twice.
if (p && p.isValid()) {
openChatByPhone({ phone_num: p.formatInternational(), jobid: jobid });
} else {

View File

@@ -9,15 +9,15 @@ import {
Table,
Tooltip,
} from "antd";
import moment from "moment";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import { alphaSort } from "../../utils/sorters";
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
import moment from "moment";
import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants";
import { alphaSort } from "../../utils/sorters";
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
const [state, setState] = useState({
sortedInfo: {},
@@ -115,6 +115,14 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
sortOrder:
state.sortedInfo.columnKey === "model" && state.sortedInfo.order,
},
{
title: t("courtesycars.fields.color"),
dataIndex: "color",
key: "color",
sorter: (a, b) => alphaSort(a.color, b.color),
sortOrder:
state.sortedInfo.columnKey === "color" && state.sortedInfo.order,
},
{
title: t("courtesycars.fields.plate"),
dataIndex: "plate",

View File

@@ -109,7 +109,7 @@ export function EmailOverlayContainer({
]
: []),
],
media: selectedMedia.filter((m) => m.isSelected).map((m) => m.src),
media: selectedMedia.filter((m) => m.isSelected).map((m) => m.fullsize),
//attachments,
});
notification["success"]({ message: t("emails.successes.sent") });

View File

@@ -8,7 +8,7 @@ export default function JobLinesBillRefernece({ jobline }) {
return (
<div style={{ color: subletRequired && "tomato" }}>
{subletRequired && <WarningFilled />}
{`${(billLine.actual_price * billLine.quantity).toFixed(2)} (${
{`${billLine.actual_price.toFixed(2)} x ${billLine.quantity} (${
billLine.bill.vendor.name
} #${billLine.bill.invoice_number})`}
</div>

View File

@@ -33,7 +33,9 @@ const JobSearchSelect = (
useLazyQuery(SEARCH_JOBS_BY_ID_FOR_AUTOCOMPLETE);
const executeSearch = (v) => {
if (v && v !== "") callSearch(v);
console.log(v);
if (v && v.variables?.search !== "" && v.variables.search.length >= 2)
callSearch(v);
};
const debouncedExecuteSearch = _.debounce(executeSearch, 500);

View File

@@ -52,7 +52,7 @@ function JobDocumentsLocalGalleryExternal({
val.type.mime &&
val.type.mime.startsWith("image")
) {
acc.push({ ...val, src: val.thumbnail });
acc.push({ ...val, src: val.thumbnail, fullsize: val.src });
}
return acc;
}, [])

View File

@@ -231,7 +231,14 @@ export function LaborAllocationsTable({
{summary.adjustments.toFixed(1)}
</Table.Summary.Cell>
<Table.Summary.Cell>
{summary.difference.toFixed(1)}
<Typography.Text
style={{
fontWeight: "bold",
color: summary.difference >= 0 ? "green" : "red",
}}
>
{summary.difference.toFixed(1)}
</Typography.Text>
</Table.Summary.Cell>
</Table.Summary.Row>
)}

View File

@@ -21,7 +21,8 @@ const OwnerSearchSelect = ({ value, onChange, onBlur, disabled }, ref) => {
useLazyQuery(SEARCH_OWNERS_BY_ID_FOR_AUTOCOMPLETE);
const executeSearch = (v) => {
callSearch(v);
if (v && v.variables?.search !== "" && v.variables.search.length >= 2)
callSearch(v);
};
const debouncedExecuteSearch = _.debounce(executeSearch, 500);

View File

@@ -106,7 +106,11 @@ export default function OwnersListComponent({
<Input.Search
placeholder={search.search || t("general.labels.search")}
onSearch={(value) => {
search.search = value;
if (value?.length >= 3) {
search.search = value;
} else {
delete search.search;
}
history.push({ search: queryString.stringify(search) });
}}
enterButton

View File

@@ -480,8 +480,8 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
state.sortedInfo.order,
sorter: (a, b) =>
alphaSort(
bodyshop.employees.find((e) => e.id === a.employee_body)?.first_name,
bodyshop.employees.find((e) => e.id === b.employee_body)?.first_name
bodyshop.employees?.find((e) => e.id === a.employee_body)?.first_name,
bodyshop.employees?.find((e) => e.id === b.employee_body)?.first_name
),
render: (text, record) => (
<ProductionListEmployeeAssignment
@@ -499,8 +499,8 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
state.sortedInfo.order,
sorter: (a, b) =>
alphaSort(
bodyshop.employees.find((e) => e.id === a.employee_prep)?.first_name,
bodyshop.employees.find((e) => e.id === b.employee_prep)?.first_name
bodyshop.employees?.find((e) => e.id === a.employee_prep)?.first_name,
bodyshop.employees?.find((e) => e.id === b.employee_prep)?.first_name
),
render: (text, record) => (
<ProductionListEmployeeAssignment
@@ -517,8 +517,8 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
state.sortedInfo.columnKey === "employee_csr" && state.sortedInfo.order,
sorter: (a, b) =>
alphaSort(
bodyshop.employees.find((e) => e.id === a.employee_csr)?.first_name,
bodyshop.employees.find((e) => e.id === b.employee_csr)?.first_name
bodyshop.employees?.find((e) => e.id === a.employee_csr)?.first_name,
bodyshop.employees?.find((e) => e.id === b.employee_csr)?.first_name
),
render: (text, record) => (
<ProductionListEmployeeAssignment record={record} type="employee_csr" />
@@ -533,9 +533,9 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
state.sortedInfo.order,
sorter: (a, b) =>
alphaSort(
bodyshop.employees.find((e) => e.id === a.employee_refinish)
bodyshop.employees?.find((e) => e.id === a.employee_refinish)
?.first_name,
bodyshop.employees.find((e) => e.id === b.employee_refinish)
bodyshop.employees?.find((e) => e.id === b.employee_refinish)
?.first_name
),
render: (text, record) => (

View File

@@ -55,25 +55,27 @@ export function ProductionListPrint({ bodyshop }) {
<Menu.SubMenu
title={t("reportcenter.templates.production_by_technician_one")}
>
{bodyshop.employees.map((e) => (
<Menu.Item
key={e.id}
onClick={async () => {
setLoading(true);
await GenerateDocument(
{
name: production_by_technician_one.key,
variables: { id: e.id },
},
{},
"p"
);
setLoading(false);
}}
>
{e.first_name} {e.last_name}
</Menu.Item>
))}
{bodyshop.employees
.filter((e) => e.active)
.map((e) => (
<Menu.Item
key={e.id}
onClick={async () => {
setLoading(true);
await GenerateDocument(
{
name: production_by_technician_one.key,
variables: { id: e.id },
},
{},
"p"
);
setLoading(false);
}}
>
{e.first_name} {e.last_name}
</Menu.Item>
))}
</Menu.SubMenu>
<Menu.SubMenu
title={t("reportcenter.templates.production_by_category_one")}

View File

@@ -16,13 +16,12 @@ const VehicleSearchSelect = ({ value, onChange, onBlur, disabled }, ref) => {
SEARCH_VEHICLES_FOR_AUTOCOMPLETE
);
const [
callIdSearch,
{ loading: idLoading, error: idError, data: idData },
] = useLazyQuery(SEARCH_VEHICLES_BY_ID_FOR_AUTOCOMPLETE);
const [callIdSearch, { loading: idLoading, error: idError, data: idData }] =
useLazyQuery(SEARCH_VEHICLES_BY_ID_FOR_AUTOCOMPLETE);
const executeSearch = (v) => {
callSearch(v);
if (v && v.variables?.search !== "" && v.variables.search.length >= 2)
callSearch(v);
};
const debouncedExecuteSearch = _.debounce(executeSearch, 500);

View File

@@ -5,6 +5,7 @@ export const INSERT_NEW_BILL = gql`
insert_bills(objects: $bill) {
returning {
id
invoice_number
}
}
}

View File

@@ -1298,7 +1298,7 @@ export const SEARCH_JOBS_FOR_AUTOCOMPLETE = gql`
) {
search_jobs(
args: { search: $search }
limit: 50
limit: 25
where: {
_and: {
converted: { _eq: $isConverted }

View File

@@ -36,7 +36,7 @@ export const SEARCH_OWNERS_FOR_AUTOCOMPLETE = gql`
query SEARCH_OWNERS_FOR_AUTOCOMPLETE($search: String) {
search_owners(
args: { search: $search }
limit: 50
limit: 25
order_by: { ownr_ln: desc_nulls_last }
) {
id

View File

@@ -184,7 +184,7 @@ export const SEARCH_VEHICLES_BY_ID_FOR_AUTOCOMPLETE = gql`
`;
export const SEARCH_VEHICLES_FOR_AUTOCOMPLETE = gql`
query SEARCH_VEHICLES_FOR_AUTOCOMPLETE($search: String) {
search_vehicles(args: { search: $search }, limit: 50) {
search_vehicles(args: { search: $search }, limit: 25) {
id
v_vin
v_model_yr

View File

@@ -6,6 +6,7 @@ const INITIAL_STATE = {
isSending: false,
error: null,
message: null,
searchingForConversation: false,
};
const messagingReducer = (state = INITIAL_STATE, action) => {
@@ -17,10 +18,16 @@ const messagingReducer = (state = INITIAL_STATE, action) => {
...state,
visible: !state.visible,
};
case MessagingActionTypes.OPEN_CHAT_BY_PHONE:
return {
...state,
searchingForConversation: true,
};
case MessagingActionTypes.SET_SELECTED_CONVERSATION:
return {
...state,
visible: true,
searchingForConversation: false,
selectedConversationId: action.payload,
};
case MessagingActionTypes.SEND_MESSAGE:

View File

@@ -4,7 +4,7 @@ import { all, call, put, select, takeLatest } from "redux-saga/effects";
import { logImEXEvent } from "../../firebase/firebase.utils";
import {
CONVERSATION_ID_BY_PHONE,
CREATE_CONVERSATION
CREATE_CONVERSATION,
} from "../../graphql/conversations.queries";
import { INSERT_CONVERSATION_TAG } from "../../graphql/job-conversations.queries";
import client from "../../utils/GraphQLClient";
@@ -12,7 +12,7 @@ import { selectBodyshop } from "../user/user.selectors";
import {
sendMessageFailure,
sendMessageSuccess,
setSelectedConversation
setSelectedConversation,
} from "./messaging.actions";
import MessagingActionTypes from "./messaging.types";
@@ -79,6 +79,7 @@ export function* openChatByPhone({ payload }) {
});
} else {
console.log("ERROR: Multiple conversations found. ");
yield put(setSelectedConversation(null));
}
} catch (error) {
console.log("Error in sendMessage saga.", error);

View File

@@ -26,3 +26,8 @@ export const selectMessage = createSelector(
[selectMessaging],
(messaging) => messaging.message
);
export const searchingForConversation = createSelector(
[selectMessaging],
(messaging) => messaging.searchingForConversation
);

View File

@@ -2660,18 +2660,22 @@
"gsr_labor_only": "Gross Sales - Labor Only",
"hours_sold_detail_closed": "Hours Sold Detail - Closed",
"hours_sold_detail_closed_csr": "Hours Sold Detail - Closed by CSR",
"hours_sold_detail_closed_estimator": "Hours Sold Detail - Closed by Estimator",
"hours_sold_detail_closed_ins_co": "Hours Sold Detail - Closed by Source",
"hours_sold_detail_closed_status": "Hours Sold Detail - Closed by Status",
"hours_sold_detail_open": "Hours Sold Detail - Open",
"hours_sold_detail_open_csr": "Hours Sold Detail - Open by CSR",
"hours_sold_detail_open_estimator": "Hours Sold Detail - Open by Estimator",
"hours_sold_detail_open_ins_co": "Hours Sold Detail - Open by Source",
"hours_sold_detail_open_status": "Hours Sold Detail - Open by Status",
"hours_sold_summary_closed": "Hours Sold Summary - Closed",
"hours_sold_summary_closed_csr": "Hours Sold Summary - Closed by CSR",
"hours_sold_summary_closed_estimator": "Hours Sold Summary - Closed by Estimator",
"hours_sold_summary_closed_ins_co": "Hours Sold Summary - Closed by Source",
"hours_sold_summary_closed_status": "Hours Sold Summary - Closed by Status",
"hours_sold_summary_open": "Hours Sold Summary - Open",
"hours_sold_summary_open_csr": "Hours Sold Summary - Open CSR",
"hours_sold_summary_open_estimator": "Hours Sold Summary - Open Estimator",
"hours_sold_summary_open_ins_co": "Hours Sold Summary - Open by Source",
"hours_sold_summary_open_status": "Hours Sold Summary - Open by Status",
"job_costing_ro_csr": "Job Costing by CSR",

View File

@@ -2660,18 +2660,22 @@
"gsr_labor_only": "",
"hours_sold_detail_closed": "",
"hours_sold_detail_closed_csr": "",
"hours_sold_detail_closed_estimator": "",
"hours_sold_detail_closed_ins_co": "",
"hours_sold_detail_closed_status": "",
"hours_sold_detail_open": "",
"hours_sold_detail_open_csr": "",
"hours_sold_detail_open_estimator": "",
"hours_sold_detail_open_ins_co": "",
"hours_sold_detail_open_status": "",
"hours_sold_summary_closed": "",
"hours_sold_summary_closed_csr": "",
"hours_sold_summary_closed_estimator": "",
"hours_sold_summary_closed_ins_co": "",
"hours_sold_summary_closed_status": "",
"hours_sold_summary_open": "",
"hours_sold_summary_open_csr": "",
"hours_sold_summary_open_estimator": "",
"hours_sold_summary_open_ins_co": "",
"hours_sold_summary_open_status": "",
"job_costing_ro_csr": "",

View File

@@ -2660,18 +2660,22 @@
"gsr_labor_only": "",
"hours_sold_detail_closed": "",
"hours_sold_detail_closed_csr": "",
"hours_sold_detail_closed_estimator": "",
"hours_sold_detail_closed_ins_co": "",
"hours_sold_detail_closed_status": "",
"hours_sold_detail_open": "",
"hours_sold_detail_open_csr": "",
"hours_sold_detail_open_estimator": "",
"hours_sold_detail_open_ins_co": "",
"hours_sold_detail_open_status": "",
"hours_sold_summary_closed": "",
"hours_sold_summary_closed_csr": "",
"hours_sold_summary_closed_estimator": "",
"hours_sold_summary_closed_ins_co": "",
"hours_sold_summary_closed_status": "",
"hours_sold_summary_open": "",
"hours_sold_summary_open_csr": "",
"hours_sold_summary_open_estimator": "",
"hours_sold_summary_open_ins_co": "",
"hours_sold_summary_open_status": "",
"job_costing_ro_csr": "",

View File

@@ -1,15 +1,14 @@
import { gql } from "@apollo/client";
import { notification } from "antd";
import jsreport from "@jsreport/browser-client";
import { notification } from "antd";
import axios from "axios";
import _ from "lodash";
import moment from "moment";
import { auth } from "../firebase/firebase.utils";
import { setEmailOptions } from "../redux/email/email.actions";
import { store } from "../redux/store";
import client from "../utils/GraphQLClient";
import { TemplateList } from "./TemplateConstants";
import cleanAxios from "./CleanAxios";
import axios from "axios";
import { TemplateList } from "./TemplateConstants";
const server = process.env.REACT_APP_REPORTS_SERVER_URL;
jsreport.serverUrl = server;
@@ -255,7 +254,7 @@ export async function RenderTemplates(
headerpath: `/${bodyshop.imexshopid}/header.html`,
footerpath: `/${bodyshop.imexshopid}/footer.html`,
bodyshop: bodyshop,
offset: moment().utcOffset(),
offset: bodyshop.timezone,
},
};

View File

@@ -907,6 +907,74 @@ export const TemplateList = (type, context) => {
},
group: "sales",
},
hours_sold_detail_closed_estimator: {
title: i18n.t(
"reportcenter.templates.hours_sold_detail_closed_estimator"
),
description: "",
subject: i18n.t(
"reportcenter.templates.hours_sold_detail_closed_estimator"
),
key: "hours_sold_detail_closed_estimator",
//idtype: "vendor",
disabled: false,
rangeFilter: {
object: i18n.t("reportcenter.labels.objects.jobs"),
field: i18n.t("jobs.fields.date_invoiced"),
},
group: "sales",
},
hours_sold_detail_open_estimator: {
title: i18n.t(
"reportcenter.templates.hours_sold_detail_open_estimator"
),
description: "",
subject: i18n.t(
"reportcenter.templates.hours_sold_detail_open_estimator"
),
key: "hours_sold_detail_open_estimator",
//idtype: "vendor",
disabled: false,
rangeFilter: {
object: i18n.t("reportcenter.labels.objects.jobs"),
field: i18n.t("jobs.fields.date_open"),
},
group: "sales",
},
hours_sold_summary_closed_estimator: {
title: i18n.t(
"reportcenter.templates.hours_sold_summary_closed_estimator"
),
description: "",
subject: i18n.t(
"reportcenter.templates.hours_sold_summary_closed_estimator"
),
key: "hours_sold_summary_closed_estimator",
//idtype: "vendor",
disabled: false,
rangeFilter: {
object: i18n.t("reportcenter.labels.objects.jobs"),
field: i18n.t("jobs.fields.date_invoiced"),
},
group: "sales",
},
hours_sold_summary_open_estimator: {
title: i18n.t(
"reportcenter.templates.hours_sold_summary_open_estimator"
),
description: "",
subject: i18n.t(
"reportcenter.templates.hours_sold_summary_open_estimator"
),
key: "hours_sold_summary_open_estimator",
//idtype: "vendor",
disabled: false,
rangeFilter: {
object: i18n.t("reportcenter.labels.objects.jobs"),
field: i18n.t("jobs.fields.date_invoiced"),
},
group: "sales",
},
hours_sold_summary_open_status: {
title: i18n.t(
"reportcenter.templates.hours_sold_summary_open_status"