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
@@ -3753,6 +3753,27 @@
</concept_node>
</children>
</folder_node>
<concept_node>
<name>md_jobline_presets</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>md_payment_types</name>
<definition_loaded>false</definition_loaded>
@@ -4858,6 +4879,27 @@
<folder_node>
<name>shop</name>
<children>
<concept_node>
<name>dashboard</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>rbac</name>
<definition_loaded>false</definition_loaded>
@@ -10511,6 +10553,27 @@
<folder_node>
<name>errors</name>
<children>
<concept_node>
<name>refreshrequired</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>updatinglayout</name>
<definition_loaded>false</definition_loaded>
@@ -10534,9 +10597,182 @@
</concept_node>
</children>
</folder_node>
<folder_node>
<name>labels</name>
<children>
<concept_node>
<name>bodyhrs</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>dollarsinproduction</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>prodhrs</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>refhrs</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
<folder_node>
<name>titles</name>
<children>
<concept_node>
<name>monthlyemployeeefficiency</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>monthlyjobcosting</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>monthlylaborsales</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>monthlypartssales</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>monthlyrevenuegraph</name>
<definition_loaded>false</definition_loaded>
@@ -10558,6 +10794,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>prodhrssummary</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>productiondollars</name>
<definition_loaded>false</definition_loaded>
@@ -12449,7 +12706,7 @@
</translations>
</concept_node>
<concept_node>
<name>submit</name>
<name>senderrortosupport</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
@@ -12470,7 +12727,7 @@
</translations>
</concept_node>
<concept_node>
<name>submitticket</name>
<name>submit</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
@@ -12904,6 +13161,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>globalsearch</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>hours</name>
<definition_loaded>false</definition_loaded>
@@ -15588,6 +15866,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>presets</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
<folder_node>
@@ -20494,6 +20793,69 @@
<folder_node>
<name>labels</name>
<children>
<concept_node>
<name>actual_completion_inferred</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>actual_delivery_inferred</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>actual_in_inferred</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>additionaltotal</name>
<definition_loaded>false</definition_loaded>
@@ -21155,6 +21517,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>closejob</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>contracts</name>
<definition_loaded>false</definition_loaded>
@@ -24009,6 +24392,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>dashboard</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>enterbills</name>
<definition_loaded>false</definition_loaded>
@@ -24177,6 +24581,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>newjob</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>owners</name>
<definition_loaded>false</definition_loaded>
@@ -34094,6 +34519,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>dashboard</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>export-logs</name>
<definition_loaded>false</definition_loaded>
@@ -34852,6 +35298,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>dashboard</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>export-logs</name>
<definition_loaded>false</definition_loaded>

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

View File

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

View File

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

View File

@@ -1,6 +1,11 @@
.chat-affix {
position: absolute;
position: fixed;
left: 2vw;
bottom: 2vh;
z-index: 999;
-webkit-box-shadow: 0px 0px 2px 0px rgba(69, 69, 69, 1);
-moz-box-shadow: 0px 0px 2px 0px rgba(69, 69, 69, 1);
box-shadow: 0px 0px 2px 0px rgba(69, 69, 69, 1);
}
.chat-affix-open {

View File

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

View File

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

View File

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

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

View File

@@ -1,30 +1,40 @@
import { ArrowDownOutlined, ArrowUpOutlined } from "@ant-design/icons";
import { Card, Statistic } from "antd";
import Dinero from "dinero.js";
import moment from "moment";
import React from "react";
import { useTranslation } from "react-i18next";
import DashboardRefreshRequired from "../refresh-required.component";
export default function DashboardProjectedMonthlySales({ data, ...cardProps }) {
const { t } = useTranslation();
const aboveTargetMonthlySales = false;
if (!data) return null;
if (!data.projected_monthly_sales)
return <DashboardRefreshRequired {...cardProps} />;
const dollars =
data.projected_monthly_sales &&
data.projected_monthly_sales.reduce(
(acc, val) => acc.add(Dinero(val.job_totals.totals.subtotal)),
Dinero()
);
return (
<Card {...cardProps}>
<Statistic
title={t("dashboard.titles.projectedmonthlysales")}
value={222000.0}
precision={2}
prefix={
<div>
{aboveTargetMonthlySales ? (
<ArrowUpOutlined />
) : (
<ArrowDownOutlined />
)}
$
</div>
}
valueStyle={{ color: aboveTargetMonthlySales ? "green" : "red" }}
/>
<Card title={t("dashboard.titles.projectedmonthlysales")} {...cardProps}>
<Statistic value={dollars.toFormat()} />
</Card>
);
}
export const DashboardProjectedMonthlySalesGql = `
projected_monthly_sales: jobs(where: {_or: [{_and: [{date_invoiced: {_gte: "${moment()
.startOf("month")
.format("YYYY-MM-DD")}"}}, {date_invoiced: {_lte: "${moment()
.endOf("month")
.format("YYYY-MM-DD")}"}}]}, {_and: [{scheduled_completion: {_gte: "${moment()
.startOf("month")
.format("YYYY-MM-DD")}"}}, {scheduled_completion: {_lte: "${moment()
.endOf("month")
.format("YYYY-MM-DD")}"}}]}]}) {
id
date_invoiced
job_totals
}
`;

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 Dinero from "dinero.js";
import React from "react";
import { useTranslation } from "react-i18next";
import { ArrowDownOutlined, ArrowUpOutlined } from "@ant-design/icons";
import DashboardRefreshRequired from "../refresh-required.component";
export default function DashboardTotalProductionDollars({
data,
...cardProps
}) {
const { t } = useTranslation();
const aboveTargetProductionDollars = false;
if (!data) return null;
if (!data.production_jobs) return <DashboardRefreshRequired {...cardProps} />;
const dollars =
data.production_jobs &&
data.production_jobs.reduce(
(acc, val) => acc.add(Dinero(val.job_totals.totals.subtotal)),
Dinero()
);
return (
<Card {...cardProps}>
<Statistic
title={t("dashboard.titles.productiondollars")}
value={175000.0}
precision={2}
prefix={
<div>
{aboveTargetProductionDollars ? (
<ArrowUpOutlined />
) : (
<ArrowDownOutlined />
)}
$
</div>
}
valueStyle={{ color: aboveTargetProductionDollars ? "green" : "red" }}
/>
<Card title={t("dashboard.labels.dollarsinproduction")} {...cardProps}>
<Statistic value={dollars.toFormat()} />
</Card>
);
}

View File

@@ -1,19 +1,63 @@
import { Card, Space, Statistic } from "antd";
import React from "react";
import { Card, Statistic } from "antd";
import { useTranslation } from "react-i18next";
import { ArrowDownOutlined, ArrowUpOutlined } from "@ant-design/icons";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../../redux/user/user.selectors";
import DashboardRefreshRequired from "../refresh-required.component";
export default function DashboardTotalProductionHours({ data, ...cardProps }) {
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({});
export default connect(
mapStateToProps,
mapDispatchToProps
)(DashboardTotalProductionHours);
export function DashboardTotalProductionHours({
bodyshop,
data,
...cardProps
}) {
const { t } = useTranslation();
const aboveTargetHours = true;
if (!data) return null;
if (!data.production_jobs) return <DashboardRefreshRequired {...cardProps} />;
const hours =
data.production_jobs &&
data.production_jobs.reduce(
(acc, val) => {
return {
body: acc.body + val.labhrs.aggregate.sum.mod_lb_hrs,
ref: acc.ref + val.larhrs.aggregate.sum.mod_lb_hrs,
total:
acc.total +
val.labhrs.aggregate.sum.mod_lb_hrs +
val.larhrs.aggregate.sum.mod_lb_hrs,
};
},
{ body: 0, ref: 0, total: 0 }
);
const aboveTargetHours = hours.total >= bodyshop.prodtargethrs;
return (
<Card {...cardProps}>
<Statistic
title={t("dashboard.titles.productionhours")}
value={750}
prefix={aboveTargetHours ? <ArrowUpOutlined /> : <ArrowDownOutlined />}
valueStyle={{ color: aboveTargetHours ? "green" : "red" }}
/>
<Card {...cardProps} title={t("dashboard.titles.prodhrssummary")}>
<Space wrap style={{ flex: 1 }}>
<Statistic
title={t("dashboard.labels.bodyhrs")}
value={hours.body.toFixed(1)}
/>
<Statistic
title={t("dashboard.labels.refhrs")}
value={hours.ref.toFixed(1)}
/>
<Statistic
title={t("dashboard.labels.prodhrs")}
value={hours.total.toFixed(1)}
valueStyle={{ color: aboveTargetHours ? "green" : "red" }}
/>
</Space>
</Card>
);
}
export const DashboardTotalProductionHoursGql = ``;

View File

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

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 {
// background-color: green;
.react-resizable {
position: relative;
}
.react-resizable-handle {
position: absolute;
width: 20px;
height: 20px;
background-repeat: no-repeat;
background-origin: content-box;
box-sizing: border-box;
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2IDYiIHN0eWxlPSJiYWNrZ3JvdW5kLWNvbG9yOiNmZmZmZmYwMCIgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSI2cHgiIGhlaWdodD0iNnB4Ij48ZyBvcGFjaXR5PSIwLjMwMiI+PHBhdGggZD0iTSA2IDYgTCAwIDYgTCAwIDQuMiBMIDQgNC4yIEwgNC4yIDQuMiBMIDQuMiAwIEwgNiAwIEwgNiA2IEwgNiA2IFoiIGZpbGw9IiMwMDAwMDAiLz48L2c+PC9zdmc+");
background-position: bottom right;
padding: 0 3px 3px 0;
}
.react-resizable-handle-sw {
bottom: 0;
left: 0;
cursor: sw-resize;
transform: rotate(90deg);
}
.react-resizable-handle-se {
bottom: 0;
right: 0;
cursor: se-resize;
}
.react-resizable-handle-nw {
top: 0;
left: 0;
cursor: nw-resize;
transform: rotate(180deg);
}
.react-resizable-handle-ne {
top: 0;
right: 0;
cursor: ne-resize;
transform: rotate(270deg);
}
.react-resizable-handle-w,
.react-resizable-handle-e {
top: 50%;
margin-top: -10px;
cursor: ew-resize;
}
.react-resizable-handle-w {
left: 0;
transform: rotate(135deg);
}
.react-resizable-handle-e {
right: 0;
transform: rotate(315deg);
}
.react-resizable-handle-n,
.react-resizable-handle-s {
left: 50%;
margin-left: -10px;
cursor: ns-resize;
}
.react-resizable-handle-n {
top: 0;
transform: rotate(225deg);
}
.react-resizable-handle-s {
bottom: 0;
transform: rotate(45deg);
}
.react-grid-layout {
position: relative;
transition: height 200ms ease;
}
.react-grid-item {
transition: all 200ms ease;
transition-property: left, top;
}
.react-grid-item.cssTransforms {
transition-property: transform;
}
.react-grid-item.resizing {
z-index: 1;
will-change: width, height;
}
.react-grid-item.react-draggable-dragging {
transition: none;
z-index: 3;
will-change: transform;
}
.react-grid-item.dropping {
visibility: hidden;
}
.react-grid-item.react-grid-placeholder {
background: red;
opacity: 0.2;
transition-duration: 100ms;
z-index: 2;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-o-user-select: none;
user-select: none;
}
.react-grid-item > .react-resizable-handle {
position: absolute;
width: 20px;
height: 20px;
bottom: 0;
right: 0;
cursor: se-resize;
}
.react-grid-item > .react-resizable-handle::after {
content: "";
position: absolute;
right: 3px;
bottom: 3px;
width: 5px;
height: 5px;
border-right: 2px solid rgba(0, 0, 0, 0.4);
border-bottom: 2px solid rgba(0, 0, 0, 0.4);
}
.react-resizable-hide > .react-resizable-handle {
display: none;
}
.dashboard-card {
height: 100%;
width: 100%;
.ant-card-body {
// background-color: red;
height: 100%;
height: 80%;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
// // background-color: red;
// height: 90%;
// width: 100%;
// padding: 8px;
// display: flex;
// flex-direction: column;
// align-items: center;
// justify-content: center;
}
.ant-spin-nested-loading {
height: 100%;
.ant-spin-container {
height: 100%;
.ant-table {
height: 100%;
.ant-table-container {
height: 100%;
}
}
}
}
}

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.
let takenat;
if (fileType.includes("image")) {
const exif = await exifr.parse(file);
try {
const exif = await exifr.parse(file);
takenat = exif && exif.DateTimeOriginal;
takenat = exif && exif.DateTimeOriginal;
} catch (error) {
console.log("Unable to parse image file for EXIF Data");
}
}
const documentInsert = await client.mutate({
mutation: INSERT_NEW_DOCUMENT,

View File

@@ -5,9 +5,14 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectCurrentUser } from "../../redux/user/user.selectors";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
@@ -34,24 +39,41 @@ class ErrorBoundary extends React.Component {
}
handleErrorSubmit = () => {
const errorDescription = `**Please add relevant details about what you were doing before you encountered this issue**
window.$crisp.push([
"do",
"message:send",
[
"text",
`I hit the following error: \n\n
${this.state.error.message}\n\n
${this.state.error.stack}\n\n
URL:${window.location} as ${this.props.currentUser.email} for ${
this.props.bodyshop && this.props.bodyshop.name
}
`,
],
]);
----
System Generated Log:
${this.state.error.message}
${this.state.error.stack}
`;
window.$crisp.push(["do", "chat:open"]);
// const errorDescription = `**Please add relevant details about what you were doing before you encountered this issue**
const URL = `https://bodyshop.atlassian.net/servicedesk/customer/portal/3/group/8/create/26?summary=123&description=${encodeURI(
errorDescription
)}&customfield_10049=${window.location}&email=${
this.props.currentUser.email
}`;
console.log(`URL`, URL);
window.open(URL, "_blank");
// ----
// System Generated Log:
// ${this.state.error.message}
// ${this.state.error.stack}
// `;
// const URL = `https://bodyshop.atlassian.net/servicedesk/customer/portal/3/group/8/create/26?summary=123&description=${encodeURI(
// errorDescription
// )}&customfield_10049=${window.location}&email=${
// this.props.currentUser.email
// }`;
// console.log(`URL`, URL);
// window.open(URL, "_blank");
};
render() {
console.log("this.props :>> ", this.props);
const { t } = this.props;
const { error, info } = this.state;
if (this.state.hasErrored === true) {
@@ -91,7 +113,7 @@ ${this.state.error.stack}
{t("general.actions.refresh")}
</Button>
<Button onClick={this.handleErrorSubmit}>
{t("general.actions.submitticket")}
{t("general.actions.senderrortosupport")}
</Button>
</Space>
}

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

@@ -825,6 +825,180 @@ export default function ShopInfoGeneral({ form }) {
}}
</Form.List>
</LayoutFormRow>
<LayoutFormRow grow header={t("bodyshop.fields.md_jobline_presets")}>
<Form.List name={["md_jobline_presets"]}>
{(fields, { add, remove, move }) => {
return (
<div>
{fields.map((field, index) => (
<Form.Item key={field.key}>
<LayoutFormRow noDivider>
<Form.Item
label={t("general.labels.label")}
key={`${index}label`}
name={[field.name, "label"]}
>
<Input />
</Form.Item>
<Form.Item
label={t("joblines.fields.line_desc")}
key={`${index}line_desc`}
name={[field.name, "line_desc"]}
>
<Input />
</Form.Item>
<Form.Item
label={t("joblines.fields.mod_lbr_ty")}
key={`${index}mod_lbr_ty`}
name={[field.name, "mod_lbr_ty"]}
>
<Select allowClear>
<Select.Option value="LAA">
{t("joblines.fields.lbr_types.LAA")}
</Select.Option>
<Select.Option value="LAB">
{t("joblines.fields.lbr_types.LAB")}
</Select.Option>
<Select.Option value="LAD">
{t("joblines.fields.lbr_types.LAD")}
</Select.Option>
<Select.Option value="LAE">
{t("joblines.fields.lbr_types.LAE")}
</Select.Option>
<Select.Option value="LAF">
{t("joblines.fields.lbr_types.LAF")}
</Select.Option>
<Select.Option value="LAG">
{t("joblines.fields.lbr_types.LAG")}
</Select.Option>
<Select.Option value="LAM">
{t("joblines.fields.lbr_types.LAM")}
</Select.Option>
<Select.Option value="LAR">
{t("joblines.fields.lbr_types.LAR")}
</Select.Option>
<Select.Option value="LAS">
{t("joblines.fields.lbr_types.LAS")}
</Select.Option>
<Select.Option value="LAU">
{t("joblines.fields.lbr_types.LAU")}
</Select.Option>
<Select.Option value="LA1">
{t("joblines.fields.lbr_types.LA1")}
</Select.Option>
<Select.Option value="LA2">
{t("joblines.fields.lbr_types.LA2")}
</Select.Option>
<Select.Option value="LA3">
{t("joblines.fields.lbr_types.LA3")}
</Select.Option>
<Select.Option value="LA4">
{t("joblines.fields.lbr_types.LA4")}
</Select.Option>
</Select>
</Form.Item>
<Form.Item
label={t("joblines.fields.mod_lb_hrs")}
key={`${index}mod_lb_hrs`}
name={[field.name, "mod_lb_hrs"]}
>
<InputNumber precision={1} />
</Form.Item>
<Form.Item
label={t("joblines.fields.part_type")}
key={`${index}part_type`}
name={[field.name, "part_type"]}
>
<Select allowClear>
<Select.Option value="PAA">
{t("joblines.fields.part_types.PAA")}
</Select.Option>
<Select.Option value="PAC">
{t("joblines.fields.part_types.PAC")}
</Select.Option>
<Select.Option value="PAE">
{t("joblines.fields.part_types.PAE")}
</Select.Option>
<Select.Option value="PAL">
{t("joblines.fields.part_types.PAL")}
</Select.Option>
<Select.Option value="PAM">
{t("joblines.fields.part_types.PAM")}
</Select.Option>
<Select.Option value="PAN">
{t("joblines.fields.part_types.PAN")}
</Select.Option>
<Select.Option value="PAO">
{t("joblines.fields.part_types.PAO")}
</Select.Option>
<Select.Option value="PAR">
{t("joblines.fields.part_types.PAR")}
</Select.Option>
<Select.Option value="PAS">
{t("joblines.fields.part_types.PAS")}
</Select.Option>
</Select>
</Form.Item>
<Form.Item
label={t("joblines.fields.oem_partno")}
key={`${index}oem_partno`}
name={[field.name, "oem_partno"]}
>
<Input />
</Form.Item>
<Form.Item
label={t("joblines.fields.part_qty")}
key={`${index}part_qty`}
name={[field.name, "part_qty"]}
>
<CurrencyInput precision={2} min={0} />
</Form.Item>
<Form.Item
label={t("joblines.fields.act_price")}
key={`${index}act_price`}
name={[field.name, "act_price"]}
>
<CurrencyInput precision={2} min={0} />
</Form.Item>
<Form.Item
label={t("joblines.fields.prt_dsmk_p")}
key={`${index}prt_dsmk_p`}
name={[field.name, "prt_dsmk_p"]}
>
<InputNumber precision={0} min={0} max={100} />
</Form.Item>
<Space wrap>
<DeleteFilled
onClick={() => {
remove(field.name);
}}
/>
<FormListMoveArrows
move={move}
index={index}
total={fields.length}
/>
</Space>
</LayoutFormRow>
</Form.Item>
))}
<Form.Item>
<Button
type="dashed"
onClick={() => {
add();
}}
style={{ width: "100%" }}
>
{t("general.actions.add")}
</Button>
</Form.Item>
</div>
);
}}
</Form.List>
</LayoutFormRow>
</div>
);
}

View File

@@ -501,6 +501,18 @@ export default function ShopInfoRbacComponent({ form }) {
>
<InputNumber />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.rbac.shop.dashboard")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
name={["md_rbac", "shop:dashboard"]}
>
<InputNumber />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.rbac.shop.rbac")}
rules={[

View File

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

View File

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

View File

@@ -88,6 +88,7 @@ export const QUERY_BODYSHOP = gql`
enforce_referral
website
jc_hourly_rates
md_jobline_presets
employees {
id
active
@@ -173,6 +174,7 @@ export const UPDATE_SHOP = gql`
enforce_referral
website
jc_hourly_rates
md_jobline_presets
employees {
id
first_name
@@ -243,33 +245,3 @@ export const QUERY_STRIPE_ID = gql`
}
}
`;
export const QUERY_DASHBOARD_DETAILS = gql`
query QUERY_DASHBOARD_DETAILS {
jobs {
id
clm_total
scheduled_completion
date_invoiced
ins_co_nm
}
compJobs: jobs(where: { inproduction: { _eq: true } }) {
id
scheduled_completion
labhrs: joblines_aggregate(where: { mod_lbr_ty: { _neq: "LAR" } }) {
aggregate {
sum {
mod_lb_hrs
}
}
}
larhrs: joblines_aggregate(where: { mod_lbr_ty: { _eq: "LAR" } }) {
aggregate {
sum {
mod_lb_hrs
}
}
}
}
}
`;

View File

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

View File

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

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`
query SEARCH_VEHICLES_BY_ID_FOR_AUTOCOMPLETE($id: uuid!) {
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 { Button, Form, notification, Popconfirm, Space } from "antd";
import {
Button,
Form,
notification,
Popconfirm,
Space,
Alert,
Divider,
PageHeader,
} from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -13,6 +22,9 @@ import { generateJobLinesUpdatesForInvoicing } from "../../graphql/jobs-lines.qu
import { UPDATE_JOB } from "../../graphql/jobs.queries";
import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import LayoutFormRow from "../../components/layout-form-row/layout-form-row.component";
import DateTimePicker from "../../components/form-date-time-picker/form-date-time-picker.component";
import moment from "moment";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
jobRO: selectJobReadOnly,
@@ -82,31 +94,110 @@ export function JobsCloseComponent({ job, bodyshop, jobRO }) {
layout="vertical"
form={form}
onFinish={handleFinish}
initialValues={{ joblines: job.joblines }}
initialValues={{
joblines: job.joblines,
actual_in: job.actual_in
? moment(job.actual_in)
: job.scheduled_in && moment(job.scheduled_in),
actual_completion: job.actual_completion
? moment(job.actual_completion)
: job.scheduled_completion && moment(job.scheduled_completion),
actual_delivery: job.actual_delivery
? moment(job.actual_delivery)
: job.scheduled_delivery && moment(job.scheduled_delivery),
}}
scrollToFirstError
>
<Space>
<JobsCloseAutoAllocate
joblines={job.joblines}
form={form}
disabled={!!job.date_exported || jobRO}
/>
<PageHeader
title={t("jobs.labels.closejob", { ro_number: job.ro_number })}
extra={
<Space>
<JobsCloseAutoAllocate
joblines={job.joblines}
form={form}
disabled={!!job.date_exported || jobRO}
/>
<Popconfirm
onConfirm={() => form.submit()}
disabled={jobRO}
okText={t("general.labels.yes")}
cancelText={t("general.labels.no")}
title={t("jobs.labels.closeconfirm")}
>
<Button loading={loading} type="danger" disabled={jobRO}>
{t("general.actions.close")}
</Button>
</Popconfirm>
<Popconfirm
onConfirm={() => form.submit()}
disabled={jobRO}
okText={t("general.labels.yes")}
cancelText={t("general.labels.no")}
title={t("jobs.labels.closeconfirm")}
>
<Button loading={loading} type="danger" disabled={jobRO}>
{t("general.actions.close")}
</Button>
</Popconfirm>
<JobsScoreboardAdd job={job} disabled={false} />
<JobsScoreboardAdd job={job} disabled={false} />
</Space>
}
/>
<Space wrap direction="vertical" style={{ width: "100%" }}>
<FormsFieldChanged form={form} />
{!job.actual_in && job.scheduled_in && (
<Alert
type="warning"
message={t("jobs.labels.actual_in_inferred")}
/>
)}
{!job.actual_completion && job.scheduled_completion && (
<Alert
type="warning"
message={t("jobs.labels.actual_completion_inferred")}
/>
)}
{!job.actual_delivery && job.scheduled_delivery && (
<Alert
type="warning"
message={t("jobs.labels.actual_delivery_inferred")}
/>
)}
</Space>
<FormsFieldChanged form={form} />
<LayoutFormRow>
<Form.Item
label={t("jobs.fields.actual_in")}
rules={[
{
required: true,
},
]}
name="actual_in"
>
<DateTimePicker disabled={jobRO} />
</Form.Item>
<Form.Item shouldUpdate>
{() => {
return (
<Form.Item
label={t("jobs.fields.actual_completion")}
name="actual_completion"
rules={[
{
required: true,
},
]}
>
<DateTimePicker disabled={jobRO} />
</Form.Item>
);
}}
</Form.Item>
<Form.Item
label={t("jobs.fields.actual_delivery")}
name="actual_delivery"
rules={[
{
required: true,
},
]}
>
<DateTimePicker disabled={jobRO} />
</Form.Item>
</LayoutFormRow>
<Divider />
<JobsCloseLines job={job} />
</Form>
</div>

View File

@@ -57,7 +57,7 @@ function JobsCreateContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
useEffect(() => {
document.title = t("titles.jobs-create");
setSelectedHeader("availablejobs");
setSelectedHeader("newjob");
setBreadcrumbs([
{ link: "/manage/available", label: t("titles.bc.availablejobs") },
{
@@ -162,6 +162,9 @@ function JobsCreateContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
tax_sub_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
tax_lbr_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
tax_levies_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
federal_tax_rate: bodyshop.bill_tax_rates.federal_tax_rate / 100,
state_tax_rate: bodyshop.bill_tax_rates.state_tax_rate / 100,
local_tax_rate: bodyshop.bill_tax_rates.local_tax_rate / 100,
parts_tax_rates: {
PAA: {
prt_type: "PAA",

View File

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

View File

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

View File

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

View File

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

View File

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

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_hour_split
- md_ins_cos
- md_jobline_presets
- md_labor_rates
- md_messaging_presets
- md_notes_presets
@@ -849,6 +850,7 @@ tables:
- md_classes
- md_hour_split
- md_ins_cos
- md_jobline_presets
- md_labor_rates
- md_messaging_presets
- md_notes_presets

View File

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

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
.create(autoHouseObject, {
version: "1.0",
encoding: "UTF-8",
})
.end({ pretty: true });
.end({ pretty: true, allowEmptyTags: true });
//***TODO Change filing naming when creating the cron job. IM_ShopInternalName_DDMMYYYY_HHMMSS.xml
res.type("application/xml");
@@ -48,6 +52,8 @@ exports.default = async (req, res) => {
const CreateRepairOrderTag = (job, errorCallback) => {
//Level 2
const repairCosts = CreateCosts(job);
try {
const ret = {
RepairOrderInformation: {
@@ -63,8 +69,8 @@ const CreateRepairOrderTag = (job, errorCallback) => {
ShopState: job.bodyshop.state,
ShopZip: job.bodyshop.zip_post,
ShopPhone: job.bodyshop.phone,
EstimatorID: `${job.est_ct_fn} ${job.est_ct_ln}`,
EstimatorName: `${job.est_ct_fn} ${job.est_ct_ln}`,
EstimatorID: `${job.est_ct_fn || ""} ${job.est_ct_ln || ""}`,
EstimatorName: `${job.est_ct_fn || ""} ${job.est_ct_ln || ""}`,
},
CustomerInformation: {
FirstName: job.ownr_fn,
@@ -97,7 +103,7 @@ const CreateRepairOrderTag = (job, errorCallback) => {
VehiclePaintCode: null,
VehicleTrimCode: null,
VehicleBodyStyle: null,
DriveableFlag: job.tlos_ind ? "Y" : "N",
DriveableFlag: job.driveable ? "Y" : "N",
},
InsuranceInformation: {
@@ -251,25 +257,39 @@ const CreateRepairOrderTag = (job, errorCallback) => {
},
RevisedTotals: {
BodyHours: job.job_totals.rates.lab.hours,
BodyRepairHours: job.joblines
.filter((line) => repairOpCodes.includes(line.lbr_op))
.reduce((acc, val) => acc + val.mod_lb_hrs, 0),
BodyReplaceHours: job.joblines
.filter((line) => replaceOpCodes.includes(line.lbr_op))
.reduce((acc, val) => acc + val.mod_lb_hrs, 0),
RefinishHours: job.job_totals.rates.lar.hours,
MechanicalHours: job.job_totals.rates.lam.hours,
StructuralHours: job.job_totals.rates.las.hours,
PartsTotal: Dinero(job.job_totals.parts.parts.total).toFormat(
AHDineroFormat
),
PartsTotalCost: 0,
PartsTotalCost: repairCosts.PartsTotalCost.toFormat(AHDineroFormat),
PartsOEM: Dinero(
job.job_totals.parts.parts.list.PAN &&
job.job_totals.parts.parts.list.PAN.total
).toFormat(AHDineroFormat),
PartsOEMCost: 0,
)
.add(
Dinero(
job.job_totals.parts.parts.list.PAP &&
job.job_totals.parts.parts.list.PAP.total
)
)
.toFormat(AHDineroFormat),
PartsOEMCost: repairCosts.PartsOemCost.toFormat(AHDineroFormat),
PartsAM: Dinero(
job.job_totals.parts.parts.list.PAA &&
job.job_totals.parts.parts.list.PAA.total
).toFormat(AHDineroFormat),
PartsAMCost: 0,
PartsAMCost: repairCosts.PartsAMCost.toFormat(AHDineroFormat),
PartsReconditioned: null,
PartsReconditionedCost: null,
PartsReconditionedCost:
repairCosts.PartsReconditionedCost.toFormat(AHDineroFormat),
PartsRecycled: Dinero(
job.job_totals.parts.parts.list.PAR &&
job.job_totals.parts.parts.list.PAR.total
@@ -389,10 +409,108 @@ const CreateRepairOrderTag = (job, errorCallback) => {
};
return ret;
} catch (error) {
console.log("Error calculating job", error);
errorCallback(job, error);
}
};
const CreateCosts = (job) => {
//Create a mapping based on AH Requirements
const billTotalsByCostCenters = job.bills.reduce((bill_acc, bill_val) => {
//At the bill level.
bill_val.billlines.map((line_val) => {
//At the bill line level.
//console.log("JobCostingPartsTable -> line_val", line_val);
if (!bill_acc[line_val.cost_center])
bill_acc[line_val.cost_center] = Dinero();
bill_acc[line_val.cost_center] = bill_acc[line_val.cost_center].add(
Dinero({
amount: Math.round((line_val.actual_cost || 0) * 100),
})
.multiply(line_val.quantity)
.multiply(bill_val.is_credit_memo ? -1 : 1)
);
return null;
});
return bill_acc;
}, {});
const materialsHours = { mapaHrs: 0, mashHrs: 0 };
//If the hourly rates for job costing are set, add them in.
if (job.bodyshop.jc_hourly_rates && job.bodyshop.jc_hourly_rates.mapa) {
if (
!billTotalsByCostCenters[
job.bodyshop.md_responsibility_centers.defaults.costs.MAPA
]
)
billTotalsByCostCenters[
job.bodyshop.md_responsibility_centers.defaults.costs.MAPA
] = Dinero();
billTotalsByCostCenters[
job.bodyshop.md_responsibility_centers.defaults.costs.MAPA
] = billTotalsByCostCenters[
job.bodyshop.md_responsibility_centers.defaults.costs.MAPA
].add(
Dinero({
amount:
(job.bodyshop.jc_hourly_rates &&
job.bodyshop.jc_hourly_rates.mapa * 100) ||
0,
}).multiply(materialsHours.mapaHrs)
);
}
const ticketTotalsByCostCenter = job.timetickets.reduce(
(ticket_acc, ticket_val) => {
//At the invoice level.
if (!ticket_acc[ticket_val.cost_center])
ticket_acc[ticket_val.cost_center] = Dinero();
ticket_acc[ticket_val.cost_center] = ticket_acc[
ticket_val.cost_center
].add(
Dinero({
amount: Math.round((ticket_val.rate || 0) * 100),
}).multiply(ticket_val.actualhrs || ticket_val.productivehrs || 0)
);
return ticket_acc;
},
{}
);
const defaultCosts = job.bodyshop.md_responsibility_centers.defaults.costs;
return {
PartsTotalCost: Object.keys(billTotalsByCostCenters).reduce((acc, key) => {
return acc.add(billTotalsByCostCenters[key]);
}, Dinero()),
PartsOemCost: (billTotalsByCostCenters[defaultCosts.PAN] || Dinero()).add(
billTotalsByCostCenters[defaultCosts.PAP] || Dinero()
),
PartsAMCost: billTotalsByCostCenters[defaultCosts.PAA] || Dinero(),
PartsReconditionedCost: Dinero(),
PartsRecycledCost: billTotalsByCostCenters[defaultCosts.PAR] || Dinero(),
PartsOtherCost: billTotalsByCostCenters[defaultCosts.PAO] || Dinero(),
SubletTotalCost: billTotalsByCostCenters[defaultCosts.PAS] || Dinero(),
BodyLaborTotalCost: ticketTotalsByCostCenter[defaultCosts.LAB] || Dinero(),
RefinishLaborTotalCost:
ticketTotalsByCostCenter[defaultCosts.LAR] || Dinero(),
MechanicalLaborTotalCost:
ticketTotalsByCostCenter[defaultCosts.LAM] || Dinero(),
StructuralLaborTotalCost:
ticketTotalsByCostCenter[defaultCosts.LAS] || Dinero(),
PMTotalCost: billTotalsByCostCenters[defaultCosts.MAPA] || Dinero(),
BMTotalCost: billTotalsByCostCenters[defaultCosts.MASH] || Dinero(),
MiscTotalCost: billTotalsByCostCenters[defaultCosts.PAO] || Dinero(),
TowingTotalCost: billTotalsByCostCenters[defaultCosts.TOW] || Dinero(),
StorageTotalCost: Dinero(),
DetailTotal: Dinero(),
DetailTotalCost: Dinero(),
SalesTaxTotalCost: Dinero(),
};
};
const StatusMapping = (status, md_ro_statuses) => {
//EST, SCH, ARR, IPR, RDY, DEL, CLO, CAN, UNDEFINED.
const {
@@ -493,3 +611,6 @@ const generateNullDetailLine = () => {
EstimateAmount: null,
};
};
const repairOpCodes = ["OP4", "OP9", "OP10"];
const replaceOpCodes = ["OP2", "OP5", "OP11", "OP12"];

View File

@@ -338,6 +338,7 @@ exports.AUTOHOUSE_QUERY = `query AUTOHOUSE_EXPORT($start: timestamptz) {
rate_mapa
rate_mash
job_totals
driveable
bodyshop {
id
shopname
@@ -350,6 +351,8 @@ exports.AUTOHOUSE_QUERY = `query AUTOHOUSE_EXPORT($start: timestamptz) {
md_ro_statuses
md_order_statuses
autohouseid
md_responsibility_centers
jc_hourly_rates
}
joblines (where:{removed: {_eq:false}}){
id
@@ -366,7 +369,10 @@ exports.AUTOHOUSE_QUERY = `query AUTOHOUSE_EXPORT($start: timestamptz) {
part_qty
part_type
oem_partno
billlines (order_by:{bill:{date:desc_nulls_last}}) {
lbr_op
profitcenter_part
profitcenter_labor
billlines (order_by:{bill:{date:desc_nulls_last}}) {
actual_cost
actual_price
quantity
@@ -377,7 +383,27 @@ exports.AUTOHOUSE_QUERY = `query AUTOHOUSE_EXPORT($start: timestamptz) {
invoice_number
}
}
}
} bills {
id
federal_tax_rate
local_tax_rate
state_tax_rate
is_credit_memo
billlines {
actual_cost
cost_center
id
quantity
}
}
timetickets {
id
rate
cost_center
actualhrs
productivehrs
}
area_of_damage
employee_prep_rel {
first_name

View File

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

View File

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