Compare commits

...

11 Commits

Author SHA1 Message Date
Dave
01fec9fa79 hotfix/missing-dms-migration - Add Back DMS_ID 2025-12-19 15:12:24 -05:00
Dave Richer
2f88d613c3 Merged in release/2025-12-19-mini (pull request #2732)
Release/2025 12 19 mini - IO-3467 IO-3468 IO-3402 IO-3473
2025-12-19 19:14:30 +00:00
Dave Richer
c9467b3982 Merged in feature/IO-3402-Import-Add-Notifiers (pull request #2734)
feature/IO-3402-Import-Add-Notifiers - Fix Normalize
2025-12-19 19:12:04 +00:00
Dave
ca4c48bd5c release/2025-12-19-mini - Merge 2025-12-19 12:41:23 -05:00
Dave Richer
e5fd5c8bcb Merged in feature/IO-3468-sentry-exceptions (pull request #2731)
Feature/IO-3468 sentry exceptions
2025-12-19 17:39:23 +00:00
Dave Richer
46945a24a7 Merged in feature/IO-3467-report-center-filter-ui (pull request #2730)
IO-3467 Resolve UI on report center filter.
2025-12-19 17:35:47 +00:00
Dave Richer
be746500a6 Merged in feature/IO-3402-Import-Add-Notifiers (pull request #2729)
Feature/IO-3402 Import Add Notifiers
2025-12-19 17:35:12 +00:00
Dave
71c6d9fa94 IO-3473 trim user input 2025-12-19 12:07:30 -05:00
Dave
6d94ce7e5c feature/IO-3468-Senty-Exceptions - Fix unused import 2025-12-18 13:52:55 -05:00
Patrick Fic
182a8d59ab IO-3468 Add sentry exceptions & minor nul coalesce fixes. 2025-12-16 10:28:00 -08:00
Patrick Fic
f1847ef650 IO-3467 Resolve UI on report center filter. 2025-12-15 08:37:32 -08:00
10 changed files with 155 additions and 56 deletions

View File

@@ -609,7 +609,7 @@ export function JobsDetailHeaderActions({
<FormDateTimePickerComponent
onBlur={() => {
const start = form.getFieldValue("start");
form.setFieldsValue({ end: start.add(30, "minutes") });
form.setFieldsValue({ end: start?.add(30, "minutes") });
}}
/>
</Form.Item>

View File

@@ -144,7 +144,7 @@ export function ProductionListEmpAssignment({ insertAuditTrail, bodyshop, record
<Spin spinning={loading}>
{record[type] ? (
<div>
<span>{`${theEmployee.first_name || ""} ${theEmployee.last_name || ""}`}</span>
<span>{`${theEmployee?.first_name || ""} ${theEmployee?.last_name || ""}`}</span>
<DeleteFilled style={iconStyle} onClick={() => handleRemove(type)} />
</div>
) : (

View File

@@ -143,7 +143,7 @@ export function ReportCenterModalComponent({ reportCenterModal, bodyshop }) {
//TODO: Find a way to filter out / blur on demand.
return (
<div>
<div className="report-center-modal">
<Form onFinish={handleFinish} autoComplete={"off"} layout="vertical" form={form}>
<Input.Search onChange={(e) => setSearch(e.target.value)} value={search} />
<Form.Item name="defaultSorters" hidden />
@@ -163,13 +163,14 @@ export function ReportCenterModalComponent({ reportCenterModal, bodyshop }) {
{Object.keys(grouped)
//.filter((key) => !groupExcludeKeyFilter.includes(key))
.map((key) => (
<Col md={8} sm={12} key={key}>
<Col xs={24} sm={12} md={Object.keys(grouped).length === 1 ? 24 : 8} key={key}>
<Card.Grid
style={{
width: "100%",
height: "100%",
maxHeight: "33vh",
overflowY: "scroll"
overflowY: "scroll",
minWidth: "200px"
}}
>
<Typography.Title level={4}>{t(`reportcenter.labels.groups.${key}`)}</Typography.Title>
@@ -177,7 +178,7 @@ export function ReportCenterModalComponent({ reportCenterModal, bodyshop }) {
<BlurWrapperComponent
featureName={groupExcludeKeyFilter.find((g) => g.key === key).featureName}
>
<ul style={{ listStyleType: "none", columns: "2 auto" }}>
<ul style={{ listStyleType: "none", columns: grouped[key].length > 4 ? "2 auto" : "1", padding: 0, margin: 0 }}>
{grouped[key].map((item) => (
<li key={item.key}>
<Radio key={item.key} value={item.key}>
@@ -188,7 +189,7 @@ export function ReportCenterModalComponent({ reportCenterModal, bodyshop }) {
</ul>
</BlurWrapperComponent>
) : (
<ul style={{ listStyleType: "none", columns: "2 auto" }}>
<ul style={{ listStyleType: "none", columns: grouped[key].length > 4 ? "2 auto" : "1", padding: 0, margin: 0 }}>
{grouped[key].map((item) =>
item.featureNameRestricted ? (
<li key={item.key}>

View File

@@ -11,3 +11,38 @@
}
}
}
// Report center modal fixes for column layout
.report-center-modal {
.ant-form-item .ant-radio-group {
width: 100%;
.ant-card-grid {
padding: 16px;
box-sizing: border-box;
ul {
width: 100%;
li {
margin-bottom: 8px;
break-inside: avoid;
page-break-inside: avoid;
.ant-radio-wrapper {
display: flex;
align-items: flex-start;
width: 100%;
span:not(.ant-radio) {
word-break: break-word;
overflow-wrap: break-word;
hyphens: auto;
flex: 1;
}
}
}
}
}
}
}

View File

@@ -5,7 +5,7 @@ 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'
// import posthog from 'posthog-js'
const config = JSON.parse(import.meta.env.VITE_APP_FIREBASE_CONFIG);
initializeApp(config);
@@ -74,7 +74,6 @@ onMessage(messaging, (payload) => {
export const logImEXEvent = (eventName, additionalParams, stateProp = null) => {
try {
const state = stateProp || store.getState();
const eventParams = {
@@ -99,8 +98,7 @@ export const logImEXEvent = (eventName, additionalParams, stateProp = null) => {
// );
logEvent(analytics, eventName, eventParams);
//amplitude.track(eventName, eventParams);
posthog.capture(eventName, eventParams);
//posthog.capture(eventName, eventParams);
} finally {
//If it fails, just keep going.
}

View File

@@ -31,7 +31,8 @@ if (!import.meta.env.DEV) {
"Module specifier, 'fs' does not start",
"Module specifier, 'zlib' does not start with",
"Messaging: This browser doesn't support the API's required to use the Firebase SDK.",
"Failed to update a ServiceWorker for scope"
"Failed to update a ServiceWorker for scope",
"Network Error"
],
integrations: [
// See docs for support of different versions of variation of react router

View File

@@ -24,11 +24,13 @@ const lightningCssTargets = browserslistToTargets(
})
);
const currentDatePST = new Date()
.toLocaleDateString("en-US", { timeZone: "America/Los_Angeles", year: "numeric", month: "2-digit", day: "2-digit" })
.split("/")
.reverse()
.join("-");
const pstFormatter = new Intl.DateTimeFormat("en-CA", {
timeZone: "America/Los_Angeles",
year: "numeric",
month: "2-digit",
day: "2-digit"
});
const currentDatePST = pstFormatter.format(new Date());
const getFormattedTimestamp = () =>
new Date().toLocaleTimeString("en-US", { hour12: true }).replace("AM", "a.m.").replace("PM", "p.m.");

View File

@@ -0,0 +1,4 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- alter table "public"."jobs" add column "dms_id" text
-- null;

View File

@@ -0,0 +1,2 @@
alter table "public"."jobs" add column "dms_id" text
null;

View File

@@ -77,9 +77,8 @@ const generateResetLink = async (email) => {
*/
const ensureExternalIdUnique = async (externalId) => {
const resp = await client.request(CHECK_EXTERNAL_SHOP_ID, { key: externalId });
if (resp.bodyshops.length) {
throw { status: 400, message: `external_shop_id '${externalId}' is already in use.` };
}
return !!resp.bodyshops.length;
};
/**
@@ -225,10 +224,25 @@ const patchPartsManagementProvisioning = async (req, res) => {
*/
const partsManagementProvisioning = async (req, res) => {
const { logger } = req;
const body = { ...req.body, userEmail: req.body.userEmail?.toLowerCase() };
// Trim and normalize email early
const body = {
...req.body,
userEmail: req.body.userEmail?.trim().toLowerCase()
};
const trim = (value) => (typeof value === "string" ? value.trim() : value);
const trimIfString = (value) =>
value !== null && value !== undefined && typeof value === "string" ? value.trim() : value;
try {
// Ensure email is present and trimmed before checking registration
if (!body.userEmail) {
throw { status: 400, message: "userEmail is required" };
}
await ensureEmailNotRegistered(body.userEmail);
requireFields(body, [
"external_shop_id",
"shopname",
@@ -242,28 +256,68 @@ const partsManagementProvisioning = async (req, res) => {
"userEmail"
]);
// TODO add in check for early access
await ensureExternalIdUnique(body.external_shop_id);
// Trim all top-level string fields
const trimmedBody = {
...body,
external_shop_id: trim(body.external_shop_id),
shopname: trim(body.shopname),
address1: trim(body.address1),
address2: trimIfString(body.address2),
city: trim(body.city),
state: trim(body.state),
zip_post: trim(body.zip_post),
country: trim(body.country),
email: trim(body.email),
phone: trim(body.phone),
timezone: trimIfString(body.timezone),
logoUrl: trimIfString(body.logoUrl),
userPassword: body.userPassword, // passwords should NOT be trimmed (preserves intentional spaces if any, though rare)
vendors: Array.isArray(body.vendors)
? body.vendors.map((v) => ({
name: trim(v.name),
street1: trimIfString(v.street1),
street2: trimIfString(v.street2),
city: trimIfString(v.city),
state: trimIfString(v.state),
zip: trimIfString(v.zip),
country: trimIfString(v.country),
email: trimIfString(v.email),
cost_center: trimIfString(v.cost_center),
phone: trimIfString(v.phone),
dmsid: trimIfString(v.dmsid),
discount: v.discount ?? 0,
due_date: v.due_date ?? null,
favorite: v.favorite ?? [],
active: v.active ?? true
}))
: []
};
logger.log("admin-create-shop-user", "debug", body.userEmail, null, {
const duplicateCheck = await ensureExternalIdUnique(trimmedBody.external_shop_id);
if (duplicateCheck) {
throw { status: 400, message: `external_shop_id '${trimmedBody.external_shop_id}' is already in use.` };
}
logger.log("admin-create-shop-user", "debug", trimmedBody.userEmail, null, {
request: req.body,
ioadmin: true
});
const shopInput = {
shopname: body.shopname,
address1: body.address1,
address2: body.address2 || null,
city: body.city,
state: body.state,
zip_post: body.zip_post,
country: body.country,
email: body.email,
external_shop_id: body.external_shop_id,
timezone: body.timezone || DefaultNewShop.timezone,
phone: body.phone,
shopname: trimmedBody.shopname,
address1: trimmedBody.address1,
address2: trimmedBody.address2,
city: trimmedBody.city,
state: trimmedBody.state,
zip_post: trimmedBody.zip_post,
country: trimmedBody.country,
email: trimmedBody.email,
external_shop_id: trimmedBody.external_shop_id,
timezone: trimmedBody.timezone || DefaultNewShop.timezone,
phone: trimmedBody.phone,
logo_img_path: {
src: body.logoUrl,
src: trimmedBody.logoUrl || null, // allow empty logo
width: "",
height: "",
headerMargin: DefaultNewShop.logo_img_path.headerMargin
@@ -288,35 +342,37 @@ const partsManagementProvisioning = async (req, res) => {
appt_alt_transport: DefaultNewShop.appt_alt_transport,
md_jobline_presets: DefaultNewShop.md_jobline_presets,
vendors: {
data: body.vendors.map((v) => ({
data: trimmedBody.vendors.map((v) => ({
name: v.name,
street1: v.street1 || null,
street2: v.street2 || null,
city: v.city || null,
state: v.state || null,
zip: v.zip || null,
country: v.country || null,
email: v.email || null,
discount: v.discount ?? 0,
due_date: v.due_date ?? null,
cost_center: v.cost_center || null,
favorite: v.favorite ?? [],
phone: v.phone || null,
active: v.active ?? true,
dmsid: v.dmsid || null
street1: v.street1,
street2: v.street2,
city: v.city,
state: v.state,
zip: v.zip,
country: v.country,
email: v.email,
discount: v.discount,
due_date: v.due_date,
cost_center: v.cost_center,
favorite: v.favorite,
phone: v.phone,
active: v.active,
dmsid: v.dmsid
}))
}
};
const newShopId = await insertBodyshop(shopInput);
const userRecord = await createFirebaseUser(body.userEmail, body.userPassword);
const userRecord = await createFirebaseUser(trimmedBody.userEmail, trimmedBody.userPassword);
let resetLink = null;
if (!body.userPassword) resetLink = await generateResetLink(body.userEmail);
if (!trimmedBody.userPassword) {
resetLink = await generateResetLink(trimmedBody.userEmail);
}
const createdUser = await insertUserAssociation(userRecord.uid, body.userEmail, newShopId);
const createdUser = await insertUserAssociation(userRecord.uid, trimmedBody.userEmail, newShopId);
return res.status(200).json({
shop: { id: newShopId, shopname: body.shopname },
shop: { id: newShopId, shopname: trimmedBody.shopname },
user: {
id: createdUser.id,
email: createdUser.email,
@@ -324,7 +380,7 @@ const partsManagementProvisioning = async (req, res) => {
}
});
} catch (err) {
logger.log("admin-create-shop-user-error", "error", body.userEmail, null, {
logger.log("admin-create-shop-user-error", "error", body.userEmail || "unknown", null, {
message: err.message,
detail: err.detail || err
});