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
839 changed files with 10974 additions and 18394 deletions

View File

@@ -11,6 +11,7 @@ node_modules
# Files to exclude # Files to exclude
.ebignore .ebignore
.editorconfig .editorconfig
.eslintrc.json
.gitignore .gitignore
.prettierrc.js .prettierrc.js
Dockerfile Dockerfile
@@ -18,6 +19,6 @@ README.MD
bodyshop_translations.babel bodyshop_translations.babel
docker-compose.yml docker-compose.yml
ecosystem.config.js ecosystem.config.js
eslint.config.mjs
# Optional: Exclude logs and temporary files # Optional: Exclude logs and temporary files
*.log *.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,96 +1,116 @@
// index.js // index.js
import express from "express"; import express from 'express';
import fetch from "node-fetch"; import fetch from 'node-fetch';
import { simpleParser } from "mailparser"; import {simpleParser} from 'mailparser';
const app = express(); const app = express();
const PORT = 3334; const PORT = 3334;
app.get("/", async (req, res) => { app.get('/', async (req, res) => {
try { try {
const response = await fetch("http://localhost:4566/_aws/ses"); const response = await fetch('http://localhost:4566/_aws/ses');
if (!response.ok) { if (!response.ok) {
throw new Error("Network response was not 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) { async function parseMessages(messages) {
const parsedMessages = await Promise.all( const parsedMessages = await Promise.all(
messages.map(async (message, index) => { messages.map(async (message, index) => {
try { try {
const parsed = await simpleParser(message.RawData); const parsed = await simpleParser(message.RawData);
return ` 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: lightgray">
<div class="shadow-md rounded-lg p-4 mb-6" style="background-color: white"> <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">
<div class="mb-2"><span class="font-semibold">From:</span> ${message.Source}</div> <span class="font-bold text-lg">Message ${index + 1}</span>
<div class="mb-2"><span class="font-semibold">To:</span> ${parsed.to.text || "No To Address"}</div> </div>
<div class="mb-2"><span class="font-semibold">Subject:</span> ${parsed.subject || "No Subject"}</div> <div class="mb-2">
<div class="mb-2"><span class="font-semibold">Region:</span> ${message.Region}</div> <span class="font-semibold">From:</span> ${message.Source}
<div class="mb-2"><span class="font-semibold">Timestamp:</span> ${message.Timestamp}</div> </div>
</div> <div class="mb-2">
<div class="prose">${parsed.html || parsed.textAsHtml || "No HTML content available"}</div> <span class="font-semibold">Region:</span> ${message.Region}
</div> </div>
`; <div class="mb-2">
} catch (error) { <span class="font-semibold">Timestamp:</span> ${message.Timestamp}
console.error("Error parsing email:", error); </div>
return ` </div>
<div class="bg-white shadow-md rounded-lg p-4 mb-6"> <div class="prose">
<div class="mb-2"><span class="font-bold text-lg">Message ${index + 1}</span></div> ${parsed.html || parsed.textAsHtml || 'No HTML content available'}
<div class="mb-2"><span class="font-semibold">From:</span> ${message.Source}</div> </div>
<div class="mb-2"><span class="font-semibold">Region:</span> ${message.Region}</div> </div>
<div class="mb-2"><span class="font-semibold">Timestamp:</span> ${message.Timestamp}</div> `;
<div class="text-red-500">Error parsing email content</div> } catch (error) {
</div> 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>
return parsedMessages.join(""); </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) { function renderHtml(messagesHtml) {
return ` return `
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Email Messages Viewer</title> <title>Email Messages Viewer</title>
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
<style> <style>
body { body {
background-color: #f3f4f6; background-color: #f3f4f6;
font-family: Arial, sans-serif; font-family: Arial, sans-serif;
} }
.container { .container {
max-width: 800px; max-width: 800px;
margin: 50px auto; margin: 50px auto;
padding: 20px; padding: 20px;
} }
.prose { .prose {
line-height: 1.6; line-height: 1.6;
} }
</style> </style>
</head> </head>
<body> <body>
<div class="container bg-white shadow-lg rounded-lg p-6"> <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> <h1 class="text-2xl font-bold text-center mb-6">Email Messages Viewer</h1>
<div id="messages-container">${messagesHtml}</div> <div id="messages-container">
</div> ${messagesHtml}
</body> </div>
</html> </div>
`; </body>
</html>
`;
} }
app.listen(PORT, () => { 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", "license": "ISC",
"dependencies": { "dependencies": {
"express": "^5.1.0", "express": "^5.1.0",
"mailparser": "^3.7.4", "mailparser": "^3.7.2",
"node-fetch": "^3.3.2" "node-fetch": "^3.3.2"
} }
}, },
@@ -634,9 +634,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/libmime": { "node_modules/libmime": {
"version": "5.3.7", "version": "5.3.6",
"resolved": "https://registry.npmjs.org/libmime/-/libmime-5.3.7.tgz", "resolved": "https://registry.npmjs.org/libmime/-/libmime-5.3.6.tgz",
"integrity": "sha512-FlDb3Wtha8P01kTL3P9M+ZDNDWPKPmKHWaU/cG/lg5pfuAwdflVpZE+wm9m7pKmC5ww6s+zTxBKS1p6yl3KpSw==", "integrity": "sha512-j9mBC7eiqi6fgBPAGvKCXJKJSIASanYF4EeA4iBzSG0HxQxmXnR3KbyWqTn4CwsKSebqCv2f5XZfAO6sKzgvwA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"encoding-japanese": "2.2.0", "encoding-japanese": "2.2.0",
@@ -661,31 +661,31 @@
} }
}, },
"node_modules/mailparser": { "node_modules/mailparser": {
"version": "3.7.4", "version": "3.7.2",
"resolved": "https://registry.npmjs.org/mailparser/-/mailparser-3.7.4.tgz", "resolved": "https://registry.npmjs.org/mailparser/-/mailparser-3.7.2.tgz",
"integrity": "sha512-Beh4yyR4jLq3CZZ32asajByrXnW8dLyKCAQD3WvtTiBnMtFWhxO+wa93F6sJNjDmfjxXs4NRNjw3XAGLqZR3Vg==", "integrity": "sha512-iI0p2TCcIodR1qGiRoDBBwboSSff50vQAWytM5JRggLfABa4hHYCf3YVujtuzV454xrOP352VsAPIzviqMTo4Q==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"encoding-japanese": "2.2.0", "encoding-japanese": "2.2.0",
"he": "1.2.0", "he": "1.2.0",
"html-to-text": "9.0.5", "html-to-text": "9.0.5",
"iconv-lite": "0.6.3", "iconv-lite": "0.6.3",
"libmime": "5.3.7", "libmime": "5.3.6",
"linkify-it": "5.0.0", "linkify-it": "5.0.0",
"mailsplit": "5.4.5", "mailsplit": "5.4.2",
"nodemailer": "7.0.4", "nodemailer": "6.9.16",
"punycode.js": "2.3.1", "punycode.js": "2.3.1",
"tlds": "1.259.0" "tlds": "1.255.0"
} }
}, },
"node_modules/mailsplit": { "node_modules/mailsplit": {
"version": "5.4.5", "version": "5.4.2",
"resolved": "https://registry.npmjs.org/mailsplit/-/mailsplit-5.4.5.tgz", "resolved": "https://registry.npmjs.org/mailsplit/-/mailsplit-5.4.2.tgz",
"integrity": "sha512-oMfhmvclR689IIaQmIcR5nODnZRRVwAKtqFT407TIvmhX2OLUBnshUTcxzQBt3+96sZVDud9NfSe1NxAkUNXEQ==", "integrity": "sha512-4cczG/3Iu3pyl8JgQ76dKkisurZTmxMrA4dj/e8d2jKYcFTZ7MxOzg1gTioTDMPuFXwTrVuN/gxhkrO7wLg7qA==",
"license": "(MIT OR EUPL-1.1+)", "license": "(MIT OR EUPL-1.1+)",
"dependencies": { "dependencies": {
"libbase64": "1.3.0", "libbase64": "1.3.0",
"libmime": "5.3.7", "libmime": "5.3.6",
"libqp": "2.1.1" "libqp": "2.1.1"
} }
}, },
@@ -793,9 +793,9 @@
} }
}, },
"node_modules/nodemailer": { "node_modules/nodemailer": {
"version": "7.0.4", "version": "6.9.16",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.4.tgz", "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.16.tgz",
"integrity": "sha512-9O00Vh89/Ld2EcVCqJ/etd7u20UhME0f/NToPfArwPEe1Don1zy4mAIz6ariRr7mJ2RDxtaDzN0WJVdVXPtZaw==", "integrity": "sha512-psAuZdTIRN08HKVd/E8ObdV6NO7NTBY3KsC30F7M4H1OnmLCUNaS56FpYxyb26zWLSyYF9Ozch9KYHhHegsiOQ==",
"license": "MIT-0", "license": "MIT-0",
"engines": { "engines": {
"node": ">=6.0.0" "node": ">=6.0.0"
@@ -1114,9 +1114,9 @@
} }
}, },
"node_modules/tlds": { "node_modules/tlds": {
"version": "1.259.0", "version": "1.255.0",
"resolved": "https://registry.npmjs.org/tlds/-/tlds-1.259.0.tgz", "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.255.0.tgz",
"integrity": "sha512-AldGGlDP0PNgwppe2quAvuBl18UcjuNtOnDuUkqhd6ipPqrYYBt3aTxK1QTsBVknk97lS2JcafWMghjGWFtunw==", "integrity": "sha512-tcwMRIioTcF/FcxLev8MJWxCp+GUALRhFEqbDoZrnowmKSGqPrl5pqS+Sut2m8BgJ6S4FExCSSpGffZ0Tks6Aw==",
"license": "MIT", "license": "MIT",
"bin": { "bin": {
"tlds": "bin.js" "tlds": "bin.js"

View File

@@ -12,7 +12,7 @@
"description": "", "description": "",
"dependencies": { "dependencies": {
"express": "^5.1.0", "express": "^5.1.0",
"mailparser": "^3.7.4", "mailparser": "^3.7.2",
"node-fetch": "^3.3.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> </translation>
</translations> </translations>
</concept_node> </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> <concept_node>
<name>sendmaterialscosting</name> <name>sendmaterialscosting</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>

View File

@@ -14,7 +14,3 @@ VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
VITE_APP_INSTANCE=IMEX VITE_APP_INSTANCE=IMEX
TEST_USERNAME="test@imex.dev" TEST_USERNAME="test@imex.dev"
TEST_PASSWORD="test123" 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 VITE_APP_INSTANCE=ROME
TEST_USERNAME="test@imex.dev" TEST_USERNAME="test@imex.dev"
TEST_PASSWORD="test123" 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_REPORTS_SERVER_URL=https://reports.imex.online
VITE_APP_SPLIT_API=et9pjkik6bn67he5evpmpr1agoo7gactphgk VITE_APP_SPLIT_API=et9pjkik6bn67he5evpmpr1agoo7gactphgk
VITE_APP_INSTANCE=IMEX 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_REPORTS_SERVER_URL=https://reports.romeonline.io
VITE_APP_SPLIT_API=et9pjkik6bn67he5evpmpr1agoo7gactphgk VITE_APP_SPLIT_API=et9pjkik6bn67he5evpmpr1agoo7gactphgk
VITE_APP_INSTANCE=ROME 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_IS_TEST=true
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
VITE_APP_INSTANCE=IMEX 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_IS_TEST=true
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
VITE_APP_INSTANCE=ROME 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 pluginJs from "@eslint/js";
import pluginReact from "eslint-plugin-react"; import pluginReact from "eslint-plugin-react";
/** @type {import("eslint").Linter.Config[]} */ /** @type {import('eslint').Linter.Config[]} */
export default [ export default [
{ ignores: ["node_modules/**", "dist/**", "build/**", "dev-dist/**"] },
{ {
files: ["**/*.{js,mjs,cjs,jsx}"] files: ["**/*.{js,mjs,cjs,jsx}"]
}, },
@@ -12,13 +12,9 @@ export default [
pluginJs.configs.recommended, pluginJs.configs.recommended,
{ {
...pluginReact.configs.flat.recommended, ...pluginReact.configs.flat.recommended,
settings: {
react: { version: "detect" }
},
rules: { rules: {
...pluginReact.configs.flat.recommended.rules, ...pluginReact.configs.flat.recommended.rules,
"react/prop-types": 0, "react/prop-types": 0
"react/no-children-prop": 0 // Disable react/no-children-prop rule
} }
}, },
pluginReact.configs.flat["jsx-runtime"] pluginReact.configs.flat["jsx-runtime"]

6075
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,5 @@
// Scripts for firebase and firebase messaging // 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"); 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"); 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 // 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); firebase.initializeApp(firebaseConfig);
// Retrieve firebase messaging // Retrieve firebase messaging
// eslint-disable-next-line no-undef
const messaging = firebase.messaging(); const messaging = firebase.messaging();
messaging.onBackgroundMessage(function (payload) { messaging.onBackgroundMessage(function (payload) {
// Customize notification here // Customize notification here
console.log("[firebase-messaging-sw.js] Received background message ", payload); console.log("[firebase-messaging-sw.js] Received background message ", payload);
// eslint-disable-next-line no-undef
self.registration.showNotification(notificationTitle, notificationOptions); self.registration.showNotification(notificationTitle, notificationOptions);
}); });

View File

@@ -1,20 +1,16 @@
import { ApolloProvider } from "@apollo/client"; import { ApolloProvider } from "@apollo/client";
import * as Sentry from "@sentry/react";
import { SplitFactoryProvider, useSplitClient } from "@splitsoftware/splitio-react"; import { SplitFactoryProvider, useSplitClient } from "@splitsoftware/splitio-react";
import { ConfigProvider } from "antd"; import { ConfigProvider } from "antd";
import enLocale from "antd/es/locale/en_US"; import enLocale from "antd/es/locale/en_US";
import { useEffect, useMemo } from "react"; import { useEffect } from "react";
import { CookiesProvider } from "react-cookie";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect, useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { createStructuredSelector } from "reselect";
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component"; 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 client from "../utils/GraphQLClient"; import client from "../utils/GraphQLClient";
import App from "./App"; 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 // Base Split configuration
const config = { const config = {
@@ -28,53 +24,19 @@ const config = {
function SplitClientProvider({ children }) { function SplitClientProvider({ children }) {
const imexshopid = useSelector((state) => state.user.imexshopid); // Access imexshopid from Redux store const imexshopid = useSelector((state) => state.user.imexshopid); // Access imexshopid from Redux store
const splitClient = useSplitClient({ key: imexshopid || "anon" }); // Use imexshopid or fallback to "anon" const splitClient = useSplitClient({ key: imexshopid || "anon" }); // Use imexshopid or fallback to "anon"
useEffect(() => { useEffect(() => {
if (splitClient && imexshopid) { 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}`); console.log(`Split client initialized with key: ${imexshopid}, isReady: ${splitClient.isReady}`);
} }
}, [splitClient, imexshopid]); }, [splitClient, imexshopid]);
return children; return children;
} }
const mapDispatchToProps = (dispatch) => ({ function AppContainer() {
setDarkMode: (isDarkMode) => dispatch(setDarkMode(isDarkMode))
});
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser
});
function AppContainer({ currentUser, setDarkMode }) {
const { t } = useTranslation(); const { t } = useTranslation();
const isDarkMode = useSelector(selectDarkMode);
const theme = useMemo(() => getTheme(isDarkMode), [isDarkMode]);
// Update data-theme attribute when dark mode changes
useEffect(() => {
document.documentElement.setAttribute("data-theme", isDarkMode ? "dark" : "light");
return () => document.documentElement.removeAttribute("data-theme");
}, [isDarkMode]);
// Sync Redux darkMode with localStorage on user change
useEffect(() => {
if (currentUser?.uid) {
const savedMode = localStorage.getItem(`dark-mode-${currentUser.uid}`);
if (savedMode !== null) {
setDarkMode(JSON.parse(savedMode));
} else {
setDarkMode(false); // default to light mode
}
} else {
setDarkMode(false);
}
}, [currentUser?.uid]);
// Persist darkMode to localStorage when it or user changes
useEffect(() => {
if (currentUser?.uid) {
localStorage.setItem(`dark-mode-${currentUser.uid}`, JSON.stringify(isDarkMode));
}
}, [isDarkMode, currentUser?.uid]);
return ( return (
<CookiesProvider> <CookiesProvider>
@@ -82,9 +44,10 @@ function AppContainer({ currentUser, setDarkMode }) {
<ConfigProvider <ConfigProvider
input={{ autoComplete: "new-password" }} input={{ autoComplete: "new-password" }}
locale={enLocale} locale={enLocale}
theme={theme} theme={themeProvider}
form={{ form={{
validateMessages: { validateMessages: {
// eslint-disable-next-line no-template-curly-in-string
required: t("general.validation.required", { label: "${label}" }) required: t("general.validation.required", { label: "${label}" })
} }
}} }}
@@ -101,4 +64,4 @@ function AppContainer({ currentUser, setDarkMode }) {
); );
} }
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 { Route, Routes, useNavigate } from "react-router-dom";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import DocumentEditorContainer from "../components/document-editor/document-editor.container"; 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 LoadingSpinner from "../components/loading-spinner/loading-spinner.component";
import DisclaimerPage from "../pages/disclaimer/disclaimer.page"; import DisclaimerPage from "../pages/disclaimer/disclaimer.page";
import LandingPage from "../pages/landing/landing.page"; import LandingPage from "../pages/landing/landing.page";
import TechPageContainer from "../pages/tech/tech.page.container"; import TechPageContainer from "../pages/tech/tech.page.container";
import SimplifiedPartsPageContainer from "../pages/simplified-parts/simplified-parts.page.container.jsx"; import { setOnline } from "../redux/application/application.actions";
import { setIsPartsEntry, setOnline } from "../redux/application/application.actions"; import { selectOnline } from "../redux/application/application.selectors";
import { selectIsPartsEntry, selectOnline } from "../redux/application/application.selectors";
import { checkUserSession } from "../redux/user/user.actions"; import { checkUserSession } from "../redux/user/user.actions";
import { selectBodyshop, selectCurrentEula, selectCurrentUser } from "../redux/user/user.selectors"; import { selectBodyshop, selectCurrentEula, selectCurrentUser } from "../redux/user/user.selectors";
import PrivateRoute from "../components/PrivateRoute"; import PrivateRoute from "../components/PrivateRoute";
@@ -24,37 +23,26 @@ import InstanceRenderMgr from "../utils/instanceRenderMgr";
import ProductFruitsWrapper from "./ProductFruitsWrapper.jsx"; import ProductFruitsWrapper from "./ProductFruitsWrapper.jsx";
import { NotificationProvider } from "../contexts/Notifications/notificationContext.jsx"; import { NotificationProvider } from "../contexts/Notifications/notificationContext.jsx";
import SocketProvider from "../contexts/SocketIO/socketProvider.jsx"; import SocketProvider from "../contexts/SocketIO/socketProvider.jsx";
import SoundWrapper from "./SoundWrapper.jsx";
const ResetPassword = lazy(() => import("../pages/reset-password/reset-password.component")); const ResetPassword = lazy(() => import("../pages/reset-password/reset-password.component"));
const ManagePage = lazy(() => import("../pages/manage/manage.page.container")); const ManagePage = lazy(() => import("../pages/manage/manage.page.container"));
const SignInPage = lazy(() => import("../pages/sign-in/sign-in.page")); const SignInPage = lazy(() => import("../pages/sign-in/sign-in.page"));
const CsiPage = lazy(() => import("../pages/csi/csi.container.page")); const CsiPage = lazy(() => import("../pages/csi/csi.container.page"));
const MobilePaymentContainer = lazy(() => import("../pages/mobile-payment/mobile-payment.container"));
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
online: selectOnline, online: selectOnline,
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
currentEula: selectCurrentEula, currentEula: selectCurrentEula
isPartsEntry: selectIsPartsEntry
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
checkUserSession: () => dispatch(checkUserSession()), checkUserSession: () => dispatch(checkUserSession()),
setOnline: (isOnline) => dispatch(setOnline(isOnline)), setOnline: (isOnline) => dispatch(setOnline(isOnline))
setIsPartsEntry: (isParts) => dispatch(setIsPartsEntry(isParts))
}); });
export function App({ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline, currentEula }) {
bodyshop,
checkUserSession,
currentUser,
online,
setOnline,
setIsPartsEntry,
currentEula,
isPartsEntry
}) {
const client = useSplitClient().client; const client = useSplitClient().client;
const [listenersAdded, setListenersAdded] = useState(false); const [listenersAdded, setListenersAdded] = useState(false);
const { t } = useTranslation(); const { t } = useTranslation();
@@ -64,14 +52,12 @@ export function App({
if (!navigator.onLine) { if (!navigator.onLine) {
setOnline(false); setOnline(false);
} }
checkUserSession(); checkUserSession();
}, [checkUserSession, setOnline]); }, [checkUserSession, setOnline]);
useEffect(() => { //const b = Grid.useBreakpoint();
const pathname = window.location.pathname; // console.log("Breakpoints:", b);
const isParts = pathname === "/parts" || pathname.startsWith("/parts/");
setIsPartsEntry(isParts);
}, [setIsPartsEntry]);
// Associate event listeners, memoize to prevent multiple listeners being added // Associate event listeners, memoize to prevent multiple listeners being added
useEffect(() => { useEffect(() => {
@@ -158,91 +144,86 @@ export function App({
currentUser={currentUser} currentUser={currentUser}
bodyshop={bodyshop} bodyshop={bodyshop}
workspaceCode={bodyshop?.tours_enabled ? "9BkbEseqNqxw8jUH" : ""} workspaceCode={bodyshop?.tours_enabled ? "9BkbEseqNqxw8jUH" : ""}
isPartsEntry={isPartsEntry}
/> />
<NotificationProvider> <NotificationProvider>
<SoundWrapper bodyshop={bodyshop}> <Routes>
<Routes> <Route
<Route path="*"
path="*" element={
element={ <ErrorBoundary>
<ErrorBoundary> <LandingPage />
<LandingPage /> </ErrorBoundary>
</ErrorBoundary> }
} />
/> <Route
<Route path="/signin"
path="/signin" element={
element={ <ErrorBoundary>
<ErrorBoundary> <SignInPage />
<SignInPage /> </ErrorBoundary>
</ErrorBoundary> }
} />
/> <Route
<Route path="/resetpassword"
path="/resetpassword" element={
element={ <ErrorBoundary>
<ErrorBoundary> <ResetPassword />
<ResetPassword /> </ErrorBoundary>
</ErrorBoundary> }
} />
/> <Route
<Route path="/csi/:surveyId"
path="/csi/:surveyId" element={
element={ <ErrorBoundary>
<ErrorBoundary> <CsiPage />
<CsiPage /> </ErrorBoundary>
</ErrorBoundary> }
} />
/> <Route
<Route path="/disclaimer"
path="/disclaimer" element={
element={ <ErrorBoundary>
<ErrorBoundary> <DisclaimerPage />
<DisclaimerPage /> </ErrorBoundary>
</ErrorBoundary> }
} />
/> <Route
<Route path="/mp/:paymentIs"
path="/manage/*" element={
element={ <ErrorBoundary>
<ErrorBoundary> <MobilePaymentContainer />
<SocketProvider bodyshop={bodyshop} navigate={navigate} currentUser={currentUser}> </ErrorBoundary>
<PrivateRoute isAuthorized={currentUser.authorized} /> }
</SocketProvider> />
</ErrorBoundary> <Route
} path="/manage/*"
> element={
<Route path="*" element={<ManagePage />} /> <ErrorBoundary>
</Route> <SocketProvider bodyshop={bodyshop} navigate={navigate} currentUser={currentUser}>
<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>
<PrivateRoute isAuthorized={currentUser.authorized} /> <PrivateRoute isAuthorized={currentUser.authorized} />
</ErrorBoundary> </SocketProvider>
} </ErrorBoundary>
> }
<Route path="*" element={<SimplifiedPartsPageContainer />} /> >
</Route> <Route path="*" element={<ManagePage />} />
<Route path="/edit/*" element={<PrivateRoute isAuthorized={currentUser.authorized} />}> </Route>
<Route path="*" element={<DocumentEditorContainer />} /> <Route
</Route> path="/tech/*"
</Routes> element={
</SoundWrapper> <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> </NotificationProvider>
</Suspense> </Suspense>
); );

View File

@@ -1,226 +1,15 @@
:root { //Global Styles.
--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"; @import "react-big-calendar/lib/sass/styles";
.ant-menu-item-divider { .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 { .ant-menu-submenu-title {
color: var(--menu-submenu-text) !important; color: rgba(255, 255, 255, 0.65) !important;
} }
.imex-table-header { .imex-table-header {
@@ -257,7 +46,7 @@
} }
.ellipses { .ellipses {
display: inline-block; display: inline-block; /* for em, a, span, etc (inline by default) */
text-overflow: ellipsis; text-overflow: ellipsis;
width: calc(95%); width: calc(95%);
overflow: hidden; overflow: hidden;
@@ -271,23 +60,22 @@
} }
} }
// Scrollbar styles (uncomment if needed, updated for dark mode)
// ::-webkit-scrollbar-track { // ::-webkit-scrollbar-track {
// -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); // -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
// border-radius: 0.2rem; // border-radius: 0.2rem;
// background-color: var(--table-stripe-bg); // background-color: #f5f5f5;
// } // }
// ::-webkit-scrollbar { // ::-webkit-scrollbar {
// width: 0.25rem; // width: 0.25rem;
// max-height: 0.25rem; // max-height: 0.25rem;
// background-color: var(--table-stripe-bg); // background-color: #f5f5f5;
// } // }
// ::-webkit-scrollbar-thumb { // ::-webkit-scrollbar-thumb {
// border-radius: 0.2rem; // border-radius: 0.2rem;
// -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); // -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, .ant-input-number-input,
@@ -300,27 +88,28 @@
.production-alert { .production-alert {
animation: alertBlinker 1s linear infinite; animation: alertBlinker 1s linear infinite;
color: var(--alert-color); color: blue;
} }
@keyframes alertBlinker { @keyframes alertBlinker {
50% { 50% {
color: var(--completion-past-color); color: red;
opacity: 100; opacity: 100;
//opacity: 0;
} }
} }
.blue { .blue {
color: var(--alert-color); color: blue;
} }
.production-completion-soon { .production-completion-soon {
color: var(--completion-soon-color); color: rgba(255, 140, 0, 0.8);
font-weight: bold; font-weight: bold;
} }
.production-completion-past { .production-completion-past {
color: var(--completion-past-color); color: rgba(255, 0, 0, 0.8);
font-weight: bold; font-weight: bold;
} }
@@ -350,7 +139,7 @@
} }
.react-kanban-column { .react-kanban-column {
background-color: var(--kanban-column-bg) !important; background-color: #ddd !important;
} }
.production-list-table { .production-list-table {
@@ -362,18 +151,18 @@
.ReactGridGallery_tile-icon-bar { .ReactGridGallery_tile-icon-bar {
div { div {
svg { svg {
fill: var(--alert-color); fill: #1890ff;
} }
} }
} }
.job-line-manual { .job-line-manual {
color: var(--job-line-manual-color); color: tomato;
font-style: italic; font-style: italic;
} }
.ant-table-tbody > tr.ant-table-row:nth-child(2n) > td { .ant-table-tbody > tr.ant-table-row:nth-child(2n) > td {
background-color: var(--table-stripe-bg); background-color: #f4f4f4;
} }
.rowWithColor > td { .rowWithColor > td {
@@ -381,15 +170,15 @@
} }
.muted-button { .muted-button {
color: var(--muted-button-color); color: lightgray;
border: none; border: none;
background: none; background: none;
cursor: pointer; cursor: pointer;
font-size: 16px; font-size: 16px; /* Adjust as needed */
} }
.muted-button:hover { .muted-button:hover {
color: var(--muted-button-hover-color); color: darkgrey;
} }
.notification-alert-unordered-list { .notification-alert-unordered-list {
@@ -401,31 +190,3 @@
margin-right: 0; 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);
}
}
//.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 PropTypes from "prop-types";
import { ProductFruits } from "react-product-fruits"; import { ProductFruits } from "react-product-fruits";
import dayjs from "dayjs"; import dayjs from "dayjs";
const ProductFruitsWrapper = memo(({ currentUser, bodyshop, workspaceCode, isPartsEntry }) => { const ProductFruitsWrapper = React.memo(({ currentUser, bodyshop, workspaceCode }) => {
const featureProps = bodyshop?.features const featureProps = bodyshop?.features
? Object.entries(bodyshop.features).reduce((acc, [key, value]) => { ? Object.entries(bodyshop.features).reduce((acc, [key, value]) => {
acc[key] = value === true || (typeof value === "string" && dayjs(value).isAfter(dayjs())); acc[key] = value === true || (typeof value === "string" && dayjs(value).isAfter(dayjs()));
@@ -12,7 +12,6 @@ const ProductFruitsWrapper = memo(({ currentUser, bodyshop, workspaceCode, isPar
: {}; : {};
return ( return (
!isPartsEntry &&
workspaceCode && workspaceCode &&
currentUser?.authorized === true && currentUser?.authorized === true &&
currentUser?.email && ( currentUser?.email && (
@@ -31,8 +30,6 @@ const ProductFruitsWrapper = memo(({ currentUser, bodyshop, workspaceCode, isPar
); );
}); });
ProductFruitsWrapper.displayName = "ProductFruitsWrapper";
export default ProductFruitsWrapper; export default ProductFruitsWrapper;
ProductFruitsWrapper.propTypes = { 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; const { defaultAlgorithm, darkAlgorithm } = theme;
let isDarkMode = false;
/** /**
* Default theme * Default theme
* @type {{components: {Menu: {itemDividerBorderColor: string}}}} * @type {{components: {Menu: {itemDividerBorderColor: string}}}}
*/ */
const defaultTheme = (isDarkMode) => ({ const defaultTheme = {
components: { components: {
Table: { Table: {
rowHoverBg: isDarkMode ? "#2a2a2a" : "#e7f3ff", rowHoverBg: "#e7f3ff",
rowSelectedBg: isDarkMode ? "#333333" : "#e6f7ff", rowSelectedBg: "#e6f7ff",
headerSortHoverBg: "transparent" headerSortHoverBg: "transparent"
}, },
Menu: { Menu: {
darkItemHoverBg: isDarkMode ? "#004a77" : "#1890ff", darkItemHoverBg: "#1890ff",
itemHoverBg: isDarkMode ? "#004a77" : "#1890ff", itemHoverBg: "#1890ff",
horizontalItemHoverBg: isDarkMode ? "#004a77" : "#1890ff" horizontalItemHoverBg: "#1890ff"
} }
}, },
token: { token: {
colorPrimary: InstanceRenderMgr( colorPrimary: InstanceRenderMgr({
{ imex: "#1890ff",
imex: isDarkMode ? "#4da8ff" : "#1890ff", rome: "#326ade"
rome: isDarkMode ? "#5b8ce6" : "#326ade" }),
}, colorInfo: InstanceRenderMgr({
isDarkMode imex: "#1890ff",
), rome: "#326ade"
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
} }
}); };
/** /**
* Development theme * Development theme
@@ -66,9 +60,8 @@ const prodTheme = {};
const currentTheme = import.meta.env.DEV ? devTheme : prodTheme; const currentTheme = import.meta.env.DEV ? devTheme : prodTheme;
const getTheme = (isDarkMode) => ({ const finaltheme = {
algorithm: isDarkMode ? darkAlgorithm : defaultAlgorithm, algorithm: isDarkMode ? darkAlgorithm : defaultAlgorithm,
...defaultsDeep(currentTheme, defaultTheme) ...defaultsDeep(currentTheme, defaultTheme)
}); };
export default finaltheme;
export default getTheme;

View File

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

View File

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

View File

@@ -142,16 +142,7 @@ export function AccountingReceivablesTableComponent({ bodyshop, loading, jobs, r
refetch={refetch} refetch={refetch}
/> />
<Link to={`/manage/jobs/${record.id}/close`}> <Link to={`/manage/jobs/${record.id}/close`}>
<Button <Button>{t("jobs.labels.viewallocations")}</Button>
style={{
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
verticalAlign: "middle"
}}
>
{t("jobs.labels.viewallocations")}
</Button>
</Link> </Link>
</Space> </Space>
) )

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
import { DeleteFilled } from "@ant-design/icons"; import { DeleteFilled } from "@ant-design/icons";
import { useMutation } from "@apollo/client"; import { useMutation } from "@apollo/client";
import { Button, Popconfirm } from "antd"; import { Button, Popconfirm } from "antd";
import { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { DELETE_BILL } from "../../graphql/bills.queries"; import { DELETE_BILL } from "../../graphql/bills.queries";
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component"; 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") }); notification["success"]({ message: t("bills.successes.deleted") });
insertAuditTrail({ insertAuditTrail({
jobid: jobid, jobid: jobid,

View File

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

View File

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

View File

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

View File

@@ -85,8 +85,6 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
} }
setLoading(true); setLoading(true);
// eslint-disable-next-line no-unused-vars
const { upload, location, outstanding_returns, inventory, federal_tax_exempt, ...remainingValues } = values; const { upload, location, outstanding_returns, inventory, federal_tax_exempt, ...remainingValues } = values;
let adjustmentsToInsert = {}; let adjustmentsToInsert = {};
@@ -104,13 +102,9 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
const { const {
deductedfromlbr, deductedfromlbr,
lbr_adjustment, lbr_adjustment,
// eslint-disable-next-line no-unused-vars
location: lineLocation, location: lineLocation,
// eslint-disable-next-line no-unused-vars
part_type, part_type,
// eslint-disable-next-line no-unused-vars
create_ppc, create_ppc,
// eslint-disable-next-line no-unused-vars
original_actual_price, original_actual_price,
...restI ...restI
} = i; } = i;

View File

@@ -1,11 +1,11 @@
import { Form, Input, Table } from "antd"; import { Form, Input, Table } from "antd";
import { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { alphaSort } from "../../utils/sorters"; import { alphaSort } from "../../utils/sorters";
import BillFormItemsExtendedFormItem from "./bill-form-lines.extended.formitem.component"; 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 [search, setSearch] = useState("");
const { t } = useTranslation(); const { t } = useTranslation();
const columns = [ const columns = [

View File

@@ -1,3 +1,4 @@
import React from "react";
import { MinusCircleFilled, PlusCircleFilled, WarningOutlined } from "@ant-design/icons"; import { MinusCircleFilled, PlusCircleFilled, WarningOutlined } from "@ant-design/icons";
import { Button, Form, Input, InputNumber, Select, Space, Switch } from "antd"; import { Button, Form, Input, InputNumber, Select, Space, Switch } from "antd";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -11,7 +12,7 @@ import CiecaSelect from "../../utils/Ciecaselect";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop bodyshop: selectBodyshop
}); });
const mapDispatchToProps = () => ({ const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
}); });
export default connect(mapStateToProps, mapDispatchToProps)(BillFormItemsExtendedFormItem); export default connect(mapStateToProps, mapDispatchToProps)(BillFormItemsExtendedFormItem);
@@ -21,6 +22,7 @@ export function BillFormItemsExtendedFormItem({
bodyshop, bodyshop,
form, form,
record, record,
index,
disabled, disabled,
responsibilityCenters, responsibilityCenters,
discount discount
@@ -76,7 +78,7 @@ export function BillFormItemsExtendedFormItem({
...billlineskeys, ...billlineskeys,
[record.id]: { [record.id]: {
...billlineskeys[billlineskeys], ...billlineskeys[billlineskeys],
actual_cost: billlineskeys[billlineskeys].actual_cost actual_cost: !!billlineskeys[billlineskeys].actual_cost
? billlineskeys[billlineskeys].actual_cost ? billlineskeys[billlineskeys].actual_cost
: Math.round((parseFloat(e.target.value) * (1 - discount) + Number.EPSILON) * 100) / 100 : Math.round((parseFloat(e.target.value) * (1 - discount) + Number.EPSILON) * 100) / 100
} }
@@ -91,7 +93,7 @@ export function BillFormItemsExtendedFormItem({
<Form.Item shouldUpdate> <Form.Item shouldUpdate>
{() => { {() => {
const line = value; 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); const lineDiscount = (1 - Math.round((line.actual_cost / line.actual_price) * 100) / 100).toPrecision(2);
if (lineDiscount - discount === 0) return <div />; if (lineDiscount - discount === 0) return <div />;

View File

@@ -2,7 +2,7 @@ import Icon, { UploadOutlined } from "@ant-design/icons";
import { useApolloClient } from "@apollo/client"; import { useApolloClient } from "@apollo/client";
import { useSplitTreatments } from "@splitsoftware/splitio-react"; import { useSplitTreatments } from "@splitsoftware/splitio-react";
import { Alert, Divider, Form, Input, Select, Space, Statistic, Switch, Upload } from "antd"; 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 { useTranslation } from "react-i18next";
import { MdOpenInNew } from "react-icons/md"; import { MdOpenInNew } from "react-icons/md";
import { connect } from "react-redux"; import { connect } from "react-redux";
@@ -26,7 +26,7 @@ import DateTimePicker from "../form-date-time-picker/form-date-time-picker.compo
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop bodyshop: selectBodyshop
}); });
const mapDispatchToProps = () => ({}); const mapDispatchToProps = (dispatch) => ({});
export function BillFormComponent({ export function BillFormComponent({
bodyshop, bodyshop,
@@ -254,7 +254,7 @@ export function BillFormComponent({
required: true required: true
//message: t("general.validation.required"), //message: t("general.validation.required"),
}, },
() => ({ ({ getFieldValue }) => ({
validator(rule, value) { validator(rule, value) {
if (ClosingPeriod.treatment === "on" && bodyshop.accountingconfig.ClosingPeriod) { if (ClosingPeriod.treatment === "on" && bodyshop.accountingconfig.ClosingPeriod) {
if ( if (
@@ -374,10 +374,8 @@ export function BillFormComponent({
let totals; let totals;
if (!!values.total && !!values.billlines && values.billlines.length > 0) if (!!values.total && !!values.billlines && values.billlines.length > 0)
totals = CalculateBillTotal(values); totals = CalculateBillTotal(values);
if (totals) if (!!totals)
return ( return (
// TODO: Align is not correct
// eslint-disable-next-line react/no-unknown-property
<div align="right"> <div align="right">
<Space size="large" wrap> <Space size="large" wrap>
<Statistic title={t("bills.labels.subtotal")} value={totals.subtotal.toFormat()} precision={2} /> <Statistic title={t("bills.labels.subtotal")} value={totals.subtotal.toFormat()} precision={2} />
@@ -460,7 +458,7 @@ export function BillFormComponent({
if (Array.isArray(e)) { if (Array.isArray(e)) {
return e; return e;
} }
return e?.fileList; return e && e.fileList;
}} }}
> >
<Upload.Dragger multiple={true} name="logo" beforeUpload={() => false} listType="picture"> <Upload.Dragger multiple={true} name="logo" beforeUpload={() => false} listType="picture">

View File

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

View File

@@ -1,6 +1,7 @@
import { DeleteFilled, DollarCircleFilled } from "@ant-design/icons"; import { DeleteFilled, DollarCircleFilled } from "@ant-design/icons";
import { useSplitTreatments } from "@splitsoftware/splitio-react"; import { useSplitTreatments } from "@splitsoftware/splitio-react";
import { Button, Checkbox, Form, Input, InputNumber, Select, Space, Switch, Table, Tooltip } from "antd"; import { Button, Checkbox, Form, Input, InputNumber, Select, Space, Switch, Table, Tooltip } from "antd";
import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
@@ -15,7 +16,7 @@ const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser //currentUser: selectCurrentUser
bodyshop: selectBodyshop bodyshop: selectBodyshop
}); });
const mapDispatchToProps = () => ({ const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
}); });
@@ -26,7 +27,8 @@ export function BillEnterModalLinesComponent({
discount, discount,
form, form,
responsibilityCenters, responsibilityCenters,
billEdit billEdit,
billid
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const { setFieldsValue, getFieldsValue, getFieldValue } = form; const { setFieldsValue, getFieldsValue, getFieldValue } = form;
@@ -124,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"), title: t("billlines.fields.quantity"),
@@ -156,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"), title: t("billlines.fields.actual_price"),
@@ -186,7 +188,7 @@ export function BillEnterModalLinesComponent({
if (idx === index) { if (idx === index) {
return { return {
...item, ...item,
actual_cost: item.actual_cost actual_cost: !!item.actual_cost
? item.actual_cost ? item.actual_cost
: Math.round((parseFloat(e.target.value) * (1 - discount) + Number.EPSILON) * 100) / 100 : Math.round((parseFloat(e.target.value) * (1 - discount) + Number.EPSILON) * 100) / 100
}; };
@@ -256,7 +258,7 @@ export function BillEnterModalLinesComponent({
<Form.Item shouldUpdate noStyle> <Form.Item shouldUpdate noStyle>
{() => { {() => {
const line = getFieldsValue(["billlines"]).billlines[index]; const line = getFieldsValue(["billlines"]).billlines[index];
if (!line) return null; if (!!!line) return null;
let lineDiscount = 1 - line.actual_cost / line.actual_price; let lineDiscount = 1 - line.actual_cost / line.actual_price;
if (isNaN(lineDiscount)) lineDiscount = 0; if (isNaN(lineDiscount)) lineDiscount = 0;
return ( return (
@@ -320,7 +322,7 @@ export function BillEnterModalLinesComponent({
] ]
}; };
}, },
formInput: () => ( formInput: (record, index) => (
<Select showSearch style={{ minWidth: "3rem" }} disabled={disabled}> <Select showSearch style={{ minWidth: "3rem" }} disabled={disabled}>
{bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber {bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber
? CiecaSelect(true, false) ? CiecaSelect(true, false)
@@ -342,7 +344,7 @@ export function BillEnterModalLinesComponent({
name: [field.name, "location"] name: [field.name, "location"]
}; };
}, },
formInput: () => ( formInput: (record, index) => (
<Select disabled={disabled}> <Select disabled={disabled}>
{bodyshop.md_parts_locations.map((loc, idx) => ( {bodyshop.md_parts_locations.map((loc, idx) => (
<Select.Option key={idx} value={loc}> <Select.Option key={idx} value={loc}>
@@ -364,7 +366,7 @@ export function BillEnterModalLinesComponent({
name: [field.name, "deductedfromlbr"] name: [field.name, "deductedfromlbr"]
}; };
}, },
formInput: () => <Switch disabled={disabled} />, formInput: (record, index) => <Switch disabled={disabled} />,
additional: (record, index) => ( additional: (record, index) => (
<Form.Item shouldUpdate noStyle style={{ display: "inline-block" }}> <Form.Item shouldUpdate noStyle style={{ display: "inline-block" }}>
{() => { {() => {
@@ -476,7 +478,7 @@ export function BillEnterModalLinesComponent({
name: [field.name, "applicable_taxes", "federal"] name: [field.name, "applicable_taxes", "federal"]
}; };
}, },
formInput: () => <Switch disabled={disabled} /> formInput: (record, index) => <Switch disabled={disabled} />
} }
] ]
}), }),
@@ -493,7 +495,7 @@ export function BillEnterModalLinesComponent({
name: [field.name, "applicable_taxes", "state"] name: [field.name, "applicable_taxes", "state"]
}; };
}, },
formInput: () => <Switch disabled={disabled} /> formInput: (record, index) => <Switch disabled={disabled} />
}, },
...InstanceRenderManager({ ...InstanceRenderManager({
@@ -511,7 +513,7 @@ export function BillEnterModalLinesComponent({
name: [field.name, "applicable_taxes", "local"] name: [field.name, "applicable_taxes", "local"]
}; };
}, },
formInput: () => <Switch disabled={disabled} /> formInput: (record, index) => <Switch disabled={disabled} />
} }
] ]
}), }),
@@ -573,7 +575,7 @@ export function BillEnterModalLinesComponent({
} }
]} ]}
> >
{(fields, { add, remove }) => { {(fields, { add, remove, move }) => {
return ( return (
<> <>
<Table <Table
@@ -610,7 +612,19 @@ export function BillEnterModalLinesComponent({
export default connect(mapStateToProps, mapDispatchToProps)(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); const propsFinal = formItemProps && formItemProps(record);
if (propsFinal && "key" in propsFinal) { if (propsFinal && "key" in propsFinal) {
delete propsFinal.key; delete propsFinal.key;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,6 @@ import { useTranslation } from "react-i18next";
import { FaTasks } from "react-icons/fa"; import { FaTasks } from "react-icons/fa";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { selectJobReadOnly } from "../../redux/application/application.selectors"; import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { setModalContext } from "../../redux/modals/modals.actions"; import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
@@ -76,7 +75,6 @@ export function BillsListTableComponent({
<Button <Button
title={t("tasks.buttons.create")} title={t("tasks.buttons.create")}
onClick={() => { onClick={() => {
logImEXEvent("bills_create_task", {});
setTaskUpsertContext({ setTaskUpsertContext({
context: { context: {
jobid: job.id, jobid: job.id,
@@ -111,13 +109,6 @@ export function BillsListTableComponent({
key: "vendorname", key: "vendorname",
sorter: (a, b) => alphaSort(a.vendor.name, b.vendor.name), sorter: (a, b) => alphaSort(a.vendor.name, b.vendor.name),
sortOrder: state.sortedInfo.columnKey === "vendorname" && state.sortedInfo.order, 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> render: (text, record) => <span>{record.vendor.name}</span>
}, },
{ {
@@ -169,7 +160,6 @@ export function BillsListTableComponent({
const handleTableChange = (pagination, filters, sorter) => { const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
logImEXEvent("bills_list_sort_filter", { pagination, filters, sorter });
}; };
const filteredBills = bills const filteredBills = bills
@@ -211,7 +201,6 @@ export function BillsListTableComponent({
<Button <Button
disabled={!hasBillsAccess} disabled={!hasBillsAccess}
onClick={() => { onClick={() => {
logImEXEvent("bills_reconcile", {});
setReconciliationContext({ setReconciliationContext({
actions: { refetch: billsQuery.refetch }, actions: { refetch: billsQuery.refetch },
context: { 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 { QUERY_ALL_VENDORS } from "../../graphql/vendors.queries";
import { useQuery } from "@apollo/client"; import { useQuery } from "@apollo/client";
import queryString from "query-string"; import queryString from "query-string";
@@ -100,9 +100,9 @@ export default function BillsVendorsList() {
selectedRowKeys: [search.vendorid], selectedRowKeys: [search.vendorid],
type: "radio" type: "radio"
}} }}
onRow={(record) => { onRow={(record, rowIndex) => {
return { return {
onClick: () => { onClick: (event) => {
handleOnRowClick(record); handleOnRowClick(record);
} // click row } // click row
}; };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,13 +9,13 @@ import "./chat-affix.styles.scss";
import { registerMessagingHandlers, unregisterMessagingHandlers } from "./registerMessagingSocketHandlers"; import { registerMessagingHandlers, unregisterMessagingHandlers } from "./registerMessagingSocketHandlers";
import { useSocket } from "../../contexts/SocketIO/useSocket.js"; import { useSocket } from "../../contexts/SocketIO/useSocket.js";
export function ChatAffixContainer({ bodyshop, chatVisible, currentUser }) { export function ChatAffixContainer({ bodyshop, chatVisible }) {
const { t } = useTranslation(); const { t } = useTranslation();
const client = useApolloClient(); const client = useApolloClient();
const { socket } = useSocket(); const { socket } = useSocket();
useEffect(() => { useEffect(() => {
if (!bodyshop?.messagingservicesid) return; if (!bodyshop || !bodyshop.messagingservicesid) return;
async function SubscribeToTopicForFCMNotification() { async function SubscribeToTopicForFCMNotification() {
try { try {
@@ -35,8 +35,8 @@ export function ChatAffixContainer({ bodyshop, chatVisible, currentUser }) {
SubscribeToTopicForFCMNotification(); SubscribeToTopicForFCMNotification();
// Register WebSocket handlers // Register WebSocket handlers
if (socket?.connected) { if (socket && socket.connected) {
registerMessagingHandlers({ socket, client, currentUser, bodyshop, t }); registerMessagingHandlers({ socket, client });
return () => { return () => {
unregisterMessagingHandlers({ socket }); unregisterMessagingHandlers({ socket });
@@ -44,11 +44,11 @@ export function ChatAffixContainer({ bodyshop, chatVisible, currentUser }) {
} }
}, [bodyshop, socket, t, client]); }, [bodyshop, socket, t, client]);
if (!bodyshop?.messagingservicesid) return <></>; if (!bodyshop || !bodyshop.messagingservicesid) return <></>;
return ( return (
<div className={`chat-affix ${chatVisible ? "chat-affix-open" : ""}`}> <div className={`chat-affix ${chatVisible ? "chat-affix-open" : ""}`}>
{bodyshop?.messagingservicesid ? <ChatPopupComponent /> : null} {bodyshop && bodyshop.messagingservicesid ? <ChatPopupComponent /> : null}
</div> </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 { 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) => { const logLocal = (message, ...args) => {
if (import.meta.env.VITE_APP_IS_TEST || !import.meta.env.PROD) { if (import.meta.env.VITE_APP_IS_TEST || !import.meta.env.PROD) {
@@ -31,48 +26,16 @@ const enrichConversation = (conversation, isOutbound) => ({
__typename: "conversations" __typename: "conversations"
}); });
// Can be uncommonted to test the playback of the notification sound export const registerMessagingHandlers = ({ socket, client }) => {
// 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 }) => {
if (!(socket && client)) return; if (!(socket && client)) return;
const handleNewMessageSummary = async (message) => { const handleNewMessageSummary = async (message) => {
const { conversationId, newConversation, existingConversation, isoutbound } = 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 }); logLocal("handleNewMessageSummary - Start", { message, isNew: !existingConversation });
const queryVariables = { offset: 0 }; 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) { if (!existingConversation && conversationId) {
// Attempt to read from the cache to determine if this is actually a new conversation // Attempt to read from the cache to determine if this is actually a new conversation
try { try {
@@ -94,7 +57,7 @@ export const registerMessagingHandlers = ({ socket, client, currentUser, bodysho
existingConversation: true existingConversation: true
}); });
} }
} catch { } catch (error) {
logLocal("handleNewMessageSummary - Cache miss", { conversationId }); logLocal("handleNewMessageSummary - Cache miss", { conversationId });
} }
} }
@@ -328,6 +291,8 @@ export const registerMessagingHandlers = ({ socket, client, currentUser, bodysho
case "conversation-unarchived": case "conversation-unarchived":
case "conversation-archived": 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 { try {
const listQueryVariables = { offset: 0 }; const listQueryVariables = { offset: 0 };
const detailsQueryVariables = { conversationId }; const detailsQueryVariables = { conversationId };
@@ -363,7 +328,7 @@ export const registerMessagingHandlers = ({ socket, client, currentUser, bodysho
} }
break; break;
case "tag-added": { case "tag-added":
// Ensure `job_conversations` is properly formatted // Ensure `job_conversations` is properly formatted
const formattedJobConversations = job_conversations.map((jc) => ({ const formattedJobConversations = job_conversations.map((jc) => ({
__typename: "job_conversations", __typename: "job_conversations",
@@ -410,7 +375,6 @@ export const registerMessagingHandlers = ({ socket, client, currentUser, bodysho
}); });
break; break;
}
case "tag-removed": case "tag-removed":
try { try {
@@ -498,7 +462,7 @@ export const registerMessagingHandlers = ({ socket, client, currentUser, bodysho
logLocal("handlePhoneNumberOptedOut - Error", { error: error.message }); logLocal("handlePhoneNumberOptedOut - Error", { error: error.message });
} }
}; };
// New handler for phone number opt-in // New handler for phone number opt-in
const handlePhoneNumberOptedIn = async (data) => { const handlePhoneNumberOptedIn = async (data) => {
const { bodyshopid, phone_number } = data; const { bodyshopid, phone_number } = data;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -19,7 +19,7 @@ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop bodyshop: selectBodyshop
}); });
const mapDispatchToProps = () => ({}); const mapDispatchToProps = (dispatch) => ({});
export default connect(mapStateToProps, mapDispatchToProps)(ChatMediaSelector); export default connect(mapStateToProps, mapDispatchToProps)(ChatMediaSelector);

View File

@@ -5,7 +5,7 @@
max-height: 480px; max-height: 480px;
overflow-y: auto; overflow-y: auto;
padding: 8px; padding: 8px;
background-color: var(--popover-bg); background-color: #fff;
border-radius: 8px; border-radius: 8px;
} }
} }
@@ -17,7 +17,7 @@
} }
.error-message { .error-message {
color: var(--error-text); color: red;
font-size: 12px; font-size: 12px;
text-align: center; text-align: center;
margin-bottom: 8px; margin-bottom: 8px;
@@ -25,13 +25,14 @@
.no-jobs-message { .no-jobs-message {
font-size: 14px; font-size: 14px;
color: var(--no-jobs-text); color: #888;
text-align: center; text-align: center;
padding: 8px; padding: 8px;
} }
/* Style images within gallery components */ /* Style images within gallery components */
.media-selector-content img { .media-selector-content img {
object-fit: cover; object-fit: cover;
border-radius: 4px; border-radius: 4px;
margin: 4px; margin: 4px;
@@ -39,8 +40,8 @@
} }
/* Grid layout for gallery components */ /* Grid layout for gallery components */
.media-selector-content .ant-image, .media-selector-content .ant-image, /* Assuming gallery components use Ant Design's Image */
.media-selector-content .gallery-container { .media-selector-content .gallery-container { /* Fallback for custom gallery classes */
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: 4px; 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 { Virtuoso } from "react-virtuoso";
import { renderMessage } from "./renderMessage"; import { renderMessage } from "./renderMessage";
import "./chat-message-list.styles.scss"; import "./chat-message-list.styles.scss";
@@ -76,7 +76,7 @@ export default function ChatMessageListComponent({ messages }) {
<Virtuoso <Virtuoso
ref={virtuosoRef} ref={virtuosoRef}
data={messages} 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)} itemContent={(index) => renderMessage(messages, index)}
followOutput={(isAtBottom) => handleScrollStateChange(isAtBottom)} followOutput={(isAtBottom) => handleScrollStateChange(isAtBottom)}
initialTopMostItemIndex={messages.length - 1} initialTopMostItemIndex={messages.length - 1}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
import { useQuery } from "@apollo/client"; import { useQuery } from "@apollo/client";
import dayjs from "../../utils/day"; import dayjs from "../../utils/day";
import React from "react";
import { QUERY_AVAILABLE_CC } from "../../graphql/courtesy-car.queries"; import { QUERY_AVAILABLE_CC } from "../../graphql/courtesy-car.queries";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import ContractCarsComponent from "./contract-cars.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 { Button, Form, InputNumber, Popover, Radio, Select, Space } from "antd";
import axios from "axios"; import axios from "axios";
import dayjs from "../../utils/day"; import dayjs from "../../utils/day";
import { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
@@ -16,7 +16,7 @@ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
currentUser: selectCurrentUser currentUser: selectCurrentUser
}); });
const mapDispatchToProps = () => ({ const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
}); });
@@ -270,7 +270,7 @@ export function ContractConvertToRo({ bodyshop, currentUser, contract, disabled
// awaitRefetchQueries: true, // awaitRefetchQueries: true,
}); });
if (result.errors) { if (!!result.errors) {
notification["error"]({ notification["error"]({
message: t("jobs.errors.inserting", { message: t("jobs.errors.inserting", {
message: JSON.stringify(result.errors) message: JSON.stringify(result.errors)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
import { useQuery } from "@apollo/client"; import { useQuery } from "@apollo/client";
import React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { QUERY_ALL_ACTIVE_JOBS } from "../../graphql/jobs.queries"; import { QUERY_ALL_ACTIVE_JOBS } from "../../graphql/jobs.queries";

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