Compare commits

...

34 Commits

Author SHA1 Message Date
Allan Carr
a182ea0869 IO-3315 CDK InService Date
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2025-07-29 10:04:13 -07:00
Patrick Fic
d92bab113e Merged in hotfix/2025-07-22-SocketProvider (pull request #2430)
Hotfix/2025 07 22 SocketProvider
2025-07-22 21:24:59 +00:00
Patrick Fic
93c6e2b601 Revert socket transport settings. 2025-07-22 14:23:25 -07:00
Dave Richer
19a90571f6 Down Socket Reconnects 2025-07-22 17:11:52 -04:00
Dave Richer
953e70efef Merged in release/2025-07-18 (pull request #2422)
Release 2025-07-18 into master-AIO - IO-1054, IO-3252, IO-3286, IO-3291, IO-3296, IO-3303, IO-3309
2025-07-19 00:57:33 +00:00
Dave Richer
a6bae390e5 feature/IO-3255-simplified-parts-management - Merge release / resolve conflicts 2025-07-18 14:55:00 -04:00
Allan Carr
cf9d8d649d Merged in feature/IO-3309-Date-Restriction-Removal (pull request #2419)
IO-3309 Date Field Rescriton Removal

Approved-by: Dave Richer
2025-07-17 23:36:53 +00:00
Allan Carr
a25051c4c2 IO-3309 Date Field Rescriton Removal
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2025-07-17 16:31:08 -07:00
Allan Carr
d5c3152631 Merged in feature/IO-3252-Reschedule-Job (pull request #2417)
IO-3252 Reschedule Job with Existing Data

Approved-by: Dave Richer
2025-07-16 21:08:03 +00:00
Allan Carr
66c425bf96 IO-3252 Fix Spelling Mistake in Feature Request
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2025-07-16 14:12:16 -07:00
Allan Carr
ffad0dfbf7 IO-3252 Reschedule Job with Existing Data
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2025-07-16 14:02:30 -07:00
Patrick Fic
17285fc029 Merged in hotfix/2025-07-15-nginxtune (pull request #2416)
Remove all testing due to failed test in RO.
2025-07-16 19:07:20 +00:00
Patrick Fic
401e3cff73 Remove all testing due to failed test in RO. 2025-07-16 12:05:40 -07:00
Patrick Fic
865680e019 Merged in hotfix/2025-07-15-nginxtune (pull request #2415)
Adjust body and buffer sizes.
2025-07-15 21:07:46 +00:00
Patrick Fic
9f97ca0336 Adjust body and buffer sizes. 2025-07-15 14:06:55 -07:00
Patrick Fic
5df38f8612 Merged in hotfix/2025-07-15-nginxtune (pull request #2414)
Override nginx.conf
2025-07-15 20:46:27 +00:00
Patrick Fic
63c5719420 Override nginx.conf 2025-07-15 13:45:37 -07:00
Patrick Fic
d6c80f1420 Merged in hotfix/2025-07-15-nginxtune (pull request #2413)
Additional change
2025-07-15 20:36:13 +00:00
Patrick Fic
fade927c9e Additional change 2025-07-15 13:35:17 -07:00
Patrick Fic
9f472ce1d0 Merged in hotfix/2025-07-15-nginxtune (pull request #2412)
Add worker process limits for EB config.
2025-07-15 20:21:38 +00:00
Patrick Fic
47a56e32b9 Add worker process limits for EB config. 2025-07-15 13:20:41 -07:00
Allan Carr
f13f79acb6 Merged in feature/IO-3286-Additional-Product-Fruit-IDs (pull request #2406)
IO-3286 Additional Product Fruit IDs

Approved-by: Dave Richer
2025-07-15 18:01:05 +00:00
Dave Richer
bfa9fddb9e Merged in feature/IO-3303-logging (pull request #2410)
[DO NOT MERGE] - IO-3303 Logging/Socket Params into Master-AIO

Approved-by: Patrick Fic
2025-07-14 23:51:09 +00:00
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
Dave Richer
624414799e Merge branch 'feature/IO-3303-Socket-IO-Optimization-Auto-Add-Watchers-Gate' into release/2025-07-18
# Conflicts:
#	client/src/contexts/SocketIO/socketProvider.jsx
2025-07-14 18:45:34 -04:00
Dave Richer
72091e9eae feature/IO-3303-Socket-IO-Optimization-Auto-Add-Watchers-Gate - SocketIO Optimization / Auto Add Watchers Gate 2025-07-14 18:42:27 -04:00
Dave Richer
9cfacdd025 Merged in feature/IO-3291-Tasks-Notifications (pull request #2407)
feature/IO-3291-Tasks-Notifications: Package Bumps
2025-07-14 17:34:30 +00:00
Allan Carr
655e516246 IO-3286 Additional Product Fruit IDs
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2025-07-11 11:07:12 -07:00
Dave Richer
7b12f0a3b9 Merged in feature/IO-3291-Tasks-Notifications (pull request #2404)
Feature/IO-3291 Tasks Notifications
2025-07-11 17:40:06 +00:00
Allan Carr
5c4267f3ef Merge branch 'master-AIO' into feature/IO-3286-Additional-Product-Fruit-IDs 2025-07-11 10:31:22 -07:00
Allan Carr
e79e512291 Merged in feature/IO-3296-Scheduled-Delivery-Dashboard (pull request #2402)
Feature/IO-3296 Scheduled Delivery Dashboard

Approved-by: Dave Richer
2025-07-09 22:30:29 +00:00
Allan Carr
f0064abfbe IO-3296 Schedule Delivery Dashboard
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2025-07-09 15:16:31 -07:00
Allan Carr
32bdea559e IO-3296 Schedule Delivery Dashboard
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2025-07-09 14:37:51 -07:00
22 changed files with 670 additions and 169 deletions

2
.gitignore vendored
View File

@@ -130,3 +130,5 @@ test-output.txt
server/job/test/fixtures server/job/test/fixtures
.github .github
_reference/ragmate/.ragmate.env
docker_data

View File

@@ -1,2 +1,2 @@
client_max_body_size 50M; client_max_body_size 50M;
client_body_buffer_size 5M; client_body_buffer_size 5M;

View File

@@ -0,0 +1,411 @@
import { BranchesOutlined, ExclamationCircleFilled, PauseCircleOutlined } from "@ant-design/icons";
import { Card, Space, Switch, Table, Tooltip, Typography } from "antd";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { TimeFormatter } from "../../../utils/DateFormatter";
import { onlyUnique } from "../../../utils/arrayHelper";
import dayjs from "../../../utils/day";
import { alphaSort, dateSort } from "../../../utils/sorters";
import useLocalStorage from "../../../utils/useLocalStorage";
import ChatOpenButton from "../../chat-open-button/chat-open-button.component";
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../../owner-name-display/owner-name-display.component";
import DashboardRefreshRequired from "../refresh-required.component";
export default function DashboardScheduledDeliveryToday({ data, ...cardProps }) {
const { t } = useTranslation();
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: {}
});
const [isTvModeScheduledDelivery, setIsTvModeScheduledDelivery] = useLocalStorage("isTvModeScheduledDelivery", false);
if (!data) return null;
if (!data.scheduled_delivery_today) return <DashboardRefreshRequired {...cardProps} />;
const scheduledDeliveryToday = data.scheduled_delivery_today.map((item) => {
const joblines_body = item.joblines
? item.joblines.filter((l) => l.mod_lbr_ty !== "LAR").reduce((acc, val) => acc + val.mod_lb_hrs, 0)
: 0;
const joblines_ref = item.joblines
? item.joblines.filter((l) => l.mod_lbr_ty === "LAR").reduce((acc, val) => acc + val.mod_lb_hrs, 0)
: 0;
return {
...item,
joblines_body,
joblines_ref
};
});
const tvFontSize = 18;
const tvFontWeight = "bold";
const tvColumns = [
{
title: t("jobs.fields.scheduled_delivery"),
dataIndex: "scheduled_delivery",
key: "scheduled_delivery",
ellipsis: true,
sorter: (a, b) => dateSort(a.scheduled_delivery, b.scheduled_delivery),
sortOrder: state.sortedInfo.columnKey === "scheduled_delivery" && state.sortedInfo.order,
render: (text, record) => (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
<TimeFormatter>{record.scheduled_delivery}</TimeFormatter>
</span>
)
},
{
title: t("jobs.fields.ro_number"),
dataIndex: "ro_number",
key: "ro_number",
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
sortOrder: state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
render: (text, record) => (
<Link to={"/manage/jobs/" + record.jobid} onClick={(e) => e.stopPropagation()}>
<Space>
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
{record.ro_number || t("general.labels.na")}
{record.production_vars && record.production_vars.alert ? (
<ExclamationCircleFilled className="production-alert" />
) : null}
{record.suspended && <PauseCircleOutlined style={{ color: "orangered" }} />}
{record.iouparent && (
<Tooltip title={t("jobs.labels.iou")}>
<BranchesOutlined style={{ color: "orangered" }} />
</Tooltip>
)}
</span>
</Space>
</Link>
)
},
{
title: t("jobs.fields.owner"),
dataIndex: "owner",
key: "owner",
ellipsis: true,
sorter: (a, b) => alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
sortOrder: state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => {
return record.ownerid ? (
<Link to={"/manage/owners/" + record.ownerid} onClick={(e) => e.stopPropagation()}>
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
<OwnerNameDisplay ownerObject={record} />
</span>
</Link>
) : (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
<OwnerNameDisplay ownerObject={record} />
</span>
);
}
},
{
title: t("jobs.fields.vehicle"),
dataIndex: "vehicle",
key: "vehicle",
ellipsis: true,
sorter: (a, b) =>
alphaSort(
`${a.v_model_yr || ""} ${a.v_make_desc || ""} ${a.v_model_desc || ""}`,
`${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
),
sortOrder: state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
render: (text, record) => {
return record.vehicleid ? (
<Link to={"/manage/vehicles/" + record.vehicleid} onClick={(e) => e.stopPropagation()}>
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${record.v_model_desc || ""}`}
</span>
</Link>
) : (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>{`${
record.v_model_yr || ""
} ${record.v_make_desc || ""} ${record.v_model_desc || ""}`}</span>
);
}
},
{
title: t("appointments.fields.alt_transport"),
dataIndex: "alt_transport",
key: "alt_transport",
ellipsis: true,
sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport),
sortOrder: state.sortedInfo.columnKey === "alt_transport" && state.sortedInfo.order,
filters:
(scheduledDeliveryToday &&
scheduledDeliveryToday
.map((j) => j.alt_transport)
.filter(onlyUnique)
.map((s) => {
return {
text: s || t("dashboard.errors.atp"),
value: [s]
};
})
.sort((a, b) => alphaSort(a.text, b.text))) ||
[],
onFilter: (value, record) => value.includes(record.alt_transport),
render: (text, record) => (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>{record.alt_transport}</span>
)
},
{
title: t("jobs.fields.status"),
dataIndex: "status",
key: "status",
ellipsis: true,
sorter: (a, b) => alphaSort(a.status, b.status),
sortOrder: state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
filters:
(scheduledDeliveryToday &&
scheduledDeliveryToday
.map((j) => j.status)
.filter(onlyUnique)
.map((s) => {
return {
text: s || t("dashboard.errors.status"),
value: [s]
};
})
.sort((a, b) => alphaSort(a.text, b.text))) ||
[],
onFilter: (value, record) => value.includes(record.status),
render: (text, record) => <span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>{record.status}</span>
},
{
title: t("jobs.fields.lab"),
dataIndex: "joblines_body",
key: "joblines_body",
sorter: (a, b) => a.joblines_body - b.joblines_body,
sortOrder: state.sortedInfo.columnKey === "joblines_body" && state.sortedInfo.order,
align: "right",
render: (text, record) => (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>{record.joblines_body.toFixed(1)}</span>
)
},
{
title: t("jobs.fields.lar"),
dataIndex: "joblines_ref",
key: "joblines_ref",
sorter: (a, b) => a.joblines_ref - b.joblines_ref,
sortOrder: state.sortedInfo.columnKey === "joblines_ref" && state.sortedInfo.order,
align: "right",
render: (text, record) => (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>{record.joblines_ref.toFixed(1)}</span>
)
}
];
const columns = [
{
title: t("jobs.fields.scheduled_delivery"),
dataIndex: "scheduled_delivery",
key: "scheduled_delivery",
ellipsis: true,
sorter: (a, b) => dateSort(a.scheduled_delivery, b.scheduled_delivery),
sortOrder: state.sortedInfo.columnKey === "scheduled_delivery" && state.sortedInfo.order,
render: (text, record) => <TimeFormatter>{record.scheduled_delivery}</TimeFormatter>
},
{
title: t("jobs.fields.ro_number"),
dataIndex: "ro_number",
key: "ro_number",
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
sortOrder: state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
render: (text, record) => (
<Link to={"/manage/jobs/" + record.jobid} onClick={(e) => e.stopPropagation()}>
<Space>
{record.ro_number || t("general.labels.na")}
{record.production_vars && record.production_vars.alert ? (
<ExclamationCircleFilled className="production-alert" />
) : null}
{record.suspended && <PauseCircleOutlined style={{ color: "orangered" }} />}
{record.iouparent && (
<Tooltip title={t("jobs.labels.iou")}>
<BranchesOutlined style={{ color: "orangered" }} />
</Tooltip>
)}
</Space>
</Link>
)
},
{
title: t("jobs.fields.owner"),
dataIndex: "owner",
key: "owner",
ellipsis: true,
sorter: (a, b) => alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
sortOrder: state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => {
return record.ownerid ? (
<Link to={"/manage/owners/" + record.ownerid} onClick={(e) => e.stopPropagation()}>
<OwnerNameDisplay ownerObject={record} />
</Link>
) : (
<span>
<OwnerNameDisplay ownerObject={record} />
</span>
);
}
},
{
title: t("dashboard.labels.phone"),
dataIndex: "ownr_ph",
key: "ownr_ph",
ellipsis: true,
responsive: ["md"],
render: (text, record) => (
<Space size="small" wrap>
<ChatOpenButton phone={record.ownr_ph1} jobid={record.jobid} />
<ChatOpenButton phone={record.ownr_ph2} jobid={record.jobid} />
</Space>
)
},
{
title: t("jobs.fields.ownr_ea"),
dataIndex: "ownr_ea",
key: "ownr_ea",
ellipsis: true,
responsive: ["md"],
render: (text, record) => <a href={`mailto:${record.ownr_ea}`}>{record.ownr_ea}</a>
},
{
title: t("jobs.fields.vehicle"),
dataIndex: "vehicle",
key: "vehicle",
ellipsis: true,
sorter: (a, b) =>
alphaSort(
`${a.v_model_yr || ""} ${a.v_make_desc || ""} ${a.v_model_desc || ""}`,
`${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
),
sortOrder: state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
render: (text, record) => {
return record.vehicleid ? (
<Link to={"/manage/vehicles/" + record.vehicleid} onClick={(e) => e.stopPropagation()}>
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${record.v_model_desc || ""}`}
</Link>
) : (
<span>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${record.v_model_desc || ""}`}</span>
);
}
},
{
title: t("jobs.fields.ins_co_nm"),
dataIndex: "ins_co_nm",
key: "ins_co_nm",
ellipsis: true,
responsive: ["md"],
sorter: (a, b) => alphaSort(a.ins_co_nm, b.ins_co_nm),
sortOrder: state.sortedInfo.columnKey === "ins_co_nm" && state.sortedInfo.order,
filters:
(scheduledDeliveryToday &&
scheduledDeliveryToday
.map((j) => j.ins_co_nm)
.filter(onlyUnique)
.map((s) => {
return {
text: s || t("dashboard.errors.insco"),
value: [s]
};
})
.sort((a, b) => alphaSort(a.text, b.text))) ||
[],
onFilter: (value, record) => value.includes(record.ins_co_nm)
},
{
title: t("appointments.fields.alt_transport"),
dataIndex: "alt_transport",
key: "alt_transport",
ellipsis: true,
sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport),
sortOrder: state.sortedInfo.columnKey === "alt_transport" && state.sortedInfo.order,
filters:
(scheduledDeliveryToday &&
scheduledDeliveryToday
.map((j) => j.alt_transport)
.filter(onlyUnique)
.map((s) => {
return {
text: s || t("dashboard.errors.atp"),
value: [s]
};
})
.sort((a, b) => alphaSort(a.text, b.text))) ||
[],
onFilter: (value, record) => value.includes(record.alt_transport)
}
];
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
return (
<Card
title={t("dashboard.titles.scheduleddeliverydate", {
date: dayjs().startOf("day").format("MM/DD/YYYY")
})}
extra={
<Space>
<Typography.Text>{t("general.labels.tvmode")}</Typography.Text>
<Switch
onClick={() => setIsTvModeScheduledDelivery(!isTvModeScheduledDelivery)}
defaultChecked={isTvModeScheduledDelivery}
/>
</Space>
}
{...cardProps}
>
<div style={{ height: "100%" }}>
<Table
onChange={handleTableChange}
pagination={false}
columns={isTvModeScheduledDelivery ? tvColumns : columns}
scroll={{ x: true, y: "calc(100% - 2em)" }}
rowKey="id"
style={{ height: "85%" }}
dataSource={scheduledDeliveryToday}
size={isTvModeScheduledDelivery ? "small" : "middle"}
/>
</div>
</Card>
);
}
export const DashboardScheduledDeliveryTodayGql = `
scheduled_delivery_today: jobs(where: {
date_invoiced: {_is_null: true},
ro_number: {_is_null: false},
voided: {_eq: false},
scheduled_delivery: {_gte: "${dayjs().startOf("day").toISOString()}",
_lte: "${dayjs().endOf("day").toISOString()}"}}) {
alt_transport
clm_no
jobid: id
joblines(where: {removed: {_eq: false}}) {
mod_lb_hrs
mod_lbr_ty
}
ins_co_nm
iouparent
ownerid
ownr_co_nm
ownr_ea
ownr_fn
ownr_ln
ownr_ph1
ownr_ph2
production_vars
ro_number
scheduled_delivery
status
suspended
v_make_desc
v_model_desc
v_model_yr
v_vin
vehicleid
}
`;

View File

@@ -1,11 +1,11 @@
import { BranchesOutlined, ExclamationCircleFilled, PauseCircleOutlined } from "@ant-design/icons"; import { BranchesOutlined, ExclamationCircleFilled, PauseCircleOutlined } from "@ant-design/icons";
import { Card, Space, Switch, Table, Tooltip, Typography } from "antd"; import { Card, Space, Switch, Table, Tooltip, Typography } from "antd";
import dayjs from "../../../utils/day"; import { useState } from "react";
import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { TimeFormatter } from "../../../utils/DateFormatter"; import { TimeFormatter } from "../../../utils/DateFormatter";
import { onlyUnique } from "../../../utils/arrayHelper"; import { onlyUnique } from "../../../utils/arrayHelper";
import dayjs from "../../../utils/day";
import { alphaSort, dateSort } from "../../../utils/sorters"; import { alphaSort, dateSort } from "../../../utils/sorters";
import useLocalStorage from "../../../utils/useLocalStorage"; import useLocalStorage from "../../../utils/useLocalStorage";
import ChatOpenButton from "../../chat-open-button/chat-open-button.component"; import ChatOpenButton from "../../chat-open-button/chat-open-button.component";
@@ -169,7 +169,7 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
.filter(onlyUnique) .filter(onlyUnique)
.map((s) => { .map((s) => {
return { return {
text: s || "No Alt. Transport", text: s || t("dashboard.errors.atp"),
value: [s] value: [s]
}; };
}) })
@@ -313,7 +313,7 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
.filter(onlyUnique) .filter(onlyUnique)
.map((s) => { .map((s) => {
return { return {
text: s || "No Ins. Co.*", text: s || t("dashboard.errors.insco"),
value: [s] value: [s]
}; };
}) })
@@ -335,7 +335,7 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
.filter(onlyUnique) .filter(onlyUnique)
.map((s) => { .map((s) => {
return { return {
text: s || "No Alt. Transport", text: s || t("dashboard.errors.atp"),
value: [s] value: [s]
}; };
}) })

View File

@@ -1,11 +1,11 @@
import { BranchesOutlined, ExclamationCircleFilled, PauseCircleOutlined } from "@ant-design/icons"; import { BranchesOutlined, ExclamationCircleFilled, PauseCircleOutlined } from "@ant-design/icons";
import { Card, Space, Switch, Table, Tooltip, Typography } from "antd"; import { Card, Space, Switch, Table, Tooltip, Typography } from "antd";
import dayjs from "../../../utils/day"; import { useState } from "react";
import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { TimeFormatter } from "../../../utils/DateFormatter"; import { TimeFormatter } from "../../../utils/DateFormatter";
import { onlyUnique } from "../../../utils/arrayHelper"; import { onlyUnique } from "../../../utils/arrayHelper";
import dayjs from "../../../utils/day";
import { alphaSort, dateSort } from "../../../utils/sorters"; import { alphaSort, dateSort } from "../../../utils/sorters";
import useLocalStorage from "../../../utils/useLocalStorage"; import useLocalStorage from "../../../utils/useLocalStorage";
import ChatOpenButton from "../../chat-open-button/chat-open-button.component"; import ChatOpenButton from "../../chat-open-button/chat-open-button.component";
@@ -138,7 +138,7 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
.filter(onlyUnique) .filter(onlyUnique)
.map((s) => { .map((s) => {
return { return {
text: s || "No Alt. Transport*", text: s || t("dashboard.errors.atp"),
value: [s] value: [s]
}; };
}) })
@@ -154,7 +154,7 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
dataIndex: "status", dataIndex: "status",
key: "status", key: "status",
ellipsis: true, ellipsis: true,
sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport), sorter: (a, b) => alphaSort(a.status, b.status),
sortOrder: state.sortedInfo.columnKey === "status" && state.sortedInfo.order, sortOrder: state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
filters: filters:
(scheduledOutToday && (scheduledOutToday &&
@@ -163,7 +163,7 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
.filter(onlyUnique) .filter(onlyUnique)
.map((s) => { .map((s) => {
return { return {
text: s || "No Status*", text: s || t("dashboard.errors.status"),
value: [s] value: [s]
}; };
}) })
@@ -306,7 +306,7 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
.filter(onlyUnique) .filter(onlyUnique)
.map((s) => { .map((s) => {
return { return {
text: s || "No Ins. Co.*", text: s || t("dashboard.errors.insco"),
value: [s] value: [s]
}; };
}) })
@@ -328,7 +328,7 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
.filter(onlyUnique) .filter(onlyUnique)
.map((s) => { .map((s) => {
return { return {
text: s || "No Alt. Transport*", text: s || t("dashboard.errors.atp"),
value: [s] value: [s]
}; };
}) })

View File

@@ -1,30 +1,33 @@
import i18next from "i18next"; import i18next from "i18next";
import DashboardTotalProductionDollars from "../dashboard-components/total-production-dollars/total-production-dollars.component.jsx"; import JobLifecycleDashboardComponent, {
import { JobLifecycleDashboardGQL
DashboardTotalProductionHours, } from "../dashboard-components/job-lifecycle/job-lifecycle-dashboard.component.jsx";
DashboardTotalProductionHoursGql
} from "../dashboard-components/total-production-hours/total-production-hours.component.jsx";
import DashboardProjectedMonthlySales, {
DashboardProjectedMonthlySalesGql
} from "../dashboard-components/pojected-monthly-sales/projected-monthly-sales.component.jsx";
import DashboardMonthlyRevenueGraph, {
DashboardMonthlyRevenueGraphGql
} from "../dashboard-components/monthly-revenue-graph/monthly-revenue-graph.component.jsx";
import DashboardMonthlyJobCosting from "../dashboard-components/monthly-job-costing/monthly-job-costing.component.jsx";
import DashboardMonthlyPartsSales from "../dashboard-components/monthly-parts-sales/monthly-parts-sales.component.jsx";
import DashboardMonthlyLaborSales from "../dashboard-components/monthly-labor-sales/monthly-labor-sales.component.jsx";
import DashboardMonthlyEmployeeEfficiency, { import DashboardMonthlyEmployeeEfficiency, {
DashboardMonthlyEmployeeEfficiencyGql DashboardMonthlyEmployeeEfficiencyGql
} from "../dashboard-components/monthly-employee-efficiency/monthly-employee-efficiency.component.jsx"; } from "../dashboard-components/monthly-employee-efficiency/monthly-employee-efficiency.component.jsx";
import DashboardMonthlyJobCosting from "../dashboard-components/monthly-job-costing/monthly-job-costing.component.jsx";
import DashboardMonthlyLaborSales from "../dashboard-components/monthly-labor-sales/monthly-labor-sales.component.jsx";
import DashboardMonthlyPartsSales from "../dashboard-components/monthly-parts-sales/monthly-parts-sales.component.jsx";
import DashboardMonthlyRevenueGraph, {
DashboardMonthlyRevenueGraphGql
} from "../dashboard-components/monthly-revenue-graph/monthly-revenue-graph.component.jsx";
import DashboardProjectedMonthlySales, {
DashboardProjectedMonthlySalesGql
} from "../dashboard-components/pojected-monthly-sales/projected-monthly-sales.component.jsx";
import DashboardScheduledDeliveryToday, {
DashboardScheduledDeliveryTodayGql
} from "../dashboard-components/scheduled-delivery-today/scheduled-delivery-today.component.jsx";
import DashboardScheduledInToday, { import DashboardScheduledInToday, {
DashboardScheduledInTodayGql DashboardScheduledInTodayGql
} from "../dashboard-components/scheduled-in-today/scheduled-in-today.component.jsx"; } from "../dashboard-components/scheduled-in-today/scheduled-in-today.component.jsx";
import DashboardScheduledOutToday, { import DashboardScheduledOutToday, {
DashboardScheduledOutTodayGql DashboardScheduledOutTodayGql
} from "../dashboard-components/scheduled-out-today/scheduled-out-today.component.jsx"; } from "../dashboard-components/scheduled-out-today/scheduled-out-today.component.jsx";
import JobLifecycleDashboardComponent, { import DashboardTotalProductionDollars from "../dashboard-components/total-production-dollars/total-production-dollars.component.jsx";
JobLifecycleDashboardGQL import {
} from "../dashboard-components/job-lifecycle/job-lifecycle-dashboard.component.jsx"; DashboardTotalProductionHours,
DashboardTotalProductionHoursGql
} from "../dashboard-components/total-production-hours/total-production-hours.component.jsx";
const componentList = { const componentList = {
ProductionDollars: { ProductionDollars: {
@@ -118,6 +121,15 @@ const componentList = {
w: 10, w: 10,
h: 3 h: 3
}, },
ScheduleDeliveryToday: {
label: i18next.t("dashboard.titles.scheduleddeliverytoday"),
component: DashboardScheduledDeliveryToday,
gqlFragment: DashboardScheduledDeliveryTodayGql,
minW: 6,
minH: 2,
w: 10,
h: 3
},
JobLifecycle: { JobLifecycle: {
label: i18next.t("dashboard.titles.joblifecycle"), label: i18next.t("dashboard.titles.joblifecycle"),
component: JobLifecycleDashboardComponent, component: JobLifecycleDashboardComponent,

View File

@@ -14,7 +14,6 @@ import {
Typography Typography
} from "antd"; } from "antd";
import Dinero from "dinero.js"; import Dinero from "dinero.js";
import React 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";
@@ -24,14 +23,14 @@ import i18n from "../../translations/i18n";
import dayjs from "../../utils/day"; import dayjs from "../../utils/day";
import DmsCdkMakes from "../dms-cdk-makes/dms-cdk-makes.component"; import DmsCdkMakes from "../dms-cdk-makes/dms-cdk-makes.component";
import DmsCdkMakesRefetch from "../dms-cdk-makes/dms-cdk-makes.refetch.component"; import DmsCdkMakesRefetch from "../dms-cdk-makes/dms-cdk-makes.refetch.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
import CurrencyInput from "../form-items-formatted/currency-form-item.component"; import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
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))
}); });
export default connect(mapStateToProps, mapDispatchToProps)(DmsPostForm); export default connect(mapStateToProps, mapDispatchToProps)(DmsPostForm);
@@ -93,7 +92,9 @@ export function DmsPostForm({ bodyshop, socket, job, logsRef }) {
}) })
: "" : ""
}`.slice(0, 239), }`.slice(0, 239),
inservicedate: dayjs("2019-01-01") inservicedate: dayjs(
`${(job.v_model_yr && (job.v_model_yr < 100 ? (job.v_model_yr >= (dayjs().year() + 1) % 100 ? 1900 + parseInt(job.v_model_yr) : 2000 + parseInt(job.v_model_yr)) : job.v_model_yr)) || 2019}-01-01`
)
}} }}
> >
<LayoutFormRow grow> <LayoutFormRow grow>

View File

@@ -1,6 +1,6 @@
import { DatePicker, Space, TimePicker } from "antd"; import { DatePicker, Space, TimePicker } from "antd";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import React, { useCallback, useState } from "react"; import { useCallback, useState } 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";
@@ -94,7 +94,24 @@ const DateTimePicker = ({
showTime={false} showTime={false}
format="MM/DD/YYYY" format="MM/DD/YYYY"
value={value ? dayjs(value) : null} value={value ? dayjs(value) : null}
onChange={handleChange} 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);
}
}}
placeholder={t("general.labels.date")} placeholder={t("general.labels.date")}
onBlur={handleBlur} onBlur={handleBlur}
disabledDate={handleDisabledDate} disabledDate={handleDisabledDate}
@@ -105,13 +122,25 @@ const DateTimePicker = ({
<TimePicker <TimePicker
format="hh:mm a" format="hh:mm a"
minuteStep={15} minuteStep={15}
value={value && dayjs(value).hour() === 0 && dayjs(value).minute() === 0 ? null : dayjs(value)}
defaultOpenValue={dayjs(value) defaultOpenValue={dayjs(value)
.hour(dayjs().hour()) .hour(dayjs().hour())
.minute(Math.floor(dayjs().minute() / 15) * 15) .minute(Math.floor(dayjs().minute() / 15) * 15)
.second(0)} .second(0)}
onChange={(value) => { onChange={(timeValue) => {
handleChange(value); if (timeValue) {
onBlur(); // 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();
}} }}
placeholder={t("general.labels.time")} placeholder={t("general.labels.time")}
{...restProps} {...restProps}

View File

@@ -385,7 +385,9 @@ export function ScheduleEventComponent({
previousEvent: event.id, previousEvent: event.id,
color: event.color, color: event.color,
alt_transport: event.job && event.job.alt_transport, alt_transport: event.job && event.job.alt_transport,
note: event.note note: event.note,
scheduled_in: event.job && event.job.scheduled_in,
scheduled_completion: event.job && event.job.scheduled_completion
} }
}); });
}} }}

View File

@@ -1,13 +1,13 @@
import { Form, Statistic, Tooltip } from "antd"; import { Form, Statistic, Tooltip } from "antd";
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";
import { selectJobReadOnly } from "../../redux/application/application.selectors"; import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import dayjs from "../../utils/day";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component"; import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
import FormRow from "../layout-form-row/layout-form-row.component"; import FormRow from "../layout-form-row/layout-form-row.component";
import dayjs from "../../utils/day";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
jobRO: selectJobReadOnly, jobRO: selectJobReadOnly,
@@ -43,14 +43,14 @@ export function JobsDetailDatesComponent({ jobRO, job, bodyshop }) {
</Form.Item> </Form.Item>
<Form.Item label={t("jobs.fields.estimate_sent_approval")} name="estimate_sent_approval"> <Form.Item label={t("jobs.fields.estimate_sent_approval")} name="estimate_sent_approval">
<DateTimePicker <DateTimePicker
disabled={true} disabled={jobRO}
value={job.estimate_sent_approval ? dayjs(job.estimate_sent_approval) : null} value={job.estimate_sent_approval ? dayjs(job.estimate_sent_approval) : null}
placeholder={t("general.labels.na")} placeholder={t("general.labels.na")}
/> />
</Form.Item> </Form.Item>
<Form.Item label={t("jobs.fields.estimate_approved")} name="estimate_approved"> <Form.Item label={t("jobs.fields.estimate_approved")} name="estimate_approved">
<DateTimePicker <DateTimePicker
disabled={true} disabled={jobRO}
value={job.estimate_approved ? dayjs(job.estimate_approved) : null} value={job.estimate_approved ? dayjs(job.estimate_approved) : null}
placeholder={t("general.labels.na")} placeholder={t("general.labels.na")}
/> />
@@ -63,7 +63,7 @@ export function JobsDetailDatesComponent({ jobRO, job, bodyshop }) {
</Form.Item> </Form.Item>
<Tooltip title={t("jobs.labels.scheduledinchange")}> <Tooltip title={t("jobs.labels.scheduledinchange")}>
<Form.Item label={t("jobs.fields.scheduled_in")} name="scheduled_in"> <Form.Item label={t("jobs.fields.scheduled_in")} name="scheduled_in">
<DateTimePicker disabled={true || jobRO} /> <DateTimePicker disabled={true} />
</Form.Item> </Form.Item>
</Tooltip> </Tooltip>
<Form.Item label={t("jobs.fields.actual_in")} name="actual_in"> <Form.Item label={t("jobs.fields.actual_in")} name="actual_in">
@@ -110,16 +110,16 @@ export function JobsDetailDatesComponent({ jobRO, job, bodyshop }) {
</FormRow> </FormRow>
<FormRow header={t("jobs.forms.admindates")}> <FormRow header={t("jobs.forms.admindates")}>
<Form.Item label={t("jobs.fields.date_invoiced")} name="date_invoiced"> <Form.Item label={t("jobs.fields.date_invoiced")} name="date_invoiced">
<DateTimePicker disabled={true || jobRO} /> <DateTimePicker disabled={true} />
</Form.Item> </Form.Item>
<Form.Item label={t("jobs.fields.date_exported")} name="date_exported"> <Form.Item label={t("jobs.fields.date_exported")} name="date_exported">
<DateTimePicker disabled={true || jobRO} /> <DateTimePicker disabled={true} />
</Form.Item> </Form.Item>
<Form.Item label={t("jobs.fields.date_void")} name="date_void"> <Form.Item label={t("jobs.fields.date_void")} name="date_void">
<DateTimePicker disabled={true || jobRO} /> <DateTimePicker disabled={true} />
</Form.Item> </Form.Item>
<Form.Item label={t("jobs.fields.date_lost_sale")} name="date_lost_sale"> <Form.Item label={t("jobs.fields.date_lost_sale")} name="date_lost_sale">
<DateTimePicker disabled={true || jobRO} /> <DateTimePicker disabled={true} />
</Form.Item> </Form.Item>
</FormRow> </FormRow>
</div> </div>

View File

@@ -673,7 +673,9 @@ export function JobsDetailHeaderActions({
context: { context: {
jobId: job.id, jobId: job.id,
job: job, job: job,
alt_transport: job.alt_transport alt_transport: job.alt_transport,
scheduled_in: job.scheduled_in,
scheduled_completion: job.scheduled_completion
} }
}); });
} }
@@ -1090,11 +1092,7 @@ export function JobsDetailHeaderActions({
{t("menus.jobsactions.deletejob")} {t("menus.jobsactions.deletejob")}
</Popconfirm> </Popconfirm>
) : ( ) : (
<Popconfirm <Popconfirm title={t("jobs.labels.deletewatchers")} onClick={(e) => e.stopPropagation()} showCancel={false}>
title={t("jobs.labels.deletewatchers")}
onClick={(e) => e.stopPropagation()}
showCancel={false}
>
{t("menus.jobsactions.deletejob")} {t("menus.jobsactions.deletejob")}
</Popconfirm> </Popconfirm>
) )

View File

@@ -3,7 +3,7 @@ import { Button, Card, Input, Space, Table, Typography } from "antd";
import axios from "axios"; import axios from "axios";
import _ from "lodash"; import _ from "lodash";
import queryString from "query-string"; import queryString from "query-string";
import React, { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Link, useLocation, useNavigate } from "react-router-dom"; import { Link, useLocation, useNavigate } from "react-router-dom";
@@ -20,7 +20,7 @@ const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser //currentUser: selectCurrentUser
bodyshop: selectBodyshop bodyshop: selectBodyshop
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = () => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
}); });
@@ -203,6 +203,8 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
return ( return (
<Card <Card
id="all-jobs-list"
title={t("titles.bc.jobs-all")}
extra={ extra={
<Space wrap> <Space wrap>
{search.search && ( {search.search && (
@@ -256,6 +258,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
rowKey="id" rowKey="id"
dataSource={search?.search ? openSearchResults : jobs} dataSource={search?.search ? openSearchResults : jobs}
onChange={handleTableChange} onChange={handleTableChange}
id="all-jobs-list-table"
/> />
</Card> </Card>
); );

View File

@@ -2,7 +2,7 @@ import { BranchesOutlined, ExclamationCircleFilled, PauseCircleOutlined, SyncOut
import { useQuery } from "@apollo/client"; import { useQuery } from "@apollo/client";
import { Button, Card, Grid, Input, Space, Table, Tooltip } from "antd"; import { Button, Card, Grid, Input, Space, Table, Tooltip } from "antd";
import queryString from "query-string"; import queryString from "query-string";
import React, { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Link, useLocation, useNavigate } from "react-router-dom"; import { Link, useLocation, useNavigate } from "react-router-dom";
@@ -22,7 +22,7 @@ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop bodyshop: selectBodyshop
}); });
const mapDispatchToProps = (dispatch) => ({}); const mapDispatchToProps = () => ({});
export function JobsList({ bodyshop }) { export function JobsList({ bodyshop }) {
const searchParams = queryString.parse(useLocation().search); const searchParams = queryString.parse(useLocation().search);
@@ -342,13 +342,14 @@ export function JobsList({ bodyshop }) {
type: "radio" type: "radio"
}} }}
onChange={handleTableChange} onChange={handleTableChange}
onRow={(record, rowIndex) => { onRow={(record) => {
return { return {
onClick: (event) => { onClick: () => {
handleOnRowClick(record); handleOnRowClick(record);
} }
}; };
}} }}
id="active-jobs-list-table"
/> />
</Card> </Card>
); );

View File

@@ -1,10 +1,10 @@
import { useMutation, useQuery } from "@apollo/client"; import { useMutation, useQuery } from "@apollo/client";
import { Form, Modal } from "antd"; import { Form, Modal } from "antd";
import dayjs from "../../utils/day"; import { useEffect, useState } from "react";
import React, { useEffect, useState } 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";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
import { logImEXEvent } from "../../firebase/firebase.utils"; import { logImEXEvent } from "../../firebase/firebase.utils";
import { import {
CANCEL_APPOINTMENT_BY_ID, CANCEL_APPOINTMENT_BY_ID,
@@ -19,9 +19,9 @@ import { selectSchedule } from "../../redux/modals/modals.selectors";
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors"; import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings"; import AuditTrailMapping from "../../utils/AuditTrailMappings";
import { DateTimeFormat } from "../../utils/DateFormatter"; import { DateTimeFormat } from "../../utils/DateFormatter";
import dayjs from "../../utils/day";
import { TemplateList } from "../../utils/TemplateConstants"; import { TemplateList } from "../../utils/TemplateConstants";
import ScheduleJobModalComponent from "./schedule-job-modal.component"; import ScheduleJobModalComponent from "./schedule-job-modal.component";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -72,7 +72,7 @@ export function ScheduleJobModalContainer({
variables: { jobid: jobId }, variables: { jobid: jobId },
fetchPolicy: "network-only", fetchPolicy: "network-only",
nextFetchPolicy: "network-only", nextFetchPolicy: "network-only",
skip: !open || !!!jobId skip: !open || !jobId
}); });
useEffect(() => { useEffect(() => {
@@ -93,12 +93,12 @@ export function ScheduleJobModalContainer({
logImEXEvent("schedule_new_appointment"); logImEXEvent("schedule_new_appointment");
setLoading(true); setLoading(true);
if (!!previousEvent) { if (previousEvent) {
const cancelAppt = await cancelAppointment({ const cancelAppt = await cancelAppointment({
variables: { appid: previousEvent } variables: { appid: previousEvent }
}); });
if (!!cancelAppt.errors) { if (cancelAppt.errors) {
notification["error"]({ notification["error"]({
message: t("appointments.errors.canceling", { message: t("appointments.errors.canceling", {
message: JSON.stringify(cancelAppt.errors) message: JSON.stringify(cancelAppt.errors)
@@ -146,7 +146,7 @@ export function ScheduleJobModalContainer({
}); });
} }
if (!!appt.errors) { if (appt.errors) {
notification["error"]({ notification["error"]({
message: t("appointments.errors.saving", { message: t("appointments.errors.saving", {
message: JSON.stringify(appt.errors) message: JSON.stringify(appt.errors)
@@ -172,7 +172,7 @@ export function ScheduleJobModalContainer({
} }
}); });
if (!!jobUpdate.errors) { if (jobUpdate.errors) {
notification["error"]({ notification["error"]({
message: t("appointments.errors.saving", { message: t("appointments.errors.saving", {
message: JSON.stringify(jobUpdate.errors) message: JSON.stringify(jobUpdate.errors)
@@ -222,9 +222,9 @@ export function ScheduleJobModalContainer({
initialValues={{ initialValues={{
notifyCustomer: !!(job && job.ownr_ea), notifyCustomer: !!(job && job.ownr_ea),
email: (job && job.ownr_ea) || "", email: (job && job.ownr_ea) || "",
start: null,
// smartDates: [], // smartDates: [],
scheduled_completion: null, start: context.scheduled_in,
scheduled_completion: context.scheduled_completion ,
color: context.color, color: context.color,
alt_transport: context.alt_transport, alt_transport: context.alt_transport,
note: context.note note: context.note

View File

@@ -158,7 +158,10 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser }) => {
auth: { token, bodyshopId: bodyshop.id }, auth: { token, bodyshopId: bodyshop.id },
reconnectionAttempts: Infinity, reconnectionAttempts: Infinity,
reconnectionDelay: 2000, 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; socketRef.current = socketInstance;
@@ -249,7 +252,7 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser }) => {
break; break;
} }
}; };
const handleConnect = () => { const handleConnect = () => {
socketInstance.emit("join-bodyshop-room", bodyshop.id); socketInstance.emit("join-bodyshop-room", bodyshop.id);
setClientId(socketInstance.id); setClientId(socketInstance.id);

View File

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

View File

@@ -975,7 +975,10 @@
"addcomponent": "Add Component" "addcomponent": "Add Component"
}, },
"errors": { "errors": {
"atp": "No Alt. Transport*",
"insco": "No Ins. Co.*",
"refreshrequired": "You must refresh the dashboard data to see this component.", "refreshrequired": "You must refresh the dashboard data to see this component.",
"status": "No Status*",
"updatinglayout": "Error saving updated layout {{message}}" "updatinglayout": "Error saving updated layout {{message}}"
}, },
"labels": { "labels": {
@@ -998,6 +1001,8 @@
"productiondollars": "Total Dollars in Production", "productiondollars": "Total Dollars in Production",
"productionhours": "Total Hours in Production", "productionhours": "Total Hours in Production",
"projectedmonthlysales": "Projected Monthly Sales", "projectedmonthlysales": "Projected Monthly Sales",
"scheduleddeliverydate": "Scheduled Delivery Date: {{date}}",
"scheduleddeliverytoday": "Scheduled Delivery Today",
"scheduledindate": "Scheduled In Today: {{date}}", "scheduledindate": "Scheduled In Today: {{date}}",
"scheduledintoday": "Scheduled In Today", "scheduledintoday": "Scheduled In Today",
"scheduledoutdate": "Scheduled Out Today: {{date}}", "scheduledoutdate": "Scheduled Out Today: {{date}}",
@@ -3518,7 +3523,7 @@
"dashboard": "Dashboard", "dashboard": "Dashboard",
"dms": "DMS Export", "dms": "DMS Export",
"export-logs": "Export Logs", "export-logs": "Export Logs",
"feature-request": "Feature Requet", "feature-request": "Feature Request",
"inventory": "Inventory", "inventory": "Inventory",
"jobs": "Jobs", "jobs": "Jobs",
"jobs-active": "Active Jobs", "jobs-active": "Active Jobs",

View File

@@ -975,7 +975,10 @@
"addcomponent": "" "addcomponent": ""
}, },
"errors": { "errors": {
"atp": "",
"insco": "",
"refreshrequired": "", "refreshrequired": "",
"status": "",
"updatinglayout": "" "updatinglayout": ""
}, },
"labels": { "labels": {
@@ -998,6 +1001,8 @@
"productiondollars": "", "productiondollars": "",
"productionhours": "", "productionhours": "",
"projectedmonthlysales": "", "projectedmonthlysales": "",
"scheduleddeliverydate": "",
"scheduleddeliverytoday": "",
"scheduledindate": "", "scheduledindate": "",
"scheduledintoday": "", "scheduledintoday": "",
"scheduledoutdate": "", "scheduledoutdate": "",

View File

@@ -975,7 +975,10 @@
"addcomponent": "" "addcomponent": ""
}, },
"errors": { "errors": {
"atp": "",
"insco": "",
"refreshrequired": "", "refreshrequired": "",
"status": "",
"updatinglayout": "" "updatinglayout": ""
}, },
"labels": { "labels": {
@@ -998,6 +1001,8 @@
"productiondollars": "", "productiondollars": "",
"productionhours": "", "productionhours": "",
"projectedmonthlysales": "", "projectedmonthlysales": "",
"scheduleddeliverydate": "",
"scheduleddeliverytoday": "",
"scheduledindate": "", "scheduledindate": "",
"scheduledintoday": "", "scheduledintoday": "",
"scheduledoutdate": "", "scheduledoutdate": "",

View File

@@ -19,7 +19,7 @@ async function JobCosting(req, res) {
const client = req.userGraphQLClient; const client = req.userGraphQLClient;
//Uncomment for further testing //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 { try {
const resp = await client.setHeaders({ Authorization: BearerToken }).request(queries.QUERY_JOB_COSTING_DETAILS, { 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; const client = req.userGraphQLClient;
//Uncomment for further testing //Uncomment for further testing
// logger.log("job-costing-multi-start", "DEBUG", req?.user?.email, null, { logger.log("job-costing-multi-start", "DEBUG", req?.user?.email, null, {
// jobids jobids
// }); });
try { try {
const resp = await client const resp = await client
@@ -589,7 +589,7 @@ function GenerateCostingData(job) {
amount: Math.round((job.storage_payable || 0) * 100) amount: Math.round((job.storage_payable || 0) * 100)
}); });
} }
//Is it a DMS Setup? //Is it a DMS Setup?
const selectedDmsAllocationConfig = const selectedDmsAllocationConfig =
(job.bodyshop.md_responsibility_centers.dms_defaults && (job.bodyshop.md_responsibility_centers.dms_defaults &&

View File

@@ -8,6 +8,7 @@ const getLifecycleStatusColor = require("../utils/getLifecycleStatusColor");
const jobLifecycle = async (req, res) => { const jobLifecycle = async (req, res) => {
// Grab the jobids and statuses from the request body // Grab the jobids and statuses from the request body
const { jobids, statuses } = req.body; const { jobids, statuses } = req.body;
const { logger } = req;
if (!jobids) { if (!jobids) {
return res.status(400).json({ return res.status(400).json({
@@ -16,102 +17,118 @@ const jobLifecycle = async (req, res) => {
} }
const jobIDs = _.isArray(jobids) ? jobids : [jobids]; 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({ return res.status(200).json({
jobIDs, jobIDs,
transitions: [] transition: groupedTransitions,
}); durations: {
} jobs: jobIDs.length,
summations: finalSummations,
const transitionsByJobId = _.groupBy(resp.transitions, "jobid"); totalStatuses: finalSummations.length,
total: finalTotal,
const groupedTransitions = {}; statusCounts: finalStatusCounts,
const allDurations = []; humanReadable: durationToHumanReadable(moment.duration(finalTotal)),
averageValue: _.size(jobIDs) > 0 ? finalTotal / jobIDs.length : 0,
for (let jobId in transitionsByJobId) { averageHumanReadable:
let lifecycle = transitionsByJobId[jobId].map((transition) => { _.size(jobIDs) > 0
transition.start_readable = transition.start ? moment(transition.start).fromNow() : "N/A"; ? durationToHumanReadable(moment.duration(finalTotal / jobIDs.length))
transition.end_readable = transition.end ? moment(transition.end).fromNow() : "N/A"; : durationToHumanReadable(moment.duration(0))
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;
}); });
} catch (error) {
const durations = calculateStatusDuration(lifecycle, statuses); logger.log("job-lifecycle-error", "ERROR", req?.user?.email, null, {
jobids: jobIDs,
groupedTransitions[jobId] = { statuses: statuses ? JSON.stringify(statuses) : "N/A",
lifecycle, error: error.message
durations });
}; return res.status(500).json({
error: "Internal server error"
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
});
});
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; module.exports = jobLifecycle;

View File

@@ -50,7 +50,12 @@ const autoAddWatchers = async (req) => {
try { try {
// Fetch bodyshop data from Redis // Fetch bodyshop data from Redis
const bodyshopData = await getBodyshopFromRedis(shopId); const bodyshopData = await getBodyshopFromRedis(shopId);
const notificationFollowers = bodyshopData?.notification_followers || []; let notificationFollowers = bodyshopData?.notification_followers;
// Bail if notification_followers is missing or not an array
if (!notificationFollowers || !Array.isArray(notificationFollowers)) {
return;
}
// Execute queries in parallel // Execute queries in parallel
const [notificationData, existingWatchersData] = await Promise.all([ const [notificationData, existingWatchersData] = await Promise.all([