Merge branch 'master-AIO' into feature/IO-2776-cdk-fortellis

This commit is contained in:
Patrick Fic
2025-09-11 14:40:20 -07:00
19 changed files with 128 additions and 66 deletions

View File

@@ -233,9 +233,7 @@ export function App({
path="/parts/*"
element={
<ErrorBoundary>
<SocketProvider bodyshop={bodyshop} navigate={navigate} currentUser={currentUser}>
<PrivateRoute isAuthorized={currentUser.authorized} />
</SocketProvider>
<PrivateRoute isAuthorized={currentUser.authorized} />
</ErrorBoundary>
}
>

View File

@@ -109,6 +109,13 @@ export function BillsListTableComponent({
key: "vendorname",
sorter: (a, b) => alphaSort(a.vendor.name, b.vendor.name),
sortOrder: state.sortedInfo.columnKey === "vendorname" && state.sortedInfo.order,
filters: bills
? [...new Set(bills.map((bill) => bill.vendor.name))].map((name) => ({
text: name,
value: name
}))
: [],
onFilter: (value, record) => record.vendor.name === value,
render: (text, record) => <span>{record.vendor.name}</span>
},
{

View File

@@ -16,7 +16,7 @@ const mapStateToProps = createStructuredSelector({
export function GlobalFooter({ isPartsEntry }) {
const { t } = useTranslation();
if (isPartsEntry) {
return (
<Footer>
@@ -35,7 +35,6 @@ export function GlobalFooter({ isPartsEntry }) {
rome: t("titles.romeonline")
})} - ${import.meta.env.VITE_APP_GIT_SHA_DATE}`}
</div>
<WssStatusDisplayComponent />
<Link to="/disclaimer" target="_blank" style={{ color: "#ccc" }}>
Disclaimer & Notices
</Link>

View File

@@ -10,9 +10,12 @@ const mapStateToProps = createStructuredSelector({
const mapDispatchToProps = () => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export const DEFAULT_COL_LAYOUT = { xs: 24, sm: 24, md: 8, lg: 4, xl: 4, xxl: 4 };
export default connect(mapStateToProps, mapDispatchToProps)(JobPartsQueueCount);
export function JobPartsQueueCount({ bodyshop, parts, style }) {
export function JobPartsQueueCount({ bodyshop, parts, defaultColLayout = DEFAULT_COL_LAYOUT }) {
const partsStatus = useMemo(() => {
if (!parts) return null;
return parts.reduce(
@@ -35,35 +38,34 @@ export function JobPartsQueueCount({ bodyshop, parts, style }) {
}, [bodyshop, parts]);
if (!parts) return null;
return (
<Row style={style}>
<Col span={4}>
<Row>
<Col {...defaultColLayout}>
<Tooltip title="Total">
<Tag>{partsStatus.total}</Tag>
</Tooltip>
</Col>
<Col span={4}>
<Col {...defaultColLayout}>
<Tooltip title="No Status">
<Tag color="gold">{partsStatus["null"]}</Tag>
</Tooltip>
</Col>
<Col span={4}>
<Col {...defaultColLayout}>
<Tooltip title={bodyshop.md_order_statuses.default_ordered}>
<Tag color="blue">{partsStatus[bodyshop.md_order_statuses.default_ordered]}</Tag>
</Tooltip>
</Col>
<Col span={4}>
<Col {...defaultColLayout}>
<Tooltip title={bodyshop.md_order_statuses.default_received}>
<Tag color="green">{partsStatus[bodyshop.md_order_statuses.default_received]}</Tag>
</Tooltip>
</Col>
<Col span={4}>
<Col {...defaultColLayout}>
<Tooltip title={bodyshop.md_order_statuses.default_returned}>
<Tag color="orange">{partsStatus[bodyshop.md_order_statuses.default_returned]}</Tag>
</Tooltip>
</Col>
<Col span={4}>
<Col {...defaultColLayout}>
<Tooltip title={bodyshop.md_order_statuses.default_bo}>
<Tag color="red">{partsStatus[bodyshop.md_order_statuses.default_bo]}</Tag>
</Tooltip>

View File

@@ -233,7 +233,7 @@ export function PartsQueueListComponent({ bodyshop }) {
title: t("jobs.fields.partsstatus"),
dataIndex: "partsstatus",
key: "partsstatus",
render: (text, record) => <JobPartsQueueCount style={{ minWidth: "10rem" }} parts={record.joblines_status} />
render: (text, record) => <JobPartsQueueCount parts={record.joblines_status} />
},
{
title: t("jobs.fields.comment"),

View File

@@ -1183,7 +1183,17 @@ export function ShopInfoGeneral({ form, bodyshop }) {
{fields.map((field, index) => (
<Form.Item key={field.key}>
<LayoutFormRow noDivider>
<Form.Item label={t("general.labels.label")} key={`${index}label`} name={[field.name, "label"]}>
<Form.Item
label={t("general.labels.label")}
key={`${index}label`}
name={[field.name, "label"]}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
>
<Input />
</Form.Item>
<Form.Item
@@ -1294,7 +1304,17 @@ export function ShopInfoGeneral({ form, bodyshop }) {
{fields.map((field, index) => (
<Form.Item key={field.key}>
<LayoutFormRow noDivider>
<Form.Item label={t("general.labels.label")} key={`${index}label`} name={[field.name, "label"]}>
<Form.Item
label={t("general.labels.label")}
key={`${index}label`}
name={[field.name, "label"]}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
>
<Input />
</Form.Item>
<Form.Item
@@ -1483,15 +1503,31 @@ export function ShopInfoGeneral({ form, bodyshop }) {
{fields.map((field, index) => (
<Form.Item key={field.key}>
<LayoutFormRow noDivider>
<Form.Item label={t("general.labels.label")} key={`${index}label`} name={[field.name, "label"]}>
<Form.Item
label={t("general.labels.label")}
key={`${index}label`}
name={[field.name, "label"]}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("bodyshop.labels.md_to_emails_emails")}
key={`${index}emails`}
name={[field.name, "emails"]}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
>
<Select mode="tags" tokenSeparators={[",", ";"]} />
<FormItemEmail email={form.getFieldValue([field.name, "emails"])} />
</Form.Item>
<Space>

View File

@@ -147,7 +147,7 @@ export function SimplifiedPartsJobsListComponent({
title: t("jobs.fields.partsstatus"),
dataIndex: "partsstatus",
key: "partsstatus",
render: (text, record) => <JobPartsQueueCount style={{ minWidth: "10rem" }} parts={record.joblines_status} />
render: (text, record) => <JobPartsQueueCount parts={record.joblines_status} />
},
{
title: t("jobs.fields.comment"),

View File

@@ -14,16 +14,18 @@ import VendorsPhonebookAdd from "../vendors-phonebook-add/vendors-phonebook-add.
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { selectIsPartsEntry } from "../../redux/application/application.selectors.js";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
bodyshop: selectBodyshop,
isPartsEntry: selectIsPartsEntry
});
const mapDispatchToProps = () => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(VendorsFormComponent);
export function VendorsFormComponent({ bodyshop, form, formLoading, handleDelete, selectedvendor }) {
export function VendorsFormComponent({ bodyshop, form, formLoading, handleDelete, selectedvendor, isPartsEntry }) {
const { t } = useTranslation();
const client = useApolloClient();
@@ -57,8 +59,7 @@ export function VendorsFormComponent({ bodyshop, form, formLoading, handleDelete
>
{t("general.actions.delete")}
</Button>
<VendorsPhonebookAdd form={form} disabled={form.isFieldsTouched()} />
{!isPartsEntry && <VendorsPhonebookAdd form={form} disabled={form.isFieldsTouched()} />}
</Space>
}
/>
@@ -148,12 +149,18 @@ export function VendorsFormComponent({ bodyshop, form, formLoading, handleDelete
</Form.Item>
</LayoutFormRow>
<LayoutFormRow grow>
<Form.Item label={t("vendors.fields.discount")} name="discount">
<InputNumber min={0} max={1} precision={2} step={0.01} />
</Form.Item>
<Form.Item label={t("vendors.fields.due_date")} name="due_date">
<InputNumber min={0} />
</Form.Item>
{!isPartsEntry && (
<>
<Form.Item label={t("vendors.fields.discount")} name="discount">
<InputNumber min={0} max={1} precision={2} step={0.01} />
</Form.Item>
<Form.Item label={t("vendors.fields.due_date")} name="due_date">
<InputNumber min={0} />
</Form.Item>
</>
)}
{
// <Form.Item
// label={t("vendors.fields.cost_center")}

View File

@@ -1,5 +1,6 @@
import { EditFilled, SyncOutlined } from "@ant-design/icons";
import { Button, Card, Checkbox, Input, Space, Table, Typography } from "antd";
import { useQuery } from "@apollo/client";
import axios from "axios";
import queryString from "query-string";
import { useEffect, useState } from "react";
@@ -16,6 +17,7 @@ import { TemplateList } from "../../utils/TemplateConstants";
import { pageLimit } from "../../utils/config";
import { alphaSort, dateSort } from "../../utils/sorters";
import useLocalStorage from "../../utils/useLocalStorage";
import { QUERY_ALL_VENDORS } from "../../graphql/vendors.queries";
const mapDispatchToProps = (dispatch) => ({
setBillEnterContext: (context) => dispatch(setModalContext({ context: context, modal: "billEnter" }))
@@ -33,25 +35,22 @@ export function BillsListPage({ loading, data, refetch, total, setBillEnterConte
});
const Templates = TemplateList("bill");
const { t } = useTranslation();
const { data: vendorsData } = useQuery(QUERY_ALL_VENDORS);
const columns = [
{
title: t("bills.fields.vendorname"),
dataIndex: "vendorname",
key: "vendorname",
// sortObject: (direction) => {
// return {
// vendor: {
// name: direction
// ? direction === "descend"
// ? "desc"
// : "asc"
// : "desc",
// },
// };
// },
// sorter: (a, b) => alphaSort(a.vendor.name, b.vendor.name),
// sortOrder:
// state.sortedInfo.columnKey === "vendorname" && state.sortedInfo.order,
sorter: (a, b) => alphaSort(a.vendor.name, b.vendor.name),
sortObject: (order) => ({
vendor: { name: order === "descend" ? "desc" : "asc" }
}),
filters: (vendorsData?.vendors || []).map((v) => ({ text: v.name, value: v.id })),
filteredValue: state.filteredInfo.vendorname || null,
onFilter: (value, record) => record.vendorid === value,
sortOrder: state.sortedInfo.columnKey === "vendorname" && state.sortedInfo.order,
render: (text, record) => <span>{record.vendor.name}</span>
},
{
@@ -65,20 +64,11 @@ export function BillsListPage({ loading, data, refetch, total, setBillEnterConte
title: t("jobs.fields.ro_number"),
dataIndex: "ro_number",
key: "ro_number",
// sortObject: (direction) => {
// return {
// job: {
// ro_number: direction
// ? direction === "descend"
// ? "desc"
// : "asc"
// : "desc",
// },
// };
// },
// sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
// sortOrder:
// state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
sortObject: (order) => ({
job: { ro_number: order === "descend" ? "desc" : "asc" }
}),
sortOrder: state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
render: (text, record) => record.job && <Link to={`/manage/jobs/${record.job.id}`}>{record.job.ro_number}</Link>
},
{
@@ -175,7 +165,8 @@ export function BillsListPage({ loading, data, refetch, total, setBillEnterConte
];
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
// Persist filters (including vendorname) and sorting
setState({ ...state, filteredInfo: { ...state.filteredInfo, ...filters }, sortedInfo: sorter });
search.page = pagination.current;
if (sorter && sorter.column && sorter.column.sortObject) {
search.searchObj = JSON.stringify(sorter.column.sortObject(sorter.order));

View File

@@ -959,6 +959,7 @@
- enforce_referral
- entegral_configuration
- entegral_id
- external_shop_id
- features
- federal_tax_id
- id
@@ -1012,6 +1013,8 @@
- prodtargethrs
- production_config
- region_config
- rr_configuration
- rr_dealerid
- schedule_end_time
- schedule_start_time
- scoreboard_target
@@ -1035,7 +1038,6 @@
- use_fippa
- use_paint_scale_data
- uselocalmediaserver
- external_shop_id
- website
- workingdays
- zip_post
@@ -1068,6 +1070,7 @@
- enforce_conversion_category
- enforce_conversion_csr
- enforce_referral
- external_shop_id
- federal_tax_id
- id
- inhousevendorid
@@ -1113,6 +1116,7 @@
- phone
- prodtargethrs
- production_config
- rr_configuration
- schedule_end_time
- schedule_start_time
- scoreboard_target
@@ -1131,7 +1135,6 @@
- use_fippa
- use_paint_scale_data
- uselocalmediaserver
- external_shop_id
- website
- workingdays
- zip_post

View File

@@ -0,0 +1,4 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- alter table "public"."bodyshops" add column "rr_configuration" jsonb
-- null default jsonb_build_object();

View File

@@ -0,0 +1,2 @@
alter table "public"."bodyshops" add column "rr_configuration" jsonb
null default jsonb_build_object();

View File

@@ -0,0 +1,4 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- alter table "public"."bodyshops" add column "rr_dealierid" text
-- null;

View File

@@ -0,0 +1,2 @@
alter table "public"."bodyshops" add column "rr_dealierid" text
null;

View File

@@ -0,0 +1 @@
alter table "public"."bodyshops" rename column "rr_dealerid" to "rr_dealierid";

View File

@@ -0,0 +1 @@
alter table "public"."bodyshops" rename column "rr_dealierid" to "rr_dealerid";

View File

@@ -88,7 +88,7 @@ const partsManagementDeprovisioning = async (req, res) => {
const { logger } = req;
const { shopId } = req.body;
if (process.env.NODE_ENV === "production") {
if (process.env.NODE_ENV === "production" || process.env.HOSTNAME?.endsWith("compute.internal")) {
return res.status(403).json({ error: "Deprovisioning not allowed in production environment." });
}

View File

@@ -91,7 +91,8 @@ const extractUpdatedJobLines = (addsChgs = {}, jobId, currentJobLineNotes = {})
unq_seq: parseInt(line.UniqueSequenceNum || 0, 10),
status: line.LineStatusCode || null,
line_desc: line.LineDesc || null,
manual_line: line.ManualLineInd !== undefined ? coerceManual(line.ManualLineInd) : null
manual_line: false
// manual_line: line.ManualLineInd !== undefined ? coerceManual(line.ManualLineInd) : null
};
const lineOut = { ...base };

View File

@@ -1,5 +1,6 @@
const express = require("express");
const router = express.Router();
const logger = require("../../server/utils/logger");
// Pull secrets from env
const { VSSTA_INTEGRATION_SECRET, PARTS_MANAGEMENT_INTEGRATION_SECRET } = process.env;
@@ -11,7 +12,7 @@ if (typeof VSSTA_INTEGRATION_SECRET === "string" && VSSTA_INTEGRATION_SECRET.len
router.post("/vssta", vsstaMiddleware, vsstaIntegration);
} else {
console.warn("VSSTA_INTEGRATION_SECRET is not set — skipping /vssta integration route");
logger.logger.warn("VSSTA_INTEGRATION_SECRET is not set — skipping /vssta integration route");
}
// Only load Parts Management routes if that secret is set
@@ -45,14 +46,17 @@ if (typeof PARTS_MANAGEMENT_INTEGRATION_SECRET === "string" && PARTS_MANAGEMENT_
);
// Deprovisioning route
router.post("/parts-management/deprovision", partsManagementIntegrationMiddleware, partsManagementDeprovisioning);
if (process.env.NODE_ENV !== "production" && !process.env.HOSTNAME?.endsWith("compute.internal")) {
logger.logger.warn("Parts Management Deprovisioning route has been loaded.");
router.post("/parts-management/deprovision", partsManagementIntegrationMiddleware, partsManagementDeprovisioning);
}
/**
* Route to handle Parts Management Provisioning
*/
router.post("/parts-management/provision", partsManagementIntegrationMiddleware, partsManagementProvisioning);
} else {
console.warn("PARTS_MANAGEMENT_INTEGRATION_SECRET is not set — skipping /parts-management/provision route");
logger.logger.warn("PARTS_MANAGEMENT_INTEGRATION_SECRET is not set — skipping /parts-management/provision route");
}
module.exports = router;