Merge branch 'release/2023-04-28' into feature/america

This commit is contained in:
Patrick Fic
2023-04-27 15:21:42 -07:00
73 changed files with 3394 additions and 922 deletions

View File

@@ -143,13 +143,16 @@
}
}
//Update row highlighting on production board.
//Update row highlighting on production board.
.ant-table-tbody > tr.ant-table-row:hover > td {
background: #eaeaea !important;
}
.job-line-manual{
.job-line-manual {
color: tomato;
font-style: italic;
}
td.ant-table-column-sort {
background-color: transparent;
}

View File

@@ -7,7 +7,9 @@ import { createStructuredSelector } from "reselect";
import { selectBreadcrumbs } from "../../redux/application/application.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import GlobalSearch from "../global-search/global-search.component";
import GlobalSearchOs from "../global-search/global-search-os.component";
import "./breadcrumbs.styles.scss";
import { useTreatments } from "@splitsoftware/splitio-react";
const mapStateToProps = createStructuredSelector({
breadcrumbs: selectBreadcrumbs,
@@ -15,6 +17,12 @@ const mapStateToProps = createStructuredSelector({
});
export function BreadCrumbs({ breadcrumbs, bodyshop }) {
const { OpenSearch } = useTreatments(
["OpenSearch"],
{},
bodyshop && bodyshop.imexshopid
);
return (
<Row className="breadcrumb-container">
<Col xs={24} sm={24} md={16}>
@@ -38,7 +46,7 @@ export function BreadCrumbs({ breadcrumbs, bodyshop }) {
</Breadcrumb>
</Col>
<Col xs={24} sm={24} md={8}>
<GlobalSearch />
{OpenSearch.treatment === "on" ? <GlobalSearchOs /> : <GlobalSearch />}
</Col>
</Row>
);

View File

@@ -10,7 +10,10 @@ export default function CABCpvrtCalculator({ disabled, form }) {
const handleFinish = async (values) => {
logImEXEvent("job_ca_bc_pvrt_calculate");
form.setFieldsValue({ ca_bc_pvrt: ((values.rate||0) * (values.days||0)).toFixed(2) });
form.setFieldsValue({
ca_bc_pvrt: ((values.rate || 0) * (values.days || 0)).toFixed(2),
});
form.setFields([{ name: "ca_bc_pvrt", touched: true }]);
setVisibility(false);
};

View File

@@ -0,0 +1,216 @@
import { AutoComplete, Divider, Input, Space } from "antd";
import axios from "axios";
import _ from "lodash";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link, useHistory } from "react-router-dom";
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
import OwnerNameDisplay, {
OwnerNameDisplayFunction,
} from "../owner-name-display/owner-name-display.component";
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
export default function GlobalSearchOs() {
const { t } = useTranslation();
const history = useHistory();
const [loading, setLoading] = useState(false);
const [data, setData] = useState(false);
const executeSearch = async (v) => {
if (v && v && v !== "" && v.length >= 3) {
try {
setLoading(true);
const searchData = await axios.post("/search", {
search: v,
});
const resultsByType = {
payments: [],
jobs: [],
bills: [],
owners: [],
vehicles: [],
};
searchData.data.hits.hits.forEach((hit) => {
resultsByType[hit._index].push(hit._source);
});
setData([
{
label: renderTitle(t("menus.header.search.jobs")),
options: resultsByType.jobs.map((job) => {
return {
key: job.id,
value: job.ro_number || "N/A",
label: (
<Link to={`/manage/jobs/${job.id}`}>
<Space size="small" split={<Divider type="vertical" />}>
<strong>{job.ro_number || t("general.labels.na")}</strong>
<span>{`${job.status || ""}`}</span>
<span>
<OwnerNameDisplay ownerObject={job} />
</span>
<span>{`${job.v_model_yr || ""} ${
job.v_make_desc || ""
} ${job.v_model_desc || ""}`}</span>
<span>{`${job.clm_no || ""}`}</span>
</Space>
</Link>
),
};
}),
},
{
label: renderTitle(t("menus.header.search.owners")),
options: resultsByType.owners.map((owner) => {
return {
key: owner.id,
value: OwnerNameDisplayFunction(owner),
label: (
<Link to={`/manage/owners/${owner.id}`}>
<Space
size="small"
split={<Divider type="vertical" />}
wrap
>
<span>
<OwnerNameDisplay ownerObject={owner} />
</span>
<PhoneNumberFormatter>
{owner.ownr_ph1}
</PhoneNumberFormatter>
<PhoneNumberFormatter>
{owner.ownr_ph2}
</PhoneNumberFormatter>
</Space>
</Link>
),
};
}),
},
{
label: renderTitle(t("menus.header.search.vehicles")),
options: resultsByType.vehicles.map((vehicle) => {
return {
key: vehicle.id,
value: `${vehicle.v_model_yr || ""} ${
vehicle.v_make_desc || ""
} ${vehicle.v_model_desc || ""}`,
label: (
<Link to={`/manage/vehicles/${vehicle.id}`}>
<Space size="small" split={<Divider type="vertical" />}>
<span>
{`${vehicle.v_model_yr || ""} ${
vehicle.v_make_desc || ""
} ${vehicle.v_model_desc || ""}`}
</span>
<span>{vehicle.plate_no || ""}</span>
<span>
<VehicleVinDisplay>
{vehicle.v_vin || ""}
</VehicleVinDisplay>
</span>
</Space>
</Link>
),
};
}),
},
{
label: renderTitle(t("menus.header.search.payments")),
options: resultsByType.payments.map((payment) => {
return {
key: payment.id,
value: `${payment.job?.ro_number} ${payment.amount}`,
label: (
<Link to={`/manage/jobs/${payment.job?.id}`}>
<Space size="small" split={<Divider type="vertical" />}>
<span>{payment.paymentnum}</span>
<span>{payment.job?.ro_number}</span>
<span>{payment.memo || ""}</span>
<span>{payment.amount || ""}</span>
<span>{payment.transactionid || ""}</span>
</Space>
</Link>
),
};
}),
},
{
label: renderTitle(t("menus.header.search.bills")),
options: resultsByType.bills.map((bill) => {
return {
key: bill.id,
value: `${bill.invoice_number} - ${bill.vendor.name}`,
label: (
<Link to={`/manage/bills?billid=${bill.id}`}>
<Space size="small" split={<Divider type="vertical" />}>
<span>{bill.invoice_number}</span>
<span>{bill.vendor.name}</span>
<span>{bill.date}</span>
</Space>
</Link>
),
};
}),
},
// {
// label: renderTitle(t("menus.header.search.phonebook")),
// options: resultsByType.search_phonebook.map((pb) => {
// return {
// key: pb.id,
// value: `${pb.firstname || ""} ${pb.lastname || ""} ${
// pb.company || ""
// }`,
// label: (
// <Link to={`/manage/phonebook?phonebookentry=${pb.id}`}>
// <Space size="small" split={<Divider type="vertical" />}>
// <span>{`${pb.firstname || ""} ${pb.lastname || ""} ${
// pb.company || ""
// }`}</span>
// <PhoneNumberFormatter>{pb.phone1}</PhoneNumberFormatter>
// <span>{pb.email}</span>
// </Space>
// </Link>
// ),
// };
// }),
// },
]);
} catch (error) {
console.log("Error while fetching search results", error);
} finally {
setLoading(false);
}
}
};
const debouncedExecuteSearch = _.debounce(executeSearch, 750);
const handleSearch = (value) => {
debouncedExecuteSearch(value);
};
const renderTitle = (title) => {
return <span>{title}</span>;
};
return (
<AutoComplete
options={data}
onSearch={handleSearch}
defaultActiveFirstOption
onSelect={(val, opt) => {
history.push(opt.label.props.to);
}}
onClear={() => setData([])}
>
<Input.Search
size="large"
placeholder={t("general.labels.globalsearch")}
enterButton
allowClear
loading={loading}
/>
</AutoComplete>
);
}

View File

@@ -8,7 +8,7 @@ import { GLOBAL_SEARCH_QUERY } from "../../graphql/search.queries";
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
import AlertComponent from "../alert/alert.component";
import OwnerNameDisplay, {
OwnerNameDisplayFunction
OwnerNameDisplayFunction,
} from "../owner-name-display/owner-name-display.component";
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
export default function GlobalSearch() {
@@ -18,11 +18,18 @@ export default function GlobalSearch() {
useLazyQuery(GLOBAL_SEARCH_QUERY);
const executeSearch = (v) => {
if (v && v.variables.search && v.variables.search !== "") callSearch(v);
if (
v &&
v.variables.search &&
v.variables.search !== "" &&
v.variables.search.length >= 3
)
callSearch(v);
};
const debouncedExecuteSearch = _.debounce(executeSearch, 750);
const handleSearch = (value) => {
console.log("Handle Search");
debouncedExecuteSearch({ variables: { search: value } });
};
@@ -37,7 +44,7 @@ export default function GlobalSearch() {
options: data.search_jobs.map((job) => {
return {
key: job.id,
value: job.ro_number,
value: job.ro_number || "N/A",
label: (
<Link to={`/manage/jobs/${job.id}`}>
<Space size="small" split={<Divider type="vertical" />}>

View File

@@ -82,7 +82,7 @@ export function JobsDetailRates({ jobRO, form, job, bodyshop }) {
>
<CurrencyInput disabled={jobRO || bodyshop.cdk_dealerid} />
</Form.Item>
<Space align="end">
<Space align="center">
<Form.Item label={t("jobs.fields.ca_bc_pvrt")} name="ca_bc_pvrt">
<CurrencyInput disabled={jobRO} min={0} />
</Form.Item>

View File

@@ -34,8 +34,8 @@ function JobsDocumentsComponent({
const fileType = DetermineFileType(value.type);
if (value.type.startsWith("image")) {
acc.images.push({
src: GenerateSrcUrl(value),
thumbnail: GenerateThumbUrl(value),
// src: GenerateSrcUrl(value),
src: GenerateThumbUrl(value),
// src: GenerateSrcUrl(value),
// thumbnail: GenerateThumbUrl(value),
fullsize: GenerateSrcUrl(value),

View File

@@ -1,7 +1,7 @@
import React, { useEffect } from "react";
import { Gallery } from "react-grid-gallery";
import { useTranslation } from "react-i18next";
import { GenerateSrcUrl, GenerateThumbUrl } from "./job-documents.utility";
import { GenerateThumbUrl } from "./job-documents.utility";
function JobsDocumentGalleryExternal({
data,
@@ -15,8 +15,8 @@ function JobsDocumentGalleryExternal({
let documents = data.reduce((acc, value) => {
if (value.type.startsWith("image")) {
acc.push({
src: GenerateSrcUrl(value),
thumbnail: GenerateThumbUrl(value),
//src: GenerateSrcUrl(value),
src: GenerateThumbUrl(value),
thumbnailHeight: 225,
thumbnailWidth: 225,
isSelected: false,

View File

@@ -52,11 +52,15 @@ function JobDocumentsLocalGalleryExternal({
val.type.mime &&
val.type.mime.startsWith("image")
) {
acc.push(val);
acc.push({ ...val, src: val.thumbnail });
}
return acc;
}, [])
: [];
console.log(
"🚀 ~ file: jobs-documents-local-gallery.external.component.jsx:48 ~ useEffect ~ documents:",
documents
);
setgalleryImages(documents);
}, [allMedia, jobId, setgalleryImages, t]);

View File

@@ -34,7 +34,7 @@ export function NoteUpsertModalContainer({
const [updateNote] = useMutation(UPDATE_NOTE);
const { visible, context, actions } = noteUpsertModal;
const { jobId, existingNote } = context;
const { jobId, existingNote, text } = context;
const { refetch } = actions;
const [form] = Form.useForm();
@@ -45,8 +45,12 @@ export function NoteUpsertModalContainer({
form.setFieldsValue(existingNote);
} else if (!existingNote && visible) {
form.resetFields();
if (text) {
form.setFieldValue("text", text);
}
}
}, [existingNote, form, visible]);
}, [existingNote, form, visible, text]);
const handleFinish = async (formValues) => {
const { relatedros, ...values } = formValues;
@@ -82,6 +86,7 @@ export function NoteUpsertModalContainer({
{ ...values, jobid: jobId, created_by: currentUser.email },
],
},
refetchQueries: ["QUERY_NOTES_BY_JOB_PK"],
});
if (AdditionalNoteInserts.length > 0) {

View File

@@ -201,6 +201,7 @@ export function PartsOrderListTableComponent({
subject: record.return
? Templates.parts_return_slip.subject
: Templates.parts_order.subject,
to: record.vendor.email,
}}
id={job.id}
/>
@@ -296,7 +297,6 @@ export function PartsOrderListTableComponent({
sortOrder:
state.sortedInfo.columnKey === "quantity" && state.sortedInfo.order,
},
{
title: t("parts_orders.fields.act_price"),
dataIndex: "act_price",

View File

@@ -1,12 +1,23 @@
import Icon from "@ant-design/icons";
import { useMutation } from "@apollo/client";
import { Button, Input, Popover } from "antd";
import { Button, Input, Popover, Space } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { FaRegStickyNote } from "react-icons/fa";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { UPDATE_JOB } from "../../graphql/jobs.queries";
export default function ProductionListColumnProductionNote({ record }) {
import { setModalContext } from "../../redux/modals/modals.actions";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
const mapStateToProps = createStructuredSelector({});
const mapDispatchToProps = (dispatch) => ({
setNoteUpsertContext: (context) =>
dispatch(setModalContext({ context: context, modal: "noteUpsert" })),
});
function ProductionListColumnProductionNote({ record, setNoteUpsertContext }) {
const { t } = useTranslation();
const [note, setNote] = useState(
@@ -60,12 +71,26 @@ export default function ProductionListColumnProductionNote({ record }) {
// onPressEnter={handleSaveNote}
autoFocus
allowClear
style={{ marginBottom: "1em" }}
/>
<div>
<Button onClick={handleSaveNote}>
<Space>
<Button onClick={handleSaveNote} type="primary">
{t("general.actions.save")}
</Button>
</div>
<Button
onClick={() => {
setVisible(false);
setNoteUpsertContext({
context: {
jobId: record.id,
text: note,
},
});
}}
>
{t("notes.actions.savetojobnotes")}
</Button>
</Space>
</div>
}
trigger={["click"]}
@@ -85,3 +110,8 @@ export default function ProductionListColumnProductionNote({ record }) {
</Popover>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(ProductionListColumnProductionNote);

View File

@@ -92,7 +92,11 @@ export function ReportCenterModalComponent({ reportCenterModal }) {
to: values.to,
subject: Templates[values.key]?.subject,
},
values.sendby === "email" ? "e" : "p",
values.sendbyexcel === "excel"
? "x"
: values.sendby === "email"
? "e"
: "p",
id
);
setLoading(false);
@@ -250,15 +254,38 @@ export function ReportCenterModalComponent({ reportCenterModal }) {
ranges={DatePIckerRanges}
/>
</Form.Item>
<Form.Item
label={t("general.labels.sendby")}
name="sendby"
initialValue="print"
>
<Radio.Group>
<Radio value="email">{t("general.labels.email")}</Radio>
<Radio value="print">{t("general.labels.print")}</Radio>
</Radio.Group>
<Form.Item style={{ margin: 0, padding: 0 }} dependencies={["key"]}>
{() => {
const key = form.getFieldValue("key");
//Kind of Id
const reporttype = Templates[key] && Templates[key].reporttype;
if (reporttype === "excel")
return (
<Form.Item
label={t("general.labels.sendby")}
name="sendbyexcel"
initialValue="excel"
>
<Radio.Group>
<Radio value="excel">{t("general.labels.excel")}</Radio>
</Radio.Group>
</Form.Item>
);
if (reporttype !== "excel")
return (
<Form.Item
label={t("general.labels.sendby")}
name="sendby"
initialValue="print"
>
<Radio.Group>
<Radio value="email">{t("general.labels.email")}</Radio>
<Radio value="print">{t("general.labels.print")}</Radio>
</Radio.Group>
</Form.Item>
);
}}
</Form.Item>
<div

View File

@@ -0,0 +1,39 @@
import Dinero from "dinero.js";
const CustomTooltip = ({ active, payload, label }) => {
if (active && payload && payload.length) {
return (
<div
style={{
backgroundColor: "white",
border: "1px solid gray",
padding: "0.5rem",
}}
>
<p style={{ margin: "0" }}>{label}</p>
{payload.map((data, index) => {
if (data.dataKey === "sales" || data.dataKey === "accSales")
return (
<p
style={{ margin: "10px 0", color: data.color }}
key={index}
>{`${data.name} : ${Dinero({
amount: Math.round(data.value * 100),
}).toFormat()}`}</p>
);
return (
<p
style={{ margin: "10px 0", color: data.color }}
key={index}
>{`${data.name} : ${data.value}`}</p>
);
})}
</div>
);
}
return null;
};
export default CustomTooltip;

View File

@@ -1,3 +1,4 @@
import Dinero from "dinero.js";
import { Card } from "antd";
import moment from "moment";
import React from "react";
@@ -18,6 +19,7 @@ import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import * as Utils from "../scoreboard-targets-table/scoreboard-targets-table.util";
import _ from "lodash";
import CustomTooltip from "./chart-custom-tooltip";
const graphProps = {
strokeWidth: 3,
@@ -44,14 +46,19 @@ export function ScoreboardChart({ sbEntriesByDate, bodyshop }) {
return {
bodyhrs: dayAcc.bodyhrs + dayVal.bodyhrs,
painthrs: dayAcc.painthrs + dayVal.painthrs,
sales:
dayAcc.painthrs +
dayVal.job.job_totals.totals.subtotal.amount / 100 +
2500,
};
},
{ bodyhrs: 0, painthrs: 0 }
{ bodyhrs: 0, painthrs: 0, sales: 0 }
);
} else {
dayhrs = {
bodyhrs: 0,
painthrs: 0,
sales: 0,
};
}
@@ -73,6 +80,13 @@ export function ScoreboardChart({ sbEntriesByDate, bodyshop }) {
: dayhrs.painthrs + dayhrs.bodyhrs,
1
),
sales: _.round(dayhrs.sales, 2),
accSales: _.round(
acc.length > 0
? acc[acc.length - 1].accSales + dayhrs.sales
: dayhrs.sales,
2
),
};
return [...acc, theValue];
@@ -87,22 +101,27 @@ export function ScoreboardChart({ sbEntriesByDate, bodyshop }) {
>
<CartesianGrid stroke="#f5f5f5" />
<XAxis dataKey="date" strokeWidth={graphProps.strokeWidth} />
<YAxis strokeWidth={graphProps.strokeWidth} />
<Tooltip />
<Legend />
<Area
type="monotone"
name="Accumulated Hours"
dataKey="accHrs"
fill="lightgreen"
stroke="green"
<YAxis
strokeWidth={graphProps.strokeWidth}
// allowDataOverflow
dataKey="sales"
yAxisId="right"
tickFormatter={(value) =>
Dinero({ amount: Math.round(value * 100) }).toFormat()
}
orientation="right"
/>
<YAxis yAxisId="left" strokeWidth={graphProps.strokeWidth} />
<Tooltip content={<CustomTooltip />} />
<Legend />
<Bar
name="Body Hours"
dataKey="bodyHrs"
stackId="day"
barSize={20}
fill="darkblue"
yAxisId="left"
/>
<Bar
name="Paint Hours"
@@ -110,12 +129,42 @@ export function ScoreboardChart({ sbEntriesByDate, bodyshop }) {
stackId="day"
barSize={20}
fill="darkred"
yAxisId="left"
/>
<Line
name="Target Hours"
type="monotone"
dataKey="accTargetHrs"
stroke="#ff7300"
yAxisId="left"
strokeWidth={graphProps.strokeWidth}
/>
<Area
type="monotone"
name="MTD Hours"
dataKey="accHrs"
fill="lightblue"
stroke="blue"
yAxisId="left"
/>
{
// <Area
// type="monotone"
// name="MTD Sales"
// dataKey="accSales"
// fill="lightgreen"
// stroke="green"
// yAxisId="right"
// />
}
<Bar
name="Sales"
dataKey="sales"
stackId="day"
barSize={20}
fill="darkgreen"
yAxisId="right"
strokeWidth={graphProps.strokeWidth}
/>
</ComposedChart>

View File

@@ -241,9 +241,11 @@ export default function ScoreboardTimeTickets() {
);
ret.totalEffieciencyOverPeriod =
(totalActualAndProductive.totalOverPeriod /
totalActualAndProductive.actualTotalOverPeriod) *
100;
totalActualAndProductive.actualTotalOverPeriod
? (totalActualAndProductive.totalOverPeriod /
totalActualAndProductive.actualTotalOverPeriod) *
100
: 0;
roundObject(ret);
roundObject(totals);

View File

@@ -116,7 +116,7 @@ export function ScoreboardTicketsStats({ data, bodyshop }) {
<Col span={12}>
<Statistic
title={t("scoreboard.labels.efficiencyoverperiod")}
value={`${data.totalEffieciencyOverPeriod}%`}
value={`${data.totalEffieciencyOverPeriod || 0}%`}
/>
</Col>
</Row>

View File

@@ -11,13 +11,13 @@ import {
} from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import FormItemEmail from "../form-items-formatted/email-form-item.component";
import PhoneFormItem, {
PhoneItemFormatterValidation,
} from "../form-items-formatted/phone-form-item.component";
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import FormItemEmail from "../form-items-formatted/email-form-item.component";
import momentTZ from "moment-timezone";
const timeZonesList = momentTZ.tz.names();
@@ -551,6 +551,13 @@ export default function ShopInfoGeneral({ form }) {
>
<CurrencyInput />
</Form.Item>
<Form.Item
name={["use_paint_scale_data"]}
label={t("bodyshop.fields.use_paint_scale_data")}
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item
name={["attach_pdf_to_email"]}
label={t("bodyshop.fields.attach_pdf_to_email")}

View File

@@ -0,0 +1,131 @@
import { useQuery } from "@apollo/client";
import { Card, Col, Space, Statistic, Typography } from "antd";
import moment from "moment";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { QUERY_TIME_TICKETS_TECHNICIAN_IN_RANGE } from "../../graphql/timetickets.queries";
import { selectTechnician } from "../../redux/tech/tech.selectors";
import AlertComponent from "../alert/alert.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
const { Title } = Typography;
const mapStateToProps = createStructuredSelector({
technician: selectTechnician,
});
const mapDispatchToProps = (dispatch) => ({});
const TechJobStatistics = ({ technician }) => {
const { t } = useTranslation();
const startDate = moment().startOf("week");
const endDate = moment().endOf("week");
const { loading, error, data } = useQuery(
QUERY_TIME_TICKETS_TECHNICIAN_IN_RANGE,
{
variables: {
start: startDate.format("YYYY-MM-DD"),
end: endDate.format("YYYY-MM-DD"),
fixedStart: moment().startOf("month").format("YYYY-MM-DD"),
fixedEnd: moment().endOf("month").format("YYYY-MM-DD"),
employeeid: technician.id,
},
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
}
);
const totals = useMemo(() => {
if (data && data.timetickets && data.fixedperiod) {
const week = data.timetickets.reduce(
(acc, val) => {
acc.productivehrs = acc.productivehrs + val.productivehrs;
acc.actualhrs = acc.actualhrs + val.actualhrs;
return acc;
},
{ productivehrs: 0, actualhrs: 0 }
);
const month = data.fixedperiod.reduce(
(acc, val) => {
acc.productivehrs = acc.productivehrs + val.productivehrs;
acc.actualhrs = acc.actualhrs + val.actualhrs;
return acc;
},
{ productivehrs: 0, actualhrs: 0 }
);
return {
week,
month,
};
}
return {
week: { productivehrs: 0, actualhrs: 0 },
month: { productivehrs: 0, actualhrs: 0 },
};
}, [data]);
if (loading) return <LoadingSpinner />;
if (error) return <AlertComponent message={error.message} type="error" />;
return (
<Card title={t("scoreboard.labels.productivestatistics")}>
<Space size={100}>
<Col>
<Title level={5}>{t("scoreboard.labels.thisweek")}</Title>
<Space size={20}>
<Statistic
title={t("timetickets.fields.productivehrs")}
value={totals.week.productivehrs.toFixed(2)}
/>
<Statistic
title={t("timetickets.fields.actualhrs")}
value={totals.week.actualhrs.toFixed(2)}
/>
<Statistic
title={t("timetickets.labels.efficiency")}
value={
totals.week.actualhrs
? `${(
(totals.week.productivehrs / totals.week.actualhrs) *
100
).toFixed(2)}%`
: "0%"
}
/>
</Space>
</Col>
<Col>
<Title level={5}>{t("scoreboard.labels.thismonth")}</Title>
<Space size={20}>
<Statistic
title={t("timetickets.fields.productivehrs")}
value={totals.month.productivehrs.toFixed(2)}
/>
<Statistic
title={t("timetickets.fields.actualhrs")}
value={totals.month.actualhrs.toFixed(2)}
/>
<Statistic
title={t("timetickets.labels.efficiency")}
value={
totals.month.actualhrs
? `${(
(totals.month.productivehrs / totals.month.actualhrs) *
100
).toFixed(2)}%`
: "0%"
}
/>
</Space>
</Col>
</Space>
</Card>
);
};
export default connect(mapStateToProps, mapDispatchToProps)(TechJobStatistics);

View File

@@ -29,7 +29,17 @@ export function TechSider({ technician, techLogout }) {
};
return (
<Sider collapsible collapsed={collapsed} onCollapse={onCollapse}>
<Sider
style={{
height: "100vh",
position: "sticky",
top: 0,
left: 0,
}}
collapsible
collapsed={collapsed}
onCollapse={onCollapse}
>
<Menu theme="dark" defaultSelectedKeys={["1"]} mode="inline">
<Menu.Item
key="1"

View File

@@ -69,6 +69,7 @@ export const QUERY_BILLS_BY_JOBID = gql`
vendor {
id
name
email
}
order_date
deliver_by
@@ -104,6 +105,7 @@ export const QUERY_BILLS_BY_JOBID = gql`
vendor {
id
name
email
}
total
invoice_number

View File

@@ -117,6 +117,7 @@ export const QUERY_BODYSHOP = gql`
md_parts_scan
enforce_conversion_category
tt_enforce_hours_for_tech_console
use_paint_scale_data
employees {
user_email
id

View File

@@ -1279,7 +1279,7 @@ export const SEARCH_JOBS_BY_ID_FOR_AUTOCOMPLETE = gql`
export const SEARCH_FOR_JOBS = gql`
query SEARCH_FOR_JOBS($search: String!) {
search_jobs(args: { search: $search }) {
search_jobs(args: { search: $search }, limit: 25) {
id
ro_number
ownr_fn

View File

@@ -4,7 +4,15 @@ export const INSERT_NEW_NOTE = gql`
mutation INSERT_NEW_NOTE($noteInput: [notes_insert_input!]!) {
insert_notes(objects: $noteInput) {
returning {
created_at
created_by
critical
id
jobid
private
text
updated_at
audit
}
}
}
@@ -15,8 +23,8 @@ export const QUERY_NOTES_BY_JOB_PK = gql`
jobs_by_pk(id: $id) {
id
ro_number
vehicle{
jobs{
vehicle {
jobs {
id
ro_number
status

View File

@@ -19,6 +19,7 @@ export const QUERY_SCOREBOARD = gql`
v_make_desc
v_model_desc
v_model_yr
job_totals
}
}
}

View File

@@ -2,22 +2,20 @@ import { gql } from "@apollo/client";
export const GLOBAL_SEARCH_QUERY = gql`
query GLOBAL_SEARCH_QUERY($search: String) {
search_jobs(args: { search: $search }) {
search_jobs(args: { search: $search }, limit: 25) {
id
ro_number
status
clm_no
v_model_yr
v_model_desc
v_make_desc
v_color
ownr_fn
ownr_ln
ownr_co_nm
}
search_owners(args: { search: $search }) {
search_owners(args: { search: $search }, limit: 25) {
id
ownr_fn
ownr_ln
@@ -25,7 +23,7 @@ export const GLOBAL_SEARCH_QUERY = gql`
ownr_ph1
ownr_ph2
}
search_vehicles(args: { search: $search }) {
search_vehicles(args: { search: $search }, limit: 25) {
id
v_model_yr
v_model_desc
@@ -34,7 +32,7 @@ export const GLOBAL_SEARCH_QUERY = gql`
v_vin
plate_no
}
search_payments(args: { search: $search }) {
search_payments(args: { search: $search }, limit: 25) {
id
amount
paymentnum
@@ -45,7 +43,7 @@ export const GLOBAL_SEARCH_QUERY = gql`
memo
transactionid
}
search_bills(args: { search: $search }) {
search_bills(args: { search: $search }, limit: 25) {
id
date
invoice_number
@@ -54,7 +52,7 @@ export const GLOBAL_SEARCH_QUERY = gql`
name
}
}
search_phonebook(args: { search: $search }) {
search_phonebook(args: { search: $search }, limit: 25) {
id
firstname
lastname

View File

@@ -59,6 +59,81 @@ export const QUERY_TIME_TICKETS_IN_RANGE = gql`
}
`;
export const QUERY_TIME_TICKETS_TECHNICIAN_IN_RANGE = gql`
query QUERY_TIME_TICKETS_TECHNICIAN_IN_RANGE(
$employeeid: uuid!
$start: date!
$end: date!
$fixedStart: date!
$fixedEnd: date!
) {
timetickets(
where: {
date: { _gte: $start, _lte: $end }
employeeid: { _eq: $employeeid }
}
order_by: { date: desc_nulls_first }
) {
actualhrs
ciecacode
clockoff
clockon
cost_center
created_at
date
id
rate
productivehrs
memo
jobid
flat_rate
job {
id
ro_number
}
employeeid
employee {
id
employee_number
first_name
last_name
}
}
fixedperiod: timetickets(
where: {
date: { _gte: $fixedStart, _lte: $fixedEnd }
employeeid: { _eq: $employeeid }
}
order_by: { date: desc_nulls_first }
) {
actualhrs
ciecacode
clockoff
clockon
cost_center
created_at
date
id
rate
productivehrs
memo
jobid
flat_rate
job {
id
ro_number
}
employeeid
employee {
id
employee_number
first_name
last_name
}
}
}
`;
export const QUERY_TIME_TICKETS_IN_RANGE_SB = gql`
query QUERY_TIME_TICKETS_IN_RANGE_SB(
$start: date!

View File

@@ -39,7 +39,7 @@ export function AllJobs({ setBreadcrumbs, setSelectedHeader }) {
...(statusFilters ? { statusList: JSON.parse(statusFilters) } : {}),
order: [
{
[sortcolumn || "created_at"]:
[sortcolumn || "ro_number"]:
sortorder && sortorder !== "false"
? sortorder === "descend"
? "desc"

View File

@@ -52,6 +52,7 @@ import AuditTrailMapping from "../../utils/AuditTrailMappings";
import { insertAuditTrail } from "../../redux/application/application.actions";
import JobsDocumentsLocalGallery from "../../components/jobs-documents-local-gallery/jobs-documents-local-gallery.container";
import UndefinedToNull from "../../utils/undefinedtonull";
import NoteUpsertModalComponent from "../../components/note-upsert-modal/note-upsert-modal.container";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -97,7 +98,11 @@ export function JobsDetailPage({
variables: {
jobId: job.id,
job: {
...UndefinedToNull(values, ["alt_transport", "category", "referral_source"]),
...UndefinedToNull(values, [
"alt_transport",
"category",
"referral_source",
]),
parts_tax_rates: {
...job.parts_tax_rates,
...values.parts_tax_rates,
@@ -231,6 +236,7 @@ export function JobsDetailPage({
<ScheduleJobModalContainer />
<JobReconciliationModal />
<JobLineUpsertModalContainer />
<NoteUpsertModalComponent />
<Form
form={form}
name="JobDetailForm"

View File

@@ -2,10 +2,12 @@ import { Divider } from "antd";
import React from "react";
import TechClockInFormContainer from "../../components/tech-job-clock-in-form/tech-job-clock-in-form.container";
import TechClockedInList from "../../components/tech-job-clocked-in-list/tech-job-clocked-in-list.component";
import TechJobStatistics from "../../components/tech-job-statistics/tech-job-statistics.component";
export default function TechClockComponent() {
return (
<div>
<TechJobStatistics />
<TechClockInFormContainer />
<Divider />
<TechClockedInList />

View File

@@ -1,9 +1,10 @@
.tech-content-container {
overflow-y: auto;
overflow-y: visible;
padding: 1rem;
background: #fff;
}
.tech-layout-container {
height: 100vh;
position: relative;
min-height: 100vh;
}

View File

@@ -548,6 +548,7 @@
"tt_allow_post_to_invoiced": "Allow Time Tickets to be posted to Invoiced & Exported Jobs",
"tt_enforce_hours_for_tech_console": "Restrict Claimable hours from Tech Console",
"use_fippa": "Use FIPPA for Names on Generated Documents?",
"use_paint_scale_data": "Use Paint Scale Data for Job Costing?",
"uselocalmediaserver": "Use Local Media Server?",
"website": "Website",
"zip_post": "Zip/Postal Code"
@@ -1030,6 +1031,7 @@
"created_at": "Created At",
"email": "Email",
"errors": "Errors",
"excel": "Excel",
"exceptiontitle": "An error has occurred.",
"friday": "Friday",
"globalsearch": "Global Search",
@@ -1977,7 +1979,8 @@
"actions": "Actions",
"deletenote": "Delete Note",
"edit": "Edit Note",
"new": "New Note"
"new": "New Note",
"savetojobnotes": "Save to Job Notes"
},
"errors": {
"inserting": "Error inserting note. {{error}}"
@@ -2243,6 +2246,7 @@
"csi_invitation": "CSI Invitation",
"csi_invitation_action": "CSI Invite",
"diagnostic_authorization": "Diagnostic Authorization",
"dms_posting_sheet": "DMS Posting Sheet",
"envelope_return_address": "#10 Envelope Return Address Label",
"estimate": "Estimate Only",
"estimate_detail": "Estimate Details",
@@ -2461,6 +2465,8 @@
"export_payables": "Export Log - Payables",
"export_payments": "Export Log - Payments",
"export_receivables": "Export Log - Receivables",
"exported_gsr_by_ro": "Exported Gross Sales - Excel",
"exported_gsr_by_ro_labor": "Exported Gross Sales (Labor) - Excel",
"gsr_by_atp": "",
"gsr_by_ats": "Gross Sales by ATS",
"gsr_by_category": "Gross Sales by Category",

View File

@@ -548,6 +548,7 @@
"tt_allow_post_to_invoiced": "",
"tt_enforce_hours_for_tech_console": "",
"use_fippa": "",
"use_paint_scale_data": "",
"uselocalmediaserver": "",
"website": "",
"zip_post": ""
@@ -1030,6 +1031,7 @@
"created_at": "",
"email": "",
"errors": "",
"excel": "",
"exceptiontitle": "",
"friday": "",
"globalsearch": "",
@@ -1977,7 +1979,8 @@
"actions": "Comportamiento",
"deletenote": "Borrar nota",
"edit": "Editar nota",
"new": "Nueva nota"
"new": "Nueva nota",
"savetojobnotes": ""
},
"errors": {
"inserting": ""
@@ -2243,6 +2246,7 @@
"csi_invitation": "",
"csi_invitation_action": "",
"diagnostic_authorization": "",
"dms_posting_sheet": "",
"envelope_return_address": "",
"estimate": "",
"estimate_detail": "",
@@ -2461,6 +2465,8 @@
"export_payables": "",
"export_payments": "",
"export_receivables": "",
"exported_gsr_by_ro": "",
"exported_gsr_by_ro_labor": "",
"gsr_by_atp": "",
"gsr_by_ats": "",
"gsr_by_category": "",

View File

@@ -548,6 +548,7 @@
"tt_allow_post_to_invoiced": "",
"tt_enforce_hours_for_tech_console": "",
"use_fippa": "",
"use_paint_scale_data": "",
"uselocalmediaserver": "",
"website": "",
"zip_post": ""
@@ -1030,6 +1031,7 @@
"created_at": "",
"email": "",
"errors": "",
"excel": "",
"exceptiontitle": "",
"friday": "",
"globalsearch": "",
@@ -1977,7 +1979,8 @@
"actions": "actes",
"deletenote": "Supprimer la note",
"edit": "Note éditée",
"new": "Nouvelle note"
"new": "Nouvelle note",
"savetojobnotes": ""
},
"errors": {
"inserting": ""
@@ -2243,6 +2246,7 @@
"csi_invitation": "",
"csi_invitation_action": "",
"diagnostic_authorization": "",
"dms_posting_sheet": "",
"envelope_return_address": "",
"estimate": "",
"estimate_detail": "",
@@ -2461,6 +2465,8 @@
"export_payables": "",
"export_payments": "",
"export_receivables": "",
"exported_gsr_by_ro": "",
"exported_gsr_by_ro_labor": "",
"gsr_by_atp": "",
"gsr_by_ats": "",
"gsr_by_category": "",

View File

@@ -20,7 +20,6 @@ export const TemplateList = (type, context) => {
disabled: false,
group: "authorization",
},
fippa_authorization: {
title: i18n.t("printcenter.jobs.fippa_authorization"),
description: "CASL Authorization",
@@ -101,7 +100,6 @@ export const TemplateList = (type, context) => {
disabled: false,
group: "ro",
},
job_notes: {
title: i18n.t("printcenter.jobs.job_notes"),
description: "All Jobs Notes",
@@ -330,7 +328,6 @@ export const TemplateList = (type, context) => {
disabled: false,
group: "post",
},
vehicle_delivery_check: {
title: i18n.t("printcenter.jobs.vehicle_delivery_check"),
description: "All Jobs Notes",
@@ -339,7 +336,6 @@ export const TemplateList = (type, context) => {
disabled: false,
group: "post",
},
guarantee: {
title: i18n.t("printcenter.jobs.guarantee"),
description: "All Jobs Notes",
@@ -496,6 +492,14 @@ export const TemplateList = (type, context) => {
disabled: false,
group: "financial",
},
dms_posting_sheet: {
title: i18n.t("printcenter.jobs.dms_posting_sheet"),
description: "DMS Posting Sheet",
subject: i18n.t("printcenter.jobs.dms_posting_sheet"),
key: "dms_posting_sheet",
disabled: false,
group: "financial",
},
}
: {}),
...(!type || type === "job_special"
@@ -622,7 +626,6 @@ export const TemplateList = (type, context) => {
},
group: "sales",
},
hours_sold_detail_closed_ins_co: {
title: i18n.t(
"reportcenter.templates.hours_sold_detail_closed_ins_co"
@@ -640,7 +643,6 @@ export const TemplateList = (type, context) => {
},
group: "sales",
},
hours_sold_summary_closed: {
title: i18n.t("reportcenter.templates.hours_sold_summary_closed"),
description: "",
@@ -654,7 +656,6 @@ export const TemplateList = (type, context) => {
},
group: "sales",
},
hours_sold_summary_closed_ins_co: {
title: i18n.t(
"reportcenter.templates.hours_sold_summary_closed_ins_co"
@@ -672,7 +673,6 @@ export const TemplateList = (type, context) => {
},
group: "sales",
},
hours_sold_detail_open: {
title: i18n.t("reportcenter.templates.hours_sold_detail_open"),
description: "",
@@ -686,7 +686,6 @@ export const TemplateList = (type, context) => {
},
group: "sales",
},
hours_sold_detail_open_ins_co: {
title: i18n.t(
"reportcenter.templates.hours_sold_detail_open_ins_co"
@@ -704,7 +703,6 @@ export const TemplateList = (type, context) => {
},
group: "sales",
},
hours_sold_summary_open: {
title: i18n.t("reportcenter.templates.hours_sold_summary_open"),
description: "",
@@ -718,7 +716,6 @@ export const TemplateList = (type, context) => {
},
group: "sales",
},
hours_sold_summary_open_ins_co: {
title: i18n.t(
"reportcenter.templates.hours_sold_summary_open_ins_co"
@@ -736,7 +733,6 @@ export const TemplateList = (type, context) => {
},
group: "sales",
},
hours_sold_detail_closed_csr: {
title: i18n.t(
"reportcenter.templates.hours_sold_detail_closed_csr"
@@ -1090,7 +1086,6 @@ export const TemplateList = (type, context) => {
},
group: "customers",
},
schedule: {
title: i18n.t("reportcenter.templates.schedule"),
subject: i18n.t("reportcenter.templates.schedule"),
@@ -1102,7 +1097,6 @@ export const TemplateList = (type, context) => {
},
group: "customers",
},
timetickets: {
title: i18n.t("reportcenter.templates.timetickets"),
subject: i18n.t("reportcenter.templates.timetickets"),
@@ -1126,7 +1120,6 @@ export const TemplateList = (type, context) => {
title: i18n.t("reportcenter.templates.attendance_detail"),
subject: i18n.t("reportcenter.templates.attendance_detail"),
key: "attendance_detail",
disabled: false,
rangeFilter: {
object: i18n.t("reportcenter.labels.objects.timetickets"),
@@ -1138,7 +1131,6 @@ export const TemplateList = (type, context) => {
title: i18n.t("reportcenter.templates.attendance_summary"),
subject: i18n.t("reportcenter.templates.attendance_summary"),
key: "attendance_summary",
disabled: false,
rangeFilter: {
object: i18n.t("reportcenter.labels.objects.timetickets"),
@@ -1158,7 +1150,6 @@ export const TemplateList = (type, context) => {
},
group: "payroll",
},
timetickets_summary: {
title: i18n.t("reportcenter.templates.timetickets_summary"),
subject: i18n.t("reportcenter.templates.timetickets_summary"),
@@ -1171,7 +1162,6 @@ export const TemplateList = (type, context) => {
},
group: "payroll",
},
estimator_detail: {
title: i18n.t("reportcenter.templates.estimator_detail"),
description: "",
@@ -1224,7 +1214,6 @@ export const TemplateList = (type, context) => {
},
group: "purchases",
},
void_ros: {
title: i18n.t("reportcenter.templates.void_ros"),
description: "",
@@ -1329,7 +1318,6 @@ export const TemplateList = (type, context) => {
},
group: "sales",
},
gsr_by_estimator: {
title: i18n.t("reportcenter.templates.gsr_by_estimator"),
description: "",
@@ -1814,7 +1802,9 @@ export const TemplateList = (type, context) => {
group: "jobs",
},
purchase_return_ratio_grouped_by_vendor_detail: {
title: i18n.t("reportcenter.templates.purchase_return_ratio_grouped_by_vendor_detail"),
title: i18n.t(
"reportcenter.templates.purchase_return_ratio_grouped_by_vendor_detail"
),
subject: i18n.t(
"reportcenter.templates.purchase_return_ratio_grouped_by_vendor_detail"
),
@@ -1828,7 +1818,9 @@ export const TemplateList = (type, context) => {
group: "purchases",
},
purchase_return_ratio_grouped_by_vendor_summary: {
title: i18n.t("reportcenter.templates.purchase_return_ratio_grouped_by_vendor_summary"),
title: i18n.t(
"reportcenter.templates.purchase_return_ratio_grouped_by_vendor_summary"
),
subject: i18n.t(
"reportcenter.templates.purchase_return_ratio_grouped_by_vendor_summary"
),
@@ -1843,9 +1835,7 @@ export const TemplateList = (type, context) => {
},
production_over_time: {
title: i18n.t("reportcenter.templates.production_over_time"),
subject: i18n.t(
"reportcenter.templates.production_over_time"
),
subject: i18n.t("reportcenter.templates.production_over_time"),
key: "production_over_time",
//idtype: "vendor",
disabled: false,
@@ -1857,9 +1847,7 @@ export const TemplateList = (type, context) => {
},
customer_list: {
title: i18n.t("reportcenter.templates.customer_list"),
subject: i18n.t(
"reportcenter.templates.customer_list"
),
subject: i18n.t("reportcenter.templates.customer_list"),
key: "customer_list",
//idtype: "vendor",
disabled: false,
@@ -1869,6 +1857,32 @@ export const TemplateList = (type, context) => {
},
group: "customers",
},
exported_gsr_by_ro: {
title: i18n.t("reportcenter.templates.exported_gsr_by_ro"),
subject: i18n.t("reportcenter.templates.exported_gsr_by_ro"),
key: "exported_gsr_by_ro",
//idtype: "vendor",
reporttype: "excel",
disabled: false,
rangeFilter: {
object: i18n.t("reportcenter.labels.objects.jobs"),
field: i18n.t("jobs.fields.date_exported"),
},
group: "sales",
},
exported_gsr_by_ro_labor: {
title: i18n.t("reportcenter.templates.exported_gsr_by_ro_labor"),
subject: i18n.t("reportcenter.templates.exported_gsr_by_ro_labor"),
key: "exported_gsr_by_ro_labor",
//idtype: "vendor",
reporttype: "excel",
disabled: false,
rangeFilter: {
object: i18n.t("reportcenter.labels.objects.jobs"),
field: i18n.t("jobs.fields.date_exported"),
},
group: "sales",
},
}
: {}),
...(!type || type === "courtesycarcontract"
@@ -1963,7 +1977,6 @@ export const TemplateList = (type, context) => {
},
}
: {}),
...(!type || type === "production"
? {
production_by_last_name: {