Compare commits
30 Commits
feature/IO
...
feature/IO
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ae41b7016 | ||
|
|
9c59fd4c00 | ||
|
|
a9f959cced | ||
|
|
414897bba0 | ||
|
|
7467a31d76 | ||
|
|
894f6bf6d2 | ||
|
|
744dfa8163 | ||
|
|
2293119518 | ||
|
|
bd529a0dfa | ||
|
|
57ad89747f | ||
|
|
3ae8f38adb | ||
|
|
dc5ed1a39c | ||
|
|
aa6e6b8980 | ||
|
|
1dc80c068b | ||
|
|
bd0c4ceae2 | ||
|
|
30b58c6ea5 | ||
|
|
a55e9224f8 | ||
|
|
0c80abb3ca | ||
|
|
7137e611cd | ||
|
|
6f9d291d36 | ||
|
|
f2a2653eae | ||
|
|
73c25ab91f | ||
|
|
780449bac6 | ||
|
|
2509a1ecf3 | ||
|
|
16075f7ddd | ||
|
|
27d28e7ffc | ||
|
|
66b87e5c45 | ||
|
|
c1e1dff7d2 | ||
|
|
f76eb7abf5 | ||
|
|
25ea2a80a3 |
@@ -110,7 +110,7 @@ export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, c
|
||||
trigger="click"
|
||||
open={open}
|
||||
onOpenChange={handleVisibleChange}
|
||||
overlayClassName="media-selector-popover"
|
||||
classNames={{ root: "media-selector-popover" }}
|
||||
>
|
||||
<Badge count={selectedMedia.filter((s) => s.isSelected).length}>
|
||||
<PictureFilled style={{ margin: "0 .5rem" }} />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
.media-selector-popover {
|
||||
.ant-popover-inner-content {
|
||||
position: relative;
|
||||
max-width: 640px;
|
||||
max-height: 480px;
|
||||
overflow-y: auto;
|
||||
@@ -36,11 +37,6 @@
|
||||
border-radius: 4px;
|
||||
margin: 4px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
/* Grid layout for gallery components */
|
||||
|
||||
@@ -335,7 +335,11 @@ export function JobsDetailHeader({ job, bodyshop, disabled, insertAuditTrail })
|
||||
</Card>
|
||||
</Col>
|
||||
<Col {...colSpan}>
|
||||
<Card style={{ height: "100%" }} title={t("jobs.labels.employeeassignments")}>
|
||||
<Card
|
||||
style={{ height: "100%" }}
|
||||
title=<span id="job-employee-assignments-title">{t("jobs.labels.employeeassignments")}</span>
|
||||
id={"job-employee-assignments"}
|
||||
>
|
||||
<div>
|
||||
<JobEmployeeAssignments job={job} />
|
||||
<Divider style={{ margin: ".5rem" }} />
|
||||
|
||||
@@ -3,7 +3,6 @@ import axios from "axios";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import cleanAxios from "../../utils/CleanAxios";
|
||||
import formatBytes from "../../utils/formatbytes";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
@@ -12,7 +11,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
const mapDispatchToProps = () => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
@@ -26,7 +25,7 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobsDocumentsImgproxyDownloadButton);
|
||||
|
||||
export function JobsDocumentsImgproxyDownloadButton({ bodyshop, galleryImages, identifier, jobId }) {
|
||||
export function JobsDocumentsImgproxyDownloadButton({ galleryImages, identifier, jobId }) {
|
||||
const { t } = useTranslation();
|
||||
const [download, setDownload] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -46,32 +45,40 @@ export function JobsDocumentsImgproxyDownloadButton({ bodyshop, galleryImages, i
|
||||
}
|
||||
|
||||
function standardMediaDownload(bufferData) {
|
||||
const a = document.createElement("a");
|
||||
const url = window.URL.createObjectURL(new Blob([bufferData]));
|
||||
a.href = url;
|
||||
a.download = `${identifier || "documents"}.zip`;
|
||||
a.click();
|
||||
try {
|
||||
const a = document.createElement("a");
|
||||
const url = window.URL.createObjectURL(new Blob([bufferData]));
|
||||
a.href = url;
|
||||
a.download = `${identifier || "documents"}.zip`;
|
||||
a.click();
|
||||
} catch (error) {
|
||||
setLoading(false);
|
||||
setDownload(null);
|
||||
}
|
||||
}
|
||||
|
||||
const handleDownload = async () => {
|
||||
logImEXEvent("jobs_documents_download");
|
||||
setLoading(true);
|
||||
const zipUrl = await axios({
|
||||
url: "/media/imgproxy/download",
|
||||
method: "POST",
|
||||
data: { jobId, documentids: imagesToDownload.map((_) => _.id) }
|
||||
});
|
||||
try {
|
||||
const response = await axios({
|
||||
url: "/media/imgproxy/download",
|
||||
method: "POST",
|
||||
responseType: "blob",
|
||||
data: { jobId, documentids: imagesToDownload.map((_) => _.id) },
|
||||
onDownloadProgress: downloadProgress
|
||||
});
|
||||
|
||||
const theDownloadedZip = await cleanAxios({
|
||||
url: zipUrl.data.url,
|
||||
method: "GET",
|
||||
responseType: "arraybuffer",
|
||||
onDownloadProgress: downloadProgress
|
||||
});
|
||||
setLoading(false);
|
||||
setDownload(null);
|
||||
setLoading(false);
|
||||
setDownload(null);
|
||||
|
||||
standardMediaDownload(theDownloadedZip.data);
|
||||
// Use the response data (Blob) to trigger download
|
||||
standardMediaDownload(response.data);
|
||||
} catch (error) {
|
||||
setLoading(false);
|
||||
setDownload(null);
|
||||
// handle error (optional)
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -98,7 +98,13 @@ function JobsDocumentsImgproxyComponent({
|
||||
jobId={jobId}
|
||||
totalSize={totalSize}
|
||||
billId={billId}
|
||||
callbackAfterUpload={billsCallback || fetchThumbnails || refetch}
|
||||
callbackAfterUpload={
|
||||
billsCallback ||
|
||||
function () {
|
||||
isFunction(refetch) && refetch();
|
||||
isFunction(fetchThumbnails) && fetchThumbnails();
|
||||
}
|
||||
}
|
||||
ignoreSizeLimit={ignoreSizeLimit}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import Icon from "@ant-design/icons";
|
||||
import { Card, Popover, Space } from "antd";
|
||||
import _ from "lodash";
|
||||
import { groupBy } from "lodash";
|
||||
import dayjs from "../../utils/day";
|
||||
|
||||
import React, { useMemo } from "react";
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { MdFileDownload, MdFileUpload } from "react-icons/md";
|
||||
import { connect } from "react-redux";
|
||||
@@ -26,21 +26,12 @@ const mapStateToProps = createStructuredSelector({
|
||||
calculating: selectScheduleLoadCalculating
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({});
|
||||
const mapDispatchToProps = () => ({});
|
||||
|
||||
export function ScheduleCalendarHeaderComponent({
|
||||
bodyshop,
|
||||
label,
|
||||
refetch,
|
||||
date,
|
||||
load,
|
||||
calculating,
|
||||
events,
|
||||
...otherProps
|
||||
}) {
|
||||
export function ScheduleCalendarHeaderComponent({ bodyshop, label, refetch, date, load, calculating, events }) {
|
||||
const ATSToday = useMemo(() => {
|
||||
if (!events) return [];
|
||||
return _.groupBy(
|
||||
return groupBy(
|
||||
events.filter((e) => !e.vacation && e.isintake && dayjs(date).isSame(dayjs(e.start), "day")),
|
||||
"job.alt_transport"
|
||||
);
|
||||
@@ -155,7 +146,11 @@ export function ScheduleCalendarHeaderComponent({
|
||||
<Space size="small">
|
||||
<Icon component={MdFileDownload} style={{ color: "green" }} />
|
||||
<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>
|
||||
</Space>
|
||||
</Popover>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Card } from "antd";
|
||||
import Dinero from "dinero.js";
|
||||
import _ from "lodash";
|
||||
import { round } from "lodash";
|
||||
import dayjs from "../../utils/day";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import {
|
||||
Area,
|
||||
@@ -29,7 +28,7 @@ const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
const mapDispatchToProps = () => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ScoreboardChart);
|
||||
@@ -40,7 +39,7 @@ export function ScoreboardChart({ sbEntriesByDate, bodyshop }) {
|
||||
const data = listOfBusDays.reduce((acc, val) => {
|
||||
//Sum up the current day.
|
||||
let dayhrs;
|
||||
if (!!sbEntriesByDate[val]) {
|
||||
if (sbEntriesByDate[val]) {
|
||||
dayhrs = sbEntriesByDate[val].reduce(
|
||||
(dayAcc, dayVal) => {
|
||||
return {
|
||||
@@ -61,9 +60,9 @@ export function ScoreboardChart({ sbEntriesByDate, bodyshop }) {
|
||||
|
||||
const theValue = {
|
||||
date: dayjs(val).format("D ddd"),
|
||||
paintHrs: _.round(dayhrs.painthrs, 1),
|
||||
bodyHrs: _.round(dayhrs.bodyhrs, 1),
|
||||
accTargetHrs: _.round(
|
||||
paintHrs: round(dayhrs.painthrs, 1),
|
||||
bodyHrs: round(dayhrs.bodyhrs, 1),
|
||||
accTargetHrs: round(
|
||||
Utils.AsOfDateTargetHours(
|
||||
bodyshop.scoreboard_target.dailyBodyTarget + bodyshop.scoreboard_target.dailyPaintTarget,
|
||||
val
|
||||
@@ -72,14 +71,14 @@ export function ScoreboardChart({ sbEntriesByDate, bodyshop }) {
|
||||
bodyshop.scoreboard_target.dailyPaintTarget,
|
||||
1
|
||||
),
|
||||
accHrs: _.round(
|
||||
accHrs: round(
|
||||
acc.length > 0
|
||||
? acc[acc.length - 1].accHrs + dayhrs.painthrs + dayhrs.bodyhrs
|
||||
: dayhrs.painthrs + dayhrs.bodyhrs,
|
||||
1
|
||||
),
|
||||
sales: _.round(dayhrs.sales, 2),
|
||||
accSales: _.round(acc.length > 0 ? acc[acc.length - 1].accSales + dayhrs.sales : dayhrs.sales, 2)
|
||||
sales: round(dayhrs.sales, 2),
|
||||
accSales: round(acc.length > 0 ? acc[acc.length - 1].accSales + dayhrs.sales : dayhrs.sales, 2)
|
||||
};
|
||||
|
||||
return [...acc, theValue];
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
import { Col, Row } from "antd";
|
||||
import React, { useEffect } from "react";
|
||||
import { Col, Row, Spin } from "antd";
|
||||
import { useEffect, useState } from "react";
|
||||
import ScoreboardChart from "../scoreboard-chart/scoreboard-chart.component";
|
||||
import ScoreboardLastDays from "../scoreboard-last-days/scoreboard-last-days.component";
|
||||
import ScoreboardTargetsTable from "../scoreboard-targets-table/scoreboard-targets-table.component";
|
||||
|
||||
import { useApolloClient, useQuery } from "@apollo/client";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { GET_BLOCKED_DAYS, QUERY_SCOREBOARD } from "../../graphql/scoreboard.queries";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import dayjs from "../../utils/day";
|
||||
import {
|
||||
clearHolidays,
|
||||
clearWorkingWeekdays,
|
||||
setHolidays,
|
||||
setWorkingWeekdays
|
||||
} from "../scoreboard-targets-table/scoreboard-targets-table.util";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
const mapDispatchToProps = () => ({});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ScoreboardDisplayComponent);
|
||||
|
||||
export function ScoreboardDisplayComponent({ bodyshop }) {
|
||||
@@ -26,63 +28,76 @@ export function ScoreboardDisplayComponent({ bodyshop }) {
|
||||
start: dayjs().startOf("month"),
|
||||
end: dayjs().endOf("month")
|
||||
},
|
||||
pollInterval: 60000*5
|
||||
pollInterval: 60000 * 5
|
||||
});
|
||||
|
||||
const { data } = scoreboardSubscription;
|
||||
const client = useApolloClient();
|
||||
const scoreBoardlist = (data && data.scoreboard) || [];
|
||||
|
||||
const scoreBoardlist = data?.scoreboard || [];
|
||||
const sbEntriesByDate = {};
|
||||
|
||||
scoreBoardlist.forEach((i) => {
|
||||
const entryDate = i.date;
|
||||
if (!!!sbEntriesByDate[entryDate]) {
|
||||
if (!sbEntriesByDate[entryDate]) {
|
||||
sbEntriesByDate[entryDate] = [];
|
||||
}
|
||||
sbEntriesByDate[entryDate].push(i);
|
||||
});
|
||||
|
||||
const [loading, setLoading] = useState(true); // Loading state
|
||||
|
||||
useEffect(() => {
|
||||
//Update the locals.
|
||||
async function setDayJSSettings() {
|
||||
let appointments;
|
||||
try {
|
||||
let appointments;
|
||||
|
||||
if (!bodyshop.scoreboard_target.ignoreblockeddays) {
|
||||
const { data } = await client.query({
|
||||
query: GET_BLOCKED_DAYS,
|
||||
variables: {
|
||||
start: dayjs().startOf("month"),
|
||||
end: dayjs().endOf("month")
|
||||
}
|
||||
});
|
||||
appointments = data.appointments;
|
||||
}
|
||||
|
||||
dayjs.updateLocale("ca", {
|
||||
workingWeekdays: translateSettingsToWorkingDays(bodyshop.workingdays),
|
||||
...(appointments
|
||||
? {
|
||||
holidays: appointments.map((h) => dayjs(h.start).format("MM-DD-YYYY"))
|
||||
if (!bodyshop.scoreboard_target.ignoreblockeddays) {
|
||||
const { data } = await client.query({
|
||||
query: GET_BLOCKED_DAYS,
|
||||
variables: {
|
||||
start: dayjs().startOf("month"),
|
||||
end: dayjs().endOf("month")
|
||||
}
|
||||
: {}),
|
||||
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();
|
||||
|
||||
// Cleanup on unmount
|
||||
return () => {
|
||||
clearHolidays();
|
||||
clearWorkingWeekdays();
|
||||
};
|
||||
}, [client, bodyshop]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Row justify="center" align="middle" style={{ minHeight: "100vh" }}>
|
||||
<Spin size="large" />
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
<ScoreboardTargetsTable scoreBoardlist={scoreBoardlist} />
|
||||
</Col>
|
||||
|
||||
<Col span={24}>
|
||||
<ScoreboardLastDays sbEntriesByDate={sbEntriesByDate} />
|
||||
</Col>
|
||||
|
||||
<Col span={24}>
|
||||
<ScoreboardChart sbEntriesByDate={sbEntriesByDate} />
|
||||
</Col>
|
||||
@@ -92,27 +107,12 @@ export function ScoreboardDisplayComponent({ bodyshop }) {
|
||||
|
||||
function translateSettingsToWorkingDays(workingdays) {
|
||||
const days = [];
|
||||
|
||||
if (workingdays.monday) {
|
||||
days.push(1);
|
||||
}
|
||||
if (workingdays.tuesday) {
|
||||
days.push(2);
|
||||
}
|
||||
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);
|
||||
}
|
||||
if (workingdays.monday) days.push(1);
|
||||
if (workingdays.tuesday) days.push(2);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
@@ -10,7 +9,7 @@ import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
const mapDispatchToProps = () => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
@@ -26,7 +25,7 @@ export function ScoreboardLastDays({ bodyshop, sbEntriesByDate }) {
|
||||
<Row>
|
||||
{ArrayOfDate.map((a) => (
|
||||
<Col span={2} key={a}>
|
||||
{!!sbEntriesByDate ? <ScoreboardDayStat date={a} entries={sbEntriesByDate[a] || []} /> : <LoadingSkeleton />}
|
||||
{sbEntriesByDate ? <ScoreboardDayStat date={a} entries={sbEntriesByDate[a] || []} /> : <LoadingSkeleton />}
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { CalendarOutlined } from "@ant-design/icons";
|
||||
import { Card, Col, Divider, Row, Statistic } from "antd";
|
||||
import _ from "lodash";
|
||||
import { groupBy } from "lodash";
|
||||
import dayjs from "../../utils/day";
|
||||
import React, { useMemo } from "react";
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
@@ -13,7 +13,7 @@ import * as Util from "./scoreboard-targets-table.util";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
const mapDispatchToProps = () => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
@@ -24,7 +24,7 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const values = useMemo(() => {
|
||||
const dateHash = _.groupBy(scoreBoardlist, "date");
|
||||
const dateHash = groupBy(scoreBoardlist, "date");
|
||||
|
||||
let ret = {
|
||||
todayBody: 0,
|
||||
@@ -213,4 +213,5 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ScoreboardTargetsTable);
|
||||
|
||||
@@ -1,29 +1,172 @@
|
||||
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) =>
|
||||
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) =>
|
||||
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();
|
||||
|
||||
/**
|
||||
* 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();
|
||||
|
||||
/**
|
||||
* 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 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 = () => {
|
||||
const days = [];
|
||||
let dateStart = dayjs().startOf("month");
|
||||
@@ -36,6 +179,13 @@ export const ListOfDaysInCurrentMonth = () => {
|
||||
return days;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates a list of all days between two dates.
|
||||
* @param start
|
||||
* @param end
|
||||
* @returns {*[]}
|
||||
* @constructor
|
||||
*/
|
||||
export const ListDaysBetween = ({ start, end }) => {
|
||||
const days = [];
|
||||
let dateStart = dayjs(start);
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { DeleteFilled } from "@ant-design/icons";
|
||||
import { useApolloClient, useMutation, useQuery } from "@apollo/client";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import { Button, Card, Form, Input, InputNumber, Select, Switch, Table } from "antd";
|
||||
import { useForm } from "antd/es/form/Form";
|
||||
import dayjs from "../../utils/day";
|
||||
import React, { useEffect } from "react";
|
||||
import queryString from "query-string";
|
||||
import { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import {
|
||||
CHECK_EMPLOYEE_NUMBER,
|
||||
@@ -20,19 +22,17 @@ import {
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CiecaSelect from "../../utils/Ciecaselect";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import dayjs from "../../utils/day";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
|
||||
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
import ShopEmployeeAddVacation from "./shop-employees-add-vacation.component";
|
||||
import queryString from "query-string";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
const mapDispatchToProps = () => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
@@ -83,7 +83,7 @@ export function ShopEmployeesFormComponent({ bodyshop }) {
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((r) => {
|
||||
.then(() => {
|
||||
notification["success"]({
|
||||
message: t("employees.successes.save")
|
||||
});
|
||||
@@ -120,13 +120,13 @@ export function ShopEmployeesFormComponent({ bodyshop }) {
|
||||
title: t("employees.fields.vacation.start"),
|
||||
dataIndex: "start",
|
||||
key: "start",
|
||||
render: (text, record) => <DateFormatter>{text}</DateFormatter>
|
||||
render: (text) => <DateFormatter>{text}</DateFormatter>
|
||||
},
|
||||
{
|
||||
title: t("employees.fields.vacation.end"),
|
||||
dataIndex: "end",
|
||||
key: "end",
|
||||
render: (text, record) => <DateFormatter>{text}</DateFormatter>
|
||||
render: (text) => <DateFormatter>{text}</DateFormatter>
|
||||
},
|
||||
{
|
||||
title: t("employees.fields.vacation.length"),
|
||||
@@ -210,7 +210,7 @@ export function ShopEmployeesFormComponent({ bodyshop }) {
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
({ getFieldValue }) => ({
|
||||
() => ({
|
||||
async validator(rule, value) {
|
||||
if (value) {
|
||||
const response = await client.query({
|
||||
@@ -369,8 +369,9 @@ export function ShopEmployeesFormComponent({ bodyshop }) {
|
||||
add();
|
||||
}}
|
||||
style={{ width: "100%" }}
|
||||
id="add-employee-rate-button"
|
||||
>
|
||||
{t("employees.actions.newrate")}
|
||||
<span id="new-employee-rate">{t("employees.actions.newrate")}</span>
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</div>
|
||||
@@ -383,7 +384,7 @@ export function ShopEmployeesFormComponent({ bodyshop }) {
|
||||
title={() => <ShopEmployeeAddVacation employee={data && data.employees_by_pk} />}
|
||||
columns={columns}
|
||||
rowKey={"id"}
|
||||
dataSource={data ? data.employees_by_pk.employee_vacations : []}
|
||||
dataSource={data?.employees_by_pk?.employee_vacations ?? []}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -14,6 +14,7 @@ import PhoneFormItem, { PhoneItemFormatterValidation } from "../form-items-forma
|
||||
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
const timeZonesList = Intl.supportedValuesOf("timeZone");
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
@@ -143,236 +144,246 @@ export function ShopInfoGeneral({ form, bodyshop }) {
|
||||
<InputNumber min={0} />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<FeatureWrapper featureName="export" noauth={() => null}>
|
||||
<LayoutFormRow header={t("bodyshop.labels.accountingsetup")} id="accountingsetup">
|
||||
<Form.Item label={t("bodyshop.labels.qbo")} valuePropName="checked" name={["accountingconfig", "qbo"]}>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
{InstanceRenderManager({
|
||||
imex: (
|
||||
<Form.Item shouldUpdate noStyle>
|
||||
{() => (
|
||||
<LayoutFormRow header={t("bodyshop.labels.accountingsetup")} id="accountingsetup">
|
||||
{HasFeatureAccess({ featureName: "export", bodyshop }) && (
|
||||
<>
|
||||
<Form.Item label={t("bodyshop.labels.qbo")} valuePropName="checked" name={["accountingconfig", "qbo"]}>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
{InstanceRenderManager({
|
||||
imex: (
|
||||
<Form.Item shouldUpdate noStyle>
|
||||
{() => (
|
||||
<Form.Item
|
||||
label={t("bodyshop.labels.qbo_usa")}
|
||||
shouldUpdate
|
||||
valuePropName="checked"
|
||||
name={["accountingconfig", "qbo_usa"]}
|
||||
>
|
||||
<Switch disabled={!form.getFieldValue(["accountingconfig", "qbo"])} />
|
||||
</Form.Item>
|
||||
)}
|
||||
</Form.Item>
|
||||
)
|
||||
})}
|
||||
<Form.Item label={t("bodyshop.labels.qbo_departmentid")} name={["accountingconfig", "qbo_departmentid"]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.labels.accountingtiers")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["accountingconfig", "tiers"]}
|
||||
>
|
||||
<Radio.Group>
|
||||
<Radio value={2}>2</Radio>
|
||||
<Radio value={3}>3</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
return (
|
||||
<Form.Item
|
||||
label={t("bodyshop.labels.qbo_usa")}
|
||||
label={t("bodyshop.labels.2tiersetup")}
|
||||
shouldUpdate
|
||||
valuePropName="checked"
|
||||
name={["accountingconfig", "qbo_usa"]}
|
||||
rules={[
|
||||
{
|
||||
required: form.getFieldValue(["accountingconfig", "tiers"]) === 2
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["accountingconfig", "twotierpref"]}
|
||||
>
|
||||
<Switch disabled={!form.getFieldValue(["accountingconfig", "qbo"])} />
|
||||
<Radio.Group disabled={form.getFieldValue(["accountingconfig", "tiers"]) === 3}>
|
||||
<Radio value="name">{t("bodyshop.labels.2tiername")}</Radio>
|
||||
<Radio value="source">{t("bodyshop.labels.2tiersource")}</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
)}
|
||||
</Form.Item>
|
||||
)
|
||||
})}
|
||||
<Form.Item label={t("bodyshop.labels.qbo_departmentid")} name={["accountingconfig", "qbo_departmentid"]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.labels.accountingtiers")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["accountingconfig", "tiers"]}
|
||||
>
|
||||
<Radio.Group>
|
||||
<Radio value={2}>2</Radio>
|
||||
<Radio value={3}>3</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
return (
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.labels.printlater")}
|
||||
valuePropName="checked"
|
||||
name={["accountingconfig", "printlater"]}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.labels.emaillater")}
|
||||
valuePropName="checked"
|
||||
name={["accountingconfig", "emaillater"]}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.inhousevendorid")}
|
||||
name={"inhousevendorid"}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.default_adjustment_rate")}
|
||||
name={"default_adjustment_rate"}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} precision={2} />
|
||||
</Form.Item>
|
||||
{InstanceRenderManager({
|
||||
imex: (
|
||||
<Form.Item label={t("bodyshop.fields.federal_tax_id")} name="federal_tax_id">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
)
|
||||
})}
|
||||
<Form.Item label={t("bodyshop.fields.state_tax_id")} name="state_tax_id">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
{HasFeatureAccess({ featureName: "bills", bodyshop }) && (
|
||||
<>
|
||||
{InstanceRenderManager({
|
||||
imex: (
|
||||
<Form.Item
|
||||
label={t("bodyshop.labels.2tiersetup")}
|
||||
shouldUpdate
|
||||
label={t("bodyshop.fields.invoice_federal_tax_rate")}
|
||||
name={["bill_tax_rates", "federal_tax_rate"]}
|
||||
rules={[
|
||||
{
|
||||
required: form.getFieldValue(["accountingconfig", "tiers"]) === 2
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["accountingconfig", "twotierpref"]}
|
||||
>
|
||||
<Radio.Group disabled={form.getFieldValue(["accountingconfig", "tiers"]) === 3}>
|
||||
<Radio value="name">{t("bodyshop.labels.2tiername")}</Radio>
|
||||
<Radio value="source">{t("bodyshop.labels.2tiersource")}</Radio>
|
||||
</Radio.Group>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.labels.printlater")}
|
||||
valuePropName="checked"
|
||||
name={["accountingconfig", "printlater"]}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.labels.emaillater")}
|
||||
valuePropName="checked"
|
||||
name={["accountingconfig", "emaillater"]}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.inhousevendorid")}
|
||||
name={"inhousevendorid"}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.default_adjustment_rate")}
|
||||
name={"default_adjustment_rate"}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} precision={2} />
|
||||
</Form.Item>
|
||||
{InstanceRenderManager({
|
||||
imex: (
|
||||
<Form.Item label={t("bodyshop.fields.federal_tax_id")} name="federal_tax_id">
|
||||
)
|
||||
})}
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.invoice_state_tax_rate")}
|
||||
name={["bill_tax_rates", "state_tax_rate"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.invoice_local_tax_rate")}
|
||||
name={["bill_tax_rates", "local_tax_rate"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
<Form.Item
|
||||
name={["md_payment_types"]}
|
||||
label={t("bodyshop.fields.md_payment_types")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
type: "array"
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select mode="tags" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["md_categories"]}
|
||||
label={t("bodyshop.fields.md_categories")}
|
||||
rules={[
|
||||
{
|
||||
//message: t("general.validation.required"),
|
||||
type: "array"
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select mode="tags" />
|
||||
</Form.Item>
|
||||
{HasFeatureAccess({ featureName: "export", bodyshop }) && (
|
||||
<>
|
||||
<Form.Item
|
||||
name={["accountingconfig", "ReceivableCustomField1"]}
|
||||
label={t("bodyshop.fields.ReceivableCustomField", { number: 1 })}
|
||||
>
|
||||
{ReceivableCustomFieldSelect}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["accountingconfig", "ReceivableCustomField2"]}
|
||||
label={t("bodyshop.fields.ReceivableCustomField", { number: 2 })}
|
||||
>
|
||||
{ReceivableCustomFieldSelect}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["accountingconfig", "ReceivableCustomField3"]}
|
||||
label={t("bodyshop.fields.ReceivableCustomField", { number: 3 })}
|
||||
>
|
||||
{ReceivableCustomFieldSelect}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["md_classes"]}
|
||||
label={t("bodyshop.fields.md_classes")}
|
||||
rules={[
|
||||
({ getFieldValue }) => {
|
||||
return {
|
||||
required: getFieldValue("enforce_class"),
|
||||
//message: t("general.validation.required"),
|
||||
type: "array"
|
||||
};
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select mode="tags" />
|
||||
</Form.Item>
|
||||
<Form.Item name={["enforce_class"]} label={t("bodyshop.fields.enforce_class")} valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
{ClosingPeriod.treatment === "on" && (
|
||||
<Form.Item
|
||||
name={["accountingconfig", "ClosingPeriod"]}
|
||||
label={t("bodyshop.fields.closingperiod")} //{t("reportcenter.labels.dates")}
|
||||
>
|
||||
<DatePicker.RangePicker format="MM/DD/YYYY" presets={DatePickerRanges} />
|
||||
</Form.Item>
|
||||
)}
|
||||
{ADPPayroll.treatment === "on" && (
|
||||
<Form.Item name={["accountingconfig", "companyCode"]} label={t("bodyshop.fields.companycode")}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
)
|
||||
})}
|
||||
<Form.Item label={t("bodyshop.fields.state_tax_id")} name="state_tax_id">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
{InstanceRenderManager({
|
||||
imex: (
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.invoice_federal_tax_rate")}
|
||||
name={["bill_tax_rates", "federal_tax_rate"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber />
|
||||
)}
|
||||
{ADPPayroll.treatment === "on" && (
|
||||
<Form.Item name={["accountingconfig", "batchID"]} label={t("bodyshop.fields.batchid")}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
)
|
||||
})}
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.invoice_state_tax_rate")}
|
||||
name={["bill_tax_rates", "state_tax_rate"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.invoice_local_tax_rate")}
|
||||
name={["bill_tax_rates", "local_tax_rate"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["md_payment_types"]}
|
||||
label={t("bodyshop.fields.md_payment_types")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
type: "array"
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select mode="tags" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["md_categories"]}
|
||||
label={t("bodyshop.fields.md_categories")}
|
||||
rules={[
|
||||
{
|
||||
//message: t("general.validation.required"),
|
||||
type: "array"
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select mode="tags" />
|
||||
</Form.Item>
|
||||
<Form.Item name={["enforce_class"]} label={t("bodyshop.fields.enforce_class")} valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["accountingconfig", "ReceivableCustomField1"]}
|
||||
label={t("bodyshop.fields.ReceivableCustomField", { number: 1 })}
|
||||
>
|
||||
{ReceivableCustomFieldSelect}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["accountingconfig", "ReceivableCustomField2"]}
|
||||
label={t("bodyshop.fields.ReceivableCustomField", { number: 2 })}
|
||||
>
|
||||
{ReceivableCustomFieldSelect}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["accountingconfig", "ReceivableCustomField3"]}
|
||||
label={t("bodyshop.fields.ReceivableCustomField", { number: 3 })}
|
||||
>
|
||||
{ReceivableCustomFieldSelect}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["md_classes"]}
|
||||
label={t("bodyshop.fields.md_classes")}
|
||||
rules={[
|
||||
({ getFieldValue }) => {
|
||||
return {
|
||||
required: getFieldValue("enforce_class"),
|
||||
//message: t("general.validation.required"),
|
||||
type: "array"
|
||||
};
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select mode="tags" />
|
||||
</Form.Item>
|
||||
{ClosingPeriod.treatment === "on" && (
|
||||
<Form.Item
|
||||
name={["accountingconfig", "ClosingPeriod"]}
|
||||
label={t("bodyshop.fields.closingperiod")} //{t("reportcenter.labels.dates")}
|
||||
>
|
||||
<DatePicker.RangePicker format="MM/DD/YYYY" presets={DatePickerRanges} />
|
||||
</Form.Item>
|
||||
)}
|
||||
{ADPPayroll.treatment === "on" && (
|
||||
<Form.Item name={["accountingconfig", "companyCode"]} label={t("bodyshop.fields.companycode")}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
)}
|
||||
{ADPPayroll.treatment === "on" && (
|
||||
<Form.Item name={["accountingconfig", "batchID"]} label={t("bodyshop.fields.batchid")}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
)}
|
||||
</LayoutFormRow>
|
||||
</FeatureWrapper>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</LayoutFormRow>
|
||||
<FeatureWrapper featureName="scoreboard" noauth={() => null}>
|
||||
<LayoutFormRow header={t("bodyshop.labels.scoreboardsetup")} id="scoreboardsetup">
|
||||
<Form.Item
|
||||
@@ -822,7 +833,11 @@ export function ShopInfoGeneral({ form, bodyshop }) {
|
||||
}}
|
||||
</Form.List>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow grow header=<span id="insurancecos-header">{t("bodyshop.labels.insurancecos")}</span> id="insurancecos">
|
||||
<LayoutFormRow
|
||||
grow
|
||||
header=<span id="insurancecos-header">{t("bodyshop.labels.insurancecos")}</span>
|
||||
id="insurancecos"
|
||||
>
|
||||
<Form.List name={["md_ins_cos"]}>
|
||||
{(fields, { add, remove, move }) => {
|
||||
return (
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,7 @@ import { ColorPicker } from "./shop-info.rostatus.component";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
const mapDispatchToProps = () => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
@@ -22,7 +22,7 @@ export function ShopInfoSchedulingComponent({ form, bodyshop }) {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<LayoutFormRow>
|
||||
<LayoutFormRow id="shopinfo-scheduling">
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.appt_length")}
|
||||
name={"appt_length"}
|
||||
@@ -44,6 +44,7 @@ export function ShopInfoSchedulingComponent({ form, bodyshop }) {
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
id="schedule_start_time"
|
||||
>
|
||||
<TimePicker disableSeconds={true} format="HH:mm" />
|
||||
</Form.Item>
|
||||
@@ -56,6 +57,7 @@ export function ShopInfoSchedulingComponent({ form, bodyshop }) {
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
id="schedule_end_time"
|
||||
>
|
||||
<TimePicker disableSeconds={true} format="HH:mm" />
|
||||
</Form.Item>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Icon, { FieldTimeOutlined } from "@ant-design/icons";
|
||||
import { Card, Tabs } from "antd";
|
||||
import queryString from "query-string";
|
||||
import React, { useEffect } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FaShieldAlt } from "react-icons/fa";
|
||||
import { connect } from "react-redux";
|
||||
@@ -78,7 +78,7 @@ export function ScoreboardContainer({ setBreadcrumbs, setSelectedHeader }) {
|
||||
<RbacWrapper action="scoreboard:view">
|
||||
<Tabs
|
||||
activeKey={tab || "sb"}
|
||||
destroyInactiveTabPane
|
||||
destroyOnHidden
|
||||
onChange={(key) => {
|
||||
searchParams.tab = key;
|
||||
history({
|
||||
|
||||
12
package-lock.json
generated
12
package-lock.json
generated
@@ -63,7 +63,8 @@
|
||||
"winston": "^3.17.0",
|
||||
"winston-cloudwatch": "^6.3.0",
|
||||
"xml2js": "^0.6.2",
|
||||
"xmlbuilder2": "^3.1.1"
|
||||
"xmlbuilder2": "^3.1.1",
|
||||
"yazl": "^3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.28.0",
|
||||
@@ -13057,6 +13058,15 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yazl": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/yazl/-/yazl-3.3.1.tgz",
|
||||
"integrity": "sha512-BbETDVWG+VcMUle37k5Fqp//7SDOK2/1+T7X8TD96M3D9G8jK5VLUdQVdVjGi8im7FGkazX7kk5hkU8X4L5Bng==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-crc32": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/yocto-queue": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||
|
||||
@@ -70,7 +70,8 @@
|
||||
"winston": "^3.17.0",
|
||||
"winston-cloudwatch": "^6.3.0",
|
||||
"xml2js": "^0.6.2",
|
||||
"xmlbuilder2": "^3.1.1"
|
||||
"xmlbuilder2": "^3.1.1",
|
||||
"yazl": "^3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.28.0",
|
||||
|
||||
@@ -381,7 +381,7 @@ async function CalculateRatesTotals({ job, client }) {
|
||||
|
||||
if (item.mod_lbr_ty) {
|
||||
//Check to see if it has 0 hours and a price instead.
|
||||
if (item.mod_lb_hrs === 0 && item.act_price > 0 && item.lbr_op === "OP14") {
|
||||
if (item.lbr_op === "OP14" && item.act_price > 0 && (!item.part_type || item.mod_lb_hrs === 0)) {
|
||||
//Scenario where SGI may pay out hours using a part price.
|
||||
if (!ret[item.mod_lbr_ty.toLowerCase()].total) {
|
||||
ret[item.mod_lbr_ty.toLowerCase()].total = Dinero();
|
||||
|
||||
@@ -314,7 +314,8 @@ function CalculateRatesTotals(ratesList) {
|
||||
|
||||
if (item.mod_lbr_ty) {
|
||||
//Check to see if it has 0 hours and a price instead.
|
||||
if (item.mod_lb_hrs === 0 && item.act_price > 0 && item.lbr_op === "OP14") {
|
||||
//Extend for when there are hours and a price.
|
||||
if (item.lbr_op === "OP14" && item.act_price > 0 && (!item.part_type || item.mod_lb_hrs === 0)) {
|
||||
//Scenario where SGI may pay out hours using a part price.
|
||||
if (!ret[item.mod_lbr_ty.toLowerCase()].total) {
|
||||
ret[item.mod_lbr_ty.toLowerCase()].total = Dinero();
|
||||
|
||||
@@ -20,6 +20,7 @@ const {
|
||||
GET_DOCUMENTS_BY_IDS,
|
||||
DELETE_MEDIA_DOCUMENTS
|
||||
} = require("../graphql-client/queries");
|
||||
const yazl = require("yazl");
|
||||
|
||||
const imgproxyBaseUrl = process.env.IMGPROXY_BASE_URL; // `https://u4gzpp5wm437dnm75qa42tvza40fguqr.lambda-url.ca-central-1.on.aws` //Direct Lambda function access to bypass CDN.
|
||||
const imgproxySalt = process.env.IMGPROXY_SALT;
|
||||
@@ -102,13 +103,7 @@ const getThumbnailUrls = async (req, res) => {
|
||||
//<Cloudfront_to_lambda>/<hmac with SHA of entire request URI path (with base64 encoded URL if needed), beginning with un-encoded/un-hashed Salt>/<remainder of url - resize params >/< base 64 URL encoded to image path>
|
||||
//When working with documents from Cloudinary, the URL does not include the extension.
|
||||
|
||||
let key;
|
||||
|
||||
if (/\.[^/.]+$/.test(document.key)) {
|
||||
key = document.key;
|
||||
} else {
|
||||
key = `${document.key}.${document.extension.toLowerCase()}`;
|
||||
}
|
||||
let key = keyStandardize(document)
|
||||
// Build the S3 path to the object.
|
||||
const fullS3Path = `s3://${imgproxyDestinationBucket}/${key}`;
|
||||
const base64UrlEncodedKeyString = base64UrlEncode(fullS3Path);
|
||||
@@ -168,78 +163,73 @@ const getThumbnailUrls = async (req, res) => {
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
const downloadFiles = async (req, res) => {
|
||||
//Given a series of document IDs or keys, generate a file (or a link) to download all images in bulk
|
||||
const { jobId, billid, documentids } = req.body;
|
||||
|
||||
logger.log("imgproxy-download", "DEBUG", req.user?.email, jobId, { billid, jobId, documentids });
|
||||
|
||||
const client = req.userGraphQLClient;
|
||||
let data;
|
||||
try {
|
||||
logger.log("imgproxy-download", "DEBUG", req.user?.email, jobId, { billid, jobId, documentids });
|
||||
|
||||
//Delayed as the key structure may change slightly from what it is currently and will require evaluating mobile components.
|
||||
const client = req.userGraphQLClient;
|
||||
|
||||
//Query for the keys of the document IDs
|
||||
const data = await client.request(GET_DOCUMENTS_BY_IDS, { documentIds: documentids });
|
||||
|
||||
//Using the Keys, get all the S3 links, zip them, and send back to the client.
|
||||
const s3client = new S3Client({ region: InstanceRegion() });
|
||||
const archiveStream = archiver("zip");
|
||||
|
||||
archiveStream.on("error", (error) => {
|
||||
console.error("Archival encountered an error:", error);
|
||||
throw new Error(error);
|
||||
});
|
||||
|
||||
const passThrough = new stream.PassThrough();
|
||||
|
||||
archiveStream.pipe(passThrough);
|
||||
|
||||
for (const key of data.documents.map((d) => d.key)) {
|
||||
const response = await s3client.send(
|
||||
new GetObjectCommand({
|
||||
Bucket: imgproxyDestinationBucket,
|
||||
Key: key
|
||||
})
|
||||
);
|
||||
|
||||
archiveStream.append(response.Body, { name: path.basename(key) });
|
||||
}
|
||||
|
||||
await archiveStream.finalize();
|
||||
|
||||
const archiveKey = `archives/${jobId || "na"}/archive-${new Date().toISOString()}.zip`;
|
||||
|
||||
const parallelUploads3 = new Upload({
|
||||
client: s3client,
|
||||
queueSize: 4, // optional concurrency configuration
|
||||
leavePartsOnError: false, // optional manually handle dropped parts
|
||||
params: { Bucket: imgproxyDestinationBucket, Key: archiveKey, Body: passThrough }
|
||||
});
|
||||
|
||||
// Disabled progress logging for upload, uncomment if needed
|
||||
// parallelUploads3.on("httpUploadProgress", (progress) => {
|
||||
// console.log(progress);
|
||||
// });
|
||||
|
||||
await parallelUploads3.done();
|
||||
|
||||
//Generate the presigned URL to download it.
|
||||
const presignedUrl = await getSignedUrl(
|
||||
s3client,
|
||||
new GetObjectCommand({ Bucket: imgproxyDestinationBucket, Key: archiveKey }),
|
||||
{ expiresIn: 360 }
|
||||
);
|
||||
|
||||
return res.json({ success: true, url: presignedUrl });
|
||||
//Iterate over them, build the link based on the media type, and return the array.
|
||||
data = await client.request(GET_DOCUMENTS_BY_IDS, { documentIds: documentids });
|
||||
} catch (error) {
|
||||
logger.log("imgproxy-thumbnails-error", "ERROR", req.user?.email, jobId, {
|
||||
logger.log("imgproxy-download-error", "ERROR", req.user?.email, jobId, {
|
||||
jobId,
|
||||
billid,
|
||||
message: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
return res.status(400).json({ message: error.message });
|
||||
}
|
||||
|
||||
return res.status(400).json({ message: error.message, stack: error.stack });
|
||||
const s3client = new S3Client({ region: InstanceRegion() });
|
||||
const zipfile = new yazl.ZipFile();
|
||||
|
||||
const filename = `archive-${jobId || "na"}-${new Date().toISOString().replace(/[:.]/g, "-")}.zip`;
|
||||
res.setHeader("Content-Type", "application/zip");
|
||||
res.setHeader("Content-Disposition", `attachment; filename="${filename}"`);
|
||||
|
||||
// Handle zipfile stream errors
|
||||
zipfile.outputStream.on("error", (err) => {
|
||||
logger.log("imgproxy-download-zipstream-error", "ERROR", req.user?.email, jobId, { message: err.message, stack: err.stack });
|
||||
// Cannot send another response here, just destroy the connection
|
||||
res.destroy(err);
|
||||
});
|
||||
|
||||
zipfile.outputStream.pipe(res);
|
||||
|
||||
try {
|
||||
for (const doc of data.documents) {
|
||||
let key = keyStandardize(doc)
|
||||
let response;
|
||||
try {
|
||||
response = await s3client.send(
|
||||
new GetObjectCommand({
|
||||
Bucket: imgproxyDestinationBucket,
|
||||
Key: key
|
||||
})
|
||||
);
|
||||
} catch (err) {
|
||||
logger.log("imgproxy-download-s3-error", "ERROR", req.user?.email, jobId, { key, message: err.message, stack: err.stack });
|
||||
// Optionally, skip this file or add a placeholder file in the zip
|
||||
continue;
|
||||
}
|
||||
// Attach error handler to S3 stream
|
||||
response.Body.on("error", (err) => {
|
||||
logger.log("imgproxy-download-s3stream-error", "ERROR", req.user?.email, jobId, { key, message: err.message, stack: err.stack });
|
||||
res.destroy(err);
|
||||
});
|
||||
zipfile.addReadStream(response.Body, path.basename(key));
|
||||
}
|
||||
zipfile.end();
|
||||
} catch (error) {
|
||||
logger.log("imgproxy-download-error", "ERROR", req.user?.email, jobId, {
|
||||
jobId,
|
||||
billid,
|
||||
message: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
// Cannot send another response here, just destroy the connection
|
||||
res.destroy(error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -392,6 +382,15 @@ const moveFiles = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
const keyStandardize = (doc) => {
|
||||
if (/\.[^/.]+$/.test(doc.key)) {
|
||||
return doc.key;
|
||||
} else {
|
||||
return `${doc.key}.${doc.extension.toLowerCase()}`;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
module.exports = {
|
||||
generateSignedUploadUrls,
|
||||
getThumbnailUrls,
|
||||
|
||||
Reference in New Issue
Block a user