Compare commits

...

38 Commits

Author SHA1 Message Date
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
ff73a14610 IO-3532 resolve tooltip on owner name. 2026-02-02 12:55:29 -08:00
Patrick Fic
cadcfc9b0d IO-3532 Resolve parts queue pages. 2026-02-02 11:21:22 -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
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
30 changed files with 2922 additions and 811 deletions

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

110
client/package-lock.json generated
View File

@@ -11,7 +11,7 @@
"dependencies": {
"@amplitude/analytics-browser": "^2.34.0",
"@ant-design/pro-layout": "^7.22.6",
"@apollo/client": "^4.1.2",
"@apollo/client": "^4.1.3",
"@emotion/is-prop-valid": "^1.4.0",
"@fingerprintjs/fingerprintjs": "^5.0.1",
"@firebase/analytics": "^0.10.19",
@@ -22,7 +22,7 @@
"@jsreport/browser-client": "^3.1.0",
"@reduxjs/toolkit": "^2.11.2",
"@sentry/cli": "^3.1.0",
"@sentry/react": "^10.37.0",
"@sentry/react": "^10.38.0",
"@sentry/vite-plugin": "^4.8.0",
"@splitsoftware/splitio-react": "^2.6.1",
"@tanem/react-nprogress": "^5.0.58",
@@ -43,7 +43,7 @@
"i18next": "^25.8.0",
"i18next-browser-languagedetector": "^8.2.0",
"immutability-helper": "^3.1.1",
"libphonenumber-js": "^1.12.35",
"libphonenumber-js": "^1.12.36",
"lightningcss": "^1.31.1",
"logrocket": "^12.0.0",
"markerjs2": "^2.32.7",
@@ -51,7 +51,7 @@
"normalize-url": "^8.1.1",
"object-hash": "^3.0.0",
"phone": "^3.1.70",
"posthog-js": "^1.335.5",
"posthog-js": "^1.336.4",
"prop-types": "^15.8.1",
"query-string": "^9.3.1",
"raf-schd": "^4.0.3",
@@ -540,9 +540,9 @@
}
},
"node_modules/@apollo/client": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/@apollo/client/-/client-4.1.2.tgz",
"integrity": "sha512-MxlWuO94Y6TRf6+d4KfG5bCUXg5NP4s7zPKRA0PDNNa18K86zcbpHUgWKdx6wMT/5KVMeC5rsZkDqZLr/R0mFw==",
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/@apollo/client/-/client-4.1.3.tgz",
"integrity": "sha512-2D0eN9R0IHj9qp1RwjM1/brKqcBGldlDfY0YiP5ecCj9FtVrhOtXqMj98SZ1CA0YGDY5X+dxx32Ljh7J0VHTfA==",
"license": "MIT",
"workspaces": [
"dist",
@@ -4771,18 +4771,18 @@
}
},
"node_modules/@posthog/core": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.14.1.tgz",
"integrity": "sha512-DtmJ1y1IDauX8yAZtIotRAYDRkgCCMLk5S9vFFRX7vufhWblQuRUOgn9WYSJrocJlZKm1aEjDzGQ0uyL7HcdLw==",
"version": "1.17.0",
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.17.0.tgz",
"integrity": "sha512-8pDNL+/u9ojzXloA5wILVDXBCV5daJ7w2ipCALQlEEZmL752cCKhRpbyiHn3tjKXh3Hy6aOboJneYa1JdlVHrQ==",
"license": "MIT",
"dependencies": {
"cross-spawn": "^7.0.6"
}
},
"node_modules/@posthog/types": {
"version": "1.335.5",
"resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.335.5.tgz",
"integrity": "sha512-QYj5c8wSaXGvV4ugEN65GHD0sIXRveGiZxV4tqpyoP7YIAvAwwA0do0yNfTrEjDXucCQn25pMbCqO25hJrMi5w==",
"version": "1.336.4",
"resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.336.4.tgz",
"integrity": "sha512-BY3cq/8segbXEvHbEXx9SWmaKJEM0AGgsOgMFH2yy13AV+rUHsGcp4Z5LDI5pU25DURN9EAZvzcoVyYy/Iokmw==",
"license": "MIT"
},
"node_modules/@protobufjs/aspromise": {
@@ -6619,50 +6619,50 @@
]
},
"node_modules/@sentry-internal/browser-utils": {
"version": "10.37.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-10.37.0.tgz",
"integrity": "sha512-rqdESYaVio9Ktz55lhUhtBsBUCF3wvvJuWia5YqoHDd+egyIfwWxITTAa0TSEyZl7283A4WNHNl0hyeEMblmfA==",
"version": "10.38.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-10.38.0.tgz",
"integrity": "sha512-UOJtYmdcxHCcV0NPfXFff/a95iXl/E0EhuQ1y0uE0BuZDMupWSF5t2BgC4HaE5Aw3RTjDF3XkSHWoIF6ohy7eA==",
"license": "MIT",
"dependencies": {
"@sentry/core": "10.37.0"
"@sentry/core": "10.38.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@sentry-internal/feedback": {
"version": "10.37.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-10.37.0.tgz",
"integrity": "sha512-P0PVlfrDvfvCYg2KPIS7YUG/4i6ZPf8z1MicXx09C9Cz9W9UhSBh/nii13eBdDtLav2BFMKhvaFMcghXHX03Hw==",
"version": "10.38.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-10.38.0.tgz",
"integrity": "sha512-JXneg9zRftyfy1Fyfc39bBlF/Qd8g4UDublFFkVvdc1S6JQPlK+P6q22DKz3Pc8w3ySby+xlIq/eTu9Pzqi4KA==",
"license": "MIT",
"dependencies": {
"@sentry/core": "10.37.0"
"@sentry/core": "10.38.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@sentry-internal/replay": {
"version": "10.37.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-10.37.0.tgz",
"integrity": "sha512-snuk12ZaDerxesSnetNIwKoth/51R0y/h3eXD/bGtXp+hnSkeXN5HanI/RJl297llRjn4zJYRShW9Nx86Ay0Dw==",
"version": "10.38.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-10.38.0.tgz",
"integrity": "sha512-YWIkL6/dnaiQyFiZXJ/nN+NXGv/15z45ia86bE/TMq01CubX/DUOilgsFz0pk2v/pg3tp/U2MskLO9Hz0cnqeg==",
"license": "MIT",
"dependencies": {
"@sentry-internal/browser-utils": "10.37.0",
"@sentry/core": "10.37.0"
"@sentry-internal/browser-utils": "10.38.0",
"@sentry/core": "10.38.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@sentry-internal/replay-canvas": {
"version": "10.37.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-10.37.0.tgz",
"integrity": "sha512-PyIYSbjLs+L5essYV0MyIsh4n5xfv2eV7l0nhUoPJv9Bak3kattQY3tholOj0EP3SgKgb+8HSZnmazgF++Hbog==",
"version": "10.38.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-10.38.0.tgz",
"integrity": "sha512-OXWM9jEqNYh4VTvrMu7v+z1anz+QKQ/fZXIZdsO7JTT2lGNZe58UUMeoq386M+Saxen8F9SUH7yTORy/8KI5qw==",
"license": "MIT",
"dependencies": {
"@sentry-internal/replay": "10.37.0",
"@sentry/core": "10.37.0"
"@sentry-internal/replay": "10.38.0",
"@sentry/core": "10.38.0"
},
"engines": {
"node": ">=18"
@@ -6678,16 +6678,16 @@
}
},
"node_modules/@sentry/browser": {
"version": "10.37.0",
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.37.0.tgz",
"integrity": "sha512-kheqJNqGZP5TSBCPv4Vienv1sfZwXKHQDYR+xrdHHYdZqwWuZMJJW/cLO9XjYAe+B9NnJ4UwJOoY4fPvU+HQ1Q==",
"version": "10.38.0",
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.38.0.tgz",
"integrity": "sha512-3phzp1YX4wcQr9mocGWKbjv0jwtuoDBv7+Y6Yfrys/kwyaL84mDLjjQhRf4gL5SX7JdYkhBp4WaiNlR0UC4kTA==",
"license": "MIT",
"dependencies": {
"@sentry-internal/browser-utils": "10.37.0",
"@sentry-internal/feedback": "10.37.0",
"@sentry-internal/replay": "10.37.0",
"@sentry-internal/replay-canvas": "10.37.0",
"@sentry/core": "10.37.0"
"@sentry-internal/browser-utils": "10.38.0",
"@sentry-internal/feedback": "10.38.0",
"@sentry-internal/replay": "10.38.0",
"@sentry-internal/replay-canvas": "10.38.0",
"@sentry/core": "10.38.0"
},
"engines": {
"node": ">=18"
@@ -7096,22 +7096,22 @@
}
},
"node_modules/@sentry/core": {
"version": "10.37.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.37.0.tgz",
"integrity": "sha512-hkRz7S4gkKLgPf+p3XgVjVm7tAfvcEPZxeACCC6jmoeKhGkzN44nXwLiqqshJ25RMcSrhfFvJa/FlBg6zupz7g==",
"version": "10.38.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.38.0.tgz",
"integrity": "sha512-1pubWDZE5y5HZEPMAZERP4fVl2NH3Ihp1A+vMoVkb3Qc66Diqj1WierAnStlZP7tCx0TBa0dK85GTW/ZFYyB9g==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@sentry/react": {
"version": "10.37.0",
"resolved": "https://registry.npmjs.org/@sentry/react/-/react-10.37.0.tgz",
"integrity": "sha512-XLnXJOHgsCeVAVBbO+9AuGlZWnCxLQHLOmKxpIr8wjE3g7dHibtug6cv8JLx78O4dd7aoCqv2TTyyKY9FLJ2EQ==",
"version": "10.38.0",
"resolved": "https://registry.npmjs.org/@sentry/react/-/react-10.38.0.tgz",
"integrity": "sha512-3UiKo6QsqTyPGUt0XWRY9KLaxc/cs6Kz4vlldBSOXEL6qPDL/EfpwNJT61osRo81VFWu8pKu7ZY2bvLPryrnBQ==",
"license": "MIT",
"dependencies": {
"@sentry/browser": "10.37.0",
"@sentry/core": "10.37.0"
"@sentry/browser": "10.38.0",
"@sentry/core": "10.38.0"
},
"engines": {
"node": ">=18"
@@ -12831,9 +12831,9 @@
}
},
"node_modules/libphonenumber-js": {
"version": "1.12.35",
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.35.tgz",
"integrity": "sha512-T/Cz6iLcsZdb5jDncDcUNhSAJ0VlSC9TnsqtBNdpkaAmy24/R1RhErtNWVWBrcUZKs9hSgaVsBkc7HxYnazIfw==",
"version": "1.12.36",
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.36.tgz",
"integrity": "sha512-woWhKMAVx1fzzUnMCyOzglgSgf6/AFHLASdOBcchYCyvWSGWt12imw3iu2hdI5d4dGZRsNWAmWiz37sDKUPaRQ==",
"license": "MIT"
},
"node_modules/lightningcss": {
@@ -14974,9 +14974,9 @@
"license": "MIT"
},
"node_modules/posthog-js": {
"version": "1.335.5",
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.335.5.tgz",
"integrity": "sha512-1zCEdn7bc1mQ/jpd62YY8U1CyNiftIBE6uKqE2L+mjZ5aJyB2rtUAXefaTbaR/3A98tItjSej4aIa8FBN+O1fw==",
"version": "1.336.4",
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.336.4.tgz",
"integrity": "sha512-NX81XaqOjS/gue3UsbAAuJxi6vD0AGy1HUvywBIhAArCwbTXKS04NhEFwUcYJdrmwXUf94MntEIWGoc1pTFDtg==",
"license": "SEE LICENSE IN LICENSE",
"dependencies": {
"@opentelemetry/api": "^1.9.0",
@@ -14984,8 +14984,8 @@
"@opentelemetry/exporter-logs-otlp-http": "^0.208.0",
"@opentelemetry/resources": "^2.2.0",
"@opentelemetry/sdk-logs": "^0.208.0",
"@posthog/core": "1.14.1",
"@posthog/types": "1.335.5",
"@posthog/core": "1.17.0",
"@posthog/types": "1.336.4",
"core-js": "^3.38.1",
"dompurify": "^3.3.1",
"fflate": "^0.4.8",

View File

@@ -10,7 +10,7 @@
"dependencies": {
"@amplitude/analytics-browser": "^2.34.0",
"@ant-design/pro-layout": "^7.22.6",
"@apollo/client": "^4.1.2",
"@apollo/client": "^4.1.3",
"@emotion/is-prop-valid": "^1.4.0",
"@fingerprintjs/fingerprintjs": "^5.0.1",
"@firebase/analytics": "^0.10.19",
@@ -21,7 +21,7 @@
"@jsreport/browser-client": "^3.1.0",
"@reduxjs/toolkit": "^2.11.2",
"@sentry/cli": "^3.1.0",
"@sentry/react": "^10.37.0",
"@sentry/react": "^10.38.0",
"@sentry/vite-plugin": "^4.8.0",
"@splitsoftware/splitio-react": "^2.6.1",
"@tanem/react-nprogress": "^5.0.58",
@@ -42,7 +42,7 @@
"i18next": "^25.8.0",
"i18next-browser-languagedetector": "^8.2.0",
"immutability-helper": "^3.1.1",
"libphonenumber-js": "^1.12.35",
"libphonenumber-js": "^1.12.36",
"lightningcss": "^1.31.1",
"logrocket": "^12.0.0",
"markerjs2": "^2.32.7",
@@ -50,7 +50,7 @@
"normalize-url": "^8.1.1",
"object-hash": "^3.0.0",
"phone": "^3.1.70",
"posthog-js": "^1.335.5",
"posthog-js": "^1.336.4",
"prop-types": "^15.8.1",
"query-string": "^9.3.1",
"raf-schd": "^4.0.3",

View File

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

View File

@@ -1,5 +1,5 @@
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 { DateFormatter } from "../../utils/DateFormatter";
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 }) {
const { t } = useTranslation();
return (
<>
<Card>
{!create && <FormFieldsChanged form={form} />}
<LayoutFormRow>
<LayoutFormRow noDivider={true}>
{!create && (
<Form.Item
label={t("contracts.fields.status")}
@@ -310,6 +310,6 @@ export default function ContractFormComponent({ form, create = false, selectedJo
<InputNumber precision={2} />
</Form.Item>
</LayoutFormRow>
</>
</Card>
);
}

View File

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

View File

@@ -49,12 +49,15 @@ export function DmsCdkVehicles({ form, job }) {
open={open}
onCancel={() => setOpen(false)}
onOk={() => {
form.setFieldsValue({
dms_make: selectedModel.makecode,
dms_model: selectedModel.modelcode
});
setOpen(false);
if (selectedModel) {
form.setFieldsValue({
dms_make: selectedModel.makecode,
dms_model: selectedModel.modelcode
});
setOpen(false);
}
}}
okButtonProps={{ disabled: !selectedModel }}
>
{error && <AlertComponent title={error.message} type="error" />}
<Table

View File

@@ -53,13 +53,13 @@ export default function FortellisCustomerSelector({ bodyshop, jobid, socket }) {
render: (_t, r) => <Checkbox disabled checked={r.vinOwner} />
},
{
title: t("jobs.fields.dms.name1"),
title: t("jobs.fields.dms.first_name"),
dataIndex: ["customerName", "firstName"],
key: "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"],
key: "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"]}
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) => (
<Select.Option key={payer.name}>{payer.name}</Select.Option>
))}

View File

@@ -1,29 +1,65 @@
import { useMemo } from "react";
import { Tag, Tooltip } from "antd";
import { Tooltip } from "antd";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { useTranslation } from "react-i18next";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
const mapDispatchToProps = () => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
const mapDispatchToProps = () => ({});
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 function JobPartsQueueCount({ bodyshop, parts }) {
const { t } = useTranslation();
const partsStatus = useMemo(() => {
if (!parts) return null;
const statusKeys = ["default_bo", "default_ordered", "default_received", "default_returned"];
return parts.reduce(
(acc, val) => {
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;
},
{
@@ -34,45 +70,38 @@ export function JobPartsQueueCount({ bodyshop, parts }) {
);
}, [bodyshop, parts]);
if (!parts) return null;
if (!parts || !partsStatus) return null;
return (
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(40px, 1fr))",
gap: "8px",
width: "100%",
justifyItems: "start"
display: "inline-flex", // ✅ shrink-wraps, fixes the “extra box” to the right
gap: 2,
alignItems: "center",
whiteSpace: "nowrap"
}}
>
<Tooltip title="Total">
<Tag style={{ minWidth: "40px", textAlign: "center" }}>{partsStatus.total}</Tag>
</Tooltip>
<Tooltip title={t("dashboard.errors.status_normal")}>
<Tag color="gold" style={{ minWidth: "40px", textAlign: "center" }}>
{partsStatus["null"]}
</Tag>
</Tooltip>
<Tooltip title={bodyshop.md_order_statuses.default_bo}>
<Tag color="red" style={{ minWidth: "40px", textAlign: "center" }}>
{partsStatus[bodyshop.md_order_statuses.default_bo]}
</Tag>
</Tooltip>
<Tooltip title={bodyshop.md_order_statuses.default_ordered}>
<Tag color="blue" style={{ minWidth: "40px", textAlign: "center" }}>
{partsStatus[bodyshop.md_order_statuses.default_ordered]}
</Tag>
</Tooltip>
<Tooltip title={bodyshop.md_order_statuses.default_received}>
<Tag color="green" style={{ minWidth: "40px", textAlign: "center" }}>
{partsStatus[bodyshop.md_order_statuses.default_received]}
</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>
<CompactTag tooltip="Total" color="gray">
{partsStatus.total}
</CompactTag>
<CompactTag tooltip="No Status" color="gold">
{partsStatus["null"]}
</CompactTag>
<CompactTag tooltip={bodyshop.md_order_statuses.default_bo} color="red">
{partsStatus[bodyshop.md_order_statuses.default_bo]}
</CompactTag>
<CompactTag tooltip={bodyshop.md_order_statuses.default_ordered} color="blue">
{partsStatus[bodyshop.md_order_statuses.default_ordered]}
</CompactTag>
<CompactTag tooltip={bodyshop.md_order_statuses.default_received} color="green">
{partsStatus[bodyshop.md_order_statuses.default_received]}
</CompactTag>
<CompactTag tooltip={bodyshop.md_order_statuses.default_returned} color="orange">
{partsStatus[bodyshop.md_order_statuses.default_returned]}
</CompactTag>
</div>
);
}

View File

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

View File

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

View File

@@ -1,10 +1,11 @@
import { DownCircleFilled } from "@ant-design/icons";
import { useApolloClient, useMutation, useQuery } from "@apollo/client/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 parsePhoneNumber from "libphonenumber-js";
import { useCallback, useMemo, useRef, useState } from "react";
import { HasRbacAccess } from "../../components/rbac-wrapper/rbac-wrapper.component";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
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 { openChatByPhone, setMessage } from "../../redux/messaging/messaging.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 { DateTimeFormatter } from "../../utils/DateFormatter";
import { TemplateList } from "../../utils/TemplateConstants";
@@ -28,7 +29,6 @@ import dayjs from "../../utils/day";
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.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 AddToProduction from "./jobs-detail-header-actions.addtoproduction.util";
import DuplicateJob from "./jobs-detail-header-actions.duplicate.util";
@@ -39,7 +39,8 @@ const EMPTY_ARRAY = Object.freeze([]);
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
jobRO: selectJobReadOnly,
currentUser: selectCurrentUser
currentUser: selectCurrentUser,
authLevel: selectAuthLevel
});
const mapDispatchToProps = (dispatch) => ({
@@ -117,7 +118,8 @@ export function JobsDetailHeaderActions({
openChatByPhone,
setMessage,
setTimeTicketTaskContext,
setTaskUpsertContext
setTaskUpsertContext,
authLevel
}) {
const { t } = useTranslation();
const client = useApolloClient();
@@ -129,10 +131,6 @@ export function JobsDetailHeaderActions({
const jobId = job?.id;
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 [insertAppointment] = useMutation(INSERT_MANUAL_APPT);
const [deleteJob] = useMutation(DELETE_JOB);
@@ -150,6 +148,8 @@ export function JobsDetailHeaderActions({
const devEmails = ["imex.dev", "rome.dev"];
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 canSubmitForTesting = (isDevEnv && hasValidEmail(devEmails)) || (isProdEnv && hasValidEmail(prodEmails));
@@ -179,83 +179,69 @@ export function JobsDetailHeaderActions({
const jobInPreProduction = preProductionStatuses.includes(jobStatus);
const jobInPostProduction = postProductionStatuses.includes(jobStatus);
const openConfirm = useCallback((key) => {
confirmKeyRef.current = key;
setConfirmKey(key);
setDropdownOpen(true);
}, []);
const makeConfirmId = () =>
globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(16).slice(2)}`;
const closeConfirm = useCallback(() => {
confirmKeyRef.current = null;
setConfirmKey(null);
}, []);
const [modal, modalContextHolder] = Modal.useModal();
const handleDropdownOpenChange = useCallback(
(nextOpen, info) => {
if (!nextOpen && info?.source === "menu" && confirmKeyRef.current) return;
setDropdownOpen(nextOpen);
if (!nextOpen) closeConfirm();
},
[closeConfirm]
);
const confirmInstancesRef = useRef(new Map());
const renderPopconfirmMenuLabel = ({
key,
text,
const closeConfirmById = (id) => {
const inst = confirmInstancesRef.current.get(id);
if (inst) inst.destroy(); // hard close
confirmInstancesRef.current.delete(id);
};
const openConfirmFromMenu = ({
variant = "confirm", // "confirm" | "info" | "warning"
title,
content,
okText,
cancelText,
showCancel = true,
closeDropdownOnConfirm = true,
onConfirm
}) => (
<Popconfirm
title={title}
okText={okText}
cancelText={cancelText}
showCancel={showCancel}
open={confirmKey === key}
onOpenChange={(nextOpen) => {
if (nextOpen) openConfirm(key);
else closeConfirm();
}}
onConfirm={(e) => {
e?.stopPropagation?.();
closeConfirm();
onOk,
onCancel
}) => {
// close the dropdown immediately; confirm dialog is separate
setDropdownOpen(false);
// Critical: for informational popconfirms, keep the dropdown open so the Popconfirm can cleanly close.
if (closeDropdownOnConfirm) {
setDropdownOpen(false);
const id = makeConfirmId();
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);
}}
onCancel={(e) => {
e?.stopPropagation?.();
closeConfirm();
// Keep dropdown open on cancel so the user can continue using the menu.
}}
getPopupContainer={() => document.body}
>
<div
style={{ width: "100%" }}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
openConfirm(key);
}}
>
{text}
</div>
</Popconfirm>
);
confirmInstancesRef.current.set(id, inst);
return id;
};
const handleDropdownOpenChange = useCallback((nextOpen) => {
setDropdownOpen(nextOpen);
}, []);
// Function to show modal
const showCancelScheduleModal = () => {
setIsCancelScheduleModalVisible(true);
};
// Function to handle Cancel
const handleCancelScheduleModalCancel = () => {
setIsCancelScheduleModalVisible(false);
};
@@ -476,6 +462,11 @@ export function JobsDetailHeaderActions({
};
const handleVoidJob = async () => {
if (!canVoidJob) {
notification.error({ title: t("general.messages.rbacunauth") });
return;
}
//delete the job.
const result = await voidJob({
variables: {
@@ -964,26 +955,26 @@ export function JobsDetailHeaderActions({
{
key: "duplicate",
id: "job-actions-duplicate",
label: renderPopconfirmMenuLabel({
key: "confirm-duplicate",
text: t("menus.jobsactions.duplicate"),
title: t("jobs.labels.duplicateconfirm"),
okText: t("general.labels.yes"),
cancelText: t("general.labels.no"),
onConfirm: handleDuplicate
})
label: t("menus.jobsactions.duplicate"),
onClick: () =>
openConfirmFromMenu({
title: t("jobs.labels.duplicateconfirm"),
okText: t("general.labels.yes"),
cancelText: t("general.labels.no"),
onOk: handleDuplicate
})
},
{
key: "duplicatenolines",
id: "job-actions-duplicatenolines",
label: renderPopconfirmMenuLabel({
key: "confirm-duplicate-nolines",
text: t("menus.jobsactions.duplicatenolines"),
title: t("jobs.labels.duplicateconfirm"),
okText: t("general.labels.yes"),
cancelText: t("general.labels.no"),
onConfirm: handleDuplicateConfirm
})
label: t("menus.jobsactions.duplicatenolines"),
onClick: () =>
openConfirmFromMenu({
title: t("jobs.labels.duplicateconfirm"),
okText: t("general.labels.yes"),
cancelText: t("general.labels.no"),
onOk: handleDuplicateConfirm
})
}
]
},
@@ -1156,26 +1147,25 @@ export function JobsDetailHeaderActions({
menuItems.push({
key: "deletejob",
id: "job-actions-deletejob",
label:
jobWatchersCount === 0
? renderPopconfirmMenuLabel({
key: "confirm-deletejob",
text: t("menus.jobsactions.deletejob"),
title: t("jobs.labels.deleteconfirm"),
okText: t("general.labels.yes"),
cancelText: t("general.labels.no"),
onConfirm: handleDeleteJob
})
: renderPopconfirmMenuLabel({
key: "confirm-deletejob-watchers",
text: t("menus.jobsactions.deletejob"),
title: t("jobs.labels.deletewatchers"),
showCancel: false,
closeDropdownOnConfirm: false, // <-- FIX: keep dropdown mounted so Popconfirm can close cleanly
onConfirm: () => {
// informational confirm only
}
})
label: t("menus.jobsactions.deletejob"),
onClick: () => {
if (jobWatchersCount === 0) {
openConfirmFromMenu({
title: t("jobs.labels.deleteconfirm"),
okText: t("general.labels.yes"),
cancelText: t("general.labels.no"),
onOk: handleDeleteJob
});
} else {
// informational "OK only"
openConfirmFromMenu({
variant: "info",
title: t("jobs.labels.deletewatchers"),
okText: t("general.actions.ok"),
showCancel: false
});
}
}
});
}
@@ -1188,22 +1178,18 @@ export function JobsDetailHeaderActions({
label: t("appointments.labels.manualevent")
});
if (!jobRO && job.converted) {
if (!jobRO && job.converted && canVoidJob) {
menuItems.push({
key: "voidjob",
id: "job-actions-voidjob",
label: (
<RbacWrapper action="jobs:void" noauth>
{renderPopconfirmMenuLabel({
key: "confirm-voidjob",
text: t("menus.jobsactions.void"),
title: t("jobs.labels.voidjob"),
okText: t("general.labels.yes"),
cancelText: t("general.labels.no"),
onConfirm: handleVoidJob
})}
</RbacWrapper>
)
label: t("menus.jobsactions.void"),
onClick: () =>
openConfirmFromMenu({
title: t("jobs.labels.voidjob"),
okText: t("general.labels.yes"),
cancelText: t("general.labels.no"),
onOk: handleVoidJob
})
});
}
@@ -1235,6 +1221,7 @@ export function JobsDetailHeaderActions({
return (
<>
{modalContextHolder}
<Modal
title={t("menus.jobsactions.cancelallappointments")}
open={isCancelScheduleModalVisible}

View File

@@ -2,6 +2,7 @@ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { store } from "../../redux/store";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { Tooltip } from "antd";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
@@ -11,15 +12,27 @@ const mapDispatchToProps = () => ({
});
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;
if (!emptyTest || emptyTest === "null" || emptyTest.trim() === "") return "N/A";
if (bodyshop.last_name_first)
return `${ownerObject?.ownr_ln || ""}, ${ownerObject?.ownr_fn || ""} ${ownerObject?.ownr_co_nm || ""}`.trim();
return `${ownerObject?.ownr_fn || ""} ${ownerObject?.ownr_ln || ""} ${ownerObject.ownr_co_nm || ""}`.trim();
let returnString;
if (bodyshop.last_name_first) {
returnString =
`${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) {

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 { 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({
bodyshop: selectBodyshop,
isPartsEntry: selectIsPartsEntry
@@ -199,10 +206,7 @@ export function PartsOrderModalComponent({
key={`${index}act_price`}
name={[field.name, "act_price"]}
>
<Space.Compact style={{ width: "100%" }}>
<PartsOrderModalPriceChange form={form} field={field} />
<CurrencyInput style={{ flex: 1 }} />
</Space.Compact>
<PriceInputWrapper form={form} field={field} />
</Form.Item>
{isReturn && (
<Form.Item

View File

@@ -89,7 +89,8 @@ export function PartsOrderModalContainer({
return {
...p,
job_line_id: jobLineId
job_line_id: jobLineId,
...(isReturn && { cm_received: false })
};
});

View File

@@ -1,6 +1,6 @@
import { SyncOutlined } from "@ant-design/icons";
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 queryString from "query-string";
import { useState } from "react";
@@ -31,6 +31,8 @@ export function PartsQueueListComponent({ bodyshop }) {
const { selected, sortcolumn, sortorder, statusFilters } = searchParams;
const history = useNavigate();
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, {
fetchPolicy: "network-only",
@@ -92,6 +94,7 @@ export function PartsQueueListComponent({ bodyshop }) {
title: t("jobs.fields.ro_number"),
dataIndex: "ro_number",
key: "ro_number",
width: "110px",
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
sortOrder: sortcolumn === "ro_number" && sortorder,
@@ -103,16 +106,20 @@ export function PartsQueueListComponent({ bodyshop }) {
title: t("jobs.fields.owner"),
dataIndex: "ownr_ln",
key: "ownr_ln",
width: "8%",
ellipsis: {
showTitle: true
},
sorter: (a, b) => alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
sortOrder: sortcolumn === "ownr_ln" && sortorder,
render: (text, record) => {
return record.ownerid ? (
<Link to={"/manage/owners/" + record.ownerid}>
<OwnerNameDisplay ownerObject={record} />
<OwnerNameDisplay ownerObject={record} withToolTip />
</Link>
) : (
<span>
<OwnerNameDisplay ownerObject={record} />
<OwnerNameDisplay ownerObject={record} withToolTip />
</span>
);
}
@@ -187,7 +194,7 @@ export function PartsQueueListComponent({ bodyshop }) {
ellipsis: true,
sorter: (a, b) => dateSort(a.scheduled_in, b.scheduled_in),
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"),
@@ -196,7 +203,9 @@ export function PartsQueueListComponent({ bodyshop }) {
ellipsis: true,
sorter: (a, b) => dateSort(a.scheduled_completion, b.scheduled_completion),
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"),
@@ -227,16 +236,23 @@ export function PartsQueueListComponent({ bodyshop }) {
title: t("jobs.fields.updated_at"),
dataIndex: "updated_at",
key: "updated_at",
width: "110px",
sorter: (a, b) => dateSort(a.updated_at, b.updated_at),
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"),
dataIndex: "partsstatus",
key: "partsstatus",
width: countsOnly ? "180px" : "110px",
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"),
dataIndex: "queued_for_parts",
key: "queued_for_parts",
width: "120px",
sorter: (a, b) => a.queued_for_parts - b.queued_for_parts,
sortOrder: sortcolumn === "queued_for_parts" && sortorder,
filteredValue: filter?.queued_for_parts || null,
@@ -275,6 +292,12 @@ export function PartsQueueListComponent({ bodyshop }) {
<Button onClick={() => refetch()}>
<SyncOutlined />
</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
className="imex-table-header__search"
placeholder={t("general.labels.search")}
@@ -299,7 +322,7 @@ export function PartsQueueListComponent({ bodyshop }) {
rowKey="id"
dataSource={jobs}
style={{ height: "100%" }}
scroll={{ x: true }}
//scroll={{ x: true }}
onChange={handleTableChange}
rowSelection={{
onSelect: (record) => {

View File

@@ -356,7 +356,10 @@ export const MUTATION_BACKORDER_PART_LINE = gql`
export const QUERY_UNRECEIVED_LINES = gql`
query QUERY_UNRECEIVED_LINES($jobId: uuid!, $vendorId: uuid!) {
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
id

View File

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

View File

@@ -82,10 +82,23 @@ export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail, set
const handleFinish = async ({ removefromproduction, ...values }) => {
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({
mutation: generateJobLinesUpdatesForInvoicing(values.joblines)
});
if (result.errors) {
setLoading(false);
return; // Abandon the rest of the close.
}

View File

@@ -48,6 +48,7 @@
"arrivedon": "Arrived on: ",
"arrivingjobs": "Arriving Jobs",
"blocked": "Blocked",
"bp": "B/P",
"cancelledappointment": "Canceled appointment for: ",
"completingjobs": "Completing Jobs",
"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.",
"nocompletingjobs": "No Jobs scheduled for completion.",
"nodateselected": "No date has been selected.",
"owner": "Owner",
"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. ",
"ro_number": "RO #",
"scheduled_completion": "Scheduled Completion",
"scheduledfor": "Scheduled appointment for: ",
"severalerrorsfound": "Several Jobs have issues which may prevent accurate smart scheduling. Click to expand.",
"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}}.",
"suggesteddates": "Suggested Dates",
"ro_number": "RO #",
"owner": "Owner",
"vehicle": "Vehicle",
"bp": "B/P",
"scheduled_completion": "Scheduled Completion"
"vehicle": "Vehicle"
},
"successes": {
"canceled": "Appointment canceled successfully.",
@@ -90,6 +90,11 @@
"actions": "Actions"
}
},
"audio": {
"manager": {
"description": "Click anywhere to enable the message ding."
}
},
"audit": {
"fields": {
"cc": "CC",
@@ -149,11 +154,6 @@
"tasks_updated": "Task '{{title}}' updated by {{updatedBy}}"
}
},
"audio": {
"manager": {
"description": "Click anywhere to enable the message ding."
}
},
"billlines": {
"actions": {
"newline": "New Line"
@@ -281,9 +281,9 @@
},
"errors": {
"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.",
"saving": "Error encountered while saving. {{message}}",
"duplicate_insurance_company": "Duplicate insurance company name. Each insurance company name must be unique"
"saving": "Error encountered while saving. {{message}}"
},
"fields": {
"ReceivableCustomField": "QBO Receivable Custom Field {{number}}",
@@ -564,21 +564,18 @@
"responsibilitycenter_tax_tier": "Tax {{typeNum}} Tier {{typeNumIterator}}",
"responsibilitycenter_tax_type": "Tax {{typeNum}} Type",
"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",
"ar": "Accounts Receivable",
"ats": "ATS",
"federal_tax": "Federal Tax",
"federal_tax_itc": "Federal Tax Credit",
"gogcode": "GOG Code (BreakOut)",
"gst_override": "GST Override Account #",
"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",
"la1": "LA1",
"la2": "LA2",
@@ -597,6 +594,7 @@
"local_tax": "Local Tax",
"mapa": "Paint Materials",
"mash": "Shop Materials",
"nontaxable": "Non-taxable",
"paa": "Aftermarket",
"pac": "Chrome",
"pag": "Glass",
@@ -617,6 +615,8 @@
"state": "State Tax Applies"
},
"state_tax": "State Tax",
"taxable": "Taxable",
"taxable_flag": "Taxable?",
"tow": "Towing"
},
"schedule_end_time": "Schedule Ending Time",
@@ -678,8 +678,6 @@
"zip_post": "Zip/Postal Code"
},
"labels": {
"parts_shop_management": "Shop Management",
"parts_vendor_management": "Vendor Management",
"2tiername": "Name => RO",
"2tiersetup": "2 Tier Setup",
"2tiersource": "Source => RO",
@@ -702,11 +700,11 @@
"payers": "Payers"
},
"cdk_dealerid": "CDK Dealer ID",
"rr_dealerid": "Reynolds Store Number",
"costsmapping": "Costs Mapping",
"dms_allocations": "DMS Allocations",
"pbs_serialnumber": "PBS Serial Number",
"profitsmapping": "Profits Mapping",
"rr_dealerid": "Reynolds Store Number",
"title": "DMS"
},
"emaillater": "Email Later",
@@ -733,6 +731,8 @@
"followers": "Notifications"
},
"orderstatuses": "Order Statuses",
"parts_shop_management": "Shop Management",
"parts_vendor_management": "Vendor Management",
"partslocations": "Parts Locations",
"partsscan": "Parts Scanning",
"printlater": "Print Later",
@@ -1228,8 +1228,6 @@
},
"general": {
"actions": {
"select": "Select",
"optional": "Optional",
"add": "Add",
"autoupdate": "{{app}} will automatically update in {{time}} seconds. Please save all changes.",
"calculate": "Calculate",
@@ -1249,6 +1247,7 @@
"login": "Login",
"next": "Next",
"ok": "Ok",
"optional": "Optional",
"previous": "Previous",
"print": "Print",
"refresh": "Refresh",
@@ -1259,6 +1258,7 @@
"save": "Save",
"saveandnew": "Save and New",
"saveas": "Save As",
"select": "Select",
"selectall": "Select All",
"send": "Send",
"sendbysms": "Send by SMS",
@@ -1288,8 +1288,6 @@
"vehicle": "Vehicle"
},
"labels": {
"selected": "Selected",
"settings": "Settings",
"actions": "Actions",
"areyousure": "Are you sure?",
"barcode": "Barcode",
@@ -1343,8 +1341,10 @@
"search": "Search...",
"searchresults": "Results for {{search}}",
"selectdate": "Select date...",
"selected": "Selected",
"sendagain": "Send Again",
"sendby": "Send By",
"settings": "Settings",
"signin": "Sign In",
"sms": "SMS",
"status": "Status",
@@ -1587,13 +1587,13 @@
"labels": {
"adjustmenttobeadded": "Adjustment to be added: {{adjustment}}",
"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.",
"edit": "Edit Line",
"ioucreated": "IOU",
"new": "New Line",
"nostatus": "No Status",
"presets": "Jobline Presets",
"bulk_location_help": "This will set the same location on all selected lines."
"presets": "Jobline Presets"
},
"successes": {
"created": "Job line created successfully.",
@@ -1676,7 +1676,9 @@
"deleted": "Error deleting Job. {{error}}",
"exporting": "Error exporting Job. {{error}}",
"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}}",
"missingjoblineids": "Missing job line IDs for job lines.",
"noaccess": "This Job does not exist or you do not have access to it.",
"nodamage": "No damage points on estimate.",
"nodates": "No dates specified for this Job.",
@@ -1699,9 +1701,9 @@
"actual_delivery": "Actual Delivery",
"actual_in": "Actual In",
"acv_amount": "ACV Amount",
"admin_clerk": "Admin Clerk",
"adjustment_bottom_line": "Adjustments",
"adjustmenthours": "Adjustment Hours",
"admin_clerk": "Admin Clerk",
"alt_transport": "Alt. Trans.",
"area_of_damage_impact": {
"10": "Left Front Side",
@@ -1783,6 +1785,7 @@
"depreciation_taxes": "Betterment/Depreciation/Taxes",
"dms": {
"address": "Customer Address",
"advisor": "Advisor #",
"amount": "Amount",
"center": "Center",
"control_type": {
@@ -1795,12 +1798,13 @@
"dms_model_override": "Override DMS Make/Model",
"dms_unsold": "New, Unsold Vehicle",
"dms_wip_acctnumber": "Cost WIP DMS Acct #",
"first_name": "First Name",
"id": "DMS ID",
"inservicedate": "In Service Date",
"journal": "Journal #",
"make_override": "Make Override",
"advisor": "Advisor #",
"last_name": "Last Name",
"lines": "Posting Lines",
"make_override": "Make Override",
"name1": "Customer Name",
"payer": {
"amount": "Amount",
@@ -1941,7 +1945,7 @@
"amount": "Amount",
"name": "Name"
},
"queued_for_parts": "Queued for Parts",
"queued_for_parts": "Queued",
"rate_ats": "ATS Rate",
"rate_ats_flat": "ATS Flat Rate",
"rate_la1": "LA1",
@@ -2443,6 +2447,7 @@
"labels": {
"addlabel": "Add a label to this conversation.",
"archive": "Archive",
"mark_unread": "Mark as unread",
"maxtenimages": "You can only select up to a maximum of 10 images at a time.",
"messaging": "Messaging",
"no_consent": "Opted-out",
@@ -2455,8 +2460,7 @@
"selectmedia": "Select Media",
"sentby": "Sent by {{by}} at {{time}}",
"typeamessage": "Send a message...",
"unarchive": "Unarchive",
"mark_unread": "Mark as unread"
"unarchive": "Unarchive"
},
"render": {
"conversation_list": "Conversation List"
@@ -2610,20 +2614,20 @@
"name": "Owner Details"
},
"labels": {
"cell": "Cell",
"create_new": "Create a new owner record.",
"deleteconfirm": "Are you sure you want to delete this owner? This cannot be undone.",
"email": "Email",
"existing_owners": "Existing Owners",
"fromclaim": "Current Claim",
"fromowner": "Historical Owner Record",
"relatedjobs": "Related Jobs",
"updateowner": "Update Owner",
"work": "Work",
"home": "Home",
"cell": "Cell",
"other": "Other",
"email": "Email",
"phone": "Phone",
"sms": "SMS"
"relatedjobs": "Related Jobs",
"sms": "SMS",
"updateowner": "Update Owner",
"work": "Work"
},
"successes": {
"delete": "Owner deleted successfully.",
@@ -2634,6 +2638,10 @@
"actions": {
"order": "Order Parts",
"orderinhouse": "Order as In House"
},
"labels": {
"view_counts_only": "View Parts Counts Only",
"view_timestamps": "Show timestamps"
}
},
"parts_dispatch": {
@@ -2983,8 +2991,6 @@
"settings": "Error saving board settings: {{error}}"
},
"labels": {
"click_for_statuses": "Click to view parts statuses",
"partsreceived": "Parts Received",
"actual_in": "Actual In",
"addnewprofile": "Add New Profile",
"alert": "Alert",
@@ -3003,6 +3009,7 @@
"card_size": "Card Size",
"cardcolor": "Colored Cards",
"cardsettings": "Card Settings",
"click_for_statuses": "Click to view parts statuses",
"clm_no": "Claim Number",
"comment": "Comment",
"compact": "Compact Cards",
@@ -3023,6 +3030,7 @@
"orientation": "Board Orientation",
"ownr_nm": "Customer Name",
"paintpriority": "P/P",
"partsreceived": "Parts Received",
"partsstatus": "Parts Status",
"production_note": "Production Note",
"refinishhours": "R",
@@ -3569,17 +3577,12 @@
}
},
"titles": {
"parts_settings": "Parts Management Settings | {{app}}",
"simplified-parts-jobs": "Parts Management | {{app}}",
"accounting-payables": "Payables | {{app}}",
"accounting-payments": "Payments | {{app}}",
"accounting-receivables": "Receivables | {{app}}",
"all_tasks": "All Tasks",
"all_tasks": "All Tasks | {{app}}",
"app": "",
"bc": {
"simplified-parts-jobs": "Jobs",
"parts": "Parts",
"parts_settings": "Settings",
"accounting-payables": "Payables",
"accounting-payments": "Payments",
"accounting-receivables": "Receivables",
@@ -3611,7 +3614,9 @@
"my_tasks": "My Tasks",
"owner-detail": "{{name}}",
"owners": "Owners",
"parts": "Parts",
"parts-queue": "Parts Queue",
"parts_settings": "Settings",
"payments-all": "All Payments",
"phonebook": "Phonebook",
"productionboard": "Production Board - Visual",
@@ -3623,6 +3628,7 @@
"shop-csi": "CSI Responses",
"shop-templates": "Shop Templates",
"shop-vendors": "Vendors",
"simplified-parts-jobs": "Jobs",
"tasks": "Tasks",
"temporarydocs": "Temporary Documents",
"timetickets": "Time Tickets",
@@ -3655,10 +3661,12 @@
"jobsdetail": "Job {{ro_number}} | {{app}}",
"jobsdocuments": "Job Documents {{ro_number}} | {{app}}",
"manageroot": "Home | {{app}}",
"my_tasks": "My Tasks",
"my_tasks": "My Tasks | {{app}}",
"owners": "All Owners | {{app}}",
"owners-detail": "{{name}} | {{app}}",
"parts": "",
"parts-queue": "Parts Queue | {{app}}",
"parts_settings": "Parts Management Settings | {{app}}",
"payments-all": "Payments | {{app}}",
"phonebook": "Phonebook | {{app}}",
"productionboard": "Production Board - Visual | {{app}}",
@@ -3674,6 +3682,7 @@
"shop-csi": "CSI Responses | {{app}}",
"shop-templates": "Shop Templates | {{app}}",
"shop_vendors": "Vendors | {{app}}",
"simplified-parts-jobs": "Parts Management | {{app}}",
"tasks": "Tasks",
"techconsole": "Technician Console | {{app}}",
"techjobclock": "Technician Job Clock | {{app}}",
@@ -3834,10 +3843,10 @@
"user": {
"actions": {
"changepassword": "Change Password",
"signout": "Sign Out",
"updateprofile": "Update Profile",
"dark_theme": "Switch to Dark Theme",
"light_theme": "Switch to Light Theme",
"dark_theme": "Switch to Dark Theme"
"signout": "Sign Out",
"updateprofile": "Update Profile"
},
"errors": {
"updating": "Error updating user or association {{message}}"
@@ -3851,14 +3860,14 @@
"labels": {
"actions": "Actions",
"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_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": {
"passwordchanged": "Password changed successfully. "

View File

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

View File

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

662
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -18,23 +18,23 @@
"job-totals-fixtures:local": "docker exec node-app /usr/bin/node /app/download-job-totals-fixtures.js"
},
"dependencies": {
"@aws-sdk/client-cloudwatch-logs": "^3.975.0",
"@aws-sdk/client-elasticache": "^3.975.0",
"@aws-sdk/client-s3": "^3.975.0",
"@aws-sdk/client-secrets-manager": "^3.975.0",
"@aws-sdk/client-ses": "^3.975.0",
"@aws-sdk/credential-provider-node": "^3.972.1",
"@aws-sdk/lib-storage": "^3.975.0",
"@aws-sdk/s3-request-presigner": "^3.975.0",
"@aws-sdk/client-cloudwatch-logs": "^3.978.0",
"@aws-sdk/client-elasticache": "^3.978.0",
"@aws-sdk/client-s3": "^3.978.0",
"@aws-sdk/client-secrets-manager": "^3.978.0",
"@aws-sdk/client-ses": "^3.978.0",
"@aws-sdk/credential-provider-node": "^3.972.3",
"@aws-sdk/lib-storage": "^3.978.0",
"@aws-sdk/s3-request-presigner": "^3.978.0",
"@opensearch-project/opensearch": "^2.13.0",
"@socket.io/admin-ui": "^0.5.1",
"@socket.io/redis-adapter": "^8.3.0",
"archiver": "^7.0.1",
"aws4": "^1.13.2",
"axios": "^1.13.3",
"axios": "^1.13.4",
"axios-curlirize": "^2.0.0",
"better-queue": "^3.8.12",
"bullmq": "^5.67.1",
"bullmq": "^5.67.2",
"chart.js": "^4.5.1",
"cloudinary": "^2.9.0",
"compression": "^1.8.1",
@@ -44,7 +44,7 @@
"dinero.js": "^1.9.1",
"dotenv": "^17.2.3",
"express": "^4.21.1",
"fast-xml-parser": "^5.3.3",
"fast-xml-parser": "^5.3.4",
"firebase-admin": "^13.6.0",
"graphql": "^16.12.0",
"graphql-request": "^6.1.0",
@@ -65,7 +65,7 @@
"recursive-diff": "^1.0.9",
"rimraf": "^6.1.2",
"skia-canvas": "^3.0.8",
"soap": "^1.6.3",
"soap": "^1.6.4",
"socket.io": "^4.8.3",
"socket.io-adapter": "^2.5.6",
"ssh2-sftp-client": "^11.0.0",
@@ -82,7 +82,7 @@
"@eslint/js": "^9.39.2",
"eslint": "^9.39.2",
"eslint-plugin-react": "^7.37.5",
"globals": "^17.1.0",
"globals": "^17.2.0",
"mock-require": "^3.0.3",
"p-limit": "^3.1.0",
"prettier": "^3.8.1",

View File

@@ -306,8 +306,7 @@ async function FortellisSelectedCustomer({ socket, redisHelpers, selectedCustome
CreateFortellisLogEvent(socket, "ERROR", `{7.1} Error posting vehicle service history. ${error.message}`);
}
//TODO: IF THE VEHICLE SERVICE HISTORY FAILS, WE NEED TO MARK IT AS SUCH AND NOT DELETE THE TRANSACTION.
//socket.emit("export-success", JobData.id);
socket.emit("export-success", JobData.id);
} else {
//There was something wrong. Throw an error to trigger clean up.
//throw new Error("Error posting DMS Batch Transaction");
@@ -431,10 +430,10 @@ async function QueryDmsCustomerByName({ socket, redisHelpers, JobData }) {
const ownerName =
JobData.ownr_co_nm && JobData.ownr_co_nm.trim() !== ""
//? [["firstName", JobData.ownr_co_nm.replace(replaceSpecialRegex, "").toUpperCase()]] // Commented out until we receive direction.
? [["email", JobData.ownr_ea.toUpperCase()]]
? [["phone", JobData.ownr_ph1?.replace(replaceSpecialRegex, "")]]
: [
["firstName", JobData.ownr_fn.replace(replaceSpecialRegex, "").toUpperCase()],
["lastName", JobData.ownr_ln.replace(replaceSpecialRegex, "").toUpperCase()]
["firstName", JobData.ownr_fn?.replace(replaceSpecialRegex, "").toUpperCase()],
["lastName", JobData.ownr_ln?.replace(replaceSpecialRegex, "").toUpperCase()]
];
try {
const result = await MakeFortellisCall({

View File

@@ -1725,6 +1725,7 @@ query QUERY_JOB_COSTING_DETAILS($id: uuid!) {
profitcenter_part
profitcenter_labor
act_price_before_ppc
manual_line
}
bills {
id
@@ -1842,6 +1843,7 @@ exports.QUERY_JOB_COSTING_DETAILS_MULTI = ` query QUERY_JOB_COSTING_DETAILS_MULT
op_code_desc
profitcenter_part
profitcenter_labor
manual_line
}
bills {
id

View File

@@ -343,7 +343,7 @@ function GenerateCostingData(job) {
if (!acc.labor[laborProfitCenter]) acc.labor[laborProfitCenter] = Dinero();
acc.labor[laborProfitCenter] = acc.labor[laborProfitCenter].add(laborAmount);
if (val.act_price > 0 && val.lbr_op === "OP14") {
if (val.act_price > 0 && val.lbr_op === "OP14" && !val.part_type) {
//Scenario where SGI may pay out hours using a part price.
acc.labor[laborProfitCenter] = acc.labor[laborProfitCenter].add(
Dinero({
@@ -363,6 +363,9 @@ function GenerateCostingData(job) {
if (val.mod_lbr_ty !== "LAR" && mashOpCodes.includes(val.lbr_op)) {
materialsHours.mashHrs += val.mod_lb_hrs || 0;
}
if (val.manual_line === true && !mashOpCodes.includes(val.lbr_op) && val.mod_lbr_ty !== "LAR" ) {
materialsHours.mashHrs += val.mod_lb_hrs || 0;
}
}
}
@@ -499,7 +502,7 @@ function GenerateCostingData(job) {
let disc = Dinero(),
markup = Dinero();
const convertedKey = Object.keys(defaultProfits).find((k) => defaultProfits[k] === key);
if (job.parts_tax_rates && job.parts_tax_rates[convertedKey.toUpperCase()]) {
if (convertedKey && job.parts_tax_rates && job.parts_tax_rates[convertedKey.toUpperCase()]) {
if (
job.parts_tax_rates[convertedKey.toUpperCase()].prt_discp !== undefined &&
job.parts_tax_rates[convertedKey.toUpperCase()].prt_discp >= 0
@@ -523,14 +526,16 @@ function GenerateCostingData(job) {
}
if (InstanceManager({ rome: true })) {
const correspondingCiecaStlTotalLine = job.cieca_stl?.data.find(
(c) => c.ttl_typecd === convertedKey.toUpperCase()
);
if (
correspondingCiecaStlTotalLine &&
Math.abs(jobLineTotalsByProfitCenter.parts[key].getAmount() - correspondingCiecaStlTotalLine.ttl_amt * 100) > 1
) {
jobLineTotalsByProfitCenter.parts[key] = jobLineTotalsByProfitCenter.parts[key].add(disc).add(markup);
if (convertedKey) {
const correspondingCiecaStlTotalLine = job.cieca_stl?.data.find(
(c) => c.ttl_typecd === convertedKey.toUpperCase()
);
if (
correspondingCiecaStlTotalLine &&
Math.abs(jobLineTotalsByProfitCenter.parts[key].getAmount() - correspondingCiecaStlTotalLine.ttl_amt * 100) > 1
) {
jobLineTotalsByProfitCenter.parts[key] = jobLineTotalsByProfitCenter.parts[key].add(disc).add(markup);
}
}
}
});