Merged in release/2022-06-30 (pull request #525)

Release/2022 06 30
This commit is contained in:
Patrick Fic
2022-06-28 22:50:16 +00:00
17 changed files with 339 additions and 79 deletions

View File

@@ -1,4 +1,4 @@
<babeledit_project version="1.2" be_version="2.7.1">
<babeledit_project be_version="2.7.1" version="1.2">
<!--
BabelEdit project file
@@ -2063,6 +2063,32 @@
</concept_node>
</children>
</folder_node>
<folder_node>
<name>validation</name>
<children>
<concept_node>
<name>atleastone</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>
</children>
</folder_node>
</children>
</folder_node>
<folder_node>
@@ -8138,6 +8164,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>default_quote</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>
<name>default_received</name>
<definition_loaded>false</definition_loaded>
@@ -33967,6 +34014,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>is_quote</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>
<name>mark_as_received</name>
<definition_loaded>false</definition_loaded>

View File

@@ -8,6 +8,7 @@
"@asseinfo/react-kanban": "^2.2.0",
"@craco/craco": "^6.4.3",
"@fingerprintjs/fingerprintjs": "^3.3.3",
"@jsreport/browser-client": "^3.1.0",
"@sentry/react": "^7.1.1",
"@sentry/tracing": "^7.1.1",
"@splitsoftware/splitio-react": "^1.4.1",

View File

@@ -552,7 +552,20 @@ export function BillEnterModalLinesComponent({
});
return (
<Form.List name="billlines">
<Form.List
name="billlines"
rules={[
{
validator: async (_, billlines) => {
if (!billlines || billlines.length < 1) {
return Promise.reject(
new Error(t("billlines.validation.atleastone"))
);
}
},
},
]}
>
{(fields, { add, remove, move }) => {
return (
<>

View File

@@ -217,21 +217,31 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
<JobsRelatedRos jobid={job.id} job={job} />
</DataLabel>
{job.vehicle && job.vehicle.notes && (
<DataLabel label={t("vehicles.fields.notes")}>
<span style={{ whiteSpace: "pre" }}>{job.vehicle.notes}</span>
<DataLabel
label={t("vehicles.fields.notes")}
valueStyle={{ whiteSpace: "pre-wrap" }}
>
{job.vehicle.notes}
</DataLabel>
)}
{job.vehicle && job.vehicle.v_paint_codes && (
<DataLabel
label={t("vehicles.fields.v_paint_codes", { number: "" })}
>
<span style={{ whiteSpace: "pre" }}>
{Object.keys(job.vehicle.v_paint_codes)
.filter(
(key) =>
job.vehicle.v_paint_codes[key] !== "" &&
job.vehicle.v_paint_codes[key] !== null &&
job.vehicle.v_paint_codes[key] !== undefined
)
.map((key, idx) => (
<Tag key={idx}>{job.vehicle.v_paint_codes[key]}</Tag>
))}
</span>
</DataLabel>
)}
{
// job.vehicle && job.vehicle.v_paint_codes && (
// <DataLabel label={t("vehicles.fields.v_paint_codes")}>
// <span style={{ whiteSpace: "pre" }}>
// {Object.keys(job.vehicle.v_paint_codes).map((key, idx) => (
// <Tag key={idx}>{job.vehicle.v_paint_codes[key]}</Tag>
// ))}
// </span>
// </DataLabel>
// )
}
</div>
</Card>
</Col>

View File

@@ -290,18 +290,42 @@ export function PartsOrderModalComponent({
>
<Input.TextArea rows={3} />
</Form.Item>
{OEConnection.treatment === "on" && !isReturn && (
<Form.Item
name="is_quote"
label={t("parts_orders.labels.is_quote")}
valuePropName="checked"
>
<Checkbox />
</Form.Item>
)}
<Radio.Group
defaultValue={sendType}
onChange={(e) => setSendType(e.target.value)}
>
<Radio value={"none"}>{t("general.labels.none")}</Radio>
<Radio value={"e"}>{t("parts_orders.labels.email")}</Radio>
<Radio value={"p"}>{t("parts_orders.labels.print")}</Radio>
{OEConnection.treatment === "on" && !isReturn && (
<Radio value={"oec"}>{t("parts_orders.labels.oec")}</Radio>
)}
</Radio.Group>
<Form.Item noStyle shouldUpdate>
{() => {
const is_quote = form.getFieldValue("is_quote");
if (is_quote) setSendType("oec");
return (
<Radio.Group
defaultValue={sendType}
value={sendType}
onChange={(e) => setSendType(e.target.value)}
>
<Radio disabled={is_quote} value={"none"}>
{t("general.labels.none")}
</Radio>
<Radio disabled={is_quote} value={"e"}>
{t("parts_orders.labels.email")}
</Radio>
<Radio disabled={is_quote} value={"p"}>
{t("parts_orders.labels.print")}
</Radio>
{OEConnection.treatment === "on" && !isReturn && (
<Radio value={"oec"}>{t("parts_orders.labels.oec")}</Radio>
)}
</Radio.Group>
);
}}
</Form.Item>
</div>
);
}

View File

@@ -93,31 +93,48 @@ export function PartsOrderModalContainer({
const [updateJobLines] = useMutation(UPDATE_JOB_LINE_STATUS);
const [updateJob] = useMutation(UPDATE_JOB);
const handleFinish = async ({ removefrompartsqueue, ...values }) => {
const handleFinish = async ({
removefrompartsqueue,
is_quote,
...values
}) => {
logImEXEvent("parts_order_insert");
setSaving(true);
const insertResult = await insertPartOrder({
variables: {
po: [
{
...values,
order_date: moment().format("YYYY-MM-DD"),
orderedby: currentUser.email,
jobid: jobId,
user_email: currentUser.email,
return: isReturn,
status: bodyshop.md_order_statuses.default_ordered || "Ordered*",
},
],
},
refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID"],
});
if (!!insertResult.error) {
notification["error"]({
message: t("parts_orders.errors.creating"),
description: JSON.stringify(insertResult.error),
let insertResult;
if (!is_quote) {
await insertPartOrder({
variables: {
po: [
{
...values,
order_date: moment().format("YYYY-MM-DD"),
orderedby: currentUser.email,
jobid: jobId,
user_email: currentUser.email,
return: isReturn,
status: bodyshop.md_order_statuses.default_ordered || "Ordered*",
},
],
},
refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID"],
});
if (!!insertResult.error) {
notification["error"]({
message: t("parts_orders.errors.creating"),
description: JSON.stringify(insertResult.error),
});
return;
}
insertAuditTrail({
jobid: jobId,
operation: isReturn
? AuditTrailMapping.jobspartsreturn(
insertResult.data.insert_parts_orders.returning[0].order_number
)
: AuditTrailMapping.jobspartsorder(
insertResult.data.insert_parts_orders.returning[0].order_number
),
});
return;
}
const jobLinesResult = await updateJobLines({
@@ -127,6 +144,8 @@ export function PartsOrderModalContainer({
.map((item) => item.job_line_id),
status: isReturn
? bodyshop.md_order_statuses.default_returned || "Returned*"
: is_quote
? bodyshop.md_order_statuses.default_quote || "Quote"
: bodyshop.md_order_statuses.default_ordered || "Ordered*",
},
});
@@ -142,17 +161,6 @@ export function PartsOrderModalContainer({
});
}
insertAuditTrail({
jobid: jobId,
operation: isReturn
? AuditTrailMapping.jobspartsreturn(
insertResult.data.insert_parts_orders.returning[0].order_number
)
: AuditTrailMapping.jobspartsorder(
insertResult.data.insert_parts_orders.returning[0].order_number
),
});
if (!!jobLinesResult.errors) {
notification["error"]({
message: t("parts_orders.errors.creating"),

View File

@@ -15,6 +15,7 @@ import {
import { selectBodyshop } from "../../redux/user/user.selectors";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import ScheduleBlockDay from "../schedule-block-day/schedule-block-day.component";
import ScheduleCalendarHeaderGraph from "./schedule-calendar-header-graph.component";
const mapStateToProps = createStructuredSelector({
@@ -66,6 +67,9 @@ export function ScheduleCalendarHeaderComponent({
<td>
<Link to={`/manage/jobs/${j.id}`}>{j.ro_number}</Link>
</td>
<td>
<OwnerNameDisplay ownerObject={j} />
</td>
<td>
{`(${(
j.labhrs.aggregate.sum.mod_lb_hrs +
@@ -99,6 +103,9 @@ export function ScheduleCalendarHeaderComponent({
<td>
<Link to={`/manage/jobs/${j.id}`}>{j.ro_number}</Link>
</td>
<td>
<OwnerNameDisplay ownerObject={j} />
</td>
<td>
{`(${(
j.labhrs.aggregate.sum.mod_lb_hrs +

View File

@@ -2,9 +2,28 @@ import { Form, Input } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
export default function ShopInfoOrderStatusComponent({ form }) {
const { t } = useTranslation();
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { useTreatments } from "@splitsoftware/splitio-react";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(ShopInfoOrderStatusComponent);
export function ShopInfoOrderStatusComponent({ bodyshop, form }) {
const { t } = useTranslation();
const { OEConnection } = useTreatments(
["OEConnection"],
{},
bodyshop.imexshopid
);
return (
<LayoutFormRow header={t("bodyshop.labels.orderstatuses")}>
<Form.Item
@@ -56,6 +75,20 @@ export default function ShopInfoOrderStatusComponent({ form }) {
>
<Input />
</Form.Item>
{OEConnection.treatment === "on" && (
<Form.Item
label={t("bodyshop.fields.statuses.default_quote")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
name={["md_order_statuses", "default_quote"]}
>
<Input />
</Form.Item>
)}
</LayoutFormRow>
);
}

View File

@@ -324,6 +324,9 @@ export const QUERY_SCHEDULE_LOAD_DATA = gql`
ro_number
scheduled_completion
actual_completion
ownr_fn
ownr_ln
ownr_co_nm
labhrs: joblines_aggregate(
where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }
) {
@@ -352,6 +355,9 @@ export const QUERY_SCHEDULE_LOAD_DATA = gql`
id
scheduled_in
ro_number
ownr_fn
ownr_ln
ownr_co_nm
labhrs: joblines_aggregate(
where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }
) {

View File

@@ -17,6 +17,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
import { onlyUnique } from "../../utils/arrayHelper";
import { DateTimeFormatter, TimeAgoFormatter } from "../../utils/DateFormatter";
import { alphaSort, dateSort } from "../../utils/sorters";
import useLocalStorage from "../../utils/useLocalStorage";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -31,6 +32,7 @@ export function PartsQueuePageComponent({ bodyshop }) {
statusFilters,
} = searchParams;
const history = useHistory();
const [filter, setFilter] = useLocalStorage("filter_parts_queue", null);
const { loading, error, data, refetch } = useQuery(QUERY_PARTS_QUEUE, {
fetchPolicy: "network-only",
@@ -92,7 +94,7 @@ export function PartsQueuePageComponent({ bodyshop }) {
// searchParams.page = pagination.current;
searchParams.sortcolumn = sorter.columnKey;
searchParams.sortorder = sorter.order;
setFilter(filters);
history.push({ search: queryString.stringify(searchParams) });
};
@@ -247,6 +249,7 @@ export function PartsQueuePageComponent({ bodyshop }) {
key: "queued_for_parts",
sorter: (a, b) => a.queued_for_parts - b.queued_for_parts,
sortOrder: sortcolumn === "queued_for_parts" && sortorder,
filteredValue: filter.queued_for_parts || null,
filters: [
{
text: "Queued",

View File

@@ -136,6 +136,9 @@
"other": "-- Not On Estimate --",
"reconciled": "Reconciled!",
"unreconciled": "Unreconciled"
},
"validation": {
"atleastone": "At least one bill line must be entered."
}
},
"bills": {
@@ -502,6 +505,7 @@
"default_imported": "Default Imported Status",
"default_invoiced": "Default Invoiced Status",
"default_ordered": "Default Ordered Status",
"default_quote": "Default Quote Status",
"default_received": "Default Received Status",
"default_returned": "Default Returned",
"default_scheduled": "Default Scheduled Status",
@@ -2013,6 +2017,7 @@
"confirmdelete": "Are you sure you want to delete this item? It cannot be recovered. Job line statuses will not be updated and may require manual review. ",
"email": "Send by Email",
"inthisorder": "Parts in this Order",
"is_quote": "Parts Quote?",
"mark_as_received": "Mark as Received?",
"newpartsorder": "New Parts Order",
"notyetordered": "This part has not yet been ordered.",

View File

@@ -136,6 +136,9 @@
"other": "",
"reconciled": "",
"unreconciled": ""
},
"validation": {
"atleastone": ""
}
},
"bills": {
@@ -502,6 +505,7 @@
"default_imported": "",
"default_invoiced": "",
"default_ordered": "",
"default_quote": "",
"default_received": "",
"default_returned": "",
"default_scheduled": "",
@@ -2013,6 +2017,7 @@
"confirmdelete": "",
"email": "Enviar por correo electrónico",
"inthisorder": "Partes en este pedido",
"is_quote": "",
"mark_as_received": "",
"newpartsorder": "",
"notyetordered": "",

View File

@@ -136,6 +136,9 @@
"other": "",
"reconciled": "",
"unreconciled": ""
},
"validation": {
"atleastone": ""
}
},
"bills": {
@@ -502,6 +505,7 @@
"default_imported": "",
"default_invoiced": "",
"default_ordered": "",
"default_quote": "",
"default_received": "",
"default_returned": "",
"default_scheduled": "",
@@ -2013,6 +2017,7 @@
"confirmdelete": "",
"email": "Envoyé par email",
"inthisorder": "Pièces dans cette commande",
"is_quote": "",
"mark_as_received": "",
"newpartsorder": "",
"notyetordered": "",

View File

@@ -1,7 +1,7 @@
import { gql } from "@apollo/client";
import { notification } from "antd";
import axios from "axios";
import jsreport from "jsreport-browser-client-dist";
import jsreport from "@jsreport/browser-client";
import _ from "lodash";
import moment from "moment";
import { auth } from "../firebase/firebase.utils";
@@ -9,7 +9,9 @@ import { setEmailOptions } from "../redux/email/email.actions";
import { store } from "../redux/store";
import client from "../utils/GraphQLClient";
import { TemplateList } from "./TemplateConstants";
const server = process.env.REACT_APP_REPORTS_SERVER_URL;
jsreport.serverUrl = server;
const Templates = TemplateList();
@@ -21,6 +23,10 @@ export default async function RenderTemplate(
renderAsExcel = false,
renderAsText = false
) {
if (window.jsr3) {
jsreport.serverUrl = "https://reports3.test.imex.online/";
}
//Query assets that match the template name. Must be in format <<templateName>>.query
let { contextData, useShopSpecificTemplate } = await fetchContextData(
templateObject
@@ -69,7 +75,7 @@ export default async function RenderTemplate(
};
try {
const render = await jsreport.renderAsync(reportRequest);
const render = await jsreport.render(reportRequest);
if (!renderAsHtml) {
render.download(
@@ -104,7 +110,7 @@ export default async function RenderTemplate(
},
};
const pdfRender = await jsreport.renderAsync(pdfRequest);
const pdfRender = await jsreport.render(pdfRequest);
pdf = pdfRender.toDataURI();
}
return new Promise((resolve, reject) => {
@@ -152,6 +158,9 @@ export async function RenderTemplates(
// (template) => template.name === item.templateObject.name
// );
// });
if (window.jsr3) {
jsreport.serverUrl = "https://reports3.test.imex.online/";
}
unsortedTemplatesAndData.sort(function (a, b) {
return (
@@ -242,7 +251,7 @@ export async function RenderTemplates(
};
try {
const render = await jsreport.renderAsync(reportRequest);
const render = await jsreport.render(reportRequest);
if (!renderAsHtml) {
render.download("Speed Print");
} else {

View File

@@ -0,0 +1,40 @@
import { useState } from "react";
export default function useLocalStorage(key, initialValue) {
// State to store our value
// Pass initial state function to useState so logic is only executed once
const [storedValue, setStoredValue] = useState(() => {
if (typeof window === "undefined") {
return initialValue;
}
try {
// Get from local storage by key
const item = window.localStorage.getItem(key);
// Parse stored json or if none return initialValue
return item ? JSON.parse(item) : initialValue;
} catch (error) {
// If error also return initialValue
console.log(error);
return initialValue;
}
});
// Return a wrapped version of useState's setter function that ...
// ... persists the new value to localStorage.
const setValue = (value) => {
try {
// Allow value to be a function so we have same API as useState
const valueToStore =
value instanceof Function ? value(storedValue) : value;
// Save state
setStoredValue(valueToStore);
// Save to local storage
if (typeof window !== "undefined") {
window.localStorage.setItem(key, JSON.stringify(valueToStore));
}
} catch (error) {
// A more advanced implementation would handle the error case
console.log(error);
}
};
return [storedValue, setValue];
}

View File

@@ -1918,6 +1918,11 @@
"@types/yargs" "^16.0.0"
chalk "^4.0.0"
"@jsreport/browser-client@^3.1.0":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@jsreport/browser-client/-/browser-client-3.1.0.tgz#a84011087ca8a29a6dc6a852fa05ffaf1983a679"
integrity sha512-ZElwn2KRIzkUzAyD5UKGxULZUhokWuPOlMzrmiur4WirqH3yoiHlOJEdnRGkjjE/fhZzCR8gBFZ/TuOW/fsOIw==
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"

View File

@@ -39,7 +39,7 @@ exports.default = async (req, res) => {
const { bodyshops } = await client.request(queries.GET_AUTOHOUSE_SHOPS);
const specificShopIds = req.body.bodyshopIds; // ['uuid]
const { start, end } = req.body; //YYYY-MM-DD
const { start, end, skipUpload } = req.body; //YYYY-MM-DD
const allxmlsToUpload = [];
const allErrors = [];
try {
@@ -107,7 +107,7 @@ exports.default = async (req, res) => {
} catch (error) {
//Error at the shop level.
logger.log("autohouse-error-shop", "ERROR", "api", bodyshop.id, {
error,
...error,
});
allErrors.push({
@@ -125,17 +125,29 @@ exports.default = async (req, res) => {
}
}
// for (const xmlObj of allxmlsToUpload) {
// fs.writeFileSync(`./logs/${xmlObj.filename}`, xmlObj.xml);
// }
if (skipUpload) {
for (const xmlObj of allxmlsToUpload) {
fs.writeFileSync(`./logs/${xmlObj.filename}`, xmlObj.xml);
}
// res.json(allxmlsToUpload);
// return;
res.json(allxmlsToUpload);
sendServerEmail({
subject: `Autohouse Report ${moment().format("MM-DD-YY")}`,
text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))}
Uploaded: ${JSON.stringify(
allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count })),
null,
2
)}
`,
});
return;
}
let sftp = new Client();
sftp.on("error", (errors) =>
logger.log("autohouse-sftp-error", "ERROR", "api", null, {
errors,
...errors,
})
);
try {
@@ -160,7 +172,7 @@ exports.default = async (req, res) => {
//***TODO Change filing naming when creating the cron job. IM_ShopInternalName_DDMMYYYY_HHMMSS.xml
} catch (error) {
logger.log("autohouse-sftp-error", "ERROR", "api", null, {
error,
...error,
});
} finally {
sftp.end();
@@ -169,7 +181,7 @@ exports.default = async (req, res) => {
subject: `Autohouse Report ${moment().format("MM-DD-YY")}`,
text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))}
Uploaded: ${JSON.stringify(
allxmlsToUpload.map((x) => x.filename),
allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count })),
null,
2
)}
@@ -185,7 +197,11 @@ const CreateRepairOrderTag = (job, errorCallback) => {
//Level 2
if (!job.job_totals) {
errorCallback({ job, error: { toString: () => "No job totals for RO." } });
errorCallback({
jobid: jobid,
ro_number: job.ro_number,
error: { toString: () => "No job totals for RO." },
});
return {};
}
@@ -658,7 +674,7 @@ const CreateRepairOrderTag = (job, errorCallback) => {
error,
});
errorCallback({ job, error });
errorCallback({ jobid: jobid, ro_number: job.ro_number, error });
}
};
@@ -889,7 +905,9 @@ const GenerateDetailLines = (job, line, statuses) => {
OriginalCost: null,
OriginalInvoiceNumber: null,
PriceEach: line.act_price || 0,
PartNumber: _.escape(line.oem_partno.replace(/[^\x00-\x7F]/g, "")),
PartNumber: line.oem_partno
? line.oem_partno.replace(/[^\x00-\x7F]/g, "")
: "",
ProfitPercent: null,
PurchaseOrderNumber: null,
Qty: line.part_qty || 0,