Compare commits

..

1 Commits

Author SHA1 Message Date
Patrick Fic
3775789e9d Merged in feature/IO-3322-intellipay-refund (pull request #2457)
IO-3332 Add error message to intellipay refund error.

Approved-by: Dave Richer
2025-08-11 17:46:24 +00:00
930 changed files with 15011 additions and 32776 deletions

View File

@@ -11,6 +11,7 @@ node_modules
# Files to exclude
.ebignore
.editorconfig
.eslintrc.json
.gitignore
.prettierrc.js
Dockerfile
@@ -18,6 +19,6 @@ README.MD
bodyshop_translations.babel
docker-compose.yml
ecosystem.config.js
eslint.config.mjs
# Optional: Exclude logs and temporary files
*.log

19
.eslintrc.json Normal file
View File

@@ -0,0 +1,19 @@
{
"env": {
"es6": true,
"node": true
},
"extends": "eslint:recommended",
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module"
},
"rules": {
"no-console": "off"
},
"settings": {}
}

View File

@@ -1,346 +0,0 @@
Fortellis Notes
Subscription ID
- Appears to give us a list of all dealerships we have access to, and `apiDmsInfo` contains the integrations that are enabled for that dealership.
- Will likely need to filter based on the DMS ID or something?
- Should store the whole subscription object. Contains department information needed in subsequent calls.
Department ID
- May have multiple departments. Appears that financial stuff goes to Accounting, History will go to Service.
- TODO: How do we handle the multiple departments that may come up.
###Internal Questions
* Overview of the redis storing mechanism to cache this data.
*
# GL Wip Posting
## Org Helper Return Data
```json
[
{
"acctgLgnID": "DEVWB-A",
"applCode": "V",
"coID": "77",
"companyName": "TEST SYS C187092 DEVWB",
"lgnDesc": "DEV WRITE BACK VMS",
"logon": "DEVWB-V"
},
{
"acctgLgnID": "DEVWB-A",
"applCode": "F",
"coID": "77",
"companyName": "TEST SYS C187092 DEVWB",
"lgnDesc": "DEV WRITE BACK F&I SALES",
"logon": "DEVWB-FI"
},
{
"acctgLgnID": "DEVWB-A",
"applCode": "CS",
"coID": "77",
"companyName": "TEST SYS C187092 DEVWB",
"lgnDesc": "DEV WRITE BACK SERVICE",
"logon": "DEVWB-S"
},
{
"acctgLgnID": "DEVWB-A",
"applCode": "A",
"coID": "77",
"companyName": "TEST SYS C187092 DEVWB",
"lgnDesc": "DEV WRITE BACK ACCTG",
"logon": "DEVWB-A"
},
{
"acctgLgnID": "DEVWB-A",
"applCode": "SL",
"coID": "77",
"companyName": "TEST SYS C187092 DEVWB",
"lgnDesc": "DEV WRTIE BACK SLS MGMT",
"logon": "DEVWB-SL"
},
{
"acctgLgnID": "DEVWB-A",
"applCode": "O",
"coID": "77",
"companyName": "TEST SYS C187092 DEVWB",
"lgnDesc": "DEV WRITE BACK PARTS",
"logon": "DEVWB-I"
}
]
```
## Journal Helper Return Data
```json
[
{
"companyNo": "77",
"jrnlID": "32",
"jrnlName": "PARTS SALES",
"jrnlType": "S",
"intercoFlag": "0",
"defaultDocType": "4",
"errCode": "",
"errMsg": ""
},
{
"companyNo": "77",
"jrnlID": "92",
"jrnlName": "YTD ADJUSTMENTS",
"jrnlType": "Y",
"intercoFlag": "0",
"defaultDocType": "3",
"errCode": "",
"errMsg": ""
},
{
"companyNo": "77",
"jrnlID": "12",
"jrnlName": "FLEET SALES",
"jrnlType": "S",
"intercoFlag": "0",
"defaultDocType": "9",
"errCode": "",
"errMsg": ""
},
{
"companyNo": "77",
"jrnlID": "57",
"jrnlName": "CASH RECEIPTS (OPEN-ITEM)",
"jrnlType": "R",
"intercoFlag": "0",
"defaultDocType": "1",
"errCode": "",
"errMsg": ""
},
{
"companyNo": "77",
"jrnlID": "93",
"jrnlName": "SET UP HISTORY",
"jrnlType": "H",
"intercoFlag": "0",
"defaultDocType": "10",
"errCode": "",
"errMsg": ""
},
{
"companyNo": "77",
"jrnlID": "88",
"jrnlName": "F/S STATISCAL DATA",
"jrnlType": "F",
"intercoFlag": "0",
"defaultDocType": "10",
"errCode": "",
"errMsg": ""
},
{
"companyNo": "77",
"jrnlID": "58",
"jrnlName": "WARRANTY CREDITS",
"jrnlType": "G",
"intercoFlag": "0",
"defaultDocType": "3",
"errCode": "",
"errMsg": ""
},
{
"companyNo": "77",
"jrnlID": "FC",
"jrnlName": "FINANCE CHARGE",
"jrnlType": "A",
"intercoFlag": "0",
"defaultDocType": "12",
"errCode": "",
"errMsg": ""
},
{
"companyNo": "77",
"jrnlID": "94",
"jrnlName": "SET UP SCHEDULES",
"jrnlType": "C",
"intercoFlag": "0",
"defaultDocType": "3",
"errCode": "",
"errMsg": ""
},
{
"companyNo": "77",
"jrnlID": "95",
"jrnlName": "SET UP GENERAL LEDGER",
"jrnlType": "B",
"intercoFlag": "0",
"defaultDocType": "3",
"errCode": "",
"errMsg": ""
},
{
"companyNo": "77",
"jrnlID": "20",
"jrnlName": "USED VEHICLE SALES",
"jrnlType": "S",
"intercoFlag": "0",
"defaultDocType": "9",
"errCode": "",
"errMsg": ""
},
{
"companyNo": "77",
"jrnlID": "60",
"jrnlName": "CASH DISBURSEMENTS",
"jrnlType": "G",
"intercoFlag": "0",
"defaultDocType": "2",
"errCode": "",
"errMsg": ""
},
{
"companyNo": "77",
"jrnlID": "30",
"jrnlName": "SERVICE SALES",
"jrnlType": "S",
"intercoFlag": "0",
"defaultDocType": "7",
"errCode": "",
"errMsg": ""
},
{
"companyNo": "77",
"jrnlID": "40",
"jrnlName": "PAYROLL",
"jrnlType": "G",
"intercoFlag": "0",
"defaultDocType": "11",
"errCode": "",
"errMsg": ""
},
{
"companyNo": "77",
"jrnlID": "15",
"jrnlName": "DEALER TRADES",
"jrnlType": "S",
"intercoFlag": "0",
"defaultDocType": "9",
"errCode": "",
"errMsg": ""
},
{
"companyNo": "77",
"jrnlID": "70",
"jrnlName": "NEW VEHICLE PURCHASES",
"jrnlType": "G",
"intercoFlag": "0",
"defaultDocType": "8",
"errCode": "",
"errMsg": ""
},
{
"companyNo": "77",
"jrnlID": "25",
"jrnlName": "USED WHOLESALE",
"jrnlType": "S",
"intercoFlag": "0",
"defaultDocType": "9",
"errCode": "",
"errMsg": ""
},
{
"companyNo": "77",
"jrnlID": "75",
"jrnlName": "GENERAL PURCHASES",
"jrnlType": "G",
"intercoFlag": "0",
"defaultDocType": "5",
"errCode": "",
"errMsg": ""
},
{
"companyNo": "77",
"jrnlID": "10",
"jrnlName": "NEW VEHICLE SALES",
"jrnlType": "S",
"intercoFlag": "0",
"defaultDocType": "9",
"errCode": "",
"errMsg": ""
},
{
"companyNo": "77",
"jrnlID": "80",
"jrnlName": "GENERAL JOURNAL",
"jrnlType": "G",
"intercoFlag": "0",
"defaultDocType": "3",
"errCode": "",
"errMsg": ""
},
{
"companyNo": "77",
"jrnlID": "11",
"jrnlName": "WORK IN PROGRESS",
"jrnlType": "G",
"intercoFlag": "0",
"defaultDocType": "10",
"errCode": "",
"errMsg": ""
},
{
"companyNo": "77",
"jrnlID": "56",
"jrnlName": "CASH RECEIPTS (BALANCE FWD)",
"jrnlType": "G",
"intercoFlag": "0",
"defaultDocType": "1",
"errCode": "",
"errMsg": ""
},
{
"companyNo": "77",
"jrnlID": "81",
"jrnlName": "STANDARD ENTRIES",
"jrnlType": "G",
"intercoFlag": "0",
"defaultDocType": "6",
"errCode": "",
"errMsg": ""
},
{
"companyNo": "77",
"jrnlID": "51",
"jrnlName": "CASH RECEIPTS JOURNAL - EFT",
"jrnlType": "G",
"intercoFlag": "0",
"defaultDocType": "10",
"errCode": "",
"errMsg": ""
},
{
"companyNo": "77",
"jrnlID": "61",
"jrnlName": "CASH DISBURSMENTS -EFT",
"jrnlType": "G",
"intercoFlag": "0",
"defaultDocType": "10",
"errCode": "",
"errMsg": ""
},
{
"companyNo": "77",
"jrnlID": "71",
"jrnlName": "USED VEHICLE PURCHASES",
"jrnlType": "G",
"intercoFlag": "0",
"defaultDocType": "8",
"errCode": "",
"errMsg": ""
}
]
```
# Feedback
- Receiving bad request errors, with no details. API errors page doesn't indicate what's wrong for certain types of error codes.
- API Error page works on a several minute delay.

View File

@@ -1,96 +1,116 @@
// index.js
import express from "express";
import fetch from "node-fetch";
import { simpleParser } from "mailparser";
import express from 'express';
import fetch from 'node-fetch';
import {simpleParser} from 'mailparser';
const app = express();
const PORT = 3334;
app.get("/", async (req, res) => {
try {
const response = await fetch("http://localhost:4566/_aws/ses");
if (!response.ok) {
throw new Error("Network response was not ok");
app.get('/', async (req, res) => {
try {
const response = await fetch('http://localhost:4566/_aws/ses');
if (!response.ok) {
throw new Error('Network response was not ok');
}
const data = await response.json();
const messagesHtml = await parseMessages(data.messages);
res.send(renderHtml(messagesHtml));
} catch (error) {
console.error('Error fetching messages:', error);
res.status(500).send('Error fetching messages');
}
const data = await response.json();
const messagesHtml = await parseMessages(data.messages);
res.send(renderHtml(messagesHtml));
} catch (error) {
console.error("Error fetching messages:", error);
res.status(500).send("Error fetching messages");
}
});
async function parseMessages(messages) {
const parsedMessages = await Promise.all(
messages.map(async (message, index) => {
try {
const parsed = await simpleParser(message.RawData);
return `
<div class="shadow-md rounded-lg p-4 mb-6" style="background-color: lightgray">
<div class="shadow-md rounded-lg p-4 mb-6" style="background-color: white">
<div class="mb-2"><span class="font-bold text-lg">Message ${index + 1}</span></div>
<div class="mb-2"><span class="font-semibold">From:</span> ${message.Source}</div>
<div class="mb-2"><span class="font-semibold">To:</span> ${parsed.to.text || "No To Address"}</div>
<div class="mb-2"><span class="font-semibold">Subject:</span> ${parsed.subject || "No Subject"}</div>
<div class="mb-2"><span class="font-semibold">Region:</span> ${message.Region}</div>
<div class="mb-2"><span class="font-semibold">Timestamp:</span> ${message.Timestamp}</div>
</div>
<div class="prose">${parsed.html || parsed.textAsHtml || "No HTML content available"}</div>
</div>
`;
} catch (error) {
console.error("Error parsing email:", error);
return `
<div class="bg-white shadow-md rounded-lg p-4 mb-6">
<div class="mb-2"><span class="font-bold text-lg">Message ${index + 1}</span></div>
<div class="mb-2"><span class="font-semibold">From:</span> ${message.Source}</div>
<div class="mb-2"><span class="font-semibold">Region:</span> ${message.Region}</div>
<div class="mb-2"><span class="font-semibold">Timestamp:</span> ${message.Timestamp}</div>
<div class="text-red-500">Error parsing email content</div>
</div>
`;
}
})
);
return parsedMessages.join("");
const parsedMessages = await Promise.all(
messages.map(async (message, index) => {
try {
const parsed = await simpleParser(message.RawData);
return `
<div class="shadow-md rounded-lg p-4 mb-6" style="background-color: lightgray">
<div class="shadow-md rounded-lg p-4 mb-6" style="background-color: white">
<div class="mb-2">
<span class="font-bold text-lg">Message ${index + 1}</span>
</div>
<div class="mb-2">
<span class="font-semibold">From:</span> ${message.Source}
</div>
<div class="mb-2">
<span class="font-semibold">Region:</span> ${message.Region}
</div>
<div class="mb-2">
<span class="font-semibold">Timestamp:</span> ${message.Timestamp}
</div>
</div>
<div class="prose">
${parsed.html || parsed.textAsHtml || 'No HTML content available'}
</div>
</div>
`;
} catch (error) {
console.error('Error parsing email:', error);
return `
<div class="bg-white shadow-md rounded-lg p-4 mb-6">
<div class="mb-2">
<span class="font-bold text-lg">Message ${index + 1}</span>
</div>
<div class="mb-2">
<span class="font-semibold">From:</span> ${message.Source}
</div>
<div class="mb-2">
<span class="font-semibold">Region:</span> ${message.Region}
</div>
<div class="mb-2">
<span class="font-semibold">Timestamp:</span> ${message.Timestamp}
</div>
<div class="text-red-500">
Error parsing email content
</div>
</div>
`;
}
})
);
return parsedMessages.join('');
}
function renderHtml(messagesHtml) {
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Email Messages Viewer</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
body {
background-color: #f3f4f6;
font-family: Arial, sans-serif;
}
.container {
max-width: 800px;
margin: 50px auto;
padding: 20px;
}
.prose {
line-height: 1.6;
}
</style>
</head>
<body>
<div class="container bg-white shadow-lg rounded-lg p-6">
<h1 class="text-2xl font-bold text-center mb-6">Email Messages Viewer</h1>
<div id="messages-container">${messagesHtml}</div>
</div>
</body>
</html>
`;
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Email Messages Viewer</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
body {
background-color: #f3f4f6;
font-family: Arial, sans-serif;
}
.container {
max-width: 800px;
margin: 50px auto;
padding: 20px;
}
.prose {
line-height: 1.6;
}
</style>
</head>
<body>
<div class="container bg-white shadow-lg rounded-lg p-6">
<h1 class="text-2xl font-bold text-center mb-6">Email Messages Viewer</h1>
<div id="messages-container">
${messagesHtml}
</div>
</div>
</body>
</html>
`;
}
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
console.log(`Server is running on http://localhost:${PORT}`);
});

View File

@@ -10,7 +10,7 @@
"license": "ISC",
"dependencies": {
"express": "^5.1.0",
"mailparser": "^3.7.4",
"mailparser": "^3.7.2",
"node-fetch": "^3.3.2"
}
},
@@ -634,9 +634,9 @@
"license": "MIT"
},
"node_modules/libmime": {
"version": "5.3.7",
"resolved": "https://registry.npmjs.org/libmime/-/libmime-5.3.7.tgz",
"integrity": "sha512-FlDb3Wtha8P01kTL3P9M+ZDNDWPKPmKHWaU/cG/lg5pfuAwdflVpZE+wm9m7pKmC5ww6s+zTxBKS1p6yl3KpSw==",
"version": "5.3.6",
"resolved": "https://registry.npmjs.org/libmime/-/libmime-5.3.6.tgz",
"integrity": "sha512-j9mBC7eiqi6fgBPAGvKCXJKJSIASanYF4EeA4iBzSG0HxQxmXnR3KbyWqTn4CwsKSebqCv2f5XZfAO6sKzgvwA==",
"license": "MIT",
"dependencies": {
"encoding-japanese": "2.2.0",
@@ -661,31 +661,31 @@
}
},
"node_modules/mailparser": {
"version": "3.7.4",
"resolved": "https://registry.npmjs.org/mailparser/-/mailparser-3.7.4.tgz",
"integrity": "sha512-Beh4yyR4jLq3CZZ32asajByrXnW8dLyKCAQD3WvtTiBnMtFWhxO+wa93F6sJNjDmfjxXs4NRNjw3XAGLqZR3Vg==",
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/mailparser/-/mailparser-3.7.2.tgz",
"integrity": "sha512-iI0p2TCcIodR1qGiRoDBBwboSSff50vQAWytM5JRggLfABa4hHYCf3YVujtuzV454xrOP352VsAPIzviqMTo4Q==",
"license": "MIT",
"dependencies": {
"encoding-japanese": "2.2.0",
"he": "1.2.0",
"html-to-text": "9.0.5",
"iconv-lite": "0.6.3",
"libmime": "5.3.7",
"libmime": "5.3.6",
"linkify-it": "5.0.0",
"mailsplit": "5.4.5",
"nodemailer": "7.0.4",
"mailsplit": "5.4.2",
"nodemailer": "6.9.16",
"punycode.js": "2.3.1",
"tlds": "1.259.0"
"tlds": "1.255.0"
}
},
"node_modules/mailsplit": {
"version": "5.4.5",
"resolved": "https://registry.npmjs.org/mailsplit/-/mailsplit-5.4.5.tgz",
"integrity": "sha512-oMfhmvclR689IIaQmIcR5nODnZRRVwAKtqFT407TIvmhX2OLUBnshUTcxzQBt3+96sZVDud9NfSe1NxAkUNXEQ==",
"version": "5.4.2",
"resolved": "https://registry.npmjs.org/mailsplit/-/mailsplit-5.4.2.tgz",
"integrity": "sha512-4cczG/3Iu3pyl8JgQ76dKkisurZTmxMrA4dj/e8d2jKYcFTZ7MxOzg1gTioTDMPuFXwTrVuN/gxhkrO7wLg7qA==",
"license": "(MIT OR EUPL-1.1+)",
"dependencies": {
"libbase64": "1.3.0",
"libmime": "5.3.7",
"libmime": "5.3.6",
"libqp": "2.1.1"
}
},
@@ -793,9 +793,9 @@
}
},
"node_modules/nodemailer": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.4.tgz",
"integrity": "sha512-9O00Vh89/Ld2EcVCqJ/etd7u20UhME0f/NToPfArwPEe1Don1zy4mAIz6ariRr7mJ2RDxtaDzN0WJVdVXPtZaw==",
"version": "6.9.16",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.16.tgz",
"integrity": "sha512-psAuZdTIRN08HKVd/E8ObdV6NO7NTBY3KsC30F7M4H1OnmLCUNaS56FpYxyb26zWLSyYF9Ozch9KYHhHegsiOQ==",
"license": "MIT-0",
"engines": {
"node": ">=6.0.0"
@@ -1114,9 +1114,9 @@
}
},
"node_modules/tlds": {
"version": "1.259.0",
"resolved": "https://registry.npmjs.org/tlds/-/tlds-1.259.0.tgz",
"integrity": "sha512-AldGGlDP0PNgwppe2quAvuBl18UcjuNtOnDuUkqhd6ipPqrYYBt3aTxK1QTsBVknk97lS2JcafWMghjGWFtunw==",
"version": "1.255.0",
"resolved": "https://registry.npmjs.org/tlds/-/tlds-1.255.0.tgz",
"integrity": "sha512-tcwMRIioTcF/FcxLev8MJWxCp+GUALRhFEqbDoZrnowmKSGqPrl5pqS+Sut2m8BgJ6S4FExCSSpGffZ0Tks6Aw==",
"license": "MIT",
"bin": {
"tlds": "bin.js"

View File

@@ -12,7 +12,7 @@
"description": "",
"dependencies": {
"express": "^5.1.0",
"mailparser": "^3.7.4",
"mailparser": "^3.7.2",
"node-fetch": "^3.3.2"
}
}

View File

@@ -1,61 +0,0 @@
# PATCH /integrations/parts-management/job/:id/status
Update (patch) the status of a job created under parts management. This endpoint is only available
for jobs whose parent bodyshop has an `external_shop_id` (i.e., is provisioned for parts
management).
## Endpoint
```
PATCH /integrations/parts-management/job/:id/status
```
- `:id` is the UUID of the job to update.
## Request Headers
- `Authorization`: (if required by your integration middleware)
- `Content-Type: application/json`
## Request Body
Send a JSON object with the following field:
- `status` (string, required): The new status for the job.
Example:
```
PATCH /integrations/parts-management/job/123e4567-e89b-12d3-a456-426614174000/status
Content-Type: application/json
{
"status": "IN_PROGRESS"
}
```
## Success Response
- **200 OK**
- Returns the updated job object with the new status.
```
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"status": "IN_PROGRESS",
...
}
```
## Error Responses
- **400 Bad Request**: Missing status field, or parent bodyshop does not have an `external_shop_id`.
- **404 Not Found**: No job found with the given ID.
- **500 Internal Server Error**: Unexpected error.
## Notes
- Only jobs whose parent bodyshop has an `external_shop_id` can be patched via this route.
- Fields other than `status` will be ignored if included in the request body.
- The route is protected by the same middleware as other parts management endpoints.

View File

@@ -1,86 +0,0 @@
# PATCH /integrations/parts-management/provision/:id
Update (patch) select fields for a parts management bodyshop. Only available for shops that have an
`external_shop_id` (i.e., are provisioned for parts management).
## Endpoint
```
PATCH /integrations/parts-management/provision/:id
```
- `:id` is the UUID of the bodyshop to update.
## Request Headers
- `Authorization`: (if required by your integration middleware)
- `Content-Type: application/json`
## Request Body
Send a JSON object with one or more of the following fields to update:
- `shopname` (string)
- `address1` (string)
- `address2` (string, optional)
- `city` (string)
- `state` (string)
- `zip_post` (string)
- `country` (string)
- `email` (string, shop's email, not user email)
- `timezone` (string)
- `phone` (string)
- `logo_img_path` (object, e.g. `{ src, width, height, headerMargin }`)
Any fields not included in the request body will remain unchanged.
## Example Request
```
PATCH /integrations/parts-management/provision/123e4567-e89b-12d3-a456-426614174000
Content-Type: application/json
{
"shopname": "New Shop Name",
"address1": "123 Main St",
"city": "Springfield",
"state": "IL",
"zip_post": "62704",
"country": "USA",
"email": "shop@example.com",
"timezone": "America/Chicago",
"phone": "555-123-4567",
"logo_img_path": {
"src": "https://example.com/logo.png",
"width": "200",
"height": "100",
"headerMargin": 10
}
}
```
## Success Response
- **200 OK**
- Returns the updated shop object with the patched fields.
```
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"shopname": "New Shop Name",
...
}
```
## Error Responses
- **400 Bad Request**: No valid fields provided, or shop does not have an `external_shop_id`.
- **404 Not Found**: No shop found with the given ID.
- **500 Internal Server Error**: Unexpected error.
## Notes
- Only shops with an `external_shop_id` can be patched via this route.
- Fields not listed above will be ignored if included in the request body.
- The route is protected by the same middleware as other parts management endpoints.

View File

@@ -1,10 +0,0 @@
services:
ragmate:
image: ghcr.io/ragmate/ragmate:latest
ports:
- "11434:11434"
env_file:
- .ragmate.env
volumes:
- .:/project
- ./docker_data/ragmate:/apps/cache

View File

@@ -5305,27 +5305,6 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>ro_posting</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>sendmaterialscosting</name>
<definition_loaded>false</definition_loaded>

View File

@@ -14,7 +14,3 @@ VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
VITE_APP_INSTANCE=IMEX
TEST_USERNAME="test@imex.dev"
TEST_PASSWORD="test123"
VITE_PUBLIC_POSTHOG_KEY=phc_xtLmBIu0rjWwExY73Oj5DTH1bGbwq1G1Y8jnlTceien
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_KEY=6228a598e57cd66875cfd41604f1f891

View File

@@ -16,7 +16,3 @@ VITE_APP_COUNTRY=USA
VITE_APP_INSTANCE=ROME
TEST_USERNAME="test@imex.dev"
TEST_PASSWORD="test123"
VITE_PUBLIC_POSTHOG_KEY=phc_xtLmBIu0rjWwExY73Oj5DTH1bGbwq1G1Y8jnlTceien
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_KEY=46b1193a867d4e3131ae4c3a64a3fc78

View File

@@ -13,7 +13,3 @@ VITE_APP_AXIOS_BASE_API_URL=https://api.imex.online/
VITE_APP_REPORTS_SERVER_URL=https://reports.imex.online
VITE_APP_SPLIT_API=et9pjkik6bn67he5evpmpr1agoo7gactphgk
VITE_APP_INSTANCE=IMEX
VITE_PUBLIC_POSTHOG_KEY=phc_xtLmBIu0rjWwExY73Oj5DTH1bGbwq1G1Y8jnlTceien
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_KEY=6228a598e57cd66875cfd41604f1f891

View File

@@ -13,7 +13,3 @@ VITE_APP_AXIOS_BASE_API_URL=https://api.romeonline.io/
VITE_APP_REPORTS_SERVER_URL=https://reports.romeonline.io
VITE_APP_SPLIT_API=et9pjkik6bn67he5evpmpr1agoo7gactphgk
VITE_APP_INSTANCE=ROME
VITE_PUBLIC_POSTHOG_KEY=phc_xtLmBIu0rjWwExY73Oj5DTH1bGbwq1G1Y8jnlTceien
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_KEY=46b1193a867d4e3131ae4c3a64a3fc78

View File

@@ -13,7 +13,3 @@ VITE_APP_REPORTS_SERVER_URL=https://reports.test.imex.online
VITE_APP_IS_TEST=true
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
VITE_APP_INSTANCE=IMEX
VITE_PUBLIC_POSTHOG_KEY=phc_xtLmBIu0rjWwExY73Oj5DTH1bGbwq1G1Y8jnlTceien
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_KEY=6228a598e57cd66875cfd41604f1f891

View File

@@ -13,7 +13,3 @@ VITE_APP_REPORTS_SERVER_URL=https://reports.test.romeonline.io
VITE_APP_IS_TEST=true
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
VITE_APP_INSTANCE=ROME
VITE_PUBLIC_POSTHOG_KEY=phc_xtLmBIu0rjWwExY73Oj5DTH1bGbwq1G1Y8jnlTceien
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_KEY=46b1193a867d4e3131ae4c3a64a3fc78

8
client/.eslintrc Normal file
View File

@@ -0,0 +1,8 @@
{
"extends": [
"react-app"
],
"rules": {
"no-useless-rename": "off"
}
}

View File

@@ -2,9 +2,9 @@ import globals from "globals";
import pluginJs from "@eslint/js";
import pluginReact from "eslint-plugin-react";
/** @type {import("eslint").Linter.Config[]} */
/** @type {import('eslint').Linter.Config[]} */
export default [
{ ignores: ["node_modules/**", "dist/**", "build/**", "dev-dist/**"] },
{
files: ["**/*.{js,mjs,cjs,jsx}"]
},
@@ -12,13 +12,9 @@ export default [
pluginJs.configs.recommended,
{
...pluginReact.configs.flat.recommended,
settings: {
react: { version: "detect" }
},
rules: {
...pluginReact.configs.flat.recommended.rules,
"react/prop-types": 0,
"react/no-children-prop": 0 // Disable react/no-children-prop rule
"react/prop-types": 0
}
},
pluginReact.configs.flat["jsx-runtime"]

6161
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,82 +8,79 @@
"private": true,
"proxy": "http://localhost:4000",
"dependencies": {
"@amplitude/analytics-browser": "^2.30.1",
"@ant-design/pro-layout": "^7.22.6",
"@apollo/client": "^3.13.9",
"@emotion/is-prop-valid": "^1.4.0",
"@ant-design/pro-layout": "^7.22.4",
"@apollo/client": "^3.13.6",
"@emotion/is-prop-valid": "^1.3.1",
"@fingerprintjs/fingerprintjs": "^4.6.1",
"@firebase/analytics": "^0.10.19",
"@firebase/app": "^0.14.6",
"@firebase/auth": "^1.11.1",
"@firebase/firestore": "^4.9.2",
"@firebase/messaging": "^0.12.22",
"@firebase/analytics": "^0.10.16",
"@firebase/app": "^0.13.1",
"@firebase/auth": "^1.10.6",
"@firebase/firestore": "^4.7.17",
"@firebase/messaging": "^0.12.21",
"@jsreport/browser-client": "^3.1.0",
"@reduxjs/toolkit": "^2.10.1",
"@sentry/cli": "^2.58.2",
"@sentry/react": "^9.43.0",
"@sentry/vite-plugin": "^4.6.0",
"@splitsoftware/splitio-react": "^2.6.0",
"@tanem/react-nprogress": "^5.0.56",
"antd": "^5.28.1",
"@reduxjs/toolkit": "^2.8.2",
"@sentry/cli": "^2.47.1",
"@sentry/react": "^9.38.0",
"@sentry/vite-plugin": "^3.5.0",
"@splitsoftware/splitio-react": "^2.3.1",
"@tanem/react-nprogress": "^5.0.53",
"antd": "^5.25.4",
"apollo-link-logger": "^2.0.1",
"apollo-link-sentry": "^4.4.0",
"apollo-link-sentry": "^4.3.0",
"autosize": "^6.0.1",
"axios": "^1.13.2",
"axios": "^1.8.4",
"classnames": "^2.5.1",
"css-box-model": "^1.2.1",
"dayjs": "^1.11.19",
"dayjs-business-days2": "^1.3.1",
"dayjs": "^1.11.13",
"dayjs-business-days2": "^1.3.0",
"dinero.js": "^1.9.1",
"dotenv": "^17.2.3",
"dotenv": "^16.4.7",
"env-cmd": "^10.1.0",
"exifr": "^7.1.3",
"graphql": "^16.12.0",
"i18next": "^25.6.2",
"i18next-browser-languagedetector": "^8.2.0",
"graphql": "^16.11.0",
"i18next": "^24.2.3",
"i18next-browser-languagedetector": "^8.1.0",
"immutability-helper": "^3.1.1",
"libphonenumber-js": "^1.12.26",
"lightningcss": "^1.30.2",
"libphonenumber-js": "^1.12.10",
"logrocket": "^9.0.2",
"markerjs2": "^2.32.7",
"markerjs2": "^2.32.4",
"memoize-one": "^6.0.0",
"normalize-url": "^8.1.0",
"normalize-url": "^8.0.2",
"object-hash": "^3.0.0",
"phone": "^3.1.67",
"posthog-js": "^1.294.0",
"phone": "^3.1.59",
"prop-types": "^15.8.1",
"query-string": "^9.3.1",
"query-string": "^9.2.0",
"raf-schd": "^4.0.3",
"react": "^18.3.1",
"react-big-calendar": "^1.19.4",
"react-big-calendar": "^1.19.2",
"react-color": "^2.19.3",
"react-cookie": "^8.0.1",
"react-dom": "^18.3.1",
"react-drag-listview": "^2.0.0",
"react-grid-gallery": "^1.0.1",
"react-grid-layout": "1.3.4",
"react-i18next": "^15.7.3",
"react-i18next": "^15.5.2",
"react-icons": "^5.5.0",
"react-image-lightbox": "^5.1.4",
"react-markdown": "^10.1.0",
"react-number-format": "^5.4.3",
"react-popopo": "^2.1.9",
"react-product-fruits": "^2.2.62",
"react-product-fruits": "^2.2.61",
"react-redux": "^9.2.0",
"react-resizable": "^3.0.5",
"react-router-dom": "^6.30.0",
"react-sticky": "^6.0.3",
"react-virtuoso": "^4.14.1",
"react-virtuoso": "^4.12.8",
"recharts": "^2.15.2",
"redux": "^5.0.1",
"redux-actions": "^3.0.3",
"redux-persist": "^6.0.0",
"redux-saga": "^1.4.2",
"redux-saga": "^1.3.0",
"redux-state-sync": "^3.1.4",
"reselect": "^5.1.1",
"sass": "^1.94.0",
"sass": "^1.89.1",
"socket.io-client": "^4.8.1",
"styled-components": "^6.1.19",
"styled-components": "^6.1.18",
"subscriptions-transport-ws": "^0.11.0",
"use-memo-one": "^1.1.3",
"vite-plugin-ejs": "^1.7.0",
@@ -110,9 +107,7 @@
"test:e2e:rome": "playwright test --config playwright.rome.config.js",
"test:e2e:imex:headed": "playwright test --config playwright.config.js --headed",
"test:e2e:rome:headed": "playwright test --config playwright.rome.config.js --headed",
"test:e2e:report": "playwright show-report",
"lint": "eslint .",
"lint:fix": "eslint . --fix"
"test:e2e:report": "playwright show-report"
},
"browserslist": {
"production": [
@@ -133,39 +128,40 @@
"@rollup/rollup-linux-x64-gnu": "4.6.1"
},
"devDependencies": {
"@ant-design/icons": "^6.1.0",
"@ant-design/icons": "^6.0.0",
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/preset-react": "^7.28.5",
"@dotenvx/dotenvx": "^1.51.1",
"@babel/preset-react": "^7.27.1",
"@dotenvx/dotenvx": "^1.47.5",
"@emotion/babel-plugin": "^11.13.5",
"@emotion/react": "^11.14.0",
"@eslint/js": "^9.39.1",
"@playwright/test": "^1.56.1",
"@sentry/webpack-plugin": "^4.6.0",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@eslint/js": "^9.31.0",
"@playwright/test": "^1.54.1",
"@sentry/webpack-plugin": "^3.5.0",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@vitejs/plugin-react": "^4.6.0",
"browserslist": "^4.28.0",
"@vitejs/plugin-react": "^4.5.1",
"browserslist": "^4.25.0",
"browserslist-to-esbuild": "^2.1.1",
"chalk": "^5.6.2",
"eslint": "^9.39.1",
"chalk": "^5.4.1",
"eslint": "^8.57.1",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-react": "^7.37.5",
"globals": "^15.15.0",
"jsdom": "^26.0.0",
"memfs": "^4.51.0",
"memfs": "^4.17.2",
"os-browserify": "^0.3.0",
"playwright": "^1.56.1",
"playwright": "^1.54.1",
"react-error-overlay": "^6.1.0",
"redux-logger": "^3.0.6",
"source-map-explorer": "^2.5.3",
"vite": "^7.2.2",
"vite-plugin-babel": "^1.3.2",
"vite": "^6.3.5",
"vite-plugin-babel": "^1.3.1",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-node-polyfills": "^0.24.0",
"vite-plugin-pwa": "^1.1.0",
"vite-plugin-node-polyfills": "^0.23.0",
"vite-plugin-pwa": "^1.0.0",
"vite-plugin-style-import": "^2.0.0",
"vitest": "^3.2.4",
"vitest": "^3.2.3",
"workbox-window": "^7.3.0"
}
}

View File

@@ -20,7 +20,6 @@ export default defineConfig({
command: "npm run start:imex",
ignoreHTTPSErrors: true,
url: "https://localhost:3000/health", // Health check endpoint will tell us when the server is ready
// eslint-disable-next-line no-undef
reuseExistingServer: !process.env.CI // Reuse server locally, not in CI
}
});

View File

@@ -20,7 +20,6 @@ export default defineConfig({
command: "npm run start:rome",
ignoreHTTPSErrors: true,
url: "https://localhost:3000/health", // Health check endpoint will tell us when the server is ready
// eslint-disable-next-line no-undef
reuseExistingServer: !process.env.CI // Reuse server locally, not in CI
}
});

View File

@@ -1,7 +1,5 @@
// Scripts for firebase and firebase messaging
// eslint-disable-next-line no-undef
importScripts("https://www.gstatic.com/firebasejs/10.14.1/firebase-app-compat.js");
// eslint-disable-next-line no-undef
importScripts("https://www.gstatic.com/firebasejs/10.14.1/firebase-messaging-compat.js");
// Initialize the Firebase app in the service worker by passing the generated config
@@ -44,16 +42,13 @@ switch (this.location.hostname) {
};
}
// eslint-disable-next-line no-undef
firebase.initializeApp(firebaseConfig);
// Retrieve firebase messaging
// eslint-disable-next-line no-undef
const messaging = firebase.messaging();
messaging.onBackgroundMessage(function (payload) {
// Customize notification here
console.log("[firebase-messaging-sw.js] Received background message ", payload);
// eslint-disable-next-line no-undef
self.registration.showNotification(notificationTitle, notificationOptions);
});

View File

@@ -1,106 +1,42 @@
import { ApolloProvider } from "@apollo/client";
import * as Sentry from "@sentry/react";
import { SplitFactoryProvider, useSplitClient } from "@splitsoftware/splitio-react";
import { ConfigProvider } from "antd";
import enLocale from "antd/es/locale/en_US";
import { useEffect, useMemo } from "react";
import { CookiesProvider } from "react-cookie";
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { connect, useSelector } from "react-redux";
import { createStructuredSelector } from "reselect";
import { useSelector } from "react-redux";
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
import { setDarkMode } from "../redux/application/application.actions";
import { selectDarkMode } from "../redux/application/application.selectors";
import { selectCurrentUser } from "../redux/user/user.selectors.js";
import { signOutStart } from "../redux/user/user.actions";
import client from "../utils/GraphQLClient";
import App from "./App";
import getTheme from "./themeProvider";
import * as Sentry from "@sentry/react";
import themeProvider from "./themeProvider";
import { CookiesProvider } from "react-cookie";
// Base Split configuration
const config = {
core: {
authorizationKey: import.meta.env.VITE_APP_SPLIT_API,
key: "anon"
key: "anon" // Default key, overridden dynamically by SplitClientProvider
}
};
// Custom provider to manage the Split client key based on imexshopid from Redux
function SplitClientProvider({ children }) {
const imexshopid = useSelector((state) => state.user.imexshopid);
const splitClient = useSplitClient({ key: imexshopid || "anon" });
const imexshopid = useSelector((state) => state.user.imexshopid); // Access imexshopid from Redux store
const splitClient = useSplitClient({ key: imexshopid || "anon" }); // Use imexshopid or fallback to "anon"
useEffect(() => {
if (splitClient && imexshopid) {
// Log readiness for debugging; no need for ready() since isReady is available
console.log(`Split client initialized with key: ${imexshopid}, isReady: ${splitClient.isReady}`);
}
}, [splitClient, imexshopid]);
return children;
}
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser
});
const mapDispatchToProps = (dispatch) => ({
setDarkMode: (isDarkMode) => dispatch(setDarkMode(isDarkMode)),
signOutStart: () => dispatch(signOutStart())
});
function AppContainer({ currentUser, setDarkMode, signOutStart }) {
function AppContainer() {
const { t } = useTranslation();
const isDarkMode = useSelector(selectDarkMode);
const theme = useMemo(() => getTheme(isDarkMode), [isDarkMode]);
// Global seamless logout listener with redirect to /signin
useEffect(() => {
const handleSeamlessLogout = (event) => {
if (event.data?.type !== "seamlessLogoutRequest") return;
const requestOrigin = event.origin;
if (currentUser?.authorized !== true) {
window.parent.postMessage(
{ type: "seamlessLogoutResponse", status: "already_logged_out" },
requestOrigin || "*"
);
return;
}
signOutStart();
window.parent.postMessage({ type: "seamlessLogoutResponse", status: "logged_out" }, requestOrigin || "*");
};
window.addEventListener("message", handleSeamlessLogout);
return () => {
window.removeEventListener("message", handleSeamlessLogout);
};
}, [signOutStart, currentUser]);
// Update data-theme attribute
useEffect(() => {
document.documentElement.setAttribute("data-theme", isDarkMode ? "dark" : "light");
return () => document.documentElement.removeAttribute("data-theme");
}, [isDarkMode]);
// Sync darkMode with localStorage
useEffect(() => {
if (currentUser?.uid) {
const savedMode = localStorage.getItem(`dark-mode-${currentUser.uid}`);
if (savedMode !== null) {
setDarkMode(JSON.parse(savedMode));
} else {
setDarkMode(false);
}
} else {
setDarkMode(false);
}
}, [currentUser?.uid, setDarkMode]);
// Persist darkMode
useEffect(() => {
if (currentUser?.uid) {
localStorage.setItem(`dark-mode-${currentUser.uid}`, JSON.stringify(isDarkMode));
}
}, [isDarkMode, currentUser?.uid]);
return (
<CookiesProvider>
@@ -108,9 +44,10 @@ function AppContainer({ currentUser, setDarkMode, signOutStart }) {
<ConfigProvider
input={{ autoComplete: "new-password" }}
locale={enLocale}
theme={theme}
theme={themeProvider}
form={{
validateMessages: {
// eslint-disable-next-line no-template-curly-in-string
required: t("general.validation.required", { label: "${label}" })
}
}}
@@ -127,4 +64,4 @@ function AppContainer({ currentUser, setDarkMode, signOutStart }) {
);
}
export default Sentry.withProfiler(connect(mapStateToProps, mapDispatchToProps)(AppContainer));
export default Sentry.withProfiler(AppContainer);

View File

@@ -7,14 +7,13 @@ import { connect } from "react-redux";
import { Route, Routes, useNavigate } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import DocumentEditorContainer from "../components/document-editor/document-editor.container";
import ErrorBoundary from "../components/error-boundary/error-boundary.component";
import ErrorBoundary from "../components/error-boundary/error-boundary.component"; // Component Imports
import LoadingSpinner from "../components/loading-spinner/loading-spinner.component";
import DisclaimerPage from "../pages/disclaimer/disclaimer.page";
import LandingPage from "../pages/landing/landing.page";
import TechPageContainer from "../pages/tech/tech.page.container";
import SimplifiedPartsPageContainer from "../pages/simplified-parts/simplified-parts.page.container.jsx";
import { setIsPartsEntry, setOnline } from "../redux/application/application.actions";
import { selectIsPartsEntry, selectOnline } from "../redux/application/application.selectors";
import { setOnline } from "../redux/application/application.actions";
import { selectOnline } from "../redux/application/application.selectors";
import { checkUserSession } from "../redux/user/user.actions";
import { selectBodyshop, selectCurrentEula, selectCurrentUser } from "../redux/user/user.selectors";
import PrivateRoute from "../components/PrivateRoute";
@@ -24,37 +23,26 @@ import InstanceRenderMgr from "../utils/instanceRenderMgr";
import ProductFruitsWrapper from "./ProductFruitsWrapper.jsx";
import { NotificationProvider } from "../contexts/Notifications/notificationContext.jsx";
import SocketProvider from "../contexts/SocketIO/socketProvider.jsx";
import SoundWrapper from "./SoundWrapper.jsx";
const ResetPassword = lazy(() => import("../pages/reset-password/reset-password.component"));
const ManagePage = lazy(() => import("../pages/manage/manage.page.container"));
const SignInPage = lazy(() => import("../pages/sign-in/sign-in.page"));
const CsiPage = lazy(() => import("../pages/csi/csi.container.page"));
const MobilePaymentContainer = lazy(() => import("../pages/mobile-payment/mobile-payment.container"));
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
online: selectOnline,
bodyshop: selectBodyshop,
currentEula: selectCurrentEula,
isPartsEntry: selectIsPartsEntry
currentEula: selectCurrentEula
});
const mapDispatchToProps = (dispatch) => ({
checkUserSession: () => dispatch(checkUserSession()),
setOnline: (isOnline) => dispatch(setOnline(isOnline)),
setIsPartsEntry: (isParts) => dispatch(setIsPartsEntry(isParts))
setOnline: (isOnline) => dispatch(setOnline(isOnline))
});
export function App({
bodyshop,
checkUserSession,
currentUser,
online,
setOnline,
setIsPartsEntry,
currentEula,
isPartsEntry
}) {
export function App({ bodyshop, checkUserSession, currentUser, online, setOnline, currentEula }) {
const client = useSplitClient().client;
const [listenersAdded, setListenersAdded] = useState(false);
const { t } = useTranslation();
@@ -64,14 +52,12 @@ export function App({
if (!navigator.onLine) {
setOnline(false);
}
checkUserSession();
}, [checkUserSession, setOnline]);
useEffect(() => {
const pathname = window.location.pathname;
const isParts = pathname === "/parts" || pathname.startsWith("/parts/");
setIsPartsEntry(isParts);
}, [setIsPartsEntry]);
//const b = Grid.useBreakpoint();
// console.log("Breakpoints:", b);
// Associate event listeners, memoize to prevent multiple listeners being added
useEffect(() => {
@@ -158,91 +144,86 @@ export function App({
currentUser={currentUser}
bodyshop={bodyshop}
workspaceCode={bodyshop?.tours_enabled ? "9BkbEseqNqxw8jUH" : ""}
isPartsEntry={isPartsEntry}
/>
<NotificationProvider>
<SoundWrapper bodyshop={bodyshop}>
<Routes>
<Route
path="*"
element={
<ErrorBoundary>
<LandingPage />
</ErrorBoundary>
}
/>
<Route
path="/signin"
element={
<ErrorBoundary>
<SignInPage />
</ErrorBoundary>
}
/>
<Route
path="/resetpassword"
element={
<ErrorBoundary>
<ResetPassword />
</ErrorBoundary>
}
/>
<Route
path="/csi/:surveyId"
element={
<ErrorBoundary>
<CsiPage />
</ErrorBoundary>
}
/>
<Route
path="/disclaimer"
element={
<ErrorBoundary>
<DisclaimerPage />
</ErrorBoundary>
}
/>
<Route
path="/manage/*"
element={
<ErrorBoundary>
<SocketProvider bodyshop={bodyshop} navigate={navigate} currentUser={currentUser}>
<PrivateRoute isAuthorized={currentUser.authorized} />
</SocketProvider>
</ErrorBoundary>
}
>
<Route path="*" element={<ManagePage />} />
</Route>
<Route
path="/tech/*"
element={
<ErrorBoundary>
<SocketProvider bodyshop={bodyshop} navigate={navigate} currentUser={currentUser}>
<PrivateRoute isAuthorized={currentUser.authorized} />
</SocketProvider>
</ErrorBoundary>
}
>
<Route path="*" element={<TechPageContainer />} />
</Route>
<Route
path="/parts/*"
element={
<ErrorBoundary>
<Routes>
<Route
path="*"
element={
<ErrorBoundary>
<LandingPage />
</ErrorBoundary>
}
/>
<Route
path="/signin"
element={
<ErrorBoundary>
<SignInPage />
</ErrorBoundary>
}
/>
<Route
path="/resetpassword"
element={
<ErrorBoundary>
<ResetPassword />
</ErrorBoundary>
}
/>
<Route
path="/csi/:surveyId"
element={
<ErrorBoundary>
<CsiPage />
</ErrorBoundary>
}
/>
<Route
path="/disclaimer"
element={
<ErrorBoundary>
<DisclaimerPage />
</ErrorBoundary>
}
/>
<Route
path="/mp/:paymentIs"
element={
<ErrorBoundary>
<MobilePaymentContainer />
</ErrorBoundary>
}
/>
<Route
path="/manage/*"
element={
<ErrorBoundary>
<SocketProvider bodyshop={bodyshop} navigate={navigate} currentUser={currentUser}>
<PrivateRoute isAuthorized={currentUser.authorized} />
</ErrorBoundary>
}
>
<Route path="*" element={<SimplifiedPartsPageContainer />} />
</Route>
<Route path="/edit/*" element={<PrivateRoute isAuthorized={currentUser.authorized} />}>
<Route path="*" element={<DocumentEditorContainer />} />
</Route>
</Routes>
</SoundWrapper>
</SocketProvider>
</ErrorBoundary>
}
>
<Route path="*" element={<ManagePage />} />
</Route>
<Route
path="/tech/*"
element={
<ErrorBoundary>
<SocketProvider bodyshop={bodyshop} navigate={navigate} currentUser={currentUser}>
<PrivateRoute isAuthorized={currentUser.authorized} />
</SocketProvider>
</ErrorBoundary>
}
>
<Route path="*" element={<TechPageContainer />} />
</Route>
<Route path="/edit/*" element={<PrivateRoute isAuthorized={currentUser.authorized} />}>
<Route path="*" element={<DocumentEditorContainer />} />
</Route>
</Routes>
</NotificationProvider>
</Suspense>
);

View File

@@ -1,225 +1,15 @@
@use "react-big-calendar/lib/sass/styles" as rbc;
:root {
--table-stripe-bg: #f4f4f4; /* Light mode table stripe */
--menu-divider-color: #74695c; /* Light mode menu divider */
--menu-submenu-text: rgba(255, 255, 255, 0.65); /* Light mode submenu text */
--kanban-column-bg: #ddd; /* Light mode kanban column */
--alert-color: blue; /* Light mode alert */
--completion-soon-color: rgba(255, 140, 0, 0.8); /* Light mode completion soon */
--completion-past-color: rgba(255, 0, 0, 0.8); /* Light mode completion past */
--job-line-manual-color: tomato; /* Light mode job line manual */
--muted-button-color: lightgray; /* Light mode muted button */
--muted-button-hover-color: darkgrey; /* Light mode muted button hover */
--table-border-color: #ddd; /* Light mode table border */
--table-hover-bg: #f5f5f5; /* Light mode table hover */
--popover-bg: #fff; /* Light mode popover background */
--error-text: red; /* Light mode error message */
--no-jobs-text: #888; /* Light mode no jobs message */
--message-yours-bg: #eee; /* Light mode yours message background */
--message-mine-bg-start: #00d0ea; /* Light mode mine message gradient start */
--message-mine-bg-end: #0085d1; /* Light mode mine message gradient end */
--message-mine-text: white; /* Light mode mine message text */
--message-mine-tail-bg: white; /* Light mode mine/yours message tail */
--system-message-bg: #f5f5f5; /* Light mode system message background */
--system-message-text: #555; /* Light mode system message text */
--system-label-text: #888; /* Light mode system label/date text */
--message-icon-color: whitesmoke; /* Light mode message icon */
--eula-card-bg: lightgray; /* Light mode eula card background */
--notification-bg: #fff; /* Light mode notification background */
--notification-text: rgba(0, 0, 0, 0.85); /* Light mode notification text */
--notification-border: #d9d9d9; /* Light mode notification border */
--notification-header-bg: #fafafa; /* Light mode notification header background */
--notification-header-border: #f0f0f0; /* Light mode notification header border */
--notification-header-text: rgba(0, 0, 0, 0.85); /* Light mode notification header text */
--notification-toggle-icon: #1677ff; /* Light mode notification toggle icon */
--notification-switch-bg: #1677ff; /* Light mode notification switch background */
--notification-btn-link: #1677ff; /* Light mode notification link button */
--notification-btn-link-hover: #69b1ff; /* Light mode notification link button hover */
--notification-btn-link-disabled: rgba(0, 0, 0, 0.25); /* Light mode notification link button disabled */
--notification-btn-link-active: #0958d9; /* Light mode notification link button active */
--notification-read-bg: #fff; /* Light mode notification read background */
--notification-read-text: rgba(0, 0, 0, 0.65); /* Light mode notification read text */
--notification-unread-bg: #f5f5f5; /* Light mode notification unread background */
--notification-unread-text: rgba(0, 0, 0, 0.85); /* Light mode notification unread text */
--notification-item-hover-bg: #fafafa; /* Light mode notification item hover background */
--notification-ro-number: #1677ff; /* Light mode notification RO number */
--notification-relative-time: rgba(0, 0, 0, 0.45); /* Light mode notification relative time */
--alert-bg: #fff1f0; /* Light mode alert background */
--alert-text: rgba(0, 0, 0, 0.85); /* Light mode alert text */
--alert-border: #ffa39e; /* Light mode alert border */
--alert-message: #ff4d4f; /* Light mode alert message */
--share-badge-bg: #cccccc; /* Light mode share badge background */
--column-header-bg: #d0d0d0; /* Light mode column header background */
--footer-bg: #d0d0d0; /* Light mode footer background */
--tech-icon-color: orangered; /* Light mode tech icon color */
--clone-border-color: #1890ff; /* Light mode clone border color */
--event-arrived-bg: rgba(4, 141, 4, 0.4); /* Light mode arrived event background */
--event-block-bg: tomato; /* Light mode block event background */
--event-selected-bg: slategrey; /* Light mode selected event background */
--task-bg: #fff; /* Light mode task center background */
--task-text: rgba(0, 0, 0, 0.85); /* Light mode task text */
--task-border: #d9d9d9; /* Light mode task border */
--task-header-bg: #fafafa; /* Light mode task header background */
--task-header-border: #f0f0f0; /* Light mode task header border */
--task-section-bg: #f5f5f5; /* Light mode task section background */
--task-section-border: #e8e8e8; /* Light mode task section border */
--task-row-hover-bg: #f5f5f5; /* Light mode task row hover background */
--task-row-border: #f0f0f0; /* Light mode task row border */
--task-ro-number: #1677ff; /* Light mode task RO number */
--task-due-text: rgba(0, 0, 0, 0.45); /* Light mode task due text */
--task-button-bg: #1677ff; /* Light mode task button background */
--task-button-hover-bg: #4096ff; /* Light mode task button hover background */
--task-button-disabled-bg: #d9d9d9; /* Light mode task button disabled background */
--task-button-text: white; /* Light mode task button text */
--task-message-text: rgba(0, 0, 0, 0.45); /* Light mode task message text */
--mask-bg: rgba(0, 0, 0, 0.05); /* Light mode mask background */
--board-text-color: #393939; /* Light mode board text color */
--section-bg: #e3e3e3; /* Light mode section background */
--detail-text-color: #4d4d4d; /* Light mode detail text color */
--card-selected-bg: rgba(128, 128, 128, 0.2); /* Light mode selected card background */
--card-stripe-even-bg: #f0f2f5; /* Light mode even card background */
--card-stripe-odd-bg: #ffffff; /* Light mode odd card background */
--bar-border-color: #f0f2f5; /* Light mode bar border and background */
--tag-wrapper-bg: #f0f2f5; /* Light mode tag wrapper background */
--tag-wrapper-text: #000; /* Light mode tag wrapper text */
--preview-bg: lightgray; /* Light mode preview background */
--preview-border-color: #2196F3; /* Light mode preview border color */
--event-bg-fallback: #c4c4c4; /* Light mode event background fallback */
--card-bg-fallback: #ffffff; /* Light mode card background fallback */
--card-text-fallback: black; /* Light mode card text fallback */
--table-row-even-bg: rgb(236, 236, 236); /* Light mode table row even background */
--status-row-bg-fallback: #ffffff; /* Light mode status row fallback background */
--reset-link-color: #0000ff; /* Light mode reset link color */
--error-header-text: tomato; /* Light mode error header text */
--tooltip-bg: white; /* Light mode tooltip background */
--tooltip-border: gray; /* Light mode tooltip border */
--tooltip-text-fallback: black; /* Light mode tooltip text fallback */
--teams-button-bg: #6264A7; /* Light mode Teams button background */
--teams-button-border: #6264A7; /* Light mode Teams button border */
--teams-button-text: #FFFFFF; /* Light mode Teams button text and icon */
--content-bg: #fff; /* Light mode content background */
--legend-bg-fallback: #ffffff; /* Light mode legend background fallback */
--tech-content-bg: #fff; /* Light mode tech content background */
--today-bg: #ffffff; /* Light mode today background */
--today-text: #000000; /* Light mode today text */
--off-range-bg: #f8f8f8; /* Light mode off-range background */
}
[data-theme="dark"] {
--table-stripe-bg: #2a2a2a; /* Dark mode table stripe */
--menu-divider-color: #5c5c5c; /* Dark mode menu divider */
--menu-submenu-text: rgba(255, 255, 255, 0.85); /* Dark mode submenu text */
--kanban-column-bg: #333333; /* Dark mode kanban column */
--alert-color: #4da8ff; /* Dark mode alert */
--completion-soon-color: #ff8c1a; /* Dark mode completion soon */
--completion-past-color: #ff4d4f; /* Dark mode completion past */
--job-line-manual-color: #ff6347; /* Dark mode job line manual */
--muted-button-color: #666666; /* Dark mode muted button */
--muted-button-hover-color: #999999; /* Dark mode muted button hover */
--table-border-color: #5c5c5c; /* Dark mode table border */
--table-hover-bg: #2a2a2a; /* Dark mode table hover */
--popover-bg: #2a2a2a; /* Dark mode popover background */
--error-text: #ff4d4f; /* Dark mode error message */
--no-jobs-text: #999999; /* Dark mode no jobs message */
--message-yours-bg: #2a2a2a; /* Dark mode yours message background */
--message-mine-bg-start: #4da8ff; /* Dark mode mine message gradient start */
--message-mine-bg-end: #326ade; /* Dark mode mine message gradient end */
--message-mine-text: #ffffff; /* Dark mode mine message text */
--message-mine-tail-bg: #1f1f1f; /* Dark mode mine/yours message tail */
--system-message-bg: #333333; /* Dark mode system message background */
--system-message-text: #cccccc; /* Dark mode system message text */
--system-label-text: #999999; /* Dark mode system label/date text */
--message-icon-color: #cccccc; /* Dark mode message icon */
--eula-card-bg: #2a2a2a; /* Dark mode eula card background */
--notification-bg: #2a2a2a; /* Dark mode notification background */
--notification-text: rgba(255, 255, 255, 0.85); /* Dark mode notification text */
--notification-border: #5c5c5c; /* Dark mode notification border */
--notification-header-bg: #333333; /* Dark mode notification header background */
--notification-header-border: #444444; /* Dark mode notification header border */
--notification-header-text: rgba(255, 255, 255, 0.85); /* Dark mode notification header text */
--notification-toggle-icon: #4da8ff; /* Dark mode notification toggle icon */
--notification-switch-bg: #4da8ff; /* Dark mode notification switch background */
--notification-btn-link: #4da8ff; /* Dark mode notification link button */
--notification-btn-link-hover: #80c1ff; /* Dark mode notification link button hover */
--notification-btn-link-disabled: rgba(255, 255, 255, 0.25); /* Dark mode notification link button disabled */
--notification-btn-link-active: #2681ff; /* Dark mode notification link button active */
--notification-read-bg: #2a2a2a; /* Dark mode notification read background */
--notification-read-text: rgba(255, 255, 255, 0.65); /* Dark mode notification read text */
--notification-unread-bg: #333333; /* Dark mode notification unread background */
--notification-unread-text: rgba(255, 255, 255, 0.85); /* Dark mode notification unread text */
--notification-item-hover-bg: #3a3a3a; /* Dark mode notification item hover background */
--notification-ro-number: #4da8ff; /* Dark mode notification RO number */
--notification-relative-time: rgba(255, 255, 255, 0.45); /* Dark mode notification relative time */
--alert-bg: #3a1a1a; /* Dark mode alert background */
--alert-text: rgba(255, 255, 255, 0.85); /* Dark mode alert text */
--alert-border: #ff6666; /* Dark mode alert border */
--alert-message: #ff6666; /* Dark mode alert message */
--share-badge-bg: #666666; /* Dark mode share badge background */
--column-header-bg: #333333; /* Dark mode column header background */
--footer-bg: #333333; /* Dark mode footer background */
--tech-icon-color: #ff4500; /* Dark mode tech icon color */
--clone-border-color: #4da8ff; /* Dark mode clone border color */
--event-arrived-bg: rgba(4, 141, 4, 0.6); /* Dark mode arrived event background */
--event-block-bg: tomato; /* Dark mode block event background */
--event-selected-bg: #4a5e6e; /* Dark mode selected event background */
--task-bg: #2a2a2a; /* Dark mode task center background */
--task-text: rgba(255, 255, 255, 0.85); /* Dark mode task text */
--task-border: #5c5c5c; /* Dark mode task border */
--task-header-bg: #333333; /* Dark mode task header background */
--task-header-border: #444444; /* Dark mode task header border */
--task-section-bg: #333333; /* Dark mode task section background */
--task-section-border: #444444; /* Dark mode task section border */
--task-row-hover-bg: #3a3a3a; /* Dark mode task row hover background */
--task-row-border: #444444; /* Dark mode task row border */
--task-ro-number: #4da8ff; /* Dark mode task RO number */
--task-due-text: rgba(255, 255, 255, 0.45); /* Dark mode task due text */
--task-button-bg: #4da8ff; /* Dark mode task button background */
--task-button-hover-bg: #80c1ff; /* Dark mode task button hover background */
--task-button-disabled-bg: #666666; /* Dark mode task button disabled background */
--task-button-text: #ffffff; /* Dark mode task button text */
--task-message-text: rgba(255, 255, 255, 0.45); /* Dark mode task message text */
--mask-bg: rgba(255, 255, 255, 0.05); /* Dark mode mask background */
--board-text-color: #cccccc; /* Dark mode board text color */
--section-bg: #333333; /* Dark mode section background */
--detail-text-color: #bbbbbb; /* Dark mode detail text color */
--card-selected-bg: rgba(255, 255, 255, 0.1); /* Dark mode selected card background */
--card-stripe-even-bg: #2a2a2a; /* Dark mode even card background */
--card-stripe-odd-bg: #1f1f1f; /* Dark mode odd card background */
--bar-border-color: #2a2a2a; /* Dark mode bar border and background */
--tag-wrapper-bg: #2a2a2a; /* Dark mode tag wrapper background */
--tag-wrapper-text: #cccccc; /* Dark mode tag wrapper text */
--preview-bg: #2a2a2a; /* Dark mode preview background */
--preview-border-color: #4da8ff; /* Dark mode preview border color */
--event-bg-fallback: #262626; /* Dark mode event background fallback */
--card-bg-fallback: #2a2a2a; /* Dark mode card background fallback */
--card-text-fallback: #cccccc; /* Dark mode card text fallback */
--table-row-even-bg: #2a2a2a; /* Dark mode table row even background */
--status-row-bg-fallback: #1f1f1f; /* Dark mode status row fallback background */
--reset-link-color: #4da8ff; /* Dark mode reset link color */
--error-header-text: #ff6347; /* Dark mode error header text */
--tooltip-bg: #2a2a2a; /* Dark mode tooltip background */
--tooltip-border: #5c5c5c; /* Dark mode tooltip border */
--tooltip-text-fallback: #cccccc; /* Dark mode tooltip text fallback */
--teams-button-bg: #7b7dc4; /* Dark mode Teams button background */
--teams-button-border: #7b7dc4; /* Dark mode Teams button border */
--teams-button-text: #ffffff; /* Dark mode Teams button text and icon */
--content-bg: #2a2a2a; /* Dark mode content background */
--legend-bg-fallback: #2a2a2a; /* Dark mode legend background fallback */
--tech-content-bg: #2a2a2a; /* Dark mode tech content background */
--today-bg: #4a5e6e; /* Dark mode today background */
--today-text: #ffffff; /* Dark mode today text */
--off-range-bg: #333333; /* Dark mode off-range background */
--svg-background: #FFF; /* Dark mode SVG background */
}
//Global Styles.
@import "react-big-calendar/lib/sass/styles";
.ant-menu-item-divider {
border-bottom: 1px solid var(--menu-divider-color) !important;
border-bottom: 1px solid #74695c !important;
}
// Note: Monitor this in dark mode to ensure text visibility
// TODO: This was added because the newest release of ant was making the text color and the background color the same on a selected header
// Tried all available tokens (https://ant.design/components/menu?locale=en-US) and even reverted all our custom styles, to no avail
// This should be kept an eye on, especially if implementing DARK MODE
.ant-menu-submenu-title {
color: var(--menu-submenu-text) !important;
color: rgba(255, 255, 255, 0.65) !important;
}
.imex-table-header {
@@ -256,7 +46,7 @@
}
.ellipses {
display: inline-block;
display: inline-block; /* for em, a, span, etc (inline by default) */
text-overflow: ellipsis;
width: calc(95%);
overflow: hidden;
@@ -270,23 +60,22 @@
}
}
// Scrollbar styles (uncomment if needed, updated for dark mode)
// ::-webkit-scrollbar-track {
// -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
// border-radius: 0.2rem;
// background-color: var(--table-stripe-bg);
// background-color: #f5f5f5;
// }
// ::-webkit-scrollbar {
// width: 0.25rem;
// max-height: 0.25rem;
// background-color: var(--table-stripe-bg);
// background-color: #f5f5f5;
// }
// ::-webkit-scrollbar-thumb {
// border-radius: 0.2rem;
// -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
// background-color: var(--alert-color);
// background-color: #188fff;
// }
.ant-input-number-input,
@@ -299,27 +88,28 @@
.production-alert {
animation: alertBlinker 1s linear infinite;
color: var(--alert-color);
color: blue;
}
@keyframes alertBlinker {
50% {
color: var(--completion-past-color);
color: red;
opacity: 100;
//opacity: 0;
}
}
.blue {
color: var(--alert-color);
color: blue;
}
.production-completion-soon {
color: var(--completion-soon-color);
color: rgba(255, 140, 0, 0.8);
font-weight: bold;
}
.production-completion-past {
color: var(--completion-past-color);
color: rgba(255, 0, 0, 0.8);
font-weight: bold;
}
@@ -349,7 +139,7 @@
}
.react-kanban-column {
background-color: var(--kanban-column-bg) !important;
background-color: #ddd !important;
}
.production-list-table {
@@ -361,18 +151,18 @@
.ReactGridGallery_tile-icon-bar {
div {
svg {
fill: var(--alert-color);
fill: #1890ff;
}
}
}
.job-line-manual {
color: var(--job-line-manual-color);
color: tomato;
font-style: italic;
}
.ant-table-tbody > tr.ant-table-row:nth-child(2n) > td {
background-color: var(--table-stripe-bg);
background-color: #f4f4f4;
}
.rowWithColor > td {
@@ -380,15 +170,15 @@
}
.muted-button {
color: var(--muted-button-color);
color: lightgray;
border: none;
background: none;
cursor: pointer;
font-size: 16px;
font-size: 16px; /* Adjust as needed */
}
.muted-button:hover {
color: var(--muted-button-hover-color);
color: darkgrey;
}
.notification-alert-unordered-list {
@@ -400,49 +190,3 @@
margin-right: 0;
}
}
.content-container {
padding: 1rem;
}
// Override react-big-calendar styles for dark mode only
[data-theme="dark"] {
.car-svg {
background-color: var(--svg-background);
}
.rbc-today {
background-color: var(--today-bg);
color: var(--today-text);
}
.rbc-off-range {
background-color: var(--off-range-bg);
}
.rbc-day-bg.rbc-today {
background-color: var(--today-bg);
}
}
.dms-equal-height-col {
display: flex; // make the Col a flex container
}
/* If the direct child is an AntD Card, make it fill the column */
.dms-equal-height-col > .ant-card {
flex: 1 1 auto;
display: flex;
flex-direction: column;
}
/* Optional: if you want the card body to fill vertically too */
.dms-equal-height-col > .ant-card .ant-card-body {
flex: 1;
display: flex;
flex-direction: column;
}
//.rbc-time-header-gutter {
// padding: 0;
//}

View File

@@ -1,9 +1,9 @@
import { memo } from "react";
import React from "react";
import PropTypes from "prop-types";
import { ProductFruits } from "react-product-fruits";
import dayjs from "dayjs";
const ProductFruitsWrapper = memo(({ currentUser, bodyshop, workspaceCode, isPartsEntry }) => {
const ProductFruitsWrapper = React.memo(({ currentUser, bodyshop, workspaceCode }) => {
const featureProps = bodyshop?.features
? Object.entries(bodyshop.features).reduce((acc, [key, value]) => {
acc[key] = value === true || (typeof value === "string" && dayjs(value).isAfter(dayjs()));
@@ -12,7 +12,6 @@ const ProductFruitsWrapper = memo(({ currentUser, bodyshop, workspaceCode, isPar
: {};
return (
!isPartsEntry &&
workspaceCode &&
currentUser?.authorized === true &&
currentUser?.email && (
@@ -31,8 +30,6 @@ const ProductFruitsWrapper = memo(({ currentUser, bodyshop, workspaceCode, isPar
);
});
ProductFruitsWrapper.displayName = "ProductFruitsWrapper";
export default ProductFruitsWrapper;
ProductFruitsWrapper.propTypes = {

View File

@@ -1,43 +0,0 @@
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useNotification } from "../contexts/Notifications/notificationContext.jsx";
import { initNewMessageSound, unlockAudio } from "./../utils/soundManager";
import { initSingleTabAudioLeader } from "../utils/singleTabAudioLeader";
export default function SoundWrapper({ children, bodyshop }) {
const { t } = useTranslation();
const notification = useNotification();
useEffect(() => {
if (!bodyshop?.id) return;
// 1) Init single-tab leader election (only one tab should play sounds), scoped by bodyshopId
const cleanupLeader = initSingleTabAudioLeader(bodyshop.id);
// 2) Initialize base audio
initNewMessageSound("https://images.imex.online/app/messageTone.wav", 0.7);
// 3) Show a one-time prompt when autoplay blocks first play
const onNeedsUnlock = () => {
notification.info({
description: t("audio.manager.description"),
duration: 3
});
};
window.addEventListener("sound-needs-unlock", onNeedsUnlock);
// 4) Proactively unlock on first gesture (once per session)
const gesture = () => unlockAudio(bodyshop.id);
window.addEventListener("click", gesture, { once: true, passive: true });
window.addEventListener("touchstart", gesture, { once: true, passive: true });
window.addEventListener("keydown", gesture, { once: true });
return () => {
cleanupLeader();
window.removeEventListener("sound-needs-unlock", onNeedsUnlock);
// gesture listeners were added with {once:true}
};
}, [notification, t, bodyshop?.id]); // include bodyshop.id so this runs when org changes
return <>{children}</>;
}

View File

@@ -4,42 +4,36 @@ import InstanceRenderMgr from "../utils/instanceRenderMgr";
const { defaultAlgorithm, darkAlgorithm } = theme;
let isDarkMode = false;
/**
* Default theme
* @type {{components: {Menu: {itemDividerBorderColor: string}}}}
*/
const defaultTheme = (isDarkMode) => ({
const defaultTheme = {
components: {
Table: {
rowHoverBg: isDarkMode ? "#2a2a2a" : "#e7f3ff",
rowSelectedBg: isDarkMode ? "#333333" : "#e6f7ff",
rowHoverBg: "#e7f3ff",
rowSelectedBg: "#e6f7ff",
headerSortHoverBg: "transparent"
},
Menu: {
darkItemHoverBg: isDarkMode ? "#004a77" : "#1890ff",
itemHoverBg: isDarkMode ? "#004a77" : "#1890ff",
horizontalItemHoverBg: isDarkMode ? "#004a77" : "#1890ff"
darkItemHoverBg: "#1890ff",
itemHoverBg: "#1890ff",
horizontalItemHoverBg: "#1890ff"
}
},
token: {
colorPrimary: InstanceRenderMgr(
{
imex: isDarkMode ? "#4da8ff" : "#1890ff",
rome: isDarkMode ? "#5b8ce6" : "#326ade"
},
isDarkMode
),
colorInfo: InstanceRenderMgr(
{
imex: isDarkMode ? "#4da8ff" : "#1890ff",
rome: isDarkMode ? "#5b8ce6" : "#326ade"
},
isDarkMode
),
colorError: isDarkMode ? "#ff4d4f" : "#f5222d",
colorBgBase: isDarkMode ? "#1f1f1f" : "#ffffff" // Align with Ant Design dark mode
colorPrimary: InstanceRenderMgr({
imex: "#1890ff",
rome: "#326ade"
}),
colorInfo: InstanceRenderMgr({
imex: "#1890ff",
rome: "#326ade"
})
}
});
};
/**
* Development theme
@@ -66,9 +60,8 @@ const prodTheme = {};
const currentTheme = import.meta.env.DEV ? devTheme : prodTheme;
const getTheme = (isDarkMode) => ({
const finaltheme = {
algorithm: isDarkMode ? darkAlgorithm : defaultAlgorithm,
...defaultsDeep(currentTheme, defaultTheme)
});
export default getTheme;
};
export default finaltheme;

View File

@@ -1,7 +1,7 @@
import { useEffect } from "react";
import React, { useEffect } from "react";
import { Outlet, useLocation, useNavigate } from "react-router-dom";
function PrivateRoute({ isAuthorized }) {
function PrivateRoute({ component: Component, isAuthorized, ...rest }) {
const location = useLocation();
const navigate = useNavigate();

View File

@@ -1,4 +1,5 @@
import { Button } from "antd";
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { setModalContext } from "../../redux/modals/modals.actions";

View File

@@ -142,16 +142,7 @@ export function AccountingReceivablesTableComponent({ bodyshop, loading, jobs, r
refetch={refetch}
/>
<Link to={`/manage/jobs/${record.id}/close`}>
<Button
style={{
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
verticalAlign: "middle"
}}
>
{t("jobs.labels.viewallocations")}
</Button>
<Button>{t("jobs.labels.viewallocations")}</Button>
</Link>
</Space>
)
@@ -180,7 +171,7 @@ export function AccountingReceivablesTableComponent({ bodyshop, loading, jobs, r
<Card
extra={
<Space wrap>
{!bodyshop.cdk_dealerid && !bodyshop.pbs_serialnumber && !bodyshop.rr_dealerid && (
{!bodyshop.cdk_dealerid && !bodyshop.pbs_serialnumber && (
<>
<JobMarkSelectedExported
jobIds={selectedJobs}
@@ -198,7 +189,7 @@ export function AccountingReceivablesTableComponent({ bodyshop, loading, jobs, r
/>
</>
)}
{bodyshop.accountingconfig?.qbo && <QboAuthorizeComponent />}
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && <QboAuthorizeComponent />}
<Input.Search
value={state.search}
onChange={handleSearch}

View File

@@ -1,4 +1,5 @@
import { Alert } from "antd";
import React from "react";
export default function AlertComponent(props) {
return <Alert {...props} />;

View File

@@ -1,4 +1,5 @@
import { Button, InputNumber, Popover, Select } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";

View File

@@ -1,4 +1,4 @@
import { useState } from "react";
import React, { useState } from "react";
import AllocationsAssignmentComponent from "./allocations-assignment.component";
import { useMutation } from "@apollo/client";
import { INSERT_ALLOCATION } from "../../graphql/allocations.queries";
@@ -18,7 +18,7 @@ export default function AllocationsAssignmentContainer({ jobLineId, hours, refet
const handleAssignment = () => {
insertAllocation({ variables: { alloc: { ...assignment } } })
.then(() => {
.then((r) => {
notification["success"]({
message: t("allocations.successes.save")
});

View File

@@ -1,4 +1,5 @@
import { Button, Popover, Select } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";

View File

@@ -1,4 +1,4 @@
import { useState } from "react";
import React, { useState } from "react";
import AllocationsBulkAssignment from "./allocations-bulk-assignment.component";
import { useMutation } from "@apollo/client";
import { INSERT_ALLOCATION } from "../../graphql/allocations.queries";
@@ -24,7 +24,7 @@ export default function AllocationsBulkAssignmentContainer({ jobLines, refetch }
return acc;
}, []);
insertAllocation({ variables: { alloc: allocs } }).then(() => {
insertAllocation({ variables: { alloc: allocs } }).then((r) => {
notification["success"]({
message: t("employees.successes.save")
});

View File

@@ -1,4 +1,5 @@
import Icon from "@ant-design/icons";
import React from "react";
import { MdRemoveCircleOutline } from "react-icons/md";
export default function AllocationsLabelComponent({ allocation, handleClick }) {

View File

@@ -1,3 +1,4 @@
import React from "react";
import { useMutation } from "@apollo/client";
import { DELETE_ALLOCATION } from "../../graphql/allocations.queries";
import AllocationsLabelComponent from "./allocations-employee-label.component";
@@ -12,13 +13,13 @@ export default function AllocationsLabelContainer({ allocation, refetch }) {
const handleClick = (e) => {
e.preventDefault();
deleteAllocation({ variables: { id: allocation.id } })
.then(() => {
.then((r) => {
notification["success"]({
message: t("allocations.successes.deleted")
});
if (refetch) refetch();
})
.catch(() => {
.catch((error) => {
notification["error"]({ message: t("allocations.errors.deleting") });
});
};

View File

@@ -1,4 +1,4 @@
import { useState } from "react";
import React, { useState } from "react";
import { Table } from "antd";
import { alphaSort } from "../../utils/sorters";
import { DateTimeFormatter } from "../../utils/DateFormatter";

View File

@@ -1,3 +1,4 @@
import React from "react";
import AuditTrailListComponent from "./audit-trail-list.component";
import { useQuery } from "@apollo/client";
import { QUERY_AUDIT_TRAIL } from "../../graphql/audit_trail.queries";

View File

@@ -1,5 +1,5 @@
import { Table } from "antd";
import { useState } from "react";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import { alphaSort } from "../../utils/sorters";

View File

@@ -1,3 +1,4 @@
import React from "react";
import { List } from "antd";
import Icon from "@ant-design/icons";
import { FaArrowRight } from "react-icons/fa";

View File

@@ -1,4 +1,5 @@
import { Popover, Tag } from "antd";
import React from "react";
import Barcode from "react-barcode";
import { useTranslation } from "react-i18next";

View File

@@ -1,5 +1,5 @@
import { Checkbox, Form, Skeleton, Typography } from "antd";
import { useEffect } from "react";
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import ReadOnlyFormItemComponent from "../form-items-formatted/read-only-form-item.component";
import "./bill-cm-returns-table.styles.scss";
@@ -33,7 +33,7 @@ export default function BillCmdReturnsTableComponent({ form, returnLoading, retu
return (
<Form.List name="outstanding_returns">
{(fields) => {
{(fields, { add, remove, move }) => {
return (
<>
<Typography.Title level={4}>{t("bills.labels.creditsnotreceived")}</Typography.Title>

View File

@@ -6,7 +6,7 @@
td {
padding: 8px;
text-align: left;
border-bottom: 1px solid var(--table-border-color);
border-bottom: 1px solid #ddd;
.ant-form-item {
margin-bottom: 0px !important;
@@ -14,6 +14,6 @@
}
tr:hover {
background-color: var(--table-hover-bg);
background-color: #f5f5f5;
}
}

View File

@@ -1,7 +1,7 @@
import { DeleteFilled } from "@ant-design/icons";
import { useMutation } from "@apollo/client";
import { Button, Popconfirm } from "antd";
import { useState } from "react";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { DELETE_BILL } from "../../graphql/bills.queries";
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
@@ -43,7 +43,7 @@ export function BillDeleteButton({ bill, jobid, callback, insertAuditTrail }) {
}
});
if (!result.errors) {
if (!!!result.errors) {
notification["success"]({ message: t("bills.successes.deleted") });
insertAuditTrail({
jobid: jobid,

View File

@@ -2,7 +2,7 @@ import { PageHeader } from "@ant-design/pro-layout";
import { useMutation, useQuery } from "@apollo/client";
import { Button, Divider, Form, Popconfirm, Space } from "antd";
import queryString from "query-string";
import { useState } from "react";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useLocation } from "react-router-dom";
@@ -10,6 +10,7 @@ import { createStructuredSelector } from "reselect";
import { DELETE_BILL_LINE, INSERT_NEW_BILL_LINES, UPDATE_BILL_LINE } from "../../graphql/bill-lines.queries";
import { QUERY_BILL_BY_PK, UPDATE_BILL } from "../../graphql/bills.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import dayjs from "../../utils/day";
@@ -27,12 +28,13 @@ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
setPartsOrderContext: (context) => dispatch(setModalContext({ context: context, modal: "partsOrder" })),
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
});
export default connect(mapStateToProps, mapDispatchToProps)(BillDetailEditcontainer);
export function BillDetailEditcontainer({ insertAuditTrail, bodyshop }) {
export function BillDetailEditcontainer({ setPartsOrderContext, insertAuditTrail, bodyshop }) {
const search = queryString.parse(useLocation().search);
const { t } = useTranslation();
@@ -46,7 +48,7 @@ export function BillDetailEditcontainer({ insertAuditTrail, bodyshop }) {
const { loading, error, data, refetch } = useQuery(QUERY_BILL_BY_PK, {
variables: { billid: search.billid },
skip: !search.billid,
skip: !!!search.billid,
fetchPolicy: "network-only",
nextFetchPolicy: "network-only"
});
@@ -69,7 +71,7 @@ export function BillDetailEditcontainer({ insertAuditTrail, bodyshop }) {
setUpdateLoading(true);
//let adjustmentsToInsert = {};
const { billlines, ...bill } = values;
const { billlines, upload, ...bill } = values;
const updates = [];
updates.push(
update_bill({
@@ -96,7 +98,6 @@ export function BillDetailEditcontainer({ insertAuditTrail, bodyshop }) {
});
billlines.forEach((billline) => {
// eslint-disable-next-line no-unused-vars
const { deductedfromlbr, inventories, jobline, original_actual_price, create_ppc, ...il } = billline;
delete il.__typename;
@@ -151,8 +152,8 @@ export function BillDetailEditcontainer({ insertAuditTrail, bodyshop }) {
if (error) return <AlertComponent message={error.message} type="error" />;
if (!search.billid) return <></>; //<div>{t("bills.labels.noneselected")}</div>;
const exported = data?.bills_by_pk && data.bills_by_pk.exported;
const isinhouse = data?.bills_by_pk && data.bills_by_pk.isinhouse;
const exported = data && data.bills_by_pk && data.bills_by_pk.exported;
const isinhouse = data && data.bills_by_pk && data.bills_by_pk.isinhouse;
return (
<>
@@ -182,8 +183,8 @@ export function BillDetailEditcontainer({ insertAuditTrail, bodyshop }) {
{t("general.actions.save")}
</Button>
</Popconfirm>
<BillReeportButtonComponent bill={data?.bills_by_pk} />
<BillMarkExportedButton bill={data?.bills_by_pk} />
<BillReeportButtonComponent bill={data && data.bills_by_pk} />
<BillMarkExportedButton bill={data && data.bills_by_pk} />
</Space>
}
/>
@@ -219,11 +220,11 @@ const transformData = (data) => {
billlines: data.bills_by_pk.billlines.map((i) => {
return {
...i,
joblineid: i.joblineid ? i.joblineid : "noline",
joblineid: !!i.joblineid ? i.joblineid : "noline",
applicable_taxes: {
federal: i.applicable_taxes?.federal || false,
state: i.applicable_taxes?.state || false,
local: i.applicable_taxes?.local || false
federal: (i.applicable_taxes && i.applicable_taxes.federal) || false,
state: (i.applicable_taxes && i.applicable_taxes.state) || false,
local: (i.applicable_taxes && i.applicable_taxes.local) || false
}
};
}),

View File

@@ -1,15 +1,18 @@
import { Button, Checkbox, Form, Modal } from "antd";
import queryString from "query-string";
import { useEffect, useState } from "react";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useLocation, useNavigate } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import ReadOnlyFormItemComponent from "../form-items-formatted/read-only-form-item.component";
const mapStateToProps = createStructuredSelector({});
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
setPartsOrderContext: (context) =>
dispatch(
@@ -17,12 +20,20 @@ const mapDispatchToProps = (dispatch) => ({
context: context,
modal: "partsOrder"
})
),
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(
insertAuditTrail({
jobid,
operation,
type
})
)
});
export default connect(mapStateToProps, mapDispatchToProps)(BillDetailEditReturn);
export function BillDetailEditReturn({ setPartsOrderContext, data, disabled }) {
export function BillDetailEditReturn({ setPartsOrderContext, insertAuditTrail, bodyshop, data, disabled }) {
const search = queryString.parse(useLocation().search);
const history = useNavigate();
const { t } = useTranslation();
@@ -75,9 +86,9 @@ export function BillDetailEditReturn({ setPartsOrderContext, data, disabled }) {
title={t("bills.actions.return")}
onOk={() => form.submit()}
>
<Form initialValues={data?.bills_by_pk} onFinish={handleFinish} form={form}>
<Form initialValues={data && data.bills_by_pk} onFinish={handleFinish} form={form}>
<Form.List name={["billlines"]}>
{(fields) => {
{(fields, { add, remove, move }) => {
return (
<table style={{ tableLayout: "auto", width: "100%" }}>
<thead>

View File

@@ -1,5 +1,6 @@
import { Drawer, Grid } from "antd";
import queryString from "query-string";
import React from "react";
import { useLocation, useNavigate } from "react-router-dom";
import BillDetailEditComponent from "./bill-detail-edit-component";

View File

@@ -26,7 +26,6 @@ import { CalculateBillTotal } from "../bill-form/bill-form.totals.utility";
import { handleUpload as handleLocalUpload } from "../documents-local-upload/documents-local-upload.utility";
import { handleUpload } from "../documents-upload/documents-upload.utility";
import { handleUpload as handleUploadToImageProxy } from "../documents-upload-imgproxy/documents-upload-imgproxy.utility";
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
const mapStateToProps = createStructuredSelector({
billEnterModal: selectBillEnterModal,
@@ -86,8 +85,6 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
}
setLoading(true);
// eslint-disable-next-line no-unused-vars
const { upload, location, outstanding_returns, inventory, federal_tax_exempt, ...remainingValues } = values;
let adjustmentsToInsert = {};
@@ -105,13 +102,9 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
const {
deductedfromlbr,
lbr_adjustment,
// eslint-disable-next-line no-unused-vars
location: lineLocation,
// eslint-disable-next-line no-unused-vars
part_type,
// eslint-disable-next-line no-unused-vars
create_ppc,
// eslint-disable-next-line no-unused-vars
original_actual_price,
...restI
} = i;
@@ -451,9 +444,7 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
setEnterAgain(false);
}}
>
<RbacWrapper action="bills:enter">
<BillFormContainer form={form} disableInvNumber={billEnterModal.context.disableInvNumber} />
</RbacWrapper>
<BillFormContainer form={form} disableInvNumber={billEnterModal.context.disableInvNumber} />
</Form>
</Modal>
);

View File

@@ -1,11 +1,11 @@
import { Form, Input, Table } from "antd";
import { useState } from "react";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { alphaSort } from "../../utils/sorters";
import BillFormItemsExtendedFormItem from "./bill-form-lines.extended.formitem.component";
export default function BillFormLinesExtended({ lineData, discount, form, responsibilityCenters }) {
export default function BillFormLinesExtended({ lineData, discount, form, responsibilityCenters, disabled }) {
const [search, setSearch] = useState("");
const { t } = useTranslation();
const columns = [

View File

@@ -1,3 +1,4 @@
import React from "react";
import { MinusCircleFilled, PlusCircleFilled, WarningOutlined } from "@ant-design/icons";
import { Button, Form, Input, InputNumber, Select, Space, Switch } from "antd";
import { useTranslation } from "react-i18next";
@@ -7,12 +8,11 @@ import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import CiecaSelect from "../../utils/Ciecaselect";
import { bodyshopHasDmsKey } from "../../utils/dmsUtils.js";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
const mapDispatchToProps = () => ({
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(BillFormItemsExtendedFormItem);
@@ -22,6 +22,7 @@ export function BillFormItemsExtendedFormItem({
bodyshop,
form,
record,
index,
disabled,
responsibilityCenters,
discount
@@ -45,7 +46,7 @@ export function BillFormItemsExtendedFormItem({
quantity: record.part_qty || 1,
actual_price: record.act_price,
cost_center: record.part_type
? bodyshopHasDmsKey(bodyshop)
? bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid
? record.part_type
: responsibilityCenters.defaults && (responsibilityCenters.defaults.costs[record.part_type] || null)
: null
@@ -77,7 +78,7 @@ export function BillFormItemsExtendedFormItem({
...billlineskeys,
[record.id]: {
...billlineskeys[billlineskeys],
actual_cost: billlineskeys[billlineskeys].actual_cost
actual_cost: !!billlineskeys[billlineskeys].actual_cost
? billlineskeys[billlineskeys].actual_cost
: Math.round((parseFloat(e.target.value) * (1 - discount) + Number.EPSILON) * 100) / 100
}
@@ -92,7 +93,7 @@ export function BillFormItemsExtendedFormItem({
<Form.Item shouldUpdate>
{() => {
const line = value;
if (!line) return null;
if (!!!line) return null;
const lineDiscount = (1 - Math.round((line.actual_cost / line.actual_price) * 100) / 100).toPrecision(2);
if (lineDiscount - discount === 0) return <div />;
@@ -101,7 +102,7 @@ export function BillFormItemsExtendedFormItem({
</Form.Item>
<Form.Item label={t("billlines.fields.cost_center")} name={["billlineskeys", record.id, "cost_center"]}>
<Select showSearch style={{ minWidth: "3rem" }} disabled={disabled}>
{bodyshopHasDmsKey(bodyshop)
{bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber
? CiecaSelect(true, false)
: responsibilityCenters.costs.map((item) => <Select.Option key={item.name}>{item.name}</Select.Option>)}
</Select>

View File

@@ -2,7 +2,7 @@ import Icon, { UploadOutlined } from "@ant-design/icons";
import { useApolloClient } from "@apollo/client";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import { Alert, Divider, Form, Input, Select, Space, Statistic, Switch, Upload } from "antd";
import { useEffect, useState } from "react";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { MdOpenInNew } from "react-icons/md";
import { connect } from "react-redux";
@@ -22,12 +22,11 @@ import VendorSearchSelect from "../vendor-search-select/vendor-search-select.com
import BillFormLines from "./bill-form.lines.component";
import { CalculateBillTotal } from "./bill-form.totals.utility";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
import { bodyshopHasDmsKey } from "../../utils/dmsUtils.js";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
const mapDispatchToProps = () => ({});
const mapDispatchToProps = (dispatch) => ({});
export function BillFormComponent({
bodyshop,
@@ -255,7 +254,7 @@ export function BillFormComponent({
required: true
//message: t("general.validation.required"),
},
() => ({
({ getFieldValue }) => ({
validator(rule, value) {
if (ClosingPeriod.treatment === "on" && bodyshop.accountingconfig.ClosingPeriod) {
if (
@@ -355,7 +354,7 @@ export function BillFormComponent({
<Form.Item span={3} label={t("bills.fields.local_tax_rate")} name="local_tax_rate">
<CurrencyInput min={0} />
</Form.Item>
{bodyshopHasDmsKey(bodyshop) ? (
{bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid ? (
<Form.Item span={2} label={t("bills.labels.federal_tax_exempt")} name="federal_tax_exempt">
<Switch onChange={handleFederalTaxExemptSwitchToggle} />
</Form.Item>
@@ -375,10 +374,8 @@ export function BillFormComponent({
let totals;
if (!!values.total && !!values.billlines && values.billlines.length > 0)
totals = CalculateBillTotal(values);
if (totals)
if (!!totals)
return (
// TODO: Align is not correct
// eslint-disable-next-line react/no-unknown-property
<div align="right">
<Space size="large" wrap>
<Statistic title={t("bills.labels.subtotal")} value={totals.subtotal.toFormat()} precision={2} />
@@ -461,7 +458,7 @@ export function BillFormComponent({
if (Array.isArray(e)) {
return e;
}
return e?.fileList;
return e && e.fileList;
}}
>
<Upload.Dragger multiple={true} name="logo" beforeUpload={() => false} listType="picture">

View File

@@ -1,5 +1,6 @@
import { useLazyQuery, useQuery } from "@apollo/client";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { QUERY_OUTSTANDING_INVENTORY } from "../../graphql/inventory.queries";

View File

@@ -1,6 +1,7 @@
import { DeleteFilled, DollarCircleFilled } from "@ant-design/icons";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import { Button, Checkbox, Form, Input, InputNumber, Select, Space, Switch, Table, Tooltip } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -10,13 +11,12 @@ import InstanceRenderManager from "../../utils/instanceRenderMgr";
import BillLineSearchSelect from "../bill-line-search-select/bill-line-search-select.component";
import BilllineAddInventory from "../billline-add-inventory/billline-add-inventory.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import { bodyshopHasDmsKey } from "../../utils/dmsUtils.js";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
bodyshop: selectBodyshop
});
const mapDispatchToProps = () => ({
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
@@ -27,7 +27,8 @@ export function BillEnterModalLinesComponent({
discount,
form,
responsibilityCenters,
billEdit
billEdit,
billid
}) {
const { t } = useTranslation();
const { setFieldsValue, getFieldsValue, getFieldValue } = form;
@@ -46,7 +47,7 @@ export function BillEnterModalLinesComponent({
title: t("billlines.fields.jobline"),
dataIndex: "joblineid",
editable: true,
minWidth: "10rem",
width: "20rem",
formItemProps: (field) => {
return {
key: `${field.index}joblinename`,
@@ -72,9 +73,9 @@ export function BillEnterModalLinesComponent({
disabled={disabled}
options={lineData}
style={{
//width: "10rem",
// maxWidth: "20rem",
minWidth: "20rem",
width: "20rem",
maxWidth: "20rem",
minWidth: "10rem",
whiteSpace: "normal",
height: "auto",
minHeight: "32px" // default height of Ant Design inputs
@@ -91,7 +92,7 @@ export function BillEnterModalLinesComponent({
actual_price: opt.cost,
original_actual_price: opt.cost,
cost_center: opt.part_type
? bodyshopHasDmsKey(bodyshop)
? bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid
? opt.part_type !== "PAE"
? opt.part_type
: null
@@ -111,7 +112,7 @@ export function BillEnterModalLinesComponent({
title: t("billlines.fields.line_desc"),
dataIndex: "line_desc",
editable: true,
minWidth: "10rem",
width: "20rem",
formItemProps: (field) => {
return {
key: `${field.index}line_desc`,
@@ -125,7 +126,7 @@ export function BillEnterModalLinesComponent({
]
};
},
formInput: () => <Input.TextArea disabled={disabled} autoSize />
formInput: (record, index) => <Input.TextArea disabled={disabled} autoSize />
},
{
title: t("billlines.fields.quantity"),
@@ -157,7 +158,7 @@ export function BillEnterModalLinesComponent({
]
};
},
formInput: () => <InputNumber precision={0} min={1} disabled={disabled} />
formInput: (record, index) => <InputNumber precision={0} min={1} disabled={disabled} />
},
{
title: t("billlines.fields.actual_price"),
@@ -187,7 +188,7 @@ export function BillEnterModalLinesComponent({
if (idx === index) {
return {
...item,
actual_cost: item.actual_cost
actual_cost: !!item.actual_cost
? item.actual_cost
: Math.round((parseFloat(e.target.value) * (1 - discount) + Number.EPSILON) * 100) / 100
};
@@ -233,7 +234,7 @@ export function BillEnterModalLinesComponent({
title: t("billlines.fields.actual_cost"),
dataIndex: "actual_cost",
editable: true,
width: "10rem",
width: "8rem",
formItemProps: (field) => {
return {
@@ -257,7 +258,7 @@ export function BillEnterModalLinesComponent({
<Form.Item shouldUpdate noStyle>
{() => {
const line = getFieldsValue(["billlines"]).billlines[index];
if (!line) return null;
if (!!!line) return null;
let lineDiscount = 1 - line.actual_cost / line.actual_price;
if (isNaN(lineDiscount)) lineDiscount = 0;
return (
@@ -321,9 +322,9 @@ export function BillEnterModalLinesComponent({
]
};
},
formInput: () => (
formInput: (record, index) => (
<Select showSearch style={{ minWidth: "3rem" }} disabled={disabled}>
{bodyshopHasDmsKey(bodyshop)
{bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber
? CiecaSelect(true, false)
: responsibilityCenters.costs.map((item) => <Select.Option key={item.name}>{item.name}</Select.Option>)}
</Select>
@@ -343,7 +344,7 @@ export function BillEnterModalLinesComponent({
name: [field.name, "location"]
};
},
formInput: () => (
formInput: (record, index) => (
<Select disabled={disabled}>
{bodyshop.md_parts_locations.map((loc, idx) => (
<Select.Option key={idx} value={loc}>
@@ -358,7 +359,6 @@ export function BillEnterModalLinesComponent({
title: t("billlines.labels.deductedfromlbr"),
dataIndex: "deductedfromlbr",
editable: true,
width: "40px",
formItemProps: (field) => {
return {
valuePropName: "checked",
@@ -366,7 +366,7 @@ export function BillEnterModalLinesComponent({
name: [field.name, "deductedfromlbr"]
};
},
formInput: () => <Switch disabled={disabled} />,
formInput: (record, index) => <Switch disabled={disabled} />,
additional: (record, index) => (
<Form.Item shouldUpdate noStyle style={{ display: "inline-block" }}>
{() => {
@@ -466,7 +466,7 @@ export function BillEnterModalLinesComponent({
title: t("billlines.fields.federal_tax_applicable"),
dataIndex: "applicable_taxes.federal",
editable: true,
width: "40px",
formItemProps: (field) => {
return {
key: `${field.index}fedtax`,
@@ -478,7 +478,7 @@ export function BillEnterModalLinesComponent({
name: [field.name, "applicable_taxes", "federal"]
};
},
formInput: () => <Switch disabled={disabled} />
formInput: (record, index) => <Switch disabled={disabled} />
}
]
}),
@@ -487,7 +487,7 @@ export function BillEnterModalLinesComponent({
title: t("billlines.fields.state_tax_applicable"),
dataIndex: "applicable_taxes.state",
editable: true,
width: "40px",
formItemProps: (field) => {
return {
key: `${field.index}statetax`,
@@ -495,7 +495,7 @@ export function BillEnterModalLinesComponent({
name: [field.name, "applicable_taxes", "state"]
};
},
formInput: () => <Switch disabled={disabled} />
formInput: (record, index) => <Switch disabled={disabled} />
},
...InstanceRenderManager({
@@ -505,7 +505,7 @@ export function BillEnterModalLinesComponent({
title: t("billlines.fields.local_tax_applicable"),
dataIndex: "applicable_taxes.local",
editable: true,
width: "40px",
formItemProps: (field) => {
return {
key: `${field.index}localtax`,
@@ -513,7 +513,7 @@ export function BillEnterModalLinesComponent({
name: [field.name, "applicable_taxes", "local"]
};
},
formInput: () => <Switch disabled={disabled} />
formInput: (record, index) => <Switch disabled={disabled} />
}
]
}),
@@ -575,7 +575,7 @@ export function BillEnterModalLinesComponent({
}
]}
>
{(fields, { add, remove }) => {
{(fields, { add, remove, move }) => {
return (
<>
<Table
@@ -612,7 +612,19 @@ export function BillEnterModalLinesComponent({
export default connect(mapStateToProps, mapDispatchToProps)(BillEnterModalLinesComponent);
const EditableCell = ({ dataIndex, record, children, formInput, formItemProps, additional, wrapper, ...restProps }) => {
const EditableCell = ({
dataIndex,
title,
inputType,
record,
index,
children,
formInput,
formItemProps,
additional,
wrapper,
...restProps
}) => {
const propsFinal = formItemProps && formItemProps(record);
if (propsFinal && "key" in propsFinal) {
delete propsFinal.key;

View File

@@ -9,10 +9,10 @@ export const CalculateBillTotal = (invoice) => {
let stateTax = Dinero({ amount: 0 });
let localTax = Dinero({ amount: 0 });
if (!billlines) return null;
if (!!!billlines) return null;
billlines.forEach((i) => {
if (i) {
if (!!i) {
const itemTotal = Dinero({
amount: Math.round((i.actual_cost || 0) * 100)
}).multiply(i.quantity || 1);

View File

@@ -1,5 +1,5 @@
import { Checkbox, Form, Skeleton, Typography } from "antd";
import { useEffect } from "react";
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import ReadOnlyFormItemComponent from "../form-items-formatted/read-only-form-item.component";
import "./bill-inventory-table.styles.scss";
@@ -13,7 +13,7 @@ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
billEnterModal: selectBillEnterModal
});
const mapDispatchToProps = () => ({
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(BillInventoryTable);
@@ -22,7 +22,7 @@ export function BillInventoryTable({ billEnterModal, bodyshop, form, billEdit, i
const { t } = useTranslation();
useEffect(() => {
if (inventoryData?.inventory) {
if (inventoryData && inventoryData.inventory) {
form.setFieldsValue({
inventory: billEnterModal.context.consumeinventoryid
? inventoryData.inventory.map((i) => {
@@ -47,7 +47,7 @@ export function BillInventoryTable({ billEnterModal, bodyshop, form, billEdit, i
return (
<Form.List name="inventory">
{(fields) => {
{(fields, { add, remove, move }) => {
return (
<>
<Typography.Title level={4}>{t("inventory.labels.inventory")}</Typography.Title>

View File

@@ -6,7 +6,7 @@
td {
padding: 8px;
text-align: left;
border-bottom: 1px solid var(--table-border-color);
border-bottom: 1px solid #ddd;
.ant-form-item {
margin-bottom: 0px !important;
@@ -14,6 +14,6 @@
}
tr:hover {
background-color: var(--table-hover-bg);
background-color: #f5f5f5;
}
}

View File

@@ -39,32 +39,30 @@ const BillLineSearchSelect = ({ options, disabled, allowRemoved, ...restProps },
style: {
...(item.removed ? { textDecoration: "line-through" } : {})
},
name: generateLineName(item),
label: generateLineName(item)
name: `${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
item.oem_partno ? ` - ${item.oem_partno}` : ""
}${item.alt_partno ? ` (${item.alt_partno})` : ""}`.trim(),
label: (
<div style={{ whiteSpace: "normal", wordBreak: "break-word" }}>
<span>
{`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
item.oem_partno ? ` - ${item.oem_partno}` : ""
}${item.alt_partno ? ` (${item.alt_partno})` : ""}`.trim()}
</span>
{InstanceRenderMgr({
rome: item.act_price === 0 && item.mod_lb_hrs > 0 && (
<span style={{ float: "right", paddingleft: "1rem" }}>{`${item.mod_lb_hrs} units`}</span>
)
})}
<span style={{ float: "right", paddingleft: "1rem" }}>
{item.act_price ? `$${item.act_price && item.act_price.toFixed(2)}` : ``}
</span>
</div>
)
}))
]}
{...restProps}
></Select>
);
};
function generateLineName(item) {
return (
<div style={{ whiteSpace: "normal", wordBreak: "break-word" }}>
<span>
{`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
item.oem_partno ? ` - ${item.oem_partno}` : ""
}${item.alt_partno ? ` (${item.alt_partno})` : ""}`.trim()}
</span>
{InstanceRenderMgr({
rome: item.act_price === 0 && item.mod_lb_hrs > 0 && (
<span style={{ float: "right", paddingleft: "1rem" }}>{`${item.mod_lb_hrs} units`}</span>
)
})}
<span style={{ float: "right", paddingleft: "1rem" }}>
{item.act_price ? `$${item.act_price && item.act_price.toFixed(2)}` : ``}
</span>
</div>
);
}
export default forwardRef(BillLineSearchSelect);

View File

@@ -1,6 +1,6 @@
import { gql, useMutation } from "@apollo/client";
import { Button } from "antd";
import { useState } from "react";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -15,7 +15,7 @@ const mapStateToProps = createStructuredSelector({
authLevel: selectAuthLevel,
currentUser: selectCurrentUser
});
const mapDispatchToProps = () => ({
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});

View File

@@ -1,5 +1,5 @@
import { Button, Space } from "antd";
import { useState } from "react";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants";
@@ -26,7 +26,7 @@ export default function BillPrintButton({ billid }) {
null,
notification
);
} catch {
} catch (e) {
console.warn("Warning: Error generating a document.");
}
setLoading(false);

View File

@@ -1,6 +1,6 @@
import { gql, useMutation } from "@apollo/client";
import { Button } from "antd";
import { useState } from "react";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -13,7 +13,7 @@ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
authLevel: selectAuthLevel
});
const mapDispatchToProps = () => ({
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});

View File

@@ -3,7 +3,7 @@ import { useMutation } from "@apollo/client";
import { Button, Tooltip } from "antd";
import { t } from "i18next";
import dayjs from "./../../utils/day";
import { useState } from "react";
import React, { useState } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { INSERT_INVENTORY_AND_CREDIT } from "../../graphql/inventory.queries";
@@ -17,7 +17,7 @@ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
currentUser: selectCurrentUser
});
const mapDispatchToProps = () => ({
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(BilllineAddInventory);

View File

@@ -5,7 +5,6 @@ import { useTranslation } from "react-i18next";
import { FaTasks } from "react-icons/fa";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
@@ -76,7 +75,6 @@ export function BillsListTableComponent({
<Button
title={t("tasks.buttons.create")}
onClick={() => {
logImEXEvent("bills_create_task", {});
setTaskUpsertContext({
context: {
jobid: job.id,
@@ -111,13 +109,6 @@ export function BillsListTableComponent({
key: "vendorname",
sorter: (a, b) => alphaSort(a.vendor.name, b.vendor.name),
sortOrder: state.sortedInfo.columnKey === "vendorname" && state.sortedInfo.order,
filters: bills
? [...new Set(bills.map((bill) => bill.vendor.name))].map((name) => ({
text: name,
value: name
}))
: [],
onFilter: (value, record) => record.vendor.name === value,
render: (text, record) => <span>{record.vendor.name}</span>
},
{
@@ -169,7 +160,6 @@ export function BillsListTableComponent({
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
logImEXEvent("bills_list_sort_filter", { pagination, filters, sorter });
};
const filteredBills = bills
@@ -211,7 +201,6 @@ export function BillsListTableComponent({
<Button
disabled={!hasBillsAccess}
onClick={() => {
logImEXEvent("bills_reconcile", {});
setReconciliationContext({
actions: { refetch: billsQuery.refetch },
context: {

View File

@@ -1,4 +1,4 @@
import { useState } from "react";
import React, { useState } from "react";
import { QUERY_ALL_VENDORS } from "../../graphql/vendors.queries";
import { useQuery } from "@apollo/client";
import queryString from "query-string";
@@ -100,9 +100,9 @@ export default function BillsVendorsList() {
selectedRowKeys: [search.vendorid],
type: "radio"
}}
onRow={(record) => {
onRow={(record, rowIndex) => {
return {
onClick: () => {
onClick: (event) => {
handleOnRowClick(record);
} // click row
};

View File

@@ -1,9 +1,10 @@
import { HomeFilled } from "@ant-design/icons";
import { Breadcrumb, Col, Row } from "antd";
import { selectBreadcrumbs, selectIsPartsEntry } from "../../redux/application/application.selectors";
import React from "react";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { selectBreadcrumbs } from "../../redux/application/application.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import GlobalSearch from "../global-search/global-search.component";
import GlobalSearchOs from "../global-search/global-search-os.component";
@@ -12,19 +13,18 @@ import { useSplitTreatments } from "@splitsoftware/splitio-react";
const mapStateToProps = createStructuredSelector({
breadcrumbs: selectBreadcrumbs,
bodyshop: selectBodyshop,
isPartsEntry: selectIsPartsEntry
bodyshop: selectBodyshop
});
export function BreadCrumbs({ breadcrumbs, bodyshop, isPartsEntry }) {
export function BreadCrumbs({ breadcrumbs, bodyshop }) {
const {
treatments: { OpenSearch }
} = useSplitTreatments({
attributes: {},
names: ["OpenSearch"],
splitKey: bodyshop?.imexshopid
splitKey: bodyshop && bodyshop.imexshopid
});
// TODO - Client Update - Technically key is not doing anything here
return (
<Row className="breadcrumb-container">
<Col xs={24} sm={24} md={16}>
@@ -34,8 +34,8 @@ export function BreadCrumbs({ breadcrumbs, bodyshop, isPartsEntry }) {
{
key: "home",
title: (
<Link to={isPartsEntry ? `/parts/` : `/manage/`}>
<HomeFilled /> {(bodyshop?.shopname && `(${bodyshop.shopname})`) || ""}
<Link to={`/manage/`}>
<HomeFilled /> {(bodyshop && bodyshop.shopname && `(${bodyshop.shopname})`) || ""}
</Link>
)
},

View File

@@ -1,5 +1,5 @@
import { Button, Form, Modal } from "antd";
import { useEffect, useState } from "react";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -32,13 +32,12 @@ export function ContractsFindModalContainer({ caBcEtfTableModal, toggleModalVisi
logImEXEvent("ca_bc_etf_table_parse");
setLoading(true);
const claimNumbers = [];
values.table.split("\n").forEach((row) => {
values.table.split("\n").forEach((row, idx, arr) => {
const { 1: claim, 2: shortclaim, 4: amount } = row.split("\t");
if (!claim || !shortclaim) return;
const trimmedShortClaim = shortclaim.trim();
// const trimmedClaim = claim.trim();
if (amount.slice(-1) === "-") {
// NO OP
}
claimNumbers.push({

View File

@@ -1,13 +1,17 @@
import { Form, Input, Radio } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({});
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
export default connect(mapStateToProps, null)(PartsReceiveModalComponent);
export function PartsReceiveModalComponent() {
export function PartsReceiveModalComponent({ bodyshop, form }) {
const { t } = useTranslation();
return (

View File

@@ -1,6 +1,6 @@
import { CalculatorFilled } from "@ant-design/icons";
import { Button, Form, InputNumber, Popover, Space } from "antd";
import { useState } from "react";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { logImEXEvent } from "../../firebase/firebase.utils";

View File

@@ -2,7 +2,7 @@ import { CopyFilled, DeleteFilled } from "@ant-design/icons";
import { useLazyQuery, useMutation } from "@apollo/client";
import { Button, Card, Col, Form, Input, message, Row, Space, Spin, Statistic } from "antd";
import axios from "axios";
import { useState } from "react";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -14,7 +14,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component";
import JobSearchSelectComponent from "../job-search-select/job-search-select.component";
import { getCurrentUser, logImEXEvent } from "../../firebase/firebase.utils";
import { getCurrentUser } from "../../firebase/firebase.utils";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
const mapStateToProps = createStructuredSelector({
@@ -124,7 +124,6 @@ const CardPaymentModalComponent = ({
const { payments } = form.getFieldsValue();
try {
logImEXEvent("payment_cc_lightbox");
const response = await axios.post("/intellipay/lightbox_credentials", {
bodyshop,
refresh: !!window.intellipay,
@@ -134,6 +133,7 @@ const CardPaymentModalComponent = ({
});
if (window.intellipay) {
// eslint-disable-next-line no-eval
eval(response.data);
pollForIntelliPay(() => {
SetIntellipayCallbackFunctions();
@@ -149,7 +149,7 @@ const CardPaymentModalComponent = ({
window.intellipay.initialize();
});
}
} catch {
} catch (error) {
notification.open({
type: "error",
message: t("job_payments.notifications.error.openingip")
@@ -172,7 +172,6 @@ const CardPaymentModalComponent = ({
try {
const { payments } = form.getFieldsValue();
logImEXEvent("payment_cc_shortlink");
const response = await axios.post("/intellipay/generate_payment_url", {
bodyshop,
amount: payments.reduce((acc, val) => acc + (val?.amount || 0), 0),
@@ -188,7 +187,7 @@ const CardPaymentModalComponent = ({
message.success(t("general.actions.copied"));
}
setLoading(false);
} catch {
} catch (error) {
notification.open({
type: "error",
message: t("job_payments.notifications.error.openingip")
@@ -360,7 +359,7 @@ function pollForIntelliPay(callbackFunction) {
const startTime = Date.now();
function checkFixAmount() {
if (window.intellipay?.fixAmount) {
if (window.intellipay && window.intellipay.fixAmount !== undefined) {
callbackFunction();
return;
}

View File

@@ -1,20 +1,23 @@
import { Button, Modal } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectCardPayment } from "../../redux/modals/modals.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CardPaymentModalComponent from "./card-payment-modal.component";
const mapStateToProps = createStructuredSelector({
cardPaymentModal: selectCardPayment
cardPaymentModal: selectCardPayment,
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment"))
});
function CardPaymentModalContainer({ cardPaymentModal, toggleModalVisible }) {
function CardPaymentModalContainer({ cardPaymentModal, toggleModalVisible, bodyshop }) {
const { open } = cardPaymentModal;
const { t } = useTranslation();

View File

@@ -9,13 +9,13 @@ import "./chat-affix.styles.scss";
import { registerMessagingHandlers, unregisterMessagingHandlers } from "./registerMessagingSocketHandlers";
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
export function ChatAffixContainer({ bodyshop, chatVisible, currentUser }) {
export function ChatAffixContainer({ bodyshop, chatVisible }) {
const { t } = useTranslation();
const client = useApolloClient();
const { socket } = useSocket();
useEffect(() => {
if (!bodyshop?.messagingservicesid) return;
if (!bodyshop || !bodyshop.messagingservicesid) return;
async function SubscribeToTopicForFCMNotification() {
try {
@@ -35,8 +35,8 @@ export function ChatAffixContainer({ bodyshop, chatVisible, currentUser }) {
SubscribeToTopicForFCMNotification();
// Register WebSocket handlers
if (socket?.connected) {
registerMessagingHandlers({ socket, client, currentUser, bodyshop, t });
if (socket && socket.connected) {
registerMessagingHandlers({ socket, client });
return () => {
unregisterMessagingHandlers({ socket });
@@ -44,11 +44,11 @@ export function ChatAffixContainer({ bodyshop, chatVisible, currentUser }) {
}
}, [bodyshop, socket, t, client]);
if (!bodyshop?.messagingservicesid) return <></>;
if (!bodyshop || !bodyshop.messagingservicesid) return <></>;
return (
<div className={`chat-affix ${chatVisible ? "chat-affix-open" : ""}`}>
{bodyshop?.messagingservicesid ? <ChatPopupComponent /> : null}
{bodyshop && bodyshop.messagingservicesid ? <ChatPopupComponent /> : null}
</div>
);
}

View File

@@ -1,10 +1,5 @@
import { gql } from "@apollo/client";
import { playNewMessageSound } from "../../utils/soundManager.js";
import { isLeaderTab } from "../../utils/singleTabAudioLeader";
import { CONVERSATION_LIST_QUERY, GET_CONVERSATION_DETAILS } from "../../graphql/conversations.queries";
import { QUERY_ACTIVE_ASSOCIATION_SOUND } from "../../graphql/user.queries";
import { gql } from "@apollo/client";
const logLocal = (message, ...args) => {
if (import.meta.env.VITE_APP_IS_TEST || !import.meta.env.PROD) {
@@ -31,48 +26,16 @@ const enrichConversation = (conversation, isOutbound) => ({
__typename: "conversations"
});
// Can be uncommonted to test the playback of the notification sound
// window.testTone = () => {
// const notificationSound = new Audio(newMessageSound);
// notificationSound.play().catch((error) => {
// console.error("Error playing notification sound:", error);
// });
// };
export const registerMessagingHandlers = ({ socket, client, currentUser, bodyshop }) => {
export const registerMessagingHandlers = ({ socket, client }) => {
if (!(socket && client)) return;
const handleNewMessageSummary = async (message) => {
const { conversationId, newConversation, existingConversation, isoutbound } = message;
// True only when DB value is strictly true; falls back to true on cache miss
const isNewMessageSoundEnabled = (client) => {
try {
const email = currentUser?.email;
if (!email) return true; // default allow if we can't resolve user
const res = client.readQuery({
query: QUERY_ACTIVE_ASSOCIATION_SOUND,
variables: { email }
});
const flag = res?.associations?.[0]?.new_message_sound;
return flag === true; // strictly true => enabled
} catch {
// If the query hasn't been seeded in cache yet, default ON
return true;
}
};
logLocal("handleNewMessageSummary - Start", { message, isNew: !existingConversation });
const queryVariables = { offset: 0 };
if (!isoutbound) {
// Play notification sound for new inbound message (scoped to bodyshop)
if (isLeaderTab(bodyshop.id) && isNewMessageSoundEnabled(client)) {
playNewMessageSound(bodyshop.id);
}
}
if (!existingConversation && conversationId) {
// Attempt to read from the cache to determine if this is actually a new conversation
try {
@@ -94,7 +57,7 @@ export const registerMessagingHandlers = ({ socket, client, currentUser, bodysho
existingConversation: true
});
}
} catch {
} catch (error) {
logLocal("handleNewMessageSummary - Cache miss", { conversationId });
}
}
@@ -328,6 +291,8 @@ export const registerMessagingHandlers = ({ socket, client, currentUser, bodysho
case "conversation-unarchived":
case "conversation-archived":
// Would like to someday figure out how to get this working without refetch queries,
// But I have but a solid 4 hours into it, and there are just too many weird occurrences
try {
const listQueryVariables = { offset: 0 };
const detailsQueryVariables = { conversationId };
@@ -363,7 +328,7 @@ export const registerMessagingHandlers = ({ socket, client, currentUser, bodysho
}
break;
case "tag-added": {
case "tag-added":
// Ensure `job_conversations` is properly formatted
const formattedJobConversations = job_conversations.map((jc) => ({
__typename: "job_conversations",
@@ -410,7 +375,6 @@ export const registerMessagingHandlers = ({ socket, client, currentUser, bodysho
});
break;
}
case "tag-removed":
try {
@@ -498,7 +462,7 @@ export const registerMessagingHandlers = ({ socket, client, currentUser, bodysho
logLocal("handlePhoneNumberOptedOut - Error", { error: error.message });
}
};
// New handler for phone number opt-in
const handlePhoneNumberOptedIn = async (data) => {
const { bodyshopid, phone_number } = data;

View File

@@ -29,7 +29,9 @@ const mapDispatchToProps = (dispatch) => ({
function ChatConversationListComponent({ conversationList, selectedConversation, setSelectedConversation, bodyshop }) {
const { t } = useTranslation();
const [, forceUpdate] = useState(false);
const phoneNumbers = conversationList.map((item) => phone(item.phone_num, "CA").phoneNumber.replace(/^\+1/, ""));
const { data: optOutData } = useQuery(GET_PHONE_NUMBER_OPT_OUTS, {
variables: {
bodyshopid: bodyshop.id,
@@ -62,12 +64,15 @@ function ChatConversationListComponent({ conversationList, selectedConversation,
const item = sortedConversationList[index];
const normalizedPhone = phone(item.phone_num, "CA").phoneNumber.replace(/^\+1/, "");
const hasOptOutEntry = optOutMap.has(normalizedPhone);
const cardContentRight = <TimeAgoFormatter>{item.updated_at}</TimeAgoFormatter>;
const cardContentLeft =
item.job_conversations.length > 0
? item.job_conversations.map((j, idx) => <Tag key={idx}>{j.job.ro_number}</Tag>)
: null;
const names = <>{_.uniq(item.job_conversations.map((j) => OwnerNameDisplayFunction(j.job)))}</>;
const names = <>{_.uniq(item.job_conversations.map((j, idx) => OwnerNameDisplayFunction(j.job)))}</>;
const cardTitle = (
<>
{item.label && <Tag color="blue">{item.label}</Tag>}
@@ -80,6 +85,7 @@ function ChatConversationListComponent({ conversationList, selectedConversation,
)}
</>
);
const cardExtra = (
<>
<Badge count={item.messages_aggregate.aggregate.count} />
@@ -92,10 +98,11 @@ function ChatConversationListComponent({ conversationList, selectedConversation,
)}
</>
);
const getCardStyle = () =>
item.id === selectedConversation
? { backgroundColor: "var(--card-selected-bg)" }
: { backgroundColor: index % 2 === 0 ? "var(--card-stripe-even-bg)" : "var(--card-stripe-odd-bg)" };
? { backgroundColor: "rgba(128, 128, 128, 0.2)" }
: { backgroundColor: index % 2 === 0 ? "#f0f2f5" : "#ffffff" };
return (
<List.Item

View File

@@ -21,7 +21,7 @@ export function ChatConversationTitleTags({ jobConversations, bodyshop }) {
const handleRemoveTag = async (jobId) => {
const convId = jobConversations[0].conversationid;
if (convId) {
if (!!convId) {
await removeJobConversation({
variables: {
conversationId: convId,

View File

@@ -1,4 +1,5 @@
import { Space } from "antd";
import React from "react";
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
import ChatArchiveButton from "../chat-archive-button/chat-archive-button.component";
import ChatConversationTitleTags from "../chat-conversation-title-tags/chat-conversation-title-tags.component";
@@ -15,10 +16,10 @@ const mapDispatchToProps = () => ({});
export function ChatConversationTitle({ conversation }) {
return (
<Space className="chat-title" wrap>
<PhoneNumberFormatter>{conversation?.phone_num}</PhoneNumberFormatter>
<PhoneNumberFormatter>{conversation && conversation.phone_num}</PhoneNumberFormatter>
<ChatLabelComponent conversation={conversation} />
<ChatPrintButton conversation={conversation} />
<ChatConversationTitleTags jobConversations={conversation?.job_conversations || []} />
<ChatConversationTitleTags jobConversations={(conversation && conversation.job_conversations) || []} />
<ChatTagRoContainer conversation={conversation || []} />
<ChatArchiveButton conversation={conversation} />
</Space>

View File

@@ -1,3 +1,4 @@
import React from "react";
import AlertComponent from "../alert/alert.component";
import ChatConversationTitle from "../chat-conversation-title/chat-conversation-title.component";
import ChatMessageListComponent from "../chat-messages-list/chat-message-list.component";

View File

@@ -14,7 +14,7 @@ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
const mapDispatchToProps = () => ({});
const mapDispatchToProps = (dispatch) => ({});
export function ChatLabel({ conversation, bodyshop }) {
const [loading, setLoading] = useState(false);

View File

@@ -19,7 +19,7 @@ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
const mapDispatchToProps = () => ({});
const mapDispatchToProps = (dispatch) => ({});
export default connect(mapStateToProps, mapDispatchToProps)(ChatMediaSelector);
@@ -40,11 +40,7 @@ export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, c
variables: {
jobId: conversation.job_conversations[0]?.jobid
},
skip:
!open ||
!conversation.job_conversations ||
conversation.job_conversations.length === 0 ||
bodyshop.uselocalmediaserver
skip: !open || !conversation.job_conversations || conversation.job_conversations.length === 0
});
const handleVisibleChange = (change) => {
@@ -52,8 +48,7 @@ export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, c
};
useEffect(() => {
// Instead of wiping the array (which holds media objects), just clear selection flags
setSelectedMedia((prev) => prev.map((m) => ({ ...m, isSelected: false })));
setSelectedMedia([]);
}, [setSelectedMedia, conversation]);
//Knowingly taking on the technical debt of poor implementation below. Done this way to avoid an edge case where no component may be displayed.
@@ -80,7 +75,6 @@ export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, c
<JobDocumentsLocalGalleryExternal
externalMediaState={[selectedMedia, setSelectedMedia]}
jobId={conversation.job_conversations[0]?.jobid}
context="chat"
/>
)}
</>
@@ -96,7 +90,6 @@ export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, c
<JobDocumentsLocalGalleryExternal
externalMediaState={[selectedMedia, setSelectedMedia]}
jobId={conversation.job_conversations[0]?.jobid}
context="chat"
/>
)}
</>
@@ -117,7 +110,6 @@ export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, c
trigger="click"
open={open}
onOpenChange={handleVisibleChange}
destroyOnHidden
classNames={{ root: "media-selector-popover" }}
>
<Badge count={selectedMedia.filter((s) => s.isSelected).length}>

View File

@@ -5,7 +5,7 @@
max-height: 480px;
overflow-y: auto;
padding: 8px;
background-color: var(--popover-bg);
background-color: #fff;
border-radius: 8px;
}
}
@@ -17,7 +17,7 @@
}
.error-message {
color: var(--error-text);
color: red;
font-size: 12px;
text-align: center;
margin-bottom: 8px;
@@ -25,13 +25,14 @@
.no-jobs-message {
font-size: 14px;
color: var(--no-jobs-text);
color: #888;
text-align: center;
padding: 8px;
}
/* Style images within gallery components */
.media-selector-content img {
object-fit: cover;
border-radius: 4px;
margin: 4px;
@@ -39,8 +40,8 @@
}
/* Grid layout for gallery components */
.media-selector-content .ant-image,
.media-selector-content .gallery-container {
.media-selector-content .ant-image, /* Assuming gallery components use Ant Design's Image */
.media-selector-content .gallery-container { /* Fallback for custom gallery classes */
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: 4px;

View File

@@ -1,4 +1,4 @@
import { useCallback, useEffect, useRef, useState } from "react";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { Virtuoso } from "react-virtuoso";
import { renderMessage } from "./renderMessage";
import "./chat-message-list.styles.scss";
@@ -76,7 +76,7 @@ export default function ChatMessageListComponent({ messages }) {
<Virtuoso
ref={virtuosoRef}
data={messages}
overscan={messages.reduce((acc, message) => acc + (message.image_path?.length || 0), 0) ? messages.length : 0}
overscan={!!messages.reduce((acc, message) => acc + (message.image_path?.length || 0), 0) ? messages.length : 0}
itemContent={(index) => renderMessage(messages, index)}
followOutput={(isAtBottom) => handleScrollStateChange(isAtBottom)}
initialTopMostItemIndex={messages.length - 1}

View File

@@ -44,6 +44,7 @@
.chat-send-message-button {
margin: 0.3rem;
padding-left: 0.5rem;
}
.message-icon {
@@ -51,7 +52,7 @@
bottom: 0.1rem;
right: 0.3rem;
margin: 0 0.1rem;
color: var(--message-icon-color);
color: whitesmoke;
z-index: 5;
}
@@ -79,7 +80,7 @@
&:last-child:after {
width: 10px;
background: var(--message-mine-tail-bg);
background: white;
z-index: 1;
}
}
@@ -91,11 +92,11 @@
.message {
margin-right: 20%;
background-color: var(--message-yours-bg);
background-color: #eee;
&:last-child:before {
left: -7px;
background: var(--message-yours-bg);
background: #eee;
border-bottom-right-radius: 15px;
}
@@ -111,14 +112,14 @@
align-items: flex-end;
.message {
color: var(--message-mine-text);
color: white;
margin-left: 25%;
background: linear-gradient(to bottom, var(--message-mine-bg-start) 0%, var(--message-mine-bg-end) 100%);
background: linear-gradient(to bottom, #00d0ea 0%, #0085d1 100%);
padding-bottom: 0.6rem;
&:last-child:before {
right: -8px;
background: linear-gradient(to bottom, var(--message-mine-bg-start) 0%, var(--message-mine-bg-end) 100%);
background: linear-gradient(to bottom, #00d0ea 0%, #0085d1 100%);
border-bottom-left-radius: 15px;
}
@@ -134,31 +135,32 @@
margin: 0.5rem 10%;
.message {
background-color: var(--system-message-bg);
background-color: #f5f5f5;
border-radius: 10px;
padding: 0.5rem 1rem;
text-align: center;
font-style: italic;
color: var(--system-message-text);
color: #555;
width: fit-content;
max-width: 80%;
}
.system-label {
font-size: 0.75rem;
color: var(--system-label-text);
color: #888;
margin-bottom: 0.2rem;
display: block;
}
.system-date {
font-size: 0.75rem;
color: var(--system-label-text);
color: #888;
margin-top: 0.2rem;
text-align: center;
}
}
.virtuoso-container {
flex: 1;
overflow: auto;

View File

@@ -1,5 +1,6 @@
import { PlusCircleOutlined } from "@ant-design/icons";
import { Dropdown } from "antd";
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { setMessage } from "../../redux/messaging/messaging.actions";

View File

@@ -1,6 +1,6 @@
import { MailOutlined, PrinterOutlined } from "@ant-design/icons";
import { Space, Spin } from "antd";
import { useState } from "react";
import React, { useState } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { setEmailOptions } from "../../redux/email/email.actions";
@@ -31,7 +31,7 @@ export function ChatPrintButton({ conversation }) {
type,
conversation.id,
notification
).catch(() => {
).catch((e) => {
console.warn("Something went wrong generating a document.");
});
setLoading(false);

View File

@@ -1,5 +1,6 @@
import { CloseCircleOutlined, LoadingOutlined } from "@ant-design/icons";
import { Empty, Select, Space } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";

View File

@@ -1,4 +1,5 @@
import { Checkbox, Form } from "antd";
import React from "react";
export default function JobIntakeFormCheckboxComponent({ formItem, readOnly }) {
const { name, label, required } = formItem;

View File

@@ -1,3 +1,4 @@
import React from "react";
import FormTypes from "./config-form-types";
export default function ConfirmFormComponents({ componentList, readOnly }) {
@@ -6,7 +7,7 @@ export default function ConfirmFormComponents({ componentList, readOnly }) {
{componentList.map((f, idx) => {
const Comp = FormTypes[f.type];
if (Comp) {
if (!!Comp) {
return <Comp key={idx} formItem={f} readOnly={readOnly} />;
} else {
return <div key={idx}>Error</div>;

View File

@@ -1,4 +1,5 @@
import { Form, Rate } from "antd";
import React from "react";
export default function JobIntakeFormCheckboxComponent({ formItem, readOnly }) {
const { name, label, required } = formItem;

View File

@@ -1,4 +1,5 @@
import { Form, Slider } from "antd";
import React from "react";
export default function JobIntakeFormCheckboxComponent({ formItem, readOnly }) {
const { name, label, required, min, max } = formItem;

View File

@@ -1,4 +1,5 @@
import { Form, Input } from "antd";
import React from "react";
export default function JobIntakeFormCheckboxComponent({ formItem, readOnly }) {
const { name, label, required } = formItem;

View File

@@ -1,4 +1,5 @@
import { Form, Input } from "antd";
import React from "react";
export default function JobIntakeFormCheckboxComponent({ formItem, readOnly }) {
const { name, label, required, rows } = formItem;

View File

@@ -1,3 +1,4 @@
import React from "react";
import { Button, Result } from "antd";
import { useTranslation } from "react-i18next";
import InstanceRenderManager from "../../utils/instanceRenderMgr";

View File

@@ -1,5 +1,5 @@
import { Card, Input, Table } from "antd";
import { useState } from "react";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { alphaSort } from "../../utils/sorters";
@@ -114,9 +114,9 @@ export default function ContractsCarsComponent({ loading, data, selectedCarId, h
type: "radio",
selectedRowKeys: [selectedCarId]
}}
onRow={(record) => {
onRow={(record, rowIndex) => {
return {
onClick: () => {
onClick: (event) => {
handleSelect(record);
}
};

View File

@@ -1,5 +1,6 @@
import { useQuery } from "@apollo/client";
import dayjs from "../../utils/day";
import React from "react";
import { QUERY_AVAILABLE_CC } from "../../graphql/courtesy-car.queries";
import AlertComponent from "../alert/alert.component";
import ContractCarsComponent from "./contract-cars.component";

View File

@@ -2,7 +2,7 @@ import { useMutation } from "@apollo/client";
import { Button, Form, InputNumber, Popover, Radio, Select, Space } from "antd";
import axios from "axios";
import dayjs from "../../utils/day";
import { useState } from "react";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useNavigate } from "react-router-dom";
@@ -16,7 +16,7 @@ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
currentUser: selectCurrentUser
});
const mapDispatchToProps = () => ({
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
@@ -270,7 +270,7 @@ export function ContractConvertToRo({ bodyshop, currentUser, contract, disabled
// awaitRefetchQueries: true,
});
if (result.errors) {
if (!!result.errors) {
notification["error"]({
message: t("jobs.errors.inserting", {
message: JSON.stringify(result.errors)

View File

@@ -1,4 +1,5 @@
import { Card } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import DataLabel from "../data-label/data-label.component";

View File

@@ -1,6 +1,6 @@
import { useLazyQuery } from "@apollo/client";
import { Button } from "antd";
import { useEffect } from "react";
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { GET_JOB_FOR_CC_CONTRACT } from "../../graphql/jobs.queries";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";

View File

@@ -1,5 +1,6 @@
import { WarningFilled } from "@ant-design/icons";
import { Form, Input, InputNumber, Space } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { DateFormatter } from "../../utils/DateFormatter";
import dayjs from "../../utils/day";

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