- merge adjustments

Signed-off-by: Dave Richer <dave@imexsystems.ca>
This commit is contained in:
Dave Richer
2024-01-26 21:20:49 -05:00
142 changed files with 54394 additions and 3099 deletions

View File

@@ -13,7 +13,7 @@ import Icon, {
FileFilled,
//GlobalOutlined,
HomeFilled,
ImportOutlined,
ImportOutlined, InfoCircleOutlined,
LineChartOutlined,
PaperClipOutlined,
PhoneOutlined,
@@ -26,8 +26,8 @@ import Icon, {
UserOutlined,
} from "@ant-design/icons";
import { useTreatments } from "@splitsoftware/splitio-react";
import { Layout, Menu } from "antd";
import React from "react";
import {Layout, Menu, Switch, Tooltip} from "antd";
import React, {useEffect, useState} from "react";
import { useTranslation } from "react-i18next";
import { BsKanban } from "react-icons/bs";
import {
@@ -52,6 +52,7 @@ import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import {handleBeta, setBeta, checkBeta} from "../../utils/handleBeta";
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
@@ -102,9 +103,21 @@ function Header({
{},
bodyshop && bodyshop.imexshopid
);
const [betaSwitch, setBetaSwitch] = useState(false);
const { t } = useTranslation();
useEffect(() => {
const isBeta = checkBeta();
setBetaSwitch(isBeta);
}, []);
const betaSwitchChange = (checked) => {
setBeta(checked);
setBetaSwitch(checked);
handleBeta();
}
return (
<Layout.Header>
<Menu
@@ -430,6 +443,17 @@ function Header({
</Menu.Item>
))}
</Menu.SubMenu>
<Menu.Item style={{marginLeft: 'auto'}} key="profile">
<Tooltip title="A more modern ImEX Online is ready for you to try! You can switch back at any time.">
<InfoCircleOutlined/>
<span style={{marginRight: 8}}>Try the new ImEX Online</span>
<Switch
checked={betaSwitch}
onChange={betaSwitchChange}
/>
</Tooltip>
</Menu.Item>
</Menu>
</Layout.Header>
);

View File

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

View File

@@ -1,15 +1,37 @@
import React from "react";
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 { 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,
title,
extraLink,
technician,
...otherProps
}) {
const { t } = useTranslation();
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 (
<Card
size="small"

View File

@@ -0,0 +1,246 @@
import React, {useCallback, useEffect, useState} from 'react';
import moment from "moment";
import axios from 'axios';
import {Badge, Card, Space, Table, Tag} from 'antd';
import {gql, useQuery} from "@apollo/client";
import {DateTimeFormatterFunction} from "../../utils/DateFormatter";
import {isEmpty} from "lodash";
import {useTranslation} from "react-i18next";
require('./job-lifecycle.styles.scss');
// show text on bar if text can fit
export function JobLifecycleComponent({job, statuses, ...rest}) {
const [loading, setLoading] = useState(true);
const [lifecycleData, setLifecycleData] = useState(null);
const {t} = useTranslation(); // Used for tracking external state changes.
const {data} = useQuery(gql`
query get_job_test($id: uuid!){
jobs_by_pk(id:$id){
id
status
}
}
`, {
variables: {
id: job.id
},
fetchPolicy: 'cache-only'
});
/**
* Gets the lifecycle data for the job.
* @returns {Promise<void>}
*/
const getLifecycleData = useCallback(async () => {
if (job && job.id && statuses && statuses.statuses) {
try {
setLoading(true);
const response = await axios.post("/job/lifecycle", {
jobids: job.id,
statuses: statuses.statuses
});
const data = response.data.transition[job.id];
setLifecycleData(data);
} catch (err) {
console.error(`${t('job_lifecycle.errors.fetch')}: ${err.message}`);
} finally {
setLoading(false);
}
}
}, [job, statuses, t]);
useEffect(() => {
if (!data) return;
setTimeout(() => {
getLifecycleData().catch(err => console.error(`${t('job_lifecycle.errors.fetch')}: ${err.message}`));
}, 500);
}, [data, getLifecycleData, t]);
const columns = [
{
title: t('job_lifecycle.columns.value'),
dataIndex: 'value',
key: 'value',
},
{
title: t('job_lifecycle.columns.start'),
dataIndex: 'start',
key: 'start',
render: (text) => DateTimeFormatterFunction(text),
sorter: (a, b) => moment(a.start).unix() - moment(b.start).unix(),
},
{
title: t('job_lifecycle.columns.relative_start'),
dataIndex: 'start_readable',
key: 'start_readable',
},
{
title: t('job_lifecycle.columns.end'),
dataIndex: 'end',
key: 'end',
sorter: (a, b) => {
if (isEmpty(a.end) || isEmpty(b.end)) {
if (isEmpty(a.end) && isEmpty(b.end)) {
return 0;
}
return isEmpty(a.end) ? 1 : -1;
}
return moment(a.end).unix() - moment(b.end).unix();
},
render: (text) => isEmpty(text) ? t('job_lifecycle.content.not_available') : DateTimeFormatterFunction(text)
},
{
title: t('job_lifecycle.columns.relative_end'),
dataIndex: 'end_readable',
key: 'end_readable',
},
{
title: t('job_lifecycle.columns.duration'),
dataIndex: 'duration_readable',
key: 'duration_readable',
sorter: (a, b) => a.duration - b.duration,
},
];
return (
<Card loading={loading} title={t('job_lifecycle.content.title')}>
{!loading ? (
lifecycleData && lifecycleData.lifecycle && lifecycleData.durations ? (
<Space direction='vertical' style={{width: '100%'}}>
<Card
type='inner'
title={(
<Space direction='horizontal' size='small'>
<Badge status='processing' count={lifecycleData.durations.totalStatuses}/>
{t('job_lifecycle.content.title_durations')}
</Space>
)}
style={{width: '100%'}}
>
<div id="bar-container" style={{
display: 'flex',
width: '100%',
height: '100px',
textAlign: 'center',
borderRadius: '5px',
borderWidth: '5px',
borderStyle: 'solid',
borderColor: '#f0f2f5',
margin: 0,
padding: 0
}}>
{lifecycleData.durations.summations.map((key, index, array) => {
const isFirst = index === 0;
const isLast = index === array.length - 1;
return (
<div key={key.status} style={{
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
margin: 0,
padding: 0,
borderTop: '1px solid #f0f2f5',
borderBottom: '1px solid #f0f2f5',
borderLeft: isFirst ? '1px solid #f0f2f5' : undefined,
borderRight: isLast ? '1px solid #f0f2f5' : undefined,
backgroundColor: key.color,
width: `${key.percentage}%`
}}
aria-label={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
title={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
>
{key.percentage > 15 ?
<>
<div>{key.roundedPercentage}</div>
<div style={{
backgroundColor: '#f0f2f5',
borderRadius: '5px',
paddingRight: '2px',
paddingLeft: '2px',
fontSize: '0.8rem',
}}>
{key.status}
</div>
</>
: null}
</div>
);
})}
</div>
<Card type='inner' title={t('job_lifecycle.content.legend_title')}
style={{marginTop: '10px'}}>
<div>
{lifecycleData.durations.summations.map((key) => (
<Tag color={key.color} style={{width: '13vh', padding: '4px', margin: '4px'}}>
<div
aria-label={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
title={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
style={{
backgroundColor: '#f0f2f5',
color: '#000',
padding: '4px',
textAlign: 'center'
}}>
{key.status} ({key.roundedPercentage})
</div>
</Tag>
))}
</div>
</Card>
{(lifecycleData?.durations?.humanReadableTotal) ||
(lifecycleData.lifecycle[0] && lifecycleData.lifecycle[0].value && lifecycleData?.durations?.totalCurrentStatusDuration?.humanReadable) ?
<Card style={{marginTop: '10px'}}>
<ul>
{lifecycleData.durations && lifecycleData.durations.humanReadableTotal &&
<li>
<span
style={{fontWeight: 'bold'}}>{t('job_lifecycle.content.previous_status_accumulated_time')}:</span> {lifecycleData.durations.humanReadableTotal}
</li>
}
{lifecycleData.lifecycle[0] && lifecycleData.lifecycle[0].value && lifecycleData?.durations?.totalCurrentStatusDuration?.humanReadable &&
<li>
<span
style={{fontWeight: 'bold'}}>{t('job_lifecycle.content.current_status_accumulated_time')} ({lifecycleData.lifecycle[0].value}):</span> {lifecycleData.durations.totalCurrentStatusDuration.humanReadable}
</li>
}
</ul>
</Card>
: null}
</Card>
<Card type='inner' title={(
<>
<Space direction="horizontal" size="small">
<Badge status='processing' count={lifecycleData.lifecycle.length}/>
{t('job_lifecycle.content.title_transitions')}
</Space>
</>
)}>
<Table style={{
overflow: 'auto',
width: '100%',
}} columns={columns} dataSource={lifecycleData.lifecycle}/>
</Card>
</Space>
) : (
<Card type='inner' style={{textAlign: 'center'}}>
{t('job_lifecycle.content.data_unavailable')}
</Card>
)
) : (
<Card type='inner' title={t('job_lifecycle.content.title_loading')}>
{t('job_lifecycle.content.loading')}
</Card>
)}
</Card>
);
}
export default JobLifecycleComponent;

View File

@@ -53,12 +53,14 @@ export function JobsAdminStatus({ insertAuditTrail, bodyshop, job }) {
);
return (
<Dropdown overlay={statusmenu} trigger={["click"]} key="changestatus">
<Button shape="round">
<span>{job.status}</span>
<>
<Dropdown overlay={statusmenu} trigger={["click"]} key="changestatus">
<Button shape="round">
<span>{job.status}</span>
<DownCircleFilled />
</Button>
</Dropdown>
<DownCircleFilled />
</Button>
</Dropdown>
</>
);
}

View File

@@ -1,34 +1,18 @@
import { useMutation } from "@apollo/client";
import { Button, notification } from "antd";
import { gql } from "@apollo/client";
import { Button, Space, notification } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import {
DELETE_DELIVERY_CHECKLIST,
DELETE_INTAKE_CHECKLIST,
} from "../../graphql/jobs.queries";
export default function JobAdminDeleteIntake({ job }) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [deleteIntake] = useMutation(gql`
mutation DELETE_INTAKE($jobId: uuid!) {
update_jobs_by_pk(
pk_columns: { id: $jobId }
_set: { intakechecklist: null }
) {
id
intakechecklist
}
}
`);
const [DELETE_DELIVERY] = useMutation(gql`
mutation DELETE_DELIVERY($jobId: uuid!) {
update_jobs_by_pk(
pk_columns: { id: $jobId }
_set: { deliverchecklist: null }
) {
id
deliverchecklist
}
}
`);
const [deleteIntake] = useMutation(DELETE_INTAKE_CHECKLIST);
const [deleteDelivery] = useMutation(DELETE_DELIVERY_CHECKLIST);
const handleDelete = async (values) => {
setLoading(true);
@@ -50,7 +34,7 @@ export default function JobAdminDeleteIntake({ job }) {
const handleDeleteDelivery = async (values) => {
setLoading(true);
const result = await DELETE_DELIVERY({
const result = await deleteDelivery({
variables: { jobId: job.id },
});
@@ -68,12 +52,22 @@ export default function JobAdminDeleteIntake({ job }) {
return (
<>
<Button loading={loading} onClick={handleDelete}>
{t("jobs.labels.deleteintake")}
</Button>
<Button loading={loading} onClick={handleDeleteDelivery}>
{t("jobs.labels.deletedelivery")}
</Button>
<Space wrap>
<Button
loading={loading}
onClick={handleDelete}
disabled={!job.intakechecklist}
>
{t("jobs.labels.deleteintake")}
</Button>
<Button
loading={loading}
onClick={handleDeleteDelivery}
disabled={!job.deliverychecklist}
>
{t("jobs.labels.deletedelivery")}
</Button>
</Space>
</>
);
}

View File

@@ -1,5 +1,5 @@
import { gql, useMutation } from "@apollo/client";
import { Button, notification } from "antd";
import { useMutation } from "@apollo/client";
import { Button, Space, notification } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
@@ -7,6 +7,11 @@ import moment from "moment";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
import {
MARK_JOB_AS_EXPORTED,
MARK_JOB_AS_UNINVOICED,
MARK_JOB_FOR_REEXPORT,
} from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import {
selectBodyshop,
@@ -35,58 +40,18 @@ export function JobAdminMarkReexport({
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [insertExportLog] = useMutation(INSERT_EXPORT_LOG);
const [markJobForReexport] = useMutation(gql`
mutation MARK_JOB_FOR_REEXPORT($jobId: uuid!) {
update_jobs_by_pk(
pk_columns: { id: $jobId }
_set: { date_exported: null
status: "${bodyshop.md_ro_statuses.default_invoiced}"
}
) {
id
date_exported
status
date_invoiced
}
}
`);
const [markJobExported] = useMutation(gql`
mutation MARK_JOB_AS_EXPORTED($jobId: uuid!, $date_exported: timestamptz!) {
update_jobs_by_pk(
pk_columns: { id: $jobId }
_set: { date_exported: $date_exported
status: "${bodyshop.md_ro_statuses.default_exported}"
}
) {
id
date_exported
date_invoiced
status
}
}
`);
const [markJobUninvoiced] = useMutation(gql`
mutation MARK_JOB_AS_UNINVOICED($jobId: uuid!, ) {
update_jobs_by_pk(
pk_columns: { id: $jobId }
_set: { date_exported: null
date_invoiced: null
status: "${bodyshop.md_ro_statuses.default_delivered}"
}
) {
id
date_exported
date_invoiced
status
}
}
`);
const [markJobForReexport] = useMutation(MARK_JOB_FOR_REEXPORT);
const [markJobExported] = useMutation(MARK_JOB_AS_EXPORTED);
const [markJobUninvoiced] = useMutation(MARK_JOB_AS_UNINVOICED);
const handleMarkForExport = async () => {
setLoading(true);
const result = await markJobForReexport({
variables: { jobId: job.id },
variables: {
jobId: job.id,
default_invoiced: bodyshop.md_ro_statuses.default_invoiced,
},
});
if (!result.errors) {
@@ -108,7 +73,11 @@ export function JobAdminMarkReexport({
const handleMarkExported = async () => {
setLoading(true);
const result = await markJobExported({
variables: { jobId: job.id, date_exported: moment() },
variables: {
jobId: job.id,
date_exported: moment(),
default_exported: bodyshop.md_ro_statuses.default_exported,
},
});
await insertExportLog({
@@ -144,7 +113,10 @@ export function JobAdminMarkReexport({
const handleUninvoice = async () => {
setLoading(true);
const result = await markJobUninvoiced({
variables: { jobId: job.id },
variables: {
jobId: job.id,
default_delivered: bodyshop.md_ro_statuses.default_delivered,
},
});
if (!result.errors) {
@@ -165,27 +137,29 @@ export function JobAdminMarkReexport({
return (
<>
<Button
loading={loading}
disabled={!job.date_exported}
onClick={handleMarkForExport}
>
{t("jobs.labels.markforreexport")}
</Button>
<Button
loading={loading}
disabled={job.date_exported}
onClick={handleMarkExported}
>
{t("jobs.actions.markasexported")}
</Button>
<Button
loading={loading}
disabled={!job.date_invoiced || job.date_exported}
onClick={handleUninvoice}
>
{t("jobs.actions.uninvoice")}
</Button>
<Space wrap>
<Button
loading={loading}
disabled={!job.date_exported}
onClick={handleMarkForExport}
>
{t("jobs.labels.markforreexport")}
</Button>
<Button
loading={loading}
disabled={job.date_exported}
onClick={handleMarkExported}
>
{t("jobs.actions.markasexported")}
</Button>
<Button
loading={loading}
disabled={!job.date_invoiced || job.date_exported}
onClick={handleUninvoice}
>
{t("jobs.actions.uninvoice")}
</Button>
</Space>
</>
);
}

View File

@@ -0,0 +1,65 @@
import { useMutation } from "@apollo/client";
import { Switch, notification } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { UPDATE_REMOVE_FROM_AR } from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
const mapStateToProps = createStructuredSelector({});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
});
export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminRemoveAR);
export function JobsAdminRemoveAR({ insertAuditTrail, job }) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [switchValue, setSwitchValue] = useState(job.remove_from_ar);
const [mutationUpdateRemoveFromAR] = useMutation(UPDATE_REMOVE_FROM_AR);
const handleChange = async (value) => {
setLoading(true);
const result = await mutationUpdateRemoveFromAR({
variables: { jobId: job.id, remove_from_ar: value },
});
if (!result.errors) {
notification["success"]({ message: t("jobs.successes.save") });
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.admin_job_remove_from_ar(value),
});
setSwitchValue(value);
} else {
notification["error"]({
message: t("jobs.errors.saving", {
error: JSON.stringify(result.errors),
}),
});
}
setLoading(false);
};
return (
<>
<div style={{ display: "flex", alignItems: "center" }}>
<div style={{ marginRight: "10px" }}>
{t("jobs.labels.remove_from_ar")}:
</div>
<div>
<Switch
checked={switchValue}
loading={loading}
onChange={handleChange}
/>
</div>
</div>
</>
);
}

View File

@@ -1,9 +1,10 @@
import { gql, useMutation } from "@apollo/client";
import { useMutation } from "@apollo/client";
import { Button, notification } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { UNVOID_JOB } from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import {
selectBodyshop,
@@ -29,66 +30,17 @@ export function JobsAdminUnvoid({
}) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [updateJob] = useMutation(gql`
mutation UNVOID_JOB($jobId: uuid!) {
update_jobs_by_pk(pk_columns: {id: $jobId}, _set: {voided: false, status: "${
bodyshop.md_ro_statuses.default_imported
}", date_void: null}) {
id
date_void
voided
status
}
insert_notes(objects: {jobid: $jobId, audit: true, created_by: "${
currentUser.email
}", text: "${t("jobs.labels.unvoidnote")}"}) {
returning {
id
}
}
}
`);
// const result = await voidJob({
// variables: {
// jobId: job.id,
// job: {
// status: bodyshop.md_ro_statuses.default_void,
// voided: true,
// },
// note: [
// {
// jobid: job.id,
// created_by: currentUser.email,
// audit: true,
// text: t("jobs.labels.voidnote", {
// date: moment().format("MM/DD/yyy"),
// time: moment().format("hh:mm a"),
// }),
// },
// ],
// },
// });
// if (!!!result.errors) {
// notification["success"]({
// message: t("jobs.successes.voided"),
// });
// //go back to jobs list.
// history.push(`/manage/`);
// } else {
// notification["error"]({
// message: t("jobs.errors.voiding", {
// error: JSON.stringify(result.errors),
// }),
// });
// }
const [mutationUnvoidJob] = useMutation(UNVOID_JOB);
const handleUpdate = async (values) => {
setLoading(true);
const result = await updateJob({
variables: { jobId: job.id },
const result = await mutationUnvoidJob({
variables: {
jobId: job.id,
default_imported: bodyshop.md_ro_statuses.default_imported,
currentUserEmail: currentUser.email,
text: t("jobs.labels.unvoidnote"),
},
});
if (!result.errors) {
@@ -110,8 +62,10 @@ mutation UNVOID_JOB($jobId: uuid!) {
};
return (
<Button loading={loading} disabled={!job.voided} onClick={handleUpdate}>
{t("jobs.actions.unvoid")}
</Button>
<>
<Button loading={loading} disabled={!job.voided} onClick={handleUpdate}>
{t("jobs.actions.unvoid")}
</Button>
</>
);
}

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
import {
SyncOutlined,
BranchesOutlined,
ExclamationCircleFilled,
PauseCircleOutlined,
BranchesOutlined,
SyncOutlined,
} from "@ant-design/icons";
import { useQuery } from "@apollo/client";
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 { QUERY_ALL_ACTIVE_JOBS } from "../../graphql/jobs.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { onlyUnique } from "../../utils/arrayHelper";
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 ChatOpenButton from "../chat-open-button/chat-open-button.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
bodyshop: selectBodyshop,
});
export function JobsList({ bodyshop }) {
const searchParams = queryString.parse(useLocation().search);
const { selected } = searchParams;
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
.filter((screen) => !!screen[1])
.slice(-1)[0];
const { loading, error, data, refetch } = useQuery(QUERY_ALL_ACTIVE_JOBS, {
variables: {
statuses: bodyshop.md_ro_statuses.active_statuses || ["Open", "Open*"],
},
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
const searchParams = queryString.parse(useLocation().search);
const { selected } = searchParams;
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
.filter((screen) => !!screen[1])
.slice(-1)[0];
const { loading, error, data, refetch } = useQuery(QUERY_ALL_ACTIVE_JOBS, {
variables: {
statuses: bodyshop.md_ro_statuses.active_statuses || ["Open", "Open*"],
},
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: { text: "" },
});
const [state, setState] = useState({ sortedInfo: {} });
const [filter, setFilter] = useLocalStorage("filter_jobs_list", null);
const { t } = useTranslation();
const history = useHistory();
const [searchText, setSearchText] = useState("");
const { t } = useTranslation();
const history = useHistory();
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
? searchText === ""
? data.jobs
: data.jobs.filter(
(j) =>
(j.ro_number || "")
.toString()
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.ownr_co_nm || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.comments || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.ownr_fn || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.ownr_ln || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.clm_no || "").toLowerCase().includes(searchText.toLowerCase()) ||
(j.plate_no || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.v_model_desc || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.est_ct_fn || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.est_ct_ln || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.v_make_desc || "")
.toLowerCase()
.includes(searchText.toLowerCase())
)
: [];
const jobs = data
? searchText === ""
? data.jobs
: data.jobs.filter(
(j) =>
(j.ro_number || "")
.toString()
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.ownr_co_nm || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.comments || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.ownr_fn || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.ownr_ln || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.clm_no || "").toLowerCase().includes(searchText.toLowerCase()) ||
(j.plate_no || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.v_model_desc || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.est_ct_fn || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.est_ct_ln || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.v_make_desc || "")
.toLowerCase()
.includes(searchText.toLowerCase())
)
: [];
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, sortedInfo: sorter });
setFilter(filters);
};
const handleOnRowClick = (record) => {
if (record) {
if (record.id) {
history.push({
search: queryString.stringify({
...searchParams,
selected: record.id,
}),
});
}
}
};
const handleOnRowClick = (record) => {
if (record) {
if (record.id) {
history.push({
search: queryString.stringify({
...searchParams,
selected: record.id,
}),
});
}
}
};
const columns = [
{
title: t("jobs.fields.ro_number"),
dataIndex: "ro_number",
key: "ro_number",
sorter: (a, b) =>
parseInt((a.ro_number || "0").replace(/\D/g, "")) -
parseInt((b.ro_number || "0").replace(/\D/g, "")),
sortOrder:
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
render: (text, record) => (
<Link
to={"/manage/jobs/" + record.id}
onClick={(e) => e.stopPropagation()}
>
<Space>
{record.ro_number || t("general.labels.na")}
{record.production_vars && record.production_vars.alert ? (
<ExclamationCircleFilled className="production-alert" />
) : null}
{record.suspended && (
<PauseCircleOutlined style={{ color: "orangered" }} />
)}
{record.iouparent && (
<Tooltip title={t("jobs.labels.iou")}>
<BranchesOutlined style={{ color: "orangered" }} />
</Tooltip>
)}
</Space>
</Link>
),
},
{
title: t("jobs.fields.owner"),
dataIndex: "owner",
key: "owner",
ellipsis: true,
responsive: ["md"],
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => {
return record.ownerid ? (
<Link
to={"/manage/owners/" + record.ownerid}
onClick={(e) => e.stopPropagation()}
>
<OwnerNameDisplay ownerObject={record} />
</Link>
) : (
<span>
const columns = [
{
title: t("jobs.fields.ro_number"),
dataIndex: "ro_number",
key: "ro_number",
sorter: (a, b) =>
parseInt((a.ro_number || "0").replace(/\D/g, "")) -
parseInt((b.ro_number || "0").replace(/\D/g, "")),
sortOrder:
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
render: (text, record) => (
<Link
to={"/manage/jobs/" + record.id}
onClick={(e) => e.stopPropagation()}
>
<Space>
{record.ro_number || t("general.labels.na")}
{record.production_vars && record.production_vars.alert ? (
<ExclamationCircleFilled className="production-alert" />
) : null}
{record.suspended && (
<PauseCircleOutlined style={{ color: "orangered" }} />
)}
{record.iouparent && (
<Tooltip title={t("jobs.labels.iou")}>
<BranchesOutlined style={{ color: "orangered" }} />
</Tooltip>
)}
</Space>
</Link>
),
},
{
title: t("jobs.fields.owner"),
dataIndex: "owner",
key: "owner",
ellipsis: true,
responsive: ["md"],
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => {
return record.ownerid ? (
<Link
to={"/manage/owners/" + record.ownerid}
onClick={(e) => e.stopPropagation()}
>
<OwnerNameDisplay ownerObject={record} />
</Link>
) : (
<span>
<OwnerNameDisplay ownerObject={record} />
</span>
);
);
},
},
{
title: t("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",
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>
);
};
}}
/>
</Card>
);
}
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 CurrencyFormatter from "../../utils/CurrencyFormatter";
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 ChatOpenButton from "../chat-open-button/chat-open-button.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import {pageLimit} from "../../utils/config";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -53,10 +54,8 @@ export function JobsReadyList({ bodyshop }) {
nextFetchPolicy: "network-only",
});
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: { text: "" },
});
const [state, setState] = useState({ sortedInfo: {} });
const [filter, setFilter] = useLocalStorage("filter_jobs_ready", null);
const { t } = useTranslation();
const history = useHistory();
@@ -105,7 +104,8 @@ export function JobsReadyList({ bodyshop }) {
: [];
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
setState({ ...state, sortedInfo: sorter });
setFilter(filters);
};
const handleOnRowClick = (record) => {
@@ -129,7 +129,6 @@ export function JobsReadyList({ bodyshop }) {
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
sortOrder:
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
render: (text, record) => (
<Link
to={"/manage/jobs/" + record.id}
@@ -157,7 +156,6 @@ export function JobsReadyList({ bodyshop }) {
dataIndex: "owner",
key: "owner",
ellipsis: true,
responsive: ["md"],
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
sortOrder:
@@ -197,16 +195,15 @@ export function JobsReadyList({ bodyshop }) {
<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
@@ -217,11 +214,17 @@ export function JobsReadyList({ bodyshop }) {
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",
@@ -274,6 +277,7 @@ export function JobsReadyList({ bodyshop }) {
dataIndex: "ins_co_nm",
key: "ins_co_nm",
ellipsis: true,
filteredValue: filter?.ins_co_nm || null,
filters:
(jobs &&
jobs
@@ -281,10 +285,11 @@ export function JobsReadyList({ bodyshop }) {
.filter(onlyUnique)
.map((s) => {
return {
text: s,
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"],
@@ -295,7 +300,6 @@ export function JobsReadyList({ bodyshop }) {
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,
@@ -306,9 +310,10 @@ export function JobsReadyList({ bodyshop }) {
{
title: t("jobs.labels.estimator"),
dataIndex: "jobs.labels.estimator",
key: "jobs.labels.estimator",
key: "estimator",
ellipsis: true,
responsive: ["xl"],
filteredValue: filter?.estimator || null,
filterSearch: true,
filters:
(jobs &&
@@ -317,10 +322,11 @@ export function JobsReadyList({ bodyshop }) {
.filter(onlyUnique)
.map((s) => {
return {
text: s || "N/A",
text: s || "No Estimator*",
value: [s],
};
})) ||
})
.sort((a, b) => alphaSort(a.text, b.text))) ||
[],
onFilter: (value, record) =>
value.includes(

View File

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

View File

@@ -101,7 +101,14 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
dataIndex: "ownr",
key: "ownr",
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),
sortOrder:
state.sortedInfo.columnKey === "ownr" && state.sortedInfo.order,
@@ -118,13 +125,18 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
),
sortOrder:
state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
render: (text, record) => (
<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>
),
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}`}>{`${
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"),

View File

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

View File

@@ -93,8 +93,8 @@ export function ReportCenterModalComponent({ reportCenterModal, bodyshop }) {
const handleFinish = async (values) => {
setLoading(true);
const start = values.dates[0];
const end = values.dates[1];
const start = values.dates ? values.dates[0] : null;
const end = values.dates ? values.dates[1] : null;
const { id } = values;
await GenerateDocument(
@@ -264,20 +264,30 @@ export function ReportCenterModalComponent({ reportCenterModal, bodyshop }) {
else return null;
}}
</Form.Item>
<Form.Item
name="dates"
label={t("reportcenter.labels.dates")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<DatePicker.RangePicker
format="MM/DD/YYYY"
ranges={DatePIckerRanges}
/>
<Form.Item style={{ margin: 0, padding: 0 }} dependencies={["key"]}>
{() => {
const key = form.getFieldValue("key");
const datedisable = Templates[key] && Templates[key].datedisable;
if (datedisable !== true) {
return (
<Form.Item
name="dates"
label={t("reportcenter.labels.dates")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<DatePicker.RangePicker
format="MM/DD/YYYY"
ranges={DatePIckerRanges}
/>
</Form.Item>
);
} else return null;
}}
</Form.Item>
<Form.Item style={{ margin: 0, padding: 0 }} dependencies={["key"]}>
{() => {

View File

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

View File

@@ -1,14 +1,20 @@
import { Button, Table } from "antd";
import queryString from "query-string";
import React from "react";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { useHistory, useLocation } from "react-router-dom";
import { alphaSort } from "../../utils/sorters";
export default function ShopEmployeesListComponent({ loading, employees }) {
const { t } = useTranslation();
const history = useHistory();
const search = queryString.parse(useLocation().search);
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: { text: "" },
});
const handleOnRowClick = (record) => {
if (record) {
search.employeeId = record.id;
@@ -18,32 +24,82 @@ export default function ShopEmployeesListComponent({ loading, employees }) {
history.push({ search: queryString.stringify(search) });
}
};
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
const columns = [
{
title: t("employees.fields.employee_number"),
dataIndex: "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"),
dataIndex: "first_name",
key: "first_name",
title: t("employees.labels.name"),
dataIndex: "employee_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"),
dataIndex: "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) =>
record.flat_rate
? t("employees.labels.flat_rate")
: 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 (
<div>
@@ -74,6 +130,7 @@ export default function ShopEmployeesListComponent({ loading, employees }) {
type: "radio",
selectedRowKeys: [search.employeeId],
}}
onChange={handleTableChange}
onRow={(record, rowIndex) => {
return {
onClick: (event) => {

View File

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

View File

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