Fix Formatting issues

Signed-off-by: Dave Richer <dave@imexsystems.ca>
This commit is contained in:
Dave Richer
2024-04-08 22:25:07 -04:00
parent df0f8ef9dc
commit 33c282051b
34 changed files with 1805 additions and 1820 deletions

View File

@@ -147,23 +147,19 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
/> />
} }
> >
{ <ProductFruits
import.meta.env.PRODUCT_FRUITS_DISABLED !== 'true' && ( workspaceCode={InstanceRenderMgr({
<ProductFruits imex: null,
workspaceCode={InstanceRenderMgr({ rome: null,
imex: null, promanager: "aoJoEifvezYI0Z0P"
rome: null, })}
promanager: "aoJoEifvezYI0Z0P" debug
})} language="en"
debug user={{
language="en" email: currentUser.email,
user={{ username: currentUser.email
email: currentUser.email, }}
username: currentUser.email />
}}
/>
)
}
<Routes> <Routes>
<Route <Route

View File

@@ -1,49 +1,55 @@
import {EditFilled, SyncOutlined} from "@ant-design/icons"; import { EditFilled, SyncOutlined } from "@ant-design/icons";
import {Button, Card, Checkbox, Input, Space, Table} from "antd"; import { Button, Card, Checkbox, Input, Space, Table } from "antd";
import React, {useState} from "react"; import React, { useState } from "react";
import {useTranslation} from "react-i18next"; import { useTranslation } from "react-i18next";
import {connect} from "react-redux"; import { connect } from "react-redux";
import {createStructuredSelector} from "reselect"; import { createStructuredSelector } from "reselect";
import {selectJobReadOnly} from "../../redux/application/application.selectors"; import { selectJobReadOnly } from "../../redux/application/application.selectors";
import {setModalContext} from "../../redux/modals/modals.actions"; 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 {DateFormatter} from "../../utils/DateFormatter"; import { DateFormatter } from "../../utils/DateFormatter";
import {alphaSort, dateSort} from "../../utils/sorters"; import { alphaSort, dateSort } from "../../utils/sorters";
import {TemplateList} from "../../utils/TemplateConstants"; import { TemplateList } from "../../utils/TemplateConstants";
import BillDeleteButton from "../bill-delete-button/bill-delete-button.component"; import BillDeleteButton from "../bill-delete-button/bill-delete-button.component";
import BillDetailEditReturnComponent from "../bill-detail-edit/bill-detail-edit-return.component"; import BillDetailEditReturnComponent from "../bill-detail-edit/bill-detail-edit-return.component";
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component"; import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
import {FaTasks} from "react-icons/fa"; import { FaTasks } from "react-icons/fa";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
jobRO: selectJobReadOnly, jobRO: selectJobReadOnly,
bodyshop: selectBodyshop, bodyshop: selectBodyshop
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
setBillEnterContext: (context) => dispatch(setModalContext({ setBillEnterContext: (context) =>
context: context, dispatch(
modal: "billEnter" setModalContext({
})), context: context,
setReconciliationContext: (context) => dispatch(setModalContext({ modal: "billEnter"
context: context, })
modal: "reconciliation" ),
})), setReconciliationContext: (context) =>
setTaskUpsertContext: (context) => dispatch(setModalContext({context, modal: 'taskUpsert'})), dispatch(
setModalContext({
context: context,
modal: "reconciliation"
})
),
setTaskUpsertContext: (context) => dispatch(setModalContext({ context, modal: "taskUpsert" }))
}); });
export function BillsListTableComponent({ export function BillsListTableComponent({
bodyshop, bodyshop,
jobRO, jobRO,
job, job,
billsQuery, billsQuery,
handleOnRowClick, handleOnRowClick,
setBillEnterContext, setBillEnterContext,
setReconciliationContext, setReconciliationContext,
setTaskUpsertContext, setTaskUpsertContext
}) { }) {
const {t} = useTranslation(); const { t } = useTranslation();
const [state, setState] = useState({ const [state, setState] = useState({
sortedInfo: {} sortedInfo: {}
@@ -54,27 +60,29 @@ export function BillsListTableComponent({
const Templates = TemplateList("bill"); const Templates = TemplateList("bill");
const bills = billsQuery.data ? billsQuery.data.bills : []; const bills = billsQuery.data ? billsQuery.data.bills : [];
const {refetch} = billsQuery; const { refetch } = billsQuery;
const recordActions = (record, showView = false) => ( const recordActions = (record, showView = false) => (
<Space wrap> <Space wrap>
{showView && ( {showView && (
<Button onClick={() => handleOnRowClick(record)}> <Button onClick={() => handleOnRowClick(record)}>
<EditFilled/> <EditFilled />
</Button> </Button>
)} )}
<Button title={t('tasks.buttons.create')} onClick={() => { <Button
setTaskUpsertContext({ title={t("tasks.buttons.create")}
context: { onClick={() => {
jobid: job.id, setTaskUpsertContext({
billid: record.id context: {
} jobid: job.id,
}); billid: record.id
}}> }
<FaTasks/> });
}}
>
<FaTasks />
</Button> </Button>
<BillDeleteButton bill={record} jobid={job.id}/> <BillDeleteButton bill={record} jobid={job.id} />
<BillDetailEditReturnComponent <BillDetailEditReturnComponent
data={{ bills_by_pk: { ...record, jobid: job.id, job: job } }} data={{ bills_by_pk: { ...record, jobid: job.id, job: job } }}
disabled={record.is_credit_memo || record.vendorid === bodyshop.inhousevendorid || jobRO} disabled={record.is_credit_memo || record.vendorid === bodyshop.inhousevendorid || jobRO}
@@ -84,9 +92,9 @@ export function BillsListTableComponent({
<PrintWrapperComponent <PrintWrapperComponent
templateObject={{ templateObject={{
name: Templates.inhouse_invoice.key, name: Templates.inhouse_invoice.key,
variables: {id: record.id} variables: { id: record.id }
}} }}
messageObject={{subject: Templates.inhouse_invoice.subject}} messageObject={{ subject: Templates.inhouse_invoice.subject }}
/> />
)} )}
</Space> </Space>
@@ -129,7 +137,7 @@ export function BillsListTableComponent({
key: "is_credit_memo", key: "is_credit_memo",
sorter: (a, b) => a.is_credit_memo - b.is_credit_memo, sorter: (a, b) => a.is_credit_memo - b.is_credit_memo,
sortOrder: state.sortedInfo.columnKey === "is_credit_memo" && state.sortedInfo.order, sortOrder: state.sortedInfo.columnKey === "is_credit_memo" && state.sortedInfo.order,
render: (text, record) => <Checkbox checked={record.is_credit_memo}/> render: (text, record) => <Checkbox checked={record.is_credit_memo} />
}, },
{ {
title: t("bills.fields.exported"), title: t("bills.fields.exported"),
@@ -137,7 +145,7 @@ export function BillsListTableComponent({
key: "exported", key: "exported",
sorter: (a, b) => a.exported - b.exported, sorter: (a, b) => a.exported - b.exported,
sortOrder: state.sortedInfo.columnKey === "exported" && state.sortedInfo.order, sortOrder: state.sortedInfo.columnKey === "exported" && state.sortedInfo.order,
render: (text, record) => <Checkbox checked={record.exported}/> render: (text, record) => <Checkbox checked={record.exported} />
}, },
{ {
title: t("general.labels.actions"), title: t("general.labels.actions"),
@@ -148,18 +156,18 @@ export function BillsListTableComponent({
]; ];
const handleTableChange = (pagination, filters, sorter) => { const handleTableChange = (pagination, filters, sorter) => {
setState({...state, filteredInfo: filters, sortedInfo: sorter}); setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
}; };
const filteredBills = bills const filteredBills = bills
? searchText === "" ? searchText === ""
? bills ? bills
: bills.filter( : bills.filter(
(b) => (b) =>
(b.invoice_number || "").toLowerCase().includes(searchText.toLowerCase()) || (b.invoice_number || "").toLowerCase().includes(searchText.toLowerCase()) ||
(b.vendor.name || "").toLowerCase().includes(searchText.toLowerCase()) || (b.vendor.name || "").toLowerCase().includes(searchText.toLowerCase()) ||
(b.total || "").toString().toLowerCase().includes(searchText.toLowerCase()) (b.total || "").toString().toLowerCase().includes(searchText.toLowerCase())
) )
: []; : [];
return ( return (
@@ -168,14 +176,14 @@ export function BillsListTableComponent({
extra={ extra={
<Space wrap> <Space wrap>
<Button onClick={() => refetch()}> <Button onClick={() => refetch()}>
<SyncOutlined/> <SyncOutlined />
</Button> </Button>
{job && job.converted ? ( {job && job.converted ? (
<> <>
<Button <Button
onClick={() => { onClick={() => {
setBillEnterContext({ setBillEnterContext({
actions: {refetch: billsQuery.refetch}, actions: { refetch: billsQuery.refetch },
context: { context: {
job job
} }
@@ -187,7 +195,7 @@ export function BillsListTableComponent({
<Button <Button
onClick={() => { onClick={() => {
setReconciliationContext({ setReconciliationContext({
actions: {refetch: billsQuery.refetch}, actions: { refetch: billsQuery.refetch },
context: { context: {
job, job,
bills: (billsQuery.data && billsQuery.data.bills) || [] bills: (billsQuery.data && billsQuery.data.bills) || []

View File

@@ -26,35 +26,26 @@ import Icon, {
UnorderedListOutlined, UnorderedListOutlined,
UserOutlined UserOutlined
} from "@ant-design/icons"; } from "@ant-design/icons";
import {useSplitTreatments} from "@splitsoftware/splitio-react"; import { useSplitTreatments } from "@splitsoftware/splitio-react";
import {Layout, Menu, Switch, Tooltip} from "antd"; import { Layout, Menu, Switch, Tooltip } from "antd";
import React, {useEffect, useState} from "react"; import React, { useEffect, useState } from "react";
import {useTranslation} from "react-i18next"; import { useTranslation } from "react-i18next";
import {BsKanban} from "react-icons/bs"; import { BsKanban } from "react-icons/bs";
import { import { FaCalendarAlt, FaCarCrash, FaCreditCard, FaFileInvoiceDollar, FaTasks } from "react-icons/fa";
FaCalendarAlt, import { GiPayMoney, GiPlayerTime, GiSettingsKnobs } from "react-icons/gi";
FaCarCrash, import { IoBusinessOutline } from "react-icons/io5";
FaCreditCard, import { RiSurveyLine } from "react-icons/ri";
FaFileInvoiceDollar, import { connect } from "react-redux";
FaTasks import { Link } from "react-router-dom";
} from "react-icons/fa"; import { createStructuredSelector } from "reselect";
import {GiPayMoney, GiPlayerTime, GiSettingsKnobs} from "react-icons/gi"; import { selectRecentItems, selectSelectedHeader } from "../../redux/application/application.selectors";
import {IoBusinessOutline} from "react-icons/io5"; import { setModalContext } from "../../redux/modals/modals.actions";
import {RiSurveyLine} from "react-icons/ri"; import { signOutStart } from "../../redux/user/user.actions";
import {connect} from "react-redux"; import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import {Link} from "react-router-dom"; import { FiLogOut } from "react-icons/fi";
import {createStructuredSelector} from "reselect"; import { checkBeta, handleBeta, setBeta } from "../../utils/betaHandler";
import {
selectRecentItems,
selectSelectedHeader
} from "../../redux/application/application.selectors";
import {setModalContext} from "../../redux/modals/modals.actions";
import {signOutStart} from "../../redux/user/user.actions";
import {selectBodyshop, selectCurrentUser} from "../../redux/user/user.selectors";
import {FiLogOut} from "react-icons/fi";
import {checkBeta, handleBeta, setBeta} from "../../utils/betaHandler";
import InstanceRenderManager from "../../utils/instanceRenderMgr"; import InstanceRenderManager from "../../utils/instanceRenderMgr";
import {HasFeatureAccess} from "../feature-wrapper/feature-wrapper.component"; import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
@@ -64,53 +55,68 @@ const mapStateToProps = createStructuredSelector({
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
setBillEnterContext: (context) => dispatch(setModalContext({ setBillEnterContext: (context) =>
context: context, dispatch(
modal: "billEnter" setModalContext({
})), context: context,
setTimeTicketContext: (context) => dispatch(setModalContext({ modal: "billEnter"
context: context, })
modal: "timeTicket" ),
})), setTimeTicketContext: (context) =>
setPaymentContext: (context) => dispatch(setModalContext({context: context, modal: "payment"})), dispatch(
setReportCenterContext: (context) => dispatch(setModalContext({ setModalContext({
context: context, context: context,
modal: "reportCenter" modal: "timeTicket"
})), })
),
setPaymentContext: (context) => dispatch(setModalContext({ context: context, modal: "payment" })),
setReportCenterContext: (context) =>
dispatch(
setModalContext({
context: context,
modal: "reportCenter"
})
),
signOutStart: () => dispatch(signOutStart()), signOutStart: () => dispatch(signOutStart()),
setCardPaymentContext: (context) => dispatch(setModalContext({ setCardPaymentContext: (context) =>
context: context, dispatch(
modal: "cardPayment" setModalContext({
})), context: context,
setTaskUpsertContext: (context) => dispatch(setModalContext({ modal: "cardPayment"
context: context, })
modal: 'taskUpsert' ),
})), setTaskUpsertContext: (context) =>
dispatch(
setModalContext({
context: context,
modal: "taskUpsert"
})
)
}); });
function Header({ function Header({
handleMenuClick, handleMenuClick,
currentUser, currentUser,
bodyshop, bodyshop,
selectedHeader, selectedHeader,
signOutStart, signOutStart,
setBillEnterContext, setBillEnterContext,
setTimeTicketContext, setTimeTicketContext,
setPaymentContext, setPaymentContext,
setReportCenterContext, setReportCenterContext,
recentItems, recentItems,
setCardPaymentContext, setCardPaymentContext,
setTaskUpsertContext, setTaskUpsertContext
}) { }) {
const { const {
treatments: {ImEXPay, DmsAp, Simple_Inventory} treatments: { ImEXPay, DmsAp, Simple_Inventory }
} = useSplitTreatments({ } = useSplitTreatments({
attributes: {}, attributes: {},
names: ["ImEXPay", "DmsAp", "Simple_Inventory"], names: ["ImEXPay", "DmsAp", "Simple_Inventory"],
splitKey: bodyshop && bodyshop.imexshopid splitKey: bodyshop && bodyshop.imexshopid
}); });
const [betaSwitch, setBetaSwitch] = useState(false); const [betaSwitch, setBetaSwitch] = useState(false);
const {t} = useTranslation(); const { t } = useTranslation();
useEffect(() => { useEffect(() => {
const isBeta = checkBeta(); const isBeta = checkBeta();
@@ -129,18 +135,18 @@ function Header({
InstanceRenderManager({ InstanceRenderManager({
imex: true, imex: true,
rome: true, rome: true,
promanager: HasFeatureAccess({featureName: "bills", bodyshop}) promanager: HasFeatureAccess({ featureName: "bills", bodyshop })
}) })
) { ) {
accountingChildren.push( accountingChildren.push(
{ {
key: "bills", key: "bills",
icon: <Icon component={FaFileInvoiceDollar}/>, icon: <Icon component={FaFileInvoiceDollar} />,
label: <Link to="/manage/bills">{t("menus.header.bills")}</Link> label: <Link to="/manage/bills">{t("menus.header.bills")}</Link>
}, },
{ {
key: "enterbills", key: "enterbills",
icon: <Icon component={GiPayMoney}/>, icon: <Icon component={GiPayMoney} />,
label: t("menus.header.enterbills"), label: t("menus.header.enterbills"),
onClick: () => { onClick: () => {
setBillEnterContext({ setBillEnterContext({
@@ -159,7 +165,7 @@ function Header({
}, },
{ {
key: "inventory", key: "inventory",
icon: <Icon component={FaFileInvoiceDollar}/>, icon: <Icon component={FaFileInvoiceDollar} />,
label: <Link to="/manage/inventory">{t("menus.header.inventory")}</Link> label: <Link to="/manage/inventory">{t("menus.header.inventory")}</Link>
} }
); );
@@ -168,7 +174,7 @@ function Header({
InstanceRenderManager({ InstanceRenderManager({
imex: true, imex: true,
rome: true, rome: true,
promanager: HasFeatureAccess({featureName: "payments", bodyshop}) promanager: HasFeatureAccess({ featureName: "payments", bodyshop })
}) })
) { ) {
accountingChildren.push( accountingChildren.push(
@@ -177,12 +183,12 @@ function Header({
}, },
{ {
key: "allpayments", key: "allpayments",
icon: <BankFilled/>, icon: <BankFilled />,
label: <Link to="/manage/payments">{t("menus.header.allpayments")}</Link> label: <Link to="/manage/payments">{t("menus.header.allpayments")}</Link>
}, },
{ {
key: "enterpayments", key: "enterpayments",
icon: <Icon component={FaCreditCard}/>, icon: <Icon component={FaCreditCard} />,
label: t("menus.header.enterpayment"), label: t("menus.header.enterpayment"),
onClick: () => { onClick: () => {
setPaymentContext({ setPaymentContext({
@@ -197,7 +203,7 @@ function Header({
if (ImEXPay.treatment === "on") { if (ImEXPay.treatment === "on") {
accountingChildren.push({ accountingChildren.push({
key: "entercardpayments", key: "entercardpayments",
icon: <Icon component={FaCreditCard}/>, icon: <Icon component={FaCreditCard} />,
label: t("menus.header.entercardpayment"), label: t("menus.header.entercardpayment"),
onClick: () => { onClick: () => {
setCardPaymentContext({ setCardPaymentContext({
@@ -212,7 +218,7 @@ function Header({
InstanceRenderManager({ InstanceRenderManager({
imex: true, imex: true,
rome: true, rome: true,
promanager: HasFeatureAccess({featureName: "timetickets", bodyshop}) promanager: HasFeatureAccess({ featureName: "timetickets", bodyshop })
}) })
) { ) {
accountingChildren.push( accountingChildren.push(
@@ -221,7 +227,7 @@ function Header({
}, },
{ {
key: "timetickets", key: "timetickets",
icon: <FieldTimeOutlined/>, icon: <FieldTimeOutlined />,
label: <Link to="/manage/timetickets">{t("menus.header.timetickets")}</Link> label: <Link to="/manage/timetickets">{t("menus.header.timetickets")}</Link>
} }
); );
@@ -229,14 +235,14 @@ function Header({
if (bodyshop?.md_tasks_presets?.use_approvals) { if (bodyshop?.md_tasks_presets?.use_approvals) {
accountingChildren.push({ accountingChildren.push({
key: "ttapprovals", key: "ttapprovals",
icon: <FieldTimeOutlined/>, icon: <FieldTimeOutlined />,
label: <Link to="/manage/ttapprovals">{t("menus.header.ttapprovals")}</Link> label: <Link to="/manage/ttapprovals">{t("menus.header.ttapprovals")}</Link>
}); });
} }
accountingChildren.push( accountingChildren.push(
{ {
key: "entertimetickets", key: "entertimetickets",
icon: <Icon component={GiPlayerTime}/>, icon: <Icon component={GiPlayerTime} />,
label: t("menus.header.entertimeticket"), label: t("menus.header.entertimeticket"),
onClick: () => { onClick: () => {
setTimeTicketContext({ setTimeTicketContext({
@@ -258,8 +264,7 @@ function Header({
const accountingExportChildren = [ const accountingExportChildren = [
{ {
key: "receivables", key: "receivables",
label: <Link label: <Link to="/manage/accounting/receivables">{t("menus.header.accounting-receivables")}</Link>
to="/manage/accounting/receivables">{t("menus.header.accounting-receivables")}</Link>
} }
]; ];
@@ -291,12 +296,12 @@ function Header({
InstanceRenderManager({ InstanceRenderManager({
imex: true, imex: true,
rome: true, rome: true,
promanager: HasFeatureAccess({featureName: "export", bodyshop}) promanager: HasFeatureAccess({ featureName: "export", bodyshop })
}) })
) { ) {
accountingChildren.push({ accountingChildren.push({
key: "accountingexport", key: "accountingexport",
icon: <ExportOutlined/>, icon: <ExportOutlined />,
label: t("menus.header.export"), label: t("menus.header.export"),
children: accountingExportChildren children: accountingExportChildren
}); });
@@ -305,44 +310,44 @@ function Header({
const menuItems = [ const menuItems = [
{ {
key: "home", key: "home",
icon: <HomeFilled/>, icon: <HomeFilled />,
label: <Link to="/manage/">{t("menus.header.home")}</Link> label: <Link to="/manage/">{t("menus.header.home")}</Link>
}, },
{ {
key: "schedule", key: "schedule",
icon: <Icon component={FaCalendarAlt}/>, icon: <Icon component={FaCalendarAlt} />,
label: <Link to="/manage/schedule">{t("menus.header.schedule")}</Link> label: <Link to="/manage/schedule">{t("menus.header.schedule")}</Link>
}, },
{ {
key: "jobssubmenu", key: "jobssubmenu",
id: "header-jobs", id: "header-jobs",
icon: <Icon component={FaCarCrash}/>, icon: <Icon component={FaCarCrash} />,
label: t("menus.header.jobs"), label: t("menus.header.jobs"),
children: [ children: [
{ {
key: "activejobs", key: "activejobs",
icon: <FileFilled/>, icon: <FileFilled />,
label: <Link to="/manage/jobs">{t("menus.header.activejobs")}</Link> label: <Link to="/manage/jobs">{t("menus.header.activejobs")}</Link>
}, },
{ {
key: "readyjobs", key: "readyjobs",
icon: <CheckCircleOutlined/>, icon: <CheckCircleOutlined />,
label: <Link to="/manage/jobs/ready">{t("menus.header.readyjobs")}</Link> label: <Link to="/manage/jobs/ready">{t("menus.header.readyjobs")}</Link>
}, },
{ {
key: "parts-queue", key: "parts-queue",
icon: <ToolFilled/>, icon: <ToolFilled />,
label: <Link to="/manage/partsqueue">{t("menus.header.parts-queue")}</Link> label: <Link to="/manage/partsqueue">{t("menus.header.parts-queue")}</Link>
}, },
{ {
key: "availablejobs", key: "availablejobs",
id: "header-jobs-available", id: "header-jobs-available",
icon: <ImportOutlined/>, icon: <ImportOutlined />,
label: <Link to="/manage/available">{t("menus.header.availablejobs")}</Link> label: <Link to="/manage/available">{t("menus.header.availablejobs")}</Link>
}, },
{ {
key: "newjob", key: "newjob",
icon: <FileAddOutlined/>, icon: <FileAddOutlined />,
label: <Link to="/manage/jobs/new">{t("menus.header.newjob")}</Link> label: <Link to="/manage/jobs/new">{t("menus.header.newjob")}</Link>
}, },
{ {
@@ -350,7 +355,7 @@ function Header({
}, },
{ {
key: "alljobs", key: "alljobs",
icon: <UnorderedListOutlined/>, icon: <UnorderedListOutlined />,
label: <Link to="/manage/jobs/all">{t("menus.header.alljobs")}</Link> label: <Link to="/manage/jobs/all">{t("menus.header.alljobs")}</Link>
}, },
{ {
@@ -358,54 +363,54 @@ function Header({
}, },
{ {
key: "productionlist", key: "productionlist",
icon: <ScheduleOutlined/>, icon: <ScheduleOutlined />,
label: <Link to="/manage/production/list">{t("menus.header.productionlist")}</Link> label: <Link to="/manage/production/list">{t("menus.header.productionlist")}</Link>
}, },
...(InstanceRenderManager({ ...(InstanceRenderManager({
imex: true, imex: true,
rome: true, rome: true,
promanager: HasFeatureAccess({featureName: "visualboard", bodyshop}) promanager: HasFeatureAccess({ featureName: "visualboard", bodyshop })
}) })
? [ ? [
{ {
key: "productionboard", key: "productionboard",
icon: <Icon component={BsKanban}/>, icon: <Icon component={BsKanban} />,
label: <Link to="/manage/production/board">{t("menus.header.productionboard")}</Link> label: <Link to="/manage/production/board">{t("menus.header.productionboard")}</Link>
} }
] ]
: []), : []),
...(InstanceRenderManager({ ...(InstanceRenderManager({
imex: true, imex: true,
rome: true, rome: true,
promanager: HasFeatureAccess({featureName: "scoreboard", bodyshop}) promanager: HasFeatureAccess({ featureName: "scoreboard", bodyshop })
}) })
? [ ? [
{ {
type: "divider" type: "divider"
}, },
{ {
key: "scoreboard", key: "scoreboard",
icon: <LineChartOutlined/>, icon: <LineChartOutlined />,
label: <Link to="/manage/scoreboard">{t("menus.header.scoreboard")}</Link> label: <Link to="/manage/scoreboard">{t("menus.header.scoreboard")}</Link>
} }
] ]
: []) : [])
] ]
}, },
{ {
key: "customers", key: "customers",
icon: <UserOutlined/>, icon: <UserOutlined />,
label: t("menus.header.customers"), label: t("menus.header.customers"),
children: [ children: [
{ {
key: "owners", key: "owners",
icon: <TeamOutlined/>, icon: <TeamOutlined />,
label: <Link to="/manage/owners">{t("menus.header.owners")}</Link> label: <Link to="/manage/owners">{t("menus.header.owners")}</Link>
}, },
{ {
key: "vehicles", key: "vehicles",
icon: <CarFilled/>, icon: <CarFilled />,
label: <Link to="/manage/vehicles">{t("menus.header.vehicles")}</Link> label: <Link to="/manage/vehicles">{t("menus.header.vehicles")}</Link>
} }
] ]
@@ -416,108 +421,106 @@ function Header({
promanager: false // HasFeatureAccess({ featureName: 'courtesycars', bodyshop }), promanager: false // HasFeatureAccess({ featureName: 'courtesycars', bodyshop }),
}) })
? [ ? [
{ {
key: "ccs", key: "ccs",
icon: <CarFilled/>, icon: <CarFilled />,
label: t("menus.header.courtesycars"), label: t("menus.header.courtesycars"),
children: [ children: [
{ {
key: "courtesycarsall", key: "courtesycarsall",
icon: <CarFilled/>, icon: <CarFilled />,
label: <Link to="/manage/courtesycars">{t("menus.header.courtesycars-all")}</Link> label: <Link to="/manage/courtesycars">{t("menus.header.courtesycars-all")}</Link>
}, },
{ {
key: "contracts", key: "contracts",
icon: <FileFilled/>, icon: <FileFilled />,
label: <Link label: <Link to="/manage/courtesycars/contracts">{t("menus.header.courtesycars-contracts")}</Link>
to="/manage/courtesycars/contracts">{t("menus.header.courtesycars-contracts")}</Link> },
}, {
{ key: "newcontract",
key: "newcontract", icon: <FileAddFilled />,
icon: <FileAddFilled/>, label: <Link to="/manage/courtesycars/contracts/new">{t("menus.header.courtesycars-newcontract")}</Link>
label: <Link }
to="/manage/courtesycars/contracts/new">{t("menus.header.courtesycars-newcontract")}</Link> ]
} }
] ]
}
]
: []), : []),
...(accountingChildren.length > 0 ...(accountingChildren.length > 0
? [ ? [
{ {
key: "accounting", key: "accounting",
icon: <DollarCircleFilled/>, icon: <DollarCircleFilled />,
label: t("menus.header.accounting"), label: t("menus.header.accounting"),
children: accountingChildren children: accountingChildren
} }
] ]
: []), : []),
{ {
key: "phonebook", key: "phonebook",
icon: <PhoneOutlined/>, icon: <PhoneOutlined />,
label: <Link to="/manage/phonebook">{t("menus.header.phonebook")}</Link> label: <Link to="/manage/phonebook">{t("menus.header.phonebook")}</Link>
}, },
...(InstanceRenderManager({ ...(InstanceRenderManager({
imex: true, imex: true,
rome: true, rome: true,
promanager: HasFeatureAccess({featureName: "media", bodyshop}) promanager: HasFeatureAccess({ featureName: "media", bodyshop })
}) })
? [ ? [
{ {
key: "temporarydocs", key: "temporarydocs",
icon: <PaperClipOutlined/>, icon: <PaperClipOutlined />,
label: <Link to="/manage/temporarydocs">{t("menus.header.temporarydocs")}</Link> label: <Link to="/manage/temporarydocs">{t("menus.header.temporarydocs")}</Link>
} }
] ]
: []), : []),
{ {
key: 'tasks', key: "tasks",
id: 'tasks', id: "tasks",
icon: <FaTasks/>, icon: <FaTasks />,
label: t('menus.header.tasks'), label: t("menus.header.tasks"),
children: [ children: [
{ {
key: 'createTask', key: "createTask",
icon: <PlusCircleOutlined/>, icon: <PlusCircleOutlined />,
label: t('menus.header.create_task'), label: t("menus.header.create_task"),
onClick: () => { onClick: () => {
setTaskUpsertContext({ setTaskUpsertContext({
actions: {}, actions: {},
context: {}, context: {}
}); });
}, }
}, },
{ {
key: 'mytasks', key: "mytasks",
icon: <FaTasks/>, icon: <FaTasks />,
label: <Link to="/manage/tasks/mytasks">{t('menus.header.my_tasks')}</Link>, label: <Link to="/manage/tasks/mytasks">{t("menus.header.my_tasks")}</Link>
}, },
{ {
key: 'all_tasks', key: "all_tasks",
icon: <FaTasks/>, icon: <FaTasks />,
label: <Link to="/manage/tasks/alltasks">{t('menus.header.all_tasks')}</Link>, label: <Link to="/manage/tasks/alltasks">{t("menus.header.all_tasks")}</Link>
} }
] ]
}, },
{ {
key: 'shopsubmenu', key: "shopsubmenu",
icon: <SettingOutlined/>, icon: <SettingOutlined />,
label: t("menus.header.shop"), label: t("menus.header.shop"),
children: [ children: [
{ {
key: "shop", key: "shop",
icon: <Icon component={GiSettingsKnobs}/>, icon: <Icon component={GiSettingsKnobs} />,
label: <Link to="/manage/shop?tab=info">{t("menus.header.shop_config")}</Link> label: <Link to="/manage/shop?tab=info">{t("menus.header.shop_config")}</Link>
}, },
{ {
key: "dashboard", key: "dashboard",
icon: <DashboardFilled/>, icon: <DashboardFilled />,
label: <Link to="/manage/dashboard">{t("menus.header.dashboard")}</Link> label: <Link to="/manage/dashboard">{t("menus.header.dashboard")}</Link>
}, },
{ {
key: "reportcenter", key: "reportcenter",
icon: <BarChartOutlined/>, icon: <BarChartOutlined />,
label: t("menus.header.reportcenter"), label: t("menus.header.reportcenter"),
onClick: () => { onClick: () => {
setReportCenterContext({ setReportCenterContext({
@@ -528,21 +531,21 @@ function Header({
}, },
{ {
key: "shop-vendors", key: "shop-vendors",
icon: <Icon component={IoBusinessOutline}/>, icon: <Icon component={IoBusinessOutline} />,
label: <Link to="/manage/shop/vendors">{t("menus.header.shop_vendors")}</Link> label: <Link to="/manage/shop/vendors">{t("menus.header.shop_vendors")}</Link>
}, },
...(InstanceRenderManager({ ...(InstanceRenderManager({
imex: true, imex: true,
rome: true, rome: true,
promanager: HasFeatureAccess({featureName: "csi", bodyshop}) promanager: HasFeatureAccess({ featureName: "csi", bodyshop })
}) })
? [ ? [
{ {
key: "shop-csi", key: "shop-csi",
icon: <Icon component={RiSurveyLine}/>, icon: <Icon component={RiSurveyLine} />,
label: <Link to="/manage/shop/csi">{t("menus.header.shop_csi")}</Link> label: <Link to="/manage/shop/csi">{t("menus.header.shop_csi")}</Link>
} }
] ]
: []) : [])
] ]
}, },
@@ -552,14 +555,14 @@ function Header({
children: [ children: [
{ {
key: "signout", key: "signout",
icon: <Icon component={FiLogOut}/>, icon: <Icon component={FiLogOut} />,
danger: true, danger: true,
label: t("user.actions.signout"), label: t("user.actions.signout"),
onClick: () => signOutStart() onClick: () => signOutStart()
}, },
{ {
key: "help", key: "help",
icon: <Icon component={QuestionCircleFilled}/>, icon: <Icon component={QuestionCircleFilled} />,
label: t("menus.header.help"), label: t("menus.header.help"),
onClick: () => { onClick: () => {
window.open( window.open(
@@ -585,19 +588,19 @@ function Header({
...(InstanceRenderManager({ ...(InstanceRenderManager({
imex: true, imex: true,
rome: true, rome: true,
promanager: HasFeatureAccess({featureName: "timetickets", bodyshop}) promanager: HasFeatureAccess({ featureName: "timetickets", bodyshop })
}) })
? [ ? [
{ {
key: "shiftclock", key: "shiftclock",
icon: <Icon component={GiPlayerTime}/>, icon: <Icon component={GiPlayerTime} />,
label: <Link to="/manage/shiftclock">{t("menus.header.shiftclock")}</Link> label: <Link to="/manage/shiftclock">{t("menus.header.shiftclock")}</Link>
} }
] ]
: []), : []),
{ {
key: "profile", key: "profile",
icon: <UserOutlined/>, icon: <UserOutlined />,
label: <Link to="/manage/profile">{t("menus.currentuser.profile")}</Link> label: <Link to="/manage/profile">{t("menus.currentuser.profile")}</Link>
} }
// { // {
@@ -631,7 +634,7 @@ function Header({
}, },
{ {
key: "recent", key: "recent",
icon: <ClockCircleFilled/>, icon: <ClockCircleFilled />,
children: recentItems.map((i, idx) => ({ children: recentItems.map((i, idx) => ({
key: idx, key: idx,
label: <Link to={i.url}>{i.label}</Link> label: <Link to={i.url}>{i.label}</Link>
@@ -645,7 +648,7 @@ function Header({
imex: () => { imex: () => {
menuItems.push({ menuItems.push({
key: "beta-switch", key: "beta-switch",
style: {marginLeft: "auto"}, style: { marginLeft: "auto" },
label: ( label: (
<Tooltip <Tooltip
title={`A more modern ${InstanceRenderManager({ title={`A more modern ${InstanceRenderManager({
@@ -654,9 +657,9 @@ function Header({
promanager: t("titles.promanager") promanager: t("titles.promanager")
})} is ready for you to try! You can switch back at any time.`} })} is ready for you to try! You can switch back at any time.`}
> >
<InfoCircleOutlined/> <InfoCircleOutlined />
<span style={{marginRight: 8}}>Try the new app</span> <span style={{ marginRight: 8 }}>Try the new app</span>
<Switch checked={betaSwitch} onChange={betaSwitchChange}/> <Switch checked={betaSwitch} onChange={betaSwitchChange} />
</Tooltip> </Tooltip>
) )
}); });

View File

@@ -1,17 +1,17 @@
import {useQuery} from "@apollo/client"; import { useQuery } from "@apollo/client";
import {Col, Divider, Row, Skeleton, Space, Timeline, Typography} from "antd"; import { Col, Divider, Row, Skeleton, Space, Timeline, Typography } from "antd";
import React from "react"; import React from "react";
import {useTranslation} from "react-i18next"; import { useTranslation } from "react-i18next";
import {Link} from "react-router-dom"; import { Link } from "react-router-dom";
import {GET_JOB_LINE_ORDERS} from "../../graphql/jobs.queries"; import { GET_JOB_LINE_ORDERS } from "../../graphql/jobs.queries";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import {DateFormatter} from "../../utils/DateFormatter"; import { DateFormatter } from "../../utils/DateFormatter";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import {connect} from "react-redux"; import { connect } from "react-redux";
import {createStructuredSelector} from "reselect"; import { createStructuredSelector } from "reselect";
import {selectBodyshop, selectCurrentUser} from "../../redux/user/user.selectors"; import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import {QUERY_JOBLINE_TASKS_PAGINATED} from "../../graphql/tasks.queries.js"; import { QUERY_JOBLINE_TASKS_PAGINATED } from "../../graphql/tasks.queries.js";
import TaskListContainer from "../task-list/task-list.container.jsx"; import TaskListContainer from "../task-list/task-list.container.jsx";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
@@ -23,9 +23,9 @@ const mapDispatchToProps = (dispatch) => ({
}); });
export default connect(mapStateToProps, mapDispatchToProps)(JobLinesExpander); export default connect(mapStateToProps, mapDispatchToProps)(JobLinesExpander);
export function JobLinesExpander({jobline, jobid, bodyshop, currentUser}) { export function JobLinesExpander({ jobline, jobid, bodyshop, currentUser }) {
const {t} = useTranslation(); const { t } = useTranslation();
const {loading, error, data} = useQuery(GET_JOB_LINE_ORDERS, { const { loading, error, data } = useQuery(GET_JOB_LINE_ORDERS, {
fetchPolicy: "network-only", fetchPolicy: "network-only",
nextFetchPolicy: "network-only", nextFetchPolicy: "network-only",
variables: { variables: {
@@ -33,8 +33,8 @@ export function JobLinesExpander({jobline, jobid, bodyshop, currentUser}) {
} }
}); });
if (loading) return <Skeleton/>; if (loading) return <Skeleton />;
if (error) return <AlertComponent message={error.message} type="error"/>; if (error) return <AlertComponent message={error.message} type="error" />;
return ( return (
<Row> <Row>
@@ -44,23 +44,23 @@ export function JobLinesExpander({jobline, jobid, bodyshop, currentUser}) {
items={ items={
data.parts_order_lines.length > 0 data.parts_order_lines.length > 0
? data.parts_order_lines.map((line) => ({ ? data.parts_order_lines.map((line) => ({
key: line.id, key: line.id,
children: ( children: (
<Space split={<Divider type="vertical"/>} wrap> <Space split={<Divider type="vertical" />} wrap>
<Link to={`/manage/jobs/${jobid}?partsorderid=${line.parts_order.id}`}> <Link to={`/manage/jobs/${jobid}?partsorderid=${line.parts_order.id}`}>
{line.parts_order.order_number} {line.parts_order.order_number}
</Link> </Link>
<DateFormatter>{line.parts_order.order_date}</DateFormatter> <DateFormatter>{line.parts_order.order_date}</DateFormatter>
{line.parts_order.vendor.name} {line.parts_order.vendor.name}
</Space> </Space>
) )
})) }))
: [ : [
{ {
key: "no-orders", key: "no-orders",
children: t("parts_orders.labels.notyetordered") children: t("parts_orders.labels.notyetordered")
} }
] ]
} }
/>{" "} />{" "}
</Col> </Col>
@@ -70,33 +70,33 @@ export function JobLinesExpander({jobline, jobid, bodyshop, currentUser}) {
items={ items={
data.billlines.length > 0 data.billlines.length > 0
? data.billlines.map((line) => ({ ? data.billlines.map((line) => ({
key: line.id, key: line.id,
children: ( children: (
<Row wrap> <Row wrap>
<Col span={4}> <Col span={4}>
<Link to={`/manage/jobs/${jobid}?tab=partssublet&billid=${line.bill.id}`}> <Link to={`/manage/jobs/${jobid}?tab=partssublet&billid=${line.bill.id}`}>
{line.bill.invoice_number} {line.bill.invoice_number}
</Link> </Link>
</Col> </Col>
<Col span={4}> <Col span={4}>
<span> <span>
{`${t("billlines.fields.actual_price")}: `} {`${t("billlines.fields.actual_price")}: `}
<CurrencyFormatter>{line.actual_price}</CurrencyFormatter> <CurrencyFormatter>{line.actual_price}</CurrencyFormatter>
</span> </span>
</Col> </Col>
<Col span={4}> <Col span={4}>
<span> <span>
{`${t("billlines.fields.actual_cost")}: `} {`${t("billlines.fields.actual_cost")}: `}
<CurrencyFormatter>{line.actual_cost}</CurrencyFormatter> <CurrencyFormatter>{line.actual_cost}</CurrencyFormatter>
</span> </span>
</Col> </Col>
<Col span={4}> <Col span={4}>
<DateFormatter>{line.bill.date}</DateFormatter> <DateFormatter>{line.bill.date}</DateFormatter>
</Col> </Col>
<Col span={4}> {line.bill.vendor.name}</Col> <Col span={4}> {line.bill.vendor.name}</Col>
</Row> </Row>
) )
})) }))
: [ : [
{ {
key: "no-orders", key: "no-orders",
@@ -112,31 +112,35 @@ export function JobLinesExpander({jobline, jobid, bodyshop, currentUser}) {
items={ items={
data.parts_dispatch_lines.length > 0 data.parts_dispatch_lines.length > 0
? data.parts_dispatch_lines.map((line) => ({ ? data.parts_dispatch_lines.map((line) => ({
key: line.id, key: line.id,
children: ( children: (
<Space split={<Divider type="vertical"/>} wrap> <Space split={<Divider type="vertical" />} wrap>
<Link <Link to={`/manage/jobs/${jobid}?partsorderid=${line.id}`}>{line.parts_dispatch.number}</Link>
to={`/manage/jobs/${jobid}?partsorderid=${line.id}`}>{line.parts_dispatch.number}</Link> {bodyshop.employees.find((e) => e.id === line.parts_dispatch.employeeid)?.first_name}
{bodyshop.employees.find((e) => e.id === line.parts_dispatch.employeeid)?.first_name} <Space>
<Space> {t("parts_dispatch_lines.fields.accepted_at")}
{t("parts_dispatch_lines.fields.accepted_at")} <DateFormatter>{line.accepted_at}</DateFormatter>
<DateFormatter>{line.accepted_at}</DateFormatter> </Space>
</Space> </Space>
</Space> )
) }))
}))
: { : {
key: "dispatch-lines", key: "dispatch-lines",
children: t("parts_orders.labels.notyetordered") children: t("parts_orders.labels.notyetordered")
} }
} }
/> />
</Col> </Col>
<Col md={24} lg={24}> <Col md={24} lg={24}>
<TaskListContainer currentUser={currentUser} bodyshop={bodyshop} parentJobId={jobid} <TaskListContainer
relationshipType={'joblineid'} relationshipId={jobline.id} currentUser={currentUser}
query={QUERY_JOBLINE_TASKS_PAGINATED} bodyshop={bodyshop}
titleTranslation='tasks.titles.job_tasks'/> parentJobId={jobid}
relationshipType={"joblineid"}
relationshipId={jobline.id}
query={QUERY_JOBLINE_TASKS_PAGINATED}
titleTranslation="tasks.titles.job_tasks"
/>
</Col> </Col>
</Row> </Row>
); );

View File

@@ -42,7 +42,7 @@ import JobSendPartPriceChangeComponent from "../job-send-parts-price-change/job-
import PartsOrderModalContainer from "../parts-order-modal/parts-order-modal.container"; import PartsOrderModalContainer from "../parts-order-modal/parts-order-modal.container";
import JobLinesExpander from "./job-lines-expander.component"; import JobLinesExpander from "./job-lines-expander.component";
import JobLinesPartPriceChange from "./job-lines-part-price-change.component"; import JobLinesPartPriceChange from "./job-lines-part-price-change.component";
import {FaTasks} from "react-icons/fa"; import { FaTasks } from "react-icons/fa";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -54,7 +54,7 @@ const mapDispatchToProps = (dispatch) => ({
setJobLineEditContext: (context) => dispatch(setModalContext({ context: context, modal: "jobLineEdit" })), setJobLineEditContext: (context) => dispatch(setModalContext({ context: context, modal: "jobLineEdit" })),
setPartsOrderContext: (context) => dispatch(setModalContext({ context: context, modal: "partsOrder" })), setPartsOrderContext: (context) => dispatch(setModalContext({ context: context, modal: "partsOrder" })),
setBillEnterContext: (context) => dispatch(setModalContext({ context: context, modal: "billEnter" })), setBillEnterContext: (context) => dispatch(setModalContext({ context: context, modal: "billEnter" })),
setTaskUpsertContext: (context) => dispatch(setModalContext({context, modal: 'taskUpsert'})), setTaskUpsertContext: (context) => dispatch(setModalContext({ context, modal: "taskUpsert" }))
}); });
export function JobLinesComponent({ export function JobLinesComponent({
@@ -334,15 +334,18 @@ export function JobLinesComponent({
> >
<EditFilled /> <EditFilled />
</Button> </Button>
<Button title={t('tasks.buttons.create')} onClick={() => { <Button
setTaskUpsertContext({ title={t("tasks.buttons.create")}
context: { onClick={() => {
jobid: job.id, setTaskUpsertContext({
joblineid: record.id context: {
} jobid: job.id,
}); joblineid: record.id
}}> }
<FaTasks/> });
}}
>
<FaTasks />
</Button> </Button>
<Button <Button
disabled={jobRO} disabled={jobRO}
@@ -457,7 +460,7 @@ export function JobLinesComponent({
disabled={(job && !job.converted) || (selectedLines.length > 0 ? false : true) || jobRO || technician} disabled={(job && !job.converted) || (selectedLines.length > 0 ? false : true) || jobRO || technician}
onClick={() => { onClick={() => {
setBillEnterContext({ setBillEnterContext({
actions: {refetch: refetch }, actions: { refetch: refetch },
context: { context: {
disableInvNumber: true, disableInvNumber: true,
job: { id: job.id }, job: { id: job.id },
@@ -465,7 +468,7 @@ export function JobLinesComponent({
vendorid: bodyshop.inhousevendorid, vendorid: bodyshop.inhousevendorid,
invoice_number: "ih", invoice_number: "ih",
isinhouse: true, isinhouse: true,
date: dayjs(), date: dayjs(),
total: 0, total: 0,
billlines: selectedLines.map((p) => { billlines: selectedLines.map((p) => {
return { return {

View File

@@ -2,40 +2,25 @@ import { LoadingOutlined } from "@ant-design/icons";
import { useLazyQuery } from "@apollo/client"; import { useLazyQuery } from "@apollo/client";
import { Select, Space, Spin, Tag } from "antd"; import { Select, Space, Spin, Tag } from "antd";
import _ from "lodash"; import _ from "lodash";
import React, {forwardRef, useEffect, useState} from "react"; import React, { forwardRef, useEffect, useState } from "react";
import {useTranslation} from "react-i18next"; import { useTranslation } from "react-i18next";
import { import { SEARCH_JOBS_BY_ID_FOR_AUTOCOMPLETE, SEARCH_JOBS_FOR_AUTOCOMPLETE } from "../../graphql/jobs.queries";
SEARCH_JOBS_BY_ID_FOR_AUTOCOMPLETE,
SEARCH_JOBS_FOR_AUTOCOMPLETE
} from "../../graphql/jobs.queries";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component"; import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
const {Option} = Select; const { Option } = Select;
const JobSearchSelect = ( const JobSearchSelect = (
{ { disabled, convertedOnly = false, notInvoiced = false, notExported = true, clm_no = false, ...restProps },
disabled,
convertedOnly = false,
notInvoiced = false,
notExported = true,
clm_no = false,
...restProps
},
ref ref
) => { ) => {
const {t} = useTranslation(); const { t } = useTranslation();
const [theOptions, setTheOptions] = useState([]); const [theOptions, setTheOptions] = useState([]);
const [callSearch, {loading, error, data}] = useLazyQuery(SEARCH_JOBS_FOR_AUTOCOMPLETE, {}); const [callSearch, { loading, error, data }] = useLazyQuery(SEARCH_JOBS_FOR_AUTOCOMPLETE, {});
const [ const [callIdSearch, { loading: idLoading, error: idError, data: idData }] = useLazyQuery(
callIdSearch, SEARCH_JOBS_BY_ID_FOR_AUTOCOMPLETE
{ );
loading: idLoading,
error: idError,
data: idData
}
] = useLazyQuery(SEARCH_JOBS_BY_ID_FOR_AUTOCOMPLETE);
const executeSearch = (v) => { const executeSearch = (v) => {
if (v && v.variables?.search !== "" && v.variables.search.length >= 2) callSearch(v); if (v && v.variables?.search !== "" && v.variables.search.length >= 2) callSearch(v);
@@ -48,10 +33,10 @@ const JobSearchSelect = (
search: value, search: value,
...(convertedOnly || notExported ...(convertedOnly || notExported
? { ? {
...(convertedOnly ? {isConverted: true} : {}), ...(convertedOnly ? { isConverted: true } : {}),
...(notExported ? {notExported: true} : {}), ...(notExported ? { notExported: true } : {}),
...(notInvoiced ? {notInvoiced: true} : {}) ...(notInvoiced ? { notInvoiced: true } : {})
} }
: {}) : {})
} }
}); });
@@ -59,7 +44,7 @@ const JobSearchSelect = (
useEffect(() => { useEffect(() => {
if (restProps.value) { if (restProps.value) {
callIdSearch({variables: {id: restProps.value}}); // Sometimes results in a no-op. Not sure how to fix. callIdSearch({ variables: { id: restProps.value } }); // Sometimes results in a no-op. Not sure how to fix.
} }
}, [restProps.value, callIdSearch]); }, [restProps.value, callIdSearch]);
@@ -89,14 +74,14 @@ const JobSearchSelect = (
filterOption={false} filterOption={false}
onSearch={handleSearch} onSearch={handleSearch}
//loading={loading || idLoading} //loading={loading || idLoading}
suffixIcon={(loading || idLoading) && <Spin/>} suffixIcon={(loading || idLoading) && <Spin />}
notFoundContent={loading ? <LoadingOutlined/> : null} notFoundContent={loading ? <LoadingOutlined /> : null}
{...restProps} {...restProps}
> >
{theOptions {theOptions
? theOptions.map((o) => ( ? theOptions.map((o) => (
<Option key={o.id} value={o.id} status={o.status}> <Option key={o.id} value={o.id} status={o.status}>
<Space align="center"> <Space align="center">
<span> <span>
{`${clm_no && o.clm_no ? `${o.clm_no} | ` : ""}${ {`${clm_no && o.clm_no ? `${o.clm_no} | ` : ""}${
o.ro_number || t("general.labels.na") o.ro_number || t("general.labels.na")
@@ -104,17 +89,17 @@ const JobSearchSelect = (
o.v_model_desc || "" o.v_model_desc || ""
}`} }`}
</span> </span>
<Tag> <Tag>
<strong>{o.status}</strong> <strong>{o.status}</strong>
</Tag> </Tag>
</Space> </Space>
</Option> </Option>
)) ))
: null} : null}
</Select> </Select>
{error ? <AlertComponent message={error.message} type="error"/> : null} {error ? <AlertComponent message={error.message} type="error" /> : null}
{idError ? <AlertComponent message={idError.message} type="error"/> : null} {idError ? <AlertComponent message={idError.message} type="error" /> : null}
</div> </div>
); );
}; };

View File

@@ -21,7 +21,6 @@ export function JobTotalsTableTotals({ bodyshop, job }) {
const { t } = useTranslation(); const { t } = useTranslation();
const data = useMemo(() => { const data = useMemo(() => {
return [ return [
{ {
key: t("jobs.labels.subtotal"), key: t("jobs.labels.subtotal"),

View File

@@ -1,51 +1,35 @@
import {DownCircleFilled} from "@ant-design/icons"; import { DownCircleFilled } from "@ant-design/icons";
import {useApolloClient, useMutation} from "@apollo/client"; import { useApolloClient, useMutation } from "@apollo/client";
import { import { Button, Card, Dropdown, Form, Input, Modal, notification, Popconfirm, Popover, Select, Space } from "antd";
Button, import React, { useMemo, useState } from "react";
Card, import { useTranslation } from "react-i18next";
Dropdown, import { connect } from "react-redux";
Form, import { Link, useNavigate } from "react-router-dom";
Input, import { createStructuredSelector } from "reselect";
Modal, import { auth, logImEXEvent } from "../../firebase/firebase.utils";
notification, import { CANCEL_APPOINTMENTS_BY_JOB_ID, INSERT_MANUAL_APPT } from "../../graphql/appointments.queries";
Popconfirm, import { DELETE_JOB, UPDATE_JOB, VOID_JOB } from "../../graphql/jobs.queries";
Popover, import { insertAuditTrail } from "../../redux/application/application.actions";
Select, import { selectJobReadOnly } from "../../redux/application/application.selectors";
Space import { setModalContext } from "../../redux/modals/modals.actions";
} from "antd"; import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import React, {useMemo, useState} from "react";
import {useTranslation} from "react-i18next";
import {connect} from "react-redux";
import {Link, useNavigate} from "react-router-dom";
import {createStructuredSelector} from "reselect";
import {auth, logImEXEvent} from "../../firebase/firebase.utils";
import {
CANCEL_APPOINTMENTS_BY_JOB_ID,
INSERT_MANUAL_APPT
} from "../../graphql/appointments.queries";
import {DELETE_JOB, UPDATE_JOB, VOID_JOB} from "../../graphql/jobs.queries";
import {insertAuditTrail} from "../../redux/application/application.actions";
import {selectJobReadOnly} from "../../redux/application/application.selectors";
import {setModalContext} from "../../redux/modals/modals.actions";
import {selectBodyshop, selectCurrentUser} from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings"; import AuditTrailMapping from "../../utils/AuditTrailMappings";
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component"; import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
import AddToProduction from "./jobs-detail-header-actions.addtoproduction.util"; import AddToProduction from "./jobs-detail-header-actions.addtoproduction.util";
import DuplicateJob from "./jobs-detail-header-actions.duplicate.util"; import DuplicateJob from "./jobs-detail-header-actions.duplicate.util";
import axios from "axios"; import axios from "axios";
import {setEmailOptions} from "../../redux/email/email.actions"; import { setEmailOptions } from "../../redux/email/email.actions";
import {openChatByPhone, setMessage} from "../../redux/messaging/messaging.actions"; import { openChatByPhone, setMessage } from "../../redux/messaging/messaging.actions";
import {GET_CURRENT_QUESTIONSET_ID, INSERT_CSI} from "../../graphql/csi.queries"; import { GET_CURRENT_QUESTIONSET_ID, INSERT_CSI } from "../../graphql/csi.queries";
import {TemplateList} from "../../utils/TemplateConstants"; import { TemplateList } from "../../utils/TemplateConstants";
import parsePhoneNumber from "libphonenumber-js"; import parsePhoneNumber from "libphonenumber-js";
import {HasFeatureAccess} from "../feature-wrapper/feature-wrapper.component"; import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
import {DateTimeFormatter} from "../../utils/DateFormatter"; import { DateTimeFormatter } from "../../utils/DateFormatter";
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component"; import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
import dayjs from "../../utils/day"; import dayjs from "../../utils/day";
import {useSplitTreatments} from "@splitsoftware/splitio-react"; import { useSplitTreatments } from "@splitsoftware/splitio-react";
import InstanceRenderManager from "../../utils/instanceRenderMgr"; import InstanceRenderManager from "../../utils/instanceRenderMgr";
import JobsDetailHeaderActionsToggleProduction import JobsDetailHeaderActionsToggleProduction from "./jobs-detail-header-actions.toggle-production";
from "./jobs-detail-header-actions.toggle-production";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -54,57 +38,75 @@ const mapStateToProps = createStructuredSelector({
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
setScheduleContext: (context) => dispatch(setModalContext({context: context, modal: "schedule"})), setScheduleContext: (context) => dispatch(setModalContext({ context: context, modal: "schedule" })),
setBillEnterContext: (context) => dispatch(setModalContext({ setBillEnterContext: (context) =>
context: context, dispatch(
modal: "billEnter" setModalContext({
})), context: context,
setPaymentContext: (context) => dispatch(setModalContext({context: context, modal: "payment"})), modal: "billEnter"
setJobCostingContext: (context) => dispatch(setModalContext({ })
context: context, ),
modal: "jobCosting" setPaymentContext: (context) => dispatch(setModalContext({ context: context, modal: "payment" })),
})), setJobCostingContext: (context) =>
setTimeTicketContext: (context) => dispatch(setModalContext({ dispatch(
context: context, setModalContext({
modal: "timeTicket" context: context,
})), modal: "jobCosting"
setCardPaymentContext: (context) => dispatch(setModalContext({ })
context: context, ),
modal: "cardPayment" setTimeTicketContext: (context) =>
})), dispatch(
insertAuditTrail: ({jobid, operation, type}) => dispatch(insertAuditTrail({ setModalContext({
jobid, context: context,
operation, modal: "timeTicket"
type })
})), ),
setTimeTicketTaskContext: (context) => dispatch(setModalContext({ setCardPaymentContext: (context) =>
context: context, dispatch(
modal: "timeTicketTask" setModalContext({
})), context: context,
modal: "cardPayment"
})
),
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(
insertAuditTrail({
jobid,
operation,
type
})
),
setTimeTicketTaskContext: (context) =>
dispatch(
setModalContext({
context: context,
modal: "timeTicketTask"
})
),
setEmailOptions: (e) => dispatch(setEmailOptions(e)), setEmailOptions: (e) => dispatch(setEmailOptions(e)),
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)), openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
setMessage: (text) => dispatch(setMessage(text)) setMessage: (text) => dispatch(setMessage(text))
}); });
export function JobsDetailHeaderActions({ export function JobsDetailHeaderActions({
job, job,
bodyshop, bodyshop,
currentUser, currentUser,
refetch, refetch,
setScheduleContext, setScheduleContext,
setBillEnterContext, setBillEnterContext,
setPaymentContext, setPaymentContext,
setJobCostingContext, setJobCostingContext,
jobRO, jobRO,
setTimeTicketContext, setTimeTicketContext,
setCardPaymentContext, setCardPaymentContext,
insertAuditTrail, insertAuditTrail,
setEmailOptions, setEmailOptions,
openChatByPhone, openChatByPhone,
setMessage, setMessage,
setTimeTicketTaskContext setTimeTicketTaskContext
}) { }) {
const {t} = useTranslation(); const { t } = useTranslation();
const client = useApolloClient(); const client = useApolloClient();
const history = useNavigate(); const history = useNavigate();
const [form] = Form.useForm(); const [form] = Form.useForm();
@@ -118,7 +120,7 @@ export function JobsDetailHeaderActions({
const [cancelAllAppointments] = useMutation(CANCEL_APPOINTMENTS_BY_JOB_ID); const [cancelAllAppointments] = useMutation(CANCEL_APPOINTMENTS_BY_JOB_ID);
const { const {
treatments: {ImEXPay} treatments: { ImEXPay }
} = useSplitTreatments({ } = useSplitTreatments({
attributes: {}, attributes: {},
names: ["ImEXPay"], names: ["ImEXPay"],
@@ -152,7 +154,7 @@ export function JobsDetailHeaderActions({
DuplicateJob( DuplicateJob(
client, client,
job.id, job.id,
{defaultOpenStatus: bodyshop.md_ro_statuses.default_imported}, { defaultOpenStatus: bodyshop.md_ro_statuses.default_imported },
(newJobId) => { (newJobId) => {
history(`/manage/jobs/${newJobId}`); history(`/manage/jobs/${newJobId}`);
notification["success"]({ notification["success"]({
@@ -163,7 +165,7 @@ export function JobsDetailHeaderActions({
); );
const handleDuplicateConfirm = () => const handleDuplicateConfirm = () =>
DuplicateJob(client, job.id, {defaultOpenStatus: bodyshop.md_ro_statuses.default_imported}, (newJobId) => { DuplicateJob(client, job.id, { defaultOpenStatus: bodyshop.md_ro_statuses.default_imported }, (newJobId) => {
history(`/manage/jobs/${newJobId}`); history(`/manage/jobs/${newJobId}`);
notification["success"]({ notification["success"]({
message: t("jobs.successes.duplicated") message: t("jobs.successes.duplicated")
@@ -177,7 +179,7 @@ export function JobsDetailHeaderActions({
try { try {
insertAppointment({ insertAppointment({
variables: { variables: {
apt: {...values, isintake: false, jobid: job.id, bodyshopid: bodyshop.id} apt: { ...values, isintake: false, jobid: job.id, bodyshopid: bodyshop.id }
}, },
refetchQueries: ["QUERY_ALL_ACTIVE_APPOINTMENTS"] refetchQueries: ["QUERY_ALL_ACTIVE_APPOINTMENTS"]
}); });
@@ -194,7 +196,7 @@ export function JobsDetailHeaderActions({
const handleDeleteJob = async () => { const handleDeleteJob = async () => {
//delete the job. //delete the job.
const result = await deleteJob({variables: {id: job.id}}); const result = await deleteJob({ variables: { id: job.id } });
if (!!!result.errors) { if (!!!result.errors) {
notification["success"]({ notification["success"]({
@@ -255,7 +257,7 @@ export function JobsDetailHeaderActions({
}); });
if (!!!result.errors) { if (!!!result.errors) {
notification["success"]({message: t("csi.successes.created")}); notification["success"]({ message: t("csi.successes.created") });
} else { } else {
notification["error"]({ notification["error"]({
message: t("csi.errors.creating", { message: t("csi.errors.creating", {
@@ -410,7 +412,7 @@ export function JobsDetailHeaderActions({
try { try {
QbXmlResponse = await axios.post( QbXmlResponse = await axios.post(
"/accounting/qbxml/receivables", "/accounting/qbxml/receivables",
{jobIds: [job.id], custDataOnly: true}, { jobIds: [job.id], custDataOnly: true },
{ {
headers: { headers: {
Authorization: `Bearer ${await auth.currentUser.getIdToken()}` Authorization: `Bearer ${await auth.currentUser.getIdToken()}`
@@ -519,7 +521,7 @@ export function JobsDetailHeaderActions({
setIsCancelScheduleModalVisible(false); setIsCancelScheduleModalVisible(false);
}; };
const handleLostSaleFinish = async ({lost_sale_reason}) => { const handleLostSaleFinish = async ({ lost_sale_reason }) => {
const jobUpdate = await cancelAllAppointments({ const jobUpdate = await cancelAllAppointments({
variables: { variables: {
jobid: job.id, jobid: job.id,
@@ -559,10 +561,10 @@ export function JobsDetailHeaderActions({
} }
]} ]}
> >
<Input/> <Input />
</Form.Item> </Form.Item>
<Form.Item label={t("appointments.fields.note")} name="note"> <Form.Item label={t("appointments.fields.note")} name="note">
<Input/> <Input />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t("appointments.fields.start")} label={t("appointments.fields.start")}
@@ -577,7 +579,7 @@ export function JobsDetailHeaderActions({
<FormDateTimePickerComponent <FormDateTimePickerComponent
onBlur={() => { onBlur={() => {
const start = form.getFieldValue("start"); const start = form.getFieldValue("start");
form.setFieldsValue({end: start.add(30, "minutes")}); form.setFieldsValue({ end: start.add(30, "minutes") });
}} }}
/> />
</Form.Item> </Form.Item>
@@ -589,10 +591,10 @@ export function JobsDetailHeaderActions({
required: true required: true
//message: t("general.validation.required"), //message: t("general.validation.required"),
}, },
({getFieldValue}) => ({ ({ getFieldValue }) => ({
async validator(rule, value) { async validator(rule, value) {
if (value) { if (value) {
const {start} = form.getFieldsValue(); const { start } = form.getFieldsValue();
if (dayjs(start).isAfter(dayjs(value))) { if (dayjs(start).isAfter(dayjs(value))) {
return Promise.reject(t("employees.labels.endmustbeafterstart")); return Promise.reject(t("employees.labels.endmustbeafterstart"));
} else { } else {
@@ -605,7 +607,7 @@ export function JobsDetailHeaderActions({
}) })
]} ]}
> >
<FormDateTimePickerComponent/> <FormDateTimePickerComponent />
</Form.Item> </Form.Item>
<Form.Item label={t("appointments.fields.color")} name="color"> <Form.Item label={t("appointments.fields.color")} name="color">
<Select> <Select>
@@ -636,7 +638,7 @@ export function JobsDetailHeaderActions({
onClick: () => { onClick: () => {
logImEXEvent("job_header_schedule"); logImEXEvent("job_header_schedule");
setScheduleContext({ setScheduleContext({
actions: {refetch: refetch}, actions: { refetch: refetch },
context: { context: {
jobId: job.id, jobId: job.id,
job: job, job: job,
@@ -680,8 +682,7 @@ export function JobsDetailHeaderActions({
{ {
key: "checklist", key: "checklist",
disabled: !job.converted, disabled: !job.converted,
label: <Link label: <Link to={`/manage/jobs/${job.id}/checklist`}>{t("jobs.actions.viewchecklist")}</Link>
to={`/manage/jobs/${job.id}/checklist`}>{t("jobs.actions.viewchecklist")}</Link>
} }
], ],
rome: "USE_IMEX", rome: "USE_IMEX",
@@ -689,35 +690,35 @@ export function JobsDetailHeaderActions({
{ {
key: "toggleproduction", key: "toggleproduction",
disabled: !job.converted || jobRO, disabled: !job.converted || jobRO,
label: <JobsDetailHeaderActionsToggleProduction job={job} refetch={refetch}/> label: <JobsDetailHeaderActionsToggleProduction job={job} refetch={refetch} />
} }
] ]
}), }),
...(InstanceRenderManager({ ...(InstanceRenderManager({
imex: true, imex: true,
rome: "USE_IMEX", rome: "USE_IMEX",
promanager: HasFeatureAccess({featureName: "timetickets", bodyshop}) promanager: HasFeatureAccess({ featureName: "timetickets", bodyshop })
}) })
? [ ? [
{ {
key: "entertimetickets", key: "entertimetickets",
disabled: !job.converted || (!bodyshop.tt_allow_post_to_invoiced && job.date_invoiced), disabled: !job.converted || (!bodyshop.tt_allow_post_to_invoiced && job.date_invoiced),
label: t("timetickets.actions.enter"), label: t("timetickets.actions.enter"),
onClick: () => { onClick: () => {
logImEXEvent("job_header_enter_time_ticekts"); logImEXEvent("job_header_enter_time_ticekts");
setTimeTicketContext({ setTimeTicketContext({
actions: {}, actions: {},
context: { context: {
jobId: job.id, jobId: job.id,
created_by: currentUser.displayName created_by: currentUser.displayName
? currentUser.email.concat(" | ", currentUser.displayName) ? currentUser.email.concat(" | ", currentUser.displayName)
: currentUser.email : currentUser.email
} }
}); });
}
} }
} ]
]
: []) : [])
]; ];
@@ -728,7 +729,7 @@ export function JobsDetailHeaderActions({
onClick: () => { onClick: () => {
setTimeTicketTaskContext({ setTimeTicketTaskContext({
actions: {}, actions: {},
context: {jobid: job.id} context: { jobid: job.id }
}); });
}, },
label: t("timetickets.actions.claimtasks") label: t("timetickets.actions.claimtasks")
@@ -744,7 +745,7 @@ export function JobsDetailHeaderActions({
setPaymentContext({ setPaymentContext({
actions: {}, actions: {},
context: {jobid: job.id} context: { jobid: job.id }
}); });
} }
}); });
@@ -759,18 +760,18 @@ export function JobsDetailHeaderActions({
setCardPaymentContext({ setCardPaymentContext({
actions: {}, actions: {},
context: {jobid: job.id} context: { jobid: job.id }
}); });
} }
}); });
} }
if (HasFeatureAccess({featureName: "courtesycars"})) { if (HasFeatureAccess({ featureName: "courtesycars" })) {
menuItems.push({ menuItems.push({
key: "cccontract", key: "cccontract",
disabled: jobRO || !job.converted, disabled: jobRO || !job.converted,
label: ( label: (
<Link state={{jobId: job.id}} to="/manage/courtesycars/contracts/new"> <Link state={{ jobId: job.id }} to="/manage/courtesycars/contracts/new">
{t("menus.jobsactions.newcccontract")} {t("menus.jobsactions.newcccontract")}
</Link> </Link>
) )
@@ -780,17 +781,17 @@ export function JobsDetailHeaderActions({
menuItems.push( menuItems.push(
job.inproduction job.inproduction
? { ? {
key: "removefromproduction", key: "removefromproduction",
disabled: !job.converted, disabled: !job.converted,
label: t("jobs.actions.removefromproduction"), label: t("jobs.actions.removefromproduction"),
onClick: () => AddToProduction(client, job.id, refetch, true) onClick: () => AddToProduction(client, job.id, refetch, true)
} }
: { : {
key: "addtoproduction", key: "addtoproduction",
disabled: !job.converted, disabled: !job.converted,
label: t("jobs.actions.addtoproduction"), label: t("jobs.actions.addtoproduction"),
onClick: () => AddToProduction(client, job.id, refetch) onClick: () => AddToProduction(client, job.id, refetch)
} }
); );
menuItems.push( menuItems.push(
@@ -846,25 +847,25 @@ export function JobsDetailHeaderActions({
...(InstanceRenderManager({ ...(InstanceRenderManager({
imex: true, imex: true,
rome: true, rome: true,
promanager: HasFeatureAccess({featureName: "bills", bodyshop}) promanager: HasFeatureAccess({ featureName: "bills", bodyshop })
}) })
? [ ? [
{ {
key: "postbills", key: "postbills",
disabled: !job.converted, disabled: !job.converted,
label: t("jobs.actions.postbills"), label: t("jobs.actions.postbills"),
onClick: () => { onClick: () => {
logImEXEvent("job_header_enter_bills"); logImEXEvent("job_header_enter_bills");
setBillEnterContext({ setBillEnterContext({
actions: {refetch: refetch}, actions: { refetch: refetch },
context: { context: {
job: job job: job
} }
}); });
}
} }
} ]
]
: []), : []),
{ {
@@ -875,7 +876,7 @@ export function JobsDetailHeaderActions({
const result = await updateJob({ const result = await updateJob({
variables: { variables: {
jobId: job.id, jobId: job.id,
job: {queued_for_parts: true} job: { queued_for_parts: true }
} }
}); });
@@ -925,7 +926,7 @@ export function JobsDetailHeaderActions({
InstanceRenderManager({ InstanceRenderManager({
imex: true, imex: true,
rome: true, rome: true,
promanager: HasFeatureAccess({featureName: "export", bodyshop}) promanager: HasFeatureAccess({ featureName: "export", bodyshop })
}) })
) { ) {
menuItems.push({ menuItems.push({
@@ -936,7 +937,7 @@ export function JobsDetailHeaderActions({
}); });
} }
if (HasFeatureAccess({featureName: "csi", bodyshop})) { if (HasFeatureAccess({ featureName: "csi", bodyshop })) {
const children = [ const children = [
{ {
key: "email", key: "email",
@@ -966,20 +967,20 @@ export function JobsDetailHeaderActions({
...job.csiinvites.map((item, idx) => { ...job.csiinvites.map((item, idx) => {
return item.completedon return item.completedon
? { ? {
key: idx, key: idx,
label: ( label: (
<Link to={`/manage/shop/csi?responseid=${item.id}`}> <Link to={`/manage/shop/csi?responseid=${item.id}`}>
<DateTimeFormatter>{item.completedon}</DateTimeFormatter> <DateTimeFormatter>{item.completedon}</DateTimeFormatter>
</Link> </Link>
) )
} }
: { : {
key: idx, key: idx,
onClick: () => { onClick: () => {
navigator.clipboard.writeText(`${window.location.protocol}//${window.location.host}/csi/${item.id}`); navigator.clipboard.writeText(`${window.location.protocol}//${window.location.host}/csi/${item.id}`);
}, },
label: t("general.actions.copylink") label: t("general.actions.copylink")
}; };
}) })
); );
} }
@@ -999,7 +1000,7 @@ export function JobsDetailHeaderActions({
logImEXEvent("job_header_job_costing"); logImEXEvent("job_header_job_costing");
setJobCostingContext({ setJobCostingContext({
actions: {refetch: refetch}, actions: { refetch: refetch },
context: { context: {
jobId: job.id jobId: job.id
} }
@@ -1109,10 +1110,10 @@ export function JobsDetailHeaderActions({
<Dropdown menu={menu} trigger={["click"]} key="changestatus"> <Dropdown menu={menu} trigger={["click"]} key="changestatus">
<Button> <Button>
<span>{t("general.labels.actions")}</span> <span>{t("general.labels.actions")}</span>
<DownCircleFilled/> <DownCircleFilled />
</Button> </Button>
</Dropdown> </Dropdown>
<Popover content={popOverContent} open={visibility}/> <Popover content={popOverContent} open={visibility} />
</> </>
); );
} }

View File

@@ -1,16 +1,16 @@
import {SyncOutlined} from "@ant-design/icons"; import { SyncOutlined } from "@ant-design/icons";
import {Button, Card, Input, Space, Table, Typography} from "antd"; import { Button, Card, Input, Space, Table, Typography } from "antd";
import axios from "axios"; import axios from "axios";
import _ from "lodash"; import _ from "lodash";
import queryString from "query-string"; import queryString from "query-string";
import React, {useEffect, useState} from "react"; import React, { useEffect, useState } from "react";
import {useTranslation} from "react-i18next"; import { useTranslation } from "react-i18next";
import {connect} from "react-redux"; import { connect } from "react-redux";
import {Link, useLocation, useNavigate} from "react-router-dom"; import { Link, useLocation, useNavigate } 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 { pageLimit } from "../../utils/config";
import useLocalStorage from "../../utils/useLocalStorage"; 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";
@@ -23,15 +23,15 @@ const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
}); });
export function JobsList({bodyshop, refetch, loading, jobs, total}) { 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 [filter, setFilter] = useLocalStorage("filter_jobs_all", null);
const {page, sortcolumn, sortorder} = search; const { page, sortcolumn, sortorder } = search;
const history = useNavigate(); const history = useNavigate();
const {t} = useTranslation(); const { t } = useTranslation();
const columns = [ const columns = [
{ {
title: t("jobs.fields.ro_number"), title: t("jobs.fields.ro_number"),
@@ -54,11 +54,11 @@ export function JobsList({bodyshop, refetch, loading, jobs, total}) {
render: (text, record) => { render: (text, record) => {
return record.ownerid ? ( return record.ownerid ? (
<Link to={"/manage/owners/" + record.ownerid}> <Link to={"/manage/owners/" + record.ownerid}>
<OwnerNameDisplay ownerObject={record}/> <OwnerNameDisplay ownerObject={record} />
</Link> </Link>
) : ( ) : (
<span> <span>
<OwnerNameDisplay ownerObject={record}/> <OwnerNameDisplay ownerObject={record} />
</span> </span>
); );
} }
@@ -69,7 +69,7 @@ export function JobsList({bodyshop, refetch, loading, jobs, total}) {
key: "ownr_ph1", key: "ownr_ph1",
ellipsis: true, ellipsis: true,
render: (text, record) => <StartChatButton phone={record.ownr_ph1} jobid={record.id}/> render: (text, record) => <StartChatButton phone={record.ownr_ph1} jobid={record.id} />
}, },
{ {
title: t("jobs.fields.ownr_ph2"), title: t("jobs.fields.ownr_ph2"),
@@ -77,7 +77,7 @@ export function JobsList({bodyshop, refetch, loading, jobs, total}) {
key: "ownr_ph2", key: "ownr_ph2",
ellipsis: true, ellipsis: true,
render: (text, record) => <StartChatButton phone={record.ownr_ph2} jobid={record.id}/> render: (text, record) => <StartChatButton phone={record.ownr_ph2} jobid={record.id} />
}, },
{ {
title: t("jobs.fields.status"), title: t("jobs.fields.status"),
@@ -92,7 +92,7 @@ export function JobsList({bodyshop, refetch, loading, jobs, total}) {
}, },
filteredValue: filter?.status || null, 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] };
}), }),
onFilter: (value, record) => value.includes(record.status) onFilter: (value, record) => value.includes(record.status)
}, },
@@ -178,7 +178,7 @@ export function JobsList({bodyshop, refetch, loading, jobs, total}) {
delete search.statusFilters; delete search.statusFilters;
} }
setFilter(filters); setFilter(filters);
history({search: queryString.stringify(search)}); history({ search: queryString.stringify(search) });
}; };
useEffect(() => { useEffect(() => {
@@ -210,13 +210,13 @@ export function JobsList({bodyshop, refetch, loading, jobs, total}) {
{search.search && ( {search.search && (
<> <>
<Typography.Title level={4}> <Typography.Title level={4}>
{t("general.labels.searchresults", {search: search.search})} {t("general.labels.searchresults", { search: search.search })}
</Typography.Title> </Typography.Title>
<Button <Button
onClick={() => { onClick={() => {
delete search.search; delete search.search;
delete search.page; delete search.page;
history({search: queryString.stringify(search)}); history({ search: queryString.stringify(search) });
}} }}
> >
{t("general.actions.clear")} {t("general.actions.clear")}
@@ -224,13 +224,13 @@ export function JobsList({bodyshop, refetch, loading, jobs, total}) {
</> </>
)} )}
<Button onClick={() => refetch()}> <Button onClick={() => refetch()}>
<SyncOutlined/> <SyncOutlined />
</Button> </Button>
<Input.Search <Input.Search
placeholder={search.search || t("general.labels.search")} placeholder={search.search || t("general.labels.search")}
onSearch={(value) => { onSearch={(value) => {
search.search = value; search.search = value;
history({search: queryString.stringify(search)}); history({ search: queryString.stringify(search) });
searchJobs(value); searchJobs(value);
}} }}
loading={loading || searchLoading} loading={loading || searchLoading}
@@ -244,15 +244,15 @@ export function JobsList({bodyshop, refetch, loading, jobs, total}) {
pagination={ pagination={
search?.search search?.search
? { ? {
pageSize: pageLimit, pageSize: pageLimit,
showSizeChanger: false showSizeChanger: false
} }
: { : {
pageSize: pageLimit, pageSize: pageLimit,
current: parseInt(page || 1), current: parseInt(page || 1),
total: total, total: total,
showSizeChanger: false showSizeChanger: false
} }
} }
columns={columns} columns={columns}
rowKey="id" rowKey="id"

View File

@@ -1,28 +1,23 @@
import { import { BranchesOutlined, ExclamationCircleFilled, PauseCircleOutlined, SyncOutlined } from "@ant-design/icons";
BranchesOutlined, import { useQuery } from "@apollo/client";
ExclamationCircleFilled, import { Button, Card, Grid, Input, Space, Table, Tooltip } from "antd";
PauseCircleOutlined,
SyncOutlined
} from "@ant-design/icons";
import {useQuery} from "@apollo/client";
import {Button, Card, Grid, Input, Space, Table, Tooltip} from "antd";
import queryString from "query-string"; import queryString from "query-string";
import React, {useState} from "react"; import React, { useState } from "react";
import {useTranslation} from "react-i18next"; import { useTranslation } from "react-i18next";
import {connect} from "react-redux"; import { connect } from "react-redux";
import {Link, useLocation, useNavigate} from "react-router-dom"; import { Link, useLocation, useNavigate } 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 { onlyUnique } from "../../utils/arrayHelper";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import {alphaSort, statusSort} from "../../utils/sorters"; import { alphaSort, statusSort } from "../../utils/sorters";
import useLocalStorage from "../../utils/useLocalStorage"; 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 {setJoyRideSteps} from "../../redux/application/application.actions"; import { setJoyRideSteps } from "../../redux/application/application.actions";
import {OwnerNameDisplayFunction} from "./../owner-name-display/owner-name-display.component"; import { OwnerNameDisplayFunction } from "./../owner-name-display/owner-name-display.component";
import InstanceRenderManager from "../../utils/instanceRenderMgr"; import InstanceRenderManager from "../../utils/instanceRenderMgr";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
@@ -33,13 +28,13 @@ const mapDispatchToProps = (dispatch) => ({
setJoyRideSteps: (steps) => dispatch(setJoyRideSteps(steps)) setJoyRideSteps: (steps) => dispatch(setJoyRideSteps(steps))
}); });
export function JobsList({bodyshop, setJoyRideSteps}) { export function JobsList({ bodyshop, setJoyRideSteps }) {
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*"]
}, },
@@ -47,36 +42,36 @@ export function JobsList({bodyshop, setJoyRideSteps}) {
nextFetchPolicy: "network-only" nextFetchPolicy: "network-only"
}); });
const [state, setState] = useState({sortedInfo: {}}); const [state, setState] = useState({ sortedInfo: {} });
const [filter, setFilter] = useLocalStorage("filter_jobs_list", null); const [filter, setFilter] = useLocalStorage("filter_jobs_list", null);
const {t} = useTranslation(); const { t } = useTranslation();
const history = useNavigate(); const history = useNavigate();
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 || "").toString().toLowerCase().includes(searchText.toLowerCase()) || (j.ro_number || "").toString().toLowerCase().includes(searchText.toLowerCase()) ||
(j.ownr_co_nm || "").toLowerCase().includes(searchText.toLowerCase()) || (j.ownr_co_nm || "").toLowerCase().includes(searchText.toLowerCase()) ||
(j.comments || "").toLowerCase().includes(searchText.toLowerCase()) || (j.comments || "").toLowerCase().includes(searchText.toLowerCase()) ||
(j.ownr_fn || "").toLowerCase().includes(searchText.toLowerCase()) || (j.ownr_fn || "").toLowerCase().includes(searchText.toLowerCase()) ||
(j.ownr_ln || "").toLowerCase().includes(searchText.toLowerCase()) || (j.ownr_ln || "").toLowerCase().includes(searchText.toLowerCase()) ||
(j.clm_no || "").toLowerCase().includes(searchText.toLowerCase()) || (j.clm_no || "").toLowerCase().includes(searchText.toLowerCase()) ||
(j.plate_no || "").toLowerCase().includes(searchText.toLowerCase()) || (j.plate_no || "").toLowerCase().includes(searchText.toLowerCase()) ||
(j.v_model_desc || "").toLowerCase().includes(searchText.toLowerCase()) || (j.v_model_desc || "").toLowerCase().includes(searchText.toLowerCase()) ||
(j.est_ct_fn || "").toLowerCase().includes(searchText.toLowerCase()) || (j.est_ct_fn || "").toLowerCase().includes(searchText.toLowerCase()) ||
(j.est_ct_ln || "").toLowerCase().includes(searchText.toLowerCase()) || (j.est_ct_ln || "").toLowerCase().includes(searchText.toLowerCase()) ||
(j.v_make_desc || "").toLowerCase().includes(searchText.toLowerCase()) (j.v_make_desc || "").toLowerCase().includes(searchText.toLowerCase())
) )
: []; : [];
const handleTableChange = (pagination, filters, sorter) => { const handleTableChange = (pagination, filters, sorter) => {
setState({...state, sortedInfo: sorter}); setState({ ...state, sortedInfo: sorter });
setFilter(filters); setFilter(filters);
}; };
@@ -107,12 +102,12 @@ export function JobsList({bodyshop, setJoyRideSteps}) {
<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 && <PauseCircleOutlined style={{color: "orangered"}}/>} {record.suspended && <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>
@@ -131,11 +126,11 @@ export function JobsList({bodyshop, setJoyRideSteps}) {
render: (text, record) => { render: (text, record) => {
return record.ownerid ? ( return record.ownerid ? (
<Link to={"/manage/owners/" + record.ownerid} onClick={(e) => e.stopPropagation()}> <Link to={"/manage/owners/" + record.ownerid} onClick={(e) => e.stopPropagation()}>
<OwnerNameDisplay ownerObject={record}/> <OwnerNameDisplay ownerObject={record} />
</Link> </Link>
) : ( ) : (
<span> <span>
<OwnerNameDisplay ownerObject={record}/> <OwnerNameDisplay ownerObject={record} />
</span> </span>
); );
} }
@@ -146,7 +141,7 @@ export function JobsList({bodyshop, setJoyRideSteps}) {
key: "ownr_ph1", key: "ownr_ph1",
ellipsis: true, ellipsis: true,
responsive: ["md"], responsive: ["md"],
render: (text, record) => <ChatOpenButton phone={record.ownr_ph1} jobid={record.id}/> render: (text, record) => <ChatOpenButton phone={record.ownr_ph1} jobid={record.id} />
}, },
{ {
title: t("jobs.fields.ownr_ph2"), title: t("jobs.fields.ownr_ph2"),
@@ -154,7 +149,7 @@ export function JobsList({bodyshop, setJoyRideSteps}) {
key: "ownr_ph2", key: "ownr_ph2",
ellipsis: true, ellipsis: true,
responsive: ["md"], responsive: ["md"],
render: (text, record) => <ChatOpenButton phone={record.ownr_ph2} jobid={record.id}/> render: (text, record) => <ChatOpenButton phone={record.ownr_ph2} jobid={record.id} />
}, },
{ {
@@ -349,7 +344,7 @@ export function JobsList({bodyshop, setJoyRideSteps}) {
})} })}
<Button onClick={() => refetch()}> <Button onClick={() => refetch()}>
<SyncOutlined/> <SyncOutlined />
</Button> </Button>
<Input.Search <Input.Search
placeholder={t("general.labels.search")} placeholder={t("general.labels.search")}
@@ -364,7 +359,7 @@ export function JobsList({bodyshop, setJoyRideSteps}) {
> >
<Table <Table
loading={loading} loading={loading}
pagination={{defaultPageSize: 50}} pagination={{ defaultPageSize: 50 }}
columns={columns} columns={columns}
rowKey="id" rowKey="id"
dataSource={jobs} dataSource={jobs}

View File

@@ -1,62 +1,66 @@
import {DeleteFilled, EyeFilled, SyncOutlined} from "@ant-design/icons"; import { DeleteFilled, EyeFilled, SyncOutlined } from "@ant-design/icons";
import {useMutation} from "@apollo/client"; import { useMutation } from "@apollo/client";
import {Button, Card, Checkbox, Drawer, Grid, Input, Popconfirm, Space, Table} from "antd"; import { Button, Card, Checkbox, Drawer, Grid, Input, Popconfirm, Space, Table } from "antd";
import {PageHeader} from "@ant-design/pro-layout"; import { PageHeader } from "@ant-design/pro-layout";
import queryString from "query-string"; import queryString from "query-string";
import React, {useState} from "react"; import React, { useState } from "react";
import {useTranslation} from "react-i18next"; import { useTranslation } from "react-i18next";
import {connect} from "react-redux"; import { connect } from "react-redux";
import {useLocation} from "react-router-dom"; import { useLocation } from "react-router-dom";
import {createStructuredSelector} from "reselect"; import { createStructuredSelector } from "reselect";
import {logImEXEvent} from "../../firebase/firebase.utils"; import { logImEXEvent } from "../../firebase/firebase.utils";
import {DELETE_PARTS_ORDER} from "../../graphql/parts-orders.queries"; import { DELETE_PARTS_ORDER } from "../../graphql/parts-orders.queries";
import {selectJobReadOnly} from "../../redux/application/application.selectors"; import { selectJobReadOnly } from "../../redux/application/application.selectors";
import {setModalContext} from "../../redux/modals/modals.actions"; 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 {DateFormatter} from "../../utils/DateFormatter"; import { DateFormatter } from "../../utils/DateFormatter";
import {alphaSort} from "../../utils/sorters"; import { alphaSort } from "../../utils/sorters";
import {TemplateList} from "../../utils/TemplateConstants"; import { TemplateList } from "../../utils/TemplateConstants";
import DataLabel from "../data-label/data-label.component"; import DataLabel from "../data-label/data-label.component";
import PartsOrderBackorderEta import PartsOrderBackorderEta from "../parts-order-backorder-eta/parts-order-backorder-eta.component";
from "../parts-order-backorder-eta/parts-order-backorder-eta.component";
import PartsOrderCmReceived from "../parts-order-cm-received/parts-order-cm-received.component"; import PartsOrderCmReceived from "../parts-order-cm-received/parts-order-cm-received.component";
import PartsOrderDeleteLine from "../parts-order-delete-line/parts-order-delete-line.component"; import PartsOrderDeleteLine from "../parts-order-delete-line/parts-order-delete-line.component";
import PartsOrderLineBackorderButton import PartsOrderLineBackorderButton from "../parts-order-line-backorder-button/parts-order-line-backorder-button.component";
from "../parts-order-line-backorder-button/parts-order-line-backorder-button.component";
import PartsReceiveModalContainer from "../parts-receive-modal/parts-receive-modal.container"; import PartsReceiveModalContainer from "../parts-receive-modal/parts-receive-modal.container";
import PrintWrapper from "../print-wrapper/print-wrapper.component"; import PrintWrapper from "../print-wrapper/print-wrapper.component";
import FeatureWrapperComponent from "../feature-wrapper/feature-wrapper.component"; import FeatureWrapperComponent from "../feature-wrapper/feature-wrapper.component";
import {FaTasks} from "react-icons/fa"; import { FaTasks } from "react-icons/fa";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
jobRO: selectJobReadOnly, jobRO: selectJobReadOnly,
bodyshop: selectBodyshop, bodyshop: selectBodyshop
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
setBillEnterContext: (context) => dispatch(setModalContext({ setBillEnterContext: (context) =>
context: context, dispatch(
modal: "billEnter" setModalContext({
})), context: context,
setPartsReceiveContext: (context) => dispatch(setModalContext({ modal: "billEnter"
context: context, })
modal: "partsReceive" ),
})), setPartsReceiveContext: (context) =>
setTaskUpsertContext: (context) => dispatch(setModalContext({context, modal: 'taskUpsert'})), dispatch(
setModalContext({
context: context,
modal: "partsReceive"
})
),
setTaskUpsertContext: (context) => dispatch(setModalContext({ context, modal: "taskUpsert" }))
}); });
export function PartsOrderListTableComponent({ export function PartsOrderListTableComponent({
setBillEnterContext, setBillEnterContext,
bodyshop, bodyshop,
jobRO, jobRO,
job, job,
billsQuery, billsQuery,
handleOnRowClick, handleOnRowClick,
setPartsReceiveContext, setPartsReceiveContext,
setTaskUpsertContext, setTaskUpsertContext
}) { }) {
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];
@@ -71,9 +75,9 @@ export function PartsOrderListTableComponent({
}; };
const drawerPercentage = selectedBreakpoint ? bpoints[selectedBreakpoint[0]] : "100%"; const drawerPercentage = selectedBreakpoint ? bpoints[selectedBreakpoint[0]] : "100%";
const responsibilityCenters = bodyshop.md_responsibility_centers; const responsibilityCenters = bodyshop.md_responsibility_centers;
const Templates = TemplateList("partsorder", {job}); const Templates = TemplateList("partsorder", { job });
const {t} = useTranslation(); const { t } = useTranslation();
const [state, setState] = useState({ const [state, setState] = useState({
sortedInfo: {} sortedInfo: {}
}); });
@@ -84,13 +88,13 @@ export function PartsOrderListTableComponent({
const [deletePartsOrder] = useMutation(DELETE_PARTS_ORDER); const [deletePartsOrder] = useMutation(DELETE_PARTS_ORDER);
const parts_orders = billsQuery.data ? billsQuery.data.parts_orders : []; const parts_orders = billsQuery.data ? billsQuery.data.parts_orders : [];
const {refetch} = billsQuery; const { refetch } = billsQuery;
const recordActions = (record, showView = false) => ( const recordActions = (record, showView = false) => (
<Space direction='horizontal' wrap> <Space direction="horizontal" wrap>
{showView && ( {showView && (
<Button onClick={() => handleOnRowClick(record)}> <Button onClick={() => handleOnRowClick(record)}>
<EyeFilled/> <EyeFilled />
</Button> </Button>
)} )}
@@ -99,7 +103,7 @@ export function PartsOrderListTableComponent({
onClick={() => { onClick={() => {
logImEXEvent("parts_order_receive_bill"); logImEXEvent("parts_order_receive_bill");
setPartsReceiveContext({ setPartsReceiveContext({
actions: {refetch: refetch}, actions: { refetch: refetch },
context: { context: {
jobId: job.id, jobId: job.id,
job: job, job: job,
@@ -119,15 +123,18 @@ export function PartsOrderListTableComponent({
> >
{t("parts_orders.actions.receive")} {t("parts_orders.actions.receive")}
</Button> </Button>
<Button title={t('tasks.buttons.create')} onClick={() => { <Button
setTaskUpsertContext({ title={t("tasks.buttons.create")}
context: { onClick={() => {
jobid: job.id, setTaskUpsertContext({
partsorderid: record.id context: {
} jobid: job.id,
}); partsorderid: record.id
}}> }
<FaTasks/> });
}}
>
<FaTasks />
</Button> </Button>
<Popconfirm <Popconfirm
title={t("parts_orders.labels.confirmdelete")} title={t("parts_orders.labels.confirmdelete")}
@@ -136,11 +143,11 @@ export function PartsOrderListTableComponent({
//Delete the parts return.! //Delete the parts return.!
await deletePartsOrder({ await deletePartsOrder({
variables: {partsOrderId: record.id}, variables: { partsOrderId: record.id },
update(cache) { update(cache) {
cache.modify({ cache.modify({
fields: { fields: {
parts_orders(existingPartsOrders, {readField}) { parts_orders(existingPartsOrders, { readField }) {
return existingPartsOrders.filter((billref) => record.id !== readField("id", billref)); return existingPartsOrders.filter((billref) => record.id !== readField("id", billref));
} }
} }
@@ -150,7 +157,7 @@ export function PartsOrderListTableComponent({
}} }}
> >
<Button disabled={jobRO}> <Button disabled={jobRO}>
<DeleteFilled/> <DeleteFilled />
</Button> </Button>
</Popconfirm> </Popconfirm>
<FeatureWrapperComponent featureName="bills" noauth={() => null}> <FeatureWrapperComponent featureName="bills" noauth={() => null}>
@@ -160,7 +167,7 @@ export function PartsOrderListTableComponent({
logImEXEvent("parts_order_receive_bill"); logImEXEvent("parts_order_receive_bill");
setBillEnterContext({ setBillEnterContext({
actions: {refetch: refetch}, actions: { refetch: refetch },
context: { context: {
job: job, job: job,
bill: { bill: {
@@ -180,7 +187,7 @@ export function PartsOrderListTableComponent({
? pol.jobline.part_type ? pol.jobline.part_type
: null : null
: responsibilityCenters.defaults && : responsibilityCenters.defaults &&
(responsibilityCenters.defaults.costs[pol.jobline.part_type] || null) (responsibilityCenters.defaults.costs[pol.jobline.part_type] || null)
: null : null
}; };
}) })
@@ -195,7 +202,7 @@ export function PartsOrderListTableComponent({
<PrintWrapper <PrintWrapper
templateObject={{ templateObject={{
name: record.return ? Templates.parts_return_slip.key : Templates.parts_order.key, name: record.return ? Templates.parts_return_slip.key : Templates.parts_order.key,
variables: {id: record.id} variables: { id: record.id }
}} }}
messageObject={{ messageObject={{
subject: record.return ? Templates.parts_return_slip.subject : Templates.parts_order.subject, subject: record.return ? Templates.parts_return_slip.subject : Templates.parts_order.subject,
@@ -236,7 +243,7 @@ export function PartsOrderListTableComponent({
key: "return", key: "return",
sorter: (a, b) => a.return - b.return, sorter: (a, b) => a.return - b.return,
sortOrder: state.sortedInfo.columnKey === "return" && state.sortedInfo.order, sortOrder: state.sortedInfo.columnKey === "return" && state.sortedInfo.order,
render: (text, record) => <Checkbox checked={record.return}/> render: (text, record) => <Checkbox checked={record.return} />
}, },
{ {
title: t("parts_orders.fields.deliver_by"), title: t("parts_orders.fields.deliver_by"),
@@ -260,7 +267,7 @@ export function PartsOrderListTableComponent({
]; ];
const handleTableChange = (pagination, filters, sorter) => { const handleTableChange = (pagination, filters, sorter) => {
setState({...state, filteredInfo: filters, sortedInfo: sorter}); setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
}; };
const selectedPartsOrderRecord = parts_orders.find((r) => r.id === selectedpartsorder); const selectedPartsOrderRecord = parts_orders.find((r) => r.id === selectedpartsorder);
@@ -291,15 +298,15 @@ export function PartsOrderListTableComponent({
}, },
...(selectedPartsOrderRecord && selectedPartsOrderRecord.return ...(selectedPartsOrderRecord && selectedPartsOrderRecord.return
? [ ? [
{ {
title: t("parts_orders.fields.cost"), title: t("parts_orders.fields.cost"),
dataIndex: "cost", dataIndex: "cost",
key: "cost", key: "cost",
sorter: (a, b) => a.cost - b.cost, sorter: (a, b) => a.cost - b.cost,
sortOrder: state.sortedInfo.columnKey === "cost" && state.sortedInfo.order, sortOrder: state.sortedInfo.columnKey === "cost" && state.sortedInfo.order,
render: (text, record) => <CurrencyFormatter>{record.cost}</CurrencyFormatter> render: (text, record) => <CurrencyFormatter>{record.cost}</CurrencyFormatter>
} }
] ]
: []), : []),
{ {
title: t("parts_orders.fields.part_type"), title: t("parts_orders.fields.part_type"),
@@ -327,19 +334,19 @@ export function PartsOrderListTableComponent({
...(selectedPartsOrderRecord && selectedPartsOrderRecord.return ...(selectedPartsOrderRecord && selectedPartsOrderRecord.return
? [ ? [
{ {
title: t("parts_orders.fields.cm_received"), title: t("parts_orders.fields.cm_received"),
dataIndex: "cm_received", dataIndex: "cm_received",
key: "cm_received", key: "cm_received",
render: (text, record) => ( render: (text, record) => (
<PartsOrderCmReceived <PartsOrderCmReceived
orderLineId={record.id} orderLineId={record.id}
checked={record.cm_received} checked={record.cm_received}
partsorderid={selectedPartsOrderRecord.id} partsorderid={selectedPartsOrderRecord.id}
/> />
) )
} }
] ]
: []), : []),
{ {
title: t("parts_orders.fields.backordered_on"), title: t("parts_orders.fields.backordered_on"),
@@ -388,8 +395,7 @@ export function PartsOrderListTableComponent({
return ( return (
<div> <div>
<PageHeader title={record && `${record.vendor.name} - ${record.order_number}`} <PageHeader title={record && `${record.vendor.name} - ${record.order_number}`} extra={recordActions(record)} />
extra={recordActions(record)}/>
<Table <Table
scroll={{ scroll={{
x: true //y: "50rem" x: true //y: "50rem"
@@ -399,7 +405,7 @@ export function PartsOrderListTableComponent({
dataSource={record.parts_order_lines} dataSource={record.parts_order_lines}
/> />
<DataLabel label={t("parts_orders.fields.comments")}> <DataLabel label={t("parts_orders.fields.comments")}>
<div style={{whiteSpace: "pre"}}>{record.comments}</div> <div style={{ whiteSpace: "pre" }}>{record.comments}</div>
</DataLabel> </DataLabel>
</div> </div>
); );
@@ -409,10 +415,10 @@ export function PartsOrderListTableComponent({
? searchText === "" ? searchText === ""
? parts_orders ? parts_orders
: parts_orders.filter( : parts_orders.filter(
(b) => (b) =>
(b.order_number || "").toString().toLowerCase().includes(searchText.toLowerCase()) || (b.order_number || "").toString().toLowerCase().includes(searchText.toLowerCase()) ||
(b.vendor.name || "").toLowerCase().includes(searchText.toLowerCase()) (b.vendor.name || "").toLowerCase().includes(searchText.toLowerCase())
) )
: []; : [];
return ( return (
@@ -421,7 +427,7 @@ export function PartsOrderListTableComponent({
extra={ extra={
<Space wrap> <Space wrap>
<Button onClick={() => refetch()}> <Button onClick={() => refetch()}>
<SyncOutlined/> <SyncOutlined />
</Button> </Button>
<Input.Search <Input.Search
placeholder={t("general.labels.search")} placeholder={t("general.labels.search")}
@@ -434,7 +440,7 @@ export function PartsOrderListTableComponent({
</Space> </Space>
} }
> >
<PartsReceiveModalContainer/> <PartsReceiveModalContainer />
<Drawer <Drawer
placement="right" placement="right"
onClose={() => handleOnRowClick(null)} onClose={() => handleOnRowClick(null)}

View File

@@ -1,9 +1,9 @@
import {Button, Card, Space, Switch, Table} from "antd"; import { Button, Card, Space, Switch, Table } from "antd";
import queryString from "query-string"; import queryString from "query-string";
import React, {useCallback, useState} from "react"; import React, { useCallback, useState } from "react";
import {useTranslation} from "react-i18next"; import { useTranslation } from "react-i18next";
import {Link, useLocation, useNavigate} from "react-router-dom"; import { Link, useLocation, useNavigate } from "react-router-dom";
import {pageLimit} from "../../utils/config"; import { pageLimit } from "../../utils/config";
import dayjs from "../../utils/day"; import dayjs from "../../utils/day";
import { import {
CheckCircleFilled, CheckCircleFilled,
@@ -15,9 +15,9 @@ import {
PlusCircleFilled, PlusCircleFilled,
SyncOutlined SyncOutlined
} from "@ant-design/icons"; } from "@ant-design/icons";
import {DateFormatter} from "../../utils/DateFormatter.jsx"; import { DateFormatter } from "../../utils/DateFormatter.jsx";
import {connect} from 'react-redux'; import { connect } from "react-redux";
import {setModalContext} from '../../redux/modals/modals.actions'; import { setModalContext } from "../../redux/modals/modals.actions";
/** /**
* Task List Component * Task List Component
@@ -25,7 +25,7 @@ import {setModalContext} from '../../redux/modals/modals.actions';
* @returns {Element} * @returns {Element}
* @constructor * @constructor
*/ */
const DueDateRecord = ({dueDate}) => { const DueDateRecord = ({ dueDate }) => {
if (!dueDate) return <></>; if (!dueDate) return <></>;
const dueDateDayjs = dayjs(dueDate); const dueDateDayjs = dayjs(dueDate);
@@ -33,11 +33,11 @@ const DueDateRecord = ({dueDate}) => {
const isBeforeToday = dueDateDayjs.isBefore(dayjs()); const isBeforeToday = dueDateDayjs.isBefore(dayjs());
return ( return (
<div title={relativeDueDate} style={{color: isBeforeToday ? 'red' : 'black'}}> <div title={relativeDueDate} style={{ color: isBeforeToday ? "red" : "black" }}>
<DateFormatter>{dueDate}</DateFormatter> <DateFormatter>{dueDate}</DateFormatter>
</div> </div>
); );
} };
/** /**
* Priority Label Component * Priority Label Component
@@ -45,30 +45,38 @@ const DueDateRecord = ({dueDate}) => {
* @returns {Element} * @returns {Element}
* @constructor * @constructor
*/ */
const PriorityLabel = ({priority}) => { const PriorityLabel = ({ priority }) => {
switch (priority) { switch (priority) {
case 1: case 1:
return <div> return (
High <ExclamationCircleFilled style={{marginLeft: '5px', color: 'red'}}/> <div>
</div>; High <ExclamationCircleFilled style={{ marginLeft: "5px", color: "red" }} />
</div>
);
case 2: case 2:
return <div> return (
Medium <ExclamationCircleFilled style={{marginLeft: '5px', color: 'yellow'}}/> <div>
</div>; Medium <ExclamationCircleFilled style={{ marginLeft: "5px", color: "yellow" }} />
</div>
);
case 3: case 3:
return <div> return (
Low <ExclamationCircleFilled style={{marginLeft: '5px', color: 'green'}}/> <div>
</div>; Low <ExclamationCircleFilled style={{ marginLeft: "5px", color: "green" }} />
</div>
);
default: default:
return <div> return (
None <ExclamationCircleFilled style={{marginLeft: '5px', color: 'black'}}/> <div>
</div>; None <ExclamationCircleFilled style={{ marginLeft: "5px", color: "black" }} />
</div>
);
} }
} };
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
// Existing dispatch props... // Existing dispatch props...
setTaskUpsertContext: (context) => dispatch(setModalContext({context, modal: 'taskUpsert'})), setTaskUpsertContext: (context) => dispatch(setModalContext({ context, modal: "taskUpsert" }))
}); });
const mapStateToProps = (state) => ({ const mapStateToProps = (state) => ({
@@ -78,69 +86,59 @@ const mapStateToProps = (state) => ({
export default connect(mapStateToProps, mapDispatchToProps)(TaskListComponent); export default connect(mapStateToProps, mapDispatchToProps)(TaskListComponent);
function TaskListComponent({ function TaskListComponent({
bodyshop, bodyshop,
loading, loading,
tasks, tasks,
total, total,
titleTranslation, titleTranslation,
refetch, refetch,
toggleCompletedStatus, toggleCompletedStatus,
setTaskUpsertContext, setTaskUpsertContext,
toggleDeletedStatus, toggleDeletedStatus,
relationshipType, relationshipType,
relationshipId, relationshipId,
onlyMine, onlyMine,
parentJobId, parentJobId,
showRo = true, showRo = true
}) { }) {
const {t} = useTranslation(); const { t } = useTranslation();
const search = queryString.parse(useLocation().search); const search = queryString.parse(useLocation().search);
// Extract Query Params // Extract Query Params
const { const { page, sortcolumn, sortorder, deleted, completed, mine } = search;
page,
sortcolumn,
sortorder,
deleted,
completed,
mine
} = search;
const history = useNavigate(); const history = useNavigate();
const columns = []; const columns = [];
if (!onlyMine) { if (!onlyMine) {
columns.push( columns.push({
{ title: t("tasks.fields.assigned_to"),
title: t("tasks.fields.assigned_to"), dataIndex: "assigned_to",
dataIndex: "assigned_to", key: "assigned_to",
key: "assigned_to", width: "8%",
width: '8%', sorter: true,
sorter: true, sortOrder: sortcolumn === "assigned_to" && sortorder,
sortOrder: sortcolumn === "assigned_to" && sortorder, render: (text, record) => {
render: (text, record) => { const employee = bodyshop?.employees?.find((e) => e.user_email === record.assigned_to);
const employee = bodyshop?.employees?.find(e => e.user_email === record.assigned_to); return employee ? `${employee.first_name} ${employee.last_name}` : t("general.labels.na");
return employee ? `${employee.first_name} ${employee.last_name}` : t("general.labels.na");
}
} }
); });
} }
if (showRo) { if (showRo) {
columns.push( columns.push({
{ title: t("tasks.fields.job.ro_number"),
title: t("tasks.fields.job.ro_number"), dataIndex: ["job", "ro_number"],
dataIndex: ["job", "ro_number"], key: "job.ro_number",
key: "job.ro_number", width: "8%",
width: '8%', render: (text, record) =>
render: (text, record) => record.job ? (
record.job <Link to={`/manage/jobs/${record.job.id}`}>{record.job.ro_number || t("general.labels.na")}</Link>
? <Link ) : (
to={`/manage/jobs/${record.job.id}`}>{record.job.ro_number || t("general.labels.na")}</Link> t("general.labels.na")
: t("general.labels.na") )
} });
);
} }
columns.push( columns.push(
@@ -148,44 +146,47 @@ function TaskListComponent({
title: t("tasks.fields.jobline"), title: t("tasks.fields.jobline"),
dataIndex: ["jobline", "id"], dataIndex: ["jobline", "id"],
key: "jobline.id", key: "jobline.id",
width: '8%', width: "8%",
render: (text, record) => record?.jobline?.line_desc || '' render: (text, record) => record?.jobline?.line_desc || ""
}, },
{ {
title: t("tasks.fields.parts_order"), title: t("tasks.fields.parts_order"),
dataIndex: ["parts_order", "id"], dataIndex: ["parts_order", "id"],
key: "part_order.id", key: "part_order.id",
width: '8%', width: "8%",
render: (text, record) => render: (text, record) =>
record.parts_order record.parts_order ? (
? <Link <Link to={`/manage/jobs/${record.job.id}?partsorderid=${record.parts_order.id}&tab=partssublet`}>
to={`/manage/jobs/${record.job.id}?partsorderid=${record.parts_order.id}&tab=partssublet`}>
{record.parts_order.order_number && record.parts_order.vendor && record.parts_order.vendor.name {record.parts_order.order_number && record.parts_order.vendor && record.parts_order.vendor.name
? `${record.parts_order.order_number} - ${record.parts_order.vendor.name}` ? `${record.parts_order.order_number} - ${record.parts_order.vendor.name}`
: t("general.labels.na")} : t("general.labels.na")}
</Link> </Link>
: '' ) : (
""
)
}, },
{ {
title: t("tasks.fields.bill"), title: t("tasks.fields.bill"),
dataIndex: ["bill", "id"], dataIndex: ["bill", "id"],
key: "bill.id", key: "bill.id",
width: '8%', width: "8%",
render: (text, record) => render: (text, record) =>
record.bill record.bill ? (
? <Link to={`/manage/jobs/${record.job.id}?billid=${record.bill.id}&tab=partssublet`}> <Link to={`/manage/jobs/${record.job.id}?billid=${record.bill.id}&tab=partssublet`}>
{record.bill.invoice_number && record.bill.vendor && record.bill.vendor.name {record.bill.invoice_number && record.bill.vendor && record.bill.vendor.name
? `${record.bill.invoice_number} - ${record.bill.vendor.name}` ? `${record.bill.invoice_number} - ${record.bill.vendor.name}`
: t("general.labels.na")} : t("general.labels.na")}
</Link> </Link>
: '' ) : (
""
)
}, },
{ {
title: t("tasks.fields.title"), title: t("tasks.fields.title"),
dataIndex: "title", dataIndex: "title",
key: "title", key: "title",
sorter: true, sorter: true,
sortOrder: sortcolumn === "title" && sortorder, sortOrder: sortcolumn === "title" && sortorder
}, },
{ {
title: t("tasks.fields.due_date"), title: t("tasks.fields.due_date"),
@@ -193,8 +194,8 @@ function TaskListComponent({
key: "due_date", key: "due_date",
sorter: true, sorter: true,
sortOrder: sortcolumn === "due_date" && sortorder, sortOrder: sortcolumn === "due_date" && sortorder,
width: '8%', width: "8%",
render: (text, record) => <DueDateRecord dueDate={record.due_date}/>, render: (text, record) => <DueDateRecord dueDate={record.due_date} />
}, },
{ {
title: t("tasks.fields.remind_at"), title: t("tasks.fields.remind_at"),
@@ -202,8 +203,8 @@ function TaskListComponent({
key: "remind_at", key: "remind_at",
sorter: true, sorter: true,
sortOrder: sortcolumn === "remind_at" && sortorder, sortOrder: sortcolumn === "remind_at" && sortorder,
width: '8%', width: "8%",
render: (text, record) => <DueDateRecord dueDate={record.remind_at}/>, render: (text, record) => <DueDateRecord dueDate={record.remind_at} />
}, },
{ {
title: t("tasks.fields.priority"), title: t("tasks.fields.priority"),
@@ -211,75 +212,82 @@ function TaskListComponent({
key: "priority", key: "priority",
sorter: true, sorter: true,
sortOrder: sortcolumn === "priority" && sortorder, sortOrder: sortcolumn === "priority" && sortorder,
width: '8%', width: "8%",
render: (text, record) => <PriorityLabel priority={record.priority} render: (text, record) => <PriorityLabel priority={record.priority} />
/>
}, },
{ {
title: t("tasks.fields.actions"), title: t("tasks.fields.actions"),
key: "toggleCompleted", key: "toggleCompleted",
width: '5%', width: "5%",
render: (text, record) => ( render: (text, record) => (
<Space direction='horizontal'> <Space direction="horizontal">
<Button title={t('tasks.buttons.edit')} onClick={() => { <Button
setTaskUpsertContext({ title={t("tasks.buttons.edit")}
context: { onClick={() => {
existingTask: record, setTaskUpsertContext({
}, context: {
}); existingTask: record
}}> }
<EditFilled/> });
}}
>
<EditFilled />
</Button> </Button>
<Button title={t('tasks.buttons.complete')} <Button
onClick={() => toggleCompletedStatus(record.id, record.completed)}> title={t("tasks.buttons.complete")}
{record.completed ? <CheckCircleOutlined/> : onClick={() => toggleCompletedStatus(record.id, record.completed)}
<CheckCircleFilled/>} >
{record.completed ? <CheckCircleOutlined /> : <CheckCircleFilled />}
</Button> </Button>
<Button title={t('tasks.buttons.delete')} <Button title={t("tasks.buttons.delete")} onClick={() => toggleDeletedStatus(record.id, record.deleted)}>
onClick={() => toggleDeletedStatus(record.id, record.deleted)}> {record.deleted ? <DeleteFilled /> : <DeleteOutlined />}
{record.deleted ? <DeleteFilled/> : <DeleteOutlined/>}
</Button> </Button>
</Space> </Space>
), )
} }
); );
const [state, setState] = useState({ const [state, setState] = useState({
sortedInfo: {}, sortedInfo: {},
filteredInfo: {text: ""}, filteredInfo: { text: "" }
}); });
const handleCreateTask = useCallback(() => { const handleCreateTask = useCallback(() => {
setTaskUpsertContext({ setTaskUpsertContext({
actions: {}, actions: {},
context: { context: {
jobid: parentJobId, jobid: parentJobId,
[relationshipType]: relationshipId, [relationshipType]: relationshipId
}, }
}); });
}, [ parentJobId, relationshipId, relationshipType, setTaskUpsertContext]); }, [parentJobId, relationshipId, relationshipType, setTaskUpsertContext]);
const handleTableChange = (pagination, filters, sorter) => { const handleTableChange = (pagination, filters, sorter) => {
setState({...state, filteredInfo: filters, sortedInfo: sorter}); setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
search.page = pagination.current; search.page = pagination.current;
search.sortcolumn = sorter.columnKey; search.sortcolumn = sorter.columnKey;
search.sortorder = sorter.order; search.sortorder = sorter.order;
history({search: queryString.stringify(search)}); history({ search: queryString.stringify(search) });
}; };
const handleSwitchChange = useCallback((param, value) => { const handleSwitchChange = useCallback(
if (value) { (param, value) => {
search[param] = "true"; if (value) {
} else { search[param] = "true";
delete search[param]; } else {
} delete search[param];
history({search: queryString.stringify(search)}); }
}, [history, search]); history({ search: queryString.stringify(search) });
},
[history, search]
);
const expandableRow = (record) => { const expandableRow = (record) => {
return <Card title={t('tasks.fields.description')} size='small'> return (
{record.description} <Card title={t("tasks.fields.description")} size="small">
</Card> {record.description}
</Card>
);
}; };
/** /**
@@ -288,46 +296,43 @@ function TaskListComponent({
*/ */
const tasksExtra = useCallback(() => { const tasksExtra = useCallback(() => {
return ( return (
<Space direction='horizontal'> <Space direction="horizontal">
{!onlyMine && ( {!onlyMine && (
<Switch <Switch
checkedChildren={t('tasks.buttons.myTasks')} checkedChildren={t("tasks.buttons.myTasks")}
unCheckedChildren={t('tasks.buttons.allTasks')} unCheckedChildren={t("tasks.buttons.allTasks")}
title={t('tasks.titles.mine')} title={t("tasks.titles.mine")}
checked={mine === "true"} checked={mine === "true"}
onChange={(value) => handleSwitchChange('mine', value)} onChange={(value) => handleSwitchChange("mine", value)}
/> />
)} )}
<Switch <Switch
checkedChildren={<CheckCircleFilled/>} checkedChildren={<CheckCircleFilled />}
unCheckedChildren={<CheckCircleOutlined/>} unCheckedChildren={<CheckCircleOutlined />}
title={t('tasks.titles.completed')} title={t("tasks.titles.completed")}
checked={completed === "true"} checked={completed === "true"}
onChange={(value) => handleSwitchChange('completed', value)} onChange={(value) => handleSwitchChange("completed", value)}
/> />
<Switch <Switch
checkedChildren={<DeleteOutlined/>} checkedChildren={<DeleteOutlined />}
unCheckedChildren={<DeleteFilled/>} unCheckedChildren={<DeleteFilled />}
title={t('tasks.titles.deleted')} title={t("tasks.titles.deleted")}
checked={deleted === "true"} checked={deleted === "true"}
onChange={(value) => handleSwitchChange('deleted', value)} onChange={(value) => handleSwitchChange("deleted", value)}
/> />
<Button title={t('tasks.buttons.create')} onClick={handleCreateTask}> <Button title={t("tasks.buttons.create")} onClick={handleCreateTask}>
<PlusCircleFilled/>{t('tasks.buttons.create')} <PlusCircleFilled />
{t("tasks.buttons.create")}
</Button> </Button>
<Button title={t('tasks.buttons.refresh')} <Button title={t("tasks.buttons.refresh")} onClick={() => refetch()}>
onClick={() => refetch()}> <SyncOutlined />
<SyncOutlined/>
</Button> </Button>
</Space> </Space>
); );
}, [refetch, deleted, completed, mine, onlyMine, t, handleSwitchChange, handleCreateTask]); }, [refetch, deleted, completed, mine, onlyMine, t, handleSwitchChange, handleCreateTask]);
return ( return (
<Card <Card title={titleTranslation} extra={tasksExtra()}>
title={titleTranslation}
extra={tasksExtra()}
>
<Table <Table
loading={loading} loading={loading}
pagination={{ pagination={{
@@ -335,16 +340,16 @@ function TaskListComponent({
current: parseInt(page || 1), current: parseInt(page || 1),
total: total, total: total,
responsive: true, responsive: true,
showQuickJumper: true, showQuickJumper: true
}} }}
columns={columns} columns={columns}
rowKey="id" rowKey="id"
scroll={{x: true}} scroll={{ x: true }}
dataSource={tasks} dataSource={tasks}
onChange={handleTableChange} onChange={handleTableChange}
expandable={{ expandable={{
expandedRowRender: expandableRow, expandedRowRender: expandableRow,
rowExpandable: record => record.description, rowExpandable: (record) => record.description
}} }}
/> />
</Card> </Card>

View File

@@ -1,73 +1,63 @@
import queryString from "query-string"; import queryString from "query-string";
import {useLocation} from "react-router-dom"; import { useLocation } from "react-router-dom";
import {useMutation, useQuery} from "@apollo/client"; import { useMutation, useQuery } from "@apollo/client";
import { import { MUTATION_TOGGLE_TASK_COMPLETED, MUTATION_TOGGLE_TASK_DELETED } from "../../graphql/tasks.queries.js";
MUTATION_TOGGLE_TASK_COMPLETED, import { pageLimit } from "../../utils/config.js";
MUTATION_TOGGLE_TASK_DELETED,
} from "../../graphql/tasks.queries.js";
import {pageLimit} from "../../utils/config.js";
import AlertComponent from "../alert/alert.component.jsx"; import AlertComponent from "../alert/alert.component.jsx";
import React, {useEffect} from "react"; import React, { useEffect } from "react";
import TaskListComponent from "./task-list.component.jsx"; import TaskListComponent from "./task-list.component.jsx";
import {notification} from "antd"; import { notification } from "antd";
import {useTranslation} from "react-i18next"; import { useTranslation } from "react-i18next";
import {useDispatch} from "react-redux"; import { useDispatch } from "react-redux";
import {insertAuditTrail} from "../../redux/application/application.actions.js"; import { insertAuditTrail } from "../../redux/application/application.actions.js";
import AuditTrailMapping from "../../utils/AuditTrailMappings.js"; import AuditTrailMapping from "../../utils/AuditTrailMappings.js";
export default function TaskListContainer({ export default function TaskListContainer({
bodyshop, bodyshop,
titleTranslation, titleTranslation,
query, query,
relationshipType, relationshipType,
relationshipId, relationshipId,
currentUser, currentUser,
onlyMine, onlyMine,
parentJobId, parentJobId,
showRo = true showRo = true
}) { }) {
const {t} = useTranslation(); const { t } = useTranslation();
const searchParams = queryString.parse(useLocation().search); const searchParams = queryString.parse(useLocation().search);
const {page, sortcolumn, sortorder, deleted, completed, mine} = searchParams; const { page, sortcolumn, sortorder, deleted, completed, mine } = searchParams;
const dispatch = useDispatch(); const dispatch = useDispatch();
const {loading, error, data, refetch} = useQuery( const { loading, error, data, refetch } = useQuery(query, {
query, fetchPolicy: "network-only",
{ nextFetchPolicy: "network-only",
fetchPolicy: "network-only", variables: {
nextFetchPolicy: "network-only", bodyshop: bodyshop.id,
variables: { [relationshipType]: relationshipId,
bodyshop: bodyshop.id, deleted: deleted === "true",
[relationshipType]: relationshipId, completed: completed === "true",
deleted: deleted === 'true', assigned_to: mine === "true" ? currentUser.email : undefined, // replace currentUserID with the actual ID of the current user
completed: completed === "true", offset: page ? (page - 1) * pageLimit : 0,
assigned_to: mine === "true" ? currentUser.email : undefined, // replace currentUserID with the actual ID of the current user limit: pageLimit,
offset: page ? (page - 1) * pageLimit : 0, order: [
limit: pageLimit, {
order: [ [sortcolumn || "created_at"]: sortorder ? (sortorder === "descend" ? "desc" : "asc") : "desc"
{ }
[sortcolumn || "created_at"]: sortorder ]
? sortorder === "descend"
? "desc"
: "asc"
: "desc",
},
],
},
} }
); });
/** /**
* Refetch tasks when a task is updated * Refetch tasks when a task is updated
*/ */
useEffect(() => { useEffect(() => {
const handleTaskUpdated = async (event) => { const handleTaskUpdated = async (event) => {
await refetch().catch(e => `Something went wrong fetching tasks: ${e.message || ''}`); await refetch().catch((e) => `Something went wrong fetching tasks: ${e.message || ""}`);
}; };
window.addEventListener('taskUpdated', handleTaskUpdated); window.addEventListener("taskUpdated", handleTaskUpdated);
// Clean up the event listener when the component is unmounted. // Clean up the event listener when the component is unmounted.
return () => { return () => {
window.removeEventListener('taskUpdated', handleTaskUpdated); window.removeEventListener("taskUpdated", handleTaskUpdated);
}; };
}, [refetch]); }, [refetch]);
@@ -97,27 +87,25 @@ export default function TaskListContainer({
dispatch( dispatch(
insertAuditTrail({ insertAuditTrail({
jobid: toggledTask.data.update_tasks_by_pk.jobid, jobid: toggledTask.data.update_tasks_by_pk.jobid,
operation: toggledTask?.data?.update_tasks_by_pk?.completed ? AuditTrailMapping.tasksCompleted( operation: toggledTask?.data?.update_tasks_by_pk?.completed
toggledTask.data.update_tasks_by_pk.title, ? AuditTrailMapping.tasksCompleted(toggledTask.data.update_tasks_by_pk.title, currentUser.email)
currentUser.email : AuditTrailMapping.tasksUncompleted(toggledTask.data.update_tasks_by_pk.title, currentUser.email),
) : AuditTrailMapping.tasksUncompleted(
toggledTask.data.update_tasks_by_pk.title,
currentUser.email
),
type: toggledTask?.data?.update_tasks_by_pk?.completed ? "tasksCompleted" : "tasksUncompleted" type: toggledTask?.data?.update_tasks_by_pk?.completed ? "tasksCompleted" : "tasksUncompleted"
}) })
) );
} }
window.dispatchEvent(new CustomEvent('taskUpdated', { window.dispatchEvent(
detail: {message: 'A task has been completed.'}, new CustomEvent("taskUpdated", {
})); detail: { message: "A task has been completed." }
})
);
notification["success"]({ notification["success"]({
message: t("tasks.successes.completed"), message: t("tasks.successes.completed")
}); });
} catch (err) { } catch (err) {
notification["error"]({ notification["error"]({
message: t("tasks.failures.completed"), message: t("tasks.failures.completed")
}); });
} }
}; };
@@ -148,35 +136,31 @@ export default function TaskListContainer({
dispatch( dispatch(
insertAuditTrail({ insertAuditTrail({
jobid: toggledTask.data.update_tasks_by_pk.jobid, jobid: toggledTask.data.update_tasks_by_pk.jobid,
operation: toggledTask?.data?.update_tasks_by_pk?.deleted ? AuditTrailMapping.tasksDeleted( operation: toggledTask?.data?.update_tasks_by_pk?.deleted
toggledTask.data.update_tasks_by_pk.title, ? AuditTrailMapping.tasksDeleted(toggledTask.data.update_tasks_by_pk.title, currentUser.email)
currentUser.email : AuditTrailMapping.tasksUndeleted(toggledTask.data.update_tasks_by_pk.title, currentUser.email),
) : AuditTrailMapping.tasksUndeleted(
toggledTask.data.update_tasks_by_pk.title,
currentUser.email
),
type: toggledTask?.data?.update_tasks_by_pk?.deleted ? "tasksDeleted" : "tasksUndeleted" type: toggledTask?.data?.update_tasks_by_pk?.deleted ? "tasksDeleted" : "tasksUndeleted"
}) })
) );
} }
window.dispatchEvent(
window.dispatchEvent(new CustomEvent('taskUpdated', { new CustomEvent("taskUpdated", {
detail: {message: 'A task has been deleted.'}, detail: { message: "A task has been deleted." }
})); })
);
notification["success"]({ notification["success"]({
message: t("tasks.successes.deleted"), message: t("tasks.successes.deleted")
}); });
} catch (err) { } catch (err) {
console.dir(err); console.dir(err);
notification["error"]({ notification["error"]({
message: t("tasks.failures.deleted"), message: t("tasks.failures.deleted")
}); });
} }
}; };
if (error) return <AlertComponent message={error.message} type="error"/>; if (error) return <AlertComponent message={error.message} type="error" />;
return ( return (
<TaskListComponent <TaskListComponent

View File

@@ -1,49 +1,44 @@
import {Col, Form, Input, Row, Select, Switch} from "antd"; import { Col, Form, Input, Row, Select, Switch } from "antd";
import React from "react"; import React from "react";
import {useTranslation} from "react-i18next"; import { useTranslation } from "react-i18next";
import {FormDatePicker} from "../form-date-picker/form-date-picker.component.jsx"; import { FormDatePicker } from "../form-date-picker/form-date-picker.component.jsx";
import {createStructuredSelector} from "reselect"; import { createStructuredSelector } from "reselect";
import {selectBodyshop, selectCurrentUser} from "../../redux/user/user.selectors.js"; import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors.js";
import dayjs from '../../utils/day'; import dayjs from "../../utils/day";
import {connect} from "react-redux"; import { connect } from "react-redux";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component.jsx"; import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component.jsx";
import JobSearchSelectComponent from "../job-search-select/job-search-select.component.jsx"; import JobSearchSelectComponent from "../job-search-select/job-search-select.component.jsx";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
currentUser: selectCurrentUser, currentUser: selectCurrentUser
}); });
const mapDispatchToProps = (dispatch) => ({}); const mapDispatchToProps = (dispatch) => ({});
export default connect( export default connect(mapStateToProps, mapDispatchToProps)(TaskUpsertModalComponent);
mapStateToProps,
mapDispatchToProps
)(TaskUpsertModalComponent);
export function TaskUpsertModalComponent({ export function TaskUpsertModalComponent({
form, form,
bodyshop, bodyshop,
currentUser, currentUser,
selectedJobId, selectedJobId,
setSelectedJobId, setSelectedJobId,
selectedJobDetails, selectedJobDetails,
loading, loading,
error, error
}) { }) {
const {t} = useTranslation(); const { t } = useTranslation();
const datePickerPresets = [ const datePickerPresets = [
{label: t('tasks.date_presets.today'), value: dayjs()}, { label: t("tasks.date_presets.today"), value: dayjs() },
{label: t('tasks.date_presets.tomorrow'), value: dayjs().add(1, 'day')}, { label: t("tasks.date_presets.tomorrow"), value: dayjs().add(1, "day") },
{label: t('tasks.date_presets.next_week'), value: dayjs().add(1, 'week')}, { label: t("tasks.date_presets.next_week"), value: dayjs().add(1, "week") },
{label: t('tasks.date_presets.two_weeks'), value: dayjs().add(2, 'weeks')}, { label: t("tasks.date_presets.two_weeks"), value: dayjs().add(2, "weeks") },
{label: t('tasks.date_presets.three_weeks'), value: dayjs().add(3, 'weeks')}, { label: t("tasks.date_presets.three_weeks"), value: dayjs().add(3, "weeks") },
{label: t('tasks.date_presets.one_month'), value: dayjs().add(1, 'month')}, { label: t("tasks.date_presets.one_month"), value: dayjs().add(1, "month") },
{label: t('tasks.date_presets.three_months'), value: dayjs().add(3, 'month')}, { label: t("tasks.date_presets.three_months"), value: dayjs().add(3, "month") }
]; ];
const clearRelations = () => { const clearRelations = () => {
@@ -52,7 +47,7 @@ export function TaskUpsertModalComponent({
partsorderid: null, partsorderid: null,
joblineid: null joblineid: null
}); });
} };
/** /**
* Change the selected job id * Change the selected job id
@@ -64,8 +59,7 @@ export function TaskUpsertModalComponent({
clearRelations(); clearRelations();
}; };
if (loading || error) return <LoadingSkeleton active />;
if (loading || error) return <LoadingSkeleton active/>;
return ( return (
<> <>
@@ -76,24 +70,20 @@ export function TaskUpsertModalComponent({
name="title" name="title"
rules={[ rules={[
{ {
required: true, required: true
}, }
]} ]}
> >
<Input placeholder={t("tasks.fields.title")}/> <Input placeholder={t("tasks.fields.title")} />
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={4}> <Col span={4}>
<Form.Item <Form.Item label={t("tasks.fields.priority")} name="priority" initialValue={3}>
label={t("tasks.fields.priority")}
name="priority"
initialValue={3}
>
<Select <Select
options={[ options={[
{value: 3, label: t("tasks.fields.priorities.low")}, { value: 3, label: t("tasks.fields.priorities.low") },
{value: 2, label: t("tasks.fields.priorities.medium")}, { value: 2, label: t("tasks.fields.priorities.medium") },
{value: 1, label: t("tasks.fields.priorities.high")}, { value: 1, label: t("tasks.fields.priorities.high") }
]} ]}
/> />
</Form.Item> </Form.Item>
@@ -106,11 +96,11 @@ export function TaskUpsertModalComponent({
initialValue={false} initialValue={false}
rules={[ rules={[
{ {
required: true, required: true
}, }
]} ]}
> >
<Switch/> <Switch />
</Form.Item> </Form.Item>
</Col> </Col>
</Row> </Row>
@@ -122,23 +112,27 @@ export function TaskUpsertModalComponent({
label={t("tasks.fields.jobid")} label={t("tasks.fields.jobid")}
rules={[ rules={[
{ {
required: true, required: true
}, }
]} ]}
> >
<JobSearchSelectComponent placeholder={t('tasks.placeholders.jobid')} <JobSearchSelectComponent
onSelect={changeJobId} onClear={changeJobId} autoFocus={false}/> placeholder={t("tasks.placeholders.jobid")}
onSelect={changeJobId}
onClear={changeJobId}
autoFocus={false}
/>
</Form.Item> </Form.Item>
</Col> </Col>
</Row> </Row>
<Row gutter={[16, 16]}> <Row gutter={[16, 16]}>
<Col span={8}> <Col span={8}>
<Form.Item <Form.Item label={t("tasks.fields.joblineid")} name="joblineid">
label={t("tasks.fields.joblineid")} <Select
name="joblineid" allowClear
> placeholder={t("tasks.placeholders.joblineid")}
<Select allowClear placeholder={t("tasks.placeholders.joblineid")} disabled={!selectedJobDetails || !selectedJobId}
disabled={!selectedJobDetails || !selectedJobId}> >
{selectedJobDetails?.joblines?.map((jobline) => ( {selectedJobDetails?.joblines?.map((jobline) => (
<Select.Option key={jobline.id} value={jobline.id}> <Select.Option key={jobline.id} value={jobline.id}>
{jobline.line_desc} {jobline.line_desc}
@@ -148,12 +142,12 @@ export function TaskUpsertModalComponent({
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={8}> <Col span={8}>
<Form.Item <Form.Item label={t("tasks.fields.partsorderid")} name="partsorderid">
label={t("tasks.fields.partsorderid")} <Select
name="partsorderid" allowClear
> placeholder={t("tasks.placeholders.partsorderid")}
<Select allowClear placeholder={t("tasks.placeholders.partsorderid")} disabled={!selectedJobDetails || !selectedJobId}
disabled={!selectedJobDetails || !selectedJobId}> >
{selectedJobDetails?.parts_orders?.map((partsOrder) => ( {selectedJobDetails?.parts_orders?.map((partsOrder) => (
<Select.Option key={partsOrder.id} value={partsOrder.id}> <Select.Option key={partsOrder.id} value={partsOrder.id}>
{partsOrder.order_number} - {partsOrder.vendor.name} {partsOrder.order_number} - {partsOrder.vendor.name}
@@ -163,12 +157,12 @@ export function TaskUpsertModalComponent({
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={8}> <Col span={8}>
<Form.Item <Form.Item label={t("tasks.fields.billid")} name="billid">
label={t("tasks.fields.billid")} <Select
name="billid" allowClear
> placeholder={t("tasks.placeholders.billid")}
<Select allowClear placeholder={t("tasks.placeholders.billid")} disabled={!selectedJobDetails || !selectedJobId}
disabled={!selectedJobDetails || !selectedJobId}> >
{selectedJobDetails?.bills?.map((bill) => ( {selectedJobDetails?.bills?.map((bill) => (
<Select.Option key={bill.id} value={bill.id}> <Select.Option key={bill.id} value={bill.id}>
{bill.invoice_number} - {bill.vendor.name} {bill.invoice_number} - {bill.vendor.name}
@@ -186,46 +180,36 @@ export function TaskUpsertModalComponent({
initialValue={currentUser.email} initialValue={currentUser.email}
rules={[ rules={[
{ {
required: true, required: true
}, }
]} ]}
> >
<Select placeholder={t("tasks.placeholders.assigned_to")}> <Select placeholder={t("tasks.placeholders.assigned_to")}>
{bodyshop.employees.filter(x => x.active).map((employee) => ( {bodyshop.employees
<Select.Option key={employee.id} value={employee.user_email}> .filter((x) => x.active)
{employee.first_name} {employee.last_name} .map((employee) => (
</Select.Option> <Select.Option key={employee.id} value={employee.user_email}>
))} {employee.first_name} {employee.last_name}
</Select.Option>
))}
</Select> </Select>
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={8}> <Col span={8}>
<Form.Item <Form.Item label={t("tasks.fields.due_date")} name="due_date">
label={t("tasks.fields.due_date")} <FormDatePicker format="MM/DD/YYYY" presets={datePickerPresets} />
name="due_date"
>
<FormDatePicker format="MM/DD/YYYY" presets={datePickerPresets}/>
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={8}> <Col span={8}>
<Form.Item <Form.Item label={t("tasks.fields.remind_at")} name="remind_at">
label={t("tasks.fields.remind_at")} <FormDatePicker format="MM/DD/YYYY" presets={datePickerPresets} />
name="remind_at"
>
<FormDatePicker format="MM/DD/YYYY" presets={datePickerPresets}/>
</Form.Item> </Form.Item>
</Col> </Col>
</Row> </Row>
<Row gutter={[16, 16]}> <Row gutter={[16, 16]}>
<Col span={24}> <Col span={24}>
<Form.Item <Form.Item label={t("tasks.fields.description")} name="description">
label={t("tasks.fields.description")} <Input.TextArea rows={8} placeholder={t("tasks.fields.description")} />
name="description"
>
<Input.TextArea
rows={8}
placeholder={t("tasks.fields.description")}
/>
</Form.Item> </Form.Item>
</Col> </Col>
</Row> </Row>

View File

@@ -1,30 +1,26 @@
import {useMutation, useQuery} from "@apollo/client"; import { useMutation, useQuery } from "@apollo/client";
import {Form, Modal, notification} from "antd"; import { Form, Modal, notification } from "antd";
import React, {useEffect, useState} from "react"; import React, { useEffect, useState } from "react";
import {useTranslation} from "react-i18next"; import { useTranslation } from "react-i18next";
import {connect} from "react-redux"; import { connect } from "react-redux";
import {createStructuredSelector} from "reselect"; import { createStructuredSelector } from "reselect";
import { import { MUTATION_INSERT_NEW_TASK, MUTATION_UPDATE_TASK, QUERY_GET_TASK_BY_ID } from "../../graphql/tasks.queries";
MUTATION_INSERT_NEW_TASK, import { QUERY_GET_TASKS_JOB_DETAILS_BY_ID } from "../../graphql/jobs.queries.js";
MUTATION_UPDATE_TASK, import { toggleModalVisible } from "../../redux/modals/modals.actions";
QUERY_GET_TASK_BY_ID import { selectTaskUpsert } from "../../redux/modals/modals.selectors";
} from "../../graphql/tasks.queries"; import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import {QUERY_GET_TASKS_JOB_DETAILS_BY_ID} from "../../graphql/jobs.queries.js";
import {toggleModalVisible} from "../../redux/modals/modals.actions";
import {selectTaskUpsert} from "../../redux/modals/modals.selectors";
import {selectBodyshop, selectCurrentUser} from "../../redux/user/user.selectors";
import TaskUpsertModalComponent from "./task-upsert-modal.component"; import TaskUpsertModalComponent from "./task-upsert-modal.component";
import {replaceUndefinedWithNull} from "../../utils/undefinedtonull.js"; import { replaceUndefinedWithNull } from "../../utils/undefinedtonull.js";
import {useNavigate} from "react-router-dom"; import { useNavigate } from "react-router-dom";
import axios from "axios"; import axios from "axios";
import dayjs from '../../utils/day'; import dayjs from "../../utils/day";
import {insertAuditTrail} from "../../redux/application/application.actions.js"; import { insertAuditTrail } from "../../redux/application/application.actions.js";
import AuditTrailMapping from "../../utils/AuditTrailMappings.js"; import AuditTrailMapping from "../../utils/AuditTrailMappings.js";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
taskUpsert: selectTaskUpsert, taskUpsert: selectTaskUpsert
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => dispatch(toggleModalVisible("taskUpsert")), toggleModalVisible: () => dispatch(toggleModalVisible("taskUpsert")),
@@ -32,32 +28,22 @@ const mapDispatchToProps = (dispatch) => ({
dispatch(insertAuditTrail({ jobid, billid, operation, type })) dispatch(insertAuditTrail({ jobid, billid, operation, type }))
}); });
export function TaskUpsertModalContainer({ export function TaskUpsertModalContainer({ bodyshop, currentUser, taskUpsert, toggleModalVisible, insertAuditTrail }) {
bodyshop, const { t } = useTranslation();
currentUser,
taskUpsert,
toggleModalVisible,
insertAuditTrail
}) {
const {t} = useTranslation();
const history = useNavigate(); const history = useNavigate();
const [insertTask] = useMutation(MUTATION_INSERT_NEW_TASK); const [insertTask] = useMutation(MUTATION_INSERT_NEW_TASK);
const [updateTask] = useMutation(MUTATION_UPDATE_TASK); const [updateTask] = useMutation(MUTATION_UPDATE_TASK);
const {open, context, actions} = taskUpsert; const { open, context, actions } = taskUpsert;
const {jobid, joblineid, billid, partsorderid, taskId, existingTask} = context; const { jobid, joblineid, billid, partsorderid, taskId, existingTask } = context;
const {refetch} = actions; const { refetch } = actions;
const [form] = Form.useForm(); const [form] = Form.useForm();
const [selectedJobId, setSelectedJobId] = useState(null); const [selectedJobId, setSelectedJobId] = useState(null);
const [selectedJobDetails, setSelectedJobDetails] = useState(null); const [selectedJobDetails, setSelectedJobDetails] = useState(null);
const [jobIdState, setJobIdState] = useState(null); const [jobIdState, setJobIdState] = useState(null);
const { const { loading, error, data } = useQuery(QUERY_GET_TASKS_JOB_DETAILS_BY_ID, {
loading, variables: { id: jobIdState },
error, skip: !jobIdState
data
} = useQuery(QUERY_GET_TASKS_JOB_DETAILS_BY_ID, {
variables: {id: jobIdState},
skip: !jobIdState,
}); });
const { const {
@@ -65,8 +51,8 @@ export function TaskUpsertModalContainer({
error: taskError, error: taskError,
data: taskData data: taskData
} = useQuery(QUERY_GET_TASK_BY_ID, { } = useQuery(QUERY_GET_TASK_BY_ID, {
variables: {id: taskId}, variables: { id: taskId },
skip: !taskId, skip: !taskId
}); });
// Use Effect to hydrate existing task if only a taskid is provided // Use Effect to hydrate existing task if only a taskid is provided
@@ -100,25 +86,24 @@ export function TaskUpsertModalContainer({
form.setFieldsValue(existingTask); form.setFieldsValue(existingTask);
} else if (!existingTask && open) { } else if (!existingTask && open) {
form.resetFields(); form.resetFields();
if (joblineid) form.setFieldsValue({joblineid}); if (joblineid) form.setFieldsValue({ joblineid });
if (billid) form.setFieldsValue({billid}); if (billid) form.setFieldsValue({ billid });
if (partsorderid) form.setFieldsValue({partsorderid}); if (partsorderid) form.setFieldsValue({ partsorderid });
} }
return () => { return () => {
setSelectedJobId(null); setSelectedJobId(null);
}; };
}, [jobid, existingTask, form, open, joblineid, billid, partsorderid]); }, [jobid, existingTask, form, open, joblineid, billid, partsorderid]);
/** /**
* Remove the taskid from the URL * Remove the taskid from the URL
*/ */
const removeTaskIdFromUrl = () => { const removeTaskIdFromUrl = () => {
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
if (!urlParams.has('taskid')) return; if (!urlParams.has("taskid")) return;
urlParams.delete('taskid'); urlParams.delete("taskid");
history(`${window.location.pathname}?${urlParams}`); history(`${window.location.pathname}?${urlParams}`);
} };
/** /**
* Handle existing task * Handle existing task
@@ -132,48 +117,51 @@ export function TaskUpsertModalContainer({
variables: { variables: {
taskId: existingTask.id, taskId: existingTask.id,
task: replaceUndefinedWithNull(values) task: replaceUndefinedWithNull(values)
}, }
}); });
if (!taskData.errors) { if (!taskData.errors) {
const oldTask = taskData?.data?.update_tasks?.returning[0]; const oldTask = taskData?.data?.update_tasks?.returning[0];
insertAuditTrail({ insertAuditTrail({
jobid: oldTask.jobid, jobid: oldTask.jobid,
operation: AuditTrailMapping.tasksUpdated( operation: AuditTrailMapping.tasksUpdated(oldTask.title, currentUser.email),
oldTask.title,
currentUser.email
),
type: "tasksUpdated" type: "tasksUpdated"
}); });
} }
if (isAssignedToDirty) { if (isAssignedToDirty) {
// TODO This is being moved serverside // TODO This is being moved serverside
axios.post("/sendemail", { axios
from: { .post("/sendemail", {
name: bodyshop.shopname, from: {
address: bodyshop.email, name: bodyshop.shopname,
}, address: bodyshop.email
ReplyTo: { },
Email: 'noreply@imex.online' ReplyTo: {
}, Email: "noreply@imex.online"
to: values.assigned_to, },
subject: `A Task has been re-assigned to you on ${bodyshop.shopname} - ${values.title}`, to: values.assigned_to,
templateStrings: { subject: `A Task has been re-assigned to you on ${bodyshop.shopname} - ${values.title}`,
header: values.title, templateStrings: {
subHeader: `Assigned by ${currentUser.email} ${values.due_at ? `| Due on ${dayjs(values.due_at).format('MM/DD/YYYY')}` : ''}`, header: values.title,
body: `<a href="${window.location.protocol}//${window.location.host}/manage/tasks/alltasks?taskid=${existingTask.id}">Please sign in to your account to view the task details.</a>` subHeader: `Assigned by ${currentUser.email} ${values.due_at ? `| Due on ${dayjs(values.due_at).format("MM/DD/YYYY")}` : ""}`,
} body: `<a href="${window.location.protocol}//${window.location.host}/manage/tasks/alltasks?taskid=${existingTask.id}">Please sign in to your account to view the task details.</a>`
}).catch(e => console.error(`Something went wrong sending email to Assigned party on Task creation. ${e.message || ''}`)); }
})
.catch((e) =>
console.error(`Something went wrong sending email to Assigned party on Task creation. ${e.message || ""}`)
);
} }
window.dispatchEvent(new CustomEvent('taskUpdated', { window.dispatchEvent(
detail: {message: 'A task has been created or edited.'}, new CustomEvent("taskUpdated", {
})); detail: { message: "A task has been created or edited." }
})
);
notification["success"]({ notification["success"]({
message: t("tasks.successes.updated"), message: t("tasks.successes.updated")
}); });
if (refetch) await refetch(); if (refetch) await refetch();
@@ -189,21 +177,18 @@ export function TaskUpsertModalContainer({
...values, ...values,
created_by: currentUser.email, created_by: currentUser.email,
bodyshopid: bodyshop.id bodyshopid: bodyshop.id
}, }
], ]
}, }
}); });
const newTask = newTaskData?.data?.insert_tasks?.returning[0]; const newTask = newTaskData?.data?.insert_tasks?.returning[0];
const newTaskID = newTask?.id; const newTaskID = newTask?.id;
if (!newTaskData.errors) { if (!newTaskData.errors) {
insertAuditTrail({ insertAuditTrail({
jobid: newTask.jobid, jobid: newTask.jobid,
operation: AuditTrailMapping.tasksCreated( operation: AuditTrailMapping.tasksCreated(newTask.title, currentUser.email),
newTask.title,
currentUser.email
),
type: "tasksCreated" type: "tasksCreated"
}); });
} }
@@ -215,30 +200,36 @@ export function TaskUpsertModalContainer({
// send notification to the assigned user // send notification to the assigned user
// TODO: This is being moved serverside // TODO: This is being moved serverside
axios.post("/sendemail", { axios
from: { .post("/sendemail", {
name: bodyshop.shopname, from: {
address: bodyshop.email, name: bodyshop.shopname,
}, address: bodyshop.email
replyTo: { },
Email: 'noreply@imex.online' replyTo: {
}, Email: "noreply@imex.online"
to: values.assigned_to, },
subject: `A new Task has been assigned to you on ${bodyshop.shopname} - ${values.title}`, to: values.assigned_to,
templateName: 'taskAssigned', subject: `A new Task has been assigned to you on ${bodyshop.shopname} - ${values.title}`,
templateStrings: { templateName: "taskAssigned",
header: values.title, templateStrings: {
subHeader: `Assigned by ${currentUser.email} ${values.due_at ? `| Due on ${dayjs(values.due_at).format('MM/DD/YYYY')}` : ''}`, header: values.title,
body: `<a href="${window.location.protocol}//${window.location.host}/manage/tasks/alltasks?taskid=${newTaskID}">Please sign to your account to view the task details.</a>` subHeader: `Assigned by ${currentUser.email} ${values.due_at ? `| Due on ${dayjs(values.due_at).format("MM/DD/YYYY")}` : ""}`,
} body: `<a href="${window.location.protocol}//${window.location.host}/manage/tasks/alltasks?taskid=${newTaskID}">Please sign to your account to view the task details.</a>`
}).catch(e => console.error(`Something went wrong sending email to Assigned party on Task edit. ${e.message || ''}`)); }
})
.catch((e) =>
console.error(`Something went wrong sending email to Assigned party on Task edit. ${e.message || ""}`)
);
window.dispatchEvent(new CustomEvent('taskUpdated', { window.dispatchEvent(
detail: {message: 'A task has been created or edited.'}, new CustomEvent("taskUpdated", {
})); detail: { message: "A task has been created or edited." }
})
);
notification["success"]({ notification["success"]({
message: t("tasks.successes.created"), message: t("tasks.successes.created")
}); });
}; };
@@ -248,7 +239,7 @@ export function TaskUpsertModalContainer({
* @returns {Promise<[{jobid, bodyshopid, created_by},...*]>} * @returns {Promise<[{jobid, bodyshopid, created_by},...*]>}
*/ */
const handleFinish = async (formValues) => { const handleFinish = async (formValues) => {
const {...values} = formValues; const { ...values } = formValues;
if (existingTask) { if (existingTask) {
await handleExistingTask(values); await handleExistingTask(values);
} else { } else {
@@ -273,18 +264,18 @@ export function TaskUpsertModalContainer({
destroyOnClose destroyOnClose
> >
<Form form={form} onFinish={handleFinish} layout="vertical"> <Form form={form} onFinish={handleFinish} layout="vertical">
<TaskUpsertModalComponent form={form} loading={loading || (taskId && taskLoading)} <TaskUpsertModalComponent
error={error} data={data} form={form}
selectedJobId={selectedJobId} loading={loading || (taskId && taskLoading)}
setSelectedJobId={setSelectedJobId} error={error}
selectedJobDetails={selectedJobDetails}/> data={data}
selectedJobId={selectedJobId}
setSelectedJobId={setSelectedJobId}
selectedJobDetails={selectedJobDetails}
/>
</Form> </Form>
</Modal> </Modal>
); );
} }
export default connect( export default connect(mapStateToProps, mapDispatchToProps)(TaskUpsertModalContainer);
mapStateToProps,
mapDispatchToProps
)(TaskUpsertModalContainer);

View File

@@ -1,4 +1,4 @@
import {gql} from "@apollo/client"; import { gql } from "@apollo/client";
export const INSERT_NEW_BILL = gql` export const INSERT_NEW_BILL = gql`
mutation INSERT_NEW_BILL($bill: [bills_insert_input!]!) { mutation INSERT_NEW_BILL($bill: [bills_insert_input!]!) {

View File

@@ -1,4 +1,4 @@
import {gql} from "@apollo/client"; import { gql } from "@apollo/client";
export const QUERY_ALL_ACTIVE_JOBS_PAGINATED = gql` export const QUERY_ALL_ACTIVE_JOBS_PAGINATED = gql`
query QUERY_ALL_JOBS_PAGINATED_STATUS_FILTERED( query QUERY_ALL_JOBS_PAGINATED_STATUS_FILTERED(
@@ -1979,19 +1979,19 @@ export const QUERY_JOB_CLOSE_DETAILS = gql`
kmout kmout
qb_multiple_payers qb_multiple_payers
lbr_adjustments lbr_adjustments
payments { payments {
amount amount
created_at created_at
date date
exportedat exportedat
id id
jobid jobid
memo memo
payer payer
paymentnum paymentnum
transactionid transactionid
type type
} }
joblines(where: { removed: { _eq: false } }, order_by: { line_no: asc }) { joblines(where: { removed: { _eq: false } }, order_by: { line_no: asc }) {
id id
removed removed
@@ -2004,7 +2004,7 @@ export const QUERY_JOB_CLOSE_DETAILS = gql`
db_price db_price
act_price act_price
part_qty part_qty
notes notes
mod_lbr_ty mod_lbr_ty
db_hrs db_hrs
mod_lb_hrs mod_lb_hrs
@@ -2016,9 +2016,9 @@ export const QUERY_JOB_CLOSE_DETAILS = gql`
prt_dsmk_p prt_dsmk_p
convertedtolbr convertedtolbr
convertedtolbr_data convertedtolbr_data
act_price_before_ppc act_price_before_ppc
sublet_ignored sublet_ignored
sublet_completed sublet_completed
} }
} }
} }

View File

@@ -1,5 +1,4 @@
import {gql} from "@apollo/client"; import { gql } from "@apollo/client";
const PARTIAL_TASK_FIELDS = gql` const PARTIAL_TASK_FIELDS = gql`
fragment TaskFields on tasks { fragment TaskFields on tasks {
@@ -70,7 +69,8 @@ export const QUERY_GET_TASK_BY_ID = gql`
tasks_by_pk(id: $id) { tasks_by_pk(id: $id) {
...TaskFields ...TaskFields
} }
}`; }
`;
export const QUERY_ALL_TASKS_PAGINATED = gql` export const QUERY_ALL_TASKS_PAGINATED = gql`
${PARTIAL_TASK_FIELDS} ${PARTIAL_TASK_FIELDS}
@@ -88,20 +88,20 @@ export const QUERY_ALL_TASKS_PAGINATED = gql`
limit: $limit limit: $limit
order_by: $order order_by: $order
where: { where: {
bodyshopid: {_eq: $bodyshop}, bodyshopid: { _eq: $bodyshop }
deleted: {_eq: $deleted}, deleted: { _eq: $deleted }
assigned_to: {_eq: $assigned_to}, assigned_to: { _eq: $assigned_to }
completed: {_eq: $completed} completed: { _eq: $completed }
} }
) { ) {
...TaskFields ...TaskFields
} }
tasks_aggregate( tasks_aggregate(
where: { where: {
bodyshopid: {_eq: $bodyshop}, bodyshopid: { _eq: $bodyshop }
deleted: {_eq: $deleted}, deleted: { _eq: $deleted }
assigned_to: {_eq: $assigned_to}, assigned_to: { _eq: $assigned_to }
completed: {_eq: $completed} completed: { _eq: $completed }
} }
) { ) {
aggregate { aggregate {
@@ -129,22 +129,22 @@ export const QUERY_JOBLINE_TASKS_PAGINATED = gql`
limit: $limit limit: $limit
order_by: $order order_by: $order
where: { where: {
joblineid: {_eq: $joblineid}, joblineid: { _eq: $joblineid }
bodyshopid: {_eq: $bodyshop}, bodyshopid: { _eq: $bodyshop }
deleted: {_eq: $deleted}, deleted: { _eq: $deleted }
assigned_to: {_eq: $assigned_to}, assigned_to: { _eq: $assigned_to }
completed: {_eq: $completed} completed: { _eq: $completed }
} }
) { ) {
...TaskFields ...TaskFields
} }
tasks_aggregate( tasks_aggregate(
where: { where: {
joblineid: {_eq: $joblineid}, joblineid: { _eq: $joblineid }
bodyshopid: {_eq: $bodyshop}, bodyshopid: { _eq: $bodyshop }
deleted: {_eq: $deleted}, deleted: { _eq: $deleted }
assigned_to: {_eq: $assigned_to}, assigned_to: { _eq: $assigned_to }
completed: {_eq: $completed} completed: { _eq: $completed }
} }
) { ) {
aggregate { aggregate {
@@ -172,22 +172,22 @@ export const QUERY_PARTSORDER_TASKS_PAGINATED = gql`
limit: $limit limit: $limit
order_by: $order order_by: $order
where: { where: {
partsorderid: {_eq: $partsorderid}, partsorderid: { _eq: $partsorderid }
bodyshopid: {_eq: $bodyshop}, bodyshopid: { _eq: $bodyshop }
deleted: {_eq: $deleted}, deleted: { _eq: $deleted }
assigned_to: {_eq: $assigned_to}, assigned_to: { _eq: $assigned_to }
completed: {_eq: $completed} completed: { _eq: $completed }
} }
) { ) {
...TaskFields ...TaskFields
} }
tasks_aggregate( tasks_aggregate(
where: { where: {
partsorderid: {_eq: $partsorderid}, partsorderid: { _eq: $partsorderid }
bodyshopid: {_eq: $bodyshop}, bodyshopid: { _eq: $bodyshop }
deleted: {_eq: $deleted}, deleted: { _eq: $deleted }
assigned_to: {_eq: $assigned_to}, assigned_to: { _eq: $assigned_to }
completed: {_eq: $completed} completed: { _eq: $completed }
} }
) { ) {
aggregate { aggregate {
@@ -215,22 +215,22 @@ export const QUERY_BILL_TASKS_PAGINATED = gql`
limit: $limit limit: $limit
order_by: $order order_by: $order
where: { where: {
billid: {_eq: $billid}, billid: { _eq: $billid }
bodyshopid: {_eq: $bodyshop}, bodyshopid: { _eq: $bodyshop }
deleted: {_eq: $deleted}, deleted: { _eq: $deleted }
assigned_to: {_eq: $assigned_to}, assigned_to: { _eq: $assigned_to }
completed: {_eq: $completed} completed: { _eq: $completed }
} }
) { ) {
...TaskFields ...TaskFields
} }
tasks_aggregate( tasks_aggregate(
where: { where: {
billid: {_eq: $billid}, billid: { _eq: $billid }
bodyshopid: {_eq: $bodyshop}, bodyshopid: { _eq: $bodyshop }
deleted: {_eq: $deleted}, deleted: { _eq: $deleted }
assigned_to: {_eq: $assigned_to}, assigned_to: { _eq: $assigned_to }
completed: {_eq: $completed} completed: { _eq: $completed }
} }
) { ) {
aggregate { aggregate {
@@ -258,22 +258,22 @@ export const QUERY_JOB_TASKS_PAGINATED = gql`
limit: $limit limit: $limit
order_by: $order order_by: $order
where: { where: {
jobid: {_eq: $jobid}, jobid: { _eq: $jobid }
bodyshopid: {_eq: $bodyshop}, bodyshopid: { _eq: $bodyshop }
deleted: {_eq: $deleted}, deleted: { _eq: $deleted }
assigned_to: {_eq: $assigned_to}, assigned_to: { _eq: $assigned_to }
completed: {_eq: $completed} completed: { _eq: $completed }
} }
) { ) {
...TaskFields ...TaskFields
} }
tasks_aggregate( tasks_aggregate(
where: { where: {
jobid: {_eq: $jobid}, jobid: { _eq: $jobid }
bodyshopid: {_eq: $bodyshop}, bodyshopid: { _eq: $bodyshop }
deleted: {_eq: $deleted}, deleted: { _eq: $deleted }
assigned_to: {_eq: $assigned_to}, assigned_to: { _eq: $assigned_to }
completed: {_eq: $completed} completed: { _eq: $completed }
} }
) { ) {
aggregate { aggregate {
@@ -299,20 +299,20 @@ export const QUERY_MY_TASKS_PAGINATED = gql`
limit: $limit limit: $limit
order_by: $order order_by: $order
where: { where: {
assigned_to: {_eq: $user}, assigned_to: { _eq: $user }
bodyshopid: {_eq: $bodyshop}, bodyshopid: { _eq: $bodyshop }
deleted: {_eq: $deleted}, deleted: { _eq: $deleted }
completed: {_eq: $completed} completed: { _eq: $completed }
} }
) { ) {
...TaskFields ...TaskFields
} }
tasks_aggregate( tasks_aggregate(
where: { where: {
assigned_to: {_eq: $user}, assigned_to: { _eq: $user }
bodyshopid: {_eq: $bodyshop}, bodyshopid: { _eq: $bodyshop }
deleted: {_eq: $deleted}, deleted: { _eq: $deleted }
completed: {_eq: $completed} completed: { _eq: $completed }
} }
) { ) {
aggregate { aggregate {
@@ -328,7 +328,7 @@ export const QUERY_MY_TASKS_PAGINATED = gql`
*/ */
export const MUTATION_TOGGLE_TASK_COMPLETED = gql` export const MUTATION_TOGGLE_TASK_COMPLETED = gql`
mutation MUTATION_TOGGLE_TASK_COMPLETED($id: uuid!, $completed: Boolean!, $completed_at: timestamptz) { mutation MUTATION_TOGGLE_TASK_COMPLETED($id: uuid!, $completed: Boolean!, $completed_at: timestamptz) {
update_tasks_by_pk(pk_columns: {id: $id}, _set: {completed: $completed, completed_at: $completed_at}) { update_tasks_by_pk(pk_columns: { id: $id }, _set: { completed: $completed, completed_at: $completed_at }) {
id id
title title
completed completed
@@ -344,7 +344,7 @@ export const MUTATION_TOGGLE_TASK_COMPLETED = gql`
*/ */
export const MUTATION_TOGGLE_TASK_DELETED = gql` export const MUTATION_TOGGLE_TASK_DELETED = gql`
mutation MUTATION_TOGGLE_TASK_DELETED($id: uuid!, $deleted: Boolean!, $deleted_at: timestamptz) { mutation MUTATION_TOGGLE_TASK_DELETED($id: uuid!, $deleted: Boolean!, $deleted_at: timestamptz) {
update_tasks_by_pk(pk_columns: {id: $id}, _set: {deleted: $deleted, deleted_at: $deleted_at}) { update_tasks_by_pk(pk_columns: { id: $id }, _set: { deleted: $deleted, deleted_at: $deleted_at }) {
id id
title title
deleted deleted
@@ -359,7 +359,7 @@ export const MUTATION_TOGGLE_TASK_DELETED = gql`
* @type {DocumentNode} * @type {DocumentNode}
*/ */
export const MUTATION_INSERT_NEW_TASK = gql` export const MUTATION_INSERT_NEW_TASK = gql`
mutation MUTATION_INSERT_NEW_TASK($taskInput: [tasks_insert_input!]!) { mutation MUTATION_INSERT_NEW_TASK($taskInput: [tasks_insert_input!]!) {
insert_tasks(objects: $taskInput) { insert_tasks(objects: $taskInput) {
returning { returning {
id id
@@ -414,4 +414,4 @@ export const MUTATION_UPDATE_TASK = gql`
} }
} }
} }
` `;

View File

@@ -8,64 +8,53 @@ import Icon, {
SyncOutlined, SyncOutlined,
ToolFilled ToolFilled
} from "@ant-design/icons"; } from "@ant-design/icons";
import {Badge, Button, Divider, Form, notification, Space, Tabs} from "antd"; import { Badge, Button, Divider, Form, notification, Space, Tabs } from "antd";
import {PageHeader} from "@ant-design/pro-layout"; import { PageHeader } from "@ant-design/pro-layout";
import Axios from "axios"; import Axios from "axios";
import dayjs from "../../utils/day"; import dayjs from "../../utils/day";
import queryString from "query-string"; import queryString from "query-string";
import React, {useEffect, useState} from "react"; import React, { useEffect, useState } from "react";
import {useTranslation} from "react-i18next"; import { useTranslation } from "react-i18next";
import {FaHardHat, FaRegStickyNote, FaShieldAlt, FaTasks} from "react-icons/fa"; import { FaHardHat, FaRegStickyNote, FaShieldAlt, FaTasks } from "react-icons/fa";
import {connect} from "react-redux"; import { connect } from "react-redux";
import {useLocation, useNavigate} from "react-router-dom"; import { useLocation, useNavigate } from "react-router-dom";
import {createStructuredSelector} from "reselect"; import { createStructuredSelector } from "reselect";
import FormFieldsChanged import FormFieldsChanged from "../../components/form-fields-changed-alert/form-fields-changed-alert.component";
from "../../components/form-fields-changed-alert/form-fields-changed-alert.component";
import JobAuditTrail from "../../components/job-audit-trail/job-audit-trail.component"; import JobAuditTrail from "../../components/job-audit-trail/job-audit-trail.component";
import JobsLinesContainer from "../../components/job-detail-lines/job-lines.container"; import JobsLinesContainer from "../../components/job-detail-lines/job-lines.container";
import JobLifecycleComponent from "../../components/job-lifecycle/job-lifecycle.component"; import JobLifecycleComponent from "../../components/job-lifecycle/job-lifecycle.component";
import JobLineUpsertModalContainer import JobLineUpsertModalContainer from "../../components/job-lines-upsert-modal/job-lines-upsert-modal.container";
from "../../components/job-lines-upsert-modal/job-lines-upsert-modal.container"; import JobReconciliationModal from "../../components/job-reconciliation-modal/job-reconciliation.modal.container";
import JobReconciliationModal
from "../../components/job-reconciliation-modal/job-reconciliation.modal.container";
import JobSyncButton from "../../components/job-sync-button/job-sync-button.component"; import JobSyncButton from "../../components/job-sync-button/job-sync-button.component";
import JobsChangeStatus from "../../components/jobs-change-status/jobs-change-status.component"; import JobsChangeStatus from "../../components/jobs-change-status/jobs-change-status.component";
import JobsConvertButton from "../../components/jobs-convert-button/jobs-convert-button.component"; import JobsConvertButton from "../../components/jobs-convert-button/jobs-convert-button.component";
import JobsDetailDatesComponent import JobsDetailDatesComponent from "../../components/jobs-detail-dates/jobs-detail-dates.component";
from "../../components/jobs-detail-dates/jobs-detail-dates.component";
import JobsDetailGeneral from "../../components/jobs-detail-general/jobs-detail-general.component"; import JobsDetailGeneral from "../../components/jobs-detail-general/jobs-detail-general.component";
import JobsDetailHeaderActions import JobsDetailHeaderActions from "../../components/jobs-detail-header-actions/jobs-detail-header-actions.component";
from "../../components/jobs-detail-header-actions/jobs-detail-header-actions.component";
import JobsDetailHeader from "../../components/jobs-detail-header/jobs-detail-header.component"; import JobsDetailHeader from "../../components/jobs-detail-header/jobs-detail-header.component";
import JobsDetailLaborContainer import JobsDetailLaborContainer from "../../components/jobs-detail-labor/jobs-detail-labor.container";
from "../../components/jobs-detail-labor/jobs-detail-labor.container";
import JobsDetailPliContainer from "../../components/jobs-detail-pli/jobs-detail-pli.container"; import JobsDetailPliContainer from "../../components/jobs-detail-pli/jobs-detail-pli.container";
import JobsDetailRates from "../../components/jobs-detail-rates/jobs-detail-rates.component"; import JobsDetailRates from "../../components/jobs-detail-rates/jobs-detail-rates.component";
import JobsDetailTotals from "../../components/jobs-detail-totals/jobs-detail-totals.component"; import JobsDetailTotals from "../../components/jobs-detail-totals/jobs-detail-totals.component";
import JobsDocumentsGalleryContainer import JobsDocumentsGalleryContainer from "../../components/jobs-documents-gallery/jobs-documents-gallery.container";
from "../../components/jobs-documents-gallery/jobs-documents-gallery.container"; import JobsDocumentsLocalGallery from "../../components/jobs-documents-local-gallery/jobs-documents-local-gallery.container";
import JobsDocumentsLocalGallery
from "../../components/jobs-documents-local-gallery/jobs-documents-local-gallery.container";
import JobNotesContainer from "../../components/jobs-notes/jobs-notes.container"; import JobNotesContainer from "../../components/jobs-notes/jobs-notes.container";
import NoteUpsertModalComponent import NoteUpsertModalComponent from "../../components/note-upsert-modal/note-upsert-modal.container";
from "../../components/note-upsert-modal/note-upsert-modal.container"; import ScheduleJobModalContainer from "../../components/schedule-job-modal/schedule-job-modal.container";
import ScheduleJobModalContainer import { insertAuditTrail } from "../../redux/application/application.actions";
from "../../components/schedule-job-modal/schedule-job-modal.container"; import { selectJobReadOnly } from "../../redux/application/application.selectors";
import {insertAuditTrail} from "../../redux/application/application.actions"; import { setModalContext } from "../../redux/modals/modals.actions";
import {selectJobReadOnly} from "../../redux/application/application.selectors"; import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import {setModalContext} from "../../redux/modals/modals.actions";
import {selectBodyshop, selectCurrentUser} from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings"; import AuditTrailMapping from "../../utils/AuditTrailMappings";
import UndefinedToNull from "../../utils/undefinedtonull"; import UndefinedToNull from "../../utils/undefinedtonull";
import _ from "lodash"; import _ from "lodash";
import JobProfileDataWarning import JobProfileDataWarning from "../../components/job-profile-data-warning/job-profile-data-warning.component";
from "../../components/job-profile-data-warning/job-profile-data-warning.component"; import { DateTimeFormat } from "../../utils/DateFormatter";
import {DateTimeFormat} from "../../utils/DateFormatter";
import InstanceRenderManager from "../../utils/instanceRenderMgr"; import InstanceRenderManager from "../../utils/instanceRenderMgr";
import {HasFeatureAccess} from "../../components/feature-wrapper/feature-wrapper.component"; import { HasFeatureAccess } from "../../components/feature-wrapper/feature-wrapper.component";
import TaskListContainer from "../../components/task-list/task-list.container.jsx"; import TaskListContainer from "../../components/task-list/task-list.container.jsx";
import {QUERY_JOB_TASKS_PAGINATED} from "../../graphql/tasks.queries.js"; import { QUERY_JOB_TASKS_PAGINATED } from "../../graphql/tasks.queries.js";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -73,29 +62,35 @@ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser currentUser: selectCurrentUser
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
setPrintCenterContext: (context) => dispatch(setModalContext({ setPrintCenterContext: (context) =>
context: context, dispatch(
modal: "printCenter" setModalContext({
})), context: context,
insertAuditTrail: ({jobid, operation, type}) => dispatch(insertAuditTrail({ modal: "printCenter"
jobid, })
operation, ),
type insertAuditTrail: ({ jobid, operation, type }) =>
})) dispatch(
insertAuditTrail({
jobid,
operation,
type
})
)
}); });
export function JobsDetailPage({ export function JobsDetailPage({
bodyshop, bodyshop,
setPrintCenterContext, setPrintCenterContext,
jobRO, jobRO,
job, job,
mutationUpdateJob, mutationUpdateJob,
handleSubmit, handleSubmit,
currentUser, currentUser,
insertAuditTrail, insertAuditTrail,
refetch refetch
}) { }) {
const {t} = useTranslation(); const { t } = useTranslation();
const [form] = Form.useForm(); const [form] = Form.useForm();
const history = useNavigate(); const history = useNavigate();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -111,13 +106,13 @@ export function JobsDetailPage({
useEffect(() => { useEffect(() => {
const handleTaskUpdated = async (event) => { const handleTaskUpdated = async (event) => {
await refetch().catch(e => `Something went wrong fetching tasks: ${e.message || ''}`); await refetch().catch((e) => `Something went wrong fetching tasks: ${e.message || ""}`);
}; };
window.addEventListener('taskUpdated', handleTaskUpdated); window.addEventListener("taskUpdated", handleTaskUpdated);
// Clean up the event listener when the component is unmounted. // Clean up the event listener when the component is unmounted.
return () => { return () => {
window.removeEventListener('taskUpdated', handleTaskUpdated); window.removeEventListener("taskUpdated", handleTaskUpdated);
}; };
}, [refetch]); }, [refetch]);
@@ -156,7 +151,7 @@ export function JobsDetailPage({
}; };
return acc; return acc;
}, {}), }, {}),
cieca_pfo: {...job.cieca_pfo, ...values.cieca_pfo} cieca_pfo: { ...job.cieca_pfo, ...values.cieca_pfo }
} }
} }
}); });
@@ -251,15 +246,15 @@ export function JobsDetailPage({
}} }}
key="refresh" key="refresh"
> >
<SyncOutlined/> <SyncOutlined />
{t("general.labels.refresh")} {t("general.labels.refresh")}
</Button> </Button>
<JobsChangeStatus job={job}/> <JobsChangeStatus job={job} />
<JobSyncButton job={job}/> <JobSyncButton job={job} />
<Button <Button
onClick={() => { onClick={() => {
setPrintCenterContext({ setPrintCenterContext({
actions: {refetch: refetch}, actions: { refetch: refetch },
context: { context: {
id: job.id, id: job.id,
job: job, job: job,
@@ -269,12 +264,11 @@ export function JobsDetailPage({
}} }}
key="printing" key="printing"
> >
<PrinterFilled/> <PrinterFilled />
{t("jobs.actions.printCenter")} {t("jobs.actions.printCenter")}
</Button> </Button>
<JobsConvertButton job={job} refetch={refetch} <JobsConvertButton job={job} refetch={refetch} parentFormIsFieldsTouched={form.isFieldsTouched} />
parentFormIsFieldsTouched={form.isFieldsTouched}/> <JobsDetailHeaderActions key="actions" job={job} refetch={refetch} />
<JobsDetailHeaderActions key="actions" job={job} refetch={refetch}/>
<Button type="primary" loading={loading} disabled={jobRO} onClick={() => form.submit()}> <Button type="primary" loading={loading} disabled={jobRO} onClick={() => form.submit()}>
{t("general.actions.save")} {t("general.actions.save")}
</Button> </Button>
@@ -283,10 +277,10 @@ export function JobsDetailPage({
return ( return (
<div> <div>
<ScheduleJobModalContainer/> <ScheduleJobModalContainer />
<JobReconciliationModal/> <JobReconciliationModal />
<JobLineUpsertModalContainer/> <JobLineUpsertModalContainer />
<NoteUpsertModalComponent/> <NoteUpsertModalComponent />
<Form <Form
form={form} form={form}
name="JobDetailForm" name="JobDetailForm"
@@ -300,120 +294,128 @@ export function JobsDetailPage({
title={job.ro_number || t("general.labels.na")} title={job.ro_number || t("general.labels.na")}
extra={menuExtra} extra={menuExtra}
/> />
<JobsDetailHeader job={job}/> <JobsDetailHeader job={job} />
<Divider type="horizontal"/> <Divider type="horizontal" />
<JobProfileDataWarning job={job}/> <JobProfileDataWarning job={job} />
<FormFieldsChanged form={form}/> <FormFieldsChanged form={form} />
<Tabs <Tabs
defaultActiveKey={search.tab} defaultActiveKey={search.tab}
onChange={(key) => history({search: `?tab=${key}`})} onChange={(key) => history({ search: `?tab=${key}` })}
tabBarStyle={{fontWeight: "bold", borderBottom: "10px"}} tabBarStyle={{ fontWeight: "bold", borderBottom: "10px" }}
items={[ items={[
{ {
key: "general", key: "general",
icon: <Icon component={FaShieldAlt}/>, icon: <Icon component={FaShieldAlt} />,
label: t("menus.jobsdetail.general"), label: t("menus.jobsdetail.general"),
forceRender: true, forceRender: true,
children: <JobsDetailGeneral job={job} form={form}/> children: <JobsDetailGeneral job={job} form={form} />
}, },
{ {
key: "repairdata", key: "repairdata",
icon: <BarsOutlined/>, icon: <BarsOutlined />,
label: t("menus.jobsdetail.repairdata"), label: t("menus.jobsdetail.repairdata"),
forceRender: true, forceRender: true,
children: <JobsLinesContainer job={job} joblines={job.joblines} refetch={refetch} children: <JobsLinesContainer job={job} joblines={job.joblines} refetch={refetch} form={form} />
form={form}/>
}, },
{ {
key: "rates", key: "rates",
icon: <DollarCircleOutlined/>, icon: <DollarCircleOutlined />,
label: t("menus.jobsdetail.rates"), label: t("menus.jobsdetail.rates"),
forceRender: true, forceRender: true,
children: <JobsDetailRates job={job} form={form}/> children: <JobsDetailRates job={job} form={form} />
}, },
{ {
key: "totals", key: "totals",
icon: <DollarCircleOutlined/>, icon: <DollarCircleOutlined />,
label: t("menus.jobsdetail.totals"), label: t("menus.jobsdetail.totals"),
children: <JobsDetailTotals job={job} refetch={refetch}/> children: <JobsDetailTotals job={job} refetch={refetch} />
}, },
{ {
key: "partssublet", key: "partssublet",
icon: <ToolFilled/>, icon: <ToolFilled />,
label: HasFeatureAccess({featureName: "bills", bodyshop}) label: HasFeatureAccess({ featureName: "bills", bodyshop })
? t("menus.jobsdetail.partssublet") ? t("menus.jobsdetail.partssublet")
: t("menus.jobsdetail.parts"), : t("menus.jobsdetail.parts"),
children: <JobsDetailPliContainer job={job}/> children: <JobsDetailPliContainer job={job} />
}, },
...(InstanceRenderManager({ ...(InstanceRenderManager({
imex: true, imex: true,
rome: true, rome: true,
promanager: HasFeatureAccess({featureName: "timetickets", bodyshop}) promanager: HasFeatureAccess({ featureName: "timetickets", bodyshop })
}) })
? [ ? [
{ {
key: "labor", key: "labor",
icon: <Icon component={FaHardHat}/>, icon: <Icon component={FaHardHat} />,
label: t("menus.jobsdetail.labor"), label: t("menus.jobsdetail.labor"),
children: <JobsDetailLaborContainer job={job} jobId={job.id}/> children: <JobsDetailLaborContainer job={job} jobId={job.id} />
} }
] ]
: []), : []),
{ {
key: "lifecycle", key: "lifecycle",
icon: <BarsOutlined/>, icon: <BarsOutlined />,
label: t("menus.jobsdetail.lifecycle"), label: t("menus.jobsdetail.lifecycle"),
children: <JobLifecycleComponent job={job} statuses={bodyshop.md_ro_statuses}/> children: <JobLifecycleComponent job={job} statuses={bodyshop.md_ro_statuses} />
}, },
{ {
key: "dates", key: "dates",
icon: <CalendarFilled/>, icon: <CalendarFilled />,
label: t("menus.jobsdetail.dates"), label: t("menus.jobsdetail.dates"),
forceRender: true, forceRender: true,
children: <JobsDetailDatesComponent job={job}/> children: <JobsDetailDatesComponent job={job} />
}, },
...(InstanceRenderManager({ ...(InstanceRenderManager({
imex: true, imex: true,
rome: true, rome: true,
promanager: HasFeatureAccess({featureName: "media", bodyshop}) promanager: HasFeatureAccess({ featureName: "media", bodyshop })
}) })
? [ ? [
{ {
key: "documents", key: "documents",
icon: <FileImageFilled/>, icon: <FileImageFilled />,
label: t("jobs.labels.documents"), label: t("jobs.labels.documents"),
children: bodyshop.uselocalmediaserver ? ( children: bodyshop.uselocalmediaserver ? (
<JobsDocumentsLocalGallery job={job}/> <JobsDocumentsLocalGallery job={job} />
) : ( ) : (
<JobsDocumentsGalleryContainer jobId={job.id}/> <JobsDocumentsGalleryContainer jobId={job.id} />
) )
} }
] ]
: []), : []),
{ {
key: "notes", key: "notes",
icon: <Icon component={FaRegStickyNote}/>, icon: <Icon component={FaRegStickyNote} />,
label: t("jobs.labels.notes"), label: t("jobs.labels.notes"),
children: <JobNotesContainer jobId={job.id}/> children: <JobNotesContainer jobId={job.id} />
}, },
{ {
key: "audit", key: "audit",
icon: <HistoryOutlined/>, icon: <HistoryOutlined />,
label: t("jobs.labels.audit"), label: t("jobs.labels.audit"),
children: <JobAuditTrail jobId={job.id}/> children: <JobAuditTrail jobId={job.id} />
}, },
{ {
key: 'tasks', key: "tasks",
icon: <FaTasks/>, icon: <FaTasks />,
label: <Space direction='horizontal'> label: (
{t("jobs.labels.tasks")}{job.tasks_aggregate.aggregate.count > 0 && <Space direction="horizontal">
<Badge count={job.tasks_aggregate.aggregate.count}/>} {t("jobs.labels.tasks")}
</Space>, {job.tasks_aggregate.aggregate.count > 0 && <Badge count={job.tasks_aggregate.aggregate.count} />}
children: <TaskListContainer currentUser={currentUser} bodyshop={bodyshop} </Space>
relationshipType={'jobid'} relationshipId={job.id} ),
query={QUERY_JOB_TASKS_PAGINATED} children: (
titleTranslation='tasks.titles.job_tasks' showRo={false}/> <TaskListContainer
}, currentUser={currentUser}
bodyshop={bodyshop}
relationshipType={"jobid"}
relationshipId={job.id}
query={QUERY_JOB_TASKS_PAGINATED}
titleTranslation="tasks.titles.job_tasks"
showRo={false}
/>
)
}
]} ]}
/> />
</Form> </Form>
@@ -424,7 +426,7 @@ export function JobsDetailPage({
export default connect(mapStateToProps, mapDispatchToProps)(JobsDetailPage); export default connect(mapStateToProps, mapDispatchToProps)(JobsDetailPage);
const transformJobToForm = (job) => { const transformJobToForm = (job) => {
const transformedJob = {...job}; const transformedJob = { ...job };
transformedJob.parts_tax_rates = Object.keys(transformedJob.parts_tax_rates).reduce((acc, parttype) => { transformedJob.parts_tax_rates = Object.keys(transformedJob.parts_tax_rates).reduce((acc, parttype) => {
acc[parttype] = Object.keys(transformedJob.parts_tax_rates[parttype]).reduce((innerAcc, key) => { acc[parttype] = Object.keys(transformedJob.parts_tax_rates[parttype]).reduce((innerAcc, key) => {

View File

@@ -1,10 +1,10 @@
import {Button, Collapse, FloatButton, Layout, Space, Spin, Tag} from "antd"; import { Button, Collapse, FloatButton, Layout, Space, Spin, Tag } from "antd";
// import preval from "preval.macro"; // import preval from "preval.macro";
import React, {lazy, Suspense, useEffect, useState} from "react"; import React, { lazy, Suspense, useEffect, useState } from "react";
import {useTranslation} from "react-i18next"; import { useTranslation } from "react-i18next";
import {connect} from "react-redux"; import { connect } from "react-redux";
import {Link, Route, Routes} from "react-router-dom"; import { Link, Route, Routes } from "react-router-dom";
import {createStructuredSelector} from "reselect"; import { createStructuredSelector } from "reselect";
import BreadCrumbs from "../../components/breadcrumbs/breadcrumbs.component"; import BreadCrumbs from "../../components/breadcrumbs/breadcrumbs.component";
import ChatAffixContainer from "../../components/chat-affix/chat-affix.container"; import ChatAffixContainer from "../../components/chat-affix/chat-affix.container";
import ConflictComponent from "../../components/conflict/conflict.component"; import ConflictComponent from "../../components/conflict/conflict.component";
@@ -17,26 +17,21 @@ import TestComponent from "../../components/_test/test.page";
import HeaderContainer from "../../components/header/header.container"; import HeaderContainer from "../../components/header/header.container";
import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component"; import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component";
import PartnerPingComponent from "../../components/partner-ping/partner-ping.component"; import PartnerPingComponent from "../../components/partner-ping/partner-ping.component";
import PrintCenterModalContainer import PrintCenterModalContainer from "../../components/print-center-modal/print-center-modal.container";
from "../../components/print-center-modal/print-center-modal.container";
import ShopSubStatusComponent from "../../components/shop-sub-status/shop-sub-status.component"; import ShopSubStatusComponent from "../../components/shop-sub-status/shop-sub-status.component";
import {requestForToken} from "../../firebase/firebase.utils"; import { requestForToken } from "../../firebase/firebase.utils";
import {selectBodyshop, selectInstanceConflict} from "../../redux/user/user.selectors"; import { selectBodyshop, selectInstanceConflict } from "../../redux/user/user.selectors";
import UpdateAlert from "../../components/update-alert/update-alert.component"; import UpdateAlert from "../../components/update-alert/update-alert.component";
import {setJoyRideFinished} from "../../redux/application/application.actions.js"; import { setJoyRideFinished } from "../../redux/application/application.actions.js";
import { import { selectEnableJoyRide, selectJoyRideSteps } from "../../redux/application/application.selectors.js";
selectEnableJoyRide,
selectJoyRideSteps
} from "../../redux/application/application.selectors.js";
import InstanceRenderManager from "../../utils/instanceRenderMgr.js"; import InstanceRenderManager from "../../utils/instanceRenderMgr.js";
import "./manage.page.styles.scss"; import "./manage.page.styles.scss";
const JobsPage = lazy(() => import("../jobs/jobs.page")); const JobsPage = lazy(() => import("../jobs/jobs.page"));
const CardPaymentModalContainer = lazy(() => const CardPaymentModalContainer = lazy(
import("../../components/card-payment-modal/card-payment-modal.container.") () => import("../../components/card-payment-modal/card-payment-modal.container.")
); );
const JobsDetailPage = lazy(() => import("../jobs-detail/jobs-detail.page.container")); const JobsDetailPage = lazy(() => import("../jobs-detail/jobs-detail.page.container"));
@@ -64,8 +59,8 @@ const JobCostingModal = lazy(() => import("../../components/job-costing-modal/jo
const ReportCenterModal = lazy(() => import("../../components/report-center-modal/report-center-modal.container")); const ReportCenterModal = lazy(() => import("../../components/report-center-modal/report-center-modal.container"));
const BillEnterModalContainer = lazy(() => import("../../components/bill-enter-modal/bill-enter-modal.container")); const BillEnterModalContainer = lazy(() => import("../../components/bill-enter-modal/bill-enter-modal.container"));
const TimeTicketModalContainer = lazy(() => import("../../components/time-ticket-modal/time-ticket-modal.container")); const TimeTicketModalContainer = lazy(() => import("../../components/time-ticket-modal/time-ticket-modal.container"));
const TimeTicketModalTask = lazy(() => const TimeTicketModalTask = lazy(
import("../../components/time-ticket-task-modal/time-ticket-task-modal.container") () => import("../../components/time-ticket-task-modal/time-ticket-task-modal.container")
); );
const PaymentModalContainer = lazy(() => import("../../components/payment-modal/payment-modal.container")); const PaymentModalContainer = lazy(() => import("../../components/payment-modal/payment-modal.container"));
const ProductionListPage = lazy(() => import("../production-list/production-list.container")); const ProductionListPage = lazy(() => import("../production-list/production-list.container"));
@@ -106,7 +101,7 @@ const MyTasksPage = lazy(() => import("../tasks/myTasksPageContainer.jsx"));
const AllTasksPage = lazy(() => import("../tasks/allTasksPageContainer.jsx")); const AllTasksPage = lazy(() => import("../tasks/allTasksPageContainer.jsx"));
const TaskUpsertModalContainer = lazy(() => import("../../components/task-upsert-modal/task-upsert-modal.container")); const TaskUpsertModalContainer = lazy(() => import("../../components/task-upsert-modal/task-upsert-modal.container"));
const {Content, Footer} = Layout; const { Content, Footer } = Layout;
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
conflict: selectInstanceConflict, conflict: selectInstanceConflict,
@@ -119,8 +114,8 @@ const mapDispatchToProps = (dispatch) => ({
setJoyRideFinished: (steps) => dispatch(setJoyRideFinished(steps)) setJoyRideFinished: (steps) => dispatch(setJoyRideFinished(steps))
}); });
export function Manage({conflict, bodyshop, enableJoyRide, joyRideSteps, setJoyRideFinished}) { export function Manage({ conflict, bodyshop, enableJoyRide, joyRideSteps, setJoyRideFinished }) {
const {t} = useTranslation(); const { t } = useTranslation();
const [chatVisible] = useState(false); const [chatVisible] = useState(false);
const [tours, setTours] = useState([]); const [tours, setTours] = useState([]);
@@ -156,264 +151,266 @@ export function Manage({conflict, bodyshop, enableJoyRide, joyRideSteps, setJoyR
/> />
} }
> >
<PaymentModalContainer/> <PaymentModalContainer />
<CardPaymentModalContainer/> <CardPaymentModalContainer />
<TaskUpsertModalContainer/> <TaskUpsertModalContainer />
<BreadCrumbs/> <BreadCrumbs />
<BillEnterModalContainer/> <BillEnterModalContainer />
<JobCostingModal/> <JobCostingModal />
<ReportCenterModal/> <ReportCenterModal />
<EmailOverlayContainer/> <EmailOverlayContainer />
<TimeTicketModalContainer/> <TimeTicketModalContainer />
<TimeTicketModalTask/> <TimeTicketModalTask />
<PrintCenterModalContainer/> <PrintCenterModalContainer />
<Routes> <Routes>
<Route path="/_test" element={<TestComponent/>}/> <Route path="/_test" element={<TestComponent />} />
<Route path="/" element={<ManageRootPage/>}/> <Route path="/" element={<ManageRootPage />} />
<Route <Route
path="/jobs" path="/jobs"
element={ element={
<Suspense fallback={<Spin/>}> <Suspense fallback={<Spin />}>
<JobsPage/> <JobsPage />
</Suspense> </Suspense>
} }
/> />
<Route <Route
path="/jobs/:jobId/intake" path="/jobs/:jobId/intake"
element={ element={
<Suspense fallback={<Spin/>}> <Suspense fallback={<Spin />}>
<JobIntake/> <JobIntake />
</Suspense> </Suspense>
} }
/> />
<Route <Route
path="/jobs/:jobId/deliver" path="/jobs/:jobId/deliver"
element={ element={
<Suspense fallback={<Spin/>}> <Suspense fallback={<Spin />}>
<JobDeliver/> <JobDeliver />
</Suspense> </Suspense>
} }
/> />
<Route <Route
path="/jobs/:jobId/checklist" path="/jobs/:jobId/checklist"
element={ element={
<Suspense fallback={<Spin/>}> <Suspense fallback={<Spin />}>
<JobChecklistView/> <JobChecklistView />
</Suspense> </Suspense>
} }
/> />
<Route <Route
path="/jobs/:jobId/close" path="/jobs/:jobId/close"
element={ element={
<Suspense fallback={<Spin/>}> <Suspense fallback={<Spin />}>
<JobsClose/> <JobsClose />
</Suspense> </Suspense>
} }
/> />
<Route <Route
path="/jobs/:jobId/admin" path="/jobs/:jobId/admin"
element={ element={
<Suspense fallback={<Spin/>}> <Suspense fallback={<Spin />}>
<JobsAdmin/> <JobsAdmin />
</Suspense> </Suspense>
} }
/> />
<Route <Route
path="/jobs/all" path="/jobs/all"
element={ element={
<Suspense fallback={<Spin/>}> <Suspense fallback={<Spin />}>
<AllJobs/> <AllJobs />
</Suspense> </Suspense>
} }
/> />
<Route <Route
path="/jobs/ready" path="/jobs/ready"
element={ element={
<Suspense fallback={<Spin/>}> <Suspense fallback={<Spin />}>
<ReadyJobs/> <ReadyJobs />
</Suspense> </Suspense>
} }
/> />
<Route <Route
path="/jobs/new" path="/jobs/new"
element={ element={
<Suspense fallback={<Spin/>}> <Suspense fallback={<Spin />}>
<JobsCreateContainerPage/> <JobsCreateContainerPage />
</Suspense> </Suspense>
} }
/> />
<Route <Route
path="/jobs/:jobId" path="/jobs/:jobId"
element={ element={
<Suspense fallback={<Spin/>}> <Suspense fallback={<Spin />}>
<JobsDetailPage/> <JobsDetailPage />
</Suspense> </Suspense>
} }
/> />
<Route <Route
path="/temporarydocs/" path="/temporarydocs/"
element={ element={
<Suspense fallback={<Spin/>}> <Suspense fallback={<Spin />}>
<TempDocs/> <TempDocs />
</Suspense> </Suspense>
} }
/> />
<Route <Route
path='/tasks/mytasks' path="/tasks/mytasks"
element={ element={
<Suspense fallback={<Spin/>}> <Suspense fallback={<Spin />}>
<MyTasksPage/> <MyTasksPage />
</Suspense>} </Suspense>
}
/> />
<Route <Route
path='/tasks/alltasks' path="/tasks/alltasks"
element={ element={
<Suspense fallback={<Spin/>}> <Suspense fallback={<Spin />}>
<AllTasksPage/> <AllTasksPage />
</Suspense>} </Suspense>
}
/> />
<Route <Route
path="/inventory/" path="/inventory/"
element={ element={
<Suspense fallback={<Spin/>}> <Suspense fallback={<Spin />}>
<InventoryListPage/> <InventoryListPage />
</Suspense> </Suspense>
} }
/> />
<Route <Route
path="/courtesycars/" path="/courtesycars/"
element={ element={
<Suspense fallback={<Spin/>}> <Suspense fallback={<Spin />}>
<CourtesyCarsPage/> <CourtesyCarsPage />
</Suspense> </Suspense>
} }
/> />
<Route <Route
path="/courtesycars/new" path="/courtesycars/new"
element={ element={
<Suspense fallback={<Spin/>}> <Suspense fallback={<Spin />}>
<CourtesyCarCreateContainer/> <CourtesyCarCreateContainer />
</Suspense> </Suspense>
} }
/> />
<Route <Route
path="/courtesycars/contracts" path="/courtesycars/contracts"
element={ element={
<Suspense fallback={<Spin/>}> <Suspense fallback={<Spin />}>
<ContractsList/> <ContractsList />
</Suspense> </Suspense>
} }
/> />
<Route <Route
path="/courtesycars/contracts/new" path="/courtesycars/contracts/new"
element={ element={
<Suspense fallback={<Spin/>}> <Suspense fallback={<Spin />}>
<ContractCreatePage/> <ContractCreatePage />
</Suspense> </Suspense>
} }
/> />
<Route <Route
path="/courtesycars/contracts/:contractId" path="/courtesycars/contracts/:contractId"
element={ element={
<Suspense fallback={<Spin/>}> <Suspense fallback={<Spin />}>
<ContractDetailPage/> <ContractDetailPage />
</Suspense> </Suspense>
} }
/> />
<Route <Route
path="/courtesycars/:ccId" path="/courtesycars/:ccId"
element={ element={
<Suspense fallback={<Spin/>}> <Suspense fallback={<Spin />}>
<CourtesyCarDetailContainer/> <CourtesyCarDetailContainer />
</Suspense> </Suspense>
} }
/> />
<Route <Route
path="/profile" path="/profile"
element={ element={
<Suspense fallback={<Spin/>}> <Suspense fallback={<Spin />}>
<ProfilePage/> <ProfilePage />
</Suspense> </Suspense>
} }
/> />
<Route <Route
path="/vehicles" path="/vehicles"
element={ element={
<Suspense fallback={<Spin/>}> <Suspense fallback={<Spin />}>
<VehiclesContainer/> <VehiclesContainer />
</Suspense> </Suspense>
} }
/> />
<Route <Route
path="/production/list" path="/production/list"
element={ element={
<Suspense fallback={<Spin/>}> <Suspense fallback={<Spin />}>
<ProductionListPage/> <ProductionListPage />
</Suspense> </Suspense>
} }
/> />
<Route <Route
path="/production/board" path="/production/board"
element={ element={
<Suspense fallback={<Spin/>}> <Suspense fallback={<Spin />}>
<ProductionBoardPage/> <ProductionBoardPage />
</Suspense> </Suspense>
} }
/> />
<Route <Route
path="/vehicles/:vehId" path="/vehicles/:vehId"
element={ element={
<Suspense fallback={<Spin/>}> <Suspense fallback={<Spin />}>
<VehiclesDetailContainer/> <VehiclesDetailContainer />
</Suspense> </Suspense>
} }
/> />
<Route <Route
path="/bills" path="/bills"
element={ element={
<Suspense fallback={<Spin/>}> <Suspense fallback={<Spin />}>
<BillsListPage/> <BillsListPage />
</Suspense> </Suspense>
} }
/> />
<Route <Route
path="/owners" path="/owners"
element={ element={
<Suspense fallback={<Spin/>}> <Suspense fallback={<Spin />}>
<OwnersContainer/> <OwnersContainer />
</Suspense> </Suspense>
} }
/> />
<Route <Route
path="/owners/:ownerId" path="/owners/:ownerId"
element={ element={
<Suspense fallback={<Spin/>}> <Suspense fallback={<Spin />}>
<OwnersDetailContainer/> <OwnersDetailContainer />
</Suspense> </Suspense>
} }
/> />
<Route <Route
path="/schedule" path="/schedule"
element={ element={
<Suspense fallback={<Spin/>}> <Suspense fallback={<Spin />}>
<ScheduleContainer/> <ScheduleContainer />
</Suspense> </Suspense>
} }
/> />
<Route <Route
path="/available" path="/available"
element={ element={
<Suspense fallback={<Spin/>}> <Suspense fallback={<Spin />}>
<JobsAvailablePage/> <JobsAvailablePage />
</Suspense> </Suspense>
} }
/> />
<Route <Route
path="/shop" path="/shop"
element={ element={
<Suspense fallback={<Spin/>}> <Suspense fallback={<Spin />}>
<ShopPage/> <ShopPage />
</Suspense> </Suspense>
} }
/> />
@@ -426,16 +423,16 @@ export function Manage({conflict, bodyshop, enableJoyRide, joyRideSteps, setJoyR
<Route <Route
path="/shop/vendors" path="/shop/vendors"
element={ element={
<Suspense fallback={<Spin/>}> <Suspense fallback={<Spin />}>
<ShopVendorPageContainer/> <ShopVendorPageContainer />
</Suspense> </Suspense>
} }
/> />
<Route <Route
path="/shop/csi" path="/shop/csi"
element={ element={
<Suspense fallback={<Spin/>}> <Suspense fallback={<Spin />}>
<ShopCsiPageContainer/> <ShopCsiPageContainer />
</Suspense> </Suspense>
} }
/> />
@@ -443,8 +440,8 @@ export function Manage({conflict, bodyshop, enableJoyRide, joyRideSteps, setJoyR
<Route <Route
path="/accounting/qbo" path="/accounting/qbo"
element={ element={
<Suspense fallback={<Spin/>}> <Suspense fallback={<Spin />}>
<AccountingQboCallback/> <AccountingQboCallback />
</Suspense> </Suspense>
} }
/> />
@@ -452,56 +449,56 @@ export function Manage({conflict, bodyshop, enableJoyRide, joyRideSteps, setJoyR
<Route <Route
path="/accounting/receivables" path="/accounting/receivables"
element={ element={
<Suspense fallback={<Spin/>}> <Suspense fallback={<Spin />}>
<AccountingReceivables/> <AccountingReceivables />
</Suspense> </Suspense>
} }
/> />
<Route <Route
path="/accounting/payables" path="/accounting/payables"
element={ element={
<Suspense fallback={<Spin/>}> <Suspense fallback={<Spin />}>
<AccountingPayables/> <AccountingPayables />
</Suspense> </Suspense>
} }
/> />
<Route <Route
path="/accounting/payments" path="/accounting/payments"
element={ element={
<Suspense fallback={<Spin/>}> <Suspense fallback={<Spin />}>
<AccountingPayments/> <AccountingPayments />
</Suspense> </Suspense>
} }
/> />
<Route <Route
path="/accounting/exportlogs" path="/accounting/exportlogs"
element={ element={
<Suspense fallback={<Spin/>}> <Suspense fallback={<Spin />}>
<ExportLogs/> <ExportLogs />
</Suspense> </Suspense>
} }
/> />
<Route <Route
path="/ttapprovals" path="/ttapprovals"
element={ element={
<Suspense fallback={<Spin/>}> <Suspense fallback={<Spin />}>
<TtApprovals/> <TtApprovals />
</Suspense> </Suspense>
} }
/> />
<Route <Route
path="/partsqueue" path="/partsqueue"
element={ element={
<Suspense fallback={<Spin/>}> <Suspense fallback={<Spin />}>
<PartsQueue/> <PartsQueue />
</Suspense> </Suspense>
} }
/> />
<Route <Route
path="/phonebook" path="/phonebook"
element={ element={
<Suspense fallback={<Spin/>}> <Suspense fallback={<Spin />}>
<Phonebook/> <Phonebook />
</Suspense> </Suspense>
} }
/> />
@@ -509,65 +506,65 @@ export function Manage({conflict, bodyshop, enableJoyRide, joyRideSteps, setJoyR
<Route <Route
path="/payments" path="/payments"
element={ element={
<Suspense fallback={<Spin/>}> <Suspense fallback={<Spin />}>
<PaymentsAll/> <PaymentsAll />
</Suspense> </Suspense>
} }
/> />
<Route <Route
path="/shiftclock" path="/shiftclock"
element={ element={
<Suspense fallback={<Spin/>}> <Suspense fallback={<Spin />}>
<ShiftClock/> <ShiftClock />
</Suspense> </Suspense>
} }
/> />
<Route <Route
path="/scoreboard" path="/scoreboard"
element={ element={
<Suspense fallback={<Spin/>}> <Suspense fallback={<Spin />}>
<Scoreboard/> <Scoreboard />
</Suspense> </Suspense>
} }
/> />
<Route <Route
path="/timetickets" path="/timetickets"
element={ element={
<Suspense fallback={<Spin/>}> <Suspense fallback={<Spin />}>
<TimeTicketsAll/> <TimeTicketsAll />
</Suspense> </Suspense>
} }
/> />
<Route <Route
path="/help" path="/help"
element={ element={
<Suspense fallback={<Spin/>}> <Suspense fallback={<Spin />}>
<Help/> <Help />
</Suspense> </Suspense>
} }
/> />
<Route path="/emailtest" element={<EmailTest/>}/> <Route path="/emailtest" element={<EmailTest />} />
<Route <Route
path="/dashboard" path="/dashboard"
element={ element={
<Suspense fallback={<Spin/>}> <Suspense fallback={<Spin />}>
<Dashboard/> <Dashboard />
</Suspense> </Suspense>
} }
/> />
<Route <Route
path="/dms" path="/dms"
element={ element={
<Suspense fallback={<Spin/>}> <Suspense fallback={<Spin />}>
<Dms/> <Dms />
</Suspense> </Suspense>
} }
/> />
<Route <Route
path="/dmsap" path="/dmsap"
element={ element={
<Suspense fallback={<Spin/>}> <Suspense fallback={<Spin />}>
<DmsPayables/> <DmsPayables />
</Suspense> </Suspense>
} }
/> />
@@ -577,16 +574,16 @@ export function Manage({conflict, bodyshop, enableJoyRide, joyRideSteps, setJoyR
let PageContent; let PageContent;
if (conflict) PageContent = <ConflictComponent/>; if (conflict) PageContent = <ConflictComponent />;
else if (bodyshop && bodyshop.sub_status !== "active") PageContent = <ShopSubStatusComponent/>; else if (bodyshop && bodyshop.sub_status !== "active") PageContent = <ShopSubStatusComponent />;
else PageContent = AppRouteTable; else PageContent = AppRouteTable;
return ( return (
<> <>
<ChatAffixContainer bodyshop={bodyshop} chatVisible={chatVisible}/> <ChatAffixContainer bodyshop={bodyshop} chatVisible={chatVisible} />
<Layout style={{minHeight: "100vh"}} className="layout-container"> <Layout style={{ minHeight: "100vh" }} className="layout-container">
<UpdateAlert/> <UpdateAlert />
<HeaderContainer/> <HeaderContainer />
<Content className="content-container"> <Content className="content-container">
<Joyride <Joyride
debug debug
@@ -603,12 +600,12 @@ export function Manage({conflict, bodyshop, enableJoyRide, joyRideSteps, setJoyR
} }
}} }}
/> />
<PartnerPingComponent/> <PartnerPingComponent />
<Sentry.ErrorBoundary fallback={<ErrorBoundary/>} showDialog> <Sentry.ErrorBoundary fallback={<ErrorBoundary />} showDialog>
{PageContent} {PageContent}
</Sentry.ErrorBoundary> </Sentry.ErrorBoundary>
<FloatButton.BackTop style={{right: 100, bottom: 25}}/> <FloatButton.BackTop style={{ right: 100, bottom: 25 }} />
</Content> </Content>
<Footer> <Footer>
<div <div
@@ -620,7 +617,7 @@ export function Manage({conflict, bodyshop, enableJoyRide, joyRideSteps, setJoyR
margin: "1rem 0rem" margin: "1rem 0rem"
}} }}
> >
<div style={{display: "flex"}}> <div style={{ display: "flex" }}>
<div> <div>
{`${InstanceRenderManager({ {`${InstanceRenderManager({
imex: t("titles.imexonline"), imex: t("titles.imexonline"),
@@ -628,9 +625,9 @@ export function Manage({conflict, bodyshop, enableJoyRide, joyRideSteps, setJoyR
promanager: t("titles.promanager") promanager: t("titles.promanager")
})} - ${import.meta.env.VITE_APP_GIT_SHA_DATE}`} })} - ${import.meta.env.VITE_APP_GIT_SHA_DATE}`}
</div> </div>
<div id="noticeable-widget" style={{marginLeft: "1rem"}}/> <div id="noticeable-widget" style={{ marginLeft: "1rem" }} />
</div> </div>
<Link to="/disclaimer" target="_blank" style={{color: "#ccc"}}> <Link to="/disclaimer" target="_blank" style={{ color: "#ccc" }}>
Disclaimer & Notices Disclaimer & Notices
</Link> </Link>
</div> </div>
@@ -647,8 +644,7 @@ export function Manage({conflict, bodyshop, enableJoyRide, joyRideSteps, setJoyR
Get Tours Get Tours
</Button> </Button>
{tours.map((tour) => ( {tours.map((tour) => (
<Tag key={tour.id} <Tag key={tour.id} onClick={() => window.productFruits.api.tours.tryStartTour(tour.id)}>
onClick={() => window.productFruits.api.tours.tryStartTour(tour.id)}>
{tour.name} {tour.name}
</Tag> </Tag>
))} ))}

View File

@@ -1,75 +1,70 @@
import React, {useEffect} from "react"; import React, { useEffect } from "react";
import {useTranslation} from "react-i18next"; import { useTranslation } from "react-i18next";
import TasksPageComponent from "./tasks.page.component"; import TasksPageComponent from "./tasks.page.component";
import queryString from "query-string"; import queryString from "query-string";
import {connect} from "react-redux"; import { connect } from "react-redux";
import {createStructuredSelector} from "reselect"; import { createStructuredSelector } from "reselect";
import {setBreadcrumbs, setSelectedHeader} from "../../redux/application/application.actions"; import { setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
import InstanceRenderManager from "../../utils/instanceRenderMgr.js"; import InstanceRenderManager from "../../utils/instanceRenderMgr.js";
import {selectBodyshop, selectCurrentUser} from "../../redux/user/user.selectors.js"; import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors.js";
import TaskPageTypes from "./taskPageTypes.jsx"; import TaskPageTypes from "./taskPageTypes.jsx";
import {setModalContext} from "../../redux/modals/modals.actions.js"; import { setModalContext } from "../../redux/modals/modals.actions.js";
import {useLocation} from "react-router-dom"; import { useLocation } from "react-router-dom";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
currentUser: selectCurrentUser, currentUser: selectCurrentUser
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)), setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
setSelectedHeader: (key) => dispatch(setSelectedHeader(key)), setSelectedHeader: (key) => dispatch(setSelectedHeader(key)),
setTaskUpsertContext: (context) => dispatch(setModalContext({context, modal: 'taskUpsert'})), setTaskUpsertContext: (context) => dispatch(setModalContext({ context, modal: "taskUpsert" }))
}); });
export function MyTasksPageContainer({ export function MyTasksPageContainer({
bodyshop, bodyshop,
currentUser, currentUser,
setBreadcrumbs, setBreadcrumbs,
setSelectedHeader, setSelectedHeader,
setTaskUpsertContext setTaskUpsertContext
}) { }) {
const {t} = useTranslation(); const { t } = useTranslation();
const searchParams = queryString.parse((useLocation().search)); const searchParams = queryString.parse(useLocation().search);
useEffect(() => { useEffect(() => {
document.title = t("titles.all_tasks", { document.title = t("titles.all_tasks", {
app: InstanceRenderManager({ app: InstanceRenderManager({
imex: '$t(titles.imexonline)', imex: "$t(titles.imexonline)",
rome: '$t(titles.romeonline)', rome: "$t(titles.romeonline)",
promanager: '$t(titles.promanager)' promanager: "$t(titles.promanager)"
}) })
}); });
setSelectedHeader("all_tasks"); setSelectedHeader("all_tasks");
setBreadcrumbs([ setBreadcrumbs([
{ {
link: "/manage/tasks/alltasks", link: "/manage/tasks/alltasks",
label: t("titles.bc.all_tasks"), label: t("titles.bc.all_tasks")
},]); }
]);
}, [t, setBreadcrumbs, setSelectedHeader]); }, [t, setBreadcrumbs, setSelectedHeader]);
// This takes care of the ability to deep link a task from the URL (Dispatches the modal) // This takes care of the ability to deep link a task from the URL (Dispatches the modal)
useEffect(() => { useEffect(() => {
// Check for a query string in the URL // Check for a query string in the URL
const urlParams = new URLSearchParams(searchParams); const urlParams = new URLSearchParams(searchParams);
const taskId = urlParams.get('taskid'); const taskId = urlParams.get("taskid");
if (taskId) { if (taskId) {
setTaskUpsertContext({ setTaskUpsertContext({
context: { context: {
taskId, taskId
}, }
}); });
urlParams.delete('taskid'); urlParams.delete("taskid");
} }
}, [setTaskUpsertContext]); }, [setTaskUpsertContext]);
return ( return <TasksPageComponent type={TaskPageTypes.ALL_TASKS} currentUser={currentUser} bodyshop={bodyshop} />;
<TasksPageComponent type={TaskPageTypes.ALL_TASKS} currentUser={currentUser}
bodyshop={bodyshop}/>
);
} }
export default connect( export default connect(mapStateToProps, mapDispatchToProps)(MyTasksPageContainer);
mapStateToProps,
mapDispatchToProps
)(MyTasksPageContainer);

View File

@@ -1,49 +1,44 @@
import React, {useEffect} from "react"; import React, { useEffect } from "react";
import {useTranslation} from "react-i18next"; import { useTranslation } from "react-i18next";
import TasksPageComponent from "./tasks.page.component"; import TasksPageComponent from "./tasks.page.component";
import {connect} from "react-redux"; import { connect } from "react-redux";
import {createStructuredSelector} from "reselect"; import { createStructuredSelector } from "reselect";
import {setBreadcrumbs, setSelectedHeader} from "../../redux/application/application.actions"; import { setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
import InstanceRenderManager from "../../utils/instanceRenderMgr.js"; import InstanceRenderManager from "../../utils/instanceRenderMgr.js";
import {selectBodyshop, selectCurrentUser} from "../../redux/user/user.selectors.js"; import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors.js";
import TaskPageTypes from "./taskPageTypes.jsx"; import TaskPageTypes from "./taskPageTypes.jsx";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
currentUser: selectCurrentUser, currentUser: selectCurrentUser
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)), setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
setSelectedHeader: (key) => dispatch(setSelectedHeader(key)), setSelectedHeader: (key) => dispatch(setSelectedHeader(key))
}); });
export function MyTasksPageContainer({bodyshop, currentUser, setBreadcrumbs, setSelectedHeader}) { export function MyTasksPageContainer({ bodyshop, currentUser, setBreadcrumbs, setSelectedHeader }) {
const {t} = useTranslation(); const { t } = useTranslation();
useEffect(() => { useEffect(() => {
document.title = t("titles.my_tasks", { document.title = t("titles.my_tasks", {
app: InstanceRenderManager({ app: InstanceRenderManager({
imex: '$t(titles.imexonline)', imex: "$t(titles.imexonline)",
rome: '$t(titles.romeonline)', rome: "$t(titles.romeonline)",
promanager: '$t(titles.promanager)' promanager: "$t(titles.promanager)"
}) })
}); });
setSelectedHeader("my_tasks"); setSelectedHeader("my_tasks");
setBreadcrumbs([ setBreadcrumbs([
{ {
link: "/manage/tasks/mytasks", link: "/manage/tasks/mytasks",
label: t("titles.bc.my_tasks"), label: t("titles.bc.my_tasks")
},]); }
]);
}, [t, setBreadcrumbs, setSelectedHeader]); }, [t, setBreadcrumbs, setSelectedHeader]);
return ( return <TasksPageComponent type={TaskPageTypes.MY_TASKS} currentUser={currentUser} bodyshop={bodyshop} />;
<TasksPageComponent type={TaskPageTypes.MY_TASKS} currentUser={currentUser}
bodyshop={bodyshop}/>
);
} }
export default connect( export default connect(mapStateToProps, mapDispatchToProps)(MyTasksPageContainer);
mapStateToProps,
mapDispatchToProps
)(MyTasksPageContainer);

View File

@@ -1,6 +1,6 @@
export const TaskPageTypes = { export const TaskPageTypes = {
MY_TASKS: 'myTasks', MY_TASKS: "myTasks",
ALL_TASKS: 'allTasks', ALL_TASKS: "allTasks"
}; };
export default TaskPageTypes; export default TaskPageTypes;

View File

@@ -1,21 +1,32 @@
import React from "react"; import React from "react";
import TaskListContainer from "../../components/task-list/task-list.container.jsx"; import TaskListContainer from "../../components/task-list/task-list.container.jsx";
import {QUERY_ALL_TASKS_PAGINATED, QUERY_MY_TASKS_PAGINATED} from "../../graphql/tasks.queries.js"; import { QUERY_ALL_TASKS_PAGINATED, QUERY_MY_TASKS_PAGINATED } from "../../graphql/tasks.queries.js";
import taskPageTypes from "./taskPageTypes.jsx"; import taskPageTypes from "./taskPageTypes.jsx";
export default function TasksPageComponent({bodyshop, currentUser, type}) { export default function TasksPageComponent({ bodyshop, currentUser, type }) {
switch (type) { switch (type) {
case taskPageTypes.MY_TASKS: case taskPageTypes.MY_TASKS:
return <TaskListContainer onlyMine={true} relationshipId={currentUser.email} return (
relationshipType={'user'} query={QUERY_MY_TASKS_PAGINATED} <TaskListContainer
bodyshop={bodyshop} titleTranslation={'tasks.titles.my_tasks'} onlyMine={true}
currentUser={currentUser}/> relationshipId={currentUser.email}
relationshipType={"user"}
query={QUERY_MY_TASKS_PAGINATED}
bodyshop={bodyshop}
titleTranslation={"tasks.titles.my_tasks"}
currentUser={currentUser}
/>
);
case taskPageTypes.ALL_TASKS: case taskPageTypes.ALL_TASKS:
return <TaskListContainer query={QUERY_ALL_TASKS_PAGINATED} bodyshop={bodyshop} return (
titleTranslation={'tasks.titles.all_tasks'} <TaskListContainer
currentUser={currentUser}/> query={QUERY_ALL_TASKS_PAGINATED}
bodyshop={bodyshop}
titleTranslation={"tasks.titles.all_tasks"}
currentUser={currentUser}
/>
);
default: default:
return <></> return <></>;
} }
} }

View File

@@ -13,7 +13,7 @@ const INITIAL_STATE = {
billEnter: { ...baseModal }, billEnter: { ...baseModal },
courtesyCarReturn: { ...baseModal }, courtesyCarReturn: { ...baseModal },
noteUpsert: { ...baseModal }, noteUpsert: { ...baseModal },
taskUpsert: {...baseModal, }, taskUpsert: { ...baseModal },
schedule: { ...baseModal }, schedule: { ...baseModal },
partsOrder: { ...baseModal }, partsOrder: { ...baseModal },
timeTicket: { ...baseModal }, timeTicket: { ...baseModal },

View File

@@ -230,7 +230,7 @@
"markforreexport": "Mark for Re-export", "markforreexport": "Mark for Re-export",
"new": "New Bill", "new": "New Bill",
"nobilllines": "This part has not yet been recieved.", "nobilllines": "This part has not yet been recieved.",
"noneselected": "No bill selected.", "noneselected": "No bill selected.",
"onlycmforinvoiced": "Only credit memos can be entered for any Job that has been invoiced, exported, or voided.", "onlycmforinvoiced": "Only credit memos can be entered for any Job that has been invoiced, exported, or voided.",
"printlabels": "Print Labels", "printlabels": "Print Labels",
"retailtotal": "Bills Retail Total", "retailtotal": "Bills Retail Total",
@@ -375,18 +375,18 @@
"md_payment_types": "Payment Types", "md_payment_types": "Payment Types",
"md_referral_sources": "Referral Sources", "md_referral_sources": "Referral Sources",
"md_ro_guard": { "md_ro_guard": {
"enabled": "RO Guard Enabled?", "enabled": "RO Guard Enabled?",
"enforce_ar": "Enforce AR Balance", "enforce_ar": "Enforce AR Balance",
"enforce_bills": "Enforce Bill Discrepancy", "enforce_bills": "Enforce Bill Discrepancy",
"enforce_cm": "Enforce Credit Memo Entry", "enforce_cm": "Enforce Credit Memo Entry",
"enforce_labor": "Enforce Labor Allocation", "enforce_labor": "Enforce Labor Allocation",
"enforce_ppd": "Enforce PPD Sync", "enforce_ppd": "Enforce PPD Sync",
"enforce_profit": "Enforce Profit Requirement", "enforce_profit": "Enforce Profit Requirement",
"enforce_sublet": "Enforce Sublet Completion", "enforce_sublet": "Enforce Sublet Completion",
"masterbypass": "Master Bypass Password (not encrypted)", "masterbypass": "Master Bypass Password (not encrypted)",
"totalgppercent_minimum": "Minimum Total Gross Profit %" "totalgppercent_minimum": "Minimum Total Gross Profit %"
}, },
"md_tasks_presets": { "md_tasks_presets": {
"enable_tasks": "Enable Hour Flagging", "enable_tasks": "Enable Hour Flagging",
"hourstype": "Hour Types", "hourstype": "Hour Types",
"memo": "Time Ticket Memo", "memo": "Time Ticket Memo",
@@ -659,7 +659,7 @@
"licensing": "Licensing", "licensing": "Licensing",
"md_parts_scan": "Parts Scan Rules", "md_parts_scan": "Parts Scan Rules",
"md_ro_guard": "RO Guard", "md_ro_guard": "RO Guard",
"md_tasks_presets": "Tasks Presets", "md_tasks_presets": "Tasks Presets",
"md_to_emails": "Preset To Emails", "md_to_emails": "Preset To Emails",
"md_to_emails_emails": "Emails", "md_to_emails_emails": "Emails",
"messagingpresets": "Messaging Presets", "messagingpresets": "Messaging Presets",
@@ -681,9 +681,9 @@
"title": "Responsibility Centers" "title": "Responsibility Centers"
}, },
"roguard": { "roguard": {
"title": "RO Guard" "title": "RO Guard"
}, },
"scheduling": "SMART Scheduling", "scheduling": "SMART Scheduling",
"scoreboardsetup": "Scoreboard Setup", "scoreboardsetup": "Scoreboard Setup",
"shopinfo": "Shop Information", "shopinfo": "Shop Information",
"speedprint": "Speed Print Configuration", "speedprint": "Speed Print Configuration",
@@ -1380,7 +1380,7 @@
}, },
"fields": { "fields": {
"act_price": "Retail Price", "act_price": "Retail Price",
"act_price_before_ppc": "Original Part Price", "act_price_before_ppc": "Original Part Price",
"ah_detail_line": "Mark as Detail Labor Line (Autohouse Only)", "ah_detail_line": "Mark as Detail Labor Line (Autohouse Only)",
"assigned_team": "Team", "assigned_team": "Team",
"assigned_team_name": "Team {{name}}", "assigned_team_name": "Team {{name}}",
@@ -1852,7 +1852,7 @@
}, },
"labels": { "labels": {
"accountsreceivable": "Accounts Receivable", "accountsreceivable": "Accounts Receivable",
"act_price_ppc": "New Part Price", "act_price_ppc": "New Part Price",
"actual_completion_inferred": "$t(jobs.fields.actual_completion) inferred using $t(jobs.fields.scheduled_completion).", "actual_completion_inferred": "$t(jobs.fields.actual_completion) inferred using $t(jobs.fields.scheduled_completion).",
"actual_delivery_inferred": "$t(jobs.fields.actual_delivery) inferred using $t(jobs.fields.scheduled_delivery).", "actual_delivery_inferred": "$t(jobs.fields.actual_delivery) inferred using $t(jobs.fields.scheduled_delivery).",
"actual_in_inferred": "$t(jobs.fields.actual_in) inferred using $t(jobs.fields.scheduled_in).", "actual_in_inferred": "$t(jobs.fields.actual_in) inferred using $t(jobs.fields.scheduled_in).",
@@ -1973,7 +1973,7 @@
"markforreexport": "Mark for Re-export", "markforreexport": "Mark for Re-export",
"mash": "Shop Materials", "mash": "Shop Materials",
"masterbypass": "Master Bypass Password", "masterbypass": "Master Bypass Password",
"materials": { "materials": {
"mapa": "" "mapa": ""
}, },
"missingprofileinfo": "This job has missing tax profile info. To ensure correct totals calculations, re-import the job.", "missingprofileinfo": "This job has missing tax profile info. To ensure correct totals calculations, re-import the job.",
@@ -1982,12 +1982,12 @@
"notes": "Notes", "notes": "Notes",
"othertotal": "Other Totals", "othertotal": "Other Totals",
"outstanding_ar": "A balance is outstanding on this RO. Payments can still be entered when the job is closed. ", "outstanding_ar": "A balance is outstanding on this RO. Payments can still be entered when the job is closed. ",
"outstanding_credit_memos": "Outstanding credit memos have not been entered against this job. Credit Memos may still be posted once the job is closed.", "outstanding_credit_memos": "Outstanding credit memos have not been entered against this job. Credit Memos may still be posted once the job is closed.",
"outstanding_ppd": "There are outstanding PPDs that may not have been synced back to the estimate.", "outstanding_ppd": "There are outstanding PPDs that may not have been synced back to the estimate.",
"outstanding_reconciliation_discrep": "At least one discrepancy is not 0. This may indicate that this job is not properly reconciled and should not be closed.", "outstanding_reconciliation_discrep": "At least one discrepancy is not 0. This may indicate that this job is not properly reconciled and should not be closed.",
"outstanding_sublets": "There are sublet lines on the job which have not been marked as completed. ", "outstanding_sublets": "There are sublet lines on the job which have not been marked as completed. ",
"outstandinghours": "There are outstanding hours on the job that have not been paid or have been overpaid.", "outstandinghours": "There are outstanding hours on the job that have not been paid or have been overpaid.",
"override_header": "Override estimate header on import?", "override_header": "Override estimate header on import?",
"ownerassociation": "Owner Association", "ownerassociation": "Owner Association",
"parts": "Parts", "parts": "Parts",
"parts_lines": "Parts Lines", "parts_lines": "Parts Lines",
@@ -1996,7 +1996,7 @@
"partsfilter": "Parts Only", "partsfilter": "Parts Only",
"partssubletstotal": "Parts & Sublets Total", "partssubletstotal": "Parts & Sublets Total",
"partstotal": "Parts Total (ex. Taxes)", "partstotal": "Parts Total (ex. Taxes)",
"performance": "Performance", "performance": "Performance",
"pimraryamountpayable": "Total Primary Payable", "pimraryamountpayable": "Total Primary Payable",
"plitooltips": { "plitooltips": {
"billtotal": "The total amount of all bill lines that have been posted against this RO (not including credits, taxes, or labor adjustments).", "billtotal": "The total amount of all bill lines that have been posted against this RO (not including credits, taxes, or labor adjustments).",
@@ -2012,9 +2012,9 @@
}, },
"ppc": "This line contains a part price change.", "ppc": "This line contains a part price change.",
"ppdnotexported": "PPDs not Exported", "ppdnotexported": "PPDs not Exported",
"profileadjustments": "Profile Disc./Mkup", "profileadjustments": "Profile Disc./Mkup",
"profits": "Job Profits", "profits": "Job Profits",
"prt_dsmk_total": "Line Item Adjustment", "prt_dsmk_total": "Line Item Adjustment",
"rates": "Rates", "rates": "Rates",
"rates_subtotal": "All Rates Subtotal", "rates_subtotal": "All Rates Subtotal",
"reconciliation": { "reconciliation": {
@@ -2034,19 +2034,19 @@
"remove_from_ar": "Remove from AR", "remove_from_ar": "Remove from AR",
"returntotals": "Return Totals", "returntotals": "Return Totals",
"ro_guard": { "ro_guard": {
"eforce_profit": "Profit margins enforced.", "eforce_profit": "Profit margins enforced.",
"enforce_ar": "AR collection enforced.", "enforce_ar": "AR collection enforced.",
"enforce_bills": "Bill discrepancy enforced.", "enforce_bills": "Bill discrepancy enforced.",
"enforce_cm": "Credit memo entry enforced.", "enforce_cm": "Credit memo entry enforced.",
"enforce_labor": "Labor allocations enforced.", "enforce_labor": "Labor allocations enforced.",
"enforce_ppd": "PPD sync enforced.", "enforce_ppd": "PPD sync enforced.",
"enforce_sublet": "Sublet completion enforced.", "enforce_sublet": "Sublet completion enforced.",
"enforce_validation": "Master Bypass Required: {{message}}", "enforce_validation": "Master Bypass Required: {{message}}",
"enforced": "This check has been enforced by your shop manager. Enter the master bypass password to close the Job." "enforced": "This check has been enforced by your shop manager. Enter the master bypass password to close the Job."
}, },
"roguard": "RO Guard", "roguard": "RO Guard",
"roguardwarnings": "RO Guard Warnings", "roguardwarnings": "RO Guard Warnings",
"rosaletotal": "RO Parts Total", "rosaletotal": "RO Parts Total",
"sale_additional": "Sales - Additional", "sale_additional": "Sales - Additional",
"sale_labor": "Sales - Labor", "sale_labor": "Sales - Labor",
"sale_parts": "Sales - Parts", "sale_parts": "Sales - Parts",
@@ -2056,7 +2056,7 @@
"scheduledinchange": "The scheduled in is based off the latest appointment. To change this date, please schedule or reschedule the Job. ", "scheduledinchange": "The scheduled in is based off the latest appointment. To change this date, please schedule or reschedule the Job. ",
"specialcoveragepolicy": "Special Coverage Policy Applies", "specialcoveragepolicy": "Special Coverage Policy Applies",
"state_tax_amt": "Provincial/State Taxes", "state_tax_amt": "Provincial/State Taxes",
"subletsnotcompleted": "Outstanding Sublets", "subletsnotcompleted": "Outstanding Sublets",
"subletstotal": "Sublets Total", "subletstotal": "Sublets Total",
"subtotal": "Subtotal", "subtotal": "Subtotal",
"supplementnote": "The Job had a supplement imported.", "supplementnote": "The Job had a supplement imported.",
@@ -2596,7 +2596,7 @@
"markreexported": "Payment marked for re-export successfully", "markreexported": "Payment marked for re-export successfully",
"payment": "Payment created successfully. ", "payment": "Payment created successfully. ",
"paymentupdate": "Payment updated successfully. ", "paymentupdate": "Payment updated successfully. ",
"stripe": "Credit card transaction charged successfully." "stripe": "Credit card transaction charged successfully."
} }
}, },
"phonebook": { "phonebook": {

View File

@@ -374,18 +374,18 @@
"md_payment_types": "", "md_payment_types": "",
"md_referral_sources": "", "md_referral_sources": "",
"md_ro_guard": { "md_ro_guard": {
"enabled": "", "enabled": "",
"enforce_ar": "", "enforce_ar": "",
"enforce_bills": "", "enforce_bills": "",
"enforce_cm": "", "enforce_cm": "",
"enforce_labor": "", "enforce_labor": "",
"enforce_ppd": "", "enforce_ppd": "",
"enforce_profit": "", "enforce_profit": "",
"enforce_sublet": "", "enforce_sublet": "",
"masterbypass": "", "masterbypass": "",
"totalgppercent_minimum": "" "totalgppercent_minimum": ""
}, },
"md_tasks_presets": { "md_tasks_presets": {
"enable_tasks": "", "enable_tasks": "",
"hourstype": "", "hourstype": "",
"memo": "", "memo": "",
@@ -658,7 +658,7 @@
"licensing": "", "licensing": "",
"md_parts_scan": "", "md_parts_scan": "",
"md_ro_guard": "", "md_ro_guard": "",
"md_tasks_presets": "", "md_tasks_presets": "",
"md_to_emails": "", "md_to_emails": "",
"md_to_emails_emails": "", "md_to_emails_emails": "",
"messagingpresets": "", "messagingpresets": "",
@@ -678,9 +678,9 @@
"sales_tax_codes": "", "sales_tax_codes": "",
"tax_accounts": "", "tax_accounts": "",
"title": "" "title": ""
}, },
"roguard": { "roguard": {
"title": "" "title": ""
}, },
"scheduling": "", "scheduling": "",
"scoreboardsetup": "", "scoreboardsetup": "",
@@ -1380,7 +1380,7 @@
"fields": { "fields": {
"act_price": "Precio actual", "act_price": "Precio actual",
"act_price_before_ppc": "", "act_price_before_ppc": "",
"ah_detail_line": "", "ah_detail_line": "",
"assigned_team": "", "assigned_team": "",
"assigned_team_name": "", "assigned_team_name": "",
"create_ppc": "", "create_ppc": "",
@@ -1851,7 +1851,7 @@
}, },
"labels": { "labels": {
"accountsreceivable": "", "accountsreceivable": "",
"act_price_ppc": "", "act_price_ppc": "",
"actual_completion_inferred": "", "actual_completion_inferred": "",
"actual_delivery_inferred": "", "actual_delivery_inferred": "",
"actual_in_inferred": "", "actual_in_inferred": "",
@@ -1972,7 +1972,7 @@
"markforreexport": "", "markforreexport": "",
"mash": "", "mash": "",
"masterbypass": "", "masterbypass": "",
"materials": { "materials": {
"mapa": "" "mapa": ""
}, },
"missingprofileinfo": "", "missingprofileinfo": "",
@@ -1980,12 +1980,12 @@
"net_repairs": "", "net_repairs": "",
"notes": "Notas", "notes": "Notas",
"othertotal": "", "othertotal": "",
"outstanding_ar": "", "outstanding_ar": "",
"outstanding_credit_memos": "", "outstanding_credit_memos": "",
"outstanding_ppd": "", "outstanding_ppd": "",
"outstanding_reconciliation_discrep": "", "outstanding_reconciliation_discrep": "",
"outstanding_sublets": "", "outstanding_sublets": "",
"outstandinghours": "", "outstandinghours": "",
"override_header": "¿Anular encabezado estimado al importar?", "override_header": "¿Anular encabezado estimado al importar?",
"ownerassociation": "", "ownerassociation": "",
"parts": "Partes", "parts": "Partes",
@@ -1996,7 +1996,7 @@
"partssubletstotal": "", "partssubletstotal": "",
"partstotal": "", "partstotal": "",
"performance": "", "performance": "",
"pimraryamountpayable": "", "pimraryamountpayable": "",
"plitooltips": { "plitooltips": {
"billtotal": "", "billtotal": "",
"calculatedcreditsnotreceived": "", "calculatedcreditsnotreceived": "",
@@ -2011,9 +2011,9 @@
}, },
"ppc": "", "ppc": "",
"ppdnotexported": "", "ppdnotexported": "",
"profileadjustments": "", "profileadjustments": "",
"profits": "", "profits": "",
"prt_dsmk_total": "", "prt_dsmk_total": "",
"rates": "Tarifas", "rates": "Tarifas",
"rates_subtotal": "", "rates_subtotal": "",
"reconciliation": { "reconciliation": {
@@ -2033,19 +2033,19 @@
"remove_from_ar": "", "remove_from_ar": "",
"returntotals": "", "returntotals": "",
"ro_guard": { "ro_guard": {
"eforce_profit": "", "eforce_profit": "",
"enforce_ar": "", "enforce_ar": "",
"enforce_bills": "", "enforce_bills": "",
"enforce_cm": "", "enforce_cm": "",
"enforce_labor": "", "enforce_labor": "",
"enforce_ppd": "", "enforce_ppd": "",
"enforce_sublet": "", "enforce_sublet": "",
"enforce_validation": "", "enforce_validation": "",
"enforced": "" "enforced": ""
}, },
"roguard": "", "roguard": "",
"roguardwarnings": "", "roguardwarnings": "",
"rosaletotal": "", "rosaletotal": "",
"sale_additional": "", "sale_additional": "",
"sale_labor": "", "sale_labor": "",
"sale_parts": "", "sale_parts": "",
@@ -2055,7 +2055,7 @@
"scheduledinchange": "", "scheduledinchange": "",
"specialcoveragepolicy": "", "specialcoveragepolicy": "",
"state_tax_amt": "", "state_tax_amt": "",
"subletsnotcompleted": "", "subletsnotcompleted": "",
"subletstotal": "", "subletstotal": "",
"subtotal": "", "subtotal": "",
"supplementnote": "", "supplementnote": "",
@@ -2595,7 +2595,7 @@
"markreexported": "", "markreexported": "",
"payment": "", "payment": "",
"paymentupdate": "", "paymentupdate": "",
"stripe": "" "stripe": ""
} }
}, },
"phonebook": { "phonebook": {

View File

@@ -374,18 +374,18 @@
"md_payment_types": "", "md_payment_types": "",
"md_referral_sources": "", "md_referral_sources": "",
"md_ro_guard": { "md_ro_guard": {
"enabled": "", "enabled": "",
"enforce_ar": "", "enforce_ar": "",
"enforce_bills": "", "enforce_bills": "",
"enforce_cm": "", "enforce_cm": "",
"enforce_labor": "", "enforce_labor": "",
"enforce_ppd": "", "enforce_ppd": "",
"enforce_profit": "", "enforce_profit": "",
"enforce_sublet": "", "enforce_sublet": "",
"masterbypass": "", "masterbypass": "",
"totalgppercent_minimum": "" "totalgppercent_minimum": ""
}, },
"md_tasks_presets": { "md_tasks_presets": {
"enable_tasks": "", "enable_tasks": "",
"hourstype": "", "hourstype": "",
"memo": "", "memo": "",
@@ -658,7 +658,7 @@
"licensing": "", "licensing": "",
"md_parts_scan": "", "md_parts_scan": "",
"md_ro_guard": "", "md_ro_guard": "",
"md_tasks_presets": "", "md_tasks_presets": "",
"md_to_emails": "", "md_to_emails": "",
"md_to_emails_emails": "", "md_to_emails_emails": "",
"messagingpresets": "", "messagingpresets": "",
@@ -678,9 +678,9 @@
"sales_tax_codes": "", "sales_tax_codes": "",
"tax_accounts": "", "tax_accounts": "",
"title": "" "title": ""
}, },
"roguard": { "roguard": {
"title": "" "title": ""
}, },
"scheduling": "", "scheduling": "",
"scoreboardsetup": "", "scoreboardsetup": "",
@@ -1380,7 +1380,7 @@
"fields": { "fields": {
"act_price": "Prix actuel", "act_price": "Prix actuel",
"act_price_before_ppc": "", "act_price_before_ppc": "",
"ah_detail_line": "", "ah_detail_line": "",
"assigned_team": "", "assigned_team": "",
"assigned_team_name": "", "assigned_team_name": "",
"create_ppc": "", "create_ppc": "",
@@ -1851,7 +1851,7 @@
}, },
"labels": { "labels": {
"accountsreceivable": "", "accountsreceivable": "",
"act_price_ppc": "", "act_price_ppc": "",
"actual_completion_inferred": "", "actual_completion_inferred": "",
"actual_delivery_inferred": "", "actual_delivery_inferred": "",
"actual_in_inferred": "", "actual_in_inferred": "",
@@ -1972,7 +1972,7 @@
"markforreexport": "", "markforreexport": "",
"mash": "", "mash": "",
"masterbypass": "", "masterbypass": "",
"materials": { "materials": {
"mapa": "" "mapa": ""
}, },
"missingprofileinfo": "", "missingprofileinfo": "",
@@ -1980,12 +1980,12 @@
"net_repairs": "", "net_repairs": "",
"notes": "Remarques", "notes": "Remarques",
"othertotal": "", "othertotal": "",
"outstanding_ar": "", "outstanding_ar": "",
"outstanding_credit_memos": "", "outstanding_credit_memos": "",
"outstanding_ppd": "", "outstanding_ppd": "",
"outstanding_reconciliation_discrep": "", "outstanding_reconciliation_discrep": "",
"outstanding_sublets": "", "outstanding_sublets": "",
"outstandinghours": "", "outstandinghours": "",
"override_header": "Remplacer l'en-tête d'estimation à l'importation?", "override_header": "Remplacer l'en-tête d'estimation à l'importation?",
"ownerassociation": "", "ownerassociation": "",
"parts": "les pièces", "parts": "les pièces",
@@ -1996,7 +1996,7 @@
"partssubletstotal": "", "partssubletstotal": "",
"partstotal": "", "partstotal": "",
"performance": "", "performance": "",
"pimraryamountpayable": "", "pimraryamountpayable": "",
"plitooltips": { "plitooltips": {
"billtotal": "", "billtotal": "",
"calculatedcreditsnotreceived": "", "calculatedcreditsnotreceived": "",
@@ -2011,9 +2011,9 @@
}, },
"ppc": "", "ppc": "",
"ppdnotexported": "", "ppdnotexported": "",
"profileadjustments": "", "profileadjustments": "",
"profits": "", "profits": "",
"prt_dsmk_total": "", "prt_dsmk_total": "",
"rates": "Les taux", "rates": "Les taux",
"rates_subtotal": "", "rates_subtotal": "",
"reconciliation": { "reconciliation": {
@@ -2033,19 +2033,19 @@
"remove_from_ar": "", "remove_from_ar": "",
"returntotals": "", "returntotals": "",
"ro_guard": { "ro_guard": {
"eforce_profit": "", "eforce_profit": "",
"enforce_ar": "", "enforce_ar": "",
"enforce_bills": "", "enforce_bills": "",
"enforce_cm": "", "enforce_cm": "",
"enforce_labor": "", "enforce_labor": "",
"enforce_ppd": "", "enforce_ppd": "",
"enforce_sublet": "", "enforce_sublet": "",
"enforce_validation": "", "enforce_validation": "",
"enforced": "" "enforced": ""
}, },
"roguard": "", "roguard": "",
"roguardwarnings": "", "roguardwarnings": "",
"rosaletotal": "", "rosaletotal": "",
"sale_additional": "", "sale_additional": "",
"sale_labor": "", "sale_labor": "",
"sale_parts": "", "sale_parts": "",
@@ -2055,7 +2055,7 @@
"scheduledinchange": "", "scheduledinchange": "",
"specialcoveragepolicy": "", "specialcoveragepolicy": "",
"state_tax_amt": "", "state_tax_amt": "",
"subletsnotcompleted": "", "subletsnotcompleted": "",
"subletstotal": "", "subletstotal": "",
"subtotal": "", "subtotal": "",
"supplementnote": "", "supplementnote": "",
@@ -2595,7 +2595,7 @@
"markreexported": "", "markreexported": "",
"payment": "", "payment": "",
"paymentupdate": "", "paymentupdate": "",
"stripe": "" "stripe": ""
} }
}, },
"phonebook": { "phonebook": {

View File

@@ -40,33 +40,37 @@ const AuditTrailMapping = {
jobsupplement: () => i18n.t("audit_trail.messages.jobsupplement"), jobsupplement: () => i18n.t("audit_trail.messages.jobsupplement"),
jobsuspend: (status) => i18n.t("audit_trail.messages.jobsuspend", { status }), jobsuspend: (status) => i18n.t("audit_trail.messages.jobsuspend", { status }),
jobvoid: () => i18n.t("audit_trail.messages.jobvoid"), jobvoid: () => i18n.t("audit_trail.messages.jobvoid"),
// Tasks Entries // Tasks Entries
tasksCreated: (title, createdBy) => i18n.t("audit_trail.messages.tasks_created", { tasksCreated: (title, createdBy) =>
title, i18n.t("audit_trail.messages.tasks_created", {
createdBy title,
}), createdBy
tasksUpdated: (title, updatedBy) => i18n.t("audit_trail.messages.tasks_updated", { }),
title, tasksUpdated: (title, updatedBy) =>
updatedBy i18n.t("audit_trail.messages.tasks_updated", {
}), title,
tasksDeleted: (title, deletedBy) => i18n.t("audit_trail.messages.tasks_deleted", { updatedBy
title, }),
deletedBy tasksDeleted: (title, deletedBy) =>
}), i18n.t("audit_trail.messages.tasks_deleted", {
tasksUndeleted: (title, undeletedBy) => i18n.t("audit_trail.messages.tasks_undeleted", { title,
title, deletedBy
undeletedBy }),
}), tasksUndeleted: (title, undeletedBy) =>
tasksCompleted: (title, completedBy) => i18n.t("audit_trail.messages.tasks_completed", { i18n.t("audit_trail.messages.tasks_undeleted", {
title, title,
completedBy undeletedBy
}), }),
tasksUncompleted: (title, uncompletedBy) => i18n.t("audit_trail.messages.tasks_uncompleted", { tasksCompleted: (title, completedBy) =>
title, i18n.t("audit_trail.messages.tasks_completed", {
uncompletedBy title,
}), completedBy
}),
tasksUncompleted: (title, uncompletedBy) =>
i18n.t("audit_trail.messages.tasks_uncompleted", {
title,
uncompletedBy
})
}; };
export default AuditTrailMapping; export default AuditTrailMapping;

View File

@@ -29,7 +29,7 @@ export function replaceUndefinedWithNull(obj, keys) {
return Object.fromEntries( return Object.fromEntries(
Object.entries(obj).map(([key, value]) => { Object.entries(obj).map(([key, value]) => {
if (keys) { if (keys) {
return [key, (keys.includes(key) && value === undefined) ? null : value]; return [key, keys.includes(key) && value === undefined ? null : value];
} else { } else {
return [key, value === undefined ? null : value]; return [key, value === undefined ? null : value];
} }

16
package-lock.json generated
View File

@@ -56,6 +56,7 @@
"devDependencies": { "devDependencies": {
"@trivago/prettier-plugin-sort-imports": "^4.3.0", "@trivago/prettier-plugin-sort-imports": "^4.3.0",
"concurrently": "^8.2.2", "concurrently": "^8.2.2",
"prettier": "^3.2.5",
"source-map-explorer": "^2.5.2" "source-map-explorer": "^2.5.2"
}, },
"engines": { "engines": {
@@ -5900,6 +5901,21 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/prettier": {
"version": "3.2.5",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz",
"integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==",
"dev": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/process": { "node_modules/process": {
"version": "0.11.10", "version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",

View File

@@ -66,6 +66,7 @@
"devDependencies": { "devDependencies": {
"@trivago/prettier-plugin-sort-imports": "^4.3.0", "@trivago/prettier-plugin-sort-imports": "^4.3.0",
"concurrently": "^8.2.2", "concurrently": "^8.2.2",
"prettier": "^3.2.5",
"source-map-explorer": "^2.5.2" "source-map-explorer": "^2.5.2"
} }
} }

View File

@@ -1,6 +1,6 @@
const moment = require("moment"); const moment = require("moment");
const {default: RenderInstanceManager} = require("../utils/instanceMgr"); const { default: RenderInstanceManager } = require("../utils/instanceMgr");
const {header, end, start} = require("./html"); const { header, end, start } = require("./html");
// Required Strings // Required Strings
// - header - The header of the email // - header - The header of the email
@@ -13,21 +13,21 @@ const {header, end, start} = require("./html");
const defaultFooter = () => { const defaultFooter = () => {
return RenderInstanceManager({ return RenderInstanceManager({
imex: 'ImEX Online Collision Repair Management System', imex: "ImEX Online Collision Repair Management System",
rome: 'Rome Technologies', rome: "Rome Technologies",
promanager: 'ProManager', promanager: "ProManager"
}); });
}; };
const now = () => moment().format('MM/DD/YYYY @ hh:mm a'); const now = () => moment().format("MM/DD/YYYY @ hh:mm a");
const generateEmailTemplate = (strings) => { const generateEmailTemplate = (strings) => {
return (
return ` `
<html xmlns="http://www.w3.org/1999/xhtml" lang="en-US">` <html xmlns="http://www.w3.org/1999/xhtml" lang="en-US">` +
+ header header +
+ start start +
+ ` `
<table class="row"> <table class="row">
<tbody> <tbody>
<tr> <tr>
@@ -101,8 +101,9 @@ const generateEmailTemplate = (strings) => {
</th> </th>
</tr> </tr>
</tbody> </tbody>
</table>` </table>` +
+ end end
);
}; };
module.exports = generateEmailTemplate; module.exports = generateEmailTemplate;

View File

@@ -5,13 +5,13 @@ require("dotenv").config({
const axios = require("axios"); const axios = require("axios");
let nodemailer = require("nodemailer"); let nodemailer = require("nodemailer");
let aws = require("@aws-sdk/client-ses"); let aws = require("@aws-sdk/client-ses");
let {defaultProvider} = require("@aws-sdk/credential-provider-node"); let { defaultProvider } = require("@aws-sdk/credential-provider-node");
const InstanceManager = require("../utils/instanceMgr").default; const InstanceManager = require("../utils/instanceMgr").default;
const logger = require("../utils/logger"); const logger = require("../utils/logger");
const client = require("../graphql-client/graphql-client").client; const client = require("../graphql-client/graphql-client").client;
const queries = require("../graphql-client/queries"); const queries = require("../graphql-client/queries");
const {isObject} = require("lodash"); const { isObject } = require("lodash");
const generateEmailTemplate = require('./generateTemplate'); const generateEmailTemplate = require("./generateTemplate");
const ses = new aws.SES({ const ses = new aws.SES({
// The key apiVersion is no longer supported in v3, and can be removed. // The key apiVersion is no longer supported in v3, and can be removed.
@@ -25,10 +25,10 @@ const ses = new aws.SES({
}); });
let transporter = nodemailer.createTransport({ let transporter = nodemailer.createTransport({
SES: {ses, aws} SES: { ses, aws }
}); });
exports.sendServerEmail = async function ({subject, text}) { exports.sendServerEmail = async function ({ subject, text }) {
if (process.env.NODE_ENV === undefined) return; if (process.env.NODE_ENV === undefined) return;
try { try {
transporter.sendMail( transporter.sendMail(
@@ -60,7 +60,7 @@ exports.sendServerEmail = async function ({subject, text}) {
logger.log("server-email-failure", "error", null, null, error); logger.log("server-email-failure", "error", null, null, error);
} }
}; };
exports.sendTaskEmail = async function ({to, subject, text, attachments}) { exports.sendTaskEmail = async function ({ to, subject, text, attachments }) {
try { try {
transporter.sendMail( transporter.sendMail(
{ {
@@ -125,12 +125,12 @@ exports.sendEmail = async (req, res) => {
attachments: attachments:
[ [
...((req.body.attachments && ...((req.body.attachments &&
req.body.attachments.map((a) => { req.body.attachments.map((a) => {
return { return {
filename: a.filename, filename: a.filename,
path: a.path path: a.path
}; };
})) || })) ||
[]), []),
...downloadedMedia.map((a) => { ...downloadedMedia.map((a) => {
return { return {
@@ -186,14 +186,14 @@ exports.sendEmail = async (req, res) => {
subject: req.body.subject, subject: req.body.subject,
bodyshopid: req.body.bodyshopid bodyshopid: req.body.bodyshopid
}); });
res.status(500).json({success: false, error: err}); res.status(500).json({ success: false, error: err });
} }
} }
); );
}; };
async function getImage(imageUrl) { async function getImage(imageUrl) {
let image = await axios.get(imageUrl, {responseType: "arraybuffer"}); let image = await axios.get(imageUrl, { responseType: "arraybuffer" });
let raw = Buffer.from(image.data).toString("base64"); let raw = Buffer.from(image.data).toString("base64");
return "data:" + image.headers["content-type"] + ";base64," + raw; return "data:" + image.headers["content-type"] + ";base64," + raw;
} }
@@ -281,10 +281,10 @@ exports.emailBounce = async function (req, res) {
})} has tried to deliver an email with the subject: ${subject} to the intended recipients but encountered an error. })} has tried to deliver an email with the subject: ${subject} to the intended recipients but encountered an error.
${body.bounce?.bouncedRecipients.map( ${body.bounce?.bouncedRecipients.map(
(r) => (r) =>
`Recipient: ${r.emailAddress} | Status: ${r.action} | Code: ${r.diagnosticCode} `Recipient: ${r.emailAddress} | Status: ${r.action} | Code: ${r.diagnosticCode}
` `
)} )}
` `
}, },
(err, info) => { (err, info) => {