Compare commits

...

107 Commits

Author SHA1 Message Date
Dave
34fe0cc3bf 6feature/IO-3556-Chattr-Integration - Move to BULLMQ stack 2026-02-12 12:56:17 -05:00
Dave
188a7b47b1 feature/IO-3556-Chattr-Integration - Switch Consent to true 2026-02-11 11:59:16 -05:00
Dave
d08bfc61cd feature/IO-3556-Chattr-Integration - Retry beef up / tweeks 2026-02-11 11:37:47 -05:00
Dave
503c217c99 fix 2026-02-11 09:52:22 -05:00
Dave
b444639fca Merge branch 'hotfix/2026-02-10-backend' into feature/IO-3556-Chattr-Integration 2026-02-10 18:03:37 -05:00
Dave
6ee7e56b9b hotfix/2026-02-10-backend - Move chatter DB stuff over 2026-02-10 17:58:26 -05:00
Dave
0340ca5fcc feature/IO-3556-Chattr-Integration - Add in Redis caching for Chatter 2026-02-10 17:25:59 -05:00
Dave
1b2fc8b114 feature/IO-3556-Chattr-Integration 2026-02-10 17:17:44 -05:00
Dave
3745d7a414 feature/IO-3556-Chattr-Integration 2026-02-10 12:48:48 -05:00
Dave Richer
56559dd3ff Merged in hotfix/2026-02-03 (pull request #2965)
Hotfix/2026 02 03

Approved-by: Allan Carr
2026-02-03 21:49:28 +00:00
Dave
fde137d7f7 Merge branch 'feature/IO-3550-Labor-Adjustment-Popover' into hotfix/2026-02-03 2026-02-03 16:00:28 -05:00
Dave
b797bf7dc9 Merge branch 'feature/IO-3548-Bill-Modal-TabOrder' into hotfix/2026-02-03 2026-02-03 16:00:08 -05:00
Dave
b87d1a65fe feature/IO-3550-Labor-Adjustment-Popover - Fix 2026-02-03 15:57:24 -05:00
Dave
019b3cf4da feature/IO-3548-Bill-Modal-TabOrder 2026-02-03 15:50:48 -05:00
Dave
ad520ab23e feature/IO-3548-Bill-Modal-TabOrder 2026-02-03 15:42:10 -05:00
Dave
05ae0801e5 feature/IO-3545-Production-Board-List-DND - EMP assignment selector fix 2026-02-03 15:29:03 -05:00
Dave
332ade96e5 feature/IO-3545-Production-Board-List-DND - Checkpoint 2026-02-03 15:17:20 -05:00
Dave
3acec55c0e feature/IO-3545-Production-Board-List-DND - Checkpoint 2026-02-03 15:01:10 -05:00
Dave
da0462f14c feature/IO-3545-Production-Board-List-DND - Checkpoint 2026-02-03 14:56:04 -05:00
Dave
2cc9fa961e feature/IO-3545-Production-Board-List-DND - Checkpoint 2026-02-03 14:34:42 -05:00
Dave
1b6fe4d18e feature/IO-3545-Production-Board-List-DND - Checkpoint 2026-02-03 13:26:17 -05:00
Dave
22aae0a7f1 feature/IO-3545-Production-Board-List-DND - Checkpoint 2026-02-03 13:21:32 -05:00
Patrick Fic
db1b701a96 Merged in hotfix/2026-02-02 (pull request #2951)
Hotfix/2026 02 02

Approved-by: Dave Richer
2026-02-02 22:49:14 +00:00
Dave
2746421c09 hotfix/2026-02-02 - 2026-02-02 17:48:03 -05:00
Dave
5217120994 hotfix/2026-02-02 - Parts order manual discounting box 2026-02-02 17:39:47 -05:00
Dave
77f72a2a12 Merge branch 'hotfix/2026-02-02' of bitbucket.org:snaptsoft/bodyshop into hotfix/2026-02-02 2026-02-02 17:11:06 -05:00
Dave
a84ad4ee32 hotfix/2026-02-02 - remove check on missing line ids 2026-02-02 17:10:56 -05:00
Dave Richer
2cacd75822 Merged in bugfix/IO-3533 (pull request #2948)
bugfix/IO-3533 - Fix
2026-02-02 22:05:56 +00:00
Dave
217a0b84ac bugfix/IO-3533 - Fix 2026-02-02 17:04:06 -05:00
Dave Richer
f53ed8c427 Merged in feature/IO-3532-parts-queue-ui-adjustments (pull request #2944)
feature/IO-3532-parts-queue-ui-adjustments

Approved-by: Patrick Fic
2026-02-02 21:51:31 +00:00
Dave Richer
f8b7588a04 Merged in feature/IO-3542-fix-searches (pull request #2945)
feature/IO-3542-fix-searches

Approved-by: Patrick Fic
2026-02-02 21:46:24 +00:00
Patrick Fic
ee3cb4456d Merged in feature/IO-3531-apollo-rerender (pull request #2946)
IO-3531 remove loading on parts order page.
2026-02-02 21:45:59 +00:00
Patrick Fic
ae05692c46 IO-3531 remove loading on parts order page. 2026-02-02 13:45:25 -08:00
Dave
e01a2af5a4 feature/IO-3542-fix-searches 2026-02-02 16:44:49 -05:00
Dave
9c0cb5f80b Merge branch 'feature/IO-3532-parts-queue-ui-adjustments' of bitbucket.org:snaptsoft/bodyshop into feature/IO-3532-parts-queue-ui-adjustments 2026-02-02 16:15:23 -05:00
Dave
1f726aca4d feature/IO-3532-parts-queue-ui-adjustments 2026-02-02 16:14:44 -05:00
Patrick Fic
b9f398cf2d Merged in feature/IO-3532-parts-queue-ui-adjustments (pull request #2943)
IO-3532 resolve tooltip on owner name.
2026-02-02 20:57:05 +00:00
Patrick Fic
ff73a14610 IO-3532 resolve tooltip on owner name. 2026-02-02 12:55:29 -08:00
Patrick Fic
1e44d4fe42 Merged in feature/IO-3539-print-center-popovers (pull request #2941)
IO-3539 resolve print center popoves.
2026-02-02 20:38:49 +00:00
Patrick Fic
0f42875d1b IO-3539 resolve print center popoves. 2026-02-02 12:38:29 -08:00
Patrick Fic
a0f1299006 Merged in feature/IO-3538-receivec-cm-on-parts-order (pull request #2940)
IO-3538 Resolve missing id on receive return.
2026-02-02 20:23:20 +00:00
Patrick Fic
87d8a5d746 IO-3538 Resolve missing id on receive return. 2026-02-02 12:22:58 -08:00
Patrick Fic
268851902a Merged in feature/IO-3535-fed-tax-toggle-bill-posting (pull request #2935)
IO-3535 Resolve federal tax default off on received parts order.
2026-02-02 20:07:26 +00:00
Dave Richer
68bb7d2529 Merged in bugfix/IO-3533 (pull request #2937)
bugfix/IO-3533 - Disable on blurr and on focus handlers in bill entry modal

Approved-by: Patrick Fic
2026-02-02 20:06:13 +00:00
Patrick Fic
d50db12330 Merged in feature/IO-3534-bill-discrep-coloring (pull request #2936)
IO-3534 Adjust value prop to content for antd prop change to fix color display.
2026-02-02 20:05:54 +00:00
Patrick Fic
1438986c18 Merged in feature/IO-3532-parts-queue-ui-adjustments (pull request #2938)
IO-3532 Resolve parts queue pages.
2026-02-02 20:05:15 +00:00
Patrick Fic
c047699fbb Merged in feature/IO-3531-apollo-rerender (pull request #2939)
IO-3531 Change global apollo config setting to prevent rerenders.
2026-02-02 20:03:29 +00:00
Patrick Fic
e5b7fcb919 IO-3531 Change global apollo config setting to prevent rerenders. 2026-02-02 12:02:11 -08:00
Patrick Fic
cadcfc9b0d IO-3532 Resolve parts queue pages. 2026-02-02 11:21:22 -08:00
Dave
55023ceaca feature/IO-3534-bill-discrep-coloring: Remove unused console.log 2026-02-02 12:47:04 -05:00
Dave
45e143578c bugfix/IO-3533 - Disable on blurr and on focus handlers in bill entry modal 2026-02-02 12:34:54 -05:00
Patrick Fic
28a41f7637 IO-3534 Adjust value prop to content for antd prop change to fix color display. 2026-02-02 09:33:37 -08:00
Patrick Fic
2a2edeadb9 IO-3535 Resolve federal tax default off on received parts order. 2026-02-02 09:25:58 -08:00
Allan Carr
849d967b56 Merged in hotfix/2026-01-30 (pull request #2931)
IO-3529 Fix for Parts Return
2026-02-01 01:49:08 +00:00
Allan Carr
519d7e8d87 Merged in feature/IO-3529-DMS-Make-Code (pull request #2932)
IO-3529 CM Recieved Fix
2026-02-01 01:41:46 +00:00
Allan Carr
b08435607e IO-3529 CM Recieved Fix
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2026-01-31 17:43:16 -08:00
Allan Carr
ea9e4ffcad IO-3529 Fix for Parts Return
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2026-01-31 16:59:17 -08:00
Allan Carr
6c814c7dc6 Merged in feature/IO-3529-DMS-Make-Code (pull request #2929)
IO-3529 Fix for Parts Return
2026-02-01 00:57:43 +00:00
Allan Carr
cc9e536059 Merged in hotfix/2026-01-30 (pull request #2928)
IO-3529 Job Lines on Closing add IDs
2026-01-31 19:37:46 +00:00
Allan Carr
dadc9892d0 IO-3529 Job Lines on Closing add IDs
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2026-01-31 11:32:05 -08:00
Allan Carr
b05e20ce0d Merged in feature/IO-3529-DMS-Make-Code (pull request #2926)
IO-3529 Job Lines on Closing add IDs
2026-01-31 19:30:29 +00:00
Allan Carr
eb36b12cb0 Merged in hotfix/2026-01-30 (pull request #2925)
IO-3529 DMS Make Code
2026-01-31 06:46:32 +00:00
Allan Carr
bf5a099fa6 IO-3529 DMS Make Code
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2026-01-30 22:39:56 -08:00
Allan Carr
ff3d24c623 Merged in feature/IO-3529-DMS-Make-Code (pull request #2923)
IO-3529 DMS Make Code
2026-01-31 06:38:26 +00:00
Dave Richer
27b955a701 Merged in release/2026-01-23 (pull request #2918)
Release/2026 01 23 into master-AIO - IO-3497, IO-3499, IO-3503, IO-3509, IO-3512, IO-3514, IO-3523
2026-01-31 03:23:30 +00:00
Allan Carr
1896c4db59 Merged in feature/IO-3503-Job-Costing-Fixes (pull request #2921)
IO-3503 Job Costing Corrections

Approved-by: Dave Richer
2026-01-31 01:10:20 +00:00
Allan Carr
78770ed54e IO-3503 Job Costing Corrections
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2026-01-30 16:45:25 -08:00
Dave Richer
9e2ae2cc10 Merged in feature/IO-3499-React-19 (pull request #2919)
feature/IO-3499-React-19 -Checkpoint
2026-01-30 22:32:54 +00:00
Dave
3a0f6101c8 feature/IO-3499-React-19 -Checkpoint 2026-01-30 17:32:12 -05:00
Dave Richer
f0dfa2717f Merged in feature/IO-3499-React-19 (pull request #2916)
feature/IO-3499-React-19 -Checkpoint
2026-01-30 17:33:51 +00:00
Dave
1f3be72d9d feature/IO-3499-React-19 -Checkpoint 2026-01-30 12:33:20 -05:00
Allan Carr
3d9ad799f3 Merged in hotfix/2026-01-29 (pull request #2915)
IO-3522 Replace Email with Phone1
2026-01-29 21:31:13 +00:00
Allan Carr
6e17ef10bb Merged in feature/IO-3522-Fortellis-Bug-Fix (pull request #2914)
IO-3522 Replace Email with Phone1
2026-01-29 21:28:48 +00:00
Allan Carr
fdc06e79a6 IO-3522 Replace Email with Phone1
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2026-01-29 13:29:01 -08:00
Allan Carr
66924367fc Merged in feature/IO-3522-Fortellis-Bug-Fix (pull request #2913)
IO-3522 Replace Email with Phone1
2026-01-29 21:28:11 +00:00
Allan Carr
f76165552e Merged in hotfix/2026-01-29 (pull request #2912)
IO-3522 Fortellis Null Coalesce for Owner data
2026-01-29 21:06:39 +00:00
Allan Carr
80fbb847d8 Merged in feature/IO-3522-Fortellis-Bug-Fix (pull request #2911)
IO-3522 Fortellis Null Coalesce for Owner data
2026-01-29 21:02:40 +00:00
Allan Carr
ca1703e724 Merged in feature/IO-3522-Fortellis-Bug-Fix (pull request #2910)
Feature/IO-3522 Fortellis Bug Fix

Approved-by: Patrick Fic
2026-01-29 21:00:45 +00:00
Allan Carr
163819809c IO-3522 Fortellis Null Coalesce for Owner data
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2026-01-29 12:57:14 -08:00
Dave Richer
42fa85e145 Merged in feature/IO-3499-React-19 (pull request #2908)
feature/IO-3499-React-19 -Checkpoint
2026-01-29 17:37:01 +00:00
Dave
13104f36e3 feature/IO-3499-React-19 -Checkpoint 2026-01-29 12:36:34 -05:00
Dave Richer
0c9f7df9ac Merged in feature/IO-3499-React-19 (pull request #2907)
feature/IO-3499-React-19 -Checkpoint
2026-01-29 17:33:26 +00:00
Dave
a9280a83ba feature/IO-3499-React-19 -Checkpoint 2026-01-29 12:31:04 -05:00
Dave Richer
78d816fa8b Merged in feature/IO-3499-React-19 (pull request #2905)
feature/IO-3499-React-19 -Checkpoint
2026-01-28 19:02:42 +00:00
Dave
9f573fc5b4 feature/IO-3499-React-19 -Checkpoint 2026-01-28 14:02:06 -05:00
Dave Richer
4a1b1fe905 Merged in feature/IO-3499-React-19 (pull request #2902)
Feature/IO-3499 React 19
2026-01-28 02:31:43 +00:00
Dave
5f81ec2099 feature/IO-3499-React-19 -Checkpoint 2026-01-27 21:31:03 -05:00
Dave
147977be58 feature/IO-3499-React-19 -Checkpoint 2026-01-27 20:57:16 -05:00
Dave
4dfda4b371 feature/IO-3499-React-19 -Checkpoint 2026-01-27 20:40:04 -05:00
Dave
02feba2804 feature/IO-3499-React-19 -Checkpoint 2026-01-27 19:54:40 -05:00
Dave Richer
a9fb77189e Merged in feature/IO-3499-React-19 (pull request #2900)
Feature/IO-3499 React 19
2026-01-28 00:27:51 +00:00
Dave
3bacad69e3 feature/IO-3499-React-19 -Checkpoint 2026-01-27 19:26:42 -05:00
Patrick Fic
70b6aa63ed Merged in feature/IO-3517-fortellis-hotfix (pull request #2899)
IO-3517 Resolve emit on fortellis completion.
2026-01-27 19:32:02 +00:00
Patrick Fic
844a879f1c IO-3517 Resolve emit on fortellis completion. 2026-01-27 11:31:10 -08:00
Dave
6415b302dc Merge remote-tracking branch 'origin/release/2026-01-23' into feature/IO-3499-React-19 2026-01-27 10:29:54 -05:00
Allan Carr
d40dd649e2 Merged in feature/IO-3512-Page-Titles (pull request #2896)
IO-3512 Great Search Fix

Approved-by: Dave Richer
2026-01-27 15:28:01 +00:00
Allan Carr
35366eda22 IO-3512 Great Search Fix
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2026-01-27 07:27:43 -08:00
Dave
9a53896aa4 feature/IO-3499-React-19 -Add pointer icons to clickable things in the production list view 2026-01-26 15:04:03 -05:00
Dave Richer
278765d019 Merged in feature/IO-3499-React-19 (pull request #2894)
feature/IO-3499-React-19 -Checkpoint
2026-01-26 19:06:47 +00:00
Dave
6fd5fc8f66 Merge remote-tracking branch 'origin/release/2026-01-23' into feature/IO-3499-React-19 2026-01-26 14:04:10 -05:00
Dave
346a6e69c7 feature/IO-3499-React-19 -Checkpoint 2026-01-26 14:02:31 -05:00
Allan Carr
5d5fa8fead Merged in feature/IO-3514-Tech-Console-Fixes (pull request #2890)
IO-3514 Print Center Restrict Financial Group on Tech Station and Fix Drawer Close on Tech Console

Approved-by: Dave Richer
2026-01-26 16:32:33 +00:00
Allan Carr
30dae4e365 Merged in feature/IO-3509-Duplicate-Job-Open-Estimate-Date (pull request #2891)
IO-3509 Duplicate Job Open Estimate Date

Approved-by: Dave Richer
2026-01-26 14:56:24 +00:00
Allan Carr
f6899f744b Merged in feature/IO-3512-Page-Titles (pull request #2889)
IO-3512 Page Title

Approved-by: Dave Richer
2026-01-26 14:54:58 +00:00
Allan Carr
cca23a5b11 IO-3509 Correction for date_estimate to handle tz correctly
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2026-01-23 23:07:11 -08:00
Allan Carr
2acddcb9ac IO-3509 Duplicate Job Open Estimate Date
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2026-01-23 22:51:53 -08:00
Allan Carr
ff57592c12 IO-3512 Page Title
Fix Search UI to be consistent across product

Signed-off-by: Allan Carr <allan@imexsystems.ca>
2026-01-23 20:02:19 -08:00
119 changed files with 5583 additions and 1860 deletions

View File

@@ -13,4 +13,5 @@
.env.development.local .env.development.local
.env.test.local .env.test.local
.env.production.local .env.production.local
bodyshop_translations.babel .env.localstack.docker
bodyshop_translations.babel

View File

@@ -0,0 +1,593 @@
# React 19 & Ant Design 6 Upgrade - Deprecation Fixes Report
## Overview
This document outlines all deprecations fixed during the upgrade from React 18 to React 19 and Ant Design 5 to Ant Design 6 in the branch `feature/IO-3499-React-19` compared to `origin/master-AIO`.
---
## 1. Core Dependency Updates
### React & React DOM
- **Upgraded from:** React ^18.3.1 → React ^19.2.4
- **Upgraded from:** React DOM ^18.3.1 → React DOM ^19.2.4
- **Impact:** Enabled React 19 compiler optimizations and new concurrent features
### Ant Design
- **Upgraded from:** Ant Design ^5.28.1 → ^6.2.2
- **Upgraded from:** @ant-design/icons ^5.6.1 → ^6.1.0
- **Impact:** Access to Ant Design 6 improvements and API changes
### Apollo GraphQL
- **@apollo/client:** ^3.13.9 → ^4.1.3
- **apollo-link-logger:** ^2.0.1 → ^3.0.0
- **graphql-ws:** ^6.0.7 (added for WebSocket subscriptions)
- **Impact:** Major version upgrade with breaking changes to import paths and API
### React Ecosystem Libraries
- **react-router-dom:** ^6.30.0 → ^7.13.0
- **react-i18next:** ^15.7.3 → ^16.5.4
- **react-grid-layout:** 1.3.4 → ^2.2.2
- **@testing-library/react:** ^16.3.1 → ^16.3.2
- **styled-components:** ^6.2.0 → ^6.3.8
### Build Tools
- **Vite:** ^7.3.1 (maintained, peer dependencies updated)
- **vite-plugin-babel:** ^1.3.2 → ^1.4.1
- **vite-plugin-node-polyfills:** ^0.24.0 → ^0.25.0
- **vitest:** ^3.2.4 → ^4.0.18
### Monitoring & Analytics
- **@sentry/react:** ^9.43.0 → ^10.38.0
- **@sentry/cli:** ^2.58.2 → ^3.1.0
- **@sentry/vite-plugin:** ^4.6.1 → ^4.8.0
- **logrocket:** ^9.0.2 → ^12.0.0
- **posthog-js:** ^1.315.1 → ^1.336.4
- **@amplitude/analytics-browser:** ^2.33.1 → ^2.34.0
### Other Key Dependencies
- **axios:** ^1.13.2 → ^1.13.4
- **env-cmd:** ^10.1.0 → ^11.0.0
- **i18next:** ^25.7.4 → ^25.8.0
- **libphonenumber-js:** ^1.12.33 → ^1.12.36
- **lightningcss:** ^1.30.2 → ^1.31.1
- **@fingerprintjs/fingerprintjs:** ^4.6.1 → ^5.0.1
- **@firebase/app:** ^0.14.6 → ^0.14.7
- **@firebase/firestore:** ^4.9.3 → ^4.10.0
### Infrastructure
- **Node.js:** 22.x → 24.x (Dockerfile updated)
---
## 2. React 19 Compiler Optimizations
### Manual Memoization Removed
React 19's new compiler automatically optimizes components, making manual memoization unnecessary and potentially counterproductive.
#### 2.1 `useMemo` Hook Removals
**Example - Job Watchers:**
```javascript
// BEFORE
const jobWatchers = useMemo(() => (watcherData?.job_watchers ? [...watcherData.job_watchers] : []), [watcherData]);
// AFTER
// Do NOT clone arrays; keep referential stability for React Compiler and to reduce rerenders.
const jobWatchers = watcherData?.job_watchers ?? EMPTY_ARRAY;
```
**Benefits:**
- Eliminates unnecessary array cloning
- Maintains referential stability for React Compiler
- Reduces re-renders
- Cleaner, more readable code
**Files Affected:**
- Multiple kanban components
- Production board components
- Job management components
#### 2.2 `useCallback` Hook Removals
**Example - Card Lookup Function:**
```javascript
// BEFORE
const getCardByID = useCallback((data, cardId) => {
for (const lane of data.lanes) {
for (const card of lane.cards) {
// ... logic
}
}
}, [/* dependencies */]);
// AFTER
const getCardByID = (data, cardId) => {
for (const lane of data.lanes) {
for (const card of lane.cards) {
// ... logic
}
}
};
```
**Benefits:**
- React 19 compiler automatically optimizes function references
- Reduced complexity in component code
- No need to manage dependency arrays
**Files Affected:**
- production-board-kanban.component.jsx
- production-board-kanban.container.jsx
- Multiple board controller components
#### 2.3 `React.memo()` Wrapper Removals
**Example - EllipsesToolTip Component:**
```javascript
// BEFORE
const EllipsesToolTip = memo(({ title, children, kiosk }) => {
if (kiosk || !title) {
return <div className="ellipses no-select">{children}</div>;
}
return (
<Tooltip title={title}>
<div className="ellipses no-select">{children}</div>
</Tooltip>
);
});
EllipsesToolTip.displayName = "EllipsesToolTip";
// AFTER
function EllipsesToolTip({ title, children, kiosk }) {
if (kiosk || !title) {
return <div className="ellipses no-select">{children}</div>;
}
return (
<Tooltip title={title}>
<div className="ellipses no-select">{children}</div>
</Tooltip>
);
}
```
**Benefits:**
- Compiler handles optimization automatically
- No need for manual displayName assignment
- Standard function syntax is cleaner
**Files Affected:**
- production-board-kanban-card.component.jsx
- EllipsesToolTip components
- Various utility components
---
## 3. State Management Optimizations
### Deep Cloning Elimination
React 19's compiler efficiently handles change detection, eliminating the need for manual deep cloning.
**Example - Board Lanes State Update:**
```javascript
// BEFORE
setBoardLanes((prevBoardLanes) => {
const deepClonedData = cloneDeep(newBoardData);
if (!isEqual(prevBoardLanes, deepClonedData)) {
return deepClonedData;
}
return prevBoardLanes;
});
// AFTER
setBoardLanes(newBoardData);
```
**Benefits:**
- Removed lodash dependencies (`cloneDeep`, `isEqual`) from components
- Reduced memory overhead
- Faster state updates
- React 19's compiler handles change detection efficiently
---
## 4. Import Cleanup
### React Import Simplifications
**Example - Removed Unnecessary Hook Imports:**
```javascript
// BEFORE
import { useMemo, useState, useEffect, useCallback } from "react";
// AFTER
import { useState, useEffect } from "react";
```
Multiple files had their React imports streamlined by removing `useMemo`, `useCallback`, and `memo` imports that are no longer needed.
---
## 5. Apollo Client 4.x Migration
### Import Path Changes
Apollo Client 4.x requires React-specific imports to come from `@apollo/client/react` instead of the main package.
**Example - Hook Imports:**
```javascript
// BEFORE (Apollo Client 3.x)
import { useQuery, useMutation, useLazyQuery } from "@apollo/client";
import { ApolloProvider } from "@apollo/client";
import { useApolloClient } from "@apollo/client";
// AFTER (Apollo Client 4.x)
import { useQuery, useMutation, useLazyQuery } from "@apollo/client/react";
import { ApolloProvider } from "@apollo/client/react";
import { useApolloClient } from "@apollo/client/react";
```
**Benefits:**
- Better tree-shaking for non-React Apollo Client usage
- Clearer separation between core and React-specific functionality
- Reduced bundle size for React-only applications
**Files Affected:**
- All components using Apollo hooks (50+ files)
- Main app provider component
- GraphQL container components
### `useLazyQuery` API Changes
The return value destructuring pattern for `useLazyQuery` changed in Apollo Client 4.x.
**Example - Query Function Extraction:**
```javascript
// BEFORE (Apollo Client 3.x)
const [, { data, refetch, queryLoading }] = useLazyQuery(QUERY_RO_AND_OWNER_BY_JOB_PKS, {
variables: { jobids: [context.jobid] },
skip: !context?.jobid
});
// AFTER (Apollo Client 4.x)
const [loadRoAndOwnerByJobPks, { data, loading: queryLoading, error: queryError, refetch, called }] = useLazyQuery(
QUERY_RO_AND_OWNER_BY_JOB_PKS
);
// Call the query function explicitly when needed
useEffect(() => {
if (context?.jobid) {
loadRoAndOwnerByJobPks({ variables: { jobids: [context.jobid] } });
}
}, [context?.jobid, loadRoAndOwnerByJobPks]);
```
**Key Changes:**
- **Query function must be destructured**: Previously ignored with `,` now must be named
- **Options moved to function call**: `variables` and other options passed when calling the query function
- **`loading` renamed**: More consistent with `useQuery` hook naming
- **`called` property added**: Track if the query has been executed at least once
- **No more `skip` option**: Logic moved to conditional query execution
**Benefits:**
- More explicit control over when queries execute
- Better alignment with `useQuery` API patterns
- Clearer code showing query execution timing
**Files Affected:**
- card-payment-modal.component.jsx
- bill-form.container.jsx
- Multiple job and payment components
---
## 6. forwardRef Pattern Migration
React 19 simplifies ref handling by allowing `ref` to be passed as a regular prop, eliminating the need for `forwardRef` in most cases.
### forwardRef Wrapper Removal
**Example - Component Signature Change:**
```javascript
// BEFORE
import { forwardRef } from "react";
const BillLineSearchSelect = ({ options, disabled, allowRemoved, ...restProps }, ref) => {
const { t } = useTranslation();
return (
<Select
ref={ref}
options={generateOptions(options, allowRemoved, t)}
disabled={disabled}
{...restProps}
/>
);
};
export default forwardRef(BillLineSearchSelect);
// AFTER
const BillLineSearchSelect = ({ options, disabled, allowRemoved, ref, ...restProps }) => {
const { t } = useTranslation();
return (
<Select
ref={ref}
options={generateOptions(options, allowRemoved, t)}
disabled={disabled}
{...restProps}
/>
);
};
export default BillLineSearchSelect;
```
**Key Changes:**
- **`ref` as regular prop**: Moved from second parameter to first parameter as a regular prop
- **No `forwardRef` import needed**: Removed from React imports
- **No `forwardRef` wrapper**: Export component directly
- **Same ref behavior**: Works identically from parent component perspective
**Benefits:**
- Simpler component API (single parameter instead of two)
- Reduced boilerplate code
- Better TypeScript inference
- More intuitive for developers
**Components Migrated:**
- BillLineSearchSelect
- ContractStatusComponent
- CourtesyCarFuelComponent
- CourtesyCarReadinessComponent
- CourtesyCarStatusComponent
- EmployeeTeamSearchSelect
- FormInputNumberCalculator
- FormItemCurrency
- FormItemEmail
- 10+ additional form components
---
## 7. React.lazy Import Cleanup
React 19 makes `React.lazy` usage more seamless, and in some cases lazy imports were removed where they were no longer beneficial.
**Example - Lazy Import Removal:**
```javascript
// BEFORE
import { lazy, Suspense, useEffect, useRef, useState } from "react";
const LazyComponent = lazy(() => import('./HeavyComponent'));
// AFTER
import { Suspense, useEffect, useRef, useState } from "react";
// Lazy loading handled differently or component loaded directly
```
**Context:**
- Some components had lazy imports removed where the loading behavior wasn't needed
- `Suspense` boundaries maintained for actual lazy-loaded components
- React 19 improves Suspense integration
**Files Affected:**
- Multiple route components
- Dashboard components
- Heavy data visualization components
---
## 8. StrictMode Integration
React 19's StrictMode was explicitly added to help catch potential issues during development.
**Addition:**
```javascript
import { StrictMode } from "react";
root.render(
<StrictMode>
<App />
</StrictMode>
);
```
**Benefits:**
- Detects unexpected side effects
- Warns about deprecated APIs
- Validates React 19 best practices
- Double-invokes effects in development to catch issues
**Impact:**
- Helps ensure components work correctly with React 19 compiler
- Catches potential issues with state management
- Comment added: "This handles React StrictMode double-mounting"
---
## 9. React 19 New Hooks (Added Documentation)
The upgrade includes documentation for React 19's new concurrent hooks:
### `useFormStatus`
Track form submission state for better UX during async operations.
### `useOptimistic`
Implement optimistic UI updates that rollback on failure.
### `useActionState`
Manage server actions with pending states and error handling.
---
## 10. ESLint Configuration Updates
### React Compiler Plugin Added
**Addition to eslint.config.js:**
```javascript
plugins: {
"react-compiler": pluginReactCompiler
},
rules: {
"react-compiler/react-compiler": "error"
}
```
**Purpose:**
- Enforces React 19 compiler best practices
- Warns about patterns that prevent compiler optimizations
- Ensures code is compatible with automatic optimizations
---
## 11. Testing Library Updates
### @testing-library/react
- **Upgraded:** ^16.3.1 → ^16.3.2
- **Impact:** React 19 compatibility maintained
- Tests continue to work with updated React APIs
---
## 12. Peer Dependencies Updates
Multiple packages updated their peer dependency requirements to support React 19:
**Examples:**
```json
// BEFORE
"peerDependencies": {
"react": ">=16.9.0",
"react-dom": ">=16.9.0"
}
// AFTER
"peerDependencies": {
"react": ">=18.0.0",
"react-dom": ">=18.0.0"
}
```
**Affected Packages:**
- Multiple internal and external dependencies
- Ensures ecosystem compatibility with React 19
---
## 13. Ant Design 6 Changes
### Icon Package Update
- @ant-design/icons upgraded from ^5.6.1 to ^6.1.0
- Icon imports remain compatible (no breaking changes in usage patterns)
### Component API Compatibility
- Existing Ant Design component usage remains largely compatible
- Form.Item, Button, Modal, Table, and other components work with existing code
- No major API breaking changes required in application code
---
## 14. Validation & Quality Assurance
Based on the optimization summary included in the changes:
### Deprecations Verified as Fixed ✓
- **propTypes:** None found (already removed or using TypeScript)
- **defaultProps:** None found (using default parameters instead)
- **ReactDOM.render:** Already using createRoot
- **componentWillMount/Receive/Update:** No legacy lifecycle methods found
- **String refs:** Migrated to ref objects and useRef hooks
### Performance Improvements
- Cleaner, more readable code
- Reduced bundle size (removed unnecessary memoization wrappers)
- Better performance through compiler-optimized memoization
- Fewer function closures and re-creations
- Reduced memory overhead from eliminated deep cloning
---
## Summary Statistics
### Dependencies Updated
- **Core:** 3 major updates (React, Ant Design, Apollo Client)
- **GraphQL:** 2 packages (Apollo Client 3→4, apollo-link-logger 2→3)
- **Ecosystem:** 10+ related libraries (router, i18next, grid layout, etc.)
- **Build Tools:** 3 plugins/tools (Vite plugins, vitest)
- **Monitoring:** 6 packages (Sentry, LogRocket, PostHog, Amplitude)
- **Infrastructure:** Node.js 22 → 24
### Code Patterns Modernized
- **useMemo removals:** 15+ instances across multiple files
- **useCallback removals:** 10+ instances
- **memo() wrapper removals:** 5+ components
- **Deep clone eliminations:** Multiple state management simplifications
- **Import cleanups:** Dozens of simplified import statements
- **Apollo import migrations:** 50+ files updated to `/react` imports
- **forwardRef removals:** 15+ components migrated to direct ref props
- **useLazyQuery updates:** Multiple query patterns updated for Apollo 4.x API
- **lazy import cleanups:** Several unnecessary lazy imports removed
- **StrictMode integration:** Added to development builds
### Files Impacted
- **Production board kanban components:** Compiler optimization removals
- **Trello-board controllers and components:** Memoization removals
- **Job management components:** State management simplifications
- **All GraphQL components:** Apollo Client 4.x import migrations (50+ files)
- **Form components:** forwardRef pattern migrations (15+ components)
- **Payment components:** useLazyQuery API updates
- **Various utility components:** Import cleanups
- **Build configuration files:** ESLint React compiler plugin
- **Docker infrastructure:** Node.js 22→24 upgrade
- **App root:** StrictMode integration
- **Package manifests:** 30+ dependency upgrades
---
## Recommendations for Future Development
1. **Avoid Manual Memoization:** Let React 19 compiler handle optimization automatically
2. **Use ESLint React Compiler Plugin:** Catch patterns that prevent optimizations
3. **Maintain Referential Stability:** Use constant empty arrays/objects instead of creating new ones
4. **Leverage New React 19 Hooks:** Use `useOptimistic`, `useFormStatus`, and `useActionState` for better UX
5. **Monitor Compiler Warnings:** Address any compiler optimization warnings during development
6. **Apollo Client 4.x Imports:** Always import React hooks from `@apollo/client/react`
7. **Ref as Props:** Use `ref` as a regular prop instead of `forwardRef` wrapper
8. **useLazyQuery Pattern:** Extract query function and call explicitly rather than using `skip` option
9. **StrictMode Aware:** Ensure components handle double-mounting in development properly
10. **Keep Dependencies Updated:** Monitor for peer dependency compatibility as ecosystem evolves
---
## Conclusion
This comprehensive upgrade successfully modernizes the codebase across multiple dimensions:
### Major Achievements
1. **React 19 Migration:** Leveraged new compiler optimizations by removing manual memoization
2. **Apollo Client 4.x:** Updated all GraphQL operations to new import patterns and APIs
3. **Ant Design 6:** Maintained UI consistency while gaining access to latest features
4. **forwardRef Elimination:** Simplified 15+ components by using refs as regular props
5. **Dependency Modernization:** Updated 30+ packages including monitoring, build tools, and ecosystem libraries
6. **Infrastructure Upgrade:** Node.js 24.x support for latest runtime features
### Code Quality Improvements
- **Cleaner code:** Removed unnecessary wrappers and boilerplate
- **Better performance:** Compiler-optimized rendering without manual hints
- **Reduced bundle size:** Removed lodash cloning, unnecessary lazy imports, and redundant memoization
- **Improved maintainability:** Simpler patterns that are easier to understand and modify
- **Enhanced DX:** ESLint integration catches optimization blockers during development
### Migration Completeness
✅ All React 18→19 deprecations addressed
✅ All Apollo Client 3→4 breaking changes handled
✅ All Ant Design 5→6 updates applied
✅ All monitoring libraries updated to latest versions
✅ StrictMode integration for development safety
✅ Comprehensive testing library compatibility maintained
**No breaking changes to application functionality** - The upgrade maintains backward compatibility in behavior while providing forward-looking improvements in implementation.

File diff suppressed because it is too large Load Diff

View File

@@ -18,4 +18,3 @@ VITE_PUBLIC_POSTHOG_KEY=phc_xtLmBIu0rjWwExY73Oj5DTH1bGbwq1G1Y8jnlTceien
VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
VITE_APP_AMP_URL=https://vp8k908qy2.execute-api.ca-central-1.amazonaws.com VITE_APP_AMP_URL=https://vp8k908qy2.execute-api.ca-central-1.amazonaws.com
VITE_APP_AMP_KEY=6228a598e57cd66875cfd41604f1f891 VITE_APP_AMP_KEY=6228a598e57cd66875cfd41604f1f891
VITE_ENABLE_COMPILER_IN_DEV=1

View File

@@ -20,4 +20,3 @@ VITE_PUBLIC_POSTHOG_KEY=phc_xtLmBIu0rjWwExY73Oj5DTH1bGbwq1G1Y8jnlTceien
VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
VITE_APP_AMP_URL=https://vp8k908qy2.execute-api.ca-central-1.amazonaws.com VITE_APP_AMP_URL=https://vp8k908qy2.execute-api.ca-central-1.amazonaws.com
VITE_APP_AMP_KEY=46b1193a867d4e3131ae4c3a64a3fc78 VITE_APP_AMP_KEY=46b1193a867d4e3131ae4c3a64a3fc78
VITE_ENABLE_COMPILER_IN_DEV=1

3
client/.gitignore vendored
View File

@@ -13,3 +13,6 @@ playwright/.cache/
# Sentry Config File # Sentry Config File
.sentryclirc .sentryclirc
/dev-dist /dev-dist
# Local environment overrides (not version controlled)
.env.development.local.overrides

1021
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,9 +8,13 @@
"private": true, "private": true,
"proxy": "http://localhost:4000", "proxy": "http://localhost:4000",
"dependencies": { "dependencies": {
"@amplitude/analytics-browser": "^2.33.4", "@amplitude/analytics-browser": "^2.34.0",
"@ant-design/pro-layout": "^7.22.6", "@ant-design/pro-layout": "^7.22.6",
"@apollo/client": "^4.1.1", "@apollo/client": "^4.1.3",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@emotion/is-prop-valid": "^1.4.0", "@emotion/is-prop-valid": "^1.4.0",
"@fingerprintjs/fingerprintjs": "^5.0.1", "@fingerprintjs/fingerprintjs": "^5.0.1",
"@firebase/analytics": "^0.10.19", "@firebase/analytics": "^0.10.19",
@@ -21,14 +25,14 @@
"@jsreport/browser-client": "^3.1.0", "@jsreport/browser-client": "^3.1.0",
"@reduxjs/toolkit": "^2.11.2", "@reduxjs/toolkit": "^2.11.2",
"@sentry/cli": "^3.1.0", "@sentry/cli": "^3.1.0",
"@sentry/react": "^10.35.0", "@sentry/react": "^10.38.0",
"@sentry/vite-plugin": "^4.7.0", "@sentry/vite-plugin": "^4.8.0",
"@splitsoftware/splitio-react": "^2.6.1", "@splitsoftware/splitio-react": "^2.6.1",
"@tanem/react-nprogress": "^5.0.56", "@tanem/react-nprogress": "^5.0.58",
"antd": "^6.2.1", "antd": "^6.2.2",
"apollo-link-logger": "^3.0.0", "apollo-link-logger": "^3.0.0",
"autosize": "^6.0.1", "autosize": "^6.0.1",
"axios": "^1.13.2", "axios": "^1.13.4",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"css-box-model": "^1.2.1", "css-box-model": "^1.2.1",
"dayjs": "^1.11.19", "dayjs": "^1.11.19",
@@ -38,31 +42,30 @@
"env-cmd": "^11.0.0", "env-cmd": "^11.0.0",
"exifr": "^7.1.3", "exifr": "^7.1.3",
"graphql": "^16.12.0", "graphql": "^16.12.0",
"graphql-ws": "^6.0.6", "graphql-ws": "^6.0.7",
"i18next": "^25.8.0", "i18next": "^25.8.0",
"i18next-browser-languagedetector": "^8.2.0", "i18next-browser-languagedetector": "^8.2.0",
"immutability-helper": "^3.1.1", "immutability-helper": "^3.1.1",
"libphonenumber-js": "^1.12.34", "libphonenumber-js": "^1.12.36",
"lightningcss": "^1.31.0", "lightningcss": "^1.31.1",
"logrocket": "^11.0.0", "logrocket": "^12.0.0",
"markerjs2": "^2.32.7", "markerjs2": "^2.32.7",
"memoize-one": "^6.0.0", "memoize-one": "^6.0.0",
"normalize-url": "^8.1.1", "normalize-url": "^8.1.1",
"object-hash": "^3.0.0", "object-hash": "^3.0.0",
"phone": "^3.1.69", "phone": "^3.1.70",
"posthog-js": "^1.335.0", "posthog-js": "^1.336.4",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"query-string": "^9.3.1", "query-string": "^9.3.1",
"raf-schd": "^4.0.3", "raf-schd": "^4.0.3",
"react": "^19.2.3", "react": "^19.2.4",
"react-big-calendar": "^1.19.4", "react-big-calendar": "^1.19.4",
"react-color": "^2.19.3", "react-color": "^2.19.3",
"react-cookie": "^8.0.1", "react-cookie": "^8.0.1",
"react-dom": "^19.2.3", "react-dom": "^19.2.4",
"react-drag-listview": "^2.0.0",
"react-grid-gallery": "^1.0.1", "react-grid-gallery": "^1.0.1",
"react-grid-layout": "^2.2.2", "react-grid-layout": "^2.2.2",
"react-i18next": "^16.5.3", "react-i18next": "^16.5.4",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-image-lightbox": "^5.1.4", "react-image-lightbox": "^5.1.4",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
@@ -71,10 +74,10 @@
"react-product-fruits": "^2.2.62", "react-product-fruits": "^2.2.62",
"react-redux": "^9.2.0", "react-redux": "^9.2.0",
"react-resizable": "^3.1.3", "react-resizable": "^3.1.3",
"react-router-dom": "^7.12.0", "react-router-dom": "^7.13.0",
"react-sticky": "^6.0.3", "react-sticky": "^6.0.3",
"react-virtuoso": "^4.18.1", "react-virtuoso": "^4.18.1",
"recharts": "^3.6.0", "recharts": "^3.7.0",
"redux": "^5.0.1", "redux": "^5.0.1",
"redux-actions": "^3.0.3", "redux-actions": "^3.0.3",
"redux-persist": "^6.0.0", "redux-persist": "^6.0.0",
@@ -82,7 +85,7 @@
"redux-state-sync": "^3.1.4", "redux-state-sync": "^3.1.4",
"reselect": "^5.1.1", "reselect": "^5.1.1",
"rxjs": "^7.8.2", "rxjs": "^7.8.2",
"sass": "^1.97.2", "sass": "^1.97.3",
"socket.io-client": "^4.8.3", "socket.io-client": "^4.8.3",
"styled-components": "^6.3.8", "styled-components": "^6.3.8",
"vite-plugin-ejs": "^1.7.0", "vite-plugin-ejs": "^1.7.0",
@@ -92,15 +95,17 @@
"postinstall": "echo 'when updating react-big-calendar, remember to check to localizer in the calendar wrapper'", "postinstall": "echo 'when updating react-big-calendar, remember to check to localizer in the calendar wrapper'",
"analyze": "source-map-explorer 'build/static/js/*.js'", "analyze": "source-map-explorer 'build/static/js/*.js'",
"start": "vite", "start": "vite",
"build": "dotenvx run --env-file=.env.development.imex -- vite build", "build": "vite build",
"start:imex": "dotenvx run --env-file=.env.development.imex -- vite", "build:dev:imex": "dotenvx run --env-file=.env.development.imex --env-file=.env.development.local.overrides -- vite build",
"start:rome": "dotenvx run --env-file=.env.development.rome -- vite", "build:dev:rome": "dotenvx run --env-file=.env.development.rome --env-file=.env.development.local.overrides -- vite build",
"preview:imex": "dotenvx run --env-file=.env.development.imex -- vite preview", "build:test:imex": "dotenvx run --env-file=.env.test.imex -- vite build",
"preview:rome": "dotenvx run --env-file=.env.development.rome -- vite preview", "build:test:rome": "dotenvx run --env-file=.env.test.rome -- vite build",
"build:test:imex": "env-cmd -f .env.test.imex -- npm run build", "build:production:imex": "env-cmd -f .env.production.imex vite build",
"build:test:rome": "env-cmd -f .env.test.rome -- npm run build", "build:production:rome": "env-cmd -f .env.production.rome vite build",
"build:production:imex": "env-cmd -f .env.production.imex -- npm run build", "start:imex": "dotenvx run --env-file=.env.development.imex --env-file=.env.development.local.overrides -- vite",
"build:production:rome": "env-cmd -f .env.production.rome -- npm run build", "start:rome": "dotenvx run --env-file=.env.development.rome --env-file=.env.development.local.overrides -- vite",
"preview:imex": "dotenvx run --env-file=.env.development.imex --env-file=.env.development.local.overrides -- vite preview",
"preview:rome": "dotenvx run --env-file=.env.development.rome --env-file=.env.development.local.overrides -- vite preview",
"madge": "madge --image ./madge-graph.svg --extensions js,jsx,ts,tsx --circular .", "madge": "madge --image ./madge-graph.svg --extensions js,jsx,ts,tsx --circular .",
"eulaize": "node src/utils/eulaize.js", "eulaize": "node src/utils/eulaize.js",
"test:unit": "vitest run", "test:unit": "vitest run",
@@ -151,7 +156,7 @@
"eslint": "^9.39.2", "eslint": "^9.39.2",
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-compiler": "^19.1.0-rc.2", "eslint-plugin-react-compiler": "^19.1.0-rc.2",
"globals": "^17.1.0", "globals": "^17.2.0",
"jsdom": "^27.4.0", "jsdom": "^27.4.0",
"memfs": "^4.56.10", "memfs": "^4.56.10",
"os-browserify": "^0.3.0", "os-browserify": "^0.3.0",

View File

@@ -100,14 +100,7 @@ export function App({
if (currentUser.authorized && bodyshop) { if (currentUser.authorized && bodyshop) {
client.setAttribute("imexshopid", bodyshop.imexshopid); client.setAttribute("imexshopid", bodyshop.imexshopid);
if ( if (client.getTreatment("LogRocket_Tracking") === "on") {
client.getTreatment("LogRocket_Tracking") === "on" ||
window.location.hostname ===
InstanceRenderMgr({
imex: "beta.imex.online",
rome: "beta.romeonline.io"
})
) {
console.log("LR Start"); console.log("LR Start");
LogRocket.init( LogRocket.init(
InstanceRenderMgr({ InstanceRenderMgr({

View File

@@ -446,3 +446,32 @@
//.rbc-time-header-gutter { //.rbc-time-header-gutter {
// padding: 0; // padding: 0;
//} //}
/* globally allow shrink inside table cells */
.prod-list-table .ant-table-cell,
.prod-list-table .ant-table-cell > * {
min-width: 0;
}
/* common AntD offenders */
.prod-list-table > .ant-table-cell .ant-space,
.ant-table-cell .ant-space-item {
min-width: 0;
}
/* Keep your custom header content on the left, push AntD sorter to the far right */
.prod-list-table .ant-table-column-sorters {
display: flex !important;
align-items: center;
width: 100%;
}
.prod-list-table .ant-table-column-title {
flex: 1 1 auto;
min-width: 0; /* allows ellipsis to work */
}
.prod-list-table .ant-table-column-sorter {
margin-left: auto;
flex: 0 0 auto;
}

View File

@@ -169,7 +169,13 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, bills, ref
refetch={refetch} refetch={refetch}
/> />
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && <QboAuthorizeComponent />} {bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && <QboAuthorizeComponent />}
<Input value={state.search} onChange={handleSearch} placeholder={t("general.labels.search")} allowClear /> <Input
value={state.search}
onChange={handleSearch}
placeholder={t("general.labels.search")}
allowClear
enterButton
/>
</Space> </Space>
} }
> >

View File

@@ -182,7 +182,13 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, payments,
refetch={refetch} refetch={refetch}
/> />
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && <QboAuthorizeComponent />} {bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && <QboAuthorizeComponent />}
<Input value={state.search} onChange={handleSearch} placeholder={t("general.labels.search")} allowClear /> <Input
value={state.search}
onChange={handleSearch}
placeholder={t("general.labels.search")}
allowClear
enterButton
/>
</Space> </Space>
} }
> >

View File

@@ -204,6 +204,7 @@ export function AccountingReceivablesTableComponent({ bodyshop, loading, jobs, r
onChange={handleSearch} onChange={handleSearch}
placeholder={t("general.labels.search")} placeholder={t("general.labels.search")}
allowClear allowClear
enterButton
/> />
</Space> </Space>
} }

View File

@@ -52,6 +52,9 @@ export default function BillCmdReturnsTableComponent({ form, returnLoading, retu
{fields.map((field, index) => ( {fields.map((field, index) => (
<tr key={field.key}> <tr key={field.key}>
<td> <td>
<Form.Item hidden key={`${index}id`} name={[field.name, "id"]}>
<ReadOnlyFormItemComponent />
</Form.Item>
<Form.Item <Form.Item
// label={t("joblines.fields.line_desc")} // label={t("joblines.fields.line_desc")}
key={`${index}line_desc`} key={`${index}line_desc`}

View File

@@ -48,7 +48,7 @@ export function BillDetailEditReturn({ setPartsOrderContext, data, disabled }) {
// db_price: i.actual_price, // db_price: i.actual_price,
act_price: i.actual_price, act_price: i.actual_price,
cost: i.actual_cost, cost: i.actual_cost,
quantity: i.quantity, part_qty: i.quantity,
joblineid: i.joblineid, joblineid: i.joblineid,
oem_partno: i.jobline && i.jobline.oem_partno, oem_partno: i.jobline && i.jobline.oem_partno,
part_type: i.jobline && i.jobline.part_type part_type: i.jobline && i.jobline.part_type
@@ -104,6 +104,10 @@ export function BillDetailEditReturn({ setPartsOrderContext, data, disabled }) {
{fields.map((field, index) => ( {fields.map((field, index) => (
<tr key={field.key}> <tr key={field.key}>
<td> <td>
{/* Hidden field to preserve the id */}
<Form.Item name={[field.name, "id"]} hidden>
<input type="hidden" />
</Form.Item>
<Form.Item <Form.Item
// label={t("joblines.fields.selected")} // label={t("joblines.fields.selected")}
key={`${index}selected`} key={`${index}selected`}

View File

@@ -373,9 +373,11 @@ export function BillFormComponent({
"local_tax_rate" "local_tax_rate"
]); ]);
let totals; let totals;
if (!!values.total && !!values.billlines && values.billlines.length > 0) if (!!values.total && !!values.billlines && values.billlines.length > 0) {
totals = CalculateBillTotal(values); totals = CalculateBillTotal(values);
if (totals) }
if (totals) {
return ( return (
// TODO: Align is not correct // TODO: Align is not correct
// eslint-disable-next-line react/no-unknown-property // eslint-disable-next-line react/no-unknown-property
@@ -414,7 +416,7 @@ export function BillFormComponent({
<Statistic <Statistic
title={t("bills.labels.discrepancy")} title={t("bills.labels.discrepancy")}
styles={{ styles={{
value: { content: {
color: totals.discrepancy.getAmount() === 0 ? "green" : "red" color: totals.discrepancy.getAmount() === 0 ? "green" : "red"
} }
}} }}
@@ -427,6 +429,7 @@ export function BillFormComponent({
) : null} ) : null}
</div> </div>
); );
}
return null; return null;
}} }}
</Form.Item> </Form.Item>

View File

@@ -1,6 +1,7 @@
import { DeleteFilled, DollarCircleFilled } from "@ant-design/icons"; import { DeleteFilled, DollarCircleFilled } from "@ant-design/icons";
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import { Button, Checkbox, Form, Input, InputNumber, Select, Space, Switch, Table, Tooltip } from "antd"; import { Button, Checkbox, Form, Input, InputNumber, Select, Space, Switch, Table, Tooltip } from "antd";
import { useRef } 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";
@@ -32,6 +33,7 @@ export function BillEnterModalLinesComponent({
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const { setFieldsValue, getFieldsValue, getFieldValue } = form; const { setFieldsValue, getFieldsValue, getFieldValue } = form;
const firstFieldRefs = useRef({});
const CONTROL_HEIGHT = 32; const CONTROL_HEIGHT = 32;
@@ -90,6 +92,7 @@ export function BillEnterModalLinesComponent({
}); });
}; };
// Only fill actual_cost when the user forward-tabs out of Retail (actual_price)
const autofillActualCost = (index) => { const autofillActualCost = (index) => {
Promise.resolve().then(() => { Promise.resolve().then(() => {
const retailRaw = form.getFieldValue(["billlines", index, "actual_price"]); const retailRaw = form.getFieldValue(["billlines", index, "actual_price"]);
@@ -154,6 +157,9 @@ export function BillEnterModalLinesComponent({
), ),
formInput: (record, index) => ( formInput: (record, index) => (
<BillLineSearchSelect <BillLineSearchSelect
ref={(el) => {
firstFieldRefs.current[index] = el;
}}
disabled={disabled} disabled={disabled}
options={lineData} options={lineData}
style={{ style={{
@@ -164,10 +170,9 @@ export function BillEnterModalLinesComponent({
}} }}
allowRemoved={form.getFieldValue("is_credit_memo") || false} allowRemoved={form.getFieldValue("is_credit_memo") || false}
onSelect={(value, opt) => { onSelect={(value, opt) => {
const d = normalizeDiscount(discount); // IMPORTANT:
const retail = Number(opt.cost); // Do NOT autofill actual_cost here. It should only fill when the user forward-tabs
const computedActual = Number.isFinite(retail) ? round2(retail * (1 - d)) : null; // from Retail (actual_price) -> Actual Cost (actual_cost).
setFieldsValue({ setFieldsValue({
billlines: (getFieldValue("billlines") || []).map((item, idx) => { billlines: (getFieldValue("billlines") || []).map((item, idx) => {
if (idx !== index) return item; if (idx !== index) return item;
@@ -178,7 +183,7 @@ export function BillEnterModalLinesComponent({
quantity: opt.part_qty || 1, quantity: opt.part_qty || 1,
actual_price: opt.cost, actual_price: opt.cost,
original_actual_price: opt.cost, original_actual_price: opt.cost,
actual_cost: isBlank(item.actual_cost) ? computedActual : item.actual_cost, // actual_cost intentionally untouched here
cost_center: opt.part_type cost_center: opt.part_type
? bodyshopHasDmsKey(bodyshop) ? bodyshopHasDmsKey(bodyshop)
? opt.part_type !== "PAE" ? opt.part_type !== "PAE"
@@ -205,7 +210,7 @@ export function BillEnterModalLinesComponent({
label: t("billlines.fields.line_desc"), label: t("billlines.fields.line_desc"),
rules: [{ required: true }] rules: [{ required: true }]
}), }),
formInput: () => <Input.TextArea disabled={disabled} autoSize /> formInput: () => <Input.TextArea disabled={disabled} autoSize tabIndex={0} />
}, },
{ {
title: t("billlines.fields.quantity"), title: t("billlines.fields.quantity"),
@@ -234,7 +239,7 @@ export function BillEnterModalLinesComponent({
}) })
] ]
}), }),
formInput: () => <InputNumber precision={0} min={1} disabled={disabled} /> formInput: () => <InputNumber precision={0} min={1} disabled={disabled} tabIndex={0} />
}, },
{ {
title: t("billlines.fields.actual_price"), title: t("billlines.fields.actual_price"),
@@ -251,9 +256,10 @@ export function BillEnterModalLinesComponent({
<CurrencyInput <CurrencyInput
min={0} min={0}
disabled={disabled} disabled={disabled}
onBlur={() => autofillActualCost(index)} tabIndex={0}
// NOTE: Autofill should only happen on forward Tab out of Retail
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === "Tab") autofillActualCost(index); if (e.key === "Tab" && !e.shiftKey) autofillActualCost(index);
}} }}
/> />
), ),
@@ -328,8 +334,9 @@ export function BillEnterModalLinesComponent({
min={0} min={0}
disabled={disabled} disabled={disabled}
controls={false} controls={false}
tabIndex={0}
style={{ width: "100%", height: CONTROL_HEIGHT }} style={{ width: "100%", height: CONTROL_HEIGHT }}
onFocus={() => autofillActualCost(index)} // NOTE: No auto-fill on focus/blur; only triggered from Retail on Tab
/> />
</Form.Item> </Form.Item>
</div> </div>
@@ -392,7 +399,7 @@ export function BillEnterModalLinesComponent({
rules: [{ required: true }] rules: [{ required: true }]
}), }),
formInput: () => ( formInput: () => (
<Select showSearch style={{ minWidth: "3rem" }} disabled={disabled}> <Select showSearch style={{ minWidth: "3rem" }} disabled={disabled} tabIndex={0}>
{bodyshopHasDmsKey(bodyshop) {bodyshopHasDmsKey(bodyshop)
? CiecaSelect(true, false) ? CiecaSelect(true, false)
: responsibilityCenters.costs.map((item) => <Select.Option key={item.name}>{item.name}</Select.Option>)} : responsibilityCenters.costs.map((item) => <Select.Option key={item.name}>{item.name}</Select.Option>)}
@@ -412,7 +419,7 @@ export function BillEnterModalLinesComponent({
name: [field.name, "location"] name: [field.name, "location"]
}), }),
formInput: () => ( formInput: () => (
<Select disabled={disabled}> <Select disabled={disabled} tabIndex={0}>
{bodyshop.md_parts_locations.map((loc, idx) => ( {bodyshop.md_parts_locations.map((loc, idx) => (
<Select.Option key={idx} value={loc}> <Select.Option key={idx} value={loc}>
{loc} {loc}
@@ -432,7 +439,7 @@ export function BillEnterModalLinesComponent({
key: `${field.name}deductedfromlbr`, key: `${field.name}deductedfromlbr`,
name: [field.name, "deductedfromlbr"] name: [field.name, "deductedfromlbr"]
}), }),
formInput: () => <Switch disabled={disabled} />, formInput: () => <Switch disabled={disabled} tabIndex={0} />,
additional: (record, index) => ( additional: (record, index) => (
<Form.Item shouldUpdate noStyle style={{ display: "inline-block" }}> <Form.Item shouldUpdate noStyle style={{ display: "inline-block" }}>
{() => { {() => {
@@ -517,9 +524,13 @@ export function BillEnterModalLinesComponent({
formItemProps: (field) => ({ formItemProps: (field) => ({
key: `${field.name}fedtax`, key: `${field.name}fedtax`,
valuePropName: "checked", valuePropName: "checked",
name: [field.name, "applicable_taxes", "federal"] name: [field.name, "applicable_taxes", "federal"],
initialValue: InstanceRenderManager({
imex: true,
rome: false
})
}), }),
formInput: () => <Switch disabled={disabled} /> formInput: () => <Switch disabled={disabled} tabIndex={0} />
} }
] ]
}), }),
@@ -534,7 +545,7 @@ export function BillEnterModalLinesComponent({
valuePropName: "checked", valuePropName: "checked",
name: [field.name, "applicable_taxes", "state"] name: [field.name, "applicable_taxes", "state"]
}), }),
formInput: () => <Switch disabled={disabled} /> formInput: () => <Switch disabled={disabled} tabIndex={0} />
}, },
...InstanceRenderManager({ ...InstanceRenderManager({
@@ -550,7 +561,7 @@ export function BillEnterModalLinesComponent({
valuePropName: "checked", valuePropName: "checked",
name: [field.name, "applicable_taxes", "local"] name: [field.name, "applicable_taxes", "local"]
}), }),
formInput: () => <Switch disabled={disabled} /> formInput: () => <Switch disabled={disabled} tabIndex={0} />
} }
] ]
}), }),
@@ -570,6 +581,7 @@ export function BillEnterModalLinesComponent({
icon={<DeleteFilled />} icon={<DeleteFilled />}
disabled={disabled || invLen > 0} disabled={disabled || invLen > 0}
onClick={() => remove(record.name)} onClick={() => remove(record.name)}
tabIndex={0}
/> />
{Simple_Inventory.treatment === "on" && ( {Simple_Inventory.treatment === "on" && (
@@ -641,12 +653,19 @@ export function BillEnterModalLinesComponent({
<Button <Button
disabled={disabled} disabled={disabled}
onClick={() => { onClick={() => {
const newIndex = fields.length;
add( add(
InstanceRenderManager({ InstanceRenderManager({
imex: { applicable_taxes: { federal: true } }, imex: { applicable_taxes: { federal: true } },
rome: { applicable_taxes: { federal: false } } rome: { applicable_taxes: { federal: false } }
}) })
); );
setTimeout(() => {
const firstField = firstFieldRefs.current[newIndex];
if (firstField?.focus) {
firstField.focus();
}
}, 100);
}} }}
style={{ width: "100%" }} style={{ width: "100%" }}
> >

View File

@@ -232,6 +232,7 @@ export function BillsListTableComponent({
e.preventDefault(); e.preventDefault();
setSearchText(e.target.value); setSearchText(e.target.value);
}} }}
enterButton
/> />
</Space> </Space>
} }

View File

@@ -57,7 +57,6 @@ const CardPaymentModalComponent = ({
QUERY_RO_AND_OWNER_BY_JOB_PKS, QUERY_RO_AND_OWNER_BY_JOB_PKS,
{ {
fetchPolicy: "network-only", fetchPolicy: "network-only",
notifyOnNetworkStatusChange: true
} }
); );

View File

@@ -47,7 +47,6 @@ export function ChatPopupComponent({ chatVisible, selectedConversation, toggleCh
const [getConversations, { loading, data, refetch, called }] = useLazyQuery(CONVERSATION_LIST_QUERY, { const [getConversations, { loading, data, refetch, called }] = useLazyQuery(CONVERSATION_LIST_QUERY, {
fetchPolicy: "network-only", fetchPolicy: "network-only",
nextFetchPolicy: "network-only", nextFetchPolicy: "network-only",
notifyOnNetworkStatusChange: true,
...(pollInterval > 0 ? { pollInterval } : {}) ...(pollInterval > 0 ? { pollInterval } : {})
}); });
@@ -108,9 +107,12 @@ export function ChatPopupComponent({ chatVisible, selectedConversation, toggleCh
hasLoadedConversationsOnceRef.current = true; hasLoadedConversationsOnceRef.current = true;
getConversations({ variables: { offset: 0 } }).catch((err) => { getConversations({ variables: { offset: 0 } }).catch((err) => {
console.error(`Error fetching conversations: ${err?.message || ""}`, err); // Ignore abort errors (they're expected when component unmounts)
if (err?.name !== "AbortError") {
console.error(`Error fetching conversations: ${err?.message || ""}`, err);
}
}); });
}, [getConversations]); }, []);
const handleManualRefresh = async () => { const handleManualRefresh = async () => {
try { try {

View File

@@ -99,6 +99,7 @@ export default function ContractsCarsComponent({ loading, data, selectedCarId, h
placeholder={t("general.labels.search")} placeholder={t("general.labels.search")}
value={state.search} value={state.search}
onChange={(e) => setState({ ...state, search: e.target.value })} onChange={(e) => setState({ ...state, search: e.target.value })}
enterButton
/> />
} }
> >

View File

@@ -1,5 +1,5 @@
import { WarningFilled } from "@ant-design/icons"; import { WarningFilled } from "@ant-design/icons";
import { Form, Input, InputNumber, Space } from "antd"; import { Card, Form, Input, InputNumber, Space } from "antd";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { DateFormatter } from "../../utils/DateFormatter"; import { DateFormatter } from "../../utils/DateFormatter";
import dayjs from "../../utils/day"; import dayjs from "../../utils/day";
@@ -19,9 +19,9 @@ import ContractFormJobPrefill from "./contract-form-job-prefill.component";
export default function ContractFormComponent({ form, create = false, selectedJobState, selectedCar }) { export default function ContractFormComponent({ form, create = false, selectedJobState, selectedCar }) {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<> <Card>
{!create && <FormFieldsChanged form={form} />} {!create && <FormFieldsChanged form={form} />}
<LayoutFormRow> <LayoutFormRow noDivider={true}>
{!create && ( {!create && (
<Form.Item <Form.Item
label={t("contracts.fields.status")} label={t("contracts.fields.status")}
@@ -310,6 +310,6 @@ export default function ContractFormComponent({ form, create = false, selectedJo
<InputNumber precision={2} /> <InputNumber precision={2} />
</Form.Item> </Form.Item>
</LayoutFormRow> </LayoutFormRow>
</> </Card>
); );
} }

View File

@@ -123,6 +123,7 @@ export default function ContractsJobsComponent({ loading, data, selectedJob, han
placeholder={t("general.labels.search")} placeholder={t("general.labels.search")}
value={state.search} value={state.search}
onChange={(e) => setState({ ...state, search: e.target.value })} onChange={(e) => setState({ ...state, search: e.target.value })}
enterButton
/> />
} }
> >

View File

@@ -164,6 +164,7 @@ export function ContractsList({ bodyshop, loading, contracts, refetch, total, se
const updatedSearch = { ...search, search: value }; const updatedSearch = { ...search, search: value };
history({ search: queryString.stringify(updatedSearch) }); history({ search: queryString.stringify(updatedSearch) });
}} }}
enterButton
/> />
</Space> </Space>
} }

View File

@@ -1,6 +1,6 @@
import { WarningFilled } from "@ant-design/icons"; import { WarningFilled } from "@ant-design/icons";
import { useApolloClient } from "@apollo/client/react"; import { useApolloClient } from "@apollo/client/react";
import { Button, Form, Input, InputNumber, Space } from "antd"; import { Button, Card, Form, Input, InputNumber, Space } from "antd";
import { PageHeader } from "@ant-design/pro-layout"; import { PageHeader } from "@ant-design/pro-layout";
import dayjs from "../../utils/day"; import dayjs from "../../utils/day";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -19,7 +19,7 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading, newC
const client = useApolloClient(); const client = useApolloClient();
return ( return (
<div> <Card>
<PageHeader <PageHeader
title={t("menus.header.courtesycars")} title={t("menus.header.courtesycars")}
extra={ extra={
@@ -314,6 +314,6 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading, newC
<CurrencyInput /> <CurrencyInput />
</Form.Item> </Form.Item>
</LayoutFormRow> </LayoutFormRow>
</div> </Card>
); );
} }

View File

@@ -96,6 +96,7 @@ export default function DashboardMonthlyJobCosting({ data, ...cardProps }) {
e.preventDefault(); e.preventDefault();
setSearchText(e.target.value); setSearchText(e.target.value);
}} }}
enterButton
/> />
</Space> </Space>
} }

View File

@@ -36,7 +36,7 @@ export function DashboardTotalProductionHours({ bodyshop, data, ...cardProps })
<Statistic <Statistic
title={t("dashboard.labels.prodhrs")} title={t("dashboard.labels.prodhrs")}
value={hours.total.toFixed(1)} value={hours.total.toFixed(1)}
styles={{ value: { color: aboveTargetHours ? "green" : "red" } }} styles={{ content: { color: aboveTargetHours ? "green" : "red" } }}
/> />
</Space> </Space>
</Card> </Card>

View File

@@ -49,12 +49,15 @@ export function DmsCdkVehicles({ form, job }) {
open={open} open={open}
onCancel={() => setOpen(false)} onCancel={() => setOpen(false)}
onOk={() => { onOk={() => {
form.setFieldsValue({ if (selectedModel) {
dms_make: selectedModel.makecode, form.setFieldsValue({
dms_model: selectedModel.modelcode dms_make: selectedModel.makecode,
}); dms_model: selectedModel.modelcode
setOpen(false); });
setOpen(false);
}
}} }}
okButtonProps={{ disabled: !selectedModel }}
> >
{error && <AlertComponent title={error.message} type="error" />} {error && <AlertComponent title={error.message} type="error" />}
<Table <Table
@@ -62,6 +65,7 @@ export function DmsCdkVehicles({ form, job }) {
<Input.Search <Input.Search
onSearch={(val) => callSearch({ variables: { search: val } })} onSearch={(val) => callSearch({ variables: { search: val } })}
placeholder={t("general.labels.search")} placeholder={t("general.labels.search")}
enterButton
/> />
)} )}
columns={columns} columns={columns}

View File

@@ -53,13 +53,13 @@ export default function FortellisCustomerSelector({ bodyshop, jobid, socket }) {
render: (_t, r) => <Checkbox disabled checked={r.vinOwner} /> render: (_t, r) => <Checkbox disabled checked={r.vinOwner} />
}, },
{ {
title: t("jobs.fields.dms.name1"), title: t("jobs.fields.dms.first_name"),
dataIndex: ["customerName", "firstName"], dataIndex: ["customerName", "firstName"],
key: "firstName", key: "firstName",
sorter: (a, b) => alphaSort(a.customerName?.firstName, b.customerName?.firstName) sorter: (a, b) => alphaSort(a.customerName?.firstName, b.customerName?.firstName)
}, },
{ {
title: t("jobs.fields.dms.name1"), title: t("jobs.fields.dms.last_name"),
dataIndex: ["customerName", "lastName"], dataIndex: ["customerName", "lastName"],
key: "lastName", key: "lastName",
sorter: (a, b) => alphaSort(a.customerName?.lastName, b.customerName?.lastName) sorter: (a, b) => alphaSort(a.customerName?.lastName, b.customerName?.lastName)

View File

@@ -272,7 +272,7 @@ export default function CdkLikePostForm({ bodyshop, socket, job, logsRef, mode,
name={[field.name, "name"]} name={[field.name, "name"]}
rules={[{ required: true }]} rules={[{ required: true }]}
> >
<Select style={{ minWidth: "15rem" }} onSelect={(value) => handlePayerSelect(value, index)}> <Select style={{ width: "100%" }} onSelect={(value) => handlePayerSelect(value, index)}>
{bodyshop.cdk_configuration?.payers?.map((payer) => ( {bodyshop.cdk_configuration?.payers?.map((payer) => (
<Select.Option key={payer.name}>{payer.name}</Select.Option> <Select.Option key={payer.name}>{payer.name}</Select.Option>
))} ))}
@@ -404,7 +404,7 @@ export default function CdkLikePostForm({ bodyshop, socket, job, logsRef, mode,
<Typography.Title>=</Typography.Title> <Typography.Title>=</Typography.Title>
<Statistic <Statistic
title={t("jobs.labels.dms.notallocated")} title={t("jobs.labels.dms.notallocated")}
styles={{ value: { color: discrep.getAmount() === 0 ? "green" : "red" } }} styles={{ content: { color: discrep.getAmount() === 0 ? "green" : "red" } }}
value={discrep.toFormat()} value={discrep.toFormat()}
/> />
<Button disabled={disablePost} htmlType="submit"> <Button disabled={disablePost} htmlType="submit">

View File

@@ -14,8 +14,11 @@ export default function GlobalSearch() {
const [callSearch, { loading, error, data }] = useLazyQuery(GLOBAL_SEARCH_QUERY); const [callSearch, { loading, error, data }] = useLazyQuery(GLOBAL_SEARCH_QUERY);
const navigate = useNavigate(); const navigate = useNavigate();
const executeSearch = (v) => { const executeSearch = (variables) => {
if (v && v.variables.search && v.variables.search !== "" && v.variables.search.length >= 3) callSearch(v); if (variables?.search !== "" && variables?.search?.length >= 3)
callSearch({
variables
});
}; };
const debouncedExecuteSearch = _.debounce(executeSearch, 750); const debouncedExecuteSearch = _.debounce(executeSearch, 750);
@@ -157,7 +160,9 @@ export default function GlobalSearch() {
return ( return (
<AutoComplete <AutoComplete
options={options} options={options}
onSearch={handleSearch} showSearch={{
onSearch: handleSearch
}}
defaultActiveFirstOption defaultActiveFirstOption
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key !== "Enter") return; if (e.key !== "Enter") return;

View File

@@ -97,7 +97,7 @@ export function Jobd3RdPartyModal({ bodyshop, jobId, job, technician }) {
return ( return (
<> <>
<Button onClick={showModal}>{t("printcenter.jobs.3rdpartypayer")}</Button> <Button onClick={showModal}>{t("printcenter.jobs.3rdpartypayer")}</Button>
<Modal open={isModalVisible} onOk={handleOk} onCancel={handleCancel}> <Modal open={isModalVisible} onOk={handleOk} onCancel={handleCancel} getContainer={() => document.body}>
<Form onFinish={handleFinish} autoComplete={"off"} layout="vertical" form={form}> <Form onFinish={handleFinish} autoComplete={"off"} layout="vertical" form={form}>
<Form.Item label={t("bills.fields.vendor")} name="vendorid"> <Form.Item label={t("bills.fields.vendor")} name="vendorid">
<VendorSearchSelect options={VendorAutoCompleteData?.vendors} onSelect={handleVendorSelect} /> <VendorSearchSelect options={VendorAutoCompleteData?.vendors} onSelect={handleVendorSelect} />

View File

@@ -76,6 +76,7 @@ export default function JobCostingPartsTable({ data, summaryData }) {
e.preventDefault(); e.preventDefault();
setSearchText(e.target.value); setSearchText(e.target.value);
}} }}
enterButton
/> />
</Space> </Space>
); );

View File

@@ -50,7 +50,8 @@ export function JobCreateIOU({ bodyshop, currentUser, job, selectedJobLines, tec
config: { config: {
status: bodyshop.md_ro_statuses.default_open, status: bodyshop.md_ro_statuses.default_open,
bodyshopid: bodyshop.id, bodyshopid: bodyshop.id,
useremail: currentUser.email useremail: currentUser.email,
timezone: bodyshop.timezone
}, },
currentUser currentUser
}); });

View File

@@ -682,6 +682,7 @@ export function JobLinesComponent({
e.preventDefault(); e.preventDefault();
setSearchText(e.target.value); setSearchText(e.target.value);
}} }}
enterButton
/> />
</Space> </Space>
} }

View File

@@ -1,29 +1,65 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { Tag, Tooltip } from "antd"; import { Tooltip } from "antd";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import { useTranslation } from "react-i18next";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop bodyshop: selectBodyshop
}); });
const mapDispatchToProps = () => ({ const mapDispatchToProps = () => ({});
//setUserLanguage: language => dispatch(setUserLanguage(language))
}); const colorMap = {
gray: { bg: "#fafafa", border: "#d9d9d9", text: "#000000" },
gold: { bg: "#fffbe6", border: "#ffe58f", text: "#d48806" },
red: { bg: "#fff1f0", border: "#ffccc7", text: "#cf1322" },
blue: { bg: "#e6f7ff", border: "#91d5ff", text: "#0958d9" },
green: { bg: "#f6ffed", border: "#b7eb8f", text: "#389e0d" },
orange: { bg: "#fff7e6", border: "#ffd591", text: "#d46b08" }
};
function CompactTag({ color = "gray", children, tooltip = "" }) {
const colors = colorMap[color] || colorMap.gray;
return (
<span
style={{
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
padding: "0 2px",
fontSize: "12px",
lineHeight: "20px",
backgroundColor: colors.bg,
border: `1px solid ${colors.border}`,
borderRadius: "2px",
color: colors.text,
minWidth: "24px",
textAlign: "center"
}}
>
<Tooltip title={tooltip}>{children}</Tooltip>
</span>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(JobPartsQueueCount); export default connect(mapStateToProps, mapDispatchToProps)(JobPartsQueueCount);
export function JobPartsQueueCount({ bodyshop, parts }) { export function JobPartsQueueCount({ bodyshop, parts }) {
const { t } = useTranslation();
const partsStatus = useMemo(() => { const partsStatus = useMemo(() => {
if (!parts) return null; if (!parts) return null;
const statusKeys = ["default_bo", "default_ordered", "default_received", "default_returned"]; const statusKeys = ["default_bo", "default_ordered", "default_received", "default_returned"];
return parts.reduce( return parts.reduce(
(acc, val) => { (acc, val) => {
if (val.part_type === "PAS" || val.part_type === "PASL") return acc; if (val.part_type === "PAS" || val.part_type === "PASL") return acc;
acc.total = acc.total + val.count;
acc[val.status] = acc[val.status] + val.count; acc.total += val.count;
// NOTE: if val.status is null, object key becomes "null"
acc[val.status] = (acc[val.status] ?? 0) + val.count;
return acc; return acc;
}, },
{ {
@@ -34,45 +70,38 @@ export function JobPartsQueueCount({ bodyshop, parts }) {
); );
}, [bodyshop, parts]); }, [bodyshop, parts]);
if (!parts) return null; if (!parts || !partsStatus) return null;
return ( return (
<div <div
style={{ style={{
display: "grid", display: "inline-flex", // ✅ shrink-wraps, fixes the “extra box” to the right
gridTemplateColumns: "repeat(auto-fit, minmax(40px, 1fr))", gap: 2,
gap: "8px", alignItems: "center",
width: "100%", whiteSpace: "nowrap"
justifyItems: "start"
}} }}
> >
<Tooltip title="Total"> <CompactTag tooltip="Total" color="gray">
<Tag style={{ minWidth: "40px", textAlign: "center" }}>{partsStatus.total}</Tag> {partsStatus.total}
</Tooltip> </CompactTag>
<Tooltip title={t("dashboard.errors.status_normal")}>
<Tag color="gold" style={{ minWidth: "40px", textAlign: "center" }}> <CompactTag tooltip="No Status" color="gold">
{partsStatus["null"]} {partsStatus["null"]}
</Tag> </CompactTag>
</Tooltip>
<Tooltip title={bodyshop.md_order_statuses.default_bo}> <CompactTag tooltip={bodyshop.md_order_statuses.default_bo} color="red">
<Tag color="red" style={{ minWidth: "40px", textAlign: "center" }}> {partsStatus[bodyshop.md_order_statuses.default_bo]}
{partsStatus[bodyshop.md_order_statuses.default_bo]} </CompactTag>
</Tag>
</Tooltip> <CompactTag tooltip={bodyshop.md_order_statuses.default_ordered} color="blue">
<Tooltip title={bodyshop.md_order_statuses.default_ordered}> {partsStatus[bodyshop.md_order_statuses.default_ordered]}
<Tag color="blue" style={{ minWidth: "40px", textAlign: "center" }}> </CompactTag>
{partsStatus[bodyshop.md_order_statuses.default_ordered]} <CompactTag tooltip={bodyshop.md_order_statuses.default_received} color="green">
</Tag> {partsStatus[bodyshop.md_order_statuses.default_received]}
</Tooltip> </CompactTag>
<Tooltip title={bodyshop.md_order_statuses.default_received}> <CompactTag tooltip={bodyshop.md_order_statuses.default_returned} color="orange">
<Tag color="green" style={{ minWidth: "40px", textAlign: "center" }}> {partsStatus[bodyshop.md_order_statuses.default_returned]}
{partsStatus[bodyshop.md_order_statuses.default_received]} </CompactTag>
</Tag>
</Tooltip>
<Tooltip title={bodyshop.md_order_statuses.default_returned}>
<Tag color="orange" style={{ minWidth: "40px", textAlign: "center" }}>
{partsStatus[bodyshop.md_order_statuses.default_returned]}
</Tag>
</Tooltip>
</div> </div>
); );
} }

View File

@@ -18,10 +18,17 @@ const mapStateToProps = createStructuredSelector({
* @param parts * @param parts
* @param displayMode * @param displayMode
* @param popoverPlacement * @param popoverPlacement
* @param countsOnly
* @returns {JSX.Element} * @returns {JSX.Element}
* @constructor * @constructor
*/ */
export function JobPartsReceived({ bodyshop, parts, displayMode = "full", popoverPlacement = "top" }) { export function JobPartsReceived({
bodyshop,
parts,
displayMode = "full",
popoverPlacement = "top",
countsOnly = false
}) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const { t } = useTranslation(); const { t } = useTranslation();
@@ -61,6 +68,8 @@ export function JobPartsReceived({ bodyshop, parts, displayMode = "full", popove
[canOpen] [canOpen]
); );
if (countsOnly) return <JobPartsQueueCount parts={parts} />;
const displayText = const displayText =
displayMode === "compact" ? summary.percentLabel : `${summary.percentLabel} (${summary.received}/${summary.total})`; displayMode === "compact" ? summary.percentLabel : `${summary.percentLabel} (${summary.received}/${summary.total})`;
@@ -74,7 +83,7 @@ export function JobPartsReceived({ bodyshop, parts, displayMode = "full", popove
trigger={["click"]} trigger={["click"]}
placement={popoverPlacement} placement={popoverPlacement}
content={ content={
<div onClick={stop} style={{ minWidth: 260 }}> <div onClick={stop}>
<JobPartsQueueCount parts={parts} /> <JobPartsQueueCount parts={parts} />
</div> </div>
} }
@@ -99,7 +108,8 @@ JobPartsReceived.propTypes = {
bodyshop: PropTypes.object, bodyshop: PropTypes.object,
parts: PropTypes.array, parts: PropTypes.array,
displayMode: PropTypes.oneOf(["full", "compact"]), displayMode: PropTypes.oneOf(["full", "compact"]),
popoverPlacement: PropTypes.string popoverPlacement: PropTypes.string,
countsOnly: PropTypes.bool
}; };
export default connect(mapStateToProps)(JobPartsReceived); export default connect(mapStateToProps)(JobPartsReceived);

View File

@@ -136,6 +136,7 @@ export function JobsAvailableScan({ partnerVersion, refetch }) {
onChange={(e) => { onChange={(e) => {
setSearchText(e.currentTarget.value); setSearchText(e.currentTarget.value);
}} }}
enterButton
/> />
</Space> </Space>
} }

View File

@@ -196,13 +196,16 @@ export function JobsAvailableComponent({ bodyshop, loading, data, refetch, addJo
> >
{t("general.actions.deleteall")} {t("general.actions.deleteall")}
</Button> </Button>
<Input.Search <Input.Search
placeholder={t("general.labels.search")} placeholder={t("general.labels.search")}
onChange={(e) => { onChange={(e) => {
setSearchText(e.currentTarget.value); setSearchText(e.currentTarget.value);
}} }}
enterButton
/> />
<Link to="/manage/jobs/new">
<Button>{t("jobs.actions.manualnew")}</Button>
</Link>
</Space> </Space>
} }
> >

View File

@@ -42,6 +42,10 @@ export function JobsCloseLines({ bodyshop, job, jobRO }) {
<tbody> <tbody>
{fields.map((field, index) => ( {fields.map((field, index) => (
<tr key={field.key}> <tr key={field.key}>
{/* Hidden field to preserve jobline ID */}
<Form.Item hidden name={[field.name, "id"]}>
<input />
</Form.Item>
<td> <td>
<Form.Item <Form.Item
// label={t("joblines.fields.line_desc")} // label={t("joblines.fields.line_desc")}

View File

@@ -24,7 +24,7 @@ export default function JobsCreateVehicleInfoPredefined({ disabled, form }) {
<div> <div>
<Table <Table
size="small" size="small"
title={() => <Input.Search onSearch={(value) => setSearch(value)} />} title={() => <Input.Search onSearch={(value) => setSearch(value)} enterButton/>}
dataSource={filteredPredefinedVehicles} dataSource={filteredPredefinedVehicles}
columns={[ columns={[
{ {

View File

@@ -1,10 +1,11 @@
import { DownCircleFilled } from "@ant-design/icons"; import { DownCircleFilled } from "@ant-design/icons";
import { useApolloClient, useMutation, useQuery } from "@apollo/client/react"; import { useApolloClient, useMutation, useQuery } from "@apollo/client/react";
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import { Button, Card, Dropdown, Form, Input, Modal, Popconfirm, Popover, Select, Space } from "antd"; import { Button, Card, Dropdown, Form, Input, Modal, Popover, Select, Space } from "antd";
import axios from "axios"; import axios from "axios";
import parsePhoneNumber from "libphonenumber-js"; import parsePhoneNumber from "libphonenumber-js";
import { useCallback, useMemo, useRef, useState } from "react"; import { useCallback, useMemo, useRef, useState } from "react";
import { HasRbacAccess } from "../../components/rbac-wrapper/rbac-wrapper.component";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Link, useNavigate } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
@@ -20,7 +21,7 @@ import { selectJobReadOnly } from "../../redux/application/application.selectors
import { setEmailOptions } from "../../redux/email/email.actions"; import { setEmailOptions } from "../../redux/email/email.actions";
import { openChatByPhone, setMessage } from "../../redux/messaging/messaging.actions"; import { openChatByPhone, setMessage } from "../../redux/messaging/messaging.actions";
import { setModalContext } from "../../redux/modals/modals.actions"; import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors"; import { selectAuthLevel, selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings"; import AuditTrailMapping from "../../utils/AuditTrailMappings";
import { DateTimeFormatter } from "../../utils/DateFormatter"; import { DateTimeFormatter } from "../../utils/DateFormatter";
import { TemplateList } from "../../utils/TemplateConstants"; import { TemplateList } from "../../utils/TemplateConstants";
@@ -28,7 +29,6 @@ import dayjs from "../../utils/day";
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component"; import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component"; import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
import LockerWrapperComponent from "../lock-wrapper/lock-wrapper.component"; import LockerWrapperComponent from "../lock-wrapper/lock-wrapper.component";
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
import ShareToTeamsButton from "../share-to-teams/share-to-teams.component.jsx"; import ShareToTeamsButton from "../share-to-teams/share-to-teams.component.jsx";
import AddToProduction from "./jobs-detail-header-actions.addtoproduction.util"; import AddToProduction from "./jobs-detail-header-actions.addtoproduction.util";
import DuplicateJob from "./jobs-detail-header-actions.duplicate.util"; import DuplicateJob from "./jobs-detail-header-actions.duplicate.util";
@@ -39,7 +39,8 @@ const EMPTY_ARRAY = Object.freeze([]);
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
jobRO: selectJobReadOnly, jobRO: selectJobReadOnly,
currentUser: selectCurrentUser currentUser: selectCurrentUser,
authLevel: selectAuthLevel
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
@@ -117,7 +118,8 @@ export function JobsDetailHeaderActions({
openChatByPhone, openChatByPhone,
setMessage, setMessage,
setTimeTicketTaskContext, setTimeTicketTaskContext,
setTaskUpsertContext setTaskUpsertContext,
authLevel
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const client = useApolloClient(); const client = useApolloClient();
@@ -129,10 +131,6 @@ export function JobsDetailHeaderActions({
const jobId = job?.id; const jobId = job?.id;
const watcherVars = useMemo(() => ({ jobid: jobId }), [jobId]); const watcherVars = useMemo(() => ({ jobid: jobId }), [jobId]);
// Option A: coordinated Dropdown + Popconfirm open state so the menu doesn't unmount before Popconfirm renders.
const [confirmKey, setConfirmKey] = useState(null);
const confirmKeyRef = useRef(null);
const [isCancelScheduleModalVisible, setIsCancelScheduleModalVisible] = useState(false); const [isCancelScheduleModalVisible, setIsCancelScheduleModalVisible] = useState(false);
const [insertAppointment] = useMutation(INSERT_MANUAL_APPT); const [insertAppointment] = useMutation(INSERT_MANUAL_APPT);
const [deleteJob] = useMutation(DELETE_JOB); const [deleteJob] = useMutation(DELETE_JOB);
@@ -150,6 +148,8 @@ export function JobsDetailHeaderActions({
const devEmails = ["imex.dev", "rome.dev"]; const devEmails = ["imex.dev", "rome.dev"];
const prodEmails = ["imex.prod", "rome.prod", "imex.test", "rome.test"]; const prodEmails = ["imex.prod", "rome.prod", "imex.test", "rome.test"];
const canVoidJob = useMemo(() => HasRbacAccess({ authLevel, bodyshop, action: "jobs:void" }), [authLevel, bodyshop]);
const hasValidEmail = (emails) => emails.some((email) => userEmail.endsWith(email)); const hasValidEmail = (emails) => emails.some((email) => userEmail.endsWith(email));
const canSubmitForTesting = (isDevEnv && hasValidEmail(devEmails)) || (isProdEnv && hasValidEmail(prodEmails)); const canSubmitForTesting = (isDevEnv && hasValidEmail(devEmails)) || (isProdEnv && hasValidEmail(prodEmails));
@@ -157,7 +157,6 @@ export function JobsDetailHeaderActions({
variables: watcherVars, variables: watcherVars,
skip: !jobId, skip: !jobId,
fetchPolicy: "cache-first", fetchPolicy: "cache-first",
notifyOnNetworkStatusChange: true
}); });
const jobWatchersCount = jobWatchersData?.job_watchers?.length ?? job?.job_watchers?.length ?? 0; const jobWatchersCount = jobWatchersData?.job_watchers?.length ?? job?.job_watchers?.length ?? 0;
@@ -179,83 +178,69 @@ export function JobsDetailHeaderActions({
const jobInPreProduction = preProductionStatuses.includes(jobStatus); const jobInPreProduction = preProductionStatuses.includes(jobStatus);
const jobInPostProduction = postProductionStatuses.includes(jobStatus); const jobInPostProduction = postProductionStatuses.includes(jobStatus);
const openConfirm = useCallback((key) => { const makeConfirmId = () =>
confirmKeyRef.current = key; globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(16).slice(2)}`;
setConfirmKey(key);
setDropdownOpen(true);
}, []);
const closeConfirm = useCallback(() => { const [modal, modalContextHolder] = Modal.useModal();
confirmKeyRef.current = null;
setConfirmKey(null);
}, []);
const handleDropdownOpenChange = useCallback( const confirmInstancesRef = useRef(new Map());
(nextOpen, info) => {
if (!nextOpen && info?.source === "menu" && confirmKeyRef.current) return;
setDropdownOpen(nextOpen);
if (!nextOpen) closeConfirm();
},
[closeConfirm]
);
const renderPopconfirmMenuLabel = ({ const closeConfirmById = (id) => {
key, const inst = confirmInstancesRef.current.get(id);
text, if (inst) inst.destroy(); // hard close
confirmInstancesRef.current.delete(id);
};
const openConfirmFromMenu = ({
variant = "confirm", // "confirm" | "info" | "warning"
title, title,
content,
okText, okText,
cancelText, cancelText,
showCancel = true, showCancel = true,
closeDropdownOnConfirm = true, onOk,
onConfirm onCancel
}) => ( }) => {
<Popconfirm // close the dropdown immediately; confirm dialog is separate
title={title} setDropdownOpen(false);
okText={okText}
cancelText={cancelText}
showCancel={showCancel}
open={confirmKey === key}
onOpenChange={(nextOpen) => {
if (nextOpen) openConfirm(key);
else closeConfirm();
}}
onConfirm={(e) => {
e?.stopPropagation?.();
closeConfirm();
// Critical: for informational popconfirms, keep the dropdown open so the Popconfirm can cleanly close. const id = makeConfirmId();
if (closeDropdownOnConfirm) {
setDropdownOpen(false); const openFn = variant === "info" ? modal.info : variant === "warning" ? modal.warning : modal.confirm;
const inst = openFn({
title,
content,
okText,
cancelText,
centered: true,
maskClosable: false,
onCancel: () => {
closeConfirmById(id);
onCancel?.();
},
onOk: async () => {
try {
await onOk?.();
} finally {
closeConfirmById(id);
} }
},
...(showCancel ? {} : { okCancel: false })
});
onConfirm?.(e); confirmInstancesRef.current.set(id, inst);
}} return id;
onCancel={(e) => { };
e?.stopPropagation?.();
closeConfirm(); const handleDropdownOpenChange = useCallback((nextOpen) => {
// Keep dropdown open on cancel so the user can continue using the menu. setDropdownOpen(nextOpen);
}} }, []);
getPopupContainer={() => document.body}
>
<div
style={{ width: "100%" }}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
openConfirm(key);
}}
>
{text}
</div>
</Popconfirm>
);
// Function to show modal
const showCancelScheduleModal = () => { const showCancelScheduleModal = () => {
setIsCancelScheduleModalVisible(true); setIsCancelScheduleModalVisible(true);
}; };
// Function to handle Cancel
const handleCancelScheduleModalCancel = () => { const handleCancelScheduleModalCancel = () => {
setIsCancelScheduleModalVisible(false); setIsCancelScheduleModalVisible(false);
}; };
@@ -264,7 +249,7 @@ export function JobsDetailHeaderActions({
DuplicateJob({ DuplicateJob({
apolloClient: client, apolloClient: client,
jobId: job.id, jobId: job.id,
config: { defaultOpenStatus: bodyshop.md_ro_statuses.default_imported }, config: { defaultOpenStatus: bodyshop.md_ro_statuses.default_imported, timezone: bodyshop.timezone },
completionCallback: (newJobId) => { completionCallback: (newJobId) => {
history(`/manage/jobs/${newJobId}`); history(`/manage/jobs/${newJobId}`);
notification.success({ notification.success({
@@ -279,7 +264,7 @@ export function JobsDetailHeaderActions({
DuplicateJob({ DuplicateJob({
apolloClient: client, apolloClient: client,
jobId: job.id, jobId: job.id,
config: { defaultOpenStatus: bodyshop.md_ro_statuses.default_imported }, config: { defaultOpenStatus: bodyshop.md_ro_statuses.default_imported, timezone: bodyshop.timezone },
completionCallback: (newJobId) => { completionCallback: (newJobId) => {
history(`/manage/jobs/${newJobId}`); history(`/manage/jobs/${newJobId}`);
notification.success({ notification.success({
@@ -476,6 +461,11 @@ export function JobsDetailHeaderActions({
}; };
const handleVoidJob = async () => { const handleVoidJob = async () => {
if (!canVoidJob) {
notification.error({ title: t("general.messages.rbacunauth") });
return;
}
//delete the job. //delete the job.
const result = await voidJob({ const result = await voidJob({
variables: { variables: {
@@ -964,26 +954,26 @@ export function JobsDetailHeaderActions({
{ {
key: "duplicate", key: "duplicate",
id: "job-actions-duplicate", id: "job-actions-duplicate",
label: renderPopconfirmMenuLabel({ label: t("menus.jobsactions.duplicate"),
key: "confirm-duplicate", onClick: () =>
text: t("menus.jobsactions.duplicate"), openConfirmFromMenu({
title: t("jobs.labels.duplicateconfirm"), title: t("jobs.labels.duplicateconfirm"),
okText: t("general.labels.yes"), okText: t("general.labels.yes"),
cancelText: t("general.labels.no"), cancelText: t("general.labels.no"),
onConfirm: handleDuplicate onOk: handleDuplicate
}) })
}, },
{ {
key: "duplicatenolines", key: "duplicatenolines",
id: "job-actions-duplicatenolines", id: "job-actions-duplicatenolines",
label: renderPopconfirmMenuLabel({ label: t("menus.jobsactions.duplicatenolines"),
key: "confirm-duplicate-nolines", onClick: () =>
text: t("menus.jobsactions.duplicatenolines"), openConfirmFromMenu({
title: t("jobs.labels.duplicateconfirm"), title: t("jobs.labels.duplicateconfirm"),
okText: t("general.labels.yes"), okText: t("general.labels.yes"),
cancelText: t("general.labels.no"), cancelText: t("general.labels.no"),
onConfirm: handleDuplicateConfirm onOk: handleDuplicateConfirm
}) })
} }
] ]
}, },
@@ -1156,26 +1146,25 @@ export function JobsDetailHeaderActions({
menuItems.push({ menuItems.push({
key: "deletejob", key: "deletejob",
id: "job-actions-deletejob", id: "job-actions-deletejob",
label: label: t("menus.jobsactions.deletejob"),
jobWatchersCount === 0 onClick: () => {
? renderPopconfirmMenuLabel({ if (jobWatchersCount === 0) {
key: "confirm-deletejob", openConfirmFromMenu({
text: t("menus.jobsactions.deletejob"), title: t("jobs.labels.deleteconfirm"),
title: t("jobs.labels.deleteconfirm"), okText: t("general.labels.yes"),
okText: t("general.labels.yes"), cancelText: t("general.labels.no"),
cancelText: t("general.labels.no"), onOk: handleDeleteJob
onConfirm: handleDeleteJob });
}) } else {
: renderPopconfirmMenuLabel({ // informational "OK only"
key: "confirm-deletejob-watchers", openConfirmFromMenu({
text: t("menus.jobsactions.deletejob"), variant: "info",
title: t("jobs.labels.deletewatchers"), title: t("jobs.labels.deletewatchers"),
showCancel: false, okText: t("general.actions.ok"),
closeDropdownOnConfirm: false, // <-- FIX: keep dropdown mounted so Popconfirm can close cleanly showCancel: false
onConfirm: () => { });
// informational confirm only }
} }
})
}); });
} }
@@ -1188,22 +1177,18 @@ export function JobsDetailHeaderActions({
label: t("appointments.labels.manualevent") label: t("appointments.labels.manualevent")
}); });
if (!jobRO && job.converted) { if (!jobRO && job.converted && canVoidJob) {
menuItems.push({ menuItems.push({
key: "voidjob", key: "voidjob",
id: "job-actions-voidjob", id: "job-actions-voidjob",
label: ( label: t("menus.jobsactions.void"),
<RbacWrapper action="jobs:void" noauth> onClick: () =>
{renderPopconfirmMenuLabel({ openConfirmFromMenu({
key: "confirm-voidjob", title: t("jobs.labels.voidjob"),
text: t("menus.jobsactions.void"), okText: t("general.labels.yes"),
title: t("jobs.labels.voidjob"), cancelText: t("general.labels.no"),
okText: t("general.labels.yes"), onOk: handleVoidJob
cancelText: t("general.labels.no"), })
onConfirm: handleVoidJob
})}
</RbacWrapper>
)
}); });
} }
@@ -1235,6 +1220,7 @@ export function JobsDetailHeaderActions({
return ( return (
<> <>
{modalContextHolder}
<Modal <Modal
title={t("menus.jobsactions.cancelallappointments")} title={t("menus.jobsactions.cancelallappointments")}
open={isCancelScheduleModalVisible} open={isCancelScheduleModalVisible}

View File

@@ -15,7 +15,7 @@ export default async function DuplicateJob({
}) { }) {
logImEXEvent("job_duplicate"); logImEXEvent("job_duplicate");
const { defaultOpenStatus } = config; const { defaultOpenStatus, timezone } = config;
//get a list of all fields on the job //get a list of all fields on the job
const res = await apolloClient.query({ const res = await apolloClient.query({
query: QUERY_JOB_FOR_DUPE, query: QUERY_JOB_FOR_DUPE,
@@ -31,9 +31,12 @@ export default async function DuplicateJob({
delete existingJob.updatedat; delete existingJob.updatedat;
delete existingJob.cieca_stl; delete existingJob.cieca_stl;
delete existingJob.cieca_ttl; delete existingJob.cieca_ttl;
!keepJobLines && delete existingJob.clm_total;
const newJob = { const newJob = {
...existingJob, ...existingJob,
date_estimated: dayjs().tz(timezone, false).format("YYYY-MM-DD"),
date_open: dayjs(),
status: defaultOpenStatus status: defaultOpenStatus
}; };
@@ -70,7 +73,7 @@ export default async function DuplicateJob({
export async function CreateIouForJob({ apolloClient, jobId, config, jobLinesToKeep, currentUser }) { export async function CreateIouForJob({ apolloClient, jobId, config, jobLinesToKeep, currentUser }) {
logImEXEvent("job_create_iou"); logImEXEvent("job_create_iou");
const { status } = config; const { status, timezone } = config;
//get a list of all fields on the job //get a list of all fields on the job
const res = await apolloClient.query({ const res = await apolloClient.query({
query: QUERY_JOB_FOR_DUPE, query: QUERY_JOB_FOR_DUPE,
@@ -88,10 +91,10 @@ export async function CreateIouForJob({ apolloClient, jobId, config, jobLinesToK
const newJob = { const newJob = {
...existingJob, ...existingJob,
converted: true, converted: true,
status: status, status: status,
iouparent: jobId, iouparent: jobId,
date_estimated: dayjs().tz(timezone, false).format("YYYY-MM-DD"),
date_open: dayjs(), date_open: dayjs(),
audit_trails: { audit_trails: {
data: [ data: [

View File

@@ -143,7 +143,7 @@ export function JobsDetailHeader({ job, bodyshop, disabled, insertAuditTrail, is
label={t("jobs.fields.comment")} label={t("jobs.fields.comment")}
styles={{ value: { overflow: "hidden", textOverflow: "ellipsis" } }} styles={{ value: { overflow: "hidden", textOverflow: "ellipsis" } }}
> >
<ProductionListColumnComment record={job} /> <ProductionListColumnComment record={job} usePortal={true} />
</DataLabel> </DataLabel>
{!isPartsEntry && <DataLabel label={t("jobs.fields.ins_co_nm_short")}>{job.ins_co_nm}</DataLabel>} {!isPartsEntry && <DataLabel label={t("jobs.fields.ins_co_nm_short")}>{job.ins_co_nm}</DataLabel>}
<DataLabel label={t("jobs.fields.clm_no")}>{job.clm_no}</DataLabel> <DataLabel label={t("jobs.fields.clm_no")}>{job.clm_no}</DataLabel>
@@ -176,7 +176,7 @@ export function JobsDetailHeader({ job, bodyshop, disabled, insertAuditTrail, is
</DataLabel> </DataLabel>
)} )}
<DataLabel label={t("jobs.fields.production_vars.note")}> <DataLabel label={t("jobs.fields.production_vars.note")}>
<ProductionListColumnProductionNote record={job} /> <ProductionListColumnProductionNote record={job} usePortal={true} />
</DataLabel> </DataLabel>
<DataLabel label={t("jobs.fields.estimate_sent_approval")}> <DataLabel label={t("jobs.fields.estimate_sent_approval")}>
<Space> <Space>

View File

@@ -132,7 +132,13 @@ export function LaborAllocationsAdjustmentEdit({
); );
return ( return (
<Popover open={open} onOpenChange={(vis) => setOpen(vis)} content={overlay} trigger="click"> <Popover
getPopupContainer={(trigger) => trigger?.parentElement || document.body}
open={open}
onOpenChange={(vis) => setOpen(vis)}
content={overlay}
trigger="click"
>
{children} {children}
</Popover> </Popover>
); );

View File

@@ -56,7 +56,6 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount,
where: whereClause where: whereClause
}, },
fetchPolicy: "cache-and-network", fetchPolicy: "cache-and-network",
notifyOnNetworkStatusChange: true,
errorPolicy: "all", errorPolicy: "all",
pollInterval: isConnected ? 0 : day.duration(NOTIFICATION_POLL_INTERVAL_SECONDS, "seconds").asMilliseconds(), pollInterval: isConnected ? 0 : day.duration(NOTIFICATION_POLL_INTERVAL_SECONDS, "seconds").asMilliseconds(),
skip: skipQuery skip: skipQuery

View File

@@ -48,6 +48,7 @@ export default function OwnerFindModalContainer({
value={searchText} value={searchText}
onChange={(e) => setSearchText(e.target.value)} onChange={(e) => setSearchText(e.target.value)}
onSearch={(val) => callSearchowners({ variables: { search: val.trim() } })} onSearch={(val) => callSearchowners({ variables: { search: val.trim() } })}
enterButton
/> />
<OwnerFindModalComponent <OwnerFindModalComponent
selectedOwner={selectedOwner} selectedOwner={selectedOwner}

View File

@@ -2,6 +2,7 @@ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { store } from "../../redux/store"; import { store } from "../../redux/store";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import { Tooltip } from "antd";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop bodyshop: selectBodyshop
@@ -11,15 +12,27 @@ const mapDispatchToProps = () => ({
}); });
export default connect(mapStateToProps, mapDispatchToProps)(OwnerNameDisplay); export default connect(mapStateToProps, mapDispatchToProps)(OwnerNameDisplay);
export function OwnerNameDisplay({ bodyshop, ownerObject }) { export function OwnerNameDisplay({ bodyshop, ownerObject, withToolTip = false }) {
const emptyTest = ownerObject?.ownr_fn + ownerObject?.ownr_ln + ownerObject?.ownr_co_nm; const emptyTest = ownerObject?.ownr_fn + ownerObject?.ownr_ln + ownerObject?.ownr_co_nm;
if (!emptyTest || emptyTest === "null" || emptyTest.trim() === "") return "N/A"; if (!emptyTest || emptyTest === "null" || emptyTest.trim() === "") return "N/A";
if (bodyshop.last_name_first) let returnString;
return `${ownerObject?.ownr_ln || ""}, ${ownerObject?.ownr_fn || ""} ${ownerObject?.ownr_co_nm || ""}`.trim(); if (bodyshop.last_name_first) {
returnString =
return `${ownerObject?.ownr_fn || ""} ${ownerObject?.ownr_ln || ""} ${ownerObject.ownr_co_nm || ""}`.trim(); `${ownerObject?.ownr_ln || ""}, ${ownerObject?.ownr_fn || ""} ${ownerObject?.ownr_co_nm || ""}`.trim();
} else {
returnString = `${ownerObject?.ownr_fn || ""} ${ownerObject?.ownr_ln || ""} ${ownerObject.ownr_co_nm || ""}`.trim();
}
if (withToolTip) {
return (
<Tooltip title={returnString} mouseEnterDelay={0.5}>
{returnString}
</Tooltip>
);
} else {
return returnString;
}
} }
export function OwnerNameDisplayFunction(ownerObject, forceFirstLast = false) { export function OwnerNameDisplayFunction(ownerObject, forceFirstLast = false) {

View File

@@ -16,9 +16,10 @@ const OwnerSearchSelect = ({ value, onChange, onBlur, disabled, ref }) => {
SEARCH_OWNERS_BY_ID_FOR_AUTOCOMPLETE SEARCH_OWNERS_BY_ID_FOR_AUTOCOMPLETE
); );
const executeSearch = (v) => { const executeSearch = (variables) => {
if (v && v.variables?.search !== "" && v.variables.search.length >= 2) callSearch({ variables: v.variables }); if (variables?.search !== "" && variables?.search?.length >= 2) callSearch({ variables });
}; };
const debouncedExecuteSearch = _.debounce(executeSearch, 500); const debouncedExecuteSearch = _.debounce(executeSearch, 500);
const handleSearch = (value) => { const handleSearch = (value) => {

View File

@@ -101,6 +101,7 @@ export function PartDispatchTableComponent({ bodyshop, job, billsQuery }) {
e.preventDefault(); e.preventDefault();
setSearchText(e.target.value); setSearchText(e.target.value);
}} }}
enterButton
/> />
</Space> </Space>
} }

View File

@@ -295,6 +295,7 @@ export function PartsOrderListTableComponent({
e.preventDefault(); e.preventDefault();
setSearchText(e.target.value); setSearchText(e.target.value);
}} }}
enterButton
/> />
</Space> </Space>
} }

View File

@@ -1,94 +1,121 @@
import { DownOutlined } from "@ant-design/icons"; import { DownOutlined } from "@ant-design/icons";
import { Dropdown, InputNumber, Space } from "antd"; import { Button, Divider, Dropdown, InputNumber, Space, theme } from "antd";
import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { logImEXEvent } from "../../firebase/firebase.utils"; import { logImEXEvent } from "../../firebase/firebase.utils";
const DISCOUNT_PRESETS = [5, 10, 15, 20, 25, 40];
export default function PartsOrderModalPriceChange({ form, field }) { export default function PartsOrderModalPriceChange({ form, field }) {
const { t } = useTranslation(); const { t } = useTranslation();
const menu = { const { token } = theme.useToken();
items: [
{
key: "5",
label: t("parts_orders.labels.discount", { percent: "5%" })
},
{
key: "10",
label: t("parts_orders.labels.discount", { percent: "10%" })
},
{
key: "15",
label: t("parts_orders.labels.discount", { percent: "15%" })
},
{
key: "20",
label: t("parts_orders.labels.discount", { percent: "20%" })
},
{
key: "25",
label: t("parts_orders.labels.discount", { percent: "25%" })
},
{
key: "40",
label: t("parts_orders.labels.discount", { percent: "40%" })
},
{
key: "custom",
label: (
<Space.Compact>
<InputNumber
onClick={(e) => e.stopPropagation()}
onKeyUp={(e) => {
if (e.key === "Enter") {
const values = form.getFieldsValue();
const { parts_order_lines } = values;
form.setFieldsValue({ const [open, setOpen] = useState(false);
parts_order_lines: { const [customPercent, setCustomPercent] = useState(0);
data: parts_order_lines.data.map((p, idx) => {
if (idx !== field.name) return p; const applyDiscountPercent = (percent) => {
console.log(p, e.target.value, (p.act_price || 0) * ((100 - (e.target.value || 0)) / 100)); const pct = Number(percent) || 0;
return {
...p, const values = form.getFieldsValue();
act_price: (p.act_price || 0) * ((100 - (e.target.value || 0)) / 100) const parts_order_lines = values?.parts_order_lines;
}; const data = Array.isArray(parts_order_lines?.data) ? parts_order_lines.data : [];
}) if (!data.length) return;
}
}); form.setFieldsValue({
e.target.value = 0; parts_order_lines: {
} data: data.map((p, idx) => {
}} if (idx !== field.name) return p;
min={0} return {
max={100} ...p,
/> act_price: (p.act_price || 0) * ((100 - pct) / 100)
<span style={{ padding: "0 11px", backgroundColor: "#fafafa", border: "1px solid #d9d9d9", borderLeft: 0 }}>%</span> };
</Space.Compact> })
)
} }
], });
};
const applyCustom = () => {
logImEXEvent("parts_order_manual_discount", {});
applyDiscountPercent(customPercent);
setCustomPercent(0);
setOpen(false);
};
const menu = {
// Kill the menu “card” styling so our wrapper becomes the single card.
style: {
background: "transparent",
boxShadow: "none"
},
items: DISCOUNT_PRESETS.map((pct) => ({
key: String(pct),
label: t("parts_orders.labels.discount", { percent: `${pct}%` })
})),
onClick: ({ key }) => { onClick: ({ key }) => {
logImEXEvent("parts_order_manual_discount", {}); logImEXEvent("parts_order_manual_discount", {});
if (key === "custom") return; applyDiscountPercent(key);
const values = form.getFieldsValue(); setOpen(false);
const { parts_order_lines } = values;
form.setFieldsValue({
parts_order_lines: {
data: parts_order_lines.data.map((p, idx) => {
if (idx !== field.name) return p;
return {
...p,
act_price: (p.act_price || 0) * ((100 - key) / 100)
};
})
}
});
} }
}; };
return ( return (
<Dropdown menu={menu} trigger="click"> <Dropdown
menu={menu}
trigger={["click"]}
open={open}
onOpenChange={(nextOpen) => setOpen(nextOpen)}
getPopupContainer={(triggerNode) => triggerNode?.parentElement ?? document.body}
popupRender={(menus) => (
<div
// This makes the whole dropdown (menu + footer) look like one panel in both light/dark.
style={{
background: token.colorBgElevated,
borderRadius: token.borderRadiusLG,
boxShadow: token.boxShadowSecondary,
overflow: "hidden",
minWidth: 180
}}
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
>
{menus}
<Divider style={{ margin: 0 }} />
<div style={{ padding: token.paddingXS }}>
<Space.Compact style={{ width: "100%" }}>
<InputNumber
value={customPercent}
min={0}
max={100}
precision={0}
controls={false}
style={{ width: "100%" }}
formatter={(v) => (v === null || v === undefined ? "" : `${v}%`)}
parser={(v) =>
String(v ?? "")
.replace("%", "")
.trim()
}
onChange={(v) => setCustomPercent(v ?? 0)}
onKeyDown={(e) => {
e.stopPropagation();
if (e.key === "Enter") {
e.preventDefault();
applyCustom();
}
}}
/>
<Button type="primary" onClick={applyCustom}>
{t("general.labels.apply")}
</Button>
</Space.Compact>
</div>
</div>
)}
>
<Space> <Space>
% % <DownOutlined />
<DownOutlined />
</Space> </Space>
</Dropdown> </Dropdown>
); );

View File

@@ -13,6 +13,13 @@ import PartsOrderModalPriceChange from "./parts-order-modal-price-change.compone
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx"; import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
import { selectIsPartsEntry } from "../../redux/application/application.selectors.js"; import { selectIsPartsEntry } from "../../redux/application/application.selectors.js";
const PriceInputWrapper = ({ value, onChange, form, field }) => (
<Space.Compact style={{ width: "100%" }}>
<PartsOrderModalPriceChange form={form} field={field} />
<CurrencyInput style={{ flex: 1 }} value={value} onChange={onChange} />
</Space.Compact>
);
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
isPartsEntry: selectIsPartsEntry isPartsEntry: selectIsPartsEntry
@@ -199,10 +206,7 @@ export function PartsOrderModalComponent({
key={`${index}act_price`} key={`${index}act_price`}
name={[field.name, "act_price"]} name={[field.name, "act_price"]}
> >
<Space.Compact style={{ width: "100%" }}> <PriceInputWrapper form={form} field={field} />
<PartsOrderModalPriceChange form={form} field={field} />
<CurrencyInput style={{ flex: 1 }} />
</Space.Compact>
</Form.Item> </Form.Item>
{isReturn && ( {isReturn && (
<Form.Item <Form.Item

View File

@@ -17,7 +17,6 @@ import AuditTrailMapping from "../../utils/AuditTrailMappings";
import { GenerateDocument } from "../../utils/RenderTemplate"; import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants"; import { TemplateList } from "../../utils/TemplateConstants";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import PartsOrderModalComponent from "./parts-order-modal.component"; import PartsOrderModalComponent from "./parts-order-modal.component";
import axios from "axios"; import axios from "axios";
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
@@ -66,7 +65,7 @@ export function PartsOrderModalContainer({
const sendTypeState = useState("e"); const sendTypeState = useState("e");
const sendType = sendTypeState[0]; const sendType = sendTypeState[0];
const { loading, error, data } = useQuery(QUERY_ALL_VENDORS_FOR_ORDER, { const { error, data } = useQuery(QUERY_ALL_VENDORS_FOR_ORDER, {
skip: !open, skip: !open,
variables: { jobId: jobId }, variables: { jobId: jobId },
fetchPolicy: "network-only", fetchPolicy: "network-only",
@@ -89,20 +88,11 @@ export function PartsOrderModalContainer({
return { return {
...p, ...p,
job_line_id: jobLineId job_line_id: jobLineId,
...(isReturn && { cm_received: false })
}; };
}); });
const missingIdx = forcedLines.findIndex((l) => !l?.job_line_id);
if (missingIdx !== -1) {
notification.error({
title: t("parts_orders.errors.creating"),
description: `Missing job_line_id for parts line #${missingIdx + 1}`
});
setSaving(false);
return;
}
let insertResult; let insertResult;
try { try {
insertResult = await insertPartOrder({ insertResult = await insertPartOrder({
@@ -371,6 +361,7 @@ export function PartsOrderModalContainer({
} }
}, [open, linesToOrder, form]); }, [open, linesToOrder, form]);
//This used to have a loading component spinner for the vendor data. With Apollo 4, the NetworkState isn't emitting correctly, so loading just gets set to true the second time, and no longer works as expected.
return ( return (
<Modal <Modal
open={open} open={open}
@@ -389,18 +380,14 @@ export function PartsOrderModalContainer({
> >
{error ? <AlertComponent title={error.message} type="error" /> : null} {error ? <AlertComponent title={error.message} type="error" /> : null}
<Form form={form} layout="vertical" autoComplete="no" onFinish={handleFinish} initialValues={initialValues}> <Form form={form} layout="vertical" autoComplete="no" onFinish={handleFinish} initialValues={initialValues}>
{loading ? ( <PartsOrderModalComponent
<LoadingSpinner /> form={form}
) : ( vendorList={data?.vendors || []}
<PartsOrderModalComponent sendTypeState={sendTypeState}
form={form} isReturn={isReturn}
vendorList={data?.vendors || []} preferredMake={data && data.jobs[0] && data.jobs[0].v_make_desc}
sendTypeState={sendTypeState} job={job}
isReturn={isReturn} />
preferredMake={data && data.jobs[0] && data.jobs[0].v_make_desc}
job={job}
/>
)}
</Form> </Form>
</Modal> </Modal>
); );

View File

@@ -1,6 +1,6 @@
import { SyncOutlined } from "@ant-design/icons"; import { SyncOutlined } from "@ant-design/icons";
import { useQuery } from "@apollo/client/react"; import { useQuery } from "@apollo/client/react";
import { Button, Card, Input, Space, Table } from "antd"; import { Button, Card, Checkbox, Input, Space, Table } from "antd";
import _ from "lodash"; import _ from "lodash";
import queryString from "query-string"; import queryString from "query-string";
import { useState } from "react"; import { useState } from "react";
@@ -31,6 +31,8 @@ export function PartsQueueListComponent({ bodyshop }) {
const { selected, sortcolumn, sortorder, statusFilters } = searchParams; const { selected, sortcolumn, sortorder, statusFilters } = searchParams;
const history = useNavigate(); const history = useNavigate();
const [filter, setFilter] = useLocalStorage("filter_parts_queue", null); const [filter, setFilter] = useLocalStorage("filter_parts_queue", null);
const [viewTimeStamp, setViewTimeStamp] = useLocalStorage("parts_queue_timestamps", false);
const [countsOnly, setCountsOnly] = useLocalStorage("parts_queue_counts_only", false);
const { loading, error, data, refetch } = useQuery(QUERY_PARTS_QUEUE, { const { loading, error, data, refetch } = useQuery(QUERY_PARTS_QUEUE, {
fetchPolicy: "network-only", fetchPolicy: "network-only",
@@ -92,6 +94,7 @@ export function PartsQueueListComponent({ bodyshop }) {
title: t("jobs.fields.ro_number"), title: t("jobs.fields.ro_number"),
dataIndex: "ro_number", dataIndex: "ro_number",
key: "ro_number", key: "ro_number",
width: "110px",
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number), sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
sortOrder: sortcolumn === "ro_number" && sortorder, sortOrder: sortcolumn === "ro_number" && sortorder,
@@ -103,16 +106,20 @@ export function PartsQueueListComponent({ bodyshop }) {
title: t("jobs.fields.owner"), title: t("jobs.fields.owner"),
dataIndex: "ownr_ln", dataIndex: "ownr_ln",
key: "ownr_ln", key: "ownr_ln",
width: "8%",
ellipsis: {
showTitle: true
},
sorter: (a, b) => alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)), sorter: (a, b) => alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
sortOrder: sortcolumn === "ownr_ln" && sortorder, sortOrder: sortcolumn === "ownr_ln" && sortorder,
render: (text, record) => { render: (text, record) => {
return record.ownerid ? ( return record.ownerid ? (
<Link to={"/manage/owners/" + record.ownerid}> <Link to={"/manage/owners/" + record.ownerid}>
<OwnerNameDisplay ownerObject={record} /> <OwnerNameDisplay ownerObject={record} withToolTip />
</Link> </Link>
) : ( ) : (
<span> <span>
<OwnerNameDisplay ownerObject={record} /> <OwnerNameDisplay ownerObject={record} withToolTip />
</span> </span>
); );
} }
@@ -187,7 +194,7 @@ export function PartsQueueListComponent({ bodyshop }) {
ellipsis: true, ellipsis: true,
sorter: (a, b) => dateSort(a.scheduled_in, b.scheduled_in), sorter: (a, b) => dateSort(a.scheduled_in, b.scheduled_in),
sortOrder: sortcolumn === "scheduled_in" && sortorder, sortOrder: sortcolumn === "scheduled_in" && sortorder,
render: (text, record) => <DateTimeFormatter>{record.scheduled_in}</DateTimeFormatter> render: (text, record) => <DateTimeFormatter hideTime={!viewTimeStamp}>{record.scheduled_in}</DateTimeFormatter>
}, },
{ {
title: t("jobs.fields.scheduled_completion"), title: t("jobs.fields.scheduled_completion"),
@@ -196,7 +203,9 @@ export function PartsQueueListComponent({ bodyshop }) {
ellipsis: true, ellipsis: true,
sorter: (a, b) => dateSort(a.scheduled_completion, b.scheduled_completion), sorter: (a, b) => dateSort(a.scheduled_completion, b.scheduled_completion),
sortOrder: sortcolumn === "scheduled_completion" && sortorder, sortOrder: sortcolumn === "scheduled_completion" && sortorder,
render: (text, record) => <DateTimeFormatter>{record.scheduled_completion}</DateTimeFormatter> render: (text, record) => (
<DateTimeFormatter hideTime={!viewTimeStamp}>{record.scheduled_completion}</DateTimeFormatter>
)
}, },
// { // {
// title: t("vehicles.fields.plate_no"), // title: t("vehicles.fields.plate_no"),
@@ -227,16 +236,23 @@ export function PartsQueueListComponent({ bodyshop }) {
title: t("jobs.fields.updated_at"), title: t("jobs.fields.updated_at"),
dataIndex: "updated_at", dataIndex: "updated_at",
key: "updated_at", key: "updated_at",
width: "110px",
sorter: (a, b) => dateSort(a.updated_at, b.updated_at), sorter: (a, b) => dateSort(a.updated_at, b.updated_at),
sortOrder: sortcolumn === "updated_at" && sortorder, sortOrder: sortcolumn === "updated_at" && sortorder,
render: (text, record) => <TimeAgoFormatter>{record.updated_at}</TimeAgoFormatter> render: (text, record) => <TimeAgoFormatter removeAgoString>{record.updated_at}</TimeAgoFormatter>
}, },
{ {
title: t("jobs.fields.partsstatus"), title: t("jobs.fields.partsstatus"),
dataIndex: "partsstatus", dataIndex: "partsstatus",
key: "partsstatus", key: "partsstatus",
width: countsOnly ? "180px" : "110px",
render: (text, record) => ( render: (text, record) => (
<JobPartsReceived parts={record.joblines_status} displayMode="full" popoverPlacement="topLeft" /> <JobPartsReceived
parts={record.joblines_status}
displayMode="full"
popoverPlacement="middle"
countsOnly={countsOnly}
/>
) )
}, },
{ {
@@ -249,6 +265,7 @@ export function PartsQueueListComponent({ bodyshop }) {
title: t("jobs.fields.queued_for_parts"), title: t("jobs.fields.queued_for_parts"),
dataIndex: "queued_for_parts", dataIndex: "queued_for_parts",
key: "queued_for_parts", key: "queued_for_parts",
width: "120px",
sorter: (a, b) => a.queued_for_parts - b.queued_for_parts, sorter: (a, b) => a.queued_for_parts - b.queued_for_parts,
sortOrder: sortcolumn === "queued_for_parts" && sortorder, sortOrder: sortcolumn === "queued_for_parts" && sortorder,
filteredValue: filter?.queued_for_parts || null, filteredValue: filter?.queued_for_parts || null,
@@ -269,11 +286,18 @@ export function PartsQueueListComponent({ bodyshop }) {
return ( return (
<Card <Card
title={t("titles.bc.parts-queue")}
extra={ extra={
<Space wrap> <Space wrap>
<Button onClick={() => refetch()}> <Button onClick={() => refetch()}>
<SyncOutlined /> <SyncOutlined />
</Button> </Button>
<Checkbox checked={countsOnly} onChange={(e) => setCountsOnly(e.target.checked)}>
{t("parts.labels.view_counts_only")}
</Checkbox>
<Checkbox checked={viewTimeStamp} onChange={(e) => setViewTimeStamp(e.target.checked)}>
{t("parts.labels.view_timestamps")}
</Checkbox>
<Input.Search <Input.Search
className="imex-table-header__search" className="imex-table-header__search"
placeholder={t("general.labels.search")} placeholder={t("general.labels.search")}
@@ -298,7 +322,7 @@ export function PartsQueueListComponent({ bodyshop }) {
rowKey="id" rowKey="id"
dataSource={jobs} dataSource={jobs}
style={{ height: "100%" }} style={{ height: "100%" }}
scroll={{ x: true }} //scroll={{ x: true }}
onChange={handleTableChange} onChange={handleTableChange}
rowSelection={{ rowSelection={{
onSelect: (record) => { onSelect: (record) => {

View File

@@ -33,7 +33,7 @@ export default function PaymentFormTotalPayments({ jobid }) {
{balance && ( {balance && (
<Statistic <Statistic
title={t("payments.labels.balance")} title={t("payments.labels.balance")}
styles={{ value: { color: balance.getAmount() !== 0 ? "red" : "green" } }} styles={{ content: { color: balance.getAmount() !== 0 ? "red" : "green" } }}
value={(balance && balance.toFormat()) || ""} value={(balance && balance.toFormat()) || ""}
/> />
)} )}

View File

@@ -129,6 +129,7 @@ function PhoneNumberConsentList({ bodyshop }) {
placeholder={t("general.labels.search")} placeholder={t("general.labels.search")}
onSearch={(value) => setSearch(value)} onSearch={(value) => setSearch(value)}
style={{ marginBottom: 16 }} style={{ marginBottom: 16 }}
enterButton
/> />
<Table <Table

View File

@@ -108,7 +108,7 @@ export function PrintCenterJobsLabels({ jobId }) {
</Card> </Card>
); );
return ( return (
<Popover content={content} open={isModalVisible}> <Popover content={content} open={isModalVisible} getPopupContainer={(trigger) => trigger.parentElement}>
<Button onClick={() => setIsModalVisible(true)}>{t("printcenter.jobs.labels.labels")}</Button> <Button onClick={() => setIsModalVisible(true)}>{t("printcenter.jobs.labels.labels")}</Button>
</Popover> </Popover>
); );

View File

@@ -45,6 +45,7 @@ export function ProductionBoardFilters({ bodyshop, filter, setFilter, loading })
setFilter({ ...filter, search: e.target.value }); setFilter({ ...filter, search: e.target.value });
logImEXEvent("visual_board_filter_search", { search: e.target.value }); logImEXEvent("visual_board_filter_search", { search: e.target.value });
}} }}
enterButton
/> />
<EmployeeSearchSelectComponent <EmployeeSearchSelectComponent
style={{ minWidth: "20rem" }} style={{ minWidth: "20rem" }}

View File

@@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { DragDropContext } from "../dnd/lib"; import { DragDropContext } from "../dnd/lib";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
@@ -7,6 +7,7 @@ import { PopoverWrapper } from "react-popopo";
import * as actions from "../../../../redux/trello/trello.actions.js"; import * as actions from "../../../../redux/trello/trello.actions.js";
import { BoardWrapper } from "../styles/Base.js"; import { BoardWrapper } from "../styles/Base.js";
import ProductionStatistics from "../../production-board-kanban.statistics.jsx"; import ProductionStatistics from "../../production-board-kanban.statistics.jsx";
import isEqual from "lodash/isEqual";
const useDragMap = () => { const useDragMap = () => {
const dragMapRef = useRef(new Map()); const dragMapRef = useRef(new Map());
@@ -47,8 +48,9 @@ const BoardContainer = ({
const dispatch = useDispatch(); const dispatch = useDispatch();
const currentReducerData = useSelector((state) => (state.trello.lanes ? state.trello : {})); const currentReducerData = useSelector((state) => (state.trello.lanes ? state.trello : {}));
const { setDragTime, getLastDragTime } = useDragMap(); const { setDragTime, getLastDragTime } = useDragMap();
const previousDataRef = useRef(null);
const wireEventBus = () => { const wireEventBus = useCallback(() => {
const eventBus = { const eventBus = {
publish: (event) => { publish: (event) => {
switch (event.type) { switch (event.type) {
@@ -68,14 +70,17 @@ const BoardContainer = ({
} }
}; };
eventBusHandle(eventBus); eventBusHandle(eventBus);
}; }, [dispatch, eventBusHandle]);
useEffect(() => { useEffect(() => {
dispatch(actions.loadBoard(data)); if (!isEqual(previousDataRef.current, data)) {
if (eventBusHandle) { previousDataRef.current = data;
wireEventBus(); dispatch(actions.loadBoard(data));
if (eventBusHandle) {
wireEventBus();
}
} }
}, [data, eventBusHandle, dispatch]); }, [data, wireEventBus, dispatch]);
useEffect(() => { useEffect(() => {
onDataChange(currentReducerData); onDataChange(currentReducerData);
@@ -153,12 +158,17 @@ const BoardContainer = ({
} }
}; };
const boardKey = useMemo(() => {
// React Compiler: Generate stable key from lane IDs
return currentReducerData.lanes?.map((l) => l.id).join("-") || "empty";
}, [currentReducerData.lanes]);
return ( return (
<div> <div>
<ProductionStatistics data={queryData} reducerData={currentReducerData} cardSettings={cardSettings} /> <ProductionStatistics data={queryData} reducerData={currentReducerData} cardSettings={cardSettings} />
<PopoverWrapper> <PopoverWrapper>
<BoardWrapper orientation={orientation}> <BoardWrapper orientation={orientation}>
<DragDropContext onDragEnd={onLaneDrag} onDragStart={onDragStart} contextId="production-board"> <DragDropContext key={boardKey} onDragEnd={onLaneDrag} onDragStart={onDragStart} contextId="production-board">
{currentReducerData.lanes.map((lane, index) => ( {currentReducerData.lanes.map((lane, index) => (
<Lane <Lane
key={lane.id} key={lane.id}

View File

@@ -1,14 +1,28 @@
import { useEffect, useMemo } from "react"; import { useEffect, useMemo, useRef } from "react";
import createRegistry from "./create-registry"; import createRegistry from "./create-registry";
export default function useRegistry() { export default function useRegistry() {
const registry = useMemo(createRegistry, []); const registry = useMemo(createRegistry, []);
const cleanupScheduledRef = useRef(false);
useEffect(() => { useEffect(() => {
// Cancel any scheduled cleanup when component mounts
// This handles React StrictMode double-mounting
cleanupScheduledRef.current = false;
return function unmount() { return function unmount() {
// Mark cleanup as scheduled
cleanupScheduledRef.current = true;
// clean up the registry to avoid any leaks // clean up the registry to avoid any leaks
// doing it after an animation frame so that other things unmounting // doing it after an animation frame so that other things unmounting
// can continue to interact with the registry // can continue to interact with the registry
requestAnimationFrame(registry.clean); requestAnimationFrame(() => {
// Only clean if still scheduled (not cancelled by remount)
if (cleanupScheduledRef.current) {
registry.clean();
}
});
}; };
}, [registry]); }, [registry]);
return registry; return registry;

View File

@@ -171,6 +171,7 @@ export default function useDroppablePublisher(args) {
} }
registry.droppable.unregister(entry); registry.droppable.unregister(entry);
}; };
// eslint-disable-next-line react-compiler/react-compiler
}, [callbacks, descriptor, dragStopped, entry, marshal, registry.droppable]); }, [callbacks, descriptor, dragStopped, entry, marshal, registry.droppable]);
// update is enabled with the marshal // update is enabled with the marshal

View File

@@ -63,10 +63,10 @@ const ProductionListColumnAlert = ({ id, productionVars, refetch, insertAuditTra
okText={t("general.labels.yes")} okText={t("general.labels.yes")}
cancelText={t("general.labels.no")} cancelText={t("general.labels.no")}
> >
<Button className="production-alert" icon={<ExclamationCircleFilled />} /> <Button className="production-alert" icon={<ExclamationCircleFilled />} style={{ cursor: "pointer" }} />
</Popconfirm> </Popconfirm>
) : ( ) : (
<Button className="muted-button" icon={<PlusCircleFilled />} onClick={handleAlertToggle} /> <Button className="muted-button" icon={<PlusCircleFilled />} onClick={handleAlertToggle} style={{ cursor: "pointer" }} />
); );
}; };

View File

@@ -48,7 +48,7 @@ export default function ProductionListColumnBodyPriority({ record }) {
return ( return (
<Dropdown menu={menu} trigger={["click"]}> <Dropdown menu={menu} trigger={["click"]}>
<div style={{ width: "100%", height: "19px" }}>{record.production_vars?.bodypriority}</div> <div style={{ width: "100%", height: "19px", cursor: "pointer" }}>{record.production_vars?.bodypriority}</div>
</Dropdown> </Dropdown>
); );
} }

View File

@@ -7,7 +7,7 @@ import { FaRegStickyNote } from "react-icons/fa";
import { UPDATE_JOB } from "../../graphql/jobs.queries"; import { UPDATE_JOB } from "../../graphql/jobs.queries";
import { logImEXEvent } from "../../firebase/firebase.utils"; import { logImEXEvent } from "../../firebase/firebase.utils";
export default function ProductionListColumnComment({ record }) { export default function ProductionListColumnComment({ record, usePortal = false }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [note, setNote] = useState(record.comment || ""); const [note, setNote] = useState(record.comment || "");
@@ -43,16 +43,20 @@ export default function ProductionListColumnComment({ record }) {
}; };
const content = ( const content = (
<div style={{ width: "30em" }} onMouseDown={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}> <div
style={{ width: "30em" }}
onClick={(e) => e.stopPropagation()}
onPointerDown={(e) => e.stopPropagation()}
>
<Input.TextArea <Input.TextArea
id={`job-comment-${record.id}`}
name="comment"
rows={5} rows={5}
value={note} value={note}
onChange={handleChange} onChange={handleChange}
autoFocus autoFocus
allowClear allowClear
style={{ marginBottom: "1em" }} style={{ marginBottom: "1em" }}
onMouseDown={(e) => e.stopPropagation()}
onClick={(e) => e.stopPropagation()}
/> />
<div> <div>
<Button onClick={handleSaveNote} type="primary"> <Button onClick={handleSaveNote} type="primary">
@@ -63,7 +67,15 @@ export default function ProductionListColumnComment({ record }) {
); );
return ( return (
<Popover onOpenChange={handleOpenChange} open={open} content={content} trigger="click" fresh> <Popover
onOpenChange={handleOpenChange}
open={open}
content={content}
trigger="click"
destroyOnHidden
styles={{ body: { padding: '12px' } }}
{...(usePortal ? { getPopupContainer: (trigger) => trigger.parentElement || document.body } : {})}
>
<div <div
style={{ style={{
width: "100%", width: "100%",

View File

@@ -64,6 +64,7 @@ const productionListColumnsData = ({ technician, state, activeStatuses, data, bo
ellipsis: true, ellipsis: true,
render: (text, record) => ( render: (text, record) => (
<div <div
style={{ cursor: "pointer" }}
onClick={() => { onClick={() => {
store.dispatch( store.dispatch(
setModalContext({ setModalContext({

View File

@@ -86,7 +86,8 @@ export default function ProductionListDate({ record, field, time, pastIndicator
<div <div
onClick={() => setOpen(true)} onClick={() => setOpen(true)}
style={{ style={{
height: "19px" height: "19px",
cursor: "pointer"
}} }}
className={className} className={className}
> >

View File

@@ -48,7 +48,7 @@ export default function ProductionListColumnDetailPriority({ record }) {
return ( return (
<Dropdown menu={menu} trigger={["click"]}> <Dropdown menu={menu} trigger={["click"]}>
<div style={{ width: "100%", height: "19px" }}>{record.production_vars?.detailpriority}</div> <div style={{ width: "100%", height: "19px", cursor: "pointer" }}>{record.production_vars?.detailpriority}</div>
</Dropdown> </Dropdown>
); );
} }

View File

@@ -35,8 +35,6 @@ export function ProductionListEmpAssignment({ insertAuditTrail, bodyshop, record
const result = await updateJob({ const result = await updateJob({
variables: { jobId: record.id, job: { [empAssignment]: employeeid } } variables: { jobId: record.id, job: { [empAssignment]: employeeid } }
// awaitRefetchQueries: true,
}); });
insertAuditTrail({ insertAuditTrail({
@@ -55,6 +53,7 @@ export function ProductionListEmpAssignment({ insertAuditTrail, bodyshop, record
await refetch(); await refetch();
setAssignment({ operation: null, employeeid: null });
setLoading(false); setLoading(false);
}; };
const handleRemove = async (operation) => { const handleRemove = async (operation) => {
@@ -84,6 +83,7 @@ export function ProductionListEmpAssignment({ insertAuditTrail, bodyshop, record
await refetch(); await refetch();
setAssignment({ operation: null, employeeid: null });
setLoading(false); setLoading(false);
}; };
@@ -94,29 +94,31 @@ export function ProductionListEmpAssignment({ insertAuditTrail, bodyshop, record
const [visibility, setVisibility] = useState(false); const [visibility, setVisibility] = useState(false);
const onChange = (e, option) => { const onChange = (e, option) => {
setAssignment({ ...assignment, employeeid: e, name: option.name }); setAssignment({ ...assignment, employeeid: e, name: option.label });
}; };
const employeeOptions = bodyshop.employees
.filter((emp) => emp.active)
.map((emp) => ({
value: emp.id,
label: `${emp.first_name} ${emp.last_name}`,
name: `${emp.first_name} ${emp.last_name}`
}));
const popContent = ( const popContent = (
<Row gutter={[16, 16]}> <Row gutter={[16, 16]}>
<Col span={24}> <Col span={24}>
<Select <Select
id="employeeSelector" id="employeeSelector"
showSearch={{ showSearch={{
optionFilterProp: "children", optionFilterProp: "label",
filterOption: (input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0 filterOption: (input, option) => option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}} }}
style={{ width: 200 }} style={{ width: 200 }}
value={assignment.employeeid}
onChange={onChange} onChange={onChange}
> options={employeeOptions}
{bodyshop.employees />
.filter((emp) => emp.active)
.map((emp) => (
<Select.Option value={emp.id} key={emp.id} name={`${emp.first_name} ${emp.last_name}`}>
{`${emp.first_name} ${emp.last_name}`}
</Select.Option>
))}
</Select>
</Col> </Col>
<Col span={24}> <Col span={24}>
<Space wrap> <Space wrap>
@@ -141,25 +143,25 @@ export function ProductionListEmpAssignment({ insertAuditTrail, bodyshop, record
if (record[type]) theEmployee = bodyshop.employees.find((e) => e.id === record[type]); if (record[type]) theEmployee = bodyshop.employees.find((e) => e.id === record[type]);
return ( return (
<Popover destroyOnHidden content={popContent} open={visibility}> <Spin spinning={loading}>
<Spin spinning={loading}> {record[type] ? (
{record[type] ? ( <div style={{ cursor: "pointer" }}>
<div> <span>{`${theEmployee?.first_name || ""} ${theEmployee?.last_name || ""}`}</span>
<span>{`${theEmployee?.first_name || ""} ${theEmployee?.last_name || ""}`}</span> <DeleteFilled style={iconStyle} onClick={() => handleRemove(type)} />
<DeleteFilled style={iconStyle} onClick={() => handleRemove(type)} /> </div>
</div> ) : (
) : ( <Popover destroyOnHidden content={popContent} open={visibility} trigger="click">
<PlusCircleFilled <PlusCircleFilled
style={iconStyle} style={{ ...iconStyle, cursor: "pointer" }}
className="muted-button" className="muted-button"
onClick={() => { onClick={() => {
setAssignment({ operation: type }); setAssignment({ operation: type, employeeid: null });
setVisibility(true); setVisibility(true);
}} }}
/> />
)} </Popover>
</Spin> )}
</Popover> </Spin>
); );
} }

View File

@@ -124,7 +124,8 @@ export function ProductionLastContacted({ currentUser, record }) {
<div <div
onClick={() => setOpen(true)} onClick={() => setOpen(true)}
style={{ style={{
height: "19px" height: "19px",
cursor: "pointer"
}} }}
> >
<DateFormatter bordered={false}>{record.date_last_contacted}</DateFormatter> <DateFormatter bordered={false}>{record.date_last_contacted}</DateFormatter>

View File

@@ -48,7 +48,7 @@ export default function ProductionListColumnPaintPriority({ record }) {
return ( return (
<Dropdown menu={menu} trigger={["click"]}> <Dropdown menu={menu} trigger={["click"]}>
<div style={{ width: "100%", height: "19px" }}>{record.production_vars?.paintpriority}</div> <div style={{ width: "100%", height: "19px", cursor: "pointer" }}>{record.production_vars?.paintpriority}</div>
</Dropdown> </Dropdown>
); );
} }

View File

@@ -16,7 +16,7 @@ const mapDispatchToProps = (dispatch) => ({
setNoteUpsertContext: (context) => dispatch(setModalContext({ context: context, modal: "noteUpsert" })) setNoteUpsertContext: (context) => dispatch(setModalContext({ context: context, modal: "noteUpsert" }))
}); });
function ProductionListColumnProductionNote({ record, setNoteUpsertContext }) { function ProductionListColumnProductionNote({ record, setNoteUpsertContext, usePortal = false }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [note, setNote] = useState(record.production_vars?.note || ""); const [note, setNote] = useState(record.production_vars?.note || "");
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
@@ -59,16 +59,20 @@ function ProductionListColumnProductionNote({ record, setNoteUpsertContext }) {
); );
const content = ( const content = (
<div style={{ width: "30em" }} onMouseDown={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}> <div
style={{ width: "30em" }}
onClick={(e) => e.stopPropagation()}
onPointerDown={(e) => e.stopPropagation()}
>
<Input.TextArea <Input.TextArea
id={`job-production-note-${record.id}`}
name="production_note"
rows={5} rows={5}
value={note} value={note}
onChange={handleChange} onChange={handleChange}
autoFocus autoFocus
allowClear allowClear
style={{ marginBottom: "1em" }} style={{ marginBottom: "1em" }}
onMouseDown={(e) => e.stopPropagation()}
onClick={(e) => e.stopPropagation()}
/> />
<Space> <Space>
<Button onClick={handleSaveNote} type="primary"> <Button onClick={handleSaveNote} type="primary">
@@ -92,7 +96,15 @@ function ProductionListColumnProductionNote({ record, setNoteUpsertContext }) {
); );
return ( return (
<Popover onOpenChange={handleOpenChange} open={open} content={content} trigger="click" fresh> <Popover
onOpenChange={handleOpenChange}
open={open}
content={content}
trigger="click"
destroyOnHidden
styles={{ body: { padding: '12px' } }}
{...(usePortal ? { getPopupContainer: (trigger) => trigger.parentElement || document.body } : {})}
>
<div <div
style={{ style={{
width: "100%", width: "100%",

View File

@@ -1,15 +1,19 @@
import { SyncOutlined } from "@ant-design/icons"; import { HolderOutlined, SyncOutlined } from "@ant-design/icons";
import { PageHeader } from "@ant-design/pro-layout"; import { PageHeader } from "@ant-design/pro-layout";
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import { Button, Dropdown, Input, Space, Statistic, Table } from "antd"; import { Button, Dropdown, Input, Space, Statistic, Table } from "antd";
import _ from "lodash"; import _ from "lodash";
import { useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import ReactDragListView from "react-drag-listview"; import { closestCenter, DndContext, DragOverlay, PointerSensor, useSensor, useSensors } from "@dnd-kit/core";
import { arrayMove, horizontalListSortingStrategy, SortableContext, useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { restrictToHorizontalAxis } from "@dnd-kit/modifiers";
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";
import { selectTechnician } from "../../redux/tech/tech.selectors"; import { selectTechnician } from "../../redux/tech/tech.selectors";
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 Prompt from "../../utils/prompt.js"; import Prompt from "../../utils/prompt.js";
import AlertComponent from "../alert/alert.component.jsx"; import AlertComponent from "../alert/alert.component.jsx";
import ProductionListColumnsAdd from "../production-list-columns/production-list-columns.add.component"; import ProductionListColumnsAdd from "../production-list-columns/production-list-columns.add.component";
@@ -23,12 +27,81 @@ import { logImEXEvent } from "../../firebase/firebase.utils.js";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
technician: selectTechnician, technician: selectTechnician,
currentUser: selectCurrentUser currentUser: selectCurrentUser,
isDarkMode: selectDarkMode
}); });
export function ProductionListTable({ loading, data, refetch, bodyshop, technician, currentUser }) { // Draggable header cell component - combines drag and resize
function DraggableHeaderCell(props) {
const { children, columnKey, onResize, width, ...restProps } = props;
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: columnKey,
disabled: !columnKey
});
const style = {
...restProps.style,
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.3 : 1,
userSelect: "none",
textAlign: "left"
};
// If no columnKey, render as regular header
if (!columnKey) {
return <ResizeableTitle {...props} />;
}
// Only apply drag listeners to elements with data-drag-handle attribute
const filteredListeners = listeners
? {
onPointerDown: (e) => {
// Only trigger drag if clicking on the drag handle
if (e.target.closest('[data-drag-handle="true"]')) {
listeners.onPointerDown?.(e);
}
}
}
: {};
// Combine drag functionality with resize
return (
<ResizeableTitle
{...restProps}
ref={setNodeRef}
style={style}
onResize={onResize}
width={width}
dragAttributes={attributes}
dragListeners={filteredListeners}
>
{children}
</ResizeableTitle>
);
}
export function ProductionListTable({ loading, data, refetch, bodyshop, technician, currentUser, isDarkMode }) {
const [searchText, setSearchText] = useState(""); const [searchText, setSearchText] = useState("");
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
// NEW: smoother resize
const [isResizing, setIsResizing] = useState(false);
const resizeRafRef = useRef(null);
const pendingResizeRef = useRef(null);
const [activeId, setActiveId] = useState(null);
const MIN_COL_WIDTH = 20;
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 1
}
})
);
const { const {
treatments: { Production_List_Status_Colors, Enhanced_Payroll } treatments: { Production_List_Status_Colors, Enhanced_Payroll }
} = useTreatmentsWithConfig({ } = useTreatmentsWithConfig({
@@ -36,8 +109,10 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
names: ["Production_List_Status_Colors", "Enhanced_Payroll"], names: ["Production_List_Status_Colors", "Enhanced_Payroll"],
splitKey: bodyshop.imexshopid splitKey: bodyshop.imexshopid
}); });
const assoc = bodyshop.associations.find((a) => a.useremail === currentUser.email); const assoc = bodyshop.associations.find((a) => a.useremail === currentUser.email);
const defaultView = assoc?.default_prod_list_view; const defaultView = assoc?.default_prod_list_view;
const initialStateRef = useRef( const initialStateRef = useRef(
(bodyshop.production_config && (bodyshop.production_config &&
bodyshop.production_config.find((p) => p.name === defaultView)?.columns.tableState) || bodyshop.production_config.find((p) => p.name === defaultView)?.columns.tableState) ||
@@ -46,6 +121,7 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
filteredInfo: { text: "" } filteredInfo: { text: "" }
} }
); );
const initialColumnsRef = useRef( const initialColumnsRef = useRef(
(initialStateRef.current && (initialStateRef.current &&
bodyshop?.production_config bodyshop?.production_config
@@ -66,14 +142,36 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
})) || })) ||
[] []
); );
const [state, setState] = useState(initialStateRef.current); const [state, setState] = useState(initialStateRef.current);
const [columns, setColumns] = useState(initialColumnsRef.current); const [columns, setColumns] = useState(initialColumnsRef.current);
const scrollX = useMemo(() => {
// keep scroll width aligned with the actual column widths so AntD doesn't clamp at a fixed floor
const sum = columns.reduce((acc, c) => acc + (c.width ?? 100), 0);
return Math.max(sum, 1);
}, [columns]);
const { t } = useTranslation(); const { t } = useTranslation();
const matchingColumnConfig = useMemo(() => { const matchingColumnConfig = useMemo(() => {
return bodyshop?.production_config?.find((p) => p.name === defaultView); return bodyshop?.production_config?.find((p) => p.name === defaultView);
}, [bodyshop.production_config, defaultView]); }, [bodyshop.production_config, defaultView]);
// NEW: cleanup RAF on unmount
useEffect(() => { useEffect(() => {
return () => {
if (resizeRafRef.current) cancelAnimationFrame(resizeRafRef.current);
};
}, []);
useEffect(() => {
// NEW: while resizing, dont regenerate columns
if (isResizing) return;
// NEW: bail early BEFORE expensive ProductionListColumns(...) call
if (!_.isEqual(initialColumnsRef.current, columns)) return;
const newColumns = const newColumns =
matchingColumnConfig?.columns.columnKeys.map((k) => { matchingColumnConfig?.columns.columnKeys.map((k) => {
return { return {
@@ -89,10 +187,8 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
width: k.width ?? 100 width: k.width ?? 100
}; };
}) || []; }) || [];
// Only update columns if they haven't been manually changed by the user
if (_.isEqual(initialColumnsRef.current, columns)) { setColumns(newColumns);
setColumns(newColumns);
}
}, [ }, [
matchingColumnConfig, matchingColumnConfig,
bodyshop, bodyshop,
@@ -102,7 +198,8 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
Production_List_Status_Colors, Production_List_Status_Colors,
refetch, refetch,
state, state,
columns columns,
isResizing
]); ]);
const handleTableChange = (pagination, filters, sorter) => { const handleTableChange = (pagination, filters, sorter) => {
@@ -118,17 +215,30 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
logImEXEvent("production_list_sort_filter", { pagination, filters, sorter }); logImEXEvent("production_list_sort_filter", { pagination, filters, sorter });
}; };
const onDragEnd = (fromIndex, toIndex) => { const onDragStart = ({ active }) => {
if (fromIndex === toIndex) return; setActiveId(active.id);
const columnsCopy = [...columns]; };
const [movedItem] = columnsCopy.splice(fromIndex, 1);
columnsCopy.splice(toIndex, 0, movedItem); const onDragEnd = ({ active, over }) => {
if (!_.isEqual(columnsCopy, columns)) { setActiveId(null);
setColumns(columnsCopy); if (!over || active.id === over.id) return;
setHasUnsavedChanges(true);
const oldIndex = columns.findIndex((col) => col.key === active.id);
const newIndex = columns.findIndex((col) => col.key === over.id);
if (oldIndex !== -1 && newIndex !== -1) {
const newColumns = arrayMove(columns, oldIndex, newIndex);
if (!_.isEqual(newColumns, columns)) {
setColumns(newColumns);
setHasUnsavedChanges(true);
}
} }
}; };
const onDragCancel = () => {
setActiveId(null);
};
const removeColumn = (e) => { const removeColumn = (e) => {
const { key } = e; const { key } = e;
const newColumns = columns.filter((i) => i.key !== key); const newColumns = columns.filter((i) => i.key !== key);
@@ -139,19 +249,55 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
logImEXEvent("production_list_remove_column", { key }); logImEXEvent("production_list_remove_column", { key });
}; };
const handleResize = // NEW: commit widths via rAF (less jank)
(index) => const applyColumnWidth = useCallback((columnKey, width) => {
(e, { size }) => { const nextWidth = Math.max(MIN_COL_WIDTH, Math.round(width));
const nextColumns = [...columns]; setColumns((prev) => {
nextColumns[index] = { const idx = prev.findIndex((c) => c.key === columnKey);
...nextColumns[index], if (idx === -1) return prev;
width: size.width
}; const currentWidth = prev[idx].width ?? 100;
if (!_.isEqual(nextColumns, columns)) { if (currentWidth === nextWidth) return prev;
setColumns(nextColumns);
const next = prev.slice();
next[idx] = { ...next[idx], width: nextWidth };
return next;
});
}, []);
const handleResize = useCallback(
(columnKey) =>
(e, { size }) => {
pendingResizeRef.current = { columnKey, width: size.width };
if (resizeRafRef.current) return;
resizeRafRef.current = requestAnimationFrame(() => {
resizeRafRef.current = null;
const pending = pendingResizeRef.current;
if (!pending) return;
applyColumnWidth(pending.columnKey, pending.width);
});
},
[applyColumnWidth]
);
const handleResizeStart = useCallback(() => {
setIsResizing(true);
}, []);
const handleResizeStop = useCallback(
(columnKey) =>
(e, { size }) => {
setIsResizing(false);
// Ensure final width is committed
applyColumnWidth(columnKey, size.width);
setHasUnsavedChanges(true); setHasUnsavedChanges(true);
} logImEXEvent("production_list_resize_column", { key: columnKey, width: size.width });
}; },
[applyColumnWidth]
);
const addColumn = (newColumn) => { const addColumn = (newColumn) => {
const updatedColumns = [...columns, newColumn]; const updatedColumns = [...columns, newColumn];
@@ -163,19 +309,53 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
}; };
const headerItem = (col) => { const headerItem = (col) => {
const menu = { const menu = { onClick: removeColumn, items: [{ key: col.key, label: t("production.actions.removecolumn") }] };
onClick: removeColumn,
items: [
{
key: col.key,
label: t("production.actions.removecolumn")
}
]
};
return ( return (
<Dropdown className="prod-header-dropdown" menu={menu} trigger={["contextMenu"]}> <div
<span>{col.title}</span> style={{
</Dropdown> display: "flex",
alignItems: "left",
width: "100%",
userSelect: "none",
minWidth: 0 // critical: allow the flex row to shrink
}}
>
<span
className="drag-handle-trigger"
data-drag-handle="true"
style={{
marginRight: 8,
color: "#999",
cursor: "grab",
padding: 4,
display: "inline-flex",
alignItems: "left",
userSelect: "none",
flex: "0 0 auto"
}}
title="Drag to reorder column"
>
<HolderOutlined />
</span>
<Dropdown className="prod-header-dropdown" menu={menu} trigger={["contextMenu"]}>
<span
style={{
flex: "1 1 auto",
minWidth: 0, // critical: allow text to shrink
overflow: "hidden", // clip
textOverflow: "ellipsis", // show …
whiteSpace: "nowrap", // keep single line
cursor: "default",
userSelect: "none",
display: "block"
}}
>
{col.title}
</span>
</Dropdown>
</div>
); );
}; };
@@ -274,6 +454,9 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
onSave={() => { onSave={() => {
setHasUnsavedChanges(false); setHasUnsavedChanges(false);
initialStateRef.current = state; initialStateRef.current = state;
// NEW: after saving, treat current columns as the baseline
initialColumnsRef.current = columns;
}} }}
/> />
<Input <Input
@@ -286,60 +469,104 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
} }
/> />
<ProductionListDetail jobs={dataSource} /> <ProductionListDetail jobs={dataSource} />
<ReactDragListView.DragColumn onDragEnd={onDragEnd} nodeSelector="th" handleSelector=".prod-header-dropdown"> <DndContext
<Table sensors={sensors}
sticky onDragStart={onDragStart}
pagination={false} onDragEnd={onDragEnd}
size="small" onDragCancel={onDragCancel}
{...(Production_List_Status_Colors.treatment === "on" && { collisionDetection={closestCenter}
onRow: (record, index) => { modifiers={[restrictToHorizontalAxis]}
if (!bodyshop.md_ro_statuses.production_colors) return null; >
const color = bodyshop.md_ro_statuses.production_colors.find((x) => x.status === record.status); <SortableContext items={columns.map((col) => col.key)} strategy={horizontalListSortingStrategy}>
if (!color) { <Table
if (index % 2 === 0) sticky
tableLayout="fixed"
className="prod-list-table"
pagination={false}
size="small"
{...(Production_List_Status_Colors.treatment === "on" &&
!isResizing && {
onRow: (record, index) => {
if (!bodyshop.md_ro_statuses.production_colors) return null;
const color = bodyshop.md_ro_statuses.production_colors.find((x) => x.status === record.status);
if (!color) {
if (index % 2 === 0)
return {
style: {
backgroundColor: "var(--table-row-even-bg)"
}
};
return null;
}
return { return {
className: "rowWithColor",
style: { style: {
backgroundColor: "var(--table-row-even-bg)" "--bgColor": color.color
? `rgba(${color.color.r},${color.color.g},${color.color.b},${color.color.a || 1})`
: "var(--status-row-bg-fallback)"
} }
}; };
return null;
}
return {
className: "rowWithColor",
style: {
"--bgColor": color.color
? `rgba(${color.color.r},${color.color.g},${color.color.b},${color.color.a || 1})`
: "var(--status-row-bg-fallback)"
} }
})}
components={{
header: {
cell: DraggableHeaderCell
}
}}
columns={columns.map((c) => {
return {
...c,
filteredValue: state.filteredInfo[c.key] || null,
sortOrder: state.sortedInfo.columnKey === c.key && state.sortedInfo.order,
title: headerItem(c),
ellipsis: true,
width: c.width ?? 100,
onHeaderCell: (column) => ({
columnKey: column.key,
width: column.width,
onResize: handleResize(column.key),
onResizeStart: handleResizeStart,
onResizeStop: handleResizeStop(column.key)
})
}; };
} })}
})} rowKey="id"
components={{ loading={loading}
header: { dataSource={dataSource}
cell: ResizeableTitle scroll={{ x: scrollX }}
} onChange={handleTableChange}
}} />
columns={columns.map((c, index) => { </SortableContext>
return {
...c, <DragOverlay dropAnimation={null}>
filteredValue: state.filteredInfo[c.key] || null, {activeId ? (
sortOrder: state.sortedInfo.columnKey === c.key && state.sortedInfo.order, <div
title: headerItem(c), style={{
ellipsis: true, backgroundColor: isDarkMode ? "#141414" : "white",
width: c.width ?? 100, color: isDarkMode ? "white" : "#000",
onHeaderCell: (column) => ({ border: `2px solid ${isDarkMode ? "#177ddc" : "#1890ff"}`,
width: column.width, borderRadius: "4px",
onResize: handleResize(index) padding: "12px 16px",
}) boxShadow: "0 4px 12px rgba(0, 0, 0, 0.25)",
}; cursor: "grabbing",
})} display: "flex",
rowKey="id" alignItems: "center",
loading={loading} fontWeight: 500,
dataSource={dataSource} minWidth: "120px"
scroll={{ x: 1000 }} }}
onChange={handleTableChange} >
/> <HolderOutlined style={{ marginRight: "8px", color: isDarkMode ? "white" : "#000", fontSize: "16px" }} />
</ReactDragListView.DragColumn> <span>
{(() => {
const col = columns.find((c) => c.key === activeId);
const title = typeof col?.title === "string" ? col.title : col?.dataIndex || col?.key || "Column";
return title;
})()}
</span>
</div>
) : null}
</DragOverlay>
</DndContext>
</div> </div>
); );
} }

View File

@@ -1,28 +1,37 @@
import { forwardRef } from "react";
import { Resizable } from "react-resizable"; import { Resizable } from "react-resizable";
import "react-resizable/css/styles.css";
export default function ResizableComponent(props) { const ResizableComponent = forwardRef((props, ref) => {
const { onResize, width, ...restProps } = props; const { onResize, onResizeStart, onResizeStop, width, dragAttributes, dragListeners, ...restProps } = props;
if (!width) { if (!width) {
return <th {...restProps} />; return <th ref={ref} {...restProps} {...(dragAttributes || {})} {...(dragListeners || {})} />;
} }
return ( return (
<Resizable <Resizable
width={width || 200} width={width}
height={0} height={0}
onResize={onResize} onResize={onResize}
onResizeStart={onResizeStart}
onResizeStop={onResizeStop}
draggableOpts={{ enableUserSelectHack: false }} draggableOpts={{ enableUserSelectHack: false }}
handle={ resizeHandles={["e"]}
axis="x"
handle={(axis, handleRef) => (
<span <span
className="react-resizable-handle" ref={handleRef}
onClick={(e) => { className={`react-resizable-handle react-resizable-handle-${axis}`}
e.stopPropagation(); onClick={(e) => e.stopPropagation()}
}}
/> />
} )}
> >
<th {...restProps} /> <th ref={ref} {...restProps} {...(dragAttributes || {})} {...(dragListeners || {})} />
</Resizable> </Resizable>
); );
} });
ResizableComponent.displayName = "ResizableComponent";
export default ResizableComponent;

View File

@@ -44,6 +44,7 @@ export default function ProfileShopsComponent({ loading, data, updateActiveShop
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
allowClear allowClear
placeholder={t("general.labels.search")} placeholder={t("general.labels.search")}
enterButton
/> />
} }
> >

View File

@@ -145,7 +145,7 @@ export function ReportCenterModalComponent({ reportCenterModal, bodyshop }) {
return ( return (
<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} enterButton />
<Form.Item name="defaultSorters" hidden> <Form.Item name="defaultSorters" hidden>
<Input type="hidden" /> <Input type="hidden" />
</Form.Item> </Form.Item>

View File

@@ -3,8 +3,7 @@ import { Button, Form, Input, InputNumber, Select, Space, Switch } from "antd";
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";
import { selectBodyshop } from "../../redux/user/user.selectors"; import FeatureWrapper from "../feature-wrapper/feature-wrapper.component";
import FeatureWrapper, { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component"; import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import FormItemEmail from "../form-items-formatted/email-form-item.component"; import FormItemEmail from "../form-items-formatted/email-form-item.component";
import PhoneFormItem, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component"; import PhoneFormItem, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component";
@@ -12,15 +11,13 @@ import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.c
import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import LayoutFormRow from "../layout-form-row/layout-form-row.component";
const timeZonesList = Intl.supportedValuesOf("timeZone"); const timeZonesList = Intl.supportedValuesOf("timeZone");
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({});
bodyshop: selectBodyshop
});
const mapDispatchToProps = () => ({ const mapDispatchToProps = () => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
}); });
export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoGeneral); export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoGeneral);
export function ShopInfoGeneral({ form, bodyshop }) { export function ShopInfoGeneral({ form }) {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
@@ -378,34 +375,6 @@ export function ShopInfoGeneral({ form, bodyshop }) {
> >
<Select mode="tags" /> <Select mode="tags" />
</Form.Item>, </Form.Item>,
...(HasFeatureAccess({ featureName: "timetickets", bodyshop })
? [
<Form.Item
key="tt_allow_post_to_invoiced"
name={["tt_allow_post_to_invoiced"]}
label={t("bodyshop.fields.tt_allow_post_to_invoiced")}
valuePropName="checked"
>
<Switch />
</Form.Item>,
<Form.Item
key="tt_enforce_hours_for_tech_console"
name={["tt_enforce_hours_for_tech_console"]}
label={t("bodyshop.fields.tt_enforce_hours_for_tech_console")}
valuePropName="checked"
>
<Switch />
</Form.Item>,
<Form.Item
key="bill_allow_post_to_closed"
name={["bill_allow_post_to_closed"]}
label={t("bodyshop.fields.bill_allow_post_to_closed")}
valuePropName="checked"
>
<Switch />
</Form.Item>
]
: []),
<Form.Item <Form.Item
key="md_ded_notes" key="md_ded_notes"
name={["md_ded_notes"]} name={["md_ded_notes"]}

View File

@@ -313,6 +313,38 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
> >
<Select mode="tags" /> <Select mode="tags" />
</Form.Item>, </Form.Item>,
...(HasFeatureAccess({ featureName: "timetickets", bodyshop })
? [
<Form.Item
key="tt_allow_post_to_invoiced"
name={["tt_allow_post_to_invoiced"]}
label={t("bodyshop.fields.tt_allow_post_to_invoiced")}
valuePropName="checked"
>
<Switch />
</Form.Item>,
<Form.Item
key="tt_enforce_hours_for_tech_console"
name={["tt_enforce_hours_for_tech_console"]}
label={t("bodyshop.fields.tt_enforce_hours_for_tech_console")}
valuePropName="checked"
>
<Switch />
</Form.Item>
]
: []),
...(HasFeatureAccess({ featureName: "bills", bodyshop })
? [
<Form.Item
key="bill_allow_post_to_closed"
name={["bill_allow_post_to_closed"]}
label={t("bodyshop.fields.bill_allow_post_to_closed")}
valuePropName="checked"
>
<Switch />
</Form.Item>
]
: []),
...(HasFeatureAccess({ featureName: "export", bodyshop }) ...(HasFeatureAccess({ featureName: "export", bodyshop })
? [ ? [
...(ClosingPeriod.treatment === "on" ...(ClosingPeriod.treatment === "on"

View File

@@ -18,9 +18,10 @@ const VehicleSearchSelect = ({ value, onChange, onBlur, disabled, ref }) => {
SEARCH_VEHICLES_BY_ID_FOR_AUTOCOMPLETE SEARCH_VEHICLES_BY_ID_FOR_AUTOCOMPLETE
); );
const executeSearch = (v) => { const executeSearch = (variables) => {
if (v && v.variables?.search !== "" && v.variables.search.length >= 2) callSearch({ variables: v.variables }); if (variables?.search !== "" && variables?.search?.length >= 2) callSearch({ variables });
}; };
const debouncedExecuteSearch = _.debounce(executeSearch, 500); const debouncedExecuteSearch = _.debounce(executeSearch, 500);
const handleSearch = (value) => { const handleSearch = (value) => {

View File

@@ -356,7 +356,10 @@ export const MUTATION_BACKORDER_PART_LINE = gql`
export const QUERY_UNRECEIVED_LINES = gql` export const QUERY_UNRECEIVED_LINES = gql`
query QUERY_UNRECEIVED_LINES($jobId: uuid!, $vendorId: uuid!) { query QUERY_UNRECEIVED_LINES($jobId: uuid!, $vendorId: uuid!) {
parts_order_lines( parts_order_lines(
where: { parts_order: { jobid: { _eq: $jobId }, vendorid: { _eq: $vendorId } }, cm_received: { _neq: true } } where: {
parts_order: { jobid: { _eq: $jobId }, vendorid: { _eq: $vendorId }, return: { _eq: true } }
_or: [{ cm_received: { _neq: true } }, { cm_received: { _is_null: true } }]
}
) { ) {
cm_received cm_received
id id

View File

@@ -82,13 +82,14 @@ const rootEl = document.getElementById("root");
if (!rootEl) throw new Error('Missing root element: <div id="root" />'); if (!rootEl) throw new Error('Missing root element: <div id="root" />');
const appTree = import.meta.env.DEV ? ( const appTree =
<StrictMode> import.meta.env.DEV && import.meta.env?.VITE_DISABLE_STRICT_MODE !== "true" ? (
<StrictMode>
<App />
</StrictMode>
) : (
<App /> <App />
</StrictMode> );
) : (
<App />
);
ReactDOM.createRoot(rootEl).render(appTree); ReactDOM.createRoot(rootEl).render(appTree);

View File

@@ -41,19 +41,25 @@ export function ContractDetailPageContainer({ setBreadcrumbs, addRecentItem, set
useEffect(() => { useEffect(() => {
setSelectedHeader("contracts"); setSelectedHeader("contracts");
document.title = loading
? InstanceRenderManager({ const appName = InstanceRenderManager({
imex: t("titles.imexonline"), imex: "$t(titles.imexonline)",
rome: t("titles.romeonline") rome: "$t(titles.romeonline)"
}) });
: error
? InstanceRenderManager({ const fallbackTitle = InstanceRenderManager({
imex: t("titles.imexonline"), imex: t("titles.imexonline"),
rome: t("titles.romeonline") rome: t("titles.romeonline")
}) });
: t("titles.contracts-detail", {
id: (data?.cccontracts_by_pk && data.cccontracts_by_pk.agreementnumber) || "" if (loading || error) {
}); document.title = fallbackTitle;
} else {
document.title = t("titles.contracts-detail", {
id: (data?.cccontracts_by_pk && data.cccontracts_by_pk.agreementnumber) || "",
app: appName
});
}
setBreadcrumbs([ setBreadcrumbs([
{ link: "/manage/courtesycars", label: t("titles.bc.courtesycars") }, { link: "/manage/courtesycars", label: t("titles.bc.courtesycars") },

View File

@@ -181,6 +181,7 @@ export function ExportLogsPageComponent() {
searchParams.search = value; searchParams.search = value;
history({ search: queryString.stringify(searchParams) }); history({ search: queryString.stringify(searchParams) });
}} }}
enterButton
/> />
</Space> </Space>
} }

View File

@@ -4,7 +4,6 @@ import { PageHeader } from "@ant-design/pro-layout";
import { useEffect } from "react"; import { useEffect } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import AlertComponent from "../../components/alert/alert.component"; import AlertComponent from "../../components/alert/alert.component";
import JobsAvailableTableContainer from "../../components/jobs-available-table/jobs-available-table.container"; import JobsAvailableTableContainer from "../../components/jobs-available-table/jobs-available-table.container";
@@ -25,6 +24,26 @@ const mapDispatchToProps = (dispatch) => ({
export function JobsAvailablePageContainer({ partnerVersion, setBreadcrumbs, setSelectedHeader }) { export function JobsAvailablePageContainer({ partnerVersion, setBreadcrumbs, setSelectedHeader }) {
const { t } = useTranslation(); const { t } = useTranslation();
const getOS = () => {
const userAgent = navigator.userAgent;
if (userAgent.indexOf("Win") !== -1) return "windows";
if (userAgent.indexOf("Mac") !== -1) return "mac";
if (userAgent.indexOf("Linux") !== -1) return "linux";
return "unknown";
};
const os = getOS();
const downloadUrl = InstanceRenderManager({
imex:
os === "windows"
? "https://imex-partner.s3.ca-central-1.amazonaws.com/imex-partner-x64.exe"
: "https://imex-partner.s3.ca-central-1.amazonaws.com/imex-partner-arm64.dmg",
rome:
os === "windows"
? "https://rome-partner.s3.us-east-2.amazonaws.com/rome-partner-x64.exe"
: "https://rome-partner.s3.us-east-2.amazonaws.com/rome-partner-arm64.dmg"
});
useEffect(() => { useEffect(() => {
document.title = t("titles.jobsavailable", { document.title = t("titles.jobsavailable", {
app: InstanceRenderManager({ app: InstanceRenderManager({
@@ -39,24 +58,12 @@ export function JobsAvailablePageContainer({ partnerVersion, setBreadcrumbs, set
return ( return (
<RbacWrapper action="jobs:available-list"> <RbacWrapper action="jobs:available-list">
<div> <div>
<PageHeader <PageHeader />
title={t("titles.bc.availablejobs")}
extra={
<Link to="/manage/jobs/new">
<Button>{t("jobs.actions.manualnew")}</Button>
</Link>
}
/>
{!partnerVersion && ( {!partnerVersion && (
<AlertComponent <AlertComponent
type="warning" type="warning"
action={ action={
<a <a href={downloadUrl}>
href={InstanceRenderManager({
imex: "https://partner.imex.online/Setup.exe",
rome: "https://partner.romeonline.io/Setup.exe"
})}
>
<Button size="small">{t("general.actions.download")}</Button> <Button size="small">{t("general.actions.download")}</Button>
</a> </a>
} }

View File

@@ -82,10 +82,23 @@ export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail, set
const handleFinish = async ({ removefromproduction, ...values }) => { const handleFinish = async ({ removefromproduction, ...values }) => {
setLoading(true); setLoading(true);
// Validate that all joblines have valid IDs
const joblinesWithIds = values.joblines.filter(jl => jl && jl.id);
if (joblinesWithIds.length !== values.joblines.length) {
notification.error({
title: t("jobs.errors.invalidjoblines"),
message: t("jobs.errors.missingjoblineids")
});
setLoading(false);
return;
}
const result = await client.mutate({ const result = await client.mutate({
mutation: generateJobLinesUpdatesForInvoicing(values.joblines) mutation: generateJobLinesUpdatesForInvoicing(values.joblines)
}); });
if (result.errors) { if (result.errors) {
setLoading(false);
return; // Abandon the rest of the close. return; // Abandon the rest of the close.
} }
@@ -497,7 +510,7 @@ export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail, set
<Statistic <Statistic
title={t("jobs.labels.pimraryamountpayable")} title={t("jobs.labels.pimraryamountpayable")}
styles={{ styles={{
value: { content: {
color: discrep.getAmount() >= 0 ? "green" : "red" color: discrep.getAmount() >= 0 ? "green" : "red"
} }
}} }}

View File

@@ -2,7 +2,7 @@ import { FloatButton, Layout, Spin } from "antd";
import { Route, Routes } from "react-router-dom"; import { Route, Routes } from "react-router-dom";
// import preval from "preval.macro"; // import preval from "preval.macro";
import { lazy, Suspense, useEffect, useRef, useState } from "react"; import { Suspense, 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";
@@ -29,87 +29,88 @@ import {
import InstanceRenderManager from "../../utils/instanceRenderMgr.js"; import InstanceRenderManager from "../../utils/instanceRenderMgr.js";
import useAlertsNotifications from "../../hooks/useAlertsNotifications.jsx"; import useAlertsNotifications from "../../hooks/useAlertsNotifications.jsx";
import { selectDarkMode } from "../../redux/application/application.selectors.js"; import { selectDarkMode } from "../../redux/application/application.selectors.js";
import { lazyDev } from "../../utils/lazyWithPreload.jsx";
const PrintCenterModalContainer = lazy( const PrintCenterModalContainer = lazyDev(
() => import("../../components/print-center-modal/print-center-modal.container") () => import("../../components/print-center-modal/print-center-modal.container")
); );
const NoteUpsertModal = lazy(() => import("../../components/note-upsert-modal/note-upsert-modal.container.jsx")); const NoteUpsertModal = lazyDev(() => import("../../components/note-upsert-modal/note-upsert-modal.container.jsx"));
const JobsPage = lazy(() => import("../jobs/jobs.page")); const JobsPage = lazyDev(() => import("../jobs/jobs.page"));
const CardPaymentModalContainer = lazy( const CardPaymentModalContainer = lazyDev(
() => import("../../components/card-payment-modal/card-payment-modal.container.jsx") () => import("../../components/card-payment-modal/card-payment-modal.container.jsx")
); );
const JobsDetailPage = lazy(() => import("../jobs-detail/jobs-detail.page.container")); const JobsDetailPage = lazyDev(() => import("../jobs-detail/jobs-detail.page.container"));
const InventoryListPage = lazy(() => import("../inventory/inventory.page")); const InventoryListPage = lazyDev(() => import("../inventory/inventory.page"));
const ProfilePage = lazy(() => import("../profile/profile.container.page")); const ProfilePage = lazyDev(() => import("../profile/profile.container.page"));
const JobsAvailablePage = lazy(() => import("../jobs-available/jobs-available.page.container")); const JobsAvailablePage = lazyDev(() => import("../jobs-available/jobs-available.page.container"));
const ScheduleContainer = lazy(() => import("../schedule/schedule.page.container")); const ScheduleContainer = lazyDev(() => import("../schedule/schedule.page.container"));
const VehiclesContainer = lazy(() => import("../vehicles/vehicles.page.container")); const VehiclesContainer = lazyDev(() => import("../vehicles/vehicles.page.container"));
const VehiclesDetailContainer = lazy(() => import("../vehicles-detail/vehicles-detail.page.container")); const VehiclesDetailContainer = lazyDev(() => import("../vehicles-detail/vehicles-detail.page.container"));
const OwnersContainer = lazy(() => import("../owners/owners.page.container")); const OwnersContainer = lazyDev(() => import("../owners/owners.page.container"));
const OwnersDetailContainer = lazy(() => import("../owners-detail/owners-detail.page.container")); const OwnersDetailContainer = lazyDev(() => import("../owners-detail/owners-detail.page.container"));
const ShopPage = lazy(() => import("../shop/shop.page.component")); const ShopPage = lazyDev(() => import("../shop/shop.page.component"));
const ShopVendorPageContainer = lazy(() => import("../shop-vendor/shop-vendor.page.container")); const ShopVendorPageContainer = lazyDev(() => import("../shop-vendor/shop-vendor.page.container"));
const EmailOverlayContainer = lazy(() => import("../../components/email-overlay/email-overlay.container.jsx")); const EmailOverlayContainer = lazyDev(() => import("../../components/email-overlay/email-overlay.container.jsx"));
const JobsCreateContainerPage = lazy(() => import("../jobs-create/jobs-create.container")); const JobsCreateContainerPage = lazyDev(() => import("../jobs-create/jobs-create.container"));
const CourtesyCarCreateContainer = lazy(() => import("../courtesy-car-create/courtesy-car-create.page.container")); const CourtesyCarCreateContainer = lazyDev(() => import("../courtesy-car-create/courtesy-car-create.page.container"));
const CourtesyCarDetailContainer = lazy(() => import("../courtesy-car-detail/courtesy-car-detail.page.container")); const CourtesyCarDetailContainer = lazyDev(() => import("../courtesy-car-detail/courtesy-car-detail.page.container"));
const CourtesyCarsPage = lazy(() => import("../courtesy-cars/courtesy-cars.page.container")); const CourtesyCarsPage = lazyDev(() => import("../courtesy-cars/courtesy-cars.page.container"));
const ContractCreatePage = lazy(() => import("../contract-create/contract-create.page.container")); const ContractCreatePage = lazyDev(() => import("../contract-create/contract-create.page.container"));
const ContractDetailPage = lazy(() => import("../contract-detail/contract-detail.page.container")); const ContractDetailPage = lazyDev(() => import("../contract-detail/contract-detail.page.container"));
const ContractsList = lazy(() => import("../contracts/contracts.page.container")); const ContractsList = lazyDev(() => import("../contracts/contracts.page.container"));
const BillsListPage = lazy(() => import("../bills/bills.page.container")); const BillsListPage = lazyDev(() => import("../bills/bills.page.container"));
const FeatureRequestPage = lazy(() => import("../feature-request/feature-request.page.jsx")); const FeatureRequestPage = lazyDev(() => import("../feature-request/feature-request.page.jsx"));
const JobCostingModal = lazy(() => import("../../components/job-costing-modal/job-costing-modal.container")); const JobCostingModal = lazyDev(() => import("../../components/job-costing-modal/job-costing-modal.container"));
const ReportCenterModal = lazy(() => import("../../components/report-center-modal/report-center-modal.container")); const ReportCenterModal = lazyDev(() => import("../../components/report-center-modal/report-center-modal.container"));
const BillEnterModalContainer = lazy(() => import("../../components/bill-enter-modal/bill-enter-modal.container")); const BillEnterModalContainer = lazyDev(() => import("../../components/bill-enter-modal/bill-enter-modal.container"));
const TimeTicketModalContainer = lazy(() => import("../../components/time-ticket-modal/time-ticket-modal.container")); const TimeTicketModalContainer = lazyDev(() => import("../../components/time-ticket-modal/time-ticket-modal.container"));
const TimeTicketModalTask = lazy( const TimeTicketModalTask = lazyDev(
() => import("../../components/time-ticket-task-modal/time-ticket-task-modal.container") () => import("../../components/time-ticket-task-modal/time-ticket-task-modal.container")
); );
const PaymentModalContainer = lazy(() => import("../../components/payment-modal/payment-modal.container")); const PaymentModalContainer = lazyDev(() => import("../../components/payment-modal/payment-modal.container"));
const ProductionListPage = lazy(() => import("../production-list/production-list.container")); const ProductionListPage = lazyDev(() => import("../production-list/production-list.container"));
const ProductionBoardPage = lazy(() => import("../production-board/production-board.container")); const ProductionBoardPage = lazyDev(() => import("../production-board/production-board.container"));
// const ShopTemplates = lazy(() => // const ShopTemplates = lazyDev(() =>
// import("../shop-templates/shop-templates.container") // import("../shop-templates/shop-templates.container")
// ); // );
const JobIntake = lazy(() => import("../jobs-intake/jobs-intake.page.container")); const JobIntake = lazyDev(() => import("../jobs-intake/jobs-intake.page.container"));
const JobChecklistView = lazy(() => import("../jobs-checklist-view/jobs-checklist-view.page")); const JobChecklistView = lazyDev(() => import("../jobs-checklist-view/jobs-checklist-view.page"));
const JobDeliver = lazy(() => import("../jobs-deliver/jobs-delivery.page.container")); const JobDeliver = lazyDev(() => import("../jobs-deliver/jobs-delivery.page.container"));
const AccountingQboCallback = lazy(() => import("../accounting-qbo/accounting-qbo.page")); const AccountingQboCallback = lazyDev(() => import("../accounting-qbo/accounting-qbo.page"));
const AccountingReceivables = lazy(() => import("../accounting-receivables/accounting-receivables.container")); const AccountingReceivables = lazyDev(() => import("../accounting-receivables/accounting-receivables.container"));
const AccountingPayables = lazy(() => import("../accounting-payables/accounting-payables.container")); const AccountingPayables = lazyDev(() => import("../accounting-payables/accounting-payables.container"));
const AccountingPayments = lazy(() => import("../accounting-payments/accounting-payments.container")); const AccountingPayments = lazyDev(() => import("../accounting-payments/accounting-payments.container"));
const AllJobs = lazy(() => import("../jobs-all/jobs-all.container")); const AllJobs = lazyDev(() => import("../jobs-all/jobs-all.container"));
const ReadyJobs = lazy(() => import("../jobs-ready/jobs-ready.page")); const ReadyJobs = lazyDev(() => import("../jobs-ready/jobs-ready.page"));
const JobsClose = lazy(() => import("../jobs-close/jobs-close.container")); const JobsClose = lazyDev(() => import("../jobs-close/jobs-close.container"));
const JobsAdmin = lazy(() => import("../jobs-admin/jobs-admin.page")); const JobsAdmin = lazyDev(() => import("../jobs-admin/jobs-admin.page"));
const TempDocs = lazy(() => import("../temporary-docs/temporary-docs.container")); const TempDocs = lazyDev(() => import("../temporary-docs/temporary-docs.container"));
const ShopCsiPageContainer = lazy(() => import("../shop-csi/shop-csi.container.page")); const ShopCsiPageContainer = lazyDev(() => import("../shop-csi/shop-csi.container.page"));
const PaymentsAll = lazy(() => import("../payments-all/payments-all.container.page")); const PaymentsAll = lazyDev(() => import("../payments-all/payments-all.container.page"));
const ShiftClock = lazy(() => import("../shift-clock/shift-clock.page")); const ShiftClock = lazyDev(() => import("../shift-clock/shift-clock.page"));
const Scoreboard = lazy(() => import("../scoreboard/scoreboard.page.container")); const Scoreboard = lazyDev(() => import("../scoreboard/scoreboard.page.container"));
const TimeTicketsAll = lazy(() => import("../time-tickets/time-tickets.container")); const TimeTicketsAll = lazyDev(() => import("../time-tickets/time-tickets.container"));
const Help = lazy(() => import("../help/help.page")); const Help = lazyDev(() => import("../help/help.page"));
const PartsQueue = lazy(() => import("../parts-queue/parts-queue.page.container")); const PartsQueue = lazyDev(() => import("../parts-queue/parts-queue.page.container"));
const ExportLogs = lazy(() => import("../export-logs/export-logs.page.container")); const ExportLogs = lazyDev(() => import("../export-logs/export-logs.page.container"));
const Phonebook = lazy(() => import("../phonebook/phonebook.page.container")); const Phonebook = lazyDev(() => import("../phonebook/phonebook.page.container"));
const EmailTest = lazy(() => import("../../components/email-test/email-test-component")); const EmailTest = lazyDev(() => import("../../components/email-test/email-test-component"));
const Dashboard = lazy(() => import("../dashboard/dashboard.container")); const Dashboard = lazyDev(() => import("../dashboard/dashboard.container"));
const Dms = lazy(() => import("../dms/dms.container")); const Dms = lazyDev(() => import("../dms/dms.container"));
const DmsPayables = lazy(() => import("../dms-payables/dms-payables.container")); const DmsPayables = lazyDev(() => import("../dms-payables/dms-payables.container"));
const ManageRootPage = lazy(() => import("../manage-root/manage-root.page.container")); const ManageRootPage = lazyDev(() => import("../manage-root/manage-root.page.container"));
const TtApprovals = lazy(() => import("../tt-approvals/tt-approvals.page.container")); const TtApprovals = lazyDev(() => import("../tt-approvals/tt-approvals.page.container"));
const MyTasksPage = lazy(() => import("../tasks/myTasksPageContainer.jsx")); const MyTasksPage = lazyDev(() => import("../tasks/myTasksPageContainer.jsx"));
const AllTasksPage = lazy(() => import("../tasks/allTasksPageContainer.jsx")); const AllTasksPage = lazyDev(() => import("../tasks/allTasksPageContainer.jsx"));
const TaskUpsertModalContainer = lazy(() => import("../../components/task-upsert-modal/task-upsert-modal.container")); const TaskUpsertModalContainer = lazyDev(() => import("../../components/task-upsert-modal/task-upsert-modal.container"));
const { Content } = Layout; const { Content } = Layout;
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({

View File

@@ -167,6 +167,7 @@ export function PhonebookPageComponent({ bodyshop, authLevel }) {
searchParams.page = 1; searchParams.page = 1;
history({ search: queryString.stringify(searchParams) }); history({ search: queryString.stringify(searchParams) });
}} }}
enterButton
/> />
</Space> </Space>
} }

View File

@@ -1,6 +1,6 @@
import * as Sentry from "@sentry/react"; import * as Sentry from "@sentry/react";
import { FloatButton, Layout, Spin } from "antd"; import { FloatButton, Layout, Spin } from "antd";
import { lazy, Suspense, useEffect } from "react"; import { Suspense, useEffect } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Navigate, Route, Routes, useLocation } from "react-router-dom"; import { Navigate, Route, Routes, useLocation } from "react-router-dom";
@@ -15,20 +15,21 @@ import UpdateAlert from "../../components/update-alert/update-alert.component.js
import { selectBodyshop, selectInstanceConflict } from "../../redux/user/user.selectors.js"; import { selectBodyshop, selectInstanceConflict } from "../../redux/user/user.selectors.js";
import InstanceRenderManager from "../../utils/instanceRenderMgr.js"; import InstanceRenderManager from "../../utils/instanceRenderMgr.js";
import useAlertsNotifications from "../../hooks/useAlertsNotifications.jsx"; import useAlertsNotifications from "../../hooks/useAlertsNotifications.jsx";
import { lazyDev } from "../../utils/lazyWithPreload.jsx";
const SimplifiedPartsJobsPage = lazy(() => import("../simplified-parts-jobs/simplified-parts-jobs.page.jsx")); const SimplifiedPartsJobsPage = lazyDev(() => import("../simplified-parts-jobs/simplified-parts-jobs.page.jsx"));
const SimplifiedPartsJobsDetailPage = lazy( const SimplifiedPartsJobsDetailPage = lazyDev(
() => import("../simplified-parts-jobs-detail/simplified-parts-jobs-detail.container.jsx") () => import("../simplified-parts-jobs-detail/simplified-parts-jobs-detail.container.jsx")
); );
const PartsSettingsPage = lazy(() => import("../parts-settings/parts-settings.page.component.jsx")); const PartsSettingsPage = lazyDev(() => import("../parts-settings/parts-settings.page.component.jsx"));
const ShopVendorPageContainer = lazy(() => import("../shop-vendor/shop-vendor.page.container.jsx")); const ShopVendorPageContainer = lazyDev(() => import("../shop-vendor/shop-vendor.page.container.jsx"));
const EmailOverlayContainer = lazy(() => import("../../components/email-overlay/email-overlay.container.jsx")); const EmailOverlayContainer = lazyDev(() => import("../../components/email-overlay/email-overlay.container.jsx"));
const ReportCenterModal = lazy(() => import("../../components/report-center-modal/report-center-modal.container.jsx")); const ReportCenterModal = lazyDev(() => import("../../components/report-center-modal/report-center-modal.container.jsx"));
const PrintCenterModalContainer = lazy( const PrintCenterModalContainer = lazyDev(
() => import("../../components/print-center-modal/print-center-modal.container") () => import("../../components/print-center-modal/print-center-modal.container")
); );
const VehiclesContainer = lazy(() => import("../vehicles/vehicles.page.container.jsx")); const VehiclesContainer = lazyDev(() => import("../vehicles/vehicles.page.container.jsx"));
const VehiclesDetailContainer = lazy(() => import("../vehicles-detail/vehicles-detail.page.container.jsx")); const VehiclesDetailContainer = lazyDev(() => import("../vehicles-detail/vehicles-detail.page.container.jsx"));
const { Content } = Layout; const { Content } = Layout;
// Redirector to strip '/parts/jobs' from path for non-detail routes // Redirector to strip '/parts/jobs' from path for non-detail routes

View File

@@ -1,5 +1,5 @@
import { Card, FloatButton, Layout } from "antd"; import { Card, FloatButton, Layout } from "antd";
import { lazy, Suspense, useEffect } from "react"; import { Suspense, useEffect } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Route, Routes, useNavigate } from "react-router-dom"; import { Route, Routes, useNavigate } from "react-router-dom";
@@ -15,25 +15,31 @@ import { selectTechnician } from "../../redux/tech/tech.selectors";
import InstanceRenderManager from "../../utils/instanceRenderMgr"; import InstanceRenderManager from "../../utils/instanceRenderMgr";
import "./tech.page.styles.scss"; import "./tech.page.styles.scss";
import UpsellComponent, { upsellEnum } from "../../components/upsell/upsell.component.jsx"; import UpsellComponent, { upsellEnum } from "../../components/upsell/upsell.component.jsx";
import { lazyDev } from "../../utils/lazyWithPreload.jsx";
const TimeTicketModalContainer = lazy(() => import("../../components/time-ticket-modal/time-ticket-modal.container")); const NoteUpsertModal = lazyDev(() => import("../../components/note-upsert-modal/note-upsert-modal.container.jsx"));
const EmailOverlayContainer = lazy(() => import("../../components/email-overlay/email-overlay.container.jsx")); const TimeTicketModalContainer = lazyDev(
const PrintCenterModalContainer = lazy( () => import("../../components/time-ticket-modal/time-ticket-modal.container")
);
const EmailOverlayContainer = lazyDev(() => import("../../components/email-overlay/email-overlay.container.jsx"));
const PrintCenterModalContainer = lazyDev(
() => import("../../components/print-center-modal/print-center-modal.container") () => import("../../components/print-center-modal/print-center-modal.container")
); );
const TechLogin = lazy(() => import("../../components/tech-login/tech-login.component")); const TechLogin = lazyDev(() => import("../../components/tech-login/tech-login.component"));
const TechLookup = lazy(() => import("../tech-lookup/tech-lookup.container")); const TechLookup = lazyDev(() => import("../tech-lookup/tech-lookup.container"));
const ProductionListPage = lazy(() => import("../production-list/production-list.container")); const ProductionListPage = lazyDev(() => import("../production-list/production-list.container"));
const ProductionBoardPage = lazy(() => import("../production-board/production-board.container")); const ProductionBoardPage = lazyDev(() => import("../production-board/production-board.container"));
const TechJobClock = lazy(() => import("../tech-job-clock/tech-job-clock.component")); const TechJobClock = lazyDev(() => import("../tech-job-clock/tech-job-clock.component"));
const TechShiftClock = lazy(() => import("../tech-shift-clock/tech-shift-clock.component")); const TechShiftClock = lazyDev(() => import("../tech-shift-clock/tech-shift-clock.component"));
const TimeTicketModalTask = lazy( const TimeTicketModalTask = lazyDev(
() => import("../../components/time-ticket-task-modal/time-ticket-task-modal.container") () => import("../../components/time-ticket-task-modal/time-ticket-task-modal.container")
); );
const TechAssignedProdJobs = lazy(() => import("../tech-assigned-prod-jobs/tech-assigned-prod-jobs.component")); const TechAssignedProdJobs = lazyDev(() => import("../tech-assigned-prod-jobs/tech-assigned-prod-jobs.component"));
const TechDispatchedParts = lazy(() => import("../tech-dispatched-parts/tech-dispatched-parts.page")); const TechDispatchedParts = lazyDev(() => import("../tech-dispatched-parts/tech-dispatched-parts.page"));
const TaskUpsertModalContainer = lazy(() => import("../../components/task-upsert-modal/task-upsert-modal.container")); const TaskUpsertModalContainer = lazyDev(
() => import("../../components/task-upsert-modal/task-upsert-modal.container")
);
const { Content } = Layout; const { Content } = Layout;
@@ -69,6 +75,8 @@ export function TechPage({ technician }) {
<TechHeader /> <TechHeader />
<TaskUpsertModalContainer /> <TaskUpsertModalContainer />
<NoteUpsertModal />
<Content className="tech-content-container"> <Content className="tech-content-container">
<ErrorBoundary> <ErrorBoundary>
<Suspense <Suspense

View File

@@ -48,6 +48,7 @@
"arrivedon": "Arrived on: ", "arrivedon": "Arrived on: ",
"arrivingjobs": "Arriving Jobs", "arrivingjobs": "Arriving Jobs",
"blocked": "Blocked", "blocked": "Blocked",
"bp": "B/P",
"cancelledappointment": "Canceled appointment for: ", "cancelledappointment": "Canceled appointment for: ",
"completingjobs": "Completing Jobs", "completingjobs": "Completing Jobs",
"dataconsistency": "<0>{{ro_number}}</0> has a data consistency issue. It may have been excluded for scheduling purposes. CODE: {{code}}.", "dataconsistency": "<0>{{ro_number}}</0> has a data consistency issue. It may have been excluded for scheduling purposes. CODE: {{code}}.",
@@ -59,18 +60,17 @@
"noarrivingjobs": "No Jobs are arriving.", "noarrivingjobs": "No Jobs are arriving.",
"nocompletingjobs": "No Jobs scheduled for completion.", "nocompletingjobs": "No Jobs scheduled for completion.",
"nodateselected": "No date has been selected.", "nodateselected": "No date has been selected.",
"owner": "Owner",
"priorappointments": "Previous Appointments", "priorappointments": "Previous Appointments",
"reminder": "This is {{shopname}} reminding you about an appointment on {{date}} at {{time}}. Please let us know if you are not able to make the appointment. We look forward to seeing you soon. ", "reminder": "This is {{shopname}} reminding you about an appointment on {{date}} at {{time}}. Please let us know if you are not able to make the appointment. We look forward to seeing you soon. ",
"ro_number": "RO #",
"scheduled_completion": "Scheduled Completion",
"scheduledfor": "Scheduled appointment for: ", "scheduledfor": "Scheduled appointment for: ",
"severalerrorsfound": "Several Jobs have issues which may prevent accurate smart scheduling. Click to expand.", "severalerrorsfound": "Several Jobs have issues which may prevent accurate smart scheduling. Click to expand.",
"smartscheduling": "Smart Scheduling", "smartscheduling": "Smart Scheduling",
"smspaymentreminder": "This is {{shopname}} reminding you about your remaining balance of {{amount}}. To pay for the said balance click the link {{payment_link}}.", "smspaymentreminder": "This is {{shopname}} reminding you about your remaining balance of {{amount}}. To pay for the said balance click the link {{payment_link}}.",
"suggesteddates": "Suggested Dates", "suggesteddates": "Suggested Dates",
"ro_number": "RO #", "vehicle": "Vehicle"
"owner": "Owner",
"vehicle": "Vehicle",
"bp": "B/P",
"scheduled_completion": "Scheduled Completion"
}, },
"successes": { "successes": {
"canceled": "Appointment canceled successfully.", "canceled": "Appointment canceled successfully.",
@@ -90,6 +90,11 @@
"actions": "Actions" "actions": "Actions"
} }
}, },
"audio": {
"manager": {
"description": "Click anywhere to enable the message ding."
}
},
"audit": { "audit": {
"fields": { "fields": {
"cc": "CC", "cc": "CC",
@@ -149,11 +154,6 @@
"tasks_updated": "Task '{{title}}' updated by {{updatedBy}}" "tasks_updated": "Task '{{title}}' updated by {{updatedBy}}"
} }
}, },
"audio": {
"manager": {
"description": "Click anywhere to enable the message ding."
}
},
"billlines": { "billlines": {
"actions": { "actions": {
"newline": "New Line" "newline": "New Line"
@@ -281,9 +281,9 @@
}, },
"errors": { "errors": {
"creatingdefaultview": "Error creating default view.", "creatingdefaultview": "Error creating default view.",
"duplicate_insurance_company": "Duplicate insurance company name. Each insurance company name must be unique",
"loading": "Unable to load shop details. Please call technical support.", "loading": "Unable to load shop details. Please call technical support.",
"saving": "Error encountered while saving. {{message}}", "saving": "Error encountered while saving. {{message}}"
"duplicate_insurance_company": "Duplicate insurance company name. Each insurance company name must be unique"
}, },
"fields": { "fields": {
"ReceivableCustomField": "QBO Receivable Custom Field {{number}}", "ReceivableCustomField": "QBO Receivable Custom Field {{number}}",
@@ -564,21 +564,18 @@
"responsibilitycenter_tax_tier": "Tax {{typeNum}} Tier {{typeNumIterator}}", "responsibilitycenter_tax_tier": "Tax {{typeNum}} Tier {{typeNumIterator}}",
"responsibilitycenter_tax_type": "Tax {{typeNum}} Type", "responsibilitycenter_tax_type": "Tax {{typeNum}} Type",
"responsibilitycenters": { "responsibilitycenters": {
"gogcode": "GOG Code (BreakOut)",
"item_type": "Item Type",
"item_type_gog": "GOG",
"item_type_paint": "Paint Materials",
"item_type_freight": "Freight",
"taxable_flag": "Taxable?",
"taxable": "Taxable",
"nontaxable": "Non-taxable",
"ap": "Accounts Payable", "ap": "Accounts Payable",
"ar": "Accounts Receivable", "ar": "Accounts Receivable",
"ats": "ATS", "ats": "ATS",
"federal_tax": "Federal Tax", "federal_tax": "Federal Tax",
"federal_tax_itc": "Federal Tax Credit", "federal_tax_itc": "Federal Tax Credit",
"gogcode": "GOG Code (BreakOut)",
"gst_override": "GST Override Account #", "gst_override": "GST Override Account #",
"invoiceexemptcode": "QuickBooks US - Invoice Tax Exempt Code", "invoiceexemptcode": "QuickBooks US - Invoice Tax Exempt Code",
"item_type": "Item Type",
"item_type_freight": "Freight",
"item_type_gog": "GOG",
"item_type_paint": "Paint Materials",
"itemexemptcode": "QuickBooks US - Line Item Tax Exempt Code", "itemexemptcode": "QuickBooks US - Line Item Tax Exempt Code",
"la1": "LA1", "la1": "LA1",
"la2": "LA2", "la2": "LA2",
@@ -597,6 +594,7 @@
"local_tax": "Local Tax", "local_tax": "Local Tax",
"mapa": "Paint Materials", "mapa": "Paint Materials",
"mash": "Shop Materials", "mash": "Shop Materials",
"nontaxable": "Non-taxable",
"paa": "Aftermarket", "paa": "Aftermarket",
"pac": "Chrome", "pac": "Chrome",
"pag": "Glass", "pag": "Glass",
@@ -617,6 +615,8 @@
"state": "State Tax Applies" "state": "State Tax Applies"
}, },
"state_tax": "State Tax", "state_tax": "State Tax",
"taxable": "Taxable",
"taxable_flag": "Taxable?",
"tow": "Towing" "tow": "Towing"
}, },
"schedule_end_time": "Schedule Ending Time", "schedule_end_time": "Schedule Ending Time",
@@ -678,8 +678,6 @@
"zip_post": "Zip/Postal Code" "zip_post": "Zip/Postal Code"
}, },
"labels": { "labels": {
"parts_shop_management": "Shop Management",
"parts_vendor_management": "Vendor Management",
"2tiername": "Name => RO", "2tiername": "Name => RO",
"2tiersetup": "2 Tier Setup", "2tiersetup": "2 Tier Setup",
"2tiersource": "Source => RO", "2tiersource": "Source => RO",
@@ -702,11 +700,11 @@
"payers": "Payers" "payers": "Payers"
}, },
"cdk_dealerid": "CDK Dealer ID", "cdk_dealerid": "CDK Dealer ID",
"rr_dealerid": "Reynolds Store Number",
"costsmapping": "Costs Mapping", "costsmapping": "Costs Mapping",
"dms_allocations": "DMS Allocations", "dms_allocations": "DMS Allocations",
"pbs_serialnumber": "PBS Serial Number", "pbs_serialnumber": "PBS Serial Number",
"profitsmapping": "Profits Mapping", "profitsmapping": "Profits Mapping",
"rr_dealerid": "Reynolds Store Number",
"title": "DMS" "title": "DMS"
}, },
"emaillater": "Email Later", "emaillater": "Email Later",
@@ -733,6 +731,8 @@
"followers": "Notifications" "followers": "Notifications"
}, },
"orderstatuses": "Order Statuses", "orderstatuses": "Order Statuses",
"parts_shop_management": "Shop Management",
"parts_vendor_management": "Vendor Management",
"partslocations": "Parts Locations", "partslocations": "Parts Locations",
"partsscan": "Parts Scanning", "partsscan": "Parts Scanning",
"printlater": "Print Later", "printlater": "Print Later",
@@ -1228,8 +1228,6 @@
}, },
"general": { "general": {
"actions": { "actions": {
"select": "Select",
"optional": "Optional",
"add": "Add", "add": "Add",
"autoupdate": "{{app}} will automatically update in {{time}} seconds. Please save all changes.", "autoupdate": "{{app}} will automatically update in {{time}} seconds. Please save all changes.",
"calculate": "Calculate", "calculate": "Calculate",
@@ -1249,6 +1247,7 @@
"login": "Login", "login": "Login",
"next": "Next", "next": "Next",
"ok": "Ok", "ok": "Ok",
"optional": "Optional",
"previous": "Previous", "previous": "Previous",
"print": "Print", "print": "Print",
"refresh": "Refresh", "refresh": "Refresh",
@@ -1259,6 +1258,7 @@
"save": "Save", "save": "Save",
"saveandnew": "Save and New", "saveandnew": "Save and New",
"saveas": "Save As", "saveas": "Save As",
"select": "Select",
"selectall": "Select All", "selectall": "Select All",
"send": "Send", "send": "Send",
"sendbysms": "Send by SMS", "sendbysms": "Send by SMS",
@@ -1288,8 +1288,7 @@
"vehicle": "Vehicle" "vehicle": "Vehicle"
}, },
"labels": { "labels": {
"selected": "Selected", "apply": "Apply",
"settings": "Settings",
"actions": "Actions", "actions": "Actions",
"areyousure": "Are you sure?", "areyousure": "Are you sure?",
"barcode": "Barcode", "barcode": "Barcode",
@@ -1343,8 +1342,10 @@
"search": "Search...", "search": "Search...",
"searchresults": "Results for {{search}}", "searchresults": "Results for {{search}}",
"selectdate": "Select date...", "selectdate": "Select date...",
"selected": "Selected",
"sendagain": "Send Again", "sendagain": "Send Again",
"sendby": "Send By", "sendby": "Send By",
"settings": "Settings",
"signin": "Sign In", "signin": "Sign In",
"sms": "SMS", "sms": "SMS",
"status": "Status", "status": "Status",
@@ -1587,13 +1588,13 @@
"labels": { "labels": {
"adjustmenttobeadded": "Adjustment to be added: {{adjustment}}", "adjustmenttobeadded": "Adjustment to be added: {{adjustment}}",
"billref": "Latest Bill", "billref": "Latest Bill",
"bulk_location_help": "This will set the same location on all selected lines.",
"convertedtolabor": "This line has been converted to labor. Ensure you adjust the profit center for the amount accordingly.", "convertedtolabor": "This line has been converted to labor. Ensure you adjust the profit center for the amount accordingly.",
"edit": "Edit Line", "edit": "Edit Line",
"ioucreated": "IOU", "ioucreated": "IOU",
"new": "New Line", "new": "New Line",
"nostatus": "No Status", "nostatus": "No Status",
"presets": "Jobline Presets", "presets": "Jobline Presets"
"bulk_location_help": "This will set the same location on all selected lines."
}, },
"successes": { "successes": {
"created": "Job line created successfully.", "created": "Job line created successfully.",
@@ -1676,7 +1677,9 @@
"deleted": "Error deleting Job. {{error}}", "deleted": "Error deleting Job. {{error}}",
"exporting": "Error exporting Job. {{error}}", "exporting": "Error exporting Job. {{error}}",
"exporting-partner": "Unable to connect to partner application. Please ensure it is running and logged in.", "exporting-partner": "Unable to connect to partner application. Please ensure it is running and logged in.",
"invalidjoblines": "Job has invalid job lines.",
"invoicing": "Error invoicing Job. {{error}}", "invoicing": "Error invoicing Job. {{error}}",
"missingjoblineids": "Missing job line IDs for job lines.",
"noaccess": "This Job does not exist or you do not have access to it.", "noaccess": "This Job does not exist or you do not have access to it.",
"nodamage": "No damage points on estimate.", "nodamage": "No damage points on estimate.",
"nodates": "No dates specified for this Job.", "nodates": "No dates specified for this Job.",
@@ -1699,9 +1702,9 @@
"actual_delivery": "Actual Delivery", "actual_delivery": "Actual Delivery",
"actual_in": "Actual In", "actual_in": "Actual In",
"acv_amount": "ACV Amount", "acv_amount": "ACV Amount",
"admin_clerk": "Admin Clerk",
"adjustment_bottom_line": "Adjustments", "adjustment_bottom_line": "Adjustments",
"adjustmenthours": "Adjustment Hours", "adjustmenthours": "Adjustment Hours",
"admin_clerk": "Admin Clerk",
"alt_transport": "Alt. Trans.", "alt_transport": "Alt. Trans.",
"area_of_damage_impact": { "area_of_damage_impact": {
"10": "Left Front Side", "10": "Left Front Side",
@@ -1783,6 +1786,7 @@
"depreciation_taxes": "Betterment/Depreciation/Taxes", "depreciation_taxes": "Betterment/Depreciation/Taxes",
"dms": { "dms": {
"address": "Customer Address", "address": "Customer Address",
"advisor": "Advisor #",
"amount": "Amount", "amount": "Amount",
"center": "Center", "center": "Center",
"control_type": { "control_type": {
@@ -1795,12 +1799,13 @@
"dms_model_override": "Override DMS Make/Model", "dms_model_override": "Override DMS Make/Model",
"dms_unsold": "New, Unsold Vehicle", "dms_unsold": "New, Unsold Vehicle",
"dms_wip_acctnumber": "Cost WIP DMS Acct #", "dms_wip_acctnumber": "Cost WIP DMS Acct #",
"first_name": "First Name",
"id": "DMS ID", "id": "DMS ID",
"inservicedate": "In Service Date", "inservicedate": "In Service Date",
"journal": "Journal #", "journal": "Journal #",
"make_override": "Make Override", "last_name": "Last Name",
"advisor": "Advisor #",
"lines": "Posting Lines", "lines": "Posting Lines",
"make_override": "Make Override",
"name1": "Customer Name", "name1": "Customer Name",
"payer": { "payer": {
"amount": "Amount", "amount": "Amount",
@@ -1941,7 +1946,7 @@
"amount": "Amount", "amount": "Amount",
"name": "Name" "name": "Name"
}, },
"queued_for_parts": "Queued for Parts", "queued_for_parts": "Queued",
"rate_ats": "ATS Rate", "rate_ats": "ATS Rate",
"rate_ats_flat": "ATS Flat Rate", "rate_ats_flat": "ATS Flat Rate",
"rate_la1": "LA1", "rate_la1": "LA1",
@@ -2443,6 +2448,7 @@
"labels": { "labels": {
"addlabel": "Add a label to this conversation.", "addlabel": "Add a label to this conversation.",
"archive": "Archive", "archive": "Archive",
"mark_unread": "Mark as unread",
"maxtenimages": "You can only select up to a maximum of 10 images at a time.", "maxtenimages": "You can only select up to a maximum of 10 images at a time.",
"messaging": "Messaging", "messaging": "Messaging",
"no_consent": "Opted-out", "no_consent": "Opted-out",
@@ -2455,8 +2461,7 @@
"selectmedia": "Select Media", "selectmedia": "Select Media",
"sentby": "Sent by {{by}} at {{time}}", "sentby": "Sent by {{by}} at {{time}}",
"typeamessage": "Send a message...", "typeamessage": "Send a message...",
"unarchive": "Unarchive", "unarchive": "Unarchive"
"mark_unread": "Mark as unread"
}, },
"render": { "render": {
"conversation_list": "Conversation List" "conversation_list": "Conversation List"
@@ -2610,20 +2615,20 @@
"name": "Owner Details" "name": "Owner Details"
}, },
"labels": { "labels": {
"cell": "Cell",
"create_new": "Create a new owner record.", "create_new": "Create a new owner record.",
"deleteconfirm": "Are you sure you want to delete this owner? This cannot be undone.", "deleteconfirm": "Are you sure you want to delete this owner? This cannot be undone.",
"email": "Email",
"existing_owners": "Existing Owners", "existing_owners": "Existing Owners",
"fromclaim": "Current Claim", "fromclaim": "Current Claim",
"fromowner": "Historical Owner Record", "fromowner": "Historical Owner Record",
"relatedjobs": "Related Jobs",
"updateowner": "Update Owner",
"work": "Work",
"home": "Home", "home": "Home",
"cell": "Cell",
"other": "Other", "other": "Other",
"email": "Email",
"phone": "Phone", "phone": "Phone",
"sms": "SMS" "relatedjobs": "Related Jobs",
"sms": "SMS",
"updateowner": "Update Owner",
"work": "Work"
}, },
"successes": { "successes": {
"delete": "Owner deleted successfully.", "delete": "Owner deleted successfully.",
@@ -2634,6 +2639,10 @@
"actions": { "actions": {
"order": "Order Parts", "order": "Order Parts",
"orderinhouse": "Order as In House" "orderinhouse": "Order as In House"
},
"labels": {
"view_counts_only": "View Parts Counts Only",
"view_timestamps": "Show timestamps"
} }
}, },
"parts_dispatch": { "parts_dispatch": {
@@ -2983,8 +2992,6 @@
"settings": "Error saving board settings: {{error}}" "settings": "Error saving board settings: {{error}}"
}, },
"labels": { "labels": {
"click_for_statuses": "Click to view parts statuses",
"partsreceived": "Parts Received",
"actual_in": "Actual In", "actual_in": "Actual In",
"addnewprofile": "Add New Profile", "addnewprofile": "Add New Profile",
"alert": "Alert", "alert": "Alert",
@@ -3003,6 +3010,7 @@
"card_size": "Card Size", "card_size": "Card Size",
"cardcolor": "Colored Cards", "cardcolor": "Colored Cards",
"cardsettings": "Card Settings", "cardsettings": "Card Settings",
"click_for_statuses": "Click to view parts statuses",
"clm_no": "Claim Number", "clm_no": "Claim Number",
"comment": "Comment", "comment": "Comment",
"compact": "Compact Cards", "compact": "Compact Cards",
@@ -3023,6 +3031,7 @@
"orientation": "Board Orientation", "orientation": "Board Orientation",
"ownr_nm": "Customer Name", "ownr_nm": "Customer Name",
"paintpriority": "P/P", "paintpriority": "P/P",
"partsreceived": "Parts Received",
"partsstatus": "Parts Status", "partsstatus": "Parts Status",
"production_note": "Production Note", "production_note": "Production Note",
"refinishhours": "R", "refinishhours": "R",
@@ -3569,17 +3578,12 @@
} }
}, },
"titles": { "titles": {
"parts_settings": "Parts Management Settings | {{app}}",
"simplified-parts-jobs": "Parts Management | {{app}}",
"accounting-payables": "Payables | {{app}}", "accounting-payables": "Payables | {{app}}",
"accounting-payments": "Payments | {{app}}", "accounting-payments": "Payments | {{app}}",
"accounting-receivables": "Receivables | {{app}}", "accounting-receivables": "Receivables | {{app}}",
"all_tasks": "All Tasks", "all_tasks": "All Tasks | {{app}}",
"app": "", "app": "",
"bc": { "bc": {
"simplified-parts-jobs": "Jobs",
"parts": "Parts",
"parts_settings": "Settings",
"accounting-payables": "Payables", "accounting-payables": "Payables",
"accounting-payments": "Payments", "accounting-payments": "Payments",
"accounting-receivables": "Receivables", "accounting-receivables": "Receivables",
@@ -3611,7 +3615,9 @@
"my_tasks": "My Tasks", "my_tasks": "My Tasks",
"owner-detail": "{{name}}", "owner-detail": "{{name}}",
"owners": "Owners", "owners": "Owners",
"parts": "Parts",
"parts-queue": "Parts Queue", "parts-queue": "Parts Queue",
"parts_settings": "Settings",
"payments-all": "All Payments", "payments-all": "All Payments",
"phonebook": "Phonebook", "phonebook": "Phonebook",
"productionboard": "Production Board - Visual", "productionboard": "Production Board - Visual",
@@ -3623,6 +3629,7 @@
"shop-csi": "CSI Responses", "shop-csi": "CSI Responses",
"shop-templates": "Shop Templates", "shop-templates": "Shop Templates",
"shop-vendors": "Vendors", "shop-vendors": "Vendors",
"simplified-parts-jobs": "Jobs",
"tasks": "Tasks", "tasks": "Tasks",
"temporarydocs": "Temporary Documents", "temporarydocs": "Temporary Documents",
"timetickets": "Time Tickets", "timetickets": "Time Tickets",
@@ -3655,10 +3662,12 @@
"jobsdetail": "Job {{ro_number}} | {{app}}", "jobsdetail": "Job {{ro_number}} | {{app}}",
"jobsdocuments": "Job Documents {{ro_number}} | {{app}}", "jobsdocuments": "Job Documents {{ro_number}} | {{app}}",
"manageroot": "Home | {{app}}", "manageroot": "Home | {{app}}",
"my_tasks": "My Tasks", "my_tasks": "My Tasks | {{app}}",
"owners": "All Owners | {{app}}", "owners": "All Owners | {{app}}",
"owners-detail": "{{name}} | {{app}}", "owners-detail": "{{name}} | {{app}}",
"parts": "",
"parts-queue": "Parts Queue | {{app}}", "parts-queue": "Parts Queue | {{app}}",
"parts_settings": "Parts Management Settings | {{app}}",
"payments-all": "Payments | {{app}}", "payments-all": "Payments | {{app}}",
"phonebook": "Phonebook | {{app}}", "phonebook": "Phonebook | {{app}}",
"productionboard": "Production Board - Visual | {{app}}", "productionboard": "Production Board - Visual | {{app}}",
@@ -3674,6 +3683,7 @@
"shop-csi": "CSI Responses | {{app}}", "shop-csi": "CSI Responses | {{app}}",
"shop-templates": "Shop Templates | {{app}}", "shop-templates": "Shop Templates | {{app}}",
"shop_vendors": "Vendors | {{app}}", "shop_vendors": "Vendors | {{app}}",
"simplified-parts-jobs": "Parts Management | {{app}}",
"tasks": "Tasks", "tasks": "Tasks",
"techconsole": "Technician Console | {{app}}", "techconsole": "Technician Console | {{app}}",
"techjobclock": "Technician Job Clock | {{app}}", "techjobclock": "Technician Job Clock | {{app}}",
@@ -3834,10 +3844,10 @@
"user": { "user": {
"actions": { "actions": {
"changepassword": "Change Password", "changepassword": "Change Password",
"signout": "Sign Out", "dark_theme": "Switch to Dark Theme",
"updateprofile": "Update Profile",
"light_theme": "Switch to Light Theme", "light_theme": "Switch to Light Theme",
"dark_theme": "Switch to Dark Theme" "signout": "Sign Out",
"updateprofile": "Update Profile"
}, },
"errors": { "errors": {
"updating": "Error updating user or association {{message}}" "updating": "Error updating user or association {{message}}"
@@ -3851,14 +3861,14 @@
"labels": { "labels": {
"actions": "Actions", "actions": "Actions",
"changepassword": "Change Password", "changepassword": "Change Password",
"profileinfo": "Profile Info",
"user_settings": "User Settings",
"play_sound_for_new_messages": "Play a sound for new messages",
"notification_sound_on": "Sound is ON",
"notification_sound_off": "Sound is OFF",
"notification_sound_enabled": "Notification sound enabled",
"notification_sound_disabled": "Notification sound disabled", "notification_sound_disabled": "Notification sound disabled",
"notification_sound_help": "Toggle the ding for incoming chat messages." "notification_sound_enabled": "Notification sound enabled",
"notification_sound_help": "Toggle the ding for incoming chat messages.",
"notification_sound_off": "Sound is OFF",
"notification_sound_on": "Sound is ON",
"play_sound_for_new_messages": "Play a sound for new messages",
"profileinfo": "Profile Info",
"user_settings": "User Settings"
}, },
"successess": { "successess": {
"passwordchanged": "Password changed successfully. " "passwordchanged": "Password changed successfully. "

View File

@@ -48,6 +48,7 @@
"arrivedon": "Llegado el:", "arrivedon": "Llegado el:",
"arrivingjobs": "", "arrivingjobs": "",
"blocked": "", "blocked": "",
"bp": "",
"cancelledappointment": "Cita cancelada para:", "cancelledappointment": "Cita cancelada para:",
"completingjobs": "", "completingjobs": "",
"dataconsistency": "", "dataconsistency": "",
@@ -59,18 +60,17 @@
"noarrivingjobs": "", "noarrivingjobs": "",
"nocompletingjobs": "", "nocompletingjobs": "",
"nodateselected": "No se ha seleccionado ninguna fecha.", "nodateselected": "No se ha seleccionado ninguna fecha.",
"owner": "",
"priorappointments": "Nombramientos previos", "priorappointments": "Nombramientos previos",
"reminder": "", "reminder": "",
"ro_number": "",
"scheduled_completion": "",
"scheduledfor": "Cita programada para:", "scheduledfor": "Cita programada para:",
"severalerrorsfound": "", "severalerrorsfound": "",
"smartscheduling": "", "smartscheduling": "",
"smspaymentreminder": "", "smspaymentreminder": "",
"suggesteddates": "", "suggesteddates": "",
"ro_number": "", "vehicle": ""
"owner": "",
"vehicle": "",
"bp": "",
"scheduled_completion": ""
}, },
"successes": { "successes": {
"canceled": "Cita cancelada con éxito.", "canceled": "Cita cancelada con éxito.",
@@ -90,6 +90,11 @@
"actions": "Comportamiento" "actions": "Comportamiento"
} }
}, },
"audio": {
"manager": {
"description": ""
}
},
"audit": { "audit": {
"fields": { "fields": {
"cc": "", "cc": "",
@@ -149,11 +154,6 @@
"tasks_updated": "" "tasks_updated": ""
} }
}, },
"audio": {
"manager": {
"description": ""
}
},
"billlines": { "billlines": {
"actions": { "actions": {
"newline": "" "newline": ""
@@ -281,9 +281,9 @@
}, },
"errors": { "errors": {
"creatingdefaultview": "", "creatingdefaultview": "",
"duplicate_insurance_company": "",
"loading": "No se pueden cargar los detalles de la tienda. Por favor llame al soporte técnico.", "loading": "No se pueden cargar los detalles de la tienda. Por favor llame al soporte técnico.",
"saving": "", "saving": ""
"duplicate_insurance_company": ""
}, },
"fields": { "fields": {
"ReceivableCustomField": "", "ReceivableCustomField": "",
@@ -564,21 +564,18 @@
"responsibilitycenter_tax_tier": "", "responsibilitycenter_tax_tier": "",
"responsibilitycenter_tax_type": "", "responsibilitycenter_tax_type": "",
"responsibilitycenters": { "responsibilitycenters": {
"gogcode": "",
"item_type": "Item Type",
"item_type_gog": "",
"item_type_paint": "",
"item_type_freight": "",
"taxable_flag": "",
"taxable": "",
"nontaxable": "",
"ap": "", "ap": "",
"ar": "", "ar": "",
"ats": "", "ats": "",
"federal_tax": "", "federal_tax": "",
"federal_tax_itc": "", "federal_tax_itc": "",
"gogcode": "",
"gst_override": "", "gst_override": "",
"invoiceexemptcode": "", "invoiceexemptcode": "",
"item_type": "Item Type",
"item_type_freight": "",
"item_type_gog": "",
"item_type_paint": "",
"itemexemptcode": "", "itemexemptcode": "",
"la1": "", "la1": "",
"la2": "", "la2": "",
@@ -597,6 +594,7 @@
"local_tax": "", "local_tax": "",
"mapa": "", "mapa": "",
"mash": "", "mash": "",
"nontaxable": "",
"paa": "", "paa": "",
"pac": "", "pac": "",
"pag": "", "pag": "",
@@ -617,6 +615,8 @@
"state": "" "state": ""
}, },
"state_tax": "", "state_tax": "",
"taxable": "",
"taxable_flag": "",
"tow": "" "tow": ""
}, },
"schedule_end_time": "", "schedule_end_time": "",
@@ -678,8 +678,6 @@
"zip_post": "" "zip_post": ""
}, },
"labels": { "labels": {
"parts_shop_management": "",
"parts_vendor_management": "",
"2tiername": "", "2tiername": "",
"2tiersetup": "", "2tiersetup": "",
"2tiersource": "", "2tiersource": "",
@@ -702,11 +700,11 @@
"payers": "" "payers": ""
}, },
"cdk_dealerid": "", "cdk_dealerid": "",
"rr_dealerid": "",
"costsmapping": "", "costsmapping": "",
"dms_allocations": "", "dms_allocations": "",
"pbs_serialnumber": "", "pbs_serialnumber": "",
"profitsmapping": "", "profitsmapping": "",
"rr_dealerid": "",
"title": "" "title": ""
}, },
"emaillater": "", "emaillater": "",
@@ -733,6 +731,8 @@
"followers": "" "followers": ""
}, },
"orderstatuses": "", "orderstatuses": "",
"parts_shop_management": "",
"parts_vendor_management": "",
"partslocations": "", "partslocations": "",
"partsscan": "", "partsscan": "",
"printlater": "", "printlater": "",
@@ -1247,6 +1247,7 @@
"login": "", "login": "",
"next": "", "next": "",
"ok": "", "ok": "",
"optional": "",
"previous": "", "previous": "",
"print": "", "print": "",
"refresh": "", "refresh": "",
@@ -1257,6 +1258,7 @@
"save": "Salvar", "save": "Salvar",
"saveandnew": "", "saveandnew": "",
"saveas": "", "saveas": "",
"select": "",
"selectall": "", "selectall": "",
"send": "", "send": "",
"sendbysms": "", "sendbysms": "",
@@ -1286,9 +1288,8 @@
"vehicle": "" "vehicle": ""
}, },
"labels": { "labels": {
"selected": "", "apply": "",
"actions": "Comportamiento", "actions": "Comportamiento",
"settings": "",
"areyousure": "", "areyousure": "",
"barcode": "código de barras", "barcode": "código de barras",
"cancel": "", "cancel": "",
@@ -1341,8 +1342,10 @@
"search": "Buscar...", "search": "Buscar...",
"searchresults": "", "searchresults": "",
"selectdate": "", "selectdate": "",
"selected": "",
"sendagain": "", "sendagain": "",
"sendby": "", "sendby": "",
"settings": "",
"signin": "", "signin": "",
"sms": "", "sms": "",
"status": "", "status": "",
@@ -1585,13 +1588,13 @@
"labels": { "labels": {
"adjustmenttobeadded": "", "adjustmenttobeadded": "",
"billref": "", "billref": "",
"bulk_location_help": "",
"convertedtolabor": "", "convertedtolabor": "",
"edit": "Línea de edición", "edit": "Línea de edición",
"ioucreated": "", "ioucreated": "",
"new": "Nueva línea", "new": "Nueva línea",
"nostatus": "", "nostatus": "",
"presets": "", "presets": ""
"bulk_location_help": ""
}, },
"successes": { "successes": {
"created": "", "created": "",
@@ -1674,7 +1677,9 @@
"deleted": "Error al eliminar el trabajo.", "deleted": "Error al eliminar el trabajo.",
"exporting": "", "exporting": "",
"exporting-partner": "", "exporting-partner": "",
"invalidjoblines": "",
"invoicing": "", "invoicing": "",
"missingjoblineids": "",
"noaccess": "Este trabajo no existe o no tiene acceso a él.", "noaccess": "Este trabajo no existe o no tiene acceso a él.",
"nodamage": "", "nodamage": "",
"nodates": "No hay fechas especificadas para este trabajo.", "nodates": "No hay fechas especificadas para este trabajo.",
@@ -1698,8 +1703,8 @@
"actual_in": "Real en", "actual_in": "Real en",
"acv_amount": "", "acv_amount": "",
"adjustment_bottom_line": "Ajustes", "adjustment_bottom_line": "Ajustes",
"admin_clerk": "",
"adjustmenthours": "", "adjustmenthours": "",
"admin_clerk": "",
"alt_transport": "", "alt_transport": "",
"area_of_damage_impact": { "area_of_damage_impact": {
"10": "", "10": "",
@@ -1781,6 +1786,7 @@
"depreciation_taxes": "Depreciación / Impuestos", "depreciation_taxes": "Depreciación / Impuestos",
"dms": { "dms": {
"address": "", "address": "",
"advisor": "",
"amount": "", "amount": "",
"center": "", "center": "",
"control_type": { "control_type": {
@@ -1791,21 +1797,23 @@
"dms_make": "", "dms_make": "",
"dms_model": "", "dms_model": "",
"dms_model_override": "", "dms_model_override": "",
"make_override": "",
"advisor": "",
"dms_unsold": "", "dms_unsold": "",
"dms_wip_acctnumber": "", "dms_wip_acctnumber": "",
"first_name": "",
"id": "", "id": "",
"inservicedate": "", "inservicedate": "",
"journal": "", "journal": "",
"last_name": "",
"lines": "", "lines": "",
"make_override": "",
"name1": "", "name1": "",
"payer": { "payer": {
"amount": "", "amount": "",
"control_type": "", "control_type": "",
"controlnumber": "", "controlnumber": "",
"dms_acctnumber": "", "dms_acctnumber": "",
"name": "" "name": "",
"payer_type": ""
}, },
"sale": "", "sale": "",
"sale_dms_acctnumber": "", "sale_dms_acctnumber": "",
@@ -2440,6 +2448,7 @@
"labels": { "labels": {
"addlabel": "", "addlabel": "",
"archive": "", "archive": "",
"mark_unread": "",
"maxtenimages": "", "maxtenimages": "",
"messaging": "Mensajería", "messaging": "Mensajería",
"no_consent": "", "no_consent": "",
@@ -2452,8 +2461,7 @@
"selectmedia": "", "selectmedia": "",
"sentby": "", "sentby": "",
"typeamessage": "Enviar un mensaje...", "typeamessage": "Enviar un mensaje...",
"unarchive": "", "unarchive": ""
"mark_unread": ""
}, },
"render": { "render": {
"conversation_list": "" "conversation_list": ""
@@ -2607,20 +2615,20 @@
"name": "" "name": ""
}, },
"labels": { "labels": {
"cell": "",
"create_new": "Crea un nuevo registro de propietario.", "create_new": "Crea un nuevo registro de propietario.",
"deleteconfirm": "", "deleteconfirm": "",
"email": "",
"existing_owners": "Propietarios existentes", "existing_owners": "Propietarios existentes",
"fromclaim": "", "fromclaim": "",
"fromowner": "", "fromowner": "",
"relatedjobs": "",
"updateowner": "",
"work": "",
"home": "", "home": "",
"cell": "",
"other": "", "other": "",
"email": "",
"phone": "", "phone": "",
"sms": "" "relatedjobs": "",
"sms": "",
"updateowner": "",
"work": ""
}, },
"successes": { "successes": {
"delete": "", "delete": "",
@@ -2631,6 +2639,10 @@
"actions": { "actions": {
"order": "Pedido de piezas", "order": "Pedido de piezas",
"orderinhouse": "" "orderinhouse": ""
},
"labels": {
"view_counts_only": "",
"view_timestamps": ""
} }
}, },
"parts_dispatch": { "parts_dispatch": {
@@ -2980,8 +2992,6 @@
"settings": "" "settings": ""
}, },
"labels": { "labels": {
"click_for_statuses": "",
"partsreceived": "",
"actual_in": "", "actual_in": "",
"addnewprofile": "", "addnewprofile": "",
"alert": "", "alert": "",
@@ -3000,6 +3010,7 @@
"card_size": "", "card_size": "",
"cardcolor": "", "cardcolor": "",
"cardsettings": "", "cardsettings": "",
"click_for_statuses": "",
"clm_no": "", "clm_no": "",
"comment": "", "comment": "",
"compact": "", "compact": "",
@@ -3020,6 +3031,7 @@
"orientation": "", "orientation": "",
"ownr_nm": "", "ownr_nm": "",
"paintpriority": "", "paintpriority": "",
"partsreceived": "",
"partsstatus": "", "partsstatus": "",
"production_note": "", "production_note": "",
"refinishhours": "", "refinishhours": "",
@@ -3566,18 +3578,12 @@
} }
}, },
"titles": { "titles": {
"simplified-parts-jobs": "",
"parts": "",
"parts_settings": "",
"accounting-payables": "", "accounting-payables": "",
"accounting-payments": "", "accounting-payments": "",
"accounting-receivables": "", "accounting-receivables": "",
"all_tasks": "", "all_tasks": "",
"app": "", "app": "",
"bc": { "bc": {
"simplified-parts-jobs": "",
"parts": "",
"parts_settings": "",
"accounting-payables": "", "accounting-payables": "",
"accounting-payments": "", "accounting-payments": "",
"accounting-receivables": "", "accounting-receivables": "",
@@ -3609,7 +3615,9 @@
"my_tasks": "", "my_tasks": "",
"owner-detail": "", "owner-detail": "",
"owners": "", "owners": "",
"parts": "",
"parts-queue": "", "parts-queue": "",
"parts_settings": "",
"payments-all": "", "payments-all": "",
"phonebook": "", "phonebook": "",
"productionboard": "", "productionboard": "",
@@ -3621,6 +3629,7 @@
"shop-csi": "", "shop-csi": "",
"shop-templates": "", "shop-templates": "",
"shop-vendors": "", "shop-vendors": "",
"simplified-parts-jobs": "",
"tasks": "", "tasks": "",
"temporarydocs": "", "temporarydocs": "",
"timetickets": "", "timetickets": "",
@@ -3656,7 +3665,9 @@
"my_tasks": "", "my_tasks": "",
"owners": "Todos los propietarios | {{app}}", "owners": "Todos los propietarios | {{app}}",
"owners-detail": "", "owners-detail": "",
"parts": "",
"parts-queue": "", "parts-queue": "",
"parts_settings": "",
"payments-all": "", "payments-all": "",
"phonebook": "", "phonebook": "",
"productionboard": "", "productionboard": "",
@@ -3672,6 +3683,7 @@
"shop-csi": "", "shop-csi": "",
"shop-templates": "", "shop-templates": "",
"shop_vendors": "Vendedores | {{app}}", "shop_vendors": "Vendedores | {{app}}",
"simplified-parts-jobs": "",
"tasks": "", "tasks": "",
"techconsole": "{{app}}", "techconsole": "{{app}}",
"techjobclock": "{{app}}", "techjobclock": "{{app}}",
@@ -3832,10 +3844,10 @@
"user": { "user": {
"actions": { "actions": {
"changepassword": "", "changepassword": "",
"signout": "desconectar", "dark_theme": "",
"updateprofile": "Actualización del perfil",
"light_theme": "", "light_theme": "",
"dark_theme": "" "signout": "desconectar",
"updateprofile": "Actualización del perfil"
}, },
"errors": { "errors": {
"updating": "" "updating": ""
@@ -3849,14 +3861,14 @@
"labels": { "labels": {
"actions": "", "actions": "",
"changepassword": "", "changepassword": "",
"profileinfo": "",
"user_settings": "",
"play_sound_for_new_messages": "",
"notification_sound_on": "",
"notification_sound_off": "",
"notification_sound_enabled": "",
"notification_sound_disabled": "", "notification_sound_disabled": "",
"notification_sound_help": "" "notification_sound_enabled": "",
"notification_sound_help": "",
"notification_sound_off": "",
"notification_sound_on": "",
"play_sound_for_new_messages": "",
"profileinfo": "",
"user_settings": ""
}, },
"successess": { "successess": {
"passwordchanged": "" "passwordchanged": ""

View File

@@ -48,6 +48,7 @@
"arrivedon": "Arrivé le:", "arrivedon": "Arrivé le:",
"arrivingjobs": "", "arrivingjobs": "",
"blocked": "", "blocked": "",
"bp": "",
"cancelledappointment": "Rendez-vous annulé pour:", "cancelledappointment": "Rendez-vous annulé pour:",
"completingjobs": "", "completingjobs": "",
"dataconsistency": "", "dataconsistency": "",
@@ -59,18 +60,17 @@
"noarrivingjobs": "", "noarrivingjobs": "",
"nocompletingjobs": "", "nocompletingjobs": "",
"nodateselected": "Aucune date n'a été sélectionnée.", "nodateselected": "Aucune date n'a été sélectionnée.",
"owner": "",
"priorappointments": "Rendez-vous précédents", "priorappointments": "Rendez-vous précédents",
"reminder": "", "reminder": "",
"ro_number": "",
"scheduled_completion": "",
"scheduledfor": "Rendez-vous prévu pour:", "scheduledfor": "Rendez-vous prévu pour:",
"severalerrorsfound": "", "severalerrorsfound": "",
"smartscheduling": "", "smartscheduling": "",
"smspaymentreminder": "", "smspaymentreminder": "",
"suggesteddates": "", "suggesteddates": "",
"ro_number": "", "vehicle": ""
"owner": "",
"vehicle": "",
"bp": "",
"scheduled_completion": ""
}, },
"successes": { "successes": {
"canceled": "Rendez-vous annulé avec succès.", "canceled": "Rendez-vous annulé avec succès.",
@@ -90,6 +90,11 @@
"actions": "actes" "actions": "actes"
} }
}, },
"audio": {
"manager": {
"description": ""
}
},
"audit": { "audit": {
"fields": { "fields": {
"cc": "", "cc": "",
@@ -149,11 +154,6 @@
"tasks_updated": "" "tasks_updated": ""
} }
}, },
"audio": {
"manager": {
"description": ""
}
},
"billlines": { "billlines": {
"actions": { "actions": {
"newline": "" "newline": ""
@@ -281,9 +281,9 @@
}, },
"errors": { "errors": {
"creatingdefaultview": "", "creatingdefaultview": "",
"duplicate_insurance_company": "",
"loading": "Impossible de charger les détails de la boutique. Veuillez appeler le support technique.", "loading": "Impossible de charger les détails de la boutique. Veuillez appeler le support technique.",
"saving": "", "saving": ""
"duplicate_insurance_company": ""
}, },
"fields": { "fields": {
"ReceivableCustomField": "", "ReceivableCustomField": "",
@@ -564,21 +564,18 @@
"responsibilitycenter_tax_tier": "", "responsibilitycenter_tax_tier": "",
"responsibilitycenter_tax_type": "", "responsibilitycenter_tax_type": "",
"responsibilitycenters": { "responsibilitycenters": {
"gogcode": "",
"item_type": "Item Type",
"item_type_gog": "",
"item_type_paint": "",
"item_type_freight": "",
"taxable_flag": "",
"taxable": "",
"nontaxable": "",
"ap": "", "ap": "",
"ar": "", "ar": "",
"ats": "", "ats": "",
"federal_tax": "", "federal_tax": "",
"federal_tax_itc": "", "federal_tax_itc": "",
"gogcode": "",
"gst_override": "", "gst_override": "",
"invoiceexemptcode": "", "invoiceexemptcode": "",
"item_type": "Item Type",
"item_type_freight": "",
"item_type_gog": "",
"item_type_paint": "",
"itemexemptcode": "", "itemexemptcode": "",
"la1": "", "la1": "",
"la2": "", "la2": "",
@@ -597,6 +594,7 @@
"local_tax": "", "local_tax": "",
"mapa": "", "mapa": "",
"mash": "", "mash": "",
"nontaxable": "",
"paa": "", "paa": "",
"pac": "", "pac": "",
"pag": "", "pag": "",
@@ -617,6 +615,8 @@
"state": "" "state": ""
}, },
"state_tax": "", "state_tax": "",
"taxable": "",
"taxable_flag": "",
"tow": "" "tow": ""
}, },
"schedule_end_time": "", "schedule_end_time": "",
@@ -678,8 +678,6 @@
"zip_post": "" "zip_post": ""
}, },
"labels": { "labels": {
"parts_shop_management": "",
"parts_vendor_management": "",
"2tiername": "", "2tiername": "",
"2tiersetup": "", "2tiersetup": "",
"2tiersource": "", "2tiersource": "",
@@ -702,11 +700,11 @@
"payers": "" "payers": ""
}, },
"cdk_dealerid": "", "cdk_dealerid": "",
"rr_dealerid": "",
"costsmapping": "", "costsmapping": "",
"dms_allocations": "", "dms_allocations": "",
"pbs_serialnumber": "", "pbs_serialnumber": "",
"profitsmapping": "", "profitsmapping": "",
"rr_dealerid": "",
"title": "" "title": ""
}, },
"emaillater": "", "emaillater": "",
@@ -733,6 +731,8 @@
"followers": "" "followers": ""
}, },
"orderstatuses": "", "orderstatuses": "",
"parts_shop_management": "",
"parts_vendor_management": "",
"partslocations": "", "partslocations": "",
"partsscan": "", "partsscan": "",
"printlater": "", "printlater": "",
@@ -1247,6 +1247,7 @@
"login": "", "login": "",
"next": "", "next": "",
"ok": "", "ok": "",
"optional": "",
"previous": "", "previous": "",
"print": "", "print": "",
"refresh": "", "refresh": "",
@@ -1257,6 +1258,7 @@
"save": "sauvegarder", "save": "sauvegarder",
"saveandnew": "", "saveandnew": "",
"saveas": "", "saveas": "",
"select": "",
"selectall": "", "selectall": "",
"send": "", "send": "",
"sendbysms": "", "sendbysms": "",
@@ -1286,8 +1288,7 @@
"vehicle": "" "vehicle": ""
}, },
"labels": { "labels": {
"selected": "", "apply": "",
"settings": "",
"actions": "actes", "actions": "actes",
"areyousure": "", "areyousure": "",
"barcode": "code à barre", "barcode": "code à barre",
@@ -1341,8 +1342,10 @@
"search": "Chercher...", "search": "Chercher...",
"searchresults": "", "searchresults": "",
"selectdate": "", "selectdate": "",
"selected": "",
"sendagain": "", "sendagain": "",
"sendby": "", "sendby": "",
"settings": "",
"signin": "", "signin": "",
"sms": "", "sms": "",
"status": "", "status": "",
@@ -1585,13 +1588,13 @@
"labels": { "labels": {
"adjustmenttobeadded": "", "adjustmenttobeadded": "",
"billref": "", "billref": "",
"bulk_location_help": "",
"convertedtolabor": "", "convertedtolabor": "",
"edit": "Ligne d'édition", "edit": "Ligne d'édition",
"ioucreated": "", "ioucreated": "",
"new": "Nouvelle ligne", "new": "Nouvelle ligne",
"nostatus": "", "nostatus": "",
"presets": "", "presets": ""
"bulk_location_help": ""
}, },
"successes": { "successes": {
"created": "", "created": "",
@@ -1674,7 +1677,9 @@
"deleted": "Erreur lors de la suppression du travail.", "deleted": "Erreur lors de la suppression du travail.",
"exporting": "", "exporting": "",
"exporting-partner": "", "exporting-partner": "",
"invalidjoblines": "",
"invoicing": "", "invoicing": "",
"missingjoblineids": "",
"noaccess": "Ce travail n'existe pas ou vous n'y avez pas accès.", "noaccess": "Ce travail n'existe pas ou vous n'y avez pas accès.",
"nodamage": "", "nodamage": "",
"nodates": "Aucune date spécifiée pour ce travail.", "nodates": "Aucune date spécifiée pour ce travail.",
@@ -1697,9 +1702,9 @@
"actual_delivery": "Livraison réelle", "actual_delivery": "Livraison réelle",
"actual_in": "En réel", "actual_in": "En réel",
"acv_amount": "", "acv_amount": "",
"admin_clerk": "",
"adjustment_bottom_line": "Ajustements", "adjustment_bottom_line": "Ajustements",
"adjustmenthours": "", "adjustmenthours": "",
"admin_clerk": "",
"alt_transport": "", "alt_transport": "",
"area_of_damage_impact": { "area_of_damage_impact": {
"10": "", "10": "",
@@ -1781,6 +1786,7 @@
"depreciation_taxes": "Amortissement / taxes", "depreciation_taxes": "Amortissement / taxes",
"dms": { "dms": {
"address": "", "address": "",
"advisor": "",
"amount": "", "amount": "",
"center": "", "center": "",
"control_type": { "control_type": {
@@ -1791,21 +1797,23 @@
"dms_make": "", "dms_make": "",
"dms_model": "", "dms_model": "",
"dms_model_override": "", "dms_model_override": "",
"make_override": "",
"advisor": "",
"dms_unsold": "", "dms_unsold": "",
"dms_wip_acctnumber": "", "dms_wip_acctnumber": "",
"first_name": "",
"id": "", "id": "",
"inservicedate": "", "inservicedate": "",
"journal": "", "journal": "",
"last_name": "",
"lines": "", "lines": "",
"make_override": "",
"name1": "", "name1": "",
"payer": { "payer": {
"amount": "", "amount": "",
"control_type": "", "control_type": "",
"controlnumber": "", "controlnumber": "",
"dms_acctnumber": "", "dms_acctnumber": "",
"name": "" "name": "",
"payer_type": ""
}, },
"sale": "", "sale": "",
"sale_dms_acctnumber": "", "sale_dms_acctnumber": "",
@@ -2429,7 +2437,6 @@
"actions": { "actions": {
"link": "", "link": "",
"new": "", "new": "",
"openchat": "" "openchat": ""
}, },
"errors": { "errors": {
@@ -2441,6 +2448,7 @@
"labels": { "labels": {
"addlabel": "", "addlabel": "",
"archive": "", "archive": "",
"mark_unread": "",
"maxtenimages": "", "maxtenimages": "",
"messaging": "Messagerie", "messaging": "Messagerie",
"no_consent": "", "no_consent": "",
@@ -2453,8 +2461,7 @@
"selectmedia": "", "selectmedia": "",
"sentby": "", "sentby": "",
"typeamessage": "Envoyer un message...", "typeamessage": "Envoyer un message...",
"unarchive": "", "unarchive": ""
"mark_unread": ""
}, },
"render": { "render": {
"conversation_list": "" "conversation_list": ""
@@ -2608,20 +2615,20 @@
"name": "" "name": ""
}, },
"labels": { "labels": {
"cell": "",
"create_new": "Créez un nouvel enregistrement de propriétaire.", "create_new": "Créez un nouvel enregistrement de propriétaire.",
"deleteconfirm": "", "deleteconfirm": "",
"email": "",
"existing_owners": "Propriétaires existants", "existing_owners": "Propriétaires existants",
"fromclaim": "", "fromclaim": "",
"fromowner": "", "fromowner": "",
"relatedjobs": "",
"updateowner": "",
"work": "",
"home": "", "home": "",
"cell": "",
"other": "", "other": "",
"email": "",
"phone": "", "phone": "",
"sms": "" "relatedjobs": "",
"sms": "",
"updateowner": "",
"work": ""
}, },
"successes": { "successes": {
"delete": "", "delete": "",
@@ -2632,6 +2639,10 @@
"actions": { "actions": {
"order": "Commander des pièces", "order": "Commander des pièces",
"orderinhouse": "" "orderinhouse": ""
},
"labels": {
"view_counts_only": "",
"view_timestamps": ""
} }
}, },
"parts_dispatch": { "parts_dispatch": {
@@ -2981,8 +2992,6 @@
"settings": "" "settings": ""
}, },
"labels": { "labels": {
"click_for_statuses": "",
"partsreceived": "",
"actual_in": "", "actual_in": "",
"addnewprofile": "", "addnewprofile": "",
"alert": "", "alert": "",
@@ -3001,6 +3010,7 @@
"card_size": "", "card_size": "",
"cardcolor": "", "cardcolor": "",
"cardsettings": "", "cardsettings": "",
"click_for_statuses": "",
"clm_no": "", "clm_no": "",
"comment": "", "comment": "",
"compact": "", "compact": "",
@@ -3021,6 +3031,7 @@
"orientation": "", "orientation": "",
"ownr_nm": "", "ownr_nm": "",
"paintpriority": "", "paintpriority": "",
"partsreceived": "",
"partsstatus": "", "partsstatus": "",
"production_note": "", "production_note": "",
"refinishhours": "", "refinishhours": "",
@@ -3567,18 +3578,12 @@
} }
}, },
"titles": { "titles": {
"simplified-parts-jobs": "",
"parts": "",
"parts_settings": "",
"accounting-payables": "", "accounting-payables": "",
"accounting-payments": "", "accounting-payments": "",
"accounting-receivables": "", "accounting-receivables": "",
"all_tasks": "", "all_tasks": "",
"app": "", "app": "",
"bc": { "bc": {
"simplified-parts-jobs": "",
"parts": "",
"parts_settings": "",
"accounting-payables": "", "accounting-payables": "",
"accounting-payments": "", "accounting-payments": "",
"accounting-receivables": "", "accounting-receivables": "",
@@ -3610,7 +3615,9 @@
"my_tasks": "", "my_tasks": "",
"owner-detail": "", "owner-detail": "",
"owners": "", "owners": "",
"parts": "",
"parts-queue": "", "parts-queue": "",
"parts_settings": "",
"payments-all": "", "payments-all": "",
"phonebook": "", "phonebook": "",
"productionboard": "", "productionboard": "",
@@ -3622,6 +3629,7 @@
"shop-csi": "", "shop-csi": "",
"shop-templates": "", "shop-templates": "",
"shop-vendors": "", "shop-vendors": "",
"simplified-parts-jobs": "",
"tasks": "", "tasks": "",
"temporarydocs": "", "temporarydocs": "",
"timetickets": "", "timetickets": "",
@@ -3657,7 +3665,9 @@
"my_tasks": "", "my_tasks": "",
"owners": "Tous les propriétaires | {{app}}", "owners": "Tous les propriétaires | {{app}}",
"owners-detail": "", "owners-detail": "",
"parts": "",
"parts-queue": "", "parts-queue": "",
"parts_settings": "",
"payments-all": "", "payments-all": "",
"phonebook": "", "phonebook": "",
"productionboard": "", "productionboard": "",
@@ -3673,6 +3683,7 @@
"shop-csi": "", "shop-csi": "",
"shop-templates": "", "shop-templates": "",
"shop_vendors": "Vendeurs | {{app}}", "shop_vendors": "Vendeurs | {{app}}",
"simplified-parts-jobs": "",
"tasks": "", "tasks": "",
"techconsole": "{{app}}", "techconsole": "{{app}}",
"techjobclock": "{{app}}", "techjobclock": "{{app}}",
@@ -3833,10 +3844,10 @@
"user": { "user": {
"actions": { "actions": {
"changepassword": "", "changepassword": "",
"signout": "Déconnexion", "dark_theme": "",
"updateprofile": "Mettre à jour le profil",
"light_theme": "", "light_theme": "",
"dark_theme": "" "signout": "Déconnexion",
"updateprofile": "Mettre à jour le profil"
}, },
"errors": { "errors": {
"updating": "" "updating": ""
@@ -3850,14 +3861,14 @@
"labels": { "labels": {
"actions": "", "actions": "",
"changepassword": "", "changepassword": "",
"profileinfo": "",
"user_settings": "",
"play_sound_for_new_messages": "",
"notification_sound_on": "",
"notification_sound_off": "",
"notification_sound_enabled": "",
"notification_sound_disabled": "", "notification_sound_disabled": "",
"notification_sound_help": "" "notification_sound_enabled": "",
"notification_sound_help": "",
"notification_sound_off": "",
"notification_sound_on": "",
"play_sound_for_new_messages": "",
"profileinfo": "",
"user_settings": ""
}, },
"successess": { "successess": {
"passwordchanged": "" "passwordchanged": ""

View File

@@ -5,8 +5,10 @@ export function DateFormatter(props) {
return props.children ? dayjs(props.children).format(props.includeDay ? "ddd MM/DD/YYYY" : "MM/DD/YYYY") : null; return props.children ? dayjs(props.children).format(props.includeDay ? "ddd MM/DD/YYYY" : "MM/DD/YYYY") : null;
} }
export function DateTimeFormatter(props) { export function DateTimeFormatter({ hideTime, ...props }) {
return props.children ? dayjs(props.children).format(props.format ? props.format : "MM/DD/YYYY hh:mm a") : null; return props.children
? dayjs(props.children).format(props.format ? props.format : `MM/DD/YYYY${hideTime ? "" : " hh:mm a"}`)
: null;
} }
export function DateTimeFormatterFunction(date) { export function DateTimeFormatterFunction(date) {
@@ -17,11 +19,11 @@ export function TimeFormatter(props) {
return props.children ? dayjs(props.children).format(props.format ? props.format : "hh:mm a") : null; return props.children ? dayjs(props.children).format(props.format ? props.format : "hh:mm a") : null;
} }
export function TimeAgoFormatter(props) { export function TimeAgoFormatter({ removeAgoString = false, ...props }) {
const m = dayjs(props.children); const m = dayjs(props.children);
return props.children ? ( return props.children ? (
<Tooltip placement="top" title={m.format("MM/DD/YYY hh:mm A")}> <Tooltip placement="top" title={m.format("MM/DD/YYYY hh:mm A")}>
{m.fromNow()} {m.fromNow(removeAgoString)}
</Tooltip> </Tooltip>
) : null; ) : null;
} }

View File

@@ -248,7 +248,8 @@ const client = new ApolloClient({
watchQuery: { watchQuery: {
fetchPolicy: "network-only", fetchPolicy: "network-only",
nextFetchPolicy: "network-only", nextFetchPolicy: "network-only",
errorPolicy: "ignore" errorPolicy: "ignore",
notifyOnNetworkStatusChange: false
}, },
query: { query: {
fetchPolicy: "network-only", fetchPolicy: "network-only",

View File

@@ -0,0 +1,35 @@
import { lazy } from "react";
/**
* Conditionally uses lazy loading based on environment.
* By default, uses React.lazy in all environments.
* Set VITE_DISABLE_LAZY_LOADING=true to load modules immediately in development (avoids compilation delays).
*
* Usage: const MyComponent = lazyDev(() => import('./MyComponent'));
*/
export const lazyDev = (importFunc) => {
// Check if lazy loading should be disabled (dev only, opt-in via env var)
const disableLazyLoading = import.meta.env.DEV && import.meta.env?.VITE_DISABLE_LAZY_LOADING === "true";
if (!disableLazyLoading) {
// Default behavior: use React.lazy for code splitting
return lazy(importFunc);
}
// Dev mode with lazy loading disabled: load immediately to avoid delays
let Component = null;
const promise = importFunc().then((module) => {
Component = module.default;
});
const LazyDevComponent = (props) => {
if (!Component) {
throw promise; // Suspense will catch this
}
return <Component {...props} />;
};
LazyDevComponent.displayName = "LazyDevComponent";
return LazyDevComponent;
};

View File

@@ -11,6 +11,8 @@ import { VitePWA } from "vite-plugin-pwa";
import InstanceRenderManager from "./src/utils/instanceRenderMgr"; import InstanceRenderManager from "./src/utils/instanceRenderMgr";
import browserslist from "browserslist"; import browserslist from "browserslist";
import { browserslistToTargets } from "lightningcss"; import { browserslistToTargets } from "lightningcss";
import { fileURLToPath } from "url";
import { dirname, resolve } from "path";
process.env.VITE_APP_GIT_SHA_DATE = new Date().toLocaleString("en-US", { timeZone: "America/Los_Angeles" }); process.env.VITE_APP_GIT_SHA_DATE = new Date().toLocaleString("en-US", { timeZone: "America/Los_Angeles" });
const commitHash = child.execSync("git rev-parse HEAD").toString().trimEnd(); const commitHash = child.execSync("git rev-parse HEAD").toString().trimEnd();
@@ -43,13 +45,19 @@ const httpsCerts = {
cert: await fsPromises.readFile("../certs/cert.pem") cert: await fsPromises.readFile("../certs/cert.pem")
}; };
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
export default defineConfig(({ command, mode }) => { export default defineConfig(({ command, mode }) => {
// Only enable React Compiler on build in production/test (keeps dev as fast as possible) // React Compiler is always enabled for production/test builds
// In dev mode, it's enabled by default but can be disabled with VITE_DISABLE_COMPILER_IN_DEV=true
const isBuild = command === "build"; const isBuild = command === "build";
const isTestBuild = const isTestBuild =
mode === "test" || process.env.VITE_APP_IS_TEST === "true" || process.env.VITE_APP_IS_TEST === "1"; mode === "test" || process.env.VITE_APP_IS_TEST === "true" || process.env.VITE_APP_IS_TEST === "1";
const enableReactCompiler = const enableReactCompiler =
process.env.VITE_ENABLE_COMPILER_IN_DEV || (isBuild && (mode === "production" || isTestBuild)); (isBuild && (mode === "production" || isTestBuild)) || // Always enable for prod/test builds
process.env?.VITE_DISABLE_COMPILER_IN_DEV !== "true"; // In dev, enable unless explicitly disabled
logger.info( logger.info(
enableReactCompiler ? chalk.green.bold("React Compiler enabled") : chalk.yellow.bold("React Compiler disabled") enableReactCompiler ? chalk.green.bold("React Compiler enabled") : chalk.yellow.bold("React Compiler disabled")
@@ -57,6 +65,13 @@ export default defineConfig(({ command, mode }) => {
return { return {
base: "/", base: "/",
resolve: {
dedupe: ["styled-components", "react", "react-dom"],
alias: {
// Force all styled-components imports to resolve to the same location (absolute path)
"styled-components": resolve(__dirname, "node_modules/styled-components/dist/styled-components.browser.esm.js")
}
},
plugins: [ plugins: [
ViteEjsPlugin((viteConfig) => ({ env: viteConfig.env })), ViteEjsPlugin((viteConfig) => ({ env: viteConfig.env })),
@@ -243,7 +258,8 @@ export default defineConfig(({ command, mode }) => {
// Strip console/debugger in prod to shrink bundles // Strip console/debugger in prod to shrink bundles
esbuild: { esbuild: {
//drop: ["console", "debugger"] // drop: mode === "production" ? ["console", "debugger"] : [],
legalComments: "none" // Remove license comments in production
}, },
optimizeDeps: { optimizeDeps: {
@@ -265,11 +281,14 @@ export default defineConfig(({ command, mode }) => {
"@firebase/firestore", "@firebase/firestore",
"@firebase/auth", "@firebase/auth",
"@firebase/messaging", "@firebase/messaging",
"@firebase/util" "@firebase/util",
"styled-components"
], ],
esbuildOptions: { esbuildOptions: {
loader: { ".jsx": "jsx", ".tsx": "tsx" } loader: { ".jsx": "jsx", ".tsx": "tsx" }
} },
// Force styled-components to be pre-bundled and deduplicated
force: mode === "development"
}, },
css: { css: {

View File

@@ -38,8 +38,6 @@ services:
condition: service_healthy condition: service_healthy
localstack: localstack:
condition: service_healthy condition: service_healthy
aws-cli:
condition: service_completed_successfully
ports: ports:
- "4001:4000" # Different external port for local access - "4001:4000" # Different external port for local access
volumes: volumes:
@@ -65,8 +63,6 @@ services:
condition: service_healthy condition: service_healthy
localstack: localstack:
condition: service_healthy condition: service_healthy
aws-cli:
condition: service_completed_successfully
ports: ports:
- "4002:4000" # Different external port for local access - "4002:4000" # Different external port for local access
volumes: volumes:
@@ -92,8 +88,6 @@ services:
condition: service_healthy condition: service_healthy
localstack: localstack:
condition: service_healthy condition: service_healthy
aws-cli:
condition: service_completed_successfully
ports: ports:
- "4003:4000" # Different external port for local access - "4003:4000" # Different external port for local access
volumes: volumes:
@@ -156,23 +150,18 @@ services:
# LocalStack # LocalStack
localstack: localstack:
image: localstack/localstack image: localstack/localstack:4.13.1
container_name: localstack container_name: localstack
hostname: localstack hostname: localstack
networks: networks:
- redis-cluster-net - redis-cluster-net
restart: unless-stopped restart: unless-stopped
volumes: volumes:
- ./certs:/tmp/certs:ro # only if your script reads /tmp/certs/...
- ./localstack/init:/etc/localstack/init/ready.d:ro
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
environment: env_file:
- SERVICES=s3,ses,secretsmanager,cloudwatch,logs - .env.localstack.docker
- DEBUG=0
- AWS_ACCESS_KEY_ID=test
- AWS_SECRET_ACCESS_KEY=test
- AWS_DEFAULT_REGION=ca-central-1
- EXTRA_CORS_ALLOWED_HEADERS=Authorization,Content-Type
- EXTRA_CORS_ALLOWED_ORIGINS=*
- EXTRA_CORS_EXPOSE_HEADERS=Authorization,Content-Type
ports: ports:
- "4566:4566" - "4566:4566"
healthcheck: healthcheck:
@@ -182,36 +171,6 @@ services:
retries: 5 retries: 5
start_period: 20s start_period: 20s
# AWS-CLI
aws-cli:
image: amazon/aws-cli
container_name: aws-cli
hostname: aws-cli
networks:
- redis-cluster-net
depends_on:
localstack:
condition: service_healthy
volumes:
- './localstack:/tmp/localstack'
- './certs:/tmp/certs'
environment:
- AWS_ACCESS_KEY_ID=test
- AWS_SECRET_ACCESS_KEY=test
- AWS_DEFAULT_REGION=ca-central-1
entrypoint: /bin/sh -c
command: >
"
aws --endpoint-url=http://localstack:4566 ses verify-domain-identity --domain imex.online --region ca-central-1
aws --endpoint-url=http://localstack:4566 ses verify-email-identity --email-address noreply@imex.online --region ca-central-1
aws --endpoint-url=http://localstack:4566 secretsmanager create-secret --name CHATTER_PRIVATE_KEY --secret-string file:///tmp/certs/io-ftp-test.key
aws --endpoint-url=http://localstack:4566 logs create-log-group --log-group-name development --region ca-central-1
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket imex-large-log --create-bucket-configuration LocationConstraint=ca-central-1
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket imex-carfax-uploads --create-bucket-configuration LocationConstraint=ca-central-1
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket rome-carfax-uploads --create-bucket-configuration LocationConstraint=ca-central-1
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket rps-carfax-uploads --create-bucket-configuration LocationConstraint=ca-central-1
"
networks: networks:
redis-cluster-net: redis-cluster-net:
driver: bridge driver: bridge

View File

@@ -68,23 +68,18 @@ services:
# LocalStack: Used to emulate AWS services locally, currently setup for SES # LocalStack: Used to emulate AWS services locally, currently setup for SES
# Notes: Set the ENV Debug to 1 for additional logging # Notes: Set the ENV Debug to 1 for additional logging
localstack: localstack:
image: localstack/localstack image: localstack/localstack:4.13.1
container_name: localstack container_name: localstack
hostname: localstack hostname: localstack
networks: networks:
- redis-cluster-net - redis-cluster-net
restart: unless-stopped restart: unless-stopped
volumes: volumes:
- ./certs:/tmp/certs:ro # only if your script reads /tmp/certs/...
- ./localstack/init:/etc/localstack/init/ready.d:ro
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
environment: env_file:
- SERVICES=s3,ses,secretsmanager,cloudwatch,logs - .env.localstack.docker
- DEBUG=0
- AWS_ACCESS_KEY_ID=test
- AWS_SECRET_ACCESS_KEY=test
- AWS_DEFAULT_REGION=ca-central-1
- EXTRA_CORS_ALLOWED_HEADERS=Authorization,Content-Type
- EXTRA_CORS_ALLOWED_ORIGINS=*
- EXTRA_CORS_EXPOSE_HEADERS=Authorization,Content-Type
ports: ports:
- "4566:4566" - "4566:4566"
healthcheck: healthcheck:
@@ -94,38 +89,6 @@ services:
retries: 5 retries: 5
start_period: 20s start_period: 20s
# AWS-CLI - Used in conjunction with LocalStack to set required permission to send emails
aws-cli:
image: amazon/aws-cli
container_name: aws-cli
hostname: aws-cli
networks:
- redis-cluster-net
depends_on:
localstack:
condition: service_healthy
volumes:
- './localstack:/tmp/localstack'
- './certs:/tmp/certs'
environment:
- AWS_ACCESS_KEY_ID=test
- AWS_SECRET_ACCESS_KEY=test
- AWS_DEFAULT_REGION=ca-central-1
entrypoint: /bin/sh -c
command: >
"
aws --endpoint-url=http://localstack:4566 ses verify-domain-identity --domain imex.online --region ca-central-1
aws --endpoint-url=http://localstack:4566 ses verify-email-identity --email-address noreply@imex.online --region ca-central-1
aws --endpoint-url=http://localstack:4566 secretsmanager create-secret --name CHATTER_PRIVATE_KEY --secret-string file:///tmp/certs/io-ftp-test.key
aws --endpoint-url=http://localstack:4566 logs create-log-group --log-group-name development --region ca-central-1
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket imex-large-log --create-bucket-configuration LocationConstraint=ca-central-1
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket imex-job-totals --create-bucket-configuration LocationConstraint=ca-central-1
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket parts-estimates --create-bucket-configuration LocationConstraint=ca-central-1
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket imex-carfax-uploads --create-bucket-configuration LocationConstraint=ca-central-1
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket rome-carfax-uploads --create-bucket-configuration LocationConstraint=ca-central-1
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket rps-carfax-uploads --create-bucket-configuration LocationConstraint=ca-central-1
"
# Node App: The Main IMEX API # Node App: The Main IMEX API
node-app: node-app:
build: build:
@@ -145,8 +108,7 @@ services:
condition: service_healthy condition: service_healthy
localstack: localstack:
condition: service_healthy condition: service_healthy
aws-cli:
condition: service_completed_successfully
ports: ports:
- "4000:4000" - "4000:4000"
- "9229:9229" - "9229:9229"

View File

@@ -947,6 +947,7 @@
- carfax_exclude - carfax_exclude
- cdk_configuration - cdk_configuration
- cdk_dealerid - cdk_dealerid
- chatter_company_id
- chatterid - chatterid
- city - city
- claimscorpid - claimscorpid
@@ -1063,6 +1064,7 @@
- bill_allow_post_to_closed - bill_allow_post_to_closed
- bill_tax_rates - bill_tax_rates
- cdk_configuration - cdk_configuration
- chatter_company_id
- city - city
- country - country
- created_at - created_at

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"."bodyshops" add column "chatter_company_id" text
-- null;

Some files were not shown because too many files have changed in this diff Show More