Compare commits
103 Commits
feature/IO
...
feature/IO
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8422ea83ae | ||
|
|
1b84087ef8 | ||
|
|
a9fdf3da18 | ||
|
|
fa2c729ac2 | ||
|
|
95bb5b03c2 | ||
|
|
318482c195 | ||
|
|
eea9e8e2cc | ||
|
|
cde12f9970 | ||
|
|
48def2b74d | ||
|
|
dde7a99956 | ||
|
|
49fb2caac0 | ||
|
|
df964aa14e | ||
|
|
7619360f37 | ||
|
|
f15f371e86 | ||
|
|
34fe0cc3bf | ||
|
|
7acaefb5c5 | ||
|
|
ab02da47a2 | ||
|
|
673670eeb4 | ||
|
|
2a7dec90d5 | ||
|
|
6e0b1f65a7 | ||
|
|
8671d1254d | ||
|
|
0ea254ed4e | ||
|
|
331dcfc063 | ||
|
|
c46804cfdf | ||
|
|
484d09d635 | ||
|
|
188a7b47b1 | ||
|
|
a6ca93f482 | ||
|
|
d08bfc61cd | ||
|
|
e6100851b8 | ||
|
|
e9795072d5 | ||
|
|
9b4de1645e | ||
|
|
503c217c99 | ||
|
|
2333067e02 | ||
|
|
953172493e | ||
|
|
b444639fca | ||
|
|
6ee7e56b9b | ||
|
|
ffd5acb21a | ||
|
|
0340ca5fcc | ||
|
|
1b2fc8b114 | ||
|
|
3745d7a414 | ||
|
|
a0efac9bd8 | ||
|
|
17a772563c | ||
|
|
b1ce356bd8 | ||
|
|
9818cac30e | ||
|
|
171277630e | ||
|
|
d8b400cb8c | ||
|
|
fe7bf684aa | ||
|
|
7e6c97b3cf | ||
|
|
773f3d4c84 | ||
|
|
9c6fe1905d | ||
|
|
2126cccff1 | ||
|
|
40d5e02415 | ||
|
|
5b891281d1 | ||
|
|
56559dd3ff | ||
|
|
fde137d7f7 | ||
|
|
b797bf7dc9 | ||
|
|
37c3be5cde | ||
|
|
b87d1a65fe | ||
|
|
35c832dbc3 | ||
|
|
019b3cf4da | ||
|
|
27f4385539 | ||
|
|
ad520ab23e | ||
|
|
b3716521ec | ||
|
|
05ae0801e5 | ||
|
|
332ade96e5 | ||
|
|
3acec55c0e | ||
|
|
da0462f14c | ||
|
|
2cc9fa961e | ||
|
|
2646e85863 | ||
|
|
1b6fe4d18e | ||
|
|
22aae0a7f1 | ||
|
|
71043313d6 | ||
|
|
c9620a3f6f | ||
|
|
cfbd6f93c3 | ||
|
|
cdfae5a429 | ||
|
|
db1b701a96 | ||
|
|
2746421c09 | ||
|
|
5217120994 | ||
|
|
77f72a2a12 | ||
|
|
a84ad4ee32 | ||
|
|
2cacd75822 | ||
|
|
217a0b84ac | ||
|
|
f53ed8c427 | ||
|
|
f8b7588a04 | ||
|
|
ee3cb4456d | ||
|
|
ae05692c46 | ||
|
|
e01a2af5a4 | ||
|
|
b9f398cf2d | ||
|
|
1e44d4fe42 | ||
|
|
0f42875d1b | ||
|
|
a0f1299006 | ||
|
|
87d8a5d746 | ||
|
|
268851902a | ||
|
|
68bb7d2529 | ||
|
|
d50db12330 | ||
|
|
1438986c18 | ||
|
|
c047699fbb | ||
|
|
e5b7fcb919 | ||
|
|
55023ceaca | ||
|
|
45e143578c | ||
|
|
28a41f7637 | ||
|
|
2a2edeadb9 | ||
|
|
52c9b9a290 |
@@ -13,4 +13,5 @@
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
bodyshop_translations.babel
|
||||
.env.localstack.docker
|
||||
bodyshop_translations.babel
|
||||
|
||||
949
client/package-lock.json
generated
949
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -8,41 +8,45 @@
|
||||
"private": true,
|
||||
"proxy": "http://localhost:4000",
|
||||
"dependencies": {
|
||||
"@amplitude/analytics-browser": "^2.34.0",
|
||||
"@amplitude/analytics-browser": "^2.35.0",
|
||||
"@ant-design/pro-layout": "^7.22.6",
|
||||
"@apollo/client": "^4.1.3",
|
||||
"@apollo/client": "^4.1.4",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/modifiers": "^9.0.0",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@emotion/is-prop-valid": "^1.4.0",
|
||||
"@fingerprintjs/fingerprintjs": "^5.0.1",
|
||||
"@firebase/analytics": "^0.10.19",
|
||||
"@firebase/app": "^0.14.7",
|
||||
"@firebase/app": "^0.14.8",
|
||||
"@firebase/auth": "^1.12.0",
|
||||
"@firebase/firestore": "^4.10.0",
|
||||
"@firebase/firestore": "^4.11.0",
|
||||
"@firebase/messaging": "^0.12.22",
|
||||
"@jsreport/browser-client": "^3.1.0",
|
||||
"@reduxjs/toolkit": "^2.11.2",
|
||||
"@sentry/cli": "^3.1.0",
|
||||
"@sentry/react": "^10.38.0",
|
||||
"@sentry/vite-plugin": "^4.8.0",
|
||||
"@sentry/cli": "^3.2.0",
|
||||
"@sentry/react": "^10.39.0",
|
||||
"@sentry/vite-plugin": "^4.9.1",
|
||||
"@splitsoftware/splitio-react": "^2.6.1",
|
||||
"@tanem/react-nprogress": "^5.0.58",
|
||||
"antd": "^6.2.2",
|
||||
"@tanem/react-nprogress": "^5.0.63",
|
||||
"antd": "^6.3.0",
|
||||
"apollo-link-logger": "^3.0.0",
|
||||
"autosize": "^6.0.1",
|
||||
"axios": "^1.13.4",
|
||||
"axios": "^1.13.5",
|
||||
"classnames": "^2.5.1",
|
||||
"css-box-model": "^1.2.1",
|
||||
"dayjs": "^1.11.19",
|
||||
"dayjs-business-days2": "^1.3.2",
|
||||
"dinero.js": "^1.9.1",
|
||||
"dotenv": "^17.2.3",
|
||||
"dotenv": "^17.3.1",
|
||||
"env-cmd": "^11.0.0",
|
||||
"exifr": "^7.1.3",
|
||||
"graphql": "^16.12.0",
|
||||
"graphql-ws": "^6.0.7",
|
||||
"i18next": "^25.8.0",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"i18next": "^25.8.11",
|
||||
"i18next-browser-languagedetector": "^8.2.1",
|
||||
"immutability-helper": "^3.1.1",
|
||||
"libphonenumber-js": "^1.12.36",
|
||||
"libphonenumber-js": "^1.12.37",
|
||||
"lightningcss": "^1.31.1",
|
||||
"logrocket": "^12.0.0",
|
||||
"markerjs2": "^2.32.7",
|
||||
@@ -50,7 +54,7 @@
|
||||
"normalize-url": "^8.1.1",
|
||||
"object-hash": "^3.0.0",
|
||||
"phone": "^3.1.70",
|
||||
"posthog-js": "^1.336.4",
|
||||
"posthog-js": "^1.351.1",
|
||||
"prop-types": "^15.8.1",
|
||||
"query-string": "^9.3.1",
|
||||
"raf-schd": "^4.0.3",
|
||||
@@ -59,7 +63,6 @@
|
||||
"react-color": "^2.19.3",
|
||||
"react-cookie": "^8.0.1",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-drag-listview": "^2.0.0",
|
||||
"react-grid-gallery": "^1.0.1",
|
||||
"react-grid-layout": "^2.2.2",
|
||||
"react-i18next": "^16.5.4",
|
||||
@@ -84,7 +87,7 @@
|
||||
"rxjs": "^7.8.2",
|
||||
"sass": "^1.97.3",
|
||||
"socket.io-client": "^4.8.3",
|
||||
"styled-components": "^6.3.8",
|
||||
"styled-components": "^6.3.10",
|
||||
"vite-plugin-ejs": "^1.7.0",
|
||||
"web-vitals": "^5.1.0"
|
||||
},
|
||||
@@ -141,11 +144,11 @@
|
||||
"@emotion/babel-plugin": "^11.13.5",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@playwright/test": "^1.58.0",
|
||||
"@playwright/test": "^1.58.2",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@vitejs/plugin-react": "^5.1.2",
|
||||
"@vitejs/plugin-react": "^5.1.4",
|
||||
"babel-plugin-react-compiler": "^1.0.0",
|
||||
"browserslist": "^4.28.1",
|
||||
"browserslist-to-esbuild": "^2.1.1",
|
||||
@@ -153,16 +156,16 @@
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-compiler": "^19.1.0-rc.2",
|
||||
"globals": "^17.2.0",
|
||||
"jsdom": "^27.4.0",
|
||||
"globals": "^17.3.0",
|
||||
"jsdom": "^28.1.0",
|
||||
"memfs": "^4.56.10",
|
||||
"os-browserify": "^0.3.0",
|
||||
"playwright": "^1.58.0",
|
||||
"playwright": "^1.58.2",
|
||||
"react-error-overlay": "^6.1.0",
|
||||
"redux-logger": "^3.0.6",
|
||||
"source-map-explorer": "^2.5.3",
|
||||
"vite": "^7.3.1",
|
||||
"vite-plugin-babel": "^1.4.1",
|
||||
"vite-plugin-babel": "^1.5.1",
|
||||
"vite-plugin-eslint": "^1.8.1",
|
||||
"vite-plugin-node-polyfills": "^0.25.0",
|
||||
"vite-plugin-pwa": "^1.2.0",
|
||||
|
||||
@@ -446,3 +446,32 @@
|
||||
//.rbc-time-header-gutter {
|
||||
// padding: 0;
|
||||
//}
|
||||
|
||||
/* globally allow shrink inside table cells */
|
||||
.prod-list-table .ant-table-cell,
|
||||
.prod-list-table .ant-table-cell > * {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* common AntD offenders */
|
||||
.prod-list-table > .ant-table-cell .ant-space,
|
||||
.ant-table-cell .ant-space-item {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Keep your custom header content on the left, push AntD sorter to the far right */
|
||||
.prod-list-table .ant-table-column-sorters {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.prod-list-table .ant-table-column-title {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0; /* allows ellipsis to work */
|
||||
}
|
||||
|
||||
.prod-list-table .ant-table-column-sorter {
|
||||
margin-left: auto;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
@@ -182,7 +182,7 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, bills, ref
|
||||
<Table
|
||||
loading={loading}
|
||||
dataSource={dataSource}
|
||||
pagination={{ placement: "top", pageSize: exportPageLimit }}
|
||||
pagination={{ placement: "top", pageSize: exportPageLimit, showSizeChanger: false }}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
onChange={handleTableChange}
|
||||
|
||||
@@ -195,7 +195,7 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, payments,
|
||||
<Table
|
||||
loading={loading}
|
||||
dataSource={dataSource}
|
||||
pagination={{ placement: "top", pageSize: exportPageLimit }}
|
||||
pagination={{ placement: "top", pageSize: exportPageLimit, showSizeChanger: false }}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
onChange={handleTableChange}
|
||||
|
||||
@@ -212,7 +212,7 @@ export function AccountingReceivablesTableComponent({ bodyshop, loading, jobs, r
|
||||
<Table
|
||||
loading={loading}
|
||||
dataSource={dataSource}
|
||||
pagination={{ placement: "top", pageSize: exportPageLimit }}
|
||||
pagination={{ placement: "top", pageSize: exportPageLimit, showSizeChanger: false }}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
onChange={handleTableChange}
|
||||
|
||||
@@ -29,19 +29,18 @@ export function AllocationsAssignmentComponent({
|
||||
<Select
|
||||
id="employeeSelector"
|
||||
showSearch={{
|
||||
optionFilterProp: "children",
|
||||
filterOption: (input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
optionFilterProp: "label",
|
||||
filterOption: (input, option) => option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}}
|
||||
style={{ width: 200 }}
|
||||
placeholder="Select a person"
|
||||
onChange={onChange}
|
||||
>
|
||||
{bodyshop.employees.map((emp) => (
|
||||
<Select.Option value={emp.id} key={emp.id}>
|
||||
{`${emp.first_name} ${emp.last_name}`}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
options={bodyshop.employees.map((emp) => ({
|
||||
value: emp.id,
|
||||
key: emp.id,
|
||||
label: `${emp.first_name} ${emp.last_name}`
|
||||
}))}
|
||||
/>
|
||||
<InputNumber
|
||||
defaultValue={assignment.hours}
|
||||
placeholder={t("joblines.fields.mod_lb_hrs")}
|
||||
|
||||
@@ -31,19 +31,17 @@ export default connect(
|
||||
<div>
|
||||
<Select
|
||||
showSearch={{
|
||||
optionFilterProp: "children",
|
||||
filterOption: (input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
optionFilterProp: "label",
|
||||
filterOption: (input, option) => option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}}
|
||||
style={{ width: 200 }}
|
||||
placeholder="Select a person"
|
||||
onChange={onChange}
|
||||
>
|
||||
{bodyshop.employees.map((emp) => (
|
||||
<Select.Option value={emp.id} key={emp.id}>
|
||||
{`${emp.first_name} ${emp.last_name}`}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
options={bodyshop.employees.map((emp) => ({
|
||||
value: emp.id,
|
||||
label: `${emp.first_name} ${emp.last_name}`
|
||||
}))}
|
||||
/>
|
||||
|
||||
<Button type="primary" disabled={!assignment.employeeid} onClick={handleAssignment}>
|
||||
Assign
|
||||
|
||||
@@ -52,6 +52,9 @@ export default function BillCmdReturnsTableComponent({ form, returnLoading, retu
|
||||
{fields.map((field, index) => (
|
||||
<tr key={field.key}>
|
||||
<td>
|
||||
<Form.Item hidden key={`${index}id`} name={[field.name, "id"]}>
|
||||
<ReadOnlyFormItemComponent />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
// label={t("joblines.fields.line_desc")}
|
||||
key={`${index}line_desc`}
|
||||
|
||||
@@ -99,20 +99,22 @@ export function BillFormItemsExtendedFormItem({
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item label={t("billlines.fields.cost_center")} name={["billlineskeys", record.id, "cost_center"]}>
|
||||
<Select showSearch style={{ minWidth: "3rem" }} disabled={disabled}>
|
||||
{bodyshopHasDmsKey(bodyshop)
|
||||
? CiecaSelect(true, false)
|
||||
: responsibilityCenters.costs.map((item) => <Select.Option key={item.name}>{item.name}</Select.Option>)}
|
||||
</Select>
|
||||
<Select
|
||||
showSearch
|
||||
style={{ minWidth: "3rem" }}
|
||||
disabled={disabled}
|
||||
options={
|
||||
bodyshopHasDmsKey(bodyshop)
|
||||
? CiecaSelect(true, false)
|
||||
: responsibilityCenters.costs.map((item) => ({ value: item.name, label: item.name }))
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label={t("billlines.fields.location")} name={["billlineskeys", record.id, "location"]}>
|
||||
<Select disabled={disabled}>
|
||||
{bodyshop.md_parts_locations.map((loc, idx) => (
|
||||
<Select.Option key={idx} value={loc}>
|
||||
{loc}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select
|
||||
disabled={disabled}
|
||||
options={bodyshop.md_parts_locations.map((loc) => ({ value: loc, label: loc }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("billlines.fields.deductedfromlbr")}
|
||||
@@ -136,22 +138,10 @@ export function BillFormItemsExtendedFormItem({
|
||||
]}
|
||||
name={["billlineskeys", record.id, "lbr_adjustment", "mod_lbr_ty"]}
|
||||
>
|
||||
<Select allowClear>
|
||||
<Select.Option value="LAA">{t("joblines.fields.lbr_types.LAA")}</Select.Option>
|
||||
<Select.Option value="LAB">{t("joblines.fields.lbr_types.LAB")}</Select.Option>
|
||||
<Select.Option value="LAD">{t("joblines.fields.lbr_types.LAD")}</Select.Option>
|
||||
<Select.Option value="LAE">{t("joblines.fields.lbr_types.LAE")}</Select.Option>
|
||||
<Select.Option value="LAF">{t("joblines.fields.lbr_types.LAF")}</Select.Option>
|
||||
<Select.Option value="LAG">{t("joblines.fields.lbr_types.LAG")}</Select.Option>
|
||||
<Select.Option value="LAM">{t("joblines.fields.lbr_types.LAM")}</Select.Option>
|
||||
<Select.Option value="LAR">{t("joblines.fields.lbr_types.LAR")}</Select.Option>
|
||||
<Select.Option value="LAS">{t("joblines.fields.lbr_types.LAS")}</Select.Option>
|
||||
<Select.Option value="LAU">{t("joblines.fields.lbr_types.LAU")}</Select.Option>
|
||||
<Select.Option value="LA1">{t("joblines.fields.lbr_types.LA1")}</Select.Option>
|
||||
<Select.Option value="LA2">{t("joblines.fields.lbr_types.LA2")}</Select.Option>
|
||||
<Select.Option value="LA3">{t("joblines.fields.lbr_types.LA3")}</Select.Option>
|
||||
<Select.Option value="LA4">{t("joblines.fields.lbr_types.LA4")}</Select.Option>
|
||||
</Select>
|
||||
<Select
|
||||
allowClear
|
||||
options={CiecaSelect(false, true)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.labels.adjustmentrate")}
|
||||
|
||||
@@ -328,13 +328,12 @@ export function BillFormComponent({
|
||||
</Form.Item>
|
||||
{!billEdit && (
|
||||
<Form.Item label={t("bills.fields.allpartslocation")} name="location">
|
||||
<Select style={{ width: "10rem" }} disabled={disabled} allowClear>
|
||||
{bodyshop.md_parts_locations.map((loc, idx) => (
|
||||
<Select.Option key={idx} value={loc}>
|
||||
{loc}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select
|
||||
style={{ width: "10rem" }}
|
||||
disabled={disabled}
|
||||
allowClear
|
||||
options={bodyshop.md_parts_locations.map((loc) => ({ value: loc, label: loc }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
</LayoutFormRow>
|
||||
@@ -373,9 +372,11 @@ export function BillFormComponent({
|
||||
"local_tax_rate"
|
||||
]);
|
||||
let totals;
|
||||
if (!!values.total && !!values.billlines && values.billlines.length > 0)
|
||||
if (!!values.total && !!values.billlines && values.billlines.length > 0) {
|
||||
totals = CalculateBillTotal(values);
|
||||
if (totals)
|
||||
}
|
||||
|
||||
if (totals) {
|
||||
return (
|
||||
// TODO: Align is not correct
|
||||
// eslint-disable-next-line react/no-unknown-property
|
||||
@@ -414,7 +415,7 @@ export function BillFormComponent({
|
||||
<Statistic
|
||||
title={t("bills.labels.discrepancy")}
|
||||
styles={{
|
||||
value: {
|
||||
content: {
|
||||
color: totals.discrepancy.getAmount() === 0 ? "green" : "red"
|
||||
}
|
||||
}}
|
||||
@@ -427,6 +428,7 @@ export function BillFormComponent({
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
</Form.Item>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { DeleteFilled, DollarCircleFilled } from "@ant-design/icons";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { Button, Checkbox, Form, Input, InputNumber, Select, Space, Switch, Table, Tooltip } from "antd";
|
||||
import { useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
@@ -32,6 +33,7 @@ export function BillEnterModalLinesComponent({
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { setFieldsValue, getFieldsValue, getFieldValue } = form;
|
||||
const firstFieldRefs = useRef({});
|
||||
|
||||
const CONTROL_HEIGHT = 32;
|
||||
|
||||
@@ -90,6 +92,7 @@ export function BillEnterModalLinesComponent({
|
||||
});
|
||||
};
|
||||
|
||||
// Only fill actual_cost when the user forward-tabs out of Retail (actual_price)
|
||||
const autofillActualCost = (index) => {
|
||||
Promise.resolve().then(() => {
|
||||
const retailRaw = form.getFieldValue(["billlines", index, "actual_price"]);
|
||||
@@ -154,6 +157,9 @@ export function BillEnterModalLinesComponent({
|
||||
),
|
||||
formInput: (record, index) => (
|
||||
<BillLineSearchSelect
|
||||
ref={(el) => {
|
||||
firstFieldRefs.current[index] = el;
|
||||
}}
|
||||
disabled={disabled}
|
||||
options={lineData}
|
||||
style={{
|
||||
@@ -164,10 +170,9 @@ export function BillEnterModalLinesComponent({
|
||||
}}
|
||||
allowRemoved={form.getFieldValue("is_credit_memo") || false}
|
||||
onSelect={(value, opt) => {
|
||||
const d = normalizeDiscount(discount);
|
||||
const retail = Number(opt.cost);
|
||||
const computedActual = Number.isFinite(retail) ? round2(retail * (1 - d)) : null;
|
||||
|
||||
// IMPORTANT:
|
||||
// Do NOT autofill actual_cost here. It should only fill when the user forward-tabs
|
||||
// from Retail (actual_price) -> Actual Cost (actual_cost).
|
||||
setFieldsValue({
|
||||
billlines: (getFieldValue("billlines") || []).map((item, idx) => {
|
||||
if (idx !== index) return item;
|
||||
@@ -178,7 +183,7 @@ export function BillEnterModalLinesComponent({
|
||||
quantity: opt.part_qty || 1,
|
||||
actual_price: opt.cost,
|
||||
original_actual_price: opt.cost,
|
||||
actual_cost: isBlank(item.actual_cost) ? computedActual : item.actual_cost,
|
||||
// actual_cost intentionally untouched here
|
||||
cost_center: opt.part_type
|
||||
? bodyshopHasDmsKey(bodyshop)
|
||||
? opt.part_type !== "PAE"
|
||||
@@ -205,7 +210,7 @@ export function BillEnterModalLinesComponent({
|
||||
label: t("billlines.fields.line_desc"),
|
||||
rules: [{ required: true }]
|
||||
}),
|
||||
formInput: () => <Input.TextArea disabled={disabled} autoSize />
|
||||
formInput: () => <Input.TextArea disabled={disabled} autoSize tabIndex={0} />
|
||||
},
|
||||
{
|
||||
title: t("billlines.fields.quantity"),
|
||||
@@ -234,7 +239,7 @@ export function BillEnterModalLinesComponent({
|
||||
})
|
||||
]
|
||||
}),
|
||||
formInput: () => <InputNumber precision={0} min={1} disabled={disabled} />
|
||||
formInput: () => <InputNumber precision={0} min={1} disabled={disabled} tabIndex={0} />
|
||||
},
|
||||
{
|
||||
title: t("billlines.fields.actual_price"),
|
||||
@@ -251,9 +256,10 @@ export function BillEnterModalLinesComponent({
|
||||
<CurrencyInput
|
||||
min={0}
|
||||
disabled={disabled}
|
||||
onBlur={() => autofillActualCost(index)}
|
||||
tabIndex={0}
|
||||
// NOTE: Autofill should only happen on forward Tab out of Retail
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Tab") autofillActualCost(index);
|
||||
if (e.key === "Tab" && !e.shiftKey) autofillActualCost(index);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
@@ -328,6 +334,7 @@ export function BillEnterModalLinesComponent({
|
||||
min={0}
|
||||
disabled={disabled}
|
||||
controls={false}
|
||||
tabIndex={0}
|
||||
style={{ width: "100%", height: CONTROL_HEIGHT }}
|
||||
onFocus={() => autofillActualCost(index)}
|
||||
/>
|
||||
@@ -392,11 +399,17 @@ export function BillEnterModalLinesComponent({
|
||||
rules: [{ required: true }]
|
||||
}),
|
||||
formInput: () => (
|
||||
<Select showSearch style={{ minWidth: "3rem" }} disabled={disabled}>
|
||||
{bodyshopHasDmsKey(bodyshop)
|
||||
? CiecaSelect(true, false)
|
||||
: responsibilityCenters.costs.map((item) => <Select.Option key={item.name}>{item.name}</Select.Option>)}
|
||||
</Select>
|
||||
<Select
|
||||
showSearch
|
||||
style={{ minWidth: "3rem" }}
|
||||
disabled={disabled}
|
||||
tabIndex={0}
|
||||
options={
|
||||
bodyshopHasDmsKey(bodyshop)
|
||||
? CiecaSelect(true, false)
|
||||
: responsibilityCenters.costs.map((item) => ({ value: item.name, label: item.name }))
|
||||
}
|
||||
/>
|
||||
)
|
||||
},
|
||||
...(billEdit
|
||||
@@ -412,13 +425,11 @@ export function BillEnterModalLinesComponent({
|
||||
name: [field.name, "location"]
|
||||
}),
|
||||
formInput: () => (
|
||||
<Select disabled={disabled}>
|
||||
{bodyshop.md_parts_locations.map((loc, idx) => (
|
||||
<Select.Option key={idx} value={loc}>
|
||||
{loc}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select
|
||||
disabled={disabled}
|
||||
tabIndex={0}
|
||||
options={bodyshop.md_parts_locations.map((loc) => ({ value: loc, label: loc }))}
|
||||
/>
|
||||
)
|
||||
}
|
||||
]),
|
||||
@@ -432,7 +443,7 @@ export function BillEnterModalLinesComponent({
|
||||
key: `${field.name}deductedfromlbr`,
|
||||
name: [field.name, "deductedfromlbr"]
|
||||
}),
|
||||
formInput: () => <Switch disabled={disabled} />,
|
||||
formInput: () => <Switch disabled={disabled} tabIndex={0} />,
|
||||
additional: (record, index) => (
|
||||
<Form.Item shouldUpdate noStyle style={{ display: "inline-block" }}>
|
||||
{() => {
|
||||
@@ -459,22 +470,10 @@ export function BillEnterModalLinesComponent({
|
||||
rules={[{ required: true }]}
|
||||
name={[record.name, "lbr_adjustment", "mod_lbr_ty"]}
|
||||
>
|
||||
<Select allowClear>
|
||||
<Select.Option value="LAA">{t("joblines.fields.lbr_types.LAA")}</Select.Option>
|
||||
<Select.Option value="LAB">{t("joblines.fields.lbr_types.LAB")}</Select.Option>
|
||||
<Select.Option value="LAD">{t("joblines.fields.lbr_types.LAD")}</Select.Option>
|
||||
<Select.Option value="LAE">{t("joblines.fields.lbr_types.LAE")}</Select.Option>
|
||||
<Select.Option value="LAF">{t("joblines.fields.lbr_types.LAF")}</Select.Option>
|
||||
<Select.Option value="LAG">{t("joblines.fields.lbr_types.LAG")}</Select.Option>
|
||||
<Select.Option value="LAM">{t("joblines.fields.lbr_types.LAM")}</Select.Option>
|
||||
<Select.Option value="LAR">{t("joblines.fields.lbr_types.LAR")}</Select.Option>
|
||||
<Select.Option value="LAS">{t("joblines.fields.lbr_types.LAS")}</Select.Option>
|
||||
<Select.Option value="LAU">{t("joblines.fields.lbr_types.LAU")}</Select.Option>
|
||||
<Select.Option value="LA1">{t("joblines.fields.lbr_types.LA1")}</Select.Option>
|
||||
<Select.Option value="LA2">{t("joblines.fields.lbr_types.LA2")}</Select.Option>
|
||||
<Select.Option value="LA3">{t("joblines.fields.lbr_types.LA3")}</Select.Option>
|
||||
<Select.Option value="LA4">{t("joblines.fields.lbr_types.LA4")}</Select.Option>
|
||||
</Select>
|
||||
<Select
|
||||
allowClear
|
||||
options={CiecaSelect(false, true)}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{Enhanced_Payroll.treatment === "on" ? (
|
||||
@@ -517,9 +516,13 @@ export function BillEnterModalLinesComponent({
|
||||
formItemProps: (field) => ({
|
||||
key: `${field.name}fedtax`,
|
||||
valuePropName: "checked",
|
||||
name: [field.name, "applicable_taxes", "federal"]
|
||||
name: [field.name, "applicable_taxes", "federal"],
|
||||
initialValue: InstanceRenderManager({
|
||||
imex: true,
|
||||
rome: false
|
||||
})
|
||||
}),
|
||||
formInput: () => <Switch disabled={disabled} />
|
||||
formInput: () => <Switch disabled={disabled} tabIndex={0} />
|
||||
}
|
||||
]
|
||||
}),
|
||||
@@ -534,7 +537,7 @@ export function BillEnterModalLinesComponent({
|
||||
valuePropName: "checked",
|
||||
name: [field.name, "applicable_taxes", "state"]
|
||||
}),
|
||||
formInput: () => <Switch disabled={disabled} />
|
||||
formInput: () => <Switch disabled={disabled} tabIndex={0} />
|
||||
},
|
||||
|
||||
...InstanceRenderManager({
|
||||
@@ -550,7 +553,7 @@ export function BillEnterModalLinesComponent({
|
||||
valuePropName: "checked",
|
||||
name: [field.name, "applicable_taxes", "local"]
|
||||
}),
|
||||
formInput: () => <Switch disabled={disabled} />
|
||||
formInput: () => <Switch disabled={disabled} tabIndex={0} />
|
||||
}
|
||||
]
|
||||
}),
|
||||
@@ -570,6 +573,7 @@ export function BillEnterModalLinesComponent({
|
||||
icon={<DeleteFilled />}
|
||||
disabled={disabled || invLen > 0}
|
||||
onClick={() => remove(record.name)}
|
||||
tabIndex={0}
|
||||
/>
|
||||
|
||||
{Simple_Inventory.treatment === "on" && (
|
||||
@@ -641,12 +645,19 @@ export function BillEnterModalLinesComponent({
|
||||
<Button
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
const newIndex = fields.length;
|
||||
add(
|
||||
InstanceRenderManager({
|
||||
imex: { applicable_taxes: { federal: true } },
|
||||
rome: { applicable_taxes: { federal: false } }
|
||||
})
|
||||
);
|
||||
setTimeout(() => {
|
||||
const firstField = firstFieldRefs.current[newIndex];
|
||||
if (firstField?.focus) {
|
||||
firstField.focus();
|
||||
}
|
||||
}, 100);
|
||||
}}
|
||||
style={{ width: "100%" }}
|
||||
>
|
||||
|
||||
@@ -57,7 +57,6 @@ const CardPaymentModalComponent = ({
|
||||
QUERY_RO_AND_OWNER_BY_JOB_PKS,
|
||||
{
|
||||
fetchPolicy: "network-only",
|
||||
notifyOnNetworkStatusChange: true
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -47,7 +47,6 @@ export function ChatPopupComponent({ chatVisible, selectedConversation, toggleCh
|
||||
const [getConversations, { loading, data, refetch, called }] = useLazyQuery(CONVERSATION_LIST_QUERY, {
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only",
|
||||
notifyOnNetworkStatusChange: true,
|
||||
...(pollInterval > 0 ? { pollInterval } : {})
|
||||
});
|
||||
|
||||
|
||||
@@ -19,13 +19,12 @@ export default function ChatTagRoComponent({ roOptions, loading, handleSearch, h
|
||||
placeholder={t("general.labels.search")}
|
||||
onSelect={handleInsertTag}
|
||||
notFoundContent={loading ? <LoadingOutlined /> : <Empty />}
|
||||
>
|
||||
{roOptions.map((item, idx) => (
|
||||
<Select.Option key={item.id || idx}>
|
||||
{` ${item.ro_number || ""} | ${OwnerNameDisplayFunction(item)}`}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
options={roOptions.map((item, idx) => ({
|
||||
key: item.id || idx,
|
||||
value: item.id || idx,
|
||||
label: ` ${item.ro_number || ""} | ${OwnerNameDisplayFunction(item)}`
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
{loading ? <LoadingOutlined /> : null}
|
||||
|
||||
|
||||
@@ -309,13 +309,13 @@ export function ContractConvertToRo({ bodyshop, currentUser, contract, disabled
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select>
|
||||
{bodyshop.md_ins_cos.map((s) => (
|
||||
<Select.Option key={s.name} value={s.name}>
|
||||
{s.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select
|
||||
options={bodyshop.md_ins_cos.map((s) => ({
|
||||
key: s.name,
|
||||
value: s.name,
|
||||
label: s.name
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={"class"}
|
||||
@@ -327,13 +327,13 @@ export function ContractConvertToRo({ bodyshop, currentUser, contract, disabled
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select>
|
||||
{bodyshop.md_classes.map((s) => (
|
||||
<Select.Option key={s} value={s}>
|
||||
{s}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select
|
||||
options={bodyshop.md_classes.map((s) => ({
|
||||
key: s,
|
||||
value: s,
|
||||
label: s
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.labels.convertform.applycleanupcharge")}
|
||||
|
||||
@@ -2,8 +2,6 @@ import { useEffect, useState } from "react";
|
||||
import { Select } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
const ContractStatusComponent = ({ value, onChange, ref }) => {
|
||||
const [option, setOption] = useState(value);
|
||||
const { t } = useTranslation();
|
||||
@@ -15,11 +13,17 @@ const ContractStatusComponent = ({ value, onChange, ref }) => {
|
||||
}, [value, option, onChange]);
|
||||
|
||||
return (
|
||||
<Select ref={ref} value={option} style={{ width: 100 }} onChange={setOption}>
|
||||
<Option value="contracts.status.new">{t("contracts.status.new")}</Option>
|
||||
<Option value="contracts.status.out">{t("contracts.status.out")}</Option>
|
||||
<Option value="contracts.status.returned">{t("contracts.status.out")}</Option>
|
||||
</Select>
|
||||
<Select
|
||||
ref={ref}
|
||||
value={option}
|
||||
style={{ width: 100 }}
|
||||
onChange={setOption}
|
||||
options={[
|
||||
{ value: "contracts.status.new", label: t("contracts.status.new") },
|
||||
{ value: "contracts.status.out", label: t("contracts.status.out") },
|
||||
{ value: "contracts.status.returned", label: t("contracts.status.out") }
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -2,8 +2,6 @@ import { Select } from "antd";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
const CourtesyCarReadinessComponent = ({ value, onChange, ref }) => {
|
||||
const [option, setOption] = useState(value);
|
||||
const { t } = useTranslation();
|
||||
@@ -23,10 +21,11 @@ const CourtesyCarReadinessComponent = ({ value, onChange, ref }) => {
|
||||
width: 100
|
||||
}}
|
||||
onChange={setOption}
|
||||
>
|
||||
<Option value="courtesycars.readiness.ready">{t("courtesycars.readiness.ready")}</Option>
|
||||
<Option value="courtesycars.readiness.notready">{t("courtesycars.readiness.notready")}</Option>
|
||||
</Select>
|
||||
options={[
|
||||
{ value: "courtesycars.readiness.ready", label: t("courtesycars.readiness.ready") },
|
||||
{ value: "courtesycars.readiness.notready", label: t("courtesycars.readiness.notready") }
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export default CourtesyCarReadinessComponent;
|
||||
|
||||
@@ -2,8 +2,6 @@ import { useEffect, useState } from "react";
|
||||
import { Select } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
const CourtesyCarStatusComponent = ({ value, onChange, ref }) => {
|
||||
const [option, setOption] = useState(value);
|
||||
const { t } = useTranslation();
|
||||
@@ -22,14 +20,15 @@ const CourtesyCarStatusComponent = ({ value, onChange, ref }) => {
|
||||
width: 100
|
||||
}}
|
||||
onChange={setOption}
|
||||
>
|
||||
<Option value="courtesycars.status.in">{t("courtesycars.status.in")}</Option>
|
||||
<Option value="courtesycars.status.inservice">{t("courtesycars.status.inservice")}</Option>
|
||||
<Option value="courtesycars.status.out">{t("courtesycars.status.out")}</Option>
|
||||
<Option value="courtesycars.status.sold">{t("courtesycars.status.sold")}</Option>
|
||||
<Option value="courtesycars.status.leasereturn">{t("courtesycars.status.leasereturn")}</Option>
|
||||
<Option value="courtesycars.status.unavailable">{t("courtesycars.status.unavailable")}</Option>
|
||||
</Select>
|
||||
options={[
|
||||
{ value: "courtesycars.status.in", label: t("courtesycars.status.in") },
|
||||
{ value: "courtesycars.status.inservice", label: t("courtesycars.status.inservice") },
|
||||
{ value: "courtesycars.status.out", label: t("courtesycars.status.out") },
|
||||
{ value: "courtesycars.status.sold", label: t("courtesycars.status.sold") },
|
||||
{ value: "courtesycars.status.leasereturn", label: t("courtesycars.status.leasereturn") },
|
||||
{ value: "courtesycars.status.unavailable", label: t("courtesycars.status.unavailable") }
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export default CourtesyCarStatusComponent;
|
||||
|
||||
@@ -36,7 +36,7 @@ export function DashboardTotalProductionHours({ bodyshop, data, ...cardProps })
|
||||
<Statistic
|
||||
title={t("dashboard.labels.prodhrs")}
|
||||
value={hours.total.toFixed(1)}
|
||||
styles={{ value: { color: aboveTargetHours ? "green" : "red" } }}
|
||||
styles={{ content: { color: aboveTargetHours ? "green" : "red" } }}
|
||||
/>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
@@ -23,13 +23,13 @@ export default connect(mapStateToProps, mapDispatchToProps)(DmsCustomerSelector)
|
||||
* @constructor
|
||||
*/
|
||||
export function DmsCustomerSelector(props) {
|
||||
const { bodyshop, jobid, socket, rrOptions = {} } = props;
|
||||
const { bodyshop, jobid, job, socket, rrOptions = {} } = props;
|
||||
|
||||
// Centralized "mode" (provider + transport)
|
||||
const mode = props.mode;
|
||||
|
||||
// Stable base props for children
|
||||
const base = useMemo(() => ({ bodyshop, jobid, socket }), [bodyshop, jobid, socket]);
|
||||
const base = useMemo(() => ({ bodyshop, jobid, job, socket }), [bodyshop, jobid, job, socket]);
|
||||
|
||||
switch (mode) {
|
||||
case DMS_MAP.reynolds: {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Alert, Button, Checkbox, Col, message, Space, Table } from "antd";
|
||||
import { Alert, Button, Checkbox, message, Modal, Space, Table } from "antd";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
@@ -47,6 +47,7 @@ const rrAddressToString = (addr) => {
|
||||
export default function RRCustomerSelector({
|
||||
jobid,
|
||||
socket,
|
||||
job,
|
||||
rrOpenRoLimit = false,
|
||||
onRrOpenRoFinished,
|
||||
rrValidationPending = false,
|
||||
@@ -59,15 +60,26 @@ export default function RRCustomerSelector({
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
// Show dialog automatically when validation is pending
|
||||
// BUT: skip this for early RO flow (job already has dms_id)
|
||||
useEffect(() => {
|
||||
if (rrValidationPending) setOpen(true);
|
||||
}, [rrValidationPending]);
|
||||
if (rrValidationPending && !job?.dms_id) {
|
||||
setOpen(true);
|
||||
}
|
||||
}, [rrValidationPending, job?.dms_id]);
|
||||
|
||||
// Listen for RR customer selection list
|
||||
useEffect(() => {
|
||||
if (!socket) return;
|
||||
const handleRrSelectCustomer = (list) => {
|
||||
const normalized = normalizeRrList(list);
|
||||
|
||||
// If list is empty, it means early RO exists and customer selection should be skipped
|
||||
// Don't open the modal in this case
|
||||
if (normalized.length === 0) {
|
||||
setRefreshing(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setOpen(true);
|
||||
setCustomerList(normalized);
|
||||
const firstOwner = normalized.find((r) => r.vinOwner)?.custNo;
|
||||
@@ -127,6 +139,10 @@ export default function RRCustomerSelector({
|
||||
});
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const refreshRrSearch = () => {
|
||||
setRefreshing(true);
|
||||
const to = setTimeout(() => setRefreshing(false), 12000);
|
||||
@@ -141,8 +157,6 @@ export default function RRCustomerSelector({
|
||||
socket.emit("rr-export-job", { jobId: jobid });
|
||||
};
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
const columns = [
|
||||
{ title: t("jobs.fields.dms.id"), dataIndex: "custNo", key: "custNo" },
|
||||
{
|
||||
@@ -169,8 +183,45 @@ export default function RRCustomerSelector({
|
||||
return !rrOwnerSet.has(String(record.custNo));
|
||||
};
|
||||
|
||||
// For early RO flow: show validation banner even when modal is closed
|
||||
if (!open) {
|
||||
if (rrValidationPending && job?.dms_id) {
|
||||
return (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
title="Complete Validation in Reynolds"
|
||||
description={
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
<div>
|
||||
We created the Repair Order. Please validate the totals and taxes in the DMS system. When done,
|
||||
click <strong>Finished</strong> to finalize and mark this export as complete.
|
||||
</div>
|
||||
<div>
|
||||
<Space>
|
||||
<Button type="primary" onClick={onValidationFinished}>
|
||||
Finished
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Col span={24}>
|
||||
<Modal
|
||||
open={open}
|
||||
onCancel={handleClose}
|
||||
footer={null}
|
||||
width={800}
|
||||
title={t("dms.selectCustomer")}
|
||||
>
|
||||
<Table
|
||||
title={() => (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
@@ -196,8 +247,8 @@ export default function RRCustomerSelector({
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Validation step banner */}
|
||||
{rrValidationPending && (
|
||||
{/* Validation step banner - only show for NON-early RO flow (legacy) */}
|
||||
{rrValidationPending && !job?.dms_id && (
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
@@ -262,6 +313,6 @@ export default function RRCustomerSelector({
|
||||
getCheckboxProps: (record) => ({ disabled: rrDisableRow(record) })
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ export function DmsLogEvents({
|
||||
return {
|
||||
key: idx,
|
||||
color: logLevelColor(level),
|
||||
children: (
|
||||
content: (
|
||||
<Space orientation="vertical" size={4} style={{ display: "flex" }}>
|
||||
{/* Row 1: summary + inline "Details" toggle */}
|
||||
<Space wrap align="start">
|
||||
@@ -113,7 +113,7 @@ export function DmsLogEvents({
|
||||
[logs, openSet, colorizeJson, isDarkMode, showDetails]
|
||||
);
|
||||
|
||||
return <Timeline pending reverse items={items} />;
|
||||
return <Timeline reverse items={items} />;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -272,11 +272,19 @@ export default function CdkLikePostForm({ bodyshop, socket, job, logsRef, mode,
|
||||
name={[field.name, "name"]}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select style={{ width: "100%" }} onSelect={(value) => handlePayerSelect(value, index)}>
|
||||
{bodyshop.cdk_configuration?.payers?.map((payer) => (
|
||||
<Select.Option key={payer.name}>{payer.name}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select
|
||||
showSearch={{
|
||||
optionFilterProp: "label",
|
||||
filterOption: (input, option) => option.label.toLowerCase().includes(input.toLowerCase())
|
||||
}}
|
||||
style={{ width: "100%" }}
|
||||
onSelect={(value) => handlePayerSelect(value, index)}
|
||||
options={bodyshop.cdk_configuration?.payers?.map((payer) => ({
|
||||
key: payer.name,
|
||||
value: payer.name,
|
||||
label: payer.name
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
@@ -404,7 +412,7 @@ export default function CdkLikePostForm({ bodyshop, socket, job, logsRef, mode,
|
||||
<Typography.Title>=</Typography.Title>
|
||||
<Statistic
|
||||
title={t("jobs.labels.dms.notallocated")}
|
||||
styles={{ value: { color: discrep.getAmount() === 0 ? "green" : "red" } }}
|
||||
styles={{ content: { color: discrep.getAmount() === 0 ? "green" : "red" } }}
|
||||
value={discrep.toFormat()}
|
||||
/>
|
||||
<Button disabled={disablePost} htmlType="submit">
|
||||
|
||||
@@ -208,8 +208,18 @@ export default function RRPostForm({
|
||||
});
|
||||
};
|
||||
|
||||
// Check if early RO was created (job has all early RO fields)
|
||||
const hasEarlyRO = !!(job?.dms_id && job?.dms_customer_id && job?.dms_advisor_id);
|
||||
|
||||
return (
|
||||
<Card title={t("jobs.labels.dms.postingform")}>
|
||||
{hasEarlyRO && (
|
||||
<Typography.Paragraph type="success" strong style={{ marginBottom: 16 }}>
|
||||
✅ {t("jobs.labels.dms.earlyro.created")} {job.dms_id}
|
||||
<br />
|
||||
<Typography.Text type="secondary">{t("jobs.labels.dms.earlyro.willupdate")}</Typography.Text>
|
||||
</Typography.Paragraph>
|
||||
)}
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
@@ -218,96 +228,96 @@ export default function RRPostForm({
|
||||
initialValues={initialValues}
|
||||
>
|
||||
<Row gutter={[16, 12]} align="bottom">
|
||||
{/* Advisor + inline Refresh */}
|
||||
<Col xs={24} sm={24} md={12} lg={8}>
|
||||
<Form.Item label={t("jobs.fields.dms.advisor")} required>
|
||||
<Space.Compact block>
|
||||
<Form.Item
|
||||
name="advisorNo"
|
||||
noStyle
|
||||
rules={[{ required: true, message: t("general.validation.required") }]}
|
||||
>
|
||||
<Select
|
||||
style={{ flex: 1 }}
|
||||
loading={advLoading}
|
||||
allowClear
|
||||
placeholder={t("general.actions.select", "Select...")}
|
||||
popupMatchSelectWidth
|
||||
options={advisors
|
||||
.map((a) => {
|
||||
const value = getAdvisorNumber(a);
|
||||
if (value == null) return null;
|
||||
return { value: String(value), label: getAdvisorLabel(a) || String(value) };
|
||||
})
|
||||
.filter(Boolean)}
|
||||
notFoundContent={advLoading ? t("general.labels.loading") : t("general.labels.none")}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Tooltip title={t("general.actions.refresh")}>
|
||||
<Button
|
||||
aria-label={t("general.actions.refresh")}
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={() => fetchRrAdvisors(true)}
|
||||
loading={advLoading}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Space.Compact>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
{/* RR OpCode (prefix / base / suffix) */}
|
||||
<Col xs={24} sm={12} md={12} lg={8}>
|
||||
<Form.Item
|
||||
required
|
||||
label={
|
||||
<Space size="small" align="center">
|
||||
{t("jobs.fields.dms.rr_opcode", "RR OpCode")}
|
||||
{isCustomOpCode && (
|
||||
{/* Advisor + inline Refresh - Only show if no early RO */}
|
||||
{!hasEarlyRO && (
|
||||
<Col xs={24} sm={24} md={12} lg={8}>
|
||||
<Form.Item label={t("jobs.fields.dms.advisor")} required>
|
||||
<Space.Compact block>
|
||||
<Form.Item
|
||||
name="advisorNo"
|
||||
noStyle
|
||||
rules={[{ required: true, message: t("general.validation.required") }]}
|
||||
>
|
||||
<Select
|
||||
style={{ flex: 1 }}
|
||||
loading={advLoading}
|
||||
allowClear
|
||||
placeholder={t("general.actions.select", "Select...")}
|
||||
popupMatchSelectWidth
|
||||
options={advisors
|
||||
.map((a) => {
|
||||
const value = getAdvisorNumber(a);
|
||||
if (value == null) return null;
|
||||
return { value: String(value), label: getAdvisorLabel(a) || String(value) };
|
||||
})
|
||||
.filter(Boolean)}
|
||||
notFoundContent={advLoading ? t("general.labels.loading") : t("general.labels.none")}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Tooltip title={t("general.actions.refresh")}>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<RollbackOutlined />}
|
||||
onClick={handleResetOpCode}
|
||||
style={{ padding: 0 }}
|
||||
>
|
||||
{t("jobs.fields.dms.rr_opcode_reset", "Reset")}
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Space.Compact block>
|
||||
<Form.Item name="opPrefix" noStyle>
|
||||
<Input
|
||||
allowClear
|
||||
maxLength={4}
|
||||
style={{ width: "30%" }}
|
||||
placeholder={t("jobs.fields.dms.rr_opcode_prefix", "Prefix")}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="opBase"
|
||||
noStyle
|
||||
rules={[{ required: true, message: t("general.validation.required") }]}
|
||||
>
|
||||
<Input
|
||||
allowClear
|
||||
maxLength={10}
|
||||
style={{ width: "40%" }}
|
||||
placeholder={t("jobs.fields.dms.rr_opcode_base", "Base")}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="opSuffix" noStyle>
|
||||
<Input
|
||||
allowClear
|
||||
maxLength={4}
|
||||
style={{ width: "30%" }}
|
||||
placeholder={t("jobs.fields.dms.rr_opcode_suffix", "Suffix")}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Space.Compact>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
aria-label={t("general.actions.refresh")}
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={() => fetchRrAdvisors(true)}
|
||||
loading={advLoading}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Space.Compact>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
)}
|
||||
|
||||
{/* RR OpCode (prefix / base / suffix) - Only show if no early RO */}
|
||||
{!hasEarlyRO && (
|
||||
<Col xs={24} sm={12} md={12} lg={8}>
|
||||
<Form.Item
|
||||
required
|
||||
label={
|
||||
<Space size="small" align="center">
|
||||
{t("jobs.fields.dms.rr_opcode", "RR OpCode")}
|
||||
{isCustomOpCode && (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<RollbackOutlined />}
|
||||
onClick={handleResetOpCode}
|
||||
style={{ padding: 0 }}
|
||||
>
|
||||
{t("jobs.fields.dms.rr_opcode_reset", "Reset")}
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Space.Compact block>
|
||||
<Form.Item name="opPrefix" noStyle>
|
||||
<Input
|
||||
allowClear
|
||||
maxLength={4}
|
||||
style={{ width: "30%" }}
|
||||
placeholder={t("jobs.fields.dms.rr_opcode_prefix", "Prefix")}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="opBase" noStyle rules={[{ required: true }]}>
|
||||
<Input
|
||||
allowClear
|
||||
maxLength={10}
|
||||
style={{ width: "40%" }}
|
||||
placeholder={t("jobs.fields.dms.rr_opcode_base", "Base")}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="opSuffix" noStyle>
|
||||
<Input
|
||||
allowClear
|
||||
maxLength={4}
|
||||
style={{ width: "30%" }}
|
||||
placeholder={t("jobs.fields.dms.rr_opcode_suffix", "Suffix")}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Space.Compact>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
)}
|
||||
|
||||
<Col xs={12} sm={8} md={6} lg={4}>
|
||||
<Form.Item name="kmin" label={t("jobs.fields.kmin")} initialValue={job?.kmin} rules={[{ required: true }]}>
|
||||
@@ -355,13 +365,14 @@ export default function RRPostForm({
|
||||
{/* Validation */}
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
const advisorOk = !!form.getFieldValue("advisorNo");
|
||||
// When early RO exists, advisor is already set, so we don't need to validate it
|
||||
const advisorOk = hasEarlyRO ? true : !!form.getFieldValue("advisorNo");
|
||||
return (
|
||||
<Space size="large" wrap align="center">
|
||||
<Statistic title={t("jobs.labels.subtotal")} value={totals.totalSale.toFormat()} />
|
||||
<Typography.Title>=</Typography.Title>
|
||||
<Button disabled={!advisorOk} htmlType="submit">
|
||||
{t("jobs.actions.dms.post")}
|
||||
<Button disabled={!advisorOk} htmlType="submit" type={hasEarlyRO ? "default" : "primary"}>
|
||||
{hasEarlyRO ? t("jobs.actions.dms.update_ro") : t("jobs.actions.dms.post")}
|
||||
</Button>
|
||||
</Space>
|
||||
);
|
||||
|
||||
367
client/src/components/dms-post-form/rr-early-ro-form.jsx
Normal file
367
client/src/components/dms-post-form/rr-early-ro-form.jsx
Normal file
@@ -0,0 +1,367 @@
|
||||
import { ReloadOutlined } from "@ant-design/icons";
|
||||
import { Alert, Button, Form, Input, InputNumber, Modal, Radio, Select, Space, Table, Typography } from "antd";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
// Simple customer selector table
|
||||
function CustomerSelectorTable({ customers, onSelect, isSubmitting }) {
|
||||
const [selectedCustNo, setSelectedCustNo] = useState(null);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: "Select",
|
||||
key: "select",
|
||||
width: 80,
|
||||
render: (_, record) => (
|
||||
<Radio checked={selectedCustNo === record.custNo} onChange={() => setSelectedCustNo(record.custNo)} />
|
||||
)
|
||||
},
|
||||
{ title: "Customer ID", dataIndex: "custNo", key: "custNo" },
|
||||
{ title: "Name", dataIndex: "name", key: "name" },
|
||||
{
|
||||
title: "VIN Owner",
|
||||
key: "vinOwner",
|
||||
render: (_, record) => (record.vinOwner || record.isVehicleOwner ? "Yes" : "No")
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Table columns={columns} dataSource={customers} rowKey="custNo" pagination={false} size="small" />
|
||||
<div style={{ marginTop: 16, display: "flex", gap: 8 }}>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => onSelect(selectedCustNo, false)}
|
||||
disabled={!selectedCustNo || isSubmitting}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
Use Selected Customer
|
||||
</Button>
|
||||
<Button onClick={() => onSelect(null, true)} disabled={isSubmitting} loading={isSubmitting}>
|
||||
Create New Customer
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* RR Early RO Creation Form
|
||||
* Used from convert button or admin page to create minimal RO before full export
|
||||
* @param bodyshop
|
||||
* @param socket
|
||||
* @param job
|
||||
* @param onSuccess - callback when RO is created successfully
|
||||
* @param onCancel - callback to close modal
|
||||
* @param showCancelButton - whether to show cancel button
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
export default function RREarlyROForm({ bodyshop, socket, job, onSuccess, onCancel, showCancelButton = true }) {
|
||||
const [form] = Form.useForm();
|
||||
|
||||
// Advisors
|
||||
const [advisors, setAdvisors] = useState([]);
|
||||
const [advLoading, setAdvLoading] = useState(false);
|
||||
|
||||
// Customer selection
|
||||
const [customerCandidates, setCustomerCandidates] = useState([]);
|
||||
const [showCustomerSelector, setShowCustomerSelector] = useState(false);
|
||||
|
||||
// Loading and success states
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [earlyRoCreated, setEarlyRoCreated] = useState(!!job?.dms_id);
|
||||
const [createdRoNumber, setCreatedRoNumber] = useState(job?.dms_id || null);
|
||||
|
||||
// Derive default OpCode parts from bodyshop config (matching dms.container.jsx logic)
|
||||
const initialValues = useMemo(() => {
|
||||
const cfg = bodyshop?.rr_configuration || {};
|
||||
const defaults =
|
||||
cfg.opCodeDefault ||
|
||||
cfg.op_code_default ||
|
||||
cfg.op_codes?.default ||
|
||||
cfg.defaults?.opCode ||
|
||||
cfg.defaults ||
|
||||
cfg.default ||
|
||||
{};
|
||||
|
||||
const prefix = defaults.prefix ?? defaults.opCodePrefix ?? "";
|
||||
const base = defaults.base ?? defaults.opCodeBase ?? "";
|
||||
const suffix = defaults.suffix ?? defaults.opCodeSuffix ?? "";
|
||||
|
||||
return {
|
||||
kmin: job?.kmin || 0,
|
||||
opPrefix: prefix,
|
||||
opBase: base,
|
||||
opSuffix: suffix
|
||||
};
|
||||
}, [bodyshop, job]);
|
||||
|
||||
const getAdvisorNumber = (a) => a?.advisorId;
|
||||
const getAdvisorLabel = (a) => `${a?.firstName || ""} ${a?.lastName || ""}`.trim();
|
||||
|
||||
const fetchRrAdvisors = (refresh = false) => {
|
||||
if (!socket) return;
|
||||
setAdvLoading(true);
|
||||
|
||||
const onResult = (payload) => {
|
||||
try {
|
||||
const list = payload?.result ?? payload ?? [];
|
||||
setAdvisors(Array.isArray(list) ? list : []);
|
||||
} finally {
|
||||
setAdvLoading(false);
|
||||
socket.off("rr-get-advisors:result", onResult);
|
||||
}
|
||||
};
|
||||
|
||||
socket.once("rr-get-advisors:result", onResult);
|
||||
socket.emit("rr-get-advisors", { departmentType: "B", refresh }, (ack) => {
|
||||
if (ack?.ok) {
|
||||
const list = ack.result ?? [];
|
||||
setAdvisors(Array.isArray(list) ? list : []);
|
||||
} else if (ack) {
|
||||
console.error("Error fetching RR Advisors:", ack.error);
|
||||
}
|
||||
setAdvLoading(false);
|
||||
socket.off("rr-get-advisors:result", onResult);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchRrAdvisors(false);
|
||||
}, [bodyshop?.id, socket]);
|
||||
|
||||
const handleStartEarlyRO = async (values) => {
|
||||
if (!socket) {
|
||||
console.error("Socket not available");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
|
||||
const txEnvelope = {
|
||||
advisorNo: values.advisorNo,
|
||||
story: values.story || "",
|
||||
kmin: values.kmin || job?.kmin || 0,
|
||||
opPrefix: values.opPrefix || "",
|
||||
opBase: values.opBase || "",
|
||||
opSuffix: values.opSuffix || ""
|
||||
};
|
||||
|
||||
// Emit the early RO creation request
|
||||
socket.emit("rr-create-early-ro", {
|
||||
jobId: job.id,
|
||||
txEnvelope
|
||||
});
|
||||
|
||||
// Wait for customer selection
|
||||
const customerListener = (candidates) => {
|
||||
console.log("Received rr-select-customer event with candidates:", candidates);
|
||||
setCustomerCandidates(candidates || []);
|
||||
setShowCustomerSelector(true);
|
||||
setIsSubmitting(false);
|
||||
socket.off("rr-select-customer", customerListener);
|
||||
};
|
||||
|
||||
socket.once("rr-select-customer", customerListener);
|
||||
|
||||
// Handle failures
|
||||
const failureListener = (payload) => {
|
||||
if (payload?.jobId === job.id) {
|
||||
console.error("Early RO creation failed:", payload.error);
|
||||
alert(`Failed to create early RO: ${payload.error}`);
|
||||
setIsSubmitting(false);
|
||||
setShowCustomerSelector(false);
|
||||
socket.off("export-failed", failureListener);
|
||||
socket.off("rr-select-customer", customerListener);
|
||||
}
|
||||
};
|
||||
|
||||
socket.once("export-failed", failureListener);
|
||||
};
|
||||
|
||||
const handleCustomerSelected = (custNo, createNew = false) => {
|
||||
if (!socket) return;
|
||||
|
||||
console.log("handleCustomerSelected called:", { custNo, createNew, custNoType: typeof custNo });
|
||||
|
||||
setIsSubmitting(true);
|
||||
setShowCustomerSelector(false);
|
||||
|
||||
const payload = {
|
||||
jobId: job.id,
|
||||
custNo: createNew ? null : custNo,
|
||||
create: createNew
|
||||
};
|
||||
|
||||
console.log("Emitting rr-early-customer-selected:", payload);
|
||||
|
||||
// Emit customer selection
|
||||
socket.emit("rr-early-customer-selected", payload, (ack) => {
|
||||
console.log("Received ack from rr-early-customer-selected:", ack);
|
||||
setIsSubmitting(false);
|
||||
|
||||
if (ack?.ok) {
|
||||
const roNumber = ack.dmsRoNo || ack.outsdRoNo;
|
||||
setEarlyRoCreated(true);
|
||||
setCreatedRoNumber(roNumber);
|
||||
onSuccess?.({ roNumber, ...ack });
|
||||
} else {
|
||||
alert(`Failed to create early RO: ${ack?.error || "Unknown error"}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Also listen for socket events
|
||||
const successListener = (payload) => {
|
||||
if (payload?.jobId === job.id) {
|
||||
const roNumber = payload.dmsRoNo || payload.outsdRoNo;
|
||||
console.log("Early RO created:", roNumber);
|
||||
socket.off("rr-early-ro-created", successListener);
|
||||
socket.off("export-failed", failureListener);
|
||||
}
|
||||
};
|
||||
|
||||
const failureListener = (payload) => {
|
||||
if (payload?.jobId === job.id) {
|
||||
console.error("Early RO creation failed:", payload.error);
|
||||
setIsSubmitting(false);
|
||||
setEarlyRoCreated(false);
|
||||
socket.off("rr-early-ro-created", successListener);
|
||||
socket.off("export-failed", failureListener);
|
||||
}
|
||||
};
|
||||
|
||||
socket.once("rr-early-ro-created", successListener);
|
||||
socket.once("export-failed", failureListener);
|
||||
};
|
||||
|
||||
// If early RO already created, show success message
|
||||
if (earlyRoCreated) {
|
||||
return (
|
||||
<Alert
|
||||
title="Early Reynolds RO Created"
|
||||
description={`RO Number: ${createdRoNumber || "N/A"} - You can now convert the job.`}
|
||||
type="success"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// If showing customer selector, render modal
|
||||
if (showCustomerSelector) {
|
||||
return (
|
||||
<>
|
||||
<Typography.Title level={5}>Create Early Reynolds RO</Typography.Title>
|
||||
<Typography.Paragraph type="secondary">Waiting for customer selection...</Typography.Paragraph>
|
||||
|
||||
<Modal
|
||||
title="Select Customer for Early RO"
|
||||
open={true}
|
||||
width={800}
|
||||
footer={null}
|
||||
onCancel={() => {
|
||||
setShowCustomerSelector(false);
|
||||
setIsSubmitting(false);
|
||||
}}
|
||||
>
|
||||
<CustomerSelectorTable
|
||||
customers={customerCandidates}
|
||||
onSelect={handleCustomerSelected}
|
||||
isSubmitting={isSubmitting}
|
||||
/>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Handle manual submit (since we can't nest forms)
|
||||
const handleManualSubmit = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
handleStartEarlyRO(values);
|
||||
} catch (error) {
|
||||
console.error("Validation failed:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Show the form
|
||||
return (
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<Typography.Title level={5}>Create Early Reynolds RO</Typography.Title>
|
||||
<Typography.Paragraph type="secondary" style={{ fontSize: "12px" }}>
|
||||
Complete this section to create a minimal RO in Reynolds before converting the job.
|
||||
</Typography.Paragraph>
|
||||
|
||||
<Form form={form} layout="vertical" component={false} initialValues={initialValues}>
|
||||
<Form.Item name="advisorNo" label="Advisor" rules={[{ required: true, message: "Please select an advisor" }]}>
|
||||
<Select
|
||||
showSearch={{
|
||||
optionFilterProp: "children",
|
||||
filterOption: (input, option) => (option?.children?.toLowerCase() ?? "").includes(input.toLowerCase())
|
||||
}}
|
||||
loading={advLoading}
|
||||
placeholder="Select advisor..."
|
||||
popupRender={(menu) => (
|
||||
<>
|
||||
{menu}
|
||||
<Button
|
||||
type="link"
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={() => fetchRrAdvisors(true)}
|
||||
style={{ width: "100%", textAlign: "left" }}
|
||||
>
|
||||
Refresh Advisors
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
>
|
||||
{advisors.map((adv) => (
|
||||
<Select.Option key={getAdvisorNumber(adv)} value={getAdvisorNumber(adv)}>
|
||||
{getAdvisorLabel(adv)}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="kmin"
|
||||
label="Mileage In"
|
||||
rules={[
|
||||
{ required: true, message: "Please enter initial mileage" },
|
||||
{ type: "number", min: 1, message: "Mileage must be greater than 0" }
|
||||
]}
|
||||
>
|
||||
<InputNumber min={1} style={{ width: "100%" }} />
|
||||
</Form.Item>
|
||||
|
||||
{/* RR OpCode (prefix / base / suffix) */}
|
||||
<Form.Item required label="RR OpCode">
|
||||
<Space.Compact block>
|
||||
<Form.Item name="opPrefix" noStyle>
|
||||
<Input allowClear maxLength={4} style={{ width: "30%" }} placeholder="Prefix" />
|
||||
</Form.Item>
|
||||
<Form.Item name="opBase" noStyle rules={[{ required: true, message: "Base Required" }]}>
|
||||
<Input allowClear maxLength={10} style={{ width: "40%" }} placeholder="Base" />
|
||||
</Form.Item>
|
||||
<Form.Item name="opSuffix" noStyle>
|
||||
<Input allowClear maxLength={4} style={{ width: "30%" }} placeholder="Suffix" />
|
||||
</Form.Item>
|
||||
</Space.Compact>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="story" label="Comments / Story (Optional)">
|
||||
<Input.TextArea rows={2} maxLength={240} showCount placeholder="Enter comments or story..." />
|
||||
</Form.Item>
|
||||
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<Space>
|
||||
<Button type="primary" onClick={handleManualSubmit} loading={isSubmitting} disabled={advLoading}>
|
||||
Create Early RO
|
||||
</Button>
|
||||
{showCancelButton && <Button onClick={onCancel}>Cancel</Button>}
|
||||
</Space>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
33
client/src/components/dms-post-form/rr-early-ro-modal.jsx
Normal file
33
client/src/components/dms-post-form/rr-early-ro-modal.jsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Modal } from "antd";
|
||||
import RREarlyROForm from "./rr-early-ro-form";
|
||||
|
||||
/**
|
||||
* Modal wrapper for RR Early RO Creation Form
|
||||
* @param open - boolean to control modal visibility
|
||||
* @param onClose - callback when modal is closed
|
||||
* @param onSuccess - callback when RO is created successfully
|
||||
* @param bodyshop - bodyshop object
|
||||
* @param socket - socket.io connection
|
||||
* @param job - job object
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
export default function RREarlyROModal({ open, onClose, onSuccess, bodyshop, socket, job }) {
|
||||
const handleSuccess = (result) => {
|
||||
onSuccess?.(result);
|
||||
onClose?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onCancel={onClose}
|
||||
footer={null}
|
||||
width={700}
|
||||
destroyOnHidden
|
||||
title="Create Reynolds Repair Order"
|
||||
>
|
||||
<RREarlyROForm bodyshop={bodyshop} socket={socket} job={job} onSuccess={handleSuccess} onCancel={onClose} />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -86,11 +86,13 @@ export function EmailOverlayComponent({ emailConfig, form, selectedMediaState, b
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select>
|
||||
<Select.Option key={currentUser.email}>{currentUser.email}</Select.Option>
|
||||
<Select.Option key={bodyshop.email}>{bodyshop.email}</Select.Option>
|
||||
{bodyshop.md_from_emails && bodyshop.md_from_emails.map((e) => <Select.Option key={e}>{e}</Select.Option>)}
|
||||
</Select>
|
||||
<Select
|
||||
options={[
|
||||
{ key: currentUser.email, value: currentUser.email, label: currentUser.email },
|
||||
{ key: bodyshop.email, value: bodyshop.email, label: bodyshop.email },
|
||||
...(bodyshop.md_from_emails ? bodyshop.md_from_emails.map((e) => ({ key: e, value: e, label: e })) : [])
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Select, Space, Tag } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const { Option } = Select;
|
||||
//To be used as a form element only.
|
||||
|
||||
const EmployeeSearchSelectEmail = ({ options, ...props }) => {
|
||||
@@ -12,26 +11,24 @@ const EmployeeSearchSelectEmail = ({ options, ...props }) => {
|
||||
showSearch={{
|
||||
optionFilterProp: "search"
|
||||
}}
|
||||
// value={option}
|
||||
style={{
|
||||
width: 400
|
||||
}}
|
||||
options={options?.map((o) => ({
|
||||
key: o.id,
|
||||
value: o.user_email,
|
||||
search: `${o.employee_number} ${o.first_name} ${o.last_name}`,
|
||||
label: (
|
||||
<Space>
|
||||
{`${o.employee_number} ${o.first_name} ${o.last_name}`}
|
||||
<Tag color="green">
|
||||
{o.flat_rate ? t("timetickets.labels.flat_rate") : t("timetickets.labels.straight_time")}
|
||||
</Tag>
|
||||
</Space>
|
||||
)
|
||||
}))}
|
||||
{...props}
|
||||
>
|
||||
{options
|
||||
? options.map((o) => (
|
||||
<Option key={o.id} value={o.user_email} search={`${o.employee_number} ${o.first_name} ${o.last_name}`}>
|
||||
<Space>
|
||||
{`${o.employee_number} ${o.first_name} ${o.last_name}`}
|
||||
|
||||
<Tag color="green">
|
||||
{o.flat_rate ? t("timetickets.labels.flat_rate") : t("timetickets.labels.straight_time")}
|
||||
</Tag>
|
||||
</Space>
|
||||
</Option>
|
||||
))
|
||||
: null}
|
||||
</Select>
|
||||
/>
|
||||
);
|
||||
};
|
||||
export default EmployeeSearchSelectEmail;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Select, Space, Tag } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const { Option } = Select;
|
||||
//To be used as a form element only.
|
||||
|
||||
const EmployeeSearchSelect = ({ options, showEmail, ...props }) => {
|
||||
@@ -12,30 +11,29 @@ const EmployeeSearchSelect = ({ options, showEmail, ...props }) => {
|
||||
showSearch={{
|
||||
optionFilterProp: "search"
|
||||
}}
|
||||
// value={option}
|
||||
style={{
|
||||
width: 400
|
||||
}}
|
||||
options={options?.map((o) => ({
|
||||
key: o.id,
|
||||
value: o.id,
|
||||
search: `${o.employee_number} ${o.first_name} ${o.last_name}`,
|
||||
label: (
|
||||
<Space size="small">
|
||||
{`${o.employee_number ?? ""} ${o.first_name} ${o.last_name}`}
|
||||
<Tag color="green" style={{ padding: "0.1 0.1rem", marginRight: "1px", marginLeft: "1px" }}>
|
||||
{o.flat_rate ? t("timetickets.labels.flat_rate") : t("timetickets.labels.straight_time")}
|
||||
</Tag>
|
||||
{showEmail && o.user_email ? (
|
||||
<Tag color="blue" style={{ padding: "0.1 0.1rem", marginRight: "1px", marginLeft: "1px" }}>
|
||||
{o.user_email}
|
||||
</Tag>
|
||||
) : null}
|
||||
</Space>
|
||||
)
|
||||
}))}
|
||||
{...props}
|
||||
>
|
||||
{options
|
||||
? options.map((o) => (
|
||||
<Option key={o.id} value={o.id} search={`${o.employee_number} ${o.first_name} ${o.last_name}`}>
|
||||
<Space size="small">
|
||||
{`${o.employee_number ?? ""} ${o.first_name} ${o.last_name}`}
|
||||
<Tag color="green" style={{ padding: "0.1 0.1rem", marginRight: "1px", marginLeft: "1px" }}>
|
||||
{o.flat_rate ? t("timetickets.labels.flat_rate") : t("timetickets.labels.straight_time")}
|
||||
</Tag>
|
||||
{showEmail && o.user_email ? (
|
||||
<Tag color="blue" style={{ padding: "0.1 0.1rem", marginRight: "1px", marginLeft: "1px" }}>
|
||||
{o.user_email}
|
||||
</Tag>
|
||||
) : null}
|
||||
</Space>
|
||||
</Option>
|
||||
))
|
||||
: null}
|
||||
</Select>
|
||||
/>
|
||||
);
|
||||
};
|
||||
export default EmployeeSearchSelect;
|
||||
|
||||
@@ -14,8 +14,11 @@ export default function GlobalSearch() {
|
||||
const [callSearch, { loading, error, data }] = useLazyQuery(GLOBAL_SEARCH_QUERY);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const executeSearch = (v) => {
|
||||
if (v && v.variables.search && v.variables.search !== "" && v.variables.search.length >= 3) callSearch(v);
|
||||
const executeSearch = (variables) => {
|
||||
if (variables?.search !== "" && variables?.search?.length >= 3)
|
||||
callSearch({
|
||||
variables
|
||||
});
|
||||
};
|
||||
const debouncedExecuteSearch = _.debounce(executeSearch, 750);
|
||||
|
||||
@@ -157,7 +160,9 @@ export default function GlobalSearch() {
|
||||
return (
|
||||
<AutoComplete
|
||||
options={options}
|
||||
onSearch={handleSearch}
|
||||
showSearch={{
|
||||
onSearch: handleSearch
|
||||
}}
|
||||
defaultActiveFirstOption
|
||||
onKeyDown={(e) => {
|
||||
if (e.key !== "Enter") return;
|
||||
|
||||
@@ -67,16 +67,19 @@ export function Jobd3RdPartyModal({ bodyshop, jobId, job, technician }) {
|
||||
);
|
||||
};
|
||||
|
||||
const handleInsSelect = (value, option) => {
|
||||
form.setFieldsValue({
|
||||
addr1: option.obj.name,
|
||||
addr2: option.obj.street1,
|
||||
addr3: option.obj.street2,
|
||||
city: option.obj.city,
|
||||
state: option.obj.state,
|
||||
zip: option.obj.zip,
|
||||
vendorid: null
|
||||
});
|
||||
const handleInsSelect = (value) => {
|
||||
const selectedVendor = bodyshop.md_ins_cos.find(s => s.name === value);
|
||||
if (selectedVendor) {
|
||||
form.setFieldsValue({
|
||||
addr1: selectedVendor.name,
|
||||
addr2: selectedVendor.street1,
|
||||
addr3: selectedVendor.street2,
|
||||
city: selectedVendor.city,
|
||||
state: selectedVendor.state,
|
||||
zip: selectedVendor.zip,
|
||||
vendorid: null
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleVendorSelect = (vendorid) => {
|
||||
@@ -97,19 +100,19 @@ export function Jobd3RdPartyModal({ bodyshop, jobId, job, technician }) {
|
||||
return (
|
||||
<>
|
||||
<Button onClick={showModal}>{t("printcenter.jobs.3rdpartypayer")}</Button>
|
||||
<Modal open={isModalVisible} onOk={handleOk} onCancel={handleCancel}>
|
||||
<Modal open={isModalVisible} onOk={handleOk} onCancel={handleCancel} getContainer={() => document.body}>
|
||||
<Form onFinish={handleFinish} autoComplete={"off"} layout="vertical" form={form}>
|
||||
<Form.Item label={t("bills.fields.vendor")} name="vendorid">
|
||||
<VendorSearchSelect options={VendorAutoCompleteData?.vendors} onSelect={handleVendorSelect} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("bodyshop.fields.md_ins_co.name")} name="ins_co_id">
|
||||
<Select onSelect={handleInsSelect}>
|
||||
{bodyshop.md_ins_cos.map((s) => (
|
||||
<Select.Option key={s.name} obj={s} value={s.name}>
|
||||
{s.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select
|
||||
onSelect={handleInsSelect}
|
||||
options={bodyshop.md_ins_cos.map((s) => ({
|
||||
value: s.name,
|
||||
label: s.name
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<LayoutFormRow grow>
|
||||
<Form.Item label={t("printcenter.jobs.3rdpartyfields.addr1")} name="addr1">
|
||||
|
||||
@@ -88,17 +88,15 @@ export function JoblineBulkAssign({ setSelectedLines, selectedLines, insertAudit
|
||||
>
|
||||
<Select
|
||||
showSearch={{
|
||||
optionFilterProp: "children",
|
||||
filterOption: (input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
optionFilterProp: "label",
|
||||
filterOption: (input, option) => option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}}
|
||||
style={{ width: 200 }}
|
||||
>
|
||||
{bodyshop.employee_teams.map((team) => (
|
||||
<Select.Option value={team.id} key={team.id} name={team.name}>
|
||||
{team.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
options={bodyshop.employee_teams.map((team) => ({
|
||||
value: team.id,
|
||||
label: team.name
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Space wrap>
|
||||
|
||||
@@ -122,22 +122,26 @@ export function JobLineConvertToLabor({
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select allowClear showSearch={{ optionFilterProp: "children" }}>
|
||||
<Select.Option value="LAA">{t("joblines.fields.lbr_types.LAA")}</Select.Option>
|
||||
<Select.Option value="LAB">{t("joblines.fields.lbr_types.LAB")}</Select.Option>
|
||||
<Select.Option value="LAD">{t("joblines.fields.lbr_types.LAD")}</Select.Option>
|
||||
<Select.Option value="LAE">{t("joblines.fields.lbr_types.LAE")}</Select.Option>
|
||||
<Select.Option value="LAF">{t("joblines.fields.lbr_types.LAF")}</Select.Option>
|
||||
<Select.Option value="LAG">{t("joblines.fields.lbr_types.LAG")}</Select.Option>
|
||||
<Select.Option value="LAM">{t("joblines.fields.lbr_types.LAM")}</Select.Option>
|
||||
<Select.Option value="LAR">{t("joblines.fields.lbr_types.LAR")}</Select.Option>
|
||||
<Select.Option value="LAS">{t("joblines.fields.lbr_types.LAS")}</Select.Option>
|
||||
<Select.Option value="LAU">{t("joblines.fields.lbr_types.LAU")}</Select.Option>
|
||||
<Select.Option value="LA1">{t("joblines.fields.lbr_types.LA1")}</Select.Option>
|
||||
<Select.Option value="LA2">{t("joblines.fields.lbr_types.LA2")}</Select.Option>
|
||||
<Select.Option value="LA3">{t("joblines.fields.lbr_types.LA3")}</Select.Option>
|
||||
<Select.Option value="LA4">{t("joblines.fields.lbr_types.LA4")}</Select.Option>
|
||||
</Select>
|
||||
<Select
|
||||
allowClear
|
||||
showSearch
|
||||
options={[
|
||||
{ value: "LAA", label: t("joblines.fields.lbr_types.LAA") },
|
||||
{ value: "LAB", label: t("joblines.fields.lbr_types.LAB") },
|
||||
{ value: "LAD", label: t("joblines.fields.lbr_types.LAD") },
|
||||
{ value: "LAE", label: t("joblines.fields.lbr_types.LAE") },
|
||||
{ value: "LAF", label: t("joblines.fields.lbr_types.LAF") },
|
||||
{ value: "LAG", label: t("joblines.fields.lbr_types.LAG") },
|
||||
{ value: "LAM", label: t("joblines.fields.lbr_types.LAM") },
|
||||
{ value: "LAR", label: t("joblines.fields.lbr_types.LAR") },
|
||||
{ value: "LAS", label: t("joblines.fields.lbr_types.LAS") },
|
||||
{ value: "LAU", label: t("joblines.fields.lbr_types.LAU") },
|
||||
{ value: "LA1", label: t("joblines.fields.lbr_types.LA1") },
|
||||
{ value: "LA2", label: t("joblines.fields.lbr_types.LA2") },
|
||||
{ value: "LA3", label: t("joblines.fields.lbr_types.LA3") },
|
||||
{ value: "LA4", label: t("joblines.fields.lbr_types.LA4") }
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item shouldUpdate>
|
||||
|
||||
@@ -115,19 +115,18 @@ export function JobLineDispatchButton({
|
||||
>
|
||||
<Select
|
||||
showSearch={{
|
||||
optionFilterProp: "children",
|
||||
filterOption: (input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
optionFilterProp: "label",
|
||||
filterOption: (input, option) => option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}}
|
||||
style={{ width: 200 }}
|
||||
>
|
||||
{bodyshop.employees
|
||||
options={bodyshop.employees
|
||||
.filter((emp) => emp.active)
|
||||
.map((emp) => (
|
||||
<Select.Option value={emp.id} key={emp.id} name={`${emp.first_name} ${emp.last_name}`}>
|
||||
{`${emp.first_name} ${emp.last_name}`}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
.map((emp) => ({
|
||||
value: emp.id,
|
||||
key: emp.id,
|
||||
label: `${emp.first_name} ${emp.last_name}`
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Space wrap>
|
||||
|
||||
@@ -64,13 +64,12 @@ export function JobLineStatusPopup({ bodyshop, jobline, disabled }) {
|
||||
onSelect={handleChange}
|
||||
onBlur={handleSave}
|
||||
onClear={() => handleChange(null)}
|
||||
>
|
||||
{Object.values(bodyshop.md_order_statuses).map((s, idx) => (
|
||||
<Select.Option key={idx} value={s}>
|
||||
{s}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
options={Object.values(bodyshop.md_order_statuses).map((s, idx) => ({
|
||||
key: idx,
|
||||
value: s,
|
||||
label: s
|
||||
}))}
|
||||
/>
|
||||
</LoadingSpinner>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -75,13 +75,12 @@ export function JoblineTeamAssignment({ bodyshop, jobline, disabled, jobId, inse
|
||||
onSelect={handleChange}
|
||||
onBlur={handleSave}
|
||||
onClear={() => handleChange(null)}
|
||||
>
|
||||
{Object.values(bodyshop.employee_teams).map((s, idx) => (
|
||||
<Select.Option key={idx} value={s.id}>
|
||||
{s.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
options={Object.values(bodyshop.employee_teams).map((s) => ({
|
||||
key: s.id,
|
||||
value: s.id,
|
||||
label: s.name
|
||||
}))}
|
||||
/>
|
||||
</LoadingSpinner>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -67,22 +67,22 @@ export function JobLinesUpsertModalComponent({ bodyshop, open, jobLine, handleCa
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow grow>
|
||||
<Form.Item label={t("joblines.fields.mod_lbr_ty")} name="mod_lbr_ty">
|
||||
<Select allowClear>
|
||||
<Select.Option value="LAA">{t("joblines.fields.lbr_types.LAA")}</Select.Option>
|
||||
<Select.Option value="LAB">{t("joblines.fields.lbr_types.LAB")}</Select.Option>
|
||||
<Select.Option value="LAD">{t("joblines.fields.lbr_types.LAD")}</Select.Option>
|
||||
<Select.Option value="LAE">{t("joblines.fields.lbr_types.LAE")}</Select.Option>
|
||||
<Select.Option value="LAF">{t("joblines.fields.lbr_types.LAF")}</Select.Option>
|
||||
<Select.Option value="LAG">{t("joblines.fields.lbr_types.LAG")}</Select.Option>
|
||||
<Select.Option value="LAM">{t("joblines.fields.lbr_types.LAM")}</Select.Option>
|
||||
<Select.Option value="LAR">{t("joblines.fields.lbr_types.LAR")}</Select.Option>
|
||||
<Select.Option value="LAS">{t("joblines.fields.lbr_types.LAS")}</Select.Option>
|
||||
<Select.Option value="LAU">{t("joblines.fields.lbr_types.LAU")}</Select.Option>
|
||||
<Select.Option value="LA1">{t("joblines.fields.lbr_types.LA1")}</Select.Option>
|
||||
<Select.Option value="LA2">{t("joblines.fields.lbr_types.LA2")}</Select.Option>
|
||||
<Select.Option value="LA3">{t("joblines.fields.lbr_types.LA3")}</Select.Option>
|
||||
<Select.Option value="LA4">{t("joblines.fields.lbr_types.LA4")}</Select.Option>
|
||||
</Select>
|
||||
<Select allowClear options={[
|
||||
{ value: "LAA", label: t("joblines.fields.lbr_types.LAA") },
|
||||
{ value: "LAB", label: t("joblines.fields.lbr_types.LAB") },
|
||||
{ value: "LAD", label: t("joblines.fields.lbr_types.LAD") },
|
||||
{ value: "LAE", label: t("joblines.fields.lbr_types.LAE") },
|
||||
{ value: "LAF", label: t("joblines.fields.lbr_types.LAF") },
|
||||
{ value: "LAG", label: t("joblines.fields.lbr_types.LAG") },
|
||||
{ value: "LAM", label: t("joblines.fields.lbr_types.LAM") },
|
||||
{ value: "LAR", label: t("joblines.fields.lbr_types.LAR") },
|
||||
{ value: "LAS", label: t("joblines.fields.lbr_types.LAS") },
|
||||
{ value: "LAU", label: t("joblines.fields.lbr_types.LAU") },
|
||||
{ value: "LA1", label: t("joblines.fields.lbr_types.LA1") },
|
||||
{ value: "LA2", label: t("joblines.fields.lbr_types.LA2") },
|
||||
{ value: "LA3", label: t("joblines.fields.lbr_types.LA3") },
|
||||
{ value: "LA4", label: t("joblines.fields.lbr_types.LA4") }
|
||||
]} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("joblines.fields.op_code_desc")} name="op_code_desc">
|
||||
<Input />
|
||||
@@ -128,17 +128,17 @@ export function JobLinesUpsertModalComponent({ bodyshop, open, jobLine, handleCa
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow>
|
||||
<Form.Item label={t("joblines.fields.part_type")} name="part_type">
|
||||
<Select allowClear>
|
||||
<Select.Option value="PAA">{t("joblines.fields.part_types.PAA")}</Select.Option>
|
||||
<Select.Option value="PAC">{t("joblines.fields.part_types.PAC")}</Select.Option>
|
||||
<Select.Option value="PAE">{t("joblines.fields.part_types.PAE")}</Select.Option>
|
||||
<Select.Option value="PAL">{t("joblines.fields.part_types.PAL")}</Select.Option>
|
||||
<Select.Option value="PAM">{t("joblines.fields.part_types.PAM")}</Select.Option>
|
||||
<Select.Option value="PAN">{t("joblines.fields.part_types.PAN")}</Select.Option>
|
||||
<Select.Option value="PAO">{t("joblines.fields.part_types.PAO")}</Select.Option>
|
||||
<Select.Option value="PAR">{t("joblines.fields.part_types.PAR")}</Select.Option>
|
||||
<Select.Option value="PAS">{t("joblines.fields.part_types.PAS")}</Select.Option>
|
||||
</Select>
|
||||
<Select allowClear options={[
|
||||
{ value: "PAA", label: t("joblines.fields.part_types.PAA") },
|
||||
{ value: "PAC", label: t("joblines.fields.part_types.PAC") },
|
||||
{ value: "PAE", label: t("joblines.fields.part_types.PAE") },
|
||||
{ value: "PAL", label: t("joblines.fields.part_types.PAL") },
|
||||
{ value: "PAM", label: t("joblines.fields.part_types.PAM") },
|
||||
{ value: "PAN", label: t("joblines.fields.part_types.PAN") },
|
||||
{ value: "PAO", label: t("joblines.fields.part_types.PAO") },
|
||||
{ value: "PAR", label: t("joblines.fields.part_types.PAR") },
|
||||
{ value: "PAS", label: t("joblines.fields.part_types.PAS") }
|
||||
]} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("joblines.fields.oem_partno")} name="oem_partno">
|
||||
<Input />
|
||||
|
||||
@@ -8,8 +8,6 @@ import { SEARCH_JOBS_BY_ID_FOR_AUTOCOMPLETE, SEARCH_JOBS_FOR_AUTOCOMPLETE } from
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
const JobSearchSelect = ({
|
||||
disabled,
|
||||
convertedOnly = false,
|
||||
@@ -87,24 +85,24 @@ const JobSearchSelect = ({
|
||||
style={{ width: "100%" }}
|
||||
suffixIcon={(loading || idLoading) && <Spin />} // matches OLD spinner semantics
|
||||
notFoundContent={loading ? <LoadingOutlined /> : null} // matches OLD (loading only)
|
||||
>
|
||||
{theOptions
|
||||
? theOptions.map((o) => (
|
||||
<Option key={o.id} value={o.id} status={o.status}>
|
||||
<Space align="center">
|
||||
<span>
|
||||
{`${clm_no && o.clm_no ? `${o.clm_no} | ` : ""}${o.ro_number || t("general.labels.na")} | ${OwnerNameDisplayFunction(
|
||||
o
|
||||
)} | ${o.v_model_yr || ""} ${o.v_make_desc || ""} ${o.v_model_desc || ""}`}
|
||||
</span>
|
||||
<Tag>
|
||||
<strong>{o.status}</strong>
|
||||
</Tag>
|
||||
</Space>
|
||||
</Option>
|
||||
))
|
||||
: null}
|
||||
</Select>
|
||||
options={theOptions?.map((o) => ({
|
||||
key: o.id,
|
||||
value: o.id,
|
||||
status: o.status,
|
||||
label: (
|
||||
<Space align="center">
|
||||
<span>
|
||||
{`${clm_no && o.clm_no ? `${o.clm_no} | ` : ""}${o.ro_number || t("general.labels.na")} | ${OwnerNameDisplayFunction(
|
||||
o
|
||||
)} | ${o.v_model_yr || ""} ${o.v_make_desc || ""} ${o.v_model_desc || ""}`}
|
||||
</span>
|
||||
<Tag>
|
||||
<strong>{o.status}</strong>
|
||||
</Tag>
|
||||
</Space>
|
||||
)
|
||||
}))}
|
||||
/>
|
||||
|
||||
{error ? <AlertComponent title={error.message} type="error" /> : null}
|
||||
{idError ? <AlertComponent title={idError.message} type="error" /> : null}
|
||||
|
||||
@@ -59,13 +59,12 @@ export function JobsAdminClass({ bodyshop, job }) {
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select>
|
||||
{bodyshop.md_classes.map((s) => (
|
||||
<Select.Option key={s} value={s}>
|
||||
{s}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select
|
||||
options={bodyshop.md_classes.map((s) => ({
|
||||
value: s,
|
||||
label: s
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
|
||||
@@ -42,11 +42,11 @@ export function JobsCloseLines({ bodyshop, job, jobRO }) {
|
||||
<tbody>
|
||||
{fields.map((field, index) => (
|
||||
<tr key={field.key}>
|
||||
{/* Hidden field to preserve jobline ID */}
|
||||
<Form.Item hidden name={[field.name, "id"]}>
|
||||
<input />
|
||||
</Form.Item>
|
||||
<td>
|
||||
{/* Hidden field to preserve jobline ID without injecting a div under <tr> */}
|
||||
<Form.Item noStyle name={[field.name, "id"]}>
|
||||
<input type="hidden" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
// label={t("joblines.fields.line_desc")}
|
||||
key={`${index}line_desc`}
|
||||
@@ -141,13 +141,11 @@ export function JobsCloseLines({ bodyshop, job, jobRO }) {
|
||||
option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}}
|
||||
disabled={jobRO}
|
||||
>
|
||||
{bodyshop.md_responsibility_centers.profits.map((p) => (
|
||||
<Select.Option key={p.name} value={p.name}>
|
||||
{p.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
options={bodyshop.md_responsibility_centers.profits.map((p) => ({
|
||||
value: p.name,
|
||||
label: p.name
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
</td>
|
||||
<td>
|
||||
@@ -171,13 +169,11 @@ export function JobsCloseLines({ bodyshop, job, jobRO }) {
|
||||
option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}}
|
||||
disabled={jobRO}
|
||||
>
|
||||
{bodyshop.md_responsibility_centers.profits.map((p) => (
|
||||
<Select.Option key={p.name} value={p.name}>
|
||||
{p.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
options={bodyshop.md_responsibility_centers.profits.map((p) => ({
|
||||
value: p.name,
|
||||
label: p.name
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -1,24 +1,28 @@
|
||||
import { useMutation } from "@apollo/client/react";
|
||||
import { Button, Form, Input, Popover, Select, Space, Switch } from "antd";
|
||||
import { Button, Divider, Form, Input, Modal, Select, Space, Switch } from "antd";
|
||||
import axios from "axios";
|
||||
import { some } from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { CONVERT_JOB_TO_RO } from "../../graphql/jobs.queries";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import { DMS_MAP, getDmsMode } from "../../utils/dmsUtils";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||
import RREarlyROForm from "../dms-post-form/rr-early-ro-form";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
bodyshop: selectBodyshop,
|
||||
jobRO: selectJobReadOnly
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(
|
||||
@@ -33,18 +37,83 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
export function JobsConvertButton({ bodyshop, job, refetch, jobRO, insertAuditTrail, parentFormIsFieldsTouched }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const [earlyRoCreated, setEarlyRoCreated] = useState(!!job?.dms_id);
|
||||
const [earlyRoCreatedThisSession, setEarlyRoCreatedThisSession] = useState(false);
|
||||
|
||||
const [mutationConvertJob] = useMutation(CONVERT_JOB_TO_RO);
|
||||
const { t } = useTranslation();
|
||||
const [form] = Form.useForm();
|
||||
const notification = useNotification();
|
||||
const allFormValues = Form.useWatch([], form);
|
||||
const { socket } = useSocket();
|
||||
|
||||
const {
|
||||
treatments: { Fortellis }
|
||||
} = useTreatmentsWithConfig({
|
||||
attributes: {},
|
||||
names: ["Fortellis"],
|
||||
splitKey: bodyshop?.imexshopid
|
||||
});
|
||||
|
||||
const dmsMode = getDmsMode(bodyshop, Fortellis.treatment);
|
||||
const isReynoldsMode = dmsMode === DMS_MAP.reynolds;
|
||||
|
||||
const insuranceOptions = useMemo(
|
||||
() =>
|
||||
(bodyshop?.md_ins_cos ?? []).map((s) => ({
|
||||
value: s.name,
|
||||
label: s.name
|
||||
})),
|
||||
[bodyshop?.md_ins_cos]
|
||||
);
|
||||
|
||||
const classOptions = useMemo(
|
||||
() =>
|
||||
(bodyshop?.md_classes ?? []).map((s) => ({
|
||||
value: s,
|
||||
label: s
|
||||
})),
|
||||
[bodyshop?.md_classes]
|
||||
);
|
||||
|
||||
const referralOptions = useMemo(
|
||||
() =>
|
||||
(bodyshop?.md_referral_sources ?? []).map((s) => ({
|
||||
value: s,
|
||||
label: s
|
||||
})),
|
||||
[bodyshop?.md_referral_sources]
|
||||
);
|
||||
|
||||
const csrOptions = useMemo(
|
||||
() =>
|
||||
(bodyshop?.employees ?? [])
|
||||
.filter((emp) => emp.active)
|
||||
.map((emp) => ({
|
||||
value: emp.id,
|
||||
label: `${emp.first_name} ${emp.last_name}`
|
||||
})),
|
||||
[bodyshop?.employees]
|
||||
);
|
||||
|
||||
const categoryOptions = useMemo(
|
||||
() =>
|
||||
(bodyshop?.md_categories ?? []).map((s) => ({
|
||||
value: s,
|
||||
label: s
|
||||
})),
|
||||
[bodyshop?.md_categories]
|
||||
);
|
||||
|
||||
const handleConvert = async ({ employee_csr, category, ...values }) => {
|
||||
if (parentFormIsFieldsTouched()) {
|
||||
alert(t("jobs.labels.savebeforeconversion"));
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
const res = await mutationConvertJob({
|
||||
variables: {
|
||||
jobId: job.id,
|
||||
@@ -58,13 +127,11 @@ export function JobsConvertButton({ bodyshop, job, refetch, jobRO, insertAuditTr
|
||||
});
|
||||
|
||||
if (values.ca_gst_registrant) {
|
||||
await axios.post("/job/totalsssu", {
|
||||
id: job.id
|
||||
});
|
||||
await axios.post("/job/totalsssu", { id: job.id });
|
||||
}
|
||||
|
||||
if (!res.errors) {
|
||||
refetch();
|
||||
refetch?.();
|
||||
notification.success({
|
||||
title: t("jobs.successes.converted")
|
||||
});
|
||||
@@ -77,182 +144,183 @@ export function JobsConvertButton({ bodyshop, job, refetch, jobRO, insertAuditTr
|
||||
|
||||
setOpen(false);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const submitDisabled = useCallback(() => some(allFormValues, (v) => v === undefined), [allFormValues]);
|
||||
|
||||
const popMenu = (
|
||||
<div>
|
||||
<Form
|
||||
layout="vertical"
|
||||
form={form}
|
||||
onFinish={handleConvert}
|
||||
initialValues={{
|
||||
driveable: true,
|
||||
towin: job.towin,
|
||||
ca_gst_registrant: job.ca_gst_registrant,
|
||||
employee_csr: job.employee_csr,
|
||||
category: job.category,
|
||||
referral_source: job.referral_source,
|
||||
referral_source_extra: job.referral_source_extra ?? ""
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
name={["ins_co_nm"]}
|
||||
label={t("jobs.fields.ins_co_nm")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select showSearch>
|
||||
{bodyshop.md_ins_cos.map((s, i) => (
|
||||
<Select.Option key={i} value={s.name}>
|
||||
{s.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
{bodyshop.enforce_class && (
|
||||
<Form.Item
|
||||
name={"class"}
|
||||
label={t("jobs.fields.class")}
|
||||
rules={[
|
||||
{
|
||||
required: bodyshop.enforce_class
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select>
|
||||
{bodyshop.md_classes.map((s) => (
|
||||
<Select.Option key={s} value={s}>
|
||||
{s}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
)}
|
||||
{bodyshop.enforce_referral && (
|
||||
<>
|
||||
<Form.Item
|
||||
name={"referral_source"}
|
||||
label={t("jobs.fields.referralsource")}
|
||||
rules={[
|
||||
{
|
||||
required: bodyshop.enforce_referral
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select>
|
||||
{bodyshop.md_referral_sources.map((s) => (
|
||||
<Select.Option key={s} value={s}>
|
||||
{s}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label={t("jobs.fields.referral_source_extra")} name="referral_source_extra">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
{bodyshop.enforce_conversion_csr && (
|
||||
<Form.Item
|
||||
name={"employee_csr"}
|
||||
label={t(
|
||||
InstanceRenderManager({
|
||||
imex: "jobs.fields.employee_csr",
|
||||
rome: "jobs.fields.employee_csr_writer"
|
||||
})
|
||||
)}
|
||||
rules={[
|
||||
{
|
||||
required: bodyshop.enforce_conversion_csr
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
showSearch={{
|
||||
optionFilterProp: "children",
|
||||
filterOption: (input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}}
|
||||
style={{ width: 200 }}
|
||||
>
|
||||
{bodyshop.employees
|
||||
.filter((emp) => emp.active)
|
||||
.map((emp) => (
|
||||
<Select.Option value={emp.id} key={emp.id} name={`${emp.first_name} ${emp.last_name}`}>
|
||||
{`${emp.first_name} ${emp.last_name}`}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
)}
|
||||
{bodyshop.enforce_conversion_category && (
|
||||
<Form.Item
|
||||
name={"category"}
|
||||
label={t("jobs.fields.category")}
|
||||
rules={[
|
||||
{
|
||||
required: bodyshop.enforce_conversion_category
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select allowClear>
|
||||
{bodyshop.md_categories.map((s) => (
|
||||
<Select.Option key={s} value={s}>
|
||||
{s}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
)}
|
||||
{bodyshop.region_config.toLowerCase().startsWith("ca") && (
|
||||
<Form.Item label={t("jobs.fields.ca_gst_registrant")} name="ca_gst_registrant" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item label={t("jobs.fields.driveable")} name="driveable" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("jobs.fields.towin")} name="towin" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Space wrap>
|
||||
<Button disabled={submitDisabled()} type="primary" danger onClick={() => form.submit()} loading={loading}>
|
||||
{t("jobs.actions.convert")}
|
||||
</Button>
|
||||
<Button onClick={() => setOpen(false)}>{t("general.actions.close")}</Button>
|
||||
</Space>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
const handleEarlyROSuccess = (result) => {
|
||||
setEarlyRoCreated(true);
|
||||
setEarlyRoCreatedThisSession(true);
|
||||
notification.success({
|
||||
title: t("jobs.successes.early_ro_created"),
|
||||
description: `RO Number: ${result.roNumber || "N/A"}`
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
refetch?.();
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
const handleModalClose = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
if (job.converted) return <></>;
|
||||
|
||||
return (
|
||||
<Popover open={open} content={popMenu}>
|
||||
<>
|
||||
<Button
|
||||
key="convert"
|
||||
type="primary"
|
||||
danger
|
||||
// style={{ display: job.converted ? "none" : "" }}
|
||||
disabled={job.converted || jobRO}
|
||||
loading={loading}
|
||||
onClick={() => {
|
||||
setEarlyRoCreated(!!job?.dms_id);
|
||||
setEarlyRoCreatedThisSession(false);
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
{t("jobs.actions.convert")}
|
||||
</Button>
|
||||
</Popover>
|
||||
|
||||
<Modal
|
||||
open={open}
|
||||
onCancel={handleModalClose}
|
||||
closable={!(earlyRoCreatedThisSession && !job.converted)}
|
||||
maskClosable={!(earlyRoCreatedThisSession && !job.converted)}
|
||||
title={t("jobs.actions.convert")}
|
||||
footer={null}
|
||||
width={700}
|
||||
destroyOnHidden
|
||||
>
|
||||
<Form
|
||||
layout="vertical"
|
||||
form={form}
|
||||
preserve={false}
|
||||
onFinish={handleConvert}
|
||||
initialValues={{
|
||||
driveable: true,
|
||||
towin: job.towin,
|
||||
ca_gst_registrant: job.ca_gst_registrant,
|
||||
employee_csr: job.employee_csr,
|
||||
category: job.category,
|
||||
referral_source: job.referral_source,
|
||||
referral_source_extra: job.referral_source_extra ?? ""
|
||||
}}
|
||||
>
|
||||
{isReynoldsMode && !job.dms_id && !earlyRoCreated && (
|
||||
<>
|
||||
<RREarlyROForm
|
||||
bodyshop={bodyshop}
|
||||
socket={socket}
|
||||
job={job}
|
||||
onSuccess={handleEarlyROSuccess}
|
||||
showCancelButton={false}
|
||||
/>
|
||||
<Divider />
|
||||
</>
|
||||
)}
|
||||
|
||||
<Form.Item
|
||||
name={["ins_co_nm"]}
|
||||
label={t("jobs.fields.ins_co_nm")}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select
|
||||
showSearch={{
|
||||
optionFilterProp:'label'
|
||||
}}
|
||||
options={insuranceOptions}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{bodyshop.enforce_class && (
|
||||
<Form.Item name="class" label={t("jobs.fields.class")} rules={[{ required: bodyshop.enforce_class }]}>
|
||||
<Select options={classOptions} />
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{bodyshop.enforce_referral && (
|
||||
<>
|
||||
<Form.Item
|
||||
name="referral_source"
|
||||
label={t("jobs.fields.referralsource")}
|
||||
rules={[{ required: bodyshop.enforce_referral }]}
|
||||
>
|
||||
<Select options={referralOptions} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t("jobs.fields.referral_source_extra")} name="referral_source_extra">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
{bodyshop.enforce_conversion_csr && (
|
||||
<Form.Item
|
||||
name="employee_csr"
|
||||
label={t(
|
||||
InstanceRenderManager({
|
||||
imex: "jobs.fields.employee_csr",
|
||||
rome: "jobs.fields.employee_csr_writer"
|
||||
})
|
||||
)}
|
||||
rules={[{ required: bodyshop.enforce_conversion_csr }]}
|
||||
>
|
||||
<Select
|
||||
showSearch={{
|
||||
optionFilterProp: 'label',
|
||||
filterOption: (input, option) =>
|
||||
(option?.label ?? "").toLowerCase().includes(input.toLowerCase())
|
||||
}}
|
||||
style={{ width: 200 }}
|
||||
|
||||
options={csrOptions}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{bodyshop.enforce_conversion_category && (
|
||||
<Form.Item name="category" label={t("jobs.fields.category")} rules={[{ required: bodyshop.enforce_conversion_category }]}>
|
||||
<Select allowClear options={categoryOptions} />
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{bodyshop.region_config.toLowerCase().startsWith("ca") && (
|
||||
<Form.Item label={t("jobs.fields.ca_gst_registrant")} name="ca_gst_registrant" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Form.Item label={t("jobs.fields.driveable")} name="driveable" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t("jobs.fields.towin")} name="towin" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
|
||||
<Space wrap style={{ marginTop: 16 }}>
|
||||
<Button
|
||||
disabled={submitDisabled() || (isReynoldsMode && !job.dms_id && !earlyRoCreated)}
|
||||
type="primary"
|
||||
danger
|
||||
onClick={() => form.submit()}
|
||||
loading={loading}
|
||||
>
|
||||
{t("jobs.actions.convert")}
|
||||
</Button>
|
||||
|
||||
<Button onClick={handleModalClose} disabled={earlyRoCreatedThisSession && !job.converted}>
|
||||
{t("general.actions.close")}
|
||||
</Button>
|
||||
</Space>
|
||||
</Form>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -60,13 +60,13 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("jobs.fields.ins_co_nm")} name="ins_co_nm">
|
||||
<Select onChange={handleInsCoChange}>
|
||||
{bodyshop.md_ins_cos.map((s) => (
|
||||
<Select.Option key={s.name} value={s.name}>
|
||||
{s.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select
|
||||
onChange={handleInsCoChange}
|
||||
options={bodyshop.md_ins_cos.map((s) => ({
|
||||
value: s.name,
|
||||
label: s.name
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label={t("jobs.fields.ins_addr1")} name="ins_addr1">
|
||||
<Input />
|
||||
@@ -192,13 +192,12 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("jobs.fields.referralsource")} name="referral_source">
|
||||
<Select>
|
||||
{bodyshop.md_referral_sources.map((s) => (
|
||||
<Select.Option key={s} value={s}>
|
||||
{s}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select
|
||||
options={bodyshop.md_referral_sources.map((s) => ({
|
||||
value: s,
|
||||
label: s
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label={t("jobs.fields.referral_source_extra")} name="referral_source_extra">
|
||||
<Input />
|
||||
@@ -221,10 +220,13 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
|
||||
<CurrencyInput min={0} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("jobs.fields.ded_status")} name="ded_status">
|
||||
<Select allowClear>
|
||||
<Select.Option value="W">{t("jobs.labels.deductible.waived")}</Select.Option>
|
||||
<Select.Option value="Y">{t("jobs.labels.deductible.stands")}</Select.Option>
|
||||
</Select>
|
||||
<Select
|
||||
allowClear
|
||||
options={[
|
||||
{ value: "W", label: t("jobs.labels.deductible.waived") },
|
||||
{ value: "Y", label: t("jobs.labels.deductible.stands") }
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label={t("jobs.fields.depreciation_taxes")} name="depreciation_taxes">
|
||||
<CurrencyInput />
|
||||
|
||||
@@ -43,20 +43,19 @@ export function JobsDetailGeneral({ bodyshop, jobRO, job, form }) {
|
||||
<Input disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("jobs.fields.ded_status")} name="ded_status">
|
||||
<Select disabled={jobRO}>
|
||||
<Select.Option value="W">{t("jobs.labels.deductible.waived")}</Select.Option>
|
||||
<Select.Option value="Y">{t("jobs.labels.deductible.stands")}</Select.Option>
|
||||
</Select>
|
||||
<Select disabled={jobRO} options={[
|
||||
{ value: "W", label: t("jobs.labels.deductible.waived") },
|
||||
{ value: "Y", label: t("jobs.labels.deductible.stands") }
|
||||
]} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("jobs.fields.ded_amt")} name="ded_amt">
|
||||
<CurrencyInput disabled={jobRO} min={0} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("jobs.fields.ded_note")} name="ded_note">
|
||||
<Select disabled={jobRO}>
|
||||
{bodyshop.md_ded_notes.map((n, index) => (
|
||||
<Select.Option key={index}>{n}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select disabled={jobRO} options={bodyshop.md_ded_notes.map((n) => ({
|
||||
value: n,
|
||||
label: n
|
||||
}))} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("jobs.fields.policy_no")} name="policy_no">
|
||||
<Input disabled={jobRO} />
|
||||
@@ -66,13 +65,10 @@ export function JobsDetailGeneral({ bodyshop, jobRO, job, form }) {
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t("jobs.fields.ins_co_nm")} name="ins_co_nm">
|
||||
<Select disabled={jobRO} onChange={handleInsCoChange}>
|
||||
{bodyshop.md_ins_cos.map((s) => (
|
||||
<Select.Option key={s.name} value={s.name}>
|
||||
{s.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select disabled={jobRO} onChange={handleInsCoChange} options={bodyshop.md_ins_cos.map((s) => ({
|
||||
value: s.name,
|
||||
label: s.name
|
||||
}))} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("jobs.fields.ins_addr1")} name="ins_addr1">
|
||||
<Input disabled={jobRO} />
|
||||
@@ -123,25 +119,19 @@ export function JobsDetailGeneral({ bodyshop, jobRO, job, form }) {
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select disabled={jobRO} allowClear>
|
||||
{bodyshop.md_referral_sources.map((s) => (
|
||||
<Select.Option key={s} value={s}>
|
||||
{s}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select disabled={jobRO} allowClear options={bodyshop.md_referral_sources.map((s) => ({
|
||||
value: s,
|
||||
label: s
|
||||
}))} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("jobs.fields.referral_source_extra")} name="referral_source_extra">
|
||||
<Input disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("jobs.fields.alt_transport")} name="alt_transport">
|
||||
<Select disabled={jobRO} allowClear>
|
||||
{bodyshop.appt_alt_transport.map((s) => (
|
||||
<Select.Option key={s} value={s}>
|
||||
{s}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select disabled={jobRO} allowClear options={bodyshop.appt_alt_transport.map((s) => ({
|
||||
value: s,
|
||||
label: s
|
||||
}))} />
|
||||
</Form.Item>
|
||||
</FormRow>
|
||||
<Row gutter={[16, 16]}>
|
||||
@@ -243,15 +233,11 @@ export function JobsDetailGeneral({ bodyshop, jobRO, job, form }) {
|
||||
</FormRow>
|
||||
<FormRow header={t("jobs.forms.other")}>
|
||||
<Form.Item label={t("jobs.fields.category")} name="category">
|
||||
<Select disabled={jobRO} allowClear>
|
||||
{bodyshop.md_categories.map((s) => (
|
||||
<Select.Option key={s} value={s}>
|
||||
{s}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select disabled={jobRO} allowClear options={bodyshop.md_categories.map((s) => ({
|
||||
value: s,
|
||||
label: s
|
||||
}))} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t("jobs.fields.selling_dealer")} name="selling_dealer">
|
||||
<Input disabled={jobRO} />
|
||||
</Form.Item>
|
||||
@@ -267,6 +253,21 @@ export function JobsDetailGeneral({ bodyshop, jobRO, job, form }) {
|
||||
<Form.Item label={t("jobs.fields.lost_sale_reason")} name="lost_sale_reason">
|
||||
<Input disabled={jobRO} allowClear />
|
||||
</Form.Item>
|
||||
{bodyshop.rr_dealerid && (
|
||||
<Form.Item label={t("jobs.fields.dms.id")} name="dms_id">
|
||||
<Input disabled />
|
||||
</Form.Item>
|
||||
)}
|
||||
{bodyshop.rr_dealerid && (
|
||||
<Form.Item label={t("jobs.fields.dms.advisor")} name="dms_advisor_id">
|
||||
<Input disabled />
|
||||
</Form.Item>
|
||||
)}
|
||||
{bodyshop.rr_dealerid && (
|
||||
<Form.Item label={t("jobs.fields.dms.customer")} name="dms_customer_id">
|
||||
<Input disabled />
|
||||
</Form.Item>
|
||||
)}
|
||||
</FormRow>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -157,7 +157,6 @@ export function JobsDetailHeaderActions({
|
||||
variables: watcherVars,
|
||||
skip: !jobId,
|
||||
fetchPolicy: "cache-first",
|
||||
notifyOnNetworkStatusChange: true
|
||||
});
|
||||
|
||||
const jobWatchersCount = jobWatchersData?.job_watchers?.length ?? job?.job_watchers?.length ?? 0;
|
||||
@@ -715,13 +714,12 @@ export function JobsDetailHeaderActions({
|
||||
<FormDateTimePickerComponent />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("appointments.fields.color")} name="color">
|
||||
<Select>
|
||||
{bodyshop.appt_colors.map((col, idx) => (
|
||||
<Select.Option key={idx} value={col.color.hex}>
|
||||
{col.label}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select
|
||||
options={bodyshop.appt_colors.map((col) => ({
|
||||
value: col.color.hex,
|
||||
label: col.label
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Space wrap>
|
||||
|
||||
@@ -94,22 +94,26 @@ export function LaborAllocationsAdjustmentEdit({
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select allowClear disabled={!!mod_lbr_ty}>
|
||||
<Select.Option value="LAA">{t("joblines.fields.lbr_types.LAA")}</Select.Option>
|
||||
<Select.Option value="LAB">{t("joblines.fields.lbr_types.LAB")}</Select.Option>
|
||||
<Select.Option value="LAD">{t("joblines.fields.lbr_types.LAD")}</Select.Option>
|
||||
<Select.Option value="LAE">{t("joblines.fields.lbr_types.LAE")}</Select.Option>
|
||||
<Select.Option value="LAF">{t("joblines.fields.lbr_types.LAF")}</Select.Option>
|
||||
<Select.Option value="LAG">{t("joblines.fields.lbr_types.LAG")}</Select.Option>
|
||||
<Select.Option value="LAM">{t("joblines.fields.lbr_types.LAM")}</Select.Option>
|
||||
<Select.Option value="LAR">{t("joblines.fields.lbr_types.LAR")}</Select.Option>
|
||||
<Select.Option value="LAS">{t("joblines.fields.lbr_types.LAS")}</Select.Option>
|
||||
<Select.Option value="LAU">{t("joblines.fields.lbr_types.LAU")}</Select.Option>
|
||||
<Select.Option value="LA1">{t("joblines.fields.lbr_types.LA1")}</Select.Option>
|
||||
<Select.Option value="LA2">{t("joblines.fields.lbr_types.LA2")}</Select.Option>
|
||||
<Select.Option value="LA3">{t("joblines.fields.lbr_types.LA3")}</Select.Option>
|
||||
<Select.Option value="LA4">{t("joblines.fields.lbr_types.LA4")}</Select.Option>
|
||||
</Select>
|
||||
<Select
|
||||
allowClear
|
||||
disabled={!!mod_lbr_ty}
|
||||
options={[
|
||||
{ value: "LAA", label: t("joblines.fields.lbr_types.LAA") },
|
||||
{ value: "LAB", label: t("joblines.fields.lbr_types.LAB") },
|
||||
{ value: "LAD", label: t("joblines.fields.lbr_types.LAD") },
|
||||
{ value: "LAE", label: t("joblines.fields.lbr_types.LAE") },
|
||||
{ value: "LAF", label: t("joblines.fields.lbr_types.LAF") },
|
||||
{ value: "LAG", label: t("joblines.fields.lbr_types.LAG") },
|
||||
{ value: "LAM", label: t("joblines.fields.lbr_types.LAM") },
|
||||
{ value: "LAR", label: t("joblines.fields.lbr_types.LAR") },
|
||||
{ value: "LAS", label: t("joblines.fields.lbr_types.LAS") },
|
||||
{ value: "LAU", label: t("joblines.fields.lbr_types.LAU") },
|
||||
{ value: "LA1", label: t("joblines.fields.lbr_types.LA1") },
|
||||
{ value: "LA2", label: t("joblines.fields.lbr_types.LA2") },
|
||||
{ value: "LA3", label: t("joblines.fields.lbr_types.LA3") },
|
||||
{ value: "LA4", label: t("joblines.fields.lbr_types.LA4") }
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.adjustmenthours")}
|
||||
@@ -132,7 +136,13 @@ export function LaborAllocationsAdjustmentEdit({
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={(vis) => setOpen(vis)} content={overlay} trigger="click">
|
||||
<Popover
|
||||
getPopupContainer={(trigger) => trigger?.parentElement || document.body}
|
||||
open={open}
|
||||
onOpenChange={(vis) => setOpen(vis)}
|
||||
content={overlay}
|
||||
trigger="click"
|
||||
>
|
||||
{children}
|
||||
</Popover>
|
||||
);
|
||||
|
||||
@@ -56,7 +56,6 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount,
|
||||
where: whereClause
|
||||
},
|
||||
fetchPolicy: "cache-and-network",
|
||||
notifyOnNetworkStatusChange: true,
|
||||
errorPolicy: "all",
|
||||
pollInterval: isConnected ? 0 : day.duration(NOTIFICATION_POLL_INTERVAL_SECONDS, "seconds").asMilliseconds(),
|
||||
skip: skipQuery
|
||||
|
||||
@@ -7,8 +7,6 @@ import { SEARCH_OWNERS_BY_ID_FOR_AUTOCOMPLETE, SEARCH_OWNERS_FOR_AUTOCOMPLETE }
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
const OwnerSearchSelect = ({ value, onChange, onBlur, disabled, ref }) => {
|
||||
const [callSearch, { loading, error, data }] = useLazyQuery(SEARCH_OWNERS_FOR_AUTOCOMPLETE);
|
||||
|
||||
@@ -16,9 +14,10 @@ const OwnerSearchSelect = ({ value, onChange, onBlur, disabled, ref }) => {
|
||||
SEARCH_OWNERS_BY_ID_FOR_AUTOCOMPLETE
|
||||
);
|
||||
|
||||
const executeSearch = (v) => {
|
||||
if (v && v.variables?.search !== "" && v.variables.search.length >= 2) callSearch({ variables: v.variables });
|
||||
const executeSearch = (variables) => {
|
||||
if (variables?.search !== "" && variables?.search?.length >= 2) callSearch({ variables });
|
||||
};
|
||||
|
||||
const debouncedExecuteSearch = _.debounce(executeSearch, 500);
|
||||
|
||||
const handleSearch = (value) => {
|
||||
@@ -70,15 +69,12 @@ const OwnerSearchSelect = ({ value, onChange, onBlur, disabled, ref }) => {
|
||||
onSelect={handleSelect}
|
||||
notFoundContent={loading ? <LoadingOutlined /> : <Empty />}
|
||||
onBlur={onBlur}
|
||||
>
|
||||
{theOptions
|
||||
? theOptions.map((o) => (
|
||||
<Option key={o.id} value={o.id}>
|
||||
{`${OwnerNameDisplayFunction(o)} | ${o.ownr_addr1 || ""} `}
|
||||
</Option>
|
||||
))
|
||||
: null}
|
||||
</Select>
|
||||
options={theOptions?.map((o) => ({
|
||||
key: o.id,
|
||||
value: o.id,
|
||||
label: `${OwnerNameDisplayFunction(o)} | ${o.ownr_addr1 || ""} `
|
||||
}))}
|
||||
/>
|
||||
{idLoading || loading ? <LoadingOutlined /> : null}
|
||||
{error ? <AlertComponent title={error.message} type="error" /> : null}
|
||||
{idError ? <AlertComponent title={idError.message} type="error" /> : null}
|
||||
|
||||
@@ -1,94 +1,121 @@
|
||||
import { DownOutlined } from "@ant-design/icons";
|
||||
import { Dropdown, InputNumber, Space } from "antd";
|
||||
import { Button, Divider, Dropdown, InputNumber, Space, theme } from "antd";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
|
||||
const DISCOUNT_PRESETS = [5, 10, 15, 20, 25, 40];
|
||||
|
||||
export default function PartsOrderModalPriceChange({ form, field }) {
|
||||
const { t } = useTranslation();
|
||||
const menu = {
|
||||
items: [
|
||||
{
|
||||
key: "5",
|
||||
label: t("parts_orders.labels.discount", { percent: "5%" })
|
||||
},
|
||||
{
|
||||
key: "10",
|
||||
label: t("parts_orders.labels.discount", { percent: "10%" })
|
||||
},
|
||||
{
|
||||
key: "15",
|
||||
label: t("parts_orders.labels.discount", { percent: "15%" })
|
||||
},
|
||||
{
|
||||
key: "20",
|
||||
label: t("parts_orders.labels.discount", { percent: "20%" })
|
||||
},
|
||||
{
|
||||
key: "25",
|
||||
label: t("parts_orders.labels.discount", { percent: "25%" })
|
||||
},
|
||||
{
|
||||
key: "40",
|
||||
label: t("parts_orders.labels.discount", { percent: "40%" })
|
||||
},
|
||||
{
|
||||
key: "custom",
|
||||
label: (
|
||||
<Space.Compact>
|
||||
<InputNumber
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onKeyUp={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
const values = form.getFieldsValue();
|
||||
const { parts_order_lines } = values;
|
||||
const { token } = theme.useToken();
|
||||
|
||||
form.setFieldsValue({
|
||||
parts_order_lines: {
|
||||
data: parts_order_lines.data.map((p, idx) => {
|
||||
if (idx !== field.name) return p;
|
||||
console.log(p, e.target.value, (p.act_price || 0) * ((100 - (e.target.value || 0)) / 100));
|
||||
return {
|
||||
...p,
|
||||
act_price: (p.act_price || 0) * ((100 - (e.target.value || 0)) / 100)
|
||||
};
|
||||
})
|
||||
}
|
||||
});
|
||||
e.target.value = 0;
|
||||
}
|
||||
}}
|
||||
min={0}
|
||||
max={100}
|
||||
/>
|
||||
<span style={{ padding: "0 11px", backgroundColor: "#fafafa", border: "1px solid #d9d9d9", borderLeft: 0 }}>%</span>
|
||||
</Space.Compact>
|
||||
)
|
||||
const [open, setOpen] = useState(false);
|
||||
const [customPercent, setCustomPercent] = useState(0);
|
||||
|
||||
const applyDiscountPercent = (percent) => {
|
||||
const pct = Number(percent) || 0;
|
||||
|
||||
const values = form.getFieldsValue();
|
||||
const parts_order_lines = values?.parts_order_lines;
|
||||
const data = Array.isArray(parts_order_lines?.data) ? parts_order_lines.data : [];
|
||||
if (!data.length) return;
|
||||
|
||||
form.setFieldsValue({
|
||||
parts_order_lines: {
|
||||
data: data.map((p, idx) => {
|
||||
if (idx !== field.name) return p;
|
||||
return {
|
||||
...p,
|
||||
act_price: (p.act_price || 0) * ((100 - pct) / 100)
|
||||
};
|
||||
})
|
||||
}
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
const applyCustom = () => {
|
||||
logImEXEvent("parts_order_manual_discount", {});
|
||||
applyDiscountPercent(customPercent);
|
||||
setCustomPercent(0);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const menu = {
|
||||
// Kill the menu “card” styling so our wrapper becomes the single card.
|
||||
style: {
|
||||
background: "transparent",
|
||||
boxShadow: "none"
|
||||
},
|
||||
items: DISCOUNT_PRESETS.map((pct) => ({
|
||||
key: String(pct),
|
||||
label: t("parts_orders.labels.discount", { percent: `${pct}%` })
|
||||
})),
|
||||
onClick: ({ key }) => {
|
||||
logImEXEvent("parts_order_manual_discount", {});
|
||||
if (key === "custom") return;
|
||||
const values = form.getFieldsValue();
|
||||
const { parts_order_lines } = values;
|
||||
form.setFieldsValue({
|
||||
parts_order_lines: {
|
||||
data: parts_order_lines.data.map((p, idx) => {
|
||||
if (idx !== field.name) return p;
|
||||
return {
|
||||
...p,
|
||||
act_price: (p.act_price || 0) * ((100 - key) / 100)
|
||||
};
|
||||
})
|
||||
}
|
||||
});
|
||||
applyDiscountPercent(key);
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dropdown menu={menu} trigger="click">
|
||||
<Dropdown
|
||||
menu={menu}
|
||||
trigger={["click"]}
|
||||
open={open}
|
||||
onOpenChange={(nextOpen) => setOpen(nextOpen)}
|
||||
getPopupContainer={(triggerNode) => triggerNode?.parentElement ?? document.body}
|
||||
popupRender={(menus) => (
|
||||
<div
|
||||
// This makes the whole dropdown (menu + footer) look like one panel in both light/dark.
|
||||
style={{
|
||||
background: token.colorBgElevated,
|
||||
borderRadius: token.borderRadiusLG,
|
||||
boxShadow: token.boxShadowSecondary,
|
||||
overflow: "hidden",
|
||||
minWidth: 180
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
{menus}
|
||||
|
||||
<Divider style={{ margin: 0 }} />
|
||||
|
||||
<div style={{ padding: token.paddingXS }}>
|
||||
<Space.Compact style={{ width: "100%" }}>
|
||||
<InputNumber
|
||||
value={customPercent}
|
||||
min={0}
|
||||
max={100}
|
||||
precision={0}
|
||||
controls={false}
|
||||
style={{ width: "100%" }}
|
||||
formatter={(v) => (v === null || v === undefined ? "" : `${v}%`)}
|
||||
parser={(v) =>
|
||||
String(v ?? "")
|
||||
.replace("%", "")
|
||||
.trim()
|
||||
}
|
||||
onChange={(v) => setCustomPercent(v ?? 0)}
|
||||
onKeyDown={(e) => {
|
||||
e.stopPropagation();
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
applyCustom();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button type="primary" onClick={applyCustom}>
|
||||
{t("general.labels.apply")}
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<Space>
|
||||
%
|
||||
<DownOutlined />
|
||||
% <DownOutlined />
|
||||
</Space>
|
||||
</Dropdown>
|
||||
);
|
||||
|
||||
@@ -158,19 +158,21 @@ export function PartsOrderModalComponent({
|
||||
key={`${index}part_type`}
|
||||
name={[field.name, "part_type"]}
|
||||
>
|
||||
<Select disabled={!(sendType === "oec" && OEConnection_PriceChange.treatment === "on")}>
|
||||
<Select.Option value="PAA">{t("joblines.fields.part_types.PAA")}</Select.Option>
|
||||
<Select.Option value="PAC">{t("joblines.fields.part_types.PAC")}</Select.Option>
|
||||
|
||||
<Select.Option value="PAL">{t("joblines.fields.part_types.PAL")}</Select.Option>
|
||||
<Select.Option value="PAG">{t("joblines.fields.part_types.PAG")}</Select.Option>
|
||||
<Select.Option value="PAM">{t("joblines.fields.part_types.PAM")}</Select.Option>
|
||||
<Select.Option value="PAP">{t("joblines.fields.part_types.PAP")}</Select.Option>
|
||||
<Select.Option value="PAN">{t("joblines.fields.part_types.PAN")}</Select.Option>
|
||||
<Select.Option value="PAO">{t("joblines.fields.part_types.PAO")}</Select.Option>
|
||||
<Select.Option value="PAR">{t("joblines.fields.part_types.PAR")}</Select.Option>
|
||||
<Select.Option value="PAS">{t("joblines.fields.part_types.PAS")}</Select.Option>
|
||||
</Select>
|
||||
<Select
|
||||
disabled={!(sendType === "oec" && OEConnection_PriceChange.treatment === "on")}
|
||||
options={[
|
||||
{ value: "PAA", label: t("joblines.fields.part_types.PAA") },
|
||||
{ value: "PAC", label: t("joblines.fields.part_types.PAC") },
|
||||
{ value: "PAL", label: t("joblines.fields.part_types.PAL") },
|
||||
{ value: "PAG", label: t("joblines.fields.part_types.PAG") },
|
||||
{ value: "PAM", label: t("joblines.fields.part_types.PAM") },
|
||||
{ value: "PAP", label: t("joblines.fields.part_types.PAP") },
|
||||
{ value: "PAN", label: t("joblines.fields.part_types.PAN") },
|
||||
{ value: "PAO", label: t("joblines.fields.part_types.PAO") },
|
||||
{ value: "PAR", label: t("joblines.fields.part_types.PAR") },
|
||||
{ value: "PAS", label: t("joblines.fields.part_types.PAS") }
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("parts_orders.fields.oem_partno")}
|
||||
|
||||
@@ -17,7 +17,6 @@ import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import { GenerateDocument } from "../../utils/RenderTemplate";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||
import PartsOrderModalComponent from "./parts-order-modal.component";
|
||||
import axios from "axios";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
@@ -66,7 +65,7 @@ export function PartsOrderModalContainer({
|
||||
const sendTypeState = useState("e");
|
||||
const sendType = sendTypeState[0];
|
||||
|
||||
const { loading, error, data } = useQuery(QUERY_ALL_VENDORS_FOR_ORDER, {
|
||||
const { error, data } = useQuery(QUERY_ALL_VENDORS_FOR_ORDER, {
|
||||
skip: !open,
|
||||
variables: { jobId: jobId },
|
||||
fetchPolicy: "network-only",
|
||||
@@ -94,16 +93,6 @@ export function PartsOrderModalContainer({
|
||||
};
|
||||
});
|
||||
|
||||
const missingIdx = forcedLines.findIndex((l) => !l?.job_line_id);
|
||||
if (missingIdx !== -1) {
|
||||
notification.error({
|
||||
title: t("parts_orders.errors.creating"),
|
||||
description: `Missing job_line_id for parts line #${missingIdx + 1}`
|
||||
});
|
||||
setSaving(false);
|
||||
return;
|
||||
}
|
||||
|
||||
let insertResult;
|
||||
try {
|
||||
insertResult = await insertPartOrder({
|
||||
@@ -372,6 +361,7 @@ export function PartsOrderModalContainer({
|
||||
}
|
||||
}, [open, linesToOrder, form]);
|
||||
|
||||
//This used to have a loading component spinner for the vendor data. With Apollo 4, the NetworkState isn't emitting correctly, so loading just gets set to true the second time, and no longer works as expected.
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
@@ -390,18 +380,14 @@ export function PartsOrderModalContainer({
|
||||
>
|
||||
{error ? <AlertComponent title={error.message} type="error" /> : null}
|
||||
<Form form={form} layout="vertical" autoComplete="no" onFinish={handleFinish} initialValues={initialValues}>
|
||||
{loading ? (
|
||||
<LoadingSpinner />
|
||||
) : (
|
||||
<PartsOrderModalComponent
|
||||
form={form}
|
||||
vendorList={data?.vendors || []}
|
||||
sendTypeState={sendTypeState}
|
||||
isReturn={isReturn}
|
||||
preferredMake={data && data.jobs[0] && data.jobs[0].v_make_desc}
|
||||
job={job}
|
||||
/>
|
||||
)}
|
||||
<PartsOrderModalComponent
|
||||
form={form}
|
||||
vendorList={data?.vendors || []}
|
||||
sendTypeState={sendTypeState}
|
||||
isReturn={isReturn}
|
||||
preferredMake={data && data.jobs[0] && data.jobs[0].v_make_desc}
|
||||
job={job}
|
||||
/>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -29,13 +29,12 @@ export function PartsReceiveModalComponent({ bodyshop, form }) {
|
||||
})
|
||||
});
|
||||
}}
|
||||
>
|
||||
{bodyshop.md_parts_locations.map((loc, idx) => (
|
||||
<Select.Option key={idx} value={loc}>
|
||||
{loc}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
options={bodyshop.md_parts_locations.map((loc, idx) => ({
|
||||
key: idx,
|
||||
value: loc,
|
||||
label: loc
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<Typography.Title level={4}>{t("parts_orders.labels.inthisorder")}</Typography.Title>
|
||||
@@ -85,13 +84,14 @@ export function PartsReceiveModalComponent({ bodyshop, form }) {
|
||||
key={`${index}location`}
|
||||
name={[field.name, "location"]}
|
||||
>
|
||||
<Select style={{ width: "10rem" }}>
|
||||
{bodyshop.md_parts_locations.map((loc, idx) => (
|
||||
<Select.Option key={idx} value={loc}>
|
||||
{loc}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select
|
||||
style={{ width: "10rem" }}
|
||||
options={bodyshop.md_parts_locations.map((loc, idx) => ({
|
||||
key: idx,
|
||||
value: loc,
|
||||
label: loc
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("parts_orders.fields.quantity")}
|
||||
|
||||
@@ -2,8 +2,6 @@ import { Button, Card, Divider, Form, Input, Select, Space } from "antd";
|
||||
import { DeleteOutlined, PlusOutlined } from "@ant-design/icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
export default function PartsShopInfoEmailPresets() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -26,13 +24,7 @@ export default function PartsShopInfoEmailPresets() {
|
||||
label={t("bodyshop.labels.email_type")}
|
||||
rules={[{ required: true, message: t("bodyshop.errors.email_type_required") }]}
|
||||
>
|
||||
<Select placeholder={t("bodyshop.placeholders.select_email_type")}>
|
||||
{emailTypes.map((type) => (
|
||||
<Option key={type.value} value={type.value}>
|
||||
{type.label}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select placeholder={t("bodyshop.placeholders.select_email_type")} options={emailTypes} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
|
||||
@@ -91,20 +91,25 @@ export function PaymentFormComponent({ form, bodyshop, disabled }) {
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select disabled={disabled}>
|
||||
<Select.Option value={t("payments.labels.customer")}>{t("payments.labels.customer")}</Select.Option>
|
||||
{Qb_Multi_Ar.treatment === "on" ? (
|
||||
<Select.OptGroup label={t("payments.labels.external")}>
|
||||
{bodyshop.md_ins_cos.map((i, idx) => (
|
||||
<Select.Option key={idx} value={i.name}>
|
||||
{i.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select.OptGroup>
|
||||
) : (
|
||||
<Select.Option value={t("payments.labels.insurance")}>{t("payments.labels.insurance")}</Select.Option>
|
||||
)}
|
||||
</Select>
|
||||
<Select disabled={disabled}
|
||||
options={Qb_Multi_Ar.treatment === "on"
|
||||
? [
|
||||
{ value: t("payments.labels.customer"), label: t("payments.labels.customer") },
|
||||
{
|
||||
label: t("payments.labels.external"),
|
||||
options: bodyshop.md_ins_cos.map((i, idx) => ({
|
||||
key: idx,
|
||||
value: i.name,
|
||||
label: i.name
|
||||
}))
|
||||
}
|
||||
]
|
||||
: [
|
||||
{ value: t("payments.labels.customer"), label: t("payments.labels.customer") },
|
||||
{ value: t("payments.labels.insurance"), label: t("payments.labels.insurance") }
|
||||
]
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
@@ -117,13 +122,13 @@ export function PaymentFormComponent({ form, bodyshop, disabled }) {
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select disabled={disabled}>
|
||||
{bodyshop.md_payment_types.map((v, idx) => (
|
||||
<Select.Option key={idx} value={v}>
|
||||
{v}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select disabled={disabled}
|
||||
options={bodyshop.md_payment_types.map((v, idx) => ({
|
||||
key: idx,
|
||||
value: v,
|
||||
label: v
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow grow>
|
||||
|
||||
@@ -33,7 +33,7 @@ export default function PaymentFormTotalPayments({ jobid }) {
|
||||
{balance && (
|
||||
<Statistic
|
||||
title={t("payments.labels.balance")}
|
||||
styles={{ value: { color: balance.getAmount() !== 0 ? "red" : "green" } }}
|
||||
styles={{ content: { color: balance.getAmount() !== 0 ? "red" : "green" } }}
|
||||
value={(balance && balance.toFormat()) || ""}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -108,7 +108,7 @@ export function PrintCenterJobsLabels({ jobId }) {
|
||||
</Card>
|
||||
);
|
||||
return (
|
||||
<Popover content={content} open={isModalVisible}>
|
||||
<Popover content={content} open={isModalVisible} getPopupContainer={(trigger) => trigger.parentElement}>
|
||||
<Button onClick={() => setIsModalVisible(true)}>{t("printcenter.jobs.labels.labels")}</Button>
|
||||
</Popover>
|
||||
);
|
||||
|
||||
@@ -35,8 +35,6 @@ export function ProductionListEmpAssignment({ insertAuditTrail, bodyshop, record
|
||||
|
||||
const result = await updateJob({
|
||||
variables: { jobId: record.id, job: { [empAssignment]: employeeid } }
|
||||
|
||||
// awaitRefetchQueries: true,
|
||||
});
|
||||
|
||||
insertAuditTrail({
|
||||
@@ -55,6 +53,7 @@ export function ProductionListEmpAssignment({ insertAuditTrail, bodyshop, record
|
||||
|
||||
await refetch();
|
||||
|
||||
setAssignment({ operation: null, employeeid: null });
|
||||
setLoading(false);
|
||||
};
|
||||
const handleRemove = async (operation) => {
|
||||
@@ -84,6 +83,7 @@ export function ProductionListEmpAssignment({ insertAuditTrail, bodyshop, record
|
||||
|
||||
await refetch();
|
||||
|
||||
setAssignment({ operation: null, employeeid: null });
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
@@ -94,29 +94,31 @@ export function ProductionListEmpAssignment({ insertAuditTrail, bodyshop, record
|
||||
|
||||
const [visibility, setVisibility] = useState(false);
|
||||
const onChange = (e, option) => {
|
||||
setAssignment({ ...assignment, employeeid: e, name: option.name });
|
||||
setAssignment({ ...assignment, employeeid: e, name: option.label });
|
||||
};
|
||||
|
||||
const employeeOptions = bodyshop.employees
|
||||
.filter((emp) => emp.active)
|
||||
.map((emp) => ({
|
||||
value: emp.id,
|
||||
label: `${emp.first_name} ${emp.last_name}`,
|
||||
name: `${emp.first_name} ${emp.last_name}`
|
||||
}));
|
||||
|
||||
const popContent = (
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
<Select
|
||||
id="employeeSelector"
|
||||
showSearch={{
|
||||
optionFilterProp: "children",
|
||||
filterOption: (input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
optionFilterProp: "label",
|
||||
filterOption: (input, option) => option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}}
|
||||
style={{ width: 200 }}
|
||||
value={assignment.employeeid}
|
||||
onChange={onChange}
|
||||
>
|
||||
{bodyshop.employees
|
||||
.filter((emp) => emp.active)
|
||||
.map((emp) => (
|
||||
<Select.Option value={emp.id} key={emp.id} name={`${emp.first_name} ${emp.last_name}`}>
|
||||
{`${emp.first_name} ${emp.last_name}`}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
options={employeeOptions}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Space wrap>
|
||||
@@ -141,25 +143,25 @@ export function ProductionListEmpAssignment({ insertAuditTrail, bodyshop, record
|
||||
if (record[type]) theEmployee = bodyshop.employees.find((e) => e.id === record[type]);
|
||||
|
||||
return (
|
||||
<Popover destroyOnHidden content={popContent} open={visibility}>
|
||||
<Spin spinning={loading}>
|
||||
{record[type] ? (
|
||||
<div style={{ cursor: "pointer" }}>
|
||||
<span>{`${theEmployee?.first_name || ""} ${theEmployee?.last_name || ""}`}</span>
|
||||
<DeleteFilled style={iconStyle} onClick={() => handleRemove(type)} />
|
||||
</div>
|
||||
) : (
|
||||
<Spin spinning={loading}>
|
||||
{record[type] ? (
|
||||
<div style={{ cursor: "pointer" }}>
|
||||
<span>{`${theEmployee?.first_name || ""} ${theEmployee?.last_name || ""}`}</span>
|
||||
<DeleteFilled style={iconStyle} onClick={() => handleRemove(type)} />
|
||||
</div>
|
||||
) : (
|
||||
<Popover destroyOnHidden content={popContent} open={visibility} trigger="click">
|
||||
<PlusCircleFilled
|
||||
style={{ ...iconStyle, cursor: "pointer" }}
|
||||
className="muted-button"
|
||||
onClick={() => {
|
||||
setAssignment({ operation: type });
|
||||
setAssignment({ operation: type, employeeid: null });
|
||||
setVisibility(true);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Spin>
|
||||
</Popover>
|
||||
</Popover>
|
||||
)}
|
||||
</Spin>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -453,10 +453,10 @@ export function ProductionListConfigManager({
|
||||
}}
|
||||
onSelect={handleSelect}
|
||||
placeholder={t("production.labels.selectview")}
|
||||
optionLabelProp="label"
|
||||
popupMatchSelectWidth={false}
|
||||
value={activeView}
|
||||
disabled={open || isAddingNewProfile} // Disable the Select box when the popover is open or adding a new profile
|
||||
optionLabelProp="label"
|
||||
>
|
||||
{bodyshop?.production_config &&
|
||||
bodyshop.production_config
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import { SyncOutlined } from "@ant-design/icons";
|
||||
import { HolderOutlined, SyncOutlined } from "@ant-design/icons";
|
||||
import { PageHeader } from "@ant-design/pro-layout";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { Button, Dropdown, Input, Space, Statistic, Table } from "antd";
|
||||
import _ from "lodash";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import ReactDragListView from "react-drag-listview";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { closestCenter, DndContext, DragOverlay, PointerSensor, useSensor, useSensors } from "@dnd-kit/core";
|
||||
import { arrayMove, horizontalListSortingStrategy, SortableContext, useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { restrictToHorizontalAxis } from "@dnd-kit/modifiers";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectTechnician } from "../../redux/tech/tech.selectors";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import { selectDarkMode } from "../../redux/application/application.selectors.js";
|
||||
import Prompt from "../../utils/prompt.js";
|
||||
import AlertComponent from "../alert/alert.component.jsx";
|
||||
import ProductionListColumnsAdd from "../production-list-columns/production-list-columns.add.component";
|
||||
@@ -23,12 +27,81 @@ import { logImEXEvent } from "../../firebase/firebase.utils.js";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
technician: selectTechnician,
|
||||
currentUser: selectCurrentUser
|
||||
currentUser: selectCurrentUser,
|
||||
isDarkMode: selectDarkMode
|
||||
});
|
||||
|
||||
export function ProductionListTable({ loading, data, refetch, bodyshop, technician, currentUser }) {
|
||||
// Draggable header cell component - combines drag and resize
|
||||
function DraggableHeaderCell(props) {
|
||||
const { children, columnKey, onResize, width, ...restProps } = props;
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||
id: columnKey,
|
||||
disabled: !columnKey
|
||||
});
|
||||
|
||||
const style = {
|
||||
...restProps.style,
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0.3 : 1,
|
||||
userSelect: "none",
|
||||
textAlign: "left"
|
||||
};
|
||||
|
||||
// If no columnKey, render as regular header
|
||||
if (!columnKey) {
|
||||
return <ResizeableTitle {...props} />;
|
||||
}
|
||||
|
||||
// Only apply drag listeners to elements with data-drag-handle attribute
|
||||
const filteredListeners = listeners
|
||||
? {
|
||||
onPointerDown: (e) => {
|
||||
// Only trigger drag if clicking on the drag handle
|
||||
if (e.target.closest('[data-drag-handle="true"]')) {
|
||||
listeners.onPointerDown?.(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
: {};
|
||||
|
||||
// Combine drag functionality with resize
|
||||
return (
|
||||
<ResizeableTitle
|
||||
{...restProps}
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
onResize={onResize}
|
||||
width={width}
|
||||
dragAttributes={attributes}
|
||||
dragListeners={filteredListeners}
|
||||
>
|
||||
{children}
|
||||
</ResizeableTitle>
|
||||
);
|
||||
}
|
||||
|
||||
export function ProductionListTable({ loading, data, refetch, bodyshop, technician, currentUser, isDarkMode }) {
|
||||
const [searchText, setSearchText] = useState("");
|
||||
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
||||
|
||||
// NEW: smoother resize
|
||||
const [isResizing, setIsResizing] = useState(false);
|
||||
const resizeRafRef = useRef(null);
|
||||
const pendingResizeRef = useRef(null);
|
||||
|
||||
const [activeId, setActiveId] = useState(null);
|
||||
|
||||
const MIN_COL_WIDTH = 20;
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: {
|
||||
distance: 1
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const {
|
||||
treatments: { Production_List_Status_Colors, Enhanced_Payroll }
|
||||
} = useTreatmentsWithConfig({
|
||||
@@ -36,8 +109,10 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
|
||||
names: ["Production_List_Status_Colors", "Enhanced_Payroll"],
|
||||
splitKey: bodyshop.imexshopid
|
||||
});
|
||||
|
||||
const assoc = bodyshop.associations.find((a) => a.useremail === currentUser.email);
|
||||
const defaultView = assoc?.default_prod_list_view;
|
||||
|
||||
const initialStateRef = useRef(
|
||||
(bodyshop.production_config &&
|
||||
bodyshop.production_config.find((p) => p.name === defaultView)?.columns.tableState) ||
|
||||
@@ -46,6 +121,7 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
|
||||
filteredInfo: { text: "" }
|
||||
}
|
||||
);
|
||||
|
||||
const initialColumnsRef = useRef(
|
||||
(initialStateRef.current &&
|
||||
bodyshop?.production_config
|
||||
@@ -66,14 +142,36 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
|
||||
})) ||
|
||||
[]
|
||||
);
|
||||
|
||||
const [state, setState] = useState(initialStateRef.current);
|
||||
const [columns, setColumns] = useState(initialColumnsRef.current);
|
||||
|
||||
const scrollX = useMemo(() => {
|
||||
// keep scroll width aligned with the actual column widths so AntD doesn't clamp at a fixed floor
|
||||
const sum = columns.reduce((acc, c) => acc + (c.width ?? 100), 0);
|
||||
return Math.max(sum, 1);
|
||||
}, [columns]);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const matchingColumnConfig = useMemo(() => {
|
||||
return bodyshop?.production_config?.find((p) => p.name === defaultView);
|
||||
}, [bodyshop.production_config, defaultView]);
|
||||
|
||||
// NEW: cleanup RAF on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (resizeRafRef.current) cancelAnimationFrame(resizeRafRef.current);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// NEW: while resizing, don’t regenerate columns
|
||||
if (isResizing) return;
|
||||
|
||||
// NEW: bail early BEFORE expensive ProductionListColumns(...) call
|
||||
if (!_.isEqual(initialColumnsRef.current, columns)) return;
|
||||
|
||||
const newColumns =
|
||||
matchingColumnConfig?.columns.columnKeys.map((k) => {
|
||||
return {
|
||||
@@ -89,10 +187,8 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
|
||||
width: k.width ?? 100
|
||||
};
|
||||
}) || [];
|
||||
// Only update columns if they haven't been manually changed by the user
|
||||
if (_.isEqual(initialColumnsRef.current, columns)) {
|
||||
setColumns(newColumns);
|
||||
}
|
||||
|
||||
setColumns(newColumns);
|
||||
}, [
|
||||
matchingColumnConfig,
|
||||
bodyshop,
|
||||
@@ -102,7 +198,8 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
|
||||
Production_List_Status_Colors,
|
||||
refetch,
|
||||
state,
|
||||
columns
|
||||
columns,
|
||||
isResizing
|
||||
]);
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
@@ -118,17 +215,30 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
|
||||
logImEXEvent("production_list_sort_filter", { pagination, filters, sorter });
|
||||
};
|
||||
|
||||
const onDragEnd = (fromIndex, toIndex) => {
|
||||
if (fromIndex === toIndex) return;
|
||||
const columnsCopy = [...columns];
|
||||
const [movedItem] = columnsCopy.splice(fromIndex, 1);
|
||||
columnsCopy.splice(toIndex, 0, movedItem);
|
||||
if (!_.isEqual(columnsCopy, columns)) {
|
||||
setColumns(columnsCopy);
|
||||
setHasUnsavedChanges(true);
|
||||
const onDragStart = ({ active }) => {
|
||||
setActiveId(active.id);
|
||||
};
|
||||
|
||||
const onDragEnd = ({ active, over }) => {
|
||||
setActiveId(null);
|
||||
if (!over || active.id === over.id) return;
|
||||
|
||||
const oldIndex = columns.findIndex((col) => col.key === active.id);
|
||||
const newIndex = columns.findIndex((col) => col.key === over.id);
|
||||
|
||||
if (oldIndex !== -1 && newIndex !== -1) {
|
||||
const newColumns = arrayMove(columns, oldIndex, newIndex);
|
||||
if (!_.isEqual(newColumns, columns)) {
|
||||
setColumns(newColumns);
|
||||
setHasUnsavedChanges(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onDragCancel = () => {
|
||||
setActiveId(null);
|
||||
};
|
||||
|
||||
const removeColumn = (e) => {
|
||||
const { key } = e;
|
||||
const newColumns = columns.filter((i) => i.key !== key);
|
||||
@@ -139,19 +249,55 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
|
||||
logImEXEvent("production_list_remove_column", { key });
|
||||
};
|
||||
|
||||
const handleResize =
|
||||
(index) =>
|
||||
(e, { size }) => {
|
||||
const nextColumns = [...columns];
|
||||
nextColumns[index] = {
|
||||
...nextColumns[index],
|
||||
width: size.width
|
||||
};
|
||||
if (!_.isEqual(nextColumns, columns)) {
|
||||
setColumns(nextColumns);
|
||||
// NEW: commit widths via rAF (less jank)
|
||||
const applyColumnWidth = useCallback((columnKey, width) => {
|
||||
const nextWidth = Math.max(MIN_COL_WIDTH, Math.round(width));
|
||||
setColumns((prev) => {
|
||||
const idx = prev.findIndex((c) => c.key === columnKey);
|
||||
if (idx === -1) return prev;
|
||||
|
||||
const currentWidth = prev[idx].width ?? 100;
|
||||
if (currentWidth === nextWidth) return prev;
|
||||
|
||||
const next = prev.slice();
|
||||
next[idx] = { ...next[idx], width: nextWidth };
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleResize = useCallback(
|
||||
(columnKey) =>
|
||||
(e, { size }) => {
|
||||
pendingResizeRef.current = { columnKey, width: size.width };
|
||||
|
||||
if (resizeRafRef.current) return;
|
||||
resizeRafRef.current = requestAnimationFrame(() => {
|
||||
resizeRafRef.current = null;
|
||||
const pending = pendingResizeRef.current;
|
||||
if (!pending) return;
|
||||
applyColumnWidth(pending.columnKey, pending.width);
|
||||
});
|
||||
},
|
||||
[applyColumnWidth]
|
||||
);
|
||||
|
||||
const handleResizeStart = useCallback(() => {
|
||||
setIsResizing(true);
|
||||
}, []);
|
||||
|
||||
const handleResizeStop = useCallback(
|
||||
(columnKey) =>
|
||||
(e, { size }) => {
|
||||
setIsResizing(false);
|
||||
|
||||
// Ensure final width is committed
|
||||
applyColumnWidth(columnKey, size.width);
|
||||
|
||||
setHasUnsavedChanges(true);
|
||||
}
|
||||
};
|
||||
logImEXEvent("production_list_resize_column", { key: columnKey, width: size.width });
|
||||
},
|
||||
[applyColumnWidth]
|
||||
);
|
||||
|
||||
const addColumn = (newColumn) => {
|
||||
const updatedColumns = [...columns, newColumn];
|
||||
@@ -163,19 +309,53 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
|
||||
};
|
||||
|
||||
const headerItem = (col) => {
|
||||
const menu = {
|
||||
onClick: removeColumn,
|
||||
items: [
|
||||
{
|
||||
key: col.key,
|
||||
label: t("production.actions.removecolumn")
|
||||
}
|
||||
]
|
||||
};
|
||||
const menu = { onClick: removeColumn, items: [{ key: col.key, label: t("production.actions.removecolumn") }] };
|
||||
|
||||
return (
|
||||
<Dropdown className="prod-header-dropdown" menu={menu} trigger={["contextMenu"]}>
|
||||
<span>{col.title}</span>
|
||||
</Dropdown>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "left",
|
||||
width: "100%",
|
||||
userSelect: "none",
|
||||
minWidth: 0 // critical: allow the flex row to shrink
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="drag-handle-trigger"
|
||||
data-drag-handle="true"
|
||||
style={{
|
||||
marginRight: 8,
|
||||
color: "#999",
|
||||
cursor: "grab",
|
||||
padding: 4,
|
||||
display: "inline-flex",
|
||||
alignItems: "left",
|
||||
userSelect: "none",
|
||||
flex: "0 0 auto"
|
||||
}}
|
||||
title="Drag to reorder column"
|
||||
>
|
||||
<HolderOutlined />
|
||||
</span>
|
||||
|
||||
<Dropdown className="prod-header-dropdown" menu={menu} trigger={["contextMenu"]}>
|
||||
<span
|
||||
style={{
|
||||
flex: "1 1 auto",
|
||||
minWidth: 0, // critical: allow text to shrink
|
||||
overflow: "hidden", // clip
|
||||
textOverflow: "ellipsis", // show …
|
||||
whiteSpace: "nowrap", // keep single line
|
||||
cursor: "default",
|
||||
userSelect: "none",
|
||||
display: "block"
|
||||
}}
|
||||
>
|
||||
{col.title}
|
||||
</span>
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -274,6 +454,9 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
|
||||
onSave={() => {
|
||||
setHasUnsavedChanges(false);
|
||||
initialStateRef.current = state;
|
||||
|
||||
// NEW: after saving, treat current columns as the baseline
|
||||
initialColumnsRef.current = columns;
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
@@ -286,60 +469,104 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
|
||||
}
|
||||
/>
|
||||
<ProductionListDetail jobs={dataSource} />
|
||||
<ReactDragListView.DragColumn onDragEnd={onDragEnd} nodeSelector="th" handleSelector=".prod-header-dropdown">
|
||||
<Table
|
||||
sticky
|
||||
pagination={false}
|
||||
size="small"
|
||||
{...(Production_List_Status_Colors.treatment === "on" && {
|
||||
onRow: (record, index) => {
|
||||
if (!bodyshop.md_ro_statuses.production_colors) return null;
|
||||
const color = bodyshop.md_ro_statuses.production_colors.find((x) => x.status === record.status);
|
||||
if (!color) {
|
||||
if (index % 2 === 0)
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
onDragStart={onDragStart}
|
||||
onDragEnd={onDragEnd}
|
||||
onDragCancel={onDragCancel}
|
||||
collisionDetection={closestCenter}
|
||||
modifiers={[restrictToHorizontalAxis]}
|
||||
>
|
||||
<SortableContext items={columns.map((col) => col.key)} strategy={horizontalListSortingStrategy}>
|
||||
<Table
|
||||
sticky
|
||||
tableLayout="fixed"
|
||||
className="prod-list-table"
|
||||
pagination={false}
|
||||
size="small"
|
||||
{...(Production_List_Status_Colors.treatment === "on" &&
|
||||
!isResizing && {
|
||||
onRow: (record, index) => {
|
||||
if (!bodyshop.md_ro_statuses.production_colors) return null;
|
||||
const color = bodyshop.md_ro_statuses.production_colors.find((x) => x.status === record.status);
|
||||
if (!color) {
|
||||
if (index % 2 === 0)
|
||||
return {
|
||||
style: {
|
||||
backgroundColor: "var(--table-row-even-bg)"
|
||||
}
|
||||
};
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
className: "rowWithColor",
|
||||
style: {
|
||||
backgroundColor: "var(--table-row-even-bg)"
|
||||
"--bgColor": color.color
|
||||
? `rgba(${color.color.r},${color.color.g},${color.color.b},${color.color.a || 1})`
|
||||
: "var(--status-row-bg-fallback)"
|
||||
}
|
||||
};
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
className: "rowWithColor",
|
||||
style: {
|
||||
"--bgColor": color.color
|
||||
? `rgba(${color.color.r},${color.color.g},${color.color.b},${color.color.a || 1})`
|
||||
: "var(--status-row-bg-fallback)"
|
||||
}
|
||||
})}
|
||||
components={{
|
||||
header: {
|
||||
cell: DraggableHeaderCell
|
||||
}
|
||||
}}
|
||||
columns={columns.map((c) => {
|
||||
return {
|
||||
...c,
|
||||
filteredValue: state.filteredInfo[c.key] || null,
|
||||
sortOrder: state.sortedInfo.columnKey === c.key && state.sortedInfo.order,
|
||||
title: headerItem(c),
|
||||
ellipsis: true,
|
||||
width: c.width ?? 100,
|
||||
onHeaderCell: (column) => ({
|
||||
columnKey: column.key,
|
||||
width: column.width,
|
||||
onResize: handleResize(column.key),
|
||||
onResizeStart: handleResizeStart,
|
||||
onResizeStop: handleResizeStop(column.key)
|
||||
})
|
||||
};
|
||||
}
|
||||
})}
|
||||
components={{
|
||||
header: {
|
||||
cell: ResizeableTitle
|
||||
}
|
||||
}}
|
||||
columns={columns.map((c, index) => {
|
||||
return {
|
||||
...c,
|
||||
filteredValue: state.filteredInfo[c.key] || null,
|
||||
sortOrder: state.sortedInfo.columnKey === c.key && state.sortedInfo.order,
|
||||
title: headerItem(c),
|
||||
ellipsis: true,
|
||||
width: c.width ?? 100,
|
||||
onHeaderCell: (column) => ({
|
||||
width: column.width,
|
||||
onResize: handleResize(index)
|
||||
})
|
||||
};
|
||||
})}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
dataSource={dataSource}
|
||||
scroll={{ x: 1000 }}
|
||||
onChange={handleTableChange}
|
||||
/>
|
||||
</ReactDragListView.DragColumn>
|
||||
})}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
dataSource={dataSource}
|
||||
scroll={{ x: scrollX }}
|
||||
onChange={handleTableChange}
|
||||
/>
|
||||
</SortableContext>
|
||||
|
||||
<DragOverlay dropAnimation={null}>
|
||||
{activeId ? (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: isDarkMode ? "#141414" : "white",
|
||||
color: isDarkMode ? "white" : "#000",
|
||||
border: `2px solid ${isDarkMode ? "#177ddc" : "#1890ff"}`,
|
||||
borderRadius: "4px",
|
||||
padding: "12px 16px",
|
||||
boxShadow: "0 4px 12px rgba(0, 0, 0, 0.25)",
|
||||
cursor: "grabbing",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
fontWeight: 500,
|
||||
minWidth: "120px"
|
||||
}}
|
||||
>
|
||||
<HolderOutlined style={{ marginRight: "8px", color: isDarkMode ? "white" : "#000", fontSize: "16px" }} />
|
||||
<span>
|
||||
{(() => {
|
||||
const col = columns.find((c) => c.key === activeId);
|
||||
const title = typeof col?.title === "string" ? col.title : col?.dataIndex || col?.key || "Column";
|
||||
return title;
|
||||
})()}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
</DragOverlay>
|
||||
</DndContext>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,28 +1,37 @@
|
||||
import { forwardRef } from "react";
|
||||
import { Resizable } from "react-resizable";
|
||||
import "react-resizable/css/styles.css";
|
||||
|
||||
export default function ResizableComponent(props) {
|
||||
const { onResize, width, ...restProps } = props;
|
||||
const ResizableComponent = forwardRef((props, ref) => {
|
||||
const { onResize, onResizeStart, onResizeStop, width, dragAttributes, dragListeners, ...restProps } = props;
|
||||
|
||||
if (!width) {
|
||||
return <th {...restProps} />;
|
||||
return <th ref={ref} {...restProps} {...(dragAttributes || {})} {...(dragListeners || {})} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Resizable
|
||||
width={width || 200}
|
||||
width={width}
|
||||
height={0}
|
||||
onResize={onResize}
|
||||
onResizeStart={onResizeStart}
|
||||
onResizeStop={onResizeStop}
|
||||
draggableOpts={{ enableUserSelectHack: false }}
|
||||
handle={
|
||||
resizeHandles={["e"]}
|
||||
axis="x"
|
||||
handle={(axis, handleRef) => (
|
||||
<span
|
||||
className="react-resizable-handle"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
ref={handleRef}
|
||||
className={`react-resizable-handle react-resizable-handle-${axis}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
}
|
||||
)}
|
||||
>
|
||||
<th {...restProps} />
|
||||
<th ref={ref} {...restProps} {...(dragAttributes || {})} {...(dragListeners || {})} />
|
||||
</Resizable>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
ResizableComponent.displayName = "ResizableComponent";
|
||||
|
||||
export default ResizableComponent;
|
||||
|
||||
@@ -158,20 +158,28 @@ export function ScheduleJobModalComponent({
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow grow>
|
||||
<Form.Item name="color" label={t("appointments.fields.color")}>
|
||||
<Select allowClear>
|
||||
{bodyshop.appt_colors &&
|
||||
bodyshop.appt_colors.map((color) => (
|
||||
<Select.Option style={{ color: color.color.hex }} key={color.color.hex} value={color.color.hex}>
|
||||
{color.label}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select
|
||||
allowClear
|
||||
options={
|
||||
bodyshop.appt_colors &&
|
||||
bodyshop.appt_colors.map((color) => ({
|
||||
value: color.color.hex,
|
||||
label: color.label
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name={"alt_transport"} label={t("jobs.fields.alt_transport")}>
|
||||
<Select allowClear>
|
||||
{bodyshop.appt_alt_transport &&
|
||||
bodyshop.appt_alt_transport.map((alt) => <Select.Option key={alt}>{alt}</Select.Option>)}
|
||||
</Select>
|
||||
<Select
|
||||
allowClear
|
||||
options={
|
||||
bodyshop.appt_alt_transport &&
|
||||
bodyshop.appt_alt_transport.map((alt) => ({
|
||||
value: alt,
|
||||
label: alt
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name={"note"} label={t("appointments.fields.note")}>
|
||||
<Input />
|
||||
|
||||
@@ -120,13 +120,12 @@ export function ScheduleManualEvent({ bodyshop, event }) {
|
||||
<FormDateTimePickerComponent />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("appointments.fields.color")} name="color">
|
||||
<Select>
|
||||
{bodyshop.appt_colors.map((col, idx) => (
|
||||
<Select.Option key={idx} value={col.color.hex}>
|
||||
{col.label}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select
|
||||
options={bodyshop.appt_colors.map((col) => ({
|
||||
value: col.color.hex,
|
||||
label: col.label
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Space wrap>
|
||||
|
||||
@@ -325,22 +325,20 @@ export function ShopEmployeesFormComponent({ bodyshop }) {
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select>
|
||||
<Select.Option key={"shift"} value="timetickets.labels.shift">
|
||||
{t("timetickets.labels.shift")}
|
||||
</Select.Option>
|
||||
|
||||
{bodyshop.cdk_dealerid ||
|
||||
bodyshop.pbs_serialnumber ||
|
||||
bodyshop.rr_dealerid ||
|
||||
Enhanced_Payroll.treatment === "on"
|
||||
? CiecaSelect(false, true)
|
||||
: bodyshop.md_responsibility_centers.costs.map((c) => (
|
||||
<Select.Option key={c.name} value={c.name}>
|
||||
{c.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select
|
||||
options={[
|
||||
{ value: "timetickets.labels.shift", label: t("timetickets.labels.shift") },
|
||||
...(bodyshop.cdk_dealerid ||
|
||||
bodyshop.pbs_serialnumber ||
|
||||
bodyshop.rr_dealerid ||
|
||||
Enhanced_Payroll.treatment === "on"
|
||||
? CiecaSelect(false, true)
|
||||
: bodyshop.md_responsibility_centers.costs.map((c) => ({
|
||||
value: c.name,
|
||||
label: c.name
|
||||
})))
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("employees.fields.rate")}
|
||||
|
||||
@@ -1039,22 +1039,25 @@ export function ShopInfoGeneral({ form }) {
|
||||
key={`${index}mod_lbr_ty`}
|
||||
name={[field.name, "mod_lbr_ty"]}
|
||||
>
|
||||
<Select allowClear>
|
||||
<Select.Option value="LAA">{t("joblines.fields.lbr_types.LAA")}</Select.Option>
|
||||
<Select.Option value="LAB">{t("joblines.fields.lbr_types.LAB")}</Select.Option>
|
||||
<Select.Option value="LAD">{t("joblines.fields.lbr_types.LAD")}</Select.Option>
|
||||
<Select.Option value="LAE">{t("joblines.fields.lbr_types.LAE")}</Select.Option>
|
||||
<Select.Option value="LAF">{t("joblines.fields.lbr_types.LAF")}</Select.Option>
|
||||
<Select.Option value="LAG">{t("joblines.fields.lbr_types.LAG")}</Select.Option>
|
||||
<Select.Option value="LAM">{t("joblines.fields.lbr_types.LAM")}</Select.Option>
|
||||
<Select.Option value="LAR">{t("joblines.fields.lbr_types.LAR")}</Select.Option>
|
||||
<Select.Option value="LAS">{t("joblines.fields.lbr_types.LAS")}</Select.Option>
|
||||
<Select.Option value="LAU">{t("joblines.fields.lbr_types.LAU")}</Select.Option>
|
||||
<Select.Option value="LA1">{t("joblines.fields.lbr_types.LA1")}</Select.Option>
|
||||
<Select.Option value="LA2">{t("joblines.fields.lbr_types.LA2")}</Select.Option>
|
||||
<Select.Option value="LA3">{t("joblines.fields.lbr_types.LA3")}</Select.Option>
|
||||
<Select.Option value="LA4">{t("joblines.fields.lbr_types.LA4")}</Select.Option>
|
||||
</Select>
|
||||
<Select
|
||||
allowClear
|
||||
options={[
|
||||
{ value: "LAA", label: t("joblines.fields.lbr_types.LAA") },
|
||||
{ value: "LAB", label: t("joblines.fields.lbr_types.LAB") },
|
||||
{ value: "LAD", label: t("joblines.fields.lbr_types.LAD") },
|
||||
{ value: "LAE", label: t("joblines.fields.lbr_types.LAE") },
|
||||
{ value: "LAF", label: t("joblines.fields.lbr_types.LAF") },
|
||||
{ value: "LAG", label: t("joblines.fields.lbr_types.LAG") },
|
||||
{ value: "LAM", label: t("joblines.fields.lbr_types.LAM") },
|
||||
{ value: "LAR", label: t("joblines.fields.lbr_types.LAR") },
|
||||
{ value: "LAS", label: t("joblines.fields.lbr_types.LAS") },
|
||||
{ value: "LAU", label: t("joblines.fields.lbr_types.LAU") },
|
||||
{ value: "LA1", label: t("joblines.fields.lbr_types.LA1") },
|
||||
{ value: "LA2", label: t("joblines.fields.lbr_types.LA2") },
|
||||
{ value: "LA3", label: t("joblines.fields.lbr_types.LA3") },
|
||||
{ value: "LA4", label: t("joblines.fields.lbr_types.LA4") }
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("joblines.fields.mod_lb_hrs")}
|
||||
@@ -1068,17 +1071,20 @@ export function ShopInfoGeneral({ form }) {
|
||||
key={`${index}part_type`}
|
||||
name={[field.name, "part_type"]}
|
||||
>
|
||||
<Select allowClear>
|
||||
<Select.Option value="PAA">{t("joblines.fields.part_types.PAA")}</Select.Option>
|
||||
<Select.Option value="PAC">{t("joblines.fields.part_types.PAC")}</Select.Option>
|
||||
<Select.Option value="PAE">{t("joblines.fields.part_types.PAE")}</Select.Option>
|
||||
<Select.Option value="PAL">{t("joblines.fields.part_types.PAL")}</Select.Option>
|
||||
<Select.Option value="PAM">{t("joblines.fields.part_types.PAM")}</Select.Option>
|
||||
<Select.Option value="PAN">{t("joblines.fields.part_types.PAN")}</Select.Option>
|
||||
<Select.Option value="PAO">{t("joblines.fields.part_types.PAO")}</Select.Option>
|
||||
<Select.Option value="PAR">{t("joblines.fields.part_types.PAR")}</Select.Option>
|
||||
<Select.Option value="PAS">{t("joblines.fields.part_types.PAS")}</Select.Option>
|
||||
</Select>
|
||||
<Select
|
||||
allowClear
|
||||
options={[
|
||||
{ value: "PAA", label: t("joblines.fields.part_types.PAA") },
|
||||
{ value: "PAC", label: t("joblines.fields.part_types.PAC") },
|
||||
{ value: "PAE", label: t("joblines.fields.part_types.PAE") },
|
||||
{ value: "PAL", label: t("joblines.fields.part_types.PAL") },
|
||||
{ value: "PAM", label: t("joblines.fields.part_types.PAM") },
|
||||
{ value: "PAN", label: t("joblines.fields.part_types.PAN") },
|
||||
{ value: "PAO", label: t("joblines.fields.part_types.PAO") },
|
||||
{ value: "PAR", label: t("joblines.fields.part_types.PAR") },
|
||||
{ value: "PAS", label: t("joblines.fields.part_types.PAS") }
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("joblines.fields.oem_partno")}
|
||||
|
||||
@@ -51,13 +51,7 @@ export default function ShopInfoIntakeChecklistComponent({ form }) {
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select>
|
||||
{Object.keys(ConfigFormTypes).map((i) => (
|
||||
<Select.Option key={i} value={i}>
|
||||
{i}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select options={Object.keys(ConfigFormTypes).map((i) => ({ value: i, label: i }))} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.intake.label")}
|
||||
@@ -156,13 +150,13 @@ export default function ShopInfoIntakeChecklistComponent({ form }) {
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select mode="multiple">
|
||||
{Object.keys(TemplateListGenerated).map((i) => (
|
||||
<Select.Option key={TemplateListGenerated[i].key} value={TemplateListGenerated[i].key}>
|
||||
{TemplateListGenerated[i].title}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select
|
||||
mode="multiple"
|
||||
options={Object.keys(TemplateListGenerated).map((i) => ({
|
||||
value: TemplateListGenerated[i].key,
|
||||
label: TemplateListGenerated[i].title
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["intakechecklist", "next_contact_hours"]}
|
||||
@@ -205,13 +199,7 @@ export default function ShopInfoIntakeChecklistComponent({ form }) {
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select>
|
||||
{Object.keys(ConfigFormTypes).map((i) => (
|
||||
<Select.Option key={i} value={i}>
|
||||
{i}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select options={Object.keys(ConfigFormTypes).map((i) => ({ value: i, label: i }))} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
@@ -310,13 +298,13 @@ export default function ShopInfoIntakeChecklistComponent({ form }) {
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select mode="multiple">
|
||||
{Object.keys(TemplateListGenerated).map((i) => (
|
||||
<Select.Option key={TemplateListGenerated[i].key} value={TemplateListGenerated[i].key}>
|
||||
{TemplateListGenerated[i].title}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select
|
||||
mode="multiple"
|
||||
options={Object.keys(TemplateListGenerated).map((i) => ({
|
||||
value: TemplateListGenerated[i].key,
|
||||
label: TemplateListGenerated[i].title
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["deliverchecklist", "actual_delivery"]}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -80,13 +80,7 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select mode="multiple">
|
||||
{options.map((item, idx) => (
|
||||
<Select.Option key={idx} value={item}>
|
||||
{item}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select mode="multiple" options={options.map((item) => ({ value: item, label: item }))} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["md_ro_statuses", "pre_production_statuses"]}
|
||||
@@ -99,13 +93,7 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select mode="multiple">
|
||||
{options.map((item, idx) => (
|
||||
<Select.Option key={idx} value={item}>
|
||||
{item}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select mode="multiple" options={options.map((item) => ({ value: item, label: item }))} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["md_ro_statuses", "production_statuses"]}
|
||||
@@ -118,13 +106,7 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select mode="multiple">
|
||||
{options.map((item, idx) => (
|
||||
<Select.Option key={idx} value={item}>
|
||||
{item}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select mode="multiple" options={options.map((item) => ({ value: item, label: item }))} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["md_ro_statuses", "post_production_statuses"]}
|
||||
@@ -137,13 +119,7 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select mode="multiple">
|
||||
{options.map((item, idx) => (
|
||||
<Select.Option key={idx} value={item}>
|
||||
{item}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select mode="multiple" options={options.map((item) => ({ value: item, label: item }))} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["md_ro_statuses", "ready_statuses"]}
|
||||
@@ -156,13 +132,7 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select mode="multiple">
|
||||
{options.map((item, idx) => (
|
||||
<Select.Option key={idx} value={item}>
|
||||
{item}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select mode="multiple" options={options.map((item) => ({ value: item, label: item }))} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["md_ro_statuses", "additional_board_statuses"]}
|
||||
@@ -175,13 +145,7 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select mode="multiple">
|
||||
{options.map((item, idx) => (
|
||||
<Select.Option key={idx} value={item}>
|
||||
{item}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select mode="multiple" options={options.map((item) => ({ value: item, label: item }))} />
|
||||
</Form.Item>
|
||||
<LayoutFormRow noDivider>
|
||||
<Form.Item
|
||||
@@ -194,13 +158,7 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
|
||||
]}
|
||||
name={["md_ro_statuses", "default_scheduled"]}
|
||||
>
|
||||
<Select>
|
||||
{options.map((item, idx) => (
|
||||
<Select.Option key={idx} value={item}>
|
||||
{item}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select options={options.map((item) => ({ value: item, label: item }))} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.statuses.default_arrived")}
|
||||
@@ -212,13 +170,7 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
|
||||
]}
|
||||
name={["md_ro_statuses", "default_arrived"]}
|
||||
>
|
||||
<Select>
|
||||
{options.map((item, idx) => (
|
||||
<Select.Option key={idx} value={item}>
|
||||
{item}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select options={options.map((item) => ({ value: item, label: item }))} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.statuses.default_exported")}
|
||||
@@ -230,13 +182,7 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
|
||||
]}
|
||||
name={["md_ro_statuses", "default_exported"]}
|
||||
>
|
||||
<Select>
|
||||
{options.map((item, idx) => (
|
||||
<Select.Option key={idx} value={item}>
|
||||
{item}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select options={options.map((item) => ({ value: item, label: item }))} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.statuses.default_imported")}
|
||||
@@ -248,13 +194,7 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
|
||||
]}
|
||||
name={["md_ro_statuses", "default_imported"]}
|
||||
>
|
||||
<Select>
|
||||
{options.map((item, idx) => (
|
||||
<Select.Option key={idx} value={item}>
|
||||
{item}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select options={options.map((item) => ({ value: item, label: item }))} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.statuses.default_invoiced")}
|
||||
@@ -266,13 +206,7 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
|
||||
]}
|
||||
name={["md_ro_statuses", "default_invoiced"]}
|
||||
>
|
||||
<Select>
|
||||
{options.map((item, idx) => (
|
||||
<Select.Option key={idx} value={item}>
|
||||
{item}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select options={options.map((item) => ({ value: item, label: item }))} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.statuses.default_completed")}
|
||||
@@ -284,13 +218,7 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
|
||||
]}
|
||||
name={["md_ro_statuses", "default_completed"]}
|
||||
>
|
||||
<Select>
|
||||
{options.map((item, idx) => (
|
||||
<Select.Option key={idx} value={item}>
|
||||
{item}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select options={options.map((item) => ({ value: item, label: item }))} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.statuses.default_delivered")}
|
||||
@@ -302,13 +230,7 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
|
||||
]}
|
||||
name={["md_ro_statuses", "default_delivered"]}
|
||||
>
|
||||
<Select>
|
||||
{options.map((item, idx) => (
|
||||
<Select.Option key={idx} value={item}>
|
||||
{item}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select options={options.map((item) => ({ value: item, label: item }))} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.statuses.default_void")}
|
||||
@@ -320,13 +242,7 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
|
||||
]}
|
||||
name={["md_ro_statuses", "default_void"]}
|
||||
>
|
||||
<Select>
|
||||
{options.map((item, idx) => (
|
||||
<Select.Option key={idx} value={item}>
|
||||
{item}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select options={options.map((item) => ({ value: item, label: item }))} />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
{Production_List_Status_Colors.treatment === "on" && (
|
||||
@@ -352,13 +268,7 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select>
|
||||
{productionStatus.map((item, idx) => (
|
||||
<Select.Option key={idx} value={item}>
|
||||
{item}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select options={productionStatus.map((item) => ({ value: item, label: item }))} />
|
||||
</Form.Item>
|
||||
<DeleteFilled
|
||||
onClick={() => {
|
||||
|
||||
@@ -60,13 +60,13 @@ export default function ShopInfoSpeedPrint() {
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select mode="multiple">
|
||||
{Object.keys(TemplateListGenerated).map((key, idx) => (
|
||||
<Select.Option key={idx} value={TemplateListGenerated[key].key}>
|
||||
{TemplateListGenerated[key].title}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select
|
||||
mode="multiple"
|
||||
options={Object.keys(TemplateListGenerated).map((key) => ({
|
||||
value: TemplateListGenerated[key].key,
|
||||
label: TemplateListGenerated[key].title
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Space wrap>
|
||||
|
||||
@@ -43,85 +43,43 @@ export function ShopInfoIntellipay({ bodyshop, form }) {
|
||||
label={t("bodyshop.fields.intellipay_config.payment_map.visa")}
|
||||
name={["intellipay_config", "payment_map", "visa"]}
|
||||
>
|
||||
<Select showSearch>
|
||||
{bodyshop.md_payment_types.map((item, idx) => (
|
||||
<Select.Option key={idx} value={item}>
|
||||
{item}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select showSearch options={bodyshop.md_payment_types.map((item) => ({ value: item, label: item }))} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.intellipay_config.payment_map.mast")}
|
||||
name={["intellipay_config", "payment_map", "mast"]}
|
||||
>
|
||||
<Select showSearch>
|
||||
{bodyshop.md_payment_types.map((item, idx) => (
|
||||
<Select.Option key={idx} value={item}>
|
||||
{item}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select showSearch options={bodyshop.md_payment_types.map((item) => ({ value: item, label: item }))} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.intellipay_config.payment_map.amex")}
|
||||
name={["intellipay_config", "payment_map", "amex"]}
|
||||
>
|
||||
<Select showSearch>
|
||||
{bodyshop.md_payment_types.map((item, idx) => (
|
||||
<Select.Option key={idx} value={item}>
|
||||
{item}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select showSearch options={bodyshop.md_payment_types.map((item) => ({ value: item, label: item }))} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.intellipay_config.payment_map.disc")}
|
||||
name={["intellipay_config", "payment_map", "disc"]}
|
||||
>
|
||||
<Select showSearch>
|
||||
{bodyshop.md_payment_types.map((item, idx) => (
|
||||
<Select.Option key={idx} value={item}>
|
||||
{item}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select showSearch options={bodyshop.md_payment_types.map((item) => ({ value: item, label: item }))} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.intellipay_config.payment_map.dnrs")}
|
||||
name={["intellipay_config", "payment_map", "dnrs"]}
|
||||
>
|
||||
<Select showSearch>
|
||||
{bodyshop.md_payment_types.map((item, idx) => (
|
||||
<Select.Option key={idx} value={item}>
|
||||
{item}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select showSearch options={bodyshop.md_payment_types.map((item) => ({ value: item, label: item }))} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.intellipay_config.payment_map.jcb")}
|
||||
name={["intellipay_config", "payment_map", "jcb"]}
|
||||
>
|
||||
<Select showSearch>
|
||||
{bodyshop.md_payment_types.map((item, idx) => (
|
||||
<Select.Option key={idx} value={item}>
|
||||
{item}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select showSearch options={bodyshop.md_payment_types.map((item) => ({ value: item, label: item }))} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.intellipay_config.payment_map.intr")}
|
||||
name={["intellipay_config", "payment_map", "intr"]}
|
||||
>
|
||||
<Select showSearch>
|
||||
{bodyshop.md_payment_types.map((item, idx) => (
|
||||
<Select.Option key={idx} value={item}>
|
||||
{item}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select showSearch options={bodyshop.md_payment_types.map((item) => ({ value: item, label: item }))} />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
</>
|
||||
|
||||
@@ -57,21 +57,23 @@ export function TechClockInComponent({ form, bodyshop, technician }) {
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select>
|
||||
{emps &&
|
||||
emps.rates.map((item) => (
|
||||
<Select.Option key={item.cost_center} value={item.cost_center}>
|
||||
{item.cost_center === "timetickets.labels.shift"
|
||||
<Select
|
||||
options={
|
||||
emps &&
|
||||
emps.rates.map((item) => ({
|
||||
value: item.cost_center,
|
||||
label:
|
||||
item.cost_center === "timetickets.labels.shift"
|
||||
? t(item.cost_center)
|
||||
: bodyshop.cdk_dealerid ||
|
||||
bodyshop.pbs_serialnumber ||
|
||||
bodyshop.rr_dealerid ||
|
||||
Enhanced_Payroll.treatment === "on"
|
||||
? t(`joblines.fields.lbr_types.${item.cost_center.toUpperCase()}`)
|
||||
: item.cost_center}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
: item.cost_center
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<Divider />
|
||||
|
||||
@@ -201,22 +201,22 @@ export function TechClockOffButton({
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select disabled={isShiftTicket}>
|
||||
{isShiftTicket ? (
|
||||
<Select.Option value="timetickets.labels.shift">{t("timetickets.labels.shift")}</Select.Option>
|
||||
) : (
|
||||
emps &&
|
||||
emps.rates.map((item) => (
|
||||
<Select.Option key={item.cost_center}>
|
||||
{item.cost_center === "timetickets.labels.shift"
|
||||
? t(item.cost_center)
|
||||
: hasDmsKey
|
||||
? t(`joblines.fields.lbr_types.${item.cost_center.toUpperCase()}`)
|
||||
: item.cost_center}
|
||||
</Select.Option>
|
||||
))
|
||||
)}
|
||||
</Select>
|
||||
<Select disabled={isShiftTicket}
|
||||
options={
|
||||
isShiftTicket
|
||||
? [{ value: "timetickets.labels.shift", label: t("timetickets.labels.shift") }]
|
||||
: emps &&
|
||||
emps.rates.map((item) => ({
|
||||
value: item.cost_center,
|
||||
label:
|
||||
item.cost_center === "timetickets.labels.shift"
|
||||
? t(item.cost_center)
|
||||
: hasDmsKey
|
||||
? t(`joblines.fields.lbr_types.${item.cost_center.toUpperCase()}`)
|
||||
: item.cost_center
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{isShiftTicket ? (
|
||||
@@ -232,11 +232,12 @@ export function TechClockOffButton({
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select>
|
||||
{bodyshop.md_ro_statuses.production_statuses.map((item) => (
|
||||
<Select.Option key={item}></Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select
|
||||
options={bodyshop.md_ro_statuses.production_statuses.map((item) => ({
|
||||
value: item,
|
||||
label: item
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
|
||||
@@ -91,21 +91,22 @@ export function TimeTicketModalComponent({
|
||||
value={value === "timetickets.labels.shift" ? t(value) : value}
|
||||
{...props}
|
||||
disabled={value === "timetickets.labels.shift" || disabled}
|
||||
>
|
||||
{emps &&
|
||||
emps.rates.map((item) => (
|
||||
<Select.Option key={item.cost_center} value={item.cost_center}>
|
||||
{item.cost_center === "timetickets.labels.shift"
|
||||
options={
|
||||
emps &&
|
||||
emps.rates.map((item) => ({
|
||||
value: item.cost_center,
|
||||
label:
|
||||
item.cost_center === "timetickets.labels.shift"
|
||||
? t(item.cost_center)
|
||||
: bodyshop.cdk_dealerid ||
|
||||
bodyshop.pbs_serialnumber ||
|
||||
bodyshop.rr_dealerid ||
|
||||
Enhanced_Payroll.treatment === "on"
|
||||
? t(`joblines.fields.lbr_types.${item.cost_center.toUpperCase()}`)
|
||||
: item.cost_center}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
: item.cost_center
|
||||
}))
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
const MemoInput = ({ value, ...props }) => (
|
||||
|
||||
@@ -20,13 +20,15 @@ export function TimeTicketShiftFormComponent() {
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select>
|
||||
<Select.Option value="timetickets.labels.amshift">{t("timetickets.labels.amshift")}</Select.Option>
|
||||
<Select.Option value="timetickets.labels.ambreak">{t("timetickets.labels.ambreak")}</Select.Option>
|
||||
<Select.Option value="timetickets.labels.lunch">{t("timetickets.labels.lunch")}</Select.Option>
|
||||
<Select.Option value="timetickets.labels.pmshift">{t("timetickets.labels.pmshift")}</Select.Option>
|
||||
<Select.Option value="timetickets.labels.pmbreak">{t("timetickets.labels.pmbreak")}</Select.Option>
|
||||
</Select>
|
||||
<Select
|
||||
options={[
|
||||
{ value: "timetickets.labels.amshift", label: t("timetickets.labels.amshift") },
|
||||
{ value: "timetickets.labels.ambreak", label: t("timetickets.labels.ambreak") },
|
||||
{ value: "timetickets.labels.lunch", label: t("timetickets.labels.lunch") },
|
||||
{ value: "timetickets.labels.pmshift", label: t("timetickets.labels.pmshift") },
|
||||
{ value: "timetickets.labels.pmbreak", label: t("timetickets.labels.pmbreak") }
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -9,8 +9,6 @@ import {
|
||||
} from "../../graphql/vehicles.queries";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
const VehicleSearchSelect = ({ value, onChange, onBlur, disabled, ref }) => {
|
||||
const [callSearch, { loading, error, data }] = useLazyQuery(SEARCH_VEHICLES_FOR_AUTOCOMPLETE);
|
||||
|
||||
@@ -18,9 +16,10 @@ const VehicleSearchSelect = ({ value, onChange, onBlur, disabled, ref }) => {
|
||||
SEARCH_VEHICLES_BY_ID_FOR_AUTOCOMPLETE
|
||||
);
|
||||
|
||||
const executeSearch = (v) => {
|
||||
if (v && v.variables?.search !== "" && v.variables.search.length >= 2) callSearch({ variables: v.variables });
|
||||
const executeSearch = (variables) => {
|
||||
if (variables?.search !== "" && variables?.search?.length >= 2) callSearch({ variables });
|
||||
};
|
||||
|
||||
const debouncedExecuteSearch = _.debounce(executeSearch, 500);
|
||||
|
||||
const handleSearch = (value) => {
|
||||
@@ -72,15 +71,12 @@ const VehicleSearchSelect = ({ value, onChange, onBlur, disabled, ref }) => {
|
||||
onSelect={handleSelect}
|
||||
notFoundContent={loading ? <LoadingOutlined /> : <Empty />}
|
||||
onBlur={onBlur}
|
||||
>
|
||||
{theOptions
|
||||
? theOptions.map((o) => (
|
||||
<Option key={o.id} value={o.id}>
|
||||
{`${o.v_vin || ""} ${o.v_model_yr || ""} ${o.v_make_desc || ""} ${o.v_model_desc || ""} `}
|
||||
</Option>
|
||||
))
|
||||
: null}
|
||||
</Select>
|
||||
options={theOptions?.map((o) => ({
|
||||
key: o.id,
|
||||
value: o.id,
|
||||
label: `${o.v_vin || ""} ${o.v_model_yr || ""} ${o.v_make_desc || ""} ${o.v_model_desc || ""} `
|
||||
}))}
|
||||
/>
|
||||
{idLoading || loading ? <LoadingOutlined /> : null}
|
||||
{error ? <AlertComponent title={error.message} type="error" /> : null}
|
||||
{idError ? <AlertComponent title={idError.message} type="error" /> : null}
|
||||
|
||||
@@ -3,8 +3,6 @@ import { Select, Space, Tag } from "antd";
|
||||
import { useEffect, useState } from "react";
|
||||
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
// To be used as a form element only.
|
||||
|
||||
const VendorSearchSelect = ({ value, onChange, options, onSelect, disabled, preferredMake, showPhone, ref }) => {
|
||||
@@ -21,10 +19,57 @@ const VendorSearchSelect = ({ value, onChange, options, onSelect, disabled, pref
|
||||
? options.filter((o) => o.favorite.filter((f) => f.toLowerCase() === preferredMake.toLowerCase()).length > 0)
|
||||
: [];
|
||||
|
||||
const formatOption = (o, isFavorite = false) => ({
|
||||
key: isFavorite ? `favorite-${o.id}` : o.id,
|
||||
value: o.id,
|
||||
name: o.name,
|
||||
discount: o.discount,
|
||||
label: (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
flexWrap: "nowrap",
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap"
|
||||
}}
|
||||
>
|
||||
{o.name}
|
||||
</div>
|
||||
<Space style={{ marginLeft: "1rem" }}>
|
||||
{isFavorite && <HeartOutlined style={{ color: "red" }} />}
|
||||
{!isFavorite &&
|
||||
o.tags?.map((tag, idx) => (
|
||||
<Tag key={idx} style={{ marginLeft: "0.5rem" }}>
|
||||
{tag}
|
||||
</Tag>
|
||||
))}
|
||||
{o.phone && showPhone && <PhoneNumberFormatter>{o.phone}</PhoneNumberFormatter>}
|
||||
{o.discount && o.discount !== 0 ? <Tag color="green">{`${o.discount * 100}%`}</Tag> : null}
|
||||
</Space>
|
||||
</div>
|
||||
)
|
||||
});
|
||||
|
||||
const allOptions = [
|
||||
...(favorites?.map((o) => formatOption(o, true)) || []),
|
||||
...(options?.map((o) => formatOption(o, false)) || [])
|
||||
];
|
||||
|
||||
return (
|
||||
<Select
|
||||
ref={ref}
|
||||
showSearch
|
||||
showSearch={{
|
||||
optionFilterProp: "name"
|
||||
}}
|
||||
value={option}
|
||||
style={{
|
||||
width: "100%"
|
||||
@@ -59,76 +104,11 @@ const VendorSearchSelect = ({ value, onChange, options, onSelect, disabled, pref
|
||||
}}
|
||||
popupMatchSelectWidth={false}
|
||||
onChange={setOption}
|
||||
optionFilterProp="name"
|
||||
onSelect={onSelect}
|
||||
disabled={disabled || false}
|
||||
optionLabelProp="name"
|
||||
>
|
||||
{favorites &&
|
||||
favorites.map((o) => (
|
||||
<Option key={`favorite-${o.id}`} value={o.id} name={o.name} discount={o.discount}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
flexWrap: "nowrap",
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap"
|
||||
}}
|
||||
>
|
||||
{o.name}
|
||||
</div>
|
||||
<Space style={{ marginLeft: "1rem" }}>
|
||||
<HeartOutlined style={{ color: "red" }} />
|
||||
{o.phone && showPhone && <PhoneNumberFormatter>{o.phone}</PhoneNumberFormatter>}
|
||||
{o.discount && o.discount !== 0 ? <Tag color="green">{`${o.discount * 100}%`}</Tag> : null}
|
||||
</Space>
|
||||
</div>
|
||||
</Option>
|
||||
))}
|
||||
{options &&
|
||||
options.map((o) => (
|
||||
<Option key={o.id} value={o.id} name={o.name} discount={o.discount}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
flexWrap: "nowrap",
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap"
|
||||
}}
|
||||
>
|
||||
{o.name}
|
||||
</div>
|
||||
<Space style={{ marginLeft: "1rem" }}>
|
||||
{o.tags?.map((tag, idx) => (
|
||||
<Tag key={idx} style={{ marginLeft: "0.5rem" }}>
|
||||
{tag}
|
||||
</Tag>
|
||||
))}
|
||||
{o.phone && showPhone && <PhoneNumberFormatter>{o.phone}</PhoneNumberFormatter>}
|
||||
{o.discount && o.discount !== 0 ? <Tag color="green">{`${o.discount * 100}%`}</Tag> : null}
|
||||
</Space>
|
||||
</div>
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
options={allOptions}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export default VendorSearchSelect;
|
||||
|
||||
@@ -470,6 +470,9 @@ export const GET_JOB_BY_PK = gql`
|
||||
clm_total
|
||||
comment
|
||||
converted
|
||||
dms_id
|
||||
dms_customer_id
|
||||
dms_advisor_id
|
||||
csiinvites {
|
||||
completedon
|
||||
id
|
||||
@@ -491,6 +494,9 @@ export const GET_JOB_BY_PK = gql`
|
||||
ded_status
|
||||
deliverchecklist
|
||||
depreciation_taxes
|
||||
dms_id
|
||||
dms_advisor_id
|
||||
dms_customer_id
|
||||
driveable
|
||||
employee_body
|
||||
employee_body_rel {
|
||||
@@ -1995,6 +2001,9 @@ export const QUERY_JOB_CLOSE_DETAILS = gql`
|
||||
qb_multiple_payers
|
||||
lbr_adjustments
|
||||
ownr_ea
|
||||
dms_id
|
||||
dms_customer_id
|
||||
dms_advisor_id
|
||||
payments {
|
||||
amount
|
||||
created_at
|
||||
@@ -2216,6 +2225,9 @@ export const QUERY_JOB_EXPORT_DMS = gql`
|
||||
plate_no
|
||||
plate_st
|
||||
ownr_co_nm
|
||||
dms_id
|
||||
dms_customer_id
|
||||
dms_advisor_id
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -119,12 +119,13 @@ export function DmsContainer({ setBreadcrumbs, setSelectedHeader }) {
|
||||
setLogLevel(value);
|
||||
socket.emit("set-log-level", value);
|
||||
}}
|
||||
>
|
||||
<Select.Option key="DEBUG">DEBUG</Select.Option>
|
||||
<Select.Option key="INFO">INFO</Select.Option>
|
||||
<Select.Option key="WARN">WARN</Select.Option>
|
||||
<Select.Option key="ERROR">ERROR</Select.Option>
|
||||
</Select>
|
||||
options={[
|
||||
{ key: "DEBUG", value: "DEBUG", label: "DEBUG" },
|
||||
{ key: "INFO", value: "INFO", label: "INFO" },
|
||||
{ key: "WARN", value: "WARN", label: "WARN" },
|
||||
{ key: "ERROR", value: "ERROR", label: "ERROR" }
|
||||
]}
|
||||
/>
|
||||
<Button onClick={() => setLogs([])}>Clear Logs</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
|
||||
@@ -426,6 +426,24 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
||||
|
||||
if (data.jobs_by_pk?.date_exported) return <Result status="warning" title={t("dms.errors.alreadyexported")} />;
|
||||
|
||||
// Check if Reynolds mode requires early RO
|
||||
const hasEarlyRO = !!(data.jobs_by_pk?.dms_id && data.jobs_by_pk?.dms_customer_id && data.jobs_by_pk?.dms_advisor_id);
|
||||
|
||||
if (isRrMode && !hasEarlyRO) {
|
||||
return (
|
||||
<Result
|
||||
status="warning"
|
||||
title={t("dms.errors.earlyrorequired")}
|
||||
subTitle={t("dms.errors.earlyrorequired.message")}
|
||||
extra={
|
||||
<Link to={`/manage/jobs/${jobId}/admin`}>
|
||||
<Button type="primary">{t("general.actions.gotoadmin")}</Button>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AlertComponent style={{ marginBottom: 10 }} title={bannerMessage} type="warning" showIcon closable />
|
||||
@@ -486,6 +504,7 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
||||
|
||||
<DmsCustomerSelector
|
||||
jobid={jobId}
|
||||
job={data?.jobs_by_pk}
|
||||
bodyshop={bodyshop}
|
||||
socket={activeSocket}
|
||||
mode={mode}
|
||||
@@ -522,13 +541,14 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
||||
setLogLevel(value);
|
||||
setActiveLogLevel(value);
|
||||
}}
|
||||
>
|
||||
<Select.Option key="SILLY">SILLY</Select.Option>
|
||||
<Select.Option key="DEBUG">DEBUG</Select.Option>
|
||||
<Select.Option key="INFO">INFO</Select.Option>
|
||||
<Select.Option key="WARN">WARN</Select.Option>
|
||||
<Select.Option key="ERROR">ERROR</Select.Option>
|
||||
</Select>
|
||||
options={[
|
||||
{ key: "SILLY", value: "SILLY", label: "SILLY" },
|
||||
{ key: "DEBUG", value: "DEBUG", label: "DEBUG" },
|
||||
{ key: "INFO", value: "INFO", label: "INFO" },
|
||||
{ key: "WARN", value: "WARN", label: "WARN" },
|
||||
{ key: "ERROR", value: "ERROR", label: "ERROR" }
|
||||
]}
|
||||
/>
|
||||
<Button onClick={() => setLogs([])}>Clear Logs</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { useQuery } from "@apollo/client/react";
|
||||
import { Card, Col, Result, Row, Space, Typography } from "antd";
|
||||
import { useEffect } from "react";
|
||||
import { useMutation, useQuery } from "@apollo/client/react";
|
||||
import { Button, Card, Col, Form, Input, Modal, Result, Row, Select, Space, Switch, Typography } from "antd";
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { some } from "lodash";
|
||||
import axios from "axios";
|
||||
import AlertComponent from "../../components/alert/alert.component";
|
||||
import JobCalculateTotals from "../../components/job-calculate-totals/job-calculate-totals.component";
|
||||
import ScoreboardAddButton from "../../components/job-scoreboard-add-button/job-scoreboard-add-button.component";
|
||||
@@ -19,13 +22,26 @@ import JobsAdminRemoveAR from "../../components/jobs-admin-remove-ar/jobs-admin-
|
||||
import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component";
|
||||
import NotFound from "../../components/not-found/not-found.component";
|
||||
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
|
||||
import { GET_JOB_BY_PK } from "../../graphql/jobs.queries";
|
||||
import RREarlyROModal from "../../components/dms-post-form/rr-early-ro-modal";
|
||||
import { GET_JOB_BY_PK, CONVERT_JOB_TO_RO } from "../../graphql/jobs.queries";
|
||||
import { setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext";
|
||||
import { DMS_MAP, getDmsMode } from "../../utils/dmsUtils";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
|
||||
setSelectedHeader: (key) => dispatch(setSelectedHeader(key))
|
||||
setSelectedHeader: (key) => dispatch(setSelectedHeader(key)),
|
||||
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
|
||||
});
|
||||
|
||||
const colSpan = {
|
||||
@@ -39,14 +55,36 @@ const cardStyle = {
|
||||
height: "100%"
|
||||
};
|
||||
|
||||
export function JobsCloseContainer({ setBreadcrumbs, setSelectedHeader }) {
|
||||
export function JobsCloseContainer({ setBreadcrumbs, setSelectedHeader, bodyshop, insertAuditTrail }) {
|
||||
const { jobId } = useParams();
|
||||
const { loading, error, data } = useQuery(GET_JOB_BY_PK, {
|
||||
const { loading, error, data, refetch } = useQuery(GET_JOB_BY_PK, {
|
||||
variables: { id: jobId },
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only"
|
||||
});
|
||||
const { t } = useTranslation();
|
||||
const { socket } = useSocket(); // Extract socket from context
|
||||
const notification = useNotification();
|
||||
const [showEarlyROModal, setShowEarlyROModal] = useState(false);
|
||||
const [showConvertModal, setShowConvertModal] = useState(false);
|
||||
const [convertLoading, setConvertLoading] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const [mutationConvertJob] = useMutation(CONVERT_JOB_TO_RO);
|
||||
const allFormValues = Form.useWatch([], form);
|
||||
|
||||
// Get Fortellis treatment for proper DMS mode detection
|
||||
const {
|
||||
treatments: { Fortellis }
|
||||
} = useTreatmentsWithConfig({
|
||||
attributes: {},
|
||||
names: ["Fortellis"],
|
||||
splitKey: bodyshop?.imexshopid
|
||||
});
|
||||
|
||||
// Check if bodyshop has Reynolds integration using the proper getDmsMode function
|
||||
const dmsMode = getDmsMode(bodyshop, Fortellis.treatment);
|
||||
const isReynoldsMode = dmsMode === DMS_MAP.reynolds;
|
||||
const job = data?.jobs_by_pk;
|
||||
useEffect(() => {
|
||||
setSelectedHeader("activejobs");
|
||||
document.title = t("titles.jobs-admin", {
|
||||
@@ -75,6 +113,55 @@ export function JobsCloseContainer({ setBreadcrumbs, setSelectedHeader }) {
|
||||
]);
|
||||
}, [setBreadcrumbs, t, jobId, data, setSelectedHeader]);
|
||||
|
||||
const handleEarlyROSuccess = (result) => {
|
||||
notification.success({
|
||||
title: t("jobs.successes.early_ro_created"),
|
||||
description: `RO Number: ${result.roNumber || "N/A"}`
|
||||
});
|
||||
setShowEarlyROModal(false);
|
||||
refetch?.();
|
||||
};
|
||||
|
||||
const handleConvert = async ({ employee_csr, category, ...values }) => {
|
||||
if (!job?.id) return;
|
||||
setConvertLoading(true);
|
||||
const res = await mutationConvertJob({
|
||||
variables: {
|
||||
jobId: job.id,
|
||||
job: {
|
||||
converted: true,
|
||||
...(bodyshop?.enforce_conversion_csr ? { employee_csr } : {}),
|
||||
...(bodyshop?.enforce_conversion_category ? { category } : {}),
|
||||
...values
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (values.ca_gst_registrant) {
|
||||
await axios.post("/job/totalsssu", {
|
||||
id: job.id
|
||||
});
|
||||
}
|
||||
|
||||
if (!res.errors) {
|
||||
refetch();
|
||||
notification.success({
|
||||
title: t("jobs.successes.converted")
|
||||
});
|
||||
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.jobconverted(res.data.update_jobs.returning[0].ro_number),
|
||||
type: "jobconverted"
|
||||
});
|
||||
|
||||
setShowConvertModal(false);
|
||||
}
|
||||
setConvertLoading(false);
|
||||
};
|
||||
|
||||
const submitDisabled = useCallback(() => some(allFormValues, (v) => v === undefined), [allFormValues]);
|
||||
|
||||
if (loading) return <LoadingSpinner />;
|
||||
if (error) return <AlertComponent title={error.message} type="error" />;
|
||||
if (!data.jobs_by_pk) return <NotFound />;
|
||||
@@ -99,6 +186,16 @@ export function JobsCloseContainer({ setBreadcrumbs, setSelectedHeader }) {
|
||||
<JobsAdminUnvoid job={data ? data.jobs_by_pk : {}} />
|
||||
<JobsAdminStatus job={data ? data.jobs_by_pk : {}} />
|
||||
<JobsAdminRemoveAR job={data ? data.jobs_by_pk : {}} />
|
||||
{isReynoldsMode && job?.converted && !job?.dms_id && !job?.dms_customer_id && !job?.dms_advisor_id && (
|
||||
<Button className="ant-btn ant-btn-default" onClick={() => setShowEarlyROModal(true)}>
|
||||
{t("jobs.actions.dms.createearlyro", "Create RR RO")}
|
||||
</Button>
|
||||
)}
|
||||
{isReynoldsMode && !job?.converted && !job?.dms_id && (
|
||||
<Button type="primary" danger onClick={() => setShowConvertModal(true)}>
|
||||
{t("jobs.actions.convertwithoutearlyro", "Convert without Early RO")}
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
@@ -124,8 +221,173 @@ export function JobsCloseContainer({ setBreadcrumbs, setSelectedHeader }) {
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* Early RO Modal */}
|
||||
<RREarlyROModal
|
||||
open={showEarlyROModal}
|
||||
onClose={() => setShowEarlyROModal(false)}
|
||||
onSuccess={handleEarlyROSuccess}
|
||||
bodyshop={bodyshop}
|
||||
socket={socket}
|
||||
job={job}
|
||||
/>
|
||||
|
||||
{/* Convert without Early RO Modal */}
|
||||
<Modal
|
||||
open={showConvertModal}
|
||||
onCancel={() => setShowConvertModal(false)}
|
||||
title={t("jobs.actions.convertwithoutearlyro", "Convert without Early RO")}
|
||||
footer={null}
|
||||
width={700}
|
||||
destroyOnHidden
|
||||
>
|
||||
<Form
|
||||
layout="vertical"
|
||||
form={form}
|
||||
onFinish={handleConvert}
|
||||
initialValues={{
|
||||
driveable: true,
|
||||
towin: job?.towin,
|
||||
ca_gst_registrant: job?.ca_gst_registrant,
|
||||
employee_csr: job?.employee_csr,
|
||||
category: job?.category,
|
||||
referral_source: job?.referral_source,
|
||||
referral_source_extra: job?.referral_source_extra ?? ""
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
name={["ins_co_nm"]}
|
||||
label={t("jobs.fields.ins_co_nm")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select showSearch>
|
||||
{bodyshop?.md_ins_cos?.map((s, i) => (
|
||||
<Select.Option key={i} value={s.name}>
|
||||
{s.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
{bodyshop?.enforce_class && (
|
||||
<Form.Item
|
||||
name={"class"}
|
||||
label={t("jobs.fields.class")}
|
||||
rules={[
|
||||
{
|
||||
required: bodyshop.enforce_class
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select>
|
||||
{bodyshop?.md_classes?.map((s) => (
|
||||
<Select.Option key={s} value={s}>
|
||||
{s}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
)}
|
||||
{bodyshop?.enforce_referral && (
|
||||
<>
|
||||
<Form.Item
|
||||
name={"referral_source"}
|
||||
label={t("jobs.fields.referralsource")}
|
||||
rules={[
|
||||
{
|
||||
required: bodyshop.enforce_referral
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select>
|
||||
{bodyshop?.md_referral_sources?.map((s) => (
|
||||
<Select.Option key={s} value={s}>
|
||||
{s}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label={t("jobs.fields.referral_source_extra")} name="referral_source_extra">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
{bodyshop?.enforce_conversion_csr && (
|
||||
<Form.Item
|
||||
name={"employee_csr"}
|
||||
label={t(
|
||||
InstanceRenderManager({
|
||||
imex: "jobs.fields.employee_csr",
|
||||
rome: "jobs.fields.employee_csr_writer"
|
||||
})
|
||||
)}
|
||||
rules={[
|
||||
{
|
||||
required: bodyshop.enforce_conversion_csr
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
showSearch={{
|
||||
optionFilterProp: "children",
|
||||
filterOption: (input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}}
|
||||
style={{ width: 200 }}
|
||||
>
|
||||
{bodyshop?.employees
|
||||
?.filter((emp) => emp.active)
|
||||
?.map((emp) => (
|
||||
<Select.Option value={emp.id} key={emp.id} name={`${emp.first_name} ${emp.last_name}`}>
|
||||
{`${emp.first_name} ${emp.last_name}`}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
)}
|
||||
{bodyshop?.enforce_conversion_category && (
|
||||
<Form.Item
|
||||
name={"category"}
|
||||
label={t("jobs.fields.category")}
|
||||
rules={[
|
||||
{
|
||||
required: bodyshop.enforce_conversion_category
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select allowClear>
|
||||
{bodyshop?.md_categories?.map((s) => (
|
||||
<Select.Option key={s} value={s}>
|
||||
{s}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
)}
|
||||
{bodyshop?.region_config?.toLowerCase().startsWith("ca") && (
|
||||
<Form.Item label={t("jobs.fields.ca_gst_registrant")} name="ca_gst_registrant" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item label={t("jobs.fields.driveable")} name="driveable" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("jobs.fields.towin")} name="towin" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
|
||||
<Space wrap style={{ marginTop: 16 }}>
|
||||
<Button disabled={submitDisabled()} type="primary" danger onClick={() => form.submit()} loading={convertLoading}>
|
||||
{t("jobs.actions.convert")}
|
||||
</Button>
|
||||
<Button onClick={() => setShowConvertModal(false)}>{t("general.actions.close")}</Button>
|
||||
</Space>
|
||||
</Form>
|
||||
</Modal>
|
||||
</RbacWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(null, mapDispatchToProps)(JobsCloseContainer);
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobsCloseContainer);
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Modal,
|
||||
Popconfirm,
|
||||
Row,
|
||||
Select,
|
||||
@@ -42,7 +43,7 @@ import { setModalContext } from "../../redux/modals/modals.actions.js";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import dayjs from "../../utils/day";
|
||||
import { bodyshopHasDmsKey } from "../../utils/dmsUtils.js";
|
||||
import { bodyshopHasDmsKey, DMS_MAP, getDmsMode } from "../../utils/dmsUtils.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -71,6 +72,11 @@ export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail, set
|
||||
const notification = useNotification();
|
||||
|
||||
const hasDMSKey = bodyshopHasDmsKey(bodyshop);
|
||||
const dmsMode = getDmsMode(bodyshop, "off");
|
||||
const isReynoldsMode = dmsMode === DMS_MAP.reynolds;
|
||||
const hasEarlyRO = !!(job?.dms_id && job?.dms_customer_id && job?.dms_advisor_id);
|
||||
const canSendToDMS = !isReynoldsMode || hasEarlyRO;
|
||||
const [showEarlyROModal, setShowEarlyROModal] = useState(false);
|
||||
|
||||
const {
|
||||
treatments: { Qb_Multi_Ar, ClosingPeriod }
|
||||
@@ -82,18 +88,18 @@ export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail, set
|
||||
|
||||
const handleFinish = async ({ removefromproduction, ...values }) => {
|
||||
setLoading(true);
|
||||
|
||||
|
||||
// Validate that all joblines have valid IDs
|
||||
const joblinesWithIds = values.joblines.filter(jl => jl && jl.id);
|
||||
const joblinesWithIds = values.joblines.filter((jl) => jl && jl.id);
|
||||
if (joblinesWithIds.length !== values.joblines.length) {
|
||||
notification.error({
|
||||
title: t("jobs.errors.invalidjoblines"),
|
||||
message: t("jobs.errors.missingjoblineids")
|
||||
description: t("jobs.errors.missingjoblineids")
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const result = await client.mutate({
|
||||
mutation: generateJobLinesUpdatesForInvoicing(values.joblines)
|
||||
});
|
||||
@@ -208,9 +214,17 @@ export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail, set
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
{bodyshopHasDmsKey(bodyshop) && (
|
||||
<Link to={`/manage/dms?jobId=${job.id}`}>
|
||||
<Button disabled={job.date_exported || !jobRO}>{t("jobs.actions.sendtodms")}</Button>
|
||||
</Link>
|
||||
<>
|
||||
{canSendToDMS ? (
|
||||
<Link to={`/manage/dms?jobId=${job.id}`}>
|
||||
<Button disabled={job.date_exported || !jobRO}>{t("jobs.actions.sendtodms")}</Button>
|
||||
</Link>
|
||||
) : (
|
||||
<Button disabled={job.date_exported || !jobRO} onClick={() => setShowEarlyROModal(true)}>
|
||||
{t("jobs.actions.sendtodms")}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => {
|
||||
@@ -426,13 +440,15 @@ export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail, set
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select style={{ minWidth: "12rem" }} disabled={jobRO}>
|
||||
{bodyshop.md_ins_cos.map((s) => (
|
||||
<Select.Option key={s.name} value={s.name}>
|
||||
{s.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select
|
||||
style={{ minWidth: "12rem" }}
|
||||
disabled={jobRO}
|
||||
options={bodyshop.md_ins_cos.map((s) => ({
|
||||
key: s.name,
|
||||
value: s.name,
|
||||
label: s.name
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
@@ -510,7 +526,7 @@ export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail, set
|
||||
<Statistic
|
||||
title={t("jobs.labels.pimraryamountpayable")}
|
||||
styles={{
|
||||
value: {
|
||||
content: {
|
||||
color: discrep.getAmount() >= 0 ? "green" : "red"
|
||||
}
|
||||
}}
|
||||
@@ -527,6 +543,30 @@ export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail, set
|
||||
<Divider />
|
||||
<JobsCloseLines job={job} />
|
||||
</Form>
|
||||
|
||||
{/* Early RO Required Modal */}
|
||||
<Modal
|
||||
open={showEarlyROModal}
|
||||
onCancel={() => setShowEarlyROModal(false)}
|
||||
footer={null}
|
||||
title={
|
||||
<Space>
|
||||
<Typography.Text type="warning" style={{ fontSize: "1.2em" }}>
|
||||
⚠️
|
||||
</Typography.Text>
|
||||
<span>{t("dms.errors.earlyrorequired")}</span>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Space orientation="vertical" size="large" style={{ width: "100%" }}>
|
||||
<Typography.Paragraph>{t("dms.errors.earlyrorequired.message")}</Typography.Paragraph>
|
||||
<Link to={`/manage/jobs/${job.id}/admin`}>
|
||||
<Button type="primary" block onClick={() => setShowEarlyROModal(false)}>
|
||||
{t("general.actions.gotoadmin")}
|
||||
</Button>
|
||||
</Link>
|
||||
</Space>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,10 @@ import "./tech.page.styles.scss";
|
||||
import UpsellComponent, { upsellEnum } from "../../components/upsell/upsell.component.jsx";
|
||||
import { lazyDev } from "../../utils/lazyWithPreload.jsx";
|
||||
|
||||
const TimeTicketModalContainer = lazyDev(() => import("../../components/time-ticket-modal/time-ticket-modal.container"));
|
||||
const NoteUpsertModal = lazyDev(() => import("../../components/note-upsert-modal/note-upsert-modal.container.jsx"));
|
||||
const TimeTicketModalContainer = lazyDev(
|
||||
() => import("../../components/time-ticket-modal/time-ticket-modal.container")
|
||||
);
|
||||
const EmailOverlayContainer = lazyDev(() => import("../../components/email-overlay/email-overlay.container.jsx"));
|
||||
const PrintCenterModalContainer = lazyDev(
|
||||
() => import("../../components/print-center-modal/print-center-modal.container")
|
||||
@@ -34,7 +37,9 @@ const TimeTicketModalTask = lazyDev(
|
||||
const TechAssignedProdJobs = lazyDev(() => import("../tech-assigned-prod-jobs/tech-assigned-prod-jobs.component"));
|
||||
const TechDispatchedParts = lazyDev(() => import("../tech-dispatched-parts/tech-dispatched-parts.page"));
|
||||
|
||||
const TaskUpsertModalContainer = lazyDev(() => import("../../components/task-upsert-modal/task-upsert-modal.container"));
|
||||
const TaskUpsertModalContainer = lazyDev(
|
||||
() => import("../../components/task-upsert-modal/task-upsert-modal.container")
|
||||
);
|
||||
|
||||
const { Content } = Layout;
|
||||
|
||||
@@ -70,6 +75,8 @@ export function TechPage({ technician }) {
|
||||
<TechHeader />
|
||||
|
||||
<TaskUpsertModalContainer />
|
||||
<NoteUpsertModal />
|
||||
|
||||
<Content className="tech-content-container">
|
||||
<ErrorBoundary>
|
||||
<Suspense
|
||||
|
||||
@@ -1047,7 +1047,9 @@
|
||||
},
|
||||
"dms": {
|
||||
"errors": {
|
||||
"alreadyexported": "This job has already been sent to the DMS. If you need to resend it, please use admin permissions to mark the job for re-export."
|
||||
"alreadyexported": "This job has already been sent to the DMS. If you need to resend it, please use admin permissions to mark the job for re-export.",
|
||||
"earlyrorequired": "Early RO Required",
|
||||
"earlyrorequired.message": "This job requires an early Repair Order to be created before posting to Reynolds. Please use the admin panel to create the early RO first."
|
||||
},
|
||||
"labels": {
|
||||
"refreshallocations": "Refresh to see DMS Allocations."
|
||||
@@ -1244,6 +1246,7 @@
|
||||
"deselectall": "Deselect All",
|
||||
"download": "Download",
|
||||
"edit": "Edit",
|
||||
"gotoadmin": "Go to Admin Panel",
|
||||
"login": "Login",
|
||||
"next": "Next",
|
||||
"ok": "Ok",
|
||||
@@ -1288,6 +1291,7 @@
|
||||
"vehicle": "Vehicle"
|
||||
},
|
||||
"labels": {
|
||||
"apply": "Apply",
|
||||
"actions": "Actions",
|
||||
"areyousure": "Are you sure?",
|
||||
"barcode": "Barcode",
|
||||
@@ -1621,11 +1625,13 @@
|
||||
"changestatus": "Change Status",
|
||||
"changestimator": "Change Estimator",
|
||||
"convert": "Convert",
|
||||
"convertwithoutearlyro": "Convert without Early RO",
|
||||
"createiou": "Create IOU",
|
||||
"deliver": "Deliver",
|
||||
"deliver_quick": "Quick Deliver",
|
||||
"dms": {
|
||||
"addpayer": "Add Payer",
|
||||
"createearlyro": "Create RR RO",
|
||||
"createnewcustomer": "Create New Customer",
|
||||
"findmakemodelcode": "Find Make/Model Code",
|
||||
"getmakes": "Get Makes",
|
||||
@@ -1634,6 +1640,7 @@
|
||||
},
|
||||
"post": "Post",
|
||||
"refetchmakesmodels": "Refetch Make and Model Codes",
|
||||
"update_ro": "Update RO",
|
||||
"usegeneric": "Use Generic Customer",
|
||||
"useselected": "Use Selected Customer"
|
||||
},
|
||||
@@ -1793,6 +1800,7 @@
|
||||
},
|
||||
"cost": "Cost",
|
||||
"cost_dms_acctnumber": "Cost DMS Acct #",
|
||||
"customer": "Customer #",
|
||||
"dms_make": "DMS Make",
|
||||
"dms_model": "DMS Model",
|
||||
"dms_model_override": "Override DMS Make/Model",
|
||||
@@ -1817,7 +1825,11 @@
|
||||
"sale": "Sale",
|
||||
"sale_dms_acctnumber": "Sale DMS Acct #",
|
||||
"story": "Story",
|
||||
"vinowner": "VIN Owner"
|
||||
"vinowner": "VIN Owner",
|
||||
"rr_opcode": "RR OpCode",
|
||||
"rr_opcode_prefix": "Prefix",
|
||||
"rr_opcode_suffix": "Suffix",
|
||||
"rr_opcode_base": "Base"
|
||||
},
|
||||
"dms_allocation": "DMS Allocation",
|
||||
"driveable": "Driveable",
|
||||
@@ -2102,6 +2114,11 @@
|
||||
"damageto": "Damage to $t(jobs.fields.area_of_damage_impact.{{area_of_damage}}).",
|
||||
"defaultstory": "B/S RO: {{ro_number}}. Owner: {{ownr_nm}}. Insurance Co: {{ins_co_nm}}. Claim/PO #: {{clm_po}}",
|
||||
"disablebillwip": "Cost and WIP for bills has been ignored per shop configuration.",
|
||||
"earlyro": {
|
||||
"created": "Early RO Created:",
|
||||
"fields": "Required fields:",
|
||||
"willupdate": "This will update the existing RO with full job data."
|
||||
},
|
||||
"invoicedatefuture": "Invoice date must be today or in the future for CDK posting.",
|
||||
"kmoutnotgreaterthankmin": "Mileage out must be greater than mileage in.",
|
||||
"logs": "Logs",
|
||||
@@ -2259,6 +2276,7 @@
|
||||
"delete": "Job deleted successfully.",
|
||||
"deleted": "Job deleted successfully.",
|
||||
"duplicated": "Job duplicated successfully. ",
|
||||
"early_ro_created": "Early RO Created",
|
||||
"exported": "Job(s) exported successfully. ",
|
||||
"invoiced": "Job closed and invoiced successfully.",
|
||||
"ioucreated": "IOU created successfully. Click to see.",
|
||||
|
||||
@@ -1047,7 +1047,9 @@
|
||||
},
|
||||
"dms": {
|
||||
"errors": {
|
||||
"alreadyexported": ""
|
||||
"alreadyexported": "",
|
||||
"earlyrorequired": "",
|
||||
"earlyrorequired.message": ""
|
||||
},
|
||||
"labels": {
|
||||
"refreshallocations": ""
|
||||
@@ -1244,6 +1246,7 @@
|
||||
"deselectall": "",
|
||||
"download": "",
|
||||
"edit": "Editar",
|
||||
"gotoadmin": "",
|
||||
"login": "",
|
||||
"next": "",
|
||||
"ok": "",
|
||||
@@ -1288,6 +1291,7 @@
|
||||
"vehicle": ""
|
||||
},
|
||||
"labels": {
|
||||
"apply": "",
|
||||
"actions": "Comportamiento",
|
||||
"areyousure": "",
|
||||
"barcode": "código de barras",
|
||||
@@ -1621,11 +1625,13 @@
|
||||
"changestatus": "Cambiar Estado",
|
||||
"changestimator": "",
|
||||
"convert": "Convertir",
|
||||
"convertwithoutearlyro": "",
|
||||
"createiou": "",
|
||||
"deliver": "",
|
||||
"deliver_quick": "",
|
||||
"dms": {
|
||||
"addpayer": "",
|
||||
"createearlyro": "",
|
||||
"createnewcustomer": "",
|
||||
"findmakemodelcode": "",
|
||||
"getmakes": "",
|
||||
@@ -1634,6 +1640,7 @@
|
||||
},
|
||||
"post": "",
|
||||
"refetchmakesmodels": "",
|
||||
"update_ro": "",
|
||||
"usegeneric": "",
|
||||
"useselected": ""
|
||||
},
|
||||
@@ -1793,6 +1800,7 @@
|
||||
},
|
||||
"cost": "",
|
||||
"cost_dms_acctnumber": "",
|
||||
"customer": "",
|
||||
"dms_make": "",
|
||||
"dms_model": "",
|
||||
"dms_model_override": "",
|
||||
@@ -1817,7 +1825,11 @@
|
||||
"sale": "",
|
||||
"sale_dms_acctnumber": "",
|
||||
"story": "",
|
||||
"vinowner": ""
|
||||
"vinowner": "",
|
||||
"rr_opcode": "",
|
||||
"rr_opcode_prefix": "",
|
||||
"rr_opcode_suffix": "",
|
||||
"rr_opcode_base": ""
|
||||
},
|
||||
"dms_allocation": "",
|
||||
"driveable": "",
|
||||
@@ -2102,6 +2114,11 @@
|
||||
"damageto": "",
|
||||
"defaultstory": "",
|
||||
"disablebillwip": "",
|
||||
"earlyro": {
|
||||
"created": "",
|
||||
"fields": "",
|
||||
"willupdate": ""
|
||||
},
|
||||
"invoicedatefuture": "",
|
||||
"kmoutnotgreaterthankmin": "",
|
||||
"logs": "",
|
||||
@@ -2259,6 +2276,7 @@
|
||||
"delete": "",
|
||||
"deleted": "Trabajo eliminado con éxito.",
|
||||
"duplicated": "",
|
||||
"early_ro_created": "",
|
||||
"exported": "",
|
||||
"invoiced": "",
|
||||
"ioucreated": "",
|
||||
|
||||
@@ -1047,7 +1047,9 @@
|
||||
},
|
||||
"dms": {
|
||||
"errors": {
|
||||
"alreadyexported": ""
|
||||
"alreadyexported": "",
|
||||
"earlyrorequired": "",
|
||||
"earlyrorequired.message": ""
|
||||
},
|
||||
"labels": {
|
||||
"refreshallocations": ""
|
||||
@@ -1244,6 +1246,7 @@
|
||||
"deselectall": "",
|
||||
"download": "",
|
||||
"edit": "modifier",
|
||||
"gotoadmin": "",
|
||||
"login": "",
|
||||
"next": "",
|
||||
"ok": "",
|
||||
@@ -1288,6 +1291,7 @@
|
||||
"vehicle": ""
|
||||
},
|
||||
"labels": {
|
||||
"apply": "",
|
||||
"actions": "actes",
|
||||
"areyousure": "",
|
||||
"barcode": "code à barre",
|
||||
@@ -1621,11 +1625,13 @@
|
||||
"changestatus": "Changer le statut",
|
||||
"changestimator": "",
|
||||
"convert": "Convertir",
|
||||
"convertwithoutearlyro": "",
|
||||
"createiou": "",
|
||||
"deliver": "",
|
||||
"deliver_quick": "",
|
||||
"dms": {
|
||||
"addpayer": "",
|
||||
"createearlyro": "",
|
||||
"createnewcustomer": "",
|
||||
"findmakemodelcode": "",
|
||||
"getmakes": "",
|
||||
@@ -1634,6 +1640,7 @@
|
||||
},
|
||||
"post": "",
|
||||
"refetchmakesmodels": "",
|
||||
"update_ro": "",
|
||||
"usegeneric": "",
|
||||
"useselected": ""
|
||||
},
|
||||
@@ -1793,6 +1800,7 @@
|
||||
},
|
||||
"cost": "",
|
||||
"cost_dms_acctnumber": "",
|
||||
"customer": "",
|
||||
"dms_make": "",
|
||||
"dms_model": "",
|
||||
"dms_model_override": "",
|
||||
@@ -1817,7 +1825,11 @@
|
||||
"sale": "",
|
||||
"sale_dms_acctnumber": "",
|
||||
"story": "",
|
||||
"vinowner": ""
|
||||
"vinowner": "",
|
||||
"rr_opcode": "",
|
||||
"rr_opcode_prefix": "",
|
||||
"rr_opcode_suffix": "",
|
||||
"rr_opcode_base": ""
|
||||
},
|
||||
"dms_allocation": "",
|
||||
"driveable": "",
|
||||
@@ -2102,6 +2114,11 @@
|
||||
"damageto": "",
|
||||
"defaultstory": "",
|
||||
"disablebillwip": "",
|
||||
"earlyro": {
|
||||
"created": "",
|
||||
"fields": "",
|
||||
"willupdate": ""
|
||||
},
|
||||
"invoicedatefuture": "",
|
||||
"kmoutnotgreaterthankmin": "",
|
||||
"logs": "",
|
||||
@@ -2259,6 +2276,7 @@
|
||||
"delete": "",
|
||||
"deleted": "Le travail a bien été supprimé.",
|
||||
"duplicated": "",
|
||||
"early_ro_created": "",
|
||||
"exported": "",
|
||||
"invoiced": "",
|
||||
"ioucreated": "",
|
||||
|
||||
@@ -1,44 +1,43 @@
|
||||
import { Select } from "antd";
|
||||
import i18n from "../translations/i18n";
|
||||
|
||||
export default function CiecaSelect(parts = true, labor = true) {
|
||||
return (
|
||||
<>
|
||||
{labor && (
|
||||
<>
|
||||
<Select.Option value="LAA">{i18n.t("joblines.fields.lbr_types.LAA")}</Select.Option>
|
||||
<Select.Option value="LAB">{i18n.t("joblines.fields.lbr_types.LAB")}</Select.Option>
|
||||
<Select.Option value="LAD">{i18n.t("joblines.fields.lbr_types.LAD")}</Select.Option>
|
||||
<Select.Option value="LAE">{i18n.t("joblines.fields.lbr_types.LAE")}</Select.Option>
|
||||
<Select.Option value="LAF">{i18n.t("joblines.fields.lbr_types.LAF")}</Select.Option>
|
||||
<Select.Option value="LAG">{i18n.t("joblines.fields.lbr_types.LAG")}</Select.Option>
|
||||
<Select.Option value="LAM">{i18n.t("joblines.fields.lbr_types.LAM")}</Select.Option>
|
||||
<Select.Option value="LAR">{i18n.t("joblines.fields.lbr_types.LAR")}</Select.Option>
|
||||
<Select.Option value="LAS">{i18n.t("joblines.fields.lbr_types.LAS")}</Select.Option>
|
||||
<Select.Option value="LAU">{i18n.t("joblines.fields.lbr_types.LAU")}</Select.Option>
|
||||
<Select.Option value="LA1">{i18n.t("joblines.fields.lbr_types.LA1")}</Select.Option>
|
||||
<Select.Option value="LA2">{i18n.t("joblines.fields.lbr_types.LA2")}</Select.Option>
|
||||
<Select.Option value="LA3">{i18n.t("joblines.fields.lbr_types.LA3")}</Select.Option>
|
||||
<Select.Option value="LA4">{i18n.t("joblines.fields.lbr_types.LA4")}</Select.Option>
|
||||
</>
|
||||
)}
|
||||
{parts && (
|
||||
<>
|
||||
<Select.Option value="PAA">{i18n.t("joblines.fields.part_types.PAA")}</Select.Option>
|
||||
<Select.Option value="PAC">{i18n.t("joblines.fields.part_types.PAC")}</Select.Option>
|
||||
|
||||
<Select.Option value="PAL">{i18n.t("joblines.fields.part_types.PAL")}</Select.Option>
|
||||
<Select.Option value="PAG">{i18n.t("joblines.fields.part_types.PAG")}</Select.Option>
|
||||
<Select.Option value="PAM">{i18n.t("joblines.fields.part_types.PAM")}</Select.Option>
|
||||
<Select.Option value="PAP">{i18n.t("joblines.fields.part_types.PAP")}</Select.Option>
|
||||
<Select.Option value="PAN">{i18n.t("joblines.fields.part_types.PAN")}</Select.Option>
|
||||
<Select.Option value="PAO">{i18n.t("joblines.fields.part_types.PAO")}</Select.Option>
|
||||
<Select.Option value="PAR">{i18n.t("joblines.fields.part_types.PAR")}</Select.Option>
|
||||
<Select.Option value="PAS">{i18n.t("joblines.fields.part_types.PAS")}</Select.Option>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
const options = [];
|
||||
|
||||
if (labor) {
|
||||
options.push(
|
||||
{ value: "LAA", label: i18n.t("joblines.fields.lbr_types.LAA") },
|
||||
{ value: "LAB", label: i18n.t("joblines.fields.lbr_types.LAB") },
|
||||
{ value: "LAD", label: i18n.t("joblines.fields.lbr_types.LAD") },
|
||||
{ value: "LAE", label: i18n.t("joblines.fields.lbr_types.LAE") },
|
||||
{ value: "LAF", label: i18n.t("joblines.fields.lbr_types.LAF") },
|
||||
{ value: "LAG", label: i18n.t("joblines.fields.lbr_types.LAG") },
|
||||
{ value: "LAM", label: i18n.t("joblines.fields.lbr_types.LAM") },
|
||||
{ value: "LAR", label: i18n.t("joblines.fields.lbr_types.LAR") },
|
||||
{ value: "LAS", label: i18n.t("joblines.fields.lbr_types.LAS") },
|
||||
{ value: "LAU", label: i18n.t("joblines.fields.lbr_types.LAU") },
|
||||
{ value: "LA1", label: i18n.t("joblines.fields.lbr_types.LA1") },
|
||||
{ value: "LA2", label: i18n.t("joblines.fields.lbr_types.LA2") },
|
||||
{ value: "LA3", label: i18n.t("joblines.fields.lbr_types.LA3") },
|
||||
{ value: "LA4", label: i18n.t("joblines.fields.lbr_types.LA4") }
|
||||
);
|
||||
}
|
||||
|
||||
if (parts) {
|
||||
options.push(
|
||||
{ value: "PAA", label: i18n.t("joblines.fields.part_types.PAA") },
|
||||
{ value: "PAC", label: i18n.t("joblines.fields.part_types.PAC") },
|
||||
{ value: "PAL", label: i18n.t("joblines.fields.part_types.PAL") },
|
||||
{ value: "PAG", label: i18n.t("joblines.fields.part_types.PAG") },
|
||||
{ value: "PAM", label: i18n.t("joblines.fields.part_types.PAM") },
|
||||
{ value: "PAP", label: i18n.t("joblines.fields.part_types.PAP") },
|
||||
{ value: "PAN", label: i18n.t("joblines.fields.part_types.PAN") },
|
||||
{ value: "PAO", label: i18n.t("joblines.fields.part_types.PAO") },
|
||||
{ value: "PAR", label: i18n.t("joblines.fields.part_types.PAR") },
|
||||
{ value: "PAS", label: i18n.t("joblines.fields.part_types.PAS") }
|
||||
);
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
export function GetPartTypeName(part_type) {
|
||||
|
||||
@@ -248,7 +248,8 @@ const client = new ApolloClient({
|
||||
watchQuery: {
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only",
|
||||
errorPolicy: "ignore"
|
||||
errorPolicy: "ignore",
|
||||
notifyOnNetworkStatusChange: false
|
||||
},
|
||||
query: {
|
||||
fetchPolicy: "network-only",
|
||||
|
||||
@@ -146,7 +146,8 @@ export async function generateTemplate(
|
||||
if (templateQueryToExecute) {
|
||||
const { data } = await client.query({
|
||||
query: gql(finalQuery),
|
||||
variables: { ...templateObject.variables }
|
||||
variables: { ...templateObject.variables },
|
||||
fetchPolicy: "no-cache"
|
||||
});
|
||||
contextData = data;
|
||||
}
|
||||
|
||||
@@ -38,8 +38,6 @@ services:
|
||||
condition: service_healthy
|
||||
localstack:
|
||||
condition: service_healthy
|
||||
aws-cli:
|
||||
condition: service_completed_successfully
|
||||
ports:
|
||||
- "4001:4000" # Different external port for local access
|
||||
volumes:
|
||||
@@ -65,8 +63,6 @@ services:
|
||||
condition: service_healthy
|
||||
localstack:
|
||||
condition: service_healthy
|
||||
aws-cli:
|
||||
condition: service_completed_successfully
|
||||
ports:
|
||||
- "4002:4000" # Different external port for local access
|
||||
volumes:
|
||||
@@ -92,8 +88,6 @@ services:
|
||||
condition: service_healthy
|
||||
localstack:
|
||||
condition: service_healthy
|
||||
aws-cli:
|
||||
condition: service_completed_successfully
|
||||
ports:
|
||||
- "4003:4000" # Different external port for local access
|
||||
volumes:
|
||||
@@ -156,23 +150,18 @@ services:
|
||||
|
||||
# LocalStack
|
||||
localstack:
|
||||
image: localstack/localstack
|
||||
image: localstack/localstack:4.13.1
|
||||
container_name: localstack
|
||||
hostname: localstack
|
||||
networks:
|
||||
- redis-cluster-net
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./certs:/tmp/certs:ro # only if your script reads /tmp/certs/...
|
||||
- ./localstack/init:/etc/localstack/init/ready.d:ro
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
environment:
|
||||
- SERVICES=s3,ses,secretsmanager,cloudwatch,logs
|
||||
- DEBUG=0
|
||||
- AWS_ACCESS_KEY_ID=test
|
||||
- AWS_SECRET_ACCESS_KEY=test
|
||||
- AWS_DEFAULT_REGION=ca-central-1
|
||||
- EXTRA_CORS_ALLOWED_HEADERS=Authorization,Content-Type
|
||||
- EXTRA_CORS_ALLOWED_ORIGINS=*
|
||||
- EXTRA_CORS_EXPOSE_HEADERS=Authorization,Content-Type
|
||||
env_file:
|
||||
- .env.localstack.docker
|
||||
ports:
|
||||
- "4566:4566"
|
||||
healthcheck:
|
||||
@@ -182,36 +171,6 @@ services:
|
||||
retries: 5
|
||||
start_period: 20s
|
||||
|
||||
# AWS-CLI
|
||||
aws-cli:
|
||||
image: amazon/aws-cli
|
||||
container_name: aws-cli
|
||||
hostname: aws-cli
|
||||
networks:
|
||||
- redis-cluster-net
|
||||
depends_on:
|
||||
localstack:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- './localstack:/tmp/localstack'
|
||||
- './certs:/tmp/certs'
|
||||
environment:
|
||||
- AWS_ACCESS_KEY_ID=test
|
||||
- AWS_SECRET_ACCESS_KEY=test
|
||||
- AWS_DEFAULT_REGION=ca-central-1
|
||||
entrypoint: /bin/sh -c
|
||||
command: >
|
||||
"
|
||||
aws --endpoint-url=http://localstack:4566 ses verify-domain-identity --domain imex.online --region ca-central-1
|
||||
aws --endpoint-url=http://localstack:4566 ses verify-email-identity --email-address noreply@imex.online --region ca-central-1
|
||||
aws --endpoint-url=http://localstack:4566 secretsmanager create-secret --name CHATTER_PRIVATE_KEY --secret-string file:///tmp/certs/io-ftp-test.key
|
||||
aws --endpoint-url=http://localstack:4566 logs create-log-group --log-group-name development --region ca-central-1
|
||||
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket imex-large-log --create-bucket-configuration LocationConstraint=ca-central-1
|
||||
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket imex-carfax-uploads --create-bucket-configuration LocationConstraint=ca-central-1
|
||||
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket rome-carfax-uploads --create-bucket-configuration LocationConstraint=ca-central-1
|
||||
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket rps-carfax-uploads --create-bucket-configuration LocationConstraint=ca-central-1
|
||||
"
|
||||
|
||||
networks:
|
||||
redis-cluster-net:
|
||||
driver: bridge
|
||||
|
||||
@@ -68,23 +68,18 @@ services:
|
||||
# LocalStack: Used to emulate AWS services locally, currently setup for SES
|
||||
# Notes: Set the ENV Debug to 1 for additional logging
|
||||
localstack:
|
||||
image: localstack/localstack
|
||||
image: localstack/localstack:4.13.1
|
||||
container_name: localstack
|
||||
hostname: localstack
|
||||
networks:
|
||||
- redis-cluster-net
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./certs:/tmp/certs:ro # only if your script reads /tmp/certs/...
|
||||
- ./localstack/init:/etc/localstack/init/ready.d:ro
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
environment:
|
||||
- SERVICES=s3,ses,secretsmanager,cloudwatch,logs
|
||||
- DEBUG=0
|
||||
- AWS_ACCESS_KEY_ID=test
|
||||
- AWS_SECRET_ACCESS_KEY=test
|
||||
- AWS_DEFAULT_REGION=ca-central-1
|
||||
- EXTRA_CORS_ALLOWED_HEADERS=Authorization,Content-Type
|
||||
- EXTRA_CORS_ALLOWED_ORIGINS=*
|
||||
- EXTRA_CORS_EXPOSE_HEADERS=Authorization,Content-Type
|
||||
env_file:
|
||||
- .env.localstack.docker
|
||||
ports:
|
||||
- "4566:4566"
|
||||
healthcheck:
|
||||
@@ -94,38 +89,6 @@ services:
|
||||
retries: 5
|
||||
start_period: 20s
|
||||
|
||||
# AWS-CLI - Used in conjunction with LocalStack to set required permission to send emails
|
||||
aws-cli:
|
||||
image: amazon/aws-cli
|
||||
container_name: aws-cli
|
||||
hostname: aws-cli
|
||||
networks:
|
||||
- redis-cluster-net
|
||||
depends_on:
|
||||
localstack:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- './localstack:/tmp/localstack'
|
||||
- './certs:/tmp/certs'
|
||||
|
||||
environment:
|
||||
- AWS_ACCESS_KEY_ID=test
|
||||
- AWS_SECRET_ACCESS_KEY=test
|
||||
- AWS_DEFAULT_REGION=ca-central-1
|
||||
entrypoint: /bin/sh -c
|
||||
command: >
|
||||
"
|
||||
aws --endpoint-url=http://localstack:4566 ses verify-domain-identity --domain imex.online --region ca-central-1
|
||||
aws --endpoint-url=http://localstack:4566 ses verify-email-identity --email-address noreply@imex.online --region ca-central-1
|
||||
aws --endpoint-url=http://localstack:4566 secretsmanager create-secret --name CHATTER_PRIVATE_KEY --secret-string file:///tmp/certs/io-ftp-test.key
|
||||
aws --endpoint-url=http://localstack:4566 logs create-log-group --log-group-name development --region ca-central-1
|
||||
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket imex-large-log --create-bucket-configuration LocationConstraint=ca-central-1
|
||||
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket imex-job-totals --create-bucket-configuration LocationConstraint=ca-central-1
|
||||
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket parts-estimates --create-bucket-configuration LocationConstraint=ca-central-1
|
||||
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket imex-carfax-uploads --create-bucket-configuration LocationConstraint=ca-central-1
|
||||
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket rome-carfax-uploads --create-bucket-configuration LocationConstraint=ca-central-1
|
||||
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket rps-carfax-uploads --create-bucket-configuration LocationConstraint=ca-central-1
|
||||
"
|
||||
# Node App: The Main IMEX API
|
||||
node-app:
|
||||
build:
|
||||
@@ -145,8 +108,7 @@ services:
|
||||
condition: service_healthy
|
||||
localstack:
|
||||
condition: service_healthy
|
||||
aws-cli:
|
||||
condition: service_completed_successfully
|
||||
|
||||
ports:
|
||||
- "4000:4000"
|
||||
- "9229:9229"
|
||||
|
||||
@@ -947,6 +947,7 @@
|
||||
- carfax_exclude
|
||||
- cdk_configuration
|
||||
- cdk_dealerid
|
||||
- chatter_company_id
|
||||
- chatterid
|
||||
- city
|
||||
- claimscorpid
|
||||
@@ -1063,6 +1064,7 @@
|
||||
- bill_allow_post_to_closed
|
||||
- bill_tax_rates
|
||||
- cdk_configuration
|
||||
- chatter_company_id
|
||||
- city
|
||||
- country
|
||||
- created_at
|
||||
@@ -3702,7 +3704,9 @@
|
||||
- ded_status
|
||||
- deliverchecklist
|
||||
- depreciation_taxes
|
||||
- dms_advisor_id
|
||||
- dms_allocation
|
||||
- dms_customer_id
|
||||
- dms_id
|
||||
- driveable
|
||||
- employee_body
|
||||
@@ -3983,7 +3987,9 @@
|
||||
- ded_status
|
||||
- deliverchecklist
|
||||
- depreciation_taxes
|
||||
- dms_advisor_id
|
||||
- dms_allocation
|
||||
- dms_customer_id
|
||||
- dms_id
|
||||
- driveable
|
||||
- employee_body
|
||||
@@ -4276,7 +4282,9 @@
|
||||
- ded_status
|
||||
- deliverchecklist
|
||||
- depreciation_taxes
|
||||
- dms_advisor_id
|
||||
- dms_allocation
|
||||
- dms_customer_id
|
||||
- dms_id
|
||||
- driveable
|
||||
- employee_body
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Could not auto-generate a down migration.
|
||||
-- Please write an appropriate down migration for the SQL below:
|
||||
-- alter table "public"."bodyshops" add column "chatter_company_id" text
|
||||
-- null;
|
||||
@@ -0,0 +1,2 @@
|
||||
alter table "public"."bodyshops" add column "chatter_company_id" text
|
||||
null;
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Could not auto-generate a down migration.
|
||||
-- Please write an appropriate down migration for the SQL below:
|
||||
-- alter table "public"."jobs" add column "dms_customer_id" text
|
||||
-- null;
|
||||
@@ -0,0 +1,2 @@
|
||||
alter table "public"."jobs" add column "dms_customer_id" text
|
||||
null;
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Could not auto-generate a down migration.
|
||||
-- Please write an appropriate down migration for the SQL below:
|
||||
-- alter table "public"."jobs" add column "dms_advisor_id" text
|
||||
-- null;
|
||||
@@ -0,0 +1,2 @@
|
||||
alter table "public"."jobs" add column "dms_advisor_id" text
|
||||
null;
|
||||
65
localstack/init/10-bootstrap.sh
Normal file
65
localstack/init/10-bootstrap.sh
Normal file
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env bash
|
||||
echo "Running LocalStack bootstrap script: 10-bootstrap.sh"
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REGION="${AWS_DEFAULT_REGION:-ca-central-1}"
|
||||
|
||||
# awslocal is the LocalStack wrapper so you don't need --endpoint-url
|
||||
# (it targets the LocalStack gateway automatically)
|
||||
# Docs: https://docs.localstack.cloud/.../aws-cli/
|
||||
ensure_bucket() {
|
||||
local b="$1"
|
||||
if ! awslocal s3api head-bucket --bucket "$b" >/dev/null 2>&1; then
|
||||
awslocal s3api create-bucket \
|
||||
--bucket "$b" \
|
||||
--create-bucket-configuration LocationConstraint="$REGION" \
|
||||
--region "$REGION" >/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_log_group() {
|
||||
local lg="$1"
|
||||
awslocal logs create-log-group --log-group-name "$lg" --region "$REGION" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
ensure_secret_string() {
|
||||
local name="$1"
|
||||
local value="$2"
|
||||
|
||||
if awslocal secretsmanager describe-secret --secret-id "$name" >/dev/null 2>&1; then
|
||||
awslocal secretsmanager update-secret --secret-id "$name" --secret-string "$value" >/dev/null
|
||||
else
|
||||
awslocal secretsmanager create-secret --name "$name" --secret-string "$value" >/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_secret_file() {
|
||||
local name="$1"
|
||||
local filepath="$2"
|
||||
|
||||
if awslocal secretsmanager describe-secret --secret-id "$name" >/dev/null 2>&1; then
|
||||
awslocal secretsmanager update-secret --secret-id "$name" --secret-string "file://$filepath" >/dev/null
|
||||
else
|
||||
awslocal secretsmanager create-secret --name "$name" --secret-string "file://$filepath" >/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
# SES identities (idempotent-ish; ignoring if it already exists)
|
||||
awslocal ses verify-domain-identity --domain imex.online --region "$REGION" >/dev/null || true
|
||||
awslocal ses verify-email-identity --email-address noreply@imex.online --region "$REGION" >/dev/null || true
|
||||
|
||||
# Secrets
|
||||
ensure_secret_file "CHATTER_PRIVATE_KEY" "/tmp/certs/io-ftp-test.key"
|
||||
ensure_secret_string "CHATTER_COMPANY_KEY_6713" "${CHATTER_COMPANY_KEY_6713:-REPLACE_ME}"
|
||||
|
||||
# Logs
|
||||
ensure_log_group "development"
|
||||
|
||||
# Buckets
|
||||
ensure_bucket "imex-job-totals"
|
||||
ensure_bucket "parts-estimate"
|
||||
ensure_bucket "imex-large-log"
|
||||
ensure_bucket "imex-carfax-uploads"
|
||||
ensure_bucket "rome-carfax-uploads"
|
||||
ensure_bucket "rps-carfax-uploads"
|
||||
1062
package-lock.json
generated
1062
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
38
package.json
38
package.json
@@ -18,23 +18,23 @@
|
||||
"job-totals-fixtures:local": "docker exec node-app /usr/bin/node /app/download-job-totals-fixtures.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-cloudwatch-logs": "^3.978.0",
|
||||
"@aws-sdk/client-elasticache": "^3.978.0",
|
||||
"@aws-sdk/client-s3": "^3.978.0",
|
||||
"@aws-sdk/client-secrets-manager": "^3.978.0",
|
||||
"@aws-sdk/client-ses": "^3.978.0",
|
||||
"@aws-sdk/credential-provider-node": "^3.972.3",
|
||||
"@aws-sdk/lib-storage": "^3.978.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.978.0",
|
||||
"@aws-sdk/client-cloudwatch-logs": "^3.992.0",
|
||||
"@aws-sdk/client-elasticache": "^3.992.0",
|
||||
"@aws-sdk/client-s3": "^3.992.0",
|
||||
"@aws-sdk/client-secrets-manager": "^3.992.0",
|
||||
"@aws-sdk/client-ses": "^3.992.0",
|
||||
"@aws-sdk/credential-provider-node": "^3.972.9",
|
||||
"@aws-sdk/lib-storage": "^3.992.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.992.0",
|
||||
"@opensearch-project/opensearch": "^2.13.0",
|
||||
"@socket.io/admin-ui": "^0.5.1",
|
||||
"@socket.io/redis-adapter": "^8.3.0",
|
||||
"archiver": "^7.0.1",
|
||||
"aws4": "^1.13.2",
|
||||
"axios": "^1.13.4",
|
||||
"axios": "^1.13.5",
|
||||
"axios-curlirize": "^2.0.0",
|
||||
"better-queue": "^3.8.12",
|
||||
"bullmq": "^5.67.2",
|
||||
"bullmq": "^5.69.3",
|
||||
"chart.js": "^4.5.1",
|
||||
"cloudinary": "^2.9.0",
|
||||
"compression": "^1.8.1",
|
||||
@@ -42,17 +42,17 @@
|
||||
"cors": "^2.8.6",
|
||||
"crisp-status-reporter": "^1.2.2",
|
||||
"dinero.js": "^1.9.1",
|
||||
"dotenv": "^17.2.3",
|
||||
"dotenv": "^17.3.1",
|
||||
"express": "^4.21.1",
|
||||
"fast-xml-parser": "^5.3.4",
|
||||
"firebase-admin": "^13.6.0",
|
||||
"fast-xml-parser": "^5.3.6",
|
||||
"firebase-admin": "^13.6.1",
|
||||
"graphql": "^16.12.0",
|
||||
"graphql-request": "^6.1.0",
|
||||
"intuit-oauth": "^4.2.2",
|
||||
"ioredis": "^5.9.2",
|
||||
"ioredis": "^5.9.3",
|
||||
"json-2-csv": "^5.5.10",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"juice": "^11.1.0",
|
||||
"juice": "^11.1.1",
|
||||
"lodash": "^4.17.23",
|
||||
"moment": "^2.30.1",
|
||||
"moment-timezone": "^0.6.0",
|
||||
@@ -63,13 +63,13 @@
|
||||
"phone": "^3.1.70",
|
||||
"query-string": "7.1.3",
|
||||
"recursive-diff": "^1.0.9",
|
||||
"rimraf": "^6.1.2",
|
||||
"rimraf": "^6.1.3",
|
||||
"skia-canvas": "^3.0.8",
|
||||
"soap": "^1.6.4",
|
||||
"soap": "^1.7.1",
|
||||
"socket.io": "^4.8.3",
|
||||
"socket.io-adapter": "^2.5.6",
|
||||
"ssh2-sftp-client": "^11.0.0",
|
||||
"twilio": "^5.12.0",
|
||||
"twilio": "^5.12.2",
|
||||
"uuid": "^11.1.0",
|
||||
"winston": "^3.19.0",
|
||||
"winston-cloudwatch": "^6.3.0",
|
||||
@@ -82,7 +82,7 @@
|
||||
"@eslint/js": "^9.39.2",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"globals": "^17.2.0",
|
||||
"globals": "^17.3.0",
|
||||
"mock-require": "^3.0.3",
|
||||
"p-limit": "^3.1.0",
|
||||
"prettier": "^3.8.1",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user