Compare commits

..

23 Commits

Author SHA1 Message Date
Dave Richer
b13c42b02f IO-2742-redis - Merge Release / Up deps (prior to parking)
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-09-24 14:44:22 -04:00
Dave Richer
93f6a80fda Merge remote-tracking branch 'origin/release/2024-09-27' into feature/IO-2742-redis 2024-09-24 14:21:51 -04:00
Dave Richer
5e14258839 IO-2742-redis - PR Changes
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-09-24 14:20:09 -04:00
Dave Richer
5c54cf6c44 feature/IO-2924-Refactor-Production-Board-For-Sockets - Checkpoint
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-09-16 11:48:17 -04:00
Dave Richer
a2d95dbce3 feature/IO-2742-redis - Checkpoint - clear stage before moving to sub task
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-09-12 11:53:29 -04:00
Dave Richer
51c181dab7 feature/IO-2742-redis - Checkpoint, Final cleanup of server.js
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-09-11 20:52:49 -04:00
Dave Richer
4486858a86 feature/IO-2742-redis - Checkpoint, All optimizations to prevent multiple requests to redis have been applied. Things like using lists for the Log Events, to getting and setting multiple values at the same time when given the chance.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-09-11 20:27:40 -04:00
Dave Richer
f606228792 feature/IO-2742-redis - Checkpoint, Redis fully implemented.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-09-11 19:08:24 -04:00
Dave Richer
bca0a35cdd Remove express line
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-09-11 11:12:14 -04:00
Dave Richer
0ee51ed4c1 - Update
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-09-11 11:06:36 -04:00
Dave Richer
11b903f24b - Fix DMS Socket (missing reference)
- Add preview mode

Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-09-11 10:30:55 -04:00
Dave Richer
f83deb0c26 - Checkpoint
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-09-10 17:15:08 -04:00
Dave Richer
9a0d973b26 Normalize SocketIO App wide.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-09-10 16:23:38 -04:00
Dave Richer
ea0a717895 feature/IO-2742-redis - Normalize Network
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-09-10 15:17:13 -04:00
Dave Richer
40f1a2f6ed feature/IO-2742-redis - Dep Bump
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-09-10 14:34:50 -04:00
Dave Richer
3433b1a600 Merge remote-tracking branch 'origin/master-AIO' into feature/IO-2742-redis 2024-09-10 14:28:44 -04:00
Dave Richer
49a7313abb - Checkpoint
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-09-09 23:27:28 -04:00
Dave Richer
319b24ce8a Merge remote-tracking branch 'origin/master-AIO' into feature/IO-2742-redis 2024-09-09 11:39:48 -04:00
Dave Richer
08d8a4f7dc - Checkpoint
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-09-09 11:39:39 -04:00
Dave Richer
03e8b62e4f - Checkpoint
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-09-06 13:22:17 -04:00
Dave Richer
44db8f20e9 - Checkpoint
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-09-06 11:53:08 -04:00
Dave Richer
f28068d0e7 Merge remote-tracking branch 'origin/release/2024-09-06' into feature/IO-2742-redis 2024-09-05 19:38:00 -04:00
Patrick Fic
5f082b9619 Basic connection to elasticache server. 2024-08-22 14:48:00 -07:00
131 changed files with 6871 additions and 10486 deletions

View File

@@ -1,24 +0,0 @@
# Directories to exclude
.circleci
.idea
.platform
.vscode
_reference
client
redis/dockerdata
hasura
node_modules
# Files to exclude
.ebignore
.editorconfig
.eslintrc.json
.gitignore
.prettierrc.js
Dockerfile
README.MD
bodyshop_translations.babel
docker-compose.yml
ecosystem.config.js
# Optional: Exclude logs and temporary files
*.log

View File

View File

@@ -1,2 +1 @@
client_max_body_size 50M;
client_body_buffer_size 5M;
client_max_body_size 50M;

15
.vscode/launch.json vendored
View File

@@ -14,21 +14,6 @@
"request": "launch",
"url": "http://localhost:3000",
"webRoot": "${workspaceRoot}/client/src"
},
{
"name": "Attach to Node.js in Docker",
"type": "node",
"request": "attach",
"address": "localhost",
"port": 9229,
"localRoot": "${workspaceFolder}",
"remoteRoot": "/app",
"protocol": "inspector",
"restart": true,
"sourceMaps": true,
"skipFiles": [
"<node_internals>/**"
]
}
]
}

View File

@@ -1,47 +0,0 @@
# Use Amazon Linux 2023 as the base image
FROM amazonlinux:2023
# Install Git and Node.js (Amazon Linux 2023 uses the DNF package manager)
RUN dnf install -y git \
&& curl -sL https://rpm.nodesource.com/setup_20.x | bash - \
&& dnf install -y nodejs \
&& dnf clean all
# Install dependencies required by node-canvas
RUN dnf install -y \
gcc \
gcc-c++ \
cairo-devel \
pango-devel \
libjpeg-turbo-devel \
giflib-devel \
libpng-devel \
make \
python3 \
python3-pip \
&& dnf clean all
# Set the working directory
WORKDIR /app
# This is because our test route uses a git commit hash
RUN git config --global --add safe.directory /app
# Copy package.json and package-lock.json
COPY package.json ./
# Install Nodemon
RUN npm install -g nodemon
# Install dependencies
RUN npm i --no-package-lock
# Copy the rest of your application code
COPY . .
# Expose the port your app runs on (adjust if necessary)
EXPOSE 4000 9229
# Start the application
CMD ["nodemon", "--legacy-watch", "--inspect=0.0.0.0:9229", "server.js"]

View File

@@ -1,64 +0,0 @@
# Setting up External Networking and Static IP for WSL2 using Hyper-V
This guide will walk you through the steps to configure your WSL2 (Windows Subsystem for Linux) instance to use an external Hyper-V virtual switch, enabling it to connect directly to your local network. Additionally, you'll learn how to assign a static IP address to your WSL2 instance.
## Prerequisites
1. **Windows 11**
2. **Docker Desktop For Windows (Latest Version)
# Docker Setup
Inside the root of the project exists the `docker-compose.yaml` file, you can simply run
`docker-compose up` to launch the backend.
Things to note:
- When installing NPM packages, you will need to rebuild the `node-app` container
- Making changes to the server files will restart the `node-app`
# Local Stack
- LocalStack Front end (Optional) - https://apps.microsoft.com/detail/9ntrnft9zws2?hl=en-us&gl=US
- http://localhost:4566/_aws/ses will allow you to see emails sent
# Docker Commands
## General `docker-compose` Commands:
1. Bring up the services, force a rebuild of all services, and do not use the cache: `docker-compose up --build --no-cache`
2. Start Containers in Detached Mode: This will run the containers in the background (detached mode): `docker-compose up -d`
3. Stop and Remove Containers: Stops and removes the containers gracefully: `docker-compose down`
4. Stop containers without removing them: `docker-compose stop`
5. Remove Containers, Volumes, and Networks: `docker-compose down --volumes`
6. Force rebuild of containers: `docker-compose build --no-cache`
7. View running Containers: `docker-compose ps`
8. View a specific containers logs: `docker-compose logs <container-name>`
9. Scale services (multiple instances of a service): `docker-compose up --scale <container-name>=<instances number> -d`
10. Watch a specific containers logs in realtime with timestamps: `docker-compose logs -f --timestamps <container-name>`
## Volume Management Commands
1. List Docker volumes: `docker volume ls`
2. Remove Unused volumes `docker volume prune`
3. Remove specific volumes `docker volume rm <volume-name>`
4. Inspect a volume: `docker volume inspect <volume-name>`
## Container Image Management Commands:
1. List running containers: `docker ps`
2. List all containers: `docker os -a`
3. Remove Stopped containers: `docker container prune`
4. Remove a specific container: `docker container rm <container-name>`
5. Remove a specific image: `docker rmi <image-name>:<version>`
6. Remove all unused images: `docker image prune -a`
## Network Management Commands:
1. List networks: `docker network ls`
2. Inspect a specific network: `docker network inspect <network-name>`
3. Remove a specific network: `docker network rm <network-name>`
4. Remove unused networks: `docker network prune`
## Debugging and maintenance:
1. Enter a Running container: `docker exec -it <container name> /bin/bash` (could also be `/bin/sh` or for example `redis-cli` on a redis node)
2. View container resource usage: `docker stats`
3. Check Disk space used by Docker: `docker system df`
4. Remove all unused Data (Nuclear option): `docker system prune`
## Specific examples
1. To simulate a Clean state, one should run `docker system prune` followed by `docker volume prune -a`
2. You can run `docker-compose up` without the `-d` option, and you will get what is identical to the experience you were used to, this includes being able to control-c and bring the entire stack down

View File

@@ -1 +0,0 @@
node_modules

View File

@@ -1,7 +0,0 @@
This will connect to your dockers local stack session and render the email in HTML.
```shell
node index.js
```
http://localhost:3334

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +0,0 @@
{
"name": "localemailviewer",
"version": "1.0.0",
"main": "index.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"express": "^4.21.1",
"mailparser": "^3.7.1",
"node-fetch": "^3.3.2"
}
}

View File

@@ -1,27 +0,0 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn
NhAAAAAwEAAQAAAQEAvNl5fuVmLNv72BZNxnTqX5CHf5Xi8UxjYaYxHITSCx7blnhpVYLd
qXvcOWXzbsfjch/den73QiW4n2FYz75oGMhUGlOYzdWKA9I9Sj09Qy1R06RhwDiZGd5qaM
swEeXpkNmi2u4Qd2kJeDfUQUigjC09V81O/vrniGtQAJScfiG/itdm+Ufn09Z4MYk0HWjq
iDokNEskoEPsibYIrb+Q6vdtuPkZO+wU/smXhPtgw5ST6oQdmm/gVNsRg5XNzxrire+z1G
WatnnVL3hPnnfpnf8W589dyms7GGJwhPerSGTN1bn0T4+9C69Cd7LBJtxiuFdRmdlGLLLP
RR48Rur71wAAA9AEfVsdBH1bHQAAAAdzc2gtcnNhAAABAQC82Xl+5WYs2/vYFk3GdOpfkI
d/leLxTGNhpjEchNILHtuWeGlVgt2pe9w5ZfNux+NyH916fvdCJbifYVjPvmgYyFQaU5jN
1YoD0j1KPT1DLVHTpGHAOJkZ3mpoyzAR5emQ2aLa7hB3aQl4N9RBSKCMLT1XzU7++ueIa1
AAlJx+Ib+K12b5R+fT1ngxiTQdaOqIOiQ0SySgQ+yJtgitv5Dq9224+Rk77BT+yZeE+2DD
lJPqhB2ab+BU2xGDlc3PGuKt77PUZZq2edUveE+ed+md/xbnz13KazsYYnCE96tIZM3Vuf
RPj70Lr0J3ssEm3GK4V1GZ2UYsss9FHjxG6vvXAAAAAwEAAQAAAQAQTosSLQbMmtY9S3e9
yjyusdExcCTfhyQRu4MEHmfws+JsNMuLqbgwOVTD1AzYJQR7x0qdmDcLjCxL/uDnV16vvS
Sd/Vf1dhnryIyoS29tzI0DRG94ZKq7tBvmHp1w/jRT4KcSVnovhW9e5Rs74+SRFhr06PKI
S+wQOIv48Nwue9+QUMsMCpWgKXHx7SHNTHvnAfqdhi9O29SWlMA+v+mELZ5Cl+HU0UTt2I
A1BxOe1N8FjN7KE2viJexsl3is1PuqMkpLl/wyHBJTVzUadl6DRALJQIm7/YO5goE72YOV
Lpo27do3zjhC87dlKdATvZUzfKV0LuUVdxq/PNDZMUbBAAAAgQDShAqDZiDrdTUaGXfUVm
QzcnVNbh2/KgZh4uux9QNHST562W6cnN7qxoRwVrM4BCOk1Kl73QQZW4nDvXX3PVC5j038
8AXkcBHS9j9f4h72ue7D2jqlbHFa7aGU9zYgk9mbBF+GX3tDntkAIQjLtwOLfj1iiJ/clX
mHFUAY1V4L8AAAAIEA3E4t/v0yU5D9AOI0r17UNYqfeyDoKAEDR4QbbFjO1l0kLnEJy7Zx
Mhj18GilYg2y0P0v8dSM/oWXS8Hua2t5i9Exlv6gHhGlQ80mwYcVGIxewZ/pPeCPw0U+kt
EKUjt09m9Oe7+6xHQsTBj9hY8/vqPmQwRalZFcLdhHiDiVKTcAAACBANtykaPXdVzEFx7D
UOlsjVL7zM0EVOFXf9JJQ6BhazhmsEI2PYt3IpgGMo8cXkoUofAOIYjf421AabN1BqSO5J
XTMxM0ZV3JmLLi804Mu9h1iFrVTBdLYOMJdc2VCo1EwHWpo9SXOyjxce/znvcIOU04aZhu
TaPg816X+E+gw5JhAAAAFGRhdmVARGF2ZVJpY2hlci1JTUVYAQIDBAUG
-----END OPENSSH PRIVATE KEY-----

View File

@@ -1 +0,0 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC82Xl+5WYs2/vYFk3GdOpfkId/leLxTGNhpjEchNILHtuWeGlVgt2pe9w5ZfNux+NyH916fvdCJbifYVjPvmgYyFQaU5jN1YoD0j1KPT1DLVHTpGHAOJkZ3mpoyzAR5emQ2aLa7hB3aQl4N9RBSKCMLT1XzU7++ueIa1AAlJx+Ib+K12b5R+fT1ngxiTQdaOqIOiQ0SySgQ+yJtgitv5Dq9224+Rk77BT+yZeE+2DDlJPqhB2ab+BU2xGDlc3PGuKt77PUZZq2edUveE+ed+md/xbnz13KazsYYnCE96tIZM3VufRPj70Lr0J3ssEm3GK4V1GZ2UYsss9FHjxG6vvX dave@DaveRicher-IMEX

1634
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@
"private": true,
"proxy": "http://localhost:4000",
"dependencies": {
"@ant-design/pro-layout": "^7.19.12",
"@ant-design/pro-layout": "^7.20.2",
"@apollo/client": "^3.11.8",
"@emotion/is-prop-valid": "^1.3.1",
"@fingerprintjs/fingerprintjs": "^4.5.0",
@@ -19,7 +19,7 @@
"@splitsoftware/splitio-react": "^1.13.0",
"@tanem/react-nprogress": "^5.0.51",
"@vitejs/plugin-react": "^4.3.1",
"antd": "^5.20.1",
"antd": "^5.21.0",
"apollo-link-logger": "^2.0.1",
"apollo-link-sentry": "^3.3.0",
"autosize": "^6.0.1",

View File

@@ -2,6 +2,8 @@ import { ApolloProvider } from "@apollo/client";
import { SplitFactoryProvider, SplitSdk } from "@splitsoftware/splitio-react";
import { ConfigProvider } from "antd";
import enLocale from "antd/es/locale/en_US";
import dayjs from "../utils/day";
import "dayjs/locale/en";
import React from "react";
import { useTranslation } from "react-i18next";
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
@@ -17,6 +19,8 @@ if (import.meta.env.DEV) {
Userpilot.initialize("NX-69145f08");
}
dayjs.locale("en");
const config = {
core: {
authorizationKey: import.meta.env.VITE_APP_SPLIT_API,

View File

@@ -9,7 +9,7 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter } from "../../utils/DateFormatter";
import { exportPageLimit } from "../../utils/config";
import { pageLimit } from "../../utils/config";
import { alphaSort, dateSort } from "../../utils/sorters";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
import PayableExportAll from "../payable-export-all-button/payable-export-all-button.component";
@@ -175,7 +175,7 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, bills, ref
<Table
loading={loading}
dataSource={dataSource}
pagination={{ position: "top", pageSize: exportPageLimit }}
pagination={{ position: "top", pageSize: pageLimit }}
columns={columns}
rowKey="id"
onChange={handleTableChange}

View File

@@ -8,7 +8,7 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter";
import { exportPageLimit } from "../../utils/config";
import { pageLimit } from "../../utils/config";
import { alphaSort, dateSort } from "../../utils/sorters";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
@@ -177,7 +177,7 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, payments,
<Table
loading={loading}
dataSource={dataSource}
pagination={{ position: "top", pageSize: exportPageLimit }}
pagination={{ position: "top", pageSize: pageLimit }}
columns={columns}
rowKey="id"
onChange={handleTableChange}

View File

@@ -1,18 +1,18 @@
import { Button, Card, Input, Space, Table } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { exportPageLimit } from "../../utils/config";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter } from "../../utils/DateFormatter";
import { alphaSort, dateSort, statusSort } from "../../utils/sorters";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
import JobExportButton from "../jobs-close-export-button/jobs-close-export-button.component";
import JobsExportAllButton from "../jobs-export-all-button/jobs-export-all-button.component";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { DateFormatter } from "../../utils/DateFormatter";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
import JobMarkSelectedExported from "../jobs-mark-selected-exported/jobs-mark-selected-exported";
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
@@ -201,7 +201,7 @@ export function AccountingReceivablesTableComponent({ bodyshop, loading, jobs, r
<Table
loading={loading}
dataSource={dataSource}
pagination={{ position: "top", pageSize: exportPageLimit }}
pagination={{ position: "top" }}
columns={columns}
rowKey="id"
onChange={handleTableChange}

View File

@@ -7,10 +7,10 @@ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CiecaSelect from "../../utils/Ciecaselect";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import BillLineSearchSelect from "../bill-line-search-select/bill-line-search-select.component";
import BilllineAddInventory from "../billline-add-inventory/billline-add-inventory.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
@@ -72,14 +72,7 @@ export function BillEnterModalLinesComponent({
<BillLineSearchSelect
disabled={disabled}
options={lineData}
style={{
width: "20rem",
maxWidth: "20rem",
minWidth: "10rem",
whiteSpace: "normal",
height: "auto",
minHeight: "32px" // default height of Ant Design inputs
}}
style={{ width: "100%", minWidth: "10rem" }}
allowRemoved={form.getFieldValue("is_credit_memo") || false}
onSelect={(value, opt) => {
setFieldsValue({
@@ -112,7 +105,7 @@ export function BillEnterModalLinesComponent({
title: t("billlines.fields.line_desc"),
dataIndex: "line_desc",
editable: true,
width: "20rem",
formItemProps: (field) => {
return {
key: `${field.index}line_desc`,
@@ -126,7 +119,7 @@ export function BillEnterModalLinesComponent({
]
};
},
formInput: (record, index) => <Input.TextArea disabled={disabled} autoSize />
formInput: (record, index) => <Input disabled={disabled} />
},
{
title: t("billlines.fields.quantity"),

View File

@@ -11,7 +11,7 @@ const BillLineSearchSelect = ({ options, disabled, allowRemoved, ...restProps },
disabled={disabled}
ref={ref}
showSearch
popupMatchSelectWidth={true}
popupMatchSelectWidth={false}
optionLabelProp={"name"}
// optionFilterProp="line_desc"
filterOption={(inputValue, option) => {
@@ -43,7 +43,7 @@ const BillLineSearchSelect = ({ options, disabled, allowRemoved, ...restProps },
item.oem_partno ? ` - ${item.oem_partno}` : ""
}${item.alt_partno ? ` (${item.alt_partno})` : ""}`.trim(),
label: (
<div style={{ whiteSpace: 'normal', wordBreak: 'break-word' }}>
<>
<span>
{`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
item.oem_partno ? ` - ${item.oem_partno}` : ""
@@ -57,7 +57,7 @@ const BillLineSearchSelect = ({ options, disabled, allowRemoved, ...restProps },
<span style={{ float: "right", paddingleft: "1rem" }}>
{item.act_price ? `$${item.act_price && item.act_price.toFixed(2)}` : ``}
</span>
</div>
</>
)
}))
]}

View File

@@ -1,17 +1,15 @@
import { WarningFilled } from "@ant-design/icons";
import { Form, Input, InputNumber, Space } from "antd";
import dayjs from "../../utils/day";
import React from "react";
import { useTranslation } from "react-i18next";
import { DateFormatter } from "../../utils/DateFormatter";
import dayjs from "../../utils/day";
//import ContractLicenseDecodeButton from "../contract-license-decode-button/contract-license-decode-button.component";
import ContractStatusSelector from "../contract-status-select/contract-status-select.component";
import ContractsRatesChangeButton from "../contracts-rates-change-button/contracts-rates-change-button.component";
import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel-select.component";
import {
default as DateTimePicker,
default as FormDateTimePicker
} from "../form-date-time-picker/form-date-time-picker.component";
import FormDateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import InputPhone, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
@@ -20,10 +18,10 @@ import ContractFormJobPrefill from "./contract-form-job-prefill.component";
export default function ContractFormComponent({ form, create = false, selectedJobState, selectedCar }) {
const { t } = useTranslation();
return (
<>
{!create && <FormFieldsChanged form={form} />}
<div>
<FormFieldsChanged form={form} />
<LayoutFormRow>
{!create && (
{create ? null : (
<Form.Item
label={t("contracts.fields.status")}
name="status"
@@ -52,7 +50,7 @@ export default function ContractFormComponent({ form, create = false, selectedJo
<Form.Item label={t("contracts.fields.scheduledreturn")} name="scheduledreturn">
<FormDateTimePicker />
</Form.Item>
{!create && (
{create ? null : (
<Form.Item label={t("contracts.fields.actualreturn")} name="actualreturn">
<FormDateTimePicker />
</Form.Item>
@@ -124,7 +122,7 @@ export default function ContractFormComponent({ form, create = false, selectedJo
}}
</Form.Item>
)}
{!create && (
{create ? null : (
<Form.Item label={t("contracts.fields.kmend")} name="kmend">
<InputNumber />
</Form.Item>
@@ -147,21 +145,25 @@ export default function ContractFormComponent({ form, create = false, selectedJo
>
<CourtesyCarFuelSlider />
</Form.Item>
{!create && (
{create ? null : (
<Form.Item label={t("contracts.fields.fuelin")} name="fuelin" span={8}>
<CourtesyCarFuelSlider />
</Form.Item>
)}
</LayoutFormRow>
<LayoutFormRow header={t("contracts.labels.driverinformation")}>
<div>
<Space wrap>
{create && selectedJobState && (
<ContractFormJobPrefill jobId={selectedJobState && selectedJobState[0]} form={form} />
{selectedJobState && (
<div>
<ContractFormJobPrefill jobId={selectedJobState && selectedJobState[0]} form={form} />
</div>
)}
{/* {<ContractLicenseDecodeButton form={form} />} */}
{
//<ContractLicenseDecodeButton form={form} />
}
</Space>
</LayoutFormRow>
<LayoutFormRow noDivider={true}>
</div>
<LayoutFormRow header={t("contracts.labels.driverinformation")}>
<Form.Item
label={t("contracts.fields.driver_dlnumber")}
name="driver_dlnumber"
@@ -181,8 +183,9 @@ export default function ContractFormComponent({ form, create = false, selectedJo
const dlExpiresBeforeReturn = dayjs(form.getFieldValue("driver_dlexpiry")).isBefore(
dayjs(form.getFieldValue("scheduledreturn"))
);
return (
<>
<div>
<Form.Item
label={t("contracts.fields.driver_dlexpiry")}
name="driver_dlexpiry"
@@ -201,10 +204,11 @@ export default function ContractFormComponent({ form, create = false, selectedJo
<span>{t("contracts.labels.dlexpirebeforereturn")}</span>
</Space>
)}
</>
</div>
);
}}
</Form.Item>
<Form.Item label={t("contracts.fields.driver_dlst")} name="driver_dlst">
<Input />
</Form.Item>
@@ -311,6 +315,6 @@ export default function ContractFormComponent({ form, create = false, selectedJo
<InputNumber precision={2} />
</Form.Item>
</LayoutFormRow>
</>
</div>
);
}

View File

@@ -1,62 +1,66 @@
import { SyncOutlined } from "@ant-design/icons";
import { Button, Card, Form, Input, Table } from "antd";
import React, { useEffect, useState } from "react";
import React, { useEffect, useState, useContext } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { pageLimit } from "../../utils/config";
import SocketContext from "../../contexts/SocketIO/socketContext.jsx"; // Import SocketContext
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps)(DmsAllocationsSummaryAp);
export default connect(mapStateToProps, mapDispatchToProps)(DmsAllocationsSummaryAp);
export function DmsAllocationsSummaryAp({ socket, bodyshop, billids, title }) {
export function DmsAllocationsSummaryAp({ bodyshop, billids, title }) {
const { t } = useTranslation();
const [allocationsSummary, setAllocationsSummary] = useState([]);
const { socket } = useContext(SocketContext);
useEffect(() => {
socket.on("ap-export-success", (billid) => {
if (!socket || !socket.connected) return;
const handleSuccess = async (billid) => {
setAllocationsSummary((allocationsSummary) =>
allocationsSummary.map((a) => {
if (a.billid !== billid) return a;
return { ...a, status: "Successful" };
})
);
});
socket.on("ap-export-failure", ({ billid, error }) => {
allocationsSummary.map((a) => {
if (a.billid !== billid) return a;
return { ...a, status: error };
});
});
if (socket.disconnected) socket.connect();
return () => {
socket.removeListener("ap-export-success");
socket.removeListener("ap-export-failure");
//socket.disconnect();
try {
await new Promise((resolve, reject) => {
socket.emit("clear-dms-session", (response) => {
if (response && response.status === "ok") {
resolve();
} else {
reject(new Error("Failed to clear DMS session"));
}
});
});
} catch (error) {
console.error("Failed to clear DMS session", error);
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
socket.on("ap-export-success", handleSuccess);
return () => {
socket.off("ap-export-success", handleSuccess);
};
}, [socket]);
useEffect(() => {
if (socket.connected) {
if (socket && socket.connected) {
socket.emit("pbs-calculate-allocations-ap", billids, (ack) => {
setAllocationsSummary(ack);
socket.allocationsSummary = ack;
});
}
}, [socket, socket.connected, billids]);
console.log(allocationsSummary);
const columns = [
{
title: t("general.labels.status"),
@@ -68,35 +72,40 @@ export function DmsAllocationsSummaryAp({ socket, bodyshop, billids, title }) {
dataIndex: ["Posting", "Reference"],
key: "reference"
},
{
title: t("jobs.fields.dms.lines"),
dataIndex: "Lines",
key: "Lines",
render: (text, record) => (
<table style={{ tableLayout: "auto", width: "100%" }}>
<tr>
<th>{t("bills.fields.invoice_number")}</th>
<th>{t("bodyshop.fields.dms.dms_acctnumber")}</th>
<th>{t("jobs.fields.dms.amount")}</th>
</tr>
{record.Posting.Lines.map((l, idx) => (
<tr key={idx}>
<td>{l.InvoiceNumber}</td>
<td>{l.Account}</td>
<td>{l.Amount}</td>
<thead>
<tr>
<th>{t("bills.fields.invoice_number")}</th>
<th>{t("bodyshop.fields.dms.dms_acctnumber")}</th>
<th>{t("jobs.fields.dms.amount")}</th>
</tr>
))}
</thead>
<tbody>
{record.Posting.Lines.map((l, idx) => (
<tr key={idx}>
<td>{l.InvoiceNumber}</td>
<td>{l.Account}</td>
<td>{l.Amount}</td>
</tr>
))}
</tbody>
</table>
)
}
];
const handleFinish = async (values) => {
socket.emit(`pbs-export-ap`, {
billids,
txEnvelope: values
});
if (socket) {
socket.emit("pbs-export-ap", {
billids,
txEnvelope: values
});
}
};
return (
@@ -105,7 +114,9 @@ export function DmsAllocationsSummaryAp({ socket, bodyshop, billids, title }) {
extra={
<Button
onClick={() => {
socket.emit("pbs-calculate-allocations-ap", billids, (ack) => setAllocationsSummary(ack));
if (socket) {
socket.emit("pbs-calculate-allocations-ap", billids, (ack) => setAllocationsSummary(ack));
}
}}
>
<SyncOutlined />
@@ -124,12 +135,7 @@ export function DmsAllocationsSummaryAp({ socket, bodyshop, billids, title }) {
name="journal"
label={t("jobs.fields.dms.journal")}
initialValue={bodyshop.cdk_configuration && bodyshop.cdk_configuration.default_journal}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
rules={[{ required: true }]}
>
<Input />
</Form.Item>

View File

@@ -24,7 +24,7 @@ export function DmsAllocationsSummary({ socket, bodyshop, jobId, title }) {
const [allocationsSummary, setAllocationsSummary] = useState([]);
useEffect(() => {
if (socket.connected) {
if (socket && socket.connected) {
socket.emit("cdk-calculate-allocations", jobId, (ack) => {
setAllocationsSummary(ack);
socket.allocationsSummary = ack;

View File

@@ -1,37 +1,51 @@
import { Button, Checkbox, Col, Table } from "antd";
import React, { useState } from "react";
import React, { useContext, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { socket } from "../../pages/dms/dms.container";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { alphaSort } from "../../utils/sorters";
import SocketContext from "../../contexts/SocketIO/socketContext"; // Import Socket Context
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
const mapDispatchToProps = (dispatch) => ({});
export default connect(mapStateToProps, mapDispatchToProps)(DmsCustomerSelector);
export function DmsCustomerSelector({ bodyshop }) {
const { t } = useTranslation();
const [customerList, setcustomerList] = useState([]);
const { socket } = useContext(SocketContext); // Use Socket Context
const [customerList, setCustomerList] = useState([]);
const [open, setOpen] = useState(false);
const [selectedCustomer, setSelectedCustomer] = useState(null);
const [dmsType, setDmsType] = useState("cdk");
socket.on("cdk-select-customer", (customerList, callback) => {
setOpen(true);
setDmsType("cdk");
setcustomerList(customerList);
});
socket.on("pbs-select-customer", (customerList, callback) => {
setOpen(true);
setDmsType("pbs");
setcustomerList(customerList);
});
useEffect(() => {
if (socket) {
const handleCdkSelectCustomer = (customerList) => {
setOpen(true);
setDmsType("cdk");
setCustomerList(customerList);
};
const handlePbsSelectCustomer = (customerList) => {
setOpen(true);
setDmsType("pbs");
setCustomerList(customerList);
};
socket.on("cdk-select-customer", handleCdkSelectCustomer);
socket.on("pbs-select-customer", handlePbsSelectCustomer);
// Clean up listeners on unmount
return () => {
socket.off("cdk-select-customer", handleCdkSelectCustomer);
socket.off("pbs-select-customer", handlePbsSelectCustomer);
};
}
}, [socket]);
const onUseSelected = () => {
setOpen(false);
@@ -69,17 +83,11 @@ export function DmsCustomerSelector({ bodyshop }) {
key: "name1",
sorter: (a, b) => alphaSort(a.name1 && a.name1.fullName, b.name1 && b.name1.fullName)
},
{
title: t("jobs.fields.dms.address"),
//dataIndex: ["name2", "fullName"],
key: "address",
render: (record, value) =>
`${
record.address && record.address.addressLine && record.address.addressLine[0]
}, ${record.address && record.address.city} ${
record.address && record.address.stateOrProvince
} ${record.address && record.address.postalCode}`
render: (record) =>
`${record.address?.addressLine?.[0]}, ${record.address?.city} ${record.address?.stateOrProvince} ${record.address?.postalCode}`
}
];
@@ -95,15 +103,15 @@ export function DmsCustomerSelector({ bodyshop }) {
sorter: (a, b) => alphaSort(a.LastName, b.LastName),
render: (text, record) => `${record.FirstName || ""} ${record.LastName || ""}`
},
{
title: t("jobs.fields.dms.address"),
key: "address",
render: (record, value) => `${record.Address}, ${record.City} ${record.State} ${record.ZipCode}`
render: (record) => `${record.Address}, ${record.City} ${record.State} ${record.ZipCode}`
}
];
if (!open) return null;
return (
<Col span={24}>
<Table
@@ -125,7 +133,6 @@ export function DmsCustomerSelector({ bodyshop }) {
columns={dmsType === "cdk" ? cdkColumns : pbsColumns}
rowKey={(record) => (dmsType === "cdk" ? record.id.value : record.ContactId)}
dataSource={customerList}
//onChange={handleTableChange}
rowSelection={{
onSelect: (record) => {
setSelectedCustomer(dmsType === "cdk" ? record.id.value : record.ContactId);

View File

@@ -4,11 +4,8 @@ import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
const mapStateToProps = createStructuredSelector({});
const mapDispatchToProps = (dispatch) => ({
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
@@ -17,7 +14,7 @@ const mapDispatchToProps = (dispatch) => ({
export default connect(mapStateToProps, mapDispatchToProps)(DmsLogEvents);
export function DmsLogEvents({ socket, logs, bodyshop }) {
export function DmsLogEvents({ logs }) {
return (
<Timeline
pending
@@ -40,6 +37,8 @@ export function DmsLogEvents({ socket, logs, bodyshop }) {
function LogLevelHierarchy(level) {
switch (level) {
case "TRACE":
return "pink";
case "DEBUG":
return "orange";
case "INFO":

View File

@@ -8,7 +8,7 @@ import { INSERT_EULA_ACCEPTANCE } from "../../graphql/user.queries";
import { useMutation } from "@apollo/client";
import { acceptEula } from "../../redux/user/user.actions";
import { useTranslation } from "react-i18next";
import dayjs from "../../utils/day";
import day from "../../utils/day";
import "./eula.styles.scss";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
@@ -208,7 +208,7 @@ const EulaFormComponent = ({ form, handleChange, onFinish, t }) => (
{
required: true,
validator: (_, value) => {
if (dayjs(value).isSame(dayjs(), "day")) {
if (day(value).isSame(day(), "day")) {
return Promise.resolve();
}
return Promise.reject(new Error(t("eula.messages.date_accepted")));

View File

@@ -4,37 +4,19 @@ import React, { useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import dayjs from "../../utils/day";
import { fuzzyMatchDate } from "./formats.js";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors.js";
import { connect } from "react-redux";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
const DateTimePicker = ({
value,
onChange,
onBlur,
id,
onlyFuture,
onlyToday,
isDateOnly = false,
bodyshop,
...restProps
}) => {
const DateTimePicker = ({ value, onChange, onBlur, id, onlyFuture, onlyToday, isDateOnly = false, ...restProps }) => {
const [isManualInput, setIsManualInput] = useState(false);
const { t } = useTranslation();
const handleChange = useCallback(
(newDate) => {
if (!newDate) return;
if (onChange) {
onChange(bodyshop?.timezone ? dayjs(newDate).tz(bodyshop.timezone, true) : newDate);
onChange(newDate || null);
}
setIsManualInput(false);
},
[onChange, bodyshop?.timezone]
[onChange]
);
const handleBlur = useCallback(
@@ -120,4 +102,4 @@ DateTimePicker.propTypes = {
isDateOnly: PropTypes.bool
};
export default connect(mapStateToProps, null)(DateTimePicker);
export default React.memo(DateTimePicker);

View File

@@ -3,15 +3,13 @@ import axios from "axios";
import _ from "lodash";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link, useNavigate } from "react-router-dom";
import { Link } from "react-router-dom";
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
export default function GlobalSearchOs() {
const { t } = useTranslation();
const navigate = useNavigate();
const [loading, setLoading] = useState(false);
const [data, setData] = useState(false);
@@ -179,18 +177,7 @@ export default function GlobalSearchOs() {
};
return (
<AutoComplete
options={data}
onSearch={handleSearch}
onKeyDown={(e) => {
if (e.key !== "Enter") return;
const firstUrlForSearch = data?.[0]?.options?.[0]?.label?.props?.to;
if (!firstUrlForSearch) return;
navigate(firstUrlForSearch);
}}
defaultActiveFirstOption
onClear={() => setData([])}
>
<AutoComplete options={data} onSearch={handleSearch} defaultActiveFirstOption onClear={() => setData([])}>
<Input.Search
size="large"
placeholder={t("general.labels.globalsearch")}

View File

@@ -3,7 +3,7 @@ import { AutoComplete, Divider, Input, Space } from "antd";
import _ from "lodash";
import React from "react";
import { useTranslation } from "react-i18next";
import { Link, useNavigate } from "react-router-dom";
import { Link } from "react-router-dom";
import { GLOBAL_SEARCH_QUERY } from "../../graphql/search.queries";
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
import AlertComponent from "../alert/alert.component";
@@ -13,7 +13,6 @@ import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.compon
export default function GlobalSearch() {
const { t } = useTranslation();
const [callSearch, { loading, error, data }] = useLazyQuery(GLOBAL_SEARCH_QUERY);
const navigate = useNavigate();
const executeSearch = (v) => {
if (v && v.variables.search && v.variables.search !== "" && v.variables.search.length >= 3) callSearch(v);
@@ -21,6 +20,7 @@ export default function GlobalSearch() {
const debouncedExecuteSearch = _.debounce(executeSearch, 750);
const handleSearch = (value) => {
console.log("Handle Search");
debouncedExecuteSearch({ variables: { search: value } });
};
@@ -156,17 +156,7 @@ export default function GlobalSearch() {
if (error) return <AlertComponent message={error.message} type="error" />;
return (
<AutoComplete
options={options}
onSearch={handleSearch}
defaultActiveFirstOption
onKeyDown={(e) => {
if (e.key !== "Enter") return;
const firstUrlForSearch = options?.[0]?.options?.[0]?.label?.props?.to;
if (!firstUrlForSearch) return;
navigate(firstUrlForSearch);
}}
>
<AutoComplete options={options} onSearch={handleSearch} defaultActiveFirstOption>
<Input.Search
size="large"
placeholder={t("general.labels.globalsearch")}

View File

@@ -23,7 +23,6 @@ import ScheduleEventColor from "./schedule-event.color.component";
import ScheduleEventNote from "./schedule-event.note.component";
import { useMutation } from "@apollo/client";
import { UPDATE_APPOINTMENT } from "../../graphql/appointments.queries";
import ProductionListColumnComment from "../production-list-columns/production-list-columns.comment.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
@@ -128,9 +127,6 @@ export function ScheduleEventComponent({
{(event.job && event.job.alt_transport) || ""}
<ScheduleAtChange job={event && event.job} />
</DataLabel>
<DataLabel label={t("jobs.fields.comment")} valueStyle={{ overflow: "hidden", textOverflow: "ellipsis" }}>
<ProductionListColumnComment record={event && event.job} />
</DataLabel>
<ScheduleEventNote event={event} />
</div>
) : (
@@ -320,7 +316,6 @@ export function ScheduleEventComponent({
})`}
{event.job && event.job.alt_transport && <div style={{ margin: ".1rem" }}>{event.job.alt_transport}</div>}
{event?.job?.comment && `C: ${event.job.comment}`}
</Space>
) : (
<div

View File

@@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useState } from "react";
import dayjs from "../../utils/day";
import day from "../../utils/day";
import axios from "axios";
import { Badge, Card, Space, Table, Tag } from "antd";
import { gql, useQuery } from "@apollo/client";
@@ -72,7 +72,7 @@ export function JobLifecycleComponent({ job, statuses, ...rest }) {
dataIndex: "start",
key: "start",
render: (text) => DateTimeFormatterFunction(text),
sorter: (a, b) => dayjs(a.start).unix() - dayjs(b.start).unix()
sorter: (a, b) => day(a.start).unix() - day(b.start).unix()
},
{
title: t("job_lifecycle.columns.relative_start"),
@@ -90,7 +90,7 @@ export function JobLifecycleComponent({ job, statuses, ...rest }) {
}
return isEmpty(a.end) ? 1 : -1;
}
return dayjs(a.end).unix() - dayjs(b.end).unix();
return day(a.end).unix() - day(b.end).unix();
},
render: (text) => (isEmpty(text) ? t("job_lifecycle.content.not_available") : DateTimeFormatterFunction(text))
},

View File

@@ -2,7 +2,7 @@ import React, { useState } from "react";
import { useMutation } from "@apollo/client";
import { Button, Form, notification, Popover, Select, Space } from "antd";
import dayjs from "../../utils/day";
import day from "../../utils/day";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -48,7 +48,7 @@ export function JobLineDispatchButton({
const result = await dispatchLines({
variables: {
partsDispatch: {
dispatched_at: dayjs(),
dispatched_at: day(),
employeeid: values.employeeid,
jobid: job.id,
dispatched_by: currentUser.email,
@@ -138,11 +138,7 @@ export function JobLineDispatchButton({
return (
<Popover open={visible} content={popMenu}>
<Button
disabled={selectedLines.length === 0 || jobRO || disabled}
loading={loading}
onClick={() => setVisible(true)}
>
<Button disabled={selectedLines.length === 0 || jobRO || disabled} loading={loading} onClick={() => setVisible(true)}>
{t("joblines.actions.dispatchparts", { count: selectedLines.length })}
</Button>
</Popover>

View File

@@ -1,8 +1,5 @@
import { useMutation } from "@apollo/client";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import { notification } from "antd";
import Axios from "axios";
import Dinero from "dinero.js";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -10,10 +7,13 @@ import { createStructuredSelector } from "reselect";
import { INSERT_NEW_JOB_LINE, UPDATE_JOB_LINE } from "../../graphql/jobs-lines.queries";
import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectJobLineEditModal } from "../../redux/modals/modals.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CriticalPartsScan from "../../utils/criticalPartsScan";
import UndefinedToNull from "../../utils/undefinedtonull";
import JobLinesUpdsertModal from "./job-lines-upsert-modal.component";
import Axios from "axios";
import Dinero from "dinero.js";
import CriticalPartsScan from "../../utils/criticalPartsScan";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
const mapStateToProps = createStructuredSelector({
jobLineEditModal: selectJobLineEditModal,
@@ -82,15 +82,13 @@ function JobLinesUpsertModalContainer({ jobLineEditModal, toggleModalVisible, bo
variables: {
lineId: jobLineEditModal.context.id,
line: {
...UndefinedToNull({
...values,
prt_dsmk_m: Dinero({
amount: Math.round(values.act_price * 100)
})
.percentage(Math.abs(values.prt_dsmk_p || 0))
.multiply(values.prt_dsmk_p >= 0 ? 1 : -1)
.toFormat(0.0)
...values,
prt_dsmk_m: Dinero({
amount: Math.round(values.act_price * 100)
})
.percentage(Math.abs(values.prt_dsmk_p || 0))
.multiply(values.prt_dsmk_p >= 0 ? 1 : -1)
.toFormat(0.0)
}
},
refetchQueries: ["GET_LINE_TICKET_BY_PK"]

View File

@@ -219,7 +219,7 @@ export function JobsExportAllButton({
};
return (
<Button onClick={handleQbxml} loading={loading} disabled={disabled || jobIds?.length > 10}>
<Button onClick={handleQbxml} loading={loading} disabled={disabled}>
{t("jobs.actions.exportselected")}
</Button>
);

View File

@@ -1,6 +1,6 @@
import { useMutation } from "@apollo/client";
import { Button, Card, Col, notification, Row, Table } from "antd";
import dayjs from "../../utils/day";
import day from "../../utils/day";
import React from "react";
import { useTranslation } from "react-i18next";
import { UPDATE_PARTS_DISPATCH_LINE } from "../../graphql/parts-dispatch.queries";
@@ -11,7 +11,7 @@ export default function PartsDispatchExpander({ dispatch, job }) {
const [updateDispatchLine] = useMutation(UPDATE_PARTS_DISPATCH_LINE);
const handleAccept = async ({ partsDispatchLineId }) => {
const accepted_at = dayjs();
const accepted_at = day();
const result = await updateDispatchLine({
variables: { id: partsDispatchLineId, line: { accepted_at } },
optimisticResponse: {

View File

@@ -242,8 +242,7 @@ export function PartsOrderListTableComponent({
title: t("general.labels.actions"),
dataIndex: "actions",
key: "actions",
render: (text, record) => recordActions(record, true),
id: "parts-order-list-table-actions"
render: (text, record) => recordActions(record, true)
}
];

View File

@@ -200,7 +200,7 @@ export function PayableExportAll({
);
return (
<Button onClick={handleQbxml} loading={loading} disabled={disabled || billids?.length > 10}>
<Button onClick={handleQbxml} loading={loading} disabled={disabled}>
{t("jobs.actions.exportselected")}
</Button>
);

View File

@@ -180,7 +180,7 @@ export function PaymentsExportAllButton({
};
return (
<Button onClick={handleQbxml} loading={loading} disabled={disabled || paymentIds?.length > 10}>
<Button onClick={handleQbxml} loading={loading} disabled={disabled}>
{t("jobs.actions.exportselected")}
</Button>
);

View File

@@ -1,14 +1,12 @@
import { SyncOutlined } from "@ant-design/icons";
import { PageHeader } from "@ant-design/pro-layout";
import { useApolloClient } from "@apollo/client";
import Board from "./trello-board/index";
import { Button, notification, Skeleton, Space } from "antd";
import cloneDeep from "lodash/cloneDeep";
import isEqual from "lodash/isEqual";
import { PageHeader } from "@ant-design/pro-layout";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import NoteUpsertModal from "../../components/note-upsert-modal/note-upsert-modal.container";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { generate_UPDATE_JOB_KANBAN } from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
@@ -17,13 +15,14 @@ import AuditTrailMapping from "../../utils/AuditTrailMappings";
import IndefiniteLoading from "../indefinite-loading/indefinite-loading.component";
import ProductionBoardFilters from "../production-board-filters/production-board-filters.component";
import ProductionListDetailComponent from "../production-list-detail/production-list-detail.component";
import ProductionListPrint from "../production-list-table/production-list-print.component.jsx";
import CardColorLegend from "./production-board-kanban-card-color-legend.component.jsx";
import "./production-board-kanban.styles.scss";
import { createBoardData } from "./production-board-kanban.utils.js";
import { defaultFilters, mergeWithDefaults } from "./settings/defaultKanbanSettings.js";
import ProductionBoardKanbanSettings from "./settings/production-board-kanban.settings.component.jsx";
import Board from "./trello-board/index";
import cloneDeep from "lodash/cloneDeep";
import isEqual from "lodash/isEqual";
import { defaultFilters, mergeWithDefaults } from "./settings/defaultKanbanSettings.js";
import NoteUpsertModal from "../../components/note-upsert-modal/note-upsert-modal.container";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
@@ -186,7 +185,7 @@ function ProductionBoardKanbanComponent({ data, bodyshop, refetch, insertAuditTr
const cardSettings = useMemo(() => {
const kanbanSettings = associationSettings?.kanban_settings;
return mergeWithDefaults(kanbanSettings);
}, [associationSettings?.kanban_settings]);
}, [associationSettings]);
const handleSettingsChange = () => {
setFilter(defaultFilters);
@@ -215,7 +214,6 @@ function ProductionBoardKanbanComponent({ data, bodyshop, refetch, insertAuditTr
bodyshop={bodyshop}
data={data}
/>
<ProductionListPrint />
</Space>
}
/>

View File

@@ -1,9 +1,8 @@
import React, { useContext, useEffect, useMemo, useRef } from "react";
import { useApolloClient, useQuery, useSubscription } from "@apollo/client";
import React, { useEffect, useMemo, useRef } from "react";
import { useQuery, useSubscription } from "@apollo/client";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import {
QUERY_EXACT_JOB_IN_PRODUCTION,
QUERY_JOBS_IN_PRODUCTION,
SUBSCRIPTION_JOBS_IN_PRODUCTION,
SUBSCRIPTION_JOBS_IN_PRODUCTION_VIEW
@@ -11,8 +10,6 @@ import {
import { QUERY_KANBAN_SETTINGS } from "../../graphql/user.queries";
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import ProductionBoardKanbanComponent from "./production-board-kanban.component";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -20,20 +17,7 @@ const mapStateToProps = createStructuredSelector({
});
function ProductionBoardKanbanContainer({ bodyshop, currentUser, subscriptionType = "direct" }) {
const fired = useRef(false);
const client = useApolloClient();
const { socket } = useContext(SocketContext); // Get the socket from context
const reconnectTimeout = useRef(null); // To store the reconnect timeout
const disconnectTime = useRef(null); // To track disconnection time
const acceptableReconnectTime = 2000; // 2 seconds threshold
const {
treatments: { Websocket_Production }
} = useSplitTreatments({
attributes: {},
names: ["Websocket_Production"],
splitKey: bodyshop && bodyshop.imexshopid
});
const fired = useRef(false); // useRef to keep track of whether the subscription fired
const combinedStatuses = useMemo(
() => [
@@ -50,12 +34,9 @@ function ProductionBoardKanbanContainer({ bodyshop, currentUser, subscriptionTyp
onError: (error) => console.error(`Error fetching jobs in production: ${error.message}`)
});
const subscriptionEnabled = Websocket_Production?.treatment === "off";
const { data: updatedJobs } = useSubscription(
subscriptionType === "view" ? SUBSCRIPTION_JOBS_IN_PRODUCTION_VIEW : SUBSCRIPTION_JOBS_IN_PRODUCTION,
{
skip: !subscriptionEnabled,
onError: (error) => console.error(`Error subscribing to jobs in production: ${error.message}`)
}
);
@@ -65,112 +46,23 @@ function ProductionBoardKanbanContainer({ bodyshop, currentUser, subscriptionTyp
onError: (error) => console.error(`Error fetching Kanban settings: ${error.message}`)
});
useEffect(() => {
if (subscriptionEnabled) {
if (!updatedJobs) {
return;
}
if (!fired.current) {
fired.current = true;
return;
}
refetch().catch((err) => console.error(`Error re-fetching jobs in production: ${err.message}`));
}
}, [updatedJobs, refetch, subscriptionEnabled]);
// This provides us the current version of the Lanes from the Redux store
// const currentReducerData = useSelector((state) => (state.trello.lanes ? state.trello : {}));
// Socket.IO implementation for users with Split treatment "off"
useEffect(() => {
if (subscriptionEnabled || !socket || !bodyshop || !bodyshop.id) {
if (!updatedJobs) {
return;
}
const handleJobUpdates = async (jobChangedData) => {
const jobId = jobChangedData.id;
// Access the existing cache for QUERY_JOBS_IN_PRODUCTION
const existingJobsCache = client.readQuery({
query: QUERY_JOBS_IN_PRODUCTION
});
const existingJobs = existingJobsCache?.jobs || [];
// Check if the job already exists in the cached jobs
const existingJob = existingJobs.find((job) => job.id === jobId);
if (existingJob) {
// If the job exists, we update the cache without making any additional queries
client.writeQuery({
query: QUERY_JOBS_IN_PRODUCTION,
data: {
jobs: existingJobs.map((job) =>
job.id === jobId ? { ...existingJob, ...jobChangedData, __typename: "jobs" } : job
)
}
});
} else {
// If the job doesn't exist, fetch it from the server and then add it to the cache
try {
const { data: jobData } = await client.query({
query: QUERY_EXACT_JOB_IN_PRODUCTION,
variables: { id: jobId },
fetchPolicy: "network-only"
});
// Add the job to the existing cached jobs
client.writeQuery({
query: QUERY_JOBS_IN_PRODUCTION,
data: {
jobs: [...existingJobs, { ...jobData.job, __typename: "jobs" }]
}
});
} catch (error) {
console.error(`Error fetching job ${jobId}: ${error.message}`);
}
}
};
const handleDisconnect = () => {
// Capture the disconnection time
disconnectTime.current = Date.now();
};
const handleReconnect = () => {
const reconnectTime = Date.now();
const disconnectionDuration = reconnectTime - disconnectTime.current;
// Only refetch if disconnection was longer than the acceptable reconnect time
if (disconnectionDuration >= acceptableReconnectTime) {
if (!reconnectTimeout.current) {
reconnectTimeout.current = setTimeout(() => {
const randomDelay = Math.floor(Math.random() * (30000 - 10000 + 1)) + 10000; // Random delay between 10 and 30 seconds
setTimeout(() => {
if (refetch) refetch().catch((err) => console.error(`Issue `));
reconnectTimeout.current = null; // Clear the timeout reference after refetch
}, randomDelay);
}, acceptableReconnectTime);
}
}
};
// Listen for 'job-changed', 'disconnect', and 'connect' events
socket.on("production-job-updated", handleJobUpdates);
socket.on("disconnect", handleDisconnect);
socket.on("connect", handleReconnect);
// Clean up on unmount or when dependencies change
return () => {
socket.off("production-job-updated", handleJobUpdates);
socket.off("disconnect", handleDisconnect);
socket.off("connect", handleReconnect);
if (reconnectTimeout.current) {
clearTimeout(reconnectTimeout.current);
}
};
}, [subscriptionEnabled, socket, bodyshop, client, refetch]);
if (!fired.current) {
fired.current = true;
return;
}
refetch().catch((err) => console.error(`Error re-fetching jobs in production: ${err.message}`));
}, [updatedJobs, refetch]);
const filteredAssociationSettings = useMemo(() => {
return associationSettings?.associations[0] || null;
}, [associationSettings?.associations]);
}, [associationSettings]);
return (
<ProductionBoardKanbanComponent

View File

@@ -1,66 +1,15 @@
import InstanceRenderManager from "../../../utils/instanceRenderMgr.js";
const statisticsItems = [
{ id: 0, name: "totalHrs", label: "total_hours_in_production" },
{ id: 1, name: "totalAmountInProduction", label: "total_amount_in_production" },
{ id: 2, name: "totalLAB", label: "total_lab_in_production" },
{ id: 3, name: "totalLAR", label: "total_lar_in_production" },
{ id: 4, name: "jobsInProduction", label: "jobs_in_production" },
{
id: 5,
name: "totalHrsOnBoard",
label: InstanceRenderManager({
imex: "total_hours_in_view",
rome: "total_hours_on_board",
promanager: "total_hours_on_board"
})
},
{
id: 6,
name: "totalAmountOnBoard",
label: InstanceRenderManager({
imex: "total_amount_in_view",
rome: "total_amount_on_board",
promanager: "total_amount_on_board"
})
},
{
id: 7,
name: "totalLABOnBoard",
label: InstanceRenderManager({
imex: "total_lab_in_view",
rome: "total_lab_on_board",
promanager: "total_lab_on_board"
})
},
{
id: 8,
name: "totalLAROnBoard",
label: InstanceRenderManager({
imex: "total_lar_in_view",
rome: "total_lar_on_board",
promanager: "total_lar_on_board"
})
},
{
id: 9,
name: "jobsOnBoard",
label: InstanceRenderManager({
imex: "total_jobs_in_view",
rome: "total_jobs_on_board",
promanager: "total_jobs_on_board"
})
},
{
id: 10,
name: "tasksOnBoard",
label: InstanceRenderManager({
imex: "tasks_in_view",
rome: "tasks_on_board",
promanager: "tasks_on_board"
})
},
{ id: 5, name: "totalHrsOnBoard", label: "total_hours_on_board" },
{ id: 6, name: "totalAmountOnBoard", label: "total_amount_on_board" },
{ id: 7, name: "totalLABOnBoard", label: "total_lab_on_board" },
{ id: 8, name: "totalLAROnBoard", label: "total_lar_on_board" },
{ id: 9, name: "jobsOnBoard", label: "total_jobs_on_board" },
{ id: 10, name: "tasksOnBoard", label: "tasks_on_board" },
{ id: 11, name: "tasksInProduction", label: "tasks_in_production" }
];

View File

@@ -21,26 +21,25 @@ export function ProductionListColumnStatus({ record, bodyshop, insertAuditTrail
const [loading, setLoading] = useState(false);
const handleSetStatus = async (e) => {
if (bodyshop.md_ro_statuses.production_statuses.includes(record.status) && !bodyshop.md_ro_statuses.post_production_statuses.includes(record.status)) {
logImEXEvent("production_change_status");
// e.stopPropagation();
setLoading(true);
const { key } = e;
await updateJob({
variables: {
jobId: record.id,
job: {
status: key
}
logImEXEvent("production_change_status");
// e.stopPropagation();
setLoading(true);
const { key } = e;
await updateJob({
variables: {
jobId: record.id,
job: {
status: key
}
});
insertAuditTrail({
jobid: record.id,
operation: AuditTrailMapping.jobstatuschange(key),
type: "jobstatuschange"
});
setLoading(false);
}
}
});
insertAuditTrail({
jobid: record.id,
operation: AuditTrailMapping.jobstatuschange(key),
type: "jobstatuschange"
});
setLoading(false);
};
const menu = {

View File

@@ -457,42 +457,41 @@ export function ProductionListConfigManager({
value={activeView}
disabled={open || isAddingNewProfile} // Disable the Select box when the popover is open or adding a new profile
>
{bodyshop?.production_config &&
bodyshop.production_config
.slice()
.sort((a, b) =>
a.name === t("production.constants.main_profile")
? -1
: b.name === t("production.constants.main_profile")
? 1
: 0
) //
.map((config) => (
<Select.Option key={config.name} label={config.name}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<span
style={{
flex: 1,
maxWidth: "80%",
marginRight: "1rem",
textOverflow: "ellipsis"
}}
{bodyshop.production_config
.slice()
.sort((a, b) =>
a.name === t("production.constants.main_profile")
? -1
: b.name === t("production.constants.main_profile")
? 1
: 0
) //
.map((config) => (
<Select.Option key={config.name} label={config.name}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<span
style={{
flex: 1,
maxWidth: "80%",
marginRight: "1rem",
textOverflow: "ellipsis"
}}
>
{config.name}
</span>
{config.name !== t("production.constants.main_profile") && (
<Popconfirm
placement="right"
title={t("general.labels.areyousure")}
onConfirm={() => handleTrash(config.name)}
onCancel={(e) => e.stopPropagation()}
>
{config.name}
</span>
{config.name !== t("production.constants.main_profile") && (
<Popconfirm
placement="right"
title={t("general.labels.areyousure")}
onConfirm={() => handleTrash(config.name)}
onCancel={(e) => e.stopPropagation()}
>
<DeleteOutlined onClick={(e) => e.stopPropagation()} />
</Popconfirm>
)}
</div>
</Select.Option>
))}
<DeleteOutlined onClick={(e) => e.stopPropagation()} />
</Popconfirm>
)}
</div>
</Select.Option>
))}
<Select.Option key="add_new" label={t("production.labels.addnewprofile")}>
<div style={{ display: "flex", alignItems: "center" }}>
<PlusOutlined style={{ marginRight: "0.5rem" }} />

View File

@@ -51,8 +51,8 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
const initialColumnsRef = useRef(
(initialStateRef.current &&
bodyshop?.production_config
?.find((p) => p.name === defaultView)
bodyshop.production_config
.find((p) => p.name === defaultView)
?.columns.columnKeys.map((k) => {
return {
...ProductionListColumns({
@@ -76,7 +76,7 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
const { t } = useTranslation();
const matchingColumnConfig = useMemo(() => {
return bodyshop?.production_config?.find((p) => p.name === defaultView);
return bodyshop.production_config.find((p) => p.name === defaultView);
}, [bodyshop.production_config, defaultView]);
useEffect(() => {

View File

@@ -1,5 +1,5 @@
import { useApolloClient, useQuery, useSubscription } from "@apollo/client";
import React, { useContext, useEffect, useState, useRef } from "react";
import React, { useEffect, useState } from "react";
import {
QUERY_EXACT_JOB_IN_PRODUCTION,
QUERY_EXACT_JOBS_IN_PRODUCTION,
@@ -9,46 +9,19 @@ import {
} from "../../graphql/jobs.queries";
import ProductionListTable from "./production-list-table.component";
import _ from "lodash";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
export default function ProductionListTableContainer({ bodyshop, subscriptionType = "direct" }) {
const client = useApolloClient();
const { socket } = useContext(SocketContext);
const [joblist, setJoblist] = useState([]);
const reconnectTimeout = useRef(null); // To store the reconnect timeout
const disconnectTime = useRef(null); // To store the time of disconnection
const acceptableReconnectTime = 2000; // 2 seconds threshold
// Get Split treatment
const {
treatments: { Websocket_Production }
} = useSplitTreatments({
attributes: {},
names: ["Websocket_Production"],
splitKey: bodyshop && bodyshop.imexshopid
});
// Determine if subscription is enabled
const subscriptionEnabled = Websocket_Production?.treatment === "off";
// Use GraphQL query
export default function ProductionListTableContainer({ subscriptionType = "direct" }) {
const { refetch, loading, data } = useQuery(QUERY_JOBS_IN_PRODUCTION, {
pollInterval: 3600000,
fetchPolicy: "network-only",
nextFetchPolicy: "network-only"
});
// Use GraphQL subscription when subscription is enabled
const client = useApolloClient();
const [joblist, setJoblist] = useState([]);
const { data: updatedJobs } = useSubscription(
subscriptionType === "view" ? SUBSCRIPTION_JOBS_IN_PRODUCTION_VIEW : SUBSCRIPTION_JOBS_IN_PRODUCTION,
{
skip: !subscriptionEnabled
}
subscriptionType === "view" ? SUBSCRIPTION_JOBS_IN_PRODUCTION_VIEW : SUBSCRIPTION_JOBS_IN_PRODUCTION
);
// Update joblist when data changes
useEffect(() => {
if (!(data && data.jobs)) return;
setJoblist(
@@ -58,134 +31,34 @@ export default function ProductionListTableContainer({ bodyshop, subscriptionTyp
);
}, [data]);
// Handle updates from GraphQL subscription
useEffect(() => {
if (subscriptionEnabled) {
if (!updatedJobs || joblist.length === 0) return;
if (!updatedJobs || joblist.length === 0) return;
const jobDiff = _.differenceWith(
joblist,
updatedJobs.jobs,
(a, b) => a.id === b.id && a.updated_at === b.updated_at
);
const jobDiff = _.differenceWith(
joblist,
updatedJobs.jobs,
(a, b) => a.id === b.id && a.updated_at === b.updated_at
);
if (jobDiff.length > 1) {
getUpdatedJobsData(jobDiff.map((j) => j.id));
} else if (jobDiff.length === 1) {
jobDiff.forEach((job) => {
getUpdatedJobData(job.id);
});
}
setJoblist(updatedJobs.jobs);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [updatedJobs, subscriptionEnabled]);
// Handle updates from Socket.IO when subscription is disabled
useEffect(() => {
if (subscriptionEnabled || !socket || !bodyshop || !bodyshop.id) {
return;
}
const handleJobUpdates = async (jobChangedData) => {
const jobId = jobChangedData.id;
// Access the existing cache for QUERY_JOBS_IN_PRODUCTION
const existingJobsCache = client.readQuery({
query: QUERY_JOBS_IN_PRODUCTION
if (jobDiff.length > 1) {
getUpdatedJobsData(jobDiff.map((j) => j.id));
} else if (jobDiff.length === 1) {
jobDiff.forEach((job) => {
getUpdatedJobData(job.id);
});
}
const existingJobs = existingJobsCache?.jobs || [];
setJoblist(updatedJobs.jobs);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [updatedJobs]);
// Check if the job already exists in the cached jobs
const existingJob = existingJobs.find((job) => job.id === jobId);
if (existingJob) {
// If the job exists, we update the cache without making any additional queries
client.writeQuery({
query: QUERY_JOBS_IN_PRODUCTION,
data: {
jobs: existingJobs.map((job) =>
job.id === jobId ? { ...existingJob, ...jobChangedData, __typename: "jobs" } : job
)
}
});
} else {
// If the job doesn't exist, fetch it from the server and then add it to the cache
try {
const { data: jobData } = await client.query({
query: QUERY_EXACT_JOB_IN_PRODUCTION,
variables: { id: jobId },
fetchPolicy: "network-only"
});
// Add the job to the existing cached jobs
client.writeQuery({
query: QUERY_JOBS_IN_PRODUCTION,
data: {
jobs: [...existingJobs, { ...jobData.job, __typename: "jobs" }]
}
});
} catch (error) {
console.error(`Error fetching job ${jobId}: ${error.message}`);
}
}
};
const handleDisconnect = () => {
// Capture the time when the disconnection happens
disconnectTime.current = Date.now();
};
const handleReconnect = () => {
// Calculate how long the disconnection lasted
const reconnectTime = Date.now();
const disconnectionDuration = reconnectTime - disconnectTime.current;
// If disconnection lasted less than acceptable reconnect time, do nothing
if (disconnectionDuration < acceptableReconnectTime) {
return;
}
// Schedule a refetch with a random delay between 10 and 30 seconds
if (!reconnectTimeout.current) {
reconnectTimeout.current = setTimeout(() => {
const randomDelay = Math.floor(Math.random() * (30000 - 10000 + 1)) + 10000; // Random delay between 10 and 30 seconds
setTimeout(() => {
if (refetch) refetch();
reconnectTimeout.current = null; // Clear the timeout reference after refetch
}, randomDelay);
}, acceptableReconnectTime);
}
};
// Listen for 'production-job-updated', 'disconnect', and 'connect' events
socket.on("production-job-updated", handleJobUpdates);
socket.on("disconnect", handleDisconnect);
socket.on("connect", handleReconnect);
// Clean up on unmount or when dependencies change
return () => {
socket.off("production-job-updated", handleJobUpdates);
socket.off("disconnect", handleDisconnect);
socket.off("connect", handleReconnect);
if (reconnectTimeout.current) {
clearTimeout(reconnectTimeout.current);
}
};
}, [subscriptionEnabled, socket, bodyshop, client, refetch]);
// Functions to fetch updated job data
const getUpdatedJobData = async (jobId) => {
await client.query({
client.query({
query: QUERY_EXACT_JOB_IN_PRODUCTION,
variables: { id: jobId },
fetchPolicy: "network-only"
variables: { id: jobId }
});
};
const getUpdatedJobsData = (jobIds) => {
const getUpdatedJobsData = async (jobIds) => {
client.query({
query: QUERY_EXACT_JOBS_IN_PRODUCTION,
variables: { ids: jobIds }

View File

@@ -120,6 +120,14 @@ var formats = {
};
const localizer = (dayjsLib) => {
// load dayjs plugins
dayjsLib.extend(isBetween);
dayjsLib.extend(isSameOrAfter);
dayjsLib.extend(isSameOrBefore);
dayjsLib.extend(localeData);
dayjsLib.extend(localizedFormat);
dayjsLib.extend(minMax);
dayjsLib.extend(utc);
var locale = function locale(dj, c) {
return c ? dj.locale(c) : dj;
};
@@ -128,8 +136,7 @@ const localizer = (dayjsLib) => {
// then use the timezone aware version
//TODO This was the issue entirely...
// var dayjs = dayjsLib.tz ? dayjsLib.tz : dayjsLib;
// var dayjs = dayjsLib.tz ? dayjsLib.tz : dayjsLib;
var dayjs = dayjsLib;
function getTimezoneOffset(date) {

View File

@@ -20,7 +20,6 @@ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
problemJobs: selectProblemJobs
});
const localizer = local(dayjs);
export function ScheduleCalendarWrapperComponent({

View File

@@ -1,3 +1,10 @@
import { Button, Card, Space, Switch, Table } from "antd";
import queryString from "query-string";
import React, { useCallback, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { Link, useLocation, useNavigate } from "react-router-dom";
import { pageLimit } from "../../utils/config";
import dayjs from "../../utils/day";
import {
CheckCircleFilled,
CheckCircleOutlined,
@@ -8,16 +15,9 @@ import {
PlusCircleFilled,
SyncOutlined
} from "@ant-design/icons";
import { Button, Card, Space, Switch, Table } from "antd";
import queryString from "query-string";
import React, { useCallback, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link, useLocation, useNavigate } from "react-router-dom";
import { setModalContext } from "../../redux/modals/modals.actions";
import { pageLimit } from "../../utils/config";
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter.jsx";
import dayjs from "../../utils/day";
import { connect } from "react-redux";
import { setModalContext } from "../../redux/modals/modals.actions";
/**
* Task List Component
@@ -140,17 +140,6 @@ function TaskListComponent({
render: (text, record) => <DateTimeFormatter>{record.created_at}</DateTimeFormatter>
});
columns.push({
title: t("tasks.fields.created_by"),
dataIndex: "created_by",
key: "created_by",
width: "10%",
defaultSortOrder: "descend",
sorter: true,
sortOrder: sortcolumn === "created_by" && sortorder,
render: (text, record) => record.created_by
});
if (!onlyMine) {
columns.push({
title: t("tasks.fields.assigned_to"),

View File

@@ -1,6 +1,6 @@
import { useMutation } from "@apollo/client";
import { Button, notification } from "antd";
import dayjs from "../../utils/day";
import day from "../../utils/day";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -29,7 +29,7 @@ export function TimeTicketsCommit({ bodyshop, currentUser, timeticket, disabled,
? { commited_by: null, committed_at: null }
: {
commited_by: currentUser.email,
committed_at: dayjs()
committed_at: day()
};
const result = await updateTimeTicket({

View File

@@ -1,6 +1,6 @@
import { useMutation } from "@apollo/client";
import { Button, notification } from "antd";
import dayjs from "../../utils/day";
import day from "../../utils/day";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -34,7 +34,7 @@ export function TimeTicketsCommit({
timeticketIds: timetickets.map((ticket) => ticket.id),
timeticket: {
commited_by: currentUser.email,
committed_at: dayjs()
committed_at: day()
}
},
update(cache) {
@@ -47,7 +47,7 @@ export function TimeTicketsCommit({
return {
...ticket,
commited_by: currentUser.email,
committed_at: dayjs()
committed_at: day()
};
}
return ticket;

View File

@@ -1,7 +1,7 @@
import { useApolloClient } from "@apollo/client";
import { Button, notification } from "antd";
import _ from "lodash";
import dayjs from "../../utils/day";
import day from "../../utils/day";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -49,7 +49,7 @@ export function TtApproveButton({
})),
approvalIds: selectedTickets,
approvalUpdate: {
approved_at: dayjs(),
approved_at: day(),
approved_by: currentUser.email
}
}

View File

@@ -1,18 +0,0 @@
import { connect } from "react-redux";
import { GlobalOutlined } from "@ant-design/icons";
import { createStructuredSelector } from "reselect";
import React from "react";
import { selectWssStatus } from "../../redux/application/application.selectors";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
wssStatus: selectWssStatus
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(WssStatusDisplay);
export function WssStatusDisplay({ wssStatus }) {
console.log("🚀 ~ WssStatusDisplay ~ wssStatus:", wssStatus);
return <GlobalOutlined style={{ color: wssStatus === "connected" ? "green" : "red", marginRight: ".5rem" }} />;
}

View File

@@ -1,88 +1,54 @@
import { useEffect, useState, useRef } from "react";
import { useEffect, useState } from "react";
import SocketIO from "socket.io-client";
import { auth } from "../../firebase/firebase.utils";
import { store } from "../../redux/store";
import { setWssStatus } from "../../redux/application/application.actions";
const useSocket = (bodyshop) => {
const socketRef = useRef(null);
const [clientId, setClientId] = useState(null);
const [socket, setSocket] = useState(null);
const [clientId, setClientId] = useState(null); // State to store unique identifier
useEffect(() => {
const unsubscribe = auth.onIdTokenChanged(async (user) => {
if (user) {
const newToken = await user.getIdToken();
if (bodyshop && bodyshop.id) {
const endpoint = import.meta.env.PROD ? import.meta.env.VITE_APP_AXIOS_BASE_API_URL : "https://localhost:3000";
if (socketRef.current) {
// Send new token to server
socketRef.current.emit("update-token", newToken);
} else if (bodyshop && bodyshop.id) {
// Initialize the socket
const endpoint = import.meta.env.PROD ? import.meta.env.VITE_APP_AXIOS_BASE_API_URL : "";
const socketInstance = SocketIO(endpoint, {
path: "/ws", // Ensure this matches the Vite proxy and backend path
withCredentials: true,
auth: async (callback) => {
const token = auth.currentUser && (await auth.currentUser.getIdToken());
callback({ token });
},
reconnectionAttempts: Infinity, // Try reconnecting forever
reconnectionDelay: 2000, // How long to wait between reconnection attempts
reconnectionDelayMax: 10000 // Maximum delay between attempts
});
const socketInstance = SocketIO(endpoint, {
path: "/wss",
withCredentials: true,
auth: { token: newToken },
reconnectionAttempts: Infinity,
reconnectionDelay: 2000,
reconnectionDelayMax: 10000
});
setSocket(socketInstance);
socketRef.current = socketInstance;
socketInstance.on("connect", () => {
console.log("Socket connected:", socketInstance.id);
setClientId(socketInstance.id);
});
const handleBodyshopMessage = (message) => {
if (!import.meta.env.DEV) return;
console.log(`Received message for bodyshop ${bodyshop.id}:`, message);
};
socketInstance.on("reconnect", (attempt) => {
console.log(`Socket reconnected after ${attempt} attempts`);
});
const handleConnect = () => {
console.log("Socket connected:", socketInstance.id);
socketInstance.emit("join-bodyshop-room", bodyshop.id);
setClientId(socketInstance.id);
store.dispatch(setWssStatus("connected"))
};
socketInstance.on("connect_error", (err) => {
console.error("Socket connection error:", err);
});
const handleReconnect = (attempt) => {
console.log(`Socket reconnected after ${attempt} attempts`);
store.dispatch(setWssStatus("connected"))
};
socketInstance.on("disconnect", () => {
console.log("Socket disconnected");
});
const handleConnectionError = (err) => {
console.error("Socket connection error:", err);
store.dispatch(setWssStatus("error"))
};
const handleDisconnect = () => {
console.log("Socket disconnected");
store.dispatch(setWssStatus("disconnected"))
};
socketInstance.on("connect", handleConnect);
socketInstance.on("reconnect", handleReconnect);
socketInstance.on("connect_error", handleConnectionError);
socketInstance.on("disconnect", handleDisconnect);
socketInstance.on("bodyshop-message", handleBodyshopMessage);
}
} else {
// User is not authenticated
if (socketRef.current) {
socketRef.current.disconnect();
socketRef.current = null;
}
}
});
// Clean up the listener on unmount
return () => {
unsubscribe();
if (socketRef.current) {
socketRef.current.disconnect();
socketRef.current = null;
}
};
return () => {
socketInstance.disconnect();
};
}
}, [bodyshop]);
return { socket: socketRef.current, clientId };
// Return both socket and clientId
return { socket, clientId };
};
export default useSocket;

View File

@@ -48,7 +48,6 @@ export const QUERY_ALL_ACTIVE_APPOINTMENTS = gql`
v_model_desc
est_ct_fn
est_ct_ln
comment
labhrs: joblines_aggregate(where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }) {
aggregate {
sum {

View File

@@ -1,20 +1,16 @@
import { Button, Card, Col, notification, Row, Select, Space } from "antd";
import React, { useEffect, useRef, useState } from "react";
import React, { useContext, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useLocation, useNavigate } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import SocketIO from "socket.io-client";
import DmsAllocationsSummaryApComponent from "../../components/dms-allocations-summary-ap/dms-allocations-summary-ap.component";
import DmsLogEvents from "../../components/dms-log-events/dms-log-events.component";
import { auth } from "../../firebase/firebase.utils";
import { setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import SocketContext from "../../contexts/SocketIO/socketContext";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
const mapStateToProps = createStructuredSelector({});
const mapDispatchToProps = (dispatch) => ({
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
@@ -23,17 +19,9 @@ const mapDispatchToProps = (dispatch) => ({
export default connect(mapStateToProps, mapDispatchToProps)(DmsContainer);
export const socket = SocketIO(import.meta.env.PROD ? import.meta.env.VITE_APP_AXIOS_BASE_API_URL : "", {
path: "/ws",
withCredentials: true,
auth: async (callback) => {
const token = auth.currentUser && (await auth.currentUser.getIdToken());
callback({ token });
}
});
export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
export function DmsContainer({ setBreadcrumbs, setSelectedHeader }) {
const { t } = useTranslation();
const { socket } = useContext(SocketContext);
const [logLevel, setLogLevel] = useState("DEBUG");
const history = useNavigate();
const [logs, setLogs] = useState([]);
@@ -64,40 +52,43 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
}, [t, setBreadcrumbs, setSelectedHeader]);
useEffect(() => {
socket.on("connect", () => socket.emit("set-log-level", logLevel));
socket.on("reconnect", () => {
setLogs((logs) => {
return [
if (socket) {
const handleConnect = () => socket.emit("set-log-level", logLevel);
const handleReconnect = () => {
setLogs((logs) => [
...logs,
{
timestamp: new Date(),
level: "WARNING",
message: "Reconnected to CDK Export Service"
message: "Reconnected to DMS Export Service"
}
];
});
});
]);
};
const handleLogEvent = (payload) => {
setLogs((logs) => [...logs, payload]);
};
const handleExportComplete = () => {
notification.open({
type: "success",
message: t("jobs.labels.dms.apexported")
});
};
socket.on("log-event", (payload) => {
setLogs((logs) => {
return [...logs, payload];
});
});
socket.on("connect", handleConnect);
socket.on("reconnect", handleReconnect);
socket.on("log-event", handleLogEvent);
socket.on("ap-export-complete", handleExportComplete);
socket.on("ap-export-complete", (payload) => {
notification.open({
type: "success",
message: t("jobs.labels.dms.apexported")
});
});
if (socket.disconnected) socket.connect();
if (socket.disconnected) socket.connect();
return () => {
socket.removeAllListeners();
socket.disconnect();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return () => {
socket.off("connect", handleConnect);
socket.off("reconnect", handleReconnect);
socket.off("log-event", handleLogEvent);
socket.off("ap-export-complete", handleExportComplete);
};
}
}, [socket, logLevel, t]);
if (!state?.billids) {
history(`/manage/accounting/payables`);
@@ -123,6 +114,7 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
socket.emit("set-log-level", value);
}}
>
<Select.Option key="TRACE">TRACE</Select.Option>
<Select.Option key="DEBUG">DEBUG</Select.Option>
<Select.Option key="INFO">INFO</Select.Option>
<Select.Option key="WARNING">WARNING</Select.Option>
@@ -132,27 +124,20 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
<Button
onClick={() => {
setLogs([]);
socket.disconnect();
socket.connect();
if (socket) {
socket.emit("clear-dms-session");
}
}}
>
Reconnect
Clear Session
</Button>
</Space>
}
>
<DmsLogEvents socket={socket} logs={logs} />
<DmsLogEvents logs={logs} />
</Card>
</div>
</Col>
</Row>
);
}
export const determineDmsType = (bodyshop) => {
if (bodyshop.cdk_dealerid) return "cdk";
else {
return "pbs";
}
};

View File

@@ -1,12 +1,11 @@
import { useQuery } from "@apollo/client";
import { Button, Card, Col, notification, Result, Row, Select, Space } from "antd";
import queryString from "query-string";
import React, { useEffect, useRef, useState } from "react";
import React, { useContext, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link, useLocation, useNavigate } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import SocketIO from "socket.io-client";
import AlertComponent from "../../components/alert/alert.component";
import DmsAllocationsSummary from "../../components/dms-allocations-summary/dms-allocations-summary.component";
import DmsCustomerSelector from "../../components/dms-customer-selector/dms-customer-selector.component";
@@ -14,12 +13,12 @@ import DmsLogEvents from "../../components/dms-log-events/dms-log-events.compone
import DmsPostForm from "../../components/dms-post-form/dms-post-form.component";
import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component";
import { OwnerNameDisplayFunction } from "../../components/owner-name-display/owner-name-display.component";
import { auth } from "../../firebase/firebase.utils";
import { QUERY_JOB_EXPORT_DMS } from "../../graphql/jobs.queries";
import { insertAuditTrail, setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import SocketContext from "../../contexts/SocketIO/socketContext";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
@@ -28,25 +27,21 @@ const mapStateToProps = createStructuredSelector({
const mapDispatchToProps = (dispatch) => ({
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
setSelectedHeader: (key) => dispatch(setSelectedHeader(key)),
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(
insertAuditTrail({
jobid,
operation,
type
})
)
});
export default connect(mapStateToProps, mapDispatchToProps)(DmsContainer);
export const socket = SocketIO(
import.meta.env.PROD ? import.meta.env.VITE_APP_AXIOS_BASE_API_URL : "", // for dev testing,
{
path: "/ws",
withCredentials: true,
auth: async (callback) => {
const token = auth.currentUser && (await auth.currentUser.getIdToken());
callback({ token });
}
}
);
export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, insertAuditTrail }) {
const { t } = useTranslation();
const { socket } = useContext(SocketContext);
const [logLevel, setLogLevel] = useState("DEBUG");
const history = useNavigate();
const [logs, setLogs] = useState([]);
@@ -59,6 +54,7 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
fetchPolicy: "network-only",
nextFetchPolicy: "network-only"
});
const logsRef = useRef(null);
useEffect(() => {
@@ -83,47 +79,73 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
}, [t, setBreadcrumbs, setSelectedHeader]);
useEffect(() => {
socket.on("connect", () => socket.emit("set-log-level", logLevel));
socket.on("reconnect", () => {
setLogs((logs) => {
return [
if (socket) {
const handleConnect = () => {
socket.emit("set-log-level", logLevel);
};
const handleReconnect = () => {
setLogs((logs) => [
...logs,
{
timestamp: new Date(),
level: "WARNING",
message: "Reconnected to CDK Export Service"
message: "Reconnected to DMS Export Service"
}
];
});
});
socket.on("connect_error", (err) => {
console.log(`connect_error due to ${err}`, err);
notification.error({ message: err.message });
});
socket.on("log-event", (payload) => {
setLogs((logs) => {
return [...logs, payload];
});
});
socket.on("export-success", (payload) => {
notification.success({
message: t("jobs.successes.exported")
});
insertAuditTrail({
jobid: payload,
operation: AuditTrailMapping.jobexported(),
type: "jobexported"
});
history("/manage/accounting/receivables");
});
]);
};
if (socket.disconnected) socket.connect();
return () => {
socket.removeAllListeners();
socket.disconnect();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const handleConnectError = (err) => {
console.log(`connect_error due to ${err}`, err);
notification.error({ message: err.message });
};
const handleLogEvent = (payload) => {
setLogs((logs) => [...logs, payload]);
};
const handleExportSuccess = async (payload) => {
notification.success({
message: t("jobs.successes.exported")
});
insertAuditTrail({
jobid: payload,
operation: AuditTrailMapping.jobexported(),
type: "jobexported"
});
try {
await new Promise((resolve, reject) => {
socket.emit("clear-dms-session", (response) => {
if (response && response.status === "ok") {
resolve();
} else {
reject(new Error("Failed to clear DMS session"));
}
});
});
} catch (error) {
console.error("Failed to clear DMS session", error);
}
history("/manage/accounting/receivables");
};
socket.on("connect", handleConnect);
socket.on("reconnect", handleReconnect);
socket.on("connect_error", handleConnectError);
socket.on("log-event", handleLogEvent);
socket.on("export-success", handleExportSuccess);
return () => {
socket.off("connect", handleConnect);
socket.off("reconnect", handleReconnect);
socket.off("connect_error", handleConnectError);
socket.off("log-event", handleLogEvent);
socket.off("export-success", handleExportSuccess);
};
}
}, [socket, logLevel, t, insertAuditTrail, history]);
if (loading) return <LoadingSpinner />;
if (error) return <AlertComponent message={error.message} type="error" />;
@@ -173,6 +195,7 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
socket.emit("set-log-level", value);
}}
>
<Select.Option key="TRACE">TRACE</Select.Option>
<Select.Option key="DEBUG">DEBUG</Select.Option>
<Select.Option key="INFO">INFO</Select.Option>
<Select.Option key="WARNING">WARNING</Select.Option>
@@ -182,16 +205,17 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
<Button
onClick={() => {
setLogs([]);
socket.disconnect();
socket.connect();
if (socket) {
socket.emit("clear-dms-session");
}
}}
>
Reconnect
Clear Session
</Button>
</Space>
}
>
<DmsLogEvents socket={socket} logs={logs} />
<DmsLogEvents logs={logs} />
</Card>
</div>
</Col>

View File

@@ -18,14 +18,13 @@ import LoadingSpinner from "../../components/loading-spinner/loading-spinner.com
import PartnerPingComponent from "../../components/partner-ping/partner-ping.component";
import PrintCenterModalContainer from "../../components/print-center-modal/print-center-modal.container";
import ShopSubStatusComponent from "../../components/shop-sub-status/shop-sub-status.component";
import { requestForToken } from "../../firebase/firebase.utils";
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
import { selectBodyshop, selectInstanceConflict } from "../../redux/user/user.selectors";
import UpdateAlert from "../../components/update-alert/update-alert.component";
import InstanceRenderManager from "../../utils/instanceRenderMgr.js";
import "./manage.page.styles.scss";
import WssStatusDisplayComponent from "../../components/wss-status-display/wss-status-display.component.jsx";
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
import { requestForToken } from "../../firebase/firebase.utils.js";
const JobsPage = lazy(() => import("../jobs/jobs.page"));
@@ -133,6 +132,27 @@ export function Manage({ conflict, bodyshop }) {
});
}, [t]);
useEffect(() => {
if (socket && bodyshop && bodyshop.id) {
const handleConnect = () => {
socket.emit("join-bodyshop-room", bodyshop.id);
};
const handleBodyshopMessage = (message) => {
console.log(`Received message for bodyshop ${bodyshop.id}:`, message);
};
socket.on("connect", handleConnect);
socket.on("bodyshop-message", handleBodyshopMessage);
return () => {
socket.emit("leave-bodyshop-room", bodyshop.id);
socket.off("connect", handleConnect);
socket.off("bodyshop-message", handleBodyshopMessage);
};
}
}, [socket, bodyshop]);
const AppRouteTable = (
<Suspense
fallback={
@@ -605,8 +625,7 @@ export function Manage({ conflict, bodyshop }) {
}}
>
<div style={{ display: "flex" }}>
<WssStatusDisplayComponent />
<div onClick={broadcastMessage}>
<div>
{`${InstanceRenderManager({
imex: t("titles.imexonline"),
rome: t("titles.romeonline"),
@@ -615,6 +634,8 @@ export function Manage({ conflict, bodyshop }) {
</div>
<div id="noticeable-widget" style={{ marginLeft: "1rem" }} />
</div>
<button onClick={broadcastMessage}>Broadcast Message</button>
<Link to="/disclaimer" target="_blank" style={{ color: "#ccc" }}>
Disclaimer & Notices
</Link>

View File

@@ -26,7 +26,7 @@ export function ProductionListComponent({ bodyshop }) {
return (
<>
<NoteUpsertModal />
<ProductionListTable bodyshop={bodyshop} subscriptionType={Production_Use_View.treatment} />
<ProductionListTable subscriptionType={Production_Use_View.treatment} />
</>
);
}

View File

@@ -67,7 +67,3 @@ export const setUpdateAvailable = (isUpdateAvailable) => ({
type: ApplicationActionTypes.SET_UPDATE_AVAILABLE,
payload: isUpdateAvailable
});
export const setWssStatus = (status) => ({
type: ApplicationActionTypes.SET_WSS_STATUS,
payload: status
});

View File

@@ -3,7 +3,6 @@ import ApplicationActionTypes from "./application.types";
const INITIAL_STATE = {
loading: false,
online: true,
wssStatus: "disconnected",
updateAvailable: false,
breadcrumbs: [],
recentItems: [],
@@ -88,9 +87,6 @@ const applicationReducer = (state = INITIAL_STATE, action) => {
case ApplicationActionTypes.SET_PROBLEM_JOBS: {
return { ...state, problemJobs: action.payload };
}
case ApplicationActionTypes.SET_WSS_STATUS: {
return { ...state, wssStatus: action.payload };
}
default:
return state;
}

View File

@@ -22,4 +22,3 @@ export const selectJobReadOnly = createSelector([selectApplication], (applicatio
export const selectOnline = createSelector([selectApplication], (application) => application.online);
export const selectProblemJobs = createSelector([selectApplication], (application) => application.problemJobs);
export const selectUpdateAvailable = createSelector([selectApplication], (application) => application.updateAvailable);
export const selectWssStatus = createSelector([selectApplication], (application) => application.wssStatus);

View File

@@ -12,7 +12,6 @@ const ApplicationActionTypes = {
SET_ONLINE_STATUS: "SET_ONLINE_STATUS",
INSERT_AUDIT_TRAIL: "INSERT_AUDIT_TRAIL",
SET_PROBLEM_JOBS: "SET_PROBLEM_JOBS",
SET_UPDATE_AVAILABLE: "SET_UPDATE_AVAILABLE",
SET_WSS_STATUS: "SET_WSS_STATUS"
SET_UPDATE_AVAILABLE: "SET_UPDATE_AVAILABLE"
};
export default ApplicationActionTypes;

View File

@@ -28,7 +28,7 @@ import {
} from "../../firebase/firebase.utils";
import { QUERY_EULA } from "../../graphql/bodyshop.queries";
import client from "../../utils/GraphQLClient";
import dayjs from "../../utils/day";
import day from "../../utils/day";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import {
checkInstanceId,
@@ -96,7 +96,7 @@ export function* isUserAuthenticated() {
const eulaQuery = yield client.query({
query: QUERY_EULA,
variables: {
now: dayjs()
now: day()
}
});
@@ -242,10 +242,6 @@ export function* signInSuccessSaga({ payload }) {
window.$crisp.push(["set", "user:nickname", [payload.displayName || payload.email]]);
window.$crisp.push(["set", "session:segments", [["user"]]]);
},
rome: () => {
window.$zoho.salesiq.visitor.name(payload.displayName || payload.email);
window.$zoho.salesiq.visitor.email(payload.email);
},
promanager: () => {
Userpilot.identify(payload.email, {
email: payload.email
@@ -314,7 +310,8 @@ export function* SetAuthLevelFromShopDetails({ payload }) {
try {
const userEmail = yield select((state) => state.user.currentUser.email);
try {
dayjs.tz.setDefault(payload.timezone);
//console.log("Setting shop timezone.");
// dayjs.tz.setDefault(payload.timezone);
} catch (error) {
console.log(error);
}
@@ -374,9 +371,6 @@ export function* SetAuthLevelFromShopDetails({ payload }) {
if (authRecord[0] && authRecord[0].user.validemail) {
window.$crisp.push(["set", "user:email", [authRecord[0].user.email]]);
}
},
rome: () => {
window.$zoho.salesiq.visitor.info({ "Shop Name": payload.shopname });
}
});
} catch (error) {

View File

@@ -119,7 +119,7 @@
"jobclosedwithbypass": "Job was invoiced using the master bypass password. ",
"jobconverted": "Job converted and assigned number {{ro_number}}.",
"jobdelivery": "Job intake completed. Status set to {{status}}. Actual completion is {{actual_completion}}.",
"jobexported": "Job has been exported",
"jobexported": "",
"jobfieldchanged": "Job field $t(jobs.fields.{{field}}) changed to {{value}}.",
"jobimported": "Job imported.",
"jobinproductionchange": "Job production status set to {{inproduction}}",
@@ -2849,21 +2849,15 @@
"jobs_in_production": "Jobs in Production",
"tasks_in_production": "Tasks in Production",
"tasks_on_board": "Tasks on Board",
"tasks_in_view": "Tasks in View",
"total_amount_in_production": "Dollars in Production",
"total_amount_on_board": "Dollars on Board",
"total_amount_in_view": "Dollars in View",
"total_hours_in_production": "Hours in Production",
"total_hours_on_board": "Hours on Board",
"total_hours_in_view": "Hours in View",
"total_jobs_on_board": "Jobs on Board",
"total_jobs_in_view": "Jobs in View",
"total_lab_in_production": "Body Hours in Production",
"total_lab_on_board": "Body Hours on Board",
"total_lab_in_view": "Body Hours in View",
"total_lar_in_production": "Refinish Hours in Production",
"total_lar_on_board": "Refinish Hours on Board",
"total_lar_in_view": "Refinish Hours in View"
"total_lar_on_board": "Refinish Hours on Board"
},
"statistics_title": "Statistics"
},
@@ -2875,21 +2869,15 @@
"tasks": "Tasks",
"tasks_in_production": "Tasks in Production",
"tasks_on_board": "Tasks on Board",
"tasks_in_view": "Tasks in View",
"total_amount_in_production": "Dollars in Production",
"total_amount_on_board": "Dollars on Board",
"total_amount_in_view": "Dollars in View",
"total_hours_in_production": "Hours in Production",
"total_hours_on_board": "Hours on Board",
"total_hours_in_view": "Hours in View",
"total_jobs_on_board": "Jobs on Board",
"total_jobs_in_view": "Jobs in View",
"total_lab_in_production": "Body Hours in Production",
"total_lab_on_board": "Body Hours on Board",
"total_lab_in_view": "Body Hours in View",
"total_lar_in_production": "Refinish Hours in Production",
"total_lar_on_board": "Refinish Hours on Board",
"total_lar_in_view": "Refinish Hours in View"
"total_lar_on_board": "Refinish Hours on Board"
},
"successes": {
"removed": "Job removed from production."
@@ -3044,7 +3032,6 @@
"production_by_target_date": "Production by Target Date",
"production_by_technician": "Production by Technician",
"production_by_technician_one": "Production filtered by Technician",
"production_not_production_status": "Production not in Production Status",
"production_over_time": "Production Level over Time",
"psr_by_make": "Percent of Sales by Vehicle Make",
"purchase_return_ratio_grouped_by_vendor_detail": "Purchase & Return Ratio by Vendor (Detail)",
@@ -3187,7 +3174,6 @@
"billid": "Bill",
"completed": "Completed",
"created_at": "Created At",
"created_by": "Created By",
"description": "Description",
"due_date": "Due Date",
"job": {

View File

@@ -2849,21 +2849,15 @@
"jobs_in_production": "",
"tasks_in_production": "",
"tasks_on_board": "",
"tasks_in_view": "",
"total_amount_in_production": "",
"total_amount_on_board": "",
"total_amount_in_view": "",
"total_hours_in_production": "",
"total_hours_on_board": "",
"total_hours_in_view": "",
"total_jobs_on_board": "",
"total_jobs_in_view": "",
"total_lab_in_production": "",
"total_lab_on_board": "",
"total_lab_in_view": "",
"total_lar_in_production": "",
"total_lar_on_board": "",
"total_lar_in_view": ""
"total_lar_on_board": ""
},
"statistics_title": ""
},
@@ -2875,21 +2869,15 @@
"tasks": "",
"tasks_in_production": "",
"tasks_on_board": "",
"tasks_in_view": "",
"total_amount_in_production": "",
"total_amount_on_board": "",
"total_amount_in_view": "",
"total_hours_in_production": "",
"total_hours_on_board": "",
"total_hours_in_view": "",
"total_jobs_on_board": "",
"total_jobs_in_view": "",
"total_lab_in_production": "",
"total_lab_on_board": "",
"total_lab_in_view": "",
"total_lar_in_production": "",
"total_lar_on_board": "",
"total_lar_in_view": ""
"total_lar_on_board": ""
},
"successes": {
"removed": ""
@@ -3044,7 +3032,6 @@
"production_by_target_date": "",
"production_by_technician": "",
"production_by_technician_one": "",
"production_not_production_status": "",
"production_over_time": "",
"psr_by_make": "",
"purchase_return_ratio_grouped_by_vendor_detail": "",
@@ -3187,7 +3174,6 @@
"billid": "",
"completed": "",
"created_at": "",
"created_by": "",
"description": "",
"due_date": "",
"job": {

File diff suppressed because it is too large Load Diff

View File

@@ -2333,14 +2333,6 @@ export const TemplateList = (type, context) => {
key: "production_by_technician",
//idtype: "vendor",
disabled: false
},
production_not_production_status: {
title: i18n.t("reportcenter.templates.production_not_production_status"),
description: "",
subject: i18n.t("reportcenter.templates.production_not_production_status"),
key: "production_not_production_status",
//idtype: "vendor",
disabled: false
}
}
: {}),

View File

@@ -1,4 +1,3 @@
// Sometimes referred to as PageSize, this variable controls the amount of records
// to show on one page during pagination.
export const pageLimit = 50;
export const exportPageLimit = 10;

View File

@@ -1,6 +1,5 @@
import dayjs from "dayjs";
import "dayjs/locale/en";
import dayjsBusinessDays from "dayjs-business-days2";
import isSameOrAfter from "dayjs/plugin/isSameOrAfter";
import updateLocale from "dayjs/plugin/updateLocale";
@@ -65,6 +64,4 @@ dayjs.extend(minMax);
dayjs.extend(isBetween);
dayjs.extend(dayjsBusinessDays);
dayjs.locale("en");
export default dayjs;

View File

@@ -2,5 +2,5 @@ import { store } from "../redux/store";
export function CreateExplorerLinkForJob({ jobid }) {
const bodyshop = store.getState().user.bodyshop;
return `imexmedia://`.concat(encodeURIComponent(`${bodyshop.localmediaservernetwork}\\Jobs\\${jobid}`));
return `imexmedia://${bodyshop.localmediaservernetwork}\\Jobs\\${jobid}`;
}

View File

@@ -9,7 +9,6 @@ import eslint from "vite-plugin-eslint";
import { VitePWA } from "vite-plugin-pwa";
import InstanceRenderManager from "./src/utils/instanceRenderMgr";
import chalk from "chalk";
//import { visualizer } from "rollup-plugin-visualizer";
process.env.VITE_APP_GIT_SHA_DATE = new Date().toLocaleString("en-US", {
timeZone: "America/Los_Angeles"
@@ -47,7 +46,6 @@ export const logger = createLogger("info", {
export default defineConfig({
base: "/",
plugins: [
//visualizer(),
ViteEjsPlugin((viteConfig) => ({ env: viteConfig.env })),
VitePWA({
injectRegister: "auto",
@@ -126,12 +124,6 @@ export default defineConfig({
secure: false,
ws: true
},
"/wss": {
target: "ws://localhost:4000",
rewriteWsOrigin: true,
secure: false,
ws: true
},
"/api": {
target: "http://localhost:4000",
changeOrigin: true,
@@ -168,12 +160,6 @@ export default defineConfig({
secure: false,
ws: true
},
"/wss": {
target: "ws://localhost:4000",
rewriteWsOrigin: true,
secure: false,
ws: true
},
"/api": {
target: "http://localhost:4000",
changeOrigin: true,
@@ -188,32 +174,7 @@ export default defineConfig({
manualChunks: {
antd: ["antd"],
"react-redux": ["react-redux"],
redux: ["redux"],
lodash: ["lodash"],
"@sentry/react": ["@sentry/react"],
"@splitsoftware/splitio-react": ["@splitsoftware/splitio-react"],
logrocket: ["logrocket"],
"firebase/app": ["firebase/app"],
"firebase/firestore": ["firebase/firestore"],
"firebase/firestore/lite": ["firebase/firestore/lite"],
"firebase/auth": ["firebase/auth"],
"firebase/functions": ["firebase/functions"],
"firebase/storage": ["firebase/storage"],
"firebase/database": ["firebase/database"],
"firebase/remote-config": ["firebase/remote-config"],
"firebase/performance": ["firebase/performance"],
"@firebase/app": ["@firebase/app"],
"@firebase/firestore": ["@firebase/firestore"],
"@firebase/firestore/lite": ["@firebase/firestore/lite"],
"@firebase/auth": ["@firebase/auth"],
"@firebase/functions": ["@firebase/functions"],
"@firebase/storage": ["@firebase/storage"],
"@firebase/database": ["@firebase/database"],
"@firebase/remote-config": ["@firebase/remote-config"],
"@firebase/performance": ["@firebase/performance"],
markerjs2: ["markerjs2"],
"@apollo/client": ["@apollo/client"],
"libphonenumber-js": ["libphonenumber-js"]
redux: ["redux"]
}
}
}
@@ -223,8 +184,6 @@ export default defineConfig({
"react",
"react-dom",
"antd",
"lodash",
"@sentry/react",
"@apollo/client",
"@reduxjs/toolkit",
"axios",

View File

@@ -1,180 +0,0 @@
#############################
# Ports Exposed
# 4000 - Imex Node API
# 4556 - LocalStack (Local AWS)
# 3333 - SocketIO Admin-UI (Optional)
# 3334 - Redis-Insights (Optional)
#############################
services:
# Redis Node 1
redis-node-1:
build:
context: ./redis
container_name: redis-node-1
hostname: redis-node-1
restart: unless-stopped
networks:
- redis-cluster-net
volumes:
- redis-node-1-data:/data
- redis-lock:/redis-lock
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 10
# Redis Node 2
redis-node-2:
build:
context: ./redis
container_name: redis-node-2
hostname: redis-node-2
restart: unless-stopped
networks:
- redis-cluster-net
volumes:
- redis-node-2-data:/data
- redis-lock:/redis-lock
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 10
# Redis Node 3
redis-node-3:
build:
context: ./redis
container_name: redis-node-3
hostname: redis-node-3
restart: unless-stopped
networks:
- redis-cluster-net
volumes:
- redis-node-3-data:/data
- redis-lock:/redis-lock
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 10
# LocalStack: Used to emulate AWS services locally, currently setup for SES
# Notes: Set the ENV Debug to 1 for additional logging
localstack:
image: localstack/localstack
container_name: localstack
hostname: localstack
networks:
- redis-cluster-net
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- SERVICES=ses,secretsmanager,cloudwatch,logs
- DEBUG=0
- AWS_ACCESS_KEY_ID=test
- AWS_SECRET_ACCESS_KEY=test
- AWS_DEFAULT_REGION=ca-central-1
- EXTRA_CORS_ALLOWED_HEADERS=Authorization,Content-Type
- EXTRA_CORS_ALLOWED_ORIGINS=*
- EXTRA_CORS_EXPOSE_HEADERS=Authorization,Content-Type
ports:
- "4566:4566"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:4566/_localstack/health"]
interval: 10s
timeout: 5s
retries: 5
start_period: 20s
# AWS-CLI - Used in conjunction with LocalStack to set required permission to send emails
aws-cli:
image: amazon/aws-cli
container_name: aws-cli
hostname: aws-cli
networks:
- redis-cluster-net
depends_on:
localstack:
condition: service_healthy
volumes:
- './localstack:/tmp/localstack'
- './certs:/tmp/certs'
environment:
- AWS_ACCESS_KEY_ID=test
- AWS_SECRET_ACCESS_KEY=test
- AWS_DEFAULT_REGION=ca-central-1
entrypoint: /bin/sh -c
command: >
"
aws --endpoint-url=http://localstack:4566 ses verify-domain-identity --domain imex.online --region ca-central-1
aws --endpoint-url=http://localstack:4566 ses verify-email-identity --email-address noreply@imex.online --region ca-central-1
aws --endpoint-url=http://localstack:4566 secretsmanager create-secret --name CHATTER_PRIVATE_KEY --secret-string file:///tmp/certs/id_rsa
aws --endpoint-url=http://localstack:4566 logs create-log-group --log-group-name development --region ca-central-1
"
# Node App: The Main IMEX API
node-app:
build:
context: .
container_name: node-app
hostname: imex-api
networks:
- redis-cluster-net
env_file:
- .env.development
depends_on:
redis-node-1:
condition: service_healthy
redis-node-2:
condition: service_healthy
redis-node-3:
condition: service_healthy
localstack:
condition: service_healthy
aws-cli:
condition: service_completed_successfully
ports:
- "4000:4000"
- "9229:9229"
volumes:
- .:/app
- node-app-npm-cache:/app/node_modules
# ## Optional Container to Observe SocketIO data
# socketio-admin-ui:
# image: maitrungduc1410/socket.io-admin-ui
# container_name: socketio-admin-ui
# networks:
# - redis-cluster-net
# ports:
# - "3333:80"
# ##Optional Container to Observe Redis Cluster Data
# redis-insight:
# image: redislabs/redisinsight:latest
# container_name: redis-insight
# hostname: redis-insight
# restart: unless-stopped
# ports:
# - "3334:5540"
# networks:
# - redis-cluster-net
# volumes:
# - redis-insight-data:/db
networks:
redis-cluster-net:
driver: bridge
volumes:
node-app-npm-cache:
redis-node-1-data:
redis-node-2-data:
redis-node-3-data:
redis-lock:
redis-insight-data:

View File

@@ -1,35 +0,0 @@
{
"watch": [
"server.js",
"server"
],
"ignore": [
"client",
".circleci",
".platform",
".idea",
".vscode",
"_reference",
"firebase",
"hasura",
"logs",
"redis",
".dockerignore",
".ebignore",
".editorconfig",
".env.development",
".env.development.rome",
".eslintrc.json",
".gitignore",
".npmmrc",
".prettierrc.js",
"bodyshop_translations.babel",
"Dockerfile",
"ecosystem.config.js",
"job-totals-testing-util.js",
"nodemon.json",
"package.json",
"package-lock.json",
"setadmin.js"
]
}

2177
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -19,13 +19,10 @@
"makeitpretty": "prettier --write \"**/*.{css,js,json,jsx,scss}\""
},
"dependencies": {
"@aws-sdk/client-cloudwatch-logs": "^3.679.0",
"@aws-sdk/client-elasticache": "^3.675.0",
"@aws-sdk/client-secrets-manager": "^3.675.0",
"@aws-sdk/client-ses": "^3.675.0",
"@aws-sdk/credential-provider-node": "^3.675.0",
"@aws-sdk/client-secrets-manager": "^3.654.0",
"@aws-sdk/client-ses": "^3.654.0",
"@aws-sdk/credential-provider-node": "^3.654.0",
"@opensearch-project/opensearch": "^2.12.0",
"@socket.io/admin-ui": "^0.5.1",
"@socket.io/redis-adapter": "^8.3.0",
"aws4": "^1.13.2",
"axios": "^1.7.7",
@@ -33,41 +30,39 @@
"bluebird": "^3.7.2",
"body-parser": "^1.20.3",
"canvas": "^2.11.2",
"chart.js": "^4.4.5",
"cloudinary": "^2.5.1",
"chart.js": "^4.4.4",
"cloudinary": "^2.5.0",
"compression": "^1.7.4",
"cookie-parser": "^1.4.7",
"cookie-parser": "^1.4.6",
"cors": "2.8.5",
"csrf": "^3.1.0",
"dinero.js": "^1.9.1",
"dotenv": "^16.4.5",
"express": "^4.21.1",
"firebase-admin": "^12.6.0",
"express": "^4.21.0",
"firebase-admin": "^12.5.0",
"graphql": "^16.9.0",
"graphql-request": "^6.1.0",
"graylog2": "^0.2.1",
"inline-css": "^4.0.2",
"intuit-oauth": "^4.1.2",
"ioredis": "^5.4.1",
"json-2-csv": "^5.5.6",
"json-2-csv": "^5.5.5",
"lodash": "^4.17.21",
"moment": "^2.30.1",
"moment-timezone": "^0.5.46",
"moment-timezone": "^0.5.45",
"multer": "^1.4.5-lts.1",
"node-mailjet": "^6.0.6",
"node-persist": "^4.0.3",
"nodemailer": "^6.9.15",
"phone": "^3.1.51",
"phone": "^3.1.50",
"recursive-diff": "^1.0.9",
"redis": "^4.7.0",
"rimraf": "^6.0.1",
"soap": "^1.1.5",
"soap": "^1.1.4",
"socket.io": "^4.8.0",
"socket.io-adapter": "^2.5.5",
"ssh2-sftp-client": "^10.0.3",
"twilio": "^4.23.0",
"uuid": "^10.0.0",
"winston": "^3.15.0",
"winston-cloudwatch": "^6.3.0",
"xml2js": "^0.6.2",
"xmlbuilder2": "^3.1.1"
},

View File

@@ -1,20 +0,0 @@
# Use the official Redis image
FROM redis:7.0-alpine
# Copy the Redis configuration file
COPY redis.conf /usr/local/etc/redis/redis.conf
# Copy the entrypoint script
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
# Make the entrypoint script executable
RUN chmod +x /usr/local/bin/entrypoint.sh
# Debugging step: List contents of /usr/local/bin
RUN ls -l /usr/local/bin
# Expose Redis ports
EXPOSE 6379 16379
# Set the entrypoint
ENTRYPOINT ["entrypoint.sh"]

View File

@@ -1,36 +0,0 @@
#!/bin/sh
LOCKFILE="/redis-lock/redis-cluster-init.lock"
# Start Redis server in the background
redis-server /usr/local/etc/redis/redis.conf &
# Wait for Redis server to start
sleep 5
# Only initialize the cluster if the lock file doesn't exist
if [ ! -f "$LOCKFILE" ]; then
echo "Initializing Redis Cluster..."
# Create lock file to prevent further initialization attempts
touch "$LOCKFILE"
if [ $? -eq 0 ]; then
echo "Lock file created successfully at $LOCKFILE."
else
echo "Failed to create lock file."
fi
# Initialize the Redis cluster
yes yes | redis-cli --cluster create \
redis-node-1:6379 \
redis-node-2:6379 \
redis-node-3:6379 \
--cluster-replicas 0
echo "Redis Cluster initialized."
else
echo "Cluster already initialized, skipping initialization."
fi
# Keep the container running
tail -f /dev/null

View File

@@ -1,6 +0,0 @@
bind 0.0.0.0
port 6379
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
appendonly yes

392
server.js
View File

@@ -1,32 +1,20 @@
// Load environment variables THIS MUST BE AT THE TOP
const express = require("express");
const cors = require("cors");
const bodyParser = require("body-parser");
const path = require("path");
const compression = require("compression");
const cookieParser = require("cookie-parser");
const http = require("http");
const { Server } = require("socket.io");
const { createClient } = require("redis");
const { createAdapter } = require("@socket.io/redis-adapter");
const logger = require("./server/utils/logger");
// Load environment variables
require("dotenv").config({
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`)
});
const cors = require("cors");
const http = require("http");
const Redis = require("ioredis");
const express = require("express");
const bodyParser = require("body-parser");
const compression = require("compression");
const cookieParser = require("cookie-parser");
const { Server } = require("socket.io");
const { createAdapter } = require("@socket.io/redis-adapter");
const { instrument } = require("@socket.io/admin-ui");
const { isString, isEmpty } = require("lodash");
const logger = require("./server/utils/logger");
const { applyRedisHelpers } = require("./server/utils/redisHelpers");
const { applyIOHelpers } = require("./server/utils/ioHelpers");
const { redisSocketEvents } = require("./server/web-sockets/redisSocketEvents");
const { ElastiCacheClient, DescribeCacheClustersCommand } = require("@aws-sdk/client-elasticache");
const { default: InstanceManager } = require("./server/utils/instanceMgr");
const CLUSTER_RETRY_BASE_DELAY = 100;
const CLUSTER_RETRY_MAX_DELAY = 5000;
const CLUSTER_RETRY_JITTER = 100;
/**
* CORS Origin for Socket.IO
* @type {string[][]}
@@ -35,13 +23,12 @@ const SOCKETIO_CORS_ORIGIN = [
"https://test.imex.online",
"https://www.test.imex.online",
"http://localhost:3000",
"https://localhost:3000",
"https://imex.online",
"https://www.imex.online",
"https://romeonline.io",
"https://www.romeonline.io",
"https://test.romeonline.io",
"https://www.test.romeonline.io",
"https://beta.test.romeonline.io",
"https://www.beta.test.romeonline.io",
"https://beta.romeonline.io",
"https://www.beta.romeonline.io",
"https://beta.test.imex.online",
@@ -51,20 +38,16 @@ const SOCKETIO_CORS_ORIGIN = [
"https://www.test.promanager.web-est.com",
"https://test.promanager.web-est.com",
"https://www.promanager.web-est.com",
"https://promanager.web-est.com",
"https://www.promanager.web-est.com",
"https://old.imex.online",
"https://www.old.imex.online",
"https://wsadmin.imex.online",
"https://www.wsadmin.imex.online"
"https://www.old.imex.online"
];
const SOCKETIO_CORS_ORIGIN_DEV = ["http://localhost:3333", "https://localhost:3333"];
/**
* Middleware for Express app
* @param app
*/
const applyMiddleware = ({ app }) => {
const applyMiddleware = (app) => {
app.use(compression());
app.use(cookieParser());
app.use(bodyParser.json({ limit: "50mb" }));
@@ -82,7 +65,7 @@ const applyMiddleware = ({ app }) => {
* Route groupings for Express app
* @param app
*/
const applyRoutes = ({ app }) => {
const applyRoutes = (app) => {
app.use("/", require("./server/routes/miscellaneousRoutes"));
app.use("/notifications", require("./server/routes/notificationsRoutes"));
app.use("/render", require("./server/routes/renderRoutes"));
@@ -108,151 +91,35 @@ const applyRoutes = ({ app }) => {
});
};
/**
* Fetch Redis nodes from AWS ElastiCache
* @returns {Promise<string[]>}
*/
const getRedisNodesFromAWS = async () => {
const client = new ElastiCacheClient({
region: InstanceManager({
imex: "ca-central-1",
rome: "us-east-2"
})
});
const params = {
ReplicationGroupId: process.env.REDIS_CLUSTER_ID,
ShowCacheNodeInfo: true
};
try {
// Fetch the cache clusters associated with the replication group
const command = new DescribeCacheClustersCommand(params);
const response = await client.send(command);
const cacheClusters = response.CacheClusters;
return cacheClusters.flatMap((cluster) =>
cluster.CacheNodes.map((node) => `${node.Endpoint.Address}:${node.Endpoint.Port}`)
);
} catch (err) {
logger.log(`Error fetching Redis nodes from AWS: ${err.message}`, "ERROR", "redis", "api");
throw err;
}
};
/**
* Connect to Redis Cluster
* @returns {Promise<unknown>}
*/
const connectToRedisCluster = async () => {
let redisServers;
if (isString(process.env?.REDIS_CLUSTER_ID) && !isEmpty(process.env?.REDIS_CLUSTER_ID)) {
// Fetch Redis nodes from AWS if AWS environment variables are present
redisServers = await getRedisNodesFromAWS();
} else {
// Use the Dockerized Redis cluster in development
if (isEmpty(process.env?.REDIS_URL) || !isString(process.env?.REDIS_URL)) {
logger.log(`No or Malformed REDIS_URL present.`, "ERROR", "redis", "api");
process.exit(1);
}
try {
redisServers = JSON.parse(process.env.REDIS_URL);
} catch (error) {
logger.log(`Failed to parse REDIS_URL: ${error.message}. Exiting...`, "ERROR", "redis", "api");
process.exit(1);
}
}
const clusterRetryStrategy = (times) => {
const delay =
Math.min(CLUSTER_RETRY_BASE_DELAY + times * 50, CLUSTER_RETRY_MAX_DELAY) + Math.random() * CLUSTER_RETRY_JITTER;
logger.log(`Redis cluster not yet ready. Retrying in ${delay.toFixed(2)}ms`, "WARN", "redis", "api");
return delay;
};
const redisCluster = new Redis.Cluster(redisServers, {
clusterRetryStrategy,
enableAutoPipelining: true,
enableReadyCheck: true,
redisOptions: {
// connectTimeout: 10000, // Timeout for connecting in ms
// idleTimeoutMillis: 30000, // Close idle connections after 30s
// maxRetriesPerRequest: 5 // Retry a maximum of 5 times per request
}
});
return new Promise((resolve, reject) => {
redisCluster.on("ready", () => {
logger.log(`Redis cluster connection established.`, "INFO", "redis", "api");
resolve(redisCluster);
});
redisCluster.on("error", (err) => {
logger.log(`Redis cluster connection failed: ${err.message}`, "ERROR", "redis", "api");
reject(err);
});
});
};
/**
* Apply Redis to the server
* @param server
* @param app
*/
const applySocketIO = async ({ server, app }) => {
const redisCluster = await connectToRedisCluster();
// Handle errors
redisCluster.on("error", (err) => {
logger.log(`Redis ERROR`, "ERROR", "redis", "api");
});
const pubClient = redisCluster;
const applySocketIO = async (server, app) => {
// Redis client setup for Pub/Sub and Key-Value Store
const pubClient = createClient({ url: process.env.REDIS_URL || "redis://localhost:6379" });
const subClient = pubClient.duplicate();
pubClient.on("error", (err) => logger.log(`Redis pubClient error: ${err}`, "ERROR", "redis"));
subClient.on("error", (err) => logger.log(`Redis subClient error: ${err}`, "ERROR", "redis"));
try {
await Promise.all([pubClient.connect(), subClient.connect()]);
logger.log(`[${process.env.NODE_ENV}] Connected to Redis`, "INFO", "redis", "api");
} catch (redisError) {
logger.log("Failed to connect to Redis", "ERROR", "redis", redisError);
}
process.on("SIGINT", async () => {
logger.log("Closing Redis connections...", "INFO", "redis", "api");
try {
await Promise.all([pubClient.disconnect(), subClient.disconnect()]);
logger.log("Redis connections closed. Process will exit.", "INFO", "redis", "api");
} catch (error) {
logger.log(`Error closing Redis connections: ${error.message}`, "ERROR", "redis", "api");
}
await Promise.all([pubClient.disconnect(), subClient.disconnect()]);
process.exit(0);
});
const ioRedis = new Server(server, {
path: "/wss",
adapter: createAdapter(pubClient, subClient),
cors: {
origin:
process.env?.NODE_ENV === "development"
? [...SOCKETIO_CORS_ORIGIN, ...SOCKETIO_CORS_ORIGIN_DEV]
: SOCKETIO_CORS_ORIGIN,
methods: ["GET", "POST"],
credentials: true,
exposedHeaders: ["set-cookie"]
}
});
if (isString(process.env.REDIS_ADMIN_PASS) && !isEmpty(process.env.REDIS_ADMIN_PASS)) {
logger.log(`Initializing Redis Admin UI....`, "INFO", "redis", "api");
instrument(ioRedis, {
auth: {
type: "basic",
username: "admin",
password: process.env.REDIS_ADMIN_PASS
},
mode: process.env.REDIS_ADMIN_MODE || "development"
});
}
const io = new Server(server, {
path: "/ws",
adapter: createAdapter(pubClient, subClient),
cors: {
origin: SOCKETIO_CORS_ORIGIN,
methods: ["GET", "POST"],
@@ -261,22 +128,173 @@ const applySocketIO = async ({ server, app }) => {
}
});
const api = {
pubClient,
io,
ioRedis,
redisCluster
};
app.use((req, res, next) => {
Object.assign(req, api);
req.pubClient = pubClient;
req.io = io;
next();
});
Object.assign(module.exports, { io, pubClient });
return { pubClient, io };
};
/**
* Apply Redis helper functions
* @param pubClient
* @param app
*/
const applyRedisHelpers = (pubClient, app) => {
// Store session data in Redis
const setSessionData = async (socketId, key, value) => {
await pubClient.hSet(`socket:${socketId}`, key, JSON.stringify(value)); // Use Redis pubClient
};
// Retrieve session data from Redis
const getSessionData = async (socketId, key) => {
const data = await pubClient.hGet(`socket:${socketId}`, key);
return data ? JSON.parse(data) : null;
};
// Clear session data from Redis
const clearSessionData = async (socketId) => {
await pubClient.del(`socket:${socketId}`);
};
// Store multiple session data in Redis
const setMultipleSessionData = async (socketId, keyValues) => {
// keyValues is expected to be an object { key1: value1, key2: value2, ... }
const entries = Object.entries(keyValues).map(([key, value]) => [key, JSON.stringify(value)]);
await pubClient.hSet(`socket:${socketId}`, ...entries.flat());
};
// Retrieve multiple session data from Redis
const getMultipleSessionData = async (socketId, keys) => {
const data = await pubClient.hmGet(`socket:${socketId}`, keys);
// Redis returns an object with null values for missing keys, so we parse the non-null ones
return Object.fromEntries(keys.map((key, index) => [key, data[index] ? JSON.parse(data[index]) : null]));
};
const setMultipleFromArraySessionData = async (socketId, keyValueArray) => {
// Use Redis multi/pipeline to batch the commands
const multi = pubClient.multi();
keyValueArray.forEach(([key, value]) => {
multi.hSet(`socket:${socketId}`, key, JSON.stringify(value));
});
await multi.exec(); // Execute all queued commands
};
// Helper function to add an item to the end of the Redis list
const addItemToEndOfList = async (socketId, key, newItem) => {
try {
await pubClient.rPush(`socket:${socketId}:${key}`, JSON.stringify(newItem));
} catch (error) {
console.error(`Error adding item to the end of the list for socket ${socketId}:`, error);
}
};
// Helper function to add an item to the beginning of the Redis list
const addItemToBeginningOfList = async (socketId, key, newItem) => {
try {
await pubClient.lPush(`socket:${socketId}:${key}`, JSON.stringify(newItem));
} catch (error) {
console.error(`Error adding item to the beginning of the list for socket ${socketId}:`, error);
}
};
// Helper function to clear a list in Redis
const clearList = async (socketId, key) => {
try {
await pubClient.del(`socket:${socketId}:${key}`);
} catch (error) {
console.error(`Error clearing list for socket ${socketId}:`, error);
}
};
const api = {
setSessionData,
getSessionData,
clearSessionData,
setMultipleSessionData,
getMultipleSessionData,
setMultipleFromArraySessionData,
addItemToEndOfList,
addItemToBeginningOfList,
clearList
};
Object.assign(module.exports, api);
return api;
app.use((req, res, next) => {
req.sessionUtils = api;
next();
});
// // Demo to show how all the helper functions work
// const demoSessionData = async () => {
// const socketId = "testSocketId";
//
// // Store session data using setSessionData
// await exports.setSessionData(socketId, "field1", "Hello, Redis!");
//
// // Retrieve session data using getSessionData
// const field1Value = await exports.getSessionData(socketId, "field1");
// console.log("Retrieved single field value:", field1Value);
//
// // Store multiple session data using setMultipleSessionData
// await exports.setMultipleSessionData(socketId, { field2: "Second Value", field3: "Third Value" });
//
// // Retrieve multiple session data using getMultipleSessionData
// const multipleFields = await exports.getMultipleSessionData(socketId, ["field2", "field3"]);
// console.log("Retrieved multiple field values:", multipleFields);
//
// // Store multiple session data using setMultipleFromArraySessionData
// await exports.setMultipleFromArraySessionData(socketId, [
// ["field4", "Fourth Value"],
// ["field5", "Fifth Value"]
// ]);
//
// // Retrieve and log all fields
// const allFields = await exports.getMultipleSessionData(socketId, [
// "field1",
// "field2",
// "field3",
// "field4",
// "field5"
// ]);
// console.log("Retrieved all field values:", allFields);
//
// // Add item to the end of a Redis list
// await exports.addItemToEndOfList(socketId, "logEvents", { event: "Log Event 1", timestamp: new Date() });
// await exports.addItemToEndOfList(socketId, "logEvents", { event: "Log Event 2", timestamp: new Date() });
//
// // Add item to the beginning of a Redis list
// await exports.addItemToBeginningOfList(socketId, "logEvents", { event: "First Log Event", timestamp: new Date() });
//
// // Retrieve the entire list (using lRange)
// const logEvents = await pubClient.lRange(`socket:${socketId}:logEvents`, 0, -1);
// console.log("Log Events List:", logEvents.map(JSON.parse));
//
// // **Add the new code below to test clearList**
//
// // Clear the list using clearList
// await exports.clearList(socketId, "logEvents");
// console.log("Log Events List cleared.");
//
// // Retrieve the list after clearing to confirm it's empty
// const logEventsAfterClear = await pubClient.lRange(`socket:${socketId}:logEvents`, 0, -1);
// console.log("Log Events List after clearing:", logEventsAfterClear); // Should be an empty array
//
// // Clear session data
// await exports.clearSessionData(socketId);
// console.log("Session data cleared.");
// };
//
// if (process.env.NODE_ENV === "development") {
// demoSessionData();
// }
};
/**
@@ -289,31 +307,19 @@ const main = async () => {
const server = http.createServer(app);
const { pubClient, ioRedis } = await applySocketIO({ server, app });
const redisHelpers = applyRedisHelpers({ pubClient, app, logger });
const ioHelpers = applyIOHelpers({ app, redisHelpers, ioRedis, logger });
// Legacy Socket Events
const { pubClient } = await applySocketIO(server, app);
applyRedisHelpers(pubClient, app);
require("./server/web-sockets/web-socket");
applyMiddleware({ app });
applyRoutes({ app });
redisSocketEvents({ io: ioRedis, redisHelpers, ioHelpers, logger });
applyMiddleware(app);
applyRoutes(app);
try {
await server.listen(port);
logger.log(`Server started on port ${port}`, "INFO", "api");
logger.log(`[${process.env.NODE_ENV}] Server started on port ${port}`, "INFO", "api");
} catch (error) {
logger.log(`Server failed to start on port ${port}`, "ERROR", "api", error);
logger.log(`[${process.env.NODE_ENV}] Server failed to start on port ${port}`, "ERROR", "api", error);
}
};
// Start server
main().catch((error) => {
logger.log(`Main-API-Error: Something was not caught in the application.`, "error", "api", null, {
error: error.message,
errorjson: JSON.stringify(error)
});
// Note: If we want the app to crash on all uncaught async operations, we would
// need to put a `process.exit(1);` here
});
main();

View File

@@ -12,67 +12,92 @@ const AxiosLib = require("axios").default;
const axios = AxiosLib.create();
const { PBS_ENDPOINTS, PBS_CREDENTIALS } = require("./pbs-constants");
const { CheckForErrors } = require("./pbs-job-export");
const { getSessionData, getMultipleSessionData, setMultipleSessionData } = require("../../../server");
const uuid = require("uuid").v4;
axios.interceptors.request.use((x) => {
const socket = x.socket;
const headers = {
...x.headers.common,
...x.headers[x.method],
...x.headers
};
const printable = `${new Date()} | Request: ${x.method.toUpperCase()} | ${
x.url
} | ${JSON.stringify(x.data)} | ${JSON.stringify(headers)}`;
//console.log(printable);
axios.interceptors.request.use(
async (x) => {
const socket = x.socket;
CdkBase.createJsonEvent(socket, "SILLY", `Raw Request: ${printable}`, x.data);
const headers = {
...x.headers.common,
...x.headers[x.method],
...x.headers
};
return x;
});
const printable = `${new Date()} | Request: ${x.method.toUpperCase()} | ${
x.url
} | ${JSON.stringify(x.data)} | ${JSON.stringify(headers)}`;
axios.interceptors.response.use((x) => {
const socket = x.config.socket;
console.log(printable);
const printable = `${new Date()} | Response: ${x.status} | ${JSON.stringify(x.data)}`;
//console.log(printable);
CdkBase.createJsonEvent(socket, "SILLY", `Raw Response: ${printable}`, x.data);
// Use await properly here for the async operation
await CdkBase.createJsonEvent(socket, "TRACE", `Raw Request: ${printable}`, x.data);
return x;
});
return x; // Return the modified request
},
(error) => {
return Promise.reject(error); // Proper error handling
}
);
axios.interceptors.response.use(
async (x) => {
const socket = x.config.socket;
const printable = `${new Date()} | Response: ${x.status} | ${JSON.stringify(x.data)}`;
console.log(printable);
// Use await properly here for the async operation
await CdkBase.createJsonEvent(socket, "TRACE", `Raw Response: ${printable}`, x.data);
return x; // Return the modified response
},
(error) => {
return Promise.reject(error); // Proper error handling
}
);
async function PbsCalculateAllocationsAp(socket, billids) {
try {
CdkBase.createLogEvent(socket, "DEBUG", `Received request to calculate allocations for ${billids}`);
await CdkBase.createLogEvent(socket, "DEBUG", `Received request to calculate allocations for ${billids}`);
const { bills, bodyshops } = await QueryBillData(socket, billids);
const bodyshop = bodyshops[0];
socket.bodyshop = bodyshop;
socket.bills = bills;
await setMultipleSessionData(socket.id, {
bills,
bodyshop
});
const txEnvelope = await getSessionData(socket.id, "txEnvelope");
//Each bill will enter it's own top level transaction.
const transactionlist = [];
if (bills.length === 0) {
CdkBase.createLogEvent(
await CdkBase.createLogEvent(
socket,
"ERROR",
`No bills found for export. Ensure they have not already been exported and try again.`
);
}
bills.forEach((bill) => {
//Keep the allocations at the bill level.
const transactionObject = {
SerialNumber: socket.bodyshop.pbs_serialnumber,
SerialNumber: bodyshop.pbs_serialnumber,
billid: bill.id,
Posting: {
Reference: bill.invoice_number,
JournalCode: socket.txEnvelope ? socket.txEnvelope.journal : null,
TransactionDate: moment().tz(socket.bodyshop.timezone).toISOString(), //"0001-01-01T00:00:00.0000000Z",
//Description: "Bulk AP posting.",
//AdditionalInfo: "String",
Source: "ImEX Online", //TODO:AIO Resolve this for rome online.
Lines: [] //socket.apAllocations,
Reference: bill.invoice_number,
JournalCode: txEnvelope?.journal,
TransactionDate: moment().tz(bodyshop.timezone).toISOString(),
Source: "ImEX Online", // TODO: Resolve this for Rome Online.
Lines: [] // Will be populated with allocation data,
}
};
@@ -117,13 +142,13 @@ async function PbsCalculateAllocationsAp(socket, billids) {
};
}
//Add the line amount.
// Add the line amount.
billHash[cc.name] = {
...billHash[cc.name],
Amount: billHash[cc.name].Amount.add(lineDinero)
};
//Does the line have taxes?
// Does the line have taxes?
if (bl.applicable_taxes.federal) {
billHash[bodyshop.md_responsibility_centers.taxes.federal_itc.name] = {
...billHash[bodyshop.md_responsibility_centers.taxes.federal_itc.name],
@@ -145,7 +170,7 @@ async function PbsCalculateAllocationsAp(socket, billids) {
let APAmount = Dinero();
Object.keys(billHash).map((key) => {
if (billHash[key].Amount.getAmount() > 0 || billHash[key].Amount.getAmount() < 0) {
if (billHash[key].Amount.getAmount() !== 0) {
transactionObject.Posting.Lines.push({
...billHash[key],
Amount: billHash[key].Amount.toFormat("0.00")
@@ -169,19 +194,19 @@ async function PbsCalculateAllocationsAp(socket, billids) {
return transactionlist;
} catch (error) {
CdkBase.createLogEvent(socket, "ERROR", `Error encountered in PbsCalculateAllocationsAp. ${error}`);
await CdkBase.createLogEvent(socket, "ERROR", `Error encountered in PbsCalculateAllocationsAp. ${error}`);
}
}
exports.PbsCalculateAllocationsAp = PbsCalculateAllocationsAp;
async function QueryBillData(socket, billids) {
CdkBase.createLogEvent(socket, "DEBUG", `Querying bill data for id(s) ${billids}`);
await CdkBase.createLogEvent(socket, "DEBUG", `Querying bill data for id(s) ${billids}`);
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {});
const result = await client
.setHeaders({ Authorization: `Bearer ${socket.handshake.auth.token}` })
.request(queries.GET_PBS_AP_ALLOCATIONS, { billids: billids });
CdkBase.createLogEvent(socket, "SILLY", `Bill data query result ${JSON.stringify(result, null, 2)}`);
await CdkBase.createLogEvent(socket, "TRACE", `Bill data query result ${JSON.stringify(result, null, 2)}`);
return result;
}
@@ -196,40 +221,49 @@ function getCostAccount(billline, respcenters) {
}
exports.PbsExportAp = async function (socket, { billids, txEnvelope }) {
CdkBase.createLogEvent(socket, "DEBUG", `Exporting selected AP.`);
await CdkBase.createLogEvent(socket, "DEBUG", `Exporting selected AP.`);
//apAllocations has the same shap as the lines key for the accounting posting to PBS.
socket.apAllocations = await PbsCalculateAllocationsAp(socket, billids);
socket.txEnvelope = txEnvelope;
for (const allocation of socket.apAllocations) {
const apAllocations = await PbsCalculateAllocationsAp(socket, billids);
await setMultipleSessionData(socket.id, {
apAllocations,
txEnvelope
});
for (const allocation of apAllocations) {
const { billid, ...restAllocation } = allocation;
const { data: AccountPostingChange } = await axios.post(PBS_ENDPOINTS.AccountingPostingChange, restAllocation, {
auth: PBS_CREDENTIALS,
socket
});
CheckForErrors(socket, AccountPostingChange);
CheckForErrors(socket, AccountPostingChange).catch((err) =>
console.error(`Error running CheckingForErrors in pbs-ap-allocations`)
);
if (AccountPostingChange.WasSuccessful) {
CdkBase.createLogEvent(socket, "DEBUG", `Marking bill as exported.`);
await CdkBase.createLogEvent(socket, "DEBUG", `Marking bill as exported.`);
await MarkApExported(socket, [billid]);
socket.emit("ap-export-success", billid);
} else {
CdkBase.createLogEvent(socket, "ERROR", `Export was not succesful.`);
await CdkBase.createLogEvent(socket, "ERROR", `Export was not successful.`);
socket.emit("ap-export-failure", {
billid,
error: AccountPostingChange.Message
});
}
}
socket.emit("ap-export-complete");
};
async function MarkApExported(socket, billids) {
CdkBase.createLogEvent(socket, "DEBUG", `Marking bills as exported for id ${billids}`);
const { bills, bodyshop } = await getMultipleSessionData(socket.id, ["bills", "bodyshop"]);
await CdkBase.createLogEvent(socket, "DEBUG", `Marking bills as exported for id ${billids}`);
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {});
const result = await client
return await client
.setHeaders({ Authorization: `Bearer ${socket.handshake.auth.token}` })
.request(queries.MARK_BILLS_EXPORTED, {
billids,
@@ -237,13 +271,11 @@ async function MarkApExported(socket, billids) {
exported: true,
exported_at: new Date()
},
logs: socket.bills.map((bill) => ({
bodyshopid: socket.bodyshop.id,
logs: bills.map((bill) => ({
bodyshopid: bodyshop.id,
billid: bill.id,
successful: true,
useremail: socket.user.email
}))
});
return result;
}

View File

@@ -12,136 +12,169 @@ const CalculateAllocations = require("../../cdk/cdk-calculate-allocations").defa
const CdkBase = require("../../web-sockets/web-socket");
const moment = require("moment-timezone");
const Dinero = require("dinero.js");
const { setSessionData, getSessionData, getMultipleSessionData, setMultipleSessionData } = require("../../../server");
const InstanceManager = require("../../utils/instanceMgr").default;
const axios = AxiosLib.create();
axios.interceptors.request.use((x) => {
const socket = x.socket;
axios.interceptors.request.use(
async (x) => {
const socket = x.socket;
const headers = {
...x.headers.common,
...x.headers[x.method],
...x.headers
};
const printable = `${new Date()} | Request: ${x.method.toUpperCase()} | ${
x.url
} | ${JSON.stringify(x.data)} | ${JSON.stringify(headers)}`;
//console.log(printable);
const headers = {
...x.headers.common,
...x.headers[x.method],
...x.headers
};
CdkBase.createJsonEvent(socket, "SILLY", `Raw Request: ${printable}`, x.data);
const printable = `${new Date()} | Request: ${x.method.toUpperCase()} | ${
x.url
} | ${JSON.stringify(x.data)} | ${JSON.stringify(headers)}`;
return x;
});
console.log(printable);
axios.interceptors.response.use((x) => {
const socket = x.config.socket;
await CdkBase.createJsonEvent(socket, "TRACE", `Raw Request: ${printable}`, x.data);
const printable = `${new Date()} | Response: ${x.status} | ${JSON.stringify(x.data)}`;
//console.log(printable);
CdkBase.createJsonEvent(socket, "SILLY", `Raw Response: ${printable}`, x.data);
return x; // Make sure to return the request object
},
(error) => {
return Promise.reject(error);
}
);
return x;
});
axios.interceptors.response.use(
async (x) => {
const socket = x.config.socket;
const printable = `${new Date()} | Response: ${x.status} | ${JSON.stringify(x.data)}`;
console.log(printable);
await CdkBase.createJsonEvent(socket, "TRACE", `Raw Response: ${printable}`, x.data);
return x; // Make sure to return the response object
},
(error) => {
return Promise.reject(error);
}
);
exports.default = async function (socket, { txEnvelope, jobid }) {
socket.logEvents = [];
socket.recordid = jobid;
socket.txEnvelope = txEnvelope;
try {
CdkBase.createLogEvent(socket, "DEBUG", `Received Job export request for id ${jobid}`);
await setMultipleSessionData(socket.id, {
recordid: jobid,
txEnvelope
});
await CdkBase.createLogEvent(socket, "DEBUG", `Received Job export request for id ${jobid}`);
const JobData = await QueryJobData(socket, jobid);
socket.JobData = JobData;
CdkBase.createLogEvent(socket, "DEBUG", `Querying the DMS for the Vehicle Record.`);
//Query for the Vehicle record to get the associated customer.
socket.DmsVeh = await QueryVehicleFromDms(socket);
await setSessionData(socket.id, "JobData", JobData);
await CdkBase.createLogEvent(socket, "DEBUG", `Querying the DMS for the Vehicle Record.`);
// Query for the Vehicle record to get the associated customer
const DmsVeh = await QueryVehicleFromDms(socket);
await setSessionData(socket.id, "DmsVeh", DmsVeh);
let DMSVehCustomer;
//Todo: Need to validate the lines and methods below.
if (socket.DmsVeh && socket.DmsVeh.CustomerRef) {
//Get the associated customer from the Vehicle Record.
socket.DMSVehCustomer = await QueryCustomerBycodeFromDms(socket, socket.DmsVeh.CustomerRef);
if (DmsVeh?.CustomerRef) {
// Get the associated customer from the Vehicle Record
DMSVehCustomer = await QueryCustomerBycodeFromDms(socket, DmsVeh.CustomerRef);
await setSessionData(socket.id, "DMSVehCustomer", DMSVehCustomer);
}
socket.DMSCustList = await QueryCustomersFromDms(socket);
const DMSCustList = await QueryCustomersFromDms(socket);
await setSessionData(socket.id, "DMSCustList", DMSCustList);
socket.emit("pbs-select-customer", [
...(socket.DMSVehCustomer ? [{ ...socket.DMSVehCustomer, vinOwner: true }] : []),
...socket.DMSCustList
...(DMSVehCustomer ? [{ ...DMSVehCustomer, vinOwner: true }] : []),
...DMSCustList
]);
} catch (error) {
CdkBase.createLogEvent(socket, "ERROR", `Error encountered in PbsJobExport. ${error}`);
await CdkBase.createLogEvent(socket, "ERROR", `Error encountered in PbsJobExport. ${error}`);
}
};
exports.PbsSelectedCustomer = async function PbsSelectedCustomer(socket, selectedCustomerId) {
try {
if (socket.JobData.bodyshop.pbs_configuration.disablecontactvehicle === false) {
CdkBase.createLogEvent(socket, "DEBUG", `User selected customer ${selectedCustomerId || "NEW"}`);
const JobData = await getSessionData(socket.id, "JobData");
//Upsert the contact information as per Wafaa's Email.
CdkBase.createLogEvent(
if (JobData.bodyshop.pbs_configuration.disablecontactvehicle === false) {
await CdkBase.createLogEvent(socket, "DEBUG", `User selected customer ${selectedCustomerId || "NEW"}`);
// Upsert the contact information as per Wafaa's Email
await CdkBase.createLogEvent(
socket,
"DEBUG",
`Upserting contact information to DMS for ${
socket.JobData.ownr_fn || ""
} ${socket.JobData.ownr_ln || ""} ${socket.JobData.ownr_co_nm || ""}`
JobData.ownr_fn || ""
} ${JobData.ownr_ln || ""} ${JobData.ownr_co_nm || ""}`
);
const ownerRef = await UpsertContactData(socket, selectedCustomerId);
CdkBase.createLogEvent(socket, "DEBUG", `Upserting vehicle information to DMS for ${socket.JobData.v_vin}`);
await CdkBase.createLogEvent(socket, "DEBUG", `Upserting vehicle information to DMS for ${JobData.v_vin}`);
await UpsertVehicleData(socket, ownerRef.ReferenceId);
} else {
CdkBase.createLogEvent(
await CdkBase.createLogEvent(
socket,
"DEBUG",
`Contact and Vehicle updates disabled. Skipping to accounting data insert.`
);
}
CdkBase.createLogEvent(socket, "DEBUG", `Inserting account data.`);
CdkBase.createLogEvent(socket, "DEBUG", `Inserting accounting posting data..`);
await CdkBase.createLogEvent(socket, "DEBUG", `Inserting account data.`);
await CdkBase.createLogEvent(socket, "DEBUG", `Inserting accounting posting data..`);
const insertResponse = await InsertAccountPostingData(socket);
// TODO: Insert Clear session
if (insertResponse.WasSuccessful) {
CdkBase.createLogEvent(socket, "DEBUG", `Marking job as exported.`);
await MarkJobExported(socket, socket.JobData.id);
await CdkBase.createLogEvent(socket, "DEBUG", `Marking job as exported.`);
await MarkJobExported(socket, JobData.id);
socket.emit("export-success", socket.JobData.id);
socket.emit("export-success", JobData.id);
} else {
CdkBase.createLogEvent(socket, "ERROR", `Export was not succesful.`);
await CdkBase.createLogEvent(socket, "ERROR", `Export was not successful.`);
}
} catch (error) {
CdkBase.createLogEvent(socket, "ERROR", `Error encountered in CdkSelectedCustomer. ${error}`);
await CdkBase.createLogEvent(socket, "ERROR", `Error encountered in PbsSelectedCustomer. ${error}`);
await InsertFailedExportLog(socket, error);
}
};
async function CheckForErrors(socket, response) {
if (response.WasSuccessful === undefined || response.WasSuccessful === true) {
CdkBase.createLogEvent(socket, "DEBUG", `Successful response from DMS. ${response.Message || ""}`);
await CdkBase.createLogEvent(socket, "DEBUG", `Successful response from DMS. ${response.Message || ""}`);
} else {
CdkBase.createLogEvent(socket, "ERROR", `Error received from DMS: ${response.Message}`);
CdkBase.createLogEvent(socket, "SILLY", `Error received from DMS: ${JSON.stringify(response)}`);
await CdkBase.createLogEvent(socket, "ERROR", `Error received from DMS: ${response.Message}`);
await CdkBase.createLogEvent(socket, "TRACE", `Error received from DMS: ${JSON.stringify(response)}`);
}
}
exports.CheckForErrors = CheckForErrors;
async function QueryJobData(socket, jobid) {
CdkBase.createLogEvent(socket, "DEBUG", `Querying job data for id ${jobid}`);
await CdkBase.createLogEvent(socket, "DEBUG", `Querying job data for id ${jobid}`);
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {});
const result = await client
.setHeaders({ Authorization: `Bearer ${socket.handshake.auth.token}` })
.request(queries.QUERY_JOBS_FOR_PBS_EXPORT, { id: jobid });
CdkBase.createLogEvent(socket, "SILLY", `Job data query result ${JSON.stringify(result, null, 2)}`);
await CdkBase.createLogEvent(socket, "TRACE", `Job data query result ${JSON.stringify(result, null, 2)}`);
return result.jobs_by_pk;
}
async function QueryVehicleFromDms(socket) {
try {
if (!socket.JobData.v_vin) return null;
const JobData = await getSessionData(socket.id, "JobData");
if (!JobData.v_vin) return null;
const { data: VehicleGetResponse, request } = await axios.post(
PBS_ENDPOINTS.VehicleGet,
{
SerialNumber: socket.JobData.bodyshop.pbs_serialnumber,
SerialNumber: JobData.bodyshop.pbs_serialnumber,
// VehicleId: "00000000000000000000000000000000",
// Year: "String",
// Make: "String",
@@ -149,7 +182,7 @@ async function QueryVehicleFromDms(socket) {
// Trim: "String",
// ModelNumber: "String",
// StockNumber: "String",
VIN: socket.JobData.v_vin
VIN: JobData.v_vin
// LicenseNumber: "String",
// Lot: "String",
// Status: "String",
@@ -166,26 +199,31 @@ async function QueryVehicleFromDms(socket) {
{ auth: PBS_CREDENTIALS, socket }
);
CheckForErrors(socket, VehicleGetResponse);
await CheckForErrors(socket, VehicleGetResponse);
return VehicleGetResponse;
} catch (error) {
CdkBase.createLogEvent(socket, "ERROR", `Error in QueryVehicleFromDms - ${error}`);
await CdkBase.createLogEvent(socket, "ERROR", `Error in QueryVehicleFromDms - ${error}`);
throw new Error(error);
}
}
async function QueryCustomersFromDms(socket) {
try {
// Retrieve JobData from session storage
const JobData = await getSessionData(socket.id, "JobData");
// Make an API call to PBS to query customer details
const { data: CustomerGetResponse } = await axios.post(
PBS_ENDPOINTS.ContactGet,
{
SerialNumber: socket.JobData.bodyshop.pbs_serialnumber,
SerialNumber: JobData.bodyshop.pbs_serialnumber,
//ContactId: "00000000000000000000000000000000",
// ContactCode: socket.JobData.owner.accountingid,
FirstName: socket.JobData.ownr_fn,
LastName: socket.JobData.ownr_co_nm ? socket.JobData.ownr_co_nm : socket.JobData.ownr_ln,
PhoneNumber: socket.JobData.ownr_ph1,
EmailAddress: socket.JobData.ownr_ea
// ContactCode: JobData.owner.accountingid,
FirstName: JobData.ownr_fn,
LastName: JobData.ownr_co_nm ? JobData.ownr_co_nm : JobData.ownr_ln,
PhoneNumber: JobData.ownr_ph1,
EmailAddress: JobData.ownr_ea
// ModifiedSince: "0001-01-01T00:00:00.0000000Z",
// ModifiedUntil: "0001-01-01T00:00:00.0000000Z",
// ContactIdList: ["00000000000000000000000000000000"],
@@ -197,27 +235,36 @@ async function QueryCustomersFromDms(socket) {
},
{ auth: PBS_CREDENTIALS, socket }
);
CheckForErrors(socket, CustomerGetResponse);
// Check for errors in the PBS response
await CheckForErrors(socket, CustomerGetResponse);
// Return the list of contacts from the PBS response
return CustomerGetResponse && CustomerGetResponse.Contacts;
} catch (error) {
CdkBase.createLogEvent(socket, "ERROR", `Error in QueryCustomersFromDms - ${error}`);
// Log any errors encountered during the API call
await CdkBase.createLogEvent(socket, "ERROR", `Error in QueryCustomersFromDms - ${error}`);
throw new Error(error);
}
}
async function QueryCustomerBycodeFromDms(socket, CustomerRef) {
try {
// Retrieve JobData from session storage
const JobData = await getSessionData(socket.id, "JobData");
// Make an API call to PBS to query customer by ContactId
const { data: CustomerGetResponse } = await axios.post(
PBS_ENDPOINTS.ContactGet,
{
SerialNumber: socket.JobData.bodyshop.pbs_serialnumber,
SerialNumber: JobData.bodyshop.pbs_serialnumber,
ContactId: CustomerRef
//ContactCode: socket.JobData.owner.accountingid,
//FirstName: socket.JobData.ownr_co_nm
// ? socket.JobData.ownr_co_nm
// : socket.JobData.ownr_fn,
//LastName: socket.JobData.ownr_ln,
//PhoneNumber: socket.JobData.ownr_ph1,
//ContactCode: JobData.owner.accountingid,
//FirstName: JobData.ownr_co_nm
// ? JobData.ownr_co_nm
// : JobData.ownr_fn,
//LastName: JobData.ownr_ln,
//PhoneNumber: JobData.ownr_ph1,
// EmailAddress: "String",
// ModifiedSince: "0001-01-01T00:00:00.0000000Z",
// ModifiedUntil: "0001-01-01T00:00:00.0000000Z",
@@ -230,33 +277,42 @@ async function QueryCustomerBycodeFromDms(socket, CustomerRef) {
},
{ auth: PBS_CREDENTIALS, socket }
);
CheckForErrors(socket, CustomerGetResponse);
// Check for errors in the PBS response
await CheckForErrors(socket, CustomerGetResponse);
// Return the list of contacts from the PBS response
return CustomerGetResponse && CustomerGetResponse.Contacts;
} catch (error) {
CdkBase.createLogEvent(socket, "ERROR", `Error in QueryCustomersFromDms - ${error}`);
// Log any errors encountered during the API call
await CdkBase.createLogEvent(socket, "ERROR", `Error in QueryCustomerBycodeFromDms - ${error}`);
throw new Error(error);
}
}
async function UpsertContactData(socket, selectedCustomerId) {
try {
// Retrieve JobData from session storage
const JobData = await getSessionData(socket.id, "JobData");
// Make an API call to PBS to upsert contact data
const { data: ContactChangeResponse } = await axios.post(
PBS_ENDPOINTS.ContactChange,
{
ContactInfo: {
// Id: socket.JobData.owner.id,
// Id: JobData.owner.id,
...(selectedCustomerId ? { ContactId: selectedCustomerId } : {}),
SerialNumber: socket.JobData.bodyshop.pbs_serialnumber,
Code: socket.JobData.owner.accountingid,
...(socket.JobData.ownr_co_nm
SerialNumber: JobData.bodyshop.pbs_serialnumber,
Code: JobData.owner.accountingid,
...(JobData.ownr_co_nm
? {
//LastName: socket.JobData.ownr_ln,
FirstName: socket.JobData.ownr_co_nm,
//LastName: JobData.ownr_ln,
FirstName: JobData.ownr_co_nm,
IsBusiness: true
}
: {
LastName: socket.JobData.ownr_ln,
FirstName: socket.JobData.ownr_fn,
LastName: JobData.ownr_ln,
FirstName: JobData.ownr_fn,
IsBusiness: false
}),
@@ -266,20 +322,20 @@ async function UpsertContactData(socket, selectedCustomerId) {
IsInactive: false,
//ApartmentNumber: "String",
Address: socket.JobData.ownr_addr1,
City: socket.JobData.ownr_city,
//County: socket.JobData.ownr_addr1,
State: socket.JobData.ownr_st,
ZipCode: socket.JobData.ownr_zip,
Address: JobData.ownr_addr1,
City: JobData.ownr_city,
//County: JobData.ownr_addr1,
State: JobData.ownr_st,
ZipCode: JobData.ownr_zip,
//BusinessPhone: "String",
//BusinessPhoneExt: "String",
HomePhone: socket.JobData.ownr_ph2,
CellPhone: socket.JobData.ownr_ph1,
HomePhone: JobData.ownr_ph2,
CellPhone: JobData.ownr_ph1,
//BusinessPhoneRawReverse: "String",
//HomePhoneRawReverse: "String",
//CellPhoneRawReverse: "String",
//FaxNumber: "String",
EmailAddress: socket.JobData.ownr_ea
EmailAddress: JobData.ownr_ea
//Notes: "String",
//CriticalMemo: "String",
//BirthDate: "0001-01-01T00:00:00.0000000Z",
@@ -312,39 +368,43 @@ async function UpsertContactData(socket, selectedCustomerId) {
},
{ auth: PBS_CREDENTIALS, socket }
);
CheckForErrors(socket, ContactChangeResponse);
await CheckForErrors(socket, ContactChangeResponse);
return ContactChangeResponse;
} catch (error) {
CdkBase.createLogEvent(socket, "ERROR", `Error in UpsertContactData - ${error}`);
await CdkBase.createLogEvent(socket, "ERROR", `Error in UpsertContactData - ${error}`);
throw new Error(error);
}
}
async function UpsertVehicleData(socket, ownerRef) {
try {
const JobData = await getSessionData(socket.id, "JobData");
const { data: VehicleChangeResponse } = await axios.post(
PBS_ENDPOINTS.VehicleChange,
{
VehicleInfo: {
//Id: "string/00000000-0000-0000-0000-000000000000",
//VehicleId: "00000000000000000000000000000000",
SerialNumber: socket.JobData.bodyshop.pbs_serialnumber,
SerialNumber: JobData.bodyshop.pbs_serialnumber,
//StockNumber: "String",
VIN: socket.JobData.v_vin,
LicenseNumber: socket.JobData.plate_no,
VIN: JobData.v_vin,
LicenseNumber: JobData.plate_no,
//FleetNumber: "String",
//Status: "String",
OwnerRef: ownerRef, // "00000000000000000000000000000000",
ModelNumber: socket.JobData.vehicle && socket.JobData.vehicle.v_makecode,
Make: socket.JobData.v_make_desc,
Model: socket.JobData.v_model_desc,
Trim: socket.JobData.vehicle && socket.JobData.vehicle.v_trimcode,
ModelNumber: JobData.vehicle && JobData.vehicle.v_makecode,
Make: JobData.v_make_desc,
Model: JobData.v_model_desc,
Trim: JobData.vehicle && JobData.vehicle.v_trimcode,
//VehicleType: "String",
Year: socket.JobData.v_model_yr,
Odometer: socket.JobData.kmout,
Year: JobData.v_model_yr,
Odometer: JobData.kmout,
ExteriorColor: {
Code: socket.JobData.v_color,
Description: socket.JobData.v_color
Code: JobData.v_color,
Description: JobData.v_color
}
// InteriorColor: { Code: "String", Description: "String" },
//Engine: "String",
@@ -465,100 +525,112 @@ async function UpsertVehicleData(socket, ownerRef) {
},
{ auth: PBS_CREDENTIALS, socket }
);
CheckForErrors(socket, VehicleChangeResponse);
await CheckForErrors(socket, VehicleChangeResponse);
return VehicleChangeResponse;
} catch (error) {
CdkBase.createLogEvent(socket, "ERROR", `Error in UpsertVehicleData - ${error}`);
await CdkBase.createLogEvent(socket, "ERROR", `Error in UpsertVehicleData - ${error}`);
throw new Error(error);
}
}
async function InsertAccountPostingData(socket) {
try {
const allocations = await CalculateAllocations(socket, socket.JobData.id);
const { JobData, txEnvelope } = await getMultipleSessionData(socket.id, ["JobData", "txEnvelope"]);
const allocations = await CalculateAllocations(socket, JobData.id);
const wips = [];
allocations.forEach((alloc) => {
//Add the sale item from each allocation.
// Add the sale item from each allocation if the amount is greater than 0 and not a tax
if (alloc.sale.getAmount() > 0 && !alloc.tax) {
const item = {
Account: alloc.profitCenter.dms_acctnumber,
ControlNumber: socket.JobData.ro_number,
ControlNumber: JobData.ro_number,
Amount: alloc.sale.multiply(-1).toFormat("0.00"),
//Comment: "String",
//AdditionalInfo: "String",
InvoiceNumber: socket.JobData.ro_number,
InvoiceDate: moment(socket.JobData.date_invoiced).tz(socket.JobData.bodyshop.timezone).toISOString()
// Comment: "String",
// AdditionalInfo: "String",
InvoiceNumber: JobData.ro_number,
InvoiceDate: moment(JobData.date_invoiced).tz(JobData.bodyshop.timezone).toISOString()
};
wips.push(item);
}
//Add the cost Item.
// Add the cost item if the cost amount is greater than 0 and not a tax
if (alloc.cost.getAmount() > 0 && !alloc.tax) {
const item = {
Account: alloc.costCenter.dms_acctnumber,
ControlNumber: socket.JobData.ro_number,
ControlNumber: JobData.ro_number,
Amount: alloc.cost.toFormat("0.00"),
//Comment: "String",
//AdditionalInfo: "String",
InvoiceNumber: socket.JobData.ro_number,
InvoiceDate: moment(socket.JobData.date_invoiced).tz(socket.JobData.bodyshop.timezone).toISOString()
// Comment: "String",
// AdditionalInfo: "String",
InvoiceNumber: JobData.ro_number,
InvoiceDate: moment(JobData.date_invoiced).tz(JobData.bodyshop.timezone).toISOString()
};
wips.push(item);
const itemWip = {
Account: alloc.costCenter.dms_wip_acctnumber,
ControlNumber: socket.JobData.ro_number,
ControlNumber: JobData.ro_number,
Amount: alloc.cost.multiply(-1).toFormat("0.00"),
//Comment: "String",
//AdditionalInfo: "String",
InvoiceNumber: socket.JobData.ro_number,
InvoiceDate: moment(socket.JobData.date_invoiced).tz(socket.JobData.bodyshop.timezone).toISOString()
// Comment: "String",
// AdditionalInfo: "String",
InvoiceNumber: JobData.ro_number,
InvoiceDate: moment(JobData.date_invoiced).tz(JobData.bodyshop.timezone).toISOString()
};
wips.push(itemWip);
//Add to the WIP account.
// Add to the WIP account.
}
// Add tax-related entries if applicable
if (alloc.tax) {
if (alloc.sale.getAmount() > 0) {
const item2 = {
Account: alloc.profitCenter.dms_acctnumber,
ControlNumber: socket.JobData.ro_number,
ControlNumber: JobData.ro_number,
Amount: alloc.sale.multiply(-1).toFormat("0.00"),
//Comment: "String",
//AdditionalInfo: "String",
InvoiceNumber: socket.JobData.ro_number,
InvoiceDate: moment(socket.JobData.date_invoiced).tz(socket.JobData.bodyshop.timezone).toISOString()
// Comment: "String",
// AdditionalInfo: "String",
InvoiceNumber: JobData.ro_number,
InvoiceDate: moment(JobData.date_invoiced).tz(JobData.bodyshop.timezone).toISOString()
};
wips.push(item2);
}
}
});
socket.txEnvelope.payers.forEach((payer) => {
// Add payer information
txEnvelope.payers.forEach((payer) => {
const item = {
Account: payer.dms_acctnumber,
ControlNumber: payer.controlnumber,
Amount: Dinero({ amount: Math.round(payer.amount * 100) }).toFormat("0.0"),
//Comment: "String",
//AdditionalInfo: "String",
InvoiceNumber: socket.JobData.ro_number,
InvoiceDate: moment(socket.JobData.date_invoiced).tz(socket.JobData.bodyshop.timezone).toISOString()
// Comment: "String",
// AdditionalInfo: "String",
InvoiceNumber: JobData.ro_number,
InvoiceDate: moment(JobData.date_invoiced).tz(JobData.bodyshop.timezone).toISOString()
};
wips.push(item);
});
socket.transWips = wips;
await setSessionData(socket.id, "transWips", wips);
const { data: AccountPostingChange } = await axios.post(
PBS_ENDPOINTS.AccountingPostingChange,
{
SerialNumber: socket.JobData.bodyshop.pbs_serialnumber,
SerialNumber: JobData.bodyshop.pbs_serialnumber,
Posting: {
Reference: socket.JobData.ro_number,
JournalCode: socket.txEnvelope.journal,
TransactionDate: moment(socket.JobData.date_invoiced).tz(socket.JobData.bodyshop.timezone).toISOString(), //"0001-01-01T00:00:00.0000000Z",
Description: socket.txEnvelope.story,
//AdditionalInfo: "String",
Reference: JobData.ro_number,
JournalCode: txEnvelope.journal,
TransactionDate: moment(JobData.date_invoiced).tz(JobData.bodyshop.timezone).toISOString(), // "0001-01-01T00:00:00.0000000Z",
Description: txEnvelope.story,
// AdditionalInfo: "String",
Source: InstanceManager({ imex: "ImEX Online", rome: "Rome Online" }),
Lines: wips
}
@@ -566,58 +638,56 @@ async function InsertAccountPostingData(socket) {
{ auth: PBS_CREDENTIALS, socket }
);
CheckForErrors(socket, AccountPostingChange);
await CheckForErrors(socket, AccountPostingChange);
return AccountPostingChange;
} catch (error) {
CdkBase.createLogEvent(socket, "ERROR", `Error in InsertAccountPostingData - ${error}`);
await CdkBase.createLogEvent(socket, "ERROR", `Error in InsertAccountPostingData - ${error}`);
throw new Error(error);
}
}
async function MarkJobExported(socket, jobid) {
CdkBase.createLogEvent(socket, "DEBUG", `Marking job as exported for id ${jobid}`);
await CdkBase.createLogEvent(socket, "DEBUG", `Marking job as exported for id ${jobid}`);
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {});
const result = await client
const { JobData, transWips } = await getMultipleSessionData(socket.id, ["JobData", "transWips"]);
return await client
.setHeaders({ Authorization: `Bearer ${socket.handshake.auth.token}` })
.request(queries.MARK_JOB_EXPORTED, {
jobId: jobid,
job: {
status: socket.JobData.bodyshop.md_ro_statuses.default_exported || "Exported*",
status: JobData.bodyshop.md_ro_statuses.default_exported || "Exported*",
date_exported: new Date()
},
log: {
bodyshopid: socket.JobData.bodyshop.id,
bodyshopid: JobData.bodyshop.id,
jobid: jobid,
successful: true,
useremail: socket.user.email,
metadata: socket.transWips
metadata: transWips
},
bill: {
exported: true,
exported_at: new Date()
}
});
return result;
}
async function InsertFailedExportLog(socket, error) {
try {
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {});
const result = await client
.setHeaders({ Authorization: `Bearer ${socket.handshake.auth.token}` })
.request(queries.INSERT_EXPORT_LOG, {
log: {
bodyshopid: socket.JobData.bodyshop.id,
jobid: socket.JobData.id,
successful: false,
message: [error],
useremail: socket.user.email
}
});
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {});
const JobData = await getSessionData(socket.id, "JobData");
return result;
} catch (error2) {
CdkBase.createLogEvent(socket, "ERROR", `Error in InsertFailedExportLog - ${error} - ${JSON.stringify(error2)}`);
}
return await client
.setHeaders({ Authorization: `Bearer ${socket.handshake.auth.token}` })
.request(queries.INSERT_EXPORT_LOG, {
log: {
bodyshopid: JobData.bodyshop.id,
jobid: JobData.id,
successful: false,
message: [error],
useremail: socket.user.email
}
});
}

View File

@@ -207,7 +207,7 @@ exports.default = function ({ bodyshop, jobs_by_pk, qbo = false, items, taxCodes
});
if (!hasMapaLine && jobs_by_pk.job_totals.rates.mapa.total.amount > 0) {
// //console.log("Adding MAPA Line Manually.");
// console.log("Adding MAPA Line Manually.");
const mapaAccountName = responsibilityCenters.defaults.profits.MAPA;
const mapaAccount = responsibilityCenters.profits.find((c) => c.name === mapaAccountName);
@@ -272,12 +272,12 @@ exports.default = function ({ bodyshop, jobs_by_pk, qbo = false, items, taxCodes
});
}
} else {
////console.log("NO MAPA ACCOUNT FOUND!!");
//console.log("NO MAPA ACCOUNT FOUND!!");
}
}
if (!hasMashLine && jobs_by_pk.job_totals.rates.mash.total.amount > 0) {
// //console.log("Adding MASH Line Manually.");
// console.log("Adding MASH Line Manually.");
const mashAccountName = responsibilityCenters.defaults.profits.MASH;
@@ -341,7 +341,7 @@ exports.default = function ({ bodyshop, jobs_by_pk, qbo = false, items, taxCodes
});
}
} else {
// //console.log("NO MASH ACCOUNT FOUND!!");
// console.log("NO MASH ACCOUNT FOUND!!");
}
}
@@ -795,7 +795,7 @@ exports.default = function ({ bodyshop, jobs_by_pk, qbo = false, items, taxCodes
: taxCodes[taxAccountCode];
for (let tyCounter = 1; tyCounter <= 5; tyCounter++) {
const taxAmount = Dinero(job_totals.totals.us_sales_tax_breakdown[`ty${tyCounter}Tax`]);
//console.log(`Tax ${tyCounter}`, taxAmount.toFormat());
console.log(`Tax ${tyCounter}`, taxAmount.toFormat());
if (taxAmount.getAmount() > 0) {
if (qbo) {
InvoiceLineAdd.push({

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