Compare commits
34 Commits
feature/IO
...
feature/IO
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a182ea0869 | ||
|
|
d92bab113e | ||
|
|
93c6e2b601 | ||
|
|
19a90571f6 | ||
|
|
953e70efef | ||
|
|
a6bae390e5 | ||
|
|
cf9d8d649d | ||
|
|
a25051c4c2 | ||
|
|
d5c3152631 | ||
|
|
66c425bf96 | ||
|
|
ffad0dfbf7 | ||
|
|
17285fc029 | ||
|
|
401e3cff73 | ||
|
|
865680e019 | ||
|
|
9f97ca0336 | ||
|
|
5df38f8612 | ||
|
|
63c5719420 | ||
|
|
d6c80f1420 | ||
|
|
fade927c9e | ||
|
|
9f472ce1d0 | ||
|
|
47a56e32b9 | ||
|
|
f13f79acb6 | ||
|
|
bfa9fddb9e | ||
|
|
28abd9707e | ||
|
|
5f621e1ae0 | ||
|
|
624414799e | ||
|
|
72091e9eae | ||
|
|
9cfacdd025 | ||
|
|
655e516246 | ||
|
|
7b12f0a3b9 | ||
|
|
5c4267f3ef | ||
|
|
e79e512291 | ||
|
|
f0064abfbe | ||
|
|
32bdea559e |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -130,3 +130,5 @@ test-output.txt
|
|||||||
server/job/test/fixtures
|
server/job/test/fixtures
|
||||||
|
|
||||||
.github
|
.github
|
||||||
|
_reference/ragmate/.ragmate.env
|
||||||
|
docker_data
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
client_max_body_size 50M;
|
client_max_body_size 50M;
|
||||||
client_body_buffer_size 5M;
|
client_body_buffer_size 5M;
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -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]
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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]
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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": "",
|
||||||
|
|||||||
@@ -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": "",
|
||||||
|
|||||||
@@ -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 &&
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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([
|
||||||
|
|||||||
Reference in New Issue
Block a user