IO-306 Further development of dashboard.
This commit is contained in:
@@ -10647,6 +10647,69 @@
|
|||||||
<folder_node>
|
<folder_node>
|
||||||
<name>titles</name>
|
<name>titles</name>
|
||||||
<children>
|
<children>
|
||||||
|
<concept_node>
|
||||||
|
<name>monthlyjobcosting</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>monthlylaborsales</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>monthlypartssales</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
<concept_node>
|
<concept_node>
|
||||||
<name>monthlyrevenuegraph</name>
|
<name>monthlyrevenuegraph</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<definition_loaded>false</definition_loaded>
|
||||||
@@ -10668,6 +10731,27 @@
|
|||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>prodhrssummary</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
<concept_node>
|
<concept_node>
|
||||||
<name>productiondollars</name>
|
<name>productiondollars</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<definition_loaded>false</definition_loaded>
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
import { Card, Input, Space, Table, Typography } from "antd";
|
||||||
|
import axios from "axios";
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { alphaSort } from "../../../utils/sorters";
|
||||||
|
import LoadingSkeleton from "../../loading-skeleton/loading-skeleton.component";
|
||||||
|
import Dinero from "dinero.js";
|
||||||
|
import DashboardRefreshRequired from "../refresh-required.component";
|
||||||
|
|
||||||
|
export default function DashboardMonthlyJobCosting({ data, ...cardProps }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [costingData, setcostingData] = useState(null);
|
||||||
|
const [searchText, setSearchText] = useState("");
|
||||||
|
const [state, setState] = useState({
|
||||||
|
sortedInfo: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function getCostingData() {
|
||||||
|
if (data && data.monthly_sales) {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await axios.post("/job/costingmulti", {
|
||||||
|
jobids: data.monthly_sales.map((x) => x.id),
|
||||||
|
});
|
||||||
|
setcostingData(response.data);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getCostingData();
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
if (!data) return null;
|
||||||
|
if (!data.monthly_sales) return <DashboardRefreshRequired {...cardProps} />;
|
||||||
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
|
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||||
|
};
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: t("bodyshop.fields.responsibilitycenter"),
|
||||||
|
dataIndex: "cost_center",
|
||||||
|
key: "cost_center",
|
||||||
|
sorter: (a, b) => alphaSort(a.cost_center, b.cost_center),
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "cost_center" && state.sortedInfo.order,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.labels.sales"),
|
||||||
|
dataIndex: "sales",
|
||||||
|
key: "sales",
|
||||||
|
sorter: (a, b) =>
|
||||||
|
parseFloat(a.sales.substring(1)) - parseFloat(b.sales.substring(1)),
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "sales" && state.sortedInfo.order,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
title: t("jobs.labels.costs"),
|
||||||
|
dataIndex: "costs",
|
||||||
|
key: "costs",
|
||||||
|
sorter: (a, b) =>
|
||||||
|
parseFloat(a.costs.substring(1)) - parseFloat(b.costs.substring(1)),
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "costs" && state.sortedInfo.order,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
title: t("jobs.labels.gpdollars"),
|
||||||
|
dataIndex: "gpdollars",
|
||||||
|
key: "gpdollars",
|
||||||
|
sorter: (a, b) =>
|
||||||
|
parseFloat(a.gpdollars.substring(1)) -
|
||||||
|
parseFloat(b.gpdollars.substring(1)),
|
||||||
|
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "gpdollars" && state.sortedInfo.order,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.labels.gppercent"),
|
||||||
|
dataIndex: "gppercent",
|
||||||
|
key: "gppercent",
|
||||||
|
sorter: (a, b) =>
|
||||||
|
parseFloat(a.gppercent.slice(0, -1) || 0) -
|
||||||
|
parseFloat(b.gppercent.slice(0, -1) || 0),
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "gppercent" && state.sortedInfo.order,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const filteredData =
|
||||||
|
searchText === ""
|
||||||
|
? (costingData && costingData.allCostCenterData) || []
|
||||||
|
: costingData.allCostCenterData.filter((d) =>
|
||||||
|
(d.cost_center || "")
|
||||||
|
.toString()
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(searchText.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
title={t("dashboard.titles.monthlyjobcosting")}
|
||||||
|
extra={
|
||||||
|
<Space wrap>
|
||||||
|
<Input.Search
|
||||||
|
placeholder={t("general.labels.search")}
|
||||||
|
value={searchText}
|
||||||
|
onChange={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setSearchText(e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
{...cardProps}
|
||||||
|
>
|
||||||
|
<LoadingSkeleton loading={loading}>
|
||||||
|
<div style={{ height: "100%" }}>
|
||||||
|
<Table
|
||||||
|
onChange={handleTableChange}
|
||||||
|
pagination={{ position: "top", defaultPageSize: 50 }}
|
||||||
|
columns={columns}
|
||||||
|
scroll={{ x: true, y: "calc(100% - 4em)" }}
|
||||||
|
rowKey="id"
|
||||||
|
style={{ height: "100%" }}
|
||||||
|
dataSource={filteredData}
|
||||||
|
summary={() => (
|
||||||
|
<Table.Summary.Row>
|
||||||
|
<Table.Summary.Cell>
|
||||||
|
<Typography.Title level={4}>
|
||||||
|
{t("general.labels.totals")}
|
||||||
|
</Typography.Title>
|
||||||
|
</Table.Summary.Cell>
|
||||||
|
<Table.Summary.Cell>
|
||||||
|
{Dinero(
|
||||||
|
costingData &&
|
||||||
|
costingData.allSummaryData &&
|
||||||
|
costingData.allSummaryData.totalSales
|
||||||
|
).toFormat()}
|
||||||
|
</Table.Summary.Cell>
|
||||||
|
<Table.Summary.Cell>
|
||||||
|
{Dinero(
|
||||||
|
costingData &&
|
||||||
|
costingData.allSummaryData &&
|
||||||
|
costingData.allSummaryData.totalCost
|
||||||
|
).toFormat()}
|
||||||
|
</Table.Summary.Cell>
|
||||||
|
<Table.Summary.Cell>
|
||||||
|
{Dinero(
|
||||||
|
costingData &&
|
||||||
|
costingData.allSummaryData &&
|
||||||
|
costingData.allSummaryData.gpdollars
|
||||||
|
).toFormat()}
|
||||||
|
</Table.Summary.Cell>
|
||||||
|
<Table.Summary.Cell></Table.Summary.Cell>
|
||||||
|
</Table.Summary.Row>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</LoadingSkeleton>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
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(i.status),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
console.log(
|
||||||
|
"🚀 ~ file: monthly-parts-sales.component.jsx ~ line 34 ~ chartData",
|
||||||
|
chartData
|
||||||
|
);
|
||||||
|
|
||||||
|
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 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={8} textAnchor="middle" fill={fill}>
|
||||||
|
{payload.name}
|
||||||
|
</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}
|
||||||
|
/>
|
||||||
|
<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>
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
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(i.status),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
console.log(
|
||||||
|
"🚀 ~ file: monthly-parts-sales.component.jsx ~ line 34 ~ chartData",
|
||||||
|
chartData
|
||||||
|
);
|
||||||
|
|
||||||
|
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 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={8} textAnchor="middle" fill={fill}>
|
||||||
|
{payload.name}
|
||||||
|
</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}
|
||||||
|
/>
|
||||||
|
<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>
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -16,18 +16,16 @@ import {
|
|||||||
} from "recharts";
|
} from "recharts";
|
||||||
import Dinero from "dinero.js";
|
import Dinero from "dinero.js";
|
||||||
import * as Utils from "../../scoreboard-targets-table/scoreboard-targets-table.util";
|
import * as Utils from "../../scoreboard-targets-table/scoreboard-targets-table.util";
|
||||||
|
import DashboardRefreshRequired from "../refresh-required.component";
|
||||||
|
|
||||||
export default function DashboardMonthlyRevenueGraph({ data, ...cardProps }) {
|
export default function DashboardMonthlyRevenueGraph({ data, ...cardProps }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
if (!data) return null;
|
if (!data) return null;
|
||||||
|
if (!data.monthly_sales) return <DashboardRefreshRequired {...cardProps} />;
|
||||||
|
|
||||||
const jobsByDate = _.groupBy(data.monthly_sales, (item) =>
|
const jobsByDate = _.groupBy(data.monthly_sales, (item) =>
|
||||||
moment(item.date_invoiced).format("YYYY-MM-DD")
|
moment(item.date_invoiced).format("YYYY-MM-DD")
|
||||||
);
|
);
|
||||||
console.log(
|
|
||||||
"🚀 ~ file: monthly-revenue-graph.component.jsx ~ line 27 ~ jobsByDate",
|
|
||||||
jobsByDate
|
|
||||||
);
|
|
||||||
|
|
||||||
const listOfDays = Utils.ListOfDaysInCurrentMonth();
|
const listOfDays = Utils.ListOfDaysInCurrentMonth();
|
||||||
|
|
||||||
@@ -56,44 +54,38 @@ export default function DashboardMonthlyRevenueGraph({ data, ...cardProps }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card title={t("dashboard.titles.monthlyrevenuegraph")} {...cardProps}>
|
<Card title={t("dashboard.titles.monthlyrevenuegraph")} {...cardProps}>
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<div style={{ height: "100%" }}>
|
||||||
<ComposedChart
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
data={chartData}
|
<ComposedChart
|
||||||
margin={{ top: 20, right: 20, bottom: 20, left: 20 }}
|
data={chartData}
|
||||||
>
|
margin={{ top: 20, right: 20, bottom: 20, left: 20 }}
|
||||||
<CartesianGrid stroke="#f5f5f5" />
|
>
|
||||||
<XAxis dataKey="date" />
|
<CartesianGrid stroke="#f5f5f5" />
|
||||||
<YAxis />
|
<XAxis dataKey="date" />
|
||||||
<Tooltip />
|
<YAxis />
|
||||||
<Legend />
|
<Tooltip />
|
||||||
<Area
|
<Legend />
|
||||||
type="monotone"
|
<Area
|
||||||
name="Accumulated Sales"
|
type="monotone"
|
||||||
dataKey="accSales"
|
name="Accumulated Sales"
|
||||||
fill="#8884d8"
|
dataKey="accSales"
|
||||||
stroke="#8884d8"
|
fill="#8884d8"
|
||||||
/>
|
stroke="#8884d8"
|
||||||
<Bar
|
/>
|
||||||
name="Daily Sales"
|
<Bar
|
||||||
dataKey="dailySales"
|
name="Daily Sales"
|
||||||
//stackId="day"
|
dataKey="dailySales"
|
||||||
barSize={20}
|
//stackId="day"
|
||||||
fill="#413ea0"
|
barSize={20}
|
||||||
/>
|
fill="#413ea0"
|
||||||
</ComposedChart>
|
/>
|
||||||
</ResponsiveContainer>
|
</ComposedChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DashboardMonthlyRevenueGraphGql = `
|
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
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -1,30 +1,40 @@
|
|||||||
import { ArrowDownOutlined, ArrowUpOutlined } from "@ant-design/icons";
|
|
||||||
import { Card, Statistic } from "antd";
|
import { Card, Statistic } from "antd";
|
||||||
|
import Dinero from "dinero.js";
|
||||||
|
import moment from "moment";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import DashboardRefreshRequired from "../refresh-required.component";
|
||||||
export default function DashboardProjectedMonthlySales({ data, ...cardProps }) {
|
export default function DashboardProjectedMonthlySales({ data, ...cardProps }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const aboveTargetMonthlySales = false;
|
if (!data) return null;
|
||||||
|
if (!data.projected_monthly_sales)
|
||||||
|
return <DashboardRefreshRequired {...cardProps} />;
|
||||||
|
|
||||||
|
const dollars =
|
||||||
|
data.projected_monthly_sales &&
|
||||||
|
data.projected_monthly_sales.reduce(
|
||||||
|
(acc, val) => acc.add(Dinero(val.job_totals.totals.subtotal)),
|
||||||
|
Dinero()
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<Card {...cardProps}>
|
<Card title={t("dashboard.titles.projectedmonthlysales")} {...cardProps}>
|
||||||
<Statistic
|
<Statistic value={dollars.toFormat()} />
|
||||||
title={t("dashboard.titles.projectedmonthlysales")}
|
|
||||||
value={222000.0}
|
|
||||||
precision={2}
|
|
||||||
prefix={
|
|
||||||
<div>
|
|
||||||
{aboveTargetMonthlySales ? (
|
|
||||||
<ArrowUpOutlined />
|
|
||||||
) : (
|
|
||||||
<ArrowDownOutlined />
|
|
||||||
)}
|
|
||||||
$
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
valueStyle={{ color: aboveTargetMonthlySales ? "green" : "red" }}
|
|
||||||
/>
|
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const DashboardProjectedMonthlySalesGql = `
|
||||||
|
projected_monthly_sales: jobs(where: {_or: [{_and: [{date_invoiced: {_gte: "${moment()
|
||||||
|
.startOf("month")
|
||||||
|
.format("YYYY-MM-DD")}"}}, {date_invoiced: {_lte: "${moment()
|
||||||
|
.endOf("month")
|
||||||
|
.format("YYYY-MM-DD")}"}}]}, {_and: [{scheduled_completion: {_gte: "${moment()
|
||||||
|
.startOf("month")
|
||||||
|
.format("YYYY-MM-DD")}"}}, {scheduled_completion: {_lte: "${moment()
|
||||||
|
.endOf("month")
|
||||||
|
.format("YYYY-MM-DD")}"}}]}]}) {
|
||||||
|
id
|
||||||
|
date_invoiced
|
||||||
|
job_totals
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { Card, Typography } from "antd";
|
||||||
|
import { SyncOutlined } from "@ant-design/icons";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
export default function DashboardRefreshRequired(props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card {...props}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SyncOutlined />
|
||||||
|
<Typography.Title level={4}>
|
||||||
|
{t("dashboard.errors.refreshrequired")}
|
||||||
|
</Typography.Title>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import { Card, Statistic } from "antd";
|
|||||||
import Dinero from "dinero.js";
|
import Dinero from "dinero.js";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import DashboardRefreshRequired from "../refresh-required.component";
|
||||||
|
|
||||||
export default function DashboardTotalProductionDollars({
|
export default function DashboardTotalProductionDollars({
|
||||||
data,
|
data,
|
||||||
@@ -9,18 +10,17 @@ export default function DashboardTotalProductionDollars({
|
|||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
if (!data) return null;
|
if (!data) return null;
|
||||||
|
if (!data.production_jobs) return <DashboardRefreshRequired {...cardProps} />;
|
||||||
const dollars = data.production_jobs.reduce(
|
const dollars =
|
||||||
(acc, val) => acc.add(Dinero(val.job_totals.totals.subtotal)),
|
data.production_jobs &&
|
||||||
Dinero()
|
data.production_jobs.reduce(
|
||||||
);
|
(acc, val) => acc.add(Dinero(val.job_totals.totals.subtotal)),
|
||||||
|
Dinero()
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card {...cardProps}>
|
<Card title={t("dashboard.labels.dollarsinproduction")} {...cardProps}>
|
||||||
<Statistic
|
<Statistic value={dollars.toFormat()} />
|
||||||
title={t("dashboard.labels.dollarsinproduction")}
|
|
||||||
value={dollars.toFormat()}
|
|
||||||
/>
|
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import React from "react";
|
|
||||||
import { Card, Space, Statistic } from "antd";
|
import { Card, Space, Statistic } from "antd";
|
||||||
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { ArrowDownOutlined, ArrowUpOutlined } from "@ant-design/icons";
|
|
||||||
|
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop } from "../../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../../redux/user/user.selectors";
|
||||||
|
import DashboardRefreshRequired from "../refresh-required.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
});
|
});
|
||||||
@@ -22,22 +22,25 @@ export function DashboardTotalProductionHours({
|
|||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
if (!data) return null;
|
if (!data) return null;
|
||||||
const hours = data.production_jobs.reduce(
|
if (!data.production_jobs) return <DashboardRefreshRequired {...cardProps} />;
|
||||||
(acc, val) => {
|
const hours =
|
||||||
return {
|
data.production_jobs &&
|
||||||
body: acc.body + val.labhrs.aggregate.sum.mod_lb_hrs,
|
data.production_jobs.reduce(
|
||||||
ref: acc.ref + val.larhrs.aggregate.sum.mod_lb_hrs,
|
(acc, val) => {
|
||||||
total:
|
return {
|
||||||
acc.total +
|
body: acc.body + val.labhrs.aggregate.sum.mod_lb_hrs,
|
||||||
val.labhrs.aggregate.sum.mod_lb_hrs +
|
ref: acc.ref + val.larhrs.aggregate.sum.mod_lb_hrs,
|
||||||
val.larhrs.aggregate.sum.mod_lb_hrs,
|
total:
|
||||||
};
|
acc.total +
|
||||||
},
|
val.labhrs.aggregate.sum.mod_lb_hrs +
|
||||||
{ body: 0, ref: 0, total: 0 }
|
val.larhrs.aggregate.sum.mod_lb_hrs,
|
||||||
);
|
};
|
||||||
|
},
|
||||||
|
{ body: 0, ref: 0, total: 0 }
|
||||||
|
);
|
||||||
const aboveTargetHours = hours.total >= bodyshop.prodtargethrs;
|
const aboveTargetHours = hours.total >= bodyshop.prodtargethrs;
|
||||||
return (
|
return (
|
||||||
<Card {...cardProps}>
|
<Card {...cardProps} title={t("dashboard.titles.prodhrssummary")}>
|
||||||
<Space wrap style={{ flex: 1 }}>
|
<Space wrap style={{ flex: 1 }}>
|
||||||
<Statistic title={t("dashboard.labels.bodyhrs")} value={hours.body} />
|
<Statistic title={t("dashboard.labels.bodyhrs")} value={hours.body} />
|
||||||
<Statistic title={t("dashboard.labels.refhrs")} value={hours.ref} />
|
<Statistic title={t("dashboard.labels.refhrs")} value={hours.ref} />
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import Icon, { SyncOutlined } from "@ant-design/icons";
|
import Icon, { SyncOutlined } from "@ant-design/icons";
|
||||||
import { gql, useMutation, useQuery } from "@apollo/client";
|
import { gql, useMutation, useQuery } from "@apollo/client";
|
||||||
import { Button, Dropdown, Menu, notification, PageHeader, Space } from "antd";
|
import { Button, Dropdown, Menu, notification, PageHeader, Space } from "antd";
|
||||||
|
import _ from "lodash";
|
||||||
|
import moment from "moment";
|
||||||
import React, { useState } from "react";
|
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";
|
||||||
@@ -14,10 +16,15 @@ import {
|
|||||||
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 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, {
|
import DashboardMonthlyRevenueGraph, {
|
||||||
DashboardMonthlyRevenueGraphGql,
|
DashboardMonthlyRevenueGraphGql,
|
||||||
} from "../dashboard-components/monthly-revenue-graph/monthly-revenue-graph.component";
|
} from "../dashboard-components/monthly-revenue-graph/monthly-revenue-graph.component";
|
||||||
import DashboardProjectedMonthlySales from "../dashboard-components/pojected-monthly-sales/projected-monthly-sales.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 DashboardTotalProductionDollars from "../dashboard-components/total-production-dollars/total-production-dollars.component";
|
||||||
import DashboardTotalProductionHours, {
|
import DashboardTotalProductionHours, {
|
||||||
DashboardTotalProductionHoursGql,
|
DashboardTotalProductionHoursGql,
|
||||||
@@ -44,8 +51,9 @@ export function DashboardGridComponent({ currentUser, bodyshop }) {
|
|||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
...(bodyshop.associations[0].user.dashboardlayout
|
...(bodyshop.associations[0].user.dashboardlayout
|
||||||
? bodyshop.associations[0].user.dashboardlayout
|
? bodyshop.associations[0].user.dashboardlayout
|
||||||
: { items: [], layout: [], layouts: [] }),
|
: { items: [], layout: {}, layouts: [] }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { loading, error, data, refetch } = useQuery(
|
const { loading, error, data, refetch } = useQuery(
|
||||||
createDashboardQuery(state)
|
createDashboardQuery(state)
|
||||||
);
|
);
|
||||||
@@ -74,7 +82,12 @@ export function DashboardGridComponent({ currentUser, bodyshop }) {
|
|||||||
const handleRemoveComponent = (key) => {
|
const handleRemoveComponent = (key) => {
|
||||||
logImEXEvent("dashboard_remove_component", { name: key });
|
logImEXEvent("dashboard_remove_component", { name: key });
|
||||||
const idxToRemove = state.items.findIndex((i) => i.i === key);
|
const idxToRemove = state.items.findIndex((i) => i.i === key);
|
||||||
const items = state.items;
|
console.log(
|
||||||
|
"🚀 ~ file: dashboard-grid.component.jsx ~ line 81 ~ idxToRemove",
|
||||||
|
idxToRemove
|
||||||
|
);
|
||||||
|
const items = _.cloneDeep(state.items);
|
||||||
|
|
||||||
items.splice(idxToRemove, 1);
|
items.splice(idxToRemove, 1);
|
||||||
setState({ ...state, items });
|
setState({ ...state, items });
|
||||||
};
|
};
|
||||||
@@ -88,7 +101,7 @@ export function DashboardGridComponent({ currentUser, bodyshop }) {
|
|||||||
{
|
{
|
||||||
i: e.key,
|
i: e.key,
|
||||||
x: (state.items.length * 2) % (state.cols || 12),
|
x: (state.items.length * 2) % (state.cols || 12),
|
||||||
y: Infinity, // puts it at the bottom
|
y: 99, // puts it at the bottom
|
||||||
w: componentList[e.key].w || 2,
|
w: componentList[e.key].w || 2,
|
||||||
h: componentList[e.key].h || 2,
|
h: componentList[e.key].h || 2,
|
||||||
},
|
},
|
||||||
@@ -100,11 +113,6 @@ export function DashboardGridComponent({ currentUser, bodyshop }) {
|
|||||||
() => GenerateDashboardData(data),
|
() => GenerateDashboardData(data),
|
||||||
[data]
|
[data]
|
||||||
);
|
);
|
||||||
|
|
||||||
// const onBreakpointChange = (breakpoint, cols) => {
|
|
||||||
// setState({ ...state, breakpoint: breakpoint, cols: cols });
|
|
||||||
// };
|
|
||||||
|
|
||||||
const existingLayoutKeys = state.items.map((i) => i.i);
|
const existingLayoutKeys = state.items.map((i) => i.i);
|
||||||
const addComponentOverlay = (
|
const addComponentOverlay = (
|
||||||
<Menu onClick={handleAddComponent}>
|
<Menu onClick={handleAddComponent}>
|
||||||
@@ -149,7 +157,14 @@ export function DashboardGridComponent({ currentUser, bodyshop }) {
|
|||||||
{state.items.map((item, index) => {
|
{state.items.map((item, index) => {
|
||||||
const TheComponent = componentList[item.i].component;
|
const TheComponent = componentList[item.i].component;
|
||||||
return (
|
return (
|
||||||
<div key={item.i} data-grid={item}>
|
<div
|
||||||
|
key={item.i}
|
||||||
|
data-grid={{
|
||||||
|
...item,
|
||||||
|
minH: componentList[item.i].minH || 1,
|
||||||
|
minW: componentList[item.i].minW || 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<LoadingSkeleton loading={loading}>
|
<LoadingSkeleton loading={loading}>
|
||||||
<Icon
|
<Icon
|
||||||
component={MdClose}
|
component={MdClose}
|
||||||
@@ -196,7 +211,7 @@ const componentList = {
|
|||||||
ProjectedMonthlySales: {
|
ProjectedMonthlySales: {
|
||||||
label: "Projected Monthly Sales",
|
label: "Projected Monthly Sales",
|
||||||
component: DashboardProjectedMonthlySales,
|
component: DashboardProjectedMonthlySales,
|
||||||
gqlFragment: null,
|
gqlFragment: DashboardProjectedMonthlySalesGql,
|
||||||
w: 2,
|
w: 2,
|
||||||
h: 1,
|
h: 1,
|
||||||
},
|
},
|
||||||
@@ -204,6 +219,35 @@ const componentList = {
|
|||||||
label: "Monthly Sales Graph",
|
label: "Monthly Sales Graph",
|
||||||
component: DashboardMonthlyRevenueGraph,
|
component: DashboardMonthlyRevenueGraph,
|
||||||
gqlFragment: DashboardMonthlyRevenueGraphGql,
|
gqlFragment: DashboardMonthlyRevenueGraphGql,
|
||||||
|
w: 4,
|
||||||
|
h: 2,
|
||||||
|
minW: 4,
|
||||||
|
minH: 2,
|
||||||
|
},
|
||||||
|
MonthlyJobCosting: {
|
||||||
|
label: "Monthly Job Costing",
|
||||||
|
component: DashboardMonthlyJobCosting,
|
||||||
|
gqlFragment: null,
|
||||||
|
minW: 6,
|
||||||
|
minH: 3,
|
||||||
|
w: 6,
|
||||||
|
h: 3,
|
||||||
|
},
|
||||||
|
MonthlyPartsSales: {
|
||||||
|
label: "Monthly Parts Sales",
|
||||||
|
component: DashboardMonthlyPartsSales,
|
||||||
|
gqlFragment: null,
|
||||||
|
minW: 2,
|
||||||
|
minH: 2,
|
||||||
|
w: 2,
|
||||||
|
h: 2,
|
||||||
|
},
|
||||||
|
MonthlyLaborSales: {
|
||||||
|
label: "Monthly Parts Sales",
|
||||||
|
component: DashboardMonthlyLaborSales,
|
||||||
|
gqlFragment: null,
|
||||||
|
minW: 2,
|
||||||
|
minH: 2,
|
||||||
w: 2,
|
w: 2,
|
||||||
h: 2,
|
h: 2,
|
||||||
},
|
},
|
||||||
@@ -216,6 +260,46 @@ const createDashboardQuery = (state) => {
|
|||||||
return gql`
|
return gql`
|
||||||
query QUERY_DASHBOARD_DETAILS {
|
query QUERY_DASHBOARD_DETAILS {
|
||||||
${componentBasedAdditions}
|
${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 } }) {
|
production_jobs: jobs(where: { inproduction: { _eq: true } }) {
|
||||||
id
|
id
|
||||||
ro_number
|
ro_number
|
||||||
|
|||||||
@@ -127,13 +127,28 @@
|
|||||||
.dashboard-card {
|
.dashboard-card {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
.ant-card-body {
|
.ant-card-body {
|
||||||
// background-color: red;
|
height: 80%;
|
||||||
height: 90%;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
// // background-color: red;
|
||||||
flex-direction: column;
|
// height: 90%;
|
||||||
align-items: center;
|
// width: 100%;
|
||||||
|
// padding: 8px;
|
||||||
|
// display: flex;
|
||||||
|
// flex-direction: column;
|
||||||
|
// align-items: center;
|
||||||
|
// justify-content: center;
|
||||||
|
}
|
||||||
|
.ant-spin-nested-loading {
|
||||||
|
height: 100%;
|
||||||
|
.ant-spin-container {
|
||||||
|
height: 100%;
|
||||||
|
.ant-table {
|
||||||
|
height: 100%;
|
||||||
|
.ant-table-container {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import Icon, {
|
|||||||
BarChartOutlined,
|
BarChartOutlined,
|
||||||
CarFilled,
|
CarFilled,
|
||||||
ClockCircleFilled,
|
ClockCircleFilled,
|
||||||
|
DashboardFilled,
|
||||||
DollarCircleFilled,
|
DollarCircleFilled,
|
||||||
ExportOutlined,
|
ExportOutlined,
|
||||||
FieldTimeOutlined,
|
FieldTimeOutlined,
|
||||||
@@ -18,7 +19,6 @@ import Icon, {
|
|||||||
ScheduleOutlined,
|
ScheduleOutlined,
|
||||||
SettingOutlined,
|
SettingOutlined,
|
||||||
TeamOutlined,
|
TeamOutlined,
|
||||||
DashboardFilled,
|
|
||||||
ToolFilled,
|
ToolFilled,
|
||||||
UnorderedListOutlined,
|
UnorderedListOutlined,
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
@@ -46,7 +46,6 @@ import {
|
|||||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
import { signOutStart } from "../../redux/user/user.actions";
|
import { signOutStart } from "../../redux/user/user.actions";
|
||||||
import { selectCurrentUser } from "../../redux/user/user.selectors";
|
import { selectCurrentUser } from "../../redux/user/user.selectors";
|
||||||
import GlobalSearch from "../global-search/global-search.component";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
currentUser: selectCurrentUser,
|
currentUser: selectCurrentUser,
|
||||||
|
|||||||
@@ -672,12 +672,16 @@
|
|||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"bodyhrs": "Body Hrs",
|
"bodyhrs": "Body Hrs",
|
||||||
"dollarsinproduction": "Dollars in Production",
|
"dollarsinproduction": "Dollars in Production'",
|
||||||
"prodhrs": "Production Hrs",
|
"prodhrs": "Production Hrs",
|
||||||
"refhrs": "Refinish Hrs"
|
"refhrs": "Refinish Hrs"
|
||||||
},
|
},
|
||||||
"titles": {
|
"titles": {
|
||||||
|
"monthlyjobcosting": "Monthly Job Costing ",
|
||||||
|
"monthlylaborsales": "Monthly Labor Sales",
|
||||||
|
"monthlypartssales": "Monthly Parts Sales",
|
||||||
"monthlyrevenuegraph": "Monthly Revenue Graph",
|
"monthlyrevenuegraph": "Monthly Revenue Graph",
|
||||||
|
"prodhrssummary": "Production Hours Summary",
|
||||||
"productiondollars": "Total dollars in production",
|
"productiondollars": "Total dollars in production",
|
||||||
"productionhours": "Total hours in production",
|
"productionhours": "Total hours in production",
|
||||||
"projectedmonthlysales": "Projected Monthly Sales"
|
"projectedmonthlysales": "Projected Monthly Sales"
|
||||||
|
|||||||
@@ -677,7 +677,11 @@
|
|||||||
"refhrs": ""
|
"refhrs": ""
|
||||||
},
|
},
|
||||||
"titles": {
|
"titles": {
|
||||||
|
"monthlyjobcosting": "",
|
||||||
|
"monthlylaborsales": "",
|
||||||
|
"monthlypartssales": "",
|
||||||
"monthlyrevenuegraph": "",
|
"monthlyrevenuegraph": "",
|
||||||
|
"prodhrssummary": "",
|
||||||
"productiondollars": "",
|
"productiondollars": "",
|
||||||
"productionhours": "",
|
"productionhours": "",
|
||||||
"projectedmonthlysales": ""
|
"projectedmonthlysales": ""
|
||||||
|
|||||||
@@ -677,7 +677,11 @@
|
|||||||
"refhrs": ""
|
"refhrs": ""
|
||||||
},
|
},
|
||||||
"titles": {
|
"titles": {
|
||||||
|
"monthlyjobcosting": "",
|
||||||
|
"monthlylaborsales": "",
|
||||||
|
"monthlypartssales": "",
|
||||||
"monthlyrevenuegraph": "",
|
"monthlyrevenuegraph": "",
|
||||||
|
"prodhrssummary": "",
|
||||||
"productiondollars": "",
|
"productiondollars": "",
|
||||||
"productionhours": "",
|
"productionhours": "",
|
||||||
"projectedmonthlysales": ""
|
"projectedmonthlysales": ""
|
||||||
|
|||||||
Reference in New Issue
Block a user