dayjs(a.start).unix() - dayjs(b.start).unix(),
- render: (text, record) => (
+ render: (text) => (
{DateTimeFormatterFunction(text)}
@@ -119,8 +119,7 @@ export function JobLifecycleComponent({ bodyshop, job, statuses, ...rest }) {
}
return dayjs(a.end).unix() - dayjs(b.end).unix();
},
-
- render: (text, record) => (
+ render: (text) => (
{isEmpty(text) ? t("job_lifecycle.content.not_available") : DateTimeFormatterFunction(text)}
@@ -170,7 +169,7 @@ export function JobLifecycleComponent({ bodyshop, job, statuses, ...rest }) {
borderRadius: "5px",
borderWidth: "5px",
borderStyle: "solid",
- borderColor: "#f0f2f5",
+ borderColor: "var(--bar-border-color)",
margin: 0,
padding: 0
}}
@@ -189,12 +188,10 @@ export function JobLifecycleComponent({ bodyshop, job, statuses, ...rest }) {
alignItems: "center",
margin: 0,
padding: 0,
-
- borderTop: "1px solid #f0f2f5",
- borderBottom: "1px solid #f0f2f5",
- borderLeft: isFirst ? "1px solid #f0f2f5" : undefined,
- borderRight: isLast ? "1px solid #f0f2f5" : undefined,
-
+ borderTop: "1px solid var(--bar-border-color)",
+ borderBottom: "1px solid var(--bar-border-color)",
+ borderLeft: isFirst ? "1px solid var(--bar-border-color)" : undefined,
+ borderRight: isLast ? "1px solid var(--bar-border-color)" : undefined,
backgroundColor: key.color,
width: `${key.percentage}%`
}}
@@ -206,7 +203,7 @@ export function JobLifecycleComponent({ bodyshop, job, statuses, ...rest }) {
{key.roundedPercentage}
);
}
+
export default connect(mapStateToProps, mapDispatchToProps)(JobLifecycleComponent);
diff --git a/client/src/components/jobs-close-lines/jobs-close-lines.styles.scss b/client/src/components/jobs-close-lines/jobs-close-lines.styles.scss
index e68b23d22..1e4c60073 100644
--- a/client/src/components/jobs-close-lines/jobs-close-lines.styles.scss
+++ b/client/src/components/jobs-close-lines/jobs-close-lines.styles.scss
@@ -6,7 +6,7 @@
td {
padding: 8px;
text-align: left;
- border-bottom: 1px solid #ddd;
+ border-bottom: 1px solid var(--table-border-color);
.ant-form-item {
margin-bottom: 0px !important;
@@ -14,6 +14,6 @@
}
tr:hover {
- background-color: #f5f5f5;
+ background-color: var(--table-hover-bg);
}
}
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 8ada3616f..c7f7d46e4 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
@@ -46,8 +46,8 @@ function JobsDocumentsImgproxyComponent({
const [modalState, setModalState] = useState({ open: false, index: 0 });
const fetchThumbnails = useCallback(() => {
- fetchImgproxyThumbnails({ setStateCallback: setGalleryImages, jobId });
- }, [jobId, setGalleryImages]);
+ fetchImgproxyThumbnails({ setStateCallback: setGalleryImages, jobId, billId });
+ }, [jobId, billId, setGalleryImages]);
useEffect(() => {
if (data) {
@@ -208,8 +208,8 @@ function JobsDocumentsImgproxyComponent({
export default connect(mapStateToProps, mapDispatchToProps)(JobsDocumentsImgproxyComponent);
-export const fetchImgproxyThumbnails = async ({ setStateCallback, jobId, imagesOnly }) => {
- const result = await axios.post("/media/imgproxy/thumbnails", { jobid: jobId });
+export const fetchImgproxyThumbnails = async ({ setStateCallback, jobId, billId, imagesOnly }) => {
+ const result = await axios.post("/media/imgproxy/thumbnails", { jobid: jobId, billid: billId });
const documents = result.data.reduce(
(acc, value) => {
if (value.type.startsWith("image")) {
diff --git a/client/src/components/note-upsert-modal/note-upsert-modal.container.jsx b/client/src/components/note-upsert-modal/note-upsert-modal.container.jsx
index 8f3e33d69..90911f056 100644
--- a/client/src/components/note-upsert-modal/note-upsert-modal.container.jsx
+++ b/client/src/components/note-upsert-modal/note-upsert-modal.container.jsx
@@ -1,4 +1,4 @@
-import { useApolloClient, useMutation } from "@apollo/client";
+import { useMutation } from "@apollo/client";
import { Form, Modal } from "antd";
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
@@ -42,9 +42,7 @@ export function NoteUpsertModalContainer({ currentUser, noteUpsertModal, toggleM
const { refetch } = actions;
const [form] = Form.useForm();
-
- const { client } = useApolloClient();
-
+
useEffect(() => {
//Required to prevent infinite looping.
if (existingNote && open) {
diff --git a/client/src/components/notification-center/notification-center.styles.scss b/client/src/components/notification-center/notification-center.styles.scss
index 89ae0beae..98e2c6890 100644
--- a/client/src/components/notification-center/notification-center.styles.scss
+++ b/client/src/components/notification-center/notification-center.styles.scss
@@ -4,9 +4,9 @@
right: 0;
width: 400px;
max-width: 400px;
- background: #fff;
- color: rgba(0, 0, 0, 0.85);
- border: 1px solid #d9d9d9;
+ background: var(--notification-bg);
+ color: var(--notification-text);
+ border: 1px solid var(--notification-border);
border-radius: 6px;
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08), 0 3px 6px rgba(0, 0, 0, 0.06);
z-index: 1000;
@@ -19,23 +19,22 @@
.notification-header {
padding: 4px 16px;
- border-bottom: 1px solid #f0f0f0;
+ border-bottom: 1px solid var(--notification-header-border);
display: flex;
justify-content: space-between;
align-items: center;
- background: #fafafa;
+ background: var(--notification-header-bg);
h3 {
margin: 0;
font-size: 14px;
- color: rgba(0, 0, 0, 0.85);
+ color: var(--notification-header-text);
}
.notification-controls {
display: flex;
align-items: center;
gap: 8px;
-
// Styles for the eye icon and switch (custom classes)
.notification-toggle {
align-items: center; // Ensure vertical alignment
@@ -43,7 +42,7 @@
.notification-toggle-icon {
font-size: 14px;
- color: #1677ff;
+ color: var(--notification-toggle-icon);
vertical-align: middle;
}
@@ -59,7 +58,8 @@
}
&.ant-switch-checked {
- background-color: #1677ff;
+ background-color: var(--notification-switch-bg);
+
.ant-switch-handle {
left: calc(100% - 14px);
}
@@ -70,37 +70,37 @@
// Styles for the "Mark All Read" button (restore original link button style)
.ant-btn-link {
padding: 0;
- color: #1677ff;
+ color: var(--notification-btn-link);
&:hover {
- color: #69b1ff;
+ color: var(--notification-btn-link-hover);
}
&:disabled {
- color: rgba(0, 0, 0, 0.25);
+ color: var(--notification-btn-link-disabled);
cursor: not-allowed;
}
&.active {
- color: #0958d9;
+ color: var(--notification-btn-link-active);
}
}
}
}
.notification-read {
- background: #fff;
- color: rgba(0, 0, 0, 0.65);
+ background: var(--notification-read-bg);
+ color: var(--notification-read-text);
}
.notification-unread {
- background: #f5f5f5;
- color: rgba(0, 0, 0, 0.85);
+ background: var(--notification-unread-bg);
+ color: var(--notification-unread-text);
}
.notification-item {
padding: 12px 16px;
- border-bottom: 1px solid #f0f0f0;
+ border-bottom: 1px solid var(--notification-header-border);
display: block;
overflow: visible;
width: 100%;
@@ -108,7 +108,7 @@
cursor: pointer;
&:hover {
- background: #fafafa;
+ background: var(--notification-item-hover-bg);
}
.notification-content {
@@ -125,7 +125,7 @@
.ro-number {
margin: 0;
- color: #1677ff;
+ color: var(--notification-ro-number);
flex-shrink: 0;
white-space: nowrap;
}
@@ -133,7 +133,7 @@
.relative-time {
margin: 0;
font-size: 12px;
- color: rgba(0, 0, 0, 0.45);
+ color: var(--notification-relative-time);
white-space: nowrap;
flex-shrink: 0;
margin-left: auto;
@@ -164,12 +164,12 @@
.ant-alert {
margin: 8px;
- background: #fff1f0;
- color: rgba(0, 0, 0, 0.85);
- border: 1px solid #ffa39e;
+ background: var(--alert-bg);
+ color: var(--alert-text);
+ border: 1px solid var(--alert-border);
.ant-alert-message {
- color: #ff4d4f;
+ color: var(--alert-message);
}
}
}
diff --git a/client/src/components/payment-expanded-row/payment-expanded-row.component.jsx b/client/src/components/payment-expanded-row/payment-expanded-row.component.jsx
index d0d3f53e4..dfb6e5296 100644
--- a/client/src/components/payment-expanded-row/payment-expanded-row.component.jsx
+++ b/client/src/components/payment-expanded-row/payment-expanded-row.component.jsx
@@ -16,10 +16,10 @@ import { useNotification } from "../../contexts/Notifications/notificationContex
const { confirm } = Modal;
-const openNotificationWithIcon = (type, t, notification) => {
+const openNotificationWithIcon = (type, t, notification, message) => {
notification[type]({
message: t("job_payments.notifications.error.title"),
- description: t("job_payments.notifications.error.description")
+ description: t("job_payments.notifications.error.description", { message: message || "Unknown error." })
});
};
@@ -99,7 +99,7 @@ const PaymentExpandedRowComponent = ({ record, bodyshop }) => {
});
if (refundResponse.data.status < 0) {
- openNotificationWithIcon("error", t, notification);
+ openNotificationWithIcon("error", t, notification, refundResponse.data.message);
return;
}
diff --git a/client/src/components/production-board-kanban/production-board-kanban-card-color-legend.component.jsx b/client/src/components/production-board-kanban/production-board-kanban-card-color-legend.component.jsx
index abae21de0..b4017546c 100644
--- a/client/src/components/production-board-kanban/production-board-kanban-card-color-legend.component.jsx
+++ b/client/src/components/production-board-kanban/production-board-kanban-card-color-legend.component.jsx
@@ -1,26 +1,29 @@
import { Col, List, Space, Typography } from "antd";
-import React from "react";
import { useTranslation } from "react-i18next";
const CardColorLegend = ({ bodyshop }) => {
const { t } = useTranslation();
const data = bodyshop.ssbuckets.map((bucket) => {
- let color = { r: 255, g: 255, b: 255 };
-
+ let color = { r: 255, g: 255, b: 255, a: 1 }; // Default to white with full opacity
if (bucket.color) {
color = bucket.color;
-
if (bucket.color.rgb) {
- color = bucket.color.rgb;
+ color = { ...bucket.color.rgb, a: bucket.color.a || 1 };
}
}
-
return {
label: bucket.label,
color
};
});
+ const getBackgroundColor = (color) => {
+ // Return dynamic color if valid, otherwise use fallback
+ return color && color.r !== undefined && color.g !== undefined && color.b !== undefined
+ ? `rgba(${color.r},${color.g},${color.b},${color.a || 1})`
+ : "var(--legend-bg-fallback)";
+ };
+
return (
{t("production.labels.legend")}
@@ -36,7 +39,7 @@ const CardColorLegend = ({ bodyshop }) => {
style={{
width: "1.5rem",
aspectRatio: "1/1",
- backgroundColor: `rgba(${item.color.r},${item.color.g},${item.color.b},${item.color.a})`
+ backgroundColor: getBackgroundColor(item.color)
}}
>
{item.label}
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 ef3c2126b..bc6d38a56 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
@@ -11,13 +11,10 @@ import React, { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { DateTimeFormatter } from "../../utils/DateFormatter";
-
import ProductionAlert from "../production-list-columns/production-list-columns.alert.component";
import ProductionListColumnProductionNote from "../production-list-columns/production-list-columns.productionnote.component";
import ProductionSubletsManageComponent from "../production-sublets-manage/production-sublets-manage.component";
-
import dayjs from "../../utils/day";
-
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import ShareToTeamsButton from "../share-to-teams/share-to-teams.component.jsx";
@@ -25,11 +22,25 @@ import { PiMicrosoftTeamsLogo } from "react-icons/pi";
const cardColor = (ssbuckets, totalHrs) => {
const bucket = ssbuckets.find((bucket) => bucket.gte <= totalHrs && (!bucket.lt || bucket.lt > totalHrs));
- return bucket && bucket.color ? bucket.color.rgb || bucket.color : { r: 255, g: 255, b: 255 };
+ return bucket && bucket.color
+ ? bucket.color.rgb || bucket.color
+ : {
+ r: 255,
+ g: 255,
+ b: 255,
+ a: 1,
+ fallback: "var(--card-bg-fallback)"
+ };
};
-const getContrastYIQ = (bgColor) =>
- (bgColor.r * 299 + bgColor.g * 587 + bgColor.b * 114) / 1000 >= 128 ? "black" : "white";
+const getContrastYIQ = (bgColor, isDarkMode = document.documentElement.getAttribute("data-theme") === "dark") => {
+ // Use fallback if bgColor is invalid
+ if (!bgColor || bgColor.fallback) return isDarkMode ? "var(--card-text-fallback)" : "black";
+ // Calculate luminance for contrast
+ const luminance = (bgColor.r * 299 + bgColor.g * 587 + bgColor.b * 114) / 1000;
+ // Adjust threshold for dark mode to ensure readable text
+ return luminance >= (isDarkMode ? 150 : 128) ? "black" : isDarkMode ? "var(--card-text-fallback)" : "white";
+};
const findEmployeeById = (employees, id) => employees.find((e) => e.id === id);
@@ -44,6 +55,8 @@ const EllipsesToolTip = React.memo(({ title, children, kiosk }) => {
);
});
+EllipsesToolTip.displayName = "EllipsesToolTip";
+
const OwnerNameToolTip = ({ metadata, cardSettings }) =>
cardSettings?.ownr_nm && (
@@ -214,9 +227,8 @@ const EstimatorToolTip = ({ metadata, cardSettings }) => {
);
};
-const SubtotalTooltip = ({ metadata, cardSettings, t }) => {
+const SubtotalTooltip = ({ metadata, cardSettings }) => {
const dineroAmount = Dinero(metadata?.job_totals?.totals?.subtotal ?? Dinero()).toFormat();
-
return (
cardSettings?.subtotal && (
@@ -300,12 +312,10 @@ const TasksToolTip = ({ metadata, cardSettings, t }) =>
);
-export default function ProductionBoardCard({ technician, card, bodyshop, cardSettings, clone }) {
+export default function ProductionBoardCard({ technician, card, bodyshop, cardSettings }) {
const { t } = useTranslation();
const { metadata } = card;
-
const employees = useMemo(() => bodyshop.employees, [bodyshop.employees]);
-
const { employee_body, employee_prep, employee_refinish, employee_csr } = useMemo(() => {
return {
employee_body: metadata?.employee_body && findEmployeeById(employees, metadata.employee_body),
@@ -314,7 +324,6 @@ export default function ProductionBoardCard({ technician, card, bodyshop, cardSe
employee_csr: metadata?.employee_csr && findEmployeeById(employees, metadata.employee_csr)
};
}, [metadata, employees]);
-
const pastDueAlert = useMemo(() => {
if (!metadata?.scheduled_completion) return null;
const completionDate = dayjs(metadata.scheduled_completion);
@@ -322,16 +331,13 @@ export default function ProductionBoardCard({ technician, card, bodyshop, cardSe
if (dayjs().add(1, "day").isSame(completionDate, "day")) return "production-completion-soon";
return null;
}, [metadata?.scheduled_completion]);
-
const totalHrs = useMemo(() => {
return metadata?.labhrs && metadata?.larhrs
? metadata.labhrs.aggregate.sum.mod_lb_hrs + metadata.larhrs.aggregate.sum.mod_lb_hrs
: 0;
}, [metadata?.labhrs, metadata?.larhrs]);
-
const bgColor = useMemo(() => cardColor(bodyshop.ssbuckets, totalHrs), [bodyshop.ssbuckets, totalHrs]);
const contrastYIQ = useMemo(() => getContrastYIQ(bgColor), [bgColor]);
-
const isBodyEmpty = useMemo(() => {
return !(
cardSettings?.ownr_nm ||
@@ -413,8 +419,10 @@ export default function ProductionBoardCard({ technician, card, bodyshop, cardSe
className={`react-trello-card ${cardSettings.kiosk ? "kiosk-mode" : ""}`}
size="small"
style={{
- backgroundColor: cardSettings?.cardcolor && `rgba(${bgColor.r},${bgColor.g},${bgColor.b},${bgColor.a})`,
- color: cardSettings?.cardcolor && contrastYIQ
+ backgroundColor: cardSettings?.cardcolor
+ ? bgColor.fallback || `rgba(${bgColor.r},${bgColor.g},${bgColor.b},${bgColor.a || 1})`
+ : "var(--card-bg-fallback)",
+ color: cardSettings?.cardcolor ? contrastYIQ : "var(--card-text-fallback)"
}}
title={!isBodyEmpty ? headerContent : null}
extra={
diff --git a/client/src/components/production-board-kanban/production-board-kanban.styles.scss b/client/src/components/production-board-kanban/production-board-kanban.styles.scss
index 027b249a0..48ccc0fa1 100644
--- a/client/src/components/production-board-kanban/production-board-kanban.styles.scss
+++ b/client/src/components/production-board-kanban/production-board-kanban.styles.scss
@@ -11,7 +11,7 @@
}
.share-to-teams-badge {
- background-color: #cccccc;
+ background-color: var(--share-badge-bg);
border-radius: 50%;
width: 24px;
height: 24px;
@@ -23,7 +23,7 @@
.react-trello-column-header {
font-weight: bold;
cursor: pointer;
- background-color: #d0d0d0;
+ background-color: var(--column-header-bg);
border-radius: 5px 5px 0 0;
}
@@ -31,13 +31,14 @@
background: transparent;
border: none;
}
+
.react-trello-footer {
- background-color: #d0d0d0;
+ background-color: var(--footer-bg);
border-radius: 0 0 5px 5px;
}
.grid-item {
- margin: 1px; // TODO: (Note) THis is where we set the margin for vertical
+ margin: 1px; // TODO: (Note) This is where we set the margin for vertical
}
.lane-title {
@@ -53,27 +54,33 @@
justify-content: center;
align-items: center;
position: relative;
+
.body-empty-container {
position: absolute;
right: 0;
}
+
.tech-container {
font-weight: bolder;
text-align: center;
flex: 1;
+
.branches-outlined {
- color: orangered;
+ color: var(--tech-icon-color);
}
}
+
.inner-container {
display: flex;
align-items: center;
position: absolute;
left: 0;
+
.circle-outline {
- color: orangered;
+ color: var(--tech-icon-color);
margin-left: 8px;
}
+
.iou-parent {
margin-left: 8px;
}
@@ -81,6 +88,6 @@
}
.clone.is-dragging .ant-card {
- border: #1890ff 2px solid !important;
+ border: 2px solid var(--clone-border-color) !important;
border-radius: 12px;
}
diff --git a/client/src/components/production-board-kanban/trello-board/styles/Base.js b/client/src/components/production-board-kanban/trello-board/styles/Base.js
index 02f545ad2..93575fbd5 100644
--- a/client/src/components/production-board-kanban/trello-board/styles/Base.js
+++ b/client/src/components/production-board-kanban/trello-board/styles/Base.js
@@ -58,7 +58,7 @@ export const StyleHorizontal = styled.div`
height: 100%;
min-height: 1px;
overflow-y: visible;
- overflow-x: visible; // change this line
+ overflow-x: visible;
}
.react-trello-lane.lane-collapsed {
@@ -85,17 +85,17 @@ export const StyleHorizontal = styled.div`
.react-trello-card {
height: auto;
- margin: 2px;
+ margin: 2px 0 2px;
}
.size-memory-wrapper {
- display: flex; /* This makes it a flex container */
- flex-direction: column; /* Aligns children vertically */
+ display: flex;
+ flex-direction: column;
}
.size-memory-wrapper .ant-card {
- flex-grow: 1; /* Allows the card to expand to fill the available space */
- width: 100%; /* Ensures the card stretches to fill the width of its parent */
+ flex-grow: 1;
+ width: 100%;
}
`;
@@ -131,7 +131,7 @@ export const StyleVertical = styled.div`
.grid-item {
display: flex;
- width: ${(props) => props.gridItemWidth}; /* Use props to set width */
+ width: ${(props) => props.gridItemWidth};
align-content: stretch;
box-sizing: border-box;
}
@@ -148,13 +148,13 @@ export const StyleVertical = styled.div`
}
.size-memory-wrapper {
- display: flex; /* This makes it a flex container */
- flex-direction: column; /* Aligns children vertically */
+ display: flex;
+ flex-direction: column;
}
.size-memory-wrapper .ant-card {
- flex-grow: 1; /* Allows the card to expand to fill the available space */
- width: 100%; /* Ensures the card stretches to fill the width of its parent */
+ flex-grow: 1;
+ width: 100%;
}
.react-trello-lane .lane-collapsed {
@@ -163,7 +163,7 @@ export const StyleVertical = styled.div`
`;
export const BoardWrapper = styled.div`
- color: #393939;
+ color: var(--board-text-color);
height: 100%;
overflow-x: auto;
overflow-y: hidden;
@@ -171,7 +171,7 @@ export const BoardWrapper = styled.div`
`;
export const Section = styled.section`
- background-color: #e3e3e3;
+ background-color: var(--section-bg);
border-radius: 3px;
margin: 2px 2px;
height: 100%;
@@ -197,6 +197,6 @@ export const ScrollableLane = styled.div`
export const Detail = styled.div`
font-size: 12px;
- color: #4d4d4d;
+ color: var(--detail-text-color);
white-space: pre-wrap;
`;
diff --git a/client/src/components/production-list-table/production-list-table.component.jsx b/client/src/components/production-list-table/production-list-table.component.jsx
index 09e41b5e8..8e523f490 100644
--- a/client/src/components/production-list-table/production-list-table.component.jsx
+++ b/client/src/components/production-list-table/production-list-table.component.jsx
@@ -28,7 +28,6 @@ const mapStateToProps = createStructuredSelector({
export function ProductionListTable({ loading, data, refetch, bodyshop, technician, currentUser }) {
const [searchText, setSearchText] = useState("");
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
-
const {
treatments: { Production_List_Status_Colors, Enhanced_Payroll }
} = useSplitTreatments({
@@ -36,10 +35,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 && assoc.default_prod_list_view;
-
const initialStateRef = useRef(
(bodyshop.production_config &&
bodyshop.production_config.find((p) => p.name === defaultView)?.columns.tableState) ||
@@ -48,7 +45,6 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
filteredInfo: { text: "" }
}
);
-
const initialColumnsRef = useRef(
(initialStateRef.current &&
bodyshop?.production_config
@@ -69,12 +65,9 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
})) ||
[]
);
-
const [state, setState] = useState(initialStateRef.current);
const [columns, setColumns] = useState(initialColumnsRef.current);
-
const { t } = useTranslation();
-
const matchingColumnConfig = useMemo(() => {
return bodyshop?.production_config?.find((p) => p.name === defaultView);
}, [bodyshop.production_config, defaultView]);
@@ -95,7 +88,6 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
width: k.width ?? 100
};
}) || [];
-
// Only update columns if they haven't been manually changed by the user
if (_.isEqual(initialColumnsRef.current, columns)) {
setColumns(newColumns);
@@ -126,11 +118,9 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
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);
@@ -140,7 +130,6 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
const removeColumn = (e) => {
const { key } = e;
const newColumns = columns.filter((i) => i.key !== key);
-
if (!_.isEqual(newColumns, columns)) {
setColumns(newColumns);
setHasUnsavedChanges(true);
@@ -155,7 +144,6 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
...nextColumns[index],
width: size.width
};
-
if (!_.isEqual(nextColumns, columns)) {
setColumns(nextColumns);
setHasUnsavedChanges(true);
@@ -180,7 +168,6 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
}
]
};
-
return (
{col.title}
@@ -206,13 +193,12 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
item.v_model_desc,
item.v_make_desc
];
-
return fieldsToSearch.some((field) => (field || "").toString().toLowerCase().includes(searchText.toLowerCase()));
};
const dataSource = searchText === "" ? data : data.filter((j) => filterData(j, searchText));
- if (!!!columns) return No columns found.
;
+ if (!columns) return No columns found.
;
const totalHrs = data
.reduce(
@@ -236,7 +222,8 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
onClick={resetChanges}
style={{
cursor: "pointer",
- textDecoration: "underline"
+ textDecoration: "underline",
+ color: "var(--reset-link-color)"
}}
>
{t("general.actions.reset")}
@@ -269,7 +256,6 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
data={data}
onColumnAdd={addColumn}
/>
-
{
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: `rgb(236, 236, 236)`
+ backgroundColor: "var(--table-row-even-bg)"
}
};
-
return null;
}
-
return {
className: "rowWithColor",
style: {
- "--bgColor": `rgb(${color.color.r},${color.color.g},${color.color.b},${color.color.a})`
+ "--bgColor": color.color
+ ? `rgba(${color.color.r},${color.color.g},${color.color.b},${color.color.a || 1})`
+ : "var(--status-row-bg-fallback)"
}
};
}
diff --git a/client/src/components/schedule-calendar-wrapper/schedule-calendar-header.component.jsx b/client/src/components/schedule-calendar-wrapper/schedule-calendar-header.component.jsx
index faea7a23d..bee287332 100644
--- a/client/src/components/schedule-calendar-wrapper/schedule-calendar-header.component.jsx
+++ b/client/src/components/schedule-calendar-wrapper/schedule-calendar-header.component.jsx
@@ -212,6 +212,10 @@ export function ScheduleCalendarHeaderComponent({ bodyshop, label, refetch, date
return bodyshop.workingdays[day];
};
+ const blocked = isDayBlocked.length > 0;
+ const headerStyle = blocked ? { color: "#fff" } : { color: isShopOpen(date) ? "" : "tomato" };
+ const headerClass = `imex-calendar-header-card ${blocked ? "imex-calendar-header-card--blocked" : ""}`.trim();
+
return (
0} date={date} refetch={refetch}>
diff --git a/client/src/components/schedule-calendar-wrapper/schedule-calendar.styles.scss b/client/src/components/schedule-calendar-wrapper/schedule-calendar.styles.scss
index 6753cd293..03bb447dc 100644
--- a/client/src/components/schedule-calendar-wrapper/schedule-calendar.styles.scss
+++ b/client/src/components/schedule-calendar-wrapper/schedule-calendar.styles.scss
@@ -19,11 +19,42 @@
// }
.imex-event-arrived {
- background-color: rgba(4, 141, 4, 0.4);
+ background-color: var(--event-arrived-bg);
}
.imex-event-block {
- background-color: rgba(212, 2, 2, 0.6);
+ background-color: var(--event-block-bg);
+}
+
+/* Ensure readable text when fallback background is used */
+.imex-event-fallback,
+.imex-event-fallback .rbc-event-content,
+.imex-event-fallback .rbc-event-label,
+.imex-event-fallback a {
+ color: var(--card-text-fallback) !important;
+}
+
+/* Optional subtle border to distinguish on white backgrounds */
+.imex-event-fallback {
+ border: 1px solid var(--bar-border-color);
+}
+
+/* Header day card styling */
+.imex-calendar-header-card {
+ display: inline-block;
+ padding: 0.15rem 0.35rem;
+ border-radius: 0.25rem;
+}
+
+.imex-calendar-header-card--blocked {
+ background-color: var(--event-block-bg);
+ color: #ffffff;
+}
+
+.imex-calendar-header-card--blocked a,
+.imex-calendar-header-card--blocked span,
+.imex-calendar-header-card--blocked .ant-typography {
+ color: #ffffff;
}
.rbc-month-view {
@@ -31,12 +62,12 @@
}
.rbc-event.rbc-selected {
- background-color: slategrey;
+ background-color: var(--event-selected-bg);
}
.imex-calendar-load {
max-width: 12rem;
position: relative;
left: 50%;
- transform: translateX(-50%);
+ transform: translate(-50%);
}
diff --git a/client/src/components/schedule-calendar-wrapper/scheduler-calendar-wrapper.component.jsx b/client/src/components/schedule-calendar-wrapper/scheduler-calendar-wrapper.component.jsx
index 13941cd08..8bedaa41a 100644
--- a/client/src/components/schedule-calendar-wrapper/scheduler-calendar-wrapper.component.jsx
+++ b/client/src/components/schedule-calendar-wrapper/scheduler-calendar-wrapper.component.jsx
@@ -36,16 +36,40 @@ export function ScheduleCalendarWrapperComponent({
const search = queryString.parse(useLocation().search);
const history = useNavigate();
const { t } = useTranslation();
+
+ // Determine current view to compute styles consistently
+ const currentView = search.view || defaultView || "week";
+
const handleEventPropStyles = (event, start, end, isSelected) => {
+ const hasColor = Boolean(event?.color?.hex || event?.color);
+ const useBg = currentView !== "agenda";
+
+ // Prioritize explicit blocked-day background to ensure red in all themes
+ let bg;
+ if (useBg) {
+ if (event?.block) {
+ bg = "var(--event-block-bg)";
+ } else if (hasColor) {
+ bg = event?.color?.hex ?? event?.color;
+ } else {
+ bg = "var(--event-bg-fallback)";
+ }
+ }
+
+ const usedFallback = !hasColor && !event?.block; // only mark as fallback when not blocked
+
+ const classes = [
+ "imex-event",
+ event.arrived && "imex-event-arrived",
+ event.block && "imex-event-block",
+ usedFallback && "imex-event-fallback"
+ ]
+ .filter(Boolean)
+ .join(" ");
+
return {
- ...(event.color && !((search.view || defaultView) === "agenda")
- ? {
- style: {
- backgroundColor: event.color && event.color.hex ? event.color.hex : event.color
- }
- }
- : {}),
- className: `${event.arrived ? "imex-event-arrived" : ""} ${event.block ? "imex-event-block" : ""}`
+ ...(bg ? { style: { backgroundColor: bg } } : {}),
+ className: classes
};
};
@@ -60,7 +84,9 @@ export function ScheduleCalendarWrapperComponent({
{t("appointments.labels.severalerrorsfound")}}
+ header={
+ {t("appointments.labels.severalerrorsfound")}
+ }
>
{problemJobs.map((problem) => (
@@ -70,7 +96,7 @@ export function ScheduleCalendarWrapperComponent({
message={
]}
+ components={[]}
values={{
ro_number: problem.ro_number,
code: problem.code
@@ -91,7 +117,7 @@ export function ScheduleCalendarWrapperComponent({
message={
]}
+ components={[]}
values={{
ro_number: problem.ro_number,
code: problem.code
@@ -102,12 +128,11 @@ export function ScheduleCalendarWrapperComponent({
))}
))}
-
{
+ onNavigate={(date) => {
search.date = date.toISOString().substr(0, 10);
history({ search: queryString.stringify(search) });
}}
diff --git a/client/src/components/schedule-calendar/schedule-calendar.component.jsx b/client/src/components/schedule-calendar/schedule-calendar.component.jsx
index 476094dd8..48ae26a6c 100644
--- a/client/src/components/schedule-calendar/schedule-calendar.component.jsx
+++ b/client/src/components/schedule-calendar/schedule-calendar.component.jsx
@@ -2,7 +2,7 @@ import { SyncOutlined } from "@ant-design/icons";
import { Button, Card, Checkbox, Col, Row, Select, Space } from "antd";
import { PageHeader } from "@ant-design/pro-layout";
import { t } from "i18next";
-import React, { useMemo } from "react";
+import { useMemo } from "react";
import useLocalStorage from "../../utils/useLocalStorage";
import ScheduleAtsSummary from "../schedule-ats-summary/schedule-ats-summary.component";
import ScheduleCalendarWrapperComponent from "../schedule-calendar-wrapper/scheduler-calendar-wrapper.component";
@@ -18,7 +18,7 @@ import _ from "lodash";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
-const mapDispatchToProps = (dispatch) => ({
+const mapDispatchToProps = () => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleCalendarComponent);
diff --git a/client/src/components/schedule-calendar/schedule-calendar.container.jsx b/client/src/components/schedule-calendar/schedule-calendar.container.jsx
index 8bf50ca7f..cc7bc5fa3 100644
--- a/client/src/components/schedule-calendar/schedule-calendar.container.jsx
+++ b/client/src/components/schedule-calendar/schedule-calendar.container.jsx
@@ -1,6 +1,6 @@
import { useQuery } from "@apollo/client";
import queryString from "query-string";
-import React, { useEffect, useMemo } from "react";
+import { useEffect, useMemo } from "react";
import { useLocation } from "react-router-dom";
import { QUERY_ALL_ACTIVE_APPOINTMENTS } from "../../graphql/appointments.queries";
import AlertComponent from "../alert/alert.component";
@@ -32,7 +32,7 @@ export function ScheduleCalendarContainer({ calculateScheduleLoad }) {
startd: range.start,
endd: range.end
},
- skip: !!!range.start || !!!range.end,
+ skip: !range.start || !range.end,
fetchPolicy: "network-only",
nextFetchPolicy: "network-only"
});
diff --git a/client/src/components/scoreboard-chart/chart-custom-tooltip.jsx b/client/src/components/scoreboard-chart/chart-custom-tooltip.jsx
index c43b26f97..e2c5012f2 100644
--- a/client/src/components/scoreboard-chart/chart-custom-tooltip.jsx
+++ b/client/src/components/scoreboard-chart/chart-custom-tooltip.jsx
@@ -5,26 +5,25 @@ const CustomTooltip = ({ active, payload, label }) => {
return (
{label}
{payload.map((data, index) => {
+ const textColor = data.color || "var(--tooltip-text-fallback)";
if (data.dataKey === "sales" || data.dataKey === "accSales")
return (
-
{`${data.name} : ${Dinero({
+
{`${data.name} : ${Dinero({
amount: Math.round(data.value * 100)
}).toFormat()}`}
);
-
- return
{`${data.name} : ${data.value}`}
;
+ return
{`${data.name} : ${data.value}`}
;
})}
);
}
-
return null;
};
diff --git a/client/src/components/scoreboard-timetickets-stats/chart-custom-tooltip.jsx b/client/src/components/scoreboard-timetickets-stats/chart-custom-tooltip.jsx
index 37e450cc7..1050f5ffb 100644
--- a/client/src/components/scoreboard-timetickets-stats/chart-custom-tooltip.jsx
+++ b/client/src/components/scoreboard-timetickets-stats/chart-custom-tooltip.jsx
@@ -3,15 +3,16 @@ const CustomTooltip = ({ active, payload, label }) => {
return (
{label}
{payload.map((data, index) => {
+ const textColor = data.color || "var(--tooltip-text-fallback)";
return (
-
{`${
+
{`${
data.name
} : ${data.value.toFixed(1)}`}
);
@@ -19,7 +20,6 @@ const CustomTooltip = ({ active, payload, label }) => {
);
}
-
return null;
};
diff --git a/client/src/components/share-to-teams/share-to-teams.component.jsx b/client/src/components/share-to-teams/share-to-teams.component.jsx
index a314a826c..6d935db31 100644
--- a/client/src/components/share-to-teams/share-to-teams.component.jsx
+++ b/client/src/components/share-to-teams/share-to-teams.component.jsx
@@ -41,7 +41,6 @@ const ShareToTeamsComponent = ({
}) => {
const location = useLocation();
const { t } = useTranslation();
-
const currentUrl =
urlOverride ||
encodeURIComponent(`${window.location.origin}${location.pathname}${location.search}${location.hash}`);
@@ -49,31 +48,24 @@ const ShareToTeamsComponent = ({
pageTitleOverride ||
encodeURIComponent(typeof document !== "undefined" ? document.title : t("general.actions.sharetoteams"));
const messageText = messageTextOverride || encodeURIComponent(t("general.actions.sharetoteams"));
-
// Construct the Teams share URL with parameters
const teamsShareUrl = `https://teams.microsoft.com/share?href=${currentUrl}&preText=${messageText}&title=${pageTitle}`;
-
// Function to open the centered share link in a new window/tab
const handleShare = () => {
const screenWidth = window.screen.width;
const screenHeight = window.screen.height;
const windowWidth = 600;
const windowHeight = 400;
-
const left = screenWidth / 2 - windowWidth / 2;
const top = screenHeight / 2 - windowHeight / 2;
-
const windowFeatures = `width=${windowWidth},height=${windowHeight},left=${left},top=${top}`;
-
// noinspection JSIgnoredPromiseFromCall
window.open(teamsShareUrl, "_blank", windowFeatures);
};
-
// Feature is disabled
if (!bodyshop?.md_functionality_toggles?.teams) {
return null;
}
-
if (noIcon) {
return (
@@ -81,16 +73,15 @@ const ShareToTeamsComponent = ({
);
}
-
return (
}
+ icon={}
onClick={handleShare}
title={linkText === null ? t("general.actions.sharetoteams") : linkText}
/>
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 2a20e9549..6803e53ae 100644
--- a/client/src/components/shop-info/shop-info.general.component.jsx
+++ b/client/src/components/shop-info/shop-info.general.component.jsx
@@ -145,124 +145,168 @@ export function ShopInfoGeneral({ form, bodyshop }) {
- {HasFeatureAccess({ featureName: "export", bodyshop }) && (
- <>
-
-
-
- {InstanceRenderManager({
- imex: (
-
- {() => (
-
-
-
- )}
-
- )
- })}
-
-
-
-
-
- 2
- 3
-
-
-
- {() => {
- return (
-
-
- {t("bodyshop.labels.2tiername")}
- {t("bodyshop.labels.2tiersource")}
-
-
- );
- }}
-
-
-
-
-
-
-
- >
- )}
-
-
-
-
-
-
- {InstanceRenderManager({
- imex: (
-
-
-
- )
- })}
-
-
-
- {HasFeatureAccess({ featureName: "bills", bodyshop }) && (
- <>
- {InstanceRenderManager({
- imex: (
+ {[
+ ...(HasFeatureAccess({ featureName: "export", bodyshop })
+ ? [
+
+ ,
+ InstanceRenderManager({
+ imex: (
+
+ {() => (
+
+
+
+ )}
+
+ )
+ }),
+
+
+ ,
+
+
+ 2
+ 3
+
+ ,
+
+ {() => {
+ return (
+
+
+ {t("bodyshop.labels.2tiername")}
+ {t("bodyshop.labels.2tiersource")}
+
+
+ );
+ }}
+ ,
+
+
+ ,
+
+
+
+ ]
+ : []),
+
+
+ ,
+
+
+ ,
+ InstanceRenderManager({
+ imex: (
+
+
+
+ )
+ }),
+
+
+ ,
+ ...(HasFeatureAccess({ featureName: "bills", bodyshop })
+ ? [
+ InstanceRenderManager({
+ imex: (
+
+
+
+ )
+ }),
+
+
+ ,
+
- )
- })}
-
-
-
-
-
-
- >
- )}
-
-
-
-
-
-
- {HasFeatureAccess({ featureName: "export", bodyshop }) && (
- <>
-
- {ReceivableCustomFieldSelect}
-
-
- {ReceivableCustomFieldSelect}
-
-
- {ReceivableCustomFieldSelect}
-
- {
- return {
- required: getFieldValue("enforce_class"),
- //message: t("general.validation.required"),
- type: "array"
- };
- }
- ]}
- >
-
-
-
-
-
- {ClosingPeriod.treatment === "on" && (
-
-
-
- )}
- {ADPPayroll.treatment === "on" && (
-
-
-
- )}
- {ADPPayroll.treatment === "on" && (
-
-
-
- )}
- >
- )}
+ ]
+ : []),
+
+
+ ,
+
+
+ ,
+ ...(HasFeatureAccess({ featureName: "export", bodyshop })
+ ? [
+
+ {ReceivableCustomFieldSelect}
+ ,
+
+ {ReceivableCustomFieldSelect}
+ ,
+
+ {ReceivableCustomFieldSelect}
+ ,
+ {
+ return {
+ required: getFieldValue("enforce_class"),
+ //message: t("general.validation.required"),
+ type: "array"
+ };
+ }
+ ]}
+ >
+
+ ,
+
+
+ ,
+ ...(ClosingPeriod.treatment === "on"
+ ? [
+
+
+
+ ]
+ : []),
+ ...(ADPPayroll.treatment === "on"
+ ? [
+
+
+
+ ]
+ : []),
+ ...(ADPPayroll.treatment === "on"
+ ? [
+
+
+
+ ]
+ : [])
+ ]
+ : [])
+ ]}
null}>
@@ -446,211 +491,255 @@ export function ShopInfoGeneral({ form, bodyshop }) {
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ({
- validator(rule, value) {
- if (!value && !getFieldValue(["md_hour_split", "paint"])) {
- return Promise.resolve();
- }
- if (value + getFieldValue(["md_hour_split", "paint"]) === 1) {
- return Promise.resolve();
- }
- return Promise.reject(t("bodyshop.validation.larsplit"));
+ {[
+
-
-
- ({
- validator(rule, value) {
- if (!value && !getFieldValue(["md_hour_split", "paint"])) {
- return Promise.resolve();
- }
- if (value + getFieldValue(["md_hour_split", "prep"]) === 1) {
- return Promise.resolve();
- }
- return Promise.reject(t("bodyshop.validation.larsplit"));
+ ]}
+ >
+
+ ,
+
+
+ ,
+
+
+ ,
+
+
+ ,
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {HasFeatureAccess({ featureName: "timetickets", bodyshop }) && (
- <>
-
-
-
-
-
-
-
-
-
- >
- )}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ ]}
+ >
+
+ ,
+
+
+ ,
+ ({
+ validator(rule, value) {
+ if (!value && !getFieldValue(["md_hour_split", "paint"])) {
+ return Promise.resolve();
+ }
+ if (value + getFieldValue(["md_hour_split", "paint"]) === 1) {
+ return Promise.resolve();
+ }
+ return Promise.reject(t("bodyshop.validation.larsplit"));
+ }
+ })
+ ]}
+ >
+
+ ,
+ ({
+ validator(rule, value) {
+ if (!value && !getFieldValue(["md_hour_split", "paint"])) {
+ return Promise.resolve();
+ }
+ if (value + getFieldValue(["md_hour_split", "prep"]) === 1) {
+ return Promise.resolve();
+ }
+ return Promise.reject(t("bodyshop.validation.larsplit"));
+ }
+ })
+ ]}
+ >
+
+ ,
+
+
+ ,
+
+
+ ,
+
+
+ ,
+
+
+ ,
+
+
+ ,
+
+
+ ,
+
+
+ ,
+ ...(HasFeatureAccess({ featureName: "timetickets", bodyshop })
+ ? [
+
+
+ ,
+
+
+ ,
+
+
+
+ ]
+ : []),
+
+
+ ,
+
+
+ ,
+
+
+ ,
+
+
+ ,
+
+
+ ,
+
+
+ ,
+
+
+
+ ]}
{
parent[lastKey] = value;
};
- const preserveHiddenFormData = () => {
+ const preserveHiddenFormData = useCallback(() => {
const preservationData = {};
let hasDataToPreserve = false;
@@ -51,7 +51,7 @@ export const useFormDataPreservation = (form, bodyshop, featureConfig) => {
if (hasDataToPreserve) {
form.setFieldsValue(preservationData);
}
- };
+ }, [form, featureConfig, bodyshop]);
const getCompleteFormValues = () => {
const currentFormValues = form.getFieldsValue();
@@ -88,7 +88,7 @@ export const useFormDataPreservation = (form, bodyshop, featureConfig) => {
useEffect(() => {
preserveHiddenFormData();
- }, [bodyshop]);
+ }, [bodyshop, preserveHiddenFormData]);
return { preserveHiddenFormData, getCompleteFormValues, createSubmissionHandler };
};
diff --git a/client/src/components/task-center/task-center.styles.scss b/client/src/components/task-center/task-center.styles.scss
index 062aa133d..411158e54 100644
--- a/client/src/components/task-center/task-center.styles.scss
+++ b/client/src/components/task-center/task-center.styles.scss
@@ -4,9 +4,9 @@
right: 0;
width: 500px;
max-width: 500px;
- background: #fff;
- color: rgba(0, 0, 0, 0.85);
- border: 1px solid #d9d9d9;
+ background: var(--task-bg);
+ color: var(--task-text);
+ border: 1px solid var(--task-border);
border-radius: 6px;
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08), 0 3px 6px rgba(0, 0, 0, 0.06);
z-index: 1000;
@@ -19,11 +19,11 @@
.task-header {
padding: 4px 10px;
- border-bottom: 1px solid #f0f0f0;
+ border-bottom: 1px solid var(--task-header-border);
display: flex;
justify-content: space-between;
align-items: center;
- background: #fafafa;
+ background: var(--task-header-bg);
h3 {
font-size: 14px;
@@ -32,14 +32,14 @@
.create-task-button {
border: none;
- color: white;
+ color: var(--task-button-text);
padding: 4px 12px;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
&:hover {
- background-color: #40a9ff;
+ background-color: var(--task-button-hover-bg);
}
}
}
@@ -52,10 +52,9 @@
.section-title {
padding: 0px 10px;
margin: 0px;
- //font-size: 12px;
- background: #f5f5f5;
+ background: var(--task-section-bg);
font-weight: 650;
- border-bottom: 1px solid #e8e8e8;
+ border-bottom: 1px solid var(--task-section-border);
position: sticky;
top: 0;
z-index: 1;
@@ -68,22 +67,21 @@
.task-row {
cursor: pointer;
- border-bottom: 1px solid #f0f0f0;
+ border-bottom: 1px solid var(--task-row-border);
display: flex;
justify-content: space-between;
align-items: flex-start;
&:hover {
- background: #f5f5f5;
+ background: var(--task-row-hover-bg);
}
.task-title-cell {
flex: 1;
padding: 6px 8px;
vertical-align: top;
- //font-size: 12px;
line-height: 1.2;
- max-width: 350px; // or whatever fits your layout
+ max-width: 350px;
.task-title {
font-size: 16px;
@@ -91,44 +89,42 @@
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
- max-width: 100%; // Or a specific width if you want more control
+ max-width: 100%;
display: inline-block;
vertical-align: middle;
}
.task-ro-number {
margin-top: 20px;
- color: #1677ff;
+ color: var(--task-ro-number);
}
}
.task-due-cell {
padding: 6px 8px;
vertical-align: top;
- //font-size: 12px;
line-height: 1.2;
text-align: right;
white-space: nowrap;
- color: rgba(0, 0, 0, 0.45);
+ color: var(--task-due-text);
}
}
button {
margin: 8px auto;
padding: 4px 10px;
- background-color: #1677ff;
- color: white;
+ background-color: var(--task-button-bg);
+ color: var(--task-button-text);
border: none;
border-radius: 4px;
- //font-size: 12px;
cursor: pointer;
&:hover {
- background-color: #4096ff;
+ background-color: var(--task-button-hover-bg);
}
&:disabled {
- background-color: #d9d9d9;
+ background-color: var(--task-button-disabled-bg);
cursor: not-allowed;
}
}
@@ -137,7 +133,7 @@
.error-message {
padding: 16px;
text-align: center;
- color: rgba(0, 0, 0, 0.45);
+ color: var(--task-message-text);
}
.loading-footer {
diff --git a/client/src/components/time-ticket-task-modal/time-ticket-task-modal.styles.scss b/client/src/components/time-ticket-task-modal/time-ticket-task-modal.styles.scss
index 91fd3345e..07db99890 100644
--- a/client/src/components/time-ticket-task-modal/time-ticket-task-modal.styles.scss
+++ b/client/src/components/time-ticket-task-modal/time-ticket-task-modal.styles.scss
@@ -6,7 +6,7 @@
td {
padding: 8px;
text-align: left;
- border-bottom: 1px solid #ddd;
+ border-bottom: 1px solid var(--table-border-color);
.ant-form-item {
margin-bottom: 0px !important;
@@ -14,6 +14,6 @@
}
tr:hover {
- background-color: #f5f5f5;
+ background-color: var(--table-hover-bg);
}
}
diff --git a/client/src/components/upsell/upsell.component.jsx b/client/src/components/upsell/upsell.component.jsx
index 652cbeab9..3170e4b70 100644
--- a/client/src/components/upsell/upsell.component.jsx
+++ b/client/src/components/upsell/upsell.component.jsx
@@ -11,7 +11,7 @@ import {
} from "@ant-design/icons";
import { Button, Card, Result } from "antd";
import i18n from "i18next";
-import React, { useEffect, useRef } from "react";
+import { useEffect, useRef } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { store } from "../../redux/store.js";
@@ -21,7 +21,6 @@ import "./upsell.styles.scss";
export default function UpsellComponent({ featureName, subFeatureName, upsell, disableMask }) {
const { t } = useTranslation();
const resultProps = upsell || upsellEnum[featureName][subFeatureName];
-
const componentRef = useRef(null);
useEffect(() => {
@@ -34,12 +33,10 @@ export default function UpsellComponent({ featureName, subFeatureName, upsell, d
mask.style.left = 0;
mask.style.width = "100%";
mask.style.height = "100%";
- mask.style.backgroundColor = "rgba(0, 0, 0, 0.05)";
+ mask.style.backgroundColor = "var(--mask-bg)";
// mask.style.zIndex = 9999;
parentElement.style.position = "relative";
-
parentElement.prepend(mask);
-
return () => {
parentElement.removeChild(mask);
};
@@ -47,18 +44,22 @@ export default function UpsellComponent({ featureName, subFeatureName, upsell, d
}, [disableMask]);
if (!resultProps) return ;
+
return (
} {...resultProps} />
);
}
+
//Kept in the same function as the result props line must mirror and doesnt warrant a separate function.
export function UpsellMaskWrapper({ children, upsell, featureName, subFeatureName }) {
const resultProps = upsell || upsellEnum[featureName][subFeatureName];
return (
-
{children}
+
+ {children}
+
} {...resultProps} />
@@ -71,7 +72,6 @@ export function UpsellMaskWrapper({ children, upsell, featureName, subFeatureNam
//This is kept in this function as pulling it out into it's own util/enum prevents passing JSX as an `extra` prop
export const upsellEnum = () => {
const { currentUser, bodyshop } = store.getState().user;
-
const [first_name, ...last_name] = currentUser?.displayName ? currentUser.displayName.split(" ") : [];
const LearnMoreLink = encodeURI(
InstanceRenderManager({
@@ -79,7 +79,6 @@ export const upsellEnum = () => {
rome: `https://forms.zohopublic.com/rometech/form/ROLearnMore/formperma/0G29z8LgLlvKK8nno-b7s-GHgNXwIFlrMeE0mC394L4?first_name=${first_name || ""}&last_name=${last_name.join(" ") || ""}&shop_name=${bodyshop?.shopname || ""}&email=${currentUser?.email || ""}&shop_phone=${bodyshop?.phone || ""}`
})
);
-
return {
bills: {
autoreconcile: {
diff --git a/client/src/components/upsell/upsell.styles.scss b/client/src/components/upsell/upsell.styles.scss
index bc6be85d0..dc898853e 100644
--- a/client/src/components/upsell/upsell.styles.scss
+++ b/client/src/components/upsell/upsell.styles.scss
@@ -1,6 +1,5 @@
.mask-wrapper {
position: relative;
-//Newly added
display: flex;
justify-content: center;
align-items: center;
@@ -8,12 +7,8 @@
}
.mask-content {
- // filter: blur(5px);
- background-color: rgba(0, 0, 0, 0.05);
+ background-color: var(--mask-content-bg);
pointer-events: none;
-
- //Newly added
- //width: 100%;
}
.mask-overlay {
@@ -22,35 +17,8 @@
left: 50%;
transform: translate(-50%, -50%);
z-index: 10;
- // width: 100%
}
.mask-overlay .ant-card {
max-width: 100%;
}
-
-// .mask-wrapper {
-// position: relative;
-// display: inline-block;
-// }
-
-// .mask-content {
-// filter: blur(5px);
-// pointer-events: none;
-// }
-
-// .mask-overlay {
-// position: absolute;
-// top: 0;
-// left: 0;
-// width: 100%;
-// height: 100%;
-// display: flex;
-// justify-content: center;
-// align-items: center;
-// z-index: 10;
-// }
-
-// .mask-overlay .ant-card {
-// max-width: 100%;
-// }
diff --git a/client/src/firebase/firebase.utils.js b/client/src/firebase/firebase.utils.js
index 9a09c8c73..9a836eceb 100644
--- a/client/src/firebase/firebase.utils.js
+++ b/client/src/firebase/firebase.utils.js
@@ -4,6 +4,8 @@ import { getAuth, updatePassword, updateProfile } from "@firebase/auth";
import { getFirestore } from "@firebase/firestore";
import { getMessaging, getToken, onMessage } from "@firebase/messaging";
import { store } from "../redux/store";
+import * as amplitude from '@amplitude/analytics-browser';
+import posthog from 'posthog-js'
const config = JSON.parse(import.meta.env.VITE_APP_FIREBASE_CONFIG);
initializeApp(config);
@@ -71,25 +73,33 @@ onMessage(messaging, (payload) => {
});
export const logImEXEvent = (eventName, additionalParams, stateProp = null) => {
- const state = stateProp || store.getState();
- const eventParams = {
- shop: (state.user && state.user.bodyshop && state.user.bodyshop.shopname) || null,
- user: (state.user && state.user.currentUser && state.user.currentUser.email) || null,
- ...additionalParams
- };
- // axios.post("/ioevent", {
- // useremail: (state.user && state.user.currentUser && state.user.currentUser.email) || null,
- // bodyshopid: (state.user && state.user.bodyshop && state.user.bodyshop.id) || null,
- // operationName: eventName,
- // variables: additionalParams,
- // dbevent: false,
- // env: `master-AIO|${import.meta.env.VITE_APP_GIT_SHA_DATE}`
- // });
- // console.log(
- // "%c[Analytics]",
- // "background-color: green ;font-weight:bold;",
- // eventName,
- // eventParams
- // );
- logEvent(analytics, eventName, eventParams);
+ try {
+
+ const state = stateProp || store.getState();
+ const eventParams = {
+ shop: (state.user && state.user.bodyshop && state.user.bodyshop.shopname) || null,
+ user: (state.user && state.user.currentUser && state.user.currentUser.email) || null,
+ ...additionalParams
+ };
+ // axios.post("/ioevent", {
+ // useremail: (state.user && state.user.currentUser && state.user.currentUser.email) || null,
+ // bodyshopid: (state.user && state.user.bodyshop && state.user.bodyshop.id) || null,
+ // operationName: eventName,
+ // variables: additionalParams,
+ // dbevent: false,
+ // env: `master-AIO|${import.meta.env.VITE_APP_GIT_SHA_DATE}`
+ // });
+ // console.log(
+ // "%c[Analytics]",
+ // "background-color: green ;font-weight:bold;",
+ // eventName,
+ // eventParams
+ // );
+ logEvent(analytics, eventName, eventParams);
+ amplitude.track(eventName, eventParams);
+ posthog.capture(eventName, eventParams);
+
+ } finally {
+ //If it fails, just keep going.
+ }
};
diff --git a/client/src/index.jsx b/client/src/index.jsx
index 327d57e84..3c41b9f5b 100644
--- a/client/src/index.jsx
+++ b/client/src/index.jsx
@@ -14,6 +14,8 @@ import { persistor, store } from "./redux/store";
import reportWebVitals from "./reportWebVitals";
import "./translations/i18n";
import "./utils/CleanAxios";
+import * as amplitude from "@amplitude/analytics-browser";
+import { PostHogProvider } from "posthog-js/react";
window.global ||= window;
@@ -23,10 +25,10 @@ registerSW({ immediate: true });
// Dinero.globalLocale = "en-CA";
Dinero.globalRoundingMode = "HALF_EVEN";
+amplitude.init("6228a598e57cd66875cfd41604f1f891", {});
const sentryCreateBrowserRouter = Sentry.wrapCreateBrowserRouterV6(createBrowserRouter);
const router = sentryCreateBrowserRouter(createRoutesFromElements(} />));
-
if (import.meta.env.DEV) {
let styles =
"font-weight: bold; font-size: 50px;color: red; 6px 6px 0 rgb(226,91,14) , 9px 9px 0 rgb(245,221,8) , 12px 12px 0 rgb(5,148,68) ";
@@ -37,7 +39,12 @@ function App() {
return (
} persistor={persistor}>
-
+
+
+
);
diff --git a/client/src/pages/csi/csi.container.page.jsx b/client/src/pages/csi/csi.container.page.jsx
index e3240ad38..f11ac3144 100644
--- a/client/src/pages/csi/csi.container.page.jsx
+++ b/client/src/pages/csi/csi.container.page.jsx
@@ -1,7 +1,7 @@
//import {useMutation, useQuery } from "@apollo/client";
import { Button, Form, Layout, Result, Typography } from "antd";
import axios from "axios";
-import React, { useCallback, useEffect, useState } from "react";
+import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useParams } from "react-router-dom";
@@ -16,7 +16,8 @@ import InstanceRenderManager from "../../utils/instanceRenderMgr";
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser
});
-const mapDispatchToProps = (dispatch) => ({});
+
+const mapDispatchToProps = () => ({});
export default connect(mapStateToProps, mapDispatchToProps)(CsiContainerPage);
@@ -28,7 +29,6 @@ export function CsiContainerPage({ currentUser }) {
loading: false,
submitted: false
});
-
const { t } = useTranslation();
const getAxiosData = useCallback(async () => {
@@ -39,7 +39,6 @@ export function CsiContainerPage({ currentUser }) {
console.log("Unable to attach to crisp instance. ");
}
setSubmitting((prevSubmitting) => ({ ...prevSubmitting, loading: true }));
-
const response = await axios.post("/csi/lookup", {
surveyId
});
@@ -91,7 +90,7 @@ export function CsiContainerPage({ currentUser }) {
setSubmitting({ ...submitting, loading: true, submitting: true });
const result = await axios.post("/csi/submit", { surveyId, values });
console.log("result", result);
- if (!!!result.errors && result.data.update_csi.affected_rows > 0) {
+ if (!result.errors && result.data.update_csi.affected_rows > 0) {
setSubmitting({ ...submitting, loading: false, submitted: true });
}
} catch (error) {
@@ -110,7 +109,7 @@ export function CsiContainerPage({ currentUser }) {
-
{submitting.error ? : null}
-
{submitting.submitted ? (
({
type: ApplicationActionTypes.SET_WSS_STATUS,
payload: status
});
+export const toggleDarkMode = () => ({
+ type: ApplicationActionTypes.TOGGLE_DARK_MODE
+});
+
+export const setDarkMode = (value) => ({
+ type: ApplicationActionTypes.SET_DARK_MODE,
+ payload: value
+});
diff --git a/client/src/redux/application/application.reducer.js b/client/src/redux/application/application.reducer.js
index 6d5421e27..1641dddfc 100644
--- a/client/src/redux/application/application.reducer.js
+++ b/client/src/redux/application/application.reducer.js
@@ -16,7 +16,8 @@ const INITIAL_STATE = {
},
jobReadOnly: false,
partnerVersion: null,
- alerts: {}
+ alerts: {},
+ darkMode: false
};
const applicationReducer = (state = INITIAL_STATE, action) => {
@@ -104,6 +105,18 @@ const applicationReducer = (state = INITIAL_STATE, action) => {
alerts: newAlertsMap
};
}
+ case ApplicationActionTypes.TOGGLE_DARK_MODE: {
+ const newDarkModeState = !state.darkMode;
+ return {
+ ...state,
+ darkMode: newDarkModeState
+ };
+ }
+ case ApplicationActionTypes.SET_DARK_MODE:
+ return {
+ ...state,
+ darkMode: action.payload
+ };
default:
return state;
}
diff --git a/client/src/redux/application/application.sagas.js b/client/src/redux/application/application.sagas.js
index a07c5a244..a37475325 100644
--- a/client/src/redux/application/application.sagas.js
+++ b/client/src/redux/application/application.sagas.js
@@ -6,6 +6,7 @@ import client from "../../utils/GraphQLClient";
import { CalculateLoad, CheckJobBucket } from "../../utils/SSSUtils";
import { scheduleLoadFailure, scheduleLoadSuccess, setProblemJobs } from "./application.actions";
import ApplicationActionTypes from "./application.types";
+import { logImEXEvent } from "../../firebase/firebase.utils";
export function* onCalculateScheduleLoad() {
yield takeLatest(ApplicationActionTypes.CALCULATE_SCHEDULE_LOAD, calculateScheduleLoad);
@@ -106,17 +107,14 @@ export function* calculateScheduleLoad({ payload: end }) {
const AddJobForSchedulingCalc = !item.inproduction;
- if (!!load[itemDate]) {
+ if (load[itemDate]) {
load[itemDate].allHoursIn =
(load[itemDate].allHoursIn || 0) +
item.labhrs.aggregate.sum.mod_lb_hrs +
item.larhrs.aggregate.sum.mod_lb_hrs;
- load[itemDate].allHoursInBody =
- (load[itemDate].allHoursInBody || 0) +
- item.labhrs.aggregate.sum.mod_lb_hrs;
+ load[itemDate].allHoursInBody = (load[itemDate].allHoursInBody || 0) + item.labhrs.aggregate.sum.mod_lb_hrs;
load[itemDate].allHoursInRefinish =
- (load[itemDate].allHoursInRefinish || 0) +
- item.larhrs.aggregate.sum.mod_lb_hrs;
+ (load[itemDate].allHoursInRefinish || 0) + item.larhrs.aggregate.sum.mod_lb_hrs;
//If the job hasn't already arrived, add it to the jobs in list.
// Make sure it also hasn't already been completed, or isn't an in and out job.
//This prevents the duplicate counting.
@@ -124,15 +122,9 @@ export function* calculateScheduleLoad({ payload: end }) {
if (AddJobForSchedulingCalc) {
load[itemDate].jobsIn.push(item);
load[itemDate].hoursIn =
- (load[itemDate].hoursIn || 0) +
- item.labhrs.aggregate.sum.mod_lb_hrs +
- item.larhrs.aggregate.sum.mod_lb_hrs;
- load[itemDate].hoursInBody =
- (load[itemDate].hoursInBody || 0) +
- item.labhrs.aggregate.sum.mod_lb_hrs;
- load[itemDate].hoursInRefinish =
- (load[itemDate].hoursInRefinish || 0) +
- item.larhrs.aggregate.sum.mod_lb_hrs;
+ (load[itemDate].hoursIn || 0) + item.labhrs.aggregate.sum.mod_lb_hrs + item.larhrs.aggregate.sum.mod_lb_hrs;
+ load[itemDate].hoursInBody = (load[itemDate].hoursInBody || 0) + item.labhrs.aggregate.sum.mod_lb_hrs;
+ load[itemDate].hoursInRefinish = (load[itemDate].hoursInRefinish || 0) + item.larhrs.aggregate.sum.mod_lb_hrs;
}
} else {
load[itemDate] = {
@@ -140,21 +132,14 @@ export function* calculateScheduleLoad({ payload: end }) {
jobsIn: AddJobForSchedulingCalc ? [item] : [], //Same as above, only add it if it isn't already in production.
jobsOut: [],
allJobsOut: [],
- allHoursIn:
- item.labhrs.aggregate.sum.mod_lb_hrs +
- item.larhrs.aggregate.sum.mod_lb_hrs,
+ allHoursIn: item.labhrs.aggregate.sum.mod_lb_hrs + item.larhrs.aggregate.sum.mod_lb_hrs,
allHoursInBody: item.labhrs.aggregate.sum.mod_lb_hrs,
allHoursInRefinish: item.larhrs.aggregate.sum.mod_lb_hrs,
hoursIn: AddJobForSchedulingCalc
- ? item.labhrs.aggregate.sum.mod_lb_hrs +
- item.larhrs.aggregate.sum.mod_lb_hrs
- : 0,
- hoursInBody: AddJobForSchedulingCalc
- ? item.labhrs.aggregate.sum.mod_lb_hrs
- : 0,
- hoursInRefinish: AddJobForSchedulingCalc
- ? item.larhrs.aggregate.sum.mod_lb_hrs
+ ? item.labhrs.aggregate.sum.mod_lb_hrs + item.larhrs.aggregate.sum.mod_lb_hrs
: 0,
+ hoursInBody: AddJobForSchedulingCalc ? item.labhrs.aggregate.sum.mod_lb_hrs : 0,
+ hoursInRefinish: AddJobForSchedulingCalc ? item.larhrs.aggregate.sum.mod_lb_hrs : 0
};
}
});
@@ -170,17 +155,14 @@ export function* calculateScheduleLoad({ payload: end }) {
const itemDate = dayjs(item.actual_completion || item.scheduled_completion).format("YYYY-MM-DD");
//Skip it, it's already completed.
- if (!!load[itemDate]) {
+ if (load[itemDate]) {
load[itemDate].allHoursOut =
(load[itemDate].allHoursOut || 0) +
item.labhrs.aggregate.sum.mod_lb_hrs +
item.larhrs.aggregate.sum.mod_lb_hrs;
- load[itemDate].allHoursOutBody =
- (load[itemDate].allHoursOutBody || 0) +
- item.labhrs.aggregate.sum.mod_lb_hrs;
+ load[itemDate].allHoursOutBody = (load[itemDate].allHoursOutBody || 0) + item.labhrs.aggregate.sum.mod_lb_hrs;
load[itemDate].allHoursOutRefinish =
- (load[itemDate].allHoursOutRefinish || 0) +
- item.larhrs.aggregate.sum.mod_lb_hrs;
+ (load[itemDate].allHoursOutRefinish || 0) + item.larhrs.aggregate.sum.mod_lb_hrs;
//Add only the jobs that are still in production to get rid of.
//If it's not in production, we'd subtract unnecessarily.
load[itemDate].allJobsOut.push(item);
@@ -191,12 +173,9 @@ export function* calculateScheduleLoad({ payload: end }) {
(load[itemDate].hoursOut || 0) +
item.labhrs.aggregate.sum.mod_lb_hrs +
item.larhrs.aggregate.sum.mod_lb_hrs;
- load[itemDate].hoursOutBody =
- (load[itemDate].hoursOutBody || 0) +
- item.labhrs.aggregate.sum.mod_lb_hrs;
+ load[itemDate].hoursOutBody = (load[itemDate].hoursOutBody || 0) + item.labhrs.aggregate.sum.mod_lb_hrs;
load[itemDate].hoursOutRefinish =
- (load[itemDate].hoursOutRefinish || 0) +
- item.larhrs.aggregate.sum.mod_lb_hrs;
+ (load[itemDate].hoursOutRefinish || 0) + item.larhrs.aggregate.sum.mod_lb_hrs;
}
} else {
load[itemDate] = {
@@ -205,11 +184,9 @@ export function* calculateScheduleLoad({ payload: end }) {
hoursOut: AddJobForSchedulingCalc
? item.labhrs.aggregate.sum.mod_lb_hrs + item.larhrs.aggregate.sum.mod_lb_hrs
: 0,
- allHoursOut:
- item.labhrs.aggregate.sum.mod_lb_hrs +
- item.larhrs.aggregate.sum.mod_lb_hrs,
+ allHoursOut: item.labhrs.aggregate.sum.mod_lb_hrs + item.larhrs.aggregate.sum.mod_lb_hrs,
allHoursOutBody: item.labhrs.aggregate.sum.mod_lb_hrs,
- allHoursOutRefinish: item.larhrs.aggregate.sum.mod_lb_hrs,
+ allHoursOutRefinish: item.larhrs.aggregate.sum.mod_lb_hrs
};
}
});
@@ -222,7 +199,7 @@ export function* calculateScheduleLoad({ payload: end }) {
const prev = dayjs(today)
.add(day - 1, "day")
.format("YYYY-MM-DD");
- if (!!!load[current]) {
+ if (!load[current]) {
load[current] = {};
}
@@ -298,6 +275,14 @@ export function* insertAuditTrailSaga({ payload: { jobid, billid, operation, typ
});
}
-export function* applicationSagas() {
- yield all([call(onCalculateScheduleLoad), call(onInsertAuditTrail)]);
+export function* onToggleDarkMode() {
+ yield takeLatest(ApplicationActionTypes.TOGGLE_DARK_MODE, function* () {
+ const state = yield select();
+ const darkMode = state.application.darkMode;
+ logImEXEvent("dark_mode_toggled", { darkMode });
+ });
+}
+
+export function* applicationSagas() {
+ yield all([call(onCalculateScheduleLoad), call(onInsertAuditTrail), call(onToggleDarkMode)]);
}
diff --git a/client/src/redux/application/application.selectors.js b/client/src/redux/application/application.selectors.js
index 6b0d1c2c4..606031be7 100644
--- a/client/src/redux/application/application.selectors.js
+++ b/client/src/redux/application/application.selectors.js
@@ -24,3 +24,4 @@ export const selectProblemJobs = createSelector([selectApplication], (applicatio
export const selectUpdateAvailable = createSelector([selectApplication], (application) => application.updateAvailable);
export const selectWssStatus = createSelector([selectApplication], (application) => application.wssStatus);
export const selectAlerts = createSelector([selectApplication], (application) => application.alerts);
+export const selectDarkMode = createSelector([selectApplication], (application) => application.darkMode);
diff --git a/client/src/redux/application/application.types.js b/client/src/redux/application/application.types.js
index 26c1b4c7d..9b1695f32 100644
--- a/client/src/redux/application/application.types.js
+++ b/client/src/redux/application/application.types.js
@@ -14,6 +14,8 @@ const ApplicationActionTypes = {
SET_PROBLEM_JOBS: "SET_PROBLEM_JOBS",
SET_UPDATE_AVAILABLE: "SET_UPDATE_AVAILABLE",
SET_WSS_STATUS: "SET_WSS_STATUS",
- ADD_ALERTS: "ADD_ALERTS"
+ ADD_ALERTS: "ADD_ALERTS",
+ TOGGLE_DARK_MODE: "TOGGLE_DARK_MODE",
+ SET_DARK_MODE: "SET_DARK_MODE"
};
export default ApplicationActionTypes;
diff --git a/client/src/redux/user/user.sagas.js b/client/src/redux/user/user.sagas.js
index 79e3926f7..362118e71 100644
--- a/client/src/redux/user/user.sagas.js
+++ b/client/src/redux/user/user.sagas.js
@@ -48,6 +48,8 @@ import {
validatePasswordResetSuccess
} from "./user.actions";
import UserActionTypes from "./user.types";
+import * as amplitude from "@amplitude/analytics-browser";
+import posthog from "posthog-js";
const fpPromise = FingerprintJS.load();
@@ -82,8 +84,6 @@ export function* onCheckUserSession() {
export function* isUserAuthenticated() {
try {
- logImEXEvent("redux_auth_check");
-
const user = yield getCurrentUser();
if (!user) {
yield put(unauthorizedUser());
@@ -91,6 +91,8 @@ export function* isUserAuthenticated() {
}
LogRocket.identify(user.email);
+ amplitude.setUserId(user.email);
+ posthog.identify(user.email);
const eulaQuery = yield client.query({
query: QUERY_EULA,
@@ -136,7 +138,8 @@ export function* signOutStart() {
imexshopid: state.user.bodyshop.imexshopid,
type: "messaging"
});
- } catch (error) {
+ amplitude.reset();
+ } catch {
console.log("No FCM token. Skipping unsubscribe.");
}
@@ -161,7 +164,7 @@ export function* updateUserDetails(userDetails) {
type: "success",
message: i18next.t("profile.successes.updated")
});
- } catch (error) {
+ } catch {
//yield put(signOutFailure(error.message));
}
}
@@ -268,7 +271,7 @@ export function* signInSuccessSaga({ payload }) {
setUserId(analytics, payload.email);
setUserProperties(analytics, payload);
- yield logImEXEvent("redux_sign_in_success");
+ yield;
}
export function* onSendPasswordResetStart() {
@@ -335,6 +338,7 @@ export function* SetAuthLevelFromShopDetails({ payload }) {
}
try {
+ amplitude.setGroup("Shop", payload.shopname);
window.$crisp.push(["set", "user:company", [payload.shopname]]);
window.$crisp.push(["set", "session:segments", [[`region:${payload.region_config}`]]]);
if (authRecord[0] && authRecord[0].user.validemail) {
diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json
index cc09ea94a..b24535d7e 100644
--- a/client/src/translations/en_us/common.json
+++ b/client/src/translations/en_us/common.json
@@ -1456,9 +1456,9 @@
},
"notifications": {
"error": {
- "description": "Please try again. Make sure the refund amount does not exceeds the payment amount.",
+ "description": "An error has occurred processing the refund: {{message}}",
"openingip": "Error connecting to IntelliPay service.",
- "title": "Error placing refund"
+ "title": "Error issuing refund"
}
},
"titles": {
@@ -3782,7 +3782,9 @@
"actions": {
"changepassword": "Change Password",
"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}}"
diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json
index f4d6498d8..5e4b03372 100644
--- a/client/src/translations/es/common.json
+++ b/client/src/translations/es/common.json
@@ -3782,7 +3782,9 @@
"actions": {
"changepassword": "",
"signout": "desconectar",
- "updateprofile": "Actualización del perfil"
+ "updateprofile": "Actualización del perfil",
+ "light_theme": "",
+ "dark_theme": ""
},
"errors": {
"updating": ""
diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json
index 1dd5b385e..a33e8b971 100644
--- a/client/src/translations/fr/common.json
+++ b/client/src/translations/fr/common.json
@@ -3782,7 +3782,9 @@
"actions": {
"changepassword": "",
"signout": "Déconnexion",
- "updateprofile": "Mettre à jour le profil"
+ "updateprofile": "Mettre à jour le profil",
+ "light_theme": "",
+ "dark_theme": ""
},
"errors": {
"updating": ""
diff --git a/server/accounting/qbo/qbo-payments.js b/server/accounting/qbo/qbo-payments.js
index f0d4ccdde..db288efe5 100644
--- a/server/accounting/qbo/qbo-payments.js
+++ b/server/accounting/qbo/qbo-payments.js
@@ -10,7 +10,6 @@ const queries = require("../../graphql-client/queries");
const { refresh: refreshOauthToken, setNewRefreshToken } = require("./qbo-callback");
const OAuthClient = require("intuit-oauth");
const moment = require("moment-timezone");
-const GraphQLClient = require("graphql-request").GraphQLClient;
const {
QueryInsuranceCo,
InsertInsuranceCo,
@@ -28,7 +27,7 @@ exports.default = async (req, res) => {
clientId: process.env.QBO_CLIENT_ID,
clientSecret: process.env.QBO_SECRET,
environment: process.env.NODE_ENV === "production" ? "production" : "sandbox",
- redirectUri: process.env.QBO_REDIRECT_URI,
+ redirectUri: process.env.QBO_REDIRECT_URI
});
try {
//Fetch the API Access Tokens & Set them for the session.
@@ -131,22 +130,20 @@ exports.default = async (req, res) => {
// //No error. Mark the payment exported & insert export log.
if (elgen) {
- const result = await client
- .setHeaders({ Authorization: BearerToken })
- .request(queries.QBO_MARK_PAYMENT_EXPORTED, {
- paymentId: payment.id,
- payment: {
- exportedat: moment().tz(bodyshop.timezone)
- },
- logs: [
- {
- bodyshopid: bodyshop.id,
- paymentid: payment.id,
- successful: true,
- useremail: req.user.email
- }
- ]
- });
+ await client.setHeaders({ Authorization: BearerToken }).request(queries.QBO_MARK_PAYMENT_EXPORTED, {
+ paymentId: payment.id,
+ payment: {
+ exportedat: moment().tz(bodyshop.timezone)
+ },
+ logs: [
+ {
+ bodyshopid: bodyshop.id,
+ paymentid: payment.id,
+ successful: true,
+ useremail: req.user.email
+ }
+ ]
+ });
}
ret.push({ paymentid: payment.id, success: true });
@@ -156,7 +153,7 @@ exports.default = async (req, res) => {
});
//Add the export log error.
if (elgen) {
- const result = await client.setHeaders({ Authorization: BearerToken }).request(queries.INSERT_EXPORT_LOG, {
+ await client.setHeaders({ Authorization: BearerToken }).request(queries.INSERT_EXPORT_LOG, {
logs: [
{
bodyshopid: bodyshop.id,
@@ -190,7 +187,7 @@ exports.default = async (req, res) => {
}
};
-async function InsertPayment(oauthClient, qbo_realmId, req, payment, parentRef, bodyshop) {
+async function InsertPayment(oauthClient, qbo_realmId, req, payment, parentRef) {
const { paymentMethods, invoices } = await QueryMetaData(
oauthClient,
qbo_realmId,
@@ -227,20 +224,20 @@ async function InsertPayment(oauthClient, qbo_realmId, req, payment, parentRef,
PaymentRefNum: payment.transactionid,
...(invoices && invoices.length === 1 && invoices[0]
? {
- Line: [
- {
- Amount: Dinero({
- amount: Math.round(payment.amount * 100)
- }).toFormat(DineroQbFormat),
- LinkedTxn: [
- {
- TxnId: invoices[0].Id,
- TxnType: "Invoice"
- }
- ]
- }
- ]
- }
+ Line: [
+ {
+ Amount: Dinero({
+ amount: Math.round(payment.amount * 100)
+ }).toFormat(DineroQbFormat),
+ LinkedTxn: [
+ {
+ TxnId: invoices[0].Id,
+ TxnType: "Invoice"
+ }
+ ]
+ }
+ ]
+ }
: {})
};
logger.log("qbo-payments-objectlog", "DEBUG", req.user.email, payment.id, {
@@ -263,7 +260,7 @@ async function InsertPayment(oauthClient, qbo_realmId, req, payment, parentRef,
status: result.response?.status,
bodyshopid: payment.job.shopid,
email: req.user.email
- })
+ });
setNewRefreshToken(req.user.email, result);
return result && result.Bill;
} catch (error) {
@@ -291,7 +288,7 @@ async function QueryMetaData(oauthClient, qbo_realmId, req, ro_number, isCreditM
status: invoice.response?.status,
bodyshopid,
email: req.user.email
- })
+ });
const paymentMethods = await oauthClient.makeApiCall({
url: urlBuilder(qbo_realmId, "query", `select * From PaymentMethod`),
method: "POST",
@@ -306,7 +303,7 @@ async function QueryMetaData(oauthClient, qbo_realmId, req, ro_number, isCreditM
status: paymentMethods.response?.status,
bodyshopid,
email: req.user.email
- })
+ });
setNewRefreshToken(req.user.email, paymentMethods);
// const classes = await oauthClient.makeApiCall({
@@ -358,7 +355,7 @@ async function QueryMetaData(oauthClient, qbo_realmId, req, ro_number, isCreditM
status: taxCodes.response?.status,
bodyshopid,
email: req.user.email
- })
+ });
const items = await oauthClient.makeApiCall({
url: urlBuilder(qbo_realmId, "query", `select * From Item`),
method: "POST",
@@ -373,7 +370,7 @@ async function QueryMetaData(oauthClient, qbo_realmId, req, ro_number, isCreditM
status: items.response?.status,
bodyshopid,
email: req.user.email
- })
+ });
setNewRefreshToken(req.user.email, items);
const itemMapping = {};
@@ -412,8 +409,8 @@ async function QueryMetaData(oauthClient, qbo_realmId, req, ro_number, isCreditM
};
}
-async function InsertCreditMemo(oauthClient, qbo_realmId, req, payment, parentRef, bodyshop) {
- const { paymentMethods, invoices, items, taxCodes } = await QueryMetaData(
+async function InsertCreditMemo(oauthClient, qbo_realmId, req, payment, parentRef) {
+ const { invoices, items, taxCodes } = await QueryMetaData(
oauthClient,
qbo_realmId,
req,
@@ -449,14 +446,14 @@ async function InsertCreditMemo(oauthClient, qbo_realmId, req, payment, parentRe
TaxCodeRef: {
value:
taxCodes[
- findTaxCode(
- {
- local: false,
- federal: false,
- state: false
- },
- payment.job.bodyshop.md_responsibility_centers.sales_tax_codes
- )
+ findTaxCode(
+ {
+ local: false,
+ federal: false,
+ state: false
+ },
+ payment.job.bodyshop.md_responsibility_centers.sales_tax_codes
+ )
]
}
}
@@ -483,12 +480,14 @@ async function InsertCreditMemo(oauthClient, qbo_realmId, req, payment, parentRe
status: result.response?.status,
bodyshopid: req.user.bodyshopid,
email: req.user.email
- })
+ });
setNewRefreshToken(req.user.email, result);
return result && result.Bill;
} catch (error) {
logger.log("qbo-payables-error", "DEBUG", req.user.email, payment.id, {
- error: error && error.message,
+ error: error,
+ validationError: JSON.stringify(error?.response?.data),
+ accountmeta: JSON.stringify({ items, taxCodes }),
method: "InsertCreditMemo"
});
throw error;
diff --git a/server/data/autohouse.js b/server/data/autohouse.js
index 81679522b..586d9789b 100644
--- a/server/data/autohouse.js
+++ b/server/data/autohouse.js
@@ -3,7 +3,6 @@ const queries = require("../graphql-client/queries");
const Dinero = require("dinero.js");
const moment = require("moment-timezone");
var builder = require("xmlbuilder2");
-const _ = require("lodash");
const logger = require("../utils/logger");
const fs = require("fs");
require("dotenv").config({
@@ -16,6 +15,7 @@ const { sendServerEmail } = require("../email/sendemail");
const AHDineroFormat = "0.00";
const AhDateFormat = "MMDDYYYY";
+const NON_ASCII_REGEX = /[^\x20-\x7E]/g;
const repairOpCodes = ["OP4", "OP9", "OP10"];
const replaceOpCodes = ["OP2", "OP5", "OP11", "OP12"];
@@ -37,13 +37,11 @@ const ftpSetup = {
exports.default = async (req, res) => {
// Only process if in production environment.
if (process.env.NODE_ENV !== "production") {
- res.sendStatus(403);
- return;
+ return res.sendStatus(403);
}
// Only process if the appropriate token is provided.
if (req.headers["x-imex-auth"] !== process.env.AUTOHOUSE_AUTH_TOKEN) {
- res.sendStatus(401);
- return;
+ return res.sendStatus(401);
}
// Send immediate response and continue processing.
@@ -822,7 +820,7 @@ const GenerateDetailLines = (job, line, statuses) => {
BackOrdered: line.status === statuses.default_bo ? "1" : "0",
Cost: (line.billlines[0] && (line.billlines[0].actual_cost * line.billlines[0].quantity).toFixed(2)) || 0,
//Critical: null,
- Description: line.line_desc ? line.line_desc.replace(/[^\x00-\x7F]/g, "") : "",
+ Description: line.line_desc ? line.line_desc.replace(NON_ASCII_REGEX, "") : "",
DiscountMarkup: line.prt_dsmk_m || 0,
InvoiceNumber: line.billlines[0] && line.billlines[0].bill.invoice_number,
IOUPart: 0,
@@ -834,7 +832,7 @@ const GenerateDetailLines = (job, line, statuses) => {
OriginalCost: null,
OriginalInvoiceNumber: null,
PriceEach: line.act_price || 0,
- PartNumber: line.oem_partno ? line.oem_partno.replace(/[^\x00-\x7F]/g, "") : "",
+ PartNumber: line.oem_partno ? line.oem_partno.replace(NON_ASCII_REGEX, "") : "",
ProfitPercent: null,
PurchaseOrderNumber: null,
Qty: line.part_qty || 0,
diff --git a/server/data/carfax.js b/server/data/carfax.js
new file mode 100644
index 000000000..e1583a365
--- /dev/null
+++ b/server/data/carfax.js
@@ -0,0 +1,408 @@
+const path = require("path");
+const queries = require("../graphql-client/queries");
+const Dinero = require("dinero.js");
+const moment = require("moment-timezone");
+const logger = require("../utils/logger");
+const InstanceManager = require("../utils/instanceMgr").default;
+const { isString, isEmpty } = require("lodash");
+const fs = require("fs");
+const client = require("../graphql-client/graphql-client").client;
+const { sendServerEmail } = require("../email/sendemail");
+const { uploadFileToS3 } = require("../utils/s3");
+const crypto = require("crypto");
+
+require("dotenv").config({
+ path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`)
+});
+let Client = require("ssh2-sftp-client");
+
+const AHDateFormat = "YYYY-MM-DD";
+
+const NON_ASCII_REGEX = /[^\x20-\x7E]/g;
+
+const ftpSetup = {
+ host: process.env.CARFAX_HOST,
+ port: process.env.CARFAX_PORT,
+ username: process.env.CARFAX_USER,
+ password: process.env.CARFAX_PASSWORD,
+ debug:
+ process.env.NODE_ENV !== "production"
+ ? (message, ...data) => logger.log(message, "DEBUG", "api", null, data)
+ : () => {},
+ algorithms: {
+ serverHostKey: ["ssh-rsa", "ssh-dss", "rsa-sha2-256", "rsa-sha2-512", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384"]
+ }
+};
+
+const S3_BUCKET_NAME = InstanceManager({
+ imex: "imex-carfax-uploads",
+ rome: "rome-carfax-uploads"
+});
+const region = InstanceManager.InstanceRegion;
+const isLocal = isString(process.env?.LOCALSTACK_HOSTNAME) && !isEmpty(process.env?.LOCALSTACK_HOSTNAME);
+
+const uploadToS3 = (jsonObj) => {
+ const webPath = isLocal
+ ? `https://${S3_BUCKET_NAME}.s3.localhost.localstack.cloud:4566/${jsonObj.filename}`
+ : `https://${S3_BUCKET_NAME}.s3.${region}.amazonaws.com/${jsonObj.filename}`;
+
+ uploadFileToS3({ bucketName: S3_BUCKET_NAME, key: jsonObj.filename, content: jsonObj.json })
+ .then(() => {
+ logger.log("CARFAX-s3-upload", "DEBUG", "api", jsonObj.bodyshopid, {
+ imexshopid: jsonObj.imexshopid,
+ filename: jsonObj.filename,
+ webPath
+ });
+ })
+ .catch((error) => {
+ logger.log("CARFAX-s3-upload-error", "ERROR", "api", jsonObj.bodyshopid, {
+ imexshopid: jsonObj.imexshopid,
+ filename: jsonObj.filename,
+ webPath,
+ error: error.message,
+ stack: error.stack
+ });
+ });
+};
+
+exports.default = async (req, res) => {
+ // Only process if in production environment.
+ if (process.env.NODE_ENV !== "production") {
+ return res.sendStatus(403);
+ }
+ // Only process if the appropriate token is provided.
+ if (req.headers["x-imex-auth"] !== process.env.AUTOHOUSE_AUTH_TOKEN) {
+ return res.sendStatus(401);
+ }
+
+ // Send immediate response and continue processing.
+ res.status(202).json({
+ success: true,
+ message: "Processing request ...",
+ timestamp: new Date().toISOString()
+ });
+
+ try {
+ logger.log("CARFAX-start", "DEBUG", "api", null, null);
+ const allXMLResults = [];
+ const allErrors = [];
+
+ const { bodyshops } = await client.request(queries.GET_CARFAX_SHOPS); //Query for the List of Bodyshop Clients.
+ const specificShopIds = req.body.bodyshopIds; // ['uuid];
+ const { start, end, skipUpload, ignoreDateFilter } = req.body; //YYYY-MM-DD
+
+ const shopsToProcess =
+ specificShopIds?.length > 0 ? bodyshops.filter((shop) => specificShopIds.includes(shop.id)) : bodyshops;
+ logger.log("CARFAX-shopsToProcess-generated", "DEBUG", "api", null, null);
+
+ if (shopsToProcess.length === 0) {
+ logger.log("CARFAX-shopsToProcess-empty", "DEBUG", "api", null, null);
+ return;
+ }
+
+ await processShopData(shopsToProcess, start, end, skipUpload, ignoreDateFilter, allXMLResults, allErrors);
+
+ await sendServerEmail({
+ subject: `CARFAX Report ${moment().format("MM-DD-YY")}`,
+ text: `Errors:\n${JSON.stringify(allErrors, null, 2)}\n\nUploaded:\n${JSON.stringify(
+ allXMLResults.map((x) => ({
+ imexshopid: x.imexshopid,
+ filename: x.filename,
+ count: x.count,
+ result: x.result
+ })),
+ null,
+ 2
+ )}`
+ });
+
+ logger.log("CARFAX-end", "DEBUG", "api", null, null);
+ } catch (error) {
+ logger.log("CARFAX-error", "ERROR", "api", null, { error: error.message, stack: error.stack });
+ }
+};
+
+async function processShopData(shopsToProcess, start, end, skipUpload, ignoreDateFilter, allXMLResults, allErrors) {
+ for (const bodyshop of shopsToProcess) {
+ const shopid = bodyshop.imexshopid?.toLowerCase() || bodyshop.shopname.replace(/[^a-zA-Z0-9]/g, "").toLowerCase()
+ const erroredJobs = [];
+ try {
+ logger.log("CARFAX-start-shop-extract", "DEBUG", "api", bodyshop.id, {
+ shopname: bodyshop.shopname
+ });
+
+ const { jobs, bodyshops_by_pk } = await client.request(queries.CARFAX_QUERY, {
+ bodyshopid: bodyshop.id,
+ ...(ignoreDateFilter
+ ? {}
+ : {
+ start: start ? moment(start).startOf("day") : moment().subtract(7, "days").startOf("day"),
+ ...(end && { end: moment(end).endOf("day") })
+ })
+ });
+
+ const carfaxObject = {
+ shopid: shopid,
+ shop_name: bodyshop.shopname,
+ job: jobs.map((j) =>
+ CreateRepairOrderTag({ ...j, bodyshop: bodyshops_by_pk }, function ({ job, error }) {
+ erroredJobs.push({ job: job, error: error.toString() });
+ })
+ )
+ };
+
+ if (erroredJobs.length > 0) {
+ logger.log("CARFAX-failed-jobs", "ERROR", "api", bodyshop.id, {
+ count: erroredJobs.length,
+ jobs: JSON.stringify(erroredJobs.map((j) => j.job.ro_number))
+ });
+ }
+
+ const jsonObj = {
+ bodyshopid: bodyshop.id,
+ imexshopid: shopid,
+ json: JSON.stringify(carfaxObject, null, 2),
+ filename: `${shopid}_${moment().format("DDMMYYYY_HHMMss")}.json`,
+ count: carfaxObject.job.length
+ };
+
+ if (skipUpload) {
+ fs.writeFileSync(`./logs/${jsonObj.filename}`, jsonObj.json);
+ } else {
+ await uploadViaSFTP(jsonObj);
+ }
+
+ allXMLResults.push({
+ bodyshopid: bodyshop.id,
+ imexshopid: shopid,
+ count: jsonObj.count,
+ filename: jsonObj.filename,
+ result: jsonObj.result
+ });
+
+ logger.log("CARFAX-end-shop-extract", "DEBUG", "api", bodyshop.id, {
+ shopname: bodyshop.shopname
+ });
+ } catch (error) {
+ //Error at the shop level.
+ logger.log("CARFAX-error-shop", "ERROR", "api", bodyshop.id, { error: error.message, stack: error.stack });
+
+ allErrors.push({
+ bodyshopid: bodyshop.id,
+ imexshopid: shopid,
+ CARFAXid: bodyshop.CARFAXid,
+ fatal: true,
+ errors: [error.toString()]
+ });
+ } finally {
+ allErrors.push({
+ bodyshopid: bodyshop.id,
+ imexshopid: shopid,
+ CARFAXid: bodyshop.CARFAXid,
+ errors: erroredJobs.map((ej) => ({
+ ro_number: ej.job?.ro_number,
+ jobid: ej.job?.id,
+ error: ej.error
+ }))
+ });
+ }
+ }
+}
+
+async function uploadViaSFTP(jsonObj) {
+ const sftp = new Client();
+ sftp.on("error", (errors) =>
+ logger.log("CARFAX-sftp-connection-error", "ERROR", "api", jsonObj.bodyshopid, {
+ error: errors.message,
+ stack: errors.stack
+ })
+ );
+ try {
+ // Upload to S3 first.
+ uploadToS3(jsonObj);
+
+ //Connect to the FTP and upload all.
+ await sftp.connect(ftpSetup);
+
+ try {
+ jsonObj.result = await sftp.put(Buffer.from(jsonObj.json), `${jsonObj.filename}`);
+ logger.log("CARFAX-sftp-upload", "DEBUG", "api", jsonObj.bodyshopid, {
+ imexshopid: jsonObj.imexshopid,
+ filename: jsonObj.filename,
+ result: jsonObj.result
+ });
+ } catch (error) {
+ logger.log("CARFAX-sftp-upload-error", "ERROR", "api", jsonObj.bodyshopid, {
+ filename: jsonObj.filename,
+ error: error.message,
+ stack: error.stack
+ });
+ throw error;
+ }
+ } catch (error) {
+ logger.log("CARFAX-sftp-error", "ERROR", "api", jsonObj.bodyshopid, { error: error.message, stack: error.stack });
+ throw error;
+ } finally {
+ sftp.end();
+ }
+}
+
+const CreateRepairOrderTag = (job, errorCallback) => {
+ if (!job.job_totals) {
+ errorCallback({
+ jobid: job.id,
+ job: job,
+ ro_number: job.ro_number,
+ error: { toString: () => "No job totals for RO." }
+ });
+ return {};
+ }
+
+ try {
+ const ret = {
+ ro_number: crypto.createHash("md5").update(job.ro_number, "utf8").digest("hex"),
+ v_vin: job.v_vin || "",
+ v_year: job.v_model_yr
+ ? parseInt(job.v_model_yr.match(/\d/g))
+ ? parseInt(job.v_model_yr.match(/\d/g).join(""), 10)
+ : ""
+ : "",
+ v_make: job.v_make_desc || "",
+ v_model: job.v_model_desc || "",
+
+ date_estimated:
+ (job.date_estimated && moment(job.date_estimated).tz(job.bodyshop.timezone).format(AHDateFormat)) ||
+ (job.created_at && moment(job.created_at).tz(job.bodyshop.timezone).format(AHDateFormat)) ||
+ "",
+ data_opened:
+ (job.date_open && moment(job.date_open).tz(job.bodyshop.timezone).format(AHDateFormat)) ||
+ (job.created_at && moment(job.created_at).tz(job.bodyshop.timezone).format(AHDateFormat)) ||
+ "",
+ date_invoiced:
+ (job.date_invoiced && moment(job.date_invoiced).tz(job.bodyshop.timezone).format(AHDateFormat)) || "",
+ loss_date: (job.loss_date && moment(job.loss_date).format(AHDateFormat)) || "",
+
+ ins_co_nm: job.ins_co_nm || "",
+ loss_desc: job.loss_desc || "",
+ theft_ind: job.theft_ind,
+ tloss_ind: job.tlos_ind,
+ subtotal: Dinero(job.job_totals.totals.subtotal).toUnit(),
+
+ areaofdamage: {
+ impact1: generateAreaOfDamage(job.area_of_damage?.impact1 || ""),
+ impact2: generateAreaOfDamage(job.area_of_damage?.impact2 || "")
+ },
+
+ jobLines: job.joblines.length > 0 ? job.joblines.map((jl) => GenerateDetailLines(jl)) : [generateNullDetailLine()]
+ };
+ return ret;
+ } catch (error) {
+ logger.log("CARFAX-job-data-error", "ERROR", "api", null, { error: error.message, stack: error.stack });
+ errorCallback({ jobid: job.id, ro_number: job.ro_number, error });
+ }
+};
+
+const GenerateDetailLines = (line) => {
+ const ret = {
+ line_desc: line.line_desc ? line.line_desc.replace(NON_ASCII_REGEX, "") : null,
+ oem_partno: line.oem_partno ? line.oem_partno.replace(NON_ASCII_REGEX, "") : null,
+ alt_partno: line.alt_partno ? line.alt_partno.replace(NON_ASCII_REGEX, "") : null,
+ lbr_ty: generateLaborType(line.mod_lbr_ty),
+ part_qty: line.part_qty || 0,
+ part_type: generatePartType(line.part_type),
+ act_price: line.act_price || 0
+ };
+ return ret;
+};
+
+const generateNullDetailLine = () => {
+ return {
+ line_desc: null,
+ oem_partno: null,
+ alt_partno: null,
+ lbr_ty: null,
+ part_qty: 0,
+ part_type: null,
+ act_price: 0
+ };
+};
+
+const generateAreaOfDamage = (loc) => {
+ const areaMap = {
+ "01": "Right Front Corner",
+ "02": "Right Front Side",
+ "03": "Right Side",
+ "04": "Right Rear Side",
+ "05": "Right Rear Corner",
+ "06": "Rear",
+ "07": "Left Rear Corner",
+ "08": "Left Rear Side",
+ "09": "Left Side",
+ 10: "Left Front Side",
+ 11: "Left Front Corner",
+ 12: "Front",
+ 13: "Rollover",
+ 14: "Uknown",
+ 15: "Total Loss",
+ 16: "Non-Collision",
+ 19: "All Over",
+ 25: "Hood",
+ 26: "Deck Lid",
+ 27: "Roof",
+ 28: "Undercarriage",
+ 34: "All Over"
+ };
+ return areaMap[loc] || null;
+};
+
+const generateLaborType = (type) => {
+ const laborTypeMap = {
+ laa: "Aluminum",
+ lab: "Body",
+ lad: "Diagnostic",
+ lae: "Electrical",
+ laf: "Frame",
+ lag: "Glass",
+ lam: "Mechanical",
+ lar: "Refinish",
+ las: "Structural",
+ lau: "Other - LAU",
+ la1: "Other - LA1",
+ la2: "Other - LA2",
+ la3: "Other - LA3",
+ la4: "Other - LA4",
+ null: "Other",
+ mapa: "Paint Materials",
+ mash: "Shop Materials",
+ rates_subtotal: "Labor Total",
+ "timetickets.labels.shift": "Shift",
+ "timetickets.labels.amshift": "Morning Shift",
+ "timetickets.labels.ambreak": "Morning Break",
+ "timetickets.labels.pmshift": "Afternoon Shift",
+ "timetickets.labels.pmbreak": "Afternoon Break",
+ "timetickets.labels.lunch": "Lunch"
+ };
+
+ return laborTypeMap[type?.toLowerCase()] || null;
+};
+
+const generatePartType = (type) => {
+ const partTypeMap = {
+ paa: "Aftermarket",
+ pae: "Existing",
+ pag: "Glass",
+ pal: "LKQ",
+ pan: "OEM",
+ pao: "Other",
+ pas: "Sublet",
+ pasl: "Sublet",
+ ccc: "CC Cleaning",
+ ccd: "CC Damage Waiver",
+ ccdr: "CC Daily Rate",
+ ccf: "CC Refuel",
+ ccm: "CC Mileage",
+ prt_dsmk_total: "Line Item Adjustment"
+ };
+
+ return partTypeMap[type?.toLowerCase()] || null;
+};
diff --git a/server/data/chatter.js b/server/data/chatter.js
index 3f84988ca..c54a58c2c 100644
--- a/server/data/chatter.js
+++ b/server/data/chatter.js
@@ -28,13 +28,11 @@ const ftpSetup = {
exports.default = async (req, res) => {
// Only process if in production environment.
if (process.env.NODE_ENV !== "production") {
- res.sendStatus(403);
- return;
+ return res.sendStatus(403);
}
// Only process if the appropriate token is provided.
if (req.headers["x-imex-auth"] !== process.env.AUTOHOUSE_AUTH_TOKEN) {
- res.sendStatus(401);
- return;
+ return res.sendStatus(401);
}
// Send immediate response and continue processing.
diff --git a/server/data/claimscorp.js b/server/data/claimscorp.js
index 70737bd03..cadeef606 100644
--- a/server/data/claimscorp.js
+++ b/server/data/claimscorp.js
@@ -3,7 +3,6 @@ const queries = require("../graphql-client/queries");
const Dinero = require("dinero.js");
const moment = require("moment-timezone");
var builder = require("xmlbuilder2");
-const _ = require("lodash");
const logger = require("../utils/logger");
const fs = require("fs");
require("dotenv").config({
@@ -36,13 +35,11 @@ const ftpSetup = {
exports.default = async (req, res) => {
// Only process if in production environment.
if (process.env.NODE_ENV !== "production") {
- res.sendStatus(403);
- return;
+ return res.sendStatus(403);
}
// Only process if the appropriate token is provided.
if (req.headers["x-imex-auth"] !== process.env.AUTOHOUSE_AUTH_TOKEN) {
- res.sendStatus(401);
- return;
+ return res.sendStatus(401);
}
// Send immediate response and continue processing.
diff --git a/server/data/data.js b/server/data/data.js
index e89d041b9..9d88f14d8 100644
--- a/server/data/data.js
+++ b/server/data/data.js
@@ -5,4 +5,5 @@ exports.claimscorp = require("./claimscorp").default;
exports.kaizen = require("./kaizen").default;
exports.usageReport = require("./usageReport").default;
exports.podium = require("./podium").default;
-exports.emsUpload = require("./emsUpload").default;
\ No newline at end of file
+exports.emsUpload = require("./emsUpload").default;
+exports.carfax = require("./carfax").default;
\ No newline at end of file
diff --git a/server/data/kaizen.js b/server/data/kaizen.js
index 897a57770..d8d3a9dc5 100644
--- a/server/data/kaizen.js
+++ b/server/data/kaizen.js
@@ -3,7 +3,6 @@ const queries = require("../graphql-client/queries");
const Dinero = require("dinero.js");
const moment = require("moment-timezone");
var builder = require("xmlbuilder2");
-const _ = require("lodash");
const logger = require("../utils/logger");
const fs = require("fs");
require("dotenv").config({
@@ -35,13 +34,11 @@ const ftpSetup = {
exports.default = async (req, res) => {
// Only process if in production environment.
if (process.env.NODE_ENV !== "production") {
- res.sendStatus(403);
- return;
+ return res.sendStatus(403);
}
// Only process if the appropriate token is provided.
if (req.headers["x-imex-auth"] !== process.env.AUTOHOUSE_AUTH_TOKEN) {
- res.sendStatus(401);
- return;
+ return res.sendStatus(401);
}
// Send immediate response and continue processing.
diff --git a/server/data/podium.js b/server/data/podium.js
index 9f6327ef4..5490b00db 100644
--- a/server/data/podium.js
+++ b/server/data/podium.js
@@ -29,13 +29,11 @@ const ftpSetup = {
exports.default = async (req, res) => {
// Only process if in production environment.
if (process.env.NODE_ENV !== "production") {
- res.sendStatus(403);
- return;
+ return res.sendStatus(403);
}
// Only process if the appropriate token is provided.
if (req.headers["x-imex-auth"] !== process.env.AUTOHOUSE_AUTH_TOKEN) {
- res.sendStatus(401);
- return;
+ return res.sendStatus(401);
}
// Send immediate response and continue processing.
diff --git a/server/graphql-client/queries.js b/server/graphql-client/queries.js
index f700bb17c..a05dc9538 100644
--- a/server/graphql-client/queries.js
+++ b/server/graphql-client/queries.js
@@ -878,6 +878,43 @@ exports.CHATTER_QUERY = `query CHATTER_EXPORT($start: timestamptz, $bodyshopid:
}
}`;
+exports.CARFAX_QUERY = `query CARFAX_EXPORT($start: timestamptz, $bodyshopid: uuid!, $end: timestamptz) {
+ bodyshops_by_pk(id: $bodyshopid){
+ id
+ shopname
+ imexshopid
+ timezone
+ }
+ jobs(where: {_and: [{converted: {_eq: true}}, {v_vin: {_is_null: false}}, {date_invoiced: {_gt: $start}}, {date_invoiced: {_lte: $end}}, {shopid: {_eq: $bodyshopid}}]}) {
+ id
+ created_at
+ ro_number
+ v_model_yr
+ v_model_desc
+ v_make_desc
+ v_vin
+ date_estimated
+ date_open
+ date_invoiced
+ loss_date
+ ins_co_nm
+ loss_desc
+ theft_ind
+ tlos_ind
+ job_totals
+ area_of_damage
+ joblines(where: {removed: {_eq: false}}) {
+ line_desc
+ oem_partno
+ alt_partno
+ mod_lbr_ty
+ part_qty
+ part_type
+ act_price
+ }
+ }
+}`;
+
exports.CLAIMSCORP_QUERY = `query CLAIMSCORP_EXPORT($start: timestamptz, $bodyshopid: uuid!, $end: timestamptz) {
bodyshops_by_pk(id: $bodyshopid){
id
@@ -1816,6 +1853,14 @@ exports.GET_CHATTER_SHOPS = `query GET_CHATTER_SHOPS {
}
}`;
+exports.GET_CARFAX_SHOPS = `query GET_CARFAX_SHOPS {
+ bodyshops{
+ id
+ shopname
+ imexshopid
+ }
+}`;
+
exports.GET_CLAIMSCORP_SHOPS = `query GET_CLAIMSCORP_SHOPS {
bodyshops(where: {claimscorpid: {_is_null: false}, _or: {claimscorpid: {_neq: ""}}}){
id
@@ -2846,6 +2891,26 @@ exports.GET_DOCUMENTS_BY_JOB = `
}
}
}`;
+exports.GET_DOCUMENTS_BY_BILL = `
+query GET_DOCUMENTS_BY_BILL($billId: uuid!) {
+ documents_aggregate(where: {billid: {_eq: $billId}}) {
+ aggregate {
+ sum {
+ size
+ }
+ }
+ }
+ documents(order_by: {takenat: desc}, where: {billid: {_eq: $billId}}) {
+ id
+ name
+ key
+ type
+ size
+ takenat
+ extension
+ }
+}
+`;
exports.QUERY_TEMPORARY_DOCS = ` query QUERY_TEMPORARY_DOCS {
documents(where: { jobid: { _is_null: true } }, order_by: { takenat: desc }) {
diff --git a/server/media/imgproxy-media.js b/server/media/imgproxy-media.js
index 46a8dd840..576c1cd69 100644
--- a/server/media/imgproxy-media.js
+++ b/server/media/imgproxy-media.js
@@ -18,6 +18,7 @@ const {
GET_DOCUMENTS_BY_JOB,
QUERY_TEMPORARY_DOCS,
GET_DOCUMENTS_BY_IDS,
+ GET_DOCUMENTS_BY_BILL,
DELETE_MEDIA_DOCUMENTS
} = require("../graphql-client/queries");
const yazl = require("yazl");
@@ -90,9 +91,11 @@ const getThumbnailUrls = async (req, res) => {
//Delayed as the key structure may change slightly from what it is currently and will require evaluating mobile components.
const client = req.userGraphQLClient;
//If there's no jobid and no billid, we're in temporary documents.
- const data = await (jobid
- ? client.request(GET_DOCUMENTS_BY_JOB, { jobId: jobid })
- : client.request(QUERY_TEMPORARY_DOCS));
+ const data = await (
+ billid ? client.request(GET_DOCUMENTS_BY_BILL, { billId: billid }) :
+ jobid
+ ? client.request(GET_DOCUMENTS_BY_JOB, { jobId: jobid })
+ : client.request(QUERY_TEMPORARY_DOCS));
const thumbResizeParams = `rs:fill:250:250:1/g:ce`;
const s3client = new S3Client({ region: InstanceRegion() });
diff --git a/server/render/canvas-handler.js b/server/render/canvas-handler.js
index b46760946..b5e9ab879 100644
--- a/server/render/canvas-handler.js
+++ b/server/render/canvas-handler.js
@@ -73,37 +73,23 @@ const processCanvasRequest = async (req, res) => {
// Default width and height
const width = isNumber(w) && w > 0 ? w : 500;
const height = isNumber(h) && h > 0 ? h : 275;
-
const configuration = getChartConfiguration(keys, values, override);
- let canvas = null;
- let ctx = null;
let chart = null;
- let chartImage = null;
try {
- // Create the canvas
- canvas = new Canvas(width, height);
- ctx = canvas.getContext("2d");
+ const canvas = new Canvas(width, height);
+ const ctx = canvas.getContext("2d");
- // Render the chart
chart = new Chart(ctx, configuration);
- // Generate and send the image
- chartImage = (await canvas.toBuffer("image/png")).toString("base64");
+ const chartImage = (await canvas.toBuffer("image/png")).toString("base64");
res.status(200).send(`data:image/png;base64,${chartImage}`);
} catch (error) {
- // Log the error and send the response
logger.log("canvas-error", "error", "jsr", null, { error: error.message });
- res.status(500).send("Failed to generate canvas.");
+ res.status(500).send("Error generating canvas");
} finally {
- // Cleanup resources
- if (chart) {
- chart.destroy();
- }
- ctx = null;
- canvas = null;
- chartImage = null;
+ chart?.destroy();
}
};
@@ -118,15 +104,18 @@ const enqueueRequest = (req, res) => {
};
const processNextInQueue = async () => {
- while (requestQueue.length > 0) {
- const { req, res } = requestQueue.shift();
- try {
- await processCanvasRequest(req, res);
- } catch (err) {
- console.error("canvas-queue-error", "error", "jsr", null, { error: err.message });
+ try {
+ while (requestQueue.length > 0) {
+ const { req, res } = requestQueue.shift();
+ try {
+ await processCanvasRequest(req, res);
+ } catch (err) {
+ console.error("canvas-queue-error", "error", "jsr", null, { error: err.message });
+ }
}
+ } finally {
+ isProcessing = false;
}
- isProcessing = false;
};
exports.canvastest = function (req, res) {
@@ -134,7 +123,10 @@ exports.canvastest = function (req, res) {
};
exports.canvas = async (req, res) => {
- if (isProcessing || !enqueueRequest(req, res)) return;
- isProcessing = true;
- processNextInQueue().catch((err) => console.error("canvas-processing-error", { error: err.message }));
+ if (!enqueueRequest(req, res)) return;
+
+ if (!isProcessing) {
+ isProcessing = true;
+ processNextInQueue().catch((err) => console.error("canvas-processing-error", { error: err.message }));
+ }
};
diff --git a/server/routes/dataRoutes.js b/server/routes/dataRoutes.js
index f8212c36d..8e7bc04fd 100644
--- a/server/routes/dataRoutes.js
+++ b/server/routes/dataRoutes.js
@@ -1,6 +1,6 @@
const express = require("express");
const router = express.Router();
-const { autohouse, claimscorp, chatter, kaizen, usageReport, podium } = require("../data/data");
+const { autohouse, claimscorp, chatter, kaizen, usageReport, podium, carfax } = require("../data/data");
router.post("/ah", autohouse);
router.post("/cc", claimscorp);
@@ -8,5 +8,6 @@ router.post("/chatter", chatter);
router.post("/kaizen", kaizen);
router.post("/usagereport", usageReport);
router.post("/podium", podium);
+router.post("/carfax", carfax);
module.exports = router;