Compare commits

...

105 Commits

Author SHA1 Message Date
Allan Carr
02a49efbea IO-2762 Return from Bill Reference in Parts Return Drawer
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-04-22 14:14:54 -07:00
Patrick Fic
1620b94a7b Merged in release/2024-04-19 (pull request #1414)
Resolve schedule header display.
2024-04-15 20:47:48 +00:00
Patrick Fic
81f94eac6c Resolve schedule header display. 2024-04-15 13:47:19 -07:00
Allan Carr
ec7509670d Merged in release/2024-04-12 (pull request #1413)
Release/2024 04 12

Approved-by: Dave Richer
2024-04-12 17:08:21 +00:00
Allan Carr
0acfd3c4b1 Merged in feature/IO-2609-Calendar-BPT-HRS (pull request #1409)
IO-2609 Fix Spelling Mistake in object name
2024-04-11 21:39:06 +00:00
Allan Carr
bfc4cb1ad9 IO-2609 Fix Spelling Mistake in object name
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-04-11 14:38:08 -07:00
Allan Carr
1f42be2e54 Merged in feature/IO-2753-Qty-Parts-Order-Modal (pull request #1405)
IO-2753 Parts Order/Return Quantity restrict to above 0
2024-04-11 21:22:37 +00:00
Allan Carr
30d344af6b Merged in feature/IO-2752-BO-ETA-Jobline-Expander (pull request #1404)
IO-2752 BO ETA Jobline Expander
2024-04-11 21:22:19 +00:00
Allan Carr
3ca989fd8c Merged in release/2024-04-05 (pull request #1401)
Release/2024 04 05

Approved-by: Dave Richer
2024-04-11 21:22:03 +00:00
Allan Carr
ce2086a480 IO-2753 Parts Order/Return Quantity restrict to above 0
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-04-11 12:43:14 -07:00
Allan Carr
63f7106d2b IO-2752 BO ETA Jobline Expander
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-04-11 11:55:55 -07:00
Allan Carr
e75d8d1874 Merged in feature/IO-2609-Calendar-BPT-HRS (pull request #1402)
IO-2609 Body & Refinish Times included in Calendar View

Approved-by: Dave Richer
2024-04-11 16:27:02 +00:00
Allan Carr
07bf84ed69 IO-2609 Body & Refinish Times included in Calendar View
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-04-09 11:55:20 -07:00
Allan Carr
c2cc7b1e9e Merged in feature/IO-2731-Payment-Edit (pull request #1399)
IO-2731 Payment Edit
2024-04-08 19:56:43 +00:00
Allan Carr
fe55eccbf9 Merged in feature/IO-2749-Parts-Return-Pass-Jobs-Data (pull request #1398)
IO-2749 Pass Jobs data from Parts Return to Parts Order Modal
2024-04-08 19:55:49 +00:00
Allan Carr
88a71dd647 IO-2731 Payment Edit
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-04-08 12:23:04 -07:00
Allan Carr
c3b395c99e IO-2749 Pass Jobs data from Parts Return to Parts Order Modal
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-04-08 09:53:20 -07:00
Allan Carr
33d5d9b462 Merged in feature/IO-2568-Payment-Modal-Button-Spacing (pull request #1396)
IO-2568 Button Padding in Print Center Label Modal
2024-04-05 19:03:09 +00:00
Allan Carr
b5a371d0cf IO-2568 Button Padding in Print Center Label Modal
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-04-05 12:01:26 -07:00
Allan Carr
3b35f38ad5 Merged in feature/IO-2750-Missing-Jobline-Fields (pull request #1393)
IO-2750 Missing Mutation return fields
2024-04-05 15:55:13 +00:00
Allan Carr
1f5c1b9658 IO-2750 Missing Mutation return fields
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-04-04 17:00:30 -07:00
Patrick Fic
37196e65c3 Add schema changes for RO Guard to bodyshop table. 2024-04-03 09:46:52 -07:00
Allan Carr
b20c605c85 Merged in feature/IO-2568-Payment-Modal-Button-Spacing (pull request #1384)
IO-2568 Payment Modal Button Spacing

Approved-by: Dave Richer
2024-04-02 19:03:49 +00:00
Allan Carr
d8ac708536 Merged in feature/IO-2552-PVRT-Button-Spacing (pull request #1386)
IO-2552 PVRT Button Spacing and Alignment

Approved-by: Dave Richer
2024-04-02 19:03:36 +00:00
Allan Carr
8a32fe50f3 Merged in feature/IO-2730-Bill-Search-Result-Align (pull request #1383)
IO-2730 Bill Search Result Align

Approved-by: Dave Richer
2024-04-02 19:03:06 +00:00
Allan Carr
7897a490bd Merged in feature/IO-2563-Repair-Line-Expander-Bills-Transaltion (pull request #1385)
IO-2563 Repair LIne Expander Bills Translation

Approved-by: Dave Richer
2024-04-02 19:02:50 +00:00
Allan Carr
17d73fc6d7 Merged in feature/IO-2553-Edit-CC-Unsaved-Changes (pull request #1388)
IO-2553 Unsaved Changes on Edit CC

Approved-by: Dave Richer
2024-04-02 19:02:28 +00:00
Allan Carr
7d1910086e IO-2553 Unsaved Changes on Edit CC
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-04-02 10:39:41 -07:00
Allan Carr
817c41afb9 IO-2552 PVRT Button Spacing and Alignment
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-04-01 14:27:25 -07:00
Allan Carr
04315a9045 IO-2563 Repair LIne Expander Bills Translation
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-04-01 14:07:58 -07:00
Allan Carr
d0871ffe21 IO-2568 Payment Modal Button Spacing
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-04-01 13:52:25 -07:00
Allan Carr
dca587d6e0 IO-2730 Bill Search Result Align
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-04-01 13:20:30 -07:00
Allan Carr
19ec4cb021 Merged in release/2024-03-28 (pull request #1382)
Release/2024 03 28
2024-03-28 21:15:25 +00:00
Allan Carr
346e82bdbc Merged in feature/IO-2702-Limit-Production-Colors-to-Production-Statuses (pull request #1380)
IO-2702 Adjust concat
2024-03-28 20:24:19 +00:00
Allan Carr
fd9575e5a5 IO-2702 Adjust concat
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-28 13:25:13 -07:00
Patrick Fic
af6bc0fb5a Resolve hasura API url again. 2024-03-28 10:23:22 -07:00
Allan Carr
d0e289757f Merged in feature/IO-2705-Insurance-Co-ID (pull request #1375)
IO-2705 Insurance Co Id and correct Select Insurance and Layout for concistency

Approved-by: Dave Richer
2024-03-28 16:59:52 +00:00
Allan Carr
becc54f7b3 Merged in feature/IO-2714-New-Payment-Cache (pull request #1376)
IO-2714 New Payment Cache

Approved-by: Dave Richer
2024-03-28 16:51:59 +00:00
Allan Carr
e3fabdac91 Merged in feature/IO-2727-Payment-Export-Re-Export-Button (pull request #1377)
IO-2727 Payment Re-export/Export button

Approved-by: Dave Richer
2024-03-28 16:51:20 +00:00
Allan Carr
40621db556 Merged in feature/IO-2702-Limit-Production-Colors-to-Production-Statuses (pull request #1374)
IO-2702 Limit Production Colors to Production Statuses

Approved-by: Dave Richer
2024-03-28 16:50:51 +00:00
Patrick Fic
c2e64a124d Cherry picked from Rome changes to apply to IO. 2024-03-27 12:21:28 -07:00
Allan Carr
1fa83d124d IO-2727 Payment Re-export/Export button
Send data back to page

Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-27 12:14:47 -07:00
Allan Carr
cc734d3981 IO-2714 New Payment Cache
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-26 16:04:05 -07:00
Allan Carr
b41d69593d IO-2705 Insurance Co Id and correct Select Insurance and Layout for concistency
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-26 14:53:52 -07:00
Allan Carr
b7055aac84 IO-2702 Limit Production Colors to Production Statuses
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-26 13:45:04 -07:00
Dave Richer
09e505399c Merged in release/2024-03-22 (pull request #1372)
Release/2024 03 22

Approved-by: Patrick Fic
2024-03-22 18:27:16 +00:00
Allan Carr
42f9f275c7 Merged in feature/IO-2721-Owners-Note-in-Owners-Card (pull request #1370)
IO-2721 Owners Note in Owners Card

Approved-by: Dave Richer
2024-03-22 16:18:23 +00:00
Allan Carr
39b119b2e8 IO-2721 Owners Note in Owners Card
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-21 17:49:46 -07:00
Allan Carr
5bc224206c Merged in feature/IO-2711-Check-Box-Visibility (pull request #1362)
IO-2711 Check Box Visibility

Approved-by: Patrick Fic
2024-03-21 21:42:43 +00:00
Allan Carr
190da863c2 Merged in feature/IO-2698-Fuel-Level-&-Sorter (pull request #1363)
IO-2698 Fuel Level & Sorter

Approved-by: Patrick Fic
2024-03-21 21:41:12 +00:00
Allan Carr
2711b5fce5 Merged in feature/IO-2686-Disable-Return-on-isinhouse (pull request #1364)
IO-2686 Disable Return Item button on Bill Drawer if InHouse

Approved-by: Patrick Fic
2024-03-21 21:40:39 +00:00
Allan Carr
8e9358cd6f Merged in feature/IO-2713-Bills-&-Media-Visual-Seperation (pull request #1365)
IO-2713 Visually Seperate Bill Entry & Media Areas

Approved-by: Patrick Fic
2024-03-21 21:40:12 +00:00
Allan Carr
0e31bbb789 Merged in feature/IO-2710-Job-Assignment (pull request #1366)
IO-2710 Job Assignment

Approved-by: Patrick Fic
2024-03-21 21:39:36 +00:00
Allan Carr
3b635aeed3 Merged in feature/IO-2719-Missing-CC-Filter (pull request #1367)
IO-2719 Missing CC Filter

Approved-by: Patrick Fic
2024-03-21 21:39:05 +00:00
Allan Carr
1f896b1ede IO-2719 Missing CC Filter
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-21 11:09:13 -07:00
Allan Carr
b1ca09bd4f IO-2710 Prettierr
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-19 17:55:59 -07:00
Allan Carr
f9ca36ec89 IO-2710 Job Assignment
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-19 17:52:47 -07:00
Allan Carr
9d479d4b4d IO-2713 Visually Seperate Bill Entry & Media Areas
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-19 17:19:22 -07:00
Allan Carr
23b5b740cb IO-2686 Disable Return Item button on Bill Drawer if InHouse
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-19 17:17:15 -07:00
Allan Carr
8f9b05b974 IO-2698 Fuel Level & Sorter
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-19 14:41:34 -07:00
Allan Carr
d1132e7d45 IO-2711 Check Box Visibility
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-19 14:30:31 -07:00
Dave Richer
960b0b4d09 Merged in feature/IO-2650-Bugfix-On-Statuses (pull request #1361)
- Hasura Migration Changes
2024-03-19 20:26:09 +00:00
Dave Richer
f35ea026b8 - Hasura Migration Changes
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-19 16:25:21 -04:00
Patrick Fic
b1fc2828c8 Merge branch 'feature/IO-2682-Hasura-Migrations-For-Tasks' into release/2024-03-22 2024-03-18 15:51:14 -07:00
Dave Richer
f98c9e6f71 Merged in release/2024-03-15 (pull request #1354)
Release - 2024 03 15

Approved-by: Allan Carr
2024-03-15 18:50:55 +00:00
Patrick Fic
28f2e8ad30 Merged in feature/IO-2679-interactivity-tracking (pull request #1358)
Feature/IO-2679 interactivity tracking
2024-03-15 17:25:59 +00:00
Patrick Fic
c27e206687 Add index to audit trail. 2024-03-15 10:24:00 -07:00
Patrick Fic
01fd253f1d Manual modification to hasura migration. 2024-03-15 10:23:13 -07:00
Patrick Fic
e67bc0d953 Merged in feature/IO-2679-interactivity-tracking (pull request #1356)
Add ioevent logging for events.
2024-03-15 16:56:37 +00:00
Patrick Fic
3eab3e2fb6 Add ioevent logging for events. 2024-03-15 09:55:14 -07:00
Dave Richer
3adf6b649b Merged in feature/IO-2678-Linkable-schedule (pull request #1351)
- Implement
2024-03-15 14:49:06 +00:00
Dave Richer
f8243aa2b3 - Implement
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-15 10:47:46 -04:00
Dave Richer
3c3f50d138 Merged in feature/IO-2650-Lifecycle-V2 (pull request #1348)
- Fix bug
2024-03-14 18:59:37 +00:00
Dave Richer
4f7e1b81ac - Fix bug
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-14 14:59:06 -04:00
Dave Richer
806bdc4c70 Merged in feature/IO-2650-Lifecycle-V2 (pull request #1346)
- Missing translation
2024-03-14 16:38:12 +00:00
Dave Richer
a0572a0cec - Missing translation
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-14 12:37:38 -04:00
Dave Richer
04cdf13e86 Merged in feature/IO-2650-Lifecycle-V2 (pull request #1344)
Feature/IO-2650 Lifecycle V2
2024-03-14 16:33:14 +00:00
Dave Richer
50349e91dc - remove duplicated code
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-14 12:32:40 -04:00
Dave Richer
9998a8f154 - Fix bug on humanReadable field we have not consumed until now.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-14 12:28:07 -04:00
Allan Carr
9dcbcb2a43 Merged in feature/IO-2671-Vehicle-Detail-Table (pull request #1343)
IO-2671 Add in appropriate sorters at same time

Approved-by: Dave Richer
2024-03-14 15:58:46 +00:00
Allan Carr
5773f7a0f3 Merged in feature/IO-2650-Lifecycle-Report (pull request #1345)
IO-2650 Job Lifecycle Report Center Reports

Approved-by: Dave Richer
2024-03-14 15:58:17 +00:00
Allan Carr
8614d88e71 Merged in feature/IO-2630-Parts-Queue-Mods (pull request #1342)
IO-2630 Adjust for onRow selection

Approved-by: Dave Richer
2024-03-14 15:57:45 +00:00
Allan Carr
fa5e26c52a IO-2650 Job Lifecycle Report Center Reports
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-13 17:22:42 -07:00
Dave Richer
90e1cbd390 - Job Lifecycle Dashboard Component.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-13 19:20:10 -04:00
Allan Carr
947a3c6a88 IO-2671 Add in appropriate sorters at same time
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-13 12:31:51 -07:00
Allan Carr
a33662e6f0 IO-2630 Adjust for oRow selection
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-13 12:04:45 -07:00
Patrick Fic
6d1f04369e Merged in feature/IO-2674-reorder-resp-centers (pull request #1340)
IO-2674 Add reordering arrows for responsibility centers in IO.

Approved-by: Allan Carr
2024-03-13 17:07:09 +00:00
Patrick Fic
4a27726ef3 Adjusted label for payers & added reorder to payers. 2024-03-13 12:54:31 -04:00
Allan Carr
eedba97237 Merged in feature/IO-2625-BPT-Hrs-in-Employee-Assignment (pull request #1339)
IO-2625 B/P/T Hrs Display in Employee Assignment Block

Approved-by: Dave Richer
2024-03-13 16:50:47 +00:00
Allan Carr
bcf095ed4f Merged in feature/IO-2669-Next-Contact-Date-formating (pull request #1337)
IO-2669 Next Contact Formating

Approved-by: Dave Richer
2024-03-13 16:50:00 +00:00
Allan Carr
c3f9e268c7 Merged in feature/IO-2671-Vehicle-Detail-Table (pull request #1338)
IO-2671 Missing field in Vehicle query

Approved-by: Dave Richer
2024-03-13 16:49:36 +00:00
Allan Carr
c8442f0750 Merged in feature/IO-2570-Totals-Card-on-Draw-have-Customer-Owing (pull request #1341)
IO-2570 Change Ded to Customer Owing amount

Approved-by: Dave Richer
2024-03-13 16:49:13 +00:00
Allan Carr
da5b446c30 Merge branch 'master' into feature/IO-2630-Parts-Queue-Mods
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-13 09:49:02 -07:00
Allan Carr
e13b2bb969 Merged in feature/IO-2520-Kaizen-Data-Pump (pull request #1336)
IO-2520 Change where email notification occurs

Approved-by: Dave Richer
2024-03-13 16:48:01 +00:00
Allan Carr
e8969c4698 IO-2570 Change Ded to Customer Owing amount
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-13 09:47:33 -07:00
Patrick Fic
346d32a2bb IO-2674 Add reordering arrows for responsibility centers in IO. 2024-03-13 08:02:20 -04:00
Dave Richer
6ba00a90be Merge branch 'master' into feature/IO-2650-Lifecycle-V2 2024-03-12 18:11:08 -04:00
Dave Richer
4293d20313 - Backend Changes for Lifecycle Data
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-12 18:10:09 -04:00
Allan Carr
706c70c509 IO-2625 B/P/T Hrs Display in Employee Assignment Block
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-12 14:49:12 -07:00
Allan Carr
e872b1bf0a IO-2671 Missing field in Vehicle query
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-12 11:55:07 -07:00
Allan Carr
379fa060d8 IO-2669 Next Contact Formating
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-12 11:24:57 -07:00
Allan Carr
a6258c6456 Merged in feature/IO-2650-Lifecycle-Report (pull request #1335)
IO-2650 Lifecycle Report for Print Center

Approved-by: Dave Richer
2024-03-12 17:01:19 +00:00
Allan Carr
d6bf0a225b IO-2520 Change where email notification occurs
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-11 17:46:19 -07:00
Allan Carr
ec2b914e5e Merge branch 'master' into feature/IO-2520-Kaizen-Data-Pump
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-11 17:45:26 -07:00
Allan Carr
309a20148a IO-2650 Lifecycle Report for Print Center
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-11 17:41:37 -07:00
72 changed files with 3278 additions and 653 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
import { useMutation, useQuery } from "@apollo/client";
import { Button, Form, PageHeader, Popconfirm, Space } from "antd";
import { Button, Divider, Form, PageHeader, Popconfirm, Space } from "antd";
import moment from "moment";
import queryString from "query-string";
import React, { useState } from "react";
@@ -208,7 +208,7 @@ export function BillDetailEditcontainer({
layout="vertical"
>
<BillFormContainer form={form} billEdit disabled={exported} />
<Divider orientation="left">{t("general.labels.media")}</Divider>
{bodyshop.uselocalmediaserver ? (
<JobsDocumentsLocalGallery
job={{ id: data ? data.bills_by_pk.jobid : null }}

View File

@@ -45,6 +45,7 @@ export function BillDetailEditReturn({
actions: {},
context: {
jobId: data.bills_by_pk.jobid,
job: data.bills_by_pk.job,
vendorId: data.bills_by_pk.vendorid,
returnFromBill: data.bills_by_pk.id,
invoiceNumber: data.bills_by_pk.invoice_number,
@@ -173,7 +174,11 @@ export function BillDetailEditReturn({
</Form>
</Modal>
<Button
disabled={data.bills_by_pk.is_credit_memo || disabled}
disabled={
data.bills_by_pk.is_credit_memo ||
data.bills_by_pk.isinhouse ||
disabled
}
onClick={() => {
setVisible(true);
}}

View File

@@ -504,10 +504,11 @@ export function BillFormComponent({
billEdit={billEdit}
/>
)}
<Divider orientation="left" style={{ display: billEdit ? "none" : null }}>
{t("documents.labels.upload")}
</Divider>
<Form.Item
name="upload"
label="Upload"
style={{ display: billEdit ? "none" : null }}
valuePropName="fileList"
getValueFromEvent={(e) => {

View File

@@ -60,7 +60,7 @@ export function BillsListTableComponent({
)}
<BillDeleteButton bill={record} jobid={job.id} />
<BillDetailEditReturnComponent
data={{ bills_by_pk: { ...record, jobid: job.id } }}
data={{ bills_by_pk: { ...record, jobid: job.id, job: job } }}
disabled={
record.is_credit_memo ||
record.vendorid === bodyshop.inhousevendorid ||

View File

@@ -1,8 +1,8 @@
import React, { useState } from "react";
import { Button, Form, InputNumber, Popover } from "antd";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { useTranslation } from "react-i18next";
import { CalculatorFilled } from "@ant-design/icons";
import { Button, Form, InputNumber, Popover, Space } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { logImEXEvent } from "../../firebase/firebase.utils";
export default function CABCpvrtCalculator({ disabled, form }) {
const [visibility, setVisibility] = useState(false);
@@ -26,10 +26,14 @@ export default function CABCpvrtCalculator({ disabled, form }) {
<Form.Item name="days" label={t("jobs.labels.ca_bc_pvrt.days")}>
<InputNumber precision={0} min={0} />
</Form.Item>
<Button type="primary" htmlType="submit">
{t("general.actions.calculate")}
</Button>
<Button onClick={() => setVisibility(false)}>Close</Button>
<div style={{ display: "flex", justifyContent: "flex-end" }}>
<Space>
<Button type="primary" htmlType="submit">
{t("general.actions.calculate")}
</Button>
<Button onClick={() => setVisibility(false)}>Close</Button>
</Space>
</div>
</Form>
</div>
);

View File

@@ -10,11 +10,15 @@ import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel
import CourtesyCarReadiness from "../courtesy-car-readiness-select/courtesy-car-readiness-select.component";
import CourtesyCarStatus from "../courtesy-car-status-select/courtesy-car-status-select.component";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
//import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
export default function CourtesyCarCreateFormComponent({
form,
saveLoading,
newCC,
}) {
const { t } = useTranslation();
const client = useApolloClient();
@@ -33,7 +37,7 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
}
/>
{/* <FormFieldsChanged form={form} /> */}
{newCC ? null : <FormFieldsChanged form={form} />}
<LayoutFormRow header={t("courtesycars.labels.vehicle")}>
<Form.Item
label={t("courtesycars.fields.year")}

View File

@@ -61,6 +61,10 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
text: t("courtesycars.status.in"),
value: "courtesycars.status.in",
},
{
text: t("courtesycars.status.inservice"),
value: "courtesycars.status.inservice",
},
{
text: t("courtesycars.status.out"),
value: "courtesycars.status.out",
@@ -74,7 +78,7 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
value: "courtesycars.status.leasereturn",
},
],
onFilter: (value, record) => value.includes(record.status),
onFilter: (value, record) => record.status === value,
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
render: (text, record) => {
@@ -178,7 +182,7 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
title: t("courtesycars.fields.fuel"),
dataIndex: "fuel",
key: "fuel",
sorter: (a, b) => alphaSort(a.fuel, b.fuel),
sorter: (a, b) => a.fuel - b.fuel,
sortOrder:
state.sortedInfo.columnKey === "fuel" && state.sortedInfo.order,
render: (text, record) => {
@@ -187,12 +191,14 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
return t("courtesycars.labels.fuel.full");
case 88:
return t("courtesycars.labels.fuel.78");
case 75:
return t("courtesycars.labels.fuel.34");
case 63:
return t("courtesycars.labels.fuel.58");
case 50:
return t("courtesycars.labels.fuel.12");
case 38:
return t("courtesycars.labels.fuel.34");
return t("courtesycars.labels.fuel.38");
case 25:
return t("courtesycars.labels.fuel.14");
case 13:

View File

@@ -0,0 +1,169 @@
import {Card, Table, Tag} from "antd";
import LoadingSkeleton from "../../loading-skeleton/loading-skeleton.component";
import {useTranslation} from "react-i18next";
import React, {useEffect, useState} from "react";
import moment from "moment";
import DashboardRefreshRequired from "../refresh-required.component";
import axios from "axios";
const fortyFiveDaysAgo = () => moment().subtract(45, 'days').toLocaleString();
export default function JobLifecycleDashboardComponent({data, bodyshop, ...cardProps}) {
const {t} = useTranslation();
const [loading, setLoading] = useState(false);
const [lifecycleData, setLifecycleData] = useState(null);
useEffect(() => {
async function getLifecycleData() {
if (data && data.job_lifecycle) {
setLoading(true);
const response = await axios.post("/job/lifecycle", {
jobids: data.job_lifecycle.map(x => x.id),
statuses: bodyshop.md_ro_statuses
});
setLifecycleData(response.data.durations);
setLoading(false);
}
}
getLifecycleData().catch(e => {
console.error(`Error in getLifecycleData: ${e}`);
})
}, [data, bodyshop]);
const columns = [
{
title: t('job_lifecycle.columns.status'),
dataIndex: 'status',
bgColor: 'red',
key: 'status',
render: (text, record) => {
return <Tag color={record.color}>{record.status}</Tag>
}
},
{
title: t('job_lifecycle.columns.human_readable'),
dataIndex: 'humanReadable',
key: 'humanReadable',
},
{
title: t('job_lifecycle.columns.status_count'),
key: 'statusCount',
render: (text, record) => {
return lifecycleData.statusCounts[record.status];
}
},
{
title: t('job_lifecycle.columns.percentage'),
dataIndex: 'percentage',
key: 'percentage',
render: (text, record) => {
return record.percentage.toFixed(2) + '%';
}
},
];
if (!data) return null;
if (!data.job_lifecycle || !lifecycleData) return <DashboardRefreshRequired {...cardProps} />;
const extra = `${t('job_lifecycle.content.calculated_based_on')} ${lifecycleData.jobs} ${t('job_lifecycle.content.jobs_in_since')} ${fortyFiveDaysAgo()}`
return (
<Card title={t("job_lifecycle.titles.dashboard")} {...cardProps}>
<LoadingSkeleton loading={loading}>
<div style={{overflow: 'scroll', height: "100%"}}>
<div id="bar-container" style={{
display: 'flex',
width: '100%',
height: '100px',
textAlign: 'center',
borderRadius: '5px',
borderWidth: '5px',
borderStyle: 'solid',
borderColor: '#f0f2f5',
margin: 0,
padding: 0
}}>
{lifecycleData.summations.map((key, index, array) => {
const isFirst = index === 0;
const isLast = index === array.length - 1;
return (
<div key={key.status} style={{
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
margin: 0,
padding: 0,
borderTop: '1px solid #f0f2f5',
borderBottom: '1px solid #f0f2f5',
borderLeft: isFirst ? '1px solid #f0f2f5' : undefined,
borderRight: isLast ? '1px solid #f0f2f5' : undefined,
backgroundColor: key.color,
width: `${key.percentage}%`
}}
aria-label={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
title={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
>
{key.percentage > 15 ?
<>
<div>{key.roundedPercentage}</div>
<div style={{
backgroundColor: '#f0f2f5',
borderRadius: '5px',
paddingRight: '2px',
paddingLeft: '2px',
fontSize: '0.8rem',
}}>
{key.status}
</div>
</>
: null}
</div>
);
})}
</div>
<Card extra={extra} type='inner' title={t('job_lifecycle.content.legend_title')}
style={{marginTop: '10px'}}>
<div>
{lifecycleData.summations.map((key) => (
<Tag color={key.color} style={{width: '13vh', padding: '4px', margin: '4px'}}>
<div
aria-label={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
title={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
style={{
backgroundColor: '#f0f2f5',
color: '#000',
padding: '4px',
textAlign: 'center'
}}>
{key.status} [{lifecycleData.statusCounts[key.status]}] ({key.roundedPercentage})
</div>
</Tag>
))}
</div>
</Card>
<Card style={{marginTop: "5px"}} type='inner' title={t("job_lifecycle.titles.top_durations")}>
<Table size="small" pagination={false} columns={columns}
dataSource={lifecycleData.summations.sort((a, b) => b.value - a.value).slice(0, 3)}/>
</Card>
</div>
</LoadingSkeleton>
</Card>
);
}
export const JobLifecycleDashboardGQL = `
job_lifecycle: jobs(where: {
actual_in: {
_gte: "${moment().subtract(45, 'days').toISOString()}"
}
}) {
id
actual_in
} `;

View File

@@ -1,380 +1,391 @@
import Icon, { SyncOutlined } from "@ant-design/icons";
import { gql, useMutation, useQuery } from "@apollo/client";
import { Button, Dropdown, Menu, PageHeader, Space, notification } from "antd";
import Icon, {SyncOutlined} from "@ant-design/icons";
import {gql, useMutation, useQuery} from "@apollo/client";
import {Button, Dropdown, Menu, notification, PageHeader, Space} from "antd";
import i18next from "i18next";
import _ from "lodash";
import moment from "moment";
import React, { useState } from "react";
import { Responsive, WidthProvider } from "react-grid-layout";
import { useTranslation } from "react-i18next";
import { MdClose } from "react-icons/md";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { UPDATE_DASHBOARD_LAYOUT } from "../../graphql/user.queries";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import React, {useState} from "react";
import {Responsive, WidthProvider} from "react-grid-layout";
import {useTranslation} from "react-i18next";
import {MdClose} from "react-icons/md";
import {connect} from "react-redux";
import {createStructuredSelector} from "reselect";
import {logImEXEvent} from "../../firebase/firebase.utils";
import {UPDATE_DASHBOARD_LAYOUT} from "../../graphql/user.queries";
import {selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors";
import AlertComponent from "../alert/alert.component";
import DashboardMonthlyEmployeeEfficiency, {
DashboardMonthlyEmployeeEfficiencyGql,
DashboardMonthlyEmployeeEfficiencyGql,
} from "../dashboard-components/monthly-employee-efficiency/monthly-employee-efficiency.component";
import DashboardMonthlyJobCosting from "../dashboard-components/monthly-job-costing/monthly-job-costing.component";
import DashboardMonthlyLaborSales from "../dashboard-components/monthly-labor-sales/monthly-labor-sales.component";
import DashboardMonthlyPartsSales from "../dashboard-components/monthly-parts-sales/monthly-parts-sales.component";
import DashboardMonthlyRevenueGraph, {
DashboardMonthlyRevenueGraphGql,
DashboardMonthlyRevenueGraphGql,
} from "../dashboard-components/monthly-revenue-graph/monthly-revenue-graph.component";
import DashboardProjectedMonthlySales, {
DashboardProjectedMonthlySalesGql,
DashboardProjectedMonthlySalesGql,
} from "../dashboard-components/pojected-monthly-sales/projected-monthly-sales.component";
import DashboardTotalProductionDollars from "../dashboard-components/total-production-dollars/total-production-dollars.component";
import DashboardTotalProductionDollars
from "../dashboard-components/total-production-dollars/total-production-dollars.component";
import DashboardTotalProductionHours, {
DashboardTotalProductionHoursGql,
DashboardTotalProductionHoursGql,
} from "../dashboard-components/total-production-hours/total-production-hours.component";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
//Combination of the following:
// /node_modules/react-grid-layout/css/styles.css
// /node_modules/react-resizable/css/styles.css
import DashboardScheduledInToday, {
DashboardScheduledInTodayGql,
DashboardScheduledInTodayGql,
} from "../dashboard-components/scheduled-in-today/scheduled-in-today.component";
import DashboardScheduledOutToday, {
DashboardScheduledOutTodayGql,
DashboardScheduledOutTodayGql,
} from "../dashboard-components/scheduled-out-today/scheduled-out-today.component";
import JobLifecycleDashboardComponent, {
JobLifecycleDashboardGQL
} from "../dashboard-components/job-lifecycle/job-lifecycle-dashboard.component";
import "./dashboard-grid.styles.scss";
import { GenerateDashboardData } from "./dashboard-grid.utils";
import {GenerateDashboardData} from "./dashboard-grid.utils";
const ResponsiveReactGridLayout = WidthProvider(Responsive);
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
bodyshop: selectBodyshop,
currentUser: selectCurrentUser,
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export function DashboardGridComponent({ currentUser, bodyshop }) {
const { t } = useTranslation();
const [state, setState] = useState({
...(bodyshop.associations[0].user.dashboardlayout
? bodyshop.associations[0].user.dashboardlayout
: { items: [], layout: {}, layouts: [] }),
});
const { loading, error, data, refetch } = useQuery(
createDashboardQuery(state),
{ fetchPolicy: "network-only", nextFetchPolicy: "network-only" }
);
const [updateLayout] = useMutation(UPDATE_DASHBOARD_LAYOUT);
const handleLayoutChange = async (layout, layouts) => {
logImEXEvent("dashboard_change_layout");
setState({ ...state, layout, layouts });
const result = await updateLayout({
variables: {
email: currentUser.email,
layout: { ...state, layout, layouts },
},
export function DashboardGridComponent({currentUser, bodyshop}) {
const {t} = useTranslation();
const [state, setState] = useState({
...(bodyshop.associations[0].user.dashboardlayout
? bodyshop.associations[0].user.dashboardlayout
: {items: [], layout: {}, layouts: []}),
});
if (!!result.errors) {
notification["error"]({
message: t("dashboard.errors.updatinglayout", {
message: JSON.stringify(result.errors),
}),
});
}
};
const handleRemoveComponent = (key) => {
logImEXEvent("dashboard_remove_component", { name: key });
const idxToRemove = state.items.findIndex((i) => i.i === key);
const items = _.cloneDeep(state.items);
const {loading, error, data, refetch} = useQuery(
createDashboardQuery(state),
{fetchPolicy: "network-only", nextFetchPolicy: "network-only"}
);
items.splice(idxToRemove, 1);
setState({ ...state, items });
};
const [updateLayout] = useMutation(UPDATE_DASHBOARD_LAYOUT);
const handleAddComponent = (e) => {
logImEXEvent("dashboard_add_component", { name: e });
setState({
...state,
items: [
...state.items,
{
i: e.key,
x: (state.items.length * 2) % (state.cols || 12),
y: 99, // puts it at the bottom
w: componentList[e.key].w || 2,
h: componentList[e.key].h || 2,
},
],
});
};
const handleLayoutChange = async (layout, layouts) => {
logImEXEvent("dashboard_change_layout");
const dashboarddata = React.useMemo(
() => GenerateDashboardData(data),
[data]
);
const existingLayoutKeys = state.items.map((i) => i.i);
const addComponentOverlay = (
<Menu onClick={handleAddComponent}>
{Object.keys(componentList).map((key) => (
<Menu.Item
key={key}
value={key}
disabled={existingLayoutKeys.includes(key)}
>
{componentList[key].label}
</Menu.Item>
))}
</Menu>
);
setState({...state, layout, layouts});
if (error) return <AlertComponent message={error.message} type="error" />;
return (
<div>
<PageHeader
extra={
<Space>
<Button onClick={() => refetch()}>
<SyncOutlined />
</Button>
<Dropdown overlay={addComponentOverlay} trigger={["click"]}>
<Button>{t("dashboard.actions.addcomponent")}</Button>
</Dropdown>
</Space>
const result = await updateLayout({
variables: {
email: currentUser.email,
layout: {...state, layout, layouts},
},
});
if (!!result.errors) {
notification["error"]({
message: t("dashboard.errors.updatinglayout", {
message: JSON.stringify(result.errors),
}),
});
}
/>
};
const handleRemoveComponent = (key) => {
logImEXEvent("dashboard_remove_component", {name: key});
const idxToRemove = state.items.findIndex((i) => i.i === key);
<ResponsiveReactGridLayout
className="layout"
breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }}
cols={{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }}
width="100%"
layouts={state.layouts}
onLayoutChange={handleLayoutChange}
// onBreakpointChange={onBreakpointChange}
>
{state.items.map((item, index) => {
const TheComponent = componentList[item.i].component;
return (
<div
key={item.i}
data-grid={{
...item,
minH: componentList[item.i].minH || 1,
minW: componentList[item.i].minW || 1,
}}
const items = _.cloneDeep(state.items);
items.splice(idxToRemove, 1);
setState({...state, items});
};
const handleAddComponent = (e) => {
logImEXEvent("dashboard_add_component", {name: e});
setState({
...state,
items: [
...state.items,
{
i: e.key,
x: (state.items.length * 2) % (state.cols || 12),
y: 99, // puts it at the bottom
w: componentList[e.key].w || 2,
h: componentList[e.key].h || 2,
},
],
});
};
const dashboarddata = React.useMemo(
() => GenerateDashboardData(data),
[data]
);
const existingLayoutKeys = state.items.map((i) => i.i);
const addComponentOverlay = (
<Menu onClick={handleAddComponent}>
{Object.keys(componentList).map((key) => (
<Menu.Item
key={key}
value={key}
disabled={existingLayoutKeys.includes(key)}
>
{componentList[key].label}
</Menu.Item>
))}
</Menu>
);
if (error) return <AlertComponent message={error.message} type="error"/>;
return (
<div>
<PageHeader
extra={
<Space>
<Button onClick={() => refetch()}>
<SyncOutlined/>
</Button>
<Dropdown overlay={addComponentOverlay} trigger={["click"]}>
<Button>{t("dashboard.actions.addcomponent")}</Button>
</Dropdown>
</Space>
}
/>
<ResponsiveReactGridLayout
className="layout"
breakpoints={{lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0}}
cols={{lg: 12, md: 10, sm: 6, xs: 4, xxs: 2}}
width="100%"
layouts={state.layouts}
onLayoutChange={handleLayoutChange}
// onBreakpointChange={onBreakpointChange}
>
<LoadingSkeleton loading={loading}>
<Icon
component={MdClose}
key={item.i}
style={{
position: "absolute",
zIndex: "2",
right: ".25rem",
top: ".25rem",
cursor: "pointer",
}}
onClick={() => handleRemoveComponent(item.i)}
/>
<TheComponent className="dashboard-card" data={dashboarddata} />
</LoadingSkeleton>
</div>
);
})}
</ResponsiveReactGridLayout>
</div>
);
{state.items.map((item, index) => {
const TheComponent = componentList[item.i].component;
return (
<div
key={item.i}
data-grid={{
...item,
minH: componentList[item.i].minH || 1,
minW: componentList[item.i].minW || 1,
}}
>
<LoadingSkeleton loading={loading}>
<Icon
component={MdClose}
key={item.i}
style={{
position: "absolute",
zIndex: "2",
right: ".25rem",
top: ".25rem",
cursor: "pointer",
}}
onClick={() => handleRemoveComponent(item.i)}
/>
<TheComponent className="dashboard-card" bodyshop={bodyshop} data={dashboarddata}/>
</LoadingSkeleton>
</div>
);
})}
</ResponsiveReactGridLayout>
</div>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
mapStateToProps,
mapDispatchToProps
)(DashboardGridComponent);
const componentList = {
ProductionDollars: {
label: i18next.t("dashboard.titles.productiondollars"),
component: DashboardTotalProductionDollars,
gqlFragment: null,
w: 1,
h: 1,
minW: 2,
minH: 1,
},
ProductionHours: {
label: i18next.t("dashboard.titles.productionhours"),
component: DashboardTotalProductionHours,
gqlFragment: DashboardTotalProductionHoursGql,
w: 3,
h: 1,
minW: 3,
minH: 1,
},
ProjectedMonthlySales: {
label: i18next.t("dashboard.titles.projectedmonthlysales"),
component: DashboardProjectedMonthlySales,
gqlFragment: DashboardProjectedMonthlySalesGql,
w: 2,
h: 1,
minW: 2,
minH: 1,
},
MonthlyRevenueGraph: {
label: i18next.t("dashboard.titles.monthlyrevenuegraph"),
component: DashboardMonthlyRevenueGraph,
gqlFragment: DashboardMonthlyRevenueGraphGql,
w: 4,
h: 2,
minW: 4,
minH: 2,
},
MonthlyJobCosting: {
label: i18next.t("dashboard.titles.monthlyjobcosting"),
component: DashboardMonthlyJobCosting,
gqlFragment: null,
minW: 6,
minH: 3,
w: 6,
h: 3,
},
MonthlyPartsSales: {
label: i18next.t("dashboard.titles.monthlypartssales"),
component: DashboardMonthlyPartsSales,
gqlFragment: null,
minW: 2,
minH: 2,
w: 2,
h: 2,
},
MonthlyLaborSales: {
label: i18next.t("dashboard.titles.monthlylaborsales"),
component: DashboardMonthlyLaborSales,
gqlFragment: null,
minW: 2,
minH: 2,
w: 2,
h: 2,
},
MonthlyEmployeeEfficency: {
label: i18next.t("dashboard.titles.monthlyemployeeefficiency"),
component: DashboardMonthlyEmployeeEfficiency,
gqlFragment: DashboardMonthlyEmployeeEfficiencyGql,
minW: 2,
minH: 2,
w: 2,
h: 2,
},
ScheduleInToday: {
label: i18next.t("dashboard.titles.scheduledintoday"),
component: DashboardScheduledInToday,
gqlFragment: DashboardScheduledInTodayGql,
minW: 6,
minH: 2,
w: 10,
h: 3,
},
ScheduleOutToday: {
label: i18next.t("dashboard.titles.scheduledouttoday"),
component: DashboardScheduledOutToday,
gqlFragment: DashboardScheduledOutTodayGql,
minW: 6,
minH: 2,
w: 10,
h: 3,
},
ProductionDollars: {
label: i18next.t("dashboard.titles.productiondollars"),
component: DashboardTotalProductionDollars,
gqlFragment: null,
w: 1,
h: 1,
minW: 2,
minH: 1,
},
ProductionHours: {
label: i18next.t("dashboard.titles.productionhours"),
component: DashboardTotalProductionHours,
gqlFragment: DashboardTotalProductionHoursGql,
w: 3,
h: 1,
minW: 3,
minH: 1,
},
ProjectedMonthlySales: {
label: i18next.t("dashboard.titles.projectedmonthlysales"),
component: DashboardProjectedMonthlySales,
gqlFragment: DashboardProjectedMonthlySalesGql,
w: 2,
h: 1,
minW: 2,
minH: 1,
},
MonthlyRevenueGraph: {
label: i18next.t("dashboard.titles.monthlyrevenuegraph"),
component: DashboardMonthlyRevenueGraph,
gqlFragment: DashboardMonthlyRevenueGraphGql,
w: 4,
h: 2,
minW: 4,
minH: 2,
},
MonthlyJobCosting: {
label: i18next.t("dashboard.titles.monthlyjobcosting"),
component: DashboardMonthlyJobCosting,
gqlFragment: null,
minW: 6,
minH: 3,
w: 6,
h: 3,
},
MonthlyPartsSales: {
label: i18next.t("dashboard.titles.monthlypartssales"),
component: DashboardMonthlyPartsSales,
gqlFragment: null,
minW: 2,
minH: 2,
w: 2,
h: 2,
},
MonthlyLaborSales: {
label: i18next.t("dashboard.titles.monthlylaborsales"),
component: DashboardMonthlyLaborSales,
gqlFragment: null,
minW: 2,
minH: 2,
w: 2,
h: 2,
},
// Typo in Efficency should be Efficiency, but changing it would reset users dashboard settings
MonthlyEmployeeEfficency: {
label: i18next.t("dashboard.titles.monthlyemployeeefficiency"),
component: DashboardMonthlyEmployeeEfficiency,
gqlFragment: DashboardMonthlyEmployeeEfficiencyGql,
minW: 2,
minH: 2,
w: 2,
h: 2,
},
ScheduleInToday: {
label: i18next.t("dashboard.titles.scheduledintoday"),
component: DashboardScheduledInToday,
gqlFragment: DashboardScheduledInTodayGql,
minW: 6,
minH: 2,
w: 10,
h: 3,
},
ScheduleOutToday: {
label: i18next.t("dashboard.titles.scheduledouttoday"),
component: DashboardScheduledOutToday,
gqlFragment: DashboardScheduledOutTodayGql,
minW: 6,
minH: 2,
w: 10,
h: 3,
},
JobLifecycle: {
label: i18next.t("dashboard.titles.joblifecycle"),
component: JobLifecycleDashboardComponent,
gqlFragment: JobLifecycleDashboardGQL,
minW: 6,
minH: 3,
w: 6,
h: 3,
},
};
const createDashboardQuery = (state) => {
const componentBasedAdditions =
state &&
Array.isArray(state.layout) &&
state.layout
.map((item, index) => componentList[item.i].gqlFragment || "")
.join("");
return gql`
query QUERY_DASHBOARD_DETAILS { ${componentBasedAdditions || ""}
monthly_sales: jobs(where: {_and: [
{ voided: {_eq: false}},
{date_invoiced: {_gte: "${moment()
const componentBasedAdditions =
state &&
Array.isArray(state.layout) &&
state.layout
.map((item, index) => componentList[item.i].gqlFragment || "")
.join("");
return gql`
query QUERY_DASHBOARD_DETAILS { ${componentBasedAdditions || ""}
monthly_sales: jobs(where: {_and: [
{ voided: {_eq: false}},
{date_invoiced: {_gte: "${moment()
.startOf("month")
.startOf("day")
.toISOString()}"}}, {date_invoiced: {_lte: "${moment()
.startOf("day")
.toISOString()}"}}, {date_invoiced: {_lte: "${moment()
.endOf("month")
.endOf("day")
.toISOString()}"}}]}) {
id
ro_number
date_invoiced
job_totals
rate_la1
rate_la2
rate_la3
rate_la4
rate_laa
rate_lab
rate_lad
rate_lae
rate_laf
rate_lag
rate_lam
rate_lar
rate_las
rate_lau
rate_ma2s
rate_ma2t
rate_ma3s
rate_mabl
rate_macs
rate_mahw
rate_mapa
rate_mash
rate_matd
joblines(where: { removed: { _eq: false } }) {
id
mod_lbr_ty
mod_lb_hrs
act_price
part_qty
part_type
}
}
production_jobs: jobs(where: { inproduction: { _eq: true } }) {
id
ro_number
ins_co_nm
job_totals
joblines(where: { removed: { _eq: false } }) {
id
mod_lbr_ty
mod_lb_hrs
act_price
part_qty
part_type
}
labhrs: joblines_aggregate(where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }) {
aggregate {
sum {
mod_lb_hrs
}
}
}
larhrs: joblines_aggregate(where: { mod_lbr_ty: { _eq: "LAR" }, removed: { _eq: false } }) {
aggregate {
sum {
mod_lb_hrs
}
}
}
}
}`;
ro_number
date_invoiced
job_totals
rate_la1
rate_la2
rate_la3
rate_la4
rate_laa
rate_lab
rate_lad
rate_lae
rate_laf
rate_lag
rate_lam
rate_lar
rate_las
rate_lau
rate_ma2s
rate_ma2t
rate_ma3s
rate_mabl
rate_macs
rate_mahw
rate_mapa
rate_mash
rate_matd
joblines(where: { removed: { _eq: false } }) {
id
mod_lbr_ty
mod_lb_hrs
act_price
part_qty
part_type
}
}
production_jobs: jobs(where: { inproduction: { _eq: true } }) {
id
ro_number
ins_co_nm
job_totals
joblines(where: { removed: { _eq: false } }) {
id
mod_lbr_ty
mod_lb_hrs
act_price
part_qty
part_type
}
labhrs: joblines_aggregate(where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }) {
aggregate {
sum {
mod_lb_hrs
}
}
}
larhrs: joblines_aggregate(where: { mod_lbr_ty: { _eq: "LAR" }, removed: { _eq: false } }) {
aggregate {
sum {
mod_lb_hrs
}
}
}
}
}`;
};

View File

@@ -18,10 +18,8 @@ export default function JobDetailCardsTotalsComponent({ loading, data }) {
/>
<Statistic
className="imex-flex-row__margin-large"
title={t("jobs.fields.ded_amt")}
value={Dinero({
amount: Math.round((data.ded_amt || 0) * 100),
}).toFormat()}
title={t("jobs.fields.customerowing")}
value={Dinero(data.job_totals.totals.custPayable.total).toFormat()}
/>
<Statistic
className="imex-flex-row__margin-large"

View File

@@ -1,5 +1,5 @@
import { useQuery } from "@apollo/client";
import { Col, Divider, Row, Skeleton, Space, Timeline, Typography } from "antd";
import { Col, Row, Skeleton, Timeline, Typography } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
@@ -31,15 +31,27 @@ export default function JobLinesExpander({ jobline, jobid }) {
{data.parts_order_lines.length > 0 ? (
data.parts_order_lines.map((line) => (
<Timeline.Item key={line.id}>
<Space split={<Divider type="vertical" />} wrap>
<Link
to={`/manage/jobs/${jobid}?partsorderid=${line.parts_order.id}`}
>
{line.parts_order.order_number}
</Link>
<DateFormatter>{line.parts_order.order_date}</DateFormatter>
{line.parts_order.vendor.name}
</Space>
<Row wrap>
<Col span={4}>
<Link
to={`/manage/jobs/${jobid}?partsorderid=${line.parts_order.id}`}
>
{line.parts_order.order_number}
</Link>
</Col>
<Col span={4}>
<DateFormatter>{line.parts_order.order_date}</DateFormatter>
</Col>
<Col span={4}>{line.parts_order.vendor.name}</Col>
{line.backordered_eta ? (
<Col span={4}>
<span>
{`${t("parts_orders.fields.backordered_eta")}: `}
<DateFormatter>{line.backordered_eta}</DateFormatter>
</span>
</Col>
) : null}
</Row>
</Timeline.Item>
))
) : (
@@ -71,7 +83,7 @@ export default function JobLinesExpander({ jobline, jobid }) {
</Col>
<Col span={4}>
<span>
{`${t("billlines.fields.actual_cost")}: `}
{`${t("billlines.fields.actual_cost")}: `}
<CurrencyFormatter>{line.actual_cost}</CurrencyFormatter>
</span>
</Col>
@@ -84,7 +96,7 @@ export default function JobLinesExpander({ jobline, jobid }) {
))
) : (
<Timeline.Item>
{t("parts_orders.labels.notyetordered")}
{t("bills.labels.nobilllines")}
</Timeline.Item>
)}
</Timeline>

View File

@@ -23,7 +23,6 @@ export function JobEmployeeAssignments({
jobRO,
body,
refinish,
prep,
csr,
handleAdd,
@@ -78,7 +77,7 @@ export function JobEmployeeAssignments({
setVisibility(false);
}}
>
Assign
{t("allocations.actions.assign")}
</Button>
<Button onClick={() => setVisibility(false)}>Close</Button>
</Space>

View File

@@ -43,13 +43,13 @@ export function JobEmployeeAssignmentsContainer({
});
if (refetch) refetch();
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobassignmentchange(operation, name),
type: "jobassignmentchange",
});
if (!!result.errors) {
if (!!!result.errors) {
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobassignmentchange(operation, name),
type: "jobassignmentchange",
});
} else {
notification["error"]({
message: t("jobs.errors.assigning", {
message: JSON.stringify(result.errors),
@@ -67,18 +67,19 @@ export function JobEmployeeAssignmentsContainer({
variables: { jobId: job.id, job: { [empAssignment]: null } },
});
if (!!result.errors) {
if (!!!result.errors) {
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobassignmentremoved(operation),
type: "jobassignmentremoved",
});
} else {
notification["error"]({
message: t("jobs.errors.assigning", {
message: JSON.stringify(result.errors),
}),
});
}
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobassignmentremoved(operation),
type: "jobassignmentremoved",
});
setLoading(false);
};

View File

@@ -82,7 +82,7 @@ export default function JobReconciliationBillsTable({
state.sortedInfo.order,
render: (text, record) => (
<Checkbox disabled checked={record.bill.is_credit_memo} />
<Checkbox checked={record.bill.is_credit_memo} />
),
},
];

View File

@@ -1,4 +1,12 @@
import { Collapse, Form, Input, InputNumber, Select, Switch } from "antd";
import {
Collapse,
Form,
Input,
InputNumber,
Select,
Space,
Switch,
} from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -10,6 +18,8 @@ import FormItemEmail from "../form-items-formatted/email-form-item.component";
import FormItemPhone, {
PhoneItemFormatterValidation,
} from "../form-items-formatted/phone-form-item.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 JobsDetailRatesChangeButton from "../jobs-detail-rates-change-button/jobs-detail-rates-change-button.component";
import JobsDetailRatesParts from "../jobs-detail-rates/jobs-detail-rates.parts.component";
import JobsMarkPstExempt from "../jobs-mark-pst-exempt/jobs-mark-pst-exempt.component";
@@ -25,6 +35,15 @@ const mapDispatchToProps = (dispatch) => ({
export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
const { t } = useTranslation();
const { getFieldValue } = form;
const handleInsCoChange = (value) => {
const selectedCompany = bodyshop.md_ins_cos.find((s) => s.name === value);
if (selectedCompany) {
form.setFieldValue("ins_addr1", selectedCompany.street1);
form.setFieldValue("ins_city", selectedCompany.city);
}
};
return (
<div>
<Collapse defaultActiveKey="insurance">
@@ -34,26 +53,20 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
forceRender
>
<LayoutFormRow>
<Form.Item label={t("jobs.fields.ins_co_id")} name="ins_co_id">
<Form.Item label={t("jobs.fields.clm_no")} name="clm_no">
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.policy_no")} name="policy_no">
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.clm_no")} name="clm_no">
<Input />
</Form.Item>
<Form.Item
label={t("jobs.fields.regie_number")}
name="regie_number"
>
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.loss_date")} name="loss_date">
<FormDatePicker />
</Form.Item>
<Form.Item label={t("jobs.fields.ins_co_nm")} name="ins_co_nm">
<Select>
<Select onChange={handleInsCoChange}>
{bodyshop.md_ins_cos.map((s) => (
<Select.Option key={s.name} value={s.name}>
{s.name}
@@ -67,7 +80,15 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
<Form.Item label={t("jobs.fields.ins_city")} name="ins_city">
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.ins_ct_ln")} name="ins_ct_ln">
<Form.Item
label={
<Space>
{t("jobs.fields.ins_ct_ln")}
<JobsDetailChangeFilehandler form={form} />
</Space>
}
name="ins_ct_ln"
>
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.ins_ct_fn")} name="ins_ct_fn">
@@ -95,11 +116,24 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
>
<FormItemEmail email={getFieldValue("ins_ea")} />
</Form.Item>
<Form.Item label={t("jobs.fields.loss_date")} name="loss_date">
<FormDatePicker />
</Form.Item>
<Form.Item label={t("jobs.fields.kmin")} name="kmin">
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.est_co_nm")} name="est_co_nm">
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.est_ct_fn")} name="est_ct_fn">
<Form.Item
label={
<Space>
{t("jobs.fields.est_ct_fn")}
<JobsDetailChangeEstimator form={form} />
</Space>
}
name="est_ct_fn"
>
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.est_ct_ln")} name="est_ct_ln">

View File

@@ -37,6 +37,15 @@ const lossColDamage = { sm: { span: 24 }, md: { span: 6 }, lg: { span: 4 } };
export function JobsDetailGeneral({ bodyshop, jobRO, job, form }) {
const { getFieldValue } = form;
const { t } = useTranslation();
const handleInsCoChange = (value) => {
const selectedCompany = bodyshop.md_ins_cos.find((s) => s.name === value);
if (selectedCompany) {
form.setFieldValue("ins_addr1", selectedCompany.street1);
form.setFieldValue("ins_city", selectedCompany.city);
}
};
return (
<div>
<FormRow header={t("jobs.forms.claiminfo")}>
@@ -71,7 +80,7 @@ export function JobsDetailGeneral({ bodyshop, jobRO, job, form }) {
</Form.Item>
<Form.Item label={t("jobs.fields.ins_co_nm")} name="ins_co_nm">
<Select disabled={jobRO}>
<Select disabled={jobRO} onChange={handleInsCoChange}>
{bodyshop.md_ins_cos.map((s) => (
<Select.Option key={s.name} value={s.name}>
{s.name}

View File

@@ -4,7 +4,8 @@ import {
PauseCircleOutlined,
WarningFilled,
} from "@ant-design/icons";
import { Card, Col, Row, Space, Tag, Tooltip } from "antd";
import { Card, Col, Divider, Row, Space, Tag, Tooltip } from "antd";
import moment from "moment";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -62,6 +63,13 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
${job.v_make_desc || ""}
${job.v_model_desc || ""}`.trim();
const bodyHrs = job.joblines
.filter((j) => j.mod_lbr_ty !== "LAR")
.reduce((acc, val) => acc + val.mod_lb_hrs, 0);
const refinishHrs = job.joblines
.filter((line) => line.mod_lbr_ty === "LAR")
.reduce((acc, val) => acc + val.mod_lb_hrs, 0);
const ownerTitle = OwnerNameDisplayFunction(job).trim();
return (
@@ -93,7 +101,13 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
{job.status === bodyshop.md_ro_statuses.default_scheduled &&
job.scheduled_in ? (
<Tag>
<DateTimeFormatter>{job.scheduled_in}</DateTimeFormatter>
<Link
to={`/manage/schedule?date=${moment(
job.scheduled_in
).format("YYYY-MM-DD")}`}
>
<DateTimeFormatter>{job.scheduled_in}</DateTimeFormatter>
</Link>
</Tag>
) : null}
</Space>
@@ -211,6 +225,12 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
{job.owner?.tax_number || ""}
</DataLabel>
)}
<DataLabel
label={t("owners.fields.note")}
valueStyle={{ overflow: "hidden", textOverflow: "ellipsis" }}
>
{job.owner?.note || ""}
</DataLabel>
</div>
</Card>
</Col>
@@ -299,6 +319,11 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
>
<div>
<JobEmployeeAssignments job={job} />
<Divider style={{ margin: ".5rem" }} />
<DataLabel label={t("jobs.labels.labor_hrs")}>
{bodyHrs.toFixed(1)} / {refinishHrs.toFixed(1)} /{" "}
{(bodyHrs + refinishHrs).toFixed(1)}
</DataLabel>
</div>
</Card>
</Col>

View File

@@ -24,7 +24,7 @@ function OwnerDetailJobsComponent({ bodyshop, owner }) {
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
const columns = [
{
title: t("jobs.fields.ro_number"),
@@ -44,6 +44,15 @@ function OwnerDetailJobsComponent({ bodyshop, owner }) {
title: t("jobs.fields.vehicle"),
dataIndex: "vehicleid",
key: "vehicleid",
sorter: (a, b) =>
alphaSort(
`${a.v_model_yr || ""} ${a.v_make_desc || ""} ${
a.v_model_desc || ""
}`,
`${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
),
sortOrder:
state.sortedInfo.columnKey === "vehicleid" && state.sortedInfo.order,
render: (text, record) =>
record.vehicleid ? (
<Link to={`/manage/vehicles/${record.vehicleid}`}>
@@ -67,9 +76,15 @@ function OwnerDetailJobsComponent({ bodyshop, owner }) {
title: t("jobs.fields.status"),
dataIndex: "status",
key: "status",
sorter: (a, b) => statusSort(a.status, b.status, bodyshop.md_ro_statuses.statuses),
sorter: (a, b) =>
statusSort(a.status, b.status, bodyshop.md_ro_statuses.statuses),
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
filters: bodyshop.md_ro_statuses.statuses.map((status) => ({
text: status,
value: status,
})),
onFilter: (value, record) => value.includes(record.status),
},
{

View File

@@ -1,5 +1,5 @@
import { DeleteFilled, EyeFilled, SyncOutlined } from "@ant-design/icons";
import { useMutation } from "@apollo/client";
import { useLazyQuery, useMutation } from "@apollo/client";
import {
Button,
Card,
@@ -13,20 +13,21 @@ import {
Table,
} from "antd";
import queryString from "query-string";
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { QUERY_BILL_BY_PK } from "../../graphql/bills.queries";
import { DELETE_PARTS_ORDER } from "../../graphql/parts-orders.queries";
import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter } from "../../utils/DateFormatter";
import { alphaSort } from "../../utils/sorters";
import { TemplateList } from "../../utils/TemplateConstants";
import { alphaSort } from "../../utils/sorters";
import DataLabel from "../data-label/data-label.component";
import PartsOrderBackorderEta from "../parts-order-backorder-eta/parts-order-backorder-eta.component";
import PartsOrderCmReceived from "../parts-order-cm-received/parts-order-cm-received.component";
@@ -78,19 +79,46 @@ export function PartsOrderListTableComponent({
const [state, setState] = useState({
sortedInfo: {},
});
const [returnfrombill, setReturnFromBill] = useState();
const [billData, setBillData] = useState();
const search = queryString.parse(useLocation().search);
const selectedpartsorder = search.partsorderid;
const [searchText, setSearchText] = useState("");
const [billQuery] = useLazyQuery(QUERY_BILL_BY_PK);
const [deletePartsOrder] = useMutation(DELETE_PARTS_ORDER);
const parts_orders = billsQuery.data ? billsQuery.data.parts_orders : [];
const { refetch } = billsQuery;
useEffect(() => {
if (returnfrombill === null) {
setBillData(null);
} else {
const fetchData = async () => {
const result = await billQuery({
variables: { billid: returnfrombill },
});
setBillData(result.data);
};
fetchData();
}
}, [returnfrombill, billQuery]);
const recordActions = (record, showView = false) => (
<Space wrap>
{showView && (
<Button onClick={() => handleOnRowClick(record)}>
<Button
onClick={() => {
if (record.returnfrombill) {
setReturnFromBill(record.returnfrombill);
} else {
setReturnFromBill(null);
}
handleOnRowClick(record);
}}
>
<EyeFilled />
</Button>
)}
@@ -417,7 +445,11 @@ export function PartsOrderListTableComponent({
return (
<div>
<PageHeader
title={record && `${record.vendor.name} - ${record.order_number}`}
title={
billData
? `${record.vendor.name} - ${record.order_number} - ${t("bills.labels.returnfrombill")}: ${billData.bills_by_pk.invoice_number}`
: `${record.vendor.name} - ${record.order_number}`
}
extra={recordActions(record)}
/>
<Table

View File

@@ -1,17 +1,17 @@
import { DeleteFilled, WarningFilled, DownOutlined } from "@ant-design/icons";
import { DeleteFilled, DownOutlined, WarningFilled } from "@ant-design/icons";
import { useTreatments } from "@splitsoftware/splitio-react";
import {
Checkbox,
Divider,
Dropdown,
Form,
Input,
InputNumber,
Menu,
Radio,
Select,
Space,
Tag,
Select,
Menu,
Dropdown,
Checkbox,
} from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
@@ -255,7 +255,7 @@ export function PartsOrderModalComponent({
},
]}
>
<InputNumber />
<InputNumber min={1} />
</Form.Item>
<Form.Item
label={t("parts_orders.fields.act_price")}

View File

@@ -95,15 +95,13 @@ export function PartsQueueListComponent({ bodyshop }) {
};
const handleOnRowClick = (record) => {
if (record) {
if (record.id) {
history.push({
search: queryString.stringify({
...searchParams,
selected: record.id,
}),
});
}
if (record?.id) {
history.replace({
search: queryString.stringify({
...searchParams,
selected: record.id,
}),
});
}
};
@@ -350,6 +348,13 @@ export function PartsQueueListComponent({ bodyshop }) {
selectedRowKeys: [selected],
type: "radio",
}}
onRow={(record, rowIndex) => {
return {
onClick: (event) => {
handleOnRowClick(record);
},
};
}}
/>
</Card>
);

View File

@@ -1,16 +1,18 @@
import React from "react";
import { Button, notification } from "antd";
import { useMutation } from "@apollo/client";
import { Button, notification } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
import { setModalContext } from "../../redux/modals/modals.actions";
import { connect } from "react-redux";
import { UPDATE_PAYMENT } from "../../graphql/payments.queries";
import { selectCurrentUser } from "../../redux/user/user.selectors";
import { createStructuredSelector } from "reselect";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
import { UPDATE_PAYMENT } from "../../graphql/payments.queries";
import { setModalContext } from "../../redux/modals/modals.actions";
import { selectPayment } from "../../redux/modals/modals.selectors";
import { selectCurrentUser } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
paymentModal: selectPayment,
});
const mapDispatchToProps = (dispatch) => ({
@@ -24,6 +26,7 @@ const PaymentMarkForExportButton = ({
refetch,
setPaymentContext,
currentUser,
paymentModal,
}) => {
const { t } = useTranslation();
const [insertExportLog, { loading: exportLogLoading }] =
@@ -46,7 +49,6 @@ const PaymentMarkForExportButton = ({
],
},
});
const paymentUpdateResponse = await updatePayment({
variables: {
paymentId: payment.id,
@@ -55,25 +57,33 @@ const PaymentMarkForExportButton = ({
},
},
});
if (!!!paymentUpdateResponse.errors) {
notification.open({
type: "success",
key: "paymentsuccessmarkforexport",
message: t("payments.successes.markexported"),
});
if (refetch) refetch();
setPaymentContext({
actions: {
refetch,
},
context: {
...paymentModal.context,
...payment,
exportedat: today,
},
});
if (refetch) {
if (paymentModal.context.refetchRequiresContext) {
refetch(
paymentUpdateResponse &&
paymentUpdateResponse.data.update_payments.returning[0]
);
} else {
refetch();
}
}
} else {
notification["error"]({
message: t("payments.errors.exporting", {

View File

@@ -1,5 +1,4 @@
import { useMutation } from "@apollo/client";
import { Button, Form, Modal, notification, Space } from "antd";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
@@ -19,8 +18,8 @@ import {
import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants";
import PaymentForm from "../payment-form/payment-form.component";
import PaymentReexportButton from "../payment-reexport-button/payment-reexport-button.component";
import PaymentMarkForExportButton from "../payment-mark-export-button/payment-mark-export-button-component";
import PaymentReexportButton from "../payment-reexport-button/payment-reexport-button.component";
const mapStateToProps = createStructuredSelector({
paymentModal: selectPayment,
@@ -97,16 +96,21 @@ function PaymentModalContainer({
});
if (!!!updatedPayment.errors) {
notification["success"]({ message: t("payments.successes.payment") });
notification["success"]({ message: t("payments.successes.paymentupdate") });
} else {
notification["error"]({ message: t("payments.errors.payment") });
notification["error"]({ message: t("payments.errors.paymentupdate") });
}
}
if (actions.refetch)
actions.refetch(
updatedPayment && updatedPayment.data.update_payments.returning[0]
);
if (actions.refetch) {
if (context.refetchRequiresContext) {
actions.refetch(
updatedPayment && updatedPayment.data.update_payments.returning[0]
);
} else {
actions.refetch();
}
}
if (enterAgain) {
const prev = form.getFieldsValue(["date"]);
@@ -159,7 +163,7 @@ function PaymentModalContainer({
}}
afterClose={() => form.resetFields()}
footer={
<span>
<Space>
<Button onClick={handleCancel}>{t("general.actions.cancel")}</Button>
<Button loading={loading} onClick={() => form.submit()}>
{t("general.actions.save")}
@@ -175,7 +179,7 @@ function PaymentModalContainer({
{t("general.actions.saveandnew")}
</Button>
)}
</span>
</Space>
}
>
{!context || (context && !context.id) ? null : (
@@ -194,7 +198,6 @@ function PaymentModalContainer({
autoComplete={"off"}
form={form}
layout="vertical"
initialValues={context || {}}
disabled={context?.exportedat}
>
<PaymentForm form={form} />

View File

@@ -1,17 +1,27 @@
import React from "react";
import { Button, notification } from "antd";
import { useTranslation } from "react-i18next";
import { UPDATE_PAYMENT } from "../../graphql/payments.queries";
import { useMutation } from "@apollo/client";
import { setModalContext } from "../../redux/modals/modals.actions";
import { Button, notification } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { UPDATE_PAYMENT } from "../../graphql/payments.queries";
import { setModalContext } from "../../redux/modals/modals.actions";
import { selectPayment } from "../../redux/modals/modals.selectors";
const mapStateToProps = createStructuredSelector({
paymentModal: selectPayment,
});
const mapDispatchToProps = (dispatch) => ({
setPaymentContext: (context) =>
dispatch(setModalContext({ context: context, modal: "payment" })),
});
const PaymentReexportButton = ({ payment, refetch, setPaymentContext }) => {
const PaymentReexportButton = ({
paymentModal,
payment,
refetch,
setPaymentContext,
}) => {
const { t } = useTranslation();
const [updatePayment, { loading }] = useMutation(UPDATE_PAYMENT);
@@ -24,25 +34,32 @@ const PaymentReexportButton = ({ payment, refetch, setPaymentContext }) => {
},
},
});
if (!!!paymentUpdateResponse.errors) {
notification.open({
type: "success",
key: "paymentsuccessexport",
message: t("payments.successes.markreexported"),
});
if (refetch) refetch();
setPaymentContext({
actions: {
refetch,
},
context: {
...paymentModal.context,
...payment,
exportedat: null,
},
});
if (refetch) {
if (paymentModal.context.refetchRequiresContext) {
refetch(
paymentUpdateResponse &&
paymentUpdateResponse.data.update_payments.returning[0]
);
} else {
refetch();
}
}
} else {
notification["error"]({
message: t("payments.errors.exporting", {
@@ -63,4 +80,7 @@ const PaymentReexportButton = ({ payment, refetch, setPaymentContext }) => {
);
};
export default connect(null, mapDispatchToProps)(PaymentReexportButton);
export default connect(
mapStateToProps,
mapDispatchToProps
)(PaymentReexportButton);

View File

@@ -14,11 +14,11 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter";
import { TemplateList } from "../../utils/TemplateConstants";
import { pageLimit } from "../../utils/config";
import { alphaSort } from "../../utils/sorters";
import CaBcEtfTableModalContainer from "../ca-bc-etf-table-modal/ca-bc-etf-table-modal.container";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
import {pageLimit} from "../../utils/config";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
@@ -184,7 +184,10 @@ export function PaymentsListPaginated({
}
: refetch,
},
context: apolloResults ? apolloResults : record,
context: {
...(apolloResults ? apolloResults : record),
refetchRequiresContext: true,
},
});
}}
>

View File

@@ -6,6 +6,7 @@ import {
notification,
Popover,
Radio,
Space,
} from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
@@ -115,10 +116,16 @@ export function PrintCenterJobsLabels({ bodyshop, jobId }) {
>
<InputNumber min={1} precision={0} max={99} />
</Form.Item>
<Button type="primary" loading={loading} onClick={handleOk}>
{t("general.actions.print")}
</Button>
<Button onClick={handleCancel}>{t("general.actions.cancel")}</Button>
<div style={{ display: "flex", justifyContent: "flex-end" }}>
<Space>
<Button type="primary" loading={loading} onClick={handleOk}>
{t("general.actions.print")}
</Button>
<Button onClick={handleCancel}>
{t("general.actions.cancel")}
</Button>
</Space>
</div>
</Form>
</Card>
);

View File

@@ -18,8 +18,8 @@ const sortByParentId = (arr) => {
//console.log("sortByParentId -> byParentsIdsList", byParentsIdsList);
while (byParentsIdsList[parentId]) {
sortedList.push(byParentsIdsList[parentId][0]);
parentId = byParentsIdsList[parentId][0].id;
sortedList.push(...byParentsIdsList[parentId]); //Spread in the whole list in case several items have the same parents.
parentId = byParentsIdsList[parentId][byParentsIdsList[parentId].length -1].id; //Grab the ID from the last one.
}
if (byParentsIdsList["null"])

View File

@@ -1,7 +1,6 @@
import { BranchesOutlined, PauseCircleOutlined } from "@ant-design/icons";
import { Checkbox, Space, Tooltip } from "antd";
import i18n from "i18next";
import moment from "moment";
import { Link } from "react-router-dom";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { TimeFormatter } from "../../utils/DateFormatter";
@@ -190,17 +189,12 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
state.sortedInfo.columnKey === "date_next_contact" &&
state.sortedInfo.order,
render: (text, record) => (
<span
style={{
color:
record.date_next_contact &&
moment(record.date_next_contact).isBefore(moment())
? "red"
: "",
}}
>
<ProductionListDate record={record} field="date_next_contact" time />
</span>
<ProductionListDate
record={record}
field="date_next_contact"
pastIndicator
time
/>
),
},
{
@@ -308,7 +302,7 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
onFilter: (value, record) =>
value.includes(record.special_coverage_policy),
render: (text, record) => (
<Checkbox disabled checked={record.special_coverage_policy} />
<Checkbox checked={record.special_coverage_policy} />
),
},

View File

@@ -1,5 +1,5 @@
import Icon from "@ant-design/icons";
import { Popover } from "antd";
import { Popover, Space } from "antd";
import _ from "lodash";
import moment from "moment";
import React, { useMemo } from "react";
@@ -69,19 +69,22 @@ export function ScheduleCalendarHeaderComponent({
{loadData && loadData.allJobsOut ? (
loadData.allJobsOut.map((j) => (
<tr key={j.id}>
<td>
<Link to={`/manage/jobs/${j.id}`}>{j.ro_number}</Link>
<td style={{ padding: "2.5px" }}>
<Link to={`/manage/jobs/${j.id}`}>{j.ro_number}</Link> (
{j.status})
</td>
<td>
<td style={{ padding: "2.5px" }}>
<OwnerNameDisplay ownerObject={j} />
</td>
<td>
{`(${(
j.labhrs.aggregate.sum.mod_lb_hrs +
j.larhrs.aggregate.sum.mod_lb_hrs
<td style={{ padding: "2.5px" }}>
{`(${j.labhrs?.aggregate?.sum?.mod_lb_hrs?.toFixed(1) || 0}/${
j.larhrs?.aggregate?.sum?.mod_lb_hrs?.toFixed(1) || 0
}/${(
j.labhrs.aggregate?.sum?.mod_lb_hrs +
j.larhrs.aggregate?.sum?.mod_lb_hrs
).toFixed(1)} ${t("general.labels.hours")})`}
</td>
<td>
<td style={{ padding: "2.5px" }}>
<DateTimeFormatter>
{j.scheduled_completion}
</DateTimeFormatter>
@@ -90,7 +93,9 @@ export function ScheduleCalendarHeaderComponent({
))
) : (
<tr>
<td>{t("appointments.labels.nocompletingjobs")}</td>
<td style={{ padding: "2.5px" }}>
{t("appointments.labels.nocompletingjobs")}
</td>
</tr>
)}
</tbody>
@@ -105,27 +110,30 @@ export function ScheduleCalendarHeaderComponent({
{loadData && loadData.allJobsIn ? (
loadData.allJobsIn.map((j) => (
<tr key={j.id}>
<td>
<td style={{ padding: "2.5px" }}>
<Link to={`/manage/jobs/${j.id}`}>{j.ro_number}</Link>
{j.status}
</td>
<td>
<td style={{ padding: "2.5px" }}>
<OwnerNameDisplay ownerObject={j} />
</td>
<td>
{`(${(
j.labhrs.aggregate.sum.mod_lb_hrs +
j.larhrs.aggregate.sum.mod_lb_hrs
<td style={{ padding: "2.5px" }}>
{`(${j.labhrs?.aggregate?.sum.mod_lb_hrs?.toFixed(1) || 0}/${
j.larhrs?.aggregate?.sum?.mod_lb_hrs?.toFixed(1) || 0
}/${(
j.labhrs?.aggregate?.sum?.mod_lb_hrs +
j.larhrs?.aggregate?.sum?.mod_lb_hrs
).toFixed(1)} ${t("general.labels.hours")})`}
</td>
<td>
<td style={{ padding: "2.5px" }}>
<DateTimeFormatter>{j.scheduled_in}</DateTimeFormatter>
</td>
</tr>
))
) : (
<tr>
<td>{t("appointments.labels.noarrivingjobs")}</td>
<td style={{ padding: "2.5px" }}>
{t("appointments.labels.noarrivingjobs")}
</td>
</tr>
)}
</tbody>
@@ -136,25 +144,32 @@ export function ScheduleCalendarHeaderComponent({
const LoadComponent = loadData ? (
<div>
<div className="imex-flex-row imex-flex-row__flex-space-around">
<Popover
placement={"bottom"}
content={jobsInPopup}
trigger="hover"
title={t("appointments.labels.arrivingjobs")}
>
<Icon component={MdFileDownload} style={{ color: "green" }} />
{(loadData.allHoursIn || 0) && loadData.allHoursIn.toFixed(2)}
</Popover>
<Popover
placement={"bottom"}
content={jobsOutPopup}
trigger="hover"
title={t("appointments.labels.completingjobs")}
>
<Icon component={MdFileUpload} style={{ color: "red" }} />
{(loadData.allHoursOut || 0) && loadData.allHoursOut.toFixed(2)}
</Popover>
<ScheduleCalendarHeaderGraph loadData={loadData} />
<Space>
<Popover
placement={"bottom"}
content={jobsInPopup}
trigger="hover"
title={t("appointments.labels.arrivingjobs")}
>
<Icon component={MdFileDownload} style={{ color: "green" }} />
{(loadData.allHoursInBody || 0) &&
loadData.allHoursInBody.toFixed(1)}
/
{(loadData.allHoursInRefinish || 0) &&
loadData.allHoursInRefinish.toFixed(1)}
/{(loadData.allHoursIn || 0) && loadData.allHoursIn.toFixed(1)}
</Popover>
<Popover
placement={"bottom"}
content={jobsOutPopup}
trigger="hover"
title={t("appointments.labels.completingjobs")}
>
<Icon component={MdFileUpload} style={{ color: "red" }} />
{(loadData.allHoursOut || 0) && loadData.allHoursOut.toFixed(1)}
</Popover>
<ScheduleCalendarHeaderGraph loadData={loadData} />
</Space>
</div>
<div>
<ul style={{ listStyleType: "none", columns: "2 auto", padding: 0 }}>

View File

@@ -5,6 +5,7 @@ import {
Input,
InputNumber,
Select,
Space,
Switch,
Typography,
} from "antd";
@@ -17,6 +18,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { useTreatments } from "@splitsoftware/splitio-react";
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
const SelectorDiv = styled.div`
.ant-form-item .ant-select {
@@ -191,7 +193,7 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
</LayoutFormRow>
<LayoutFormRow header={t("bodyshop.labels.dms.cdk.payers")}>
<Form.List name={["cdk_configuration", "payers"]}>
{(fields, { add, remove }) => {
{(fields, { add, remove, move }) => {
return (
<div>
{fields.map((field, index) => (
@@ -249,11 +251,18 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
</Select>
</Form.Item>
<DeleteFilled
onClick={() => {
remove(field.name);
}}
/>
<Space align="center">
<DeleteFilled
onClick={() => {
remove(field.name);
}}
/>
<FormListMoveArrows
move={move}
index={index}
total={fields.length}
/>
</Space>
</LayoutFormRow>
</Form.Item>
))}
@@ -345,7 +354,7 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
id="costs"
>
<Form.List name={["md_responsibility_centers", "costs"]}>
{(fields, { add, remove }) => {
{(fields, { add, remove, move }) => {
return (
<div>
{fields.map((field, index) => (
@@ -462,12 +471,18 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
<Input onBlur={handleBlur} />
</Form.Item>
)}
<DeleteFilled
onClick={() => {
remove(field.name);
}}
/>
<Space align="center">
<DeleteFilled
onClick={() => {
remove(field.name);
}}
/>
<FormListMoveArrows
move={move}
index={index}
total={fields.length}
/>
</Space>
</LayoutFormRow>
</Form.Item>
))}
@@ -493,7 +508,7 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
id="profits"
>
<Form.List name={["md_responsibility_centers", "profits"]}>
{(fields, { add, remove }) => {
{(fields, { add, remove, move }) => {
return (
<div>
{fields.map((field, index) => (
@@ -595,11 +610,18 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
<Input onBlur={handleBlur} />
</Form.Item>
)}
<DeleteFilled
onClick={() => {
remove(field.name);
}}
/>
<Space align="center">
<DeleteFilled
onClick={() => {
remove(field.name);
}}
/>
<FormListMoveArrows
move={move}
index={index}
total={fields.length}
/>
</Space>
</LayoutFormRow>
</Form.Item>
))}

View File

@@ -39,8 +39,23 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
form.getFieldValue(["md_ro_statuses", "statuses"]) || []
);
const [productionStatus, setProductionStatus] = useState(
(
form.getFieldValue(["md_ro_statuses", "production_statuses"]) || []
).concat(
form.getFieldValue(["md_ro_statuses", "additional_board_statuses"]) || []
) || []
);
const handleBlur = () => {
setOptions(form.getFieldValue(["md_ro_statuses", "statuses"]));
setProductionStatus(
form
.getFieldValue(["md_ro_statuses", "production_statuses"])
.concat(
form.getFieldValue(["md_ro_statuses", "additional_board_statuses"])
)
);
};
return (
@@ -346,7 +361,7 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
]}
>
<Select>
{options.map((item, idx) => (
{productionStatus.map((item, idx) => (
<Select.Option key={idx} value={item}>
{item}
</Select.Option>

View File

@@ -7,7 +7,9 @@ import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { alphaSort, statusSort } from "../../utils/sorters";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import OwnerNameDisplay, {
OwnerNameDisplayFunction,
} from "../owner-name-display/owner-name-display.component";
import VehicleDetailUpdateJobsComponent from "../vehicle-detail-update-jobs/vehicle-detail-update-jobs.component";
const mapStateToProps = createStructuredSelector({
@@ -45,6 +47,10 @@ export function VehicleDetailJobsComponent({ vehicle, bodyshop }) {
title: t("jobs.fields.owner"),
dataIndex: "owner",
key: "owner",
sorter: (a, b) =>
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => (
<Link to={`/manage/owners/${record.owner.id}`}>
<OwnerNameDisplay ownerObject={record} />
@@ -63,9 +69,15 @@ export function VehicleDetailJobsComponent({ vehicle, bodyshop }) {
title: t("jobs.fields.status"),
dataIndex: "status",
key: "status",
sorter: (a, b) => statusSort(a.status, b.status, bodyshop.md_ro_statuses.statuses),
sorter: (a, b) =>
statusSort(a.status, b.status, bodyshop.md_ro_statuses.statuses),
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
filters: bodyshop.md_ro_statuses.statuses.map((status) => ({
text: status,
value: status,
})),
onFilter: (value, record) => value.includes(record.status),
},
{

View File

@@ -4,8 +4,9 @@ import queryString from "query-string";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link, useHistory, useLocation } from "react-router-dom";
import { pageLimit } from "../../utils/config";
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
import {pageLimit} from "../../utils/config";
import { alphaSort } from './../../utils/sorters';
export default function VehiclesListComponent({
loading,
vehicles,
@@ -31,6 +32,8 @@ export default function VehiclesListComponent({
title: t("vehicles.fields.v_vin"),
dataIndex: "v_vin",
key: "v_vin",
sorter: (a, b) => alphaSort(a.v_vin, b.v_vin),
sortOrder: state.sortedInfo.columnKey === "v_vin" && state.sortedInfo.order,
render: (text, record) => (
<Link to={"/manage/vehicles/" + record.id}>
<VehicleVinDisplay>{record.v_vin || "N/A"}</VehicleVinDisplay>
@@ -51,8 +54,10 @@ export default function VehiclesListComponent({
},
{
title: t("vehicles.fields.plate_no"),
dataIndex: "plate",
key: "plate",
dataIndex: "plate_no",
key: "plate_no",
sorter: (a, b) => alphaSort(a.plate_no, b.plate_no),
sortOrder: state.sortedInfo.columnKey === "plate_no" && state.sortedInfo.order,
render: (text, record) => {
return (
<span>{`${record.plate_st || ""} | ${record.plate_no || ""}`}</span>

View File

@@ -4,6 +4,8 @@ import { getAuth, updatePassword, updateProfile } from "firebase/auth";
import { getFirestore } from "firebase/firestore";
import { getMessaging, getToken, onMessage } from "firebase/messaging";
import { store } from "../redux/store";
import axios from "axios";
import { checkBeta } from "../utils/handleBeta";
const config = JSON.parse(process.env.REACT_APP_FIREBASE_CONFIG);
initializeApp(config);
@@ -86,6 +88,18 @@ export const logImEXEvent = (eventName, additionalParams, stateProp = null) => {
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: checkBeta() ? "beta" : "master",
});
// console.log(
// "%c[Analytics]",
// "background-color: green ;font-weight:bold;",

View File

@@ -73,6 +73,7 @@ export const QUERY_BILLS_BY_JOBID = gql`
order_date
deliver_by
return
returnfrombill
orderedby
parts_order_lines {
id

View File

@@ -46,7 +46,7 @@ export const QUERY_AVAILABLE_CC = gql`
`;
export const CHECK_CC_FLEET_NUMBER = gql`
query CHECK_VENDOR_NAME($name: String!) {
query CHECK_CC_FLEET_NUMBER($name: String!) {
courtesycars_aggregate(where: { fleetnumber: { _ilike: $name } }) {
aggregate {
count

View File

@@ -172,6 +172,12 @@ export const UPDATE_JOB_LINE = gql`
id
notes
mod_lbr_ty
mod_lb_hrs
part_type
op_code_desc
prt_dsmk_m
prt_dsmk_p
tax_part
part_qty
db_price
act_price

View File

@@ -704,6 +704,7 @@ export const GET_JOB_BY_PK = gql`
other_amount_payable
owner {
id
note
ownr_fn
ownr_ln
ownr_co_nm
@@ -2204,6 +2205,8 @@ export const GET_JOB_LINE_ORDERS = gql`
parts_order_lines(where: { job_line_id: { _eq: $joblineid } }) {
id
act_price
backordered_eta
backordered_on
parts_order {
id
order_date

View File

@@ -31,6 +31,7 @@ export const QUERY_VEHICLE_BY_ID = gql`
jobs(order_by: { date_open: desc }) {
id
ro_number
ownr_co_nm
ownr_fn
ownr_ln
owner {

View File

@@ -13,8 +13,8 @@ import { setModalContext } from "../../redux/modals/modals.actions";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter } from "../../utils/DateFormatter";
import { TemplateList } from "../../utils/TemplateConstants";
import { pageLimit } from "../../utils/config";
import { alphaSort, dateSort } from "../../utils/sorters";
import {pageLimit} from "../../utils/config";
const mapDispatchToProps = (dispatch) => ({
setPartsOrderContext: (context) =>
@@ -125,9 +125,7 @@ export function BillsListPage({
sortOrder:
state.sortedInfo.columnKey === "is_credit_memo" &&
state.sortedInfo.order,
render: (text, record) => (
<Checkbox disabled checked={record.is_credit_memo} />
),
render: (text, record) => <Checkbox checked={record.is_credit_memo} />,
},
{
title: t("bills.fields.exported"),
@@ -136,7 +134,7 @@ export function BillsListPage({
sorter: (a, b) => a.exported - b.exported,
sortOrder:
state.sortedInfo.columnKey === "exported" && state.sortedInfo.order,
render: (text, record) => <Checkbox disabled checked={record.exported} />,
render: (text, record) => <Checkbox checked={record.exported} />,
},
{
title: t("general.labels.actions"),
@@ -243,7 +241,7 @@ export function BillsListPage({
extra={
<Space wrap>
{search.search && (
<>
<Space align="center">
<Typography.Title level={4}>
{t("general.labels.searchresults", { search: search.search })}
</Typography.Title>
@@ -256,7 +254,7 @@ export function BillsListPage({
>
{t("general.actions.clear")}
</Button>
</>
</Space>
)}
<Button onClick={() => refetch()}>
<SyncOutlined />

View File

@@ -76,7 +76,11 @@ export function CourtesyCarCreateContainer({
onFinish={handleFinish}
layout="vertical"
>
<CourtesyCarFormComponent form={form} saveLoading={loading} />
<CourtesyCarFormComponent
form={form}
saveLoading={loading}
newCC={true}
/>
</Form>
</RbacWrapper>
);

View File

@@ -167,9 +167,7 @@ export function ExportLogsPageComponent({ bodyshop }) {
{ text: "False", value: false },
],
onFilter: (value, record) => record.successful === value,
render: (text, record) => (
<Checkbox disabled checked={record.successful} />
),
render: (text, record) => <Checkbox checked={record.successful} />,
},
{
title: t("general.labels.message"),

View File

@@ -131,7 +131,12 @@ export function* calculateScheduleLoad({ payload: end }) {
(load[itemDate].allHoursIn || 0) +
item.labhrs.aggregate.sum.mod_lb_hrs +
item.larhrs.aggregate.sum.mod_lb_hrs;
load[itemDate].allHoursInBody =
(load[itemDate].allHoursInBody || 0) +
item.labhrs.aggregate.sum.mod_lb_hrs;
load[itemDate].allHoursInRefinish =
(load[itemDate].allHoursInRefinish || 0) +
item.larhrs.aggregate.sum.mod_lb_hrs;
//If the job hasn't already arrived, add it to the jobs in list.
// Make sure it also hasn't already been completed, or isn't an in and out job.
//This prevents the duplicate counting.
@@ -142,6 +147,12 @@ export function* calculateScheduleLoad({ payload: end }) {
(load[itemDate].hoursIn || 0) +
item.labhrs.aggregate.sum.mod_lb_hrs +
item.larhrs.aggregate.sum.mod_lb_hrs;
load[itemDate].hoursInBody =
(load[itemDate].hoursInBody || 0) +
item.labhrs.aggregate.sum.mod_lb_hrs;
load[itemDate].hoursInRefinish =
(load[itemDate].hoursInRefinish || 0) +
item.larhrs.aggregate.sum.mod_lb_hrs;
}
} else {
load[itemDate] = {
@@ -152,10 +163,18 @@ export function* calculateScheduleLoad({ payload: end }) {
allHoursIn:
item.labhrs.aggregate.sum.mod_lb_hrs +
item.larhrs.aggregate.sum.mod_lb_hrs,
allHoursInBody: item.labhrs.aggregate.sum.mod_lb_hrs,
allHoursInRefinish: item.larhrs.aggregate.sum.mod_lb_hrs,
hoursIn: AddJobForSchedulingCalc
? item.labhrs.aggregate.sum.mod_lb_hrs +
item.larhrs.aggregate.sum.mod_lb_hrs
: 0,
hoursInBody: AddJobForSchedulingCalc
? item.labhrs.aggregate.sum.mod_lb_hrs
: 0,
hoursInRefinish: AddJobForSchedulingCalc
? item.larhrs.aggregate.sum.mod_lb_hrs
: 0,
};
}
});
@@ -179,6 +198,12 @@ export function* calculateScheduleLoad({ payload: end }) {
(load[itemDate].allHoursOut || 0) +
item.labhrs.aggregate.sum.mod_lb_hrs +
item.larhrs.aggregate.sum.mod_lb_hrs;
load[itemDate].allHoursOutBody =
(load[itemDate].allHoursOutBody || 0) +
item.labhrs.aggregate.sum.mod_lb_hrs;
load[itemDate].allHoursOutRefinish =
(load[itemDate].allHoursOutRefinish || 0) +
item.larhrs.aggregate.sum.mod_lb_hrs;
//Add only the jobs that are still in production to get rid of.
//If it's not in production, we'd subtract unnecessarily.
load[itemDate].allJobsOut.push(item);
@@ -189,6 +214,12 @@ export function* calculateScheduleLoad({ payload: end }) {
(load[itemDate].hoursOut || 0) +
item.labhrs.aggregate.sum.mod_lb_hrs +
item.larhrs.aggregate.sum.mod_lb_hrs;
load[itemDate].hoursOutBody =
(load[itemDate].hoursOutBody || 0) +
item.labhrs.aggregate.sum.mod_lb_hrs;
load[itemDate].hoursOutRefinish =
(load[itemDate].hoursOutRefinish || 0) +
item.larhrs.aggregate.sum.mod_lb_hrs;
}
} else {
load[itemDate] = {
@@ -201,6 +232,8 @@ export function* calculateScheduleLoad({ payload: end }) {
allHoursOut:
item.labhrs.aggregate.sum.mod_lb_hrs +
item.larhrs.aggregate.sum.mod_lb_hrs,
allHoursOutBody: item.labhrs.aggregate.sum.mod_lb_hrs,
allHoursOutRefinish: item.larhrs.aggregate.sum.mod_lb_hrs,
};
}
});

View File

@@ -217,10 +217,12 @@
"markexported": "Mark Exported",
"markforreexport": "Mark for Re-export",
"new": "New Bill",
"nobilllines": "This part has not yet been recieved.",
"noneselected": "No bill selected.",
"onlycmforinvoiced": "Only credit memos can be entered for any Job that has been invoiced, exported, or voided.",
"onlycmforinvoiced": "Only credit memos can be entered for any Job that has been invoiced, exported, or voided.",
"printlabels": "Print Labels",
"retailtotal": "Bills Retail Total",
"returnfrombill": "Return From Bill",
"savewithdiscrepancy": "You are about to save this bill with a discrepancy. The system will continue to use the calculated amount using the bill lines. Press cancel to return to the bill.",
"state_tax": "Provincial/State Tax",
"subtotal": "Subtotal",
@@ -259,6 +261,7 @@
"saving": "Error encountered while saving. {{message}}"
},
"fields": {
"ReceivableCustomField": "QBO Receivable Custom Field {{number}}",
"address1": "Address 1",
"address2": "Address 2",
"appt_alt_transport": "Appointment Alternative Transportation Options",
@@ -477,7 +480,6 @@
"editaccess": "Users -> Edit access"
}
},
"ReceivableCustomField": "QBO Receivable Custom Field {{number}}",
"responsibilitycenter": "Responsibility Center",
"responsibilitycenter_accountdesc": "Account Description",
"responsibilitycenter_accountitem": "Item",
@@ -608,7 +610,7 @@
"dms": {
"cdk": {
"controllist": "Control Number List",
"payers": "CDK Payers"
"payers": "Payers"
},
"cdk_dealerid": "CDK Dealer ID",
"pbs_serialnumber": "PBS Serial Number",
@@ -844,8 +846,8 @@
"notconfigured": "You do not have any current CSI Question Sets configured.",
"notfoundsubtitle": "We were unable to find a survey using the link you provided. Please ensure the URL is correct or reach out to your shop for more help.",
"notfoundtitle": "No survey found.",
"surveycompletetitle": "Survey previously completed",
"surveycompletesubtitle": "This survey was already completed on {{date}}."
"surveycompletesubtitle": "This survey was already completed on {{date}}.",
"surveycompletetitle": "Survey previously completed"
},
"fields": {
"completedon": "Completed On",
@@ -854,13 +856,13 @@
"validuntil": "Valid Until"
},
"labels": {
"copyright": "Copyright © $t(titles.app). All Rights Reserved.",
"greeting": "Hi {{name}}!",
"intro": "At {{shopname}}, we value your feedback. We would love to hear what you have to say. Please fill out the form below.",
"nologgedinuser": "Please log out of $t(titles.app)",
"nologgedinuser_sub": "Users of $t(titles.app) cannot complete CSI surveys while logged in. Please log out and try again.",
"noneselected": "No response selected.",
"title": "Customer Satisfaction Survey",
"greeting": "Hi {{name}}!",
"intro": "At {{shopname}}, we value your feedback. We would love to hear what you have to say. Please fill out the form below.",
"copyright": "Copyright © $t(titles.app). All Rights Reserved."
"title": "Customer Satisfaction Survey"
},
"successes": {
"created": "CSI created successfully.",
@@ -898,7 +900,8 @@
"scheduledindate": "Sheduled In Today: {{date}}",
"scheduledintoday": "Sheduled In Today",
"scheduledoutdate": "Sheduled Out Today: {{date}}",
"scheduledouttoday": "Sheduled Out Today"
"scheduledouttoday": "Sheduled Out Today",
"joblifecycle": "Job Lifecycle"
}
},
"dms": {
@@ -1113,6 +1116,7 @@
"loadingshop": "Loading shop data...",
"loggingin": "Authorizing...",
"markedexported": "Manually marked as exported.",
"media": "Media",
"message": "Message",
"monday": "Monday",
"na": "N/A",
@@ -1236,7 +1240,15 @@
"relative_end": "Relative End",
"relative_start": "Relative Start",
"start": "Start",
"value": "Value"
"value": "Value",
"status": "Status",
"percentage": "Percentage",
"human_readable": "Human Readable",
"status_count": "In Status"
},
"titles": {
"dashboard": "Job Lifecycle",
"top_durations": "Top Durations"
},
"content": {
"current_status_accumulated_time": "Current Status Accumulated Time",
@@ -1248,7 +1260,9 @@
"title": "Job Lifecycle Component",
"title_durations": "Historical Status Durations",
"title_loading": "Loading",
"title_transitions": "Transitions"
"title_transitions": "Transitions",
"calculated_based_on": "Calculated based on",
"jobs_in_since": "Jobs in since"
},
"errors": {
"fetch": "Error getting Job Lifecycle Data"
@@ -1561,7 +1575,7 @@
"federal_tax_payable": "Federal Tax Payable",
"federal_tax_rate": "Federal Tax Rate",
"ins_addr1": "Insurance Co. Address",
"ins_city": "Insurance City",
"ins_city": "Insurance Co. City",
"ins_co_id": "Insurance Co. ID",
"ins_co_nm": "Insurance Company Name",
"ins_co_nm_short": "Ins. Co.",
@@ -1823,6 +1837,7 @@
"job": "Job Details",
"jobcosting": "Job Costing",
"jobtotals": "Job Totals",
"labor_hrs": "B/P/T Hrs",
"labor_rates_subtotal": "Labor Rates Subtotal",
"laborallocations": "Labor Allocations",
"labortotals": "Labor Totals",
@@ -2319,6 +2334,7 @@
"markexported": "Payment(s) marked exported.",
"markreexported": "Payment marked for re-export successfully",
"payment": "Payment created successfully. ",
"paymentupdate": "Payment updated successfully. ",
"stripe": "Credit card transaction charged successfully."
}
},
@@ -2417,6 +2433,7 @@
"invoice_total_payable": "Invoice (Total Payable)",
"iou_form": "IOU Form",
"job_costing_ro": "Job Costing",
"job_lifecycle_ro": "Job Lifecycle",
"job_notes": "Job Notes",
"key_tag": "Key Tag",
"labels": {
@@ -2583,17 +2600,17 @@
},
"labels": {
"advanced_filters": "Advanced Filters and Sorters",
"advanced_filters_show": "Show",
"advanced_filters_hide": "Hide",
"advanced_filters_filters": "Filters",
"advanced_filters_sorters": "Sorters",
"advanced_filters_filter_field": "Field",
"advanced_filters_sorter_field": "Field",
"advanced_filters_true": "True",
"advanced_filters_false": "False",
"advanced_filters_sorter_direction": "Direction",
"advanced_filters_filter_field": "Field",
"advanced_filters_filter_operator": "Operator",
"advanced_filters_filter_value": "Value",
"advanced_filters_filters": "Filters",
"advanced_filters_hide": "Hide",
"advanced_filters_show": "Show",
"advanced_filters_sorter_direction": "Direction",
"advanced_filters_sorter_field": "Field",
"advanced_filters_sorters": "Sorters",
"advanced_filters_true": "True",
"dates": "Dates",
"employee": "Employee",
"filterson": "Filters on {{object}}: {{field}}",
@@ -2675,6 +2692,8 @@
"job_costing_ro_date_summary": "Job Costing by RO - Summary",
"job_costing_ro_estimator": "Job Costing by Estimator",
"job_costing_ro_ins_co": "Job Costing by RO Source",
"job_lifecycle_date_detail": "Job Lifecycle by Date - Detail",
"job_lifecycle_date_summary": "Job Lifecycle by Date - Summary",
"jobs_completed_not_invoiced": "Jobs Completed not Invoiced",
"jobs_invoiced_not_exported": "Jobs Invoiced not Exported",
"jobs_reconcile": "Parts/Sublet/Labor Reconciliation",

View File

@@ -221,6 +221,7 @@
"onlycmforinvoiced": "",
"printlabels": "",
"retailtotal": "",
"returnfrombill": "",
"savewithdiscrepancy": "",
"state_tax": "",
"subtotal": "",
@@ -259,6 +260,7 @@
"saving": ""
},
"fields": {
"ReceivableCustomField": "",
"address1": "",
"address2": "",
"appt_alt_transport": "",
@@ -477,7 +479,6 @@
"editaccess": ""
}
},
"ReceivableCustomField": "",
"responsibilitycenter": "",
"responsibilitycenter_accountdesc": "",
"responsibilitycenter_accountitem": "",
@@ -844,8 +845,8 @@
"notconfigured": "",
"notfoundsubtitle": "",
"notfoundtitle": "",
"surveycompletetitle": "",
"surveycompletesubtitle": ""
"surveycompletesubtitle": "",
"surveycompletetitle": ""
},
"fields": {
"completedon": "",
@@ -854,13 +855,13 @@
"validuntil": ""
},
"labels": {
"copyright": "",
"greeting": "",
"intro": "",
"nologgedinuser": "",
"nologgedinuser_sub": "",
"noneselected": "",
"title": "",
"greeting": "",
"intro": "",
"copyright": ""
"title": ""
},
"successes": {
"created": "",
@@ -898,7 +899,8 @@
"scheduledindate": "",
"scheduledintoday": "",
"scheduledoutdate": "",
"scheduledouttoday": ""
"scheduledouttoday": "",
"joblifecycle": ""
}
},
"dms": {
@@ -1236,7 +1238,15 @@
"relative_end": "",
"relative_start": "",
"start": "",
"value": ""
"value": "",
"status": "",
"percentage": "",
"human_readable": "",
"status_count": ""
},
"titles": {
"dashboard": "",
"top_durations": ""
},
"content": {
"current_status_accumulated_time": "",
@@ -1248,7 +1258,9 @@
"title": "",
"title_durations": "",
"title_loading": "",
"title_transitions": ""
"title_transitions": "",
"calculated_based_on": "",
"jobs_in_since": ""
},
"errors": {
"fetch": "Error al obtener los datos del ciclo de vida del trabajo"
@@ -1823,6 +1835,7 @@
"job": "",
"jobcosting": "",
"jobtotals": "",
"labor_hrs": "",
"labor_rates_subtotal": "",
"laborallocations": "",
"labortotals": "",
@@ -2319,6 +2332,7 @@
"markexported": "",
"markreexported": "",
"payment": "",
"paymentupdate": "",
"stripe": ""
}
},
@@ -2417,6 +2431,7 @@
"invoice_total_payable": "",
"iou_form": "",
"job_costing_ro": "",
"job_lifecycle_ro": "",
"job_notes": "",
"key_tag": "",
"labels": {
@@ -2583,17 +2598,17 @@
},
"labels": {
"advanced_filters": "",
"advanced_filters_show": "",
"advanced_filters_hide": "",
"advanced_filters_filters": "",
"advanced_filters_sorters": "",
"advanced_filters_filter_field": "",
"advanced_filters_sorter_field": "",
"advanced_filters_true": "",
"advanced_filters_false": "",
"advanced_filters_sorter_direction": "",
"advanced_filters_filter_field": "",
"advanced_filters_filter_operator": "",
"advanced_filters_filter_value": "",
"advanced_filters_filters": "",
"advanced_filters_hide": "",
"advanced_filters_show": "",
"advanced_filters_sorter_direction": "",
"advanced_filters_sorter_field": "",
"advanced_filters_sorters": "",
"advanced_filters_true": "",
"dates": "",
"employee": "",
"filterson": "",
@@ -2675,6 +2690,8 @@
"job_costing_ro_date_summary": "",
"job_costing_ro_estimator": "",
"job_costing_ro_ins_co": "",
"job_lifecycle_date_detail": "",
"job_lifecycle_date_summary": "",
"jobs_completed_not_invoiced": "",
"jobs_invoiced_not_exported": "",
"jobs_reconcile": "",

View File

@@ -221,6 +221,7 @@
"onlycmforinvoiced": "",
"printlabels": "",
"retailtotal": "",
"returnfrombill": "",
"savewithdiscrepancy": "",
"state_tax": "",
"subtotal": "",
@@ -259,6 +260,7 @@
"saving": ""
},
"fields": {
"ReceivableCustomField": "",
"address1": "",
"address2": "",
"appt_alt_transport": "",
@@ -477,7 +479,6 @@
"editaccess": ""
}
},
"ReceivableCustomField": "",
"responsibilitycenter": "",
"responsibilitycenter_accountdesc": "",
"responsibilitycenter_accountitem": "",
@@ -844,8 +845,8 @@
"notconfigured": "",
"notfoundsubtitle": "",
"notfoundtitle": "",
"surveycompletetitle": "",
"surveycompletesubtitle": ""
"surveycompletesubtitle": "",
"surveycompletetitle": ""
},
"fields": {
"completedon": "",
@@ -854,13 +855,13 @@
"validuntil": ""
},
"labels": {
"copyright": "",
"greeting": "",
"intro": "",
"nologgedinuser": "",
"nologgedinuser_sub": "",
"noneselected": "",
"title": "",
"greeting": "",
"intro": "",
"copyright": ""
"title": ""
},
"successes": {
"created": "",
@@ -1236,7 +1237,15 @@
"relative_end": "",
"relative_start": "",
"start": "",
"value": ""
"value": "",
"status": "",
"percentage": "",
"human_readable": "",
"status_count": ""
},
"titles": {
"dashboard": "",
"top_durations": ""
},
"content": {
"current_status_accumulated_time": "",
@@ -1248,7 +1257,10 @@
"title": "",
"title_durations": "",
"title_loading": "",
"title_transitions": ""
"title_transitions": "",
"calculated_based_on": "",
"jobs_in_since": "",
"joblifecycle": ""
},
"errors": {
"fetch": "Erreur lors de l'obtention des données du cycle de vie des tâches"
@@ -1823,6 +1835,7 @@
"job": "",
"jobcosting": "",
"jobtotals": "",
"labor_hrs": "",
"labor_rates_subtotal": "",
"laborallocations": "",
"labortotals": "",
@@ -2319,6 +2332,7 @@
"markexported": "",
"markreexported": "",
"payment": "",
"paymentupdate": "",
"stripe": ""
}
},
@@ -2417,6 +2431,7 @@
"invoice_total_payable": "",
"iou_form": "",
"job_costing_ro": "",
"job_lifecycle_ro": "",
"job_notes": "",
"key_tag": "",
"labels": {
@@ -2583,17 +2598,17 @@
},
"labels": {
"advanced_filters": "",
"advanced_filters_show": "",
"advanced_filters_hide": "",
"advanced_filters_filters": "",
"advanced_filters_sorters": "",
"advanced_filters_filter_field": "",
"advanced_filters_sorter_field": "",
"advanced_filters_true": "",
"advanced_filters_false": "",
"advanced_filters_sorter_direction": "",
"advanced_filters_filter_field": "",
"advanced_filters_filter_operator": "",
"advanced_filters_filter_value": "",
"advanced_filters_filters": "",
"advanced_filters_hide": "",
"advanced_filters_show": "",
"advanced_filters_sorter_direction": "",
"advanced_filters_sorter_field": "",
"advanced_filters_sorters": "",
"advanced_filters_true": "",
"dates": "",
"employee": "",
"filterson": "",
@@ -2675,6 +2690,8 @@
"job_costing_ro_date_summary": "",
"job_costing_ro_estimator": "",
"job_costing_ro_ins_co": "",
"job_lifecycle_date_detail": "",
"job_lifecycle_date_summary": "",
"jobs_completed_not_invoiced": "",
"jobs_invoiced_not_exported": "",
"jobs_reconcile": "",

View File

@@ -514,6 +514,14 @@ export const TemplateList = (type, context) => {
group: "financial",
dms: true,
},
job_lifecycle_ro: {
title: i18n.t("printcenter.jobs.job_lifecycle_ro"),
description: "",
subject: i18n.t("printcenter.jobs.job_lifecycle_ro"),
key: "job_lifecycle_ro",
disabled: false,
group: "post",
},
}
: {}),
...(!type || type === "job_special"
@@ -2048,6 +2056,30 @@ export const TemplateList = (type, context) => {
datedisable: true,
group: "customers",
},
job_lifecycle_date_detail: {
title: i18n.t("reportcenter.templates.job_lifecycle_date_detail"),
subject: i18n.t("reportcenter.templates.job_lifecycle_date_detail"),
key: "job_lifecycle_date_detail",
//idtype: "vendor",
disabled: false,
rangeFilter: {
object: i18n.t("reportcenter.labels.objects.jobs"),
field: i18n.t("jobs.fields.date_invoiced"),
},
group: "jobs",
},
job_lifecycle_date_summary: {
title: i18n.t("reportcenter.templates.job_lifecycle_date_summary"),
subject: i18n.t("reportcenter.templates.job_lifecycle_date_summary"),
key: "job_lifecycle_date_summary",
//idtype: "vendor",
disabled: false,
rangeFilter: {
object: i18n.t("reportcenter.labels.objects.jobs"),
field: i18n.t("jobs.fields.date_invoiced"),
},
group: "jobs",
},
}
: {}),
...(!type || type === "courtesycarcontract"

View File

@@ -969,6 +969,7 @@
- md_rbac
- md_referral_sources
- md_responsibility_centers
- md_ro_guard
- md_ro_statuses
- md_tasks_presets
- md_to_emails
@@ -1068,6 +1069,7 @@
- md_rbac
- md_referral_sources
- md_responsibility_centers
- md_ro_guard
- md_ro_statuses
- md_tasks_presets
- md_to_emails
@@ -5761,6 +5763,7 @@
_eq: X-Hasura-User-Id
- active:
_eq: true
allow_aggregations: true
update_permissions:
- role: user
permission:

View File

@@ -0,0 +1,4 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- alter table "public"."ioevents" add column "useremail" text
-- not null;

View File

@@ -0,0 +1 @@
alter table "public"."ioevents" add column "useremail" text;

View File

@@ -0,0 +1,4 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- alter table "public"."ioevents" add column "bodyshopid" uuid
-- null;

View File

@@ -0,0 +1,2 @@
alter table "public"."ioevents" add column "bodyshopid" uuid
null;

View File

@@ -0,0 +1 @@
alter table "public"."ioevents" alter column "useremail" set not null;

View File

@@ -0,0 +1 @@
alter table "public"."ioevents" alter column "useremail" drop not null;

View File

@@ -0,0 +1,4 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- alter table "public"."ioevents" add column "env" text
-- null;

View File

@@ -0,0 +1,2 @@
alter table "public"."ioevents" add column "env" text
null;

View File

@@ -0,0 +1 @@
DROP INDEX IF EXISTS "public"."ioevents_useremail";

View File

@@ -0,0 +1,2 @@
CREATE INDEX "ioevents_useremail" on
"public"."ioevents" using btree ("useremail");

View File

@@ -0,0 +1 @@
alter table "public"."ioevents" drop constraint "ioevents_useremail_fkey";

View File

@@ -0,0 +1,5 @@
alter table "public"."ioevents"
add constraint "ioevents_useremail_fkey"
foreign key ("useremail")
references "public"."users"
("email") on update set null on delete set null;

View File

@@ -0,0 +1 @@
alter table "public"."ioevents" drop constraint "ioevents_bodyshopid_fkey";

View File

@@ -0,0 +1,5 @@
alter table "public"."ioevents"
add constraint "ioevents_bodyshopid_fkey"
foreign key ("bodyshopid")
references "public"."bodyshops"
("id") on update set null on delete set null;

View File

@@ -0,0 +1 @@
DROP INDEX IF EXISTS "public"."idx_audit_trail_type";

View File

@@ -0,0 +1,2 @@
CREATE INDEX "idx_audit_trail_type" on
"public"."audit_trail" using btree ("type");

View File

@@ -0,0 +1,4 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- alter table "public"."bodyshops" add column "md_ro_guard" jsonb
-- null default jsonb_build_object();

View File

@@ -0,0 +1,2 @@
alter table "public"."bodyshops" add column "md_ro_guard" jsonb
null default jsonb_build_object();

View File

@@ -201,19 +201,24 @@ exports.default = async (req, res) => {
} finally {
sftp.end();
}
sendServerEmail({
subject: `Kaizen Report ${moment().format("MM-DD-YY")}`,
text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))}
Uploaded: ${JSON.stringify(
allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count })),
null,
2
)}
`,
});
// sendServerEmail({
// subject: `Kaizen Report ${moment().format("MM-DD-YY")}`,
// text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))}
// Uploaded: ${JSON.stringify(
// allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count })),
// null,
// 2
// )}
// `,
// });
res.sendStatus(200);
} catch (error) {
res.status(200).json(error);
sendServerEmail({
subject: `Kaizen Report ${moment().format("MM-DD-YY @ HH:mm:ss")}`,
text: `Errors: JSON.stringify(error)}
All Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))}`,
});
}
};

View File

@@ -11,27 +11,40 @@ require("dotenv").config({
});
exports.default = async (req, res) => {
const { operationName, time, dbevent, user, imexshopid } = req.body;
const {
useremail,
bodyshopid,
operationName,
variables,
env,
time,
dbevent,
user,
} = req.body;
try {
// await client.request(queries.INSERT_IOEVENT, {
// event: {
// operationname: operationName,
// time,
// dbevent,
// },
// });
console.log("IOEVENT", operationName, time, dbevent, user, imexshopid);
logger.log("ioevent", "trace", user, null, {
imexshopid,
operationName,
time,
dbevent,
await client.request(queries.INSERT_IOEVENT, {
event: {
operationname: operationName,
time,
dbevent,
env,
variables,
bodyshopid,
useremail,
},
});
res.sendStatus(200);
} catch (error) {
console.log("error", error);
res.status(400).send(error);
logger.log("ioevent-error", "trace", user, null, {
operationname: operationName,
time,
dbevent,
env,
variables,
bodyshopid,
useremail,
});
res.sendStatus(200);
}
};

View File

@@ -3,6 +3,7 @@ const queries = require("../graphql-client/queries");
const moment = require("moment");
const durationToHumanReadable = require("../utils/durationToHumanReadable");
const calculateStatusDuration = require("../utils/calculateStatusDuration");
const getLifecycleStatusColor = require("../utils/getLifecycleStatusColor");
const jobLifecycle = async (req, res) => {
// Grab the jobids and statuses from the request body
@@ -28,12 +29,12 @@ const jobLifecycle = async (req, res) => {
jobIDs,
transitions: []
});
}
const transitionsByJobId = _.groupBy(resp.transitions, 'jobid');
const groupedTransitions = {};
const allDurations = [];
for (let jobId in transitionsByJobId) {
let lifecycle = transitionsByJobId[jobId].map(transition => {
@@ -53,15 +54,57 @@ const jobLifecycle = async (req, res) => {
return transition;
});
const durations = calculateStatusDuration(lifecycle, statuses);
groupedTransitions[jobId] = {
lifecycle: lifecycle,
durations: calculateStatusDuration(lifecycle, statuses),
lifecycle,
durations
};
if (durations?.summations) {
allDurations.push(durations.summations);
}
}
const finalSummations = [];
const flatGroupedAllDurations = _.groupBy(allDurations.flat(),'status');
const finalStatusCounts = Object.keys(flatGroupedAllDurations).reduce((acc, status) => {
acc[status] = flatGroupedAllDurations[status].length;
return acc;
}, {});
// Calculate total value of all statuses
const finalTotal = Object.values(flatGroupedAllDurations).reduce((total, statusArr) => {
return total + statusArr.reduce((acc, curr) => acc + curr.value, 0);
}, 0);
Object.keys(flatGroupedAllDurations).forEach(status => {
const value = flatGroupedAllDurations[status].reduce((acc, curr) => acc + curr.value, 0);
const humanReadable = durationToHumanReadable(moment.duration(value));
const percentage = (value / finalTotal) * 100;
const color = getLifecycleStatusColor(status);
const roundedPercentage = `${Math.round(percentage)}%`;
finalSummations.push({
status,
value,
humanReadable,
percentage,
color,
roundedPercentage
});
});
return res.status(200).json({
jobIDs,
transition: groupedTransitions,
durations: {
jobs: jobIDs.length,
summations: finalSummations,
totalStatuses: finalSummations.length,
total: finalTotal,
statusCounts: finalStatusCounts,
humanReadable: durationToHumanReadable(moment.duration(finalTotal))
}
});
}

View File

@@ -1,15 +1,7 @@
const durationToHumanReadable = require("./durationToHumanReadable");
const moment = require("moment");
const getLifecycleStatusColor = require("./getLifecycleStatusColor");
const _ = require("lodash");
const crypto = require('crypto');
const getColor = (key) => {
const hash = crypto.createHash('sha256');
hash.update(key);
const hashedKey = hash.digest('hex');
const num = parseInt(hashedKey, 16);
return '#' + (num % 16777215).toString(16).padStart(6, '0');
};
const calculateStatusDuration = (transitions, statuses) => {
let statusDuration = {};
@@ -33,26 +25,16 @@ const calculateStatusDuration = (transitions, statuses) => {
if (!transition.prev_value) {
statusDuration[transition.value] = {
value: duration,
humanReadable: transition.duration_readable
humanReadable: durationToHumanReadable(moment.duration(duration))
};
} else if (!transition.next_value) {
} else {
if (statusDuration[transition.value]) {
statusDuration[transition.value].value += duration;
statusDuration[transition.value].humanReadable = transition.duration_readable;
statusDuration[transition.value].humanReadable = durationToHumanReadable(moment.duration(statusDuration[transition.value].value));
} else {
statusDuration[transition.value] = {
value: duration,
humanReadable: transition.duration_readable
};
}
} else {
if (statusDuration[transition.value]) {
statusDuration[transition.value].value += duration;
statusDuration[transition.value].humanReadable = transition.duration_readable;
} else {
statusDuration[transition.value] = {
value: duration,
humanReadable: transition.duration_readable
humanReadable: durationToHumanReadable(moment.duration(duration))
};
}
}
@@ -79,7 +61,7 @@ const calculateStatusDuration = (transitions, statuses) => {
value,
humanReadable,
percentage: statusDuration[status].percentage,
color: getColor(status),
color: getLifecycleStatusColor(status),
roundedPercentage: `${Math.round(statusDuration[status].percentage)}%`
});
}

View File

@@ -0,0 +1,11 @@
const crypto = require('crypto');
const getLifecycleStatusColor = (key) => {
const hash = crypto.createHash('sha256');
hash.update(key);
const hashedKey = hash.digest('hex');
const num = parseInt(hashedKey, 16);
return '#' + (num % 16777215).toString(16).padStart(6, '0');
};
module.exports = getLifecycleStatusColor;