Compare commits

..

1 Commits

Author SHA1 Message Date
Dave
1566084d9c bugfix/IO-3531 - Disable unnecessary refetch queries causing page reload. 2026-02-02 12:25:19 -05:00
126 changed files with 3752 additions and 7850 deletions

View File

@@ -13,5 +13,4 @@
.env.development.local
.env.test.local
.env.production.local
.env.localstack.docker
bodyshop_translations.babel
bodyshop_translations.babel

File diff suppressed because it is too large Load Diff

949
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,45 +8,41 @@
"private": true,
"proxy": "http://localhost:4000",
"dependencies": {
"@amplitude/analytics-browser": "^2.35.0",
"@amplitude/analytics-browser": "^2.34.0",
"@ant-design/pro-layout": "^7.22.6",
"@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",
"@apollo/client": "^4.1.3",
"@emotion/is-prop-valid": "^1.4.0",
"@fingerprintjs/fingerprintjs": "^5.0.1",
"@firebase/analytics": "^0.10.19",
"@firebase/app": "^0.14.8",
"@firebase/app": "^0.14.7",
"@firebase/auth": "^1.12.0",
"@firebase/firestore": "^4.11.0",
"@firebase/firestore": "^4.10.0",
"@firebase/messaging": "^0.12.22",
"@jsreport/browser-client": "^3.1.0",
"@reduxjs/toolkit": "^2.11.2",
"@sentry/cli": "^3.2.0",
"@sentry/react": "^10.39.0",
"@sentry/vite-plugin": "^4.9.1",
"@sentry/cli": "^3.1.0",
"@sentry/react": "^10.38.0",
"@sentry/vite-plugin": "^4.8.0",
"@splitsoftware/splitio-react": "^2.6.1",
"@tanem/react-nprogress": "^5.0.63",
"antd": "^6.3.0",
"@tanem/react-nprogress": "^5.0.58",
"antd": "^6.2.2",
"apollo-link-logger": "^3.0.0",
"autosize": "^6.0.1",
"axios": "^1.13.5",
"axios": "^1.13.4",
"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.3.1",
"dotenv": "^17.2.3",
"env-cmd": "^11.0.0",
"exifr": "^7.1.3",
"graphql": "^16.12.0",
"graphql-ws": "^6.0.7",
"i18next": "^25.8.11",
"i18next-browser-languagedetector": "^8.2.1",
"i18next": "^25.8.0",
"i18next-browser-languagedetector": "^8.2.0",
"immutability-helper": "^3.1.1",
"libphonenumber-js": "^1.12.37",
"libphonenumber-js": "^1.12.36",
"lightningcss": "^1.31.1",
"logrocket": "^12.0.0",
"markerjs2": "^2.32.7",
@@ -54,7 +50,7 @@
"normalize-url": "^8.1.1",
"object-hash": "^3.0.0",
"phone": "^3.1.70",
"posthog-js": "^1.351.1",
"posthog-js": "^1.336.4",
"prop-types": "^15.8.1",
"query-string": "^9.3.1",
"raf-schd": "^4.0.3",
@@ -63,6 +59,7 @@
"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",
@@ -87,7 +84,7 @@
"rxjs": "^7.8.2",
"sass": "^1.97.3",
"socket.io-client": "^4.8.3",
"styled-components": "^6.3.10",
"styled-components": "^6.3.8",
"vite-plugin-ejs": "^1.7.0",
"web-vitals": "^5.1.0"
},
@@ -144,11 +141,11 @@
"@emotion/babel-plugin": "^11.13.5",
"@emotion/react": "^11.14.0",
"@eslint/js": "^9.39.2",
"@playwright/test": "^1.58.2",
"@playwright/test": "^1.58.0",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@vitejs/plugin-react": "^5.1.4",
"@vitejs/plugin-react": "^5.1.2",
"babel-plugin-react-compiler": "^1.0.0",
"browserslist": "^4.28.1",
"browserslist-to-esbuild": "^2.1.1",
@@ -156,16 +153,16 @@
"eslint": "^9.39.2",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-compiler": "^19.1.0-rc.2",
"globals": "^17.3.0",
"jsdom": "^28.1.0",
"globals": "^17.2.0",
"jsdom": "^27.4.0",
"memfs": "^4.56.10",
"os-browserify": "^0.3.0",
"playwright": "^1.58.2",
"playwright": "^1.58.0",
"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.5.1",
"vite-plugin-babel": "^1.4.1",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-node-polyfills": "^0.25.0",
"vite-plugin-pwa": "^1.2.0",

View File

@@ -446,32 +446,3 @@
//.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;
}

View File

@@ -182,7 +182,7 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, bills, ref
<Table
loading={loading}
dataSource={dataSource}
pagination={{ placement: "top", pageSize: exportPageLimit, showSizeChanger: false }}
pagination={{ placement: "top", pageSize: exportPageLimit }}
columns={columns}
rowKey="id"
onChange={handleTableChange}

View File

@@ -195,7 +195,7 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, payments,
<Table
loading={loading}
dataSource={dataSource}
pagination={{ placement: "top", pageSize: exportPageLimit, showSizeChanger: false }}
pagination={{ placement: "top", pageSize: exportPageLimit }}
columns={columns}
rowKey="id"
onChange={handleTableChange}

View File

@@ -212,7 +212,7 @@ export function AccountingReceivablesTableComponent({ bodyshop, loading, jobs, r
<Table
loading={loading}
dataSource={dataSource}
pagination={{ placement: "top", pageSize: exportPageLimit, showSizeChanger: false }}
pagination={{ placement: "top", pageSize: exportPageLimit }}
columns={columns}
rowKey="id"
onChange={handleTableChange}

View File

@@ -29,18 +29,19 @@ export function AllocationsAssignmentComponent({
<Select
id="employeeSelector"
showSearch={{
optionFilterProp: "label",
filterOption: (input, option) => option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
optionFilterProp: "children",
filterOption: (input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}}
style={{ width: 200 }}
placeholder="Select a person"
onChange={onChange}
options={bodyshop.employees.map((emp) => ({
value: emp.id,
key: emp.id,
label: `${emp.first_name} ${emp.last_name}`
}))}
/>
>
{bodyshop.employees.map((emp) => (
<Select.Option value={emp.id} key={emp.id}>
{`${emp.first_name} ${emp.last_name}`}
</Select.Option>
))}
</Select>
<InputNumber
defaultValue={assignment.hours}
placeholder={t("joblines.fields.mod_lb_hrs")}

View File

@@ -31,17 +31,19 @@ export default connect(
<div>
<Select
showSearch={{
optionFilterProp: "label",
filterOption: (input, option) => option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
optionFilterProp: "children",
filterOption: (input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}}
style={{ width: 200 }}
placeholder="Select a person"
onChange={onChange}
options={bodyshop.employees.map((emp) => ({
value: emp.id,
label: `${emp.first_name} ${emp.last_name}`
}))}
/>
>
{bodyshop.employees.map((emp) => (
<Select.Option value={emp.id} key={emp.id}>
{`${emp.first_name} ${emp.last_name}`}
</Select.Option>
))}
</Select>
<Button type="primary" disabled={!assignment.employeeid} onClick={handleAssignment}>
Assign

View File

@@ -52,9 +52,6 @@ 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`}

View File

@@ -99,22 +99,20 @@ 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}
options={
bodyshopHasDmsKey(bodyshop)
? CiecaSelect(true, false)
: responsibilityCenters.costs.map((item) => ({ value: item.name, label: item.name }))
}
/>
<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>
</Form.Item>
<Form.Item label={t("billlines.fields.location")} name={["billlineskeys", record.id, "location"]}>
<Select
disabled={disabled}
options={bodyshop.md_parts_locations.map((loc) => ({ value: loc, label: loc }))}
/>
<Select disabled={disabled}>
{bodyshop.md_parts_locations.map((loc, idx) => (
<Select.Option key={idx} value={loc}>
{loc}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
label={t("billlines.fields.deductedfromlbr")}
@@ -138,10 +136,22 @@ export function BillFormItemsExtendedFormItem({
]}
name={["billlineskeys", record.id, "lbr_adjustment", "mod_lbr_ty"]}
>
<Select
allowClear
options={CiecaSelect(false, true)}
/>
<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>
</Form.Item>
<Form.Item
label={t("jobs.labels.adjustmentrate")}

View File

@@ -328,12 +328,13 @@ export function BillFormComponent({
</Form.Item>
{!billEdit && (
<Form.Item label={t("bills.fields.allpartslocation")} name="location">
<Select
style={{ width: "10rem" }}
disabled={disabled}
allowClear
options={bodyshop.md_parts_locations.map((loc) => ({ value: loc, label: loc }))}
/>
<Select style={{ width: "10rem" }} disabled={disabled} allowClear>
{bodyshop.md_parts_locations.map((loc, idx) => (
<Select.Option key={idx} value={loc}>
{loc}
</Select.Option>
))}
</Select>
</Form.Item>
)}
</LayoutFormRow>
@@ -372,11 +373,9 @@ 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
@@ -415,7 +414,7 @@ export function BillFormComponent({
<Statistic
title={t("bills.labels.discrepancy")}
styles={{
content: {
value: {
color: totals.discrepancy.getAmount() === 0 ? "green" : "red"
}
}}
@@ -428,7 +427,6 @@ export function BillFormComponent({
) : null}
</div>
);
}
return null;
}}
</Form.Item>

View File

@@ -1,7 +1,6 @@
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";
@@ -33,7 +32,6 @@ export function BillEnterModalLinesComponent({
}) {
const { t } = useTranslation();
const { setFieldsValue, getFieldsValue, getFieldValue } = form;
const firstFieldRefs = useRef({});
const CONTROL_HEIGHT = 32;
@@ -92,7 +90,6 @@ 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"]);
@@ -157,9 +154,6 @@ export function BillEnterModalLinesComponent({
),
formInput: (record, index) => (
<BillLineSearchSelect
ref={(el) => {
firstFieldRefs.current[index] = el;
}}
disabled={disabled}
options={lineData}
style={{
@@ -170,9 +164,10 @@ export function BillEnterModalLinesComponent({
}}
allowRemoved={form.getFieldValue("is_credit_memo") || false}
onSelect={(value, opt) => {
// IMPORTANT:
// Do NOT autofill actual_cost here. It should only fill when the user forward-tabs
// from Retail (actual_price) -> Actual Cost (actual_cost).
const d = normalizeDiscount(discount);
const retail = Number(opt.cost);
const computedActual = Number.isFinite(retail) ? round2(retail * (1 - d)) : null;
setFieldsValue({
billlines: (getFieldValue("billlines") || []).map((item, idx) => {
if (idx !== index) return item;
@@ -183,7 +178,7 @@ export function BillEnterModalLinesComponent({
quantity: opt.part_qty || 1,
actual_price: opt.cost,
original_actual_price: opt.cost,
// actual_cost intentionally untouched here
actual_cost: isBlank(item.actual_cost) ? computedActual : item.actual_cost,
cost_center: opt.part_type
? bodyshopHasDmsKey(bodyshop)
? opt.part_type !== "PAE"
@@ -210,7 +205,7 @@ export function BillEnterModalLinesComponent({
label: t("billlines.fields.line_desc"),
rules: [{ required: true }]
}),
formInput: () => <Input.TextArea disabled={disabled} autoSize tabIndex={0} />
formInput: () => <Input.TextArea disabled={disabled} autoSize />
},
{
title: t("billlines.fields.quantity"),
@@ -239,7 +234,7 @@ export function BillEnterModalLinesComponent({
})
]
}),
formInput: () => <InputNumber precision={0} min={1} disabled={disabled} tabIndex={0} />
formInput: () => <InputNumber precision={0} min={1} disabled={disabled} />
},
{
title: t("billlines.fields.actual_price"),
@@ -256,10 +251,9 @@ export function BillEnterModalLinesComponent({
<CurrencyInput
min={0}
disabled={disabled}
tabIndex={0}
// NOTE: Autofill should only happen on forward Tab out of Retail
onBlur={() => autofillActualCost(index)}
onKeyDown={(e) => {
if (e.key === "Tab" && !e.shiftKey) autofillActualCost(index);
if (e.key === "Tab") autofillActualCost(index);
}}
/>
),
@@ -334,7 +328,6 @@ export function BillEnterModalLinesComponent({
min={0}
disabled={disabled}
controls={false}
tabIndex={0}
style={{ width: "100%", height: CONTROL_HEIGHT }}
onFocus={() => autofillActualCost(index)}
/>
@@ -399,17 +392,11 @@ export function BillEnterModalLinesComponent({
rules: [{ required: true }]
}),
formInput: () => (
<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 }))
}
/>
<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>
)
},
...(billEdit
@@ -425,11 +412,13 @@ export function BillEnterModalLinesComponent({
name: [field.name, "location"]
}),
formInput: () => (
<Select
disabled={disabled}
tabIndex={0}
options={bodyshop.md_parts_locations.map((loc) => ({ value: loc, label: loc }))}
/>
<Select disabled={disabled}>
{bodyshop.md_parts_locations.map((loc, idx) => (
<Select.Option key={idx} value={loc}>
{loc}
</Select.Option>
))}
</Select>
)
}
]),
@@ -443,7 +432,7 @@ export function BillEnterModalLinesComponent({
key: `${field.name}deductedfromlbr`,
name: [field.name, "deductedfromlbr"]
}),
formInput: () => <Switch disabled={disabled} tabIndex={0} />,
formInput: () => <Switch disabled={disabled} />,
additional: (record, index) => (
<Form.Item shouldUpdate noStyle style={{ display: "inline-block" }}>
{() => {
@@ -470,10 +459,22 @@ export function BillEnterModalLinesComponent({
rules={[{ required: true }]}
name={[record.name, "lbr_adjustment", "mod_lbr_ty"]}
>
<Select
allowClear
options={CiecaSelect(false, true)}
/>
<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>
</Form.Item>
{Enhanced_Payroll.treatment === "on" ? (
@@ -516,13 +517,9 @@ export function BillEnterModalLinesComponent({
formItemProps: (field) => ({
key: `${field.name}fedtax`,
valuePropName: "checked",
name: [field.name, "applicable_taxes", "federal"],
initialValue: InstanceRenderManager({
imex: true,
rome: false
})
name: [field.name, "applicable_taxes", "federal"]
}),
formInput: () => <Switch disabled={disabled} tabIndex={0} />
formInput: () => <Switch disabled={disabled} />
}
]
}),
@@ -537,7 +534,7 @@ export function BillEnterModalLinesComponent({
valuePropName: "checked",
name: [field.name, "applicable_taxes", "state"]
}),
formInput: () => <Switch disabled={disabled} tabIndex={0} />
formInput: () => <Switch disabled={disabled} />
},
...InstanceRenderManager({
@@ -553,7 +550,7 @@ export function BillEnterModalLinesComponent({
valuePropName: "checked",
name: [field.name, "applicable_taxes", "local"]
}),
formInput: () => <Switch disabled={disabled} tabIndex={0} />
formInput: () => <Switch disabled={disabled} />
}
]
}),
@@ -573,7 +570,6 @@ export function BillEnterModalLinesComponent({
icon={<DeleteFilled />}
disabled={disabled || invLen > 0}
onClick={() => remove(record.name)}
tabIndex={0}
/>
{Simple_Inventory.treatment === "on" && (
@@ -645,19 +641,12 @@ 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%" }}
>

View File

@@ -57,6 +57,7 @@ const CardPaymentModalComponent = ({
QUERY_RO_AND_OWNER_BY_JOB_PKS,
{
fetchPolicy: "network-only",
notifyOnNetworkStatusChange: true
}
);

View File

@@ -47,6 +47,7 @@ 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 } : {})
});

View File

@@ -19,12 +19,13 @@ export default function ChatTagRoComponent({ roOptions, loading, handleSearch, h
placeholder={t("general.labels.search")}
onSelect={handleInsertTag}
notFoundContent={loading ? <LoadingOutlined /> : <Empty />}
options={roOptions.map((item, idx) => ({
key: item.id || idx,
value: item.id || idx,
label: ` ${item.ro_number || ""} | ${OwnerNameDisplayFunction(item)}`
}))}
/>
>
{roOptions.map((item, idx) => (
<Select.Option key={item.id || idx}>
{` ${item.ro_number || ""} | ${OwnerNameDisplayFunction(item)}`}
</Select.Option>
))}
</Select>
</div>
{loading ? <LoadingOutlined /> : null}

View File

@@ -309,13 +309,13 @@ export function ContractConvertToRo({ bodyshop, currentUser, contract, disabled
}
]}
>
<Select
options={bodyshop.md_ins_cos.map((s) => ({
key: s.name,
value: s.name,
label: s.name
}))}
/>
<Select>
{bodyshop.md_ins_cos.map((s) => (
<Select.Option key={s.name} value={s.name}>
{s.name}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
name={"class"}
@@ -327,13 +327,13 @@ export function ContractConvertToRo({ bodyshop, currentUser, contract, disabled
}
]}
>
<Select
options={bodyshop.md_classes.map((s) => ({
key: s,
value: s,
label: s
}))}
/>
<Select>
{bodyshop.md_classes.map((s) => (
<Select.Option key={s} value={s}>
{s}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
label={t("contracts.labels.convertform.applycleanupcharge")}

View File

@@ -2,6 +2,8 @@ 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();
@@ -13,17 +15,11 @@ const ContractStatusComponent = ({ value, onChange, ref }) => {
}, [value, option, onChange]);
return (
<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") }
]}
/>
<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>
);
};

View File

@@ -2,6 +2,8 @@ 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();
@@ -21,11 +23,10 @@ const CourtesyCarReadinessComponent = ({ value, onChange, ref }) => {
width: 100
}}
onChange={setOption}
options={[
{ value: "courtesycars.readiness.ready", label: t("courtesycars.readiness.ready") },
{ value: "courtesycars.readiness.notready", label: t("courtesycars.readiness.notready") }
]}
/>
>
<Option value="courtesycars.readiness.ready">{t("courtesycars.readiness.ready")}</Option>
<Option value="courtesycars.readiness.notready">{t("courtesycars.readiness.notready")}</Option>
</Select>
);
};
export default CourtesyCarReadinessComponent;

View File

@@ -2,6 +2,8 @@ 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();
@@ -20,15 +22,14 @@ const CourtesyCarStatusComponent = ({ value, onChange, ref }) => {
width: 100
}}
onChange={setOption}
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") }
]}
/>
>
<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>
);
};
export default CourtesyCarStatusComponent;

View File

@@ -36,7 +36,7 @@ export function DashboardTotalProductionHours({ bodyshop, data, ...cardProps })
<Statistic
title={t("dashboard.labels.prodhrs")}
value={hours.total.toFixed(1)}
styles={{ content: { color: aboveTargetHours ? "green" : "red" } }}
styles={{ value: { color: aboveTargetHours ? "green" : "red" } }}
/>
</Space>
</Card>

View File

@@ -23,13 +23,13 @@ export default connect(mapStateToProps, mapDispatchToProps)(DmsCustomerSelector)
* @constructor
*/
export function DmsCustomerSelector(props) {
const { bodyshop, jobid, job, socket, rrOptions = {} } = props;
const { bodyshop, jobid, socket, rrOptions = {} } = props;
// Centralized "mode" (provider + transport)
const mode = props.mode;
// Stable base props for children
const base = useMemo(() => ({ bodyshop, jobid, job, socket }), [bodyshop, jobid, job, socket]);
const base = useMemo(() => ({ bodyshop, jobid, socket }), [bodyshop, jobid, socket]);
switch (mode) {
case DMS_MAP.reynolds: {

View File

@@ -1,4 +1,4 @@
import { Alert, Button, Checkbox, message, Modal, Space, Table } from "antd";
import { Alert, Button, Checkbox, Col, message, Space, Table } from "antd";
import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { alphaSort } from "../../utils/sorters";
@@ -47,7 +47,6 @@ const rrAddressToString = (addr) => {
export default function RRCustomerSelector({
jobid,
socket,
job,
rrOpenRoLimit = false,
onRrOpenRoFinished,
rrValidationPending = false,
@@ -60,26 +59,15 @@ 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 && !job?.dms_id) {
setOpen(true);
}
}, [rrValidationPending, job?.dms_id]);
if (rrValidationPending) setOpen(true);
}, [rrValidationPending]);
// 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;
@@ -139,10 +127,6 @@ export default function RRCustomerSelector({
});
};
const handleClose = () => {
setOpen(false);
};
const refreshRrSearch = () => {
setRefreshing(true);
const to = setTimeout(() => setRefreshing(false), 12000);
@@ -157,6 +141,8 @@ 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" },
{
@@ -183,45 +169,8 @@ 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 (
<Modal
open={open}
onCancel={handleClose}
footer={null}
width={800}
title={t("dms.selectCustomer")}
>
<Col span={24}>
<Table
title={() => (
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
@@ -247,8 +196,8 @@ export default function RRCustomerSelector({
/>
)}
{/* Validation step banner - only show for NON-early RO flow (legacy) */}
{rrValidationPending && !job?.dms_id && (
{/* Validation step banner */}
{rrValidationPending && (
<Alert
type="info"
showIcon
@@ -313,6 +262,6 @@ export default function RRCustomerSelector({
getCheckboxProps: (record) => ({ disabled: rrDisableRow(record) })
}}
/>
</Modal>
</Col>
);
}

View File

@@ -69,7 +69,7 @@ export function DmsLogEvents({
return {
key: idx,
color: logLevelColor(level),
content: (
children: (
<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 reverse items={items} />;
return <Timeline pending reverse items={items} />;
}
/**

View File

@@ -272,19 +272,11 @@ export default function CdkLikePostForm({ bodyshop, socket, job, logsRef, mode,
name={[field.name, "name"]}
rules={[{ required: true }]}
>
<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
}))}
/>
<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>
</Form.Item>
</Col>
@@ -412,7 +404,7 @@ export default function CdkLikePostForm({ bodyshop, socket, job, logsRef, mode,
<Typography.Title>=</Typography.Title>
<Statistic
title={t("jobs.labels.dms.notallocated")}
styles={{ content: { color: discrep.getAmount() === 0 ? "green" : "red" } }}
styles={{ value: { color: discrep.getAmount() === 0 ? "green" : "red" } }}
value={discrep.toFormat()}
/>
<Button disabled={disablePost} htmlType="submit">

View File

@@ -208,18 +208,8 @@ 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"
@@ -228,96 +218,96 @@ export default function RRPostForm({
initialValues={initialValues}
>
<Row gutter={[16, 12]} align="bottom">
{/* 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
aria-label={t("general.actions.refresh")}
icon={<ReloadOutlined />}
onClick={() => fetchRrAdvisors(true)}
loading={advLoading}
/>
</Tooltip>
</Space.Compact>
</Form.Item>
</Col>
)}
{/* 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) - 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>
)}
{/* 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 && (
<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>
<Col xs={12} sm={8} md={6} lg={4}>
<Form.Item name="kmin" label={t("jobs.fields.kmin")} initialValue={job?.kmin} rules={[{ required: true }]}>
@@ -365,14 +355,13 @@ export default function RRPostForm({
{/* Validation */}
<Form.Item shouldUpdate>
{() => {
// When early RO exists, advisor is already set, so we don't need to validate it
const advisorOk = hasEarlyRO ? true : !!form.getFieldValue("advisorNo");
const advisorOk = !!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" type={hasEarlyRO ? "default" : "primary"}>
{hasEarlyRO ? t("jobs.actions.dms.update_ro") : t("jobs.actions.dms.post")}
<Button disabled={!advisorOk} htmlType="submit">
{t("jobs.actions.dms.post")}
</Button>
</Space>
);

View File

@@ -1,367 +0,0 @@
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>
);
}

View File

@@ -1,33 +0,0 @@
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>
);
}

View File

@@ -86,13 +86,11 @@ export function EmailOverlayComponent({ emailConfig, form, selectedMediaState, b
}
]}
>
<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 })) : [])
]}
/>
<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>
</Form.Item>
<Form.Item
label={

View File

@@ -1,6 +1,7 @@
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 }) => {
@@ -11,24 +12,26 @@ 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;

View File

@@ -1,6 +1,7 @@
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 }) => {
@@ -11,29 +12,30 @@ 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;

View File

@@ -14,11 +14,8 @@ export default function GlobalSearch() {
const [callSearch, { loading, error, data }] = useLazyQuery(GLOBAL_SEARCH_QUERY);
const navigate = useNavigate();
const executeSearch = (variables) => {
if (variables?.search !== "" && variables?.search?.length >= 3)
callSearch({
variables
});
const executeSearch = (v) => {
if (v && v.variables.search && v.variables.search !== "" && v.variables.search.length >= 3) callSearch(v);
};
const debouncedExecuteSearch = _.debounce(executeSearch, 750);
@@ -160,9 +157,7 @@ export default function GlobalSearch() {
return (
<AutoComplete
options={options}
showSearch={{
onSearch: handleSearch
}}
onSearch={handleSearch}
defaultActiveFirstOption
onKeyDown={(e) => {
if (e.key !== "Enter") return;

View File

@@ -67,19 +67,16 @@ export function Jobd3RdPartyModal({ bodyshop, jobId, job, technician }) {
);
};
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 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 handleVendorSelect = (vendorid) => {
@@ -100,19 +97,19 @@ export function Jobd3RdPartyModal({ bodyshop, jobId, job, technician }) {
return (
<>
<Button onClick={showModal}>{t("printcenter.jobs.3rdpartypayer")}</Button>
<Modal open={isModalVisible} onOk={handleOk} onCancel={handleCancel} getContainer={() => document.body}>
<Modal open={isModalVisible} onOk={handleOk} onCancel={handleCancel}>
<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}
options={bodyshop.md_ins_cos.map((s) => ({
value: s.name,
label: s.name
}))}
/>
<Select onSelect={handleInsSelect}>
{bodyshop.md_ins_cos.map((s) => (
<Select.Option key={s.name} obj={s} value={s.name}>
{s.name}
</Select.Option>
))}
</Select>
</Form.Item>
<LayoutFormRow grow>
<Form.Item label={t("printcenter.jobs.3rdpartyfields.addr1")} name="addr1">

View File

@@ -88,15 +88,17 @@ export function JoblineBulkAssign({ setSelectedLines, selectedLines, insertAudit
>
<Select
showSearch={{
optionFilterProp: "label",
filterOption: (input, option) => option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
optionFilterProp: "children",
filterOption: (input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}}
style={{ width: 200 }}
options={bodyshop.employee_teams.map((team) => ({
value: team.id,
label: team.name
}))}
/>
>
{bodyshop.employee_teams.map((team) => (
<Select.Option value={team.id} key={team.id} name={team.name}>
{team.name}
</Select.Option>
))}
</Select>
</Form.Item>
<Space wrap>

View File

@@ -122,26 +122,22 @@ export function JobLineConvertToLabor({
}
]}
>
<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") }
]}
/>
<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>
</Form.Item>
<Form.Item shouldUpdate>

View File

@@ -115,18 +115,19 @@ export function JobLineDispatchButton({
>
<Select
showSearch={{
optionFilterProp: "label",
filterOption: (input, option) => option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
optionFilterProp: "children",
filterOption: (input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}}
style={{ width: 200 }}
options={bodyshop.employees
>
{bodyshop.employees
.filter((emp) => emp.active)
.map((emp) => ({
value: emp.id,
key: emp.id,
label: `${emp.first_name} ${emp.last_name}`
}))}
/>
.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>
<Space wrap>

View File

@@ -64,12 +64,13 @@ export function JobLineStatusPopup({ bodyshop, jobline, disabled }) {
onSelect={handleChange}
onBlur={handleSave}
onClear={() => handleChange(null)}
options={Object.values(bodyshop.md_order_statuses).map((s, idx) => ({
key: idx,
value: s,
label: s
}))}
/>
>
{Object.values(bodyshop.md_order_statuses).map((s, idx) => (
<Select.Option key={idx} value={s}>
{s}
</Select.Option>
))}
</Select>
</LoadingSpinner>
</div>
);

View File

@@ -75,12 +75,13 @@ export function JoblineTeamAssignment({ bodyshop, jobline, disabled, jobId, inse
onSelect={handleChange}
onBlur={handleSave}
onClear={() => handleChange(null)}
options={Object.values(bodyshop.employee_teams).map((s) => ({
key: s.id,
value: s.id,
label: s.name
}))}
/>
>
{Object.values(bodyshop.employee_teams).map((s, idx) => (
<Select.Option key={idx} value={s.id}>
{s.name}
</Select.Option>
))}
</Select>
</LoadingSpinner>
</div>
);

View File

@@ -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 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") }
]} />
<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>
</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 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") }
]} />
<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>
</Form.Item>
<Form.Item label={t("joblines.fields.oem_partno")} name="oem_partno">
<Input />

View File

@@ -1,65 +1,29 @@
import { useMemo } from "react";
import { Tooltip } from "antd";
import { Tag, 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 = () => ({});
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>
);
}
const mapDispatchToProps = () => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
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 += val.count;
// NOTE: if val.status is null, object key becomes "null"
acc[val.status] = (acc[val.status] ?? 0) + val.count;
acc.total = acc.total + val.count;
acc[val.status] = acc[val.status] + val.count;
return acc;
},
{
@@ -70,38 +34,45 @@ export function JobPartsQueueCount({ bodyshop, parts }) {
);
}, [bodyshop, parts]);
if (!parts || !partsStatus) return null;
if (!parts) return null;
return (
<div
style={{
display: "inline-flex", // ✅ shrink-wraps, fixes the “extra box” to the right
gap: 2,
alignItems: "center",
whiteSpace: "nowrap"
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(40px, 1fr))",
gap: "8px",
width: "100%",
justifyItems: "start"
}}
>
<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>
<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>
</div>
);
}

View File

@@ -18,17 +18,10 @@ const mapStateToProps = createStructuredSelector({
* @param parts
* @param displayMode
* @param popoverPlacement
* @param countsOnly
* @returns {JSX.Element}
* @constructor
*/
export function JobPartsReceived({
bodyshop,
parts,
displayMode = "full",
popoverPlacement = "top",
countsOnly = false
}) {
export function JobPartsReceived({ bodyshop, parts, displayMode = "full", popoverPlacement = "top" }) {
const [open, setOpen] = useState(false);
const { t } = useTranslation();
@@ -68,8 +61,6 @@ export function JobPartsReceived({
[canOpen]
);
if (countsOnly) return <JobPartsQueueCount parts={parts} />;
const displayText =
displayMode === "compact" ? summary.percentLabel : `${summary.percentLabel} (${summary.received}/${summary.total})`;
@@ -83,7 +74,7 @@ export function JobPartsReceived({
trigger={["click"]}
placement={popoverPlacement}
content={
<div onClick={stop}>
<div onClick={stop} style={{ minWidth: 260 }}>
<JobPartsQueueCount parts={parts} />
</div>
}
@@ -108,8 +99,7 @@ JobPartsReceived.propTypes = {
bodyshop: PropTypes.object,
parts: PropTypes.array,
displayMode: PropTypes.oneOf(["full", "compact"]),
popoverPlacement: PropTypes.string,
countsOnly: PropTypes.bool
popoverPlacement: PropTypes.string
};
export default connect(mapStateToProps)(JobPartsReceived);

View File

@@ -8,6 +8,8 @@ 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,
@@ -85,24 +87,24 @@ const JobSearchSelect = ({
style={{ width: "100%" }}
suffixIcon={(loading || idLoading) && <Spin />} // matches OLD spinner semantics
notFoundContent={loading ? <LoadingOutlined /> : null} // matches OLD (loading only)
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>
)
}))}
/>
>
{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>
{error ? <AlertComponent title={error.message} type="error" /> : null}
{idError ? <AlertComponent title={idError.message} type="error" /> : null}

View File

@@ -59,12 +59,13 @@ export function JobsAdminClass({ bodyshop, job }) {
}
]}
>
<Select
options={bodyshop.md_classes.map((s) => ({
value: s,
label: s
}))}
/>
<Select>
{bodyshop.md_classes.map((s) => (
<Select.Option key={s} value={s}>
{s}
</Select.Option>
))}
</Select>
</Form.Item>
</Form>

View File

@@ -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,11 +141,13 @@ export function JobsCloseLines({ bodyshop, job, jobRO }) {
option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}}
disabled={jobRO}
options={bodyshop.md_responsibility_centers.profits.map((p) => ({
value: p.name,
label: p.name
}))}
/>
>
{bodyshop.md_responsibility_centers.profits.map((p) => (
<Select.Option key={p.name} value={p.name}>
{p.name}
</Select.Option>
))}
</Select>
</Form.Item>
</td>
<td>
@@ -169,11 +171,13 @@ export function JobsCloseLines({ bodyshop, job, jobRO }) {
option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}}
disabled={jobRO}
options={bodyshop.md_responsibility_centers.profits.map((p) => ({
value: p.name,
label: p.name
}))}
/>
>
{bodyshop.md_responsibility_centers.profits.map((p) => (
<Select.Option key={p.name} value={p.name}>
{p.name}
</Select.Option>
))}
</Select>
</Form.Item>
</td>
</tr>

View File

@@ -1,28 +1,24 @@
import { useMutation } from "@apollo/client/react";
import { Button, Divider, Form, Input, Modal, Select, Space, Switch } from "antd";
import { Button, Form, Input, Popover, Select, Space, Switch } from "antd";
import axios from "axios";
import { some } from "lodash";
import { useCallback, useMemo, useState } from "react";
import { useCallback, 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(
@@ -37,83 +33,18 @@ 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,
@@ -127,11 +58,13 @@ 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")
});
@@ -144,183 +77,182 @@ export function JobsConvertButton({ bodyshop, job, refetch, jobRO, insertAuditTr
setOpen(false);
}
setLoading(false);
};
const submitDisabled = useCallback(() => some(allFormValues, (v) => v === undefined), [allFormValues]);
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);
};
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>
);
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>
<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>
</>
</Popover>
);
}

View File

@@ -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}
options={bodyshop.md_ins_cos.map((s) => ({
value: s.name,
label: s.name
}))}
/>
<Select onChange={handleInsCoChange}>
{bodyshop.md_ins_cos.map((s) => (
<Select.Option key={s.name} value={s.name}>
{s.name}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item label={t("jobs.fields.ins_addr1")} name="ins_addr1">
<Input />
@@ -192,12 +192,13 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.referralsource")} name="referral_source">
<Select
options={bodyshop.md_referral_sources.map((s) => ({
value: s,
label: s
}))}
/>
<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 />
@@ -220,13 +221,10 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
<CurrencyInput min={0} />
</Form.Item>
<Form.Item label={t("jobs.fields.ded_status")} name="ded_status">
<Select
allowClear
options={[
{ value: "W", label: t("jobs.labels.deductible.waived") },
{ value: "Y", label: t("jobs.labels.deductible.stands") }
]}
/>
<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>
</Form.Item>
<Form.Item label={t("jobs.fields.depreciation_taxes")} name="depreciation_taxes">
<CurrencyInput />

View File

@@ -43,19 +43,20 @@ 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} options={[
{ value: "W", label: t("jobs.labels.deductible.waived") },
{ value: "Y", label: t("jobs.labels.deductible.stands") }
]} />
<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>
</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} options={bodyshop.md_ded_notes.map((n) => ({
value: n,
label: n
}))} />
<Select disabled={jobRO}>
{bodyshop.md_ded_notes.map((n, index) => (
<Select.Option key={index}>{n}</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item label={t("jobs.fields.policy_no")} name="policy_no">
<Input disabled={jobRO} />
@@ -65,10 +66,13 @@ 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} options={bodyshop.md_ins_cos.map((s) => ({
value: s.name,
label: s.name
}))} />
<Select disabled={jobRO} onChange={handleInsCoChange}>
{bodyshop.md_ins_cos.map((s) => (
<Select.Option key={s.name} value={s.name}>
{s.name}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item label={t("jobs.fields.ins_addr1")} name="ins_addr1">
<Input disabled={jobRO} />
@@ -119,19 +123,25 @@ export function JobsDetailGeneral({ bodyshop, jobRO, job, form }) {
}
]}
>
<Select disabled={jobRO} allowClear options={bodyshop.md_referral_sources.map((s) => ({
value: s,
label: s
}))} />
<Select disabled={jobRO} allowClear>
{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 disabled={jobRO} />
</Form.Item>
<Form.Item label={t("jobs.fields.alt_transport")} name="alt_transport">
<Select disabled={jobRO} allowClear options={bodyshop.appt_alt_transport.map((s) => ({
value: s,
label: s
}))} />
<Select disabled={jobRO} allowClear>
{bodyshop.appt_alt_transport.map((s) => (
<Select.Option key={s} value={s}>
{s}
</Select.Option>
))}
</Select>
</Form.Item>
</FormRow>
<Row gutter={[16, 16]}>
@@ -233,11 +243,15 @@ 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 options={bodyshop.md_categories.map((s) => ({
value: s,
label: s
}))} />
<Select disabled={jobRO} allowClear>
{bodyshop.md_categories.map((s) => (
<Select.Option key={s} value={s}>
{s}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item label={t("jobs.fields.selling_dealer")} name="selling_dealer">
<Input disabled={jobRO} />
</Form.Item>
@@ -253,21 +267,6 @@ 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>
);

View File

@@ -157,6 +157,7 @@ export function JobsDetailHeaderActions({
variables: watcherVars,
skip: !jobId,
fetchPolicy: "cache-first",
notifyOnNetworkStatusChange: true
});
const jobWatchersCount = jobWatchersData?.job_watchers?.length ?? job?.job_watchers?.length ?? 0;
@@ -714,12 +715,13 @@ export function JobsDetailHeaderActions({
<FormDateTimePickerComponent />
</Form.Item>
<Form.Item label={t("appointments.fields.color")} name="color">
<Select
options={bodyshop.appt_colors.map((col) => ({
value: col.color.hex,
label: col.label
}))}
/>
<Select>
{bodyshop.appt_colors.map((col, idx) => (
<Select.Option key={idx} value={col.color.hex}>
{col.label}
</Select.Option>
))}
</Select>
</Form.Item>
<Space wrap>

View File

@@ -81,16 +81,17 @@ export function JobsDetailHeader({ job, bodyshop, disabled, insertAuditTrail, is
const employeeData = bodyshop.associations.find((a) => a.useremail === job.admin_clerk)?.user?.employee ?? null;
// Handle checkbox changes
const handleCheckboxChange = async (field, checked) => {
const handleCheckboxChange = async (field, e) => {
e.preventDefault();
e.stopPropagation();
const checked = e.target.checked;
const value = checked ? dayjs().toISOString() : null;
try {
const ret = await updateJob({
variables: {
jobId: job.id,
job: { [field]: value }
},
refetchQueries: ["GET_JOB_BY_PK"],
awaitRefetchQueries: true
}
});
insertAuditTrail({
jobid: job.id,
@@ -182,7 +183,7 @@ export function JobsDetailHeader({ job, bodyshop, disabled, insertAuditTrail, is
<Space>
<Checkbox
checked={!!job.estimate_sent_approval}
onChange={(e) => handleCheckboxChange("estimate_sent_approval", e.target.checked)}
onChange={(e) => handleCheckboxChange("estimate_sent_approval", e)}
disabled={disabled || isPartsEntry}
>
{job.estimate_sent_approval && (
@@ -197,7 +198,7 @@ export function JobsDetailHeader({ job, bodyshop, disabled, insertAuditTrail, is
<Space>
<Checkbox
checked={!!job.estimate_approved}
onChange={(e) => handleCheckboxChange("estimate_approved", e.target.checked)}
onChange={(e) => handleCheckboxChange("estimate_approved", e)}
disabled={disabled || isPartsEntry}
>
{job.estimate_approved && (

View File

@@ -94,26 +94,22 @@ export function LaborAllocationsAdjustmentEdit({
}
]}
>
<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") }
]}
/>
<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>
</Form.Item>
<Form.Item
label={t("jobs.fields.adjustmenthours")}
@@ -136,13 +132,7 @@ export function LaborAllocationsAdjustmentEdit({
);
return (
<Popover
getPopupContainer={(trigger) => trigger?.parentElement || document.body}
open={open}
onOpenChange={(vis) => setOpen(vis)}
content={overlay}
trigger="click"
>
<Popover open={open} onOpenChange={(vis) => setOpen(vis)} content={overlay} trigger="click">
{children}
</Popover>
);

View File

@@ -56,6 +56,7 @@ 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

View File

@@ -2,7 +2,6 @@ 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
@@ -12,27 +11,15 @@ const mapDispatchToProps = () => ({
});
export default connect(mapStateToProps, mapDispatchToProps)(OwnerNameDisplay);
export function OwnerNameDisplay({ bodyshop, ownerObject, withToolTip = false }) {
export function OwnerNameDisplay({ bodyshop, ownerObject }) {
const emptyTest = ownerObject?.ownr_fn + ownerObject?.ownr_ln + ownerObject?.ownr_co_nm;
if (!emptyTest || emptyTest === "null" || emptyTest.trim() === "") return "N/A";
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;
}
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();
}
export function OwnerNameDisplayFunction(ownerObject, forceFirstLast = false) {

View File

@@ -7,6 +7,8 @@ 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);
@@ -14,10 +16,9 @@ const OwnerSearchSelect = ({ value, onChange, onBlur, disabled, ref }) => {
SEARCH_OWNERS_BY_ID_FOR_AUTOCOMPLETE
);
const executeSearch = (variables) => {
if (variables?.search !== "" && variables?.search?.length >= 2) callSearch({ variables });
const executeSearch = (v) => {
if (v && v.variables?.search !== "" && v.variables.search.length >= 2) callSearch({ variables: v.variables });
};
const debouncedExecuteSearch = _.debounce(executeSearch, 500);
const handleSearch = (value) => {
@@ -69,12 +70,15 @@ const OwnerSearchSelect = ({ value, onChange, onBlur, disabled, ref }) => {
onSelect={handleSelect}
notFoundContent={loading ? <LoadingOutlined /> : <Empty />}
onBlur={onBlur}
options={theOptions?.map((o) => ({
key: o.id,
value: o.id,
label: `${OwnerNameDisplayFunction(o)} | ${o.ownr_addr1 || ""} `
}))}
/>
>
{theOptions
? theOptions.map((o) => (
<Option key={o.id} value={o.id}>
{`${OwnerNameDisplayFunction(o)} | ${o.ownr_addr1 || ""} `}
</Option>
))
: null}
</Select>
{idLoading || loading ? <LoadingOutlined /> : null}
{error ? <AlertComponent title={error.message} type="error" /> : null}
{idError ? <AlertComponent title={idError.message} type="error" /> : null}

View File

@@ -1,121 +1,94 @@
import { DownOutlined } from "@ant-design/icons";
import { Button, Divider, Dropdown, InputNumber, Space, theme } from "antd";
import { useState } from "react";
import { Dropdown, InputNumber, Space } from "antd";
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 { token } = theme.useToken();
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}%` })
})),
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;
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>
)
}
],
onClick: ({ key }) => {
logImEXEvent("parts_order_manual_discount", {});
applyDiscountPercent(key);
setOpen(false);
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)
};
})
}
});
}
};
return (
<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>
)}
>
<Dropdown menu={menu} trigger="click">
<Space>
% <DownOutlined />
%
<DownOutlined />
</Space>
</Dropdown>
);

View File

@@ -158,21 +158,19 @@ export function PartsOrderModalComponent({
key={`${index}part_type`}
name={[field.name, "part_type"]}
>
<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") }
]}
/>
<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>
</Form.Item>
<Form.Item
label={t("parts_orders.fields.oem_partno")}

View File

@@ -17,6 +17,7 @@ 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";
@@ -65,7 +66,7 @@ export function PartsOrderModalContainer({
const sendTypeState = useState("e");
const sendType = sendTypeState[0];
const { error, data } = useQuery(QUERY_ALL_VENDORS_FOR_ORDER, {
const { loading, error, data } = useQuery(QUERY_ALL_VENDORS_FOR_ORDER, {
skip: !open,
variables: { jobId: jobId },
fetchPolicy: "network-only",
@@ -93,6 +94,16 @@ 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({
@@ -361,7 +372,6 @@ 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}
@@ -380,14 +390,18 @@ export function PartsOrderModalContainer({
>
{error ? <AlertComponent title={error.message} type="error" /> : null}
<Form form={form} layout="vertical" autoComplete="no" onFinish={handleFinish} initialValues={initialValues}>
<PartsOrderModalComponent
form={form}
vendorList={data?.vendors || []}
sendTypeState={sendTypeState}
isReturn={isReturn}
preferredMake={data && data.jobs[0] && data.jobs[0].v_make_desc}
job={job}
/>
{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}
/>
)}
</Form>
</Modal>
);

View File

@@ -1,6 +1,6 @@
import { SyncOutlined } from "@ant-design/icons";
import { useQuery } from "@apollo/client/react";
import { Button, Card, Checkbox, Input, Space, Table } from "antd";
import { Button, Card, Input, Space, Table } from "antd";
import _ from "lodash";
import queryString from "query-string";
import { useState } from "react";
@@ -31,8 +31,6 @@ 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",
@@ -94,7 +92,6 @@ 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,
@@ -106,20 +103,16 @@ 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} withToolTip />
<OwnerNameDisplay ownerObject={record} />
</Link>
) : (
<span>
<OwnerNameDisplay ownerObject={record} withToolTip />
<OwnerNameDisplay ownerObject={record} />
</span>
);
}
@@ -194,7 +187,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 hideTime={!viewTimeStamp}>{record.scheduled_in}</DateTimeFormatter>
render: (text, record) => <DateTimeFormatter>{record.scheduled_in}</DateTimeFormatter>
},
{
title: t("jobs.fields.scheduled_completion"),
@@ -203,9 +196,7 @@ 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 hideTime={!viewTimeStamp}>{record.scheduled_completion}</DateTimeFormatter>
)
render: (text, record) => <DateTimeFormatter>{record.scheduled_completion}</DateTimeFormatter>
},
// {
// title: t("vehicles.fields.plate_no"),
@@ -236,23 +227,16 @@ 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 removeAgoString>{record.updated_at}</TimeAgoFormatter>
render: (text, record) => <TimeAgoFormatter>{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="middle"
countsOnly={countsOnly}
/>
<JobPartsReceived parts={record.joblines_status} displayMode="full" popoverPlacement="topLeft" />
)
},
{
@@ -265,7 +249,6 @@ 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,
@@ -292,12 +275,6 @@ 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")}
@@ -322,7 +299,7 @@ export function PartsQueueListComponent({ bodyshop }) {
rowKey="id"
dataSource={jobs}
style={{ height: "100%" }}
//scroll={{ x: true }}
scroll={{ x: true }}
onChange={handleTableChange}
rowSelection={{
onSelect: (record) => {

View File

@@ -29,12 +29,13 @@ export function PartsReceiveModalComponent({ bodyshop, form }) {
})
});
}}
options={bodyshop.md_parts_locations.map((loc, idx) => ({
key: idx,
value: loc,
label: loc
}))}
/>
>
{bodyshop.md_parts_locations.map((loc, idx) => (
<Select.Option key={idx} value={loc}>
{loc}
</Select.Option>
))}
</Select>
</Form.Item>
</LayoutFormRow>
<Typography.Title level={4}>{t("parts_orders.labels.inthisorder")}</Typography.Title>
@@ -84,14 +85,13 @@ export function PartsReceiveModalComponent({ bodyshop, form }) {
key={`${index}location`}
name={[field.name, "location"]}
>
<Select
style={{ width: "10rem" }}
options={bodyshop.md_parts_locations.map((loc, idx) => ({
key: idx,
value: loc,
label: loc
}))}
/>
<Select style={{ width: "10rem" }}>
{bodyshop.md_parts_locations.map((loc, idx) => (
<Select.Option key={idx} value={loc}>
{loc}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
label={t("parts_orders.fields.quantity")}

View File

@@ -2,6 +2,8 @@ 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();
@@ -24,7 +26,13 @@ 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")} options={emailTypes} />
<Select placeholder={t("bodyshop.placeholders.select_email_type")}>
{emailTypes.map((type) => (
<Option key={type.value} value={type.value}>
{type.label}
</Option>
))}
</Select>
</Form.Item>
<Form.Item
{...restField}

View File

@@ -91,25 +91,20 @@ export function PaymentFormComponent({ form, bodyshop, disabled }) {
}
]}
>
<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") }
]
}
/>
<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>
</Form.Item>
<Form.Item
@@ -122,13 +117,13 @@ export function PaymentFormComponent({ form, bodyshop, disabled }) {
}
]}
>
<Select disabled={disabled}
options={bodyshop.md_payment_types.map((v, idx) => ({
key: idx,
value: v,
label: v
}))}
/>
<Select disabled={disabled}>
{bodyshop.md_payment_types.map((v, idx) => (
<Select.Option key={idx} value={v}>
{v}
</Select.Option>
))}
</Select>
</Form.Item>
</LayoutFormRow>
<LayoutFormRow grow>

View File

@@ -33,7 +33,7 @@ export default function PaymentFormTotalPayments({ jobid }) {
{balance && (
<Statistic
title={t("payments.labels.balance")}
styles={{ content: { color: balance.getAmount() !== 0 ? "red" : "green" } }}
styles={{ value: { color: balance.getAmount() !== 0 ? "red" : "green" } }}
value={(balance && balance.toFormat()) || ""}
/>
)}

View File

@@ -108,7 +108,7 @@ export function PrintCenterJobsLabels({ jobId }) {
</Card>
);
return (
<Popover content={content} open={isModalVisible} getPopupContainer={(trigger) => trigger.parentElement}>
<Popover content={content} open={isModalVisible}>
<Button onClick={() => setIsModalVisible(true)}>{t("printcenter.jobs.labels.labels")}</Button>
</Popover>
);

View File

@@ -35,6 +35,8 @@ export function ProductionListEmpAssignment({ insertAuditTrail, bodyshop, record
const result = await updateJob({
variables: { jobId: record.id, job: { [empAssignment]: employeeid } }
// awaitRefetchQueries: true,
});
insertAuditTrail({
@@ -53,7 +55,6 @@ export function ProductionListEmpAssignment({ insertAuditTrail, bodyshop, record
await refetch();
setAssignment({ operation: null, employeeid: null });
setLoading(false);
};
const handleRemove = async (operation) => {
@@ -83,7 +84,6 @@ export function ProductionListEmpAssignment({ insertAuditTrail, bodyshop, record
await refetch();
setAssignment({ operation: null, employeeid: null });
setLoading(false);
};
@@ -94,31 +94,29 @@ export function ProductionListEmpAssignment({ insertAuditTrail, bodyshop, record
const [visibility, setVisibility] = useState(false);
const onChange = (e, option) => {
setAssignment({ ...assignment, employeeid: e, name: option.label });
setAssignment({ ...assignment, employeeid: e, name: option.name });
};
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: "label",
filterOption: (input, option) => option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
optionFilterProp: "children",
filterOption: (input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}}
style={{ width: 200 }}
value={assignment.employeeid}
onChange={onChange}
options={employeeOptions}
/>
>
{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>
</Col>
<Col span={24}>
<Space wrap>
@@ -143,25 +141,25 @@ export function ProductionListEmpAssignment({ insertAuditTrail, bodyshop, record
if (record[type]) theEmployee = bodyshop.employees.find((e) => e.id === record[type]);
return (
<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">
<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>
) : (
<PlusCircleFilled
style={{ ...iconStyle, cursor: "pointer" }}
className="muted-button"
onClick={() => {
setAssignment({ operation: type, employeeid: null });
setAssignment({ operation: type });
setVisibility(true);
}}
/>
</Popover>
)}
</Spin>
)}
</Spin>
</Popover>
);
}

View File

@@ -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

View File

@@ -1,19 +1,15 @@
import { HolderOutlined, SyncOutlined } from "@ant-design/icons";
import { 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 { 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 { useEffect, useMemo, useRef, useState } from "react";
import ReactDragListView from "react-drag-listview";
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";
@@ -27,81 +23,12 @@ import { logImEXEvent } from "../../firebase/firebase.utils.js";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
technician: selectTechnician,
currentUser: selectCurrentUser,
isDarkMode: selectDarkMode
currentUser: selectCurrentUser
});
// 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 }) {
export function ProductionListTable({ loading, data, refetch, bodyshop, technician, currentUser }) {
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({
@@ -109,10 +36,8 @@ 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) ||
@@ -121,7 +46,6 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
filteredInfo: { text: "" }
}
);
const initialColumnsRef = useRef(
(initialStateRef.current &&
bodyshop?.production_config
@@ -142,36 +66,14 @@ 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, dont 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 {
@@ -187,8 +89,10 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
width: k.width ?? 100
};
}) || [];
setColumns(newColumns);
// Only update columns if they haven't been manually changed by the user
if (_.isEqual(initialColumnsRef.current, columns)) {
setColumns(newColumns);
}
}, [
matchingColumnConfig,
bodyshop,
@@ -198,8 +102,7 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
Production_List_Status_Colors,
refetch,
state,
columns,
isResizing
columns
]);
const handleTableChange = (pagination, filters, sorter) => {
@@ -215,30 +118,17 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
logImEXEvent("production_list_sort_filter", { pagination, filters, sorter });
};
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 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 onDragCancel = () => {
setActiveId(null);
};
const removeColumn = (e) => {
const { key } = e;
const newColumns = columns.filter((i) => i.key !== key);
@@ -249,55 +139,19 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
logImEXEvent("production_list_remove_column", { key });
};
// 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);
const handleResize =
(index) =>
(e, { size }) => {
const nextColumns = [...columns];
nextColumns[index] = {
...nextColumns[index],
width: size.width
};
if (!_.isEqual(nextColumns, columns)) {
setColumns(nextColumns);
setHasUnsavedChanges(true);
logImEXEvent("production_list_resize_column", { key: columnKey, width: size.width });
},
[applyColumnWidth]
);
}
};
const addColumn = (newColumn) => {
const updatedColumns = [...columns, newColumn];
@@ -309,53 +163,19 @@ 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 (
<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>
<Dropdown className="prod-header-dropdown" menu={menu} trigger={["contextMenu"]}>
<span>{col.title}</span>
</Dropdown>
);
};
@@ -454,9 +274,6 @@ 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
@@ -469,104 +286,60 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
}
/>
<ProductionListDetail jobs={dataSource} />
<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;
}
<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)
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)"
backgroundColor: "var(--table-row-even-bg)"
}
};
}
})}
components={{
header: {
cell: DraggableHeaderCell
return null;
}
}}
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)
})
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)"
}
};
})}
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>
}
})}
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>
</div>
);
}

View File

@@ -1,37 +1,28 @@
import { forwardRef } from "react";
import { Resizable } from "react-resizable";
import "react-resizable/css/styles.css";
const ResizableComponent = forwardRef((props, ref) => {
const { onResize, onResizeStart, onResizeStop, width, dragAttributes, dragListeners, ...restProps } = props;
export default function ResizableComponent(props) {
const { onResize, width, ...restProps } = props;
if (!width) {
return <th ref={ref} {...restProps} {...(dragAttributes || {})} {...(dragListeners || {})} />;
return <th {...restProps} />;
}
return (
<Resizable
width={width}
width={width || 200}
height={0}
onResize={onResize}
onResizeStart={onResizeStart}
onResizeStop={onResizeStop}
draggableOpts={{ enableUserSelectHack: false }}
resizeHandles={["e"]}
axis="x"
handle={(axis, handleRef) => (
handle={
<span
ref={handleRef}
className={`react-resizable-handle react-resizable-handle-${axis}`}
onClick={(e) => e.stopPropagation()}
className="react-resizable-handle"
onClick={(e) => {
e.stopPropagation();
}}
/>
)}
}
>
<th ref={ref} {...restProps} {...(dragAttributes || {})} {...(dragListeners || {})} />
<th {...restProps} />
</Resizable>
);
});
ResizableComponent.displayName = "ResizableComponent";
export default ResizableComponent;
}

View File

@@ -158,28 +158,20 @@ export function ScheduleJobModalComponent({
</LayoutFormRow>
<LayoutFormRow grow>
<Form.Item name="color" label={t("appointments.fields.color")}>
<Select
allowClear
options={
bodyshop.appt_colors &&
bodyshop.appt_colors.map((color) => ({
value: color.color.hex,
label: color.label
}))
}
/>
<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>
</Form.Item>
<Form.Item name={"alt_transport"} label={t("jobs.fields.alt_transport")}>
<Select
allowClear
options={
bodyshop.appt_alt_transport &&
bodyshop.appt_alt_transport.map((alt) => ({
value: alt,
label: alt
}))
}
/>
<Select allowClear>
{bodyshop.appt_alt_transport &&
bodyshop.appt_alt_transport.map((alt) => <Select.Option key={alt}>{alt}</Select.Option>)}
</Select>
</Form.Item>
<Form.Item name={"note"} label={t("appointments.fields.note")}>
<Input />

View File

@@ -120,12 +120,13 @@ export function ScheduleManualEvent({ bodyshop, event }) {
<FormDateTimePickerComponent />
</Form.Item>
<Form.Item label={t("appointments.fields.color")} name="color">
<Select
options={bodyshop.appt_colors.map((col) => ({
value: col.color.hex,
label: col.label
}))}
/>
<Select>
{bodyshop.appt_colors.map((col, idx) => (
<Select.Option key={idx} value={col.color.hex}>
{col.label}
</Select.Option>
))}
</Select>
</Form.Item>
<Space wrap>

View File

@@ -325,20 +325,22 @@ export function ShopEmployeesFormComponent({ bodyshop }) {
}
]}
>
<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
})))
]}
/>
<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>
</Form.Item>
<Form.Item
label={t("employees.fields.rate")}

View File

@@ -1039,25 +1039,22 @@ export function ShopInfoGeneral({ form }) {
key={`${index}mod_lbr_ty`}
name={[field.name, "mod_lbr_ty"]}
>
<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") }
]}
/>
<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>
</Form.Item>
<Form.Item
label={t("joblines.fields.mod_lb_hrs")}
@@ -1071,20 +1068,17 @@ export function ShopInfoGeneral({ form }) {
key={`${index}part_type`}
name={[field.name, "part_type"]}
>
<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") }
]}
/>
<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>
</Form.Item>
<Form.Item
label={t("joblines.fields.oem_partno")}

View File

@@ -51,7 +51,13 @@ export default function ShopInfoIntakeChecklistComponent({ form }) {
}
]}
>
<Select options={Object.keys(ConfigFormTypes).map((i) => ({ value: i, label: i }))} />
<Select>
{Object.keys(ConfigFormTypes).map((i) => (
<Select.Option key={i} value={i}>
{i}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
label={t("jobs.fields.intake.label")}
@@ -150,13 +156,13 @@ export default function ShopInfoIntakeChecklistComponent({ form }) {
}
]}
>
<Select
mode="multiple"
options={Object.keys(TemplateListGenerated).map((i) => ({
value: TemplateListGenerated[i].key,
label: TemplateListGenerated[i].title
}))}
/>
<Select mode="multiple">
{Object.keys(TemplateListGenerated).map((i) => (
<Select.Option key={TemplateListGenerated[i].key} value={TemplateListGenerated[i].key}>
{TemplateListGenerated[i].title}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
name={["intakechecklist", "next_contact_hours"]}
@@ -199,7 +205,13 @@ export default function ShopInfoIntakeChecklistComponent({ form }) {
}
]}
>
<Select options={Object.keys(ConfigFormTypes).map((i) => ({ value: i, label: i }))} />
<Select>
{Object.keys(ConfigFormTypes).map((i) => (
<Select.Option key={i} value={i}>
{i}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
@@ -298,13 +310,13 @@ export default function ShopInfoIntakeChecklistComponent({ form }) {
}
]}
>
<Select
mode="multiple"
options={Object.keys(TemplateListGenerated).map((i) => ({
value: TemplateListGenerated[i].key,
label: TemplateListGenerated[i].title
}))}
/>
<Select mode="multiple">
{Object.keys(TemplateListGenerated).map((i) => (
<Select.Option key={TemplateListGenerated[i].key} value={TemplateListGenerated[i].key}>
{TemplateListGenerated[i].title}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
name={["deliverchecklist", "actual_delivery"]}

View File

@@ -80,7 +80,13 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
}
]}
>
<Select mode="multiple" options={options.map((item) => ({ value: item, label: item }))} />
<Select mode="multiple">
{options.map((item, idx) => (
<Select.Option key={idx} value={item}>
{item}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
name={["md_ro_statuses", "pre_production_statuses"]}
@@ -93,7 +99,13 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
}
]}
>
<Select mode="multiple" options={options.map((item) => ({ value: item, label: item }))} />
<Select mode="multiple">
{options.map((item, idx) => (
<Select.Option key={idx} value={item}>
{item}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
name={["md_ro_statuses", "production_statuses"]}
@@ -106,7 +118,13 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
}
]}
>
<Select mode="multiple" options={options.map((item) => ({ value: item, label: item }))} />
<Select mode="multiple">
{options.map((item, idx) => (
<Select.Option key={idx} value={item}>
{item}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
name={["md_ro_statuses", "post_production_statuses"]}
@@ -119,7 +137,13 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
}
]}
>
<Select mode="multiple" options={options.map((item) => ({ value: item, label: item }))} />
<Select mode="multiple">
{options.map((item, idx) => (
<Select.Option key={idx} value={item}>
{item}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
name={["md_ro_statuses", "ready_statuses"]}
@@ -132,7 +156,13 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
}
]}
>
<Select mode="multiple" options={options.map((item) => ({ value: item, label: item }))} />
<Select mode="multiple">
{options.map((item, idx) => (
<Select.Option key={idx} value={item}>
{item}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
name={["md_ro_statuses", "additional_board_statuses"]}
@@ -145,7 +175,13 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
}
]}
>
<Select mode="multiple" options={options.map((item) => ({ value: item, label: item }))} />
<Select mode="multiple">
{options.map((item, idx) => (
<Select.Option key={idx} value={item}>
{item}
</Select.Option>
))}
</Select>
</Form.Item>
<LayoutFormRow noDivider>
<Form.Item
@@ -158,7 +194,13 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
]}
name={["md_ro_statuses", "default_scheduled"]}
>
<Select options={options.map((item) => ({ value: item, label: item }))} />
<Select>
{options.map((item, idx) => (
<Select.Option key={idx} value={item}>
{item}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
label={t("bodyshop.fields.statuses.default_arrived")}
@@ -170,7 +212,13 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
]}
name={["md_ro_statuses", "default_arrived"]}
>
<Select options={options.map((item) => ({ value: item, label: item }))} />
<Select>
{options.map((item, idx) => (
<Select.Option key={idx} value={item}>
{item}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
label={t("bodyshop.fields.statuses.default_exported")}
@@ -182,7 +230,13 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
]}
name={["md_ro_statuses", "default_exported"]}
>
<Select options={options.map((item) => ({ value: item, label: item }))} />
<Select>
{options.map((item, idx) => (
<Select.Option key={idx} value={item}>
{item}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
label={t("bodyshop.fields.statuses.default_imported")}
@@ -194,7 +248,13 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
]}
name={["md_ro_statuses", "default_imported"]}
>
<Select options={options.map((item) => ({ value: item, label: item }))} />
<Select>
{options.map((item, idx) => (
<Select.Option key={idx} value={item}>
{item}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
label={t("bodyshop.fields.statuses.default_invoiced")}
@@ -206,7 +266,13 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
]}
name={["md_ro_statuses", "default_invoiced"]}
>
<Select options={options.map((item) => ({ value: item, label: item }))} />
<Select>
{options.map((item, idx) => (
<Select.Option key={idx} value={item}>
{item}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
label={t("bodyshop.fields.statuses.default_completed")}
@@ -218,7 +284,13 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
]}
name={["md_ro_statuses", "default_completed"]}
>
<Select options={options.map((item) => ({ value: item, label: item }))} />
<Select>
{options.map((item, idx) => (
<Select.Option key={idx} value={item}>
{item}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
label={t("bodyshop.fields.statuses.default_delivered")}
@@ -230,7 +302,13 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
]}
name={["md_ro_statuses", "default_delivered"]}
>
<Select options={options.map((item) => ({ value: item, label: item }))} />
<Select>
{options.map((item, idx) => (
<Select.Option key={idx} value={item}>
{item}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
label={t("bodyshop.fields.statuses.default_void")}
@@ -242,7 +320,13 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
]}
name={["md_ro_statuses", "default_void"]}
>
<Select options={options.map((item) => ({ value: item, label: item }))} />
<Select>
{options.map((item, idx) => (
<Select.Option key={idx} value={item}>
{item}
</Select.Option>
))}
</Select>
</Form.Item>
</LayoutFormRow>
{Production_List_Status_Colors.treatment === "on" && (
@@ -268,7 +352,13 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
}
]}
>
<Select options={productionStatus.map((item) => ({ value: item, label: item }))} />
<Select>
{productionStatus.map((item, idx) => (
<Select.Option key={idx} value={item}>
{item}
</Select.Option>
))}
</Select>
</Form.Item>
<DeleteFilled
onClick={() => {

View File

@@ -60,13 +60,13 @@ export default function ShopInfoSpeedPrint() {
}
]}
>
<Select
mode="multiple"
options={Object.keys(TemplateListGenerated).map((key) => ({
value: TemplateListGenerated[key].key,
label: TemplateListGenerated[key].title
}))}
/>
<Select mode="multiple">
{Object.keys(TemplateListGenerated).map((key, idx) => (
<Select.Option key={idx} value={TemplateListGenerated[key].key}>
{TemplateListGenerated[key].title}
</Select.Option>
))}
</Select>
</Form.Item>
<Space wrap>

View File

@@ -43,43 +43,85 @@ export function ShopInfoIntellipay({ bodyshop, form }) {
label={t("bodyshop.fields.intellipay_config.payment_map.visa")}
name={["intellipay_config", "payment_map", "visa"]}
>
<Select showSearch options={bodyshop.md_payment_types.map((item) => ({ value: item, label: item }))} />
<Select showSearch>
{bodyshop.md_payment_types.map((item, idx) => (
<Select.Option key={idx} value={item}>
{item}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
label={t("bodyshop.fields.intellipay_config.payment_map.mast")}
name={["intellipay_config", "payment_map", "mast"]}
>
<Select showSearch options={bodyshop.md_payment_types.map((item) => ({ value: item, label: item }))} />
<Select showSearch>
{bodyshop.md_payment_types.map((item, idx) => (
<Select.Option key={idx} value={item}>
{item}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
label={t("bodyshop.fields.intellipay_config.payment_map.amex")}
name={["intellipay_config", "payment_map", "amex"]}
>
<Select showSearch options={bodyshop.md_payment_types.map((item) => ({ value: item, label: item }))} />
<Select showSearch>
{bodyshop.md_payment_types.map((item, idx) => (
<Select.Option key={idx} value={item}>
{item}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
label={t("bodyshop.fields.intellipay_config.payment_map.disc")}
name={["intellipay_config", "payment_map", "disc"]}
>
<Select showSearch options={bodyshop.md_payment_types.map((item) => ({ value: item, label: item }))} />
<Select showSearch>
{bodyshop.md_payment_types.map((item, idx) => (
<Select.Option key={idx} value={item}>
{item}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
label={t("bodyshop.fields.intellipay_config.payment_map.dnrs")}
name={["intellipay_config", "payment_map", "dnrs"]}
>
<Select showSearch options={bodyshop.md_payment_types.map((item) => ({ value: item, label: item }))} />
<Select showSearch>
{bodyshop.md_payment_types.map((item, idx) => (
<Select.Option key={idx} value={item}>
{item}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
label={t("bodyshop.fields.intellipay_config.payment_map.jcb")}
name={["intellipay_config", "payment_map", "jcb"]}
>
<Select showSearch options={bodyshop.md_payment_types.map((item) => ({ value: item, label: item }))} />
<Select showSearch>
{bodyshop.md_payment_types.map((item, idx) => (
<Select.Option key={idx} value={item}>
{item}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
label={t("bodyshop.fields.intellipay_config.payment_map.intr")}
name={["intellipay_config", "payment_map", "intr"]}
>
<Select showSearch options={bodyshop.md_payment_types.map((item) => ({ value: item, label: item }))} />
<Select showSearch>
{bodyshop.md_payment_types.map((item, idx) => (
<Select.Option key={idx} value={item}>
{item}
</Select.Option>
))}
</Select>
</Form.Item>
</LayoutFormRow>
</>

View File

@@ -33,8 +33,7 @@ export function TaskListContainer({
currentUser,
onlyMine,
parentJobId,
showRo = true,
disableJobRefetch = false
showRo = true
}) {
const { t } = useTranslation();
const notification = useNotification();
@@ -91,10 +90,6 @@ export function TaskListContainer({
refetchQueries: [Object.keys(query)[0]]
};
if (!disableJobRefetch) {
toggledTaskObject.refetchQueries.push("GET_JOB_BY_PK");
}
const toggledTask = await toggleTaskCompleted(toggledTaskObject);
if (!toggledTask.errors) {
@@ -144,10 +139,6 @@ export function TaskListContainer({
refetchQueries: [Object.keys(query)[0]]
};
if (!disableJobRefetch) {
toggledTaskObject.refetchQueries.push("GET_JOB_BY_PK");
}
const toggledTask = await toggleTaskDeleted(toggledTaskObject);
if (!toggledTask.errors) {

View File

@@ -57,23 +57,21 @@ export function TechClockInComponent({ form, bodyshop, technician }) {
}
]}
>
<Select
options={
emps &&
emps.rates.map((item) => ({
value: item.cost_center,
label:
item.cost_center === "timetickets.labels.shift"
<Select>
{emps &&
emps.rates.map((item) => (
<Select.Option key={item.cost_center} value={item.cost_center}>
{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
}))
}
/>
: item.cost_center}
</Select.Option>
))}
</Select>
</Form.Item>
</LayoutFormRow>
<Divider />

View File

@@ -201,22 +201,22 @@ export function TechClockOffButton({
}
]}
>
<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
}))
}
/>
<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>
</Form.Item>
{isShiftTicket ? (
@@ -232,12 +232,11 @@ export function TechClockOffButton({
}
]}
>
<Select
options={bodyshop.md_ro_statuses.production_statuses.map((item) => ({
value: item,
label: item
}))}
/>
<Select>
{bodyshop.md_ro_statuses.production_statuses.map((item) => (
<Select.Option key={item}></Select.Option>
))}
</Select>
</Form.Item>
)}

View File

@@ -91,22 +91,21 @@ export function TimeTicketModalComponent({
value={value === "timetickets.labels.shift" ? t(value) : value}
{...props}
disabled={value === "timetickets.labels.shift" || disabled}
options={
emps &&
emps.rates.map((item) => ({
value: item.cost_center,
label:
item.cost_center === "timetickets.labels.shift"
>
{emps &&
emps.rates.map((item) => (
<Select.Option key={item.cost_center} value={item.cost_center}>
{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
}))
}
/>
: item.cost_center}
</Select.Option>
))}
</Select>
);
const MemoInput = ({ value, ...props }) => (

View File

@@ -20,15 +20,13 @@ export function TimeTicketShiftFormComponent() {
}
]}
>
<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") }
]}
/>
<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>
</Form.Item>
</div>
);

View File

@@ -9,6 +9,8 @@ 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);
@@ -16,10 +18,9 @@ const VehicleSearchSelect = ({ value, onChange, onBlur, disabled, ref }) => {
SEARCH_VEHICLES_BY_ID_FOR_AUTOCOMPLETE
);
const executeSearch = (variables) => {
if (variables?.search !== "" && variables?.search?.length >= 2) callSearch({ variables });
const executeSearch = (v) => {
if (v && v.variables?.search !== "" && v.variables.search.length >= 2) callSearch({ variables: v.variables });
};
const debouncedExecuteSearch = _.debounce(executeSearch, 500);
const handleSearch = (value) => {
@@ -71,12 +72,15 @@ const VehicleSearchSelect = ({ value, onChange, onBlur, disabled, ref }) => {
onSelect={handleSelect}
notFoundContent={loading ? <LoadingOutlined /> : <Empty />}
onBlur={onBlur}
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 || ""} `
}))}
/>
>
{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>
{idLoading || loading ? <LoadingOutlined /> : null}
{error ? <AlertComponent title={error.message} type="error" /> : null}
{idError ? <AlertComponent title={idError.message} type="error" /> : null}

View File

@@ -3,6 +3,8 @@ 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 }) => {
@@ -19,57 +21,10 @@ 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={{
optionFilterProp: "name"
}}
showSearch
value={option}
style={{
width: "100%"
@@ -104,11 +59,76 @@ const VendorSearchSelect = ({ value, onChange, options, onSelect, disabled, pref
}}
popupMatchSelectWidth={false}
onChange={setOption}
optionFilterProp="name"
onSelect={onSelect}
disabled={disabled || false}
optionLabelProp="name"
options={allOptions}
/>
>
{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>
);
};
export default VendorSearchSelect;

View File

@@ -470,9 +470,6 @@ export const GET_JOB_BY_PK = gql`
clm_total
comment
converted
dms_id
dms_customer_id
dms_advisor_id
csiinvites {
completedon
id
@@ -494,9 +491,6 @@ 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 {
@@ -2001,9 +1995,6 @@ 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
@@ -2225,9 +2216,6 @@ export const QUERY_JOB_EXPORT_DMS = gql`
plate_no
plate_st
ownr_co_nm
dms_id
dms_customer_id
dms_advisor_id
}
}
`;

View File

@@ -119,13 +119,12 @@ export function DmsContainer({ setBreadcrumbs, setSelectedHeader }) {
setLogLevel(value);
socket.emit("set-log-level", value);
}}
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" }
]}
/>
>
<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>
<Button onClick={() => setLogs([])}>Clear Logs</Button>
<Button
onClick={() => {

View File

@@ -426,24 +426,6 @@ 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 />
@@ -504,7 +486,6 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
<DmsCustomerSelector
jobid={jobId}
job={data?.jobs_by_pk}
bodyshop={bodyshop}
socket={activeSocket}
mode={mode}
@@ -541,14 +522,13 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
setLogLevel(value);
setActiveLogLevel(value);
}}
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" }
]}
/>
>
<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>
<Button onClick={() => setLogs([])}>Clear Logs</Button>
<Button
onClick={() => {

View File

@@ -1,12 +1,9 @@
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 { useQuery } from "@apollo/client/react";
import { Card, Col, Result, Row, Space, Typography } from "antd";
import { useEffect } 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";
@@ -22,26 +19,13 @@ 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 RREarlyROModal from "../../components/dms-post-form/rr-early-ro-modal";
import { GET_JOB_BY_PK, CONVERT_JOB_TO_RO } from "../../graphql/jobs.queries";
import { GET_JOB_BY_PK } 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)),
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
setSelectedHeader: (key) => dispatch(setSelectedHeader(key))
});
const colSpan = {
@@ -55,36 +39,14 @@ const cardStyle = {
height: "100%"
};
export function JobsCloseContainer({ setBreadcrumbs, setSelectedHeader, bodyshop, insertAuditTrail }) {
export function JobsCloseContainer({ setBreadcrumbs, setSelectedHeader }) {
const { jobId } = useParams();
const { loading, error, data, refetch } = useQuery(GET_JOB_BY_PK, {
const { loading, error, data } = 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", {
@@ -113,55 +75,6 @@ export function JobsCloseContainer({ setBreadcrumbs, setSelectedHeader, bodyshop
]);
}, [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 />;
@@ -186,16 +99,6 @@ export function JobsCloseContainer({ setBreadcrumbs, setSelectedHeader, bodyshop
<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>
@@ -221,173 +124,8 @@ export function JobsCloseContainer({ setBreadcrumbs, setSelectedHeader, bodyshop
</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(mapStateToProps, mapDispatchToProps)(JobsCloseContainer);
export default connect(null, mapDispatchToProps)(JobsCloseContainer);

View File

@@ -9,7 +9,6 @@ import {
Form,
Input,
InputNumber,
Modal,
Popconfirm,
Row,
Select,
@@ -43,7 +42,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, DMS_MAP, getDmsMode } from "../../utils/dmsUtils.js";
import { bodyshopHasDmsKey } from "../../utils/dmsUtils.js";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -72,11 +71,6 @@ 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 }
@@ -88,18 +82,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"),
description: t("jobs.errors.missingjoblineids")
message: t("jobs.errors.missingjoblineids")
});
setLoading(false);
return;
}
const result = await client.mutate({
mutation: generateJobLinesUpdatesForInvoicing(values.joblines)
});
@@ -214,17 +208,9 @@ export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail, set
</Button>
</Popconfirm>
{bodyshopHasDmsKey(bodyshop) && (
<>
{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>
)}
</>
<Link to={`/manage/dms?jobId=${job.id}`}>
<Button disabled={job.date_exported || !jobRO}>{t("jobs.actions.sendtodms")}</Button>
</Link>
)}
<Button
onClick={() => {
@@ -440,15 +426,13 @@ export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail, set
}
]}
>
<Select
style={{ minWidth: "12rem" }}
disabled={jobRO}
options={bodyshop.md_ins_cos.map((s) => ({
key: s.name,
value: s.name,
label: s.name
}))}
/>
<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>
</Form.Item>
<Form.Item
@@ -526,7 +510,7 @@ export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail, set
<Statistic
title={t("jobs.labels.pimraryamountpayable")}
styles={{
content: {
value: {
color: discrep.getAmount() >= 0 ? "green" : "red"
}
}}
@@ -543,30 +527,6 @@ 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>
);
}

View File

@@ -23,17 +23,10 @@ export function TasksPageComponent({ bodyshop, currentUser, type }) {
relationshipType={"assigned_to"}
query={{ QUERY_MY_TASKS_PAGINATED }}
titleTranslation={"tasks.titles.my_tasks"}
disableJobRefetch={true}
/>
);
case taskPageTypes.ALL_TASKS:
return (
<TaskListContainer
query={{ QUERY_ALL_TASKS_PAGINATED }}
titleTranslation={"tasks.titles.all_tasks"}
disableJobRefetch={true}
/>
);
return <TaskListContainer query={{ QUERY_ALL_TASKS_PAGINATED }} titleTranslation={"tasks.titles.all_tasks"} />;
default:
return <></>;
}

View File

@@ -17,10 +17,7 @@ import "./tech.page.styles.scss";
import UpsellComponent, { upsellEnum } from "../../components/upsell/upsell.component.jsx";
import { lazyDev } from "../../utils/lazyWithPreload.jsx";
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 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")
@@ -37,9 +34,7 @@ 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;
@@ -75,8 +70,6 @@ export function TechPage({ technician }) {
<TechHeader />
<TaskUpsertModalContainer />
<NoteUpsertModal />
<Content className="tech-content-container">
<ErrorBoundary>
<Suspense

View File

@@ -48,7 +48,6 @@
"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}}.",
@@ -60,17 +59,18 @@
"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",
"vehicle": "Vehicle"
"ro_number": "RO #",
"owner": "Owner",
"vehicle": "Vehicle",
"bp": "B/P",
"scheduled_completion": "Scheduled Completion"
},
"successes": {
"canceled": "Appointment canceled successfully.",
@@ -90,11 +90,6 @@
"actions": "Actions"
}
},
"audio": {
"manager": {
"description": "Click anywhere to enable the message ding."
}
},
"audit": {
"fields": {
"cc": "CC",
@@ -154,6 +149,11 @@
"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}}"
"saving": "Error encountered while saving. {{message}}",
"duplicate_insurance_company": "Duplicate insurance company name. Each insurance company name must be unique"
},
"fields": {
"ReceivableCustomField": "QBO Receivable Custom Field {{number}}",
@@ -564,18 +564,21 @@
"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",
@@ -594,7 +597,6 @@
"local_tax": "Local Tax",
"mapa": "Paint Materials",
"mash": "Shop Materials",
"nontaxable": "Non-taxable",
"paa": "Aftermarket",
"pac": "Chrome",
"pag": "Glass",
@@ -615,8 +617,6 @@
"state": "State Tax Applies"
},
"state_tax": "State Tax",
"taxable": "Taxable",
"taxable_flag": "Taxable?",
"tow": "Towing"
},
"schedule_end_time": "Schedule Ending Time",
@@ -678,6 +678,8 @@
"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",
@@ -700,11 +702,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",
@@ -731,8 +733,6 @@
"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,9 +1047,7 @@
},
"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.",
"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."
"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."
},
"labels": {
"refreshallocations": "Refresh to see DMS Allocations."
@@ -1230,6 +1228,8 @@
},
"general": {
"actions": {
"select": "Select",
"optional": "Optional",
"add": "Add",
"autoupdate": "{{app}} will automatically update in {{time}} seconds. Please save all changes.",
"calculate": "Calculate",
@@ -1246,11 +1246,9 @@
"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",
@@ -1261,7 +1259,6 @@
"save": "Save",
"saveandnew": "Save and New",
"saveas": "Save As",
"select": "Select",
"selectall": "Select All",
"send": "Send",
"sendbysms": "Send by SMS",
@@ -1291,7 +1288,8 @@
"vehicle": "Vehicle"
},
"labels": {
"apply": "Apply",
"selected": "Selected",
"settings": "Settings",
"actions": "Actions",
"areyousure": "Are you sure?",
"barcode": "Barcode",
@@ -1345,10 +1343,8 @@
"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",
@@ -1591,13 +1587,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"
"presets": "Jobline Presets",
"bulk_location_help": "This will set the same location on all selected lines."
},
"successes": {
"created": "Job line created successfully.",
@@ -1625,13 +1621,11 @@
"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",
@@ -1640,7 +1634,6 @@
},
"post": "Post",
"refetchmakesmodels": "Refetch Make and Model Codes",
"update_ro": "Update RO",
"usegeneric": "Use Generic Customer",
"useselected": "Use Selected Customer"
},
@@ -1708,9 +1701,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",
@@ -1791,8 +1784,9 @@
"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": {
@@ -1800,19 +1794,17 @@
},
"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 #",
"last_name": "Last Name",
"lines": "Posting Lines",
"make_override": "Make Override",
"advisor": "Advisor #",
"lines": "Posting Lines",
"name1": "Customer Name",
"payer": {
"amount": "Amount",
@@ -1825,11 +1817,7 @@
"sale": "Sale",
"sale_dms_acctnumber": "Sale DMS Acct #",
"story": "Story",
"vinowner": "VIN Owner",
"rr_opcode": "RR OpCode",
"rr_opcode_prefix": "Prefix",
"rr_opcode_suffix": "Suffix",
"rr_opcode_base": "Base"
"vinowner": "VIN Owner"
},
"dms_allocation": "DMS Allocation",
"driveable": "Driveable",
@@ -1957,7 +1945,7 @@
"amount": "Amount",
"name": "Name"
},
"queued_for_parts": "Queued",
"queued_for_parts": "Queued for Parts",
"rate_ats": "ATS Rate",
"rate_ats_flat": "ATS Flat Rate",
"rate_la1": "LA1",
@@ -2114,11 +2102,6 @@
"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",
@@ -2276,7 +2259,6 @@
"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.",
@@ -2465,7 +2447,6 @@
"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",
@@ -2478,7 +2459,8 @@
"selectmedia": "Select Media",
"sentby": "Sent by {{by}} at {{time}}",
"typeamessage": "Send a message...",
"unarchive": "Unarchive"
"unarchive": "Unarchive",
"mark_unread": "Mark as unread"
},
"render": {
"conversation_list": "Conversation List"
@@ -2632,20 +2614,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",
"home": "Home",
"other": "Other",
"phone": "Phone",
"relatedjobs": "Related Jobs",
"sms": "SMS",
"updateowner": "Update Owner",
"work": "Work"
"work": "Work",
"home": "Home",
"cell": "Cell",
"other": "Other",
"email": "Email",
"phone": "Phone",
"sms": "SMS"
},
"successes": {
"delete": "Owner deleted successfully.",
@@ -2656,10 +2638,6 @@
"actions": {
"order": "Order Parts",
"orderinhouse": "Order as In House"
},
"labels": {
"view_counts_only": "View Parts Counts Only",
"view_timestamps": "Show timestamps"
}
},
"parts_dispatch": {
@@ -3009,6 +2987,8 @@
"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",
@@ -3027,7 +3007,6 @@
"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",
@@ -3048,7 +3027,6 @@
"orientation": "Board Orientation",
"ownr_nm": "Customer Name",
"paintpriority": "P/P",
"partsreceived": "Parts Received",
"partsstatus": "Parts Status",
"production_note": "Production Note",
"refinishhours": "R",
@@ -3595,12 +3573,17 @@
}
},
"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",
@@ -3632,9 +3615,7 @@
"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",
@@ -3646,7 +3627,6 @@
"shop-csi": "CSI Responses",
"shop-templates": "Shop Templates",
"shop-vendors": "Vendors",
"simplified-parts-jobs": "Jobs",
"tasks": "Tasks",
"temporarydocs": "Temporary Documents",
"timetickets": "Time Tickets",
@@ -3682,9 +3662,7 @@
"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}}",
@@ -3700,7 +3678,6 @@
"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}}",
@@ -3861,10 +3838,10 @@
"user": {
"actions": {
"changepassword": "Change Password",
"dark_theme": "Switch to Dark Theme",
"light_theme": "Switch to Light Theme",
"signout": "Sign Out",
"updateprofile": "Update Profile"
"updateprofile": "Update Profile",
"light_theme": "Switch to Light Theme",
"dark_theme": "Switch to Dark Theme"
},
"errors": {
"updating": "Error updating user or association {{message}}"
@@ -3878,14 +3855,14 @@
"labels": {
"actions": "Actions",
"changepassword": "Change Password",
"notification_sound_disabled": "Notification sound disabled",
"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"
"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."
},
"successess": {
"passwordchanged": "Password changed successfully. "

View File

@@ -48,7 +48,6 @@
"arrivedon": "Llegado el:",
"arrivingjobs": "",
"blocked": "",
"bp": "",
"cancelledappointment": "Cita cancelada para:",
"completingjobs": "",
"dataconsistency": "",
@@ -60,17 +59,18 @@
"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": "",
"vehicle": ""
"ro_number": "",
"owner": "",
"vehicle": "",
"bp": "",
"scheduled_completion": ""
},
"successes": {
"canceled": "Cita cancelada con éxito.",
@@ -90,11 +90,6 @@
"actions": "Comportamiento"
}
},
"audio": {
"manager": {
"description": ""
}
},
"audit": {
"fields": {
"cc": "",
@@ -154,6 +149,11 @@
"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": ""
"saving": "",
"duplicate_insurance_company": ""
},
"fields": {
"ReceivableCustomField": "",
@@ -564,18 +564,21 @@
"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": "",
@@ -594,7 +597,6 @@
"local_tax": "",
"mapa": "",
"mash": "",
"nontaxable": "",
"paa": "",
"pac": "",
"pag": "",
@@ -615,8 +617,6 @@
"state": ""
},
"state_tax": "",
"taxable": "",
"taxable_flag": "",
"tow": ""
},
"schedule_end_time": "",
@@ -678,6 +678,8 @@
"zip_post": ""
},
"labels": {
"parts_shop_management": "",
"parts_vendor_management": "",
"2tiername": "",
"2tiersetup": "",
"2tiersource": "",
@@ -700,11 +702,11 @@
"payers": ""
},
"cdk_dealerid": "",
"rr_dealerid": "",
"costsmapping": "",
"dms_allocations": "",
"pbs_serialnumber": "",
"profitsmapping": "",
"rr_dealerid": "",
"title": ""
},
"emaillater": "",
@@ -731,8 +733,6 @@
"followers": ""
},
"orderstatuses": "",
"parts_shop_management": "",
"parts_vendor_management": "",
"partslocations": "",
"partsscan": "",
"printlater": "",
@@ -1047,9 +1047,7 @@
},
"dms": {
"errors": {
"alreadyexported": "",
"earlyrorequired": "",
"earlyrorequired.message": ""
"alreadyexported": ""
},
"labels": {
"refreshallocations": ""
@@ -1246,11 +1244,9 @@
"deselectall": "",
"download": "",
"edit": "Editar",
"gotoadmin": "",
"login": "",
"next": "",
"ok": "",
"optional": "",
"previous": "",
"print": "",
"refresh": "",
@@ -1261,7 +1257,6 @@
"save": "Salvar",
"saveandnew": "",
"saveas": "",
"select": "",
"selectall": "",
"send": "",
"sendbysms": "",
@@ -1291,8 +1286,9 @@
"vehicle": ""
},
"labels": {
"apply": "",
"selected": "",
"actions": "Comportamiento",
"settings": "",
"areyousure": "",
"barcode": "código de barras",
"cancel": "",
@@ -1345,10 +1341,8 @@
"search": "Buscar...",
"searchresults": "",
"selectdate": "",
"selected": "",
"sendagain": "",
"sendby": "",
"settings": "",
"signin": "",
"sms": "",
"status": "",
@@ -1591,13 +1585,13 @@
"labels": {
"adjustmenttobeadded": "",
"billref": "",
"bulk_location_help": "",
"convertedtolabor": "",
"edit": "Línea de edición",
"ioucreated": "",
"new": "Nueva línea",
"nostatus": "",
"presets": ""
"presets": "",
"bulk_location_help": ""
},
"successes": {
"created": "",
@@ -1625,13 +1619,11 @@
"changestatus": "Cambiar Estado",
"changestimator": "",
"convert": "Convertir",
"convertwithoutearlyro": "",
"createiou": "",
"deliver": "",
"deliver_quick": "",
"dms": {
"addpayer": "",
"createearlyro": "",
"createnewcustomer": "",
"findmakemodelcode": "",
"getmakes": "",
@@ -1640,7 +1632,6 @@
},
"post": "",
"refetchmakesmodels": "",
"update_ro": "",
"usegeneric": "",
"useselected": ""
},
@@ -1709,8 +1700,8 @@
"actual_in": "Real en",
"acv_amount": "",
"adjustment_bottom_line": "Ajustes",
"adjustmenthours": "",
"admin_clerk": "",
"adjustmenthours": "",
"alt_transport": "",
"area_of_damage_impact": {
"10": "",
@@ -1791,8 +1782,9 @@
"ded_status": "Estado deducible",
"depreciation_taxes": "Depreciación / Impuestos",
"dms": {
"first_name": "",
"last_name": "",
"address": "",
"advisor": "",
"amount": "",
"center": "",
"control_type": {
@@ -1800,36 +1792,29 @@
},
"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": "",
"payer_type": ""
"name": ""
},
"sale": "",
"sale_dms_acctnumber": "",
"story": "",
"vinowner": "",
"rr_opcode": "",
"rr_opcode_prefix": "",
"rr_opcode_suffix": "",
"rr_opcode_base": ""
"vinowner": ""
},
"dms_allocation": "",
"driveable": "",
@@ -2114,11 +2099,6 @@
"damageto": "",
"defaultstory": "",
"disablebillwip": "",
"earlyro": {
"created": "",
"fields": "",
"willupdate": ""
},
"invoicedatefuture": "",
"kmoutnotgreaterthankmin": "",
"logs": "",
@@ -2276,7 +2256,6 @@
"delete": "",
"deleted": "Trabajo eliminado con éxito.",
"duplicated": "",
"early_ro_created": "",
"exported": "",
"invoiced": "",
"ioucreated": "",
@@ -2465,7 +2444,6 @@
"labels": {
"addlabel": "",
"archive": "",
"mark_unread": "",
"maxtenimages": "",
"messaging": "Mensajería",
"no_consent": "",
@@ -2478,7 +2456,8 @@
"selectmedia": "",
"sentby": "",
"typeamessage": "Enviar un mensaje...",
"unarchive": ""
"unarchive": "",
"mark_unread": ""
},
"render": {
"conversation_list": ""
@@ -2632,20 +2611,20 @@
"name": ""
},
"labels": {
"cell": "",
"create_new": "Crea un nuevo registro de propietario.",
"deleteconfirm": "",
"email": "",
"existing_owners": "Propietarios existentes",
"fromclaim": "",
"fromowner": "",
"home": "",
"other": "",
"phone": "",
"relatedjobs": "",
"sms": "",
"updateowner": "",
"work": ""
"work": "",
"home": "",
"cell": "",
"other": "",
"email": "",
"phone": "",
"sms": ""
},
"successes": {
"delete": "",
@@ -2656,10 +2635,6 @@
"actions": {
"order": "Pedido de piezas",
"orderinhouse": ""
},
"labels": {
"view_counts_only": "",
"view_timestamps": ""
}
},
"parts_dispatch": {
@@ -3009,6 +2984,8 @@
"settings": ""
},
"labels": {
"click_for_statuses": "",
"partsreceived": "",
"actual_in": "",
"addnewprofile": "",
"alert": "",
@@ -3027,7 +3004,6 @@
"card_size": "",
"cardcolor": "",
"cardsettings": "",
"click_for_statuses": "",
"clm_no": "",
"comment": "",
"compact": "",
@@ -3048,7 +3024,6 @@
"orientation": "",
"ownr_nm": "",
"paintpriority": "",
"partsreceived": "",
"partsstatus": "",
"production_note": "",
"refinishhours": "",
@@ -3595,12 +3570,18 @@
}
},
"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": "",
@@ -3632,9 +3613,7 @@
"my_tasks": "",
"owner-detail": "",
"owners": "",
"parts": "",
"parts-queue": "",
"parts_settings": "",
"payments-all": "",
"phonebook": "",
"productionboard": "",
@@ -3646,7 +3625,6 @@
"shop-csi": "",
"shop-templates": "",
"shop-vendors": "",
"simplified-parts-jobs": "",
"tasks": "",
"temporarydocs": "",
"timetickets": "",
@@ -3682,9 +3660,7 @@
"my_tasks": "",
"owners": "Todos los propietarios | {{app}}",
"owners-detail": "",
"parts": "",
"parts-queue": "",
"parts_settings": "",
"payments-all": "",
"phonebook": "",
"productionboard": "",
@@ -3700,7 +3676,6 @@
"shop-csi": "",
"shop-templates": "",
"shop_vendors": "Vendedores | {{app}}",
"simplified-parts-jobs": "",
"tasks": "",
"techconsole": "{{app}}",
"techjobclock": "{{app}}",
@@ -3861,10 +3836,10 @@
"user": {
"actions": {
"changepassword": "",
"dark_theme": "",
"light_theme": "",
"signout": "desconectar",
"updateprofile": "Actualización del perfil"
"updateprofile": "Actualización del perfil",
"light_theme": "",
"dark_theme": ""
},
"errors": {
"updating": ""
@@ -3878,14 +3853,14 @@
"labels": {
"actions": "",
"changepassword": "",
"notification_sound_disabled": "",
"notification_sound_enabled": "",
"notification_sound_help": "",
"notification_sound_off": "",
"notification_sound_on": "",
"play_sound_for_new_messages": "",
"profileinfo": "",
"user_settings": ""
"user_settings": "",
"play_sound_for_new_messages": "",
"notification_sound_on": "",
"notification_sound_off": "",
"notification_sound_enabled": "",
"notification_sound_disabled": "",
"notification_sound_help": ""
},
"successess": {
"passwordchanged": ""

View File

@@ -48,7 +48,6 @@
"arrivedon": "Arrivé le:",
"arrivingjobs": "",
"blocked": "",
"bp": "",
"cancelledappointment": "Rendez-vous annulé pour:",
"completingjobs": "",
"dataconsistency": "",
@@ -60,17 +59,18 @@
"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": "",
"vehicle": ""
"ro_number": "",
"owner": "",
"vehicle": "",
"bp": "",
"scheduled_completion": ""
},
"successes": {
"canceled": "Rendez-vous annulé avec succès.",
@@ -90,11 +90,6 @@
"actions": "actes"
}
},
"audio": {
"manager": {
"description": ""
}
},
"audit": {
"fields": {
"cc": "",
@@ -154,6 +149,11 @@
"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": ""
"saving": "",
"duplicate_insurance_company": ""
},
"fields": {
"ReceivableCustomField": "",
@@ -564,18 +564,21 @@
"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": "",
@@ -594,7 +597,6 @@
"local_tax": "",
"mapa": "",
"mash": "",
"nontaxable": "",
"paa": "",
"pac": "",
"pag": "",
@@ -615,8 +617,6 @@
"state": ""
},
"state_tax": "",
"taxable": "",
"taxable_flag": "",
"tow": ""
},
"schedule_end_time": "",
@@ -678,6 +678,8 @@
"zip_post": ""
},
"labels": {
"parts_shop_management": "",
"parts_vendor_management": "",
"2tiername": "",
"2tiersetup": "",
"2tiersource": "",
@@ -700,11 +702,11 @@
"payers": ""
},
"cdk_dealerid": "",
"rr_dealerid": "",
"costsmapping": "",
"dms_allocations": "",
"pbs_serialnumber": "",
"profitsmapping": "",
"rr_dealerid": "",
"title": ""
},
"emaillater": "",
@@ -731,8 +733,6 @@
"followers": ""
},
"orderstatuses": "",
"parts_shop_management": "",
"parts_vendor_management": "",
"partslocations": "",
"partsscan": "",
"printlater": "",
@@ -1047,9 +1047,7 @@
},
"dms": {
"errors": {
"alreadyexported": "",
"earlyrorequired": "",
"earlyrorequired.message": ""
"alreadyexported": ""
},
"labels": {
"refreshallocations": ""
@@ -1246,11 +1244,9 @@
"deselectall": "",
"download": "",
"edit": "modifier",
"gotoadmin": "",
"login": "",
"next": "",
"ok": "",
"optional": "",
"previous": "",
"print": "",
"refresh": "",
@@ -1261,7 +1257,6 @@
"save": "sauvegarder",
"saveandnew": "",
"saveas": "",
"select": "",
"selectall": "",
"send": "",
"sendbysms": "",
@@ -1291,7 +1286,8 @@
"vehicle": ""
},
"labels": {
"apply": "",
"selected": "",
"settings": "",
"actions": "actes",
"areyousure": "",
"barcode": "code à barre",
@@ -1345,10 +1341,8 @@
"search": "Chercher...",
"searchresults": "",
"selectdate": "",
"selected": "",
"sendagain": "",
"sendby": "",
"settings": "",
"signin": "",
"sms": "",
"status": "",
@@ -1591,13 +1585,13 @@
"labels": {
"adjustmenttobeadded": "",
"billref": "",
"bulk_location_help": "",
"convertedtolabor": "",
"edit": "Ligne d'édition",
"ioucreated": "",
"new": "Nouvelle ligne",
"nostatus": "",
"presets": ""
"presets": "",
"bulk_location_help": ""
},
"successes": {
"created": "",
@@ -1625,13 +1619,11 @@
"changestatus": "Changer le statut",
"changestimator": "",
"convert": "Convertir",
"convertwithoutearlyro": "",
"createiou": "",
"deliver": "",
"deliver_quick": "",
"dms": {
"addpayer": "",
"createearlyro": "",
"createnewcustomer": "",
"findmakemodelcode": "",
"getmakes": "",
@@ -1640,7 +1632,6 @@
},
"post": "",
"refetchmakesmodels": "",
"update_ro": "",
"usegeneric": "",
"useselected": ""
},
@@ -1708,9 +1699,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": "",
@@ -1791,8 +1782,9 @@
"ded_status": "Statut de franchise",
"depreciation_taxes": "Amortissement / taxes",
"dms": {
"first_name": "",
"last_name": "",
"address": "",
"advisor": "",
"amount": "",
"center": "",
"control_type": {
@@ -1800,36 +1792,29 @@
},
"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": "",
"payer_type": ""
"name": ""
},
"sale": "",
"sale_dms_acctnumber": "",
"story": "",
"vinowner": "",
"rr_opcode": "",
"rr_opcode_prefix": "",
"rr_opcode_suffix": "",
"rr_opcode_base": ""
"vinowner": ""
},
"dms_allocation": "",
"driveable": "",
@@ -2114,11 +2099,6 @@
"damageto": "",
"defaultstory": "",
"disablebillwip": "",
"earlyro": {
"created": "",
"fields": "",
"willupdate": ""
},
"invoicedatefuture": "",
"kmoutnotgreaterthankmin": "",
"logs": "",
@@ -2276,7 +2256,6 @@
"delete": "",
"deleted": "Le travail a bien été supprimé.",
"duplicated": "",
"early_ro_created": "",
"exported": "",
"invoiced": "",
"ioucreated": "",
@@ -2454,6 +2433,7 @@
"actions": {
"link": "",
"new": "",
"openchat": ""
},
"errors": {
@@ -2465,7 +2445,6 @@
"labels": {
"addlabel": "",
"archive": "",
"mark_unread": "",
"maxtenimages": "",
"messaging": "Messagerie",
"no_consent": "",
@@ -2478,7 +2457,8 @@
"selectmedia": "",
"sentby": "",
"typeamessage": "Envoyer un message...",
"unarchive": ""
"unarchive": "",
"mark_unread": ""
},
"render": {
"conversation_list": ""
@@ -2632,20 +2612,20 @@
"name": ""
},
"labels": {
"cell": "",
"create_new": "Créez un nouvel enregistrement de propriétaire.",
"deleteconfirm": "",
"email": "",
"existing_owners": "Propriétaires existants",
"fromclaim": "",
"fromowner": "",
"home": "",
"other": "",
"phone": "",
"relatedjobs": "",
"sms": "",
"updateowner": "",
"work": ""
"work": "",
"home": "",
"cell": "",
"other": "",
"email": "",
"phone": "",
"sms": ""
},
"successes": {
"delete": "",
@@ -2656,10 +2636,6 @@
"actions": {
"order": "Commander des pièces",
"orderinhouse": ""
},
"labels": {
"view_counts_only": "",
"view_timestamps": ""
}
},
"parts_dispatch": {
@@ -3009,6 +2985,8 @@
"settings": ""
},
"labels": {
"click_for_statuses": "",
"partsreceived": "",
"actual_in": "",
"addnewprofile": "",
"alert": "",
@@ -3027,7 +3005,6 @@
"card_size": "",
"cardcolor": "",
"cardsettings": "",
"click_for_statuses": "",
"clm_no": "",
"comment": "",
"compact": "",
@@ -3048,7 +3025,6 @@
"orientation": "",
"ownr_nm": "",
"paintpriority": "",
"partsreceived": "",
"partsstatus": "",
"production_note": "",
"refinishhours": "",
@@ -3595,12 +3571,18 @@
}
},
"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": "",
@@ -3632,9 +3614,7 @@
"my_tasks": "",
"owner-detail": "",
"owners": "",
"parts": "",
"parts-queue": "",
"parts_settings": "",
"payments-all": "",
"phonebook": "",
"productionboard": "",
@@ -3646,7 +3626,6 @@
"shop-csi": "",
"shop-templates": "",
"shop-vendors": "",
"simplified-parts-jobs": "",
"tasks": "",
"temporarydocs": "",
"timetickets": "",
@@ -3682,9 +3661,7 @@
"my_tasks": "",
"owners": "Tous les propriétaires | {{app}}",
"owners-detail": "",
"parts": "",
"parts-queue": "",
"parts_settings": "",
"payments-all": "",
"phonebook": "",
"productionboard": "",
@@ -3700,7 +3677,6 @@
"shop-csi": "",
"shop-templates": "",
"shop_vendors": "Vendeurs | {{app}}",
"simplified-parts-jobs": "",
"tasks": "",
"techconsole": "{{app}}",
"techjobclock": "{{app}}",
@@ -3861,10 +3837,10 @@
"user": {
"actions": {
"changepassword": "",
"dark_theme": "",
"light_theme": "",
"signout": "Déconnexion",
"updateprofile": "Mettre à jour le profil"
"updateprofile": "Mettre à jour le profil",
"light_theme": "",
"dark_theme": ""
},
"errors": {
"updating": ""
@@ -3878,14 +3854,14 @@
"labels": {
"actions": "",
"changepassword": "",
"notification_sound_disabled": "",
"notification_sound_enabled": "",
"notification_sound_help": "",
"notification_sound_off": "",
"notification_sound_on": "",
"play_sound_for_new_messages": "",
"profileinfo": "",
"user_settings": ""
"user_settings": "",
"play_sound_for_new_messages": "",
"notification_sound_on": "",
"notification_sound_off": "",
"notification_sound_enabled": "",
"notification_sound_disabled": "",
"notification_sound_help": ""
},
"successess": {
"passwordchanged": ""

View File

@@ -1,43 +1,44 @@
import { Select } from "antd";
import i18n from "../translations/i18n";
export default function CiecaSelect(parts = true, labor = true) {
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;
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>
</>
)}
</>
);
}
export function GetPartTypeName(part_type) {

View File

@@ -5,10 +5,8 @@ export function DateFormatter(props) {
return props.children ? dayjs(props.children).format(props.includeDay ? "ddd MM/DD/YYYY" : "MM/DD/YYYY") : 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 DateTimeFormatter(props) {
return props.children ? dayjs(props.children).format(props.format ? props.format : "MM/DD/YYYY hh:mm a") : null;
}
export function DateTimeFormatterFunction(date) {
@@ -19,11 +17,11 @@ export function TimeFormatter(props) {
return props.children ? dayjs(props.children).format(props.format ? props.format : "hh:mm a") : null;
}
export function TimeAgoFormatter({ removeAgoString = false, ...props }) {
export function TimeAgoFormatter(props) {
const m = dayjs(props.children);
return props.children ? (
<Tooltip placement="top" title={m.format("MM/DD/YYYY hh:mm A")}>
{m.fromNow(removeAgoString)}
<Tooltip placement="top" title={m.format("MM/DD/YYY hh:mm A")}>
{m.fromNow()}
</Tooltip>
) : null;
}

View File

@@ -248,8 +248,7 @@ const client = new ApolloClient({
watchQuery: {
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
errorPolicy: "ignore",
notifyOnNetworkStatusChange: false
errorPolicy: "ignore"
},
query: {
fetchPolicy: "network-only",

View File

@@ -146,8 +146,7 @@ export async function generateTemplate(
if (templateQueryToExecute) {
const { data } = await client.query({
query: gql(finalQuery),
variables: { ...templateObject.variables },
fetchPolicy: "no-cache"
variables: { ...templateObject.variables }
});
contextData = data;
}

View File

@@ -38,6 +38,8 @@ services:
condition: service_healthy
localstack:
condition: service_healthy
aws-cli:
condition: service_completed_successfully
ports:
- "4001:4000" # Different external port for local access
volumes:
@@ -63,6 +65,8 @@ services:
condition: service_healthy
localstack:
condition: service_healthy
aws-cli:
condition: service_completed_successfully
ports:
- "4002:4000" # Different external port for local access
volumes:
@@ -88,6 +92,8 @@ services:
condition: service_healthy
localstack:
condition: service_healthy
aws-cli:
condition: service_completed_successfully
ports:
- "4003:4000" # Different external port for local access
volumes:
@@ -150,18 +156,23 @@ services:
# LocalStack
localstack:
image: localstack/localstack:4.13.1
image: localstack/localstack
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
env_file:
- .env.localstack.docker
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
ports:
- "4566:4566"
healthcheck:
@@ -171,6 +182,36 @@ 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

View File

@@ -68,18 +68,23 @@ 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:4.13.1
image: localstack/localstack
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
env_file:
- .env.localstack.docker
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
ports:
- "4566:4566"
healthcheck:
@@ -89,6 +94,38 @@ 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:
@@ -108,7 +145,8 @@ services:
condition: service_healthy
localstack:
condition: service_healthy
aws-cli:
condition: service_completed_successfully
ports:
- "4000:4000"
- "9229:9229"

View File

@@ -947,7 +947,6 @@
- carfax_exclude
- cdk_configuration
- cdk_dealerid
- chatter_company_id
- chatterid
- city
- claimscorpid
@@ -1064,7 +1063,6 @@
- bill_allow_post_to_closed
- bill_tax_rates
- cdk_configuration
- chatter_company_id
- city
- country
- created_at
@@ -3704,9 +3702,7 @@
- ded_status
- deliverchecklist
- depreciation_taxes
- dms_advisor_id
- dms_allocation
- dms_customer_id
- dms_id
- driveable
- employee_body
@@ -3987,9 +3983,7 @@
- ded_status
- deliverchecklist
- depreciation_taxes
- dms_advisor_id
- dms_allocation
- dms_customer_id
- dms_id
- driveable
- employee_body
@@ -4282,9 +4276,7 @@
- ded_status
- deliverchecklist
- depreciation_taxes
- dms_advisor_id
- dms_allocation
- dms_customer_id
- dms_id
- driveable
- employee_body

Some files were not shown because too many files have changed in this diff Show More