Compare commits
107 Commits
bugfix/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 | ||
|
|
9c0cb5f80b | ||
|
|
1f726aca4d | ||
|
|
b9f398cf2d | ||
|
|
ff73a14610 | ||
|
|
1e44d4fe42 | ||
|
|
0f42875d1b | ||
|
|
a0f1299006 | ||
|
|
87d8a5d746 | ||
|
|
268851902a | ||
|
|
68bb7d2529 | ||
|
|
d50db12330 | ||
|
|
1438986c18 | ||
|
|
c047699fbb | ||
|
|
e5b7fcb919 | ||
|
|
cadcfc9b0d | ||
|
|
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
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
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 />
|
||||
|
||||
@@ -1,29 +1,65 @@
|
||||
import { useMemo } from "react";
|
||||
import { Tag, Tooltip } from "antd";
|
||||
import { Tooltip } from "antd";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = () => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
const mapDispatchToProps = () => ({});
|
||||
|
||||
const colorMap = {
|
||||
gray: { bg: "#fafafa", border: "#d9d9d9", text: "#000000" },
|
||||
gold: { bg: "#fffbe6", border: "#ffe58f", text: "#d48806" },
|
||||
red: { bg: "#fff1f0", border: "#ffccc7", text: "#cf1322" },
|
||||
blue: { bg: "#e6f7ff", border: "#91d5ff", text: "#0958d9" },
|
||||
green: { bg: "#f6ffed", border: "#b7eb8f", text: "#389e0d" },
|
||||
orange: { bg: "#fff7e6", border: "#ffd591", text: "#d46b08" }
|
||||
};
|
||||
|
||||
function CompactTag({ color = "gray", children, tooltip = "" }) {
|
||||
const colors = colorMap[color] || colorMap.gray;
|
||||
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: "0 2px",
|
||||
fontSize: "12px",
|
||||
lineHeight: "20px",
|
||||
backgroundColor: colors.bg,
|
||||
border: `1px solid ${colors.border}`,
|
||||
borderRadius: "2px",
|
||||
color: colors.text,
|
||||
minWidth: "24px",
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
<Tooltip title={tooltip}>{children}</Tooltip>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobPartsQueueCount);
|
||||
|
||||
export function JobPartsQueueCount({ bodyshop, parts }) {
|
||||
const { t } = useTranslation();
|
||||
const partsStatus = useMemo(() => {
|
||||
if (!parts) return null;
|
||||
|
||||
const statusKeys = ["default_bo", "default_ordered", "default_received", "default_returned"];
|
||||
|
||||
return parts.reduce(
|
||||
(acc, val) => {
|
||||
if (val.part_type === "PAS" || val.part_type === "PASL") return acc;
|
||||
acc.total = acc.total + val.count;
|
||||
acc[val.status] = acc[val.status] + val.count;
|
||||
|
||||
acc.total += val.count;
|
||||
|
||||
// NOTE: if val.status is null, object key becomes "null"
|
||||
acc[val.status] = (acc[val.status] ?? 0) + val.count;
|
||||
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
@@ -34,45 +70,38 @@ export function JobPartsQueueCount({ bodyshop, parts }) {
|
||||
);
|
||||
}, [bodyshop, parts]);
|
||||
|
||||
if (!parts) return null;
|
||||
if (!parts || !partsStatus) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(auto-fit, minmax(40px, 1fr))",
|
||||
gap: "8px",
|
||||
width: "100%",
|
||||
justifyItems: "start"
|
||||
display: "inline-flex", // ✅ shrink-wraps, fixes the “extra box” to the right
|
||||
gap: 2,
|
||||
alignItems: "center",
|
||||
whiteSpace: "nowrap"
|
||||
}}
|
||||
>
|
||||
<Tooltip title="Total">
|
||||
<Tag style={{ minWidth: "40px", textAlign: "center" }}>{partsStatus.total}</Tag>
|
||||
</Tooltip>
|
||||
<Tooltip title={t("dashboard.errors.status_normal")}>
|
||||
<Tag color="gold" style={{ minWidth: "40px", textAlign: "center" }}>
|
||||
{partsStatus["null"]}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
<Tooltip title={bodyshop.md_order_statuses.default_bo}>
|
||||
<Tag color="red" style={{ minWidth: "40px", textAlign: "center" }}>
|
||||
{partsStatus[bodyshop.md_order_statuses.default_bo]}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
<Tooltip title={bodyshop.md_order_statuses.default_ordered}>
|
||||
<Tag color="blue" style={{ minWidth: "40px", textAlign: "center" }}>
|
||||
{partsStatus[bodyshop.md_order_statuses.default_ordered]}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
<Tooltip title={bodyshop.md_order_statuses.default_received}>
|
||||
<Tag color="green" style={{ minWidth: "40px", textAlign: "center" }}>
|
||||
{partsStatus[bodyshop.md_order_statuses.default_received]}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
<Tooltip title={bodyshop.md_order_statuses.default_returned}>
|
||||
<Tag color="orange" style={{ minWidth: "40px", textAlign: "center" }}>
|
||||
{partsStatus[bodyshop.md_order_statuses.default_returned]}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
<CompactTag tooltip="Total" color="gray">
|
||||
{partsStatus.total}
|
||||
</CompactTag>
|
||||
|
||||
<CompactTag tooltip="No Status" color="gold">
|
||||
{partsStatus["null"]}
|
||||
</CompactTag>
|
||||
|
||||
<CompactTag tooltip={bodyshop.md_order_statuses.default_bo} color="red">
|
||||
{partsStatus[bodyshop.md_order_statuses.default_bo]}
|
||||
</CompactTag>
|
||||
|
||||
<CompactTag tooltip={bodyshop.md_order_statuses.default_ordered} color="blue">
|
||||
{partsStatus[bodyshop.md_order_statuses.default_ordered]}
|
||||
</CompactTag>
|
||||
<CompactTag tooltip={bodyshop.md_order_statuses.default_received} color="green">
|
||||
{partsStatus[bodyshop.md_order_statuses.default_received]}
|
||||
</CompactTag>
|
||||
<CompactTag tooltip={bodyshop.md_order_statuses.default_returned} color="orange">
|
||||
{partsStatus[bodyshop.md_order_statuses.default_returned]}
|
||||
</CompactTag>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,10 +18,17 @@ const mapStateToProps = createStructuredSelector({
|
||||
* @param parts
|
||||
* @param displayMode
|
||||
* @param popoverPlacement
|
||||
* @param countsOnly
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
export function JobPartsReceived({ bodyshop, parts, displayMode = "full", popoverPlacement = "top" }) {
|
||||
export function JobPartsReceived({
|
||||
bodyshop,
|
||||
parts,
|
||||
displayMode = "full",
|
||||
popoverPlacement = "top",
|
||||
countsOnly = false
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -61,6 +68,8 @@ export function JobPartsReceived({ bodyshop, parts, displayMode = "full", popove
|
||||
[canOpen]
|
||||
);
|
||||
|
||||
if (countsOnly) return <JobPartsQueueCount parts={parts} />;
|
||||
|
||||
const displayText =
|
||||
displayMode === "compact" ? summary.percentLabel : `${summary.percentLabel} (${summary.received}/${summary.total})`;
|
||||
|
||||
@@ -74,7 +83,7 @@ export function JobPartsReceived({ bodyshop, parts, displayMode = "full", popove
|
||||
trigger={["click"]}
|
||||
placement={popoverPlacement}
|
||||
content={
|
||||
<div onClick={stop} style={{ minWidth: 260 }}>
|
||||
<div onClick={stop}>
|
||||
<JobPartsQueueCount parts={parts} />
|
||||
</div>
|
||||
}
|
||||
@@ -99,7 +108,8 @@ JobPartsReceived.propTypes = {
|
||||
bodyshop: PropTypes.object,
|
||||
parts: PropTypes.array,
|
||||
displayMode: PropTypes.oneOf(["full", "compact"]),
|
||||
popoverPlacement: PropTypes.string
|
||||
popoverPlacement: PropTypes.string,
|
||||
countsOnly: PropTypes.bool
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(JobPartsReceived);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,6 +2,7 @@ import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { store } from "../../redux/store";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { Tooltip } from "antd";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
@@ -11,15 +12,27 @@ const mapDispatchToProps = () => ({
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(OwnerNameDisplay);
|
||||
|
||||
export function OwnerNameDisplay({ bodyshop, ownerObject }) {
|
||||
export function OwnerNameDisplay({ bodyshop, ownerObject, withToolTip = false }) {
|
||||
const emptyTest = ownerObject?.ownr_fn + ownerObject?.ownr_ln + ownerObject?.ownr_co_nm;
|
||||
|
||||
if (!emptyTest || emptyTest === "null" || emptyTest.trim() === "") return "N/A";
|
||||
|
||||
if (bodyshop.last_name_first)
|
||||
return `${ownerObject?.ownr_ln || ""}, ${ownerObject?.ownr_fn || ""} ${ownerObject?.ownr_co_nm || ""}`.trim();
|
||||
|
||||
return `${ownerObject?.ownr_fn || ""} ${ownerObject?.ownr_ln || ""} ${ownerObject.ownr_co_nm || ""}`.trim();
|
||||
let returnString;
|
||||
if (bodyshop.last_name_first) {
|
||||
returnString =
|
||||
`${ownerObject?.ownr_ln || ""}, ${ownerObject?.ownr_fn || ""} ${ownerObject?.ownr_co_nm || ""}`.trim();
|
||||
} else {
|
||||
returnString = `${ownerObject?.ownr_fn || ""} ${ownerObject?.ownr_ln || ""} ${ownerObject.ownr_co_nm || ""}`.trim();
|
||||
}
|
||||
if (withToolTip) {
|
||||
return (
|
||||
<Tooltip title={returnString} mouseEnterDelay={0.5}>
|
||||
{returnString}
|
||||
</Tooltip>
|
||||
);
|
||||
} else {
|
||||
return returnString;
|
||||
}
|
||||
}
|
||||
|
||||
export function OwnerNameDisplayFunction(ownerObject, forceFirstLast = false) {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { SyncOutlined } from "@ant-design/icons";
|
||||
import { useQuery } from "@apollo/client/react";
|
||||
import { Button, Card, Input, Space, Table } from "antd";
|
||||
import { Button, Card, Checkbox, Input, Space, Table } from "antd";
|
||||
import _ from "lodash";
|
||||
import queryString from "query-string";
|
||||
import { useState } from "react";
|
||||
@@ -31,6 +31,8 @@ export function PartsQueueListComponent({ bodyshop }) {
|
||||
const { selected, sortcolumn, sortorder, statusFilters } = searchParams;
|
||||
const history = useNavigate();
|
||||
const [filter, setFilter] = useLocalStorage("filter_parts_queue", null);
|
||||
const [viewTimeStamp, setViewTimeStamp] = useLocalStorage("parts_queue_timestamps", false);
|
||||
const [countsOnly, setCountsOnly] = useLocalStorage("parts_queue_counts_only", false);
|
||||
|
||||
const { loading, error, data, refetch } = useQuery(QUERY_PARTS_QUEUE, {
|
||||
fetchPolicy: "network-only",
|
||||
@@ -92,6 +94,7 @@ export function PartsQueueListComponent({ bodyshop }) {
|
||||
title: t("jobs.fields.ro_number"),
|
||||
dataIndex: "ro_number",
|
||||
key: "ro_number",
|
||||
width: "110px",
|
||||
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
|
||||
sortOrder: sortcolumn === "ro_number" && sortorder,
|
||||
|
||||
@@ -103,16 +106,20 @@ export function PartsQueueListComponent({ bodyshop }) {
|
||||
title: t("jobs.fields.owner"),
|
||||
dataIndex: "ownr_ln",
|
||||
key: "ownr_ln",
|
||||
width: "8%",
|
||||
ellipsis: {
|
||||
showTitle: true
|
||||
},
|
||||
sorter: (a, b) => alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
|
||||
sortOrder: sortcolumn === "ownr_ln" && sortorder,
|
||||
render: (text, record) => {
|
||||
return record.ownerid ? (
|
||||
<Link to={"/manage/owners/" + record.ownerid}>
|
||||
<OwnerNameDisplay ownerObject={record} />
|
||||
<OwnerNameDisplay ownerObject={record} withToolTip />
|
||||
</Link>
|
||||
) : (
|
||||
<span>
|
||||
<OwnerNameDisplay ownerObject={record} />
|
||||
<OwnerNameDisplay ownerObject={record} withToolTip />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -187,7 +194,7 @@ export function PartsQueueListComponent({ bodyshop }) {
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => dateSort(a.scheduled_in, b.scheduled_in),
|
||||
sortOrder: sortcolumn === "scheduled_in" && sortorder,
|
||||
render: (text, record) => <DateTimeFormatter>{record.scheduled_in}</DateTimeFormatter>
|
||||
render: (text, record) => <DateTimeFormatter hideTime={!viewTimeStamp}>{record.scheduled_in}</DateTimeFormatter>
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.scheduled_completion"),
|
||||
@@ -196,7 +203,9 @@ export function PartsQueueListComponent({ bodyshop }) {
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => dateSort(a.scheduled_completion, b.scheduled_completion),
|
||||
sortOrder: sortcolumn === "scheduled_completion" && sortorder,
|
||||
render: (text, record) => <DateTimeFormatter>{record.scheduled_completion}</DateTimeFormatter>
|
||||
render: (text, record) => (
|
||||
<DateTimeFormatter hideTime={!viewTimeStamp}>{record.scheduled_completion}</DateTimeFormatter>
|
||||
)
|
||||
},
|
||||
// {
|
||||
// title: t("vehicles.fields.plate_no"),
|
||||
@@ -227,16 +236,23 @@ export function PartsQueueListComponent({ bodyshop }) {
|
||||
title: t("jobs.fields.updated_at"),
|
||||
dataIndex: "updated_at",
|
||||
key: "updated_at",
|
||||
width: "110px",
|
||||
sorter: (a, b) => dateSort(a.updated_at, b.updated_at),
|
||||
sortOrder: sortcolumn === "updated_at" && sortorder,
|
||||
render: (text, record) => <TimeAgoFormatter>{record.updated_at}</TimeAgoFormatter>
|
||||
render: (text, record) => <TimeAgoFormatter removeAgoString>{record.updated_at}</TimeAgoFormatter>
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.partsstatus"),
|
||||
dataIndex: "partsstatus",
|
||||
key: "partsstatus",
|
||||
width: countsOnly ? "180px" : "110px",
|
||||
render: (text, record) => (
|
||||
<JobPartsReceived parts={record.joblines_status} displayMode="full" popoverPlacement="topLeft" />
|
||||
<JobPartsReceived
|
||||
parts={record.joblines_status}
|
||||
displayMode="full"
|
||||
popoverPlacement="middle"
|
||||
countsOnly={countsOnly}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
@@ -249,6 +265,7 @@ export function PartsQueueListComponent({ bodyshop }) {
|
||||
title: t("jobs.fields.queued_for_parts"),
|
||||
dataIndex: "queued_for_parts",
|
||||
key: "queued_for_parts",
|
||||
width: "120px",
|
||||
sorter: (a, b) => a.queued_for_parts - b.queued_for_parts,
|
||||
sortOrder: sortcolumn === "queued_for_parts" && sortorder,
|
||||
filteredValue: filter?.queued_for_parts || null,
|
||||
@@ -275,6 +292,12 @@ export function PartsQueueListComponent({ bodyshop }) {
|
||||
<Button onClick={() => refetch()}>
|
||||
<SyncOutlined />
|
||||
</Button>
|
||||
<Checkbox checked={countsOnly} onChange={(e) => setCountsOnly(e.target.checked)}>
|
||||
{t("parts.labels.view_counts_only")}
|
||||
</Checkbox>
|
||||
<Checkbox checked={viewTimeStamp} onChange={(e) => setViewTimeStamp(e.target.checked)}>
|
||||
{t("parts.labels.view_timestamps")}
|
||||
</Checkbox>
|
||||
<Input.Search
|
||||
className="imex-table-header__search"
|
||||
placeholder={t("general.labels.search")}
|
||||
@@ -299,7 +322,7 @@ export function PartsQueueListComponent({ bodyshop }) {
|
||||
rowKey="id"
|
||||
dataSource={jobs}
|
||||
style={{ height: "100%" }}
|
||||
scroll={{ x: true }}
|
||||
//scroll={{ x: true }}
|
||||
onChange={handleTableChange}
|
||||
rowSelection={{
|
||||
onSelect: (record) => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
"arrivedon": "Arrived on: ",
|
||||
"arrivingjobs": "Arriving Jobs",
|
||||
"blocked": "Blocked",
|
||||
"bp": "B/P",
|
||||
"cancelledappointment": "Canceled appointment for: ",
|
||||
"completingjobs": "Completing Jobs",
|
||||
"dataconsistency": "<0>{{ro_number}}</0> has a data consistency issue. It may have been excluded for scheduling purposes. CODE: {{code}}.",
|
||||
@@ -59,18 +60,17 @@
|
||||
"noarrivingjobs": "No Jobs are arriving.",
|
||||
"nocompletingjobs": "No Jobs scheduled for completion.",
|
||||
"nodateselected": "No date has been selected.",
|
||||
"owner": "Owner",
|
||||
"priorappointments": "Previous Appointments",
|
||||
"reminder": "This is {{shopname}} reminding you about an appointment on {{date}} at {{time}}. Please let us know if you are not able to make the appointment. We look forward to seeing you soon. ",
|
||||
"ro_number": "RO #",
|
||||
"scheduled_completion": "Scheduled Completion",
|
||||
"scheduledfor": "Scheduled appointment for: ",
|
||||
"severalerrorsfound": "Several Jobs have issues which may prevent accurate smart scheduling. Click to expand.",
|
||||
"smartscheduling": "Smart Scheduling",
|
||||
"smspaymentreminder": "This is {{shopname}} reminding you about your remaining balance of {{amount}}. To pay for the said balance click the link {{payment_link}}.",
|
||||
"suggesteddates": "Suggested Dates",
|
||||
"ro_number": "RO #",
|
||||
"owner": "Owner",
|
||||
"vehicle": "Vehicle",
|
||||
"bp": "B/P",
|
||||
"scheduled_completion": "Scheduled Completion"
|
||||
"vehicle": "Vehicle"
|
||||
},
|
||||
"successes": {
|
||||
"canceled": "Appointment canceled successfully.",
|
||||
@@ -90,6 +90,11 @@
|
||||
"actions": "Actions"
|
||||
}
|
||||
},
|
||||
"audio": {
|
||||
"manager": {
|
||||
"description": "Click anywhere to enable the message ding."
|
||||
}
|
||||
},
|
||||
"audit": {
|
||||
"fields": {
|
||||
"cc": "CC",
|
||||
@@ -149,11 +154,6 @@
|
||||
"tasks_updated": "Task '{{title}}' updated by {{updatedBy}}"
|
||||
}
|
||||
},
|
||||
"audio": {
|
||||
"manager": {
|
||||
"description": "Click anywhere to enable the message ding."
|
||||
}
|
||||
},
|
||||
"billlines": {
|
||||
"actions": {
|
||||
"newline": "New Line"
|
||||
@@ -281,9 +281,9 @@
|
||||
},
|
||||
"errors": {
|
||||
"creatingdefaultview": "Error creating default view.",
|
||||
"duplicate_insurance_company": "Duplicate insurance company name. Each insurance company name must be unique",
|
||||
"loading": "Unable to load shop details. Please call technical support.",
|
||||
"saving": "Error encountered while saving. {{message}}",
|
||||
"duplicate_insurance_company": "Duplicate insurance company name. Each insurance company name must be unique"
|
||||
"saving": "Error encountered while saving. {{message}}"
|
||||
},
|
||||
"fields": {
|
||||
"ReceivableCustomField": "QBO Receivable Custom Field {{number}}",
|
||||
@@ -564,21 +564,18 @@
|
||||
"responsibilitycenter_tax_tier": "Tax {{typeNum}} Tier {{typeNumIterator}}",
|
||||
"responsibilitycenter_tax_type": "Tax {{typeNum}} Type",
|
||||
"responsibilitycenters": {
|
||||
"gogcode": "GOG Code (BreakOut)",
|
||||
"item_type": "Item Type",
|
||||
"item_type_gog": "GOG",
|
||||
"item_type_paint": "Paint Materials",
|
||||
"item_type_freight": "Freight",
|
||||
"taxable_flag": "Taxable?",
|
||||
"taxable": "Taxable",
|
||||
"nontaxable": "Non-taxable",
|
||||
"ap": "Accounts Payable",
|
||||
"ar": "Accounts Receivable",
|
||||
"ats": "ATS",
|
||||
"federal_tax": "Federal Tax",
|
||||
"federal_tax_itc": "Federal Tax Credit",
|
||||
"gogcode": "GOG Code (BreakOut)",
|
||||
"gst_override": "GST Override Account #",
|
||||
"invoiceexemptcode": "QuickBooks US - Invoice Tax Exempt Code",
|
||||
"item_type": "Item Type",
|
||||
"item_type_freight": "Freight",
|
||||
"item_type_gog": "GOG",
|
||||
"item_type_paint": "Paint Materials",
|
||||
"itemexemptcode": "QuickBooks US - Line Item Tax Exempt Code",
|
||||
"la1": "LA1",
|
||||
"la2": "LA2",
|
||||
@@ -597,6 +594,7 @@
|
||||
"local_tax": "Local Tax",
|
||||
"mapa": "Paint Materials",
|
||||
"mash": "Shop Materials",
|
||||
"nontaxable": "Non-taxable",
|
||||
"paa": "Aftermarket",
|
||||
"pac": "Chrome",
|
||||
"pag": "Glass",
|
||||
@@ -617,6 +615,8 @@
|
||||
"state": "State Tax Applies"
|
||||
},
|
||||
"state_tax": "State Tax",
|
||||
"taxable": "Taxable",
|
||||
"taxable_flag": "Taxable?",
|
||||
"tow": "Towing"
|
||||
},
|
||||
"schedule_end_time": "Schedule Ending Time",
|
||||
@@ -678,8 +678,6 @@
|
||||
"zip_post": "Zip/Postal Code"
|
||||
},
|
||||
"labels": {
|
||||
"parts_shop_management": "Shop Management",
|
||||
"parts_vendor_management": "Vendor Management",
|
||||
"2tiername": "Name => RO",
|
||||
"2tiersetup": "2 Tier Setup",
|
||||
"2tiersource": "Source => RO",
|
||||
@@ -702,11 +700,11 @@
|
||||
"payers": "Payers"
|
||||
},
|
||||
"cdk_dealerid": "CDK Dealer ID",
|
||||
"rr_dealerid": "Reynolds Store Number",
|
||||
"costsmapping": "Costs Mapping",
|
||||
"dms_allocations": "DMS Allocations",
|
||||
"pbs_serialnumber": "PBS Serial Number",
|
||||
"profitsmapping": "Profits Mapping",
|
||||
"rr_dealerid": "Reynolds Store Number",
|
||||
"title": "DMS"
|
||||
},
|
||||
"emaillater": "Email Later",
|
||||
@@ -733,6 +731,8 @@
|
||||
"followers": "Notifications"
|
||||
},
|
||||
"orderstatuses": "Order Statuses",
|
||||
"parts_shop_management": "Shop Management",
|
||||
"parts_vendor_management": "Vendor Management",
|
||||
"partslocations": "Parts Locations",
|
||||
"partsscan": "Parts Scanning",
|
||||
"printlater": "Print Later",
|
||||
@@ -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."
|
||||
@@ -1228,8 +1230,6 @@
|
||||
},
|
||||
"general": {
|
||||
"actions": {
|
||||
"select": "Select",
|
||||
"optional": "Optional",
|
||||
"add": "Add",
|
||||
"autoupdate": "{{app}} will automatically update in {{time}} seconds. Please save all changes.",
|
||||
"calculate": "Calculate",
|
||||
@@ -1246,9 +1246,11 @@
|
||||
"deselectall": "Deselect All",
|
||||
"download": "Download",
|
||||
"edit": "Edit",
|
||||
"gotoadmin": "Go to Admin Panel",
|
||||
"login": "Login",
|
||||
"next": "Next",
|
||||
"ok": "Ok",
|
||||
"optional": "Optional",
|
||||
"previous": "Previous",
|
||||
"print": "Print",
|
||||
"refresh": "Refresh",
|
||||
@@ -1259,6 +1261,7 @@
|
||||
"save": "Save",
|
||||
"saveandnew": "Save and New",
|
||||
"saveas": "Save As",
|
||||
"select": "Select",
|
||||
"selectall": "Select All",
|
||||
"send": "Send",
|
||||
"sendbysms": "Send by SMS",
|
||||
@@ -1288,8 +1291,7 @@
|
||||
"vehicle": "Vehicle"
|
||||
},
|
||||
"labels": {
|
||||
"selected": "Selected",
|
||||
"settings": "Settings",
|
||||
"apply": "Apply",
|
||||
"actions": "Actions",
|
||||
"areyousure": "Are you sure?",
|
||||
"barcode": "Barcode",
|
||||
@@ -1343,8 +1345,10 @@
|
||||
"search": "Search...",
|
||||
"searchresults": "Results for {{search}}",
|
||||
"selectdate": "Select date...",
|
||||
"selected": "Selected",
|
||||
"sendagain": "Send Again",
|
||||
"sendby": "Send By",
|
||||
"settings": "Settings",
|
||||
"signin": "Sign In",
|
||||
"sms": "SMS",
|
||||
"status": "Status",
|
||||
@@ -1587,13 +1591,13 @@
|
||||
"labels": {
|
||||
"adjustmenttobeadded": "Adjustment to be added: {{adjustment}}",
|
||||
"billref": "Latest Bill",
|
||||
"bulk_location_help": "This will set the same location on all selected lines.",
|
||||
"convertedtolabor": "This line has been converted to labor. Ensure you adjust the profit center for the amount accordingly.",
|
||||
"edit": "Edit Line",
|
||||
"ioucreated": "IOU",
|
||||
"new": "New Line",
|
||||
"nostatus": "No Status",
|
||||
"presets": "Jobline Presets",
|
||||
"bulk_location_help": "This will set the same location on all selected lines."
|
||||
"presets": "Jobline Presets"
|
||||
},
|
||||
"successes": {
|
||||
"created": "Job line created successfully.",
|
||||
@@ -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"
|
||||
},
|
||||
@@ -1701,9 +1708,9 @@
|
||||
"actual_delivery": "Actual Delivery",
|
||||
"actual_in": "Actual In",
|
||||
"acv_amount": "ACV Amount",
|
||||
"admin_clerk": "Admin Clerk",
|
||||
"adjustment_bottom_line": "Adjustments",
|
||||
"adjustmenthours": "Adjustment Hours",
|
||||
"admin_clerk": "Admin Clerk",
|
||||
"alt_transport": "Alt. Trans.",
|
||||
"area_of_damage_impact": {
|
||||
"10": "Left Front Side",
|
||||
@@ -1784,9 +1791,8 @@
|
||||
"ded_status": "Deductible Status",
|
||||
"depreciation_taxes": "Betterment/Depreciation/Taxes",
|
||||
"dms": {
|
||||
"first_name": "First Name",
|
||||
"last_name": "Last Name",
|
||||
"address": "Customer Address",
|
||||
"advisor": "Advisor #",
|
||||
"amount": "Amount",
|
||||
"center": "Center",
|
||||
"control_type": {
|
||||
@@ -1794,17 +1800,19 @@
|
||||
},
|
||||
"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",
|
||||
"dms_unsold": "New, Unsold Vehicle",
|
||||
"dms_wip_acctnumber": "Cost WIP DMS Acct #",
|
||||
"first_name": "First Name",
|
||||
"id": "DMS ID",
|
||||
"inservicedate": "In Service Date",
|
||||
"journal": "Journal #",
|
||||
"make_override": "Make Override",
|
||||
"advisor": "Advisor #",
|
||||
"last_name": "Last Name",
|
||||
"lines": "Posting Lines",
|
||||
"make_override": "Make Override",
|
||||
"name1": "Customer Name",
|
||||
"payer": {
|
||||
"amount": "Amount",
|
||||
@@ -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",
|
||||
@@ -1945,7 +1957,7 @@
|
||||
"amount": "Amount",
|
||||
"name": "Name"
|
||||
},
|
||||
"queued_for_parts": "Queued for Parts",
|
||||
"queued_for_parts": "Queued",
|
||||
"rate_ats": "ATS Rate",
|
||||
"rate_ats_flat": "ATS Flat Rate",
|
||||
"rate_la1": "LA1",
|
||||
@@ -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.",
|
||||
@@ -2447,6 +2465,7 @@
|
||||
"labels": {
|
||||
"addlabel": "Add a label to this conversation.",
|
||||
"archive": "Archive",
|
||||
"mark_unread": "Mark as unread",
|
||||
"maxtenimages": "You can only select up to a maximum of 10 images at a time.",
|
||||
"messaging": "Messaging",
|
||||
"no_consent": "Opted-out",
|
||||
@@ -2459,8 +2478,7 @@
|
||||
"selectmedia": "Select Media",
|
||||
"sentby": "Sent by {{by}} at {{time}}",
|
||||
"typeamessage": "Send a message...",
|
||||
"unarchive": "Unarchive",
|
||||
"mark_unread": "Mark as unread"
|
||||
"unarchive": "Unarchive"
|
||||
},
|
||||
"render": {
|
||||
"conversation_list": "Conversation List"
|
||||
@@ -2614,20 +2632,20 @@
|
||||
"name": "Owner Details"
|
||||
},
|
||||
"labels": {
|
||||
"cell": "Cell",
|
||||
"create_new": "Create a new owner record.",
|
||||
"deleteconfirm": "Are you sure you want to delete this owner? This cannot be undone.",
|
||||
"email": "Email",
|
||||
"existing_owners": "Existing Owners",
|
||||
"fromclaim": "Current Claim",
|
||||
"fromowner": "Historical Owner Record",
|
||||
"relatedjobs": "Related Jobs",
|
||||
"updateowner": "Update Owner",
|
||||
"work": "Work",
|
||||
"home": "Home",
|
||||
"cell": "Cell",
|
||||
"other": "Other",
|
||||
"email": "Email",
|
||||
"phone": "Phone",
|
||||
"sms": "SMS"
|
||||
"relatedjobs": "Related Jobs",
|
||||
"sms": "SMS",
|
||||
"updateowner": "Update Owner",
|
||||
"work": "Work"
|
||||
},
|
||||
"successes": {
|
||||
"delete": "Owner deleted successfully.",
|
||||
@@ -2638,6 +2656,10 @@
|
||||
"actions": {
|
||||
"order": "Order Parts",
|
||||
"orderinhouse": "Order as In House"
|
||||
},
|
||||
"labels": {
|
||||
"view_counts_only": "View Parts Counts Only",
|
||||
"view_timestamps": "Show timestamps"
|
||||
}
|
||||
},
|
||||
"parts_dispatch": {
|
||||
@@ -2987,8 +3009,6 @@
|
||||
"settings": "Error saving board settings: {{error}}"
|
||||
},
|
||||
"labels": {
|
||||
"click_for_statuses": "Click to view parts statuses",
|
||||
"partsreceived": "Parts Received",
|
||||
"actual_in": "Actual In",
|
||||
"addnewprofile": "Add New Profile",
|
||||
"alert": "Alert",
|
||||
@@ -3007,6 +3027,7 @@
|
||||
"card_size": "Card Size",
|
||||
"cardcolor": "Colored Cards",
|
||||
"cardsettings": "Card Settings",
|
||||
"click_for_statuses": "Click to view parts statuses",
|
||||
"clm_no": "Claim Number",
|
||||
"comment": "Comment",
|
||||
"compact": "Compact Cards",
|
||||
@@ -3027,6 +3048,7 @@
|
||||
"orientation": "Board Orientation",
|
||||
"ownr_nm": "Customer Name",
|
||||
"paintpriority": "P/P",
|
||||
"partsreceived": "Parts Received",
|
||||
"partsstatus": "Parts Status",
|
||||
"production_note": "Production Note",
|
||||
"refinishhours": "R",
|
||||
@@ -3573,17 +3595,12 @@
|
||||
}
|
||||
},
|
||||
"titles": {
|
||||
"parts_settings": "Parts Management Settings | {{app}}",
|
||||
"simplified-parts-jobs": "Parts Management | {{app}}",
|
||||
"accounting-payables": "Payables | {{app}}",
|
||||
"accounting-payments": "Payments | {{app}}",
|
||||
"accounting-receivables": "Receivables | {{app}}",
|
||||
"all_tasks": "All Tasks | {{app}}",
|
||||
"app": "",
|
||||
"bc": {
|
||||
"simplified-parts-jobs": "Jobs",
|
||||
"parts": "Parts",
|
||||
"parts_settings": "Settings",
|
||||
"accounting-payables": "Payables",
|
||||
"accounting-payments": "Payments",
|
||||
"accounting-receivables": "Receivables",
|
||||
@@ -3615,7 +3632,9 @@
|
||||
"my_tasks": "My Tasks",
|
||||
"owner-detail": "{{name}}",
|
||||
"owners": "Owners",
|
||||
"parts": "Parts",
|
||||
"parts-queue": "Parts Queue",
|
||||
"parts_settings": "Settings",
|
||||
"payments-all": "All Payments",
|
||||
"phonebook": "Phonebook",
|
||||
"productionboard": "Production Board - Visual",
|
||||
@@ -3627,6 +3646,7 @@
|
||||
"shop-csi": "CSI Responses",
|
||||
"shop-templates": "Shop Templates",
|
||||
"shop-vendors": "Vendors",
|
||||
"simplified-parts-jobs": "Jobs",
|
||||
"tasks": "Tasks",
|
||||
"temporarydocs": "Temporary Documents",
|
||||
"timetickets": "Time Tickets",
|
||||
@@ -3662,7 +3682,9 @@
|
||||
"my_tasks": "My Tasks | {{app}}",
|
||||
"owners": "All Owners | {{app}}",
|
||||
"owners-detail": "{{name}} | {{app}}",
|
||||
"parts": "",
|
||||
"parts-queue": "Parts Queue | {{app}}",
|
||||
"parts_settings": "Parts Management Settings | {{app}}",
|
||||
"payments-all": "Payments | {{app}}",
|
||||
"phonebook": "Phonebook | {{app}}",
|
||||
"productionboard": "Production Board - Visual | {{app}}",
|
||||
@@ -3678,6 +3700,7 @@
|
||||
"shop-csi": "CSI Responses | {{app}}",
|
||||
"shop-templates": "Shop Templates | {{app}}",
|
||||
"shop_vendors": "Vendors | {{app}}",
|
||||
"simplified-parts-jobs": "Parts Management | {{app}}",
|
||||
"tasks": "Tasks",
|
||||
"techconsole": "Technician Console | {{app}}",
|
||||
"techjobclock": "Technician Job Clock | {{app}}",
|
||||
@@ -3838,10 +3861,10 @@
|
||||
"user": {
|
||||
"actions": {
|
||||
"changepassword": "Change Password",
|
||||
"signout": "Sign Out",
|
||||
"updateprofile": "Update Profile",
|
||||
"dark_theme": "Switch to Dark Theme",
|
||||
"light_theme": "Switch to Light Theme",
|
||||
"dark_theme": "Switch to Dark Theme"
|
||||
"signout": "Sign Out",
|
||||
"updateprofile": "Update Profile"
|
||||
},
|
||||
"errors": {
|
||||
"updating": "Error updating user or association {{message}}"
|
||||
@@ -3855,14 +3878,14 @@
|
||||
"labels": {
|
||||
"actions": "Actions",
|
||||
"changepassword": "Change Password",
|
||||
"profileinfo": "Profile Info",
|
||||
"user_settings": "User Settings",
|
||||
"play_sound_for_new_messages": "Play a sound for new messages",
|
||||
"notification_sound_on": "Sound is ON",
|
||||
"notification_sound_off": "Sound is OFF",
|
||||
"notification_sound_enabled": "Notification sound enabled",
|
||||
"notification_sound_disabled": "Notification sound disabled",
|
||||
"notification_sound_help": "Toggle the ding for incoming chat messages."
|
||||
"notification_sound_enabled": "Notification sound enabled",
|
||||
"notification_sound_help": "Toggle the ding for incoming chat messages.",
|
||||
"notification_sound_off": "Sound is OFF",
|
||||
"notification_sound_on": "Sound is ON",
|
||||
"play_sound_for_new_messages": "Play a sound for new messages",
|
||||
"profileinfo": "Profile Info",
|
||||
"user_settings": "User Settings"
|
||||
},
|
||||
"successess": {
|
||||
"passwordchanged": "Password changed successfully. "
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
"arrivedon": "Llegado el:",
|
||||
"arrivingjobs": "",
|
||||
"blocked": "",
|
||||
"bp": "",
|
||||
"cancelledappointment": "Cita cancelada para:",
|
||||
"completingjobs": "",
|
||||
"dataconsistency": "",
|
||||
@@ -59,18 +60,17 @@
|
||||
"noarrivingjobs": "",
|
||||
"nocompletingjobs": "",
|
||||
"nodateselected": "No se ha seleccionado ninguna fecha.",
|
||||
"owner": "",
|
||||
"priorappointments": "Nombramientos previos",
|
||||
"reminder": "",
|
||||
"ro_number": "",
|
||||
"scheduled_completion": "",
|
||||
"scheduledfor": "Cita programada para:",
|
||||
"severalerrorsfound": "",
|
||||
"smartscheduling": "",
|
||||
"smspaymentreminder": "",
|
||||
"suggesteddates": "",
|
||||
"ro_number": "",
|
||||
"owner": "",
|
||||
"vehicle": "",
|
||||
"bp": "",
|
||||
"scheduled_completion": ""
|
||||
"vehicle": ""
|
||||
},
|
||||
"successes": {
|
||||
"canceled": "Cita cancelada con éxito.",
|
||||
@@ -90,6 +90,11 @@
|
||||
"actions": "Comportamiento"
|
||||
}
|
||||
},
|
||||
"audio": {
|
||||
"manager": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"audit": {
|
||||
"fields": {
|
||||
"cc": "",
|
||||
@@ -149,11 +154,6 @@
|
||||
"tasks_updated": ""
|
||||
}
|
||||
},
|
||||
"audio": {
|
||||
"manager": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"billlines": {
|
||||
"actions": {
|
||||
"newline": ""
|
||||
@@ -281,9 +281,9 @@
|
||||
},
|
||||
"errors": {
|
||||
"creatingdefaultview": "",
|
||||
"duplicate_insurance_company": "",
|
||||
"loading": "No se pueden cargar los detalles de la tienda. Por favor llame al soporte técnico.",
|
||||
"saving": "",
|
||||
"duplicate_insurance_company": ""
|
||||
"saving": ""
|
||||
},
|
||||
"fields": {
|
||||
"ReceivableCustomField": "",
|
||||
@@ -564,21 +564,18 @@
|
||||
"responsibilitycenter_tax_tier": "",
|
||||
"responsibilitycenter_tax_type": "",
|
||||
"responsibilitycenters": {
|
||||
"gogcode": "",
|
||||
"item_type": "Item Type",
|
||||
"item_type_gog": "",
|
||||
"item_type_paint": "",
|
||||
"item_type_freight": "",
|
||||
"taxable_flag": "",
|
||||
"taxable": "",
|
||||
"nontaxable": "",
|
||||
"ap": "",
|
||||
"ar": "",
|
||||
"ats": "",
|
||||
"federal_tax": "",
|
||||
"federal_tax_itc": "",
|
||||
"gogcode": "",
|
||||
"gst_override": "",
|
||||
"invoiceexemptcode": "",
|
||||
"item_type": "Item Type",
|
||||
"item_type_freight": "",
|
||||
"item_type_gog": "",
|
||||
"item_type_paint": "",
|
||||
"itemexemptcode": "",
|
||||
"la1": "",
|
||||
"la2": "",
|
||||
@@ -597,6 +594,7 @@
|
||||
"local_tax": "",
|
||||
"mapa": "",
|
||||
"mash": "",
|
||||
"nontaxable": "",
|
||||
"paa": "",
|
||||
"pac": "",
|
||||
"pag": "",
|
||||
@@ -617,6 +615,8 @@
|
||||
"state": ""
|
||||
},
|
||||
"state_tax": "",
|
||||
"taxable": "",
|
||||
"taxable_flag": "",
|
||||
"tow": ""
|
||||
},
|
||||
"schedule_end_time": "",
|
||||
@@ -678,8 +678,6 @@
|
||||
"zip_post": ""
|
||||
},
|
||||
"labels": {
|
||||
"parts_shop_management": "",
|
||||
"parts_vendor_management": "",
|
||||
"2tiername": "",
|
||||
"2tiersetup": "",
|
||||
"2tiersource": "",
|
||||
@@ -702,11 +700,11 @@
|
||||
"payers": ""
|
||||
},
|
||||
"cdk_dealerid": "",
|
||||
"rr_dealerid": "",
|
||||
"costsmapping": "",
|
||||
"dms_allocations": "",
|
||||
"pbs_serialnumber": "",
|
||||
"profitsmapping": "",
|
||||
"rr_dealerid": "",
|
||||
"title": ""
|
||||
},
|
||||
"emaillater": "",
|
||||
@@ -733,6 +731,8 @@
|
||||
"followers": ""
|
||||
},
|
||||
"orderstatuses": "",
|
||||
"parts_shop_management": "",
|
||||
"parts_vendor_management": "",
|
||||
"partslocations": "",
|
||||
"partsscan": "",
|
||||
"printlater": "",
|
||||
@@ -1047,7 +1047,9 @@
|
||||
},
|
||||
"dms": {
|
||||
"errors": {
|
||||
"alreadyexported": ""
|
||||
"alreadyexported": "",
|
||||
"earlyrorequired": "",
|
||||
"earlyrorequired.message": ""
|
||||
},
|
||||
"labels": {
|
||||
"refreshallocations": ""
|
||||
@@ -1244,9 +1246,11 @@
|
||||
"deselectall": "",
|
||||
"download": "",
|
||||
"edit": "Editar",
|
||||
"gotoadmin": "",
|
||||
"login": "",
|
||||
"next": "",
|
||||
"ok": "",
|
||||
"optional": "",
|
||||
"previous": "",
|
||||
"print": "",
|
||||
"refresh": "",
|
||||
@@ -1257,6 +1261,7 @@
|
||||
"save": "Salvar",
|
||||
"saveandnew": "",
|
||||
"saveas": "",
|
||||
"select": "",
|
||||
"selectall": "",
|
||||
"send": "",
|
||||
"sendbysms": "",
|
||||
@@ -1286,9 +1291,8 @@
|
||||
"vehicle": ""
|
||||
},
|
||||
"labels": {
|
||||
"selected": "",
|
||||
"apply": "",
|
||||
"actions": "Comportamiento",
|
||||
"settings": "",
|
||||
"areyousure": "",
|
||||
"barcode": "código de barras",
|
||||
"cancel": "",
|
||||
@@ -1341,8 +1345,10 @@
|
||||
"search": "Buscar...",
|
||||
"searchresults": "",
|
||||
"selectdate": "",
|
||||
"selected": "",
|
||||
"sendagain": "",
|
||||
"sendby": "",
|
||||
"settings": "",
|
||||
"signin": "",
|
||||
"sms": "",
|
||||
"status": "",
|
||||
@@ -1585,13 +1591,13 @@
|
||||
"labels": {
|
||||
"adjustmenttobeadded": "",
|
||||
"billref": "",
|
||||
"bulk_location_help": "",
|
||||
"convertedtolabor": "",
|
||||
"edit": "Línea de edición",
|
||||
"ioucreated": "",
|
||||
"new": "Nueva línea",
|
||||
"nostatus": "",
|
||||
"presets": "",
|
||||
"bulk_location_help": ""
|
||||
"presets": ""
|
||||
},
|
||||
"successes": {
|
||||
"created": "",
|
||||
@@ -1619,11 +1625,13 @@
|
||||
"changestatus": "Cambiar Estado",
|
||||
"changestimator": "",
|
||||
"convert": "Convertir",
|
||||
"convertwithoutearlyro": "",
|
||||
"createiou": "",
|
||||
"deliver": "",
|
||||
"deliver_quick": "",
|
||||
"dms": {
|
||||
"addpayer": "",
|
||||
"createearlyro": "",
|
||||
"createnewcustomer": "",
|
||||
"findmakemodelcode": "",
|
||||
"getmakes": "",
|
||||
@@ -1632,6 +1640,7 @@
|
||||
},
|
||||
"post": "",
|
||||
"refetchmakesmodels": "",
|
||||
"update_ro": "",
|
||||
"usegeneric": "",
|
||||
"useselected": ""
|
||||
},
|
||||
@@ -1700,8 +1709,8 @@
|
||||
"actual_in": "Real en",
|
||||
"acv_amount": "",
|
||||
"adjustment_bottom_line": "Ajustes",
|
||||
"admin_clerk": "",
|
||||
"adjustmenthours": "",
|
||||
"admin_clerk": "",
|
||||
"alt_transport": "",
|
||||
"area_of_damage_impact": {
|
||||
"10": "",
|
||||
@@ -1782,9 +1791,8 @@
|
||||
"ded_status": "Estado deducible",
|
||||
"depreciation_taxes": "Depreciación / Impuestos",
|
||||
"dms": {
|
||||
"first_name": "",
|
||||
"last_name": "",
|
||||
"address": "",
|
||||
"advisor": "",
|
||||
"amount": "",
|
||||
"center": "",
|
||||
"control_type": {
|
||||
@@ -1792,29 +1800,36 @@
|
||||
},
|
||||
"cost": "",
|
||||
"cost_dms_acctnumber": "",
|
||||
"customer": "",
|
||||
"dms_make": "",
|
||||
"dms_model": "",
|
||||
"dms_model_override": "",
|
||||
"make_override": "",
|
||||
"advisor": "",
|
||||
"dms_unsold": "",
|
||||
"dms_wip_acctnumber": "",
|
||||
"first_name": "",
|
||||
"id": "",
|
||||
"inservicedate": "",
|
||||
"journal": "",
|
||||
"last_name": "",
|
||||
"lines": "",
|
||||
"make_override": "",
|
||||
"name1": "",
|
||||
"payer": {
|
||||
"amount": "",
|
||||
"control_type": "",
|
||||
"controlnumber": "",
|
||||
"dms_acctnumber": "",
|
||||
"name": ""
|
||||
"name": "",
|
||||
"payer_type": ""
|
||||
},
|
||||
"sale": "",
|
||||
"sale_dms_acctnumber": "",
|
||||
"story": "",
|
||||
"vinowner": ""
|
||||
"vinowner": "",
|
||||
"rr_opcode": "",
|
||||
"rr_opcode_prefix": "",
|
||||
"rr_opcode_suffix": "",
|
||||
"rr_opcode_base": ""
|
||||
},
|
||||
"dms_allocation": "",
|
||||
"driveable": "",
|
||||
@@ -2099,6 +2114,11 @@
|
||||
"damageto": "",
|
||||
"defaultstory": "",
|
||||
"disablebillwip": "",
|
||||
"earlyro": {
|
||||
"created": "",
|
||||
"fields": "",
|
||||
"willupdate": ""
|
||||
},
|
||||
"invoicedatefuture": "",
|
||||
"kmoutnotgreaterthankmin": "",
|
||||
"logs": "",
|
||||
@@ -2256,6 +2276,7 @@
|
||||
"delete": "",
|
||||
"deleted": "Trabajo eliminado con éxito.",
|
||||
"duplicated": "",
|
||||
"early_ro_created": "",
|
||||
"exported": "",
|
||||
"invoiced": "",
|
||||
"ioucreated": "",
|
||||
@@ -2444,6 +2465,7 @@
|
||||
"labels": {
|
||||
"addlabel": "",
|
||||
"archive": "",
|
||||
"mark_unread": "",
|
||||
"maxtenimages": "",
|
||||
"messaging": "Mensajería",
|
||||
"no_consent": "",
|
||||
@@ -2456,8 +2478,7 @@
|
||||
"selectmedia": "",
|
||||
"sentby": "",
|
||||
"typeamessage": "Enviar un mensaje...",
|
||||
"unarchive": "",
|
||||
"mark_unread": ""
|
||||
"unarchive": ""
|
||||
},
|
||||
"render": {
|
||||
"conversation_list": ""
|
||||
@@ -2611,20 +2632,20 @@
|
||||
"name": ""
|
||||
},
|
||||
"labels": {
|
||||
"cell": "",
|
||||
"create_new": "Crea un nuevo registro de propietario.",
|
||||
"deleteconfirm": "",
|
||||
"email": "",
|
||||
"existing_owners": "Propietarios existentes",
|
||||
"fromclaim": "",
|
||||
"fromowner": "",
|
||||
"relatedjobs": "",
|
||||
"updateowner": "",
|
||||
"work": "",
|
||||
"home": "",
|
||||
"cell": "",
|
||||
"other": "",
|
||||
"email": "",
|
||||
"phone": "",
|
||||
"sms": ""
|
||||
"relatedjobs": "",
|
||||
"sms": "",
|
||||
"updateowner": "",
|
||||
"work": ""
|
||||
},
|
||||
"successes": {
|
||||
"delete": "",
|
||||
@@ -2635,6 +2656,10 @@
|
||||
"actions": {
|
||||
"order": "Pedido de piezas",
|
||||
"orderinhouse": ""
|
||||
},
|
||||
"labels": {
|
||||
"view_counts_only": "",
|
||||
"view_timestamps": ""
|
||||
}
|
||||
},
|
||||
"parts_dispatch": {
|
||||
@@ -2984,8 +3009,6 @@
|
||||
"settings": ""
|
||||
},
|
||||
"labels": {
|
||||
"click_for_statuses": "",
|
||||
"partsreceived": "",
|
||||
"actual_in": "",
|
||||
"addnewprofile": "",
|
||||
"alert": "",
|
||||
@@ -3004,6 +3027,7 @@
|
||||
"card_size": "",
|
||||
"cardcolor": "",
|
||||
"cardsettings": "",
|
||||
"click_for_statuses": "",
|
||||
"clm_no": "",
|
||||
"comment": "",
|
||||
"compact": "",
|
||||
@@ -3024,6 +3048,7 @@
|
||||
"orientation": "",
|
||||
"ownr_nm": "",
|
||||
"paintpriority": "",
|
||||
"partsreceived": "",
|
||||
"partsstatus": "",
|
||||
"production_note": "",
|
||||
"refinishhours": "",
|
||||
@@ -3570,18 +3595,12 @@
|
||||
}
|
||||
},
|
||||
"titles": {
|
||||
"simplified-parts-jobs": "",
|
||||
"parts": "",
|
||||
"parts_settings": "",
|
||||
"accounting-payables": "",
|
||||
"accounting-payments": "",
|
||||
"accounting-receivables": "",
|
||||
"all_tasks": "",
|
||||
"app": "",
|
||||
"bc": {
|
||||
"simplified-parts-jobs": "",
|
||||
"parts": "",
|
||||
"parts_settings": "",
|
||||
"accounting-payables": "",
|
||||
"accounting-payments": "",
|
||||
"accounting-receivables": "",
|
||||
@@ -3613,7 +3632,9 @@
|
||||
"my_tasks": "",
|
||||
"owner-detail": "",
|
||||
"owners": "",
|
||||
"parts": "",
|
||||
"parts-queue": "",
|
||||
"parts_settings": "",
|
||||
"payments-all": "",
|
||||
"phonebook": "",
|
||||
"productionboard": "",
|
||||
@@ -3625,6 +3646,7 @@
|
||||
"shop-csi": "",
|
||||
"shop-templates": "",
|
||||
"shop-vendors": "",
|
||||
"simplified-parts-jobs": "",
|
||||
"tasks": "",
|
||||
"temporarydocs": "",
|
||||
"timetickets": "",
|
||||
@@ -3660,7 +3682,9 @@
|
||||
"my_tasks": "",
|
||||
"owners": "Todos los propietarios | {{app}}",
|
||||
"owners-detail": "",
|
||||
"parts": "",
|
||||
"parts-queue": "",
|
||||
"parts_settings": "",
|
||||
"payments-all": "",
|
||||
"phonebook": "",
|
||||
"productionboard": "",
|
||||
@@ -3676,6 +3700,7 @@
|
||||
"shop-csi": "",
|
||||
"shop-templates": "",
|
||||
"shop_vendors": "Vendedores | {{app}}",
|
||||
"simplified-parts-jobs": "",
|
||||
"tasks": "",
|
||||
"techconsole": "{{app}}",
|
||||
"techjobclock": "{{app}}",
|
||||
@@ -3836,10 +3861,10 @@
|
||||
"user": {
|
||||
"actions": {
|
||||
"changepassword": "",
|
||||
"signout": "desconectar",
|
||||
"updateprofile": "Actualización del perfil",
|
||||
"dark_theme": "",
|
||||
"light_theme": "",
|
||||
"dark_theme": ""
|
||||
"signout": "desconectar",
|
||||
"updateprofile": "Actualización del perfil"
|
||||
},
|
||||
"errors": {
|
||||
"updating": ""
|
||||
@@ -3853,14 +3878,14 @@
|
||||
"labels": {
|
||||
"actions": "",
|
||||
"changepassword": "",
|
||||
"profileinfo": "",
|
||||
"user_settings": "",
|
||||
"play_sound_for_new_messages": "",
|
||||
"notification_sound_on": "",
|
||||
"notification_sound_off": "",
|
||||
"notification_sound_enabled": "",
|
||||
"notification_sound_disabled": "",
|
||||
"notification_sound_help": ""
|
||||
"notification_sound_enabled": "",
|
||||
"notification_sound_help": "",
|
||||
"notification_sound_off": "",
|
||||
"notification_sound_on": "",
|
||||
"play_sound_for_new_messages": "",
|
||||
"profileinfo": "",
|
||||
"user_settings": ""
|
||||
},
|
||||
"successess": {
|
||||
"passwordchanged": ""
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
"arrivedon": "Arrivé le:",
|
||||
"arrivingjobs": "",
|
||||
"blocked": "",
|
||||
"bp": "",
|
||||
"cancelledappointment": "Rendez-vous annulé pour:",
|
||||
"completingjobs": "",
|
||||
"dataconsistency": "",
|
||||
@@ -59,18 +60,17 @@
|
||||
"noarrivingjobs": "",
|
||||
"nocompletingjobs": "",
|
||||
"nodateselected": "Aucune date n'a été sélectionnée.",
|
||||
"owner": "",
|
||||
"priorappointments": "Rendez-vous précédents",
|
||||
"reminder": "",
|
||||
"ro_number": "",
|
||||
"scheduled_completion": "",
|
||||
"scheduledfor": "Rendez-vous prévu pour:",
|
||||
"severalerrorsfound": "",
|
||||
"smartscheduling": "",
|
||||
"smspaymentreminder": "",
|
||||
"suggesteddates": "",
|
||||
"ro_number": "",
|
||||
"owner": "",
|
||||
"vehicle": "",
|
||||
"bp": "",
|
||||
"scheduled_completion": ""
|
||||
"vehicle": ""
|
||||
},
|
||||
"successes": {
|
||||
"canceled": "Rendez-vous annulé avec succès.",
|
||||
@@ -90,6 +90,11 @@
|
||||
"actions": "actes"
|
||||
}
|
||||
},
|
||||
"audio": {
|
||||
"manager": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"audit": {
|
||||
"fields": {
|
||||
"cc": "",
|
||||
@@ -149,11 +154,6 @@
|
||||
"tasks_updated": ""
|
||||
}
|
||||
},
|
||||
"audio": {
|
||||
"manager": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"billlines": {
|
||||
"actions": {
|
||||
"newline": ""
|
||||
@@ -281,9 +281,9 @@
|
||||
},
|
||||
"errors": {
|
||||
"creatingdefaultview": "",
|
||||
"duplicate_insurance_company": "",
|
||||
"loading": "Impossible de charger les détails de la boutique. Veuillez appeler le support technique.",
|
||||
"saving": "",
|
||||
"duplicate_insurance_company": ""
|
||||
"saving": ""
|
||||
},
|
||||
"fields": {
|
||||
"ReceivableCustomField": "",
|
||||
@@ -564,21 +564,18 @@
|
||||
"responsibilitycenter_tax_tier": "",
|
||||
"responsibilitycenter_tax_type": "",
|
||||
"responsibilitycenters": {
|
||||
"gogcode": "",
|
||||
"item_type": "Item Type",
|
||||
"item_type_gog": "",
|
||||
"item_type_paint": "",
|
||||
"item_type_freight": "",
|
||||
"taxable_flag": "",
|
||||
"taxable": "",
|
||||
"nontaxable": "",
|
||||
"ap": "",
|
||||
"ar": "",
|
||||
"ats": "",
|
||||
"federal_tax": "",
|
||||
"federal_tax_itc": "",
|
||||
"gogcode": "",
|
||||
"gst_override": "",
|
||||
"invoiceexemptcode": "",
|
||||
"item_type": "Item Type",
|
||||
"item_type_freight": "",
|
||||
"item_type_gog": "",
|
||||
"item_type_paint": "",
|
||||
"itemexemptcode": "",
|
||||
"la1": "",
|
||||
"la2": "",
|
||||
@@ -597,6 +594,7 @@
|
||||
"local_tax": "",
|
||||
"mapa": "",
|
||||
"mash": "",
|
||||
"nontaxable": "",
|
||||
"paa": "",
|
||||
"pac": "",
|
||||
"pag": "",
|
||||
@@ -617,6 +615,8 @@
|
||||
"state": ""
|
||||
},
|
||||
"state_tax": "",
|
||||
"taxable": "",
|
||||
"taxable_flag": "",
|
||||
"tow": ""
|
||||
},
|
||||
"schedule_end_time": "",
|
||||
@@ -678,8 +678,6 @@
|
||||
"zip_post": ""
|
||||
},
|
||||
"labels": {
|
||||
"parts_shop_management": "",
|
||||
"parts_vendor_management": "",
|
||||
"2tiername": "",
|
||||
"2tiersetup": "",
|
||||
"2tiersource": "",
|
||||
@@ -702,11 +700,11 @@
|
||||
"payers": ""
|
||||
},
|
||||
"cdk_dealerid": "",
|
||||
"rr_dealerid": "",
|
||||
"costsmapping": "",
|
||||
"dms_allocations": "",
|
||||
"pbs_serialnumber": "",
|
||||
"profitsmapping": "",
|
||||
"rr_dealerid": "",
|
||||
"title": ""
|
||||
},
|
||||
"emaillater": "",
|
||||
@@ -733,6 +731,8 @@
|
||||
"followers": ""
|
||||
},
|
||||
"orderstatuses": "",
|
||||
"parts_shop_management": "",
|
||||
"parts_vendor_management": "",
|
||||
"partslocations": "",
|
||||
"partsscan": "",
|
||||
"printlater": "",
|
||||
@@ -1047,7 +1047,9 @@
|
||||
},
|
||||
"dms": {
|
||||
"errors": {
|
||||
"alreadyexported": ""
|
||||
"alreadyexported": "",
|
||||
"earlyrorequired": "",
|
||||
"earlyrorequired.message": ""
|
||||
},
|
||||
"labels": {
|
||||
"refreshallocations": ""
|
||||
@@ -1244,9 +1246,11 @@
|
||||
"deselectall": "",
|
||||
"download": "",
|
||||
"edit": "modifier",
|
||||
"gotoadmin": "",
|
||||
"login": "",
|
||||
"next": "",
|
||||
"ok": "",
|
||||
"optional": "",
|
||||
"previous": "",
|
||||
"print": "",
|
||||
"refresh": "",
|
||||
@@ -1257,6 +1261,7 @@
|
||||
"save": "sauvegarder",
|
||||
"saveandnew": "",
|
||||
"saveas": "",
|
||||
"select": "",
|
||||
"selectall": "",
|
||||
"send": "",
|
||||
"sendbysms": "",
|
||||
@@ -1286,8 +1291,7 @@
|
||||
"vehicle": ""
|
||||
},
|
||||
"labels": {
|
||||
"selected": "",
|
||||
"settings": "",
|
||||
"apply": "",
|
||||
"actions": "actes",
|
||||
"areyousure": "",
|
||||
"barcode": "code à barre",
|
||||
@@ -1341,8 +1345,10 @@
|
||||
"search": "Chercher...",
|
||||
"searchresults": "",
|
||||
"selectdate": "",
|
||||
"selected": "",
|
||||
"sendagain": "",
|
||||
"sendby": "",
|
||||
"settings": "",
|
||||
"signin": "",
|
||||
"sms": "",
|
||||
"status": "",
|
||||
@@ -1585,13 +1591,13 @@
|
||||
"labels": {
|
||||
"adjustmenttobeadded": "",
|
||||
"billref": "",
|
||||
"bulk_location_help": "",
|
||||
"convertedtolabor": "",
|
||||
"edit": "Ligne d'édition",
|
||||
"ioucreated": "",
|
||||
"new": "Nouvelle ligne",
|
||||
"nostatus": "",
|
||||
"presets": "",
|
||||
"bulk_location_help": ""
|
||||
"presets": ""
|
||||
},
|
||||
"successes": {
|
||||
"created": "",
|
||||
@@ -1619,11 +1625,13 @@
|
||||
"changestatus": "Changer le statut",
|
||||
"changestimator": "",
|
||||
"convert": "Convertir",
|
||||
"convertwithoutearlyro": "",
|
||||
"createiou": "",
|
||||
"deliver": "",
|
||||
"deliver_quick": "",
|
||||
"dms": {
|
||||
"addpayer": "",
|
||||
"createearlyro": "",
|
||||
"createnewcustomer": "",
|
||||
"findmakemodelcode": "",
|
||||
"getmakes": "",
|
||||
@@ -1632,6 +1640,7 @@
|
||||
},
|
||||
"post": "",
|
||||
"refetchmakesmodels": "",
|
||||
"update_ro": "",
|
||||
"usegeneric": "",
|
||||
"useselected": ""
|
||||
},
|
||||
@@ -1699,9 +1708,9 @@
|
||||
"actual_delivery": "Livraison réelle",
|
||||
"actual_in": "En réel",
|
||||
"acv_amount": "",
|
||||
"admin_clerk": "",
|
||||
"adjustment_bottom_line": "Ajustements",
|
||||
"adjustmenthours": "",
|
||||
"admin_clerk": "",
|
||||
"alt_transport": "",
|
||||
"area_of_damage_impact": {
|
||||
"10": "",
|
||||
@@ -1782,9 +1791,8 @@
|
||||
"ded_status": "Statut de franchise",
|
||||
"depreciation_taxes": "Amortissement / taxes",
|
||||
"dms": {
|
||||
"first_name": "",
|
||||
"last_name": "",
|
||||
"address": "",
|
||||
"advisor": "",
|
||||
"amount": "",
|
||||
"center": "",
|
||||
"control_type": {
|
||||
@@ -1792,29 +1800,36 @@
|
||||
},
|
||||
"cost": "",
|
||||
"cost_dms_acctnumber": "",
|
||||
"customer": "",
|
||||
"dms_make": "",
|
||||
"dms_model": "",
|
||||
"dms_model_override": "",
|
||||
"make_override": "",
|
||||
"advisor": "",
|
||||
"dms_unsold": "",
|
||||
"dms_wip_acctnumber": "",
|
||||
"first_name": "",
|
||||
"id": "",
|
||||
"inservicedate": "",
|
||||
"journal": "",
|
||||
"last_name": "",
|
||||
"lines": "",
|
||||
"make_override": "",
|
||||
"name1": "",
|
||||
"payer": {
|
||||
"amount": "",
|
||||
"control_type": "",
|
||||
"controlnumber": "",
|
||||
"dms_acctnumber": "",
|
||||
"name": ""
|
||||
"name": "",
|
||||
"payer_type": ""
|
||||
},
|
||||
"sale": "",
|
||||
"sale_dms_acctnumber": "",
|
||||
"story": "",
|
||||
"vinowner": ""
|
||||
"vinowner": "",
|
||||
"rr_opcode": "",
|
||||
"rr_opcode_prefix": "",
|
||||
"rr_opcode_suffix": "",
|
||||
"rr_opcode_base": ""
|
||||
},
|
||||
"dms_allocation": "",
|
||||
"driveable": "",
|
||||
@@ -2099,6 +2114,11 @@
|
||||
"damageto": "",
|
||||
"defaultstory": "",
|
||||
"disablebillwip": "",
|
||||
"earlyro": {
|
||||
"created": "",
|
||||
"fields": "",
|
||||
"willupdate": ""
|
||||
},
|
||||
"invoicedatefuture": "",
|
||||
"kmoutnotgreaterthankmin": "",
|
||||
"logs": "",
|
||||
@@ -2256,6 +2276,7 @@
|
||||
"delete": "",
|
||||
"deleted": "Le travail a bien été supprimé.",
|
||||
"duplicated": "",
|
||||
"early_ro_created": "",
|
||||
"exported": "",
|
||||
"invoiced": "",
|
||||
"ioucreated": "",
|
||||
@@ -2433,7 +2454,6 @@
|
||||
"actions": {
|
||||
"link": "",
|
||||
"new": "",
|
||||
|
||||
"openchat": ""
|
||||
},
|
||||
"errors": {
|
||||
@@ -2445,6 +2465,7 @@
|
||||
"labels": {
|
||||
"addlabel": "",
|
||||
"archive": "",
|
||||
"mark_unread": "",
|
||||
"maxtenimages": "",
|
||||
"messaging": "Messagerie",
|
||||
"no_consent": "",
|
||||
@@ -2457,8 +2478,7 @@
|
||||
"selectmedia": "",
|
||||
"sentby": "",
|
||||
"typeamessage": "Envoyer un message...",
|
||||
"unarchive": "",
|
||||
"mark_unread": ""
|
||||
"unarchive": ""
|
||||
},
|
||||
"render": {
|
||||
"conversation_list": ""
|
||||
@@ -2612,20 +2632,20 @@
|
||||
"name": ""
|
||||
},
|
||||
"labels": {
|
||||
"cell": "",
|
||||
"create_new": "Créez un nouvel enregistrement de propriétaire.",
|
||||
"deleteconfirm": "",
|
||||
"email": "",
|
||||
"existing_owners": "Propriétaires existants",
|
||||
"fromclaim": "",
|
||||
"fromowner": "",
|
||||
"relatedjobs": "",
|
||||
"updateowner": "",
|
||||
"work": "",
|
||||
"home": "",
|
||||
"cell": "",
|
||||
"other": "",
|
||||
"email": "",
|
||||
"phone": "",
|
||||
"sms": ""
|
||||
"relatedjobs": "",
|
||||
"sms": "",
|
||||
"updateowner": "",
|
||||
"work": ""
|
||||
},
|
||||
"successes": {
|
||||
"delete": "",
|
||||
@@ -2636,6 +2656,10 @@
|
||||
"actions": {
|
||||
"order": "Commander des pièces",
|
||||
"orderinhouse": ""
|
||||
},
|
||||
"labels": {
|
||||
"view_counts_only": "",
|
||||
"view_timestamps": ""
|
||||
}
|
||||
},
|
||||
"parts_dispatch": {
|
||||
@@ -2985,8 +3009,6 @@
|
||||
"settings": ""
|
||||
},
|
||||
"labels": {
|
||||
"click_for_statuses": "",
|
||||
"partsreceived": "",
|
||||
"actual_in": "",
|
||||
"addnewprofile": "",
|
||||
"alert": "",
|
||||
@@ -3005,6 +3027,7 @@
|
||||
"card_size": "",
|
||||
"cardcolor": "",
|
||||
"cardsettings": "",
|
||||
"click_for_statuses": "",
|
||||
"clm_no": "",
|
||||
"comment": "",
|
||||
"compact": "",
|
||||
@@ -3025,6 +3048,7 @@
|
||||
"orientation": "",
|
||||
"ownr_nm": "",
|
||||
"paintpriority": "",
|
||||
"partsreceived": "",
|
||||
"partsstatus": "",
|
||||
"production_note": "",
|
||||
"refinishhours": "",
|
||||
@@ -3571,18 +3595,12 @@
|
||||
}
|
||||
},
|
||||
"titles": {
|
||||
"simplified-parts-jobs": "",
|
||||
"parts": "",
|
||||
"parts_settings": "",
|
||||
"accounting-payables": "",
|
||||
"accounting-payments": "",
|
||||
"accounting-receivables": "",
|
||||
"all_tasks": "",
|
||||
"app": "",
|
||||
"bc": {
|
||||
"simplified-parts-jobs": "",
|
||||
"parts": "",
|
||||
"parts_settings": "",
|
||||
"accounting-payables": "",
|
||||
"accounting-payments": "",
|
||||
"accounting-receivables": "",
|
||||
@@ -3614,7 +3632,9 @@
|
||||
"my_tasks": "",
|
||||
"owner-detail": "",
|
||||
"owners": "",
|
||||
"parts": "",
|
||||
"parts-queue": "",
|
||||
"parts_settings": "",
|
||||
"payments-all": "",
|
||||
"phonebook": "",
|
||||
"productionboard": "",
|
||||
@@ -3626,6 +3646,7 @@
|
||||
"shop-csi": "",
|
||||
"shop-templates": "",
|
||||
"shop-vendors": "",
|
||||
"simplified-parts-jobs": "",
|
||||
"tasks": "",
|
||||
"temporarydocs": "",
|
||||
"timetickets": "",
|
||||
@@ -3661,7 +3682,9 @@
|
||||
"my_tasks": "",
|
||||
"owners": "Tous les propriétaires | {{app}}",
|
||||
"owners-detail": "",
|
||||
"parts": "",
|
||||
"parts-queue": "",
|
||||
"parts_settings": "",
|
||||
"payments-all": "",
|
||||
"phonebook": "",
|
||||
"productionboard": "",
|
||||
@@ -3677,6 +3700,7 @@
|
||||
"shop-csi": "",
|
||||
"shop-templates": "",
|
||||
"shop_vendors": "Vendeurs | {{app}}",
|
||||
"simplified-parts-jobs": "",
|
||||
"tasks": "",
|
||||
"techconsole": "{{app}}",
|
||||
"techjobclock": "{{app}}",
|
||||
@@ -3837,10 +3861,10 @@
|
||||
"user": {
|
||||
"actions": {
|
||||
"changepassword": "",
|
||||
"signout": "Déconnexion",
|
||||
"updateprofile": "Mettre à jour le profil",
|
||||
"dark_theme": "",
|
||||
"light_theme": "",
|
||||
"dark_theme": ""
|
||||
"signout": "Déconnexion",
|
||||
"updateprofile": "Mettre à jour le profil"
|
||||
},
|
||||
"errors": {
|
||||
"updating": ""
|
||||
@@ -3854,14 +3878,14 @@
|
||||
"labels": {
|
||||
"actions": "",
|
||||
"changepassword": "",
|
||||
"profileinfo": "",
|
||||
"user_settings": "",
|
||||
"play_sound_for_new_messages": "",
|
||||
"notification_sound_on": "",
|
||||
"notification_sound_off": "",
|
||||
"notification_sound_enabled": "",
|
||||
"notification_sound_disabled": "",
|
||||
"notification_sound_help": ""
|
||||
"notification_sound_enabled": "",
|
||||
"notification_sound_help": "",
|
||||
"notification_sound_off": "",
|
||||
"notification_sound_on": "",
|
||||
"play_sound_for_new_messages": "",
|
||||
"profileinfo": "",
|
||||
"user_settings": ""
|
||||
},
|
||||
"successess": {
|
||||
"passwordchanged": ""
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -5,8 +5,10 @@ export function DateFormatter(props) {
|
||||
return props.children ? dayjs(props.children).format(props.includeDay ? "ddd MM/DD/YYYY" : "MM/DD/YYYY") : null;
|
||||
}
|
||||
|
||||
export function DateTimeFormatter(props) {
|
||||
return props.children ? dayjs(props.children).format(props.format ? props.format : "MM/DD/YYYY hh:mm a") : null;
|
||||
export function DateTimeFormatter({ hideTime, ...props }) {
|
||||
return props.children
|
||||
? dayjs(props.children).format(props.format ? props.format : `MM/DD/YYYY${hideTime ? "" : " hh:mm a"}`)
|
||||
: null;
|
||||
}
|
||||
|
||||
export function DateTimeFormatterFunction(date) {
|
||||
@@ -17,11 +19,11 @@ export function TimeFormatter(props) {
|
||||
return props.children ? dayjs(props.children).format(props.format ? props.format : "hh:mm a") : null;
|
||||
}
|
||||
|
||||
export function TimeAgoFormatter(props) {
|
||||
export function TimeAgoFormatter({ removeAgoString = false, ...props }) {
|
||||
const m = dayjs(props.children);
|
||||
return props.children ? (
|
||||
<Tooltip placement="top" title={m.format("MM/DD/YYY hh:mm A")}>
|
||||
{m.fromNow()}
|
||||
<Tooltip placement="top" title={m.format("MM/DD/YYYY hh:mm A")}>
|
||||
{m.fromNow(removeAgoString)}
|
||||
</Tooltip>
|
||||
) : null;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user