Merged in feature/IO-3499-React-19 (pull request #2883)
Feature/IO-3499 React 19
This commit is contained in:
@@ -3,11 +3,10 @@ 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, useMemo } from "react";
|
import { useEffect } from "react";
|
||||||
import { CookiesProvider } from "react-cookie";
|
import { CookiesProvider } from "react-cookie";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect, useSelector } from "react-redux";
|
import { useDispatch, 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 { setDarkMode } from "../redux/application/application.actions";
|
||||||
import { selectDarkMode } from "../redux/application/application.selectors";
|
import { selectDarkMode } from "../redux/application/application.selectors";
|
||||||
@@ -28,93 +27,99 @@ const config = {
|
|||||||
function SplitClientProvider({ children }) {
|
function SplitClientProvider({ children }) {
|
||||||
const imexshopid = useSelector((state) => state.user.imexshopid);
|
const imexshopid = useSelector((state) => state.user.imexshopid);
|
||||||
const splitClient = useSplitClient({ key: imexshopid || "anon" });
|
const splitClient = useSplitClient({ key: imexshopid || "anon" });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (splitClient && imexshopid) {
|
if (import.meta.env.DEV && splitClient && imexshopid) {
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
function AppContainer() {
|
||||||
currentUser: selectCurrentUser
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
|
||||||
setDarkMode: (isDarkMode) => dispatch(setDarkMode(isDarkMode)),
|
|
||||||
signOutStart: () => dispatch(signOutStart())
|
|
||||||
});
|
|
||||||
|
|
||||||
function AppContainer({ currentUser, setDarkMode, signOutStart }) {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const currentUser = useSelector(selectCurrentUser);
|
||||||
const isDarkMode = useSelector(selectDarkMode);
|
const isDarkMode = useSelector(selectDarkMode);
|
||||||
const theme = useMemo(() => getTheme(isDarkMode), [isDarkMode]);
|
|
||||||
|
const theme = () => getTheme(isDarkMode);
|
||||||
|
|
||||||
|
const antdInput = () => ({ autoComplete: "new-password" });
|
||||||
|
|
||||||
|
const antdForm = () => ({
|
||||||
|
validateMessages: {
|
||||||
|
required: t("general.validation.required", { label: "${label}" })
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Global seamless logout listener with redirect to /signin
|
// Global seamless logout listener with redirect to /signin
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleSeamlessLogout = (event) => {
|
const handleSeamlessLogout = (event) => {
|
||||||
if (event.data?.type !== "seamlessLogoutRequest") return;
|
if (event.data?.type !== "seamlessLogoutRequest") return;
|
||||||
|
|
||||||
const requestOrigin = event.origin;
|
// Only accept messages from the parent window
|
||||||
|
if (event.source !== window.parent) return;
|
||||||
|
|
||||||
|
const targetOrigin = event.origin || "*";
|
||||||
|
|
||||||
if (currentUser?.authorized !== true) {
|
if (currentUser?.authorized !== true) {
|
||||||
window.parent.postMessage(
|
window.parent?.postMessage({ type: "seamlessLogoutResponse", status: "already_logged_out" }, targetOrigin);
|
||||||
{ type: "seamlessLogoutResponse", status: "already_logged_out" },
|
|
||||||
requestOrigin || "*"
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
signOutStart();
|
dispatch(signOutStart());
|
||||||
window.parent.postMessage({ type: "seamlessLogoutResponse", status: "logged_out" }, requestOrigin || "*");
|
window.parent?.postMessage({ type: "seamlessLogoutResponse", status: "logged_out" }, targetOrigin);
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener("message", handleSeamlessLogout);
|
window.addEventListener("message", handleSeamlessLogout);
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener("message", handleSeamlessLogout);
|
window.removeEventListener("message", handleSeamlessLogout);
|
||||||
};
|
};
|
||||||
}, [signOutStart, currentUser]);
|
}, [dispatch, currentUser?.authorized]);
|
||||||
|
|
||||||
// Update data-theme attribute
|
// Update data-theme attribute (no cleanup to avoid transient style churn)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.documentElement.setAttribute("data-theme", isDarkMode ? "dark" : "light");
|
document.documentElement.dataset.theme = isDarkMode ? "dark" : "light";
|
||||||
return () => document.documentElement.removeAttribute("data-theme");
|
|
||||||
}, [isDarkMode]);
|
}, [isDarkMode]);
|
||||||
|
|
||||||
// Sync darkMode with localStorage
|
// Sync darkMode with localStorage
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentUser?.uid) {
|
const uid = currentUser?.uid;
|
||||||
const savedMode = localStorage.getItem(`dark-mode-${currentUser.uid}`);
|
|
||||||
if (savedMode !== null) {
|
if (!uid) {
|
||||||
setDarkMode(JSON.parse(savedMode));
|
dispatch(setDarkMode(false));
|
||||||
} else {
|
return;
|
||||||
setDarkMode(false);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setDarkMode(false);
|
|
||||||
}
|
}
|
||||||
}, [currentUser?.uid, setDarkMode]);
|
|
||||||
|
const key = `dark-mode-${uid}`;
|
||||||
|
const raw = localStorage.getItem(key);
|
||||||
|
|
||||||
|
if (raw == null) {
|
||||||
|
dispatch(setDarkMode(false));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
dispatch(setDarkMode(Boolean(JSON.parse(raw))));
|
||||||
|
} catch {
|
||||||
|
dispatch(setDarkMode(false));
|
||||||
|
}
|
||||||
|
}, [currentUser?.uid, dispatch]);
|
||||||
|
|
||||||
// Persist darkMode
|
// Persist darkMode
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentUser?.uid) {
|
const uid = currentUser?.uid;
|
||||||
localStorage.setItem(`dark-mode-${currentUser.uid}`, JSON.stringify(isDarkMode));
|
if (!uid) return;
|
||||||
}
|
|
||||||
|
localStorage.setItem(`dark-mode-${uid}`, JSON.stringify(isDarkMode));
|
||||||
}, [isDarkMode, currentUser?.uid]);
|
}, [isDarkMode, currentUser?.uid]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CookiesProvider>
|
<CookiesProvider>
|
||||||
<ApolloProvider client={client}>
|
<ApolloProvider client={client}>
|
||||||
<ConfigProvider
|
<ConfigProvider input={antdInput} locale={enLocale} theme={theme} form={antdForm}>
|
||||||
input={{ autoComplete: "new-password" }}
|
|
||||||
locale={enLocale}
|
|
||||||
theme={theme}
|
|
||||||
form={{
|
|
||||||
validateMessages: {
|
|
||||||
required: t("general.validation.required", { label: "${label}" })
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<GlobalLoadingBar />
|
<GlobalLoadingBar />
|
||||||
<SplitFactoryProvider config={config}>
|
<SplitFactoryProvider config={config}>
|
||||||
<SplitClientProvider>
|
<SplitClientProvider>
|
||||||
@@ -127,4 +132,4 @@ function AppContainer({ currentUser, setDarkMode, signOutStart }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Sentry.withProfiler(connect(mapStateToProps, mapDispatchToProps)(AppContainer));
|
export default Sentry.withProfiler(AppContainer);
|
||||||
|
|||||||
@@ -77,13 +77,7 @@ export function BillDeleteButton({ bill, jobid, callback, insertAuditTrail }) {
|
|||||||
return (
|
return (
|
||||||
<RbacWrapper action="bills:delete" noauth={<></>}>
|
<RbacWrapper action="bills:delete" noauth={<></>}>
|
||||||
<Popconfirm disabled={bill.exported} onConfirm={handleDelete} title={t("bills.labels.deleteconfirm")}>
|
<Popconfirm disabled={bill.exported} onConfirm={handleDelete} title={t("bills.labels.deleteconfirm")}>
|
||||||
<Button
|
<Button icon={<DeleteFilled />} disabled={bill.exported} loading={loading} />
|
||||||
disabled={bill.exported}
|
|
||||||
// onClick={handleDelete}
|
|
||||||
loading={loading}
|
|
||||||
>
|
|
||||||
<DeleteFilled />
|
|
||||||
</Button>
|
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
</RbacWrapper>
|
</RbacWrapper>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export function BillFormItemsExtendedFormItem({
|
|||||||
if (!value)
|
if (!value)
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
|
icon={<PlusCircleFilled />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const values = form.getFieldsValue("billlineskeys");
|
const values = form.getFieldsValue("billlineskeys");
|
||||||
|
|
||||||
@@ -53,9 +54,7 @@ export function BillFormItemsExtendedFormItem({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
<PlusCircleFilled />
|
|
||||||
</Button>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -196,6 +195,7 @@ export function BillFormItemsExtendedFormItem({
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
icon={<MinusCircleFilled />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const values = form.getFieldsValue("billlineskeys");
|
const values = form.getFieldsValue("billlineskeys");
|
||||||
|
|
||||||
@@ -207,9 +207,7 @@ export function BillFormItemsExtendedFormItem({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
<MinusCircleFilled />
|
|
||||||
</Button>
|
|
||||||
</Space>
|
</Space>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -564,11 +564,10 @@ export function BillEnterModalLinesComponent({
|
|||||||
{() => (
|
{() => (
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<Button
|
<Button
|
||||||
|
icon={<DeleteFilled />}
|
||||||
disabled={disabled || getFieldValue("billlines")[record.fieldKey]?.inventories?.length > 0}
|
disabled={disabled || getFieldValue("billlines")[record.fieldKey]?.inventories?.length > 0}
|
||||||
onClick={() => remove(record.name)}
|
onClick={() => remove(record.name)}
|
||||||
>
|
/>
|
||||||
<DeleteFilled />
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{Simple_Inventory.treatment === "on" && (
|
{Simple_Inventory.treatment === "on" && (
|
||||||
<BilllineAddInventory
|
<BilllineAddInventory
|
||||||
|
|||||||
@@ -124,11 +124,11 @@ export function BilllineAddInventory({ currentUser, bodyshop, billline, disabled
|
|||||||
return (
|
return (
|
||||||
<Tooltip title={t("inventory.actions.addtoinventory")}>
|
<Tooltip title={t("inventory.actions.addtoinventory")}>
|
||||||
<Button
|
<Button
|
||||||
|
icon={<FileAddFilled />}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
disabled={disabled || billline?.inventories?.length >= billline.quantity}
|
disabled={disabled || billline?.inventories?.length >= billline.quantity}
|
||||||
onClick={addToInventory}
|
onClick={addToInventory}
|
||||||
>
|
>
|
||||||
<FileAddFilled />
|
|
||||||
{billline?.inventories?.length > 0 && <div>({billline?.inventories?.length} in inv)</div>}
|
{billline?.inventories?.length > 0 && <div>({billline?.inventories?.length} in inv)</div>}
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -84,15 +84,14 @@ export function BillsListTableComponent({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
icon={<FaTasks />}
|
||||||
<FaTasks />
|
/>
|
||||||
</Button>
|
|
||||||
<BillDeleteButton bill={record} jobid={job.id} />
|
<BillDeleteButton bill={record} jobid={job.id} />
|
||||||
<BillDetailEditReturnComponent
|
<BillDetailEditReturnComponent
|
||||||
data={{ bills_by_pk: { ...record, jobid: job.id, job: job } }}
|
data={{ bills_by_pk: { ...record, jobid: job.id, job: job } }}
|
||||||
disabled={record.is_credit_memo || record.vendorid === bodyshop.inhousevendorid || jobRO}
|
disabled={record.is_credit_memo || record.vendorid === bodyshop.inhousevendorid || jobRO}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{record.isinhouse && (
|
{record.isinhouse && (
|
||||||
<PrintWrapperComponent
|
<PrintWrapperComponent
|
||||||
templateObject={{
|
templateObject={{
|
||||||
@@ -190,9 +189,7 @@ export function BillsListTableComponent({
|
|||||||
title={t("bills.labels.bills")}
|
title={t("bills.labels.bills")}
|
||||||
extra={
|
extra={
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<Button onClick={() => refetch()}>
|
<Button onClick={() => refetch()} icon={<SyncOutlined />} />
|
||||||
<SyncOutlined />
|
|
||||||
</Button>
|
|
||||||
{job && job.converted ? (
|
{job && job.converted ? (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -41,9 +41,7 @@ export default function CABCpvrtCalculator({ disabled, form }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover destroyOnHidden content={popContent} open={visibility} disabled={disabled}>
|
<Popover destroyOnHidden content={popContent} open={visibility} disabled={disabled}>
|
||||||
<Button disabled={disabled} onClick={() => setVisibility(true)}>
|
<Button disabled={disabled} onClick={() => setVisibility(true)} icon={<CalculatorFilled />} />
|
||||||
<CalculatorFilled />
|
|
||||||
</Button>
|
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,11 +12,16 @@ export function ChatAffixContainer({ bodyshop, chatVisible, currentUser }) {
|
|||||||
const client = useApolloClient();
|
const client = useApolloClient();
|
||||||
const { socket } = useSocket();
|
const { socket } = useSocket();
|
||||||
|
|
||||||
// 1) FCM subscription (independent of socket handler registration)
|
const messagingServicesId = bodyshop?.messagingservicesid;
|
||||||
useEffect(() => {
|
const bodyshopId = bodyshop?.id;
|
||||||
if (!bodyshop?.messagingservicesid) return;
|
const imexshopid = bodyshop?.imexshopid;
|
||||||
|
|
||||||
async function subscribeToTopicForFCMNotification() {
|
const messagingEnabled = Boolean(messagingServicesId);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!messagingEnabled) return;
|
||||||
|
|
||||||
|
(async () => {
|
||||||
try {
|
try {
|
||||||
await requestForToken();
|
await requestForToken();
|
||||||
await axios.post("/notifications/subscribe", {
|
await axios.post("/notifications/subscribe", {
|
||||||
@@ -24,23 +29,19 @@ export function ChatAffixContainer({ bodyshop, chatVisible, currentUser }) {
|
|||||||
vapidKey: import.meta.env.VITE_APP_FIREBASE_PUBLIC_VAPID_KEY
|
vapidKey: import.meta.env.VITE_APP_FIREBASE_PUBLIC_VAPID_KEY
|
||||||
}),
|
}),
|
||||||
type: "messaging",
|
type: "messaging",
|
||||||
imexshopid: bodyshop.imexshopid
|
imexshopid
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Error attempting to subscribe to messaging topic: ", error);
|
console.log("Error attempting to subscribe to messaging topic: ", error);
|
||||||
}
|
}
|
||||||
}
|
})();
|
||||||
|
}, [messagingEnabled, imexshopid]);
|
||||||
|
|
||||||
subscribeToTopicForFCMNotification();
|
|
||||||
}, [bodyshop?.messagingservicesid, bodyshop?.imexshopid]);
|
|
||||||
|
|
||||||
// 2) Register socket handlers as soon as socket is connected (regardless of chatVisible)
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!socket) return;
|
if (!socket) return;
|
||||||
if (!bodyshop?.messagingservicesid) return;
|
if (!messagingEnabled) return;
|
||||||
if (!bodyshop?.id) return;
|
if (!bodyshopId) return;
|
||||||
|
|
||||||
// If socket isn't connected yet, ensure no stale handlers remain.
|
|
||||||
if (!socket.connected) {
|
if (!socket.connected) {
|
||||||
unregisterMessagingHandlers({ socket });
|
unregisterMessagingHandlers({ socket });
|
||||||
return;
|
return;
|
||||||
@@ -56,16 +57,14 @@ export function ChatAffixContainer({ bodyshop, chatVisible, currentUser }) {
|
|||||||
bodyshop
|
bodyshop
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => unregisterMessagingHandlers({ socket });
|
||||||
unregisterMessagingHandlers({ socket });
|
}, [socket, messagingEnabled, bodyshopId, client, currentUser?.email, bodyshop]);
|
||||||
};
|
|
||||||
}, [socket, socket?.connected, bodyshop?.id, bodyshop?.messagingservicesid, client, currentUser?.email]);
|
|
||||||
|
|
||||||
if (!bodyshop?.messagingservicesid) return <></>;
|
if (!messagingEnabled) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`chat-affix ${chatVisible ? "chat-affix-open" : ""}`}>
|
<div className={`chat-affix ${chatVisible ? "chat-affix-open" : ""}`}>
|
||||||
{bodyshop?.messagingservicesid ? <ChatPopupComponent /> : null}
|
{messagingEnabled ? <ChatPopupComponent /> : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ function ChatConversationListComponent({ conversationList, selectedConversation,
|
|||||||
onClick={() => setSelectedConversation(item.id)}
|
onClick={() => setSelectedConversation(item.id)}
|
||||||
className={`chat-list-item ${item.id === selectedConversation ? "chat-list-selected-conversation" : ""}`}
|
className={`chat-list-item ${item.id === selectedConversation ? "chat-list-selected-conversation" : ""}`}
|
||||||
>
|
>
|
||||||
<Card style={getCardStyle()} variant={true} size="small" extra={cardExtra} title={cardTitle}>
|
<Card style={getCardStyle()} variant="outlined" size="small" extra={cardExtra} title={cardTitle}>
|
||||||
<div style={{ display: "inline-block", width: "70%", textAlign: "left" }}>{cardContentLeft}</div>
|
<div style={{ display: "inline-block", width: "70%", textAlign: "left" }}>{cardContentLeft}</div>
|
||||||
<div style={{ display: "inline-block", width: "30%", textAlign: "right" }}>{cardContentRight}</div>
|
<div style={{ display: "inline-block", width: "30%", textAlign: "right" }}>{cardContentRight}</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,22 +1,23 @@
|
|||||||
|
import { Button } from "antd";
|
||||||
import parsePhoneNumber from "libphonenumber-js";
|
import parsePhoneNumber from "libphonenumber-js";
|
||||||
|
import { useCallback, useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { openChatByPhone } from "../../redux/messaging/messaging.actions";
|
|
||||||
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
|
||||||
|
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { openChatByPhone } from "../../redux/messaging/messaging.actions";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import { searchingForConversation } from "../../redux/messaging/messaging.selectors";
|
import { searchingForConversation } from "../../redux/messaging/messaging.selectors";
|
||||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
|
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
searchingForConversation: searchingForConversation
|
searchingForConversation
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
openChatByPhone: (phone) => dispatch(openChatByPhone(phone))
|
openChatByPhone: (payload) => dispatch(openChatByPhone(payload))
|
||||||
});
|
});
|
||||||
|
|
||||||
export function ChatOpenButton({ bodyshop, searchingForConversation, phone, type, jobid, openChatByPhone }) {
|
export function ChatOpenButton({ bodyshop, searchingForConversation, phone, type, jobid, openChatByPhone }) {
|
||||||
@@ -24,31 +25,59 @@ export function ChatOpenButton({ bodyshop, searchingForConversation, phone, type
|
|||||||
const { socket } = useSocket();
|
const { socket } = useSocket();
|
||||||
const notification = useNotification();
|
const notification = useNotification();
|
||||||
|
|
||||||
if (!phone) return <></>;
|
if (!phone) return null;
|
||||||
|
|
||||||
if (!bodyshop.messagingservicesid) {
|
const messagingEnabled = Boolean(bodyshop?.messagingservicesid);
|
||||||
return <PhoneNumberFormatter type={type}>{phone}</PhoneNumberFormatter>;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const parsed = useMemo(() => {
|
||||||
|
if (!messagingEnabled) return null;
|
||||||
|
try {
|
||||||
|
return parsePhoneNumber(phone, "CA") || null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}, [messagingEnabled, phone]);
|
||||||
|
|
||||||
|
const isValid = Boolean(parsed?.isValid?.() && parsed.isValid());
|
||||||
|
const clickable = messagingEnabled && !searchingForConversation && isValid;
|
||||||
|
|
||||||
|
const onClick = useCallback(
|
||||||
|
(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (!messagingEnabled) return;
|
||||||
|
if (searchingForConversation) return;
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
notification.error({ title: t("messaging.error.invalidphone") });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
openChatByPhone({
|
||||||
|
phone_num: parsed.formatInternational(),
|
||||||
|
jobid,
|
||||||
|
socket
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[messagingEnabled, searchingForConversation, isValid, parsed, jobid, socket, openChatByPhone, notification, t]
|
||||||
|
);
|
||||||
|
|
||||||
|
const content = <PhoneNumberFormatter type={type}>{phone}</PhoneNumberFormatter>;
|
||||||
|
|
||||||
|
// If not clickable, render plain formatted text (no link styling)
|
||||||
|
if (!clickable) return content;
|
||||||
|
|
||||||
|
// Clickable: render as a link-styled button (best for a “command”)
|
||||||
return (
|
return (
|
||||||
<a
|
<Button
|
||||||
href="# "
|
type="link"
|
||||||
onClick={(e) => {
|
onClick={onClick}
|
||||||
e.preventDefault();
|
className="chat-open-button-link"
|
||||||
e.stopPropagation();
|
aria-label={t("messaging.actions.openchat") || "Open chat"}
|
||||||
|
|
||||||
if (searchingForConversation) return; // Prevent finding the same thing twice.
|
|
||||||
|
|
||||||
const p = parsePhoneNumber(phone, "CA");
|
|
||||||
if (p && p.isValid()) {
|
|
||||||
openChatByPhone({ phone_num: p.formatInternational(), jobid, socket });
|
|
||||||
} else {
|
|
||||||
notification.error({ title: t("messaging.error.invalidphone") });
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<PhoneNumberFormatter type={type}>{phone}</PhoneNumberFormatter>
|
{content}
|
||||||
</a>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -156,9 +156,8 @@ export function ContractsList({ bodyshop, loading, contracts, refetch, total, se
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Button onClick={() => setContractFinderContext()}>{t("contracts.actions.find")}</Button>
|
<Button onClick={() => setContractFinderContext()}>{t("contracts.actions.find")}</Button>
|
||||||
<Button onClick={() => refetch()}>
|
<Button onClick={() => refetch()} icon={<SyncOutlined />} />
|
||||||
<SyncOutlined />
|
|
||||||
</Button>
|
|
||||||
<Input.Search
|
<Input.Search
|
||||||
placeholder={search.searh || t("general.labels.search")}
|
placeholder={search.searh || t("general.labels.search")}
|
||||||
onSearch={(value) => {
|
onSearch={(value) => {
|
||||||
|
|||||||
@@ -255,9 +255,8 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
|
|||||||
title={t("menus.header.courtesycars")}
|
title={t("menus.header.courtesycars")}
|
||||||
extra={
|
extra={
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<Button onClick={() => refetch()}>
|
<Button onClick={() => refetch()} icon={<SyncOutlined />} />
|
||||||
<SyncOutlined />
|
|
||||||
</Button>
|
|
||||||
<Dropdown trigger="click" menu={menu}>
|
<Dropdown trigger="click" menu={menu}>
|
||||||
<Button>{t("general.labels.print")}</Button>
|
<Button>{t("general.labels.print")}</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|||||||
@@ -85,13 +85,7 @@ export default function CsiResponseListPaginated({ refetch, loading, responses,
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card extra={<Button onClick={() => refetch()} icon={<SyncOutlined />} />}>
|
||||||
extra={
|
|
||||||
<Button onClick={() => refetch()}>
|
|
||||||
<SyncOutlined />
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Table
|
<Table
|
||||||
loading={loading}
|
loading={loading}
|
||||||
pagination={{ placement: "top", pageSize: pageLimit, current: parseInt(state.page || 1), total: total }}
|
pagination={{ placement: "top", pageSize: pageLimit, current: parseInt(state.page || 1), total: total }}
|
||||||
|
|||||||
@@ -196,9 +196,7 @@ export function DashboardGridComponent({ currentUser }) {
|
|||||||
<PageHeader
|
<PageHeader
|
||||||
extra={
|
extra={
|
||||||
<Space>
|
<Space>
|
||||||
<Button onClick={() => refetch()}>
|
<Button onClick={() => refetch()} icon={<SyncOutlined />} />
|
||||||
<SyncOutlined />
|
|
||||||
</Button>
|
|
||||||
<Dropdown menu={menu} trigger={["click"]}>
|
<Dropdown menu={menu} trigger={["click"]}>
|
||||||
<Button>{t("dashboard.actions.addcomponent")}</Button>
|
<Button>{t("dashboard.actions.addcomponent")}</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { SyncOutlined } from "@ant-design/icons";
|
import { SyncOutlined } from "@ant-design/icons";
|
||||||
import { Button, Card, Form, Input, Table } from "antd";
|
import { Button, Card, Form, Input, Table } from "antd";
|
||||||
import { useEffect, useState, useRef } from "react";
|
import { useEffect, useRef, 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";
|
||||||
@@ -111,9 +111,8 @@ export function DmsAllocationsSummaryAp({ socket, bodyshop, billids, title }) {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
socket.emit("pbs-calculate-allocations-ap", billids, (ack) => setAllocationsSummary(ack));
|
socket.emit("pbs-calculate-allocations-ap", billids, (ack) => setAllocationsSummary(ack));
|
||||||
}}
|
}}
|
||||||
>
|
icon={<SyncOutlined />}
|
||||||
<SyncOutlined />
|
/>
|
||||||
</Button>
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Table
|
<Table
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Alert, Button, Card, Table, Typography } from "antd";
|
import { Alert, Button, Card, Table, Typography } from "antd";
|
||||||
import { SyncOutlined } from "@ant-design/icons";
|
import { SyncOutlined } from "@ant-design/icons";
|
||||||
import { useCallback, useEffect, useState, useRef } from "react";
|
import { useCallback, useEffect, useRef, 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";
|
||||||
@@ -110,11 +110,7 @@ export function DmsAllocationsSummary({ mode, socket, bodyshop, jobId, title, on
|
|||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
title={title}
|
title={title}
|
||||||
extra={
|
extra={<Button onClick={fetchAllocations} aria-label={t("general.actions.refresh")} icon={<SyncOutlined />} />}
|
||||||
<Button onClick={fetchAllocations} aria-label={t("general.actions.refresh")}>
|
|
||||||
<SyncOutlined />
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{bodyshop.pbs_configuration?.disablebillwip && (
|
{bodyshop.pbs_configuration?.disablebillwip && (
|
||||||
<Alert type="warning" title={t("jobs.labels.dms.disablebillwip")} />
|
<Alert type="warning" title={t("jobs.labels.dms.disablebillwip")} />
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Alert, Button, Card, Table, Tabs, Typography } from "antd";
|
import { Alert, Button, Card, Table, Tabs, Typography } from "antd";
|
||||||
import { SyncOutlined } from "@ant-design/icons";
|
import { SyncOutlined } from "@ant-design/icons";
|
||||||
import { useCallback, useEffect, useMemo, useState, useRef } from "react";
|
import { useCallback, useEffect, useMemo, useRef, 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";
|
||||||
@@ -329,11 +329,7 @@ export function RrAllocationsSummary({ socket, bodyshop, jobId, title, onAllocat
|
|||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
title={title}
|
title={title}
|
||||||
extra={
|
extra={<Button onClick={fetchAllocations} aria-label={t("general.actions.refresh")} icon={<SyncOutlined />} />}
|
||||||
<Button onClick={fetchAllocations} aria-label={t("general.actions.refresh")}>
|
|
||||||
<SyncOutlined />
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{bodyshop.pbs_configuration?.disablebillwip && (
|
{bodyshop.pbs_configuration?.disablebillwip && (
|
||||||
<Alert type="warning" title={t("jobs.labels.dms.disablebillwip")} />
|
<Alert type="warning" title={t("jobs.labels.dms.disablebillwip")} />
|
||||||
|
|||||||
@@ -53,9 +53,7 @@ export default function InventoryLineDelete({ inventoryline, disabled, refetch }
|
|||||||
onConfirm={handleDelete}
|
onConfirm={handleDelete}
|
||||||
title={t("inventory.labels.deleteconfirm")}
|
title={t("inventory.labels.deleteconfirm")}
|
||||||
>
|
>
|
||||||
<Button disabled={disabled || inventoryline.consumedbybillid} loading={loading}>
|
<Button disabled={disabled || inventoryline.consumedbybillid} loading={loading} icon={<DeleteFilled />} />
|
||||||
<DeleteFilled />
|
|
||||||
</Button>
|
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
</RbacWrapper>
|
</RbacWrapper>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -110,9 +110,9 @@ export function JobsList({ refetch, loading, jobs, total, setInventoryUpsertCont
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
icon={<EditFilled />}
|
||||||
<EditFilled />
|
/>
|
||||||
</Button>
|
|
||||||
<InventoryLineDelete inventoryline={record} refetch={refetch} />
|
<InventoryLineDelete inventoryline={record} refetch={refetch} />
|
||||||
</Space>
|
</Space>
|
||||||
)
|
)
|
||||||
@@ -155,9 +155,9 @@ export function JobsList({ refetch, loading, jobs, total, setInventoryUpsertCont
|
|||||||
context: {}
|
context: {}
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
icon={<FileAddFilled />}
|
||||||
<FileAddFilled />
|
/>
|
||||||
</Button>
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const updatedSearch = { ...search };
|
const updatedSearch = { ...search };
|
||||||
@@ -172,9 +172,8 @@ export function JobsList({ refetch, loading, jobs, total, setInventoryUpsertCont
|
|||||||
{search.showall ? t("inventory.labels.showavailable") : t("inventory.labels.showall")}
|
{search.showall ? t("inventory.labels.showavailable") : t("inventory.labels.showall")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button onClick={() => refetch()}>
|
<Button onClick={() => refetch()} icon={<SyncOutlined />} />
|
||||||
<SyncOutlined />
|
|
||||||
</Button>
|
|
||||||
<Input.Search
|
<Input.Search
|
||||||
placeholder={search.search || t("general.labels.search")}
|
placeholder={search.search || t("general.labels.search")}
|
||||||
onSearch={(value) => {
|
onSearch={(value) => {
|
||||||
|
|||||||
@@ -61,9 +61,7 @@ export function ScheduleEventNote({ event }) {
|
|||||||
) : (
|
) : (
|
||||||
<Input.TextArea rows={3} value={note} onChange={(e) => setNote(e.target.value)} style={{ maxWidth: "8vw" }} />
|
<Input.TextArea rows={3} value={note} onChange={(e) => setNote(e.target.value)} style={{ maxWidth: "8vw" }} />
|
||||||
)}
|
)}
|
||||||
<Button onClick={toggleEdit} loading={loading}>
|
<Button onClick={toggleEdit} loading={loading} icon={editing ? <SaveFilled /> : <EditFilled />} />
|
||||||
{editing ? <SaveFilled /> : <EditFilled />}
|
|
||||||
</Button>
|
|
||||||
</Space>
|
</Space>
|
||||||
</DataLabel>
|
</DataLabel>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -159,9 +159,8 @@ export function JobAuditTrail({ bodyshop, jobId }) {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
refetch();
|
refetch();
|
||||||
}}
|
}}
|
||||||
>
|
icon={<SyncOutlined />}
|
||||||
<SyncOutlined />
|
/>
|
||||||
</Button>
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Table loading={loading} columns={columns} rowKey="id" dataSource={data ? data.audit_trail : []} />
|
<Table loading={loading} columns={columns} rowKey="id" dataSource={data ? data.audit_trail : []} />
|
||||||
|
|||||||
@@ -61,9 +61,12 @@ export default function JobIntakeTemplateList({ templates }) {
|
|||||||
renderItem={(template) => (
|
renderItem={(template) => (
|
||||||
<List.Item
|
<List.Item
|
||||||
actions={[
|
actions={[
|
||||||
<Button key="checkListTemplateButton" loading={loading} onClick={() => renderTemplate(template)}>
|
<Button
|
||||||
<PrinterFilled />
|
key="checkListTemplateButton"
|
||||||
</Button>
|
loading={loading}
|
||||||
|
onClick={() => renderTemplate(template)}
|
||||||
|
icon={<PrinterFilled />}
|
||||||
|
/>
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<List.Item.Meta
|
<List.Item.Meta
|
||||||
|
|||||||
@@ -395,9 +395,8 @@ export function JobLinesComponent({
|
|||||||
context: { ...record, jobid: job.id }
|
context: { ...record, jobid: job.id }
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
icon={<EditFilled />}
|
||||||
<EditFilled />
|
/>
|
||||||
</Button>
|
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
title={t("tasks.buttons.create")}
|
title={t("tasks.buttons.create")}
|
||||||
@@ -409,9 +408,9 @@ export function JobLinesComponent({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
icon={<FaTasks />}
|
||||||
<FaTasks />
|
/>
|
||||||
</Button>
|
|
||||||
{(record.manual_line || jobIsPrivate) && !technician && (
|
{(record.manual_line || jobIsPrivate) && !technician && (
|
||||||
<Button
|
<Button
|
||||||
disabled={jobRO}
|
disabled={jobRO}
|
||||||
@@ -431,9 +430,8 @@ export function JobLinesComponent({
|
|||||||
await axios.post("/job/totalsssu", { id: job.id });
|
await axios.post("/job/totalsssu", { id: job.id });
|
||||||
if (refetch) refetch();
|
if (refetch) refetch();
|
||||||
}}
|
}}
|
||||||
>
|
icon={<DeleteFilled />}
|
||||||
<DeleteFilled />
|
/>
|
||||||
</Button>
|
|
||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
)
|
)
|
||||||
@@ -542,9 +540,7 @@ export function JobLinesComponent({
|
|||||||
title={t("jobs.labels.estimatelines")}
|
title={t("jobs.labels.estimatelines")}
|
||||||
extra={
|
extra={
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<Button onClick={() => refetch()}>
|
<Button onClick={() => refetch()} icon={<SyncOutlined />} />
|
||||||
<SyncOutlined />
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{/* Bulk Update Location */}
|
{/* Bulk Update Location */}
|
||||||
<Button
|
<Button
|
||||||
@@ -609,8 +605,8 @@ export function JobLinesComponent({
|
|||||||
|
|
||||||
setSelectedLines([]);
|
setSelectedLines([]);
|
||||||
}}
|
}}
|
||||||
|
icon={<HomeOutlined />}
|
||||||
>
|
>
|
||||||
<HomeOutlined />
|
|
||||||
{t("parts.actions.orderinhouse")}
|
{t("parts.actions.orderinhouse")}
|
||||||
{selectedLines.length > 0 && ` (${selectedLines.length})`}
|
{selectedLines.length > 0 && ` (${selectedLines.length})`}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -641,6 +637,7 @@ export function JobLinesComponent({
|
|||||||
|
|
||||||
{!isPartsEntry && (
|
{!isPartsEntry && (
|
||||||
<Button
|
<Button
|
||||||
|
icon={<FilterFilled />}
|
||||||
id="job-lines-filter-parts-only-button"
|
id="job-lines-filter-parts-only-button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setState((state) => ({
|
setState((state) => ({
|
||||||
@@ -652,7 +649,7 @@ export function JobLinesComponent({
|
|||||||
}));
|
}));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FilterFilled /> {t("jobs.actions.filterpartsonly")}
|
{t("jobs.actions.filterpartsonly")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -187,9 +187,8 @@ export function JobLineConvertToLabor({
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
{...otherBtnProps}
|
{...otherBtnProps}
|
||||||
>
|
icon={<ClockCircleOutlined />}
|
||||||
<ClockCircleOutlined />
|
/>
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Popover>
|
</Popover>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -107,9 +107,8 @@ export function JobPayments({ job, bodyshop, setPaymentContext, setCardPaymentCo
|
|||||||
context: record
|
context: record
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
icon={<EditFilled />}
|
||||||
<EditFilled />
|
/>
|
||||||
</Button>
|
|
||||||
<PrintWrapperComponent
|
<PrintWrapperComponent
|
||||||
templateObject={{
|
templateObject={{
|
||||||
name: TemplateList("payment").payment_receipt.key,
|
name: TemplateList("payment").payment_receipt.key,
|
||||||
|
|||||||
@@ -11,8 +11,7 @@ export default function JobSyncButton({ job }) {
|
|||||||
};
|
};
|
||||||
if (job?.available_jobs && job?.available_jobs?.length > 0)
|
if (job?.available_jobs && job?.available_jobs?.length > 0)
|
||||||
return (
|
return (
|
||||||
<Button onClick={handleClick}>
|
<Button onClick={handleClick} icon={<SyncOutlined />}>
|
||||||
<SyncOutlined />
|
|
||||||
{t("jobs.actions.sync")}
|
{t("jobs.actions.sync")}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -53,10 +53,8 @@ export function JobsAdminStatus({ insertAuditTrail, bodyshop, job }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown menu={statusMenu} trigger={["click"]} key="changestatus">
|
<Dropdown menu={statusMenu} trigger={["click"]} key="changestatus">
|
||||||
<Button shape="round">
|
<Button icon={<DownCircleFilled />} iconPlacement="end" shape="round">
|
||||||
<span>{job.status}</span>
|
<span>{job.status}</span>
|
||||||
|
|
||||||
<DownCircleFilled />
|
|
||||||
</Button>
|
</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -94,11 +94,7 @@ export function JobsAvailableScan({ partnerVersion, refetch }) {
|
|||||||
{
|
{
|
||||||
title: t("general.labels.actions"),
|
title: t("general.labels.actions"),
|
||||||
key: "actions",
|
key: "actions",
|
||||||
render: (text, record) => (
|
render: (text, record) => <Button icon={<DownloadOutlined />} onClick={() => handleImport(record.filepath)} />
|
||||||
<Button onClick={() => handleImport(record.filepath)}>
|
|
||||||
<DownloadOutlined />
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -126,15 +122,14 @@ export function JobsAvailableScan({ partnerVersion, refetch }) {
|
|||||||
extra={
|
extra={
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<Button
|
<Button
|
||||||
|
icon={<SyncOutlined />}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
disabled={!partnerVersion}
|
disabled={!partnerVersion}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
scanEstimates();
|
scanEstimates();
|
||||||
}}
|
}}
|
||||||
id="scan-estimates-button"
|
id="scan-estimates-button"
|
||||||
>
|
/>
|
||||||
<SyncOutlined />
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Input.Search
|
<Input.Search
|
||||||
placeholder={t("general.labels.search")}
|
placeholder={t("general.labels.search")}
|
||||||
|
|||||||
@@ -135,17 +135,16 @@ export function JobsAvailableComponent({ bodyshop, loading, data, refetch, addJo
|
|||||||
refetch();
|
refetch();
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
icon={<DeleteFilled />}
|
||||||
<DeleteFilled />
|
/>
|
||||||
</Button>
|
|
||||||
{!isClosed && (
|
{!isClosed && (
|
||||||
<>
|
<>
|
||||||
<Button onClick={() => addJobAsNew(record)} disabled={record.issupplement}>
|
<Button
|
||||||
<PlusCircleFilled />
|
onClick={() => addJobAsNew(record)}
|
||||||
</Button>
|
disabled={record.issupplement}
|
||||||
<Button onClick={() => addJobAsSupp(record)}>
|
icon={<PlusCircleFilled />}
|
||||||
<DownloadOutlined />
|
/>
|
||||||
</Button>
|
<Button onClick={() => addJobAsSupp(record)} icon={<DownloadOutlined />} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{isClosed && <Alert type="error" title={t("jobs.labels.alreadyclosed")}></Alert>}
|
{isClosed && <Alert type="error" title={t("jobs.labels.alreadyclosed")}></Alert>}
|
||||||
@@ -175,9 +174,8 @@ export function JobsAvailableComponent({ bodyshop, loading, data, refetch, addJo
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
refetch();
|
refetch();
|
||||||
}}
|
}}
|
||||||
>
|
icon={<SyncOutlined />}
|
||||||
<SyncOutlined />
|
/>
|
||||||
</Button>
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
deleteAllAvailableJobs()
|
deleteAllAvailableJobs()
|
||||||
|
|||||||
@@ -96,10 +96,8 @@ export function JobsChangeStatus({ job, bodyshop, jobRO, insertAuditTrail, isPar
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown menu={statusMenu} trigger={["click"]} key="changestatus" disabled={jobRO || !job.converted}>
|
<Dropdown menu={statusMenu} trigger={["click"]} key="changestatus" disabled={jobRO || !job.converted}>
|
||||||
<Button shape="round">
|
<Button shape="round" icon={<DownCircleFilled />} iconPlacement="end">
|
||||||
<span>{job.status}</span>
|
<span>{job.status}</span>
|
||||||
|
|
||||||
<DownCircleFilled />
|
|
||||||
</Button>
|
</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -56,9 +56,8 @@ export default function JobsCreateVehicleInfoPredefined({ disabled, form }) {
|
|||||||
setOpen(false);
|
setOpen(false);
|
||||||
setSearch("");
|
setSearch("");
|
||||||
}}
|
}}
|
||||||
>
|
icon={<PlusOutlined />}
|
||||||
<PlusOutlined />
|
/>
|
||||||
</Button>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
|
|||||||
@@ -1286,9 +1286,8 @@ export function JobsDetailHeaderActions({
|
|||||||
open={dropdownOpen}
|
open={dropdownOpen}
|
||||||
onOpenChange={handleDropdownOpenChange}
|
onOpenChange={handleDropdownOpenChange}
|
||||||
>
|
>
|
||||||
<Button>
|
<Button icon={<DownCircleFilled />} iconPlacement="end">
|
||||||
<span>{t("general.labels.actions")}</span>
|
<span>{t("general.labels.actions")}</span>
|
||||||
<DownCircleFilled />
|
|
||||||
</Button>
|
</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|
||||||
|
|||||||
@@ -128,9 +128,7 @@ function JobsDocumentsComponent({
|
|||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]}>
|
||||||
<Col span={24}>
|
<Col span={24}>
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<Button onClick={() => refetch && refetch()}>
|
<Button onClick={() => refetch && refetch()} icon={<SyncOutlined />} />
|
||||||
<SyncOutlined />
|
|
||||||
</Button>
|
|
||||||
<JobsDocumentsGallerySelectAllComponent galleryImages={galleryImages} setGalleryImages={setgalleryImages} />
|
<JobsDocumentsGallerySelectAllComponent galleryImages={galleryImages} setGalleryImages={setgalleryImages} />
|
||||||
<JobsDocumentsDownloadButton galleryImages={galleryImages} identifier={downloadIdentifier} />
|
<JobsDocumentsDownloadButton galleryImages={galleryImages} identifier={downloadIdentifier} />
|
||||||
<JobsDocumentsDeleteButton galleryImages={galleryImages} deletionCallback={billsCallback || refetch} />
|
<JobsDocumentsDeleteButton galleryImages={galleryImages} deletionCallback={billsCallback || refetch} />
|
||||||
|
|||||||
@@ -65,9 +65,8 @@ function JobsDocumentsImgproxyComponent({
|
|||||||
//Do the imgproxy refresh too
|
//Do the imgproxy refresh too
|
||||||
fetchThumbnails();
|
fetchThumbnails();
|
||||||
}}
|
}}
|
||||||
>
|
icon={<SyncOutlined />}
|
||||||
<SyncOutlined />
|
/>
|
||||||
</Button>
|
|
||||||
<JobsDocumentsGallerySelectAllComponent galleryImages={galleryImages} setGalleryImages={setGalleryImages} />
|
<JobsDocumentsGallerySelectAllComponent galleryImages={galleryImages} setGalleryImages={setGalleryImages} />
|
||||||
{!billId && (
|
{!billId && (
|
||||||
<JobsDocumentsGalleryReassign galleryImages={galleryImages} callback={fetchThumbnails || refetch} />
|
<JobsDocumentsGalleryReassign galleryImages={galleryImages} callback={fetchThumbnails || refetch} />
|
||||||
|
|||||||
@@ -102,9 +102,8 @@ export function JobsDocumentsLocalGallery({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
icon={<SyncOutlined />}
|
||||||
<SyncOutlined />
|
/>
|
||||||
</Button>
|
|
||||||
<a href={CreateExplorerLinkForJob({ jobid: job.id })}>
|
<a href={CreateExplorerLinkForJob({ jobid: job.id })}>
|
||||||
<Button>{t("documents.labels.openinexplorer")}</Button>
|
<Button>{t("documents.labels.openinexplorer")}</Button>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -179,9 +179,8 @@ export default function JobsFindModalComponent({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
jobsListRefetch();
|
jobsListRefetch();
|
||||||
}}
|
}}
|
||||||
>
|
icon={<SyncOutlined />}
|
||||||
<SyncOutlined />
|
/>
|
||||||
</Button>
|
|
||||||
<Input
|
<Input
|
||||||
value={modalSearch}
|
value={modalSearch}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
|||||||
@@ -224,9 +224,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
|
|||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Button onClick={() => refetch()}>
|
<Button onClick={() => refetch()} icon={<SyncOutlined />} />
|
||||||
<SyncOutlined />
|
|
||||||
</Button>
|
|
||||||
<Input.Search
|
<Input.Search
|
||||||
placeholder={search.search || t("general.labels.search")}
|
placeholder={search.search || t("general.labels.search")}
|
||||||
onSearch={(value) => {
|
onSearch={(value) => {
|
||||||
|
|||||||
@@ -313,9 +313,7 @@ export function JobsList({ bodyshop }) {
|
|||||||
title={t("titles.bc.jobs-active")}
|
title={t("titles.bc.jobs-active")}
|
||||||
extra={
|
extra={
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<Button onClick={() => refetch()}>
|
<Button onClick={() => refetch()} icon={<SyncOutlined />} />
|
||||||
<SyncOutlined />
|
|
||||||
</Button>
|
|
||||||
<Input.Search
|
<Input.Search
|
||||||
placeholder={t("general.labels.search")}
|
placeholder={t("general.labels.search")}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
|||||||
@@ -121,9 +121,12 @@ export function JobNotesComponent({
|
|||||||
width: 200,
|
width: 200,
|
||||||
render: (text, record) => (
|
render: (text, record) => (
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<Button loading={deleteLoading} disabled={record.audit || jobRO} onClick={() => handleNoteDelete(record.id)}>
|
<Button
|
||||||
<DeleteFilled />
|
loading={deleteLoading}
|
||||||
</Button>
|
disabled={record.audit || jobRO}
|
||||||
|
onClick={() => handleNoteDelete(record.id)}
|
||||||
|
icon={<DeleteFilled />}
|
||||||
|
/>
|
||||||
<Button
|
<Button
|
||||||
disabled={record.audit || jobRO}
|
disabled={record.audit || jobRO}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -135,9 +138,8 @@ export function JobNotesComponent({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
icon={<EditFilled />}
|
||||||
<EditFilled />
|
/>
|
||||||
</Button>
|
|
||||||
<PrintWrapperComponent
|
<PrintWrapperComponent
|
||||||
templateObject={{
|
templateObject={{
|
||||||
name: Templates.individual_job_note.key,
|
name: Templates.individual_job_note.key,
|
||||||
|
|||||||
@@ -297,9 +297,7 @@ export function JobsReadyList({ bodyshop }) {
|
|||||||
extra={
|
extra={
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<span>({readyStatuses && readyStatuses.join(", ")})</span>
|
<span>({readyStatuses && readyStatuses.join(", ")})</span>
|
||||||
<Button onClick={() => refetch()}>
|
<Button onClick={() => refetch()} icon={<SyncOutlined />} />
|
||||||
<SyncOutlined />
|
|
||||||
</Button>
|
|
||||||
<Input.Search
|
<Input.Search
|
||||||
placeholder={t("general.labels.search")}
|
placeholder={t("general.labels.search")}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
|||||||
@@ -246,9 +246,8 @@ export function PayrollLaborAllocationsTable({
|
|||||||
setTotals(data);
|
setTotals(data);
|
||||||
refetch();
|
refetch();
|
||||||
}}
|
}}
|
||||||
>
|
icon={<SyncOutlined />}
|
||||||
<SyncOutlined />
|
/>
|
||||||
</Button>
|
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ const NotificationCenterComponent = ({
|
|||||||
onNotificationClick,
|
onNotificationClick,
|
||||||
unreadCount,
|
unreadCount,
|
||||||
isEmployee,
|
isEmployee,
|
||||||
|
isDarkMode,
|
||||||
ref
|
ref
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -112,14 +113,16 @@ const NotificationCenterComponent = ({
|
|||||||
<Alert title={t("notifications.labels.employee-notification")} type="warning" />
|
<Alert title={t("notifications.labels.employee-notification")} type="warning" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Virtuoso
|
<div className={isDarkMode ? "notification-center--dark" : "notification-center--light"} style={{ height: "400px", width: "100%" }}>
|
||||||
ref={virtuosoRef}
|
<Virtuoso
|
||||||
style={{ height: "400px", width: "100%" }}
|
ref={virtuosoRef}
|
||||||
data={notifications}
|
style={{ height: "100%", width: "100%" }}
|
||||||
totalCount={notifications.length}
|
data={notifications}
|
||||||
endReached={loadMore}
|
totalCount={notifications.length}
|
||||||
itemContent={renderNotification}
|
endReached={loadMore}
|
||||||
/>
|
itemContent={renderNotification}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import NotificationCenterComponent from "./notification-center.component";
|
|||||||
import { GET_NOTIFICATIONS } from "../../graphql/notifications.queries";
|
import { GET_NOTIFICATIONS } from "../../graphql/notifications.queries";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors.js";
|
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors.js";
|
||||||
|
import { selectDarkMode } from "../../redux/application/application.selectors.js";
|
||||||
import day from "../../utils/day.js";
|
import day from "../../utils/day.js";
|
||||||
import { INITIAL_NOTIFICATIONS, useSocket } from "../../contexts/SocketIO/useSocket.js";
|
import { INITIAL_NOTIFICATIONS, useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||||
import { useIsEmployee } from "../../utils/useIsEmployee.js";
|
import { useIsEmployee } from "../../utils/useIsEmployee.js";
|
||||||
@@ -22,7 +23,7 @@ const NOTIFICATION_POLL_INTERVAL_SECONDS = 60;
|
|||||||
* @returns {JSX.Element}
|
* @returns {JSX.Element}
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount, currentUser }) => {
|
const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount, currentUser, isDarkMode }) => {
|
||||||
const [showUnreadOnly, setShowUnreadOnly] = useState(false);
|
const [showUnreadOnly, setShowUnreadOnly] = useState(false);
|
||||||
const [notifications, setNotifications] = useState([]);
|
const [notifications, setNotifications] = useState([]);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
@@ -213,13 +214,15 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount,
|
|||||||
loadMore={loadMore}
|
loadMore={loadMore}
|
||||||
onNotificationClick={handleNotificationClick}
|
onNotificationClick={handleNotificationClick}
|
||||||
unreadCount={unreadCount}
|
unreadCount={unreadCount}
|
||||||
|
isDarkMode={isDarkMode}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
currentUser: selectCurrentUser
|
currentUser: selectCurrentUser,
|
||||||
|
isDarkMode: selectDarkMode
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps, null)(NotificationCenterContainer);
|
export default connect(mapStateToProps, null)(NotificationCenterContainer);
|
||||||
|
|||||||
@@ -173,3 +173,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notification-center--dark {
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-center--light {
|
||||||
|
color-scheme: light;
|
||||||
|
}
|
||||||
|
|||||||
@@ -99,9 +99,7 @@ export default function OwnersListComponent({ loading, owners, total, refetch })
|
|||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Button onClick={() => refetch()}>
|
<Button onClick={() => refetch()} icon={<SyncOutlined />} />
|
||||||
<SyncOutlined />
|
|
||||||
</Button>
|
|
||||||
<Input.Search
|
<Input.Search
|
||||||
placeholder={search.search || t("general.labels.search")}
|
placeholder={search.search || t("general.labels.search")}
|
||||||
onSearch={(value) => {
|
onSearch={(value) => {
|
||||||
|
|||||||
@@ -93,10 +93,7 @@ export function PartDispatchTableComponent({ bodyshop, job, billsQuery }) {
|
|||||||
title={t("parts_dispatch.labels.parts_dispatch")}
|
title={t("parts_dispatch.labels.parts_dispatch")}
|
||||||
extra={
|
extra={
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<Button onClick={() => refetch()}>
|
<Button onClick={() => refetch()} icon={<SyncOutlined />} />
|
||||||
<SyncOutlined />
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Input.Search
|
<Input.Search
|
||||||
placeholder={t("general.labels.search")}
|
placeholder={t("general.labels.search")}
|
||||||
value={searchText}
|
value={searchText}
|
||||||
|
|||||||
@@ -34,9 +34,7 @@ export default function PartsOrderDeleteLine({ disabled, partsLineId, partsOrder
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button disabled={disabled}>
|
<Button disabled={disabled} icon={<DeleteFilled />} />
|
||||||
<DeleteFilled />
|
|
||||||
</Button>
|
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -150,9 +150,8 @@ export function PartsOrderListTableDrawerComponent({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
icon={<FaTasks />}
|
||||||
<FaTasks />
|
/>
|
||||||
</Button>
|
|
||||||
)}
|
)}
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
title={t("parts_orders.labels.confirmdelete")}
|
title={t("parts_orders.labels.confirmdelete")}
|
||||||
@@ -173,9 +172,7 @@ export function PartsOrderListTableDrawerComponent({
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button disabled={jobRO}>
|
<Button disabled={jobRO} icon={<DeleteFilled />} />
|
||||||
<DeleteFilled />
|
|
||||||
</Button>
|
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
{!isPartsEntry && (
|
{!isPartsEntry && (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export function PartsOrderModalComponent({
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Form.Item name="returnfrombill" hidden>
|
<Form.Item name="returnfrombill" hidden>
|
||||||
<Input />
|
<Input type="hidden" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<LayoutFormRow grow noDivider>
|
<LayoutFormRow grow noDivider>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
|
|||||||
@@ -144,6 +144,7 @@ export function PaymentsListPaginated({
|
|||||||
render: (text, record) => (
|
render: (text, record) => (
|
||||||
<Space>
|
<Space>
|
||||||
<Button
|
<Button
|
||||||
|
icon={<EditFilled />}
|
||||||
// disabled={record.exportedat}
|
// disabled={record.exportedat}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
let apolloResults;
|
let apolloResults;
|
||||||
@@ -174,9 +175,7 @@ export function PaymentsListPaginated({
|
|||||||
context: { ...(apolloResults ? apolloResults : record), refetchRequiresContext: true }
|
context: { ...(apolloResults ? apolloResults : record), refetchRequiresContext: true }
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
<EditFilled />
|
|
||||||
</Button>
|
|
||||||
<PrintWrapperComponent
|
<PrintWrapperComponent
|
||||||
templateObject={{
|
templateObject={{
|
||||||
name: Templates.payment_receipt.key,
|
name: Templates.payment_receipt.key,
|
||||||
@@ -245,9 +244,7 @@ export function PaymentsListPaginated({
|
|||||||
<Button onClick={() => setCaBcEtfTableContext()}>{t("payments.labels.ca_bc_etf_table")}</Button>
|
<Button onClick={() => setCaBcEtfTableContext()}>{t("payments.labels.ca_bc_etf_table")}</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Button onClick={() => refetch()}>
|
<Button onClick={() => refetch()} icon={<SyncOutlined />} />
|
||||||
<SyncOutlined />
|
|
||||||
</Button>
|
|
||||||
<Input.Search
|
<Input.Search
|
||||||
placeholder={search.search || t("general.labels.search")}
|
placeholder={search.search || t("general.labels.search")}
|
||||||
onSearch={(value) => {
|
onSearch={(value) => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { MailFilled, PrinterFilled } from "@ant-design/icons";
|
import { MailFilled, PrinterFilled } from "@ant-design/icons";
|
||||||
import { Space, Spin } from "antd";
|
import { Button, Space, Spin } from "antd";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { GenerateDocument } from "../../utils/RenderTemplate";
|
import { GenerateDocument } from "../../utils/RenderTemplate";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
@@ -26,16 +26,18 @@ export default function PrintWrapperComponent({
|
|||||||
<Space>
|
<Space>
|
||||||
{children || null}
|
{children || null}
|
||||||
{!emailOnly && (
|
{!emailOnly && (
|
||||||
<PrinterFilled
|
<Button
|
||||||
|
style={{ cursor: disabled ? "not-allowed" : null }}
|
||||||
|
icon={<PrinterFilled />}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClick={() => handlePrint("p")}
|
onClick={() => handlePrint("p")}
|
||||||
style={{ cursor: disabled ? "not-allowed" : null }}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<MailFilled
|
<Button
|
||||||
|
style={{ cursor: disabled ? "not-allowed" : null }}
|
||||||
|
icon={<MailFilled />}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClick={() => handlePrint("e")}
|
onClick={() => handlePrint("e")}
|
||||||
style={{ cursor: disabled ? "not-allowed" : null }}
|
|
||||||
/>
|
/>
|
||||||
{loading && <Spin />}
|
{loading && <Spin />}
|
||||||
</Space>
|
</Space>
|
||||||
|
|||||||
@@ -191,9 +191,7 @@ function ProductionBoardKanbanComponent({ data, bodyshop, refetch, insertAuditTr
|
|||||||
style={{ paddingInline: 0, paddingBlock: 0 }}
|
style={{ paddingInline: 0, paddingBlock: 0 }}
|
||||||
extra={
|
extra={
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<Button onClick={() => refetch && refetch()}>
|
<Button onClick={() => refetch && refetch()} icon={<SyncOutlined />} />
|
||||||
<SyncOutlined />
|
|
||||||
</Button>
|
|
||||||
<ProductionBoardFilters filter={filter} setFilter={setFilter} loading={isMoving} />
|
<ProductionBoardFilters filter={filter} setFilter={setFilter} loading={isMoving} />
|
||||||
<ProductionBoardKanbanSettings
|
<ProductionBoardKanbanSettings
|
||||||
parentLoading={setLoading}
|
parentLoading={setLoading}
|
||||||
|
|||||||
@@ -251,9 +251,8 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
refetch && refetch();
|
refetch && refetch();
|
||||||
}}
|
}}
|
||||||
>
|
icon={<SyncOutlined />}
|
||||||
<SyncOutlined />
|
/>
|
||||||
</Button>
|
|
||||||
<ProductionListColumnsAdd
|
<ProductionListColumnsAdd
|
||||||
columnState={[columns, setColumns]}
|
columnState={[columns, setColumns]}
|
||||||
tableState={state}
|
tableState={state}
|
||||||
|
|||||||
@@ -69,9 +69,8 @@ export default function ProductionSubletsManageComponent({ subletJobLines }) {
|
|||||||
handleSubletMark(s, "complete");
|
handleSubletMark(s, "complete");
|
||||||
}}
|
}}
|
||||||
type={s.sublet_completed ? "primary" : "ghost"}
|
type={s.sublet_completed ? "primary" : "ghost"}
|
||||||
>
|
icon={<CheckCircleFilled style={{ color: s.sublet_completed ? "green" : undefined }} />}
|
||||||
<CheckCircleFilled style={{ color: s.sublet_completed ? "green" : undefined }} />
|
/>,
|
||||||
</Button>,
|
|
||||||
<Button
|
<Button
|
||||||
key="sublet"
|
key="sublet"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
@@ -80,9 +79,8 @@ export default function ProductionSubletsManageComponent({ subletJobLines }) {
|
|||||||
handleSubletMark(s, "ignore");
|
handleSubletMark(s, "ignore");
|
||||||
}}
|
}}
|
||||||
type={s.sublet_ignored ? "primary" : "ghost"}
|
type={s.sublet_ignored ? "primary" : "ghost"}
|
||||||
>
|
icon={<EyeInvisibleFilled style={{ color: s.sublet_ignored ? "tomato" : undefined }} />}
|
||||||
<EyeInvisibleFilled style={{ color: s.sublet_ignored ? "tomato" : undefined }} />
|
/>
|
||||||
</Button>
|
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<List.Item.Meta title={s.line_desc} />
|
<List.Item.Meta title={s.line_desc} />
|
||||||
|
|||||||
@@ -146,7 +146,9 @@ export function ReportCenterModalComponent({ reportCenterModal, bodyshop }) {
|
|||||||
<div className="report-center-modal">
|
<div className="report-center-modal">
|
||||||
<Form onFinish={handleFinish} autoComplete={"off"} layout="vertical" form={form}>
|
<Form onFinish={handleFinish} autoComplete={"off"} layout="vertical" form={form}>
|
||||||
<Input.Search onChange={(e) => setSearch(e.target.value)} value={search} />
|
<Input.Search onChange={(e) => setSearch(e.target.value)} value={search} />
|
||||||
<Form.Item name="defaultSorters" hidden />
|
<Form.Item name="defaultSorters" hidden>
|
||||||
|
<Input type="hidden" />
|
||||||
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="key"
|
name="key"
|
||||||
label={t("reportcenter.labels.key")}
|
label={t("reportcenter.labels.key")}
|
||||||
|
|||||||
@@ -61,9 +61,8 @@ export default function ScheduleProductionList() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover content={content} trigger="click" placement="bottomRight">
|
<Popover content={content} trigger="click" placement="bottomRight">
|
||||||
<Button onClick={() => callQuery({ variables: {} })}>
|
<Button onClick={() => callQuery({ variables: {} })} icon={<DownOutlined />} iconPlacement="end">
|
||||||
{t("appointments.labels.inproduction")}
|
{t("appointments.labels.inproduction")}
|
||||||
<DownOutlined />
|
|
||||||
</Button>
|
</Button>
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -123,9 +123,8 @@ export default function ScoreboardJobsList() {
|
|||||||
<Card
|
<Card
|
||||||
extra={
|
extra={
|
||||||
<Space align="middle" wrap>
|
<Space align="middle" wrap>
|
||||||
<Button onClick={() => refetch()}>
|
<Button onClick={() => refetch()} icon={<SyncOutlined />} />
|
||||||
<SyncOutlined />
|
|
||||||
</Button>
|
|
||||||
<Typography.Title level={4}>
|
<Typography.Title level={4}>
|
||||||
{t("general.labels.searchresults", { search: state.search })}
|
{t("general.labels.searchresults", { search: state.search })}
|
||||||
</Typography.Title>
|
</Typography.Title>
|
||||||
|
|||||||
@@ -37,9 +37,5 @@ export default function ScoreboardRemoveButton({ scoreboardId }) {
|
|||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
return (
|
return <Button onClick={handleDelete} loading={loading} icon={<DeleteFilled />} />;
|
||||||
<Button onClick={handleDelete} loading={loading}>
|
|
||||||
<DeleteFilled />
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -159,9 +159,8 @@ export function ShopEmployeesFormComponent({ bodyshop }) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
icon={<DeleteFilled />}
|
||||||
<DeleteFilled />
|
/>
|
||||||
</Button>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
|||||||
{fields.map((field, index) => (
|
{fields.map((field, index) => (
|
||||||
<Form.Item key={field.key} style={{ padding: 0, margin: 2 }}>
|
<Form.Item key={field.key} style={{ padding: 0, margin: 2 }}>
|
||||||
<Form.Item label={t("employees.fields.id")} key={`${index}`} name={[field.name, "id"]} hidden>
|
<Form.Item label={t("employees.fields.id")} key={`${index}`} name={[field.name, "id"]} hidden>
|
||||||
<Input />
|
<Input type="hidden" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<LayoutFormRow grow>
|
<LayoutFormRow grow>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
|
|||||||
@@ -228,9 +228,8 @@ export function SimplifiedPartsJobsListComponent({
|
|||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Button onClick={() => refetch()}>
|
<Button onClick={() => refetch()} icon={<SyncOutlined />} />
|
||||||
<SyncOutlined />
|
|
||||||
</Button>
|
|
||||||
<Input.Search
|
<Input.Search
|
||||||
placeholder={search.search || t("general.labels.search")}
|
placeholder={search.search || t("general.labels.search")}
|
||||||
onSearch={(value) => {
|
onSearch={(value) => {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ const TaskCenterComponent = ({
|
|||||||
hasMore,
|
hasMore,
|
||||||
createNewTask,
|
createNewTask,
|
||||||
incompleteTaskCount,
|
incompleteTaskCount,
|
||||||
|
isDarkMode,
|
||||||
ref
|
ref
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -140,22 +141,24 @@ const TaskCenterComponent = ({
|
|||||||
{tasks.length === 0 && !loading ? (
|
{tasks.length === 0 && !loading ? (
|
||||||
<div className="no-tasks-message">{t("tasks.labels.no_tasks")}</div>
|
<div className="no-tasks-message">{t("tasks.labels.no_tasks")}</div>
|
||||||
) : (
|
) : (
|
||||||
<Virtuoso
|
<div className={isDarkMode ? "task-center--dark" : "task-center--light"} style={{ height: "550px", width: "100%" }}>
|
||||||
ref={virtuosoRef}
|
<Virtuoso
|
||||||
style={{ height: "550px", width: "100%" }}
|
ref={virtuosoRef}
|
||||||
groupCounts={groupCounts}
|
style={{ height: "100%", width: "100%" }}
|
||||||
groupContent={groupContent}
|
groupCounts={groupCounts}
|
||||||
itemContent={itemContent}
|
groupContent={groupContent}
|
||||||
endReached={hasMore && !loading ? onLoadMore : undefined}
|
itemContent={itemContent}
|
||||||
components={{
|
endReached={hasMore && !loading ? onLoadMore : undefined}
|
||||||
Footer: () =>
|
components={{
|
||||||
loading ? (
|
Footer: () =>
|
||||||
<div className="loading-footer">
|
loading ? (
|
||||||
<Spin />
|
<div className="loading-footer">
|
||||||
</div>
|
<Spin />
|
||||||
) : null
|
</div>
|
||||||
}}
|
) : null
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useQuery } from "@apollo/client/react";
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||||
|
import { selectDarkMode } from "../../redux/application/application.selectors.js";
|
||||||
import { INITIAL_TASKS, TASKS_CENTER_POLL_INTERVAL, useSocket } from "../../contexts/SocketIO/useSocket";
|
import { INITIAL_TASKS, TASKS_CENTER_POLL_INTERVAL, useSocket } from "../../contexts/SocketIO/useSocket";
|
||||||
import { useIsEmployee } from "../../utils/useIsEmployee";
|
import { useIsEmployee } from "../../utils/useIsEmployee";
|
||||||
import TaskCenterComponent from "./task-center.component";
|
import TaskCenterComponent from "./task-center.component";
|
||||||
@@ -11,7 +12,8 @@ import { QUERY_TASKS_NO_DUE_DATE_PAGINATED, QUERY_TASKS_WITH_DUE_DATES } from ".
|
|||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
currentUser: selectCurrentUser
|
currentUser: selectCurrentUser,
|
||||||
|
isDarkMode: selectDarkMode
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
@@ -24,7 +26,8 @@ const TaskCenterContainer = ({
|
|||||||
bodyshop,
|
bodyshop,
|
||||||
currentUser,
|
currentUser,
|
||||||
setTaskUpsertContext,
|
setTaskUpsertContext,
|
||||||
incompleteTaskCount
|
incompleteTaskCount,
|
||||||
|
isDarkMode
|
||||||
}) => {
|
}) => {
|
||||||
const [tasks, setTasks] = useState([]);
|
const [tasks, setTasks] = useState([]);
|
||||||
const { isConnected } = useSocket();
|
const { isConnected } = useSocket();
|
||||||
@@ -128,6 +131,7 @@ const TaskCenterContainer = ({
|
|||||||
hasMore={hasMore}
|
hasMore={hasMore}
|
||||||
createNewTask={createNewTask}
|
createNewTask={createNewTask}
|
||||||
incompleteTaskCount={incompleteTaskCount}
|
incompleteTaskCount={incompleteTaskCount}
|
||||||
|
isDarkMode={isDarkMode}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -141,3 +141,11 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.task-center--dark {
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-center--light {
|
||||||
|
color-scheme: light;
|
||||||
|
}
|
||||||
|
|||||||
@@ -334,11 +334,9 @@ function TaskListComponent({
|
|||||||
checked={deleted === "true"}
|
checked={deleted === "true"}
|
||||||
onChange={(value) => handleSwitchChange("deleted", value)}
|
onChange={(value) => handleSwitchChange("deleted", value)}
|
||||||
/>
|
/>
|
||||||
<Button title={t("tasks.buttons.refresh")} onClick={() => refetch()}>
|
<Button title={t("tasks.buttons.refresh")} onClick={() => refetch()} icon={<SyncOutlined />} />
|
||||||
<SyncOutlined />
|
|
||||||
</Button>
|
<Button title={t("tasks.buttons.create")} onClick={handleCreateTask} icon={<PlusCircleFilled />}>
|
||||||
<Button title={t("tasks.buttons.create")} onClick={handleCreateTask}>
|
|
||||||
<PlusCircleFilled />
|
|
||||||
{t("tasks.buttons.create")}
|
{t("tasks.buttons.create")}
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ export function TechLookupJobsDrawer({ bodyshop, setPrintCenterContext }) {
|
|||||||
title={data.jobs_by_pk.ro_number || t("general.labels.na")}
|
title={data.jobs_by_pk.ro_number || t("general.labels.na")}
|
||||||
extra={
|
extra={
|
||||||
<Button
|
<Button
|
||||||
|
icon={<PrinterFilled />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setPrintCenterContext({
|
setPrintCenterContext({
|
||||||
actions: { refetch: refetch },
|
actions: { refetch: refetch },
|
||||||
@@ -87,7 +88,6 @@ export function TechLookupJobsDrawer({ bodyshop, setPrintCenterContext }) {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PrinterFilled />
|
|
||||||
{t("jobs.actions.printCenter")}
|
{t("jobs.actions.printCenter")}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -158,9 +158,7 @@ export function TechLookupJobsList({ bodyshop }) {
|
|||||||
<Card
|
<Card
|
||||||
extra={
|
extra={
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<Button onClick={() => refetch()}>
|
<Button onClick={() => refetch()} icon={<SyncOutlined />} />
|
||||||
<SyncOutlined />
|
|
||||||
</Button>
|
|
||||||
<Input.Search
|
<Input.Search
|
||||||
placeholder={t("general.labels.search")}
|
placeholder={t("general.labels.search")}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
|||||||
@@ -95,7 +95,18 @@ export default function TimeTicketCalculatorComponent({
|
|||||||
<Form.Item name="percent">
|
<Form.Item name="percent">
|
||||||
<Space.Compact>
|
<Space.Compact>
|
||||||
<InputNumber min={0} max={100} precision={1} />
|
<InputNumber min={0} max={100} precision={1} />
|
||||||
<span style={{ padding: "0 11px", backgroundColor: "#fafafa", border: "1px solid #d9d9d9", borderLeft: 0, display: "flex", alignItems: "center" }}>%</span>
|
<span
|
||||||
|
style={{
|
||||||
|
padding: "0 11px",
|
||||||
|
backgroundColor: "#fafafa",
|
||||||
|
border: "1px solid #d9d9d9",
|
||||||
|
borderLeft: 0,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
%
|
||||||
|
</span>
|
||||||
</Space.Compact>
|
</Space.Compact>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Button htmlType="submit">Calculate</Button>
|
<Button htmlType="submit">Calculate</Button>
|
||||||
@@ -112,11 +123,8 @@ export default function TimeTicketCalculatorComponent({
|
|||||||
placement="right"
|
placement="right"
|
||||||
destroyOnHidden
|
destroyOnHidden
|
||||||
>
|
>
|
||||||
<Button onClick={(e) => e.preventDefault()}>
|
<Button onClick={(e) => e.preventDefault()} icon={<DownOutlined />} iconPlacement="end">
|
||||||
<Space>
|
Draw Calculator
|
||||||
Draw Calculator
|
|
||||||
<DownOutlined />
|
|
||||||
</Space>
|
|
||||||
</Button>
|
</Button>
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -322,9 +322,8 @@ export function TimeTicketList({
|
|||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
refetch();
|
refetch();
|
||||||
}}
|
}}
|
||||||
>
|
icon={<SyncOutlined />}
|
||||||
<SyncOutlined />
|
/>
|
||||||
</Button>
|
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -184,8 +184,8 @@ export function TimeTicketModalComponent({
|
|||||||
}}
|
}}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item name="flat_rate" label={t("timetickets.fields.flat_rate")} valuePropName="checked" noStyle hidden>
|
<Form.Item name="flat_rate" label={t("timetickets.fields.flat_rate")} valuePropName="checked" hidden>
|
||||||
<Switch style={{ display: "none" }} />
|
<Switch />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
|
|
||||||
|
|||||||
@@ -176,9 +176,7 @@ export function TtApprovalsListComponent({
|
|||||||
completedCallback={setSelectedTickets}
|
completedCallback={setSelectedTickets}
|
||||||
refetch={refetch}
|
refetch={refetch}
|
||||||
/>
|
/>
|
||||||
<Button onClick={() => refetch()}>
|
<Button onClick={() => refetch()} icon={<SyncOutlined />} />
|
||||||
<SyncOutlined />
|
|
||||||
</Button>
|
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -91,9 +91,7 @@ export default function VehiclesListComponent({ loading, vehicles, total, refetc
|
|||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Button onClick={() => refetch()}>
|
<Button onClick={() => refetch()} icon={<SyncOutlined />} />
|
||||||
<SyncOutlined />
|
|
||||||
</Button>
|
|
||||||
<Input.Search
|
<Input.Search
|
||||||
placeholder={search.search || t("general.labels.search")}
|
placeholder={search.search || t("general.labels.search")}
|
||||||
onSearch={(value) => {
|
onSearch={(value) => {
|
||||||
|
|||||||
@@ -73,9 +73,7 @@ export default function VendorsListComponent({ handleNewVendor, loading, handleO
|
|||||||
extra={
|
extra={
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<Button onClick={handleNewVendor}>{t("vendors.actions.new")}</Button>
|
<Button onClick={handleNewVendor}>{t("vendors.actions.new")}</Button>
|
||||||
<Button onClick={() => refetch()}>
|
<Button onClick={() => refetch()} icon={<SyncOutlined />} />
|
||||||
<SyncOutlined />
|
|
||||||
</Button>
|
|
||||||
<Input.Search
|
<Input.Search
|
||||||
placeholder={t("general.labels.search")}
|
placeholder={t("general.labels.search")}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import "./utils/sentry"; //Must be first.
|
import "./utils/sentry"; // Must be first.
|
||||||
import * as Sentry from "@sentry/react";
|
import * as Sentry from "@sentry/react";
|
||||||
import { ConfigProvider } from "antd";
|
|
||||||
import Dinero from "dinero.js";
|
import Dinero from "dinero.js";
|
||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import { Provider } from "react-redux";
|
import { Provider } from "react-redux";
|
||||||
@@ -14,9 +13,10 @@ import { persistor, store } from "./redux/store";
|
|||||||
import reportWebVitals from "./reportWebVitals";
|
import reportWebVitals from "./reportWebVitals";
|
||||||
import "./translations/i18n";
|
import "./translations/i18n";
|
||||||
import "./utils/CleanAxios";
|
import "./utils/CleanAxios";
|
||||||
//import * as amplitude from "@amplitude/analytics-browser";
|
// import * as amplitude from "@amplitude/analytics-browser";
|
||||||
import { PostHogProvider } from "posthog-js/react";
|
import { PostHogProvider } from "posthog-js/react";
|
||||||
import posthog from "posthog-js";
|
import posthog from "posthog-js";
|
||||||
|
import { StrictMode } from "react";
|
||||||
|
|
||||||
window.global ||= window;
|
window.global ||= window;
|
||||||
|
|
||||||
@@ -52,39 +52,44 @@ posthog.init(import.meta.env.VITE_PUBLIC_POSTHOG_KEY, {
|
|||||||
|
|
||||||
const sentryCreateBrowserRouter = Sentry.wrapCreateBrowserRouterV6(createBrowserRouter);
|
const sentryCreateBrowserRouter = Sentry.wrapCreateBrowserRouterV6(createBrowserRouter);
|
||||||
|
|
||||||
const router = sentryCreateBrowserRouter(
|
const router = sentryCreateBrowserRouter(createRoutesFromElements(<Route path="*" element={<AppContainer />} />), {
|
||||||
createRoutesFromElements(<Route path="*" element={<AppContainer />} />),
|
future: {
|
||||||
{
|
v7_startTransition: true,
|
||||||
future: {
|
v7_relativeSplatPath: true
|
||||||
v7_startTransition: true,
|
|
||||||
v7_relativeSplatPath: true,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
|
|
||||||
if (import.meta.env.DEV) {
|
if (import.meta.env.DEV) {
|
||||||
let styles =
|
const styles =
|
||||||
"font-weight: bold; font-size: 50px;color: red; 6px 6px 0 rgb(226,91,14) , 9px 9px 0 rgb(245,221,8) , 12px 12px 0 rgb(5,148,68) ";
|
"font-weight: bold; font-size: 50px;color: red; 6px 6px 0 rgb(226,91,14) , 9px 9px 0 rgb(245,221,8) , 12px 12px 0 rgb(5,148,68) ";
|
||||||
|
|
||||||
console.log("%c %s", styles, `VER: ${import.meta.env.VITE_APP_INSTANCE}`);
|
console.log("%c %s", styles, `VER: ${import.meta.env.VITE_APP_INSTANCE}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<PersistGate loading={<LoadingSpinner message="Restoring your settings..." />} persistor={persistor}>
|
<Provider store={store}>
|
||||||
<Provider store={store}>
|
<PersistGate loading={<LoadingSpinner message="Restoring your settings..." />} persistor={persistor}>
|
||||||
<PostHogProvider client={posthog}>
|
<PostHogProvider client={posthog}>
|
||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
</PostHogProvider>
|
</PostHogProvider>
|
||||||
</Provider>
|
</PersistGate>
|
||||||
</PersistGate>
|
</Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Used for ANTD Component Tokens
|
const rootEl = document.getElementById("root");
|
||||||
// https://ant.design/docs/react/migrate-less-variables
|
|
||||||
ReactDOM.createRoot(document.getElementById("root")).render(
|
if (!rootEl) throw new Error('Missing root element: <div id="root" />');
|
||||||
<ConfigProvider>
|
|
||||||
|
const appTree = import.meta.env.DEV ? (
|
||||||
|
<StrictMode>
|
||||||
<App />
|
<App />
|
||||||
</ConfigProvider>
|
</StrictMode>
|
||||||
|
) : (
|
||||||
|
<App />
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ReactDOM.createRoot(rootEl).render(appTree);
|
||||||
|
|
||||||
reportWebVitals();
|
reportWebVitals();
|
||||||
|
|||||||
@@ -105,9 +105,7 @@ export function BillsListPage({ loading, data, refetch, total, setBillEnterConte
|
|||||||
render: (text, record) => (
|
render: (text, record) => (
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<Link to={`/manage/bills?billid=${record.id}`}>
|
<Link to={`/manage/bills?billid=${record.id}`}>
|
||||||
<Button>
|
<Button icon={<EditFilled />} />
|
||||||
<EditFilled />
|
|
||||||
</Button>
|
|
||||||
</Link>
|
</Link>
|
||||||
{
|
{
|
||||||
// <Button
|
// <Button
|
||||||
@@ -204,9 +202,7 @@ export function BillsListPage({ loading, data, refetch, total, setBillEnterConte
|
|||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
)}
|
)}
|
||||||
<Button onClick={() => refetch()}>
|
<Button onClick={() => refetch()} icon={<SyncOutlined />} />
|
||||||
<SyncOutlined />
|
|
||||||
</Button>
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setBillEnterContext({
|
setBillEnterContext({
|
||||||
|
|||||||
@@ -174,9 +174,7 @@ export function ExportLogsPageComponent() {
|
|||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Button onClick={() => refetch()}>
|
<Button onClick={() => refetch()} icon={<SyncOutlined />} />
|
||||||
<SyncOutlined />
|
|
||||||
</Button>
|
|
||||||
<Input.Search
|
<Input.Search
|
||||||
placeholder={searchParams.search || t("general.labels.search")}
|
placeholder={searchParams.search || t("general.labels.search")}
|
||||||
onSearch={(value) => {
|
onSearch={(value) => {
|
||||||
|
|||||||
@@ -271,17 +271,18 @@ export function JobsDetailPage({
|
|||||||
const menuExtra = (
|
const menuExtra = (
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<Button
|
<Button
|
||||||
|
icon={<SyncOutlined />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
refetch();
|
refetch();
|
||||||
}}
|
}}
|
||||||
key="refresh"
|
key="refresh"
|
||||||
>
|
>
|
||||||
<SyncOutlined />
|
|
||||||
{t("general.labels.refresh")}
|
{t("general.labels.refresh")}
|
||||||
</Button>
|
</Button>
|
||||||
<JobsChangeStatus job={job} />
|
<JobsChangeStatus job={job} />
|
||||||
<JobSyncButton job={job} />
|
<JobSyncButton job={job} />
|
||||||
<Button
|
<Button
|
||||||
|
icon={<PrinterFilled />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setPrintCenterContext({
|
setPrintCenterContext({
|
||||||
actions: { refetch: refetch },
|
actions: { refetch: refetch },
|
||||||
@@ -294,7 +295,6 @@ export function JobsDetailPage({
|
|||||||
}}
|
}}
|
||||||
key="printing"
|
key="printing"
|
||||||
>
|
>
|
||||||
<PrinterFilled />
|
|
||||||
{t("jobs.actions.printCenter")}
|
{t("jobs.actions.printCenter")}
|
||||||
</Button>
|
</Button>
|
||||||
<JobsConvertButton job={job} refetch={refetch} parentFormIsFieldsTouched={form.isFieldsTouched} />
|
<JobsConvertButton job={job} refetch={refetch} parentFormIsFieldsTouched={form.isFieldsTouched} />
|
||||||
|
|||||||
@@ -159,9 +159,7 @@ export function PhonebookPageComponent({ bodyshop, authLevel }) {
|
|||||||
<Button disabled={hasNoAccess} onClick={handleNewPhonebook}>
|
<Button disabled={hasNoAccess} onClick={handleNewPhonebook}>
|
||||||
{t("phonebook.actions.new")}
|
{t("phonebook.actions.new")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={() => refetch()}>
|
<Button onClick={() => refetch()} icon={<SyncOutlined />} />
|
||||||
<SyncOutlined />
|
|
||||||
</Button>
|
|
||||||
<Input.Search
|
<Input.Search
|
||||||
placeholder={searchParams.search || t("general.labels.search")}
|
placeholder={searchParams.search || t("general.labels.search")}
|
||||||
onSearch={(value) => {
|
onSearch={(value) => {
|
||||||
|
|||||||
@@ -94,18 +94,19 @@ export function SimplifiedPartsJobDetailComponent({ setPrintCenterContext, jobRO
|
|||||||
const menuExtra = (
|
const menuExtra = (
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<Button
|
<Button
|
||||||
|
icon={<SyncOutlined />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
refetch();
|
refetch();
|
||||||
}}
|
}}
|
||||||
key="refresh"
|
key="refresh"
|
||||||
>
|
>
|
||||||
<SyncOutlined />
|
|
||||||
{t("general.labels.refresh")}
|
{t("general.labels.refresh")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<JobsChangeStatus job={job} />
|
<JobsChangeStatus job={job} />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
icon={<PrinterFilled />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setPrintCenterContext({
|
setPrintCenterContext({
|
||||||
actions: { refetch: refetch },
|
actions: { refetch: refetch },
|
||||||
@@ -118,7 +119,6 @@ export function SimplifiedPartsJobDetailComponent({ setPrintCenterContext, jobRO
|
|||||||
}}
|
}}
|
||||||
key="printing"
|
key="printing"
|
||||||
>
|
>
|
||||||
<PrinterFilled />
|
|
||||||
{t("jobs.actions.printCenter")}
|
{t("jobs.actions.printCenter")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
|||||||
@@ -176,9 +176,7 @@ export function TechAssignedProdJobs({ setTimeTicketTaskContext, technician, bod
|
|||||||
<Card
|
<Card
|
||||||
extra={
|
extra={
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<Button onClick={() => refetch()}>
|
<Button onClick={() => refetch()} icon={<SyncOutlined />} />
|
||||||
<SyncOutlined />
|
|
||||||
</Button>
|
|
||||||
<Input.Search
|
<Input.Search
|
||||||
placeholder={t("general.labels.search")}
|
placeholder={t("general.labels.search")}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
|||||||
@@ -104,9 +104,7 @@ export function TechDispatchedParts({ technician, bodyshop }) {
|
|||||||
<Card
|
<Card
|
||||||
extra={
|
extra={
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<Button onClick={() => refetch()}>
|
<Button onClick={() => refetch()} icon={<SyncOutlined />} />
|
||||||
<SyncOutlined />
|
|
||||||
</Button>
|
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -2431,7 +2431,8 @@
|
|||||||
"messaging": {
|
"messaging": {
|
||||||
"actions": {
|
"actions": {
|
||||||
"link": "Link to Job",
|
"link": "Link to Job",
|
||||||
"new": "New Conversation"
|
"new": "New Conversation",
|
||||||
|
"openchat": "Open Chat"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"invalidphone": "The phone number is invalid. Unable to open conversation. ",
|
"invalidphone": "The phone number is invalid. Unable to open conversation. ",
|
||||||
|
|||||||
@@ -2428,7 +2428,8 @@
|
|||||||
"messaging": {
|
"messaging": {
|
||||||
"actions": {
|
"actions": {
|
||||||
"link": "",
|
"link": "",
|
||||||
"new": ""
|
"new": "",
|
||||||
|
"openchat": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"invalidphone": "",
|
"invalidphone": "",
|
||||||
|
|||||||
@@ -2428,7 +2428,9 @@
|
|||||||
"messaging": {
|
"messaging": {
|
||||||
"actions": {
|
"actions": {
|
||||||
"link": "",
|
"link": "",
|
||||||
"new": ""
|
"new": "",
|
||||||
|
|
||||||
|
"openchat": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"invalidphone": "",
|
"invalidphone": "",
|
||||||
|
|||||||
@@ -38,4 +38,15 @@ i18n
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Enable HMR for translation files in development
|
||||||
|
if (import.meta.hot) {
|
||||||
|
import.meta.hot.accept(() => {
|
||||||
|
// When translation files change, do a full reload
|
||||||
|
// This is the most reliable approach for i18n updates
|
||||||
|
if (!import.meta.env.VITE_STOP_RELOAD_ON_HOT_UPDATE) {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export default i18n;
|
export default i18n;
|
||||||
|
|||||||
@@ -11,13 +11,8 @@ export default function PhoneNumberFormatter({ children, type }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
<Text>{phone}</Text>
|
<span>{phone}</span>
|
||||||
{type ? (
|
{type ? <Text type="secondary"> ({type})</Text> : null}
|
||||||
<>
|
|
||||||
{" "}
|
|
||||||
<Text type="secondary">({type})</Text>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user