diff --git a/client/src/components/jobs-documents-imgproxy-gallery/jobs-documents-imgproxy-gallery.component.jsx b/client/src/components/jobs-documents-imgproxy-gallery/jobs-documents-imgproxy-gallery.component.jsx
index 8f78cf675..d34e0645e 100644
--- a/client/src/components/jobs-documents-imgproxy-gallery/jobs-documents-imgproxy-gallery.component.jsx
+++ b/client/src/components/jobs-documents-imgproxy-gallery/jobs-documents-imgproxy-gallery.component.jsx
@@ -3,7 +3,7 @@ import { Button, Card, Col, Row, Space } from "antd";
import axios from "axios";
import i18n from "i18next";
import { isFunction } from "lodash";
-import { useCallback, useEffect, useState } from "react";
+import { useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import Lightbox from "react-image-lightbox";
import "react-image-lightbox/style.css";
@@ -12,12 +12,12 @@ import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import DocumentsUploadImgproxyComponent from "../documents-upload-imgproxy/documents-upload-imgproxy.component";
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
+import LocalMediaGrid from "../jobs-documents-local-gallery/local-media-grid.component";
import UpsellComponent, { upsellEnum } from "../upsell/upsell.component";
import JobsDocumentsDownloadButton from "./jobs-document-imgproxy-gallery.download.component";
import JobsDocumentsGalleryReassign from "./jobs-document-imgproxy-gallery.reassign.component";
import JobsDocumentsDeleteButton from "./jobs-documents-imgproxy-gallery.delete.component";
import JobsDocumentsGallerySelectAllComponent from "./jobs-documents-imgproxy-gallery.selectall.component";
-import LocalMediaGrid from "../jobs-documents-local-gallery/local-media-grid.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
@@ -38,6 +38,9 @@ function JobsDocumentsImgproxyComponent({
const [galleryImages, setGalleryImages] = useState({ images: [], other: [] });
const { t } = useTranslation();
const [modalState, setModalState] = useState({ open: false, index: 0 });
+ const [previewUrls, setPreviewUrls] = useState({});
+ const [previewError, setPreviewError] = useState(null);
+ const previewUrlsRef = useRef({});
const fetchThumbnails = useCallback(() => {
fetchImgproxyThumbnails({ setStateCallback: setGalleryImages, jobId, billId });
@@ -49,8 +52,86 @@ function JobsDocumentsImgproxyComponent({
}
}, [data, fetchThumbnails]);
+ useEffect(() => {
+ return () => {
+ Object.values(previewUrlsRef.current).forEach(URL.revokeObjectURL);
+ };
+ }, []);
+
+ const selectedImage = modalState.open ? galleryImages.images[modalState.index] : null;
+
+ useEffect(() => {
+ if (!modalState.open || !selectedImage?.id) return;
+
+ if (previewUrlsRef.current[selectedImage.id]) {
+ setPreviewError(null);
+ return;
+ }
+
+ const controller = new AbortController();
+
+ async function loadPreviewImage() {
+ setPreviewError(null);
+
+ try {
+ const response = await axios.post(
+ "/media/imgproxy/original",
+ { documentId: selectedImage.id },
+ {
+ responseType: "blob",
+ signal: controller.signal
+ }
+ );
+ const blobUrl = URL.createObjectURL(response.data);
+
+ previewUrlsRef.current = {
+ ...previewUrlsRef.current,
+ [selectedImage.id]: blobUrl
+ };
+ setPreviewUrls(previewUrlsRef.current);
+ } catch (error) {
+ if (axios.isCancel?.(error) || error.name === "CanceledError") return;
+
+ console.error("Failed to fetch original image blob", error);
+ setPreviewError(error);
+ }
+ }
+
+ loadPreviewImage();
+
+ return () => {
+ controller.abort();
+ };
+ }, [modalState.open, selectedImage?.id]);
+
+ useEffect(() => {
+ if (modalState.open && !selectedImage) {
+ setModalState({ open: false, index: 0 });
+ }
+ }, [modalState.open, selectedImage]);
+
+ const openEditorForImage = useCallback((image) => {
+ if (!image?.id) return;
+
+ const newWindow = window.open(
+ `${window.location.protocol}//${window.location.host}/edit?documentId=${image.id}`,
+ "_blank",
+ "noopener,noreferrer"
+ );
+ if (newWindow) newWindow.opener = null;
+ }, []);
+
const hasMediaAccess = HasFeatureAccess({ bodyshop, featureName: "media" });
const hasMobileAccess = HasFeatureAccess({ bodyshop, featureName: "mobile" });
+ const previewSrc = selectedImage ? previewUrls[selectedImage.id] : null;
+ const getLightboxImageSrc = useCallback(
+ (index) => {
+ const image = galleryImages.images[index];
+ return image ? previewUrls[image.id] || image.src : undefined;
+ },
+ [galleryImages.images, previewUrls]
+ );
+
return (
@@ -147,30 +228,33 @@ function JobsDocumentsImgproxyComponent({
/>
- {modalState.open && (
+ {modalState.open && selectedImage && (
{
- const newWindow = window.open(
- `${window.location.protocol}//${window.location.host}/edit?documentId=${
- galleryImages.images[modalState.index].id
- }`,
- "_blank",
- "noopener,noreferrer"
- );
- if (newWindow) newWindow.opener = null;
+ openEditorForImage(selectedImage);
}}
/>
]}
- mainSrc={galleryImages.images[modalState.index].fullsize}
- nextSrc={galleryImages.images[(modalState.index + 1) % galleryImages.images.length].fullsize}
- prevSrc={
+ imageLoadErrorMessage={previewError ? t("general.errors.notfound") : undefined}
+ mainSrc={previewSrc || selectedImage.src}
+ mainSrcThumbnail={selectedImage.src}
+ nextSrc={getLightboxImageSrc((modalState.index + 1) % galleryImages.images.length)}
+ nextSrcThumbnail={galleryImages.images[(modalState.index + 1) % galleryImages.images.length]?.src}
+ prevSrc={getLightboxImageSrc(
+ (modalState.index + galleryImages.images.length - 1) % galleryImages.images.length
+ )}
+ prevSrcThumbnail={
galleryImages.images[(modalState.index + galleryImages.images.length - 1) % galleryImages.images.length]
- .fullsize
+ ?.src
}
- onCloseRequest={() => setModalState({ open: false, index: 0 })}
+ reactModalProps={{ ariaHideApp: false }}
+ onCloseRequest={() => {
+ setModalState({ open: false, index: 0 });
+ setPreviewError(null);
+ }}
onMovePrevRequest={() =>
setModalState({
...modalState,
diff --git a/client/src/components/owners-list/owners-list.component.jsx b/client/src/components/owners-list/owners-list.component.jsx
index eefec34ca..e6fe1efb9 100644
--- a/client/src/components/owners-list/owners-list.component.jsx
+++ b/client/src/components/owners-list/owners-list.component.jsx
@@ -11,12 +11,13 @@ import ResponsiveTable from "../responsive-table/responsive-table.component";
export default function OwnersListComponent({ loading, owners, total, refetch }) {
const search = queryString.parse(useLocation().search);
- const {
- page
- // sortcolumn, sortorder
- } = search;
+ const { page, pageSize } = search;
const history = useNavigate();
+ const currentPage = Number.parseInt(page || "1", 10);
+ const parsedPageSize = Number.parseInt(pageSize || String(pageLimit), 10);
+ const currentPageSize = Number.isNaN(parsedPageSize) ? pageLimit : parsedPageSize;
+
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: { text: "" }
@@ -71,10 +72,14 @@ export default function OwnersListComponent({ loading, owners, total, refetch })
];
const handleTableChange = (pagination, filters, sorter) => {
+ const nextPageSize = pagination?.pageSize || currentPageSize;
+ const pageSizeChanged = nextPageSize !== currentPageSize;
+
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
const updatedSearch = {
...search,
- page: pagination.current,
+ pageSize: nextPageSize,
+ page: pageSizeChanged ? 1 : pagination.current,
sortcolumn: sorter.columnKey,
sortorder: sorter.order
};
@@ -119,7 +124,7 @@ export default function OwnersListComponent({ loading, owners, total, refetch })
>
+
+
+
{
- const originalLine = linesToOrder?.[index];
- const jobLineId = isReturn ? originalLine?.joblineid : originalLine?.id;
-
- return {
- ...p,
- job_line_id: jobLineId,
- ...(isReturn && { cm_received: false })
- };
+ const forcedLines = buildSubmittedPartsOrderLines({
+ submittedLines,
+ linesToOrder,
+ isReturn
});
let insertResult;
@@ -147,10 +143,7 @@ export function PartsOrderModalContainer({
type: isReturn ? "jobspartsreturn" : "jobspartsorder"
});
- // Use linesToOrder from context instead of form values to preserve job line ids
- const jobLineIds = (linesToOrder ?? [])
- .filter((line) => (isReturn ? line.joblineid : line.id))
- .map((line) => (isReturn ? line.joblineid : line.id));
+ const jobLineIds = getSubmittedPartsOrderJobLineIds(forcedLines);
try {
const jobLinesResult = await updateJobLines({
@@ -206,23 +199,20 @@ export function PartsOrderModalContainer({
isinhouse: true,
date: dayjs(),
total: 0,
- billlines: forcedLines.map((p, index) => {
- const originalLine = linesToOrder?.[index];
- return {
- joblineid: isReturn ? originalLine?.joblineid : originalLine?.id,
- actual_price: p.act_price,
- actual_cost: 0, // p.act_price,
- line_desc: p.line_desc,
- line_remarks: p.line_remarks,
- part_type: p.part_type,
- quantity: p.quantity || 1,
- applicable_taxes: {
- local: false,
- state: false,
- federal: false
- }
- };
- })
+ billlines: forcedLines.map((p) => ({
+ joblineid: p.job_line_id,
+ actual_price: p.act_price,
+ actual_cost: 0, // p.act_price,
+ line_desc: p.line_desc,
+ line_remarks: p.line_remarks,
+ part_type: p.part_type,
+ quantity: p.quantity || 1,
+ applicable_taxes: {
+ local: false,
+ state: false,
+ federal: false
+ }
+ }))
}
}
});
diff --git a/client/src/components/parts-order-modal/parts-order-modal.utils.js b/client/src/components/parts-order-modal/parts-order-modal.utils.js
new file mode 100644
index 000000000..1517704ce
--- /dev/null
+++ b/client/src/components/parts-order-modal/parts-order-modal.utils.js
@@ -0,0 +1,23 @@
+export const getPartsOrderJobLineId = ({ line, originalLine, isReturn }) => {
+ return line?.job_line_id || (isReturn ? originalLine?.joblineid : originalLine?.id);
+};
+
+export const buildSubmittedPartsOrderLines = ({ submittedLines = [], linesToOrder = [], isReturn }) => {
+ return submittedLines.map((line, index) => {
+ const jobLineId = getPartsOrderJobLineId({
+ line,
+ originalLine: linesToOrder?.[index],
+ isReturn
+ });
+
+ return {
+ ...line,
+ job_line_id: jobLineId,
+ ...(isReturn && { cm_received: false })
+ };
+ });
+};
+
+export const getSubmittedPartsOrderJobLineIds = (partsOrderLines = []) => {
+ return partsOrderLines.map((line) => line.job_line_id).filter(Boolean);
+};
diff --git a/client/src/components/parts-order-modal/parts-order-modal.utils.test.js b/client/src/components/parts-order-modal/parts-order-modal.utils.test.js
new file mode 100644
index 000000000..1c7309680
--- /dev/null
+++ b/client/src/components/parts-order-modal/parts-order-modal.utils.test.js
@@ -0,0 +1,32 @@
+import { describe, expect, it } from "vitest";
+import { buildSubmittedPartsOrderLines, getSubmittedPartsOrderJobLineIds } from "./parts-order-modal.utils.js";
+
+describe("parts order modal utilities", () => {
+ it("preserves submitted job line ids after a row is removed", () => {
+ const submittedLines = [
+ { line_desc: "second line", job_line_id: "job-line-2" },
+ { line_desc: "third line", job_line_id: "job-line-3" }
+ ];
+ const linesToOrder = [{ id: "job-line-1" }, { id: "job-line-2" }, { id: "job-line-3" }];
+
+ const result = buildSubmittedPartsOrderLines({ submittedLines, linesToOrder, isReturn: false });
+
+ expect(result.map((line) => line.job_line_id)).toEqual(["job-line-2", "job-line-3"]);
+ expect(getSubmittedPartsOrderJobLineIds(result)).toEqual(["job-line-2", "job-line-3"]);
+ });
+
+ it("falls back to original return line ids when the form omits hidden metadata", () => {
+ const submittedLines = [{ line_desc: "return line" }];
+ const linesToOrder = [{ joblineid: "return-job-line-1" }];
+
+ const result = buildSubmittedPartsOrderLines({ submittedLines, linesToOrder, isReturn: true });
+
+ expect(result).toEqual([
+ {
+ line_desc: "return line",
+ job_line_id: "return-job-line-1",
+ cm_received: false
+ }
+ ]);
+ });
+});
diff --git a/client/src/components/parts-queue-list/parts-queue.list.component.jsx b/client/src/components/parts-queue-list/parts-queue.list.component.jsx
index 3cb5872f2..c14a62344 100644
--- a/client/src/components/parts-queue-list/parts-queue.list.component.jsx
+++ b/client/src/components/parts-queue-list/parts-queue.list.component.jsx
@@ -29,7 +29,10 @@ const mapStateToProps = createStructuredSelector({
export function PartsQueueListComponent({ bodyshop }) {
const searchParams = queryString.parse(useLocation().search);
- const { selected, sortcolumn, sortorder, statusFilters } = searchParams;
+ const { selected, sortcolumn, sortorder, statusFilters, page, pageSize } = searchParams;
+ const currentPage = Number.parseInt(page || "1", 10);
+ const parsedPageSize = Number.parseInt(pageSize || String(pageLimit), 10);
+ const currentPageSize = Number.isNaN(parsedPageSize) ? pageLimit : parsedPageSize;
const history = useNavigate();
const [filter, setFilter] = useLocalStorage("filter_parts_queue", null);
const [viewTimeStamp, setViewTimeStamp] = useLocalStorage("parts_queue_timestamps", false);
@@ -66,7 +69,11 @@ export function PartsQueueListComponent({ bodyshop }) {
: [];
const handleTableChange = (pagination, filters, sorter) => {
- // searchParams.page = pagination.current;
+ const nextPageSize = pagination?.pageSize || currentPageSize;
+ const pageSizeChanged = nextPageSize !== currentPageSize;
+
+ searchParams.pageSize = nextPageSize;
+ searchParams.page = pageSizeChanged ? 1 : pagination.current;
searchParams.sortcolumn = sorter.columnKey;
searchParams.sortorder = sorter.order;
@@ -315,9 +322,10 @@ export function PartsQueueListComponent({ bodyshop }) {
loading={loading}
pagination={{
placement: "top",
- pageSize: pageLimit
- // current: parseInt(page || 1),
- // total: data && data.jobs_aggregate.aggregate.count,
+ pageSize: currentPageSize,
+ current: currentPage,
+ showSizeChanger: true,
+ total: jobs.length
}}
columns={columns}
mobileColumnKeys={["ro_number", "ownr_ln", "status", "vehicle", "partsstatus"]}
diff --git a/client/src/components/print-center-jobs/print-center-jobs.component.jsx b/client/src/components/print-center-jobs/print-center-jobs.component.jsx
index a33a387e1..b0a412f40 100644
--- a/client/src/components/print-center-jobs/print-center-jobs.component.jsx
+++ b/client/src/components/print-center-jobs/print-center-jobs.component.jsx
@@ -12,7 +12,7 @@ import Jobd3RdPartyModal from "../job-3rd-party-modal/job-3rd-party-modal.compon
import PrintCenterItem from "../print-center-item/print-center-item.component";
import PrintCenterJobsLabels from "../print-center-jobs-labels/print-center-jobs-labels.component";
import PrintCenterSpeedPrint from "../print-center-speed-print/print-center-speed-print.component";
-import { bodyshopHasDmsKey } from "../../utils/dmsUtils";
+import { bodyshopHasDmsKey, DMS_MAP, getDmsMode } from "../../utils/dmsUtils";
import { selectTechnician } from "../../redux/tech/tech.selectors";
const mapStateToProps = createStructuredSelector({
@@ -36,6 +36,8 @@ export function PrintCenterJobsComponent({ printCenterModal, bodyshop, technicia
splitKey: bodyshop.imexshopid
});
const hasDMSKey = bodyshopHasDmsKey(bodyshop);
+ const dmsMode = getDmsMode(bodyshop, "off");
+ const isReynoldsMode = dmsMode === DMS_MAP.reynolds;
const Templates = !hasDMSKey
? Object.keys(tempList)
@@ -60,6 +62,7 @@ export function PrintCenterJobsComponent({ printCenterModal, bodyshop, technicia
(temp.regions && temp.regions[bodyshop.region_config]) ||
(temp.regions && bodyshop.region_config.includes(Object.keys(temp.regions)) === true)
)
+ .filter((temp) => !isReynoldsMode || !temp.excludedDmsModes?.includes(dmsMode))
.filter((temp) => !technician || temp.group !== "financial");
const JobsReportsList =
diff --git a/client/src/components/production-board-kanban/production-board-kanban-card.component.jsx b/client/src/components/production-board-kanban/production-board-kanban-card.component.jsx
index dbb96a6ba..c12a7c6c9 100644
--- a/client/src/components/production-board-kanban/production-board-kanban-card.component.jsx
+++ b/client/src/components/production-board-kanban/production-board-kanban-card.component.jsx
@@ -431,6 +431,7 @@ export default function ProductionBoardCard({ technician, card, bodyshop, cardSe
{
const { t } = useTranslation();
const calculateTotal = (items, key, subKey) => {
- return items.reduce((acc, item) => acc + (item[key]?.aggregate?.sum?.[subKey] || 0), 0);
+ return items.reduce((acc, item) => acc + (item?.[key]?.aggregate?.sum?.[subKey] ?? 0), 0);
};
const calculateTotalAmount = (items, key) => {
- return items.reduce((acc, item) => acc.add(Dinero(item[key]?.totals?.subtotal ?? Dinero())), Dinero({ amount: 0 }));
+ return items.reduce(
+ (acc, item) => acc.add(Dinero(item?.[key]?.totals?.subtotal ?? Dinero())),
+ Dinero({ amount: 0 })
+ );
};
const calculateReducerTotalAmount = (lanes, key) => {
@@ -67,58 +70,83 @@ const ProductionStatistics = ({ data, cardSettings, reducerData }) => {
return value;
};
+ const filteredData = cardSettings.excludeSuspended === true ? data.filter((item) => item.suspended !== true) : data;
+ const filteredReducerData =
+ cardSettings.excludeSuspended === true
+ ? {
+ ...reducerData,
+ lanes: reducerData.lanes.map((lane) => ({
+ ...lane,
+ cards: lane.cards.filter((card) => card.metadata.suspended !== true)
+ }))
+ }
+ : reducerData;
+
const totalHrs = cardSettings.totalHrs
- ? parseFloat((calculateTotal(data, "labhrs", "mod_lb_hrs") + calculateTotal(data, "larhrs", "mod_lb_hrs")).toFixed(2))
+ ? parseFloat(
+ (
+ calculateTotal(filteredData, "labhrs", "mod_lb_hrs") + calculateTotal(filteredData, "larhrs", "mod_lb_hrs")
+ ).toFixed(2)
+ )
: null;
const totalLAB = cardSettings.totalLAB
- ? parseFloat(calculateTotal(data, "labhrs", "mod_lb_hrs").toFixed(2))
+ ? parseFloat(calculateTotal(filteredData, "labhrs", "mod_lb_hrs").toFixed(2))
: null;
const totalLAR = cardSettings.totalLAR
- ? parseFloat(calculateTotal(data, "larhrs", "mod_lb_hrs").toFixed(2))
+ ? parseFloat(calculateTotal(filteredData, "larhrs", "mod_lb_hrs").toFixed(2))
: null;
- const jobsInProduction = cardSettings.jobsInProduction ? data.length : null;
+ const jobsInProduction = cardSettings.jobsInProduction ? filteredData.length : null;
const totalAmountInProduction = cardSettings.totalAmountInProduction
- ? calculateTotalAmount(data, "job_totals").toFormat("$0,0.00")
+ ? calculateTotalAmount(filteredData, "job_totals").toFormat("$0,0.00")
: null;
- const totalAmountOnBoard = reducerData && cardSettings.totalAmountOnBoard
- ? calculateReducerTotalAmount(reducerData.lanes, "job_totals").toFormat("$0,0.00")
- : null;
+ const totalAmountOnBoard =
+ filteredReducerData && cardSettings.totalAmountOnBoard
+ ? calculateReducerTotalAmount(filteredReducerData.lanes, "job_totals").toFormat("$0,0.00")
+ : null;
- const totalHrsOnBoard = reducerData && cardSettings.totalHrsOnBoard
- ? parseFloat((
- calculateReducerTotal(reducerData.lanes, "labhrs", "mod_lb_hrs") +
- calculateReducerTotal(reducerData.lanes, "larhrs", "mod_lb_hrs")
- ).toFixed(2))
- : null;
+ const totalHrsOnBoard =
+ filteredReducerData && cardSettings.totalHrsOnBoard
+ ? parseFloat(
+ (
+ calculateReducerTotal(filteredReducerData.lanes, "labhrs", "mod_lb_hrs") +
+ calculateReducerTotal(filteredReducerData.lanes, "larhrs", "mod_lb_hrs")
+ ).toFixed(2)
+ )
+ : null;
- const totalLABOnBoard = reducerData && cardSettings.totalLABOnBoard
- ? parseFloat(calculateReducerTotal(reducerData.lanes, "labhrs", "mod_lb_hrs").toFixed(2))
- : null;
+ const totalLABOnBoard =
+ filteredReducerData && cardSettings.totalLABOnBoard
+ ? parseFloat(calculateReducerTotal(filteredReducerData.lanes, "labhrs", "mod_lb_hrs").toFixed(2))
+ : null;
- const totalLAROnBoard = reducerData && cardSettings.totalLAROnBoard
- ? parseFloat(calculateReducerTotal(reducerData.lanes, "larhrs", "mod_lb_hrs").toFixed(2))
- : null;
+ const totalLAROnBoard =
+ filteredReducerData && cardSettings.totalLAROnBoard
+ ? parseFloat(calculateReducerTotal(filteredReducerData.lanes, "larhrs", "mod_lb_hrs").toFixed(2))
+ : null;
- const jobsOnBoard = reducerData && cardSettings.jobsOnBoard
- ? reducerData.lanes.reduce((acc, lane) => acc + lane.cards.length, 0)
- : null;
+ const jobsOnBoard =
+ filteredReducerData && cardSettings.jobsOnBoard
+ ? filteredReducerData.lanes.reduce((acc, lane) => acc + lane.cards.length, 0)
+ : null;
const tasksInProduction = cardSettings.tasksInProduction
- ? data.reduce((acc, item) => acc + (item.tasks_aggregate?.aggregate?.count || 0), 0)
+ ? filteredData.reduce((acc, item) => acc + (item.tasks_aggregate?.aggregate?.count || 0), 0)
: null;
- const tasksOnBoard = reducerData && cardSettings.tasksOnBoard
- ? reducerData.lanes.reduce((acc, lane) => {
- return (
- acc + lane.cards.reduce((laneAcc, card) => laneAcc + (card.metadata.tasks_aggregate?.aggregate?.count || 0), 0)
- );
- }, 0)
- : null;
+ const tasksOnBoard =
+ filteredReducerData && cardSettings.tasksOnBoard
+ ? filteredReducerData.lanes.reduce((acc, lane) => {
+ return (
+ acc +
+ lane.cards.reduce((laneAcc, card) => laneAcc + (card.metadata.tasks_aggregate?.aggregate?.count || 0), 0)
+ );
+ }, 0)
+ : null;
const statistics = mergeStatistics(statisticsItems, [
{ id: 0, value: totalHrs, type: StatisticType.HOURS },
diff --git a/client/src/components/production-board-kanban/settings/StatisticsSettings.jsx b/client/src/components/production-board-kanban/settings/StatisticsSettings.jsx
index 1645bc05d..d283f6122 100644
--- a/client/src/components/production-board-kanban/settings/StatisticsSettings.jsx
+++ b/client/src/components/production-board-kanban/settings/StatisticsSettings.jsx
@@ -14,7 +14,16 @@ const StatisticsSettings = ({ t, statisticsOrder, setStatisticsOrder, setHasChan
};
return (
-
+
+
+ {t("production.settings.statistics.exclude_suspended")}
+
+
+ }
+ >
{(provided) => (
diff --git a/client/src/components/production-board-kanban/settings/defaultKanbanSettings.js b/client/src/components/production-board-kanban/settings/defaultKanbanSettings.js
index 1c7a264b7..93486a11d 100644
--- a/client/src/components/production-board-kanban/settings/defaultKanbanSettings.js
+++ b/client/src/components/production-board-kanban/settings/defaultKanbanSettings.js
@@ -91,7 +91,8 @@ const defaultKanbanSettings = {
subtotal: false,
statisticsOrder: statisticsItems.map((item) => item.id),
selectedMdInsCos: [],
- selectedEstimators: []
+ selectedEstimators: [],
+ excludeSuspended: false
};
const defaultFilters = { search: "", employeeId: null, alert: false };
diff --git a/client/src/components/report-center-modal/report-center-modal.component.jsx b/client/src/components/report-center-modal/report-center-modal.component.jsx
index 7205939f2..77d006ae4 100644
--- a/client/src/components/report-center-modal/report-center-modal.component.jsx
+++ b/client/src/components/report-center-modal/report-center-modal.component.jsx
@@ -12,6 +12,7 @@ import { QUERY_ALL_VENDORS } from "../../graphql/vendors.queries";
import { selectReportCenter } from "../../redux/modals/modals.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import DatePickerRanges from "../../utils/DatePickerRanges";
+import { DMS_MAP, getDmsMode } from "../../utils/dmsUtils";
import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants";
import dayjs from "../../utils/day";
@@ -48,12 +49,18 @@ export function ReportCenterModalComponent({ reportCenterModal, bodyshop }) {
const [loading, setLoading] = useState(false);
const { t } = useTranslation();
const Templates = TemplateList("report_center");
+ const dmsMode = getDmsMode(bodyshop, "off");
+ const isReynoldsMode = dmsMode === DMS_MAP.reynolds;
const ReportsList = Object.keys(Templates)
.map((key) => Templates[key])
.filter((temp) => {
const enhancedPayrollOn = Enhanced_Payroll.treatment === "on";
const adpPayrollOn = ADPPayroll.treatment === "on";
+ if (isReynoldsMode && temp.excludedDmsModes?.includes(dmsMode)) {
+ return false;
+ }
+
if (enhancedPayrollOn && adpPayrollOn) {
return temp.enhanced_payroll !== false || temp.adp_payroll !== false;
}
@@ -408,6 +415,6 @@ const restrictedReports = [
{ key: "job_costing_ro_estimator", days: 183 },
{ key: "job_lifecycle_date_detail", days: 183 },
{ key: "job_lifecycle_date_summary", days: 183 },
- { key: "customer_list", days: 183 },
- { key: "customer_list_excel", days: 183 }
+ { key: "customer_list", days: 736 },
+ { key: "customer_list_excel", days: 736 }
];
diff --git a/client/src/components/shop-employees/shop-employees-form.component.jsx b/client/src/components/shop-employees/shop-employees-form.component.jsx
index 8218597b9..7ff148791 100644
--- a/client/src/components/shop-employees/shop-employees-form.component.jsx
+++ b/client/src/components/shop-employees/shop-employees-form.component.jsx
@@ -220,12 +220,16 @@ export function ShopEmployeesFormComponent({ bodyshop, form, onDirtyChange, isDi
});
const savedEmployee = result?.data?.insert_employees?.returning?.[0];
- syncEmployeeFormToSavedData(savedEmployee ?? normalizedValues);
-
if (submitAction === "saveAndNew") {
+ if (isNewEmployee) {
+ resetEmployeeFormToCurrentData();
+ }
navigateToEmployee("new");
} else if (savedEmployee?.id) {
+ syncEmployeeFormToSavedData(savedEmployee ?? normalizedValues);
navigateToEmployee(savedEmployee.id);
+ } else {
+ syncEmployeeFormToSavedData(savedEmployee ?? normalizedValues);
}
notification.success({
diff --git a/client/src/components/shop-employees/shop-employees-form.component.test.jsx b/client/src/components/shop-employees/shop-employees-form.component.test.jsx
index dc022a96a..6da7ef3a6 100644
--- a/client/src/components/shop-employees/shop-employees-form.component.test.jsx
+++ b/client/src/components/shop-employees/shop-employees-form.component.test.jsx
@@ -3,7 +3,12 @@ import { Form } from "antd";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { useEffect } from "react";
import { beforeEach, describe, expect, it, vi } from "vitest";
-import { DELETE_VACATION, INSERT_EMPLOYEES, QUERY_EMPLOYEE_BY_ID, UPDATE_EMPLOYEE } from "../../graphql/employees.queries";
+import {
+ DELETE_VACATION,
+ INSERT_EMPLOYEES,
+ QUERY_EMPLOYEE_BY_ID,
+ UPDATE_EMPLOYEE
+} from "../../graphql/employees.queries";
import { ShopEmployeesFormComponent } from "./shop-employees-form.component.jsx";
const insertEmployeesMock = vi.fn();
@@ -335,6 +340,15 @@ describe("ShopEmployeesFormComponent", () => {
expect(formInstance.isFieldsTouched()).toBe(false);
});
+ await waitFor(() => {
+ expect(screen.getByRole("textbox", { name: "First Name" })).toHaveValue("");
+ expect(screen.getByRole("textbox", { name: "Last Name" })).toHaveValue("");
+ expect(screen.getByRole("textbox", { name: "Employee Number" })).toHaveValue("");
+ expect(screen.getByRole("textbox", { name: "PIN" })).toHaveValue("");
+ expect(screen.getByRole("textbox", { name: "Hire Date" })).toHaveValue("");
+ });
+
+ expect(screen.getByText("New Employee")).toBeInTheDocument();
expect(navigateMock).toHaveBeenCalledWith({
search: "employeeId=new"
});
diff --git a/client/src/components/shop-info/shop-info.general.component.jsx b/client/src/components/shop-info/shop-info.general.component.jsx
index db7294df3..a12b84d04 100644
--- a/client/src/components/shop-info/shop-info.general.component.jsx
+++ b/client/src/components/shop-info/shop-info.general.component.jsx
@@ -12,6 +12,8 @@ import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.c
import { buildSectionActionButton, renderListOrEmpty } from "../layout-form-row/config-list-actions.utils.jsx";
import InlineValidatedFormRow from "../layout-form-row/inline-validated-form-row.component.jsx";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
+import { selectBodyshop } from "../../redux/user/user.selectors";
+import { bodyshopHasDmsKey, DMS_MAP, getDmsMode } from "../../utils/dmsUtils.js";
import {
INLINE_TITLE_GROUP_STYLE,
INLINE_TITLE_HANDLE_STYLE,
@@ -25,16 +27,21 @@ import {
const timeZonesList = Intl.supportedValuesOf("timeZone");
-const mapStateToProps = createStructuredSelector({});
+const mapStateToProps = createStructuredSelector({
+ bodyshop: selectBodyshop
+});
const mapDispatchToProps = () => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoGeneral);
-export function ShopInfoGeneral({ form }) {
+export function ShopInfoGeneral({ form, bodyshop }) {
const { t } = useTranslation();
const insuranceCompanies = Form.useWatch(["md_ins_cos"], form) || [];
const duplicateInsuranceCompanyIndexes = getDuplicateIndexSetByNormalizedName(insuranceCompanies, "name");
+ const hasDMSKey = bodyshop ? bodyshopHasDmsKey(bodyshop) : false;
+ const dmsMode = bodyshop ? getDmsMode(bodyshop, "off") : "none";
+ const isReynoldsMode = hasDMSKey && dmsMode === DMS_MAP.reynolds;
return (
@@ -174,7 +181,9 @@ export function ShopInfoGeneral({ form }) {
>
-
{t("bodyshop.fields.scoreboard_setup.ignore_blocked_days")}
+
+ {t("bodyshop.fields.scoreboard_setup.ignore_blocked_days")}
+
@@ -311,7 +320,12 @@ export function ShopInfoGeneral({ form }) {
-
+
@@ -478,7 +492,12 @@ export function ShopInfoGeneral({ form }) {
{t("bodyshop.fields.system_settings.job_costing.use_paint_scale_data")}
-
+
@@ -558,7 +577,12 @@ export function ShopInfoGeneral({ form }) {
-
+
+ {isReynoldsMode && (
+
+
+
+ )}
>
diff --git a/client/src/components/vehicles-list/vehicles-list.component.jsx b/client/src/components/vehicles-list/vehicles-list.component.jsx
index 41cc1d764..5d6b33fc8 100644
--- a/client/src/components/vehicles-list/vehicles-list.component.jsx
+++ b/client/src/components/vehicles-list/vehicles-list.component.jsx
@@ -11,12 +11,13 @@ import ResponsiveTable from "../responsive-table/responsive-table.component";
export default function VehiclesListComponent({ loading, vehicles, total, refetch, basePath = "/manage" }) {
const search = queryString.parse(useLocation().search);
- const {
- page
- //sortcolumn, sortorder,
- } = search;
+ const { page, pageSize } = search;
const history = useNavigate();
+ const currentPage = Number.parseInt(page || "1", 10);
+ const parsedPageSize = Number.parseInt(pageSize || String(pageLimit), 10);
+ const currentPageSize = Number.isNaN(parsedPageSize) ? pageLimit : parsedPageSize;
+
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: { text: "" }
@@ -62,10 +63,14 @@ export default function VehiclesListComponent({ loading, vehicles, total, refetc
];
const handleTableChange = (pagination, filters, sorter) => {
+ const nextPageSize = pagination?.pageSize || currentPageSize;
+ const pageSizeChanged = nextPageSize !== currentPageSize;
+
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
const updatedSearch = {
...search,
- page: pagination.current,
+ pageSize: nextPageSize,
+ page: pageSizeChanged ? 1 : pagination.current,
sortcolumn: sorter.columnKey,
sortorder: sorter.order
};
@@ -106,7 +111,7 @@ export default function VehiclesListComponent({ loading, vehicles, total, refetc
>
;
const handleTableChange = (pagination, filters, sorter) => {
- searchParams.page = pagination.current;
+ const nextPageSize = pagination?.pageSize || currentPageSize;
+ const pageSizeChanged = nextPageSize !== currentPageSize;
+
+ searchParams.pageSize = nextPageSize;
+ searchParams.page = pageSizeChanged ? 1 : pagination.current;
searchParams.sortcolumn = sorter.columnKey;
searchParams.sortorder = sorter.order;
if (filters.status) {
@@ -191,8 +199,9 @@ export function ExportLogsPageComponent() {
loading={loading}
pagination={{
placement: "top",
- pageSize: pageLimit,
- current: parseInt(page || 1, 10),
+ pageSize: currentPageSize,
+ current: currentPage,
+ showSizeChanger: true,
total: data && data.search_exportlog_aggregate.aggregate.count
}}
columns={columns}
diff --git a/client/src/pages/jobs-admin/jobs-admin.page.jsx b/client/src/pages/jobs-admin/jobs-admin.page.jsx
index e7f9606d8..22ea7577c 100644
--- a/client/src/pages/jobs-admin/jobs-admin.page.jsx
+++ b/client/src/pages/jobs-admin/jobs-admin.page.jsx
@@ -1,6 +1,6 @@
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 { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useParams } from "react-router-dom";
@@ -23,9 +23,8 @@ import LoadingSpinner from "../../components/loading-spinner/loading-spinner.com
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 { setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
-import { insertAuditTrail } from "../../redux/application/application.actions";
+import { CONVERT_JOB_TO_RO, GET_JOB_BY_PK } from "../../graphql/jobs.queries";
+import { insertAuditTrail, setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { createStructuredSelector } from "reselect";
import { useSocket } from "../../contexts/SocketIO/useSocket";
@@ -302,7 +301,11 @@ export function JobsCloseContainer({ setBreadcrumbs, setSelectedHeader, bodyshop
}
]}
>
-