Added scoreboard initial design BOD-91

This commit is contained in:
Patrick Fic
2020-06-26 16:25:54 -07:00
parent 4516491c8c
commit 3df456e2dd
41 changed files with 1388 additions and 11 deletions

View File

@@ -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>
);
}

View File

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

View File

@@ -105,8 +105,6 @@ export function ProductionBoardKanbanComponent({ data, bodyshop }) {
}
};
console.log("ismMoving", isMoving);
return (
<div>
<IndefiniteLoading loading={isMoving} />

View File

@@ -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>
);
}

View File

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

View File

@@ -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>
);
}

View File

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

View File

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

View File

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

View File

@@ -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={[

View File

@@ -33,6 +33,7 @@ export const QUERY_BODYSHOP = gql`
appt_length
stripe_acct_id
ssbuckets
scoreboard_target
employees {
id
first_name
@@ -83,6 +84,8 @@ export const UPDATE_SHOP = gql`
invoice_tax_rates
appt_length
stripe_acct_id
ssbuckets
scoreboard_target
employees {
id
first_name

View File

@@ -0,0 +1,16 @@
import gql from "graphql-tag";
export const SUBSCRIPTION_SCOREBOARD = gql`
subscription SUBSCRIPTION_SCOREBOARD($start: date!, $end: date!) {
scoreboard(where: { _and: { date: { _gte: $start, _lte: $end } } }) {
id
painthrs
bodyhrs
date
job {
id
ro_number
}
}
}
`;

View File

@@ -113,6 +113,10 @@ const PaymentsAll = lazy(() =>
import("../payments-all/payments-all.container.page")
);
const Scoreboard = lazy(() =>
import("../scoreboard/scoreboard.page.container.jsx")
);
const { Content } = Layout;
const stripePromise = new Promise((resolve, reject) => {
@@ -305,6 +309,11 @@ export function Manage({ match, conflict }) {
path={`${match.path}/payments`}
component={PaymentsAll}
/>
<Route
exact
path={`${match.path}/scoreboard`}
component={Scoreboard}
/>
</Suspense>
)}
</ErrorBoundary>

View File

@@ -0,0 +1,10 @@
import React from "react";
import ScoreboardDisplay from "../../components/scoreboard-display/scoreboard-display.component";
export default function ProductionBoardComponent({ scoreboardSubscription }) {
return (
<div>
<ScoreboardDisplay scoreboardSubscription={scoreboardSubscription} />
</div>
);
}

View File

@@ -0,0 +1,47 @@
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { setBreadcrumbs } from "../../redux/application/application.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import ScoreboardPageComponent from "./scoreboard.page.component";
import { useSubscription } from "@apollo/react-hooks";
import { SUBSCRIPTION_SCOREBOARD } from "../../graphql/scoreboard.queries";
import moment from "moment";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
});
export function ScoreboardContainer({ setBreadcrumbs }) {
const { t } = useTranslation();
const scoreboardSubscription = useSubscription(SUBSCRIPTION_SCOREBOARD, {
variables: {
start: moment().startOf("month"),
end: moment().endOf("month"),
},
});
useEffect(() => {
document.title = t("titles.scoreboard");
setBreadcrumbs([
{
link: "/manage/scoreboard",
label: t("titles.bc.scoreboard"),
},
]);
}, [t, setBreadcrumbs]);
return (
<ScoreboardPageComponent scoreboardSubscription={scoreboardSubscription} />
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(ScoreboardContainer);

View File

@@ -16,7 +16,7 @@ const applicationReducer = (state = INITIAL_STATE, action) => {
case ApplicationActionTypes.ADD_RECENT_ITEM:
return {
...state,
recentItems: [action.payload, ...state.scheduleLoad.slice(0, 9)],
recentItems: [action.payload, ...state.recentItems.slice(0, 9)],
};
case ApplicationActionTypes.SET_BREAD_CRUMBS:
return {

View File

@@ -86,6 +86,8 @@
"appt_length": "Default Appointment Length",
"city": "City",
"country": "Country",
"dailybodytarget": "Scoreboard - Daily Body Target",
"dailypainttarget": "Scoreboard - Daily Paint Target",
"email": "General Shop Email",
"federal_tax_id": "Federal Tax ID (GST/HST)",
"insurance_vendor_id": "Insurance Vendor ID",
@@ -797,6 +799,7 @@
"productionboard": "Production Board",
"productionlist": "Production - List",
"schedule": "Schedule",
"scoreboard": "Scoreboard",
"shop": "My Shop",
"shop_config": "Configuration",
"shop_csi": "CSI",
@@ -1011,6 +1014,15 @@
"state": "Error reading page state. Please refresh."
}
},
"scoreboard": {
"labels": {
"asoftodaytarget": "As of Today",
"dailytarget": "Daily",
"monthlytarget": "Monthly",
"weeklytarget": "Weekly",
"workingdays": "Working Days / Month"
}
},
"templates": {
"errors": {
"updating": "Error updating template {{error}}."
@@ -1067,6 +1079,7 @@
"productionboard": "Production Board",
"productionlist": "Production - List",
"schedule": "Schedule",
"scoreboard": "Scoreboard",
"shop": "Manage my Shop ({{shopname}})",
"shop-csi": "CSI Responses",
"shop-templates": "Shop Templates",
@@ -1096,6 +1109,7 @@
"productionlist": "Production - List View | $t(titles.app)",
"profile": "My Profile | $t(titles.app)",
"schedule": "Schedule | $t(titles.app)",
"scoreboard": "Scoreboard | $t(titles.app)",
"shop": "My Shop | $t(titles.app)",
"shop-csi": "CSI Responses | $t(titles.app)",
"shop-templates": "Shop Templates | $t(titles.app)",

View File

@@ -86,6 +86,8 @@
"appt_length": "",
"city": "",
"country": "",
"dailybodytarget": "",
"dailypainttarget": "",
"email": "",
"federal_tax_id": "",
"insurance_vendor_id": "",
@@ -797,6 +799,7 @@
"productionboard": "",
"productionlist": "",
"schedule": "Programar",
"scoreboard": "",
"shop": "Mi tienda",
"shop_config": "Configuración",
"shop_csi": "",
@@ -1011,6 +1014,15 @@
"state": "Error al leer el estado de la página. Porfavor refresca."
}
},
"scoreboard": {
"labels": {
"asoftodaytarget": "",
"dailytarget": "",
"monthlytarget": "",
"weeklytarget": "",
"workingdays": ""
}
},
"templates": {
"errors": {
"updating": ""
@@ -1067,6 +1079,7 @@
"productionboard": "",
"productionlist": "",
"schedule": "",
"scoreboard": "",
"shop": "",
"shop-csi": "",
"shop-templates": "",
@@ -1096,6 +1109,7 @@
"productionlist": "",
"profile": "Mi perfil | $t(titles.app)",
"schedule": "Horario | $t(titles.app)",
"scoreboard": "",
"shop": "Mi tienda | $t(titles.app)",
"shop-csi": "",
"shop-templates": "",

View File

@@ -86,6 +86,8 @@
"appt_length": "",
"city": "",
"country": "",
"dailybodytarget": "",
"dailypainttarget": "",
"email": "",
"federal_tax_id": "",
"insurance_vendor_id": "",
@@ -797,6 +799,7 @@
"productionboard": "",
"productionlist": "",
"schedule": "Programme",
"scoreboard": "",
"shop": "Mon magasin",
"shop_config": "Configuration",
"shop_csi": "",
@@ -1011,6 +1014,15 @@
"state": "Erreur lors de la lecture de l'état de la page. Rafraichissez, s'il vous plait."
}
},
"scoreboard": {
"labels": {
"asoftodaytarget": "",
"dailytarget": "",
"monthlytarget": "",
"weeklytarget": "",
"workingdays": ""
}
},
"templates": {
"errors": {
"updating": ""
@@ -1067,6 +1079,7 @@
"productionboard": "",
"productionlist": "",
"schedule": "",
"scoreboard": "",
"shop": "",
"shop-csi": "",
"shop-templates": "",
@@ -1096,6 +1109,7 @@
"productionlist": "",
"profile": "Mon profil | $t(titles.app)",
"schedule": "Horaire | $t(titles.app)",
"scoreboard": "",
"shop": "Mon magasin | $t(titles.app)",
"shop-csi": "",
"shop-templates": "",