Compare commits
36 Commits
feature/IO
...
feature/IO
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae05692c46 | ||
|
|
e5b7fcb919 | ||
|
|
849d967b56 | ||
|
|
519d7e8d87 | ||
|
|
b08435607e | ||
|
|
ea9e4ffcad | ||
|
|
6c814c7dc6 | ||
|
|
cc9e536059 | ||
|
|
dadc9892d0 | ||
|
|
b05e20ce0d | ||
|
|
eb36b12cb0 | ||
|
|
bf5a099fa6 | ||
|
|
ff3d24c623 | ||
|
|
27b955a701 | ||
|
|
1896c4db59 | ||
|
|
78770ed54e | ||
|
|
9e2ae2cc10 | ||
|
|
3a0f6101c8 | ||
|
|
f0dfa2717f | ||
|
|
1f3be72d9d | ||
|
|
3d9ad799f3 | ||
|
|
6e17ef10bb | ||
|
|
fdc06e79a6 | ||
|
|
66924367fc | ||
|
|
f76165552e | ||
|
|
80fbb847d8 | ||
|
|
ca1703e724 | ||
|
|
163819809c | ||
|
|
42fa85e145 | ||
|
|
13104f36e3 | ||
|
|
0c9f7df9ac | ||
|
|
a9280a83ba | ||
|
|
78d816fa8b | ||
|
|
9f573fc5b4 | ||
|
|
70b6aa63ed | ||
|
|
844a879f1c |
593
_reference/refactorReports/REACT-19-DEPRECATION-FIXES.md
Normal file
593
_reference/refactorReports/REACT-19-DEPRECATION-FIXES.md
Normal 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.
|
||||
110
client/package-lock.json
generated
110
client/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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`}
|
||||
|
||||
@@ -57,7 +57,6 @@ const CardPaymentModalComponent = ({
|
||||
QUERY_RO_AND_OWNER_BY_JOB_PKS,
|
||||
{
|
||||
fetchPolicy: "network-only",
|
||||
notifyOnNetworkStatusChange: true
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -47,7 +47,6 @@ export function ChatPopupComponent({ chatVisible, selectedConversation, toggleCh
|
||||
const [getConversations, { loading, data, refetch, called }] = useLazyQuery(CONVERSATION_LIST_QUERY, {
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only",
|
||||
notifyOnNetworkStatusChange: true,
|
||||
...(pollInterval > 0 ? { pollInterval } : {})
|
||||
});
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -157,7 +157,6 @@ export function JobsDetailHeaderActions({
|
||||
variables: watcherVars,
|
||||
skip: !jobId,
|
||||
fetchPolicy: "cache-first",
|
||||
notifyOnNetworkStatusChange: true
|
||||
});
|
||||
|
||||
const jobWatchersCount = jobWatchersData?.job_watchers?.length ?? job?.job_watchers?.length ?? 0;
|
||||
@@ -179,83 +178,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 +461,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 +954,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 +1146,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 +1177,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 +1220,7 @@ export function JobsDetailHeaderActions({
|
||||
|
||||
return (
|
||||
<>
|
||||
{modalContextHolder}
|
||||
<Modal
|
||||
title={t("menus.jobsactions.cancelallappointments")}
|
||||
open={isCancelScheduleModalVisible}
|
||||
|
||||
@@ -56,7 +56,6 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount,
|
||||
where: whereClause
|
||||
},
|
||||
fetchPolicy: "cache-and-network",
|
||||
notifyOnNetworkStatusChange: true,
|
||||
errorPolicy: "all",
|
||||
pollInterval: isConnected ? 0 : day.duration(NOTIFICATION_POLL_INTERVAL_SECONDS, "seconds").asMilliseconds(),
|
||||
skip: skipQuery
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -17,7 +17,6 @@ import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import { GenerateDocument } from "../../utils/RenderTemplate";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||
import PartsOrderModalComponent from "./parts-order-modal.component";
|
||||
import axios from "axios";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
@@ -66,7 +65,7 @@ export function PartsOrderModalContainer({
|
||||
const sendTypeState = useState("e");
|
||||
const sendType = sendTypeState[0];
|
||||
|
||||
const { loading, error, data } = useQuery(QUERY_ALL_VENDORS_FOR_ORDER, {
|
||||
const { error, data } = useQuery(QUERY_ALL_VENDORS_FOR_ORDER, {
|
||||
skip: !open,
|
||||
variables: { jobId: jobId },
|
||||
fetchPolicy: "network-only",
|
||||
@@ -89,7 +88,8 @@ export function PartsOrderModalContainer({
|
||||
|
||||
return {
|
||||
...p,
|
||||
job_line_id: jobLineId
|
||||
job_line_id: jobLineId,
|
||||
...(isReturn && { cm_received: false })
|
||||
};
|
||||
});
|
||||
|
||||
@@ -371,6 +371,7 @@ export function PartsOrderModalContainer({
|
||||
}
|
||||
}, [open, linesToOrder, form]);
|
||||
|
||||
//This used to have a loading component spinner for the vendor data. With Apollo 4, the NetworkState isn't emitting correctly, so loading just gets set to true the second time, and no longer works as expected.
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
@@ -389,18 +390,14 @@ export function PartsOrderModalContainer({
|
||||
>
|
||||
{error ? <AlertComponent title={error.message} type="error" /> : null}
|
||||
<Form form={form} layout="vertical" autoComplete="no" onFinish={handleFinish} initialValues={initialValues}>
|
||||
{loading ? (
|
||||
<LoadingSpinner />
|
||||
) : (
|
||||
<PartsOrderModalComponent
|
||||
form={form}
|
||||
vendorList={data?.vendors || []}
|
||||
sendTypeState={sendTypeState}
|
||||
isReturn={isReturn}
|
||||
preferredMake={data && data.jobs[0] && data.jobs[0].v_make_desc}
|
||||
job={job}
|
||||
/>
|
||||
)}
|
||||
<PartsOrderModalComponent
|
||||
form={form}
|
||||
vendorList={data?.vendors || []}
|
||||
sendTypeState={sendTypeState}
|
||||
isReturn={isReturn}
|
||||
preferredMake={data && data.jobs[0] && data.jobs[0].v_make_desc}
|
||||
job={job}
|
||||
/>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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") },
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
|
||||
|
||||
@@ -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.",
|
||||
@@ -1782,6 +1784,8 @@
|
||||
"ded_status": "Deductible Status",
|
||||
"depreciation_taxes": "Betterment/Depreciation/Taxes",
|
||||
"dms": {
|
||||
"first_name": "First Name",
|
||||
"last_name": "Last Name",
|
||||
"address": "Customer Address",
|
||||
"amount": "Amount",
|
||||
"center": "Center",
|
||||
@@ -3574,7 +3578,7 @@
|
||||
"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",
|
||||
@@ -3655,7 +3659,7 @@
|
||||
"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-queue": "Parts Queue | {{app}}",
|
||||
|
||||
@@ -1674,7 +1674,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.",
|
||||
@@ -1780,6 +1782,8 @@
|
||||
"ded_status": "Estado deducible",
|
||||
"depreciation_taxes": "Depreciación / Impuestos",
|
||||
"dms": {
|
||||
"first_name": "",
|
||||
"last_name": "",
|
||||
"address": "",
|
||||
"amount": "",
|
||||
"center": "",
|
||||
|
||||
@@ -1674,7 +1674,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.",
|
||||
@@ -1780,6 +1782,8 @@
|
||||
"ded_status": "Statut de franchise",
|
||||
"depreciation_taxes": "Amortissement / taxes",
|
||||
"dms": {
|
||||
"first_name": "",
|
||||
"last_name": "",
|
||||
"address": "",
|
||||
"amount": "",
|
||||
"center": "",
|
||||
|
||||
@@ -248,7 +248,8 @@ const client = new ApolloClient({
|
||||
watchQuery: {
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only",
|
||||
errorPolicy: "ignore"
|
||||
errorPolicy: "ignore",
|
||||
notifyOnNetworkStatusChange: false
|
||||
},
|
||||
query: {
|
||||
fetchPolicy: "network-only",
|
||||
|
||||
662
package-lock.json
generated
662
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
26
package.json
26
package.json
@@ -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",
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user