Merged in feature/IO-3499-React-19 (pull request #2883)

Feature/IO-3499 React 19
This commit is contained in:
Dave Richer
2026-01-24 01:48:11 +00:00
86 changed files with 402 additions and 427 deletions

View File

@@ -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);

View File

@@ -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>
); );

View File

@@ -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>
); );
} }

View File

@@ -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

View File

@@ -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>

View File

@@ -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

View File

@@ -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>
); );
} }

View File

@@ -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>
); );
} }

View File

@@ -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>

View File

@@ -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>
); );
} }

View File

@@ -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) => {

View File

@@ -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>

View File

@@ -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 }}

View File

@@ -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>

View File

@@ -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

View File

@@ -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")} />

View File

@@ -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")} />

View File

@@ -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>
); );

View File

@@ -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) => {

View File

@@ -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>
); );

View File

@@ -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 : []} />

View File

@@ -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

View File

@@ -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>
)} )}

View File

@@ -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>
)} )}

View File

@@ -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,

View File

@@ -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>
); );

View File

@@ -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>
); );

View File

@@ -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")}

View File

@@ -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()

View File

@@ -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>
); );

View File

@@ -56,9 +56,8 @@ export default function JobsCreateVehicleInfoPredefined({ disabled, form }) {
setOpen(false); setOpen(false);
setSearch(""); setSearch("");
}} }}
> icon={<PlusOutlined />}
<PlusOutlined /> />
</Button>
) )
} }
]} ]}

View File

@@ -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>

View File

@@ -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} />

View File

@@ -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} />

View File

@@ -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>

View File

@@ -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) => {

View File

@@ -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) => {

View File

@@ -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) => {

View File

@@ -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,

View File

@@ -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) => {

View File

@@ -246,9 +246,8 @@ export function PayrollLaborAllocationsTable({
setTotals(data); setTotals(data);
refetch(); refetch();
}} }}
> icon={<SyncOutlined />}
<SyncOutlined /> />
</Button>
</Space> </Space>
} }
> >

View File

@@ -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>
); );

View File

@@ -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);

View File

@@ -173,3 +173,11 @@
} }
} }
} }
.notification-center--dark {
color-scheme: dark;
}
.notification-center--light {
color-scheme: light;
}

View File

@@ -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) => {

View File

@@ -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}

View File

@@ -34,9 +34,7 @@ export default function PartsOrderDeleteLine({ disabled, partsLineId, partsOrder
}); });
}} }}
> >
<Button disabled={disabled}> <Button disabled={disabled} icon={<DeleteFilled />} />
<DeleteFilled />
</Button>
</Popconfirm> </Popconfirm>
); );
} }

View File

@@ -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

View File

@@ -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

View File

@@ -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) => {

View File

@@ -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>

View File

@@ -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}

View File

@@ -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}

View File

@@ -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} />

View File

@@ -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")}

View File

@@ -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>
); );

View File

@@ -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>

View File

@@ -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>
);
} }

View File

@@ -159,9 +159,8 @@ export function ShopEmployeesFormComponent({ bodyshop }) {
} }
}); });
}} }}
> icon={<DeleteFilled />}
<DeleteFilled /> />
</Button>
) )
} }
]; ];

View File

@@ -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

View File

@@ -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) => {

View File

@@ -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>
); );

View File

@@ -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}
/> />
); );
}; };

View File

@@ -141,3 +141,11 @@
text-align: center; text-align: center;
} }
} }
.task-center--dark {
color-scheme: dark;
}
.task-center--light {
color-scheme: light;
}

View File

@@ -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>

View File

@@ -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>
} }

View File

@@ -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) => {

View File

@@ -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>
); );

View File

@@ -322,9 +322,8 @@ export function TimeTicketList({
onClick={async () => { onClick={async () => {
refetch(); refetch();
}} }}
> icon={<SyncOutlined />}
<SyncOutlined /> />
</Button>
</Space> </Space>
} }
> >

View File

@@ -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>

View File

@@ -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>
} }
> >

View File

@@ -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) => {

View File

@@ -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) => {

View File

@@ -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();

View File

@@ -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({

View File

@@ -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) => {

View File

@@ -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} />

View File

@@ -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) => {

View File

@@ -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>

View File

@@ -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) => {

View File

@@ -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>
} }
> >

View File

@@ -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. ",

View File

@@ -2428,7 +2428,8 @@
"messaging": { "messaging": {
"actions": { "actions": {
"link": "", "link": "",
"new": "" "new": "",
"openchat": ""
}, },
"errors": { "errors": {
"invalidphone": "", "invalidphone": "",

View File

@@ -2428,7 +2428,9 @@
"messaging": { "messaging": {
"actions": { "actions": {
"link": "", "link": "",
"new": "" "new": "",
"openchat": ""
}, },
"errors": { "errors": {
"invalidphone": "", "invalidphone": "",

View File

@@ -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;

View File

@@ -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>
); );
} }