Compare commits
2 Commits
feature/IO
...
feature/IO
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28abd9707e | ||
|
|
5f621e1ae0 |
@@ -1,6 +1,6 @@
|
||||
import { DatePicker, Space, TimePicker } from "antd";
|
||||
import PropTypes from "prop-types";
|
||||
import { useCallback, useState } from "react";
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
@@ -94,24 +94,7 @@ const DateTimePicker = ({
|
||||
showTime={false}
|
||||
format="MM/DD/YYYY"
|
||||
value={value ? dayjs(value) : null}
|
||||
onChange={(dateValue) => {
|
||||
if (dateValue) {
|
||||
// When date changes, preserve the existing time if it exists
|
||||
if (value && dayjs(value).isValid()) {
|
||||
const existingTime = dayjs(value);
|
||||
const newDateTime = dayjs(dateValue)
|
||||
.hour(existingTime.hour())
|
||||
.minute(existingTime.minute())
|
||||
.second(existingTime.second());
|
||||
handleChange(newDateTime);
|
||||
} else {
|
||||
// If no existing time, just set the date without time
|
||||
handleChange(dateValue);
|
||||
}
|
||||
} else {
|
||||
handleChange(dateValue);
|
||||
}
|
||||
}}
|
||||
onChange={handleChange}
|
||||
placeholder={t("general.labels.date")}
|
||||
onBlur={handleBlur}
|
||||
disabledDate={handleDisabledDate}
|
||||
@@ -122,25 +105,13 @@ const DateTimePicker = ({
|
||||
<TimePicker
|
||||
format="hh:mm a"
|
||||
minuteStep={15}
|
||||
value={value && dayjs(value).hour() === 0 && dayjs(value).minute() === 0 ? null : dayjs(value)}
|
||||
defaultOpenValue={dayjs(value)
|
||||
.hour(dayjs().hour())
|
||||
.minute(Math.floor(dayjs().minute() / 15) * 15)
|
||||
.second(0)}
|
||||
onChange={(timeValue) => {
|
||||
if (timeValue) {
|
||||
// When time changes, combine it with the existing date
|
||||
const existingDate = dayjs(value);
|
||||
const newDateTime = existingDate
|
||||
.hour(timeValue.hour())
|
||||
.minute(timeValue.minute())
|
||||
.second(0);
|
||||
handleChange(newDateTime);
|
||||
} else {
|
||||
// If time is cleared, just update with null time but keep date
|
||||
handleChange(timeValue);
|
||||
}
|
||||
if (onBlur) onBlur();
|
||||
onChange={(value) => {
|
||||
handleChange(value);
|
||||
onBlur();
|
||||
}}
|
||||
placeholder={t("general.labels.time")}
|
||||
{...restProps}
|
||||
|
||||
@@ -385,9 +385,7 @@ export function ScheduleEventComponent({
|
||||
previousEvent: event.id,
|
||||
color: event.color,
|
||||
alt_transport: event.job && event.job.alt_transport,
|
||||
note: event.note,
|
||||
scheduled_in: event.job && event.job.scheduled_in,
|
||||
scheduled_completion: event.job && event.job.scheduled_completion
|
||||
note: event.note
|
||||
}
|
||||
});
|
||||
}}
|
||||
|
||||
@@ -673,9 +673,7 @@ export function JobsDetailHeaderActions({
|
||||
context: {
|
||||
jobId: job.id,
|
||||
job: job,
|
||||
alt_transport: job.alt_transport,
|
||||
scheduled_in: job.scheduled_in,
|
||||
scheduled_completion: job.scheduled_completion
|
||||
alt_transport: job.alt_transport
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1092,7 +1090,11 @@ export function JobsDetailHeaderActions({
|
||||
{t("menus.jobsactions.deletejob")}
|
||||
</Popconfirm>
|
||||
) : (
|
||||
<Popconfirm title={t("jobs.labels.deletewatchers")} onClick={(e) => e.stopPropagation()} showCancel={false}>
|
||||
<Popconfirm
|
||||
title={t("jobs.labels.deletewatchers")}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
showCancel={false}
|
||||
>
|
||||
{t("menus.jobsactions.deletejob")}
|
||||
</Popconfirm>
|
||||
)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useMutation, useQuery } from "@apollo/client";
|
||||
import { Form, Modal } from "antd";
|
||||
import { useEffect, useState } from "react";
|
||||
import dayjs from "../../utils/day";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import {
|
||||
CANCEL_APPOINTMENT_BY_ID,
|
||||
@@ -19,9 +19,9 @@ import { selectSchedule } from "../../redux/modals/modals.selectors";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import { DateTimeFormat } from "../../utils/DateFormatter";
|
||||
import dayjs from "../../utils/day";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import ScheduleJobModalComponent from "./schedule-job-modal.component";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -72,7 +72,7 @@ export function ScheduleJobModalContainer({
|
||||
variables: { jobid: jobId },
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only",
|
||||
skip: !open || !jobId
|
||||
skip: !open || !!!jobId
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -93,12 +93,12 @@ export function ScheduleJobModalContainer({
|
||||
logImEXEvent("schedule_new_appointment");
|
||||
|
||||
setLoading(true);
|
||||
if (previousEvent) {
|
||||
if (!!previousEvent) {
|
||||
const cancelAppt = await cancelAppointment({
|
||||
variables: { appid: previousEvent }
|
||||
});
|
||||
|
||||
if (cancelAppt.errors) {
|
||||
if (!!cancelAppt.errors) {
|
||||
notification["error"]({
|
||||
message: t("appointments.errors.canceling", {
|
||||
message: JSON.stringify(cancelAppt.errors)
|
||||
@@ -146,7 +146,7 @@ export function ScheduleJobModalContainer({
|
||||
});
|
||||
}
|
||||
|
||||
if (appt.errors) {
|
||||
if (!!appt.errors) {
|
||||
notification["error"]({
|
||||
message: t("appointments.errors.saving", {
|
||||
message: JSON.stringify(appt.errors)
|
||||
@@ -172,7 +172,7 @@ export function ScheduleJobModalContainer({
|
||||
}
|
||||
});
|
||||
|
||||
if (jobUpdate.errors) {
|
||||
if (!!jobUpdate.errors) {
|
||||
notification["error"]({
|
||||
message: t("appointments.errors.saving", {
|
||||
message: JSON.stringify(jobUpdate.errors)
|
||||
@@ -222,9 +222,9 @@ export function ScheduleJobModalContainer({
|
||||
initialValues={{
|
||||
notifyCustomer: !!(job && job.ownr_ea),
|
||||
email: (job && job.ownr_ea) || "",
|
||||
start: null,
|
||||
// smartDates: [],
|
||||
start: context.scheduled_in,
|
||||
scheduled_completion: context.scheduled_completion ,
|
||||
scheduled_completion: null,
|
||||
color: context.color,
|
||||
alt_transport: context.alt_transport,
|
||||
note: context.note
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import { SocketContext, INITIAL_NOTIFICATIONS } from "./useSocket.js";
|
||||
import { INITIAL_NOTIFICATIONS, SocketContext } from "./useSocket.js";
|
||||
|
||||
/**
|
||||
* Socket Provider - Scenario Notifications / Web Socket related items
|
||||
@@ -157,7 +157,10 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser }) => {
|
||||
auth: { token, bodyshopId: bodyshop.id },
|
||||
reconnectionAttempts: Infinity,
|
||||
reconnectionDelay: 2000,
|
||||
reconnectionDelayMax: 10000
|
||||
reconnectionDelayMax: 60000,
|
||||
randomizationFactor: 0.5,
|
||||
transports: ["websocket", "polling"], // Add this to prefer WebSocket with polling fallback
|
||||
rememberUpgrade: true
|
||||
});
|
||||
|
||||
socketRef.current = socketInstance;
|
||||
|
||||
@@ -31,8 +31,6 @@ export const QUERY_ALL_ACTIVE_APPOINTMENTS = gql`
|
||||
color
|
||||
note
|
||||
job {
|
||||
scheduled_in
|
||||
scheduled_completion
|
||||
alt_transport
|
||||
ro_number
|
||||
ownr_ln
|
||||
|
||||
@@ -3505,7 +3505,7 @@
|
||||
"dashboard": "Dashboard",
|
||||
"dms": "DMS Export",
|
||||
"export-logs": "Export Logs",
|
||||
"feature-request": "Feature Request",
|
||||
"feature-request": "Feature Requet",
|
||||
"inventory": "Inventory",
|
||||
"jobs": "Jobs",
|
||||
"jobs-active": "Active Jobs",
|
||||
|
||||
@@ -19,7 +19,7 @@ async function JobCosting(req, res) {
|
||||
const client = req.userGraphQLClient;
|
||||
|
||||
//Uncomment for further testing
|
||||
// logger.log("job-costing-start", "DEBUG", req.user.email, jobid, null);
|
||||
logger.log("job-costing-start", "DEBUG", req.user.email, jobid, null);
|
||||
|
||||
try {
|
||||
const resp = await client.setHeaders({ Authorization: BearerToken }).request(queries.QUERY_JOB_COSTING_DETAILS, {
|
||||
@@ -47,9 +47,9 @@ async function JobCostingMulti(req, res) {
|
||||
const client = req.userGraphQLClient;
|
||||
|
||||
//Uncomment for further testing
|
||||
// logger.log("job-costing-multi-start", "DEBUG", req?.user?.email, null, {
|
||||
// jobids
|
||||
// });
|
||||
logger.log("job-costing-multi-start", "DEBUG", req?.user?.email, null, {
|
||||
jobids
|
||||
});
|
||||
|
||||
try {
|
||||
const resp = await client
|
||||
@@ -589,7 +589,7 @@ function GenerateCostingData(job) {
|
||||
amount: Math.round((job.storage_payable || 0) * 100)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
//Is it a DMS Setup?
|
||||
const selectedDmsAllocationConfig =
|
||||
(job.bodyshop.md_responsibility_centers.dms_defaults &&
|
||||
|
||||
@@ -8,6 +8,7 @@ const getLifecycleStatusColor = require("../utils/getLifecycleStatusColor");
|
||||
const jobLifecycle = async (req, res) => {
|
||||
// Grab the jobids and statuses from the request body
|
||||
const { jobids, statuses } = req.body;
|
||||
const { logger } = req;
|
||||
|
||||
if (!jobids) {
|
||||
return res.status(400).json({
|
||||
@@ -16,102 +17,118 @@ const jobLifecycle = async (req, res) => {
|
||||
}
|
||||
|
||||
const jobIDs = _.isArray(jobids) ? jobids : [jobids];
|
||||
const client = req.userGraphQLClient;
|
||||
const resp = await client.request(queries.QUERY_TRANSITIONS_BY_JOBID, { jobids: jobIDs });
|
||||
|
||||
const transitions = resp.transitions;
|
||||
logger.log("job-lifecycle-start", "DEBUG", req?.user?.email, null, {
|
||||
jobids: jobIDs
|
||||
});
|
||||
|
||||
try {
|
||||
const client = req.userGraphQLClient;
|
||||
const resp = await client.request(queries.QUERY_TRANSITIONS_BY_JOBID, { jobids: jobIDs });
|
||||
|
||||
const transitions = resp.transitions;
|
||||
|
||||
if (!transitions) {
|
||||
return res.status(200).json({
|
||||
jobIDs,
|
||||
transitions: []
|
||||
});
|
||||
}
|
||||
|
||||
const transitionsByJobId = _.groupBy(resp.transitions, "jobid");
|
||||
|
||||
const groupedTransitions = {};
|
||||
const allDurations = [];
|
||||
|
||||
for (let jobId in transitionsByJobId) {
|
||||
let lifecycle = transitionsByJobId[jobId].map((transition) => {
|
||||
transition.start_readable = transition.start ? moment(transition.start).fromNow() : "N/A";
|
||||
transition.end_readable = transition.end ? moment(transition.end).fromNow() : "N/A";
|
||||
|
||||
if (transition.duration) {
|
||||
transition.duration_seconds = Math.round(transition.duration / 1000);
|
||||
transition.duration_minutes = Math.round(transition.duration_seconds / 60);
|
||||
let duration = moment.duration(transition.duration);
|
||||
transition.duration_readable = durationToHumanReadable(duration);
|
||||
} else {
|
||||
transition.duration_seconds = 0;
|
||||
transition.duration_minutes = 0;
|
||||
transition.duration_readable = "N/A";
|
||||
}
|
||||
return transition;
|
||||
});
|
||||
|
||||
const durations = calculateStatusDuration(lifecycle, statuses);
|
||||
|
||||
groupedTransitions[jobId] = {
|
||||
lifecycle,
|
||||
durations
|
||||
};
|
||||
|
||||
if (durations?.summations) {
|
||||
allDurations.push(durations.summations);
|
||||
}
|
||||
}
|
||||
|
||||
const finalSummations = [];
|
||||
const flatGroupedAllDurations = _.groupBy(allDurations.flat(), "status");
|
||||
|
||||
const finalStatusCounts = Object.keys(flatGroupedAllDurations).reduce((acc, status) => {
|
||||
acc[status] = flatGroupedAllDurations[status].length;
|
||||
return acc;
|
||||
}, {});
|
||||
// Calculate total value of all statuses
|
||||
const finalTotal = Object.values(flatGroupedAllDurations).reduce((total, statusArr) => {
|
||||
return total + statusArr.reduce((acc, curr) => acc + curr.value, 0);
|
||||
}, 0);
|
||||
|
||||
Object.keys(flatGroupedAllDurations).forEach((status) => {
|
||||
const value = flatGroupedAllDurations[status].reduce((acc, curr) => acc + curr.value, 0);
|
||||
const humanReadable = durationToHumanReadable(moment.duration(value));
|
||||
const percentage = finalTotal > 0 ? (value / finalTotal) * 100 : 0;
|
||||
const color = getLifecycleStatusColor(status);
|
||||
const roundedPercentage = `${Math.round(percentage)}%`;
|
||||
const averageValue = _.size(jobIDs) > 0 ? value / jobIDs.length : 0;
|
||||
const averageHumanReadable = durationToHumanReadable(moment.duration(averageValue));
|
||||
finalSummations.push({
|
||||
status,
|
||||
value,
|
||||
humanReadable,
|
||||
percentage,
|
||||
color,
|
||||
roundedPercentage,
|
||||
averageValue,
|
||||
averageHumanReadable
|
||||
});
|
||||
});
|
||||
|
||||
if (!transitions) {
|
||||
return res.status(200).json({
|
||||
jobIDs,
|
||||
transitions: []
|
||||
});
|
||||
}
|
||||
|
||||
const transitionsByJobId = _.groupBy(resp.transitions, "jobid");
|
||||
|
||||
const groupedTransitions = {};
|
||||
const allDurations = [];
|
||||
|
||||
for (let jobId in transitionsByJobId) {
|
||||
let lifecycle = transitionsByJobId[jobId].map((transition) => {
|
||||
transition.start_readable = transition.start ? moment(transition.start).fromNow() : "N/A";
|
||||
transition.end_readable = transition.end ? moment(transition.end).fromNow() : "N/A";
|
||||
|
||||
if (transition.duration) {
|
||||
transition.duration_seconds = Math.round(transition.duration / 1000);
|
||||
transition.duration_minutes = Math.round(transition.duration_seconds / 60);
|
||||
let duration = moment.duration(transition.duration);
|
||||
transition.duration_readable = durationToHumanReadable(duration);
|
||||
} else {
|
||||
transition.duration_seconds = 0;
|
||||
transition.duration_minutes = 0;
|
||||
transition.duration_readable = "N/A";
|
||||
transition: groupedTransitions,
|
||||
durations: {
|
||||
jobs: jobIDs.length,
|
||||
summations: finalSummations,
|
||||
totalStatuses: finalSummations.length,
|
||||
total: finalTotal,
|
||||
statusCounts: finalStatusCounts,
|
||||
humanReadable: durationToHumanReadable(moment.duration(finalTotal)),
|
||||
averageValue: _.size(jobIDs) > 0 ? finalTotal / jobIDs.length : 0,
|
||||
averageHumanReadable:
|
||||
_.size(jobIDs) > 0
|
||||
? durationToHumanReadable(moment.duration(finalTotal / jobIDs.length))
|
||||
: durationToHumanReadable(moment.duration(0))
|
||||
}
|
||||
return transition;
|
||||
});
|
||||
|
||||
const durations = calculateStatusDuration(lifecycle, statuses);
|
||||
|
||||
groupedTransitions[jobId] = {
|
||||
lifecycle,
|
||||
durations
|
||||
};
|
||||
|
||||
if (durations?.summations) {
|
||||
allDurations.push(durations.summations);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.log("job-lifecycle-error", "ERROR", req?.user?.email, null, {
|
||||
jobids: jobIDs,
|
||||
statuses: statuses ? JSON.stringify(statuses) : "N/A",
|
||||
error: error.message
|
||||
});
|
||||
return res.status(500).json({
|
||||
error: "Internal server error"
|
||||
});
|
||||
}
|
||||
|
||||
const finalSummations = [];
|
||||
const flatGroupedAllDurations = _.groupBy(allDurations.flat(), "status");
|
||||
|
||||
const finalStatusCounts = Object.keys(flatGroupedAllDurations).reduce((acc, status) => {
|
||||
acc[status] = flatGroupedAllDurations[status].length;
|
||||
return acc;
|
||||
}, {});
|
||||
// Calculate total value of all statuses
|
||||
const finalTotal = Object.values(flatGroupedAllDurations).reduce((total, statusArr) => {
|
||||
return total + statusArr.reduce((acc, curr) => acc + curr.value, 0);
|
||||
}, 0);
|
||||
|
||||
Object.keys(flatGroupedAllDurations).forEach((status) => {
|
||||
const value = flatGroupedAllDurations[status].reduce((acc, curr) => acc + curr.value, 0);
|
||||
const humanReadable = durationToHumanReadable(moment.duration(value));
|
||||
const percentage = finalTotal > 0 ? (value / finalTotal) * 100 : 0;
|
||||
const color = getLifecycleStatusColor(status);
|
||||
const roundedPercentage = `${Math.round(percentage)}%`;
|
||||
const averageValue = _.size(jobIDs) > 0 ? value / jobIDs.length : 0;
|
||||
const averageHumanReadable = durationToHumanReadable(moment.duration(averageValue));
|
||||
finalSummations.push({
|
||||
status,
|
||||
value,
|
||||
humanReadable,
|
||||
percentage,
|
||||
color,
|
||||
roundedPercentage,
|
||||
averageValue,
|
||||
averageHumanReadable
|
||||
});
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
jobIDs,
|
||||
transition: groupedTransitions,
|
||||
durations: {
|
||||
jobs: jobIDs.length,
|
||||
summations: finalSummations,
|
||||
totalStatuses: finalSummations.length,
|
||||
total: finalTotal,
|
||||
statusCounts: finalStatusCounts,
|
||||
humanReadable: durationToHumanReadable(moment.duration(finalTotal)),
|
||||
averageValue: _.size(jobIDs) > 0 ? finalTotal / jobIDs.length : 0,
|
||||
averageHumanReadable:
|
||||
_.size(jobIDs) > 0
|
||||
? durationToHumanReadable(moment.duration(finalTotal / jobIDs.length))
|
||||
: durationToHumanReadable(moment.duration(0))
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = jobLifecycle;
|
||||
|
||||
Reference in New Issue
Block a user