Compare commits
56 Commits
feature/IO
...
bugfix/IO-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
217a0b84ac | ||
|
|
45e143578c | ||
|
|
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 | ||
|
|
4a1b1fe905 | ||
|
|
5f81ec2099 | ||
|
|
147977be58 | ||
|
|
4dfda4b371 | ||
|
|
02feba2804 | ||
|
|
a9fb77189e | ||
|
|
3bacad69e3 | ||
|
|
70b6aa63ed | ||
|
|
844a879f1c | ||
|
|
6415b302dc | ||
|
|
d40dd649e2 | ||
|
|
35366eda22 | ||
|
|
9a53896aa4 | ||
|
|
278765d019 | ||
|
|
6fd5fc8f66 | ||
|
|
346a6e69c7 | ||
|
|
5d5fa8fead | ||
|
|
30dae4e365 | ||
|
|
f6899f744b | ||
|
|
cca23a5b11 | ||
|
|
2acddcb9ac | ||
|
|
ff57592c12 |
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.
|
||||||
@@ -18,4 +18,3 @@ VITE_PUBLIC_POSTHOG_KEY=phc_xtLmBIu0rjWwExY73Oj5DTH1bGbwq1G1Y8jnlTceien
|
|||||||
VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
|
VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
|
||||||
VITE_APP_AMP_URL=https://vp8k908qy2.execute-api.ca-central-1.amazonaws.com
|
VITE_APP_AMP_URL=https://vp8k908qy2.execute-api.ca-central-1.amazonaws.com
|
||||||
VITE_APP_AMP_KEY=6228a598e57cd66875cfd41604f1f891
|
VITE_APP_AMP_KEY=6228a598e57cd66875cfd41604f1f891
|
||||||
VITE_ENABLE_COMPILER_IN_DEV=1
|
|
||||||
|
|||||||
@@ -20,4 +20,3 @@ VITE_PUBLIC_POSTHOG_KEY=phc_xtLmBIu0rjWwExY73Oj5DTH1bGbwq1G1Y8jnlTceien
|
|||||||
VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
|
VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
|
||||||
VITE_APP_AMP_URL=https://vp8k908qy2.execute-api.ca-central-1.amazonaws.com
|
VITE_APP_AMP_URL=https://vp8k908qy2.execute-api.ca-central-1.amazonaws.com
|
||||||
VITE_APP_AMP_KEY=46b1193a867d4e3131ae4c3a64a3fc78
|
VITE_APP_AMP_KEY=46b1193a867d4e3131ae4c3a64a3fc78
|
||||||
VITE_ENABLE_COMPILER_IN_DEV=1
|
|
||||||
|
|||||||
3
client/.gitignore
vendored
3
client/.gitignore
vendored
@@ -13,3 +13,6 @@ playwright/.cache/
|
|||||||
# Sentry Config File
|
# Sentry Config File
|
||||||
.sentryclirc
|
.sentryclirc
|
||||||
/dev-dist
|
/dev-dist
|
||||||
|
|
||||||
|
# Local environment overrides (not version controlled)
|
||||||
|
.env.development.local.overrides
|
||||||
|
|||||||
915
client/package-lock.json
generated
915
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -8,9 +8,9 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"proxy": "http://localhost:4000",
|
"proxy": "http://localhost:4000",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@amplitude/analytics-browser": "^2.33.4",
|
"@amplitude/analytics-browser": "^2.34.0",
|
||||||
"@ant-design/pro-layout": "^7.22.6",
|
"@ant-design/pro-layout": "^7.22.6",
|
||||||
"@apollo/client": "^4.1.1",
|
"@apollo/client": "^4.1.3",
|
||||||
"@emotion/is-prop-valid": "^1.4.0",
|
"@emotion/is-prop-valid": "^1.4.0",
|
||||||
"@fingerprintjs/fingerprintjs": "^5.0.1",
|
"@fingerprintjs/fingerprintjs": "^5.0.1",
|
||||||
"@firebase/analytics": "^0.10.19",
|
"@firebase/analytics": "^0.10.19",
|
||||||
@@ -21,14 +21,14 @@
|
|||||||
"@jsreport/browser-client": "^3.1.0",
|
"@jsreport/browser-client": "^3.1.0",
|
||||||
"@reduxjs/toolkit": "^2.11.2",
|
"@reduxjs/toolkit": "^2.11.2",
|
||||||
"@sentry/cli": "^3.1.0",
|
"@sentry/cli": "^3.1.0",
|
||||||
"@sentry/react": "^10.35.0",
|
"@sentry/react": "^10.38.0",
|
||||||
"@sentry/vite-plugin": "^4.7.0",
|
"@sentry/vite-plugin": "^4.8.0",
|
||||||
"@splitsoftware/splitio-react": "^2.6.1",
|
"@splitsoftware/splitio-react": "^2.6.1",
|
||||||
"@tanem/react-nprogress": "^5.0.56",
|
"@tanem/react-nprogress": "^5.0.58",
|
||||||
"antd": "^6.2.1",
|
"antd": "^6.2.2",
|
||||||
"apollo-link-logger": "^3.0.0",
|
"apollo-link-logger": "^3.0.0",
|
||||||
"autosize": "^6.0.1",
|
"autosize": "^6.0.1",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.4",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"css-box-model": "^1.2.1",
|
"css-box-model": "^1.2.1",
|
||||||
"dayjs": "^1.11.19",
|
"dayjs": "^1.11.19",
|
||||||
@@ -38,31 +38,31 @@
|
|||||||
"env-cmd": "^11.0.0",
|
"env-cmd": "^11.0.0",
|
||||||
"exifr": "^7.1.3",
|
"exifr": "^7.1.3",
|
||||||
"graphql": "^16.12.0",
|
"graphql": "^16.12.0",
|
||||||
"graphql-ws": "^6.0.6",
|
"graphql-ws": "^6.0.7",
|
||||||
"i18next": "^25.8.0",
|
"i18next": "^25.8.0",
|
||||||
"i18next-browser-languagedetector": "^8.2.0",
|
"i18next-browser-languagedetector": "^8.2.0",
|
||||||
"immutability-helper": "^3.1.1",
|
"immutability-helper": "^3.1.1",
|
||||||
"libphonenumber-js": "^1.12.34",
|
"libphonenumber-js": "^1.12.36",
|
||||||
"lightningcss": "^1.31.0",
|
"lightningcss": "^1.31.1",
|
||||||
"logrocket": "^11.0.0",
|
"logrocket": "^12.0.0",
|
||||||
"markerjs2": "^2.32.7",
|
"markerjs2": "^2.32.7",
|
||||||
"memoize-one": "^6.0.0",
|
"memoize-one": "^6.0.0",
|
||||||
"normalize-url": "^8.1.1",
|
"normalize-url": "^8.1.1",
|
||||||
"object-hash": "^3.0.0",
|
"object-hash": "^3.0.0",
|
||||||
"phone": "^3.1.69",
|
"phone": "^3.1.70",
|
||||||
"posthog-js": "^1.335.0",
|
"posthog-js": "^1.336.4",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"query-string": "^9.3.1",
|
"query-string": "^9.3.1",
|
||||||
"raf-schd": "^4.0.3",
|
"raf-schd": "^4.0.3",
|
||||||
"react": "^19.2.3",
|
"react": "^19.2.4",
|
||||||
"react-big-calendar": "^1.19.4",
|
"react-big-calendar": "^1.19.4",
|
||||||
"react-color": "^2.19.3",
|
"react-color": "^2.19.3",
|
||||||
"react-cookie": "^8.0.1",
|
"react-cookie": "^8.0.1",
|
||||||
"react-dom": "^19.2.3",
|
"react-dom": "^19.2.4",
|
||||||
"react-drag-listview": "^2.0.0",
|
"react-drag-listview": "^2.0.0",
|
||||||
"react-grid-gallery": "^1.0.1",
|
"react-grid-gallery": "^1.0.1",
|
||||||
"react-grid-layout": "^2.2.2",
|
"react-grid-layout": "^2.2.2",
|
||||||
"react-i18next": "^16.5.3",
|
"react-i18next": "^16.5.4",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-image-lightbox": "^5.1.4",
|
"react-image-lightbox": "^5.1.4",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
@@ -71,10 +71,10 @@
|
|||||||
"react-product-fruits": "^2.2.62",
|
"react-product-fruits": "^2.2.62",
|
||||||
"react-redux": "^9.2.0",
|
"react-redux": "^9.2.0",
|
||||||
"react-resizable": "^3.1.3",
|
"react-resizable": "^3.1.3",
|
||||||
"react-router-dom": "^7.12.0",
|
"react-router-dom": "^7.13.0",
|
||||||
"react-sticky": "^6.0.3",
|
"react-sticky": "^6.0.3",
|
||||||
"react-virtuoso": "^4.18.1",
|
"react-virtuoso": "^4.18.1",
|
||||||
"recharts": "^3.6.0",
|
"recharts": "^3.7.0",
|
||||||
"redux": "^5.0.1",
|
"redux": "^5.0.1",
|
||||||
"redux-actions": "^3.0.3",
|
"redux-actions": "^3.0.3",
|
||||||
"redux-persist": "^6.0.0",
|
"redux-persist": "^6.0.0",
|
||||||
@@ -82,7 +82,7 @@
|
|||||||
"redux-state-sync": "^3.1.4",
|
"redux-state-sync": "^3.1.4",
|
||||||
"reselect": "^5.1.1",
|
"reselect": "^5.1.1",
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
"sass": "^1.97.2",
|
"sass": "^1.97.3",
|
||||||
"socket.io-client": "^4.8.3",
|
"socket.io-client": "^4.8.3",
|
||||||
"styled-components": "^6.3.8",
|
"styled-components": "^6.3.8",
|
||||||
"vite-plugin-ejs": "^1.7.0",
|
"vite-plugin-ejs": "^1.7.0",
|
||||||
@@ -92,15 +92,17 @@
|
|||||||
"postinstall": "echo 'when updating react-big-calendar, remember to check to localizer in the calendar wrapper'",
|
"postinstall": "echo 'when updating react-big-calendar, remember to check to localizer in the calendar wrapper'",
|
||||||
"analyze": "source-map-explorer 'build/static/js/*.js'",
|
"analyze": "source-map-explorer 'build/static/js/*.js'",
|
||||||
"start": "vite",
|
"start": "vite",
|
||||||
"build": "dotenvx run --env-file=.env.development.imex -- vite build",
|
"build": "vite build",
|
||||||
"start:imex": "dotenvx run --env-file=.env.development.imex -- vite",
|
"build:dev:imex": "dotenvx run --env-file=.env.development.imex --env-file=.env.development.local.overrides -- vite build",
|
||||||
"start:rome": "dotenvx run --env-file=.env.development.rome -- vite",
|
"build:dev:rome": "dotenvx run --env-file=.env.development.rome --env-file=.env.development.local.overrides -- vite build",
|
||||||
"preview:imex": "dotenvx run --env-file=.env.development.imex -- vite preview",
|
"build:test:imex": "dotenvx run --env-file=.env.test.imex -- vite build",
|
||||||
"preview:rome": "dotenvx run --env-file=.env.development.rome -- vite preview",
|
"build:test:rome": "dotenvx run --env-file=.env.test.rome -- vite build",
|
||||||
"build:test:imex": "env-cmd -f .env.test.imex -- npm run build",
|
"build:production:imex": "env-cmd -f .env.production.imex vite build",
|
||||||
"build:test:rome": "env-cmd -f .env.test.rome -- npm run build",
|
"build:production:rome": "env-cmd -f .env.production.rome vite build",
|
||||||
"build:production:imex": "env-cmd -f .env.production.imex -- npm run build",
|
"start:imex": "dotenvx run --env-file=.env.development.imex --env-file=.env.development.local.overrides -- vite",
|
||||||
"build:production:rome": "env-cmd -f .env.production.rome -- npm run build",
|
"start:rome": "dotenvx run --env-file=.env.development.rome --env-file=.env.development.local.overrides -- vite",
|
||||||
|
"preview:imex": "dotenvx run --env-file=.env.development.imex --env-file=.env.development.local.overrides -- vite preview",
|
||||||
|
"preview:rome": "dotenvx run --env-file=.env.development.rome --env-file=.env.development.local.overrides -- vite preview",
|
||||||
"madge": "madge --image ./madge-graph.svg --extensions js,jsx,ts,tsx --circular .",
|
"madge": "madge --image ./madge-graph.svg --extensions js,jsx,ts,tsx --circular .",
|
||||||
"eulaize": "node src/utils/eulaize.js",
|
"eulaize": "node src/utils/eulaize.js",
|
||||||
"test:unit": "vitest run",
|
"test:unit": "vitest run",
|
||||||
@@ -151,7 +153,7 @@
|
|||||||
"eslint": "^9.39.2",
|
"eslint": "^9.39.2",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"eslint-plugin-react-compiler": "^19.1.0-rc.2",
|
"eslint-plugin-react-compiler": "^19.1.0-rc.2",
|
||||||
"globals": "^17.1.0",
|
"globals": "^17.2.0",
|
||||||
"jsdom": "^27.4.0",
|
"jsdom": "^27.4.0",
|
||||||
"memfs": "^4.56.10",
|
"memfs": "^4.56.10",
|
||||||
"os-browserify": "^0.3.0",
|
"os-browserify": "^0.3.0",
|
||||||
|
|||||||
@@ -100,14 +100,7 @@ export function App({
|
|||||||
if (currentUser.authorized && bodyshop) {
|
if (currentUser.authorized && bodyshop) {
|
||||||
client.setAttribute("imexshopid", bodyshop.imexshopid);
|
client.setAttribute("imexshopid", bodyshop.imexshopid);
|
||||||
|
|
||||||
if (
|
if (client.getTreatment("LogRocket_Tracking") === "on") {
|
||||||
client.getTreatment("LogRocket_Tracking") === "on" ||
|
|
||||||
window.location.hostname ===
|
|
||||||
InstanceRenderMgr({
|
|
||||||
imex: "beta.imex.online",
|
|
||||||
rome: "beta.romeonline.io"
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
console.log("LR Start");
|
console.log("LR Start");
|
||||||
LogRocket.init(
|
LogRocket.init(
|
||||||
InstanceRenderMgr({
|
InstanceRenderMgr({
|
||||||
|
|||||||
@@ -169,7 +169,13 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, bills, ref
|
|||||||
refetch={refetch}
|
refetch={refetch}
|
||||||
/>
|
/>
|
||||||
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && <QboAuthorizeComponent />}
|
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && <QboAuthorizeComponent />}
|
||||||
<Input value={state.search} onChange={handleSearch} placeholder={t("general.labels.search")} allowClear />
|
<Input
|
||||||
|
value={state.search}
|
||||||
|
onChange={handleSearch}
|
||||||
|
placeholder={t("general.labels.search")}
|
||||||
|
allowClear
|
||||||
|
enterButton
|
||||||
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -182,7 +182,13 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, payments,
|
|||||||
refetch={refetch}
|
refetch={refetch}
|
||||||
/>
|
/>
|
||||||
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && <QboAuthorizeComponent />}
|
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && <QboAuthorizeComponent />}
|
||||||
<Input value={state.search} onChange={handleSearch} placeholder={t("general.labels.search")} allowClear />
|
<Input
|
||||||
|
value={state.search}
|
||||||
|
onChange={handleSearch}
|
||||||
|
placeholder={t("general.labels.search")}
|
||||||
|
allowClear
|
||||||
|
enterButton
|
||||||
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -204,6 +204,7 @@ export function AccountingReceivablesTableComponent({ bodyshop, loading, jobs, r
|
|||||||
onChange={handleSearch}
|
onChange={handleSearch}
|
||||||
placeholder={t("general.labels.search")}
|
placeholder={t("general.labels.search")}
|
||||||
allowClear
|
allowClear
|
||||||
|
enterButton
|
||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export function BillDetailEditReturn({ setPartsOrderContext, data, disabled }) {
|
|||||||
// db_price: i.actual_price,
|
// db_price: i.actual_price,
|
||||||
act_price: i.actual_price,
|
act_price: i.actual_price,
|
||||||
cost: i.actual_cost,
|
cost: i.actual_cost,
|
||||||
quantity: i.quantity,
|
part_qty: i.quantity,
|
||||||
joblineid: i.joblineid,
|
joblineid: i.joblineid,
|
||||||
oem_partno: i.jobline && i.jobline.oem_partno,
|
oem_partno: i.jobline && i.jobline.oem_partno,
|
||||||
part_type: i.jobline && i.jobline.part_type
|
part_type: i.jobline && i.jobline.part_type
|
||||||
@@ -104,6 +104,10 @@ export function BillDetailEditReturn({ setPartsOrderContext, data, disabled }) {
|
|||||||
{fields.map((field, index) => (
|
{fields.map((field, index) => (
|
||||||
<tr key={field.key}>
|
<tr key={field.key}>
|
||||||
<td>
|
<td>
|
||||||
|
{/* Hidden field to preserve the id */}
|
||||||
|
<Form.Item name={[field.name, "id"]} hidden>
|
||||||
|
<input type="hidden" />
|
||||||
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
// label={t("joblines.fields.selected")}
|
// label={t("joblines.fields.selected")}
|
||||||
key={`${index}selected`}
|
key={`${index}selected`}
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ export function BillEnterModalLinesComponent({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Only fill actual_cost when the user forward-tabs out of Retail (actual_price)
|
||||||
const autofillActualCost = (index) => {
|
const autofillActualCost = (index) => {
|
||||||
Promise.resolve().then(() => {
|
Promise.resolve().then(() => {
|
||||||
const retailRaw = form.getFieldValue(["billlines", index, "actual_price"]);
|
const retailRaw = form.getFieldValue(["billlines", index, "actual_price"]);
|
||||||
@@ -164,10 +165,9 @@ export function BillEnterModalLinesComponent({
|
|||||||
}}
|
}}
|
||||||
allowRemoved={form.getFieldValue("is_credit_memo") || false}
|
allowRemoved={form.getFieldValue("is_credit_memo") || false}
|
||||||
onSelect={(value, opt) => {
|
onSelect={(value, opt) => {
|
||||||
const d = normalizeDiscount(discount);
|
// IMPORTANT:
|
||||||
const retail = Number(opt.cost);
|
// Do NOT autofill actual_cost here. It should only fill when the user forward-tabs
|
||||||
const computedActual = Number.isFinite(retail) ? round2(retail * (1 - d)) : null;
|
// from Retail (actual_price) -> Actual Cost (actual_cost).
|
||||||
|
|
||||||
setFieldsValue({
|
setFieldsValue({
|
||||||
billlines: (getFieldValue("billlines") || []).map((item, idx) => {
|
billlines: (getFieldValue("billlines") || []).map((item, idx) => {
|
||||||
if (idx !== index) return item;
|
if (idx !== index) return item;
|
||||||
@@ -178,7 +178,7 @@ export function BillEnterModalLinesComponent({
|
|||||||
quantity: opt.part_qty || 1,
|
quantity: opt.part_qty || 1,
|
||||||
actual_price: opt.cost,
|
actual_price: opt.cost,
|
||||||
original_actual_price: opt.cost,
|
original_actual_price: opt.cost,
|
||||||
actual_cost: isBlank(item.actual_cost) ? computedActual : item.actual_cost,
|
// actual_cost intentionally untouched here
|
||||||
cost_center: opt.part_type
|
cost_center: opt.part_type
|
||||||
? bodyshopHasDmsKey(bodyshop)
|
? bodyshopHasDmsKey(bodyshop)
|
||||||
? opt.part_type !== "PAE"
|
? opt.part_type !== "PAE"
|
||||||
@@ -251,9 +251,9 @@ export function BillEnterModalLinesComponent({
|
|||||||
<CurrencyInput
|
<CurrencyInput
|
||||||
min={0}
|
min={0}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onBlur={() => autofillActualCost(index)}
|
// NOTE: Autofill should only happen on forward Tab out of Retail
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Tab") autofillActualCost(index);
|
if (e.key === "Tab" && !e.shiftKey) autofillActualCost(index);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
@@ -329,7 +329,7 @@ export function BillEnterModalLinesComponent({
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
controls={false}
|
controls={false}
|
||||||
style={{ width: "100%", height: CONTROL_HEIGHT }}
|
style={{ width: "100%", height: CONTROL_HEIGHT }}
|
||||||
onFocus={() => autofillActualCost(index)}
|
// NOTE: No auto-fill on focus/blur; only triggered from Retail on Tab
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -232,6 +232,7 @@ export function BillsListTableComponent({
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setSearchText(e.target.value);
|
setSearchText(e.target.value);
|
||||||
}}
|
}}
|
||||||
|
enterButton
|
||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,9 +108,12 @@ export function ChatPopupComponent({ chatVisible, selectedConversation, toggleCh
|
|||||||
hasLoadedConversationsOnceRef.current = true;
|
hasLoadedConversationsOnceRef.current = true;
|
||||||
|
|
||||||
getConversations({ variables: { offset: 0 } }).catch((err) => {
|
getConversations({ variables: { offset: 0 } }).catch((err) => {
|
||||||
console.error(`Error fetching conversations: ${err?.message || ""}`, err);
|
// Ignore abort errors (they're expected when component unmounts)
|
||||||
|
if (err?.name !== "AbortError") {
|
||||||
|
console.error(`Error fetching conversations: ${err?.message || ""}`, err);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}, [getConversations]);
|
}, []);
|
||||||
|
|
||||||
const handleManualRefresh = async () => {
|
const handleManualRefresh = async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ export default function ContractsCarsComponent({ loading, data, selectedCarId, h
|
|||||||
placeholder={t("general.labels.search")}
|
placeholder={t("general.labels.search")}
|
||||||
value={state.search}
|
value={state.search}
|
||||||
onChange={(e) => setState({ ...state, search: e.target.value })}
|
onChange={(e) => setState({ ...state, search: e.target.value })}
|
||||||
|
enterButton
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { WarningFilled } from "@ant-design/icons";
|
import { WarningFilled } from "@ant-design/icons";
|
||||||
import { Form, Input, InputNumber, Space } from "antd";
|
import { Card, Form, Input, InputNumber, Space } from "antd";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { DateFormatter } from "../../utils/DateFormatter";
|
import { DateFormatter } from "../../utils/DateFormatter";
|
||||||
import dayjs from "../../utils/day";
|
import dayjs from "../../utils/day";
|
||||||
@@ -19,9 +19,9 @@ import ContractFormJobPrefill from "./contract-form-job-prefill.component";
|
|||||||
export default function ContractFormComponent({ form, create = false, selectedJobState, selectedCar }) {
|
export default function ContractFormComponent({ form, create = false, selectedJobState, selectedCar }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<>
|
<Card>
|
||||||
{!create && <FormFieldsChanged form={form} />}
|
{!create && <FormFieldsChanged form={form} />}
|
||||||
<LayoutFormRow>
|
<LayoutFormRow noDivider={true}>
|
||||||
{!create && (
|
{!create && (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("contracts.fields.status")}
|
label={t("contracts.fields.status")}
|
||||||
@@ -310,6 +310,6 @@ export default function ContractFormComponent({ form, create = false, selectedJo
|
|||||||
<InputNumber precision={2} />
|
<InputNumber precision={2} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
</>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ export default function ContractsJobsComponent({ loading, data, selectedJob, han
|
|||||||
placeholder={t("general.labels.search")}
|
placeholder={t("general.labels.search")}
|
||||||
value={state.search}
|
value={state.search}
|
||||||
onChange={(e) => setState({ ...state, search: e.target.value })}
|
onChange={(e) => setState({ ...state, search: e.target.value })}
|
||||||
|
enterButton
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ export function ContractsList({ bodyshop, loading, contracts, refetch, total, se
|
|||||||
const updatedSearch = { ...search, search: value };
|
const updatedSearch = { ...search, search: value };
|
||||||
history({ search: queryString.stringify(updatedSearch) });
|
history({ search: queryString.stringify(updatedSearch) });
|
||||||
}}
|
}}
|
||||||
|
enterButton
|
||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { WarningFilled } from "@ant-design/icons";
|
import { WarningFilled } from "@ant-design/icons";
|
||||||
import { useApolloClient } from "@apollo/client/react";
|
import { useApolloClient } from "@apollo/client/react";
|
||||||
import { Button, Form, Input, InputNumber, Space } from "antd";
|
import { Button, Card, Form, Input, InputNumber, Space } from "antd";
|
||||||
import { PageHeader } from "@ant-design/pro-layout";
|
import { PageHeader } from "@ant-design/pro-layout";
|
||||||
import dayjs from "../../utils/day";
|
import dayjs from "../../utils/day";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -19,7 +19,7 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading, newC
|
|||||||
const client = useApolloClient();
|
const client = useApolloClient();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<Card>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title={t("menus.header.courtesycars")}
|
title={t("menus.header.courtesycars")}
|
||||||
extra={
|
extra={
|
||||||
@@ -314,6 +314,6 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading, newC
|
|||||||
<CurrencyInput />
|
<CurrencyInput />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
</div>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ export default function DashboardMonthlyJobCosting({ data, ...cardProps }) {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setSearchText(e.target.value);
|
setSearchText(e.target.value);
|
||||||
}}
|
}}
|
||||||
|
enterButton
|
||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,12 +49,15 @@ export function DmsCdkVehicles({ form, job }) {
|
|||||||
open={open}
|
open={open}
|
||||||
onCancel={() => setOpen(false)}
|
onCancel={() => setOpen(false)}
|
||||||
onOk={() => {
|
onOk={() => {
|
||||||
form.setFieldsValue({
|
if (selectedModel) {
|
||||||
dms_make: selectedModel.makecode,
|
form.setFieldsValue({
|
||||||
dms_model: selectedModel.modelcode
|
dms_make: selectedModel.makecode,
|
||||||
});
|
dms_model: selectedModel.modelcode
|
||||||
setOpen(false);
|
});
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
|
okButtonProps={{ disabled: !selectedModel }}
|
||||||
>
|
>
|
||||||
{error && <AlertComponent title={error.message} type="error" />}
|
{error && <AlertComponent title={error.message} type="error" />}
|
||||||
<Table
|
<Table
|
||||||
@@ -62,6 +65,7 @@ export function DmsCdkVehicles({ form, job }) {
|
|||||||
<Input.Search
|
<Input.Search
|
||||||
onSearch={(val) => callSearch({ variables: { search: val } })}
|
onSearch={(val) => callSearch({ variables: { search: val } })}
|
||||||
placeholder={t("general.labels.search")}
|
placeholder={t("general.labels.search")}
|
||||||
|
enterButton
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
|||||||
@@ -53,13 +53,13 @@ export default function FortellisCustomerSelector({ bodyshop, jobid, socket }) {
|
|||||||
render: (_t, r) => <Checkbox disabled checked={r.vinOwner} />
|
render: (_t, r) => <Checkbox disabled checked={r.vinOwner} />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("jobs.fields.dms.name1"),
|
title: t("jobs.fields.dms.first_name"),
|
||||||
dataIndex: ["customerName", "firstName"],
|
dataIndex: ["customerName", "firstName"],
|
||||||
key: "firstName",
|
key: "firstName",
|
||||||
sorter: (a, b) => alphaSort(a.customerName?.firstName, b.customerName?.firstName)
|
sorter: (a, b) => alphaSort(a.customerName?.firstName, b.customerName?.firstName)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("jobs.fields.dms.name1"),
|
title: t("jobs.fields.dms.last_name"),
|
||||||
dataIndex: ["customerName", "lastName"],
|
dataIndex: ["customerName", "lastName"],
|
||||||
key: "lastName",
|
key: "lastName",
|
||||||
sorter: (a, b) => alphaSort(a.customerName?.lastName, b.customerName?.lastName)
|
sorter: (a, b) => alphaSort(a.customerName?.lastName, b.customerName?.lastName)
|
||||||
|
|||||||
@@ -272,7 +272,7 @@ export default function CdkLikePostForm({ bodyshop, socket, job, logsRef, mode,
|
|||||||
name={[field.name, "name"]}
|
name={[field.name, "name"]}
|
||||||
rules={[{ required: true }]}
|
rules={[{ required: true }]}
|
||||||
>
|
>
|
||||||
<Select style={{ minWidth: "15rem" }} onSelect={(value) => handlePayerSelect(value, index)}>
|
<Select style={{ width: "100%" }} onSelect={(value) => handlePayerSelect(value, index)}>
|
||||||
{bodyshop.cdk_configuration?.payers?.map((payer) => (
|
{bodyshop.cdk_configuration?.payers?.map((payer) => (
|
||||||
<Select.Option key={payer.name}>{payer.name}</Select.Option>
|
<Select.Option key={payer.name}>{payer.name}</Select.Option>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ export default function JobCostingPartsTable({ data, summaryData }) {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setSearchText(e.target.value);
|
setSearchText(e.target.value);
|
||||||
}}
|
}}
|
||||||
|
enterButton
|
||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -50,7 +50,8 @@ export function JobCreateIOU({ bodyshop, currentUser, job, selectedJobLines, tec
|
|||||||
config: {
|
config: {
|
||||||
status: bodyshop.md_ro_statuses.default_open,
|
status: bodyshop.md_ro_statuses.default_open,
|
||||||
bodyshopid: bodyshop.id,
|
bodyshopid: bodyshop.id,
|
||||||
useremail: currentUser.email
|
useremail: currentUser.email,
|
||||||
|
timezone: bodyshop.timezone
|
||||||
},
|
},
|
||||||
currentUser
|
currentUser
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -682,6 +682,7 @@ export function JobLinesComponent({
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setSearchText(e.target.value);
|
setSearchText(e.target.value);
|
||||||
}}
|
}}
|
||||||
|
enterButton
|
||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,6 +136,7 @@ export function JobsAvailableScan({ partnerVersion, refetch }) {
|
|||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setSearchText(e.currentTarget.value);
|
setSearchText(e.currentTarget.value);
|
||||||
}}
|
}}
|
||||||
|
enterButton
|
||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -196,13 +196,16 @@ export function JobsAvailableComponent({ bodyshop, loading, data, refetch, addJo
|
|||||||
>
|
>
|
||||||
{t("general.actions.deleteall")}
|
{t("general.actions.deleteall")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Input.Search
|
<Input.Search
|
||||||
placeholder={t("general.labels.search")}
|
placeholder={t("general.labels.search")}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setSearchText(e.currentTarget.value);
|
setSearchText(e.currentTarget.value);
|
||||||
}}
|
}}
|
||||||
|
enterButton
|
||||||
/>
|
/>
|
||||||
|
<Link to="/manage/jobs/new">
|
||||||
|
<Button>{t("jobs.actions.manualnew")}</Button>
|
||||||
|
</Link>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -42,6 +42,10 @@ export function JobsCloseLines({ bodyshop, job, jobRO }) {
|
|||||||
<tbody>
|
<tbody>
|
||||||
{fields.map((field, index) => (
|
{fields.map((field, index) => (
|
||||||
<tr key={field.key}>
|
<tr key={field.key}>
|
||||||
|
{/* Hidden field to preserve jobline ID */}
|
||||||
|
<Form.Item hidden name={[field.name, "id"]}>
|
||||||
|
<input />
|
||||||
|
</Form.Item>
|
||||||
<td>
|
<td>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
// label={t("joblines.fields.line_desc")}
|
// label={t("joblines.fields.line_desc")}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export default function JobsCreateVehicleInfoPredefined({ disabled, form }) {
|
|||||||
<div>
|
<div>
|
||||||
<Table
|
<Table
|
||||||
size="small"
|
size="small"
|
||||||
title={() => <Input.Search onSearch={(value) => setSearch(value)} />}
|
title={() => <Input.Search onSearch={(value) => setSearch(value)} enterButton/>}
|
||||||
dataSource={filteredPredefinedVehicles}
|
dataSource={filteredPredefinedVehicles}
|
||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { DownCircleFilled } from "@ant-design/icons";
|
import { DownCircleFilled } from "@ant-design/icons";
|
||||||
import { useApolloClient, useMutation, useQuery } from "@apollo/client/react";
|
import { useApolloClient, useMutation, useQuery } from "@apollo/client/react";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||||
import { Button, Card, Dropdown, Form, Input, Modal, Popconfirm, Popover, Select, Space } from "antd";
|
import { Button, Card, Dropdown, Form, Input, Modal, Popover, Select, Space } from "antd";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import parsePhoneNumber from "libphonenumber-js";
|
import parsePhoneNumber from "libphonenumber-js";
|
||||||
import { useCallback, useMemo, useRef, useState } from "react";
|
import { useCallback, useMemo, useRef, useState } from "react";
|
||||||
|
import { HasRbacAccess } from "../../components/rbac-wrapper/rbac-wrapper.component";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
@@ -20,7 +21,7 @@ import { selectJobReadOnly } from "../../redux/application/application.selectors
|
|||||||
import { setEmailOptions } from "../../redux/email/email.actions";
|
import { setEmailOptions } from "../../redux/email/email.actions";
|
||||||
import { openChatByPhone, setMessage } from "../../redux/messaging/messaging.actions";
|
import { openChatByPhone, setMessage } from "../../redux/messaging/messaging.actions";
|
||||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
import { selectAuthLevel, selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||||
import { TemplateList } from "../../utils/TemplateConstants";
|
import { TemplateList } from "../../utils/TemplateConstants";
|
||||||
@@ -28,7 +29,6 @@ import dayjs from "../../utils/day";
|
|||||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||||
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
|
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
|
||||||
import LockerWrapperComponent from "../lock-wrapper/lock-wrapper.component";
|
import LockerWrapperComponent from "../lock-wrapper/lock-wrapper.component";
|
||||||
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
|
|
||||||
import ShareToTeamsButton from "../share-to-teams/share-to-teams.component.jsx";
|
import ShareToTeamsButton from "../share-to-teams/share-to-teams.component.jsx";
|
||||||
import AddToProduction from "./jobs-detail-header-actions.addtoproduction.util";
|
import AddToProduction from "./jobs-detail-header-actions.addtoproduction.util";
|
||||||
import DuplicateJob from "./jobs-detail-header-actions.duplicate.util";
|
import DuplicateJob from "./jobs-detail-header-actions.duplicate.util";
|
||||||
@@ -39,7 +39,8 @@ const EMPTY_ARRAY = Object.freeze([]);
|
|||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
jobRO: selectJobReadOnly,
|
jobRO: selectJobReadOnly,
|
||||||
currentUser: selectCurrentUser
|
currentUser: selectCurrentUser,
|
||||||
|
authLevel: selectAuthLevel
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
@@ -117,7 +118,8 @@ export function JobsDetailHeaderActions({
|
|||||||
openChatByPhone,
|
openChatByPhone,
|
||||||
setMessage,
|
setMessage,
|
||||||
setTimeTicketTaskContext,
|
setTimeTicketTaskContext,
|
||||||
setTaskUpsertContext
|
setTaskUpsertContext,
|
||||||
|
authLevel
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const client = useApolloClient();
|
const client = useApolloClient();
|
||||||
@@ -129,10 +131,6 @@ export function JobsDetailHeaderActions({
|
|||||||
const jobId = job?.id;
|
const jobId = job?.id;
|
||||||
const watcherVars = useMemo(() => ({ jobid: jobId }), [jobId]);
|
const watcherVars = useMemo(() => ({ jobid: jobId }), [jobId]);
|
||||||
|
|
||||||
// Option A: coordinated Dropdown + Popconfirm open state so the menu doesn't unmount before Popconfirm renders.
|
|
||||||
const [confirmKey, setConfirmKey] = useState(null);
|
|
||||||
const confirmKeyRef = useRef(null);
|
|
||||||
|
|
||||||
const [isCancelScheduleModalVisible, setIsCancelScheduleModalVisible] = useState(false);
|
const [isCancelScheduleModalVisible, setIsCancelScheduleModalVisible] = useState(false);
|
||||||
const [insertAppointment] = useMutation(INSERT_MANUAL_APPT);
|
const [insertAppointment] = useMutation(INSERT_MANUAL_APPT);
|
||||||
const [deleteJob] = useMutation(DELETE_JOB);
|
const [deleteJob] = useMutation(DELETE_JOB);
|
||||||
@@ -150,6 +148,8 @@ export function JobsDetailHeaderActions({
|
|||||||
const devEmails = ["imex.dev", "rome.dev"];
|
const devEmails = ["imex.dev", "rome.dev"];
|
||||||
const prodEmails = ["imex.prod", "rome.prod", "imex.test", "rome.test"];
|
const prodEmails = ["imex.prod", "rome.prod", "imex.test", "rome.test"];
|
||||||
|
|
||||||
|
const canVoidJob = useMemo(() => HasRbacAccess({ authLevel, bodyshop, action: "jobs:void" }), [authLevel, bodyshop]);
|
||||||
|
|
||||||
const hasValidEmail = (emails) => emails.some((email) => userEmail.endsWith(email));
|
const hasValidEmail = (emails) => emails.some((email) => userEmail.endsWith(email));
|
||||||
const canSubmitForTesting = (isDevEnv && hasValidEmail(devEmails)) || (isProdEnv && hasValidEmail(prodEmails));
|
const canSubmitForTesting = (isDevEnv && hasValidEmail(devEmails)) || (isProdEnv && hasValidEmail(prodEmails));
|
||||||
|
|
||||||
@@ -179,83 +179,69 @@ export function JobsDetailHeaderActions({
|
|||||||
const jobInPreProduction = preProductionStatuses.includes(jobStatus);
|
const jobInPreProduction = preProductionStatuses.includes(jobStatus);
|
||||||
const jobInPostProduction = postProductionStatuses.includes(jobStatus);
|
const jobInPostProduction = postProductionStatuses.includes(jobStatus);
|
||||||
|
|
||||||
const openConfirm = useCallback((key) => {
|
const makeConfirmId = () =>
|
||||||
confirmKeyRef.current = key;
|
globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||||||
setConfirmKey(key);
|
|
||||||
setDropdownOpen(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const closeConfirm = useCallback(() => {
|
const [modal, modalContextHolder] = Modal.useModal();
|
||||||
confirmKeyRef.current = null;
|
|
||||||
setConfirmKey(null);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleDropdownOpenChange = useCallback(
|
const confirmInstancesRef = useRef(new Map());
|
||||||
(nextOpen, info) => {
|
|
||||||
if (!nextOpen && info?.source === "menu" && confirmKeyRef.current) return;
|
|
||||||
setDropdownOpen(nextOpen);
|
|
||||||
if (!nextOpen) closeConfirm();
|
|
||||||
},
|
|
||||||
[closeConfirm]
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderPopconfirmMenuLabel = ({
|
const closeConfirmById = (id) => {
|
||||||
key,
|
const inst = confirmInstancesRef.current.get(id);
|
||||||
text,
|
if (inst) inst.destroy(); // hard close
|
||||||
|
confirmInstancesRef.current.delete(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openConfirmFromMenu = ({
|
||||||
|
variant = "confirm", // "confirm" | "info" | "warning"
|
||||||
title,
|
title,
|
||||||
|
content,
|
||||||
okText,
|
okText,
|
||||||
cancelText,
|
cancelText,
|
||||||
showCancel = true,
|
showCancel = true,
|
||||||
closeDropdownOnConfirm = true,
|
onOk,
|
||||||
onConfirm
|
onCancel
|
||||||
}) => (
|
}) => {
|
||||||
<Popconfirm
|
// close the dropdown immediately; confirm dialog is separate
|
||||||
title={title}
|
setDropdownOpen(false);
|
||||||
okText={okText}
|
|
||||||
cancelText={cancelText}
|
|
||||||
showCancel={showCancel}
|
|
||||||
open={confirmKey === key}
|
|
||||||
onOpenChange={(nextOpen) => {
|
|
||||||
if (nextOpen) openConfirm(key);
|
|
||||||
else closeConfirm();
|
|
||||||
}}
|
|
||||||
onConfirm={(e) => {
|
|
||||||
e?.stopPropagation?.();
|
|
||||||
closeConfirm();
|
|
||||||
|
|
||||||
// Critical: for informational popconfirms, keep the dropdown open so the Popconfirm can cleanly close.
|
const id = makeConfirmId();
|
||||||
if (closeDropdownOnConfirm) {
|
|
||||||
setDropdownOpen(false);
|
const openFn = variant === "info" ? modal.info : variant === "warning" ? modal.warning : modal.confirm;
|
||||||
|
|
||||||
|
const inst = openFn({
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
okText,
|
||||||
|
cancelText,
|
||||||
|
centered: true,
|
||||||
|
maskClosable: false,
|
||||||
|
onCancel: () => {
|
||||||
|
closeConfirmById(id);
|
||||||
|
onCancel?.();
|
||||||
|
},
|
||||||
|
onOk: async () => {
|
||||||
|
try {
|
||||||
|
await onOk?.();
|
||||||
|
} finally {
|
||||||
|
closeConfirmById(id);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
...(showCancel ? {} : { okCancel: false })
|
||||||
|
});
|
||||||
|
|
||||||
onConfirm?.(e);
|
confirmInstancesRef.current.set(id, inst);
|
||||||
}}
|
return id;
|
||||||
onCancel={(e) => {
|
};
|
||||||
e?.stopPropagation?.();
|
|
||||||
closeConfirm();
|
const handleDropdownOpenChange = useCallback((nextOpen) => {
|
||||||
// Keep dropdown open on cancel so the user can continue using the menu.
|
setDropdownOpen(nextOpen);
|
||||||
}}
|
}, []);
|
||||||
getPopupContainer={() => document.body}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{ width: "100%" }}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
openConfirm(key);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{text}
|
|
||||||
</div>
|
|
||||||
</Popconfirm>
|
|
||||||
);
|
|
||||||
|
|
||||||
// Function to show modal
|
|
||||||
const showCancelScheduleModal = () => {
|
const showCancelScheduleModal = () => {
|
||||||
setIsCancelScheduleModalVisible(true);
|
setIsCancelScheduleModalVisible(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Function to handle Cancel
|
|
||||||
const handleCancelScheduleModalCancel = () => {
|
const handleCancelScheduleModalCancel = () => {
|
||||||
setIsCancelScheduleModalVisible(false);
|
setIsCancelScheduleModalVisible(false);
|
||||||
};
|
};
|
||||||
@@ -264,7 +250,7 @@ export function JobsDetailHeaderActions({
|
|||||||
DuplicateJob({
|
DuplicateJob({
|
||||||
apolloClient: client,
|
apolloClient: client,
|
||||||
jobId: job.id,
|
jobId: job.id,
|
||||||
config: { defaultOpenStatus: bodyshop.md_ro_statuses.default_imported },
|
config: { defaultOpenStatus: bodyshop.md_ro_statuses.default_imported, timezone: bodyshop.timezone },
|
||||||
completionCallback: (newJobId) => {
|
completionCallback: (newJobId) => {
|
||||||
history(`/manage/jobs/${newJobId}`);
|
history(`/manage/jobs/${newJobId}`);
|
||||||
notification.success({
|
notification.success({
|
||||||
@@ -279,7 +265,7 @@ export function JobsDetailHeaderActions({
|
|||||||
DuplicateJob({
|
DuplicateJob({
|
||||||
apolloClient: client,
|
apolloClient: client,
|
||||||
jobId: job.id,
|
jobId: job.id,
|
||||||
config: { defaultOpenStatus: bodyshop.md_ro_statuses.default_imported },
|
config: { defaultOpenStatus: bodyshop.md_ro_statuses.default_imported, timezone: bodyshop.timezone },
|
||||||
completionCallback: (newJobId) => {
|
completionCallback: (newJobId) => {
|
||||||
history(`/manage/jobs/${newJobId}`);
|
history(`/manage/jobs/${newJobId}`);
|
||||||
notification.success({
|
notification.success({
|
||||||
@@ -476,6 +462,11 @@ export function JobsDetailHeaderActions({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleVoidJob = async () => {
|
const handleVoidJob = async () => {
|
||||||
|
if (!canVoidJob) {
|
||||||
|
notification.error({ title: t("general.messages.rbacunauth") });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
//delete the job.
|
//delete the job.
|
||||||
const result = await voidJob({
|
const result = await voidJob({
|
||||||
variables: {
|
variables: {
|
||||||
@@ -964,26 +955,26 @@ export function JobsDetailHeaderActions({
|
|||||||
{
|
{
|
||||||
key: "duplicate",
|
key: "duplicate",
|
||||||
id: "job-actions-duplicate",
|
id: "job-actions-duplicate",
|
||||||
label: renderPopconfirmMenuLabel({
|
label: t("menus.jobsactions.duplicate"),
|
||||||
key: "confirm-duplicate",
|
onClick: () =>
|
||||||
text: t("menus.jobsactions.duplicate"),
|
openConfirmFromMenu({
|
||||||
title: t("jobs.labels.duplicateconfirm"),
|
title: t("jobs.labels.duplicateconfirm"),
|
||||||
okText: t("general.labels.yes"),
|
okText: t("general.labels.yes"),
|
||||||
cancelText: t("general.labels.no"),
|
cancelText: t("general.labels.no"),
|
||||||
onConfirm: handleDuplicate
|
onOk: handleDuplicate
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "duplicatenolines",
|
key: "duplicatenolines",
|
||||||
id: "job-actions-duplicatenolines",
|
id: "job-actions-duplicatenolines",
|
||||||
label: renderPopconfirmMenuLabel({
|
label: t("menus.jobsactions.duplicatenolines"),
|
||||||
key: "confirm-duplicate-nolines",
|
onClick: () =>
|
||||||
text: t("menus.jobsactions.duplicatenolines"),
|
openConfirmFromMenu({
|
||||||
title: t("jobs.labels.duplicateconfirm"),
|
title: t("jobs.labels.duplicateconfirm"),
|
||||||
okText: t("general.labels.yes"),
|
okText: t("general.labels.yes"),
|
||||||
cancelText: t("general.labels.no"),
|
cancelText: t("general.labels.no"),
|
||||||
onConfirm: handleDuplicateConfirm
|
onOk: handleDuplicateConfirm
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -1156,26 +1147,25 @@ export function JobsDetailHeaderActions({
|
|||||||
menuItems.push({
|
menuItems.push({
|
||||||
key: "deletejob",
|
key: "deletejob",
|
||||||
id: "job-actions-deletejob",
|
id: "job-actions-deletejob",
|
||||||
label:
|
label: t("menus.jobsactions.deletejob"),
|
||||||
jobWatchersCount === 0
|
onClick: () => {
|
||||||
? renderPopconfirmMenuLabel({
|
if (jobWatchersCount === 0) {
|
||||||
key: "confirm-deletejob",
|
openConfirmFromMenu({
|
||||||
text: t("menus.jobsactions.deletejob"),
|
title: t("jobs.labels.deleteconfirm"),
|
||||||
title: t("jobs.labels.deleteconfirm"),
|
okText: t("general.labels.yes"),
|
||||||
okText: t("general.labels.yes"),
|
cancelText: t("general.labels.no"),
|
||||||
cancelText: t("general.labels.no"),
|
onOk: handleDeleteJob
|
||||||
onConfirm: handleDeleteJob
|
});
|
||||||
})
|
} else {
|
||||||
: renderPopconfirmMenuLabel({
|
// informational "OK only"
|
||||||
key: "confirm-deletejob-watchers",
|
openConfirmFromMenu({
|
||||||
text: t("menus.jobsactions.deletejob"),
|
variant: "info",
|
||||||
title: t("jobs.labels.deletewatchers"),
|
title: t("jobs.labels.deletewatchers"),
|
||||||
showCancel: false,
|
okText: t("general.actions.ok"),
|
||||||
closeDropdownOnConfirm: false, // <-- FIX: keep dropdown mounted so Popconfirm can close cleanly
|
showCancel: false
|
||||||
onConfirm: () => {
|
});
|
||||||
// informational confirm only
|
}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1188,22 +1178,18 @@ export function JobsDetailHeaderActions({
|
|||||||
label: t("appointments.labels.manualevent")
|
label: t("appointments.labels.manualevent")
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!jobRO && job.converted) {
|
if (!jobRO && job.converted && canVoidJob) {
|
||||||
menuItems.push({
|
menuItems.push({
|
||||||
key: "voidjob",
|
key: "voidjob",
|
||||||
id: "job-actions-voidjob",
|
id: "job-actions-voidjob",
|
||||||
label: (
|
label: t("menus.jobsactions.void"),
|
||||||
<RbacWrapper action="jobs:void" noauth>
|
onClick: () =>
|
||||||
{renderPopconfirmMenuLabel({
|
openConfirmFromMenu({
|
||||||
key: "confirm-voidjob",
|
title: t("jobs.labels.voidjob"),
|
||||||
text: t("menus.jobsactions.void"),
|
okText: t("general.labels.yes"),
|
||||||
title: t("jobs.labels.voidjob"),
|
cancelText: t("general.labels.no"),
|
||||||
okText: t("general.labels.yes"),
|
onOk: handleVoidJob
|
||||||
cancelText: t("general.labels.no"),
|
})
|
||||||
onConfirm: handleVoidJob
|
|
||||||
})}
|
|
||||||
</RbacWrapper>
|
|
||||||
)
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1235,6 +1221,7 @@ export function JobsDetailHeaderActions({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{modalContextHolder}
|
||||||
<Modal
|
<Modal
|
||||||
title={t("menus.jobsactions.cancelallappointments")}
|
title={t("menus.jobsactions.cancelallappointments")}
|
||||||
open={isCancelScheduleModalVisible}
|
open={isCancelScheduleModalVisible}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export default async function DuplicateJob({
|
|||||||
}) {
|
}) {
|
||||||
logImEXEvent("job_duplicate");
|
logImEXEvent("job_duplicate");
|
||||||
|
|
||||||
const { defaultOpenStatus } = config;
|
const { defaultOpenStatus, timezone } = config;
|
||||||
//get a list of all fields on the job
|
//get a list of all fields on the job
|
||||||
const res = await apolloClient.query({
|
const res = await apolloClient.query({
|
||||||
query: QUERY_JOB_FOR_DUPE,
|
query: QUERY_JOB_FOR_DUPE,
|
||||||
@@ -31,9 +31,12 @@ export default async function DuplicateJob({
|
|||||||
delete existingJob.updatedat;
|
delete existingJob.updatedat;
|
||||||
delete existingJob.cieca_stl;
|
delete existingJob.cieca_stl;
|
||||||
delete existingJob.cieca_ttl;
|
delete existingJob.cieca_ttl;
|
||||||
|
!keepJobLines && delete existingJob.clm_total;
|
||||||
|
|
||||||
const newJob = {
|
const newJob = {
|
||||||
...existingJob,
|
...existingJob,
|
||||||
|
date_estimated: dayjs().tz(timezone, false).format("YYYY-MM-DD"),
|
||||||
|
date_open: dayjs(),
|
||||||
status: defaultOpenStatus
|
status: defaultOpenStatus
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -70,7 +73,7 @@ export default async function DuplicateJob({
|
|||||||
export async function CreateIouForJob({ apolloClient, jobId, config, jobLinesToKeep, currentUser }) {
|
export async function CreateIouForJob({ apolloClient, jobId, config, jobLinesToKeep, currentUser }) {
|
||||||
logImEXEvent("job_create_iou");
|
logImEXEvent("job_create_iou");
|
||||||
|
|
||||||
const { status } = config;
|
const { status, timezone } = config;
|
||||||
//get a list of all fields on the job
|
//get a list of all fields on the job
|
||||||
const res = await apolloClient.query({
|
const res = await apolloClient.query({
|
||||||
query: QUERY_JOB_FOR_DUPE,
|
query: QUERY_JOB_FOR_DUPE,
|
||||||
@@ -88,10 +91,10 @@ export async function CreateIouForJob({ apolloClient, jobId, config, jobLinesToK
|
|||||||
|
|
||||||
const newJob = {
|
const newJob = {
|
||||||
...existingJob,
|
...existingJob,
|
||||||
|
|
||||||
converted: true,
|
converted: true,
|
||||||
status: status,
|
status: status,
|
||||||
iouparent: jobId,
|
iouparent: jobId,
|
||||||
|
date_estimated: dayjs().tz(timezone, false).format("YYYY-MM-DD"),
|
||||||
date_open: dayjs(),
|
date_open: dayjs(),
|
||||||
audit_trails: {
|
audit_trails: {
|
||||||
data: [
|
data: [
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ export function JobsDetailHeader({ job, bodyshop, disabled, insertAuditTrail, is
|
|||||||
label={t("jobs.fields.comment")}
|
label={t("jobs.fields.comment")}
|
||||||
styles={{ value: { overflow: "hidden", textOverflow: "ellipsis" } }}
|
styles={{ value: { overflow: "hidden", textOverflow: "ellipsis" } }}
|
||||||
>
|
>
|
||||||
<ProductionListColumnComment record={job} />
|
<ProductionListColumnComment record={job} usePortal={true} />
|
||||||
</DataLabel>
|
</DataLabel>
|
||||||
{!isPartsEntry && <DataLabel label={t("jobs.fields.ins_co_nm_short")}>{job.ins_co_nm}</DataLabel>}
|
{!isPartsEntry && <DataLabel label={t("jobs.fields.ins_co_nm_short")}>{job.ins_co_nm}</DataLabel>}
|
||||||
<DataLabel label={t("jobs.fields.clm_no")}>{job.clm_no}</DataLabel>
|
<DataLabel label={t("jobs.fields.clm_no")}>{job.clm_no}</DataLabel>
|
||||||
@@ -176,7 +176,7 @@ export function JobsDetailHeader({ job, bodyshop, disabled, insertAuditTrail, is
|
|||||||
</DataLabel>
|
</DataLabel>
|
||||||
)}
|
)}
|
||||||
<DataLabel label={t("jobs.fields.production_vars.note")}>
|
<DataLabel label={t("jobs.fields.production_vars.note")}>
|
||||||
<ProductionListColumnProductionNote record={job} />
|
<ProductionListColumnProductionNote record={job} usePortal={true} />
|
||||||
</DataLabel>
|
</DataLabel>
|
||||||
<DataLabel label={t("jobs.fields.estimate_sent_approval")}>
|
<DataLabel label={t("jobs.fields.estimate_sent_approval")}>
|
||||||
<Space>
|
<Space>
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export default function OwnerFindModalContainer({
|
|||||||
value={searchText}
|
value={searchText}
|
||||||
onChange={(e) => setSearchText(e.target.value)}
|
onChange={(e) => setSearchText(e.target.value)}
|
||||||
onSearch={(val) => callSearchowners({ variables: { search: val.trim() } })}
|
onSearch={(val) => callSearchowners({ variables: { search: val.trim() } })}
|
||||||
|
enterButton
|
||||||
/>
|
/>
|
||||||
<OwnerFindModalComponent
|
<OwnerFindModalComponent
|
||||||
selectedOwner={selectedOwner}
|
selectedOwner={selectedOwner}
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ export function PartDispatchTableComponent({ bodyshop, job, billsQuery }) {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setSearchText(e.target.value);
|
setSearchText(e.target.value);
|
||||||
}}
|
}}
|
||||||
|
enterButton
|
||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -295,6 +295,7 @@ export function PartsOrderListTableComponent({
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setSearchText(e.target.value);
|
setSearchText(e.target.value);
|
||||||
}}
|
}}
|
||||||
|
enterButton
|
||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,13 @@ import PartsOrderModalPriceChange from "./parts-order-modal-price-change.compone
|
|||||||
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
|
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
|
||||||
import { selectIsPartsEntry } from "../../redux/application/application.selectors.js";
|
import { selectIsPartsEntry } from "../../redux/application/application.selectors.js";
|
||||||
|
|
||||||
|
const PriceInputWrapper = ({ value, onChange, form, field }) => (
|
||||||
|
<Space.Compact style={{ width: "100%" }}>
|
||||||
|
<PartsOrderModalPriceChange form={form} field={field} />
|
||||||
|
<CurrencyInput style={{ flex: 1 }} value={value} onChange={onChange} />
|
||||||
|
</Space.Compact>
|
||||||
|
);
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
isPartsEntry: selectIsPartsEntry
|
isPartsEntry: selectIsPartsEntry
|
||||||
@@ -199,10 +206,7 @@ export function PartsOrderModalComponent({
|
|||||||
key={`${index}act_price`}
|
key={`${index}act_price`}
|
||||||
name={[field.name, "act_price"]}
|
name={[field.name, "act_price"]}
|
||||||
>
|
>
|
||||||
<Space.Compact style={{ width: "100%" }}>
|
<PriceInputWrapper form={form} field={field} />
|
||||||
<PartsOrderModalPriceChange form={form} field={field} />
|
|
||||||
<CurrencyInput style={{ flex: 1 }} />
|
|
||||||
</Space.Compact>
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{isReturn && (
|
{isReturn && (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
|
|||||||
@@ -89,7 +89,8 @@ export function PartsOrderModalContainer({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...p,
|
...p,
|
||||||
job_line_id: jobLineId
|
job_line_id: jobLineId,
|
||||||
|
...(isReturn && { cm_received: false })
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -269,6 +269,7 @@ export function PartsQueueListComponent({ bodyshop }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
|
title={t("titles.bc.parts-queue")}
|
||||||
extra={
|
extra={
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<Button onClick={() => refetch()}>
|
<Button onClick={() => refetch()}>
|
||||||
|
|||||||
@@ -129,6 +129,7 @@ function PhoneNumberConsentList({ bodyshop }) {
|
|||||||
placeholder={t("general.labels.search")}
|
placeholder={t("general.labels.search")}
|
||||||
onSearch={(value) => setSearch(value)}
|
onSearch={(value) => setSearch(value)}
|
||||||
style={{ marginBottom: 16 }}
|
style={{ marginBottom: 16 }}
|
||||||
|
enterButton
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Table
|
<Table
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export function ProductionBoardFilters({ bodyshop, filter, setFilter, loading })
|
|||||||
setFilter({ ...filter, search: e.target.value });
|
setFilter({ ...filter, search: e.target.value });
|
||||||
logImEXEvent("visual_board_filter_search", { search: e.target.value });
|
logImEXEvent("visual_board_filter_search", { search: e.target.value });
|
||||||
}}
|
}}
|
||||||
|
enterButton
|
||||||
/>
|
/>
|
||||||
<EmployeeSearchSelectComponent
|
<EmployeeSearchSelectComponent
|
||||||
style={{ minWidth: "20rem" }}
|
style={{ minWidth: "20rem" }}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { DragDropContext } from "../dnd/lib";
|
import { DragDropContext } from "../dnd/lib";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
@@ -7,6 +7,7 @@ import { PopoverWrapper } from "react-popopo";
|
|||||||
import * as actions from "../../../../redux/trello/trello.actions.js";
|
import * as actions from "../../../../redux/trello/trello.actions.js";
|
||||||
import { BoardWrapper } from "../styles/Base.js";
|
import { BoardWrapper } from "../styles/Base.js";
|
||||||
import ProductionStatistics from "../../production-board-kanban.statistics.jsx";
|
import ProductionStatistics from "../../production-board-kanban.statistics.jsx";
|
||||||
|
import isEqual from "lodash/isEqual";
|
||||||
|
|
||||||
const useDragMap = () => {
|
const useDragMap = () => {
|
||||||
const dragMapRef = useRef(new Map());
|
const dragMapRef = useRef(new Map());
|
||||||
@@ -47,8 +48,9 @@ const BoardContainer = ({
|
|||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const currentReducerData = useSelector((state) => (state.trello.lanes ? state.trello : {}));
|
const currentReducerData = useSelector((state) => (state.trello.lanes ? state.trello : {}));
|
||||||
const { setDragTime, getLastDragTime } = useDragMap();
|
const { setDragTime, getLastDragTime } = useDragMap();
|
||||||
|
const previousDataRef = useRef(null);
|
||||||
|
|
||||||
const wireEventBus = () => {
|
const wireEventBus = useCallback(() => {
|
||||||
const eventBus = {
|
const eventBus = {
|
||||||
publish: (event) => {
|
publish: (event) => {
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
@@ -68,14 +70,17 @@ const BoardContainer = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
eventBusHandle(eventBus);
|
eventBusHandle(eventBus);
|
||||||
};
|
}, [dispatch, eventBusHandle]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(actions.loadBoard(data));
|
if (!isEqual(previousDataRef.current, data)) {
|
||||||
if (eventBusHandle) {
|
previousDataRef.current = data;
|
||||||
wireEventBus();
|
dispatch(actions.loadBoard(data));
|
||||||
|
if (eventBusHandle) {
|
||||||
|
wireEventBus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [data, eventBusHandle, dispatch]);
|
}, [data, wireEventBus, dispatch]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onDataChange(currentReducerData);
|
onDataChange(currentReducerData);
|
||||||
@@ -153,12 +158,17 @@ const BoardContainer = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const boardKey = useMemo(() => {
|
||||||
|
// React Compiler: Generate stable key from lane IDs
|
||||||
|
return currentReducerData.lanes?.map((l) => l.id).join("-") || "empty";
|
||||||
|
}, [currentReducerData.lanes]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ProductionStatistics data={queryData} reducerData={currentReducerData} cardSettings={cardSettings} />
|
<ProductionStatistics data={queryData} reducerData={currentReducerData} cardSettings={cardSettings} />
|
||||||
<PopoverWrapper>
|
<PopoverWrapper>
|
||||||
<BoardWrapper orientation={orientation}>
|
<BoardWrapper orientation={orientation}>
|
||||||
<DragDropContext onDragEnd={onLaneDrag} onDragStart={onDragStart} contextId="production-board">
|
<DragDropContext key={boardKey} onDragEnd={onLaneDrag} onDragStart={onDragStart} contextId="production-board">
|
||||||
{currentReducerData.lanes.map((lane, index) => (
|
{currentReducerData.lanes.map((lane, index) => (
|
||||||
<Lane
|
<Lane
|
||||||
key={lane.id}
|
key={lane.id}
|
||||||
|
|||||||
@@ -1,14 +1,28 @@
|
|||||||
import { useEffect, useMemo } from "react";
|
import { useEffect, useMemo, useRef } from "react";
|
||||||
import createRegistry from "./create-registry";
|
import createRegistry from "./create-registry";
|
||||||
|
|
||||||
export default function useRegistry() {
|
export default function useRegistry() {
|
||||||
const registry = useMemo(createRegistry, []);
|
const registry = useMemo(createRegistry, []);
|
||||||
|
const cleanupScheduledRef = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// Cancel any scheduled cleanup when component mounts
|
||||||
|
// This handles React StrictMode double-mounting
|
||||||
|
cleanupScheduledRef.current = false;
|
||||||
|
|
||||||
return function unmount() {
|
return function unmount() {
|
||||||
|
// Mark cleanup as scheduled
|
||||||
|
cleanupScheduledRef.current = true;
|
||||||
|
|
||||||
// clean up the registry to avoid any leaks
|
// clean up the registry to avoid any leaks
|
||||||
// doing it after an animation frame so that other things unmounting
|
// doing it after an animation frame so that other things unmounting
|
||||||
// can continue to interact with the registry
|
// can continue to interact with the registry
|
||||||
requestAnimationFrame(registry.clean);
|
requestAnimationFrame(() => {
|
||||||
|
// Only clean if still scheduled (not cancelled by remount)
|
||||||
|
if (cleanupScheduledRef.current) {
|
||||||
|
registry.clean();
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}, [registry]);
|
}, [registry]);
|
||||||
return registry;
|
return registry;
|
||||||
|
|||||||
@@ -171,6 +171,7 @@ export default function useDroppablePublisher(args) {
|
|||||||
}
|
}
|
||||||
registry.droppable.unregister(entry);
|
registry.droppable.unregister(entry);
|
||||||
};
|
};
|
||||||
|
// eslint-disable-next-line react-compiler/react-compiler
|
||||||
}, [callbacks, descriptor, dragStopped, entry, marshal, registry.droppable]);
|
}, [callbacks, descriptor, dragStopped, entry, marshal, registry.droppable]);
|
||||||
|
|
||||||
// update is enabled with the marshal
|
// update is enabled with the marshal
|
||||||
|
|||||||
@@ -63,10 +63,10 @@ const ProductionListColumnAlert = ({ id, productionVars, refetch, insertAuditTra
|
|||||||
okText={t("general.labels.yes")}
|
okText={t("general.labels.yes")}
|
||||||
cancelText={t("general.labels.no")}
|
cancelText={t("general.labels.no")}
|
||||||
>
|
>
|
||||||
<Button className="production-alert" icon={<ExclamationCircleFilled />} />
|
<Button className="production-alert" icon={<ExclamationCircleFilled />} style={{ cursor: "pointer" }} />
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
) : (
|
) : (
|
||||||
<Button className="muted-button" icon={<PlusCircleFilled />} onClick={handleAlertToggle} />
|
<Button className="muted-button" icon={<PlusCircleFilled />} onClick={handleAlertToggle} style={{ cursor: "pointer" }} />
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export default function ProductionListColumnBodyPriority({ record }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown menu={menu} trigger={["click"]}>
|
<Dropdown menu={menu} trigger={["click"]}>
|
||||||
<div style={{ width: "100%", height: "19px" }}>{record.production_vars?.bodypriority}</div>
|
<div style={{ width: "100%", height: "19px", cursor: "pointer" }}>{record.production_vars?.bodypriority}</div>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { FaRegStickyNote } from "react-icons/fa";
|
|||||||
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
|
|
||||||
export default function ProductionListColumnComment({ record }) {
|
export default function ProductionListColumnComment({ record, usePortal = false }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [note, setNote] = useState(record.comment || "");
|
const [note, setNote] = useState(record.comment || "");
|
||||||
@@ -43,16 +43,20 @@ export default function ProductionListColumnComment({ record }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const content = (
|
const content = (
|
||||||
<div style={{ width: "30em" }} onMouseDown={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}>
|
<div
|
||||||
|
style={{ width: "30em" }}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
|
id={`job-comment-${record.id}`}
|
||||||
|
name="comment"
|
||||||
rows={5}
|
rows={5}
|
||||||
value={note}
|
value={note}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
autoFocus
|
autoFocus
|
||||||
allowClear
|
allowClear
|
||||||
style={{ marginBottom: "1em" }}
|
style={{ marginBottom: "1em" }}
|
||||||
onMouseDown={(e) => e.stopPropagation()}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<Button onClick={handleSaveNote} type="primary">
|
<Button onClick={handleSaveNote} type="primary">
|
||||||
@@ -63,7 +67,15 @@ export default function ProductionListColumnComment({ record }) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover onOpenChange={handleOpenChange} open={open} content={content} trigger="click" fresh>
|
<Popover
|
||||||
|
onOpenChange={handleOpenChange}
|
||||||
|
open={open}
|
||||||
|
content={content}
|
||||||
|
trigger="click"
|
||||||
|
destroyOnHidden
|
||||||
|
styles={{ body: { padding: '12px' } }}
|
||||||
|
{...(usePortal ? { getPopupContainer: (trigger) => trigger.parentElement || document.body } : {})}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ const productionListColumnsData = ({ technician, state, activeStatuses, data, bo
|
|||||||
ellipsis: true,
|
ellipsis: true,
|
||||||
render: (text, record) => (
|
render: (text, record) => (
|
||||||
<div
|
<div
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
store.dispatch(
|
store.dispatch(
|
||||||
setModalContext({
|
setModalContext({
|
||||||
|
|||||||
@@ -86,7 +86,8 @@ export default function ProductionListDate({ record, field, time, pastIndicator
|
|||||||
<div
|
<div
|
||||||
onClick={() => setOpen(true)}
|
onClick={() => setOpen(true)}
|
||||||
style={{
|
style={{
|
||||||
height: "19px"
|
height: "19px",
|
||||||
|
cursor: "pointer"
|
||||||
}}
|
}}
|
||||||
className={className}
|
className={className}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export default function ProductionListColumnDetailPriority({ record }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown menu={menu} trigger={["click"]}>
|
<Dropdown menu={menu} trigger={["click"]}>
|
||||||
<div style={{ width: "100%", height: "19px" }}>{record.production_vars?.detailpriority}</div>
|
<div style={{ width: "100%", height: "19px", cursor: "pointer" }}>{record.production_vars?.detailpriority}</div>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,13 +144,13 @@ export function ProductionListEmpAssignment({ insertAuditTrail, bodyshop, record
|
|||||||
<Popover destroyOnHidden content={popContent} open={visibility}>
|
<Popover destroyOnHidden content={popContent} open={visibility}>
|
||||||
<Spin spinning={loading}>
|
<Spin spinning={loading}>
|
||||||
{record[type] ? (
|
{record[type] ? (
|
||||||
<div>
|
<div style={{ cursor: "pointer" }}>
|
||||||
<span>{`${theEmployee?.first_name || ""} ${theEmployee?.last_name || ""}`}</span>
|
<span>{`${theEmployee?.first_name || ""} ${theEmployee?.last_name || ""}`}</span>
|
||||||
<DeleteFilled style={iconStyle} onClick={() => handleRemove(type)} />
|
<DeleteFilled style={iconStyle} onClick={() => handleRemove(type)} />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<PlusCircleFilled
|
<PlusCircleFilled
|
||||||
style={iconStyle}
|
style={{ ...iconStyle, cursor: "pointer" }}
|
||||||
className="muted-button"
|
className="muted-button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setAssignment({ operation: type });
|
setAssignment({ operation: type });
|
||||||
|
|||||||
@@ -124,7 +124,8 @@ export function ProductionLastContacted({ currentUser, record }) {
|
|||||||
<div
|
<div
|
||||||
onClick={() => setOpen(true)}
|
onClick={() => setOpen(true)}
|
||||||
style={{
|
style={{
|
||||||
height: "19px"
|
height: "19px",
|
||||||
|
cursor: "pointer"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DateFormatter bordered={false}>{record.date_last_contacted}</DateFormatter>
|
<DateFormatter bordered={false}>{record.date_last_contacted}</DateFormatter>
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export default function ProductionListColumnPaintPriority({ record }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown menu={menu} trigger={["click"]}>
|
<Dropdown menu={menu} trigger={["click"]}>
|
||||||
<div style={{ width: "100%", height: "19px" }}>{record.production_vars?.paintpriority}</div>
|
<div style={{ width: "100%", height: "19px", cursor: "pointer" }}>{record.production_vars?.paintpriority}</div>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
setNoteUpsertContext: (context) => dispatch(setModalContext({ context: context, modal: "noteUpsert" }))
|
setNoteUpsertContext: (context) => dispatch(setModalContext({ context: context, modal: "noteUpsert" }))
|
||||||
});
|
});
|
||||||
|
|
||||||
function ProductionListColumnProductionNote({ record, setNoteUpsertContext }) {
|
function ProductionListColumnProductionNote({ record, setNoteUpsertContext, usePortal = false }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [note, setNote] = useState(record.production_vars?.note || "");
|
const [note, setNote] = useState(record.production_vars?.note || "");
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
@@ -59,16 +59,20 @@ function ProductionListColumnProductionNote({ record, setNoteUpsertContext }) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const content = (
|
const content = (
|
||||||
<div style={{ width: "30em" }} onMouseDown={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}>
|
<div
|
||||||
|
style={{ width: "30em" }}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
|
id={`job-production-note-${record.id}`}
|
||||||
|
name="production_note"
|
||||||
rows={5}
|
rows={5}
|
||||||
value={note}
|
value={note}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
autoFocus
|
autoFocus
|
||||||
allowClear
|
allowClear
|
||||||
style={{ marginBottom: "1em" }}
|
style={{ marginBottom: "1em" }}
|
||||||
onMouseDown={(e) => e.stopPropagation()}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
/>
|
/>
|
||||||
<Space>
|
<Space>
|
||||||
<Button onClick={handleSaveNote} type="primary">
|
<Button onClick={handleSaveNote} type="primary">
|
||||||
@@ -92,7 +96,15 @@ function ProductionListColumnProductionNote({ record, setNoteUpsertContext }) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover onOpenChange={handleOpenChange} open={open} content={content} trigger="click" fresh>
|
<Popover
|
||||||
|
onOpenChange={handleOpenChange}
|
||||||
|
open={open}
|
||||||
|
content={content}
|
||||||
|
trigger="click"
|
||||||
|
destroyOnHidden
|
||||||
|
styles={{ body: { padding: '12px' } }}
|
||||||
|
{...(usePortal ? { getPopupContainer: (trigger) => trigger.parentElement || document.body } : {})}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export default function ProfileShopsComponent({ loading, data, updateActiveShop
|
|||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
allowClear
|
allowClear
|
||||||
placeholder={t("general.labels.search")}
|
placeholder={t("general.labels.search")}
|
||||||
|
enterButton
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ export function ReportCenterModalComponent({ reportCenterModal, bodyshop }) {
|
|||||||
return (
|
return (
|
||||||
<div className="report-center-modal">
|
<div className="report-center-modal">
|
||||||
<Form onFinish={handleFinish} autoComplete={"off"} layout="vertical" form={form}>
|
<Form onFinish={handleFinish} autoComplete={"off"} layout="vertical" form={form}>
|
||||||
<Input.Search onChange={(e) => setSearch(e.target.value)} value={search} />
|
<Input.Search onChange={(e) => setSearch(e.target.value)} value={search} enterButton />
|
||||||
<Form.Item name="defaultSorters" hidden>
|
<Form.Item name="defaultSorters" hidden>
|
||||||
<Input type="hidden" />
|
<Input type="hidden" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ import { Button, Form, Input, InputNumber, Select, Space, Switch } from "antd";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import FeatureWrapper from "../feature-wrapper/feature-wrapper.component";
|
||||||
import FeatureWrapper, { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
|
||||||
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||||
import FormItemEmail from "../form-items-formatted/email-form-item.component";
|
import FormItemEmail from "../form-items-formatted/email-form-item.component";
|
||||||
import PhoneFormItem, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component";
|
import PhoneFormItem, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component";
|
||||||
@@ -12,15 +11,13 @@ import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.c
|
|||||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
|
|
||||||
const timeZonesList = Intl.supportedValuesOf("timeZone");
|
const timeZonesList = Intl.supportedValuesOf("timeZone");
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({});
|
||||||
bodyshop: selectBodyshop
|
|
||||||
});
|
|
||||||
const mapDispatchToProps = () => ({
|
const mapDispatchToProps = () => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
});
|
});
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoGeneral);
|
export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoGeneral);
|
||||||
|
|
||||||
export function ShopInfoGeneral({ form, bodyshop }) {
|
export function ShopInfoGeneral({ form }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -378,34 +375,6 @@ export function ShopInfoGeneral({ form, bodyshop }) {
|
|||||||
>
|
>
|
||||||
<Select mode="tags" />
|
<Select mode="tags" />
|
||||||
</Form.Item>,
|
</Form.Item>,
|
||||||
...(HasFeatureAccess({ featureName: "timetickets", bodyshop })
|
|
||||||
? [
|
|
||||||
<Form.Item
|
|
||||||
key="tt_allow_post_to_invoiced"
|
|
||||||
name={["tt_allow_post_to_invoiced"]}
|
|
||||||
label={t("bodyshop.fields.tt_allow_post_to_invoiced")}
|
|
||||||
valuePropName="checked"
|
|
||||||
>
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>,
|
|
||||||
<Form.Item
|
|
||||||
key="tt_enforce_hours_for_tech_console"
|
|
||||||
name={["tt_enforce_hours_for_tech_console"]}
|
|
||||||
label={t("bodyshop.fields.tt_enforce_hours_for_tech_console")}
|
|
||||||
valuePropName="checked"
|
|
||||||
>
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>,
|
|
||||||
<Form.Item
|
|
||||||
key="bill_allow_post_to_closed"
|
|
||||||
name={["bill_allow_post_to_closed"]}
|
|
||||||
label={t("bodyshop.fields.bill_allow_post_to_closed")}
|
|
||||||
valuePropName="checked"
|
|
||||||
>
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
key="md_ded_notes"
|
key="md_ded_notes"
|
||||||
name={["md_ded_notes"]}
|
name={["md_ded_notes"]}
|
||||||
|
|||||||
@@ -313,6 +313,38 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
|
|||||||
>
|
>
|
||||||
<Select mode="tags" />
|
<Select mode="tags" />
|
||||||
</Form.Item>,
|
</Form.Item>,
|
||||||
|
...(HasFeatureAccess({ featureName: "timetickets", bodyshop })
|
||||||
|
? [
|
||||||
|
<Form.Item
|
||||||
|
key="tt_allow_post_to_invoiced"
|
||||||
|
name={["tt_allow_post_to_invoiced"]}
|
||||||
|
label={t("bodyshop.fields.tt_allow_post_to_invoiced")}
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>,
|
||||||
|
<Form.Item
|
||||||
|
key="tt_enforce_hours_for_tech_console"
|
||||||
|
name={["tt_enforce_hours_for_tech_console"]}
|
||||||
|
label={t("bodyshop.fields.tt_enforce_hours_for_tech_console")}
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
...(HasFeatureAccess({ featureName: "bills", bodyshop })
|
||||||
|
? [
|
||||||
|
<Form.Item
|
||||||
|
key="bill_allow_post_to_closed"
|
||||||
|
name={["bill_allow_post_to_closed"]}
|
||||||
|
label={t("bodyshop.fields.bill_allow_post_to_closed")}
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
]
|
||||||
|
: []),
|
||||||
...(HasFeatureAccess({ featureName: "export", bodyshop })
|
...(HasFeatureAccess({ featureName: "export", bodyshop })
|
||||||
? [
|
? [
|
||||||
...(ClosingPeriod.treatment === "on"
|
...(ClosingPeriod.treatment === "on"
|
||||||
|
|||||||
@@ -356,7 +356,10 @@ export const MUTATION_BACKORDER_PART_LINE = gql`
|
|||||||
export const QUERY_UNRECEIVED_LINES = gql`
|
export const QUERY_UNRECEIVED_LINES = gql`
|
||||||
query QUERY_UNRECEIVED_LINES($jobId: uuid!, $vendorId: uuid!) {
|
query QUERY_UNRECEIVED_LINES($jobId: uuid!, $vendorId: uuid!) {
|
||||||
parts_order_lines(
|
parts_order_lines(
|
||||||
where: { parts_order: { jobid: { _eq: $jobId }, vendorid: { _eq: $vendorId } }, cm_received: { _neq: true } }
|
where: {
|
||||||
|
parts_order: { jobid: { _eq: $jobId }, vendorid: { _eq: $vendorId }, return: { _eq: true } }
|
||||||
|
_or: [{ cm_received: { _neq: true } }, { cm_received: { _is_null: true } }]
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
cm_received
|
cm_received
|
||||||
id
|
id
|
||||||
|
|||||||
@@ -82,13 +82,14 @@ const rootEl = document.getElementById("root");
|
|||||||
|
|
||||||
if (!rootEl) throw new Error('Missing root element: <div id="root" />');
|
if (!rootEl) throw new Error('Missing root element: <div id="root" />');
|
||||||
|
|
||||||
const appTree = import.meta.env.DEV ? (
|
const appTree =
|
||||||
<StrictMode>
|
import.meta.env.DEV && import.meta.env?.VITE_DISABLE_STRICT_MODE !== "true" ? (
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>
|
||||||
|
) : (
|
||||||
<App />
|
<App />
|
||||||
</StrictMode>
|
);
|
||||||
) : (
|
|
||||||
<App />
|
|
||||||
);
|
|
||||||
|
|
||||||
ReactDOM.createRoot(rootEl).render(appTree);
|
ReactDOM.createRoot(rootEl).render(appTree);
|
||||||
|
|
||||||
|
|||||||
@@ -41,19 +41,25 @@ export function ContractDetailPageContainer({ setBreadcrumbs, addRecentItem, set
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectedHeader("contracts");
|
setSelectedHeader("contracts");
|
||||||
document.title = loading
|
|
||||||
? InstanceRenderManager({
|
const appName = InstanceRenderManager({
|
||||||
imex: t("titles.imexonline"),
|
imex: "$t(titles.imexonline)",
|
||||||
rome: t("titles.romeonline")
|
rome: "$t(titles.romeonline)"
|
||||||
})
|
});
|
||||||
: error
|
|
||||||
? InstanceRenderManager({
|
const fallbackTitle = InstanceRenderManager({
|
||||||
imex: t("titles.imexonline"),
|
imex: t("titles.imexonline"),
|
||||||
rome: t("titles.romeonline")
|
rome: t("titles.romeonline")
|
||||||
})
|
});
|
||||||
: t("titles.contracts-detail", {
|
|
||||||
id: (data?.cccontracts_by_pk && data.cccontracts_by_pk.agreementnumber) || ""
|
if (loading || error) {
|
||||||
});
|
document.title = fallbackTitle;
|
||||||
|
} else {
|
||||||
|
document.title = t("titles.contracts-detail", {
|
||||||
|
id: (data?.cccontracts_by_pk && data.cccontracts_by_pk.agreementnumber) || "",
|
||||||
|
app: appName
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
setBreadcrumbs([
|
setBreadcrumbs([
|
||||||
{ link: "/manage/courtesycars", label: t("titles.bc.courtesycars") },
|
{ link: "/manage/courtesycars", label: t("titles.bc.courtesycars") },
|
||||||
|
|||||||
@@ -181,6 +181,7 @@ export function ExportLogsPageComponent() {
|
|||||||
searchParams.search = value;
|
searchParams.search = value;
|
||||||
history({ search: queryString.stringify(searchParams) });
|
history({ search: queryString.stringify(searchParams) });
|
||||||
}}
|
}}
|
||||||
|
enterButton
|
||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { PageHeader } from "@ant-design/pro-layout";
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import AlertComponent from "../../components/alert/alert.component";
|
import AlertComponent from "../../components/alert/alert.component";
|
||||||
import JobsAvailableTableContainer from "../../components/jobs-available-table/jobs-available-table.container";
|
import JobsAvailableTableContainer from "../../components/jobs-available-table/jobs-available-table.container";
|
||||||
@@ -25,6 +24,26 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
export function JobsAvailablePageContainer({ partnerVersion, setBreadcrumbs, setSelectedHeader }) {
|
export function JobsAvailablePageContainer({ partnerVersion, setBreadcrumbs, setSelectedHeader }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const getOS = () => {
|
||||||
|
const userAgent = navigator.userAgent;
|
||||||
|
if (userAgent.indexOf("Win") !== -1) return "windows";
|
||||||
|
if (userAgent.indexOf("Mac") !== -1) return "mac";
|
||||||
|
if (userAgent.indexOf("Linux") !== -1) return "linux";
|
||||||
|
return "unknown";
|
||||||
|
};
|
||||||
|
|
||||||
|
const os = getOS();
|
||||||
|
const downloadUrl = InstanceRenderManager({
|
||||||
|
imex:
|
||||||
|
os === "windows"
|
||||||
|
? "https://imex-partner.s3.ca-central-1.amazonaws.com/imex-partner-x64.exe"
|
||||||
|
: "https://imex-partner.s3.ca-central-1.amazonaws.com/imex-partner-arm64.dmg",
|
||||||
|
rome:
|
||||||
|
os === "windows"
|
||||||
|
? "https://rome-partner.s3.us-east-2.amazonaws.com/rome-partner-x64.exe"
|
||||||
|
: "https://rome-partner.s3.us-east-2.amazonaws.com/rome-partner-arm64.dmg"
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = t("titles.jobsavailable", {
|
document.title = t("titles.jobsavailable", {
|
||||||
app: InstanceRenderManager({
|
app: InstanceRenderManager({
|
||||||
@@ -39,24 +58,12 @@ export function JobsAvailablePageContainer({ partnerVersion, setBreadcrumbs, set
|
|||||||
return (
|
return (
|
||||||
<RbacWrapper action="jobs:available-list">
|
<RbacWrapper action="jobs:available-list">
|
||||||
<div>
|
<div>
|
||||||
<PageHeader
|
<PageHeader />
|
||||||
title={t("titles.bc.availablejobs")}
|
|
||||||
extra={
|
|
||||||
<Link to="/manage/jobs/new">
|
|
||||||
<Button>{t("jobs.actions.manualnew")}</Button>
|
|
||||||
</Link>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{!partnerVersion && (
|
{!partnerVersion && (
|
||||||
<AlertComponent
|
<AlertComponent
|
||||||
type="warning"
|
type="warning"
|
||||||
action={
|
action={
|
||||||
<a
|
<a href={downloadUrl}>
|
||||||
href={InstanceRenderManager({
|
|
||||||
imex: "https://partner.imex.online/Setup.exe",
|
|
||||||
rome: "https://partner.romeonline.io/Setup.exe"
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Button size="small">{t("general.actions.download")}</Button>
|
<Button size="small">{t("general.actions.download")}</Button>
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,10 +82,23 @@ export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail, set
|
|||||||
|
|
||||||
const handleFinish = async ({ removefromproduction, ...values }) => {
|
const handleFinish = async ({ removefromproduction, ...values }) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
|
// Validate that all joblines have valid IDs
|
||||||
|
const joblinesWithIds = values.joblines.filter(jl => jl && jl.id);
|
||||||
|
if (joblinesWithIds.length !== values.joblines.length) {
|
||||||
|
notification.error({
|
||||||
|
title: t("jobs.errors.invalidjoblines"),
|
||||||
|
message: t("jobs.errors.missingjoblineids")
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const result = await client.mutate({
|
const result = await client.mutate({
|
||||||
mutation: generateJobLinesUpdatesForInvoicing(values.joblines)
|
mutation: generateJobLinesUpdatesForInvoicing(values.joblines)
|
||||||
});
|
});
|
||||||
if (result.errors) {
|
if (result.errors) {
|
||||||
|
setLoading(false);
|
||||||
return; // Abandon the rest of the close.
|
return; // Abandon the rest of the close.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { FloatButton, Layout, Spin } from "antd";
|
|||||||
import { Route, Routes } from "react-router-dom";
|
import { Route, Routes } from "react-router-dom";
|
||||||
|
|
||||||
// import preval from "preval.macro";
|
// import preval from "preval.macro";
|
||||||
import { lazy, Suspense, useEffect, useRef, useState } from "react";
|
import { Suspense, useEffect, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
@@ -29,87 +29,88 @@ import {
|
|||||||
import InstanceRenderManager from "../../utils/instanceRenderMgr.js";
|
import InstanceRenderManager from "../../utils/instanceRenderMgr.js";
|
||||||
import useAlertsNotifications from "../../hooks/useAlertsNotifications.jsx";
|
import useAlertsNotifications from "../../hooks/useAlertsNotifications.jsx";
|
||||||
import { selectDarkMode } from "../../redux/application/application.selectors.js";
|
import { selectDarkMode } from "../../redux/application/application.selectors.js";
|
||||||
|
import { lazyDev } from "../../utils/lazyWithPreload.jsx";
|
||||||
|
|
||||||
const PrintCenterModalContainer = lazy(
|
const PrintCenterModalContainer = lazyDev(
|
||||||
() => import("../../components/print-center-modal/print-center-modal.container")
|
() => import("../../components/print-center-modal/print-center-modal.container")
|
||||||
);
|
);
|
||||||
|
|
||||||
const NoteUpsertModal = lazy(() => import("../../components/note-upsert-modal/note-upsert-modal.container.jsx"));
|
const NoteUpsertModal = lazyDev(() => import("../../components/note-upsert-modal/note-upsert-modal.container.jsx"));
|
||||||
|
|
||||||
const JobsPage = lazy(() => import("../jobs/jobs.page"));
|
const JobsPage = lazyDev(() => import("../jobs/jobs.page"));
|
||||||
|
|
||||||
const CardPaymentModalContainer = lazy(
|
const CardPaymentModalContainer = lazyDev(
|
||||||
() => import("../../components/card-payment-modal/card-payment-modal.container.jsx")
|
() => import("../../components/card-payment-modal/card-payment-modal.container.jsx")
|
||||||
);
|
);
|
||||||
|
|
||||||
const JobsDetailPage = lazy(() => import("../jobs-detail/jobs-detail.page.container"));
|
const JobsDetailPage = lazyDev(() => import("../jobs-detail/jobs-detail.page.container"));
|
||||||
const InventoryListPage = lazy(() => import("../inventory/inventory.page"));
|
const InventoryListPage = lazyDev(() => import("../inventory/inventory.page"));
|
||||||
const ProfilePage = lazy(() => import("../profile/profile.container.page"));
|
const ProfilePage = lazyDev(() => import("../profile/profile.container.page"));
|
||||||
const JobsAvailablePage = lazy(() => import("../jobs-available/jobs-available.page.container"));
|
const JobsAvailablePage = lazyDev(() => import("../jobs-available/jobs-available.page.container"));
|
||||||
const ScheduleContainer = lazy(() => import("../schedule/schedule.page.container"));
|
const ScheduleContainer = lazyDev(() => import("../schedule/schedule.page.container"));
|
||||||
const VehiclesContainer = lazy(() => import("../vehicles/vehicles.page.container"));
|
const VehiclesContainer = lazyDev(() => import("../vehicles/vehicles.page.container"));
|
||||||
const VehiclesDetailContainer = lazy(() => import("../vehicles-detail/vehicles-detail.page.container"));
|
const VehiclesDetailContainer = lazyDev(() => import("../vehicles-detail/vehicles-detail.page.container"));
|
||||||
const OwnersContainer = lazy(() => import("../owners/owners.page.container"));
|
const OwnersContainer = lazyDev(() => import("../owners/owners.page.container"));
|
||||||
const OwnersDetailContainer = lazy(() => import("../owners-detail/owners-detail.page.container"));
|
const OwnersDetailContainer = lazyDev(() => import("../owners-detail/owners-detail.page.container"));
|
||||||
const ShopPage = lazy(() => import("../shop/shop.page.component"));
|
const ShopPage = lazyDev(() => import("../shop/shop.page.component"));
|
||||||
const ShopVendorPageContainer = lazy(() => import("../shop-vendor/shop-vendor.page.container"));
|
const ShopVendorPageContainer = lazyDev(() => import("../shop-vendor/shop-vendor.page.container"));
|
||||||
const EmailOverlayContainer = lazy(() => import("../../components/email-overlay/email-overlay.container.jsx"));
|
const EmailOverlayContainer = lazyDev(() => import("../../components/email-overlay/email-overlay.container.jsx"));
|
||||||
const JobsCreateContainerPage = lazy(() => import("../jobs-create/jobs-create.container"));
|
const JobsCreateContainerPage = lazyDev(() => import("../jobs-create/jobs-create.container"));
|
||||||
const CourtesyCarCreateContainer = lazy(() => import("../courtesy-car-create/courtesy-car-create.page.container"));
|
const CourtesyCarCreateContainer = lazyDev(() => import("../courtesy-car-create/courtesy-car-create.page.container"));
|
||||||
const CourtesyCarDetailContainer = lazy(() => import("../courtesy-car-detail/courtesy-car-detail.page.container"));
|
const CourtesyCarDetailContainer = lazyDev(() => import("../courtesy-car-detail/courtesy-car-detail.page.container"));
|
||||||
const CourtesyCarsPage = lazy(() => import("../courtesy-cars/courtesy-cars.page.container"));
|
const CourtesyCarsPage = lazyDev(() => import("../courtesy-cars/courtesy-cars.page.container"));
|
||||||
const ContractCreatePage = lazy(() => import("../contract-create/contract-create.page.container"));
|
const ContractCreatePage = lazyDev(() => import("../contract-create/contract-create.page.container"));
|
||||||
const ContractDetailPage = lazy(() => import("../contract-detail/contract-detail.page.container"));
|
const ContractDetailPage = lazyDev(() => import("../contract-detail/contract-detail.page.container"));
|
||||||
const ContractsList = lazy(() => import("../contracts/contracts.page.container"));
|
const ContractsList = lazyDev(() => import("../contracts/contracts.page.container"));
|
||||||
const BillsListPage = lazy(() => import("../bills/bills.page.container"));
|
const BillsListPage = lazyDev(() => import("../bills/bills.page.container"));
|
||||||
const FeatureRequestPage = lazy(() => import("../feature-request/feature-request.page.jsx"));
|
const FeatureRequestPage = lazyDev(() => import("../feature-request/feature-request.page.jsx"));
|
||||||
|
|
||||||
const JobCostingModal = lazy(() => import("../../components/job-costing-modal/job-costing-modal.container"));
|
const JobCostingModal = lazyDev(() => import("../../components/job-costing-modal/job-costing-modal.container"));
|
||||||
const ReportCenterModal = lazy(() => import("../../components/report-center-modal/report-center-modal.container"));
|
const ReportCenterModal = lazyDev(() => import("../../components/report-center-modal/report-center-modal.container"));
|
||||||
const BillEnterModalContainer = lazy(() => import("../../components/bill-enter-modal/bill-enter-modal.container"));
|
const BillEnterModalContainer = lazyDev(() => import("../../components/bill-enter-modal/bill-enter-modal.container"));
|
||||||
const TimeTicketModalContainer = lazy(() => import("../../components/time-ticket-modal/time-ticket-modal.container"));
|
const TimeTicketModalContainer = lazyDev(() => import("../../components/time-ticket-modal/time-ticket-modal.container"));
|
||||||
const TimeTicketModalTask = lazy(
|
const TimeTicketModalTask = lazyDev(
|
||||||
() => import("../../components/time-ticket-task-modal/time-ticket-task-modal.container")
|
() => import("../../components/time-ticket-task-modal/time-ticket-task-modal.container")
|
||||||
);
|
);
|
||||||
const PaymentModalContainer = lazy(() => import("../../components/payment-modal/payment-modal.container"));
|
const PaymentModalContainer = lazyDev(() => import("../../components/payment-modal/payment-modal.container"));
|
||||||
const ProductionListPage = lazy(() => import("../production-list/production-list.container"));
|
const ProductionListPage = lazyDev(() => import("../production-list/production-list.container"));
|
||||||
const ProductionBoardPage = lazy(() => import("../production-board/production-board.container"));
|
const ProductionBoardPage = lazyDev(() => import("../production-board/production-board.container"));
|
||||||
// const ShopTemplates = lazy(() =>
|
// const ShopTemplates = lazyDev(() =>
|
||||||
// import("../shop-templates/shop-templates.container")
|
// import("../shop-templates/shop-templates.container")
|
||||||
// );
|
// );
|
||||||
const JobIntake = lazy(() => import("../jobs-intake/jobs-intake.page.container"));
|
const JobIntake = lazyDev(() => import("../jobs-intake/jobs-intake.page.container"));
|
||||||
const JobChecklistView = lazy(() => import("../jobs-checklist-view/jobs-checklist-view.page"));
|
const JobChecklistView = lazyDev(() => import("../jobs-checklist-view/jobs-checklist-view.page"));
|
||||||
const JobDeliver = lazy(() => import("../jobs-deliver/jobs-delivery.page.container"));
|
const JobDeliver = lazyDev(() => import("../jobs-deliver/jobs-delivery.page.container"));
|
||||||
const AccountingQboCallback = lazy(() => import("../accounting-qbo/accounting-qbo.page"));
|
const AccountingQboCallback = lazyDev(() => import("../accounting-qbo/accounting-qbo.page"));
|
||||||
const AccountingReceivables = lazy(() => import("../accounting-receivables/accounting-receivables.container"));
|
const AccountingReceivables = lazyDev(() => import("../accounting-receivables/accounting-receivables.container"));
|
||||||
const AccountingPayables = lazy(() => import("../accounting-payables/accounting-payables.container"));
|
const AccountingPayables = lazyDev(() => import("../accounting-payables/accounting-payables.container"));
|
||||||
const AccountingPayments = lazy(() => import("../accounting-payments/accounting-payments.container"));
|
const AccountingPayments = lazyDev(() => import("../accounting-payments/accounting-payments.container"));
|
||||||
const AllJobs = lazy(() => import("../jobs-all/jobs-all.container"));
|
const AllJobs = lazyDev(() => import("../jobs-all/jobs-all.container"));
|
||||||
const ReadyJobs = lazy(() => import("../jobs-ready/jobs-ready.page"));
|
const ReadyJobs = lazyDev(() => import("../jobs-ready/jobs-ready.page"));
|
||||||
const JobsClose = lazy(() => import("../jobs-close/jobs-close.container"));
|
const JobsClose = lazyDev(() => import("../jobs-close/jobs-close.container"));
|
||||||
const JobsAdmin = lazy(() => import("../jobs-admin/jobs-admin.page"));
|
const JobsAdmin = lazyDev(() => import("../jobs-admin/jobs-admin.page"));
|
||||||
const TempDocs = lazy(() => import("../temporary-docs/temporary-docs.container"));
|
const TempDocs = lazyDev(() => import("../temporary-docs/temporary-docs.container"));
|
||||||
|
|
||||||
const ShopCsiPageContainer = lazy(() => import("../shop-csi/shop-csi.container.page"));
|
const ShopCsiPageContainer = lazyDev(() => import("../shop-csi/shop-csi.container.page"));
|
||||||
const PaymentsAll = lazy(() => import("../payments-all/payments-all.container.page"));
|
const PaymentsAll = lazyDev(() => import("../payments-all/payments-all.container.page"));
|
||||||
const ShiftClock = lazy(() => import("../shift-clock/shift-clock.page"));
|
const ShiftClock = lazyDev(() => import("../shift-clock/shift-clock.page"));
|
||||||
const Scoreboard = lazy(() => import("../scoreboard/scoreboard.page.container"));
|
const Scoreboard = lazyDev(() => import("../scoreboard/scoreboard.page.container"));
|
||||||
const TimeTicketsAll = lazy(() => import("../time-tickets/time-tickets.container"));
|
const TimeTicketsAll = lazyDev(() => import("../time-tickets/time-tickets.container"));
|
||||||
const Help = lazy(() => import("../help/help.page"));
|
const Help = lazyDev(() => import("../help/help.page"));
|
||||||
const PartsQueue = lazy(() => import("../parts-queue/parts-queue.page.container"));
|
const PartsQueue = lazyDev(() => import("../parts-queue/parts-queue.page.container"));
|
||||||
const ExportLogs = lazy(() => import("../export-logs/export-logs.page.container"));
|
const ExportLogs = lazyDev(() => import("../export-logs/export-logs.page.container"));
|
||||||
const Phonebook = lazy(() => import("../phonebook/phonebook.page.container"));
|
const Phonebook = lazyDev(() => import("../phonebook/phonebook.page.container"));
|
||||||
|
|
||||||
const EmailTest = lazy(() => import("../../components/email-test/email-test-component"));
|
const EmailTest = lazyDev(() => import("../../components/email-test/email-test-component"));
|
||||||
const Dashboard = lazy(() => import("../dashboard/dashboard.container"));
|
const Dashboard = lazyDev(() => import("../dashboard/dashboard.container"));
|
||||||
const Dms = lazy(() => import("../dms/dms.container"));
|
const Dms = lazyDev(() => import("../dms/dms.container"));
|
||||||
const DmsPayables = lazy(() => import("../dms-payables/dms-payables.container"));
|
const DmsPayables = lazyDev(() => import("../dms-payables/dms-payables.container"));
|
||||||
const ManageRootPage = lazy(() => import("../manage-root/manage-root.page.container"));
|
const ManageRootPage = lazyDev(() => import("../manage-root/manage-root.page.container"));
|
||||||
const TtApprovals = lazy(() => import("../tt-approvals/tt-approvals.page.container"));
|
const TtApprovals = lazyDev(() => import("../tt-approvals/tt-approvals.page.container"));
|
||||||
const MyTasksPage = lazy(() => import("../tasks/myTasksPageContainer.jsx"));
|
const MyTasksPage = lazyDev(() => import("../tasks/myTasksPageContainer.jsx"));
|
||||||
const AllTasksPage = lazy(() => import("../tasks/allTasksPageContainer.jsx"));
|
const AllTasksPage = lazyDev(() => import("../tasks/allTasksPageContainer.jsx"));
|
||||||
|
|
||||||
const TaskUpsertModalContainer = lazy(() => import("../../components/task-upsert-modal/task-upsert-modal.container"));
|
const TaskUpsertModalContainer = lazyDev(() => import("../../components/task-upsert-modal/task-upsert-modal.container"));
|
||||||
const { Content } = Layout;
|
const { Content } = Layout;
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
|||||||
@@ -167,6 +167,7 @@ export function PhonebookPageComponent({ bodyshop, authLevel }) {
|
|||||||
searchParams.page = 1;
|
searchParams.page = 1;
|
||||||
history({ search: queryString.stringify(searchParams) });
|
history({ search: queryString.stringify(searchParams) });
|
||||||
}}
|
}}
|
||||||
|
enterButton
|
||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as Sentry from "@sentry/react";
|
import * as Sentry from "@sentry/react";
|
||||||
import { FloatButton, Layout, Spin } from "antd";
|
import { FloatButton, Layout, Spin } from "antd";
|
||||||
import { lazy, Suspense, useEffect } from "react";
|
import { Suspense, useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Navigate, Route, Routes, useLocation } from "react-router-dom";
|
import { Navigate, Route, Routes, useLocation } from "react-router-dom";
|
||||||
@@ -15,20 +15,21 @@ import UpdateAlert from "../../components/update-alert/update-alert.component.js
|
|||||||
import { selectBodyshop, selectInstanceConflict } from "../../redux/user/user.selectors.js";
|
import { selectBodyshop, selectInstanceConflict } from "../../redux/user/user.selectors.js";
|
||||||
import InstanceRenderManager from "../../utils/instanceRenderMgr.js";
|
import InstanceRenderManager from "../../utils/instanceRenderMgr.js";
|
||||||
import useAlertsNotifications from "../../hooks/useAlertsNotifications.jsx";
|
import useAlertsNotifications from "../../hooks/useAlertsNotifications.jsx";
|
||||||
|
import { lazyDev } from "../../utils/lazyWithPreload.jsx";
|
||||||
|
|
||||||
const SimplifiedPartsJobsPage = lazy(() => import("../simplified-parts-jobs/simplified-parts-jobs.page.jsx"));
|
const SimplifiedPartsJobsPage = lazyDev(() => import("../simplified-parts-jobs/simplified-parts-jobs.page.jsx"));
|
||||||
const SimplifiedPartsJobsDetailPage = lazy(
|
const SimplifiedPartsJobsDetailPage = lazyDev(
|
||||||
() => import("../simplified-parts-jobs-detail/simplified-parts-jobs-detail.container.jsx")
|
() => import("../simplified-parts-jobs-detail/simplified-parts-jobs-detail.container.jsx")
|
||||||
);
|
);
|
||||||
const PartsSettingsPage = lazy(() => import("../parts-settings/parts-settings.page.component.jsx"));
|
const PartsSettingsPage = lazyDev(() => import("../parts-settings/parts-settings.page.component.jsx"));
|
||||||
const ShopVendorPageContainer = lazy(() => import("../shop-vendor/shop-vendor.page.container.jsx"));
|
const ShopVendorPageContainer = lazyDev(() => import("../shop-vendor/shop-vendor.page.container.jsx"));
|
||||||
const EmailOverlayContainer = lazy(() => import("../../components/email-overlay/email-overlay.container.jsx"));
|
const EmailOverlayContainer = lazyDev(() => import("../../components/email-overlay/email-overlay.container.jsx"));
|
||||||
const ReportCenterModal = lazy(() => import("../../components/report-center-modal/report-center-modal.container.jsx"));
|
const ReportCenterModal = lazyDev(() => import("../../components/report-center-modal/report-center-modal.container.jsx"));
|
||||||
const PrintCenterModalContainer = lazy(
|
const PrintCenterModalContainer = lazyDev(
|
||||||
() => import("../../components/print-center-modal/print-center-modal.container")
|
() => import("../../components/print-center-modal/print-center-modal.container")
|
||||||
);
|
);
|
||||||
const VehiclesContainer = lazy(() => import("../vehicles/vehicles.page.container.jsx"));
|
const VehiclesContainer = lazyDev(() => import("../vehicles/vehicles.page.container.jsx"));
|
||||||
const VehiclesDetailContainer = lazy(() => import("../vehicles-detail/vehicles-detail.page.container.jsx"));
|
const VehiclesDetailContainer = lazyDev(() => import("../vehicles-detail/vehicles-detail.page.container.jsx"));
|
||||||
const { Content } = Layout;
|
const { Content } = Layout;
|
||||||
|
|
||||||
// Redirector to strip '/parts/jobs' from path for non-detail routes
|
// Redirector to strip '/parts/jobs' from path for non-detail routes
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Card, FloatButton, Layout } from "antd";
|
import { Card, FloatButton, Layout } from "antd";
|
||||||
import { lazy, Suspense, useEffect } from "react";
|
import { Suspense, useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Route, Routes, useNavigate } from "react-router-dom";
|
import { Route, Routes, useNavigate } from "react-router-dom";
|
||||||
@@ -15,25 +15,26 @@ import { selectTechnician } from "../../redux/tech/tech.selectors";
|
|||||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||||
import "./tech.page.styles.scss";
|
import "./tech.page.styles.scss";
|
||||||
import UpsellComponent, { upsellEnum } from "../../components/upsell/upsell.component.jsx";
|
import UpsellComponent, { upsellEnum } from "../../components/upsell/upsell.component.jsx";
|
||||||
|
import { lazyDev } from "../../utils/lazyWithPreload.jsx";
|
||||||
|
|
||||||
const TimeTicketModalContainer = lazy(() => import("../../components/time-ticket-modal/time-ticket-modal.container"));
|
const TimeTicketModalContainer = lazyDev(() => import("../../components/time-ticket-modal/time-ticket-modal.container"));
|
||||||
const EmailOverlayContainer = lazy(() => import("../../components/email-overlay/email-overlay.container.jsx"));
|
const EmailOverlayContainer = lazyDev(() => import("../../components/email-overlay/email-overlay.container.jsx"));
|
||||||
const PrintCenterModalContainer = lazy(
|
const PrintCenterModalContainer = lazyDev(
|
||||||
() => import("../../components/print-center-modal/print-center-modal.container")
|
() => import("../../components/print-center-modal/print-center-modal.container")
|
||||||
);
|
);
|
||||||
const TechLogin = lazy(() => import("../../components/tech-login/tech-login.component"));
|
const TechLogin = lazyDev(() => import("../../components/tech-login/tech-login.component"));
|
||||||
const TechLookup = lazy(() => import("../tech-lookup/tech-lookup.container"));
|
const TechLookup = lazyDev(() => import("../tech-lookup/tech-lookup.container"));
|
||||||
const ProductionListPage = lazy(() => import("../production-list/production-list.container"));
|
const ProductionListPage = lazyDev(() => import("../production-list/production-list.container"));
|
||||||
const ProductionBoardPage = lazy(() => import("../production-board/production-board.container"));
|
const ProductionBoardPage = lazyDev(() => import("../production-board/production-board.container"));
|
||||||
const TechJobClock = lazy(() => import("../tech-job-clock/tech-job-clock.component"));
|
const TechJobClock = lazyDev(() => import("../tech-job-clock/tech-job-clock.component"));
|
||||||
const TechShiftClock = lazy(() => import("../tech-shift-clock/tech-shift-clock.component"));
|
const TechShiftClock = lazyDev(() => import("../tech-shift-clock/tech-shift-clock.component"));
|
||||||
const TimeTicketModalTask = lazy(
|
const TimeTicketModalTask = lazyDev(
|
||||||
() => import("../../components/time-ticket-task-modal/time-ticket-task-modal.container")
|
() => import("../../components/time-ticket-task-modal/time-ticket-task-modal.container")
|
||||||
);
|
);
|
||||||
const TechAssignedProdJobs = lazy(() => import("../tech-assigned-prod-jobs/tech-assigned-prod-jobs.component"));
|
const TechAssignedProdJobs = lazyDev(() => import("../tech-assigned-prod-jobs/tech-assigned-prod-jobs.component"));
|
||||||
const TechDispatchedParts = lazy(() => import("../tech-dispatched-parts/tech-dispatched-parts.page"));
|
const TechDispatchedParts = lazyDev(() => import("../tech-dispatched-parts/tech-dispatched-parts.page"));
|
||||||
|
|
||||||
const TaskUpsertModalContainer = lazy(() => import("../../components/task-upsert-modal/task-upsert-modal.container"));
|
const TaskUpsertModalContainer = lazyDev(() => import("../../components/task-upsert-modal/task-upsert-modal.container"));
|
||||||
|
|
||||||
const { Content } = Layout;
|
const { Content } = Layout;
|
||||||
|
|
||||||
|
|||||||
@@ -1676,7 +1676,9 @@
|
|||||||
"deleted": "Error deleting Job. {{error}}",
|
"deleted": "Error deleting Job. {{error}}",
|
||||||
"exporting": "Error exporting Job. {{error}}",
|
"exporting": "Error exporting Job. {{error}}",
|
||||||
"exporting-partner": "Unable to connect to partner application. Please ensure it is running and logged in.",
|
"exporting-partner": "Unable to connect to partner application. Please ensure it is running and logged in.",
|
||||||
|
"invalidjoblines": "Job has invalid job lines.",
|
||||||
"invoicing": "Error invoicing Job. {{error}}",
|
"invoicing": "Error invoicing Job. {{error}}",
|
||||||
|
"missingjoblineids": "Missing job line IDs for job lines.",
|
||||||
"noaccess": "This Job does not exist or you do not have access to it.",
|
"noaccess": "This Job does not exist or you do not have access to it.",
|
||||||
"nodamage": "No damage points on estimate.",
|
"nodamage": "No damage points on estimate.",
|
||||||
"nodates": "No dates specified for this Job.",
|
"nodates": "No dates specified for this Job.",
|
||||||
@@ -1782,6 +1784,8 @@
|
|||||||
"ded_status": "Deductible Status",
|
"ded_status": "Deductible Status",
|
||||||
"depreciation_taxes": "Betterment/Depreciation/Taxes",
|
"depreciation_taxes": "Betterment/Depreciation/Taxes",
|
||||||
"dms": {
|
"dms": {
|
||||||
|
"first_name": "First Name",
|
||||||
|
"last_name": "Last Name",
|
||||||
"address": "Customer Address",
|
"address": "Customer Address",
|
||||||
"amount": "Amount",
|
"amount": "Amount",
|
||||||
"center": "Center",
|
"center": "Center",
|
||||||
@@ -3574,7 +3578,7 @@
|
|||||||
"accounting-payables": "Payables | {{app}}",
|
"accounting-payables": "Payables | {{app}}",
|
||||||
"accounting-payments": "Payments | {{app}}",
|
"accounting-payments": "Payments | {{app}}",
|
||||||
"accounting-receivables": "Receivables | {{app}}",
|
"accounting-receivables": "Receivables | {{app}}",
|
||||||
"all_tasks": "All Tasks",
|
"all_tasks": "All Tasks | {{app}}",
|
||||||
"app": "",
|
"app": "",
|
||||||
"bc": {
|
"bc": {
|
||||||
"simplified-parts-jobs": "Jobs",
|
"simplified-parts-jobs": "Jobs",
|
||||||
@@ -3655,7 +3659,7 @@
|
|||||||
"jobsdetail": "Job {{ro_number}} | {{app}}",
|
"jobsdetail": "Job {{ro_number}} | {{app}}",
|
||||||
"jobsdocuments": "Job Documents {{ro_number}} | {{app}}",
|
"jobsdocuments": "Job Documents {{ro_number}} | {{app}}",
|
||||||
"manageroot": "Home | {{app}}",
|
"manageroot": "Home | {{app}}",
|
||||||
"my_tasks": "My Tasks",
|
"my_tasks": "My Tasks | {{app}}",
|
||||||
"owners": "All Owners | {{app}}",
|
"owners": "All Owners | {{app}}",
|
||||||
"owners-detail": "{{name}} | {{app}}",
|
"owners-detail": "{{name}} | {{app}}",
|
||||||
"parts-queue": "Parts Queue | {{app}}",
|
"parts-queue": "Parts Queue | {{app}}",
|
||||||
|
|||||||
@@ -1674,7 +1674,9 @@
|
|||||||
"deleted": "Error al eliminar el trabajo.",
|
"deleted": "Error al eliminar el trabajo.",
|
||||||
"exporting": "",
|
"exporting": "",
|
||||||
"exporting-partner": "",
|
"exporting-partner": "",
|
||||||
|
"invalidjoblines": "",
|
||||||
"invoicing": "",
|
"invoicing": "",
|
||||||
|
"missingjoblineids": "",
|
||||||
"noaccess": "Este trabajo no existe o no tiene acceso a él.",
|
"noaccess": "Este trabajo no existe o no tiene acceso a él.",
|
||||||
"nodamage": "",
|
"nodamage": "",
|
||||||
"nodates": "No hay fechas especificadas para este trabajo.",
|
"nodates": "No hay fechas especificadas para este trabajo.",
|
||||||
@@ -1780,6 +1782,8 @@
|
|||||||
"ded_status": "Estado deducible",
|
"ded_status": "Estado deducible",
|
||||||
"depreciation_taxes": "Depreciación / Impuestos",
|
"depreciation_taxes": "Depreciación / Impuestos",
|
||||||
"dms": {
|
"dms": {
|
||||||
|
"first_name": "",
|
||||||
|
"last_name": "",
|
||||||
"address": "",
|
"address": "",
|
||||||
"amount": "",
|
"amount": "",
|
||||||
"center": "",
|
"center": "",
|
||||||
|
|||||||
@@ -1674,7 +1674,9 @@
|
|||||||
"deleted": "Erreur lors de la suppression du travail.",
|
"deleted": "Erreur lors de la suppression du travail.",
|
||||||
"exporting": "",
|
"exporting": "",
|
||||||
"exporting-partner": "",
|
"exporting-partner": "",
|
||||||
|
"invalidjoblines": "",
|
||||||
"invoicing": "",
|
"invoicing": "",
|
||||||
|
"missingjoblineids": "",
|
||||||
"noaccess": "Ce travail n'existe pas ou vous n'y avez pas accès.",
|
"noaccess": "Ce travail n'existe pas ou vous n'y avez pas accès.",
|
||||||
"nodamage": "",
|
"nodamage": "",
|
||||||
"nodates": "Aucune date spécifiée pour ce travail.",
|
"nodates": "Aucune date spécifiée pour ce travail.",
|
||||||
@@ -1780,6 +1782,8 @@
|
|||||||
"ded_status": "Statut de franchise",
|
"ded_status": "Statut de franchise",
|
||||||
"depreciation_taxes": "Amortissement / taxes",
|
"depreciation_taxes": "Amortissement / taxes",
|
||||||
"dms": {
|
"dms": {
|
||||||
|
"first_name": "",
|
||||||
|
"last_name": "",
|
||||||
"address": "",
|
"address": "",
|
||||||
"amount": "",
|
"amount": "",
|
||||||
"center": "",
|
"center": "",
|
||||||
|
|||||||
35
client/src/utils/lazyWithPreload.jsx
Normal file
35
client/src/utils/lazyWithPreload.jsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { lazy } from "react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Conditionally uses lazy loading based on environment.
|
||||||
|
* By default, uses React.lazy in all environments.
|
||||||
|
* Set VITE_DISABLE_LAZY_LOADING=true to load modules immediately in development (avoids compilation delays).
|
||||||
|
*
|
||||||
|
* Usage: const MyComponent = lazyDev(() => import('./MyComponent'));
|
||||||
|
*/
|
||||||
|
export const lazyDev = (importFunc) => {
|
||||||
|
// Check if lazy loading should be disabled (dev only, opt-in via env var)
|
||||||
|
const disableLazyLoading = import.meta.env.DEV && import.meta.env?.VITE_DISABLE_LAZY_LOADING === "true";
|
||||||
|
|
||||||
|
if (!disableLazyLoading) {
|
||||||
|
// Default behavior: use React.lazy for code splitting
|
||||||
|
return lazy(importFunc);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dev mode with lazy loading disabled: load immediately to avoid delays
|
||||||
|
let Component = null;
|
||||||
|
const promise = importFunc().then((module) => {
|
||||||
|
Component = module.default;
|
||||||
|
});
|
||||||
|
|
||||||
|
const LazyDevComponent = (props) => {
|
||||||
|
if (!Component) {
|
||||||
|
throw promise; // Suspense will catch this
|
||||||
|
}
|
||||||
|
return <Component {...props} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
LazyDevComponent.displayName = "LazyDevComponent";
|
||||||
|
|
||||||
|
return LazyDevComponent;
|
||||||
|
};
|
||||||
@@ -11,6 +11,8 @@ import { VitePWA } from "vite-plugin-pwa";
|
|||||||
import InstanceRenderManager from "./src/utils/instanceRenderMgr";
|
import InstanceRenderManager from "./src/utils/instanceRenderMgr";
|
||||||
import browserslist from "browserslist";
|
import browserslist from "browserslist";
|
||||||
import { browserslistToTargets } from "lightningcss";
|
import { browserslistToTargets } from "lightningcss";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
import { dirname, resolve } from "path";
|
||||||
|
|
||||||
process.env.VITE_APP_GIT_SHA_DATE = new Date().toLocaleString("en-US", { timeZone: "America/Los_Angeles" });
|
process.env.VITE_APP_GIT_SHA_DATE = new Date().toLocaleString("en-US", { timeZone: "America/Los_Angeles" });
|
||||||
const commitHash = child.execSync("git rev-parse HEAD").toString().trimEnd();
|
const commitHash = child.execSync("git rev-parse HEAD").toString().trimEnd();
|
||||||
@@ -43,13 +45,19 @@ const httpsCerts = {
|
|||||||
cert: await fsPromises.readFile("../certs/cert.pem")
|
cert: await fsPromises.readFile("../certs/cert.pem")
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
export default defineConfig(({ command, mode }) => {
|
export default defineConfig(({ command, mode }) => {
|
||||||
// Only enable React Compiler on build in production/test (keeps dev as fast as possible)
|
// React Compiler is always enabled for production/test builds
|
||||||
|
// In dev mode, it's enabled by default but can be disabled with VITE_DISABLE_COMPILER_IN_DEV=true
|
||||||
const isBuild = command === "build";
|
const isBuild = command === "build";
|
||||||
const isTestBuild =
|
const isTestBuild =
|
||||||
mode === "test" || process.env.VITE_APP_IS_TEST === "true" || process.env.VITE_APP_IS_TEST === "1";
|
mode === "test" || process.env.VITE_APP_IS_TEST === "true" || process.env.VITE_APP_IS_TEST === "1";
|
||||||
|
|
||||||
const enableReactCompiler =
|
const enableReactCompiler =
|
||||||
process.env.VITE_ENABLE_COMPILER_IN_DEV || (isBuild && (mode === "production" || isTestBuild));
|
(isBuild && (mode === "production" || isTestBuild)) || // Always enable for prod/test builds
|
||||||
|
process.env?.VITE_DISABLE_COMPILER_IN_DEV !== "true"; // In dev, enable unless explicitly disabled
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
enableReactCompiler ? chalk.green.bold("React Compiler enabled") : chalk.yellow.bold("React Compiler disabled")
|
enableReactCompiler ? chalk.green.bold("React Compiler enabled") : chalk.yellow.bold("React Compiler disabled")
|
||||||
@@ -57,6 +65,13 @@ export default defineConfig(({ command, mode }) => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
base: "/",
|
base: "/",
|
||||||
|
resolve: {
|
||||||
|
dedupe: ["styled-components", "react", "react-dom"],
|
||||||
|
alias: {
|
||||||
|
// Force all styled-components imports to resolve to the same location (absolute path)
|
||||||
|
"styled-components": resolve(__dirname, "node_modules/styled-components/dist/styled-components.browser.esm.js")
|
||||||
|
}
|
||||||
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
ViteEjsPlugin((viteConfig) => ({ env: viteConfig.env })),
|
ViteEjsPlugin((viteConfig) => ({ env: viteConfig.env })),
|
||||||
|
|
||||||
@@ -243,7 +258,8 @@ export default defineConfig(({ command, mode }) => {
|
|||||||
|
|
||||||
// Strip console/debugger in prod to shrink bundles
|
// Strip console/debugger in prod to shrink bundles
|
||||||
esbuild: {
|
esbuild: {
|
||||||
//drop: ["console", "debugger"]
|
// drop: mode === "production" ? ["console", "debugger"] : [],
|
||||||
|
legalComments: "none" // Remove license comments in production
|
||||||
},
|
},
|
||||||
|
|
||||||
optimizeDeps: {
|
optimizeDeps: {
|
||||||
@@ -265,11 +281,14 @@ export default defineConfig(({ command, mode }) => {
|
|||||||
"@firebase/firestore",
|
"@firebase/firestore",
|
||||||
"@firebase/auth",
|
"@firebase/auth",
|
||||||
"@firebase/messaging",
|
"@firebase/messaging",
|
||||||
"@firebase/util"
|
"@firebase/util",
|
||||||
|
"styled-components"
|
||||||
],
|
],
|
||||||
esbuildOptions: {
|
esbuildOptions: {
|
||||||
loader: { ".jsx": "jsx", ".tsx": "tsx" }
|
loader: { ".jsx": "jsx", ".tsx": "tsx" }
|
||||||
}
|
},
|
||||||
|
// Force styled-components to be pre-bundled and deduplicated
|
||||||
|
force: mode === "development"
|
||||||
},
|
},
|
||||||
|
|
||||||
css: {
|
css: {
|
||||||
|
|||||||
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"
|
"job-totals-fixtures:local": "docker exec node-app /usr/bin/node /app/download-job-totals-fixtures.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-cloudwatch-logs": "^3.974.0",
|
"@aws-sdk/client-cloudwatch-logs": "^3.978.0",
|
||||||
"@aws-sdk/client-elasticache": "^3.974.0",
|
"@aws-sdk/client-elasticache": "^3.978.0",
|
||||||
"@aws-sdk/client-s3": "^3.974.0",
|
"@aws-sdk/client-s3": "^3.978.0",
|
||||||
"@aws-sdk/client-secrets-manager": "^3.974.0",
|
"@aws-sdk/client-secrets-manager": "^3.978.0",
|
||||||
"@aws-sdk/client-ses": "^3.974.0",
|
"@aws-sdk/client-ses": "^3.978.0",
|
||||||
"@aws-sdk/credential-provider-node": "^3.972.1",
|
"@aws-sdk/credential-provider-node": "^3.972.3",
|
||||||
"@aws-sdk/lib-storage": "^3.974.0",
|
"@aws-sdk/lib-storage": "^3.978.0",
|
||||||
"@aws-sdk/s3-request-presigner": "^3.974.0",
|
"@aws-sdk/s3-request-presigner": "^3.978.0",
|
||||||
"@opensearch-project/opensearch": "^2.13.0",
|
"@opensearch-project/opensearch": "^2.13.0",
|
||||||
"@socket.io/admin-ui": "^0.5.1",
|
"@socket.io/admin-ui": "^0.5.1",
|
||||||
"@socket.io/redis-adapter": "^8.3.0",
|
"@socket.io/redis-adapter": "^8.3.0",
|
||||||
"archiver": "^7.0.1",
|
"archiver": "^7.0.1",
|
||||||
"aws4": "^1.13.2",
|
"aws4": "^1.13.2",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.4",
|
||||||
"axios-curlirize": "^2.0.0",
|
"axios-curlirize": "^2.0.0",
|
||||||
"better-queue": "^3.8.12",
|
"better-queue": "^3.8.12",
|
||||||
"bullmq": "^5.66.7",
|
"bullmq": "^5.67.2",
|
||||||
"chart.js": "^4.5.1",
|
"chart.js": "^4.5.1",
|
||||||
"cloudinary": "^2.9.0",
|
"cloudinary": "^2.9.0",
|
||||||
"compression": "^1.8.1",
|
"compression": "^1.8.1",
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
"dinero.js": "^1.9.1",
|
"dinero.js": "^1.9.1",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"express": "^4.21.1",
|
"express": "^4.21.1",
|
||||||
"fast-xml-parser": "^5.3.3",
|
"fast-xml-parser": "^5.3.4",
|
||||||
"firebase-admin": "^13.6.0",
|
"firebase-admin": "^13.6.0",
|
||||||
"graphql": "^16.12.0",
|
"graphql": "^16.12.0",
|
||||||
"graphql-request": "^6.1.0",
|
"graphql-request": "^6.1.0",
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
"recursive-diff": "^1.0.9",
|
"recursive-diff": "^1.0.9",
|
||||||
"rimraf": "^6.1.2",
|
"rimraf": "^6.1.2",
|
||||||
"skia-canvas": "^3.0.8",
|
"skia-canvas": "^3.0.8",
|
||||||
"soap": "^1.6.3",
|
"soap": "^1.6.4",
|
||||||
"socket.io": "^4.8.3",
|
"socket.io": "^4.8.3",
|
||||||
"socket.io-adapter": "^2.5.6",
|
"socket.io-adapter": "^2.5.6",
|
||||||
"ssh2-sftp-client": "^11.0.0",
|
"ssh2-sftp-client": "^11.0.0",
|
||||||
@@ -82,7 +82,7 @@
|
|||||||
"@eslint/js": "^9.39.2",
|
"@eslint/js": "^9.39.2",
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^9.39.2",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"globals": "^17.1.0",
|
"globals": "^17.2.0",
|
||||||
"mock-require": "^3.0.3",
|
"mock-require": "^3.0.3",
|
||||||
"p-limit": "^3.1.0",
|
"p-limit": "^3.1.0",
|
||||||
"prettier": "^3.8.1",
|
"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}`);
|
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 {
|
} else {
|
||||||
//There was something wrong. Throw an error to trigger clean up.
|
//There was something wrong. Throw an error to trigger clean up.
|
||||||
//throw new Error("Error posting DMS Batch Transaction");
|
//throw new Error("Error posting DMS Batch Transaction");
|
||||||
@@ -431,10 +430,10 @@ async function QueryDmsCustomerByName({ socket, redisHelpers, JobData }) {
|
|||||||
const ownerName =
|
const ownerName =
|
||||||
JobData.ownr_co_nm && JobData.ownr_co_nm.trim() !== ""
|
JobData.ownr_co_nm && JobData.ownr_co_nm.trim() !== ""
|
||||||
//? [["firstName", JobData.ownr_co_nm.replace(replaceSpecialRegex, "").toUpperCase()]] // Commented out until we receive direction.
|
//? [["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()],
|
["firstName", JobData.ownr_fn?.replace(replaceSpecialRegex, "").toUpperCase()],
|
||||||
["lastName", JobData.ownr_ln.replace(replaceSpecialRegex, "").toUpperCase()]
|
["lastName", JobData.ownr_ln?.replace(replaceSpecialRegex, "").toUpperCase()]
|
||||||
];
|
];
|
||||||
try {
|
try {
|
||||||
const result = await MakeFortellisCall({
|
const result = await MakeFortellisCall({
|
||||||
|
|||||||
@@ -1725,6 +1725,7 @@ query QUERY_JOB_COSTING_DETAILS($id: uuid!) {
|
|||||||
profitcenter_part
|
profitcenter_part
|
||||||
profitcenter_labor
|
profitcenter_labor
|
||||||
act_price_before_ppc
|
act_price_before_ppc
|
||||||
|
manual_line
|
||||||
}
|
}
|
||||||
bills {
|
bills {
|
||||||
id
|
id
|
||||||
@@ -1842,6 +1843,7 @@ exports.QUERY_JOB_COSTING_DETAILS_MULTI = ` query QUERY_JOB_COSTING_DETAILS_MULT
|
|||||||
op_code_desc
|
op_code_desc
|
||||||
profitcenter_part
|
profitcenter_part
|
||||||
profitcenter_labor
|
profitcenter_labor
|
||||||
|
manual_line
|
||||||
}
|
}
|
||||||
bills {
|
bills {
|
||||||
id
|
id
|
||||||
|
|||||||
@@ -343,7 +343,7 @@ function GenerateCostingData(job) {
|
|||||||
if (!acc.labor[laborProfitCenter]) acc.labor[laborProfitCenter] = Dinero();
|
if (!acc.labor[laborProfitCenter]) acc.labor[laborProfitCenter] = Dinero();
|
||||||
acc.labor[laborProfitCenter] = acc.labor[laborProfitCenter].add(laborAmount);
|
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.
|
//Scenario where SGI may pay out hours using a part price.
|
||||||
acc.labor[laborProfitCenter] = acc.labor[laborProfitCenter].add(
|
acc.labor[laborProfitCenter] = acc.labor[laborProfitCenter].add(
|
||||||
Dinero({
|
Dinero({
|
||||||
@@ -363,6 +363,9 @@ function GenerateCostingData(job) {
|
|||||||
if (val.mod_lbr_ty !== "LAR" && mashOpCodes.includes(val.lbr_op)) {
|
if (val.mod_lbr_ty !== "LAR" && mashOpCodes.includes(val.lbr_op)) {
|
||||||
materialsHours.mashHrs += val.mod_lb_hrs || 0;
|
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(),
|
let disc = Dinero(),
|
||||||
markup = Dinero();
|
markup = Dinero();
|
||||||
const convertedKey = Object.keys(defaultProfits).find((k) => defaultProfits[k] === key);
|
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 (
|
if (
|
||||||
job.parts_tax_rates[convertedKey.toUpperCase()].prt_discp !== undefined &&
|
job.parts_tax_rates[convertedKey.toUpperCase()].prt_discp !== undefined &&
|
||||||
job.parts_tax_rates[convertedKey.toUpperCase()].prt_discp >= 0
|
job.parts_tax_rates[convertedKey.toUpperCase()].prt_discp >= 0
|
||||||
@@ -523,14 +526,16 @@ function GenerateCostingData(job) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (InstanceManager({ rome: true })) {
|
if (InstanceManager({ rome: true })) {
|
||||||
const correspondingCiecaStlTotalLine = job.cieca_stl?.data.find(
|
if (convertedKey) {
|
||||||
(c) => c.ttl_typecd === convertedKey.toUpperCase()
|
const correspondingCiecaStlTotalLine = job.cieca_stl?.data.find(
|
||||||
);
|
(c) => c.ttl_typecd === convertedKey.toUpperCase()
|
||||||
if (
|
);
|
||||||
correspondingCiecaStlTotalLine &&
|
if (
|
||||||
Math.abs(jobLineTotalsByProfitCenter.parts[key].getAmount() - correspondingCiecaStlTotalLine.ttl_amt * 100) > 1
|
correspondingCiecaStlTotalLine &&
|
||||||
) {
|
Math.abs(jobLineTotalsByProfitCenter.parts[key].getAmount() - correspondingCiecaStlTotalLine.ttl_amt * 100) > 1
|
||||||
jobLineTotalsByProfitCenter.parts[key] = jobLineTotalsByProfitCenter.parts[key].add(disc).add(markup);
|
) {
|
||||||
|
jobLineTotalsByProfitCenter.parts[key] = jobLineTotalsByProfitCenter.parts[key].add(disc).add(markup);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -47,14 +47,14 @@ exports.totalsSsu = async function (req, res) {
|
|||||||
throw new Error("Failed to update job totals");
|
throw new Error("Failed to update job totals");
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(200).send();
|
res.status(200).json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("job-totals-ssu-USA-error", "error", req?.user?.email, id, {
|
logger.log("job-totals-ssu-USA-error", "error", req?.user?.email, id, {
|
||||||
jobid: id,
|
jobid: id,
|
||||||
error: error.message,
|
error: error.message,
|
||||||
stack: error.stack
|
stack: error.stack
|
||||||
});
|
});
|
||||||
res.status(503).send();
|
res.status(503).json({ error: "Failed to calculate totals" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -47,14 +47,14 @@ exports.totalsSsu = async function (req, res) {
|
|||||||
throw new Error("Failed to update job totals");
|
throw new Error("Failed to update job totals");
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(200).send();
|
res.status(200).json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("job-totals-ssu-error", "error", req.user.email, id, {
|
logger.log("job-totals-ssu-error", "error", req.user.email, id, {
|
||||||
jobid: id,
|
jobid: id,
|
||||||
error: error.message,
|
error: error.message,
|
||||||
stack: error.stack
|
stack: error.stack
|
||||||
});
|
});
|
||||||
res.status(503).send();
|
res.status(503).json({ error: "Failed to calculate totals" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user