Compare commits

..

7 Commits

Author SHA1 Message Date
Patrick Fic
a722ab9758 hotfix/2025-08-22 - Datahog 2025-08-22 15:35:55 -04:00
Patrick Fic
41c9c0be49 Add in amplititude 2025-08-22 15:22:45 -04:00
Allan Carr
141b05f558 IO-3349 Chart Enqueue and Label Required
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2025-08-22 11:29:33 -07:00
Dave Richer
8c9ef375be Merged in hotfix/background-colors-dark-mode (pull request #2485)
Hotfix/background colors dark mode
2025-08-18 18:39:42 +00:00
Dave
8295cb111a Fix Dark Mode Schedule 2025-08-18 14:37:00 -04:00
Dave
951d214d49 feature/IO-3255-simplified-parts-management - Beef Up Change Request Parser, add Change Request documentation data 2025-08-18 14:13:16 -04:00
Dave Richer
6f19c1dd3f Merged in release/2025-08-15 (pull request #2478)
Release/2025-08-15 into master-AIO - IO-1113, IO-3285, IO-3307, IO-3330, IO-3332, IO-3335
2025-08-16 01:13:04 +00:00
18 changed files with 1224 additions and 1132 deletions

View File

@@ -14,3 +14,5 @@ VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
VITE_APP_INSTANCE=IMEX
TEST_USERNAME="test@imex.dev"
TEST_PASSWORD="test123"
VITE_PUBLIC_POSTHOG_KEY=phc_xtLmBIu0rjWwExY73Oj5DTH1bGbwq1G1Y8jnlTceien
VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com

View File

@@ -16,3 +16,5 @@ VITE_APP_COUNTRY=USA
VITE_APP_INSTANCE=ROME
TEST_USERNAME="test@imex.dev"
TEST_PASSWORD="test123"
VITE_PUBLIC_POSTHOG_KEY=phc_xtLmBIu0rjWwExY73Oj5DTH1bGbwq1G1Y8jnlTceien
VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com

View File

@@ -13,3 +13,5 @@ VITE_APP_AXIOS_BASE_API_URL=https://api.imex.online/
VITE_APP_REPORTS_SERVER_URL=https://reports.imex.online
VITE_APP_SPLIT_API=et9pjkik6bn67he5evpmpr1agoo7gactphgk
VITE_APP_INSTANCE=IMEX
VITE_PUBLIC_POSTHOG_KEY=phc_xtLmBIu0rjWwExY73Oj5DTH1bGbwq1G1Y8jnlTceien
VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com

View File

@@ -13,3 +13,5 @@ VITE_APP_AXIOS_BASE_API_URL=https://api.romeonline.io/
VITE_APP_REPORTS_SERVER_URL=https://reports.romeonline.io
VITE_APP_SPLIT_API=et9pjkik6bn67he5evpmpr1agoo7gactphgk
VITE_APP_INSTANCE=ROME
VITE_PUBLIC_POSTHOG_KEY=phc_xtLmBIu0rjWwExY73Oj5DTH1bGbwq1G1Y8jnlTceien
VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com

View File

@@ -13,3 +13,5 @@ VITE_APP_REPORTS_SERVER_URL=https://reports.test.imex.online
VITE_APP_IS_TEST=true
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
VITE_APP_INSTANCE=IMEX
VITE_PUBLIC_POSTHOG_KEY=phc_xtLmBIu0rjWwExY73Oj5DTH1bGbwq1G1Y8jnlTceien
VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com

View File

@@ -13,3 +13,5 @@ VITE_APP_REPORTS_SERVER_URL=https://reports.test.romeonline.io
VITE_APP_IS_TEST=true
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
VITE_APP_INSTANCE=ROME
VITE_PUBLIC_POSTHOG_KEY=phc_xtLmBIu0rjWwExY73Oj5DTH1bGbwq1G1Y8jnlTceien
VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com

2115
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,7 @@
"proxy": "http://localhost:4000",
"dependencies": {
"@ant-design/pro-layout": "^7.22.4",
"@amplitude/analytics-browser": "^2.23.1",
"@apollo/client": "^3.13.6",
"@emotion/is-prop-valid": "^1.3.1",
"@fingerprintjs/fingerprintjs": "^4.6.1",
@@ -48,6 +49,7 @@
"normalize-url": "^8.0.2",
"object-hash": "^3.0.0",
"phone": "^3.1.59",
"posthog-js": "^1.260.2",
"prop-types": "^15.8.1",
"query-string": "^9.2.0",
"raf-schd": "^4.0.3",

View File

@@ -1,20 +1,20 @@
import { ApolloProvider } from "@apollo/client";
import * as Sentry from "@sentry/react";
import { SplitFactoryProvider, useSplitClient } from "@splitsoftware/splitio-react";
import { ConfigProvider } from "antd";
import enLocale from "antd/es/locale/en_US";
import { useEffect, useMemo } from "react";
import { CookiesProvider } from "react-cookie";
import { useTranslation } from "react-i18next";
import { connect, useSelector } from "react-redux";
import { createStructuredSelector } from "reselect";
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
import { setDarkMode } from "../redux/application/application.actions";
import { selectDarkMode } from "../redux/application/application.selectors";
import { selectCurrentUser } from "../redux/user/user.selectors.js";
import client from "../utils/GraphQLClient";
import App from "./App";
import * as Sentry from "@sentry/react";
import getTheme from "./themeProvider";
import { CookiesProvider } from "react-cookie";
import { createStructuredSelector } from "reselect";
import { selectCurrentUser } from "../redux/user/user.selectors.js";
import { selectDarkMode } from "../redux/application/application.selectors";
import { setDarkMode } from "../redux/application/application.actions";
// Base Split configuration
const config = {
@@ -86,7 +86,7 @@ function AppContainer({ currentUser, setDarkMode }) {
theme={theme}
form={{
validateMessages: {
required: t("general.validation.required", { label: "{{label}}" })
required: t("general.validation.required", { label: "${label}" })
}
}}
>

View File

@@ -53,7 +53,7 @@
--tech-icon-color: orangered; /* Light mode tech icon color */
--clone-border-color: #1890ff; /* Light mode clone border color */
--event-arrived-bg: rgba(4, 141, 4, 0.4); /* Light mode arrived event background */
--event-block-bg: rgba(212, 2, 2, 0.6); /* Light mode block event background */
--event-block-bg: tomato; /* Light mode block event background */
--event-selected-bg: slategrey; /* Light mode selected event background */
--task-bg: #fff; /* Light mode task center background */
--task-text: rgba(0, 0, 0, 0.85); /* Light mode task text */
@@ -83,7 +83,7 @@
--tag-wrapper-text: #000; /* Light mode tag wrapper text */
--preview-bg: lightgray; /* Light mode preview background */
--preview-border-color: #2196F3; /* Light mode preview border color */
--event-bg-fallback: #ffffff; /* Light mode event background fallback */
--event-bg-fallback: #c4c4c4; /* Light mode event background fallback */
--card-bg-fallback: #ffffff; /* Light mode card background fallback */
--card-text-fallback: black; /* Light mode card text fallback */
--table-row-even-bg: rgb(236, 236, 236); /* Light mode table row even background */
@@ -159,7 +159,7 @@
--tech-icon-color: #ff4500; /* Dark mode tech icon color */
--clone-border-color: #4da8ff; /* Dark mode clone border color */
--event-arrived-bg: rgba(4, 141, 4, 0.6); /* Dark mode arrived event background */
--event-block-bg: rgba(212, 2, 2, 0.8); /* Dark mode block event background */
--event-block-bg: tomato; /* Dark mode block event background */
--event-selected-bg: #4a5e6e; /* Dark mode selected event background */
--task-bg: #2a2a2a; /* Dark mode task center background */
--task-text: rgba(255, 255, 255, 0.85); /* Dark mode task text */
@@ -189,7 +189,7 @@
--tag-wrapper-text: #cccccc; /* Dark mode tag wrapper text */
--preview-bg: #2a2a2a; /* Dark mode preview background */
--preview-border-color: #4da8ff; /* Dark mode preview border color */
--event-bg-fallback: #2a2a2a; /* Dark mode event background fallback */
--event-bg-fallback: #262626; /* Dark mode event background fallback */
--card-bg-fallback: #2a2a2a; /* Dark mode card background fallback */
--card-text-fallback: #cccccc; /* Dark mode card text fallback */
--table-row-even-bg: #2a2a2a; /* Dark mode table row even background */

View File

@@ -424,6 +424,9 @@ export function ScheduleEventComponent({
// Adjust event color for dark mode if needed
const getEventBackground = () => {
if (event?.block) {
return "var(--event-block-bg)"; // Use a specific color for dark mode
}
const baseColor = event.color && event.color.hex ? event.color.hex : event.color || "var(--event-bg-fallback)";
// Optionally adjust color for dark mode (e.g., lighten if too dark)
return baseColor;

View File

@@ -212,6 +212,10 @@ export function ScheduleCalendarHeaderComponent({ bodyshop, label, refetch, date
return bodyshop.workingdays[day];
};
const blocked = isDayBlocked.length > 0;
const headerStyle = blocked ? { color: "#fff" } : { color: isShopOpen(date) ? "" : "tomato" };
const headerClass = `imex-calendar-header-card ${blocked ? "imex-calendar-header-card--blocked" : ""}`.trim();
return (
<div className="imex-calendar-load">
<ScheduleBlockDay alreadyBlocked={isDayBlocked.length > 0} date={date} refetch={refetch}>

View File

@@ -26,6 +26,37 @@
background-color: var(--event-block-bg);
}
/* Ensure readable text when fallback background is used */
.imex-event-fallback,
.imex-event-fallback .rbc-event-content,
.imex-event-fallback .rbc-event-label,
.imex-event-fallback a {
color: var(--card-text-fallback) !important;
}
/* Optional subtle border to distinguish on white backgrounds */
.imex-event-fallback {
border: 1px solid var(--bar-border-color);
}
/* Header day card styling */
.imex-calendar-header-card {
display: inline-block;
padding: 0.15rem 0.35rem;
border-radius: 0.25rem;
}
.imex-calendar-header-card--blocked {
background-color: var(--event-block-bg);
color: #ffffff;
}
.imex-calendar-header-card--blocked a,
.imex-calendar-header-card--blocked span,
.imex-calendar-header-card--blocked .ant-typography {
color: #ffffff;
}
.rbc-month-view {
// height: 125rem;
}

View File

@@ -37,17 +37,39 @@ export function ScheduleCalendarWrapperComponent({
const history = useNavigate();
const { t } = useTranslation();
// Determine current view to compute styles consistently
const currentView = search.view || defaultView || "week";
const handleEventPropStyles = (event, start, end, isSelected) => {
const hasColor = Boolean(event?.color?.hex || event?.color);
const useBg = currentView !== "agenda";
// Prioritize explicit blocked-day background to ensure red in all themes
let bg;
if (useBg) {
if (event?.block) {
bg = "var(--event-block-bg)";
} else if (hasColor) {
bg = event?.color?.hex ?? event?.color;
} else {
bg = "var(--event-bg-fallback)";
}
}
const usedFallback = !hasColor && !event?.block; // only mark as fallback when not blocked
const classes = [
"imex-event",
event.arrived && "imex-event-arrived",
event.block && "imex-event-block",
usedFallback && "imex-event-fallback"
]
.filter(Boolean)
.join(" ");
return {
...(event.color && !((search.view || defaultView) === "agenda")
? {
style: {
backgroundColor:
event.color && event.color.hex ? event.color.hex : event.color || "var(--event-bg-fallback)"
}
}
: {}),
className: `${event.arrived ? "imex-event-arrived" : ""} ${event.block ? "imex-event-block" : ""}`
...(bg ? { style: { backgroundColor: bg } } : {}),
className: classes
};
};

View File

@@ -4,6 +4,8 @@ import { getAuth, updatePassword, updateProfile } from "@firebase/auth";
import { getFirestore } from "@firebase/firestore";
import { getMessaging, getToken, onMessage } from "@firebase/messaging";
import { store } from "../redux/store";
import * as amplitude from '@amplitude/analytics-browser';
import posthog from 'posthog-js'
const config = JSON.parse(import.meta.env.VITE_APP_FIREBASE_CONFIG);
initializeApp(config);
@@ -71,25 +73,33 @@ onMessage(messaging, (payload) => {
});
export const logImEXEvent = (eventName, additionalParams, stateProp = null) => {
const state = stateProp || store.getState();
const eventParams = {
shop: (state.user && state.user.bodyshop && state.user.bodyshop.shopname) || null,
user: (state.user && state.user.currentUser && state.user.currentUser.email) || null,
...additionalParams
};
// axios.post("/ioevent", {
// useremail: (state.user && state.user.currentUser && state.user.currentUser.email) || null,
// bodyshopid: (state.user && state.user.bodyshop && state.user.bodyshop.id) || null,
// operationName: eventName,
// variables: additionalParams,
// dbevent: false,
// env: `master-AIO|${import.meta.env.VITE_APP_GIT_SHA_DATE}`
// });
// console.log(
// "%c[Analytics]",
// "background-color: green ;font-weight:bold;",
// eventName,
// eventParams
// );
logEvent(analytics, eventName, eventParams);
try {
const state = stateProp || store.getState();
const eventParams = {
shop: (state.user && state.user.bodyshop && state.user.bodyshop.shopname) || null,
user: (state.user && state.user.currentUser && state.user.currentUser.email) || null,
...additionalParams
};
// axios.post("/ioevent", {
// useremail: (state.user && state.user.currentUser && state.user.currentUser.email) || null,
// bodyshopid: (state.user && state.user.bodyshop && state.user.bodyshop.id) || null,
// operationName: eventName,
// variables: additionalParams,
// dbevent: false,
// env: `master-AIO|${import.meta.env.VITE_APP_GIT_SHA_DATE}`
// });
// console.log(
// "%c[Analytics]",
// "background-color: green ;font-weight:bold;",
// eventName,
// eventParams
// );
logEvent(analytics, eventName, eventParams);
amplitude.track(eventName, eventParams);
posthog.capture(eventName, eventParams);
} finally {
//If it fails, just keep going.
}
};

View File

@@ -14,6 +14,8 @@ import { persistor, store } from "./redux/store";
import reportWebVitals from "./reportWebVitals";
import "./translations/i18n";
import "./utils/CleanAxios";
import * as amplitude from "@amplitude/analytics-browser";
import { PostHogProvider } from "posthog-js/react";
window.global ||= window;
@@ -23,10 +25,10 @@ registerSW({ immediate: true });
// Dinero.globalLocale = "en-CA";
Dinero.globalRoundingMode = "HALF_EVEN";
amplitude.init("6228a598e57cd66875cfd41604f1f891", {});
const sentryCreateBrowserRouter = Sentry.wrapCreateBrowserRouterV6(createBrowserRouter);
const router = sentryCreateBrowserRouter(createRoutesFromElements(<Route path="*" element={<AppContainer />} />));
if (import.meta.env.DEV) {
let styles =
"font-weight: bold; font-size: 50px;color: red; 6px 6px 0 rgb(226,91,14) , 9px 9px 0 rgb(245,221,8) , 12px 12px 0 rgb(5,148,68) ";
@@ -37,7 +39,12 @@ function App() {
return (
<PersistGate loading={<LoadingSpinner message="Restoring your settings..." />} persistor={persistor}>
<Provider store={store}>
<RouterProvider router={router} />
<PostHogProvider
apiKey={import.meta.env.VITE_PUBLIC_POSTHOG_KEY}
options={{ autocapture: false, capture_exceptions: true }}
>
<RouterProvider router={router} />
</PostHogProvider>
</Provider>
</PersistGate>
);

View File

@@ -48,6 +48,8 @@ import {
validatePasswordResetSuccess
} from "./user.actions";
import UserActionTypes from "./user.types";
import * as amplitude from "@amplitude/analytics-browser";
import posthog from "posthog-js";
const fpPromise = FingerprintJS.load();
@@ -82,8 +84,6 @@ export function* onCheckUserSession() {
export function* isUserAuthenticated() {
try {
logImEXEvent("redux_auth_check");
const user = yield getCurrentUser();
if (!user) {
yield put(unauthorizedUser());
@@ -91,6 +91,8 @@ export function* isUserAuthenticated() {
}
LogRocket.identify(user.email);
amplitude.setUserId(user.email);
posthog.identify(user.email);
const eulaQuery = yield client.query({
query: QUERY_EULA,
@@ -136,7 +138,8 @@ export function* signOutStart() {
imexshopid: state.user.bodyshop.imexshopid,
type: "messaging"
});
} catch (error) {
amplitude.reset();
} catch {
console.log("No FCM token. Skipping unsubscribe.");
}
@@ -161,7 +164,7 @@ export function* updateUserDetails(userDetails) {
type: "success",
message: i18next.t("profile.successes.updated")
});
} catch (error) {
} catch {
//yield put(signOutFailure(error.message));
}
}
@@ -268,7 +271,7 @@ export function* signInSuccessSaga({ payload }) {
setUserId(analytics, payload.email);
setUserProperties(analytics, payload);
yield logImEXEvent("redux_sign_in_success");
yield;
}
export function* onSendPasswordResetStart() {
@@ -335,6 +338,7 @@ export function* SetAuthLevelFromShopDetails({ payload }) {
}
try {
amplitude.setGroup("Shop", payload.shopname);
window.$crisp.push(["set", "user:company", [payload.shopname]]);
window.$crisp.push(["set", "session:segments", [[`region:${payload.region_config}`]]]);
if (authRecord[0] && authRecord[0].user.validemail) {

View File

@@ -73,37 +73,23 @@ const processCanvasRequest = async (req, res) => {
// Default width and height
const width = isNumber(w) && w > 0 ? w : 500;
const height = isNumber(h) && h > 0 ? h : 275;
const configuration = getChartConfiguration(keys, values, override);
let canvas = null;
let ctx = null;
let chart = null;
let chartImage = null;
try {
// Create the canvas
canvas = new Canvas(width, height);
ctx = canvas.getContext("2d");
const canvas = new Canvas(width, height);
const ctx = canvas.getContext("2d");
// Render the chart
chart = new Chart(ctx, configuration);
// Generate and send the image
chartImage = (await canvas.toBuffer("image/png")).toString("base64");
const chartImage = (await canvas.toBuffer("image/png")).toString("base64");
res.status(200).send(`data:image/png;base64,${chartImage}`);
} catch (error) {
// Log the error and send the response
logger.log("canvas-error", "error", "jsr", null, { error: error.message });
res.status(500).send("Failed to generate canvas.");
res.status(500).send("Error generating canvas");
} finally {
// Cleanup resources
if (chart) {
chart.destroy();
}
ctx = null;
canvas = null;
chartImage = null;
chart?.destroy();
}
};
@@ -118,15 +104,18 @@ const enqueueRequest = (req, res) => {
};
const processNextInQueue = async () => {
while (requestQueue.length > 0) {
const { req, res } = requestQueue.shift();
try {
await processCanvasRequest(req, res);
} catch (err) {
console.error("canvas-queue-error", "error", "jsr", null, { error: err.message });
try {
while (requestQueue.length > 0) {
const { req, res } = requestQueue.shift();
try {
await processCanvasRequest(req, res);
} catch (err) {
console.error("canvas-queue-error", "error", "jsr", null, { error: err.message });
}
}
} finally {
isProcessing = false;
}
isProcessing = false;
};
exports.canvastest = function (req, res) {
@@ -134,7 +123,10 @@ exports.canvastest = function (req, res) {
};
exports.canvas = async (req, res) => {
if (isProcessing || !enqueueRequest(req, res)) return;
isProcessing = true;
processNextInQueue().catch((err) => console.error("canvas-processing-error", { error: err.message }));
if (!enqueueRequest(req, res)) return;
if (!isProcessing) {
isProcessing = true;
processNextInQueue().catch((err) => console.error("canvas-processing-error", { error: err.message }));
}
};