Compare commits
94 Commits
feature/IO
...
feature/IO
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1e1d5e82c | ||
|
|
5ae2e33596 | ||
|
|
e11260e8fc | ||
|
|
dfd88308e0 | ||
|
|
33579c3e6a | ||
|
|
0b9b3c027f | ||
|
|
154f9cdfe6 | ||
|
|
5b8c7d922c | ||
|
|
d20347d5dc | ||
|
|
68c4a1efd7 | ||
|
|
3e6d6fdbd1 | ||
|
|
302fd58a56 | ||
|
|
31cfdf9ea3 | ||
|
|
f93800ded4 | ||
|
|
252758747b | ||
|
|
8b39b7c7be | ||
|
|
ada07bad62 | ||
|
|
166a33af4e | ||
|
|
038aa82087 | ||
|
|
99f425eac4 | ||
|
|
b2c504c69d | ||
|
|
ac6856b136 | ||
|
|
521955089f | ||
|
|
4afff893c0 | ||
|
|
cc934fe333 | ||
|
|
ddd3b3d056 | ||
|
|
8ded028197 | ||
|
|
2660466db1 | ||
|
|
c42a0139fc | ||
|
|
02974e6e4b | ||
|
|
fe67efe47c | ||
|
|
69a35772e5 | ||
|
|
38932f4bf9 | ||
|
|
3fcb36a28e | ||
|
|
fe78f5c7ff | ||
|
|
683846c3b0 | ||
|
|
2cc0b247b6 | ||
|
|
31579354d4 | ||
|
|
0e7531dc54 | ||
|
|
268b57c38a | ||
|
|
2b8b8b8073 | ||
|
|
808eeb91e9 | ||
|
|
23dd8fc9de | ||
|
|
f499859078 | ||
|
|
84d9e3251a | ||
|
|
bd7db4dd02 | ||
|
|
e9804b736b | ||
|
|
a14874f116 | ||
|
|
ac9fac458c | ||
|
|
8f9db15852 | ||
|
|
61b3d3c18c | ||
|
|
e6f08d3b1c | ||
|
|
d5cf0f8371 | ||
|
|
52e230fc54 | ||
|
|
4011237c22 | ||
|
|
c24bfbf655 | ||
|
|
08fe8c3c70 | ||
|
|
771a239773 | ||
|
|
82195a0584 | ||
|
|
838c24b3f1 | ||
|
|
13cb68b0af | ||
|
|
7c84b08707 | ||
|
|
3165957e95 | ||
|
|
d9f59fcad4 | ||
|
|
edaeb5d77a | ||
|
|
5365d95d6f | ||
|
|
5cfefd5afd | ||
|
|
bbccdb0650 | ||
|
|
f3535c01af | ||
|
|
7f8c82b300 | ||
|
|
609ac2bd33 | ||
|
|
0883274320 | ||
|
|
fa33b88632 | ||
|
|
bec32c1d70 | ||
|
|
eb18130e51 | ||
|
|
0fbf63dec8 | ||
|
|
f817902d5c | ||
|
|
7a383aaec9 | ||
|
|
e5c0ace6cb | ||
|
|
814447373a | ||
|
|
d766a468c3 | ||
|
|
0ede2d0649 | ||
|
|
54089c2ab3 | ||
|
|
73e3d71cf1 | ||
|
|
f071a5cc9e | ||
|
|
67002b8443 | ||
|
|
3f83c6afa7 | ||
|
|
b7f57e91aa | ||
|
|
b8465c0cc7 | ||
|
|
553c154e46 | ||
|
|
5b400dce4f | ||
|
|
130745d7e7 | ||
|
|
a722ab9758 | ||
|
|
41c9c0be49 |
@@ -1,116 +1,96 @@
|
||||
// index.js
|
||||
|
||||
import express from 'express';
|
||||
import fetch from 'node-fetch';
|
||||
import {simpleParser} from 'mailparser';
|
||||
import express from "express";
|
||||
import fetch from "node-fetch";
|
||||
import { simpleParser } from "mailparser";
|
||||
|
||||
const app = express();
|
||||
const PORT = 3334;
|
||||
|
||||
app.get('/', async (req, res) => {
|
||||
try {
|
||||
const response = await fetch('http://localhost:4566/_aws/ses');
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
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');
|
||||
app.get("/", async (req, res) => {
|
||||
try {
|
||||
const response = await fetch("http://localhost:4566/_aws/ses");
|
||||
if (!response.ok) {
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
const data = await response.json();
|
||||
const messagesHtml = await parseMessages(data.messages);
|
||||
res.send(renderHtml(messagesHtml));
|
||||
} catch (error) {
|
||||
console.error("Error fetching messages:", error);
|
||||
res.status(500).send("Error fetching messages");
|
||||
}
|
||||
});
|
||||
|
||||
async function parseMessages(messages) {
|
||||
const parsedMessages = await Promise.all(
|
||||
messages.map(async (message, index) => {
|
||||
try {
|
||||
const parsed = await simpleParser(message.RawData);
|
||||
return `
|
||||
<div class="shadow-md rounded-lg p-4 mb-6" style="background-color: lightgray">
|
||||
<div class="shadow-md rounded-lg p-4 mb-6" style="background-color: white">
|
||||
<div class="mb-2">
|
||||
<span class="font-bold text-lg">Message ${index + 1}</span>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<span class="font-semibold">From:</span> ${message.Source}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<span class="font-semibold">Region:</span> ${message.Region}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<span class="font-semibold">Timestamp:</span> ${message.Timestamp}
|
||||
</div>
|
||||
</div>
|
||||
<div class="prose">
|
||||
${parsed.html || parsed.textAsHtml || 'No HTML content available'}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} catch (error) {
|
||||
console.error('Error parsing email:', error);
|
||||
return `
|
||||
<div class="bg-white shadow-md rounded-lg p-4 mb-6">
|
||||
<div class="mb-2">
|
||||
<span class="font-bold text-lg">Message ${index + 1}</span>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<span class="font-semibold">From:</span> ${message.Source}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<span class="font-semibold">Region:</span> ${message.Region}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<span class="font-semibold">Timestamp:</span> ${message.Timestamp}
|
||||
</div>
|
||||
<div class="text-red-500">
|
||||
Error parsing email content
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
})
|
||||
);
|
||||
return parsedMessages.join('');
|
||||
const parsedMessages = await Promise.all(
|
||||
messages.map(async (message, index) => {
|
||||
try {
|
||||
const parsed = await simpleParser(message.RawData);
|
||||
return `
|
||||
<div class="shadow-md rounded-lg p-4 mb-6" style="background-color: lightgray">
|
||||
<div class="shadow-md rounded-lg p-4 mb-6" style="background-color: white">
|
||||
<div class="mb-2"><span class="font-bold text-lg">Message ${index + 1}</span></div>
|
||||
<div class="mb-2"><span class="font-semibold">From:</span> ${message.Source}</div>
|
||||
<div class="mb-2"><span class="font-semibold">To:</span> ${parsed.to.text || "No To Address"}</div>
|
||||
<div class="mb-2"><span class="font-semibold">Subject:</span> ${parsed.subject || "No Subject"}</div>
|
||||
<div class="mb-2"><span class="font-semibold">Region:</span> ${message.Region}</div>
|
||||
<div class="mb-2"><span class="font-semibold">Timestamp:</span> ${message.Timestamp}</div>
|
||||
</div>
|
||||
<div class="prose">${parsed.html || parsed.textAsHtml || "No HTML content available"}</div>
|
||||
</div>
|
||||
`;
|
||||
} catch (error) {
|
||||
console.error("Error parsing email:", error);
|
||||
return `
|
||||
<div class="bg-white shadow-md rounded-lg p-4 mb-6">
|
||||
<div class="mb-2"><span class="font-bold text-lg">Message ${index + 1}</span></div>
|
||||
<div class="mb-2"><span class="font-semibold">From:</span> ${message.Source}</div>
|
||||
<div class="mb-2"><span class="font-semibold">Region:</span> ${message.Region}</div>
|
||||
<div class="mb-2"><span class="font-semibold">Timestamp:</span> ${message.Timestamp}</div>
|
||||
<div class="text-red-500">Error parsing email content</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
})
|
||||
);
|
||||
return parsedMessages.join("");
|
||||
}
|
||||
|
||||
function renderHtml(messagesHtml) {
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Email Messages Viewer</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<style>
|
||||
body {
|
||||
background-color: #f3f4f6;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 50px auto;
|
||||
padding: 20px;
|
||||
}
|
||||
.prose {
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container bg-white shadow-lg rounded-lg p-6">
|
||||
<h1 class="text-2xl font-bold text-center mb-6">Email Messages Viewer</h1>
|
||||
<div id="messages-container">
|
||||
${messagesHtml}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Email Messages Viewer</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<style>
|
||||
body {
|
||||
background-color: #f3f4f6;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 50px auto;
|
||||
padding: 20px;
|
||||
}
|
||||
.prose {
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container bg-white shadow-lg rounded-lg p-6">
|
||||
<h1 class="text-2xl font-bold text-center mb-6">Email Messages Viewer</h1>
|
||||
<div id="messages-container">${messagesHtml}</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server is running on http://localhost:${PORT}`);
|
||||
});
|
||||
console.log(`Server is running on http://localhost:${PORT}`);
|
||||
});
|
||||
|
||||
42
_reference/localEmailViewer/package-lock.json
generated
42
_reference/localEmailViewer/package-lock.json
generated
@@ -10,7 +10,7 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"express": "^5.1.0",
|
||||
"mailparser": "^3.7.2",
|
||||
"mailparser": "^3.7.4",
|
||||
"node-fetch": "^3.3.2"
|
||||
}
|
||||
},
|
||||
@@ -634,9 +634,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/libmime": {
|
||||
"version": "5.3.6",
|
||||
"resolved": "https://registry.npmjs.org/libmime/-/libmime-5.3.6.tgz",
|
||||
"integrity": "sha512-j9mBC7eiqi6fgBPAGvKCXJKJSIASanYF4EeA4iBzSG0HxQxmXnR3KbyWqTn4CwsKSebqCv2f5XZfAO6sKzgvwA==",
|
||||
"version": "5.3.7",
|
||||
"resolved": "https://registry.npmjs.org/libmime/-/libmime-5.3.7.tgz",
|
||||
"integrity": "sha512-FlDb3Wtha8P01kTL3P9M+ZDNDWPKPmKHWaU/cG/lg5pfuAwdflVpZE+wm9m7pKmC5ww6s+zTxBKS1p6yl3KpSw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"encoding-japanese": "2.2.0",
|
||||
@@ -661,31 +661,31 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mailparser": {
|
||||
"version": "3.7.2",
|
||||
"resolved": "https://registry.npmjs.org/mailparser/-/mailparser-3.7.2.tgz",
|
||||
"integrity": "sha512-iI0p2TCcIodR1qGiRoDBBwboSSff50vQAWytM5JRggLfABa4hHYCf3YVujtuzV454xrOP352VsAPIzviqMTo4Q==",
|
||||
"version": "3.7.4",
|
||||
"resolved": "https://registry.npmjs.org/mailparser/-/mailparser-3.7.4.tgz",
|
||||
"integrity": "sha512-Beh4yyR4jLq3CZZ32asajByrXnW8dLyKCAQD3WvtTiBnMtFWhxO+wa93F6sJNjDmfjxXs4NRNjw3XAGLqZR3Vg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"encoding-japanese": "2.2.0",
|
||||
"he": "1.2.0",
|
||||
"html-to-text": "9.0.5",
|
||||
"iconv-lite": "0.6.3",
|
||||
"libmime": "5.3.6",
|
||||
"libmime": "5.3.7",
|
||||
"linkify-it": "5.0.0",
|
||||
"mailsplit": "5.4.2",
|
||||
"nodemailer": "6.9.16",
|
||||
"mailsplit": "5.4.5",
|
||||
"nodemailer": "7.0.4",
|
||||
"punycode.js": "2.3.1",
|
||||
"tlds": "1.255.0"
|
||||
"tlds": "1.259.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mailsplit": {
|
||||
"version": "5.4.2",
|
||||
"resolved": "https://registry.npmjs.org/mailsplit/-/mailsplit-5.4.2.tgz",
|
||||
"integrity": "sha512-4cczG/3Iu3pyl8JgQ76dKkisurZTmxMrA4dj/e8d2jKYcFTZ7MxOzg1gTioTDMPuFXwTrVuN/gxhkrO7wLg7qA==",
|
||||
"version": "5.4.5",
|
||||
"resolved": "https://registry.npmjs.org/mailsplit/-/mailsplit-5.4.5.tgz",
|
||||
"integrity": "sha512-oMfhmvclR689IIaQmIcR5nODnZRRVwAKtqFT407TIvmhX2OLUBnshUTcxzQBt3+96sZVDud9NfSe1NxAkUNXEQ==",
|
||||
"license": "(MIT OR EUPL-1.1+)",
|
||||
"dependencies": {
|
||||
"libbase64": "1.3.0",
|
||||
"libmime": "5.3.6",
|
||||
"libmime": "5.3.7",
|
||||
"libqp": "2.1.1"
|
||||
}
|
||||
},
|
||||
@@ -793,9 +793,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/nodemailer": {
|
||||
"version": "6.9.16",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.16.tgz",
|
||||
"integrity": "sha512-psAuZdTIRN08HKVd/E8ObdV6NO7NTBY3KsC30F7M4H1OnmLCUNaS56FpYxyb26zWLSyYF9Ozch9KYHhHegsiOQ==",
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.4.tgz",
|
||||
"integrity": "sha512-9O00Vh89/Ld2EcVCqJ/etd7u20UhME0f/NToPfArwPEe1Don1zy4mAIz6ariRr7mJ2RDxtaDzN0WJVdVXPtZaw==",
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
@@ -1114,9 +1114,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tlds": {
|
||||
"version": "1.255.0",
|
||||
"resolved": "https://registry.npmjs.org/tlds/-/tlds-1.255.0.tgz",
|
||||
"integrity": "sha512-tcwMRIioTcF/FcxLev8MJWxCp+GUALRhFEqbDoZrnowmKSGqPrl5pqS+Sut2m8BgJ6S4FExCSSpGffZ0Tks6Aw==",
|
||||
"version": "1.259.0",
|
||||
"resolved": "https://registry.npmjs.org/tlds/-/tlds-1.259.0.tgz",
|
||||
"integrity": "sha512-AldGGlDP0PNgwppe2quAvuBl18UcjuNtOnDuUkqhd6ipPqrYYBt3aTxK1QTsBVknk97lS2JcafWMghjGWFtunw==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"tlds": "bin.js"
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"express": "^5.1.0",
|
||||
"mailparser": "^3.7.2",
|
||||
"mailparser": "^3.7.4",
|
||||
"node-fetch": "^3.3.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,4 +15,6 @@ VITE_APP_INSTANCE=IMEX
|
||||
TEST_USERNAME="test@imex.dev"
|
||||
TEST_PASSWORD="test123"
|
||||
VITE_PUBLIC_POSTHOG_KEY=phc_xtLmBIu0rjWwExY73Oj5DTH1bGbwq1G1Y8jnlTceien
|
||||
VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
|
||||
VITE_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
|
||||
@@ -17,4 +17,6 @@ VITE_APP_INSTANCE=ROME
|
||||
TEST_USERNAME="test@imex.dev"
|
||||
TEST_PASSWORD="test123"
|
||||
VITE_PUBLIC_POSTHOG_KEY=phc_xtLmBIu0rjWwExY73Oj5DTH1bGbwq1G1Y8jnlTceien
|
||||
VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
|
||||
VITE_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
|
||||
@@ -14,4 +14,6 @@ VITE_APP_REPORTS_SERVER_URL=https://reports.imex.online
|
||||
VITE_APP_SPLIT_API=et9pjkik6bn67he5evpmpr1agoo7gactphgk
|
||||
VITE_APP_INSTANCE=IMEX
|
||||
VITE_PUBLIC_POSTHOG_KEY=phc_xtLmBIu0rjWwExY73Oj5DTH1bGbwq1G1Y8jnlTceien
|
||||
VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
|
||||
VITE_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
|
||||
@@ -14,4 +14,6 @@ VITE_APP_REPORTS_SERVER_URL=https://reports.romeonline.io
|
||||
VITE_APP_SPLIT_API=et9pjkik6bn67he5evpmpr1agoo7gactphgk
|
||||
VITE_APP_INSTANCE=ROME
|
||||
VITE_PUBLIC_POSTHOG_KEY=phc_xtLmBIu0rjWwExY73Oj5DTH1bGbwq1G1Y8jnlTceien
|
||||
VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
|
||||
VITE_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
|
||||
@@ -14,4 +14,6 @@ VITE_APP_IS_TEST=true
|
||||
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
|
||||
VITE_APP_INSTANCE=IMEX
|
||||
VITE_PUBLIC_POSTHOG_KEY=phc_xtLmBIu0rjWwExY73Oj5DTH1bGbwq1G1Y8jnlTceien
|
||||
VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
|
||||
VITE_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
|
||||
@@ -14,4 +14,6 @@ VITE_APP_IS_TEST=true
|
||||
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
|
||||
VITE_APP_INSTANCE=ROME
|
||||
VITE_PUBLIC_POSTHOG_KEY=phc_xtLmBIu0rjWwExY73Oj5DTH1bGbwq1G1Y8jnlTceien
|
||||
VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
|
||||
VITE_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
|
||||
308
client/package-lock.json
generated
308
client/package-lock.json
generated
@@ -9,48 +9,49 @@
|
||||
"version": "0.2.1",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@amplitude/analytics-browser": "^2.23.1",
|
||||
"@amplitude/analytics-browser": "^2.23.5",
|
||||
"@ant-design/pro-layout": "^7.22.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",
|
||||
"@firebase/analytics": "^0.10.17",
|
||||
"@firebase/app": "^0.14.1",
|
||||
"@firebase/app": "^0.14.2",
|
||||
"@firebase/auth": "^1.10.8",
|
||||
"@firebase/firestore": "^4.8.0",
|
||||
"@firebase/firestore": "^4.9.1",
|
||||
"@firebase/messaging": "^0.12.22",
|
||||
"@jsreport/browser-client": "^3.1.0",
|
||||
"@reduxjs/toolkit": "^2.8.2",
|
||||
"@sentry/cli": "^2.52.0",
|
||||
"@reduxjs/toolkit": "^2.9.0",
|
||||
"@sentry/cli": "^2.53.0",
|
||||
"@sentry/react": "^9.43.0",
|
||||
"@sentry/vite-plugin": "^4.1.1",
|
||||
"@sentry/vite-plugin": "^4.3.0",
|
||||
"@splitsoftware/splitio-react": "^2.3.1",
|
||||
"@tanem/react-nprogress": "^5.0.53",
|
||||
"antd": "^5.27.1",
|
||||
"antd": "^5.27.3",
|
||||
"apollo-link-logger": "^2.0.1",
|
||||
"apollo-link-sentry": "^4.4.0",
|
||||
"autosize": "^6.0.1",
|
||||
"axios": "^1.11.0",
|
||||
"classnames": "^2.5.1",
|
||||
"css-box-model": "^1.2.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"dayjs": "^1.11.18",
|
||||
"dayjs-business-days2": "^1.3.0",
|
||||
"dinero.js": "^1.9.1",
|
||||
"dotenv": "^17.2.1",
|
||||
"dotenv": "^17.2.2",
|
||||
"env-cmd": "^10.1.0",
|
||||
"exifr": "^7.1.3",
|
||||
"graphql": "^16.11.0",
|
||||
"i18next": "^25.4.0",
|
||||
"i18next": "^25.5.2",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"immutability-helper": "^3.1.1",
|
||||
"libphonenumber-js": "^1.12.13",
|
||||
"libphonenumber-js": "^1.12.15",
|
||||
"lightningcss": "^1.30.1",
|
||||
"logrocket": "^9.0.2",
|
||||
"markerjs2": "^2.32.6",
|
||||
"memoize-one": "^6.0.0",
|
||||
"normalize-url": "^8.0.2",
|
||||
"object-hash": "^3.0.0",
|
||||
"phone": "^3.1.67",
|
||||
"posthog-js": "^1.260.2",
|
||||
"posthog-js": "^1.261.7",
|
||||
"prop-types": "^15.8.1",
|
||||
"query-string": "^9.2.2",
|
||||
"raf-schd": "^4.0.3",
|
||||
@@ -62,7 +63,7 @@
|
||||
"react-drag-listview": "^2.0.0",
|
||||
"react-grid-gallery": "^1.0.1",
|
||||
"react-grid-layout": "1.3.4",
|
||||
"react-i18next": "^15.7.1",
|
||||
"react-i18next": "^15.7.3",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-image-lightbox": "^5.1.4",
|
||||
"react-markdown": "^10.1.0",
|
||||
@@ -81,7 +82,7 @@
|
||||
"redux-saga": "^1.3.0",
|
||||
"redux-state-sync": "^3.1.4",
|
||||
"reselect": "^5.1.1",
|
||||
"sass": "^1.90.0",
|
||||
"sass": "^1.92.0",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"styled-components": "^6.1.19",
|
||||
"subscriptions-transport-ws": "^0.11.0",
|
||||
@@ -110,7 +111,6 @@
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"globals": "^15.15.0",
|
||||
"jsdom": "^26.0.0",
|
||||
"lightningcss": "^1.30.1",
|
||||
"memfs": "^4.36.3",
|
||||
"os-browserify": "^0.3.0",
|
||||
"playwright": "^1.55.0",
|
||||
@@ -141,28 +141,28 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@amplitude/analytics-browser": {
|
||||
"version": "2.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/analytics-browser/-/analytics-browser-2.23.1.tgz",
|
||||
"integrity": "sha512-TYsh7ORT9UoEF3JpmWVpyyRyeE4k8SS+6TNgEoCRj4ZtjiiWKP1CE7lEspgVBjWdSCUqS1o85Cte7c2mkj+SiA==",
|
||||
"version": "2.23.5",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/analytics-browser/-/analytics-browser-2.23.5.tgz",
|
||||
"integrity": "sha512-R1N506rifI3/axSTM3EQkVjCgeJsmhybRONOdnA3MCJwOIC77UVEOIzTVNjnAAzgBSxDNTCy6ejGgBf3PgzBog==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@amplitude/analytics-core": "^2.21.1",
|
||||
"@amplitude/analytics-core": "^2.22.1",
|
||||
"@amplitude/analytics-remote-config": "^0.4.0",
|
||||
"@amplitude/plugin-autocapture-browser": "^1.10.1",
|
||||
"@amplitude/plugin-network-capture-browser": "^1.5.1",
|
||||
"@amplitude/plugin-page-view-tracking-browser": "^2.3.42",
|
||||
"@amplitude/plugin-web-vitals-browser": "^0.1.0-beta.17",
|
||||
"@amplitude/plugin-autocapture-browser": "^1.11.1",
|
||||
"@amplitude/plugin-network-capture-browser": "^1.5.4",
|
||||
"@amplitude/plugin-page-view-tracking-browser": "^2.3.45",
|
||||
"@amplitude/plugin-web-vitals-browser": "^0.1.0-beta.20",
|
||||
"tslib": "^2.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@amplitude/analytics-client-common": {
|
||||
"version": "2.3.36",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/analytics-client-common/-/analytics-client-common-2.3.36.tgz",
|
||||
"integrity": "sha512-4MmuUuX8V9HOCrZ3VMQ3v3lkdksKQxswsO6mpm4YJvznty16+AaaupajubHik5GmmK8MV89ZqG0yLQLKiQm4yg==",
|
||||
"version": "2.3.39",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/analytics-client-common/-/analytics-client-common-2.3.39.tgz",
|
||||
"integrity": "sha512-Dt31IIalME8whTXLgnKPLh9HbHTr8dC9F51reS1gngXAkOTErzAvbBl6UIc09bjqHWmimsRYgi6nflubnqwvMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@amplitude/analytics-connector": "^1.4.8",
|
||||
"@amplitude/analytics-core": "^2.21.1",
|
||||
"@amplitude/analytics-core": "^2.22.1",
|
||||
"@amplitude/analytics-types": "^2.10.0",
|
||||
"tslib": "^2.4.1"
|
||||
}
|
||||
@@ -174,9 +174,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@amplitude/analytics-core": {
|
||||
"version": "2.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/analytics-core/-/analytics-core-2.21.1.tgz",
|
||||
"integrity": "sha512-4lfjUDl4VF4H+O9uZJsf6hlmOlVte+CJI45i8gV8vh9jUEn0/Ad3Cyeu2D9p2dUtLUgKVcXglqkoSpxPzhGWFw==",
|
||||
"version": "2.22.1",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/analytics-core/-/analytics-core-2.22.1.tgz",
|
||||
"integrity": "sha512-nzlulhS7jYQc91wOc392avBLDAiPZmIBuJ1apA640YlleX/egVxKgZVYHH3Ge4ZNkaxoESwUb4mf2R+ZI0fXxA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@amplitude/analytics-connector": "^1.6.4",
|
||||
@@ -202,12 +202,12 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@amplitude/plugin-autocapture-browser": {
|
||||
"version": "1.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/plugin-autocapture-browser/-/plugin-autocapture-browser-1.10.1.tgz",
|
||||
"integrity": "sha512-fLsad4xnxkiZ62mEFxze5SgNyxbc6qk7FMlzUPCpgkPhdbJkiogajTonEnRi+p5HU2Ze8K242gsfnR66xLEU1Q==",
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/plugin-autocapture-browser/-/plugin-autocapture-browser-1.11.1.tgz",
|
||||
"integrity": "sha512-6nus1nXlH1ru/yjx07yk1cyjc9scAsE9dO4f0xxH8xpHlYQ4yVCuYApcguIpogISlPiySAxSZ+4WDreLrpQiDw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@amplitude/analytics-core": "^2.21.1",
|
||||
"@amplitude/analytics-core": "^2.22.1",
|
||||
"@amplitude/analytics-remote-config": "^0.6.3",
|
||||
"rxjs": "^7.8.1",
|
||||
"tslib": "^2.4.1"
|
||||
@@ -241,23 +241,23 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@amplitude/plugin-network-capture-browser": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/plugin-network-capture-browser/-/plugin-network-capture-browser-1.5.1.tgz",
|
||||
"integrity": "sha512-45KD4wo+7dfFIi3Q7w3u6x3R9FQdYifSZPyDG02V7YYdOjmRFC0K4Jzx0fpmbYqsl4BQDwe4q2DC6eDPKYDn3A==",
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/plugin-network-capture-browser/-/plugin-network-capture-browser-1.5.4.tgz",
|
||||
"integrity": "sha512-GRvi44tNx2TdHQ/dnC9DLqwsaBE1gC/bmHNaudTbp/nwIM8nVCAxZaXaXJEUouK7WBAamr7a3WmFruecqCeOlA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@amplitude/analytics-core": "^2.21.1",
|
||||
"@amplitude/analytics-core": "^2.22.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"tslib": "^2.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@amplitude/plugin-page-view-tracking-browser": {
|
||||
"version": "2.3.42",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/plugin-page-view-tracking-browser/-/plugin-page-view-tracking-browser-2.3.42.tgz",
|
||||
"integrity": "sha512-MSO5hOSXdPXAUSW3vFqUz08/MrAfzn4TU1uyYL0q1MZz63bEwxppVaMnwgx1NfkyYf4zlWn0KZ6PREhXeWL0YA==",
|
||||
"version": "2.3.45",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/plugin-page-view-tracking-browser/-/plugin-page-view-tracking-browser-2.3.45.tgz",
|
||||
"integrity": "sha512-L2JH/TDTdjfexkY5hHVS3dCb4+q5H1jeIKhXUcBQ/Wx91asLY9BsH91J4bo9EK4J4Al8jVRwqJz0tIQ17qW9RQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@amplitude/analytics-client-common": "^2.3.36",
|
||||
"@amplitude/analytics-client-common": "^2.3.39",
|
||||
"@amplitude/analytics-types": "^2.10.0",
|
||||
"tslib": "^2.4.1"
|
||||
}
|
||||
@@ -2534,9 +2534,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@emotion/is-prop-valid": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.1.tgz",
|
||||
"integrity": "sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==",
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz",
|
||||
"integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@emotion/memoize": "^0.9.0"
|
||||
@@ -3280,9 +3280,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@firebase/app": {
|
||||
"version": "0.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.1.tgz",
|
||||
"integrity": "sha512-jxTrDbxnGoX7cGz7aP9E7v9iKvBbQfZ8Gz4TH3SfrrkcyIojJM3+hJnlbGnGxHrABts844AxRcg00arMZEyA6Q==",
|
||||
"version": "0.14.2",
|
||||
"resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.2.tgz",
|
||||
"integrity": "sha512-Ecx2ig/JLC9ayIQwZHqm41Tzlf4c1WUuFhFUZB1y+JIJqDRE579x7Uil7tKT8MwDpOPwrK5ZtpxdSsrfy/LF8Q==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@firebase/component": "0.7.0",
|
||||
@@ -3333,9 +3333,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@firebase/firestore": {
|
||||
"version": "4.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.9.0.tgz",
|
||||
"integrity": "sha512-5zl0+/h1GvlCSLt06RMwqFsd7uqRtnNZt4sW99k2rKRd6k/ECObIWlEnvthm2cuOSnUmwZknFqtmd1qyYSLUuQ==",
|
||||
"version": "4.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.9.1.tgz",
|
||||
"integrity": "sha512-PYVUTkhC9y8pydrqC3O1Oc4AMfkGSWdmuH9xgPJjiEbpUIUPQ4J8wJhyuash+o2u+axmyNRFP8ULNUKb+WzBzQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@firebase/component": "0.7.0",
|
||||
@@ -4090,6 +4090,12 @@
|
||||
"url": "https://opencollective.com/popperjs"
|
||||
}
|
||||
},
|
||||
"node_modules/@posthog/core": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.0.2.tgz",
|
||||
"integrity": "sha512-hWk3rUtJl2crQK0WNmwg13n82hnTwB99BT99/XI5gZSvIlYZ1TPmMZE8H2dhJJ98J/rm9vYJ/UXNzw3RV5HTpQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@protobufjs/aspromise": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
|
||||
@@ -4387,9 +4393,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@reduxjs/toolkit": {
|
||||
"version": "2.8.2",
|
||||
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.8.2.tgz",
|
||||
"integrity": "sha512-MYlOhQ0sLdw4ud48FoC5w0dH9VfWQjtCjreKwYTT3l+r427qYC5Y8PihNutepr8XrNaBUDQo9khWUwQxZaqt5A==",
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.9.0.tgz",
|
||||
"integrity": "sha512-fSfQlSRu9Z5yBkvsNhYF2rPS8cGXn/TZVrlwN1948QyZ8xMZ0JvP50S2acZNaf+o63u6aEeMjipFyksjIcWrog==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.0.0",
|
||||
@@ -5055,6 +5061,7 @@
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-4.1.1.tgz",
|
||||
"integrity": "sha512-HUpqrCK7zDVojTV6KL6BO9ZZiYrEYQqvYQrscyMsq04z+WCupXaH6YEliiNRvreR8DBJgdsG3lBRpebhUGmvfA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
@@ -5080,6 +5087,7 @@
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/bundler-plugin-core/-/bundler-plugin-core-4.1.1.tgz",
|
||||
"integrity": "sha512-Hx9RgXaD1HEYmL5aYoWwCKkVvPp4iklwfD9mvmdpQtcwLg6b6oLnPVDQaOry1ak6Pxt8smlrWcKy4IiKASlvig==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.18.5",
|
||||
@@ -5099,6 +5107,7 @@
|
||||
"version": "16.6.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
||||
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -5108,9 +5117,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/cli": {
|
||||
"version": "2.52.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.52.0.tgz",
|
||||
"integrity": "sha512-PXyo7Yv7+rVMSBGZfI/eFEzzhiKedTs25sDCjz4a3goAZ/F5R5tn3MKq30pnze5wNnoQmLujAa0uUjfNcWP+uQ==",
|
||||
"version": "2.53.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.53.0.tgz",
|
||||
"integrity": "sha512-n2ZNb+5Z6AZKQSI0SusQ7ZzFL637mfw3Xh4C3PEyVSn9LiF683fX0TTq8OeGmNZQS4maYfS95IFD+XpydU0dEA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
@@ -5127,20 +5136,20 @@
|
||||
"node": ">= 10"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@sentry/cli-darwin": "2.52.0",
|
||||
"@sentry/cli-linux-arm": "2.52.0",
|
||||
"@sentry/cli-linux-arm64": "2.52.0",
|
||||
"@sentry/cli-linux-i686": "2.52.0",
|
||||
"@sentry/cli-linux-x64": "2.52.0",
|
||||
"@sentry/cli-win32-arm64": "2.52.0",
|
||||
"@sentry/cli-win32-i686": "2.52.0",
|
||||
"@sentry/cli-win32-x64": "2.52.0"
|
||||
"@sentry/cli-darwin": "2.53.0",
|
||||
"@sentry/cli-linux-arm": "2.53.0",
|
||||
"@sentry/cli-linux-arm64": "2.53.0",
|
||||
"@sentry/cli-linux-i686": "2.53.0",
|
||||
"@sentry/cli-linux-x64": "2.53.0",
|
||||
"@sentry/cli-win32-arm64": "2.53.0",
|
||||
"@sentry/cli-win32-i686": "2.53.0",
|
||||
"@sentry/cli-win32-x64": "2.53.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/cli-darwin": {
|
||||
"version": "2.52.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.52.0.tgz",
|
||||
"integrity": "sha512-ieQs/p4yTHT27nBzy0wtAb8BSISfWlpXdgsACcwXimYa36NJRwyCqgOXUaH/BYiTdwWSHpuANbUHGJW6zljzxw==",
|
||||
"version": "2.53.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.53.0.tgz",
|
||||
"integrity": "sha512-NNPfpILMwKgpHiyJubHHuauMKltkrgLQ5tvMdxNpxY60jBNdo5VJtpESp4XmXlnidzV4j1z61V4ozU6ttDgt5Q==",
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -5151,9 +5160,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/cli-linux-arm": {
|
||||
"version": "2.52.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.52.0.tgz",
|
||||
"integrity": "sha512-tWMLU+hj+iip5Akx+S76biAOE1eMMWTDq8c0MqMv/ahHgb6/HiVngMcUsp59Oz3EczJGbTkcnS3vRTDodEcMDw==",
|
||||
"version": "2.53.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.53.0.tgz",
|
||||
"integrity": "sha512-NdRzQ15Ht83qG0/Lyu11ciy/Hu/oXbbtJUgwzACc7bWvHQA8xEwTsehWexqn1529Kfc5EjuZ0Wmj3MHmp+jOWw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -5169,9 +5178,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/cli-linux-arm64": {
|
||||
"version": "2.52.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.52.0.tgz",
|
||||
"integrity": "sha512-RxT5uzxjCkcvplmx0bavJIEYerRex2Rg/2RAVBdVvWLKFOcmeerTn/VVxPZVuDIVMVyjlZsteWPYwfUm+Ia3wQ==",
|
||||
"version": "2.53.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.53.0.tgz",
|
||||
"integrity": "sha512-xY/CZ1dVazsSCvTXzKpAgXaRqfljVfdrFaYZRUaRPf1ZJRGa3dcrivoOhSIeG/p5NdYtMvslMPY9Gm2MT0M83A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -5187,9 +5196,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/cli-linux-i686": {
|
||||
"version": "2.52.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.52.0.tgz",
|
||||
"integrity": "sha512-sKcJmIg7QWFtlNU5Bs5OZprwdIzzyYMRpFkWioPZ4TE82yvP1+2SAX31VPUlTx+7NLU6YVEWNwvSxh8LWb7iOw==",
|
||||
"version": "2.53.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.53.0.tgz",
|
||||
"integrity": "sha512-0REmBibGAB4jtqt9S6JEsFF4QybzcXHPcHtJjgMi5T0ueh952uG9wLzjSxQErCsxTKF+fL8oG0Oz5yKBuCwCCQ==",
|
||||
"cpu": [
|
||||
"x86",
|
||||
"ia32"
|
||||
@@ -5206,9 +5215,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/cli-linux-x64": {
|
||||
"version": "2.52.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.52.0.tgz",
|
||||
"integrity": "sha512-aPZ7bP02zGkuEqTiOAm4np/ggfgtzrq4ti1Xze96Csi/DV3820SCfLrPlsvcvnqq7x69IL9cI3kXjdEpgrfGxw==",
|
||||
"version": "2.53.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.53.0.tgz",
|
||||
"integrity": "sha512-9UGJL+Vy5N/YL1EWPZ/dyXLkShlNaDNrzxx4G7mTS9ywjg+BIuemo6rnN7w43K1NOjObTVO6zY0FwumJ1pCyLg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -5224,9 +5233,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/cli-win32-arm64": {
|
||||
"version": "2.52.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.52.0.tgz",
|
||||
"integrity": "sha512-90hrB5XdwJVhRpCmVrEcYoKW8nl5/V9OfVvOGeKUPvUkApLzvsInK74FYBZEVyAn1i/NdUv+Xk9q2zqUGK1aLQ==",
|
||||
"version": "2.53.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.53.0.tgz",
|
||||
"integrity": "sha512-G1kjOjrjMBY20rQcJV2GA8KQE74ufmROCDb2GXYRfjvb1fKAsm4Oh8N5+Tqi7xEHdjQoLPkE4CNW0aH68JSUDQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -5240,9 +5249,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/cli-win32-i686": {
|
||||
"version": "2.52.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.52.0.tgz",
|
||||
"integrity": "sha512-HXlSE4CaLylNrELx4KVmOQjV5bURCNuky6sjCWiTH7HyDqHEak2Rk8iLE0JNLj5RETWMvmaZnZZFfmyGlY1opg==",
|
||||
"version": "2.53.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.53.0.tgz",
|
||||
"integrity": "sha512-qbGTZUzesuUaPtY9rPXdNfwLqOZKXrJRC1zUFn52hdo6B+Dmv0m/AHwRVFHZP53Tg1NCa8bDei2K/uzRN0dUZw==",
|
||||
"cpu": [
|
||||
"x86",
|
||||
"ia32"
|
||||
@@ -5257,9 +5266,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/cli-win32-x64": {
|
||||
"version": "2.52.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.52.0.tgz",
|
||||
"integrity": "sha512-hJT0C3FwHk1Mt9oFqcci88wbO1D+yAWUL8J29HEGM5ZAqlhdh7sAtPDIC3P2LceUJOjnXihow47Bkj62juatIQ==",
|
||||
"version": "2.53.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.53.0.tgz",
|
||||
"integrity": "sha512-1TXYxYHtwgUq5KAJt3erRzzUtPqg7BlH9T7MdSPHjJatkrr/kwZqnVe2H6Arr/5NH891vOlIeSPHBdgJUAD69g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -5320,18 +5329,58 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/vite-plugin": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/vite-plugin/-/vite-plugin-4.1.1.tgz",
|
||||
"integrity": "sha512-kNIZiqRbFHJHzV0QF1RyuwMprwK2Lk354qs98P7DduU1TkzrNG3+2f8liYJaiYCrsjDvJlPHyVFBDF9IRhJGdA==",
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/vite-plugin/-/vite-plugin-4.3.0.tgz",
|
||||
"integrity": "sha512-MeTAHMmTOgBPMAjeW7/ONyXwgScZdaFFtNiALKcAODnVqC7eoHdSRIWeH5mkLr2Dvs7nqtBaDpKxRjUBgfm9LQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry/bundler-plugin-core": "4.1.1",
|
||||
"@sentry/bundler-plugin-core": "4.3.0",
|
||||
"unplugin": "1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/vite-plugin/node_modules/@sentry/babel-plugin-component-annotate": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-4.3.0.tgz",
|
||||
"integrity": "sha512-OuxqBprXRyhe8Pkfyz/4yHQJc5c3lm+TmYWSSx8u48g5yKewSQDOxkiLU5pAk3WnbLPy8XwU/PN+2BG0YFU9Nw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/vite-plugin/node_modules/@sentry/bundler-plugin-core": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/bundler-plugin-core/-/bundler-plugin-core-4.3.0.tgz",
|
||||
"integrity": "sha512-dmR4DJhJ4jqVWGWppuTL2blNFqOZZnt4aLkewbD1myFG3KVfUx8CrMQWEmGjkgPOtj5TO6xH9PyTJjXC6o5tnA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.18.5",
|
||||
"@sentry/babel-plugin-component-annotate": "4.3.0",
|
||||
"@sentry/cli": "^2.51.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"find-up": "^5.0.0",
|
||||
"glob": "^9.3.2",
|
||||
"magic-string": "0.30.8",
|
||||
"unplugin": "1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/vite-plugin/node_modules/dotenv": {
|
||||
"version": "16.6.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
||||
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/webpack-plugin": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/webpack-plugin/-/webpack-plugin-4.1.1.tgz",
|
||||
@@ -6148,9 +6197,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/antd": {
|
||||
"version": "5.27.1",
|
||||
"resolved": "https://registry.npmjs.org/antd/-/antd-5.27.1.tgz",
|
||||
"integrity": "sha512-jGMSdBN7hAMvPV27B4RhzZfL6n6yu8yDbo7oXrlJasaOqB7bSDPcjdEy1kXy3JPsny/Qazb1ykzRI4EfcByAPQ==",
|
||||
"version": "5.27.3",
|
||||
"resolved": "https://registry.npmjs.org/antd/-/antd-5.27.3.tgz",
|
||||
"integrity": "sha512-Jewp1ek1iyqoAyjWyPgzc2kioZ+7S3jh39a+tld/j4ucnuf/cBk4omfyIdhLz49pVNsaEcRp5LtJOSQPFwPgpA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ant-design/colors": "^7.2.1",
|
||||
@@ -6192,7 +6241,7 @@
|
||||
"rc-slider": "~11.1.8",
|
||||
"rc-steps": "~6.0.1",
|
||||
"rc-switch": "~4.1.0",
|
||||
"rc-table": "~7.51.1",
|
||||
"rc-table": "~7.52.6",
|
||||
"rc-tabs": "~15.7.0",
|
||||
"rc-textarea": "~1.10.2",
|
||||
"rc-tooltip": "~6.4.0",
|
||||
@@ -7897,9 +7946,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dayjs": {
|
||||
"version": "1.11.13",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
|
||||
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
|
||||
"version": "1.11.18",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz",
|
||||
"integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dayjs-business-days2": {
|
||||
@@ -8074,7 +8123,6 @@
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
|
||||
"integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -8197,9 +8245,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "17.2.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.1.tgz",
|
||||
"integrity": "sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==",
|
||||
"version": "17.2.2",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.2.tgz",
|
||||
"integrity": "sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -9846,9 +9894,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/i18next": {
|
||||
"version": "25.4.0",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.4.0.tgz",
|
||||
"integrity": "sha512-UH5aiamXsO3cfrZFurCHiB6YSs3C+s+XY9UaJllMMSbmaoXILxFgqDEZu4NbfzJFjmUo3BNMa++Rjkr3ofjfLw==",
|
||||
"version": "25.5.2",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.5.2.tgz",
|
||||
"integrity": "sha512-lW8Zeh37i/o0zVr+NoCHfNnfvVw+M6FQbRp36ZZ/NyHDJ3NJVpp2HhAUyU9WafL5AssymNoOjMRB48mmx2P6Hw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -10992,16 +11040,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/libphonenumber-js": {
|
||||
"version": "1.12.13",
|
||||
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.13.tgz",
|
||||
"integrity": "sha512-QZXnR/OGiDcBjF4hGk0wwVrPcZvbSSyzlvkjXv5LFfktj7O2VZDrt4Xs8SgR/vOFco+qk1i8J43ikMXZoTrtPw==",
|
||||
"version": "1.12.15",
|
||||
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.15.tgz",
|
||||
"integrity": "sha512-TMDCtIhWUDHh91wRC+wFuGlIzKdPzaTUHHVrIZ3vPUEoNaXFLrsIQ1ZpAeZeXApIF6rvDksMTvjrIQlLKaYxqQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lightningcss": {
|
||||
"version": "1.30.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
|
||||
"integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==",
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.3"
|
||||
@@ -11033,7 +11080,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -11054,7 +11100,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -11075,7 +11120,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -11096,7 +11140,6 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -11117,7 +11160,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -11138,7 +11180,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -11159,7 +11200,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -11180,7 +11220,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -11201,7 +11240,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -11222,7 +11260,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -13152,11 +13189,12 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/posthog-js": {
|
||||
"version": "1.260.2",
|
||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.260.2.tgz",
|
||||
"integrity": "sha512-2Q+QUz9j9+uG16wp0WcOEbezVsLZCobZyTX8NvWPMGKyPaf2lOsjbPjznsq5JiIt324B6NAqzpWYZTzvhn9k9Q==",
|
||||
"version": "1.261.7",
|
||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.261.7.tgz",
|
||||
"integrity": "sha512-Fjpbz6VfIMsEbKIN/UyTWhU1DGgVIngqoRjPGRolemIMOVzTfI77OZq8WwiBhMug+rU+wNhGCQhC41qRlR5CxA==",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"dependencies": {
|
||||
"@posthog/core": "1.0.2",
|
||||
"core-js": "^3.38.1",
|
||||
"fflate": "^0.4.8",
|
||||
"preact": "^10.19.3",
|
||||
@@ -13892,9 +13930,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rc-table": {
|
||||
"version": "7.51.1",
|
||||
"resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.51.1.tgz",
|
||||
"integrity": "sha512-5iq15mTHhvC42TlBLRCoCBLoCmGlbRZAlyF21FonFnS/DIC8DeRqnmdyVREwt2CFbPceM0zSNdEeVfiGaqYsKw==",
|
||||
"version": "7.52.7",
|
||||
"resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.52.7.tgz",
|
||||
"integrity": "sha512-yuZfnTpuHwRa4JH+F28wQfGeDzqtgIDvLBBJk5sFncXQjTExhtBNc6dPfVo5pL5SjabJEoejefs6wsrAKfhDoQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.10.1",
|
||||
@@ -14204,16 +14242,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-i18next": {
|
||||
"version": "15.7.1",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.7.1.tgz",
|
||||
"integrity": "sha512-o4VsKh30fy7p0z5ACHuyWqB6xu9WpQIQy2/ZcbCqopNnrnTVOPn/nAv9uYP4xYAWg99QMpvZ9Bu/si3eGurzGw==",
|
||||
"version": "15.7.3",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.7.3.tgz",
|
||||
"integrity": "sha512-AANws4tOE+QSq/IeMF/ncoHlMNZaVLxpa5uUGW1wjike68elVYr0018L9xYoqBr1OFO7G7boDPrbn0HpMCJxTw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6",
|
||||
"html-parse-stringify": "^3.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"i18next": ">= 23.4.0",
|
||||
"i18next": ">= 25.4.1",
|
||||
"react": ">= 16.8.0",
|
||||
"typescript": "^5"
|
||||
},
|
||||
@@ -15134,9 +15172,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.90.0",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.90.0.tgz",
|
||||
"integrity": "sha512-9GUyuksjw70uNpb1MTYWsH9MQHOHY6kwfnkafC24+7aOMZn9+rVMBxRbLvw756mrBFbIsFg6Xw9IkR2Fnn3k+Q==",
|
||||
"version": "1.92.0",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.92.0.tgz",
|
||||
"integrity": "sha512-KDNI0BxgIRDAfJgzNm5wuy+4yOCIZyrUbjSpiU/JItfih+KGXAVefKL53MTml054MmBA3DDKIBMSI/7XLxZJ3A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.0",
|
||||
|
||||
@@ -8,48 +8,48 @@
|
||||
"private": true,
|
||||
"proxy": "http://localhost:4000",
|
||||
"dependencies": {
|
||||
"@amplitude/analytics-browser": "^2.23.1",
|
||||
"@amplitude/analytics-browser": "^2.23.5",
|
||||
"@ant-design/pro-layout": "^7.22.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",
|
||||
"@firebase/analytics": "^0.10.17",
|
||||
"@firebase/app": "^0.14.1",
|
||||
"@firebase/app": "^0.14.2",
|
||||
"@firebase/auth": "^1.10.8",
|
||||
"@firebase/firestore": "^4.8.0",
|
||||
"@firebase/firestore": "^4.9.1",
|
||||
"@firebase/messaging": "^0.12.22",
|
||||
"@jsreport/browser-client": "^3.1.0",
|
||||
"@reduxjs/toolkit": "^2.8.2",
|
||||
"@sentry/cli": "^2.52.0",
|
||||
"@reduxjs/toolkit": "^2.9.0",
|
||||
"@sentry/cli": "^2.53.0",
|
||||
"@sentry/react": "^9.43.0",
|
||||
"@sentry/vite-plugin": "^4.1.1",
|
||||
"@sentry/vite-plugin": "^4.3.0",
|
||||
"@splitsoftware/splitio-react": "^2.3.1",
|
||||
"@tanem/react-nprogress": "^5.0.53",
|
||||
"antd": "^5.27.1",
|
||||
"antd": "^5.27.3",
|
||||
"apollo-link-logger": "^2.0.1",
|
||||
"apollo-link-sentry": "^4.4.0",
|
||||
"autosize": "^6.0.1",
|
||||
"axios": "^1.11.0",
|
||||
"classnames": "^2.5.1",
|
||||
"css-box-model": "^1.2.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"dayjs": "^1.11.18",
|
||||
"dayjs-business-days2": "^1.3.0",
|
||||
"dinero.js": "^1.9.1",
|
||||
"dotenv": "^17.2.1",
|
||||
"dotenv": "^17.2.2",
|
||||
"env-cmd": "^10.1.0",
|
||||
"exifr": "^7.1.3",
|
||||
"graphql": "^16.11.0",
|
||||
"i18next": "^25.4.0",
|
||||
"i18next": "^25.5.2",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"immutability-helper": "^3.1.1",
|
||||
"libphonenumber-js": "^1.12.13",
|
||||
"libphonenumber-js": "^1.12.15",
|
||||
"logrocket": "^9.0.2",
|
||||
"markerjs2": "^2.32.6",
|
||||
"memoize-one": "^6.0.0",
|
||||
"normalize-url": "^8.0.2",
|
||||
"object-hash": "^3.0.0",
|
||||
"phone": "^3.1.67",
|
||||
"posthog-js": "^1.260.2",
|
||||
"posthog-js": "^1.261.7",
|
||||
"prop-types": "^15.8.1",
|
||||
"query-string": "^9.2.2",
|
||||
"raf-schd": "^4.0.3",
|
||||
@@ -57,11 +57,12 @@
|
||||
"react-big-calendar": "^1.19.4",
|
||||
"react-color": "^2.19.3",
|
||||
"react-cookie": "^8.0.1",
|
||||
"lightningcss": "^1.30.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-drag-listview": "^2.0.0",
|
||||
"react-grid-gallery": "^1.0.1",
|
||||
"react-grid-layout": "1.3.4",
|
||||
"react-i18next": "^15.7.1",
|
||||
"react-i18next": "^15.7.3",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-image-lightbox": "^5.1.4",
|
||||
"react-markdown": "^10.1.0",
|
||||
@@ -80,7 +81,7 @@
|
||||
"redux-saga": "^1.3.0",
|
||||
"redux-state-sync": "^3.1.4",
|
||||
"reselect": "^5.1.1",
|
||||
"sass": "^1.90.0",
|
||||
"sass": "^1.92.0",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"styled-components": "^6.1.19",
|
||||
"subscriptions-transport-ws": "^0.11.0",
|
||||
@@ -152,7 +153,6 @@
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"globals": "^15.15.0",
|
||||
"jsdom": "^26.0.0",
|
||||
"lightningcss": "^1.30.1",
|
||||
"memfs": "^4.36.3",
|
||||
"os-browserify": "^0.3.0",
|
||||
"playwright": "^1.55.0",
|
||||
|
||||
@@ -24,6 +24,7 @@ import InstanceRenderMgr from "../utils/instanceRenderMgr";
|
||||
import ProductFruitsWrapper from "./ProductFruitsWrapper.jsx";
|
||||
import { NotificationProvider } from "../contexts/Notifications/notificationContext.jsx";
|
||||
import SocketProvider from "../contexts/SocketIO/socketProvider.jsx";
|
||||
import SoundWrapper from "./SoundWrapper.jsx";
|
||||
|
||||
const ResetPassword = lazy(() => import("../pages/reset-password/reset-password.component"));
|
||||
const ManagePage = lazy(() => import("../pages/manage/manage.page.container"));
|
||||
@@ -72,9 +73,6 @@ export function App({
|
||||
setIsPartsEntry(isParts);
|
||||
}, [setIsPartsEntry]);
|
||||
|
||||
//const b = Grid.useBreakpoint();
|
||||
// console.log("Breakpoints:", b);
|
||||
|
||||
// Associate event listeners, memoize to prevent multiple listeners being added
|
||||
useEffect(() => {
|
||||
const offlineListener = () => {
|
||||
@@ -164,87 +162,87 @@ export function App({
|
||||
/>
|
||||
|
||||
<NotificationProvider>
|
||||
<Routes>
|
||||
<Route
|
||||
path="*"
|
||||
element={
|
||||
<ErrorBoundary>
|
||||
<LandingPage />
|
||||
</ErrorBoundary>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/signin"
|
||||
element={
|
||||
<ErrorBoundary>
|
||||
<SignInPage />
|
||||
</ErrorBoundary>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/resetpassword"
|
||||
element={
|
||||
<ErrorBoundary>
|
||||
<ResetPassword />
|
||||
</ErrorBoundary>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/csi/:surveyId"
|
||||
element={
|
||||
<ErrorBoundary>
|
||||
<CsiPage />
|
||||
</ErrorBoundary>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/disclaimer"
|
||||
element={
|
||||
<ErrorBoundary>
|
||||
<DisclaimerPage />
|
||||
</ErrorBoundary>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/manage/*"
|
||||
element={
|
||||
<ErrorBoundary>
|
||||
<SocketProvider bodyshop={bodyshop} navigate={navigate} currentUser={currentUser}>
|
||||
<SoundWrapper bodyshop={bodyshop}>
|
||||
<Routes>
|
||||
<Route
|
||||
path="*"
|
||||
element={
|
||||
<ErrorBoundary>
|
||||
<LandingPage />
|
||||
</ErrorBoundary>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/signin"
|
||||
element={
|
||||
<ErrorBoundary>
|
||||
<SignInPage />
|
||||
</ErrorBoundary>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/resetpassword"
|
||||
element={
|
||||
<ErrorBoundary>
|
||||
<ResetPassword />
|
||||
</ErrorBoundary>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/csi/:surveyId"
|
||||
element={
|
||||
<ErrorBoundary>
|
||||
<CsiPage />
|
||||
</ErrorBoundary>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/disclaimer"
|
||||
element={
|
||||
<ErrorBoundary>
|
||||
<DisclaimerPage />
|
||||
</ErrorBoundary>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/manage/*"
|
||||
element={
|
||||
<ErrorBoundary>
|
||||
<SocketProvider bodyshop={bodyshop} navigate={navigate} currentUser={currentUser}>
|
||||
<PrivateRoute isAuthorized={currentUser.authorized} />
|
||||
</SocketProvider>
|
||||
</ErrorBoundary>
|
||||
}
|
||||
>
|
||||
<Route path="*" element={<ManagePage />} />
|
||||
</Route>
|
||||
<Route
|
||||
path="/tech/*"
|
||||
element={
|
||||
<ErrorBoundary>
|
||||
<SocketProvider bodyshop={bodyshop} navigate={navigate} currentUser={currentUser}>
|
||||
<PrivateRoute isAuthorized={currentUser.authorized} />
|
||||
</SocketProvider>
|
||||
</ErrorBoundary>
|
||||
}
|
||||
>
|
||||
<Route path="*" element={<TechPageContainer />} />
|
||||
</Route>
|
||||
<Route
|
||||
path="/parts/*"
|
||||
element={
|
||||
<ErrorBoundary>
|
||||
<PrivateRoute isAuthorized={currentUser.authorized} />
|
||||
</SocketProvider>
|
||||
</ErrorBoundary>
|
||||
}
|
||||
>
|
||||
<Route path="*" element={<ManagePage />} />
|
||||
</Route>
|
||||
<Route
|
||||
path="/tech/*"
|
||||
element={
|
||||
<ErrorBoundary>
|
||||
<SocketProvider bodyshop={bodyshop} navigate={navigate} currentUser={currentUser}>
|
||||
<PrivateRoute isAuthorized={currentUser.authorized} />
|
||||
</SocketProvider>
|
||||
</ErrorBoundary>
|
||||
}
|
||||
>
|
||||
<Route path="*" element={<TechPageContainer />} />
|
||||
</Route>
|
||||
<Route
|
||||
path="/parts/*"
|
||||
element={
|
||||
<ErrorBoundary>
|
||||
<SocketProvider bodyshop={bodyshop} navigate={navigate} currentUser={currentUser}>
|
||||
<PrivateRoute isAuthorized={currentUser.authorized} />
|
||||
</SocketProvider>
|
||||
</ErrorBoundary>
|
||||
}
|
||||
>
|
||||
<Route path="*" element={<SimplifiedPartsPageContainer />} />
|
||||
</Route>
|
||||
<Route path="/edit/*" element={<PrivateRoute isAuthorized={currentUser.authorized} />}>
|
||||
<Route path="*" element={<DocumentEditorContainer />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</ErrorBoundary>
|
||||
}
|
||||
>
|
||||
<Route path="*" element={<SimplifiedPartsPageContainer />} />
|
||||
</Route>
|
||||
<Route path="/edit/*" element={<PrivateRoute isAuthorized={currentUser.authorized} />}>
|
||||
<Route path="*" element={<DocumentEditorContainer />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</SoundWrapper>
|
||||
</NotificationProvider>
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
@@ -272,23 +272,23 @@
|
||||
}
|
||||
|
||||
// Scrollbar styles (uncomment if needed, updated for dark mode)
|
||||
::-webkit-scrollbar-track {
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
||||
border-radius: 0.2rem;
|
||||
background-color: var(--table-stripe-bg);
|
||||
}
|
||||
// ::-webkit-scrollbar-track {
|
||||
// -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
||||
// border-radius: 0.2rem;
|
||||
// background-color: var(--table-stripe-bg);
|
||||
// }
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 0.25rem;
|
||||
max-height: 0.25rem;
|
||||
background-color: var(--table-stripe-bg);
|
||||
}
|
||||
// ::-webkit-scrollbar {
|
||||
// width: 0.25rem;
|
||||
// max-height: 0.25rem;
|
||||
// background-color: var(--table-stripe-bg);
|
||||
// }
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
border-radius: 0.2rem;
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
||||
background-color: var(--alert-color);
|
||||
}
|
||||
// ::-webkit-scrollbar-thumb {
|
||||
// border-radius: 0.2rem;
|
||||
// -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
||||
// background-color: var(--alert-color);
|
||||
// }
|
||||
|
||||
.ant-input-number-input,
|
||||
.ant-input-number,
|
||||
|
||||
43
client/src/App/SoundWrapper.jsx
Normal file
43
client/src/App/SoundWrapper.jsx
Normal file
@@ -0,0 +1,43 @@
|
||||
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}</>;
|
||||
}
|
||||
@@ -142,7 +142,16 @@ export function AccountingReceivablesTableComponent({ bodyshop, loading, jobs, r
|
||||
refetch={refetch}
|
||||
/>
|
||||
<Link to={`/manage/jobs/${record.id}/close`}>
|
||||
<Button>{t("jobs.labels.viewallocations")}</Button>
|
||||
<Button
|
||||
style={{
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
verticalAlign: "middle"
|
||||
}}
|
||||
>
|
||||
{t("jobs.labels.viewallocations")}
|
||||
</Button>
|
||||
</Link>
|
||||
</Space>
|
||||
)
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { FaTasks } from "react-icons/fa";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
@@ -75,6 +76,7 @@ export function BillsListTableComponent({
|
||||
<Button
|
||||
title={t("tasks.buttons.create")}
|
||||
onClick={() => {
|
||||
logImEXEvent("bills_create_task", {});
|
||||
setTaskUpsertContext({
|
||||
context: {
|
||||
jobid: job.id,
|
||||
@@ -109,6 +111,13 @@ export function BillsListTableComponent({
|
||||
key: "vendorname",
|
||||
sorter: (a, b) => alphaSort(a.vendor.name, b.vendor.name),
|
||||
sortOrder: state.sortedInfo.columnKey === "vendorname" && state.sortedInfo.order,
|
||||
filters: bills
|
||||
? [...new Set(bills.map((bill) => bill.vendor.name))].map((name) => ({
|
||||
text: name,
|
||||
value: name
|
||||
}))
|
||||
: [],
|
||||
onFilter: (value, record) => record.vendor.name === value,
|
||||
render: (text, record) => <span>{record.vendor.name}</span>
|
||||
},
|
||||
{
|
||||
@@ -160,6 +169,7 @@ export function BillsListTableComponent({
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||
logImEXEvent("bills_list_sort_filter", { pagination, filters, sorter });
|
||||
};
|
||||
|
||||
const filteredBills = bills
|
||||
@@ -201,6 +211,7 @@ export function BillsListTableComponent({
|
||||
<Button
|
||||
disabled={!hasBillsAccess}
|
||||
onClick={() => {
|
||||
logImEXEvent("bills_reconcile", {});
|
||||
setReconciliationContext({
|
||||
actions: { refetch: billsQuery.refetch },
|
||||
context: {
|
||||
|
||||
@@ -14,7 +14,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component";
|
||||
import JobSearchSelectComponent from "../job-search-select/job-search-select.component";
|
||||
import { getCurrentUser } from "../../firebase/firebase.utils";
|
||||
import { getCurrentUser, logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
@@ -124,6 +124,7 @@ const CardPaymentModalComponent = ({
|
||||
const { payments } = form.getFieldsValue();
|
||||
|
||||
try {
|
||||
logImEXEvent("payment_cc_lightbox");
|
||||
const response = await axios.post("/intellipay/lightbox_credentials", {
|
||||
bodyshop,
|
||||
refresh: !!window.intellipay,
|
||||
@@ -171,6 +172,7 @@ const CardPaymentModalComponent = ({
|
||||
|
||||
try {
|
||||
const { payments } = form.getFieldsValue();
|
||||
logImEXEvent("payment_cc_shortlink");
|
||||
const response = await axios.post("/intellipay/generate_payment_url", {
|
||||
bodyshop,
|
||||
amount: payments.reduce((acc, val) => acc + (val?.amount || 0), 0),
|
||||
|
||||
@@ -9,13 +9,13 @@ import "./chat-affix.styles.scss";
|
||||
import { registerMessagingHandlers, unregisterMessagingHandlers } from "./registerMessagingSocketHandlers";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||
|
||||
export function ChatAffixContainer({ bodyshop, chatVisible }) {
|
||||
export function ChatAffixContainer({ bodyshop, chatVisible, currentUser }) {
|
||||
const { t } = useTranslation();
|
||||
const client = useApolloClient();
|
||||
const { socket } = useSocket();
|
||||
|
||||
useEffect(() => {
|
||||
if (!bodyshop || !bodyshop.messagingservicesid) return;
|
||||
if (!bodyshop?.messagingservicesid) return;
|
||||
|
||||
async function SubscribeToTopicForFCMNotification() {
|
||||
try {
|
||||
@@ -35,8 +35,8 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) {
|
||||
SubscribeToTopicForFCMNotification();
|
||||
|
||||
// Register WebSocket handlers
|
||||
if (socket && socket.connected) {
|
||||
registerMessagingHandlers({ socket, client });
|
||||
if (socket?.connected) {
|
||||
registerMessagingHandlers({ socket, client, currentUser, bodyshop, t });
|
||||
|
||||
return () => {
|
||||
unregisterMessagingHandlers({ socket });
|
||||
@@ -44,11 +44,11 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) {
|
||||
}
|
||||
}, [bodyshop, socket, t, client]);
|
||||
|
||||
if (!bodyshop || !bodyshop.messagingservicesid) return <></>;
|
||||
if (!bodyshop?.messagingservicesid) return <></>;
|
||||
|
||||
return (
|
||||
<div className={`chat-affix ${chatVisible ? "chat-affix-open" : ""}`}>
|
||||
{bodyshop && bodyshop.messagingservicesid ? <ChatPopupComponent /> : null}
|
||||
{bodyshop?.messagingservicesid ? <ChatPopupComponent /> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { CONVERSATION_LIST_QUERY, GET_CONVERSATION_DETAILS } from "../../graphql/conversations.queries";
|
||||
import { gql } from "@apollo/client";
|
||||
|
||||
import { playNewMessageSound } from "../../utils/soundManager.js";
|
||||
import { isLeaderTab } from "../../utils/singleTabAudioLeader";
|
||||
|
||||
import { CONVERSATION_LIST_QUERY, GET_CONVERSATION_DETAILS } from "../../graphql/conversations.queries";
|
||||
import { QUERY_ACTIVE_ASSOCIATION_SOUND } from "../../graphql/user.queries";
|
||||
|
||||
const logLocal = (message, ...args) => {
|
||||
if (import.meta.env.VITE_APP_IS_TEST || !import.meta.env.PROD) {
|
||||
console.log(`==================== ${message} ====================`);
|
||||
@@ -26,16 +31,48 @@ const enrichConversation = (conversation, isOutbound) => ({
|
||||
__typename: "conversations"
|
||||
});
|
||||
|
||||
export const registerMessagingHandlers = ({ socket, client }) => {
|
||||
// Can be uncommonted to test the playback of the notification sound
|
||||
// window.testTone = () => {
|
||||
// const notificationSound = new Audio(newMessageSound);
|
||||
// notificationSound.play().catch((error) => {
|
||||
// console.error("Error playing notification sound:", error);
|
||||
// });
|
||||
// };
|
||||
|
||||
export const registerMessagingHandlers = ({ socket, client, currentUser, bodyshop }) => {
|
||||
if (!(socket && client)) return;
|
||||
|
||||
const handleNewMessageSummary = async (message) => {
|
||||
const { conversationId, newConversation, existingConversation, isoutbound } = message;
|
||||
|
||||
// True only when DB value is strictly true; falls back to true on cache miss
|
||||
const isNewMessageSoundEnabled = (client) => {
|
||||
try {
|
||||
const email = currentUser?.email;
|
||||
if (!email) return true; // default allow if we can't resolve user
|
||||
const res = client.readQuery({
|
||||
query: QUERY_ACTIVE_ASSOCIATION_SOUND,
|
||||
variables: { email }
|
||||
});
|
||||
const flag = res?.associations?.[0]?.new_message_sound;
|
||||
return flag === true; // strictly true => enabled
|
||||
} catch {
|
||||
// If the query hasn't been seeded in cache yet, default ON
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
logLocal("handleNewMessageSummary - Start", { message, isNew: !existingConversation });
|
||||
|
||||
const queryVariables = { offset: 0 };
|
||||
|
||||
if (!isoutbound) {
|
||||
// Play notification sound for new inbound message (scoped to bodyshop)
|
||||
if (isLeaderTab(bodyshop.id) && isNewMessageSoundEnabled(client)) {
|
||||
playNewMessageSound(bodyshop.id);
|
||||
}
|
||||
}
|
||||
|
||||
if (!existingConversation && conversationId) {
|
||||
// Attempt to read from the cache to determine if this is actually a new conversation
|
||||
try {
|
||||
@@ -291,8 +328,6 @@ export const registerMessagingHandlers = ({ socket, client }) => {
|
||||
|
||||
case "conversation-unarchived":
|
||||
case "conversation-archived":
|
||||
// Would like to someday figure out how to get this working without refetch queries,
|
||||
// But I have but a solid 4 hours into it, and there are just too many weird occurrences
|
||||
try {
|
||||
const listQueryVariables = { offset: 0 };
|
||||
const detailsQueryVariables = { conversationId };
|
||||
@@ -328,7 +363,8 @@ export const registerMessagingHandlers = ({ socket, client }) => {
|
||||
}
|
||||
break;
|
||||
|
||||
case "tag-added": { // Ensure `job_conversations` is properly formatted
|
||||
case "tag-added": {
|
||||
// Ensure `job_conversations` is properly formatted
|
||||
const formattedJobConversations = job_conversations.map((jc) => ({
|
||||
__typename: "job_conversations",
|
||||
jobid: jc.jobid || jc.job?.id,
|
||||
|
||||
@@ -34,7 +34,6 @@ export function ContractsFindModalContainer({ contractFinderModal, toggleModalVi
|
||||
logImEXEvent("contract_finder_search");
|
||||
|
||||
//Execute contract find
|
||||
|
||||
callSearch({
|
||||
variables: {
|
||||
plate: (values.plate && values.plate !== "" && values.plate) || undefined,
|
||||
|
||||
@@ -2,11 +2,13 @@ import { gql } from "@apollo/client";
|
||||
import dayjs from "../../utils/day.js";
|
||||
import componentList from "./componentList.js";
|
||||
|
||||
const createDashboardQuery = (state) => {
|
||||
const createDashboardQuery = (items) => {
|
||||
const componentBasedAdditions =
|
||||
state &&
|
||||
Array.isArray(state.layout) &&
|
||||
state.layout.map((item) => componentList[item.i].gqlFragment || "").join("");
|
||||
Array.isArray(items) &&
|
||||
items
|
||||
.map((item) => (componentList[item.i] && componentList[item.i].gqlFragment) || "")
|
||||
.filter(Boolean)
|
||||
.join("");
|
||||
return gql`
|
||||
query QUERY_DASHBOARD_DETAILS { ${componentBasedAdditions || ""}
|
||||
monthly_sales: jobs(where: {_and: [
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Icon, { SyncOutlined } from "@ant-design/icons";
|
||||
import { cloneDeep, isEmpty } from "lodash";
|
||||
import { cloneDeep } from "lodash";
|
||||
import { useMutation, useQuery } from "@apollo/client";
|
||||
import { Button, Dropdown, Space } from "antd";
|
||||
import { PageHeader } from "@ant-design/pro-layout";
|
||||
@@ -34,14 +34,25 @@ const mapDispatchToProps = () => ({
|
||||
|
||||
export function DashboardGridComponent({ currentUser, bodyshop }) {
|
||||
const { t } = useTranslation();
|
||||
const [state, setState] = useState({
|
||||
...(bodyshop.associations[0].user.dashboardlayout
|
||||
? bodyshop.associations[0].user.dashboardlayout
|
||||
: { items: [], layout: {}, layouts: [] })
|
||||
const [state, setState] = useState(() => {
|
||||
const persisted = bodyshop.associations[0].user.dashboardlayout;
|
||||
// Normalize persisted structure to avoid malformed shapes that can cause recursive layout recalculations
|
||||
if (persisted) {
|
||||
return {
|
||||
items: Array.isArray(persisted.items) ? persisted.items : [],
|
||||
layout: Array.isArray(persisted.layout) ? persisted.layout : [],
|
||||
layouts: typeof persisted.layouts === "object" && !Array.isArray(persisted.layouts) ? persisted.layouts : {},
|
||||
cols: persisted.cols
|
||||
};
|
||||
}
|
||||
return { items: [], layout: [], layouts: {}, cols: 12 };
|
||||
});
|
||||
const notification = useNotification();
|
||||
|
||||
const { loading, error, data, refetch } = useQuery(createDashboardQuery(state), {
|
||||
// Memoize the query document so Apollo doesn't treat each render as a brand-new query causing continuous re-fetches
|
||||
const dashboardQueryDoc = useMemo(() => createDashboardQuery(state.items), [state.items]);
|
||||
|
||||
const { loading, error, data, refetch } = useQuery(dashboardQueryDoc, {
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only"
|
||||
});
|
||||
@@ -49,21 +60,32 @@ export function DashboardGridComponent({ currentUser, bodyshop }) {
|
||||
const [updateLayout] = useMutation(UPDATE_DASHBOARD_LAYOUT);
|
||||
|
||||
const handleLayoutChange = async (layout, layouts) => {
|
||||
logImEXEvent("dashboard_change_layout");
|
||||
try {
|
||||
logImEXEvent("dashboard_change_layout");
|
||||
|
||||
setState({ ...state, layout, layouts });
|
||||
setState((prev) => ({ ...prev, layout, layouts }));
|
||||
|
||||
const result = await updateLayout({
|
||||
variables: {
|
||||
email: currentUser.email,
|
||||
layout: { ...state, layout, layouts }
|
||||
const result = await updateLayout({
|
||||
variables: {
|
||||
email: currentUser.email,
|
||||
layout: { ...state, layout, layouts }
|
||||
}
|
||||
});
|
||||
|
||||
if (result?.errors && result.errors.length) {
|
||||
const errorMessages = result.errors.map((e) => e?.message || String(e));
|
||||
notification.error({
|
||||
message: t("dashboard.errors.updatinglayout", {
|
||||
message: errorMessages.join("; ")
|
||||
})
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (!isEmpty(result?.errors)) {
|
||||
} catch (err) {
|
||||
// Catch any unexpected errors (including potential cyclic JSON issues) so the promise never rejects unhandled
|
||||
console.error("Dashboard layout update failed", err);
|
||||
notification.error({
|
||||
message: t("dashboard.errors.updatinglayout", {
|
||||
message: JSON.stringify(result.errors)
|
||||
message: err?.message || String(err)
|
||||
})
|
||||
});
|
||||
}
|
||||
@@ -80,19 +102,26 @@ export function DashboardGridComponent({ currentUser, bodyshop }) {
|
||||
};
|
||||
|
||||
const handleAddComponent = (e) => {
|
||||
logImEXEvent("dashboard_add_component", { name: e });
|
||||
setState({
|
||||
...state,
|
||||
items: [
|
||||
...state.items,
|
||||
// Avoid passing the full AntD menu click event (contains circular refs) to analytics
|
||||
logImEXEvent("dashboard_add_component", { key: e.key });
|
||||
const compSpec = componentList[e.key] || {};
|
||||
const minW = compSpec.minW || 1;
|
||||
const minH = compSpec.minH || 1;
|
||||
const baseW = compSpec.w || 2;
|
||||
const baseH = compSpec.h || 2;
|
||||
setState((prev) => {
|
||||
const nextItems = [
|
||||
...prev.items,
|
||||
{
|
||||
i: e.key,
|
||||
x: (state.items.length * 2) % (state.cols || 12),
|
||||
y: 99, // puts it at the bottom
|
||||
w: componentList[e.key].w || 2,
|
||||
h: componentList[e.key].h || 2
|
||||
// Position near bottom: use a large y so RGL places it last without triggering cascading relayout loops
|
||||
x: (prev.items.length * 2) % (prev.cols || 12),
|
||||
y: 1000,
|
||||
w: Math.max(baseW, minW),
|
||||
h: Math.max(baseH, minH)
|
||||
}
|
||||
]
|
||||
];
|
||||
return { ...prev, items: nextItems };
|
||||
});
|
||||
};
|
||||
|
||||
@@ -130,25 +159,33 @@ export function DashboardGridComponent({ currentUser, bodyshop }) {
|
||||
className="layout"
|
||||
breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }}
|
||||
cols={{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }}
|
||||
width="100%"
|
||||
layouts={state.layouts}
|
||||
onLayoutChange={handleLayoutChange}
|
||||
>
|
||||
{state.items.map((item) => {
|
||||
const TheComponent = componentList[item.i].component;
|
||||
const spec = componentList[item.i] || {};
|
||||
const TheComponent = spec.component;
|
||||
const minW = spec.minW || 1;
|
||||
const minH = spec.minH || 1;
|
||||
// Ensure current width/height respect minimums to avoid react-grid-layout prop warnings
|
||||
const safeItem = {
|
||||
...item,
|
||||
w: Math.max(item.w || spec.w || minW, minW),
|
||||
h: Math.max(item.h || spec.h || minH, minH)
|
||||
};
|
||||
return (
|
||||
<div
|
||||
key={item.i}
|
||||
key={safeItem.i}
|
||||
data-grid={{
|
||||
...item,
|
||||
minH: componentList[item.i].minH || 1,
|
||||
minW: componentList[item.i].minW || 1
|
||||
...safeItem,
|
||||
minH,
|
||||
minW
|
||||
}}
|
||||
>
|
||||
<LoadingSkeleton loading={loading}>
|
||||
<Icon
|
||||
component={MdClose}
|
||||
key={item.i}
|
||||
key={safeItem.i}
|
||||
style={{
|
||||
position: "absolute",
|
||||
zIndex: "2",
|
||||
@@ -156,9 +193,9 @@ export function DashboardGridComponent({ currentUser, bodyshop }) {
|
||||
top: ".25rem",
|
||||
cursor: "pointer"
|
||||
}}
|
||||
onClick={() => handleRemoveComponent(item.i)}
|
||||
onClick={() => handleRemoveComponent(safeItem.i)}
|
||||
/>
|
||||
<TheComponent className="dashboard-card" bodyshop={bodyshop} data={dashboardData} />
|
||||
{TheComponent && <TheComponent className="dashboard-card" bodyshop={bodyshop} data={dashboardData} />}
|
||||
</LoadingSkeleton>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -67,11 +67,14 @@ export const uploadToS3 = async (
|
||||
}
|
||||
|
||||
//Key should be same as we provided to maintain backwards compatibility.
|
||||
const { presignedUrl: preSignedUploadUrlToS3, key: s3Key } = signedURLResponse.data.signedUrls[0];
|
||||
const { presignedUrl: preSignedUploadUrlToS3, key: s3Key, contentType } = signedURLResponse.data.signedUrls[0];
|
||||
|
||||
const options = {
|
||||
onUploadProgress: (e) => {
|
||||
if (onProgress) onProgress({ percent: (e.loaded / e.total) * 100 });
|
||||
},
|
||||
headers: {
|
||||
...contentType ? { "Content-Type": fileType } : {}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ const mapStateToProps = createStructuredSelector({
|
||||
|
||||
export function GlobalFooter({ isPartsEntry }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
||||
if (isPartsEntry) {
|
||||
return (
|
||||
<Footer>
|
||||
@@ -35,7 +35,6 @@ export function GlobalFooter({ isPartsEntry }) {
|
||||
rome: t("titles.romeonline")
|
||||
})} - ${import.meta.env.VITE_APP_GIT_SHA_DATE}`}
|
||||
</div>
|
||||
<WssStatusDisplayComponent />
|
||||
<Link to="/disclaimer" target="_blank" style={{ color: "#ccc" }}>
|
||||
Disclaimer & Notices
|
||||
</Link>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Link, useNavigate } from "react-router-dom";
|
||||
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
||||
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
|
||||
export default function GlobalSearchOs() {
|
||||
const { t } = useTranslation();
|
||||
@@ -19,6 +20,8 @@ export default function GlobalSearchOs() {
|
||||
if (v && v && v !== "" && v.length >= 3) {
|
||||
try {
|
||||
setLoading(true);
|
||||
logImEXEvent("global_search", { search: v });
|
||||
|
||||
const searchData = await axios.post("/search", {
|
||||
search: v
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Button, Card, Col, Row, Table, Tag } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { QUERY_AUDIT_TRAIL } from "../../graphql/audit_trail.queries";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
@@ -125,6 +126,7 @@ export function JobAuditTrail({ bodyshop, jobId }) {
|
||||
render: (text, record) => (
|
||||
<Button
|
||||
onClick={() => {
|
||||
logImEXEvent("jobs_audit_view_email", {});
|
||||
var win = window.open(
|
||||
"",
|
||||
"Title",
|
||||
|
||||
@@ -12,13 +12,15 @@ import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component";
|
||||
import JobLineConvertToLabor from "../job-line-convert-to-labor/job-line-convert-to-labor.component";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import { selectIsPartsEntry } from "../../redux/application/application.selectors.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
technician: selectTechnician
|
||||
technician: selectTechnician,
|
||||
isPartsEntry: selectIsPartsEntry
|
||||
});
|
||||
const mapDispatchToProps = () => ({});
|
||||
|
||||
export function JobLinesPartPriceChange({ job, line, refetch, technician }) {
|
||||
export function JobLinesPartPriceChange({ job, line, refetch, technician, isPartsEntry }) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [updatePartPrice] = useMutation(UPDATE_LINE_PPC);
|
||||
const notification = useNotification();
|
||||
@@ -64,6 +66,7 @@ export function JobLinesPartPriceChange({ job, line, refetch, technician }) {
|
||||
|
||||
const popcontent =
|
||||
!technician &&
|
||||
!isPartsEntry &&
|
||||
InstanceRenderManager({
|
||||
imex: null,
|
||||
rome: (
|
||||
|
||||
@@ -46,6 +46,7 @@ import PartsOrderModalContainer from "../parts-order-modal/parts-order-modal.con
|
||||
import JobLinesExpander from "./job-lines-expander.component";
|
||||
import JobLinesPartPriceChange from "./job-lines-part-price-change.component";
|
||||
import JobLinesExpanderSimple from "./jobs-lines-expander-simple.component";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -397,6 +398,7 @@ export function JobLinesComponent({
|
||||
filteredInfo: filters,
|
||||
sortedInfo: sorter
|
||||
}));
|
||||
logImEXEvent("joblines_table_change", { pagination, filters, sorter });
|
||||
};
|
||||
|
||||
const handleMark = (e) => {
|
||||
@@ -413,6 +415,7 @@ export function JobLinesComponent({
|
||||
])
|
||||
);
|
||||
}
|
||||
logImEXEvent("joblines_mark_lines", {});
|
||||
};
|
||||
|
||||
const markMenu = {
|
||||
@@ -481,48 +484,50 @@ export function JobLinesComponent({
|
||||
{Enhanced_Payroll.treatment === "on" && (
|
||||
<JobLineBulkAssignComponent selectedLines={selectedLines} setSelectedLines={setSelectedLines} job={job} />
|
||||
)}
|
||||
<Button
|
||||
disabled={(job && !job.converted) || (selectedLines.length > 0 ? false : true) || jobRO || technician}
|
||||
onClick={() => {
|
||||
setBillEnterContext({
|
||||
actions: { refetch: refetch },
|
||||
context: {
|
||||
disableInvNumber: true,
|
||||
job: { id: job.id },
|
||||
bill: {
|
||||
vendorid: bodyshop.inhousevendorid,
|
||||
invoice_number: "ih",
|
||||
isinhouse: true,
|
||||
date: dayjs(),
|
||||
total: 0,
|
||||
billlines: selectedLines.map((p) => {
|
||||
return {
|
||||
joblineid: p.id,
|
||||
actual_price: p.act_price,
|
||||
actual_cost: 0, //p.act_price,
|
||||
line_desc: p.line_desc,
|
||||
line_remarks: p.line_remarks,
|
||||
part_type: p.part_type,
|
||||
quantity: p.quantity || 1,
|
||||
applicable_taxes: {
|
||||
local: false,
|
||||
state: false,
|
||||
federal: false
|
||||
}
|
||||
};
|
||||
})
|
||||
{!isPartsEntry && (
|
||||
<Button
|
||||
disabled={(job && !job.converted) || (selectedLines.length > 0 ? false : true) || jobRO || technician}
|
||||
onClick={() => {
|
||||
setBillEnterContext({
|
||||
actions: { refetch: refetch },
|
||||
context: {
|
||||
disableInvNumber: true,
|
||||
job: { id: job.id },
|
||||
bill: {
|
||||
vendorid: bodyshop.inhousevendorid,
|
||||
invoice_number: "ih",
|
||||
isinhouse: true,
|
||||
date: dayjs(),
|
||||
total: 0,
|
||||
billlines: selectedLines.map((p) => {
|
||||
return {
|
||||
joblineid: p.id,
|
||||
actual_price: p.act_price,
|
||||
actual_cost: 0, //p.act_price,
|
||||
line_desc: p.line_desc,
|
||||
line_remarks: p.line_remarks,
|
||||
part_type: p.part_type,
|
||||
quantity: p.quantity || 1,
|
||||
applicable_taxes: {
|
||||
local: false,
|
||||
state: false,
|
||||
federal: false
|
||||
}
|
||||
};
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
//Clear out the selected lines. IO-785
|
||||
setSelectedLines([]);
|
||||
}}
|
||||
>
|
||||
<HomeOutlined />
|
||||
{t("parts.actions.orderinhouse")}
|
||||
{selectedLines.length > 0 && ` (${selectedLines.length})`}
|
||||
</Button>
|
||||
//Clear out the selected lines. IO-785
|
||||
setSelectedLines([]);
|
||||
}}
|
||||
>
|
||||
<HomeOutlined />
|
||||
{t("parts.actions.orderinhouse")}
|
||||
{selectedLines.length > 0 && ` (${selectedLines.length})`}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
id="job-lines-order-parts-button"
|
||||
disabled={(job && !job.converted) || (selectedLines.length > 0 ? false : true) || jobRO || technician}
|
||||
@@ -578,7 +583,8 @@ export function JobLinesComponent({
|
||||
{t("joblines.actions.new")}
|
||||
</Button>
|
||||
)}
|
||||
{InstanceRenderManager({ rome: <JobSendPartPriceChangeComponent job={job} disabled={technician} /> })}
|
||||
{!isPartsEntry &&
|
||||
InstanceRenderManager({ rome: <JobSendPartPriceChangeComponent job={job} disabled={technician} /> })}
|
||||
<JobCreateIOU job={job} selectedJobLines={selectedLines} />
|
||||
<Input.Search
|
||||
placeholder={t("general.labels.search")}
|
||||
@@ -613,12 +619,18 @@ export function JobLinesComponent({
|
||||
expanded ? (
|
||||
<MinusCircleTwoTone onClick={(e) => onExpand(record, e)} />
|
||||
) : (
|
||||
<PlusCircleTwoTone onClick={(e) => onExpand(record, e)} />
|
||||
<PlusCircleTwoTone
|
||||
onClick={(e) => {
|
||||
onExpand(record, e);
|
||||
logImEXEvent("joblines_expander", {});
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
onRow={(record) => {
|
||||
return {
|
||||
onDoubleClick: () => {
|
||||
logImEXEvent("joblines_double_click_select", {});
|
||||
const notMatchingLines = selectedLines.filter((i) => i.id !== record.id);
|
||||
notMatchingLines.length !== selectedLines.length
|
||||
? setSelectedLines(notMatchingLines)
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import dayjs from "../../utils/day";
|
||||
import axios from "axios";
|
||||
import { Badge, Card, Space, Table, Tag } from "antd";
|
||||
import { gql, useQuery } from "@apollo/client";
|
||||
import { DateTimeFormatterFunction } from "../../utils/DateFormatter";
|
||||
import { Badge, Card, Space, Table, Tag } from "antd";
|
||||
import axios from "axios";
|
||||
import { isEmpty } from "lodash";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import "./job-lifecycle.styles.scss";
|
||||
import BlurWrapperComponent from "../feature-wrapper/blur-wrapper.component";
|
||||
import UpsellComponent, { upsellEnum } from "../upsell/upsell.component";
|
||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { DateTimeFormatterFunction } from "../../utils/DateFormatter";
|
||||
import dayjs from "../../utils/day";
|
||||
import BlurWrapperComponent from "../feature-wrapper/blur-wrapper.component";
|
||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||
import UpsellComponent, { upsellEnum } from "../upsell/upsell.component";
|
||||
import "./job-lifecycle.styles.scss";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
@@ -57,6 +58,7 @@ export function JobLifecycleComponent({ bodyshop, job, statuses }) {
|
||||
jobids: job.id,
|
||||
statuses: statuses.statuses
|
||||
});
|
||||
logImEXEvent("jobs_lifecycle_data", {});
|
||||
const data = response.data.transition[job.id];
|
||||
setLifecycleData(data);
|
||||
} catch (err) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -32,6 +33,7 @@ export function JoblineBulkAssign({ setSelectedLines, selectedLines, insertAudit
|
||||
const handleConvert = async (values) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
logImEXEvent("joblines_bulk_assign", {});
|
||||
const result = await assignLines({
|
||||
variables: {
|
||||
jobline: {
|
||||
|
||||
@@ -12,6 +12,7 @@ import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selecto
|
||||
import { GenerateDocument } from "../../utils/RenderTemplate";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -46,6 +47,7 @@ export function JobLineDispatchButton({
|
||||
try {
|
||||
setLoading(true);
|
||||
//THIS HAS NOT YET BEEN TESTED. START BY FINISHING THIS FUNCTION.
|
||||
logImEXEvent("joblines_dispatch", {});
|
||||
const result = await dispatchLines({
|
||||
variables: {
|
||||
partsDispatch: {
|
||||
|
||||
@@ -10,9 +10,12 @@ const mapStateToProps = createStructuredSelector({
|
||||
const mapDispatchToProps = () => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
export const DEFAULT_COL_LAYOUT = { xs: 24, sm: 24, md: 8, lg: 4, xl: 4, xxl: 4 };
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobPartsQueueCount);
|
||||
|
||||
export function JobPartsQueueCount({ bodyshop, parts, style }) {
|
||||
export function JobPartsQueueCount({ bodyshop, parts, defaultColLayout = DEFAULT_COL_LAYOUT }) {
|
||||
const partsStatus = useMemo(() => {
|
||||
if (!parts) return null;
|
||||
return parts.reduce(
|
||||
@@ -35,35 +38,34 @@ export function JobPartsQueueCount({ bodyshop, parts, style }) {
|
||||
}, [bodyshop, parts]);
|
||||
|
||||
if (!parts) return null;
|
||||
|
||||
return (
|
||||
<Row style={style}>
|
||||
<Col span={4}>
|
||||
<Row>
|
||||
<Col {...defaultColLayout}>
|
||||
<Tooltip title="Total">
|
||||
<Tag>{partsStatus.total}</Tag>
|
||||
</Tooltip>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Col {...defaultColLayout}>
|
||||
<Tooltip title="No Status">
|
||||
<Tag color="gold">{partsStatus["null"]}</Tag>
|
||||
</Tooltip>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Col {...defaultColLayout}>
|
||||
<Tooltip title={bodyshop.md_order_statuses.default_ordered}>
|
||||
<Tag color="blue">{partsStatus[bodyshop.md_order_statuses.default_ordered]}</Tag>
|
||||
</Tooltip>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Col {...defaultColLayout}>
|
||||
<Tooltip title={bodyshop.md_order_statuses.default_received}>
|
||||
<Tag color="green">{partsStatus[bodyshop.md_order_statuses.default_received]}</Tag>
|
||||
</Tooltip>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Col {...defaultColLayout}>
|
||||
<Tooltip title={bodyshop.md_order_statuses.default_returned}>
|
||||
<Tag color="orange">{partsStatus[bodyshop.md_order_statuses.default_returned]}</Tag>
|
||||
</Tooltip>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Col {...defaultColLayout}>
|
||||
<Tooltip title={bodyshop.md_order_statuses.default_bo}>
|
||||
<Tag color="red">{partsStatus[bodyshop.md_order_statuses.default_bo]}</Tag>
|
||||
</Tooltip>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils.js";
|
||||
|
||||
export default function JobRemoveFromPartsQueue({ checked, jobId }) {
|
||||
const [updateJob] = useMutation(UPDATE_JOB);
|
||||
@@ -13,6 +14,8 @@ export default function JobRemoveFromPartsQueue({ checked, jobId }) {
|
||||
|
||||
const handleChange = async (e) => {
|
||||
setLoading(true);
|
||||
logImEXEvent("parts_queue_toggle", { estimators: e });
|
||||
|
||||
const result = await updateJob({
|
||||
variables: { jobId: jobId, job: { queued_for_parts: e.target.checked } }
|
||||
});
|
||||
|
||||
@@ -20,35 +20,27 @@ export function JobTotalsCashDiscount({ bodyshop, amountDinero }) {
|
||||
const notification = useNotification();
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
if (amountDinero && bodyshop) {
|
||||
setLoading(true);
|
||||
let response;
|
||||
try {
|
||||
response = await axios.post("/intellipay/checkfee", {
|
||||
bodyshop: { id: bodyshop.id, imexshopid: bodyshop.imexshopid, state: bodyshop.state },
|
||||
amount: Dinero(amountDinero).toFormat("0.00")
|
||||
});
|
||||
if (!amountDinero || !bodyshop) return;
|
||||
|
||||
if (response?.data?.error) {
|
||||
notification.open({
|
||||
type: "error",
|
||||
message:
|
||||
response.data?.error ||
|
||||
"Error encountered when contacting IntelliPay service to determine cash discounted price."
|
||||
});
|
||||
} else {
|
||||
setFee(response.data?.fee || 0);
|
||||
}
|
||||
} catch (error) {
|
||||
notification.open({
|
||||
type: "error",
|
||||
message:
|
||||
error.response?.data?.error ||
|
||||
"Error encountered when contacting IntelliPay service to determine cash discounted price."
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setLoading(true);
|
||||
const errorMessage = "Error encountered when contacting IntelliPay service to determine cash discounted price.";
|
||||
|
||||
try {
|
||||
const { id, imexshopid, state } = bodyshop;
|
||||
const { data } = await axios.post("/intellipay/checkfee", {
|
||||
bodyshop: { id, imexshopid, state },
|
||||
amount: Dinero(amountDinero).toUnit()
|
||||
});
|
||||
|
||||
if (data?.error) {
|
||||
notification.open({ type: "error", message: data.error || errorMessage });
|
||||
} else {
|
||||
setFee(data?.fee ?? 0);
|
||||
}
|
||||
} catch (error) {
|
||||
notification.open({ type: "error", message: error.response?.data?.error || errorMessage });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [amountDinero, bodyshop, notification]);
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { createStructuredSelector } from "reselect";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import { selectPartnerVersion } from "../../redux/application/application.selectors";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
@@ -30,11 +31,15 @@ export function JobsAvailableScan({ partnerVersion, refetch }) {
|
||||
const notification = useNotification();
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
logImEXEvent("available_jobs_scan_sort_filter", { pagination, filters, sorter });
|
||||
|
||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||
};
|
||||
|
||||
const handleImport = async (filepath) => {
|
||||
setLoading(true);
|
||||
logImEXEvent("available_jobs_scan", {});
|
||||
|
||||
const response = await axios.post("http://localhost:1337/import/", {
|
||||
filepath
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@ import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { TimeAgoFormatter } from "../../utils/DateFormatter";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
@@ -34,6 +35,7 @@ export function JobsAvailableComponent({ bodyshop, loading, data, refetch, addJo
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||
logImEXEvent("available_jobs_sort_filter", { pagination, filters, sorter });
|
||||
};
|
||||
|
||||
const columns = [
|
||||
|
||||
@@ -14,6 +14,7 @@ import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import { DateTimeFormatterFunction } from "../../utils/DateFormatter";
|
||||
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
|
||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component.jsx";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser,
|
||||
@@ -108,6 +109,9 @@ export function JobsDetailHeaderActionsToggleProduction({
|
||||
DateTimeFormatterFunction(values.actual_completion)
|
||||
)
|
||||
});
|
||||
|
||||
logImEXEvent(scenario === "pre" ? "job_intake_quick" : "job-deliver-quick", {});
|
||||
|
||||
setPopOverVisible(false);
|
||||
closeParentMenu();
|
||||
refetch();
|
||||
|
||||
@@ -139,17 +139,18 @@ export function JobsDetailHeader({ job, bodyshop, disabled, insertAuditTrail, is
|
||||
<DataLabel label={t("jobs.fields.comment")} valueStyle={{ overflow: "hidden", textOverflow: "ellipsis" }}>
|
||||
<ProductionListColumnComment record={job} />
|
||||
</DataLabel>
|
||||
<DataLabel label={t("jobs.fields.ins_co_nm_short")}>{job.ins_co_nm}</DataLabel>
|
||||
{!isPartsEntry && <DataLabel label={t("jobs.fields.ins_co_nm_short")}>{job.ins_co_nm}</DataLabel>}
|
||||
<DataLabel label={t("jobs.fields.clm_no")}>{job.clm_no}</DataLabel>
|
||||
<DataLabel label={t("jobs.fields.ponumber")} hideIfNull>
|
||||
{job.po_number}
|
||||
</DataLabel>
|
||||
<DataLabel label={t("jobs.fields.repairtotal")}>
|
||||
<CurrencyFormatter>{job.clm_total}</CurrencyFormatter>
|
||||
<span style={{ margin: "0rem .5rem" }}>/</span>
|
||||
<CurrencyFormatter>{job.owner_owing}</CurrencyFormatter>
|
||||
</DataLabel>
|
||||
|
||||
{!isPartsEntry && (
|
||||
<DataLabel label={t("jobs.fields.repairtotal")}>
|
||||
<CurrencyFormatter>{job.clm_total}</CurrencyFormatter>
|
||||
<span style={{ margin: "0rem .5rem" }}>/</span>
|
||||
<CurrencyFormatter>{job.owner_owing}</CurrencyFormatter>
|
||||
</DataLabel>
|
||||
)}
|
||||
{!isPartsEntry && (
|
||||
<>
|
||||
<DataLabel label={t("jobs.fields.alt_transport")}>
|
||||
@@ -176,7 +177,7 @@ export function JobsDetailHeader({ job, bodyshop, disabled, insertAuditTrail, is
|
||||
<Checkbox
|
||||
checked={!!job.estimate_sent_approval}
|
||||
onChange={(e) => handleCheckboxChange("estimate_sent_approval", e.target.checked)}
|
||||
disabled={disabled}
|
||||
disabled={disabled || isPartsEntry}
|
||||
>
|
||||
{job.estimate_sent_approval && (
|
||||
<span style={{ color: "#888" }}>
|
||||
@@ -191,7 +192,7 @@ export function JobsDetailHeader({ job, bodyshop, disabled, insertAuditTrail, is
|
||||
<Checkbox
|
||||
checked={!!job.estimate_approved}
|
||||
onChange={(e) => handleCheckboxChange("estimate_approved", e.target.checked)}
|
||||
disabled={disabled}
|
||||
disabled={disabled || isPartsEntry}
|
||||
>
|
||||
{job.estimate_approved && (
|
||||
<span style={{ color: "#888" }}>
|
||||
@@ -236,7 +237,7 @@ export function JobsDetailHeader({ job, bodyshop, disabled, insertAuditTrail, is
|
||||
<Card
|
||||
style={{ height: "100%" }}
|
||||
title={
|
||||
disabled ? (
|
||||
disabled || isPartsEntry ? (
|
||||
<>{ownerTitle.length > 0 ? ownerTitle : t("owner.labels.noownerinfo")}</>
|
||||
) : (
|
||||
<Link to={`/manage/owners/${job.owner.id}`}>
|
||||
@@ -247,14 +248,14 @@ export function JobsDetailHeader({ job, bodyshop, disabled, insertAuditTrail, is
|
||||
>
|
||||
<div>
|
||||
<DataLabel key="2" label={t("jobs.fields.ownr_ph1")}>
|
||||
{disabled ? (
|
||||
{disabled || isPartsEntry ? (
|
||||
<PhoneNumberFormatter>{job.ownr_ph1}</PhoneNumberFormatter>
|
||||
) : (
|
||||
<ChatOpenButton phone={job.ownr_ph1} jobid={job.id} />
|
||||
)}
|
||||
</DataLabel>
|
||||
<DataLabel key="22" label={t("jobs.fields.ownr_ph2")}>
|
||||
{disabled ? (
|
||||
{disabled || isPartsEntry ? (
|
||||
<PhoneNumberFormatter>{job.ownr_ph2}</PhoneNumberFormatter>
|
||||
) : (
|
||||
<ChatOpenButton phone={job.ownr_ph2} jobid={job.id} />
|
||||
@@ -266,7 +267,7 @@ export function JobsDetailHeader({ job, bodyshop, disabled, insertAuditTrail, is
|
||||
} ${job.ownr_st || ""} ${job.ownr_zip || ""}`}
|
||||
</DataLabel>
|
||||
<DataLabel key="4" label={t("owners.fields.ownr_ea")}>
|
||||
{disabled ? (
|
||||
{disabled || isPartsEntry ? (
|
||||
<>{job.ownr_ea || ""}</>
|
||||
) : job.ownr_ea ? (
|
||||
<a href={`mailto:${job.ownr_ea}`}>{job.ownr_ea}</a>
|
||||
@@ -316,7 +317,7 @@ export function JobsDetailHeader({ job, bodyshop, disabled, insertAuditTrail, is
|
||||
<DataLabel label={t("jobs.labels.relatedros")}>
|
||||
<JobsRelatedRos jobid={job.id} job={job} disabled={disabled} />
|
||||
</DataLabel>
|
||||
{job.vehicle && job.vehicle.notes && (
|
||||
{job.vehicle?.notes && (
|
||||
<DataLabel
|
||||
label={t("vehicles.fields.notes")}
|
||||
valueStyle={{ whiteSpace: "pre-wrap" }}
|
||||
@@ -326,7 +327,7 @@ export function JobsDetailHeader({ job, bodyshop, disabled, insertAuditTrail, is
|
||||
{job.vehicle.notes}
|
||||
</DataLabel>
|
||||
)}
|
||||
{job.vehicle && job.vehicle.v_paint_codes && (
|
||||
{job.vehicle?.v_paint_codes && (
|
||||
<DataLabel label={t("vehicles.fields.v_paint_codes", { number: "" })}>
|
||||
<span style={{ whiteSpace: "pre" }}>
|
||||
{Object.keys(job.vehicle.v_paint_codes)
|
||||
|
||||
@@ -15,6 +15,7 @@ import { alphaSort, statusSort } from "../../utils/sorters";
|
||||
import useLocalStorage from "../../utils/useLocalStorage";
|
||||
import StartChatButton from "../chat-open-button/chat-open-button.component";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
@@ -177,6 +178,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
|
||||
}
|
||||
setFilter(filters);
|
||||
history({ search: queryString.stringify(search) });
|
||||
logImEXEvent("jobs_all_list_sort_filter", { pagination, filters, sorter });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -17,6 +17,7 @@ import AlertComponent from "../alert/alert.component";
|
||||
import ChatOpenButton from "../chat-open-button/chat-open-button.component";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
import { OwnerNameDisplayFunction } from "./../owner-name-display/owner-name-display.component";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
@@ -67,6 +68,7 @@ export function JobsList({ bodyshop }) {
|
||||
: [];
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
logImEXEvent("jobs_list_sort_filter", { pagination, filters, sorter });
|
||||
setState({ ...state, sortedInfo: sorter });
|
||||
setFilter(filters);
|
||||
};
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
import { Space, Tag } from "antd";
|
||||
import { Link } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectIsPartsEntry } from "../../redux/application/application.selectors.js";
|
||||
import { connect } from "react-redux";
|
||||
import getPartsBasePath from "../../utils/getPartsBasePath.js";
|
||||
|
||||
export default function JobsRelatedRos({ job, disabled }) {
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
isPartsEntry: selectIsPartsEntry
|
||||
});
|
||||
|
||||
function JobsRelatedRos({ job, disabled, isPartsEntry }) {
|
||||
if (!(job?.vehicle && job.vehicle.jobs)) return null;
|
||||
const basePath = getPartsBasePath(isPartsEntry);
|
||||
|
||||
return (
|
||||
<Space wrap>
|
||||
{job.vehicle.jobs
|
||||
@@ -12,7 +22,7 @@ export default function JobsRelatedRos({ job, disabled }) {
|
||||
{disabled ? (
|
||||
<>{`${j.ro_number || "N/A"}${j.clm_no ? ` | ${j.clm_no}` : ""}${j.status ? ` | ${j.status}` : ""}`}</>
|
||||
) : (
|
||||
<Link to={`/manage/jobs/${j?.id}`}>{`${j.ro_number || "N/A"}${
|
||||
<Link to={`${basePath}/jobs/${j?.id}`}>{`${j.ro_number || "N/A"}${
|
||||
j.clm_no ? ` | ${j.clm_no}` : ""
|
||||
}${j.status ? ` | ${j.status}` : ""}`}</Link>
|
||||
)}
|
||||
@@ -21,3 +31,4 @@ export default function JobsRelatedRos({ job, disabled }) {
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
export default connect(mapStateToProps)(JobsRelatedRos);
|
||||
|
||||
@@ -39,11 +39,15 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount,
|
||||
return showUnreadOnly ? { ...baseWhereClause, read: { _is_null: true } } : baseWhereClause;
|
||||
}, [baseWhereClause, showUnreadOnly]);
|
||||
|
||||
// before you call useQuery, compute skip once so you can reuse it
|
||||
const skipQuery = !userAssociationId || !isEmployee;
|
||||
|
||||
const {
|
||||
data,
|
||||
fetchMore,
|
||||
loading: queryLoading,
|
||||
refetch
|
||||
refetch,
|
||||
error
|
||||
} = useQuery(GET_NOTIFICATIONS, {
|
||||
variables: {
|
||||
limit: INITIAL_NOTIFICATIONS,
|
||||
@@ -52,14 +56,26 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount,
|
||||
},
|
||||
fetchPolicy: "cache-and-network",
|
||||
notifyOnNetworkStatusChange: true,
|
||||
errorPolicy: "all",
|
||||
pollInterval: isConnected ? 0 : day.duration(NOTIFICATION_POLL_INTERVAL_SECONDS, "seconds").asMilliseconds(),
|
||||
skip: !userAssociationId || !isEmployee,
|
||||
onError: (err) => {
|
||||
console.error(`Error polling Notifications: ${err?.message || ""}`);
|
||||
setTimeout(() => refetch(), day.duration(2, "seconds").asMilliseconds());
|
||||
}
|
||||
skip: skipQuery
|
||||
});
|
||||
|
||||
// Replace onError with a side-effect that reacts to the hook’s `error`
|
||||
useEffect(() => {
|
||||
if (!error || skipQuery) return;
|
||||
|
||||
console.error(`Error polling Notifications: ${error?.message || ""}`);
|
||||
|
||||
const t = setTimeout(() => {
|
||||
// Guard: if component unmounted or query now skipped, do nothing
|
||||
if (!skipQuery) {
|
||||
refetch().catch((e) => console.error("Refetch failed:", e?.message || e));
|
||||
}
|
||||
}, day.duration(2, "seconds").asMilliseconds());
|
||||
|
||||
return () => clearTimeout(t);
|
||||
}, [error, refetch, skipQuery]);
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
// Prevent open + close behavior from the header
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import { Button, Form, Popconfirm } from "antd";
|
||||
import { PageHeader } from "@ant-design/pro-layout";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useApolloClient, useMutation } from "@apollo/client";
|
||||
import { Button, Form, Popconfirm } from "antd";
|
||||
import { phone } from "phone"; // Import phone utility for formatting
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils.js";
|
||||
import { DELETE_OWNER, UPDATE_OWNER } from "../../graphql/owners.queries";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors"; // Adjust path
|
||||
import { phoneNumberOptOutService } from "../../utils/phoneOptOutService.js"; // Adjust path
|
||||
import OwnerDetailFormComponent from "./owner-detail-form.component";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import { phone } from "phone"; // Import phone utility for formatting
|
||||
|
||||
// Connect to Redux to access bodyshop
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
@@ -55,6 +56,7 @@ function OwnerDetailFormContainer({ owner, refetch, bodyshop }) {
|
||||
|
||||
const handleDelete = async () => {
|
||||
setLoading(true);
|
||||
logImEXEvent("owner_delete", {});
|
||||
try {
|
||||
const result = await deleteOwner({
|
||||
variables: { id: owner.id }
|
||||
@@ -84,6 +86,7 @@ function OwnerDetailFormContainer({ owner, refetch, bodyshop }) {
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
setLoading(true);
|
||||
logImEXEvent("owner_update", {});
|
||||
try {
|
||||
const result = await updateOwner({
|
||||
variables: { ownerId: owner.id, owner: values }
|
||||
|
||||
@@ -227,15 +227,21 @@ export function PartsOrderListTableComponent({
|
||||
sorter: (a, b) => a.order_date - b.order_date,
|
||||
sortOrder: state.sortedInfo.columnKey === "order_date" && state.sortedInfo.order,
|
||||
render: (text, record) => <DateFormatter>{record.order_date}</DateFormatter>
|
||||
},
|
||||
{
|
||||
}
|
||||
];
|
||||
|
||||
if (!isPartsEntry) {
|
||||
columns.push({
|
||||
title: t("parts_orders.fields.return"),
|
||||
dataIndex: "return",
|
||||
key: "return",
|
||||
sorter: (a, b) => a.return - b.return,
|
||||
sortOrder: state.sortedInfo.columnKey === "return" && state.sortedInfo.order,
|
||||
render: (text, record) => <Checkbox checked={record.return} />
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
columns.push(
|
||||
{
|
||||
title: t("parts_orders.fields.deliver_by"),
|
||||
dataIndex: "deliver_by",
|
||||
@@ -256,7 +262,7 @@ export function PartsOrderListTableComponent({
|
||||
render: (text, record) => recordActions(record, true),
|
||||
id: "parts-order-list-table-actions"
|
||||
}
|
||||
];
|
||||
);
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DownOutlined } from "@ant-design/icons";
|
||||
import { Dropdown, InputNumber, Space } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
export default function PartsOrderModalPriceChange({ form, field }) {
|
||||
const { t } = useTranslation();
|
||||
const menu = {
|
||||
@@ -63,6 +63,7 @@ export default function PartsOrderModalPriceChange({ form, field }) {
|
||||
}
|
||||
],
|
||||
onClick: ({ key }) => {
|
||||
logImEXEvent("parts_order_manual_discount", {});
|
||||
if (key === "custom") return;
|
||||
const values = form.getFieldsValue();
|
||||
const { parts_order_lines } = values;
|
||||
|
||||
@@ -11,16 +11,27 @@ import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
|
||||
import PartsOrderModalPriceChange from "./parts-order-modal-price-change.component";
|
||||
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
|
||||
import { selectIsPartsEntry } from "../../redux/application/application.selectors.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
bodyshop: selectBodyshop,
|
||||
isPartsEntry: selectIsPartsEntry
|
||||
});
|
||||
const mapDispatchToProps = () => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(PartsOrderModalComponent);
|
||||
|
||||
export function PartsOrderModalComponent({ bodyshop, vendorList, sendTypeState, isReturn, preferredMake, job, form }) {
|
||||
export function PartsOrderModalComponent({
|
||||
bodyshop,
|
||||
vendorList,
|
||||
sendTypeState,
|
||||
isReturn,
|
||||
preferredMake,
|
||||
job,
|
||||
form,
|
||||
isPartsEntry
|
||||
}) {
|
||||
const [sendType, setSendType] = sendTypeState;
|
||||
|
||||
const {
|
||||
@@ -83,7 +94,7 @@ export function PartsOrderModalComponent({ bodyshop, vendorList, sendTypeState,
|
||||
</Space>
|
||||
</Tag>
|
||||
)}
|
||||
{!isReturn && (
|
||||
{!isReturn && !isPartsEntry && (
|
||||
<Form.Item
|
||||
name="removefrompartsqueue"
|
||||
label={t("parts_orders.labels.removefrompartsqueue")}
|
||||
@@ -92,7 +103,7 @@ export function PartsOrderModalComponent({ bodyshop, vendorList, sendTypeState,
|
||||
<Checkbox />
|
||||
</Form.Item>
|
||||
)}
|
||||
{OEConnection.treatment === "on" && !isReturn && (
|
||||
{OEConnection.treatment === "on" && !isReturn && !isPartsEntry && (
|
||||
<Form.Item name="is_quote" label={t("parts_orders.labels.is_quote")} valuePropName="checked">
|
||||
<Checkbox />
|
||||
</Form.Item>
|
||||
@@ -249,7 +260,7 @@ export function PartsOrderModalComponent({ bodyshop, vendorList, sendTypeState,
|
||||
<Radio disabled={is_quote} value={"p"}>
|
||||
{t("parts_orders.labels.print")}
|
||||
</Radio>
|
||||
{OEConnection.treatment === "on" && !isReturn && (
|
||||
{OEConnection.treatment === "on" && !isReturn && !isPartsEntry && (
|
||||
<Radio value={"oec"}>{t("parts_orders.labels.oec")}</Radio>
|
||||
)}
|
||||
</Radio.Group>
|
||||
|
||||
@@ -8,6 +8,7 @@ import AlertComponent from "../alert/alert.component";
|
||||
import JobsDetailHeader from "../jobs-detail-header/jobs-detail-header.component";
|
||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||
import PartsQueueJobLinesComponent from "./parts-queue-job-lines.component";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
|
||||
export default function PartsQueueDetailCard() {
|
||||
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
|
||||
@@ -37,6 +38,8 @@ export default function PartsQueueDetailCard() {
|
||||
const { t } = useTranslation();
|
||||
const handleDrawerClose = () => {
|
||||
delete searchParams.selected;
|
||||
logImEXEvent("parts_queue_drawer", {});
|
||||
|
||||
history({
|
||||
search: queryString.stringify({
|
||||
...searchParams
|
||||
|
||||
@@ -20,6 +20,7 @@ import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.c
|
||||
import JobRemoveFromPartsQueue from "../job-remove-from-parst-queue/job-remove-from-parts-queue.component";
|
||||
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||
import ProductionListColumnComment from "../production-list-columns/production-list-columns.comment.component";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
@@ -73,6 +74,7 @@ export function PartsQueueListComponent({ bodyshop }) {
|
||||
}
|
||||
setFilter(filters);
|
||||
history({ search: queryString.stringify(searchParams) });
|
||||
logImEXEvent("parts_queue_sort_filter", { pagination, filters, sorter });
|
||||
};
|
||||
|
||||
const handleOnRowClick = (record) => {
|
||||
@@ -233,7 +235,7 @@ export function PartsQueueListComponent({ bodyshop }) {
|
||||
title: t("jobs.fields.partsstatus"),
|
||||
dataIndex: "partsstatus",
|
||||
key: "partsstatus",
|
||||
render: (text, record) => <JobPartsQueueCount style={{ minWidth: "10rem" }} parts={record.joblines_status} />
|
||||
render: (text, record) => <JobPartsQueueCount parts={record.joblines_status} />
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.comment"),
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from "@ant-design/icons";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import EmployeeSearchSelectComponent from "../employee-search-select/employee-search-select.component";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
@@ -26,10 +27,12 @@ export function ProductionBoardFilters({ bodyshop, filter, setFilter, loading })
|
||||
|
||||
const toggleAlertFilter = () => {
|
||||
setFilter({ ...filter, alert: !filter.alert });
|
||||
logImEXEvent("visual_board_filter_alert", {});
|
||||
};
|
||||
|
||||
const toggleUnassignedFilter = () => {
|
||||
setFilter({ ...filter, unassigned: !filter.unassigned });
|
||||
logImEXEvent("visual_board_filter_unassigned", {});
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -40,6 +43,7 @@ export function ProductionBoardFilters({ bodyshop, filter, setFilter, loading })
|
||||
placeholder={t("general.labels.search")}
|
||||
onChange={(e) => {
|
||||
setFilter({ ...filter, search: e.target.value });
|
||||
logImEXEvent("visual_board_filter_search", { search: e.target.value });
|
||||
}}
|
||||
/>
|
||||
<EmployeeSearchSelectComponent
|
||||
@@ -47,7 +51,10 @@ export function ProductionBoardFilters({ bodyshop, filter, setFilter, loading })
|
||||
options={bodyshop.employees.filter((e) => e.active)}
|
||||
value={filter.employeeId}
|
||||
placeholder={t("production.labels.employeesearch")}
|
||||
onChange={(emp) => setFilter({ ...filter, employeeId: emp })}
|
||||
onChange={(emp) => {
|
||||
setFilter({ ...filter, employeeId: emp });
|
||||
logImEXEvent("visual_board_filter_alert", { employeeId: emp });
|
||||
}}
|
||||
allowClear
|
||||
/>
|
||||
<Button
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FaRegStickyNote } from "react-icons/fa";
|
||||
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
|
||||
export default function ProductionListColumnComment({ record }) {
|
||||
const { t } = useTranslation();
|
||||
@@ -28,6 +29,7 @@ export default function ProductionListColumnComment({ record }) {
|
||||
}).then(() => {
|
||||
if (record.refetch) record.refetch();
|
||||
});
|
||||
logImEXEvent("job_add_comment", { estimators: e });
|
||||
};
|
||||
|
||||
const handleChange = (e) => {
|
||||
|
||||
@@ -18,6 +18,7 @@ import ProductionListDetail from "../production-list-detail/production-list-deta
|
||||
import { ProductionListConfigManager } from "./production-list-config-manager.component.jsx";
|
||||
import ProductionListPrint from "./production-list-print.component";
|
||||
import ResizeableTitle from "./production-list-table.resizeable.component";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -114,6 +115,7 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
|
||||
setState(newState);
|
||||
setHasUnsavedChanges(true);
|
||||
}
|
||||
logImEXEvent("production_list_sort_filter", { pagination, filters, sorter });
|
||||
};
|
||||
|
||||
const onDragEnd = (fromIndex, toIndex) => {
|
||||
@@ -134,6 +136,7 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
|
||||
setColumns(newColumns);
|
||||
setHasUnsavedChanges(true);
|
||||
}
|
||||
logImEXEvent("production_list_remove_column", { key });
|
||||
};
|
||||
|
||||
const handleResize =
|
||||
@@ -156,6 +159,7 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
|
||||
setColumns(updatedColumns);
|
||||
setHasUnsavedChanges(true);
|
||||
}
|
||||
logImEXEvent("production_list_add_column", { key: newColumn.key });
|
||||
};
|
||||
|
||||
const headerItem = (col) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Button, Card, Col, Form, Input } from "antd";
|
||||
import { LockOutlined } from "@ant-design/icons";
|
||||
import { Button, Card, Col, Form, Input, Space, Switch, Tooltip, Typography } from "antd";
|
||||
import { AudioMutedOutlined, LockOutlined, SoundOutlined } from "@ant-design/icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
@@ -10,6 +10,8 @@ import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||
import NotificationSettingsForm from "../notification-settings/notification-settings-form.component.jsx";
|
||||
import { useMutation, useQuery } from "@apollo/client";
|
||||
import { QUERY_ACTIVE_ASSOCIATION_SOUND, UPDATE_NEW_MESSAGE_SOUND } from "../../graphql/user.queries.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser
|
||||
@@ -48,6 +50,28 @@ export default connect(
|
||||
}
|
||||
};
|
||||
|
||||
// ---- Notification sound (associations.new_message_sound) ----
|
||||
const email = currentUser?.email;
|
||||
const { data: assocData, loading: assocLoading } = useQuery(QUERY_ACTIVE_ASSOCIATION_SOUND, {
|
||||
variables: { email },
|
||||
skip: !email,
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "cache-first"
|
||||
});
|
||||
const association = assocData?.associations?.[0];
|
||||
// Treat null/undefined as ON for backward-compat
|
||||
const soundEnabled = association?.new_message_sound === true;
|
||||
|
||||
const [updateNewMessageSound, { loading: updatingSound }] = useMutation(UPDATE_NEW_MESSAGE_SOUND, {
|
||||
update(cache, { data }) {
|
||||
const updated = data?.update_associations_by_pk;
|
||||
if (!updated) return;
|
||||
cache.modify({
|
||||
id: cache.identify({ __typename: "associations", id: updated.id }),
|
||||
fields: { new_message_sound: () => updated.new_message_sound }
|
||||
});
|
||||
}
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<Col span={24}>
|
||||
@@ -80,6 +104,7 @@ export default connect(
|
||||
</Card>
|
||||
</Form>
|
||||
</Col>
|
||||
|
||||
<Col span={24}>
|
||||
<Form onFinish={handleChangePassword} autoComplete={"no"} initialValues={currentUser} layout="vertical">
|
||||
<Card
|
||||
@@ -119,6 +144,52 @@ export default connect(
|
||||
</Card>
|
||||
</Form>
|
||||
</Col>
|
||||
|
||||
{association && (
|
||||
<Col span={24}>
|
||||
<Card title={t("user.labels.user_settings")}>
|
||||
<Space align="center" size="large">
|
||||
<Typography.Text>{t("user.labels.play_sound_for_new_messages")}</Typography.Text>
|
||||
<Tooltip
|
||||
title={soundEnabled ? t("user.labels.notification_sound_on") : t("user.labels.notification_sound_off")}
|
||||
>
|
||||
<Switch
|
||||
checkedChildren={<SoundOutlined />}
|
||||
unCheckedChildren={<AudioMutedOutlined />}
|
||||
checked={!!soundEnabled}
|
||||
loading={assocLoading || updatingSound}
|
||||
onChange={(checked) => {
|
||||
updateNewMessageSound({
|
||||
variables: { id: association.id, value: checked },
|
||||
optimisticResponse: {
|
||||
update_associations_by_pk: {
|
||||
__typename: "associations",
|
||||
id: association.id,
|
||||
new_message_sound: checked
|
||||
}
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
notification.success({
|
||||
message: checked
|
||||
? t("user.labels.notification_sound_enabled")
|
||||
: t("user.labels.notification_sound_disabled")
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
notification.error({ message: e.message || "Failed to update setting" });
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
<Typography.Paragraph type="secondary" style={{ marginTop: 8 }}>
|
||||
{t("user.labels.notification_sound_help")}
|
||||
</Typography.Paragraph>
|
||||
</Card>
|
||||
</Col>
|
||||
)}
|
||||
|
||||
{scenarioNotificationsOn && (
|
||||
<Col span={24}>
|
||||
<NotificationSettingsForm />
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useEffect } from "react";
|
||||
import { useCookies } from "react-cookie";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import QboSignIn from "../../assets/qbo/C2QB_green_btn_med_default.svg";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
|
||||
export default function QboAuthorizeComponent() {
|
||||
const location = useLocation();
|
||||
@@ -12,6 +13,7 @@ export default function QboAuthorizeComponent() {
|
||||
const [setCookie] = useCookies(["access_token", "refresh_token"]);
|
||||
|
||||
const handleQbSignIn = async () => {
|
||||
logImEXEvent("qbo_sign_in_clicked");
|
||||
const result = await Axios.post("/qbo/authorize");
|
||||
window.location.href = result.data;
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import BlurWrapperComponent from "../feature-wrapper/blur-wrapper.component";
|
||||
import { upsellEnum, UpsellMaskWrapper } from "../upsell/upsell.component";
|
||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
@@ -79,7 +80,12 @@ export function ScheduleCalendarHeaderGraph({ bodyshop, loadData }) {
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover trigger="hover" placement="bottom" content={popContent}>
|
||||
<Popover
|
||||
trigger="hover"
|
||||
placement="bottom"
|
||||
onOpenChange={(open) => open && logImEXEvent("schedule_spider_graph", {})}
|
||||
content={popContent}
|
||||
>
|
||||
<RadarChartOutlined />
|
||||
</Popover>
|
||||
);
|
||||
|
||||
@@ -19,6 +19,7 @@ import OwnerNameDisplay from "../owner-name-display/owner-name-display.component
|
||||
import ScheduleBlockDay from "../schedule-block-day/schedule-block-day.component";
|
||||
import UpsellComponent, { upsellEnum } from "../upsell/upsell.component";
|
||||
import ScheduleCalendarHeaderGraph from "./schedule-calendar-header-graph.component";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -142,6 +143,7 @@ export function ScheduleCalendarHeaderComponent({ bodyshop, label, refetch, date
|
||||
content={jobsInPopup}
|
||||
trigger="hover"
|
||||
title={t("appointments.labels.arrivingjobs")}
|
||||
onOpenChange={(open) => open && logImEXEvent("schedule_popover_arriving_jobs", {})}
|
||||
>
|
||||
<Space size="small">
|
||||
<Icon component={MdFileDownload} style={{ color: "green" }} />
|
||||
@@ -159,6 +161,7 @@ export function ScheduleCalendarHeaderComponent({ bodyshop, label, refetch, date
|
||||
content={jobsOutPopup}
|
||||
trigger="hover"
|
||||
title={t("appointments.labels.completingjobs")}
|
||||
onOpenChange={(open) => open && logImEXEvent("schedule_popover_departing_jobs", {})}
|
||||
>
|
||||
<Space size="small">
|
||||
<Icon component={MdFileUpload} style={{ color: "red" }} />
|
||||
|
||||
@@ -13,6 +13,7 @@ import Event from "../job-at-change/schedule-event.container";
|
||||
import JobDetailCards from "../job-detail-cards/job-detail-cards.component";
|
||||
import local from "./localizer";
|
||||
import HeaderComponent from "./schedule-calendar-header.component";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import "./schedule-calendar.styles.scss";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
@@ -40,27 +41,26 @@ export function ScheduleCalendarWrapperComponent({
|
||||
const currentView = search.view || defaultView || "week";
|
||||
|
||||
const handleEventPropStyles = (event) => {
|
||||
const hasColor = Boolean(event?.color?.hex || event?.color);
|
||||
const { color, block, arrived } = event ?? {};
|
||||
const hasColor = Boolean(color?.hex || color);
|
||||
const useBg = currentView !== "agenda";
|
||||
|
||||
// Prioritize explicit blocked-day background to ensure red in all themes
|
||||
let bg;
|
||||
if (useBg) {
|
||||
if (event?.block) {
|
||||
bg = "var(--event-block-bg)";
|
||||
} else if (hasColor) {
|
||||
bg = event?.color?.hex ?? event?.color;
|
||||
} else {
|
||||
bg = "var(--event-bg-fallback)";
|
||||
}
|
||||
bg = block
|
||||
? "var(--event-block-bg)"
|
||||
: arrived
|
||||
? "var(--event-arrived-bg)"
|
||||
: (color?.hex ?? color ?? "var(--event-bg-fallback)");
|
||||
}
|
||||
|
||||
const usedFallback = !hasColor && !event?.block; // only mark as fallback when not blocked
|
||||
const usedFallback = !hasColor && !block && !arrived; // only mark as fallback when not blocked or arrived
|
||||
|
||||
const classes = [
|
||||
"imex-event",
|
||||
event.arrived && "imex-event-arrived",
|
||||
event.block && "imex-event-block",
|
||||
arrived && "imex-event-arrived",
|
||||
block && "imex-event-block",
|
||||
usedFallback && "imex-event-fallback"
|
||||
]
|
||||
.filter(Boolean)
|
||||
@@ -140,6 +140,8 @@ export function ScheduleCalendarWrapperComponent({
|
||||
}}
|
||||
onView={(view) => {
|
||||
search.view = view;
|
||||
logImEXEvent("schedule_change_view", { view });
|
||||
|
||||
history({ search: queryString.stringify(search) });
|
||||
}}
|
||||
step={15}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import _ from "lodash";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
@@ -80,6 +81,7 @@ export function ScheduleCalendarComponent({ data, refetch, bodyshop }) {
|
||||
value={[...estimatorsFilter]}
|
||||
onChange={(e) => {
|
||||
setEstimatiorsFilter(e);
|
||||
logImEXEvent("schedule_filter_by_estimator", { estimators: e });
|
||||
}}
|
||||
options={estimators.map((e) => ({
|
||||
label: e,
|
||||
@@ -95,6 +97,7 @@ export function ScheduleCalendarComponent({ data, refetch, bodyshop }) {
|
||||
value={filter?.ins_co_nm ? filter.ins_co_nm : []}
|
||||
onChange={(e) => {
|
||||
setFilter({ ...filter, ins_co_nm: e });
|
||||
logImEXEvent("schedule_filter_by_ins_co_nm", { ins_co_nm: e });
|
||||
}}
|
||||
options={bodyshop.md_ins_cos.map((i) => ({
|
||||
label: i.name,
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { UPDATE_SCOREBOARD_ENTRY } from "../../graphql/scoreboard.queries";
|
||||
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils.js";
|
||||
|
||||
export default function ScoreboardEntryEdit({ entry }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
@@ -16,6 +17,7 @@ export default function ScoreboardEntryEdit({ entry }) {
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
setLoading(true);
|
||||
logImEXEvent("scoreboard_edit_job", {});
|
||||
values.date = dayjs(values.date).format("YYYY-MM-DD");
|
||||
const result = await updateScoreboardentry({
|
||||
variables: { sbId: entry.id, sbInput: values }
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
@@ -52,6 +53,7 @@ const ShareToTeamsComponent = ({
|
||||
const teamsShareUrl = `https://teams.microsoft.com/share?href=${currentUrl}&preText=${messageText}&title=${pageTitle}`;
|
||||
// Function to open the centered share link in a new window/tab
|
||||
const handleShare = () => {
|
||||
logImEXEvent("share_to_teams", {});
|
||||
const screenWidth = window.screen.width;
|
||||
const screenHeight = window.screen.height;
|
||||
const windowWidth = 600;
|
||||
|
||||
@@ -23,13 +23,24 @@ export default function ShopInfoContainer() {
|
||||
});
|
||||
const notification = useNotification();
|
||||
|
||||
const combinedFeatureConfig = {
|
||||
...FEATURE_CONFIGS.general,
|
||||
...FEATURE_CONFIGS.responsibilitycenters
|
||||
};
|
||||
const combineFeatureConfigs = (...configs) =>
|
||||
(configs || [])
|
||||
.filter(Boolean)
|
||||
.flatMap((cfg) => Object.entries(cfg))
|
||||
.reduce((acc, [featureName, fieldPaths]) => {
|
||||
if (!Array.isArray(fieldPaths)) return acc;
|
||||
acc[featureName] = [...(acc[featureName] ?? []), ...fieldPaths];
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const combinedFeatureConfig = combineFeatureConfigs(FEATURE_CONFIGS.general, FEATURE_CONFIGS.responsibilitycenters);
|
||||
|
||||
// Use form data preservation for all shop-info features
|
||||
const { createSubmissionHandler } = useFormDataPreservation(form, data?.bodyshops[0], combinedFeatureConfig);
|
||||
const { createSubmissionHandler, preserveHiddenFormData } = useFormDataPreservation(
|
||||
form,
|
||||
data?.bodyshops[0],
|
||||
combinedFeatureConfig
|
||||
);
|
||||
|
||||
const handleFinish = createSubmissionHandler((values) => {
|
||||
setSaveLoading(true);
|
||||
@@ -51,8 +62,11 @@ export default function ShopInfoContainer() {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) form.resetFields();
|
||||
}, [form, data]);
|
||||
if (!data) return;
|
||||
form.resetFields();
|
||||
// After reset, re-apply hidden field preservation so values aren't wiped
|
||||
preserveHiddenFormData();
|
||||
}, [data, form, preserveHiddenFormData]);
|
||||
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
if (loading) return <LoadingSpinner />;
|
||||
|
||||
@@ -1183,7 +1183,17 @@ export function ShopInfoGeneral({ form, bodyshop }) {
|
||||
{fields.map((field, index) => (
|
||||
<Form.Item key={field.key}>
|
||||
<LayoutFormRow noDivider>
|
||||
<Form.Item label={t("general.labels.label")} key={`${index}label`} name={[field.name, "label"]}>
|
||||
<Form.Item
|
||||
label={t("general.labels.label")}
|
||||
key={`${index}label`}
|
||||
name={[field.name, "label"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
@@ -1294,7 +1304,17 @@ export function ShopInfoGeneral({ form, bodyshop }) {
|
||||
{fields.map((field, index) => (
|
||||
<Form.Item key={field.key}>
|
||||
<LayoutFormRow noDivider>
|
||||
<Form.Item label={t("general.labels.label")} key={`${index}label`} name={[field.name, "label"]}>
|
||||
<Form.Item
|
||||
label={t("general.labels.label")}
|
||||
key={`${index}label`}
|
||||
name={[field.name, "label"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
@@ -1483,15 +1503,31 @@ export function ShopInfoGeneral({ form, bodyshop }) {
|
||||
{fields.map((field, index) => (
|
||||
<Form.Item key={field.key}>
|
||||
<LayoutFormRow noDivider>
|
||||
<Form.Item label={t("general.labels.label")} key={`${index}label`} name={[field.name, "label"]}>
|
||||
<Form.Item
|
||||
label={t("general.labels.label")}
|
||||
key={`${index}label`}
|
||||
name={[field.name, "label"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.labels.md_to_emails_emails")}
|
||||
key={`${index}emails`}
|
||||
name={[field.name, "emails"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select mode="tags" tokenSeparators={[",", ";"]} />
|
||||
<FormItemEmail email={form.getFieldValue([field.name, "emails"])} />
|
||||
</Form.Item>
|
||||
|
||||
<Space>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useCallback, useEffect, useMemo } from "react";
|
||||
import { HasFeatureAccess } from "./../feature-wrapper/feature-wrapper.component";
|
||||
|
||||
/**
|
||||
@@ -8,73 +8,57 @@ import { HasFeatureAccess } from "./../feature-wrapper/feature-wrapper.component
|
||||
* @param {Object} featureConfig - Configuration object defining which features and their associated fields to preserve
|
||||
*/
|
||||
export const useFormDataPreservation = (form, bodyshop, featureConfig) => {
|
||||
const getNestedValue = (obj, path) => {
|
||||
return path.reduce((current, key) => current?.[key], obj);
|
||||
};
|
||||
|
||||
// Safe nested getters/setters using path arrays
|
||||
const getNestedValue = (obj, path) => path?.reduce((acc, key) => acc?.[key], obj);
|
||||
const setNestedValue = (obj, path, value) => {
|
||||
const lastKey = path[path.length - 1];
|
||||
const parentPath = path.slice(0, -1);
|
||||
|
||||
const parent = parentPath.reduce((current, key) => {
|
||||
if (!current[key]) current[key] = {};
|
||||
return current[key];
|
||||
const parent = path.slice(0, -1).reduce((curr, key) => {
|
||||
if (!curr[key] || typeof curr[key] !== "object") curr[key] = {};
|
||||
return curr[key];
|
||||
}, obj);
|
||||
|
||||
parent[lastKey] = value;
|
||||
};
|
||||
|
||||
const preserveHiddenFormData = useCallback(() => {
|
||||
const preservationData = {};
|
||||
let hasDataToPreserve = false;
|
||||
|
||||
// Paths for features that are NOT accessible
|
||||
const disabledPaths = useMemo(() => {
|
||||
const result = [];
|
||||
if (!featureConfig) return result;
|
||||
Object.entries(featureConfig).forEach(([featureName, fieldPaths]) => {
|
||||
const hasAccess = HasFeatureAccess({ featureName, bodyshop });
|
||||
if (hasAccess || !Array.isArray(fieldPaths)) return;
|
||||
fieldPaths.forEach((p) => Array.isArray(p) && p.length && result.push(p));
|
||||
});
|
||||
return result;
|
||||
}, [featureConfig, bodyshop]);
|
||||
|
||||
if (!hasAccess) {
|
||||
fieldPaths.forEach((fieldPath) => {
|
||||
const currentValues = form.getFieldsValue();
|
||||
let value = getNestedValue(currentValues, fieldPath);
|
||||
const preserveHiddenFormData = useCallback(() => {
|
||||
const currentValues = form.getFieldsValue();
|
||||
const preservationData = {};
|
||||
let hasAny = false;
|
||||
|
||||
if (value === undefined || value === null) {
|
||||
value = getNestedValue(bodyshop, fieldPath);
|
||||
}
|
||||
|
||||
if (value !== undefined && value !== null) {
|
||||
setNestedValue(preservationData, fieldPath, value);
|
||||
hasDataToPreserve = true;
|
||||
}
|
||||
});
|
||||
disabledPaths.forEach((path) => {
|
||||
let value = getNestedValue(currentValues, path);
|
||||
if (value == null) value = getNestedValue(bodyshop, path);
|
||||
if (value != null) {
|
||||
setNestedValue(preservationData, path, value);
|
||||
hasAny = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (hasDataToPreserve) {
|
||||
form.setFieldsValue(preservationData);
|
||||
}
|
||||
}, [form, featureConfig, bodyshop]);
|
||||
if (hasAny) form.setFieldsValue(preservationData);
|
||||
}, [form, bodyshop, disabledPaths]);
|
||||
|
||||
const getCompleteFormValues = () => {
|
||||
const currentFormValues = form.getFieldsValue();
|
||||
const completeValues = { ...currentFormValues };
|
||||
const currentValues = form.getFieldsValue();
|
||||
const complete = { ...currentValues };
|
||||
|
||||
Object.entries(featureConfig).forEach(([featureName, fieldPaths]) => {
|
||||
const hasAccess = HasFeatureAccess({ featureName, bodyshop });
|
||||
|
||||
if (!hasAccess) {
|
||||
fieldPaths.forEach((fieldPath) => {
|
||||
let value = getNestedValue(currentFormValues, fieldPath);
|
||||
if (value === undefined || value === null) {
|
||||
value = getNestedValue(bodyshop, fieldPath);
|
||||
}
|
||||
|
||||
if (value !== undefined && value !== null) {
|
||||
setNestedValue(completeValues, fieldPath, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
disabledPaths.forEach((path) => {
|
||||
let value = getNestedValue(currentValues, path);
|
||||
if (value == null) value = getNestedValue(bodyshop, path);
|
||||
if (value != null) setNestedValue(complete, path, value);
|
||||
});
|
||||
|
||||
return completeValues;
|
||||
return complete;
|
||||
};
|
||||
|
||||
const createSubmissionHandler = (originalHandler) => {
|
||||
@@ -103,8 +87,8 @@ export const FEATURE_CONFIGS = {
|
||||
["md_responsibility_centers", "profits"],
|
||||
["md_responsibility_centers", "defaults"],
|
||||
["md_responsibility_centers", "dms_defaults"],
|
||||
["md_responsibility_centers", "taxes", "itemexemptcode"],
|
||||
["md_responsibility_centers", "taxes", "invoiceexemptcode"],
|
||||
["md_responsibility_centers", "taxes"],
|
||||
["md_responsibility_centers", "cieca_pfl"],
|
||||
["md_responsibility_centers", "ar"],
|
||||
["md_responsibility_centers", "refund"],
|
||||
["md_responsibility_centers", "sales_tax_codes"],
|
||||
|
||||
@@ -9,7 +9,6 @@ import { connect } from "react-redux";
|
||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { pageLimit } from "../../utils/config";
|
||||
import { alphaSort, statusSort } from "../../utils/sorters";
|
||||
import useLocalStorage from "../../utils/useLocalStorage";
|
||||
@@ -97,14 +96,14 @@ export function SimplifiedPartsJobsListComponent({
|
||||
|
||||
ellipsis: true,
|
||||
sorter: search?.search
|
||||
? (a, b) => statusSort(a.status, b.status, bodyshop.md_ro_statuses.parts_active_statuses)
|
||||
? (a, b) => statusSort(a.status, b.status, bodyshop.md_ro_statuses?.parts_active_statuses)
|
||||
: true,
|
||||
sortOrder: sortcolumn === "status" && sortorder,
|
||||
render: (text, record) => {
|
||||
return record.status || t("general.labels.na");
|
||||
},
|
||||
filteredValue: filter?.status || null,
|
||||
filters: bodyshop.md_ro_statuses.parts_statuses.map((s) => {
|
||||
filters: bodyshop.md_ro_statuses?.parts_statuses.map((s) => {
|
||||
return { text: s, value: [s] };
|
||||
}),
|
||||
onFilter: (value, record) => value.includes(record.status)
|
||||
@@ -144,31 +143,11 @@ export function SimplifiedPartsJobsListComponent({
|
||||
sortOrder: sortcolumn === "clm_no" && sortorder,
|
||||
render: (text, record) => `${record.clm_no || ""}${record.po_number ? ` (PO: ${record.po_number})` : ""}`
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ins_co_nm"),
|
||||
dataIndex: "ins_co_nm",
|
||||
key: "ins_co_nm",
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.clm_total"),
|
||||
dataIndex: "clm_total",
|
||||
key: "clm_total",
|
||||
sorter: search?.search ? (a, b) => a.clm_total - b.clm_total : true,
|
||||
sortOrder: sortcolumn === "clm_total" && sortorder,
|
||||
render: (text, record) => {
|
||||
return record.clm_total ? (
|
||||
<CurrencyFormatter>{record.clm_total}</CurrencyFormatter>
|
||||
) : (
|
||||
t("general.labels.unknown")
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.partsstatus"),
|
||||
dataIndex: "partsstatus",
|
||||
key: "partsstatus",
|
||||
render: (text, record) => <JobPartsQueueCount style={{ minWidth: "10rem" }} parts={record.joblines_status} />
|
||||
render: (text, record) => <JobPartsQueueCount parts={record.joblines_status} />
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.comment"),
|
||||
|
||||
@@ -19,6 +19,7 @@ import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter.jsx"
|
||||
import dayjs from "../../utils/day";
|
||||
import ShareToTeamsButton from "../share-to-teams/share-to-teams.component.jsx";
|
||||
import PriorityLabel from "../../utils/tasksPriorityLabel.jsx";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils.js";
|
||||
|
||||
/**
|
||||
* Task List Component
|
||||
@@ -289,6 +290,7 @@ function TaskListComponent({
|
||||
} else {
|
||||
delete search[param];
|
||||
}
|
||||
logImEXEvent("tasks_filter", { key: param });
|
||||
history({ search: queryString.stringify(search) });
|
||||
},
|
||||
[history, search]
|
||||
|
||||
@@ -13,6 +13,7 @@ import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors.js";
|
||||
import dayjs from "../../utils/day";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -107,6 +108,7 @@ export function TaskListContainer({
|
||||
})
|
||||
);
|
||||
}
|
||||
logImEXEvent("task_completed", {});
|
||||
|
||||
notification["success"]({
|
||||
message: t("tasks.successes.completed")
|
||||
@@ -160,6 +162,7 @@ export function TaskListContainer({
|
||||
);
|
||||
}
|
||||
|
||||
logImEXEvent("task_deleted", {});
|
||||
notification["success"]({
|
||||
message: t("tasks.successes.deleted")
|
||||
});
|
||||
|
||||
@@ -17,6 +17,7 @@ import AuditTrailMapping from "../../utils/AuditTrailMappings.js";
|
||||
import { isEqual } from "lodash";
|
||||
import refetchRouteMappings from "./task-upsert-modal.route.mappings";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
@@ -180,7 +181,7 @@ export function TaskUpsertModalContainer({ bodyshop, currentUser, taskUpsert, to
|
||||
notification["success"]({
|
||||
message: t("tasks.successes.updated")
|
||||
});
|
||||
|
||||
logImEXEvent("task_update", {});
|
||||
toggleModalVisible();
|
||||
};
|
||||
|
||||
@@ -217,7 +218,7 @@ export function TaskUpsertModalContainer({ bodyshop, currentUser, taskUpsert, to
|
||||
|
||||
form.resetFields();
|
||||
toggleModalVisible();
|
||||
|
||||
logImEXEvent("task_insert", {});
|
||||
notification["success"]({
|
||||
message: t("tasks.successes.created")
|
||||
});
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { useState } from "react";
|
||||
import { Button, Form, Popconfirm } from "antd";
|
||||
import { PageHeader } from "@ant-design/pro-layout";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import VehicleDetailFormComponent from "./vehicle-detail-form.component";
|
||||
import { Button, Form, Popconfirm } from "antd";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import dayjs from "../../utils/day";
|
||||
import { DELETE_VEHICLE, UPDATE_VEHICLE } from "../../graphql/vehicles.queries";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils.js";
|
||||
import { DELETE_VEHICLE, UPDATE_VEHICLE } from "../../graphql/vehicles.queries";
|
||||
import dayjs from "../../utils/day";
|
||||
import VehicleDetailFormComponent from "./vehicle-detail-form.component";
|
||||
|
||||
function VehicleDetailFormContainer({ vehicle, refetch }) {
|
||||
const { t } = useTranslation();
|
||||
@@ -20,6 +21,7 @@ function VehicleDetailFormContainer({ vehicle, refetch }) {
|
||||
|
||||
const handleDelete = async () => {
|
||||
setLoading(true);
|
||||
logImEXEvent("vehicle_delete", {});
|
||||
const result = await deleteVehicle({
|
||||
variables: { id: vehicle.id }
|
||||
});
|
||||
@@ -42,6 +44,7 @@ function VehicleDetailFormContainer({ vehicle, refetch }) {
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
setLoading(true);
|
||||
logImEXEvent("vehicle_update", {});
|
||||
const result = await updateVehicle({
|
||||
variables: { vehId: vehicle.id, vehicle: values }
|
||||
});
|
||||
|
||||
@@ -14,16 +14,18 @@ import VendorsPhonebookAdd from "../vendors-phonebook-add/vendors-phonebook-add.
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { selectIsPartsEntry } from "../../redux/application/application.selectors.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
bodyshop: selectBodyshop,
|
||||
isPartsEntry: selectIsPartsEntry
|
||||
});
|
||||
const mapDispatchToProps = () => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(VendorsFormComponent);
|
||||
|
||||
export function VendorsFormComponent({ bodyshop, form, formLoading, handleDelete, selectedvendor }) {
|
||||
export function VendorsFormComponent({ bodyshop, form, formLoading, handleDelete, selectedvendor, isPartsEntry }) {
|
||||
const { t } = useTranslation();
|
||||
const client = useApolloClient();
|
||||
|
||||
@@ -57,8 +59,7 @@ export function VendorsFormComponent({ bodyshop, form, formLoading, handleDelete
|
||||
>
|
||||
{t("general.actions.delete")}
|
||||
</Button>
|
||||
|
||||
<VendorsPhonebookAdd form={form} disabled={form.isFieldsTouched()} />
|
||||
{!isPartsEntry && <VendorsPhonebookAdd form={form} disabled={form.isFieldsTouched()} />}
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
@@ -148,12 +149,18 @@ export function VendorsFormComponent({ bodyshop, form, formLoading, handleDelete
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow grow>
|
||||
<Form.Item label={t("vendors.fields.discount")} name="discount">
|
||||
<InputNumber min={0} max={1} precision={2} step={0.01} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("vendors.fields.due_date")} name="due_date">
|
||||
<InputNumber min={0} />
|
||||
</Form.Item>
|
||||
{!isPartsEntry && (
|
||||
<>
|
||||
<Form.Item label={t("vendors.fields.discount")} name="discount">
|
||||
<InputNumber min={0} max={1} precision={2} step={0.01} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t("vendors.fields.due_date")} name="due_date">
|
||||
<InputNumber min={0} />
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
{
|
||||
// <Form.Item
|
||||
// label={t("vendors.fields.cost_center")}
|
||||
@@ -173,7 +180,7 @@ export function VendorsFormComponent({ bodyshop, form, formLoading, handleDelete
|
||||
|
||||
<Form.Item
|
||||
name="tags"
|
||||
label={t("vendor.fields.tags")}
|
||||
label={t("vendors.fields.tags")}
|
||||
rules={[
|
||||
{
|
||||
//message: t("general.validation.required"),
|
||||
|
||||
@@ -76,9 +76,11 @@ export const logImEXEvent = (eventName, additionalParams, stateProp = null) => {
|
||||
try {
|
||||
|
||||
const state = stateProp || store.getState();
|
||||
|
||||
const eventParams = {
|
||||
shop: (state.user && state.user.bodyshop && state.user.bodyshop.shopname) || null,
|
||||
user: (state.user && state.user.currentUser && state.user.currentUser.email) || null,
|
||||
partsManagementOnly: state?.user?.partsManagementOnly,
|
||||
...additionalParams
|
||||
};
|
||||
// axios.post("/ioevent", {
|
||||
@@ -89,12 +91,12 @@ export const logImEXEvent = (eventName, additionalParams, stateProp = null) => {
|
||||
// dbevent: false,
|
||||
// env: `master-AIO|${import.meta.env.VITE_APP_GIT_SHA_DATE}`
|
||||
// });
|
||||
// console.log(
|
||||
// "%c[Analytics]",
|
||||
// "background-color: green ;font-weight:bold;",
|
||||
// eventName,
|
||||
// eventParams
|
||||
// );
|
||||
console.log(
|
||||
"%c[Analytics]",
|
||||
"background-color: green ;font-weight:bold;",
|
||||
eventName,
|
||||
eventParams
|
||||
);
|
||||
logEvent(analytics, eventName, eventParams);
|
||||
amplitude.track(eventName, eventParams);
|
||||
posthog.capture(eventName, eventParams);
|
||||
|
||||
@@ -20,8 +20,8 @@ export const DELETE_BILL = gql`
|
||||
`;
|
||||
|
||||
export const QUERY_ALL_BILLS_PAGINATED = gql`
|
||||
query QUERY_ALL_BILLS_PAGINATED($offset: Int, $limit: Int, $order: [bills_order_by!]!) {
|
||||
bills(offset: $offset, limit: $limit, order_by: $order) {
|
||||
query QUERY_ALL_BILLS_PAGINATED($offset: Int, $limit: Int, $order: [bills_order_by!]!, $where: bills_bool_exp) {
|
||||
bills(offset: $offset, limit: $limit, order_by: $order, where: $where) {
|
||||
id
|
||||
vendorid
|
||||
vendor {
|
||||
|
||||
@@ -5,6 +5,7 @@ export const QUERY_SHOP_ASSOCIATIONS = gql`
|
||||
associations(where: { shopid: { _eq: $shopid } }) {
|
||||
id
|
||||
authlevel
|
||||
new_message_sound
|
||||
shopid
|
||||
user {
|
||||
email
|
||||
@@ -28,6 +29,26 @@ export const UPDATE_ASSOCIATION = gql`
|
||||
}
|
||||
`;
|
||||
|
||||
// Query to load the active association for a given user and get the new_message_sound flag
|
||||
export const QUERY_ACTIVE_ASSOCIATION_SOUND = gql`
|
||||
query QUERY_ACTIVE_ASSOCIATION_SOUND($email: String!) {
|
||||
associations(where: { _and: { useremail: { _eq: $email }, active: { _eq: true } } }) {
|
||||
id
|
||||
new_message_sound
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// Mutation to update just the new_message_sound field
|
||||
export const UPDATE_NEW_MESSAGE_SOUND = gql`
|
||||
mutation UPDATE_NEW_MESSAGE_SOUND($id: uuid!, $value: Boolean) {
|
||||
update_associations_by_pk(pk_columns: { id: $id }, _set: { new_message_sound: $value }) {
|
||||
id
|
||||
new_message_sound
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const INSERT_EULA_ACCEPTANCE = gql`
|
||||
mutation INSERT_EULA_ACCEPTANCE($eulaAcceptance: eula_acceptances_insert_input!) {
|
||||
insert_eula_acceptances_one(object: $eulaAcceptance) {
|
||||
@@ -77,6 +98,7 @@ export const QUERY_KANBAN_SETTINGS = gql`
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const UPDATE_KANBAN_SETTINGS = gql`
|
||||
mutation UPDATE_KANBAN_SETTINGS($id: uuid!, $ks: jsonb) {
|
||||
update_associations_by_pk(pk_columns: { id: $id }, _set: { kanban_settings: $ks }) {
|
||||
|
||||
@@ -16,6 +16,7 @@ import "./translations/i18n";
|
||||
import "./utils/CleanAxios";
|
||||
import * as amplitude from "@amplitude/analytics-browser";
|
||||
import { PostHogProvider } from "posthog-js/react";
|
||||
import posthog from "posthog-js";
|
||||
|
||||
window.global ||= window;
|
||||
|
||||
@@ -25,7 +26,30 @@ registerSW({ immediate: true });
|
||||
// Dinero.globalLocale = "en-CA";
|
||||
Dinero.globalRoundingMode = "HALF_EVEN";
|
||||
|
||||
amplitude.init("6228a598e57cd66875cfd41604f1f891", {});
|
||||
amplitude.init(import.meta.env.VITE_APP_AMP_KEY, {
|
||||
defaultTracking: true,
|
||||
serverUrl: import.meta.env.VITE_APP_AMP_URL
|
||||
// {
|
||||
// attribution: {
|
||||
// excludeReferrers: true,
|
||||
// initialEmptyValue: true,
|
||||
// resetSessionOnNewCampaign: true,
|
||||
// },
|
||||
// fileDownloads: true,
|
||||
// formInteractions: true,
|
||||
// pageViews: {
|
||||
// trackHistoryChanges: 'all'
|
||||
// },
|
||||
// sessions: true
|
||||
// }
|
||||
});
|
||||
|
||||
posthog.init(import.meta.env.VITE_PUBLIC_POSTHOG_KEY, {
|
||||
autocapture: false,
|
||||
capture_exceptions: true,
|
||||
api_host: import.meta.env.VITE_PUBLIC_POSTHOG_HOST
|
||||
});
|
||||
|
||||
const sentryCreateBrowserRouter = Sentry.wrapCreateBrowserRouterV6(createBrowserRouter);
|
||||
|
||||
const router = sentryCreateBrowserRouter(createRoutesFromElements(<Route path="*" element={<AppContainer />} />));
|
||||
@@ -39,10 +63,7 @@ function App() {
|
||||
return (
|
||||
<PersistGate loading={<LoadingSpinner message="Restoring your settings..." />} persistor={persistor}>
|
||||
<Provider store={store}>
|
||||
<PostHogProvider
|
||||
apiKey={import.meta.env.VITE_PUBLIC_POSTHOG_KEY}
|
||||
options={{ autocapture: false, capture_exceptions: true }}
|
||||
>
|
||||
<PostHogProvider client={posthog}>
|
||||
<RouterProvider router={router} />
|
||||
</PostHogProvider>
|
||||
</Provider>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { EditFilled, SyncOutlined } from "@ant-design/icons";
|
||||
import { Button, Card, Checkbox, Input, Space, Table, Typography } from "antd";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import axios from "axios";
|
||||
import queryString from "query-string";
|
||||
import { useEffect, useState } from "react";
|
||||
@@ -16,6 +17,8 @@ import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import { pageLimit } from "../../utils/config";
|
||||
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||
import useLocalStorage from "../../utils/useLocalStorage";
|
||||
import { QUERY_ALL_VENDORS } from "../../graphql/vendors.queries";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setBillEnterContext: (context) => dispatch(setModalContext({ context: context, modal: "billEnter" }))
|
||||
@@ -29,29 +32,25 @@ export function BillsListPage({ loading, data, refetch, total, setBillEnterConte
|
||||
const history = useNavigate();
|
||||
const [state, setState] = useLocalStorage("bills_list_sort", {
|
||||
sortedInfo: {},
|
||||
filteredInfo: { text: "" }
|
||||
filteredInfo: { vendorname: [] }
|
||||
});
|
||||
const Templates = TemplateList("bill");
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data: vendorsData } = useQuery(QUERY_ALL_VENDORS);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t("bills.fields.vendorname"),
|
||||
dataIndex: "vendorname",
|
||||
key: "vendorname",
|
||||
// sortObject: (direction) => {
|
||||
// return {
|
||||
// vendor: {
|
||||
// name: direction
|
||||
// ? direction === "descend"
|
||||
// ? "desc"
|
||||
// : "asc"
|
||||
// : "desc",
|
||||
// },
|
||||
// };
|
||||
// },
|
||||
// sorter: (a, b) => alphaSort(a.vendor.name, b.vendor.name),
|
||||
// sortOrder:
|
||||
// state.sortedInfo.columnKey === "vendorname" && state.sortedInfo.order,
|
||||
sorter: (a, b) => alphaSort(a.vendor.name, b.vendor.name),
|
||||
sortObject: (order) => ({
|
||||
vendor: { name: order === "descend" ? "desc" : "asc" }
|
||||
}),
|
||||
filters: (vendorsData?.vendors || []).map((v) => ({ text: v.name, value: v.id })),
|
||||
filteredValue: search.vendorIds ? search.vendorIds.split(",") : null,
|
||||
sortOrder: state.sortedInfo.columnKey === "vendorname" && state.sortedInfo.order,
|
||||
render: (text, record) => <span>{record.vendor.name}</span>
|
||||
},
|
||||
{
|
||||
@@ -65,20 +64,11 @@ export function BillsListPage({ loading, data, refetch, total, setBillEnterConte
|
||||
title: t("jobs.fields.ro_number"),
|
||||
dataIndex: "ro_number",
|
||||
key: "ro_number",
|
||||
// sortObject: (direction) => {
|
||||
// return {
|
||||
// job: {
|
||||
// ro_number: direction
|
||||
// ? direction === "descend"
|
||||
// ? "desc"
|
||||
// : "asc"
|
||||
// : "desc",
|
||||
// },
|
||||
// };
|
||||
// },
|
||||
// sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
|
||||
// sortOrder:
|
||||
// state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
|
||||
sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
|
||||
sortObject: (order) => ({
|
||||
job: { ro_number: order === "descend" ? "desc" : "asc" }
|
||||
}),
|
||||
sortOrder: state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
|
||||
render: (text, record) => record.job && <Link to={`/manage/jobs/${record.job.id}`}>{record.job.ro_number}</Link>
|
||||
},
|
||||
{
|
||||
@@ -175,19 +165,37 @@ export function BillsListPage({ loading, data, refetch, total, setBillEnterConte
|
||||
];
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||
setState({
|
||||
sortedInfo: sorter,
|
||||
filteredInfo: { ...state.filteredInfo, vendorname: filters.vendorname || [] }
|
||||
});
|
||||
|
||||
search.page = pagination.current;
|
||||
if (filters.vendorname && filters.vendorname.length) {
|
||||
search.vendorIds = filters.vendorname.join(",");
|
||||
} else {
|
||||
delete search.vendorIds;
|
||||
}
|
||||
if (sorter && sorter.column && sorter.column.sortObject) {
|
||||
search.searchObj = JSON.stringify(sorter.column.sortObject(sorter.order));
|
||||
delete search.sortcolumn;
|
||||
delete search.sortorder;
|
||||
} else {
|
||||
delete search.searchObj;
|
||||
search.sortcolumn = sorter.order ? sorter.columnKey : null;
|
||||
search.sortorder = sorter.order;
|
||||
}
|
||||
search.sort = JSON.stringify({ [sorter.columnKey]: sorter.order });
|
||||
history({ search: queryString.stringify(search) });
|
||||
logImEXEvent("bills_list_sort_filter", { pagination, filters, sorter });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!search.vendorIds && state.filteredInfo.vendorname && state.filteredInfo.vendorname.length) {
|
||||
search.vendorIds = state.filteredInfo.vendorname.join(",");
|
||||
history({ search: queryString.stringify(search) });
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (search.search && search.search.trim() !== "") {
|
||||
searchBills();
|
||||
@@ -201,6 +209,7 @@ export function BillsListPage({ loading, data, refetch, total, setBillEnterConte
|
||||
search: value || search.search,
|
||||
index: "bills"
|
||||
});
|
||||
logImEXEvent("bills_search", { search: value || search.search, results: searchData?.data?.hits?.hits?.length });
|
||||
setOpenSearchResults(searchData.data.hits.hits.map((s) => s._source));
|
||||
} catch (error) {
|
||||
console.log("Error while fetching search results", error);
|
||||
|
||||
@@ -49,7 +49,8 @@ export function BillsPageContainer({ setBreadcrumbs, setSelectedHeader }) {
|
||||
: {
|
||||
[sortcolumn || "date"]: sortorder ? (sortorder === "descend" ? "desc" : "asc") : "desc"
|
||||
}
|
||||
]
|
||||
],
|
||||
where: searchParams.vendorIds ? { vendorid: { _in: searchParams.vendorIds.split(",") } } : undefined
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import ContractCreatePageComponent from "./contract-create.page.component";
|
||||
import UpsellComponent, { upsellEnum } from "../../components/upsell/upsell.component";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
@@ -57,6 +58,7 @@ export function ContractCreatePageContainer({ bodyshop, setBreadcrumbs, setSelec
|
||||
|
||||
if (!result.errors) {
|
||||
//Update the courtesy car to have the damage.
|
||||
logImEXEvent("courtesy_car_contract_created", {});
|
||||
notification["success"]({
|
||||
message: t("contracts.successes.saved")
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@ import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import JobsCreateComponent from "./jobs-create.component";
|
||||
import JobCreateContext from "./jobs-create.context";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
@@ -70,6 +70,7 @@ function JobsCreateContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
|
||||
label: t("titles.bc.jobs-new")
|
||||
}
|
||||
]);
|
||||
logImEXEvent("manual_job_create_start", {});
|
||||
}, [t, setBreadcrumbs, setSelectedHeader]);
|
||||
|
||||
const runInsertJob = (job) => {
|
||||
@@ -81,6 +82,7 @@ function JobsCreateContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
|
||||
error: null,
|
||||
newJobId: resp.data.insert_jobs.returning[0].id
|
||||
});
|
||||
logImEXEvent("manual_job_create_completed", {});
|
||||
})
|
||||
.catch((error) => {
|
||||
notification["error"]({
|
||||
|
||||
@@ -20,7 +20,12 @@ import LoadingSpinner from "../../components/loading-spinner/loading-spinner.com
|
||||
import PartnerPingComponent from "../../components/partner-ping/partner-ping.component";
|
||||
import ShopSubStatusComponent from "../../components/shop-sub-status/shop-sub-status.component";
|
||||
import UpdateAlert from "../../components/update-alert/update-alert.component";
|
||||
import { selectBodyshop, selectInstanceConflict, selectPartsManagementOnly } from "../../redux/user/user.selectors";
|
||||
import {
|
||||
selectBodyshop,
|
||||
selectCurrentUser,
|
||||
selectInstanceConflict,
|
||||
selectPartsManagementOnly
|
||||
} from "../../redux/user/user.selectors";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr.js";
|
||||
import useAlertsNotifications from "../../hooks/useAlertsNotifications.jsx";
|
||||
import { selectDarkMode } from "../../redux/application/application.selectors.js";
|
||||
@@ -109,10 +114,11 @@ const mapStateToProps = createStructuredSelector({
|
||||
conflict: selectInstanceConflict,
|
||||
bodyshop: selectBodyshop,
|
||||
partsManagementOnly: selectPartsManagementOnly,
|
||||
isDarkMode: selectDarkMode
|
||||
isDarkMode: selectDarkMode,
|
||||
currentUser: selectCurrentUser
|
||||
});
|
||||
|
||||
export function Manage({ conflict, bodyshop, partsManagementOnly, isDarkMode }) {
|
||||
export function Manage({ conflict, bodyshop, partsManagementOnly, isDarkMode, currentUser }) {
|
||||
const { t } = useTranslation();
|
||||
const [chatVisible] = useState(false);
|
||||
const didMount = useRef(false);
|
||||
@@ -588,7 +594,7 @@ export function Manage({ conflict, bodyshop, partsManagementOnly, isDarkMode })
|
||||
|
||||
return (
|
||||
<>
|
||||
<ChatAffixContainer bodyshop={bodyshop} chatVisible={chatVisible} />
|
||||
<ChatAffixContainer bodyshop={bodyshop} currentUser={currentUser} chatVisible={chatVisible} />
|
||||
<Layout style={{ minHeight: "100vh" }} className="layout-container">
|
||||
<UpdateAlert />
|
||||
<HeaderContainer />
|
||||
|
||||
@@ -11,7 +11,6 @@ import { createStructuredSelector } from "reselect";
|
||||
import FormFieldsChanged from "../../components/form-fields-changed-alert/form-fields-changed-alert.component.jsx";
|
||||
import JobsLinesContainer from "../../components/job-detail-lines/job-lines.container.jsx";
|
||||
import JobLineUpsertModalContainer from "../../components/job-lines-upsert-modal/job-lines-upsert-modal.container.jsx";
|
||||
import JobProfileDataWarning from "../../components/job-profile-data-warning/job-profile-data-warning.component.jsx";
|
||||
import JobsChangeStatus from "../../components/jobs-change-status/jobs-change-status.component.jsx";
|
||||
import JobsDetailHeaderActions from "../../components/jobs-detail-header-actions/jobs-detail-header-actions.component.jsx";
|
||||
import JobsDetailHeader from "../../components/jobs-detail-header/jobs-detail-header.component.jsx";
|
||||
@@ -133,9 +132,8 @@ export function SimplifiedPartsJobDetailComponent({ setPrintCenterContext, jobRO
|
||||
<JobLineUpsertModalContainer />
|
||||
|
||||
<PageHeader title={<Space>{job.ro_number || t("general.labels.na")}</Space>} extra={menuExtra} />
|
||||
<JobsDetailHeader job={job} disabled={true} />
|
||||
<JobsDetailHeader job={job} />
|
||||
<Divider type="horizontal" />
|
||||
<JobProfileDataWarning job={job} />
|
||||
<FormFieldsChanged form={form} />
|
||||
<Tabs
|
||||
defaultActiveKey={search.tab}
|
||||
|
||||
@@ -298,6 +298,7 @@ export function* signInSuccessSaga({ payload }) {
|
||||
|
||||
setUserId(analytics, payload.email);
|
||||
setUserProperties(analytics, payload);
|
||||
yield;
|
||||
}
|
||||
|
||||
export function* onSendPasswordResetStart() {
|
||||
|
||||
@@ -144,6 +144,11 @@
|
||||
"tasks_updated": "Task '{{title}}' updated by {{updatedBy}}"
|
||||
}
|
||||
},
|
||||
"audio": {
|
||||
"manager": {
|
||||
"description": "Click anywhere to enable the message ding."
|
||||
}
|
||||
},
|
||||
"billlines": {
|
||||
"actions": {
|
||||
"newline": "New Line"
|
||||
@@ -1249,7 +1254,8 @@
|
||||
"sizelimit": "The selected items exceed the size limit.",
|
||||
"sub_status": {
|
||||
"expired": "The subscription for this shop has expired. Please contact Sales to reactivate.",
|
||||
"trial-expired": "The trial for this shop has expired. Please contact Sales to reactivate."
|
||||
"trial-expired": "The trial for this shop has expired. Please contact Sales to reactivate.",
|
||||
"undefined": "The subscription for this shop is removed. Please contact Sales to reactivate."
|
||||
},
|
||||
"submit-for-testing": "Error submitting Job for testing."
|
||||
},
|
||||
@@ -3805,7 +3811,14 @@
|
||||
"labels": {
|
||||
"actions": "Actions",
|
||||
"changepassword": "Change Password",
|
||||
"profileinfo": "Profile Info"
|
||||
"profileinfo": "Profile Info",
|
||||
"user_settings": "User Settings",
|
||||
"play_sound_for_new_messages": "Play a sound for new messages",
|
||||
"notification_sound_on": "Sound is ON",
|
||||
"notification_sound_off": "Sound is OFF",
|
||||
"notification_sound_enabled": "Notification sound enabled",
|
||||
"notification_sound_disabled": "Notification sound disabled",
|
||||
"notification_sound_help": "Toggle the ding for incoming chat messages."
|
||||
},
|
||||
"successess": {
|
||||
"passwordchanged": "Password changed successfully. "
|
||||
|
||||
@@ -144,6 +144,11 @@
|
||||
"tasks_updated": ""
|
||||
}
|
||||
},
|
||||
"audio": {
|
||||
"manager": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"billlines": {
|
||||
"actions": {
|
||||
"newline": ""
|
||||
@@ -1249,7 +1254,8 @@
|
||||
"sizelimit": "",
|
||||
"sub_status": {
|
||||
"expired": "",
|
||||
"trial-expired": ""
|
||||
"trial-expired": "",
|
||||
"undefined": ""
|
||||
},
|
||||
"submit-for-testing": ""
|
||||
},
|
||||
@@ -3806,7 +3812,14 @@
|
||||
"labels": {
|
||||
"actions": "",
|
||||
"changepassword": "",
|
||||
"profileinfo": ""
|
||||
"profileinfo": "",
|
||||
"user_settings": "",
|
||||
"play_sound_for_new_messages": "",
|
||||
"notification_sound_on": "",
|
||||
"notification_sound_off": "",
|
||||
"notification_sound_enabled": "",
|
||||
"notification_sound_disabled": "",
|
||||
"notification_sound_help": ""
|
||||
},
|
||||
"successess": {
|
||||
"passwordchanged": ""
|
||||
|
||||
@@ -144,6 +144,11 @@
|
||||
"tasks_updated": ""
|
||||
}
|
||||
},
|
||||
"audio": {
|
||||
"manager": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"billlines": {
|
||||
"actions": {
|
||||
"newline": ""
|
||||
@@ -1249,7 +1254,8 @@
|
||||
"sizelimit": "",
|
||||
"sub_status": {
|
||||
"expired": "",
|
||||
"trial-expired": ""
|
||||
"trial-expired": "",
|
||||
"undefined": ""
|
||||
},
|
||||
"submit-for-testing": ""
|
||||
},
|
||||
@@ -3806,7 +3812,14 @@
|
||||
"labels": {
|
||||
"actions": "",
|
||||
"changepassword": "",
|
||||
"profileinfo": ""
|
||||
"profileinfo": "",
|
||||
"user_settings": "",
|
||||
"play_sound_for_new_messages": "",
|
||||
"notification_sound_on": "",
|
||||
"notification_sound_off": "",
|
||||
"notification_sound_enabled": "",
|
||||
"notification_sound_disabled": "",
|
||||
"notification_sound_help": ""
|
||||
},
|
||||
"successess": {
|
||||
"passwordchanged": ""
|
||||
|
||||
@@ -181,7 +181,10 @@ const cache = new InMemoryCache({
|
||||
const client = new ApolloClient({
|
||||
link: ApolloLink.from(middlewares),
|
||||
cache,
|
||||
connectToDevTools: import.meta.env.DEV,
|
||||
devtools: {
|
||||
name: "Imex Client",
|
||||
enabled: import.meta.env.DEV
|
||||
},
|
||||
defaultOptions: {
|
||||
watchQuery: {
|
||||
fetchPolicy: "network-only",
|
||||
|
||||
164
client/src/utils/singleTabAudioLeader.js
Normal file
164
client/src/utils/singleTabAudioLeader.js
Normal file
@@ -0,0 +1,164 @@
|
||||
// src/utils/singleTabAudioLeader.js
|
||||
// Ensures only one tab ("leader") plays sounds per bodyshop.
|
||||
//
|
||||
// Storage key: localStorage["imex:sound:leader:<bodyshopId>"] = { id, ts }
|
||||
// Channel: new BroadcastChannel("imex:sound:<bodyshopId>")
|
||||
|
||||
const STORAGE_PREFIX = "imex:sound:leader:";
|
||||
const CHANNEL_PREFIX = "imex:sound:";
|
||||
|
||||
const TTL_MS = 60_000; // leader expires after 60s without heartbeat
|
||||
const HEARTBEAT_MS = 20_000; // leader refresh interval
|
||||
const WATCHDOG_MS = 10_000; // how often non-leaders check for stale leader
|
||||
|
||||
const TAB_ID =
|
||||
typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2);
|
||||
|
||||
function channelSupported() {
|
||||
try {
|
||||
return "BroadcastChannel" in window;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function getChannel(bodyshopId) {
|
||||
if (!channelSupported() || !bodyshopId) return null;
|
||||
try {
|
||||
return new BroadcastChannel(CHANNEL_PREFIX + String(bodyshopId));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function lsKey(bodyshopId) {
|
||||
return STORAGE_PREFIX + String(bodyshopId);
|
||||
}
|
||||
|
||||
function readLeader(bodyshopId) {
|
||||
if (!bodyshopId) return null;
|
||||
try {
|
||||
const raw = localStorage.getItem(lsKey(bodyshopId));
|
||||
if (!raw) return null;
|
||||
return JSON.parse(raw);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function writeLeader(record, bodyshopId) {
|
||||
if (!bodyshopId) return;
|
||||
try {
|
||||
localStorage.setItem(lsKey(bodyshopId), JSON.stringify(record));
|
||||
const bc = getChannel(bodyshopId);
|
||||
if (bc) {
|
||||
bc.postMessage({ type: "leader-update", payload: { ...record, bodyshopId } });
|
||||
bc.close();
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
function removeLeader(bodyshopId) {
|
||||
if (!bodyshopId) return;
|
||||
try {
|
||||
const cur = readLeader(bodyshopId);
|
||||
if (cur?.id === TAB_ID) {
|
||||
localStorage.removeItem(lsKey(bodyshopId));
|
||||
const bc = getChannel(bodyshopId);
|
||||
if (bc) {
|
||||
bc.postMessage({ type: "leader-removed", payload: { id: TAB_ID, bodyshopId } });
|
||||
bc.close();
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
function now() {
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
function isStale(rec) {
|
||||
return !rec || now() - rec.ts > TTL_MS;
|
||||
}
|
||||
|
||||
function claimLeadership(bodyshopId) {
|
||||
const rec = { id: TAB_ID, ts: now() };
|
||||
writeLeader(rec, bodyshopId);
|
||||
return rec;
|
||||
}
|
||||
|
||||
/** Is THIS tab currently the leader (and not stale)? */
|
||||
export function isLeaderTab(bodyshopId) {
|
||||
const rec = readLeader(bodyshopId);
|
||||
return !!rec && rec.id === TAB_ID && !isStale(rec);
|
||||
}
|
||||
|
||||
/** Force this tab to become the leader right now. */
|
||||
export function claimLeadershipNow(bodyshopId) {
|
||||
return claimLeadership(bodyshopId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize leader election/heartbeat for this tab (scoped by bodyshopId).
|
||||
* Call once (e.g., in SoundWrapper). Returns a cleanup function.
|
||||
*/
|
||||
export function initSingleTabAudioLeader(bodyshopId) {
|
||||
if (!bodyshopId)
|
||||
return () => {
|
||||
//
|
||||
};
|
||||
|
||||
// If no leader or stale, try to claim after a tiny delay (reduce startup contention)
|
||||
if (isStale(readLeader(bodyshopId))) {
|
||||
setTimeout(() => claimLeadership(bodyshopId), 100);
|
||||
}
|
||||
|
||||
// If this tab becomes focused/visible, it can claim leadership
|
||||
const onFocus = () => claimLeadership(bodyshopId);
|
||||
const onVis = () => {
|
||||
if (document.visibilityState === "visible") claimLeadership(bodyshopId);
|
||||
};
|
||||
window.addEventListener("focus", onFocus);
|
||||
document.addEventListener("visibilitychange", onVis);
|
||||
|
||||
// Heartbeat from the leader to keep record fresh
|
||||
const heartbeat = setInterval(() => {
|
||||
if (!isLeaderTab(bodyshopId)) return;
|
||||
writeLeader({ id: TAB_ID, ts: now() }, bodyshopId);
|
||||
}, HEARTBEAT_MS);
|
||||
|
||||
// Watchdog: if leader is stale, try to claim (even if we're not focused)
|
||||
const watchdog = setInterval(() => {
|
||||
const cur = readLeader(bodyshopId);
|
||||
if (isStale(cur)) claimLeadership(bodyshopId);
|
||||
}, WATCHDOG_MS);
|
||||
|
||||
// If this tab was the leader, clean up on unload
|
||||
const onUnload = () => removeLeader(bodyshopId);
|
||||
window.addEventListener("beforeunload", onUnload);
|
||||
|
||||
// Per-bodyshop BroadcastChannel listener (optional/no-op)
|
||||
const bc = getChannel(bodyshopId);
|
||||
const onBC = bc
|
||||
? () => {
|
||||
// No state kept here; localStorage read is the source of truth.
|
||||
}
|
||||
: null;
|
||||
if (bc && onBC) bc.addEventListener("message", onBC);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("focus", onFocus);
|
||||
document.removeEventListener("visibilitychange", onVis);
|
||||
window.removeEventListener("beforeunload", onUnload);
|
||||
clearInterval(heartbeat);
|
||||
clearInterval(watchdog);
|
||||
if (bc && onBC) {
|
||||
bc.removeEventListener("message", onBC);
|
||||
bc.close();
|
||||
}
|
||||
};
|
||||
}
|
||||
97
client/src/utils/soundManager.js
Normal file
97
client/src/utils/soundManager.js
Normal file
@@ -0,0 +1,97 @@
|
||||
// src/utils/soundManager.js
|
||||
// Handles audio init, autoplay unlock, and queued plays.
|
||||
// When a tab successfully unlocks audio, it CLAIMS LEADERSHIP immediately for that bodyshop.
|
||||
|
||||
import { claimLeadershipNow } from "./singleTabAudioLeader";
|
||||
|
||||
let baseAudio = null;
|
||||
let unlocked = false;
|
||||
let queuedPlays = 0;
|
||||
let installingUnlockHandlers = false;
|
||||
|
||||
/**
|
||||
* Initialize the new-message sound.
|
||||
* @param {string} url
|
||||
* @param {number} volume
|
||||
*/
|
||||
export function initNewMessageSound(url, volume = 0.7) {
|
||||
baseAudio = new Audio(url);
|
||||
baseAudio.preload = "auto";
|
||||
baseAudio.volume = volume;
|
||||
}
|
||||
|
||||
/** Has this tab unlocked audio? (optional helper) */
|
||||
export function isAudioUnlocked() {
|
||||
return unlocked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlocks audio if not already unlocked.
|
||||
* On success, this tab immediately becomes the sound LEADER for the given bodyshop.
|
||||
*/
|
||||
export async function unlockAudio(bodyshopId) {
|
||||
if (unlocked) return;
|
||||
try {
|
||||
// Chrome/Safari: playing any media (even muted) after a gesture unlocks audio.
|
||||
const a = new Audio();
|
||||
a.muted = true;
|
||||
await a.play().catch(() => {
|
||||
// ignore
|
||||
});
|
||||
unlocked = true;
|
||||
|
||||
// Immediately become the leader because THIS tab can actually play sound.
|
||||
claimLeadershipNow(bodyshopId);
|
||||
|
||||
// Flush exactly one queued ding (avoid spamming if many queued while locked)
|
||||
if (queuedPlays > 0 && baseAudio) {
|
||||
queuedPlays = 0;
|
||||
const b = baseAudio.cloneNode(true);
|
||||
b.play().catch(() => {
|
||||
// ignore
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
removeUnlockListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/** Installs listeners to unlock audio on first gesture. */
|
||||
function addUnlockListeners(bodyshopId) {
|
||||
if (installingUnlockHandlers) return;
|
||||
installingUnlockHandlers = true;
|
||||
const handler = () => unlockAudio(bodyshopId);
|
||||
window.addEventListener("click", handler, { once: true, passive: true });
|
||||
window.addEventListener("touchstart", handler, { once: true, passive: true });
|
||||
window.addEventListener("keydown", handler, { once: true });
|
||||
}
|
||||
|
||||
/** Removes listeners to unlock audio on first gesture. */
|
||||
function removeUnlockListeners() {
|
||||
// With {once:true} they self-remove; we only reset the flag.
|
||||
installingUnlockHandlers = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Plays the new-message ding. If blocked, queue one and wait for first gesture.
|
||||
*/
|
||||
export async function playNewMessageSound(bodyshopId) {
|
||||
if (!baseAudio) return;
|
||||
try {
|
||||
const a = baseAudio.cloneNode(true);
|
||||
await a.play();
|
||||
} catch (err) {
|
||||
// Most common: NotAllowedError due to missing prior gesture
|
||||
if (err?.name === "NotAllowedError") {
|
||||
queuedPlays = Math.min(queuedPlays + 1, 1); // cap at 1
|
||||
addUnlockListeners(bodyshopId);
|
||||
|
||||
// Let the app know we need user interaction (optional UI prompt)
|
||||
window.dispatchEvent(new CustomEvent("sound-needs-unlock"));
|
||||
return;
|
||||
}
|
||||
// Other errors can be logged
|
||||
|
||||
console.error("Audio play error:", err);
|
||||
}
|
||||
}
|
||||
@@ -220,7 +220,7 @@ export default defineConfig({
|
||||
|
||||
// Strip console/debugger in prod to shrink bundles
|
||||
esbuild: {
|
||||
drop: ["console", "debugger"]
|
||||
//drop: ["console", "debugger"]
|
||||
},
|
||||
|
||||
optimizeDeps: {
|
||||
|
||||
@@ -21,7 +21,7 @@ services:
|
||||
- redis-node-1-data:/data
|
||||
- redis-lock:/redis-lock
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
test: [ "CMD", "redis-cli", "ping" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
@@ -39,7 +39,7 @@ services:
|
||||
- redis-node-2-data:/data
|
||||
- redis-lock:/redis-lock
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
test: [ "CMD", "redis-cli", "ping" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
@@ -57,7 +57,7 @@ services:
|
||||
- redis-node-3-data:/data
|
||||
- redis-lock:/redis-lock
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
test: [ "CMD", "redis-cli", "ping" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
@@ -85,7 +85,7 @@ services:
|
||||
ports:
|
||||
- "4566:4566"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:4566/_localstack/health"]
|
||||
test: [ "CMD", "curl", "-f", "http://localhost:4566/_localstack/health" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
@@ -118,6 +118,8 @@ services:
|
||||
aws --endpoint-url=http://localstack:4566 logs create-log-group --log-group-name development --region ca-central-1
|
||||
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket imex-large-log --create-bucket-configuration LocationConstraint=ca-central-1
|
||||
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket imex-job-totals --create-bucket-configuration LocationConstraint=ca-central-1
|
||||
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket parts-estimates --create-bucket-configuration LocationConstraint=ca-central-1
|
||||
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket imex-carfax-uploads --create-bucket-configuration LocationConstraint=ca-central-1
|
||||
"
|
||||
# Node App: The Main IMEX API
|
||||
node-app:
|
||||
|
||||
@@ -6,6 +6,15 @@
|
||||
headers:
|
||||
- name: x-imex-auth
|
||||
value_from_env: DATAPUMP_AUTH
|
||||
- name: CARFAX Data Pump
|
||||
webhook: '{{HASURA_API_URL}}/data/carfax'
|
||||
schedule: 0 7 * * 6
|
||||
include_in_metadata: true
|
||||
payload: {}
|
||||
headers:
|
||||
- name: x-imex-auth
|
||||
value_from_env: DATAPUMP_AUTH
|
||||
comment: Project Mexico
|
||||
- name: Chatter Data Pump
|
||||
webhook: '{{HASURA_API_URL}}/data/chatter'
|
||||
schedule: 45 5 * * *
|
||||
|
||||
@@ -215,6 +215,7 @@
|
||||
- default_prod_list_view
|
||||
- id
|
||||
- kanban_settings
|
||||
- new_message_sound
|
||||
- notification_settings
|
||||
- notifications_autoadd
|
||||
- qbo_realmId
|
||||
@@ -232,6 +233,7 @@
|
||||
- authlevel
|
||||
- default_prod_list_view
|
||||
- kanban_settings
|
||||
- new_message_sound
|
||||
- notification_settings
|
||||
- notifications_autoadd
|
||||
- qbo_realmId
|
||||
@@ -959,6 +961,7 @@
|
||||
- enforce_referral
|
||||
- entegral_configuration
|
||||
- entegral_id
|
||||
- external_shop_id
|
||||
- features
|
||||
- federal_tax_id
|
||||
- id
|
||||
@@ -1012,6 +1015,8 @@
|
||||
- prodtargethrs
|
||||
- production_config
|
||||
- region_config
|
||||
- rr_configuration
|
||||
- rr_dealerid
|
||||
- schedule_end_time
|
||||
- schedule_start_time
|
||||
- scoreboard_target
|
||||
@@ -1035,7 +1040,6 @@
|
||||
- use_fippa
|
||||
- use_paint_scale_data
|
||||
- uselocalmediaserver
|
||||
- external_shop_id
|
||||
- website
|
||||
- workingdays
|
||||
- zip_post
|
||||
@@ -1068,6 +1072,7 @@
|
||||
- enforce_conversion_category
|
||||
- enforce_conversion_csr
|
||||
- enforce_referral
|
||||
- external_shop_id
|
||||
- federal_tax_id
|
||||
- id
|
||||
- inhousevendorid
|
||||
@@ -1113,6 +1118,7 @@
|
||||
- phone
|
||||
- prodtargethrs
|
||||
- production_config
|
||||
- rr_configuration
|
||||
- schedule_end_time
|
||||
- schedule_start_time
|
||||
- scoreboard_target
|
||||
@@ -1131,7 +1137,6 @@
|
||||
- use_fippa
|
||||
- use_paint_scale_data
|
||||
- uselocalmediaserver
|
||||
- external_shop_id
|
||||
- website
|
||||
- workingdays
|
||||
- zip_post
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Could not auto-generate a down migration.
|
||||
-- Please write an appropriate down migration for the SQL below:
|
||||
-- alter table "public"."bodyshops" add column "rr_configuration" jsonb
|
||||
-- null default jsonb_build_object();
|
||||
@@ -0,0 +1,2 @@
|
||||
alter table "public"."bodyshops" add column "rr_configuration" jsonb
|
||||
null default jsonb_build_object();
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Could not auto-generate a down migration.
|
||||
-- Please write an appropriate down migration for the SQL below:
|
||||
-- alter table "public"."bodyshops" add column "rr_dealierid" text
|
||||
-- null;
|
||||
@@ -0,0 +1,2 @@
|
||||
alter table "public"."bodyshops" add column "rr_dealierid" text
|
||||
null;
|
||||
@@ -0,0 +1 @@
|
||||
alter table "public"."bodyshops" rename column "rr_dealerid" to "rr_dealierid";
|
||||
@@ -0,0 +1 @@
|
||||
alter table "public"."bodyshops" rename column "rr_dealierid" to "rr_dealerid";
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Could not auto-generate a down migration.
|
||||
-- Please write an appropriate down migration for the SQL below:
|
||||
-- alter table "public"."associations" add column "new_message_sound" boolean
|
||||
-- null;
|
||||
@@ -0,0 +1,2 @@
|
||||
alter table "public"."associations" add column "new_message_sound" boolean
|
||||
null;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE "public"."associations" ALTER COLUMN "new_message_sound" drop default;
|
||||
@@ -0,0 +1 @@
|
||||
alter table "public"."associations" alter column "new_message_sound" set default 'true';
|
||||
3235
package-lock.json
generated
3235
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user