@@ -1,169 +1,159 @@
|
||||
import { Card } from "antd";
|
||||
import {Card} from "antd";
|
||||
import _ from "lodash";
|
||||
import dayjs from "../../../utils/day";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Bar,
|
||||
CartesianGrid,
|
||||
ComposedChart,
|
||||
Legend,
|
||||
Line,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {Bar, CartesianGrid, ComposedChart, Legend, Line, ResponsiveContainer, Tooltip, XAxis, YAxis,} from "recharts";
|
||||
import * as Utils from "../../scoreboard-targets-table/scoreboard-targets-table.util";
|
||||
import DashboardRefreshRequired from "../refresh-required.component";
|
||||
|
||||
export default function DashboardMonthlyEmployeeEfficiency({
|
||||
data,
|
||||
...cardProps
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
if (!data) return null;
|
||||
if (!data.monthly_employee_efficiency)
|
||||
return <DashboardRefreshRequired {...cardProps} />;
|
||||
data,
|
||||
...cardProps
|
||||
}) {
|
||||
const {t} = useTranslation();
|
||||
if (!data) return null;
|
||||
if (!data.monthly_employee_efficiency)
|
||||
return <DashboardRefreshRequired {...cardProps} />;
|
||||
|
||||
const ticketsByDate = _.groupBy(data.monthly_employee_efficiency, (item) =>
|
||||
dayjs(item.date).format("YYYY-MM-DD")
|
||||
);
|
||||
const ticketsByDate = _.groupBy(data.monthly_employee_efficiency, (item) =>
|
||||
dayjs(item.date).format("YYYY-MM-DD")
|
||||
);
|
||||
|
||||
const listOfDays = Utils.ListOfDaysInCurrentMonth();
|
||||
const listOfDays = Utils.ListOfDaysInCurrentMonth();
|
||||
|
||||
const chartData = listOfDays.reduce((acc, val) => {
|
||||
//Sum up the current day.
|
||||
let dailyHrs;
|
||||
if (!!ticketsByDate[val]) {
|
||||
dailyHrs = ticketsByDate[val].reduce(
|
||||
(dayAcc, dayVal) => {
|
||||
return {
|
||||
actual: dayAcc.actual + dayVal.actualhrs,
|
||||
productive: dayAcc.productive + dayVal.productivehrs,
|
||||
};
|
||||
},
|
||||
{ actual: 0, productive: 0 }
|
||||
);
|
||||
} else {
|
||||
dailyHrs = { actual: 0, productive: 0 };
|
||||
}
|
||||
const chartData = listOfDays.reduce((acc, val) => {
|
||||
//Sum up the current day.
|
||||
let dailyHrs;
|
||||
if (!!ticketsByDate[val]) {
|
||||
dailyHrs = ticketsByDate[val].reduce(
|
||||
(dayAcc, dayVal) => {
|
||||
return {
|
||||
actual: dayAcc.actual + dayVal.actualhrs,
|
||||
productive: dayAcc.productive + dayVal.productivehrs,
|
||||
};
|
||||
},
|
||||
{actual: 0, productive: 0}
|
||||
);
|
||||
} else {
|
||||
dailyHrs = {actual: 0, productive: 0};
|
||||
}
|
||||
|
||||
const dailyEfficiency =
|
||||
((dailyHrs.productive - dailyHrs.actual) / dailyHrs.actual + 1) * 100;
|
||||
const dailyEfficiency =
|
||||
((dailyHrs.productive - dailyHrs.actual) / dailyHrs.actual + 1) * 100;
|
||||
|
||||
const theValue = {
|
||||
date: dayjs(val).format("DD"),
|
||||
// ...dailyHrs,
|
||||
actual: dailyHrs.actual.toFixed(1),
|
||||
productive: dailyHrs.productive.toFixed(1),
|
||||
dailyEfficiency: isNaN(dailyEfficiency) ? 0 : dailyEfficiency.toFixed(1),
|
||||
accActual:
|
||||
acc.length > 0
|
||||
? acc[acc.length - 1].accActual + dailyHrs.actual
|
||||
: dailyHrs.actual,
|
||||
const theValue = {
|
||||
date: dayjs(val).format("DD"),
|
||||
// ...dailyHrs,
|
||||
actual: dailyHrs.actual.toFixed(1),
|
||||
productive: dailyHrs.productive.toFixed(1),
|
||||
dailyEfficiency: isNaN(dailyEfficiency) ? 0 : dailyEfficiency.toFixed(1),
|
||||
accActual:
|
||||
acc.length > 0
|
||||
? acc[acc.length - 1].accActual + dailyHrs.actual
|
||||
: dailyHrs.actual,
|
||||
|
||||
accProductive:
|
||||
acc.length > 0
|
||||
? acc[acc.length - 1].accProductive + dailyHrs.productive
|
||||
: dailyHrs.productive,
|
||||
accEfficiency: 0,
|
||||
};
|
||||
accProductive:
|
||||
acc.length > 0
|
||||
? acc[acc.length - 1].accProductive + dailyHrs.productive
|
||||
: dailyHrs.productive,
|
||||
accEfficiency: 0,
|
||||
};
|
||||
|
||||
theValue.accEfficiency =
|
||||
((theValue.accProductive - theValue.accActual) /
|
||||
(theValue.accActual || 0) +
|
||||
1) *
|
||||
100;
|
||||
theValue.accEfficiency =
|
||||
((theValue.accProductive - theValue.accActual) /
|
||||
(theValue.accActual || 0) +
|
||||
1) *
|
||||
100;
|
||||
|
||||
if (isNaN(theValue.accEfficiency)) {
|
||||
theValue.accEfficiency = 0;
|
||||
} else {
|
||||
theValue.accEfficiency = theValue.accEfficiency.toFixed(1);
|
||||
}
|
||||
if (isNaN(theValue.accEfficiency)) {
|
||||
theValue.accEfficiency = 0;
|
||||
} else {
|
||||
theValue.accEfficiency = theValue.accEfficiency.toFixed(1);
|
||||
}
|
||||
|
||||
return [...acc, theValue];
|
||||
}, []);
|
||||
return [...acc, theValue];
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={t("dashboard.titles.monthlyemployeeefficiency")}
|
||||
{...cardProps}
|
||||
>
|
||||
<div style={{ height: "100%" }}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<ComposedChart
|
||||
data={chartData}
|
||||
margin={{ top: 20, right: 20, bottom: 20, left: 20 }}
|
||||
>
|
||||
<CartesianGrid stroke="#f5f5f5" />
|
||||
<XAxis dataKey="date" />
|
||||
<YAxis
|
||||
yAxisId="left"
|
||||
orientation="left"
|
||||
stroke="#8884d8"
|
||||
unit=" hrs"
|
||||
/>
|
||||
<YAxis
|
||||
yAxisId="right"
|
||||
orientation="right"
|
||||
stroke="#82ca9d"
|
||||
unit="%"
|
||||
/>
|
||||
<Tooltip />
|
||||
<Legend />
|
||||
<Line
|
||||
yAxisId="right"
|
||||
name="Accumulated Efficiency"
|
||||
type="monotone"
|
||||
unit="%"
|
||||
dataKey="accEfficiency"
|
||||
stroke="#152228"
|
||||
connectNulls
|
||||
// activeDot={{ r: 8 }}
|
||||
/>
|
||||
<Line
|
||||
name="Daily Efficiency"
|
||||
yAxisId="right"
|
||||
unit="%"
|
||||
type="monotone"
|
||||
connectNulls
|
||||
dataKey="dailyEfficiency"
|
||||
stroke="#d31717"
|
||||
/>
|
||||
<Bar
|
||||
name="Actual Hours"
|
||||
dataKey="actual"
|
||||
yAxisId="left"
|
||||
unit=" hrs"
|
||||
//stackId="day"
|
||||
barSize={20}
|
||||
fill="#102568"
|
||||
format={"0.0"}
|
||||
/>
|
||||
<Bar
|
||||
name="Productive Hours"
|
||||
dataKey="productive"
|
||||
yAxisId="left"
|
||||
unit=" hrs"
|
||||
//stackId="day"
|
||||
barSize={20}
|
||||
fill="#017664"
|
||||
format={"0.0"}
|
||||
/>
|
||||
</ComposedChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
return (
|
||||
<Card
|
||||
title={t("dashboard.titles.monthlyemployeeefficiency")}
|
||||
{...cardProps}
|
||||
>
|
||||
<div style={{height: "100%"}}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<ComposedChart
|
||||
data={chartData}
|
||||
margin={{top: 20, right: 20, bottom: 20, left: 20}}
|
||||
>
|
||||
<CartesianGrid stroke="#f5f5f5"/>
|
||||
<XAxis dataKey="date"/>
|
||||
<YAxis
|
||||
yAxisId="left"
|
||||
orientation="left"
|
||||
stroke="#8884d8"
|
||||
unit=" hrs"
|
||||
/>
|
||||
<YAxis
|
||||
yAxisId="right"
|
||||
orientation="right"
|
||||
stroke="#82ca9d"
|
||||
unit="%"
|
||||
/>
|
||||
<Tooltip/>
|
||||
<Legend/>
|
||||
<Line
|
||||
yAxisId="right"
|
||||
name="Accumulated Efficiency"
|
||||
type="monotone"
|
||||
unit="%"
|
||||
dataKey="accEfficiency"
|
||||
stroke="#152228"
|
||||
connectNulls
|
||||
// activeDot={{ r: 8 }}
|
||||
/>
|
||||
<Line
|
||||
name="Daily Efficiency"
|
||||
yAxisId="right"
|
||||
unit="%"
|
||||
type="monotone"
|
||||
connectNulls
|
||||
dataKey="dailyEfficiency"
|
||||
stroke="#d31717"
|
||||
/>
|
||||
<Bar
|
||||
name="Actual Hours"
|
||||
dataKey="actual"
|
||||
yAxisId="left"
|
||||
unit=" hrs"
|
||||
//stackId="day"
|
||||
barSize={20}
|
||||
fill="#102568"
|
||||
format={"0.0"}
|
||||
/>
|
||||
<Bar
|
||||
name="Productive Hours"
|
||||
dataKey="productive"
|
||||
yAxisId="left"
|
||||
unit=" hrs"
|
||||
//stackId="day"
|
||||
barSize={20}
|
||||
fill="#017664"
|
||||
format={"0.0"}
|
||||
/>
|
||||
</ComposedChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export const DashboardMonthlyEmployeeEfficiencyGql = `
|
||||
monthly_employee_efficiency: timetickets(where: {_and: [{date: {_gte: "${dayjs()
|
||||
.startOf("month")
|
||||
.format("YYYY-MM-DD")}"}},{date: {_lte: "${dayjs()
|
||||
.endOf("month")
|
||||
.format("YYYY-MM-DD")}"}} ]}) {
|
||||
.endOf("month")
|
||||
.format("YYYY-MM-DD")}"}} ]}) {
|
||||
actualhrs
|
||||
productivehrs
|
||||
employeeid
|
||||
|
||||
@@ -1,164 +1,164 @@
|
||||
import { Card, Input, Space, Table, Typography } from "antd";
|
||||
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 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";
|
||||
import {pageLimit} from "../../../utils/config";
|
||||
|
||||
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: {},
|
||||
});
|
||||
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);
|
||||
}
|
||||
}
|
||||
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]);
|
||||
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,
|
||||
},
|
||||
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.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)),
|
||||
{
|
||||
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())
|
||||
);
|
||||
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: pageLimit }}
|
||||
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>
|
||||
);
|
||||
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: pageLimit}}
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,67 +1,67 @@
|
||||
import { Card } from "antd";
|
||||
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 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} />;
|
||||
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 = {};
|
||||
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)
|
||||
);
|
||||
data.monthly_sales.forEach((job) => {
|
||||
job.joblines.forEach((jobline) => {
|
||||
if (!jobline.mod_lbr_ty) return;
|
||||
if (!laborData[jobline.mod_lbr_ty])
|
||||
laborData[jobline.mod_lbr_ty] = Dinero();
|
||||
laborData[jobline.mod_lbr_ty] = laborData[jobline.mod_lbr_ty].add(
|
||||
Dinero({
|
||||
amount: Math.round(
|
||||
(job[`rate_${jobline.mod_lbr_ty.toLowerCase()}`] || 0) * 100
|
||||
),
|
||||
}).multiply(jobline.mod_lb_hrs || 0)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const chartData = Object.keys(laborData).map((key) => {
|
||||
return {
|
||||
name: t(`joblines.fields.lbr_types.${key.toUpperCase()}`),
|
||||
value: laborData[key].getAmount() / 100,
|
||||
color: pieColor(key.toUpperCase()),
|
||||
};
|
||||
});
|
||||
const chartData = Object.keys(laborData).map((key) => {
|
||||
return {
|
||||
name: t(`joblines.fields.lbr_types.${key.toUpperCase()}`),
|
||||
value: laborData[key].getAmount() / 100,
|
||||
color: pieColor(key.toUpperCase()),
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<Card title={t("dashboard.titles.monthlylaborsales")} {...cardProps}>
|
||||
<div style={{ height: "100%" }}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart margin={0} padding={0}>
|
||||
<Pie
|
||||
data={chartData}
|
||||
activeIndex={activeIndex}
|
||||
activeShape={renderActiveShape}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius="60%"
|
||||
// outerRadius={80}
|
||||
fill="#8884d8"
|
||||
dataKey="value"
|
||||
onMouseEnter={(throwaway, index) => setActiveIndex(index)}
|
||||
>
|
||||
{chartData.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Pie>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
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 = `
|
||||
@@ -69,75 +69,75 @@ export const DashboardMonthlyRevenueGraphGql = `
|
||||
`;
|
||||
|
||||
const pieColor = (type) => {
|
||||
if (type === "LAA") return "lightgreen";
|
||||
else if (type === "LAB") return "dodgerblue";
|
||||
else if (type === "LAD") return "aliceblue";
|
||||
else if (type === "LAE") return "seafoam";
|
||||
else if (type === "LAG") return "chartreuse";
|
||||
else if (type === "LAF") return "magenta";
|
||||
else if (type === "LAM") return "gold";
|
||||
else if (type === "LAR") return "crimson";
|
||||
else if (type === "LAU") return "slategray";
|
||||
else if (type === "LA1") return "slategray";
|
||||
else if (type === "LA2") return "slategray";
|
||||
else if (type === "LA3") return "slategray";
|
||||
else if (type === "LA4") return "slategray";
|
||||
return "slategray";
|
||||
if (type === "LAA") return "lightgreen";
|
||||
else if (type === "LAB") return "dodgerblue";
|
||||
else if (type === "LAD") return "aliceblue";
|
||||
else if (type === "LAE") return "seafoam";
|
||||
else if (type === "LAG") return "chartreuse";
|
||||
else if (type === "LAF") return "magenta";
|
||||
else if (type === "LAM") return "gold";
|
||||
else if (type === "LAR") return "crimson";
|
||||
else if (type === "LAU") return "slategray";
|
||||
else if (type === "LA1") return "slategray";
|
||||
else if (type === "LA2") return "slategray";
|
||||
else if (type === "LA3") return "slategray";
|
||||
else if (type === "LA4") return "slategray";
|
||||
return "slategray";
|
||||
};
|
||||
|
||||
const renderActiveShape = (props) => {
|
||||
//const RADIAN = Math.PI / 180;
|
||||
const {
|
||||
cx,
|
||||
cy,
|
||||
//midAngle,
|
||||
innerRadius,
|
||||
outerRadius,
|
||||
startAngle,
|
||||
endAngle,
|
||||
fill,
|
||||
payload,
|
||||
// percent,
|
||||
value,
|
||||
} = props;
|
||||
// const sin = Math.sin(-RADIAN * midAngle);
|
||||
// const cos = Math.cos(-RADIAN * midAngle);
|
||||
// // const sx = cx + (outerRadius + 10) * cos;
|
||||
// const sy = cy + (outerRadius + 10) * sin;
|
||||
// const mx = cx + (outerRadius + 30) * cos;
|
||||
// const my = cy + (outerRadius + 30) * sin;
|
||||
// //const ex = mx + (cos >= 0 ? 1 : -1) * 22;
|
||||
// const ey = my;
|
||||
//const textAnchor = cos >= 0 ? "start" : "end";
|
||||
//const RADIAN = Math.PI / 180;
|
||||
const {
|
||||
cx,
|
||||
cy,
|
||||
//midAngle,
|
||||
innerRadius,
|
||||
outerRadius,
|
||||
startAngle,
|
||||
endAngle,
|
||||
fill,
|
||||
payload,
|
||||
// percent,
|
||||
value,
|
||||
} = props;
|
||||
// const sin = Math.sin(-RADIAN * midAngle);
|
||||
// const cos = Math.cos(-RADIAN * midAngle);
|
||||
// // const sx = cx + (outerRadius + 10) * cos;
|
||||
// const sy = cy + (outerRadius + 10) * sin;
|
||||
// const mx = cx + (outerRadius + 30) * cos;
|
||||
// const my = cy + (outerRadius + 30) * sin;
|
||||
// //const ex = mx + (cos >= 0 ? 1 : -1) * 22;
|
||||
// const ey = my;
|
||||
//const textAnchor = cos >= 0 ? "start" : "end";
|
||||
|
||||
return (
|
||||
<g>
|
||||
<text x={cx} y={cy} dy={0} textAnchor="middle" fill={fill}>
|
||||
{payload.name}
|
||||
</text>
|
||||
<text x={cx} y={cy} dy={16} textAnchor="middle" fill={fill}>
|
||||
{Dinero({ amount: Math.round(value * 100) }).toFormat()}
|
||||
</text>
|
||||
<Sector
|
||||
cx={cx}
|
||||
cy={cy}
|
||||
innerRadius={innerRadius}
|
||||
outerRadius={outerRadius}
|
||||
startAngle={startAngle}
|
||||
endAngle={endAngle}
|
||||
fill={fill}
|
||||
/>
|
||||
<Sector
|
||||
cx={cx}
|
||||
cy={cy}
|
||||
startAngle={startAngle}
|
||||
endAngle={endAngle}
|
||||
innerRadius={outerRadius + 6}
|
||||
outerRadius={outerRadius + 10}
|
||||
fill={fill}
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
return (
|
||||
<g>
|
||||
<text x={cx} y={cy} dy={0} textAnchor="middle" fill={fill}>
|
||||
{payload.name}
|
||||
</text>
|
||||
<text x={cx} y={cy} dy={16} textAnchor="middle" fill={fill}>
|
||||
{Dinero({amount: Math.round(value * 100)}).toFormat()}
|
||||
</text>
|
||||
<Sector
|
||||
cx={cx}
|
||||
cy={cy}
|
||||
innerRadius={innerRadius}
|
||||
outerRadius={outerRadius}
|
||||
startAngle={startAngle}
|
||||
endAngle={endAngle}
|
||||
fill={fill}
|
||||
/>
|
||||
<Sector
|
||||
cx={cx}
|
||||
cy={cy}
|
||||
startAngle={startAngle}
|
||||
endAngle={endAngle}
|
||||
innerRadius={outerRadius + 6}
|
||||
outerRadius={outerRadius + 10}
|
||||
fill={fill}
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
};
|
||||
// <path
|
||||
// d={`M${sx},${sy}L${mx},${my}L${ex},${ey}`}
|
||||
|
||||
@@ -1,136 +1,136 @@
|
||||
import { Card } from "antd";
|
||||
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 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} />;
|
||||
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 = {};
|
||||
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
|
||||
)
|
||||
);
|
||||
data.monthly_sales.forEach((job) => {
|
||||
job.joblines.forEach((jobline) => {
|
||||
if (!jobline.part_type) return;
|
||||
if (!partData[jobline.part_type]) partData[jobline.part_type] = Dinero();
|
||||
partData[jobline.part_type] = partData[jobline.part_type].add(
|
||||
Dinero({amount: Math.round((jobline.act_price || 0) * 100)}).multiply(
|
||||
jobline.part_qty || 0
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const chartData = Object.keys(partData).map((key) => {
|
||||
return {
|
||||
name: t(`joblines.fields.part_types.${key.toUpperCase()}`),
|
||||
value: partData[key].getAmount() / 100,
|
||||
color: pieColor(key.toUpperCase()),
|
||||
};
|
||||
});
|
||||
const chartData = Object.keys(partData).map((key) => {
|
||||
return {
|
||||
name: t(`joblines.fields.part_types.${key.toUpperCase()}`),
|
||||
value: partData[key].getAmount() / 100,
|
||||
color: pieColor(key.toUpperCase()),
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<Card title={t("dashboard.titles.monthlypartssales")} {...cardProps}>
|
||||
<div style={{ height: "100%" }}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart margin={0} padding={0}>
|
||||
<Pie
|
||||
data={chartData}
|
||||
activeIndex={activeIndex}
|
||||
activeShape={renderActiveShape}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius="60%"
|
||||
// outerRadius={80}
|
||||
fill="#8884d8"
|
||||
dataKey="value"
|
||||
onMouseEnter={(throwaway, index) => setActiveIndex(index)}
|
||||
>
|
||||
{chartData.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Pie>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
return (
|
||||
<Card title={t("dashboard.titles.monthlypartssales")} {...cardProps}>
|
||||
<div style={{height: "100%"}}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart margin={0} padding={0}>
|
||||
<Pie
|
||||
data={chartData}
|
||||
activeIndex={activeIndex}
|
||||
activeShape={renderActiveShape}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius="60%"
|
||||
// outerRadius={80}
|
||||
fill="#8884d8"
|
||||
dataKey="value"
|
||||
onMouseEnter={(throwaway, index) => setActiveIndex(index)}
|
||||
>
|
||||
{chartData.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color}/>
|
||||
))}
|
||||
</Pie>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export const DashboardMonthlyRevenueGraphGql = `
|
||||
|
||||
`;
|
||||
const pieColor = (type) => {
|
||||
if (type === "PAA") return "darkgreen";
|
||||
else if (type === "PAC") return "green";
|
||||
else if (type === "PAE") return "gold";
|
||||
else if (type === "PAG") return "seafoam";
|
||||
else if (type === "PAL") return "chartreuse";
|
||||
else if (type === "PAM") return "magenta";
|
||||
else if (type === "PAN") return "crimson";
|
||||
else if (type === "PAO") return "gold";
|
||||
else if (type === "PAP") return "crimson";
|
||||
else if (type === "PAR") return "indigo";
|
||||
else if (type === "PAS") return "dodgerblue";
|
||||
else if (type === "PASL") return "dodgerblue";
|
||||
return "slategray";
|
||||
if (type === "PAA") return "darkgreen";
|
||||
else if (type === "PAC") return "green";
|
||||
else if (type === "PAE") return "gold";
|
||||
else if (type === "PAG") return "seafoam";
|
||||
else if (type === "PAL") return "chartreuse";
|
||||
else if (type === "PAM") return "magenta";
|
||||
else if (type === "PAN") return "crimson";
|
||||
else if (type === "PAO") return "gold";
|
||||
else if (type === "PAP") return "crimson";
|
||||
else if (type === "PAR") return "indigo";
|
||||
else if (type === "PAS") return "dodgerblue";
|
||||
else if (type === "PASL") return "dodgerblue";
|
||||
return "slategray";
|
||||
};
|
||||
|
||||
const renderActiveShape = (props) => {
|
||||
// const RADIAN = Math.PI / 180;
|
||||
const {
|
||||
cx,
|
||||
cy,
|
||||
// midAngle,
|
||||
innerRadius,
|
||||
outerRadius,
|
||||
startAngle,
|
||||
endAngle,
|
||||
fill,
|
||||
payload,
|
||||
// percent,
|
||||
value,
|
||||
} = props;
|
||||
// const sin = Math.sin(-RADIAN * midAngle);
|
||||
// const cos = Math.cos(-RADIAN * midAngle);
|
||||
// const sx = cx + (outerRadius + 10) * cos;
|
||||
//const sy = cy + (outerRadius + 10) * sin;
|
||||
// const mx = cx + (outerRadius + 30) * cos;
|
||||
//const my = cy + (outerRadius + 30) * sin;
|
||||
// const ex = mx + (cos >= 0 ? 1 : -1) * 22;
|
||||
// const ey = my;
|
||||
// const textAnchor = cos >= 0 ? "start" : "end";
|
||||
// const RADIAN = Math.PI / 180;
|
||||
const {
|
||||
cx,
|
||||
cy,
|
||||
// midAngle,
|
||||
innerRadius,
|
||||
outerRadius,
|
||||
startAngle,
|
||||
endAngle,
|
||||
fill,
|
||||
payload,
|
||||
// percent,
|
||||
value,
|
||||
} = props;
|
||||
// const sin = Math.sin(-RADIAN * midAngle);
|
||||
// const cos = Math.cos(-RADIAN * midAngle);
|
||||
// const sx = cx + (outerRadius + 10) * cos;
|
||||
//const sy = cy + (outerRadius + 10) * sin;
|
||||
// const mx = cx + (outerRadius + 30) * cos;
|
||||
//const my = cy + (outerRadius + 30) * sin;
|
||||
// const ex = mx + (cos >= 0 ? 1 : -1) * 22;
|
||||
// const ey = my;
|
||||
// const textAnchor = cos >= 0 ? "start" : "end";
|
||||
|
||||
return (
|
||||
<g>
|
||||
<text x={cx} y={cy} dy={0} textAnchor="middle" fill={fill}>
|
||||
{payload.name}
|
||||
</text>
|
||||
<text x={cx} y={cy} dy={16} textAnchor="middle" fill={fill}>
|
||||
{Dinero({ amount: Math.round(value * 100) }).toFormat()}
|
||||
</text>
|
||||
<Sector
|
||||
cx={cx}
|
||||
cy={cy}
|
||||
innerRadius={innerRadius}
|
||||
outerRadius={outerRadius}
|
||||
startAngle={startAngle}
|
||||
endAngle={endAngle}
|
||||
fill={fill}
|
||||
/>
|
||||
<Sector
|
||||
cx={cx}
|
||||
cy={cy}
|
||||
startAngle={startAngle}
|
||||
endAngle={endAngle}
|
||||
innerRadius={outerRadius + 6}
|
||||
outerRadius={outerRadius + 10}
|
||||
fill={fill}
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
return (
|
||||
<g>
|
||||
<text x={cx} y={cy} dy={0} textAnchor="middle" fill={fill}>
|
||||
{payload.name}
|
||||
</text>
|
||||
<text x={cx} y={cy} dy={16} textAnchor="middle" fill={fill}>
|
||||
{Dinero({amount: Math.round(value * 100)}).toFormat()}
|
||||
</text>
|
||||
<Sector
|
||||
cx={cx}
|
||||
cy={cy}
|
||||
innerRadius={innerRadius}
|
||||
outerRadius={outerRadius}
|
||||
startAngle={startAngle}
|
||||
endAngle={endAngle}
|
||||
fill={fill}
|
||||
/>
|
||||
<Sector
|
||||
cx={cx}
|
||||
cy={cy}
|
||||
startAngle={startAngle}
|
||||
endAngle={endAngle}
|
||||
innerRadius={outerRadius + 6}
|
||||
outerRadius={outerRadius + 10}
|
||||
fill={fill}
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,93 +1,83 @@
|
||||
import { Card } from "antd";
|
||||
import {Card} from "antd";
|
||||
import dayjs from "../../../utils/day";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import _ from "lodash";
|
||||
import {
|
||||
Area,
|
||||
Bar,
|
||||
CartesianGrid,
|
||||
ComposedChart,
|
||||
Legend,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
import {Area, Bar, CartesianGrid, ComposedChart, Legend, ResponsiveContainer, Tooltip, XAxis, YAxis,} from "recharts";
|
||||
import Dinero from "dinero.js";
|
||||
import * as Utils from "../../scoreboard-targets-table/scoreboard-targets-table.util";
|
||||
import DashboardRefreshRequired from "../refresh-required.component";
|
||||
|
||||
export default function DashboardMonthlyRevenueGraph({ data, ...cardProps }) {
|
||||
const { t } = useTranslation();
|
||||
if (!data) return null;
|
||||
if (!data.monthly_sales) return <DashboardRefreshRequired {...cardProps} />;
|
||||
export default function DashboardMonthlyRevenueGraph({data, ...cardProps}) {
|
||||
const {t} = useTranslation();
|
||||
if (!data) return null;
|
||||
if (!data.monthly_sales) return <DashboardRefreshRequired {...cardProps} />;
|
||||
|
||||
const jobsByDate = _.groupBy(data.monthly_sales, (item) =>
|
||||
dayjs(item.date_invoiced).format("YYYY-MM-DD")
|
||||
);
|
||||
const jobsByDate = _.groupBy(data.monthly_sales, (item) =>
|
||||
dayjs(item.date_invoiced).format("YYYY-MM-DD")
|
||||
);
|
||||
|
||||
const listOfDays = Utils.ListOfDaysInCurrentMonth();
|
||||
const listOfDays = Utils.ListOfDaysInCurrentMonth();
|
||||
|
||||
const chartData = listOfDays.reduce((acc, val) => {
|
||||
//Sum up the current day.
|
||||
let dailySales;
|
||||
if (!!jobsByDate[val]) {
|
||||
dailySales = jobsByDate[val].reduce((dayAcc, dayVal) => {
|
||||
return dayAcc.add(
|
||||
Dinero((dayVal.job_totals && dayVal.job_totals.totals.subtotal) || 0)
|
||||
);
|
||||
}, Dinero());
|
||||
} else {
|
||||
dailySales = Dinero();
|
||||
}
|
||||
const chartData = listOfDays.reduce((acc, val) => {
|
||||
//Sum up the current day.
|
||||
let dailySales;
|
||||
if (!!jobsByDate[val]) {
|
||||
dailySales = jobsByDate[val].reduce((dayAcc, dayVal) => {
|
||||
return dayAcc.add(
|
||||
Dinero((dayVal.job_totals && dayVal.job_totals.totals.subtotal) || 0)
|
||||
);
|
||||
}, Dinero());
|
||||
} else {
|
||||
dailySales = Dinero();
|
||||
}
|
||||
|
||||
const theValue = {
|
||||
date: dayjs(val).format("DD"),
|
||||
dailySales: dailySales.getAmount() / 100,
|
||||
accSales:
|
||||
acc.length > 0
|
||||
? acc[acc.length - 1].accSales + dailySales.getAmount() / 100
|
||||
: dailySales.getAmount() / 100,
|
||||
};
|
||||
const theValue = {
|
||||
date: dayjs(val).format("DD"),
|
||||
dailySales: dailySales.getAmount() / 100,
|
||||
accSales:
|
||||
acc.length > 0
|
||||
? acc[acc.length - 1].accSales + dailySales.getAmount() / 100
|
||||
: dailySales.getAmount() / 100,
|
||||
};
|
||||
|
||||
return [...acc, theValue];
|
||||
}, []);
|
||||
return [...acc, theValue];
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Card title={t("dashboard.titles.monthlyrevenuegraph")} {...cardProps}>
|
||||
<div style={{ height: "100%" }}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<ComposedChart
|
||||
data={chartData}
|
||||
margin={{ top: 20, right: 20, bottom: 20, left: 20 }}
|
||||
>
|
||||
<CartesianGrid stroke="#f5f5f5" />
|
||||
<XAxis dataKey="date" />
|
||||
<YAxis />
|
||||
<Tooltip
|
||||
formatter={(value, name, props) => value && value.toFixed(2)}
|
||||
/>
|
||||
<Legend />
|
||||
<Area
|
||||
type="monotone"
|
||||
name="Accumulated Sales"
|
||||
dataKey="accSales"
|
||||
fill="#3CB371"
|
||||
stroke="#3CB371"
|
||||
/>
|
||||
<Bar
|
||||
name="Daily Sales"
|
||||
dataKey="dailySales"
|
||||
//stackId="day"
|
||||
barSize={20}
|
||||
fill="#413ea0"
|
||||
/>
|
||||
</ComposedChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
return (
|
||||
<Card title={t("dashboard.titles.monthlyrevenuegraph")} {...cardProps}>
|
||||
<div style={{height: "100%"}}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<ComposedChart
|
||||
data={chartData}
|
||||
margin={{top: 20, right: 20, bottom: 20, left: 20}}
|
||||
>
|
||||
<CartesianGrid stroke="#f5f5f5"/>
|
||||
<XAxis dataKey="date"/>
|
||||
<YAxis/>
|
||||
<Tooltip
|
||||
formatter={(value, name, props) => value && value.toFixed(2)}
|
||||
/>
|
||||
<Legend/>
|
||||
<Area
|
||||
type="monotone"
|
||||
name="Accumulated Sales"
|
||||
dataKey="accSales"
|
||||
fill="#3CB371"
|
||||
stroke="#3CB371"
|
||||
/>
|
||||
<Bar
|
||||
name="Daily Sales"
|
||||
dataKey="dailySales"
|
||||
//stackId="day"
|
||||
barSize={20}
|
||||
fill="#413ea0"
|
||||
/>
|
||||
</ComposedChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export const DashboardMonthlyRevenueGraphGql = `
|
||||
|
||||
@@ -1,33 +1,34 @@
|
||||
import { Card, Statistic } from "antd";
|
||||
import {Card, Statistic} from "antd";
|
||||
import Dinero from "dinero.js";
|
||||
import dayjs from "../../../utils/day";
|
||||
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 }) {
|
||||
const { t } = useTranslation();
|
||||
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 &&
|
||||
val.job_totals.totals &&
|
||||
val.job_totals.totals.subtotal
|
||||
)
|
||||
),
|
||||
Dinero()
|
||||
export default function DashboardProjectedMonthlySales({data, ...cardProps}) {
|
||||
const {t} = useTranslation();
|
||||
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 &&
|
||||
val.job_totals.totals &&
|
||||
val.job_totals.totals.subtotal
|
||||
)
|
||||
),
|
||||
Dinero()
|
||||
);
|
||||
return (
|
||||
<Card title={t("dashboard.titles.projectedmonthlysales")} {...cardProps}>
|
||||
<Statistic value={dollars.toFormat()}/>
|
||||
</Card>
|
||||
);
|
||||
return (
|
||||
<Card title={t("dashboard.titles.projectedmonthlysales")} {...cardProps}>
|
||||
<Statistic value={dollars.toFormat()} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export const DashboardProjectedMonthlySalesGql = `
|
||||
@@ -37,12 +38,12 @@ export const DashboardProjectedMonthlySalesGql = `
|
||||
{_and: [
|
||||
{date_invoiced:{_is_null: false }},
|
||||
{date_invoiced: {_gte: "${dayjs()
|
||||
.startOf("month")
|
||||
.startOf("day")
|
||||
.toISOString()}"}}, {date_invoiced: {_lte: "${dayjs()
|
||||
.endOf("month")
|
||||
.endOf("day")
|
||||
.toISOString()}"}}]},
|
||||
.startOf("month")
|
||||
.startOf("day")
|
||||
.toISOString()}"}}, {date_invoiced: {_lte: "${dayjs()
|
||||
.endOf("month")
|
||||
.endOf("day")
|
||||
.toISOString()}"}}]},
|
||||
{
|
||||
|
||||
_and:[
|
||||
@@ -51,9 +52,9 @@ _and:[
|
||||
.startOf("month")
|
||||
.startOf("day")
|
||||
.toISOString()}"}}, {actual_completion: {_lte: "${dayjs()
|
||||
.endOf("month")
|
||||
.endOf("day")
|
||||
.toISOString()}"}}
|
||||
.endOf("month")
|
||||
.endOf("day")
|
||||
.toISOString()}"}}
|
||||
|
||||
]
|
||||
},
|
||||
@@ -62,12 +63,12 @@ _and:[
|
||||
{date_invoiced: {_is_null: true}},
|
||||
{actual_completion: {_is_null: true}}
|
||||
{scheduled_completion: {_gte: "${dayjs()
|
||||
.startOf("month")
|
||||
.startOf("day")
|
||||
.toISOString()}"}}, {scheduled_completion: {_lte: "${dayjs()
|
||||
.endOf("month")
|
||||
.endOf("day")
|
||||
.toISOString()}"}}
|
||||
.startOf("month")
|
||||
.startOf("day")
|
||||
.toISOString()}"}}, {scheduled_completion: {_lte: "${dayjs()
|
||||
.endOf("month")
|
||||
.endOf("day")
|
||||
.toISOString()}"}}
|
||||
|
||||
|
||||
]}
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
import { SyncOutlined } from "@ant-design/icons";
|
||||
import { Card } from "antd";
|
||||
import {SyncOutlined} from "@ant-design/icons";
|
||||
import {Card} from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
export default function DashboardRefreshRequired(props) {
|
||||
const { t } = useTranslation();
|
||||
const {t} = useTranslation();
|
||||
|
||||
return (
|
||||
<Card {...props}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
textOverflow: "ellipsis",
|
||||
}}
|
||||
>
|
||||
<SyncOutlined style={{ fontSize: "300%", margin: "1rem" }} />
|
||||
<div>{t("dashboard.errors.refreshrequired")}</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
return (
|
||||
<Card {...props}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
textOverflow: "ellipsis",
|
||||
}}
|
||||
>
|
||||
<SyncOutlined style={{fontSize: "300%", margin: "1rem"}}/>
|
||||
<div>{t("dashboard.errors.refreshrequired")}</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,219 +1,215 @@
|
||||
import {
|
||||
BranchesOutlined,
|
||||
ExclamationCircleFilled,
|
||||
PauseCircleOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { Card, Space, Table, Tooltip } from "antd";
|
||||
import {BranchesOutlined, ExclamationCircleFilled, PauseCircleOutlined,} from "@ant-design/icons";
|
||||
import {Card, Space, Table, Tooltip} from "antd";
|
||||
import dayjs from "../../../utils/day";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import React, {useState} from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {Link} from "react-router-dom";
|
||||
import ChatOpenButton from "../../chat-open-button/chat-open-button.component";
|
||||
import OwnerNameDisplay from "../../owner-name-display/owner-name-display.component";
|
||||
import DashboardRefreshRequired from "../refresh-required.component";
|
||||
import {pageLimit} from "../../../utils/config";
|
||||
|
||||
export default function DashboardScheduledInToday({ data, ...cardProps }) {
|
||||
const { t } = useTranslation();
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
});
|
||||
if (!data) return null;
|
||||
if (!data.scheduled_in_today)
|
||||
return <DashboardRefreshRequired {...cardProps} />;
|
||||
export default function DashboardScheduledInToday({data, ...cardProps}) {
|
||||
const {t} = useTranslation();
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
});
|
||||
if (!data) return null;
|
||||
if (!data.scheduled_in_today)
|
||||
return <DashboardRefreshRequired {...cardProps} />;
|
||||
|
||||
const appt = []; // Flatten Data
|
||||
data.scheduled_in_today.forEach((item) => {
|
||||
if (item.job) {
|
||||
var i = {
|
||||
canceled: item.canceled,
|
||||
id: item.id,
|
||||
alt_transport: item.job.alt_transport,
|
||||
clm_no: item.job.clm_no,
|
||||
jobid: item.job.jobid,
|
||||
ins_co_nm: item.job.ins_co_nm,
|
||||
iouparent: item.job.iouparent,
|
||||
ownerid: item.job.ownerid,
|
||||
ownr_co_nm: item.job.ownr_co_nm,
|
||||
ownr_ea: item.job.ownr_ea,
|
||||
ownr_fn: item.job.ownr_fn,
|
||||
ownr_ln: item.job.ownr_ln,
|
||||
ownr_ph1: item.job.ownr_ph1,
|
||||
ownr_ph2: item.job.ownr_ph2,
|
||||
production_vars: item.job.production_vars,
|
||||
ro_number: item.job.ro_number,
|
||||
suspended: item.job.suspended,
|
||||
v_make_desc: item.job.v_make_desc,
|
||||
v_model_desc: item.job.v_model_desc,
|
||||
v_model_yr: item.job.v_model_yr,
|
||||
v_vin: item.job.v_vin,
|
||||
vehicleid: item.job.vehicleid,
|
||||
note: item.note,
|
||||
start: dayjs(item.start).format("hh:mm a"),
|
||||
title: item.title,
|
||||
};
|
||||
appt.push(i);
|
||||
}
|
||||
});
|
||||
appt.sort(function (a, b) {
|
||||
return new dayjs(a.start) - new dayjs(b.start);
|
||||
});
|
||||
const appt = []; // Flatten Data
|
||||
data.scheduled_in_today.forEach((item) => {
|
||||
if (item.job) {
|
||||
var i = {
|
||||
canceled: item.canceled,
|
||||
id: item.id,
|
||||
alt_transport: item.job.alt_transport,
|
||||
clm_no: item.job.clm_no,
|
||||
jobid: item.job.jobid,
|
||||
ins_co_nm: item.job.ins_co_nm,
|
||||
iouparent: item.job.iouparent,
|
||||
ownerid: item.job.ownerid,
|
||||
ownr_co_nm: item.job.ownr_co_nm,
|
||||
ownr_ea: item.job.ownr_ea,
|
||||
ownr_fn: item.job.ownr_fn,
|
||||
ownr_ln: item.job.ownr_ln,
|
||||
ownr_ph1: item.job.ownr_ph1,
|
||||
ownr_ph2: item.job.ownr_ph2,
|
||||
production_vars: item.job.production_vars,
|
||||
ro_number: item.job.ro_number,
|
||||
suspended: item.job.suspended,
|
||||
v_make_desc: item.job.v_make_desc,
|
||||
v_model_desc: item.job.v_model_desc,
|
||||
v_model_yr: item.job.v_model_yr,
|
||||
v_vin: item.job.v_vin,
|
||||
vehicleid: item.job.vehicleid,
|
||||
note: item.note,
|
||||
start: dayjs(item.start).format("hh:mm a"),
|
||||
title: item.title,
|
||||
};
|
||||
appt.push(i);
|
||||
}
|
||||
});
|
||||
appt.sort(function (a, b) {
|
||||
return new dayjs(a.start) - new dayjs(b.start);
|
||||
});
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t("jobs.fields.ro_number"),
|
||||
dataIndex: "ro_number",
|
||||
key: "ro_number",
|
||||
render: (text, record) => (
|
||||
<Link
|
||||
to={"/manage/jobs/" + record.jobid}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Space>
|
||||
{record.ro_number || t("general.labels.na")}
|
||||
{record.production_vars && record.production_vars.alert ? (
|
||||
<ExclamationCircleFilled className="production-alert" />
|
||||
) : null}
|
||||
{record.suspended && (
|
||||
<PauseCircleOutlined style={{ color: "orangered" }} />
|
||||
)}
|
||||
{record.iouparent && (
|
||||
<Tooltip title={t("jobs.labels.iou")}>
|
||||
<BranchesOutlined style={{ color: "orangered" }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</Space>
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.owner"),
|
||||
dataIndex: "owner",
|
||||
key: "owner",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
render: (text, record) => {
|
||||
return record.ownerid ? (
|
||||
<Link
|
||||
to={"/manage/owners/" + record.ownerid}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<OwnerNameDisplay ownerObject={record} />
|
||||
</Link>
|
||||
) : (
|
||||
<span>
|
||||
<OwnerNameDisplay ownerObject={record} />
|
||||
const columns = [
|
||||
{
|
||||
title: t("jobs.fields.ro_number"),
|
||||
dataIndex: "ro_number",
|
||||
key: "ro_number",
|
||||
render: (text, record) => (
|
||||
<Link
|
||||
to={"/manage/jobs/" + record.jobid}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Space>
|
||||
{record.ro_number || t("general.labels.na")}
|
||||
{record.production_vars && record.production_vars.alert ? (
|
||||
<ExclamationCircleFilled className="production-alert"/>
|
||||
) : null}
|
||||
{record.suspended && (
|
||||
<PauseCircleOutlined style={{color: "orangered"}}/>
|
||||
)}
|
||||
{record.iouparent && (
|
||||
<Tooltip title={t("jobs.labels.iou")}>
|
||||
<BranchesOutlined style={{color: "orangered"}}/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Space>
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.owner"),
|
||||
dataIndex: "owner",
|
||||
key: "owner",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
render: (text, record) => {
|
||||
return record.ownerid ? (
|
||||
<Link
|
||||
to={"/manage/owners/" + record.ownerid}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<OwnerNameDisplay ownerObject={record}/>
|
||||
</Link>
|
||||
) : (
|
||||
<span>
|
||||
<OwnerNameDisplay ownerObject={record}/>
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ownr_ph1"),
|
||||
dataIndex: "ownr_ph1",
|
||||
key: "ownr_ph1",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
render: (text, record) => (
|
||||
<ChatOpenButton phone={record.ownr_ph1} jobid={record.jobid} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ownr_ph2"),
|
||||
dataIndex: "ownr_ph2",
|
||||
key: "ownr_ph2",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
render: (text, record) => (
|
||||
<ChatOpenButton phone={record.ownr_ph2} jobid={record.jobid} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ownr_ea"),
|
||||
dataIndex: "ownr_ea",
|
||||
key: "ownr_ea",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
render: (text, record) => (
|
||||
<ChatOpenButton phone={record.ownr_ea} jobid={record.jobid} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.vehicle"),
|
||||
dataIndex: "vehicle",
|
||||
key: "vehicle",
|
||||
ellipsis: true,
|
||||
render: (text, record) => {
|
||||
return record.vehicleid ? (
|
||||
<Link
|
||||
to={"/manage/vehicles/" + record.vehicleid}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
|
||||
record.v_model_desc || ""
|
||||
}`}
|
||||
</Link>
|
||||
) : (
|
||||
<span>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
|
||||
record.v_model_desc || ""
|
||||
}`}</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ins_co_nm"),
|
||||
dataIndex: "ins_co_nm",
|
||||
key: "ins_co_nm",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
},
|
||||
{
|
||||
title: t("appointments.fields.time"),
|
||||
dataIndex: "start",
|
||||
key: "start",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
},
|
||||
{
|
||||
title: t("appointments.fields.alt_transport"),
|
||||
dataIndex: "alt_transport",
|
||||
key: "alt_transport",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
},
|
||||
];
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ownr_ph1"),
|
||||
dataIndex: "ownr_ph1",
|
||||
key: "ownr_ph1",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
render: (text, record) => (
|
||||
<ChatOpenButton phone={record.ownr_ph1} jobid={record.jobid}/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ownr_ph2"),
|
||||
dataIndex: "ownr_ph2",
|
||||
key: "ownr_ph2",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
render: (text, record) => (
|
||||
<ChatOpenButton phone={record.ownr_ph2} jobid={record.jobid}/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ownr_ea"),
|
||||
dataIndex: "ownr_ea",
|
||||
key: "ownr_ea",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
render: (text, record) => (
|
||||
<ChatOpenButton phone={record.ownr_ea} jobid={record.jobid}/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.vehicle"),
|
||||
dataIndex: "vehicle",
|
||||
key: "vehicle",
|
||||
ellipsis: true,
|
||||
render: (text, record) => {
|
||||
return record.vehicleid ? (
|
||||
<Link
|
||||
to={"/manage/vehicles/" + record.vehicleid}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
|
||||
record.v_model_desc || ""
|
||||
}`}
|
||||
</Link>
|
||||
) : (
|
||||
<span>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
|
||||
record.v_model_desc || ""
|
||||
}`}</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ins_co_nm"),
|
||||
dataIndex: "ins_co_nm",
|
||||
key: "ins_co_nm",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
},
|
||||
{
|
||||
title: t("appointments.fields.time"),
|
||||
dataIndex: "start",
|
||||
key: "start",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
},
|
||||
{
|
||||
title: t("appointments.fields.alt_transport"),
|
||||
dataIndex: "alt_transport",
|
||||
key: "alt_transport",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
},
|
||||
];
|
||||
|
||||
const handleTableChange = (sorter) => {
|
||||
setState({ ...state, sortedInfo: sorter });
|
||||
};
|
||||
const handleTableChange = (sorter) => {
|
||||
setState({...state, sortedInfo: sorter});
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={t("dashboard.titles.scheduledintoday", {
|
||||
date: dayjs().startOf("day").format("MM/DD/YYYY"),
|
||||
})}
|
||||
{...cardProps}
|
||||
>
|
||||
<div style={{ height: "100%" }}>
|
||||
<Table
|
||||
onChange={handleTableChange}
|
||||
pagination={{ position: "top", defaultPageSize: pageLimit }}
|
||||
columns={columns}
|
||||
scroll={{ x: true, y: "calc(100% - 2em)" }}
|
||||
rowKey="id"
|
||||
style={{ height: "85%" }}
|
||||
dataSource={appt}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
return (
|
||||
<Card
|
||||
title={t("dashboard.titles.scheduledintoday", {
|
||||
date: dayjs().startOf("day").format("MM/DD/YYYY"),
|
||||
})}
|
||||
{...cardProps}
|
||||
>
|
||||
<div style={{height: "100%"}}>
|
||||
<Table
|
||||
onChange={handleTableChange}
|
||||
pagination={{position: "top", defaultPageSize: pageLimit}}
|
||||
columns={columns}
|
||||
scroll={{x: true, y: "calc(100% - 2em)"}}
|
||||
rowKey="id"
|
||||
style={{height: "85%"}}
|
||||
dataSource={appt}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export const DashboardScheduledInTodayGql = `
|
||||
scheduled_in_today: appointments(where: {start: {_gte: "${dayjs()
|
||||
.startOf("day")
|
||||
.toISOString()}", _lte: "${dayjs()
|
||||
.endOf("day")
|
||||
.toISOString()}"}, canceled: {_eq: false}, block: {_neq: true}}) {
|
||||
.endOf("day")
|
||||
.toISOString()}"}, canceled: {_eq: false}, block: {_neq: true}}) {
|
||||
canceled
|
||||
id
|
||||
job {
|
||||
|
||||
@@ -1,182 +1,178 @@
|
||||
import {
|
||||
BranchesOutlined,
|
||||
ExclamationCircleFilled,
|
||||
PauseCircleOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { Card, Space, Table, Tooltip } from "antd";
|
||||
import {BranchesOutlined, ExclamationCircleFilled, PauseCircleOutlined,} from "@ant-design/icons";
|
||||
import {Card, Space, Table, Tooltip} from "antd";
|
||||
import dayjs from "../../../utils/day";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import React, {useState} from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {Link} from "react-router-dom";
|
||||
import ChatOpenButton from "../../chat-open-button/chat-open-button.component";
|
||||
import OwnerNameDisplay from "../../owner-name-display/owner-name-display.component";
|
||||
import DashboardRefreshRequired from "../refresh-required.component";
|
||||
import {pageLimit} from "../../../utils/config";
|
||||
|
||||
export default function DashboardScheduledOutToday({ data, ...cardProps }) {
|
||||
const { t } = useTranslation();
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
});
|
||||
if (!data) return null;
|
||||
if (!data.scheduled_out_today)
|
||||
return <DashboardRefreshRequired {...cardProps} />;
|
||||
export default function DashboardScheduledOutToday({data, ...cardProps}) {
|
||||
const {t} = useTranslation();
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
});
|
||||
if (!data) return null;
|
||||
if (!data.scheduled_out_today)
|
||||
return <DashboardRefreshRequired {...cardProps} />;
|
||||
|
||||
const filteredScheduledOutToday = data.scheduled_out_today.map((item) => {
|
||||
return {
|
||||
...item,
|
||||
scheduled_completion: dayjs(item.scheduled_completion).format("hh:mm a"),
|
||||
timestamp: dayjs(item.scheduled_completion).valueOf(),
|
||||
}
|
||||
}).sort((a, b) => a.timestamp - b.timestamp);
|
||||
const filteredScheduledOutToday = data.scheduled_out_today.map((item) => {
|
||||
return {
|
||||
...item,
|
||||
scheduled_completion: dayjs(item.scheduled_completion).format("hh:mm a"),
|
||||
timestamp: dayjs(item.scheduled_completion).valueOf(),
|
||||
}
|
||||
}).sort((a, b) => a.timestamp - b.timestamp);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t("jobs.fields.ro_number"),
|
||||
dataIndex: "ro_number",
|
||||
key: "ro_number",
|
||||
render: (text, record) => (
|
||||
<Link
|
||||
to={"/manage/jobs/" + record.jobid}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Space>
|
||||
{record.ro_number || t("general.labels.na")}
|
||||
{record.production_vars && record.production_vars.alert ? (
|
||||
<ExclamationCircleFilled className="production-alert" />
|
||||
) : null}
|
||||
{record.suspended && (
|
||||
<PauseCircleOutlined style={{ color: "orangered" }} />
|
||||
)}
|
||||
{record.iouparent && (
|
||||
<Tooltip title={t("jobs.labels.iou")}>
|
||||
<BranchesOutlined style={{ color: "orangered" }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</Space>
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.owner"),
|
||||
dataIndex: "owner",
|
||||
key: "owner",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
render: (text, record) => {
|
||||
return record.ownerid ? (
|
||||
<Link
|
||||
to={"/manage/owners/" + record.ownerid}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<OwnerNameDisplay ownerObject={record} />
|
||||
</Link>
|
||||
) : (
|
||||
<span>
|
||||
<OwnerNameDisplay ownerObject={record} />
|
||||
const columns = [
|
||||
{
|
||||
title: t("jobs.fields.ro_number"),
|
||||
dataIndex: "ro_number",
|
||||
key: "ro_number",
|
||||
render: (text, record) => (
|
||||
<Link
|
||||
to={"/manage/jobs/" + record.jobid}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Space>
|
||||
{record.ro_number || t("general.labels.na")}
|
||||
{record.production_vars && record.production_vars.alert ? (
|
||||
<ExclamationCircleFilled className="production-alert"/>
|
||||
) : null}
|
||||
{record.suspended && (
|
||||
<PauseCircleOutlined style={{color: "orangered"}}/>
|
||||
)}
|
||||
{record.iouparent && (
|
||||
<Tooltip title={t("jobs.labels.iou")}>
|
||||
<BranchesOutlined style={{color: "orangered"}}/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Space>
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.owner"),
|
||||
dataIndex: "owner",
|
||||
key: "owner",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
render: (text, record) => {
|
||||
return record.ownerid ? (
|
||||
<Link
|
||||
to={"/manage/owners/" + record.ownerid}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<OwnerNameDisplay ownerObject={record}/>
|
||||
</Link>
|
||||
) : (
|
||||
<span>
|
||||
<OwnerNameDisplay ownerObject={record}/>
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ownr_ph1"),
|
||||
dataIndex: "ownr_ph1",
|
||||
key: "ownr_ph1",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
render: (text, record) => (
|
||||
<ChatOpenButton phone={record.ownr_ph1} jobid={record.jobid} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ownr_ph2"),
|
||||
dataIndex: "ownr_ph2",
|
||||
key: "ownr_ph2",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
render: (text, record) => (
|
||||
<ChatOpenButton phone={record.ownr_ph2} jobid={record.jobid} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ownr_ea"),
|
||||
dataIndex: "ownr_ea",
|
||||
key: "ownr_ea",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
render: (text, record) => (
|
||||
<ChatOpenButton phone={record.ownr_ea} jobid={record.jobid} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.vehicle"),
|
||||
dataIndex: "vehicle",
|
||||
key: "vehicle",
|
||||
ellipsis: true,
|
||||
render: (text, record) => {
|
||||
return record.vehicleid ? (
|
||||
<Link
|
||||
to={"/manage/vehicles/" + record.vehicleid}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
|
||||
record.v_model_desc || ""
|
||||
}`}
|
||||
</Link>
|
||||
) : (
|
||||
<span>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
|
||||
record.v_model_desc || ""
|
||||
}`}</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ins_co_nm"),
|
||||
dataIndex: "ins_co_nm",
|
||||
key: "ins_co_nm",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.scheduled_completion"),
|
||||
dataIndex: "scheduled_completion",
|
||||
key: "scheduled_completion",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
},
|
||||
{
|
||||
title: t("appointments.fields.alt_transport"),
|
||||
dataIndex: "alt_transport",
|
||||
key: "alt_transport",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
},
|
||||
];
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ownr_ph1"),
|
||||
dataIndex: "ownr_ph1",
|
||||
key: "ownr_ph1",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
render: (text, record) => (
|
||||
<ChatOpenButton phone={record.ownr_ph1} jobid={record.jobid}/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ownr_ph2"),
|
||||
dataIndex: "ownr_ph2",
|
||||
key: "ownr_ph2",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
render: (text, record) => (
|
||||
<ChatOpenButton phone={record.ownr_ph2} jobid={record.jobid}/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ownr_ea"),
|
||||
dataIndex: "ownr_ea",
|
||||
key: "ownr_ea",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
render: (text, record) => (
|
||||
<ChatOpenButton phone={record.ownr_ea} jobid={record.jobid}/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.vehicle"),
|
||||
dataIndex: "vehicle",
|
||||
key: "vehicle",
|
||||
ellipsis: true,
|
||||
render: (text, record) => {
|
||||
return record.vehicleid ? (
|
||||
<Link
|
||||
to={"/manage/vehicles/" + record.vehicleid}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
|
||||
record.v_model_desc || ""
|
||||
}`}
|
||||
</Link>
|
||||
) : (
|
||||
<span>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
|
||||
record.v_model_desc || ""
|
||||
}`}</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ins_co_nm"),
|
||||
dataIndex: "ins_co_nm",
|
||||
key: "ins_co_nm",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.scheduled_completion"),
|
||||
dataIndex: "scheduled_completion",
|
||||
key: "scheduled_completion",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
},
|
||||
{
|
||||
title: t("appointments.fields.alt_transport"),
|
||||
dataIndex: "alt_transport",
|
||||
key: "alt_transport",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
},
|
||||
];
|
||||
|
||||
const handleTableChange = (sorter) => {
|
||||
setState({ ...state, sortedInfo: sorter });
|
||||
};
|
||||
const handleTableChange = (sorter) => {
|
||||
setState({...state, sortedInfo: sorter});
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={t("dashboard.titles.scheduledouttoday", {
|
||||
date: dayjs().startOf("day").format("MM/DD/YYYY"),
|
||||
})}
|
||||
{...cardProps}
|
||||
>
|
||||
<div style={{ height: "100%" }}>
|
||||
<Table
|
||||
onChange={handleTableChange}
|
||||
pagination={{ position: "top", defaultPageSize: pageLimit }}
|
||||
columns={columns}
|
||||
scroll={{ x: true, y: "calc(100% - 2em)" }}
|
||||
rowKey="id"
|
||||
style={{ height: "85%" }}
|
||||
dataSource={filteredScheduledOutToday}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
return (
|
||||
<Card
|
||||
title={t("dashboard.titles.scheduledouttoday", {
|
||||
date: dayjs().startOf("day").format("MM/DD/YYYY"),
|
||||
})}
|
||||
{...cardProps}
|
||||
>
|
||||
<div style={{height: "100%"}}>
|
||||
<Table
|
||||
onChange={handleTableChange}
|
||||
pagination={{position: "top", defaultPageSize: pageLimit}}
|
||||
columns={columns}
|
||||
scroll={{x: true, y: "calc(100% - 2em)"}}
|
||||
rowKey="id"
|
||||
style={{height: "85%"}}
|
||||
dataSource={filteredScheduledOutToday}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export const DashboardScheduledOutTodayGql = `
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
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 DashboardRefreshRequired from "../refresh-required.component";
|
||||
|
||||
export default function DashboardTotalProductionDollars({
|
||||
data,
|
||||
...cardProps
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
if (!data) return null;
|
||||
if (!data.production_jobs) return <DashboardRefreshRequired {...cardProps} />;
|
||||
const dollars =
|
||||
data.production_jobs &&
|
||||
data.production_jobs.reduce(
|
||||
(acc, val) =>
|
||||
acc.add(Dinero(val.job_totals && val.job_totals.totals.subtotal)),
|
||||
Dinero()
|
||||
);
|
||||
data,
|
||||
...cardProps
|
||||
}) {
|
||||
const {t} = useTranslation();
|
||||
if (!data) return null;
|
||||
if (!data.production_jobs) return <DashboardRefreshRequired {...cardProps} />;
|
||||
const dollars =
|
||||
data.production_jobs &&
|
||||
data.production_jobs.reduce(
|
||||
(acc, val) =>
|
||||
acc.add(Dinero(val.job_totals && val.job_totals.totals.subtotal)),
|
||||
Dinero()
|
||||
);
|
||||
|
||||
return (
|
||||
<Card title={t("dashboard.labels.dollarsinproduction")} {...cardProps}>
|
||||
<Statistic value={dollars.toFormat()} />
|
||||
</Card>
|
||||
);
|
||||
return (
|
||||
<Card title={t("dashboard.labels.dollarsinproduction")} {...cardProps}>
|
||||
<Statistic value={dollars.toFormat()}/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,63 +1,63 @@
|
||||
import { Card, Space, Statistic } from "antd";
|
||||
import {Card, Space, Statistic} from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../../redux/user/user.selectors";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {connect} from "react-redux";
|
||||
import {createStructuredSelector} from "reselect";
|
||||
import {selectBodyshop} from "../../../redux/user/user.selectors";
|
||||
import DashboardRefreshRequired from "../refresh-required.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({});
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(DashboardTotalProductionHours);
|
||||
|
||||
export function DashboardTotalProductionHours({
|
||||
bodyshop,
|
||||
data,
|
||||
...cardProps
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
if (!data) return null;
|
||||
if (!data.production_jobs) return <DashboardRefreshRequired {...cardProps} />;
|
||||
const hours =
|
||||
data.production_jobs &&
|
||||
data.production_jobs.reduce(
|
||||
(acc, val) => {
|
||||
return {
|
||||
body: acc.body + val.labhrs.aggregate.sum.mod_lb_hrs,
|
||||
ref: acc.ref + val.larhrs.aggregate.sum.mod_lb_hrs,
|
||||
total:
|
||||
acc.total +
|
||||
val.labhrs.aggregate.sum.mod_lb_hrs +
|
||||
val.larhrs.aggregate.sum.mod_lb_hrs,
|
||||
};
|
||||
},
|
||||
{ body: 0, ref: 0, total: 0 }
|
||||
bodyshop,
|
||||
data,
|
||||
...cardProps
|
||||
}) {
|
||||
const {t} = useTranslation();
|
||||
if (!data) return null;
|
||||
if (!data.production_jobs) return <DashboardRefreshRequired {...cardProps} />;
|
||||
const hours =
|
||||
data.production_jobs &&
|
||||
data.production_jobs.reduce(
|
||||
(acc, val) => {
|
||||
return {
|
||||
body: acc.body + val.labhrs.aggregate.sum.mod_lb_hrs,
|
||||
ref: acc.ref + val.larhrs.aggregate.sum.mod_lb_hrs,
|
||||
total:
|
||||
acc.total +
|
||||
val.labhrs.aggregate.sum.mod_lb_hrs +
|
||||
val.larhrs.aggregate.sum.mod_lb_hrs,
|
||||
};
|
||||
},
|
||||
{body: 0, ref: 0, total: 0}
|
||||
);
|
||||
const aboveTargetHours = hours.total >= bodyshop.prodtargethrs;
|
||||
return (
|
||||
<Card {...cardProps} title={t("dashboard.titles.prodhrssummary")}>
|
||||
<Space wrap style={{flex: 1}}>
|
||||
<Statistic
|
||||
title={t("dashboard.labels.bodyhrs")}
|
||||
value={hours.body.toFixed(1)}
|
||||
/>
|
||||
<Statistic
|
||||
title={t("dashboard.labels.refhrs")}
|
||||
value={hours.ref.toFixed(1)}
|
||||
/>
|
||||
<Statistic
|
||||
title={t("dashboard.labels.prodhrs")}
|
||||
value={hours.total.toFixed(1)}
|
||||
valueStyle={{color: aboveTargetHours ? "green" : "red"}}
|
||||
/>
|
||||
</Space>
|
||||
</Card>
|
||||
);
|
||||
const aboveTargetHours = hours.total >= bodyshop.prodtargethrs;
|
||||
return (
|
||||
<Card {...cardProps} title={t("dashboard.titles.prodhrssummary")}>
|
||||
<Space wrap style={{ flex: 1 }}>
|
||||
<Statistic
|
||||
title={t("dashboard.labels.bodyhrs")}
|
||||
value={hours.body.toFixed(1)}
|
||||
/>
|
||||
<Statistic
|
||||
title={t("dashboard.labels.refhrs")}
|
||||
value={hours.ref.toFixed(1)}
|
||||
/>
|
||||
<Statistic
|
||||
title={t("dashboard.labels.prodhrs")}
|
||||
value={hours.total.toFixed(1)}
|
||||
valueStyle={{ color: aboveTargetHours ? "green" : "red" }}
|
||||
/>
|
||||
</Space>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export const DashboardTotalProductionHoursGql = ``;
|
||||
|
||||
Reference in New Issue
Block a user