Added scoreboard initial design BOD-91
This commit is contained in:
@@ -5,11 +5,13 @@ import ChatConversationTitleTags from "../chat-conversation-title-tags/chat-conv
|
||||
export default function ChatConversationTitle({ conversation }) {
|
||||
return (
|
||||
<div style={{ display: "flex" }}>
|
||||
{conversation.phone_num}
|
||||
{conversation && conversation.phone_num}
|
||||
<ChatConversationTitleTags
|
||||
jobConversations={conversation.job_conversations || []}
|
||||
jobConversations={
|
||||
(conversation && conversation.job_conversations) || []
|
||||
}
|
||||
/>
|
||||
<ChatTagRoContainer conversation={conversation} />
|
||||
<ChatTagRoContainer conversation={conversation || []} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -128,6 +128,11 @@ function Header({
|
||||
{t("menus.header.productionboard")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="scoreboard">
|
||||
<Link to="/manage/scoreboard">
|
||||
{t("menus.header.scoreboard")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="activejobs">
|
||||
<Link to="/manage/jobs">{t("menus.header.activejobs")}</Link>
|
||||
</Menu.Item>
|
||||
|
||||
@@ -105,8 +105,6 @@ export function ProductionBoardKanbanComponent({ data, bodyshop }) {
|
||||
}
|
||||
};
|
||||
|
||||
console.log("ismMoving", isMoving);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<IndefiniteLoading loading={isMoving} />
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import React from "react";
|
||||
import {
|
||||
ComposedChart,
|
||||
Line,
|
||||
Area,
|
||||
Bar,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
Legend,
|
||||
ResponsiveContainer,
|
||||
} from "recharts";
|
||||
import * as Utils from "../scoreboard-targets-table/scoreboard-targets-table.util";
|
||||
import moment from "moment";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ScoreboardChart);
|
||||
|
||||
export function ScoreboardChart({ sbEntriesByDate, bodyshop }) {
|
||||
const listOfBusDays = Utils.ListOfDaysInCurrentMonth();
|
||||
|
||||
const data = listOfBusDays.reduce((acc, val) => {
|
||||
//Sum up the current day.
|
||||
let dayhrs;
|
||||
if (!!sbEntriesByDate[val]) {
|
||||
dayhrs = sbEntriesByDate[val].reduce(
|
||||
(dayAcc, dayVal) => {
|
||||
return {
|
||||
bodyhrs: dayAcc.bodyhrs + dayVal.bodyhrs,
|
||||
painthrs: dayAcc.painthrs + dayVal.painthrs,
|
||||
};
|
||||
},
|
||||
{ bodyhrs: 0, painthrs: 0 }
|
||||
);
|
||||
} else {
|
||||
dayhrs = {
|
||||
bodyhrs: 0,
|
||||
painthrs: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const theValue = {
|
||||
date: moment(val).format("D dd"),
|
||||
paintHrs: dayhrs.painthrs,
|
||||
bodyHrs: dayhrs.bodyhrs,
|
||||
accTargetHrs: Utils.AsOfDateTargetHours(
|
||||
bodyshop.scoreboard_target.dailyBodyTarget +
|
||||
bodyshop.scoreboard_target.dailyPaintTarget,
|
||||
val
|
||||
),
|
||||
accHrs:
|
||||
acc.length > 0
|
||||
? acc[acc.length - 1].accHrs + dayhrs.painthrs + dayhrs.bodyhrs
|
||||
: dayhrs.painthrs + dayhrs.bodyhrs,
|
||||
};
|
||||
|
||||
return [...acc, theValue];
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ResponsiveContainer width="100%" height={475}>
|
||||
<ComposedChart
|
||||
data={data}
|
||||
margin={{ top: 20, right: 20, bottom: 20, left: 20 }}
|
||||
>
|
||||
<CartesianGrid stroke="#f5f5f5" />
|
||||
<XAxis dataKey="date" />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Legend />
|
||||
<Area
|
||||
type="monotone"
|
||||
name="Accumulated Hours"
|
||||
dataKey="accHrs"
|
||||
fill="#8884d8"
|
||||
stroke="#8884d8"
|
||||
/>
|
||||
<Bar
|
||||
name="Body Hours"
|
||||
dataKey="bodyHrs"
|
||||
stackId="day"
|
||||
barSize={20}
|
||||
fill="#cecece"
|
||||
/>
|
||||
<Bar
|
||||
name="Paint Hours"
|
||||
dataKey="paintHrs"
|
||||
stackId="day"
|
||||
barSize={20}
|
||||
fill="#413ea0"
|
||||
/>
|
||||
<Line
|
||||
name="Target Hours"
|
||||
type="monotone"
|
||||
dataKey="accTargetHrs"
|
||||
stroke="#ff7300"
|
||||
/>
|
||||
</ComposedChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { Statistic, Card } from "antd";
|
||||
import moment from "moment";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
export function ScoreboardDayStats({ bodyshop, date, entries }) {
|
||||
const {
|
||||
lastNumberWorkingDays,
|
||||
dailyPaintTarget,
|
||||
dailyBodyTarget,
|
||||
} = bodyshop.scoreboard_target;
|
||||
|
||||
let totalHrs = 0;
|
||||
const paintHrs = entries.reduce((acc, value) => {
|
||||
totalHrs = +value.painthrs;
|
||||
return acc + value.painthrs;
|
||||
}, 0);
|
||||
|
||||
const bodyHrs = entries.reduce((acc, value) => {
|
||||
totalHrs = +value.bodyhrs;
|
||||
return acc + value.bodyhrs;
|
||||
}, 0);
|
||||
|
||||
return (
|
||||
<div className="imex-flex-row__margin">
|
||||
<Card title={moment(date).format("D - ddd")}>
|
||||
<Statistic
|
||||
valueStyle={{ color: dailyBodyTarget > bodyHrs ? "red" : "green" }}
|
||||
value={bodyHrs.toFixed(1)}
|
||||
/>
|
||||
<Statistic
|
||||
valueStyle={{ color: dailyPaintTarget > paintHrs ? "red" : "green" }}
|
||||
value={paintHrs.toFixed(1)}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ScoreboardDayStats);
|
||||
@@ -0,0 +1,31 @@
|
||||
import React from "react";
|
||||
|
||||
import ScoreboardTargetsTable from "../scoreboard-targets-table/scoreboard-targets-table.component";
|
||||
import ScoreboardLastDays from "../scoreboard-last-days/scoreboard-last-days.component";
|
||||
import ScoreboardChart from "../scoreboard-chart/scoreboard-chart.component";
|
||||
|
||||
export default function ScoreboardDisplayComponent({ scoreboardSubscription }) {
|
||||
const { loading, error, data } = scoreboardSubscription;
|
||||
|
||||
const scoreBoardlist = (data && data.scoreboard) || [];
|
||||
console.log("ScoreboardDisplayComponent -> scoreBoardlist", scoreBoardlist);
|
||||
|
||||
const sbEntriesByDate = {};
|
||||
|
||||
scoreBoardlist.forEach((i) => {
|
||||
const entryDate = i.date;
|
||||
if (!!!sbEntriesByDate[entryDate]) {
|
||||
sbEntriesByDate[entryDate] = [];
|
||||
}
|
||||
sbEntriesByDate[entryDate].push(i);
|
||||
});
|
||||
|
||||
console.log("ScoreboardDisplayComponent -> sbEntriesByDate", sbEntriesByDate);
|
||||
return (
|
||||
<div>
|
||||
<ScoreboardTargetsTable />
|
||||
<ScoreboardLastDays sbEntriesByDate={sbEntriesByDate} />
|
||||
<ScoreboardChart sbEntriesByDate={sbEntriesByDate} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import moment from "moment";
|
||||
import ScoreboardDayStat from "../scoreboard-day-stats/scoreboard-day-stats.component";
|
||||
import { Row, Col } from "antd";
|
||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
export function ScoreboardLastDays({ bodyshop, sbEntriesByDate }) {
|
||||
const { lastNumberWorkingDays } = bodyshop.scoreboard_target;
|
||||
|
||||
const ArrayOfDate = [];
|
||||
for (var i = lastNumberWorkingDays - 1; i >= 0; i--) {
|
||||
ArrayOfDate.push(
|
||||
moment().businessSubtract(i, "day").toISOString().substr(0, 10)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Row>
|
||||
{ArrayOfDate.map((a) => (
|
||||
<Col span={2} key={a}>
|
||||
{!!sbEntriesByDate ? (
|
||||
<ScoreboardDayStat date={a} entries={sbEntriesByDate[a] || []} />
|
||||
) : (
|
||||
<LoadingSkeleton />
|
||||
)}
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ScoreboardLastDays);
|
||||
@@ -0,0 +1,109 @@
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import * as Util from "./scoreboard-targets-table.util";
|
||||
import { Row, Col, Card, Statistic } from "antd";
|
||||
import { CalendarOutlined } from "@ant-design/icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
const rowGutter = [16, 16];
|
||||
const statSpans = { xs: 24, sm: 6 };
|
||||
|
||||
export function ScoreboardTargetsTable({ bodyshop }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Row gutter={rowGutter}>
|
||||
<Col xs={24} sm={{ offset: 0, span: 4 }} lg={{ offset: 5, span: 4 }}>
|
||||
<Statistic
|
||||
title={t("scoreboard.labels.workingdays")}
|
||||
value={Util.CalculateWorkingDaysThisMonth()}
|
||||
prefix={<CalendarOutlined />}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={{ offset: 0, span: 20 }} lg={{ offset: 0, span: 13 }}>
|
||||
<Row>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
title={t("scoreboard.labels.dailytarget")}
|
||||
value={bodyshop.scoreboard_target.dailyBodyTarget}
|
||||
prefix="B"
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
title={t("scoreboard.labels.weeklytarget")}
|
||||
value={Util.WeeklyTargetHrs(
|
||||
bodyshop.scoreboard_target.dailyBodyTarget,
|
||||
bodyshop
|
||||
)}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
title={t("scoreboard.labels.monthlytarget")}
|
||||
value={Util.MonthlyTargetHrs(
|
||||
bodyshop.scoreboard_target.dailyBodyTarget,
|
||||
bodyshop
|
||||
)}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
title={t("scoreboard.labels.asoftodaytarget")}
|
||||
value={Util.AsOfTodayTargetHrs(
|
||||
bodyshop.scoreboard_target.dailyBodyTarget,
|
||||
bodyshop
|
||||
)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
value={bodyshop.scoreboard_target.dailyPaintTarget}
|
||||
prefix="P"
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
value={Util.WeeklyTargetHrs(
|
||||
bodyshop.scoreboard_target.dailyPaintTarget,
|
||||
bodyshop
|
||||
)}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
value={Util.MonthlyTargetHrs(
|
||||
bodyshop.scoreboard_target.dailyPaintTarget,
|
||||
bodyshop
|
||||
)}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
value={Util.AsOfTodayTargetHrs(
|
||||
bodyshop.scoreboard_target.dailyPaintTarget,
|
||||
bodyshop
|
||||
)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ScoreboardTargetsTable);
|
||||
@@ -0,0 +1,50 @@
|
||||
import moment from "moment";
|
||||
import momentbd from "moment-business-days";
|
||||
|
||||
moment.updateLocale("ca", {
|
||||
workingWeekdays: [1, 2, 3, 4, 5],
|
||||
});
|
||||
|
||||
export const CalculateWorkingDaysThisMonth = () => {
|
||||
return moment().endOf("month").businessDaysIntoMonth();
|
||||
};
|
||||
|
||||
export const CalculateWorkingDaysAsOfToday = () => {
|
||||
return moment().businessDaysIntoMonth();
|
||||
};
|
||||
|
||||
export const WeeklyTargetHrs = (dailyTargetHrs, bodyshop) => {
|
||||
return dailyTargetHrs * 5;
|
||||
};
|
||||
|
||||
export const MonthlyTargetHrs = (dailyTargetHrs, bodyshop) => {
|
||||
return dailyTargetHrs * CalculateWorkingDaysThisMonth();
|
||||
};
|
||||
|
||||
export const AsOfTodayTargetHrs = (dailyTargetHrs, bodyshop) => {
|
||||
return dailyTargetHrs * CalculateWorkingDaysAsOfToday();
|
||||
};
|
||||
|
||||
export const AsOfDateTargetHours = (dailyTargetHours, date) => {
|
||||
return (
|
||||
dailyTargetHours * moment().startOf("month").businessDiff(moment(date))
|
||||
);
|
||||
};
|
||||
|
||||
export const ListOfBusinessDaysInCurrentMonth = () => {
|
||||
const momentListOfDays = moment().monthBusinessDays();
|
||||
|
||||
return momentListOfDays.map((i) => i.format("YYYY-MM-DD"));
|
||||
};
|
||||
|
||||
export const ListOfDaysInCurrentMonth = () => {
|
||||
const days = [];
|
||||
const dateStart = moment().startOf("month");
|
||||
const dateEnd = moment().endOf("month");
|
||||
while (dateEnd.diff(dateStart, "days") > 0) {
|
||||
days.push(dateStart.format("YYYY-MM-DD"));
|
||||
dateStart.add(1, "days");
|
||||
}
|
||||
days.push(dateEnd.format("YYYY-MM-DD"));
|
||||
return days;
|
||||
};
|
||||
@@ -96,6 +96,45 @@ export default function ShopInfoComponent({ form }) {
|
||||
<InputNumber min={15} precisio={0} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.dailypainttarget")}
|
||||
name={["scoreboard_target", "dailyPaintTarget"]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} precisio={0} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.dailybodytarget")}
|
||||
name={["scoreboard_target", "dailyBodyTarget"]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} precisio={0} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.lastnumberworkingdays")}
|
||||
name={["scoreboard_target", "lastNumberWorkingDays"]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} max={12} precisio={0} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t("bodyshop.labels.accountingtiers")}
|
||||
rules={[
|
||||
|
||||
Reference in New Issue
Block a user