Compare commits

...

80 Commits

Author SHA1 Message Date
Patrick Fic
c1881671a8 Remove logging statement. 2022-07-22 10:49:45 -07:00
Patrick Fic
e0e88acb9e IO-1979 Clear vacation form on enter. 2022-07-21 15:05:44 -07:00
Patrick Fic
14c40958b4 IO-1957 Adjust scoreboard effiiency for edge case. 2022-07-21 14:53:06 -07:00
Patrick Fic
2438857cb7 IO-1294 Refetch data after admin update. 2022-07-21 14:20:40 -07:00
Patrick Fic
9362372512 IO-1985 Add filtering for est ct on ready and active jobs screens. 2022-07-21 14:05:57 -07:00
Patrick Fic
b30192c6d5 IO-1978 Updated parts return logic and removed extra button. 2022-07-20 15:18:13 -07:00
Patrick Fic
9ddcb1f27d IO-1985 add Est to actie and ready jobs screens. 2022-07-20 15:07:37 -07:00
Patrick Fic
cd29a0f613 IO-1983 Add filter to job notes table for audit. 2022-07-20 14:39:06 -07:00
Patrick Fic
b189b045f8 IO-1988 Download LMS as zip. 2022-07-20 14:17:30 -07:00
Patrick Fic
17baca40d9 IO-1982 add alt_partno to jobs list and order button. 2022-07-19 15:57:32 -07:00
Patrick Fic
956134c360 IO-1957 Add efficiencies to timetickets scoreboard 2022-07-19 14:40:53 -07:00
Patrick Fic
d5f55c5873 IO-1979 Resolve various employee vacation bugs. 2022-07-19 13:41:14 -07:00
Patrick Fic
179147dc4e IO-1755 Add tax number to owner. 2022-07-19 13:14:22 -07:00
Patrick Fic
385de3eaaa IO-1414 Job costing GP and decimals fix. 2022-07-19 12:35:06 -07:00
Patrick Fic
22368069a4 IO-1294 Add fields changed alert to admin dates. 2022-07-19 11:21:50 -07:00
Patrick Fic
5eb8c2c03a Code cleanup for CI. 2022-07-18 17:01:52 -07:00
Patrick Fic
a23b3bf02d IO-1978 Selection of lines for parts return 2022-07-18 16:25:33 -07:00
Patrick Fic
4976e6be95 IO-1961 Updated table hover contrast. 2022-07-18 12:17:11 -07:00
Patrick Fic
1b58a29112 IO-1974 Add lease returned CC status. 2022-07-18 11:59:57 -07:00
Patrick Fic
05e236cd9c IO-1018 Warnings for courtesy car fields. 2022-07-18 11:56:05 -07:00
Patrick Fic
90bd70070d IO-1972 Add tracking for conversions to labor. 2022-07-18 11:45:02 -07:00
Patrick Fic
fb6c667a7f Merge remote-tracking branch 'origin/master' into release/2022-07-22 2022-07-18 11:39:32 -07:00
Patrick Fic
c048f21674 IO-1972 Converted to labor display. 2022-07-18 11:39:23 -07:00
Patrick Fic
05de47c833 Update parts filtering on job lines page. 2022-07-18 11:08:11 -07:00
Patrick Fic
c163e2a274 Merged in release/2022-07-15 (pull request #540)
Release/2022 07 15
2022-07-15 22:40:46 +00:00
Patrick Fic
7cf32775eb Revert changes for IO-1972. 2022-07-15 15:31:16 -07:00
Patrick Fic
27ce30527e IO-1536 Add VIN Highlighting. 2022-07-14 15:18:01 -07:00
Patrick Fic
b346c28fe0 IO-1954 schedule display improvements. 2022-07-13 16:17:21 -07:00
Patrick Fic
05a0ee30f4 IO-1972 add table for converted parts to labor lines. 2022-07-13 13:50:13 -07:00
Patrick Fic
7a0d5d712a IO-1906 Remove bin from bill edit. 2022-07-13 11:35:21 -07:00
Patrick Fic
4802c1abe8 IO-1937 Update email sizing error message. 2022-07-12 15:42:00 -07:00
Patrick Fic
7022609e22 Parts Quote create order first. 2022-07-11 15:01:57 -07:00
Patrick Fic
d8447b1197 Resolve phone number parsing issue on messaging. 2022-07-11 14:23:27 -07:00
Patrick Fic
43b140aed4 Merged in release/2022-07-08 (pull request #536)
Autohouse error fix.

Approved-by: Patrick Fic
2022-07-07 16:30:46 +00:00
Patrick Fic
d4ee6ca8ba Autohouse error fix. 2022-07-07 09:29:37 -07:00
Patrick Fic
d6673ed278 Merged in release/2022-06-30 (pull request #534)
release/2022-06-30

Approved-by: Patrick Fic
2022-06-30 21:17:23 +00:00
Patrick Fic
e0804099ee IO-1967 Adjust display of convert to labor button. 2022-06-30 14:16:10 -07:00
Patrick Fic
ece0946738 Remove updating of message count on outbound message. 2022-06-30 11:38:49 -07:00
Patrick Fic
9bcc44c0cc Updated labor adjustment audit trail. 2022-06-30 09:58:55 -07:00
Patrick Fic
b9b6759c54 IO-1967 Update audit trail for labor adjustments. 2022-06-30 09:04:17 -07:00
Patrick Fic
1f1274a54a IO0-1951 Move checkbox location for parts order quote. 2022-06-30 08:39:33 -07:00
Patrick Fic
35d4188469 IO-1947 Resolve unknown filter status for qfp. 2022-06-30 08:34:58 -07:00
Patrick Fic
a8f89c81fc IO-1951 Resolve parts order issue. 2022-06-29 16:19:16 -07:00
Patrick Fic
c382b3f2e0 Fix CI errors. 2022-06-29 15:19:47 -07:00
Patrick Fic
39dbf40a49 IO-1967 Convert dollar amount to labor. 2022-06-29 15:05:03 -07:00
Patrick Fic
037ff4c2a1 Resolve email rendering issue with JSR Update. 2022-06-29 10:54:19 -07:00
Patrick Fic
40037216aa Merged in hotfix/2022-06-28 (pull request #527)
Update autohouse error logging.

Approved-by: Patrick Fic
2022-06-29 17:34:54 +00:00
Patrick Fic
5a6a92c260 Merged in hotfix/2022-06-28 (pull request #526)
Update autohouse error logging.

Approved-by: Patrick Fic
2022-06-29 17:05:14 +00:00
Patrick Fic
fb4b12233a Update autohouse error logging. 2022-06-29 09:48:26 -07:00
Patrick Fic
d45f84afbd Change firebase handler rejection logging. 2022-06-28 16:24:49 -07:00
Patrick Fic
1d80153da1 Added placeholder for paint codes on prod board. 2022-06-28 16:14:26 -07:00
Patrick Fic
f704fd5f56 Added mix data logging. 2022-06-28 16:14:13 -07:00
Patrick Fic
82c4320f0c Add paint mix data logging. 2022-06-28 16:04:34 -07:00
Patrick Fic
bebe99f4e6 Merge branch 'hotfix/2022-06-28' into release/2022-06-30
* hotfix/2022-06-28:
  Update error handling for autohouse.
  Autohouse replace fix.
2022-06-28 13:31:59 -07:00
Patrick Fic
1e24d5d57f Merged in hotfix/2022-06-28 (pull request #524)
hotfix/2022-06-28

Approved-by: Patrick Fic
2022-06-28 15:35:03 +00:00
Patrick Fic
fbcf2b559e Update error handling for autohouse. 2022-06-28 08:34:34 -07:00
Patrick Fic
4582c493ee Autohouse replace fix. 2022-06-28 08:31:24 -07:00
Patrick Fic
ecfd284539 Upgrade to latest JSR package. 2022-06-27 22:03:10 -07:00
Patrick Fic
2a1c046dd6 IO-1943 Enter again bill functionality bugfix. 2022-06-27 16:17:45 -07:00
Patrick Fic
54ebc2e25b IO-1955 include names for incoming/outgoing jobs on schedule. 2022-06-27 14:12:06 -07:00
Patrick Fic
974a0ec1f1 IO-1951 Added quote for OEC orders. 2022-06-27 13:42:48 -07:00
Patrick Fic
acf99584ea IO-1947 Remember parts queue filter status. 2022-06-27 13:06:14 -07:00
Patrick Fic
d0d4ceb270 IO-1958 IO-1884 Vehicle card updates. 2022-06-27 12:46:29 -07:00
Patrick Fic
f2e7808fa0 Merged in release/2022-06-24 (pull request #523)
release/2022-06-24

Approved-by: Patrick Fic
2022-06-24 21:04:03 +00:00
Patrick Fic
c07458babf Add job reconciliation & autohouse filtering. 2022-06-24 09:45:11 -07:00
Patrick Fic
0892461631 Merged in release/2022-06-24 (pull request #521)
release/2022-06-24

Approved-by: Patrick Fic
2022-06-21 00:53:28 +00:00
Patrick Fic
623d407a6c IO-1942 Resolve marking credit memoes received error. 2022-06-20 15:18:54 -07:00
Patrick Fic
5e3218a145 Merged in release/2022-06-17 (pull request #519)
release/2022-06-17

Approved-by: Patrick Fic
2022-06-20 17:48:49 +00:00
Patrick Fic
706f300750 IO-1941 change owner search 2022-06-20 10:27:22 -07:00
Patrick Fic
4fad4e41c2 IO-1938 Updated sorting on time tickets. 2022-06-20 08:53:15 -07:00
Patrick Fic
1e88d5ae1b IO-1937 Add 10mb limit for emails. 2022-06-17 15:22:29 -07:00
Patrick Fic
7ba3cc5ffa Added basic creation of shops. 2022-06-15 19:03:31 -07:00
Patrick Fic
4fdd48c279 Update developement db addresses. 2022-06-14 11:32:29 -07:00
Patrick Fic
f5834ae6bc Updated dev end points. 2022-06-14 09:55:46 -07:00
Patrick Fic
db36b27819 Merged in release/2022-06-17 (pull request #517)
IO-1917 Autohouse handling for DMS.

Approved-by: Patrick Fic
2022-06-13 18:49:23 +00:00
Patrick Fic
43fbf32e99 IO-1917 Autohouse handling for DMS. 2022-06-13 11:44:35 -07:00
Patrick Fic
1b2afb9e93 Merged in release/2022-06-10 (pull request #516)
IO-1911 Round TTSB to 1 decimal.

Approved-by: Patrick Fic
2022-06-13 15:59:24 +00:00
Patrick Fic
6a109d63ce Merged in release/2022-06-10 (pull request #515)
Remove package.lock.

Approved-by: Patrick Fic
2022-06-10 23:38:47 +00:00
Patrick Fic
a6610309e9 Merged in release/2022-06-10 (pull request #514)
Updated package.lock.

Approved-by: Patrick Fic
2022-06-10 23:04:58 +00:00
Patrick Fic
db02b9c1c2 Merged in release/2022-06-10 (pull request #513)
release/2022-06-10

Approved-by: Patrick Fic
2022-06-10 22:38:15 +00:00
95 changed files with 15281 additions and 1364 deletions

View File

@@ -2063,6 +2063,32 @@
</concept_node>
</children>
</folder_node>
<folder_node>
<name>validation</name>
<children>
<concept_node>
<name>atleastone</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
</children>
</folder_node>
<folder_node>
@@ -8138,6 +8164,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>default_quote</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>default_received</name>
<definition_loaded>false</definition_loaded>
@@ -11102,6 +11149,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>dateinpast</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>dlexpirebeforereturn</name>
<definition_loaded>false</definition_loaded>
@@ -12323,6 +12391,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>leasereturn</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>out</name>
<definition_loaded>false</definition_loaded>
@@ -14688,6 +14777,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>vacationadded</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
<folder_node>
@@ -15374,6 +15484,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>sizelimit</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
<folder_node>
@@ -17758,6 +17889,27 @@
<folder_node>
<name>actions</name>
<children>
<concept_node>
<name>converttolabor</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>new</name>
<definition_loaded>false</definition_loaded>
@@ -18938,6 +19090,27 @@
<folder_node>
<name>labels</name>
<children>
<concept_node>
<name>adjustmenttobeadded</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>billref</name>
<definition_loaded>false</definition_loaded>
@@ -18959,6 +19132,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>convertedtolabor</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>edit</name>
<definition_loaded>false</definition_loaded>
@@ -26420,6 +26614,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>convertedtolabor</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>cost</name>
<definition_loaded>false</definition_loaded>
@@ -27191,6 +27406,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>estimator</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>existing_jobs</name>
<definition_loaded>false</definition_loaded>
@@ -32391,6 +32627,48 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>systemnotes</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>usernotes</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
<folder_node>
@@ -32547,6 +32825,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>saving</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>selectexistingornew</name>
<definition_loaded>false</definition_loaded>
@@ -32951,6 +33250,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>tax_number</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
<folder_node>
@@ -33946,6 +34266,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>is_quote</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>mark_as_received</name>
<definition_loaded>false</definition_loaded>
@@ -40025,6 +40366,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>jobs_reconcile</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>lag_time</name>
<definition_loaded>false</definition_loaded>
@@ -41152,6 +41514,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>efficiencyoverperiod</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>jobs</name>
<definition_loaded>false</definition_loaded>
@@ -44929,6 +45312,27 @@
<folder_node>
<name>signinerror</name>
<children>
<concept_node>
<name>auth/user-disabled</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>auth/user-not-found</name>
<definition_loaded>false</definition_loaded>

View File

@@ -8,6 +8,7 @@
"@asseinfo/react-kanban": "^2.2.0",
"@craco/craco": "^6.4.3",
"@fingerprintjs/fingerprintjs": "^3.3.3",
"@jsreport/browser-client": "^3.1.0",
"@sentry/react": "^7.1.1",
"@sentry/tracing": "^7.1.1",
"@splitsoftware/splitio-react": "^1.4.1",

View File

@@ -142,3 +142,9 @@
}
}
}
//Update row highlighting on production board.
.ant-table-tbody > tr.ant-table-row:hover > td {
background: #eaeaea !important;
}

View File

@@ -5,7 +5,6 @@ import ReadOnlyFormItemComponent from "../form-items-formatted/read-only-form-it
import "./bill-cm-returns-table.styles.scss";
export default function BillCmdReturnsTableComponent({
form,
loadOutstandingReturns,
returnLoading,
returnData,
}) {

View File

@@ -0,0 +1,251 @@
import { useMutation, useQuery } from "@apollo/client";
import { Button, Form, PageHeader, Popconfirm, Space } from "antd";
import moment from "moment";
import queryString from "query-string";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import {
DELETE_BILL_LINE,
INSERT_NEW_BILL_LINES,
UPDATE_BILL_LINE
} from "../../graphql/bill-lines.queries";
import { QUERY_BILL_BY_PK, UPDATE_BILL } from "../../graphql/bills.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import AlertComponent from "../alert/alert.component";
import BillFormContainer from "../bill-form/bill-form.container";
import BillMarkExportedButton from "../bill-mark-exported-button/bill-mark-exported-button.component";
import BillReeportButtonComponent from "../bill-reexport-button/bill-reexport-button.component";
import JobDocumentsGallery from "../jobs-documents-gallery/jobs-documents-gallery.container";
import JobsDocumentsLocalGallery from "../jobs-documents-local-gallery/jobs-documents-local-gallery.container";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
import BillDetailEditReturn from "./bill-detail-edit-return.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
setPartsOrderContext: (context) =>
dispatch(setModalContext({ context: context, modal: "partsOrder" })),
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(BillDetailEditcontainer);
export function BillDetailEditcontainer({
setPartsOrderContext,
insertAuditTrail,
bodyshop,
}) {
const search = queryString.parse(useLocation().search);
const { t } = useTranslation();
const [form] = Form.useForm();
const [visible, setVisible] = useState(false);
const [updateLoading, setUpdateLoading] = useState(false);
const [update_bill] = useMutation(UPDATE_BILL);
const [insertBillLine] = useMutation(INSERT_NEW_BILL_LINES);
const [updateBillLine] = useMutation(UPDATE_BILL_LINE);
const [deleteBillLine] = useMutation(DELETE_BILL_LINE);
const { loading, error, data, refetch } = useQuery(QUERY_BILL_BY_PK, {
variables: { billid: search.billid },
skip: !!!search.billid,
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
const handleSave = () => {
//It's got a previously deducted bill line!
if (
data.bills_by_pk.billlines.filter((b) => b.deductedfromlbr).length > 0 ||
form.getFieldValue("billlines").filter((b) => b.deductedfromlbr).length >
0
)
setVisible(true);
else {
form.submit();
}
};
const handleFinish = async (values) => {
setUpdateLoading(true);
//let adjustmentsToInsert = {};
const { billlines, upload, ...bill } = values;
const updates = [];
updates.push(
update_bill({
variables: { billId: search.billid, bill: bill },
})
);
billlines.forEach((l) => {
delete l.selected;
});
//Find bill lines that were deleted.
const deletedJobLines = [];
data.bills_by_pk.billlines.forEach((a) => {
const matchingRecord = billlines.find((b) => b.id === a.id);
if (!matchingRecord) {
deletedJobLines.push(a);
}
});
deletedJobLines.forEach((d) => {
updates.push(deleteBillLine({ variables: { id: d.id } }));
});
billlines.forEach((billline) => {
const { deductedfromlbr, inventories, jobline, ...il } = billline;
delete il.__typename;
if (il.id) {
updates.push(
updateBillLine({
variables: {
billLineId: il.id,
billLine: {
...il,
deductedfromlbr: deductedfromlbr,
joblineid: il.joblineid === "noline" ? null : il.joblineid,
},
},
})
);
} else {
//It's a new line, have to insert it.
updates.push(
insertBillLine({
variables: {
billLines: [
{
...il,
deductedfromlbr: deductedfromlbr,
billid: search.billid,
joblineid: il.joblineid === "noline" ? null : il.joblineid,
},
],
},
})
);
}
});
await Promise.all(updates);
insertAuditTrail({
jobid: bill.jobid,
billid: search.billid,
operation: AuditTrailMapping.billupdated(bill.invoice_number),
});
await refetch();
form.setFieldsValue(transformData(data));
form.resetFields();
setVisible(false);
setUpdateLoading(false);
};
if (error) return <AlertComponent message={error.message} type="error" />;
if (!search.billid) return <></>; //<div>{t("bills.labels.noneselected")}</div>;
const exported = data && data.bills_by_pk && data.bills_by_pk.exported;
return (
<>
{loading && <LoadingSkeleton />}
{data && (
<>
<PageHeader
title={
data &&
`${data.bills_by_pk.invoice_number} - ${data.bills_by_pk.vendor.name}`
}
extra={
<Space>
<BillDetailEditReturn data={data} />
<Popconfirm
visible={visible}
onConfirm={() => form.submit()}
onCancel={() => setVisible(false)}
okButtonProps={{ loading: updateLoading }}
title={t("bills.labels.editadjwarning")}
>
<Button
htmlType="submit"
disabled={exported}
onClick={handleSave}
loading={updateLoading}
type="primary"
>
{t("general.actions.save")}
</Button>
</Popconfirm>
<BillReeportButtonComponent bill={data && data.bills_by_pk} />
<BillMarkExportedButton bill={data && data.bills_by_pk} />
</Space>
}
/>
<Form
form={form}
onFinish={handleFinish}
initialValues={transformData(data)}
layout="vertical"
>
<BillFormContainer form={form} billEdit disabled={exported} />
{bodyshop.uselocalmediaserver ? (
<JobsDocumentsLocalGallery
job={{ id: data ? data.bills_by_pk.jobid : null }}
invoice_number={data ? data.bills_by_pk.invoice_number : null}
vendorid={data ? data.bills_by_pk.vendorid : null}
/>
) : (
<JobDocumentsGallery
jobId={data ? data.bills_by_pk.jobid : null}
billId={search.billid}
documentsList={data ? data.bills_by_pk.documents : []}
billsCallback={refetch}
/>
)}
</Form>
</>
)}
</>
);
}
const transformData = (data) => {
return data
? {
...data.bills_by_pk,
billlines: data.bills_by_pk.billlines.map((i) => {
return {
...i,
joblineid: !!i.joblineid ? i.joblineid : "noline",
applicable_taxes: {
federal:
(i.applicable_taxes && i.applicable_taxes.federal) || false,
state: (i.applicable_taxes && i.applicable_taxes.state) || false,
local: (i.applicable_taxes && i.applicable_taxes.local) || false,
},
};
}),
date: data.bills_by_pk ? moment(data.bills_by_pk.date) : null,
}
: {};
};

View File

@@ -0,0 +1,185 @@
import { Button, Checkbox, Form, Modal } from "antd";
import queryString from "query-string";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useHistory, useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import ReadOnlyFormItemComponent from "../form-items-formatted/read-only-form-item.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
setPartsOrderContext: (context) =>
dispatch(setModalContext({ context: context, modal: "partsOrder" })),
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(BillDetailEditReturn);
export function BillDetailEditReturn({
setPartsOrderContext,
insertAuditTrail,
bodyshop,
data,
disabled,
}) {
const search = queryString.parse(useLocation().search);
const history = useHistory();
const { t } = useTranslation();
const [form] = Form.useForm();
const [visible, setVisible] = useState(false);
const handleFinish = ({ billlines }) => {
const selectedLines = billlines.filter((l) => l.selected).map((l) => l.id);
setPartsOrderContext({
actions: {},
context: {
jobId: data.bills_by_pk.jobid,
vendorId: data.bills_by_pk.vendorid,
returnFromBill: data.bills_by_pk.id,
invoiceNumber: data.bills_by_pk.invoice_number,
linesToOrder: data.bills_by_pk.billlines
.filter((l) => selectedLines.includes(l.id))
.map((i) => {
return {
line_desc: i.line_desc,
// db_price: i.actual_price,
act_price: i.actual_price,
cost: i.actual_cost,
quantity: i.quantity,
joblineid: i.joblineid,
oem_partno: i.jobline && i.jobline.oem_partno,
part_type: i.jobline && i.jobline.part_type,
};
}),
isReturn: true,
},
});
delete search.billid;
history.push({ search: queryString.stringify(search) });
setVisible(false);
};
useEffect(() => {
if (visible === false) form.resetFields();
}, [visible, form]);
return (
<>
<Modal
visible={visible}
onCancel={() => setVisible(false)}
destroyOnClose
title={t("bills.actions.return")}
onOk={() => form.submit()}
>
<Form
initialValues={data && data.bills_by_pk}
onFinish={handleFinish}
form={form}
>
<Form.List name={["billlines"]}>
{(fields, { add, remove, move }) => {
return (
<table style={{ tableLayout: "auto", width: "100%" }}>
<thead>
<tr>
<td>
<Checkbox
onChange={(e) => {
form.setFieldsValue({
billlines: form
.getFieldsValue()
.billlines.map((b) => ({
...b,
selected: e.target.checked,
})),
});
}}
/>
</td>
<td>{t("billlines.fields.line_desc")}</td>
<td>{t("billlines.fields.quantity")}</td>
<td>{t("billlines.fields.actual_price")}</td>
<td>{t("billlines.fields.actual_cost")}</td>
</tr>
</thead>
<tbody>
{fields.map((field, index) => (
<tr key={field.key}>
<td>
<Form.Item
// label={t("joblines.fields.selected")}
key={`${index}selected`}
name={[field.name, "selected"]}
valuePropName="checked"
>
<Checkbox />
</Form.Item>
</td>
<td>
<Form.Item
// label={t("joblines.fields.line_desc")}
key={`${index}line_desc`}
name={[field.name, "line_desc"]}
>
<ReadOnlyFormItemComponent />
</Form.Item>
</td>
<td>
<Form.Item
// label={t("joblines.fields.quantity")}
key={`${index}quantity`}
name={[field.name, "quantity"]}
>
<ReadOnlyFormItemComponent />
</Form.Item>
</td>
<td>
<Form.Item
// label={t("joblines.fields.actual_price")}
key={`${index}actual_price`}
name={[field.name, "actual_price"]}
>
<ReadOnlyFormItemComponent type="currency" />
</Form.Item>
</td>
<td>
<Form.Item
// label={t("joblines.fields.actual_cost")}
key={`${index}actual_cost`}
name={[field.name, "actual_cost"]}
>
<ReadOnlyFormItemComponent type="currency" />
</Form.Item>
</td>
</tr>
))}
</tbody>
</table>
);
}}
</Form.List>
</Form>
</Modal>
<Button
disabled={data.bills_by_pk.is_credit_memo || disabled}
onClick={() => {
setVisible(true);
}}
>
{t("bills.actions.return")}
</Button>
</>
);
}

View File

@@ -1,68 +1,12 @@
import { useMutation, useQuery } from "@apollo/client";
import {
Button,
Drawer,
Form,
Grid,
PageHeader,
Popconfirm,
Space,
} from "antd";
import moment from "moment";
import { Drawer, Grid } from "antd";
import queryString from "query-string";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import React from "react";
import { useHistory, useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import {
DELETE_BILL_LINE,
INSERT_NEW_BILL_LINES,
UPDATE_BILL_LINE,
} from "../../graphql/bill-lines.queries";
import { QUERY_BILL_BY_PK, UPDATE_BILL } from "../../graphql/bills.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import AlertComponent from "../alert/alert.component";
import BillFormContainer from "../bill-form/bill-form.container";
import BillMarkExportedButton from "../bill-mark-exported-button/bill-mark-exported-button.component";
import BillReeportButtonComponent from "../bill-reexport-button/bill-reexport-button.component";
import JobDocumentsGallery from "../jobs-documents-gallery/jobs-documents-gallery.container";
import JobsDocumentsLocalGallery from "../jobs-documents-local-gallery/jobs-documents-local-gallery.container";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
import BillDetailEditComponent from "./bill-detail-edit-component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
setPartsOrderContext: (context) =>
dispatch(setModalContext({ context: context, modal: "partsOrder" })),
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(BillDetailEditcontainer);
export function BillDetailEditcontainer({
setPartsOrderContext,
insertAuditTrail,
bodyshop,
}) {
export default function BillDetailEditcontainer() {
const search = queryString.parse(useLocation().search);
const history = useHistory();
const { t } = useTranslation();
const [form] = Form.useForm();
const [visible, setVisible] = useState(false);
const [updateLoading, setUpdateLoading] = useState(false);
const [update_bill] = useMutation(UPDATE_BILL);
const [insertBillLine] = useMutation(INSERT_NEW_BILL_LINES);
const [updateBillLine] = useMutation(UPDATE_BILL_LINE);
const [deleteBillLine] = useMutation(DELETE_BILL_LINE);
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
.filter((screen) => !!screen[1])
@@ -80,114 +24,6 @@ export function BillDetailEditcontainer({
? bpoints[selectedBreakpoint[0]]
: "100%";
const { loading, error, data, refetch } = useQuery(QUERY_BILL_BY_PK, {
variables: { billid: search.billid },
skip: !!!search.billid,
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
const handleSave = () => {
//It's got a previously deducted bill line!
if (
data.bills_by_pk.billlines.filter((b) => b.deductedfromlbr).length > 0 ||
form.getFieldValue("billlines").filter((b) => b.deductedfromlbr).length >
0
)
setVisible(true);
else {
form.submit();
}
};
const handleFinish = async (values) => {
setUpdateLoading(true);
//let adjustmentsToInsert = {};
const { billlines, upload, ...bill } = values;
const updates = [];
updates.push(
update_bill({
variables: { billId: search.billid, bill: bill },
})
);
//Find bill lines that were deleted.
const deletedJobLines = [];
data.bills_by_pk.billlines.forEach((a) => {
const matchingRecord = billlines.find((b) => b.id === a.id);
if (!matchingRecord) {
deletedJobLines.push(a);
}
});
deletedJobLines.forEach((d) => {
updates.push(deleteBillLine({ variables: { id: d.id } }));
});
billlines.forEach((billline) => {
const { deductedfromlbr, inventories, jobline, ...il } = billline;
delete il.__typename;
if (il.id) {
updates.push(
updateBillLine({
variables: {
billLineId: il.id,
billLine: {
...il,
deductedfromlbr: deductedfromlbr,
joblineid: il.joblineid === "noline" ? null : il.joblineid,
},
},
})
);
} else {
//It's a new line, have to insert it.
updates.push(
insertBillLine({
variables: {
billLines: [
{
...il,
deductedfromlbr: deductedfromlbr,
billid: search.billid,
joblineid: il.joblineid === "noline" ? null : il.joblineid,
},
],
},
})
);
}
});
await Promise.all(updates);
insertAuditTrail({
jobid: bill.jobid,
billid: search.billid,
operation: AuditTrailMapping.billupdated(bill.invoice_number),
});
await refetch();
form.setFieldsValue(transformData(data));
form.resetFields();
setVisible(false);
setUpdateLoading(false);
};
useEffect(() => {
if (search.billid && data) {
form.resetFields();
}
}, [form, search.billid, data]);
if (error) return <AlertComponent message={error.message} type="error" />;
if (!search.billid) return <></>; //<div>{t("bills.labels.noneselected")}</div>;
const exported = data && data.bills_by_pk && data.bills_by_pk.exported;
return (
<Drawer
width={drawerPercentage}
@@ -195,119 +31,10 @@ export function BillDetailEditcontainer({
delete search.billid;
history.push({ search: queryString.stringify(search) });
}}
destroyOnClose
visible={search.billid}
>
{loading && <LoadingSkeleton />}
{!loading && (
<>
<PageHeader
title={
data &&
`${data.bills_by_pk.invoice_number} - ${data.bills_by_pk.vendor.name}`
}
extra={
<Space>
<Button
disabled={data.bills_by_pk.is_credit_memo}
onClick={() => {
delete search.billid;
history.push({ search: queryString.stringify(search) });
setPartsOrderContext({
actions: {},
context: {
jobId: data.bills_by_pk.jobid,
vendorId: data.bills_by_pk.vendorid,
returnFromBill: data.bills_by_pk.id,
invoiceNumber: data.bills_by_pk.invoice_number,
linesToOrder: data.bills_by_pk.billlines.map((i) => {
return {
line_desc: i.line_desc,
// db_price: i.actual_price,
act_price: i.actual_price,
cost: i.actual_cost,
quantity: i.quantity,
joblineid: i.joblineid,
oem_partno: i.jobline && i.jobline.oem_partno,
part_type: i.jobline && i.jobline.part_type,
};
}),
isReturn: true,
},
});
}}
>
{t("bills.actions.return")}
</Button>
<Popconfirm
visible={visible}
onConfirm={() => form.submit()}
onCancel={() => setVisible(false)}
okButtonProps={{ loading: updateLoading }}
title={t("bills.labels.editadjwarning")}
>
<Button
htmlType="submit"
disabled={exported}
onClick={handleSave}
loading={updateLoading}
type="primary"
>
{t("general.actions.save")}
</Button>
</Popconfirm>
<BillReeportButtonComponent bill={data && data.bills_by_pk} />
<BillMarkExportedButton bill={data && data.bills_by_pk} />
</Space>
}
/>
<Form
form={form}
onFinish={handleFinish}
initialValues={transformData(data)}
layout="vertical"
>
<BillFormContainer form={form} billEdit disabled={exported} />
{bodyshop.uselocalmediaserver ? (
<JobsDocumentsLocalGallery
job={{ id: data ? data.bills_by_pk.jobid : null }}
invoice_number={data ? data.bills_by_pk.invoice_number : null}
vendorid={data ? data.bills_by_pk.vendorid : null}
/>
) : (
<JobDocumentsGallery
jobId={data ? data.bills_by_pk.jobid : null}
billId={search.billid}
documentsList={data ? data.bills_by_pk.documents : []}
billsCallback={refetch}
/>
)}
</Form>
</>
)}
<BillDetailEditComponent />
</Drawer>
);
}
const transformData = (data) => {
return data
? {
...data.bills_by_pk,
billlines: data.bills_by_pk.billlines.map((i) => {
return {
...i,
joblineid: !!i.joblineid ? i.joblineid : "noline",
applicable_taxes: {
federal:
(i.applicable_taxes && i.applicable_taxes.federal) || false,
state: (i.applicable_taxes && i.applicable_taxes.state) || false,
local: (i.applicable_taxes && i.applicable_taxes.local) || false,
},
};
}),
date: data.bills_by_pk ? moment(data.bills_by_pk.date) : null,
}
: {};
};

View File

@@ -144,6 +144,14 @@ function BillEnterModalContainer({
adjKeys.forEach((key) => {
newAdjustments[key] =
(newAdjustments[key] || 0) + adjustmentsToInsert[key];
insertAuditTrail({
jobid: values.jobid,
operation: AuditTrailMapping.jobmodifylbradj({
mod_lbr_ty: key,
hours: adjustmentsToInsert[key].toFixed(1),
}),
});
});
const jobUpdate = client.mutate({
@@ -161,10 +169,6 @@ function BillEnterModalContainer({
});
return;
}
insertAuditTrail({
jobid: values.jobid,
operation: AuditTrailMapping.jobmodifylbradj(),
});
}
const markPolReceived =

View File

@@ -58,6 +58,7 @@ export function BillFormComponent({
{},
bodyshop.imexshopid
);
const handleVendorSelect = (props, opt) => {
setDiscount(opt.discount);
@@ -140,13 +141,14 @@ export function BillFormComponent({
onBlur={() => {
if (form.getFieldValue("jobid") !== null) {
loadLines({ variables: { id: form.getFieldValue("jobid") } });
if (form.getFieldValue("vendorid") !== null)
if (form.getFieldValue("vendorid") !== null) {
loadOutstandingReturns({
variables: {
jobId: form.getFieldValue("jobid"),
vendorId: form.getFieldValue("vendorid"),
},
});
}
}
}}
/>
@@ -273,12 +275,13 @@ export function BillFormComponent({
getFieldValue("jobid") &&
getFieldValue("vendorid")
) {
loadOutstandingReturns({
variables: {
jobId: form.getFieldValue("jobid"),
vendorId: form.getFieldValue("vendorid"),
},
});
//Removed as this would cause an additional reload when validating the form on submit and clear the values.
// loadOutstandingReturns({
// variables: {
// jobId: form.getFieldValue("jobid"),
// vendorId: form.getFieldValue("vendorid"),
// },
// });
}
if (
@@ -311,15 +314,17 @@ export function BillFormComponent({
>
<CurrencyInput min={0} disabled={disabled} />
</Form.Item>
<Form.Item label={t("bills.fields.allpartslocation")} name="location">
<Select style={{ width: "10rem" }} disabled={disabled} allowClear>
{bodyshop.md_parts_locations.map((loc, idx) => (
<Select.Option key={idx} value={loc}>
{loc}
</Select.Option>
))}
</Select>
</Form.Item>
{!billEdit && (
<Form.Item label={t("bills.fields.allpartslocation")} name="location">
<Select style={{ width: "10rem" }} disabled={disabled} allowClear>
{bodyshop.md_parts_locations.map((loc, idx) => (
<Select.Option key={idx} value={loc}>
{loc}
</Select.Option>
))}
</Select>
</Form.Item>
)}
</LayoutFormRow>
<LayoutFormRow>
<Form.Item

View File

@@ -63,7 +63,6 @@ export function BillFormContainer({
{!billEdit && (
<BillCmdReturnsTableComponent
form={form}
loadOutstandingReturns={loadOutstandingReturns}
returnLoading={returnLoading}
returnData={returnData}
/>

View File

@@ -1,14 +1,14 @@
import { DeleteFilled, DollarCircleFilled } from "@ant-design/icons";
import { useTreatments } from "@splitsoftware/splitio-react";
import {
Button,
Form,
Button, Form,
Input,
InputNumber,
Select,
Space,
Switch,
Table,
Tooltip,
Tooltip
} from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
@@ -17,9 +17,8 @@ import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CiecaSelect from "../../utils/Ciecaselect";
import BillLineSearchSelect from "../bill-line-search-select/bill-line-search-select.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import BilllineAddInventory from "../billline-add-inventory/billline-add-inventory.component";
import { useTreatments } from "@splitsoftware/splitio-react";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
@@ -323,28 +322,31 @@ export function BillEnterModalLinesComponent({
</Select>
),
},
{
title: t("billlines.fields.location"),
dataIndex: "location",
editable: true,
label: t("billlines.fields.location"),
formItemProps: (field) => {
return {
key: `${field.index}location`,
name: [field.name, "location"],
};
},
formInput: (record, index) => (
<Select disabled={disabled}>
{bodyshop.md_parts_locations.map((loc, idx) => (
<Select.Option key={idx} value={loc}>
{loc}
</Select.Option>
))}
</Select>
),
},
...(billEdit
? []
: [
{
title: t("billlines.fields.location"),
dataIndex: "location",
editable: true,
label: t("billlines.fields.location"),
formItemProps: (field) => {
return {
key: `${field.index}location`,
name: [field.name, "location"],
};
},
formInput: (record, index) => (
<Select disabled={disabled}>
{bodyshop.md_parts_locations.map((loc, idx) => (
<Select.Option key={idx} value={loc}>
{loc}
</Select.Option>
))}
</Select>
),
},
]),
{
title: t("billlines.labels.deductedfromlbr"),
dataIndex: "deductedfromlbr",
@@ -552,7 +554,20 @@ export function BillEnterModalLinesComponent({
});
return (
<Form.List name="billlines">
<Form.List
name="billlines"
rules={[
{
validator: async (_, billlines) => {
if (!billlines || billlines.length < 1) {
return Promise.reject(
new Error(t("billlines.validation.atleastone"))
);
}
},
},
]}
>
{(fields, { add, remove, move }) => {
return (
<>

View File

@@ -12,6 +12,7 @@ import { DateFormatter } from "../../utils/DateFormatter";
import { alphaSort, dateSort } from "../../utils/sorters";
import { TemplateList } from "../../utils/TemplateConstants";
import BillDeleteButton from "../bill-delete-button/bill-delete-button.component";
import BillDetailEditReturnComponent from "../bill-detail-edit/bill-detail-edit-return.component";
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
const mapStateToProps = createStructuredSelector({
@@ -58,39 +59,15 @@ export function BillsListTableComponent({
</Button>
)}
<BillDeleteButton bill={record} />
<Button
<BillDetailEditReturnComponent
data={{ bills_by_pk: { ...record, jobid: job.id } }}
disabled={
record.is_credit_memo ||
record.vendorid === bodyshop.inhousevendorid ||
jobRO
}
onClick={() => {
setPartsOrderContext({
actions: {},
context: {
jobId: job.id,
vendorId: record.vendorid,
returnFromBill: record.id,
invoiceNumber: record.invoice_number,
linesToOrder: record.billlines.map((i) => {
return {
line_desc: i.line_desc,
// db_price: i.actual_price,
act_price: i.actual_price,
cost: i.actual_cost,
quantity: i.quantity,
joblineid: i.joblineid,
oem_partno: i.jobline && i.jobline.oem_partno,
part_type: i.jobline && i.jobline.part_type,
};
}),
isReturn: true,
},
});
}}
>
{t("bills.actions.return")}
</Button>
/>
{record.isinhouse && (
<PrintWrapperComponent
templateObject={{

View File

@@ -279,31 +279,80 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
<Form.Item label={t("courtesycars.fields.notes")} name="notes">
<Input.TextArea />
</Form.Item>
<div>
<Form.Item
label={t("courtesycars.fields.registrationexpires")}
name="registrationexpires"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<FormDatePicker />
</Form.Item>
<Form.Item
shouldUpdate={(p, c) =>
p.registrationexpires !== c.registrationexpires
}
>
{() => {
const expires = form.getFieldValue("registrationexpires");
<Form.Item
label={t("courtesycars.fields.registrationexpires")}
name="registrationexpires"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<FormDatePicker />
</Form.Item>
<Form.Item
label={t("courtesycars.fields.insuranceexpires")}
name="insuranceexpires"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<FormDatePicker />
</Form.Item>
const dateover = expires && moment(expires).isBefore(moment());
if (dateover)
return (
<Space direction="vertical" style={{ color: "tomato" }}>
<span>
<WarningFilled style={{ marginRight: ".3rem" }} />
{t("contracts.labels.dateinpast")}
</span>
</Space>
);
return <></>;
}}
</Form.Item>
</div>
<div>
<Form.Item
label={t("courtesycars.fields.insuranceexpires")}
name="insuranceexpires"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<FormDatePicker />
</Form.Item>
<Form.Item
shouldUpdate={(p, c) =>
p.insuranceexpires !== c.insuranceexpires
}
>
{() => {
const expires = form.getFieldValue("insuranceexpires");
const dateover = expires && moment(expires).isBefore(moment());
if (dateover)
return (
<Space direction="vertical" style={{ color: "tomato" }}>
<span>
<WarningFilled style={{ marginRight: ".3rem" }} />
{t("contracts.labels.dateinpast")}
</span>
</Space>
);
return <></>;
}}
</Form.Item>
</div>
<Form.Item label={t("courtesycars.fields.dailycost")} name="dailycost">
<CurrencyInput />
</Form.Item>

View File

@@ -34,6 +34,9 @@ const CourtesyCarStatusComponent = ({ value, onChange }, ref) => {
<Option value="courtesycars.status.sold">
{t("courtesycars.status.sold")}
</Option>
<Option value="courtesycars.status.leasereturn">
{t("courtesycars.status.leasereturn")}
</Option>
</Select>
);
};

View File

@@ -52,6 +52,10 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
text: t("courtesycars.status.sold"),
value: "courtesycars.status.sold",
},
{
text: t("courtesycars.status.leasereturn"),
value: "courtesycars.status.leasereturn",
},
],
onFilter: (value, record) => value.includes(record.status),
sortOrder:

View File

@@ -54,7 +54,7 @@ export const uploadToCloudinary = async (
//Set variables for getting the signed URL.
let timestamp = Math.floor(Date.now() / 1000);
let public_id = key;
let tags = `${bodyshop.textid},${
let tags = `${bodyshop.imexshopid},${
tagsArray ? tagsArray.map((tag) => `${tag},`) : ""
}`;
// let eager = process.env.REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS;

View File

@@ -23,7 +23,7 @@ export default connect(
export function EmailDocumentsComponent({
emailConfig,
form,
selectedMediaState,
}) {
const { t } = useTranslation();
@@ -45,6 +45,13 @@ export function EmailDocumentsComponent({
{selectedMedia.filter((s) => s.isSelected).length >= 10 ? (
<div style={{ color: "red" }}>{t("messaging.labels.maxtenimages")}</div>
) : null}
{selectedMedia &&
selectedMedia
.filter((s) => s.isSelected)
.reduce((acc, val) => (acc = acc + val.size), 0) >=
10485760 - new Blob([form.getFieldValue("html")]).size ? (
<div style={{ color: "red" }}>{t("general.errors.sizelimit")}</div>
) : null}
{data && (
<JobDocumentsGalleryExternal
data={data ? data.documents : []}

View File

@@ -162,7 +162,10 @@ export function EmailOverlayComponent({
<Tabs>
{!bodyshop.uselocalmediaserver && (
<Tabs.TabPane tab={t("emails.labels.documents")} key="documents">
<EmailDocumentsComponent selectedMediaState={selectedMediaState} />
<EmailDocumentsComponent
selectedMediaState={selectedMediaState}
form={form}
/>
</Tabs.TabPane>
)}
<Tabs.TabPane tab={t("emails.labels.attachments")} key="attachments">
@@ -180,6 +183,24 @@ export function EmailOverlayComponent({
}
return e && e.fileList;
}}
rules={[
({ getFieldValue }) => ({
validator(rule, value) {
const totalSize = value.reduce(
(acc, val) => (acc = acc + val.size),
0
);
const limit =
10485760 - new Blob([form.getFieldValue("html")]).size;
if (totalSize > limit) {
return Promise.reject(t("general.errors.sizelimit"));
}
return Promise.resolve();
},
}),
]}
>
<Upload.Dragger
beforeUpload={Upload.LIST_IGNORE}

View File

@@ -168,7 +168,6 @@ export function EmailOverlayContainer({
useEffect(() => {
if (modalVisible) render();
}, [modalVisible]); // eslint-disable-line react-hooks/exhaustive-deps
return (
<Modal
destroyOnClose={true}
@@ -178,7 +177,15 @@ export function EmailOverlayContainer({
onCancel={() => {
toggleEmailOverlayVisible();
}}
okButtonProps={{ loading: sending }}
okButtonProps={{
loading: sending,
disabled:
selectedMedia &&
( (selectedMedia
.filter((s) => s.isSelected)
.reduce((acc, val) => (acc = acc + val.size), 0) >=
10485760 - new Blob([form.getFieldValue("html")]).size) || selectedMedia.filter((s) => s.isSelected).length > 10),
}}
>
<Form layout="vertical" form={form} onFinish={handleFinish}>
{loading && (

View File

@@ -30,6 +30,7 @@ class ErrorBoundary extends React.Component {
static getDerivedStateFromError(error) {
console.log("ErrorBoundary -> getDerivedStateFromError -> error", error);
return { hasErrored: true, error: error };
}

View File

@@ -5,7 +5,7 @@ import AlertComponent from "../alert/alert.component";
import { Prompt, useLocation } from "react-router-dom";
import "./form-fields-changed.styles.scss";
export default function FormsFieldChanged({ form }) {
export default function FormsFieldChanged({ form, skipPrompt }) {
const { t } = useTranslation();
const handleReset = () => {
@@ -25,7 +25,7 @@ export default function FormsFieldChanged({ form }) {
return (
<Space direction="vertical" style={{ width: "100%" }}>
<Prompt
when={true}
when={skipPrompt ? false : true}
message={(location) => {
if (loc.pathname === location.pathname) return false;
return t("general.messages.unsavedchangespopup");

View File

@@ -11,6 +11,7 @@ import AlertComponent from "../alert/alert.component";
import OwnerNameDisplay, {
OwnerNameDisplayFunction,
} from "../owner-name-display/owner-name-display.component";
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
export default function GlobalSearch() {
const { t } = useTranslation();
const history = useHistory();
@@ -97,7 +98,11 @@ export default function GlobalSearch() {
} ${vehicle.v_model_desc || ""}`}
</span>
<span>{vehicle.plate_no || ""}</span>
<span> {vehicle.v_vin || ""}</span>
<span>
<VehicleVinDisplay>
{vehicle.v_vin || ""}
</VehicleVinDisplay>
</span>
</Space>
</Link>
),

View File

@@ -247,40 +247,40 @@ export function ScheduleEventComponent({
);
const RegularEvent = event.isintake ? (
<div
<Space
wrap
size='small'
style={{
display: "flex",
flexWrap: "wrap",
height: "100%",
backgroundColor:
event.color && event.color.hex ? event.color.hex : event.color,
}}
>
<Space>
{event.note && <AlertFilled className="production-alert" />}
<strong>{`${event.job.ro_number || t("general.labels.na")}`}</strong>
<span>
<OwnerNameDisplay ownerObject={event.job} />
</span>
</Space>
<Space>
<span>
{`${(event.job && event.job.v_model_yr) || ""} ${
(event.job && event.job.v_make_desc) || ""
} ${(event.job && event.job.v_model_desc) || ""}`}
</span>
<span>
{`(${
(event.job && event.job.labhrs.aggregate.sum.mod_lb_hrs) || "0"
} / ${
(event.job && event.job.larhrs.aggregate.sum.mod_lb_hrs) || "0"
})`}
</span>
</Space>
{event.note && <AlertFilled className="production-alert" />}
<strong>{`${event.job.ro_number || t("general.labels.na")}`}</strong>
<OwnerNameDisplay ownerObject={event.job} />
{`${(event.job && event.job.v_model_yr) || ""} ${
(event.job && event.job.v_make_desc) || ""
} ${(event.job && event.job.v_model_desc) || ""}`}
{`(${(event.job && event.job.labhrs.aggregate.sum.mod_lb_hrs) || "0"} / ${
(event.job && event.job.larhrs.aggregate.sum.mod_lb_hrs) || "0"
})`}
{event.job && event.job.alt_transport && (
<div style={{ margin: ".1rem" }}>{event.job.alt_transport}</div>
)}
</div>
</Space>
) : (
<div style={{ height: "100%", width: "100%" }}>
<div
style={{
height: "100%",
width: "100%",
backgroundColor:
event.color && event.color.hex ? event.color.hex : event.color,
}}
>
<strong>{`${event.title || ""}`}</strong>
</div>
);
@@ -291,7 +291,13 @@ export function ScheduleEventComponent({
onVisibleChange={(vis) => !event.vacation && setVisible(vis)}
trigger="click"
content={event.block ? blockContent : popoverContent}
style={{ height: "100%", width: "100%" }}
style={{
height: "100%",
width: "100%",
backgroundColor:
event.color && event.color.hex ? event.color.hex : event.color,
}}
>
{RegularEvent}
</Popover>

View File

@@ -44,6 +44,7 @@ import JobCreateIOU from "../job-create-iou/job-create-iou.component";
import JobLinesExpander from "./job-lines-expander.component";
import { selectBodyshop } from "../../redux/user/user.selectors";
import moment from "moment";
import JobLineConvertToLabor from "../job-line-convert-to-labor/job-line-convert-to-labor.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -114,7 +115,10 @@ export function JobLinesComponent({
sortOrder:
state.sortedInfo.columnKey === "oem_partno" && state.sortedInfo.order,
ellipsis: true,
render: (text, record) => record.oem_partno,
render: (text, record) =>
`${record.oem_partno || ""} ${
record.alt_partno ? `(${record.alt_partno})` : ""
}`.trim(),
},
{
title: t("joblines.fields.op_code_desc"),
@@ -175,7 +179,7 @@ export function JobLinesComponent({
state.sortedInfo.columnKey === "act_price" && state.sortedInfo.order,
ellipsis: true,
render: (text, record) => (
<>
<JobLineConvertToLabor jobline={record} job={job}>
<CurrencyFormatter>
{record.db_ref === "900510" || record.db_ref === "900511"
? record.prt_dsmk_m
@@ -188,7 +192,7 @@ export function JobLinesComponent({
) : (
<></>
)}
</>
</JobLineConvertToLabor>
),
},
{
@@ -295,9 +299,9 @@ export function JobLinesComponent({
dataIndex: "actions",
key: "actions",
render: (text, record) => (
<div>
<Space>
{(record.manual_line || jobIsPrivate) && (
<Space>
<>
<Button
disabled={jobRO}
onClick={() => {
@@ -334,9 +338,9 @@ export function JobLinesComponent({
>
<DeleteFilled />
</Button>
</Space>
</>
)}
</div>
</Space>
),
},
];
@@ -460,7 +464,12 @@ export function JobLinesComponent({
context: {
jobId: job.id,
job: job,
linesToOrder: selectedLines,
linesToOrder: selectedLines.map((l) => ({
...l,
oem_partno: `${l.oem_partno || ""} ${
l.alt_partno ? `(${l.alt_partno})` : ""
}`.trim(),
})),
},
});
@@ -476,7 +485,18 @@ export function JobLinesComponent({
setState({
...state,
filteredInfo: {
part_type: ["PAN,PAC,PAR,PAL,PAA,PAM,PAP,PAS,PASL,PAG"],
part_type: [
"PAN",
"PAC",
"PAR",
"PAL",
"PAA",
"PAM",
"PAP",
"PAS",
"PASL",
"PAG",
],
},
});
}}

View File

@@ -0,0 +1,256 @@
import { ClockCircleOutlined } from "@ant-design/icons";
import { useApolloClient } from "@apollo/client";
import {
Button,
Card,
Form,
notification,
Popover,
Select,
Space,
Tooltip,
} from "antd";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { logImEXEvent } from "../../firebase/firebase.utils";
import {
QUERY_JOB_LBR_ADJUSTMENTS,
UPDATE_JOB,
} from "../../graphql/jobs.queries";
import _ from "lodash";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { UPDATE_JOB_LINE } from "../../graphql/jobs-lines.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(JobLineConvertToLabor);
export function JobLineConvertToLabor({
children,
jobline,
job,
insertAuditTrail,
...otherBtnProps
}) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [form] = Form.useForm();
const [visibility, setVisibility] = useState(false);
const client = useApolloClient();
const handleFinish = async (values) => {
const { mod_lbr_ty } = values;
logImEXEvent("job_convert_dollar_to_labor");
setLoading(true);
const existingAdjustments = await client.query({
query: QUERY_JOB_LBR_ADJUSTMENTS,
variables: {
id: job.id,
},
});
const newAdjustments = _.cloneDeep(
existingAdjustments.data.jobs_by_pk.lbr_adjustments
);
const adjustment = calculateAdjustment({ mod_lbr_ty, job, jobline });
newAdjustments[mod_lbr_ty] = (newAdjustments[mod_lbr_ty] || 0) + adjustment;
const jobUpdate = client.mutate({
mutation: UPDATE_JOB,
variables: {
jobId: job.id,
job: { lbr_adjustments: newAdjustments },
},
});
const lineUpdate = client.mutate({
mutation: UPDATE_JOB_LINE,
variables: {
lineId: jobline.id,
line: {
convertedtolbr: true,
convertedtolbr_data: {
mod_lbr_ty: mod_lbr_ty,
mod_lb_hrs: adjustment,
},
},
},
});
if (!!jobUpdate.errors) {
notification["error"]({
message: t("jobs.errors.saving", {
message: JSON.stringify(jobUpdate.errors),
}),
});
return;
}
if (!!lineUpdate.errors) {
notification["error"]({
message: t("joblines.errors.saving", {
message: JSON.stringify(lineUpdate.errors),
}),
});
return;
}
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobmodifylbradj({
hours: calculateAdjustment({ mod_lbr_ty, job, jobline }).toFixed(1),
mod_lbr_ty,
}),
});
setLoading(false);
setVisibility(false);
};
const overlay = (
<Card>
<Form form={form} layout="vertical" onFinish={handleFinish}>
<Form.Item
label={t("joblines.fields.mod_lbr_ty")}
name="mod_lbr_ty"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Select allowClear optionFilterProp="children" showSearch>
<Select.Option value="LAA">
{t("joblines.fields.lbr_types.LAA")}
</Select.Option>
<Select.Option value="LAB">
{t("joblines.fields.lbr_types.LAB")}
</Select.Option>
<Select.Option value="LAD">
{t("joblines.fields.lbr_types.LAD")}
</Select.Option>
<Select.Option value="LAE">
{t("joblines.fields.lbr_types.LAE")}
</Select.Option>
<Select.Option value="LAF">
{t("joblines.fields.lbr_types.LAF")}
</Select.Option>
<Select.Option value="LAG">
{t("joblines.fields.lbr_types.LAG")}
</Select.Option>
<Select.Option value="LAM">
{t("joblines.fields.lbr_types.LAM")}
</Select.Option>
<Select.Option value="LAR">
{t("joblines.fields.lbr_types.LAR")}
</Select.Option>
<Select.Option value="LAS">
{t("joblines.fields.lbr_types.LAS")}
</Select.Option>
<Select.Option value="LAU">
{t("joblines.fields.lbr_types.LAU")}
</Select.Option>
<Select.Option value="LA1">
{t("joblines.fields.lbr_types.LA1")}
</Select.Option>
<Select.Option value="LA2">
{t("joblines.fields.lbr_types.LA2")}
</Select.Option>
<Select.Option value="LA3">
{t("joblines.fields.lbr_types.LA3")}
</Select.Option>
<Select.Option value="LA4">
{t("joblines.fields.lbr_types.LA4")}
</Select.Option>
</Select>
</Form.Item>
<Form.Item shouldUpdate>
{() => {
const { mod_lbr_ty } = form.getFieldsValue();
return t("joblines.labels.adjustmenttobeadded", {
adjustment: calculateAdjustment({
mod_lbr_ty,
job,
jobline,
}).toFixed(1),
});
}}
</Form.Item>
<Space wrap>
<Button
type="primary"
disabled={jobline.convertedtolbr}
htmlType="submit"
>
{t("general.actions.save")}
</Button>
<Button onClick={() => setVisibility(false)}>
{t("general.actions.cancel")}
</Button>
</Space>
</Form>
</Card>
);
const handleClick = (e) => {
setLoading(true);
form.setFieldsValue({
// date: new moment(),
// bodyhrs: Math.round(v.bodyhrs * 10) / 10,
// painthrs: Math.round(v.painthrs * 10) / 10,
});
setVisibility(true);
setLoading(false);
};
return (
<>
{children}
{jobline.act_price !== 0 && (
<Popover
disabled={jobline.convertedtolbr}
content={overlay}
visible={visibility}
placement="bottom"
>
<Tooltip title={t("joblines.actions.converttolabor")}>
<Button
type="link"
disabled={jobline.convertedtolbr}
loading={loading}
onClick={handleClick}
{...otherBtnProps}
>
<ClockCircleOutlined />
</Button>
</Tooltip>
</Popover>
)}
</>
);
}
function calculateAdjustment({ mod_lbr_ty, job, jobline }) {
if (!mod_lbr_ty) return 0;
const rate = job[`rate_${mod_lbr_ty.toLowerCase()}`];
if (rate === 0 || rate === null || rate === undefined) return 0;
const adj = jobline.act_price / job[`rate_${mod_lbr_ty.toLowerCase()}`];
return adj;
}

View File

@@ -1,3 +1,4 @@
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import { useMutation } from "@apollo/client";
import { Button, Form, notification } from "antd";
import React, { useEffect, useState } from "react";
@@ -37,6 +38,8 @@ export function JobsAdminDatesChange({ insertAuditTrail, job }) {
setLoading(true);
const result = await updateJob({
variables: { jobId: job.id, job: values },
refetchQueries: ['GET_JOB_BY_PK'],
awaitRefetchQueries:true
});
const changedAuditFields = form.getFieldsValue(
@@ -65,6 +68,8 @@ export function JobsAdminDatesChange({ insertAuditTrail, job }) {
}),
});
}
form.resetFields();
form.resetFields();
setLoading(false);
//Get the owner details, populate it all back into the job.
};
@@ -87,6 +92,7 @@ export function JobsAdminDatesChange({ insertAuditTrail, job }) {
: null,
}}
>
<FormFieldsChanged form={form} />
<LayoutFormRow header={t("jobs.forms.estdates")}>
<Form.Item
label={t("jobs.fields.date_estimated")}

View File

@@ -1,4 +1,4 @@
import { Form, Select } from "antd";
import { Form, Select, Space, Tooltip } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -8,6 +8,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
import LaborTypeFormItem from "../form-items-formatted/labor-type-form-item.component";
import PartTypeFormItem from "../form-items-formatted/part-type-form-item.component";
import ReadOnlyFormItem from "../form-items-formatted/read-only-form-item.component";
import { WarningOutlined } from "@ant-design/icons";
import "./jobs-close-lines.styles.scss";
const mapStateToProps = createStructuredSelector({
@@ -62,14 +63,23 @@ export function JobsCloseLines({ bodyshop, job, jobRO }) {
</Form.Item>
</td>
<td>
<Form.Item
span={2}
// label={t("joblines.fields.act_price")}
key={`${index}act_price`}
name={[field.name, "act_price"]}
>
<ReadOnlyFormItem type="currency" />
</Form.Item>
<Space>
<Form.Item
span={2}
// label={t("joblines.fields.act_price")}
key={`${index}act_price`}
name={[field.name, "act_price"]}
>
<ReadOnlyFormItem type="currency" />
</Form.Item>
<Form.Item
noStyle
key={`${index}convertedtolbr`}
name={[field.name, "convertedtolbr"]}
>
<HasBeenConvertedTolabor />
</Form.Item>
</Space>
</td>
<td>
<Form.Item
@@ -192,3 +202,14 @@ export function JobsCloseLines({ bodyshop, job, jobRO }) {
);
}
export default connect(mapStateToProps, mapDispatchToProps)(JobsCloseLines);
const HasBeenConvertedTolabor = ({ value }) => {
const { t } = useTranslation();
console.log(value);
if (!value) return null;
return (
<Tooltip title={t("joblines.labels.convertedtolabor")}>
<WarningOutlined style={{ color: "tomato" }} />
</Tooltip>
);
};

View File

@@ -4,6 +4,7 @@ import { Table, Input, Card, Space } from "antd";
import { Link } from "react-router-dom";
import { alphaSort } from "../../utils/sorters";
import JobCreateContext from "../../pages/jobs-create/jobs-create.context";
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
export default function JobsCreateVehicleInfoSearchComponent({
loading,
@@ -27,7 +28,9 @@ export default function JobsCreateVehicleInfoSearchComponent({
tableState.sortedInfo.columnKey === "v_vin" &&
tableState.sortedInfo.order,
render: (text, record) => (
<Link to={"/manage/vehicles/" + record.id}>{record.v_vin}</Link>
<Link to={"/manage/vehicles/" + record.id}>
<VehicleVinDisplay>{record.v_vin}</VehicleVinDisplay>
</Link>
),
},
{

View File

@@ -23,6 +23,7 @@ import JobsRelatedRos from "../jobs-related-ros/jobs-related-ros.component";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import ProductionListColumnComment from "../production-list-columns/production-list-columns.comment.component";
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
const mapStateToProps = createStructuredSelector({
jobRO: selectJobReadOnly,
@@ -180,6 +181,11 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
<DataLabel key="4" label={t("owners.fields.ownr_ea")}>
{job.ownr_ea || ""}
</DataLabel>
{job.owner?.tax_number && (
<DataLabel key="5" label={t("owners.fields.tax_number")}>
{job.owner?.tax_number || ""}
</DataLabel>
)}
</div>
</Card>
</Col>
@@ -211,27 +217,39 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
}`})`}
</DataLabel>
<DataLabel key="4" label={t("vehicles.fields.v_vin")}>
{`${job.v_vin || t("general.labels.na")}`}
<VehicleVinDisplay>
{`${job.v_vin || t("general.labels.na")}`}
</VehicleVinDisplay>
</DataLabel>
<DataLabel label={t("jobs.labels.relatedros")}>
<JobsRelatedRos jobid={job.id} job={job} />
</DataLabel>
{job.vehicle && job.vehicle.notes && (
<DataLabel label={t("vehicles.fields.notes")}>
<span style={{ whiteSpace: "pre" }}>{job.vehicle.notes}</span>
<DataLabel
label={t("vehicles.fields.notes")}
valueStyle={{ whiteSpace: "pre-wrap" }}
>
{job.vehicle.notes}
</DataLabel>
)}
{job.vehicle && job.vehicle.v_paint_codes && (
<DataLabel
label={t("vehicles.fields.v_paint_codes", { number: "" })}
>
<span style={{ whiteSpace: "pre" }}>
{Object.keys(job.vehicle.v_paint_codes)
.filter(
(key) =>
job.vehicle.v_paint_codes[key] !== "" &&
job.vehicle.v_paint_codes[key] !== null &&
job.vehicle.v_paint_codes[key] !== undefined
)
.map((key, idx) => (
<Tag key={idx}>{job.vehicle.v_paint_codes[key]}</Tag>
))}
</span>
</DataLabel>
)}
{
// job.vehicle && job.vehicle.v_paint_codes && (
// <DataLabel label={t("vehicles.fields.v_paint_codes")}>
// <span style={{ whiteSpace: "pre" }}>
// {Object.keys(job.vehicle.v_paint_codes).map((key, idx) => (
// <Tag key={idx}>{job.vehicle.v_paint_codes[key]}</Tag>
// ))}
// </span>
// </DataLabel>
// )
}
</div>
</Card>
</Col>

View File

@@ -25,6 +25,7 @@ function JobsDocumentGalleryExternal({
id: value.id,
type: value.type,
tags: [{ value: value.type, title: value.type }],
size: value.size,
});
}

View File

@@ -14,6 +14,7 @@ import { selectAllMedia } from "../../redux/media/media.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { CreateExplorerLinkForJob } from "../../utils/localmedia";
import DocumentsLocalUploadComponent from "../documents-local-upload/documents-local-upload.component";
import JobsLocalGalleryDownloadButton from "./jobs-documents-local-gallery.download";
import JobsDocumentsLocalGalleryReassign from "./jobs-documents-local-gallery.reassign.component";
import JobsDocumentsLocalGallerySelectAllComponent from "./jobs-documents-local-gallery.selectall.component";
@@ -78,6 +79,7 @@ export function JobsDocumentsLocalGallery({
</a>
<JobsDocumentsLocalGalleryReassign jobid={job.id} />
<JobsDocumentsLocalGallerySelectAllComponent jobid={job.id} />
<JobsLocalGalleryDownloadButton job={job} />
</Space>
<Card>
<DocumentsLocalUploadComponent

View File

@@ -0,0 +1,74 @@
import { Button } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import cleanAxios from "../../utils/CleanAxios";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectAllMedia } from "../../redux/media/media.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
allMedia: selectAllMedia,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(JobsLocalGalleryDownloadButton);
export function JobsLocalGalleryDownloadButton({
bodyshop,
galleryImages,
allMedia,
job,
}) {
const { t } = useTranslation();
const [download, setDownload] = useState(null);
function downloadProgress(progressEvent) {
setDownload((currentDownloadState) => {
return {
downloaded: progressEvent.loaded || 0,
speed:
(progressEvent.loaded || 0) -
((currentDownloadState && currentDownloadState.downloaded) || 0),
};
});
}
const handleDownload = async () => {
const theDownloadedZip = await cleanAxios.post(
`${bodyshop.localmediaserverhttp}/jobs/download`,
{
jobid: job.id,
files: ((allMedia && allMedia[job.id]) || [])
.filter((i) => i.isSelected)
.map((i) => i.filename),
},
{
headers: { ims_token: bodyshop.localmediatoken },
responseType: "arraybuffer",
onDownloadProgress: downloadProgress,
}
);
setDownload(null);
standardMediaDownload(theDownloadedZip.data, job.ro_number);
};
return (
<Button loading={!!download} onClick={handleDownload}>
{t("documents.actions.download")}
</Button>
);
}
function standardMediaDownload(bufferData, filename) {
const a = document.createElement("a");
const url = window.URL.createObjectURL(new Blob([bufferData]));
a.href = url;
a.download = `${filename}.zip`;
a.click();
}

View File

@@ -77,6 +77,12 @@ export function JobsList({ bodyshop }) {
(j.v_model_desc || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.est_ct_fn || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.est_ct_ln || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.v_make_desc || "")
.toLowerCase()
.includes(searchText.toLowerCase())
@@ -264,6 +270,32 @@ export function JobsList({ bodyshop }) {
<CurrencyFormatter>{record.clm_total}</CurrencyFormatter>
),
},
{
title: t("jobs.labels.estimator"),
dataIndex: "jobs.labels.estimator",
key: "jobs.labels.estimator",
ellipsis: true,
responsive: ["xl"],
filterSearch: true,
filters:
(jobs &&
jobs
.map((j) => `${j.est_ct_fn || ""} ${j.est_ct_ln || ""}`.trim())
.filter(onlyUnique)
.map((s) => {
return {
text: s || "N/A",
value: [s],
};
})) ||
[],
onFilter: (value, record) =>
value.includes(
`${record.est_ct_fn || ""} ${record.est_ct_ln || ""}`.trim()
),
render: (text, record) =>
`${record.est_ct_fn || ""} ${record.est_ct_ln || ""}`.trim(),
},
{
title: t("jobs.fields.comment"),
dataIndex: "comment",

View File

@@ -14,6 +14,7 @@ import { selectJobReadOnly } from "../../redux/application/application.selectors
import { setModalContext } from "../../redux/modals/modals.actions";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import { TemplateList } from "../../utils/TemplateConstants";
import useLocalStorage from "../../utils/useLocalStorage";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import NoteUpsertModal from "../note-upsert-modal/note-upsert-modal.container";
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
@@ -40,6 +41,8 @@ export function JobNotesComponent({
relatedRos,
}) {
const { t } = useTranslation();
const [filter, setFilter] = useLocalStorage("filter_job_notes_icons", null);
const Templates = TemplateList("job_special", {
ro_number,
});
@@ -50,6 +53,18 @@ export function JobNotesComponent({
dataIndex: "icons",
key: "icons",
width: 80,
filteredValue: filter?.icons || null,
filters: [
{
text: t("notes.labels.usernotes"),
value: false,
},
{
text: t("notes.labels.systemnotes"),
value: true,
},
],
onFilter: (value, record) => record.audit === value,
render: (text, record) => (
<span>
{record.critical ? (
@@ -131,6 +146,10 @@ export function JobNotesComponent({
},
];
const handleTableChange = (pagination, filters, sorter) => {
setFilter(filters);
};
return (
<div>
<LayoutFormRow>
@@ -166,6 +185,7 @@ export function JobNotesComponent({
columns={columns}
rowKey="id"
dataSource={data}
onChange={handleTableChange}
/>
</Card>
</div>

View File

@@ -89,6 +89,12 @@ export function JobsReadyList({ bodyshop }) {
(j.v_model_desc || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.est_ct_fn || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.est_ct_ln || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.v_make_desc || "")
.toLowerCase()
.includes(searchText.toLowerCase())
@@ -276,6 +282,32 @@ export function JobsReadyList({ bodyshop }) {
<CurrencyFormatter>{record.clm_total}</CurrencyFormatter>
),
},
{
title: t("jobs.labels.estimator"),
dataIndex: "jobs.labels.estimator",
key: "jobs.labels.estimator",
ellipsis: true,
responsive: ["xl"],
filterSearch: true,
filters:
(jobs &&
jobs
.map((j) => `${j.est_ct_fn || ""} ${j.est_ct_ln || ""}`.trim())
.filter(onlyUnique)
.map((s) => {
return {
text: s || "N/A",
value: [s],
};
})) ||
[],
onFilter: (value, record) =>
value.includes(
`${record.est_ct_fn || ""} ${record.est_ct_ln || ""}`.trim()
),
render: (text, record) =>
`${record.est_ct_fn || ""} ${record.est_ct_ln || ""}`.trim(),
},
{
title: t("jobs.fields.comment"),
dataIndex: "comment",

View File

@@ -12,7 +12,24 @@ import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { UPDATE_JOB } from "../../graphql/jobs.queries";
export default function LaborAllocationsAdjustmentEdit({
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { insertAuditTrail } from "../../redux/application/application.actions";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(LaborAllocationsAdjustmentEdit);
export function LaborAllocationsAdjustmentEdit({
insertAuditTrail,
jobId,
mod_lbr_ty,
adjustments,
@@ -51,6 +68,15 @@ export default function LaborAllocationsAdjustmentEdit({
notification["success"]({
message: t("jobs.successes.save"),
});
insertAuditTrail({
jobid: jobId,
operation: AuditTrailMapping.jobmodifylbradj({
mod_lbr_ty: values.mod_lbr_ty,
hours:
values.hours -
((adjustments && adjustments[mod_lbr_ty]) || 0).toFixed(1),
}),
});
}
setLoading(false);
setVisible(false);

View File

@@ -1,16 +1,18 @@
import { EditFilled } from "@ant-design/icons";
import { Card, Space, Table } from "antd";
import React, { useEffect, useState } from "react";
import { Card, Col, Row, Space, Table } from "antd";
import _ from "lodash";
import React, { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { selectTechnician } from "../../redux/tech/tech.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { alphaSort } from "../../utils/sorters";
import LaborAllocationsAdjustmentEdit from "../labor-allocations-adjustment-edit/labor-allocations-adjustment-edit.component";
import "./labor-allocations-table.styles.scss";
import { CalculateAllocationsTotals } from "./labor-allocations-table.utility";
import _ from "lodash";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
technician: selectTechnician,
@@ -43,6 +45,11 @@ export function LaborAllocationsTable({
if (!jobId) setTotals([]);
}, [joblines, timetickets, bodyshop, adjustments, jobId]);
const convertedLines = useMemo(
() => joblines && joblines.filter((j) => j.convertedtolbr),
[joblines]
);
const columns = [
{
title: t("timetickets.fields.cost_center"),
@@ -114,24 +121,105 @@ export function LaborAllocationsTable({
),
},
];
const convertedTableCols = [
{
title: t("joblines.fields.line_desc"),
dataIndex: "line_desc",
key: "line_desc",
ellipsis: true,
},
{
title: t("joblines.fields.op_code_desc"),
dataIndex: "op_code_desc",
key: "op_code_desc",
ellipsis: true,
render: (text, record) =>
`${record.op_code_desc || ""}${
record.alt_partm ? ` ${record.alt_partm}` : ""
}`,
},
{
title: t("joblines.fields.act_price"),
dataIndex: "act_price",
key: "act_price",
ellipsis: true,
render: (text, record) => (
<>
<CurrencyFormatter>
{record.db_ref === "900510" || record.db_ref === "900511"
? record.prt_dsmk_m
: record.act_price}
</CurrencyFormatter>
{record.prt_dsmk_p && record.prt_dsmk_p !== 0 ? (
<span
style={{ marginLeft: ".2rem" }}
>{`(${record.prt_dsmk_p}%)`}</span>
) : (
<></>
)}
</>
),
},
{
title: t("joblines.fields.part_qty"),
dataIndex: "part_qty",
key: "part_qty",
},
{
title: t("joblines.fields.mod_lbr_ty"),
dataIndex: "conv_mod_lbr_ty",
key: "conv_mod_lbr_ty",
render: (text, record) =>
record.convertedtolbr_data && record.convertedtolbr_data.mod_lbr_ty,
},
{
title: t("joblines.fields.mod_lb_hrs"),
dataIndex: "conv_mod_lb_hrs",
key: "conv_mod_lb_hrs",
render: (text, record) =>
record.convertedtolbr_data &&
record.convertedtolbr_data.mod_lb_hrs &&
record.convertedtolbr_data.mod_lb_hrs.toFixed(1),
},
];
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
return (
<Card title={t("jobs.labels.laborallocations")}>
<Table
columns={columns}
rowKey="cost_center"
pagination={false}
onChange={handleTableChange}
dataSource={totals}
scroll={{
x: true,
}}
/>
</Card>
<Row gutter={[16, 16]}>
<Col span={24}>
<Card title={t("jobs.labels.laborallocations")}>
<Table
columns={columns}
rowKey="cost_center"
pagination={false}
onChange={handleTableChange}
dataSource={totals}
scroll={{
x: true,
}}
/>
</Card>
</Col>
{convertedLines && convertedLines.length > 0 && (
<Col span={24}>
<Card title={t("jobs.labels.convertedtolabor")}>
<Table
columns={convertedTableCols}
rowKey="id"
pagination={false}
dataSource={convertedLines}
scroll={{
x: true,
}}
/>
</Card>
</Col>
)}
</Row>
);
}
export default connect(mapStateToProps, null)(LaborAllocationsTable);

View File

@@ -94,6 +94,12 @@ export default function OwnerDetailFormComponent({ form, loading }) {
>
<Input />
</Form.Item>
<Form.Item
label={t("owners.fields.tax_number")}
name="tax_number"
>
<Input />
</Form.Item>
</LayoutFormRow>
<Form.Item label={t("owners.fields.note")} name="note">
<Input.TextArea rows={4} />

View File

@@ -20,9 +20,10 @@ function OwnerDetailFormContainer({ owner, refetch }) {
if (!!result.errors) {
notification["error"]({
message: t("owners.errors.saving", {
message: JSON.stringify(result.errors),
error: JSON.stringify(result.errors),
}),
});
setLoading(false);
return;
}

View File

@@ -31,7 +31,7 @@ export default function OwnerFindModalContainer({
useEffect(() => {
if (modalProps.visible && owner) {
const s = OwnerNameDisplayFunction(owner);
const s = OwnerNameDisplayFunction(owner, true);
setSearchText(s.trim());
callSearchowners({ variables: { search: s.trim() } });

View File

@@ -27,7 +27,7 @@ export function OwnerNameDisplay({ bodyshop, ownerObject }) {
}`.trim();
}
export function OwnerNameDisplayFunction(ownerObject) {
export function OwnerNameDisplayFunction(ownerObject, forceFirstLast = false) {
const emptyTest =
ownerObject.ownr_fn + ownerObject.ownr_ln + ownerObject.ownr_co_nm;
@@ -36,7 +36,7 @@ export function OwnerNameDisplayFunction(ownerObject) {
const rdxStore = store.getState();
if (rdxStore.user.bodyshop.last_name_first)
if (rdxStore.user.bodyshop.last_name_first && !forceFirstLast)
return `${ownerObject.ownr_ln || ""}, ${ownerObject.ownr_fn || ""} ${
ownerObject.ownr_co_nm || ""
}`.trim();

View File

@@ -124,6 +124,15 @@ export function PartsOrderModalComponent({
<Checkbox />
</Form.Item>
)}
{OEConnection.treatment === "on" && !isReturn && (
<Form.Item
name="is_quote"
label={t("parts_orders.labels.is_quote")}
valuePropName="checked"
>
<Checkbox />
</Form.Item>
)}
</LayoutFormRow>
<Divider orientation="left">
{t("parts_orders.labels.inthisorder")}
@@ -291,17 +300,32 @@ export function PartsOrderModalComponent({
<Input.TextArea rows={3} />
</Form.Item>
<Radio.Group
defaultValue={sendType}
onChange={(e) => setSendType(e.target.value)}
>
<Radio value={"none"}>{t("general.labels.none")}</Radio>
<Radio value={"e"}>{t("parts_orders.labels.email")}</Radio>
<Radio value={"p"}>{t("parts_orders.labels.print")}</Radio>
{OEConnection.treatment === "on" && !isReturn && (
<Radio value={"oec"}>{t("parts_orders.labels.oec")}</Radio>
)}
</Radio.Group>
<Form.Item noStyle shouldUpdate>
{() => {
const is_quote = form.getFieldValue("is_quote");
if (is_quote) setSendType("oec");
return (
<Radio.Group
defaultValue={sendType}
value={sendType}
onChange={(e) => setSendType(e.target.value)}
>
<Radio disabled={is_quote} value={"none"}>
{t("general.labels.none")}
</Radio>
<Radio disabled={is_quote} value={"e"}>
{t("parts_orders.labels.email")}
</Radio>
<Radio disabled={is_quote} value={"p"}>
{t("parts_orders.labels.print")}
</Radio>
{OEConnection.treatment === "on" && !isReturn && (
<Radio value={"oec"}>{t("parts_orders.labels.oec")}</Radio>
)}
</Radio.Group>
);
}}
</Form.Item>
</div>
);
}

View File

@@ -93,32 +93,56 @@ export function PartsOrderModalContainer({
const [updateJobLines] = useMutation(UPDATE_JOB_LINE_STATUS);
const [updateJob] = useMutation(UPDATE_JOB);
const handleFinish = async ({ removefrompartsqueue, ...values }) => {
const handleFinish = async ({
removefrompartsqueue,
is_quote,
...values
}) => {
logImEXEvent("parts_order_insert");
setSaving(true);
const insertResult = await insertPartOrder({
variables: {
po: [
{
...values,
order_date: moment().format("YYYY-MM-DD"),
orderedby: currentUser.email,
jobid: jobId,
user_email: currentUser.email,
return: isReturn,
status: bodyshop.md_order_statuses.default_ordered || "Ordered*",
},
],
},
refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID"],
});
if (!!insertResult.error) {
notification["error"]({
message: t("parts_orders.errors.creating"),
description: JSON.stringify(insertResult.error),
let insertResult;
insertResult = await insertPartOrder({
variables: {
po: [
{
...values,
order_date: moment().format("YYYY-MM-DD"),
orderedby: currentUser.email,
jobid: jobId,
user_email: currentUser.email,
return: isReturn,
status: is_quote
? bodyshop.md_order_statuses.default_quote || "Quote"
: bodyshop.md_order_statuses.default_ordered || "Ordered*",
},
],
},
refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID"],
});
return;
}
if (!!insertResult.errors) {
notification["error"]({
message: t("parts_orders.errors.creating"),
description: JSON.stringify(insertResult.errors),
});
return;
}
notification["success"]({
message: values.isReturn
? t("parts_orders.successes.return_created")
: t("parts_orders.successes.created"),
});
insertAuditTrail({
jobid: jobId,
operation: isReturn
? AuditTrailMapping.jobspartsreturn(
insertResult.data.insert_parts_orders.returning[0].order_number
)
: AuditTrailMapping.jobspartsorder(
insertResult.data.insert_parts_orders.returning[0].order_number
),
});
const jobLinesResult = await updateJobLines({
variables: {
@@ -127,6 +151,8 @@ export function PartsOrderModalContainer({
.map((item) => item.job_line_id),
status: isReturn
? bodyshop.md_order_statuses.default_returned || "Returned*"
: is_quote
? bodyshop.md_order_statuses.default_quote || "Quote"
: bodyshop.md_order_statuses.default_ordered || "Ordered*",
},
});
@@ -142,17 +168,6 @@ export function PartsOrderModalContainer({
});
}
insertAuditTrail({
jobid: jobId,
operation: isReturn
? AuditTrailMapping.jobspartsreturn(
insertResult.data.insert_parts_orders.returning[0].order_number
)
: AuditTrailMapping.jobspartsorder(
insertResult.data.insert_parts_orders.returning[0].order_number
),
});
if (!!jobLinesResult.errors) {
notification["error"]({
message: t("parts_orders.errors.creating"),
@@ -160,12 +175,6 @@ export function PartsOrderModalContainer({
});
}
notification["success"]({
message: values.isReturn
? t("parts_orders.successes.return_created")
: t("parts_orders.successes.created"),
});
if (values.vendorid === bodyshop.inhousevendorid) {
setBillEnterContext({
actions: { refetch: refetch },

View File

@@ -2,7 +2,6 @@ import { PauseCircleOutlined } from "@ant-design/icons";
import { Space } from "antd";
import i18n from "i18next";
import moment from "moment";
import React from "react";
import { Link } from "react-router-dom";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { TimeFormatter } from "../../utils/DateFormatter";
@@ -530,6 +529,27 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
<JobPartsQueueCount parts={record.joblines_status} record={record} />
),
},
//Added as a place holder for St Claude. Not implemented as it requires another join for a field used by only 1 client.
// {
// title: i18n.t("vehicles.fields.v_paint_codes", { number: "" }),
// dataIndex: "v_paint_codes",
// key: "v_paint_codes",
// render: (text, record) =>
// record.vehicle?.v_paint_codes ? (
// <span style={{ whiteSpace: "pre" }}>
// {Object.keys(record.vehicle.v_paint_codes)
// .filter(
// (key) =>
// record.vehicle.v_paint_codes[key] !== "" &&
// record.vehicle.v_paint_codes[key] !== null &&
// record.vehicle.v_paint_codes[key] !== undefined
// )
// .map((key, idx) => (
// <Tag key={idx}>{record.vehicle.v_paint_codes[key]}</Tag>
// ))}
// </span>
// ) : null,
// },
];
};
export default r;

View File

@@ -15,6 +15,7 @@ import {
import { selectBodyshop } from "../../redux/user/user.selectors";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import ScheduleBlockDay from "../schedule-block-day/schedule-block-day.component";
import ScheduleCalendarHeaderGraph from "./schedule-calendar-header-graph.component";
const mapStateToProps = createStructuredSelector({
@@ -38,7 +39,9 @@ export function ScheduleCalendarHeaderComponent({
const ATSToday = useMemo(() => {
if (!events) return [];
return _.groupBy(
events.filter((e) => moment(date).isSame(moment(e.start), "day")),
events.filter(
(e) => !e.vacation && moment(date).isSame(moment(e.start), "day")
),
"job.alt_transport"
);
}, [events, date]);
@@ -66,6 +69,9 @@ export function ScheduleCalendarHeaderComponent({
<td>
<Link to={`/manage/jobs/${j.id}`}>{j.ro_number}</Link>
</td>
<td>
<OwnerNameDisplay ownerObject={j} />
</td>
<td>
{`(${(
j.labhrs.aggregate.sum.mod_lb_hrs +
@@ -99,6 +105,9 @@ export function ScheduleCalendarHeaderComponent({
<td>
<Link to={`/manage/jobs/${j.id}`}>{j.ro_number}</Link>
</td>
<td>
<OwnerNameDisplay ownerObject={j} />
</td>
<td>
{`(${(
j.labhrs.aggregate.sum.mod_lb_hrs +

View File

@@ -4,6 +4,10 @@
.rbc-time-view .rbc-allday-cell {
height: unset;
}
.rbc-month-row{
overflow: unset !important;
}
// .rbc-row-content {
// display: none;
// }

View File

@@ -34,7 +34,7 @@ export function ScheduleCalendarWrapperComponent({
const { t } = useTranslation();
const handleEventPropStyles = (event, start, end, isSelected) => {
return {
...(event.color
...(event.color && !((search.view || defaultView) === "agenda")
? {
style: {
backgroundColor:

View File

@@ -80,6 +80,7 @@ export default function ScoreboardTimeTickets() {
totalThisMonth: 0,
totalLastMonth: 0,
totalOverPeriod: 0,
actualTotalOverPeriod: 0,
employees: {},
};
data.fixedperiod.forEach((ticket) => {
@@ -92,6 +93,7 @@ export default function ScoreboardTimeTickets() {
totalThisMonth: 0,
totalLastMonth: 0,
totalOverPeriod: 0,
actualTotalOverPeriod: 0,
};
}
@@ -182,6 +184,9 @@ export default function ScoreboardTimeTickets() {
ret.employees[ticket.employee.employee_number].totalOverPeriod =
ret.employees[ticket.employee.employee_number].totalOverPeriod +
ticket.productivehrs;
ret.employees[ticket.employee.employee_number].actualTotalOverPeriod =
ret.employees[ticket.employee.employee_number].actualTotalOverPeriod +
(ticket.actualhrs || 0);
if (!totals.employees[ticket.employee.employee_number])
totals.employees[ticket.employee.employee_number] = {
@@ -219,7 +224,7 @@ export default function ScoreboardTimeTickets() {
roundObject(ret);
roundObject(totals);
roundObject(ret2);
console.log(ret);
return {
fixed: ret,
timeperiod: {
@@ -231,6 +236,8 @@ export default function ScoreboardTimeTickets() {
};
}, [fixedPeriods, data, startDate, endDate]);
console.log(calculatedData);
if (error) return <AlertComponent message={error.message} type="error" />;
if (loading) return <LoadingSpinner />;
return (

View File

@@ -19,7 +19,6 @@ export default connect(
export function ScoreboardTicketsStats({ data, bodyshop }) {
const { t } = useTranslation();
console.log(data);
const columns = [
{
title: t("employees.fields.employee_number"),
@@ -57,6 +56,16 @@ export function ScoreboardTicketsStats({ data, bodyshop }) {
key: "totalOverPeriod",
sorter: (a, b) => a.totalOverPeriod - b.totalOverPeriod,
},
{
title: t("scoreboard.labels.efficiencyoverperiod"),
dataIndex: "efficiencyoverperiod",
key: "efficiencyoverperiod",
render: (text, record) =>
`${(
(record.totalOverPeriod / (record.actualTotalOverPeriod || .1)) *
100
).toFixed(1)} %`,
},
];
const tableData = data
@@ -112,6 +121,7 @@ export function ScoreboardTicketsStats({ data, bodyshop }) {
<Col md={24} lg={20}>
<Table
columns={columns}
rowKey='employee_number'
dataSource={tableData}
id="employee_number"
scroll={{ y: "300px" }}

View File

@@ -43,9 +43,10 @@ export default function ShopEmployeeAddVacation({ employee }) {
});
} else {
notification["success"]({
message: t("employees.successes.added"),
message: t("employees.successes.vacationadded"),
});
}
form.resetFields();
setLoading(false);
setVisibility(false);
};

View File

@@ -1494,7 +1494,7 @@ export default function ShopInfoGeneral({ form }) {
}
const ReceivableCustomFieldSelect = (
<Select>
<Select allowClear>
<Select.Option value="v_vin">VIN</Select.Option>
<Select.Option value="clm_no">Claim No.</Select.Option>
<Select.Option value="ded_amt">Deductible Amount</Select.Option>

View File

@@ -2,9 +2,28 @@ import { Form, Input } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
export default function ShopInfoOrderStatusComponent({ form }) {
const { t } = useTranslation();
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { useTreatments } from "@splitsoftware/splitio-react";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(ShopInfoOrderStatusComponent);
export function ShopInfoOrderStatusComponent({ bodyshop, form }) {
const { t } = useTranslation();
const { OEConnection } = useTreatments(
["OEConnection"],
{},
bodyshop.imexshopid
);
return (
<LayoutFormRow header={t("bodyshop.labels.orderstatuses")}>
<Form.Item
@@ -56,6 +75,20 @@ export default function ShopInfoOrderStatusComponent({ form }) {
>
<Input />
</Form.Item>
{OEConnection.treatment === "on" && (
<Form.Item
label={t("bodyshop.fields.statuses.default_quote")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
name={["md_order_statuses", "default_quote"]}
>
<Input />
</Form.Item>
)}
</LayoutFormRow>
);
}

View File

@@ -146,7 +146,8 @@ const JobRelatedTicketsTable = ({
title: t("employees.labels.name"),
dataIndex: "empname",
key: "empname",
sorter: (a, b) => alphaSort(a.empname, b.empname),
sorter: (a, b) =>
alphaSort(a.item.employee.last_name, b.item.employee.last_name),
sortOrder:
state.sortedInfo.columnKey === "empname" && state.sortedInfo.order,
render: (text, record) =>
@@ -172,7 +173,9 @@ const JobRelatedTicketsTable = ({
title: t("timetickets.fields.efficiency"),
dataIndex: "total",
key: "total",
sorter: (a, b) => a.total - b.total,
sorter: (a, b) =>
(a.actHrs === 0 || !a.actHrs ? 0 : (a.prodHrs / a.actHrs) * 100) -
(b.actHrs === 0 || !b.actHrs ? 0 : (b.prodHrs / b.actHrs) * 100),
sortOrder:
state.sortedInfo.columnKey === "total" && state.sortedInfo.order,
render: (text, record) =>

View File

@@ -2,6 +2,7 @@ import { Button, Col, Descriptions, Popover, Row, Tag } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
export default function VehicleTagPopoverComponent({ job }) {
const { t } = useTranslation();
@@ -27,7 +28,9 @@ export default function VehicleTagPopoverComponent({ job }) {
{`${job.plate_st || t("general.labels.na")}`}
</Descriptions.Item>
<Descriptions.Item key="4" label={t("vehicles.fields.v_vin")}>
{`${job.v_vin || t("general.labels.na")}`}
<VehicleVinDisplay>
{`${job.v_vin || t("general.labels.na")}`}
</VehicleVinDisplay>
</Descriptions.Item>
</Descriptions>
</Col>
@@ -46,7 +49,9 @@ export default function VehicleTagPopoverComponent({ job }) {
{`${job.vehicle.plate_st || t("general.labels.na")}`}
</Descriptions.Item>
<Descriptions.Item key="4" label={t("vehicles.fields.v_vin")}>
{`${job.vehicle.v_vin || t("general.labels.na")}`}
<VehicleVinDisplay>{`${
job.vehicle.v_vin || t("general.labels.na")
}`}</VehicleVinDisplay>
</Descriptions.Item>
</Descriptions>
</Col>

View File

@@ -0,0 +1,18 @@
import React from "react";
export default function VehicleVinDisplay({ children }) {
if (!children) return null;
if (typeof children !== "string" || children.length !== 17) return children;
const vin = children.trim();
const first = vin.substring(0, 9);
const second = vin.substring(9, 17);
return (
<>
<span>{first}</span>
<span style={{ textDecoration: "underline" }}>{second}</span>
</>
);
}

View File

@@ -4,6 +4,7 @@ import queryString from "query-string";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link, useHistory, useLocation } from "react-router-dom";
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
export default function VehiclesListComponent({
loading,
vehicles,
@@ -31,7 +32,7 @@ export default function VehiclesListComponent({
key: "v_vin",
render: (text, record) => (
<Link to={"/manage/vehicles/" + record.id}>
{record.v_vin || "N/A"}
<VehicleVinDisplay>{record.v_vin || "N/A"}</VehicleVinDisplay>
</Link>
),
},

View File

@@ -1,75 +1,77 @@
import { gql } from "@apollo/client";
export const QUERY_ALL_ACTIVE_APPOINTMENTS = gql`
query QUERY_ALL_ACTIVE_APPOINTMENTS(
$start: timestamptz!
$end: timestamptz!
$startd: date!
$endd: date!
query QUERY_ALL_ACTIVE_APPOINTMENTS(
$start: timestamptz!
$end: timestamptz!
$startd: date!
$endd: date!
) {
employee_vacation(
where: { _or: [{ start: { _gte: $startd } },
{ end: { _lte: $endd } },
{_and:[{start:{_lte: $startd}},{end:{_gte:$endd}}]}] }
) {
employee_vacation(
where: { _or: [{ start: { _gte: $startd } }, { end: { _lte: $endd } }] }
) {
id
start
end
employee {
id
start
end
employee {
id
last_name
first_name
last_name
first_name
}
}
appointments(
where: {
canceled: { _eq: false }
end: { _lte: $end }
start: { _gte: $start }
}
) {
start
id
end
arrived
title
isintake
block
color
note
job {
alt_transport
ro_number
ownr_ln
ownr_co_nm
ownr_fn
ownr_ph1
ownr_ph2
ownr_ea
clm_total
id
clm_no
ins_co_nm
v_model_yr
v_make_desc
v_model_desc
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
}
}
}
}
appointments(
where: {
canceled: { _eq: false }
end: { _lte: $end }
start: { _gte: $start }
}
) {
start
id
end
arrived
title
isintake
block
color
note
job {
alt_transport
ro_number
ownr_ln
ownr_co_nm
ownr_fn
ownr_ph1
ownr_ph2
ownr_ea
clm_total
id
clm_no
ins_co_nm
v_model_yr
v_make_desc
v_model_desc
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
}
}
}
}
}
}
`;
@@ -324,6 +326,9 @@ export const QUERY_SCHEDULE_LOAD_DATA = gql`
ro_number
scheduled_completion
actual_completion
ownr_fn
ownr_ln
ownr_co_nm
labhrs: joblines_aggregate(
where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }
) {
@@ -352,6 +357,9 @@ export const QUERY_SCHEDULE_LOAD_DATA = gql`
id
scheduled_in
ro_number
ownr_fn
ownr_ln
ownr_co_nm
labhrs: joblines_aggregate(
where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }
) {

View File

@@ -48,6 +48,8 @@ export const GET_LINE_TICKET_BY_PK = gql`
lbr_op
lbr_amt
op_code_desc
convertedtolbr
convertedtolbr_data
}
timetickets(where: { jobid: { _eq: $id } }) {
actualhrs
@@ -177,6 +179,8 @@ export const UPDATE_JOB_LINE = gql`
location
status
removed
convertedtolbr
convertedtolbr_data
}
}
}

View File

@@ -40,6 +40,8 @@ export const QUERY_ALL_ACTIVE_JOBS = gql`
updated_at
ded_amt
suspended
est_ct_fn
est_ct_ln
}
}
`;
@@ -621,6 +623,7 @@ export const GET_JOB_BY_PK = gql`
ownr_ctry
ownr_ph1
ownr_ph2
tax_number
}
labor_rate_desc
rate_la1
@@ -684,6 +687,7 @@ export const GET_JOB_BY_PK = gql`
line_ref
part_type
oem_partno
alt_partno
db_price
act_price
part_qty
@@ -702,6 +706,7 @@ export const GET_JOB_BY_PK = gql`
prt_dsmk_p
prt_dsmk_m
ioucreated
convertedtolbr
billlines(limit: 1, order_by: { bill: { date: desc } }) {
id
quantity
@@ -854,6 +859,7 @@ export const QUERY_JOB_CARD_DETAILS = gql`
id
allow_text_message
preferred_contact
tax_number
}
vehicleid
v_model_yr
@@ -1905,6 +1911,8 @@ export const QUERY_JOB_CLOSE_DETAILS = gql`
profitcenter_labor
profitcenter_part
prt_dsmk_p
convertedtolbr
convertedtolbr_data
}
}
}

View File

@@ -67,6 +67,7 @@ export const QUERY_OWNER_BY_ID = gql`
ownr_zip
preferred_contact
note
tax_number
jobs {
id
ro_number

View File

@@ -79,6 +79,7 @@ export const QUERY_TIME_TICKETS_IN_RANGE_SB = gql`
date
id
rate
actualhrs
productivehrs
memo
jobid

View File

@@ -24,6 +24,8 @@ import {
selectBodyshop,
selectInstanceConflict,
} from "../../redux/user/user.selectors";
import * as Sentry from "@sentry/react";
import "./manage.page.styles.scss";
const ManageRootPage = lazy(() =>
@@ -407,7 +409,10 @@ export function Manage({ match, conflict, bodyshop }) {
<Content className="content-container">
<PartnerPingComponent />
<ErrorBoundary>{PageContent}</ErrorBoundary>
<Sentry.ErrorBoundary fallback={<ErrorBoundary />} showDialog>
{PageContent}
</Sentry.ErrorBoundary>
<BackTop />
<Footer>
<div

View File

@@ -17,6 +17,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
import { onlyUnique } from "../../utils/arrayHelper";
import { DateTimeFormatter, TimeAgoFormatter } from "../../utils/DateFormatter";
import { alphaSort, dateSort } from "../../utils/sorters";
import useLocalStorage from "../../utils/useLocalStorage";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -31,6 +32,7 @@ export function PartsQueuePageComponent({ bodyshop }) {
statusFilters,
} = searchParams;
const history = useHistory();
const [filter, setFilter] = useLocalStorage("filter_parts_queue", null);
const { loading, error, data, refetch } = useQuery(QUERY_PARTS_QUEUE, {
fetchPolicy: "network-only",
@@ -92,7 +94,7 @@ export function PartsQueuePageComponent({ bodyshop }) {
// searchParams.page = pagination.current;
searchParams.sortcolumn = sorter.columnKey;
searchParams.sortorder = sorter.order;
setFilter(filters);
history.push({ search: queryString.stringify(searchParams) });
};
@@ -247,6 +249,7 @@ export function PartsQueuePageComponent({ bodyshop }) {
key: "queued_for_parts",
sorter: (a, b) => a.queued_for_parts - b.queued_for_parts,
sortOrder: sortcolumn === "queued_for_parts" && sortorder,
filteredValue: filter?.queued_for_parts || null,
filters: [
{
text: "Queued",

View File

@@ -1,10 +1,10 @@
import axios from "axios";
import phone from "phone";
import parsePhoneNumber from "libphonenumber-js";
import { all, call, put, select, takeLatest } from "redux-saga/effects";
import { logImEXEvent } from "../../firebase/firebase.utils";
import {
CONVERSATION_ID_BY_PHONE,
CREATE_CONVERSATION,
CREATE_CONVERSATION
} from "../../graphql/conversations.queries";
import { INSERT_CONVERSATION_TAG } from "../../graphql/job-conversations.queries";
import client from "../../utils/GraphQLClient";
@@ -12,7 +12,7 @@ import { selectBodyshop } from "../user/user.selectors";
import {
sendMessageFailure,
sendMessageSuccess,
setSelectedConversation,
setSelectedConversation
} from "./messaging.actions";
import MessagingActionTypes from "./messaging.types";
@@ -33,13 +33,14 @@ export function* openChatByPhone({ payload }) {
logImEXEvent("messaging_open_by_phone");
const { phone_num, jobid } = payload;
const p = parsePhoneNumber(phone_num, "CA");
const bodyshop = yield select(selectBodyshop);
try {
const {
data: { conversations },
} = yield client.query({
query: CONVERSATION_ID_BY_PHONE,
variables: { phone: phone(phone_num).phoneNumber },
variables: { phone: p.number },
});
if (conversations.length === 0) {
@@ -52,7 +53,7 @@ export function* openChatByPhone({ payload }) {
variables: {
conversation: [
{
phone_num: phone(phone_num).phoneNumber,
phone_num: p.number,
bodyshopid: bodyshop.id,
job_conversations: jobid ? { data: { jobid: jobid } } : null,
},

View File

@@ -103,7 +103,7 @@
"jobimported": "Job imported.",
"jobinproductionchange": "Job production status set to {{inproduction}}",
"jobioucreated": "IOU Created.",
"jobmodifylbradj": "Labor adjustments modified.",
"jobmodifylbradj": "Labor adjustments modified {{mod_lbr_ty}} / {{hours}}.",
"jobnoteadded": "Note added to job.",
"jobnotedeleted": "Note deleted from job.",
"jobnoteupdated": "Note updated on job.",
@@ -136,6 +136,9 @@
"other": "-- Not On Estimate --",
"reconciled": "Reconciled!",
"unreconciled": "Unreconciled"
},
"validation": {
"atleastone": "At least one bill line must be entered."
}
},
"bills": {
@@ -502,6 +505,7 @@
"default_imported": "Default Imported Status",
"default_invoiced": "Default Invoiced Status",
"default_ordered": "Default Ordered Status",
"default_quote": "Default Quote Status",
"default_received": "Default Received Status",
"default_returned": "Default Returned",
"default_scheduled": "Default Scheduled Status",
@@ -674,6 +678,7 @@
"refuelqty": "Refuel qty.?"
},
"correctdataonform": "Please review the information above. If any of it is not correct, you can fix it later.",
"dateinpast": "Date is in the past.",
"dlexpirebeforereturn": "The driver's license expires before the car is expected to return. ",
"driverinformation": "Driver's Information",
"findcontract": "Find Contract",
@@ -748,6 +753,7 @@
"status": {
"in": "Available",
"inservice": "In Service",
"leasereturn": "Lease Returned",
"out": "Rented",
"sold": "Sold"
},
@@ -916,7 +922,8 @@
},
"successes": {
"delete": "Employee deleted successfully.",
"save": "Employee saved successfully."
"save": "Employee saved successfully.",
"vacationadded": "Employee vacation added."
},
"validation": {
"unique_employee_number": "You must enter a unique employee number."
@@ -961,7 +968,8 @@
},
"errors": {
"fcm": "You must allow notification permissions to have real time messaging. Click to try again.",
"notfound": "No record was found."
"notfound": "No record was found.",
"sizelimit": "The selected items exceed the size limit."
},
"itemtypes": {
"contract": "CC Contract",
@@ -1109,6 +1117,7 @@
},
"joblines": {
"actions": {
"converttolabor": "Convert amount to Labor.",
"new": "New Line"
},
"errors": {
@@ -1174,7 +1183,9 @@
"unq_seq": "Seq #"
},
"labels": {
"adjustmenttobeadded": "Adjustment to be added: {{adjustment}}",
"billref": "Latest Bill",
"convertedtolabor": "This line has been converted to labor. Ensure you adjust the profit center for the amount accordingly.",
"edit": "Edit Line",
"ioucreated": "IOU",
"new": "New Line",
@@ -1562,6 +1573,7 @@
"closeconfirm": "Are you sure you want to close this job? This cannot be easily undone.",
"closejob": "Close Job {{ro_number}}",
"contracts": "CC Contracts",
"convertedtolabor": "Lines Converted to Labor",
"cost": "Cost",
"cost_Additional": "Cost - Additional",
"cost_labor": "Cost - Labor",
@@ -1604,6 +1616,7 @@
"duplicateconfirm": "Are you sure you want to duplicate this job? Some elements of this job will not be duplicated.",
"employeeassignments": "Employee Assignments",
"estimatelines": "Estimate Lines",
"estimator": "Estimator",
"existing_jobs": "Existing Jobs",
"federal_tax_amt": "Federal Taxes",
"gpdollars": "$ G.P.",
@@ -1907,7 +1920,9 @@
"labels": {
"addtorelatedro": "Add to Related ROs",
"newnoteplaceholder": "Add a note...",
"notetoadd": "Note to Add"
"notetoadd": "Note to Add",
"systemnotes": "System Notes",
"usernotes": "User Notes"
},
"successes": {
"create": "Note created successfully.",
@@ -1926,6 +1941,7 @@
},
"errors": {
"noaccess": "The record does not exist or you do not have access to it. ",
"saving": "Error saving owner. {{error}}.",
"selectexistingornew": "Select an existing owner record or create a new one. "
},
"fields": {
@@ -1946,7 +1962,8 @@
"ownr_st": "Province/State",
"ownr_title": "Title",
"ownr_zip": "Zip/Postal Code",
"preferred_contact": "Preferred Contact Method"
"preferred_contact": "Preferred Contact Method",
"tax_number": "Tax Number"
},
"forms": {
"address": "Address",
@@ -2012,6 +2029,7 @@
"confirmdelete": "Are you sure you want to delete this item? It cannot be recovered. Job line statuses will not be updated and may require manual review. ",
"email": "Send by Email",
"inthisorder": "Parts in this Order",
"is_quote": "Parts Quote?",
"mark_as_received": "Mark as Received?",
"newpartsorder": "New Parts Order",
"notyetordered": "This part has not yet been ordered.",
@@ -2379,6 +2397,7 @@
"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",
"jobs_reconcile": "Parts/Sublet/Labor Reconciliation",
"lag_time": "Lag Time",
"open_orders": "Open Orders by Date",
"open_orders_csr": "Open Orders by CSR",
@@ -2445,6 +2464,7 @@
"calendarperiod": "Periods based on calendar weeks/months.",
"dailyactual": "Actual (D)",
"dailytarget": "Daily",
"efficiencyoverperiod": "Efficiency over Selected Dates",
"jobs": "Jobs",
"lastmonth": "Last Month",
"lastweek": "Last Week",
@@ -2668,6 +2688,7 @@
"users": {
"errors": {
"signinerror": {
"auth/user-disabled": "User account disabled. ",
"auth/user-not-found": "A user with this email does not exist.",
"auth/wrong-password": "The email and password combination you provided is incorrect."
}

View File

@@ -136,6 +136,9 @@
"other": "",
"reconciled": "",
"unreconciled": ""
},
"validation": {
"atleastone": ""
}
},
"bills": {
@@ -502,6 +505,7 @@
"default_imported": "",
"default_invoiced": "",
"default_ordered": "",
"default_quote": "",
"default_received": "",
"default_returned": "",
"default_scheduled": "",
@@ -674,6 +678,7 @@
"refuelqty": ""
},
"correctdataonform": "",
"dateinpast": "",
"dlexpirebeforereturn": "",
"driverinformation": "",
"findcontract": "",
@@ -748,6 +753,7 @@
"status": {
"in": "",
"inservice": "",
"leasereturn": "",
"out": "",
"sold": ""
},
@@ -916,7 +922,8 @@
},
"successes": {
"delete": "Empleado eliminado con éxito.",
"save": "Empleado guardado con éxito."
"save": "Empleado guardado con éxito.",
"vacationadded": ""
},
"validation": {
"unique_employee_number": ""
@@ -961,7 +968,8 @@
},
"errors": {
"fcm": "",
"notfound": ""
"notfound": "",
"sizelimit": ""
},
"itemtypes": {
"contract": "",
@@ -1109,6 +1117,7 @@
},
"joblines": {
"actions": {
"converttolabor": "",
"new": ""
},
"errors": {
@@ -1174,7 +1183,9 @@
"unq_seq": "Seq #"
},
"labels": {
"adjustmenttobeadded": "",
"billref": "",
"convertedtolabor": "",
"edit": "Línea de edición",
"ioucreated": "",
"new": "Nueva línea",
@@ -1562,6 +1573,7 @@
"closeconfirm": "",
"closejob": "",
"contracts": "",
"convertedtolabor": "",
"cost": "",
"cost_Additional": "",
"cost_labor": "",
@@ -1604,6 +1616,7 @@
"duplicateconfirm": "",
"employeeassignments": "",
"estimatelines": "",
"estimator": "",
"existing_jobs": "Empleos existentes",
"federal_tax_amt": "",
"gpdollars": "",
@@ -1907,7 +1920,9 @@
"labels": {
"addtorelatedro": "",
"newnoteplaceholder": "Agrega una nota...",
"notetoadd": ""
"notetoadd": "",
"systemnotes": "",
"usernotes": ""
},
"successes": {
"create": "Nota creada con éxito.",
@@ -1926,6 +1941,7 @@
},
"errors": {
"noaccess": "El registro no existe o no tiene acceso a él.",
"saving": "",
"selectexistingornew": ""
},
"fields": {
@@ -1946,7 +1962,8 @@
"ownr_st": "Provincia del estado",
"ownr_title": "Título",
"ownr_zip": "código postal",
"preferred_contact": "Método de Contacto Preferido"
"preferred_contact": "Método de Contacto Preferido",
"tax_number": ""
},
"forms": {
"address": "",
@@ -2012,6 +2029,7 @@
"confirmdelete": "",
"email": "Enviar por correo electrónico",
"inthisorder": "Partes en este pedido",
"is_quote": "",
"mark_as_received": "",
"newpartsorder": "",
"notyetordered": "",
@@ -2379,6 +2397,7 @@
"job_costing_ro_date_summary": "",
"job_costing_ro_estimator": "",
"job_costing_ro_ins_co": "",
"jobs_reconcile": "",
"lag_time": "",
"open_orders": "",
"open_orders_csr": "",
@@ -2445,6 +2464,7 @@
"calendarperiod": "",
"dailyactual": "",
"dailytarget": "",
"efficiencyoverperiod": "",
"jobs": "",
"lastmonth": "",
"lastweek": "",
@@ -2668,6 +2688,7 @@
"users": {
"errors": {
"signinerror": {
"auth/user-disabled": "",
"auth/user-not-found": "",
"auth/wrong-password": ""
}

View File

@@ -136,6 +136,9 @@
"other": "",
"reconciled": "",
"unreconciled": ""
},
"validation": {
"atleastone": ""
}
},
"bills": {
@@ -502,6 +505,7 @@
"default_imported": "",
"default_invoiced": "",
"default_ordered": "",
"default_quote": "",
"default_received": "",
"default_returned": "",
"default_scheduled": "",
@@ -674,6 +678,7 @@
"refuelqty": ""
},
"correctdataonform": "",
"dateinpast": "",
"dlexpirebeforereturn": "",
"driverinformation": "",
"findcontract": "",
@@ -748,6 +753,7 @@
"status": {
"in": "",
"inservice": "",
"leasereturn": "",
"out": "",
"sold": ""
},
@@ -916,7 +922,8 @@
},
"successes": {
"delete": "L'employé a bien été supprimé.",
"save": "L'employé a enregistré avec succès."
"save": "L'employé a enregistré avec succès.",
"vacationadded": ""
},
"validation": {
"unique_employee_number": ""
@@ -961,7 +968,8 @@
},
"errors": {
"fcm": "",
"notfound": ""
"notfound": "",
"sizelimit": ""
},
"itemtypes": {
"contract": "",
@@ -1109,6 +1117,7 @@
},
"joblines": {
"actions": {
"converttolabor": "",
"new": ""
},
"errors": {
@@ -1174,7 +1183,9 @@
"unq_seq": "Seq #"
},
"labels": {
"adjustmenttobeadded": "",
"billref": "",
"convertedtolabor": "",
"edit": "Ligne d'édition",
"ioucreated": "",
"new": "Nouvelle ligne",
@@ -1562,6 +1573,7 @@
"closeconfirm": "",
"closejob": "",
"contracts": "",
"convertedtolabor": "",
"cost": "",
"cost_Additional": "",
"cost_labor": "",
@@ -1604,6 +1616,7 @@
"duplicateconfirm": "",
"employeeassignments": "",
"estimatelines": "",
"estimator": "",
"existing_jobs": "Emplois existants",
"federal_tax_amt": "",
"gpdollars": "",
@@ -1907,7 +1920,9 @@
"labels": {
"addtorelatedro": "",
"newnoteplaceholder": "Ajouter une note...",
"notetoadd": ""
"notetoadd": "",
"systemnotes": "",
"usernotes": ""
},
"successes": {
"create": "Remarque créée avec succès.",
@@ -1926,6 +1941,7 @@
},
"errors": {
"noaccess": "L'enregistrement n'existe pas ou vous n'y avez pas accès.",
"saving": "",
"selectexistingornew": ""
},
"fields": {
@@ -1946,7 +1962,8 @@
"ownr_st": "Etat / Province",
"ownr_title": "Titre",
"ownr_zip": "Zip / code postal",
"preferred_contact": "Méthode de contact préférée"
"preferred_contact": "Méthode de contact préférée",
"tax_number": ""
},
"forms": {
"address": "",
@@ -2012,6 +2029,7 @@
"confirmdelete": "",
"email": "Envoyé par email",
"inthisorder": "Pièces dans cette commande",
"is_quote": "",
"mark_as_received": "",
"newpartsorder": "",
"notyetordered": "",
@@ -2379,6 +2397,7 @@
"job_costing_ro_date_summary": "",
"job_costing_ro_estimator": "",
"job_costing_ro_ins_co": "",
"jobs_reconcile": "",
"lag_time": "",
"open_orders": "",
"open_orders_csr": "",
@@ -2445,6 +2464,7 @@
"calendarperiod": "",
"dailyactual": "",
"dailytarget": "",
"efficiencyoverperiod": "",
"jobs": "",
"lastmonth": "",
"lastweek": "",
@@ -2668,6 +2688,7 @@
"users": {
"errors": {
"signinerror": {
"auth/user-disabled": "",
"auth/user-not-found": "",
"auth/wrong-password": ""
}

View File

@@ -18,7 +18,8 @@ const AuditTrailMapping = {
i18n.t("audit_trail.messages.jobspartsorder", { order_number }),
jobspartsreturn: (order_number) =>
i18n.t("audit_trail.messages.jobspartsreturn", { order_number }),
jobmodifylbradj: () => i18n.t("audit_trail.messages.jobmodifylbradj", {}),
jobmodifylbradj: ({ mod_lbr_ty, hours }) =>
i18n.t("audit_trail.messages.jobmodifylbradj", { mod_lbr_ty, hours }),
billposted: (invoice_number) =>
i18n.t("audit_trail.messages.billposted", { invoice_number }),
billupdated: (invoice_number) =>

View File

@@ -1,7 +1,7 @@
import { gql } from "@apollo/client";
import { notification } from "antd";
import axios from "axios";
import jsreport from "jsreport-browser-client-dist";
import jsreport from "@jsreport/browser-client";
import _ from "lodash";
import moment from "moment";
import { auth } from "../firebase/firebase.utils";
@@ -9,7 +9,9 @@ import { setEmailOptions } from "../redux/email/email.actions";
import { store } from "../redux/store";
import client from "../utils/GraphQLClient";
import { TemplateList } from "./TemplateConstants";
const server = process.env.REACT_APP_REPORTS_SERVER_URL;
jsreport.serverUrl = server;
const Templates = TemplateList();
@@ -21,6 +23,10 @@ export default async function RenderTemplate(
renderAsExcel = false,
renderAsText = false
) {
if (window.jsr3) {
jsreport.serverUrl = "https://reports3.test.imex.online/";
}
//Query assets that match the template name. Must be in format <<templateName>>.query
let { contextData, useShopSpecificTemplate } = await fetchContextData(
templateObject
@@ -69,7 +75,7 @@ export default async function RenderTemplate(
};
try {
const render = await jsreport.renderAsync(reportRequest);
const render = await jsreport.render(reportRequest);
if (!renderAsHtml) {
render.download(
@@ -103,17 +109,18 @@ export default async function RenderTemplate(
}),
},
};
console.log("PDFREQ", pdfRequest);
const pdfRender = await jsreport.renderAsync(pdfRequest);
pdf = pdfRender.toDataURI();
const pdfRender = await jsreport.render(pdfRequest);
pdf = await pdfRender.toDataURI();
}
const html = await render.toString();
return new Promise((resolve, reject) => {
resolve({
pdf,
filename:
Templates[templateObject.name] &&
Templates[templateObject.name].title,
html: render.toString(),
html,
});
});
}
@@ -152,6 +159,9 @@ export async function RenderTemplates(
// (template) => template.name === item.templateObject.name
// );
// });
if (window.jsr3) {
jsreport.serverUrl = "https://reports3.test.imex.online/";
}
unsortedTemplatesAndData.sort(function (a, b) {
return (
@@ -242,13 +252,11 @@ export async function RenderTemplates(
};
try {
const render = await jsreport.renderAsync(reportRequest);
const render = await jsreport.render(reportRequest);
if (!renderAsHtml) {
render.download("Speed Print");
} else {
return new Promise((resolve, reject) => {
resolve(render.toString());
});
return render.toString();
}
} catch (error) {
notification["error"]({ message: JSON.stringify(error) });

View File

@@ -1534,6 +1534,18 @@ export const TemplateList = (type, context) => {
},
group: "purchases",
},
jobs_reconcile: {
title: i18n.t("reportcenter.templates.jobs_reconcile"),
subject: i18n.t("reportcenter.templates.jobs_reconcile"),
key: "jobs_reconcile",
//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

@@ -23,9 +23,9 @@ export default async function FcmHandler({ client, payload }) {
updated_at(oldupdated0) {
return new Date();
},
messages_aggregate(cached) {
return { aggregate: { count: cached.aggregate.count + 1 } };
},
// messages_aggregate(cached) {
// return { aggregate: { count: cached.aggregate.count + 1 } };
// },
},
});
break;

View File

@@ -0,0 +1,40 @@
import { useState } from "react";
export default function useLocalStorage(key, initialValue) {
// State to store our value
// Pass initial state function to useState so logic is only executed once
const [storedValue, setStoredValue] = useState(() => {
if (typeof window === "undefined") {
return initialValue;
}
try {
// Get from local storage by key
const item = window.localStorage.getItem(key);
// Parse stored json or if none return initialValue
return item ? JSON.parse(item) : initialValue;
} catch (error) {
// If error also return initialValue
console.log(error);
return initialValue;
}
});
// Return a wrapped version of useState's setter function that ...
// ... persists the new value to localStorage.
const setValue = (value) => {
try {
// Allow value to be a function so we have same API as useState
const valueToStore =
value instanceof Function ? value(storedValue) : value;
// Save state
setStoredValue(valueToStore);
// Save to local storage
if (typeof window !== "undefined") {
window.localStorage.setItem(key, JSON.stringify(valueToStore));
}
} catch (error) {
// A more advanced implementation would handle the error case
console.log(error);
}
};
return [storedValue, setValue];
}

View File

@@ -1918,6 +1918,11 @@
"@types/yargs" "^16.0.0"
chalk "^4.0.0"
"@jsreport/browser-client@^3.1.0":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@jsreport/browser-client/-/browser-client-3.1.0.tgz#a84011087ca8a29a6dc6a852fa05ffaf1983a679"
integrity sha512-ZElwn2KRIzkUzAyD5UKGxULZUhokWuPOlMzrmiur4WirqH3yoiHlOJEdnRGkjjE/fhZzCR8gBFZ/TuOW/fsOIw==
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"

View File

@@ -1,3 +1,3 @@
Must set the environment variables using:
firebase functions:config:set auth.graphql_endpoint="https://bodyshop-dev-db.herokuapp.com/v1/graphql" auth.hasura_secret_admin_key="Dev-BodyShopApp!"
firebase functions:config:set auth.graphql_endpoint="https://db.development.bodyshop.app/v1/graphql" auth.hasura_secret_admin_key="Dev-BodyShopApp!"

View File

@@ -1,5 +1,5 @@
version: 2
endpoint: https://bodyshop-dev-db.herokuapp.com
endpoint: https://db.development.bodyshop.app
admin_secret: Dev-BodyShopApp!
metadata_directory: metadata
actions:

View File

@@ -2384,6 +2384,8 @@
- bett_tax
- bett_type
- cert_part
- convertedtolbr
- convertedtolbr_data
- created_at
- db_hrs
- db_price
@@ -2447,6 +2449,8 @@
- bett_tax
- bett_type
- cert_part
- convertedtolbr
- convertedtolbr_data
- created_at
- db_hrs
- db_price
@@ -2521,6 +2525,8 @@
- bett_tax
- bett_type
- cert_part
- convertedtolbr
- convertedtolbr_data
- created_at
- db_hrs
- db_price
@@ -4009,6 +4015,7 @@
- ownr_zip
- preferred_contact
- shopid
- tax_number
- updated_at
filter:
bodyshop:
@@ -4044,6 +4051,7 @@
- ownr_zip
- preferred_contact
- shopid
- tax_number
- updated_at
filter:
bodyshop:

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"."joblines" add column "convertedtolbr" boolean
-- not null default 'false';

View File

@@ -0,0 +1,2 @@
alter table "public"."joblines" add column "convertedtolbr" boolean
not null default 'false';

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"."joblines" add column "convertedtolbr_data" jsonb
-- not null default jsonb_build_object();

View File

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

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"."owners" add column "tax_number" text
-- null;

View File

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

12049
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -157,7 +157,21 @@ app.post(
fb.unsubscribe
);
app.post("/adm/updateuser", fb.validateFirebaseIdToken, fb.updateUser);
app.post("/adm/getuser", fb.validateFirebaseIdToken, fb.getUser);
app.post("/adm/createuser", fb.validateFirebaseIdToken, fb.createUser);
const adm = require("./server/admin/adminops");
app.post(
"/adm/createassociation",
fb.validateFirebaseIdToken,
fb.validateAdmin,
adm.createAssociation
);
app.post(
"/adm/createshop",
fb.validateFirebaseIdToken,
fb.validateAdmin,
adm.createShop
);
//Stripe Processing
var stripe = require("./server/stripe/payment");
@@ -216,7 +230,7 @@ server.listen(port, (error) => {
if (error) throw error;
logger.log(
`[${process.env.NODE_ENV || "DEVELOPMENT"}] Server running on port ${port}`,
"DEBUG",
"INFO",
"api"
);
});

View File

@@ -179,7 +179,6 @@ exports.default = async (req, res) => {
ret.push({ paymentid: payment.id, success: true });
} catch (error) {
logger.log("qbo-payment-create-error", "ERROR", req.user.email, {
error:
(error && error.authResponse && error.authResponse.body) ||
(error && error.message),
@@ -217,7 +216,10 @@ exports.default = async (req, res) => {
res.status(200).json(ret);
} catch (error) {
console.log(error);
logger.log("qbo-payment-create-error", "ERROR", req.user.email, { error: error.message, stack: error.stack });
logger.log("qbo-payment-create-error", "ERROR", req.user.email, {
error: error.message,
stack: error.stack,
});
res.status(400).json(error);
}
};
@@ -240,7 +242,7 @@ async function InsertPayment(
if (invoices && invoices.length !== 1) {
throw new Error(
`More than 1 invoice with DocNumber ${payment.ro_number} found.`
`More than 1 invoice with DocNumber ${payment.job.ro_number} found.`
);
}

68
server/admin/adminops.js Normal file
View File

@@ -0,0 +1,68 @@
const path = require("path");
const _ = require("lodash");
const logger = require("../utils/logger");
require("dotenv").config({
path: path.resolve(
process.cwd(),
`.env.${process.env.NODE_ENV || "development"}`
),
});
const client = require("../graphql-client/graphql-client").client;
exports.createAssociation = async (req, res) => {
logger.log("admin-create-association", "ADMIN", req.user.email, null, {
request: req.body,
ioadmin: true,
});
const { shopid, authlevel, useremail } = req.body;
const result = await client.request(
`mutation INSERT_ASSOCIATION($assoc: associations_insert_input!){
insert_associations_one(object:$assoc){
id
authlevel
useremail
active
}
}`,
{
assoc: { shopid, authlevel, useremail, active: false },
}
);
res.json(result);
};
exports.createShop = async (req, res) => {
logger.log("admin-create-shop", "ADMIN", req.user.email, null, {
request: req.body,
ioadmin: true,
});
const { bodyshop, ronum } = req.body;
try {
const result = await client.request(
`mutation INSERT_BODYSHOPS($bs: bodyshops_insert_input!){
insert_bodyshops_one(object:$bs){
id
}
}`,
{
bs: {
...bodyshop,
counters: {
data: [
{ countertype: "ronum", count: ronum },
{ countertype: "ihbnum", count: 1 },
{ countertype: "paymentnum", count: 1 },
],
},
},
}
);
res.json(result);
} catch (error) {
res.status(500).json(error);
}
};

View File

@@ -39,7 +39,7 @@ exports.default = async (req, res) => {
const { bodyshops } = await client.request(queries.GET_AUTOHOUSE_SHOPS);
const specificShopIds = req.body.bodyshopIds; // ['uuid]
const { start, end } = req.body; //YYYY-MM-DD
const { start, end, skipUpload } = req.body; //YYYY-MM-DD
const allxmlsToUpload = [];
const allErrors = [];
try {
@@ -107,12 +107,13 @@ exports.default = async (req, res) => {
} catch (error) {
//Error at the shop level.
logger.log("autohouse-error-shop", "ERROR", "api", bodyshop.id, {
error,
...error,
});
allErrors.push({
bodyshopid: bodyshop.id,
imexshopid: bodyshop.imexshopid,
autuhouseid: bodyshop.autuhouseid,
fatal: true,
errors: [error.toString()],
});
@@ -120,22 +121,39 @@ exports.default = async (req, res) => {
allErrors.push({
bodyshopid: bodyshop.id,
imexshopid: bodyshop.imexshopid,
errors: erroredJobs,
autohouseid: bodyshop.autohouseid,
errors: erroredJobs.map((ej) => ({
ro_number: ej.job?.ro_number,
jobid: ej.job?.id,
error: ej.error,
})),
});
}
}
// for (const xmlObj of allxmlsToUpload) {
// fs.writeFileSync(`./logs/${xmlObj.filename}`, xmlObj.xml);
// }
if (skipUpload) {
for (const xmlObj of allxmlsToUpload) {
fs.writeFileSync(`./logs/${xmlObj.filename}`, xmlObj.xml);
}
// res.json(allxmlsToUpload);
// return;
res.json(allxmlsToUpload);
sendServerEmail({
subject: `Autohouse 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
)}
`,
});
return;
}
let sftp = new Client();
sftp.on("error", (errors) =>
logger.log("autohouse-sftp-error", "ERROR", "api", null, {
errors,
...errors,
})
);
try {
@@ -160,7 +178,7 @@ exports.default = async (req, res) => {
//***TODO Change filing naming when creating the cron job. IM_ShopInternalName_DDMMYYYY_HHMMSS.xml
} catch (error) {
logger.log("autohouse-sftp-error", "ERROR", "api", null, {
error,
...error,
});
} finally {
sftp.end();
@@ -169,7 +187,7 @@ exports.default = async (req, res) => {
subject: `Autohouse Report ${moment().format("MM-DD-YY")}`,
text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))}
Uploaded: ${JSON.stringify(
allxmlsToUpload.map((x) => x.filename),
allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count })),
null,
2
)}
@@ -185,7 +203,12 @@ const CreateRepairOrderTag = (job, errorCallback) => {
//Level 2
if (!job.job_totals) {
errorCallback({ job, error: { toString: () => "No job totals for RO." } });
errorCallback({
jobid: job.id,
job: job,
ro_number: job.ro_number,
error: { toString: () => "No job totals for RO." },
});
return {};
}
@@ -658,13 +681,14 @@ const CreateRepairOrderTag = (job, errorCallback) => {
error,
});
errorCallback({ job, error });
errorCallback({ jobid: job.id, ro_number: job.ro_number, error });
}
};
const CreateCosts = (job) => {
//Create a mapping based on AH Requirements
//For DMS, the keys in the object below are the CIECA part types.
const billTotalsByCostCenters = job.bills.reduce((bill_acc, bill_val) => {
//At the bill level.
bill_val.billlines.map((line_val) => {
@@ -731,7 +755,7 @@ const CreateCosts = (job) => {
}).multiply(job.job_totals.rates.mash.hours)
);
}
//Uses CIECA Labor types.
const ticketTotalsByCostCenter = job.timetickets.reduce(
(ticket_acc, ticket_val) => {
//At the invoice level.
@@ -750,7 +774,43 @@ const CreateCosts = (job) => {
},
{}
);
const defaultCosts = job.bodyshop.md_responsibility_centers.defaults.costs;
//CIECA STANDARD MAPPING OBJECT.
const ciecaObj = {
ATS: "ATS",
LA1: "LA1",
LA2: "LA2",
LA3: "LA3",
LA4: "LA4",
LAA: "LAA",
LAB: "LAB",
LAD: "LAD",
LAE: "LAE",
LAF: "LAF",
LAG: "LAG",
LAM: "LAM",
LAR: "LAR",
LAS: "LAS",
LAU: "LAU",
PAA: "PAA",
PAC: "PAC",
PAG: "PAG",
PAL: "PAL",
PAM: "PAM",
PAN: "PAN",
PAO: "PAO",
PAP: "PAP",
PAR: "PAR",
PAS: "PAS",
TOW: "TOW",
MAPA: "MAPA",
MASH: "MASH",
PASL: "PASL",
};
const defaultCosts =
job.bodyshop.cdk_dealerid || job.bodyshop.pbs_serialnumber
? ciecaObj
: job.bodyshop.md_responsibility_centers.defaults.costs;
return {
PartsTotalCost: Object.keys(billTotalsByCostCenters).reduce((acc, key) => {
@@ -852,7 +912,9 @@ const GenerateDetailLines = (job, line, statuses) => {
OriginalCost: null,
OriginalInvoiceNumber: null,
PriceEach: line.act_price || 0,
PartNumber: _.escape(line.oem_partno),
PartNumber: line.oem_partno
? line.oem_partno.replace(/[^\x00-\x7F]/g, "")
: "",
ProfitPercent: null,
PurchaseOrderNumber: null,
Qty: line.part_qty || 0,

View File

@@ -10,7 +10,8 @@ let nodemailer = require("nodemailer");
let aws = require("aws-sdk");
const logger = require("../utils/logger");
const ses = new aws.SES({
apiVersion: "2010-12-01",
apiVersion: "latest",
region: "ca-central-1",
});
@@ -43,6 +44,7 @@ exports.sendServerEmail = async function ({ subject, text }) {
} catch (error) {
console.log(error);
logger.log("server-email-failure", "error", null, null, error);
res.status(500).json(error);
}
};
exports.sendTaskEmail = async function ({ to, subject, text, attachments }) {
@@ -153,7 +155,7 @@ exports.sendEmail = async (req, res) => {
error: err,
});
res.json({ success: false, error: err });
res.status(500).json({ success: false, error: err });
}
}
);

View File

@@ -1,13 +1,14 @@
var admin = require("firebase-admin");
const logger = require("../utils/logger");
const path = require("path");
const { auth } = require("firebase-admin");
require("dotenv").config({
path: path.resolve(
process.cwd(),
`.env.${process.env.NODE_ENV || "development"}`
),
});
const client = require("../graphql-client/graphql-client").client;
var serviceAccount = require(process.env.FIREBASE_ADMINSDK_JSON);
admin.initializeApp({
@@ -19,54 +20,61 @@ exports.admin = admin;
const adminEmail = [
"patrick@imex.dev",
"patrick@imex.text",
//"patrick@imex.test",
"patrick@imex.prod",
"patrick@imexsystems.ca",
"patrick@thinkimex.com",
];
exports.createUser = (req, res) => {
logger.log("admin-create-user", "WARN", req.user.email, null, {
exports.createUser = async (req, res) => {
logger.log("admin-create-user", "ADMIN", req.user.email, null, {
request: req.body,
ioadmin: true,
});
if (!adminEmail.includes(req.user.email)) {
logger.log(
"admin-create-user-unauthorized",
"ERROR",
req.user.email,
null,
const { email, displayName, password, shopid, authlevel } = req.body;
try {
const userRecord = await admin
.auth()
.createUser({ email, displayName, password });
// See the UserRecord reference doc for the contents of userRecord.
const result = await client.request(
`
mutation INSERT_USER($user: users_insert_input!) {
insert_users_one(object: $user) {
email
}
}
`,
{
request: req.body,
user: req.user,
user: {
email,
authid: userRecord.uid,
associations: {
data: [{ shopid, authlevel, active: true }],
},
},
}
);
res.sendStatus(404);
}
const { email, displayName, password } = req.body;
admin
.auth()
.createUser({ email, displayName, password })
.then((userRecord) => {
// See the UserRecord reference doc for the contents of userRecord.
logger.log("admin-update-user-success", "DEBUG", req.user.email, null, {
userRecord,
});
res.json(userRecord);
})
.catch((error) => {
logger.log("admin-update-user-error", "ERROR", req.user.email, null, {
error,
});
res.status(500).json(error);
res.json({ userRecord, result });
} catch (error) {
logger.log("admin-update-user-error", "ERROR", req.user.email, null, {
error,
});
res.status(500).json(error);
}
};
exports.updateUser = (req, res) => {
logger.log("admin-update-user", "WARN", req.user.email, null, {
logger.log("admin-update-user", "ADMIN", req.user.email, null, {
request: req.body,
ioadmin: true,
});
if (!adminEmail.includes(req.user.email)) {
if (!adminEmail.includes(req.user.email) && !req.user.ioadmin) {
logger.log(
"admin-update-user-unauthorized",
"ERROR",
@@ -78,6 +86,7 @@ exports.updateUser = (req, res) => {
}
);
res.sendStatus(404);
return;
}
admin
@@ -98,8 +107,9 @@ exports.updateUser = (req, res) => {
.then((userRecord) => {
// See the UserRecord reference doc for the contents of userRecord.
logger.log("admin-update-user-success", "DEBUG", req.user.email, null, {
logger.log("admin-update-user-success", "ADMIN", req.user.email, null, {
userRecord,
ioadmin: true,
});
res.json(userRecord);
})
@@ -111,6 +121,41 @@ exports.updateUser = (req, res) => {
});
};
exports.getUser = (req, res) => {
logger.log("admin-get-user", "ADMIN", req.user.email, null, {
request: req.body,
ioadmin: true,
});
if (!adminEmail.includes(req.user.email) && !req.user.ioadmin) {
logger.log(
"admin-update-user-unauthorized",
"ERROR",
req.user.email,
null,
{
request: req.body,
user: req.user,
}
);
res.sendStatus(404);
return;
}
admin
.auth()
.getUser(req.body.uid)
.then((userRecord) => {
res.json(userRecord);
})
.catch((error) => {
logger.log("admin-get-user-error", "ERROR", req.user.email, null, {
error,
});
res.status(500).json(error);
});
};
exports.sendNotification = async (req, res) => {
setTimeout(() => {
// Send a message to the device corresponding to the provided
@@ -212,12 +257,46 @@ exports.validateFirebaseIdToken = async (req, res, next) => {
return;
} catch (error) {
logger.log("api-unauthorized-call", "WARN", null, null, {
req,
path: req.path,
body: req.body,
type: "unauthroized",
error,
...error,
});
res.status(403).send("Unauthorized");
res.status(401).send("Unauthorized");
return;
}
};
exports.validateAdmin = async (req, res, next) => {
if (!adminEmail.includes(req.user.email) && !req.user.ioadmin) {
logger.log("admin-validation-failed", "ERROR", req.user.email, null, {
request: req.body,
user: req.user,
});
res.sendStatus(404);
return;
} else {
next();
return;
}
};
//Admin claims code.
// const uid = "JEqqYlsadwPEXIiyRBR55fflfko1";
// admin
// .auth()
// .getUser(uid)
// .then((user) => {
// console.log(user);
// admin.auth().setCustomUserClaims(uid, {
// ioadmin: true,
// "https://hasura.io/jwt/claims": {
// "x-hasura-default-role": "admin",
// "x-hasura-allowed-roles": ["admin"],
// "x-hasura-user-id": uid,
// },
// });
// });

View File

@@ -611,6 +611,8 @@ exports.AUTOHOUSE_QUERY = `query AUTOHOUSE_EXPORT($start: timestamptz, $bodyshop
autohouseid
md_responsibility_centers
jc_hourly_rates
cdk_dealerid
pbs_serialnumber
timezone
}
jobs(where: {_and: [{converted: {_eq: true}}, {updated_at: {_gt: $start}}, {updated_at: {_lte: $end}}, {shopid: {_eq: $bodyshopid}}]}) {

View File

@@ -218,7 +218,7 @@ async function JobCostingMulti(req, res) {
(multiSummary.summaryData.totalLaborGp.getAmount() /
multiSummary.summaryData.totalLaborSales.getAmount()) *
100
).toFixed(2);
).toFixed(1);
multiSummary.summaryData.totalLaborGppercentFormatted = formatGpPercent(
multiSummary.summaryData.totalLaborGppercent
);
@@ -227,7 +227,7 @@ async function JobCostingMulti(req, res) {
(multiSummary.summaryData.totalPartsGp.getAmount() /
multiSummary.summaryData.totalPartsSales.getAmount()) *
100
).toFixed(2);
).toFixed(1);
multiSummary.summaryData.totalPartsGppercentFormatted = formatGpPercent(
multiSummary.summaryData.totalPartsGppercent
@@ -237,7 +237,7 @@ async function JobCostingMulti(req, res) {
(multiSummary.summaryData.totalAdditionalGp.getAmount() /
multiSummary.summaryData.totalAdditionalSales.getAmount()) *
100
).toFixed(2);
).toFixed(1);
multiSummary.summaryData.totalAdditionalGppercentFormatted =
formatGpPercent(multiSummary.summaryData.totalAdditionalGppercent);
@@ -246,7 +246,7 @@ async function JobCostingMulti(req, res) {
(multiSummary.summaryData.totalSubletGp.getAmount() /
multiSummary.summaryData.totalSubletSales.getAmount()) *
100
).toFixed(2);
).toFixed(1);
multiSummary.summaryData.totalSubletGppercentFormatted = formatGpPercent(
multiSummary.summaryData.totalSubletGppercent
@@ -256,7 +256,7 @@ async function JobCostingMulti(req, res) {
(multiSummary.summaryData.gpdollars.getAmount() /
multiSummary.summaryData.totalSales.getAmount()) *
100
).toFixed(2);
).toFixed(1);
multiSummary.summaryData.gppercentFormatted = formatGpPercent(
multiSummary.summaryData.gppercent
@@ -282,7 +282,7 @@ async function JobCostingMulti(req, res) {
(
(c.gpdollars_dinero.getAmount() / c.sales_dinero.getAmount()) *
100
).toFixed(2)
).toFixed(1)
),
};
});
@@ -730,9 +730,9 @@ function GenerateCostingData(job) {
.add(sale_sublet);
const gpdollars = totalSales.subtract(costs);
const gppercent = (
(gpdollars.getAmount() / totalSales.getAmount()) *
(gpdollars.getAmount() / Math.abs(totalSales.getAmount())) *
100
).toFixed(2);
).toFixed(1);
//Push summary data to avoid extra loop.
summaryData.totalLaborSales = summaryData.totalLaborSales.add(sale_labor);
@@ -823,7 +823,7 @@ function GenerateCostingData(job) {
(summaryData.totalLaborGp.getAmount() /
summaryData.totalLaborSales.getAmount()) *
100
).toFixed(2);
).toFixed(1);
summaryData.totalLaborGppercentFormatted = formatGpPercent(
summaryData.totalLaborGppercent
);
@@ -835,7 +835,7 @@ function GenerateCostingData(job) {
(summaryData.totalPartsGp.getAmount() /
summaryData.totalPartsSales.getAmount()) *
100
).toFixed(2);
).toFixed(1);
summaryData.totalPartsGppercentFormatted = formatGpPercent(
summaryData.totalPartsGppercent
);
@@ -846,7 +846,7 @@ function GenerateCostingData(job) {
(summaryData.totalAdditionalGp.getAmount() /
summaryData.totalAdditionalSales.getAmount()) *
100
).toFixed(2);
).toFixed(1);
summaryData.totalAdditionalGppercentFormatted = formatGpPercent(
summaryData.totalAdditionalGppercent
);
@@ -857,7 +857,7 @@ function GenerateCostingData(job) {
(summaryData.totalSubletGp.getAmount() /
summaryData.totalSubletSales.getAmount()) *
100
).toFixed(2);
).toFixed(1);
summaryData.totalSubletGppercentFormatted = formatGpPercent(
summaryData.totalSubletGppercent
);
@@ -866,9 +866,10 @@ function GenerateCostingData(job) {
summaryData.totalCost
);
summaryData.gppercent = (
(summaryData.gpdollars.getAmount() / summaryData.totalSales.getAmount()) *
(summaryData.gpdollars.getAmount() /
Math.abs(summaryData.totalSales.getAmount())) *
100
).toFixed(2);
).toFixed(1);
if (isNaN(summaryData.gppercent)) summaryData.gppercentFormatted = 0;
else if (!isFinite(summaryData.gppercent))

View File

@@ -32,6 +32,8 @@ exports.mixdataUpload = async (req, res) => {
explicitArray: false,
});
logger.log("job-mixdata-parse", "DEBUG", req.user.email, inboundRequest);
const ScaleType = DetermineScaleType(inboundRequest);
const RoNumbersFromInboundRequest = GetListOfRos(
inboundRequest,
@@ -76,6 +78,7 @@ exports.mixdataUpload = async (req, res) => {
res.status(500).JSON(error);
logger.log("job-mixdata-upload-error", "ERROR", null, null, {
error: error.message,
...error,
});
}
};

View File

@@ -5,7 +5,7 @@ const logger = new graylog2.graylog({
});
function log(message, type, user, record, object) {
if (type !== "ioevent")
if (type !== "ioevent" && type !== "DEBUG")
console.log(message, {
type,
env: process.env.NODE_ENV || "development",
@@ -13,7 +13,7 @@ function log(message, type, user, record, object) {
record,
...object,
});
logger.log(message, {
logger.log(message, message, {
type,
env: process.env.NODE_ENV || "development",
user,

23
setadmin.js Normal file
View File

@@ -0,0 +1,23 @@
var { admin } = require("./server/firebase/firebase-handler");
const uidToMakeAdmin = "yTvpfkcNnGckLd1JnoXC7bTdvtu1";
admin
.auth()
.getUser(uidToMakeAdmin)
.then((user) => {
admin
.auth()
.setCustomUserClaims(uidToMakeAdmin, {
...user.customClaims,
"https://hasura.io/jwt/claims": {
"x-hasura-default-role": "admin",
"x-hasura-allowed-roles": ["admin"],
"x-hasura-user-id": uidToMakeAdmin,
},
ioadmin: true,
})
.then(() => console.log("Success."))
.catch((error) => console.log("Error updating claims.", error));
})
.catch((error) => console.log("Error fetching user.", error));

1306
yarn.lock

File diff suppressed because it is too large Load Diff