Merge branch 'master-AIO' into feature/AIO/promanager
This commit is contained in:
@@ -9,10 +9,10 @@ const config = {
|
||||
arrowParens: "always",
|
||||
jsxSingleQuote: false,
|
||||
bracketSameLine: false,
|
||||
endOfLine: "lf",
|
||||
importOrder: ["^@core/(.*)$", "^@server/(.*)$", "^@ui/(.*)$", "^[./]"],
|
||||
importOrderSeparation: true,
|
||||
importOrderSortSpecifiers: true
|
||||
endOfLine: "lf"
|
||||
// importOrder: ["^@core/(.*)$", "^@server/(.*)$", "^@ui/(.*)$", "^[./]"],
|
||||
// importOrderSeparation: true,
|
||||
// importOrderSortSpecifiers: true
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
8
client/.eslintrc
Normal file
8
client/.eslintrc
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": [
|
||||
"react-app"
|
||||
],
|
||||
"rules": {
|
||||
"no-useless-rename": "off"
|
||||
}
|
||||
}
|
||||
39
client/package-lock.json
generated
39
client/package-lock.json
generated
@@ -96,6 +96,8 @@
|
||||
"browserslist-to-esbuild": "^2.1.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"cypress": "^13.6.6",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"eslint-plugin-cypress": "^2.15.1",
|
||||
"memfs": "^4.6.0",
|
||||
"os-browserify": "^0.3.0",
|
||||
@@ -104,6 +106,7 @@
|
||||
"source-map-explorer": "^2.5.3",
|
||||
"vite": "^5.0.11",
|
||||
"vite-plugin-babel": "^1.2.0",
|
||||
"vite-plugin-eslint": "^1.8.1",
|
||||
"vite-plugin-legacy": "^2.1.0",
|
||||
"vite-plugin-node-polyfills": "^0.19.0",
|
||||
"vite-plugin-pwa": "^0.19.0",
|
||||
@@ -11934,7 +11937,8 @@
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "8.57.0",
|
||||
"license": "MIT",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz",
|
||||
"integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.6.1",
|
||||
@@ -11987,7 +11991,8 @@
|
||||
},
|
||||
"node_modules/eslint-config-react-app": {
|
||||
"version": "7.0.1",
|
||||
"license": "MIT",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-react-app/-/eslint-config-react-app-7.0.1.tgz",
|
||||
"integrity": "sha512-K6rNzvkIeHaTd8m/QEh1Zko0KI7BACWkkneSs6s9cKZC/J27X3eZR6Upt1jkmZ/4FK+XUOPPxMEN7+lbUXfSlA==",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.16.0",
|
||||
"@babel/eslint-parser": "^7.16.3",
|
||||
@@ -26439,6 +26444,36 @@
|
||||
"vite": ">=5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vite-plugin-eslint": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-eslint/-/vite-plugin-eslint-1.8.1.tgz",
|
||||
"integrity": "sha512-PqdMf3Y2fLO9FsNPmMX+//2BF5SF8nEWspZdgl4kSt7UvHDRHVVfHvxsD7ULYzZrJDGRxR81Nq7TOFgwMnUang==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@rollup/pluginutils": "^4.2.1",
|
||||
"@types/eslint": "^8.4.5",
|
||||
"rollup": "^2.77.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": ">=7",
|
||||
"vite": ">=2"
|
||||
}
|
||||
},
|
||||
"node_modules/vite-plugin-eslint/node_modules/rollup": {
|
||||
"version": "2.79.1",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz",
|
||||
"integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/vite-plugin-legacy": {
|
||||
"version": "2.1.0",
|
||||
"dev": true,
|
||||
|
||||
@@ -140,6 +140,8 @@
|
||||
"browserslist-to-esbuild": "^2.1.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"cypress": "^13.6.6",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"eslint-plugin-cypress": "^2.15.1",
|
||||
"memfs": "^4.6.0",
|
||||
"os-browserify": "^0.3.0",
|
||||
@@ -148,6 +150,7 @@
|
||||
"source-map-explorer": "^2.5.3",
|
||||
"vite": "^5.0.11",
|
||||
"vite-plugin-babel": "^1.2.0",
|
||||
"vite-plugin-eslint": "^1.8.1",
|
||||
"vite-plugin-legacy": "^2.1.0",
|
||||
"vite-plugin-node-polyfills": "^0.19.0",
|
||||
"vite-plugin-pwa": "^0.19.0",
|
||||
|
||||
@@ -34,6 +34,7 @@ export function BillDetailEditReturn({ setPartsOrderContext, insertAuditTrail, b
|
||||
actions: {},
|
||||
context: {
|
||||
jobId: data.bills_by_pk.jobid,
|
||||
job: data.bills_by_pk.job,
|
||||
vendorId: data.bills_by_pk.vendorid,
|
||||
returnFromBill: data.bills_by_pk.id,
|
||||
invoiceNumber: data.bills_by_pk.invoice_number,
|
||||
|
||||
@@ -14,6 +14,7 @@ import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import BillDeleteButton from "../bill-delete-button/bill-delete-button.component";
|
||||
import BillDetailEditReturnComponent from "../bill-detail-edit/bill-detail-edit-return.component";
|
||||
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
|
||||
import { FaTasks } from "react-icons/fa";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
jobRO: selectJobReadOnly,
|
||||
@@ -21,9 +22,21 @@ const mapStateToProps = createStructuredSelector({
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setPartsOrderContext: (context) => dispatch(setModalContext({ context: context, modal: "partsOrder" })),
|
||||
setBillEnterContext: (context) => dispatch(setModalContext({ context: context, modal: "billEnter" })),
|
||||
setReconciliationContext: (context) => dispatch(setModalContext({ context: context, modal: "reconciliation" }))
|
||||
setBillEnterContext: (context) =>
|
||||
dispatch(
|
||||
setModalContext({
|
||||
context: context,
|
||||
modal: "billEnter"
|
||||
})
|
||||
),
|
||||
setReconciliationContext: (context) =>
|
||||
dispatch(
|
||||
setModalContext({
|
||||
context: context,
|
||||
modal: "reconciliation"
|
||||
})
|
||||
),
|
||||
setTaskUpsertContext: (context) => dispatch(setModalContext({ context, modal: "taskUpsert" }))
|
||||
});
|
||||
|
||||
export function BillsListTableComponent({
|
||||
@@ -32,9 +45,9 @@ export function BillsListTableComponent({
|
||||
job,
|
||||
billsQuery,
|
||||
handleOnRowClick,
|
||||
setPartsOrderContext,
|
||||
setBillEnterContext,
|
||||
setReconciliationContext
|
||||
setReconciliationContext,
|
||||
setTaskUpsertContext
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -48,6 +61,7 @@ export function BillsListTableComponent({
|
||||
const Templates = TemplateList("bill");
|
||||
const bills = billsQuery.data ? billsQuery.data.bills : [];
|
||||
const { refetch } = billsQuery;
|
||||
|
||||
const recordActions = (record, showView = false) => (
|
||||
<Space wrap>
|
||||
{showView && (
|
||||
@@ -55,9 +69,22 @@ export function BillsListTableComponent({
|
||||
<EditFilled />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
title={t("tasks.buttons.create")}
|
||||
onClick={() => {
|
||||
setTaskUpsertContext({
|
||||
context: {
|
||||
jobid: job.id,
|
||||
billid: record.id
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<FaTasks />
|
||||
</Button>
|
||||
<BillDeleteButton bill={record} jobid={job.id} />
|
||||
<BillDetailEditReturnComponent
|
||||
data={{ bills_by_pk: { ...record, jobid: job.id } }}
|
||||
data={{ bills_by_pk: { ...record, jobid: job.id, job: job } }}
|
||||
disabled={record.is_credit_memo || record.vendorid === bodyshop.inhousevendorid || jobRO}
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import React, { useState } from "react";
|
||||
import { Button, Form, InputNumber, Popover } from "antd";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { CalculatorFilled } from "@ant-design/icons";
|
||||
|
||||
import { Button, Form, InputNumber, Popover, Space } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
export default function CABCpvrtCalculator({ disabled, form }) {
|
||||
const [visibility, setVisibility] = useState(false);
|
||||
|
||||
@@ -27,10 +26,14 @@ export default function CABCpvrtCalculator({ disabled, form }) {
|
||||
<Form.Item name="days" label={t("jobs.labels.ca_bc_pvrt.days")}>
|
||||
<InputNumber precision={0} min={0} />
|
||||
</Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
{t("general.actions.calculate")}
|
||||
</Button>
|
||||
<Button onClick={() => setVisibility(false)}>Close</Button>
|
||||
<div style={{ display: "flex", justifyContent: "flex-end" }}>
|
||||
<Space>
|
||||
<Button type="primary" htmlType="submit">
|
||||
{t("general.actions.calculate")}
|
||||
</Button>
|
||||
<Button onClick={() => setVisibility(false)}>Close</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import { DeleteFilled } from "@ant-design/icons";
|
||||
import { useLazyQuery, useMutation } from "@apollo/client";
|
||||
import { Button, Card, Col, Form, Input, notification, Row, Space, Spin, Statistic } from "antd";
|
||||
import { Button, Card, Col, Form, Input, Row, Space, Spin, Statistic, notification } from "antd";
|
||||
import axios from "axios";
|
||||
import dayjs from "../../utils/day";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { INSERT_PAYMENT_RESPONSE, QUERY_RO_AND_OWNER_BY_JOB_PKS } from "../../graphql/payment_response.queries";
|
||||
import { INSERT_NEW_PAYMENT } from "../../graphql/payments.queries";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
||||
import { selectCardPayment } from "../../redux/modals/modals.selectors";
|
||||
@@ -28,12 +26,12 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
});
|
||||
|
||||
const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisible, insertAuditTrail }) => {
|
||||
const { context } = cardPaymentModal;
|
||||
const { context, actions } = cardPaymentModal;
|
||||
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [insertPayment] = useMutation(INSERT_NEW_PAYMENT);
|
||||
// const [insertPayment] = useMutation(INSERT_NEW_PAYMENT);
|
||||
const [insertPaymentResponse] = useMutation(INSERT_PAYMENT_RESPONSE);
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -42,7 +40,6 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
||||
skip: true
|
||||
});
|
||||
|
||||
console.log("🚀 ~ file: card-payment-modal.component..jsx:61 ~ data:", data);
|
||||
//Initialize the intellipay window.
|
||||
const SetIntellipayCallbackFunctions = () => {
|
||||
console.log("*** Set IntelliPay callback functions.");
|
||||
@@ -51,16 +48,20 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
||||
});
|
||||
|
||||
window.intellipay.runOnApproval(async function (response) {
|
||||
console.warn("*** Running On Approval Script ***");
|
||||
form.setFieldValue("paymentResponse", response);
|
||||
form.submit();
|
||||
//2024-04-25: Nothing is going to happen here anymore. We'll completely rely on the callback.
|
||||
//Add a slight delay to allow the refetch to properly get the data.
|
||||
setTimeout(() => {
|
||||
if (actions && actions.refetch && typeof actions.refetch === "function")
|
||||
actions.refetch();
|
||||
setLoading(false);
|
||||
toggleModalVisible();
|
||||
}, 750);
|
||||
});
|
||||
|
||||
window.intellipay.runOnNonApproval(async function (response) {
|
||||
// Mutate unsuccessful payment
|
||||
|
||||
const { payments } = form.getFieldsValue();
|
||||
|
||||
await insertPaymentResponse({
|
||||
variables: {
|
||||
paymentResponse: payments.map((payment) => ({
|
||||
@@ -85,50 +86,9 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
||||
});
|
||||
};
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
try {
|
||||
await insertPayment({
|
||||
variables: {
|
||||
paymentInput: values.payments.map((payment) => ({
|
||||
amount: payment.amount,
|
||||
transactionid: (values.paymentResponse.paymentid || "").toString(),
|
||||
payer: t("payments.labels.customer"),
|
||||
type: values.paymentResponse.cardbrand,
|
||||
jobid: payment.jobid,
|
||||
date: dayjs(Date.now()),
|
||||
payment_responses: {
|
||||
data: [
|
||||
{
|
||||
amount: payment.amount,
|
||||
bodyshopid: bodyshop.id,
|
||||
|
||||
jobid: payment.jobid,
|
||||
declinereason: values.paymentResponse.declinereason,
|
||||
ext_paymentid: values.paymentResponse.paymentid.toString(),
|
||||
successful: true,
|
||||
response: values.paymentResponse
|
||||
}
|
||||
]
|
||||
}
|
||||
}))
|
||||
},
|
||||
refetchQueries: ["GET_JOB_BY_PK"]
|
||||
});
|
||||
toggleModalVisible();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
notification.open({
|
||||
type: "error",
|
||||
message: t("payments.errors.inserting", { error: error.message })
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleIntelliPayCharge = async () => {
|
||||
setLoading(true);
|
||||
|
||||
//Validate
|
||||
try {
|
||||
await form.validateFields();
|
||||
@@ -140,7 +100,8 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
||||
try {
|
||||
const response = await axios.post("/intellipay/lightbox_credentials", {
|
||||
bodyshop,
|
||||
refresh: !!window.intellipay
|
||||
refresh: !!window.intellipay,
|
||||
paymentSplitMeta: form.getFieldsValue(),
|
||||
});
|
||||
|
||||
if (window.intellipay) {
|
||||
@@ -169,7 +130,6 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
||||
<Card title="Card Payment">
|
||||
<Spin spinning={loading}>
|
||||
<Form
|
||||
onFinish={handleFinish}
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
@@ -246,18 +206,14 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
||||
}
|
||||
>
|
||||
{() => {
|
||||
console.log("Updating the owner info section.");
|
||||
//If all of the job ids have been fileld in, then query and update the IP field.
|
||||
const { payments } = form.getFieldsValue();
|
||||
if (payments?.length > 0 && payments?.filter((p) => p?.jobid).length === payments?.length) {
|
||||
console.log("**Calling refetch.");
|
||||
if (
|
||||
payments?.length > 0 &&
|
||||
payments?.filter((p) => p?.jobid).length === payments?.length
|
||||
) {
|
||||
refetch({ jobids: payments.map((p) => p.jobid) });
|
||||
}
|
||||
console.log(
|
||||
"Acc info",
|
||||
data,
|
||||
payments && data && data.jobs.length > 0 ? data.jobs.map((j) => j.ro_number).join(", ") : null
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<Input
|
||||
@@ -300,6 +256,13 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
||||
type="hidden"
|
||||
value={totalAmountToCharge?.toFixed(2)}
|
||||
/>
|
||||
<Input
|
||||
className="ipayfield"
|
||||
data-ipayname="comment"
|
||||
type="hidden"
|
||||
value={btoa(JSON.stringify(payments))}
|
||||
hidden
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
// data-ipayname="submit"
|
||||
@@ -314,11 +277,6 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
|
||||
{/* Lightbox payment response when it is completed */}
|
||||
<Form.Item name="paymentResponse" hidden>
|
||||
<Input type="hidden" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Spin>
|
||||
</Card>
|
||||
|
||||
@@ -11,11 +11,15 @@ import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel
|
||||
import CourtesyCarReadiness from "../courtesy-car-readiness-select/courtesy-car-readiness-select.component";
|
||||
import CourtesyCarStatus from "../courtesy-car-status-select/courtesy-car-status-select.component";
|
||||
import FormDatePicker from "../form-date-picker/form-date-picker.component";
|
||||
//import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
|
||||
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
|
||||
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
|
||||
export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
|
||||
export default function CourtesyCarCreateFormComponent({
|
||||
form,
|
||||
saveLoading,
|
||||
newCC,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const client = useApolloClient();
|
||||
|
||||
@@ -30,7 +34,7 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
|
||||
}
|
||||
/>
|
||||
|
||||
{/* <FormFieldsChanged form={form} /> */}
|
||||
{newCC ? null : <FormFieldsChanged form={form} />}
|
||||
<LayoutFormRow header={t("courtesycars.labels.vehicle")}>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.year")}
|
||||
|
||||
@@ -28,6 +28,7 @@ const CourtesyCarStatusComponent = ({ value, onChange }, ref) => {
|
||||
<Option value="courtesycars.status.out">{t("courtesycars.status.out")}</Option>
|
||||
<Option value="courtesycars.status.sold">{t("courtesycars.status.sold")}</Option>
|
||||
<Option value="courtesycars.status.leasereturn">{t("courtesycars.status.leasereturn")}</Option>
|
||||
<Option value="courtesycars.status.unavailable">{t("courtesycars.status.unavailable")}</Option>
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -61,7 +61,11 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
|
||||
{
|
||||
text: t("courtesycars.status.leasereturn"),
|
||||
value: "courtesycars.status.leasereturn"
|
||||
}
|
||||
},
|
||||
{
|
||||
text: t("courtesycars.status.unavailable"),
|
||||
value: "courtesycars.status.unavailable",
|
||||
},
|
||||
],
|
||||
onFilter: (value, record) => record.status === value,
|
||||
sortOrder: state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Select, Space, Tag } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const { Option } = Select;
|
||||
//To be used as a form element only.
|
||||
|
||||
const EmployeeSearchSelectEmail = ({ options, ...props }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Select
|
||||
showSearch
|
||||
// value={option}
|
||||
style={{
|
||||
width: 400
|
||||
}}
|
||||
optionFilterProp="search"
|
||||
{...props}
|
||||
>
|
||||
{options
|
||||
? options.map((o) => (
|
||||
<Option key={o.id} value={o.user_email} search={`${o.employee_number} ${o.first_name} ${o.last_name}`}>
|
||||
<Space>
|
||||
{`${o.employee_number} ${o.first_name} ${o.last_name}`}
|
||||
|
||||
<Tag color="green">
|
||||
{o.flat_rate ? t("timetickets.labels.flat_rate") : t("timetickets.labels.straight_time")}
|
||||
</Tag>
|
||||
</Space>
|
||||
</Option>
|
||||
))
|
||||
: null}
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
export default EmployeeSearchSelectEmail;
|
||||
@@ -0,0 +1,48 @@
|
||||
import { DatePicker } from "antd";
|
||||
import dayjs from "../../utils/day.js";
|
||||
import React, { useRef } from "react";
|
||||
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(FormDateTimePickerEnhanced);
|
||||
|
||||
const dateFormat = "MM/DD/YYYY h:mm a";
|
||||
|
||||
export function FormDateTimePickerEnhanced({
|
||||
bodyshop,
|
||||
value,
|
||||
onBlur,
|
||||
onlyFuture,
|
||||
onlyToday,
|
||||
isDateOnly = true,
|
||||
...restProps
|
||||
}) {
|
||||
const ref = useRef();
|
||||
return (
|
||||
<div>
|
||||
<DatePicker
|
||||
ref={ref}
|
||||
value={value ? dayjs(value) : null}
|
||||
format={dateFormat}
|
||||
onBlur={onBlur}
|
||||
showToday={false}
|
||||
disabledDate={(d) => {
|
||||
if (onlyToday) {
|
||||
return !dayjs().isSame(d, "day");
|
||||
} else if (onlyFuture) {
|
||||
return dayjs().subtract(1, "day").isAfter(d);
|
||||
}
|
||||
}}
|
||||
{...restProps}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { forwardRef } from "react";
|
||||
//import DatePicker from "react-datepicker";
|
||||
//import "react-datepicker/src/stylesheets/datepicker.scss";
|
||||
import { TimePicker } from "antd";
|
||||
import { Space, TimePicker } from "antd";
|
||||
import dayjs from "../../utils/day";
|
||||
import FormDatePicker from "../form-date-picker/form-date-picker.component";
|
||||
//To be used as a form element only.
|
||||
@@ -14,7 +14,7 @@ const DateTimePicker = ({ value, onChange, onBlur, id, onlyFuture, ...restProps
|
||||
// };
|
||||
|
||||
return (
|
||||
<div id={id}>
|
||||
<Space direction="vertical" style={{ width: "100%" }} id={id}>
|
||||
<FormDatePicker
|
||||
{...restProps}
|
||||
{...(onlyFuture && {
|
||||
@@ -39,7 +39,7 @@ const DateTimePicker = ({ value, onChange, onBlur, id, onlyFuture, ...restProps
|
||||
format="hh:mm a"
|
||||
{...restProps}
|
||||
/>
|
||||
</div>
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import Icon, {
|
||||
LineChartOutlined,
|
||||
PaperClipOutlined,
|
||||
PhoneOutlined,
|
||||
PlusCircleOutlined,
|
||||
QuestionCircleFilled,
|
||||
ScheduleOutlined,
|
||||
SettingOutlined,
|
||||
@@ -30,7 +31,7 @@ import { Layout, Menu, Switch, Tooltip } from "antd";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { BsKanban } from "react-icons/bs";
|
||||
import { FaCalendarAlt, FaCarCrash, FaCreditCard, FaFileInvoiceDollar } from "react-icons/fa";
|
||||
import { FaCalendarAlt, FaCarCrash, FaCreditCard, FaFileInvoiceDollar, FaTasks } from "react-icons/fa";
|
||||
import { GiPayMoney, GiPlayerTime, GiSettingsKnobs } from "react-icons/gi";
|
||||
import { IoBusinessOutline } from "react-icons/io5";
|
||||
import { RiSurveyLine } from "react-icons/ri";
|
||||
@@ -54,12 +55,43 @@ const mapStateToProps = createStructuredSelector({
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setBillEnterContext: (context) => dispatch(setModalContext({ context: context, modal: "billEnter" })),
|
||||
setTimeTicketContext: (context) => dispatch(setModalContext({ context: context, modal: "timeTicket" })),
|
||||
setBillEnterContext: (context) =>
|
||||
dispatch(
|
||||
setModalContext({
|
||||
context: context,
|
||||
modal: "billEnter"
|
||||
})
|
||||
),
|
||||
setTimeTicketContext: (context) =>
|
||||
dispatch(
|
||||
setModalContext({
|
||||
context: context,
|
||||
modal: "timeTicket"
|
||||
})
|
||||
),
|
||||
setPaymentContext: (context) => dispatch(setModalContext({ context: context, modal: "payment" })),
|
||||
setReportCenterContext: (context) => dispatch(setModalContext({ context: context, modal: "reportCenter" })),
|
||||
setReportCenterContext: (context) =>
|
||||
dispatch(
|
||||
setModalContext({
|
||||
context: context,
|
||||
modal: "reportCenter"
|
||||
})
|
||||
),
|
||||
signOutStart: () => dispatch(signOutStart()),
|
||||
setCardPaymentContext: (context) => dispatch(setModalContext({ context: context, modal: "cardPayment" }))
|
||||
setCardPaymentContext: (context) =>
|
||||
dispatch(
|
||||
setModalContext({
|
||||
context: context,
|
||||
modal: "cardPayment"
|
||||
})
|
||||
),
|
||||
setTaskUpsertContext: (context) =>
|
||||
dispatch(
|
||||
setModalContext({
|
||||
context: context,
|
||||
modal: "taskUpsert"
|
||||
})
|
||||
)
|
||||
});
|
||||
|
||||
function Header({
|
||||
@@ -73,7 +105,8 @@ function Header({
|
||||
setPaymentContext,
|
||||
setReportCenterContext,
|
||||
recentItems,
|
||||
setCardPaymentContext
|
||||
setCardPaymentContext,
|
||||
setTaskUpsertContext
|
||||
}) {
|
||||
const {
|
||||
treatments: { ImEXPay, DmsAp, Simple_Inventory }
|
||||
@@ -278,10 +311,12 @@ function Header({
|
||||
{
|
||||
key: "home",
|
||||
icon: <HomeFilled />,
|
||||
id: "header-home",
|
||||
label: <Link to="/manage/">{t("menus.header.home")}</Link>
|
||||
},
|
||||
{
|
||||
key: "schedule",
|
||||
id: "header-schedule",
|
||||
icon: <Icon component={FaCalendarAlt} />,
|
||||
label: <Link to="/manage/schedule">{t("menus.header.schedule")}</Link>
|
||||
},
|
||||
@@ -293,16 +328,19 @@ function Header({
|
||||
children: [
|
||||
{
|
||||
key: "activejobs",
|
||||
id: "header-active-jobs",
|
||||
icon: <FileFilled />,
|
||||
label: <Link to="/manage/jobs">{t("menus.header.activejobs")}</Link>
|
||||
},
|
||||
{
|
||||
key: "readyjobs",
|
||||
id: "header-ready-jobs",
|
||||
icon: <CheckCircleOutlined />,
|
||||
label: <Link to="/manage/jobs/ready">{t("menus.header.readyjobs")}</Link>
|
||||
},
|
||||
{
|
||||
key: "parts-queue",
|
||||
id: "header-parts-queue",
|
||||
icon: <ToolFilled />,
|
||||
label: <Link to="/manage/partsqueue">{t("menus.header.parts-queue")}</Link>
|
||||
},
|
||||
@@ -314,6 +352,7 @@ function Header({
|
||||
},
|
||||
{
|
||||
key: "newjob",
|
||||
id: "header-new-job",
|
||||
icon: <FileAddOutlined />,
|
||||
label: <Link to="/manage/jobs/new">{t("menus.header.newjob")}</Link>
|
||||
},
|
||||
@@ -322,6 +361,7 @@ function Header({
|
||||
},
|
||||
{
|
||||
key: "alljobs",
|
||||
id: "header-all-jobs",
|
||||
icon: <UnorderedListOutlined />,
|
||||
label: <Link to="/manage/jobs/all">{t("menus.header.alljobs")}</Link>
|
||||
},
|
||||
@@ -330,6 +370,7 @@ function Header({
|
||||
},
|
||||
{
|
||||
key: "productionlist",
|
||||
id: "header-production-list",
|
||||
icon: <ScheduleOutlined />,
|
||||
label: <Link to="/manage/production/list">{t("menus.header.productionlist")}</Link>
|
||||
},
|
||||
@@ -341,6 +382,7 @@ function Header({
|
||||
? [
|
||||
{
|
||||
key: "productionboard",
|
||||
id: "header-production-board",
|
||||
icon: <Icon component={BsKanban} />,
|
||||
label: <Link to="/manage/production/board">{t("menus.header.productionboard")}</Link>
|
||||
}
|
||||
@@ -358,6 +400,7 @@ function Header({
|
||||
},
|
||||
{
|
||||
key: "scoreboard",
|
||||
id: "header-scoreboard",
|
||||
icon: <LineChartOutlined />,
|
||||
label: <Link to="/manage/scoreboard">{t("menus.header.scoreboard")}</Link>
|
||||
}
|
||||
@@ -368,15 +411,18 @@ function Header({
|
||||
{
|
||||
key: "customers",
|
||||
icon: <UserOutlined />,
|
||||
id: "header-customers",
|
||||
label: t("menus.header.customers"),
|
||||
children: [
|
||||
{
|
||||
key: "owners",
|
||||
id: "header-owners",
|
||||
icon: <TeamOutlined />,
|
||||
label: <Link to="/manage/owners">{t("menus.header.owners")}</Link>
|
||||
},
|
||||
{
|
||||
key: "vehicles",
|
||||
id: "header-vehicles",
|
||||
icon: <CarFilled />,
|
||||
label: <Link to="/manage/vehicles">{t("menus.header.vehicles")}</Link>
|
||||
}
|
||||
@@ -390,21 +436,25 @@ function Header({
|
||||
? [
|
||||
{
|
||||
key: "ccs",
|
||||
id: "header-css",
|
||||
icon: <CarFilled />,
|
||||
label: t("menus.header.courtesycars"),
|
||||
children: [
|
||||
{
|
||||
key: "courtesycarsall",
|
||||
id: "header-courtesycars-all",
|
||||
icon: <CarFilled />,
|
||||
label: <Link to="/manage/courtesycars">{t("menus.header.courtesycars-all")}</Link>
|
||||
},
|
||||
{
|
||||
key: "contracts",
|
||||
id: "header-contracts",
|
||||
icon: <FileFilled />,
|
||||
label: <Link to="/manage/courtesycars/contracts">{t("menus.header.courtesycars-contracts")}</Link>
|
||||
},
|
||||
{
|
||||
key: "newcontract",
|
||||
id: "header-newcontract",
|
||||
icon: <FileAddFilled />,
|
||||
label: <Link to="/manage/courtesycars/contracts/new">{t("menus.header.courtesycars-newcontract")}</Link>
|
||||
}
|
||||
@@ -417,6 +467,7 @@ function Header({
|
||||
? [
|
||||
{
|
||||
key: "accounting",
|
||||
id: "header-accounting",
|
||||
icon: <DollarCircleFilled />,
|
||||
label: t("menus.header.accounting"),
|
||||
children: accountingChildren
|
||||
@@ -425,6 +476,7 @@ function Header({
|
||||
: []),
|
||||
{
|
||||
key: "phonebook",
|
||||
id: "header-phonebook",
|
||||
icon: <PhoneOutlined />,
|
||||
label: <Link to="/manage/phonebook">{t("menus.header.phonebook")}</Link>
|
||||
},
|
||||
@@ -436,28 +488,62 @@ function Header({
|
||||
? [
|
||||
{
|
||||
key: "temporarydocs",
|
||||
id: "header-temporarydocs",
|
||||
icon: <PaperClipOutlined />,
|
||||
label: <Link to="/manage/temporarydocs">{t("menus.header.temporarydocs")}</Link>
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
key: "tasks",
|
||||
id: "tasks",
|
||||
icon: <FaTasks />,
|
||||
label: t("menus.header.tasks"),
|
||||
children: [
|
||||
{
|
||||
key: "createTask",
|
||||
icon: <PlusCircleOutlined />,
|
||||
label: t("menus.header.create_task"),
|
||||
onClick: () => {
|
||||
setTaskUpsertContext({
|
||||
actions: {},
|
||||
context: {}
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "mytasks",
|
||||
icon: <FaTasks />,
|
||||
label: <Link to="/manage/tasks/mytasks">{t("menus.header.my_tasks")}</Link>
|
||||
},
|
||||
{
|
||||
key: "all_tasks",
|
||||
icon: <FaTasks />,
|
||||
label: <Link to="/manage/tasks/alltasks">{t("menus.header.all_tasks")}</Link>
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: "shopsubmenu",
|
||||
id: "header-shopsubmenu",
|
||||
icon: <SettingOutlined />,
|
||||
label: t("menus.header.shop"),
|
||||
children: [
|
||||
{
|
||||
key: "shop",
|
||||
id: "header-shop",
|
||||
icon: <Icon component={GiSettingsKnobs} />,
|
||||
label: <Link to="/manage/shop?tab=info">{t("menus.header.shop_config")}</Link>
|
||||
},
|
||||
{
|
||||
key: "dashboard",
|
||||
id: "header-dashboard",
|
||||
icon: <DashboardFilled />,
|
||||
label: <Link to="/manage/dashboard">{t("menus.header.dashboard")}</Link>
|
||||
},
|
||||
{
|
||||
key: "reportcenter",
|
||||
id: "header-reportcenter",
|
||||
icon: <BarChartOutlined />,
|
||||
label: t("menus.header.reportcenter"),
|
||||
onClick: () => {
|
||||
@@ -469,6 +555,7 @@ function Header({
|
||||
},
|
||||
{
|
||||
key: "shop-vendors",
|
||||
id: "header-shop-vendors",
|
||||
icon: <Icon component={IoBusinessOutline} />,
|
||||
label: <Link to="/manage/shop/vendors">{t("menus.header.shop_vendors")}</Link>
|
||||
},
|
||||
@@ -480,6 +567,7 @@ function Header({
|
||||
? [
|
||||
{
|
||||
key: "shop-csi",
|
||||
id: "header-shop-csi",
|
||||
icon: <Icon component={RiSurveyLine} />,
|
||||
label: <Link to="/manage/shop/csi">{t("menus.header.shop_csi")}</Link>
|
||||
}
|
||||
@@ -493,6 +581,7 @@ function Header({
|
||||
children: [
|
||||
{
|
||||
key: "signout",
|
||||
id: "header-signout",
|
||||
icon: <Icon component={FiLogOut} />,
|
||||
danger: true,
|
||||
label: t("user.actions.signout"),
|
||||
@@ -500,6 +589,7 @@ function Header({
|
||||
},
|
||||
{
|
||||
key: "help",
|
||||
id: "header-help",
|
||||
icon: <Icon component={QuestionCircleFilled} />,
|
||||
label: t("menus.header.help"),
|
||||
onClick: () => {
|
||||
@@ -531,6 +621,7 @@ function Header({
|
||||
? [
|
||||
{
|
||||
key: "shiftclock",
|
||||
id: "header-shiftclock",
|
||||
icon: <Icon component={GiPlayerTime} />,
|
||||
label: <Link to="/manage/shiftclock">{t("menus.header.shiftclock")}</Link>
|
||||
}
|
||||
@@ -538,6 +629,7 @@ function Header({
|
||||
: []),
|
||||
{
|
||||
key: "profile",
|
||||
id: "header-profile",
|
||||
icon: <UserOutlined />,
|
||||
label: <Link to="/manage/profile">{t("menus.currentuser.profile")}</Link>
|
||||
}
|
||||
@@ -573,6 +665,7 @@ function Header({
|
||||
{
|
||||
key: "recent",
|
||||
icon: <ClockCircleFilled />,
|
||||
id: "header-recent",
|
||||
children: recentItems.map((i, idx) => ({
|
||||
key: idx,
|
||||
label: <Link to={i.url}>{i.label}</Link>
|
||||
@@ -586,6 +679,7 @@ function Header({
|
||||
imex: () => {
|
||||
menuItems.push({
|
||||
key: "beta-switch",
|
||||
id: "header-beta-switch",
|
||||
style: { marginLeft: "auto" },
|
||||
label: (
|
||||
<Tooltip
|
||||
|
||||
@@ -1,17 +1,29 @@
|
||||
import { Card, Col, Row, Space, Statistic, Tooltip, Typography } from "antd";
|
||||
import { Alert, Card, Col, Row, Space, Statistic, Tooltip, Typography } from "antd";
|
||||
import Dinero from "dinero.js";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||
import "./job-bills-total.styles.scss";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
|
||||
export default function JobBillsTotalComponent({ loading, bills, partsOrders, jobTotals }) {
|
||||
export default function JobBillsTotalComponent({
|
||||
loading,
|
||||
bills,
|
||||
partsOrders,
|
||||
jobTotals,
|
||||
showWarning,
|
||||
warningCallback
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (loading) return <LoadingSkeleton />;
|
||||
if (!!!jobTotals) return <AlertComponent type="error" message={t("jobs.errors.nofinancial")} />;
|
||||
if (!!!jobTotals) {
|
||||
if (showWarning && warningCallback && typeof warningCallback === "function") {
|
||||
warningCallback({ key: "bills", warning: t("jobs.errors.nofinancial") });
|
||||
}
|
||||
return <AlertComponent type="error" message={t("jobs.errors.nofinancial")} />;
|
||||
}
|
||||
|
||||
const totals = jobTotals;
|
||||
|
||||
@@ -97,9 +109,21 @@ export default function JobBillsTotalComponent({ loading, bills, partsOrders, jo
|
||||
const discrepWithCms = discrepWithLbrAdj.add(totalReturns);
|
||||
const calculatedCreditsNotReceived = totalReturns.subtract(billCms); //billCms is tracked as a negative number.
|
||||
|
||||
if (showWarning && warningCallback && typeof warningCallback === "function") {
|
||||
if (discrepWithCms.getAmount() !== 0) {
|
||||
warningCallback({
|
||||
key: "bills",
|
||||
warning: t("jobs.labels.outstanding_reconciliation_discrep")
|
||||
});
|
||||
}
|
||||
if (calculatedCreditsNotReceived.getAmount() > 0) {
|
||||
warningCallback({ key: "cm", warning: t("jobs.labels.outstanding_credit_memos") });
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Row gutter={16}>
|
||||
<Col span={18}>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col md={24} lg={18}>
|
||||
<Card title={t("jobs.labels.jobtotals")} style={{ height: "100%" }}>
|
||||
<Space wrap size="large">
|
||||
<Tooltip
|
||||
@@ -204,9 +228,20 @@ export default function JobBillsTotalComponent({ loading, bills, partsOrders, jo
|
||||
/>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
|
||||
{showWarning &&
|
||||
(discrepWithCms.getAmount() !== 0 ||
|
||||
discrepWithLbrAdj.getAmount() !== 0 ||
|
||||
discrepancy.getAmount() !== 0) && (
|
||||
<Alert
|
||||
style={{ margin: "8px 0px" }}
|
||||
type="warning"
|
||||
message={t("jobs.labels.outstanding_reconciliation_discrep")}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Col md={24} lg={6}>
|
||||
<Card title={t("jobs.labels.returntotals")} style={{ height: "100%" }}>
|
||||
<Space wrap>
|
||||
<Tooltip
|
||||
@@ -263,6 +298,9 @@ export default function JobBillsTotalComponent({ loading, bills, partsOrders, jo
|
||||
/>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
{showWarning && calculatedCreditsNotReceived.getAmount() > 0 && (
|
||||
<Alert style={{ margin: "8px 0px" }} type="warning" message={t("jobs.labels.outstanding_credit_memos")} />
|
||||
)}
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import React from "react";
|
||||
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { GET_LINE_TICKET_BY_PK } from "../../graphql/jobs-lines.queries";
|
||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import LaborAllocationsTableComponent from "../labor-allocations-table/labor-allocations-table.component";
|
||||
import PayrollLaborAllocationsTable from "../labor-allocations-table/labor-allocations-table.payroll.component";
|
||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
bodyshop: selectBodyshop,
|
||||
jobRO: selectJobReadOnly
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobCloseRoGuardLabor);
|
||||
|
||||
export function JobCloseRoGuardLabor({ job, jobRO, bodyshop, form, warningCallback }) {
|
||||
const { loading, error, data, refetch } = useQuery(GET_LINE_TICKET_BY_PK, {
|
||||
variables: { id: job.id },
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only"
|
||||
});
|
||||
const {
|
||||
treatments: { Enhanced_Payroll }
|
||||
} = useSplitTreatments({
|
||||
attributes: {},
|
||||
names: ["Enhanced_Payroll"],
|
||||
splitKey: bodyshop.imexshopid
|
||||
});
|
||||
|
||||
if (loading) return <LoadingSkeleton />;
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
|
||||
return Enhanced_Payroll.treatment === "on" ? (
|
||||
<PayrollLaborAllocationsTable
|
||||
jobId={job.id}
|
||||
timetickets={data ? data.timetickets : []}
|
||||
refetch={refetch}
|
||||
adjustments={data ? data.jobs_by_pk.lbr_adjustments : []}
|
||||
showWarning
|
||||
warningCallback={warningCallback}
|
||||
/>
|
||||
) : (
|
||||
<LaborAllocationsTableComponent
|
||||
jobId={job.id}
|
||||
joblines={data ? data.joblines : []}
|
||||
timetickets={data ? data.timetickets : []}
|
||||
refetch={refetch}
|
||||
adjustments={data ? data.jobs_by_pk.lbr_adjustments : []}
|
||||
showWarning
|
||||
warningCallback={warningCallback}
|
||||
disabled={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
import { Alert, Card } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { useMemo } from "react";
|
||||
import Dinero from "dinero.js";
|
||||
import DataLabel from "../data-label/data-label.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
bodyshop: selectBodyshop,
|
||||
jobRO: selectJobReadOnly
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobCloseRoGuardProfit);
|
||||
|
||||
export function JobCloseRoGuardProfit({ job, jobRO, bodyshop, form, warningCallback }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const total = useMemo(() => {
|
||||
return (
|
||||
job.payments &&
|
||||
job.payments.reduce((acc, val) => {
|
||||
acc = acc.add(Dinero({ amount: Math.round(val.amount * 100) }));
|
||||
return acc;
|
||||
}, Dinero())
|
||||
);
|
||||
}, [job.payments]);
|
||||
|
||||
const balance = useMemo(() => {
|
||||
if (job && job.job_totals && job.job_totals.totals.total_repairs)
|
||||
return Dinero(job.job_totals.totals.total_repairs).subtract(total);
|
||||
return Dinero({ amount: 0 }).subtract(total);
|
||||
}, [job, total]);
|
||||
|
||||
useEffect(() => {
|
||||
if (balance.getAmount() !== 0) {
|
||||
warningCallback({ key: "ar", warning: t("jobs.labels.outstanding_ar") });
|
||||
}
|
||||
}, [balance, t, warningCallback]);
|
||||
|
||||
return (
|
||||
<Card title={t("jobs.labels.accountsreceivable")} style={{ height: "100%" }}>
|
||||
<DataLabel label={t("payments.labels.totalpayments")}>{total.toFormat()}</DataLabel>
|
||||
<DataLabel
|
||||
valueStyle={{ color: balance.getAmount() !== 0 ? "red" : "green" }}
|
||||
label={t("payments.labels.balance")}
|
||||
>
|
||||
{balance.toFormat()}
|
||||
</DataLabel>
|
||||
{balance.getAmount() !== 0 && (
|
||||
<Alert style={{ margin: "8px 0px" }} type="warning" message={t("jobs.labels.outstanding_ar")} />
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import React from "react";
|
||||
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { QUERY_BILLS_BY_JOBID } from "../../graphql/bills.queries";
|
||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import JobBillsTotalComponent from "../job-bills-total/job-bills-total.component";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
bodyshop: selectBodyshop,
|
||||
jobRO: selectJobReadOnly
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobCloseRoGuardBills);
|
||||
|
||||
export function JobCloseRoGuardBills({ job, jobRO, bodyshop, form, warningCallback }) {
|
||||
const { loading, error, data } = useQuery(QUERY_BILLS_BY_JOBID, {
|
||||
variables: { jobid: job.id },
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only"
|
||||
});
|
||||
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
|
||||
return (
|
||||
<JobBillsTotalComponent
|
||||
loading={loading}
|
||||
bills={data ? data.bills : []}
|
||||
partsOrders={data ? data.parts_orders : []}
|
||||
jobTotals={job.job_totals}
|
||||
showWarning
|
||||
warningCallback={warningCallback}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { LockOutlined } from "@ant-design/icons";
|
||||
import { Badge, Card, Col, Collapse, Form, Input, Row, Space, Tooltip } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import JobCloseRoGuardLabor from "./job-close-ro-gaurd.labor";
|
||||
import JobCloseRoGuardAr from "./job-close-ro-guard.ar";
|
||||
import JobCloseRoGuardBills from "./job-close-ro-guard.bills";
|
||||
import JobCloseRoGuardPpd from "./job-close-ro-guard.ppd";
|
||||
import JobCloseRoGuardProfit from "./job-close-ro-guard.profit";
|
||||
import "./job-close-ro-guard.styles.scss";
|
||||
import JobCloseRoGuardSublet from "./job-close-ro-guard.sublet";
|
||||
import JobCloseRoGuardTtLifecycle from "./job-close-ro-guard.tt-lifecycle";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
bodyshop: selectBodyshop,
|
||||
jobRO: selectJobReadOnly
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobCloseRoGuardContainer);
|
||||
|
||||
export function JobCloseRoGuardContainer({ job, jobRO, bodyshop, form }) {
|
||||
const { t } = useTranslation();
|
||||
const [warnings, setWarnings] = useState([]);
|
||||
|
||||
const warningCallback = useCallback(
|
||||
({ key, warning }) =>
|
||||
setWarnings((state) => {
|
||||
if (state.find((s) => s.key === key)) return state;
|
||||
return [...state, { key, warning }];
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
if (!bodyshop?.md_ro_guard?.enabled) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{warnings.length > 0 && (
|
||||
<Card
|
||||
title={
|
||||
<Space size="small">
|
||||
<Badge count={warnings.length} />
|
||||
{t("jobs.labels.roguardwarnings")}
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<ul>
|
||||
{warnings.map((w, index) => (
|
||||
<li key={index}>
|
||||
{bodyshop.md_ro_guard[`enforce_${w.key}`] && (
|
||||
<Tooltip title={t("jobs.labels.ro_guard.enforced")}>
|
||||
<LockOutlined style={{ color: "tomato", marginRight: "8px" }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
{w.warning}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Collapse>
|
||||
<Collapse.Panel forceRender key="roguard" header={t("jobs.labels.roguard")}>
|
||||
<Row gutter={[32, 32]}>
|
||||
<Col span={24}>
|
||||
<JobCloseRoGuardBills job={job} form={form} warningCallback={warningCallback} />
|
||||
</Col>
|
||||
<Col className="ro-guard-col-multiple" md={24} lg={6}>
|
||||
<Row gutter={[32, 32]} style={{ height: "100%" }}>
|
||||
<Col span={24}>
|
||||
<JobCloseRoGuardProfit job={job} form={form} warningCallback={warningCallback} />
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<JobCloseRoGuardAr job={job} form={form} warningCallback={warningCallback} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
{InstanceRenderManager({
|
||||
rome: (
|
||||
<Col md={24} lg={8}>
|
||||
{/* <JobCloseRoGuardSublet job={job} warningCallback={warningCallback} /> */}
|
||||
<JobCloseRoGuardPpd job={job} warningCallback={warningCallback} />
|
||||
</Col>
|
||||
)
|
||||
})}
|
||||
<Col className="ro-guard-col" md={24} lg={10}>
|
||||
<JobCloseRoGuardLabor job={job} form={form} warningCallback={warningCallback} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item
|
||||
name="masterbypass"
|
||||
label={t("jobs.labels.masterbypass")}
|
||||
rules={[
|
||||
({ getFieldValue }) => ({
|
||||
validator(_, value) {
|
||||
if (
|
||||
!PasswordCheck({ bodyshop, value }) &&
|
||||
bodyshop.md_ro_guard.enforce_bills &&
|
||||
warnings.find((w) => w.key === "bills")
|
||||
) {
|
||||
return Promise.reject(
|
||||
t("jobs.labels.ro_guard.enforce_validation", {
|
||||
message: t("jobs.labels.ro_guard.enforce_bills")
|
||||
})
|
||||
);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
}),
|
||||
({ getFieldValue }) => ({
|
||||
validator(_, value) {
|
||||
if (
|
||||
!PasswordCheck({ bodyshop, value }) &&
|
||||
bodyshop.md_ro_guard.enforce_cm &&
|
||||
warnings.find((w) => w.key === "cm")
|
||||
) {
|
||||
return Promise.reject(
|
||||
t("jobs.labels.ro_guard.enforce_validation", {
|
||||
message: t("jobs.labels.ro_guard.enforce_cm")
|
||||
})
|
||||
);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
}),
|
||||
({ getFieldValue }) => ({
|
||||
validator(_, value) {
|
||||
if (
|
||||
!PasswordCheck({ bodyshop, value }) &&
|
||||
bodyshop.md_ro_guard.enforce_profit &&
|
||||
warnings.find((w) => w.key === "profit")
|
||||
) {
|
||||
return Promise.reject(
|
||||
t("jobs.labels.ro_guard.enforce_validation", {
|
||||
message: t("jobs.labels.ro_guard.enforce_profit")
|
||||
})
|
||||
);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
}),
|
||||
({ getFieldValue }) => ({
|
||||
validator(_, value) {
|
||||
if (
|
||||
!PasswordCheck({ bodyshop, value }) &&
|
||||
bodyshop.md_ro_guard.enforce_ar &&
|
||||
warnings.find((w) => w.key === "ar")
|
||||
) {
|
||||
return Promise.reject(
|
||||
t("jobs.labels.ro_guard.enforce_validation", {
|
||||
message: t("jobs.labels.ro_guard.enforce_ar")
|
||||
})
|
||||
);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
}),
|
||||
({ getFieldValue }) => ({
|
||||
validator(_, value) {
|
||||
if (
|
||||
!PasswordCheck({ bodyshop, value }) &&
|
||||
bodyshop.md_ro_guard.enforce_sublet &&
|
||||
warnings.find((w) => w.key === "sublet")
|
||||
) {
|
||||
return Promise.reject(
|
||||
t("jobs.labels.ro_guard.enforce_validation", {
|
||||
message: t("jobs.labels.ro_guard.enforce_sublet")
|
||||
})
|
||||
);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
}),
|
||||
({ getFieldValue }) => ({
|
||||
validator(_, value) {
|
||||
if (
|
||||
!PasswordCheck({ bodyshop, value }) &&
|
||||
bodyshop.md_ro_guard.enforce_ppd &&
|
||||
warnings.find((w) => w.key === "ppd")
|
||||
) {
|
||||
return Promise.reject(
|
||||
t("jobs.labels.ro_guard.enforce_validation", {
|
||||
message: t("jobs.labels.ro_guard.enforce_ppd")
|
||||
})
|
||||
);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
}),
|
||||
({ getFieldValue }) => ({
|
||||
validator(_, value) {
|
||||
if (
|
||||
!PasswordCheck({ bodyshop, value }) &&
|
||||
bodyshop.md_ro_guard.enforce_labor &&
|
||||
warnings.find((w) => w.key === "labor")
|
||||
) {
|
||||
return Promise.reject(
|
||||
t("jobs.labels.ro_guard.enforce_validation", {
|
||||
message: t("jobs.labels.ro_guard.enforce_labor")
|
||||
})
|
||||
);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
})
|
||||
]}
|
||||
>
|
||||
<Input prefix={<LockOutlined />} type="password" placeholder="Password" disabled={jobRO} />
|
||||
</Form.Item>
|
||||
</Collapse.Panel>
|
||||
|
||||
<Collapse.Panel header={t("jobs.labels.performance")}>
|
||||
<Row gutter={[32, 32]}>
|
||||
<Col className="ro-guard-col" span={24}>
|
||||
<JobCloseRoGuardTtLifecycle job={job} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Collapse.Panel>
|
||||
</Collapse>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function PasswordCheck({ bodyshop, value }) {
|
||||
return value === bodyshop?.md_ro_guard?.masterbypass;
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
import { Alert, Card, Table } from "antd";
|
||||
import { t } from "i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
bodyshop: selectBodyshop,
|
||||
jobRO: selectJobReadOnly
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobCloseRGuardPpd);
|
||||
|
||||
export function JobCloseRGuardPpd({ job, jobRO, bodyshop, form, warningCallback }) {
|
||||
const linesWithPPD = job?.joblines.filter((j) => j.act_price_before_ppc !== 0 && j.act_price_before_ppc !== null);
|
||||
|
||||
useEffect(() => {
|
||||
if (linesWithPPD.length > 0) {
|
||||
warningCallback({ key: "ppd", warning: t("jobs.labels.outstanding_sublets") });
|
||||
}
|
||||
}, [linesWithPPD.length, warningCallback]);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t("joblines.fields.line_desc"),
|
||||
dataIndex: "line_desc",
|
||||
fixed: "left",
|
||||
key: "line_desc",
|
||||
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
|
||||
onCell: (record) => ({
|
||||
className: record.manual_line && "job-line-manual",
|
||||
style: {
|
||||
...(record.critical ? { boxShadow: " -.5em 0 0 #FFC107" } : {})
|
||||
}
|
||||
}),
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: t("joblines.fields.act_price"),
|
||||
dataIndex: "act_price",
|
||||
key: "act_price",
|
||||
sorter: (a, b) => a.act_price - b.act_price,
|
||||
|
||||
ellipsis: true,
|
||||
render: (text, record) => <CurrencyFormatter>{record.act_price}</CurrencyFormatter>
|
||||
},
|
||||
{
|
||||
title: t("joblines.fields.act_price_before_ppc"),
|
||||
dataIndex: "act_price_before_ppc",
|
||||
key: "act_price_before_ppc",
|
||||
sorter: (a, b) => a.act_price_before_ppc - b.act_price_before_ppc,
|
||||
|
||||
ellipsis: true,
|
||||
render: (text, record) => <CurrencyFormatter>{record.act_price_before_ppc}</CurrencyFormatter>
|
||||
},
|
||||
{
|
||||
title: t("joblines.fields.part_qty"),
|
||||
dataIndex: "part_qty",
|
||||
key: "part_qty"
|
||||
},
|
||||
{
|
||||
title: t("joblines.fields.notes"),
|
||||
dataIndex: "notes",
|
||||
key: "notes"
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<Card title={t("jobs.labels.ppdnotexported")}>
|
||||
<Table dataSource={linesWithPPD} columns={columns} pagination={false} rowKey="id" bordered size="small" />
|
||||
{linesWithPPD.length > 0 && (
|
||||
<Alert style={{ margin: "8px 0px" }} type="warning" message={t("jobs.labels.outstanding_ppd")} />
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import { Alert, Card } from "antd";
|
||||
import axios from "axios";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import JobCostingStatistics from "../job-costing-statistics/job-costing-statistics.component";
|
||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
bodyshop: selectBodyshop,
|
||||
jobRO: selectJobReadOnly
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobCloseRoGuardProfit);
|
||||
|
||||
export function JobCloseRoGuardProfit({ job, jobRO, bodyshop, form, warningCallback }) {
|
||||
const [costingData, setCostingData] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
async function getData() {
|
||||
try {
|
||||
if (job.id) {
|
||||
setLoading(true);
|
||||
const { data } = await axios.post("/job/costing", { jobid: job.id });
|
||||
setCostingData(data);
|
||||
}
|
||||
} catch (error) {}
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
getData();
|
||||
}, [job.id]);
|
||||
|
||||
const enforceProfitPassword =
|
||||
parseFloat(costingData?.summaryData.gppercent) < bodyshop?.md_ro_guard?.totalgppercent_minimum;
|
||||
|
||||
useEffect(() => {
|
||||
if (enforceProfitPassword && typeof warningCallback === "function") {
|
||||
warningCallback({ key: "profit", warning: t("jobs.labels.profitbypassrequired") });
|
||||
}
|
||||
}, [enforceProfitPassword, t, warningCallback]);
|
||||
|
||||
if (loading || !costingData) return <LoadingSkeleton />;
|
||||
|
||||
return (
|
||||
<Card title={t("jobs.labels.profits")} style={{ height: "100%" }}>
|
||||
<JobCostingStatistics summaryData={costingData?.summaryData} onlyGP />
|
||||
{enforceProfitPassword && (
|
||||
<Alert style={{ margin: "8px 0px" }} type="warning" message={t("jobs.labels.profitbypassrequired")} />
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
.ro-guard-col {
|
||||
.ant-card {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
.ro-guard-col-50 {
|
||||
.ant-card {
|
||||
height: 50%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
import { Alert, Card, Table } from "antd";
|
||||
import { t } from "i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
bodyshop: selectBodyshop,
|
||||
jobRO: selectJobReadOnly
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobCloseRGuardSublet);
|
||||
|
||||
export function JobCloseRGuardSublet({ job, jobRO, bodyshop, form, warningCallback }) {
|
||||
const subletsNotDone = job?.joblines.filter(
|
||||
(j) => (j.part_type === "PAS" || j.part_type === "PASL") && (!j.sublet_completed || !j.sublet_ignored)
|
||||
);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t("joblines.fields.line_desc"),
|
||||
dataIndex: "line_desc",
|
||||
fixed: "left",
|
||||
key: "line_desc",
|
||||
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
|
||||
onCell: (record) => ({
|
||||
className: record.manual_line && "job-line-manual",
|
||||
style: {
|
||||
...(record.critical ? { boxShadow: " -.5em 0 0 #FFC107" } : {})
|
||||
}
|
||||
}),
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: t("joblines.fields.act_price"),
|
||||
dataIndex: "act_price",
|
||||
key: "act_price",
|
||||
sorter: (a, b) => a.act_price - b.act_price,
|
||||
|
||||
ellipsis: true,
|
||||
render: (text, record) => <CurrencyFormatter>{record.act_price}</CurrencyFormatter>
|
||||
},
|
||||
{
|
||||
title: t("joblines.fields.part_qty"),
|
||||
dataIndex: "part_qty",
|
||||
key: "part_qty"
|
||||
},
|
||||
{
|
||||
title: t("joblines.fields.notes"),
|
||||
dataIndex: "notes",
|
||||
key: "notes"
|
||||
}
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
if (subletsNotDone.length > 0) {
|
||||
warningCallback({ key: "sublet", warning: t("jobs.labels.outstanding_sublets") });
|
||||
}
|
||||
}, [subletsNotDone.length, warningCallback]);
|
||||
|
||||
return (
|
||||
<Card title={t("jobs.labels.subletsnotcompleted")}>
|
||||
<Table dataSource={subletsNotDone} columns={columns} pagination={false} rowKey="id" bordered size="small" />
|
||||
{subletsNotDone.length > 0 && (
|
||||
<Alert style={{ margin: "8px 0px" }} type="warning" message={t("jobs.labels.outstanding_sublets")} />
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { createStructuredSelector } from 'reselect';
|
||||
import { selectJobReadOnly } from '../../redux/application/application.selectors';
|
||||
import { selectBodyshop } from '../../redux/user/user.selectors';
|
||||
import JobTotalsTableTotals from '../job-totals-table/job-totals.table.totals.component';
|
||||
import { Card } from 'antd';
|
||||
import { t } from 'i18next';
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
bodyshop: selectBodyshop,
|
||||
jobRO: selectJobReadOnly,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobCloseRGuardTotals);
|
||||
|
||||
export function JobCloseRGuardTotals({ job, jobRO, bodyshop, form }) {
|
||||
return (
|
||||
<Card title={t("jobs.labels.cards.totals")}>
|
||||
<JobTotalsTableTotals job={job} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import React from "react";
|
||||
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import JobLifecycleComponent from "../job-lifecycle/job-lifecycle.component";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
bodyshop: selectBodyshop,
|
||||
jobRO: selectJobReadOnly
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobCloseRoGuardTTLifeCycle);
|
||||
|
||||
export function JobCloseRoGuardTTLifeCycle({ job, jobRO, bodyshop, form }) {
|
||||
return (
|
||||
<div>
|
||||
<JobLifecycleComponent job={job} statuses={bodyshop.md_ro_statuses} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,35 @@
|
||||
import { Statistic } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Dinero from "dinero.js";
|
||||
import { Space, Statistic } from 'antd';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Dinero from 'dinero.js';
|
||||
|
||||
export default function JobCostingStatistics({ summaryData }) {
|
||||
export default function JobCostingStatistics({ summaryData, onlyGP }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const gpTotals = (
|
||||
<>
|
||||
<Space wrap direction="horizontal" size={'large'}>
|
||||
<Statistic
|
||||
value={Dinero(summaryData.totalSales).toFormat()}
|
||||
title={t('jobs.labels.total_sales')}
|
||||
/>
|
||||
<Statistic
|
||||
value={Dinero(summaryData.totalCost).toFormat()}
|
||||
title={t('jobs.labels.total_cost')}
|
||||
/>
|
||||
</Space>{' '}
|
||||
<Space wrap direction="horizontal" size={'large'}>
|
||||
<Statistic
|
||||
value={Dinero(summaryData.gpdollars).toFormat()}
|
||||
title={t('jobs.labels.gpdollars')}
|
||||
/>
|
||||
<Statistic value={summaryData.gppercentFormatted} title={t('jobs.labels.gppercent')} />
|
||||
</Space>
|
||||
</>
|
||||
);
|
||||
|
||||
if (onlyGP) return gpTotals;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="imex-flex-row imex-flex-row__flex-space-around">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { Col, Divider, Row, Skeleton, Space, Timeline, Typography } from "antd";
|
||||
import { Col, Row, Skeleton, Space, Timeline, Typography } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
@@ -7,17 +7,19 @@ import { GET_JOB_LINE_ORDERS } from "../../graphql/jobs.queries";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { QUERY_JOBLINE_TASKS_PAGINATED } from "../../graphql/tasks.queries.js";
|
||||
import TaskListContainer from "../task-list/task-list.container.jsx";
|
||||
import FeatureWrapper from "../feature-wrapper/feature-wrapper.component.jsx";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobLinesExpander);
|
||||
|
||||
export function JobLinesExpander({ jobline, jobid, bodyshop }) {
|
||||
@@ -43,13 +45,25 @@ export function JobLinesExpander({ jobline, jobid, bodyshop }) {
|
||||
? data.parts_order_lines.map((line) => ({
|
||||
key: line.id,
|
||||
children: (
|
||||
<Space split={<Divider type="vertical" />} wrap>
|
||||
<Link to={`/manage/jobs/${jobid}?partsorderid=${line.parts_order.id}`}>
|
||||
{line.parts_order.order_number}
|
||||
</Link>
|
||||
<DateFormatter>{line.parts_order.order_date}</DateFormatter>
|
||||
{line.parts_order.vendor.name}
|
||||
</Space>
|
||||
<Row wrap>
|
||||
<Col span={4}>
|
||||
<Link to={`/manage/jobs/${jobid}?partsorderid=${line.parts_order.id}`}>
|
||||
{line.parts_order.order_number}
|
||||
</Link>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<DateFormatter>{line.parts_order.order_date}</DateFormatter>
|
||||
</Col>
|
||||
<Col span={4}>{line.parts_order.vendor.name}</Col>
|
||||
{line.backordered_eta ? (
|
||||
<Col span={4}>
|
||||
<span>
|
||||
{`${t("parts_orders.fields.backordered_eta")}: `}
|
||||
<DateFormatter>{line.backordered_eta}</DateFormatter>
|
||||
</span>
|
||||
</Col>
|
||||
) : null}
|
||||
</Row>
|
||||
)
|
||||
}))
|
||||
: [
|
||||
@@ -61,48 +75,6 @@ export function JobLinesExpander({ jobline, jobid, bodyshop }) {
|
||||
}
|
||||
/>{" "}
|
||||
</Col>
|
||||
<Col md={24} lg={8}>
|
||||
<Typography.Title level={4}>{t("bills.labels.bills")}</Typography.Title>
|
||||
<Timeline
|
||||
items={
|
||||
data.billlines.length > 0
|
||||
? data.billlines.map((line) => ({
|
||||
key: line.id,
|
||||
children: (
|
||||
<Row wrap>
|
||||
<Col span={4}>
|
||||
<Link to={`/manage/jobs/${jobid}?tab=partssublet&billid=${line.bill.id}`}>
|
||||
{line.bill.invoice_number}
|
||||
</Link>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<span>
|
||||
{`${t("billlines.fields.actual_price")}: `}
|
||||
<CurrencyFormatter>{line.actual_price}</CurrencyFormatter>
|
||||
</span>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<span>
|
||||
{`${t("billlines.fields.actual_cost")}: `}
|
||||
<CurrencyFormatter>{line.actual_cost}</CurrencyFormatter>
|
||||
</span>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<DateFormatter>{line.bill.date}</DateFormatter>
|
||||
</Col>
|
||||
<Col span={4}> {line.bill.vendor.name}</Col>
|
||||
</Row>
|
||||
)
|
||||
}))
|
||||
: [
|
||||
{
|
||||
key: "no-orders",
|
||||
children: t("parts_orders.labels.notyetordered")
|
||||
}
|
||||
]
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
<Col md={24} lg={8}>
|
||||
<Typography.Title level={4}>{t("parts_dispatch.labels.parts_dispatch")}</Typography.Title>
|
||||
<Timeline
|
||||
@@ -111,23 +83,84 @@ export function JobLinesExpander({ jobline, jobid, bodyshop }) {
|
||||
? data.parts_dispatch_lines.map((line) => ({
|
||||
key: line.id,
|
||||
children: (
|
||||
<Space split={<Divider type="vertical" />} wrap>
|
||||
<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}
|
||||
<Space>
|
||||
{t("parts_dispatch_lines.fields.accepted_at")}
|
||||
<DateFormatter>{line.accepted_at}</DateFormatter>
|
||||
</Space>
|
||||
</Space>
|
||||
<Row>
|
||||
<Col span={8}>
|
||||
<Link to={`/manage/jobs/${jobid}?partsorderid=${line.id}`}>{line.parts_dispatch.number}</Link>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
{bodyshop.employees.find((e) => e.id === line.parts_dispatch.employeeid)?.first_name}
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Space>
|
||||
{t("parts_dispatch_lines.fields.accepted_at")}
|
||||
<DateFormatter>{line.accepted_at}</DateFormatter>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
}))
|
||||
: {
|
||||
key: "dispatch-lines",
|
||||
children: t("parts_orders.labels.notyetordered")
|
||||
}
|
||||
: [
|
||||
{
|
||||
key: "dispatch-lines",
|
||||
children: t("parts_dispatch.labels.notyetdispatched")
|
||||
}
|
||||
]
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
<FeatureWrapper featureName="bills" noauth={() => null}>
|
||||
<Col md={24} lg={8}>
|
||||
<Typography.Title level={4}>{t("bills.labels.bills")}</Typography.Title>
|
||||
<Timeline
|
||||
items={
|
||||
data.billlines.length > 0
|
||||
? data.billlines.map((line) => ({
|
||||
key: line.id,
|
||||
children: (
|
||||
<Row wrap>
|
||||
<Col span={4}>
|
||||
<Link to={`/manage/jobs/${jobid}?tab=partssublet&billid=${line.bill.id}`}>
|
||||
{line.bill.invoice_number}
|
||||
</Link>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<span>
|
||||
{`${t("billlines.fields.actual_price")}: `}
|
||||
<CurrencyFormatter>{line.actual_price}</CurrencyFormatter>
|
||||
</span>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<span>
|
||||
{`${t("billlines.fields.actual_cost")}: `}
|
||||
<CurrencyFormatter>{line.actual_cost}</CurrencyFormatter>
|
||||
</span>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<DateFormatter>{line.bill.date}</DateFormatter>
|
||||
</Col>
|
||||
<Col span={4}> {line.bill.vendor.name}</Col>
|
||||
</Row>
|
||||
)
|
||||
}))
|
||||
: [
|
||||
{
|
||||
key: "no-orders",
|
||||
children: t("bills.labels.nobilllines")
|
||||
}
|
||||
]
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
</FeatureWrapper>
|
||||
<Col md={24} lg={24}>
|
||||
<TaskListContainer
|
||||
parentJobId={jobid}
|
||||
relationshipType={"joblineid"}
|
||||
relationshipId={jobline.id}
|
||||
query={{ QUERY_JOBLINE_TASKS_PAGINATED }}
|
||||
titleTranslation="tasks.titles.job_tasks"
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -42,6 +42,8 @@ import JobSendPartPriceChangeComponent from "../job-send-parts-price-change/job-
|
||||
import PartsOrderModalContainer from "../parts-order-modal/parts-order-modal.container";
|
||||
import JobLinesExpander from "./job-lines-expander.component";
|
||||
import JobLinesPartPriceChange from "./job-lines-part-price-change.component";
|
||||
import { FaTasks } from "react-icons/fa";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -52,7 +54,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setJobLineEditContext: (context) => dispatch(setModalContext({ context: context, modal: "jobLineEdit" })),
|
||||
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" }))
|
||||
});
|
||||
|
||||
export function JobLinesComponent({
|
||||
@@ -67,7 +70,8 @@ export function JobLinesComponent({
|
||||
job,
|
||||
setJobLineEditContext,
|
||||
form,
|
||||
setBillEnterContext
|
||||
setBillEnterContext,
|
||||
setTaskUpsertContext
|
||||
}) {
|
||||
const [deleteJobLine] = useMutation(DELETE_JOB_LINE_BY_PK);
|
||||
const {
|
||||
@@ -331,6 +335,24 @@ export function JobLinesComponent({
|
||||
>
|
||||
<EditFilled />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Button
|
||||
title={t("tasks.buttons.create")}
|
||||
onClick={() => {
|
||||
setTaskUpsertContext({
|
||||
context: {
|
||||
jobid: job.id,
|
||||
joblineid: record.id
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<FaTasks />
|
||||
</Button>
|
||||
{(record.manual_line || jobIsPrivate) && (
|
||||
<>
|
||||
<Button
|
||||
disabled={jobRO}
|
||||
onClick={async () => {
|
||||
@@ -452,7 +474,7 @@ export function JobLinesComponent({
|
||||
vendorid: bodyshop.inhousevendorid,
|
||||
invoice_number: "ih",
|
||||
isinhouse: true,
|
||||
date: new dayjs(),
|
||||
date: dayjs(),
|
||||
total: 0,
|
||||
billlines: selectedLines.map((p) => {
|
||||
return {
|
||||
@@ -531,7 +553,7 @@ export function JobLinesComponent({
|
||||
>
|
||||
{t("joblines.actions.new")}
|
||||
</Button>
|
||||
{bodyshop.region_config.toLowerCase().startsWith("us") && <JobSendPartPriceChangeComponent job={job} />}
|
||||
{InstanceRenderManager({ rome: <JobSendPartPriceChangeComponent job={job} /> })}
|
||||
<JobCreateIOU job={job} selectedJobLines={selectedLines} />
|
||||
<Input.Search
|
||||
placeholder={t("general.labels.search")}
|
||||
|
||||
@@ -154,7 +154,7 @@ export function JobLineConvertToLabor({ children, jobline, job, insertAuditTrail
|
||||
setLoading(true);
|
||||
|
||||
form.setFieldsValue({
|
||||
// date: new dayjs(),
|
||||
// date: dayjs(),
|
||||
// bodyhrs: Math.round(v.bodyhrs * 10) / 10,
|
||||
// painthrs: Math.round(v.painthrs * 10) / 10,
|
||||
});
|
||||
|
||||
@@ -133,6 +133,7 @@ export function JobPayments({
|
||||
}
|
||||
];
|
||||
|
||||
//Same as in RO guard. If changed, update in both.
|
||||
const total = useMemo(() => {
|
||||
return (
|
||||
job.payments &&
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { LoadingOutlined } from "@ant-design/icons";
|
||||
import { useLazyQuery } from "@apollo/client";
|
||||
import { Select, Space, Spin, Tag } from "antd";
|
||||
import _ from "lodash";
|
||||
@@ -6,8 +7,6 @@ import { useTranslation } from "react-i18next";
|
||||
import { SEARCH_JOBS_BY_ID_FOR_AUTOCOMPLETE, SEARCH_JOBS_FOR_AUTOCOMPLETE } from "../../graphql/jobs.queries";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||
import { SearchOutlined } from "@ant-design/icons";
|
||||
import { LoadingOutlined } from "@ant-design/icons";
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
@@ -75,7 +74,7 @@ const JobSearchSelect = (
|
||||
filterOption={false}
|
||||
onSearch={handleSearch}
|
||||
//loading={loading || idLoading}
|
||||
suffixIcon={loading && <Spin />}
|
||||
suffixIcon={(loading || idLoading) && <Spin />}
|
||||
notFoundContent={loading ? <LoadingOutlined /> : null}
|
||||
{...restProps}
|
||||
>
|
||||
@@ -86,9 +85,9 @@ const JobSearchSelect = (
|
||||
<span>
|
||||
{`${clm_no && o.clm_no ? `${o.clm_no} | ` : ""}${
|
||||
o.ro_number || t("general.labels.na")
|
||||
} | ${OwnerNameDisplayFunction(o)} | ${
|
||||
o.v_model_yr || ""
|
||||
} ${o.v_make_desc || ""} ${o.v_model_desc || ""}`}
|
||||
} | ${OwnerNameDisplayFunction(o)} | ${o.v_model_yr || ""} ${o.v_make_desc || ""} ${
|
||||
o.v_model_desc || ""
|
||||
}`}
|
||||
</span>
|
||||
<Tag>
|
||||
<strong>{o.status}</strong>
|
||||
|
||||
@@ -165,6 +165,8 @@ export function JobTotalsTableTotals({ bodyshop, job }) {
|
||||
bold: true
|
||||
}
|
||||
];
|
||||
// TODO: was removed by Patrick during a CI bug fix.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [job.job_totals, job.cieca_pft, t, bodyshop.md_responsibility_centers]);
|
||||
|
||||
const columns = [
|
||||
|
||||
@@ -15,23 +15,43 @@ export const GetSupplementDelta = async (client, jobId, newLines) => {
|
||||
const linesToUpdate = [];
|
||||
|
||||
newLines.forEach((newLine) => {
|
||||
const matchingIndex = existingLines.findIndex((eL) => eL.unq_seq === newLine.unq_seq);
|
||||
const matchingIndexLines = [];
|
||||
existingLines.forEach((eL, index) => {
|
||||
if (eL.unq_seq === newLine.unq_seq) {
|
||||
matchingIndexLines.push({ index, record: eL });
|
||||
}
|
||||
});
|
||||
|
||||
//Should do a check to make sure there is only 1 matching unq sequence number.
|
||||
|
||||
if (matchingIndex >= 0) {
|
||||
//Found a relevant matching line. Add it to lines to update.
|
||||
linesToUpdate.push({
|
||||
id: existingLines[matchingIndex].id,
|
||||
newData: { ...newLine, removed: false, act_price_before_ppc: null }
|
||||
});
|
||||
|
||||
//Splice out item we found for performance.
|
||||
existingLines.splice(matchingIndex, 1);
|
||||
} else {
|
||||
if (matchingIndexLines.length === 0) {
|
||||
//Didn't find a match. Must be a new line.
|
||||
linesToInsert.push(newLine);
|
||||
}
|
||||
//If we find only 1, we can use the old logic.
|
||||
else if (matchingIndexLines.length === 1) {
|
||||
//Found a relevant matching line. Add it to lines to update.
|
||||
linesToUpdate.push({
|
||||
id: matchingIndexLines[0].record.id,
|
||||
newData: { ...newLine, removed: false, act_price_before_ppc: null }
|
||||
});
|
||||
//Splice out item we found for performance.
|
||||
existingLines.splice(matchingIndexLines[0].index, 1);
|
||||
} else if (matchingIndexLines.length === 2) {
|
||||
//if we find 2, we need to separate it out by the non-refinish lines and splice one out.
|
||||
//Find the mathcing one depending on whether this is the refiniish one or not.
|
||||
const matchingLine = matchingIndexLines.find((i) =>
|
||||
newLine.mod_lbr_ty === "LAR" ? i.record.mod_lbr_ty === "LAR" : i.record.mod_lbr_ty !== "LAR"
|
||||
);
|
||||
|
||||
linesToUpdate.push({
|
||||
id: matchingLine.record.id,
|
||||
newData: { ...newLine, removed: false, act_price_before_ppc: null }
|
||||
});
|
||||
//Splice out item we found for performance.
|
||||
existingLines.splice(matchingLine.index, 1);
|
||||
} else {
|
||||
//We found more than 2 matching lines. We should never get here. Throw a warning and back out!
|
||||
throw new Error("Too many matching lines found. Ensure EMS file is valid.");
|
||||
}
|
||||
});
|
||||
|
||||
//Wahtever is left in the existing lines, are lines that should be removed.
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { DownCircleFilled } from "@ant-design/icons";
|
||||
import { useApolloClient, useMutation } from "@apollo/client";
|
||||
import { Button, Card, Dropdown, Form, Input, Modal, notification, Popconfirm, Popover, Select, Space } from "antd";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import { Button, Card, Dropdown, Form, Input, Modal, Popconfirm, Popover, Select, Space, notification } from "antd";
|
||||
import axios from "axios";
|
||||
import parsePhoneNumber from "libphonenumber-js";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
@@ -8,27 +11,24 @@ 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 { GET_CURRENT_QUESTIONSET_ID, INSERT_CSI } from "../../graphql/csi.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 { setEmailOptions } from "../../redux/email/email.actions";
|
||||
import { openChatByPhone, setMessage } from "../../redux/messaging/messaging.actions";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import dayjs from "../../utils/day";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
|
||||
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
|
||||
import AddToProduction from "./jobs-detail-header-actions.addtoproduction.util";
|
||||
import DuplicateJob from "./jobs-detail-header-actions.duplicate.util";
|
||||
import axios from "axios";
|
||||
import { setEmailOptions } from "../../redux/email/email.actions";
|
||||
import { openChatByPhone, setMessage } from "../../redux/messaging/messaging.actions";
|
||||
import { GET_CURRENT_QUESTIONSET_ID, INSERT_CSI } from "../../graphql/csi.queries";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import parsePhoneNumber from "libphonenumber-js";
|
||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
|
||||
import dayjs from "../../utils/day";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import JobsDetailHeaderActionsToggleProduction from "./jobs-detail-header-actions.toggle-production";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
@@ -39,13 +39,57 @@ const mapStateToProps = createStructuredSelector({
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setScheduleContext: (context) => dispatch(setModalContext({ context: context, modal: "schedule" })),
|
||||
setBillEnterContext: (context) => dispatch(setModalContext({ context: context, modal: "billEnter" })),
|
||||
setBillEnterContext: (context) =>
|
||||
dispatch(
|
||||
setModalContext({
|
||||
context: context,
|
||||
modal: "billEnter"
|
||||
})
|
||||
),
|
||||
setPaymentContext: (context) => dispatch(setModalContext({ context: context, modal: "payment" })),
|
||||
setJobCostingContext: (context) => dispatch(setModalContext({ context: context, modal: "jobCosting" })),
|
||||
setTimeTicketContext: (context) => dispatch(setModalContext({ context: context, modal: "timeTicket" })),
|
||||
setCardPaymentContext: (context) => dispatch(setModalContext({ context: context, modal: "cardPayment" })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
setTimeTicketTaskContext: (context) => dispatch(setModalContext({ context: context, modal: "timeTicketTask" })),
|
||||
setJobCostingContext: (context) =>
|
||||
dispatch(
|
||||
setModalContext({
|
||||
context: context,
|
||||
modal: "jobCosting"
|
||||
})
|
||||
),
|
||||
setTimeTicketContext: (context) =>
|
||||
dispatch(
|
||||
setModalContext({
|
||||
context: context,
|
||||
modal: "timeTicket"
|
||||
})
|
||||
),
|
||||
setCardPaymentContext: (context) =>
|
||||
dispatch(
|
||||
setModalContext({
|
||||
context: context,
|
||||
modal: "cardPayment"
|
||||
})
|
||||
),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(
|
||||
insertAuditTrail({
|
||||
jobid,
|
||||
operation,
|
||||
type
|
||||
})
|
||||
),
|
||||
setTimeTicketTaskContext: (context) =>
|
||||
dispatch(
|
||||
setModalContext({
|
||||
context: context,
|
||||
modal: "timeTicketTask"
|
||||
})
|
||||
),
|
||||
setTaskUpsertContext: (context) =>
|
||||
dispatch(
|
||||
setModalContext({
|
||||
context: context,
|
||||
modal: "taskUpsert"
|
||||
})
|
||||
),
|
||||
setEmailOptions: (e) => dispatch(setEmailOptions(e)),
|
||||
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
|
||||
setMessage: (text) => dispatch(setMessage(text))
|
||||
@@ -67,7 +111,8 @@ export function JobsDetailHeaderActions({
|
||||
setEmailOptions,
|
||||
openChatByPhone,
|
||||
setMessage,
|
||||
setTimeTicketTaskContext
|
||||
setTimeTicketTaskContext,
|
||||
setTaskUpsertContext
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const client = useApolloClient();
|
||||
@@ -722,14 +767,14 @@ export function JobsDetailHeaderActions({
|
||||
logImEXEvent("job_header_enter_card_payment");
|
||||
|
||||
setCardPaymentContext({
|
||||
actions: {},
|
||||
actions: { refetch },
|
||||
context: { jobid: job.id }
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (HasFeatureAccess({ featureName: "courtesycars" })) {
|
||||
if (HasFeatureAccess({ featureName: "courtesycars", bodyshop })) {
|
||||
menuItems.push({
|
||||
key: "cccontract",
|
||||
disabled: jobRO || !job.converted,
|
||||
@@ -741,10 +786,20 @@ export function JobsDetailHeaderActions({
|
||||
});
|
||||
}
|
||||
|
||||
menuItems.push({
|
||||
key: "createtask",
|
||||
label: t("menus.header.create_task"),
|
||||
onClick: () =>
|
||||
setTaskUpsertContext({
|
||||
actions: {},
|
||||
context: { jobid: job.id }
|
||||
})
|
||||
});
|
||||
|
||||
menuItems.push(
|
||||
job.inproduction
|
||||
? {
|
||||
key: "addtoproduction",
|
||||
key: "removefromproduction",
|
||||
disabled: !job.converted,
|
||||
label: t("jobs.actions.removefromproduction"),
|
||||
onClick: () => AddToProduction(client, job.id, refetch, true)
|
||||
|
||||
@@ -96,7 +96,6 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
|
||||
}),
|
||||
onFilter: (value, record) => value.includes(record.status)
|
||||
},
|
||||
|
||||
{
|
||||
title: t("jobs.fields.vehicle"),
|
||||
dataIndex: "vehicle",
|
||||
|
||||
@@ -176,7 +176,6 @@ export function JobsList({ bodyshop, setJoyRideSteps }) {
|
||||
[],
|
||||
onFilter: (value, record) => value.includes(record.status)
|
||||
},
|
||||
|
||||
{
|
||||
title: t("jobs.fields.vehicle"),
|
||||
dataIndex: "vehicle",
|
||||
@@ -316,34 +315,6 @@ export function JobsList({ bodyshop, setJoyRideSteps }) {
|
||||
title={t("titles.bc.jobs-active")}
|
||||
extra={
|
||||
<Space wrap>
|
||||
{InstanceRenderManager({
|
||||
promanager: (
|
||||
<Button
|
||||
onClick={() =>
|
||||
setJoyRideSteps([
|
||||
{
|
||||
target: "#active-jobs-list",
|
||||
content: "This is where you will see all work coming in and currently here."
|
||||
},
|
||||
{
|
||||
target: "#header-jobs",
|
||||
spotlightClicks: true,
|
||||
disableOverlayClose: true,
|
||||
content:
|
||||
"The jobs menu lets you access additional pages to see more information. You can import new jobs, search all jobs, or manage your current production."
|
||||
},
|
||||
{
|
||||
target: "#header-jobs-available",
|
||||
content: "You can find jobs to import here."
|
||||
}
|
||||
])
|
||||
}
|
||||
>
|
||||
Start Walk Through
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
|
||||
<Button onClick={() => refetch()}>
|
||||
<SyncOutlined />
|
||||
</Button>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { EditFilled } from "@ant-design/icons";
|
||||
import { Card, Col, Row, Space, Table, Typography } from "antd";
|
||||
import { Alert, Card, Col, Row, Space, Table, Typography } from "antd";
|
||||
import _ from "lodash";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -18,7 +18,17 @@ const mapStateToProps = createStructuredSelector({
|
||||
technician: selectTechnician
|
||||
});
|
||||
|
||||
export function LaborAllocationsTable({ jobId, joblines, timetickets, bodyshop, adjustments, technician }) {
|
||||
export function LaborAllocationsTable({
|
||||
jobId,
|
||||
joblines,
|
||||
timetickets,
|
||||
bodyshop,
|
||||
adjustments,
|
||||
technician,
|
||||
showWarning,
|
||||
warningCallback,
|
||||
disabled
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [totals, setTotals] = useState([]);
|
||||
const [state, setState] = useState({
|
||||
@@ -73,7 +83,7 @@ export function LaborAllocationsTable({ jobId, joblines, timetickets, bodyshop,
|
||||
render: (text, record) => (
|
||||
<Space wrap>
|
||||
{record.adjustments.toFixed(1)}
|
||||
{!technician && (
|
||||
{!technician && !disabled && (
|
||||
<LaborAllocationsAdjustmentEdit
|
||||
jobId={jobId}
|
||||
adjustments={adjustments}
|
||||
@@ -176,6 +186,10 @@ export function LaborAllocationsTable({ jobId, joblines, timetickets, bodyshop,
|
||||
{ hrs_total: 0, hrs_claimed: 0, adjustments: 0, difference: 0 }
|
||||
);
|
||||
|
||||
if (summary.difference !== 0 && typeof warningCallback === "function") {
|
||||
warningCallback({ key: "labor", warning: t("jobs.labels.outstandinghours") });
|
||||
}
|
||||
|
||||
return (
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
@@ -227,6 +241,9 @@ export function LaborAllocationsTable({ jobId, joblines, timetickets, bodyshop,
|
||||
</Card>
|
||||
</Col>
|
||||
)}
|
||||
{showWarning && summary.difference !== 0 && (
|
||||
<Alert style={{ margin: "8px 0px" }} type="warning" message={t("jobs.labels.outstandinghours")} />
|
||||
)}
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button, Card, Col, notification, Row, Space, Table, Typography } from "antd";
|
||||
import { Alert, Button, Card, Col, notification, Row, Space, Table, Typography } from "antd";
|
||||
import { SyncOutlined } from "@ant-design/icons";
|
||||
import axios from "axios";
|
||||
import _ from "lodash";
|
||||
@@ -23,7 +23,9 @@ export function PayrollLaborAllocationsTable({
|
||||
bodyshop,
|
||||
adjustments,
|
||||
technician,
|
||||
refetch
|
||||
refetch,
|
||||
showWarning,
|
||||
warningCallback
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [totals, setTotals] = useState([]);
|
||||
@@ -191,6 +193,10 @@ export function PayrollLaborAllocationsTable({
|
||||
{ hrs_total: 0, hrs_claimed: 0, adjustments: 0, difference: 0 }
|
||||
);
|
||||
|
||||
if (summary.difference !== 0 && typeof warningCallback === "function") {
|
||||
warningCallback({ key: "labor", warning: t("jobs.labels.outstandinghours") });
|
||||
}
|
||||
|
||||
return (
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
@@ -285,6 +291,9 @@ export function PayrollLaborAllocationsTable({
|
||||
</Card>
|
||||
</Col>
|
||||
)}
|
||||
{showWarning && summary.difference !== 0 && (
|
||||
<Alert style={{ margin: "8px 0px" }} type="warning" message={t("jobs.labels.outstandinghours")} />
|
||||
)}
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@ import { Link } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { alphaSort, statusSort } from "../../utils/sorters";
|
||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import { alphaSort, dateSort, statusSort } from "../../utils/sorters";
|
||||
import OwnerDetailUpdateJobsComponent from "../owner-detail-update-jobs/owner-detail-update-jobs.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
@@ -75,7 +76,18 @@ function OwnerDetailJobsComponent({ bodyshop, owner }) {
|
||||
})),
|
||||
onFilter: (value, record) => value.includes(record.status)
|
||||
},
|
||||
|
||||
{
|
||||
title: t("jobs.fields.actual_completion"),
|
||||
dataIndex: "actual_completion",
|
||||
key: "actual_completion",
|
||||
render: (text, record) => (
|
||||
<DateTimeFormatter>{record.actual_completion}</DateTimeFormatter>
|
||||
),
|
||||
sorter: (a, b) => dateSort(a.actual_completion, b.actual_completion),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "actual_completion" &&
|
||||
state.sortedInfo.order,
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.clm_total"),
|
||||
dataIndex: "clm_total",
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
import { DeleteFilled, EyeFilled, SyncOutlined } from "@ant-design/icons";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { useLazyQuery, useMutation } from "@apollo/client";
|
||||
import { Button, Card, Checkbox, Drawer, Grid, Input, Popconfirm, Space, Table } from "antd";
|
||||
import { PageHeader } from "@ant-design/pro-layout";
|
||||
|
||||
import queryString from "query-string";
|
||||
import React, { useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { QUERY_BILL_BY_PK } from "../../graphql/bills.queries";
|
||||
import { DELETE_PARTS_ORDER } from "../../graphql/parts-orders.queries";
|
||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import DataLabel from "../data-label/data-label.component";
|
||||
import PartsOrderBackorderEta from "../parts-order-backorder-eta/parts-order-backorder-eta.component";
|
||||
import PartsOrderCmReceived from "../parts-order-cm-received/parts-order-cm-received.component";
|
||||
@@ -26,6 +27,7 @@ import PartsOrderLineBackorderButton from "../parts-order-line-backorder-button/
|
||||
import PartsReceiveModalContainer from "../parts-receive-modal/parts-receive-modal.container";
|
||||
import PrintWrapper from "../print-wrapper/print-wrapper.component";
|
||||
import FeatureWrapperComponent from "../feature-wrapper/feature-wrapper.component";
|
||||
import { FaTasks } from "react-icons/fa";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
jobRO: selectJobReadOnly,
|
||||
@@ -33,8 +35,21 @@ const mapStateToProps = createStructuredSelector({
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setBillEnterContext: (context) => dispatch(setModalContext({ context: context, modal: "billEnter" })),
|
||||
setPartsReceiveContext: (context) => dispatch(setModalContext({ context: context, modal: "partsReceive" }))
|
||||
setBillEnterContext: (context) =>
|
||||
dispatch(
|
||||
setModalContext({
|
||||
context: context,
|
||||
modal: "billEnter"
|
||||
})
|
||||
),
|
||||
setPartsReceiveContext: (context) =>
|
||||
dispatch(
|
||||
setModalContext({
|
||||
context: context,
|
||||
modal: "partsReceive"
|
||||
})
|
||||
),
|
||||
setTaskUpsertContext: (context) => dispatch(setModalContext({ context, modal: "taskUpsert" }))
|
||||
});
|
||||
|
||||
export function PartsOrderListTableComponent({
|
||||
@@ -44,7 +59,8 @@ export function PartsOrderListTableComponent({
|
||||
job,
|
||||
billsQuery,
|
||||
handleOnRowClick,
|
||||
setPartsReceiveContext
|
||||
setPartsReceiveContext,
|
||||
setTaskUpsertContext
|
||||
}) {
|
||||
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
|
||||
.filter((screen) => !!screen[1])
|
||||
@@ -66,19 +82,46 @@ export function PartsOrderListTableComponent({
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {}
|
||||
});
|
||||
|
||||
const [returnfrombill, setReturnFromBill] = useState();
|
||||
const [billData, setBillData] = useState();
|
||||
const search = queryString.parse(useLocation().search);
|
||||
const selectedpartsorder = search.partsorderid;
|
||||
const [searchText, setSearchText] = useState("");
|
||||
|
||||
const [billQuery] = useLazyQuery(QUERY_BILL_BY_PK);
|
||||
const [deletePartsOrder] = useMutation(DELETE_PARTS_ORDER);
|
||||
|
||||
const parts_orders = billsQuery.data ? billsQuery.data.parts_orders : [];
|
||||
const { refetch } = billsQuery;
|
||||
|
||||
useEffect(() => {
|
||||
if (returnfrombill === null) {
|
||||
setBillData(null);
|
||||
} else {
|
||||
const fetchData = async () => {
|
||||
const result = await billQuery({
|
||||
variables: { billid: returnfrombill },
|
||||
});
|
||||
setBillData(result.data);
|
||||
};
|
||||
fetchData();
|
||||
}
|
||||
}, [returnfrombill, billQuery]);
|
||||
|
||||
const recordActions = (record, showView = false) => (
|
||||
<Space wrap>
|
||||
<Space direction="horizontal" wrap>
|
||||
{showView && (
|
||||
<Button onClick={() => handleOnRowClick(record)}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (record.returnfrombill) {
|
||||
setReturnFromBill(record.returnfrombill);
|
||||
} else {
|
||||
setReturnFromBill(null);
|
||||
}
|
||||
handleOnRowClick(record);
|
||||
}}
|
||||
>
|
||||
<EyeFilled />
|
||||
</Button>
|
||||
)}
|
||||
@@ -108,7 +151,19 @@ export function PartsOrderListTableComponent({
|
||||
>
|
||||
{t("parts_orders.actions.receive")}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
title={t("tasks.buttons.create")}
|
||||
onClick={() => {
|
||||
setTaskUpsertContext({
|
||||
context: {
|
||||
jobid: job.id,
|
||||
partsorderid: record.id
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<FaTasks />
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title={t("parts_orders.labels.confirmdelete")}
|
||||
disabled={jobRO}
|
||||
@@ -368,7 +423,14 @@ export function PartsOrderListTableComponent({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader title={record && `${record.vendor.name} - ${record.order_number}`} extra={recordActions(record)} />
|
||||
<PageHeader
|
||||
title={
|
||||
billData
|
||||
? `${record.vendor.name} - ${record.order_number} - ${t("bills.labels.returnfrombill")}: ${billData.bills_by_pk.invoice_number}`
|
||||
: `${record.vendor.name} - ${record.order_number}`
|
||||
}
|
||||
extra={recordActions(record)}
|
||||
/>
|
||||
<Table
|
||||
scroll={{
|
||||
x: true //y: "50rem"
|
||||
|
||||
@@ -182,7 +182,7 @@ export function PartsOrderModalComponent({ bodyshop, vendorList, sendTypeState,
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber />
|
||||
<InputNumber min={1} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("parts_orders.fields.act_price")}
|
||||
|
||||
@@ -158,7 +158,7 @@ export function PartsOrderModalContainer({
|
||||
vendorid: bodyshop.inhousevendorid,
|
||||
invoice_number: "ih",
|
||||
isinhouse: true,
|
||||
date: new dayjs(),
|
||||
date: dayjs(),
|
||||
total: 0,
|
||||
billlines: values.parts_order_lines.data.map((p) => {
|
||||
return {
|
||||
|
||||
@@ -119,8 +119,14 @@ const PaymentExpandedRowComponent = ({ record, bodyshop }) => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Descriptions title={t("job_payments.titles.descriptions")} contentStyle={{ fontWeight: "600" }} column={4}>
|
||||
<Descriptions.Item label={t("job_payments.titles.payer")}>{record.payer}</Descriptions.Item>
|
||||
<Descriptions
|
||||
title={t("job_payments.titles.descriptions")}
|
||||
contentStyle={{ fontWeight: "600" }}
|
||||
column={4}
|
||||
>
|
||||
<Descriptions.Item label={t("job_payments.titles.hint")}>
|
||||
{payment_response?.response?.methodhint}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={t("job_payments.titles.payername")}>
|
||||
{payment_response?.response?.nameOnCard ?? ""}
|
||||
</Descriptions.Item>
|
||||
@@ -132,7 +138,7 @@ const PaymentExpandedRowComponent = ({ record, bodyshop }) => {
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={t("job_payments.titles.transactionid")}>{record.transactionid}</Descriptions.Item>
|
||||
<Descriptions.Item label={t("job_payments.titles.paymentid")}>
|
||||
{payment_response?.response?.paymentreferenceid ?? ""}
|
||||
{payment_response?.ext_paymentid ?? ""}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={t("job_payments.titles.paymenttype")}>{record.type}</Descriptions.Item>
|
||||
<Descriptions.Item label={t("job_payments.titles.paymentnum")}>{record.paymentnum}</Descriptions.Item>
|
||||
|
||||
@@ -7,17 +7,19 @@ import { createStructuredSelector } from "reselect";
|
||||
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
|
||||
import { UPDATE_PAYMENT } from "../../graphql/payments.queries";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { selectPayment } from "../../redux/modals/modals.selectors";
|
||||
import { selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser
|
||||
currentUser: selectCurrentUser,
|
||||
paymentModal: selectPayment
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setPaymentContext: (context) => dispatch(setModalContext({ context: context, modal: "payment" }))
|
||||
});
|
||||
|
||||
const PaymentMarkForExportButton = ({ bodyshop, payment, refetch, setPaymentContext, currentUser }) => {
|
||||
const PaymentMarkForExportButton = ({ bodyshop, payment, refetch, setPaymentContext, currentUser, paymentModal }) => {
|
||||
const { t } = useTranslation();
|
||||
const [insertExportLog, { loading: exportLogLoading }] = useMutation(INSERT_EXPORT_LOG);
|
||||
const [updatePayment, { loading: updatePaymentLoading }] = useMutation(UPDATE_PAYMENT);
|
||||
@@ -56,16 +58,21 @@ const PaymentMarkForExportButton = ({ bodyshop, payment, refetch, setPaymentCont
|
||||
refetch
|
||||
},
|
||||
context: {
|
||||
...paymentModal.context,
|
||||
...paymentModal.context,
|
||||
...payment,
|
||||
smartRefetch: true,
|
||||
exportedat: today
|
||||
}
|
||||
});
|
||||
|
||||
if (refetch)
|
||||
refetch(
|
||||
paymentUpdateResponse &&
|
||||
paymentUpdateResponse.data.update_payments.returning[0]
|
||||
);
|
||||
if (refetch) {
|
||||
if (paymentModal.context.refetchRequiresContext) {
|
||||
refetch(paymentUpdateResponse && paymentUpdateResponse.data.update_payments.returning[0]);
|
||||
} else {
|
||||
refetch();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
notification["error"]({
|
||||
message: t("payments.errors.exporting", {
|
||||
|
||||
@@ -1,33 +1,30 @@
|
||||
import { useMutation } from "@apollo/client";
|
||||
|
||||
import { Button, Form, Modal, notification, Space } from "antd";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { INSERT_NEW_PAYMENT, UPDATE_PAYMENT } from "../../graphql/payments.queries";
|
||||
import { setEmailOptions } from "../../redux/email/email.actions";
|
||||
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
||||
import { selectPayment } from "../../redux/modals/modals.selectors";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import { GenerateDocument } from "../../utils/RenderTemplate";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import {useMutation} from "@apollo/client";
|
||||
import {Button, Form, Modal, notification, Space} from "antd";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {connect} from "react-redux";
|
||||
import {createStructuredSelector} from "reselect";
|
||||
import {INSERT_NEW_PAYMENT, UPDATE_PAYMENT} from "../../graphql/payments.queries";
|
||||
import {toggleModalVisible} from "../../redux/modals/modals.actions";
|
||||
import {selectPayment} from "../../redux/modals/modals.selectors";
|
||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
||||
import {GenerateDocument} from "../../utils/RenderTemplate";
|
||||
import {TemplateList} from "../../utils/TemplateConstants";
|
||||
import PaymentForm from "../payment-form/payment-form.component";
|
||||
import PaymentMarkForExportButton from "../payment-mark-export-button/payment-mark-export-button-component";
|
||||
import PaymentMarkForExportButton
|
||||
from "../payment-mark-export-button/payment-mark-export-button-component";
|
||||
import PaymentReexportButton from "../payment-reexport-button/payment-reexport-button.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
paymentModal: selectPayment,
|
||||
bodyshop: selectBodyshop,
|
||||
currentUser: selectCurrentUser
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setEmailOptions: (e) => dispatch(setEmailOptions(e)),
|
||||
toggleModalVisible: () => dispatch(toggleModalVisible("payment"))
|
||||
});
|
||||
|
||||
function PaymentModalContainer({ paymentModal, toggleModalVisible, bodyshop, currentUser, setEmailOptions }) {
|
||||
function PaymentModalContainer({paymentModal, toggleModalVisible, bodyshop }) {
|
||||
const [form] = Form.useForm();
|
||||
const [enterAgain, setEnterAgain] = useState(false);
|
||||
const [insertPayment] = useMutation(INSERT_NEW_PAYMENT);
|
||||
@@ -35,7 +32,6 @@ function PaymentModalContainer({ paymentModal, toggleModalVisible, bodyshop, cur
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { context, actions, open } = paymentModal;
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
@@ -85,13 +81,19 @@ function PaymentModalContainer({ paymentModal, toggleModalVisible, bodyshop, cur
|
||||
});
|
||||
|
||||
if (!!!updatedPayment.errors) {
|
||||
notification["success"]({ message: t("payments.successes.payment") });
|
||||
notification["success"]({ message: t("payments.successes.paymentupdate") });
|
||||
} else {
|
||||
notification["error"]({ message: t("payments.errors.payment") });
|
||||
notification["error"]({ message: t("payments.errors.paymentupdate") });
|
||||
}
|
||||
}
|
||||
|
||||
if (actions.refetch) actions.refetch(updatedPayment && updatedPayment.data.update_payments.returning[0]);
|
||||
if (actions.refetch) {
|
||||
if (context.refetchRequiresContext) {
|
||||
actions.refetch(updatedPayment && updatedPayment.data.update_payments.returning[0]);
|
||||
} else {
|
||||
actions.refetch();
|
||||
}
|
||||
}
|
||||
|
||||
if (enterAgain) {
|
||||
const prev = form.getFieldsValue(["date"]);
|
||||
@@ -166,13 +168,7 @@ function PaymentModalContainer({ paymentModal, toggleModalVisible, bodyshop, cur
|
||||
</Space>
|
||||
)}
|
||||
|
||||
<Form
|
||||
onFinish={handleFinish}
|
||||
autoComplete={"off"}
|
||||
form={form}
|
||||
layout="vertical"
|
||||
disabled={context?.exportedat}
|
||||
>
|
||||
<Form onFinish={handleFinish} autoComplete={"off"} form={form} layout="vertical" disabled={context?.exportedat}>
|
||||
<PaymentForm form={form} />
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
@@ -3,14 +3,20 @@ import { Button, notification } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { UPDATE_PAYMENT } from "../../graphql/payments.queries";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { selectPayment } from "../../redux/modals/modals.selectors";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
paymentModal: selectPayment
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setPaymentContext: (context) => dispatch(setModalContext({ context: context, modal: "payment" }))
|
||||
});
|
||||
|
||||
const PaymentReexportButton = ({ payment, refetch, setPaymentContext }) => {
|
||||
const PaymentReexportButton = ({ paymentModal, payment, refetch, setPaymentContext }) => {
|
||||
const { t } = useTranslation();
|
||||
const [updatePayment, { loading }] = useMutation(UPDATE_PAYMENT);
|
||||
|
||||
@@ -34,15 +40,20 @@ const PaymentReexportButton = ({ payment, refetch, setPaymentContext }) => {
|
||||
refetch
|
||||
},
|
||||
context: {
|
||||
...paymentModal.context,
|
||||
...paymentModal.context,
|
||||
...payment,
|
||||
exportedat: null
|
||||
}
|
||||
});
|
||||
if (refetch)
|
||||
refetch(
|
||||
paymentUpdateResponse &&
|
||||
paymentUpdateResponse.data.update_payments.returning[0]
|
||||
);
|
||||
|
||||
if (refetch) {
|
||||
if (paymentModal.context.refetchRequiresContext) {
|
||||
refetch(paymentUpdateResponse && paymentUpdateResponse.data.update_payments.returning[0]);
|
||||
} else {
|
||||
refetch();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
notification["error"]({
|
||||
message: t("payments.errors.exporting", {
|
||||
@@ -59,4 +70,4 @@ const PaymentReexportButton = ({ payment, refetch, setPaymentContext }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(null, mapDispatchToProps)(PaymentReexportButton);
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(PaymentReexportButton);
|
||||
|
||||
@@ -14,11 +14,11 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import { pageLimit } from "../../utils/config";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import CaBcEtfTableModalContainer from "../ca-bc-etf-table-modal/ca-bc-etf-table-modal.container";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
|
||||
import { pageLimit } from "../../utils/config";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
@@ -171,7 +171,7 @@ export function PaymentsListPaginated({
|
||||
}
|
||||
: refetch
|
||||
},
|
||||
context: apolloResults ? apolloResults : record
|
||||
context: { ...(apolloResults ? apolloResults : record), refetchRequiresContext: true }
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
import { Button, Card, Form, InputNumber, notification, Popover, Radio } from "antd";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Form,
|
||||
InputNumber,
|
||||
notification,
|
||||
Popover,
|
||||
Radio,
|
||||
Space,
|
||||
} from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
@@ -95,10 +104,16 @@ export function PrintCenterJobsLabels({ bodyshop, jobId }) {
|
||||
>
|
||||
<InputNumber min={1} precision={0} max={99} />
|
||||
</Form.Item>
|
||||
<Button type="primary" loading={loading} onClick={handleOk}>
|
||||
{t("general.actions.print")}
|
||||
</Button>
|
||||
<Button onClick={handleCancel}>{t("general.actions.cancel")}</Button>
|
||||
<div style={{ display: "flex", justifyContent: "flex-end" }}>
|
||||
<Space>
|
||||
<Button type="primary" loading={loading} onClick={handleOk}>
|
||||
{t("general.actions.print")}
|
||||
</Button>
|
||||
<Button onClick={handleCancel}>
|
||||
{t("general.actions.cancel")}
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</Form>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -2,23 +2,24 @@ import { useLazyQuery } from "@apollo/client";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import { Button, Card, Col, DatePicker, Form, Input, Radio, Row, Typography } from "antd";
|
||||
import _ from "lodash";
|
||||
import dayjs from "../../utils/day";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { QUERY_ACTIVE_EMPLOYEES } from "../../graphql/employees.queries";
|
||||
import { QUERY_ACTIVE_EMPLOYEES, QUERY_ACTIVE_EMPLOYEES_WITH_EMAIL } from "../../graphql/employees.queries";
|
||||
import { QUERY_ALL_VENDORS } from "../../graphql/vendors.queries";
|
||||
import { selectReportCenter } from "../../redux/modals/modals.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import DatePickerRanges from "../../utils/DatePickerRanges";
|
||||
import { GenerateDocument } from "../../utils/RenderTemplate";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import dayjs from "../../utils/day";
|
||||
import EmployeeSearchSelectEmail from "../employee-search-select/employee-search-select-email.component";
|
||||
import EmployeeSearchSelect from "../employee-search-select/employee-search-select.component";
|
||||
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
|
||||
import "./report-center-modal.styles.scss";
|
||||
import ReportCenterModalFiltersSortersComponent from "./report-center-modal-filters-sorters-component";
|
||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
|
||||
import ReportCenterModalFiltersSortersComponent from "./report-center-modal-filters-sorters-component";
|
||||
import "./report-center-modal.styles.scss";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
reportCenterModal: selectReportCenter,
|
||||
@@ -66,6 +67,13 @@ export function ReportCenterModalComponent({ reportCenterModal, bodyshop }) {
|
||||
skip: !(open && Templates[form.getFieldValue("key")] && Templates[form.getFieldValue("key")].idtype)
|
||||
});
|
||||
|
||||
const [callEmployeeWithEmailQuery, { data: employeeWithEmailData, called: employeeWithEmailCalled }] = useLazyQuery(
|
||||
QUERY_ACTIVE_EMPLOYEES_WITH_EMAIL,
|
||||
{
|
||||
skip: !(open && Templates[form.getFieldValue("key")] && Templates[form.getFieldValue("key")].idtype)
|
||||
}
|
||||
);
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
setLoading(true);
|
||||
const start = values.dates ? values.dates[0] : null;
|
||||
@@ -197,6 +205,7 @@ export function ReportCenterModalComponent({ reportCenterModal, bodyshop }) {
|
||||
}
|
||||
if (!vendorCalled && idtype === "vendor") callVendorQuery();
|
||||
if (!employeeCalled && idtype === "employee") callEmployeeQuery();
|
||||
if (!employeeWithEmailCalled && idtype === "employeeWithEmail") callEmployeeWithEmailQuery();
|
||||
if (idtype === "vendor")
|
||||
return (
|
||||
<Form.Item
|
||||
@@ -227,6 +236,22 @@ export function ReportCenterModalComponent({ reportCenterModal, bodyshop }) {
|
||||
<EmployeeSearchSelect options={employeeData ? employeeData.employees : []} />
|
||||
</Form.Item>
|
||||
);
|
||||
//This was introduced with tasks before assigned_to was shifted to UUID. Keeping in place for reference in the future if needed.
|
||||
if (idtype === "employeeWithEmail")
|
||||
return (
|
||||
<Form.Item
|
||||
name="id"
|
||||
label={t("reportcenter.labels.employee")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<EmployeeSearchSelectEmail options={employeeWithEmailData ? employeeWithEmailData.employees : []} />
|
||||
</Form.Item>
|
||||
);
|
||||
else return null;
|
||||
}}
|
||||
</Form.Item>
|
||||
|
||||
@@ -59,25 +59,33 @@ export function ScheduleCalendarHeaderComponent({
|
||||
{loadData && loadData.allJobsOut ? (
|
||||
loadData.allJobsOut.map((j) => (
|
||||
<tr key={j.id}>
|
||||
<td>
|
||||
<Link to={`/manage/jobs/${j.id}`}>{j.ro_number}</Link>
|
||||
<td style={{ padding: "2.5px" }}>
|
||||
<Link to={`/manage/jobs/${j.id}`}>{j.ro_number}</Link> (
|
||||
{j.status})
|
||||
</td>
|
||||
<td>
|
||||
<td style={{ padding: "2.5px" }}>
|
||||
<OwnerNameDisplay ownerObject={j} />
|
||||
</td>
|
||||
<td>
|
||||
{`(${(j.labhrs.aggregate.sum.mod_lb_hrs + j.larhrs.aggregate.sum.mod_lb_hrs).toFixed(
|
||||
1
|
||||
)} ${t("general.labels.hours")})`}
|
||||
<td style={{ padding: "2.5px" }}>
|
||||
{`(${j.labhrs?.aggregate?.sum?.mod_lb_hrs?.toFixed(1) || 0}/${
|
||||
j.larhrs?.aggregate?.sum?.mod_lb_hrs?.toFixed(1) || 0
|
||||
}/${(
|
||||
j.labhrs.aggregate?.sum?.mod_lb_hrs +
|
||||
j.larhrs.aggregate?.sum?.mod_lb_hrs
|
||||
).toFixed(1)} ${t("general.labels.hours")})`}
|
||||
</td>
|
||||
<td>
|
||||
<DateTimeFormatter>{j.scheduled_completion}</DateTimeFormatter>
|
||||
<td style={{ padding: "2.5px" }}>
|
||||
<DateTimeFormatter>
|
||||
{j.scheduled_completion}
|
||||
</DateTimeFormatter>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td>{t("appointments.labels.nocompletingjobs")}</td>
|
||||
<td style={{ padding: "2.5px" }}>
|
||||
{t("appointments.labels.nocompletingjobs")}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
@@ -92,26 +100,30 @@ export function ScheduleCalendarHeaderComponent({
|
||||
{loadData && loadData.allJobsIn ? (
|
||||
loadData.allJobsIn.map((j) => (
|
||||
<tr key={j.id}>
|
||||
<td>
|
||||
<td style={{ padding: "2.5px" }}>
|
||||
<Link to={`/manage/jobs/${j.id}`}>{j.ro_number}</Link>
|
||||
{j.status}
|
||||
</td>
|
||||
<td>
|
||||
<td style={{ padding: "2.5px" }}>
|
||||
<OwnerNameDisplay ownerObject={j} />
|
||||
</td>
|
||||
<td>
|
||||
{`(${(j.labhrs.aggregate.sum.mod_lb_hrs + j.larhrs.aggregate.sum.mod_lb_hrs).toFixed(
|
||||
1
|
||||
)} ${t("general.labels.hours")})`}
|
||||
<td style={{ padding: "2.5px" }}>
|
||||
{`(${j.labhrs?.aggregate?.sum.mod_lb_hrs?.toFixed(1) || 0}/${
|
||||
j.larhrs?.aggregate?.sum?.mod_lb_hrs?.toFixed(1) || 0
|
||||
}/${(
|
||||
j.labhrs?.aggregate?.sum?.mod_lb_hrs +
|
||||
j.larhrs?.aggregate?.sum?.mod_lb_hrs
|
||||
).toFixed(1)} ${t("general.labels.hours")})`}
|
||||
</td>
|
||||
<td>
|
||||
<td style={{ padding: "2.5px" }}>
|
||||
<DateTimeFormatter>{j.scheduled_in}</DateTimeFormatter>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td>{t("appointments.labels.noarrivingjobs")}</td>
|
||||
<td style={{ padding: "2.5px" }}>
|
||||
{t("appointments.labels.noarrivingjobs")}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
@@ -121,27 +133,33 @@ export function ScheduleCalendarHeaderComponent({
|
||||
|
||||
const LoadComponent = loadData ? (
|
||||
<div>
|
||||
<Space align="center">
|
||||
<Popover
|
||||
placement={"bottom"}
|
||||
content={jobsInPopup}
|
||||
trigger="hover"
|
||||
title={t("appointments.labels.arrivingjobs")}
|
||||
>
|
||||
<Icon component={MdFileDownload} style={{ color: "green" }} />
|
||||
{(loadData.allHoursIn || 0) && loadData.allHoursIn.toFixed(2)}
|
||||
</Popover>
|
||||
<Popover
|
||||
placement={"bottom"}
|
||||
content={jobsOutPopup}
|
||||
trigger="hover"
|
||||
title={t("appointments.labels.completingjobs")}
|
||||
>
|
||||
<Icon component={MdFileUpload} style={{ color: "red" }} />
|
||||
{(loadData.allHoursOut || 0) && loadData.allHoursOut.toFixed(2)}
|
||||
</Popover>
|
||||
<ScheduleCalendarHeaderGraph loadData={loadData} />
|
||||
</Space>
|
||||
<Space align="center">
|
||||
<Popover
|
||||
placement={"bottom"}
|
||||
content={jobsInPopup}
|
||||
trigger="hover"
|
||||
title={t("appointments.labels.arrivingjobs")}
|
||||
>
|
||||
<Icon component={MdFileDownload} style={{ color: "green" }} />
|
||||
{(loadData.allHoursInBody || 0) &&
|
||||
loadData.allHoursInBody.toFixed(1)}
|
||||
/
|
||||
{(loadData.allHoursInRefinish || 0) &&
|
||||
loadData.allHoursInRefinish.toFixed(1)}
|
||||
/{(loadData.allHoursIn || 0) && loadData.allHoursIn.toFixed(1)}
|
||||
</Popover>
|
||||
<Popover
|
||||
placement={"bottom"}
|
||||
content={jobsOutPopup}
|
||||
trigger="hover"
|
||||
title={t("appointments.labels.completingjobs")}
|
||||
>
|
||||
<Icon component={MdFileUpload} style={{ color: "red" }} />
|
||||
{(loadData.allHoursOut || 0) && loadData.allHoursOut.toFixed(1)}
|
||||
</Popover>
|
||||
<ScheduleCalendarHeaderGraph loadData={loadData} />
|
||||
</Space>
|
||||
|
||||
<div>
|
||||
<ul style={{ listStyleType: "none", columns: "2 auto", padding: 0 }}>
|
||||
{Object.keys(ATSToday).map((key, idx) => (
|
||||
|
||||
@@ -19,6 +19,7 @@ import { useLocation, useNavigate } from "react-router-dom";
|
||||
import ShopInfoTaskPresets from "./shop-info.task-presets.component";
|
||||
import queryString from "query-string";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import ShopInfoRoGuard from "./shop-info.roguard.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
@@ -46,44 +47,52 @@ export function ShopInfoComponent({ bodyshop, form, saveLoading }) {
|
||||
{
|
||||
key: "general",
|
||||
label: t("bodyshop.labels.shopinfo"),
|
||||
children: <ShopInfoGeneral form={form} />
|
||||
children: <ShopInfoGeneral form={form} />,
|
||||
id: "tab-shop-general"
|
||||
},
|
||||
{
|
||||
key: "speedprint",
|
||||
label: t("bodyshop.labels.speedprint"),
|
||||
children: <ShopInfoSpeedPrint form={form} />
|
||||
children: <ShopInfoSpeedPrint form={form} />,
|
||||
id: "tab-shop-speedprint"
|
||||
},
|
||||
{
|
||||
key: "rbac",
|
||||
label: t("bodyshop.labels.rbac"),
|
||||
children: <ShopInfoRbacComponent form={form} />
|
||||
children: <ShopInfoRbacComponent form={form} />,
|
||||
id: "tab-shop-rbac"
|
||||
},
|
||||
{
|
||||
key: "roStatus",
|
||||
label: t("bodyshop.labels.jobstatuses"),
|
||||
children: <ShopInfoROStatusComponent form={form} />
|
||||
children: <ShopInfoROStatusComponent form={form} />,
|
||||
id: "tab-shop-rostatus"
|
||||
},
|
||||
{
|
||||
key: "scheduling",
|
||||
label: t("bodyshop.labels.scheduling"),
|
||||
children: <ShopInfoSchedulingComponent form={form} />
|
||||
children: <ShopInfoSchedulingComponent form={form} />,
|
||||
id: "tab-shop-scheduling"
|
||||
},
|
||||
{
|
||||
key: "orderStatus",
|
||||
label: t("bodyshop.labels.orderstatuses"),
|
||||
children: <ShopInfoOrderStatusComponent form={form} />
|
||||
children: <ShopInfoOrderStatusComponent form={form} />,
|
||||
id: "tab-shop-orderstatus"
|
||||
},
|
||||
{
|
||||
key: "responsibilityCenters",
|
||||
label: t("bodyshop.labels.responsibilitycenters.title"),
|
||||
children: <ShopInfoResponsibilityCenterComponent form={form} />
|
||||
children: <ShopInfoResponsibilityCenterComponent form={form} />,
|
||||
id: "tab-shop-responsibilitycenters"
|
||||
},
|
||||
...InstanceRenderManager({
|
||||
imex: [
|
||||
{
|
||||
key: "checklists",
|
||||
label: t("bodyshop.labels.checklists"),
|
||||
children: <ShopInfoIntakeChecklistComponent form={form} />
|
||||
children: <ShopInfoIntakeChecklistComponent form={form} />,
|
||||
id: "tab-shop-checklists"
|
||||
}
|
||||
],
|
||||
rome: "USE_IMEX",
|
||||
@@ -92,14 +101,16 @@ export function ShopInfoComponent({ bodyshop, form, saveLoading }) {
|
||||
{
|
||||
key: "laborrates",
|
||||
label: t("bodyshop.labels.laborrates"),
|
||||
children: <ShopInfoLaborRates form={form} />
|
||||
children: <ShopInfoLaborRates form={form} />,
|
||||
id: "tab-shop-laborrates"
|
||||
},
|
||||
...(CriticalPartsScanning.treatment === "on"
|
||||
? [
|
||||
{
|
||||
key: "partsscan",
|
||||
label: t("bodyshop.labels.partsscan"),
|
||||
children: <ShopInfoPartsScan form={form} />
|
||||
children: <ShopInfoPartsScan form={form} />,
|
||||
id: "tab-shop-partsscan"
|
||||
}
|
||||
]
|
||||
: []),
|
||||
@@ -108,10 +119,23 @@ export function ShopInfoComponent({ bodyshop, form, saveLoading }) {
|
||||
{
|
||||
key: "task-presets",
|
||||
label: t("bodyshop.labels.task-presets"),
|
||||
children: <ShopInfoTaskPresets form={form} />
|
||||
children: <ShopInfoTaskPresets form={form} />,
|
||||
id: "tab-shop-task-presets"
|
||||
}
|
||||
]
|
||||
: [])
|
||||
: []),
|
||||
...InstanceRenderManager({
|
||||
imex: [
|
||||
{
|
||||
key: "roguard",
|
||||
label: t("bodyshop.labels.roguard.title"),
|
||||
children: <ShopInfoRoGuard form={form} />,
|
||||
id: "tab-shop-roguard"
|
||||
}
|
||||
],
|
||||
rome: "USE_IMEX",
|
||||
promanager: []
|
||||
})
|
||||
];
|
||||
return (
|
||||
<Card
|
||||
|
||||
116
client/src/components/shop-info/shop-info.roguard.component.jsx
Normal file
116
client/src/components/shop-info/shop-info.roguard.component.jsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import { Form, Input, InputNumber, Switch } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
|
||||
export default function ShopInfoRoGuard({ form }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<LayoutFormRow header={t("bodyshop.labels.md_ro_guard")}>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.md_ro_guard.enabled")}
|
||||
name={["md_ro_guard", "enabled"]}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<Form.Item noStyle dependencies={[["md_ro_guard", "enabled"]]}>
|
||||
{() => {
|
||||
const disabled = !form.getFieldValue(["md_ro_guard", "enabled"]);
|
||||
return (
|
||||
<LayoutFormRow noDivider>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.md_ro_guard.totalgppercent_minimum")}
|
||||
name={["md_ro_guard", "totalgppercent_minimum"]}
|
||||
rules={[
|
||||
{
|
||||
required: !disabled
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} max={100} precision={1} disabled={disabled} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.md_ro_guard.masterbypass")}
|
||||
name={["md_ro_guard", "masterbypass"]}
|
||||
rules={[
|
||||
{
|
||||
required: !disabled
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input type="password" allowClear disabled={disabled} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.md_ro_guard.enforce_bills")}
|
||||
name={["md_ro_guard", "enforce_bills"]}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch disabled={disabled} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.md_ro_guard.enforce_cm")}
|
||||
name={["md_ro_guard", "enforce_cm"]}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch disabled={disabled} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.md_ro_guard.enforce_profit")}
|
||||
name={["md_ro_guard", "enforce_profit"]}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch disabled={disabled} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.md_ro_guard.enforce_ar")}
|
||||
name={["md_ro_guard", "enforce_ar"]}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch disabled={disabled} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.md_ro_guard.enforce_sublet")}
|
||||
name={["md_ro_guard", "enforce_sublet"]}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch disabled={disabled} />
|
||||
</Form.Item>
|
||||
|
||||
{InstanceRenderManager({
|
||||
rome: (
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.md_ro_guard.enforce_ppd")}
|
||||
name={["md_ro_guard", "enforce_ppd"]}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch disabled={disabled} />
|
||||
</Form.Item>
|
||||
)
|
||||
})}
|
||||
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.md_ro_guard.enforce_labor")}
|
||||
name={["md_ro_guard", "enforce_labor"]}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch disabled={disabled} />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
382
client/src/components/task-list/task-list.component.jsx
Normal file
382
client/src/components/task-list/task-list.component.jsx
Normal file
@@ -0,0 +1,382 @@
|
||||
import { Button, Card, Space, Switch, Table } from "antd";
|
||||
import queryString from "query-string";
|
||||
import React, { useCallback, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||
import { pageLimit } from "../../utils/config";
|
||||
import dayjs from "../../utils/day";
|
||||
import {
|
||||
CheckCircleFilled,
|
||||
CheckCircleOutlined,
|
||||
DeleteFilled,
|
||||
DeleteOutlined,
|
||||
EditFilled,
|
||||
ExclamationCircleFilled,
|
||||
PlusCircleFilled,
|
||||
SyncOutlined
|
||||
} from "@ant-design/icons";
|
||||
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter.jsx";
|
||||
import { connect } from "react-redux";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
|
||||
/**
|
||||
* Task List Component
|
||||
* @param dueDate
|
||||
* @returns {Element}
|
||||
* @constructor
|
||||
*/
|
||||
const DueDateRecord = ({ dueDate }) => {
|
||||
if (!dueDate) return <></>;
|
||||
|
||||
const dueDateDayjs = dayjs(dueDate);
|
||||
const relativeDueDate = dueDateDayjs.fromNow();
|
||||
const isBeforeToday = dueDateDayjs.isBefore(dayjs());
|
||||
|
||||
return (
|
||||
<div title={relativeDueDate} style={isBeforeToday ? { color: "red" } : {}}>
|
||||
<DateFormatter>{dueDate}</DateFormatter>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const RemindAtRecord = ({ remindAt }) => {
|
||||
if (!remindAt) return <></>;
|
||||
|
||||
const remindAtDayjs = dayjs(remindAt);
|
||||
const relativeRemindAtDate = remindAtDayjs.fromNow();
|
||||
const isBeforeToday = remindAtDayjs.isBefore(dayjs());
|
||||
|
||||
return (
|
||||
<div title={relativeRemindAtDate} style={isBeforeToday ? { color: "red" } : {}}>
|
||||
<DateTimeFormatter>{remindAt}</DateTimeFormatter>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Priority Label Component
|
||||
* @param priority
|
||||
* @returns {Element}
|
||||
* @constructor
|
||||
*/
|
||||
const PriorityLabel = ({ priority }) => {
|
||||
switch (priority) {
|
||||
case 1:
|
||||
return (
|
||||
<div>
|
||||
High <ExclamationCircleFilled style={{ marginLeft: "5px", color: "red" }} />
|
||||
</div>
|
||||
);
|
||||
case 2:
|
||||
return (
|
||||
<div>
|
||||
Medium <ExclamationCircleFilled style={{ marginLeft: "5px", color: "yellow" }} />
|
||||
</div>
|
||||
);
|
||||
case 3:
|
||||
return (
|
||||
<div>
|
||||
Low <ExclamationCircleFilled style={{ marginLeft: "5px", color: "green" }} />
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<div>
|
||||
None <ExclamationCircleFilled style={{ marginLeft: "5px" }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
// Existing dispatch props...
|
||||
setTaskUpsertContext: (context) => dispatch(setModalContext({ context, modal: "taskUpsert" }))
|
||||
});
|
||||
|
||||
const mapStateToProps = (state) => ({});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(TaskListComponent);
|
||||
|
||||
function TaskListComponent({
|
||||
bodyshop,
|
||||
loading,
|
||||
tasks,
|
||||
total,
|
||||
titleTranslation,
|
||||
refetch,
|
||||
toggleCompletedStatus,
|
||||
setTaskUpsertContext,
|
||||
toggleDeletedStatus,
|
||||
relationshipType,
|
||||
relationshipId,
|
||||
onlyMine,
|
||||
parentJobId,
|
||||
query,
|
||||
showRo = true
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
|
||||
const search = queryString.parse(useLocation().search);
|
||||
|
||||
// Extract Query Params
|
||||
const { page, sortcolumn, sortorder, deleted, completed, mine } = search;
|
||||
|
||||
const history = useNavigate();
|
||||
const columns = [];
|
||||
|
||||
useEffect(() => {
|
||||
// This is a hack to force the page to change if the query params change (partssublet for example)
|
||||
}, [location]);
|
||||
|
||||
columns.push({
|
||||
title: t("tasks.fields.created_at"),
|
||||
dataIndex: "created_at",
|
||||
key: "created_at",
|
||||
width: "10%",
|
||||
defaultSortOrder: "descend",
|
||||
sorter: true,
|
||||
sortOrder: sortcolumn === "created_at" && sortorder,
|
||||
render: (text, record) => <DateTimeFormatter>{record.created_at}</DateTimeFormatter>
|
||||
});
|
||||
|
||||
if (!onlyMine) {
|
||||
columns.push({
|
||||
title: t("tasks.fields.assigned_to"),
|
||||
dataIndex: "assigned_to",
|
||||
key: "assigned_to",
|
||||
width: "8%",
|
||||
sorter: true,
|
||||
sortOrder: sortcolumn === "assigned_to" && sortorder,
|
||||
render: (text, record) => {
|
||||
const employee = bodyshop?.employees?.find((e) => e.id === record.assigned_to);
|
||||
return employee ? `${employee.first_name} ${employee.last_name}` : t("general.labels.na");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (showRo) {
|
||||
columns.push({
|
||||
title: t("tasks.fields.job.ro_number"),
|
||||
dataIndex: ["job", "ro_number"],
|
||||
key: "job.ro_number",
|
||||
width: "8%",
|
||||
render: (text, record) =>
|
||||
record.job ? (
|
||||
<Link to={`/manage/jobs/${record.job.id}?tab=tasks`}>{record.job.ro_number || t("general.labels.na")}</Link>
|
||||
) : (
|
||||
t("general.labels.na")
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
columns.push(
|
||||
{
|
||||
title: t("tasks.fields.jobline"),
|
||||
dataIndex: ["jobline", "id"],
|
||||
key: "jobline.id",
|
||||
width: "8%",
|
||||
render: (text, record) => record?.jobline?.line_desc || ""
|
||||
},
|
||||
{
|
||||
title: t("tasks.fields.parts_order"),
|
||||
dataIndex: ["parts_order", "id"],
|
||||
key: "part_order.id",
|
||||
width: "8%",
|
||||
render: (text, record) =>
|
||||
record.parts_order ? (
|
||||
<Link 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.name}`
|
||||
: t("general.labels.na")}
|
||||
</Link>
|
||||
) : (
|
||||
""
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t("tasks.fields.bill"),
|
||||
dataIndex: ["bill", "id"],
|
||||
key: "bill.id",
|
||||
width: "10%",
|
||||
render: (text, record) =>
|
||||
record.bill ? (
|
||||
<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.name}`
|
||||
: t("general.labels.na")}
|
||||
</Link>
|
||||
) : (
|
||||
""
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t("tasks.fields.title"),
|
||||
dataIndex: "title",
|
||||
key: "title",
|
||||
sorter: true,
|
||||
sortOrder: sortcolumn === "title" && sortorder
|
||||
},
|
||||
{
|
||||
title: t("tasks.fields.due_date"),
|
||||
dataIndex: "due_date",
|
||||
key: "due_date",
|
||||
sorter: true,
|
||||
sortOrder: sortcolumn === "due_date" && sortorder,
|
||||
width: "8%",
|
||||
render: (text, record) => <DueDateRecord dueDate={record.due_date} />
|
||||
},
|
||||
{
|
||||
title: t("tasks.fields.remind_at"),
|
||||
dataIndex: "remind_at",
|
||||
key: "remind_at",
|
||||
sorter: true,
|
||||
sortOrder: sortcolumn === "remind_at" && sortorder,
|
||||
width: "10%",
|
||||
render: (text, record) => <RemindAtRecord remindAt={record.remind_at} />
|
||||
},
|
||||
{
|
||||
title: t("tasks.fields.priority"),
|
||||
dataIndex: "priority",
|
||||
key: "priority",
|
||||
sorter: true,
|
||||
sortOrder: sortcolumn === "priority" && sortorder,
|
||||
width: "8%",
|
||||
render: (text, record) => <PriorityLabel priority={record.priority} />
|
||||
},
|
||||
{
|
||||
title: t("tasks.fields.actions"),
|
||||
key: "toggleCompleted",
|
||||
width: "5%",
|
||||
render: (text, record) => (
|
||||
<Space direction="horizontal">
|
||||
<Button
|
||||
title={t("tasks.buttons.edit")}
|
||||
onClick={() => {
|
||||
setTaskUpsertContext({
|
||||
context: {
|
||||
existingTask: record,
|
||||
query
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<EditFilled />
|
||||
</Button>
|
||||
<Button
|
||||
title={t("tasks.buttons.complete")}
|
||||
onClick={() => toggleCompletedStatus(record.id, record.completed)}
|
||||
>
|
||||
{record.completed ? <CheckCircleOutlined /> : <CheckCircleFilled />}
|
||||
</Button>
|
||||
<Button title={t("tasks.buttons.delete")} onClick={() => toggleDeletedStatus(record.id, record.deleted)}>
|
||||
{record.deleted ? <DeleteOutlined /> : <DeleteFilled />}
|
||||
</Button>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
);
|
||||
|
||||
const handleCreateTask = useCallback(() => {
|
||||
setTaskUpsertContext({
|
||||
actions: {},
|
||||
context: {
|
||||
jobid: parentJobId,
|
||||
[relationshipType]: relationshipId,
|
||||
query
|
||||
}
|
||||
});
|
||||
}, [parentJobId, relationshipId, relationshipType, setTaskUpsertContext, query]);
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
search.page = pagination.current;
|
||||
search.sortcolumn = sorter.columnKey;
|
||||
search.sortorder = sorter.order;
|
||||
history({ search: queryString.stringify(search) });
|
||||
};
|
||||
|
||||
const handleSwitchChange = useCallback(
|
||||
(param, value) => {
|
||||
if (value) {
|
||||
search[param] = "true";
|
||||
} else {
|
||||
delete search[param];
|
||||
}
|
||||
history({ search: queryString.stringify(search) });
|
||||
},
|
||||
[history, search]
|
||||
);
|
||||
|
||||
const expandableRow = (record) => {
|
||||
return (
|
||||
<Card title={t("tasks.fields.description")} size="small">
|
||||
{record.description}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Extra actions for the tasks
|
||||
* @type {Function}
|
||||
*/
|
||||
const tasksExtra = useCallback(() => {
|
||||
return (
|
||||
<Space direction="horizontal">
|
||||
{!onlyMine && (
|
||||
<Switch
|
||||
checkedChildren={t("tasks.buttons.myTasks")}
|
||||
unCheckedChildren={t("tasks.buttons.allTasks")}
|
||||
title={t("tasks.titles.mine")}
|
||||
checked={mine === "true"}
|
||||
onChange={(value) => handleSwitchChange("mine", value)}
|
||||
/>
|
||||
)}
|
||||
<Switch
|
||||
checkedChildren={<CheckCircleFilled />}
|
||||
unCheckedChildren={<CheckCircleOutlined />}
|
||||
title={t("tasks.titles.completed")}
|
||||
checked={completed === "true"}
|
||||
onChange={(value) => handleSwitchChange("completed", value)}
|
||||
/>
|
||||
<Switch
|
||||
checkedChildren={<DeleteFilled />}
|
||||
unCheckedChildren={<DeleteOutlined />}
|
||||
title={t("tasks.titles.deleted")}
|
||||
checked={deleted === "true"}
|
||||
onChange={(value) => handleSwitchChange("deleted", value)}
|
||||
/>
|
||||
<Button title={t("tasks.buttons.refresh")} onClick={() => refetch()}>
|
||||
<SyncOutlined />
|
||||
</Button>
|
||||
<Button title={t("tasks.buttons.create")} onClick={handleCreateTask}>
|
||||
<PlusCircleFilled />
|
||||
{t("tasks.buttons.create")}
|
||||
</Button>
|
||||
</Space>
|
||||
);
|
||||
}, [refetch, deleted, completed, mine, onlyMine, t, handleSwitchChange, handleCreateTask]);
|
||||
|
||||
return (
|
||||
<Card title={titleTranslation} extra={tasksExtra()}>
|
||||
<Table
|
||||
loading={loading}
|
||||
pagination={{
|
||||
pageSize: pageLimit,
|
||||
current: parseInt(page || 1),
|
||||
total: total,
|
||||
responsive: true,
|
||||
showQuickJumper: true
|
||||
}}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
scroll={{ x: true }}
|
||||
dataSource={tasks}
|
||||
onChange={handleTableChange}
|
||||
expandable={{
|
||||
expandedRowRender: expandableRow,
|
||||
rowExpandable: (record) => record.description
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
193
client/src/components/task-list/task-list.container.jsx
Normal file
193
client/src/components/task-list/task-list.container.jsx
Normal file
@@ -0,0 +1,193 @@
|
||||
import queryString from "query-string";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { useMutation, useQuery } from "@apollo/client";
|
||||
import { MUTATION_TOGGLE_TASK_COMPLETED, MUTATION_TOGGLE_TASK_DELETED } from "../../graphql/tasks.queries.js";
|
||||
import { pageLimit } from "../../utils/config.js";
|
||||
import AlertComponent from "../alert/alert.component.jsx";
|
||||
import React from "react";
|
||||
import TaskListComponent from "./task-list.component.jsx";
|
||||
import { notification } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect, useDispatch } from "react-redux";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions.js";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings.js";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors.js";
|
||||
import dayjs from "../../utils/day";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
currentUser: selectCurrentUser
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(TaskListContainer);
|
||||
|
||||
export function TaskListContainer({
|
||||
bodyshop,
|
||||
titleTranslation,
|
||||
query,
|
||||
relationshipType,
|
||||
relationshipId,
|
||||
currentUser,
|
||||
onlyMine,
|
||||
parentJobId,
|
||||
showRo = true,
|
||||
disableJobRefetch = false
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const searchParams = queryString.parse(useLocation().search);
|
||||
const {
|
||||
page,
|
||||
sortcolumn,
|
||||
sortorder,
|
||||
deleted,
|
||||
completed //mine
|
||||
} = searchParams;
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { loading, error, data, refetch } = useQuery(query[Object.keys(query)[0]], {
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only",
|
||||
variables: {
|
||||
bodyshop: bodyshop.id,
|
||||
[relationshipType]: relationshipId,
|
||||
deleted: deleted === "true",
|
||||
completed: completed === "true", //TODO: Find where mine is set.
|
||||
assigned_to: onlyMine ? bodyshop?.employees?.find((e) => e.user_email === currentUser.email)?.id : undefined, // replace currentUserID with the actual ID of the current user
|
||||
offset: page ? (page - 1) * pageLimit : 0,
|
||||
limit: pageLimit,
|
||||
order: [
|
||||
{
|
||||
[sortcolumn || "created_at"]: sortorder ? (sortorder === "descend" ? "desc" : "asc") : "desc"
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Toggle task completed mutation
|
||||
*/
|
||||
const [toggleTaskCompleted] = useMutation(MUTATION_TOGGLE_TASK_COMPLETED);
|
||||
|
||||
/**
|
||||
* Toggle task completed status
|
||||
* @param id
|
||||
* @param currentStatus
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const toggleCompletedStatus = async (id, currentStatus) => {
|
||||
const completed_at = !currentStatus ? dayjs().toISOString() : null;
|
||||
|
||||
try {
|
||||
const toggledTaskObject = {
|
||||
variables: {
|
||||
id: id,
|
||||
completed: !currentStatus,
|
||||
completed_at: completed_at
|
||||
},
|
||||
refetchQueries: [Object.keys(query)[0]]
|
||||
};
|
||||
|
||||
if (!disableJobRefetch) {
|
||||
toggledTaskObject.refetchQueries.push("GET_JOB_BY_PK");
|
||||
}
|
||||
|
||||
const toggledTask = await toggleTaskCompleted(toggledTaskObject);
|
||||
|
||||
if (!toggledTask.errors) {
|
||||
dispatch(
|
||||
insertAuditTrail({
|
||||
jobid: toggledTask.data.update_tasks_by_pk.jobid,
|
||||
operation: toggledTask?.data?.update_tasks_by_pk?.completed
|
||||
? AuditTrailMapping.tasksCompleted(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"
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
notification["success"]({
|
||||
message: t("tasks.successes.completed")
|
||||
});
|
||||
} catch (err) {
|
||||
notification["error"]({
|
||||
message: t("tasks.failures.completed")
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggle task deleted mutation
|
||||
*/
|
||||
const [toggleTaskDeleted] = useMutation(MUTATION_TOGGLE_TASK_DELETED);
|
||||
|
||||
/**
|
||||
* Toggle task deleted status
|
||||
* @param id
|
||||
* @param currentStatus
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
|
||||
const toggleDeletedStatus = async (id, currentStatus) => {
|
||||
const deleted_at = !currentStatus ? dayjs().toISOString() : null;
|
||||
try {
|
||||
const toggledTaskObject = {
|
||||
variables: {
|
||||
id: id,
|
||||
deleted: !currentStatus,
|
||||
deleted_at: deleted_at
|
||||
},
|
||||
refetchQueries: [Object.keys(query)[0]]
|
||||
};
|
||||
|
||||
if (!disableJobRefetch) {
|
||||
toggledTaskObject.refetchQueries.push("GET_JOB_BY_PK");
|
||||
}
|
||||
|
||||
const toggledTask = await toggleTaskDeleted(toggledTaskObject);
|
||||
|
||||
if (!toggledTask.errors) {
|
||||
dispatch(
|
||||
insertAuditTrail({
|
||||
jobid: toggledTask.data.update_tasks_by_pk.jobid,
|
||||
operation: toggledTask?.data?.update_tasks_by_pk?.deleted
|
||||
? AuditTrailMapping.tasksDeleted(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"
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
notification["success"]({
|
||||
message: t("tasks.successes.deleted")
|
||||
});
|
||||
} catch (err) {
|
||||
notification["error"]({
|
||||
message: t("tasks.failures.deleted")
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
|
||||
return (
|
||||
<TaskListComponent
|
||||
loading={loading}
|
||||
tasks={data ? data.tasks : null}
|
||||
total={data ? data.tasks_aggregate.aggregate.count : 0}
|
||||
titleTranslation={t(titleTranslation || "tasks.title")}
|
||||
refetch={refetch}
|
||||
toggleCompletedStatus={toggleCompletedStatus}
|
||||
toggleDeletedStatus={toggleDeletedStatus}
|
||||
relationshipType={relationshipType}
|
||||
relationshipId={relationshipId}
|
||||
onlyMine={onlyMine}
|
||||
showRo={showRo}
|
||||
parentJobId={parentJobId}
|
||||
bodyshop={bodyshop}
|
||||
query={query}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
import { Col, Form, Input, Row, Select, Switch } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FormDatePicker } from "../form-date-picker/form-date-picker.component.jsx";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors.js";
|
||||
import dayjs from "../../utils/day";
|
||||
import { connect } from "react-redux";
|
||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component.jsx";
|
||||
import JobSearchSelectComponent from "../job-search-select/job-search-select.component.jsx";
|
||||
import { FormDateTimePickerEnhanced } from "../form-date-time-picker-enhanced/form-date-time-picker-enhanced.component.jsx";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
currentUser: selectCurrentUser
|
||||
});
|
||||
|
||||
const mapDispatchToProps = () => ({});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(TaskUpsertModalComponent);
|
||||
|
||||
export function TaskUpsertModalComponent({
|
||||
form,
|
||||
bodyshop,
|
||||
currentUser,
|
||||
selectedJobId,
|
||||
setSelectedJobId,
|
||||
selectedJobDetails,
|
||||
existingTask,
|
||||
loading,
|
||||
error
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const datePickerPresets = [
|
||||
{ label: t("tasks.date_presets.today"), value: dayjs().add(1, "hour") },
|
||||
{ label: t("tasks.date_presets.tomorrow"), value: dayjs().add(1, "day").startOf("day") },
|
||||
{ label: t("tasks.date_presets.next_week"), value: dayjs().add(1, "week").startOf("day") },
|
||||
{ label: t("tasks.date_presets.two_weeks"), value: dayjs().add(2, "weeks").startOf("day") },
|
||||
{ label: t("tasks.date_presets.three_weeks"), value: dayjs().add(3, "weeks").startOf("day") },
|
||||
{ label: t("tasks.date_presets.one_month"), value: dayjs().add(1, "month").startOf("day") },
|
||||
{ label: t("tasks.date_presets.three_months"), value: dayjs().add(3, "month").startOf("day") }
|
||||
];
|
||||
|
||||
const generatePresets = (job) => {
|
||||
if (!job || !selectedJobDetails) return datePickerPresets; // return default presets if no job selected
|
||||
const relativePresets = [];
|
||||
|
||||
if (selectedJobDetails?.scheduled_completion) {
|
||||
const scheduledCompletion = dayjs(selectedJobDetails.scheduled_completion);
|
||||
|
||||
if (scheduledCompletion.isAfter(dayjs())) {
|
||||
relativePresets.push(
|
||||
{
|
||||
label: `${t("tasks.date_presets.completion")} -1 ${t("tasks.date_presets.day")}`,
|
||||
value: scheduledCompletion.subtract(1, "day").startOf("day")
|
||||
},
|
||||
{
|
||||
label: `${t("tasks.date_presets.completion")} -2 ${t("tasks.date_presets.days")}`,
|
||||
value: scheduledCompletion.subtract(2, "day").startOf("day")
|
||||
},
|
||||
{
|
||||
label: `${t("tasks.date_presets.completion")} -3 ${t("tasks.date_presets.days")}`,
|
||||
value: scheduledCompletion.subtract(3, "day").startOf("day")
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedJobDetails?.scheduled_delivery) {
|
||||
const scheduledDelivery = dayjs(selectedJobDetails.scheduled_delivery);
|
||||
if (scheduledDelivery.isAfter(dayjs())) {
|
||||
relativePresets.push(
|
||||
{
|
||||
label: `${t("tasks.date_presets.delivery")} -1 ${t("tasks.date_presets.day")}`,
|
||||
value: scheduledDelivery.subtract(1, "day").startOf("day")
|
||||
},
|
||||
{
|
||||
label: `${t("tasks.date_presets.delivery")} -2 ${t("tasks.date_presets.days")}`,
|
||||
value: scheduledDelivery.subtract(2, "day").startOf("day")
|
||||
},
|
||||
{
|
||||
label: `${t("tasks.date_presets.delivery")} -3 ${t("tasks.date_presets.days")}`,
|
||||
value: scheduledDelivery.subtract(3, "day").startOf("day")
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return [...relativePresets, ...datePickerPresets];
|
||||
};
|
||||
|
||||
const clearRelations = () => {
|
||||
form.setFieldsValue({
|
||||
billid: null,
|
||||
partsorderid: null,
|
||||
joblineid: null
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Change the selected job id
|
||||
* @param jobId
|
||||
*/
|
||||
const changeJobId = (jobId) => {
|
||||
setSelectedJobId(jobId || null);
|
||||
// Reset the form fields when selectedJobId changes
|
||||
clearRelations();
|
||||
};
|
||||
|
||||
if (loading || error) return <LoadingSkeleton active />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={16}>
|
||||
<Form.Item
|
||||
label={t("tasks.fields.title")}
|
||||
name="title"
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input placeholder={t("tasks.fields.title")} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Form.Item label={t("tasks.fields.priority")} name="priority" initialValue={2}>
|
||||
<Select
|
||||
options={[
|
||||
{ value: 3, label: t("tasks.fields.priorities.low") },
|
||||
{ value: 2, label: t("tasks.fields.priorities.medium") },
|
||||
{ value: 1, label: t("tasks.fields.priorities.high") }
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Form.Item
|
||||
label={t("tasks.fields.completed")}
|
||||
name="completed"
|
||||
valuePropName="checked"
|
||||
initialValue={false}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="jobid"
|
||||
initialValue={selectedJobId}
|
||||
label={t("tasks.fields.jobid")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
}
|
||||
]}
|
||||
>
|
||||
<JobSearchSelectComponent
|
||||
placeholder={t("tasks.placeholders.jobid")}
|
||||
onSelect={changeJobId}
|
||||
onClear={changeJobId}
|
||||
autoFocus={false}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={8}>
|
||||
<Form.Item label={t("tasks.fields.joblineid")} name="joblineid">
|
||||
<Select
|
||||
allowClear
|
||||
placeholder={t("tasks.placeholders.joblineid")}
|
||||
disabled={!selectedJobDetails || !selectedJobId}
|
||||
options={selectedJobDetails?.joblines?.map((jobline) => ({
|
||||
key: jobline.id,
|
||||
value: jobline.id,
|
||||
label: jobline.line_desc
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item label={t("tasks.fields.partsorderid")} name="partsorderid">
|
||||
<Select
|
||||
allowClear
|
||||
placeholder={t("tasks.placeholders.partsorderid")}
|
||||
disabled={!selectedJobDetails || !selectedJobId}
|
||||
options={selectedJobDetails?.parts_orders?.map((partsOrder) => ({
|
||||
key: partsOrder.id,
|
||||
value: partsOrder.id,
|
||||
label: `${partsOrder.order_number} - ${partsOrder.vendor.name}`
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item label={t("tasks.fields.billid")} name="billid">
|
||||
<Select
|
||||
allowClear
|
||||
placeholder={t("tasks.placeholders.billid")}
|
||||
disabled={!selectedJobDetails || !selectedJobId}
|
||||
options={selectedJobDetails?.bills?.map((bill) => ({
|
||||
key: bill.id,
|
||||
value: bill.id,
|
||||
label: `${bill.invoice_number} - ${bill.vendor.name}`
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={8}>
|
||||
<Form.Item
|
||||
label={t("tasks.fields.assigned_to")}
|
||||
name="assigned_to"
|
||||
initialValue={
|
||||
bodyshop.employees.find((employee) => employee?.user_email === currentUser.email && employee.active)?.id
|
||||
}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
placeholder={t("tasks.placeholders.assigned_to")}
|
||||
options={bodyshop.employees
|
||||
.filter((x) => x.active && x.user_email)
|
||||
.map((employee) => ({
|
||||
key: employee.id,
|
||||
value: employee.id,
|
||||
label: `${employee.first_name} ${employee.last_name}`
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item label={t("tasks.fields.due_date")} name="due_date">
|
||||
<FormDatePicker
|
||||
onlyFuture
|
||||
format="MM/DD/YYYY"
|
||||
presets={generatePresets(selectedJobDetails)}
|
||||
rules={[
|
||||
{
|
||||
validator: (_, value) => {
|
||||
if (!value || existingTask?.due_date === value || dayjs(value).isAfter(dayjs())) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(new Error(t("tasks.validation.due_at_error_message")));
|
||||
}
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item
|
||||
label={t("tasks.fields.remind_at")}
|
||||
name="remind_at"
|
||||
rules={[
|
||||
{
|
||||
validator: (_, value) => {
|
||||
if (!value || existingTask?.remind_at === value || dayjs(value).isAfter(dayjs().add(15, "minute"))) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(new Error(t("tasks.validation.remind_at_error_message")));
|
||||
}
|
||||
}
|
||||
]}
|
||||
>
|
||||
<FormDateTimePickerEnhanced
|
||||
onlyFuture
|
||||
showTime
|
||||
minuteStep={15}
|
||||
presets={generatePresets(selectedJobDetails)}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
<Form.Item label={t("tasks.fields.description")} name="description">
|
||||
<Input.TextArea rows={8} placeholder={t("tasks.fields.description")} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
import { useMutation, useQuery } from "@apollo/client";
|
||||
import { Form, Modal, notification } from "antd";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { MUTATION_INSERT_NEW_TASK, MUTATION_UPDATE_TASK, QUERY_GET_TASK_BY_ID } from "../../graphql/tasks.queries";
|
||||
import { GET_JOB_BY_PK, 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 { replaceUndefinedWithNull } from "../../utils/undefinedtonull.js";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions.js";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings.js";
|
||||
import { isEqual } from "lodash";
|
||||
import refetchRouteMappings from "./task-upsert-modal.route.mappings";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
bodyshop: selectBodyshop,
|
||||
taskUpsert: selectTaskUpsert
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
toggleModalVisible: () => dispatch(toggleModalVisible("taskUpsert")),
|
||||
insertAuditTrail: ({ jobid, billid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, billid, operation, type }))
|
||||
});
|
||||
|
||||
export function TaskUpsertModalContainer({ bodyshop, currentUser, taskUpsert, toggleModalVisible, insertAuditTrail }) {
|
||||
const { t } = useTranslation();
|
||||
const history = useNavigate();
|
||||
const [insertTask] = useMutation(MUTATION_INSERT_NEW_TASK);
|
||||
const [updateTask] = useMutation(MUTATION_UPDATE_TASK);
|
||||
const { open, context } = taskUpsert;
|
||||
const { jobid, joblineid, billid, partsorderid, taskId, existingTask, query } = context;
|
||||
const [form] = Form.useForm();
|
||||
const [selectedJobId, setSelectedJobId] = useState(null);
|
||||
const [selectedJobDetails, setSelectedJobDetails] = useState(null);
|
||||
const [jobIdState, setJobIdState] = useState(null);
|
||||
const [isTouched, setIsTouched] = useState(false);
|
||||
const location = useLocation();
|
||||
|
||||
const { loading, error, data } = useQuery(QUERY_GET_TASKS_JOB_DETAILS_BY_ID, {
|
||||
variables: { id: jobIdState },
|
||||
skip: !jobIdState
|
||||
});
|
||||
|
||||
const {
|
||||
loading: taskLoading,
|
||||
error: taskError,
|
||||
data: taskData
|
||||
} = useQuery(QUERY_GET_TASK_BY_ID, {
|
||||
variables: { id: taskId },
|
||||
skip: !taskId
|
||||
});
|
||||
// Use Effect to hydrate existing task if only a taskid is provided
|
||||
useEffect(() => {
|
||||
if (!taskLoading && !taskError && taskData && taskData?.tasks_by_pk) {
|
||||
form.setFieldsValue(taskData.tasks_by_pk);
|
||||
setSelectedJobId(taskData.tasks_by_pk.jobid);
|
||||
}
|
||||
}, [taskLoading, taskError, taskData, form]);
|
||||
|
||||
// Use Effect to hydrate selected job details
|
||||
useEffect(() => {
|
||||
if (!loading && !error && data) {
|
||||
setSelectedJobDetails(data.jobs_by_pk);
|
||||
}
|
||||
}, [loading, error, data]);
|
||||
|
||||
// Use Effect to toggle to set jobid state
|
||||
useEffect(() => {
|
||||
if (selectedJobId) {
|
||||
setJobIdState(selectedJobId);
|
||||
}
|
||||
}, [selectedJobId]);
|
||||
|
||||
// Use Effect to hydrate form fields
|
||||
useEffect(() => {
|
||||
if (jobid || existingTask?.id) {
|
||||
setSelectedJobId(jobid || existingTask.jobid);
|
||||
}
|
||||
if (existingTask && open) {
|
||||
form.setFieldsValue(existingTask);
|
||||
} else if (!existingTask && open) {
|
||||
form.resetFields();
|
||||
if (joblineid) form.setFieldsValue({ joblineid });
|
||||
if (billid) form.setFieldsValue({ billid });
|
||||
if (partsorderid) form.setFieldsValue({ partsorderid });
|
||||
}
|
||||
return () => {
|
||||
setSelectedJobId(null);
|
||||
setIsTouched(false);
|
||||
};
|
||||
}, [jobid, existingTask, form, open, joblineid, billid, partsorderid]);
|
||||
|
||||
/**
|
||||
* Remove the taskid from the URL
|
||||
*/
|
||||
const removeTaskIdFromUrl = () => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
if (!urlParams.has("taskid")) return;
|
||||
urlParams.delete("taskid");
|
||||
history(`${window.location.pathname}?${urlParams}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate refetch queries
|
||||
* @param jobId
|
||||
* @returns {*[]}
|
||||
*/
|
||||
const generateRefetchQueries = (jobId) => {
|
||||
const refetchQueries = [];
|
||||
|
||||
if (location.pathname.includes("/manage/jobs") && jobId) {
|
||||
refetchQueries.push({
|
||||
query: GET_JOB_BY_PK,
|
||||
variables: {
|
||||
id: jobId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// We have a specified query
|
||||
if (query && Object.keys(query).length) {
|
||||
refetchQueries.push(Object.keys(query)[0]);
|
||||
}
|
||||
// We don't have a specified query, check the page
|
||||
else {
|
||||
refetchRouteMappings.forEach((mapping) => {
|
||||
if (location.pathname.includes(mapping.route)) {
|
||||
refetchQueries.push(mapping.query);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return refetchQueries;
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle existing task
|
||||
* @param id
|
||||
* @param jobId
|
||||
* @param values
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const handleExistingTask = async (id, jobId, values) => {
|
||||
const task = replaceUndefinedWithNull(values);
|
||||
|
||||
// Remind at is dirty so lets clear remind_at_sent
|
||||
if (task?.remind_at) {
|
||||
task.remind_at_sent = null;
|
||||
}
|
||||
|
||||
const taskObject = {
|
||||
variables: {
|
||||
taskId: id,
|
||||
task
|
||||
}
|
||||
};
|
||||
|
||||
taskObject.refetchQueries = generateRefetchQueries(jobId);
|
||||
|
||||
const taskData = await updateTask(taskObject);
|
||||
|
||||
if (!taskData.errors) {
|
||||
const oldTask = taskData?.data?.update_tasks?.returning[0];
|
||||
|
||||
insertAuditTrail({
|
||||
jobid: oldTask.jobid,
|
||||
operation: AuditTrailMapping.tasksUpdated(oldTask.title, currentUser.email),
|
||||
type: "tasksUpdated"
|
||||
});
|
||||
}
|
||||
|
||||
notification["success"]({
|
||||
message: t("tasks.successes.updated")
|
||||
});
|
||||
|
||||
toggleModalVisible();
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle new task
|
||||
* @param values
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const handleNewTask = async (values) => {
|
||||
const taskObject = {
|
||||
variables: {
|
||||
taskInput: [
|
||||
{
|
||||
...values,
|
||||
created_by: currentUser.email,
|
||||
bodyshopid: bodyshop.id
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
taskObject.refetchQueries = generateRefetchQueries(values.jobid);
|
||||
|
||||
const newTaskData = await insertTask(taskObject);
|
||||
const newTask = newTaskData?.data?.insert_tasks?.returning[0];
|
||||
|
||||
if (!newTaskData.errors) {
|
||||
insertAuditTrail({
|
||||
jobid: newTask.jobid,
|
||||
operation: AuditTrailMapping.tasksCreated(newTask.title, currentUser.email),
|
||||
type: "tasksCreated"
|
||||
});
|
||||
}
|
||||
|
||||
form.resetFields();
|
||||
toggleModalVisible();
|
||||
|
||||
notification["success"]({
|
||||
message: t("tasks.successes.created")
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle the form submit
|
||||
* @param formValues
|
||||
* @returns {Promise<[{jobid, bodyshopid, created_by},...*]>}
|
||||
*/
|
||||
const handleFinish = async (formValues) => {
|
||||
if (existingTask || taskData?.tasks_by_pk) {
|
||||
const taskSource = existingTask || taskData?.tasks_by_pk;
|
||||
const dirtyValues = Object.keys(formValues).reduce((acc, key) => {
|
||||
if (!isEqual(formValues[key], taskSource[key])) {
|
||||
acc[key] = formValues[key];
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
try {
|
||||
await handleExistingTask(taskSource.id, taskSource.jobid, dirtyValues);
|
||||
} catch (e) {
|
||||
notification["error"]({
|
||||
message: t("tasks.failures.updated")
|
||||
});
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await handleNewTask(formValues);
|
||||
} catch (e) {
|
||||
notification["error"]({
|
||||
message: t("tasks.failures.created")
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={existingTask ? t("tasks.actions.edit") : t("tasks.actions.new")}
|
||||
open={open}
|
||||
okText={t("general.actions.save")}
|
||||
width="50%"
|
||||
onOk={() => {
|
||||
removeTaskIdFromUrl();
|
||||
form.submit();
|
||||
}}
|
||||
onCancel={() => {
|
||||
removeTaskIdFromUrl();
|
||||
toggleModalVisible();
|
||||
}}
|
||||
okButtonProps={{ disabled: !isTouched }}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={handleFinish}
|
||||
layout="vertical"
|
||||
onFieldsChange={() => {
|
||||
setIsTouched(true);
|
||||
}}
|
||||
>
|
||||
<TaskUpsertModalComponent
|
||||
form={form}
|
||||
loading={loading || (taskId && taskLoading)}
|
||||
error={error}
|
||||
data={data}
|
||||
existingTask={existingTask || taskData?.tasks_by_pk}
|
||||
selectedJobId={selectedJobId}
|
||||
setSelectedJobId={setSelectedJobId}
|
||||
selectedJobDetails={selectedJobDetails}
|
||||
/>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(TaskUpsertModalContainer);
|
||||
@@ -0,0 +1,15 @@
|
||||
import {
|
||||
QUERY_ALL_TASKS_PAGINATED,
|
||||
QUERY_JOB_TASKS_PAGINATED,
|
||||
QUERY_MY_TASKS_PAGINATED
|
||||
} from "../../graphql/tasks.queries.js";
|
||||
|
||||
const getQueryName = (query) => Object.keys(query)[0];
|
||||
|
||||
const refetchRouteMappings = [
|
||||
{query: getQueryName({QUERY_MY_TASKS_PAGINATED}), route: "/manage/tasks/mytasks"},
|
||||
{query: getQueryName({QUERY_ALL_TASKS_PAGINATED}), route: "/manage/tasks/alltasks"},
|
||||
{query: getQueryName({QUERY_JOB_TASKS_PAGINATED}), route: "/manage/jobs"}
|
||||
];
|
||||
|
||||
export default refetchRouteMappings;
|
||||
@@ -6,7 +6,8 @@ import { Link } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { alphaSort, statusSort } from "../../utils/sorters";
|
||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import { alphaSort, dateSort, statusSort } from "../../utils/sorters";
|
||||
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||
import VehicleDetailUpdateJobsComponent from "../vehicle-detail-update-jobs/vehicle-detail-update-jobs.component";
|
||||
|
||||
@@ -67,9 +68,20 @@ export function VehicleDetailJobsComponent({ vehicle, bodyshop }) {
|
||||
text: status,
|
||||
value: status
|
||||
})),
|
||||
onFilter: (value, record) => value.includes(record.status)
|
||||
onFilter: (value, record) => value.includes(record.status),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.actual_completion"),
|
||||
dataIndex: "actual_completion",
|
||||
key: "actual_completion",
|
||||
render: (text, record) => (
|
||||
<DateTimeFormatter>{record.actual_completion}</DateTimeFormatter>
|
||||
),
|
||||
sorter: (a, b) => dateSort(a.actual_completion, b.actual_completion),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "actual_completion" &&
|
||||
state.sortedInfo.order,
|
||||
},
|
||||
|
||||
{
|
||||
title: t("jobs.fields.clm_total"),
|
||||
dataIndex: "clm_total",
|
||||
|
||||
@@ -59,9 +59,14 @@ export const QUERY_BILLS_BY_JOBID = gql`
|
||||
name
|
||||
email
|
||||
}
|
||||
tasks {
|
||||
id
|
||||
due_date
|
||||
}
|
||||
order_date
|
||||
deliver_by
|
||||
return
|
||||
returnfrombill
|
||||
orderedby
|
||||
parts_order_lines {
|
||||
id
|
||||
|
||||
@@ -138,6 +138,7 @@ export const QUERY_BODYSHOP = gql`
|
||||
tt_enforce_hours_for_tech_console
|
||||
md_tasks_presets
|
||||
use_paint_scale_data
|
||||
md_ro_guard
|
||||
employee_teams(order_by: { name: asc }, where: { active: { _eq: true } }) {
|
||||
id
|
||||
name
|
||||
@@ -265,6 +266,7 @@ export const UPDATE_SHOP = gql`
|
||||
enforce_conversion_category
|
||||
tt_enforce_hours_for_tech_console
|
||||
md_tasks_presets
|
||||
md_ro_guard
|
||||
employee_teams(order_by: { name: asc }, where: { active: { _eq: true } }) {
|
||||
id
|
||||
name
|
||||
|
||||
@@ -41,7 +41,7 @@ export const QUERY_AVAILABLE_CC = gql`
|
||||
`;
|
||||
|
||||
export const CHECK_CC_FLEET_NUMBER = gql`
|
||||
query CHECK_VENDOR_NAME($name: String!) {
|
||||
query CHECK_CC_FLEET_NUMBER($name: String!) {
|
||||
courtesycars_aggregate(where: { fleetnumber: { _ilike: $name } }) {
|
||||
aggregate {
|
||||
count
|
||||
|
||||
@@ -67,6 +67,24 @@ export const QUERY_ACTIVE_EMPLOYEES = gql`
|
||||
}
|
||||
`;
|
||||
|
||||
export const QUERY_ACTIVE_EMPLOYEES_WITH_EMAIL = gql`
|
||||
query QUERY_ACTIVE_EMPLOYEES_WITH_EMAIL {
|
||||
employees(where: {active: {_eq: true}, user_email: {_is_null: false}}) {
|
||||
last_name
|
||||
id
|
||||
first_name
|
||||
employee_number
|
||||
active
|
||||
termination_date
|
||||
hire_date
|
||||
flat_rate
|
||||
rates
|
||||
pin
|
||||
user_email
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const INSERT_EMPLOYEES = gql`
|
||||
mutation INSERT_EMPLOYEES($employees: [employees_insert_input!]!) {
|
||||
insert_employees(objects: $employees) {
|
||||
|
||||
@@ -219,6 +219,12 @@ export const UPDATE_JOB_LINE = gql`
|
||||
id
|
||||
notes
|
||||
mod_lbr_ty
|
||||
mod_lb_hrs
|
||||
part_type
|
||||
op_code_desc
|
||||
prt_dsmk_m
|
||||
prt_dsmk_p
|
||||
tax_part
|
||||
part_qty
|
||||
db_price
|
||||
act_price
|
||||
|
||||
@@ -499,6 +499,11 @@ export const QUERY_JOB_COSTING_DETAILS = gql`
|
||||
export const GET_JOB_BY_PK = gql`
|
||||
query GET_JOB_BY_PK($id: uuid!) {
|
||||
jobs_by_pk(id: $id) {
|
||||
tasks_aggregate(where: { completed: { _eq: false }, deleted: { _eq: false } }) {
|
||||
aggregate {
|
||||
count
|
||||
}
|
||||
}
|
||||
actual_completion
|
||||
actual_delivery
|
||||
actual_in
|
||||
@@ -1969,6 +1974,19 @@ export const QUERY_JOB_CLOSE_DETAILS = gql`
|
||||
kmout
|
||||
qb_multiple_payers
|
||||
lbr_adjustments
|
||||
payments {
|
||||
amount
|
||||
created_at
|
||||
date
|
||||
exportedat
|
||||
id
|
||||
jobid
|
||||
memo
|
||||
payer
|
||||
paymentnum
|
||||
transactionid
|
||||
type
|
||||
}
|
||||
joblines(where: { removed: { _eq: false } }, order_by: { line_no: asc }) {
|
||||
id
|
||||
removed
|
||||
@@ -1981,6 +1999,7 @@ export const QUERY_JOB_CLOSE_DETAILS = gql`
|
||||
db_price
|
||||
act_price
|
||||
part_qty
|
||||
notes
|
||||
mod_lbr_ty
|
||||
db_hrs
|
||||
mod_lb_hrs
|
||||
@@ -1992,6 +2011,9 @@ export const QUERY_JOB_CLOSE_DETAILS = gql`
|
||||
prt_dsmk_p
|
||||
convertedtolbr
|
||||
convertedtolbr_data
|
||||
act_price_before_ppc
|
||||
sublet_ignored
|
||||
sublet_completed
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2039,12 +2061,12 @@ export const generate_UPDATE_JOB_KANBAN = (
|
||||
}`;
|
||||
|
||||
return gql`
|
||||
mutation UPDATE_JOB_KANBAN {
|
||||
${oldChildId ? oldChildQuery : ""}
|
||||
${movedId ? movedQuery : ""}
|
||||
${newChildId ? newChildQuery : ""}
|
||||
}
|
||||
`;
|
||||
mutation UPDATE_JOB_KANBAN {
|
||||
${oldChildId ? oldChildQuery : ""}
|
||||
${movedId ? movedQuery : ""}
|
||||
${newChildId ? newChildQuery : ""}
|
||||
}
|
||||
`;
|
||||
};
|
||||
|
||||
export const QUERY_JOB_LBR_ADJUSTMENTS = gql`
|
||||
@@ -2064,6 +2086,34 @@ export const DELETE_JOB = gql`
|
||||
}
|
||||
`;
|
||||
|
||||
export const QUERY_GET_TASKS_JOB_DETAILS_BY_ID = gql`
|
||||
query QUERY_GET_TASKS_JOB_DETAILS_BY_ID($id: uuid!) {
|
||||
jobs_by_pk(id: $id) {
|
||||
id
|
||||
scheduled_delivery
|
||||
scheduled_completion
|
||||
joblines {
|
||||
id
|
||||
line_desc
|
||||
}
|
||||
bills {
|
||||
id
|
||||
vendor {
|
||||
name
|
||||
}
|
||||
invoice_number
|
||||
}
|
||||
parts_orders {
|
||||
id
|
||||
vendor {
|
||||
name
|
||||
}
|
||||
order_number
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_JOB_FOR_CC_CONTRACT = gql`
|
||||
query GET_JOB_FOR_CC_CONTRACT($id: uuid!) {
|
||||
jobs_by_pk(id: $id) {
|
||||
@@ -2221,6 +2271,8 @@ export const GET_JOB_LINE_ORDERS = gql`
|
||||
parts_order_lines(where: { job_line_id: { _eq: $joblineid } }) {
|
||||
id
|
||||
act_price
|
||||
backordered_eta
|
||||
backordered_on
|
||||
parts_order {
|
||||
id
|
||||
order_date
|
||||
|
||||
@@ -67,6 +67,7 @@ export const QUERY_OWNER_BY_ID = gql`
|
||||
tax_number
|
||||
jobs(order_by: { date_open: desc }) {
|
||||
id
|
||||
actual_completion
|
||||
ro_number
|
||||
clm_no
|
||||
status
|
||||
|
||||
383
client/src/graphql/tasks.queries.js
Normal file
383
client/src/graphql/tasks.queries.js
Normal file
@@ -0,0 +1,383 @@
|
||||
import { gql } from "@apollo/client";
|
||||
|
||||
export const PARTIAL_TASK_FIELDS = gql`
|
||||
fragment TaskFields on tasks {
|
||||
id
|
||||
created_at
|
||||
updated_at
|
||||
title
|
||||
description
|
||||
deleted
|
||||
deleted_at
|
||||
due_date
|
||||
created_by
|
||||
assigned_to
|
||||
assigned_to_employee {
|
||||
id
|
||||
user_email
|
||||
}
|
||||
completed
|
||||
completed_at
|
||||
remind_at
|
||||
priority
|
||||
job {
|
||||
id
|
||||
ro_number
|
||||
joblines {
|
||||
id
|
||||
line_desc
|
||||
}
|
||||
bills {
|
||||
id
|
||||
vendor {
|
||||
name
|
||||
}
|
||||
invoice_number
|
||||
}
|
||||
parts_orders {
|
||||
id
|
||||
vendor {
|
||||
name
|
||||
}
|
||||
order_number
|
||||
}
|
||||
}
|
||||
jobid
|
||||
jobline {
|
||||
id
|
||||
line_desc
|
||||
}
|
||||
joblineid
|
||||
parts_order {
|
||||
id
|
||||
vendor {
|
||||
name
|
||||
}
|
||||
order_number
|
||||
}
|
||||
partsorderid
|
||||
bill {
|
||||
id
|
||||
vendor {
|
||||
name
|
||||
}
|
||||
invoice_number
|
||||
}
|
||||
billid
|
||||
}
|
||||
`;
|
||||
|
||||
export const QUERY_GET_TASK_BY_ID = gql`
|
||||
${PARTIAL_TASK_FIELDS}
|
||||
query QUERY_GET_TASK_BY_ID($id: uuid!) {
|
||||
tasks_by_pk(id: $id) {
|
||||
...TaskFields
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const QUERY_ALL_TASKS_PAGINATED = gql`
|
||||
${PARTIAL_TASK_FIELDS}
|
||||
query QUERY_ALL_TASKS_PAGINATED(
|
||||
$offset: Int
|
||||
$limit: Int
|
||||
$bodyshop: uuid!
|
||||
$deleted: Boolean
|
||||
$completed: Boolean
|
||||
$assigned_to: uuid
|
||||
$order: [tasks_order_by!]!
|
||||
) {
|
||||
tasks(
|
||||
offset: $offset
|
||||
limit: $limit
|
||||
order_by: $order
|
||||
where: {
|
||||
bodyshopid: { _eq: $bodyshop }
|
||||
deleted: { _eq: $deleted }
|
||||
assigned_to: { _eq: $assigned_to }
|
||||
completed: { _eq: $completed }
|
||||
}
|
||||
) {
|
||||
...TaskFields
|
||||
}
|
||||
tasks_aggregate(
|
||||
where: {
|
||||
bodyshopid: { _eq: $bodyshop }
|
||||
deleted: { _eq: $deleted }
|
||||
assigned_to: { _eq: $assigned_to }
|
||||
completed: { _eq: $completed }
|
||||
}
|
||||
) {
|
||||
aggregate {
|
||||
count
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// Query for joblineid
|
||||
export const QUERY_JOBLINE_TASKS_PAGINATED = gql`
|
||||
${PARTIAL_TASK_FIELDS}
|
||||
query QUERY_JOBLINE_TASKS_PAGINATED(
|
||||
$offset: Int
|
||||
$limit: Int
|
||||
$joblineid: uuid!
|
||||
$bodyshop: uuid!
|
||||
$deleted: Boolean
|
||||
$completed: Boolean
|
||||
$assigned_to: uuid
|
||||
$order: [tasks_order_by!]!
|
||||
) {
|
||||
tasks(
|
||||
offset: $offset
|
||||
limit: $limit
|
||||
order_by: $order
|
||||
where: {
|
||||
joblineid: { _eq: $joblineid }
|
||||
bodyshopid: { _eq: $bodyshop }
|
||||
deleted: { _eq: $deleted }
|
||||
assigned_to: { _eq: $assigned_to }
|
||||
completed: { _eq: $completed }
|
||||
}
|
||||
) {
|
||||
...TaskFields
|
||||
}
|
||||
tasks_aggregate(
|
||||
where: {
|
||||
joblineid: { _eq: $joblineid }
|
||||
bodyshopid: { _eq: $bodyshop }
|
||||
deleted: { _eq: $deleted }
|
||||
assigned_to: { _eq: $assigned_to }
|
||||
completed: { _eq: $completed }
|
||||
}
|
||||
) {
|
||||
aggregate {
|
||||
count
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// Query for partsorderid
|
||||
export const QUERY_PARTSORDER_TASKS_PAGINATED = gql`
|
||||
${PARTIAL_TASK_FIELDS}
|
||||
query QUERY_PARTSORDER_TASKS_PAGINATED(
|
||||
$offset: Int
|
||||
$limit: Int
|
||||
$partsorderid: uuid!
|
||||
$bodyshop: uuid!
|
||||
$deleted: Boolean
|
||||
$completed: Boolean
|
||||
$assigned_to: uuid
|
||||
$order: [tasks_order_by!]!
|
||||
) {
|
||||
tasks(
|
||||
offset: $offset
|
||||
limit: $limit
|
||||
order_by: $order
|
||||
where: {
|
||||
partsorderid: { _eq: $partsorderid }
|
||||
bodyshopid: { _eq: $bodyshop }
|
||||
deleted: { _eq: $deleted }
|
||||
assigned_to: { _eq: $assigned_to }
|
||||
completed: { _eq: $completed }
|
||||
}
|
||||
) {
|
||||
...TaskFields
|
||||
}
|
||||
tasks_aggregate(
|
||||
where: {
|
||||
partsorderid: { _eq: $partsorderid }
|
||||
bodyshopid: { _eq: $bodyshop }
|
||||
deleted: { _eq: $deleted }
|
||||
assigned_to: { _eq: $assigned_to }
|
||||
completed: { _eq: $completed }
|
||||
}
|
||||
) {
|
||||
aggregate {
|
||||
count
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// Query for billid
|
||||
export const QUERY_BILL_TASKS_PAGINATED = gql`
|
||||
${PARTIAL_TASK_FIELDS}
|
||||
query QUERY_BILL_TASKS_PAGINATED(
|
||||
$offset: Int
|
||||
$limit: Int
|
||||
$billid: uuid!
|
||||
$bodyshop: uuid!
|
||||
$deleted: Boolean
|
||||
$completed: Boolean
|
||||
$assigned_to: uuid
|
||||
$order: [tasks_order_by!]!
|
||||
) {
|
||||
tasks(
|
||||
offset: $offset
|
||||
limit: $limit
|
||||
order_by: $order
|
||||
where: {
|
||||
billid: { _eq: $billid }
|
||||
bodyshopid: { _eq: $bodyshop }
|
||||
deleted: { _eq: $deleted }
|
||||
assigned_to: { _eq: $assigned_to }
|
||||
completed: { _eq: $completed }
|
||||
}
|
||||
) {
|
||||
...TaskFields
|
||||
}
|
||||
tasks_aggregate(
|
||||
where: {
|
||||
billid: { _eq: $billid }
|
||||
bodyshopid: { _eq: $bodyshop }
|
||||
deleted: { _eq: $deleted }
|
||||
assigned_to: { _eq: $assigned_to }
|
||||
completed: { _eq: $completed }
|
||||
}
|
||||
) {
|
||||
aggregate {
|
||||
count
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// Use the fragment in your queries
|
||||
export const QUERY_JOB_TASKS_PAGINATED = gql`
|
||||
${PARTIAL_TASK_FIELDS}
|
||||
query QUERY_JOB_TASKS_PAGINATED(
|
||||
$offset: Int
|
||||
$limit: Int
|
||||
$jobid: uuid!
|
||||
$bodyshop: uuid!
|
||||
$deleted: Boolean
|
||||
$completed: Boolean
|
||||
$assigned_to: uuid
|
||||
$order: [tasks_order_by!]!
|
||||
) {
|
||||
tasks(
|
||||
offset: $offset
|
||||
limit: $limit
|
||||
order_by: $order
|
||||
where: {
|
||||
jobid: { _eq: $jobid }
|
||||
bodyshopid: { _eq: $bodyshop }
|
||||
deleted: { _eq: $deleted }
|
||||
assigned_to: { _eq: $assigned_to }
|
||||
completed: { _eq: $completed }
|
||||
}
|
||||
) {
|
||||
...TaskFields
|
||||
}
|
||||
tasks_aggregate(
|
||||
where: {
|
||||
jobid: { _eq: $jobid }
|
||||
bodyshopid: { _eq: $bodyshop }
|
||||
deleted: { _eq: $deleted }
|
||||
assigned_to: { _eq: $assigned_to }
|
||||
completed: { _eq: $completed }
|
||||
}
|
||||
) {
|
||||
aggregate {
|
||||
count
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const QUERY_MY_TASKS_PAGINATED = gql`
|
||||
${PARTIAL_TASK_FIELDS}
|
||||
query QUERY_MY_TASKS_PAGINATED(
|
||||
$offset: Int
|
||||
$limit: Int
|
||||
$assigned_to: uuid!
|
||||
$bodyshop: uuid!
|
||||
$deleted: Boolean
|
||||
$completed: Boolean
|
||||
$order: [tasks_order_by!]!
|
||||
) {
|
||||
tasks(
|
||||
offset: $offset
|
||||
limit: $limit
|
||||
order_by: $order
|
||||
where: {
|
||||
assigned_to: { _eq: $assigned_to }
|
||||
bodyshopid: { _eq: $bodyshop }
|
||||
deleted: { _eq: $deleted }
|
||||
completed: { _eq: $completed }
|
||||
}
|
||||
) {
|
||||
...TaskFields
|
||||
}
|
||||
tasks_aggregate(
|
||||
where: {
|
||||
assigned_to: { _eq: $assigned_to }
|
||||
bodyshopid: { _eq: $bodyshop }
|
||||
deleted: { _eq: $deleted }
|
||||
completed: { _eq: $completed }
|
||||
}
|
||||
) {
|
||||
aggregate {
|
||||
count
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* Toggle task completed mutation
|
||||
* @type {DocumentNode}
|
||||
*/
|
||||
export const MUTATION_TOGGLE_TASK_COMPLETED = gql`
|
||||
${PARTIAL_TASK_FIELDS}
|
||||
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 }) {
|
||||
...TaskFields
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* Toggle task deleted mutation
|
||||
* @type {DocumentNode}
|
||||
*/
|
||||
export const MUTATION_TOGGLE_TASK_DELETED = gql`
|
||||
${PARTIAL_TASK_FIELDS}
|
||||
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 }) {
|
||||
...TaskFields
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* Insert new task mutation
|
||||
* @type {DocumentNode}
|
||||
*/
|
||||
export const MUTATION_INSERT_NEW_TASK = gql`
|
||||
${PARTIAL_TASK_FIELDS}
|
||||
mutation MUTATION_INSERT_NEW_TASK($taskInput: [tasks_insert_input!]!) {
|
||||
insert_tasks(objects: $taskInput) {
|
||||
returning {
|
||||
...TaskFields
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* Update task mutation
|
||||
* @type {DocumentNode}
|
||||
*/
|
||||
export const MUTATION_UPDATE_TASK = gql`
|
||||
${PARTIAL_TASK_FIELDS}
|
||||
mutation MUTATION_UPDATE_TASK($taskId: uuid!, $task: tasks_set_input!) {
|
||||
update_tasks(where: { id: { _eq: $taskId } }, _set: $task) {
|
||||
returning {
|
||||
...TaskFields
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -30,6 +30,7 @@ export const QUERY_VEHICLE_BY_ID = gql`
|
||||
notes
|
||||
jobs(order_by: { date_open: desc }) {
|
||||
id
|
||||
actual_completion
|
||||
ro_number
|
||||
ownr_co_nm
|
||||
ownr_fn
|
||||
|
||||
@@ -216,7 +216,7 @@ export function BillsListPage({ loading, data, refetch, total, setPartsOrderCont
|
||||
extra={
|
||||
<Space wrap>
|
||||
{search.search && (
|
||||
<>
|
||||
<Space align="center">
|
||||
<Typography.Title level={4}>
|
||||
{t("general.labels.searchresults", { search: search.search })}
|
||||
</Typography.Title>
|
||||
@@ -229,7 +229,7 @@ export function BillsListPage({ loading, data, refetch, total, setPartsOrderCont
|
||||
>
|
||||
{t("general.actions.clear")}
|
||||
</Button>
|
||||
</>
|
||||
</Space>
|
||||
)}
|
||||
<Button onClick={() => refetch()}>
|
||||
<SyncOutlined />
|
||||
|
||||
@@ -72,7 +72,11 @@ export function CourtesyCarCreateContainer({ bodyshop, setBreadcrumbs, setSelect
|
||||
<RbacWrapper action="courtesycar:create">
|
||||
<FeatureWrapperComponent featureName="courtesycars">
|
||||
<Form form={form} autoComplete="new-password" onFinish={handleFinish} layout="vertical">
|
||||
<CourtesyCarFormComponent form={form} saveLoading={loading} />
|
||||
<CourtesyCarFormComponent
|
||||
form={form}
|
||||
saveLoading={loading}
|
||||
newCC={true}
|
||||
/>
|
||||
</Form>
|
||||
</FeatureWrapperComponent>
|
||||
</RbacWrapper>
|
||||
|
||||
@@ -51,6 +51,17 @@ export function JobsAvailablePageContainer({ partnerVersion, setBreadcrumbs, set
|
||||
{!partnerVersion && (
|
||||
<AlertComponent
|
||||
type="warning"
|
||||
action={
|
||||
<a
|
||||
href={InstanceRenderManager({
|
||||
imex: "https://partner.imex.online/Setup.exe",
|
||||
rome: "https://partner.romeonline.io/Setup.exe",
|
||||
promanager: "https://dzaenazwrgg60.cloudfront.net/Setup.exe"
|
||||
})}
|
||||
>
|
||||
<Button size="small">{t("general.actions.download")}</Button>
|
||||
</a>
|
||||
}
|
||||
message={t("general.messages.partnernotrunning", {
|
||||
app: InstanceRenderManager({
|
||||
imex: "$t(titles.imexonline)",
|
||||
|
||||
@@ -41,6 +41,7 @@ import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import JobCloseRoGuardContainer from "../../components/job-close-ro-guard/job-close-ro-guard.container";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -119,6 +120,13 @@ export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail }) {
|
||||
operation: AuditTrailMapping.jobinvoiced(),
|
||||
type: "jobinvoiced"
|
||||
});
|
||||
if (values.masterbypass) {
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.jobclosedwithbypass(),
|
||||
type: "jobclosedwithbypass"
|
||||
});
|
||||
}
|
||||
// history(`/manage/jobs/${job.id}`);
|
||||
} else {
|
||||
setLoading(false);
|
||||
@@ -183,7 +191,7 @@ export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail }) {
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
|
||||
<JobCloseRoGuardContainer form={form} job={job} />
|
||||
<Space wrap direction="vertical" style={{ width: "100%" }}>
|
||||
<FormsFieldChanged form={form} />
|
||||
{!job.actual_in && job.scheduled_in && <Alert type="warning" message={t("jobs.labels.actual_in_inferred")} />}
|
||||
|
||||
@@ -20,18 +20,21 @@ export default function JobsCreateComponent({ form }) {
|
||||
const steps = [
|
||||
{
|
||||
title: t("jobs.labels.create.vehicleinfo"),
|
||||
id: "step-job-vehicleinfo",
|
||||
content: <JobsCreateVehicleInfoContainer form={form} />,
|
||||
validation: !!state.vehicle.new || !!state.vehicle.selectedid || !!state.vehicle.none,
|
||||
error: t("vehicles.errors.selectexistingornew")
|
||||
},
|
||||
{
|
||||
title: t("jobs.labels.create.ownerinfo"),
|
||||
id: "step-job-ownerinfo",
|
||||
content: <JobsCreateOwnerInfoContainer />,
|
||||
validation: !!state.owner.new || !!state.owner.selectedid,
|
||||
error: t("owners.errors.selectexistingornew")
|
||||
},
|
||||
{
|
||||
title: t("jobs.labels.create.jobinfo"),
|
||||
id: "step-job-jobinfo",
|
||||
content: <JobsCreateJobsInfo form={form} selected={pageIndex === 2} />
|
||||
}
|
||||
];
|
||||
|
||||
@@ -8,7 +8,7 @@ import Icon, {
|
||||
SyncOutlined,
|
||||
ToolFilled
|
||||
} from "@ant-design/icons";
|
||||
import { 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 Axios from "axios";
|
||||
@@ -16,7 +16,7 @@ import dayjs from "../../utils/day";
|
||||
import queryString from "query-string";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FaHardHat, FaRegStickyNote, FaShieldAlt } from "react-icons/fa";
|
||||
import { FaHardHat, FaRegStickyNote, FaShieldAlt, FaTasks } from "react-icons/fa";
|
||||
import { connect } from "react-redux";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
@@ -53,14 +53,29 @@ import JobProfileDataWarning from "../../components/job-profile-data-warning/job
|
||||
import { DateTimeFormat } from "../../utils/DateFormatter";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import { HasFeatureAccess } from "../../components/feature-wrapper/feature-wrapper.component";
|
||||
import TaskListContainer from "../../components/task-list/task-list.container.jsx";
|
||||
import { QUERY_JOB_TASKS_PAGINATED } from "../../graphql/tasks.queries.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
jobRO: selectJobReadOnly
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setPrintCenterContext: (context) => dispatch(setModalContext({ context: context, modal: "printCenter" })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
|
||||
setPrintCenterContext: (context) =>
|
||||
dispatch(
|
||||
setModalContext({
|
||||
context: context,
|
||||
modal: "printCenter"
|
||||
})
|
||||
),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(
|
||||
insertAuditTrail({
|
||||
jobid,
|
||||
operation,
|
||||
type
|
||||
})
|
||||
)
|
||||
});
|
||||
|
||||
export function JobsDetailPage({
|
||||
@@ -69,7 +84,6 @@ export function JobsDetailPage({
|
||||
jobRO,
|
||||
job,
|
||||
mutationUpdateJob,
|
||||
handleSubmit,
|
||||
insertAuditTrail,
|
||||
refetch
|
||||
}) {
|
||||
@@ -365,6 +379,25 @@ export function JobsDetailPage({
|
||||
icon: <HistoryOutlined />,
|
||||
label: t("jobs.labels.audit"),
|
||||
children: <JobAuditTrail jobId={job.id} />
|
||||
},
|
||||
{
|
||||
key: "tasks",
|
||||
icon: <FaTasks />,
|
||||
label: (
|
||||
<Space direction="horizontal">
|
||||
{t("jobs.labels.tasks")}
|
||||
{job.tasks_aggregate.aggregate.count > 0 && <Badge count={job.tasks_aggregate.aggregate.count} />}
|
||||
</Space>
|
||||
),
|
||||
children: (
|
||||
<TaskListContainer
|
||||
relationshipType={"jobid"}
|
||||
relationshipId={job.id}
|
||||
query={{ QUERY_JOB_TASKS_PAGINATED }}
|
||||
titleTranslation="tasks.titles.job_tasks"
|
||||
showRo={false}
|
||||
/>
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FloatButton, Layout, Spin, Collapse, Button, Space, Tag } from "antd";
|
||||
import { FloatButton, Layout, Spin } from "antd";
|
||||
// import preval from "preval.macro";
|
||||
import React, { lazy, Suspense, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -30,8 +30,8 @@ import "./manage.page.styles.scss";
|
||||
|
||||
const JobsPage = lazy(() => import("../jobs/jobs.page"));
|
||||
|
||||
const CardPaymentModalContainer = lazy(() =>
|
||||
import("../../components/card-payment-modal/card-payment-modal.container.")
|
||||
const CardPaymentModalContainer = lazy(
|
||||
() => import("../../components/card-payment-modal/card-payment-modal.container.")
|
||||
);
|
||||
|
||||
const JobsDetailPage = lazy(() => import("../jobs-detail/jobs-detail.page.container"));
|
||||
@@ -59,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 BillEnterModalContainer = lazy(() => import("../../components/bill-enter-modal/bill-enter-modal.container"));
|
||||
const TimeTicketModalContainer = lazy(() => import("../../components/time-ticket-modal/time-ticket-modal.container"));
|
||||
const TimeTicketModalTask = lazy(() =>
|
||||
import("../../components/time-ticket-task-modal/time-ticket-task-modal.container")
|
||||
const TimeTicketModalTask = lazy(
|
||||
() => import("../../components/time-ticket-task-modal/time-ticket-task-modal.container")
|
||||
);
|
||||
const PaymentModalContainer = lazy(() => import("../../components/payment-modal/payment-modal.container"));
|
||||
const ProductionListPage = lazy(() => import("../production-list/production-list.container"));
|
||||
@@ -97,7 +97,10 @@ const Dms = lazy(() => import("../dms/dms.container"));
|
||||
const DmsPayables = lazy(() => import("../dms-payables/dms-payables.container"));
|
||||
const ManageRootPage = lazy(() => import("../manage-root/manage-root.page.container"));
|
||||
const TtApprovals = lazy(() => import("../tt-approvals/tt-approvals.page.container"));
|
||||
const MyTasksPage = lazy(() => import("../tasks/myTasksPageContainer.jsx"));
|
||||
const AllTasksPage = lazy(() => import("../tasks/allTasksPageContainer.jsx"));
|
||||
|
||||
const TaskUpsertModalContainer = lazy(() => import("../../components/task-upsert-modal/task-upsert-modal.container"));
|
||||
const { Content, Footer } = Layout;
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
@@ -114,7 +117,6 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
export function Manage({ conflict, bodyshop, enableJoyRide, joyRideSteps, setJoyRideFinished }) {
|
||||
const { t } = useTranslation();
|
||||
const [chatVisible] = useState(false);
|
||||
const [tours, setTours] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const widgetId = InstanceRenderManager({
|
||||
@@ -147,12 +149,10 @@ export function Manage({ conflict, bodyshop, enableJoyRide, joyRideSteps, setJoy
|
||||
})}
|
||||
/>
|
||||
}
|
||||
This
|
||||
>
|
||||
<PaymentModalContainer />
|
||||
|
||||
<CardPaymentModalContainer />
|
||||
|
||||
<TaskUpsertModalContainer />
|
||||
<BreadCrumbs />
|
||||
<BillEnterModalContainer />
|
||||
<JobCostingModal />
|
||||
@@ -252,6 +252,22 @@ export function Manage({ conflict, bodyshop, enableJoyRide, joyRideSteps, setJoy
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/tasks/mytasks"
|
||||
element={
|
||||
<Suspense fallback={<Spin />}>
|
||||
<MyTasksPage />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/tasks/alltasks"
|
||||
element={
|
||||
<Suspense fallback={<Spin />}>
|
||||
<AllTasksPage />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/inventory/"
|
||||
element={
|
||||
@@ -613,28 +629,6 @@ export function Manage({ conflict, bodyshop, enableJoyRide, joyRideSteps, setJoy
|
||||
Disclaimer & Notices
|
||||
</Link>
|
||||
</div>
|
||||
{InstanceRenderManager({
|
||||
promanager: (
|
||||
<Collapse>
|
||||
<Collapse.Panel header="DEVELOPMENT ONLY - ProductFruits Tours">
|
||||
<Space>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
setTours(await window.productFruits.api.tours.getTours());
|
||||
}}
|
||||
>
|
||||
Get Tours
|
||||
</Button>
|
||||
{tours.map((tour) => (
|
||||
<Tag key={tour.id} onClick={() => window.productFruits.api.tours.tryStartTour(tour.id)}>
|
||||
{tour.name}
|
||||
</Tag>
|
||||
))}
|
||||
</Space>
|
||||
</Collapse.Panel>
|
||||
</Collapse>
|
||||
)
|
||||
})}
|
||||
</Footer>
|
||||
</Layout>
|
||||
</>
|
||||
|
||||
60
client/src/pages/tasks/allTasksPageContainer.jsx
Normal file
60
client/src/pages/tasks/allTasksPageContainer.jsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import TasksPageComponent from "./tasks.page.component";
|
||||
import queryString from "query-string";
|
||||
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr.js";
|
||||
import TaskPageTypes from "./taskPageTypes.jsx";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions.js";
|
||||
import { useLocation } from "react-router-dom";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
|
||||
setSelectedHeader: (key) => dispatch(setSelectedHeader(key)),
|
||||
setTaskUpsertContext: (context) => dispatch(setModalContext({ context, modal: "taskUpsert" }))
|
||||
});
|
||||
|
||||
export function AllTasksPageContainer({ setBreadcrumbs, setSelectedHeader, setTaskUpsertContext }) {
|
||||
const { t } = useTranslation();
|
||||
const searchParams = queryString.parse(useLocation().search);
|
||||
useEffect(() => {
|
||||
document.title = t("titles.all_tasks", {
|
||||
app: InstanceRenderManager({
|
||||
imex: "$t(titles.imexonline)",
|
||||
rome: "$t(titles.romeonline)",
|
||||
promanager: "$t(titles.promanager)"
|
||||
})
|
||||
});
|
||||
setSelectedHeader("all_tasks");
|
||||
setBreadcrumbs([
|
||||
{
|
||||
link: "/manage/tasks/alltasks",
|
||||
label: t("titles.bc.all_tasks")
|
||||
}
|
||||
]);
|
||||
}, [t, setBreadcrumbs, setSelectedHeader]);
|
||||
|
||||
// This takes care of the ability to deep link a task from the URL (Dispatches the modal)
|
||||
useEffect(() => {
|
||||
// Check for a query string in the URL
|
||||
const urlParams = new URLSearchParams(searchParams);
|
||||
const taskId = urlParams.get("taskid");
|
||||
if (taskId) {
|
||||
setTaskUpsertContext({
|
||||
context: {
|
||||
taskId
|
||||
}
|
||||
});
|
||||
urlParams.delete("taskid");
|
||||
}
|
||||
}, [setTaskUpsertContext, searchParams]);
|
||||
|
||||
return <TasksPageComponent type={TaskPageTypes.ALL_TASKS} />;
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(AllTasksPageContainer);
|
||||
40
client/src/pages/tasks/myTasksPageContainer.jsx
Normal file
40
client/src/pages/tasks/myTasksPageContainer.jsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import TasksPageComponent from "./tasks.page.component";
|
||||
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr.js";
|
||||
import TaskPageTypes from "./taskPageTypes.jsx";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
|
||||
setSelectedHeader: (key) => dispatch(setSelectedHeader(key))
|
||||
});
|
||||
|
||||
export function MyTasksPageContainer({ setBreadcrumbs, setSelectedHeader }) {
|
||||
const { t } = useTranslation();
|
||||
useEffect(() => {
|
||||
document.title = t("titles.my_tasks", {
|
||||
app: InstanceRenderManager({
|
||||
imex: "$t(titles.imexonline)",
|
||||
rome: "$t(titles.romeonline)",
|
||||
promanager: "$t(titles.promanager)"
|
||||
})
|
||||
});
|
||||
setSelectedHeader("my_tasks");
|
||||
setBreadcrumbs([
|
||||
{
|
||||
link: "/manage/tasks/mytasks",
|
||||
label: t("titles.bc.my_tasks")
|
||||
}
|
||||
]);
|
||||
}, [t, setBreadcrumbs, setSelectedHeader]);
|
||||
|
||||
return <TasksPageComponent type={TaskPageTypes.MY_TASKS} />;
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(MyTasksPageContainer);
|
||||
6
client/src/pages/tasks/taskPageTypes.jsx
Normal file
6
client/src/pages/tasks/taskPageTypes.jsx
Normal file
@@ -0,0 +1,6 @@
|
||||
export const TaskPageTypes = {
|
||||
MY_TASKS: "myTasks",
|
||||
ALL_TASKS: "allTasks"
|
||||
};
|
||||
|
||||
export default TaskPageTypes;
|
||||
41
client/src/pages/tasks/tasks.page.component.jsx
Normal file
41
client/src/pages/tasks/tasks.page.component.jsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import React from "react";
|
||||
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 taskPageTypes from "./taskPageTypes.jsx";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors.js";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(TasksPageComponent);
|
||||
|
||||
export function TasksPageComponent({ bodyshop, currentUser, type }) {
|
||||
switch (type) {
|
||||
case taskPageTypes.MY_TASKS:
|
||||
return (
|
||||
<TaskListContainer
|
||||
onlyMine={true}
|
||||
relationshipId={bodyshop?.employees?.find((e) => e.user_email === currentUser.email)?.id}
|
||||
relationshipType={"assigned_to"}
|
||||
query={{ QUERY_MY_TASKS_PAGINATED }}
|
||||
titleTranslation={"tasks.titles.my_tasks"}
|
||||
disableJobRefetch={true}
|
||||
/>
|
||||
);
|
||||
case taskPageTypes.ALL_TASKS:
|
||||
return (
|
||||
<TaskListContainer
|
||||
query={{ QUERY_ALL_TASKS_PAGINATED }}
|
||||
titleTranslation={"tasks.titles.all_tasks"}
|
||||
disableJobRefetch={true}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
}
|
||||
@@ -111,7 +111,12 @@ export function* calculateScheduleLoad({ payload: end }) {
|
||||
(load[itemDate].allHoursIn || 0) +
|
||||
item.labhrs.aggregate.sum.mod_lb_hrs +
|
||||
item.larhrs.aggregate.sum.mod_lb_hrs;
|
||||
|
||||
load[itemDate].allHoursInBody =
|
||||
(load[itemDate].allHoursInBody || 0) +
|
||||
item.labhrs.aggregate.sum.mod_lb_hrs;
|
||||
load[itemDate].allHoursInRefinish =
|
||||
(load[itemDate].allHoursInRefinish || 0) +
|
||||
item.larhrs.aggregate.sum.mod_lb_hrs;
|
||||
//If the job hasn't already arrived, add it to the jobs in list.
|
||||
// Make sure it also hasn't already been completed, or isn't an in and out job.
|
||||
//This prevents the duplicate counting.
|
||||
@@ -119,7 +124,15 @@ export function* calculateScheduleLoad({ payload: end }) {
|
||||
if (AddJobForSchedulingCalc) {
|
||||
load[itemDate].jobsIn.push(item);
|
||||
load[itemDate].hoursIn =
|
||||
(load[itemDate].hoursIn || 0) + item.labhrs.aggregate.sum.mod_lb_hrs + item.larhrs.aggregate.sum.mod_lb_hrs;
|
||||
(load[itemDate].hoursIn || 0) +
|
||||
item.labhrs.aggregate.sum.mod_lb_hrs +
|
||||
item.larhrs.aggregate.sum.mod_lb_hrs;
|
||||
load[itemDate].hoursInBody =
|
||||
(load[itemDate].hoursInBody || 0) +
|
||||
item.labhrs.aggregate.sum.mod_lb_hrs;
|
||||
load[itemDate].hoursInRefinish =
|
||||
(load[itemDate].hoursInRefinish || 0) +
|
||||
item.larhrs.aggregate.sum.mod_lb_hrs;
|
||||
}
|
||||
} else {
|
||||
load[itemDate] = {
|
||||
@@ -127,10 +140,21 @@ export function* calculateScheduleLoad({ payload: end }) {
|
||||
jobsIn: AddJobForSchedulingCalc ? [item] : [], //Same as above, only add it if it isn't already in production.
|
||||
jobsOut: [],
|
||||
allJobsOut: [],
|
||||
allHoursIn: item.labhrs.aggregate.sum.mod_lb_hrs + item.larhrs.aggregate.sum.mod_lb_hrs,
|
||||
allHoursIn:
|
||||
item.labhrs.aggregate.sum.mod_lb_hrs +
|
||||
item.larhrs.aggregate.sum.mod_lb_hrs,
|
||||
allHoursInBody: item.labhrs.aggregate.sum.mod_lb_hrs,
|
||||
allHoursInRefinish: item.larhrs.aggregate.sum.mod_lb_hrs,
|
||||
hoursIn: AddJobForSchedulingCalc
|
||||
? item.labhrs.aggregate.sum.mod_lb_hrs + item.larhrs.aggregate.sum.mod_lb_hrs
|
||||
: 0
|
||||
? item.labhrs.aggregate.sum.mod_lb_hrs +
|
||||
item.larhrs.aggregate.sum.mod_lb_hrs
|
||||
: 0,
|
||||
hoursInBody: AddJobForSchedulingCalc
|
||||
? item.labhrs.aggregate.sum.mod_lb_hrs
|
||||
: 0,
|
||||
hoursInRefinish: AddJobForSchedulingCalc
|
||||
? item.larhrs.aggregate.sum.mod_lb_hrs
|
||||
: 0,
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -151,6 +175,12 @@ export function* calculateScheduleLoad({ payload: end }) {
|
||||
(load[itemDate].allHoursOut || 0) +
|
||||
item.labhrs.aggregate.sum.mod_lb_hrs +
|
||||
item.larhrs.aggregate.sum.mod_lb_hrs;
|
||||
load[itemDate].allHoursOutBody =
|
||||
(load[itemDate].allHoursOutBody || 0) +
|
||||
item.labhrs.aggregate.sum.mod_lb_hrs;
|
||||
load[itemDate].allHoursOutRefinish =
|
||||
(load[itemDate].allHoursOutRefinish || 0) +
|
||||
item.larhrs.aggregate.sum.mod_lb_hrs;
|
||||
//Add only the jobs that are still in production to get rid of.
|
||||
//If it's not in production, we'd subtract unnecessarily.
|
||||
load[itemDate].allJobsOut.push(item);
|
||||
@@ -161,6 +191,12 @@ export function* calculateScheduleLoad({ payload: end }) {
|
||||
(load[itemDate].hoursOut || 0) +
|
||||
item.labhrs.aggregate.sum.mod_lb_hrs +
|
||||
item.larhrs.aggregate.sum.mod_lb_hrs;
|
||||
load[itemDate].hoursOutBody =
|
||||
(load[itemDate].hoursOutBody || 0) +
|
||||
item.labhrs.aggregate.sum.mod_lb_hrs;
|
||||
load[itemDate].hoursOutRefinish =
|
||||
(load[itemDate].hoursOutRefinish || 0) +
|
||||
item.larhrs.aggregate.sum.mod_lb_hrs;
|
||||
}
|
||||
} else {
|
||||
load[itemDate] = {
|
||||
@@ -169,7 +205,11 @@ export function* calculateScheduleLoad({ payload: end }) {
|
||||
hoursOut: AddJobForSchedulingCalc
|
||||
? item.labhrs.aggregate.sum.mod_lb_hrs + item.larhrs.aggregate.sum.mod_lb_hrs
|
||||
: 0,
|
||||
allHoursOut: item.labhrs.aggregate.sum.mod_lb_hrs + item.larhrs.aggregate.sum.mod_lb_hrs
|
||||
allHoursOut:
|
||||
item.labhrs.aggregate.sum.mod_lb_hrs +
|
||||
item.larhrs.aggregate.sum.mod_lb_hrs,
|
||||
allHoursOutBody: item.labhrs.aggregate.sum.mod_lb_hrs,
|
||||
allHoursOutRefinish: item.larhrs.aggregate.sum.mod_lb_hrs,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -8,7 +8,8 @@ const INITIAL_STATE = {
|
||||
name: "ShopName",
|
||||
address: InstanceRenderManager({
|
||||
imex: "noreply@iemx.online",
|
||||
rome: "noreply@romeonline.io"
|
||||
rome: "noreply@romeonline.io",
|
||||
promanager: "noreply@promanager.web-est.com"
|
||||
})
|
||||
},
|
||||
to: null,
|
||||
|
||||
@@ -13,6 +13,7 @@ const INITIAL_STATE = {
|
||||
billEnter: { ...baseModal },
|
||||
courtesyCarReturn: { ...baseModal },
|
||||
noteUpsert: { ...baseModal },
|
||||
taskUpsert: { ...baseModal },
|
||||
schedule: { ...baseModal },
|
||||
partsOrder: { ...baseModal },
|
||||
timeTicket: { ...baseModal },
|
||||
|
||||
@@ -10,6 +10,8 @@ export const selectCourtesyCarReturn = createSelector([selectModals], (modals) =
|
||||
|
||||
export const selectNoteUpsert = createSelector([selectModals], (modals) => modals.noteUpsert);
|
||||
|
||||
export const selectTaskUpsert = createSelector([selectModals], (modals) => modals.taskUpsert);
|
||||
|
||||
export const selectSchedule = createSelector([selectModals], (modals) => modals.schedule);
|
||||
|
||||
export const selectPartsOrder = createSelector([selectModals], (modals) => modals.partsOrder);
|
||||
|
||||
@@ -116,6 +116,7 @@
|
||||
"jobassignmentchange": "Employee {{name}} assigned to {{operation}}",
|
||||
"jobassignmentremoved": "Employee assignment removed for {{operation}}",
|
||||
"jobchecklist": "Checklist type \"{{type}}\" completed. In production set to {{inproduction}}. Status set to {{status}}.",
|
||||
"jobclosedwithbypass": "Job was invoiced using the master bypass password. ",
|
||||
"jobconverted": "Job converted and assigned number {{ro_number}}.",
|
||||
"jobdelivery": "Job intake completed. Status set to {{status}}. Actual completion is {{actual_completion}}.",
|
||||
"jobexported": "",
|
||||
@@ -134,7 +135,13 @@
|
||||
"jobstatuschange": "Job status changed to {{status}}.",
|
||||
"jobsupplement": "Job supplement imported.",
|
||||
"jobsuspend": "Suspend Toggle set to {{status}}",
|
||||
"jobvoid": "Job has been voided."
|
||||
"jobvoid": "Job has been voided.",
|
||||
"tasks_completed": "Task '{{title}}' completed by {{completedBy}}",
|
||||
"tasks_created": "Task '{{title}}' created by {{createdBy}}",
|
||||
"tasks_deleted": "Task '{{title}}' deleted by {{deletedBy}}",
|
||||
"tasks_uncompleted": "Task '{{title}}' uncompleted by {{uncompletedBy}}",
|
||||
"tasks_undeleted": "Task '{{title}}' undeleted by {{undeletedBy}}",
|
||||
"tasks_updated": "Task '{{title}}' updated by {{updatedBy}}"
|
||||
}
|
||||
},
|
||||
"billlines": {
|
||||
@@ -223,10 +230,12 @@
|
||||
"markexported": "Mark Exported",
|
||||
"markforreexport": "Mark for Re-export",
|
||||
"new": "New Bill",
|
||||
"nobilllines": "",
|
||||
"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",
|
||||
"retailtotal": "Bills Retail Total",
|
||||
"returnfrombill": "Return From Bill",
|
||||
"savewithdiscrepancy": "You are about to save this bill with a discrepancy. The system will continue to use the calculated amount using the bill lines. Press cancel to return to the bill.",
|
||||
"state_tax": "State Tax",
|
||||
"subtotal": "Subtotal",
|
||||
@@ -367,6 +376,18 @@
|
||||
},
|
||||
"md_payment_types": "Payment Types",
|
||||
"md_referral_sources": "Referral Sources",
|
||||
"md_ro_guard": {
|
||||
"enabled": "RO Guard Enabled?",
|
||||
"enforce_ar": "Enforce AR Balance",
|
||||
"enforce_bills": "Enforce Bill Discrepancy",
|
||||
"enforce_cm": "Enforce Credit Memo Entry",
|
||||
"enforce_labor": "Enforce Labor Allocation",
|
||||
"enforce_ppd": "Enforce PPD Sync",
|
||||
"enforce_profit": "Enforce Profit Requirement",
|
||||
"enforce_sublet": "Enforce Sublet Completion",
|
||||
"masterbypass": "Master Bypass Password (not encrypted)",
|
||||
"totalgppercent_minimum": "Minimum Total Gross Profit %"
|
||||
},
|
||||
"md_tasks_presets": {
|
||||
"enable_tasks": "Enable Hour Flagging",
|
||||
"hourstype": "Hour Types",
|
||||
@@ -639,6 +660,7 @@
|
||||
"laborrates": "Labor Rates",
|
||||
"licensing": "Licensing",
|
||||
"md_parts_scan": "Parts Scan Rules",
|
||||
"md_ro_guard": "RO Guard",
|
||||
"md_tasks_presets": "Tasks Presets",
|
||||
"md_to_emails": "Preset To Emails",
|
||||
"md_to_emails_emails": "Emails",
|
||||
@@ -660,6 +682,9 @@
|
||||
"tax_accounts": "Tax Accounts",
|
||||
"title": "Responsibility Centers"
|
||||
},
|
||||
"roguard": {
|
||||
"title": "RO Guard"
|
||||
},
|
||||
"scheduling": "SMART Scheduling",
|
||||
"scoreboardsetup": "Scoreboard Setup",
|
||||
"shopinfo": "Shop Information",
|
||||
@@ -841,10 +866,11 @@
|
||||
},
|
||||
"status": {
|
||||
"in": "Available",
|
||||
"inservice": "In Service",
|
||||
"inservice": "Service/Maintenance",
|
||||
"leasereturn": "Lease Returned",
|
||||
"out": "Rented",
|
||||
"sold": "Sold"
|
||||
"sold": "Sold",
|
||||
"unavailable": "Unavailable"
|
||||
},
|
||||
"successes": {
|
||||
"saved": "Courtesy Car saved successfully."
|
||||
@@ -914,7 +940,8 @@
|
||||
"scheduledindate": "Sheduled In Today: {{date}}",
|
||||
"scheduledintoday": "Sheduled In Today",
|
||||
"scheduledoutdate": "Sheduled Out Today: {{date}}",
|
||||
"scheduledouttoday": "Sheduled Out Today"
|
||||
"scheduledouttoday": "Sheduled Out Today",
|
||||
"tasks": "Tasks"
|
||||
}
|
||||
},
|
||||
"dms": {
|
||||
@@ -1109,6 +1136,7 @@
|
||||
"delete": "Delete",
|
||||
"deleteall": "Delete All",
|
||||
"deselectall": "Deselect All",
|
||||
"download": "Download",
|
||||
"edit": "Edit",
|
||||
"login": "Login",
|
||||
"print": "Print",
|
||||
@@ -1334,6 +1362,7 @@
|
||||
"amount": "Amount",
|
||||
"dateOfPayment": "Date of Payment",
|
||||
"descriptions": "Payment Details",
|
||||
"hint": "Hint",
|
||||
"payer": "Payer",
|
||||
"payername": "Payer Name",
|
||||
"paymentid": "Payment Reference ID",
|
||||
@@ -1356,6 +1385,7 @@
|
||||
},
|
||||
"fields": {
|
||||
"act_price": "Retail Price",
|
||||
"act_price_before_ppc": "Original Part Price",
|
||||
"ah_detail_line": "Mark as Detail Labor Line (Autohouse Only)",
|
||||
"assigned_team": "Team",
|
||||
"assigned_team_name": "Team {{name}}",
|
||||
@@ -1523,6 +1553,7 @@
|
||||
"voiding": "Error voiding Job. {{error}}"
|
||||
},
|
||||
"fields": {
|
||||
"active_tasks": "Active Tasks",
|
||||
"actual_completion": "Actual Completion",
|
||||
"actual_delivery": "Actual Delivery",
|
||||
"actual_in": "Actual In",
|
||||
@@ -1825,6 +1856,7 @@
|
||||
"scheddates": "Schedule Dates"
|
||||
},
|
||||
"labels": {
|
||||
"accountsreceivable": "Accounts Receivable",
|
||||
"act_price_ppc": "New Part Price",
|
||||
"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).",
|
||||
@@ -1944,6 +1976,7 @@
|
||||
"mapa": "Paint Materials",
|
||||
"markforreexport": "Mark for Re-export",
|
||||
"mash": "Shop Materials",
|
||||
"masterbypass": "Master Bypass Password",
|
||||
"materials": {
|
||||
"mapa": ""
|
||||
},
|
||||
@@ -1952,6 +1985,12 @@
|
||||
"net_repairs": "Net Repairs",
|
||||
"notes": "Notes",
|
||||
"othertotal": "Other Totals",
|
||||
"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_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_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.",
|
||||
"override_header": "Override estimate header on import?",
|
||||
"ownerassociation": "Owner Association",
|
||||
"parts": "Parts",
|
||||
@@ -1961,6 +2000,7 @@
|
||||
"partsfilter": "Parts Only",
|
||||
"partssubletstotal": "Parts & Sublets Total",
|
||||
"partstotal": "Parts Total (ex. Taxes)",
|
||||
"performance": "Performance",
|
||||
"pimraryamountpayable": "Total Primary Payable",
|
||||
"plitooltips": {
|
||||
"billtotal": "The total amount of all bill lines that have been posted against this RO (not including credits, taxes, or labor adjustments).",
|
||||
@@ -1975,7 +2015,10 @@
|
||||
"totalreturns": "The total <b>retail</b> amount of returns created for this job."
|
||||
},
|
||||
"ppc": "This line contains a part price change.",
|
||||
"ppdnotexported": "PPDs not Exported",
|
||||
"profileadjustments": "Profile Disc./Mkup",
|
||||
"profitbypassrequired": "Minimum gross profit requirements have not been met.",
|
||||
"profits": "Job Profits",
|
||||
"prt_dsmk_total": "Line Item Adjustment",
|
||||
"rates": "Rates",
|
||||
"rates_subtotal": "All Rates Subtotal",
|
||||
@@ -1995,6 +2038,19 @@
|
||||
"relatedros": "Related ROs",
|
||||
"remove_from_ar": "Remove from AR",
|
||||
"returntotals": "Return Totals",
|
||||
"ro_guard": {
|
||||
"enforce_ar": "AR collection enforced.",
|
||||
"enforce_bills": "Bill discrepancy enforced.",
|
||||
"enforce_cm": "Credit memo entry enforced.",
|
||||
"enforce_labor": "Labor allocations enforced.",
|
||||
"enforce_ppd": "PPD sync enforced.",
|
||||
"enforce_profit": "Profit marginsenforced.",
|
||||
"enforce_sublet": "Sublet completion enforced.",
|
||||
"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."
|
||||
},
|
||||
"roguard": "RO Guard",
|
||||
"roguardwarnings": "RO Guard Warnings",
|
||||
"rosaletotal": "RO Parts Total",
|
||||
"sale_additional": "Sales - Additional",
|
||||
"sale_labor": "Sales - Labor",
|
||||
@@ -2005,11 +2061,13 @@
|
||||
"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",
|
||||
"state_tax_amt": "Provincial/State Taxes",
|
||||
"subletsnotcompleted": "Outstanding Sublets",
|
||||
"subletstotal": "Sublets Total",
|
||||
"subtotal": "Subtotal",
|
||||
"supplementnote": "The Job had a supplement imported.",
|
||||
"suspended": "SUSPENDED",
|
||||
"suspense": "Suspense",
|
||||
"tasks": "Tasks",
|
||||
"threshhold": "Max Threshold: ${{amount}}",
|
||||
"total_cost": "Total Cost",
|
||||
"total_cust_payable": "Total Customer Amount Payable",
|
||||
@@ -2107,6 +2165,7 @@
|
||||
"accounting-payments": "Payments",
|
||||
"accounting-receivables": "Receivables",
|
||||
"activejobs": "Active Jobs",
|
||||
"all_tasks": "All Tasks",
|
||||
"alljobs": "All Jobs",
|
||||
"allpayments": "All Payments",
|
||||
"availablejobs": "Available Jobs",
|
||||
@@ -2115,6 +2174,7 @@
|
||||
"courtesycars-all": "All Courtesy Cars",
|
||||
"courtesycars-contracts": "Contracts",
|
||||
"courtesycars-newcontract": "New Contract",
|
||||
"create_task": "Create Task",
|
||||
"customers": "Customers",
|
||||
"dashboard": "Dashboard",
|
||||
"enterbills": "Enter Bills",
|
||||
@@ -2127,6 +2187,7 @@
|
||||
"home": "Home",
|
||||
"inventory": "Inventory",
|
||||
"jobs": "Jobs",
|
||||
"my_tasks": "My Tasks",
|
||||
"newjob": "Create New Job",
|
||||
"owners": "Owners",
|
||||
"parts-queue": "Parts Queue",
|
||||
@@ -2153,6 +2214,7 @@
|
||||
"shop_csi": "CSI",
|
||||
"shop_templates": "Templates",
|
||||
"shop_vendors": "Vendors",
|
||||
"tasks": "Tasks",
|
||||
"temporarydocs": "Temporary Documents",
|
||||
"timetickets": "Time Tickets",
|
||||
"ttapprovals": "Time Ticket Approvals",
|
||||
@@ -2346,6 +2408,7 @@
|
||||
"percent_accepted": "% Accepted"
|
||||
},
|
||||
"labels": {
|
||||
"notyetdispatched": "This part has not been dispatched.",
|
||||
"parts_dispatch": "Parts Dispatch"
|
||||
}
|
||||
},
|
||||
@@ -2462,6 +2525,7 @@
|
||||
"markexported": "Payment(s) marked exported.",
|
||||
"markreexported": "Payment marked for re-export successfully",
|
||||
"payment": "Payment created successfully. ",
|
||||
"paymentupdate": "Payment updated successfully. ",
|
||||
"stripe": "Credit card transaction charged successfully."
|
||||
}
|
||||
},
|
||||
@@ -2563,6 +2627,7 @@
|
||||
"job_costing_ro": "Job Costing",
|
||||
"job_lifecycle_ro": "Job Lifecycle",
|
||||
"job_notes": "Job Notes",
|
||||
"job_tasks": "Job Tasks",
|
||||
"key_tag": "Key Tag",
|
||||
"labels": {
|
||||
"count": "Count",
|
||||
@@ -2763,6 +2828,7 @@
|
||||
"parts_orders": "Parts Orders",
|
||||
"payments": "Payments",
|
||||
"scoreboard": "Scoreboard",
|
||||
"tasks": "Tasks",
|
||||
"timetickets": "Timetickets"
|
||||
},
|
||||
"vendor": "Vendor"
|
||||
@@ -2878,6 +2944,8 @@
|
||||
"scoreboard_detail": "Scoreboard Detail",
|
||||
"scoreboard_summary": "Scoreboard Summary",
|
||||
"supplement_ratio_ins_co": "Supplement Ratio by Source",
|
||||
"tasks_date": "Tasks by Date",
|
||||
"tasks_date_employee": "Employee Tasks by Date",
|
||||
"thank_you_date": "Thank You Letters",
|
||||
"timetickets": "Time Tickets",
|
||||
"timetickets_employee": "Employee Time Tickets",
|
||||
@@ -2957,6 +3025,92 @@
|
||||
"updated": "Scoreboard updated."
|
||||
}
|
||||
},
|
||||
"tasks": {
|
||||
"actions": {
|
||||
"edit": "Edit Task",
|
||||
"new": "New Task"
|
||||
},
|
||||
"buttons": {
|
||||
"allTasks": "All",
|
||||
"complete": "Toggle Complete",
|
||||
"create": "Create Task",
|
||||
"delete": "Toggle Delete",
|
||||
"edit": "Edit",
|
||||
"myTasks": "Mine",
|
||||
"refresh": "Refresh"
|
||||
},
|
||||
"date_presets": {
|
||||
"completion": "Completion",
|
||||
"day": "Day",
|
||||
"days": "Days",
|
||||
"delivery": "Delivery",
|
||||
"next_week": "Next Week",
|
||||
"one_month": "One Month",
|
||||
"three_months": "Three Months",
|
||||
"three_weeks": "Three Weeks",
|
||||
"today": "Today",
|
||||
"tomorrow": "Tomorrow",
|
||||
"two_weeks": "Two Weeks"
|
||||
},
|
||||
"failures": {
|
||||
"completed": "Failed to toggle Task completion.",
|
||||
"created": "Failed to create Task.",
|
||||
"deleted": "Failed to toggle Task deletion.",
|
||||
"updated": "Failed to update Task."
|
||||
},
|
||||
"fields": {
|
||||
"actions": "Actions",
|
||||
"assigned_to": "Assigned To",
|
||||
"bill": "Bill",
|
||||
"billid": "Bill",
|
||||
"completed": "Completed",
|
||||
"created_at": "Created At",
|
||||
"description": "Description",
|
||||
"due_date": "Due Date",
|
||||
"job": {
|
||||
"ro_number": "RO #"
|
||||
},
|
||||
"jobid": "Job",
|
||||
"jobline": "Job Line",
|
||||
"joblineid": "Job Line",
|
||||
"parts_order": "Parts Order",
|
||||
"partsorderid": "Parts Order",
|
||||
"priorities": {
|
||||
"high": "High",
|
||||
"low": "Low",
|
||||
"medium": "Medium"
|
||||
},
|
||||
"priority": "Priority",
|
||||
"remind_at": "Remind At",
|
||||
"title": "Title"
|
||||
},
|
||||
"placeholders": {
|
||||
"assigned_to": "Select an Employee",
|
||||
"billid": "Select a Bill",
|
||||
"description": "Enter a description",
|
||||
"jobid": "Select a Job",
|
||||
"joblineid": "Select a Job Line",
|
||||
"partsorderid": "Select a Parts Order"
|
||||
},
|
||||
"successes": {
|
||||
"completed": "Toggled Task completion successfully.",
|
||||
"created": "Task created successfully.",
|
||||
"deleted": "Toggled Task deletion successfully.",
|
||||
"updated": "Task updated successfully."
|
||||
},
|
||||
"titles": {
|
||||
"all_tasks": "All Tasks",
|
||||
"completed": "Completed Tasks",
|
||||
"deleted": "Deleted Tasks",
|
||||
"job_tasks": "Job Tasks",
|
||||
"mine": "My Tasks",
|
||||
"my_tasks": "My Tasks"
|
||||
},
|
||||
"validation": {
|
||||
"due_at_error_message": "The due date must be in the future!",
|
||||
"remind_at_error_message": "The reminder date and time must be at least 30 minutes in the future!"
|
||||
}
|
||||
},
|
||||
"tech": {
|
||||
"fields": {
|
||||
"employeeid": "Employee ID",
|
||||
@@ -3061,11 +3215,13 @@
|
||||
"accounting-payables": "Payables | {{app}}",
|
||||
"accounting-payments": "Payments | {{app}}",
|
||||
"accounting-receivables": "Receivables | {{app}}",
|
||||
"all_tasks": "All Tasks",
|
||||
"app": "",
|
||||
"bc": {
|
||||
"accounting-payables": "Payables",
|
||||
"accounting-payments": "Payments",
|
||||
"accounting-receivables": "Receivables",
|
||||
"all_tasks": "All Tasks",
|
||||
"availablejobs": "Available Jobs",
|
||||
"bills-list": "Bills",
|
||||
"contracts": "Contracts",
|
||||
@@ -3089,6 +3245,7 @@
|
||||
"jobs-intake": "Intake",
|
||||
"jobs-new": "Create a New Job",
|
||||
"jobs-ready": "Ready Jobs",
|
||||
"my_tasks": "My Tasks",
|
||||
"owner-detail": "{{name}}",
|
||||
"owners": "Owners",
|
||||
"parts-queue": "Parts Queue",
|
||||
@@ -3103,6 +3260,7 @@
|
||||
"shop-csi": "CSI Responses",
|
||||
"shop-templates": "Shop Templates",
|
||||
"shop-vendors": "Vendors",
|
||||
"tasks": "Tasks",
|
||||
"temporarydocs": "Temporary Documents",
|
||||
"timetickets": "Time Tickets",
|
||||
"ttapprovals": "Time Ticket Approvals",
|
||||
@@ -3133,6 +3291,7 @@
|
||||
"jobsdetail": "Job {{ro_number}} | {{app}}",
|
||||
"jobsdocuments": "Job Documents {{ro_number}} | {{app}}",
|
||||
"manageroot": "Home | {{app}}",
|
||||
"my_tasks": "My Tasks",
|
||||
"owners": "All Owners | {{app}}",
|
||||
"owners-detail": "{{name}} | {{app}}",
|
||||
"parts-queue": "Parts Queue | {{app}}",
|
||||
@@ -3152,6 +3311,7 @@
|
||||
"shop-csi": "CSI Responses | {{app}}",
|
||||
"shop-templates": "Shop Templates | {{app}}",
|
||||
"shop_vendors": "Vendors | {{app}}",
|
||||
"tasks": "Tasks",
|
||||
"techconsole": "Technician Console | {{app}}",
|
||||
"techjobclock": "Technician Job Clock | {{app}}",
|
||||
"techjoblookup": "Technician Job Lookup | {{app}}",
|
||||
|
||||
@@ -116,6 +116,7 @@
|
||||
"jobassignmentchange": "",
|
||||
"jobassignmentremoved": "",
|
||||
"jobchecklist": "",
|
||||
"jobclosedwithbypass": "",
|
||||
"jobconverted": "",
|
||||
"jobdelivery": "",
|
||||
"jobexported": "",
|
||||
@@ -134,7 +135,13 @@
|
||||
"jobstatuschange": "",
|
||||
"jobsupplement": "",
|
||||
"jobsuspend": "",
|
||||
"jobvoid": ""
|
||||
"jobvoid": "",
|
||||
"tasks_completed": "",
|
||||
"tasks_created": "",
|
||||
"tasks_deleted": "",
|
||||
"tasks_uncompleted": "",
|
||||
"tasks_undeleted": "",
|
||||
"tasks_updated": ""
|
||||
}
|
||||
},
|
||||
"billlines": {
|
||||
@@ -223,10 +230,12 @@
|
||||
"markexported": "",
|
||||
"markforreexport": "",
|
||||
"new": "",
|
||||
"nobilllines": "",
|
||||
"noneselected": "",
|
||||
"onlycmforinvoiced": "",
|
||||
"printlabels": "",
|
||||
"retailtotal": "",
|
||||
"returnfrombill": "",
|
||||
"savewithdiscrepancy": "",
|
||||
"state_tax": "",
|
||||
"subtotal": "",
|
||||
@@ -367,6 +376,18 @@
|
||||
},
|
||||
"md_payment_types": "",
|
||||
"md_referral_sources": "",
|
||||
"md_ro_guard": {
|
||||
"enabled": "",
|
||||
"enforce_ar": "",
|
||||
"enforce_bills": "",
|
||||
"enforce_cm": "",
|
||||
"enforce_labor": "",
|
||||
"enforce_ppd": "",
|
||||
"enforce_profit": "",
|
||||
"enforce_sublet": "",
|
||||
"masterbypass": "",
|
||||
"totalgppercent_minimum": ""
|
||||
},
|
||||
"md_tasks_presets": {
|
||||
"enable_tasks": "",
|
||||
"hourstype": "",
|
||||
@@ -639,6 +660,7 @@
|
||||
"laborrates": "",
|
||||
"licensing": "",
|
||||
"md_parts_scan": "",
|
||||
"md_ro_guard": "",
|
||||
"md_tasks_presets": "",
|
||||
"md_to_emails": "",
|
||||
"md_to_emails_emails": "",
|
||||
@@ -660,6 +682,9 @@
|
||||
"tax_accounts": "",
|
||||
"title": ""
|
||||
},
|
||||
"roguard": {
|
||||
"title": ""
|
||||
},
|
||||
"scheduling": "",
|
||||
"scoreboardsetup": "",
|
||||
"shopinfo": "",
|
||||
@@ -844,7 +869,8 @@
|
||||
"inservice": "",
|
||||
"leasereturn": "",
|
||||
"out": "",
|
||||
"sold": ""
|
||||
"sold": "",
|
||||
"unavailable": ""
|
||||
},
|
||||
"successes": {
|
||||
"saved": ""
|
||||
@@ -914,7 +940,8 @@
|
||||
"scheduledindate": "",
|
||||
"scheduledintoday": "",
|
||||
"scheduledoutdate": "",
|
||||
"scheduledouttoday": ""
|
||||
"scheduledouttoday": "",
|
||||
"tasks": ""
|
||||
}
|
||||
},
|
||||
"dms": {
|
||||
@@ -1109,6 +1136,7 @@
|
||||
"delete": "Borrar",
|
||||
"deleteall": "",
|
||||
"deselectall": "",
|
||||
"download": "",
|
||||
"edit": "Editar",
|
||||
"login": "",
|
||||
"print": "",
|
||||
@@ -1334,6 +1362,7 @@
|
||||
"amount": "",
|
||||
"dateOfPayment": "",
|
||||
"descriptions": "",
|
||||
"hint": "",
|
||||
"payer": "",
|
||||
"payername": "",
|
||||
"paymentid": "",
|
||||
@@ -1356,6 +1385,7 @@
|
||||
},
|
||||
"fields": {
|
||||
"act_price": "Precio actual",
|
||||
"act_price_before_ppc": "",
|
||||
"ah_detail_line": "",
|
||||
"assigned_team": "",
|
||||
"assigned_team_name": "",
|
||||
@@ -1523,6 +1553,7 @@
|
||||
"voiding": ""
|
||||
},
|
||||
"fields": {
|
||||
"active_tasks": "",
|
||||
"actual_completion": "Realización real",
|
||||
"actual_delivery": "Entrega real",
|
||||
"actual_in": "Real en",
|
||||
@@ -1825,6 +1856,7 @@
|
||||
"scheddates": ""
|
||||
},
|
||||
"labels": {
|
||||
"accountsreceivable": "",
|
||||
"act_price_ppc": "",
|
||||
"actual_completion_inferred": "",
|
||||
"actual_delivery_inferred": "",
|
||||
@@ -1944,6 +1976,7 @@
|
||||
"mapa": "",
|
||||
"markforreexport": "",
|
||||
"mash": "",
|
||||
"masterbypass": "",
|
||||
"materials": {
|
||||
"mapa": ""
|
||||
},
|
||||
@@ -1952,6 +1985,12 @@
|
||||
"net_repairs": "",
|
||||
"notes": "Notas",
|
||||
"othertotal": "",
|
||||
"outstanding_ar": "",
|
||||
"outstanding_credit_memos": "",
|
||||
"outstanding_ppd": "",
|
||||
"outstanding_reconciliation_discrep": "",
|
||||
"outstanding_sublets": "",
|
||||
"outstandinghours": "",
|
||||
"override_header": "¿Anular encabezado estimado al importar?",
|
||||
"ownerassociation": "",
|
||||
"parts": "Partes",
|
||||
@@ -1961,6 +2000,7 @@
|
||||
"partsfilter": "",
|
||||
"partssubletstotal": "",
|
||||
"partstotal": "",
|
||||
"performance": "",
|
||||
"pimraryamountpayable": "",
|
||||
"plitooltips": {
|
||||
"billtotal": "",
|
||||
@@ -1975,7 +2015,10 @@
|
||||
"totalreturns": ""
|
||||
},
|
||||
"ppc": "",
|
||||
"ppdnotexported": "",
|
||||
"profileadjustments": "",
|
||||
"profitbypassrequired": "",
|
||||
"profits": "",
|
||||
"prt_dsmk_total": "",
|
||||
"rates": "Tarifas",
|
||||
"rates_subtotal": "",
|
||||
@@ -1995,6 +2038,19 @@
|
||||
"relatedros": "",
|
||||
"remove_from_ar": "",
|
||||
"returntotals": "",
|
||||
"ro_guard": {
|
||||
"enforce_ar": "",
|
||||
"enforce_bills": "",
|
||||
"enforce_cm": "",
|
||||
"enforce_labor": "",
|
||||
"enforce_ppd": "",
|
||||
"enforce_profit": "",
|
||||
"enforce_sublet": "",
|
||||
"enforce_validation": "",
|
||||
"enforced": ""
|
||||
},
|
||||
"roguard": "",
|
||||
"roguardwarnings": "",
|
||||
"rosaletotal": "",
|
||||
"sale_additional": "",
|
||||
"sale_labor": "",
|
||||
@@ -2005,11 +2061,13 @@
|
||||
"scheduledinchange": "",
|
||||
"specialcoveragepolicy": "",
|
||||
"state_tax_amt": "",
|
||||
"subletsnotcompleted": "",
|
||||
"subletstotal": "",
|
||||
"subtotal": "",
|
||||
"supplementnote": "",
|
||||
"suspended": "",
|
||||
"suspense": "",
|
||||
"tasks": "",
|
||||
"threshhold": "",
|
||||
"total_cost": "",
|
||||
"total_cust_payable": "",
|
||||
@@ -2107,6 +2165,7 @@
|
||||
"accounting-payments": "",
|
||||
"accounting-receivables": "",
|
||||
"activejobs": "Empleos activos",
|
||||
"all_tasks": "",
|
||||
"alljobs": "",
|
||||
"allpayments": "",
|
||||
"availablejobs": "Trabajos disponibles",
|
||||
@@ -2115,6 +2174,7 @@
|
||||
"courtesycars-all": "",
|
||||
"courtesycars-contracts": "",
|
||||
"courtesycars-newcontract": "",
|
||||
"create_task": "",
|
||||
"customers": "Clientes",
|
||||
"dashboard": "",
|
||||
"enterbills": "",
|
||||
@@ -2127,6 +2187,7 @@
|
||||
"home": "Casa",
|
||||
"inventory": "",
|
||||
"jobs": "Trabajos",
|
||||
"my_tasks": "",
|
||||
"newjob": "",
|
||||
"owners": "propietarios",
|
||||
"parts-queue": "",
|
||||
@@ -2153,6 +2214,7 @@
|
||||
"shop_csi": "",
|
||||
"shop_templates": "",
|
||||
"shop_vendors": "Vendedores",
|
||||
"tasks": "",
|
||||
"temporarydocs": "",
|
||||
"timetickets": "",
|
||||
"ttapprovals": "",
|
||||
@@ -2346,6 +2408,7 @@
|
||||
"percent_accepted": ""
|
||||
},
|
||||
"labels": {
|
||||
"notyetdispatched": "",
|
||||
"parts_dispatch": ""
|
||||
}
|
||||
},
|
||||
@@ -2462,6 +2525,7 @@
|
||||
"markexported": "",
|
||||
"markreexported": "",
|
||||
"payment": "",
|
||||
"paymentupdate": "",
|
||||
"stripe": ""
|
||||
}
|
||||
},
|
||||
@@ -2563,6 +2627,7 @@
|
||||
"job_costing_ro": "",
|
||||
"job_lifecycle_ro": "",
|
||||
"job_notes": "",
|
||||
"job_tasks": "",
|
||||
"key_tag": "",
|
||||
"labels": {
|
||||
"count": "",
|
||||
@@ -2763,6 +2828,7 @@
|
||||
"parts_orders": "",
|
||||
"payments": "",
|
||||
"scoreboard": "",
|
||||
"tasks": "",
|
||||
"timetickets": ""
|
||||
},
|
||||
"vendor": ""
|
||||
@@ -2878,6 +2944,8 @@
|
||||
"scoreboard_detail": "",
|
||||
"scoreboard_summary": "",
|
||||
"supplement_ratio_ins_co": "",
|
||||
"tasks_date": "",
|
||||
"tasks_date_employee": "",
|
||||
"thank_you_date": "",
|
||||
"timetickets": "",
|
||||
"timetickets_employee": "",
|
||||
@@ -2957,6 +3025,92 @@
|
||||
"updated": ""
|
||||
}
|
||||
},
|
||||
"tasks": {
|
||||
"actions": {
|
||||
"edit": "",
|
||||
"new": ""
|
||||
},
|
||||
"buttons": {
|
||||
"allTasks": "",
|
||||
"complete": "",
|
||||
"create": "",
|
||||
"delete": "",
|
||||
"edit": "",
|
||||
"myTasks": "",
|
||||
"refresh": ""
|
||||
},
|
||||
"date_presets": {
|
||||
"completion": "",
|
||||
"day": "",
|
||||
"days": "",
|
||||
"delivery": "",
|
||||
"next_week": "",
|
||||
"one_month": "",
|
||||
"three_months": "",
|
||||
"three_weeks": "",
|
||||
"today": "",
|
||||
"tomorrow": "",
|
||||
"two_weeks": ""
|
||||
},
|
||||
"failures": {
|
||||
"completed": "",
|
||||
"created": "",
|
||||
"deleted": "",
|
||||
"updated": ""
|
||||
},
|
||||
"fields": {
|
||||
"actions": "",
|
||||
"assigned_to": "",
|
||||
"bill": "",
|
||||
"billid": "",
|
||||
"completed": "",
|
||||
"created_at": "",
|
||||
"description": "",
|
||||
"due_date": "",
|
||||
"job": {
|
||||
"ro_number": ""
|
||||
},
|
||||
"jobid": "",
|
||||
"jobline": "",
|
||||
"joblineid": "",
|
||||
"parts_order": "",
|
||||
"partsorderid": "",
|
||||
"priorities": {
|
||||
"high": "",
|
||||
"low": "",
|
||||
"medium": ""
|
||||
},
|
||||
"priority": "",
|
||||
"remind_at": "",
|
||||
"title": ""
|
||||
},
|
||||
"placeholders": {
|
||||
"assigned_to": "",
|
||||
"billid": "",
|
||||
"description": "",
|
||||
"jobid": "",
|
||||
"joblineid": "",
|
||||
"partsorderid": ""
|
||||
},
|
||||
"successes": {
|
||||
"completed": "",
|
||||
"created": "",
|
||||
"deleted": "",
|
||||
"updated": ""
|
||||
},
|
||||
"titles": {
|
||||
"all_tasks": "",
|
||||
"completed": "",
|
||||
"deleted": "",
|
||||
"job_tasks": "",
|
||||
"mine": "",
|
||||
"my_tasks": ""
|
||||
},
|
||||
"validation": {
|
||||
"due_at_error_message": "",
|
||||
"remind_at_error_message": ""
|
||||
}
|
||||
},
|
||||
"tech": {
|
||||
"fields": {
|
||||
"employeeid": "",
|
||||
@@ -3061,11 +3215,13 @@
|
||||
"accounting-payables": "",
|
||||
"accounting-payments": "",
|
||||
"accounting-receivables": "",
|
||||
"all_tasks": "",
|
||||
"app": "",
|
||||
"bc": {
|
||||
"accounting-payables": "",
|
||||
"accounting-payments": "",
|
||||
"accounting-receivables": "",
|
||||
"all_tasks": "",
|
||||
"availablejobs": "",
|
||||
"bills-list": "",
|
||||
"contracts": "",
|
||||
@@ -3089,6 +3245,7 @@
|
||||
"jobs-intake": "",
|
||||
"jobs-new": "",
|
||||
"jobs-ready": "",
|
||||
"my_tasks": "",
|
||||
"owner-detail": "",
|
||||
"owners": "",
|
||||
"parts-queue": "",
|
||||
@@ -3103,6 +3260,7 @@
|
||||
"shop-csi": "",
|
||||
"shop-templates": "",
|
||||
"shop-vendors": "",
|
||||
"tasks": "",
|
||||
"temporarydocs": "",
|
||||
"timetickets": "",
|
||||
"ttapprovals": "",
|
||||
@@ -3133,6 +3291,7 @@
|
||||
"jobsdetail": "Trabajo {{ro_number}} | {{app}}",
|
||||
"jobsdocuments": "Documentos de trabajo {{ro_number}} | {{app}}",
|
||||
"manageroot": "Casa | {{app}}",
|
||||
"my_tasks": "",
|
||||
"owners": "Todos los propietarios | {{app}}",
|
||||
"owners-detail": "",
|
||||
"parts-queue": "",
|
||||
@@ -3152,6 +3311,7 @@
|
||||
"shop-csi": "",
|
||||
"shop-templates": "",
|
||||
"shop_vendors": "Vendedores | {{app}}",
|
||||
"tasks": "",
|
||||
"techconsole": "{{app}}",
|
||||
"techjobclock": "{{app}}",
|
||||
"techjoblookup": "{{app}}",
|
||||
|
||||
@@ -116,6 +116,7 @@
|
||||
"jobassignmentchange": "",
|
||||
"jobassignmentremoved": "",
|
||||
"jobchecklist": "",
|
||||
"jobclosedwithbypass": "",
|
||||
"jobconverted": "",
|
||||
"jobdelivery": "",
|
||||
"jobexported": "",
|
||||
@@ -134,7 +135,13 @@
|
||||
"jobstatuschange": "",
|
||||
"jobsupplement": "",
|
||||
"jobsuspend": "",
|
||||
"jobvoid": ""
|
||||
"jobvoid": "",
|
||||
"tasks_completed": "",
|
||||
"tasks_created": "",
|
||||
"tasks_deleted": "",
|
||||
"tasks_uncompleted": "",
|
||||
"tasks_undeleted": "",
|
||||
"tasks_updated": ""
|
||||
}
|
||||
},
|
||||
"billlines": {
|
||||
@@ -223,10 +230,12 @@
|
||||
"markexported": "",
|
||||
"markforreexport": "",
|
||||
"new": "",
|
||||
"nobilllines": "",
|
||||
"noneselected": "",
|
||||
"onlycmforinvoiced": "",
|
||||
"printlabels": "",
|
||||
"retailtotal": "",
|
||||
"returnfrombill": "",
|
||||
"savewithdiscrepancy": "",
|
||||
"state_tax": "",
|
||||
"subtotal": "",
|
||||
@@ -367,6 +376,18 @@
|
||||
},
|
||||
"md_payment_types": "",
|
||||
"md_referral_sources": "",
|
||||
"md_ro_guard": {
|
||||
"enabled": "",
|
||||
"enforce_ar": "",
|
||||
"enforce_bills": "",
|
||||
"enforce_cm": "",
|
||||
"enforce_labor": "",
|
||||
"enforce_ppd": "",
|
||||
"enforce_profit": "",
|
||||
"enforce_sublet": "",
|
||||
"masterbypass": "",
|
||||
"totalgppercent_minimum": ""
|
||||
},
|
||||
"md_tasks_presets": {
|
||||
"enable_tasks": "",
|
||||
"hourstype": "",
|
||||
@@ -639,6 +660,7 @@
|
||||
"laborrates": "",
|
||||
"licensing": "",
|
||||
"md_parts_scan": "",
|
||||
"md_ro_guard": "",
|
||||
"md_tasks_presets": "",
|
||||
"md_to_emails": "",
|
||||
"md_to_emails_emails": "",
|
||||
@@ -660,6 +682,9 @@
|
||||
"tax_accounts": "",
|
||||
"title": ""
|
||||
},
|
||||
"roguard": {
|
||||
"title": ""
|
||||
},
|
||||
"scheduling": "",
|
||||
"scoreboardsetup": "",
|
||||
"shopinfo": "",
|
||||
@@ -844,7 +869,8 @@
|
||||
"inservice": "",
|
||||
"leasereturn": "",
|
||||
"out": "",
|
||||
"sold": ""
|
||||
"sold": "",
|
||||
"unavailable": ""
|
||||
},
|
||||
"successes": {
|
||||
"saved": ""
|
||||
@@ -914,7 +940,8 @@
|
||||
"scheduledindate": "",
|
||||
"scheduledintoday": "",
|
||||
"scheduledoutdate": "",
|
||||
"scheduledouttoday": ""
|
||||
"scheduledouttoday": "",
|
||||
"tasks": ""
|
||||
}
|
||||
},
|
||||
"dms": {
|
||||
@@ -1109,6 +1136,7 @@
|
||||
"delete": "Effacer",
|
||||
"deleteall": "",
|
||||
"deselectall": "",
|
||||
"download": "",
|
||||
"edit": "modifier",
|
||||
"login": "",
|
||||
"print": "",
|
||||
@@ -1334,6 +1362,7 @@
|
||||
"amount": "",
|
||||
"dateOfPayment": "",
|
||||
"descriptions": "",
|
||||
"hint": "",
|
||||
"payer": "",
|
||||
"payername": "",
|
||||
"paymentid": "",
|
||||
@@ -1356,6 +1385,7 @@
|
||||
},
|
||||
"fields": {
|
||||
"act_price": "Prix actuel",
|
||||
"act_price_before_ppc": "",
|
||||
"ah_detail_line": "",
|
||||
"assigned_team": "",
|
||||
"assigned_team_name": "",
|
||||
@@ -1523,6 +1553,7 @@
|
||||
"voiding": ""
|
||||
},
|
||||
"fields": {
|
||||
"active_tasks": "",
|
||||
"actual_completion": "Achèvement réel",
|
||||
"actual_delivery": "Livraison réelle",
|
||||
"actual_in": "En réel",
|
||||
@@ -1825,6 +1856,7 @@
|
||||
"scheddates": ""
|
||||
},
|
||||
"labels": {
|
||||
"accountsreceivable": "",
|
||||
"act_price_ppc": "",
|
||||
"actual_completion_inferred": "",
|
||||
"actual_delivery_inferred": "",
|
||||
@@ -1944,6 +1976,7 @@
|
||||
"mapa": "",
|
||||
"markforreexport": "",
|
||||
"mash": "",
|
||||
"masterbypass": "",
|
||||
"materials": {
|
||||
"mapa": ""
|
||||
},
|
||||
@@ -1952,6 +1985,12 @@
|
||||
"net_repairs": "",
|
||||
"notes": "Remarques",
|
||||
"othertotal": "",
|
||||
"outstanding_ar": "",
|
||||
"outstanding_credit_memos": "",
|
||||
"outstanding_ppd": "",
|
||||
"outstanding_reconciliation_discrep": "",
|
||||
"outstanding_sublets": "",
|
||||
"outstandinghours": "",
|
||||
"override_header": "Remplacer l'en-tête d'estimation à l'importation?",
|
||||
"ownerassociation": "",
|
||||
"parts": "les pièces",
|
||||
@@ -1961,6 +2000,7 @@
|
||||
"partsfilter": "",
|
||||
"partssubletstotal": "",
|
||||
"partstotal": "",
|
||||
"performance": "",
|
||||
"pimraryamountpayable": "",
|
||||
"plitooltips": {
|
||||
"billtotal": "",
|
||||
@@ -1975,7 +2015,10 @@
|
||||
"totalreturns": ""
|
||||
},
|
||||
"ppc": "",
|
||||
"ppdnotexported": "",
|
||||
"profileadjustments": "",
|
||||
"profitbypassrequired": "",
|
||||
"profits": "",
|
||||
"prt_dsmk_total": "",
|
||||
"rates": "Les taux",
|
||||
"rates_subtotal": "",
|
||||
@@ -1995,6 +2038,19 @@
|
||||
"relatedros": "",
|
||||
"remove_from_ar": "",
|
||||
"returntotals": "",
|
||||
"ro_guard": {
|
||||
"enforce_ar": "",
|
||||
"enforce_bills": "",
|
||||
"enforce_cm": "",
|
||||
"enforce_labor": "",
|
||||
"enforce_ppd": "",
|
||||
"enforce_profit": "",
|
||||
"enforce_sublet": "",
|
||||
"enforce_validation": "",
|
||||
"enforced": ""
|
||||
},
|
||||
"roguard": "",
|
||||
"roguardwarnings": "",
|
||||
"rosaletotal": "",
|
||||
"sale_additional": "",
|
||||
"sale_labor": "",
|
||||
@@ -2005,11 +2061,13 @@
|
||||
"scheduledinchange": "",
|
||||
"specialcoveragepolicy": "",
|
||||
"state_tax_amt": "",
|
||||
"subletsnotcompleted": "",
|
||||
"subletstotal": "",
|
||||
"subtotal": "",
|
||||
"supplementnote": "",
|
||||
"suspended": "",
|
||||
"suspense": "",
|
||||
"tasks": "",
|
||||
"threshhold": "",
|
||||
"total_cost": "",
|
||||
"total_cust_payable": "",
|
||||
@@ -2107,6 +2165,7 @@
|
||||
"accounting-payments": "",
|
||||
"accounting-receivables": "",
|
||||
"activejobs": "Emplois actifs",
|
||||
"all_tasks": "",
|
||||
"alljobs": "",
|
||||
"allpayments": "",
|
||||
"availablejobs": "Emplois disponibles",
|
||||
@@ -2115,6 +2174,7 @@
|
||||
"courtesycars-all": "",
|
||||
"courtesycars-contracts": "",
|
||||
"courtesycars-newcontract": "",
|
||||
"create_task": "",
|
||||
"customers": "Les clients",
|
||||
"dashboard": "",
|
||||
"enterbills": "",
|
||||
@@ -2127,6 +2187,7 @@
|
||||
"home": "Accueil",
|
||||
"inventory": "",
|
||||
"jobs": "Emplois",
|
||||
"my_tasks": "",
|
||||
"newjob": "",
|
||||
"owners": "Propriétaires",
|
||||
"parts-queue": "",
|
||||
@@ -2153,6 +2214,7 @@
|
||||
"shop_csi": "",
|
||||
"shop_templates": "",
|
||||
"shop_vendors": "Vendeurs",
|
||||
"tasks": "",
|
||||
"temporarydocs": "",
|
||||
"timetickets": "",
|
||||
"ttapprovals": "",
|
||||
@@ -2346,6 +2408,7 @@
|
||||
"percent_accepted": ""
|
||||
},
|
||||
"labels": {
|
||||
"notyetdispatched": "",
|
||||
"parts_dispatch": ""
|
||||
}
|
||||
},
|
||||
@@ -2462,6 +2525,7 @@
|
||||
"markexported": "",
|
||||
"markreexported": "",
|
||||
"payment": "",
|
||||
"paymentupdate": "",
|
||||
"stripe": ""
|
||||
}
|
||||
},
|
||||
@@ -2563,6 +2627,7 @@
|
||||
"job_costing_ro": "",
|
||||
"job_lifecycle_ro": "",
|
||||
"job_notes": "",
|
||||
"job_tasks": "",
|
||||
"key_tag": "",
|
||||
"labels": {
|
||||
"count": "",
|
||||
@@ -2763,6 +2828,7 @@
|
||||
"parts_orders": "",
|
||||
"payments": "",
|
||||
"scoreboard": "",
|
||||
"tasks": "",
|
||||
"timetickets": ""
|
||||
},
|
||||
"vendor": ""
|
||||
@@ -2878,6 +2944,8 @@
|
||||
"scoreboard_detail": "",
|
||||
"scoreboard_summary": "",
|
||||
"supplement_ratio_ins_co": "",
|
||||
"tasks_date": "",
|
||||
"tasks_date_employee": "",
|
||||
"thank_you_date": "",
|
||||
"timetickets": "",
|
||||
"timetickets_employee": "",
|
||||
@@ -2957,6 +3025,92 @@
|
||||
"updated": ""
|
||||
}
|
||||
},
|
||||
"tasks": {
|
||||
"actions": {
|
||||
"edit": "",
|
||||
"new": ""
|
||||
},
|
||||
"buttons": {
|
||||
"allTasks": "",
|
||||
"complete": "",
|
||||
"create": "",
|
||||
"delete": "",
|
||||
"edit": "",
|
||||
"myTasks": "",
|
||||
"refresh": ""
|
||||
},
|
||||
"date_presets": {
|
||||
"completion": "",
|
||||
"day": "",
|
||||
"days": "",
|
||||
"delivery": "",
|
||||
"next_week": "",
|
||||
"one_month": "",
|
||||
"three_months": "",
|
||||
"three_weeks": "",
|
||||
"today": "",
|
||||
"tomorrow": "",
|
||||
"two_weeks": ""
|
||||
},
|
||||
"failures": {
|
||||
"completed": "",
|
||||
"created": "",
|
||||
"deleted": "",
|
||||
"updated": ""
|
||||
},
|
||||
"fields": {
|
||||
"actions": "",
|
||||
"assigned_to": "",
|
||||
"bill": "",
|
||||
"billid": "",
|
||||
"completed": "",
|
||||
"created_at": "",
|
||||
"description": "",
|
||||
"due_date": "",
|
||||
"job": {
|
||||
"ro_number": ""
|
||||
},
|
||||
"jobid": "",
|
||||
"jobline": "",
|
||||
"joblineid": "",
|
||||
"parts_order": "",
|
||||
"partsorderid": "",
|
||||
"priorities": {
|
||||
"high": "",
|
||||
"low": "",
|
||||
"medium": ""
|
||||
},
|
||||
"priority": "",
|
||||
"remind_at": "",
|
||||
"title": ""
|
||||
},
|
||||
"placeholders": {
|
||||
"assigned_to": "",
|
||||
"billid": "",
|
||||
"description": "",
|
||||
"jobid": "",
|
||||
"joblineid": "",
|
||||
"partsorderid": ""
|
||||
},
|
||||
"successes": {
|
||||
"completed": "",
|
||||
"created": "",
|
||||
"deleted": "",
|
||||
"updated": ""
|
||||
},
|
||||
"titles": {
|
||||
"all_tasks": "",
|
||||
"completed": "",
|
||||
"deleted": "",
|
||||
"job_tasks": "",
|
||||
"mine": "",
|
||||
"my_tasks": ""
|
||||
},
|
||||
"validation": {
|
||||
"due_at_error_message": "",
|
||||
"remind_at_error_message": ""
|
||||
}
|
||||
},
|
||||
"tech": {
|
||||
"fields": {
|
||||
"employeeid": "",
|
||||
@@ -3061,11 +3215,13 @@
|
||||
"accounting-payables": "",
|
||||
"accounting-payments": "",
|
||||
"accounting-receivables": "",
|
||||
"all_tasks": "",
|
||||
"app": "",
|
||||
"bc": {
|
||||
"accounting-payables": "",
|
||||
"accounting-payments": "",
|
||||
"accounting-receivables": "",
|
||||
"all_tasks": "",
|
||||
"availablejobs": "",
|
||||
"bills-list": "",
|
||||
"contracts": "",
|
||||
@@ -3089,6 +3245,7 @@
|
||||
"jobs-intake": "",
|
||||
"jobs-new": "",
|
||||
"jobs-ready": "",
|
||||
"my_tasks": "",
|
||||
"owner-detail": "",
|
||||
"owners": "",
|
||||
"parts-queue": "",
|
||||
@@ -3103,6 +3260,7 @@
|
||||
"shop-csi": "",
|
||||
"shop-templates": "",
|
||||
"shop-vendors": "",
|
||||
"tasks": "",
|
||||
"temporarydocs": "",
|
||||
"timetickets": "",
|
||||
"ttapprovals": "",
|
||||
@@ -3133,6 +3291,7 @@
|
||||
"jobsdetail": "Travail {{ro_number}} | {{app}}",
|
||||
"jobsdocuments": "Documents de travail {{ro_number}} | {{app}}",
|
||||
"manageroot": "Accueil | {{app}}",
|
||||
"my_tasks": "",
|
||||
"owners": "Tous les propriétaires | {{app}}",
|
||||
"owners-detail": "",
|
||||
"parts-queue": "",
|
||||
@@ -3152,6 +3311,7 @@
|
||||
"shop-csi": "",
|
||||
"shop-templates": "",
|
||||
"shop_vendors": "Vendeurs | {{app}}",
|
||||
"tasks": "",
|
||||
"techconsole": "{{app}}",
|
||||
"techjobclock": "{{app}}",
|
||||
"techjoblookup": "{{app}}",
|
||||
|
||||
@@ -24,6 +24,7 @@ const AuditTrailMapping = {
|
||||
jobimported: () => i18n.t("audit_trail.messages.jobimported"),
|
||||
jobinproductionchange: (inproduction) => i18n.t("audit_trail.messages.jobinproductionchange", { inproduction }),
|
||||
jobinvoiced: () => i18n.t("audit_trail.messages.jobinvoiced"),
|
||||
jobclosedwithbypass: () => i18n.t("audit_trail.messages.jobclosedwithbypass"),
|
||||
jobmodifylbradj: ({ mod_lbr_ty, hours }) => i18n.t("audit_trail.messages.jobmodifylbradj", { mod_lbr_ty, hours }),
|
||||
jobnoteadded: () => i18n.t("audit_trail.messages.jobnoteadded"),
|
||||
jobnoteupdated: () => i18n.t("audit_trail.messages.jobnoteupdated"),
|
||||
@@ -39,7 +40,38 @@ const AuditTrailMapping = {
|
||||
jobstatuschange: (status) => i18n.t("audit_trail.messages.jobstatuschange", { status }),
|
||||
jobsupplement: () => i18n.t("audit_trail.messages.jobsupplement"),
|
||||
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
|
||||
tasksCreated: (title, createdBy) =>
|
||||
i18n.t("audit_trail.messages.tasks_created", {
|
||||
title,
|
||||
createdBy
|
||||
}),
|
||||
tasksUpdated: (title, updatedBy) =>
|
||||
i18n.t("audit_trail.messages.tasks_updated", {
|
||||
title,
|
||||
updatedBy
|
||||
}),
|
||||
tasksDeleted: (title, deletedBy) =>
|
||||
i18n.t("audit_trail.messages.tasks_deleted", {
|
||||
title,
|
||||
deletedBy
|
||||
}),
|
||||
tasksUndeleted: (title, undeletedBy) =>
|
||||
i18n.t("audit_trail.messages.tasks_undeleted", {
|
||||
title,
|
||||
undeletedBy
|
||||
}),
|
||||
tasksCompleted: (title, completedBy) =>
|
||||
i18n.t("audit_trail.messages.tasks_completed", {
|
||||
title,
|
||||
completedBy
|
||||
}),
|
||||
tasksUncompleted: (title, uncompletedBy) =>
|
||||
i18n.t("audit_trail.messages.tasks_uncompleted", {
|
||||
title,
|
||||
uncompletedBy
|
||||
})
|
||||
};
|
||||
|
||||
export default AuditTrailMapping;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import axios from "axios";
|
||||
import { auth } from "../firebase/firebase.utils";
|
||||
import InstanceRenderManager from "./instanceRenderMgr";
|
||||
|
||||
axios.defaults.baseURL =
|
||||
import.meta.env.VITE_APP_AXIOS_BASE_API_URL ||
|
||||
@@ -12,6 +13,15 @@ export const axiosAuthInterceptorId = axios.interceptors.request.use(
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
InstanceRenderManager({
|
||||
executeFunction: true,
|
||||
args: [],
|
||||
promanager: () => {
|
||||
if (!config.url.startsWith("http://localhost:1337")) {
|
||||
config.headers["Convenient-Company"] = "promanager";
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return config;
|
||||
|
||||
@@ -10,6 +10,7 @@ import client from "../utils/GraphQLClient";
|
||||
import cleanAxios from "./CleanAxios";
|
||||
import { TemplateList } from "./TemplateConstants";
|
||||
import { generateTemplate } from "./graphQLmodifier";
|
||||
import InstanceRenderManager from "./instanceRenderMgr";
|
||||
const server = import.meta.env.VITE_APP_REPORTS_SERVER_URL;
|
||||
|
||||
jsreport.serverUrl = server;
|
||||
@@ -71,8 +72,8 @@ export default async function RenderTemplate(
|
||||
...contextData,
|
||||
...templateObject.variables,
|
||||
...templateObject.context,
|
||||
headerpath: `/${bodyshop.imexshopid}/header.html`,
|
||||
footerpath: `/${bodyshop.imexshopid}/footer.html`,
|
||||
headerpath: `/${InstanceRenderManager({ imex: bodyshop.imexshopid, rome: bodyshop.imexshopid, promanager: "GENERIC" })}/header.html`,
|
||||
footerpath: `/${InstanceRenderManager({ imex: bodyshop.imexshopid, rome: bodyshop.imexshopid, promanager: "GENERIC" })}/footer.html`,
|
||||
bodyshop: bodyshop,
|
||||
filters: templateObject?.filters,
|
||||
sorters: templateObject?.sorters,
|
||||
|
||||
@@ -587,6 +587,14 @@ export const TemplateList = (type, context) => {
|
||||
key: "job_lifecycle_ro",
|
||||
disabled: false,
|
||||
group: "post"
|
||||
},
|
||||
job_tasks: {
|
||||
title: i18n.t("printcenter.jobs.job_tasks"),
|
||||
description: "",
|
||||
subject: i18n.t("printcenter.jobs.job_tasks"),
|
||||
key: "job_tasks",
|
||||
disabled: false,
|
||||
group: "ro"
|
||||
}
|
||||
}
|
||||
: {}),
|
||||
@@ -2089,6 +2097,30 @@ export const TemplateList = (type, context) => {
|
||||
field: i18n.t("jobs.fields.date_invoiced")
|
||||
},
|
||||
group: "jobs"
|
||||
},
|
||||
tasks_date: {
|
||||
title: i18n.t("reportcenter.templates.tasks_date"),
|
||||
subject: i18n.t("reportcenter.templates.tasks_date"),
|
||||
key: "tasks_date",
|
||||
//idtype: "vendor",
|
||||
disabled: false,
|
||||
rangeFilter: {
|
||||
object: i18n.t("reportcenter.labels.objects.tasks"),
|
||||
field: i18n.t("tasks.fields.created_at")
|
||||
},
|
||||
group: "jobs"
|
||||
},
|
||||
tasks_date_employee: {
|
||||
title: i18n.t("reportcenter.templates.tasks_date_employee"),
|
||||
subject: i18n.t("reportcenter.templates.tasks_date_employee"),
|
||||
key: "tasks_date_employee",
|
||||
idtype: "employee",
|
||||
disabled: false,
|
||||
rangeFilter: {
|
||||
object: i18n.t("reportcenter.labels.objects.tasks"),
|
||||
field: i18n.t("tasks.fields.created_at")
|
||||
},
|
||||
group: "jobs"
|
||||
}
|
||||
}
|
||||
: {}),
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
/**
|
||||
* Replaces undefined values with null in an object.
|
||||
* Optionally, you can specify keys to replace.
|
||||
* If keys are specified, only those keys will be replaced.
|
||||
* If no keys are specified, all undefined values will be replaced.
|
||||
* @param obj
|
||||
* @param keys
|
||||
* @returns {*}
|
||||
* @constructor
|
||||
*/
|
||||
export default function UndefinedToNull(obj, keys) {
|
||||
Object.keys(obj).forEach((key) => {
|
||||
if (keys && keys.indexOf(key) >= 0) {
|
||||
@@ -8,3 +18,21 @@ export default function UndefinedToNull(obj, keys) {
|
||||
});
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces undefined values with null in an object. Optionally, you can specify keys to replace. If keys are specified, only those keys will be replaced. If no keys are specified, all undefined values will be replaced.
|
||||
* @param obj
|
||||
* @param keys
|
||||
* @returns {{[p: string]: unknown}}
|
||||
*/
|
||||
export function replaceUndefinedWithNull(obj, keys) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(obj).map(([key, value]) => {
|
||||
if (keys) {
|
||||
return [key, keys.includes(key) && value === undefined ? null : value];
|
||||
} else {
|
||||
return [key, value === undefined ? null : value];
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import * as path from "path";
|
||||
import * as url from "url";
|
||||
import { defineConfig } from "vite";
|
||||
import { ViteEjsPlugin } from "vite-plugin-ejs";
|
||||
import eslint from 'vite-plugin-eslint';
|
||||
|
||||
//import CompressionPlugin from 'vite-plugin-compression';
|
||||
import { VitePWA } from "vite-plugin-pwa";
|
||||
import InstanceRenderManager from "./src/utils/instanceRenderMgr";
|
||||
@@ -100,7 +102,8 @@ export default defineConfig({
|
||||
}
|
||||
}),
|
||||
reactVirtualized(),
|
||||
react()
|
||||
react(),
|
||||
eslint(),
|
||||
// CompressionPlugin(), //Cloudfront already compresses assets, so not needed.
|
||||
],
|
||||
define: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
actions: [ ]
|
||||
actions: []
|
||||
custom_types:
|
||||
enums: [ ]
|
||||
input_objects: [ ]
|
||||
objects: [ ]
|
||||
scalars: [ ]
|
||||
enums: []
|
||||
input_objects: []
|
||||
objects: []
|
||||
scalars: []
|
||||
|
||||
@@ -1 +1 @@
|
||||
[ ]
|
||||
[]
|
||||
|
||||
@@ -1 +1,8 @@
|
||||
[ ]
|
||||
- name: Task Reminders
|
||||
webhook: '{{HASURA_API_URL}}/tasks-remind-handler'
|
||||
schedule: '*/15 * * * *'
|
||||
include_in_metadata: true
|
||||
payload: {}
|
||||
headers:
|
||||
- name: event-secret
|
||||
value_from_env: EVENT_SECRET
|
||||
|
||||
@@ -1 +1 @@
|
||||
{ }
|
||||
{}
|
||||
|
||||
@@ -1 +1 @@
|
||||
[ ]
|
||||
[]
|
||||
|
||||
@@ -1 +1 @@
|
||||
[ ]
|
||||
[]
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
update_permissions:
|
||||
- role: user
|
||||
permission:
|
||||
columns: [ ]
|
||||
columns: []
|
||||
filter:
|
||||
jobline:
|
||||
job:
|
||||
@@ -705,7 +705,7 @@
|
||||
value_from_env: EVENT_SECRET
|
||||
request_transform:
|
||||
method: POST
|
||||
query_params: { }
|
||||
query_params: {}
|
||||
template_engine: Kriti
|
||||
url: '{{$base_url}}/opensearch'
|
||||
version: 2
|
||||
@@ -920,6 +920,7 @@
|
||||
- cdk_dealerid
|
||||
- city
|
||||
- claimscorpid
|
||||
- convenient_company
|
||||
- country
|
||||
- created_at
|
||||
- default_adjustment_rate
|
||||
@@ -969,6 +970,7 @@
|
||||
- md_rbac
|
||||
- md_referral_sources
|
||||
- md_responsibility_centers
|
||||
- md_ro_guard
|
||||
- md_ro_statuses
|
||||
- md_tasks_presets
|
||||
- md_to_emails
|
||||
@@ -1068,6 +1070,7 @@
|
||||
- md_rbac
|
||||
- md_referral_sources
|
||||
- md_responsibility_centers
|
||||
- md_ro_guard
|
||||
- md_ro_statuses
|
||||
- md_tasks_presets
|
||||
- md_to_emails
|
||||
@@ -1674,7 +1677,7 @@
|
||||
columns:
|
||||
- config
|
||||
- id
|
||||
filter: { }
|
||||
filter: {}
|
||||
limit: 1
|
||||
- role: user
|
||||
permission:
|
||||
@@ -2333,6 +2336,13 @@
|
||||
table:
|
||||
name: jobs
|
||||
schema: public
|
||||
- name: tasks
|
||||
using:
|
||||
foreign_key_constraint_on:
|
||||
column: assigned_to
|
||||
table:
|
||||
name: tasks
|
||||
schema: public
|
||||
- name: timetickets
|
||||
using:
|
||||
foreign_key_constraint_on:
|
||||
@@ -2512,7 +2522,7 @@
|
||||
- effective_date
|
||||
- end_date
|
||||
- content
|
||||
filter: { }
|
||||
filter: {}
|
||||
- table:
|
||||
name: exportlog
|
||||
schema: public
|
||||
@@ -4248,7 +4258,7 @@
|
||||
value_from_env: EVENT_SECRET
|
||||
request_transform:
|
||||
method: POST
|
||||
query_params: { }
|
||||
query_params: {}
|
||||
template_engine: Kriti
|
||||
url: '{{$base_url}}/job/statustransition'
|
||||
version: 2
|
||||
@@ -4262,7 +4272,7 @@
|
||||
webhook_from_env: HASURA_API_URL
|
||||
request_transform:
|
||||
method: POST
|
||||
query_params: { }
|
||||
query_params: {}
|
||||
template_engine: Kriti
|
||||
url: '{{$base_url}}/record-handler/arms'
|
||||
version: 2
|
||||
@@ -4285,7 +4295,7 @@
|
||||
value_from_env: EVENT_SECRET
|
||||
request_transform:
|
||||
method: POST
|
||||
query_params: { }
|
||||
query_params: {}
|
||||
template_engine: Kriti
|
||||
url: '{{$base_url}}/opensearch'
|
||||
version: 2
|
||||
@@ -4298,13 +4308,13 @@
|
||||
columns:
|
||||
- key
|
||||
- value
|
||||
filter: { }
|
||||
filter: {}
|
||||
- role: user
|
||||
permission:
|
||||
columns:
|
||||
- key
|
||||
- value
|
||||
filter: { }
|
||||
filter: {}
|
||||
- table:
|
||||
name: messages
|
||||
schema: public
|
||||
@@ -4736,7 +4746,7 @@
|
||||
value_from_env: EVENT_SECRET
|
||||
request_transform:
|
||||
method: POST
|
||||
query_params: { }
|
||||
query_params: {}
|
||||
template_engine: Kriti
|
||||
url: '{{$base_url}}/opensearch'
|
||||
version: 2
|
||||
@@ -5351,7 +5361,7 @@
|
||||
value_from_env: EVENT_SECRET
|
||||
request_transform:
|
||||
method: POST
|
||||
query_params: { }
|
||||
query_params: {}
|
||||
template_engine: Kriti
|
||||
url: '{{$base_url}}/opensearch'
|
||||
version: 2
|
||||
@@ -5676,6 +5686,9 @@
|
||||
name: tasks
|
||||
schema: public
|
||||
object_relationships:
|
||||
- name: assigned_to_employee
|
||||
using:
|
||||
foreign_key_constraint_on: assigned_to
|
||||
- name: bill
|
||||
using:
|
||||
foreign_key_constraint_on: billid
|
||||
@@ -5691,9 +5704,6 @@
|
||||
- name: parts_order
|
||||
using:
|
||||
foreign_key_constraint_on: partsorderid
|
||||
- name: user
|
||||
using:
|
||||
foreign_key_constraint_on: assigned_to
|
||||
- name: userByCreatedBy
|
||||
using:
|
||||
foreign_key_constraint_on: created_by
|
||||
@@ -5710,48 +5720,50 @@
|
||||
- active:
|
||||
_eq: true
|
||||
columns:
|
||||
- completed
|
||||
- deleted
|
||||
- priority
|
||||
- assigned_to
|
||||
- created_by
|
||||
- description
|
||||
- title
|
||||
- completed_at
|
||||
- created_at
|
||||
- deleted_at
|
||||
- due_date
|
||||
- remind_at
|
||||
- updated_at
|
||||
- billid
|
||||
- bodyshopid
|
||||
- completed
|
||||
- completed_at
|
||||
- created_at
|
||||
- created_by
|
||||
- deleted
|
||||
- deleted_at
|
||||
- description
|
||||
- due_date
|
||||
- id
|
||||
- jobid
|
||||
- joblineid
|
||||
- partsorderid
|
||||
- priority
|
||||
- remind_at
|
||||
- remind_at_sent
|
||||
- title
|
||||
- updated_at
|
||||
select_permissions:
|
||||
- role: user
|
||||
permission:
|
||||
columns:
|
||||
- completed
|
||||
- deleted
|
||||
- priority
|
||||
- assigned_to
|
||||
- created_by
|
||||
- description
|
||||
- title
|
||||
- completed_at
|
||||
- created_at
|
||||
- deleted_at
|
||||
- due_date
|
||||
- remind_at
|
||||
- updated_at
|
||||
- billid
|
||||
- bodyshopid
|
||||
- completed
|
||||
- completed_at
|
||||
- created_at
|
||||
- created_by
|
||||
- deleted
|
||||
- deleted_at
|
||||
- description
|
||||
- due_date
|
||||
- id
|
||||
- jobid
|
||||
- joblineid
|
||||
- partsorderid
|
||||
- priority
|
||||
- remind_at
|
||||
- remind_at_sent
|
||||
- title
|
||||
- updated_at
|
||||
filter:
|
||||
bodyshop:
|
||||
associations:
|
||||
@@ -5766,25 +5778,26 @@
|
||||
- role: user
|
||||
permission:
|
||||
columns:
|
||||
- completed
|
||||
- deleted
|
||||
- priority
|
||||
- assigned_to
|
||||
- created_by
|
||||
- description
|
||||
- title
|
||||
- completed_at
|
||||
- created_at
|
||||
- deleted_at
|
||||
- due_date
|
||||
- remind_at
|
||||
- updated_at
|
||||
- billid
|
||||
- bodyshopid
|
||||
- completed
|
||||
- completed_at
|
||||
- created_at
|
||||
- created_by
|
||||
- deleted
|
||||
- deleted_at
|
||||
- description
|
||||
- due_date
|
||||
- id
|
||||
- jobid
|
||||
- joblineid
|
||||
- partsorderid
|
||||
- priority
|
||||
- remind_at
|
||||
- remind_at_sent
|
||||
- title
|
||||
- updated_at
|
||||
filter:
|
||||
bodyshop:
|
||||
associations:
|
||||
@@ -5795,6 +5808,29 @@
|
||||
- active:
|
||||
_eq: true
|
||||
check: null
|
||||
event_triggers:
|
||||
- name: tasks_assigned_changed
|
||||
definition:
|
||||
enable_manual: false
|
||||
insert:
|
||||
columns: '*'
|
||||
update:
|
||||
columns:
|
||||
- assigned_to
|
||||
retry_conf:
|
||||
interval_sec: 10
|
||||
num_retries: 3
|
||||
timeout_sec: 60
|
||||
webhook_from_env: HASURA_API_URL
|
||||
headers:
|
||||
- name: event-secret
|
||||
value_from_env: EVENT_SECRET
|
||||
request_transform:
|
||||
method: POST
|
||||
query_params: {}
|
||||
template_engine: Kriti
|
||||
url: '{{$base_url}}/tasks-assigned-handler'
|
||||
version: 2
|
||||
- table:
|
||||
name: timetickets
|
||||
schema: public
|
||||
@@ -6015,7 +6051,7 @@
|
||||
- user:
|
||||
authid:
|
||||
_eq: X-Hasura-User-Id
|
||||
check: { }
|
||||
check: {}
|
||||
- table:
|
||||
name: tt_approval_queue
|
||||
schema: public
|
||||
@@ -6164,6 +6200,13 @@
|
||||
table:
|
||||
name: email_audit_trail
|
||||
schema: public
|
||||
- name: employees
|
||||
using:
|
||||
foreign_key_constraint_on:
|
||||
column: user_email
|
||||
table:
|
||||
name: employees
|
||||
schema: public
|
||||
- name: eula_acceptances
|
||||
using:
|
||||
foreign_key_constraint_on:
|
||||
@@ -6213,13 +6256,6 @@
|
||||
table:
|
||||
name: parts_orders
|
||||
schema: public
|
||||
- name: tasks
|
||||
using:
|
||||
foreign_key_constraint_on:
|
||||
column: assigned_to
|
||||
table:
|
||||
name: tasks
|
||||
schema: public
|
||||
- name: tasksByCreatedBy
|
||||
using:
|
||||
foreign_key_constraint_on:
|
||||
@@ -6244,7 +6280,7 @@
|
||||
insert_permissions:
|
||||
- role: user
|
||||
permission:
|
||||
check: { }
|
||||
check: {}
|
||||
columns:
|
||||
- authid
|
||||
- email
|
||||
@@ -6446,7 +6482,7 @@
|
||||
value_from_env: EVENT_SECRET
|
||||
request_transform:
|
||||
method: POST
|
||||
query_params: { }
|
||||
query_params: {}
|
||||
template_engine: Kriti
|
||||
url: '{{$base_url}}/opensearch'
|
||||
version: 2
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user