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 {ownerTitle.length > 0
: t("owner.labels.noownerinfo")} ? ownerTitle
</Link> : t("owner.labels.noownerinfo")}
</>
) : (
<Link to={`/manage/owners/${job.owner.id}`}>
{ownerTitle.length > 0
? ownerTitle
: t("owner.labels.noownerinfo")}
</Link>
)
} }
> >
<div> <div>
<DataLabel key="2" label={t("jobs.fields.ownr_ph1")}> <DataLabel key="2" label={t("jobs.fields.ownr_ph1")}>
<ChatOpenButton phone={job.ownr_ph1} jobid={job.id} /> {disabled ? (
<PhoneNumberFormatter>{job.ownr_ph1}</PhoneNumberFormatter>
) : (
<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")}>
<ChatOpenButton phone={job.ownr_ph2} jobid={job.id} /> {disabled ? (
<PhoneNumberFormatter>{job.ownr_ph2}</PhoneNumberFormatter>
) : (
<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")}{" "}
} </>
> ) : (
{vehicleTitle.length > 0 <Link to={job.vehicle && `/manage/vehicles/${job.vehicle.id}`}>
? vehicleTitle {vehicleTitle.length > 0
: t("vehicles.labels.novehinfo")} ? vehicleTitle
</Link> : t("vehicles.labels.novehinfo")}
</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,382 +14,389 @@ 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";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
}); });
export function JobsList({ bodyshop }) { export function JobsList({ bodyshop }) {
const searchParams = queryString.parse(useLocation().search); const searchParams = queryString.parse(useLocation().search);
const { selected } = searchParams; const { selected } = searchParams;
const selectedBreakpoint = Object.entries(Grid.useBreakpoint()) const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
.filter((screen) => !!screen[1]) .filter((screen) => !!screen[1])
.slice(-1)[0]; .slice(-1)[0];
const { loading, error, data, refetch } = useQuery(QUERY_ALL_ACTIVE_JOBS, { const { loading, error, data, refetch } = useQuery(QUERY_ALL_ACTIVE_JOBS, {
variables: { variables: {
statuses: bodyshop.md_ro_statuses.active_statuses || ["Open", "Open*"], statuses: bodyshop.md_ro_statuses.active_statuses || ["Open", "Open*"],
}, },
fetchPolicy: "network-only", fetchPolicy: "network-only",
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();
const [searchText, setSearchText] = useState(""); const [searchText, setSearchText] = useState("");
if (error) return <AlertComponent message={error.message} type="error" />; if (error) return <AlertComponent message={error.message} type="error" />;
const jobs = data const jobs = data
? searchText === "" ? searchText === ""
? data.jobs ? data.jobs
: data.jobs.filter( : data.jobs.filter(
(j) => (j) =>
(j.ro_number || "") (j.ro_number || "")
.toString() .toString()
.toLowerCase() .toLowerCase()
.includes(searchText.toLowerCase()) || .includes(searchText.toLowerCase()) ||
(j.ownr_co_nm || "") (j.ownr_co_nm || "")
.toLowerCase() .toLowerCase()
.includes(searchText.toLowerCase()) || .includes(searchText.toLowerCase()) ||
(j.comments || "") (j.comments || "")
.toLowerCase() .toLowerCase()
.includes(searchText.toLowerCase()) || .includes(searchText.toLowerCase()) ||
(j.ownr_fn || "") (j.ownr_fn || "")
.toLowerCase() .toLowerCase()
.includes(searchText.toLowerCase()) || .includes(searchText.toLowerCase()) ||
(j.ownr_ln || "") (j.ownr_ln || "")
.toLowerCase() .toLowerCase()
.includes(searchText.toLowerCase()) || .includes(searchText.toLowerCase()) ||
(j.clm_no || "").toLowerCase().includes(searchText.toLowerCase()) || (j.clm_no || "").toLowerCase().includes(searchText.toLowerCase()) ||
(j.plate_no || "") (j.plate_no || "")
.toLowerCase() .toLowerCase()
.includes(searchText.toLowerCase()) || .includes(searchText.toLowerCase()) ||
(j.v_model_desc || "") (j.v_model_desc || "")
.toLowerCase() .toLowerCase()
.includes(searchText.toLowerCase()) || .includes(searchText.toLowerCase()) ||
(j.est_ct_fn || "") (j.est_ct_fn || "")
.toLowerCase() .toLowerCase()
.includes(searchText.toLowerCase()) || .includes(searchText.toLowerCase()) ||
(j.est_ct_ln || "") (j.est_ct_ln || "")
.toLowerCase() .toLowerCase()
.includes(searchText.toLowerCase()) || .includes(searchText.toLowerCase()) ||
(j.v_make_desc || "") (j.v_make_desc || "")
.toLowerCase() .toLowerCase()
.includes(searchText.toLowerCase()) .includes(searchText.toLowerCase())
) )
: []; : [];
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) => {
if (record) { if (record) {
if (record.id) { if (record.id) {
history.push({ history.push({
search: queryString.stringify({ search: queryString.stringify({
...searchParams, ...searchParams,
selected: record.id, selected: record.id,
}), }),
}); });
} }
} }
}; };
const columns = [ const columns = [
{ {
title: t("jobs.fields.ro_number"), title: t("jobs.fields.ro_number"),
dataIndex: "ro_number", dataIndex: "ro_number",
key: "ro_number", key: "ro_number",
sorter: (a, b) => sorter: (a, b) =>
parseInt((a.ro_number || "0").replace(/\D/g, "")) - parseInt((a.ro_number || "0").replace(/\D/g, "")) -
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} onClick={(e) => e.stopPropagation()}
onClick={(e) => e.stopPropagation()} >
> <Space>
<Space> {record.ro_number || t("general.labels.na")}
{record.ro_number || t("general.labels.na")} {record.production_vars && record.production_vars.alert ? (
{record.production_vars && record.production_vars.alert ? ( <ExclamationCircleFilled className="production-alert" />
<ExclamationCircleFilled className="production-alert" /> ) : null}
) : null} {record.suspended && (
{record.suspended && ( <PauseCircleOutlined style={{ color: "orangered" }} />
<PauseCircleOutlined style={{ color: "orangered" }} /> )}
)} {record.iouparent && (
{record.iouparent && ( <Tooltip title={t("jobs.labels.iou")}>
<Tooltip title={t("jobs.labels.iou")}> <BranchesOutlined style={{ color: "orangered" }} />
<BranchesOutlined style={{ color: "orangered" }} /> </Tooltip>
</Tooltip> )}
)} </Space>
</Space> </Link>
</Link> ),
), },
}, {
{ title: t("jobs.fields.owner"),
title: t("jobs.fields.owner"), dataIndex: "owner",
dataIndex: "owner", key: "owner",
key: "owner", ellipsis: true,
ellipsis: true, responsive: ["md"],
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
responsive: ["md"], sortOrder:
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln), state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
sortOrder: render: (text, record) => {
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order, return record.ownerid ? (
render: (text, record) => { <Link
return record.ownerid ? ( to={"/manage/owners/" + record.ownerid}
<Link onClick={(e) => e.stopPropagation()}
to={"/manage/owners/" + record.ownerid} >
onClick={(e) => e.stopPropagation()} <OwnerNameDisplay ownerObject={record} />
> </Link>
<OwnerNameDisplay ownerObject={record} /> ) : (
</Link> <span>
) : (
<span>
<OwnerNameDisplay ownerObject={record} /> <OwnerNameDisplay ownerObject={record} />
</span> </span>
); );
},
},
{
title: t("jobs.fields.ownr_ph1"),
dataIndex: "ownr_ph1",
key: "ownr_ph1",
ellipsis: true,
responsive: ["md"],
render: (text, record) => (
<ChatOpenButton phone={record.ownr_ph1} jobid={record.id} />
),
},
{
title: t("jobs.fields.ownr_ph2"),
dataIndex: "ownr_ph2",
key: "ownr_ph2",
ellipsis: true,
responsive: ["md"],
render: (text, record) => (
<ChatOpenButton phone={record.ownr_ph2} jobid={record.id} />
),
},
{
title: t("jobs.fields.status"),
dataIndex: "status",
key: "status",
ellipsis: true,
sorter: (a, b) => alphaSort(a.status, b.status),
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
filteredValue: filter?.status || null,
filters:
(jobs &&
jobs
.map((j) => j.status)
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Status*",
value: [s],
};
})
.sort((a, b) =>
statusSort(
a.text,
b.text,
bodyshop.md_ro_statuses.active_statuses
)
)) ||
[],
onFilter: (value, record) => value.includes(record.status),
},
{
title: t("jobs.fields.vehicle"),
dataIndex: "vehicle",
key: "vehicle",
ellipsis: true,
render: (text, record) => {
return record.vehicleid ? (
<Link
to={"/manage/vehicles/" + record.vehicleid}
onClick={(e) => e.stopPropagation()}
>
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
record.v_model_desc || ""
}`}
</Link>
) : (
<span>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
record.v_model_desc || ""
}`}</span>
);
},
},
{
title: t("vehicles.fields.plate_no"),
dataIndex: "plate_no",
key: "plate_no",
ellipsis: true,
responsive: ["md"],
sorter: (a, b) => alphaSort(a.plate_no, b.plate_no),
sortOrder:
state.sortedInfo.columnKey === "plate_no" && state.sortedInfo.order,
},
{
title: t("jobs.fields.clm_no"),
dataIndex: "clm_no",
key: "clm_no",
ellipsis: true,
responsive: ["md"],
sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
sortOrder:
state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order,
render: (text, record) =>
`${record.clm_no || ""}${
record.po_number ? ` (PO: ${record.po_number})` : ""
}`,
},
{
title: t("jobs.fields.ins_co_nm"),
dataIndex: "ins_co_nm",
key: "ins_co_nm",
ellipsis: true,
filteredValue: filter?.ins_co_nm || null,
filters:
(jobs &&
jobs
.map((j) => j.ins_co_nm)
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Ins. Co.*",
value: [s],
};
})
.sort((a, b) => alphaSort(a.text, b.text))) ||
[],
onFilter: (value, record) => value.includes(record.ins_co_nm),
responsive: ["md"],
},
{
title: t("jobs.fields.clm_total"),
dataIndex: "clm_total",
key: "clm_total",
responsive: ["md"],
ellipsis: true,
sorter: (a, b) => a.clm_total - b.clm_total,
sortOrder:
state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order,
render: (text, record) => (
<CurrencyFormatter>{record.clm_total}</CurrencyFormatter>
),
},
{
title: t("jobs.labels.estimator"),
dataIndex: "jobs.labels.estimator",
key: "estimator",
ellipsis: true,
responsive: ["xl"],
filterSearch: true,
filteredValue: filter?.estimator || null,
filters:
(jobs &&
jobs
.map((j) => `${j.est_ct_fn || ""} ${j.est_ct_ln || ""}`.trim())
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Estimator*",
value: [s],
};
})
.sort((a, b) => alphaSort(a.text, b.text))) ||
[],
onFilter: (value, record) =>
value.includes(
`${record.est_ct_fn || ""} ${record.est_ct_ln || ""}`.trim()
),
render: (text, record) =>
`${record.est_ct_fn || ""} ${record.est_ct_ln || ""}`.trim(),
},
{
title: t("jobs.fields.comment"),
dataIndex: "comment",
key: "comment",
ellipsis: true,
responsive: ["md"],
},
// {
// title: t("jobs.fields.owner_owing"),
// dataIndex: "owner_owing",
// key: "owner_owing",
// responsive: ["md"],
// render: (text, record) => (
// <CurrencyFormatter>{record.owner_owing}</CurrencyFormatter>
// ),
// },
];
const scrollMapper = {
xs: true,
sm: true,
md: true,
lg: "100%",
xl: "100%",
xxl: "100%",
};
return (
<Card
title={t("titles.bc.jobs-active")}
extra={
<Space wrap>
<Button onClick={() => refetch()}>
<SyncOutlined />
</Button>
<Input.Search
placeholder={t("general.labels.search")}
onChange={(e) => {
setSearchText(e.target.value);
}}
value={searchText}
enterButton
/>
</Space>
}
>
<Table
loading={loading}
pagination={{ defaultPageSize: 50 }}
columns={columns}
rowKey="id"
dataSource={jobs}
scroll={{
x: selectedBreakpoint ? scrollMapper[selectedBreakpoint[0]] : "100%",
}}
rowSelection={{
onSelect: (record) => {
handleOnRowClick(record);
},
selectedRowKeys: [selected],
type: "radio",
}}
onChange={handleTableChange}
onRow={(record, rowIndex) => {
return {
onClick: (event) => {
handleOnRowClick(record);
}, },
}, };
{ }}
title: t("jobs.fields.ownr_ph1"), />
dataIndex: "ownr_ph1", </Card>
key: "ownr_ph1", );
ellipsis: true,
responsive: ["md"],
render: (text, record) => (
<ChatOpenButton phone={record.ownr_ph1} jobid={record.id} />
),
},
{
title: t("jobs.fields.ownr_ph2"),
dataIndex: "ownr_ph2",
key: "ownr_ph2",
ellipsis: true,
responsive: ["md"],
render: (text, record) => (
<ChatOpenButton phone={record.ownr_ph2} jobid={record.id} />
),
},
{
title: t("jobs.fields.status"),
dataIndex: "status",
key: "status",
ellipsis: true,
sorter: (a, b) => alphaSort(a.status, b.status),
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
filters:
(jobs &&
jobs
.map((j) => j.status)
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Status*",
value: [s],
};
})) ||
[],
onFilter: (value, record) => value.includes(record.status),
},
{
title: t("jobs.fields.vehicle"),
dataIndex: "vehicle",
key: "vehicle",
ellipsis: true,
render: (text, record) => {
return record.vehicleid ? (
<Link
to={"/manage/vehicles/" + record.vehicleid}
onClick={(e) => e.stopPropagation()}
>
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
record.v_model_desc || ""
}`}
</Link>
) : (
<span>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
record.v_model_desc || ""
}`}</span>
);
},
},
{
title: t("vehicles.fields.plate_no"),
dataIndex: "plate_no",
key: "plate_no",
ellipsis: true,
responsive: ["md"],
sorter: (a, b) => alphaSort(a.plate_no, b.plate_no),
sortOrder:
state.sortedInfo.columnKey === "plate_no" && state.sortedInfo.order,
},
{
title: t("jobs.fields.clm_no"),
dataIndex: "clm_no",
key: "clm_no",
ellipsis: true,
responsive: ["md"],
sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
sortOrder:
state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order,
render: (text, record) =>
`${record.clm_no || ""}${
record.po_number ? ` (PO: ${record.po_number})` : ""
}`,
},
{
title: t("jobs.fields.ins_co_nm"),
dataIndex: "ins_co_nm",
key: "ins_co_nm",
ellipsis: true,
filters:
(jobs &&
jobs
.map((j) => j.ins_co_nm)
.filter(onlyUnique)
.map((s) => {
return {
text: s,
value: [s],
};
})) ||
[],
onFilter: (value, record) => value.includes(record.ins_co_nm),
responsive: ["md"],
},
{
title: t("jobs.fields.clm_total"),
dataIndex: "clm_total",
key: "clm_total",
responsive: ["md"],
ellipsis: true,
sorter: (a, b) => a.clm_total - b.clm_total,
sortOrder:
state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order,
render: (text, record) => (
<CurrencyFormatter>{record.clm_total}</CurrencyFormatter>
),
},
{
title: t("jobs.labels.estimator"),
dataIndex: "jobs.labels.estimator",
key: "jobs.labels.estimator",
ellipsis: true,
responsive: ["xl"],
filterSearch: true,
filters:
(jobs &&
jobs
.map((j) => `${j.est_ct_fn || ""} ${j.est_ct_ln || ""}`.trim())
.filter(onlyUnique)
.map((s) => {
return {
text: s || "N/A",
value: [s],
};
})) ||
[],
onFilter: (value, record) =>
value.includes(
`${record.est_ct_fn || ""} ${record.est_ct_ln || ""}`.trim()
),
render: (text, record) =>
`${record.est_ct_fn || ""} ${record.est_ct_ln || ""}`.trim(),
},
{
title: t("jobs.fields.comment"),
dataIndex: "comment",
key: "comment",
ellipsis: true,
responsive: ["md"],
},
// {
// title: t("jobs.fields.owner_owing"),
// dataIndex: "owner_owing",
// key: "owner_owing",
// responsive: ["md"],
// render: (text, record) => (
// <CurrencyFormatter>{record.owner_owing}</CurrencyFormatter>
// ),
// },
];
const scrollMapper = {
xs: true,
sm: true,
md: true,
lg: "100%",
xl: "100%",
xxl: "100%",
};
return (
<Card
title={t("titles.bc.jobs-active")}
extra={
<Space wrap>
<Button onClick={() => refetch()}>
<SyncOutlined />
</Button>
<Input.Search
placeholder={t("general.labels.search")}
onChange={(e) => {
setSearchText(e.target.value);
}}
value={searchText}
enterButton
/>
</Space>
}
>
<Table
loading={loading}
pagination={{ defaultPageSize: 50 }}
columns={columns}
rowKey="id"
dataSource={jobs}
scroll={{
x: selectedBreakpoint ? scrollMapper[selectedBreakpoint[0]] : "100%",
}}
rowSelection={{
onSelect: (record) => {
handleOnRowClick(record);
},
selectedRowKeys: [selected],
type: "radio",
}}
onChange={handleTableChange}
onRow={(record, rowIndex) => {
return {
onClick: (event) => {
handleOnRowClick(record);
},
};
}}
/>
</Card>
);
} }
export default connect(mapStateToProps, null)(JobsList); export default connect(mapStateToProps, null)(JobsList);

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}>
<Link to={`/manage/jobs/${j?.id}`}>{`${j.ro_number || "N/A"}${ {disabled ? (
j.clm_no ? ` | ${j.clm_no}` : "" <>{`${j.ro_number || "N/A"}${j.clm_no ? ` | ${j.clm_no}` : ""}${
}${j.status ? ` | ${j.status}` : ""}`}</Link> j.status ? ` | ${j.status}` : ""
}`}</>
) : (
<Link to={`/manage/jobs/${j?.id}`}>{`${j.ro_number || "N/A"}${
j.clm_no ? ` | ${j.clm_no}` : ""
}${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,13 +100,18 @@ 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) =>
<Link to={`/manage/vehicles/${record.vehicleid}`}>{`${ technician ? (
record.v_model_yr || "" <>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
} ${record.v_make_desc || ""} ${record.v_model_desc || ""} ${ record.v_model_desc || ""
record.v_color || "" } ${record.v_color || ""} ${record.plate_no || ""}`}</>
} ${record.plate_no || ""}`}</Link> ) : (
), <Link to={`/manage/vehicles/${record.vehicleid}`}>{`${
record.v_model_yr || ""
} ${record.v_make_desc || ""} ${record.v_model_desc || ""} ${
record.v_color || ""
} ${record.plate_no || ""}`}</Link>
),
}, },
{ {
title: i18n.t("jobs.fields.actual_in"), title: i18n.t("jobs.fields.actual_in"),

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,65 +105,85 @@ export function ProductionListDetail({
{error && <AlertComponent error={JSON.stringify(error)} />} {error && <AlertComponent error={JSON.stringify(error)} />}
{!loading && data && ( {!loading && data && (
<div> <div>
<JobEmployeeAssignments job={data.jobs_by_pk} refetch={refetch} /> <Space direction="vertical">
<Descriptions bordered column={1}> <CardTemplate
<Descriptions.Item label={t("jobs.fields.ro_number")}> title={t("jobs.labels.employeeassignments")}
{theJob.ro_number || ""} loading={loading}
</Descriptions.Item> >
<Descriptions.Item label={t("jobs.fields.alt_transport")}> <JobEmployeeAssignments job={data.jobs_by_pk} refetch={refetch} />
<Space> </CardTemplate>
{data.jobs_by_pk.alt_transport || ""} <Descriptions bordered column={1}>
<JobAtChange event={{ job: data.jobs_by_pk }} /> <Descriptions.Item label={t("jobs.fields.ro_number")}>
</Space> {theJob.ro_number || ""}
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label={t("jobs.fields.clm_no")}> <Descriptions.Item label={t("jobs.fields.alt_transport")}>
{theJob.clm_no || ""} <Space>
</Descriptions.Item> {data.jobs_by_pk.alt_transport || ""}
<Descriptions.Item label={t("jobs.fields.ins_co_nm")}> <JobAtChange job={data.jobs_by_pk} />
{theJob.ins_co_nm || ""} </Space>
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label={t("jobs.fields.owner")}> <Descriptions.Item label={t("jobs.fields.clm_no")}>
<OwnerNameDisplay ownerObject={theJob} /> {theJob.clm_no || ""}
<StartChatButton </Descriptions.Item>
phone={data.jobs_by_pk.ownr_ph1} <Descriptions.Item label={t("jobs.fields.ins_co_nm")}>
jobid={data.jobs_by_pk.id} {theJob.ins_co_nm || ""}
/> </Descriptions.Item>
<StartChatButton <Descriptions.Item label={t("jobs.fields.owner")}>
phone={data.jobs_by_pk.ownr_ph2} <Space>
jobid={data.jobs_by_pk.id} <OwnerNameDisplay ownerObject={theJob} />
/> {!technician ? (
</Descriptions.Item> <>
<Descriptions.Item label={t("jobs.fields.vehicle")}> <StartChatButton
{`${theJob.v_model_yr || ""} ${theJob.v_color || ""} ${ phone={data.jobs_by_pk.ownr_ph1}
theJob.v_make_desc || "" jobid={data.jobs_by_pk.id}
} ${theJob.v_model_desc || ""}`} />
</Descriptions.Item> <StartChatButton
<Descriptions.Item label={t("jobs.fields.clm_total")}> phone={data.jobs_by_pk.ownr_ph2}
<CurrencyFormatter>{theJob.clm_total}</CurrencyFormatter> jobid={data.jobs_by_pk.id}
</Descriptions.Item> />
<Descriptions.Item label={t("jobs.fields.actual_in")}> </>
<DateFormatter>{theJob.actual_in}</DateFormatter> ) : (
</Descriptions.Item> <>
<Descriptions.Item label={t("jobs.fields.scheduled_completion")}> <PhoneNumberFormatter>
<DateFormatter>{theJob.scheduled_completion}</DateFormatter> {data.jobs_by_pk.ownr_ph1}
</Descriptions.Item> </PhoneNumberFormatter>
</Descriptions> <PhoneNumberFormatter>
{data.jobs_by_pk.ownr_ph2}
<JobDetailCardsPartsComponent </PhoneNumberFormatter>
loading={loading} </>
data={data ? data.jobs_by_pk : null} )}
/> </Space>
</Descriptions.Item>
<JobDetailCardsNotesComponent <Descriptions.Item label={t("jobs.fields.vehicle")}>
loading={loading} {`${theJob.v_model_yr || ""} ${theJob.v_color || ""} ${
data={data ? data.jobs_by_pk : null} theJob.v_make_desc || ""
/> } ${theJob.v_model_desc || ""}`}
{!bodyshop.uselocalmediaserver && ( </Descriptions.Item>
<JobDetailCardsDocumentsComponent <Descriptions.Item label={t("jobs.fields.clm_total")}>
<CurrencyFormatter>{theJob.clm_total}</CurrencyFormatter>
</Descriptions.Item>
<Descriptions.Item label={t("jobs.fields.actual_in")}>
<DateFormatter>{theJob.actual_in}</DateFormatter>
</Descriptions.Item>
<Descriptions.Item label={t("jobs.fields.scheduled_completion")}>
<DateFormatter>{theJob.scheduled_completion}</DateFormatter>
</Descriptions.Item>
</Descriptions>
<JobDetailCardsPartsComponent
loading={loading} loading={loading}
data={data ? data.jobs_by_pk : null} data={data ? data.jobs_by_pk : null}
/> />
)} <JobDetailCardsNotesComponent
loading={loading}
data={data ? data.jobs_by_pk : null}
/>
{!bodyshop.uselocalmediaserver && (
<JobDetailCardsDocumentsComponent
loading={loading}
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;