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