Merged in feature/2021-06-18 (pull request #115)
RO form items for checklist dates. Approved-by: Patrick Fic
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
<babeledit_project version="1.2" be_version="2.7.1">
|
||||
<babeledit_project be_version="2.7.1" version="1.2">
|
||||
<!--
|
||||
|
||||
BabelEdit project file
|
||||
@@ -3753,6 +3753,27 @@
|
||||
</concept_node>
|
||||
</children>
|
||||
</folder_node>
|
||||
<concept_node>
|
||||
<name>md_jobline_presets</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>md_payment_types</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
@@ -4858,6 +4879,27 @@
|
||||
<folder_node>
|
||||
<name>shop</name>
|
||||
<children>
|
||||
<concept_node>
|
||||
<name>dashboard</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>rbac</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
@@ -10511,6 +10553,27 @@
|
||||
<folder_node>
|
||||
<name>errors</name>
|
||||
<children>
|
||||
<concept_node>
|
||||
<name>refreshrequired</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>updatinglayout</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
@@ -10534,9 +10597,182 @@
|
||||
</concept_node>
|
||||
</children>
|
||||
</folder_node>
|
||||
<folder_node>
|
||||
<name>labels</name>
|
||||
<children>
|
||||
<concept_node>
|
||||
<name>bodyhrs</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>dollarsinproduction</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>prodhrs</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>refhrs</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>
|
||||
<name>titles</name>
|
||||
<children>
|
||||
<concept_node>
|
||||
<name>monthlyemployeeefficiency</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>monthlyjobcosting</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>monthlylaborsales</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>monthlypartssales</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>monthlyrevenuegraph</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
@@ -10558,6 +10794,27 @@
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>prodhrssummary</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>productiondollars</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
@@ -12449,7 +12706,7 @@
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>submit</name>
|
||||
<name>senderrortosupport</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
<description></description>
|
||||
<comment></comment>
|
||||
@@ -12470,7 +12727,7 @@
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>submitticket</name>
|
||||
<name>submit</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
<description></description>
|
||||
<comment></comment>
|
||||
@@ -12904,6 +13161,27 @@
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>globalsearch</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>hours</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
@@ -15588,6 +15866,27 @@
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>presets</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>
|
||||
@@ -20494,6 +20793,69 @@
|
||||
<folder_node>
|
||||
<name>labels</name>
|
||||
<children>
|
||||
<concept_node>
|
||||
<name>actual_completion_inferred</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>actual_delivery_inferred</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>actual_in_inferred</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>additionaltotal</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
@@ -21155,6 +21517,27 @@
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>closejob</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>contracts</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
@@ -24009,6 +24392,27 @@
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>dashboard</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>enterbills</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
@@ -24177,6 +24581,27 @@
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>newjob</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>owners</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
@@ -34094,6 +34519,27 @@
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>dashboard</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>export-logs</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
@@ -34852,6 +35298,27 @@
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>dashboard</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>export-logs</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 2.44.1 (20200629.0846)
|
||||
-->
|
||||
<!-- Title: G Pages: 1 -->
|
||||
<svg width="43pt" height="43pt"
|
||||
viewBox="0.00 0.00 43.20 43.20" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(21.6 21.6)">
|
||||
<title>G</title>
|
||||
<polygon fill="#111111" stroke="transparent" points="-21.6,21.6 -21.6,-21.6 21.6,-21.6 21.6,21.6 -21.6,21.6"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 613 B |
41288
client/package-lock.json
generated
41288
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@
|
||||
"proxy": "http://localhost:5000",
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.3.17",
|
||||
"@craco/craco": "^6.1.2",
|
||||
"@craco/craco": "^5.9.0",
|
||||
"@fingerprintjs/fingerprintjs": "^3.1.2",
|
||||
"@lourenci/react-kanban": "^2.1.0",
|
||||
"@sentry/react": "^6.3.6",
|
||||
@@ -41,6 +41,7 @@
|
||||
"react-dom": "^17.0.1",
|
||||
"react-drag-listview": "^0.1.8",
|
||||
"react-grid-gallery": "^0.5.5",
|
||||
"react-grid-layout": "^1.2.5",
|
||||
"react-i18next": "^11.8.15",
|
||||
"react-icons": "^4.2.0",
|
||||
"react-number-format": "^4.5.5",
|
||||
@@ -76,8 +77,8 @@
|
||||
"analyze": "source-map-explorer 'build/static/js/*.js'",
|
||||
"start": "craco start",
|
||||
"build": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` craco build",
|
||||
"build:test": "env-cmd -f .env.test npm run build",
|
||||
"build-deploy:test": "npm run build:test && s3cmd sync build/* s3://imex-online-test && echo '🚀 TESTING Deployed!'",
|
||||
"build:test": "env-cmd -f .env.test yarn run build",
|
||||
"build-deploy:test": "yarn run build:test && s3cmd sync build/* s3://imex-online-test && echo '🚀 TESTING Deployed!'",
|
||||
"buildcra": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` react-scripts build",
|
||||
"test": "craco test",
|
||||
"eject": "react-scripts eject",
|
||||
|
||||
@@ -5,6 +5,7 @@ import { connect } from "react-redux";
|
||||
import { Link } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBreadcrumbs } from "../../redux/application/application.selectors";
|
||||
import GlobalSearch from "../global-search/global-search.component";
|
||||
import "./breadcrumbs.styles.scss";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
@@ -14,7 +15,7 @@ const mapStateToProps = createStructuredSelector({
|
||||
export function BreadCrumbs({ breadcrumbs }) {
|
||||
return (
|
||||
<div className="breadcrumb-container imex-flex-row">
|
||||
<Breadcrumb separator=">">
|
||||
<Breadcrumb separator=">" style={{ flex: 1 }}>
|
||||
<Breadcrumb.Item>
|
||||
<Link to={`/manage`}>
|
||||
<HomeFilled />
|
||||
@@ -30,6 +31,9 @@ export function BreadCrumbs({ breadcrumbs }) {
|
||||
)
|
||||
)}
|
||||
</Breadcrumb>
|
||||
<div>
|
||||
<GlobalSearch />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import { useSubscription } from "@apollo/client";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { CONVERSATION_LIST_SUBSCRIPTION } from "../../graphql/conversations.queries";
|
||||
import { selectChatVisible } from "../../redux/messaging/messaging.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||
import ChatAffixComponent from "./chat-affix.component";
|
||||
import { Affix } from "antd";
|
||||
import "./chat-affix.styles.scss";
|
||||
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { selectChatVisible } from "../../redux/messaging/messaging.selectors";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
chatVisible: selectChatVisible,
|
||||
@@ -31,22 +29,20 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) {
|
||||
if (!bodyshop || !bodyshop.messagingservicesid) return <></>;
|
||||
|
||||
return (
|
||||
<Affix className={`chat-affix ${chatVisible ? "chat-affix-open" : ""}`}>
|
||||
<div>
|
||||
{bodyshop && bodyshop.messagingservicesid ? (
|
||||
<ChatAffixComponent
|
||||
conversationList={(data && data.conversations) || []}
|
||||
unreadCount={
|
||||
(data &&
|
||||
data.conversations.reduce((acc, val) => {
|
||||
return (acc = acc + val.messages_aggregate.aggregate.count);
|
||||
}, 0)) ||
|
||||
0
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</Affix>
|
||||
<div className={`chat-affix ${chatVisible ? "chat-affix-open" : ""}`}>
|
||||
{bodyshop && bodyshop.messagingservicesid ? (
|
||||
<ChatAffixComponent
|
||||
conversationList={(data && data.conversations) || []}
|
||||
unreadCount={
|
||||
(data &&
|
||||
data.conversations.reduce((acc, val) => {
|
||||
return (acc = acc + val.messages_aggregate.aggregate.count);
|
||||
}, 0)) ||
|
||||
0
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default connect(mapStateToProps, null)(ChatAffixContainer);
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
.chat-affix {
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
left: 2vw;
|
||||
bottom: 2vh;
|
||||
z-index: 999;
|
||||
-webkit-box-shadow: 0px 0px 2px 0px rgba(69, 69, 69, 1);
|
||||
-moz-box-shadow: 0px 0px 2px 0px rgba(69, 69, 69, 1);
|
||||
box-shadow: 0px 0px 2px 0px rgba(69, 69, 69, 1);
|
||||
}
|
||||
|
||||
.chat-affix-open {
|
||||
|
||||
@@ -17,7 +17,7 @@ export function PartsReceiveModalComponent({ bodyshop, form }) {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form.Item name="fleet" label={t("courtesycars.fields.fleetnumber")}>
|
||||
<Form.Item name="plate" label={t("courtesycars.fields.plate")}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
|
||||
@@ -44,8 +44,8 @@ export function ContractsFindModalContainer({
|
||||
|
||||
callSearch({
|
||||
variables: {
|
||||
fleet:
|
||||
(values.fleet && values.fleet !== "" && values.fleet) || undefined,
|
||||
plate:
|
||||
(values.plate && values.plate !== "" && values.plate) || undefined,
|
||||
time: values.time,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -90,13 +90,12 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
|
||||
// sorter: (a, b) => alphaSort(a.model, b.model),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "model" && state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<div>
|
||||
{record.cccontracts.length === 1
|
||||
? record.cccontracts[0].job.ro_number
|
||||
: null}
|
||||
</div>
|
||||
),
|
||||
render: (text, record) =>
|
||||
record.cccontracts.length === 1 ? (
|
||||
<Link to={`/manage/jobs/${record.cccontracts[0].job.id}`}>
|
||||
{record.cccontracts[0].job.ro_number}
|
||||
</Link>
|
||||
) : null,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
import { Card } from "antd";
|
||||
import _ from "lodash";
|
||||
import moment from "moment";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Bar,
|
||||
CartesianGrid,
|
||||
ComposedChart,
|
||||
Legend,
|
||||
Line,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
import * as Utils from "../../scoreboard-targets-table/scoreboard-targets-table.util";
|
||||
import DashboardRefreshRequired from "../refresh-required.component";
|
||||
|
||||
export default function DashboardMonthlyEmployeeEfficiency({
|
||||
data,
|
||||
...cardProps
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
if (!data) return null;
|
||||
if (!data.monthly_employee_efficiency)
|
||||
return <DashboardRefreshRequired {...cardProps} />;
|
||||
|
||||
const ticketsByDate = _.groupBy(data.monthly_employee_efficiency, (item) =>
|
||||
moment(item.date).format("YYYY-MM-DD")
|
||||
);
|
||||
|
||||
const listOfDays = Utils.ListOfDaysInCurrentMonth();
|
||||
|
||||
const chartData = listOfDays.reduce((acc, val) => {
|
||||
//Sum up the current day.
|
||||
let dailyHrs;
|
||||
if (!!ticketsByDate[val]) {
|
||||
dailyHrs = ticketsByDate[val].reduce(
|
||||
(dayAcc, dayVal) => {
|
||||
return {
|
||||
actual: dayAcc.actual + dayVal.actualhrs,
|
||||
productive: dayAcc.actual + dayVal.productivehrs,
|
||||
};
|
||||
},
|
||||
{ actual: 0, productive: 0 }
|
||||
);
|
||||
} else {
|
||||
dailyHrs = { actual: 0, productive: 0 };
|
||||
}
|
||||
|
||||
const dailyEfficiency =
|
||||
((dailyHrs.productive - dailyHrs.actual) / dailyHrs.productive + 1) * 100;
|
||||
|
||||
const theValue = {
|
||||
date: moment(val).format("DD"),
|
||||
...dailyHrs,
|
||||
dailyEfficiency: isNaN(dailyEfficiency) ? 0 : dailyEfficiency.toFixed(1),
|
||||
accActual:
|
||||
acc.length > 0
|
||||
? acc[acc.length - 1].accActual + dailyHrs.actual
|
||||
: dailyHrs.actual,
|
||||
|
||||
accProductive:
|
||||
acc.length > 0
|
||||
? acc[acc.length - 1].accProductive + dailyHrs.productive
|
||||
: dailyHrs.productive,
|
||||
accEfficiency: 0,
|
||||
};
|
||||
theValue.accEfficiency = (
|
||||
((theValue.accProductive - theValue.accActual) /
|
||||
(theValue.accProductive || 1) +
|
||||
1) *
|
||||
100
|
||||
).toFixed(1);
|
||||
|
||||
return [...acc, theValue];
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={t("dashboard.titles.monthlyemployeeefficiency")}
|
||||
{...cardProps}
|
||||
>
|
||||
<div style={{ height: "100%" }}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<ComposedChart
|
||||
data={chartData}
|
||||
margin={{ top: 20, right: 20, bottom: 20, left: 20 }}
|
||||
>
|
||||
<CartesianGrid stroke="#f5f5f5" />
|
||||
<XAxis dataKey="date" />
|
||||
<YAxis
|
||||
yAxisId="left"
|
||||
orientation="left"
|
||||
stroke="#8884d8"
|
||||
unit=" hrs"
|
||||
/>
|
||||
<YAxis
|
||||
yAxisId="right"
|
||||
orientation="right"
|
||||
stroke="#82ca9d"
|
||||
unit="%"
|
||||
/>
|
||||
<Tooltip />
|
||||
<Legend />
|
||||
<Line
|
||||
yAxisId="right"
|
||||
name="Accumulated Efficiency"
|
||||
type="monotone"
|
||||
unit="%"
|
||||
dataKey="accEfficiency"
|
||||
stroke="#152228"
|
||||
connectNulls
|
||||
// activeDot={{ r: 8 }}
|
||||
/>
|
||||
<Line
|
||||
name="Daily Efficiency"
|
||||
yAxisId="right"
|
||||
unit="%"
|
||||
type="monotone"
|
||||
connectNulls
|
||||
dataKey="dailyEfficiency"
|
||||
stroke="#d31717"
|
||||
/>
|
||||
<Bar
|
||||
name="Actual Hours"
|
||||
dataKey="actual"
|
||||
yAxisId="left"
|
||||
unit=" hrs"
|
||||
//stackId="day"
|
||||
barSize={20}
|
||||
fill="#102568"
|
||||
/>
|
||||
<Bar
|
||||
name="Productive Hours"
|
||||
dataKey="productive"
|
||||
yAxisId="left"
|
||||
unit=" hrs"
|
||||
//stackId="day"
|
||||
barSize={20}
|
||||
fill="#017664"
|
||||
/>
|
||||
</ComposedChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export const DashboardMonthlyEmployeeEfficiencyGql = `
|
||||
monthly_employee_efficiency: timetickets(where: {_and: [{date: {_gte: "${moment()
|
||||
.startOf("month")
|
||||
.format("YYYY-MM-DD")}"}},{date: {_lte: "${moment()
|
||||
.endOf("month")
|
||||
.format("YYYY-MM-DD")}"}} ]}) {
|
||||
actualhrs
|
||||
productivehrs
|
||||
employeeid
|
||||
employee {
|
||||
first_name
|
||||
last_name
|
||||
}
|
||||
date
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,163 @@
|
||||
import { Card, Input, Space, Table, Typography } from "antd";
|
||||
import axios from "axios";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { alphaSort } from "../../../utils/sorters";
|
||||
import LoadingSkeleton from "../../loading-skeleton/loading-skeleton.component";
|
||||
import Dinero from "dinero.js";
|
||||
import DashboardRefreshRequired from "../refresh-required.component";
|
||||
|
||||
export default function DashboardMonthlyJobCosting({ data, ...cardProps }) {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [costingData, setcostingData] = useState(null);
|
||||
const [searchText, setSearchText] = useState("");
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
async function getCostingData() {
|
||||
if (data && data.monthly_sales) {
|
||||
setLoading(true);
|
||||
const response = await axios.post("/job/costingmulti", {
|
||||
jobids: data.monthly_sales.map((x) => x.id),
|
||||
});
|
||||
setcostingData(response.data);
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
getCostingData();
|
||||
}, [data]);
|
||||
|
||||
if (!data) return null;
|
||||
if (!data.monthly_sales) return <DashboardRefreshRequired {...cardProps} />;
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||
};
|
||||
const columns = [
|
||||
{
|
||||
title: t("bodyshop.fields.responsibilitycenter"),
|
||||
dataIndex: "cost_center",
|
||||
key: "cost_center",
|
||||
sorter: (a, b) => alphaSort(a.cost_center, b.cost_center),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "cost_center" && state.sortedInfo.order,
|
||||
},
|
||||
{
|
||||
title: t("jobs.labels.sales"),
|
||||
dataIndex: "sales",
|
||||
key: "sales",
|
||||
sorter: (a, b) =>
|
||||
parseFloat(a.sales.substring(1)) - parseFloat(b.sales.substring(1)),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "sales" && state.sortedInfo.order,
|
||||
},
|
||||
|
||||
{
|
||||
title: t("jobs.labels.costs"),
|
||||
dataIndex: "costs",
|
||||
key: "costs",
|
||||
sorter: (a, b) =>
|
||||
parseFloat(a.costs.substring(1)) - parseFloat(b.costs.substring(1)),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "costs" && state.sortedInfo.order,
|
||||
},
|
||||
|
||||
{
|
||||
title: t("jobs.labels.gpdollars"),
|
||||
dataIndex: "gpdollars",
|
||||
key: "gpdollars",
|
||||
sorter: (a, b) =>
|
||||
parseFloat(a.gpdollars.substring(1)) -
|
||||
parseFloat(b.gpdollars.substring(1)),
|
||||
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "gpdollars" && state.sortedInfo.order,
|
||||
},
|
||||
{
|
||||
title: t("jobs.labels.gppercent"),
|
||||
dataIndex: "gppercent",
|
||||
key: "gppercent",
|
||||
sorter: (a, b) =>
|
||||
parseFloat(a.gppercent.slice(0, -1) || 0) -
|
||||
parseFloat(b.gppercent.slice(0, -1) || 0),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "gppercent" && state.sortedInfo.order,
|
||||
},
|
||||
];
|
||||
const filteredData =
|
||||
searchText === ""
|
||||
? (costingData && costingData.allCostCenterData) || []
|
||||
: costingData.allCostCenterData.filter((d) =>
|
||||
(d.cost_center || "")
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.includes(searchText.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={t("dashboard.titles.monthlyjobcosting")}
|
||||
extra={
|
||||
<Space wrap>
|
||||
<Input.Search
|
||||
placeholder={t("general.labels.search")}
|
||||
value={searchText}
|
||||
onChange={(e) => {
|
||||
e.preventDefault();
|
||||
setSearchText(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</Space>
|
||||
}
|
||||
{...cardProps}
|
||||
>
|
||||
<LoadingSkeleton loading={loading}>
|
||||
<div style={{ height: "100%" }}>
|
||||
<Table
|
||||
onChange={handleTableChange}
|
||||
pagination={{ position: "top", defaultPageSize: 50 }}
|
||||
columns={columns}
|
||||
scroll={{ x: true, y: "calc(100% - 4em)" }}
|
||||
rowKey="id"
|
||||
style={{ height: "100%" }}
|
||||
dataSource={filteredData}
|
||||
summary={() => (
|
||||
<Table.Summary.Row>
|
||||
<Table.Summary.Cell>
|
||||
<Typography.Title level={4}>
|
||||
{t("general.labels.totals")}
|
||||
</Typography.Title>
|
||||
</Table.Summary.Cell>
|
||||
<Table.Summary.Cell>
|
||||
{Dinero(
|
||||
costingData &&
|
||||
costingData.allSummaryData &&
|
||||
costingData.allSummaryData.totalSales
|
||||
).toFormat()}
|
||||
</Table.Summary.Cell>
|
||||
<Table.Summary.Cell>
|
||||
{Dinero(
|
||||
costingData &&
|
||||
costingData.allSummaryData &&
|
||||
costingData.allSummaryData.totalCost
|
||||
).toFormat()}
|
||||
</Table.Summary.Cell>
|
||||
<Table.Summary.Cell>
|
||||
{Dinero(
|
||||
costingData &&
|
||||
costingData.allSummaryData &&
|
||||
costingData.allSummaryData.gpdollars
|
||||
).toFormat()}
|
||||
</Table.Summary.Cell>
|
||||
<Table.Summary.Cell></Table.Summary.Cell>
|
||||
</Table.Summary.Row>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</LoadingSkeleton>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
import { Card } from "antd";
|
||||
import Dinero from "dinero.js";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Cell, Pie, PieChart, ResponsiveContainer, Sector } from "recharts";
|
||||
import DashboardRefreshRequired from "../refresh-required.component";
|
||||
|
||||
export default function DashboardMonthlyLaborSales({ data, ...cardProps }) {
|
||||
const { t } = useTranslation();
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
if (!data) return null;
|
||||
if (!data.monthly_sales) return <DashboardRefreshRequired {...cardProps} />;
|
||||
|
||||
const laborData = {};
|
||||
|
||||
data.monthly_sales.forEach((job) => {
|
||||
job.joblines.forEach((jobline) => {
|
||||
if (!jobline.mod_lbr_ty) return;
|
||||
if (!laborData[jobline.mod_lbr_ty])
|
||||
laborData[jobline.mod_lbr_ty] = Dinero();
|
||||
laborData[jobline.mod_lbr_ty] = laborData[jobline.mod_lbr_ty].add(
|
||||
Dinero({
|
||||
amount: Math.round(
|
||||
(job[`rate_${jobline.mod_lbr_ty.toLowerCase()}`] || 0) * 100
|
||||
),
|
||||
}).multiply(jobline.mod_lb_hrs || 0)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
const chartData = Object.keys(laborData).map((key) => {
|
||||
return {
|
||||
name: t(`joblines.fields.lbr_types.${key.toUpperCase()}`),
|
||||
value: laborData[key].getAmount() / 100,
|
||||
color: pieColor(key.toUpperCase()),
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<Card title={t("dashboard.titles.monthlylaborsales")} {...cardProps}>
|
||||
<div style={{ height: "100%" }}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart margin={0} padding={0}>
|
||||
<Pie
|
||||
data={chartData}
|
||||
activeIndex={activeIndex}
|
||||
activeShape={renderActiveShape}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius="60%"
|
||||
// outerRadius={80}
|
||||
fill="#8884d8"
|
||||
dataKey="value"
|
||||
onMouseEnter={(throwaway, index) => setActiveIndex(index)}
|
||||
>
|
||||
{chartData.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Pie>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export const DashboardMonthlyRevenueGraphGql = `
|
||||
|
||||
`;
|
||||
|
||||
const pieColor = (type) => {
|
||||
if (type === "LAA") return "lightgreen";
|
||||
else if (type === "LAB") return "dodgerblue";
|
||||
else if (type === "LAD") return "aliceblue";
|
||||
else if (type === "LAE") return "seafoam";
|
||||
else if (type === "LAG") return "chartreuse";
|
||||
else if (type === "LAF") return "magenta";
|
||||
else if (type === "LAM") return "gold";
|
||||
else if (type === "LAR") return "crimson";
|
||||
else if (type === "LAU") return "slategray";
|
||||
else if (type === "LA1") return "slategray";
|
||||
else if (type === "LA2") return "slategray";
|
||||
else if (type === "LA3") return "slategray";
|
||||
else if (type === "LA4") return "slategray";
|
||||
return "slategray";
|
||||
};
|
||||
|
||||
const renderActiveShape = (props) => {
|
||||
//const RADIAN = Math.PI / 180;
|
||||
const {
|
||||
cx,
|
||||
cy,
|
||||
//midAngle,
|
||||
innerRadius,
|
||||
outerRadius,
|
||||
startAngle,
|
||||
endAngle,
|
||||
fill,
|
||||
payload,
|
||||
// percent,
|
||||
value,
|
||||
} = props;
|
||||
// const sin = Math.sin(-RADIAN * midAngle);
|
||||
// const cos = Math.cos(-RADIAN * midAngle);
|
||||
// // const sx = cx + (outerRadius + 10) * cos;
|
||||
// const sy = cy + (outerRadius + 10) * sin;
|
||||
// const mx = cx + (outerRadius + 30) * cos;
|
||||
// const my = cy + (outerRadius + 30) * sin;
|
||||
// //const ex = mx + (cos >= 0 ? 1 : -1) * 22;
|
||||
// const ey = my;
|
||||
//const textAnchor = cos >= 0 ? "start" : "end";
|
||||
|
||||
return (
|
||||
<g>
|
||||
<text x={cx} y={cy} dy={0} textAnchor="middle" fill={fill}>
|
||||
{payload.name}
|
||||
</text>
|
||||
<text x={cx} y={cy} dy={16} textAnchor="middle" fill={fill}>
|
||||
{Dinero({ amount: Math.round(value * 100) }).toFormat()}
|
||||
</text>
|
||||
<Sector
|
||||
cx={cx}
|
||||
cy={cy}
|
||||
innerRadius={innerRadius}
|
||||
outerRadius={outerRadius}
|
||||
startAngle={startAngle}
|
||||
endAngle={endAngle}
|
||||
fill={fill}
|
||||
/>
|
||||
<Sector
|
||||
cx={cx}
|
||||
cy={cy}
|
||||
startAngle={startAngle}
|
||||
endAngle={endAngle}
|
||||
innerRadius={outerRadius + 6}
|
||||
outerRadius={outerRadius + 10}
|
||||
fill={fill}
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
};
|
||||
// <path
|
||||
// d={`M${sx},${sy}L${mx},${my}L${ex},${ey}`}
|
||||
// stroke={fill}
|
||||
// fill="none"
|
||||
// />;
|
||||
// <text
|
||||
// x={ex + (cos >= 0 ? 1 : -1) * 12}
|
||||
// y={ey}
|
||||
// textAnchor={textAnchor}
|
||||
// fill="#333"
|
||||
// >
|
||||
// {payload.name}
|
||||
// </text>
|
||||
// <text
|
||||
// x={ex + (cos >= 0 ? 1 : -1) * 12}
|
||||
// y={ey}
|
||||
// dy={18}
|
||||
// textAnchor={textAnchor}
|
||||
// fill="#999"
|
||||
// >
|
||||
// {Dinero({ amount: Math.round(value * 100) }).toFormat()}
|
||||
// </text>
|
||||
@@ -0,0 +1,136 @@
|
||||
import { Card } from "antd";
|
||||
import Dinero from "dinero.js";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Cell, Pie, PieChart, ResponsiveContainer, Sector } from "recharts";
|
||||
import DashboardRefreshRequired from "../refresh-required.component";
|
||||
|
||||
export default function DashboardMonthlyPartsSales({ data, ...cardProps }) {
|
||||
const { t } = useTranslation();
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
if (!data) return null;
|
||||
if (!data.monthly_sales) return <DashboardRefreshRequired {...cardProps} />;
|
||||
|
||||
const partData = {};
|
||||
|
||||
data.monthly_sales.forEach((job) => {
|
||||
job.joblines.forEach((jobline) => {
|
||||
if (!jobline.part_type) return;
|
||||
if (!partData[jobline.part_type]) partData[jobline.part_type] = Dinero();
|
||||
partData[jobline.part_type] = partData[jobline.part_type].add(
|
||||
Dinero({ amount: Math.round((jobline.act_price || 0) * 100) }).multiply(
|
||||
jobline.part_qty || 0
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
const chartData = Object.keys(partData).map((key) => {
|
||||
return {
|
||||
name: t(`joblines.fields.part_types.${key.toUpperCase()}`),
|
||||
value: partData[key].getAmount() / 100,
|
||||
color: pieColor(key.toUpperCase()),
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<Card title={t("dashboard.titles.monthlypartssales")} {...cardProps}>
|
||||
<div style={{ height: "100%" }}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart margin={0} padding={0}>
|
||||
<Pie
|
||||
data={chartData}
|
||||
activeIndex={activeIndex}
|
||||
activeShape={renderActiveShape}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius="60%"
|
||||
// outerRadius={80}
|
||||
fill="#8884d8"
|
||||
dataKey="value"
|
||||
onMouseEnter={(throwaway, index) => setActiveIndex(index)}
|
||||
>
|
||||
{chartData.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Pie>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export const DashboardMonthlyRevenueGraphGql = `
|
||||
|
||||
`;
|
||||
const pieColor = (type) => {
|
||||
if (type === "PAA") return "darkgreen";
|
||||
else if (type === "PAC") return "green";
|
||||
else if (type === "PAE") return "gold";
|
||||
else if (type === "PAG") return "seafoam";
|
||||
else if (type === "PAL") return "chartreuse";
|
||||
else if (type === "PAM") return "magenta";
|
||||
else if (type === "PAN") return "crimson";
|
||||
else if (type === "PAO") return "gold";
|
||||
else if (type === "PAP") return "crimson";
|
||||
else if (type === "PAR") return "indigo";
|
||||
else if (type === "PAS") return "dodgerblue";
|
||||
else if (type === "PASL") return "dodgerblue";
|
||||
return "slategray";
|
||||
};
|
||||
|
||||
const renderActiveShape = (props) => {
|
||||
// const RADIAN = Math.PI / 180;
|
||||
const {
|
||||
cx,
|
||||
cy,
|
||||
// midAngle,
|
||||
innerRadius,
|
||||
outerRadius,
|
||||
startAngle,
|
||||
endAngle,
|
||||
fill,
|
||||
payload,
|
||||
// percent,
|
||||
value,
|
||||
} = props;
|
||||
// const sin = Math.sin(-RADIAN * midAngle);
|
||||
// const cos = Math.cos(-RADIAN * midAngle);
|
||||
// const sx = cx + (outerRadius + 10) * cos;
|
||||
//const sy = cy + (outerRadius + 10) * sin;
|
||||
// const mx = cx + (outerRadius + 30) * cos;
|
||||
//const my = cy + (outerRadius + 30) * sin;
|
||||
// const ex = mx + (cos >= 0 ? 1 : -1) * 22;
|
||||
// const ey = my;
|
||||
// const textAnchor = cos >= 0 ? "start" : "end";
|
||||
|
||||
return (
|
||||
<g>
|
||||
<text x={cx} y={cy} dy={0} textAnchor="middle" fill={fill}>
|
||||
{payload.name}
|
||||
</text>
|
||||
<text x={cx} y={cy} dy={16} textAnchor="middle" fill={fill}>
|
||||
{Dinero({ amount: Math.round(value * 100) }).toFormat()}
|
||||
</text>
|
||||
<Sector
|
||||
cx={cx}
|
||||
cy={cy}
|
||||
innerRadius={innerRadius}
|
||||
outerRadius={outerRadius}
|
||||
startAngle={startAngle}
|
||||
endAngle={endAngle}
|
||||
fill={fill}
|
||||
/>
|
||||
<Sector
|
||||
cx={cx}
|
||||
cy={cy}
|
||||
startAngle={startAngle}
|
||||
endAngle={endAngle}
|
||||
innerRadius={outerRadius + 6}
|
||||
outerRadius={outerRadius + 10}
|
||||
fill={fill}
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
};
|
||||
@@ -2,29 +2,30 @@ import { Card } from "antd";
|
||||
import moment from "moment";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import _ from "lodash";
|
||||
import {
|
||||
Area,
|
||||
Bar,
|
||||
CartesianGrid,
|
||||
ComposedChart,
|
||||
Legend,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis
|
||||
Area,
|
||||
Bar,
|
||||
CartesianGrid,
|
||||
ComposedChart,
|
||||
Legend,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
import Dinero from "dinero.js";
|
||||
import * as Utils from "../../scoreboard-targets-table/scoreboard-targets-table.util";
|
||||
import DashboardRefreshRequired from "../refresh-required.component";
|
||||
|
||||
export default function DashboardMonthlyRevenueGraph({ data, ...cardProps }) {
|
||||
const { t } = useTranslation();
|
||||
if (!data) return null;
|
||||
if (!data.monthly_sales) return <DashboardRefreshRequired {...cardProps} />;
|
||||
|
||||
const jobsByDate = {
|
||||
"2020-07-5": [{ clm_total: 1224 }],
|
||||
"2020-07-8": [{ clm_total: 987 }, { clm_total: 8755 }],
|
||||
"2020-07-12": [{ clm_total: 684 }, { clm_total: 12022 }],
|
||||
"2020-07-21": [{ clm_total: 15000 }],
|
||||
"2020-07-28": [{ clm_total: 122 }, { clm_total: 4522 }],
|
||||
};
|
||||
const jobsByDate = _.groupBy(data.monthly_sales, (item) =>
|
||||
moment(item.date_invoiced).format("YYYY-MM-DD")
|
||||
);
|
||||
|
||||
const listOfDays = Utils.ListOfDaysInCurrentMonth();
|
||||
|
||||
@@ -33,17 +34,19 @@ export default function DashboardMonthlyRevenueGraph({ data, ...cardProps }) {
|
||||
let dailySales;
|
||||
if (!!jobsByDate[val]) {
|
||||
dailySales = jobsByDate[val].reduce((dayAcc, dayVal) => {
|
||||
return dayAcc + dayVal.clm_total;
|
||||
}, 0);
|
||||
return dayAcc.add(Dinero(dayVal.job_totals.totals.subtotal));
|
||||
}, Dinero());
|
||||
} else {
|
||||
dailySales = 0;
|
||||
dailySales = Dinero();
|
||||
}
|
||||
|
||||
const theValue = {
|
||||
date: moment(val).format("D dd"),
|
||||
dailySales,
|
||||
date: moment(val).format("DD"),
|
||||
dailySales: dailySales.getAmount() / 100,
|
||||
accSales:
|
||||
acc.length > 0 ? acc[acc.length - 1].accSales + dailySales : dailySales,
|
||||
acc.length > 0
|
||||
? acc[acc.length - 1].accSales + dailySales.getAmount() / 100
|
||||
: dailySales.getAmount() / 100,
|
||||
};
|
||||
|
||||
return [...acc, theValue];
|
||||
@@ -51,32 +54,38 @@ export default function DashboardMonthlyRevenueGraph({ data, ...cardProps }) {
|
||||
|
||||
return (
|
||||
<Card title={t("dashboard.titles.monthlyrevenuegraph")} {...cardProps}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<ComposedChart
|
||||
data={chartData}
|
||||
margin={{ top: 20, right: 20, bottom: 20, left: 20 }}
|
||||
>
|
||||
<CartesianGrid stroke="#f5f5f5" />
|
||||
<XAxis dataKey="date" />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Legend />
|
||||
<Area
|
||||
type="monotone"
|
||||
name="Accumulated Sales"
|
||||
dataKey="accSales"
|
||||
fill="#8884d8"
|
||||
stroke="#8884d8"
|
||||
/>
|
||||
<Bar
|
||||
name="Daily Sales"
|
||||
dataKey="dailySales"
|
||||
//stackId="day"
|
||||
barSize={20}
|
||||
fill="#413ea0"
|
||||
/>
|
||||
</ComposedChart>
|
||||
</ResponsiveContainer>
|
||||
<div style={{ height: "100%" }}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<ComposedChart
|
||||
data={chartData}
|
||||
margin={{ top: 20, right: 20, bottom: 20, left: 20 }}
|
||||
>
|
||||
<CartesianGrid stroke="#f5f5f5" />
|
||||
<XAxis dataKey="date" />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Legend />
|
||||
<Area
|
||||
type="monotone"
|
||||
name="Accumulated Sales"
|
||||
dataKey="accSales"
|
||||
fill="#3CB371"
|
||||
stroke="#3CB371"
|
||||
/>
|
||||
<Bar
|
||||
name="Daily Sales"
|
||||
dataKey="dailySales"
|
||||
//stackId="day"
|
||||
barSize={20}
|
||||
fill="#413ea0"
|
||||
/>
|
||||
</ComposedChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export const DashboardMonthlyRevenueGraphGql = `
|
||||
|
||||
`;
|
||||
|
||||
@@ -1,30 +1,40 @@
|
||||
import { ArrowDownOutlined, ArrowUpOutlined } from "@ant-design/icons";
|
||||
import { Card, Statistic } from "antd";
|
||||
import Dinero from "dinero.js";
|
||||
import moment from "moment";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import DashboardRefreshRequired from "../refresh-required.component";
|
||||
export default function DashboardProjectedMonthlySales({ data, ...cardProps }) {
|
||||
const { t } = useTranslation();
|
||||
const aboveTargetMonthlySales = false;
|
||||
if (!data) return null;
|
||||
if (!data.projected_monthly_sales)
|
||||
return <DashboardRefreshRequired {...cardProps} />;
|
||||
|
||||
const dollars =
|
||||
data.projected_monthly_sales &&
|
||||
data.projected_monthly_sales.reduce(
|
||||
(acc, val) => acc.add(Dinero(val.job_totals.totals.subtotal)),
|
||||
Dinero()
|
||||
);
|
||||
return (
|
||||
<Card {...cardProps}>
|
||||
<Statistic
|
||||
title={t("dashboard.titles.projectedmonthlysales")}
|
||||
value={222000.0}
|
||||
precision={2}
|
||||
prefix={
|
||||
<div>
|
||||
{aboveTargetMonthlySales ? (
|
||||
<ArrowUpOutlined />
|
||||
) : (
|
||||
<ArrowDownOutlined />
|
||||
)}
|
||||
$
|
||||
</div>
|
||||
}
|
||||
valueStyle={{ color: aboveTargetMonthlySales ? "green" : "red" }}
|
||||
/>
|
||||
<Card title={t("dashboard.titles.projectedmonthlysales")} {...cardProps}>
|
||||
<Statistic value={dollars.toFormat()} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export const DashboardProjectedMonthlySalesGql = `
|
||||
projected_monthly_sales: jobs(where: {_or: [{_and: [{date_invoiced: {_gte: "${moment()
|
||||
.startOf("month")
|
||||
.format("YYYY-MM-DD")}"}}, {date_invoiced: {_lte: "${moment()
|
||||
.endOf("month")
|
||||
.format("YYYY-MM-DD")}"}}]}, {_and: [{scheduled_completion: {_gte: "${moment()
|
||||
.startOf("month")
|
||||
.format("YYYY-MM-DD")}"}}, {scheduled_completion: {_lte: "${moment()
|
||||
.endOf("month")
|
||||
.format("YYYY-MM-DD")}"}}]}]}) {
|
||||
id
|
||||
date_invoiced
|
||||
job_totals
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { SyncOutlined } from "@ant-design/icons";
|
||||
import { Card } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function DashboardRefreshRequired(props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Card {...props}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
textOverflow: "ellipsis",
|
||||
}}
|
||||
>
|
||||
<SyncOutlined style={{ fontSize: "300%", margin: "1rem" }} />
|
||||
<div>{t("dashboard.errors.refreshrequired")}</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,33 +1,26 @@
|
||||
import React from "react";
|
||||
import { Card, Statistic } from "antd";
|
||||
import Dinero from "dinero.js";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ArrowDownOutlined, ArrowUpOutlined } from "@ant-design/icons";
|
||||
import DashboardRefreshRequired from "../refresh-required.component";
|
||||
|
||||
export default function DashboardTotalProductionDollars({
|
||||
data,
|
||||
...cardProps
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const aboveTargetProductionDollars = false;
|
||||
if (!data) return null;
|
||||
if (!data.production_jobs) return <DashboardRefreshRequired {...cardProps} />;
|
||||
const dollars =
|
||||
data.production_jobs &&
|
||||
data.production_jobs.reduce(
|
||||
(acc, val) => acc.add(Dinero(val.job_totals.totals.subtotal)),
|
||||
Dinero()
|
||||
);
|
||||
|
||||
return (
|
||||
<Card {...cardProps}>
|
||||
<Statistic
|
||||
title={t("dashboard.titles.productiondollars")}
|
||||
value={175000.0}
|
||||
precision={2}
|
||||
prefix={
|
||||
<div>
|
||||
{aboveTargetProductionDollars ? (
|
||||
<ArrowUpOutlined />
|
||||
) : (
|
||||
<ArrowDownOutlined />
|
||||
)}
|
||||
$
|
||||
</div>
|
||||
}
|
||||
valueStyle={{ color: aboveTargetProductionDollars ? "green" : "red" }}
|
||||
/>
|
||||
<Card title={t("dashboard.labels.dollarsinproduction")} {...cardProps}>
|
||||
<Statistic value={dollars.toFormat()} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,19 +1,63 @@
|
||||
import { Card, Space, Statistic } from "antd";
|
||||
import React from "react";
|
||||
import { Card, Statistic } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ArrowDownOutlined, ArrowUpOutlined } from "@ant-design/icons";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../../redux/user/user.selectors";
|
||||
import DashboardRefreshRequired from "../refresh-required.component";
|
||||
|
||||
export default function DashboardTotalProductionHours({ data, ...cardProps }) {
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({});
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(DashboardTotalProductionHours);
|
||||
|
||||
export function DashboardTotalProductionHours({
|
||||
bodyshop,
|
||||
data,
|
||||
...cardProps
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const aboveTargetHours = true;
|
||||
if (!data) return null;
|
||||
if (!data.production_jobs) return <DashboardRefreshRequired {...cardProps} />;
|
||||
const hours =
|
||||
data.production_jobs &&
|
||||
data.production_jobs.reduce(
|
||||
(acc, val) => {
|
||||
return {
|
||||
body: acc.body + val.labhrs.aggregate.sum.mod_lb_hrs,
|
||||
ref: acc.ref + val.larhrs.aggregate.sum.mod_lb_hrs,
|
||||
total:
|
||||
acc.total +
|
||||
val.labhrs.aggregate.sum.mod_lb_hrs +
|
||||
val.larhrs.aggregate.sum.mod_lb_hrs,
|
||||
};
|
||||
},
|
||||
{ body: 0, ref: 0, total: 0 }
|
||||
);
|
||||
const aboveTargetHours = hours.total >= bodyshop.prodtargethrs;
|
||||
return (
|
||||
<Card {...cardProps}>
|
||||
<Statistic
|
||||
title={t("dashboard.titles.productionhours")}
|
||||
value={750}
|
||||
prefix={aboveTargetHours ? <ArrowUpOutlined /> : <ArrowDownOutlined />}
|
||||
valueStyle={{ color: aboveTargetHours ? "green" : "red" }}
|
||||
/>
|
||||
<Card {...cardProps} title={t("dashboard.titles.prodhrssummary")}>
|
||||
<Space wrap style={{ flex: 1 }}>
|
||||
<Statistic
|
||||
title={t("dashboard.labels.bodyhrs")}
|
||||
value={hours.body.toFixed(1)}
|
||||
/>
|
||||
<Statistic
|
||||
title={t("dashboard.labels.refhrs")}
|
||||
value={hours.ref.toFixed(1)}
|
||||
/>
|
||||
<Statistic
|
||||
title={t("dashboard.labels.prodhrs")}
|
||||
value={hours.total.toFixed(1)}
|
||||
valueStyle={{ color: aboveTargetHours ? "green" : "red" }}
|
||||
/>
|
||||
</Space>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export const DashboardTotalProductionHoursGql = ``;
|
||||
|
||||
@@ -1,185 +1,355 @@
|
||||
// import Icon from "@ant-design/icons";
|
||||
// import { Button, Dropdown, Menu, notification } from "antd";
|
||||
// import React, { useState } from "react";
|
||||
// import { useMutation, useQuery } from "@apollo/client";
|
||||
// import { Responsive, WidthProvider } from "react-grid-layout";
|
||||
// import { useTranslation } from "react-i18next";
|
||||
// import { MdClose } from "react-icons/md";
|
||||
// import { connect } from "react-redux";
|
||||
// import { createStructuredSelector } from "reselect";
|
||||
// import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
// import { QUERY_DASHBOARD_DETAILS } from "../../graphql/bodyshop.queries";
|
||||
// import { UPDATE_DASHBOARD_LAYOUT } from "../../graphql/user.queries";
|
||||
// import {
|
||||
// selectBodyshop,
|
||||
// selectCurrentUser,
|
||||
// } from "../../redux/user/user.selectors";
|
||||
// import AlertComponent from "../alert/alert.component";
|
||||
// import DashboardMonthlyRevenueGraph from "../dashboard-components/monthly-revenue-graph/monthly-revenue-graph.component";
|
||||
// import DashboardProjectedMonthlySales from "../dashboard-components/pojected-monthly-sales/projected-monthly-sales.component";
|
||||
// import DashboardTotalProductionDollars from "../dashboard-components/total-production-dollars/total-production-dollars.component";
|
||||
// import DashboardTotalProductionHours from "../dashboard-components/total-production-hours/total-production-hours.component";
|
||||
// import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||
// //Combination of the following:
|
||||
// // /node_modules/react-grid-layout/css/styles.css
|
||||
// // /node_modules/react-resizable/css/styles.css
|
||||
// import "./dashboard-grid.styles.css";
|
||||
// import "./dashboard-grid.styles.scss";
|
||||
import Icon, { SyncOutlined } from "@ant-design/icons";
|
||||
import { gql, useMutation, useQuery } from "@apollo/client";
|
||||
import { Button, Dropdown, Menu, notification, PageHeader, Space } from "antd";
|
||||
import i18next from "i18next";
|
||||
import _ from "lodash";
|
||||
import moment from "moment";
|
||||
import React, { useState } from "react";
|
||||
import { Responsive, WidthProvider } from "react-grid-layout";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { MdClose } from "react-icons/md";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { UPDATE_DASHBOARD_LAYOUT } from "../../graphql/user.queries";
|
||||
import {
|
||||
selectBodyshop,
|
||||
selectCurrentUser,
|
||||
} from "../../redux/user/user.selectors";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import DashboardMonthlyEmployeeEfficiency, {
|
||||
DashboardMonthlyEmployeeEfficiencyGql,
|
||||
} from "../dashboard-components/monthly-employee-efficiency/monthly-employee-efficiency.component";
|
||||
import DashboardMonthlyJobCosting from "../dashboard-components/monthly-job-costing/monthly-job-costing.component";
|
||||
import DashboardMonthlyLaborSales from "../dashboard-components/monthly-labor-sales/monthly-labor-sales.component";
|
||||
import DashboardMonthlyPartsSales from "../dashboard-components/monthly-parts-sales/monthly-parts-sales.component";
|
||||
import DashboardMonthlyRevenueGraph, {
|
||||
DashboardMonthlyRevenueGraphGql,
|
||||
} from "../dashboard-components/monthly-revenue-graph/monthly-revenue-graph.component";
|
||||
import DashboardProjectedMonthlySales, {
|
||||
DashboardProjectedMonthlySalesGql,
|
||||
} from "../dashboard-components/pojected-monthly-sales/projected-monthly-sales.component";
|
||||
import DashboardTotalProductionDollars from "../dashboard-components/total-production-dollars/total-production-dollars.component";
|
||||
import DashboardTotalProductionHours, {
|
||||
DashboardTotalProductionHoursGql,
|
||||
} from "../dashboard-components/total-production-hours/total-production-hours.component";
|
||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||
//Combination of the following:
|
||||
// /node_modules/react-grid-layout/css/styles.css
|
||||
// /node_modules/react-resizable/css/styles.css
|
||||
import "./dashboard-grid.styles.scss";
|
||||
import { GenerateDashboardData } from "./dashboard-grid.utils";
|
||||
|
||||
// const ResponsiveReactGridLayout = WidthProvider(Responsive);
|
||||
const ResponsiveReactGridLayout = WidthProvider(Responsive);
|
||||
|
||||
// const mapStateToProps = createStructuredSelector({
|
||||
// currentUser: selectCurrentUser,
|
||||
// bodyshop: selectBodyshop,
|
||||
// });
|
||||
// const mapDispatchToProps = (dispatch) => ({
|
||||
// //setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
// });
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
// export function DashboardGridComponent({ currentUser, bodyshop }) {
|
||||
// const { loading, error, data } = useQuery(QUERY_DASHBOARD_DETAILS);
|
||||
// const { t } = useTranslation();
|
||||
// const [state, setState] = useState({
|
||||
// layout: bodyshop.associations[0].user.dashboardlayout || [
|
||||
// { i: "ProductionDollars", x: 0, y: 0, w: 2, h: 2 },
|
||||
// // { i: "ProductionHours", x: 2, y: 0, w: 2, h: 2 },
|
||||
// ],
|
||||
// });
|
||||
// const [updateLayout] = useMutation(UPDATE_DASHBOARD_LAYOUT);
|
||||
export function DashboardGridComponent({ currentUser, bodyshop }) {
|
||||
const { t } = useTranslation();
|
||||
const [state, setState] = useState({
|
||||
...(bodyshop.associations[0].user.dashboardlayout
|
||||
? bodyshop.associations[0].user.dashboardlayout
|
||||
: { items: [], layout: {}, layouts: [] }),
|
||||
});
|
||||
|
||||
// const handleLayoutChange = async (newLayout) => {
|
||||
// logImEXEvent("dashboard_change_layout");
|
||||
// setState({ ...state, layout: newLayout });
|
||||
// const result = await updateLayout({
|
||||
// variables: { email: currentUser.email, layout: newLayout },
|
||||
// });
|
||||
const { loading, error, data, refetch } = useQuery(
|
||||
createDashboardQuery(state)
|
||||
);
|
||||
|
||||
// if (!!result.errors) {
|
||||
// notification["error"]({
|
||||
// message: t("dashboard.errors.updatinglayout", {
|
||||
// message: JSON.stringify(result.errors),
|
||||
// }),
|
||||
// });
|
||||
// }
|
||||
// };
|
||||
const [updateLayout] = useMutation(UPDATE_DASHBOARD_LAYOUT);
|
||||
|
||||
// const handleRemoveComponent = (key) => {
|
||||
// logImEXEvent("dashboard_remove_component", { name: key });
|
||||
const handleLayoutChange = async (layout, layouts) => {
|
||||
logImEXEvent("dashboard_change_layout");
|
||||
|
||||
// const idxToRemove = state.layout.findIndex((i) => i.i === key);
|
||||
// const newLayout = state.layout;
|
||||
// newLayout.splice(idxToRemove, 1);
|
||||
// handleLayoutChange(newLayout);
|
||||
// };
|
||||
setState({ ...state, layout, layouts });
|
||||
|
||||
// const handleAddComponent = (e) => {
|
||||
// logImEXEvent("dashboard_add_component", { name: e });
|
||||
const result = await updateLayout({
|
||||
variables: {
|
||||
email: currentUser.email,
|
||||
layout: { ...state, layout, layouts },
|
||||
},
|
||||
});
|
||||
if (!!result.errors) {
|
||||
notification["error"]({
|
||||
message: t("dashboard.errors.updatinglayout", {
|
||||
message: JSON.stringify(result.errors),
|
||||
}),
|
||||
});
|
||||
}
|
||||
};
|
||||
const handleRemoveComponent = (key) => {
|
||||
logImEXEvent("dashboard_remove_component", { name: key });
|
||||
const idxToRemove = state.items.findIndex((i) => i.i === key);
|
||||
console.log(
|
||||
"🚀 ~ file: dashboard-grid.component.jsx ~ line 81 ~ idxToRemove",
|
||||
idxToRemove
|
||||
);
|
||||
const items = _.cloneDeep(state.items);
|
||||
|
||||
// handleLayoutChange([
|
||||
// ...state.layout,
|
||||
// {
|
||||
// i: e.key,
|
||||
// x: (state.layout.length * 2) % (state.cols || 12),
|
||||
// y: Infinity, // puts it at the bottom
|
||||
// w: componentList[e.key].w || 2,
|
||||
// h: componentList[e.key].h || 2,
|
||||
// },
|
||||
// ]);
|
||||
// };
|
||||
items.splice(idxToRemove, 1);
|
||||
setState({ ...state, items });
|
||||
};
|
||||
|
||||
// const onBreakpointChange = (breakpoint, cols) => {
|
||||
// setState({ ...state, breakpoint: breakpoint, cols: cols });
|
||||
// };
|
||||
const handleAddComponent = (e) => {
|
||||
logImEXEvent("dashboard_add_component", { name: e });
|
||||
setState({
|
||||
...state,
|
||||
items: [
|
||||
...state.items,
|
||||
{
|
||||
i: e.key,
|
||||
x: (state.items.length * 2) % (state.cols || 12),
|
||||
y: 99, // puts it at the bottom
|
||||
w: componentList[e.key].w || 2,
|
||||
h: componentList[e.key].h || 2,
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
// const existingLayoutKeys = state.layout.map((i) => i.i);
|
||||
// const addComponentOverlay = (
|
||||
// <Menu onClick={handleAddComponent}>
|
||||
// {Object.keys(componentList).map((key) => (
|
||||
// <Menu.Item
|
||||
// key={key}
|
||||
// value={key}
|
||||
// disabled={existingLayoutKeys.includes(key)}
|
||||
// >
|
||||
// {componentList[key].label}
|
||||
// </Menu.Item>
|
||||
// ))}
|
||||
// </Menu>
|
||||
// );
|
||||
const dashboarddata = React.useMemo(
|
||||
() => GenerateDashboardData(data),
|
||||
[data]
|
||||
);
|
||||
const existingLayoutKeys = state.items.map((i) => i.i);
|
||||
const addComponentOverlay = (
|
||||
<Menu onClick={handleAddComponent}>
|
||||
{Object.keys(componentList).map((key) => (
|
||||
<Menu.Item
|
||||
key={key}
|
||||
value={key}
|
||||
disabled={existingLayoutKeys.includes(key)}
|
||||
>
|
||||
{componentList[key].label}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu>
|
||||
);
|
||||
|
||||
// if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
|
||||
// return (
|
||||
// <div>
|
||||
// <Dropdown overlay={addComponentOverlay} trigger={["click"]}>
|
||||
// <Button>{t("dashboard.actions.addcomponent")}</Button>
|
||||
// </Dropdown>
|
||||
// <ResponsiveReactGridLayout
|
||||
// className="layout"
|
||||
// breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }}
|
||||
// cols={{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }}
|
||||
// width="100%"
|
||||
// onLayoutChange={handleLayoutChange}
|
||||
// onBreakpointChange={onBreakpointChange}
|
||||
// >
|
||||
// {state.layout.map((item, index) => {
|
||||
// const TheComponent = componentList[item.i].component;
|
||||
// return (
|
||||
// <div key={item.i} data-grid={item}>
|
||||
// <LoadingSkeleton loading={loading}>
|
||||
// <Icon
|
||||
// component={MdClose}
|
||||
// key={item.i}
|
||||
// style={{
|
||||
// position: "absolute",
|
||||
// zIndex: "2",
|
||||
// right: ".25rem",
|
||||
// top: ".25rem",
|
||||
// cursor: "pointer",
|
||||
// }}
|
||||
// onClick={() => handleRemoveComponent(item.i)}
|
||||
// />
|
||||
// <TheComponent
|
||||
// className="dashboard-card"
|
||||
// size="small"
|
||||
// style={{ height: "100%", width: "100%" }}
|
||||
// />
|
||||
// </LoadingSkeleton>
|
||||
// </div>
|
||||
// );
|
||||
// })}
|
||||
// </ResponsiveReactGridLayout>
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
return (
|
||||
<div>
|
||||
<PageHeader
|
||||
extra={
|
||||
<Space>
|
||||
<Button onClick={() => refetch()}>
|
||||
<SyncOutlined />
|
||||
</Button>
|
||||
<Dropdown overlay={addComponentOverlay} trigger={["click"]}>
|
||||
<Button>{t("dashboard.actions.addcomponent")}</Button>
|
||||
</Dropdown>
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
|
||||
// export default connect(
|
||||
// mapStateToProps,
|
||||
// mapDispatchToProps
|
||||
// )(DashboardGridComponent);
|
||||
<ResponsiveReactGridLayout
|
||||
className="layout"
|
||||
breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }}
|
||||
cols={{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }}
|
||||
width="100%"
|
||||
layouts={state.layouts}
|
||||
onLayoutChange={handleLayoutChange}
|
||||
// onBreakpointChange={onBreakpointChange}
|
||||
>
|
||||
{state.items.map((item, index) => {
|
||||
const TheComponent = componentList[item.i].component;
|
||||
return (
|
||||
<div
|
||||
key={item.i}
|
||||
data-grid={{
|
||||
...item,
|
||||
minH: componentList[item.i].minH || 1,
|
||||
minW: componentList[item.i].minW || 1,
|
||||
}}
|
||||
>
|
||||
<LoadingSkeleton loading={loading}>
|
||||
<Icon
|
||||
component={MdClose}
|
||||
key={item.i}
|
||||
style={{
|
||||
position: "absolute",
|
||||
zIndex: "2",
|
||||
right: ".25rem",
|
||||
top: ".25rem",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() => handleRemoveComponent(item.i)}
|
||||
/>
|
||||
<TheComponent className="dashboard-card" data={dashboarddata} />
|
||||
</LoadingSkeleton>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</ResponsiveReactGridLayout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// const componentList = {
|
||||
// ProductionDollars: {
|
||||
// label: "Production Dollars",
|
||||
// component: DashboardTotalProductionDollars,
|
||||
// w: 2,
|
||||
// h: 1,
|
||||
// },
|
||||
// ProductionHours: {
|
||||
// label: "Production Hours",
|
||||
// component: DashboardTotalProductionHours,
|
||||
// w: 2,
|
||||
// h: 1,
|
||||
// },
|
||||
// ProjectedMonthlySales: {
|
||||
// label: "Projected Monthly Sales",
|
||||
// component: DashboardProjectedMonthlySales,
|
||||
// w: 2,
|
||||
// h: 1,
|
||||
// },
|
||||
// MonthlyRevenueGraph: {
|
||||
// label: "Monthly Sales Graph",
|
||||
// component: DashboardMonthlyRevenueGraph,
|
||||
// w: 2,
|
||||
// h: 2,
|
||||
// },
|
||||
// };
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(DashboardGridComponent);
|
||||
|
||||
const componentList = {
|
||||
ProductionDollars: {
|
||||
label: i18next.t("dashboard.titles.productiondollars"),
|
||||
component: DashboardTotalProductionDollars,
|
||||
gqlFragment: null,
|
||||
w: 1,
|
||||
h: 1,
|
||||
minW: 2,
|
||||
minH: 1,
|
||||
},
|
||||
ProductionHours: {
|
||||
label: i18next.t("dashboard.titles.productionhours"),
|
||||
component: DashboardTotalProductionHours,
|
||||
gqlFragment: DashboardTotalProductionHoursGql,
|
||||
w: 3,
|
||||
h: 1,
|
||||
minW: 3,
|
||||
minH: 1,
|
||||
},
|
||||
ProjectedMonthlySales: {
|
||||
label: i18next.t("dashboard.titles.projectedmonthlysales"),
|
||||
component: DashboardProjectedMonthlySales,
|
||||
gqlFragment: DashboardProjectedMonthlySalesGql,
|
||||
w: 2,
|
||||
h: 1,
|
||||
minW: 2,
|
||||
minH: 1,
|
||||
},
|
||||
MonthlyRevenueGraph: {
|
||||
label: i18next.t("dashboard.titles.monthlyrevenuegraph"),
|
||||
component: DashboardMonthlyRevenueGraph,
|
||||
gqlFragment: DashboardMonthlyRevenueGraphGql,
|
||||
w: 4,
|
||||
h: 2,
|
||||
minW: 4,
|
||||
minH: 2,
|
||||
},
|
||||
MonthlyJobCosting: {
|
||||
label: i18next.t("dashboard.titles.monthlyjobcosting"),
|
||||
component: DashboardMonthlyJobCosting,
|
||||
gqlFragment: null,
|
||||
minW: 6,
|
||||
minH: 3,
|
||||
w: 6,
|
||||
h: 3,
|
||||
},
|
||||
MonthlyPartsSales: {
|
||||
label: i18next.t("dashboard.titles.productiondollars"),
|
||||
component: DashboardMonthlyPartsSales,
|
||||
gqlFragment: null,
|
||||
minW: 2,
|
||||
minH: 2,
|
||||
w: 2,
|
||||
h: 2,
|
||||
},
|
||||
MonthlyLaborSales: {
|
||||
label: i18next.t("dashboard.titles.monthlypartssales"),
|
||||
component: DashboardMonthlyLaborSales,
|
||||
gqlFragment: null,
|
||||
minW: 2,
|
||||
minH: 2,
|
||||
w: 2,
|
||||
h: 2,
|
||||
},
|
||||
MonthlyEmployeeEfficency: {
|
||||
label: i18next.t("dashboard.titles.monthlyemployeeefficiency"),
|
||||
component: DashboardMonthlyEmployeeEfficiency,
|
||||
gqlFragment: DashboardMonthlyEmployeeEfficiencyGql,
|
||||
minW: 2,
|
||||
minH: 2,
|
||||
w: 2,
|
||||
h: 2,
|
||||
},
|
||||
};
|
||||
|
||||
const createDashboardQuery = (state) => {
|
||||
const componentBasedAdditions =
|
||||
state &&
|
||||
Array.isArray(state.layout) &&
|
||||
state.layout
|
||||
.map((item, index) => componentList[item.i].gqlFragment || "")
|
||||
.join("");
|
||||
return gql`
|
||||
query QUERY_DASHBOARD_DETAILS {
|
||||
${componentBasedAdditions || ""}
|
||||
monthly_sales: jobs(where: {_and: [{date_invoiced: {_gte: "${moment()
|
||||
.startOf("month")
|
||||
.format("YYYY-MM-DD")}"}}, {date_invoiced: {_lte: "${moment()
|
||||
.endOf("month")
|
||||
.format("YYYY-MM-DD")}"}}]}) {
|
||||
id
|
||||
date_invoiced
|
||||
job_totals
|
||||
rate_la1
|
||||
rate_la2
|
||||
rate_la3
|
||||
rate_la4
|
||||
rate_laa
|
||||
rate_lab
|
||||
rate_lad
|
||||
rate_lae
|
||||
rate_laf
|
||||
rate_lag
|
||||
rate_lam
|
||||
rate_lar
|
||||
rate_las
|
||||
rate_lau
|
||||
rate_ma2s
|
||||
rate_ma2t
|
||||
rate_ma3s
|
||||
rate_mabl
|
||||
rate_macs
|
||||
rate_mahw
|
||||
rate_mapa
|
||||
rate_mash
|
||||
rate_matd
|
||||
joblines(where: { removed: { _eq: false } }) {
|
||||
id
|
||||
mod_lbr_ty
|
||||
mod_lb_hrs
|
||||
act_price
|
||||
part_qty
|
||||
part_type
|
||||
}
|
||||
}
|
||||
production_jobs: jobs(where: { inproduction: { _eq: true } }) {
|
||||
id
|
||||
ro_number
|
||||
ins_co_nm
|
||||
job_totals
|
||||
joblines(where: { removed: { _eq: false } }) {
|
||||
id
|
||||
mod_lbr_ty
|
||||
mod_lb_hrs
|
||||
act_price
|
||||
part_qty
|
||||
part_type
|
||||
}
|
||||
labhrs: joblines_aggregate(where: { mod_lbr_ty: { _neq: "LAR" } }) {
|
||||
aggregate {
|
||||
sum {
|
||||
mod_lb_hrs
|
||||
}
|
||||
}
|
||||
}
|
||||
larhrs: joblines_aggregate(where: { mod_lbr_ty: { _eq: "LAR" } }) {
|
||||
aggregate {
|
||||
sum {
|
||||
mod_lb_hrs
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
};
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
.react-resizable {
|
||||
position: relative;
|
||||
}
|
||||
.react-resizable-handle {
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-repeat: no-repeat;
|
||||
background-origin: content-box;
|
||||
box-sizing: border-box;
|
||||
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2IDYiIHN0eWxlPSJiYWNrZ3JvdW5kLWNvbG9yOiNmZmZmZmYwMCIgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSI2cHgiIGhlaWdodD0iNnB4Ij48ZyBvcGFjaXR5PSIwLjMwMiI+PHBhdGggZD0iTSA2IDYgTCAwIDYgTCAwIDQuMiBMIDQgNC4yIEwgNC4yIDQuMiBMIDQuMiAwIEwgNiAwIEwgNiA2IEwgNiA2IFoiIGZpbGw9IiMwMDAwMDAiLz48L2c+PC9zdmc+");
|
||||
background-position: bottom right;
|
||||
padding: 0 3px 3px 0;
|
||||
}
|
||||
.react-resizable-handle-sw {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
cursor: sw-resize;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
.react-resizable-handle-se {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
cursor: se-resize;
|
||||
}
|
||||
.react-resizable-handle-nw {
|
||||
top: 0;
|
||||
left: 0;
|
||||
cursor: nw-resize;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
.react-resizable-handle-ne {
|
||||
top: 0;
|
||||
right: 0;
|
||||
cursor: ne-resize;
|
||||
transform: rotate(270deg);
|
||||
}
|
||||
.react-resizable-handle-w,
|
||||
.react-resizable-handle-e {
|
||||
top: 50%;
|
||||
margin-top: -10px;
|
||||
cursor: ew-resize;
|
||||
}
|
||||
.react-resizable-handle-w {
|
||||
left: 0;
|
||||
transform: rotate(135deg);
|
||||
}
|
||||
.react-resizable-handle-e {
|
||||
right: 0;
|
||||
transform: rotate(315deg);
|
||||
}
|
||||
.react-resizable-handle-n,
|
||||
.react-resizable-handle-s {
|
||||
left: 50%;
|
||||
margin-left: -10px;
|
||||
cursor: ns-resize;
|
||||
}
|
||||
.react-resizable-handle-n {
|
||||
top: 0;
|
||||
transform: rotate(225deg);
|
||||
}
|
||||
.react-resizable-handle-s {
|
||||
bottom: 0;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
.react-grid-layout {
|
||||
position: relative;
|
||||
transition: height 200ms ease;
|
||||
}
|
||||
.react-grid-item {
|
||||
transition: all 200ms ease;
|
||||
transition-property: left, top;
|
||||
}
|
||||
.react-grid-item.cssTransforms {
|
||||
transition-property: transform;
|
||||
}
|
||||
.react-grid-item.resizing {
|
||||
z-index: 1;
|
||||
will-change: width, height;
|
||||
}
|
||||
|
||||
.react-grid-item.react-draggable-dragging {
|
||||
transition: none;
|
||||
z-index: 3;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.react-grid-item.dropping {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.react-grid-item.react-grid-placeholder {
|
||||
background: red;
|
||||
opacity: 0.2;
|
||||
transition-duration: 100ms;
|
||||
z-index: 2;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
-o-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.react-grid-item > .react-resizable-handle {
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
cursor: se-resize;
|
||||
}
|
||||
|
||||
.react-grid-item > .react-resizable-handle::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 3px;
|
||||
bottom: 3px;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-right: 2px solid rgba(0, 0, 0, 0.4);
|
||||
border-bottom: 2px solid rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.react-resizable-hide > .react-resizable-handle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,154 @@
|
||||
.dashboard-card {
|
||||
// background-color: green;
|
||||
.react-resizable {
|
||||
position: relative;
|
||||
}
|
||||
.react-resizable-handle {
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-repeat: no-repeat;
|
||||
background-origin: content-box;
|
||||
box-sizing: border-box;
|
||||
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2IDYiIHN0eWxlPSJiYWNrZ3JvdW5kLWNvbG9yOiNmZmZmZmYwMCIgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSI2cHgiIGhlaWdodD0iNnB4Ij48ZyBvcGFjaXR5PSIwLjMwMiI+PHBhdGggZD0iTSA2IDYgTCAwIDYgTCAwIDQuMiBMIDQgNC4yIEwgNC4yIDQuMiBMIDQuMiAwIEwgNiAwIEwgNiA2IEwgNiA2IFoiIGZpbGw9IiMwMDAwMDAiLz48L2c+PC9zdmc+");
|
||||
background-position: bottom right;
|
||||
padding: 0 3px 3px 0;
|
||||
}
|
||||
.react-resizable-handle-sw {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
cursor: sw-resize;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
.react-resizable-handle-se {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
cursor: se-resize;
|
||||
}
|
||||
.react-resizable-handle-nw {
|
||||
top: 0;
|
||||
left: 0;
|
||||
cursor: nw-resize;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
.react-resizable-handle-ne {
|
||||
top: 0;
|
||||
right: 0;
|
||||
cursor: ne-resize;
|
||||
transform: rotate(270deg);
|
||||
}
|
||||
.react-resizable-handle-w,
|
||||
.react-resizable-handle-e {
|
||||
top: 50%;
|
||||
margin-top: -10px;
|
||||
cursor: ew-resize;
|
||||
}
|
||||
.react-resizable-handle-w {
|
||||
left: 0;
|
||||
transform: rotate(135deg);
|
||||
}
|
||||
.react-resizable-handle-e {
|
||||
right: 0;
|
||||
transform: rotate(315deg);
|
||||
}
|
||||
.react-resizable-handle-n,
|
||||
.react-resizable-handle-s {
|
||||
left: 50%;
|
||||
margin-left: -10px;
|
||||
cursor: ns-resize;
|
||||
}
|
||||
.react-resizable-handle-n {
|
||||
top: 0;
|
||||
transform: rotate(225deg);
|
||||
}
|
||||
.react-resizable-handle-s {
|
||||
bottom: 0;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
.react-grid-layout {
|
||||
position: relative;
|
||||
transition: height 200ms ease;
|
||||
}
|
||||
.react-grid-item {
|
||||
transition: all 200ms ease;
|
||||
transition-property: left, top;
|
||||
}
|
||||
.react-grid-item.cssTransforms {
|
||||
transition-property: transform;
|
||||
}
|
||||
.react-grid-item.resizing {
|
||||
z-index: 1;
|
||||
will-change: width, height;
|
||||
}
|
||||
|
||||
.react-grid-item.react-draggable-dragging {
|
||||
transition: none;
|
||||
z-index: 3;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.react-grid-item.dropping {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.react-grid-item.react-grid-placeholder {
|
||||
background: red;
|
||||
opacity: 0.2;
|
||||
transition-duration: 100ms;
|
||||
z-index: 2;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
-o-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.react-grid-item > .react-resizable-handle {
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
cursor: se-resize;
|
||||
}
|
||||
|
||||
.react-grid-item > .react-resizable-handle::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 3px;
|
||||
bottom: 3px;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-right: 2px solid rgba(0, 0, 0, 0.4);
|
||||
border-bottom: 2px solid rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.react-resizable-hide > .react-resizable-handle {
|
||||
display: none;
|
||||
}
|
||||
.dashboard-card {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
.ant-card-body {
|
||||
// background-color: red;
|
||||
height: 100%;
|
||||
height: 80%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
// // background-color: red;
|
||||
// height: 90%;
|
||||
// width: 100%;
|
||||
// padding: 8px;
|
||||
// display: flex;
|
||||
// flex-direction: column;
|
||||
// align-items: center;
|
||||
// justify-content: center;
|
||||
}
|
||||
.ant-spin-nested-loading {
|
||||
height: 100%;
|
||||
.ant-spin-container {
|
||||
height: 100%;
|
||||
.ant-table {
|
||||
height: 100%;
|
||||
.ant-table-container {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export function GenerateDashboardData(data) {
|
||||
return data;
|
||||
}
|
||||
@@ -132,9 +132,13 @@ export const uploadToCloudinary = async (
|
||||
//Insert the document with the matching key.
|
||||
let takenat;
|
||||
if (fileType.includes("image")) {
|
||||
const exif = await exifr.parse(file);
|
||||
try {
|
||||
const exif = await exifr.parse(file);
|
||||
|
||||
takenat = exif && exif.DateTimeOriginal;
|
||||
takenat = exif && exif.DateTimeOriginal;
|
||||
} catch (error) {
|
||||
console.log("Unable to parse image file for EXIF Data");
|
||||
}
|
||||
}
|
||||
const documentInsert = await client.mutate({
|
||||
mutation: INSERT_NEW_DOCUMENT,
|
||||
|
||||
@@ -5,9 +5,14 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import {
|
||||
selectBodyshop,
|
||||
selectCurrentUser,
|
||||
} from "../../redux/user/user.selectors";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
@@ -34,24 +39,41 @@ class ErrorBoundary extends React.Component {
|
||||
}
|
||||
|
||||
handleErrorSubmit = () => {
|
||||
const errorDescription = `**Please add relevant details about what you were doing before you encountered this issue**
|
||||
window.$crisp.push([
|
||||
"do",
|
||||
"message:send",
|
||||
[
|
||||
"text",
|
||||
`I hit the following error: \n\n
|
||||
${this.state.error.message}\n\n
|
||||
${this.state.error.stack}\n\n
|
||||
URL:${window.location} as ${this.props.currentUser.email} for ${
|
||||
this.props.bodyshop && this.props.bodyshop.name
|
||||
}
|
||||
`,
|
||||
],
|
||||
]);
|
||||
|
||||
----
|
||||
System Generated Log:
|
||||
${this.state.error.message}
|
||||
${this.state.error.stack}
|
||||
`;
|
||||
window.$crisp.push(["do", "chat:open"]);
|
||||
// const errorDescription = `**Please add relevant details about what you were doing before you encountered this issue**
|
||||
|
||||
const URL = `https://bodyshop.atlassian.net/servicedesk/customer/portal/3/group/8/create/26?summary=123&description=${encodeURI(
|
||||
errorDescription
|
||||
)}&customfield_10049=${window.location}&email=${
|
||||
this.props.currentUser.email
|
||||
}`;
|
||||
console.log(`URL`, URL);
|
||||
window.open(URL, "_blank");
|
||||
// ----
|
||||
// System Generated Log:
|
||||
// ${this.state.error.message}
|
||||
// ${this.state.error.stack}
|
||||
// `;
|
||||
|
||||
// const URL = `https://bodyshop.atlassian.net/servicedesk/customer/portal/3/group/8/create/26?summary=123&description=${encodeURI(
|
||||
// errorDescription
|
||||
// )}&customfield_10049=${window.location}&email=${
|
||||
// this.props.currentUser.email
|
||||
// }`;
|
||||
// console.log(`URL`, URL);
|
||||
// window.open(URL, "_blank");
|
||||
};
|
||||
|
||||
render() {
|
||||
console.log("this.props :>> ", this.props);
|
||||
const { t } = this.props;
|
||||
const { error, info } = this.state;
|
||||
if (this.state.hasErrored === true) {
|
||||
@@ -91,7 +113,7 @@ ${this.state.error.stack}
|
||||
{t("general.actions.refresh")}
|
||||
</Button>
|
||||
<Button onClick={this.handleErrorSubmit}>
|
||||
{t("general.actions.submitticket")}
|
||||
{t("general.actions.senderrortosupport")}
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
|
||||
@@ -11,9 +11,8 @@ import AlertComponent from "../alert/alert.component";
|
||||
export default function GlobalSearch() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [callSearch, { loading, error, data }] = useLazyQuery(
|
||||
GLOBAL_SEARCH_QUERY
|
||||
);
|
||||
const [callSearch, { loading, error, data }] =
|
||||
useLazyQuery(GLOBAL_SEARCH_QUERY);
|
||||
|
||||
const executeSearch = (v) => {
|
||||
if (v && v.variables.search && v.variables.search !== "") callSearch(v);
|
||||
@@ -166,10 +165,12 @@ export default function GlobalSearch() {
|
||||
|
||||
return (
|
||||
<AutoComplete
|
||||
key="globalsearch"
|
||||
dropdownMatchSelectWidth={"false"}
|
||||
options={options}
|
||||
onSearch={handleSearch}
|
||||
allowClear
|
||||
placeholder={t("general.labels.globalsearch")}
|
||||
>
|
||||
<Input.Search loading={loading} />
|
||||
</AutoComplete>
|
||||
|
||||
@@ -3,10 +3,12 @@ import Icon, {
|
||||
BarChartOutlined,
|
||||
CarFilled,
|
||||
ClockCircleFilled,
|
||||
DashboardFilled,
|
||||
DollarCircleFilled,
|
||||
ExportOutlined,
|
||||
FieldTimeOutlined,
|
||||
FileAddFilled,
|
||||
FileAddOutlined,
|
||||
FileFilled,
|
||||
GlobalOutlined,
|
||||
HomeFilled,
|
||||
@@ -45,7 +47,6 @@ import {
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { signOutStart } from "../../redux/user/user.actions";
|
||||
import { selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import GlobalSearch from "../global-search/global-search.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
@@ -79,12 +80,11 @@ function Header({
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Layout.Header style={{ display: "flex", alignItems: "center" }}>
|
||||
<Layout.Header>
|
||||
<Menu
|
||||
mode="horizontal"
|
||||
//theme="light"
|
||||
theme={"dark"}
|
||||
style={{ flex: 1 }}
|
||||
selectedKeys={[selectedHeader]}
|
||||
onClick={handleMenuClick}
|
||||
subMenuCloseDelay={0.3}
|
||||
@@ -96,6 +96,7 @@ function Header({
|
||||
<Link to="/manage/schedule">{t("menus.header.schedule")}</Link>
|
||||
</Menu.Item>
|
||||
<Menu.SubMenu
|
||||
key="jobssubmenu"
|
||||
icon={<Icon component={FaCarCrash} />}
|
||||
title={t("menus.header.jobs")}
|
||||
>
|
||||
@@ -110,12 +111,14 @@ function Header({
|
||||
{t("menus.header.availablejobs")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
<Menu.Item key="newjob" icon={<FileAddOutlined />}>
|
||||
<Link to="/manage/jobs/new">{t("menus.header.newjob")}</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Divider key="div1" />
|
||||
<Menu.Item key="alljobs" icon={<UnorderedListOutlined />}>
|
||||
<Link to="/manage/jobs/all">{t("menus.header.alljobs")}</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
|
||||
<Menu.Divider key="div2" />
|
||||
<Menu.Item key="productionlist" icon={<ScheduleOutlined />}>
|
||||
<Link to="/manage/production/list">
|
||||
{t("menus.header.productionlist")}
|
||||
@@ -126,13 +129,13 @@ function Header({
|
||||
{t("menus.header.productionboard")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
|
||||
<Menu.Divider key="div3" />
|
||||
<Menu.Item key="scoreboard" icon={<LineChartOutlined />}>
|
||||
<Link to="/manage/scoreboard">{t("menus.header.scoreboard")}</Link>
|
||||
</Menu.Item>
|
||||
</Menu.SubMenu>
|
||||
<Menu.SubMenu
|
||||
key="customers"
|
||||
icon={<UserOutlined />}
|
||||
title={t("menus.header.customers")}
|
||||
>
|
||||
@@ -144,6 +147,7 @@ function Header({
|
||||
</Menu.Item>
|
||||
</Menu.SubMenu>
|
||||
<Menu.SubMenu
|
||||
key="ccs"
|
||||
icon={<CarFilled />}
|
||||
title={t("menus.header.courtesycars")}
|
||||
>
|
||||
@@ -164,6 +168,7 @@ function Header({
|
||||
</Menu.Item>
|
||||
</Menu.SubMenu>
|
||||
<Menu.SubMenu
|
||||
key="accounting"
|
||||
icon={<DollarCircleFilled />}
|
||||
title={t("menus.header.accounting")}
|
||||
>
|
||||
@@ -185,7 +190,7 @@ function Header({
|
||||
>
|
||||
{t("menus.header.enterbills")}
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
<Menu.Divider key="div4" />
|
||||
<Menu.Item key="allpayments" icon={<BankFilled />}>
|
||||
<Link to="/manage/payments">{t("menus.header.allpayments")}</Link>
|
||||
</Menu.Item>
|
||||
@@ -201,7 +206,7 @@ function Header({
|
||||
<Icon component={FaCreditCard} />
|
||||
{t("menus.header.enterpayment")}
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
<Menu.Divider key="div5" />
|
||||
|
||||
<Menu.Item key="timetickets" icon={<FieldTimeOutlined />}>
|
||||
<Link to="/manage/timetickets">
|
||||
@@ -220,9 +225,10 @@ function Header({
|
||||
>
|
||||
{t("menus.header.entertimeticket")}
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
<Menu.Divider key="div6" />
|
||||
|
||||
<Menu.SubMenu
|
||||
key="accountingexport"
|
||||
title={t("menus.header.export")}
|
||||
icon={<ExportOutlined />}
|
||||
>
|
||||
@@ -256,18 +262,17 @@ function Header({
|
||||
{t("menus.header.temporarydocs")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
key="help"
|
||||
onClick={() => {
|
||||
window.open("https://help.imex.online/", "_blank");
|
||||
}}
|
||||
icon={<Icon component={QuestionCircleFilled} />}
|
||||
/>
|
||||
<Menu.SubMenu title={t("menus.header.shop")} icon={<SettingOutlined />}>
|
||||
<Menu.SubMenu
|
||||
key="shopsubmenu"
|
||||
title={t("menus.header.shop")}
|
||||
icon={<SettingOutlined />}
|
||||
>
|
||||
<Menu.Item key="shop" icon={<Icon component={GiSettingsKnobs} />}>
|
||||
<Link to="/manage/shop">{t("menus.header.shop_config")}</Link>
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item key="dashboard" icon={<DashboardFilled />}>
|
||||
<Link to="/manage/dashboard">{t("menus.header.dashboard")}</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
key="reportcenter"
|
||||
icon={<BarChartOutlined />}
|
||||
@@ -293,17 +298,27 @@ function Header({
|
||||
</Menu.Item>
|
||||
</Menu.SubMenu>
|
||||
<Menu.SubMenu
|
||||
style={{ float: "right" }}
|
||||
key="user"
|
||||
title={
|
||||
currentUser.displayName ||
|
||||
currentUser.email ||
|
||||
t("general.labels.unknown")
|
||||
}
|
||||
>
|
||||
<Menu.Item danger onClick={() => signOutStart()}>
|
||||
<Menu.Item key="signout" danger onClick={() => signOutStart()}>
|
||||
{t("user.actions.signout")}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
key="help"
|
||||
onClick={() => {
|
||||
window.open("https://help.imex.online/", "_blank");
|
||||
}}
|
||||
icon={<Icon component={QuestionCircleFilled} />}
|
||||
>
|
||||
{t("menus.header.help")}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
key="rescue"
|
||||
onClick={() => {
|
||||
window.open("https://imexrescue.com/", "_blank");
|
||||
}}
|
||||
@@ -317,6 +332,7 @@ function Header({
|
||||
<Link to="/manage/profile">{t("menus.currentuser.profile")}</Link>
|
||||
</Menu.Item>
|
||||
<Menu.SubMenu
|
||||
key="langselecter"
|
||||
title={
|
||||
<span>
|
||||
<GlobalOutlined />
|
||||
@@ -335,7 +351,7 @@ function Header({
|
||||
</Menu.Item>
|
||||
</Menu.SubMenu>
|
||||
</Menu.SubMenu>
|
||||
<Menu.SubMenu style={{ float: "right" }} title={<ClockCircleFilled />}>
|
||||
<Menu.SubMenu key="recent" title={<ClockCircleFilled />}>
|
||||
{recentItems.map((i, idx) => (
|
||||
<Menu.Item key={idx}>
|
||||
<Link to={i.url}>{i.label}</Link>
|
||||
@@ -343,9 +359,6 @@ function Header({
|
||||
))}
|
||||
</Menu.SubMenu>
|
||||
</Menu>
|
||||
<div>
|
||||
<GlobalSearch />
|
||||
</div>
|
||||
</Layout.Header>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@ export function JobChecklistForm({
|
||||
|
||||
...(type === "deliver" && {
|
||||
scheduled_delivery: values.scheduled_delivery,
|
||||
actual_delivery: values.actual_delivery,
|
||||
}),
|
||||
...(type === "deliver" &&
|
||||
values.removeFromProduction && {
|
||||
@@ -147,6 +148,7 @@ export function JobChecklistForm({
|
||||
...(type === "deliver" && {
|
||||
removeFromProduction: true,
|
||||
actual_completion: job && job.actual_completion,
|
||||
actual_delivery: job && job.actual_delivery,
|
||||
}),
|
||||
...formItems
|
||||
.filter((fi) => fi.value)
|
||||
@@ -179,21 +181,21 @@ export function JobChecklistForm({
|
||||
},
|
||||
]}
|
||||
>
|
||||
<DateTimePicker />
|
||||
<DateTimePicker disabled={readOnly} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="scheduled_delivery"
|
||||
label={t("jobs.fields.scheduled_delivery")}
|
||||
disabled={readOnly}
|
||||
>
|
||||
<DateTimePicker />
|
||||
<DateTimePicker disabled={readOnly} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["production_vars", "note"]}
|
||||
label={t("jobs.fields.production_vars.note")}
|
||||
disabled={readOnly}
|
||||
>
|
||||
<Input.TextArea rows={3} />
|
||||
<Input.TextArea rows={3} disabled={readOnly} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
)}
|
||||
@@ -210,7 +212,14 @@ export function JobChecklistForm({
|
||||
},
|
||||
]}
|
||||
>
|
||||
<DateTimePicker />
|
||||
<DateTimePicker disabled={readOnly} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="actual_delivery"
|
||||
label={t("jobs.fields.actual_delivery")}
|
||||
disabled={readOnly}
|
||||
>
|
||||
<DateTimePicker disabled={readOnly} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="removeFromProduction"
|
||||
|
||||
@@ -2,7 +2,6 @@ import React from "react";
|
||||
import ConfigFormComponents from "../config-form-components/config-form-components.component";
|
||||
|
||||
export default function JobChecklistDisplay({ checklist }) {
|
||||
console.log("JobChecklistDisplay -> checklist", checklist);
|
||||
if (!checklist) return <div></div>;
|
||||
return (
|
||||
<div>
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { DownOutlined } from "@ant-design/icons";
|
||||
import { Dropdown, Menu } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
export function JoblinePresetButton({ bodyshop, form }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleSelect = (item) => {
|
||||
form.setFieldsValue(item);
|
||||
};
|
||||
|
||||
const menu = (
|
||||
<Menu>
|
||||
{bodyshop.md_jobline_presets.map((i, idx) => (
|
||||
<Menu.Item onClick={() => handleSelect(i)} onItemHover key={idx}>
|
||||
{i.label}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Dropdown trigger={["click"]} overlay={menu}>
|
||||
<a
|
||||
className="ant-dropdown-link"
|
||||
href="# "
|
||||
onClick={(e) => e.preventDefault()}
|
||||
>
|
||||
{t("joblines.labels.presets")} <DownOutlined />
|
||||
</a>
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(JoblinePresetButton);
|
||||
@@ -3,7 +3,7 @@ import React, { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import InputCurrency from "../form-items-formatted/currency-form-item.component";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
|
||||
import JoblinesPreset from "../job-lines-preset-button/job-lines-preset-button.component";
|
||||
export default function JobLinesUpsertModalComponent({
|
||||
visible,
|
||||
jobLine,
|
||||
@@ -32,6 +32,7 @@ export default function JobLinesUpsertModalComponent({
|
||||
onOk={() => form.submit()}
|
||||
okButtonProps={{ loading: loading }}
|
||||
onCancel={handleCancel}
|
||||
e
|
||||
>
|
||||
<Form
|
||||
onFinish={handleFinish}
|
||||
@@ -41,6 +42,9 @@ export default function JobLinesUpsertModalComponent({
|
||||
form={form}
|
||||
>
|
||||
<LayoutFormRow grow>
|
||||
<Form.Item label={t("joblines.fields.line_no")} name="line_no">
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("joblines.fields.line_desc")}
|
||||
rules={[
|
||||
@@ -53,6 +57,7 @@ export default function JobLinesUpsertModalComponent({
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<JoblinesPreset form={form} />
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow grow>
|
||||
<Form.Item label={t("joblines.fields.mod_lbr_ty")} name="mod_lbr_ty">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Collapse, Form, Input, Select, Switch } from "antd";
|
||||
import { Collapse, Form, Input, InputNumber, Select, Switch } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
@@ -240,6 +240,26 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
|
||||
<CurrencyInput />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.federal_tax_rate")}
|
||||
name="federal_tax_rate"
|
||||
>
|
||||
<InputNumber min={0} max={1} precision={2} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.state_tax_rate")}
|
||||
name="state_tax_rate"
|
||||
>
|
||||
<InputNumber min={0} max={1} precision={2} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.local_tax_rate")}
|
||||
name="local_tax_rate"
|
||||
>
|
||||
<InputNumber min={0} max={1} precision={2} />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow>
|
||||
<Form.Item label={t("jobs.fields.rate_lab")} name="rate_lab">
|
||||
<CurrencyInput />
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { useQuery } from "@apollo/client";
|
||||
import React, { useContext } from "react";
|
||||
import JobsCreateVehicleInfoComponent from "./jobs-create-vehicle-info.component";
|
||||
import { SEARCH_VEHICLES } from "../../graphql/vehicles.queries";
|
||||
import JobCreateContext from "../../pages/jobs-create/jobs-create.context";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import { SEARCH_VEHICLE_BY_VIN } from "../../graphql/vehicles.queries";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import JobsCreateVehicleInfoComponent from "./jobs-create-vehicle-info.component";
|
||||
|
||||
export default function JobsCreateVehicleInfoContainer({ form }) {
|
||||
const [state] = useContext(JobCreateContext);
|
||||
const { loading, error, data } = useQuery(SEARCH_VEHICLE_BY_VIN, {
|
||||
variables: { vin: `%${state.vehicle.search}%` },
|
||||
const { loading, error, data } = useQuery(SEARCH_VEHICLES, {
|
||||
variables: { search: `%${state.vehicle.search}%` },
|
||||
skip: !state.vehicle.search,
|
||||
});
|
||||
|
||||
@@ -17,7 +17,7 @@ export default function JobsCreateVehicleInfoContainer({ form }) {
|
||||
return (
|
||||
<JobsCreateVehicleInfoComponent
|
||||
loading={loading}
|
||||
vehicles={data ? data.vehicles : null}
|
||||
vehicles={data ? data.search_vehicles : null}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -200,7 +200,7 @@ export function JobsDetailHeaderActions({
|
||||
? t("production.labels.alertoff")
|
||||
: t("production.labels.alerton")}
|
||||
</Menu.Item>
|
||||
<Menu.SubMenu title={t("menus.jobsactions.duplicate")}>
|
||||
<Menu.SubMenu key="dupe" title={t("menus.jobsactions.duplicate")}>
|
||||
<Menu.Item>
|
||||
<Popconfirm
|
||||
title={t("jobs.labels.duplicateconfirm")}
|
||||
|
||||
@@ -181,6 +181,7 @@ export function JobsDetailHeaderCsi({
|
||||
|
||||
return (
|
||||
<Menu.SubMenu
|
||||
key="sendcsi"
|
||||
title={t("jobs.actions.sendcsi")}
|
||||
disabled={!job.converted}
|
||||
{...props}
|
||||
|
||||
@@ -163,9 +163,9 @@ function JobsDocumentsComponent({
|
||||
currentImageWillChange={onCurrentImageChange}
|
||||
customControls={[
|
||||
<Button
|
||||
key="edit-button"
|
||||
style={{
|
||||
float: "right",
|
||||
|
||||
zIndex: "5",
|
||||
}}
|
||||
onClick={() => {
|
||||
@@ -177,7 +177,7 @@ function JobsDocumentsComponent({
|
||||
if (newWindow) newWindow.opener = null;
|
||||
}}
|
||||
>
|
||||
<EditFilled style={{}} />
|
||||
<EditFilled />
|
||||
</Button>,
|
||||
]}
|
||||
onClickImage={(props) => {
|
||||
|
||||
@@ -37,7 +37,7 @@ export default function JobsDocumentsDeleteButton({
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
successfulDeletes.push(key);
|
||||
successfulDeletes.push(key.replace(/\.[^/.]+$/, ""));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -139,7 +139,7 @@ export function PartsOrderListTableComponent({
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
<Button
|
||||
disabled={jobRO}
|
||||
disabled={jobRO ? !record.return : jobRO}
|
||||
onClick={() => {
|
||||
logImEXEvent("parts_order_receive_bill");
|
||||
|
||||
|
||||
@@ -20,7 +20,9 @@ export default function PaymentFormTotalPayments({ jobid }) {
|
||||
|
||||
if (!data) return <></>;
|
||||
const totalPayments = data.jobs_by_pk.payments.reduce((acc, val) => {
|
||||
return acc.add(Dinero({ amount: (val.amount || 0) * 100 }));
|
||||
return acc.add(
|
||||
Dinero({ amount: Math.round(((val && val.amount) || 0) * 100) })
|
||||
);
|
||||
}, Dinero());
|
||||
|
||||
const balance =
|
||||
@@ -39,7 +41,7 @@ export default function PaymentFormTotalPayments({ jobid }) {
|
||||
<Statistic
|
||||
title={t("payments.labels.balance")}
|
||||
valueStyle={{ color: balance.getAmount() !== 0 ? "red" : "green" }}
|
||||
value={balance.toFormat()}
|
||||
value={(balance && balance.toFormat()) || ""}
|
||||
/>
|
||||
)}
|
||||
{!balance && <div>{t("jobs.errors.nofinancial")}</div>}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { useApolloClient, useMutation } from "@apollo/client";
|
||||
import { CardElement, useElements, useStripe } from "@stripe/react-stripe-js";
|
||||
import { Button, Form, Modal, notification } from "antd";
|
||||
import axios from "axios";
|
||||
@@ -7,6 +7,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { GET_JOB_INFO_FOR_STRIPE } from "../../graphql/jobs.queries";
|
||||
import {
|
||||
INSERT_NEW_PAYMENT,
|
||||
UPDATE_PAYMENT,
|
||||
@@ -44,6 +45,7 @@ function PaymentModalContainer({
|
||||
const [enterAgain, setEnterAgain] = useState(false);
|
||||
const [insertPayment] = useMutation(INSERT_NEW_PAYMENT);
|
||||
const [updatePayment] = useMutation(UPDATE_PAYMENT);
|
||||
const client = useApolloClient();
|
||||
const stripe = useStripe();
|
||||
const elements = useElements();
|
||||
const { t } = useTranslation();
|
||||
@@ -80,13 +82,22 @@ function PaymentModalContainer({
|
||||
stripe_acct_id: bodyshop.stripe_acct_id,
|
||||
});
|
||||
|
||||
const { data } = await client.query({
|
||||
query: GET_JOB_INFO_FOR_STRIPE,
|
||||
variables: { jobid: values.jobid },
|
||||
});
|
||||
|
||||
stripePayment = await stripe.confirmCardPayment(
|
||||
secretKey.data.clientSecret,
|
||||
{
|
||||
payment_method: {
|
||||
card: elements.getElement(CardElement),
|
||||
billing_details: {
|
||||
name: "Jenny Rosen",
|
||||
name: `${data.jobs_by_pk.ownr_fn || ""} ${
|
||||
data.jobs_by_pk.ownr_ln || ""
|
||||
} ${data.jobs_by_pk.ownr_co_nm || ""}`,
|
||||
email: data.jobs_by_pk.ownr_ea,
|
||||
phone: data.jobs_by_pk.ownr_ph1,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ const ret = {
|
||||
|
||||
"shop:vendors": 2,
|
||||
"shop:rbac": 1,
|
||||
"shop:dashboard": 3,
|
||||
"shop:templates": 4,
|
||||
|
||||
"temporarydocs:view": 2,
|
||||
|
||||
@@ -53,7 +53,7 @@ export function ScheduleCalendarHeaderGraph({ bodyshop, loadData }) {
|
||||
name="Ideal Load"
|
||||
dataKey="target"
|
||||
stroke="darkgreen"
|
||||
fill="whitr"
|
||||
fill="white"
|
||||
fillOpacity={0}
|
||||
/>
|
||||
<Radar
|
||||
|
||||
@@ -825,6 +825,180 @@ export default function ShopInfoGeneral({ form }) {
|
||||
}}
|
||||
</Form.List>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow grow header={t("bodyshop.fields.md_jobline_presets")}>
|
||||
<Form.List name={["md_jobline_presets"]}>
|
||||
{(fields, { add, remove, move }) => {
|
||||
return (
|
||||
<div>
|
||||
{fields.map((field, index) => (
|
||||
<Form.Item key={field.key}>
|
||||
<LayoutFormRow noDivider>
|
||||
<Form.Item
|
||||
label={t("general.labels.label")}
|
||||
key={`${index}label`}
|
||||
name={[field.name, "label"]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("joblines.fields.line_desc")}
|
||||
key={`${index}line_desc`}
|
||||
name={[field.name, "line_desc"]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("joblines.fields.mod_lbr_ty")}
|
||||
key={`${index}mod_lbr_ty`}
|
||||
name={[field.name, "mod_lbr_ty"]}
|
||||
>
|
||||
<Select allowClear>
|
||||
<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
|
||||
label={t("joblines.fields.mod_lb_hrs")}
|
||||
key={`${index}mod_lb_hrs`}
|
||||
name={[field.name, "mod_lb_hrs"]}
|
||||
>
|
||||
<InputNumber precision={1} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("joblines.fields.part_type")}
|
||||
key={`${index}part_type`}
|
||||
name={[field.name, "part_type"]}
|
||||
>
|
||||
<Select allowClear>
|
||||
<Select.Option value="PAA">
|
||||
{t("joblines.fields.part_types.PAA")}
|
||||
</Select.Option>
|
||||
<Select.Option value="PAC">
|
||||
{t("joblines.fields.part_types.PAC")}
|
||||
</Select.Option>
|
||||
<Select.Option value="PAE">
|
||||
{t("joblines.fields.part_types.PAE")}
|
||||
</Select.Option>
|
||||
<Select.Option value="PAL">
|
||||
{t("joblines.fields.part_types.PAL")}
|
||||
</Select.Option>
|
||||
<Select.Option value="PAM">
|
||||
{t("joblines.fields.part_types.PAM")}
|
||||
</Select.Option>
|
||||
<Select.Option value="PAN">
|
||||
{t("joblines.fields.part_types.PAN")}
|
||||
</Select.Option>
|
||||
<Select.Option value="PAO">
|
||||
{t("joblines.fields.part_types.PAO")}
|
||||
</Select.Option>
|
||||
<Select.Option value="PAR">
|
||||
{t("joblines.fields.part_types.PAR")}
|
||||
</Select.Option>
|
||||
<Select.Option value="PAS">
|
||||
{t("joblines.fields.part_types.PAS")}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("joblines.fields.oem_partno")}
|
||||
key={`${index}oem_partno`}
|
||||
name={[field.name, "oem_partno"]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("joblines.fields.part_qty")}
|
||||
key={`${index}part_qty`}
|
||||
name={[field.name, "part_qty"]}
|
||||
>
|
||||
<CurrencyInput precision={2} min={0} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("joblines.fields.act_price")}
|
||||
key={`${index}act_price`}
|
||||
name={[field.name, "act_price"]}
|
||||
>
|
||||
<CurrencyInput precision={2} min={0} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("joblines.fields.prt_dsmk_p")}
|
||||
key={`${index}prt_dsmk_p`}
|
||||
name={[field.name, "prt_dsmk_p"]}
|
||||
>
|
||||
<InputNumber precision={0} min={0} max={100} />
|
||||
</Form.Item>
|
||||
|
||||
<Space wrap>
|
||||
<DeleteFilled
|
||||
onClick={() => {
|
||||
remove(field.name);
|
||||
}}
|
||||
/>
|
||||
<FormListMoveArrows
|
||||
move={move}
|
||||
index={index}
|
||||
total={fields.length}
|
||||
/>
|
||||
</Space>
|
||||
</LayoutFormRow>
|
||||
</Form.Item>
|
||||
))}
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="dashed"
|
||||
onClick={() => {
|
||||
add();
|
||||
}}
|
||||
style={{ width: "100%" }}
|
||||
>
|
||||
{t("general.actions.add")}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Form.List>
|
||||
</LayoutFormRow>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -501,6 +501,18 @@ export default function ShopInfoRbacComponent({ form }) {
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.shop.dashboard")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
name={["md_rbac", "shop:dashboard"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.shop.rbac")}
|
||||
rules={[
|
||||
|
||||
@@ -51,7 +51,7 @@ export function TimeTicketShiftContainer({
|
||||
if (loading) return <LoadingSpinner />;
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
|
||||
if (!employeeId)
|
||||
if (!employeeId && !(technician && technician.id))
|
||||
return (
|
||||
<div>
|
||||
<Result
|
||||
|
||||
@@ -35,14 +35,18 @@ export const QUERY_ALL_ACTIVE_APPOINTMENTS = gql`
|
||||
v_model_yr
|
||||
v_make_desc
|
||||
v_model_desc
|
||||
labhrs: joblines_aggregate(where: { mod_lbr_ty: { _neq: "LAR" } }) {
|
||||
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" } }) {
|
||||
larhrs: joblines_aggregate(
|
||||
where: { mod_lbr_ty: { _eq: "LAR" }, removed: { _eq: false } }
|
||||
) {
|
||||
aggregate {
|
||||
sum {
|
||||
mod_lb_hrs
|
||||
@@ -128,14 +132,18 @@ export const QUERY_APPOINTMENT_BY_DATE = gql`
|
||||
v_make_desc
|
||||
v_model_desc
|
||||
}
|
||||
labhrs: joblines_aggregate(where: { mod_lbr_ty: { _neq: "LAR" } }) {
|
||||
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" } }) {
|
||||
larhrs: joblines_aggregate(
|
||||
where: { mod_lbr_ty: { _eq: "LAR" }, removed: { _eq: false } }
|
||||
) {
|
||||
aggregate {
|
||||
sum {
|
||||
mod_lb_hrs
|
||||
@@ -197,14 +205,18 @@ export const QUERY_SCHEDULE_LOAD_DATA = gql`
|
||||
query QUERY_SCHEDULE_LOAD_DATA($start: timestamptz!, $end: timestamptz!) {
|
||||
prodJobs: jobs(where: { inproduction: { _eq: true } }) {
|
||||
id
|
||||
labhrs: joblines_aggregate(where: { mod_lbr_ty: { _neq: "LAR" } }) {
|
||||
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" } }) {
|
||||
larhrs: joblines_aggregate(
|
||||
where: { mod_lbr_ty: { _eq: "LAR" }, removed: { _eq: false } }
|
||||
) {
|
||||
aggregate {
|
||||
sum {
|
||||
mod_lb_hrs
|
||||
@@ -216,17 +228,20 @@ export const QUERY_SCHEDULE_LOAD_DATA = gql`
|
||||
where: { scheduled_completion: { _gte: $start, _lte: $end } }
|
||||
) {
|
||||
id
|
||||
|
||||
ro_number
|
||||
scheduled_completion
|
||||
labhrs: joblines_aggregate(where: { mod_lbr_ty: { _neq: "LAR" } }) {
|
||||
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" } }) {
|
||||
larhrs: joblines_aggregate(
|
||||
where: { mod_lbr_ty: { _eq: "LAR" }, removed: { _eq: false } }
|
||||
) {
|
||||
aggregate {
|
||||
sum {
|
||||
mod_lb_hrs
|
||||
@@ -237,16 +252,19 @@ export const QUERY_SCHEDULE_LOAD_DATA = gql`
|
||||
arrJobs: jobs(where: { scheduled_in: { _gte: $start, _lte: $end } }) {
|
||||
id
|
||||
scheduled_in
|
||||
|
||||
ro_number
|
||||
labhrs: joblines_aggregate(where: { mod_lbr_ty: { _neq: "LAR" } }) {
|
||||
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" } }) {
|
||||
larhrs: joblines_aggregate(
|
||||
where: { mod_lbr_ty: { _eq: "LAR" }, removed: { _eq: false } }
|
||||
) {
|
||||
aggregate {
|
||||
sum {
|
||||
mod_lb_hrs
|
||||
|
||||
@@ -88,6 +88,7 @@ export const QUERY_BODYSHOP = gql`
|
||||
enforce_referral
|
||||
website
|
||||
jc_hourly_rates
|
||||
md_jobline_presets
|
||||
employees {
|
||||
id
|
||||
active
|
||||
@@ -173,6 +174,7 @@ export const UPDATE_SHOP = gql`
|
||||
enforce_referral
|
||||
website
|
||||
jc_hourly_rates
|
||||
md_jobline_presets
|
||||
employees {
|
||||
id
|
||||
first_name
|
||||
@@ -243,33 +245,3 @@ export const QUERY_STRIPE_ID = gql`
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const QUERY_DASHBOARD_DETAILS = gql`
|
||||
query QUERY_DASHBOARD_DETAILS {
|
||||
jobs {
|
||||
id
|
||||
clm_total
|
||||
scheduled_completion
|
||||
date_invoiced
|
||||
ins_co_nm
|
||||
}
|
||||
compJobs: jobs(where: { inproduction: { _eq: true } }) {
|
||||
id
|
||||
scheduled_completion
|
||||
labhrs: joblines_aggregate(where: { mod_lbr_ty: { _neq: "LAR" } }) {
|
||||
aggregate {
|
||||
sum {
|
||||
mod_lb_hrs
|
||||
}
|
||||
}
|
||||
}
|
||||
larhrs: joblines_aggregate(where: { mod_lbr_ty: { _eq: "LAR" } }) {
|
||||
aggregate {
|
||||
sum {
|
||||
mod_lb_hrs
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -191,7 +191,7 @@ export const QUERY_ACTIVE_CONTRACTS_PAGINATED = gql`
|
||||
`;
|
||||
|
||||
export const FIND_CONTRACT = gql`
|
||||
query FIND_CONTRACT($fleet: String, $time: timestamptz!) {
|
||||
query FIND_CONTRACT($plate: String, $time: timestamptz!) {
|
||||
cccontracts(
|
||||
where: {
|
||||
_or: [
|
||||
@@ -199,7 +199,7 @@ export const FIND_CONTRACT = gql`
|
||||
{ actualreturn: { _is_null: true } }
|
||||
]
|
||||
start: { _lte: $time }
|
||||
courtesycar: { fleetnumber: { _eq: $fleet } }
|
||||
courtesycar: { plate: { _eq: $plate } }
|
||||
}
|
||||
) {
|
||||
agreementnumber
|
||||
|
||||
@@ -159,6 +159,7 @@ export const UPDATE_JOB_LINE = gql`
|
||||
db_price
|
||||
act_price
|
||||
line_desc
|
||||
line_no
|
||||
oem_partno
|
||||
notes
|
||||
location
|
||||
|
||||
@@ -973,6 +973,20 @@ export const INSERT_NEW_JOB = gql`
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_JOB_INFO_FOR_STRIPE = gql`
|
||||
query GET_JOB_INFO_FOR_STRIPE($jobid: uuid!) {
|
||||
jobs_by_pk(id: $jobid) {
|
||||
id
|
||||
ro_number
|
||||
ownr_fn
|
||||
ownr_ln
|
||||
ownr_co_nm
|
||||
ownr_ph1
|
||||
ownr_ea
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const UPDATE_JOB_STATUS = gql`
|
||||
mutation UPDATE_JOB_STATUS($jobId: uuid!, $status: String!) {
|
||||
update_jobs(where: { id: { _eq: $jobId } }, _set: { status: $status }) {
|
||||
@@ -1696,6 +1710,12 @@ export const QUERY_JOB_CLOSE_DETAILS = gql`
|
||||
date_exported
|
||||
date_invoiced
|
||||
voided
|
||||
scheduled_completion
|
||||
actual_completion
|
||||
scheduled_delivery
|
||||
actual_delivery
|
||||
scheduled_in
|
||||
actual_in
|
||||
joblines(where: { removed: { _eq: false } }) {
|
||||
id
|
||||
removed
|
||||
@@ -1818,6 +1838,7 @@ export const QUERY_JOB_CHECKLISTS = gql`
|
||||
scheduled_completion
|
||||
actual_completion
|
||||
scheduled_delivery
|
||||
actual_delivery
|
||||
production_vars
|
||||
bodyshop {
|
||||
id
|
||||
|
||||
@@ -133,6 +133,36 @@ export const SEARCH_VEHICLE_BY_VIN = gql`
|
||||
}
|
||||
`;
|
||||
|
||||
export const SEARCH_VEHICLES = gql`
|
||||
query SEARCH_VEHICLES($search: String!) {
|
||||
search_vehicles(args: { search: $search }) {
|
||||
id
|
||||
plate_no
|
||||
plate_st
|
||||
v_vin
|
||||
v_model_yr
|
||||
v_model_desc
|
||||
v_make_desc
|
||||
v_color
|
||||
v_bstyle
|
||||
updated_at
|
||||
v_type
|
||||
v_trimcode
|
||||
v_tone
|
||||
v_stage
|
||||
v_prod_dt
|
||||
v_paint_codes
|
||||
v_options
|
||||
v_mldgcode
|
||||
v_makecode
|
||||
v_engine
|
||||
v_cond
|
||||
trim_color
|
||||
db_v_code
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const SEARCH_VEHICLES_BY_ID_FOR_AUTOCOMPLETE = gql`
|
||||
query SEARCH_VEHICLES_BY_ID_FOR_AUTOCOMPLETE($id: uuid!) {
|
||||
vehicles_by_pk(id: $id) {
|
||||
|
||||
36
client/src/pages/dashboard/dashboard.container.jsx
Normal file
36
client/src/pages/dashboard/dashboard.container.jsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import DashboardGridComponent from "../../components/dashboard-grid/dashboard-grid.component";
|
||||
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
|
||||
import {
|
||||
setBreadcrumbs,
|
||||
setSelectedHeader,
|
||||
} from "../../redux/application/application.actions";
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
|
||||
setSelectedHeader: (key) => dispatch(setSelectedHeader(key)),
|
||||
});
|
||||
|
||||
export function ExportsLogPageContainer({ setBreadcrumbs, setSelectedHeader }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
document.title = t("titles.dashboard");
|
||||
setSelectedHeader("dashboard");
|
||||
setBreadcrumbs([
|
||||
{
|
||||
link: "/manage/accounting/exportlogs",
|
||||
label: t("titles.bc.dashboard"),
|
||||
},
|
||||
]);
|
||||
}, [setBreadcrumbs, t, setSelectedHeader]);
|
||||
|
||||
return (
|
||||
<RbacWrapper action="shop:dashboard">
|
||||
<DashboardGridComponent />
|
||||
</RbacWrapper>
|
||||
);
|
||||
}
|
||||
export default connect(null, mapDispatchToProps)(ExportsLogPageContainer);
|
||||
@@ -1,5 +1,14 @@
|
||||
import { useApolloClient, useMutation } from "@apollo/client";
|
||||
import { Button, Form, notification, Popconfirm, Space } from "antd";
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
notification,
|
||||
Popconfirm,
|
||||
Space,
|
||||
Alert,
|
||||
Divider,
|
||||
PageHeader,
|
||||
} from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
@@ -13,6 +22,9 @@ import { generateJobLinesUpdatesForInvoicing } from "../../graphql/jobs-lines.qu
|
||||
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import LayoutFormRow from "../../components/layout-form-row/layout-form-row.component";
|
||||
import DateTimePicker from "../../components/form-date-time-picker/form-date-time-picker.component";
|
||||
import moment from "moment";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
jobRO: selectJobReadOnly,
|
||||
@@ -82,31 +94,110 @@ export function JobsCloseComponent({ job, bodyshop, jobRO }) {
|
||||
layout="vertical"
|
||||
form={form}
|
||||
onFinish={handleFinish}
|
||||
initialValues={{ joblines: job.joblines }}
|
||||
initialValues={{
|
||||
joblines: job.joblines,
|
||||
actual_in: job.actual_in
|
||||
? moment(job.actual_in)
|
||||
: job.scheduled_in && moment(job.scheduled_in),
|
||||
actual_completion: job.actual_completion
|
||||
? moment(job.actual_completion)
|
||||
: job.scheduled_completion && moment(job.scheduled_completion),
|
||||
actual_delivery: job.actual_delivery
|
||||
? moment(job.actual_delivery)
|
||||
: job.scheduled_delivery && moment(job.scheduled_delivery),
|
||||
}}
|
||||
scrollToFirstError
|
||||
>
|
||||
<Space>
|
||||
<JobsCloseAutoAllocate
|
||||
joblines={job.joblines}
|
||||
form={form}
|
||||
disabled={!!job.date_exported || jobRO}
|
||||
/>
|
||||
<PageHeader
|
||||
title={t("jobs.labels.closejob", { ro_number: job.ro_number })}
|
||||
extra={
|
||||
<Space>
|
||||
<JobsCloseAutoAllocate
|
||||
joblines={job.joblines}
|
||||
form={form}
|
||||
disabled={!!job.date_exported || jobRO}
|
||||
/>
|
||||
|
||||
<Popconfirm
|
||||
onConfirm={() => form.submit()}
|
||||
disabled={jobRO}
|
||||
okText={t("general.labels.yes")}
|
||||
cancelText={t("general.labels.no")}
|
||||
title={t("jobs.labels.closeconfirm")}
|
||||
>
|
||||
<Button loading={loading} type="danger" disabled={jobRO}>
|
||||
{t("general.actions.close")}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
<Popconfirm
|
||||
onConfirm={() => form.submit()}
|
||||
disabled={jobRO}
|
||||
okText={t("general.labels.yes")}
|
||||
cancelText={t("general.labels.no")}
|
||||
title={t("jobs.labels.closeconfirm")}
|
||||
>
|
||||
<Button loading={loading} type="danger" disabled={jobRO}>
|
||||
{t("general.actions.close")}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
|
||||
<JobsScoreboardAdd job={job} disabled={false} />
|
||||
<JobsScoreboardAdd job={job} disabled={false} />
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
|
||||
<Space wrap direction="vertical" style={{ width: "100%" }}>
|
||||
<FormsFieldChanged form={form} />
|
||||
{!job.actual_in && job.scheduled_in && (
|
||||
<Alert
|
||||
type="warning"
|
||||
message={t("jobs.labels.actual_in_inferred")}
|
||||
/>
|
||||
)}
|
||||
{!job.actual_completion && job.scheduled_completion && (
|
||||
<Alert
|
||||
type="warning"
|
||||
message={t("jobs.labels.actual_completion_inferred")}
|
||||
/>
|
||||
)}
|
||||
{!job.actual_delivery && job.scheduled_delivery && (
|
||||
<Alert
|
||||
type="warning"
|
||||
message={t("jobs.labels.actual_delivery_inferred")}
|
||||
/>
|
||||
)}
|
||||
</Space>
|
||||
<FormsFieldChanged form={form} />
|
||||
<LayoutFormRow>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.actual_in")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
},
|
||||
]}
|
||||
name="actual_in"
|
||||
>
|
||||
<DateTimePicker disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
return (
|
||||
<Form.Item
|
||||
label={t("jobs.fields.actual_completion")}
|
||||
name="actual_completion"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<DateTimePicker disabled={jobRO} />
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.actual_delivery")}
|
||||
name="actual_delivery"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<DateTimePicker disabled={jobRO} />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<Divider />
|
||||
<JobsCloseLines job={job} />
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
@@ -57,7 +57,7 @@ function JobsCreateContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
|
||||
|
||||
useEffect(() => {
|
||||
document.title = t("titles.jobs-create");
|
||||
setSelectedHeader("availablejobs");
|
||||
setSelectedHeader("newjob");
|
||||
setBreadcrumbs([
|
||||
{ link: "/manage/available", label: t("titles.bc.availablejobs") },
|
||||
{
|
||||
@@ -162,6 +162,9 @@ function JobsCreateContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
|
||||
tax_sub_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
|
||||
tax_lbr_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
|
||||
tax_levies_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
|
||||
federal_tax_rate: bodyshop.bill_tax_rates.federal_tax_rate / 100,
|
||||
state_tax_rate: bodyshop.bill_tax_rates.state_tax_rate / 100,
|
||||
local_tax_rate: bodyshop.bill_tax_rates.local_tax_rate / 100,
|
||||
parts_tax_rates: {
|
||||
PAA: {
|
||||
prt_type: "PAA",
|
||||
|
||||
@@ -158,6 +158,7 @@ const Phonebook = lazy(() => import("../phonebook/phonebook.page.container"));
|
||||
const EmailTest = lazy(() =>
|
||||
import("../../components/email-test/email-test-component")
|
||||
);
|
||||
const Dashboard = lazy(() => import("../dashboard/dashboard.container"));
|
||||
|
||||
const { Content, Footer } = Layout;
|
||||
|
||||
@@ -365,6 +366,7 @@ export function Manage({ match, conflict, bodyshop }) {
|
||||
/>
|
||||
<Route exact path={`${match.path}/help`} component={Help} />
|
||||
<Route exact path={`${match.path}/emailtest`} component={EmailTest} />
|
||||
<Route exact path={`${match.path}/dashboard`} component={Dashboard} />
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
@@ -376,41 +378,43 @@ export function Manage({ match, conflict, bodyshop }) {
|
||||
else PageContent = AppRouteTable;
|
||||
|
||||
return (
|
||||
<Layout className="layout-container">
|
||||
<HeaderContainer />
|
||||
<>
|
||||
<ChatAffixContainer />
|
||||
<Layout className="layout-container">
|
||||
<HeaderContainer />
|
||||
|
||||
<Content className="content-container">
|
||||
<FcmNotification />
|
||||
<PartnerPingComponent />
|
||||
<ErrorBoundary>{PageContent}</ErrorBoundary>
|
||||
<ChatAffixContainer />
|
||||
<BackTop />
|
||||
<Footer>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
margin: "1rem 0rem",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex" }}>
|
||||
<div>
|
||||
{`ImEX Online ${
|
||||
process.env.REACT_APP_GIT_SHA
|
||||
} - ${preval`module.exports = new Date().toLocaleString("en-US", {timeZone: "America/Los_Angeles"});`}`}
|
||||
<Content className="content-container">
|
||||
<FcmNotification />
|
||||
<PartnerPingComponent />
|
||||
<ErrorBoundary>{PageContent}</ErrorBoundary>
|
||||
<BackTop />
|
||||
<Footer>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
margin: "1rem 0rem",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex" }}>
|
||||
<div>
|
||||
{`ImEX Online ${
|
||||
process.env.REACT_APP_GIT_SHA
|
||||
} - ${preval`module.exports = new Date().toLocaleString("en-US", {timeZone: "America/Los_Angeles"});`}`}
|
||||
</div>
|
||||
<div id="noticeable-widget" style={{ marginLeft: "1rem" }} />
|
||||
</div>
|
||||
<div id="noticeable-widget" style={{ marginLeft: "1rem" }} />
|
||||
<Link to="/about" target="_blank" style={{ color: "#ccc" }}>
|
||||
Disclaimer & Notices
|
||||
</Link>
|
||||
<JiraSupportComponent />
|
||||
</div>
|
||||
<Link to="/about" target="_blank" style={{ color: "#ccc" }}>
|
||||
Disclaimer
|
||||
</Link>
|
||||
<JiraSupportComponent />
|
||||
</div>
|
||||
</Footer>
|
||||
</Content>
|
||||
</Layout>
|
||||
</Footer>
|
||||
</Content>
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
export default connect(mapStateToProps, null)(Manage);
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
}
|
||||
|
||||
.layout-container {
|
||||
height: 100vh;
|
||||
overflow-y: auto;
|
||||
// height: 100vh;
|
||||
// overflow-y: auto;
|
||||
}
|
||||
|
||||
@@ -156,7 +156,7 @@
|
||||
"markforreexport": "Mark for Re-export",
|
||||
"new": "New Bill",
|
||||
"noneselected": "No bill selected.",
|
||||
"onlycmforinvoiced": "Only credit memos can be entered for any job that has been invoiced.",
|
||||
"onlycmforinvoiced": "Only credit memos can be entered for any job that has been invoiced, exported, or voided.",
|
||||
"retailtotal": "Bills Retail Total",
|
||||
"state_tax": "Provincial/State Tax",
|
||||
"subtotal": "Subtotal",
|
||||
@@ -165,7 +165,7 @@
|
||||
"successes": {
|
||||
"created": "Invoice added successfully.",
|
||||
"deleted": "Bill deleted successfully.",
|
||||
"exported": "Bill exported successfully."
|
||||
"exported": "Bill(s) exported successfully."
|
||||
},
|
||||
"validation": {
|
||||
"manualinhouse": "Manual posting to the in house vendor is restricted. ",
|
||||
@@ -243,6 +243,7 @@
|
||||
"street2": "Street 2",
|
||||
"zip": "Zip/Postal Code"
|
||||
},
|
||||
"md_jobline_presets": "Jobline Presets",
|
||||
"md_payment_types": "Payment Types",
|
||||
"md_referral_sources": "Referral Sources",
|
||||
"messaginglabel": "Messaging Preset Label",
|
||||
@@ -322,6 +323,7 @@
|
||||
"view": "Shift Clock -> View"
|
||||
},
|
||||
"shop": {
|
||||
"dashboard": "Shop -> Dashboard",
|
||||
"rbac": "Shop -> RBAC",
|
||||
"templates": "Shop -> Templates",
|
||||
"vendors": "Shop -> Vendors"
|
||||
@@ -667,12 +669,24 @@
|
||||
"addcomponent": "Add Component"
|
||||
},
|
||||
"errors": {
|
||||
"refreshrequired": "You must refresh the dashboard data to see this component.",
|
||||
"updatinglayout": "Error saving updated layout {{message}}"
|
||||
},
|
||||
"labels": {
|
||||
"bodyhrs": "Body Hrs",
|
||||
"dollarsinproduction": "Dollars in Production",
|
||||
"prodhrs": "Production Hrs",
|
||||
"refhrs": "Refinish Hrs"
|
||||
},
|
||||
"titles": {
|
||||
"monthlyemployeeefficiency": "Monthly Employee Efficiency",
|
||||
"monthlyjobcosting": "Monthly Job Costing ",
|
||||
"monthlylaborsales": "Monthly Labor Sales",
|
||||
"monthlypartssales": "Monthly Parts Sales",
|
||||
"monthlyrevenuegraph": "Monthly Revenue Graph",
|
||||
"productiondollars": "Total dollars in production",
|
||||
"productionhours": "Total hours in production",
|
||||
"prodhrssummary": "Production Hours Summary",
|
||||
"productiondollars": "Total dollars in Production",
|
||||
"productionhours": "Total hours in Production",
|
||||
"projectedmonthlysales": "Projected Monthly Sales"
|
||||
}
|
||||
},
|
||||
@@ -798,8 +812,8 @@
|
||||
"save": "Save",
|
||||
"saveandnew": "Save and New",
|
||||
"selectall": "Select All",
|
||||
"senderrortosupport": "Send Error to Support",
|
||||
"submit": "Submit",
|
||||
"submitticket": "Submit a Support Ticket",
|
||||
"view": "View",
|
||||
"viewreleasenotes": "See What's Changed"
|
||||
},
|
||||
@@ -825,6 +839,7 @@
|
||||
"errors": "Errors",
|
||||
"exceptiontitle": "An error has occurred.",
|
||||
"friday": "Friday",
|
||||
"globalsearch": "Global Search",
|
||||
"hours": "hrs",
|
||||
"in": "In",
|
||||
"instanceconflictext": "Your $t(titles.app) account can only be used on one device at any given time. Refresh your session to take control.",
|
||||
@@ -980,7 +995,8 @@
|
||||
"billref": "Latest Bill",
|
||||
"edit": "Edit Line",
|
||||
"new": "New Line",
|
||||
"nostatus": "No Status"
|
||||
"nostatus": "No Status",
|
||||
"presets": "Jobline Presets"
|
||||
},
|
||||
"successes": {
|
||||
"created": "Job line created successfully.",
|
||||
@@ -1234,6 +1250,9 @@
|
||||
"scheddates": "Schedule Dates"
|
||||
},
|
||||
"labels": {
|
||||
"actual_completion_inferred": "$t(jobs.fields.actual_completion) inferred using $t(jobs.fields.scheduled_completion).",
|
||||
"actual_delivery_inferred": "$t(jobs.fields.actual_delivery) inferred using $t(jobs.fields.scheduled_delivery).",
|
||||
"actual_in_inferred": "$t(jobs.fields.actual_in) inferred using $t(jobs.fields.scheduled_in).",
|
||||
"additionaltotal": "Additional Total",
|
||||
"adjustmentrate": "Adjustment Rate",
|
||||
"adjustments": "Adjustments",
|
||||
@@ -1269,6 +1288,7 @@
|
||||
"checklistdocuments": "Checklist Documents",
|
||||
"checklists": "Checklists",
|
||||
"closeconfirm": "Are you sure you want to close this job? This cannot be easily undone.",
|
||||
"closejob": "Close Job {{ro_number}}",
|
||||
"contracts": "CC Contracts",
|
||||
"cost": "Cost",
|
||||
"cost_labor": "Cost - Labor",
|
||||
@@ -1389,7 +1409,7 @@
|
||||
"delete": "Job deleted successfully.",
|
||||
"deleted": "Job deleted successfully.",
|
||||
"duplicated": "Job duplicated successfully. ",
|
||||
"exported": "Job exported successfully. ",
|
||||
"exported": "Job(s) exported successfully. ",
|
||||
"invoiced": "Job closed and invoiced successfully.",
|
||||
"partsqueue": "Job added to parts queue.",
|
||||
"save": "Job saved successfully.",
|
||||
@@ -1419,6 +1439,7 @@
|
||||
"courtesycars-contracts": "Contracts",
|
||||
"courtesycars-newcontract": "New Contract",
|
||||
"customers": "Customers",
|
||||
"dashboard": "Dashboard",
|
||||
"enterbills": "Enter Bills",
|
||||
"enterpayment": "Enter Payments",
|
||||
"entertimeticket": "Enter Time Tickets",
|
||||
@@ -1427,6 +1448,7 @@
|
||||
"help": "Help",
|
||||
"home": "Home",
|
||||
"jobs": "Jobs",
|
||||
"newjob": "Create New Job",
|
||||
"owners": "Owners",
|
||||
"parts-queue": "Parts Queue",
|
||||
"phonebook": "Phonebook",
|
||||
@@ -1663,7 +1685,7 @@
|
||||
"totalpayments": "Total Payments"
|
||||
},
|
||||
"successes": {
|
||||
"exported": "Payment exported successfully.",
|
||||
"exported": "Payment(s) exported successfully.",
|
||||
"payment": "Payment created successfully. ",
|
||||
"stripe": "Credit card transaction charged successfully."
|
||||
}
|
||||
@@ -2049,6 +2071,7 @@
|
||||
"courtesycars": "Courtesy Cars",
|
||||
"courtesycars-detail": "Courtesy Car {{number}}",
|
||||
"courtesycars-new": "New Courtesy Car",
|
||||
"dashboard": "Dashboard",
|
||||
"export-logs": "Export Logs",
|
||||
"jobs": "Jobs",
|
||||
"jobs-active": "Active Jobs",
|
||||
@@ -2086,6 +2109,7 @@
|
||||
"courtesycars": "Courtesy Cars | $t(titles.app)",
|
||||
"courtesycars-create": "New Courtesy Car | $t(titles.app)",
|
||||
"courtesycars-detail": "Courtesy Car {{id}} | $t(titles.app)",
|
||||
"dashboard": "Dashboard | $t(titles.app)",
|
||||
"export-logs": "Export Logs | $t(titles.app)",
|
||||
"jobs": "Active Jobs | $t(titles.app)",
|
||||
"jobs-admin": "Job {{ro_number}} - Admin | $t(titles.app)",
|
||||
|
||||
@@ -243,6 +243,7 @@
|
||||
"street2": "",
|
||||
"zip": ""
|
||||
},
|
||||
"md_jobline_presets": "",
|
||||
"md_payment_types": "",
|
||||
"md_referral_sources": "",
|
||||
"messaginglabel": "",
|
||||
@@ -322,6 +323,7 @@
|
||||
"view": ""
|
||||
},
|
||||
"shop": {
|
||||
"dashboard": "",
|
||||
"rbac": "",
|
||||
"templates": "",
|
||||
"vendors": ""
|
||||
@@ -667,10 +669,22 @@
|
||||
"addcomponent": ""
|
||||
},
|
||||
"errors": {
|
||||
"refreshrequired": "",
|
||||
"updatinglayout": ""
|
||||
},
|
||||
"labels": {
|
||||
"bodyhrs": "",
|
||||
"dollarsinproduction": "",
|
||||
"prodhrs": "",
|
||||
"refhrs": ""
|
||||
},
|
||||
"titles": {
|
||||
"monthlyemployeeefficiency": "",
|
||||
"monthlyjobcosting": "",
|
||||
"monthlylaborsales": "",
|
||||
"monthlypartssales": "",
|
||||
"monthlyrevenuegraph": "",
|
||||
"prodhrssummary": "",
|
||||
"productiondollars": "",
|
||||
"productionhours": "",
|
||||
"projectedmonthlysales": ""
|
||||
@@ -798,8 +812,8 @@
|
||||
"save": "Salvar",
|
||||
"saveandnew": "",
|
||||
"selectall": "",
|
||||
"senderrortosupport": "",
|
||||
"submit": "",
|
||||
"submitticket": "",
|
||||
"view": "",
|
||||
"viewreleasenotes": ""
|
||||
},
|
||||
@@ -825,6 +839,7 @@
|
||||
"errors": "",
|
||||
"exceptiontitle": "",
|
||||
"friday": "",
|
||||
"globalsearch": "",
|
||||
"hours": "",
|
||||
"in": "en",
|
||||
"instanceconflictext": "",
|
||||
@@ -980,7 +995,8 @@
|
||||
"billref": "",
|
||||
"edit": "Línea de edición",
|
||||
"new": "Nueva línea",
|
||||
"nostatus": ""
|
||||
"nostatus": "",
|
||||
"presets": ""
|
||||
},
|
||||
"successes": {
|
||||
"created": "",
|
||||
@@ -1234,6 +1250,9 @@
|
||||
"scheddates": ""
|
||||
},
|
||||
"labels": {
|
||||
"actual_completion_inferred": "",
|
||||
"actual_delivery_inferred": "",
|
||||
"actual_in_inferred": "",
|
||||
"additionaltotal": "",
|
||||
"adjustmentrate": "",
|
||||
"adjustments": "",
|
||||
@@ -1269,6 +1288,7 @@
|
||||
"checklistdocuments": "",
|
||||
"checklists": "",
|
||||
"closeconfirm": "",
|
||||
"closejob": "",
|
||||
"contracts": "",
|
||||
"cost": "",
|
||||
"cost_labor": "",
|
||||
@@ -1419,6 +1439,7 @@
|
||||
"courtesycars-contracts": "",
|
||||
"courtesycars-newcontract": "",
|
||||
"customers": "Clientes",
|
||||
"dashboard": "",
|
||||
"enterbills": "",
|
||||
"enterpayment": "",
|
||||
"entertimeticket": "",
|
||||
@@ -1427,6 +1448,7 @@
|
||||
"help": "",
|
||||
"home": "Casa",
|
||||
"jobs": "Trabajos",
|
||||
"newjob": "",
|
||||
"owners": "propietarios",
|
||||
"parts-queue": "",
|
||||
"phonebook": "",
|
||||
@@ -2049,6 +2071,7 @@
|
||||
"courtesycars": "",
|
||||
"courtesycars-detail": "",
|
||||
"courtesycars-new": "",
|
||||
"dashboard": "",
|
||||
"export-logs": "",
|
||||
"jobs": "",
|
||||
"jobs-active": "",
|
||||
@@ -2086,6 +2109,7 @@
|
||||
"courtesycars": "",
|
||||
"courtesycars-create": "",
|
||||
"courtesycars-detail": "",
|
||||
"dashboard": "",
|
||||
"export-logs": "",
|
||||
"jobs": "Todos los trabajos | $t(titles.app)",
|
||||
"jobs-admin": "",
|
||||
|
||||
@@ -243,6 +243,7 @@
|
||||
"street2": "",
|
||||
"zip": ""
|
||||
},
|
||||
"md_jobline_presets": "",
|
||||
"md_payment_types": "",
|
||||
"md_referral_sources": "",
|
||||
"messaginglabel": "",
|
||||
@@ -322,6 +323,7 @@
|
||||
"view": ""
|
||||
},
|
||||
"shop": {
|
||||
"dashboard": "",
|
||||
"rbac": "",
|
||||
"templates": "",
|
||||
"vendors": ""
|
||||
@@ -667,10 +669,22 @@
|
||||
"addcomponent": ""
|
||||
},
|
||||
"errors": {
|
||||
"refreshrequired": "",
|
||||
"updatinglayout": ""
|
||||
},
|
||||
"labels": {
|
||||
"bodyhrs": "",
|
||||
"dollarsinproduction": "",
|
||||
"prodhrs": "",
|
||||
"refhrs": ""
|
||||
},
|
||||
"titles": {
|
||||
"monthlyemployeeefficiency": "",
|
||||
"monthlyjobcosting": "",
|
||||
"monthlylaborsales": "",
|
||||
"monthlypartssales": "",
|
||||
"monthlyrevenuegraph": "",
|
||||
"prodhrssummary": "",
|
||||
"productiondollars": "",
|
||||
"productionhours": "",
|
||||
"projectedmonthlysales": ""
|
||||
@@ -798,8 +812,8 @@
|
||||
"save": "sauvegarder",
|
||||
"saveandnew": "",
|
||||
"selectall": "",
|
||||
"senderrortosupport": "",
|
||||
"submit": "",
|
||||
"submitticket": "",
|
||||
"view": "",
|
||||
"viewreleasenotes": ""
|
||||
},
|
||||
@@ -825,6 +839,7 @@
|
||||
"errors": "",
|
||||
"exceptiontitle": "",
|
||||
"friday": "",
|
||||
"globalsearch": "",
|
||||
"hours": "",
|
||||
"in": "dans",
|
||||
"instanceconflictext": "",
|
||||
@@ -980,7 +995,8 @@
|
||||
"billref": "",
|
||||
"edit": "Ligne d'édition",
|
||||
"new": "Nouvelle ligne",
|
||||
"nostatus": ""
|
||||
"nostatus": "",
|
||||
"presets": ""
|
||||
},
|
||||
"successes": {
|
||||
"created": "",
|
||||
@@ -1234,6 +1250,9 @@
|
||||
"scheddates": ""
|
||||
},
|
||||
"labels": {
|
||||
"actual_completion_inferred": "",
|
||||
"actual_delivery_inferred": "",
|
||||
"actual_in_inferred": "",
|
||||
"additionaltotal": "",
|
||||
"adjustmentrate": "",
|
||||
"adjustments": "",
|
||||
@@ -1269,6 +1288,7 @@
|
||||
"checklistdocuments": "",
|
||||
"checklists": "",
|
||||
"closeconfirm": "",
|
||||
"closejob": "",
|
||||
"contracts": "",
|
||||
"cost": "",
|
||||
"cost_labor": "",
|
||||
@@ -1419,6 +1439,7 @@
|
||||
"courtesycars-contracts": "",
|
||||
"courtesycars-newcontract": "",
|
||||
"customers": "Les clients",
|
||||
"dashboard": "",
|
||||
"enterbills": "",
|
||||
"enterpayment": "",
|
||||
"entertimeticket": "",
|
||||
@@ -1427,6 +1448,7 @@
|
||||
"help": "",
|
||||
"home": "Accueil",
|
||||
"jobs": "Emplois",
|
||||
"newjob": "",
|
||||
"owners": "Propriétaires",
|
||||
"parts-queue": "",
|
||||
"phonebook": "",
|
||||
@@ -2049,6 +2071,7 @@
|
||||
"courtesycars": "",
|
||||
"courtesycars-detail": "",
|
||||
"courtesycars-new": "",
|
||||
"dashboard": "",
|
||||
"export-logs": "",
|
||||
"jobs": "",
|
||||
"jobs-active": "",
|
||||
@@ -2086,6 +2109,7 @@
|
||||
"courtesycars": "",
|
||||
"courtesycars-create": "",
|
||||
"courtesycars-detail": "",
|
||||
"dashboard": "",
|
||||
"export-logs": "",
|
||||
"jobs": "Tous les emplois | $t(titles.app)",
|
||||
"jobs-admin": "",
|
||||
|
||||
21298
client/yarn.lock
21298
client/yarn.lock
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
- args:
|
||||
cascade: false
|
||||
read_only: false
|
||||
sql: ALTER TABLE ONLY "public"."users" ALTER COLUMN "dashboardlayout" SET DEFAULT
|
||||
jsonb_build_array();
|
||||
type: run_sql
|
||||
- args:
|
||||
cascade: false
|
||||
read_only: false
|
||||
sql: ALTER TABLE "public"."users" ALTER COLUMN "dashboardlayout" SET NOT NULL;
|
||||
type: run_sql
|
||||
@@ -0,0 +1,10 @@
|
||||
- args:
|
||||
cascade: false
|
||||
read_only: false
|
||||
sql: ALTER TABLE "public"."users" ALTER COLUMN "dashboardlayout" DROP DEFAULT;
|
||||
type: run_sql
|
||||
- args:
|
||||
cascade: false
|
||||
read_only: false
|
||||
sql: ALTER TABLE "public"."users" ALTER COLUMN "dashboardlayout" DROP NOT NULL;
|
||||
type: run_sql
|
||||
@@ -0,0 +1,5 @@
|
||||
- args:
|
||||
cascade: false
|
||||
read_only: false
|
||||
sql: ALTER TABLE "public"."bodyshops" DROP COLUMN "md_jobline_presets";
|
||||
type: run_sql
|
||||
@@ -0,0 +1,6 @@
|
||||
- args:
|
||||
cascade: false
|
||||
read_only: false
|
||||
sql: ALTER TABLE "public"."bodyshops" ADD COLUMN "md_jobline_presets" jsonb NULL
|
||||
DEFAULT jsonb_build_array();
|
||||
type: run_sql
|
||||
@@ -0,0 +1,84 @@
|
||||
- args:
|
||||
role: user
|
||||
table:
|
||||
name: bodyshops
|
||||
schema: public
|
||||
type: drop_select_permission
|
||||
- args:
|
||||
permission:
|
||||
allow_aggregations: false
|
||||
columns:
|
||||
- accountingconfig
|
||||
- address1
|
||||
- address2
|
||||
- appt_alt_transport
|
||||
- appt_colors
|
||||
- appt_length
|
||||
- bill_tax_rates
|
||||
- city
|
||||
- country
|
||||
- created_at
|
||||
- default_adjustment_rate
|
||||
- deliverchecklist
|
||||
- email
|
||||
- enforce_class
|
||||
- enforce_referral
|
||||
- federal_tax_id
|
||||
- id
|
||||
- imexshopid
|
||||
- inhousevendorid
|
||||
- insurance_vendor_id
|
||||
- intakechecklist
|
||||
- jc_hourly_rates
|
||||
- jobsizelimit
|
||||
- logo_img_path
|
||||
- md_categories
|
||||
- md_ccc_rates
|
||||
- md_classes
|
||||
- md_hour_split
|
||||
- md_ins_cos
|
||||
- md_labor_rates
|
||||
- md_messaging_presets
|
||||
- md_notes_presets
|
||||
- md_order_statuses
|
||||
- md_parts_locations
|
||||
- md_payment_types
|
||||
- md_rbac
|
||||
- md_referral_sources
|
||||
- md_responsibility_centers
|
||||
- md_ro_statuses
|
||||
- messagingservicesid
|
||||
- phone
|
||||
- prodtargethrs
|
||||
- production_config
|
||||
- region_config
|
||||
- schedule_end_time
|
||||
- schedule_start_time
|
||||
- scoreboard_target
|
||||
- shopname
|
||||
- shoprates
|
||||
- speedprint
|
||||
- ssbuckets
|
||||
- state
|
||||
- state_tax_id
|
||||
- stripe_acct_id
|
||||
- sub_status
|
||||
- target_touchtime
|
||||
- template_header
|
||||
- textid
|
||||
- updated_at
|
||||
- use_fippa
|
||||
- website
|
||||
- workingdays
|
||||
- zip_post
|
||||
computed_fields: []
|
||||
filter:
|
||||
associations:
|
||||
user:
|
||||
authid:
|
||||
_eq: X-Hasura-User-Id
|
||||
role: user
|
||||
table:
|
||||
name: bodyshops
|
||||
schema: public
|
||||
type: create_select_permission
|
||||
@@ -0,0 +1,85 @@
|
||||
- args:
|
||||
role: user
|
||||
table:
|
||||
name: bodyshops
|
||||
schema: public
|
||||
type: drop_select_permission
|
||||
- args:
|
||||
permission:
|
||||
allow_aggregations: false
|
||||
columns:
|
||||
- accountingconfig
|
||||
- address1
|
||||
- address2
|
||||
- appt_alt_transport
|
||||
- appt_colors
|
||||
- appt_length
|
||||
- bill_tax_rates
|
||||
- city
|
||||
- country
|
||||
- created_at
|
||||
- default_adjustment_rate
|
||||
- deliverchecklist
|
||||
- email
|
||||
- enforce_class
|
||||
- enforce_referral
|
||||
- federal_tax_id
|
||||
- id
|
||||
- imexshopid
|
||||
- inhousevendorid
|
||||
- insurance_vendor_id
|
||||
- intakechecklist
|
||||
- jc_hourly_rates
|
||||
- jobsizelimit
|
||||
- logo_img_path
|
||||
- md_categories
|
||||
- md_ccc_rates
|
||||
- md_classes
|
||||
- md_hour_split
|
||||
- md_ins_cos
|
||||
- md_jobline_presets
|
||||
- md_labor_rates
|
||||
- md_messaging_presets
|
||||
- md_notes_presets
|
||||
- md_order_statuses
|
||||
- md_parts_locations
|
||||
- md_payment_types
|
||||
- md_rbac
|
||||
- md_referral_sources
|
||||
- md_responsibility_centers
|
||||
- md_ro_statuses
|
||||
- messagingservicesid
|
||||
- phone
|
||||
- prodtargethrs
|
||||
- production_config
|
||||
- region_config
|
||||
- schedule_end_time
|
||||
- schedule_start_time
|
||||
- scoreboard_target
|
||||
- shopname
|
||||
- shoprates
|
||||
- speedprint
|
||||
- ssbuckets
|
||||
- state
|
||||
- state_tax_id
|
||||
- stripe_acct_id
|
||||
- sub_status
|
||||
- target_touchtime
|
||||
- template_header
|
||||
- textid
|
||||
- updated_at
|
||||
- use_fippa
|
||||
- website
|
||||
- workingdays
|
||||
- zip_post
|
||||
computed_fields: []
|
||||
filter:
|
||||
associations:
|
||||
user:
|
||||
authid:
|
||||
_eq: X-Hasura-User-Id
|
||||
role: user
|
||||
table:
|
||||
name: bodyshops
|
||||
schema: public
|
||||
type: create_select_permission
|
||||
@@ -0,0 +1,78 @@
|
||||
- args:
|
||||
role: user
|
||||
table:
|
||||
name: bodyshops
|
||||
schema: public
|
||||
type: drop_update_permission
|
||||
- args:
|
||||
permission:
|
||||
columns:
|
||||
- accountingconfig
|
||||
- address1
|
||||
- address2
|
||||
- appt_alt_transport
|
||||
- appt_colors
|
||||
- appt_length
|
||||
- bill_tax_rates
|
||||
- city
|
||||
- country
|
||||
- created_at
|
||||
- default_adjustment_rate
|
||||
- deliverchecklist
|
||||
- email
|
||||
- enforce_class
|
||||
- enforce_referral
|
||||
- federal_tax_id
|
||||
- id
|
||||
- inhousevendorid
|
||||
- insurance_vendor_id
|
||||
- intakechecklist
|
||||
- jc_hourly_rates
|
||||
- logo_img_path
|
||||
- md_categories
|
||||
- md_ccc_rates
|
||||
- md_classes
|
||||
- md_hour_split
|
||||
- md_ins_cos
|
||||
- md_labor_rates
|
||||
- md_messaging_presets
|
||||
- md_notes_presets
|
||||
- md_order_statuses
|
||||
- md_parts_locations
|
||||
- md_payment_types
|
||||
- md_rbac
|
||||
- md_referral_sources
|
||||
- md_responsibility_centers
|
||||
- md_ro_statuses
|
||||
- phone
|
||||
- prodtargethrs
|
||||
- production_config
|
||||
- schedule_end_time
|
||||
- schedule_start_time
|
||||
- scoreboard_target
|
||||
- shopname
|
||||
- shoprates
|
||||
- speedprint
|
||||
- ssbuckets
|
||||
- state
|
||||
- state_tax_id
|
||||
- target_touchtime
|
||||
- updated_at
|
||||
- use_fippa
|
||||
- website
|
||||
- workingdays
|
||||
- zip_post
|
||||
filter:
|
||||
associations:
|
||||
_and:
|
||||
- user:
|
||||
authid:
|
||||
_eq: X-Hasura-User-Id
|
||||
- active:
|
||||
_eq: true
|
||||
set: {}
|
||||
role: user
|
||||
table:
|
||||
name: bodyshops
|
||||
schema: public
|
||||
type: create_update_permission
|
||||
@@ -0,0 +1,79 @@
|
||||
- args:
|
||||
role: user
|
||||
table:
|
||||
name: bodyshops
|
||||
schema: public
|
||||
type: drop_update_permission
|
||||
- args:
|
||||
permission:
|
||||
columns:
|
||||
- accountingconfig
|
||||
- address1
|
||||
- address2
|
||||
- appt_alt_transport
|
||||
- appt_colors
|
||||
- appt_length
|
||||
- bill_tax_rates
|
||||
- city
|
||||
- country
|
||||
- created_at
|
||||
- default_adjustment_rate
|
||||
- deliverchecklist
|
||||
- email
|
||||
- enforce_class
|
||||
- enforce_referral
|
||||
- federal_tax_id
|
||||
- id
|
||||
- inhousevendorid
|
||||
- insurance_vendor_id
|
||||
- intakechecklist
|
||||
- jc_hourly_rates
|
||||
- logo_img_path
|
||||
- md_categories
|
||||
- md_ccc_rates
|
||||
- md_classes
|
||||
- md_hour_split
|
||||
- md_ins_cos
|
||||
- md_jobline_presets
|
||||
- md_labor_rates
|
||||
- md_messaging_presets
|
||||
- md_notes_presets
|
||||
- md_order_statuses
|
||||
- md_parts_locations
|
||||
- md_payment_types
|
||||
- md_rbac
|
||||
- md_referral_sources
|
||||
- md_responsibility_centers
|
||||
- md_ro_statuses
|
||||
- phone
|
||||
- prodtargethrs
|
||||
- production_config
|
||||
- schedule_end_time
|
||||
- schedule_start_time
|
||||
- scoreboard_target
|
||||
- shopname
|
||||
- shoprates
|
||||
- speedprint
|
||||
- ssbuckets
|
||||
- state
|
||||
- state_tax_id
|
||||
- target_touchtime
|
||||
- updated_at
|
||||
- use_fippa
|
||||
- website
|
||||
- workingdays
|
||||
- zip_post
|
||||
filter:
|
||||
associations:
|
||||
_and:
|
||||
- user:
|
||||
authid:
|
||||
_eq: X-Hasura-User-Id
|
||||
- active:
|
||||
_eq: true
|
||||
set: {}
|
||||
role: user
|
||||
table:
|
||||
name: bodyshops
|
||||
schema: public
|
||||
type: create_update_permission
|
||||
@@ -779,6 +779,7 @@ tables:
|
||||
- md_classes
|
||||
- md_hour_split
|
||||
- md_ins_cos
|
||||
- md_jobline_presets
|
||||
- md_labor_rates
|
||||
- md_messaging_presets
|
||||
- md_notes_presets
|
||||
@@ -849,6 +850,7 @@ tables:
|
||||
- md_classes
|
||||
- md_hour_split
|
||||
- md_ins_cos
|
||||
- md_jobline_presets
|
||||
- md_labor_rates
|
||||
- md_messaging_presets
|
||||
- md_notes_presets
|
||||
|
||||
12
package.json
12
package.json
@@ -7,13 +7,13 @@
|
||||
"npm": "6.11.3"
|
||||
},
|
||||
"scripts": {
|
||||
"setup": "npm i && cd client && npm i",
|
||||
"admin": "cd admin && npm start",
|
||||
"client": "cd client && npm start",
|
||||
"setup": "yarn && cd client && yarn",
|
||||
"admin": "cd admin && yarn start",
|
||||
"client": "cd client && yarn start",
|
||||
"server": "nodemon server.js",
|
||||
"build": "cd client && npm run build",
|
||||
"dev": "concurrently --kill-others-on-fail \"npm run server\" \"npm run client\"",
|
||||
"deva": "concurrently --kill-others-on-fail \"npm run server\" \"npm run client\" \"npm run admin\"",
|
||||
"build": "cd client && yarn run build",
|
||||
"dev": "concurrently --kill-others-on-fail \"yarn run server\" \"yarn run client\"",
|
||||
"deva": "concurrently --kill-others-on-fail \"yarn run server\" \"yarn run client\" \"yarn run admin\"",
|
||||
"start": "node server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -31,13 +31,17 @@ exports.default = async (req, res) => {
|
||||
},
|
||||
};
|
||||
|
||||
console.log("***Number of Failed jobs***: ", erroredJobs.length);
|
||||
console.log(
|
||||
"***Number of Failed jobs***: ",
|
||||
erroredJobs.length,
|
||||
JSON.stringify(erroredJobs.map((x) => x.error))
|
||||
);
|
||||
var ret = builder
|
||||
.create(autoHouseObject, {
|
||||
version: "1.0",
|
||||
encoding: "UTF-8",
|
||||
})
|
||||
.end({ pretty: true });
|
||||
.end({ pretty: true, allowEmptyTags: true });
|
||||
|
||||
//***TODO Change filing naming when creating the cron job. IM_ShopInternalName_DDMMYYYY_HHMMSS.xml
|
||||
res.type("application/xml");
|
||||
@@ -48,6 +52,8 @@ exports.default = async (req, res) => {
|
||||
const CreateRepairOrderTag = (job, errorCallback) => {
|
||||
//Level 2
|
||||
|
||||
const repairCosts = CreateCosts(job);
|
||||
|
||||
try {
|
||||
const ret = {
|
||||
RepairOrderInformation: {
|
||||
@@ -63,8 +69,8 @@ const CreateRepairOrderTag = (job, errorCallback) => {
|
||||
ShopState: job.bodyshop.state,
|
||||
ShopZip: job.bodyshop.zip_post,
|
||||
ShopPhone: job.bodyshop.phone,
|
||||
EstimatorID: `${job.est_ct_fn} ${job.est_ct_ln}`,
|
||||
EstimatorName: `${job.est_ct_fn} ${job.est_ct_ln}`,
|
||||
EstimatorID: `${job.est_ct_fn || ""} ${job.est_ct_ln || ""}`,
|
||||
EstimatorName: `${job.est_ct_fn || ""} ${job.est_ct_ln || ""}`,
|
||||
},
|
||||
CustomerInformation: {
|
||||
FirstName: job.ownr_fn,
|
||||
@@ -97,7 +103,7 @@ const CreateRepairOrderTag = (job, errorCallback) => {
|
||||
VehiclePaintCode: null,
|
||||
VehicleTrimCode: null,
|
||||
VehicleBodyStyle: null,
|
||||
DriveableFlag: job.tlos_ind ? "Y" : "N",
|
||||
DriveableFlag: job.driveable ? "Y" : "N",
|
||||
},
|
||||
|
||||
InsuranceInformation: {
|
||||
@@ -251,25 +257,39 @@ const CreateRepairOrderTag = (job, errorCallback) => {
|
||||
},
|
||||
RevisedTotals: {
|
||||
BodyHours: job.job_totals.rates.lab.hours,
|
||||
BodyRepairHours: job.joblines
|
||||
.filter((line) => repairOpCodes.includes(line.lbr_op))
|
||||
.reduce((acc, val) => acc + val.mod_lb_hrs, 0),
|
||||
BodyReplaceHours: job.joblines
|
||||
.filter((line) => replaceOpCodes.includes(line.lbr_op))
|
||||
.reduce((acc, val) => acc + val.mod_lb_hrs, 0),
|
||||
RefinishHours: job.job_totals.rates.lar.hours,
|
||||
MechanicalHours: job.job_totals.rates.lam.hours,
|
||||
StructuralHours: job.job_totals.rates.las.hours,
|
||||
PartsTotal: Dinero(job.job_totals.parts.parts.total).toFormat(
|
||||
AHDineroFormat
|
||||
),
|
||||
PartsTotalCost: 0,
|
||||
PartsTotalCost: repairCosts.PartsTotalCost.toFormat(AHDineroFormat),
|
||||
PartsOEM: Dinero(
|
||||
job.job_totals.parts.parts.list.PAN &&
|
||||
job.job_totals.parts.parts.list.PAN.total
|
||||
).toFormat(AHDineroFormat),
|
||||
PartsOEMCost: 0,
|
||||
)
|
||||
.add(
|
||||
Dinero(
|
||||
job.job_totals.parts.parts.list.PAP &&
|
||||
job.job_totals.parts.parts.list.PAP.total
|
||||
)
|
||||
)
|
||||
.toFormat(AHDineroFormat),
|
||||
PartsOEMCost: repairCosts.PartsOemCost.toFormat(AHDineroFormat),
|
||||
PartsAM: Dinero(
|
||||
job.job_totals.parts.parts.list.PAA &&
|
||||
job.job_totals.parts.parts.list.PAA.total
|
||||
).toFormat(AHDineroFormat),
|
||||
PartsAMCost: 0,
|
||||
PartsAMCost: repairCosts.PartsAMCost.toFormat(AHDineroFormat),
|
||||
PartsReconditioned: null,
|
||||
PartsReconditionedCost: null,
|
||||
PartsReconditionedCost:
|
||||
repairCosts.PartsReconditionedCost.toFormat(AHDineroFormat),
|
||||
PartsRecycled: Dinero(
|
||||
job.job_totals.parts.parts.list.PAR &&
|
||||
job.job_totals.parts.parts.list.PAR.total
|
||||
@@ -389,10 +409,108 @@ const CreateRepairOrderTag = (job, errorCallback) => {
|
||||
};
|
||||
return ret;
|
||||
} catch (error) {
|
||||
console.log("Error calculating job", error);
|
||||
errorCallback(job, error);
|
||||
}
|
||||
};
|
||||
|
||||
const CreateCosts = (job) => {
|
||||
//Create a mapping based on AH Requirements
|
||||
|
||||
const billTotalsByCostCenters = job.bills.reduce((bill_acc, bill_val) => {
|
||||
//At the bill level.
|
||||
bill_val.billlines.map((line_val) => {
|
||||
//At the bill line level.
|
||||
//console.log("JobCostingPartsTable -> line_val", line_val);
|
||||
if (!bill_acc[line_val.cost_center])
|
||||
bill_acc[line_val.cost_center] = Dinero();
|
||||
|
||||
bill_acc[line_val.cost_center] = bill_acc[line_val.cost_center].add(
|
||||
Dinero({
|
||||
amount: Math.round((line_val.actual_cost || 0) * 100),
|
||||
})
|
||||
.multiply(line_val.quantity)
|
||||
.multiply(bill_val.is_credit_memo ? -1 : 1)
|
||||
);
|
||||
|
||||
return null;
|
||||
});
|
||||
return bill_acc;
|
||||
}, {});
|
||||
const materialsHours = { mapaHrs: 0, mashHrs: 0 };
|
||||
//If the hourly rates for job costing are set, add them in.
|
||||
if (job.bodyshop.jc_hourly_rates && job.bodyshop.jc_hourly_rates.mapa) {
|
||||
if (
|
||||
!billTotalsByCostCenters[
|
||||
job.bodyshop.md_responsibility_centers.defaults.costs.MAPA
|
||||
]
|
||||
)
|
||||
billTotalsByCostCenters[
|
||||
job.bodyshop.md_responsibility_centers.defaults.costs.MAPA
|
||||
] = Dinero();
|
||||
billTotalsByCostCenters[
|
||||
job.bodyshop.md_responsibility_centers.defaults.costs.MAPA
|
||||
] = billTotalsByCostCenters[
|
||||
job.bodyshop.md_responsibility_centers.defaults.costs.MAPA
|
||||
].add(
|
||||
Dinero({
|
||||
amount:
|
||||
(job.bodyshop.jc_hourly_rates &&
|
||||
job.bodyshop.jc_hourly_rates.mapa * 100) ||
|
||||
0,
|
||||
}).multiply(materialsHours.mapaHrs)
|
||||
);
|
||||
}
|
||||
const ticketTotalsByCostCenter = job.timetickets.reduce(
|
||||
(ticket_acc, ticket_val) => {
|
||||
//At the invoice level.
|
||||
if (!ticket_acc[ticket_val.cost_center])
|
||||
ticket_acc[ticket_val.cost_center] = Dinero();
|
||||
|
||||
ticket_acc[ticket_val.cost_center] = ticket_acc[
|
||||
ticket_val.cost_center
|
||||
].add(
|
||||
Dinero({
|
||||
amount: Math.round((ticket_val.rate || 0) * 100),
|
||||
}).multiply(ticket_val.actualhrs || ticket_val.productivehrs || 0)
|
||||
);
|
||||
|
||||
return ticket_acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
const defaultCosts = job.bodyshop.md_responsibility_centers.defaults.costs;
|
||||
|
||||
return {
|
||||
PartsTotalCost: Object.keys(billTotalsByCostCenters).reduce((acc, key) => {
|
||||
return acc.add(billTotalsByCostCenters[key]);
|
||||
}, Dinero()),
|
||||
PartsOemCost: (billTotalsByCostCenters[defaultCosts.PAN] || Dinero()).add(
|
||||
billTotalsByCostCenters[defaultCosts.PAP] || Dinero()
|
||||
),
|
||||
PartsAMCost: billTotalsByCostCenters[defaultCosts.PAA] || Dinero(),
|
||||
PartsReconditionedCost: Dinero(),
|
||||
PartsRecycledCost: billTotalsByCostCenters[defaultCosts.PAR] || Dinero(),
|
||||
PartsOtherCost: billTotalsByCostCenters[defaultCosts.PAO] || Dinero(),
|
||||
SubletTotalCost: billTotalsByCostCenters[defaultCosts.PAS] || Dinero(),
|
||||
BodyLaborTotalCost: ticketTotalsByCostCenter[defaultCosts.LAB] || Dinero(),
|
||||
RefinishLaborTotalCost:
|
||||
ticketTotalsByCostCenter[defaultCosts.LAR] || Dinero(),
|
||||
MechanicalLaborTotalCost:
|
||||
ticketTotalsByCostCenter[defaultCosts.LAM] || Dinero(),
|
||||
StructuralLaborTotalCost:
|
||||
ticketTotalsByCostCenter[defaultCosts.LAS] || Dinero(),
|
||||
PMTotalCost: billTotalsByCostCenters[defaultCosts.MAPA] || Dinero(),
|
||||
BMTotalCost: billTotalsByCostCenters[defaultCosts.MASH] || Dinero(),
|
||||
MiscTotalCost: billTotalsByCostCenters[defaultCosts.PAO] || Dinero(),
|
||||
TowingTotalCost: billTotalsByCostCenters[defaultCosts.TOW] || Dinero(),
|
||||
StorageTotalCost: Dinero(),
|
||||
DetailTotal: Dinero(),
|
||||
DetailTotalCost: Dinero(),
|
||||
SalesTaxTotalCost: Dinero(),
|
||||
};
|
||||
};
|
||||
|
||||
const StatusMapping = (status, md_ro_statuses) => {
|
||||
//EST, SCH, ARR, IPR, RDY, DEL, CLO, CAN, UNDEFINED.
|
||||
const {
|
||||
@@ -493,3 +611,6 @@ const generateNullDetailLine = () => {
|
||||
EstimateAmount: null,
|
||||
};
|
||||
};
|
||||
|
||||
const repairOpCodes = ["OP4", "OP9", "OP10"];
|
||||
const replaceOpCodes = ["OP2", "OP5", "OP11", "OP12"];
|
||||
|
||||
@@ -338,6 +338,7 @@ exports.AUTOHOUSE_QUERY = `query AUTOHOUSE_EXPORT($start: timestamptz) {
|
||||
rate_mapa
|
||||
rate_mash
|
||||
job_totals
|
||||
driveable
|
||||
bodyshop {
|
||||
id
|
||||
shopname
|
||||
@@ -350,6 +351,8 @@ exports.AUTOHOUSE_QUERY = `query AUTOHOUSE_EXPORT($start: timestamptz) {
|
||||
md_ro_statuses
|
||||
md_order_statuses
|
||||
autohouseid
|
||||
md_responsibility_centers
|
||||
jc_hourly_rates
|
||||
}
|
||||
joblines (where:{removed: {_eq:false}}){
|
||||
id
|
||||
@@ -366,7 +369,10 @@ exports.AUTOHOUSE_QUERY = `query AUTOHOUSE_EXPORT($start: timestamptz) {
|
||||
part_qty
|
||||
part_type
|
||||
oem_partno
|
||||
billlines (order_by:{bill:{date:desc_nulls_last}}) {
|
||||
lbr_op
|
||||
profitcenter_part
|
||||
profitcenter_labor
|
||||
billlines (order_by:{bill:{date:desc_nulls_last}}) {
|
||||
actual_cost
|
||||
actual_price
|
||||
quantity
|
||||
@@ -377,7 +383,27 @@ exports.AUTOHOUSE_QUERY = `query AUTOHOUSE_EXPORT($start: timestamptz) {
|
||||
invoice_number
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} bills {
|
||||
id
|
||||
federal_tax_rate
|
||||
local_tax_rate
|
||||
state_tax_rate
|
||||
is_credit_memo
|
||||
billlines {
|
||||
actual_cost
|
||||
cost_center
|
||||
id
|
||||
quantity
|
||||
}
|
||||
}
|
||||
timetickets {
|
||||
id
|
||||
rate
|
||||
cost_center
|
||||
actualhrs
|
||||
productivehrs
|
||||
}
|
||||
area_of_damage
|
||||
employee_prep_rel {
|
||||
first_name
|
||||
|
||||
@@ -353,11 +353,12 @@ function CalculateTaxesTotals(job, otherTotals) {
|
||||
|
||||
//Audatex sends additional glass part types. IO-774
|
||||
const BackupGlassTax =
|
||||
job.parts_tax_rates.PAGD ||
|
||||
job.parts_tax_rates.PAGF ||
|
||||
job.parts_tax_rates.PAGP ||
|
||||
job.parts_tax_rates.PAGQ ||
|
||||
job.parts_tax_rates.PAGR;
|
||||
job.parts_tax_rates &&
|
||||
(job.parts_tax_rates.PAGD ||
|
||||
job.parts_tax_rates.PAGF ||
|
||||
job.parts_tax_rates.PAGP ||
|
||||
job.parts_tax_rates.PAGQ ||
|
||||
job.parts_tax_rates.PAGR);
|
||||
|
||||
job.joblines
|
||||
.filter((jl) => !jl.removed)
|
||||
@@ -365,7 +366,7 @@ function CalculateTaxesTotals(job, otherTotals) {
|
||||
if (!val.tax_part || (!val.part_type && IsAdditionalCost(val))) {
|
||||
additionalItemsTax = additionalItemsTax.add(
|
||||
Dinero({ amount: Math.round((val.act_price || 0) * 100) })
|
||||
.multiply(val.part_qty || 1)
|
||||
.multiply(val.part_qty || 0)
|
||||
.percentage(
|
||||
((job.parts_tax_rates &&
|
||||
job.parts_tax_rates["PAN"] &&
|
||||
@@ -376,7 +377,7 @@ function CalculateTaxesTotals(job, otherTotals) {
|
||||
} else {
|
||||
statePartsTax = statePartsTax.add(
|
||||
Dinero({ amount: Math.round((val.act_price || 0) * 100) })
|
||||
.multiply(val.part_qty || 1)
|
||||
.multiply(val.part_qty || 0)
|
||||
.add(
|
||||
Dinero({
|
||||
amount: Math.round((val.act_price || 0) * 100),
|
||||
|
||||
@@ -60,7 +60,7 @@ exports.deleteFiles = async (req, res) => {
|
||||
//delete images returns.push(
|
||||
returns.push(
|
||||
await cloudinary.api.delete_resources(
|
||||
types.raw.map((x) => x.key),
|
||||
types.raw.map((x) => `${x.key}.${x.extension}`),
|
||||
{ resource_type: "raw" }
|
||||
)
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user