Compare commits

..

34 Commits

Author SHA1 Message Date
Allan Carr
b8836c7ae1 Merged in feature/IO-3030-QBO-Payment-Private-Note (pull request #1934)
IO-3030 Null Check memo
2024-11-18 16:40:15 +00:00
Allan Carr
eca31c5618 IO-3030 Null Check memo
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-11-18 08:42:52 -08:00
Allan Carr
7140b8d585 Merged in feature/IO-3031-Appointment-Schedule-View-Day (pull request #1931)
IO-3031 Appointment Schedule View Day
2024-11-16 01:33:29 +00:00
Allan Carr
5eed8d9809 IO-3031 Appointment Schedule View Day
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-11-15 17:35:49 -08:00
Allan Carr
f266ee1cfe Merged in feature/IO-3028-Word-Wrap-Line-Description (pull request #1928)
IO-3028 Adjust to TextArea with autoSize
2024-11-15 20:54:44 +00:00
Allan Carr
9550de5131 IO-3028 Adjust to TextArea with autoSize
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-11-15 12:57:08 -08:00
Patrick Fic
1f76ff882c Remove IO Event Logging. 2024-11-15 11:03:10 -08:00
Patrick Fic
749f73a272 Merged in feature/IO-2920-cash-discounting (pull request #1927)
IO-2920 Update config & totals for discount.
2024-11-15 18:58:54 +00:00
Patrick Fic
9c1774c417 Merge branch 'release/2024-11-15' into feature/IO-2920-cash-discounting 2024-11-15 10:58:22 -08:00
Allan Carr
26b3a43ce5 Merge branch 'feature/IO-3027-Datapumps-Refactor' into release/2024-11-15
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>

# Conflicts:
#	.vscode/settings.json
2024-11-15 10:05:08 -08:00
Allan Carr
9dc4546b2e Merged in feature/IO-3033-Total-Loss-Indicator (pull request #1925)
IO-3033 Total Loss Indicator

Approved-by: Dave Richer
2024-11-15 17:47:56 +00:00
Allan Carr
95aa0e45a6 IO-3033 Total Loss Indicator
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-11-14 16:47:35 -08:00
Dave Richer
e9e1e820a7 release/2024-11-15 - Expose S3 client through createS3Client
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-11-14 11:57:40 -08:00
Allan Carr
b027a4e618 IO-3031 Adjust prop
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-11-14 11:52:47 -08:00
Allan Carr
c7fc75aa5c Merged in feature/IO-3031-Appointment-Schedule-View-Day (pull request #1922)
IO-3031 View Day when Scheduling

Approved-by: Dave Richer
2024-11-14 19:50:34 +00:00
Allan Carr
98d2372daf Merged in feature/IO-3030-QBO-Payment-Private-Note (pull request #1920)
IO-3030 QBO Payment Private Note

Approved-by: Dave Richer
2024-11-14 19:40:20 +00:00
Allan Carr
bf51380167 IO-3031 View Day when Scheduling
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-11-14 11:19:09 -08:00
Dave Richer
1ec827097f Merged in feature/IO-3029-Enhanced-Logging-File-Based (pull request #1921)
feature/IO-3029-Enhanced-Logging-File-Based: Adjust XML and JSON log to always upload
2024-11-14 18:55:57 +00:00
Dave Richer
89fabf85e1 feature/IO-3029-Enhanced-Logging-File-Based: Adjust XML and JSON log to always upload
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-11-14 10:55:14 -08:00
Allan Carr
ff7dd7d3ea IO-3030 QBO Payment Private Note
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-11-14 10:37:08 -08:00
Dave Richer
8cc4f88fa7 Merged in feature/IO-3029-Enhanced-Logging-File-Based (pull request #1918)
feature/IO-3029-Enhanced-Logging-File-Based: Final Enhancements
2024-11-14 16:36:13 +00:00
Dave Richer
2439755f9e feature/IO-3029-Enhanced-Logging-File-Based: Final Enhancements
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-11-14 08:34:21 -08:00
Dave Richer
7e6ab3a5ff Merged in feature/IO-3029-Enhanced-Logging-File-Based (pull request #1916)
feature/IO-3029-Enhanced-Logging-File-Based: Update Stream Key name
2024-11-14 04:14:54 +00:00
Dave Richer
763384f05f feature/IO-3029-Enhanced-Logging-File-Based: Update Stream Key name
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-11-13 20:14:17 -08:00
Dave Richer
34f876f838 Merged in feature/IO-3029-Enhanced-Logging-File-Based (pull request #1914)
feature/IO-3029-Enhanced-Logging-File-Based: Add File based S3 Logging.
2024-11-14 03:57:24 +00:00
Dave Richer
cba2da8da7 feature/IO-3029-Enhanced-Logging-File-Based: Add fix bugs
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-11-13 13:09:37 -08:00
Dave Richer
f3d8aa3438 feature/IO-3029-Enhanced-Logging-File-Based: Add File based S3 Logging.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-11-13 12:59:56 -08:00
Allan Carr
2f3eccf3d8 Merged in feature/IO-3028-Word-Wrap-Line-Description (pull request #1912)
IO-3028 Extend to Notes
2024-11-13 18:50:32 +00:00
Allan Carr
2b3e64d607 IO-3028 Extend to Notes
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-11-13 10:49:47 -08:00
Allan Carr
05b20505bb Merged in feature/IO-3028-Word-Wrap-Line-Description (pull request #1910)
IO-3028 Word Wrap Line Description

Approved-by: Dave Richer
2024-11-13 18:17:34 +00:00
Allan Carr
bddeae945c IO-3028 Word Wrap Line Description
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-11-13 10:06:01 -08:00
Patrick Fic
5b267f03b9 Add additional GIN indexes for db. 2024-11-12 20:21:43 -08:00
Patrick Fic
9aab47d8f8 IO-2920 Update config & totals for discount. 2024-11-05 16:37:21 -08:00
Patrick Fic
f2f84e2da8 Merge branch 'master-AIO' into feature/IO-2920-cash-discounting 2024-11-05 16:00:47 -08:00
35 changed files with 12905 additions and 11239 deletions

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
* text eol=lf

View File

@@ -11156,6 +11156,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>imexpay</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>insurancecos</name>
<definition_loaded>false</definition_loaded>
@@ -11198,27 +11219,6 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>intellipay</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>intellipay_cash_discount</name>
<definition_loaded>false</definition_loaded>
@@ -11747,6 +11747,48 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>ttl_adjustment</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>ttl_tax_adjustment</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
<folder_node>
@@ -11775,6 +11817,27 @@
</concept_node>
</children>
</folder_node>
<concept_node>
<name>romepay</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>scheduling</name>
<definition_loaded>false</definition_loaded>
@@ -36253,6 +36316,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>total_cust_payable_cash_discount</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>total_repairs</name>
<definition_loaded>false</definition_loaded>
@@ -48360,6 +48444,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>tasks_in_view</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>tasks_on_board</name>
<definition_loaded>false</definition_loaded>
@@ -48402,6 +48507,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>total_amount_in_view</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>total_amount_on_board</name>
<definition_loaded>false</definition_loaded>
@@ -48444,6 +48570,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>total_hours_in_view</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>total_hours_on_board</name>
<definition_loaded>false</definition_loaded>
@@ -48465,6 +48612,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>total_jobs_in_view</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>total_jobs_on_board</name>
<definition_loaded>false</definition_loaded>
@@ -48507,6 +48675,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>total_lab_in_view</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>total_lab_on_board</name>
<definition_loaded>false</definition_loaded>
@@ -48549,6 +48738,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>total_lar_in_view</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>total_lar_on_board</name>
<definition_loaded>false</definition_loaded>
@@ -48724,6 +48934,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>tasks_in_view</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>tasks_on_board</name>
<definition_loaded>false</definition_loaded>
@@ -48766,6 +48997,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>total_amount_in_view</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>total_amount_on_board</name>
<definition_loaded>false</definition_loaded>
@@ -48808,6 +49060,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>total_hours_in_view</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>total_hours_on_board</name>
<definition_loaded>false</definition_loaded>
@@ -48829,6 +49102,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>total_jobs_in_view</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>total_jobs_on_board</name>
<definition_loaded>false</definition_loaded>
@@ -48871,6 +49165,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>total_lab_in_view</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>total_lab_on_board</name>
<definition_loaded>false</definition_loaded>
@@ -48913,6 +49228,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>total_lar_in_view</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>total_lar_on_board</name>
<definition_loaded>false</definition_loaded>
@@ -51761,6 +52097,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>production_not_production_status</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>production_over_time</name>
<definition_loaded>false</definition_loaded>
@@ -54225,6 +54582,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>created_by</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>description</name>
<definition_loaded>false</definition_loaded>
@@ -54487,6 +54865,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>related_items</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>remind_at</name>
<definition_loaded>false</definition_loaded>

View File

@@ -1,4 +1,4 @@
import { DatePicker } from "antd";
import { DatePicker, Space, TimePicker } from "antd";
import PropTypes from "prop-types";
import React, { useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
@@ -20,6 +20,7 @@ const DateTimePicker = ({
onlyFuture,
onlyToday,
isDateOnly = false,
isSeparatedTime = false,
bodyshop,
...restProps
}) => {
@@ -87,24 +88,57 @@ const DateTimePicker = ({
return (
<div onKeyDown={handleKeyDown} id={id} style={{ width: "100%" }}>
<DatePicker
showTime={
isDateOnly
? false
: {
format: "hh:mm a",
minuteStep: 15,
defaultValue: dayjs(dayjs(), "HH:mm:ss")
}
}
format={isDateOnly ? "MM/DD/YYYY" : "MM/DD/YYYY hh:mm a"}
value={value ? dayjs(value) : null}
onChange={handleChange}
placeholder={isDateOnly ? t("general.labels.date") : t("general.labels.datetime")}
onBlur={onBlur || handleBlur}
disabledDate={handleDisabledDate}
{...restProps}
/>
{isSeparatedTime && (
<Space direction="vertical" style={{ width: "100%" }}>
<DatePicker
showTime={false}
format="MM/DD/YYYY"
value={value ? dayjs(value) : null}
onChange={handleChange}
placeholder={t("general.labels.date")}
onBlur={handleBlur}
disabledDate={handleDisabledDate}
isDateOnly={true}
{...restProps}
/>
{value && (
<TimePicker
format="hh:mm a"
minuteStep={15}
defaultOpenValue={dayjs(value)
.hour(dayjs().hour())
.minute(Math.floor(dayjs().minute() / 15) * 15)
.second(0)}
onChange={(value) => {
handleChange(value);
onBlur();
}}
placeholder={t("general.labels.time")}
{...restProps}
/>
)}
</Space>
)}
{!isSeparatedTime && (
<DatePicker
showTime={
isDateOnly
? false
: {
format: "hh:mm a",
minuteStep: 15,
defaultValue: dayjs(dayjs(), "HH:mm:ss")
}
}
format={isDateOnly ? "MM/DD/YYYY" : "MM/DD/YYYY hh:mm a"}
value={value ? dayjs(value) : null}
onChange={handleChange}
placeholder={isDateOnly ? t("general.labels.date") : t("general.labels.datetime")}
onBlur={onBlur || handleBlur}
disabledDate={handleDisabledDate}
{...restProps}
/>
)}
</div>
);
};
@@ -116,7 +150,8 @@ DateTimePicker.propTypes = {
id: PropTypes.string,
onlyFuture: PropTypes.bool,
onlyToday: PropTypes.bool,
isDateOnly: PropTypes.bool
isDateOnly: PropTypes.bool,
isSeparatedTime: PropTypes.bool
};
export default connect(mapStateToProps, null)(DateTimePicker);

View File

@@ -118,8 +118,7 @@ export function JobLinesComponent({
...(record.critical ? { boxShadow: " -.5em 0 0 #FFC107" } : {})
}
}),
sortOrder: state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order,
ellipsis: true
sortOrder: state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order
},
{
title: t("joblines.fields.oem_partno"),

View File

@@ -45,7 +45,8 @@ export default function JobLineNotePopup({ jobline, disabled }) {
if (editing)
return (
<div>
<Input
<Input.TextArea
autoSize
autoFocus
suffix={loading ? <LoadingSpinner /> : null}
value={note}

View File

@@ -1,10 +1,10 @@
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import { Form, Input, InputNumber, Modal, Select, Switch } from "antd";
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import InputCurrency from "../form-items-formatted/currency-form-item.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import JoblinesPreset from "../job-lines-preset-button/job-lines-preset-button.component";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -61,7 +61,7 @@ export function JobLinesUpsertModalComponent({ bodyshop, open, jobLine, handleCa
]}
name="line_desc"
>
<Input />
<Input.TextArea autoSize />
</Form.Item>
<JoblinesPreset form={form} />
</LayoutFormRow>

View File

@@ -141,14 +141,16 @@ export function JobTotalsTableTotals({ bodyshop, job }) {
key: t("jobs.fields.ded_amt"),
total: job.job_totals.totals.custPayable.deductible
},
...(InstanceRenderManager({
imex: [{
key: t("jobs.fields.federal_tax_payable"),
total: job.job_totals.totals.custPayable.federal_tax
}],
...InstanceRenderManager({
imex: [
{
key: t("jobs.fields.federal_tax_payable"),
total: job.job_totals.totals.custPayable.federal_tax
}
],
rome: [],
promanager: "USE_ROME"
})),
}),
{
key: t("jobs.fields.other_amount_payable"),
total: job.job_totals.totals.custPayable.other_customer_amount
@@ -158,11 +160,32 @@ export function JobTotalsTableTotals({ bodyshop, job }) {
total: job.job_totals.totals.custPayable.dep_taxes
},
{
key: t("jobs.labels.total_cust_payable"),
total: job.job_totals.totals.custPayable.total,
bold: true
},
...(bodyshop.intellipay_config?.enable_cash_discount
? [
{
key: t("jobs.labels.total_cust_payable_cash_discount"),
total: job.job_totals.totals.custPayable.total,
bold: true
},
{
key: t("jobs.labels.total_cust_payable"),
total: Dinero(job.job_totals.totals.custPayable.total)
.add(
Dinero(job.job_totals.totals.custPayable.total).percentage(
bodyshop.intellipay_config?.cash_discount_percentage || 0
)
)
.toJSON(),
bold: true
}
]
: [
{
key: t("jobs.labels.total_cust_payable"),
total: job.job_totals.totals.custPayable.total,
bold: true
}
]),
{
key: t("jobs.labels.net_repairs"),
total: job.job_totals.totals.net_repairs,

View File

@@ -5,6 +5,7 @@ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import FormItemEmail from "../form-items-formatted/email-form-item.component";
import FormItemPhone, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component";
@@ -12,7 +13,6 @@ import Car from "../job-damage-visual/job-damage-visual.component";
import JobsDetailChangeEstimator from "../jobs-detail-change-estimator/jobs-detail-change-estimator.component";
import JobsDetailChangeFileHandler from "../jobs-detail-change-filehandler/jobs-detail-change-filehandler.component";
import FormRow from "../layout-form-row/layout-form-row.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
const mapStateToProps = createStructuredSelector({
jobRO: selectJobReadOnly,
@@ -185,6 +185,9 @@ export function JobsDetailGeneral({ bodyshop, jobRO, job, form }) {
<Form.Item label={t("jobs.fields.towin")} name="towin" valuePropName="checked">
<Switch disabled={jobRO} />
</Form.Item>
<Form.Item label={t("jobs.fields.tlos_ind")} name="tlos_ind" valuePropName="checked">
<Switch disabled={jobRO} />
</Form.Item>
</FormRow>
</Col>
<Col {...lossColDamage}>

View File

@@ -1,6 +1,5 @@
import { Button, Col, Form, Input, Row, Select, Space, Switch, Typography } from "antd";
import axios from "axios";
import dayjs from "../../utils/day";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -8,13 +7,14 @@ import { createStructuredSelector } from "reselect";
import { calculateScheduleLoad } from "../../redux/application/application.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { DateFormatter } from "../../utils/DateFormatter";
import dayjs from "../../utils/day";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
import EmailInput from "../form-items-formatted/email-form-item.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import ScheduleDayViewContainer from "../schedule-day-view/schedule-day-view.container";
import ScheduleExistingAppointmentsList from "../schedule-existing-appointments-list/schedule-existing-appointments-list.component";
import "./schedule-job-modal.scss";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
@@ -84,7 +84,7 @@ export function ScheduleJobModalComponent({
}
]}
>
<DateTimePicker onBlur={handleDateBlur} onlyFuture />
<DateTimePicker onBlur={handleDateBlur} onlyFuture isSeparatedTime />
</Form.Item>
<Form.Item
name="scheduled_completion"

View File

@@ -142,7 +142,7 @@ export function ShopInfoComponent({ bodyshop, form, saveLoading }) {
rome: [
{
key: "intellipay",
label: t("bodyshop.labels.intellipay"),
label: InstanceRenderManager({ rome: t("bodyshop.labels.romepay"), imex: t("bodyshop.labels.imexpay") }),
children: <ShopInfoIntellipay form={form} />
}
],

View File

@@ -676,7 +676,7 @@ export function ShopInfoGeneral({ form, bodyshop }) {
}
]}
>
<Input.TextArea rows={3} />
<Input.TextArea autoSize />
</Form.Item>
<Space wrap>
<DeleteFilled
@@ -737,7 +737,7 @@ export function ShopInfoGeneral({ form, bodyshop }) {
}
]}
>
<Input.TextArea rows={3} />
<Input.TextArea autoSize />
</Form.Item>
<Space wrap>
<DeleteFilled
@@ -1187,7 +1187,7 @@ export function ShopInfoGeneral({ form, bodyshop }) {
key={`${index}line_desc`}
name={[field.name, "line_desc"]}
>
<Input />
<Input.TextArea autoSize />
</Form.Item>
<Form.Item
label={t("joblines.fields.mod_lbr_ty")}
@@ -1330,7 +1330,7 @@ export function ShopInfoGeneral({ form, bodyshop }) {
}
]}
>
<Input />
<Input.TextArea autoSize />
</Form.Item>
<Space wrap>

View File

@@ -39,14 +39,13 @@ export function ShopInfoIntellipay({ bodyshop, form }) {
</Form.Item>
<Form.Item
label={t("bodyshop.fields.intellipay_config.cash_discount_percentage")}
valuePropName="checked"
dependencies={[["intellipay_config", "enable_cash_discount"]]}
name={["intellipay_config", "cash_discount_percentage"]}
rules={[
({ getFieldsValue }) => ({ required: form.getFieldValue(["intellipay_config", "enable_cash_discount"]) })
]}
>
<InputNumber min={0} max={100} precision={1} suffix='%'/>
<InputNumber min={0} max={100} precision={1} suffix="%" />
</Form.Item>
</LayoutFormRow>
</>

View File

@@ -81,14 +81,14 @@ export const logImEXEvent = (eventName, additionalParams, stateProp = null) => {
user: (state.user && state.user.currentUser && state.user.currentUser.email) || null,
...additionalParams
};
axios.post("/ioevent", {
useremail: (state.user && state.user.currentUser && state.user.currentUser.email) || null,
bodyshopid: (state.user && state.user.bodyshop && state.user.bodyshop.id) || null,
operationName: eventName,
variables: additionalParams,
dbevent: false,
env: `master-AIO|${import.meta.env.VITE_APP_GIT_SHA_DATE}`
});
// axios.post("/ioevent", {
// useremail: (state.user && state.user.currentUser && state.user.currentUser.email) || null,
// bodyshopid: (state.user && state.user.bodyshop && state.user.bodyshop.id) || null,
// operationName: eventName,
// variables: additionalParams,
// dbevent: false,
// env: `master-AIO|${import.meta.env.VITE_APP_GIT_SHA_DATE}`
// });
// console.log(
// "%c[Analytics]",
// "background-color: green ;font-weight:bold;",

View File

@@ -692,6 +692,7 @@ export const GET_JOB_BY_PK = gql`
tax_str_rt
tax_sub_rt
tax_tow_rt
tlos_ind
towin
towing_payable
unit_number

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -74,7 +74,7 @@ services:
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- SERVICES=ses,secretsmanager,cloudwatch,logs
- SERVICES=s3,ses,secretsmanager,cloudwatch,logs
- DEBUG=0
- AWS_ACCESS_KEY_ID=test
- AWS_SECRET_ACCESS_KEY=test
@@ -115,7 +115,8 @@ services:
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
aws --endpoint-url=http://localstack:4566 logs create-log-group --log-group-name development --region ca-central-1
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket imex-large-log --create-bucket-configuration LocationConstraint=ca-central-1
"
# Node App: The Main IMEX API
node-app:

View File

@@ -0,0 +1,10 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- CREATE INDEX jobs_search_gin_ro_number ON jobs USING GIN ((ro_number) gin_trgm_ops);
-- CREATE INDEX jobs_search_gin_ownrfn ON jobs USING GIN ((ownr_fn) gin_trgm_ops);
-- CREATE INDEX jobs_search_gin_clm_no ON jobs USING GIN ((clm_no) gin_trgm_ops);
-- CREATE INDEX jobs_search_gin_plate_no ON jobs USING GIN ((plate_no) gin_trgm_ops);
-- CREATE INDEX jobs_search_gin_v_make_desc ON jobs USING GIN (( v_make_desc) gin_trgm_ops);
-- CREATE INDEX jobs_search_gin_v_model_desc ON jobs USING GIN (( v_model_desc) gin_trgm_ops);
-- CREATE INDEX jobs_search_gin_ownr_ln ON jobs USING GIN (( ownr_ln) gin_trgm_ops);
-- CREATE INDEX jobs_search_gin_ownr_co_nm ON jobs USING GIN (( ownr_co_nm) gin_trgm_ops);

View File

@@ -0,0 +1,8 @@
CREATE INDEX jobs_search_gin_ro_number ON jobs USING GIN ((ro_number) gin_trgm_ops);
CREATE INDEX jobs_search_gin_ownrfn ON jobs USING GIN ((ownr_fn) gin_trgm_ops);
CREATE INDEX jobs_search_gin_clm_no ON jobs USING GIN ((clm_no) gin_trgm_ops);
CREATE INDEX jobs_search_gin_plate_no ON jobs USING GIN ((plate_no) gin_trgm_ops);
CREATE INDEX jobs_search_gin_v_make_desc ON jobs USING GIN (( v_make_desc) gin_trgm_ops);
CREATE INDEX jobs_search_gin_v_model_desc ON jobs USING GIN (( v_model_desc) gin_trgm_ops);
CREATE INDEX jobs_search_gin_ownr_ln ON jobs USING GIN (( ownr_ln) gin_trgm_ops);
CREATE INDEX jobs_search_gin_ownr_co_nm ON jobs USING GIN (( ownr_co_nm) gin_trgm_ops);

1297
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -21,6 +21,7 @@
"dependencies": {
"@aws-sdk/client-cloudwatch-logs": "^3.679.0",
"@aws-sdk/client-elasticache": "^3.675.0",
"@aws-sdk/client-s3": "^3.689.0",
"@aws-sdk/client-secrets-manager": "^3.675.0",
"@aws-sdk/client-ses": "^3.675.0",
"@aws-sdk/credential-provider-node": "^3.675.0",
@@ -63,7 +64,7 @@
"soap": "^1.1.5",
"socket.io": "^4.8.0",
"socket.io-adapter": "^2.5.5",
"ssh2-sftp-client": "^11.0.0",
"ssh2-sftp-client": "^10.0.3",
"twilio": "^4.23.0",
"uuid": "^10.0.0",
"winston": "^3.15.0",

View File

@@ -21,7 +21,7 @@ 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 { InstanceRegion } = require("./server/utils/instanceMgr");
const CLUSTER_RETRY_BASE_DELAY = 100;
const CLUSTER_RETRY_MAX_DELAY = 5000;
@@ -114,10 +114,7 @@ const applyRoutes = ({ app }) => {
*/
const getRedisNodesFromAWS = async () => {
const client = new ElastiCacheClient({
region: InstanceManager({
imex: "ca-central-1",
rome: "us-east-2"
})
region: InstanceRegion()
});
const params = {

View File

@@ -219,6 +219,11 @@ async function InsertPayment(oauthClient, qbo_realmId, req, payment, parentRef,
PaymentMethodRef: {
value: paymentMethods[payment.type]
},
PrivateNote: payment.memo
? payment.memo.length > 4000
? payment.memo.substring(0, 4000).trim()
: payment.memo.trim()
: "",
PaymentRefNum: payment.transactionid,
...(invoices && invoices.length === 1 && invoices[0]
? {

View File

@@ -25,15 +25,15 @@ const ftpSetup = {
port: process.env.AUTOHOUSE_PORT,
username: process.env.AUTOHOUSE_USER,
password: process.env.AUTOHOUSE_PASSWORD,
debug:
process.env.NODE_ENV !== "production"
? (message, ...data) => logger.log(message, "DEBUG", "api", null, data)
: () => {},
debug: (message, ...data) => logger.log(message, "DEBUG", "api", null, data),
algorithms: {
serverHostKey: ["ssh-rsa", "ssh-dss", "rsa-sha2-256", "rsa-sha2-512", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384"]
}
};
const allxmlsToUpload = [];
const allErrors = [];
exports.default = async (req, res) => {
// Only process if in production environment.
if (process.env.NODE_ENV !== "production") {
@@ -55,13 +55,12 @@ exports.default = async (req, res) => {
try {
logger.log("autohouse-start", "DEBUG", "api", null, null);
const allXMLResults = [];
const allErrors = [];
const { bodyshops } = await client.request(queries.GET_AUTOHOUSE_SHOPS); //Query for the List of Bodyshop Clients.
const specificShopIds = req.body.bodyshopIds; // ['uuid];
const { start, end, skipUpload } = req.body; //YYYY-MM-DD
const batchSize = 10;
const shopsToProcess =
specificShopIds?.length > 0 ? bodyshops.filter((shop) => specificShopIds.includes(shop.id)) : bodyshops;
logger.log("autohouse-shopsToProcess-generated", "DEBUG", "api", null, null);
@@ -70,18 +69,27 @@ exports.default = async (req, res) => {
logger.log("autohouse-shopsToProcess-empty", "DEBUG", "api", null, null);
return;
}
const batchPromises = [];
for (let i = 0; i < shopsToProcess.length; i += batchSize) {
const batch = shopsToProcess.slice(i, i + batchSize);
const batchPromise = (async () => {
await processBatch(batch, start, end);
await processShopData(shopsToProcess, start, end, skipUpload, allXMLResults, allErrors);
if (skipUpload) {
for (const xmlObj of allxmlsToUpload) {
fs.writeFileSync(`./logs/${xmlObj.filename}`, xmlObj.xml);
}
} else {
await uploadViaSFTP(allxmlsToUpload);
}
})();
batchPromises.push(batchPromise);
}
await Promise.all(batchPromises);
await sendServerEmail({
subject: `Autohouse Report ${moment().format("MM-DD-YY")}`,
text: `Errors:\n${JSON.stringify(allErrors, null, 2)}\n\nUploaded:\n${JSON.stringify(
allXMLResults.map((x) => ({
imexshopid: x.imexshopid,
filename: x.filename,
count: x.count,
result: x.result
})),
allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count, result: x.result })),
null,
2
)}`
@@ -93,8 +101,8 @@ exports.default = async (req, res) => {
}
};
async function processShopData(shopsToProcess, start, end, skipUpload, allXMLResults, allErrors) {
for (const bodyshop of shopsToProcess) {
async function processBatch(batch, start, end) {
for (const bodyshop of batch) {
const erroredJobs = [];
try {
logger.log("autohouse-start-shop-extract", "DEBUG", "api", bodyshop.id, {
@@ -124,27 +132,12 @@ async function processShopData(shopsToProcess, start, end, skipUpload, allXMLRes
});
}
const xmlObj = {
bodyshopid: bodyshop.id,
imexshopid: bodyshop.imexshopid,
xml: builder.create({}, autoHouseObject).end({ allowEmptyTags: true }),
filename: `IM_${bodyshop.autohouseid}_${moment().format("DDMMYYYY_HHMMss")}.xml`,
count: autoHouseObject.AutoHouseExport.RepairOrder.length
};
const ret = builder.create({}, autoHouseObject).end({ allowEmptyTags: true });
if (skipUpload) {
fs.writeFileSync(`./logs/${xmlObj.filename}`, xmlObj.xml);
} else {
await uploadViaSFTP(xmlObj);
}
allXMLResults.push({
bodyshopid: bodyshop.id,
imexshopid: bodyshop.imexshopid,
autohouseid: bodyshop.autohouseid,
count: xmlObj.count,
filename: xmlObj.filename,
result: xmlObj.result
allxmlsToUpload.push({
count: autoHouseObject.AutoHouseExport.RepairOrder.length,
xml: ret,
filename: `IM_${bodyshop.autohouseid}_${moment().format("DDMMYYYY_HHMMss")}.xml`
});
logger.log("autohouse-end-shop-extract", "DEBUG", "api", bodyshop.id, {
@@ -176,35 +169,33 @@ async function processShopData(shopsToProcess, start, end, skipUpload, allXMLRes
}
}
async function uploadViaSFTP(xmlObj) {
async function uploadViaSFTP(allxmlsToUpload) {
const sftp = new Client();
sftp.on("error", (errors) =>
logger.log("autohouse-sftp-connection-error", "ERROR", "api", xmlObj.bodyshopid, {
error: errors.message,
stack: errors.stack
})
logger.log("autohouse-sftp-connection-error", "ERROR", "api", null, { error: errors.message, stack: errors.stack })
);
try {
//Connect to the FTP and upload all.
await sftp.connect(ftpSetup);
try {
xmlObj.result = await sftp.put(Buffer.from(xmlObj.xml), `${xmlObj.filename}`);
logger.log("autohouse-sftp-upload", "DEBUG", "api", xmlObj.bodyshopid, {
imexshopid: xmlObj.imexshopid,
filename: xmlObj.filename,
result: xmlObj.result
});
} catch (error) {
logger.log("autohouse-sftp-upload-error", "ERROR", "api", xmlObj.bodyshopid, {
filename: xmlObj.filename,
error: error.message,
stack: error.stack
});
throw error;
for (const xmlObj of allxmlsToUpload) {
try {
xmlObj.result = await sftp.put(Buffer.from(xmlObj.xml), `${xmlObj.filename}`);
logger.log("autohouse-sftp-upload", "DEBUG", "api", null, {
filename: xmlObj.filename,
result: xmlObj.result
});
} catch (error) {
logger.log("autohouse-sftp-upload-error", "ERROR", "api", null, {
filename: xmlObj.filename,
error: error.message,
stack: error.stack
});
throw error;
}
}
} catch (error) {
logger.log("autohouse-sftp-error", "ERROR", "api", xmlObj.bodyshopid, { error: error.message, stack: error.stack });
logger.log("autohouse-sftp-error", "ERROR", "api", null, { error: error.message, stack: error.stack });
throw error;
} finally {
sftp.end();

View File

@@ -17,9 +17,7 @@ const ftpSetup = {
port: process.env.CHATTER_PORT,
username: process.env.CHATTER_USER,
privateKey: null,
debug: process.env.NODE_ENV !== "production"
? (message, ...data) => logger.log(message, "DEBUG", "api", null, data)
: () => {},
debug: (message, ...data) => logger.log(message, "DEBUG", "api", null, data),
algorithms: {
serverHostKey: ["ssh-rsa", "ssh-dss", "rsa-sha2-256", "rsa-sha2-512", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384"]
}

View File

@@ -24,15 +24,15 @@ const ftpSetup = {
port: process.env.CLAIMSCORP_PORT,
username: process.env.CLAIMSCORP_USER,
password: process.env.CLAIMSCORP_PASSWORD,
debug:
process.env.NODE_ENV !== "production"
? (message, ...data) => logger.log(message, "DEBUG", "api", null, data)
: () => {},
debug: (message, ...data) => logger.log(message, "DEBUG", "api", null, data),
algorithms: {
serverHostKey: ["ssh-rsa", "ssh-dss", "rsa-sha2-256", "rsa-sha2-512", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384"]
}
};
const allxmlsToUpload = [];
const allErrors = [];
exports.default = async (req, res) => {
// Only process if in production environment.
if (process.env.NODE_ENV !== "production") {
@@ -54,13 +54,12 @@ exports.default = async (req, res) => {
try {
logger.log("claimscorp-start", "DEBUG", "api", null, null);
const allXMLResults = [];
const allErrors = [];
const { bodyshops } = await client.request(queries.GET_CLAIMSCORP_SHOPS); //Query for the List of Bodyshop Clients.
const specificShopIds = req.body.bodyshopIds; // ['uuid];
const { start, end, skipUpload } = req.body; //YYYY-MM-DD
const batchSize = 10;
const shopsToProcess =
specificShopIds?.length > 0 ? bodyshops.filter((shop) => specificShopIds.includes(shop.id)) : bodyshops;
logger.log("claimscorp-shopsToProcess-generated", "DEBUG", "api", null, null);
@@ -69,18 +68,27 @@ exports.default = async (req, res) => {
logger.log("claimscorp-shopsToProcess-empty", "DEBUG", "api", null, null);
return;
}
const batchPromises = [];
for (let i = 0; i < shopsToProcess.length; i += batchSize) {
const batch = shopsToProcess.slice(i, i + batchSize);
const batchPromise = (async () => {
await processBatch(batch, start, end);
await processShopData(shopsToProcess, start, end, skipUpload, allXMLResults, allErrors);
if (skipUpload) {
for (const xmlObj of allxmlsToUpload) {
fs.writeFileSync(`./logs/${xmlObj.filename}`, xmlObj.xml);
}
} else {
await uploadViaSFTP(allxmlsToUpload);
}
})();
batchPromises.push(batchPromise);
}
await Promise.all(batchPromises);
await sendServerEmail({
subject: `ClaimsCorp Report ${moment().format("MM-DD-YY")}`,
text: `Errors:\n${JSON.stringify(allErrors, null, 2)}\n\nUploaded:\n${JSON.stringify(
allXMLResults.map((x) => ({
imexshopid: x.imexshopid,
filename: x.filename,
count: x.count,
result: x.result
})),
allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count, result: x.result })),
null,
2
)}`
@@ -92,8 +100,8 @@ exports.default = async (req, res) => {
}
};
async function processShopData(shopsToProcess, start, end, skipUpload, allXMLResults, allErrors) {
for (const bodyshop of shopsToProcess) {
async function processBatch(batch, start, end) {
for (const bodyshop of batch) {
const erroredJobs = [];
try {
logger.log("claimscorp-start-shop-extract", "DEBUG", "api", bodyshop.id, {
@@ -127,27 +135,12 @@ async function processShopData(shopsToProcess, start, end, skipUpload, allXMLRes
});
}
const xmlObj = {
bodyshopid: bodyshop.id,
imexshopid: bodyshop.imexshopid,
xml: builder.create({}, claimsCorpObject).end({ allowEmptyTags: true }),
filename: `${bodyshop.claimscorpid}-${moment().format("YYYYMMDDTHHMMss")}.xml`,
count: claimsCorpObject.DataFeed.ShopInfo.RO.length
};
const ret = builder.create({}, claimsCorpObject).end({ allowEmptyTags: true });
if (skipUpload) {
fs.writeFileSync(`./logs/${xmlObj.filename}`, xmlObj.xml);
} else {
await uploadViaSFTP(xmlObj);
}
allXMLResults.push({
bodyshopid: bodyshop.id,
imexshopid: bodyshop.imexshopid,
claimscorpid: bodyshop.claimscorpid,
count: xmlObj.count,
filename: xmlObj.filename,
result: xmlObj.result
allxmlsToUpload.push({
count: claimsCorpObject.DataFeed.ShopInfo.RO.length,
xml: ret,
filename: `${bodyshop.claimscorpid}-${moment().format("YYYYMMDDTHHMMss")}.xml`
});
logger.log("claimscorp-end-shop-extract", "DEBUG", "api", bodyshop.id, {
@@ -179,38 +172,33 @@ async function processShopData(shopsToProcess, start, end, skipUpload, allXMLRes
}
}
async function uploadViaSFTP(xmlObj) {
async function uploadViaSFTP(allxmlsToUpload) {
const sftp = new Client();
sftp.on("error", (errors) =>
logger.log("claimscorp-sftp-connection-error", "ERROR", "api", xmlObj.bodyshopid, {
error: errors.message,
stack: errors.stack
})
logger.log("claimscorp-sftp-connection-error", "ERROR", "api", null, { error: errors.message, stack: errors.stack })
);
try {
//Connect to the FTP and upload all.
await sftp.connect(ftpSetup);
try {
xmlObj.result = await sftp.put(Buffer.from(xmlObj.xml), `${xmlObj.filename}`);
logger.log("claimscorp-sftp-upload", "DEBUG", "api", xmlObj.bodyshopid, {
imexshopid: xmlObj.imexshopid,
filename: xmlObj.filename,
result: xmlObj.result
});
} catch (error) {
logger.log("claimscorp-sftp-upload-error", "ERROR", "api", xmlObj.bodyshopid, {
filename: xmlObj.filename,
error: error.message,
stack: error.stack
});
throw error;
for (const xmlObj of allxmlsToUpload) {
try {
xmlObj.result = await sftp.put(Buffer.from(xmlObj.xml), `${xmlObj.filename}`);
logger.log("claimscorp-sftp-upload", "DEBUG", "api", null, {
filename: xmlObj.filename,
result: xmlObj.result
});
} catch (error) {
logger.log("claimscorp-sftp-upload-error", "ERROR", "api", null, {
filename: xmlObj.filename,
error: error.message,
stack: error.stack
});
throw error;
}
}
} catch (error) {
logger.log("claimscorp-sftp-error", "ERROR", "api", xmlObj.bodyshopid, {
error: error.message,
stack: error.stack
});
logger.log("claimscorp-sftp-error", "ERROR", "api", null, { error: error.message, stack: error.stack });
throw error;
} finally {
sftp.end();

View File

@@ -23,15 +23,15 @@ const ftpSetup = {
port: process.env.KAIZEN_PORT,
username: process.env.KAIZEN_USER,
password: process.env.KAIZEN_PASSWORD,
debug:
process.env.NODE_ENV !== "production"
? (message, ...data) => logger.log(message, "DEBUG", "api", null, data)
: () => {},
debug: (message, ...data) => logger.log(message, "DEBUG", "api", null, data),
algorithms: {
serverHostKey: ["ssh-rsa", "ssh-dss", "rsa-sha2-256", "rsa-sha2-512", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384"]
}
};
const allxmlsToUpload = [];
const allErrors = [];
exports.default = async (req, res) => {
// Only process if in production environment.
if (process.env.NODE_ENV !== "production") {
@@ -53,13 +53,12 @@ exports.default = async (req, res) => {
try {
logger.log("kaizen-start", "DEBUG", "api", null, null);
const allXMLResults = [];
const allErrors = [];
const { bodyshops } = await client.request(queries.GET_KAIZEN_SHOPS, { imexshopid: kaizenShopsIDs }); //Query for the List of Bodyshop Clients.
const specificShopIds = req.body.bodyshopIds; // ['uuid];
const { start, end, skipUpload } = req.body; //YYYY-MM-DD
const batchSize = 10;
const shopsToProcess =
specificShopIds?.length > 0 ? bodyshops.filter((shop) => specificShopIds.includes(shop.id)) : bodyshops;
logger.log("kaizen-shopsToProcess-generated", "DEBUG", "api", null, null);
@@ -68,18 +67,27 @@ exports.default = async (req, res) => {
logger.log("kaizen-shopsToProcess-empty", "DEBUG", "api", null, null);
return;
}
const batchPromises = [];
for (let i = 0; i < shopsToProcess.length; i += batchSize) {
const batch = shopsToProcess.slice(i, i + batchSize);
const batchPromise = (async () => {
await processBatch(batch, start, end);
await processShopData(shopsToProcess, start, end, skipUpload, allXMLResults, allErrors);
if (skipUpload) {
for (const xmlObj of allxmlsToUpload) {
fs.writeFileSync(`./logs/${xmlObj.filename}`, xmlObj.xml);
}
} else {
await uploadViaSFTP(allxmlsToUpload);
}
})();
batchPromises.push(batchPromise);
}
await Promise.all(batchPromises);
await sendServerEmail({
subject: `Kaizen Report ${moment().format("MM-DD-YY")}`,
text: `Errors:\n${JSON.stringify(allErrors, null, 2)}\n\nUploaded:\n${JSON.stringify(
allXMLResults.map((x) => ({
imexshopid: x.imexshopid,
filename: x.filename,
count: x.count,
result: x.result
})),
allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count, result: x.result })),
null,
2
)}`
@@ -91,8 +99,8 @@ exports.default = async (req, res) => {
}
};
async function processShopData(shopsToProcess, start, end, skipUpload, allXMLResults, allErrors) {
for (const bodyshop of shopsToProcess) {
async function processBatch(batch, start, end) {
for (const bodyshop of batch) {
const erroredJobs = [];
try {
logger.log("kaizen-start-shop-extract", "DEBUG", "api", bodyshop.id, {
@@ -125,26 +133,12 @@ async function processShopData(shopsToProcess, start, end, skipUpload, allXMLRes
});
}
const xmlObj = {
bodyshopid: bodyshop.id,
imexshopid: bodyshop.imexshopid,
xml: builder.create({}, kaizenObject).end({ allowEmptyTags: true }),
filename: `${bodyshop.shopname}-${moment().format("YYYYMMDDTHHMMss")}.xml`,
count: kaizenObject.DataFeed.ShopInfo.Jobs.length
};
const ret = builder.create({}, kaizenObject).end({ allowEmptyTags: true });
if (skipUpload) {
fs.writeFileSync(`./logs/${xmlObj.filename}`, xmlObj.xml);
} else {
await uploadViaSFTP(xmlObj);
}
allXMLResults.push({
bodyshopid: bodyshop.id,
imexshopid: bodyshop.imexshopid,
count: xmlObj.count,
filename: xmlObj.filename,
result: xmlObj.result
allxmlsToUpload.push({
count: kaizenObject.DataFeed.ShopInfo.Jobs.length,
xml: ret,
filename: `${bodyshop.shopname}-${moment().format("YYYYMMDDTHHMMss")}.xml`
});
logger.log("kaizen-end-shop-extract", "DEBUG", "api", bodyshop.id, {
@@ -176,35 +170,33 @@ async function processShopData(shopsToProcess, start, end, skipUpload, allXMLRes
}
}
async function uploadViaSFTP(xmlObj) {
async function uploadViaSFTP(allxmlsToUpload) {
const sftp = new Client();
sftp.on("error", (errors) =>
logger.log("kaizen-sftp-connection-error", "ERROR", "api", xmlObj.bodyshopid, {
error: errors.message,
stack: errors.stack
})
logger.log("kaizen-sftp-connection-error", "ERROR", "api", null, { error: errors.message, stack: errors.stack })
);
try {
//Connect to the FTP and upload all.
await sftp.connect(ftpSetup);
try {
xmlObj.result = await sftp.put(Buffer.from(xmlObj.xml), `${xmlObj.filename}`);
logger.log("kaizen-sftp-upload", "DEBUG", "api", xmlObj.bodyshopid, {
imexshopid: xmlObj.imexshopid,
filename: xmlObj.filename,
result: xmlObj.result
});
} catch (error) {
logger.log("kaizen-sftp-upload-error", "ERROR", "api", xmlObj.bodyshopid, {
filename: xmlObj.filename,
error: error.message,
stack: error.stack
});
throw error;
for (const xmlObj of allxmlsToUpload) {
try {
xmlObj.result = await sftp.put(Buffer.from(xmlObj.xml), `${xmlObj.filename}`);
logger.log("kaizen-sftp-upload", "DEBUG", "api", null, {
filename: xmlObj.filename,
result: xmlObj.result
});
} catch (error) {
logger.log("kaizen-sftp-upload-error", "ERROR", "api", null, {
filename: xmlObj.filename,
error: error.message,
stack: error.stack
});
throw error;
}
}
} catch (error) {
logger.log("kaizen-sftp-error", "ERROR", "api", xmlObj.bodyshopid, { error: error.message, stack: error.stack });
logger.log("kaizen-sftp-error", "ERROR", "api", null, { error: error.message, stack: error.stack });
throw error;
} finally {
sftp.end();

View File

@@ -1,6 +1,6 @@
const { isString, isEmpty } = require("lodash");
const { defaultProvider } = require("@aws-sdk/credential-provider-node");
const { default: InstanceManager } = require("../utils/instanceMgr");
const { InstanceRegion } = require("../utils/instanceMgr");
const aws = require("@aws-sdk/client-ses");
const nodemailer = require("nodemailer");
const logger = require("../utils/logger");
@@ -10,12 +10,7 @@ const isLocal = isString(process.env?.LOCALSTACK_HOSTNAME) && !isEmpty(process.e
const sesConfig = {
apiVersion: "latest",
credentials: defaultProvider(),
region: isLocal
? "ca-central-1"
: InstanceManager({
imex: "ca-central-1",
rome: "us-east-2"
})
region: InstanceRegion()
};
if (isLocal) {

View File

@@ -17,12 +17,10 @@ require("dotenv").config({
const domain = process.env.NODE_ENV ? "secure" : "test";
const { SecretsManagerClient, GetSecretValueCommand } = require("@aws-sdk/client-secrets-manager");
const { InstanceRegion } = require("../utils/instanceMgr");
const client = new SecretsManagerClient({
region: InstanceManager({
imex: "ca-central-1",
rome: "us-east-2"
})
region: InstanceRegion()
});
const gqlClient = require("../graphql-client/graphql-client").client;

View File

@@ -15,7 +15,7 @@ const { canvastest } = require("../render/canvas-handler");
const { alertCheck } = require("../alerts/alertcheck");
//Test route to ensure Express is responding.
router.get("/test", async function (req, res) {
router.get("/test", eventAuthorizationMiddleware, async function (req, res) {
const commit = require("child_process").execSync("git rev-parse --short HEAD");
// console.log(app.get('trust proxy'));
// console.log("remoteAddress", req.socket.remoteAddress);
@@ -32,6 +32,32 @@ router.get("/test", async function (req, res) {
res.status(200).send(`OK - ${commit}`);
});
router.get("/test-logs", eventAuthorizationMiddleware, (req, res) => {
const { logger } = req;
// // Test 1: Log with a message that exceeds the size limit, triggering an upload to S3.
const largeMessage = "A".repeat(256 * 1024 + 1); // Message larger than the log size limit
logger.log(largeMessage, "error", "user123", null, { detail: "large log entry" });
// Test 2: Log with a message that is within the size limit, should log directly using winston.
const smallMessage = "A small log message";
logger.log(smallMessage, "info", "user123", null, { detail: "small log entry" });
// Test 3: Log with the `upload` flag set to `true`, forcing the log to be uploaded to S3.
logger.log(
"This log will be uploaded to S3 regardless of size",
"warning",
"user123",
null,
{ detail: "upload log" },
true
);
// Test 4: Log with a message that doesn't exceed the size limit and doesn't require an upload.
logger.log("Normal log entry", "debug", "user123", { id: 4 }, { detail: "normal log entry" });
return res.status(500).send("Logs tested.");
});
// Search
router.post("/search", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, os.search);
router.post("/opensearch", eventAuthorizationMiddleware, os.handler);

View File

@@ -44,4 +44,10 @@ function InstanceManager({ args, instance, debug, executeFunction, rome, promana
return propToReturn === undefined ? null : propToReturn;
}
exports.InstanceRegion = () =>
InstanceManager({
imex: "ca-central-1",
rome: "us-east-2"
});
exports.default = InstanceManager;

View File

@@ -9,6 +9,9 @@ const winston = require("winston");
const WinstonCloudWatch = require("winston-cloudwatch");
const { isString, isEmpty } = require("lodash");
const { networkInterfaces, hostname } = require("node:os");
const { uploadFileToS3 } = require("./s3");
const { v4 } = require("uuid");
const { InstanceRegion } = require("./instanceMgr");
const LOG_LEVELS = {
error: { level: 0, name: "error" },
@@ -20,6 +23,30 @@ const LOG_LEVELS = {
silly: { level: 6, name: "silly" }
};
const LOG_LENGTH_LIMIT = 256 * 1024; // 256KB
const S3_BUCKET_NAME = InstanceManager({
imex: "imex-large-log",
rome: "rome-large-log"
});
const region = InstanceRegion();
const estimateLogSize = (logEntry) => {
let estimatedSize = 0;
for (const key in logEntry) {
if (logEntry.hasOwnProperty(key)) {
const value = logEntry[key];
if (value === undefined || value === null) {
estimatedSize += key.length; // Only count the key length if value is undefined or null
} else {
estimatedSize += key.length + (typeof value === "string" ? value.length : JSON.stringify(value).length);
}
}
}
return estimatedSize;
};
const normalizeLevel = (level) => (level ? level.toLowerCase() : LOG_LEVELS.debug.name);
const createLogger = () => {
@@ -30,10 +57,7 @@ const createLogger = () => {
const winstonCloudwatchTransportDefaults = {
logGroupName: logGroupName,
awsOptions: {
region: InstanceManager({
imex: "ca-central-1",
rome: "us-east-2"
})
region
},
jsonMessage: true
};
@@ -124,15 +148,66 @@ const createLogger = () => {
);
}
const log = (message, type, user, record, meta) => {
winstonLogger.log({
const log = (message, type, user, record, meta, upload) => {
const logEntry = {
level: normalizeLevel(type),
message,
user,
record,
hostname: internalHostname,
meta
});
};
const uploadLogToS3 = (logEntry, message, type, user) => {
const uniqueId = v4();
const dateTimeString = new Date().toISOString().replace(/:/g, "-");
const envName = process.env?.NODE_ENV ? process.env.NODE_ENV : "";
const logStreamName = `${envName}-${internalHostname}-${dateTimeString}-${uniqueId}.json`;
const logString = JSON.stringify(logEntry);
const webPath = isLocal
? `https://${S3_BUCKET_NAME}.s3.localhost.localstack.cloud:4566/${logStreamName}`
: `https://${S3_BUCKET_NAME}.s3.${region}.amazonaws.com/${logStreamName}`;
uploadFileToS3({ bucketName: S3_BUCKET_NAME, key: logStreamName, content: logString })
.then(() => {
log("A log file has been uploaded to S3", "info", "S3", null, {
logStreamName,
webPath,
message: message?.slice(0, 200),
type,
user
});
})
.catch((err) => {
log("Error in S3 Upload", "error", "S3", null, {
logStreamName,
webPath,
message: message?.slice(0, 100),
type,
user,
errorMessage: err?.message?.slice(0, 100)
});
});
};
const checkAndUploadLog = () => {
const estimatedSize = estimateLogSize(logEntry);
if (estimatedSize > LOG_LENGTH_LIMIT * 0.9 || estimatedSize > LOG_LENGTH_LIMIT) {
uploadLogToS3(logEntry, message, type, user);
return true;
}
return false;
};
// Upload log immediately if upload is true, otherwise check the log size.
if (upload) {
uploadLogToS3(logEntry, message, type, user);
return;
}
if (checkAndUploadLog()) return;
winstonLogger.log(logEntry);
};
return {

109
server/utils/s3.js Normal file
View File

@@ -0,0 +1,109 @@
const {
S3Client,
PutObjectCommand,
GetObjectCommand,
ListObjectsV2Command,
DeleteObjectCommand,
CopyObjectCommand
} = require("@aws-sdk/client-s3");
const { defaultProvider } = require("@aws-sdk/credential-provider-node");
const { InstanceRegion } = require("./instanceMgr");
const { isString, isEmpty } = require("lodash");
const createS3Client = () => {
const S3Options = {
region: InstanceRegion(),
credentials: defaultProvider()
};
const isLocal = isString(process.env?.LOCALSTACK_HOSTNAME) && !isEmpty(process.env?.LOCALSTACK_HOSTNAME);
if (isLocal) {
S3Options.endpoint = `http://${process.env.LOCALSTACK_HOSTNAME}:4566`;
S3Options.forcePathStyle = true; // Needed for LocalStack to avoid bucket name as hostname
}
const s3Client = new S3Client(S3Options);
/**
* Uploads a file to the specified S3 bucket and key.
*/
const uploadFileToS3 = async ({ bucketName, key, content, contentType }) => {
const params = {
Bucket: bucketName,
Key: key,
Body: content,
ContentType: contentType ?? "application/json"
};
const command = new PutObjectCommand(params);
return await s3Client.send(command);
};
/**
* Downloads a file from the specified S3 bucket and key.
*/
const downloadFileFromS3 = async ({ bucketName, key }) => {
const params = { Bucket: bucketName, Key: key };
const command = new GetObjectCommand(params);
const data = await s3Client.send(command);
return data.Body;
};
/**
* Lists objects in the specified S3 bucket.
*/
const listFilesInS3Bucket = async (bucketName, prefix = "") => {
const params = { Bucket: bucketName, Prefix: prefix };
const command = new ListObjectsV2Command(params);
const data = await s3Client.send(command);
return data.Contents || [];
};
/**
* Deletes a file from the specified S3 bucket and key.
*/
const deleteFileFromS3 = async ({ bucketName, key }) => {
const params = { Bucket: bucketName, Key: key };
const command = new DeleteObjectCommand(params);
return await s3Client.send(command);
};
/**
* Copies a file within S3 from a source bucket/key to a destination bucket/key.
*/
const copyFileInS3 = async ({ sourceBucket, sourceKey, destinationBucket, destinationKey }) => {
const params = {
CopySource: `/${sourceBucket}/${sourceKey}`,
Bucket: destinationBucket,
Key: destinationKey
};
const command = new CopyObjectCommand(params);
return await s3Client.send(command);
};
/**
* Checks if a file exists in the specified S3 bucket and key.
*/
const fileExistsInS3 = async ({ bucketName, key }) => {
try {
await downloadFileFromS3({ bucketName, key });
return true;
} catch (error) {
if (error.name === "NoSuchKey" || error.name === "NotFound") {
return false;
}
throw error;
}
};
return {
uploadFileToS3,
downloadFileFromS3,
listFilesInS3Bucket,
deleteFileFromS3,
copyFileInS3,
fileExistsInS3,
...s3Client
};
};
module.exports = createS3Client();

View File

@@ -155,10 +155,17 @@ function createJsonEvent(socket, level, message, json) {
message
});
}
logger.log("ws-log-event-json", level, socket.user.email, socket.recordid, {
wsmessage: message,
json
});
logger.log(
"ws-log-event-json",
level,
socket.user.email,
socket.recordid,
{
wsmessage: message,
json
},
true
);
if (socket.logEvents && isArray(socket.logEvents)) {
socket.logEvents.push({
@@ -189,7 +196,8 @@ function createXmlEvent(socket, xml, message, isError = false) {
{
wsmessage: message,
xml
}
},
true
);
if (socket.logEvents && isArray(socket.logEvents)) {