IO-306 Creation of dashboard.

This commit is contained in:
Patrick Fic
2021-06-14 16:00:58 -07:00
parent 3ab31c8bee
commit db76992c70
29 changed files with 16016 additions and 12803 deletions

View File

@@ -4858,6 +4858,27 @@
<folder_node> <folder_node>
<name>shop</name> <name>shop</name>
<children> <children>
<concept_node>
<name>dashboard</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>rbac</name> <name>rbac</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -10534,6 +10555,95 @@
</concept_node> </concept_node>
</children> </children>
</folder_node> </folder_node>
<folder_node>
<name>labels</name>
<children>
<concept_node>
<name>bodyhrs</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>dollarsinproduction</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>prodhrs</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>refhrs</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
<folder_node> <folder_node>
<name>titles</name> <name>titles</name>
<children> <children>
@@ -12904,6 +13014,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>globalsearch</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>hours</name> <name>hours</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -24009,6 +24140,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>dashboard</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>enterbills</name> <name>enterbills</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -34094,6 +34246,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>dashboard</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>export-logs</name> <name>export-logs</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -34852,6 +35025,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>dashboard</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>export-logs</name> <name>export-logs</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>

23797
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@
"proxy": "http://localhost:5000", "proxy": "http://localhost:5000",
"dependencies": { "dependencies": {
"@apollo/client": "^3.3.17", "@apollo/client": "^3.3.17",
"@craco/craco": "^6.1.2", "@craco/craco": "^5.9.0",
"@fingerprintjs/fingerprintjs": "^3.1.2", "@fingerprintjs/fingerprintjs": "^3.1.2",
"@lourenci/react-kanban": "^2.1.0", "@lourenci/react-kanban": "^2.1.0",
"@sentry/react": "^6.3.6", "@sentry/react": "^6.3.6",
@@ -41,6 +41,7 @@
"react-dom": "^17.0.1", "react-dom": "^17.0.1",
"react-drag-listview": "^0.1.8", "react-drag-listview": "^0.1.8",
"react-grid-gallery": "^0.5.5", "react-grid-gallery": "^0.5.5",
"react-grid-layout": "^1.2.5",
"react-i18next": "^11.8.15", "react-i18next": "^11.8.15",
"react-icons": "^4.2.0", "react-icons": "^4.2.0",
"react-number-format": "^4.5.5", "react-number-format": "^4.5.5",

View File

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

View File

@@ -2,29 +2,32 @@ import { Card } from "antd";
import moment from "moment"; import moment from "moment";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import _ from "lodash";
import { import {
Area, Area,
Bar, Bar,
CartesianGrid, CartesianGrid,
ComposedChart, ComposedChart,
Legend, Legend,
ResponsiveContainer, ResponsiveContainer,
Tooltip, Tooltip,
XAxis, XAxis,
YAxis YAxis,
} from "recharts"; } from "recharts";
import Dinero from "dinero.js";
import * as Utils from "../../scoreboard-targets-table/scoreboard-targets-table.util"; import * as Utils from "../../scoreboard-targets-table/scoreboard-targets-table.util";
export default function DashboardMonthlyRevenueGraph({ data, ...cardProps }) { export default function DashboardMonthlyRevenueGraph({ data, ...cardProps }) {
const { t } = useTranslation(); const { t } = useTranslation();
if (!data) return null;
const jobsByDate = { const jobsByDate = _.groupBy(data.monthly_sales, (item) =>
"2020-07-5": [{ clm_total: 1224 }], moment(item.date_invoiced).format("YYYY-MM-DD")
"2020-07-8": [{ clm_total: 987 }, { clm_total: 8755 }], );
"2020-07-12": [{ clm_total: 684 }, { clm_total: 12022 }], console.log(
"2020-07-21": [{ clm_total: 15000 }], "🚀 ~ file: monthly-revenue-graph.component.jsx ~ line 27 ~ jobsByDate",
"2020-07-28": [{ clm_total: 122 }, { clm_total: 4522 }], jobsByDate
}; );
const listOfDays = Utils.ListOfDaysInCurrentMonth(); const listOfDays = Utils.ListOfDaysInCurrentMonth();
@@ -33,17 +36,19 @@ export default function DashboardMonthlyRevenueGraph({ data, ...cardProps }) {
let dailySales; let dailySales;
if (!!jobsByDate[val]) { if (!!jobsByDate[val]) {
dailySales = jobsByDate[val].reduce((dayAcc, dayVal) => { dailySales = jobsByDate[val].reduce((dayAcc, dayVal) => {
return dayAcc + dayVal.clm_total; return dayAcc.add(Dinero(dayVal.job_totals.totals.subtotal));
}, 0); }, Dinero());
} else { } else {
dailySales = 0; dailySales = Dinero();
} }
const theValue = { const theValue = {
date: moment(val).format("D dd"), date: moment(val).format("DD"),
dailySales, dailySales: dailySales.getAmount() / 100,
accSales: accSales:
acc.length > 0 ? acc[acc.length - 1].accSales + dailySales : dailySales, acc.length > 0
? acc[acc.length - 1].accSales + dailySales.getAmount() / 100
: dailySales.getAmount() / 100,
}; };
return [...acc, theValue]; return [...acc, theValue];
@@ -80,3 +85,15 @@ export default function DashboardMonthlyRevenueGraph({ data, ...cardProps }) {
</Card> </Card>
); );
} }
export const DashboardMonthlyRevenueGraphGql = `
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
}
`;

View File

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

View File

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

View File

@@ -1,185 +1,249 @@
// import Icon from "@ant-design/icons"; import Icon, { SyncOutlined } from "@ant-design/icons";
// import { Button, Dropdown, Menu, notification } from "antd"; import { gql, useMutation, useQuery } from "@apollo/client";
// import React, { useState } from "react"; import { Button, Dropdown, Menu, notification, PageHeader, Space } from "antd";
// import { useMutation, useQuery } from "@apollo/client"; import React, { useState } from "react";
// import { Responsive, WidthProvider } from "react-grid-layout"; import { Responsive, WidthProvider } from "react-grid-layout";
// import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
// import { MdClose } from "react-icons/md"; import { MdClose } from "react-icons/md";
// import { connect } from "react-redux"; import { connect } from "react-redux";
// import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
// import { logImEXEvent } from "../../firebase/firebase.utils"; import { logImEXEvent } from "../../firebase/firebase.utils";
// import { QUERY_DASHBOARD_DETAILS } from "../../graphql/bodyshop.queries"; import { UPDATE_DASHBOARD_LAYOUT } from "../../graphql/user.queries";
// import { UPDATE_DASHBOARD_LAYOUT } from "../../graphql/user.queries"; import {
// import { selectBodyshop,
// selectBodyshop, selectCurrentUser,
// selectCurrentUser, } from "../../redux/user/user.selectors";
// } from "../../redux/user/user.selectors"; import AlertComponent from "../alert/alert.component";
// import AlertComponent from "../alert/alert.component"; import DashboardMonthlyRevenueGraph, {
// import DashboardMonthlyRevenueGraph from "../dashboard-components/monthly-revenue-graph/monthly-revenue-graph.component"; DashboardMonthlyRevenueGraphGql,
// import DashboardProjectedMonthlySales from "../dashboard-components/pojected-monthly-sales/projected-monthly-sales.component"; } from "../dashboard-components/monthly-revenue-graph/monthly-revenue-graph.component";
// import DashboardTotalProductionDollars from "../dashboard-components/total-production-dollars/total-production-dollars.component"; import DashboardProjectedMonthlySales from "../dashboard-components/pojected-monthly-sales/projected-monthly-sales.component";
// import DashboardTotalProductionHours from "../dashboard-components/total-production-hours/total-production-hours.component"; import DashboardTotalProductionDollars from "../dashboard-components/total-production-dollars/total-production-dollars.component";
// import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component"; import DashboardTotalProductionHours, {
// //Combination of the following: DashboardTotalProductionHoursGql,
// // /node_modules/react-grid-layout/css/styles.css } from "../dashboard-components/total-production-hours/total-production-hours.component";
// // /node_modules/react-resizable/css/styles.css import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
// import "./dashboard-grid.styles.css"; //Combination of the following:
// import "./dashboard-grid.styles.scss"; // /node_modules/react-grid-layout/css/styles.css
// /node_modules/react-resizable/css/styles.css
import "./dashboard-grid.styles.scss";
import { GenerateDashboardData } from "./dashboard-grid.utils";
// const ResponsiveReactGridLayout = WidthProvider(Responsive); const ResponsiveReactGridLayout = WidthProvider(Responsive);
// const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
// currentUser: selectCurrentUser, currentUser: selectCurrentUser,
// bodyshop: selectBodyshop, bodyshop: selectBodyshop,
// }); });
// const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
// //setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
// }); });
// export function DashboardGridComponent({ currentUser, bodyshop }) { export function DashboardGridComponent({ currentUser, bodyshop }) {
// const { loading, error, data } = useQuery(QUERY_DASHBOARD_DETAILS); const { t } = useTranslation();
// const { t } = useTranslation(); const [state, setState] = useState({
// const [state, setState] = useState({ ...(bodyshop.associations[0].user.dashboardlayout
// layout: bodyshop.associations[0].user.dashboardlayout || [ ? bodyshop.associations[0].user.dashboardlayout
// { i: "ProductionDollars", x: 0, y: 0, w: 2, h: 2 }, : { items: [], layout: [], layouts: [] }),
// // { i: "ProductionHours", x: 2, y: 0, w: 2, h: 2 }, });
// ], const { loading, error, data, refetch } = useQuery(
// }); createDashboardQuery(state)
// const [updateLayout] = useMutation(UPDATE_DASHBOARD_LAYOUT); );
// const handleLayoutChange = async (newLayout) => { const [updateLayout] = useMutation(UPDATE_DASHBOARD_LAYOUT);
// logImEXEvent("dashboard_change_layout");
// setState({ ...state, layout: newLayout });
// const result = await updateLayout({
// variables: { email: currentUser.email, layout: newLayout },
// });
// if (!!result.errors) { const handleLayoutChange = async (layout, layouts) => {
// notification["error"]({ logImEXEvent("dashboard_change_layout");
// message: t("dashboard.errors.updatinglayout", {
// message: JSON.stringify(result.errors),
// }),
// });
// }
// };
// const handleRemoveComponent = (key) => { setState({ ...state, layout, layouts });
// logImEXEvent("dashboard_remove_component", { name: key });
// const idxToRemove = state.layout.findIndex((i) => i.i === key); const result = await updateLayout({
// const newLayout = state.layout; variables: {
// newLayout.splice(idxToRemove, 1); email: currentUser.email,
// handleLayoutChange(newLayout); 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);
const items = state.items;
items.splice(idxToRemove, 1);
setState({ ...state, items });
};
// const handleAddComponent = (e) => { const handleAddComponent = (e) => {
// logImEXEvent("dashboard_add_component", { name: e }); logImEXEvent("dashboard_add_component", { name: e });
setState({
...state,
items: [
...state.items,
{
i: e.key,
x: (state.items.length * 2) % (state.cols || 12),
y: Infinity, // puts it at the bottom
w: componentList[e.key].w || 2,
h: componentList[e.key].h || 2,
},
],
});
};
// handleLayoutChange([ const dashboarddata = React.useMemo(
// ...state.layout, () => GenerateDashboardData(data),
// { [data]
// i: e.key, );
// x: (state.layout.length * 2) % (state.cols || 12),
// y: Infinity, // puts it at the bottom
// w: componentList[e.key].w || 2,
// h: componentList[e.key].h || 2,
// },
// ]);
// };
// const onBreakpointChange = (breakpoint, cols) => { // const onBreakpointChange = (breakpoint, cols) => {
// setState({ ...state, breakpoint: breakpoint, cols: cols }); // setState({ ...state, breakpoint: breakpoint, cols: cols });
// }; // };
// const existingLayoutKeys = state.layout.map((i) => i.i); const existingLayoutKeys = state.items.map((i) => i.i);
// const addComponentOverlay = ( const addComponentOverlay = (
// <Menu onClick={handleAddComponent}> <Menu onClick={handleAddComponent}>
// {Object.keys(componentList).map((key) => ( {Object.keys(componentList).map((key) => (
// <Menu.Item <Menu.Item
// key={key} key={key}
// value={key} value={key}
// disabled={existingLayoutKeys.includes(key)} disabled={existingLayoutKeys.includes(key)}
// > >
// {componentList[key].label} {componentList[key].label}
// </Menu.Item> </Menu.Item>
// ))} ))}
// </Menu> </Menu>
// ); );
// if (error) return <AlertComponent message={error.message} type="error" />; if (error) return <AlertComponent message={error.message} type="error" />;
// return ( return (
// <div> <div>
// <Dropdown overlay={addComponentOverlay} trigger={["click"]}> <PageHeader
// <Button>{t("dashboard.actions.addcomponent")}</Button> extra={
// </Dropdown> <Space>
// <ResponsiveReactGridLayout <Button onClick={() => refetch()}>
// className="layout" <SyncOutlined />
// breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }} </Button>
// cols={{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }} <Dropdown overlay={addComponentOverlay} trigger={["click"]}>
// width="100%" <Button>{t("dashboard.actions.addcomponent")}</Button>
// onLayoutChange={handleLayoutChange} </Dropdown>
// onBreakpointChange={onBreakpointChange} </Space>
// > }
// {state.layout.map((item, index) => { />
// const TheComponent = componentList[item.i].component;
// return (
// <div key={item.i} data-grid={item}>
// <LoadingSkeleton loading={loading}>
// <Icon
// component={MdClose}
// key={item.i}
// style={{
// position: "absolute",
// zIndex: "2",
// right: ".25rem",
// top: ".25rem",
// cursor: "pointer",
// }}
// onClick={() => handleRemoveComponent(item.i)}
// />
// <TheComponent
// className="dashboard-card"
// size="small"
// style={{ height: "100%", width: "100%" }}
// />
// </LoadingSkeleton>
// </div>
// );
// })}
// </ResponsiveReactGridLayout>
// </div>
// );
// }
// export default connect( <ResponsiveReactGridLayout
// mapStateToProps, className="layout"
// mapDispatchToProps breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }}
// )(DashboardGridComponent); cols={{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }}
width="100%"
layouts={state.layouts}
onLayoutChange={handleLayoutChange}
// onBreakpointChange={onBreakpointChange}
>
{state.items.map((item, index) => {
const TheComponent = componentList[item.i].component;
return (
<div key={item.i} data-grid={item}>
<LoadingSkeleton loading={loading}>
<Icon
component={MdClose}
key={item.i}
style={{
position: "absolute",
zIndex: "2",
right: ".25rem",
top: ".25rem",
cursor: "pointer",
}}
onClick={() => handleRemoveComponent(item.i)}
/>
<TheComponent className="dashboard-card" data={dashboarddata} />
</LoadingSkeleton>
</div>
);
})}
</ResponsiveReactGridLayout>
</div>
);
}
// const componentList = { export default connect(
// ProductionDollars: { mapStateToProps,
// label: "Production Dollars", mapDispatchToProps
// component: DashboardTotalProductionDollars, )(DashboardGridComponent);
// w: 2,
// h: 1, const componentList = {
// }, ProductionDollars: {
// ProductionHours: { label: "Production Dollars",
// label: "Production Hours", component: DashboardTotalProductionDollars,
// component: DashboardTotalProductionHours, gqlFragment: null,
// w: 2, w: 2,
// h: 1, h: 1,
// }, },
// ProjectedMonthlySales: { ProductionHours: {
// label: "Projected Monthly Sales", label: "Production Hours",
// component: DashboardProjectedMonthlySales, component: DashboardTotalProductionHours,
// w: 2, gqlFragment: DashboardTotalProductionHoursGql,
// h: 1, w: 2,
// }, h: 1,
// MonthlyRevenueGraph: { },
// label: "Monthly Sales Graph", ProjectedMonthlySales: {
// component: DashboardMonthlyRevenueGraph, label: "Projected Monthly Sales",
// w: 2, component: DashboardProjectedMonthlySales,
// h: 2, gqlFragment: null,
// }, w: 2,
// }; h: 1,
},
MonthlyRevenueGraph: {
label: "Monthly Sales Graph",
component: DashboardMonthlyRevenueGraph,
gqlFragment: DashboardMonthlyRevenueGraphGql,
w: 2,
h: 2,
},
};
const createDashboardQuery = (state) => {
const componentBasedAdditions = state.layout
.map((item, index) => componentList[item.i].gqlFragment || "")
.join("");
return gql`
query QUERY_DASHBOARD_DETAILS {
${componentBasedAdditions}
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,9 +1,136 @@
.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 { .dashboard-card {
// background-color: green; height: 100%;
width: 100%;
.ant-card-body { .ant-card-body {
// background-color: red; // background-color: red;
height: 100%; height: 90%;
width: 100%; width: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -243,33 +243,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

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

@@ -158,6 +158,7 @@ const Phonebook = lazy(() => import("../phonebook/phonebook.page.container"));
const EmailTest = lazy(() => const EmailTest = lazy(() =>
import("../../components/email-test/email-test-component") import("../../components/email-test/email-test-component")
); );
const Dashboard = lazy(() => import("../dashboard/dashboard.container"));
const { Content, Footer } = Layout; const { Content, Footer } = Layout;
@@ -365,6 +366,7 @@ export function Manage({ match, conflict, bodyshop }) {
/> />
<Route exact path={`${match.path}/help`} component={Help} /> <Route exact path={`${match.path}/help`} component={Help} />
<Route exact path={`${match.path}/emailtest`} component={EmailTest} /> <Route exact path={`${match.path}/emailtest`} component={EmailTest} />
<Route exact path={`${match.path}/dashboard`} component={Dashboard} />
</Suspense> </Suspense>
); );

View File

@@ -322,6 +322,7 @@
"view": "Shift Clock -> View" "view": "Shift Clock -> View"
}, },
"shop": { "shop": {
"dashboard": "Shop -> Dashboard",
"rbac": "Shop -> RBAC", "rbac": "Shop -> RBAC",
"templates": "Shop -> Templates", "templates": "Shop -> Templates",
"vendors": "Shop -> Vendors" "vendors": "Shop -> Vendors"
@@ -669,6 +670,12 @@
"errors": { "errors": {
"updatinglayout": "Error saving updated layout {{message}}" "updatinglayout": "Error saving updated layout {{message}}"
}, },
"labels": {
"bodyhrs": "Body Hrs",
"dollarsinproduction": "Dollars in Production",
"prodhrs": "Production Hrs",
"refhrs": "Refinish Hrs"
},
"titles": { "titles": {
"monthlyrevenuegraph": "Monthly Revenue Graph", "monthlyrevenuegraph": "Monthly Revenue Graph",
"productiondollars": "Total dollars in production", "productiondollars": "Total dollars in production",
@@ -825,6 +832,7 @@
"errors": "Errors", "errors": "Errors",
"exceptiontitle": "An error has occurred.", "exceptiontitle": "An error has occurred.",
"friday": "Friday", "friday": "Friday",
"globalsearch": "Global Search",
"hours": "hrs", "hours": "hrs",
"in": "In", "in": "In",
"instanceconflictext": "Your $t(titles.app) account can only be used on one device at any given time. Refresh your session to take control.", "instanceconflictext": "Your $t(titles.app) account can only be used on one device at any given time. Refresh your session to take control.",
@@ -1419,6 +1427,7 @@
"courtesycars-contracts": "Contracts", "courtesycars-contracts": "Contracts",
"courtesycars-newcontract": "New Contract", "courtesycars-newcontract": "New Contract",
"customers": "Customers", "customers": "Customers",
"dashboard": "Dashboard",
"enterbills": "Enter Bills", "enterbills": "Enter Bills",
"enterpayment": "Enter Payments", "enterpayment": "Enter Payments",
"entertimeticket": "Enter Time Tickets", "entertimeticket": "Enter Time Tickets",
@@ -2049,6 +2058,7 @@
"courtesycars": "Courtesy Cars", "courtesycars": "Courtesy Cars",
"courtesycars-detail": "Courtesy Car {{number}}", "courtesycars-detail": "Courtesy Car {{number}}",
"courtesycars-new": "New Courtesy Car", "courtesycars-new": "New Courtesy Car",
"dashboard": "Dashboard",
"export-logs": "Export Logs", "export-logs": "Export Logs",
"jobs": "Jobs", "jobs": "Jobs",
"jobs-active": "Active Jobs", "jobs-active": "Active Jobs",
@@ -2086,6 +2096,7 @@
"courtesycars": "Courtesy Cars | $t(titles.app)", "courtesycars": "Courtesy Cars | $t(titles.app)",
"courtesycars-create": "New Courtesy Car | $t(titles.app)", "courtesycars-create": "New Courtesy Car | $t(titles.app)",
"courtesycars-detail": "Courtesy Car {{id}} | $t(titles.app)", "courtesycars-detail": "Courtesy Car {{id}} | $t(titles.app)",
"dashboard": "Dashboard | $t(titles.app)",
"export-logs": "Export Logs | $t(titles.app)", "export-logs": "Export Logs | $t(titles.app)",
"jobs": "Active Jobs | $t(titles.app)", "jobs": "Active Jobs | $t(titles.app)",
"jobs-admin": "Job {{ro_number}} - Admin | $t(titles.app)", "jobs-admin": "Job {{ro_number}} - Admin | $t(titles.app)",

View File

@@ -322,6 +322,7 @@
"view": "" "view": ""
}, },
"shop": { "shop": {
"dashboard": "",
"rbac": "", "rbac": "",
"templates": "", "templates": "",
"vendors": "" "vendors": ""
@@ -669,6 +670,12 @@
"errors": { "errors": {
"updatinglayout": "" "updatinglayout": ""
}, },
"labels": {
"bodyhrs": "",
"dollarsinproduction": "",
"prodhrs": "",
"refhrs": ""
},
"titles": { "titles": {
"monthlyrevenuegraph": "", "monthlyrevenuegraph": "",
"productiondollars": "", "productiondollars": "",
@@ -825,6 +832,7 @@
"errors": "", "errors": "",
"exceptiontitle": "", "exceptiontitle": "",
"friday": "", "friday": "",
"globalsearch": "",
"hours": "", "hours": "",
"in": "en", "in": "en",
"instanceconflictext": "", "instanceconflictext": "",
@@ -1419,6 +1427,7 @@
"courtesycars-contracts": "", "courtesycars-contracts": "",
"courtesycars-newcontract": "", "courtesycars-newcontract": "",
"customers": "Clientes", "customers": "Clientes",
"dashboard": "",
"enterbills": "", "enterbills": "",
"enterpayment": "", "enterpayment": "",
"entertimeticket": "", "entertimeticket": "",
@@ -2049,6 +2058,7 @@
"courtesycars": "", "courtesycars": "",
"courtesycars-detail": "", "courtesycars-detail": "",
"courtesycars-new": "", "courtesycars-new": "",
"dashboard": "",
"export-logs": "", "export-logs": "",
"jobs": "", "jobs": "",
"jobs-active": "", "jobs-active": "",
@@ -2086,6 +2096,7 @@
"courtesycars": "", "courtesycars": "",
"courtesycars-create": "", "courtesycars-create": "",
"courtesycars-detail": "", "courtesycars-detail": "",
"dashboard": "",
"export-logs": "", "export-logs": "",
"jobs": "Todos los trabajos | $t(titles.app)", "jobs": "Todos los trabajos | $t(titles.app)",
"jobs-admin": "", "jobs-admin": "",

View File

@@ -322,6 +322,7 @@
"view": "" "view": ""
}, },
"shop": { "shop": {
"dashboard": "",
"rbac": "", "rbac": "",
"templates": "", "templates": "",
"vendors": "" "vendors": ""
@@ -669,6 +670,12 @@
"errors": { "errors": {
"updatinglayout": "" "updatinglayout": ""
}, },
"labels": {
"bodyhrs": "",
"dollarsinproduction": "",
"prodhrs": "",
"refhrs": ""
},
"titles": { "titles": {
"monthlyrevenuegraph": "", "monthlyrevenuegraph": "",
"productiondollars": "", "productiondollars": "",
@@ -825,6 +832,7 @@
"errors": "", "errors": "",
"exceptiontitle": "", "exceptiontitle": "",
"friday": "", "friday": "",
"globalsearch": "",
"hours": "", "hours": "",
"in": "dans", "in": "dans",
"instanceconflictext": "", "instanceconflictext": "",
@@ -1419,6 +1427,7 @@
"courtesycars-contracts": "", "courtesycars-contracts": "",
"courtesycars-newcontract": "", "courtesycars-newcontract": "",
"customers": "Les clients", "customers": "Les clients",
"dashboard": "",
"enterbills": "", "enterbills": "",
"enterpayment": "", "enterpayment": "",
"entertimeticket": "", "entertimeticket": "",
@@ -2049,6 +2058,7 @@
"courtesycars": "", "courtesycars": "",
"courtesycars-detail": "", "courtesycars-detail": "",
"courtesycars-new": "", "courtesycars-new": "",
"dashboard": "",
"export-logs": "", "export-logs": "",
"jobs": "", "jobs": "",
"jobs-active": "", "jobs-active": "",
@@ -2086,6 +2096,7 @@
"courtesycars": "", "courtesycars": "",
"courtesycars-create": "", "courtesycars-create": "",
"courtesycars-detail": "", "courtesycars-detail": "",
"dashboard": "",
"export-logs": "", "export-logs": "",
"jobs": "Tous les emplois | $t(titles.app)", "jobs": "Tous les emplois | $t(titles.app)",
"jobs-admin": "", "jobs-admin": "",

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
- args:
cascade: false
read_only: false
sql: ALTER TABLE ONLY "public"."users" ALTER COLUMN "dashboardlayout" SET DEFAULT
jsonb_build_array();
type: run_sql
- args:
cascade: false
read_only: false
sql: ALTER TABLE "public"."users" ALTER COLUMN "dashboardlayout" SET NOT NULL;
type: run_sql

View File

@@ -0,0 +1,10 @@
- args:
cascade: false
read_only: false
sql: ALTER TABLE "public"."users" ALTER COLUMN "dashboardlayout" DROP DEFAULT;
type: run_sql
- args:
cascade: false
read_only: false
sql: ALTER TABLE "public"."users" ALTER COLUMN "dashboardlayout" DROP NOT NULL;
type: run_sql

View File

@@ -365,7 +365,7 @@ function CalculateTaxesTotals(job, otherTotals) {
if (!val.tax_part || (!val.part_type && IsAdditionalCost(val))) { if (!val.tax_part || (!val.part_type && IsAdditionalCost(val))) {
additionalItemsTax = additionalItemsTax.add( additionalItemsTax = additionalItemsTax.add(
Dinero({ amount: Math.round((val.act_price || 0) * 100) }) Dinero({ amount: Math.round((val.act_price || 0) * 100) })
.multiply(val.part_qty || 1) .multiply(val.part_qty || 0)
.percentage( .percentage(
((job.parts_tax_rates && ((job.parts_tax_rates &&
job.parts_tax_rates["PAN"] && job.parts_tax_rates["PAN"] &&
@@ -376,7 +376,7 @@ function CalculateTaxesTotals(job, otherTotals) {
} else { } else {
statePartsTax = statePartsTax.add( statePartsTax = statePartsTax.add(
Dinero({ amount: Math.round((val.act_price || 0) * 100) }) Dinero({ amount: Math.round((val.act_price || 0) * 100) })
.multiply(val.part_qty || 1) .multiply(val.part_qty || 0)
.add( .add(
Dinero({ Dinero({
amount: Math.round((val.act_price || 0) * 100), amount: Math.round((val.act_price || 0) * 100),