feature/IO-1054-ScoreBoard-WorkingDays - Fix

This commit is contained in:
Dave Richer
2025-07-07 10:36:58 -04:00
parent 9c59fd4c00
commit 3ae41b7016
7 changed files with 240 additions and 94 deletions

View File

@@ -3,7 +3,6 @@ import axios from "axios";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { logImEXEvent } from "../../firebase/firebase.utils"; import { logImEXEvent } from "../../firebase/firebase.utils";
import cleanAxios from "../../utils/CleanAxios";
import formatBytes from "../../utils/formatbytes"; import formatBytes from "../../utils/formatbytes";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
@@ -12,7 +11,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop bodyshop: selectBodyshop
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = () => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
}); });
@@ -26,7 +25,7 @@ const mapDispatchToProps = (dispatch) => ({
export default connect(mapStateToProps, mapDispatchToProps)(JobsDocumentsImgproxyDownloadButton); export default connect(mapStateToProps, mapDispatchToProps)(JobsDocumentsImgproxyDownloadButton);
export function JobsDocumentsImgproxyDownloadButton({ bodyshop, galleryImages, identifier, jobId }) { export function JobsDocumentsImgproxyDownloadButton({ galleryImages, identifier, jobId }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [download, setDownload] = useState(null); const [download, setDownload] = useState(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);

View File

@@ -1,9 +1,9 @@
import Icon from "@ant-design/icons"; import Icon from "@ant-design/icons";
import { Card, Popover, Space } from "antd"; import { Card, Popover, Space } from "antd";
import _ from "lodash"; import { groupBy } from "lodash";
import dayjs from "../../utils/day"; import dayjs from "../../utils/day";
import React, { useMemo } from "react"; import { useMemo } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { MdFileDownload, MdFileUpload } from "react-icons/md"; import { MdFileDownload, MdFileUpload } from "react-icons/md";
import { connect } from "react-redux"; import { connect } from "react-redux";
@@ -26,21 +26,12 @@ const mapStateToProps = createStructuredSelector({
calculating: selectScheduleLoadCalculating calculating: selectScheduleLoadCalculating
}); });
const mapDispatchToProps = (dispatch) => ({}); const mapDispatchToProps = () => ({});
export function ScheduleCalendarHeaderComponent({ export function ScheduleCalendarHeaderComponent({ bodyshop, label, refetch, date, load, calculating, events }) {
bodyshop,
label,
refetch,
date,
load,
calculating,
events,
...otherProps
}) {
const ATSToday = useMemo(() => { const ATSToday = useMemo(() => {
if (!events) return []; if (!events) return [];
return _.groupBy( return groupBy(
events.filter((e) => !e.vacation && e.isintake && dayjs(date).isSame(dayjs(e.start), "day")), events.filter((e) => !e.vacation && e.isintake && dayjs(date).isSame(dayjs(e.start), "day")),
"job.alt_transport" "job.alt_transport"
); );
@@ -155,7 +146,11 @@ export function ScheduleCalendarHeaderComponent({
<Space size="small"> <Space size="small">
<Icon component={MdFileDownload} style={{ color: "green" }} /> <Icon component={MdFileDownload} style={{ color: "green" }} />
<BlurWrapper featureName="smartscheduling"> <BlurWrapper featureName="smartscheduling">
<span>{`${(loadData.allHoursInBody || 0) && loadData.allHoursInBody.toFixed(1)}/${(loadData.allHoursInRefinish || 0) && loadData.allHoursInRefinish.toFixed(1)}/${(loadData.allHoursIn || 0) && loadData.allHoursIn.toFixed(1)}`}</span> <span>
{`${(loadData.allHoursInBody || 0) && loadData.allHoursInBody.toFixed(1)}/${
(loadData.allHoursInRefinish || 0) && loadData.allHoursInRefinish.toFixed(1)
}/${(loadData.allHoursIn || 0) && loadData.allHoursIn.toFixed(1)}`}
</span>
</BlurWrapper> </BlurWrapper>
</Space> </Space>
</Popover> </Popover>

View File

@@ -1,8 +1,7 @@
import { Card } from "antd"; import { Card } from "antd";
import Dinero from "dinero.js"; import Dinero from "dinero.js";
import _ from "lodash"; import { round } from "lodash";
import dayjs from "../../utils/day"; import dayjs from "../../utils/day";
import React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { import {
Area, Area,
@@ -29,7 +28,7 @@ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop bodyshop: selectBodyshop
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = () => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
}); });
export default connect(mapStateToProps, mapDispatchToProps)(ScoreboardChart); export default connect(mapStateToProps, mapDispatchToProps)(ScoreboardChart);
@@ -40,7 +39,7 @@ export function ScoreboardChart({ sbEntriesByDate, bodyshop }) {
const data = listOfBusDays.reduce((acc, val) => { const data = listOfBusDays.reduce((acc, val) => {
//Sum up the current day. //Sum up the current day.
let dayhrs; let dayhrs;
if (!!sbEntriesByDate[val]) { if (sbEntriesByDate[val]) {
dayhrs = sbEntriesByDate[val].reduce( dayhrs = sbEntriesByDate[val].reduce(
(dayAcc, dayVal) => { (dayAcc, dayVal) => {
return { return {
@@ -61,9 +60,9 @@ export function ScoreboardChart({ sbEntriesByDate, bodyshop }) {
const theValue = { const theValue = {
date: dayjs(val).format("D ddd"), date: dayjs(val).format("D ddd"),
paintHrs: _.round(dayhrs.painthrs, 1), paintHrs: round(dayhrs.painthrs, 1),
bodyHrs: _.round(dayhrs.bodyhrs, 1), bodyHrs: round(dayhrs.bodyhrs, 1),
accTargetHrs: _.round( accTargetHrs: round(
Utils.AsOfDateTargetHours( Utils.AsOfDateTargetHours(
bodyshop.scoreboard_target.dailyBodyTarget + bodyshop.scoreboard_target.dailyPaintTarget, bodyshop.scoreboard_target.dailyBodyTarget + bodyshop.scoreboard_target.dailyPaintTarget,
val val
@@ -72,14 +71,14 @@ export function ScoreboardChart({ sbEntriesByDate, bodyshop }) {
bodyshop.scoreboard_target.dailyPaintTarget, bodyshop.scoreboard_target.dailyPaintTarget,
1 1
), ),
accHrs: _.round( accHrs: round(
acc.length > 0 acc.length > 0
? acc[acc.length - 1].accHrs + dayhrs.painthrs + dayhrs.bodyhrs ? acc[acc.length - 1].accHrs + dayhrs.painthrs + dayhrs.bodyhrs
: dayhrs.painthrs + dayhrs.bodyhrs, : dayhrs.painthrs + dayhrs.bodyhrs,
1 1
), ),
sales: _.round(dayhrs.sales, 2), sales: round(dayhrs.sales, 2),
accSales: _.round(acc.length > 0 ? acc[acc.length - 1].accSales + dayhrs.sales : dayhrs.sales, 2) accSales: round(acc.length > 0 ? acc[acc.length - 1].accSales + dayhrs.sales : dayhrs.sales, 2)
}; };
return [...acc, theValue]; return [...acc, theValue];

View File

@@ -1,23 +1,25 @@
import { Col, Row } from "antd"; import { Col, Row, Spin } from "antd";
import { useEffect } from "react"; import { useEffect, useState } from "react";
import ScoreboardChart from "../scoreboard-chart/scoreboard-chart.component"; import ScoreboardChart from "../scoreboard-chart/scoreboard-chart.component";
import ScoreboardLastDays from "../scoreboard-last-days/scoreboard-last-days.component"; import ScoreboardLastDays from "../scoreboard-last-days/scoreboard-last-days.component";
import ScoreboardTargetsTable from "../scoreboard-targets-table/scoreboard-targets-table.component"; import ScoreboardTargetsTable from "../scoreboard-targets-table/scoreboard-targets-table.component";
import { useApolloClient, useQuery } from "@apollo/client"; import { useApolloClient, useQuery } from "@apollo/client";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { GET_BLOCKED_DAYS, QUERY_SCOREBOARD } from "../../graphql/scoreboard.queries"; import { GET_BLOCKED_DAYS, QUERY_SCOREBOARD } from "../../graphql/scoreboard.queries";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import dayjs from "../../utils/day"; import dayjs from "../../utils/day";
import {
clearHolidays,
clearWorkingWeekdays,
setHolidays,
setWorkingWeekdays
} from "../scoreboard-targets-table/scoreboard-targets-table.util";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
bodyshop: selectBodyshop bodyshop: selectBodyshop
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = () => ({});
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(ScoreboardDisplayComponent); export default connect(mapStateToProps, mapDispatchToProps)(ScoreboardDisplayComponent);
export function ScoreboardDisplayComponent({ bodyshop }) { export function ScoreboardDisplayComponent({ bodyshop }) {
@@ -32,7 +34,6 @@ export function ScoreboardDisplayComponent({ bodyshop }) {
const { data } = scoreboardSubscription; const { data } = scoreboardSubscription;
const client = useApolloClient(); const client = useApolloClient();
const scoreBoardlist = data?.scoreboard || []; const scoreBoardlist = data?.scoreboard || [];
const sbEntriesByDate = {}; const sbEntriesByDate = {};
scoreBoardlist.forEach((i) => { scoreBoardlist.forEach((i) => {
@@ -43,35 +44,52 @@ export function ScoreboardDisplayComponent({ bodyshop }) {
sbEntriesByDate[entryDate].push(i); sbEntriesByDate[entryDate].push(i);
}); });
useEffect(() => { const [loading, setLoading] = useState(true); // Loading state
//Update the locals.
async function setDayJSSettings() {
let appointments;
if (!bodyshop.scoreboard_target.ignoreblockeddays) { useEffect(() => {
const { data } = await client.query({ async function setDayJSSettings() {
query: GET_BLOCKED_DAYS, try {
variables: { let appointments;
start: dayjs().startOf("month"),
end: dayjs().endOf("month") if (!bodyshop.scoreboard_target.ignoreblockeddays) {
} const { data } = await client.query({
}); query: GET_BLOCKED_DAYS,
appointments = data.appointments; variables: {
} start: dayjs().startOf("month"),
dayjs.updateLocale(dayjs.locale(), { end: dayjs().endOf("month")
workingWeekdays: translateSettingsToWorkingDays(bodyshop.workingdays),
...(appointments?.length
? {
holidays: appointments.map((h) => dayjs(h.start).format("MM-DD-YYYY"))
} }
: {}), });
holidayFormat: "MM-DD-YYYY" appointments = data.appointments;
}); }
const holidays = appointments ? appointments.map((h) => dayjs(h.start).format("MM-DD-YYYY")) : [];
const workingWeekdays = translateSettingsToWorkingDays(bodyshop.workingdays);
// Set holidays and working weekdays
setHolidays(holidays);
setWorkingWeekdays(workingWeekdays);
} finally {
setLoading(false); // Set loading to false after processing
}
} }
setDayJSSettings(); setDayJSSettings();
// Cleanup on unmount
return () => {
clearHolidays();
clearWorkingWeekdays();
};
}, [client, bodyshop]); }, [client, bodyshop]);
if (loading) {
return (
<Row justify="center" align="middle" style={{ minHeight: "100vh" }}>
<Spin size="large" />
</Row>
);
}
return ( return (
<Row gutter={[16, 16]}> <Row gutter={[16, 16]}>
<Col span={24}> <Col span={24}>
@@ -89,27 +107,12 @@ export function ScoreboardDisplayComponent({ bodyshop }) {
function translateSettingsToWorkingDays(workingdays) { function translateSettingsToWorkingDays(workingdays) {
const days = []; const days = [];
if (workingdays.monday) days.push(1);
if (workingdays.monday) { if (workingdays.tuesday) days.push(2);
days.push(1); if (workingdays.wednesday) days.push(3);
} if (workingdays.thursday) days.push(4);
if (workingdays.tuesday) { if (workingdays.friday) days.push(5);
days.push(2); if (workingdays.saturday) days.push(6);
} if (workingdays.sunday) days.push(0);
if (workingdays.wednesday) {
days.push(3);
}
if (workingdays.thursday) {
days.push(4);
}
if (workingdays.friday) {
days.push(5);
}
if (workingdays.saturday) {
days.push(6);
}
if (workingdays.sunday) {
days.push(0);
}
return days; return days;
} }

View File

@@ -1,4 +1,3 @@
import React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
@@ -10,7 +9,7 @@ import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop bodyshop: selectBodyshop
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = () => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
}); });
@@ -26,7 +25,7 @@ export function ScoreboardLastDays({ bodyshop, sbEntriesByDate }) {
<Row> <Row>
{ArrayOfDate.map((a) => ( {ArrayOfDate.map((a) => (
<Col span={2} key={a}> <Col span={2} key={a}>
{!!sbEntriesByDate ? <ScoreboardDayStat date={a} entries={sbEntriesByDate[a] || []} /> : <LoadingSkeleton />} {sbEntriesByDate ? <ScoreboardDayStat date={a} entries={sbEntriesByDate[a] || []} /> : <LoadingSkeleton />}
</Col> </Col>
))} ))}
</Row> </Row>

View File

@@ -1,8 +1,8 @@
import { CalendarOutlined } from "@ant-design/icons"; import { CalendarOutlined } from "@ant-design/icons";
import { Card, Col, Divider, Row, Statistic } from "antd"; import { Card, Col, Divider, Row, Statistic } from "antd";
import _ from "lodash"; import { groupBy } from "lodash";
import dayjs from "../../utils/day"; import dayjs from "../../utils/day";
import React, { useMemo } from "react"; import { useMemo } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
@@ -13,7 +13,7 @@ import * as Util from "./scoreboard-targets-table.util";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop bodyshop: selectBodyshop
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = () => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
}); });
@@ -24,7 +24,7 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
const { t } = useTranslation(); const { t } = useTranslation();
const values = useMemo(() => { const values = useMemo(() => {
const dateHash = _.groupBy(scoreBoardlist, "date"); const dateHash = groupBy(scoreBoardlist, "date");
let ret = { let ret = {
todayBody: 0, todayBody: 0,
@@ -213,4 +213,5 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
</Card> </Card>
); );
} }
export default connect(mapStateToProps, mapDispatchToProps)(ScoreboardTargetsTable); export default connect(mapStateToProps, mapDispatchToProps)(ScoreboardTargetsTable);

View File

@@ -1,29 +1,172 @@
import dayjs from "../../utils/day"; import dayjs from "../../utils/day";
export const CalculateWorkingDaysThisMonth = () => dayjs().endOf("month").businessDaysInMonth().length; const DEFAULT_WORKING_DAYS = [1, 2, 3, 4, 5]; // Default to Monday-Friday
export const CalculateWorkingDaysInPeriod = (start, end) => dayjs(end).businessDiff(dayjs(start)); // Module-level state for holidays and working weekdays
let holidays = [];
let workingWeekdays = DEFAULT_WORKING_DAYS;
export const CalculateWorkingDaysAsOfToday = () => dayjs().endOf("day").businessDiff(dayjs().startOf("month")); /**
* Sets the holidays for the business logic.
* @param newHolidays
*/
export const setHolidays = (newHolidays = []) => {
holidays = newHolidays;
};
export const CalculateWorkingDaysLastMonth = () => /**
dayjs().subtract(1, "month").endOf("month").businessDaysInMonth().length; * Clears the holidays.
*/
export const clearHolidays = () => {
holidays = [];
};
/**
* Sets the working weekdays for the business logic.
* @param newWorkingWeekdays
*/
export const setWorkingWeekdays = (newWorkingWeekdays = DEFAULT_WORKING_DAYS) => {
workingWeekdays = newWorkingWeekdays;
};
/**
* Clears the working weekdays, resetting to default (Monday-Friday).
*/
export const clearWorkingWeekdays = () => {
workingWeekdays = DEFAULT_WORKING_DAYS; // Reset to default
};
/**
* Translates the bodyshop working days settings to an array of weekdays.
* @returns {*[]}
*/
export const getHolidays = () => {
return holidays;
};
/**
* Translates the working days settings from the bodyshop to an array of weekdays.
* @returns {number[]}
*/
export const getWorkingWeekdays = () => {
return workingWeekdays;
};
/**
* Calculates the number of working days in the current month, excluding holidays.
* @returns {number}
* @constructor
*/
export const CalculateWorkingDaysThisMonth = () => {
const businessDays = dayjs().businessDaysInMonth();
return businessDays.filter((day) => !holidays.includes(dayjs(day).format("MM-DD-YYYY"))).length;
};
/**
* Calculates the number of working days in a given period, excluding holidays.
* @param start
* @param end
* @returns {number}
* @constructor
*/
export const CalculateWorkingDaysInPeriod = (start, end) => {
let businessDays = dayjs(end).businessDiff(dayjs(start));
if (dayjs(end).isBusinessDay() && !holidays.includes(dayjs(end).format("MM-DD-YYYY"))) {
businessDays += 1;
}
return businessDays;
};
/**
* Calculates the number of working days as of today, excluding holidays.
* @returns {number}
* @constructor
*/
export const CalculateWorkingDaysAsOfToday = () => {
const today = dayjs().startOf("day");
let businessDays = today.businessDiff(dayjs().startOf("month"));
if (today.isBusinessDay() && !holidays.includes(today.format("MM-DD-YYYY"))) {
businessDays += 1;
}
return businessDays;
};
/**
* Calculates the number of working days in the last month, excluding holidays.
* @returns {number}
* @constructor
*/
export const CalculateWorkingDaysLastMonth = () => {
const businessDays = dayjs().subtract(1, "month").businessDaysInMonth();
return businessDays.filter((day) => !holidays.includes(dayjs(day).format("MM-DD-YYYY"))).length;
};
/**
* Calculates the weekly target hours based on daily target hours and the number of working days in the current week.
* @param dailyTargetHrs
* @returns {number}
* @constructor
*/
export const WeeklyTargetHrs = (dailyTargetHrs) => export const WeeklyTargetHrs = (dailyTargetHrs) =>
dailyTargetHrs * CalculateWorkingDaysInPeriod(dayjs().startOf("week"), dayjs().endOf("week")); dailyTargetHrs * CalculateWorkingDaysInPeriod(dayjs().startOf("week"), dayjs().endOf("week"));
/**
* Calculates the weekly target hours for a specific period.
* @param dailyTargetHrs
* @param start
* @param end
* @returns {number}
* @constructor
*/
export const WeeklyTargetHrsInPeriod = (dailyTargetHrs, start, end) => export const WeeklyTargetHrsInPeriod = (dailyTargetHrs, start, end) =>
dailyTargetHrs * CalculateWorkingDaysInPeriod(start, end); dailyTargetHrs * CalculateWorkingDaysInPeriod(start, end);
/**
* Calculates the monthly target hours based on daily target hours and the number of working days in the current month.
* @param dailyTargetHrs
* @returns {number}
* @constructor
*/
export const MonthlyTargetHrs = (dailyTargetHrs) => dailyTargetHrs * CalculateWorkingDaysThisMonth(); export const MonthlyTargetHrs = (dailyTargetHrs) => dailyTargetHrs * CalculateWorkingDaysThisMonth();
/**
* Calculates the monthly target hours for the last month based on daily target hours and the number of working days
* in the last month.
* @param dailyTargetHrs
* @returns {number}
* @constructor
*/
export const LastMonthTargetHrs = (dailyTargetHrs) => dailyTargetHrs * CalculateWorkingDaysLastMonth(); export const LastMonthTargetHrs = (dailyTargetHrs) => dailyTargetHrs * CalculateWorkingDaysLastMonth();
/**
* Calculates the target hours as of today based on daily target hours and the number of working days as of today.
* @param dailyTargetHrs
* @returns {number}
* @constructor
*/
export const AsOfTodayTargetHrs = (dailyTargetHrs) => dailyTargetHrs * CalculateWorkingDaysAsOfToday(); export const AsOfTodayTargetHrs = (dailyTargetHrs) => dailyTargetHrs * CalculateWorkingDaysAsOfToday();
export const AsOfDateTargetHours = (dailyTargetHours, date) => /**
dailyTargetHours * dayjs(date).businessDiff(dayjs().startOf("month")); * Calculates the target hours as of a specific date based on daily target hours and the number of business days up to
* that date.
* @param dailyTargetHours
* @param date
* @returns {number}
* @constructor
*/
export const AsOfDateTargetHours = (dailyTargetHours, date) => {
let businessDays = dayjs(date).businessDiff(dayjs().startOf("month"));
if (dayjs(date).isBusinessDay() && !holidays.includes(dayjs(date).format("MM-DD-YYYY"))) {
businessDays += 1;
}
return dailyTargetHours * businessDays;
};
/**
* Generates a list of all days in the current month.
* @returns {*[]}
* @constructor
*/
export const ListOfDaysInCurrentMonth = () => { export const ListOfDaysInCurrentMonth = () => {
const days = []; const days = [];
let dateStart = dayjs().startOf("month"); let dateStart = dayjs().startOf("month");
@@ -36,6 +179,13 @@ export const ListOfDaysInCurrentMonth = () => {
return days; return days;
}; };
/**
* Generates a list of all days between two dates.
* @param start
* @param end
* @returns {*[]}
* @constructor
*/
export const ListDaysBetween = ({ start, end }) => { export const ListDaysBetween = ({ start, end }) => {
const days = []; const days = [];
let dateStart = dayjs(start); let dateStart = dayjs(start);