Compare commits

..

39 Commits

Author SHA1 Message Date
Allan Carr
d9f562faa4 IO-3121 Adjust Footer
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2025-02-06 08:32:52 -08:00
Allan Carr
b8b76cb96c IO-3121 Generic Report Header
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2025-02-04 18:11:48 -08:00
Allan Carr
fa958cbbfe IO-3121 Generic Report Header
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2025-02-04 17:42:40 -08:00
Dave Richer
2146672916 release/2025-01-31 - fix teams icon 2025-02-04 13:47:58 -05:00
Dave Richer
f96460f332 Merged in feature/IO-2825-Node-22-Update (pull request #2092)
[DO NOT MERGE] Feature/IO-2825 Node 22 Update

Approved-by: Patrick Fic
2025-02-04 17:54:06 +00:00
Dave Richer
04c70876d0 Merged release/2025-01-31 into feature/IO-2825-Node-22-Update 2025-02-04 17:53:52 +00:00
Dave Richer
bc6a94eede Merged in feature/IO-3123-Code-Review-Adjustments (pull request #2099)
feature/IO-3123-Code-Review-Adjustments: Make Code review adjustments
2025-02-04 17:53:36 +00:00
Dave Richer
f288b0ee22 feature/IO-3123-Code-Review-Adjustments: Make Code review adjustments 2025-02-04 12:28:22 -05:00
Patrick Fic
e54692928b Merged in feature/IO-3076-report-trigger (pull request #2098)
IO-3076 Add cron trigger for RO usage report.

Approved-by: Patrick Fic
2025-02-04 16:28:55 +00:00
Patrick Fic
0fd8bcb1b1 IO-3076 Add cron trigger for RO usage report. 2025-02-04 08:28:06 -08:00
Dave Richer
07b18836f5 feature/IO-2825-Node-22-Update: Fix Header styling 2025-02-03 16:11:14 -05:00
Dave Richer
ff08d19d79 Merged release/2025-01-31 into feature/IO-2825-Node-22-Update 2025-02-03 16:46:01 +00:00
Dave Richer
bd6f300c8d Merged in feature/IO-2970-Production-Board-Unassigned-Filter (pull request #2096)
feature/IO-2970-Production-Board-Unassigned-Filter - Implementation
2025-01-31 18:24:50 +00:00
Dave Richer
ac2fbaf6f7 feature/IO-2970-Production-Board-Unassigned-Filter - Implementation 2025-01-31 13:23:36 -05:00
Allan Carr
f409acc7fd Merged in feature/IO-3116-Production-Flag-Translation (pull request #2093)
IO-3116 Production Flag Translation

Approved-by: Dave Richer
2025-01-31 15:59:15 +00:00
Allan Carr
06dcb20b2b Merged in feature/IO-3074-Mark-as-PST-Exempt-Job-Create (pull request #2094)
IO-3074 Mark as PST Exempt in Manaul Job Creation

Approved-by: Dave Richer
2025-01-31 15:58:42 +00:00
Allan Carr
f4fed0db9d IO-3074 Mark as PST Exempt in Manaul Job Creation
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2025-01-30 15:25:39 -08:00
Allan Carr
8430f500ef IO-3116 Production Flag Translation
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2025-01-30 14:30:47 -08:00
Dave Richer
68584243f4 Merge remote-tracking branch 'origin/release/2025-01-31' into feature/IO-2825-Node-22-Update 2025-01-30 15:37:37 -05:00
Dave Richer
c8f5c3ed9e release/2025-01-31 - Fix unused import 2025-01-30 15:36:47 -05:00
Dave Richer
994d7e17aa feature/IO-2825-Node-22-Update - Package Updates 2025-01-30 15:36:05 -05:00
Dave Richer
fd1dd6dddd Merge remote-tracking branch 'origin/release/2025-01-31' into feature/IO-2825-Node-22-Update 2025-01-30 15:32:31 -05:00
Dave Richer
1e9b82ba1e feature/IO-2825-Node-22-Update - Merge release 2025-01-30 15:32:11 -05:00
Dave Richer
312795618e Merged in feature/IO-2681-Share-To-Teams-Button (pull request #2090)
Feature/IO-2681 Share To Teams Button
2025-01-30 20:17:24 +00:00
Dave Richer
35b5645d6f feature/IO-2681-Share-To-Teams-Button - Missing translation 2025-01-30 15:16:56 -05:00
Dave Richer
a49d845f50 feature/IO-2681-Share-To-Teams-Button - Merge release 2025-01-30 15:13:48 -05:00
Dave Richer
9d44540ca8 feature/IO-2681-Share-To-Teams-Button - Final revisions. 2025-01-30 15:06:52 -05:00
Allan Carr
cc95d3bd44 Merged in feature/IO-3115-Print-Center-on-Job-Close (pull request #2088)
IO-3115 Print Center on Job Close

Approved-by: Dave Richer
2025-01-30 17:12:11 +00:00
Allan Carr
648c47cde2 IO-3115 Change Icon to internal button prop
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2025-01-30 09:12:58 -08:00
Allan Carr
17cf6e7696 Merged in feature/IO-1582-Temp-Docs-to-Exported-Files (pull request #2087)
IO-1582 Temp Docs to Exported/Invoiced Files

Approved-by: Dave Richer
2025-01-30 16:42:54 +00:00
Allan Carr
7326ffbae6 Merged in feature/IO-3114-Quick-Intake (pull request #2086)
Feature/IO-3114 Quick Intake

Approved-by: Dave Richer
2025-01-30 16:42:36 +00:00
Allan Carr
b2f73c4fba IO-3115 Print Center on Job Close
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2025-01-29 16:55:31 -08:00
Allan Carr
6628d43e12 IO-1582 Temp Docs to Exported/Invoiced Files
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2025-01-29 15:43:38 -08:00
Dave Richer
a064b8e07e feature/IO-2681-Share-To-Teams-Button - checkpoint 2025-01-29 18:19:34 -05:00
Dave Richer
da41668b3f feature/IO-3096-Notification-Preferences - Update Circle CI to Node 22 2025-01-22 10:56:53 -08:00
Dave Richer
df008abec9 Merge remote-tracking branch 'origin/master-AIO' into feature/IO-3096-Global-Notification-Preferences 2025-01-22 10:53:42 -08:00
Dave Richer
29c99f2dd9 feature/IO-3096-Global-Notification-Preferences - Upgrade Node to 22 / Remove canvas and replace it 100% with canvas-skia 2025-01-20 09:02:55 -08:00
Dave Richer
3033e84f45 Merge remote-tracking branch 'origin/master-AIO' into feature/IO-3096-Global-Notification-Preferences 2025-01-20 08:33:31 -08:00
Dave Richer
18966476e4 feature/IO-3096-Global-Notifications-Preferences
-  Package Updates
2025-01-17 09:26:30 -08:00
36 changed files with 7840 additions and 5693 deletions

View File

@@ -9,13 +9,13 @@ orbs:
jobs:
imex-api-deploy:
docker:
- image: cimg/node:18.18.2
- image: cimg/node:22.13.1
steps:
- checkout
- eb/setup
- run:
command: |
eb init imex-online-production-api -r ca-central-1 -p "Node.js 18 running on 64bit Amazon Linux 2"
eb init imex-online-production-api -r ca-central-1 -p "Node.js 22 running on 64bit Amazon Linux 2"
eb status --verbose
eb deploy
eb status
@@ -28,7 +28,7 @@ jobs:
imex-hasura-migrate:
docker:
- image: cimg/node:18.18.2
- image: cimg/node:22.13.1
parameters:
secret:
type: string
@@ -52,7 +52,7 @@ jobs:
pipeline_number: << pipeline.number >>
imex-app-build:
docker:
- image: cimg/node:18.18.2
- image: cimg/node:22.13.1
resource_class: large
working_directory: ~/repo/client
steps:
@@ -77,7 +77,7 @@ jobs:
imex-app-beta-build:
docker:
- image: cimg/node:18.18.2
- image: cimg/node:22.13.1
resource_class: large
working_directory: ~/repo/client
@@ -114,7 +114,7 @@ jobs:
- eb/setup
- run:
command: |
eb init romeonline-productionapi -r us-east-2 -p "Node.js 18 running on 64bit Amazon Linux 2"
eb init romeonline-productionapi -r us-east-2 -p "Node.js 22 on 64bit Amazon Linux 2"
eb status --verbose
eb deploy
eb status
@@ -126,7 +126,7 @@ jobs:
pipeline_number: << pipeline.number >>
rome-hasura-migrate:
docker:
- image: cimg/node:18.18.2
- image: cimg/node:22.13.1
parameters:
secret:
type: string
@@ -150,7 +150,7 @@ jobs:
pipeline_number: << pipeline.number >>
rome-app-build:
docker:
- image: cimg/node:18.18.2
- image: cimg/node:22.13.1
working_directory: ~/repo/client
@@ -181,7 +181,7 @@ jobs:
test-rome-hasura-migrate:
docker:
- image: cimg/node:18.18.2
- image: cimg/node:22.13.1
parameters:
secret:
type: string
@@ -208,7 +208,7 @@ jobs:
test-rome-app-build:
docker:
- image: cimg/node:18.18.2
- image: cimg/node:22.13.1
working_directory: ~/repo/client
@@ -239,7 +239,7 @@ jobs:
test-hasura-migrate:
docker:
- image: cimg/node:18.18.2
- image: cimg/node:22.13.1
parameters:
secret:
type: string
@@ -266,7 +266,7 @@ jobs:
imex-test-app-build:
docker:
- image: cimg/node:18.18.2
- image: cimg/node:22.13.1
resource_class: large
working_directory: ~/repo/client
@@ -286,7 +286,7 @@ jobs:
imex-test-app-beta-build:
docker:
- image: cimg/node:18.18.2
- image: cimg/node:22.13.1
resource_class: large
working_directory: ~/repo/client

View File

@@ -3,7 +3,7 @@ FROM amazonlinux:2023
# Install Git and Node.js (Amazon Linux 2023 uses the DNF package manager)
RUN dnf install -y git \
&& curl -sL https://rpm.nodesource.com/setup_20.x | bash - \
&& curl -sL https://rpm.nodesource.com/setup_22.x | bash - \
&& dnf install -y nodejs \
&& dnf clean all

9684
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,74 +8,74 @@
"private": true,
"proxy": "http://localhost:4000",
"dependencies": {
"@ant-design/pro-layout": "^7.19.12",
"@apollo/client": "^3.11.8",
"@ant-design/pro-layout": "^7.22.0",
"@apollo/client": "^3.12.6",
"@emotion/is-prop-valid": "^1.3.1",
"@fingerprintjs/fingerprintjs": "^4.5.0",
"@fingerprintjs/fingerprintjs": "^4.5.1",
"@jsreport/browser-client": "^3.1.0",
"@reduxjs/toolkit": "^2.2.7",
"@sentry/cli": "^2.36.2",
"@reduxjs/toolkit": "^2.5.0",
"@sentry/cli": "^2.40.0",
"@sentry/react": "^7.114.0",
"@splitsoftware/splitio-react": "^1.13.0",
"@tanem/react-nprogress": "^5.0.51",
"@vitejs/plugin-react": "^4.3.1",
"antd": "^5.20.1",
"@tanem/react-nprogress": "^5.0.53",
"@vitejs/plugin-react": "^4.3.4",
"antd": "^5.23.1",
"apollo-link-logger": "^2.0.1",
"apollo-link-sentry": "^3.3.0",
"autosize": "^6.0.1",
"axios": "^1.7.7",
"axios": "^1.7.9",
"classnames": "^2.5.1",
"css-box-model": "^1.2.1",
"dayjs": "^1.11.13",
"dayjs-business-days2": "^1.2.2",
"dayjs-business-days2": "^1.2.3",
"dinero.js": "^1.9.1",
"dotenv": "^16.4.5",
"dotenv": "^16.4.7",
"env-cmd": "^10.1.0",
"exifr": "^7.1.3",
"firebase": "^10.13.2",
"graphql": "^16.9.0",
"graphql": "^16.10.0",
"i18next": "^23.15.1",
"i18next-browser-languagedetector": "^8.0.0",
"i18next-browser-languagedetector": "^8.0.2",
"immutability-helper": "^3.1.1",
"libphonenumber-js": "^1.11.9",
"libphonenumber-js": "^1.11.18",
"logrocket": "^8.1.2",
"markerjs2": "^2.32.2",
"markerjs2": "^2.32.3",
"memoize-one": "^6.0.0",
"normalize-url": "^8.0.1",
"object-hash": "^3.0.0",
"prop-types": "^15.8.1",
"query-string": "^9.1.0",
"query-string": "^9.1.1",
"raf-schd": "^4.0.3",
"react": "^18.3.1",
"react-big-calendar": "^1.14.1",
"react-big-calendar": "^1.17.1",
"react-color": "^2.19.3",
"react-cookie": "^7.2.0",
"react-cookie": "^7.2.2",
"react-dom": "^18.3.1",
"react-drag-listview": "^2.0.0",
"react-grid-gallery": "^1.0.1",
"react-grid-layout": "1.3.4",
"react-i18next": "^14.1.3",
"react-icons": "^5.3.0",
"react-icons": "^5.4.0",
"react-image-lightbox": "^5.1.4",
"react-markdown": "^9.0.1",
"react-number-format": "^5.4.2",
"react-markdown": "^9.0.3",
"react-number-format": "^5.4.3",
"react-popopo": "^2.1.9",
"react-product-fruits": "^2.2.61",
"react-redux": "^9.1.2",
"react-redux": "^9.2.0",
"react-resizable": "^3.0.5",
"react-router-dom": "^6.26.2",
"react-sticky": "^6.0.3",
"react-virtuoso": "^4.10.4",
"recharts": "^2.12.7",
"recharts": "^2.15.0",
"redux": "^5.0.1",
"redux-actions": "^3.0.3",
"redux-persist": "^6.0.0",
"redux-saga": "^1.3.0",
"redux-state-sync": "^3.1.4",
"reselect": "^5.1.1",
"sass": "^1.79.3",
"socket.io-client": "^4.8.0",
"styled-components": "^6.1.13",
"sass": "^1.83.4",
"socket.io-client": "^4.8.1",
"styled-components": "^6.1.14",
"subscriptions-transport-ws": "^0.11.0",
"use-memo-one": "^1.1.3",
"userpilot": "^1.3.6",
@@ -120,36 +120,36 @@
"@rollup/rollup-linux-x64-gnu": "4.6.1"
},
"devDependencies": {
"@ant-design/icons": "^5.5.1",
"@ant-design/icons": "^5.5.2",
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/preset-react": "^7.24.7",
"@dotenvx/dotenvx": "^1.14.1",
"@emotion/babel-plugin": "^11.12.0",
"@emotion/react": "^11.13.3",
"@eslint/js": "^9.15.0",
"@babel/preset-react": "^7.26.3",
"@dotenvx/dotenvx": "^1.33.0",
"@emotion/babel-plugin": "^11.13.5",
"@emotion/react": "^11.14.0",
"@eslint/js": "^9.18.0",
"@sentry/webpack-plugin": "^2.22.4",
"@testing-library/cypress": "^10.0.2",
"browserslist": "^4.23.3",
"browserslist": "^4.24.4",
"browserslist-to-esbuild": "^2.1.1",
"chalk": "^5.3.0",
"chalk": "^5.4.1",
"cross-env": "^7.0.3",
"cypress": "^13.14.2",
"cypress": "^13.17.0",
"eslint": "^8.57.1",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-cypress": "^2.15.1",
"eslint-plugin-react": "^7.37.2",
"globals": "^15.12.0",
"memfs": "^4.12.0",
"eslint-plugin-react": "^7.37.4",
"globals": "^15.14.0",
"memfs": "^4.17.0",
"os-browserify": "^0.3.0",
"react-error-overlay": "6.0.11",
"redux-logger": "^3.0.6",
"source-map-explorer": "^2.5.3",
"vite": "^5.4.7",
"vite-plugin-babel": "^1.2.0",
"vite": "^6.0.7",
"vite-plugin-babel": "^1.3.0",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-node-polyfills": "^0.22.0",
"vite-plugin-pwa": "^0.20.5",
"vite-plugin-node-polyfills": "^0.23.0",
"vite-plugin-pwa": "^0.21.1",
"vite-plugin-style-import": "^2.0.0",
"workbox-window": "^7.1.0"
"workbox-window": "^7.3.0"
}
}

View File

@@ -5,6 +5,13 @@
border-bottom: 1px solid #74695c !important;
}
// TODO: This was added because the newest release of ant was making the text color and the background color the same on a selected header
// Tried all available tokens (https://ant.design/components/menu?locale=en-US) and even reverted all our custom styles, to no avail
// This should be kept an eye on, especially if implementing DARK MODE
.ant-menu-submenu-title {
color: rgba(255, 255, 255, 0.65) !important;
}
.imex-table-header {
display: flex;
flex-wrap: wrap;

View File

@@ -1,26 +1,23 @@
import { Collapse, Form, Input, InputNumber, Select, Space, Switch } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import FormItemEmail from "../form-items-formatted/email-form-item.component";
import FormItemPhone, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component";
import JobsDetailChangeEstimator from "../jobs-detail-change-estimator/jobs-detail-change-estimator.component";
import JobsDetailChangeFilehandler from "../jobs-detail-change-filehandler/jobs-detail-change-filehandler.component";
import JobsDetailRatesChangeButton from "../jobs-detail-rates-change-button/jobs-detail-rates-change-button.component";
import JobsDetailRatesParts from "../jobs-detail-rates/jobs-detail-rates.parts.component";
import JobsDetailRatesLabor from "../jobs-detail-rates/jobs-detail-rates.labor.component";
import JobsDetailRatesMaterials from "../jobs-detail-rates/jobs-detail-rates.materials.component";
import JobsDetailRatesOther from "../jobs-detail-rates/jobs-detail-rates.other.component";
import JobsDetailRatesParts from "../jobs-detail-rates/jobs-detail-rates.parts.component";
import JobsDetailRatesTaxes from "../jobs-detail-rates/jobs-detail-rates.taxes.component";
import JobsMarkPstExempt from "../jobs-mark-pst-exempt/jobs-mark-pst-exempt.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
@@ -199,7 +196,9 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
</Collapse.Panel>
<Collapse.Panel forceRender key="financial" header={t("menus.jobsdetail.financials")}>
<JobsDetailRatesChangeButton form={form} />
<JobsMarkPstExempt form={form} />
{InstanceRenderManager({
imex: <JobsMarkPstExempt form={form} />
})}
<LayoutFormRow>
<Form.Item label={t("jobs.fields.ded_amt")} name="ded_amt">
<CurrencyInput min={0} />
@@ -246,7 +245,6 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
</LayoutFormRow>
)
})}
<LayoutFormRow>
<Form.Item label={t("jobs.fields.rate_lab")} name="rate_lab">
<CurrencyInput />

View File

@@ -32,6 +32,7 @@ import AddToProduction from "./jobs-detail-header-actions.addtoproduction.util";
import DuplicateJob from "./jobs-detail-header-actions.duplicate.util";
import JobsDetailHeaderActionsToggleProduction from "./jobs-detail-header-actions.toggle-production";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
import ShareToTeamsButton from "../share-to-teams/share-to-teams.component.jsx";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -201,7 +202,10 @@ export function JobsDetailHeaderActions({
message: t("appointments.successes.created")
});
} catch (error) {
notification.open({ type: "error", message: t("appointments.errors.saving", { error: error.message }) });
notification.open({
type: "error",
message: t("appointments.errors.saving", { error: error.message })
});
} finally {
setLoading(false);
setVisibility(false);
@@ -838,7 +842,7 @@ export function JobsDetailHeaderActions({
id: "job-actions-addtoproduction",
disabled: !job.converted,
label: t("jobs.actions.addtoproduction"),
onClick: () => AddToProduction(client, job.id, refetch, notification)
onClick: () => AddToProduction(client, job.id, refetch, false, notification)
}
);
@@ -971,6 +975,14 @@ export function JobsDetailHeaderActions({
}
);
if (bodyshop?.md_functionality_toggles?.teams) {
menuItems.push({
key: "sharetoteams",
id: "job-actions-sharetoteams",
label: <ShareToTeamsButton noIcon={true} urlOverride={`${window.location.origin}${window.location.pathname}`} />
});
}
menuItems.push({
key: "exportcustdata",
id: "job-actions-exportcustdata",

View File

@@ -13,6 +13,7 @@ import AuditTrailMapping from "../../utils/AuditTrailMappings";
import { DateTimeFormatterFunction } from "../../utils/DateFormatter";
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
import LoadingSpinner from "../loading-spinner/loading-spinner.component.jsx";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser,
@@ -39,7 +40,7 @@ export function JobsDetailHeaderActionsToggleProduction({
const [form] = Form.useForm();
const notification = useNotification();
const [getJobDetails] = useLazyQuery(GET_JOB_BY_PK_QUICK_INTAKE, {
const [getJobDetails, { loading: jobDetailsLoading }] = useLazyQuery(GET_JOB_BY_PK_QUICK_INTAKE, {
variables: { id: job.id },
onCompleted: (data) => {
if (data?.jobs_by_pk) {
@@ -109,65 +110,69 @@ export function JobsDetailHeaderActionsToggleProduction({
const popMenu = (
<div onClick={(e) => e.stopPropagation()}>
<Form layout="vertical" form={form} onFinish={handleConvert}>
{scenario === "pre" && (
<>
<Form.Item
name={["actual_in"]}
label={t("jobs.fields.actual_in")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
>
<FormDateTimePickerComponent disabled={jobRO} />
</Form.Item>
<Form.Item
name={["scheduled_completion"]}
label={t("jobs.fields.scheduled_completion")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
>
<FormDateTimePickerComponent disabled={jobRO} />
</Form.Item>
<Form.Item name={["scheduled_delivery"]} label={t("jobs.fields.scheduled_delivery")}>
<FormDateTimePickerComponent disabled={jobRO} />
</Form.Item>
</>
)}
{scenario === "prod" && (
<>
<Form.Item
name={["actual_completion"]}
label={t("jobs.fields.actual_completion")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
>
<FormDateTimePickerComponent disabled={jobRO} />
</Form.Item>
{jobDetailsLoading ? (
<LoadingSpinner />
) : (
<Form layout="vertical" form={form} onFinish={handleConvert}>
{scenario === "pre" && (
<>
<Form.Item
name={["actual_in"]}
label={t("jobs.fields.actual_in")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
>
<FormDateTimePickerComponent disabled={jobRO} />
</Form.Item>
<Form.Item
name={["scheduled_completion"]}
label={t("jobs.fields.scheduled_completion")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
>
<FormDateTimePickerComponent disabled={jobRO} />
</Form.Item>
<Form.Item name={["scheduled_delivery"]} label={t("jobs.fields.scheduled_delivery")}>
<FormDateTimePickerComponent disabled={jobRO} />
</Form.Item>
</>
)}
{scenario === "prod" && (
<>
<Form.Item
name={["actual_completion"]}
label={t("jobs.fields.actual_completion")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
>
<FormDateTimePickerComponent disabled={jobRO} />
</Form.Item>
<Form.Item name={["actual_delivery"]} label={t("jobs.fields.actual_delivery")}>
<FormDateTimePickerComponent disabled={jobRO} />
</Form.Item>
</>
)}
<Form.Item name={["actual_delivery"]} label={t("jobs.fields.actual_delivery")}>
<FormDateTimePickerComponent disabled={jobRO} />
</Form.Item>
</>
)}
<Space wrap>
<Button type="primary" onClick={() => form.submit()} loading={loading}>
{t("general.actions.save")}
</Button>
</Space>
</Form>
<Space wrap>
<Button type="primary" onClick={() => form.submit()} loading={loading}>
{t("general.actions.save")}
</Button>
</Space>
</Form>
)}
</div>
);

View File

@@ -1,14 +1,14 @@
import { useApolloClient } from "@apollo/client";
import { Button, Form, Popover, Space } from "antd";
import axios from "axios";
import React, { useMemo, useState } from "react";
import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
import { GET_DOC_SIZE_BY_JOB } from "../../graphql/documents.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import JobSearchSelect from "../job-search-select/job-search-select.component";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
@@ -134,7 +134,7 @@ export function JobsDocumentsGalleryReassign({ bodyshop, galleryImages, callback
]}
name={"jobid"}
>
<JobSearchSelect />
<JobSearchSelect notExported={false} notInvoiced={false} />
</Form.Item>
</Form>
<Space>

View File

@@ -1,5 +1,5 @@
import { Button, Form, Popover, Space } from "antd";
import React, { useState } from "react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -60,7 +60,7 @@ export function JobsDocumentsLocalGalleryReassign({ bodyshop, jobid, allMedia, g
]}
name={"jobid"}
>
<JobSearchSelect />
<JobSearchSelect notExported={false} notInvoiced={false}/>
</Form.Item>
</Form>
<Space>

View File

@@ -19,6 +19,7 @@ import LockWrapperComponent from "../lock-wrapper/lock-wrapper.component";
import PartsReceiveModalContainer from "../parts-receive-modal/parts-receive-modal.container";
import PrintWrapper from "../print-wrapper/print-wrapper.component";
import PartsOrderDrawer from "./parts-order-list-table-drawer.component";
import ShareToTeamsButton from "../share-to-teams/share-to-teams.component.jsx";
const mapStateToProps = createStructuredSelector({
jobRO: selectJobReadOnly,
@@ -66,19 +67,20 @@ export function PartsOrderListTableComponent({
const parts_orders = billsQuery.data ? billsQuery.data.parts_orders : [];
const { refetch } = billsQuery;
const recordActions = (record, showView = false) => (
<Space direction="horizontal" wrap>
<ShareToTeamsButton
linkText={""}
urlOverride={`${window.location.origin}/manage/jobs/${job.id}?partsorderid=${record.id}&tab=partssublet `}
/>
{showView && (
<Button
icon={<EyeFilled />}
onClick={() => {
handleOnRowClick(record);
}}
>
<EyeFilled />
</Button>
/>
)}
<Button
disabled={jobRO || record.return || record.vendor.id === bodyshop.inhousevendorid}
onClick={() => {
@@ -106,6 +108,7 @@ export function PartsOrderListTableComponent({
</Button>
<Button
title={t("tasks.buttons.create")}
icon={<FaTasks />}
onClick={() => {
setTaskUpsertContext({
context: {
@@ -114,9 +117,7 @@ export function PartsOrderListTableComponent({
}
});
}}
>
<FaTasks />
</Button>
/>
<Popconfirm
title={t("parts_orders.labels.confirmdelete")}
disabled={jobRO}
@@ -137,9 +138,7 @@ export function PartsOrderListTableComponent({
});
}}
>
<Button disabled={jobRO}>
<DeleteFilled />
</Button>
<Button disabled={jobRO} icon={<DeleteFilled />} />
</Popconfirm>
<Button

View File

@@ -1,9 +1,13 @@
import { Button, Input, Space, Spin } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { ExclamationCircleFilled, ExclamationCircleOutlined } from "@ant-design/icons";
import {
ExclamationCircleFilled,
ExclamationCircleOutlined,
UserDeleteOutlined,
UsergroupDeleteOutlined
} from "@ant-design/icons";
import { selectBodyshop } from "../../redux/user/user.selectors";
import EmployeeSearchSelectComponent from "../employee-search-select/employee-search-select.component";
@@ -19,12 +23,13 @@ export default connect(mapStateToProps, mapDispatchToProps)(ProductionBoardFilte
export function ProductionBoardFilters({ bodyshop, filter, setFilter, loading }) {
const { t } = useTranslation();
const [alertFilter, setAlertFilter] = useState(false);
const toggleAlertFilter = () => {
const newAlertFilter = !alertFilter;
setAlertFilter(newAlertFilter);
setFilter({ ...filter, alert: newAlertFilter });
setFilter({ ...filter, alert: !filter.alert });
};
const toggleUnassignedFilter = () => {
setFilter({ ...filter, unassigned: !filter.unassigned });
};
return (
@@ -46,12 +51,19 @@ export function ProductionBoardFilters({ bodyshop, filter, setFilter, loading })
allowClear
/>
<Button
type={alertFilter ? "primary" : "default"}
type={filter?.alert ? "primary" : "default"}
onClick={toggleAlertFilter}
icon={alertFilter ? <ExclamationCircleFilled /> : <ExclamationCircleOutlined />}
icon={filter?.alert ? <ExclamationCircleFilled /> : <ExclamationCircleOutlined />}
>
{t("production.labels.alerts")}
</Button>
<Button
type={filter?.unassigned ? "primary" : "default"}
onClick={toggleUnassignedFilter}
icon={filter?.unassigned ? <UserDeleteOutlined /> : <UsergroupDeleteOutlined />}
>
{t("production.labels.unassigned")}
</Button>
</Space>
);
}

View File

@@ -20,6 +20,8 @@ import dayjs from "../../utils/day";
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import ShareToTeamsButton from "../share-to-teams/share-to-teams.component.jsx";
import { PiMicrosoftTeamsLogo } from "react-icons/pi";
const cardColor = (ssbuckets, totalHrs) => {
const bucket = ssbuckets.find((bucket) => bucket.gte <= totalHrs && (!bucket.lt || bucket.lt > totalHrs));
@@ -417,9 +419,20 @@ export default function ProductionBoardCard({ technician, card, bodyshop, cardSe
title={!isBodyEmpty ? headerContent : null}
extra={
!isBodyEmpty && (
<Link to={{ search: `?selected=${card.id}` }}>
<EyeFilled />
</Link>
<Space>
<ShareToTeamsButton
noIcon={true}
linkText={
<div className="share-to-teams-badge">
<PiMicrosoftTeamsLogo />
</div>
}
urlOverride={`${window.location.origin}/manage/jobs/${card.id}`}
/>
<Link to={{ search: `?selected=${card.id}` }}>
<EyeFilled />
</Link>
</Space>
)
}
>

View File

@@ -10,6 +10,16 @@
.height-preserving-container {
}
.share-to-teams-badge {
background-color: #cccccc;
border-radius: 50%;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
}
.react-trello-column-header {
font-weight: bold;
cursor: pointer;

View File

@@ -29,7 +29,7 @@ const sortByParentId = (arr) => {
// Function to create board data based on statuses and jobs, with optional filtering
export const createBoardData = ({ statuses, data, filter, cardSettings }) => {
const { search, employeeId, alert } = filter;
const { search, employeeId, alert, unassigned } = filter;
const lanes = statuses.map((status) => ({
id: status,
@@ -40,6 +40,13 @@ export const createBoardData = ({ statuses, data, filter, cardSettings }) => {
let filteredJobs =
(search === "" || !search) && !employeeId ? data : data.filter((job) => checkFilter(search, employeeId, job));
// Apply "Unassigned" filter
if (unassigned) {
filteredJobs = filteredJobs.filter(
(job) => !job.employee_body && !job.employee_prep && !job.employee_refinish && !job.employee_csr
);
}
// Filter jobs by selectedMdInsCos if it has values
if (cardSettings?.selectedMdInsCos?.length > 0) {
filteredJobs = filteredJobs.filter((job) => cardSettings.selectedMdInsCos.includes(job.ins_co_nm));

View File

@@ -1,6 +1,6 @@
import { useMutation } from "@apollo/client";
import { Button, Card, Col, Form, Popover, Row, Tabs } from "antd";
import React, { useEffect, useState } from "react";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { UPDATE_KANBAN_SETTINGS } from "../../../graphql/user.queries.js";
import { defaultKanbanSettings, mergeWithDefaults } from "./defaultKanbanSettings.js";
@@ -11,6 +11,7 @@ import FilterSettings from "./FilterSettings.jsx";
import PropTypes from "prop-types";
import { isFunction } from "lodash";
import { useNotification } from "../../../contexts/Notifications/notificationContext.jsx";
import { SettingOutlined } from "@ant-design/icons";
function ProductionBoardKanbanSettings({ associationSettings, parentLoading, bodyshop, data, onSettingsChange }) {
const [form] = Form.useForm();
@@ -153,7 +154,7 @@ function ProductionBoardKanbanSettings({ associationSettings, parentLoading, bod
return (
<Popover content={overlay} open={open} placement="topRight">
<Button loading={loading} onClick={() => setOpen(!open)}>
<Button icon={<SettingOutlined />} loading={loading} onClick={() => setOpen(!open)}>
{t("production.settings.board_settings")}
</Button>
</Popover>

View File

@@ -27,6 +27,7 @@ import ProductionListColumnNote from "./production-list-columns.productionnote.c
import ProductionListColumnCategory from "./production-list-columns.status.category";
import ProductionListColumnStatus from "./production-list-columns.status.component";
import ProductionListColumnTouchTime from "./prodution-list-columns.touchtime.component";
import ShareToTeamsButton from "../share-to-teams/share-to-teams.component.jsx";
const getEmployeeName = (employeeId, employees) => {
const employee = employees.find((e) => e.id === employeeId);
@@ -41,7 +42,17 @@ const r = ({ technician, state, activeStatuses, data, bodyshop, refetch, treatme
dataIndex: "viewdetail",
key: "viewdetail",
ellipsis: true,
render: (text, record) => <Link to={{ search: `?selected=${record.id}` }}>{i18n.t("general.labels.view")}</Link>
render: (text, record) => (
<Space>
<Link to={{ search: `?selected=${record.id}` }}>{i18n.t("general.labels.view")}</Link>
<ShareToTeamsButton
noIcon={true}
linkText={"Share"}
noIconStyle={{ color: "#1890ff" }}
urlOverride={`${window.location.origin}/manage/jobs/${record.id}`}
/>
</Space>
)
},
...(Enhanced_Payroll.treatment === "on"
? [

View File

@@ -1,5 +1,5 @@
import { Button, Dropdown } from "antd";
import React, { useState } from "react";
import { useState } from "react";
import { TemplateList } from "../../utils/TemplateConstants";
import { useTranslation } from "react-i18next";
import { GenerateDocument } from "../../utils/RenderTemplate";
@@ -7,6 +7,7 @@ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
import { PrinterFilled } from "@ant-design/icons";
const ProdTemplates = TemplateList("production");
const { production_by_technician_one, production_by_category_one, production_by_repair_status_one } =
@@ -123,7 +124,9 @@ export function ProductionListPrint({ bodyshop }) {
return (
<Dropdown trigger="click" menu={menu}>
<Button loading={loading}>{t("general.labels.print")}</Button>
<Button icon={<PrinterFilled />} loading={loading}>
{t("general.labels.print")}
</Button>
</Dropdown>
);
}

View File

@@ -0,0 +1,111 @@
import PropTypes from "prop-types";
import { Button } from "antd";
import { useLocation } from "react-router-dom";
import { PiMicrosoftTeamsLogo } from "react-icons/pi";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors.js";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
/**
* ShareToTeamsButton component for sharing content to Microsoft Teams via an HTTP link.
*
* This component creates a button or link that opens the Microsoft Teams share dialog with
* the provided URL, title, and message text through query parameters. The popup window is centered on the screen.
*
* @param {Object} props - The component's props.
* @param {string} [props.messageTextOverride] - Custom message text for sharing.
* @param {string} [props.urlOverride] - Custom URL to share instead of the current page's URL.
* @param {string} [props.pageTitleOverride] - Custom title for the shared page.
* @param {boolean} [props.noIcon=false] - If true, renders as a simple text link instead of a button with an icon.
* @param {Object} [props.noIconStyle={}] - Style object for the text link when noIcon is true.
* @param {Object} [props.buttonStyle={}] - Style object for the Ant Design button.
* @param {Object} [props.buttonIconStyle={}] - Style object for the icon within the button.
* @param {string} [props.linkText] - Text to display on the button or link.
* @returns {React.ReactElement} A button or text link for sharing to Microsoft Teams.
*/
const ShareToTeamsComponent = ({
bodyshop,
messageTextOverride,
urlOverride,
pageTitleOverride,
noIcon = false,
noIconStyle = {},
buttonStyle = {},
buttonIconStyle = {},
linkText
}) => {
const location = useLocation();
const { t } = useTranslation();
const currentUrl =
urlOverride ||
encodeURIComponent(`${window.location.origin}${location.pathname}${location.search}${location.hash}`);
const pageTitle =
pageTitleOverride ||
encodeURIComponent(typeof document !== "undefined" ? document.title : t("general.actions.sharetoteams"));
const messageText = messageTextOverride || encodeURIComponent(t("general.actions.sharetoteams"));
// Construct the Teams share URL with parameters
const teamsShareUrl = `https://teams.microsoft.com/share?href=${currentUrl}&preText=${messageText}&title=${pageTitle}`;
// Function to open the centered share link in a new window/tab
const handleShare = () => {
const screenWidth = window.screen.width;
const screenHeight = window.screen.height;
const windowWidth = 600;
const windowHeight = 400;
const left = screenWidth / 2 - windowWidth / 2;
const top = screenHeight / 2 - windowHeight / 2;
const windowFeatures = `width=${windowWidth},height=${windowHeight},left=${left},top=${top}`;
// noinspection JSIgnoredPromiseFromCall
window.open(teamsShareUrl, "_blank", windowFeatures);
};
// Feature is disabled
if (!bodyshop?.md_functionality_toggles?.teams) {
return null;
}
if (noIcon) {
return (
<div style={{ cursor: "pointer", ...noIconStyle }} onClick={handleShare}>
{!linkText ? t("general.actions.sharetoteams") : linkText}
</div>
);
}
return (
<Button
style={{
backgroundColor: "#6264A7",
borderColor: "#6264A7",
color: "#FFFFFF",
...buttonStyle
}}
icon={<PiMicrosoftTeamsLogo style={{ color: "#FFFFFF", ...buttonIconStyle }} />}
onClick={handleShare}
title={linkText === null ? t("general.actions.sharetoteams") : linkText}
/>
);
};
ShareToTeamsComponent.propTypes = {
messageTextOverride: PropTypes.string,
urlOverride: PropTypes.string,
pageTitleOverride: PropTypes.string,
noIcon: PropTypes.bool,
noIconStyle: PropTypes.object,
buttonStyle: PropTypes.object,
buttonIconStyle: PropTypes.object,
linkText: PropTypes.oneOfType([PropTypes.string, PropTypes.node])
};
export default connect(mapStateToProps)(ShareToTeamsComponent);

View File

@@ -14,7 +14,7 @@ import FormItemEmail from "../form-items-formatted/email-form-item.component";
import PhoneFormItem, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component";
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
// TODO: Client Update, this might break
const timeZonesList = Intl.supportedValuesOf("timeZone");
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
@@ -642,6 +642,15 @@ export function ShopInfoGeneral({ form, bodyshop }) {
<Input />
</Form.Item>
</LayoutFormRow>
<LayoutFormRow header={t("bodyshop.labels.shop_enabled_features")} id="sharing">
<Form.Item
label={t("general.actions.sharetoteams")}
valuePropName="checked"
name={["md_functionality_toggles", "teams"]}
>
<Switch />
</Form.Item>
</LayoutFormRow>
<LayoutFormRow grow header={t("bodyshop.labels.messagingpresets")} id="messagingpresets">
<Form.List name={["md_messaging_presets"]}>
{(fields, { add, remove, move }) => {

View File

@@ -18,6 +18,7 @@ import { setModalContext } from "../../redux/modals/modals.actions";
import { pageLimit } from "../../utils/config";
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter.jsx";
import dayjs from "../../utils/day";
import ShareToTeamsButton from "../share-to-teams/share-to-teams.component.jsx";
/**
* Task List Component
@@ -266,8 +267,13 @@ function TaskListComponent({
width: "8%",
render: (text, record) => (
<Space direction="horizontal">
<ShareToTeamsButton
linkText=""
urlOverride={`${window.location.origin}/manage/tasks/alltasks?taskid=${record.id}`}
/>
<Button
title={t("tasks.buttons.edit")}
icon={<EditFilled />}
onClick={() => {
setTaskUpsertContext({
context: {
@@ -276,18 +282,18 @@ function TaskListComponent({
}
});
}}
>
<EditFilled />
</Button>
/>
<Button
title={t("tasks.buttons.complete")}
onClick={() => toggleCompletedStatus(record.id, record.completed)}
>
{record.completed ? <CheckCircleOutlined /> : <CheckCircleFilled />}
</Button>
<Button title={t("tasks.buttons.delete")} onClick={() => toggleDeletedStatus(record.id, record.deleted)}>
{record.deleted ? <DeleteOutlined /> : <DeleteFilled />}
</Button>
icon={record.completed ? <CheckCircleOutlined /> : <CheckCircleFilled />}
/>
<Button
title={t("tasks.buttons.delete")}
onClick={() => toggleDeletedStatus(record.id, record.deleted)}
icon={record.deleted ? <DeleteOutlined /> : <DeleteFilled />}
/>
</Space>
)
}

View File

@@ -1,6 +1,6 @@
import { useMutation, useQuery } from "@apollo/client";
import { Form, Modal } from "antd";
import React, { useEffect, useMemo, useState } from "react";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -35,7 +35,7 @@ export function TaskUpsertModalContainer({ bodyshop, currentUser, taskUpsert, to
const [insertTask] = useMutation(MUTATION_INSERT_NEW_TASK);
const [updateTask] = useMutation(MUTATION_UPDATE_TASK);
const { open, context } = taskUpsert;
const { jobid, joblineid, billid, partsorderid, taskId, existingTask, query } = context;
const { jobid, joblineid, billid, partsorderid, taskId, existingTask, query, view } = context;
const [form] = Form.useForm();
const [selectedJobId, setSelectedJobId] = useState(null);
const [selectedJobDetails, setSelectedJobDetails] = useState(null);
@@ -255,16 +255,17 @@ export function TaskUpsertModalContainer({ bodyshop, currentUser, taskUpsert, to
}
};
const taskTitle = useMemo(() => {
return existingTask ? t("tasks.actions.edit") : t("tasks.actions.new");
}, [existingTask, t]);
return (
<Modal
title={<span id="task-upsert-modal-title">{taskTitle}</span>}
title={
<span id="task-upsert-modal-title">
{view ? t("tasks.actions.view") : existingTask ? t("tasks.actions.edit") : t("tasks.actions.new")}
</span>
}
open={open}
okText={t("general.actions.save")}
width="50%"
cancelText={!isTouched ? t("general.actions.ok") : t("general.actions.cancel")}
onOk={() => {
removeTaskIdFromUrl();
form.submit();
@@ -289,6 +290,7 @@ export function TaskUpsertModalContainer({ bodyshop, currentUser, taskUpsert, to
loading={loading || (taskId && taskLoading)}
error={error}
data={data}
view={view}
existingTask={existingTask || taskData?.tasks_by_pk}
selectedJobId={selectedJobId}
setSelectedJobId={setSelectedJobId}

View File

@@ -1,4 +1,5 @@
import { DeleteFilled } from "@ant-design/icons";
import { DeleteFilled, PrinterFilled } from "@ant-design/icons";
import { PageHeader } from "@ant-design/pro-layout";
import { useApolloClient, useMutation } from "@apollo/client";
import {
Alert,
@@ -16,32 +17,31 @@ import {
Switch,
Typography
} from "antd";
import { PageHeader } from "@ant-design/pro-layout";
import React, { useState } from "react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
// import { useNavigate } from 'react-router-dom';
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import Dinero from "dinero.js";
import dayjs from "../../utils/day";
import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import DateTimePicker from "../../components/form-date-time-picker/form-date-time-picker.component";
import FormsFieldChanged from "../../components/form-fields-changed-alert/form-fields-changed-alert.component";
import CurrencyInput from "../../components/form-items-formatted/currency-form-item.component";
import JobCloseRoGuardContainer from "../../components/job-close-ro-guard/job-close-ro-guard.container";
import JobsScoreboardAdd from "../../components/job-scoreboard-add-button/job-scoreboard-add-button.component";
import JobsCloseAutoAllocate from "../../components/jobs-close-auto-allocate/jobs-close-auto-allocate.component";
import JobsCloseLines from "../../components/jobs-close-lines/jobs-close-lines.component";
import LayoutFormRow from "../../components/layout-form-row/layout-form-row.component";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
import { generateJobLinesUpdatesForInvoicing } from "../../graphql/jobs-lines.queries";
import { UPDATE_JOB } from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { setModalContext } from "../../redux/modals/modals.actions.js";
import { selectBodyshop } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import JobCloseRoGuardContainer from "../../components/job-close-ro-guard/job-close-ro-guard.container";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
import dayjs from "../../utils/day";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -49,10 +49,17 @@ const mapStateToProps = createStructuredSelector({
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type })),
setPrintCenterContext: (context) =>
dispatch(
setModalContext({
context: context,
modal: "printCenter"
})
)
});
export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail }) {
export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail, setPrintCenterContext }) {
const { t } = useTranslation();
const [form] = Form.useForm();
const client = useApolloClient();
@@ -171,7 +178,6 @@ export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail }) {
extra={
<Space>
<JobsCloseAutoAllocate joblines={job.joblines} form={form} disabled={!!job.date_exported || jobRO} />
<Popconfirm
onConfirm={() => form.submit()}
disabled={jobRO}
@@ -188,6 +194,21 @@ export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail }) {
<Button disabled={job.date_exported || !jobRO}>{t("jobs.actions.sendtodms")}</Button>
</Link>
)}
<Button
onClick={() => {
setPrintCenterContext({
context: {
id: job.id,
job: job,
type: "job"
}
});
}}
key="printing"
icon={<PrinterFilled />}
>
{t("jobs.actions.printCenter")}
</Button>
<JobsScoreboardAdd job={job} disabled={false} />
</Space>
}

View File

@@ -46,7 +46,8 @@ export function AllTasksPageContainer({ setBreadcrumbs, setSelectedHeader, setTa
if (taskId) {
setTaskUpsertContext({
context: {
taskId
taskId,
view: true
}
});
urlParams.delete("taskid");

View File

@@ -235,7 +235,12 @@ export function* signInSuccessSaga({ payload }) {
try {
window.$crisp.push(["set", "user:nickname", [payload.displayName || payload.email]]);
window.$crisp.push(["set", "session:segments", [["user"]]]);
const currentUserSegment = InstanceRenderManager({
imex: "imex-online-user",
rome: "rome-online-user"
});
window.$crisp.push(["set", "session:segments", [[currentUserSegment]]]);
InstanceRenderManager({
executeFunction: true,
args: [],

View File

@@ -712,7 +712,8 @@
"ssbuckets": "Job Size Definitions",
"systemsettings": "System Settings",
"task-presets": "Task Presets",
"workingdays": "Working Days"
"workingdays": "Working Days",
"shop_enabled_features": "Shop Enabled Features"
},
"operations": {
"contains": "Contains",
@@ -1193,7 +1194,9 @@
"submit": "Submit",
"tryagain": "Try Again",
"view": "View",
"viewreleasenotes": "See What's Changed"
"viewreleasenotes": "See What's Changed",
"sharetoteams": "Share to Teams",
"ok": "Ok"
},
"errors": {
"fcm": "You must allow notification permissions to have real time messaging. Click to try again.",
@@ -1525,7 +1528,7 @@
"addDocuments": "Add Job Documents",
"addNote": "Add Note",
"addtopartsqueue": "Add to Parts Queue",
"addtoproduction": "Add In Production Flag",
"addtoproduction": "Add to Production",
"addtoscoreboard": "Add to Scoreboard",
"allocate": "Allocate",
"autoallocate": "Auto Allocate",
@@ -1568,7 +1571,7 @@
"printCenter": "Print Center",
"recalculate": "Recalculate",
"reconcile": "Reconcile",
"removefromproduction": "Remove In Production Flag",
"removefromproduction": "Remove from Production",
"schedule": "Schedule",
"sendcsi": "Send CSI",
"sendpartspricechange": "Send Parts Price Change",
@@ -2857,7 +2860,8 @@
"touchtime": "T/T",
"vertical": "Vertical",
"viewname": "View Name",
"wide": "Wide"
"wide": "Wide",
"unassigned": "Unassigned"
},
"options": {
"horizontal": "Horizontal",
@@ -3183,7 +3187,8 @@
"tasks": {
"actions": {
"edit": "Edit Task",
"new": "New Task"
"new": "New Task",
"view": "View Task"
},
"buttons": {
"allTasks": "All",

View File

@@ -712,7 +712,8 @@
"ssbuckets": "",
"systemsettings": "",
"task-presets": "",
"workingdays": ""
"workingdays": "",
"shop_enabled_features": ""
},
"operations": {
"contains": "",
@@ -1193,7 +1194,9 @@
"submit": "",
"tryagain": "",
"view": "",
"viewreleasenotes": ""
"viewreleasenotes": "",
"sharetoteams": "",
"ok": ""
},
"errors": {
"fcm": "",
@@ -2857,7 +2860,8 @@
"touchtime": "",
"vertical": "",
"viewname": "",
"wide": ""
"wide": "",
"unassigned": ""
},
"options": {
"horizontal": "",
@@ -3183,7 +3187,8 @@
"tasks": {
"actions": {
"edit": "",
"new": ""
"new": "",
"view": ""
},
"buttons": {
"allTasks": "",

View File

@@ -712,7 +712,8 @@
"ssbuckets": "",
"systemsettings": "",
"task-presets": "",
"workingdays": ""
"workingdays": "",
"shop_enabled_features": ""
},
"operations": {
"contains": "",
@@ -1193,7 +1194,9 @@
"submit": "",
"tryagain": "",
"view": "",
"viewreleasenotes": ""
"viewreleasenotes": "",
"sharetoteams": "",
"ok": ""
},
"errors": {
"fcm": "",
@@ -2857,7 +2860,8 @@
"touchtime": "",
"vertical": "",
"viewname": "",
"wide": ""
"wide": "",
"unassigned": ""
},
"options": {
"horizontal": "",
@@ -3183,7 +3187,8 @@
"tasks": {
"actions": {
"edit": "",
"new": ""
"new": "",
"view": ""
},
"buttons": {
"allTasks": "",

View File

@@ -9,7 +9,6 @@ import client from "../utils/GraphQLClient";
import cleanAxios from "./CleanAxios";
import { TemplateList } from "./TemplateConstants";
import { generateTemplate } from "./graphQLmodifier";
import InstanceRenderManager from "./instanceRenderMgr";
const server = import.meta.env.VITE_APP_REPORTS_SERVER_URL;
@@ -39,7 +38,7 @@ export default async function RenderTemplate(
jsreport.headers["Authorization"] = jsrAuth;
//Query assets that match the template name. Must be in format <<templateName>>.query
let { contextData, useShopSpecificTemplate } = await fetchContextData(templateObject, jsrAuth);
let { contextData, useShopSpecificTemplate, shopSpecificFolder } = await fetchContextData(templateObject, jsrAuth);
const { ignoreCustomMargins } = Templates[templateObject.name];
@@ -74,14 +73,8 @@ export default async function RenderTemplate(
...contextData,
...templateObject.variables,
...templateObject.context,
headerpath: `/${InstanceRenderManager({
imex: bodyshop.imexshopid,
rome: bodyshop.imexshopid
})}/header.html`,
footerpath: `/${InstanceRenderManager({
imex: bodyshop.imexshopid,
rome: bodyshop.imexshopid
})}/footer.html`,
headerpath: shopSpecificFolder ? `/${bodyshop.imexshopid}/header.html` : `/GENERIC/header.html`,
footerpath: shopSpecificFolder ? `/${bodyshop.imexshopid}/footer.html` : `/GENERIC/footer.html`,
bodyshop: bodyshop,
filters: templateObject?.filters,
sorters: templateObject?.sorters,
@@ -149,11 +142,12 @@ export async function RenderTemplates(templateObjects, bodyshop, renderAsHtml =
templateObjects.forEach((template) => {
proms.push(
(async () => {
let { contextData, useShopSpecificTemplate } = await fetchContextData(template, jsrAuth);
let { contextData, useShopSpecificTemplate, shopSpecificFolder } = await fetchContextData(template, jsrAuth);
unsortedTemplatesAndData.push({
templateObject: template,
contextData,
useShopSpecificTemplate
useShopSpecificTemplate,
shopSpecificFolder
});
})()
);
@@ -248,8 +242,8 @@ export async function RenderTemplates(templateObjects, bodyshop, renderAsHtml =
// ...rootTemplate.templateObject.variables,
// ...rootTemplate.templateObject.context,
headerpath: `/${bodyshop.imexshopid}/header.html`,
footerpath: `/${bodyshop.imexshopid}/footer.html`,
headerpath: rootTemplate.shopSpecificFolder ? `/${bodyshop.imexshopid}/header.html` : `/GENERIC/header.html`,
footerpath: rootTemplate.shopSpecificFolder ? `/${bodyshop.imexshopid}/footer.html` : `/GENERIC/footer.html`,
bodyshop: bodyshop,
offset: bodyshop.timezone
}
@@ -397,10 +391,10 @@ const fetchContextData = async (templateObject, jsrAuth) => {
});
contextData = data;
}
return { contextData, useShopSpecificTemplate };
return { contextData, useShopSpecificTemplate, shopSpecificFolder };
}
return await generateTemplate(templateQueryToExecute, templateObject, useShopSpecificTemplate);
return await generateTemplate(templateQueryToExecute, templateObject, useShopSpecificTemplate, shopSpecificFolder);
};
//export const displayTemplateInWindow = (html) => {

View File

@@ -1,6 +1,6 @@
import { gql } from "@apollo/client";
import { Kind, parse, print, visit } from "graphql";
import client from "./GraphQLClient";
import { gql } from "@apollo/client";
/* eslint-disable no-loop-func */
@@ -114,9 +114,10 @@ export function printQuery(query) {
* @param templateQueryToExecute
* @param templateObject
* @param useShopSpecificTemplate
* @returns {Promise<{contextData: {}, useShopSpecificTemplate}>}
* @param shopSpecificTemplate
* @returns {Promise<{contextData: {}, useShopSpecificTemplate, shopSpecificTemplate}>}
*/
export async function generateTemplate(templateQueryToExecute, templateObject, useShopSpecificTemplate) {
export async function generateTemplate(templateQueryToExecute, templateObject, useShopSpecificTemplate, shopSpecificFolder) {
// Advanced Filtering and Sorting modifications start here
// Parse the query and apply the filters and sorters
@@ -147,7 +148,7 @@ export async function generateTemplate(templateQueryToExecute, templateObject, u
contextData = data;
}
return { contextData, useShopSpecificTemplate };
return { contextData, useShopSpecificTemplate, shopSpecificFolder };
}
/**

View File

@@ -6,8 +6,8 @@ import eslint from "vite-plugin-eslint";
import { VitePWA } from "vite-plugin-pwa";
import InstanceRenderManager from "./src/utils/instanceRenderMgr";
import chalk from "chalk";
//import { visualizer } from "rollup-plugin-visualizer";
// Ensure your environment variables are set correctly for Vite 6
process.env.VITE_APP_GIT_SHA_DATE = new Date().toLocaleString("en-US", {
timeZone: "America/Los_Angeles"
});
@@ -22,7 +22,7 @@ export const logger = createLogger("info", {
export default defineConfig({
base: "/",
plugins: [
//visualizer(),
// Ensure all plugins are Vite 6 compatible
ViteEjsPlugin((viteConfig) => ({ env: viteConfig.env })),
VitePWA({
injectRegister: "auto",
@@ -31,14 +31,12 @@ export default defineConfig({
short_name: InstanceRenderManager({
instance: process.env.VITE_APP_INSTANCE,
imex: "ImEX Online",
rome: "Rome Online",
rome: "Rome Online"
}),
name: InstanceRenderManager({
instance: process.env.VITE_APP_INSTANCE,
imex: "ImEX Online",
rome: "Rome Online",
rome: "Rome Online"
}),
description: "The ultimate bodyshop management system.",
icons: [
@@ -46,7 +44,7 @@ export default defineConfig({
src: InstanceRenderManager({
instance: process.env.VITE_APP_INSTANCE,
imex: "favicon.png",
rome: "ro-favicon.png",
rome: "ro-favicon.png"
}),
sizes: "64x64 32x32 24x24 16x16",
type: "image/x-icon"
@@ -55,7 +53,7 @@ export default defineConfig({
src: InstanceRenderManager({
instance: process.env.VITE_APP_INSTANCE,
imex: "logo192.png",
rome: "logo192.png",
rome: "logo192.png"
}),
type: "image/png",
sizes: "192x192"
@@ -64,7 +62,7 @@ export default defineConfig({
src: InstanceRenderManager({
instance: process.env.VITE_APP_INSTANCE,
imex: "logo512.png",
rome: "ro-favicon.png",
rome: "ro-favicon.png"
}),
type: "image/png",
sizes: "512x512"
@@ -73,7 +71,7 @@ export default defineConfig({
theme_color: InstanceRenderManager({
instance: process.env.VITE_APP_INSTANCE,
imex: "#1890ff",
rome: "#fff",
rome: "#fff"
}),
background_color: "#fff",
gcm_sender_id: "103953800507"
@@ -204,8 +202,10 @@ export default defineConfig({
"react-redux"
],
esbuildOptions: {
// Update for Vite 6: Use proper file extensions
loader: {
".js": "jsx"
".jsx": "jsx",
".tsx": "tsx"
}
}
},

View File

@@ -39,3 +39,11 @@
headers:
- name: event-secret
value_from_env: EVENT_SECRET
- name: Rome Usage Report
webhook: '{{HASURA_API_URL}}/data/usagereport'
schedule: 0 12 * * 5
include_in_metadata: true
payload: {}
headers:
- name: x-imex-auth
value_from_env: DATAPUMP_AUTH

3052
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -19,12 +19,12 @@
"makeitpretty": "prettier --write \"**/*.{css,js,json,jsx,scss}\""
},
"dependencies": {
"@aws-sdk/client-cloudwatch-logs": "^3.693.0",
"@aws-sdk/client-elasticache": "^3.693.0",
"@aws-sdk/client-s3": "^3.693.0",
"@aws-sdk/client-secrets-manager": "^3.693.0",
"@aws-sdk/client-ses": "^3.693.0",
"@aws-sdk/credential-provider-node": "^3.693.0",
"@aws-sdk/client-cloudwatch-logs": "^3.738.0",
"@aws-sdk/client-elasticache": "^3.738.0",
"@aws-sdk/client-s3": "^3.738.0",
"@aws-sdk/client-secrets-manager": "^3.738.0",
"@aws-sdk/client-ses": "^3.738.0",
"@aws-sdk/credential-provider-node": "^3.738.0",
"@opensearch-project/opensearch": "^2.13.0",
"@socket.io/admin-ui": "^0.5.1",
"@socket.io/redis-adapter": "^8.3.0",
@@ -33,7 +33,6 @@
"better-queue": "^3.8.12",
"bluebird": "^3.7.2",
"body-parser": "^1.20.3",
"canvas": "^2.11.2",
"chart.js": "^4.4.6",
"cloudinary": "^2.5.1",
"compression": "^1.7.5",
@@ -41,31 +40,31 @@
"cors": "2.8.5",
"crisp-status-reporter": "^1.2.2",
"csrf": "^3.1.0",
"dd-trace": "^5.28.0",
"dd-trace": "^5.33.1",
"dinero.js": "^1.9.1",
"dotenv": "^16.4.5",
"express": "^4.21.1",
"firebase-admin": "^13.0.0",
"graphql": "^16.9.0",
"firebase-admin": "^13.0.2",
"graphql": "^16.10.0",
"graphql-request": "^6.1.0",
"inline-css": "^4.0.2",
"inline-css": "^4.0.3",
"intuit-oauth": "^4.1.3",
"ioredis": "^5.4.1",
"json-2-csv": "^5.5.6",
"ioredis": "^5.4.2",
"json-2-csv": "^5.5.8",
"juice": "^11.0.0",
"lodash": "^4.17.21",
"moment": "^2.30.1",
"moment-timezone": "^0.5.46",
"moment-timezone": "^0.5.47",
"multer": "^1.4.5-lts.1",
"node-mailjet": "^6.0.6",
"node-persist": "^4.0.3",
"nodemailer": "^6.9.16",
"phone": "^3.1.53",
"node-persist": "^4.0.4",
"nodemailer": "^6.10.0",
"phone": "^3.1.58",
"recursive-diff": "^1.0.9",
"redis": "^4.7.0",
"rimraf": "^6.0.1",
"skia-canvas": "^2.0.0",
"soap": "^1.1.6",
"skia-canvas": "^2.0.2",
"soap": "^1.1.7",
"socket.io": "^4.8.1",
"socket.io-adapter": "^2.5.5",
"ssh2-sftp-client": "^11.0.0",
@@ -77,12 +76,12 @@
"xmlbuilder2": "^3.1.1"
},
"devDependencies": {
"@eslint/js": "^9.15.0",
"@eslint/js": "^9.19.0",
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"concurrently": "^8.2.2",
"eslint": "^9.15.0",
"eslint-plugin-react": "^7.37.2",
"globals": "^15.12.0",
"eslint": "^9.19.0",
"eslint-plugin-react": "^7.37.4",
"globals": "^15.14.0",
"p-limit": "^3.1.0",
"prettier": "^3.3.3",
"source-map-explorer": "^2.5.2"

View File

@@ -1,4 +1,3 @@
const { createCanvas } = require("canvas");
const { Canvas, FontLibrary } = require("skia-canvas");
const Chart = require("chart.js/auto");
@@ -65,7 +64,7 @@ const getChartConfiguration = (keys, values, override) => {
return defaultsDeep(override || {}, defaultConfiguration);
};
const processCanvasRequest = async (req, res, isSkia = false) => {
const processCanvasRequest = async (req, res) => {
const { logger } = req;
const { w, h, values, keys, override } = req.body;
@@ -77,7 +76,6 @@ const processCanvasRequest = async (req, res, isSkia = false) => {
const configuration = getChartConfiguration(keys, values, override);
// Placeholders to allow fine control over GAC
let canvas = null;
let ctx = null;
let chart = null;
@@ -85,16 +83,15 @@ const processCanvasRequest = async (req, res, isSkia = false) => {
try {
// Create the canvas
canvas = isSkia ? new Canvas(width, height) : createCanvas(width, height);
canvas = new Canvas(width, height);
ctx = canvas.getContext("2d");
// Render the chart
chart = new Chart(ctx, configuration);
// Generate and send the image
chartImage = isSkia ? (await canvas.toBuffer("image/png")).toString("base64") : canvas.toDataURL();
res.status(200).send(isSkia ? `data:image/png;base64,${chartImage}` : chartImage);
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 });
@@ -104,27 +101,27 @@ const processCanvasRequest = async (req, res, isSkia = false) => {
if (chart) {
chart.destroy();
}
ctx = null; // Explicitly nullify for garbage collection
canvas = null; // Explicitly nullify for garbage collection
ctx = null;
canvas = null;
chartImage = null;
}
};
const enqueueRequest = (req, res, isSkia) => {
const enqueueRequest = (req, res) => {
if (requestQueue.length >= CANVAS_QUEUE_LIMIT) {
res.status(503).send("Server is busy. Please try again later.");
return false;
}
requestQueue.push({ req, res, isSkia });
requestQueue.push({ req, res });
req.logger.log("inbound-canvas-creation-queue", "debug", "jsr", null, { queue: requestQueue.length });
return true;
};
const processNextInQueue = async () => {
while (requestQueue.length > 0) {
const { req, res, isSkia } = requestQueue.shift();
const { req, res } = requestQueue.shift();
try {
await processCanvasRequest(req, res, isSkia);
await processCanvasRequest(req, res);
} catch (err) {
console.error("canvas-queue-error", "error", "jsr", null, { error: err.message });
}
@@ -137,13 +134,7 @@ exports.canvastest = function (req, res) {
};
exports.canvas = async (req, res) => {
if (isProcessing || !enqueueRequest(req, res, false)) return;
isProcessing = true;
processNextInQueue().catch((err) => console.error("canvas-processing-error", { error: err.message }));
};
exports.canvasSkia = async (req, res) => {
if (isProcessing || !enqueueRequest(req, res, true)) return;
if (isProcessing || !enqueueRequest(req, res)) return;
isProcessing = true;
processNextInQueue().catch((err) => console.error("canvas-processing-error", { error: err.message }));
};

View File

@@ -2,12 +2,12 @@ const express = require("express");
const router = express.Router();
const { inlinecss } = require("../render/inlinecss");
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
const { canvas, canvasSkia } = require("../render/canvas-handler");
const { canvas } = require("../render/canvas-handler");
const validateCanvasInputMiddleware = require("../middleware/validateCanvasInputMiddleware");
// Define the route for inline CSS rendering
router.post("/inlinecss", validateFirebaseIdTokenMiddleware, inlinecss);
router.post("/canvas", [validateFirebaseIdTokenMiddleware, validateCanvasInputMiddleware], canvas);
router.post("/canvas-skia", [validateFirebaseIdTokenMiddleware, validateCanvasInputMiddleware], canvasSkia);
router.post("/canvas-skia", validateFirebaseIdTokenMiddleware, validateCanvasInputMiddleware, canvas);
router.post("/canvas", validateFirebaseIdTokenMiddleware, validateCanvasInputMiddleware, canvas);
module.exports = router;