Compare commits
53 Commits
feature/IO
...
feature/IO
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
141b05f558 | ||
|
|
8c9ef375be | ||
|
|
8295cb111a | ||
|
|
951d214d49 | ||
|
|
6f19c1dd3f | ||
|
|
637e95c351 | ||
|
|
0cadf007b5 | ||
|
|
60258a0f5d | ||
|
|
7873405a30 | ||
|
|
7639655911 | ||
|
|
4fb1871044 | ||
|
|
e5dd1edf13 | ||
|
|
542c95c395 | ||
|
|
2b40793c77 | ||
|
|
4d475e25fa | ||
|
|
4e5aba59d7 | ||
|
|
09f96f0b68 | ||
|
|
f0c166907b | ||
|
|
c06b4e8af5 | ||
|
|
a7e21b0505 | ||
|
|
3b481afa9e | ||
|
|
75de177b7b | ||
|
|
ec6c0279de | ||
|
|
c9572d2db5 | ||
|
|
93e9e20f6f | ||
|
|
4e8ea736c5 | ||
|
|
8f00dbfc17 | ||
|
|
55d242d40d | ||
|
|
4f99ae40d3 | ||
|
|
d94b573ae6 | ||
|
|
790ab0447f | ||
|
|
3737fe457f | ||
|
|
bb4e8eb5bd | ||
|
|
27a07e8d5d | ||
|
|
e2618eee83 | ||
|
|
66c51a4be5 | ||
|
|
d5afcaeaab | ||
|
|
c332ec11b7 | ||
|
|
cf31290f05 | ||
|
|
dbbab910b6 | ||
|
|
abf01b4966 | ||
|
|
a965f9edf5 | ||
|
|
f02ca05eba | ||
|
|
7bc2d41a68 | ||
|
|
5277e90946 | ||
|
|
15ea4e6afa | ||
|
|
5b3b6a409c | ||
|
|
736e9cedfa | ||
|
|
c433103e1b | ||
|
|
2892fdbb58 | ||
|
|
c45f38e47b | ||
|
|
54a58c9fbc | ||
|
|
1934ae0758 |
File diff suppressed because it is too large
Load Diff
@@ -1,16 +1,20 @@
|
|||||||
import { ApolloProvider } from "@apollo/client";
|
import { ApolloProvider } from "@apollo/client";
|
||||||
|
import * as Sentry from "@sentry/react";
|
||||||
import { SplitFactoryProvider, useSplitClient } from "@splitsoftware/splitio-react";
|
import { SplitFactoryProvider, useSplitClient } from "@splitsoftware/splitio-react";
|
||||||
import { ConfigProvider } from "antd";
|
import { ConfigProvider } from "antd";
|
||||||
import enLocale from "antd/es/locale/en_US";
|
import enLocale from "antd/es/locale/en_US";
|
||||||
import { useEffect } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
|
import { CookiesProvider } from "react-cookie";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useSelector } from "react-redux";
|
import { connect, useSelector } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
|
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
|
||||||
|
import { setDarkMode } from "../redux/application/application.actions";
|
||||||
|
import { selectDarkMode } from "../redux/application/application.selectors";
|
||||||
|
import { selectCurrentUser } from "../redux/user/user.selectors.js";
|
||||||
import client from "../utils/GraphQLClient";
|
import client from "../utils/GraphQLClient";
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
import * as Sentry from "@sentry/react";
|
import getTheme from "./themeProvider";
|
||||||
import themeProvider from "./themeProvider";
|
|
||||||
import { CookiesProvider } from "react-cookie";
|
|
||||||
|
|
||||||
// Base Split configuration
|
// Base Split configuration
|
||||||
const config = {
|
const config = {
|
||||||
@@ -24,19 +28,54 @@ const config = {
|
|||||||
function SplitClientProvider({ children }) {
|
function SplitClientProvider({ children }) {
|
||||||
const imexshopid = useSelector((state) => state.user.imexshopid); // Access imexshopid from Redux store
|
const imexshopid = useSelector((state) => state.user.imexshopid); // Access imexshopid from Redux store
|
||||||
const splitClient = useSplitClient({ key: imexshopid || "anon" }); // Use imexshopid or fallback to "anon"
|
const splitClient = useSplitClient({ key: imexshopid || "anon" }); // Use imexshopid or fallback to "anon"
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (splitClient && imexshopid) {
|
if (splitClient && imexshopid) {
|
||||||
// Log readiness for debugging; no need for ready() since isReady is available
|
|
||||||
console.log(`Split client initialized with key: ${imexshopid}, isReady: ${splitClient.isReady}`);
|
console.log(`Split client initialized with key: ${imexshopid}, isReady: ${splitClient.isReady}`);
|
||||||
}
|
}
|
||||||
}, [splitClient, imexshopid]);
|
}, [splitClient, imexshopid]);
|
||||||
|
|
||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
|
|
||||||
function AppContainer() {
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
setDarkMode: (isDarkMode) => dispatch(setDarkMode(isDarkMode))
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
currentUser: selectCurrentUser
|
||||||
|
});
|
||||||
|
|
||||||
|
function AppContainer({ currentUser, setDarkMode }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const isDarkMode = useSelector(selectDarkMode);
|
||||||
|
const theme = useMemo(() => getTheme(isDarkMode), [isDarkMode]);
|
||||||
|
|
||||||
|
// Update data-theme attribute when dark mode changes
|
||||||
|
useEffect(() => {
|
||||||
|
document.documentElement.setAttribute("data-theme", isDarkMode ? "dark" : "light");
|
||||||
|
return () => document.documentElement.removeAttribute("data-theme");
|
||||||
|
}, [isDarkMode]);
|
||||||
|
|
||||||
|
// Sync Redux darkMode with localStorage on user change
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentUser?.uid) {
|
||||||
|
const savedMode = localStorage.getItem(`dark-mode-${currentUser.uid}`);
|
||||||
|
if (savedMode !== null) {
|
||||||
|
setDarkMode(JSON.parse(savedMode));
|
||||||
|
} else {
|
||||||
|
setDarkMode(false); // default to light mode
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setDarkMode(false);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line
|
||||||
|
}, [currentUser?.uid]);
|
||||||
|
|
||||||
|
// Persist darkMode to localStorage when it or user changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentUser?.uid) {
|
||||||
|
localStorage.setItem(`dark-mode-${currentUser.uid}`, JSON.stringify(isDarkMode));
|
||||||
|
}
|
||||||
|
}, [isDarkMode, currentUser?.uid]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CookiesProvider>
|
<CookiesProvider>
|
||||||
@@ -44,10 +83,9 @@ function AppContainer() {
|
|||||||
<ConfigProvider
|
<ConfigProvider
|
||||||
input={{ autoComplete: "new-password" }}
|
input={{ autoComplete: "new-password" }}
|
||||||
locale={enLocale}
|
locale={enLocale}
|
||||||
theme={themeProvider}
|
theme={theme}
|
||||||
form={{
|
form={{
|
||||||
validateMessages: {
|
validateMessages: {
|
||||||
// eslint-disable-next-line no-template-curly-in-string
|
|
||||||
required: t("general.validation.required", { label: "${label}" })
|
required: t("general.validation.required", { label: "${label}" })
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -64,4 +102,4 @@ function AppContainer() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Sentry.withProfiler(AppContainer);
|
export default Sentry.withProfiler(connect(mapStateToProps, mapDispatchToProps)(AppContainer));
|
||||||
|
|||||||
@@ -1,15 +1,226 @@
|
|||||||
//Global Styles.
|
:root {
|
||||||
|
--table-stripe-bg: #f4f4f4; /* Light mode table stripe */
|
||||||
|
--menu-divider-color: #74695c; /* Light mode menu divider */
|
||||||
|
--menu-submenu-text: rgba(255, 255, 255, 0.65); /* Light mode submenu text */
|
||||||
|
--kanban-column-bg: #ddd; /* Light mode kanban column */
|
||||||
|
--alert-color: blue; /* Light mode alert */
|
||||||
|
--completion-soon-color: rgba(255, 140, 0, 0.8); /* Light mode completion soon */
|
||||||
|
--completion-past-color: rgba(255, 0, 0, 0.8); /* Light mode completion past */
|
||||||
|
--job-line-manual-color: tomato; /* Light mode job line manual */
|
||||||
|
--muted-button-color: lightgray; /* Light mode muted button */
|
||||||
|
--muted-button-hover-color: darkgrey; /* Light mode muted button hover */
|
||||||
|
--table-border-color: #ddd; /* Light mode table border */
|
||||||
|
--table-hover-bg: #f5f5f5; /* Light mode table hover */
|
||||||
|
--popover-bg: #fff; /* Light mode popover background */
|
||||||
|
--error-text: red; /* Light mode error message */
|
||||||
|
--no-jobs-text: #888; /* Light mode no jobs message */
|
||||||
|
--message-yours-bg: #eee; /* Light mode yours message background */
|
||||||
|
--message-mine-bg-start: #00d0ea; /* Light mode mine message gradient start */
|
||||||
|
--message-mine-bg-end: #0085d1; /* Light mode mine message gradient end */
|
||||||
|
--message-mine-text: white; /* Light mode mine message text */
|
||||||
|
--message-mine-tail-bg: white; /* Light mode mine/yours message tail */
|
||||||
|
--system-message-bg: #f5f5f5; /* Light mode system message background */
|
||||||
|
--system-message-text: #555; /* Light mode system message text */
|
||||||
|
--system-label-text: #888; /* Light mode system label/date text */
|
||||||
|
--message-icon-color: whitesmoke; /* Light mode message icon */
|
||||||
|
--eula-card-bg: lightgray; /* Light mode eula card background */
|
||||||
|
--notification-bg: #fff; /* Light mode notification background */
|
||||||
|
--notification-text: rgba(0, 0, 0, 0.85); /* Light mode notification text */
|
||||||
|
--notification-border: #d9d9d9; /* Light mode notification border */
|
||||||
|
--notification-header-bg: #fafafa; /* Light mode notification header background */
|
||||||
|
--notification-header-border: #f0f0f0; /* Light mode notification header border */
|
||||||
|
--notification-header-text: rgba(0, 0, 0, 0.85); /* Light mode notification header text */
|
||||||
|
--notification-toggle-icon: #1677ff; /* Light mode notification toggle icon */
|
||||||
|
--notification-switch-bg: #1677ff; /* Light mode notification switch background */
|
||||||
|
--notification-btn-link: #1677ff; /* Light mode notification link button */
|
||||||
|
--notification-btn-link-hover: #69b1ff; /* Light mode notification link button hover */
|
||||||
|
--notification-btn-link-disabled: rgba(0, 0, 0, 0.25); /* Light mode notification link button disabled */
|
||||||
|
--notification-btn-link-active: #0958d9; /* Light mode notification link button active */
|
||||||
|
--notification-read-bg: #fff; /* Light mode notification read background */
|
||||||
|
--notification-read-text: rgba(0, 0, 0, 0.65); /* Light mode notification read text */
|
||||||
|
--notification-unread-bg: #f5f5f5; /* Light mode notification unread background */
|
||||||
|
--notification-unread-text: rgba(0, 0, 0, 0.85); /* Light mode notification unread text */
|
||||||
|
--notification-item-hover-bg: #fafafa; /* Light mode notification item hover background */
|
||||||
|
--notification-ro-number: #1677ff; /* Light mode notification RO number */
|
||||||
|
--notification-relative-time: rgba(0, 0, 0, 0.45); /* Light mode notification relative time */
|
||||||
|
--alert-bg: #fff1f0; /* Light mode alert background */
|
||||||
|
--alert-text: rgba(0, 0, 0, 0.85); /* Light mode alert text */
|
||||||
|
--alert-border: #ffa39e; /* Light mode alert border */
|
||||||
|
--alert-message: #ff4d4f; /* Light mode alert message */
|
||||||
|
--share-badge-bg: #cccccc; /* Light mode share badge background */
|
||||||
|
--column-header-bg: #d0d0d0; /* Light mode column header background */
|
||||||
|
--footer-bg: #d0d0d0; /* Light mode footer background */
|
||||||
|
--tech-icon-color: orangered; /* Light mode tech icon color */
|
||||||
|
--clone-border-color: #1890ff; /* Light mode clone border color */
|
||||||
|
--event-arrived-bg: rgba(4, 141, 4, 0.4); /* Light mode arrived event background */
|
||||||
|
--event-block-bg: tomato; /* Light mode block event background */
|
||||||
|
--event-selected-bg: slategrey; /* Light mode selected event background */
|
||||||
|
--task-bg: #fff; /* Light mode task center background */
|
||||||
|
--task-text: rgba(0, 0, 0, 0.85); /* Light mode task text */
|
||||||
|
--task-border: #d9d9d9; /* Light mode task border */
|
||||||
|
--task-header-bg: #fafafa; /* Light mode task header background */
|
||||||
|
--task-header-border: #f0f0f0; /* Light mode task header border */
|
||||||
|
--task-section-bg: #f5f5f5; /* Light mode task section background */
|
||||||
|
--task-section-border: #e8e8e8; /* Light mode task section border */
|
||||||
|
--task-row-hover-bg: #f5f5f5; /* Light mode task row hover background */
|
||||||
|
--task-row-border: #f0f0f0; /* Light mode task row border */
|
||||||
|
--task-ro-number: #1677ff; /* Light mode task RO number */
|
||||||
|
--task-due-text: rgba(0, 0, 0, 0.45); /* Light mode task due text */
|
||||||
|
--task-button-bg: #1677ff; /* Light mode task button background */
|
||||||
|
--task-button-hover-bg: #4096ff; /* Light mode task button hover background */
|
||||||
|
--task-button-disabled-bg: #d9d9d9; /* Light mode task button disabled background */
|
||||||
|
--task-button-text: white; /* Light mode task button text */
|
||||||
|
--task-message-text: rgba(0, 0, 0, 0.45); /* Light mode task message text */
|
||||||
|
--mask-bg: rgba(0, 0, 0, 0.05); /* Light mode mask background */
|
||||||
|
--board-text-color: #393939; /* Light mode board text color */
|
||||||
|
--section-bg: #e3e3e3; /* Light mode section background */
|
||||||
|
--detail-text-color: #4d4d4d; /* Light mode detail text color */
|
||||||
|
--card-selected-bg: rgba(128, 128, 128, 0.2); /* Light mode selected card background */
|
||||||
|
--card-stripe-even-bg: #f0f2f5; /* Light mode even card background */
|
||||||
|
--card-stripe-odd-bg: #ffffff; /* Light mode odd card background */
|
||||||
|
--bar-border-color: #f0f2f5; /* Light mode bar border and background */
|
||||||
|
--tag-wrapper-bg: #f0f2f5; /* Light mode tag wrapper background */
|
||||||
|
--tag-wrapper-text: #000; /* Light mode tag wrapper text */
|
||||||
|
--preview-bg: lightgray; /* Light mode preview background */
|
||||||
|
--preview-border-color: #2196F3; /* Light mode preview border color */
|
||||||
|
--event-bg-fallback: #c4c4c4; /* Light mode event background fallback */
|
||||||
|
--card-bg-fallback: #ffffff; /* Light mode card background fallback */
|
||||||
|
--card-text-fallback: black; /* Light mode card text fallback */
|
||||||
|
--table-row-even-bg: rgb(236, 236, 236); /* Light mode table row even background */
|
||||||
|
--status-row-bg-fallback: #ffffff; /* Light mode status row fallback background */
|
||||||
|
--reset-link-color: #0000ff; /* Light mode reset link color */
|
||||||
|
--error-header-text: tomato; /* Light mode error header text */
|
||||||
|
--tooltip-bg: white; /* Light mode tooltip background */
|
||||||
|
--tooltip-border: gray; /* Light mode tooltip border */
|
||||||
|
--tooltip-text-fallback: black; /* Light mode tooltip text fallback */
|
||||||
|
--teams-button-bg: #6264A7; /* Light mode Teams button background */
|
||||||
|
--teams-button-border: #6264A7; /* Light mode Teams button border */
|
||||||
|
--teams-button-text: #FFFFFF; /* Light mode Teams button text and icon */
|
||||||
|
--content-bg: #fff; /* Light mode content background */
|
||||||
|
--legend-bg-fallback: #ffffff; /* Light mode legend background fallback */
|
||||||
|
--tech-content-bg: #fff; /* Light mode tech content background */
|
||||||
|
--today-bg: #ffffff; /* Light mode today background */
|
||||||
|
--today-text: #000000; /* Light mode today text */
|
||||||
|
--off-range-bg: #f8f8f8; /* Light mode off-range background */
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] {
|
||||||
|
--table-stripe-bg: #2a2a2a; /* Dark mode table stripe */
|
||||||
|
--menu-divider-color: #5c5c5c; /* Dark mode menu divider */
|
||||||
|
--menu-submenu-text: rgba(255, 255, 255, 0.85); /* Dark mode submenu text */
|
||||||
|
--kanban-column-bg: #333333; /* Dark mode kanban column */
|
||||||
|
--alert-color: #4da8ff; /* Dark mode alert */
|
||||||
|
--completion-soon-color: #ff8c1a; /* Dark mode completion soon */
|
||||||
|
--completion-past-color: #ff4d4f; /* Dark mode completion past */
|
||||||
|
--job-line-manual-color: #ff6347; /* Dark mode job line manual */
|
||||||
|
--muted-button-color: #666666; /* Dark mode muted button */
|
||||||
|
--muted-button-hover-color: #999999; /* Dark mode muted button hover */
|
||||||
|
--table-border-color: #5c5c5c; /* Dark mode table border */
|
||||||
|
--table-hover-bg: #2a2a2a; /* Dark mode table hover */
|
||||||
|
--popover-bg: #2a2a2a; /* Dark mode popover background */
|
||||||
|
--error-text: #ff4d4f; /* Dark mode error message */
|
||||||
|
--no-jobs-text: #999999; /* Dark mode no jobs message */
|
||||||
|
--message-yours-bg: #2a2a2a; /* Dark mode yours message background */
|
||||||
|
--message-mine-bg-start: #4da8ff; /* Dark mode mine message gradient start */
|
||||||
|
--message-mine-bg-end: #326ade; /* Dark mode mine message gradient end */
|
||||||
|
--message-mine-text: #ffffff; /* Dark mode mine message text */
|
||||||
|
--message-mine-tail-bg: #1f1f1f; /* Dark mode mine/yours message tail */
|
||||||
|
--system-message-bg: #333333; /* Dark mode system message background */
|
||||||
|
--system-message-text: #cccccc; /* Dark mode system message text */
|
||||||
|
--system-label-text: #999999; /* Dark mode system label/date text */
|
||||||
|
--message-icon-color: #cccccc; /* Dark mode message icon */
|
||||||
|
--eula-card-bg: #2a2a2a; /* Dark mode eula card background */
|
||||||
|
--notification-bg: #2a2a2a; /* Dark mode notification background */
|
||||||
|
--notification-text: rgba(255, 255, 255, 0.85); /* Dark mode notification text */
|
||||||
|
--notification-border: #5c5c5c; /* Dark mode notification border */
|
||||||
|
--notification-header-bg: #333333; /* Dark mode notification header background */
|
||||||
|
--notification-header-border: #444444; /* Dark mode notification header border */
|
||||||
|
--notification-header-text: rgba(255, 255, 255, 0.85); /* Dark mode notification header text */
|
||||||
|
--notification-toggle-icon: #4da8ff; /* Dark mode notification toggle icon */
|
||||||
|
--notification-switch-bg: #4da8ff; /* Dark mode notification switch background */
|
||||||
|
--notification-btn-link: #4da8ff; /* Dark mode notification link button */
|
||||||
|
--notification-btn-link-hover: #80c1ff; /* Dark mode notification link button hover */
|
||||||
|
--notification-btn-link-disabled: rgba(255, 255, 255, 0.25); /* Dark mode notification link button disabled */
|
||||||
|
--notification-btn-link-active: #2681ff; /* Dark mode notification link button active */
|
||||||
|
--notification-read-bg: #2a2a2a; /* Dark mode notification read background */
|
||||||
|
--notification-read-text: rgba(255, 255, 255, 0.65); /* Dark mode notification read text */
|
||||||
|
--notification-unread-bg: #333333; /* Dark mode notification unread background */
|
||||||
|
--notification-unread-text: rgba(255, 255, 255, 0.85); /* Dark mode notification unread text */
|
||||||
|
--notification-item-hover-bg: #3a3a3a; /* Dark mode notification item hover background */
|
||||||
|
--notification-ro-number: #4da8ff; /* Dark mode notification RO number */
|
||||||
|
--notification-relative-time: rgba(255, 255, 255, 0.45); /* Dark mode notification relative time */
|
||||||
|
--alert-bg: #3a1a1a; /* Dark mode alert background */
|
||||||
|
--alert-text: rgba(255, 255, 255, 0.85); /* Dark mode alert text */
|
||||||
|
--alert-border: #ff6666; /* Dark mode alert border */
|
||||||
|
--alert-message: #ff6666; /* Dark mode alert message */
|
||||||
|
--share-badge-bg: #666666; /* Dark mode share badge background */
|
||||||
|
--column-header-bg: #333333; /* Dark mode column header background */
|
||||||
|
--footer-bg: #333333; /* Dark mode footer background */
|
||||||
|
--tech-icon-color: #ff4500; /* Dark mode tech icon color */
|
||||||
|
--clone-border-color: #4da8ff; /* Dark mode clone border color */
|
||||||
|
--event-arrived-bg: rgba(4, 141, 4, 0.6); /* Dark mode arrived event background */
|
||||||
|
--event-block-bg: tomato; /* Dark mode block event background */
|
||||||
|
--event-selected-bg: #4a5e6e; /* Dark mode selected event background */
|
||||||
|
--task-bg: #2a2a2a; /* Dark mode task center background */
|
||||||
|
--task-text: rgba(255, 255, 255, 0.85); /* Dark mode task text */
|
||||||
|
--task-border: #5c5c5c; /* Dark mode task border */
|
||||||
|
--task-header-bg: #333333; /* Dark mode task header background */
|
||||||
|
--task-header-border: #444444; /* Dark mode task header border */
|
||||||
|
--task-section-bg: #333333; /* Dark mode task section background */
|
||||||
|
--task-section-border: #444444; /* Dark mode task section border */
|
||||||
|
--task-row-hover-bg: #3a3a3a; /* Dark mode task row hover background */
|
||||||
|
--task-row-border: #444444; /* Dark mode task row border */
|
||||||
|
--task-ro-number: #4da8ff; /* Dark mode task RO number */
|
||||||
|
--task-due-text: rgba(255, 255, 255, 0.45); /* Dark mode task due text */
|
||||||
|
--task-button-bg: #4da8ff; /* Dark mode task button background */
|
||||||
|
--task-button-hover-bg: #80c1ff; /* Dark mode task button hover background */
|
||||||
|
--task-button-disabled-bg: #666666; /* Dark mode task button disabled background */
|
||||||
|
--task-button-text: #ffffff; /* Dark mode task button text */
|
||||||
|
--task-message-text: rgba(255, 255, 255, 0.45); /* Dark mode task message text */
|
||||||
|
--mask-bg: rgba(255, 255, 255, 0.05); /* Dark mode mask background */
|
||||||
|
--board-text-color: #cccccc; /* Dark mode board text color */
|
||||||
|
--section-bg: #333333; /* Dark mode section background */
|
||||||
|
--detail-text-color: #bbbbbb; /* Dark mode detail text color */
|
||||||
|
--card-selected-bg: rgba(255, 255, 255, 0.1); /* Dark mode selected card background */
|
||||||
|
--card-stripe-even-bg: #2a2a2a; /* Dark mode even card background */
|
||||||
|
--card-stripe-odd-bg: #1f1f1f; /* Dark mode odd card background */
|
||||||
|
--bar-border-color: #2a2a2a; /* Dark mode bar border and background */
|
||||||
|
--tag-wrapper-bg: #2a2a2a; /* Dark mode tag wrapper background */
|
||||||
|
--tag-wrapper-text: #cccccc; /* Dark mode tag wrapper text */
|
||||||
|
--preview-bg: #2a2a2a; /* Dark mode preview background */
|
||||||
|
--preview-border-color: #4da8ff; /* Dark mode preview border color */
|
||||||
|
--event-bg-fallback: #262626; /* Dark mode event background fallback */
|
||||||
|
--card-bg-fallback: #2a2a2a; /* Dark mode card background fallback */
|
||||||
|
--card-text-fallback: #cccccc; /* Dark mode card text fallback */
|
||||||
|
--table-row-even-bg: #2a2a2a; /* Dark mode table row even background */
|
||||||
|
--status-row-bg-fallback: #1f1f1f; /* Dark mode status row fallback background */
|
||||||
|
--reset-link-color: #4da8ff; /* Dark mode reset link color */
|
||||||
|
--error-header-text: #ff6347; /* Dark mode error header text */
|
||||||
|
--tooltip-bg: #2a2a2a; /* Dark mode tooltip background */
|
||||||
|
--tooltip-border: #5c5c5c; /* Dark mode tooltip border */
|
||||||
|
--tooltip-text-fallback: #cccccc; /* Dark mode tooltip text fallback */
|
||||||
|
--teams-button-bg: #7b7dc4; /* Dark mode Teams button background */
|
||||||
|
--teams-button-border: #7b7dc4; /* Dark mode Teams button border */
|
||||||
|
--teams-button-text: #ffffff; /* Dark mode Teams button text and icon */
|
||||||
|
--content-bg: #2a2a2a; /* Dark mode content background */
|
||||||
|
--legend-bg-fallback: #2a2a2a; /* Dark mode legend background fallback */
|
||||||
|
--tech-content-bg: #2a2a2a; /* Dark mode tech content background */
|
||||||
|
--today-bg: #4a5e6e; /* Dark mode today background */
|
||||||
|
--today-text: #ffffff; /* Dark mode today text */
|
||||||
|
--off-range-bg: #333333; /* Dark mode off-range background */
|
||||||
|
--svg-background: #FFF; /* Dark mode SVG background */
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global Styles
|
||||||
@import "react-big-calendar/lib/sass/styles";
|
@import "react-big-calendar/lib/sass/styles";
|
||||||
|
|
||||||
.ant-menu-item-divider {
|
.ant-menu-item-divider {
|
||||||
border-bottom: 1px solid #74695c !important;
|
border-bottom: 1px solid var(--menu-divider-color) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: This was added because the newest release of ant was making the text color and the background color the same on a selected header
|
// Note: Monitor this in dark mode to ensure text visibility
|
||||||
// Tried all available tokens (https://ant.design/components/menu?locale=en-US) and even reverted all our custom styles, to no avail
|
|
||||||
// This should be kept an eye on, especially if implementing DARK MODE
|
|
||||||
.ant-menu-submenu-title {
|
.ant-menu-submenu-title {
|
||||||
color: rgba(255, 255, 255, 0.65) !important;
|
color: var(--menu-submenu-text) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.imex-table-header {
|
.imex-table-header {
|
||||||
@@ -46,7 +257,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ellipses {
|
.ellipses {
|
||||||
display: inline-block; /* for em, a, span, etc (inline by default) */
|
display: inline-block;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
width: calc(95%);
|
width: calc(95%);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -60,23 +271,24 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ::-webkit-scrollbar-track {
|
// Scrollbar styles (uncomment if needed, updated for dark mode)
|
||||||
// -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
::-webkit-scrollbar-track {
|
||||||
// border-radius: 0.2rem;
|
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
||||||
// background-color: #f5f5f5;
|
border-radius: 0.2rem;
|
||||||
// }
|
background-color: var(--table-stripe-bg);
|
||||||
|
}
|
||||||
|
|
||||||
// ::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
// width: 0.25rem;
|
width: 0.25rem;
|
||||||
// max-height: 0.25rem;
|
max-height: 0.25rem;
|
||||||
// background-color: #f5f5f5;
|
background-color: var(--table-stripe-bg);
|
||||||
// }
|
}
|
||||||
|
|
||||||
// ::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
// border-radius: 0.2rem;
|
border-radius: 0.2rem;
|
||||||
// -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
||||||
// background-color: #188fff;
|
background-color: var(--alert-color);
|
||||||
// }
|
}
|
||||||
|
|
||||||
.ant-input-number-input,
|
.ant-input-number-input,
|
||||||
.ant-input-number,
|
.ant-input-number,
|
||||||
@@ -88,28 +300,27 @@
|
|||||||
|
|
||||||
.production-alert {
|
.production-alert {
|
||||||
animation: alertBlinker 1s linear infinite;
|
animation: alertBlinker 1s linear infinite;
|
||||||
color: blue;
|
color: var(--alert-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes alertBlinker {
|
@keyframes alertBlinker {
|
||||||
50% {
|
50% {
|
||||||
color: red;
|
color: var(--completion-past-color);
|
||||||
opacity: 100;
|
opacity: 100;
|
||||||
//opacity: 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.blue {
|
.blue {
|
||||||
color: blue;
|
color: var(--alert-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.production-completion-soon {
|
.production-completion-soon {
|
||||||
color: rgba(255, 140, 0, 0.8);
|
color: var(--completion-soon-color);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.production-completion-past {
|
.production-completion-past {
|
||||||
color: rgba(255, 0, 0, 0.8);
|
color: var(--completion-past-color);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,7 +350,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.react-kanban-column {
|
.react-kanban-column {
|
||||||
background-color: #ddd !important;
|
background-color: var(--kanban-column-bg) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.production-list-table {
|
.production-list-table {
|
||||||
@@ -151,18 +362,18 @@
|
|||||||
.ReactGridGallery_tile-icon-bar {
|
.ReactGridGallery_tile-icon-bar {
|
||||||
div {
|
div {
|
||||||
svg {
|
svg {
|
||||||
fill: #1890ff;
|
fill: var(--alert-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.job-line-manual {
|
.job-line-manual {
|
||||||
color: tomato;
|
color: var(--job-line-manual-color);
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-table-tbody > tr.ant-table-row:nth-child(2n) > td {
|
.ant-table-tbody > tr.ant-table-row:nth-child(2n) > td {
|
||||||
background-color: #f4f4f4;
|
background-color: var(--table-stripe-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rowWithColor > td {
|
.rowWithColor > td {
|
||||||
@@ -170,15 +381,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.muted-button {
|
.muted-button {
|
||||||
color: lightgray;
|
color: var(--muted-button-color);
|
||||||
border: none;
|
border: none;
|
||||||
background: none;
|
background: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 16px; /* Adjust as needed */
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.muted-button:hover {
|
.muted-button:hover {
|
||||||
color: darkgrey;
|
color: var(--muted-button-hover-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification-alert-unordered-list {
|
.notification-alert-unordered-list {
|
||||||
@@ -190,3 +401,27 @@
|
|||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Override react-big-calendar styles for dark mode only
|
||||||
|
[data-theme="dark"] {
|
||||||
|
.car-svg {
|
||||||
|
background-color: var(--svg-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-today {
|
||||||
|
background-color: var(--today-bg);
|
||||||
|
color: var(--today-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-off-range {
|
||||||
|
background-color: var(--off-range-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-day-bg.rbc-today {
|
||||||
|
background-color: var(--today-bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//.rbc-time-header-gutter {
|
||||||
|
// padding: 0;
|
||||||
|
//}
|
||||||
|
|||||||
@@ -4,36 +4,42 @@ import InstanceRenderMgr from "../utils/instanceRenderMgr";
|
|||||||
|
|
||||||
const { defaultAlgorithm, darkAlgorithm } = theme;
|
const { defaultAlgorithm, darkAlgorithm } = theme;
|
||||||
|
|
||||||
let isDarkMode = false;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default theme
|
* Default theme
|
||||||
* @type {{components: {Menu: {itemDividerBorderColor: string}}}}
|
* @type {{components: {Menu: {itemDividerBorderColor: string}}}}
|
||||||
*/
|
*/
|
||||||
const defaultTheme = {
|
const defaultTheme = (isDarkMode) => ({
|
||||||
components: {
|
components: {
|
||||||
Table: {
|
Table: {
|
||||||
rowHoverBg: "#e7f3ff",
|
rowHoverBg: isDarkMode ? "#2a2a2a" : "#e7f3ff",
|
||||||
rowSelectedBg: "#e6f7ff",
|
rowSelectedBg: isDarkMode ? "#333333" : "#e6f7ff",
|
||||||
headerSortHoverBg: "transparent"
|
headerSortHoverBg: "transparent"
|
||||||
},
|
},
|
||||||
Menu: {
|
Menu: {
|
||||||
darkItemHoverBg: "#1890ff",
|
darkItemHoverBg: isDarkMode ? "#004a77" : "#1890ff",
|
||||||
itemHoverBg: "#1890ff",
|
itemHoverBg: isDarkMode ? "#004a77" : "#1890ff",
|
||||||
horizontalItemHoverBg: "#1890ff"
|
horizontalItemHoverBg: isDarkMode ? "#004a77" : "#1890ff"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
token: {
|
token: {
|
||||||
colorPrimary: InstanceRenderMgr({
|
colorPrimary: InstanceRenderMgr(
|
||||||
imex: "#1890ff",
|
{
|
||||||
rome: "#326ade"
|
imex: isDarkMode ? "#4da8ff" : "#1890ff",
|
||||||
}),
|
rome: isDarkMode ? "#5b8ce6" : "#326ade"
|
||||||
colorInfo: InstanceRenderMgr({
|
},
|
||||||
imex: "#1890ff",
|
isDarkMode
|
||||||
rome: "#326ade"
|
),
|
||||||
})
|
colorInfo: InstanceRenderMgr(
|
||||||
|
{
|
||||||
|
imex: isDarkMode ? "#4da8ff" : "#1890ff",
|
||||||
|
rome: isDarkMode ? "#5b8ce6" : "#326ade"
|
||||||
|
},
|
||||||
|
isDarkMode
|
||||||
|
),
|
||||||
|
colorError: isDarkMode ? "#ff4d4f" : "#f5222d",
|
||||||
|
colorBgBase: isDarkMode ? "#1f1f1f" : "#ffffff" // Align with Ant Design dark mode
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Development theme
|
* Development theme
|
||||||
@@ -60,8 +66,9 @@ const prodTheme = {};
|
|||||||
|
|
||||||
const currentTheme = import.meta.env.DEV ? devTheme : prodTheme;
|
const currentTheme = import.meta.env.DEV ? devTheme : prodTheme;
|
||||||
|
|
||||||
const finaltheme = {
|
const getTheme = (isDarkMode) => ({
|
||||||
algorithm: isDarkMode ? darkAlgorithm : defaultAlgorithm,
|
algorithm: isDarkMode ? darkAlgorithm : defaultAlgorithm,
|
||||||
...defaultsDeep(currentTheme, defaultTheme)
|
...defaultsDeep(currentTheme, defaultTheme)
|
||||||
};
|
});
|
||||||
export default finaltheme;
|
|
||||||
|
export default getTheme;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Card, Checkbox, Input, Space, Table } from "antd";
|
import { Card, Checkbox, Input, Space, Table } from "antd";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import React, { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
@@ -16,12 +16,13 @@ import PayableExportAll from "../payable-export-all-button/payable-export-all-bu
|
|||||||
import PayableExportButton from "../payable-export-button/payable-export-button.component";
|
import PayableExportButton from "../payable-export-button/payable-export-button.component";
|
||||||
import BillMarkSelectedExported from "../payable-mark-selected-exported/payable-mark-selected-exported.component";
|
import BillMarkSelectedExported from "../payable-mark-selected-exported/payable-mark-selected-exported.component";
|
||||||
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
||||||
|
import useLocalStorage from "./../../utils/useLocalStorage";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = () => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -31,7 +32,7 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, bills, ref
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [selectedBills, setSelectedBills] = useState([]);
|
const [selectedBills, setSelectedBills] = useState([]);
|
||||||
const [transInProgress, setTransInProgress] = useState(false);
|
const [transInProgress, setTransInProgress] = useState(false);
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useLocalStorage("accounting-payables-table-state", {
|
||||||
sortedInfo: {},
|
sortedInfo: {},
|
||||||
search: ""
|
search: ""
|
||||||
});
|
});
|
||||||
@@ -181,7 +182,7 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, bills, ref
|
|||||||
onChange={handleTableChange}
|
onChange={handleTableChange}
|
||||||
rowSelection={{
|
rowSelection={{
|
||||||
onSelectAll: (selected, selectedRows) => setSelectedBills(selectedRows.map((i) => i.id)),
|
onSelectAll: (selected, selectedRows) => setSelectedBills(selectedRows.map((i) => i.id)),
|
||||||
onSelect: (record, selected, selectedRows, nativeEvent) => {
|
onSelect: (record, selected, selectedRows) => {
|
||||||
setSelectedBills(selectedRows.map((i) => i.id));
|
setSelectedBills(selectedRows.map((i) => i.id));
|
||||||
},
|
},
|
||||||
getCheckboxProps: (record) => ({
|
getCheckboxProps: (record) => ({
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Card, Input, Space, Table } from "antd";
|
import { Card, Input, Space, Table } from "antd";
|
||||||
import React, { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
@@ -10,6 +10,7 @@ import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
|||||||
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter";
|
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter";
|
||||||
import { exportPageLimit } from "../../utils/config";
|
import { exportPageLimit } from "../../utils/config";
|
||||||
import { alphaSort, dateSort } from "../../utils/sorters";
|
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||||
|
import useLocalStorage from "../../utils/useLocalStorage";
|
||||||
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
||||||
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||||
import PaymentExportButton from "../payment-export-button/payment-export-button.component";
|
import PaymentExportButton from "../payment-export-button/payment-export-button.component";
|
||||||
@@ -21,7 +22,7 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = () => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -31,7 +32,7 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, payments,
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [selectedPayments, setSelectedPayments] = useState([]);
|
const [selectedPayments, setSelectedPayments] = useState([]);
|
||||||
const [transInProgress, setTransInProgress] = useState(false);
|
const [transInProgress, setTransInProgress] = useState(false);
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useLocalStorage("accounting-payments-table-state", {
|
||||||
sortedInfo: {},
|
sortedInfo: {},
|
||||||
search: ""
|
search: ""
|
||||||
});
|
});
|
||||||
@@ -194,7 +195,7 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, payments,
|
|||||||
onChange={handleTableChange}
|
onChange={handleTableChange}
|
||||||
rowSelection={{
|
rowSelection={{
|
||||||
onSelectAll: (selected, selectedRows) => setSelectedPayments(selectedRows.map((i) => i.id)),
|
onSelectAll: (selected, selectedRows) => setSelectedPayments(selectedRows.map((i) => i.id)),
|
||||||
onSelect: (record, selected, selectedRows, nativeEvent) => {
|
onSelect: (record, selected, selectedRows) => {
|
||||||
setSelectedPayments(selectedRows.map((i) => i.id));
|
setSelectedPayments(selectedRows.map((i) => i.id));
|
||||||
},
|
},
|
||||||
getCheckboxProps: (record) => ({
|
getCheckboxProps: (record) => ({
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Button, Card, Input, Space, Table } from "antd";
|
import { Button, Card, Input, Space, Table } from "antd";
|
||||||
import React, { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
@@ -10,6 +10,7 @@ import { exportPageLimit } from "../../utils/config";
|
|||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
import { DateFormatter } from "../../utils/DateFormatter";
|
import { DateFormatter } from "../../utils/DateFormatter";
|
||||||
import { alphaSort, dateSort, statusSort } from "../../utils/sorters";
|
import { alphaSort, dateSort, statusSort } from "../../utils/sorters";
|
||||||
|
import useLocalStorage from "../../utils/useLocalStorage";
|
||||||
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
||||||
import JobExportButton from "../jobs-close-export-button/jobs-close-export-button.component";
|
import JobExportButton from "../jobs-close-export-button/jobs-close-export-button.component";
|
||||||
import JobsExportAllButton from "../jobs-export-all-button/jobs-export-all-button.component";
|
import JobsExportAllButton from "../jobs-export-all-button/jobs-export-all-button.component";
|
||||||
@@ -20,7 +21,7 @@ import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
|||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = () => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
});
|
});
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(AccountingReceivablesTableComponent);
|
export default connect(mapStateToProps, mapDispatchToProps)(AccountingReceivablesTableComponent);
|
||||||
@@ -30,7 +31,7 @@ export function AccountingReceivablesTableComponent({ bodyshop, loading, jobs, r
|
|||||||
const [selectedJobs, setSelectedJobs] = useState([]);
|
const [selectedJobs, setSelectedJobs] = useState([]);
|
||||||
const [transInProgress, setTransInProgress] = useState(false);
|
const [transInProgress, setTransInProgress] = useState(false);
|
||||||
|
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useLocalStorage("accounting-receivables-table-state", {
|
||||||
sortedInfo: {},
|
sortedInfo: {},
|
||||||
search: ""
|
search: ""
|
||||||
});
|
});
|
||||||
@@ -207,7 +208,7 @@ export function AccountingReceivablesTableComponent({ bodyshop, loading, jobs, r
|
|||||||
onChange={handleTableChange}
|
onChange={handleTableChange}
|
||||||
rowSelection={{
|
rowSelection={{
|
||||||
onSelectAll: (selected, selectedRows) => setSelectedJobs(selectedRows.map((i) => i.id)),
|
onSelectAll: (selected, selectedRows) => setSelectedJobs(selectedRows.map((i) => i.id)),
|
||||||
onSelect: (record, selected, selectedRows, nativeEvent) => {
|
onSelect: (record, selected, selectedRows) => {
|
||||||
setSelectedJobs(selectedRows.map((i) => i.id));
|
setSelectedJobs(selectedRows.map((i) => i.id));
|
||||||
},
|
},
|
||||||
getCheckboxProps: (record) => ({
|
getCheckboxProps: (record) => ({
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
td {
|
td {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
border-bottom: 1px solid #ddd;
|
border-bottom: 1px solid var(--table-border-color);
|
||||||
|
|
||||||
.ant-form-item {
|
.ant-form-item {
|
||||||
margin-bottom: 0px !important;
|
margin-bottom: 0px !important;
|
||||||
@@ -14,6 +14,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
tr:hover {
|
tr:hover {
|
||||||
background-color: #f5f5f5;
|
background-color: var(--table-hover-bg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
td {
|
td {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
border-bottom: 1px solid #ddd;
|
border-bottom: 1px solid var(--table-border-color);
|
||||||
|
|
||||||
.ant-form-item {
|
.ant-form-item {
|
||||||
margin-bottom: 0px !important;
|
margin-bottom: 0px !important;
|
||||||
@@ -14,6 +14,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
tr:hover {
|
tr:hover {
|
||||||
background-color: #f5f5f5;
|
background-color: var(--table-hover-bg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,9 +29,7 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
function ChatConversationListComponent({ conversationList, selectedConversation, setSelectedConversation, bodyshop }) {
|
function ChatConversationListComponent({ conversationList, selectedConversation, setSelectedConversation, bodyshop }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [, forceUpdate] = useState(false);
|
const [, forceUpdate] = useState(false);
|
||||||
|
|
||||||
const phoneNumbers = conversationList.map((item) => phone(item.phone_num, "CA").phoneNumber.replace(/^\+1/, ""));
|
const phoneNumbers = conversationList.map((item) => phone(item.phone_num, "CA").phoneNumber.replace(/^\+1/, ""));
|
||||||
|
|
||||||
const { data: optOutData } = useQuery(GET_PHONE_NUMBER_OPT_OUTS, {
|
const { data: optOutData } = useQuery(GET_PHONE_NUMBER_OPT_OUTS, {
|
||||||
variables: {
|
variables: {
|
||||||
bodyshopid: bodyshop.id,
|
bodyshopid: bodyshop.id,
|
||||||
@@ -64,15 +62,12 @@ function ChatConversationListComponent({ conversationList, selectedConversation,
|
|||||||
const item = sortedConversationList[index];
|
const item = sortedConversationList[index];
|
||||||
const normalizedPhone = phone(item.phone_num, "CA").phoneNumber.replace(/^\+1/, "");
|
const normalizedPhone = phone(item.phone_num, "CA").phoneNumber.replace(/^\+1/, "");
|
||||||
const hasOptOutEntry = optOutMap.has(normalizedPhone);
|
const hasOptOutEntry = optOutMap.has(normalizedPhone);
|
||||||
|
|
||||||
const cardContentRight = <TimeAgoFormatter>{item.updated_at}</TimeAgoFormatter>;
|
const cardContentRight = <TimeAgoFormatter>{item.updated_at}</TimeAgoFormatter>;
|
||||||
const cardContentLeft =
|
const cardContentLeft =
|
||||||
item.job_conversations.length > 0
|
item.job_conversations.length > 0
|
||||||
? item.job_conversations.map((j, idx) => <Tag key={idx}>{j.job.ro_number}</Tag>)
|
? item.job_conversations.map((j, idx) => <Tag key={idx}>{j.job.ro_number}</Tag>)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const names = <>{_.uniq(item.job_conversations.map((j, idx) => OwnerNameDisplayFunction(j.job)))}</>;
|
const names = <>{_.uniq(item.job_conversations.map((j, idx) => OwnerNameDisplayFunction(j.job)))}</>;
|
||||||
|
|
||||||
const cardTitle = (
|
const cardTitle = (
|
||||||
<>
|
<>
|
||||||
{item.label && <Tag color="blue">{item.label}</Tag>}
|
{item.label && <Tag color="blue">{item.label}</Tag>}
|
||||||
@@ -85,7 +80,6 @@ function ChatConversationListComponent({ conversationList, selectedConversation,
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
const cardExtra = (
|
const cardExtra = (
|
||||||
<>
|
<>
|
||||||
<Badge count={item.messages_aggregate.aggregate.count} />
|
<Badge count={item.messages_aggregate.aggregate.count} />
|
||||||
@@ -98,11 +92,10 @@ function ChatConversationListComponent({ conversationList, selectedConversation,
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
const getCardStyle = () =>
|
const getCardStyle = () =>
|
||||||
item.id === selectedConversation
|
item.id === selectedConversation
|
||||||
? { backgroundColor: "rgba(128, 128, 128, 0.2)" }
|
? { backgroundColor: "var(--card-selected-bg)" }
|
||||||
: { backgroundColor: index % 2 === 0 ? "#f0f2f5" : "#ffffff" };
|
: { backgroundColor: index % 2 === 0 ? "var(--card-stripe-even-bg)" : "var(--card-stripe-odd-bg)" };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<List.Item
|
<List.Item
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
max-height: 480px;
|
max-height: 480px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
background-color: #fff;
|
background-color: var(--popover-bg);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.error-message {
|
.error-message {
|
||||||
color: red;
|
color: var(--error-text);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
@@ -25,14 +25,13 @@
|
|||||||
|
|
||||||
.no-jobs-message {
|
.no-jobs-message {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #888;
|
color: var(--no-jobs-text);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Style images within gallery components */
|
/* Style images within gallery components */
|
||||||
.media-selector-content img {
|
.media-selector-content img {
|
||||||
|
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
margin: 4px;
|
margin: 4px;
|
||||||
@@ -40,8 +39,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Grid layout for gallery components */
|
/* Grid layout for gallery components */
|
||||||
.media-selector-content .ant-image, /* Assuming gallery components use Ant Design's Image */
|
.media-selector-content .ant-image,
|
||||||
.media-selector-content .gallery-container { /* Fallback for custom gallery classes */
|
.media-selector-content .gallery-container {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
|
|||||||
@@ -44,7 +44,6 @@
|
|||||||
.chat-send-message-button {
|
.chat-send-message-button {
|
||||||
margin: 0.3rem;
|
margin: 0.3rem;
|
||||||
padding-left: 0.5rem;
|
padding-left: 0.5rem;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-icon {
|
.message-icon {
|
||||||
@@ -52,7 +51,7 @@
|
|||||||
bottom: 0.1rem;
|
bottom: 0.1rem;
|
||||||
right: 0.3rem;
|
right: 0.3rem;
|
||||||
margin: 0 0.1rem;
|
margin: 0 0.1rem;
|
||||||
color: whitesmoke;
|
color: var(--message-icon-color);
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,7 +79,7 @@
|
|||||||
|
|
||||||
&:last-child:after {
|
&:last-child:after {
|
||||||
width: 10px;
|
width: 10px;
|
||||||
background: white;
|
background: var(--message-mine-tail-bg);
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,11 +91,11 @@
|
|||||||
|
|
||||||
.message {
|
.message {
|
||||||
margin-right: 20%;
|
margin-right: 20%;
|
||||||
background-color: #eee;
|
background-color: var(--message-yours-bg);
|
||||||
|
|
||||||
&:last-child:before {
|
&:last-child:before {
|
||||||
left: -7px;
|
left: -7px;
|
||||||
background: #eee;
|
background: var(--message-yours-bg);
|
||||||
border-bottom-right-radius: 15px;
|
border-bottom-right-radius: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,14 +111,14 @@
|
|||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
|
|
||||||
.message {
|
.message {
|
||||||
color: white;
|
color: var(--message-mine-text);
|
||||||
margin-left: 25%;
|
margin-left: 25%;
|
||||||
background: linear-gradient(to bottom, #00d0ea 0%, #0085d1 100%);
|
background: linear-gradient(to bottom, var(--message-mine-bg-start) 0%, var(--message-mine-bg-end) 100%);
|
||||||
padding-bottom: 0.6rem;
|
padding-bottom: 0.6rem;
|
||||||
|
|
||||||
&:last-child:before {
|
&:last-child:before {
|
||||||
right: -8px;
|
right: -8px;
|
||||||
background: linear-gradient(to bottom, #00d0ea 0%, #0085d1 100%);
|
background: linear-gradient(to bottom, var(--message-mine-bg-start) 0%, var(--message-mine-bg-end) 100%);
|
||||||
border-bottom-left-radius: 15px;
|
border-bottom-left-radius: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,32 +134,31 @@
|
|||||||
margin: 0.5rem 10%;
|
margin: 0.5rem 10%;
|
||||||
|
|
||||||
.message {
|
.message {
|
||||||
background-color: #f5f5f5;
|
background-color: var(--system-message-bg);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
color: #555;
|
color: var(--system-message-text);
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
max-width: 80%;
|
max-width: 80%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.system-label {
|
.system-label {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: #888;
|
color: var(--system-label-text);
|
||||||
margin-bottom: 0.2rem;
|
margin-bottom: 0.2rem;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.system-date {
|
.system-date {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: #888;
|
color: var(--system-label-text);
|
||||||
margin-top: 0.2rem;
|
margin-top: 0.2rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.virtuoso-container {
|
.virtuoso-container {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Card, Table, Tag } from "antd";
|
import { Card, Table, Tag } from "antd";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import React, { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import dayjs from "../../../utils/day";
|
import dayjs from "../../../utils/day";
|
||||||
import LoadingSkeleton from "../../loading-skeleton/loading-skeleton.component";
|
import LoadingSkeleton from "../../loading-skeleton/loading-skeleton.component";
|
||||||
@@ -69,7 +69,6 @@ export default function JobLifecycleDashboardComponent({ data, bodyshop, ...card
|
|||||||
];
|
];
|
||||||
|
|
||||||
if (!data) return null;
|
if (!data) return null;
|
||||||
|
|
||||||
if (!data.job_lifecycle || !lifecycleData) return <DashboardRefreshRequired {...cardProps} />;
|
if (!data.job_lifecycle || !lifecycleData) return <DashboardRefreshRequired {...cardProps} />;
|
||||||
|
|
||||||
const extra = `${t("job_lifecycle.content.calculated_based_on")} ${lifecycleData.jobs} ${t("job_lifecycle.content.jobs_in_since")} ${fortyFiveDaysAgo()}`;
|
const extra = `${t("job_lifecycle.content.calculated_based_on")} ${lifecycleData.jobs} ${t("job_lifecycle.content.jobs_in_since")} ${fortyFiveDaysAgo()}`;
|
||||||
@@ -88,7 +87,7 @@ export default function JobLifecycleDashboardComponent({ data, bodyshop, ...card
|
|||||||
borderRadius: "5px",
|
borderRadius: "5px",
|
||||||
borderWidth: "5px",
|
borderWidth: "5px",
|
||||||
borderStyle: "solid",
|
borderStyle: "solid",
|
||||||
borderColor: "#f0f2f5",
|
borderColor: "var(--bar-border-color)",
|
||||||
margin: 0,
|
margin: 0,
|
||||||
padding: 0
|
padding: 0
|
||||||
}}
|
}}
|
||||||
@@ -107,12 +106,10 @@ export default function JobLifecycleDashboardComponent({ data, bodyshop, ...card
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
margin: 0,
|
margin: 0,
|
||||||
padding: 0,
|
padding: 0,
|
||||||
|
borderTop: "1px solid var(--bar-border-color)",
|
||||||
borderTop: "1px solid #f0f2f5",
|
borderBottom: "1px solid var(--bar-border-color)",
|
||||||
borderBottom: "1px solid #f0f2f5",
|
borderLeft: isFirst ? "1px solid var(--bar-border-color)" : undefined,
|
||||||
borderLeft: isFirst ? "1px solid #f0f2f5" : undefined,
|
borderRight: isLast ? "1px solid var(--bar-border-color)" : undefined,
|
||||||
borderRight: isLast ? "1px solid #f0f2f5" : undefined,
|
|
||||||
|
|
||||||
backgroundColor: key.color,
|
backgroundColor: key.color,
|
||||||
width: `${key.percentage}%`
|
width: `${key.percentage}%`
|
||||||
}}
|
}}
|
||||||
@@ -124,7 +121,7 @@ export default function JobLifecycleDashboardComponent({ data, bodyshop, ...card
|
|||||||
<div>{key.roundedPercentage}</div>
|
<div>{key.roundedPercentage}</div>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: "#f0f2f5",
|
backgroundColor: "var(--tag-wrapper-bg)",
|
||||||
borderRadius: "5px",
|
borderRadius: "5px",
|
||||||
paddingRight: "2px",
|
paddingRight: "2px",
|
||||||
paddingLeft: "2px",
|
paddingLeft: "2px",
|
||||||
@@ -152,8 +149,8 @@ export default function JobLifecycleDashboardComponent({ data, bodyshop, ...card
|
|||||||
aria-label={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
|
aria-label={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
|
||||||
title={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
|
title={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: "#f0f2f5",
|
backgroundColor: "var(--tag-wrapper-bg)",
|
||||||
color: "#000",
|
color: "var(--tag-wrapper-text)",
|
||||||
padding: "4px",
|
padding: "4px",
|
||||||
textAlign: "center"
|
textAlign: "center"
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { UploadOutlined, UserAddOutlined } from "@ant-design/icons";
|
import { UploadOutlined, UserAddOutlined } from "@ant-design/icons";
|
||||||
import { Button, Divider, Dropdown, Form, Input, Select, Space, Tabs, Upload } from "antd";
|
import { Button, Divider, Dropdown, Form, Input, Select, Space, Tabs, Upload } from "antd";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import React from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
@@ -15,20 +14,24 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
currentUser: selectCurrentUser,
|
currentUser: selectCurrentUser,
|
||||||
emailConfig: selectEmailConfig
|
emailConfig: selectEmailConfig
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
|
||||||
|
const mapDispatchToProps = () => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(EmailOverlayComponent);
|
export default connect(mapStateToProps, mapDispatchToProps)(EmailOverlayComponent);
|
||||||
|
|
||||||
export function EmailOverlayComponent({ emailConfig, form, selectedMediaState, bodyshop, currentUser }) {
|
export function EmailOverlayComponent({ emailConfig, form, selectedMediaState, bodyshop, currentUser }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const handleClick = ({ item, key, keyPath }) => {
|
|
||||||
|
const handleClick = ({ item }) => {
|
||||||
const email = item.props.value;
|
const email = item.props.value;
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
to: _.uniq([...form.getFieldValue("to"), ...(typeof email === "string" ? [email] : email)])
|
to: _.uniq([...form.getFieldValue("to"), ...(typeof email === "string" ? [email] : email)])
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const handle_CC_Click = ({ item, key, keyPath }) => {
|
|
||||||
|
const handle_CC_Click = ({ item }) => {
|
||||||
const email = item.props.value;
|
const email = item.props.value;
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
cc: _.uniq([...(form.getFieldValue("cc") || ""), ...(typeof email === "string" ? [email] : email)])
|
cc: _.uniq([...(form.getFieldValue("cc") || ""), ...(typeof email === "string" ? [email] : email)])
|
||||||
@@ -52,6 +55,7 @@ export function EmailOverlayComponent({ emailConfig, form, selectedMediaState, b
|
|||||||
],
|
],
|
||||||
onClick: handleClick
|
onClick: handleClick
|
||||||
};
|
};
|
||||||
|
|
||||||
const menuCC = {
|
const menuCC = {
|
||||||
items: [
|
items: [
|
||||||
...bodyshop.employees
|
...bodyshop.employees
|
||||||
@@ -136,26 +140,22 @@ export function EmailOverlayComponent({ emailConfig, form, selectedMediaState, b
|
|||||||
>
|
>
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Divider>{t("emails.labels.preview")}</Divider>
|
<Divider>{t("emails.labels.preview")}</Divider>
|
||||||
{bodyshop.attach_pdf_to_email && <strong>{t("emails.labels.pdfcopywillbeattached")}</strong>}
|
{bodyshop.attach_pdf_to_email && <strong>{t("emails.labels.pdfcopywillbeattached")}</strong>}
|
||||||
|
|
||||||
<Form.Item shouldUpdate>
|
<Form.Item shouldUpdate>
|
||||||
{() => {
|
{() => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
padding: "1rem",
|
padding: "1rem",
|
||||||
|
backgroundColor: "var(--preview-bg)",
|
||||||
backgroundColor: "lightgray",
|
borderLeft: "6px solid var(--preview-border-color)"
|
||||||
borderLeft: "6px solid #2196F3"
|
|
||||||
}}
|
}}
|
||||||
dangerouslySetInnerHTML={{ __html: form.getFieldValue("html") }}
|
dangerouslySetInnerHTML={{ __html: form.getFieldValue("html") }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Tabs
|
<Tabs
|
||||||
defaultActiveKey="documents"
|
defaultActiveKey="documents"
|
||||||
items={[
|
items={[
|
||||||
@@ -184,12 +184,10 @@ export function EmailOverlayComponent({ emailConfig, form, selectedMediaState, b
|
|||||||
return e && e.fileList;
|
return e && e.fileList;
|
||||||
}}
|
}}
|
||||||
rules={[
|
rules={[
|
||||||
({ getFieldValue }) => ({
|
() => ({
|
||||||
validator(rule, value) {
|
validator(rule, value) {
|
||||||
const totalSize = value.reduce((acc, val) => (acc = acc + val.size), 0);
|
const totalSize = value.reduce((acc, val) => (acc = acc + val.size), 0);
|
||||||
|
|
||||||
const limit = 10485760 - new Blob([form.getFieldValue("html")]).size;
|
const limit = 10485760 - new Blob([form.getFieldValue("html")]).size;
|
||||||
|
|
||||||
if (totalSize > limit) {
|
if (totalSize > limit) {
|
||||||
return Promise.reject(t("general.errors.sizelimit"));
|
return Promise.reject(t("general.errors.sizelimit"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
.eula-markdown-card {
|
.eula-markdown-card {
|
||||||
max-height: 50vh;
|
max-height: 50vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
background-color: lightgray;
|
background-color: var(--eula-card-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.eula-markdown-div {
|
.eula-markdown-div {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { forwardRef } from "react";
|
import { forwardRef } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
const LaborTypeFormItem = ({ value, onChange }, ref) => {
|
const LaborTypeFormItem = ({ value }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
if (!value) return null;
|
if (!value) return null;
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import React, { forwardRef } from "react";
|
import { forwardRef } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
const PartTypeFormItem = ({ value, onChange }, ref) => {
|
const PartTypeFormItem = ({ value }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
if (!value) return null;
|
if (!value) return null;
|
||||||
|
|
||||||
return <div>{t(`joblines.fields.part_types.${value}`)}</div>;
|
return (
|
||||||
|
<div style={{ wordWrap: "break-word", overflowWrap: "break-word" }}>{t(`joblines.fields.part_types.${value}`)}</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
export default forwardRef(PartTypeFormItem);
|
export default forwardRef(PartTypeFormItem);
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import Dinero from "dinero.js";
|
import Dinero from "dinero.js";
|
||||||
import React, { forwardRef } from "react";
|
|
||||||
|
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
@@ -8,24 +6,25 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
|||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = () => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
});
|
});
|
||||||
|
|
||||||
const ReadOnlyFormItem = ({ bodyshop, value, type = "text", onChange }, ref) => {
|
const ReadOnlyFormItem = ({ bodyshop, value, type = "text" }) => {
|
||||||
if (!value) return null;
|
if (!value) return null;
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "employee":
|
case "employee": {
|
||||||
const emp = bodyshop.employees.find((e) => e.id === value);
|
const emp = bodyshop.employees.find((e) => e.id === value);
|
||||||
return `${emp?.first_name} ${emp?.last_name}`;
|
return `${emp?.first_name} ${emp?.last_name}`;
|
||||||
|
}
|
||||||
|
|
||||||
case "text":
|
case "text":
|
||||||
return <div>{value}</div>;
|
return <div style={{ wordWrap: "break-word", overflowWrap: "break-word" }}>{value}</div>;
|
||||||
case "currency":
|
case "currency":
|
||||||
return <div>{Dinero({ amount: Math.round(value * 100) }).toFormat()}</div>;
|
return <div>{Dinero({ amount: Math.round(value * 100) }).toFormat()}</div>;
|
||||||
default:
|
default:
|
||||||
return <div>{value}</div>;
|
return <div style={{ wordWrap: "break-word", overflowWrap: "break-word" }}>{value}</div>;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(forwardRef(ReadOnlyFormItem));
|
export default connect(mapStateToProps, mapDispatchToProps)(ReadOnlyFormItem);
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import {
|
|||||||
UsergroupAddOutlined,
|
UsergroupAddOutlined,
|
||||||
UserOutlined
|
UserOutlined
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { FaCalendarAlt, FaCarCrash, FaTasks } from "react-icons/fa";
|
import { FaCalendarAlt, FaCarCrash, FaMoon, FaSun, FaTasks } from "react-icons/fa";
|
||||||
import { BsKanban } from "react-icons/bs";
|
import { BsKanban } from "react-icons/bs";
|
||||||
import { FiLogOut } from "react-icons/fi";
|
import { FiLogOut } from "react-icons/fi";
|
||||||
import { GiPlayerTime, GiSettingsKnobs } from "react-icons/gi";
|
import { GiPlayerTime, GiSettingsKnobs } from "react-icons/gi";
|
||||||
@@ -41,7 +41,9 @@ const buildLeftMenuItems = ({
|
|||||||
setTaskUpsertContext,
|
setTaskUpsertContext,
|
||||||
setReportCenterContext,
|
setReportCenterContext,
|
||||||
signOutStart,
|
signOutStart,
|
||||||
accountingChildren
|
accountingChildren,
|
||||||
|
handleDarkModeToggle,
|
||||||
|
darkMode
|
||||||
}) => {
|
}) => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -331,6 +333,13 @@ const buildLeftMenuItems = ({
|
|||||||
label: t("user.actions.signout"),
|
label: t("user.actions.signout"),
|
||||||
onClick: () => signOutStart()
|
onClick: () => signOutStart()
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "darkmode-toggle",
|
||||||
|
id: "header-darkmode-toggle",
|
||||||
|
label: darkMode ? t("user.actions.light_theme") : t("user.actions.dark_theme"),
|
||||||
|
icon: darkMode ? <FaSun /> : <FaMoon />,
|
||||||
|
onClick: handleDarkModeToggle
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "help",
|
key: "help",
|
||||||
id: "header-help",
|
id: "header-help",
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { createStructuredSelector } from "reselect";
|
|||||||
import { TASKS_CENTER_POLL_INTERVAL, useSocket } from "../../contexts/SocketIO/useSocket.js";
|
import { TASKS_CENTER_POLL_INTERVAL, useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||||
import { GET_UNREAD_COUNT } from "../../graphql/notifications.queries.js";
|
import { GET_UNREAD_COUNT } from "../../graphql/notifications.queries.js";
|
||||||
import { QUERY_MY_TASKS_COUNT } from "../../graphql/tasks.queries.js";
|
import { QUERY_MY_TASKS_COUNT } from "../../graphql/tasks.queries.js";
|
||||||
import { selectRecentItems, selectSelectedHeader } from "../../redux/application/application.selectors";
|
import { selectDarkMode, selectRecentItems, selectSelectedHeader } from "../../redux/application/application.selectors";
|
||||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
import { signOutStart } from "../../redux/user/user.actions";
|
import { signOutStart } from "../../redux/user/user.actions";
|
||||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||||
@@ -22,13 +22,15 @@ import NotificationCenterContainer from "../notification-center/notification-cen
|
|||||||
import TaskCenterContainer from "../task-center/task-center.container.jsx";
|
import TaskCenterContainer from "../task-center/task-center.container.jsx";
|
||||||
import buildAccountingChildren from "./buildAccountingChildren.jsx";
|
import buildAccountingChildren from "./buildAccountingChildren.jsx";
|
||||||
import buildLeftMenuItems from "./buildLeftMenuItems.jsx";
|
import buildLeftMenuItems from "./buildLeftMenuItems.jsx";
|
||||||
|
import { toggleDarkMode } from "../../redux/application/application.actions";
|
||||||
|
|
||||||
// --- Redux mappings ---
|
// --- Redux mappings ---
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
currentUser: selectCurrentUser,
|
currentUser: selectCurrentUser,
|
||||||
recentItems: selectRecentItems,
|
recentItems: selectRecentItems,
|
||||||
selectedHeader: selectSelectedHeader,
|
selectedHeader: selectSelectedHeader,
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop,
|
||||||
|
darkMode: selectDarkMode
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
@@ -38,7 +40,8 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
setReportCenterContext: (context) => dispatch(setModalContext({ context, modal: "reportCenter" })),
|
setReportCenterContext: (context) => dispatch(setModalContext({ context, modal: "reportCenter" })),
|
||||||
signOutStart: () => dispatch(signOutStart()),
|
signOutStart: () => dispatch(signOutStart()),
|
||||||
setCardPaymentContext: (context) => dispatch(setModalContext({ context, modal: "cardPayment" })),
|
setCardPaymentContext: (context) => dispatch(setModalContext({ context, modal: "cardPayment" })),
|
||||||
setTaskUpsertContext: (context) => dispatch(setModalContext({ context, modal: "taskUpsert" }))
|
setTaskUpsertContext: (context) => dispatch(setModalContext({ context, modal: "taskUpsert" })),
|
||||||
|
toggleDarkMode: () => dispatch(toggleDarkMode())
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Utility Hooks ---
|
// --- Utility Hooks ---
|
||||||
@@ -84,22 +87,22 @@ function useIncompleteTaskCount(assignedToId, bodyshopId, isEmployee, isConnecte
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Main Component ---
|
// --- Main Component ---
|
||||||
function Header(props) {
|
function Header({
|
||||||
const {
|
handleMenuClick,
|
||||||
handleMenuClick,
|
currentUser,
|
||||||
currentUser,
|
bodyshop,
|
||||||
bodyshop,
|
selectedHeader,
|
||||||
selectedHeader,
|
signOutStart,
|
||||||
signOutStart,
|
setBillEnterContext,
|
||||||
setBillEnterContext,
|
setTimeTicketContext,
|
||||||
setTimeTicketContext,
|
setPaymentContext,
|
||||||
setPaymentContext,
|
setReportCenterContext,
|
||||||
setReportCenterContext,
|
recentItems,
|
||||||
recentItems,
|
setCardPaymentContext,
|
||||||
setCardPaymentContext,
|
setTaskUpsertContext,
|
||||||
setTaskUpsertContext
|
toggleDarkMode,
|
||||||
} = props;
|
darkMode
|
||||||
|
}) {
|
||||||
// Feature flags
|
// Feature flags
|
||||||
const {
|
const {
|
||||||
treatments: { ImEXPay, DmsAp, Simple_Inventory }
|
treatments: { ImEXPay, DmsAp, Simple_Inventory }
|
||||||
@@ -216,6 +219,10 @@ function Header(props) {
|
|||||||
[handleMenuClick]
|
[handleMenuClick]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleDarkModeToggle = useCallback(() => {
|
||||||
|
toggleDarkMode();
|
||||||
|
}, [toggleDarkMode]);
|
||||||
|
|
||||||
// --- Menu Items ---
|
// --- Menu Items ---
|
||||||
|
|
||||||
// built externally to keep the component clean, but on this level to prevent unnecessary re-renders
|
// built externally to keep the component clean, but on this level to prevent unnecessary re-renders
|
||||||
@@ -257,9 +264,21 @@ function Header(props) {
|
|||||||
setTaskUpsertContext,
|
setTaskUpsertContext,
|
||||||
setReportCenterContext,
|
setReportCenterContext,
|
||||||
signOutStart,
|
signOutStart,
|
||||||
accountingChildren
|
accountingChildren,
|
||||||
|
darkMode,
|
||||||
|
handleDarkModeToggle
|
||||||
}),
|
}),
|
||||||
[t, bodyshop, recentItems, setTaskUpsertContext, setReportCenterContext, signOutStart, accountingChildren]
|
[
|
||||||
|
t,
|
||||||
|
bodyshop,
|
||||||
|
recentItems,
|
||||||
|
setTaskUpsertContext,
|
||||||
|
setReportCenterContext,
|
||||||
|
signOutStart,
|
||||||
|
accountingChildren,
|
||||||
|
darkMode,
|
||||||
|
handleDarkModeToggle
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
const rightMenuItems = useMemo(() => {
|
const rightMenuItems = useMemo(() => {
|
||||||
@@ -292,6 +311,7 @@ function Header(props) {
|
|||||||
),
|
),
|
||||||
onClick: handleTaskCenterClick
|
onClick: handleTaskCenterClick
|
||||||
});
|
});
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}, [
|
}, [
|
||||||
scenarioNotificationsOn,
|
scenarioNotificationsOn,
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import ScheduleEventNote from "./schedule-event.note.component";
|
|||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
setScheduleContext: (context) => dispatch(setModalContext({ context: context, modal: "schedule" })),
|
setScheduleContext: (context) => dispatch(setModalContext({ context: context, modal: "schedule" })),
|
||||||
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
|
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
|
||||||
@@ -64,7 +65,6 @@ export function ScheduleEventComponent({
|
|||||||
const notification = useNotification();
|
const notification = useNotification();
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [popOverVisible, setPopOverVisible] = useState(false);
|
const [popOverVisible, setPopOverVisible] = useState(false);
|
||||||
|
|
||||||
const [getJobDetails] = useLazyQuery(GET_JOB_BY_PK_QUICK_INTAKE, {
|
const [getJobDetails] = useLazyQuery(GET_JOB_BY_PK_QUICK_INTAKE, {
|
||||||
variables: { id: event.job?.id },
|
variables: { id: event.job?.id },
|
||||||
onCompleted: (data) => {
|
onCompleted: (data) => {
|
||||||
@@ -83,7 +83,6 @@ export function ScheduleEventComponent({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
fetchPolicy: "network-only"
|
fetchPolicy: "network-only"
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -115,7 +114,6 @@ export function ScheduleEventComponent({
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button onClick={() => handleCancel({ id: event.id })} disabled={event.arrived}>
|
<Button onClick={() => handleCancel({ id: event.id })} disabled={event.arrived}>
|
||||||
{t("appointments.actions.unblock")}
|
{t("appointments.actions.unblock")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -133,7 +131,6 @@ export function ScheduleEventComponent({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.errors) {
|
if (!res.errors) {
|
||||||
notification["success"]({
|
notification["success"]({
|
||||||
message: t("jobs.successes.converted")
|
message: t("jobs.successes.converted")
|
||||||
@@ -180,7 +177,6 @@ export function ScheduleEventComponent({
|
|||||||
<Form.Item name={["scheduled_delivery"]} label={t("jobs.fields.scheduled_delivery")}>
|
<Form.Item name={["scheduled_delivery"]} label={t("jobs.fields.scheduled_delivery")}>
|
||||||
<FormDateTimePickerComponent disabled={event.ro_number} />
|
<FormDateTimePickerComponent disabled={event.ro_number} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<Button type="primary" onClick={() => form.submit()}>
|
<Button type="primary" onClick={() => form.submit()}>
|
||||||
{t("general.actions.save")}
|
{t("general.actions.save")}
|
||||||
@@ -210,7 +206,6 @@ export function ScheduleEventComponent({
|
|||||||
<ScheduleEventColor event={event} />
|
<ScheduleEventColor event={event} />
|
||||||
</Space>
|
</Space>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{event.job ? (
|
{event.job ? (
|
||||||
<div>
|
<div>
|
||||||
<DataLabel label={t("jobs.fields.ro_number")}>{(event.job && event.job.ro_number) || ""}</DataLabel>
|
<DataLabel label={t("jobs.fields.ro_number")}>{(event.job && event.job.ro_number) || ""}</DataLabel>
|
||||||
@@ -371,7 +366,6 @@ export function ScheduleEventComponent({
|
|||||||
</Button>
|
</Button>
|
||||||
</Popover>
|
</Popover>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{event.isintake ? (
|
{event.isintake ? (
|
||||||
<Button
|
<Button
|
||||||
disabled={event.arrived}
|
disabled={event.arrived}
|
||||||
@@ -428,27 +422,33 @@ export function ScheduleEventComponent({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Adjust event color for dark mode if needed
|
||||||
|
const getEventBackground = () => {
|
||||||
|
if (event?.block) {
|
||||||
|
return "var(--event-block-bg)"; // Use a specific color for dark mode
|
||||||
|
}
|
||||||
|
const baseColor = event.color && event.color.hex ? event.color.hex : event.color || "var(--event-bg-fallback)";
|
||||||
|
// Optionally adjust color for dark mode (e.g., lighten if too dark)
|
||||||
|
return baseColor;
|
||||||
|
};
|
||||||
|
|
||||||
const RegularEvent = event.isintake ? (
|
const RegularEvent = event.isintake ? (
|
||||||
<Space
|
<Space
|
||||||
wrap
|
wrap
|
||||||
size="small"
|
size="small"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: event.color && event.color.hex ? event.color.hex : event.color
|
backgroundColor: getEventBackground()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{event.note && <AlertFilled className="production-alert" />}
|
{event.note && <AlertFilled className="production-alert" />}
|
||||||
<strong>{`${event.job.ro_number || t("general.labels.na")}`}</strong>
|
<strong>{`${event.job.ro_number || t("general.labels.na")}`}</strong>
|
||||||
|
|
||||||
<OwnerNameDisplay ownerObject={event.job} />
|
<OwnerNameDisplay ownerObject={event.job} />
|
||||||
|
|
||||||
{`${(event.job && event.job.v_model_yr) || ""} ${
|
{`${(event.job && event.job.v_model_yr) || ""} ${
|
||||||
(event.job && event.job.v_make_desc) || ""
|
(event.job && event.job.v_make_desc) || ""
|
||||||
} ${(event.job && event.job.v_model_desc) || ""}`}
|
} ${(event.job && event.job.v_model_desc) || ""}`}
|
||||||
|
|
||||||
{`(${(event.job && event.job.labhrs.aggregate.sum.mod_lb_hrs) || "0"} / ${
|
{`(${(event.job && event.job.labhrs.aggregate.sum.mod_lb_hrs) || "0"} / ${
|
||||||
(event.job && event.job.larhrs.aggregate.sum.mod_lb_hrs) || "0"
|
(event.job && event.job.larhrs.aggregate.sum.mod_lb_hrs) || "0"
|
||||||
})`}
|
})`}
|
||||||
|
|
||||||
{event.job && event.job.alt_transport && <div style={{ margin: ".1rem" }}>{event.job.alt_transport}</div>}
|
{event.job && event.job.alt_transport && <div style={{ margin: ".1rem" }}>{event.job.alt_transport}</div>}
|
||||||
{event?.job?.comment && `C: ${event.job.comment}`}
|
{event?.job?.comment && `C: ${event.job.comment}`}
|
||||||
</Space>
|
</Space>
|
||||||
@@ -457,7 +457,7 @@ export function ScheduleEventComponent({
|
|||||||
style={{
|
style={{
|
||||||
height: "100%",
|
height: "100%",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
backgroundColor: event.color && event.color.hex ? event.color.hex : event.color
|
backgroundColor: getEventBackground()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<strong>{`${event.title || ""}`}</strong>
|
<strong>{`${event.title || ""}`}</strong>
|
||||||
@@ -473,8 +473,7 @@ export function ScheduleEventComponent({
|
|||||||
style={{
|
style={{
|
||||||
height: "100%",
|
height: "100%",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
|
backgroundColor: getEventBackground()
|
||||||
backgroundColor: event.color && event.color.hex ? event.color.hex : event.color
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{RegularEvent}
|
{RegularEvent}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
const Car = ({ dmg1, dmg2 }) => {
|
const Car = ({ dmg1, dmg2 }) => {
|
||||||
@@ -8,6 +7,7 @@ const Car = ({ dmg1, dmg2 }) => {
|
|||||||
<div style={{ position: "relative", textAlign: "center" }}>
|
<div style={{ position: "relative", textAlign: "center" }}>
|
||||||
{t("jobs.labels.cards.damage")}
|
{t("jobs.labels.cards.damage")}
|
||||||
<svg
|
<svg
|
||||||
|
className="car-svg"
|
||||||
style={{ left: 0, top: 0, width: "100%", height: "100%" }}
|
style={{ left: 0, top: 0, width: "100%", height: "100%" }}
|
||||||
id="svg166"
|
id="svg166"
|
||||||
version="1.1"
|
version="1.1"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import dayjs from "../../utils/day";
|
import dayjs from "../../utils/day";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { Badge, Card, Space, Table, Tag } from "antd";
|
import { Badge, Card, Space, Table, Tag } from "antd";
|
||||||
@@ -6,24 +6,24 @@ import { gql, useQuery } from "@apollo/client";
|
|||||||
import { DateTimeFormatterFunction } from "../../utils/DateFormatter";
|
import { DateTimeFormatterFunction } from "../../utils/DateFormatter";
|
||||||
import { isEmpty } from "lodash";
|
import { isEmpty } from "lodash";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import "./job-lifecycle.styles.scss";
|
import "./job-lifecycle.styles.scss";
|
||||||
import BlurWrapperComponent from "../feature-wrapper/blur-wrapper.component";
|
import BlurWrapperComponent from "../feature-wrapper/blur-wrapper.component";
|
||||||
|
|
||||||
import UpsellComponent, { upsellEnum } from "../upsell/upsell.component";
|
import UpsellComponent, { upsellEnum } from "../upsell/upsell.component";
|
||||||
|
|
||||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
|
||||||
|
const mapDispatchToProps = () => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
});
|
});
|
||||||
|
|
||||||
// show text on bar if text can fit
|
// show text on bar if text can fit
|
||||||
export function JobLifecycleComponent({ bodyshop, job, statuses, ...rest }) {
|
export function JobLifecycleComponent({ bodyshop, job, statuses }) {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [lifecycleData, setLifecycleData] = useState(null);
|
const [lifecycleData, setLifecycleData] = useState(null);
|
||||||
const { t } = useTranslation(); // Used for tracking external state changes.
|
const { t } = useTranslation(); // Used for tracking external state changes.
|
||||||
@@ -79,7 +79,7 @@ export function JobLifecycleComponent({ bodyshop, job, statuses, ...rest }) {
|
|||||||
title: t("job_lifecycle.columns.value"),
|
title: t("job_lifecycle.columns.value"),
|
||||||
dataIndex: "value",
|
dataIndex: "value",
|
||||||
key: "value",
|
key: "value",
|
||||||
render: (text, record) => (
|
render: (text) => (
|
||||||
<BlurWrapperComponent
|
<BlurWrapperComponent
|
||||||
featureName="lifecycle"
|
featureName="lifecycle"
|
||||||
bypass
|
bypass
|
||||||
@@ -95,7 +95,7 @@ export function JobLifecycleComponent({ bodyshop, job, statuses, ...rest }) {
|
|||||||
dataIndex: "start",
|
dataIndex: "start",
|
||||||
key: "start",
|
key: "start",
|
||||||
sorter: (a, b) => dayjs(a.start).unix() - dayjs(b.start).unix(),
|
sorter: (a, b) => dayjs(a.start).unix() - dayjs(b.start).unix(),
|
||||||
render: (text, record) => (
|
render: (text) => (
|
||||||
<BlurWrapperComponent featureName="lifecycle" bypass valueProp="children" overrideValueFunction="RandomDate">
|
<BlurWrapperComponent featureName="lifecycle" bypass valueProp="children" overrideValueFunction="RandomDate">
|
||||||
<span>{DateTimeFormatterFunction(text)}</span>
|
<span>{DateTimeFormatterFunction(text)}</span>
|
||||||
</BlurWrapperComponent>
|
</BlurWrapperComponent>
|
||||||
@@ -119,8 +119,7 @@ export function JobLifecycleComponent({ bodyshop, job, statuses, ...rest }) {
|
|||||||
}
|
}
|
||||||
return dayjs(a.end).unix() - dayjs(b.end).unix();
|
return dayjs(a.end).unix() - dayjs(b.end).unix();
|
||||||
},
|
},
|
||||||
|
render: (text) => (
|
||||||
render: (text, record) => (
|
|
||||||
<BlurWrapperComponent featureName="lifecycle" bypass valueProp="children" overrideValueFunction="RandomDate">
|
<BlurWrapperComponent featureName="lifecycle" bypass valueProp="children" overrideValueFunction="RandomDate">
|
||||||
<span>{isEmpty(text) ? t("job_lifecycle.content.not_available") : DateTimeFormatterFunction(text)}</span>
|
<span>{isEmpty(text) ? t("job_lifecycle.content.not_available") : DateTimeFormatterFunction(text)}</span>
|
||||||
</BlurWrapperComponent>
|
</BlurWrapperComponent>
|
||||||
@@ -170,7 +169,7 @@ export function JobLifecycleComponent({ bodyshop, job, statuses, ...rest }) {
|
|||||||
borderRadius: "5px",
|
borderRadius: "5px",
|
||||||
borderWidth: "5px",
|
borderWidth: "5px",
|
||||||
borderStyle: "solid",
|
borderStyle: "solid",
|
||||||
borderColor: "#f0f2f5",
|
borderColor: "var(--bar-border-color)",
|
||||||
margin: 0,
|
margin: 0,
|
||||||
padding: 0
|
padding: 0
|
||||||
}}
|
}}
|
||||||
@@ -189,12 +188,10 @@ export function JobLifecycleComponent({ bodyshop, job, statuses, ...rest }) {
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
margin: 0,
|
margin: 0,
|
||||||
padding: 0,
|
padding: 0,
|
||||||
|
borderTop: "1px solid var(--bar-border-color)",
|
||||||
borderTop: "1px solid #f0f2f5",
|
borderBottom: "1px solid var(--bar-border-color)",
|
||||||
borderBottom: "1px solid #f0f2f5",
|
borderLeft: isFirst ? "1px solid var(--bar-border-color)" : undefined,
|
||||||
borderLeft: isFirst ? "1px solid #f0f2f5" : undefined,
|
borderRight: isLast ? "1px solid var(--bar-border-color)" : undefined,
|
||||||
borderRight: isLast ? "1px solid #f0f2f5" : undefined,
|
|
||||||
|
|
||||||
backgroundColor: key.color,
|
backgroundColor: key.color,
|
||||||
width: `${key.percentage}%`
|
width: `${key.percentage}%`
|
||||||
}}
|
}}
|
||||||
@@ -206,7 +203,7 @@ export function JobLifecycleComponent({ bodyshop, job, statuses, ...rest }) {
|
|||||||
<div>{key.roundedPercentage}</div>
|
<div>{key.roundedPercentage}</div>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: "#f0f2f5",
|
backgroundColor: "var(--tag-wrapper-bg)",
|
||||||
borderRadius: "5px",
|
borderRadius: "5px",
|
||||||
paddingRight: "2px",
|
paddingRight: "2px",
|
||||||
paddingLeft: "2px",
|
paddingLeft: "2px",
|
||||||
@@ -230,8 +227,8 @@ export function JobLifecycleComponent({ bodyshop, job, statuses, ...rest }) {
|
|||||||
aria-label={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
|
aria-label={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
|
||||||
title={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
|
title={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: "#f0f2f5",
|
backgroundColor: "var(--tag-wrapper-bg)",
|
||||||
color: "#000",
|
color: "var(--tag-wrapper-text)",
|
||||||
padding: "4px",
|
padding: "4px",
|
||||||
textAlign: "center"
|
textAlign: "center"
|
||||||
}}
|
}}
|
||||||
@@ -315,4 +312,5 @@ export function JobLifecycleComponent({ bodyshop, job, statuses, ...rest }) {
|
|||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(JobLifecycleComponent);
|
export default connect(mapStateToProps, mapDispatchToProps)(JobLifecycleComponent);
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { PushpinFilled, PushpinOutlined } from "@ant-design/icons";
|
||||||
|
import { useMutation } from "@apollo/client";
|
||||||
|
import { UPDATE_NOTE } from "../../graphql/notes.queries";
|
||||||
|
|
||||||
|
function JobNotesPinToggle({ note }) {
|
||||||
|
const [updateNote] = useMutation(UPDATE_NOTE);
|
||||||
|
|
||||||
|
const handlePinToggle = () => {
|
||||||
|
updateNote({
|
||||||
|
variables: {
|
||||||
|
noteId: note.id,
|
||||||
|
note: { pinned: !note.pinned }
|
||||||
|
},
|
||||||
|
refetchQueries: ["GET_JOB_BY_PK", "QUERY_JOB_CARD_DETAILS", "QUERY_PARTS_QUEUE_CARD_DETAILS"]
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return note.pinned ? (
|
||||||
|
<PushpinFilled size="large" onClick={handlePinToggle} style={{ color: "gold" }} />
|
||||||
|
) : (
|
||||||
|
<PushpinOutlined size="large" onClick={handlePinToggle} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default JobNotesPinToggle;
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
import { DownCircleFilled } from "@ant-design/icons";
|
import { DownCircleFilled } from "@ant-design/icons";
|
||||||
import { useMutation } from "@apollo/client";
|
import { useMutation } from "@apollo/client";
|
||||||
import { Button, Dropdown } from "antd";
|
import { Button, Dropdown } from "antd";
|
||||||
import React, { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
import { UPDATE_JOB_STATUS } from "../../graphql/jobs.queries";
|
import { UPDATE_JOB_STATUS } from "../../graphql/jobs.queries";
|
||||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -24,7 +24,6 @@ export function JobsChangeStatus({ job, bodyshop, jobRO, insertAuditTrail }) {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [availableStatuses, setAvailableStatuses] = useState([]);
|
const [availableStatuses, setAvailableStatuses] = useState([]);
|
||||||
const [otherStages, setOtherStages] = useState([]);
|
|
||||||
const [mutationUpdateJobstatus] = useMutation(UPDATE_JOB_STATUS);
|
const [mutationUpdateJobstatus] = useMutation(UPDATE_JOB_STATUS);
|
||||||
const notification = useNotification();
|
const notification = useNotification();
|
||||||
|
|
||||||
@@ -32,7 +31,7 @@ export function JobsChangeStatus({ job, bodyshop, jobRO, insertAuditTrail }) {
|
|||||||
mutationUpdateJobstatus({
|
mutationUpdateJobstatus({
|
||||||
variables: { jobId: job.id, status: status }
|
variables: { jobId: job.id, status: status }
|
||||||
})
|
})
|
||||||
.then((r) => {
|
.then(() => {
|
||||||
notification["success"]({ message: t("jobs.successes.save") });
|
notification["success"]({ message: t("jobs.successes.save") });
|
||||||
insertAuditTrail({
|
insertAuditTrail({
|
||||||
jobid: job.id,
|
jobid: job.id,
|
||||||
@@ -41,7 +40,7 @@ export function JobsChangeStatus({ job, bodyshop, jobRO, insertAuditTrail }) {
|
|||||||
});
|
});
|
||||||
// refetch();
|
// refetch();
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch(() => {
|
||||||
notification["error"]({ message: t("jobs.errors.saving") });
|
notification["error"]({ message: t("jobs.errors.saving") });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -51,19 +50,14 @@ export function JobsChangeStatus({ job, bodyshop, jobRO, insertAuditTrail }) {
|
|||||||
if (job && bodyshop) {
|
if (job && bodyshop) {
|
||||||
if (bodyshop.md_ro_statuses.pre_production_statuses.includes(job.status)) {
|
if (bodyshop.md_ro_statuses.pre_production_statuses.includes(job.status)) {
|
||||||
setAvailableStatuses(bodyshop.md_ro_statuses.pre_production_statuses);
|
setAvailableStatuses(bodyshop.md_ro_statuses.pre_production_statuses);
|
||||||
if (bodyshop.md_ro_statuses.production_statuses[0])
|
|
||||||
setOtherStages([bodyshop.md_ro_statuses.production_statuses[0]]);
|
|
||||||
} else if (bodyshop.md_ro_statuses.production_statuses.includes(job.status)) {
|
} else if (bodyshop.md_ro_statuses.production_statuses.includes(job.status)) {
|
||||||
setAvailableStatuses(bodyshop.md_ro_statuses.production_statuses);
|
setAvailableStatuses(bodyshop.md_ro_statuses.production_statuses);
|
||||||
setOtherStages([bodyshop.md_ro_statuses.default_imported, bodyshop.md_ro_statuses.default_delivered]);
|
|
||||||
} else if (bodyshop.md_ro_statuses.post_production_statuses.includes(job.status)) {
|
} else if (bodyshop.md_ro_statuses.post_production_statuses.includes(job.status)) {
|
||||||
setAvailableStatuses(
|
setAvailableStatuses(
|
||||||
bodyshop.md_ro_statuses.post_production_statuses.filter(
|
bodyshop.md_ro_statuses.post_production_statuses.filter(
|
||||||
(s) => s !== bodyshop.md_ro_statuses.default_invoiced && s !== bodyshop.md_ro_statuses.default_exported
|
(s) => s !== bodyshop.md_ro_statuses.default_invoiced && s !== bodyshop.md_ro_statuses.default_exported
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
if (bodyshop.md_ro_statuses.production_statuses[0])
|
|
||||||
setOtherStages([bodyshop.md_ro_statuses.production_statuses[0]]);
|
|
||||||
} else {
|
} else {
|
||||||
console.log("Status didn't match any restrictions. Allowing all status changes.");
|
console.log("Status didn't match any restrictions. Allowing all status changes.");
|
||||||
setAvailableStatuses(bodyshop.md_ro_statuses.statuses);
|
setAvailableStatuses(bodyshop.md_ro_statuses.statuses);
|
||||||
@@ -76,16 +70,7 @@ export function JobsChangeStatus({ job, bodyshop, jobRO, insertAuditTrail }) {
|
|||||||
...availableStatuses.map((item) => ({
|
...availableStatuses.map((item) => ({
|
||||||
key: item,
|
key: item,
|
||||||
label: item
|
label: item
|
||||||
})),
|
}))
|
||||||
...(job.converted
|
|
||||||
? [
|
|
||||||
{ type: "divider" },
|
|
||||||
...otherStages.map((item) => ({
|
|
||||||
key: item,
|
|
||||||
label: item
|
|
||||||
}))
|
|
||||||
]
|
|
||||||
: [])
|
|
||||||
],
|
],
|
||||||
onClick: (e) => updateJobStatus(e.key)
|
onClick: (e) => updateJobStatus(e.key)
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import { WarningOutlined } from "@ant-design/icons";
|
||||||
import { Form, Select, Space, Tooltip } from "antd";
|
import { Form, Select, Space, Tooltip } from "antd";
|
||||||
import React from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
@@ -8,14 +8,13 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
|||||||
import LaborTypeFormItem from "../form-items-formatted/labor-type-form-item.component";
|
import LaborTypeFormItem from "../form-items-formatted/labor-type-form-item.component";
|
||||||
import PartTypeFormItem from "../form-items-formatted/part-type-form-item.component";
|
import PartTypeFormItem from "../form-items-formatted/part-type-form-item.component";
|
||||||
import ReadOnlyFormItem from "../form-items-formatted/read-only-form-item.component";
|
import ReadOnlyFormItem from "../form-items-formatted/read-only-form-item.component";
|
||||||
import { WarningOutlined } from "@ant-design/icons";
|
|
||||||
import "./jobs-close-lines.styles.scss";
|
import "./jobs-close-lines.styles.scss";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
jobRO: selectJobReadOnly
|
jobRO: selectJobReadOnly
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = () => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -24,7 +23,7 @@ export function JobsCloseLines({ bodyshop, job, jobRO }) {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Form.List name={["joblines"]}>
|
<Form.List name={["joblines"]}>
|
||||||
{(fields, { add, remove, move }) => {
|
{(fields) => {
|
||||||
return (
|
return (
|
||||||
<table className="jobs-close-table">
|
<table className="jobs-close-table">
|
||||||
<thead>
|
<thead>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
td {
|
td {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
border-bottom: 1px solid #ddd;
|
border-bottom: 1px solid var(--table-border-color);
|
||||||
|
|
||||||
.ant-form-item {
|
.ant-form-item {
|
||||||
margin-bottom: 0px !important;
|
margin-bottom: 0px !important;
|
||||||
@@ -14,6 +14,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
tr:hover {
|
tr:hover {
|
||||||
background-color: #f5f5f5;
|
background-color: var(--table-hover-bg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import JobAltTransportChange from "../job-at-change/job-at-change.component";
|
|||||||
import JobEmployeeAssignments from "../job-employee-assignments/job-employee-assignments.container";
|
import JobEmployeeAssignments from "../job-employee-assignments/job-employee-assignments.container";
|
||||||
import JobsRelatedRos from "../jobs-related-ros/jobs-related-ros.component";
|
import JobsRelatedRos from "../jobs-related-ros/jobs-related-ros.component";
|
||||||
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||||
|
import PinnedJobNotes from "../pinned-job-notes/pinned-job-notes.component.jsx";
|
||||||
import ProductionListColumnComment from "../production-list-columns/production-list-columns.comment.component";
|
import ProductionListColumnComment from "../production-list-columns/production-list-columns.comment.component";
|
||||||
import ProductionListColumnProductionNote from "../production-list-columns/production-list-columns.productionnote.component";
|
import ProductionListColumnProductionNote from "../production-list-columns/production-list-columns.productionnote.component";
|
||||||
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
|
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
|
||||||
@@ -102,254 +103,257 @@ export function JobsDetailHeader({ job, bodyshop, disabled, insertAuditTrail })
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row gutter={[16, 16]} style={{ alignItems: "stretch" }}>
|
<>
|
||||||
<Col {...colSpan}>
|
<Row gutter={[16, 16]} style={{ alignItems: "stretch" }}>
|
||||||
<Card title={"Job Status"} style={{ height: "100%" }}>
|
<Col {...colSpan}>
|
||||||
<div>
|
<Card title={"Job Status"} style={{ height: "100%" }}>
|
||||||
<DataLabel label={t("jobs.fields.status")}>
|
<div>
|
||||||
|
<DataLabel label={t("jobs.fields.status")}>
|
||||||
|
<Space wrap>
|
||||||
|
{job.status}
|
||||||
|
{job.inproduction && <Tag color="#f50">{t("jobs.labels.inproduction")}</Tag>}
|
||||||
|
{job.suspended && <PauseCircleOutlined style={{ color: "orangered" }} />}
|
||||||
|
{job.iouparent && (
|
||||||
|
<Link to={`/manage/jobs/${job.iouparent}`}>
|
||||||
|
<Tooltip title={t("jobs.labels.iou")}>
|
||||||
|
<BranchesOutlined style={{ color: "orangered" }} />
|
||||||
|
</Tooltip>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
{job.production_vars && job.production_vars.alert ? (
|
||||||
|
<ExclamationCircleFilled className="production-alert" />
|
||||||
|
) : null}
|
||||||
|
{job.status === bodyshop.md_ro_statuses.default_scheduled && job.scheduled_in ? (
|
||||||
|
<Tag>
|
||||||
|
<Link to={`/manage/schedule?date=${dayjs(job.scheduled_in).format("YYYY-MM-DD")}`}>
|
||||||
|
<DateTimeFormatter>{job.scheduled_in}</DateTimeFormatter>
|
||||||
|
</Link>
|
||||||
|
</Tag>
|
||||||
|
) : null}
|
||||||
|
</Space>
|
||||||
|
</DataLabel>
|
||||||
|
<DataLabel label={t("jobs.fields.comment")} valueStyle={{ overflow: "hidden", textOverflow: "ellipsis" }}>
|
||||||
|
<ProductionListColumnComment record={job} />
|
||||||
|
</DataLabel>
|
||||||
|
<DataLabel label={t("jobs.fields.ins_co_nm_short")}>{job.ins_co_nm}</DataLabel>
|
||||||
|
<DataLabel label={t("jobs.fields.clm_no")}>{job.clm_no}</DataLabel>
|
||||||
|
<DataLabel label={t("jobs.fields.ponumber")} hideIfNull>
|
||||||
|
{job.po_number}
|
||||||
|
</DataLabel>
|
||||||
|
<DataLabel label={t("jobs.fields.repairtotal")}>
|
||||||
|
<CurrencyFormatter>{job.clm_total}</CurrencyFormatter>
|
||||||
|
<span style={{ margin: "0rem .5rem" }}>/</span>
|
||||||
|
<CurrencyFormatter>{job.owner_owing}</CurrencyFormatter>
|
||||||
|
</DataLabel>
|
||||||
|
<DataLabel label={t("jobs.fields.alt_transport")}>
|
||||||
|
{job.alt_transport}
|
||||||
|
<JobAltTransportChange job={job} />
|
||||||
|
</DataLabel>
|
||||||
|
{job?.cccontracts?.length > 0 && (
|
||||||
|
<DataLabel label={t("jobs.labels.contracts")}>
|
||||||
|
{job.cccontracts.map((c, index) => (
|
||||||
|
<Space key={c.id} wrap>
|
||||||
|
<Link to={`/manage/courtesycars/contracts/${c.id}`}>
|
||||||
|
{`${c.agreementnumber} - ${c.courtesycar.fleetnumber} ${c.courtesycar.year} ${c.courtesycar.make} ${c.courtesycar.model}`}
|
||||||
|
{index !== job.cccontracts.length - 1 ? "," : null}
|
||||||
|
</Link>
|
||||||
|
</Space>
|
||||||
|
))}
|
||||||
|
</DataLabel>
|
||||||
|
)}
|
||||||
|
<DataLabel label={t("jobs.fields.production_vars.note")}>
|
||||||
|
<ProductionListColumnProductionNote record={job} />
|
||||||
|
</DataLabel>
|
||||||
|
<DataLabel label={t("jobs.fields.estimate_sent_approval")}>
|
||||||
|
<Space>
|
||||||
|
<Checkbox
|
||||||
|
checked={!!job.estimate_sent_approval}
|
||||||
|
onChange={(e) => handleCheckboxChange("estimate_sent_approval", e.target.checked)}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
{job.estimate_sent_approval && (
|
||||||
|
<span style={{ color: "#888" }}>
|
||||||
|
<DateTimeFormatter>{job.estimate_sent_approval}</DateTimeFormatter>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Checkbox>
|
||||||
|
</Space>
|
||||||
|
</DataLabel>
|
||||||
|
<DataLabel label={t("jobs.fields.estimate_approved")}>
|
||||||
|
<Space>
|
||||||
|
<Checkbox
|
||||||
|
checked={!!job.estimate_approved}
|
||||||
|
onChange={(e) => handleCheckboxChange("estimate_approved", e.target.checked)}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
{job.estimate_approved && (
|
||||||
|
<span style={{ color: "#888" }}>
|
||||||
|
<DateTimeFormatter>{job.estimate_approved}</DateTimeFormatter>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Checkbox>
|
||||||
|
</Space>
|
||||||
|
</DataLabel>
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
{job.status}
|
{job.special_coverage_policy && (
|
||||||
{job.inproduction && <Tag color="#f50">{t("jobs.labels.inproduction")}</Tag>}
|
<Tag color="tomato">
|
||||||
{job.suspended && <PauseCircleOutlined style={{ color: "orangered" }} />}
|
<Space>
|
||||||
{job.iouparent && (
|
<WarningFilled />
|
||||||
<Link to={`/manage/jobs/${job.iouparent}`}>
|
<span>{t("jobs.labels.specialcoveragepolicy")}</span>
|
||||||
<Tooltip title={t("jobs.labels.iou")}>
|
</Space>
|
||||||
<BranchesOutlined style={{ color: "orangered" }} />
|
|
||||||
</Tooltip>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
{job.production_vars && job.production_vars.alert ? (
|
|
||||||
<ExclamationCircleFilled className="production-alert" />
|
|
||||||
) : null}
|
|
||||||
{job.status === bodyshop.md_ro_statuses.default_scheduled && job.scheduled_in ? (
|
|
||||||
<Tag>
|
|
||||||
<Link to={`/manage/schedule?date=${dayjs(job.scheduled_in).format("YYYY-MM-DD")}`}>
|
|
||||||
<DateTimeFormatter>{job.scheduled_in}</DateTimeFormatter>
|
|
||||||
</Link>
|
|
||||||
</Tag>
|
</Tag>
|
||||||
) : null}
|
)}
|
||||||
|
{job.ca_gst_registrant && (
|
||||||
|
<Tag color="geekblue">
|
||||||
|
<Space>
|
||||||
|
<WarningFilled />
|
||||||
|
<span>{t("jobs.fields.ca_gst_registrant")}</span>
|
||||||
|
</Space>
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
{job.hit_and_run && (
|
||||||
|
<Tag color="green">
|
||||||
|
<Space>
|
||||||
|
<WarningFilled />
|
||||||
|
<span>{t("jobs.fields.hit_and_run")}</span>
|
||||||
|
</Space>
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
</DataLabel>
|
</div>
|
||||||
<DataLabel label={t("jobs.fields.comment")} valueStyle={{ overflow: "hidden", textOverflow: "ellipsis" }}>
|
</Card>
|
||||||
<ProductionListColumnComment record={job} />
|
</Col>
|
||||||
</DataLabel>
|
<Col {...colSpan}>
|
||||||
<DataLabel label={t("jobs.fields.ins_co_nm_short")}>{job.ins_co_nm}</DataLabel>
|
<Card
|
||||||
<DataLabel label={t("jobs.fields.clm_no")}>{job.clm_no}</DataLabel>
|
style={{ height: "100%" }}
|
||||||
<DataLabel label={t("jobs.fields.ponumber")} hideIfNull>
|
title={
|
||||||
{job.po_number}
|
|
||||||
</DataLabel>
|
|
||||||
<DataLabel label={t("jobs.fields.repairtotal")}>
|
|
||||||
<CurrencyFormatter>{job.clm_total}</CurrencyFormatter>
|
|
||||||
<span style={{ margin: "0rem .5rem" }}>/</span>
|
|
||||||
<CurrencyFormatter>{job.owner_owing}</CurrencyFormatter>
|
|
||||||
</DataLabel>
|
|
||||||
<DataLabel label={t("jobs.fields.alt_transport")}>
|
|
||||||
{job.alt_transport}
|
|
||||||
<JobAltTransportChange job={job} />
|
|
||||||
</DataLabel>
|
|
||||||
{job?.cccontracts?.length > 0 && (
|
|
||||||
<DataLabel label={t("jobs.labels.contracts")}>
|
|
||||||
{job.cccontracts.map((c, index) => (
|
|
||||||
<Space key={c.id} wrap>
|
|
||||||
<Link to={`/manage/courtesycars/contracts/${c.id}`}>
|
|
||||||
{`${c.agreementnumber} - ${c.courtesycar.fleetnumber} ${c.courtesycar.year} ${c.courtesycar.make} ${c.courtesycar.model}`}
|
|
||||||
{index !== job.cccontracts.length - 1 ? "," : null}
|
|
||||||
</Link>
|
|
||||||
</Space>
|
|
||||||
))}
|
|
||||||
</DataLabel>
|
|
||||||
)}
|
|
||||||
<DataLabel label={t("jobs.fields.production_vars.note")}>
|
|
||||||
<ProductionListColumnProductionNote record={job} />
|
|
||||||
</DataLabel>
|
|
||||||
<DataLabel label={t("jobs.fields.estimate_sent_approval")}>
|
|
||||||
<Space>
|
|
||||||
<Checkbox
|
|
||||||
checked={!!job.estimate_sent_approval}
|
|
||||||
onChange={(e) => handleCheckboxChange("estimate_sent_approval", e.target.checked)}
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
{job.estimate_sent_approval && (
|
|
||||||
<span style={{ color: "#888" }}>
|
|
||||||
<DateTimeFormatter>{job.estimate_sent_approval}</DateTimeFormatter>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Checkbox>
|
|
||||||
</Space>
|
|
||||||
</DataLabel>
|
|
||||||
<DataLabel label={t("jobs.fields.estimate_approved")}>
|
|
||||||
<Space>
|
|
||||||
<Checkbox
|
|
||||||
checked={!!job.estimate_approved}
|
|
||||||
onChange={(e) => handleCheckboxChange("estimate_approved", e.target.checked)}
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
{job.estimate_approved && (
|
|
||||||
<span style={{ color: "#888" }}>
|
|
||||||
<DateTimeFormatter>{job.estimate_approved}</DateTimeFormatter>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Checkbox>
|
|
||||||
</Space>
|
|
||||||
</DataLabel>
|
|
||||||
<Space wrap>
|
|
||||||
{job.special_coverage_policy && (
|
|
||||||
<Tag color="tomato">
|
|
||||||
<Space>
|
|
||||||
<WarningFilled />
|
|
||||||
<span>{t("jobs.labels.specialcoveragepolicy")}</span>
|
|
||||||
</Space>
|
|
||||||
</Tag>
|
|
||||||
)}
|
|
||||||
{job.ca_gst_registrant && (
|
|
||||||
<Tag color="geekblue">
|
|
||||||
<Space>
|
|
||||||
<WarningFilled />
|
|
||||||
<span>{t("jobs.fields.ca_gst_registrant")}</span>
|
|
||||||
</Space>
|
|
||||||
</Tag>
|
|
||||||
)}
|
|
||||||
{job.hit_and_run && (
|
|
||||||
<Tag color="green">
|
|
||||||
<Space>
|
|
||||||
<WarningFilled />
|
|
||||||
<span>{t("jobs.fields.hit_and_run")}</span>
|
|
||||||
</Space>
|
|
||||||
</Tag>
|
|
||||||
)}
|
|
||||||
</Space>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
<Col {...colSpan}>
|
|
||||||
<Card
|
|
||||||
style={{ height: "100%" }}
|
|
||||||
title={
|
|
||||||
disabled ? (
|
|
||||||
<>{ownerTitle.length > 0 ? ownerTitle : t("owner.labels.noownerinfo")}</>
|
|
||||||
) : (
|
|
||||||
<Link to={`/manage/owners/${job.owner.id}`}>
|
|
||||||
{ownerTitle.length > 0 ? ownerTitle : t("owner.labels.noownerinfo")}
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<DataLabel key="2" label={t("jobs.fields.ownr_ph1")}>
|
|
||||||
{disabled ? (
|
|
||||||
<PhoneNumberFormatter>{job.ownr_ph1}</PhoneNumberFormatter>
|
|
||||||
) : (
|
|
||||||
<ChatOpenButton phone={job.ownr_ph1} jobid={job.id} />
|
|
||||||
)}
|
|
||||||
</DataLabel>
|
|
||||||
<DataLabel key="22" label={t("jobs.fields.ownr_ph2")}>
|
|
||||||
{disabled ? (
|
|
||||||
<PhoneNumberFormatter>{job.ownr_ph2}</PhoneNumberFormatter>
|
|
||||||
) : (
|
|
||||||
<ChatOpenButton phone={job.ownr_ph2} jobid={job.id} />
|
|
||||||
)}
|
|
||||||
</DataLabel>
|
|
||||||
<DataLabel key="3" label={t("owners.fields.address")}>
|
|
||||||
{`${job.ownr_addr1 || ""} ${job.ownr_addr2 || ""} ${
|
|
||||||
job.ownr_city || ""
|
|
||||||
} ${job.ownr_st || ""} ${job.ownr_zip || ""}`}
|
|
||||||
</DataLabel>
|
|
||||||
<DataLabel key="4" label={t("owners.fields.ownr_ea")}>
|
|
||||||
{disabled ? (
|
|
||||||
<>{job.ownr_ea || ""}</>
|
|
||||||
) : job.ownr_ea ? (
|
|
||||||
<a href={`mailto:${job.ownr_ea}`}>{job.ownr_ea}</a>
|
|
||||||
) : null}
|
|
||||||
</DataLabel>
|
|
||||||
{job.owner?.tax_number && (
|
|
||||||
<DataLabel key="5" label={t("owners.fields.tax_number")}>
|
|
||||||
{job.owner?.tax_number || ""}
|
|
||||||
</DataLabel>
|
|
||||||
)}
|
|
||||||
<DataLabel label={t("owners.fields.note")} valueStyle={{ overflow: "hidden", textOverflow: "ellipsis" }}>
|
|
||||||
{job.owner?.note || ""}
|
|
||||||
</DataLabel>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
<Col {...colSpan}>
|
|
||||||
<Card
|
|
||||||
style={{ height: "100%" }}
|
|
||||||
title={
|
|
||||||
job.vehicle ? (
|
|
||||||
disabled ? (
|
disabled ? (
|
||||||
<>{vehicleTitle.length > 0 ? vehicleTitle : t("vehicles.labels.novehinfo")} </>
|
<>{ownerTitle.length > 0 ? ownerTitle : t("owner.labels.noownerinfo")}</>
|
||||||
) : (
|
) : (
|
||||||
<Link to={job.vehicle && `/manage/vehicles/${job.vehicle.id}`}>
|
<Link to={`/manage/owners/${job.owner.id}`}>
|
||||||
{vehicleTitle.length > 0 ? vehicleTitle : t("vehicles.labels.novehinfo")}
|
{ownerTitle.length > 0 ? ownerTitle : t("owner.labels.noownerinfo")}
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
) : (
|
}
|
||||||
<span></span>
|
>
|
||||||
)
|
<div>
|
||||||
}
|
<DataLabel key="2" label={t("jobs.fields.ownr_ph1")}>
|
||||||
>
|
{disabled ? (
|
||||||
<div>
|
<PhoneNumberFormatter>{job.ownr_ph1}</PhoneNumberFormatter>
|
||||||
<DataLabel key="2" label={t("vehicles.fields.plate_no")}>
|
) : (
|
||||||
{`${job.plate_no || t("general.labels.na")} (${`${job.plate_st || t("general.labels.na")}`})`}
|
<ChatOpenButton phone={job.ownr_ph1} jobid={job.id} />
|
||||||
</DataLabel>
|
)}
|
||||||
<DataLabel key="4" label={t("vehicles.fields.v_vin")}>
|
|
||||||
<VehicleVinDisplay>{`${job.v_vin || t("general.labels.na")}`}</VehicleVinDisplay>
|
|
||||||
{bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid ? (
|
|
||||||
job.v_vin?.length !== 17 ? (
|
|
||||||
<WarningFilled style={{ color: "tomato", marginLeft: ".3rem" }} />
|
|
||||||
) : null
|
|
||||||
) : null}
|
|
||||||
</DataLabel>
|
|
||||||
<DataLabel label={t("jobs.fields.regie_number")}>{job.regie_number || t("general.labels.na")}</DataLabel>
|
|
||||||
<DataLabel label={t("jobs.labels.relatedros")}>
|
|
||||||
<JobsRelatedRos jobid={job.id} job={job} disabled={disabled} />
|
|
||||||
</DataLabel>
|
|
||||||
{job.vehicle && job.vehicle.notes && (
|
|
||||||
<DataLabel
|
|
||||||
label={t("vehicles.fields.notes")}
|
|
||||||
valueStyle={{ whiteSpace: "pre-wrap" }}
|
|
||||||
valueClassName={notesClamped ? "clamp" : ""}
|
|
||||||
onValueClick={() => setNotesClamped(!notesClamped)}
|
|
||||||
>
|
|
||||||
{job.vehicle.notes}
|
|
||||||
</DataLabel>
|
</DataLabel>
|
||||||
)}
|
<DataLabel key="22" label={t("jobs.fields.ownr_ph2")}>
|
||||||
{job.vehicle && job.vehicle.v_paint_codes && (
|
{disabled ? (
|
||||||
<DataLabel label={t("vehicles.fields.v_paint_codes", { number: "" })}>
|
<PhoneNumberFormatter>{job.ownr_ph2}</PhoneNumberFormatter>
|
||||||
<span style={{ whiteSpace: "pre" }}>
|
) : (
|
||||||
{Object.keys(job.vehicle.v_paint_codes)
|
<ChatOpenButton phone={job.ownr_ph2} jobid={job.id} />
|
||||||
.filter(
|
)}
|
||||||
(key) =>
|
|
||||||
job.vehicle.v_paint_codes[key] !== "" &&
|
|
||||||
job.vehicle.v_paint_codes[key] !== null &&
|
|
||||||
job.vehicle.v_paint_codes[key] !== undefined
|
|
||||||
)
|
|
||||||
.map((key, idx) => (
|
|
||||||
<Tag key={idx}>{job.vehicle.v_paint_codes[key]}</Tag>
|
|
||||||
))}
|
|
||||||
</span>
|
|
||||||
</DataLabel>
|
</DataLabel>
|
||||||
)}
|
<DataLabel key="3" label={t("owners.fields.address")}>
|
||||||
</div>
|
{`${job.ownr_addr1 || ""} ${job.ownr_addr2 || ""} ${
|
||||||
</Card>
|
job.ownr_city || ""
|
||||||
</Col>
|
} ${job.ownr_st || ""} ${job.ownr_zip || ""}`}
|
||||||
<Col {...colSpan}>
|
</DataLabel>
|
||||||
<Card
|
<DataLabel key="4" label={t("owners.fields.ownr_ea")}>
|
||||||
style={{ height: "100%" }}
|
{disabled ? (
|
||||||
title={<span id="job-employee-assignments-title">{t("jobs.labels.employeeassignments")}</span>}
|
<>{job.ownr_ea || ""}</>
|
||||||
id={"job-employee-assignments"}
|
) : job.ownr_ea ? (
|
||||||
>
|
<a href={`mailto:${job.ownr_ea}`}>{job.ownr_ea}</a>
|
||||||
<div>
|
) : null}
|
||||||
<JobEmployeeAssignments job={job} />
|
</DataLabel>
|
||||||
<Divider style={{ margin: ".5rem" }} />
|
{job.owner?.tax_number && (
|
||||||
<DataLabel label={t("jobs.labels.labor_hrs")}>
|
<DataLabel key="5" label={t("owners.fields.tax_number")}>
|
||||||
{bodyHrs.toFixed(1)} / {refinishHrs.toFixed(1)} / {(bodyHrs + refinishHrs).toFixed(1)}
|
{job.owner?.tax_number || ""}
|
||||||
</DataLabel>
|
</DataLabel>
|
||||||
</div>
|
)}
|
||||||
</Card>
|
<DataLabel label={t("owners.fields.note")} valueStyle={{ overflow: "hidden", textOverflow: "ellipsis" }}>
|
||||||
</Col>
|
{job.owner?.note || ""}
|
||||||
</Row>
|
</DataLabel>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col {...colSpan}>
|
||||||
|
<Card
|
||||||
|
style={{ height: "100%" }}
|
||||||
|
title={
|
||||||
|
job.vehicle ? (
|
||||||
|
disabled ? (
|
||||||
|
<>{vehicleTitle.length > 0 ? vehicleTitle : t("vehicles.labels.novehinfo")} </>
|
||||||
|
) : (
|
||||||
|
<Link to={job.vehicle && `/manage/vehicles/${job.vehicle.id}`}>
|
||||||
|
{vehicleTitle.length > 0 ? vehicleTitle : t("vehicles.labels.novehinfo")}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<span></span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<DataLabel key="2" label={t("vehicles.fields.plate_no")}>
|
||||||
|
{`${job.plate_no || t("general.labels.na")} (${`${job.plate_st || t("general.labels.na")}`})`}
|
||||||
|
</DataLabel>
|
||||||
|
<DataLabel key="4" label={t("vehicles.fields.v_vin")}>
|
||||||
|
<VehicleVinDisplay>{`${job.v_vin || t("general.labels.na")}`}</VehicleVinDisplay>
|
||||||
|
{bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid ? (
|
||||||
|
job.v_vin?.length !== 17 ? (
|
||||||
|
<WarningFilled style={{ color: "tomato", marginLeft: ".3rem" }} />
|
||||||
|
) : null
|
||||||
|
) : null}
|
||||||
|
</DataLabel>
|
||||||
|
<DataLabel label={t("jobs.fields.regie_number")}>{job.regie_number || t("general.labels.na")}</DataLabel>
|
||||||
|
<DataLabel label={t("jobs.labels.relatedros")}>
|
||||||
|
<JobsRelatedRos jobid={job.id} job={job} disabled={disabled} />
|
||||||
|
</DataLabel>
|
||||||
|
{job.vehicle && job.vehicle.notes && (
|
||||||
|
<DataLabel
|
||||||
|
label={t("vehicles.fields.notes")}
|
||||||
|
valueStyle={{ whiteSpace: "pre-wrap" }}
|
||||||
|
valueClassName={notesClamped ? "clamp" : ""}
|
||||||
|
onValueClick={() => setNotesClamped(!notesClamped)}
|
||||||
|
>
|
||||||
|
{job.vehicle.notes}
|
||||||
|
</DataLabel>
|
||||||
|
)}
|
||||||
|
{job.vehicle && job.vehicle.v_paint_codes && (
|
||||||
|
<DataLabel label={t("vehicles.fields.v_paint_codes", { number: "" })}>
|
||||||
|
<span style={{ whiteSpace: "pre" }}>
|
||||||
|
{Object.keys(job.vehicle.v_paint_codes)
|
||||||
|
.filter(
|
||||||
|
(key) =>
|
||||||
|
job.vehicle.v_paint_codes[key] !== "" &&
|
||||||
|
job.vehicle.v_paint_codes[key] !== null &&
|
||||||
|
job.vehicle.v_paint_codes[key] !== undefined
|
||||||
|
)
|
||||||
|
.map((key, idx) => (
|
||||||
|
<Tag key={idx}>{job.vehicle.v_paint_codes[key]}</Tag>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
</DataLabel>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col {...colSpan}>
|
||||||
|
<Card
|
||||||
|
style={{ height: "100%" }}
|
||||||
|
title={<span id="job-employee-assignments-title">{t("jobs.labels.employeeassignments")}</span>}
|
||||||
|
id={"job-employee-assignments"}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<JobEmployeeAssignments job={job} />
|
||||||
|
<Divider style={{ margin: ".5rem" }} />
|
||||||
|
<DataLabel label={t("jobs.labels.labor_hrs")}>
|
||||||
|
{bodyHrs.toFixed(1)} / {refinishHrs.toFixed(1)} / {(bodyHrs + refinishHrs).toFixed(1)}
|
||||||
|
</DataLabel>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<PinnedJobNotes job={job} />
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,8 +46,8 @@ function JobsDocumentsImgproxyComponent({
|
|||||||
const [modalState, setModalState] = useState({ open: false, index: 0 });
|
const [modalState, setModalState] = useState({ open: false, index: 0 });
|
||||||
|
|
||||||
const fetchThumbnails = useCallback(() => {
|
const fetchThumbnails = useCallback(() => {
|
||||||
fetchImgproxyThumbnails({ setStateCallback: setGalleryImages, jobId });
|
fetchImgproxyThumbnails({ setStateCallback: setGalleryImages, jobId, billId });
|
||||||
}, [jobId, setGalleryImages]);
|
}, [jobId, billId, setGalleryImages]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
@@ -208,8 +208,8 @@ function JobsDocumentsImgproxyComponent({
|
|||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(JobsDocumentsImgproxyComponent);
|
export default connect(mapStateToProps, mapDispatchToProps)(JobsDocumentsImgproxyComponent);
|
||||||
|
|
||||||
export const fetchImgproxyThumbnails = async ({ setStateCallback, jobId, imagesOnly }) => {
|
export const fetchImgproxyThumbnails = async ({ setStateCallback, jobId, billId, imagesOnly }) => {
|
||||||
const result = await axios.post("/media/imgproxy/thumbnails", { jobid: jobId });
|
const result = await axios.post("/media/imgproxy/thumbnails", { jobid: jobId, billid: billId });
|
||||||
const documents = result.data.reduce(
|
const documents = result.data.reduce(
|
||||||
(acc, value) => {
|
(acc, value) => {
|
||||||
if (value.type.startsWith("image")) {
|
if (value.type.startsWith("image")) {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import useLocalStorage from "../../utils/useLocalStorage";
|
|||||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
import NoteUpsertModal from "../note-upsert-modal/note-upsert-modal.container";
|
import NoteUpsertModal from "../note-upsert-modal/note-upsert-modal.container";
|
||||||
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
|
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
|
||||||
|
import JobNotesPinToggle from "../job-notes-pin-toggle/job-notes-pin-toggle.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
jobRO: selectJobReadOnly
|
jobRO: selectJobReadOnly
|
||||||
@@ -47,6 +48,9 @@ export function JobNotesComponent({
|
|||||||
key: "icons",
|
key: "icons",
|
||||||
width: 80,
|
width: 80,
|
||||||
filteredValue: filter?.icons || null,
|
filteredValue: filter?.icons || null,
|
||||||
|
defaultSortOrder: "desc",
|
||||||
|
multiple: 1,
|
||||||
|
sorter: (a, b) => a.pinned - b.pinned,
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
text: t("notes.labels.usernotes"),
|
text: t("notes.labels.usernotes"),
|
||||||
@@ -63,6 +67,7 @@ export function JobNotesComponent({
|
|||||||
{record.critical ? <WarningFilled style={{ margin: 4, color: "red" }} /> : null}
|
{record.critical ? <WarningFilled style={{ margin: 4, color: "red" }} /> : null}
|
||||||
{record.private ? <EyeInvisibleFilled style={{ margin: 4 }} /> : null}
|
{record.private ? <EyeInvisibleFilled style={{ margin: 4 }} /> : null}
|
||||||
{record.audit ? <AuditOutlined style={{ margin: 4 }} /> : null}
|
{record.audit ? <AuditOutlined style={{ margin: 4 }} /> : null}
|
||||||
|
<JobNotesPinToggle note={record} />
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -100,6 +105,7 @@ export function JobNotesComponent({
|
|||||||
dataIndex: "updated_at",
|
dataIndex: "updated_at",
|
||||||
key: "updated_at",
|
key: "updated_at",
|
||||||
defaultSortOrder: "descend",
|
defaultSortOrder: "descend",
|
||||||
|
multiple: 2,
|
||||||
width: 200,
|
width: 200,
|
||||||
sorter: (a, b) => new Date(a.updated_at) - new Date(b.updated_at),
|
sorter: (a, b) => new Date(a.updated_at) - new Date(b.updated_at),
|
||||||
render: (text, record) => <DateTimeFormatter>{record.updated_at}</DateTimeFormatter>
|
render: (text, record) => <DateTimeFormatter>{record.updated_at}</DateTimeFormatter>
|
||||||
|
|||||||
@@ -23,17 +23,22 @@ export function NoteUpsertModalComponent({ form, noteUpsertModal }) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]}>
|
||||||
<Col span={8}>
|
<Col span={6}>
|
||||||
<Form.Item label={t("notes.fields.critical")} name="critical" valuePropName="checked">
|
<Form.Item label={t("notes.fields.critical")} name="critical" valuePropName="checked">
|
||||||
<Switch />
|
<Switch />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={8}>
|
<Col span={6}>
|
||||||
<Form.Item label={t("notes.fields.private")} name="private" valuePropName="checked">
|
<Form.Item label={t("notes.fields.private")} name="private" valuePropName="checked">
|
||||||
<Switch />
|
<Switch />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={8}>
|
<Col span={6}>
|
||||||
|
<Form.Item label={t("notes.fields.pinned")} name="pinned" valuePropName="checked">
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={6}>
|
||||||
<Form.Item label={t("notes.fields.type")} name="type" initialValue="general">
|
<Form.Item label={t("notes.fields.type")} name="type" initialValue="general">
|
||||||
<Select
|
<Select
|
||||||
options={[
|
options={[
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { useMutation } from "@apollo/client";
|
import { useMutation } from "@apollo/client";
|
||||||
import { Form, Modal } from "antd";
|
import { Form, Modal } from "antd";
|
||||||
import React, { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
|
import { GET_JOB_BY_PK } from "../../graphql/jobs.queries.js";
|
||||||
import { INSERT_NEW_NOTE, UPDATE_NOTE } from "../../graphql/notes.queries";
|
import { INSERT_NEW_NOTE, UPDATE_NOTE } from "../../graphql/notes.queries";
|
||||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||||
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
||||||
@@ -12,7 +14,6 @@ import { selectNoteUpsert } from "../../redux/modals/modals.selectors";
|
|||||||
import { selectCurrentUser } from "../../redux/user/user.selectors";
|
import { selectCurrentUser } from "../../redux/user/user.selectors";
|
||||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||||
import NoteUpsertModalComponent from "./note-upsert-modal.component";
|
import NoteUpsertModalComponent from "./note-upsert-modal.component";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
currentUser: selectCurrentUser,
|
currentUser: selectCurrentUser,
|
||||||
@@ -65,8 +66,9 @@ export function NoteUpsertModalContainer({ currentUser, noteUpsertModal, toggleM
|
|||||||
variables: {
|
variables: {
|
||||||
noteId: existingNote.id,
|
noteId: existingNote.id,
|
||||||
note: values
|
note: values
|
||||||
}
|
},
|
||||||
}).then((r) => {
|
refetchQueries: ["GET_JOB_BY_PK", "QUERY_JOB_CARD_DETAILS", "QUERY_PARTS_QUEUE_CARD_DETAILS"]
|
||||||
|
}).then(() => {
|
||||||
notification["success"]({
|
notification["success"]({
|
||||||
message: t("notes.successes.updated")
|
message: t("notes.successes.updated")
|
||||||
});
|
});
|
||||||
@@ -86,6 +88,33 @@ export function NoteUpsertModalContainer({ currentUser, noteUpsertModal, toggleM
|
|||||||
variables: {
|
variables: {
|
||||||
noteInput: [{ ...values, jobid: jobId, created_by: currentUser.email }]
|
noteInput: [{ ...values, jobid: jobId, created_by: currentUser.email }]
|
||||||
},
|
},
|
||||||
|
update(cache, { data: { updateNote: updatedNote } }) {
|
||||||
|
try {
|
||||||
|
const existingJob = cache.readQuery({
|
||||||
|
query: GET_JOB_BY_PK,
|
||||||
|
variables: { id: jobId }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingJob) {
|
||||||
|
cache.writeQuery({
|
||||||
|
query: GET_JOB_BY_PK,
|
||||||
|
variables: { id: jobId },
|
||||||
|
data: {
|
||||||
|
...existingJob,
|
||||||
|
job: {
|
||||||
|
...existingJob.job,
|
||||||
|
notes: updatedNote.pinned
|
||||||
|
? [updatedNote, ...existingJob.job.notes]
|
||||||
|
: existingJob.job.notes.filter((n) => n.id !== updatedNote.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Cache miss is okay, query hasn't been executed yet
|
||||||
|
console.log("Cache miss for GET_JOB_BY_PK");
|
||||||
|
}
|
||||||
|
},
|
||||||
refetchQueries: ["QUERY_NOTES_BY_JOB_PK"]
|
refetchQueries: ["QUERY_NOTES_BY_JOB_PK"]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
right: 0;
|
right: 0;
|
||||||
width: 400px;
|
width: 400px;
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
background: #fff;
|
background: var(--notification-bg);
|
||||||
color: rgba(0, 0, 0, 0.85);
|
color: var(--notification-text);
|
||||||
border: 1px solid #d9d9d9;
|
border: 1px solid var(--notification-border);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08), 0 3px 6px rgba(0, 0, 0, 0.06);
|
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08), 0 3px 6px rgba(0, 0, 0, 0.06);
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
@@ -19,23 +19,22 @@
|
|||||||
|
|
||||||
.notification-header {
|
.notification-header {
|
||||||
padding: 4px 16px;
|
padding: 4px 16px;
|
||||||
border-bottom: 1px solid #f0f0f0;
|
border-bottom: 1px solid var(--notification-header-border);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: #fafafa;
|
background: var(--notification-header-bg);
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: rgba(0, 0, 0, 0.85);
|
color: var(--notification-header-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification-controls {
|
.notification-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|
||||||
// Styles for the eye icon and switch (custom classes)
|
// Styles for the eye icon and switch (custom classes)
|
||||||
.notification-toggle {
|
.notification-toggle {
|
||||||
align-items: center; // Ensure vertical alignment
|
align-items: center; // Ensure vertical alignment
|
||||||
@@ -43,7 +42,7 @@
|
|||||||
|
|
||||||
.notification-toggle-icon {
|
.notification-toggle-icon {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #1677ff;
|
color: var(--notification-toggle-icon);
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,7 +58,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.ant-switch-checked {
|
&.ant-switch-checked {
|
||||||
background-color: #1677ff;
|
background-color: var(--notification-switch-bg);
|
||||||
|
|
||||||
.ant-switch-handle {
|
.ant-switch-handle {
|
||||||
left: calc(100% - 14px);
|
left: calc(100% - 14px);
|
||||||
}
|
}
|
||||||
@@ -70,37 +70,37 @@
|
|||||||
// Styles for the "Mark All Read" button (restore original link button style)
|
// Styles for the "Mark All Read" button (restore original link button style)
|
||||||
.ant-btn-link {
|
.ant-btn-link {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
color: #1677ff;
|
color: var(--notification-btn-link);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: #69b1ff;
|
color: var(--notification-btn-link-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:disabled {
|
&:disabled {
|
||||||
color: rgba(0, 0, 0, 0.25);
|
color: var(--notification-btn-link-disabled);
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
color: #0958d9;
|
color: var(--notification-btn-link-active);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification-read {
|
.notification-read {
|
||||||
background: #fff;
|
background: var(--notification-read-bg);
|
||||||
color: rgba(0, 0, 0, 0.65);
|
color: var(--notification-read-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification-unread {
|
.notification-unread {
|
||||||
background: #f5f5f5;
|
background: var(--notification-unread-bg);
|
||||||
color: rgba(0, 0, 0, 0.85);
|
color: var(--notification-unread-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification-item {
|
.notification-item {
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
border-bottom: 1px solid #f0f0f0;
|
border-bottom: 1px solid var(--notification-header-border);
|
||||||
display: block;
|
display: block;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -108,7 +108,7 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: #fafafa;
|
background: var(--notification-item-hover-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification-content {
|
.notification-content {
|
||||||
@@ -125,7 +125,7 @@
|
|||||||
|
|
||||||
.ro-number {
|
.ro-number {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #1677ff;
|
color: var(--notification-ro-number);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
@@ -133,7 +133,7 @@
|
|||||||
.relative-time {
|
.relative-time {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: rgba(0, 0, 0, 0.45);
|
color: var(--notification-relative-time);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
@@ -164,12 +164,12 @@
|
|||||||
|
|
||||||
.ant-alert {
|
.ant-alert {
|
||||||
margin: 8px;
|
margin: 8px;
|
||||||
background: #fff1f0;
|
background: var(--alert-bg);
|
||||||
color: rgba(0, 0, 0, 0.85);
|
color: var(--alert-text);
|
||||||
border: 1px solid #ffa39e;
|
border: 1px solid var(--alert-border);
|
||||||
|
|
||||||
.ant-alert-message {
|
.ant-alert-message {
|
||||||
color: #ff4d4f;
|
color: var(--alert-message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ import { useNotification } from "../../contexts/Notifications/notificationContex
|
|||||||
|
|
||||||
const { confirm } = Modal;
|
const { confirm } = Modal;
|
||||||
|
|
||||||
const openNotificationWithIcon = (type, t, notification) => {
|
const openNotificationWithIcon = (type, t, notification, message) => {
|
||||||
notification[type]({
|
notification[type]({
|
||||||
message: t("job_payments.notifications.error.title"),
|
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) {
|
if (refundResponse.data.status < 0) {
|
||||||
openNotificationWithIcon("error", t, notification);
|
openNotificationWithIcon("error", t, notification, refundResponse.data.message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { Card, Divider, Space } from "antd";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import JobNotesPinToggle from "../job-notes-pin-toggle/job-notes-pin-toggle.component";
|
||||||
|
|
||||||
|
function PinnedJobNotes({ job }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const pinnedNotes = useMemo(() => {
|
||||||
|
return job?.notes?.filter((note) => note.pinned); //This will be typically filtered, but adding this to maximize flexibility of the component.
|
||||||
|
}, [job.notes]);
|
||||||
|
|
||||||
|
return pinnedNotes?.length ? (
|
||||||
|
<>
|
||||||
|
<Divider />
|
||||||
|
<Space direction="vertical" style={{ width: "100%" }}>
|
||||||
|
{pinnedNotes?.map((note) => (
|
||||||
|
<Card
|
||||||
|
key={note.id}
|
||||||
|
title={`${t("notes.labels.pinned_note")} - ${t(`notes.fields.types.${note.type}`)}`}
|
||||||
|
extra={<JobNotesPinToggle note={note} />}
|
||||||
|
>
|
||||||
|
{note.text}
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</Space>
|
||||||
|
</>
|
||||||
|
) : null;
|
||||||
|
}
|
||||||
|
export default PinnedJobNotes;
|
||||||
@@ -1,26 +1,29 @@
|
|||||||
import { Col, List, Space, Typography } from "antd";
|
import { Col, List, Space, Typography } from "antd";
|
||||||
import React from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
const CardColorLegend = ({ bodyshop }) => {
|
const CardColorLegend = ({ bodyshop }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const data = bodyshop.ssbuckets.map((bucket) => {
|
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) {
|
if (bucket.color) {
|
||||||
color = bucket.color;
|
color = bucket.color;
|
||||||
|
|
||||||
if (bucket.color.rgb) {
|
if (bucket.color.rgb) {
|
||||||
color = bucket.color.rgb;
|
color = { ...bucket.color.rgb, a: bucket.color.a || 1 };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
label: bucket.label,
|
label: bucket.label,
|
||||||
color
|
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 (
|
return (
|
||||||
<Col>
|
<Col>
|
||||||
<Typography>{t("production.labels.legend")}</Typography>
|
<Typography>{t("production.labels.legend")}</Typography>
|
||||||
@@ -36,7 +39,7 @@ const CardColorLegend = ({ bodyshop }) => {
|
|||||||
style={{
|
style={{
|
||||||
width: "1.5rem",
|
width: "1.5rem",
|
||||||
aspectRatio: "1/1",
|
aspectRatio: "1/1",
|
||||||
backgroundColor: `rgba(${item.color.r},${item.color.g},${item.color.b},${item.color.a})`
|
backgroundColor: getBackgroundColor(item.color)
|
||||||
}}
|
}}
|
||||||
></div>
|
></div>
|
||||||
<div>{item.label}</div>
|
<div>{item.label}</div>
|
||||||
|
|||||||
@@ -11,13 +11,10 @@ import React, { useMemo } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||||
|
|
||||||
import ProductionAlert from "../production-list-columns/production-list-columns.alert.component";
|
import ProductionAlert from "../production-list-columns/production-list-columns.alert.component";
|
||||||
import ProductionListColumnProductionNote from "../production-list-columns/production-list-columns.productionnote.component";
|
import ProductionListColumnProductionNote from "../production-list-columns/production-list-columns.productionnote.component";
|
||||||
import ProductionSubletsManageComponent from "../production-sublets-manage/production-sublets-manage.component";
|
import ProductionSubletsManageComponent from "../production-sublets-manage/production-sublets-manage.component";
|
||||||
|
|
||||||
import dayjs from "../../utils/day";
|
import dayjs from "../../utils/day";
|
||||||
|
|
||||||
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
|
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
|
||||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||||
import ShareToTeamsButton from "../share-to-teams/share-to-teams.component.jsx";
|
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 cardColor = (ssbuckets, totalHrs) => {
|
||||||
const bucket = ssbuckets.find((bucket) => bucket.gte <= totalHrs && (!bucket.lt || bucket.lt > 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) =>
|
const getContrastYIQ = (bgColor, isDarkMode = document.documentElement.getAttribute("data-theme") === "dark") => {
|
||||||
(bgColor.r * 299 + bgColor.g * 587 + bgColor.b * 114) / 1000 >= 128 ? "black" : "white";
|
// 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);
|
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 }) =>
|
const OwnerNameToolTip = ({ metadata, cardSettings }) =>
|
||||||
cardSettings?.ownr_nm && (
|
cardSettings?.ownr_nm && (
|
||||||
<Col span={24}>
|
<Col span={24}>
|
||||||
@@ -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();
|
const dineroAmount = Dinero(metadata?.job_totals?.totals?.subtotal ?? Dinero()).toFormat();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
cardSettings?.subtotal && (
|
cardSettings?.subtotal && (
|
||||||
<Col span={cardSettings.compact ? 24 : 12}>
|
<Col span={cardSettings.compact ? 24 : 12}>
|
||||||
@@ -300,12 +312,10 @@ const TasksToolTip = ({ metadata, cardSettings, t }) =>
|
|||||||
</Col>
|
</Col>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default function ProductionBoardCard({ technician, card, bodyshop, cardSettings, clone }) {
|
export default function ProductionBoardCard({ technician, card, bodyshop, cardSettings }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { metadata } = card;
|
const { metadata } = card;
|
||||||
|
|
||||||
const employees = useMemo(() => bodyshop.employees, [bodyshop.employees]);
|
const employees = useMemo(() => bodyshop.employees, [bodyshop.employees]);
|
||||||
|
|
||||||
const { employee_body, employee_prep, employee_refinish, employee_csr } = useMemo(() => {
|
const { employee_body, employee_prep, employee_refinish, employee_csr } = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
employee_body: metadata?.employee_body && findEmployeeById(employees, metadata.employee_body),
|
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)
|
employee_csr: metadata?.employee_csr && findEmployeeById(employees, metadata.employee_csr)
|
||||||
};
|
};
|
||||||
}, [metadata, employees]);
|
}, [metadata, employees]);
|
||||||
|
|
||||||
const pastDueAlert = useMemo(() => {
|
const pastDueAlert = useMemo(() => {
|
||||||
if (!metadata?.scheduled_completion) return null;
|
if (!metadata?.scheduled_completion) return null;
|
||||||
const completionDate = dayjs(metadata.scheduled_completion);
|
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";
|
if (dayjs().add(1, "day").isSame(completionDate, "day")) return "production-completion-soon";
|
||||||
return null;
|
return null;
|
||||||
}, [metadata?.scheduled_completion]);
|
}, [metadata?.scheduled_completion]);
|
||||||
|
|
||||||
const totalHrs = useMemo(() => {
|
const totalHrs = useMemo(() => {
|
||||||
return metadata?.labhrs && metadata?.larhrs
|
return metadata?.labhrs && metadata?.larhrs
|
||||||
? metadata.labhrs.aggregate.sum.mod_lb_hrs + metadata.larhrs.aggregate.sum.mod_lb_hrs
|
? metadata.labhrs.aggregate.sum.mod_lb_hrs + metadata.larhrs.aggregate.sum.mod_lb_hrs
|
||||||
: 0;
|
: 0;
|
||||||
}, [metadata?.labhrs, metadata?.larhrs]);
|
}, [metadata?.labhrs, metadata?.larhrs]);
|
||||||
|
|
||||||
const bgColor = useMemo(() => cardColor(bodyshop.ssbuckets, totalHrs), [bodyshop.ssbuckets, totalHrs]);
|
const bgColor = useMemo(() => cardColor(bodyshop.ssbuckets, totalHrs), [bodyshop.ssbuckets, totalHrs]);
|
||||||
const contrastYIQ = useMemo(() => getContrastYIQ(bgColor), [bgColor]);
|
const contrastYIQ = useMemo(() => getContrastYIQ(bgColor), [bgColor]);
|
||||||
|
|
||||||
const isBodyEmpty = useMemo(() => {
|
const isBodyEmpty = useMemo(() => {
|
||||||
return !(
|
return !(
|
||||||
cardSettings?.ownr_nm ||
|
cardSettings?.ownr_nm ||
|
||||||
@@ -413,8 +419,10 @@ export default function ProductionBoardCard({ technician, card, bodyshop, cardSe
|
|||||||
className={`react-trello-card ${cardSettings.kiosk ? "kiosk-mode" : ""}`}
|
className={`react-trello-card ${cardSettings.kiosk ? "kiosk-mode" : ""}`}
|
||||||
size="small"
|
size="small"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: cardSettings?.cardcolor && `rgba(${bgColor.r},${bgColor.g},${bgColor.b},${bgColor.a})`,
|
backgroundColor: cardSettings?.cardcolor
|
||||||
color: cardSettings?.cardcolor && contrastYIQ
|
? 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}
|
title={!isBodyEmpty ? headerContent : null}
|
||||||
extra={
|
extra={
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.share-to-teams-badge {
|
.share-to-teams-badge {
|
||||||
background-color: #cccccc;
|
background-color: var(--share-badge-bg);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
.react-trello-column-header {
|
.react-trello-column-header {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background-color: #d0d0d0;
|
background-color: var(--column-header-bg);
|
||||||
border-radius: 5px 5px 0 0;
|
border-radius: 5px 5px 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,13 +31,14 @@
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-trello-footer {
|
.react-trello-footer {
|
||||||
background-color: #d0d0d0;
|
background-color: var(--footer-bg);
|
||||||
border-radius: 0 0 5px 5px;
|
border-radius: 0 0 5px 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-item {
|
.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 {
|
.lane-title {
|
||||||
@@ -53,27 +54,33 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
.body-empty-container {
|
.body-empty-container {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tech-container {
|
.tech-container {
|
||||||
font-weight: bolder;
|
font-weight: bolder;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
||||||
.branches-outlined {
|
.branches-outlined {
|
||||||
color: orangered;
|
color: var(--tech-icon-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.inner-container {
|
.inner-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|
||||||
.circle-outline {
|
.circle-outline {
|
||||||
color: orangered;
|
color: var(--tech-icon-color);
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.iou-parent {
|
.iou-parent {
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
@@ -81,6 +88,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.clone.is-dragging .ant-card {
|
.clone.is-dragging .ant-card {
|
||||||
border: #1890ff 2px solid !important;
|
border: 2px solid var(--clone-border-color) !important;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export const StyleHorizontal = styled.div`
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 1px;
|
min-height: 1px;
|
||||||
overflow-y: visible;
|
overflow-y: visible;
|
||||||
overflow-x: visible; // change this line
|
overflow-x: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-trello-lane.lane-collapsed {
|
.react-trello-lane.lane-collapsed {
|
||||||
@@ -85,17 +85,17 @@ export const StyleHorizontal = styled.div`
|
|||||||
|
|
||||||
.react-trello-card {
|
.react-trello-card {
|
||||||
height: auto;
|
height: auto;
|
||||||
margin: 2px;
|
margin: 2px 0 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.size-memory-wrapper {
|
.size-memory-wrapper {
|
||||||
display: flex; /* This makes it a flex container */
|
display: flex;
|
||||||
flex-direction: column; /* Aligns children vertically */
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.size-memory-wrapper .ant-card {
|
.size-memory-wrapper .ant-card {
|
||||||
flex-grow: 1; /* Allows the card to expand to fill the available space */
|
flex-grow: 1;
|
||||||
width: 100%; /* Ensures the card stretches to fill the width of its parent */
|
width: 100%;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -131,7 +131,7 @@ export const StyleVertical = styled.div`
|
|||||||
|
|
||||||
.grid-item {
|
.grid-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: ${(props) => props.gridItemWidth}; /* Use props to set width */
|
width: ${(props) => props.gridItemWidth};
|
||||||
align-content: stretch;
|
align-content: stretch;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
@@ -148,13 +148,13 @@ export const StyleVertical = styled.div`
|
|||||||
}
|
}
|
||||||
|
|
||||||
.size-memory-wrapper {
|
.size-memory-wrapper {
|
||||||
display: flex; /* This makes it a flex container */
|
display: flex;
|
||||||
flex-direction: column; /* Aligns children vertically */
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.size-memory-wrapper .ant-card {
|
.size-memory-wrapper .ant-card {
|
||||||
flex-grow: 1; /* Allows the card to expand to fill the available space */
|
flex-grow: 1;
|
||||||
width: 100%; /* Ensures the card stretches to fill the width of its parent */
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-trello-lane .lane-collapsed {
|
.react-trello-lane .lane-collapsed {
|
||||||
@@ -163,7 +163,7 @@ export const StyleVertical = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const BoardWrapper = styled.div`
|
export const BoardWrapper = styled.div`
|
||||||
color: #393939;
|
color: var(--board-text-color);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
@@ -171,7 +171,7 @@ export const BoardWrapper = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const Section = styled.section`
|
export const Section = styled.section`
|
||||||
background-color: #e3e3e3;
|
background-color: var(--section-bg);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
margin: 2px 2px;
|
margin: 2px 2px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -197,6 +197,6 @@ export const ScrollableLane = styled.div`
|
|||||||
|
|
||||||
export const Detail = styled.div`
|
export const Detail = styled.div`
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #4d4d4d;
|
color: var(--detail-text-color);
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
export function ProductionListTable({ loading, data, refetch, bodyshop, technician, currentUser }) {
|
export function ProductionListTable({ loading, data, refetch, bodyshop, technician, currentUser }) {
|
||||||
const [searchText, setSearchText] = useState("");
|
const [searchText, setSearchText] = useState("");
|
||||||
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
treatments: { Production_List_Status_Colors, Enhanced_Payroll }
|
treatments: { Production_List_Status_Colors, Enhanced_Payroll }
|
||||||
} = useSplitTreatments({
|
} = useSplitTreatments({
|
||||||
@@ -36,10 +35,8 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
|
|||||||
names: ["Production_List_Status_Colors", "Enhanced_Payroll"],
|
names: ["Production_List_Status_Colors", "Enhanced_Payroll"],
|
||||||
splitKey: bodyshop.imexshopid
|
splitKey: bodyshop.imexshopid
|
||||||
});
|
});
|
||||||
|
|
||||||
const assoc = bodyshop.associations.find((a) => a.useremail === currentUser.email);
|
const assoc = bodyshop.associations.find((a) => a.useremail === currentUser.email);
|
||||||
const defaultView = assoc && assoc.default_prod_list_view;
|
const defaultView = assoc && assoc.default_prod_list_view;
|
||||||
|
|
||||||
const initialStateRef = useRef(
|
const initialStateRef = useRef(
|
||||||
(bodyshop.production_config &&
|
(bodyshop.production_config &&
|
||||||
bodyshop.production_config.find((p) => p.name === defaultView)?.columns.tableState) ||
|
bodyshop.production_config.find((p) => p.name === defaultView)?.columns.tableState) ||
|
||||||
@@ -48,7 +45,6 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
|
|||||||
filteredInfo: { text: "" }
|
filteredInfo: { text: "" }
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const initialColumnsRef = useRef(
|
const initialColumnsRef = useRef(
|
||||||
(initialStateRef.current &&
|
(initialStateRef.current &&
|
||||||
bodyshop?.production_config
|
bodyshop?.production_config
|
||||||
@@ -69,12 +65,9 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
|
|||||||
})) ||
|
})) ||
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const [state, setState] = useState(initialStateRef.current);
|
const [state, setState] = useState(initialStateRef.current);
|
||||||
const [columns, setColumns] = useState(initialColumnsRef.current);
|
const [columns, setColumns] = useState(initialColumnsRef.current);
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const matchingColumnConfig = useMemo(() => {
|
const matchingColumnConfig = useMemo(() => {
|
||||||
return bodyshop?.production_config?.find((p) => p.name === defaultView);
|
return bodyshop?.production_config?.find((p) => p.name === defaultView);
|
||||||
}, [bodyshop.production_config, defaultView]);
|
}, [bodyshop.production_config, defaultView]);
|
||||||
@@ -95,7 +88,6 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
|
|||||||
width: k.width ?? 100
|
width: k.width ?? 100
|
||||||
};
|
};
|
||||||
}) || [];
|
}) || [];
|
||||||
|
|
||||||
// Only update columns if they haven't been manually changed by the user
|
// Only update columns if they haven't been manually changed by the user
|
||||||
if (_.isEqual(initialColumnsRef.current, columns)) {
|
if (_.isEqual(initialColumnsRef.current, columns)) {
|
||||||
setColumns(newColumns);
|
setColumns(newColumns);
|
||||||
@@ -126,11 +118,9 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
|
|||||||
|
|
||||||
const onDragEnd = (fromIndex, toIndex) => {
|
const onDragEnd = (fromIndex, toIndex) => {
|
||||||
if (fromIndex === toIndex) return;
|
if (fromIndex === toIndex) return;
|
||||||
|
|
||||||
const columnsCopy = [...columns];
|
const columnsCopy = [...columns];
|
||||||
const [movedItem] = columnsCopy.splice(fromIndex, 1);
|
const [movedItem] = columnsCopy.splice(fromIndex, 1);
|
||||||
columnsCopy.splice(toIndex, 0, movedItem);
|
columnsCopy.splice(toIndex, 0, movedItem);
|
||||||
|
|
||||||
if (!_.isEqual(columnsCopy, columns)) {
|
if (!_.isEqual(columnsCopy, columns)) {
|
||||||
setColumns(columnsCopy);
|
setColumns(columnsCopy);
|
||||||
setHasUnsavedChanges(true);
|
setHasUnsavedChanges(true);
|
||||||
@@ -140,7 +130,6 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
|
|||||||
const removeColumn = (e) => {
|
const removeColumn = (e) => {
|
||||||
const { key } = e;
|
const { key } = e;
|
||||||
const newColumns = columns.filter((i) => i.key !== key);
|
const newColumns = columns.filter((i) => i.key !== key);
|
||||||
|
|
||||||
if (!_.isEqual(newColumns, columns)) {
|
if (!_.isEqual(newColumns, columns)) {
|
||||||
setColumns(newColumns);
|
setColumns(newColumns);
|
||||||
setHasUnsavedChanges(true);
|
setHasUnsavedChanges(true);
|
||||||
@@ -155,7 +144,6 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
|
|||||||
...nextColumns[index],
|
...nextColumns[index],
|
||||||
width: size.width
|
width: size.width
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!_.isEqual(nextColumns, columns)) {
|
if (!_.isEqual(nextColumns, columns)) {
|
||||||
setColumns(nextColumns);
|
setColumns(nextColumns);
|
||||||
setHasUnsavedChanges(true);
|
setHasUnsavedChanges(true);
|
||||||
@@ -180,7 +168,6 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown className="prod-header-dropdown" menu={menu} trigger={["contextMenu"]}>
|
<Dropdown className="prod-header-dropdown" menu={menu} trigger={["contextMenu"]}>
|
||||||
<span>{col.title}</span>
|
<span>{col.title}</span>
|
||||||
@@ -206,13 +193,12 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
|
|||||||
item.v_model_desc,
|
item.v_model_desc,
|
||||||
item.v_make_desc
|
item.v_make_desc
|
||||||
];
|
];
|
||||||
|
|
||||||
return fieldsToSearch.some((field) => (field || "").toString().toLowerCase().includes(searchText.toLowerCase()));
|
return fieldsToSearch.some((field) => (field || "").toString().toLowerCase().includes(searchText.toLowerCase()));
|
||||||
};
|
};
|
||||||
|
|
||||||
const dataSource = searchText === "" ? data : data.filter((j) => filterData(j, searchText));
|
const dataSource = searchText === "" ? data : data.filter((j) => filterData(j, searchText));
|
||||||
|
|
||||||
if (!!!columns) return <div>No columns found.</div>;
|
if (!columns) return <div>No columns found.</div>;
|
||||||
|
|
||||||
const totalHrs = data
|
const totalHrs = data
|
||||||
.reduce(
|
.reduce(
|
||||||
@@ -236,7 +222,8 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
|
|||||||
onClick={resetChanges}
|
onClick={resetChanges}
|
||||||
style={{
|
style={{
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
textDecoration: "underline"
|
textDecoration: "underline",
|
||||||
|
color: "var(--reset-link-color)"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("general.actions.reset")}
|
{t("general.actions.reset")}
|
||||||
@@ -269,7 +256,6 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
|
|||||||
data={data}
|
data={data}
|
||||||
onColumnAdd={addColumn}
|
onColumnAdd={addColumn}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ProductionListConfigManager
|
<ProductionListConfigManager
|
||||||
columns={columns}
|
columns={columns}
|
||||||
setColumns={setColumns}
|
setColumns={setColumns}
|
||||||
@@ -305,24 +291,22 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
|
|||||||
{...(Production_List_Status_Colors.treatment === "on" && {
|
{...(Production_List_Status_Colors.treatment === "on" && {
|
||||||
onRow: (record, index) => {
|
onRow: (record, index) => {
|
||||||
if (!bodyshop.md_ro_statuses.production_colors) return null;
|
if (!bodyshop.md_ro_statuses.production_colors) return null;
|
||||||
|
|
||||||
const color = bodyshop.md_ro_statuses.production_colors.find((x) => x.status === record.status);
|
const color = bodyshop.md_ro_statuses.production_colors.find((x) => x.status === record.status);
|
||||||
|
|
||||||
if (!color) {
|
if (!color) {
|
||||||
if (index % 2 === 0)
|
if (index % 2 === 0)
|
||||||
return {
|
return {
|
||||||
style: {
|
style: {
|
||||||
backgroundColor: `rgb(236, 236, 236)`
|
backgroundColor: "var(--table-row-even-bg)"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
className: "rowWithColor",
|
className: "rowWithColor",
|
||||||
style: {
|
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)"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ const ret = {
|
|||||||
"shop:dashboard": 3,
|
"shop:dashboard": 3,
|
||||||
"shop:rbac": 5,
|
"shop:rbac": 5,
|
||||||
"shop:reportcenter": 2,
|
"shop:reportcenter": 2,
|
||||||
|
"shop:responsibilitycenter": 4, // Updated from "shop:responsibility" to "shop:responsibilitycenter"
|
||||||
"shop:templates": 4,
|
"shop:templates": 4,
|
||||||
"shop:vendors": 2,
|
"shop:vendors": 2,
|
||||||
|
|
||||||
|
|||||||
@@ -212,6 +212,10 @@ export function ScheduleCalendarHeaderComponent({ bodyshop, label, refetch, date
|
|||||||
return bodyshop.workingdays[day];
|
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 (
|
return (
|
||||||
<div className="imex-calendar-load">
|
<div className="imex-calendar-load">
|
||||||
<ScheduleBlockDay alreadyBlocked={isDayBlocked.length > 0} date={date} refetch={refetch}>
|
<ScheduleBlockDay alreadyBlocked={isDayBlocked.length > 0} date={date} refetch={refetch}>
|
||||||
|
|||||||
@@ -19,11 +19,42 @@
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
.imex-event-arrived {
|
.imex-event-arrived {
|
||||||
background-color: rgba(4, 141, 4, 0.4);
|
background-color: var(--event-arrived-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.imex-event-block {
|
.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 {
|
.rbc-month-view {
|
||||||
@@ -31,12 +62,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.rbc-event.rbc-selected {
|
.rbc-event.rbc-selected {
|
||||||
background-color: slategrey;
|
background-color: var(--event-selected-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.imex-calendar-load {
|
.imex-calendar-load {
|
||||||
max-width: 12rem;
|
max-width: 12rem;
|
||||||
position: relative;
|
position: relative;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translate(-50%);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,16 +36,40 @@ export function ScheduleCalendarWrapperComponent({
|
|||||||
const search = queryString.parse(useLocation().search);
|
const search = queryString.parse(useLocation().search);
|
||||||
const history = useNavigate();
|
const history = useNavigate();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
// Determine current view to compute styles consistently
|
||||||
|
const currentView = search.view || defaultView || "week";
|
||||||
|
|
||||||
const handleEventPropStyles = (event, start, end, isSelected) => {
|
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 {
|
return {
|
||||||
...(event.color && !((search.view || defaultView) === "agenda")
|
...(bg ? { style: { backgroundColor: bg } } : {}),
|
||||||
? {
|
className: classes
|
||||||
style: {
|
|
||||||
backgroundColor: event.color && event.color.hex ? event.color.hex : event.color
|
|
||||||
}
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
className: `${event.arrived ? "imex-event-arrived" : ""} ${event.block ? "imex-event-block" : ""}`
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -60,7 +84,9 @@ export function ScheduleCalendarWrapperComponent({
|
|||||||
<Collapse style={{ marginBottom: "5px" }}>
|
<Collapse style={{ marginBottom: "5px" }}>
|
||||||
<Collapse.Panel
|
<Collapse.Panel
|
||||||
key="1"
|
key="1"
|
||||||
header={<span style={{ color: "tomato" }}>{t("appointments.labels.severalerrorsfound")}</span>}
|
header={
|
||||||
|
<span style={{ color: "var(--error-header-text)" }}>{t("appointments.labels.severalerrorsfound")}</span>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Space direction="vertical" style={{ width: "100%" }}>
|
<Space direction="vertical" style={{ width: "100%" }}>
|
||||||
{problemJobs.map((problem) => (
|
{problemJobs.map((problem) => (
|
||||||
@@ -70,7 +96,7 @@ export function ScheduleCalendarWrapperComponent({
|
|||||||
message={
|
message={
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey="appointments.labels.dataconsistency"
|
i18nKey="appointments.labels.dataconsistency"
|
||||||
components={[<Link to={`/manage/jobs/${problem.id}`} target="_blank" />]}
|
components={[<Link key={problem.id} to={`/manage/jobs/${problem.id}`} target="_blank" />]}
|
||||||
values={{
|
values={{
|
||||||
ro_number: problem.ro_number,
|
ro_number: problem.ro_number,
|
||||||
code: problem.code
|
code: problem.code
|
||||||
@@ -91,7 +117,7 @@ export function ScheduleCalendarWrapperComponent({
|
|||||||
message={
|
message={
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey="appointments.labels.dataconsistency"
|
i18nKey="appointments.labels.dataconsistency"
|
||||||
components={[<Link to={`/manage/jobs/${problem.id}`} target="_blank" />]}
|
components={[<Link key={problem.id} to={`/manage/jobs/${problem.id}`} target="_blank" />]}
|
||||||
values={{
|
values={{
|
||||||
ro_number: problem.ro_number,
|
ro_number: problem.ro_number,
|
||||||
code: problem.code
|
code: problem.code
|
||||||
@@ -102,12 +128,11 @@ export function ScheduleCalendarWrapperComponent({
|
|||||||
))}
|
))}
|
||||||
</Space>
|
</Space>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<Calendar
|
<Calendar
|
||||||
events={data}
|
events={data}
|
||||||
defaultView={search.view || defaultView || "week"}
|
defaultView={search.view || defaultView || "week"}
|
||||||
date={selectedDate}
|
date={selectedDate}
|
||||||
onNavigate={(date, view, action) => {
|
onNavigate={(date) => {
|
||||||
search.date = date.toISOString().substr(0, 10);
|
search.date = date.toISOString().substr(0, 10);
|
||||||
history({ search: queryString.stringify(search) });
|
history({ search: queryString.stringify(search) });
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { SyncOutlined } from "@ant-design/icons";
|
|||||||
import { Button, Card, Checkbox, Col, Row, Select, Space } from "antd";
|
import { Button, Card, Checkbox, Col, Row, Select, Space } from "antd";
|
||||||
import { PageHeader } from "@ant-design/pro-layout";
|
import { PageHeader } from "@ant-design/pro-layout";
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
import React, { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import useLocalStorage from "../../utils/useLocalStorage";
|
import useLocalStorage from "../../utils/useLocalStorage";
|
||||||
import ScheduleAtsSummary from "../schedule-ats-summary/schedule-ats-summary.component";
|
import ScheduleAtsSummary from "../schedule-ats-summary/schedule-ats-summary.component";
|
||||||
import ScheduleCalendarWrapperComponent from "../schedule-calendar-wrapper/scheduler-calendar-wrapper.component";
|
import ScheduleCalendarWrapperComponent from "../schedule-calendar-wrapper/scheduler-calendar-wrapper.component";
|
||||||
@@ -18,7 +18,7 @@ import _ from "lodash";
|
|||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = () => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
});
|
});
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleCalendarComponent);
|
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleCalendarComponent);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useQuery } from "@apollo/client";
|
import { useQuery } from "@apollo/client";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import React, { useEffect, useMemo } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
import { QUERY_ALL_ACTIVE_APPOINTMENTS } from "../../graphql/appointments.queries";
|
import { QUERY_ALL_ACTIVE_APPOINTMENTS } from "../../graphql/appointments.queries";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
@@ -32,7 +32,7 @@ export function ScheduleCalendarContainer({ calculateScheduleLoad }) {
|
|||||||
startd: range.start,
|
startd: range.start,
|
||||||
endd: range.end
|
endd: range.end
|
||||||
},
|
},
|
||||||
skip: !!!range.start || !!!range.end,
|
skip: !range.start || !range.end,
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
nextFetchPolicy: "network-only"
|
nextFetchPolicy: "network-only"
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,26 +5,25 @@ const CustomTooltip = ({ active, payload, label }) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: "white",
|
backgroundColor: "var(--tooltip-bg)",
|
||||||
border: "1px solid gray",
|
border: "1px solid var(--tooltip-border)",
|
||||||
padding: "0.5rem"
|
padding: "0.5rem"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<p style={{ margin: "0" }}>{label}</p>
|
<p style={{ margin: "0" }}>{label}</p>
|
||||||
{payload.map((data, index) => {
|
{payload.map((data, index) => {
|
||||||
|
const textColor = data.color || "var(--tooltip-text-fallback)";
|
||||||
if (data.dataKey === "sales" || data.dataKey === "accSales")
|
if (data.dataKey === "sales" || data.dataKey === "accSales")
|
||||||
return (
|
return (
|
||||||
<p style={{ margin: "10px 0", color: data.color }} key={index}>{`${data.name} : ${Dinero({
|
<p style={{ margin: "10px 0", color: textColor }} key={index}>{`${data.name} : ${Dinero({
|
||||||
amount: Math.round(data.value * 100)
|
amount: Math.round(data.value * 100)
|
||||||
}).toFormat()}`}</p>
|
}).toFormat()}`}</p>
|
||||||
);
|
);
|
||||||
|
return <p style={{ margin: "10px 0", color: textColor }} key={index}>{`${data.name} : ${data.value}`}</p>;
|
||||||
return <p style={{ margin: "10px 0", color: data.color }} key={index}>{`${data.name} : ${data.value}`}</p>;
|
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,15 +3,16 @@ const CustomTooltip = ({ active, payload, label }) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: "white",
|
backgroundColor: "var(--tooltip-bg)",
|
||||||
border: "1px solid gray",
|
border: "1px solid var(--tooltip-border)",
|
||||||
padding: "0.5rem"
|
padding: "0.5rem"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<p style={{ margin: "0" }}>{label}</p>
|
<p style={{ margin: "0" }}>{label}</p>
|
||||||
{payload.map((data, index) => {
|
{payload.map((data, index) => {
|
||||||
|
const textColor = data.color || "var(--tooltip-text-fallback)";
|
||||||
return (
|
return (
|
||||||
<p style={{ margin: "10px 0", color: data.color }} key={index}>{`${
|
<p style={{ margin: "10px 0", color: textColor }} key={index}>{`${
|
||||||
data.name
|
data.name
|
||||||
} : ${data.value.toFixed(1)}`}</p>
|
} : ${data.value.toFixed(1)}`}</p>
|
||||||
);
|
);
|
||||||
@@ -19,7 +20,6 @@ const CustomTooltip = ({ active, payload, label }) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ const ShareToTeamsComponent = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const currentUrl =
|
const currentUrl =
|
||||||
urlOverride ||
|
urlOverride ||
|
||||||
encodeURIComponent(`${window.location.origin}${location.pathname}${location.search}${location.hash}`);
|
encodeURIComponent(`${window.location.origin}${location.pathname}${location.search}${location.hash}`);
|
||||||
@@ -49,31 +48,24 @@ const ShareToTeamsComponent = ({
|
|||||||
pageTitleOverride ||
|
pageTitleOverride ||
|
||||||
encodeURIComponent(typeof document !== "undefined" ? document.title : t("general.actions.sharetoteams"));
|
encodeURIComponent(typeof document !== "undefined" ? document.title : t("general.actions.sharetoteams"));
|
||||||
const messageText = messageTextOverride || encodeURIComponent(t("general.actions.sharetoteams"));
|
const messageText = messageTextOverride || encodeURIComponent(t("general.actions.sharetoteams"));
|
||||||
|
|
||||||
// Construct the Teams share URL with parameters
|
// Construct the Teams share URL with parameters
|
||||||
const teamsShareUrl = `https://teams.microsoft.com/share?href=${currentUrl}&preText=${messageText}&title=${pageTitle}`;
|
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
|
// Function to open the centered share link in a new window/tab
|
||||||
const handleShare = () => {
|
const handleShare = () => {
|
||||||
const screenWidth = window.screen.width;
|
const screenWidth = window.screen.width;
|
||||||
const screenHeight = window.screen.height;
|
const screenHeight = window.screen.height;
|
||||||
const windowWidth = 600;
|
const windowWidth = 600;
|
||||||
const windowHeight = 400;
|
const windowHeight = 400;
|
||||||
|
|
||||||
const left = screenWidth / 2 - windowWidth / 2;
|
const left = screenWidth / 2 - windowWidth / 2;
|
||||||
const top = screenHeight / 2 - windowHeight / 2;
|
const top = screenHeight / 2 - windowHeight / 2;
|
||||||
|
|
||||||
const windowFeatures = `width=${windowWidth},height=${windowHeight},left=${left},top=${top}`;
|
const windowFeatures = `width=${windowWidth},height=${windowHeight},left=${left},top=${top}`;
|
||||||
|
|
||||||
// noinspection JSIgnoredPromiseFromCall
|
// noinspection JSIgnoredPromiseFromCall
|
||||||
window.open(teamsShareUrl, "_blank", windowFeatures);
|
window.open(teamsShareUrl, "_blank", windowFeatures);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Feature is disabled
|
// Feature is disabled
|
||||||
if (!bodyshop?.md_functionality_toggles?.teams) {
|
if (!bodyshop?.md_functionality_toggles?.teams) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (noIcon) {
|
if (noIcon) {
|
||||||
return (
|
return (
|
||||||
<div style={{ cursor: "pointer", ...noIconStyle }} onClick={handleShare}>
|
<div style={{ cursor: "pointer", ...noIconStyle }} onClick={handleShare}>
|
||||||
@@ -81,16 +73,15 @@ const ShareToTeamsComponent = ({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: "#6264A7",
|
backgroundColor: "var(--teams-button-bg)",
|
||||||
borderColor: "#6264A7",
|
borderColor: "var(--teams-button-border)",
|
||||||
color: "#FFFFFF",
|
color: "var(--teams-button-text)",
|
||||||
...buttonStyle
|
...buttonStyle
|
||||||
}}
|
}}
|
||||||
icon={<PiMicrosoftTeamsLogo style={{ color: "#FFFFFF", ...buttonIconStyle }} />}
|
icon={<PiMicrosoftTeamsLogo style={{ color: "var(--teams-button-text)", ...buttonIconStyle }} />}
|
||||||
onClick={handleShare}
|
onClick={handleShare}
|
||||||
title={linkText === null ? t("general.actions.sharetoteams") : linkText}
|
title={linkText === null ? t("general.actions.sharetoteams") : linkText}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import { useMutation, useQuery } from "@apollo/client";
|
import { useMutation, useQuery } from "@apollo/client";
|
||||||
import { Form } from "antd";
|
import { Form } from "antd";
|
||||||
import dayjs from "../../utils/day";
|
import { useEffect, useState } from "react";
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import { QUERY_BODYSHOP, UPDATE_SHOP } from "../../graphql/bodyshop.queries";
|
import { QUERY_BODYSHOP, UPDATE_SHOP } from "../../graphql/bodyshop.queries";
|
||||||
|
import dayjs from "../../utils/day";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
import FormsFieldChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
|
import FormsFieldChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
|
||||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||||
import ShopInfoComponent from "./shop-info.component";
|
import ShopInfoComponent from "./shop-info.component";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
import { FEATURE_CONFIGS, useFormDataPreservation } from "./useFormDataPreservation";
|
||||||
|
|
||||||
export default function ShopInfoContainer() {
|
export default function ShopInfoContainer() {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
@@ -22,16 +23,24 @@ export default function ShopInfoContainer() {
|
|||||||
});
|
});
|
||||||
const notification = useNotification();
|
const notification = useNotification();
|
||||||
|
|
||||||
const handleFinish = (values) => {
|
const combinedFeatureConfig = {
|
||||||
|
...FEATURE_CONFIGS.general,
|
||||||
|
...FEATURE_CONFIGS.responsibilitycenters
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use form data preservation for all shop-info features
|
||||||
|
const { createSubmissionHandler } = useFormDataPreservation(form, data?.bodyshops[0], combinedFeatureConfig);
|
||||||
|
|
||||||
|
const handleFinish = createSubmissionHandler((values) => {
|
||||||
setSaveLoading(true);
|
setSaveLoading(true);
|
||||||
logImEXEvent("shop_update");
|
logImEXEvent("shop_update");
|
||||||
|
|
||||||
updateBodyshop({
|
updateBodyshop({
|
||||||
variables: { id: data.bodyshops[0].id, shop: values }
|
variables: { id: data.bodyshops[0].id, shop: values }
|
||||||
})
|
})
|
||||||
.then((r) => {
|
.then(() => {
|
||||||
notification["success"]({ message: t("bodyshop.successes.save") });
|
notification["success"]({ message: t("bodyshop.successes.save") });
|
||||||
refetch().then((_) => form.resetFields());
|
refetch().then(() => form.resetFields());
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
notification["error"]({
|
notification["error"]({
|
||||||
@@ -39,7 +48,7 @@ export default function ShopInfoContainer() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
setSaveLoading(false);
|
setSaveLoading(false);
|
||||||
};
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) form.resetFields();
|
if (data) form.resetFields();
|
||||||
|
|||||||
@@ -145,124 +145,168 @@ export function ShopInfoGeneral({ form, bodyshop }) {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
<LayoutFormRow header={t("bodyshop.labels.accountingsetup")} id="accountingsetup">
|
<LayoutFormRow header={t("bodyshop.labels.accountingsetup")} id="accountingsetup">
|
||||||
{HasFeatureAccess({ featureName: "export", bodyshop }) && (
|
{[
|
||||||
<>
|
...(HasFeatureAccess({ featureName: "export", bodyshop })
|
||||||
<Form.Item label={t("bodyshop.labels.qbo")} valuePropName="checked" name={["accountingconfig", "qbo"]}>
|
? [
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
{InstanceRenderManager({
|
|
||||||
imex: (
|
|
||||||
<Form.Item shouldUpdate noStyle>
|
|
||||||
{() => (
|
|
||||||
<Form.Item
|
|
||||||
label={t("bodyshop.labels.qbo_usa")}
|
|
||||||
shouldUpdate
|
|
||||||
valuePropName="checked"
|
|
||||||
name={["accountingconfig", "qbo_usa"]}
|
|
||||||
>
|
|
||||||
<Switch disabled={!form.getFieldValue(["accountingconfig", "qbo"])} />
|
|
||||||
</Form.Item>
|
|
||||||
)}
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
<Form.Item label={t("bodyshop.labels.qbo_departmentid")} name={["accountingconfig", "qbo_departmentid"]}>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("bodyshop.labels.accountingtiers")}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
name={["accountingconfig", "tiers"]}
|
|
||||||
>
|
|
||||||
<Radio.Group>
|
|
||||||
<Radio value={2}>2</Radio>
|
|
||||||
<Radio value={3}>3</Radio>
|
|
||||||
</Radio.Group>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item shouldUpdate>
|
|
||||||
{() => {
|
|
||||||
return (
|
|
||||||
<Form.Item
|
|
||||||
label={t("bodyshop.labels.2tiersetup")}
|
|
||||||
shouldUpdate
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: form.getFieldValue(["accountingconfig", "tiers"]) === 2
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
name={["accountingconfig", "twotierpref"]}
|
|
||||||
>
|
|
||||||
<Radio.Group disabled={form.getFieldValue(["accountingconfig", "tiers"]) === 3}>
|
|
||||||
<Radio value="name">{t("bodyshop.labels.2tiername")}</Radio>
|
|
||||||
<Radio value="source">{t("bodyshop.labels.2tiersource")}</Radio>
|
|
||||||
</Radio.Group>
|
|
||||||
</Form.Item>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("bodyshop.labels.printlater")}
|
|
||||||
valuePropName="checked"
|
|
||||||
name={["accountingconfig", "printlater"]}
|
|
||||||
>
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("bodyshop.labels.emaillater")}
|
|
||||||
valuePropName="checked"
|
|
||||||
name={["accountingconfig", "emaillater"]}
|
|
||||||
>
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<Form.Item
|
|
||||||
label={t("bodyshop.fields.inhousevendorid")}
|
|
||||||
name={"inhousevendorid"}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("bodyshop.fields.default_adjustment_rate")}
|
|
||||||
name={"default_adjustment_rate"}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<InputNumber min={0} precision={2} />
|
|
||||||
</Form.Item>
|
|
||||||
{InstanceRenderManager({
|
|
||||||
imex: (
|
|
||||||
<Form.Item label={t("bodyshop.fields.federal_tax_id")} name="federal_tax_id">
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
<Form.Item label={t("bodyshop.fields.state_tax_id")} name="state_tax_id">
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
{HasFeatureAccess({ featureName: "bills", bodyshop }) && (
|
|
||||||
<>
|
|
||||||
{InstanceRenderManager({
|
|
||||||
imex: (
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("bodyshop.fields.invoice_federal_tax_rate")}
|
key="qbo"
|
||||||
name={["bill_tax_rates", "federal_tax_rate"]}
|
label={t("bodyshop.labels.qbo")}
|
||||||
|
valuePropName="checked"
|
||||||
|
name={["accountingconfig", "qbo"]}
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>,
|
||||||
|
InstanceRenderManager({
|
||||||
|
imex: (
|
||||||
|
<Form.Item key="qbo_usa_wrapper" shouldUpdate noStyle>
|
||||||
|
{() => (
|
||||||
|
<Form.Item
|
||||||
|
label={t("bodyshop.labels.qbo_usa")}
|
||||||
|
shouldUpdate
|
||||||
|
valuePropName="checked"
|
||||||
|
name={["accountingconfig", "qbo_usa"]}
|
||||||
|
>
|
||||||
|
<Switch disabled={!form.getFieldValue(["accountingconfig", "qbo"])} />
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
<Form.Item
|
||||||
|
key="qbo_departmentid"
|
||||||
|
label={t("bodyshop.labels.qbo_departmentid")}
|
||||||
|
name={["accountingconfig", "qbo_departmentid"]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>,
|
||||||
|
<Form.Item
|
||||||
|
key="accountingtiers"
|
||||||
|
label={t("bodyshop.labels.accountingtiers")}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
name={["accountingconfig", "tiers"]}
|
||||||
|
>
|
||||||
|
<Radio.Group>
|
||||||
|
<Radio value={2}>2</Radio>
|
||||||
|
<Radio value={3}>3</Radio>
|
||||||
|
</Radio.Group>
|
||||||
|
</Form.Item>,
|
||||||
|
<Form.Item key="twotierpref_wrapper" shouldUpdate>
|
||||||
|
{() => {
|
||||||
|
return (
|
||||||
|
<Form.Item
|
||||||
|
label={t("bodyshop.labels.2tiersetup")}
|
||||||
|
shouldUpdate
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: form.getFieldValue(["accountingconfig", "tiers"]) === 2
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
name={["accountingconfig", "twotierpref"]}
|
||||||
|
>
|
||||||
|
<Radio.Group disabled={form.getFieldValue(["accountingconfig", "tiers"]) === 3}>
|
||||||
|
<Radio value="name">{t("bodyshop.labels.2tiername")}</Radio>
|
||||||
|
<Radio value="source">{t("bodyshop.labels.2tiersource")}</Radio>
|
||||||
|
</Radio.Group>
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.Item>,
|
||||||
|
<Form.Item
|
||||||
|
key="printlater"
|
||||||
|
label={t("bodyshop.labels.printlater")}
|
||||||
|
valuePropName="checked"
|
||||||
|
name={["accountingconfig", "printlater"]}
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>,
|
||||||
|
<Form.Item
|
||||||
|
key="emaillater"
|
||||||
|
label={t("bodyshop.labels.emaillater")}
|
||||||
|
valuePropName="checked"
|
||||||
|
name={["accountingconfig", "emaillater"]}
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
<Form.Item
|
||||||
|
key="inhousevendorid"
|
||||||
|
label={t("bodyshop.fields.inhousevendorid")}
|
||||||
|
name={"inhousevendorid"}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>,
|
||||||
|
<Form.Item
|
||||||
|
key="default_adjustment_rate"
|
||||||
|
label={t("bodyshop.fields.default_adjustment_rate")}
|
||||||
|
name={"default_adjustment_rate"}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<InputNumber min={0} precision={2} />
|
||||||
|
</Form.Item>,
|
||||||
|
InstanceRenderManager({
|
||||||
|
imex: (
|
||||||
|
<Form.Item key="federal_tax_id" label={t("bodyshop.fields.federal_tax_id")} name="federal_tax_id">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
<Form.Item key="state_tax_id" label={t("bodyshop.fields.state_tax_id")} name="state_tax_id">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>,
|
||||||
|
...(HasFeatureAccess({ featureName: "bills", bodyshop })
|
||||||
|
? [
|
||||||
|
InstanceRenderManager({
|
||||||
|
imex: (
|
||||||
|
<Form.Item
|
||||||
|
key="invoice_federal_tax_rate"
|
||||||
|
label={t("bodyshop.fields.invoice_federal_tax_rate")}
|
||||||
|
name={["bill_tax_rates", "federal_tax_rate"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<InputNumber />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
<Form.Item
|
||||||
|
key="invoice_state_tax_rate"
|
||||||
|
label={t("bodyshop.fields.invoice_state_tax_rate")}
|
||||||
|
name={["bill_tax_rates", "state_tax_rate"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<InputNumber />
|
||||||
|
</Form.Item>,
|
||||||
|
<Form.Item
|
||||||
|
key="invoice_local_tax_rate"
|
||||||
|
label={t("bodyshop.fields.invoice_local_tax_rate")}
|
||||||
|
name={["bill_tax_rates", "local_tax_rate"]}
|
||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true
|
required: true
|
||||||
@@ -272,117 +316,118 @@ export function ShopInfoGeneral({ form, bodyshop }) {
|
|||||||
>
|
>
|
||||||
<InputNumber />
|
<InputNumber />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)
|
]
|
||||||
})}
|
: []),
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("bodyshop.fields.invoice_state_tax_rate")}
|
key="md_payment_types"
|
||||||
name={["bill_tax_rates", "state_tax_rate"]}
|
name={["md_payment_types"]}
|
||||||
rules={[
|
label={t("bodyshop.fields.md_payment_types")}
|
||||||
{
|
rules={[
|
||||||
required: true
|
{
|
||||||
//message: t("general.validation.required"),
|
required: true,
|
||||||
}
|
//message: t("general.validation.required"),
|
||||||
]}
|
type: "array"
|
||||||
>
|
}
|
||||||
<InputNumber />
|
]}
|
||||||
</Form.Item>
|
>
|
||||||
<Form.Item
|
<Select mode="tags" />
|
||||||
label={t("bodyshop.fields.invoice_local_tax_rate")}
|
</Form.Item>,
|
||||||
name={["bill_tax_rates", "local_tax_rate"]}
|
<Form.Item
|
||||||
rules={[
|
key="md_categories"
|
||||||
{
|
name={["md_categories"]}
|
||||||
required: true
|
label={t("bodyshop.fields.md_categories")}
|
||||||
//message: t("general.validation.required"),
|
rules={[
|
||||||
}
|
{
|
||||||
]}
|
//message: t("general.validation.required"),
|
||||||
>
|
type: "array"
|
||||||
<InputNumber />
|
}
|
||||||
</Form.Item>
|
]}
|
||||||
</>
|
>
|
||||||
)}
|
<Select mode="tags" />
|
||||||
<Form.Item
|
</Form.Item>,
|
||||||
name={["md_payment_types"]}
|
...(HasFeatureAccess({ featureName: "export", bodyshop })
|
||||||
label={t("bodyshop.fields.md_payment_types")}
|
? [
|
||||||
rules={[
|
<Form.Item
|
||||||
{
|
key="ReceivableCustomField1"
|
||||||
required: true,
|
name={["accountingconfig", "ReceivableCustomField1"]}
|
||||||
//message: t("general.validation.required"),
|
label={t("bodyshop.fields.ReceivableCustomField", { number: 1 })}
|
||||||
type: "array"
|
>
|
||||||
}
|
{ReceivableCustomFieldSelect}
|
||||||
]}
|
</Form.Item>,
|
||||||
>
|
<Form.Item
|
||||||
<Select mode="tags" />
|
key="ReceivableCustomField2"
|
||||||
</Form.Item>
|
name={["accountingconfig", "ReceivableCustomField2"]}
|
||||||
<Form.Item
|
label={t("bodyshop.fields.ReceivableCustomField", { number: 2 })}
|
||||||
name={["md_categories"]}
|
>
|
||||||
label={t("bodyshop.fields.md_categories")}
|
{ReceivableCustomFieldSelect}
|
||||||
rules={[
|
</Form.Item>,
|
||||||
{
|
<Form.Item
|
||||||
//message: t("general.validation.required"),
|
key="ReceivableCustomField3"
|
||||||
type: "array"
|
name={["accountingconfig", "ReceivableCustomField3"]}
|
||||||
}
|
label={t("bodyshop.fields.ReceivableCustomField", { number: 3 })}
|
||||||
]}
|
>
|
||||||
>
|
{ReceivableCustomFieldSelect}
|
||||||
<Select mode="tags" />
|
</Form.Item>,
|
||||||
</Form.Item>
|
<Form.Item
|
||||||
{HasFeatureAccess({ featureName: "export", bodyshop }) && (
|
key="md_classes"
|
||||||
<>
|
name={["md_classes"]}
|
||||||
<Form.Item
|
label={t("bodyshop.fields.md_classes")}
|
||||||
name={["accountingconfig", "ReceivableCustomField1"]}
|
rules={[
|
||||||
label={t("bodyshop.fields.ReceivableCustomField", { number: 1 })}
|
({ getFieldValue }) => {
|
||||||
>
|
return {
|
||||||
{ReceivableCustomFieldSelect}
|
required: getFieldValue("enforce_class"),
|
||||||
</Form.Item>
|
//message: t("general.validation.required"),
|
||||||
<Form.Item
|
type: "array"
|
||||||
name={["accountingconfig", "ReceivableCustomField2"]}
|
};
|
||||||
label={t("bodyshop.fields.ReceivableCustomField", { number: 2 })}
|
}
|
||||||
>
|
]}
|
||||||
{ReceivableCustomFieldSelect}
|
>
|
||||||
</Form.Item>
|
<Select mode="tags" />
|
||||||
<Form.Item
|
</Form.Item>,
|
||||||
name={["accountingconfig", "ReceivableCustomField3"]}
|
<Form.Item
|
||||||
label={t("bodyshop.fields.ReceivableCustomField", { number: 3 })}
|
key="enforce_class"
|
||||||
>
|
name={["enforce_class"]}
|
||||||
{ReceivableCustomFieldSelect}
|
label={t("bodyshop.fields.enforce_class")}
|
||||||
</Form.Item>
|
valuePropName="checked"
|
||||||
<Form.Item
|
>
|
||||||
name={["md_classes"]}
|
<Switch />
|
||||||
label={t("bodyshop.fields.md_classes")}
|
</Form.Item>,
|
||||||
rules={[
|
...(ClosingPeriod.treatment === "on"
|
||||||
({ getFieldValue }) => {
|
? [
|
||||||
return {
|
<Form.Item
|
||||||
required: getFieldValue("enforce_class"),
|
key="ClosingPeriod"
|
||||||
//message: t("general.validation.required"),
|
name={["accountingconfig", "ClosingPeriod"]}
|
||||||
type: "array"
|
label={t("bodyshop.fields.closingperiod")} //{t("reportcenter.labels.dates")}
|
||||||
};
|
>
|
||||||
}
|
<DatePicker.RangePicker format="MM/DD/YYYY" presets={DatePickerRanges} />
|
||||||
]}
|
</Form.Item>
|
||||||
>
|
]
|
||||||
<Select mode="tags" />
|
: []),
|
||||||
</Form.Item>
|
...(ADPPayroll.treatment === "on"
|
||||||
<Form.Item name={["enforce_class"]} label={t("bodyshop.fields.enforce_class")} valuePropName="checked">
|
? [
|
||||||
<Switch />
|
<Form.Item
|
||||||
</Form.Item>
|
key="companyCode"
|
||||||
{ClosingPeriod.treatment === "on" && (
|
name={["accountingconfig", "companyCode"]}
|
||||||
<Form.Item
|
label={t("bodyshop.fields.companycode")}
|
||||||
name={["accountingconfig", "ClosingPeriod"]}
|
>
|
||||||
label={t("bodyshop.fields.closingperiod")} //{t("reportcenter.labels.dates")}
|
<Input />
|
||||||
>
|
</Form.Item>
|
||||||
<DatePicker.RangePicker format="MM/DD/YYYY" presets={DatePickerRanges} />
|
]
|
||||||
</Form.Item>
|
: []),
|
||||||
)}
|
...(ADPPayroll.treatment === "on"
|
||||||
{ADPPayroll.treatment === "on" && (
|
? [
|
||||||
<Form.Item name={["accountingconfig", "companyCode"]} label={t("bodyshop.fields.companycode")}>
|
<Form.Item
|
||||||
<Input />
|
key="batchID"
|
||||||
</Form.Item>
|
name={["accountingconfig", "batchID"]}
|
||||||
)}
|
label={t("bodyshop.fields.batchid")}
|
||||||
{ADPPayroll.treatment === "on" && (
|
>
|
||||||
<Form.Item name={["accountingconfig", "batchID"]} label={t("bodyshop.fields.batchid")}>
|
<Input />
|
||||||
<Input />
|
</Form.Item>
|
||||||
</Form.Item>
|
]
|
||||||
)}
|
: [])
|
||||||
</>
|
]
|
||||||
)}
|
: [])
|
||||||
|
]}
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
<FeatureWrapper featureName="scoreboard" noauth={() => null}>
|
<FeatureWrapper featureName="scoreboard" noauth={() => null}>
|
||||||
<LayoutFormRow header={t("bodyshop.labels.scoreboardsetup")} id="scoreboardsetup">
|
<LayoutFormRow header={t("bodyshop.labels.scoreboardsetup")} id="scoreboardsetup">
|
||||||
@@ -446,211 +491,255 @@ export function ShopInfoGeneral({ form, bodyshop }) {
|
|||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
</FeatureWrapper>
|
</FeatureWrapper>
|
||||||
<LayoutFormRow header={t("bodyshop.labels.systemsettings")} id="systemsettings">
|
<LayoutFormRow header={t("bodyshop.labels.systemsettings")} id="systemsettings">
|
||||||
<Form.Item
|
{[
|
||||||
name={["md_referral_sources"]}
|
<Form.Item
|
||||||
label={t("bodyshop.fields.md_referral_sources")}
|
key="md_referral_sources"
|
||||||
rules={[
|
name={["md_referral_sources"]}
|
||||||
{
|
label={t("bodyshop.fields.md_referral_sources")}
|
||||||
required: true,
|
rules={[
|
||||||
//message: t("general.validation.required"),
|
{
|
||||||
type: "array"
|
required: true,
|
||||||
}
|
//message: t("general.validation.required"),
|
||||||
]}
|
type: "array"
|
||||||
>
|
|
||||||
<Select mode="tags" />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item name={["enforce_referral"]} label={t("bodyshop.fields.enforce_referral")} valuePropName="checked">
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
name={["enforce_conversion_csr"]}
|
|
||||||
label={t("bodyshop.fields.enforce_conversion_csr")}
|
|
||||||
valuePropName="checked"
|
|
||||||
>
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
name={["enforce_conversion_category"]}
|
|
||||||
label={t("bodyshop.fields.enforce_conversion_category")}
|
|
||||||
valuePropName="checked"
|
|
||||||
>
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
name={["target_touchtime"]}
|
|
||||||
label={t("bodyshop.fields.target_touchtime")}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<InputNumber min={0.1} precision={1} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label={t("bodyshop.fields.use_fippa")} name={["use_fippa"]} valuePropName="checked">
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("bodyshop.fields.md_hour_split.prep")}
|
|
||||||
name={["md_hour_split", "prep"]}
|
|
||||||
dependencies={[["md_hour_split", "paint"]]}
|
|
||||||
rules={[
|
|
||||||
({ getFieldValue }) => ({
|
|
||||||
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"));
|
|
||||||
}
|
}
|
||||||
})
|
]}
|
||||||
]}
|
>
|
||||||
>
|
<Select mode="tags" />
|
||||||
<InputNumber min={0} max={1} precision={2} />
|
</Form.Item>,
|
||||||
</Form.Item>
|
<Form.Item
|
||||||
<Form.Item
|
key="enforce_referral"
|
||||||
label={t("bodyshop.fields.md_hour_split.paint")}
|
name={["enforce_referral"]}
|
||||||
name={["md_hour_split", "paint"]}
|
label={t("bodyshop.fields.enforce_referral")}
|
||||||
dependencies={[["md_hour_split", "prep"]]}
|
valuePropName="checked"
|
||||||
rules={[
|
>
|
||||||
({ getFieldValue }) => ({
|
<Switch />
|
||||||
validator(rule, value) {
|
</Form.Item>,
|
||||||
if (!value && !getFieldValue(["md_hour_split", "paint"])) {
|
<Form.Item
|
||||||
return Promise.resolve();
|
key="enforce_conversion_csr"
|
||||||
}
|
name={["enforce_conversion_csr"]}
|
||||||
if (value + getFieldValue(["md_hour_split", "prep"]) === 1) {
|
label={t("bodyshop.fields.enforce_conversion_csr")}
|
||||||
return Promise.resolve();
|
valuePropName="checked"
|
||||||
}
|
>
|
||||||
return Promise.reject(t("bodyshop.validation.larsplit"));
|
<Switch />
|
||||||
|
</Form.Item>,
|
||||||
|
<Form.Item
|
||||||
|
key="enforce_conversion_category"
|
||||||
|
name={["enforce_conversion_category"]}
|
||||||
|
label={t("bodyshop.fields.enforce_conversion_category")}
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>,
|
||||||
|
<Form.Item
|
||||||
|
key="target_touchtime"
|
||||||
|
name={["target_touchtime"]}
|
||||||
|
label={t("bodyshop.fields.target_touchtime")}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
}
|
}
|
||||||
})
|
]}
|
||||||
]}
|
>
|
||||||
>
|
<InputNumber min={0.1} precision={1} />
|
||||||
<InputNumber min={0} max={1} precision={2} />
|
</Form.Item>,
|
||||||
</Form.Item>
|
<Form.Item key="use_fippa" label={t("bodyshop.fields.use_fippa")} name={["use_fippa"]} valuePropName="checked">
|
||||||
<Form.Item label={t("bodyshop.fields.jc_hourly_rates.mapa")} name={["jc_hourly_rates", "mapa"]}>
|
<Switch />
|
||||||
<CurrencyInput />
|
</Form.Item>,
|
||||||
</Form.Item>
|
<Form.Item
|
||||||
<Form.Item label={t("bodyshop.fields.jc_hourly_rates.mash")} name={["jc_hourly_rates", "mash"]}>
|
key="md_hour_split_prep"
|
||||||
<CurrencyInput />
|
label={t("bodyshop.fields.md_hour_split.prep")}
|
||||||
</Form.Item>
|
name={["md_hour_split", "prep"]}
|
||||||
<Form.Item
|
dependencies={[["md_hour_split", "paint"]]}
|
||||||
name={["use_paint_scale_data"]}
|
rules={[
|
||||||
label={t("bodyshop.fields.use_paint_scale_data")}
|
({ getFieldValue }) => ({
|
||||||
valuePropName="checked"
|
validator(rule, value) {
|
||||||
>
|
if (!value && !getFieldValue(["md_hour_split", "paint"])) {
|
||||||
<Switch />
|
return Promise.resolve();
|
||||||
</Form.Item>
|
}
|
||||||
<Form.Item
|
if (value + getFieldValue(["md_hour_split", "paint"]) === 1) {
|
||||||
name={["attach_pdf_to_email"]}
|
return Promise.resolve();
|
||||||
label={t("bodyshop.fields.attach_pdf_to_email")}
|
}
|
||||||
valuePropName="checked"
|
return Promise.reject(t("bodyshop.validation.larsplit"));
|
||||||
>
|
}
|
||||||
<Switch />
|
})
|
||||||
</Form.Item>
|
]}
|
||||||
<Form.Item
|
>
|
||||||
name={["md_from_emails"]}
|
<InputNumber min={0} max={1} precision={2} />
|
||||||
label={t("bodyshop.fields.md_from_emails")}
|
</Form.Item>,
|
||||||
// rules={[
|
<Form.Item
|
||||||
// {
|
key="md_hour_split_paint"
|
||||||
// //message: t("general.validation.required"),
|
label={t("bodyshop.fields.md_hour_split.paint")}
|
||||||
// type: "array",
|
name={["md_hour_split", "paint"]}
|
||||||
// },
|
dependencies={[["md_hour_split", "prep"]]}
|
||||||
// ]}
|
rules={[
|
||||||
>
|
({ getFieldValue }) => ({
|
||||||
<Select mode="tags" />
|
validator(rule, value) {
|
||||||
</Form.Item>
|
if (!value && !getFieldValue(["md_hour_split", "paint"])) {
|
||||||
<Form.Item
|
return Promise.resolve();
|
||||||
name={["md_email_cc", "parts_order"]}
|
}
|
||||||
label={t("bodyshop.fields.md_email_cc", { template: "parts_orders" })}
|
if (value + getFieldValue(["md_hour_split", "prep"]) === 1) {
|
||||||
rules={[
|
return Promise.resolve();
|
||||||
{
|
}
|
||||||
//message: t("general.validation.required"),
|
return Promise.reject(t("bodyshop.validation.larsplit"));
|
||||||
type: "array"
|
}
|
||||||
}
|
})
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Select mode="tags" />
|
<InputNumber min={0} max={1} precision={2} />
|
||||||
</Form.Item>
|
</Form.Item>,
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name={["md_email_cc", "parts_return_slip"]}
|
key="jc_hourly_rates_mapa"
|
||||||
label={t("bodyshop.fields.md_email_cc", { template: "parts_returns" })}
|
label={t("bodyshop.fields.jc_hourly_rates.mapa")}
|
||||||
rules={[
|
name={["jc_hourly_rates", "mapa"]}
|
||||||
{
|
>
|
||||||
//message: t("general.validation.required"),
|
<CurrencyInput />
|
||||||
type: "array"
|
</Form.Item>,
|
||||||
}
|
<Form.Item
|
||||||
]}
|
key="jc_hourly_rates_mash"
|
||||||
>
|
label={t("bodyshop.fields.jc_hourly_rates.mash")}
|
||||||
<Select mode="tags" />
|
name={["jc_hourly_rates", "mash"]}
|
||||||
</Form.Item>
|
>
|
||||||
|
<CurrencyInput />
|
||||||
{HasFeatureAccess({ featureName: "timetickets", bodyshop }) && (
|
</Form.Item>,
|
||||||
<>
|
<Form.Item
|
||||||
<Form.Item
|
key="use_paint_scale_data"
|
||||||
name={["tt_allow_post_to_invoiced"]}
|
name={["use_paint_scale_data"]}
|
||||||
label={t("bodyshop.fields.tt_allow_post_to_invoiced")}
|
label={t("bodyshop.fields.use_paint_scale_data")}
|
||||||
valuePropName="checked"
|
valuePropName="checked"
|
||||||
>
|
>
|
||||||
<Switch />
|
<Switch />
|
||||||
</Form.Item>
|
</Form.Item>,
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name={["tt_enforce_hours_for_tech_console"]}
|
key="attach_pdf_to_email"
|
||||||
label={t("bodyshop.fields.tt_enforce_hours_for_tech_console")}
|
name={["attach_pdf_to_email"]}
|
||||||
valuePropName="checked"
|
label={t("bodyshop.fields.attach_pdf_to_email")}
|
||||||
>
|
valuePropName="checked"
|
||||||
<Switch />
|
>
|
||||||
</Form.Item>
|
<Switch />
|
||||||
<Form.Item
|
</Form.Item>,
|
||||||
name={["bill_allow_post_to_closed"]}
|
<Form.Item
|
||||||
label={t("bodyshop.fields.bill_allow_post_to_closed")}
|
key="md_from_emails"
|
||||||
valuePropName="checked"
|
name={["md_from_emails"]}
|
||||||
>
|
label={t("bodyshop.fields.md_from_emails")}
|
||||||
<Switch />
|
// rules={[
|
||||||
</Form.Item>
|
// {
|
||||||
</>
|
// //message: t("general.validation.required"),
|
||||||
)}
|
// type: "array",
|
||||||
<Form.Item
|
// },
|
||||||
name={["md_ded_notes"]}
|
// ]}
|
||||||
label={t("bodyshop.fields.md_ded_notes")}
|
>
|
||||||
rules={[
|
<Select mode="tags" />
|
||||||
{
|
</Form.Item>,
|
||||||
//message: t("general.validation.required"),
|
<Form.Item
|
||||||
type: "array"
|
key="md_email_cc_parts_order"
|
||||||
}
|
name={["md_email_cc", "parts_order"]}
|
||||||
]}
|
label={t("bodyshop.fields.md_email_cc", { template: "parts_orders" })}
|
||||||
>
|
rules={[
|
||||||
<Select mode="tags" />
|
{
|
||||||
</Form.Item>
|
//message: t("general.validation.required"),
|
||||||
<Form.Item
|
type: "array"
|
||||||
label={t("bodyshop.fields.md_functionality_toggles.parts_queue_toggle")}
|
}
|
||||||
name={["md_functionality_toggles", "parts_queue_toggle"]}
|
]}
|
||||||
valuePropName="checked"
|
>
|
||||||
>
|
<Select mode="tags" />
|
||||||
<Switch />
|
</Form.Item>,
|
||||||
</Form.Item>
|
<Form.Item
|
||||||
<Form.Item name={["last_name_first"]} label={t("bodyshop.fields.last_name_first")} valuePropName="checked">
|
key="md_email_cc_parts_return_slip"
|
||||||
<Switch />
|
name={["md_email_cc", "parts_return_slip"]}
|
||||||
</Form.Item>
|
label={t("bodyshop.fields.md_email_cc", { template: "parts_returns" })}
|
||||||
<Form.Item
|
rules={[
|
||||||
name={["uselocalmediaserver"]}
|
{
|
||||||
label={t("bodyshop.fields.uselocalmediaserver")}
|
//message: t("general.validation.required"),
|
||||||
valuePropName="checked"
|
type: "array"
|
||||||
>
|
}
|
||||||
<Switch />
|
]}
|
||||||
</Form.Item>
|
>
|
||||||
<Form.Item name={["localmediaserverhttp"]} label={t("bodyshop.fields.localmediaserverhttp")}>
|
<Select mode="tags" />
|
||||||
<Input />
|
</Form.Item>,
|
||||||
</Form.Item>
|
...(HasFeatureAccess({ featureName: "timetickets", bodyshop })
|
||||||
<Form.Item name={["localmediaservernetwork"]} label={t("bodyshop.fields.localmediaservernetwork")}>
|
? [
|
||||||
<Input />
|
<Form.Item
|
||||||
</Form.Item>
|
key="tt_allow_post_to_invoiced"
|
||||||
<Form.Item name={["localmediatoken"]} label={t("bodyshop.fields.localmediatoken")}>
|
name={["tt_allow_post_to_invoiced"]}
|
||||||
<Input />
|
label={t("bodyshop.fields.tt_allow_post_to_invoiced")}
|
||||||
</Form.Item>
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>,
|
||||||
|
<Form.Item
|
||||||
|
key="tt_enforce_hours_for_tech_console"
|
||||||
|
name={["tt_enforce_hours_for_tech_console"]}
|
||||||
|
label={t("bodyshop.fields.tt_enforce_hours_for_tech_console")}
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>,
|
||||||
|
<Form.Item
|
||||||
|
key="bill_allow_post_to_closed"
|
||||||
|
name={["bill_allow_post_to_closed"]}
|
||||||
|
label={t("bodyshop.fields.bill_allow_post_to_closed")}
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
<Form.Item
|
||||||
|
key="md_ded_notes"
|
||||||
|
name={["md_ded_notes"]}
|
||||||
|
label={t("bodyshop.fields.md_ded_notes")}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
type: "array"
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Select mode="tags" />
|
||||||
|
</Form.Item>,
|
||||||
|
<Form.Item
|
||||||
|
key="parts_queue_toggle"
|
||||||
|
label={t("bodyshop.fields.md_functionality_toggles.parts_queue_toggle")}
|
||||||
|
name={["md_functionality_toggles", "parts_queue_toggle"]}
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>,
|
||||||
|
<Form.Item
|
||||||
|
key="last_name_first"
|
||||||
|
name={["last_name_first"]}
|
||||||
|
label={t("bodyshop.fields.last_name_first")}
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>,
|
||||||
|
<Form.Item
|
||||||
|
key="uselocalmediaserver"
|
||||||
|
name={["uselocalmediaserver"]}
|
||||||
|
label={t("bodyshop.fields.uselocalmediaserver")}
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>,
|
||||||
|
<Form.Item
|
||||||
|
key="localmediaserverhttp"
|
||||||
|
name={["localmediaserverhttp"]}
|
||||||
|
label={t("bodyshop.fields.localmediaserverhttp")}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>,
|
||||||
|
<Form.Item
|
||||||
|
key="localmediaservernetwork"
|
||||||
|
name={["localmediaservernetwork"]}
|
||||||
|
label={t("bodyshop.fields.localmediaservernetwork")}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>,
|
||||||
|
<Form.Item key="localmediatoken" name={["localmediatoken"]} label={t("bodyshop.fields.localmediatoken")}>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
]}
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
<LayoutFormRow header={t("bodyshop.labels.shop_enabled_features")} id="sharing">
|
<LayoutFormRow header={t("bodyshop.labels.shop_enabled_features")} id="sharing">
|
||||||
<Form.Item
|
<Form.Item
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
140
client/src/components/shop-info/useFormDataPreservation.js
Normal file
140
client/src/components/shop-info/useFormDataPreservation.js
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import { useCallback, useEffect } from "react";
|
||||||
|
import { HasFeatureAccess } from "./../feature-wrapper/feature-wrapper.component";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom hook to preserve form data for conditionally hidden fields based on feature access
|
||||||
|
* @param {Object} form - Ant Design form instance
|
||||||
|
* @param {Object} bodyshop - Bodyshop data for feature access checks (also contains existing database values)
|
||||||
|
* @param {Object} featureConfig - Configuration object defining which features and their associated fields to preserve
|
||||||
|
*/
|
||||||
|
export const useFormDataPreservation = (form, bodyshop, featureConfig) => {
|
||||||
|
const getNestedValue = (obj, path) => {
|
||||||
|
return path.reduce((current, key) => current?.[key], obj);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setNestedValue = (obj, path, value) => {
|
||||||
|
const lastKey = path[path.length - 1];
|
||||||
|
const parentPath = path.slice(0, -1);
|
||||||
|
|
||||||
|
const parent = parentPath.reduce((current, key) => {
|
||||||
|
if (!current[key]) current[key] = {};
|
||||||
|
return current[key];
|
||||||
|
}, obj);
|
||||||
|
|
||||||
|
parent[lastKey] = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const preserveHiddenFormData = useCallback(() => {
|
||||||
|
const preservationData = {};
|
||||||
|
let hasDataToPreserve = false;
|
||||||
|
|
||||||
|
Object.entries(featureConfig).forEach(([featureName, fieldPaths]) => {
|
||||||
|
const hasAccess = HasFeatureAccess({ featureName, bodyshop });
|
||||||
|
|
||||||
|
if (!hasAccess) {
|
||||||
|
fieldPaths.forEach((fieldPath) => {
|
||||||
|
const currentValues = form.getFieldsValue();
|
||||||
|
let value = getNestedValue(currentValues, fieldPath);
|
||||||
|
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
value = getNestedValue(bodyshop, fieldPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value !== undefined && value !== null) {
|
||||||
|
setNestedValue(preservationData, fieldPath, value);
|
||||||
|
hasDataToPreserve = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasDataToPreserve) {
|
||||||
|
form.setFieldsValue(preservationData);
|
||||||
|
}
|
||||||
|
}, [form, featureConfig, bodyshop]);
|
||||||
|
|
||||||
|
const getCompleteFormValues = () => {
|
||||||
|
const currentFormValues = form.getFieldsValue();
|
||||||
|
const completeValues = { ...currentFormValues };
|
||||||
|
|
||||||
|
Object.entries(featureConfig).forEach(([featureName, fieldPaths]) => {
|
||||||
|
const hasAccess = HasFeatureAccess({ featureName, bodyshop });
|
||||||
|
|
||||||
|
if (!hasAccess) {
|
||||||
|
fieldPaths.forEach((fieldPath) => {
|
||||||
|
let value = getNestedValue(currentFormValues, fieldPath);
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
value = getNestedValue(bodyshop, fieldPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value !== undefined && value !== null) {
|
||||||
|
setNestedValue(completeValues, fieldPath, value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return completeValues;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createSubmissionHandler = (originalHandler) => {
|
||||||
|
return () => {
|
||||||
|
const completeValues = getCompleteFormValues();
|
||||||
|
|
||||||
|
// Call the original handler with complete values including hidden data
|
||||||
|
return originalHandler(completeValues);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
preserveHiddenFormData();
|
||||||
|
}, [bodyshop, preserveHiddenFormData]);
|
||||||
|
|
||||||
|
return { preserveHiddenFormData, getCompleteFormValues, createSubmissionHandler };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Predefined feature configurations for common shop-info components
|
||||||
|
*/
|
||||||
|
export const FEATURE_CONFIGS = {
|
||||||
|
responsibilitycenters: {
|
||||||
|
export: [
|
||||||
|
["md_responsibility_centers", "costs"],
|
||||||
|
["md_responsibility_centers", "profits"],
|
||||||
|
["md_responsibility_centers", "defaults"],
|
||||||
|
["md_responsibility_centers", "dms_defaults"],
|
||||||
|
["md_responsibility_centers", "taxes", "itemexemptcode"],
|
||||||
|
["md_responsibility_centers", "taxes", "invoiceexemptcode"],
|
||||||
|
["md_responsibility_centers", "ar"],
|
||||||
|
["md_responsibility_centers", "refund"],
|
||||||
|
["md_responsibility_centers", "sales_tax_codes"],
|
||||||
|
["md_responsibility_centers", "ttl_adjustment"],
|
||||||
|
["md_responsibility_centers", "ttl_tax_adjustment"]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
general: {
|
||||||
|
export: [
|
||||||
|
["accountingconfig", "qbo"],
|
||||||
|
["accountingconfig", "qbo_usa"],
|
||||||
|
["accountingconfig", "qbo_departmentid"],
|
||||||
|
["accountingconfig", "tiers"],
|
||||||
|
["accountingconfig", "twotierpref"],
|
||||||
|
["accountingconfig", "printlater"],
|
||||||
|
["accountingconfig", "emaillater"],
|
||||||
|
["accountingconfig", "ReceivableCustomField1"],
|
||||||
|
["accountingconfig", "ReceivableCustomField2"],
|
||||||
|
["accountingconfig", "ReceivableCustomField3"],
|
||||||
|
["md_classes"],
|
||||||
|
["enforce_class"],
|
||||||
|
["accountingconfig", "ClosingPeriod"],
|
||||||
|
["accountingconfig", "companyCode"],
|
||||||
|
["accountingconfig", "batchID"]
|
||||||
|
],
|
||||||
|
bills: [
|
||||||
|
["bill_tax_rates", "federal_tax_rate"],
|
||||||
|
["bill_tax_rates", "state_tax_rate"],
|
||||||
|
["bill_tax_rates", "local_tax_rate"]
|
||||||
|
],
|
||||||
|
timetickets: [["tt_allow_post_to_invoiced"], ["tt_enforce_hours_for_tech_console"], ["bill_allow_post_to_closed"]]
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -4,9 +4,9 @@
|
|||||||
right: 0;
|
right: 0;
|
||||||
width: 500px;
|
width: 500px;
|
||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
background: #fff;
|
background: var(--task-bg);
|
||||||
color: rgba(0, 0, 0, 0.85);
|
color: var(--task-text);
|
||||||
border: 1px solid #d9d9d9;
|
border: 1px solid var(--task-border);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08), 0 3px 6px rgba(0, 0, 0, 0.06);
|
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08), 0 3px 6px rgba(0, 0, 0, 0.06);
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
@@ -19,11 +19,11 @@
|
|||||||
|
|
||||||
.task-header {
|
.task-header {
|
||||||
padding: 4px 10px;
|
padding: 4px 10px;
|
||||||
border-bottom: 1px solid #f0f0f0;
|
border-bottom: 1px solid var(--task-header-border);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: #fafafa;
|
background: var(--task-header-bg);
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -32,14 +32,14 @@
|
|||||||
|
|
||||||
.create-task-button {
|
.create-task-button {
|
||||||
border: none;
|
border: none;
|
||||||
color: white;
|
color: var(--task-button-text);
|
||||||
padding: 4px 12px;
|
padding: 4px 12px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: #40a9ff;
|
background-color: var(--task-button-hover-bg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,10 +52,9 @@
|
|||||||
.section-title {
|
.section-title {
|
||||||
padding: 0px 10px;
|
padding: 0px 10px;
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
//font-size: 12px;
|
background: var(--task-section-bg);
|
||||||
background: #f5f5f5;
|
|
||||||
font-weight: 650;
|
font-weight: 650;
|
||||||
border-bottom: 1px solid #e8e8e8;
|
border-bottom: 1px solid var(--task-section-border);
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
@@ -68,22 +67,21 @@
|
|||||||
|
|
||||||
.task-row {
|
.task-row {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-bottom: 1px solid #f0f0f0;
|
border-bottom: 1px solid var(--task-row-border);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: #f5f5f5;
|
background: var(--task-row-hover-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-title-cell {
|
.task-title-cell {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
//font-size: 12px;
|
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
max-width: 350px; // or whatever fits your layout
|
max-width: 350px;
|
||||||
|
|
||||||
.task-title {
|
.task-title {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
@@ -91,44 +89,42 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
max-width: 100%; // Or a specific width if you want more control
|
max-width: 100%;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-ro-number {
|
.task-ro-number {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
color: #1677ff;
|
color: var(--task-ro-number);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-due-cell {
|
.task-due-cell {
|
||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
//font-size: 12px;
|
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
color: rgba(0, 0, 0, 0.45);
|
color: var(--task-due-text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
margin: 8px auto;
|
margin: 8px auto;
|
||||||
padding: 4px 10px;
|
padding: 4px 10px;
|
||||||
background-color: #1677ff;
|
background-color: var(--task-button-bg);
|
||||||
color: white;
|
color: var(--task-button-text);
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
//font-size: 12px;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: #4096ff;
|
background-color: var(--task-button-hover-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:disabled {
|
&:disabled {
|
||||||
background-color: #d9d9d9;
|
background-color: var(--task-button-disabled-bg);
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -137,7 +133,7 @@
|
|||||||
.error-message {
|
.error-message {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: rgba(0, 0, 0, 0.45);
|
color: var(--task-message-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-footer {
|
.loading-footer {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
td {
|
td {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
border-bottom: 1px solid #ddd;
|
border-bottom: 1px solid var(--table-border-color);
|
||||||
|
|
||||||
.ant-form-item {
|
.ant-form-item {
|
||||||
margin-bottom: 0px !important;
|
margin-bottom: 0px !important;
|
||||||
@@ -14,6 +14,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
tr:hover {
|
tr:hover {
|
||||||
background-color: #f5f5f5;
|
background-color: var(--table-hover-bg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { Button, Card, Result } from "antd";
|
import { Button, Card, Result } from "antd";
|
||||||
import i18n from "i18next";
|
import i18n from "i18next";
|
||||||
import React, { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { store } from "../../redux/store.js";
|
import { store } from "../../redux/store.js";
|
||||||
@@ -21,7 +21,6 @@ import "./upsell.styles.scss";
|
|||||||
export default function UpsellComponent({ featureName, subFeatureName, upsell, disableMask }) {
|
export default function UpsellComponent({ featureName, subFeatureName, upsell, disableMask }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const resultProps = upsell || upsellEnum[featureName][subFeatureName];
|
const resultProps = upsell || upsellEnum[featureName][subFeatureName];
|
||||||
|
|
||||||
const componentRef = useRef(null);
|
const componentRef = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -34,12 +33,10 @@ export default function UpsellComponent({ featureName, subFeatureName, upsell, d
|
|||||||
mask.style.left = 0;
|
mask.style.left = 0;
|
||||||
mask.style.width = "100%";
|
mask.style.width = "100%";
|
||||||
mask.style.height = "100%";
|
mask.style.height = "100%";
|
||||||
mask.style.backgroundColor = "rgba(0, 0, 0, 0.05)";
|
mask.style.backgroundColor = "var(--mask-bg)";
|
||||||
// mask.style.zIndex = 9999;
|
// mask.style.zIndex = 9999;
|
||||||
parentElement.style.position = "relative";
|
parentElement.style.position = "relative";
|
||||||
|
|
||||||
parentElement.prepend(mask);
|
parentElement.prepend(mask);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
parentElement.removeChild(mask);
|
parentElement.removeChild(mask);
|
||||||
};
|
};
|
||||||
@@ -47,18 +44,22 @@ export default function UpsellComponent({ featureName, subFeatureName, upsell, d
|
|||||||
}, [disableMask]);
|
}, [disableMask]);
|
||||||
|
|
||||||
if (!resultProps) return <Result status="info" title={t("upsell.messages.generic")} />;
|
if (!resultProps) return <Result status="info" title={t("upsell.messages.generic")} />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={componentRef}>
|
<div ref={componentRef}>
|
||||||
<Result status="info" icon={<AppstoreAddOutlined />} {...resultProps} />
|
<Result status="info" icon={<AppstoreAddOutlined />} {...resultProps} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
//Kept in the same function as the result props line must mirror and doesnt warrant a separate function.
|
//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 }) {
|
export function UpsellMaskWrapper({ children, upsell, featureName, subFeatureName }) {
|
||||||
const resultProps = upsell || upsellEnum[featureName][subFeatureName];
|
const resultProps = upsell || upsellEnum[featureName][subFeatureName];
|
||||||
return (
|
return (
|
||||||
<div className="mask-wrapper">
|
<div className="mask-wrapper">
|
||||||
<div className="mask-content">{children}</div>
|
<div className="mask-content" style={{ backgroundColor: "var(--mask-bg)" }}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
<div className="mask-overlay">
|
<div className="mask-overlay">
|
||||||
<Card size="small">
|
<Card size="small">
|
||||||
<Result status="info" icon={<AppstoreAddOutlined />} {...resultProps} />
|
<Result status="info" icon={<AppstoreAddOutlined />} {...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
|
//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 = () => {
|
export const upsellEnum = () => {
|
||||||
const { currentUser, bodyshop } = store.getState().user;
|
const { currentUser, bodyshop } = store.getState().user;
|
||||||
|
|
||||||
const [first_name, ...last_name] = currentUser?.displayName ? currentUser.displayName.split(" ") : [];
|
const [first_name, ...last_name] = currentUser?.displayName ? currentUser.displayName.split(" ") : [];
|
||||||
const LearnMoreLink = encodeURI(
|
const LearnMoreLink = encodeURI(
|
||||||
InstanceRenderManager({
|
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 || ""}`
|
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 {
|
return {
|
||||||
bills: {
|
bills: {
|
||||||
autoreconcile: {
|
autoreconcile: {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
.mask-wrapper {
|
.mask-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
//Newly added
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -8,12 +7,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mask-content {
|
.mask-content {
|
||||||
// filter: blur(5px);
|
background-color: var(--mask-content-bg);
|
||||||
background-color: rgba(0, 0, 0, 0.05);
|
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
||||||
//Newly added
|
|
||||||
//width: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mask-overlay {
|
.mask-overlay {
|
||||||
@@ -22,35 +17,8 @@
|
|||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
// width: 100%
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mask-overlay .ant-card {
|
.mask-overlay .ant-card {
|
||||||
max-width: 100%;
|
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%;
|
|
||||||
// }
|
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ const VendorSearchSelect = ({ value, onChange, options, onSelect, disabled, pref
|
|||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{discount && discount !== 0 ? <Tag color="green">{`${discount * 100}%`}</Tag> : null}
|
{discount && discount !== 0 ? <Tag color="green">{`${discount * 100}%`}</Tag> : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -116,6 +117,11 @@ const VendorSearchSelect = ({ value, onChange, options, onSelect, disabled, pref
|
|||||||
{o.name}
|
{o.name}
|
||||||
</div>
|
</div>
|
||||||
<Space style={{ marginLeft: "1rem" }}>
|
<Space style={{ marginLeft: "1rem" }}>
|
||||||
|
{o.tags?.map((tag, idx) => (
|
||||||
|
<Tag key={idx} style={{ marginLeft: "0.5rem" }}>
|
||||||
|
{tag}
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
{o.phone && showPhone && <PhoneNumberFormatter>{o.phone}</PhoneNumberFormatter>}
|
{o.phone && showPhone && <PhoneNumberFormatter>{o.phone}</PhoneNumberFormatter>}
|
||||||
{o.discount && o.discount !== 0 ? <Tag color="green">{`${o.discount * 100}%`}</Tag> : null}
|
{o.discount && o.discount !== 0 ? <Tag color="green">{`${o.discount * 100}%`}</Tag> : null}
|
||||||
</Space>
|
</Space>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { DeleteFilled } from "@ant-design/icons";
|
import { DeleteFilled } from "@ant-design/icons";
|
||||||
import { useApolloClient } from "@apollo/client";
|
import { useApolloClient } from "@apollo/client";
|
||||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||||
import { Button, Divider, Form, Input, InputNumber, Space, Switch } from "antd";
|
import { Button, Divider, Form, Input, InputNumber, Select, Space, Switch } from "antd";
|
||||||
import { PageHeader } from "@ant-design/pro-layout";
|
import { PageHeader } from "@ant-design/pro-layout";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -179,6 +179,18 @@ export function VendorsFormComponent({
|
|||||||
}
|
}
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="tags"
|
||||||
|
label={t("vendor.fields.tags")}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
type: "array"
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Select mode="tags" />
|
||||||
|
</Form.Item>
|
||||||
{DmsAp.treatment === "on" && (
|
{DmsAp.treatment === "on" && (
|
||||||
<Form.Item label={t("vendors.fields.dmsid")} name="dmsid">
|
<Form.Item label={t("vendors.fields.dmsid")} name="dmsid">
|
||||||
<Input />
|
<Input />
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { SyncOutlined } from "@ant-design/icons";
|
import { SyncOutlined } from "@ant-design/icons";
|
||||||
import { Button, Card, Input, Space, Table } from "antd";
|
import { Button, Card, Input, Space, Table, Tag } from "antd";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -38,6 +38,18 @@ export default function VendorsListComponent({ handleNewVendor, loading, handleO
|
|||||||
title: t("vendors.fields.city"),
|
title: t("vendors.fields.city"),
|
||||||
dataIndex: "city",
|
dataIndex: "city",
|
||||||
key: "city"
|
key: "city"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("vendors.fields.tags"),
|
||||||
|
dataIndex: "tags",
|
||||||
|
key: "tags",
|
||||||
|
render: (text, record) => (
|
||||||
|
<Space>
|
||||||
|
{record?.tags?.map((tag, idx) => (
|
||||||
|
<Tag key={idx}>{tag}</Tag>
|
||||||
|
))}
|
||||||
|
</Space>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -713,6 +713,19 @@ export const GET_JOB_BY_PK = gql`
|
|||||||
v_model_yr
|
v_model_yr
|
||||||
v_model_desc
|
v_model_desc
|
||||||
v_vin
|
v_vin
|
||||||
|
notes(where:{pinned: {_eq: true}}, order_by: {updated_at: desc}) {
|
||||||
|
created_at
|
||||||
|
created_by
|
||||||
|
critical
|
||||||
|
id
|
||||||
|
jobid
|
||||||
|
private
|
||||||
|
text
|
||||||
|
updated_at
|
||||||
|
audit
|
||||||
|
type
|
||||||
|
pinned
|
||||||
|
}
|
||||||
vehicle {
|
vehicle {
|
||||||
id
|
id
|
||||||
jobs {
|
jobs {
|
||||||
@@ -959,6 +972,8 @@ export const QUERY_JOB_CARD_DETAILS = gql`
|
|||||||
critical
|
critical
|
||||||
private
|
private
|
||||||
created_at
|
created_at
|
||||||
|
pinned
|
||||||
|
type
|
||||||
}
|
}
|
||||||
updated_at
|
updated_at
|
||||||
clm_total
|
clm_total
|
||||||
@@ -984,6 +999,7 @@ export const QUERY_JOB_CARD_DETAILS = gql`
|
|||||||
key
|
key
|
||||||
type
|
type
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -1048,6 +1064,8 @@ export const QUERY_TECH_JOB_DETAILS = gql`
|
|||||||
critical
|
critical
|
||||||
private
|
private
|
||||||
created_at
|
created_at
|
||||||
|
pinned
|
||||||
|
type
|
||||||
}
|
}
|
||||||
updated_at
|
updated_at
|
||||||
documents(order_by: { created_at: desc }) {
|
documents(order_by: { created_at: desc }) {
|
||||||
@@ -2323,7 +2341,7 @@ export const QUERY_JOBS_TECH_ASIGNED_TO_BY_TEAM = gql`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const QUERY_PARTS_QUEUE_CARD_DETAILS = gql`
|
export const QUERY_PARTS_QUEUE_CARD_DETAILS = gql`
|
||||||
query QUERY_JOB_CARD_DETAILS($id: uuid!) {
|
query QUERY_PARTS_QUEUE_CARD_DETAILS($id: uuid!) {
|
||||||
jobs_by_pk(id: $id) {
|
jobs_by_pk(id: $id) {
|
||||||
actual_completion
|
actual_completion
|
||||||
actual_delivery
|
actual_delivery
|
||||||
@@ -2349,6 +2367,19 @@ export const QUERY_PARTS_QUEUE_CARD_DETAILS = gql`
|
|||||||
start
|
start
|
||||||
status
|
status
|
||||||
}
|
}
|
||||||
|
notes(where:{pinned: {_eq: true}}, order_by: {updated_at: desc}) {
|
||||||
|
created_at
|
||||||
|
created_by
|
||||||
|
critical
|
||||||
|
id
|
||||||
|
jobid
|
||||||
|
private
|
||||||
|
text
|
||||||
|
updated_at
|
||||||
|
audit
|
||||||
|
type
|
||||||
|
pinned
|
||||||
|
}
|
||||||
clm_no
|
clm_no
|
||||||
clm_total
|
clm_total
|
||||||
comment
|
comment
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export const INSERT_NEW_NOTE = gql`
|
|||||||
updated_at
|
updated_at
|
||||||
audit
|
audit
|
||||||
type
|
type
|
||||||
|
pinned
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -43,6 +44,7 @@ export const QUERY_NOTES_BY_JOB_PK = gql`
|
|||||||
updated_at
|
updated_at
|
||||||
audit
|
audit
|
||||||
type
|
type
|
||||||
|
pinned
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -63,6 +65,7 @@ export const UPDATE_NOTE = gql`
|
|||||||
updated_at
|
updated_at
|
||||||
audit
|
audit
|
||||||
type
|
type
|
||||||
|
pinned
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export const QUERY_VENDOR_BY_ID = gql`
|
|||||||
active
|
active
|
||||||
phone
|
phone
|
||||||
dmsid
|
dmsid
|
||||||
|
tags
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -54,6 +55,7 @@ export const QUERY_ALL_VENDORS = gql`
|
|||||||
city
|
city
|
||||||
phone
|
phone
|
||||||
active
|
active
|
||||||
|
tags
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -89,6 +91,7 @@ export const QUERY_ALL_VENDORS_FOR_ORDER = gql`
|
|||||||
email
|
email
|
||||||
active
|
active
|
||||||
phone
|
phone
|
||||||
|
tags
|
||||||
}
|
}
|
||||||
jobs(where: { id: { _eq: $jobId } }) {
|
jobs(where: { id: { _eq: $jobId } }) {
|
||||||
v_make_desc
|
v_make_desc
|
||||||
@@ -105,6 +108,7 @@ export const SEARCH_VENDOR_AUTOCOMPLETE = gql`
|
|||||||
cost_center
|
cost_center
|
||||||
active
|
active
|
||||||
favorite
|
favorite
|
||||||
|
tags
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -124,6 +128,7 @@ export const SEARCH_VENDOR_AUTOCOMPLETE_WITH_ADDR = gql`
|
|||||||
email
|
email
|
||||||
state
|
state
|
||||||
active
|
active
|
||||||
|
tags
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { EditFilled, SyncOutlined } from "@ant-design/icons";
|
|||||||
import { Button, Card, Checkbox, Input, Space, Table, Typography } from "antd";
|
import { Button, Card, Checkbox, Input, Space, Table, Typography } from "antd";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import React, { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||||
@@ -13,8 +13,9 @@ import { setModalContext } from "../../redux/modals/modals.actions";
|
|||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
import { DateFormatter } from "../../utils/DateFormatter";
|
import { DateFormatter } from "../../utils/DateFormatter";
|
||||||
import { TemplateList } from "../../utils/TemplateConstants";
|
import { TemplateList } from "../../utils/TemplateConstants";
|
||||||
import { alphaSort, dateSort } from "../../utils/sorters";
|
|
||||||
import { pageLimit } from "../../utils/config";
|
import { pageLimit } from "../../utils/config";
|
||||||
|
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||||
|
import useLocalStorage from "../../utils/useLocalStorage";
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
setPartsOrderContext: (context) => dispatch(setModalContext({ context: context, modal: "partsOrder" })),
|
setPartsOrderContext: (context) => dispatch(setModalContext({ context: context, modal: "partsOrder" })),
|
||||||
@@ -27,7 +28,7 @@ export function BillsListPage({ loading, data, refetch, total, setPartsOrderCont
|
|||||||
const [searchLoading, setSearchLoading] = useState(false);
|
const [searchLoading, setSearchLoading] = useState(false);
|
||||||
const { page } = search;
|
const { page } = search;
|
||||||
const history = useNavigate();
|
const history = useNavigate();
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useLocalStorage("bills_list_sort", {
|
||||||
sortedInfo: {},
|
sortedInfo: {},
|
||||||
filteredInfo: { text: "" }
|
filteredInfo: { text: "" }
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
//import {useMutation, useQuery } from "@apollo/client";
|
//import {useMutation, useQuery } from "@apollo/client";
|
||||||
import { Button, Form, Layout, Result, Typography } from "antd";
|
import { Button, Form, Layout, Result, Typography } from "antd";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import React, { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
@@ -16,7 +16,8 @@ import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
|||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
currentUser: selectCurrentUser
|
currentUser: selectCurrentUser
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({});
|
|
||||||
|
const mapDispatchToProps = () => ({});
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(CsiContainerPage);
|
export default connect(mapStateToProps, mapDispatchToProps)(CsiContainerPage);
|
||||||
|
|
||||||
@@ -28,7 +29,6 @@ export function CsiContainerPage({ currentUser }) {
|
|||||||
loading: false,
|
loading: false,
|
||||||
submitted: false
|
submitted: false
|
||||||
});
|
});
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const getAxiosData = useCallback(async () => {
|
const getAxiosData = useCallback(async () => {
|
||||||
@@ -39,7 +39,6 @@ export function CsiContainerPage({ currentUser }) {
|
|||||||
console.log("Unable to attach to crisp instance. ");
|
console.log("Unable to attach to crisp instance. ");
|
||||||
}
|
}
|
||||||
setSubmitting((prevSubmitting) => ({ ...prevSubmitting, loading: true }));
|
setSubmitting((prevSubmitting) => ({ ...prevSubmitting, loading: true }));
|
||||||
|
|
||||||
const response = await axios.post("/csi/lookup", {
|
const response = await axios.post("/csi/lookup", {
|
||||||
surveyId
|
surveyId
|
||||||
});
|
});
|
||||||
@@ -91,7 +90,7 @@ export function CsiContainerPage({ currentUser }) {
|
|||||||
setSubmitting({ ...submitting, loading: true, submitting: true });
|
setSubmitting({ ...submitting, loading: true, submitting: true });
|
||||||
const result = await axios.post("/csi/submit", { surveyId, values });
|
const result = await axios.post("/csi/submit", { surveyId, values });
|
||||||
console.log("result", result);
|
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 });
|
setSubmitting({ ...submitting, loading: false, submitted: true });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -110,7 +109,7 @@ export function CsiContainerPage({ currentUser }) {
|
|||||||
<Layout style={{ display: "flex", flexDirection: "column" }}>
|
<Layout style={{ display: "flex", flexDirection: "column" }}>
|
||||||
<Layout.Content
|
<Layout.Content
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: "#fff",
|
backgroundColor: "var(--content-bg)",
|
||||||
margin: "2em 4em",
|
margin: "2em 4em",
|
||||||
padding: "2em",
|
padding: "2em",
|
||||||
overflowY: "auto",
|
overflowY: "auto",
|
||||||
@@ -139,7 +138,6 @@ export function CsiContainerPage({ currentUser }) {
|
|||||||
relateddata: { bodyshop, job },
|
relateddata: { bodyshop, job },
|
||||||
csiquestion: { config: csiquestions }
|
csiquestion: { config: csiquestions }
|
||||||
} = axiosResponse.csi_by_pk;
|
} = axiosResponse.csi_by_pk;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout style={{ display: "flex", flexDirection: "column" }}>
|
<Layout style={{ display: "flex", flexDirection: "column" }}>
|
||||||
<div
|
<div
|
||||||
@@ -184,13 +182,11 @@ export function CsiContainerPage({ currentUser }) {
|
|||||||
})}
|
})}
|
||||||
</Typography.Paragraph>
|
</Typography.Paragraph>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{submitting.error ? <AlertComponent message={submitting.error} type="error" /> : null}
|
{submitting.error ? <AlertComponent message={submitting.error} type="error" /> : null}
|
||||||
|
|
||||||
{submitting.submitted ? (
|
{submitting.submitted ? (
|
||||||
<Layout.Content
|
<Layout.Content
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: "#fff",
|
backgroundColor: "var(--content-bg)",
|
||||||
margin: "2em 4em",
|
margin: "2em 4em",
|
||||||
padding: "2em",
|
padding: "2em",
|
||||||
overflowY: "auto"
|
overflowY: "auto"
|
||||||
@@ -201,7 +197,7 @@ export function CsiContainerPage({ currentUser }) {
|
|||||||
) : (
|
) : (
|
||||||
<Layout.Content
|
<Layout.Content
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: "#fff",
|
backgroundColor: "var(--content-bg)",
|
||||||
margin: "2em 4em",
|
margin: "2em 4em",
|
||||||
padding: "2em",
|
padding: "2em",
|
||||||
overflowY: "auto"
|
overflowY: "auto"
|
||||||
|
|||||||
@@ -271,7 +271,7 @@ export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail, set
|
|||||||
{
|
{
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
({ getFieldValue }) => ({
|
() => ({
|
||||||
validator(_, value) {
|
validator(_, value) {
|
||||||
if (!bodyshop.cdk_dealerid) return Promise.resolve();
|
if (!bodyshop.cdk_dealerid) return Promise.resolve();
|
||||||
if (!value || dayjs(value).isSameOrAfter(dayjs(), "day")) {
|
if (!value || dayjs(value).isSameOrAfter(dayjs(), "day")) {
|
||||||
@@ -280,7 +280,7 @@ export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail, set
|
|||||||
return Promise.reject(new Error(t("jobs.labels.dms.invoicedatefuture")));
|
return Promise.reject(new Error(t("jobs.labels.dms.invoicedatefuture")));
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
({ getFieldValue }) => ({
|
() => ({
|
||||||
validator(_, value) {
|
validator(_, value) {
|
||||||
if (ClosingPeriod.treatment === "on" && bodyshop.accountingconfig.ClosingPeriod) {
|
if (ClosingPeriod.treatment === "on" && bodyshop.accountingconfig.ClosingPeriod) {
|
||||||
if (
|
if (
|
||||||
@@ -369,8 +369,8 @@ export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail, set
|
|||||||
<Form.List
|
<Form.List
|
||||||
name={["qb_multiple_payers"]}
|
name={["qb_multiple_payers"]}
|
||||||
rules={[
|
rules={[
|
||||||
({ getFieldValue }) => ({
|
() => ({
|
||||||
validator(_, value) {
|
validator() {
|
||||||
let totalAllocated = Dinero();
|
let totalAllocated = Dinero();
|
||||||
|
|
||||||
const payers = form.getFieldValue("qb_multiple_payers");
|
const payers = form.getFieldValue("qb_multiple_payers");
|
||||||
@@ -492,7 +492,7 @@ export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail, set
|
|||||||
<Statistic
|
<Statistic
|
||||||
title={t("jobs.labels.pimraryamountpayable")}
|
title={t("jobs.labels.pimraryamountpayable")}
|
||||||
valueStyle={{
|
valueStyle={{
|
||||||
color: discrep.getAmount() > 0 ? "green" : "red"
|
color: discrep.getAmount() >= 0 ? "green" : "red"
|
||||||
}}
|
}}
|
||||||
value={discrep.toFormat()}
|
value={discrep.toFormat()}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
import ScheduleCalendarContainer from "../../components/schedule-calendar/schedule-calendar.container";
|
import ScheduleCalendarContainer from "../../components/schedule-calendar/schedule-calendar.container";
|
||||||
|
|
||||||
export default function SchedulePageComponent() {
|
export default function SchedulePageComponent() {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
|
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
.tech-content-container {
|
.tech-content-container {
|
||||||
overflow-y: visible;
|
overflow-y: visible;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
background: #fff;
|
background: var(--tech-content-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tech-layout-container {
|
.tech-layout-container {
|
||||||
|
|||||||
@@ -77,3 +77,11 @@ export const setWssStatus = (status) => ({
|
|||||||
type: ApplicationActionTypes.SET_WSS_STATUS,
|
type: ApplicationActionTypes.SET_WSS_STATUS,
|
||||||
payload: status
|
payload: status
|
||||||
});
|
});
|
||||||
|
export const toggleDarkMode = () => ({
|
||||||
|
type: ApplicationActionTypes.TOGGLE_DARK_MODE
|
||||||
|
});
|
||||||
|
|
||||||
|
export const setDarkMode = (value) => ({
|
||||||
|
type: ApplicationActionTypes.SET_DARK_MODE,
|
||||||
|
payload: value
|
||||||
|
});
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ const INITIAL_STATE = {
|
|||||||
},
|
},
|
||||||
jobReadOnly: false,
|
jobReadOnly: false,
|
||||||
partnerVersion: null,
|
partnerVersion: null,
|
||||||
alerts: {}
|
alerts: {},
|
||||||
|
darkMode: false
|
||||||
};
|
};
|
||||||
|
|
||||||
const applicationReducer = (state = INITIAL_STATE, action) => {
|
const applicationReducer = (state = INITIAL_STATE, action) => {
|
||||||
@@ -104,6 +105,18 @@ const applicationReducer = (state = INITIAL_STATE, action) => {
|
|||||||
alerts: newAlertsMap
|
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:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import client from "../../utils/GraphQLClient";
|
|||||||
import { CalculateLoad, CheckJobBucket } from "../../utils/SSSUtils";
|
import { CalculateLoad, CheckJobBucket } from "../../utils/SSSUtils";
|
||||||
import { scheduleLoadFailure, scheduleLoadSuccess, setProblemJobs } from "./application.actions";
|
import { scheduleLoadFailure, scheduleLoadSuccess, setProblemJobs } from "./application.actions";
|
||||||
import ApplicationActionTypes from "./application.types";
|
import ApplicationActionTypes from "./application.types";
|
||||||
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
|
|
||||||
export function* onCalculateScheduleLoad() {
|
export function* onCalculateScheduleLoad() {
|
||||||
yield takeLatest(ApplicationActionTypes.CALCULATE_SCHEDULE_LOAD, calculateScheduleLoad);
|
yield takeLatest(ApplicationActionTypes.CALCULATE_SCHEDULE_LOAD, calculateScheduleLoad);
|
||||||
@@ -106,17 +107,14 @@ export function* calculateScheduleLoad({ payload: end }) {
|
|||||||
|
|
||||||
const AddJobForSchedulingCalc = !item.inproduction;
|
const AddJobForSchedulingCalc = !item.inproduction;
|
||||||
|
|
||||||
if (!!load[itemDate]) {
|
if (load[itemDate]) {
|
||||||
load[itemDate].allHoursIn =
|
load[itemDate].allHoursIn =
|
||||||
(load[itemDate].allHoursIn || 0) +
|
(load[itemDate].allHoursIn || 0) +
|
||||||
item.labhrs.aggregate.sum.mod_lb_hrs +
|
item.labhrs.aggregate.sum.mod_lb_hrs +
|
||||||
item.larhrs.aggregate.sum.mod_lb_hrs;
|
item.larhrs.aggregate.sum.mod_lb_hrs;
|
||||||
load[itemDate].allHoursInBody =
|
load[itemDate].allHoursInBody = (load[itemDate].allHoursInBody || 0) + item.labhrs.aggregate.sum.mod_lb_hrs;
|
||||||
(load[itemDate].allHoursInBody || 0) +
|
|
||||||
item.labhrs.aggregate.sum.mod_lb_hrs;
|
|
||||||
load[itemDate].allHoursInRefinish =
|
load[itemDate].allHoursInRefinish =
|
||||||
(load[itemDate].allHoursInRefinish || 0) +
|
(load[itemDate].allHoursInRefinish || 0) + item.larhrs.aggregate.sum.mod_lb_hrs;
|
||||||
item.larhrs.aggregate.sum.mod_lb_hrs;
|
|
||||||
//If the job hasn't already arrived, add it to the jobs in list.
|
//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.
|
// Make sure it also hasn't already been completed, or isn't an in and out job.
|
||||||
//This prevents the duplicate counting.
|
//This prevents the duplicate counting.
|
||||||
@@ -124,15 +122,9 @@ export function* calculateScheduleLoad({ payload: end }) {
|
|||||||
if (AddJobForSchedulingCalc) {
|
if (AddJobForSchedulingCalc) {
|
||||||
load[itemDate].jobsIn.push(item);
|
load[itemDate].jobsIn.push(item);
|
||||||
load[itemDate].hoursIn =
|
load[itemDate].hoursIn =
|
||||||
(load[itemDate].hoursIn || 0) +
|
(load[itemDate].hoursIn || 0) + item.labhrs.aggregate.sum.mod_lb_hrs + item.larhrs.aggregate.sum.mod_lb_hrs;
|
||||||
item.labhrs.aggregate.sum.mod_lb_hrs +
|
load[itemDate].hoursInBody = (load[itemDate].hoursInBody || 0) + item.labhrs.aggregate.sum.mod_lb_hrs;
|
||||||
item.larhrs.aggregate.sum.mod_lb_hrs;
|
load[itemDate].hoursInRefinish = (load[itemDate].hoursInRefinish || 0) + 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 {
|
} else {
|
||||||
load[itemDate] = {
|
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.
|
jobsIn: AddJobForSchedulingCalc ? [item] : [], //Same as above, only add it if it isn't already in production.
|
||||||
jobsOut: [],
|
jobsOut: [],
|
||||||
allJobsOut: [],
|
allJobsOut: [],
|
||||||
allHoursIn:
|
allHoursIn: item.labhrs.aggregate.sum.mod_lb_hrs + item.larhrs.aggregate.sum.mod_lb_hrs,
|
||||||
item.labhrs.aggregate.sum.mod_lb_hrs +
|
|
||||||
item.larhrs.aggregate.sum.mod_lb_hrs,
|
|
||||||
allHoursInBody: item.labhrs.aggregate.sum.mod_lb_hrs,
|
allHoursInBody: item.labhrs.aggregate.sum.mod_lb_hrs,
|
||||||
allHoursInRefinish: item.larhrs.aggregate.sum.mod_lb_hrs,
|
allHoursInRefinish: item.larhrs.aggregate.sum.mod_lb_hrs,
|
||||||
hoursIn: AddJobForSchedulingCalc
|
hoursIn: AddJobForSchedulingCalc
|
||||||
? item.labhrs.aggregate.sum.mod_lb_hrs +
|
? item.labhrs.aggregate.sum.mod_lb_hrs + item.larhrs.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,
|
: 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");
|
const itemDate = dayjs(item.actual_completion || item.scheduled_completion).format("YYYY-MM-DD");
|
||||||
//Skip it, it's already completed.
|
//Skip it, it's already completed.
|
||||||
|
|
||||||
if (!!load[itemDate]) {
|
if (load[itemDate]) {
|
||||||
load[itemDate].allHoursOut =
|
load[itemDate].allHoursOut =
|
||||||
(load[itemDate].allHoursOut || 0) +
|
(load[itemDate].allHoursOut || 0) +
|
||||||
item.labhrs.aggregate.sum.mod_lb_hrs +
|
item.labhrs.aggregate.sum.mod_lb_hrs +
|
||||||
item.larhrs.aggregate.sum.mod_lb_hrs;
|
item.larhrs.aggregate.sum.mod_lb_hrs;
|
||||||
load[itemDate].allHoursOutBody =
|
load[itemDate].allHoursOutBody = (load[itemDate].allHoursOutBody || 0) + item.labhrs.aggregate.sum.mod_lb_hrs;
|
||||||
(load[itemDate].allHoursOutBody || 0) +
|
|
||||||
item.labhrs.aggregate.sum.mod_lb_hrs;
|
|
||||||
load[itemDate].allHoursOutRefinish =
|
load[itemDate].allHoursOutRefinish =
|
||||||
(load[itemDate].allHoursOutRefinish || 0) +
|
(load[itemDate].allHoursOutRefinish || 0) + item.larhrs.aggregate.sum.mod_lb_hrs;
|
||||||
item.larhrs.aggregate.sum.mod_lb_hrs;
|
|
||||||
//Add only the jobs that are still in production to get rid of.
|
//Add only the jobs that are still in production to get rid of.
|
||||||
//If it's not in production, we'd subtract unnecessarily.
|
//If it's not in production, we'd subtract unnecessarily.
|
||||||
load[itemDate].allJobsOut.push(item);
|
load[itemDate].allJobsOut.push(item);
|
||||||
@@ -191,12 +173,9 @@ export function* calculateScheduleLoad({ payload: end }) {
|
|||||||
(load[itemDate].hoursOut || 0) +
|
(load[itemDate].hoursOut || 0) +
|
||||||
item.labhrs.aggregate.sum.mod_lb_hrs +
|
item.labhrs.aggregate.sum.mod_lb_hrs +
|
||||||
item.larhrs.aggregate.sum.mod_lb_hrs;
|
item.larhrs.aggregate.sum.mod_lb_hrs;
|
||||||
load[itemDate].hoursOutBody =
|
load[itemDate].hoursOutBody = (load[itemDate].hoursOutBody || 0) + item.labhrs.aggregate.sum.mod_lb_hrs;
|
||||||
(load[itemDate].hoursOutBody || 0) +
|
|
||||||
item.labhrs.aggregate.sum.mod_lb_hrs;
|
|
||||||
load[itemDate].hoursOutRefinish =
|
load[itemDate].hoursOutRefinish =
|
||||||
(load[itemDate].hoursOutRefinish || 0) +
|
(load[itemDate].hoursOutRefinish || 0) + item.larhrs.aggregate.sum.mod_lb_hrs;
|
||||||
item.larhrs.aggregate.sum.mod_lb_hrs;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
load[itemDate] = {
|
load[itemDate] = {
|
||||||
@@ -205,11 +184,9 @@ export function* calculateScheduleLoad({ payload: end }) {
|
|||||||
hoursOut: AddJobForSchedulingCalc
|
hoursOut: AddJobForSchedulingCalc
|
||||||
? item.labhrs.aggregate.sum.mod_lb_hrs + item.larhrs.aggregate.sum.mod_lb_hrs
|
? item.labhrs.aggregate.sum.mod_lb_hrs + item.larhrs.aggregate.sum.mod_lb_hrs
|
||||||
: 0,
|
: 0,
|
||||||
allHoursOut:
|
allHoursOut: item.labhrs.aggregate.sum.mod_lb_hrs + item.larhrs.aggregate.sum.mod_lb_hrs,
|
||||||
item.labhrs.aggregate.sum.mod_lb_hrs +
|
|
||||||
item.larhrs.aggregate.sum.mod_lb_hrs,
|
|
||||||
allHoursOutBody: item.labhrs.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)
|
const prev = dayjs(today)
|
||||||
.add(day - 1, "day")
|
.add(day - 1, "day")
|
||||||
.format("YYYY-MM-DD");
|
.format("YYYY-MM-DD");
|
||||||
if (!!!load[current]) {
|
if (!load[current]) {
|
||||||
load[current] = {};
|
load[current] = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,6 +275,14 @@ export function* insertAuditTrailSaga({ payload: { jobid, billid, operation, typ
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function* applicationSagas() {
|
export function* onToggleDarkMode() {
|
||||||
yield all([call(onCalculateScheduleLoad), call(onInsertAuditTrail)]);
|
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)]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,3 +24,4 @@ export const selectProblemJobs = createSelector([selectApplication], (applicatio
|
|||||||
export const selectUpdateAvailable = createSelector([selectApplication], (application) => application.updateAvailable);
|
export const selectUpdateAvailable = createSelector([selectApplication], (application) => application.updateAvailable);
|
||||||
export const selectWssStatus = createSelector([selectApplication], (application) => application.wssStatus);
|
export const selectWssStatus = createSelector([selectApplication], (application) => application.wssStatus);
|
||||||
export const selectAlerts = createSelector([selectApplication], (application) => application.alerts);
|
export const selectAlerts = createSelector([selectApplication], (application) => application.alerts);
|
||||||
|
export const selectDarkMode = createSelector([selectApplication], (application) => application.darkMode);
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ const ApplicationActionTypes = {
|
|||||||
SET_PROBLEM_JOBS: "SET_PROBLEM_JOBS",
|
SET_PROBLEM_JOBS: "SET_PROBLEM_JOBS",
|
||||||
SET_UPDATE_AVAILABLE: "SET_UPDATE_AVAILABLE",
|
SET_UPDATE_AVAILABLE: "SET_UPDATE_AVAILABLE",
|
||||||
SET_WSS_STATUS: "SET_WSS_STATUS",
|
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;
|
export default ApplicationActionTypes;
|
||||||
|
|||||||
@@ -426,6 +426,11 @@
|
|||||||
"messagingtext": "Messaging Preset Text",
|
"messagingtext": "Messaging Preset Text",
|
||||||
"noteslabel": "Note Label",
|
"noteslabel": "Note Label",
|
||||||
"notestext": "Note Text",
|
"notestext": "Note Text",
|
||||||
|
"notifications": {
|
||||||
|
"description": "Select employees to automatically follow new jobs and receive notifications for job updates.",
|
||||||
|
"invalid_followers": "Invalid selection. Please select valid employees.",
|
||||||
|
"placeholder": "Search for employees"
|
||||||
|
},
|
||||||
"partslocation": "Parts Location",
|
"partslocation": "Parts Location",
|
||||||
"phone": "Phone",
|
"phone": "Phone",
|
||||||
"prodtargethrs": "Production Target Hours",
|
"prodtargethrs": "Production Target Hours",
|
||||||
@@ -512,6 +517,7 @@
|
|||||||
"dashboard": "Shop -> Dashboard",
|
"dashboard": "Shop -> Dashboard",
|
||||||
"rbac": "Shop -> RBAC",
|
"rbac": "Shop -> RBAC",
|
||||||
"reportcenter": "Shop -> Report Center",
|
"reportcenter": "Shop -> Report Center",
|
||||||
|
"responsibilitycenter": "Shop -> Responsibility Centers",
|
||||||
"templates": "Shop -> Templates",
|
"templates": "Shop -> Templates",
|
||||||
"vendors": "Shop -> Vendors"
|
"vendors": "Shop -> Vendors"
|
||||||
},
|
},
|
||||||
@@ -648,15 +654,9 @@
|
|||||||
"use_paint_scale_data": "Use Paint Scale Data for Job Costing?",
|
"use_paint_scale_data": "Use Paint Scale Data for Job Costing?",
|
||||||
"uselocalmediaserver": "Use Local Media Server?",
|
"uselocalmediaserver": "Use Local Media Server?",
|
||||||
"website": "Website",
|
"website": "Website",
|
||||||
"zip_post": "Zip/Postal Code",
|
"zip_post": "Zip/Postal Code"
|
||||||
"notifications": {
|
|
||||||
"description": "Select employees to automatically follow new jobs and receive notifications for job updates.",
|
|
||||||
"placeholder": "Search for employees",
|
|
||||||
"invalid_followers": "Invalid selection. Please select valid employees."
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"consent_settings": "Phone Number Opt-Out List",
|
|
||||||
"2tiername": "Name => RO",
|
"2tiername": "Name => RO",
|
||||||
"2tiersetup": "2 Tier Setup",
|
"2tiersetup": "2 Tier Setup",
|
||||||
"2tiersource": "Source => RO",
|
"2tiersource": "Source => RO",
|
||||||
@@ -667,6 +667,7 @@
|
|||||||
"apptcolors": "Appointment Colors",
|
"apptcolors": "Appointment Colors",
|
||||||
"businessinformation": "Business Information",
|
"businessinformation": "Business Information",
|
||||||
"checklists": "Checklists",
|
"checklists": "Checklists",
|
||||||
|
"consent_settings": "Phone Number Opt-Out List",
|
||||||
"csiq": "CSI Questions",
|
"csiq": "CSI Questions",
|
||||||
"customtemplates": "Custom Templates",
|
"customtemplates": "Custom Templates",
|
||||||
"defaultcostsmapping": "Default Costs Mapping",
|
"defaultcostsmapping": "Default Costs Mapping",
|
||||||
@@ -704,6 +705,9 @@
|
|||||||
"messagingpresets": "Messaging Presets",
|
"messagingpresets": "Messaging Presets",
|
||||||
"notemplatesavailable": "No templates available to add.",
|
"notemplatesavailable": "No templates available to add.",
|
||||||
"notespresets": "Notes Presets",
|
"notespresets": "Notes Presets",
|
||||||
|
"notifications": {
|
||||||
|
"followers": "Notifications"
|
||||||
|
},
|
||||||
"orderstatuses": "Order Statuses",
|
"orderstatuses": "Order Statuses",
|
||||||
"partslocations": "Parts Locations",
|
"partslocations": "Parts Locations",
|
||||||
"partsscan": "Parts Scanning",
|
"partsscan": "Parts Scanning",
|
||||||
@@ -734,10 +738,7 @@
|
|||||||
"ssbuckets": "Job Size Definitions",
|
"ssbuckets": "Job Size Definitions",
|
||||||
"systemsettings": "System Settings",
|
"systemsettings": "System Settings",
|
||||||
"task-presets": "Task Presets",
|
"task-presets": "Task Presets",
|
||||||
"workingdays": "Working Days",
|
"workingdays": "Working Days"
|
||||||
"notifications": {
|
|
||||||
"followers": "Notifications"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"operations": {
|
"operations": {
|
||||||
"contains": "Contains",
|
"contains": "Contains",
|
||||||
@@ -783,6 +784,15 @@
|
|||||||
"completed": "Job checklist completed."
|
"completed": "Job checklist completed."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"consent": {
|
||||||
|
"associated_owners": "Associated Owners",
|
||||||
|
"created_at": "Opt-Out Date",
|
||||||
|
"no_owners": "No Associated Owners",
|
||||||
|
"phone_1": "Phone 1",
|
||||||
|
"phone_2": "Phone 2",
|
||||||
|
"phone_number": "Phone Number",
|
||||||
|
"text_body": "Users can opt out of receiving SMS messages by replying with keywords such as STOP, UNSUBSCRIBE, CANCEL, END, QUIT, STOPALL, REVOKE and OPTOUT. To opt back in, users can reply with START, YES, or UNSTOP. Even after opting out, users can still send messages to us, which will be received and processed as needed. Ensure customers are informed to reply with these keywords to manage their messaging preferences. After opting out, users receive a confirmation message and will not receive further messages until they opt back in."
|
||||||
|
},
|
||||||
"contracts": {
|
"contracts": {
|
||||||
"actions": {
|
"actions": {
|
||||||
"changerate": "Change Contract Rates",
|
"changerate": "Change Contract Rates",
|
||||||
@@ -1235,11 +1245,11 @@
|
|||||||
"fcm": "You must allow notification permissions to have real time messaging. Click to try again.",
|
"fcm": "You must allow notification permissions to have real time messaging. Click to try again.",
|
||||||
"notfound": "No record was found.",
|
"notfound": "No record was found.",
|
||||||
"sizelimit": "The selected items exceed the size limit.",
|
"sizelimit": "The selected items exceed the size limit.",
|
||||||
"submit-for-testing": "Error submitting Job for testing.",
|
|
||||||
"sub_status": {
|
"sub_status": {
|
||||||
"expired": "The subscription for this shop has expired. Please contact Sales to reactivate.",
|
"expired": "The subscription for this shop has expired. Please contact Sales to reactivate.",
|
||||||
"trial-expired": "The trial for this shop has expired. Please contact Sales to reactivate."
|
"trial-expired": "The trial for this shop has expired. Please contact Sales to reactivate."
|
||||||
}
|
},
|
||||||
|
"submit-for-testing": "Error submitting Job for testing."
|
||||||
},
|
},
|
||||||
"itemtypes": {
|
"itemtypes": {
|
||||||
"contract": "CC Contract",
|
"contract": "CC Contract",
|
||||||
@@ -1445,9 +1455,9 @@
|
|||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"error": {
|
"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.",
|
"openingip": "Error connecting to IntelliPay service.",
|
||||||
"title": "Error placing refund"
|
"title": "Error issuing refund"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"titles": {
|
"titles": {
|
||||||
@@ -1659,8 +1669,6 @@
|
|||||||
"adjustment_bottom_line": "Adjustments",
|
"adjustment_bottom_line": "Adjustments",
|
||||||
"adjustmenthours": "Adjustment Hours",
|
"adjustmenthours": "Adjustment Hours",
|
||||||
"alt_transport": "Alt. Trans.",
|
"alt_transport": "Alt. Trans.",
|
||||||
"estimate_sent_approval": "Estimate Sent for Approval",
|
|
||||||
"estimate_approved": "Estimate Approved",
|
|
||||||
"area_of_damage_impact": {
|
"area_of_damage_impact": {
|
||||||
"10": "Left Front Side",
|
"10": "Left Front Side",
|
||||||
"11": "Left Front Corner",
|
"11": "Left Front Corner",
|
||||||
@@ -1783,6 +1791,8 @@
|
|||||||
"est_ct_ln": "Estimator Last Name",
|
"est_ct_ln": "Estimator Last Name",
|
||||||
"est_ea": "Estimator Email",
|
"est_ea": "Estimator Email",
|
||||||
"est_ph1": "Estimator Phone #",
|
"est_ph1": "Estimator Phone #",
|
||||||
|
"estimate_approved": "Estimate Approved",
|
||||||
|
"estimate_sent_approval": "Estimate Sent for Approval",
|
||||||
"federal_tax_payable": "Federal Tax Payable",
|
"federal_tax_payable": "Federal Tax Payable",
|
||||||
"federal_tax_rate": "Federal Tax Rate",
|
"federal_tax_rate": "Federal Tax Rate",
|
||||||
"flat_rate_ats": "Flat Rate ATS?",
|
"flat_rate_ats": "Flat Rate ATS?",
|
||||||
@@ -1966,8 +1976,6 @@
|
|||||||
"scheddates": "Schedule Dates"
|
"scheddates": "Schedule Dates"
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"sent": "",
|
|
||||||
"approved": "",
|
|
||||||
"accountsreceivable": "Accounts Receivable",
|
"accountsreceivable": "Accounts Receivable",
|
||||||
"act_price_ppc": "New Part Price",
|
"act_price_ppc": "New Part Price",
|
||||||
"actual_completion_inferred": "$t(jobs.fields.actual_completion) inferred using $t(jobs.fields.scheduled_completion).",
|
"actual_completion_inferred": "$t(jobs.fields.actual_completion) inferred using $t(jobs.fields.scheduled_completion).",
|
||||||
@@ -1982,6 +1990,7 @@
|
|||||||
"alreadyaddedtoscoreboard": "Job has already been added to scoreboard. Saving will update the previous entry.",
|
"alreadyaddedtoscoreboard": "Job has already been added to scoreboard. Saving will update the previous entry.",
|
||||||
"alreadyclosed": "This Job has already been closed.",
|
"alreadyclosed": "This Job has already been closed.",
|
||||||
"appointmentconfirmation": "Send confirmation to customer?",
|
"appointmentconfirmation": "Send confirmation to customer?",
|
||||||
|
"approved": "",
|
||||||
"associationwarning": "Any changes to associations will require updating the data from the new parent record to the Job.",
|
"associationwarning": "Any changes to associations will require updating the data from the new parent record to the Job.",
|
||||||
"audit": "Audit Trail",
|
"audit": "Audit Trail",
|
||||||
"available": "Available",
|
"available": "Available",
|
||||||
@@ -2172,6 +2181,7 @@
|
|||||||
"sales": "Sales",
|
"sales": "Sales",
|
||||||
"savebeforeconversion": "You have unsaved changes on the Job. Please save them before converting it. ",
|
"savebeforeconversion": "You have unsaved changes on the Job. Please save them before converting it. ",
|
||||||
"scheduledinchange": "The scheduled in is based off the latest appointment. To change this date, please schedule or reschedule the Job. ",
|
"scheduledinchange": "The scheduled in is based off the latest appointment. To change this date, please schedule or reschedule the Job. ",
|
||||||
|
"sent": "",
|
||||||
"specialcoveragepolicy": "Special Coverage Policy Applies",
|
"specialcoveragepolicy": "Special Coverage Policy Applies",
|
||||||
"state_tax_amt": "Provincial/State Taxes",
|
"state_tax_amt": "Provincial/State Taxes",
|
||||||
"subletsnotcompleted": "Outstanding Sublets",
|
"subletsnotcompleted": "Outstanding Sublets",
|
||||||
@@ -2388,15 +2398,16 @@
|
|||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"invalidphone": "The phone number is invalid. Unable to open conversation. ",
|
"invalidphone": "The phone number is invalid. Unable to open conversation. ",
|
||||||
|
"no_consent": "This phone number has opted-out of Messaging.",
|
||||||
"noattachedjobs": "No Jobs have been associated to this conversation. ",
|
"noattachedjobs": "No Jobs have been associated to this conversation. ",
|
||||||
"updatinglabel": "Error updating label. {{error}}",
|
"updatinglabel": "Error updating label. {{error}}"
|
||||||
"no_consent": "This phone number has opted-out of Messaging."
|
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"addlabel": "Add a label to this conversation.",
|
"addlabel": "Add a label to this conversation.",
|
||||||
"archive": "Archive",
|
"archive": "Archive",
|
||||||
"maxtenimages": "You can only select up to a maximum of 10 images at a time.",
|
"maxtenimages": "You can only select up to a maximum of 10 images at a time.",
|
||||||
"messaging": "Messaging",
|
"messaging": "Messaging",
|
||||||
|
"no_consent": "Opted-out",
|
||||||
"noallowtxt": "This customer has not indicated their permission to be messaged.",
|
"noallowtxt": "This customer has not indicated their permission to be messaged.",
|
||||||
"nojobs": "Not associated to any Job.",
|
"nojobs": "Not associated to any Job.",
|
||||||
"nopush": "Polling Mode Enabled",
|
"nopush": "Polling Mode Enabled",
|
||||||
@@ -2406,8 +2417,7 @@
|
|||||||
"selectmedia": "Select Media",
|
"selectmedia": "Select Media",
|
||||||
"sentby": "Sent by {{by}} at {{time}}",
|
"sentby": "Sent by {{by}} at {{time}}",
|
||||||
"typeamessage": "Send a message...",
|
"typeamessage": "Send a message...",
|
||||||
"unarchive": "Unarchive",
|
"unarchive": "Unarchive"
|
||||||
"no_consent": "Opted-out"
|
|
||||||
},
|
},
|
||||||
"render": {
|
"render": {
|
||||||
"conversation_list": "Conversation List"
|
"conversation_list": "Conversation List"
|
||||||
@@ -2427,6 +2437,7 @@
|
|||||||
"fields": {
|
"fields": {
|
||||||
"createdby": "Created By",
|
"createdby": "Created By",
|
||||||
"critical": "Critical",
|
"critical": "Critical",
|
||||||
|
"pinned": "Pinned",
|
||||||
"private": "Private",
|
"private": "Private",
|
||||||
"text": "Contents",
|
"text": "Contents",
|
||||||
"type": "Type",
|
"type": "Type",
|
||||||
@@ -2445,6 +2456,7 @@
|
|||||||
"addtorelatedro": "Add to Related ROs",
|
"addtorelatedro": "Add to Related ROs",
|
||||||
"newnoteplaceholder": "Add a note...",
|
"newnoteplaceholder": "Add a note...",
|
||||||
"notetoadd": "Note to Add",
|
"notetoadd": "Note to Add",
|
||||||
|
"pinned_note": "Pinned Note",
|
||||||
"systemnotes": "System Notes",
|
"systemnotes": "System Notes",
|
||||||
"usernotes": "User Notes"
|
"usernotes": "User Notes"
|
||||||
},
|
},
|
||||||
@@ -2467,11 +2479,15 @@
|
|||||||
"fcm": "Push"
|
"fcm": "Push"
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"auto-add": "Automatically watch Jobs I import",
|
|
||||||
"auto-add-success": "Auto watcher status successfully changed.",
|
|
||||||
"auto-add-failure": "Something went wrong updating your auto watcher status.",
|
|
||||||
"add-watchers": "Add Watchers",
|
"add-watchers": "Add Watchers",
|
||||||
"add-watchers-team": "Add Team Members",
|
"add-watchers-team": "Add Team Members",
|
||||||
|
"auto-add": "Automatically watch Jobs I import",
|
||||||
|
"auto-add-description": "",
|
||||||
|
"auto-add-failure": "Something went wrong updating your auto watcher status.",
|
||||||
|
"auto-add-off": "",
|
||||||
|
"auto-add-on": "",
|
||||||
|
"auto-add-success": "Auto watcher status successfully changed.",
|
||||||
|
"employee-notification": "Notifications are disabled because you do not have an associated Employee record.",
|
||||||
"employee-search": "Search for an Employee",
|
"employee-search": "Search for an Employee",
|
||||||
"mark-all-read": "Mark All Read",
|
"mark-all-read": "Mark All Read",
|
||||||
"new-notification-title": "New Notification:",
|
"new-notification-title": "New Notification:",
|
||||||
@@ -2488,8 +2504,7 @@
|
|||||||
"teams-search": "Search for a Team",
|
"teams-search": "Search for a Team",
|
||||||
"unwatch": "Unwatch",
|
"unwatch": "Unwatch",
|
||||||
"watch": "Watch",
|
"watch": "Watch",
|
||||||
"watching-issue": "Watching",
|
"watching-issue": "Watching"
|
||||||
"employee-notification": "Notifications are disabled because you do not have an associated Employee record."
|
|
||||||
},
|
},
|
||||||
"scenarios": {
|
"scenarios": {
|
||||||
"alternate-transport-changed": "Alternate Transport Changed",
|
"alternate-transport-changed": "Alternate Transport Changed",
|
||||||
@@ -3299,17 +3314,10 @@
|
|||||||
"updated": "Scoreboard updated."
|
"updated": "Scoreboard updated."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "Phone Number Opt-Out List"
|
||||||
|
},
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"labels": {
|
|
||||||
"my_tasks_center": "Task Center",
|
|
||||||
"go_to_job": "Go to Job",
|
|
||||||
"overdue": "Overdue",
|
|
||||||
"due_today": "Today",
|
|
||||||
"upcoming": "Upcoming",
|
|
||||||
"no_due_date": "Incomplete",
|
|
||||||
"ro-number": "RO #{{ro_number}}",
|
|
||||||
"no_tasks": "No Tasks Found"
|
|
||||||
},
|
|
||||||
"actions": {
|
"actions": {
|
||||||
"edit": "Edit Task",
|
"edit": "Edit Task",
|
||||||
"new": "New Task",
|
"new": "New Task",
|
||||||
@@ -3324,9 +3332,6 @@
|
|||||||
"myTasks": "Mine",
|
"myTasks": "Mine",
|
||||||
"refresh": "Refresh"
|
"refresh": "Refresh"
|
||||||
},
|
},
|
||||||
"errors": {
|
|
||||||
"load_failure": "Failed to load Tasks."
|
|
||||||
},
|
|
||||||
"date_presets": {
|
"date_presets": {
|
||||||
"completion": "Completion",
|
"completion": "Completion",
|
||||||
"day": "Day",
|
"day": "Day",
|
||||||
@@ -3340,6 +3345,9 @@
|
|||||||
"tomorrow": "Tomorrow",
|
"tomorrow": "Tomorrow",
|
||||||
"two_weeks": "Two Weeks"
|
"two_weeks": "Two Weeks"
|
||||||
},
|
},
|
||||||
|
"errors": {
|
||||||
|
"load_failure": "Failed to load Tasks."
|
||||||
|
},
|
||||||
"failures": {
|
"failures": {
|
||||||
"completed": "Failed to toggle Task completion.",
|
"completed": "Failed to toggle Task completion.",
|
||||||
"created": "Failed to create Task.",
|
"created": "Failed to create Task.",
|
||||||
@@ -3374,6 +3382,16 @@
|
|||||||
"remind_at": "Remind At",
|
"remind_at": "Remind At",
|
||||||
"title": "Title"
|
"title": "Title"
|
||||||
},
|
},
|
||||||
|
"labels": {
|
||||||
|
"due_today": "Today",
|
||||||
|
"go_to_job": "Go to Job",
|
||||||
|
"my_tasks_center": "Task Center",
|
||||||
|
"no_due_date": "Incomplete",
|
||||||
|
"no_tasks": "No Tasks Found",
|
||||||
|
"overdue": "Overdue",
|
||||||
|
"ro-number": "RO #{{ro_number}}",
|
||||||
|
"upcoming": "Upcoming"
|
||||||
|
},
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"assigned_to": "Select an Employee",
|
"assigned_to": "Select an Employee",
|
||||||
"billid": "Select a Bill",
|
"billid": "Select a Bill",
|
||||||
@@ -3763,7 +3781,9 @@
|
|||||||
"actions": {
|
"actions": {
|
||||||
"changepassword": "Change Password",
|
"changepassword": "Change Password",
|
||||||
"signout": "Sign Out",
|
"signout": "Sign Out",
|
||||||
"updateprofile": "Update Profile"
|
"updateprofile": "Update Profile",
|
||||||
|
"light_theme": "Switch to Light Theme",
|
||||||
|
"dark_theme": "Switch to Dark Theme"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"updating": "Error updating user or association {{message}}"
|
"updating": "Error updating user or association {{message}}"
|
||||||
@@ -3873,6 +3893,7 @@
|
|||||||
"state": "Province/State",
|
"state": "Province/State",
|
||||||
"street1": "Street",
|
"street1": "Street",
|
||||||
"street2": "Address 2",
|
"street2": "Address 2",
|
||||||
|
"tags": "Tags",
|
||||||
"taxid": "Tax ID",
|
"taxid": "Tax ID",
|
||||||
"terms": "Payment Terms",
|
"terms": "Payment Terms",
|
||||||
"zip": "Zip/Postal Code"
|
"zip": "Zip/Postal Code"
|
||||||
@@ -3889,18 +3910,6 @@
|
|||||||
"validation": {
|
"validation": {
|
||||||
"unique_vendor_name": "You must enter a unique vendor name."
|
"unique_vendor_name": "You must enter a unique vendor name."
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"consent": {
|
|
||||||
"phone_number": "Phone Number",
|
|
||||||
"associated_owners": "Associated Owners",
|
|
||||||
"created_at": "Opt-Out Date",
|
|
||||||
"no_owners": "No Associated Owners",
|
|
||||||
"phone_1": "Phone 1",
|
|
||||||
"phone_2": "Phone 2",
|
|
||||||
"text_body": "Users can opt out of receiving SMS messages by replying with keywords such as STOP, UNSUBSCRIBE, CANCEL, END, QUIT, STOPALL, REVOKE and OPTOUT. To opt back in, users can reply with START, YES, or UNSTOP. Even after opting out, users can still send messages to us, which will be received and processed as needed. Ensure customers are informed to reply with these keywords to manage their messaging preferences. After opting out, users receive a confirmation message and will not receive further messages until they opt back in."
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"title": "Phone Number Opt-Out List"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -426,6 +426,11 @@
|
|||||||
"messagingtext": "",
|
"messagingtext": "",
|
||||||
"noteslabel": "",
|
"noteslabel": "",
|
||||||
"notestext": "",
|
"notestext": "",
|
||||||
|
"notifications": {
|
||||||
|
"description": "",
|
||||||
|
"invalid_followers": "",
|
||||||
|
"placeholder": ""
|
||||||
|
},
|
||||||
"partslocation": "",
|
"partslocation": "",
|
||||||
"phone": "",
|
"phone": "",
|
||||||
"prodtargethrs": "",
|
"prodtargethrs": "",
|
||||||
@@ -512,6 +517,7 @@
|
|||||||
"dashboard": "",
|
"dashboard": "",
|
||||||
"rbac": "",
|
"rbac": "",
|
||||||
"reportcenter": "",
|
"reportcenter": "",
|
||||||
|
"responsibilitycenter": "",
|
||||||
"templates": "",
|
"templates": "",
|
||||||
"vendors": ""
|
"vendors": ""
|
||||||
},
|
},
|
||||||
@@ -648,15 +654,9 @@
|
|||||||
"use_paint_scale_data": "",
|
"use_paint_scale_data": "",
|
||||||
"uselocalmediaserver": "",
|
"uselocalmediaserver": "",
|
||||||
"website": "",
|
"website": "",
|
||||||
"zip_post": "",
|
"zip_post": ""
|
||||||
"notifications": {
|
|
||||||
"description": "",
|
|
||||||
"placeholder": "",
|
|
||||||
"invalid_followers": ""
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"consent_settings": "",
|
|
||||||
"2tiername": "",
|
"2tiername": "",
|
||||||
"2tiersetup": "",
|
"2tiersetup": "",
|
||||||
"2tiersource": "",
|
"2tiersource": "",
|
||||||
@@ -667,6 +667,7 @@
|
|||||||
"apptcolors": "",
|
"apptcolors": "",
|
||||||
"businessinformation": "",
|
"businessinformation": "",
|
||||||
"checklists": "",
|
"checklists": "",
|
||||||
|
"consent_settings": "",
|
||||||
"csiq": "",
|
"csiq": "",
|
||||||
"customtemplates": "",
|
"customtemplates": "",
|
||||||
"defaultcostsmapping": "",
|
"defaultcostsmapping": "",
|
||||||
@@ -704,6 +705,9 @@
|
|||||||
"messagingpresets": "",
|
"messagingpresets": "",
|
||||||
"notemplatesavailable": "",
|
"notemplatesavailable": "",
|
||||||
"notespresets": "",
|
"notespresets": "",
|
||||||
|
"notifications": {
|
||||||
|
"followers": ""
|
||||||
|
},
|
||||||
"orderstatuses": "",
|
"orderstatuses": "",
|
||||||
"partslocations": "",
|
"partslocations": "",
|
||||||
"partsscan": "",
|
"partsscan": "",
|
||||||
@@ -734,10 +738,7 @@
|
|||||||
"ssbuckets": "",
|
"ssbuckets": "",
|
||||||
"systemsettings": "",
|
"systemsettings": "",
|
||||||
"task-presets": "",
|
"task-presets": "",
|
||||||
"workingdays": "",
|
"workingdays": ""
|
||||||
"notifications": {
|
|
||||||
"followers": ""
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"operations": {
|
"operations": {
|
||||||
"contains": "",
|
"contains": "",
|
||||||
@@ -783,6 +784,15 @@
|
|||||||
"completed": ""
|
"completed": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"consent": {
|
||||||
|
"associated_owners": "",
|
||||||
|
"created_at": "",
|
||||||
|
"no_owners": "",
|
||||||
|
"phone_1": "",
|
||||||
|
"phone_2": "",
|
||||||
|
"phone_number": "",
|
||||||
|
"text_body": ""
|
||||||
|
},
|
||||||
"contracts": {
|
"contracts": {
|
||||||
"actions": {
|
"actions": {
|
||||||
"changerate": "",
|
"changerate": "",
|
||||||
@@ -1235,11 +1245,11 @@
|
|||||||
"fcm": "",
|
"fcm": "",
|
||||||
"notfound": "",
|
"notfound": "",
|
||||||
"sizelimit": "",
|
"sizelimit": "",
|
||||||
"submit-for-testing": "",
|
|
||||||
"sub_status": {
|
"sub_status": {
|
||||||
"expired": "",
|
"expired": "",
|
||||||
"trial-expired": ""
|
"trial-expired": ""
|
||||||
}
|
},
|
||||||
|
"submit-for-testing": ""
|
||||||
},
|
},
|
||||||
"itemtypes": {
|
"itemtypes": {
|
||||||
"contract": "",
|
"contract": "",
|
||||||
@@ -1651,8 +1661,6 @@
|
|||||||
"voiding": ""
|
"voiding": ""
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"estimate_sent_approval": "",
|
|
||||||
"estimate_approved": "",
|
|
||||||
"active_tasks": "",
|
"active_tasks": "",
|
||||||
"actual_completion": "Realización real",
|
"actual_completion": "Realización real",
|
||||||
"actual_delivery": "Entrega real",
|
"actual_delivery": "Entrega real",
|
||||||
@@ -1783,6 +1791,8 @@
|
|||||||
"est_ct_ln": "Apellido del tasador",
|
"est_ct_ln": "Apellido del tasador",
|
||||||
"est_ea": "Correo electrónico del tasador",
|
"est_ea": "Correo electrónico del tasador",
|
||||||
"est_ph1": "Número de teléfono del tasador",
|
"est_ph1": "Número de teléfono del tasador",
|
||||||
|
"estimate_approved": "",
|
||||||
|
"estimate_sent_approval": "",
|
||||||
"federal_tax_payable": "Impuesto federal por pagar",
|
"federal_tax_payable": "Impuesto federal por pagar",
|
||||||
"federal_tax_rate": "",
|
"federal_tax_rate": "",
|
||||||
"flat_rate_ats": "",
|
"flat_rate_ats": "",
|
||||||
@@ -1966,8 +1976,6 @@
|
|||||||
"scheddates": ""
|
"scheddates": ""
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"sent": "",
|
|
||||||
"approved": "",
|
|
||||||
"accountsreceivable": "",
|
"accountsreceivable": "",
|
||||||
"act_price_ppc": "",
|
"act_price_ppc": "",
|
||||||
"actual_completion_inferred": "",
|
"actual_completion_inferred": "",
|
||||||
@@ -1982,6 +1990,7 @@
|
|||||||
"alreadyaddedtoscoreboard": "",
|
"alreadyaddedtoscoreboard": "",
|
||||||
"alreadyclosed": "",
|
"alreadyclosed": "",
|
||||||
"appointmentconfirmation": "¿Enviar confirmación al cliente?",
|
"appointmentconfirmation": "¿Enviar confirmación al cliente?",
|
||||||
|
"approved": "",
|
||||||
"associationwarning": "",
|
"associationwarning": "",
|
||||||
"audit": "",
|
"audit": "",
|
||||||
"available": "",
|
"available": "",
|
||||||
@@ -2172,6 +2181,7 @@
|
|||||||
"sales": "",
|
"sales": "",
|
||||||
"savebeforeconversion": "",
|
"savebeforeconversion": "",
|
||||||
"scheduledinchange": "",
|
"scheduledinchange": "",
|
||||||
|
"sent": "",
|
||||||
"specialcoveragepolicy": "",
|
"specialcoveragepolicy": "",
|
||||||
"state_tax_amt": "",
|
"state_tax_amt": "",
|
||||||
"subletsnotcompleted": "",
|
"subletsnotcompleted": "",
|
||||||
@@ -2388,15 +2398,16 @@
|
|||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"invalidphone": "",
|
"invalidphone": "",
|
||||||
|
"no_consent": "",
|
||||||
"noattachedjobs": "",
|
"noattachedjobs": "",
|
||||||
"updatinglabel": "",
|
"updatinglabel": ""
|
||||||
"no_consent": ""
|
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"addlabel": "",
|
"addlabel": "",
|
||||||
"archive": "",
|
"archive": "",
|
||||||
"maxtenimages": "",
|
"maxtenimages": "",
|
||||||
"messaging": "Mensajería",
|
"messaging": "Mensajería",
|
||||||
|
"no_consent": "",
|
||||||
"noallowtxt": "",
|
"noallowtxt": "",
|
||||||
"nojobs": "",
|
"nojobs": "",
|
||||||
"nopush": "",
|
"nopush": "",
|
||||||
@@ -2406,8 +2417,7 @@
|
|||||||
"selectmedia": "",
|
"selectmedia": "",
|
||||||
"sentby": "",
|
"sentby": "",
|
||||||
"typeamessage": "Enviar un mensaje...",
|
"typeamessage": "Enviar un mensaje...",
|
||||||
"unarchive": "",
|
"unarchive": ""
|
||||||
"no_consent": ""
|
|
||||||
},
|
},
|
||||||
"render": {
|
"render": {
|
||||||
"conversation_list": ""
|
"conversation_list": ""
|
||||||
@@ -2427,6 +2437,7 @@
|
|||||||
"fields": {
|
"fields": {
|
||||||
"createdby": "Creado por",
|
"createdby": "Creado por",
|
||||||
"critical": "Crítico",
|
"critical": "Crítico",
|
||||||
|
"pinned": "",
|
||||||
"private": "Privado",
|
"private": "Privado",
|
||||||
"text": "Contenido",
|
"text": "Contenido",
|
||||||
"type": "",
|
"type": "",
|
||||||
@@ -2445,6 +2456,7 @@
|
|||||||
"addtorelatedro": "",
|
"addtorelatedro": "",
|
||||||
"newnoteplaceholder": "Agrega una nota...",
|
"newnoteplaceholder": "Agrega una nota...",
|
||||||
"notetoadd": "",
|
"notetoadd": "",
|
||||||
|
"pinned_note": "",
|
||||||
"systemnotes": "",
|
"systemnotes": "",
|
||||||
"usernotes": ""
|
"usernotes": ""
|
||||||
},
|
},
|
||||||
@@ -2467,13 +2479,15 @@
|
|||||||
"fcm": ""
|
"fcm": ""
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"auto-add-on": "",
|
|
||||||
"auto-add-off": "",
|
|
||||||
"auto-add-success": "",
|
|
||||||
"auto-add-failure": "",
|
|
||||||
"auto-add-description": "",
|
|
||||||
"add-watchers": "",
|
"add-watchers": "",
|
||||||
"add-watchers-team": "",
|
"add-watchers-team": "",
|
||||||
|
"auto-add": "",
|
||||||
|
"auto-add-description": "",
|
||||||
|
"auto-add-failure": "",
|
||||||
|
"auto-add-off": "",
|
||||||
|
"auto-add-on": "",
|
||||||
|
"auto-add-success": "",
|
||||||
|
"employee-notification": "",
|
||||||
"employee-search": "",
|
"employee-search": "",
|
||||||
"mark-all-read": "",
|
"mark-all-read": "",
|
||||||
"new-notification-title": "",
|
"new-notification-title": "",
|
||||||
@@ -2490,8 +2504,7 @@
|
|||||||
"teams-search": "",
|
"teams-search": "",
|
||||||
"unwatch": "",
|
"unwatch": "",
|
||||||
"watch": "",
|
"watch": "",
|
||||||
"watching-issue": "",
|
"watching-issue": ""
|
||||||
"employee-notification": ""
|
|
||||||
},
|
},
|
||||||
"scenarios": {
|
"scenarios": {
|
||||||
"alternate-transport-changed": "",
|
"alternate-transport-changed": "",
|
||||||
@@ -3301,17 +3314,10 @@
|
|||||||
"updated": ""
|
"updated": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"labels": {
|
|
||||||
"my_tasks_center": "",
|
|
||||||
"go_to_job": "",
|
|
||||||
"overdue": "",
|
|
||||||
"due_today": "",
|
|
||||||
"upcoming": "",
|
|
||||||
"no_due_date": "",
|
|
||||||
"ro-number": "",
|
|
||||||
"no_tasks": ""
|
|
||||||
},
|
|
||||||
"actions": {
|
"actions": {
|
||||||
"edit": "",
|
"edit": "",
|
||||||
"new": "",
|
"new": "",
|
||||||
@@ -3326,9 +3332,6 @@
|
|||||||
"myTasks": "",
|
"myTasks": "",
|
||||||
"refresh": ""
|
"refresh": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
|
||||||
"load_failure": ""
|
|
||||||
},
|
|
||||||
"date_presets": {
|
"date_presets": {
|
||||||
"completion": "",
|
"completion": "",
|
||||||
"day": "",
|
"day": "",
|
||||||
@@ -3342,6 +3345,9 @@
|
|||||||
"tomorrow": "",
|
"tomorrow": "",
|
||||||
"two_weeks": ""
|
"two_weeks": ""
|
||||||
},
|
},
|
||||||
|
"errors": {
|
||||||
|
"load_failure": ""
|
||||||
|
},
|
||||||
"failures": {
|
"failures": {
|
||||||
"completed": "",
|
"completed": "",
|
||||||
"created": "",
|
"created": "",
|
||||||
@@ -3376,6 +3382,16 @@
|
|||||||
"remind_at": "",
|
"remind_at": "",
|
||||||
"title": ""
|
"title": ""
|
||||||
},
|
},
|
||||||
|
"labels": {
|
||||||
|
"due_today": "",
|
||||||
|
"go_to_job": "",
|
||||||
|
"my_tasks_center": "",
|
||||||
|
"no_due_date": "",
|
||||||
|
"no_tasks": "",
|
||||||
|
"overdue": "",
|
||||||
|
"ro-number": "",
|
||||||
|
"upcoming": ""
|
||||||
|
},
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"assigned_to": "",
|
"assigned_to": "",
|
||||||
"billid": "",
|
"billid": "",
|
||||||
@@ -3765,7 +3781,9 @@
|
|||||||
"actions": {
|
"actions": {
|
||||||
"changepassword": "",
|
"changepassword": "",
|
||||||
"signout": "desconectar",
|
"signout": "desconectar",
|
||||||
"updateprofile": "Actualización del perfil"
|
"updateprofile": "Actualización del perfil",
|
||||||
|
"light_theme": "",
|
||||||
|
"dark_theme": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"updating": ""
|
"updating": ""
|
||||||
@@ -3875,6 +3893,7 @@
|
|||||||
"state": "Provincia del estado",
|
"state": "Provincia del estado",
|
||||||
"street1": "calle",
|
"street1": "calle",
|
||||||
"street2": "Dirección 2",
|
"street2": "Dirección 2",
|
||||||
|
"tags": "",
|
||||||
"taxid": "Identificación del impuesto",
|
"taxid": "Identificación del impuesto",
|
||||||
"terms": "Términos de pago",
|
"terms": "Términos de pago",
|
||||||
"zip": "código postal"
|
"zip": "código postal"
|
||||||
@@ -3891,18 +3910,6 @@
|
|||||||
"validation": {
|
"validation": {
|
||||||
"unique_vendor_name": ""
|
"unique_vendor_name": ""
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"consent": {
|
|
||||||
"phone_number": "",
|
|
||||||
"associated_owners": "",
|
|
||||||
"created_at": "",
|
|
||||||
"no_owners": "",
|
|
||||||
"phone_1": "",
|
|
||||||
"phone_2": "",
|
|
||||||
"text_body": ""
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"title": ""
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -426,6 +426,11 @@
|
|||||||
"messagingtext": "",
|
"messagingtext": "",
|
||||||
"noteslabel": "",
|
"noteslabel": "",
|
||||||
"notestext": "",
|
"notestext": "",
|
||||||
|
"notifications": {
|
||||||
|
"description": "",
|
||||||
|
"invalid_followers": "",
|
||||||
|
"placeholder": ""
|
||||||
|
},
|
||||||
"partslocation": "",
|
"partslocation": "",
|
||||||
"phone": "",
|
"phone": "",
|
||||||
"prodtargethrs": "",
|
"prodtargethrs": "",
|
||||||
@@ -512,6 +517,7 @@
|
|||||||
"dashboard": "",
|
"dashboard": "",
|
||||||
"rbac": "",
|
"rbac": "",
|
||||||
"reportcenter": "",
|
"reportcenter": "",
|
||||||
|
"responsibilitycenter": "",
|
||||||
"templates": "",
|
"templates": "",
|
||||||
"vendors": ""
|
"vendors": ""
|
||||||
},
|
},
|
||||||
@@ -648,15 +654,9 @@
|
|||||||
"use_paint_scale_data": "",
|
"use_paint_scale_data": "",
|
||||||
"uselocalmediaserver": "",
|
"uselocalmediaserver": "",
|
||||||
"website": "",
|
"website": "",
|
||||||
"zip_post": "",
|
"zip_post": ""
|
||||||
"notifications": {
|
|
||||||
"description": "",
|
|
||||||
"placeholder": "",
|
|
||||||
"invalid_followers": ""
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"consent_settings": "",
|
|
||||||
"2tiername": "",
|
"2tiername": "",
|
||||||
"2tiersetup": "",
|
"2tiersetup": "",
|
||||||
"2tiersource": "",
|
"2tiersource": "",
|
||||||
@@ -667,6 +667,7 @@
|
|||||||
"apptcolors": "",
|
"apptcolors": "",
|
||||||
"businessinformation": "",
|
"businessinformation": "",
|
||||||
"checklists": "",
|
"checklists": "",
|
||||||
|
"consent_settings": "",
|
||||||
"csiq": "",
|
"csiq": "",
|
||||||
"customtemplates": "",
|
"customtemplates": "",
|
||||||
"defaultcostsmapping": "",
|
"defaultcostsmapping": "",
|
||||||
@@ -704,6 +705,9 @@
|
|||||||
"messagingpresets": "",
|
"messagingpresets": "",
|
||||||
"notemplatesavailable": "",
|
"notemplatesavailable": "",
|
||||||
"notespresets": "",
|
"notespresets": "",
|
||||||
|
"notifications": {
|
||||||
|
"followers": ""
|
||||||
|
},
|
||||||
"orderstatuses": "",
|
"orderstatuses": "",
|
||||||
"partslocations": "",
|
"partslocations": "",
|
||||||
"partsscan": "",
|
"partsscan": "",
|
||||||
@@ -734,10 +738,7 @@
|
|||||||
"ssbuckets": "",
|
"ssbuckets": "",
|
||||||
"systemsettings": "",
|
"systemsettings": "",
|
||||||
"task-presets": "",
|
"task-presets": "",
|
||||||
"workingdays": "",
|
"workingdays": ""
|
||||||
"notifications": {
|
|
||||||
"followers": ""
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"operations": {
|
"operations": {
|
||||||
"contains": "",
|
"contains": "",
|
||||||
@@ -783,6 +784,15 @@
|
|||||||
"completed": ""
|
"completed": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"consent": {
|
||||||
|
"associated_owners": "Associated Owners",
|
||||||
|
"created_at": "Opt-Out Date",
|
||||||
|
"no_owners": "No Associated Owners",
|
||||||
|
"phone_1": "Phone 1",
|
||||||
|
"phone_2": "Phone 2",
|
||||||
|
"phone_number": "Phone Number",
|
||||||
|
"text_body": ""
|
||||||
|
},
|
||||||
"contracts": {
|
"contracts": {
|
||||||
"actions": {
|
"actions": {
|
||||||
"changerate": "",
|
"changerate": "",
|
||||||
@@ -1235,11 +1245,11 @@
|
|||||||
"fcm": "",
|
"fcm": "",
|
||||||
"notfound": "",
|
"notfound": "",
|
||||||
"sizelimit": "",
|
"sizelimit": "",
|
||||||
"submit-for-testing": "",
|
|
||||||
"sub_status": {
|
"sub_status": {
|
||||||
"expired": "",
|
"expired": "",
|
||||||
"trial-expired": ""
|
"trial-expired": ""
|
||||||
}
|
},
|
||||||
|
"submit-for-testing": ""
|
||||||
},
|
},
|
||||||
"itemtypes": {
|
"itemtypes": {
|
||||||
"contract": "",
|
"contract": "",
|
||||||
@@ -1651,8 +1661,6 @@
|
|||||||
"voiding": ""
|
"voiding": ""
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"estimate_sent_approval": "",
|
|
||||||
"estimate_approved": "",
|
|
||||||
"active_tasks": "",
|
"active_tasks": "",
|
||||||
"actual_completion": "Achèvement réel",
|
"actual_completion": "Achèvement réel",
|
||||||
"actual_delivery": "Livraison réelle",
|
"actual_delivery": "Livraison réelle",
|
||||||
@@ -1783,6 +1791,8 @@
|
|||||||
"est_ct_ln": "Nom de l'évaluateur",
|
"est_ct_ln": "Nom de l'évaluateur",
|
||||||
"est_ea": "Courriel de l'évaluateur",
|
"est_ea": "Courriel de l'évaluateur",
|
||||||
"est_ph1": "Numéro de téléphone de l'évaluateur",
|
"est_ph1": "Numéro de téléphone de l'évaluateur",
|
||||||
|
"estimate_approved": "",
|
||||||
|
"estimate_sent_approval": "",
|
||||||
"federal_tax_payable": "Impôt fédéral à payer",
|
"federal_tax_payable": "Impôt fédéral à payer",
|
||||||
"federal_tax_rate": "",
|
"federal_tax_rate": "",
|
||||||
"flat_rate_ats": "",
|
"flat_rate_ats": "",
|
||||||
@@ -1966,8 +1976,6 @@
|
|||||||
"scheddates": ""
|
"scheddates": ""
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"sent": "",
|
|
||||||
"approved": "",
|
|
||||||
"accountsreceivable": "",
|
"accountsreceivable": "",
|
||||||
"act_price_ppc": "",
|
"act_price_ppc": "",
|
||||||
"actual_completion_inferred": "",
|
"actual_completion_inferred": "",
|
||||||
@@ -1982,6 +1990,7 @@
|
|||||||
"alreadyaddedtoscoreboard": "",
|
"alreadyaddedtoscoreboard": "",
|
||||||
"alreadyclosed": "",
|
"alreadyclosed": "",
|
||||||
"appointmentconfirmation": "Envoyer une confirmation au client?",
|
"appointmentconfirmation": "Envoyer une confirmation au client?",
|
||||||
|
"approved": "",
|
||||||
"associationwarning": "",
|
"associationwarning": "",
|
||||||
"audit": "",
|
"audit": "",
|
||||||
"available": "",
|
"available": "",
|
||||||
@@ -2172,6 +2181,7 @@
|
|||||||
"sales": "",
|
"sales": "",
|
||||||
"savebeforeconversion": "",
|
"savebeforeconversion": "",
|
||||||
"scheduledinchange": "",
|
"scheduledinchange": "",
|
||||||
|
"sent": "",
|
||||||
"specialcoveragepolicy": "",
|
"specialcoveragepolicy": "",
|
||||||
"state_tax_amt": "",
|
"state_tax_amt": "",
|
||||||
"subletsnotcompleted": "",
|
"subletsnotcompleted": "",
|
||||||
@@ -2388,15 +2398,16 @@
|
|||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"invalidphone": "",
|
"invalidphone": "",
|
||||||
|
"no_consent": "",
|
||||||
"noattachedjobs": "",
|
"noattachedjobs": "",
|
||||||
"updatinglabel": "",
|
"updatinglabel": ""
|
||||||
"no_consent": ""
|
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"addlabel": "",
|
"addlabel": "",
|
||||||
"archive": "",
|
"archive": "",
|
||||||
"maxtenimages": "",
|
"maxtenimages": "",
|
||||||
"messaging": "Messagerie",
|
"messaging": "Messagerie",
|
||||||
|
"no_consent": "",
|
||||||
"noallowtxt": "",
|
"noallowtxt": "",
|
||||||
"nojobs": "",
|
"nojobs": "",
|
||||||
"nopush": "",
|
"nopush": "",
|
||||||
@@ -2406,8 +2417,7 @@
|
|||||||
"selectmedia": "",
|
"selectmedia": "",
|
||||||
"sentby": "",
|
"sentby": "",
|
||||||
"typeamessage": "Envoyer un message...",
|
"typeamessage": "Envoyer un message...",
|
||||||
"unarchive": "",
|
"unarchive": ""
|
||||||
"no_consent": ""
|
|
||||||
},
|
},
|
||||||
"render": {
|
"render": {
|
||||||
"conversation_list": ""
|
"conversation_list": ""
|
||||||
@@ -2427,6 +2437,7 @@
|
|||||||
"fields": {
|
"fields": {
|
||||||
"createdby": "Créé par",
|
"createdby": "Créé par",
|
||||||
"critical": "Critique",
|
"critical": "Critique",
|
||||||
|
"pinned": "",
|
||||||
"private": "privé",
|
"private": "privé",
|
||||||
"text": "Contenu",
|
"text": "Contenu",
|
||||||
"type": "",
|
"type": "",
|
||||||
@@ -2445,6 +2456,7 @@
|
|||||||
"addtorelatedro": "",
|
"addtorelatedro": "",
|
||||||
"newnoteplaceholder": "Ajouter une note...",
|
"newnoteplaceholder": "Ajouter une note...",
|
||||||
"notetoadd": "",
|
"notetoadd": "",
|
||||||
|
"pinned_note": "",
|
||||||
"systemnotes": "",
|
"systemnotes": "",
|
||||||
"usernotes": ""
|
"usernotes": ""
|
||||||
},
|
},
|
||||||
@@ -2467,13 +2479,15 @@
|
|||||||
"fcm": ""
|
"fcm": ""
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"auto-add-on": "",
|
|
||||||
"auto-add-off": "",
|
|
||||||
"auto-add-success": "",
|
|
||||||
"auto-add-failure": "",
|
|
||||||
"auto-add-description": "",
|
|
||||||
"add-watchers": "",
|
"add-watchers": "",
|
||||||
"add-watchers-team": "",
|
"add-watchers-team": "",
|
||||||
|
"auto-add": "",
|
||||||
|
"auto-add-description": "",
|
||||||
|
"auto-add-failure": "",
|
||||||
|
"auto-add-off": "",
|
||||||
|
"auto-add-on": "",
|
||||||
|
"auto-add-success": "",
|
||||||
|
"employee-notification": "",
|
||||||
"employee-search": "",
|
"employee-search": "",
|
||||||
"mark-all-read": "",
|
"mark-all-read": "",
|
||||||
"new-notification-title": "",
|
"new-notification-title": "",
|
||||||
@@ -2490,8 +2504,7 @@
|
|||||||
"teams-search": "",
|
"teams-search": "",
|
||||||
"unwatch": "",
|
"unwatch": "",
|
||||||
"watch": "",
|
"watch": "",
|
||||||
"watching-issue": "",
|
"watching-issue": ""
|
||||||
"employee-notification": ""
|
|
||||||
},
|
},
|
||||||
"scenarios": {
|
"scenarios": {
|
||||||
"alternate-transport-changed": "",
|
"alternate-transport-changed": "",
|
||||||
@@ -3301,17 +3314,10 @@
|
|||||||
"updated": ""
|
"updated": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"labels": {
|
|
||||||
"my_tasks_center": "",
|
|
||||||
"go_to_job": "",
|
|
||||||
"overdue": "",
|
|
||||||
"due_today": "",
|
|
||||||
"upcoming": "",
|
|
||||||
"no_due_date": "",
|
|
||||||
"ro-number": "",
|
|
||||||
"no_tasks": ""
|
|
||||||
},
|
|
||||||
"actions": {
|
"actions": {
|
||||||
"edit": "",
|
"edit": "",
|
||||||
"new": "",
|
"new": "",
|
||||||
@@ -3326,9 +3332,6 @@
|
|||||||
"myTasks": "",
|
"myTasks": "",
|
||||||
"refresh": ""
|
"refresh": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
|
||||||
"load_failure": ""
|
|
||||||
},
|
|
||||||
"date_presets": {
|
"date_presets": {
|
||||||
"completion": "",
|
"completion": "",
|
||||||
"day": "",
|
"day": "",
|
||||||
@@ -3342,6 +3345,9 @@
|
|||||||
"tomorrow": "",
|
"tomorrow": "",
|
||||||
"two_weeks": ""
|
"two_weeks": ""
|
||||||
},
|
},
|
||||||
|
"errors": {
|
||||||
|
"load_failure": ""
|
||||||
|
},
|
||||||
"failures": {
|
"failures": {
|
||||||
"completed": "",
|
"completed": "",
|
||||||
"created": "",
|
"created": "",
|
||||||
@@ -3376,6 +3382,16 @@
|
|||||||
"remind_at": "",
|
"remind_at": "",
|
||||||
"title": ""
|
"title": ""
|
||||||
},
|
},
|
||||||
|
"labels": {
|
||||||
|
"due_today": "",
|
||||||
|
"go_to_job": "",
|
||||||
|
"my_tasks_center": "",
|
||||||
|
"no_due_date": "",
|
||||||
|
"no_tasks": "",
|
||||||
|
"overdue": "",
|
||||||
|
"ro-number": "",
|
||||||
|
"upcoming": ""
|
||||||
|
},
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"assigned_to": "",
|
"assigned_to": "",
|
||||||
"billid": "",
|
"billid": "",
|
||||||
@@ -3765,7 +3781,9 @@
|
|||||||
"actions": {
|
"actions": {
|
||||||
"changepassword": "",
|
"changepassword": "",
|
||||||
"signout": "Déconnexion",
|
"signout": "Déconnexion",
|
||||||
"updateprofile": "Mettre à jour le profil"
|
"updateprofile": "Mettre à jour le profil",
|
||||||
|
"light_theme": "",
|
||||||
|
"dark_theme": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"updating": ""
|
"updating": ""
|
||||||
@@ -3875,6 +3893,7 @@
|
|||||||
"state": "Etat / Province",
|
"state": "Etat / Province",
|
||||||
"street1": "rue",
|
"street1": "rue",
|
||||||
"street2": "Adresse 2 ",
|
"street2": "Adresse 2 ",
|
||||||
|
"tags": "",
|
||||||
"taxid": "Identifiant de taxe",
|
"taxid": "Identifiant de taxe",
|
||||||
"terms": "Modalités de paiement",
|
"terms": "Modalités de paiement",
|
||||||
"zip": "Zip / code postal"
|
"zip": "Zip / code postal"
|
||||||
@@ -3891,18 +3910,6 @@
|
|||||||
"validation": {
|
"validation": {
|
||||||
"unique_vendor_name": ""
|
"unique_vendor_name": ""
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"consent": {
|
|
||||||
"phone_number": "Phone Number",
|
|
||||||
"associated_owners": "Associated Owners",
|
|
||||||
"created_at": "Opt-Out Date",
|
|
||||||
"no_owners": "No Associated Owners",
|
|
||||||
"phone_1": "Phone 1",
|
|
||||||
"phone_2": "Phone 2",
|
|
||||||
"text_body": ""
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"title": ""
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,24 +48,24 @@ export default async function RenderTemplate(
|
|||||||
...(renderAsHtml
|
...(renderAsHtml
|
||||||
? {}
|
? {}
|
||||||
: {
|
: {
|
||||||
recipe: "chrome-pdf",
|
recipe: "chrome-pdf",
|
||||||
...(!ignoreCustomMargins && {
|
...(!ignoreCustomMargins && {
|
||||||
chrome: {
|
chrome: {
|
||||||
marginTop:
|
marginTop:
|
||||||
bodyshop.logo_img_path &&
|
bodyshop.logo_img_path &&
|
||||||
bodyshop.logo_img_path.headerMargin &&
|
bodyshop.logo_img_path.headerMargin &&
|
||||||
bodyshop.logo_img_path.headerMargin > 36
|
bodyshop.logo_img_path.headerMargin > 36
|
||||||
? bodyshop.logo_img_path.headerMargin
|
? bodyshop.logo_img_path.headerMargin
|
||||||
: "36px",
|
: "36px",
|
||||||
marginBottom:
|
marginBottom:
|
||||||
bodyshop.logo_img_path &&
|
bodyshop.logo_img_path &&
|
||||||
bodyshop.logo_img_path.footerMargin &&
|
bodyshop.logo_img_path.footerMargin &&
|
||||||
bodyshop.logo_img_path.footerMargin > 50
|
bodyshop.logo_img_path.footerMargin > 50
|
||||||
? bodyshop.logo_img_path.footerMargin
|
? bodyshop.logo_img_path.footerMargin
|
||||||
: "50px"
|
: "50px"
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
...(renderAsExcel ? { recipe: "html-to-xlsx" } : {}),
|
...(renderAsExcel ? { recipe: "html-to-xlsx" } : {}),
|
||||||
...(renderAsText ? { recipe: "text" } : {})
|
...(renderAsText ? { recipe: "text" } : {})
|
||||||
},
|
},
|
||||||
@@ -100,14 +100,14 @@ export default async function RenderTemplate(
|
|||||||
chrome: {
|
chrome: {
|
||||||
marginTop:
|
marginTop:
|
||||||
bodyshop.logo_img_path &&
|
bodyshop.logo_img_path &&
|
||||||
bodyshop.logo_img_path.headerMargin &&
|
bodyshop.logo_img_path.headerMargin &&
|
||||||
bodyshop.logo_img_path.headerMargin > 36
|
bodyshop.logo_img_path.headerMargin > 36
|
||||||
? bodyshop.logo_img_path.headerMargin
|
? bodyshop.logo_img_path.headerMargin
|
||||||
: "36px",
|
: "36px",
|
||||||
marginBottom:
|
marginBottom:
|
||||||
bodyshop.logo_img_path &&
|
bodyshop.logo_img_path &&
|
||||||
bodyshop.logo_img_path.footerMargin &&
|
bodyshop.logo_img_path.footerMargin &&
|
||||||
bodyshop.logo_img_path.footerMargin > 50
|
bodyshop.logo_img_path.footerMargin > 50
|
||||||
? bodyshop.logo_img_path.footerMargin
|
? bodyshop.logo_img_path.footerMargin
|
||||||
: "50px"
|
: "50px"
|
||||||
}
|
}
|
||||||
@@ -182,22 +182,22 @@ export async function RenderTemplates(templateObjects, bodyshop, renderAsHtml =
|
|||||||
...(renderAsHtml
|
...(renderAsHtml
|
||||||
? {}
|
? {}
|
||||||
: {
|
: {
|
||||||
recipe: "chrome-pdf",
|
recipe: "chrome-pdf",
|
||||||
chrome: {
|
chrome: {
|
||||||
marginTop:
|
marginTop:
|
||||||
bodyshop.logo_img_path &&
|
bodyshop.logo_img_path &&
|
||||||
bodyshop.logo_img_path.headerMargin &&
|
bodyshop.logo_img_path.headerMargin &&
|
||||||
bodyshop.logo_img_path.headerMargin > 36
|
bodyshop.logo_img_path.headerMargin > 36
|
||||||
? bodyshop.logo_img_path.headerMargin
|
? bodyshop.logo_img_path.headerMargin
|
||||||
: "36px",
|
: "36px",
|
||||||
marginBottom:
|
marginBottom:
|
||||||
bodyshop.logo_img_path &&
|
bodyshop.logo_img_path &&
|
||||||
bodyshop.logo_img_path.footerMargin &&
|
bodyshop.logo_img_path.footerMargin &&
|
||||||
bodyshop.logo_img_path.footerMargin > 50
|
bodyshop.logo_img_path.footerMargin > 50
|
||||||
? bodyshop.logo_img_path.footerMargin
|
? bodyshop.logo_img_path.footerMargin
|
||||||
: "50px"
|
: "50px"
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
pdfOperations: [
|
pdfOperations: [
|
||||||
{
|
{
|
||||||
template: {
|
template: {
|
||||||
@@ -213,14 +213,14 @@ export async function RenderTemplates(templateObjects, bodyshop, renderAsHtml =
|
|||||||
chrome: {
|
chrome: {
|
||||||
marginTop:
|
marginTop:
|
||||||
bodyshop.logo_img_path &&
|
bodyshop.logo_img_path &&
|
||||||
bodyshop.logo_img_path.headerMargin &&
|
bodyshop.logo_img_path.headerMargin &&
|
||||||
bodyshop.logo_img_path.headerMargin > 36
|
bodyshop.logo_img_path.headerMargin > 36
|
||||||
? bodyshop.logo_img_path.headerMargin
|
? bodyshop.logo_img_path.headerMargin
|
||||||
: "36px",
|
: "36px",
|
||||||
marginBottom:
|
marginBottom:
|
||||||
bodyshop.logo_img_path &&
|
bodyshop.logo_img_path &&
|
||||||
bodyshop.logo_img_path.footerMargin &&
|
bodyshop.logo_img_path.footerMargin &&
|
||||||
bodyshop.logo_img_path.footerMargin > 50
|
bodyshop.logo_img_path.footerMargin > 50
|
||||||
? bodyshop.logo_img_path.footerMargin
|
? bodyshop.logo_img_path.footerMargin
|
||||||
: "50px"
|
: "50px"
|
||||||
},
|
},
|
||||||
@@ -302,7 +302,6 @@ export const fetchFilterData = async ({ name }) => {
|
|||||||
const jsReportFilters = await cleanAxios.get(`${server}/odata/assets?$filter=name eq '${name}.filters'`, {
|
const jsReportFilters = await cleanAxios.get(`${server}/odata/assets?$filter=name eq '${name}.filters'`, {
|
||||||
headers: { Authorization: jsrAuth }
|
headers: { Authorization: jsrAuth }
|
||||||
});
|
});
|
||||||
console.log("🚀 ~ fetchFilterData ~ jsReportFilters:", jsReportFilters);
|
|
||||||
|
|
||||||
let parsedFilterData;
|
let parsedFilterData;
|
||||||
let useShopSpecificTemplate = false;
|
let useShopSpecificTemplate = false;
|
||||||
|
|||||||
@@ -4909,6 +4909,7 @@
|
|||||||
- critical
|
- critical
|
||||||
- id
|
- id
|
||||||
- jobid
|
- jobid
|
||||||
|
- pinned
|
||||||
- private
|
- private
|
||||||
- text
|
- text
|
||||||
- type
|
- type
|
||||||
@@ -4923,6 +4924,7 @@
|
|||||||
- critical
|
- critical
|
||||||
- id
|
- id
|
||||||
- jobid
|
- jobid
|
||||||
|
- pinned
|
||||||
- private
|
- private
|
||||||
- text
|
- text
|
||||||
- type
|
- type
|
||||||
@@ -4947,6 +4949,7 @@
|
|||||||
- critical
|
- critical
|
||||||
- id
|
- id
|
||||||
- jobid
|
- jobid
|
||||||
|
- pinned
|
||||||
- private
|
- private
|
||||||
- text
|
- text
|
||||||
- type
|
- type
|
||||||
@@ -7120,6 +7123,7 @@
|
|||||||
- state
|
- state
|
||||||
- street1
|
- street1
|
||||||
- street2
|
- street2
|
||||||
|
- tags
|
||||||
- updated_at
|
- updated_at
|
||||||
- zip
|
- zip
|
||||||
select_permissions:
|
select_permissions:
|
||||||
@@ -7143,6 +7147,7 @@
|
|||||||
- state
|
- state
|
||||||
- street1
|
- street1
|
||||||
- street2
|
- street2
|
||||||
|
- tags
|
||||||
- updated_at
|
- updated_at
|
||||||
- zip
|
- zip
|
||||||
filter:
|
filter:
|
||||||
@@ -7176,6 +7181,7 @@
|
|||||||
- state
|
- state
|
||||||
- street1
|
- street1
|
||||||
- street2
|
- street2
|
||||||
|
- tags
|
||||||
- updated_at
|
- updated_at
|
||||||
- zip
|
- zip
|
||||||
filter:
|
filter:
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- Could not auto-generate a down migration.
|
||||||
|
-- Please write an appropriate down migration for the SQL below:
|
||||||
|
-- alter table "public"."notes" add column "pinned" boolean
|
||||||
|
-- not null default 'false';
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
alter table "public"."notes" add column "pinned" boolean
|
||||||
|
not null default 'false';
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- Could not auto-generate a down migration.
|
||||||
|
-- Please write an appropriate down migration for the SQL below:
|
||||||
|
-- alter table "public"."vendors" add column "tags" jsonb
|
||||||
|
-- not null default jsonb_build_array();
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
alter table "public"."vendors" add column "tags" jsonb
|
||||||
|
not null default jsonb_build_array();
|
||||||
@@ -10,7 +10,6 @@ const queries = require("../../graphql-client/queries");
|
|||||||
const { refresh: refreshOauthToken, setNewRefreshToken } = require("./qbo-callback");
|
const { refresh: refreshOauthToken, setNewRefreshToken } = require("./qbo-callback");
|
||||||
const OAuthClient = require("intuit-oauth");
|
const OAuthClient = require("intuit-oauth");
|
||||||
const moment = require("moment-timezone");
|
const moment = require("moment-timezone");
|
||||||
const GraphQLClient = require("graphql-request").GraphQLClient;
|
|
||||||
const {
|
const {
|
||||||
QueryInsuranceCo,
|
QueryInsuranceCo,
|
||||||
InsertInsuranceCo,
|
InsertInsuranceCo,
|
||||||
@@ -28,7 +27,7 @@ exports.default = async (req, res) => {
|
|||||||
clientId: process.env.QBO_CLIENT_ID,
|
clientId: process.env.QBO_CLIENT_ID,
|
||||||
clientSecret: process.env.QBO_SECRET,
|
clientSecret: process.env.QBO_SECRET,
|
||||||
environment: process.env.NODE_ENV === "production" ? "production" : "sandbox",
|
environment: process.env.NODE_ENV === "production" ? "production" : "sandbox",
|
||||||
redirectUri: process.env.QBO_REDIRECT_URI,
|
redirectUri: process.env.QBO_REDIRECT_URI
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
//Fetch the API Access Tokens & Set them for the session.
|
//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.
|
// //No error. Mark the payment exported & insert export log.
|
||||||
if (elgen) {
|
if (elgen) {
|
||||||
const result = await client
|
await client.setHeaders({ Authorization: BearerToken }).request(queries.QBO_MARK_PAYMENT_EXPORTED, {
|
||||||
.setHeaders({ Authorization: BearerToken })
|
paymentId: payment.id,
|
||||||
.request(queries.QBO_MARK_PAYMENT_EXPORTED, {
|
payment: {
|
||||||
paymentId: payment.id,
|
exportedat: moment().tz(bodyshop.timezone)
|
||||||
payment: {
|
},
|
||||||
exportedat: moment().tz(bodyshop.timezone)
|
logs: [
|
||||||
},
|
{
|
||||||
logs: [
|
bodyshopid: bodyshop.id,
|
||||||
{
|
paymentid: payment.id,
|
||||||
bodyshopid: bodyshop.id,
|
successful: true,
|
||||||
paymentid: payment.id,
|
useremail: req.user.email
|
||||||
successful: true,
|
}
|
||||||
useremail: req.user.email
|
]
|
||||||
}
|
});
|
||||||
]
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ret.push({ paymentid: payment.id, success: true });
|
ret.push({ paymentid: payment.id, success: true });
|
||||||
@@ -156,7 +153,7 @@ exports.default = async (req, res) => {
|
|||||||
});
|
});
|
||||||
//Add the export log error.
|
//Add the export log error.
|
||||||
if (elgen) {
|
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: [
|
logs: [
|
||||||
{
|
{
|
||||||
bodyshopid: bodyshop.id,
|
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(
|
const { paymentMethods, invoices } = await QueryMetaData(
|
||||||
oauthClient,
|
oauthClient,
|
||||||
qbo_realmId,
|
qbo_realmId,
|
||||||
@@ -227,20 +224,20 @@ async function InsertPayment(oauthClient, qbo_realmId, req, payment, parentRef,
|
|||||||
PaymentRefNum: payment.transactionid,
|
PaymentRefNum: payment.transactionid,
|
||||||
...(invoices && invoices.length === 1 && invoices[0]
|
...(invoices && invoices.length === 1 && invoices[0]
|
||||||
? {
|
? {
|
||||||
Line: [
|
Line: [
|
||||||
{
|
{
|
||||||
Amount: Dinero({
|
Amount: Dinero({
|
||||||
amount: Math.round(payment.amount * 100)
|
amount: Math.round(payment.amount * 100)
|
||||||
}).toFormat(DineroQbFormat),
|
}).toFormat(DineroQbFormat),
|
||||||
LinkedTxn: [
|
LinkedTxn: [
|
||||||
{
|
{
|
||||||
TxnId: invoices[0].Id,
|
TxnId: invoices[0].Id,
|
||||||
TxnType: "Invoice"
|
TxnType: "Invoice"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
: {})
|
: {})
|
||||||
};
|
};
|
||||||
logger.log("qbo-payments-objectlog", "DEBUG", req.user.email, payment.id, {
|
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,
|
status: result.response?.status,
|
||||||
bodyshopid: payment.job.shopid,
|
bodyshopid: payment.job.shopid,
|
||||||
email: req.user.email
|
email: req.user.email
|
||||||
})
|
});
|
||||||
setNewRefreshToken(req.user.email, result);
|
setNewRefreshToken(req.user.email, result);
|
||||||
return result && result.Bill;
|
return result && result.Bill;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -291,7 +288,7 @@ async function QueryMetaData(oauthClient, qbo_realmId, req, ro_number, isCreditM
|
|||||||
status: invoice.response?.status,
|
status: invoice.response?.status,
|
||||||
bodyshopid,
|
bodyshopid,
|
||||||
email: req.user.email
|
email: req.user.email
|
||||||
})
|
});
|
||||||
const paymentMethods = await oauthClient.makeApiCall({
|
const paymentMethods = await oauthClient.makeApiCall({
|
||||||
url: urlBuilder(qbo_realmId, "query", `select * From PaymentMethod`),
|
url: urlBuilder(qbo_realmId, "query", `select * From PaymentMethod`),
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -306,7 +303,7 @@ async function QueryMetaData(oauthClient, qbo_realmId, req, ro_number, isCreditM
|
|||||||
status: paymentMethods.response?.status,
|
status: paymentMethods.response?.status,
|
||||||
bodyshopid,
|
bodyshopid,
|
||||||
email: req.user.email
|
email: req.user.email
|
||||||
})
|
});
|
||||||
setNewRefreshToken(req.user.email, paymentMethods);
|
setNewRefreshToken(req.user.email, paymentMethods);
|
||||||
|
|
||||||
// const classes = await oauthClient.makeApiCall({
|
// const classes = await oauthClient.makeApiCall({
|
||||||
@@ -358,7 +355,7 @@ async function QueryMetaData(oauthClient, qbo_realmId, req, ro_number, isCreditM
|
|||||||
status: taxCodes.response?.status,
|
status: taxCodes.response?.status,
|
||||||
bodyshopid,
|
bodyshopid,
|
||||||
email: req.user.email
|
email: req.user.email
|
||||||
})
|
});
|
||||||
const items = await oauthClient.makeApiCall({
|
const items = await oauthClient.makeApiCall({
|
||||||
url: urlBuilder(qbo_realmId, "query", `select * From Item`),
|
url: urlBuilder(qbo_realmId, "query", `select * From Item`),
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -373,7 +370,7 @@ async function QueryMetaData(oauthClient, qbo_realmId, req, ro_number, isCreditM
|
|||||||
status: items.response?.status,
|
status: items.response?.status,
|
||||||
bodyshopid,
|
bodyshopid,
|
||||||
email: req.user.email
|
email: req.user.email
|
||||||
})
|
});
|
||||||
setNewRefreshToken(req.user.email, items);
|
setNewRefreshToken(req.user.email, items);
|
||||||
|
|
||||||
const itemMapping = {};
|
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) {
|
async function InsertCreditMemo(oauthClient, qbo_realmId, req, payment, parentRef) {
|
||||||
const { paymentMethods, invoices, items, taxCodes } = await QueryMetaData(
|
const { invoices, items, taxCodes } = await QueryMetaData(
|
||||||
oauthClient,
|
oauthClient,
|
||||||
qbo_realmId,
|
qbo_realmId,
|
||||||
req,
|
req,
|
||||||
@@ -449,14 +446,14 @@ async function InsertCreditMemo(oauthClient, qbo_realmId, req, payment, parentRe
|
|||||||
TaxCodeRef: {
|
TaxCodeRef: {
|
||||||
value:
|
value:
|
||||||
taxCodes[
|
taxCodes[
|
||||||
findTaxCode(
|
findTaxCode(
|
||||||
{
|
{
|
||||||
local: false,
|
local: false,
|
||||||
federal: false,
|
federal: false,
|
||||||
state: false
|
state: false
|
||||||
},
|
},
|
||||||
payment.job.bodyshop.md_responsibility_centers.sales_tax_codes
|
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,
|
status: result.response?.status,
|
||||||
bodyshopid: req.user.bodyshopid,
|
bodyshopid: req.user.bodyshopid,
|
||||||
email: req.user.email
|
email: req.user.email
|
||||||
})
|
});
|
||||||
setNewRefreshToken(req.user.email, result);
|
setNewRefreshToken(req.user.email, result);
|
||||||
return result && result.Bill;
|
return result && result.Bill;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("qbo-payables-error", "DEBUG", req.user.email, payment.id, {
|
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"
|
method: "InsertCreditMemo"
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ const queries = require("../graphql-client/queries");
|
|||||||
const Dinero = require("dinero.js");
|
const Dinero = require("dinero.js");
|
||||||
const moment = require("moment-timezone");
|
const moment = require("moment-timezone");
|
||||||
var builder = require("xmlbuilder2");
|
var builder = require("xmlbuilder2");
|
||||||
const _ = require("lodash");
|
|
||||||
const logger = require("../utils/logger");
|
const logger = require("../utils/logger");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
require("dotenv").config({
|
require("dotenv").config({
|
||||||
@@ -16,6 +15,7 @@ const { sendServerEmail } = require("../email/sendemail");
|
|||||||
|
|
||||||
const AHDineroFormat = "0.00";
|
const AHDineroFormat = "0.00";
|
||||||
const AhDateFormat = "MMDDYYYY";
|
const AhDateFormat = "MMDDYYYY";
|
||||||
|
const NON_ASCII_REGEX = /[^\x20-\x7E]/g;
|
||||||
|
|
||||||
const repairOpCodes = ["OP4", "OP9", "OP10"];
|
const repairOpCodes = ["OP4", "OP9", "OP10"];
|
||||||
const replaceOpCodes = ["OP2", "OP5", "OP11", "OP12"];
|
const replaceOpCodes = ["OP2", "OP5", "OP11", "OP12"];
|
||||||
@@ -37,13 +37,11 @@ const ftpSetup = {
|
|||||||
exports.default = async (req, res) => {
|
exports.default = async (req, res) => {
|
||||||
// Only process if in production environment.
|
// Only process if in production environment.
|
||||||
if (process.env.NODE_ENV !== "production") {
|
if (process.env.NODE_ENV !== "production") {
|
||||||
res.sendStatus(403);
|
return res.sendStatus(403);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
// Only process if the appropriate token is provided.
|
// Only process if the appropriate token is provided.
|
||||||
if (req.headers["x-imex-auth"] !== process.env.AUTOHOUSE_AUTH_TOKEN) {
|
if (req.headers["x-imex-auth"] !== process.env.AUTOHOUSE_AUTH_TOKEN) {
|
||||||
res.sendStatus(401);
|
return res.sendStatus(401);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send immediate response and continue processing.
|
// Send immediate response and continue processing.
|
||||||
@@ -822,7 +820,7 @@ const GenerateDetailLines = (job, line, statuses) => {
|
|||||||
BackOrdered: line.status === statuses.default_bo ? "1" : "0",
|
BackOrdered: line.status === statuses.default_bo ? "1" : "0",
|
||||||
Cost: (line.billlines[0] && (line.billlines[0].actual_cost * line.billlines[0].quantity).toFixed(2)) || 0,
|
Cost: (line.billlines[0] && (line.billlines[0].actual_cost * line.billlines[0].quantity).toFixed(2)) || 0,
|
||||||
//Critical: null,
|
//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,
|
DiscountMarkup: line.prt_dsmk_m || 0,
|
||||||
InvoiceNumber: line.billlines[0] && line.billlines[0].bill.invoice_number,
|
InvoiceNumber: line.billlines[0] && line.billlines[0].bill.invoice_number,
|
||||||
IOUPart: 0,
|
IOUPart: 0,
|
||||||
@@ -834,7 +832,7 @@ const GenerateDetailLines = (job, line, statuses) => {
|
|||||||
OriginalCost: null,
|
OriginalCost: null,
|
||||||
OriginalInvoiceNumber: null,
|
OriginalInvoiceNumber: null,
|
||||||
PriceEach: line.act_price || 0,
|
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,
|
ProfitPercent: null,
|
||||||
PurchaseOrderNumber: null,
|
PurchaseOrderNumber: null,
|
||||||
Qty: line.part_qty || 0,
|
Qty: line.part_qty || 0,
|
||||||
|
|||||||
408
server/data/carfax.js
Normal file
408
server/data/carfax.js
Normal file
@@ -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;
|
||||||
|
};
|
||||||
@@ -28,13 +28,11 @@ const ftpSetup = {
|
|||||||
exports.default = async (req, res) => {
|
exports.default = async (req, res) => {
|
||||||
// Only process if in production environment.
|
// Only process if in production environment.
|
||||||
if (process.env.NODE_ENV !== "production") {
|
if (process.env.NODE_ENV !== "production") {
|
||||||
res.sendStatus(403);
|
return res.sendStatus(403);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
// Only process if the appropriate token is provided.
|
// Only process if the appropriate token is provided.
|
||||||
if (req.headers["x-imex-auth"] !== process.env.AUTOHOUSE_AUTH_TOKEN) {
|
if (req.headers["x-imex-auth"] !== process.env.AUTOHOUSE_AUTH_TOKEN) {
|
||||||
res.sendStatus(401);
|
return res.sendStatus(401);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send immediate response and continue processing.
|
// Send immediate response and continue processing.
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ const queries = require("../graphql-client/queries");
|
|||||||
const Dinero = require("dinero.js");
|
const Dinero = require("dinero.js");
|
||||||
const moment = require("moment-timezone");
|
const moment = require("moment-timezone");
|
||||||
var builder = require("xmlbuilder2");
|
var builder = require("xmlbuilder2");
|
||||||
const _ = require("lodash");
|
|
||||||
const logger = require("../utils/logger");
|
const logger = require("../utils/logger");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
require("dotenv").config({
|
require("dotenv").config({
|
||||||
@@ -36,13 +35,11 @@ const ftpSetup = {
|
|||||||
exports.default = async (req, res) => {
|
exports.default = async (req, res) => {
|
||||||
// Only process if in production environment.
|
// Only process if in production environment.
|
||||||
if (process.env.NODE_ENV !== "production") {
|
if (process.env.NODE_ENV !== "production") {
|
||||||
res.sendStatus(403);
|
return res.sendStatus(403);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
// Only process if the appropriate token is provided.
|
// Only process if the appropriate token is provided.
|
||||||
if (req.headers["x-imex-auth"] !== process.env.AUTOHOUSE_AUTH_TOKEN) {
|
if (req.headers["x-imex-auth"] !== process.env.AUTOHOUSE_AUTH_TOKEN) {
|
||||||
res.sendStatus(401);
|
return res.sendStatus(401);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send immediate response and continue processing.
|
// Send immediate response and continue processing.
|
||||||
|
|||||||
@@ -6,3 +6,4 @@ exports.kaizen = require("./kaizen").default;
|
|||||||
exports.usageReport = require("./usageReport").default;
|
exports.usageReport = require("./usageReport").default;
|
||||||
exports.podium = require("./podium").default;
|
exports.podium = require("./podium").default;
|
||||||
exports.emsUpload = require("./emsUpload").default;
|
exports.emsUpload = require("./emsUpload").default;
|
||||||
|
exports.carfax = require("./carfax").default;
|
||||||
@@ -3,7 +3,6 @@ const queries = require("../graphql-client/queries");
|
|||||||
const Dinero = require("dinero.js");
|
const Dinero = require("dinero.js");
|
||||||
const moment = require("moment-timezone");
|
const moment = require("moment-timezone");
|
||||||
var builder = require("xmlbuilder2");
|
var builder = require("xmlbuilder2");
|
||||||
const _ = require("lodash");
|
|
||||||
const logger = require("../utils/logger");
|
const logger = require("../utils/logger");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
require("dotenv").config({
|
require("dotenv").config({
|
||||||
@@ -35,13 +34,11 @@ const ftpSetup = {
|
|||||||
exports.default = async (req, res) => {
|
exports.default = async (req, res) => {
|
||||||
// Only process if in production environment.
|
// Only process if in production environment.
|
||||||
if (process.env.NODE_ENV !== "production") {
|
if (process.env.NODE_ENV !== "production") {
|
||||||
res.sendStatus(403);
|
return res.sendStatus(403);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
// Only process if the appropriate token is provided.
|
// Only process if the appropriate token is provided.
|
||||||
if (req.headers["x-imex-auth"] !== process.env.AUTOHOUSE_AUTH_TOKEN) {
|
if (req.headers["x-imex-auth"] !== process.env.AUTOHOUSE_AUTH_TOKEN) {
|
||||||
res.sendStatus(401);
|
return res.sendStatus(401);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send immediate response and continue processing.
|
// Send immediate response and continue processing.
|
||||||
|
|||||||
@@ -29,13 +29,11 @@ const ftpSetup = {
|
|||||||
exports.default = async (req, res) => {
|
exports.default = async (req, res) => {
|
||||||
// Only process if in production environment.
|
// Only process if in production environment.
|
||||||
if (process.env.NODE_ENV !== "production") {
|
if (process.env.NODE_ENV !== "production") {
|
||||||
res.sendStatus(403);
|
return res.sendStatus(403);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
// Only process if the appropriate token is provided.
|
// Only process if the appropriate token is provided.
|
||||||
if (req.headers["x-imex-auth"] !== process.env.AUTOHOUSE_AUTH_TOKEN) {
|
if (req.headers["x-imex-auth"] !== process.env.AUTOHOUSE_AUTH_TOKEN) {
|
||||||
res.sendStatus(401);
|
return res.sendStatus(401);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send immediate response and continue processing.
|
// Send immediate response and continue processing.
|
||||||
|
|||||||
@@ -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) {
|
exports.CLAIMSCORP_QUERY = `query CLAIMSCORP_EXPORT($start: timestamptz, $bodyshopid: uuid!, $end: timestamptz) {
|
||||||
bodyshops_by_pk(id: $bodyshopid){
|
bodyshops_by_pk(id: $bodyshopid){
|
||||||
id
|
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 {
|
exports.GET_CLAIMSCORP_SHOPS = `query GET_CLAIMSCORP_SHOPS {
|
||||||
bodyshops(where: {claimscorpid: {_is_null: false}, _or: {claimscorpid: {_neq: ""}}}){
|
bodyshops(where: {claimscorpid: {_is_null: false}, _or: {claimscorpid: {_neq: ""}}}){
|
||||||
id
|
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 {
|
exports.QUERY_TEMPORARY_DOCS = ` query QUERY_TEMPORARY_DOCS {
|
||||||
documents(where: { jobid: { _is_null: true } }, order_by: { takenat: desc }) {
|
documents(where: { jobid: { _is_null: true } }, order_by: { takenat: desc }) {
|
||||||
|
|||||||
@@ -144,6 +144,7 @@ const paymentRefund = async (req, res) => {
|
|||||||
|
|
||||||
logger.log("intellipay-refund-success", "DEBUG", req.user?.email, null, {
|
logger.log("intellipay-refund-success", "DEBUG", req.user?.email, null, {
|
||||||
requestOptions: options,
|
requestOptions: options,
|
||||||
|
response: response?.data,
|
||||||
...logResponseMeta
|
...logResponseMeta
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const {
|
|||||||
GET_DOCUMENTS_BY_JOB,
|
GET_DOCUMENTS_BY_JOB,
|
||||||
QUERY_TEMPORARY_DOCS,
|
QUERY_TEMPORARY_DOCS,
|
||||||
GET_DOCUMENTS_BY_IDS,
|
GET_DOCUMENTS_BY_IDS,
|
||||||
|
GET_DOCUMENTS_BY_BILL,
|
||||||
DELETE_MEDIA_DOCUMENTS
|
DELETE_MEDIA_DOCUMENTS
|
||||||
} = require("../graphql-client/queries");
|
} = require("../graphql-client/queries");
|
||||||
const yazl = require("yazl");
|
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.
|
//Delayed as the key structure may change slightly from what it is currently and will require evaluating mobile components.
|
||||||
const client = req.userGraphQLClient;
|
const client = req.userGraphQLClient;
|
||||||
//If there's no jobid and no billid, we're in temporary documents.
|
//If there's no jobid and no billid, we're in temporary documents.
|
||||||
const data = await (jobid
|
const data = await (
|
||||||
? client.request(GET_DOCUMENTS_BY_JOB, { jobId: jobid })
|
billid ? client.request(GET_DOCUMENTS_BY_BILL, { billId: billid }) :
|
||||||
: client.request(QUERY_TEMPORARY_DOCS));
|
jobid
|
||||||
|
? client.request(GET_DOCUMENTS_BY_JOB, { jobId: jobid })
|
||||||
|
: client.request(QUERY_TEMPORARY_DOCS));
|
||||||
|
|
||||||
const thumbResizeParams = `rs:fill:250:250:1/g:ce`;
|
const thumbResizeParams = `rs:fill:250:250:1/g:ce`;
|
||||||
const s3client = new S3Client({ region: InstanceRegion() });
|
const s3client = new S3Client({ region: InstanceRegion() });
|
||||||
|
|||||||
@@ -73,37 +73,23 @@ const processCanvasRequest = async (req, res) => {
|
|||||||
// Default width and height
|
// Default width and height
|
||||||
const width = isNumber(w) && w > 0 ? w : 500;
|
const width = isNumber(w) && w > 0 ? w : 500;
|
||||||
const height = isNumber(h) && h > 0 ? h : 275;
|
const height = isNumber(h) && h > 0 ? h : 275;
|
||||||
|
|
||||||
const configuration = getChartConfiguration(keys, values, override);
|
const configuration = getChartConfiguration(keys, values, override);
|
||||||
|
|
||||||
let canvas = null;
|
|
||||||
let ctx = null;
|
|
||||||
let chart = null;
|
let chart = null;
|
||||||
let chartImage = null;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Create the canvas
|
const canvas = new Canvas(width, height);
|
||||||
canvas = new Canvas(width, height);
|
const ctx = canvas.getContext("2d");
|
||||||
ctx = canvas.getContext("2d");
|
|
||||||
|
|
||||||
// Render the chart
|
|
||||||
chart = new Chart(ctx, configuration);
|
chart = new Chart(ctx, configuration);
|
||||||
|
|
||||||
// Generate and send the image
|
const chartImage = (await canvas.toBuffer("image/png")).toString("base64");
|
||||||
chartImage = (await canvas.toBuffer("image/png")).toString("base64");
|
|
||||||
res.status(200).send(`data:image/png;base64,${chartImage}`);
|
res.status(200).send(`data:image/png;base64,${chartImage}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Log the error and send the response
|
|
||||||
logger.log("canvas-error", "error", "jsr", null, { error: error.message });
|
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 {
|
} finally {
|
||||||
// Cleanup resources
|
chart?.destroy();
|
||||||
if (chart) {
|
|
||||||
chart.destroy();
|
|
||||||
}
|
|
||||||
ctx = null;
|
|
||||||
canvas = null;
|
|
||||||
chartImage = null;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -118,15 +104,18 @@ const enqueueRequest = (req, res) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const processNextInQueue = async () => {
|
const processNextInQueue = async () => {
|
||||||
while (requestQueue.length > 0) {
|
try {
|
||||||
const { req, res } = requestQueue.shift();
|
while (requestQueue.length > 0) {
|
||||||
try {
|
const { req, res } = requestQueue.shift();
|
||||||
await processCanvasRequest(req, res);
|
try {
|
||||||
} catch (err) {
|
await processCanvasRequest(req, res);
|
||||||
console.error("canvas-queue-error", "error", "jsr", null, { error: err.message });
|
} catch (err) {
|
||||||
|
console.error("canvas-queue-error", "error", "jsr", null, { error: err.message });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
isProcessing = false;
|
||||||
}
|
}
|
||||||
isProcessing = false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.canvastest = function (req, res) {
|
exports.canvastest = function (req, res) {
|
||||||
@@ -134,7 +123,10 @@ exports.canvastest = function (req, res) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
exports.canvas = async (req, res) => {
|
exports.canvas = async (req, res) => {
|
||||||
if (isProcessing || !enqueueRequest(req, res)) return;
|
if (!enqueueRequest(req, res)) return;
|
||||||
isProcessing = true;
|
|
||||||
processNextInQueue().catch((err) => console.error("canvas-processing-error", { error: err.message }));
|
if (!isProcessing) {
|
||||||
|
isProcessing = true;
|
||||||
|
processNextInQueue().catch((err) => console.error("canvas-processing-error", { error: err.message }));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const router = express.Router();
|
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("/ah", autohouse);
|
||||||
router.post("/cc", claimscorp);
|
router.post("/cc", claimscorp);
|
||||||
@@ -8,5 +8,6 @@ router.post("/chatter", chatter);
|
|||||||
router.post("/kaizen", kaizen);
|
router.post("/kaizen", kaizen);
|
||||||
router.post("/usagereport", usageReport);
|
router.post("/usagereport", usageReport);
|
||||||
router.post("/podium", podium);
|
router.post("/podium", podium);
|
||||||
|
router.post("/carfax", carfax);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
Reference in New Issue
Block a user