Compare commits
26 Commits
rrScratch2
...
feature/IO
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d94ce7e5c | ||
|
|
182a8d59ab | ||
|
|
6ea1c291e6 | ||
|
|
05d5c96491 | ||
|
|
35a566cbe5 | ||
|
|
f12e40e4c6 | ||
|
|
bb4e671c83 | ||
|
|
d1637d2432 | ||
|
|
1c79628613 | ||
|
|
521a7084b7 | ||
|
|
77268d5f5b | ||
|
|
1b3abf17ec | ||
|
|
3cfd445894 | ||
|
|
b510eec9aa | ||
|
|
e5eac0933f | ||
|
|
a3c71fdfc0 | ||
|
|
78750d3d96 | ||
|
|
90edf94fee | ||
|
|
065fb72677 | ||
|
|
ddc6141480 | ||
|
|
7bc137fa79 | ||
|
|
dafe9de753 | ||
|
|
78a8474a24 | ||
|
|
123066f1cd | ||
|
|
a153cca3c0 | ||
|
|
35c7c32c8e |
@@ -138,7 +138,7 @@ export function App({
|
||||
);
|
||||
}
|
||||
|
||||
if (currentEula && !currentUser.eulaIsAccepted) {
|
||||
if (!isPartsEntry && currentEula && !currentUser.eulaIsAccepted) {
|
||||
return <Eula />;
|
||||
}
|
||||
|
||||
|
||||
@@ -142,17 +142,37 @@ export default function JobLifecycleDashboardComponent({ data, bodyshop, ...card
|
||||
title={t("job_lifecycle.content.legend_title")}
|
||||
style={{ marginTop: "10px" }}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
gap: 8
|
||||
}}
|
||||
>
|
||||
{lifecycleData.summations.map((key) => (
|
||||
<Tag key={key.status} color={key.color} style={{ width: "13vh", padding: "4px", margin: "4px" }}>
|
||||
<Tag
|
||||
key={key.status}
|
||||
color={key.color}
|
||||
style={{
|
||||
// IMPORTANT: let the tag grow with its content
|
||||
width: "auto",
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
boxSizing: "border-box"
|
||||
}}
|
||||
>
|
||||
<div
|
||||
aria-label={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
|
||||
title={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
|
||||
style={{
|
||||
backgroundColor: "var(--tag-wrapper-bg)",
|
||||
color: "var(--tag-wrapper-text)",
|
||||
padding: "4px",
|
||||
textAlign: "center"
|
||||
padding: "4px 8px",
|
||||
textAlign: "center",
|
||||
whiteSpace: "nowrap" // keep it on one line while letting the pill expand
|
||||
}}
|
||||
>
|
||||
{key.status} [{lifecycleData.statusCounts[key.status]}] ({key.roundedPercentage})
|
||||
|
||||
@@ -55,7 +55,8 @@ const Eula = ({ currentEula, currentUser, acceptEula }) => {
|
||||
const useremail = currentUser.email;
|
||||
|
||||
try {
|
||||
const { ...otherFormValues } = formValues;
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { accepted_terms, ...otherFormValues } = formValues;
|
||||
|
||||
// Trim the values of the fields before submitting
|
||||
const trimmedFormValues = Object.entries(otherFormValues).reduce((acc, [key, value]) => {
|
||||
|
||||
@@ -222,17 +222,37 @@ export function JobLifecycleComponent({ bodyshop, job, statuses }) {
|
||||
</div>
|
||||
</BlurWrapperComponent>
|
||||
<Card type="inner" title={t("job_lifecycle.content.legend_title")} style={{ marginTop: "10px" }}>
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
gap: 8
|
||||
}}
|
||||
>
|
||||
{lifecycleData.durations.summations.map((key) => (
|
||||
<Tag key={key.status} color={key.color} style={{ width: "13vh", padding: "4px", margin: "4px" }}>
|
||||
<Tag
|
||||
key={key.status}
|
||||
color={key.color}
|
||||
style={{
|
||||
// let the tag grow with its content
|
||||
width: "auto",
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
boxSizing: "border-box"
|
||||
}}
|
||||
>
|
||||
<div
|
||||
aria-label={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
|
||||
title={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
|
||||
style={{
|
||||
backgroundColor: "var(--tag-wrapper-bg)",
|
||||
color: "var(--tag-wrapper-text)",
|
||||
padding: "4px",
|
||||
textAlign: "center"
|
||||
padding: "4px 8px",
|
||||
textAlign: "center",
|
||||
whiteSpace: "nowrap" // single line; tag gets wider instead of text escaping
|
||||
}}
|
||||
>
|
||||
{key.status} (
|
||||
|
||||
@@ -609,7 +609,7 @@ export function JobsDetailHeaderActions({
|
||||
<FormDateTimePickerComponent
|
||||
onBlur={() => {
|
||||
const start = form.getFieldValue("start");
|
||||
form.setFieldsValue({ end: start.add(30, "minutes") });
|
||||
form.setFieldsValue({ end: start?.add(30, "minutes") });
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
@@ -35,16 +35,14 @@ export function JobsDocumentsImgproxyDownloadButton({ galleryImages, identifier,
|
||||
...galleryImages.other.filter((image) => image.isSelected)
|
||||
];
|
||||
|
||||
function downloadProgress(progressEvent) {
|
||||
setDownload((currentDownloadState) => {
|
||||
return {
|
||||
downloaded: progressEvent.loaded || 0,
|
||||
speed: (progressEvent.loaded || 0) - ((currentDownloadState && currentDownloadState.downloaded) || 0)
|
||||
};
|
||||
});
|
||||
}
|
||||
const downloadProgress = ({ loaded }) => {
|
||||
setDownload((currentDownloadState) => ({
|
||||
downloaded: loaded ?? 0,
|
||||
speed: (loaded ?? 0) - (currentDownloadState?.downloaded ?? 0)
|
||||
}));
|
||||
};
|
||||
|
||||
function standardMediaDownload(bufferData) {
|
||||
const standardMediaDownload = (bufferData) => {
|
||||
try {
|
||||
const a = document.createElement("a");
|
||||
const url = window.URL.createObjectURL(new Blob([bufferData]));
|
||||
@@ -55,29 +53,26 @@ export function JobsDocumentsImgproxyDownloadButton({ galleryImages, identifier,
|
||||
setLoading(false);
|
||||
setDownload(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownload = async () => {
|
||||
logImEXEvent("jobs_documents_download");
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await axios({
|
||||
const { data } = await axios({
|
||||
url: "/media/imgproxy/download",
|
||||
method: "POST",
|
||||
responseType: "blob",
|
||||
data: { jobId, documentids: imagesToDownload.map((_) => _.id) },
|
||||
onDownloadProgress: downloadProgress
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
setDownload(null);
|
||||
|
||||
// Use the response data (Blob) to trigger download
|
||||
standardMediaDownload(response.data);
|
||||
standardMediaDownload(data);
|
||||
} catch {
|
||||
// handle error (optional)
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setDownload(null);
|
||||
// handle error (optional)
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -76,14 +76,14 @@ function JobsDocumentsImgproxyComponent({
|
||||
<SyncOutlined />
|
||||
</Button>
|
||||
<JobsDocumentsGallerySelectAllComponent galleryImages={galleryImages} setGalleryImages={setGalleryImages} />
|
||||
{!billId && (
|
||||
<JobsDocumentsGalleryReassign galleryImages={galleryImages} callback={fetchThumbnails || refetch} />
|
||||
)}
|
||||
<JobsDocumentsDownloadButton galleryImages={galleryImages} identifier={downloadIdentifier} jobId={jobId} />
|
||||
<JobsDocumentsDeleteButton
|
||||
galleryImages={galleryImages}
|
||||
deletionCallback={billsCallback || fetchThumbnails || refetch}
|
||||
/>
|
||||
{!billId && (
|
||||
<JobsDocumentsGalleryReassign galleryImages={galleryImages} callback={fetchThumbnails || refetch} />
|
||||
)}
|
||||
</Space>
|
||||
</Col>
|
||||
{!hasMediaAccess && (
|
||||
|
||||
@@ -67,7 +67,7 @@ export default function JobsDocumentsImgproxyDeleteButton({ galleryImages, delet
|
||||
okButtonProps={{ danger: true }}
|
||||
cancelText={t("general.actions.cancel")}
|
||||
>
|
||||
<Button disabled={imagesToDelete.length < 1} loading={loading}>
|
||||
<Button danger disabled={imagesToDelete.length < 1} loading={loading}>
|
||||
{t("documents.actions.delete")}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
|
||||
@@ -107,8 +107,8 @@ export function JobsDocumentsLocalGallery({
|
||||
<a href={CreateExplorerLinkForJob({ jobid: job.id })}>
|
||||
<Button>{t("documents.labels.openinexplorer")}</Button>
|
||||
</a>
|
||||
<JobsDocumentsLocalGalleryReassign jobid={job.id} />
|
||||
<JobsDocumentsLocalGallerySelectAllComponent jobid={job.id} />
|
||||
<JobsDocumentsLocalGalleryReassign jobid={job.id} />
|
||||
<JobsLocalGalleryDownloadButton job={job} />
|
||||
<JobsDocumentsLocalDeleteButton jobid={job.id} />
|
||||
</Space>
|
||||
|
||||
@@ -28,6 +28,8 @@ export function JobsDocumentsLocalDeleteButton({ bodyshop, getJobMedia, allMedia
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const imagesToDelete = (allMedia?.[jobid] || []).filter((i) => i.isSelected);
|
||||
|
||||
const handleDelete = async () => {
|
||||
logImEXEvent("job_documents_delete");
|
||||
setLoading(true);
|
||||
@@ -36,7 +38,7 @@ export function JobsDocumentsLocalDeleteButton({ bodyshop, getJobMedia, allMedia
|
||||
`${bodyshop.localmediaserverhttp}/jobs/delete`,
|
||||
{
|
||||
jobid: jobid,
|
||||
files: (allMedia?.[jobid] || []).filter((i) => i.isSelected).map((i) => i.filename)
|
||||
files: imagesToDelete.map((i) => i.filename)
|
||||
},
|
||||
{ headers: { ims_token: bodyshop.localmediatoken } }
|
||||
);
|
||||
@@ -60,14 +62,17 @@ export function JobsDocumentsLocalDeleteButton({ bodyshop, getJobMedia, allMedia
|
||||
|
||||
return (
|
||||
<Popconfirm
|
||||
disabled={imagesToDelete.length < 1}
|
||||
icon={<QuestionCircleOutlined style={{ color: "red" }} />}
|
||||
onConfirm={handleDelete}
|
||||
title={t("documents.labels.confirmdelete")}
|
||||
okText={t("general.actions.delete")}
|
||||
okButtonProps={{ type: "danger" }}
|
||||
okButtonProps={{ danger: true }}
|
||||
cancelText={t("general.actions.cancel")}
|
||||
>
|
||||
<Button loading={loading}>{t("documents.actions.delete")}</Button>
|
||||
<Button danger disabled={imagesToDelete.length < 1} loading={loading}>
|
||||
{t("documents.actions.delete")}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Button } from "antd";
|
||||
import { Button, Space } from "antd";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import cleanAxios from "../../utils/CleanAxios";
|
||||
|
||||
import formatBytes from "../../utils/formatbytes";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectAllMedia } from "../../redux/media/media.selectors";
|
||||
@@ -19,45 +19,63 @@ export default connect(mapStateToProps, mapDispatchToProps)(JobsLocalGalleryDown
|
||||
|
||||
export function JobsLocalGalleryDownloadButton({ bodyshop, allMedia, job }) {
|
||||
const { t } = useTranslation();
|
||||
const [download, setDownload] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [download, setDownload] = useState(false);
|
||||
|
||||
function downloadProgress(progressEvent) {
|
||||
setDownload((currentDownloadState) => {
|
||||
return {
|
||||
downloaded: progressEvent.loaded || 0,
|
||||
speed: (progressEvent.loaded || 0) - (currentDownloadState?.downloaded || 0)
|
||||
};
|
||||
});
|
||||
}
|
||||
const imagesToDownload = (allMedia?.[job.id] || []).filter((i) => i.isSelected);
|
||||
|
||||
const downloadProgress = ({ loaded }) => {
|
||||
setDownload((currentDownloadState) => ({
|
||||
downloaded: loaded || 0,
|
||||
speed: (loaded || 0) - (currentDownloadState?.downloaded || 0)
|
||||
}));
|
||||
};
|
||||
|
||||
const standardMediaDownload = (bufferData, filename) => {
|
||||
try {
|
||||
const a = document.createElement("a");
|
||||
const url = window.URL.createObjectURL(new Blob([bufferData]));
|
||||
a.href = url;
|
||||
a.download = `${filename}.zip`;
|
||||
a.click();
|
||||
} catch {
|
||||
setLoading(false);
|
||||
setDownload(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownload = async () => {
|
||||
const theDownloadedZip = await cleanAxios.post(
|
||||
`${bodyshop.localmediaserverhttp}/jobs/download`,
|
||||
{
|
||||
jobid: job.id,
|
||||
files: (allMedia?.[job.id] || []).filter((i) => i.isSelected).map((i) => i.filename)
|
||||
},
|
||||
{
|
||||
headers: { ims_token: bodyshop.localmediatoken },
|
||||
responseType: "arraybuffer",
|
||||
onDownloadProgress: downloadProgress
|
||||
}
|
||||
);
|
||||
setDownload(null);
|
||||
standardMediaDownload(theDownloadedZip.data, job.ro_number);
|
||||
const { localmediaserverhttp, localmediatoken } = bodyshop;
|
||||
const { id, ro_number } = job;
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await cleanAxios.post(
|
||||
`${localmediaserverhttp}/jobs/download`,
|
||||
{
|
||||
jobid: id,
|
||||
files: imagesToDownload.map((i) => i.filename)
|
||||
},
|
||||
{
|
||||
headers: { ims_token: localmediatoken },
|
||||
responseType: "arraybuffer",
|
||||
onDownloadProgress: downloadProgress
|
||||
}
|
||||
);
|
||||
standardMediaDownload(response.data, ro_number);
|
||||
} catch {
|
||||
// handle error (optional)
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setDownload(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Button loading={!!download} onClick={handleDownload}>
|
||||
{t("documents.actions.download")}
|
||||
<Button disabled={imagesToDownload < 1} loading={download || loading} onClick={handleDownload}>
|
||||
<Space>
|
||||
<span>{t("documents.actions.download")}</span>
|
||||
{download && <span>{`(${formatBytes(download.downloaded)} @ ${formatBytes(download.speed)} / second)`}</span>}
|
||||
</Space>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function standardMediaDownload(bufferData, filename) {
|
||||
const a = document.createElement("a");
|
||||
const url = window.URL.createObjectURL(new Blob([bufferData]));
|
||||
a.href = url;
|
||||
a.download = `${filename}.zip`;
|
||||
a.click();
|
||||
}
|
||||
|
||||
@@ -144,7 +144,7 @@ export function ProductionListEmpAssignment({ insertAuditTrail, bodyshop, record
|
||||
<Spin spinning={loading}>
|
||||
{record[type] ? (
|
||||
<div>
|
||||
<span>{`${theEmployee.first_name || ""} ${theEmployee.last_name || ""}`}</span>
|
||||
<span>{`${theEmployee?.first_name || ""} ${theEmployee?.last_name || ""}`}</span>
|
||||
<DeleteFilled style={iconStyle} onClick={() => handleRemove(type)} />
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -16,7 +16,7 @@ const mapDispatchToProps = () => ({
|
||||
export function TechHeader({ technician }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Header style={{ textAlign: "center" }}>
|
||||
<Header style={{ textAlign: "center", height: "auto", overflow: "visible" }}>
|
||||
<Typography.Title style={{ color: "#fff" }}>
|
||||
{technician
|
||||
? t("tech.labels.loggedin", {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMutation, useQuery } from "@apollo/client";
|
||||
import { Button, Card, Col, Form, InputNumber, Popover, Row, Select } from "antd";
|
||||
import { Button, Card, Form, InputNumber, Popover, Select, Space } from "antd";
|
||||
import axios from "axios";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -124,103 +124,12 @@ export function TechClockOffButton({
|
||||
cost_center: isShiftTicket ? "timetickets.labels.shift" : technician ? technician.cost_center : null
|
||||
}}
|
||||
>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={!isShiftTicket ? 8 : 24}>
|
||||
{!isShiftTicket ? (
|
||||
<div>
|
||||
<Form.Item
|
||||
label={t("timetickets.fields.actualhrs")}
|
||||
name="actualhrs"
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} precision={1} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("timetickets.fields.productivehrs")}
|
||||
name="productivehrs"
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
({ getFieldValue }) => ({
|
||||
validator(rule, value) {
|
||||
if (!bodyshop.tt_enforce_hours_for_tech_console) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (!value || getFieldValue("cost_center") === null || !lineTicketData)
|
||||
return Promise.resolve();
|
||||
|
||||
//Check the cost center,
|
||||
const totals = CalculateAllocationsTotals(
|
||||
bodyshop,
|
||||
lineTicketData.joblines,
|
||||
lineTicketData.timetickets,
|
||||
lineTicketData.jobs_by_pk.lbr_adjustments
|
||||
);
|
||||
|
||||
const fieldTypeToCheck =
|
||||
bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber ? "mod_lbr_ty" : "cost_center";
|
||||
|
||||
const costCenterDiff =
|
||||
Math.round(
|
||||
totals.find((total) => total[fieldTypeToCheck] === getFieldValue("cost_center"))
|
||||
?.difference * 10
|
||||
) / 10;
|
||||
|
||||
if (value > costCenterDiff)
|
||||
return Promise.reject(t("timetickets.validation.hoursenteredmorethanavailable"));
|
||||
else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
})
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} precision={1} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
) : null}
|
||||
<Form.Item
|
||||
name="cost_center"
|
||||
label={t("timetickets.fields.cost_center")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select disabled={isShiftTicket}>
|
||||
{isShiftTicket ? (
|
||||
<Select.Option value="timetickets.labels.shift">{t("timetickets.labels.shift")}</Select.Option>
|
||||
) : (
|
||||
emps &&
|
||||
emps.rates.map((item) => (
|
||||
<Select.Option key={item.cost_center}>
|
||||
{item.cost_center === "timetickets.labels.shift"
|
||||
? t(item.cost_center)
|
||||
: bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber
|
||||
? t(`joblines.fields.lbr_types.${item.cost_center.toUpperCase()}`)
|
||||
: item.cost_center}
|
||||
</Select.Option>
|
||||
))
|
||||
)}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
{isShiftTicket ? (
|
||||
<div></div>
|
||||
) : (
|
||||
<Space direction="vertical">
|
||||
{!isShiftTicket ? (
|
||||
<div>
|
||||
<Form.Item
|
||||
name="status"
|
||||
label={t("jobs.fields.status")}
|
||||
initialValue={lineTicketData && lineTicketData.jobs_by_pk.status}
|
||||
label={t("timetickets.fields.actualhrs")}
|
||||
name="actualhrs"
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
@@ -228,35 +137,117 @@ export function TechClockOffButton({
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select>
|
||||
{bodyshop.md_ro_statuses.production_statuses.map((item) => (
|
||||
<Select.Option key={item}></Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<InputNumber min={0} precision={1} />
|
||||
</Form.Item>
|
||||
)}
|
||||
<Button type="primary" htmlType="submit" loading={loading}>
|
||||
{t("general.actions.save")}
|
||||
</Button>
|
||||
<TechJobClockoutDelete completedCallback={completedCallback} timeTicketId={timeTicketId} />
|
||||
</Col>
|
||||
{!isShiftTicket && (
|
||||
<Col span={16}>
|
||||
<LaborAllocationContainer
|
||||
jobid={jobId || null}
|
||||
loading={queryLoading}
|
||||
lineTicketData={lineTicketData}
|
||||
/>
|
||||
</Col>
|
||||
<Form.Item
|
||||
label={t("timetickets.fields.productivehrs")}
|
||||
name="productivehrs"
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
({ getFieldValue }) => ({
|
||||
validator(rule, value) {
|
||||
if (!bodyshop.tt_enforce_hours_for_tech_console) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (!value || getFieldValue("cost_center") === null || !lineTicketData)
|
||||
return Promise.resolve();
|
||||
//Check the cost center,
|
||||
const totals = CalculateAllocationsTotals(
|
||||
bodyshop,
|
||||
lineTicketData.joblines,
|
||||
lineTicketData.timetickets,
|
||||
lineTicketData.jobs_by_pk.lbr_adjustments
|
||||
);
|
||||
const fieldTypeToCheck =
|
||||
bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber ? "mod_lbr_ty" : "cost_center";
|
||||
const costCenterDiff =
|
||||
Math.round(
|
||||
totals.find((total) => total[fieldTypeToCheck] === getFieldValue("cost_center"))
|
||||
?.difference * 10
|
||||
) / 10;
|
||||
if (value > costCenterDiff)
|
||||
return Promise.reject(t("timetickets.validation.hoursenteredmorethanavailable"));
|
||||
else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
})
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} precision={1} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
) : null}
|
||||
<Form.Item
|
||||
name="cost_center"
|
||||
label={t("timetickets.fields.cost_center")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select disabled={isShiftTicket}>
|
||||
{isShiftTicket ? (
|
||||
<Select.Option value="timetickets.labels.shift">{t("timetickets.labels.shift")}</Select.Option>
|
||||
) : (
|
||||
emps &&
|
||||
emps.rates.map((item) => (
|
||||
<Select.Option key={item.cost_center}>
|
||||
{item.cost_center === "timetickets.labels.shift"
|
||||
? t(item.cost_center)
|
||||
: bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber
|
||||
? t(`joblines.fields.lbr_types.${item.cost_center.toUpperCase()}`)
|
||||
: item.cost_center}
|
||||
</Select.Option>
|
||||
))
|
||||
)}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
{isShiftTicket ? (
|
||||
<div></div>
|
||||
) : (
|
||||
<Form.Item
|
||||
name="status"
|
||||
label={t("jobs.fields.status")}
|
||||
initialValue={lineTicketData && lineTicketData.jobs_by_pk.status}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select>
|
||||
{bodyshop.md_ro_statuses.production_statuses.map((item) => (
|
||||
<Select.Option key={item}></Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
)}
|
||||
</Row>
|
||||
<Button type="primary" htmlType="submit" loading={loading}>
|
||||
{t("general.actions.save")}
|
||||
</Button>
|
||||
<TechJobClockoutDelete completedCallback={completedCallback} timeTicketId={timeTicketId} />
|
||||
{!isShiftTicket && (
|
||||
<LaborAllocationContainer jobid={jobId || null} loading={queryLoading} lineTicketData={lineTicketData} />
|
||||
)}
|
||||
</Space>
|
||||
</Form>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover content={overlay} trigger="click">
|
||||
<Popover
|
||||
content={<div style={{ maxHeight: "75vh", overflowY: "auto" }}>{overlay}</div>}
|
||||
trigger="click"
|
||||
getPopupContainer={() => document.querySelector('#time-ticket-modal')}
|
||||
>
|
||||
<Button loading={loading} {...otherBtnProps}>
|
||||
{t("timetickets.actions.clockout")}
|
||||
</Button>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { getFirestore } from "@firebase/firestore";
|
||||
import { getMessaging, getToken, onMessage } from "@firebase/messaging";
|
||||
import { store } from "../redux/store";
|
||||
//import * as amplitude from '@amplitude/analytics-browser';
|
||||
import posthog from 'posthog-js'
|
||||
// import posthog from 'posthog-js'
|
||||
|
||||
const config = JSON.parse(import.meta.env.VITE_APP_FIREBASE_CONFIG);
|
||||
initializeApp(config);
|
||||
@@ -74,7 +74,6 @@ onMessage(messaging, (payload) => {
|
||||
|
||||
export const logImEXEvent = (eventName, additionalParams, stateProp = null) => {
|
||||
try {
|
||||
|
||||
const state = stateProp || store.getState();
|
||||
|
||||
const eventParams = {
|
||||
@@ -99,8 +98,7 @@ export const logImEXEvent = (eventName, additionalParams, stateProp = null) => {
|
||||
// );
|
||||
logEvent(analytics, eventName, eventParams);
|
||||
//amplitude.track(eventName, eventParams);
|
||||
posthog.capture(eventName, eventParams);
|
||||
|
||||
//posthog.capture(eventName, eventParams);
|
||||
} finally {
|
||||
//If it fails, just keep going.
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ import {
|
||||
} from "./user.actions";
|
||||
import UserActionTypes from "./user.types";
|
||||
//import * as amplitude from '@amplitude/analytics-browser';
|
||||
import posthog from 'posthog-js';
|
||||
import posthog from "posthog-js";
|
||||
|
||||
const fpPromise = FingerprintJS.load();
|
||||
|
||||
@@ -269,11 +269,11 @@ export function* signInSuccessSaga({ payload }) {
|
||||
instanceSeg,
|
||||
...(isParts
|
||||
? [
|
||||
InstanceRenderManager({
|
||||
imex: "ImexPartsManagement",
|
||||
rome: "RomePartsManagement"
|
||||
})
|
||||
]
|
||||
InstanceRenderManager({
|
||||
imex: "ImexPartsManagement",
|
||||
rome: "RomePartsManagement"
|
||||
})
|
||||
]
|
||||
: [])
|
||||
];
|
||||
window.$crisp.push(["set", "session:segments", [segs]]);
|
||||
@@ -375,17 +375,31 @@ export function* SetAuthLevelFromShopDetails({ payload }) {
|
||||
const isParts = yield select((state) => state.application.isPartsEntry === true);
|
||||
const instanceSeg = InstanceRenderManager({ imex: "imex", rome: "rome" });
|
||||
|
||||
let featureSegments;
|
||||
if (payload.features?.allAccess === true) {
|
||||
featureSegments = ["allAccess"];
|
||||
} else {
|
||||
const featureKeys = Object.keys(payload.features).filter(
|
||||
(key) =>
|
||||
payload.features[key] === true ||
|
||||
(typeof payload.features[key] === "string" && !isNaN(Date.parse(payload.features[key])))
|
||||
);
|
||||
featureSegments = ["basic", ...featureKeys];
|
||||
}
|
||||
const featureSegments =
|
||||
payload.features?.allAccess === true
|
||||
? ["allAccess"]
|
||||
: [
|
||||
"basic",
|
||||
...Object.keys(payload.features).filter(
|
||||
(key) =>
|
||||
payload.features[key] === true ||
|
||||
(typeof payload.features[key] === "string" && !isNaN(Date.parse(payload.features[key])))
|
||||
)
|
||||
];
|
||||
|
||||
const additionalSegments = [
|
||||
payload.cdk_dealerid && "CDK",
|
||||
payload.pbs_serialnumber && "PBS",
|
||||
// payload.rr_dealerid && "Reynolds",
|
||||
payload.accountingconfig.qbo === true && "QBO",
|
||||
payload.accountingconfig.qbo === false &&
|
||||
!payload.cdk_dealerid &&
|
||||
!payload.pbs_serialnumber &&
|
||||
// !payload.rr_dealerid &&
|
||||
"QBD"
|
||||
].filter(Boolean);
|
||||
|
||||
featureSegments.push(...additionalSegments);
|
||||
|
||||
const regionSeg = payload.region_config ? `region:${payload.region_config}` : null;
|
||||
const segments = [instanceSeg, ...(regionSeg ? [regionSeg] : []), ...featureSegments];
|
||||
|
||||
@@ -1221,7 +1221,7 @@ export const TemplateList = (type, context) => {
|
||||
payments_by_date_excel: {
|
||||
title: i18n.t("reportcenter.templates.payments_by_date_excel"),
|
||||
subject: i18n.t("reportcenter.templates.payments_by_date_excel"),
|
||||
key: "payments_by_date",
|
||||
key: "payments_by_date_excel",
|
||||
reporttype: "excel",
|
||||
disabled: false,
|
||||
rangeFilter: {
|
||||
|
||||
@@ -31,7 +31,8 @@ if (!import.meta.env.DEV) {
|
||||
"Module specifier, 'fs' does not start",
|
||||
"Module specifier, 'zlib' does not start with",
|
||||
"Messaging: This browser doesn't support the API's required to use the Firebase SDK.",
|
||||
"Failed to update a ServiceWorker for scope"
|
||||
"Failed to update a ServiceWorker for scope",
|
||||
"Network Error"
|
||||
],
|
||||
integrations: [
|
||||
// See docs for support of different versions of variation of react router
|
||||
|
||||
@@ -24,11 +24,13 @@ const lightningCssTargets = browserslistToTargets(
|
||||
})
|
||||
);
|
||||
|
||||
const currentDatePST = new Date()
|
||||
.toLocaleDateString("en-US", { timeZone: "America/Los_Angeles", year: "numeric", month: "2-digit", day: "2-digit" })
|
||||
.split("/")
|
||||
.reverse()
|
||||
.join("-");
|
||||
const pstFormatter = new Intl.DateTimeFormat("en-CA", {
|
||||
timeZone: "America/Los_Angeles",
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit"
|
||||
});
|
||||
const currentDatePST = pstFormatter.format(new Date());
|
||||
|
||||
const getFormattedTimestamp = () =>
|
||||
new Date().toLocaleTimeString("en-US", { hour12: true }).replace("AM", "a.m.").replace("PM", "p.m.");
|
||||
|
||||
@@ -117,44 +117,46 @@ async function processShopData(shopsToProcess, start, end, skipUpload, ignoreDat
|
||||
imexshopid: shopid,
|
||||
json: JSON.stringify(carfaxObject, null, 2),
|
||||
filename: `${shopid}_${moment().format("DDMMYYYY_HHMMss")}.json`,
|
||||
count: carfaxObject.job.length
|
||||
count: carfaxObject?.job?.length || 0
|
||||
};
|
||||
|
||||
if (skipUpload) {
|
||||
fs.writeFileSync(`./logs/${jsonObj.filename}`, jsonObj.json);
|
||||
uploadToS3(jsonObj, S3_BUCKET_NAME);
|
||||
} else {
|
||||
await uploadViaSFTP(jsonObj);
|
||||
if (jsonObj.count > 0) {
|
||||
await uploadViaSFTP(jsonObj);
|
||||
|
||||
await sendMexicoBillingEmail({
|
||||
subject: `${shopid.replace(/_/g, "").toUpperCase()}_MexicoRPS_${moment().format("MMDDYYYY")} ROs ${jsonObj.count} Error ${errorCode(jsonObj)}`,
|
||||
text: `Errors:\n${JSON.stringify(
|
||||
erroredJobs.map((ej) => ({
|
||||
jobid: ej.job?.id,
|
||||
error: ej.error
|
||||
})),
|
||||
null,
|
||||
2
|
||||
)}\n\nUploaded:\n${JSON.stringify(
|
||||
{
|
||||
bodyshopid: bodyshop.id,
|
||||
imexshopid: shopid,
|
||||
count: jsonObj.count,
|
||||
filename: jsonObj.filename,
|
||||
result: jsonObj.result
|
||||
},
|
||||
null,
|
||||
2
|
||||
)}`
|
||||
});
|
||||
await sendMexicoBillingEmail({
|
||||
subject: `${shopid.replace(/_/g, "").toUpperCase()}_MexicoRPS_${moment().format("MMDDYYYY")} ROs ${jsonObj.count} Error ${errorCode(jsonObj)}`,
|
||||
text: `Errors:\n${JSON.stringify(
|
||||
erroredJobs.map((ej) => ({
|
||||
jobid: ej.job?.id,
|
||||
error: ej.error
|
||||
})),
|
||||
null,
|
||||
2
|
||||
)}\n\nUploaded:\n${JSON.stringify(
|
||||
{
|
||||
bodyshopid: bodyshop.id,
|
||||
imexshopid: shopid,
|
||||
count: jsonObj.count,
|
||||
filename: jsonObj.filename,
|
||||
result: jsonObj.result
|
||||
},
|
||||
null,
|
||||
2
|
||||
)}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
allJSONResults.push({
|
||||
jsonObj.count > 0 && allJSONResults.push({
|
||||
bodyshopid: bodyshop.id,
|
||||
imexshopid: shopid,
|
||||
count: jsonObj.count,
|
||||
filename: jsonObj.filename,
|
||||
result: jsonObj.result
|
||||
result: jsonObj.result || "No Upload Result Available"
|
||||
});
|
||||
|
||||
logger.log("CARFAX-RPS-end-shop-extract", "DEBUG", "api", bodyshop.id, {
|
||||
@@ -234,11 +236,10 @@ const CreateRepairOrderTag = (job, errorCallback) => {
|
||||
const ret = {
|
||||
ro_number: crypto.createHash("md5").update(job.id, "utf8").digest("hex"),
|
||||
v_vin: job.v_vin || "",
|
||||
v_year: job.v_model_yr
|
||||
? parseInt(job.v_model_yr.match(/\d/g))
|
||||
? parseInt(job.v_model_yr.match(/\d/g).join(""), 10)
|
||||
: ""
|
||||
: "",
|
||||
v_year: (() => {
|
||||
const y = parseInt(job.v_model_yr);
|
||||
return isNaN(y) ? null : y < 100 ? y + (y >= (new Date().getFullYear() + 1) % 100 ? 1900 : 2000) : y;
|
||||
})(),
|
||||
v_make: job.v_makedesc || "",
|
||||
v_model: job.v_model || "",
|
||||
|
||||
|
||||
@@ -160,40 +160,42 @@ async function processShopData(shopsToProcess, start, end, skipUpload, ignoreDat
|
||||
imexshopid: shopid,
|
||||
json: JSON.stringify(carfaxObject, null, 2),
|
||||
filename: `${shopid}_${moment().format("DDMMYYYY_HHMMss")}.json`,
|
||||
count: carfaxObject.job.length
|
||||
count: carfaxObject?.job?.length || 0
|
||||
};
|
||||
|
||||
if (skipUpload) {
|
||||
fs.writeFileSync(`./logs/${jsonObj.filename}`, jsonObj.json);
|
||||
uploadToS3(jsonObj);
|
||||
} else {
|
||||
await uploadViaSFTP(jsonObj);
|
||||
if (jsonObj.count > 0) {
|
||||
await uploadViaSFTP(jsonObj);
|
||||
|
||||
await sendMexicoBillingEmail({
|
||||
subject: `${shopid.replace(/_/g, "").toUpperCase()}_Mexico${InstanceManager({
|
||||
imex: "IO",
|
||||
rome: "RO"
|
||||
})}_${moment().format("MMDDYYYY")} ROs ${jsonObj.count} Error ${errorCode(jsonObj)}`,
|
||||
text: `Errors:\n${JSON.stringify(
|
||||
erroredJobs.map((ej) => ({
|
||||
ro_number: ej.job?.ro_number,
|
||||
jobid: ej.job?.id,
|
||||
error: ej.error
|
||||
})),
|
||||
null,
|
||||
2
|
||||
)}\n\nUploaded:\n${JSON.stringify(
|
||||
{
|
||||
bodyshopid: bodyshop.id,
|
||||
imexshopid: shopid,
|
||||
count: jsonObj.count,
|
||||
filename: jsonObj.filename,
|
||||
result: jsonObj.result
|
||||
},
|
||||
null,
|
||||
2
|
||||
)}`
|
||||
});
|
||||
await sendMexicoBillingEmail({
|
||||
subject: `${shopid.replace(/_/g, "").toUpperCase()}_Mexico${InstanceManager({
|
||||
imex: "IO",
|
||||
rome: "RO"
|
||||
})}_${moment().format("MMDDYYYY")} ROs ${jsonObj.count} Error ${errorCode(jsonObj)}`,
|
||||
text: `Errors:\n${JSON.stringify(
|
||||
erroredJobs.map((ej) => ({
|
||||
ro_number: ej.job?.ro_number,
|
||||
jobid: ej.job?.id,
|
||||
error: ej.error
|
||||
})),
|
||||
null,
|
||||
2
|
||||
)}\n\nUploaded:\n${JSON.stringify(
|
||||
{
|
||||
bodyshopid: bodyshop.id,
|
||||
imexshopid: shopid,
|
||||
count: jsonObj.count,
|
||||
filename: jsonObj.filename,
|
||||
result: jsonObj.result
|
||||
},
|
||||
null,
|
||||
2
|
||||
)}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
allJSONResults.push({
|
||||
@@ -201,7 +203,7 @@ async function processShopData(shopsToProcess, start, end, skipUpload, ignoreDat
|
||||
imexshopid: shopid,
|
||||
count: jsonObj.count,
|
||||
filename: jsonObj.filename,
|
||||
result: jsonObj.result
|
||||
result: jsonObj.result || "No Upload Result Available"
|
||||
});
|
||||
|
||||
logger.log("CARFAX-end-shop-extract", "DEBUG", "api", bodyshop.id, {
|
||||
@@ -286,11 +288,10 @@ const CreateRepairOrderTag = (job, errorCallback) => {
|
||||
const ret = {
|
||||
ro_number: crypto.createHash("md5").update(job.ro_number, "utf8").digest("hex"),
|
||||
v_vin: job.v_vin || "",
|
||||
v_year: job.v_model_yr
|
||||
? parseInt(job.v_model_yr.match(/\d/g))
|
||||
? parseInt(job.v_model_yr.match(/\d/g).join(""), 10)
|
||||
: ""
|
||||
: "",
|
||||
v_year: (() => {
|
||||
const y = parseInt(job.v_model_yr);
|
||||
return isNaN(y) ? null : y < 100 ? y + (y >= (new Date().getFullYear() + 1) % 100 ? 1900 : 2000) : y;
|
||||
})(),
|
||||
v_make: job.v_make_desc || "",
|
||||
v_model: job.v_model_desc || "",
|
||||
|
||||
|
||||
@@ -55,7 +55,9 @@ exports.default = async (req, res) => {
|
||||
"patrick.fic@convenient-brands.com",
|
||||
"bradley.rhoades@convenient-brands.com",
|
||||
"jrome@rometech.com",
|
||||
"ivana@imexsystems.ca"
|
||||
"ivana@imexsystems.ca",
|
||||
"support@imexsystems.ca",
|
||||
"sarah@rometech.com"
|
||||
],
|
||||
subject: `RO Usage Report - ${moment().format("MM/DD/YYYY")}`,
|
||||
text: `
|
||||
|
||||
@@ -134,13 +134,16 @@ const insertUserAssociation = async (uid, email, shopId) => {
|
||||
|
||||
/**
|
||||
* PATCH handler for updating bodyshop fields.
|
||||
* Allows patching: shopname, address1, address2, city, state, zip_post, country, email, timezone, phone, logo_img_path
|
||||
* Allows patching: shopname, address1, address2, city, state, zip_post, country, email, timezone, phone
|
||||
* Also allows updating logo_img_path via a simple logoUrl string, which is expanded to the full object.
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const patchPartsManagementProvisioning = async (req, res) => {
|
||||
const { id } = req.params;
|
||||
|
||||
// Fields that can be directly patched 1:1
|
||||
const allowedFields = [
|
||||
"shopname",
|
||||
"address1",
|
||||
@@ -151,31 +154,58 @@ const patchPartsManagementProvisioning = async (req, res) => {
|
||||
"country",
|
||||
"email",
|
||||
"timezone",
|
||||
"phone",
|
||||
"logo_img_path"
|
||||
"phone"
|
||||
// NOTE: logo_img_path is handled separately via logoUrl
|
||||
];
|
||||
|
||||
const updateFields = {};
|
||||
|
||||
// Copy over simple scalar fields if present
|
||||
for (const field of allowedFields) {
|
||||
if (req.body[field] !== undefined) {
|
||||
updateFields[field] = req.body[field];
|
||||
}
|
||||
}
|
||||
|
||||
// Handle logo update via a simple href string, same behavior as provision route
|
||||
if (typeof req.body.logo_img_path === "string") {
|
||||
const trimmed = req.body.logo_img_path.trim();
|
||||
if (trimmed) {
|
||||
updateFields.logo_img_path = {
|
||||
src: trimmed,
|
||||
width: "",
|
||||
height: "",
|
||||
headerMargin: DefaultNewShop.logo_img_path.headerMargin
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(updateFields).length === 0) {
|
||||
return res.status(400).json({ error: "No valid fields provided for update." });
|
||||
}
|
||||
|
||||
// Check that the bodyshop has an external_shop_id before allowing patch
|
||||
try {
|
||||
// Fetch the bodyshop by id
|
||||
const shopResp = await client.request(
|
||||
`query GetBodyshop($id: uuid!) { bodyshops_by_pk(id: $id) { id external_shop_id } }`,
|
||||
`query GetBodyshop($id: uuid!) {
|
||||
bodyshops_by_pk(id: $id) {
|
||||
id
|
||||
external_shop_id
|
||||
}
|
||||
}`,
|
||||
{ id }
|
||||
);
|
||||
|
||||
if (!shopResp.bodyshops_by_pk?.external_shop_id) {
|
||||
return res.status(400).json({ error: "Cannot patch: bodyshop does not have an external_shop_id." });
|
||||
}
|
||||
} catch (err) {
|
||||
return res.status(500).json({ error: "Failed to validate bodyshop external_shop_id.", detail: err });
|
||||
return res.status(500).json({
|
||||
error: "Failed to validate bodyshop external_shop_id.",
|
||||
detail: err
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await client.request(UPDATE_BODYSHOP_BY_ID, { id, fields: updateFields });
|
||||
if (!resp.update_bodyshops_by_pk) {
|
||||
|
||||
@@ -81,8 +81,8 @@ const alternateTransportChangedBuilder = (data) => {
|
||||
* @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}}
|
||||
*/
|
||||
const billPostedBuilder = (data) => {
|
||||
const facing = data?.data?.isinhouse ? "in-house" : "vendor";
|
||||
const body = `An ${facing} ${data?.data?.is_credit_memo ? "credit memo" : "bill"} has been posted.`.trim();
|
||||
const facing = data?.data?.isinhouse ? "An In House" : "A Vendor";
|
||||
const body = `${facing} ${data?.data?.is_credit_memo ? "credit memo" : "bill"} has been posted.`.trim();
|
||||
|
||||
return buildNotification(data, "notifications.job.billPosted", body, {
|
||||
isInHouse: data?.data?.isinhouse,
|
||||
|
||||
Reference in New Issue
Block a user