Merged in release/2024-01-19 (pull request #1184)

Release/2024 01 19

Approved-by: Dave Richer
This commit is contained in:
Allan Carr
2024-01-19 16:59:22 +00:00
committed by Dave Richer
29 changed files with 881 additions and 508 deletions

View File

@@ -445,7 +445,7 @@ function Header({
))} ))}
</Menu.SubMenu> </Menu.SubMenu>
<Menu.Item style={{marginLeft: 'auto'}} key="profile"> <Menu.Item style={{marginLeft: 'auto'}} key="profile">
<Tooltip title="A faster more modern ImEX Online is ready for you to try! You can switch back at any time."> <Tooltip title="A more modern ImEX Online is ready for you to try! You can switch back at any time.">
<InfoCircleOutlined/> <InfoCircleOutlined/>
<span style={{marginRight: 8}}>Try the new ImEX Online</span> <span style={{marginRight: 8}}>Try the new ImEX Online</span>
<Switch <Switch

View File

@@ -7,21 +7,31 @@ import { connect } from "react-redux";
import { useHistory } from "react-router"; import { useHistory } from "react-router";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { UPDATE_JOB_LINES_IOU } from "../../graphql/jobs-lines.queries"; import { UPDATE_JOB_LINES_IOU } from "../../graphql/jobs-lines.queries";
import { selectTechnician } from "../../redux/tech/tech.selectors";
import { import {
selectBodyshop, selectBodyshop,
selectCurrentUser, selectCurrentUser,
} from "../../redux/user/user.selectors"; } from "../../redux/user/user.selectors";
import { CreateIouForJob } from "../jobs-detail-header-actions/jobs-detail-header-actions.duplicate.util"; import { CreateIouForJob } from "../jobs-detail-header-actions/jobs-detail-header-actions.duplicate.util";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
technician: selectTechnician,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
}); });
export default connect(mapStateToProps, mapDispatchToProps)(JobCreateIOU); export default connect(mapStateToProps, mapDispatchToProps)(JobCreateIOU);
export function JobCreateIOU({ bodyshop, currentUser, job, selectedJobLines }) { export function JobCreateIOU({
bodyshop,
currentUser,
job,
selectedJobLines,
technician,
}) {
const { t } = useTranslation(); const { t } = useTranslation();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const client = useApolloClient(); const client = useApolloClient();
@@ -79,13 +89,19 @@ export function JobCreateIOU({ bodyshop, currentUser, job, selectedJobLines }) {
title={t("jobs.labels.createiouwarning")} title={t("jobs.labels.createiouwarning")}
onConfirm={handleCreateIou} onConfirm={handleCreateIou}
disabled={ disabled={
!selectedJobLines || selectedJobLines.length === 0 || !job.converted !selectedJobLines ||
selectedJobLines.length === 0 ||
!job.converted ||
technician
} }
> >
<Button <Button
loading={loading} loading={loading}
disabled={ disabled={
!selectedJobLines || selectedJobLines.length === 0 || !job.converted !selectedJobLines ||
selectedJobLines.length === 0 ||
!job.converted ||
technician
} }
> >
{t("jobs.actions.createiou")} {t("jobs.actions.createiou")}

View File

@@ -1,15 +1,37 @@
import React from "react";
import { Card } from "antd"; import { Card } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { selectTechnician } from "../../redux/tech/tech.selectors";
export default function JobDetailCardTemplate({ const mapStateToProps = createStructuredSelector({
technician: selectTechnician,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(JobDetailCardTemplate);
export function JobDetailCardTemplate({
loading, loading,
title, title,
extraLink, extraLink,
technician,
...otherProps ...otherProps
}) { }) {
const { t } = useTranslation();
let extra; let extra;
if (extraLink) extra = { extra: <Link to={extraLink}>More</Link> }; if (extraLink && !technician)
extra = {
extra: <Link to={extraLink}>{t("jobs.labels.cards.more")}</Link>,
};
return ( return (
<Card <Card
size="small" size="small"

View File

@@ -19,10 +19,13 @@ export default function JobsCreateOwnerInfoNewComponent() {
label={t("owners.fields.ownr_ln")} label={t("owners.fields.ownr_ln")}
name={["owner", "data", "ownr_ln"]} name={["owner", "data", "ownr_ln"]}
rules={[ rules={[
{ ({ getFieldValue }) => ({
required: state.owner.new, required:
state.owner.new &&
(!getFieldValue(["owner", "data", "ownr_co_nm"]) ||
getFieldValue(["owner", "data", "ownr_co_nm"]) === ""),
//message: t("general.validation.required"), //message: t("general.validation.required"),
}, }),
]} ]}
> >
<Input disabled={!state.owner.new} /> <Input disabled={!state.owner.new} />
@@ -31,10 +34,13 @@ export default function JobsCreateOwnerInfoNewComponent() {
label={t("owners.fields.ownr_fn")} label={t("owners.fields.ownr_fn")}
name={["owner", "data", "ownr_fn"]} name={["owner", "data", "ownr_fn"]}
rules={[ rules={[
{ ({ getFieldValue }) => ({
required: state.owner.new, required:
state.owner.new &&
(!getFieldValue(["owner", "data", "ownr_co_nm"]) ||
getFieldValue(["owner", "data", "ownr_co_nm"]) === ""),
//message: t("general.validation.required"), //message: t("general.validation.required"),
}, }),
]} ]}
> >
<Input disabled={!state.owner.new} /> <Input disabled={!state.owner.new} />
@@ -51,6 +57,17 @@ export default function JobsCreateOwnerInfoNewComponent() {
<Form.Item <Form.Item
label={t("owners.fields.ownr_co_nm")} label={t("owners.fields.ownr_co_nm")}
name={["owner", "data", "ownr_co_nm"]} name={["owner", "data", "ownr_co_nm"]}
rules={[
({ getFieldValue }) => ({
required:
state.owner.new &&
(!getFieldValue(["owner", "data", "ownr_ln"]) ||
!getFieldValue(["owner", "data", "ownr_fn"]) ||
getFieldValue(["owner", "data", "ownr_ln"]) === "" ||
getFieldValue(["owner", "data", "ownr_fn"]) === ""),
//message: t("general.validation.required"),
}),
]}
> >
<Input disabled={!state.owner.new} /> <Input disabled={!state.owner.new} />
</Form.Item> </Form.Item>

View File

@@ -15,6 +15,7 @@ import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateTimeFormatter } from "../../utils/DateFormatter"; import { DateTimeFormatter } from "../../utils/DateFormatter";
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
import ChatOpenButton from "../chat-open-button/chat-open-button.component"; import ChatOpenButton from "../chat-open-button/chat-open-button.component";
import DataLabel from "../data-label/data-label.component"; import DataLabel from "../data-label/data-label.component";
import JobAltTransportChange from "../job-at-change/job-at-change.component"; import JobAltTransportChange from "../job-at-change/job-at-change.component";
@@ -160,19 +161,35 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
<Card <Card
style={{ height: "100%" }} style={{ height: "100%" }}
title={ title={
<Link to={disabled ? "#" : `/manage/owners/${job.owner.id}`}> disabled ? (
<>
{ownerTitle.length > 0
? ownerTitle
: t("owner.labels.noownerinfo")}
</>
) : (
<Link to={`/manage/owners/${job.owner.id}`}>
{ownerTitle.length > 0 {ownerTitle.length > 0
? ownerTitle ? ownerTitle
: t("owner.labels.noownerinfo")} : t("owner.labels.noownerinfo")}
</Link> </Link>
)
} }
> >
<div> <div>
<DataLabel key="2" label={t("jobs.fields.ownr_ph1")}> <DataLabel key="2" label={t("jobs.fields.ownr_ph1")}>
{disabled ? (
<PhoneNumberFormatter>{job.ownr_ph1}</PhoneNumberFormatter>
) : (
<ChatOpenButton phone={job.ownr_ph1} jobid={job.id} /> <ChatOpenButton phone={job.ownr_ph1} jobid={job.id} />
)}
</DataLabel> </DataLabel>
<DataLabel key="22" label={t("jobs.fields.ownr_ph2")}> <DataLabel key="22" label={t("jobs.fields.ownr_ph2")}>
{disabled ? (
<PhoneNumberFormatter>{job.ownr_ph2}</PhoneNumberFormatter>
) : (
<ChatOpenButton phone={job.ownr_ph2} jobid={job.id} /> <ChatOpenButton phone={job.ownr_ph2} jobid={job.id} />
)}
</DataLabel> </DataLabel>
<DataLabel key="3" label={t("owners.fields.address")}> <DataLabel key="3" label={t("owners.fields.address")}>
{`${job.ownr_addr1 || ""} ${job.ownr_addr2 || ""} ${ {`${job.ownr_addr1 || ""} ${job.ownr_addr2 || ""} ${
@@ -180,7 +197,11 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
} ${job.ownr_st || ""} ${job.ownr_zip || ""}`} } ${job.ownr_st || ""} ${job.ownr_zip || ""}`}
</DataLabel> </DataLabel>
<DataLabel key="4" label={t("owners.fields.ownr_ea")}> <DataLabel key="4" label={t("owners.fields.ownr_ea")}>
{job.ownr_ea || ""} {disabled ? (
<>{job.ownr_ea || ""}</>
) : job.ownr_ea ? (
<a href={`mailto:${job.ownr_ea}`}>{job.ownr_ea}</a>
) : null}
</DataLabel> </DataLabel>
{job.owner?.tax_number && ( {job.owner?.tax_number && (
<DataLabel key="5" label={t("owners.fields.tax_number")}> <DataLabel key="5" label={t("owners.fields.tax_number")}>
@@ -195,17 +216,19 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
style={{ height: "100%" }} style={{ height: "100%" }}
title={ title={
job.vehicle ? ( job.vehicle ? (
<Link disabled ? (
to={ <>
disabled {vehicleTitle.length > 0
? "#" ? vehicleTitle
: job.vehicle && `/manage/vehicles/${job.vehicle.id}` : t("vehicles.labels.novehinfo")}{" "}
} </>
> ) : (
<Link to={job.vehicle && `/manage/vehicles/${job.vehicle.id}`}>
{vehicleTitle.length > 0 {vehicleTitle.length > 0
? vehicleTitle ? vehicleTitle
: t("vehicles.labels.novehinfo")} : t("vehicles.labels.novehinfo")}
</Link> </Link>
)
) : ( ) : (
<span></span> <span></span>
) )
@@ -223,7 +246,9 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
</VehicleVinDisplay> </VehicleVinDisplay>
{bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid ? ( {bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid ? (
job.v_vin?.length !== 17 ? ( job.v_vin?.length !== 17 ? (
<WarningFilled style={{ color: "tomato", marginLeft: ".3rem" }} /> <WarningFilled
style={{ color: "tomato", marginLeft: ".3rem" }}
/>
) : null ) : null
) : null} ) : null}
</DataLabel> </DataLabel>
@@ -231,7 +256,7 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
{job.regie_number || t("general.labels.na")} {job.regie_number || t("general.labels.na")}
</DataLabel> </DataLabel>
<DataLabel label={t("jobs.labels.relatedros")}> <DataLabel label={t("jobs.labels.relatedros")}>
<JobsRelatedRos jobid={job.id} job={job} /> <JobsRelatedRos jobid={job.id} job={job} disabled={disabled} />
</DataLabel> </DataLabel>
{job.vehicle && job.vehicle.notes && ( {job.vehicle && job.vehicle.notes && (
<DataLabel <DataLabel

View File

@@ -10,9 +10,10 @@ import { Link, useHistory, useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { pageLimit } from "../../utils/config";
import useLocalStorage from "../../utils/useLocalStorage";
import StartChatButton from "../chat-open-button/chat-open-button.component"; import StartChatButton from "../chat-open-button/chat-open-button.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import {pageLimit} from "../../utils/config";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser //currentUser: selectCurrentUser
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -25,6 +26,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
const search = queryString.parse(useLocation().search); const search = queryString.parse(useLocation().search);
const [openSearchResults, setOpenSearchResults] = useState([]); const [openSearchResults, setOpenSearchResults] = useState([]);
const [searchLoading, setSearchLoading] = useState(false); const [searchLoading, setSearchLoading] = useState(false);
const [filter, setFilter] = useLocalStorage("filter_jobs_all", null);
const { page, sortcolumn, sortorder } = search; const { page, sortcolumn, sortorder } = search;
const history = useHistory(); const history = useHistory();
@@ -93,6 +95,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
render: (text, record) => { render: (text, record) => {
return record.status || t("general.labels.na"); return record.status || t("general.labels.na");
}, },
filteredValue: filter?.status || null,
filters: bodyshop.md_ro_statuses.statuses.map((s) => { filters: bodyshop.md_ro_statuses.statuses.map((s) => {
return { text: s, value: [s] }; return { text: s, value: [s] };
}), }),
@@ -189,6 +192,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
} else { } else {
delete search.statusFilters; delete search.statusFilters;
} }
setFilter(filters);
history.push({ search: queryString.stringify(search) }); history.push({ search: queryString.stringify(search) });
}; };

View File

@@ -1,8 +1,8 @@
import { import {
SyncOutlined, BranchesOutlined,
ExclamationCircleFilled, ExclamationCircleFilled,
PauseCircleOutlined, PauseCircleOutlined,
BranchesOutlined, SyncOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
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";
@@ -14,9 +14,10 @@ import { Link, useHistory, useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { QUERY_ALL_ACTIVE_JOBS } from "../../graphql/jobs.queries"; import { QUERY_ALL_ACTIVE_JOBS } from "../../graphql/jobs.queries";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import { onlyUnique } from "../../utils/arrayHelper";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { alphaSort } from "../../utils/sorters"; import { onlyUnique } from "../../utils/arrayHelper";
import { alphaSort, statusSort } from "../../utils/sorters";
import useLocalStorage from "../../utils/useLocalStorage";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import ChatOpenButton from "../chat-open-button/chat-open-button.component"; import ChatOpenButton from "../chat-open-button/chat-open-button.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
@@ -39,10 +40,8 @@ export function JobsList({ bodyshop }) {
nextFetchPolicy: "network-only", nextFetchPolicy: "network-only",
}); });
const [state, setState] = useState({ const [state, setState] = useState({ sortedInfo: {} });
sortedInfo: {}, const [filter, setFilter] = useLocalStorage("filter_jobs_list", null);
filteredInfo: { text: "" },
});
const { t } = useTranslation(); const { t } = useTranslation();
const history = useHistory(); const history = useHistory();
@@ -91,7 +90,8 @@ export function JobsList({ bodyshop }) {
: []; : [];
const handleTableChange = (pagination, filters, sorter) => { const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); setState({ ...state, sortedInfo: sorter });
setFilter(filters);
}; };
const handleOnRowClick = (record) => { const handleOnRowClick = (record) => {
@@ -117,7 +117,6 @@ export function JobsList({ bodyshop }) {
parseInt((b.ro_number || "0").replace(/\D/g, "")), parseInt((b.ro_number || "0").replace(/\D/g, "")),
sortOrder: sortOrder:
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order, state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
render: (text, record) => ( render: (text, record) => (
<Link <Link
to={"/manage/jobs/" + record.id} to={"/manage/jobs/" + record.id}
@@ -145,7 +144,6 @@ export function JobsList({ bodyshop }) {
dataIndex: "owner", dataIndex: "owner",
key: "owner", key: "owner",
ellipsis: true, ellipsis: true,
responsive: ["md"], responsive: ["md"],
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln), sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
sortOrder: sortOrder:
@@ -185,16 +183,15 @@ export function JobsList({ bodyshop }) {
<ChatOpenButton phone={record.ownr_ph2} jobid={record.id} /> <ChatOpenButton phone={record.ownr_ph2} jobid={record.id} />
), ),
}, },
{ {
title: t("jobs.fields.status"), title: t("jobs.fields.status"),
dataIndex: "status", dataIndex: "status",
key: "status", key: "status",
ellipsis: true, ellipsis: true,
sorter: (a, b) => alphaSort(a.status, b.status), sorter: (a, b) => alphaSort(a.status, b.status),
sortOrder: sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order, state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
filteredValue: filter?.status || null,
filters: filters:
(jobs && (jobs &&
jobs jobs
@@ -205,7 +202,14 @@ export function JobsList({ bodyshop }) {
text: s || "No Status*", text: s || "No Status*",
value: [s], value: [s],
}; };
})) || })
.sort((a, b) =>
statusSort(
a.text,
b.text,
bodyshop.md_ro_statuses.active_statuses
)
)) ||
[], [],
onFilter: (value, record) => value.includes(record.status), onFilter: (value, record) => value.includes(record.status),
}, },
@@ -262,6 +266,7 @@ export function JobsList({ bodyshop }) {
dataIndex: "ins_co_nm", dataIndex: "ins_co_nm",
key: "ins_co_nm", key: "ins_co_nm",
ellipsis: true, ellipsis: true,
filteredValue: filter?.ins_co_nm || null,
filters: filters:
(jobs && (jobs &&
jobs jobs
@@ -269,10 +274,11 @@ export function JobsList({ bodyshop }) {
.filter(onlyUnique) .filter(onlyUnique)
.map((s) => { .map((s) => {
return { return {
text: s, text: s || "No Ins. Co.*",
value: [s], value: [s],
}; };
})) || })
.sort((a, b) => alphaSort(a.text, b.text))) ||
[], [],
onFilter: (value, record) => value.includes(record.ins_co_nm), onFilter: (value, record) => value.includes(record.ins_co_nm),
responsive: ["md"], responsive: ["md"],
@@ -283,7 +289,6 @@ export function JobsList({ bodyshop }) {
key: "clm_total", key: "clm_total",
responsive: ["md"], responsive: ["md"],
ellipsis: true, ellipsis: true,
sorter: (a, b) => a.clm_total - b.clm_total, sorter: (a, b) => a.clm_total - b.clm_total,
sortOrder: sortOrder:
state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order, state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order,
@@ -294,10 +299,11 @@ export function JobsList({ bodyshop }) {
{ {
title: t("jobs.labels.estimator"), title: t("jobs.labels.estimator"),
dataIndex: "jobs.labels.estimator", dataIndex: "jobs.labels.estimator",
key: "jobs.labels.estimator", key: "estimator",
ellipsis: true, ellipsis: true,
responsive: ["xl"], responsive: ["xl"],
filterSearch: true, filterSearch: true,
filteredValue: filter?.estimator || null,
filters: filters:
(jobs && (jobs &&
jobs jobs
@@ -305,10 +311,11 @@ export function JobsList({ bodyshop }) {
.filter(onlyUnique) .filter(onlyUnique)
.map((s) => { .map((s) => {
return { return {
text: s || "N/A", text: s || "No Estimator*",
value: [s], value: [s],
}; };
})) || })
.sort((a, b) => alphaSort(a.text, b.text))) ||
[], [],
onFilter: (value, record) => onFilter: (value, record) =>
value.includes( value.includes(

View File

@@ -16,11 +16,12 @@ import { QUERY_ALL_ACTIVE_JOBS } from "../../graphql/jobs.queries";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { onlyUnique } from "../../utils/arrayHelper"; import { onlyUnique } from "../../utils/arrayHelper";
import { alphaSort } from "../../utils/sorters"; import { pageLimit } from "../../utils/config";
import { alphaSort, statusSort } from "../../utils/sorters";
import useLocalStorage from "../../utils/useLocalStorage";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import ChatOpenButton from "../chat-open-button/chat-open-button.component"; import ChatOpenButton from "../chat-open-button/chat-open-button.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import {pageLimit} from "../../utils/config";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -53,10 +54,8 @@ export function JobsReadyList({ bodyshop }) {
nextFetchPolicy: "network-only", nextFetchPolicy: "network-only",
}); });
const [state, setState] = useState({ const [state, setState] = useState({ sortedInfo: {} });
sortedInfo: {}, const [filter, setFilter] = useLocalStorage("filter_jobs_ready", null);
filteredInfo: { text: "" },
});
const { t } = useTranslation(); const { t } = useTranslation();
const history = useHistory(); const history = useHistory();
@@ -105,7 +104,8 @@ export function JobsReadyList({ bodyshop }) {
: []; : [];
const handleTableChange = (pagination, filters, sorter) => { const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); setState({ ...state, sortedInfo: sorter });
setFilter(filters);
}; };
const handleOnRowClick = (record) => { const handleOnRowClick = (record) => {
@@ -129,7 +129,6 @@ export function JobsReadyList({ bodyshop }) {
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number), sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
sortOrder: sortOrder:
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order, state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
render: (text, record) => ( render: (text, record) => (
<Link <Link
to={"/manage/jobs/" + record.id} to={"/manage/jobs/" + record.id}
@@ -157,7 +156,6 @@ export function JobsReadyList({ bodyshop }) {
dataIndex: "owner", dataIndex: "owner",
key: "owner", key: "owner",
ellipsis: true, ellipsis: true,
responsive: ["md"], responsive: ["md"],
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln), sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
sortOrder: sortOrder:
@@ -197,16 +195,15 @@ export function JobsReadyList({ bodyshop }) {
<ChatOpenButton phone={record.ownr_ph2} jobid={record.id} /> <ChatOpenButton phone={record.ownr_ph2} jobid={record.id} />
), ),
}, },
{ {
title: t("jobs.fields.status"), title: t("jobs.fields.status"),
dataIndex: "status", dataIndex: "status",
key: "status", key: "status",
ellipsis: true, ellipsis: true,
sorter: (a, b) => alphaSort(a.status, b.status), sorter: (a, b) => alphaSort(a.status, b.status),
sortOrder: sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order, state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
filteredValue: filter?.status || null,
filters: filters:
(jobs && (jobs &&
jobs jobs
@@ -217,11 +214,17 @@ export function JobsReadyList({ bodyshop }) {
text: s || "No Status*", text: s || "No Status*",
value: [s], value: [s],
}; };
})) || })
.sort((a, b) =>
statusSort(
a.text,
b.text,
bodyshop.md_ro_statuses.active_statuses
)
)) ||
[], [],
onFilter: (value, record) => value.includes(record.status), onFilter: (value, record) => value.includes(record.status),
}, },
{ {
title: t("jobs.fields.vehicle"), title: t("jobs.fields.vehicle"),
dataIndex: "vehicle", dataIndex: "vehicle",
@@ -274,6 +277,7 @@ export function JobsReadyList({ bodyshop }) {
dataIndex: "ins_co_nm", dataIndex: "ins_co_nm",
key: "ins_co_nm", key: "ins_co_nm",
ellipsis: true, ellipsis: true,
filteredValue: filter?.ins_co_nm || null,
filters: filters:
(jobs && (jobs &&
jobs jobs
@@ -281,10 +285,11 @@ export function JobsReadyList({ bodyshop }) {
.filter(onlyUnique) .filter(onlyUnique)
.map((s) => { .map((s) => {
return { return {
text: s, text: s || "No Ins Co.*",
value: [s], value: [s],
}; };
})) || })
.sort((a, b) => alphaSort(a.text, b.text))) ||
[], [],
onFilter: (value, record) => value.includes(record.ins_co_nm), onFilter: (value, record) => value.includes(record.ins_co_nm),
responsive: ["md"], responsive: ["md"],
@@ -295,7 +300,6 @@ export function JobsReadyList({ bodyshop }) {
key: "clm_total", key: "clm_total",
responsive: ["md"], responsive: ["md"],
ellipsis: true, ellipsis: true,
sorter: (a, b) => a.clm_total - b.clm_total, sorter: (a, b) => a.clm_total - b.clm_total,
sortOrder: sortOrder:
state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order, state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order,
@@ -306,9 +310,10 @@ export function JobsReadyList({ bodyshop }) {
{ {
title: t("jobs.labels.estimator"), title: t("jobs.labels.estimator"),
dataIndex: "jobs.labels.estimator", dataIndex: "jobs.labels.estimator",
key: "jobs.labels.estimator", key: "estimator",
ellipsis: true, ellipsis: true,
responsive: ["xl"], responsive: ["xl"],
filteredValue: filter?.estimator || null,
filterSearch: true, filterSearch: true,
filters: filters:
(jobs && (jobs &&
@@ -317,10 +322,11 @@ export function JobsReadyList({ bodyshop }) {
.filter(onlyUnique) .filter(onlyUnique)
.map((s) => { .map((s) => {
return { return {
text: s || "N/A", text: s || "No Estimator*",
value: [s], value: [s],
}; };
})) || })
.sort((a, b) => alphaSort(a.text, b.text))) ||
[], [],
onFilter: (value, record) => onFilter: (value, record) =>
value.includes( value.includes(

View File

@@ -2,7 +2,7 @@ import { Space, Tag } from "antd";
import React from "react"; import React from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
export default function JobsRelatedRos({ jobid, job }) { export default function JobsRelatedRos({ jobid, job, disabled }) {
if (!(job && job.vehicle && job.vehicle.jobs)) return null; if (!(job && job.vehicle && job.vehicle.jobs)) return null;
return ( return (
<Space wrap> <Space wrap>
@@ -10,9 +10,15 @@ export default function JobsRelatedRos({ jobid, job }) {
.filter((j) => j.id !== job.id) .filter((j) => j.id !== job.id)
.map((j) => ( .map((j) => (
<Tag key={j.id}> <Tag key={j.id}>
{disabled ? (
<>{`${j.ro_number || "N/A"}${j.clm_no ? ` | ${j.clm_no}` : ""}${
j.status ? ` | ${j.status}` : ""
}`}</>
) : (
<Link to={`/manage/jobs/${j?.id}`}>{`${j.ro_number || "N/A"}${ <Link to={`/manage/jobs/${j?.id}`}>{`${j.ro_number || "N/A"}${
j.clm_no ? ` | ${j.clm_no}` : "" j.clm_no ? ` | ${j.clm_no}` : ""
}${j.status ? ` | ${j.status}` : ""}`}</Link> }${j.status ? ` | ${j.status}` : ""}`}</Link>
)}
</Tag> </Tag>
))} ))}
</Space> </Space>

View File

@@ -76,7 +76,14 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
dataIndex: "ownr", dataIndex: "ownr",
key: "ownr", key: "ownr",
ellipsis: true, ellipsis: true,
render: (text, record) => <OwnerNameDisplay ownerObject={record} />, render: (text, record) =>
technician ? (
<OwnerNameDisplay ownerObject={record} />
) : (
<Link to={`/manage/owners/${record.ownerid}`}>
<OwnerNameDisplay ownerObject={record} />
</Link>
),
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln), sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
sortOrder: sortOrder:
state.sortedInfo.columnKey === "ownr" && state.sortedInfo.order, state.sortedInfo.columnKey === "ownr" && state.sortedInfo.order,
@@ -93,7 +100,12 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
), ),
sortOrder: sortOrder:
state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order, state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
render: (text, record) => ( render: (text, record) =>
technician ? (
<>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
record.v_model_desc || ""
} ${record.v_color || ""} ${record.plate_no || ""}`}</>
) : (
<Link to={`/manage/vehicles/${record.vehicleid}`}>{`${ <Link to={`/manage/vehicles/${record.vehicleid}`}>{`${
record.v_model_yr || "" record.v_model_yr || ""
} ${record.v_make_desc || ""} ${record.v_model_desc || ""} ${ } ${record.v_make_desc || ""} ${record.v_model_desc || ""} ${

View File

@@ -13,12 +13,14 @@ import { selectTechnician } from "../../redux/tech/tech.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter } from "../../utils/DateFormatter"; import { DateFormatter } from "../../utils/DateFormatter";
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import StartChatButton from "../chat-open-button/chat-open-button.component"; import StartChatButton from "../chat-open-button/chat-open-button.component";
import JobAtChange from "../job-at-change/job-at-change.component"; import JobAtChange from "../job-at-change/job-at-change.component";
import JobDetailCardsDocumentsComponent from "../job-detail-cards/job-detail-cards.documents.component"; import JobDetailCardsDocumentsComponent from "../job-detail-cards/job-detail-cards.documents.component";
import JobDetailCardsNotesComponent from "../job-detail-cards/job-detail-cards.notes.component"; import JobDetailCardsNotesComponent from "../job-detail-cards/job-detail-cards.notes.component";
import JobDetailCardsPartsComponent from "../job-detail-cards/job-detail-cards.parts.component"; import JobDetailCardsPartsComponent from "../job-detail-cards/job-detail-cards.parts.component";
import CardTemplate from "../job-detail-cards/job-detail-cards.template.component";
import JobEmployeeAssignments from "../job-employee-assignments/job-employee-assignments.container"; import JobEmployeeAssignments from "../job-employee-assignments/job-employee-assignments.container";
import ScoreboardAddButton from "../job-scoreboard-add-button/job-scoreboard-add-button.component"; import ScoreboardAddButton from "../job-scoreboard-add-button/job-scoreboard-add-button.component";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component"; import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
@@ -103,7 +105,13 @@ export function ProductionListDetail({
{error && <AlertComponent error={JSON.stringify(error)} />} {error && <AlertComponent error={JSON.stringify(error)} />}
{!loading && data && ( {!loading && data && (
<div> <div>
<Space direction="vertical">
<CardTemplate
title={t("jobs.labels.employeeassignments")}
loading={loading}
>
<JobEmployeeAssignments job={data.jobs_by_pk} refetch={refetch} /> <JobEmployeeAssignments job={data.jobs_by_pk} refetch={refetch} />
</CardTemplate>
<Descriptions bordered column={1}> <Descriptions bordered column={1}>
<Descriptions.Item label={t("jobs.fields.ro_number")}> <Descriptions.Item label={t("jobs.fields.ro_number")}>
{theJob.ro_number || ""} {theJob.ro_number || ""}
@@ -111,7 +119,7 @@ export function ProductionListDetail({
<Descriptions.Item label={t("jobs.fields.alt_transport")}> <Descriptions.Item label={t("jobs.fields.alt_transport")}>
<Space> <Space>
{data.jobs_by_pk.alt_transport || ""} {data.jobs_by_pk.alt_transport || ""}
<JobAtChange event={{ job: data.jobs_by_pk }} /> <JobAtChange job={data.jobs_by_pk} />
</Space> </Space>
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label={t("jobs.fields.clm_no")}> <Descriptions.Item label={t("jobs.fields.clm_no")}>
@@ -121,7 +129,10 @@ export function ProductionListDetail({
{theJob.ins_co_nm || ""} {theJob.ins_co_nm || ""}
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label={t("jobs.fields.owner")}> <Descriptions.Item label={t("jobs.fields.owner")}>
<Space>
<OwnerNameDisplay ownerObject={theJob} /> <OwnerNameDisplay ownerObject={theJob} />
{!technician ? (
<>
<StartChatButton <StartChatButton
phone={data.jobs_by_pk.ownr_ph1} phone={data.jobs_by_pk.ownr_ph1}
jobid={data.jobs_by_pk.id} jobid={data.jobs_by_pk.id}
@@ -130,6 +141,18 @@ export function ProductionListDetail({
phone={data.jobs_by_pk.ownr_ph2} phone={data.jobs_by_pk.ownr_ph2}
jobid={data.jobs_by_pk.id} jobid={data.jobs_by_pk.id}
/> />
</>
) : (
<>
<PhoneNumberFormatter>
{data.jobs_by_pk.ownr_ph1}
</PhoneNumberFormatter>
<PhoneNumberFormatter>
{data.jobs_by_pk.ownr_ph2}
</PhoneNumberFormatter>
</>
)}
</Space>
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label={t("jobs.fields.vehicle")}> <Descriptions.Item label={t("jobs.fields.vehicle")}>
{`${theJob.v_model_yr || ""} ${theJob.v_color || ""} ${ {`${theJob.v_model_yr || ""} ${theJob.v_color || ""} ${
@@ -146,12 +169,10 @@ export function ProductionListDetail({
<DateFormatter>{theJob.scheduled_completion}</DateFormatter> <DateFormatter>{theJob.scheduled_completion}</DateFormatter>
</Descriptions.Item> </Descriptions.Item>
</Descriptions> </Descriptions>
<JobDetailCardsPartsComponent <JobDetailCardsPartsComponent
loading={loading} loading={loading}
data={data ? data.jobs_by_pk : null} data={data ? data.jobs_by_pk : null}
/> />
<JobDetailCardsNotesComponent <JobDetailCardsNotesComponent
loading={loading} loading={loading}
data={data ? data.jobs_by_pk : null} data={data ? data.jobs_by_pk : null}
@@ -162,6 +183,7 @@ export function ProductionListDetail({
data={data ? data.jobs_by_pk : null} data={data ? data.jobs_by_pk : null}
/> />
)} )}
</Space>
</div> </div>
)} )}
</Drawer> </Drawer>

View File

@@ -216,6 +216,7 @@ export function ScheduleJobModalContainer({
okButtonProps={{ okButtonProps={{
loading: loading, loading: loading,
}} }}
closable={false}
> >
<Form <Form
form={form} form={form}

View File

@@ -1,14 +1,20 @@
import { Button, Table } from "antd"; import { Button, Table } from "antd";
import queryString from "query-string"; import queryString from "query-string";
import React from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useHistory, useLocation } from "react-router-dom"; import { useHistory, useLocation } from "react-router-dom";
import { alphaSort } from "../../utils/sorters";
export default function ShopEmployeesListComponent({ loading, employees }) { export default function ShopEmployeesListComponent({ loading, employees }) {
const { t } = useTranslation(); const { t } = useTranslation();
const history = useHistory(); const history = useHistory();
const search = queryString.parse(useLocation().search); const search = queryString.parse(useLocation().search);
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: { text: "" },
});
const handleOnRowClick = (record) => { const handleOnRowClick = (record) => {
if (record) { if (record) {
search.employeeId = record.id; search.employeeId = record.id;
@@ -18,32 +24,82 @@ export default function ShopEmployeesListComponent({ loading, employees }) {
history.push({ search: queryString.stringify(search) }); history.push({ search: queryString.stringify(search) });
} }
}; };
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
const columns = [ const columns = [
{ {
title: t("employees.fields.employee_number"), title: t("employees.fields.employee_number"),
dataIndex: "employee_number", dataIndex: "employee_number",
key: "employee_number", key: "employee_number",
sorter: (a, b) => alphaSort(a.employee_number, b.employee_number),
sortOrder:
state.sortedInfo.columnKey === "employee_number" &&
state.sortedInfo.order,
}, },
{ {
title: t("employees.fields.first_name"), title: t("employees.labels.name"),
dataIndex: "first_name", dataIndex: "employee_name",
key: "first_name", key: "employee_name",
sorter: (a, b) =>
alphaSort(
`${a.first_name || ""} ${a.last_name || ""}`.trim(),
`${b.first_name || ""} ${b.last_name || ""}`.trim()
),
sortOrder:
state.sortedInfo.columnKey === "employee_name" &&
state.sortedInfo.order,
render: (text, record) =>
`${record.first_name || ""} ${record.last_name || ""}`.trim(),
}, },
{
title: t("employees.fields.last_name"),
dataIndex: "last_name",
key: "last_name",
},
{ {
title: t("employees.labels.rate_type"), title: t("employees.labels.rate_type"),
dataIndex: "rate_type", dataIndex: "rate_type",
key: "rate_type", key: "rate_type",
sorter: (a, b) => Number(a.flat_rate) - Number(b.flat_rate),
sortOrder:
state.sortedInfo.columnKey === "rate_type" && state.sortedInfo.order,
filters: [
{
text: t("employees.labels.flat_rate"),
value: true,
},
{
text: t("employees.labels.straight_time"),
value: false,
},
],
onFilter: (value, record) => value === record.flate_rate,
render: (text, record) => render: (text, record) =>
record.flat_rate record.flat_rate
? t("employees.labels.flat_rate") ? t("employees.labels.flat_rate")
: t("employees.labels.straight_time"), : t("employees.labels.straight_time"),
}, },
{
title: t("employees.labels.status"),
dataIndex: "active",
key: "active",
sorter: (a, b) => Number(a.active) - Number(b.active),
sortOrder:
state.sortedInfo.columnKey === "active" && state.sortedInfo.order,
filters: [
{
text: t("employees.labels.active"),
value: true,
},
{
text: t("employees.labels.inactive"),
value: false,
},
],
onFilter: (value, record) => value === record.active,
render: (text, record) =>
record.active
? t("employees.labels.active")
: t("employees.labels.inactive"),
},
]; ];
return ( return (
<div> <div>
@@ -74,6 +130,7 @@ export default function ShopEmployeesListComponent({ loading, employees }) {
type: "radio", type: "radio",
selectedRowKeys: [search.employeeId], selectedRowKeys: [search.employeeId],
}} }}
onChange={handleTableChange}
onRow={(record, rowIndex) => { onRow={(record, rowIndex) => {
return { return {
onClick: (event) => { onClick: (event) => {

View File

@@ -166,9 +166,6 @@ export function TechClockOffButton({
}, },
({ getFieldValue }) => ({ ({ getFieldValue }) => ({
validator(rule, value) { validator(rule, value) {
console.log(
bodyshop.tt_enforce_hours_for_tech_console
);
if (!bodyshop.tt_enforce_hours_for_tech_console) { if (!bodyshop.tt_enforce_hours_for_tech_console) {
return Promise.resolve(); return Promise.resolve();
} }

View File

@@ -1,7 +1,8 @@
import { Button, Form, Input } from "antd"; import { Button, Form, Input } from "antd";
import React from "react"; import React, { useEffect } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Redirect } from "react-router-dom";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { techLoginStart } from "../../redux/tech/tech.actions"; import { techLoginStart } from "../../redux/tech/tech.actions";
import { import {
@@ -11,7 +12,6 @@ import {
} from "../../redux/tech/tech.selectors"; } from "../../redux/tech/tech.selectors";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import "./tech-login.styles.scss"; import "./tech-login.styles.scss";
import { Redirect } from "react-router-dom";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
technician: selectTechnician, technician: selectTechnician,
@@ -35,6 +35,10 @@ export function TechLogin({
techLoginStart(values); techLoginStart(values);
}; };
useEffect(() => {
document.title = t("titles.techconsole");
}, [t]);
return ( return (
<div className="tech-login-container"> <div className="tech-login-container">
{technician ? <Redirect to={`/tech/joblookup`} /> : null} {technician ? <Redirect to={`/tech/joblookup`} /> : null}

View File

@@ -3,11 +3,12 @@ import { gql } from "@apollo/client";
export const QUERY_EMPLOYEES = gql` export const QUERY_EMPLOYEES = gql`
query QUERY_EMPLOYEES { query QUERY_EMPLOYEES {
employees(order_by: { employee_number: asc }) { employees(order_by: { employee_number: asc }) {
last_name active
id employee_number
first_name first_name
flat_rate flat_rate
employee_number id
last_name
} }
} }
`; `;

View File

@@ -5,7 +5,7 @@ export const QUERY_ALL_ACTIVE_JOBS_PAGINATED = gql`
$offset: Int $offset: Int
$limit: Int $limit: Int
$order: [jobs_order_by!] $order: [jobs_order_by!]
$statuses: [String!]!, $statuses: [String!]!
$isConverted: Boolean $isConverted: Boolean
) { ) {
jobs( jobs(
@@ -120,7 +120,9 @@ export const QUERY_PARTS_QUEUE = gql`
} }
} }
jobs( jobs(
where: { _and: [{ status: { _in: $statuses }, converted: { _eq: true } }] } where: {
_and: [{ status: { _in: $statuses }, converted: { _eq: true } }]
}
offset: $offset offset: $offset
limit: $limit limit: $limit
order_by: $order order_by: $order
@@ -336,6 +338,7 @@ export const QUERY_JOBS_IN_PRODUCTION = gql`
category category
iouparent iouparent
ro_number ro_number
ownerid
ownr_fn ownr_fn
ownr_ln ownr_ln
ownr_co_nm ownr_co_nm

View File

@@ -1,10 +1,17 @@
import { Divider } from "antd"; import { Divider } from "antd";
import React from "react"; import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import TechClockInFormContainer from "../../components/tech-job-clock-in-form/tech-job-clock-in-form.container"; 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 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"; import TechJobStatistics from "../../components/tech-job-statistics/tech-job-statistics.component";
export default function TechClockComponent() { export default function TechClockComponent() {
const { t } = useTranslation();
useEffect(() => {
document.title = t("titles.techjobclock");
}, [t]);
return ( return (
<div> <div>
<TechJobStatistics /> <TechJobStatistics />

View File

@@ -1,9 +1,16 @@
import React from "react"; import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import RbacWrapperComponent from "../../components/rbac-wrapper/rbac-wrapper.component"; import RbacWrapperComponent from "../../components/rbac-wrapper/rbac-wrapper.component";
import TechLookupJobsDrawer from "../../components/tech-lookup-jobs-drawer/tech-lookup-jobs-drawer.component"; import TechLookupJobsDrawer from "../../components/tech-lookup-jobs-drawer/tech-lookup-jobs-drawer.component";
import TechLookupJobsList from "../../components/tech-lookup-jobs-list/tech-lookup-jobs-list.component"; import TechLookupJobsList from "../../components/tech-lookup-jobs-list/tech-lookup-jobs-list.component";
export default function TechLookupContainer() { export default function TechLookupContainer() {
const { t } = useTranslation();
useEffect(() => {
document.title = t("titles.techjoblookup");
}, [t]);
return ( return (
<div> <div>
<RbacWrapperComponent action="jobs:list-active"> <RbacWrapperComponent action="jobs:list-active">

View File

@@ -1,7 +1,14 @@
import React from "react"; import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import TimeTicketShift from "../../components/time-ticket-shift/time-ticket-shift.container"; import TimeTicketShift from "../../components/time-ticket-shift/time-ticket-shift.container";
export default function TechShiftClock() { export default function TechShiftClock() {
const { t } = useTranslation();
useEffect(() => {
document.title = t("titles.techshiftclock");
}, [t]);
return ( return (
<div> <div>
<TimeTicketShift isTechConsole /> <TimeTicketShift isTechConsole />

View File

@@ -1001,10 +1001,13 @@
}, },
"labels": { "labels": {
"actions": "Actions", "actions": "Actions",
"active": "Active",
"endmustbeafterstart": "End date must be after start date.", "endmustbeafterstart": "End date must be after start date.",
"flat_rate": "Flat Rate", "flat_rate": "Flat Rate",
"inactive": "Inactive",
"name": "Name", "name": "Name",
"rate_type": "Rate Type", "rate_type": "Rate Type",
"status": "Status",
"straight_time": "Straight Time" "straight_time": "Straight Time"
}, },
"successes": { "successes": {
@@ -1704,6 +1707,7 @@
"estimator": "Estimator", "estimator": "Estimator",
"filehandler": "File Handler", "filehandler": "File Handler",
"insurance": "Insurance Details", "insurance": "Insurance Details",
"more": "More",
"notes": "Notes", "notes": "Notes",
"parts": "Parts", "parts": "Parts",
"totals": "Totals", "totals": "Totals",
@@ -2024,7 +2028,7 @@
"joblookup": "Job Lookup", "joblookup": "Job Lookup",
"login": "Login", "login": "Login",
"logout": "Logout", "logout": "Logout",
"productionboard": "Production Board - Visual", "productionboard": "Production Visual",
"productionlist": "Production List", "productionlist": "Production List",
"shiftclockin": "Shift Clock" "shiftclockin": "Shift Clock"
} }
@@ -2621,6 +2625,7 @@
"open_orders": "Open Orders by Date", "open_orders": "Open Orders by Date",
"open_orders_csr": "Open Orders by CSR", "open_orders_csr": "Open Orders by CSR",
"open_orders_estimator": "Open Orders by Estimator", "open_orders_estimator": "Open Orders by Estimator",
"open_orders_excel": "Open Orders - Excel",
"open_orders_ins_co": "Open Orders by Insurance Company", "open_orders_ins_co": "Open Orders by Insurance Company",
"open_orders_referral": "Open Orders by Referral Source", "open_orders_referral": "Open Orders by Referral Source",
"open_orders_specific_csr": "Open Orders filtered by CSR", "open_orders_specific_csr": "Open Orders filtered by CSR",
@@ -2901,7 +2906,7 @@
"parts-queue": "Parts Queue | $t(titles.app)", "parts-queue": "Parts Queue | $t(titles.app)",
"payments-all": "Payments | $t(titles.app)", "payments-all": "Payments | $t(titles.app)",
"phonebook": "Phonebook | $t(titles.app)", "phonebook": "Phonebook | $t(titles.app)",
"productionboard": "Production - Board", "productionboard": "Production Board - Visual | $t(titles.app)",
"productionlist": "Production Board - List | $t(titles.app)", "productionlist": "Production Board - List | $t(titles.app)",
"profile": "My Profile | $t(titles.app)", "profile": "My Profile | $t(titles.app)",
"readyjobs": "Ready Jobs | $t(titles.app)", "readyjobs": "Ready Jobs | $t(titles.app)",
@@ -2913,6 +2918,10 @@
"shop-csi": "CSI Responses | $t(titles.app)", "shop-csi": "CSI Responses | $t(titles.app)",
"shop-templates": "Shop Templates | $t(titles.app)", "shop-templates": "Shop Templates | $t(titles.app)",
"shop_vendors": "Vendors | $t(titles.app)", "shop_vendors": "Vendors | $t(titles.app)",
"techconsole": "Technician Console | $t(titles.app)",
"techjoblookup": "Technician Job Lookup | $t(titles.app)",
"techjobclock": "Technician Job Clock | $t(titles.app)",
"techshiftclock": "Technician Shift Clock | $t(titles.app)",
"temporarydocs": "Temporary Documents | $t(titles.app)", "temporarydocs": "Temporary Documents | $t(titles.app)",
"timetickets": "Time Tickets | $t(titles.app)", "timetickets": "Time Tickets | $t(titles.app)",
"ttapprovals": "", "ttapprovals": "",

View File

@@ -1001,10 +1001,13 @@
}, },
"labels": { "labels": {
"actions": "", "actions": "",
"active": "",
"endmustbeafterstart": "", "endmustbeafterstart": "",
"flat_rate": "", "flat_rate": "",
"inactive": "",
"name": "", "name": "",
"rate_type": "", "rate_type": "",
"status": "",
"straight_time": "" "straight_time": ""
}, },
"successes": { "successes": {
@@ -1704,6 +1707,7 @@
"estimator": "Estimador", "estimator": "Estimador",
"filehandler": "File Handler", "filehandler": "File Handler",
"insurance": "detalles del seguro", "insurance": "detalles del seguro",
"more": "Más",
"notes": "Notas", "notes": "Notas",
"parts": "Partes", "parts": "Partes",
"totals": "Totales", "totals": "Totales",
@@ -2621,6 +2625,7 @@
"open_orders": "", "open_orders": "",
"open_orders_csr": "", "open_orders_csr": "",
"open_orders_estimator": "", "open_orders_estimator": "",
"open_orders_excel": "",
"open_orders_ins_co": "", "open_orders_ins_co": "",
"open_orders_referral": "", "open_orders_referral": "",
"open_orders_specific_csr": "", "open_orders_specific_csr": "",
@@ -2894,7 +2899,7 @@
"jobs-intake": "", "jobs-intake": "",
"jobsavailable": "Empleos disponibles | $t(titles.app)", "jobsavailable": "Empleos disponibles | $t(titles.app)",
"jobsdetail": "Trabajo {{ro_number}} | $t(titles.app)", "jobsdetail": "Trabajo {{ro_number}} | $t(titles.app)",
"jobsdocuments": "Documentos de trabajo {{ro_number}} | $ t (títulos.app)", "jobsdocuments": "Documentos de trabajo {{ro_number}} | $t(titles.app)",
"manageroot": "Casa | $t(titles.app)", "manageroot": "Casa | $t(titles.app)",
"owners": "Todos los propietarios | $t(titles.app)", "owners": "Todos los propietarios | $t(titles.app)",
"owners-detail": "", "owners-detail": "",
@@ -2913,6 +2918,10 @@
"shop-csi": "", "shop-csi": "",
"shop-templates": "", "shop-templates": "",
"shop_vendors": "Vendedores | $t(titles.app)", "shop_vendors": "Vendedores | $t(titles.app)",
"techconsole": "$t(titles.app)",
"techjoblookup": "$t(titles.app)",
"techjobclock": "$t(titles.app)",
"techshiftclock": "$t(titles.app)",
"temporarydocs": "", "temporarydocs": "",
"timetickets": "", "timetickets": "",
"ttapprovals": "", "ttapprovals": "",

View File

@@ -1001,10 +1001,13 @@
}, },
"labels": { "labels": {
"actions": "", "actions": "",
"active": "",
"endmustbeafterstart": "", "endmustbeafterstart": "",
"flat_rate": "", "flat_rate": "",
"inactive": "",
"name": "", "name": "",
"rate_type": "", "rate_type": "",
"status": "",
"straight_time": "" "straight_time": ""
}, },
"successes": { "successes": {
@@ -1704,6 +1707,7 @@
"estimator": "Estimateur", "estimator": "Estimateur",
"filehandler": "Gestionnaire de fichiers", "filehandler": "Gestionnaire de fichiers",
"insurance": "Détails de l'assurance", "insurance": "Détails de l'assurance",
"more": "Plus",
"notes": "Remarques", "notes": "Remarques",
"parts": "les pièces", "parts": "les pièces",
"totals": "Totaux", "totals": "Totaux",
@@ -2621,6 +2625,7 @@
"open_orders": "", "open_orders": "",
"open_orders_csr": "", "open_orders_csr": "",
"open_orders_estimator": "", "open_orders_estimator": "",
"open_orders_excel": "",
"open_orders_ins_co": "", "open_orders_ins_co": "",
"open_orders_referral": "", "open_orders_referral": "",
"open_orders_specific_csr": "", "open_orders_specific_csr": "",
@@ -2894,7 +2899,7 @@
"jobs-intake": "", "jobs-intake": "",
"jobsavailable": "Emplois disponibles | $t(titles.app)", "jobsavailable": "Emplois disponibles | $t(titles.app)",
"jobsdetail": "Travail {{ro_number}} | $t(titles.app)", "jobsdetail": "Travail {{ro_number}} | $t(titles.app)",
"jobsdocuments": "Documents de travail {{ro_number}} | $ t (titres.app)", "jobsdocuments": "Documents de travail {{ro_number}} | $t(titles.app)",
"manageroot": "Accueil | $t(titles.app)", "manageroot": "Accueil | $t(titles.app)",
"owners": "Tous les propriétaires | $t(titles.app)", "owners": "Tous les propriétaires | $t(titles.app)",
"owners-detail": "", "owners-detail": "",
@@ -2913,6 +2918,10 @@
"shop-csi": "", "shop-csi": "",
"shop-templates": "", "shop-templates": "",
"shop_vendors": "Vendeurs | $t(titles.app)", "shop_vendors": "Vendeurs | $t(titles.app)",
"techconsole": "$t(titles.app)",
"techjoblookup": "$t(titles.app)",
"techjobclock": "$t(titles.app)",
"techshiftclock": "$t(titles.app)",
"temporarydocs": "", "temporarydocs": "",
"timetickets": "", "timetickets": "",
"ttapprovals": "", "ttapprovals": "",

View File

@@ -2026,6 +2026,19 @@ export const TemplateList = (type, context) => {
}, },
group: "customers", group: "customers",
}, },
open_orders_excel: {
title: i18n.t("reportcenter.templates.open_orders_excel"),
subject: i18n.t("reportcenter.templates.open_orders_excel"),
key: "open_orders_excel",
//idtype: "vendor",
reporttype: "excel",
disabled: false,
rangeFilter: {
object: i18n.t("reportcenter.labels.objects.jobs"),
field: i18n.t("jobs.fields.date_open"),
},
group: "jobs",
},
} }
: {}), : {}),
...(!type || type === "courtesycarcontract" ...(!type || type === "courtesycarcontract"

View File

@@ -2423,6 +2423,73 @@
_eq: X-Hasura-User-Id _eq: X-Hasura-User-Id
- active: - active:
_eq: true _eq: true
- table:
name: eula_acceptances
schema: public
object_relationships:
- name: eula
using:
foreign_key_constraint_on: eulaid
- name: user
using:
foreign_key_constraint_on: useremail
insert_permissions:
- role: user
permission:
check:
user:
authid:
_eq: X-Hasura-User-Id
columns:
- address
- buisness_name
- date_accepted
- eulaid
- first_name
- last_name
- phone_number
- useremail
select_permissions:
- role: user
permission:
columns:
- address
- buisness_name
- first_name
- last_name
- phone_number
- useremail
- created_at
- date_accepted
- updated_at
- eulaid
- id
filter:
user:
authid:
_eq: X-Hasura-User-Id
- table:
name: eulas
schema: public
array_relationships:
- name: eula_acceptances
using:
foreign_key_constraint_on:
column: eulaid
table:
name: eula_acceptances
schema: public
select_permissions:
- role: user
permission:
columns:
- id
- created_at
- updated_at
- effective_date
- end_date
- content
filter: {}
- table: - table:
name: exportlog name: exportlog
schema: public schema: public
@@ -5888,6 +5955,13 @@
table: table:
name: email_audit_trail name: email_audit_trail
schema: public schema: public
- name: eula_acceptances
using:
foreign_key_constraint_on:
column: useremail
table:
name: eula_acceptances
schema: public
- name: exportlogs - name: exportlogs
using: using:
foreign_key_constraint_on: foreign_key_constraint_on:

View File

@@ -0,0 +1 @@
DROP TABLE "public"."eulas";

View File

@@ -0,0 +1,18 @@
CREATE TABLE "public"."eulas" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), "effective_date" timestamptz NOT NULL, "end_date" timestamptz, "content" text NOT NULL, PRIMARY KEY ("id") );
CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"()
RETURNS TRIGGER AS $$
DECLARE
_new record;
BEGIN
_new := NEW;
_new."updated_at" = NOW();
RETURN _new;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER "set_public_eulas_updated_at"
BEFORE UPDATE ON "public"."eulas"
FOR EACH ROW
EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"();
COMMENT ON TRIGGER "set_public_eulas_updated_at" ON "public"."eulas"
IS 'trigger to set value of column "updated_at" to current timestamp on row update';
CREATE EXTENSION IF NOT EXISTS pgcrypto;

View File

@@ -0,0 +1 @@
DROP TABLE "public"."eula_acceptances";

View File

@@ -0,0 +1,18 @@
CREATE TABLE "public"."eula_acceptances" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), "eulaid" uuid NOT NULL, "date_accepted" timestamptz NOT NULL, "first_name" text NOT NULL, "last_name" text NOT NULL, "address" text NOT NULL, "phone_number" Text NOT NULL, "buisness_name" Text NOT NULL, "useremail" text NOT NULL, PRIMARY KEY ("id") , FOREIGN KEY ("eulaid") REFERENCES "public"."eulas"("id") ON UPDATE restrict ON DELETE restrict, FOREIGN KEY ("useremail") REFERENCES "public"."users"("email") ON UPDATE restrict ON DELETE restrict);
CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"()
RETURNS TRIGGER AS $$
DECLARE
_new record;
BEGIN
_new := NEW;
_new."updated_at" = NOW();
RETURN _new;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER "set_public_eula_acceptances_updated_at"
BEFORE UPDATE ON "public"."eula_acceptances"
FOR EACH ROW
EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"();
COMMENT ON TRIGGER "set_public_eula_acceptances_updated_at" ON "public"."eula_acceptances"
IS 'trigger to set value of column "updated_at" to current timestamp on row update';
CREATE EXTENSION IF NOT EXISTS pgcrypto;