Compare commits

..

2 Commits

Author SHA1 Message Date
Dave Richer
28abd9707e feature/IO-3303-logging - Logging 2025-07-14 19:31:31 -04:00
Dave Richer
5f621e1ae0 feature/IO-3303-logging - Logging 2025-07-14 19:28:29 -04:00
9 changed files with 139 additions and 150 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -31,8 +31,6 @@ export const QUERY_ALL_ACTIVE_APPOINTMENTS = gql`
color
note
job {
scheduled_in
scheduled_completion
alt_transport
ro_number
ownr_ln

View File

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

View File

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

View File

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