Merged in feature/2021-06-18 (pull request #115)

RO form items for checklist dates.

Approved-by: Patrick Fic
This commit is contained in:
Patrick Fic
2021-06-18 20:51:47 +00:00
74 changed files with 13473 additions and 52880 deletions

View File

@@ -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 BabelEdit project file
@@ -3753,6 +3753,27 @@
</concept_node> </concept_node>
</children> </children>
</folder_node> </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> <concept_node>
<name>md_payment_types</name> <name>md_payment_types</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -4858,6 +4879,27 @@
<folder_node> <folder_node>
<name>shop</name> <name>shop</name>
<children> <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> <concept_node>
<name>rbac</name> <name>rbac</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -10511,6 +10553,27 @@
<folder_node> <folder_node>
<name>errors</name> <name>errors</name>
<children> <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> <concept_node>
<name>updatinglayout</name> <name>updatinglayout</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -10534,9 +10597,182 @@
</concept_node> </concept_node>
</children> </children>
</folder_node> </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> <folder_node>
<name>titles</name> <name>titles</name>
<children> <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> <concept_node>
<name>monthlyrevenuegraph</name> <name>monthlyrevenuegraph</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -10558,6 +10794,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </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> <concept_node>
<name>productiondollars</name> <name>productiondollars</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -12449,7 +12706,7 @@
</translations> </translations>
</concept_node> </concept_node>
<concept_node> <concept_node>
<name>submit</name> <name>senderrortosupport</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
<description></description> <description></description>
<comment></comment> <comment></comment>
@@ -12470,7 +12727,7 @@
</translations> </translations>
</concept_node> </concept_node>
<concept_node> <concept_node>
<name>submitticket</name> <name>submit</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
<description></description> <description></description>
<comment></comment> <comment></comment>
@@ -12904,6 +13161,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </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> <concept_node>
<name>hours</name> <name>hours</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -15588,6 +15866,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </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> </children>
</folder_node> </folder_node>
<folder_node> <folder_node>
@@ -20494,6 +20793,69 @@
<folder_node> <folder_node>
<name>labels</name> <name>labels</name>
<children> <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> <concept_node>
<name>additionaltotal</name> <name>additionaltotal</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -21155,6 +21517,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </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> <concept_node>
<name>contracts</name> <name>contracts</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -24009,6 +24392,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </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> <concept_node>
<name>enterbills</name> <name>enterbills</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -24177,6 +24581,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </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> <concept_node>
<name>owners</name> <name>owners</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -34094,6 +34519,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </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> <concept_node>
<name>export-logs</name> <name>export-logs</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -34852,6 +35298,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </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> <concept_node>
<name>export-logs</name> <name>export-logs</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@
"proxy": "http://localhost:5000", "proxy": "http://localhost:5000",
"dependencies": { "dependencies": {
"@apollo/client": "^3.3.17", "@apollo/client": "^3.3.17",
"@craco/craco": "^6.1.2", "@craco/craco": "^5.9.0",
"@fingerprintjs/fingerprintjs": "^3.1.2", "@fingerprintjs/fingerprintjs": "^3.1.2",
"@lourenci/react-kanban": "^2.1.0", "@lourenci/react-kanban": "^2.1.0",
"@sentry/react": "^6.3.6", "@sentry/react": "^6.3.6",
@@ -41,6 +41,7 @@
"react-dom": "^17.0.1", "react-dom": "^17.0.1",
"react-drag-listview": "^0.1.8", "react-drag-listview": "^0.1.8",
"react-grid-gallery": "^0.5.5", "react-grid-gallery": "^0.5.5",
"react-grid-layout": "^1.2.5",
"react-i18next": "^11.8.15", "react-i18next": "^11.8.15",
"react-icons": "^4.2.0", "react-icons": "^4.2.0",
"react-number-format": "^4.5.5", "react-number-format": "^4.5.5",
@@ -76,8 +77,8 @@
"analyze": "source-map-explorer 'build/static/js/*.js'", "analyze": "source-map-explorer 'build/static/js/*.js'",
"start": "craco start", "start": "craco start",
"build": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` craco build", "build": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` craco build",
"build:test": "env-cmd -f .env.test npm run build", "build:test": "env-cmd -f .env.test yarn run build",
"build-deploy:test": "npm run build:test && s3cmd sync build/* s3://imex-online-test && echo '🚀 TESTING Deployed!'", "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", "buildcra": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` react-scripts build",
"test": "craco test", "test": "craco test",
"eject": "react-scripts eject", "eject": "react-scripts eject",

View File

@@ -5,6 +5,7 @@ import { connect } from "react-redux";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { selectBreadcrumbs } from "../../redux/application/application.selectors"; import { selectBreadcrumbs } from "../../redux/application/application.selectors";
import GlobalSearch from "../global-search/global-search.component";
import "./breadcrumbs.styles.scss"; import "./breadcrumbs.styles.scss";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
@@ -14,7 +15,7 @@ const mapStateToProps = createStructuredSelector({
export function BreadCrumbs({ breadcrumbs }) { export function BreadCrumbs({ breadcrumbs }) {
return ( return (
<div className="breadcrumb-container imex-flex-row"> <div className="breadcrumb-container imex-flex-row">
<Breadcrumb separator=">"> <Breadcrumb separator=">" style={{ flex: 1 }}>
<Breadcrumb.Item> <Breadcrumb.Item>
<Link to={`/manage`}> <Link to={`/manage`}>
<HomeFilled /> <HomeFilled />
@@ -30,6 +31,9 @@ export function BreadCrumbs({ breadcrumbs }) {
) )
)} )}
</Breadcrumb> </Breadcrumb>
<div>
<GlobalSearch />
</div>
</div> </div>
); );
} }

View File

@@ -1,17 +1,15 @@
import { useSubscription } from "@apollo/client"; import { useSubscription } from "@apollo/client";
import React from "react"; import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { CONVERSATION_LIST_SUBSCRIPTION } from "../../graphql/conversations.queries"; 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 AlertComponent from "../alert/alert.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component"; import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import ChatAffixComponent from "./chat-affix.component"; import ChatAffixComponent from "./chat-affix.component";
import { Affix } from "antd";
import "./chat-affix.styles.scss"; 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({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
chatVisible: selectChatVisible, chatVisible: selectChatVisible,
@@ -31,22 +29,20 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) {
if (!bodyshop || !bodyshop.messagingservicesid) return <></>; if (!bodyshop || !bodyshop.messagingservicesid) return <></>;
return ( return (
<Affix className={`chat-affix ${chatVisible ? "chat-affix-open" : ""}`}> <div className={`chat-affix ${chatVisible ? "chat-affix-open" : ""}`}>
<div> {bodyshop && bodyshop.messagingservicesid ? (
{bodyshop && bodyshop.messagingservicesid ? ( <ChatAffixComponent
<ChatAffixComponent conversationList={(data && data.conversations) || []}
conversationList={(data && data.conversations) || []} unreadCount={
unreadCount={ (data &&
(data && data.conversations.reduce((acc, val) => {
data.conversations.reduce((acc, val) => { return (acc = acc + val.messages_aggregate.aggregate.count);
return (acc = acc + val.messages_aggregate.aggregate.count); }, 0)) ||
}, 0)) || 0
0 }
} />
/> ) : null}
) : null} </div>
</div>
</Affix>
); );
} }
export default connect(mapStateToProps, null)(ChatAffixContainer); export default connect(mapStateToProps, null)(ChatAffixContainer);

View File

@@ -1,6 +1,11 @@
.chat-affix { .chat-affix {
position: absolute; position: fixed;
left: 2vw;
bottom: 2vh; 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 { .chat-affix-open {

View File

@@ -17,7 +17,7 @@ export function PartsReceiveModalComponent({ bodyshop, form }) {
return ( return (
<div> <div>
<Form.Item name="fleet" label={t("courtesycars.fields.fleetnumber")}> <Form.Item name="plate" label={t("courtesycars.fields.plate")}>
<Input /> <Input />
</Form.Item> </Form.Item>
<Form.Item <Form.Item

View File

@@ -44,8 +44,8 @@ export function ContractsFindModalContainer({
callSearch({ callSearch({
variables: { variables: {
fleet: plate:
(values.fleet && values.fleet !== "" && values.fleet) || undefined, (values.plate && values.plate !== "" && values.plate) || undefined,
time: values.time, time: values.time,
}, },
}); });

View File

@@ -90,13 +90,12 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
// sorter: (a, b) => alphaSort(a.model, b.model), // sorter: (a, b) => alphaSort(a.model, b.model),
sortOrder: sortOrder:
state.sortedInfo.columnKey === "model" && state.sortedInfo.order, state.sortedInfo.columnKey === "model" && state.sortedInfo.order,
render: (text, record) => ( render: (text, record) =>
<div> record.cccontracts.length === 1 ? (
{record.cccontracts.length === 1 <Link to={`/manage/jobs/${record.cccontracts[0].job.id}`}>
? record.cccontracts[0].job.ro_number {record.cccontracts[0].job.ro_number}
: null} </Link>
</div> ) : null,
),
}, },
]; ];

View File

@@ -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
}
`;

View File

@@ -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>
);
}

View File

@@ -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>

View File

@@ -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>
);
};

View File

@@ -2,29 +2,30 @@ import { Card } from "antd";
import moment from "moment"; import moment from "moment";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import _ from "lodash";
import { import {
Area, Area,
Bar, Bar,
CartesianGrid, CartesianGrid,
ComposedChart, ComposedChart,
Legend, Legend,
ResponsiveContainer, ResponsiveContainer,
Tooltip, Tooltip,
XAxis, XAxis,
YAxis YAxis,
} from "recharts"; } from "recharts";
import Dinero from "dinero.js";
import * as Utils from "../../scoreboard-targets-table/scoreboard-targets-table.util"; import * as Utils from "../../scoreboard-targets-table/scoreboard-targets-table.util";
import DashboardRefreshRequired from "../refresh-required.component";
export default function DashboardMonthlyRevenueGraph({ data, ...cardProps }) { export default function DashboardMonthlyRevenueGraph({ data, ...cardProps }) {
const { t } = useTranslation(); const { t } = useTranslation();
if (!data) return null;
if (!data.monthly_sales) return <DashboardRefreshRequired {...cardProps} />;
const jobsByDate = { const jobsByDate = _.groupBy(data.monthly_sales, (item) =>
"2020-07-5": [{ clm_total: 1224 }], moment(item.date_invoiced).format("YYYY-MM-DD")
"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 listOfDays = Utils.ListOfDaysInCurrentMonth(); const listOfDays = Utils.ListOfDaysInCurrentMonth();
@@ -33,17 +34,19 @@ export default function DashboardMonthlyRevenueGraph({ data, ...cardProps }) {
let dailySales; let dailySales;
if (!!jobsByDate[val]) { if (!!jobsByDate[val]) {
dailySales = jobsByDate[val].reduce((dayAcc, dayVal) => { dailySales = jobsByDate[val].reduce((dayAcc, dayVal) => {
return dayAcc + dayVal.clm_total; return dayAcc.add(Dinero(dayVal.job_totals.totals.subtotal));
}, 0); }, Dinero());
} else { } else {
dailySales = 0; dailySales = Dinero();
} }
const theValue = { const theValue = {
date: moment(val).format("D dd"), date: moment(val).format("DD"),
dailySales, dailySales: dailySales.getAmount() / 100,
accSales: 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]; return [...acc, theValue];
@@ -51,32 +54,38 @@ export default function DashboardMonthlyRevenueGraph({ data, ...cardProps }) {
return ( return (
<Card title={t("dashboard.titles.monthlyrevenuegraph")} {...cardProps}> <Card title={t("dashboard.titles.monthlyrevenuegraph")} {...cardProps}>
<ResponsiveContainer width="100%" height="100%"> <div style={{ height: "100%" }}>
<ComposedChart <ResponsiveContainer width="100%" height="100%">
data={chartData} <ComposedChart
margin={{ top: 20, right: 20, bottom: 20, left: 20 }} data={chartData}
> margin={{ top: 20, right: 20, bottom: 20, left: 20 }}
<CartesianGrid stroke="#f5f5f5" /> >
<XAxis dataKey="date" /> <CartesianGrid stroke="#f5f5f5" />
<YAxis /> <XAxis dataKey="date" />
<Tooltip /> <YAxis />
<Legend /> <Tooltip />
<Area <Legend />
type="monotone" <Area
name="Accumulated Sales" type="monotone"
dataKey="accSales" name="Accumulated Sales"
fill="#8884d8" dataKey="accSales"
stroke="#8884d8" fill="#3CB371"
/> stroke="#3CB371"
<Bar />
name="Daily Sales" <Bar
dataKey="dailySales" name="Daily Sales"
//stackId="day" dataKey="dailySales"
barSize={20} //stackId="day"
fill="#413ea0" barSize={20}
/> fill="#413ea0"
</ComposedChart> />
</ResponsiveContainer> </ComposedChart>
</ResponsiveContainer>
</div>
</Card> </Card>
); );
} }
export const DashboardMonthlyRevenueGraphGql = `
`;

View File

@@ -1,30 +1,40 @@
import { ArrowDownOutlined, ArrowUpOutlined } from "@ant-design/icons";
import { Card, Statistic } from "antd"; import { Card, Statistic } from "antd";
import Dinero from "dinero.js";
import moment from "moment";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import DashboardRefreshRequired from "../refresh-required.component";
export default function DashboardProjectedMonthlySales({ data, ...cardProps }) { export default function DashboardProjectedMonthlySales({ data, ...cardProps }) {
const { t } = useTranslation(); 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 ( return (
<Card {...cardProps}> <Card title={t("dashboard.titles.projectedmonthlysales")} {...cardProps}>
<Statistic <Statistic value={dollars.toFormat()} />
title={t("dashboard.titles.projectedmonthlysales")}
value={222000.0}
precision={2}
prefix={
<div>
{aboveTargetMonthlySales ? (
<ArrowUpOutlined />
) : (
<ArrowDownOutlined />
)}
$
</div>
}
valueStyle={{ color: aboveTargetMonthlySales ? "green" : "red" }}
/>
</Card> </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
}
`;

View File

@@ -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>
);
}

View File

@@ -1,33 +1,26 @@
import React from "react";
import { Card, Statistic } from "antd"; import { Card, Statistic } from "antd";
import Dinero from "dinero.js";
import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { ArrowDownOutlined, ArrowUpOutlined } from "@ant-design/icons"; import DashboardRefreshRequired from "../refresh-required.component";
export default function DashboardTotalProductionDollars({ export default function DashboardTotalProductionDollars({
data, data,
...cardProps ...cardProps
}) { }) {
const { t } = useTranslation(); 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 ( return (
<Card {...cardProps}> <Card title={t("dashboard.labels.dollarsinproduction")} {...cardProps}>
<Statistic <Statistic value={dollars.toFormat()} />
title={t("dashboard.titles.productiondollars")}
value={175000.0}
precision={2}
prefix={
<div>
{aboveTargetProductionDollars ? (
<ArrowUpOutlined />
) : (
<ArrowDownOutlined />
)}
$
</div>
}
valueStyle={{ color: aboveTargetProductionDollars ? "green" : "red" }}
/>
</Card> </Card>
); );
} }

View File

@@ -1,19 +1,63 @@
import { Card, Space, Statistic } from "antd";
import React from "react"; import React from "react";
import { Card, Statistic } from "antd";
import { useTranslation } from "react-i18next"; 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 { 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 ( return (
<Card {...cardProps}> <Card {...cardProps} title={t("dashboard.titles.prodhrssummary")}>
<Statistic <Space wrap style={{ flex: 1 }}>
title={t("dashboard.titles.productionhours")} <Statistic
value={750} title={t("dashboard.labels.bodyhrs")}
prefix={aboveTargetHours ? <ArrowUpOutlined /> : <ArrowDownOutlined />} value={hours.body.toFixed(1)}
valueStyle={{ color: aboveTargetHours ? "green" : "red" }} />
/> <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> </Card>
); );
} }
export const DashboardTotalProductionHoursGql = ``;

View File

@@ -1,185 +1,355 @@
// import Icon from "@ant-design/icons"; import Icon, { SyncOutlined } from "@ant-design/icons";
// import { Button, Dropdown, Menu, notification } from "antd"; import { gql, useMutation, useQuery } from "@apollo/client";
// import React, { useState } from "react"; import { Button, Dropdown, Menu, notification, PageHeader, Space } from "antd";
// import { useMutation, useQuery } from "@apollo/client"; import i18next from "i18next";
// import { Responsive, WidthProvider } from "react-grid-layout"; import _ from "lodash";
// import { useTranslation } from "react-i18next"; import moment from "moment";
// import { MdClose } from "react-icons/md"; import React, { useState } from "react";
// import { connect } from "react-redux"; import { Responsive, WidthProvider } from "react-grid-layout";
// import { createStructuredSelector } from "reselect"; import { useTranslation } from "react-i18next";
// import { logImEXEvent } from "../../firebase/firebase.utils"; import { MdClose } from "react-icons/md";
// import { QUERY_DASHBOARD_DETAILS } from "../../graphql/bodyshop.queries"; import { connect } from "react-redux";
// import { UPDATE_DASHBOARD_LAYOUT } from "../../graphql/user.queries"; import { createStructuredSelector } from "reselect";
// import { import { logImEXEvent } from "../../firebase/firebase.utils";
// selectBodyshop, import { UPDATE_DASHBOARD_LAYOUT } from "../../graphql/user.queries";
// selectCurrentUser, import {
// } from "../../redux/user/user.selectors"; selectBodyshop,
// import AlertComponent from "../alert/alert.component"; selectCurrentUser,
// import DashboardMonthlyRevenueGraph from "../dashboard-components/monthly-revenue-graph/monthly-revenue-graph.component"; } from "../../redux/user/user.selectors";
// import DashboardProjectedMonthlySales from "../dashboard-components/pojected-monthly-sales/projected-monthly-sales.component"; import AlertComponent from "../alert/alert.component";
// import DashboardTotalProductionDollars from "../dashboard-components/total-production-dollars/total-production-dollars.component"; import DashboardMonthlyEmployeeEfficiency, {
// import DashboardTotalProductionHours from "../dashboard-components/total-production-hours/total-production-hours.component"; DashboardMonthlyEmployeeEfficiencyGql,
// import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component"; } from "../dashboard-components/monthly-employee-efficiency/monthly-employee-efficiency.component";
// //Combination of the following: import DashboardMonthlyJobCosting from "../dashboard-components/monthly-job-costing/monthly-job-costing.component";
// // /node_modules/react-grid-layout/css/styles.css import DashboardMonthlyLaborSales from "../dashboard-components/monthly-labor-sales/monthly-labor-sales.component";
// // /node_modules/react-resizable/css/styles.css import DashboardMonthlyPartsSales from "../dashboard-components/monthly-parts-sales/monthly-parts-sales.component";
// import "./dashboard-grid.styles.css"; import DashboardMonthlyRevenueGraph, {
// import "./dashboard-grid.styles.scss"; 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({ const mapStateToProps = createStructuredSelector({
// currentUser: selectCurrentUser, currentUser: selectCurrentUser,
// bodyshop: selectBodyshop, bodyshop: selectBodyshop,
// }); });
// const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
// //setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
// }); });
// export function DashboardGridComponent({ currentUser, bodyshop }) { export function DashboardGridComponent({ currentUser, bodyshop }) {
// const { loading, error, data } = useQuery(QUERY_DASHBOARD_DETAILS); const { t } = useTranslation();
// const { t } = useTranslation(); const [state, setState] = useState({
// const [state, setState] = useState({ ...(bodyshop.associations[0].user.dashboardlayout
// layout: bodyshop.associations[0].user.dashboardlayout || [ ? bodyshop.associations[0].user.dashboardlayout
// { i: "ProductionDollars", x: 0, y: 0, w: 2, h: 2 }, : { items: [], layout: {}, layouts: [] }),
// // { i: "ProductionHours", x: 2, y: 0, w: 2, h: 2 }, });
// ],
// });
// const [updateLayout] = useMutation(UPDATE_DASHBOARD_LAYOUT);
// const handleLayoutChange = async (newLayout) => { const { loading, error, data, refetch } = useQuery(
// logImEXEvent("dashboard_change_layout"); createDashboardQuery(state)
// setState({ ...state, layout: newLayout }); );
// const result = await updateLayout({
// variables: { email: currentUser.email, layout: newLayout },
// });
// if (!!result.errors) { const [updateLayout] = useMutation(UPDATE_DASHBOARD_LAYOUT);
// notification["error"]({
// message: t("dashboard.errors.updatinglayout", {
// message: JSON.stringify(result.errors),
// }),
// });
// }
// };
// const handleRemoveComponent = (key) => { const handleLayoutChange = async (layout, layouts) => {
// logImEXEvent("dashboard_remove_component", { name: key }); logImEXEvent("dashboard_change_layout");
// const idxToRemove = state.layout.findIndex((i) => i.i === key); setState({ ...state, layout, layouts });
// const newLayout = state.layout;
// newLayout.splice(idxToRemove, 1);
// handleLayoutChange(newLayout);
// };
// const handleAddComponent = (e) => { const result = await updateLayout({
// logImEXEvent("dashboard_add_component", { name: e }); 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([ items.splice(idxToRemove, 1);
// ...state.layout, setState({ ...state, items });
// { };
// 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,
// },
// ]);
// };
// const onBreakpointChange = (breakpoint, cols) => { const handleAddComponent = (e) => {
// setState({ ...state, breakpoint: breakpoint, cols: cols }); 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 dashboarddata = React.useMemo(
// const addComponentOverlay = ( () => GenerateDashboardData(data),
// <Menu onClick={handleAddComponent}> [data]
// {Object.keys(componentList).map((key) => ( );
// <Menu.Item const existingLayoutKeys = state.items.map((i) => i.i);
// key={key} const addComponentOverlay = (
// value={key} <Menu onClick={handleAddComponent}>
// disabled={existingLayoutKeys.includes(key)} {Object.keys(componentList).map((key) => (
// > <Menu.Item
// {componentList[key].label} key={key}
// </Menu.Item> value={key}
// ))} disabled={existingLayoutKeys.includes(key)}
// </Menu> >
// ); {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 ( return (
// <div> <div>
// <Dropdown overlay={addComponentOverlay} trigger={["click"]}> <PageHeader
// <Button>{t("dashboard.actions.addcomponent")}</Button> extra={
// </Dropdown> <Space>
// <ResponsiveReactGridLayout <Button onClick={() => refetch()}>
// className="layout" <SyncOutlined />
// breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }} </Button>
// cols={{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }} <Dropdown overlay={addComponentOverlay} trigger={["click"]}>
// width="100%" <Button>{t("dashboard.actions.addcomponent")}</Button>
// onLayoutChange={handleLayoutChange} </Dropdown>
// onBreakpointChange={onBreakpointChange} </Space>
// > }
// {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>
// );
// }
// export default connect( <ResponsiveReactGridLayout
// mapStateToProps, className="layout"
// mapDispatchToProps breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }}
// )(DashboardGridComponent); 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 = { export default connect(
// ProductionDollars: { mapStateToProps,
// label: "Production Dollars", mapDispatchToProps
// component: DashboardTotalProductionDollars, )(DashboardGridComponent);
// w: 2,
// h: 1, const componentList = {
// }, ProductionDollars: {
// ProductionHours: { label: i18next.t("dashboard.titles.productiondollars"),
// label: "Production Hours", component: DashboardTotalProductionDollars,
// component: DashboardTotalProductionHours, gqlFragment: null,
// w: 2, w: 1,
// h: 1, h: 1,
// }, minW: 2,
// ProjectedMonthlySales: { minH: 1,
// label: "Projected Monthly Sales", },
// component: DashboardProjectedMonthlySales, ProductionHours: {
// w: 2, label: i18next.t("dashboard.titles.productionhours"),
// h: 1, component: DashboardTotalProductionHours,
// }, gqlFragment: DashboardTotalProductionHoursGql,
// MonthlyRevenueGraph: { w: 3,
// label: "Monthly Sales Graph", h: 1,
// component: DashboardMonthlyRevenueGraph, minW: 3,
// w: 2, minH: 1,
// h: 2, },
// }, 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
}
}
}
}
}
`;
};

View File

@@ -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;
}

View File

@@ -1,12 +1,154 @@
.dashboard-card { .react-resizable {
// background-color: green; 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 { .ant-card-body {
// background-color: red; height: 80%;
height: 100%;
width: 100%; width: 100%;
display: flex; // // background-color: red;
flex-direction: column; // height: 90%;
align-items: center; // 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%;
}
}
}
} }
} }

View File

@@ -0,0 +1,3 @@
export function GenerateDashboardData(data) {
return data;
}

View File

@@ -132,9 +132,13 @@ export const uploadToCloudinary = async (
//Insert the document with the matching key. //Insert the document with the matching key.
let takenat; let takenat;
if (fileType.includes("image")) { 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({ const documentInsert = await client.mutate({
mutation: INSERT_NEW_DOCUMENT, mutation: INSERT_NEW_DOCUMENT,

View File

@@ -5,9 +5,14 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { selectCurrentUser } from "../../redux/user/user.selectors"; import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
bodyshop: selectBodyshop,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
@@ -34,24 +39,41 @@ class ErrorBoundary extends React.Component {
} }
handleErrorSubmit = () => { 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
}
`,
],
]);
---- window.$crisp.push(["do", "chat:open"]);
System Generated Log: // const errorDescription = `**Please add relevant details about what you were doing before you encountered this issue**
${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 // System Generated Log:
)}&customfield_10049=${window.location}&email=${ // ${this.state.error.message}
this.props.currentUser.email // ${this.state.error.stack}
}`; // `;
console.log(`URL`, URL);
window.open(URL, "_blank"); // 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() { render() {
console.log("this.props :>> ", this.props);
const { t } = this.props; const { t } = this.props;
const { error, info } = this.state; const { error, info } = this.state;
if (this.state.hasErrored === true) { if (this.state.hasErrored === true) {
@@ -91,7 +113,7 @@ ${this.state.error.stack}
{t("general.actions.refresh")} {t("general.actions.refresh")}
</Button> </Button>
<Button onClick={this.handleErrorSubmit}> <Button onClick={this.handleErrorSubmit}>
{t("general.actions.submitticket")} {t("general.actions.senderrortosupport")}
</Button> </Button>
</Space> </Space>
} }

View File

@@ -11,9 +11,8 @@ import AlertComponent from "../alert/alert.component";
export default function GlobalSearch() { export default function GlobalSearch() {
const { t } = useTranslation(); const { t } = useTranslation();
const [callSearch, { loading, error, data }] = useLazyQuery( const [callSearch, { loading, error, data }] =
GLOBAL_SEARCH_QUERY useLazyQuery(GLOBAL_SEARCH_QUERY);
);
const executeSearch = (v) => { const executeSearch = (v) => {
if (v && v.variables.search && v.variables.search !== "") callSearch(v); if (v && v.variables.search && v.variables.search !== "") callSearch(v);
@@ -166,10 +165,12 @@ export default function GlobalSearch() {
return ( return (
<AutoComplete <AutoComplete
key="globalsearch"
dropdownMatchSelectWidth={"false"} dropdownMatchSelectWidth={"false"}
options={options} options={options}
onSearch={handleSearch} onSearch={handleSearch}
allowClear allowClear
placeholder={t("general.labels.globalsearch")}
> >
<Input.Search loading={loading} /> <Input.Search loading={loading} />
</AutoComplete> </AutoComplete>

View File

@@ -3,10 +3,12 @@ import Icon, {
BarChartOutlined, BarChartOutlined,
CarFilled, CarFilled,
ClockCircleFilled, ClockCircleFilled,
DashboardFilled,
DollarCircleFilled, DollarCircleFilled,
ExportOutlined, ExportOutlined,
FieldTimeOutlined, FieldTimeOutlined,
FileAddFilled, FileAddFilled,
FileAddOutlined,
FileFilled, FileFilled,
GlobalOutlined, GlobalOutlined,
HomeFilled, HomeFilled,
@@ -45,7 +47,6 @@ import {
import { setModalContext } from "../../redux/modals/modals.actions"; import { setModalContext } from "../../redux/modals/modals.actions";
import { signOutStart } from "../../redux/user/user.actions"; import { signOutStart } from "../../redux/user/user.actions";
import { selectCurrentUser } from "../../redux/user/user.selectors"; import { selectCurrentUser } from "../../redux/user/user.selectors";
import GlobalSearch from "../global-search/global-search.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
@@ -79,12 +80,11 @@ function Header({
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<Layout.Header style={{ display: "flex", alignItems: "center" }}> <Layout.Header>
<Menu <Menu
mode="horizontal" mode="horizontal"
//theme="light" //theme="light"
theme={"dark"} theme={"dark"}
style={{ flex: 1 }}
selectedKeys={[selectedHeader]} selectedKeys={[selectedHeader]}
onClick={handleMenuClick} onClick={handleMenuClick}
subMenuCloseDelay={0.3} subMenuCloseDelay={0.3}
@@ -96,6 +96,7 @@ function Header({
<Link to="/manage/schedule">{t("menus.header.schedule")}</Link> <Link to="/manage/schedule">{t("menus.header.schedule")}</Link>
</Menu.Item> </Menu.Item>
<Menu.SubMenu <Menu.SubMenu
key="jobssubmenu"
icon={<Icon component={FaCarCrash} />} icon={<Icon component={FaCarCrash} />}
title={t("menus.header.jobs")} title={t("menus.header.jobs")}
> >
@@ -110,12 +111,14 @@ function Header({
{t("menus.header.availablejobs")} {t("menus.header.availablejobs")}
</Link> </Link>
</Menu.Item> </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 />}> <Menu.Item key="alljobs" icon={<UnorderedListOutlined />}>
<Link to="/manage/jobs/all">{t("menus.header.alljobs")}</Link> <Link to="/manage/jobs/all">{t("menus.header.alljobs")}</Link>
</Menu.Item> </Menu.Item>
<Menu.Divider /> <Menu.Divider key="div2" />
<Menu.Item key="productionlist" icon={<ScheduleOutlined />}> <Menu.Item key="productionlist" icon={<ScheduleOutlined />}>
<Link to="/manage/production/list"> <Link to="/manage/production/list">
{t("menus.header.productionlist")} {t("menus.header.productionlist")}
@@ -126,13 +129,13 @@ function Header({
{t("menus.header.productionboard")} {t("menus.header.productionboard")}
</Link> </Link>
</Menu.Item> </Menu.Item>
<Menu.Divider /> <Menu.Divider key="div3" />
<Menu.Item key="scoreboard" icon={<LineChartOutlined />}> <Menu.Item key="scoreboard" icon={<LineChartOutlined />}>
<Link to="/manage/scoreboard">{t("menus.header.scoreboard")}</Link> <Link to="/manage/scoreboard">{t("menus.header.scoreboard")}</Link>
</Menu.Item> </Menu.Item>
</Menu.SubMenu> </Menu.SubMenu>
<Menu.SubMenu <Menu.SubMenu
key="customers"
icon={<UserOutlined />} icon={<UserOutlined />}
title={t("menus.header.customers")} title={t("menus.header.customers")}
> >
@@ -144,6 +147,7 @@ function Header({
</Menu.Item> </Menu.Item>
</Menu.SubMenu> </Menu.SubMenu>
<Menu.SubMenu <Menu.SubMenu
key="ccs"
icon={<CarFilled />} icon={<CarFilled />}
title={t("menus.header.courtesycars")} title={t("menus.header.courtesycars")}
> >
@@ -164,6 +168,7 @@ function Header({
</Menu.Item> </Menu.Item>
</Menu.SubMenu> </Menu.SubMenu>
<Menu.SubMenu <Menu.SubMenu
key="accounting"
icon={<DollarCircleFilled />} icon={<DollarCircleFilled />}
title={t("menus.header.accounting")} title={t("menus.header.accounting")}
> >
@@ -185,7 +190,7 @@ function Header({
> >
{t("menus.header.enterbills")} {t("menus.header.enterbills")}
</Menu.Item> </Menu.Item>
<Menu.Divider /> <Menu.Divider key="div4" />
<Menu.Item key="allpayments" icon={<BankFilled />}> <Menu.Item key="allpayments" icon={<BankFilled />}>
<Link to="/manage/payments">{t("menus.header.allpayments")}</Link> <Link to="/manage/payments">{t("menus.header.allpayments")}</Link>
</Menu.Item> </Menu.Item>
@@ -201,7 +206,7 @@ function Header({
<Icon component={FaCreditCard} /> <Icon component={FaCreditCard} />
{t("menus.header.enterpayment")} {t("menus.header.enterpayment")}
</Menu.Item> </Menu.Item>
<Menu.Divider /> <Menu.Divider key="div5" />
<Menu.Item key="timetickets" icon={<FieldTimeOutlined />}> <Menu.Item key="timetickets" icon={<FieldTimeOutlined />}>
<Link to="/manage/timetickets"> <Link to="/manage/timetickets">
@@ -220,9 +225,10 @@ function Header({
> >
{t("menus.header.entertimeticket")} {t("menus.header.entertimeticket")}
</Menu.Item> </Menu.Item>
<Menu.Divider /> <Menu.Divider key="div6" />
<Menu.SubMenu <Menu.SubMenu
key="accountingexport"
title={t("menus.header.export")} title={t("menus.header.export")}
icon={<ExportOutlined />} icon={<ExportOutlined />}
> >
@@ -256,18 +262,17 @@ function Header({
{t("menus.header.temporarydocs")} {t("menus.header.temporarydocs")}
</Link> </Link>
</Menu.Item> </Menu.Item>
<Menu.Item <Menu.SubMenu
key="help" key="shopsubmenu"
onClick={() => { title={t("menus.header.shop")}
window.open("https://help.imex.online/", "_blank"); icon={<SettingOutlined />}
}} >
icon={<Icon component={QuestionCircleFilled} />}
/>
<Menu.SubMenu title={t("menus.header.shop")} icon={<SettingOutlined />}>
<Menu.Item key="shop" icon={<Icon component={GiSettingsKnobs} />}> <Menu.Item key="shop" icon={<Icon component={GiSettingsKnobs} />}>
<Link to="/manage/shop">{t("menus.header.shop_config")}</Link> <Link to="/manage/shop">{t("menus.header.shop_config")}</Link>
</Menu.Item> </Menu.Item>
<Menu.Item key="dashboard" icon={<DashboardFilled />}>
<Link to="/manage/dashboard">{t("menus.header.dashboard")}</Link>
</Menu.Item>
<Menu.Item <Menu.Item
key="reportcenter" key="reportcenter"
icon={<BarChartOutlined />} icon={<BarChartOutlined />}
@@ -293,17 +298,27 @@ function Header({
</Menu.Item> </Menu.Item>
</Menu.SubMenu> </Menu.SubMenu>
<Menu.SubMenu <Menu.SubMenu
style={{ float: "right" }} key="user"
title={ title={
currentUser.displayName || currentUser.displayName ||
currentUser.email || currentUser.email ||
t("general.labels.unknown") t("general.labels.unknown")
} }
> >
<Menu.Item danger onClick={() => signOutStart()}> <Menu.Item key="signout" danger onClick={() => signOutStart()}>
{t("user.actions.signout")} {t("user.actions.signout")}
</Menu.Item> </Menu.Item>
<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={() => { onClick={() => {
window.open("https://imexrescue.com/", "_blank"); window.open("https://imexrescue.com/", "_blank");
}} }}
@@ -317,6 +332,7 @@ function Header({
<Link to="/manage/profile">{t("menus.currentuser.profile")}</Link> <Link to="/manage/profile">{t("menus.currentuser.profile")}</Link>
</Menu.Item> </Menu.Item>
<Menu.SubMenu <Menu.SubMenu
key="langselecter"
title={ title={
<span> <span>
<GlobalOutlined /> <GlobalOutlined />
@@ -335,7 +351,7 @@ function Header({
</Menu.Item> </Menu.Item>
</Menu.SubMenu> </Menu.SubMenu>
</Menu.SubMenu> </Menu.SubMenu>
<Menu.SubMenu style={{ float: "right" }} title={<ClockCircleFilled />}> <Menu.SubMenu key="recent" title={<ClockCircleFilled />}>
{recentItems.map((i, idx) => ( {recentItems.map((i, idx) => (
<Menu.Item key={idx}> <Menu.Item key={idx}>
<Link to={i.url}>{i.label}</Link> <Link to={i.url}>{i.label}</Link>
@@ -343,9 +359,6 @@ function Header({
))} ))}
</Menu.SubMenu> </Menu.SubMenu>
</Menu> </Menu>
<div>
<GlobalSearch />
</div>
</Layout.Header> </Layout.Header>
); );
} }

View File

@@ -82,6 +82,7 @@ export function JobChecklistForm({
...(type === "deliver" && { ...(type === "deliver" && {
scheduled_delivery: values.scheduled_delivery, scheduled_delivery: values.scheduled_delivery,
actual_delivery: values.actual_delivery,
}), }),
...(type === "deliver" && ...(type === "deliver" &&
values.removeFromProduction && { values.removeFromProduction && {
@@ -147,6 +148,7 @@ export function JobChecklistForm({
...(type === "deliver" && { ...(type === "deliver" && {
removeFromProduction: true, removeFromProduction: true,
actual_completion: job && job.actual_completion, actual_completion: job && job.actual_completion,
actual_delivery: job && job.actual_delivery,
}), }),
...formItems ...formItems
.filter((fi) => fi.value) .filter((fi) => fi.value)
@@ -179,21 +181,21 @@ export function JobChecklistForm({
}, },
]} ]}
> >
<DateTimePicker /> <DateTimePicker disabled={readOnly} />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name="scheduled_delivery" name="scheduled_delivery"
label={t("jobs.fields.scheduled_delivery")} label={t("jobs.fields.scheduled_delivery")}
disabled={readOnly} disabled={readOnly}
> >
<DateTimePicker /> <DateTimePicker disabled={readOnly} />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name={["production_vars", "note"]} name={["production_vars", "note"]}
label={t("jobs.fields.production_vars.note")} label={t("jobs.fields.production_vars.note")}
disabled={readOnly} disabled={readOnly}
> >
<Input.TextArea rows={3} /> <Input.TextArea rows={3} disabled={readOnly} />
</Form.Item> </Form.Item>
</div> </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>
<Form.Item <Form.Item
name="removeFromProduction" name="removeFromProduction"

View File

@@ -2,7 +2,6 @@ import React from "react";
import ConfigFormComponents from "../config-form-components/config-form-components.component"; import ConfigFormComponents from "../config-form-components/config-form-components.component";
export default function JobChecklistDisplay({ checklist }) { export default function JobChecklistDisplay({ checklist }) {
console.log("JobChecklistDisplay -> checklist", checklist);
if (!checklist) return <div></div>; if (!checklist) return <div></div>;
return ( return (
<div> <div>

View File

@@ -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);

View File

@@ -3,7 +3,7 @@ import React, { useEffect } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import InputCurrency from "../form-items-formatted/currency-form-item.component"; import InputCurrency from "../form-items-formatted/currency-form-item.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.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({ export default function JobLinesUpsertModalComponent({
visible, visible,
jobLine, jobLine,
@@ -32,6 +32,7 @@ export default function JobLinesUpsertModalComponent({
onOk={() => form.submit()} onOk={() => form.submit()}
okButtonProps={{ loading: loading }} okButtonProps={{ loading: loading }}
onCancel={handleCancel} onCancel={handleCancel}
e
> >
<Form <Form
onFinish={handleFinish} onFinish={handleFinish}
@@ -41,6 +42,9 @@ export default function JobLinesUpsertModalComponent({
form={form} form={form}
> >
<LayoutFormRow grow> <LayoutFormRow grow>
<Form.Item label={t("joblines.fields.line_no")} name="line_no">
<InputNumber />
</Form.Item>
<Form.Item <Form.Item
label={t("joblines.fields.line_desc")} label={t("joblines.fields.line_desc")}
rules={[ rules={[
@@ -53,6 +57,7 @@ export default function JobLinesUpsertModalComponent({
> >
<Input /> <Input />
</Form.Item> </Form.Item>
<JoblinesPreset form={form} />
</LayoutFormRow> </LayoutFormRow>
<LayoutFormRow grow> <LayoutFormRow grow>
<Form.Item label={t("joblines.fields.mod_lbr_ty")} name="mod_lbr_ty"> <Form.Item label={t("joblines.fields.mod_lbr_ty")} name="mod_lbr_ty">

View File

@@ -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 React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
@@ -240,6 +240,26 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
<CurrencyInput /> <CurrencyInput />
</Form.Item> </Form.Item>
</LayoutFormRow> </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> <LayoutFormRow>
<Form.Item label={t("jobs.fields.rate_lab")} name="rate_lab"> <Form.Item label={t("jobs.fields.rate_lab")} name="rate_lab">
<CurrencyInput /> <CurrencyInput />

View File

@@ -1,14 +1,14 @@
import { useQuery } from "@apollo/client";
import React, { useContext } from "react"; 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 JobCreateContext from "../../pages/jobs-create/jobs-create.context";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import { SEARCH_VEHICLE_BY_VIN } from "../../graphql/vehicles.queries"; import JobsCreateVehicleInfoComponent from "./jobs-create-vehicle-info.component";
import { useQuery } from "@apollo/client";
export default function JobsCreateVehicleInfoContainer({ form }) { export default function JobsCreateVehicleInfoContainer({ form }) {
const [state] = useContext(JobCreateContext); const [state] = useContext(JobCreateContext);
const { loading, error, data } = useQuery(SEARCH_VEHICLE_BY_VIN, { const { loading, error, data } = useQuery(SEARCH_VEHICLES, {
variables: { vin: `%${state.vehicle.search}%` }, variables: { search: `%${state.vehicle.search}%` },
skip: !state.vehicle.search, skip: !state.vehicle.search,
}); });
@@ -17,7 +17,7 @@ export default function JobsCreateVehicleInfoContainer({ form }) {
return ( return (
<JobsCreateVehicleInfoComponent <JobsCreateVehicleInfoComponent
loading={loading} loading={loading}
vehicles={data ? data.vehicles : null} vehicles={data ? data.search_vehicles : null}
/> />
); );
} }

View File

@@ -200,7 +200,7 @@ export function JobsDetailHeaderActions({
? t("production.labels.alertoff") ? t("production.labels.alertoff")
: t("production.labels.alerton")} : t("production.labels.alerton")}
</Menu.Item> </Menu.Item>
<Menu.SubMenu title={t("menus.jobsactions.duplicate")}> <Menu.SubMenu key="dupe" title={t("menus.jobsactions.duplicate")}>
<Menu.Item> <Menu.Item>
<Popconfirm <Popconfirm
title={t("jobs.labels.duplicateconfirm")} title={t("jobs.labels.duplicateconfirm")}

View File

@@ -181,6 +181,7 @@ export function JobsDetailHeaderCsi({
return ( return (
<Menu.SubMenu <Menu.SubMenu
key="sendcsi"
title={t("jobs.actions.sendcsi")} title={t("jobs.actions.sendcsi")}
disabled={!job.converted} disabled={!job.converted}
{...props} {...props}

View File

@@ -163,9 +163,9 @@ function JobsDocumentsComponent({
currentImageWillChange={onCurrentImageChange} currentImageWillChange={onCurrentImageChange}
customControls={[ customControls={[
<Button <Button
key="edit-button"
style={{ style={{
float: "right", float: "right",
zIndex: "5", zIndex: "5",
}} }}
onClick={() => { onClick={() => {
@@ -177,7 +177,7 @@ function JobsDocumentsComponent({
if (newWindow) newWindow.opener = null; if (newWindow) newWindow.opener = null;
}} }}
> >
<EditFilled style={{}} /> <EditFilled />
</Button>, </Button>,
]} ]}
onClickImage={(props) => { onClickImage={(props) => {

View File

@@ -37,7 +37,7 @@ export default function JobsDocumentsDeleteButton({
}), }),
}); });
} else { } else {
successfulDeletes.push(key); successfulDeletes.push(key.replace(/\.[^/.]+$/, ""));
} }
}); });
}); });

View File

@@ -139,7 +139,7 @@ export function PartsOrderListTableComponent({
</Button> </Button>
</Popconfirm> </Popconfirm>
<Button <Button
disabled={jobRO} disabled={jobRO ? !record.return : jobRO}
onClick={() => { onClick={() => {
logImEXEvent("parts_order_receive_bill"); logImEXEvent("parts_order_receive_bill");

View File

@@ -20,7 +20,9 @@ export default function PaymentFormTotalPayments({ jobid }) {
if (!data) return <></>; if (!data) return <></>;
const totalPayments = data.jobs_by_pk.payments.reduce((acc, val) => { 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()); }, Dinero());
const balance = const balance =
@@ -39,7 +41,7 @@ export default function PaymentFormTotalPayments({ jobid }) {
<Statistic <Statistic
title={t("payments.labels.balance")} title={t("payments.labels.balance")}
valueStyle={{ color: balance.getAmount() !== 0 ? "red" : "green" }} valueStyle={{ color: balance.getAmount() !== 0 ? "red" : "green" }}
value={balance.toFormat()} value={(balance && balance.toFormat()) || ""}
/> />
)} )}
{!balance && <div>{t("jobs.errors.nofinancial")}</div>} {!balance && <div>{t("jobs.errors.nofinancial")}</div>}

View File

@@ -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 { CardElement, useElements, useStripe } from "@stripe/react-stripe-js";
import { Button, Form, Modal, notification } from "antd"; import { Button, Form, Modal, notification } from "antd";
import axios from "axios"; import axios from "axios";
@@ -7,6 +7,7 @@ import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils"; import { logImEXEvent } from "../../firebase/firebase.utils";
import { GET_JOB_INFO_FOR_STRIPE } from "../../graphql/jobs.queries";
import { import {
INSERT_NEW_PAYMENT, INSERT_NEW_PAYMENT,
UPDATE_PAYMENT, UPDATE_PAYMENT,
@@ -44,6 +45,7 @@ function PaymentModalContainer({
const [enterAgain, setEnterAgain] = useState(false); const [enterAgain, setEnterAgain] = useState(false);
const [insertPayment] = useMutation(INSERT_NEW_PAYMENT); const [insertPayment] = useMutation(INSERT_NEW_PAYMENT);
const [updatePayment] = useMutation(UPDATE_PAYMENT); const [updatePayment] = useMutation(UPDATE_PAYMENT);
const client = useApolloClient();
const stripe = useStripe(); const stripe = useStripe();
const elements = useElements(); const elements = useElements();
const { t } = useTranslation(); const { t } = useTranslation();
@@ -80,13 +82,22 @@ function PaymentModalContainer({
stripe_acct_id: bodyshop.stripe_acct_id, 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( stripePayment = await stripe.confirmCardPayment(
secretKey.data.clientSecret, secretKey.data.clientSecret,
{ {
payment_method: { payment_method: {
card: elements.getElement(CardElement), card: elements.getElement(CardElement),
billing_details: { 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,
}, },
}, },
} }

View File

@@ -54,6 +54,7 @@ const ret = {
"shop:vendors": 2, "shop:vendors": 2,
"shop:rbac": 1, "shop:rbac": 1,
"shop:dashboard": 3,
"shop:templates": 4, "shop:templates": 4,
"temporarydocs:view": 2, "temporarydocs:view": 2,

View File

@@ -53,7 +53,7 @@ export function ScheduleCalendarHeaderGraph({ bodyshop, loadData }) {
name="Ideal Load" name="Ideal Load"
dataKey="target" dataKey="target"
stroke="darkgreen" stroke="darkgreen"
fill="whitr" fill="white"
fillOpacity={0} fillOpacity={0}
/> />
<Radar <Radar

View File

@@ -825,6 +825,180 @@ export default function ShopInfoGeneral({ form }) {
}} }}
</Form.List> </Form.List>
</LayoutFormRow> </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> </div>
); );
} }

View File

@@ -501,6 +501,18 @@ export default function ShopInfoRbacComponent({ form }) {
> >
<InputNumber /> <InputNumber />
</Form.Item> </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 <Form.Item
label={t("bodyshop.fields.rbac.shop.rbac")} label={t("bodyshop.fields.rbac.shop.rbac")}
rules={[ rules={[

View File

@@ -51,7 +51,7 @@ export function TimeTicketShiftContainer({
if (loading) return <LoadingSpinner />; if (loading) return <LoadingSpinner />;
if (error) return <AlertComponent message={error.message} type="error" />; if (error) return <AlertComponent message={error.message} type="error" />;
if (!employeeId) if (!employeeId && !(technician && technician.id))
return ( return (
<div> <div>
<Result <Result

View File

@@ -35,14 +35,18 @@ export const QUERY_ALL_ACTIVE_APPOINTMENTS = gql`
v_model_yr v_model_yr
v_make_desc v_make_desc
v_model_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 { aggregate {
sum { sum {
mod_lb_hrs 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 { aggregate {
sum { sum {
mod_lb_hrs mod_lb_hrs
@@ -128,14 +132,18 @@ export const QUERY_APPOINTMENT_BY_DATE = gql`
v_make_desc v_make_desc
v_model_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 { aggregate {
sum { sum {
mod_lb_hrs 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 { aggregate {
sum { sum {
mod_lb_hrs mod_lb_hrs
@@ -197,14 +205,18 @@ export const QUERY_SCHEDULE_LOAD_DATA = gql`
query QUERY_SCHEDULE_LOAD_DATA($start: timestamptz!, $end: timestamptz!) { query QUERY_SCHEDULE_LOAD_DATA($start: timestamptz!, $end: timestamptz!) {
prodJobs: jobs(where: { inproduction: { _eq: true } }) { prodJobs: jobs(where: { inproduction: { _eq: true } }) {
id id
labhrs: joblines_aggregate(where: { mod_lbr_ty: { _neq: "LAR" } }) { labhrs: joblines_aggregate(
where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }
) {
aggregate { aggregate {
sum { sum {
mod_lb_hrs 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 { aggregate {
sum { sum {
mod_lb_hrs mod_lb_hrs
@@ -216,17 +228,20 @@ export const QUERY_SCHEDULE_LOAD_DATA = gql`
where: { scheduled_completion: { _gte: $start, _lte: $end } } where: { scheduled_completion: { _gte: $start, _lte: $end } }
) { ) {
id id
ro_number ro_number
scheduled_completion scheduled_completion
labhrs: joblines_aggregate(where: { mod_lbr_ty: { _neq: "LAR" } }) { labhrs: joblines_aggregate(
where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }
) {
aggregate { aggregate {
sum { sum {
mod_lb_hrs 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 { aggregate {
sum { sum {
mod_lb_hrs mod_lb_hrs
@@ -237,16 +252,19 @@ export const QUERY_SCHEDULE_LOAD_DATA = gql`
arrJobs: jobs(where: { scheduled_in: { _gte: $start, _lte: $end } }) { arrJobs: jobs(where: { scheduled_in: { _gte: $start, _lte: $end } }) {
id id
scheduled_in scheduled_in
ro_number ro_number
labhrs: joblines_aggregate(where: { mod_lbr_ty: { _neq: "LAR" } }) { labhrs: joblines_aggregate(
where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }
) {
aggregate { aggregate {
sum { sum {
mod_lb_hrs 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 { aggregate {
sum { sum {
mod_lb_hrs mod_lb_hrs

View File

@@ -88,6 +88,7 @@ export const QUERY_BODYSHOP = gql`
enforce_referral enforce_referral
website website
jc_hourly_rates jc_hourly_rates
md_jobline_presets
employees { employees {
id id
active active
@@ -173,6 +174,7 @@ export const UPDATE_SHOP = gql`
enforce_referral enforce_referral
website website
jc_hourly_rates jc_hourly_rates
md_jobline_presets
employees { employees {
id id
first_name 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
}
}
}
}
}
`;

View File

@@ -191,7 +191,7 @@ export const QUERY_ACTIVE_CONTRACTS_PAGINATED = gql`
`; `;
export const FIND_CONTRACT = gql` export const FIND_CONTRACT = gql`
query FIND_CONTRACT($fleet: String, $time: timestamptz!) { query FIND_CONTRACT($plate: String, $time: timestamptz!) {
cccontracts( cccontracts(
where: { where: {
_or: [ _or: [
@@ -199,7 +199,7 @@ export const FIND_CONTRACT = gql`
{ actualreturn: { _is_null: true } } { actualreturn: { _is_null: true } }
] ]
start: { _lte: $time } start: { _lte: $time }
courtesycar: { fleetnumber: { _eq: $fleet } } courtesycar: { plate: { _eq: $plate } }
} }
) { ) {
agreementnumber agreementnumber

View File

@@ -159,6 +159,7 @@ export const UPDATE_JOB_LINE = gql`
db_price db_price
act_price act_price
line_desc line_desc
line_no
oem_partno oem_partno
notes notes
location location

View File

@@ -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` export const UPDATE_JOB_STATUS = gql`
mutation UPDATE_JOB_STATUS($jobId: uuid!, $status: String!) { mutation UPDATE_JOB_STATUS($jobId: uuid!, $status: String!) {
update_jobs(where: { id: { _eq: $jobId } }, _set: { status: $status }) { update_jobs(where: { id: { _eq: $jobId } }, _set: { status: $status }) {
@@ -1696,6 +1710,12 @@ export const QUERY_JOB_CLOSE_DETAILS = gql`
date_exported date_exported
date_invoiced date_invoiced
voided voided
scheduled_completion
actual_completion
scheduled_delivery
actual_delivery
scheduled_in
actual_in
joblines(where: { removed: { _eq: false } }) { joblines(where: { removed: { _eq: false } }) {
id id
removed removed
@@ -1818,6 +1838,7 @@ export const QUERY_JOB_CHECKLISTS = gql`
scheduled_completion scheduled_completion
actual_completion actual_completion
scheduled_delivery scheduled_delivery
actual_delivery
production_vars production_vars
bodyshop { bodyshop {
id id

View File

@@ -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` export const SEARCH_VEHICLES_BY_ID_FOR_AUTOCOMPLETE = gql`
query SEARCH_VEHICLES_BY_ID_FOR_AUTOCOMPLETE($id: uuid!) { query SEARCH_VEHICLES_BY_ID_FOR_AUTOCOMPLETE($id: uuid!) {
vehicles_by_pk(id: $id) { vehicles_by_pk(id: $id) {

View 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);

View File

@@ -1,5 +1,14 @@
import { useApolloClient, useMutation } from "@apollo/client"; 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 React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
@@ -13,6 +22,9 @@ import { generateJobLinesUpdatesForInvoicing } from "../../graphql/jobs-lines.qu
import { UPDATE_JOB } from "../../graphql/jobs.queries"; import { UPDATE_JOB } from "../../graphql/jobs.queries";
import { selectJobReadOnly } from "../../redux/application/application.selectors"; import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { selectBodyshop } from "../../redux/user/user.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({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
jobRO: selectJobReadOnly, jobRO: selectJobReadOnly,
@@ -82,31 +94,110 @@ export function JobsCloseComponent({ job, bodyshop, jobRO }) {
layout="vertical" layout="vertical"
form={form} form={form}
onFinish={handleFinish} 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 scrollToFirstError
> >
<Space> <PageHeader
<JobsCloseAutoAllocate title={t("jobs.labels.closejob", { ro_number: job.ro_number })}
joblines={job.joblines} extra={
form={form} <Space>
disabled={!!job.date_exported || jobRO} <JobsCloseAutoAllocate
/> joblines={job.joblines}
form={form}
disabled={!!job.date_exported || jobRO}
/>
<Popconfirm <Popconfirm
onConfirm={() => form.submit()} onConfirm={() => form.submit()}
disabled={jobRO} disabled={jobRO}
okText={t("general.labels.yes")} okText={t("general.labels.yes")}
cancelText={t("general.labels.no")} cancelText={t("general.labels.no")}
title={t("jobs.labels.closeconfirm")} title={t("jobs.labels.closeconfirm")}
> >
<Button loading={loading} type="danger" disabled={jobRO}> <Button loading={loading} type="danger" disabled={jobRO}>
{t("general.actions.close")} {t("general.actions.close")}
</Button> </Button>
</Popconfirm> </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> </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} /> <JobsCloseLines job={job} />
</Form> </Form>
</div> </div>

View File

@@ -57,7 +57,7 @@ function JobsCreateContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
useEffect(() => { useEffect(() => {
document.title = t("titles.jobs-create"); document.title = t("titles.jobs-create");
setSelectedHeader("availablejobs"); setSelectedHeader("newjob");
setBreadcrumbs([ setBreadcrumbs([
{ link: "/manage/available", label: t("titles.bc.availablejobs") }, { 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_sub_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
tax_lbr_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, 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: { parts_tax_rates: {
PAA: { PAA: {
prt_type: "PAA", prt_type: "PAA",

View File

@@ -158,6 +158,7 @@ const Phonebook = lazy(() => import("../phonebook/phonebook.page.container"));
const EmailTest = lazy(() => const EmailTest = lazy(() =>
import("../../components/email-test/email-test-component") import("../../components/email-test/email-test-component")
); );
const Dashboard = lazy(() => import("../dashboard/dashboard.container"));
const { Content, Footer } = Layout; 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}/help`} component={Help} />
<Route exact path={`${match.path}/emailtest`} component={EmailTest} /> <Route exact path={`${match.path}/emailtest`} component={EmailTest} />
<Route exact path={`${match.path}/dashboard`} component={Dashboard} />
</Suspense> </Suspense>
); );
@@ -376,41 +378,43 @@ export function Manage({ match, conflict, bodyshop }) {
else PageContent = AppRouteTable; else PageContent = AppRouteTable;
return ( return (
<Layout className="layout-container"> <>
<HeaderContainer /> <ChatAffixContainer />
<Layout className="layout-container">
<HeaderContainer />
<Content className="content-container"> <Content className="content-container">
<FcmNotification /> <FcmNotification />
<PartnerPingComponent /> <PartnerPingComponent />
<ErrorBoundary>{PageContent}</ErrorBoundary> <ErrorBoundary>{PageContent}</ErrorBoundary>
<ChatAffixContainer /> <BackTop />
<BackTop /> <Footer>
<Footer> <div
<div style={{
style={{ display: "flex",
display: "flex", flexDirection: "column",
flexDirection: "column", justifyContent: "center",
justifyContent: "center", alignItems: "center",
alignItems: "center", margin: "1rem 0rem",
margin: "1rem 0rem", }}
}} >
> <div style={{ display: "flex" }}>
<div style={{ display: "flex" }}> <div>
<div> {`ImEX Online ${
{`ImEX Online ${ process.env.REACT_APP_GIT_SHA
process.env.REACT_APP_GIT_SHA } - ${preval`module.exports = new Date().toLocaleString("en-US", {timeZone: "America/Los_Angeles"});`}`}
} - ${preval`module.exports = new Date().toLocaleString("en-US", {timeZone: "America/Los_Angeles"});`}`} </div>
<div id="noticeable-widget" style={{ marginLeft: "1rem" }} />
</div> </div>
<div id="noticeable-widget" style={{ marginLeft: "1rem" }} /> <Link to="/about" target="_blank" style={{ color: "#ccc" }}>
Disclaimer & Notices
</Link>
<JiraSupportComponent />
</div> </div>
<Link to="/about" target="_blank" style={{ color: "#ccc" }}> </Footer>
Disclaimer </Content>
</Link> </Layout>
<JiraSupportComponent /> </>
</div>
</Footer>
</Content>
</Layout>
); );
} }
export default connect(mapStateToProps, null)(Manage); export default connect(mapStateToProps, null)(Manage);

View File

@@ -3,6 +3,6 @@
} }
.layout-container { .layout-container {
height: 100vh; // height: 100vh;
overflow-y: auto; // overflow-y: auto;
} }

View File

@@ -156,7 +156,7 @@
"markforreexport": "Mark for Re-export", "markforreexport": "Mark for Re-export",
"new": "New Bill", "new": "New Bill",
"noneselected": "No bill selected.", "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", "retailtotal": "Bills Retail Total",
"state_tax": "Provincial/State Tax", "state_tax": "Provincial/State Tax",
"subtotal": "Subtotal", "subtotal": "Subtotal",
@@ -165,7 +165,7 @@
"successes": { "successes": {
"created": "Invoice added successfully.", "created": "Invoice added successfully.",
"deleted": "Bill deleted successfully.", "deleted": "Bill deleted successfully.",
"exported": "Bill exported successfully." "exported": "Bill(s) exported successfully."
}, },
"validation": { "validation": {
"manualinhouse": "Manual posting to the in house vendor is restricted. ", "manualinhouse": "Manual posting to the in house vendor is restricted. ",
@@ -243,6 +243,7 @@
"street2": "Street 2", "street2": "Street 2",
"zip": "Zip/Postal Code" "zip": "Zip/Postal Code"
}, },
"md_jobline_presets": "Jobline Presets",
"md_payment_types": "Payment Types", "md_payment_types": "Payment Types",
"md_referral_sources": "Referral Sources", "md_referral_sources": "Referral Sources",
"messaginglabel": "Messaging Preset Label", "messaginglabel": "Messaging Preset Label",
@@ -322,6 +323,7 @@
"view": "Shift Clock -> View" "view": "Shift Clock -> View"
}, },
"shop": { "shop": {
"dashboard": "Shop -> Dashboard",
"rbac": "Shop -> RBAC", "rbac": "Shop -> RBAC",
"templates": "Shop -> Templates", "templates": "Shop -> Templates",
"vendors": "Shop -> Vendors" "vendors": "Shop -> Vendors"
@@ -667,12 +669,24 @@
"addcomponent": "Add Component" "addcomponent": "Add Component"
}, },
"errors": { "errors": {
"refreshrequired": "You must refresh the dashboard data to see this component.",
"updatinglayout": "Error saving updated layout {{message}}" "updatinglayout": "Error saving updated layout {{message}}"
}, },
"labels": {
"bodyhrs": "Body Hrs",
"dollarsinproduction": "Dollars in Production",
"prodhrs": "Production Hrs",
"refhrs": "Refinish Hrs"
},
"titles": { "titles": {
"monthlyemployeeefficiency": "Monthly Employee Efficiency",
"monthlyjobcosting": "Monthly Job Costing ",
"monthlylaborsales": "Monthly Labor Sales",
"monthlypartssales": "Monthly Parts Sales",
"monthlyrevenuegraph": "Monthly Revenue Graph", "monthlyrevenuegraph": "Monthly Revenue Graph",
"productiondollars": "Total dollars in production", "prodhrssummary": "Production Hours Summary",
"productionhours": "Total hours in production", "productiondollars": "Total dollars in Production",
"productionhours": "Total hours in Production",
"projectedmonthlysales": "Projected Monthly Sales" "projectedmonthlysales": "Projected Monthly Sales"
} }
}, },
@@ -798,8 +812,8 @@
"save": "Save", "save": "Save",
"saveandnew": "Save and New", "saveandnew": "Save and New",
"selectall": "Select All", "selectall": "Select All",
"senderrortosupport": "Send Error to Support",
"submit": "Submit", "submit": "Submit",
"submitticket": "Submit a Support Ticket",
"view": "View", "view": "View",
"viewreleasenotes": "See What's Changed" "viewreleasenotes": "See What's Changed"
}, },
@@ -825,6 +839,7 @@
"errors": "Errors", "errors": "Errors",
"exceptiontitle": "An error has occurred.", "exceptiontitle": "An error has occurred.",
"friday": "Friday", "friday": "Friday",
"globalsearch": "Global Search",
"hours": "hrs", "hours": "hrs",
"in": "In", "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.", "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", "billref": "Latest Bill",
"edit": "Edit Line", "edit": "Edit Line",
"new": "New Line", "new": "New Line",
"nostatus": "No Status" "nostatus": "No Status",
"presets": "Jobline Presets"
}, },
"successes": { "successes": {
"created": "Job line created successfully.", "created": "Job line created successfully.",
@@ -1234,6 +1250,9 @@
"scheddates": "Schedule Dates" "scheddates": "Schedule Dates"
}, },
"labels": { "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", "additionaltotal": "Additional Total",
"adjustmentrate": "Adjustment Rate", "adjustmentrate": "Adjustment Rate",
"adjustments": "Adjustments", "adjustments": "Adjustments",
@@ -1269,6 +1288,7 @@
"checklistdocuments": "Checklist Documents", "checklistdocuments": "Checklist Documents",
"checklists": "Checklists", "checklists": "Checklists",
"closeconfirm": "Are you sure you want to close this job? This cannot be easily undone.", "closeconfirm": "Are you sure you want to close this job? This cannot be easily undone.",
"closejob": "Close Job {{ro_number}}",
"contracts": "CC Contracts", "contracts": "CC Contracts",
"cost": "Cost", "cost": "Cost",
"cost_labor": "Cost - Labor", "cost_labor": "Cost - Labor",
@@ -1389,7 +1409,7 @@
"delete": "Job deleted successfully.", "delete": "Job deleted successfully.",
"deleted": "Job deleted successfully.", "deleted": "Job deleted successfully.",
"duplicated": "Job duplicated successfully. ", "duplicated": "Job duplicated successfully. ",
"exported": "Job exported successfully. ", "exported": "Job(s) exported successfully. ",
"invoiced": "Job closed and invoiced successfully.", "invoiced": "Job closed and invoiced successfully.",
"partsqueue": "Job added to parts queue.", "partsqueue": "Job added to parts queue.",
"save": "Job saved successfully.", "save": "Job saved successfully.",
@@ -1419,6 +1439,7 @@
"courtesycars-contracts": "Contracts", "courtesycars-contracts": "Contracts",
"courtesycars-newcontract": "New Contract", "courtesycars-newcontract": "New Contract",
"customers": "Customers", "customers": "Customers",
"dashboard": "Dashboard",
"enterbills": "Enter Bills", "enterbills": "Enter Bills",
"enterpayment": "Enter Payments", "enterpayment": "Enter Payments",
"entertimeticket": "Enter Time Tickets", "entertimeticket": "Enter Time Tickets",
@@ -1427,6 +1448,7 @@
"help": "Help", "help": "Help",
"home": "Home", "home": "Home",
"jobs": "Jobs", "jobs": "Jobs",
"newjob": "Create New Job",
"owners": "Owners", "owners": "Owners",
"parts-queue": "Parts Queue", "parts-queue": "Parts Queue",
"phonebook": "Phonebook", "phonebook": "Phonebook",
@@ -1663,7 +1685,7 @@
"totalpayments": "Total Payments" "totalpayments": "Total Payments"
}, },
"successes": { "successes": {
"exported": "Payment exported successfully.", "exported": "Payment(s) exported successfully.",
"payment": "Payment created successfully. ", "payment": "Payment created successfully. ",
"stripe": "Credit card transaction charged successfully." "stripe": "Credit card transaction charged successfully."
} }
@@ -2049,6 +2071,7 @@
"courtesycars": "Courtesy Cars", "courtesycars": "Courtesy Cars",
"courtesycars-detail": "Courtesy Car {{number}}", "courtesycars-detail": "Courtesy Car {{number}}",
"courtesycars-new": "New Courtesy Car", "courtesycars-new": "New Courtesy Car",
"dashboard": "Dashboard",
"export-logs": "Export Logs", "export-logs": "Export Logs",
"jobs": "Jobs", "jobs": "Jobs",
"jobs-active": "Active Jobs", "jobs-active": "Active Jobs",
@@ -2086,6 +2109,7 @@
"courtesycars": "Courtesy Cars | $t(titles.app)", "courtesycars": "Courtesy Cars | $t(titles.app)",
"courtesycars-create": "New Courtesy Car | $t(titles.app)", "courtesycars-create": "New Courtesy Car | $t(titles.app)",
"courtesycars-detail": "Courtesy Car {{id}} | $t(titles.app)", "courtesycars-detail": "Courtesy Car {{id}} | $t(titles.app)",
"dashboard": "Dashboard | $t(titles.app)",
"export-logs": "Export Logs | $t(titles.app)", "export-logs": "Export Logs | $t(titles.app)",
"jobs": "Active Jobs | $t(titles.app)", "jobs": "Active Jobs | $t(titles.app)",
"jobs-admin": "Job {{ro_number}} - Admin | $t(titles.app)", "jobs-admin": "Job {{ro_number}} - Admin | $t(titles.app)",

View File

@@ -243,6 +243,7 @@
"street2": "", "street2": "",
"zip": "" "zip": ""
}, },
"md_jobline_presets": "",
"md_payment_types": "", "md_payment_types": "",
"md_referral_sources": "", "md_referral_sources": "",
"messaginglabel": "", "messaginglabel": "",
@@ -322,6 +323,7 @@
"view": "" "view": ""
}, },
"shop": { "shop": {
"dashboard": "",
"rbac": "", "rbac": "",
"templates": "", "templates": "",
"vendors": "" "vendors": ""
@@ -667,10 +669,22 @@
"addcomponent": "" "addcomponent": ""
}, },
"errors": { "errors": {
"refreshrequired": "",
"updatinglayout": "" "updatinglayout": ""
}, },
"labels": {
"bodyhrs": "",
"dollarsinproduction": "",
"prodhrs": "",
"refhrs": ""
},
"titles": { "titles": {
"monthlyemployeeefficiency": "",
"monthlyjobcosting": "",
"monthlylaborsales": "",
"monthlypartssales": "",
"monthlyrevenuegraph": "", "monthlyrevenuegraph": "",
"prodhrssummary": "",
"productiondollars": "", "productiondollars": "",
"productionhours": "", "productionhours": "",
"projectedmonthlysales": "" "projectedmonthlysales": ""
@@ -798,8 +812,8 @@
"save": "Salvar", "save": "Salvar",
"saveandnew": "", "saveandnew": "",
"selectall": "", "selectall": "",
"senderrortosupport": "",
"submit": "", "submit": "",
"submitticket": "",
"view": "", "view": "",
"viewreleasenotes": "" "viewreleasenotes": ""
}, },
@@ -825,6 +839,7 @@
"errors": "", "errors": "",
"exceptiontitle": "", "exceptiontitle": "",
"friday": "", "friday": "",
"globalsearch": "",
"hours": "", "hours": "",
"in": "en", "in": "en",
"instanceconflictext": "", "instanceconflictext": "",
@@ -980,7 +995,8 @@
"billref": "", "billref": "",
"edit": "Línea de edición", "edit": "Línea de edición",
"new": "Nueva línea", "new": "Nueva línea",
"nostatus": "" "nostatus": "",
"presets": ""
}, },
"successes": { "successes": {
"created": "", "created": "",
@@ -1234,6 +1250,9 @@
"scheddates": "" "scheddates": ""
}, },
"labels": { "labels": {
"actual_completion_inferred": "",
"actual_delivery_inferred": "",
"actual_in_inferred": "",
"additionaltotal": "", "additionaltotal": "",
"adjustmentrate": "", "adjustmentrate": "",
"adjustments": "", "adjustments": "",
@@ -1269,6 +1288,7 @@
"checklistdocuments": "", "checklistdocuments": "",
"checklists": "", "checklists": "",
"closeconfirm": "", "closeconfirm": "",
"closejob": "",
"contracts": "", "contracts": "",
"cost": "", "cost": "",
"cost_labor": "", "cost_labor": "",
@@ -1419,6 +1439,7 @@
"courtesycars-contracts": "", "courtesycars-contracts": "",
"courtesycars-newcontract": "", "courtesycars-newcontract": "",
"customers": "Clientes", "customers": "Clientes",
"dashboard": "",
"enterbills": "", "enterbills": "",
"enterpayment": "", "enterpayment": "",
"entertimeticket": "", "entertimeticket": "",
@@ -1427,6 +1448,7 @@
"help": "", "help": "",
"home": "Casa", "home": "Casa",
"jobs": "Trabajos", "jobs": "Trabajos",
"newjob": "",
"owners": "propietarios", "owners": "propietarios",
"parts-queue": "", "parts-queue": "",
"phonebook": "", "phonebook": "",
@@ -2049,6 +2071,7 @@
"courtesycars": "", "courtesycars": "",
"courtesycars-detail": "", "courtesycars-detail": "",
"courtesycars-new": "", "courtesycars-new": "",
"dashboard": "",
"export-logs": "", "export-logs": "",
"jobs": "", "jobs": "",
"jobs-active": "", "jobs-active": "",
@@ -2086,6 +2109,7 @@
"courtesycars": "", "courtesycars": "",
"courtesycars-create": "", "courtesycars-create": "",
"courtesycars-detail": "", "courtesycars-detail": "",
"dashboard": "",
"export-logs": "", "export-logs": "",
"jobs": "Todos los trabajos | $t(titles.app)", "jobs": "Todos los trabajos | $t(titles.app)",
"jobs-admin": "", "jobs-admin": "",

View File

@@ -243,6 +243,7 @@
"street2": "", "street2": "",
"zip": "" "zip": ""
}, },
"md_jobline_presets": "",
"md_payment_types": "", "md_payment_types": "",
"md_referral_sources": "", "md_referral_sources": "",
"messaginglabel": "", "messaginglabel": "",
@@ -322,6 +323,7 @@
"view": "" "view": ""
}, },
"shop": { "shop": {
"dashboard": "",
"rbac": "", "rbac": "",
"templates": "", "templates": "",
"vendors": "" "vendors": ""
@@ -667,10 +669,22 @@
"addcomponent": "" "addcomponent": ""
}, },
"errors": { "errors": {
"refreshrequired": "",
"updatinglayout": "" "updatinglayout": ""
}, },
"labels": {
"bodyhrs": "",
"dollarsinproduction": "",
"prodhrs": "",
"refhrs": ""
},
"titles": { "titles": {
"monthlyemployeeefficiency": "",
"monthlyjobcosting": "",
"monthlylaborsales": "",
"monthlypartssales": "",
"monthlyrevenuegraph": "", "monthlyrevenuegraph": "",
"prodhrssummary": "",
"productiondollars": "", "productiondollars": "",
"productionhours": "", "productionhours": "",
"projectedmonthlysales": "" "projectedmonthlysales": ""
@@ -798,8 +812,8 @@
"save": "sauvegarder", "save": "sauvegarder",
"saveandnew": "", "saveandnew": "",
"selectall": "", "selectall": "",
"senderrortosupport": "",
"submit": "", "submit": "",
"submitticket": "",
"view": "", "view": "",
"viewreleasenotes": "" "viewreleasenotes": ""
}, },
@@ -825,6 +839,7 @@
"errors": "", "errors": "",
"exceptiontitle": "", "exceptiontitle": "",
"friday": "", "friday": "",
"globalsearch": "",
"hours": "", "hours": "",
"in": "dans", "in": "dans",
"instanceconflictext": "", "instanceconflictext": "",
@@ -980,7 +995,8 @@
"billref": "", "billref": "",
"edit": "Ligne d'édition", "edit": "Ligne d'édition",
"new": "Nouvelle ligne", "new": "Nouvelle ligne",
"nostatus": "" "nostatus": "",
"presets": ""
}, },
"successes": { "successes": {
"created": "", "created": "",
@@ -1234,6 +1250,9 @@
"scheddates": "" "scheddates": ""
}, },
"labels": { "labels": {
"actual_completion_inferred": "",
"actual_delivery_inferred": "",
"actual_in_inferred": "",
"additionaltotal": "", "additionaltotal": "",
"adjustmentrate": "", "adjustmentrate": "",
"adjustments": "", "adjustments": "",
@@ -1269,6 +1288,7 @@
"checklistdocuments": "", "checklistdocuments": "",
"checklists": "", "checklists": "",
"closeconfirm": "", "closeconfirm": "",
"closejob": "",
"contracts": "", "contracts": "",
"cost": "", "cost": "",
"cost_labor": "", "cost_labor": "",
@@ -1419,6 +1439,7 @@
"courtesycars-contracts": "", "courtesycars-contracts": "",
"courtesycars-newcontract": "", "courtesycars-newcontract": "",
"customers": "Les clients", "customers": "Les clients",
"dashboard": "",
"enterbills": "", "enterbills": "",
"enterpayment": "", "enterpayment": "",
"entertimeticket": "", "entertimeticket": "",
@@ -1427,6 +1448,7 @@
"help": "", "help": "",
"home": "Accueil", "home": "Accueil",
"jobs": "Emplois", "jobs": "Emplois",
"newjob": "",
"owners": "Propriétaires", "owners": "Propriétaires",
"parts-queue": "", "parts-queue": "",
"phonebook": "", "phonebook": "",
@@ -2049,6 +2071,7 @@
"courtesycars": "", "courtesycars": "",
"courtesycars-detail": "", "courtesycars-detail": "",
"courtesycars-new": "", "courtesycars-new": "",
"dashboard": "",
"export-logs": "", "export-logs": "",
"jobs": "", "jobs": "",
"jobs-active": "", "jobs-active": "",
@@ -2086,6 +2109,7 @@
"courtesycars": "", "courtesycars": "",
"courtesycars-create": "", "courtesycars-create": "",
"courtesycars-detail": "", "courtesycars-detail": "",
"dashboard": "",
"export-logs": "", "export-logs": "",
"jobs": "Tous les emplois | $t(titles.app)", "jobs": "Tous les emplois | $t(titles.app)",
"jobs-admin": "", "jobs-admin": "",

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,5 @@
- args:
cascade: false
read_only: false
sql: ALTER TABLE "public"."bodyshops" DROP COLUMN "md_jobline_presets";
type: run_sql

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -779,6 +779,7 @@ tables:
- md_classes - md_classes
- md_hour_split - md_hour_split
- md_ins_cos - md_ins_cos
- md_jobline_presets
- md_labor_rates - md_labor_rates
- md_messaging_presets - md_messaging_presets
- md_notes_presets - md_notes_presets
@@ -849,6 +850,7 @@ tables:
- md_classes - md_classes
- md_hour_split - md_hour_split
- md_ins_cos - md_ins_cos
- md_jobline_presets
- md_labor_rates - md_labor_rates
- md_messaging_presets - md_messaging_presets
- md_notes_presets - md_notes_presets

View File

@@ -7,13 +7,13 @@
"npm": "6.11.3" "npm": "6.11.3"
}, },
"scripts": { "scripts": {
"setup": "npm i && cd client && npm i", "setup": "yarn && cd client && yarn",
"admin": "cd admin && npm start", "admin": "cd admin && yarn start",
"client": "cd client && npm start", "client": "cd client && yarn start",
"server": "nodemon server.js", "server": "nodemon server.js",
"build": "cd client && npm run build", "build": "cd client && yarn run build",
"dev": "concurrently --kill-others-on-fail \"npm run server\" \"npm run client\"", "dev": "concurrently --kill-others-on-fail \"yarn run server\" \"yarn run client\"",
"deva": "concurrently --kill-others-on-fail \"npm run server\" \"npm run client\" \"npm run admin\"", "deva": "concurrently --kill-others-on-fail \"yarn run server\" \"yarn run client\" \"yarn run admin\"",
"start": "node server.js" "start": "node server.js"
}, },
"dependencies": { "dependencies": {

View File

@@ -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 var ret = builder
.create(autoHouseObject, { .create(autoHouseObject, {
version: "1.0", version: "1.0",
encoding: "UTF-8", 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 //***TODO Change filing naming when creating the cron job. IM_ShopInternalName_DDMMYYYY_HHMMSS.xml
res.type("application/xml"); res.type("application/xml");
@@ -48,6 +52,8 @@ exports.default = async (req, res) => {
const CreateRepairOrderTag = (job, errorCallback) => { const CreateRepairOrderTag = (job, errorCallback) => {
//Level 2 //Level 2
const repairCosts = CreateCosts(job);
try { try {
const ret = { const ret = {
RepairOrderInformation: { RepairOrderInformation: {
@@ -63,8 +69,8 @@ const CreateRepairOrderTag = (job, errorCallback) => {
ShopState: job.bodyshop.state, ShopState: job.bodyshop.state,
ShopZip: job.bodyshop.zip_post, ShopZip: job.bodyshop.zip_post,
ShopPhone: job.bodyshop.phone, ShopPhone: job.bodyshop.phone,
EstimatorID: `${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}`, EstimatorName: `${job.est_ct_fn || ""} ${job.est_ct_ln || ""}`,
}, },
CustomerInformation: { CustomerInformation: {
FirstName: job.ownr_fn, FirstName: job.ownr_fn,
@@ -97,7 +103,7 @@ const CreateRepairOrderTag = (job, errorCallback) => {
VehiclePaintCode: null, VehiclePaintCode: null,
VehicleTrimCode: null, VehicleTrimCode: null,
VehicleBodyStyle: null, VehicleBodyStyle: null,
DriveableFlag: job.tlos_ind ? "Y" : "N", DriveableFlag: job.driveable ? "Y" : "N",
}, },
InsuranceInformation: { InsuranceInformation: {
@@ -251,25 +257,39 @@ const CreateRepairOrderTag = (job, errorCallback) => {
}, },
RevisedTotals: { RevisedTotals: {
BodyHours: job.job_totals.rates.lab.hours, 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, RefinishHours: job.job_totals.rates.lar.hours,
MechanicalHours: job.job_totals.rates.lam.hours, MechanicalHours: job.job_totals.rates.lam.hours,
StructuralHours: job.job_totals.rates.las.hours, StructuralHours: job.job_totals.rates.las.hours,
PartsTotal: Dinero(job.job_totals.parts.parts.total).toFormat( PartsTotal: Dinero(job.job_totals.parts.parts.total).toFormat(
AHDineroFormat AHDineroFormat
), ),
PartsTotalCost: 0, PartsTotalCost: repairCosts.PartsTotalCost.toFormat(AHDineroFormat),
PartsOEM: Dinero( PartsOEM: Dinero(
job.job_totals.parts.parts.list.PAN && job.job_totals.parts.parts.list.PAN &&
job.job_totals.parts.parts.list.PAN.total 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( PartsAM: Dinero(
job.job_totals.parts.parts.list.PAA && job.job_totals.parts.parts.list.PAA &&
job.job_totals.parts.parts.list.PAA.total job.job_totals.parts.parts.list.PAA.total
).toFormat(AHDineroFormat), ).toFormat(AHDineroFormat),
PartsAMCost: 0, PartsAMCost: repairCosts.PartsAMCost.toFormat(AHDineroFormat),
PartsReconditioned: null, PartsReconditioned: null,
PartsReconditionedCost: null, PartsReconditionedCost:
repairCosts.PartsReconditionedCost.toFormat(AHDineroFormat),
PartsRecycled: Dinero( PartsRecycled: Dinero(
job.job_totals.parts.parts.list.PAR && job.job_totals.parts.parts.list.PAR &&
job.job_totals.parts.parts.list.PAR.total job.job_totals.parts.parts.list.PAR.total
@@ -389,10 +409,108 @@ const CreateRepairOrderTag = (job, errorCallback) => {
}; };
return ret; return ret;
} catch (error) { } catch (error) {
console.log("Error calculating job", error);
errorCallback(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) => { const StatusMapping = (status, md_ro_statuses) => {
//EST, SCH, ARR, IPR, RDY, DEL, CLO, CAN, UNDEFINED. //EST, SCH, ARR, IPR, RDY, DEL, CLO, CAN, UNDEFINED.
const { const {
@@ -493,3 +611,6 @@ const generateNullDetailLine = () => {
EstimateAmount: null, EstimateAmount: null,
}; };
}; };
const repairOpCodes = ["OP4", "OP9", "OP10"];
const replaceOpCodes = ["OP2", "OP5", "OP11", "OP12"];

View File

@@ -338,6 +338,7 @@ exports.AUTOHOUSE_QUERY = `query AUTOHOUSE_EXPORT($start: timestamptz) {
rate_mapa rate_mapa
rate_mash rate_mash
job_totals job_totals
driveable
bodyshop { bodyshop {
id id
shopname shopname
@@ -350,6 +351,8 @@ exports.AUTOHOUSE_QUERY = `query AUTOHOUSE_EXPORT($start: timestamptz) {
md_ro_statuses md_ro_statuses
md_order_statuses md_order_statuses
autohouseid autohouseid
md_responsibility_centers
jc_hourly_rates
} }
joblines (where:{removed: {_eq:false}}){ joblines (where:{removed: {_eq:false}}){
id id
@@ -366,7 +369,10 @@ exports.AUTOHOUSE_QUERY = `query AUTOHOUSE_EXPORT($start: timestamptz) {
part_qty part_qty
part_type part_type
oem_partno 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_cost
actual_price actual_price
quantity quantity
@@ -377,7 +383,27 @@ exports.AUTOHOUSE_QUERY = `query AUTOHOUSE_EXPORT($start: timestamptz) {
invoice_number 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 area_of_damage
employee_prep_rel { employee_prep_rel {
first_name first_name

View File

@@ -353,11 +353,12 @@ function CalculateTaxesTotals(job, otherTotals) {
//Audatex sends additional glass part types. IO-774 //Audatex sends additional glass part types. IO-774
const BackupGlassTax = const BackupGlassTax =
job.parts_tax_rates.PAGD || job.parts_tax_rates &&
job.parts_tax_rates.PAGF || (job.parts_tax_rates.PAGD ||
job.parts_tax_rates.PAGP || job.parts_tax_rates.PAGF ||
job.parts_tax_rates.PAGQ || job.parts_tax_rates.PAGP ||
job.parts_tax_rates.PAGR; job.parts_tax_rates.PAGQ ||
job.parts_tax_rates.PAGR);
job.joblines job.joblines
.filter((jl) => !jl.removed) .filter((jl) => !jl.removed)
@@ -365,7 +366,7 @@ function CalculateTaxesTotals(job, otherTotals) {
if (!val.tax_part || (!val.part_type && IsAdditionalCost(val))) { if (!val.tax_part || (!val.part_type && IsAdditionalCost(val))) {
additionalItemsTax = additionalItemsTax.add( additionalItemsTax = additionalItemsTax.add(
Dinero({ amount: Math.round((val.act_price || 0) * 100) }) Dinero({ amount: Math.round((val.act_price || 0) * 100) })
.multiply(val.part_qty || 1) .multiply(val.part_qty || 0)
.percentage( .percentage(
((job.parts_tax_rates && ((job.parts_tax_rates &&
job.parts_tax_rates["PAN"] && job.parts_tax_rates["PAN"] &&
@@ -376,7 +377,7 @@ function CalculateTaxesTotals(job, otherTotals) {
} else { } else {
statePartsTax = statePartsTax.add( statePartsTax = statePartsTax.add(
Dinero({ amount: Math.round((val.act_price || 0) * 100) }) Dinero({ amount: Math.round((val.act_price || 0) * 100) })
.multiply(val.part_qty || 1) .multiply(val.part_qty || 0)
.add( .add(
Dinero({ Dinero({
amount: Math.round((val.act_price || 0) * 100), amount: Math.round((val.act_price || 0) * 100),

View File

@@ -60,7 +60,7 @@ exports.deleteFiles = async (req, res) => {
//delete images returns.push( //delete images returns.push(
returns.push( returns.push(
await cloudinary.api.delete_resources( await cloudinary.api.delete_resources(
types.raw.map((x) => x.key), types.raw.map((x) => `${x.key}.${x.extension}`),
{ resource_type: "raw" } { resource_type: "raw" }
) )
); );