-
-
+
+
-
-
- {
- setPdfDocuments(
- pdfDocuments.map((g, idx) =>
- index === idx ? { ...g, isSelected: !g.isSelected } : g
- )
- );
- }}
- />
-
-
- {
- setgalleryImages(
- galleryImages.map((g, idx) =>
- index === idx ? { ...g, isSelected: !g.isSelected } : g
- )
- );
- }}
- >
-
- {
- //
- // {pdfDocuments.map((doc, idx) => (
- // } file={doc.src}>
- //
- //
- // ))}
- //
- }
-
+
+
+
+
{
+ setgalleryImages(
+ galleryImages.map((g, idx) =>
+ index === idx ? { ...g, isSelected: !g.isSelected } : g
+ )
+ );
+ }}
+ />
);
}
diff --git a/client/src/components/jobs-documents-gallery/jobs-documents-gallery.container.jsx b/client/src/components/jobs-documents-gallery/jobs-documents-gallery.container.jsx
index 8c1de1f84..f162d9cd7 100644
--- a/client/src/components/jobs-documents-gallery/jobs-documents-gallery.container.jsx
+++ b/client/src/components/jobs-documents-gallery/jobs-documents-gallery.container.jsx
@@ -5,14 +5,28 @@ import AlertComponent from "../alert/alert.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import JobDocuments from "./jobs-documents-gallery.component";
-export default function JobsDocumentsContainer({ jobId }) {
+export default function JobsDocumentsContainer({
+ jobId,
+ invoiceId,
+ documentsList,
+ invoicesCallback,
+}) {
const { loading, error, data, refetch } = useQuery(GET_DOCUMENTS_BY_JOB, {
variables: { jobId: jobId },
- fetchPolicy: "network-only"
+ fetchPolicy: "network-only",
+ skip: !!invoiceId,
});
if (loading) return
;
if (error) return
;
- return
;
+ return (
+
+ );
}
diff --git a/client/src/components/jobs-documents-gallery/jobs-documents-gallery.delete.component.jsx b/client/src/components/jobs-documents-gallery/jobs-documents-gallery.delete.component.jsx
new file mode 100644
index 000000000..7142170f0
--- /dev/null
+++ b/client/src/components/jobs-documents-gallery/jobs-documents-gallery.delete.component.jsx
@@ -0,0 +1,81 @@
+import { Button, notification } from "antd";
+import React, { useState } from "react";
+import { useTranslation } from "react-i18next";
+import axios from "axios";
+import { useMutation } from "@apollo/react-hooks";
+import { DELETE_DOCUMENT } from "../../graphql/documents.queries";
+import { logImEXEvent } from "../../firebase/firebase.utils";
+
+export default function JobsDocumentsDeleteButton({
+ galleryImages,
+ deletionCallback,
+}) {
+ const { t } = useTranslation();
+ const [deleteDocument] = useMutation(DELETE_DOCUMENT);
+ const imagesToDelete = galleryImages.filter((image) => image.isSelected);
+ const [loading, setLoading] = useState(false);
+ const handleDelete = () => {
+ logImEXEvent("job_documents_delete", { count: imagesToDelete.length });
+ setLoading(true);
+ imagesToDelete.forEach((image) => {
+ let timestamp = Math.floor(Date.now() / 1000);
+ let public_id = image.key;
+
+ axios
+ .post("/media/sign", {
+ public_id: public_id,
+ timestamp: timestamp,
+ })
+ .then((response) => {
+ var signature = response.data;
+ var options = {
+ headers: { "X-Requested-With": "XMLHttpRequest" },
+ };
+ const formData = new FormData();
+ formData.append("api_key", process.env.REACT_APP_CLOUDINARY_API_KEY);
+ formData.append("public_id", public_id);
+ formData.append("timestamp", timestamp);
+ formData.append("signature", signature);
+
+ axios
+ .post(
+ `${process.env.REACT_APP_CLOUDINARY_ENDPOINT}/destroy`,
+ formData,
+ options
+ )
+ .then((response) => {
+ deleteDocument({ variables: { id: image.id } })
+ .then((r) => {
+ if (deletionCallback) deletionCallback();
+ })
+ .catch((error) => {
+ notification["error"]({
+ message: t("documents.errors.deleting", {
+ message: JSON.stringify(error),
+ }),
+ });
+ });
+ //Delete it from our database.
+ })
+ .catch((error) => {
+ notification["error"]({
+ message: t("documents.errors.deleting_cloudinary", {
+ message: JSON.stringify(error),
+ }),
+ });
+ });
+ });
+ });
+ setLoading(false);
+ };
+
+ return (
+
+ );
+}
diff --git a/client/src/components/jobs-export-all-button/jobs-export-all-button.component.jsx b/client/src/components/jobs-export-all-button/jobs-export-all-button.component.jsx
new file mode 100644
index 000000000..8b8fcabac
--- /dev/null
+++ b/client/src/components/jobs-export-all-button/jobs-export-all-button.component.jsx
@@ -0,0 +1,131 @@
+import { useMutation } from "@apollo/react-hooks";
+import { Button, notification } from "antd";
+import axios from "axios";
+import React, { useState } from "react";
+import { useTranslation } from "react-i18next";
+import { connect } from "react-redux";
+import { createStructuredSelector } from "reselect";
+import { auth, logImEXEvent } from "../../firebase/firebase.utils";
+import { UPDATE_JOBS } from "../../graphql/jobs.queries";
+import { selectBodyshop } from "../../redux/user/user.selectors";
+
+const mapStateToProps = createStructuredSelector({
+ bodyshop: selectBodyshop,
+});
+
+export function JobsExportAllButton({
+ bodyshop,
+ jobIds,
+ disabled,
+ loadingCallback,
+ completedCallback,
+}) {
+ const { t } = useTranslation();
+ const [updateJob] = useMutation(UPDATE_JOBS);
+ const [loading, setLoading] = useState(false);
+ const handleQbxml = async () => {
+ logImEXEvent("jobs_export_all");
+
+ setLoading(true);
+ let QbXmlResponse;
+ try {
+ QbXmlResponse = await axios.post(
+ "/accounting/qbxml/receivables",
+ { jobIds: jobIds },
+ {
+ headers: {
+ Authorization: `Bearer ${await auth.currentUser.getIdToken(true)}`,
+ },
+ }
+ );
+ } catch (error) {
+ console.log("Error getting QBXML from Server.", error);
+ notification["error"]({
+ message: t("jobs.errors.exporting", {
+ error: "Unable to retrieve QBXML. " + JSON.stringify(error.message),
+ }),
+ });
+ setLoading(false);
+ return;
+ }
+
+ let PartnerResponse;
+ try {
+ PartnerResponse = await axios.post(
+ "http://localhost:1337/qb/",
+ // "http://609feaeae986.ngrok.io/qb/",
+ QbXmlResponse.data,
+ {
+ headers: {
+ Authorization: `Bearer ${await auth.currentUser.getIdToken(true)}`,
+ },
+ }
+ );
+ } catch (error) {
+ console.log("Error connecting to quickbooks or partner.", error);
+ notification["error"]({
+ message: t("jobs.errors.exporting-partner"),
+ });
+ setLoading(false);
+ return;
+ }
+
+ console.log("PartnerResponse", PartnerResponse);
+
+ //Check to see if any of them failed. If they didn't don't execute the update.
+ const failedTransactions = PartnerResponse.data.filter((r) => !r.success);
+ const successfulTransactions = PartnerResponse.data.filter(
+ (r) => r.success
+ );
+ if (failedTransactions.length > 0) {
+ //Uh oh. At least one was no good.
+ failedTransactions.map((ft) =>
+ notification["error"]({
+ message: t("jobs.errors.exporting", {
+ error: ft.errorMessage || "",
+ }),
+ })
+ );
+ }
+ if (successfulTransactions.length > 0) {
+ const jobUpdateResponse = await updateJob({
+ variables: {
+ jobIds: successfulTransactions.map((st) => st.id),
+ job: {
+ status: bodyshop.md_ro_statuses.default_exported || "Exported*",
+ date_exported: new Date(),
+ },
+ },
+ });
+
+ if (!!!jobUpdateResponse.errors) {
+ notification["success"]({
+ message: t("jobs.successes.exported"),
+ });
+ } else {
+ notification["error"]({
+ message: t("jobs.errors.exporting", {
+ error: JSON.stringify(jobUpdateResponse.error),
+ }),
+ });
+ }
+ }
+ //Set the list of selected invoices to be nothing.
+ if (!!completedCallback) completedCallback([]);
+ if (!!loadingCallback) loadingCallback(false);
+ setLoading(false);
+ };
+
+ return (
+
+ );
+}
+
+export default connect(mapStateToProps, null)(JobsExportAllButton);
diff --git a/client/src/components/jobs-find-modal/jobs-find-modal.component.jsx b/client/src/components/jobs-find-modal/jobs-find-modal.component.jsx
index 1ba0d8be8..98a00e5b2 100644
--- a/client/src/components/jobs-find-modal/jobs-find-modal.component.jsx
+++ b/client/src/components/jobs-find-modal/jobs-find-modal.component.jsx
@@ -1,4 +1,5 @@
-import { Checkbox, Divider, Table } from "antd";
+import { SyncOutlined } from "@ant-design/icons";
+import { Checkbox, Divider, Input, Table, Button } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
@@ -9,12 +10,14 @@ export default function JobsFindModalComponent({
setSelectedJob,
jobsList,
jobsListLoading,
- importOptionsState
+ importOptionsState,
+ modalSearchState,
+ jobsListRefetch,
}) {
const { t } = useTranslation();
-
+ const [modalSearch, setModalSearch] = modalSearchState;
const [importOptions, setImportOptions] = importOptionsState;
- console.log("importOptions", importOptions);
+
const columns = [
{
title: t("jobs.fields.ro_number"),
@@ -27,7 +30,7 @@ export default function JobsFindModalComponent({
{record.ro_number ? record.ro_number : "EST-" + record.est_number}
- )
+ ),
},
{
title: t("jobs.fields.owner"),
@@ -40,13 +43,17 @@ export default function JobsFindModalComponent({
render: (text, record) => {
return record.owner ? (
- {record.ownr_fn} {record.ownr_ln}
+ {`${record.ownr_fn || ""} ${record.ownr_ln || ""} ${
+ record.ownr_co_nm || ""
+ }`}
) : (
// t("jobs.errors.noowner")
-
{`${record.ownr_fn} ${record.ownr_ln}`}
+
{`${record.ownr_fn || ""} ${record.ownr_ln || ""} ${
+ record.ownr_co_nm || ""
+ }`}
);
- }
+ },
},
{
title: t("jobs.fields.ownr_ph1"),
@@ -60,7 +67,7 @@ export default function JobsFindModalComponent({
) : (
t("general.labels.unknown")
);
- }
+ },
},
{
title: t("jobs.fields.status"),
@@ -70,7 +77,7 @@ export default function JobsFindModalComponent({
ellipsis: true,
render: (text, record) => {
return record.status || t("general.labels.na");
- }
+ },
},
{
@@ -82,13 +89,14 @@ export default function JobsFindModalComponent({
render: (text, record) => {
return record.vehicle ? (
- {`${record.v_model_yr || ""} ${record.v_make_desc ||
- ""} ${record.v_model_desc || ""}`}
+ {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
+ record.v_model_desc || ""
+ }`}
) : (
t("jobs.errors.novehicle")
);
- }
+ },
},
{
title: t("vehicles.fields.plate_no"),
@@ -97,12 +105,12 @@ export default function JobsFindModalComponent({
width: "8%",
ellipsis: true,
render: (text, record) => {
- return record.vehicle.plate_no ? (
-
{record.vehicle.plate_no}
+ return record.plate_no ? (
+
{record.plate_no}
) : (
t("general.labels.unknown")
);
- }
+ },
},
{
title: t("jobs.fields.clm_no"),
@@ -116,11 +124,11 @@ export default function JobsFindModalComponent({
) : (
t("general.labels.unknown")
);
- }
- }
+ },
+ },
];
- const handleOnRowClick = record => {
+ const handleOnRowClick = (record) => {
if (record) {
if (record.id) {
setSelectedJob(record.id);
@@ -133,35 +141,52 @@ export default function JobsFindModalComponent({
return (
t("jobs.labels.existing_jobs")}
+ title={() => (
+
+ {t("jobs.labels.existing_jobs")}
+
+ {
+ setModalSearch(e.target.value);
+ }}
+ />
+
+ )}
size="small"
pagination={{ position: "bottom" }}
- columns={columns.map(item => ({ ...item }))}
+ columns={columns.map((item) => ({ ...item }))}
rowKey="id"
loading={jobsListLoading}
dataSource={jobsList}
rowSelection={{
- onSelect: props => {
+ onSelect: (props) => {
setSelectedJob(props.id);
},
type: "radio",
- selectedRowKeys: [selectedJob]
+ selectedRowKeys: [selectedJob],
}}
onRow={(record, rowIndex) => {
return {
- onClick: event => {
+ onClick: (event) => {
handleOnRowClick(record);
- }
+ },
};
}}
/>
+ onChange={(e) =>
setImportOptions({
...importOptions,
- overrideHeaders: e.target.checked
+ overrideHeaders: e.target.checked,
})
}
>
diff --git a/client/src/components/jobs-find-modal/jobs-find-modal.container.jsx b/client/src/components/jobs-find-modal/jobs-find-modal.container.jsx
index b05e3b653..40fa453be 100644
--- a/client/src/components/jobs-find-modal/jobs-find-modal.container.jsx
+++ b/client/src/components/jobs-find-modal/jobs-find-modal.container.jsx
@@ -1,16 +1,16 @@
+import { useQuery } from "@apollo/react-hooks";
import { Modal } from "antd";
import React from "react";
-import { useQuery } from "@apollo/react-hooks";
import { useTranslation } from "react-i18next";
+import { connect } from "react-redux";
+import { createStructuredSelector } from "reselect";
import { QUERY_ALL_ACTIVE_JOBS } from "../../graphql/jobs.queries";
+import { selectBodyshop } from "../../redux/user/user.selectors";
import AlertComponent from "../alert/alert.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import JobsFindModalComponent from "./jobs-find-modal.component";
-import { selectBodyshop } from "../../redux/user/user.selectors";
-import { connect } from "react-redux";
-import { createStructuredSelector } from "reselect";
const mapStateToProps = createStructuredSelector({
- bodyshop: selectBodyshop
+ bodyshop: selectBodyshop,
});
export default connect(
@@ -23,21 +23,62 @@ export default connect(
selectedJob,
setSelectedJob,
importOptionsState,
+ modalSearchState,
...modalProps
}) {
const { t } = useTranslation();
const jobsList = useQuery(QUERY_ALL_ACTIVE_JOBS, {
- fetchPolicy: "network-only",
variables: {
- statuses: bodyshop.md_ro_statuses.open_statuses || ["Open"]
- }
+ statuses: bodyshop.md_ro_statuses.open_statuses || ["Open"],
+ },
+ skip: !modalProps.visible,
});
+ const modalSearch = modalSearchState[0];
+
+ const jobsData =
+ jobsList.data && jobsList.data.jobs
+ ? modalSearch
+ ? jobsList.data.jobs.filter(
+ (j) =>
+ (j.ro_number || "")
+ .toLowerCase()
+ .includes(modalSearch.toLowerCase()) ||
+ (j.est_number || "")
+ .toString()
+ .toLowerCase()
+ .includes(modalSearch.toLowerCase()) ||
+ (j.ownr_fn || "")
+ .toLowerCase()
+ .includes(modalSearch.toLowerCase()) ||
+ (j.ownr_ln || "")
+ .toLowerCase()
+ .includes(modalSearch.toLowerCase()) ||
+ (j.status || "")
+ .toLowerCase()
+ .includes(modalSearch.toLowerCase()) ||
+ (j.v_make_desc || "")
+ .toLowerCase()
+ .includes(modalSearch.toLowerCase()) ||
+ (j.v_model_desc || "")
+ .toLowerCase()
+ .includes(modalSearch.toLowerCase()) ||
+ (j.clm_no || "")
+ .toLowerCase()
+ .includes(modalSearch.toLowerCase()) ||
+ (j.plate_no || "")
+ .toLowerCase()
+ .includes(modalSearch.toLowerCase())
+ )
+ : jobsList.data.jobs
+ : null;
+
return (
{loading ? : null}
@@ -48,9 +89,9 @@ export default connect(
setSelectedJob={setSelectedJob}
importOptionsState={importOptionsState}
jobsListLoading={jobsList.loading}
- jobsList={
- jobsList.data && jobsList.data.jobs ? jobsList.data.jobs : null
- }
+ jobsListRefetch={jobsList.refetch}
+ jobsList={jobsData}
+ modalSearchState={modalSearchState}
/>
) : null}
diff --git a/client/src/components/jobs-list-paginated/jobs-list-paginated.component.jsx b/client/src/components/jobs-list-paginated/jobs-list-paginated.component.jsx
new file mode 100644
index 000000000..f0c8bb6e4
--- /dev/null
+++ b/client/src/components/jobs-list-paginated/jobs-list-paginated.component.jsx
@@ -0,0 +1,216 @@
+import { SyncOutlined } from "@ant-design/icons";
+import { Button, Input, Table } from "antd";
+import queryString from "query-string";
+import React, { useState } from "react";
+import { useTranslation } from "react-i18next";
+import { Link, useHistory, useLocation } from "react-router-dom";
+import CurrencyFormatter from "../../utils/CurrencyFormatter";
+import PhoneFormatter from "../../utils/PhoneFormatter";
+import { alphaSort } from "../../utils/sorters";
+import StartChatButton from "../chat-open-button/chat-open-button.component";
+
+export default function JobsList({ refetch, loading, jobs, total }) {
+ const search = queryString.parse(useLocation().search);
+ const { page, sortcolumn, sortorder } = search;
+ const history = useHistory();
+ const [state, setState] = useState({
+ sortedInfo: {},
+ filteredInfo: { text: "" },
+ });
+
+ const { t } = useTranslation();
+ const columns = [
+ {
+ title: t("jobs.fields.ro_number"),
+ dataIndex: "ro_number",
+ key: "ro_number",
+ width: "8%",
+ sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
+ sortOrder: sortcolumn === "ro_number" && sortorder,
+
+ render: (text, record) => (
+ {record.ro_number}
+ ),
+ },
+ {
+ title: t("jobs.fields.est_number"),
+ dataIndex: "est_number",
+ key: "est_number",
+ width: "8%",
+ sorter: (a, b) => a.est_number - b.est_number,
+ sortOrder: sortcolumn === "est_number" && sortorder,
+
+ render: (text, record) => (
+ {record.est_number}
+ ),
+ },
+ {
+ title: t("jobs.fields.owner"),
+ dataIndex: "owner",
+ key: "owner",
+ ellipsis: true,
+ sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
+ width: "25%",
+ sortOrder: sortcolumn === "owner" && sortorder,
+ render: (text, record) => {
+ return record.owner ? (
+
+ {`${record.ownr_fn || ""} ${record.ownr_ln || ""} ${
+ record.ownr_co_nm || ""
+ }`}
+
+ ) : (
+ {`${record.ownr_fn || ""} ${record.ownr_ln || ""} ${
+ record.ownr_co_nm || ""
+ }`}
+ );
+ },
+ },
+ {
+ title: t("jobs.fields.ownr_ph1"),
+ dataIndex: "ownr_ph1",
+ key: "ownr_ph1",
+ width: "12%",
+ ellipsis: true,
+ render: (text, record) => {
+ return record.ownr_ph1 ? (
+
+ {record.ownr_ph1}
+
+
+ ) : null;
+ },
+ },
+ {
+ title: t("jobs.fields.status"),
+ dataIndex: "status",
+ key: "status",
+ width: "10%",
+ ellipsis: true,
+ sorter: (a, b) => alphaSort(a.status, b.status),
+ sortOrder: sortcolumn === "status" && sortorder,
+ render: (text, record) => {
+ return record.status || t("general.labels.na");
+ },
+ },
+
+ {
+ title: t("jobs.fields.vehicle"),
+ dataIndex: "vehicle",
+ key: "vehicle",
+ width: "15%",
+ ellipsis: true,
+ render: (text, record) => {
+ return record.vehicleid ? (
+
+ {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
+ record.v_model_desc || ""
+ }`}
+
+ ) : (
+ {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
+ record.v_model_desc || ""
+ }`}
+ );
+ },
+ },
+ {
+ title: t("vehicles.fields.plate_no"),
+ dataIndex: "plate_no",
+ key: "plate_no",
+ width: "8%",
+ ellipsis: true,
+ sorter: (a, b) => alphaSort(a.plate_no, b.plate_no),
+ sortOrder: sortcolumn === "plate_no" && sortorder,
+ render: (text, record) => {
+ return record.plate_no ? record.plate_no : "";
+ },
+ },
+ {
+ title: t("jobs.fields.clm_no"),
+ dataIndex: "clm_no",
+ key: "clm_no",
+ width: "12%",
+ ellipsis: true,
+ sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
+ sortOrder: sortcolumn === "clm_no" && sortorder,
+ render: (text, record) => {
+ return record.clm_no ? (
+ {record.clm_no}
+ ) : (
+ t("general.labels.unknown")
+ );
+ },
+ },
+ {
+ title: t("jobs.fields.clm_total"),
+ dataIndex: "clm_total",
+ key: "clm_total",
+ width: "10%",
+ sorter: (a, b) => a.clm_total - b.clm_total,
+ sortOrder: sortcolumn === "clm_total" && sortorder,
+ render: (text, record) => {
+ return record.clm_total ? (
+ {record.clm_total}
+ ) : (
+ t("general.labels.unknown")
+ );
+ },
+ },
+ {
+ title: t("jobs.fields.owner_owing"),
+ dataIndex: "owner_owing",
+ key: "owner_owing",
+ width: "8%",
+ render: (text, record) => (
+ {record.owner_owing}
+ ),
+ },
+ ];
+
+ const handleTableChange = (pagination, filters, sorter) => {
+ setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
+ search.page = pagination.current;
+ search.sortcolumn = sorter.columnKey;
+ search.sortorder = sorter.order;
+ history.push({ search: queryString.stringify(search) });
+ };
+
+ return (
+
+
{
+ return (
+
+
+ {
+ search.search = value;
+ history.push({ search: queryString.stringify(search) });
+ }}
+ enterButton
+ />
+
+ );
+ }}
+ />
+
+ );
+}
diff --git a/client/src/components/jobs-list/jobs-list.component.jsx b/client/src/components/jobs-list/jobs-list.component.jsx
index 690fee55b..9dcf55eb5 100644
--- a/client/src/components/jobs-list/jobs-list.component.jsx
+++ b/client/src/components/jobs-list/jobs-list.component.jsx
@@ -1,144 +1,214 @@
-import { Button, Input, Table } from "antd";
import { SyncOutlined } from "@ant-design/icons";
+import { useQuery } from "@apollo/react-hooks";
+import { Button, Input, Table } from "antd";
+import queryString from "query-string";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
-import { Link, withRouter } from "react-router-dom";
+import { connect } from "react-redux";
+import { Link, useHistory, useLocation } from "react-router-dom";
+import { createStructuredSelector } from "reselect";
+import { QUERY_ALL_ACTIVE_JOBS } from "../../graphql/jobs.queries";
+import { selectBodyshop } from "../../redux/user/user.selectors";
+import { onlyUnique } from "../../utils/arrayHelper";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import PhoneFormatter from "../../utils/PhoneFormatter";
import { alphaSort } from "../../utils/sorters";
+import AlertComponent from "../alert/alert.component";
import StartChatButton from "../chat-open-button/chat-open-button.component";
-export default withRouter(function JobsList({
- searchTextState,
- refetch,
- loading,
- jobs,
- selectedJob,
- setSelectedJob,
- history
-}) {
+const mapStateToProps = createStructuredSelector({
+ bodyshop: selectBodyshop,
+});
+
+export function JobsList({ bodyshop }) {
+ const searchParams = queryString.parse(useLocation().search);
+ const { selected } = searchParams;
+
+ const { loading, error, data, refetch } = useQuery(QUERY_ALL_ACTIVE_JOBS, {
+ variables: {
+ statuses: bodyshop.md_ro_statuses.open_statuses || ["Open", "Open*"],
+ },
+ });
+
const [state, setState] = useState({
sortedInfo: {},
- filteredInfo: { text: "" }
+ filteredInfo: { text: "" },
});
const { t } = useTranslation();
+ const history = useHistory();
+ const [searchText, setSearchText] = useState("");
+
+ if (error) return ;
+
+ const jobs = data
+ ? searchText === ""
+ ? data.jobs
+ : data.jobs.filter(
+ (j) =>
+ (j.ro_number || "")
+ .toString()
+ .toLowerCase()
+ .includes(searchText.toLowerCase()) ||
+ (j.ownr_co_nm || "")
+ .toLowerCase()
+ .includes(searchText.toLowerCase()) ||
+ (j.ownr_fn || "")
+ .toLowerCase()
+ .includes(searchText.toLowerCase()) ||
+ (j.ownr_ln || "")
+ .toLowerCase()
+ .includes(searchText.toLowerCase()) ||
+ (j.clm_no || "").toLowerCase().includes(searchText.toLowerCase()) ||
+ (j.plate_no || "")
+ .toLowerCase()
+ .includes(searchText.toLowerCase()) ||
+ (j.v_model_desc || "")
+ .toLowerCase()
+ .includes(searchText.toLowerCase()) ||
+ (j.v_make_desc || "")
+ .toLowerCase()
+ .includes(searchText.toLowerCase())
+ )
+ : [];
+
+ const handleTableChange = (pagination, filters, sorter) => {
+ setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
+ };
+
+ const handleOnRowClick = (record) => {
+ if (record) {
+ if (record.id) {
+ history.push({
+ search: queryString.stringify({
+ ...searchParams,
+ selected: record.id,
+ }),
+ });
+ }
+ }
+ };
- const setSearchText = searchTextState[1];
const columns = [
{
title: t("jobs.fields.ro_number"),
dataIndex: "ro_number",
key: "ro_number",
- width: "8%",
- // onFilter: (value, record) => record.ro_number.includes(value),
- // filteredValue: state.filteredInfo.text || null,
- sorter: (a, b) =>
- alphaSort(
- a.ro_number ? a.ro_number : "EST-" + a.est_number,
- b.ro_number ? b.ro_number : "EST-" + b.est_number
- ),
+ sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
sortOrder:
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
render: (text, record) => (
-
-
- {record.ro_number ? record.ro_number : "EST-" + record.est_number}
-
-
- )
+ {record.ro_number}
+ ),
},
+ {
+ title: t("jobs.fields.est_number"),
+ dataIndex: "est_number",
+ key: "est_number",
+ sorter: (a, b) => a.est_number - b.est_number,
+ sortOrder:
+ state.sortedInfo.columnKey === "est_number" && state.sortedInfo.order,
+
+ render: (text, record) => (
+ {record.est_number}
+ ),
+ },
+
{
title: t("jobs.fields.owner"),
dataIndex: "owner",
key: "owner",
- ellipsis: true,
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
- width: "25%",
sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => {
return record.owner ? (
- {record.ownr_fn} {record.ownr_ln}
+ {`${record.ownr_fn || ""} ${record.ownr_ln || ""} ${
+ record.ownr_co_nm || ""
+ }`}
) : (
- // t("jobs.errors.noowner")
- {`${record.ownr_fn} ${record.ownr_ln}`}
+ {`${record.ownr_fn || ""} ${record.ownr_ln || ""} ${
+ record.ownr_co_nm || ""
+ }`}
);
- }
+ },
},
{
title: t("jobs.fields.ownr_ph1"),
dataIndex: "ownr_ph1",
key: "ownr_ph1",
- width: "12%",
ellipsis: true,
render: (text, record) => {
return record.ownr_ph1 ? (
{record.ownr_ph1}
-
+
- ) : (
- t("general.labels.unknown")
- );
- }
+ ) : null;
+ },
},
{
title: t("jobs.fields.status"),
dataIndex: "status",
key: "status",
- width: "10%",
- ellipsis: true,
sorter: (a, b) => alphaSort(a.status, b.status),
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
+ filters:
+ (jobs &&
+ jobs
+ .map((j) => j.status)
+ .filter(onlyUnique)
+ .map((s) => {
+ return {
+ text: s || "No Status*",
+ value: [s],
+ };
+ })) ||
+ [],
+ onFilter: (value, record) => value.includes(record.status),
render: (text, record) => {
return record.status || t("general.labels.na");
- }
+ },
},
{
title: t("jobs.fields.vehicle"),
dataIndex: "vehicle",
key: "vehicle",
- width: "15%",
ellipsis: true,
render: (text, record) => {
return record.vehicleid ? (
- {`${record.v_model_yr || ""} ${record.v_make_desc ||
- ""} ${record.v_model_desc || ""}`}
+ {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
+ record.v_model_desc || ""
+ }`}
) : (
- t("jobs.errors.novehicle")
+ {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
+ record.v_model_desc || ""
+ }`}
);
- }
+ },
},
{
title: t("vehicles.fields.plate_no"),
dataIndex: "plate_no",
key: "plate_no",
- width: "8%",
- ellipsis: true,
sorter: (a, b) => alphaSort(a.plate_no, b.plate_no),
sortOrder:
state.sortedInfo.columnKey === "plate_no" && state.sortedInfo.order,
render: (text, record) => {
- return record.plate_no ? (
- {record.plate_no}
- ) : (
- t("general.labels.unknown")
- );
- }
+ return record.plate_no ? record.plate_no : "";
+ },
},
{
title: t("jobs.fields.clm_no"),
dataIndex: "clm_no",
key: "clm_no",
- width: "12%",
ellipsis: true,
sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
sortOrder:
@@ -149,13 +219,12 @@ export default withRouter(function JobsList({
) : (
t("general.labels.unknown")
);
- }
+ },
},
{
title: t("jobs.fields.clm_total"),
dataIndex: "clm_total",
key: "clm_total",
- width: "10%",
sorter: (a, b) => a.clm_total - b.clm_total,
sortOrder:
state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order,
@@ -165,81 +234,63 @@ export default withRouter(function JobsList({
) : (
t("general.labels.unknown")
);
- }
+ },
},
{
title: t("jobs.fields.owner_owing"),
dataIndex: "owner_owing",
key: "owner_owing",
- width: "8%",
- render: (text, record) => {
- return record.owner_owing ? (
- {record.owner_owing}
- ) : (
- t("general.labels.unknown")
- );
- }
- }
+ render: (text, record) => (
+ {record.owner_owing}
+ ),
+ },
];
- const handleTableChange = (pagination, filters, sorter) => {
- setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
- };
-
- // const handleChange = event => {
- // const { value } = event.target;
- // setState({ ...state, filterinfo: { text: [value] } });
- // };
-
- const handleOnRowClick = record => {
- if (record) {
- if (record.id) {
- setSelectedJob(record.id);
- return;
- }
- }
- setSelectedJob(null);
- };
-
return (
-
-
{
- return (
-
-
- {
- setSearchText(e.target.value);
- }}
- enterButton
- />
-
- );
- }}
- size="small"
- pagination={{ position: "top" }}
- columns={columns.map(item => ({ ...item }))}
- rowKey="id"
- dataSource={jobs}
- rowSelection={{ selectedRowKeys: [selectedJob] }}
- onChange={handleTableChange}
- onRow={(record, rowIndex) => {
- return {
- onClick: event => {
- handleOnRowClick(record);
- }, // click row
- onDoubleClick: event => {}, // double click row
- onContextMenu: event => {}, // right button click row
- onMouseEnter: event => {}, // mouse enter row
- onMouseLeave: event => {} // mouse leave row
- };
- }}
- />
-
+ {
+ return (
+
+
+ {
+ setSearchText(e.target.value);
+ }}
+ value={searchText}
+ enterButton
+ />
+
+ );
+ }}
+ rowSelection={{
+ onSelect: (record) => {
+ handleOnRowClick(record);
+ },
+ selectedRowKeys: [selected],
+ type: "radio",
+ }}
+ onChange={handleTableChange}
+ onRow={(record, rowIndex) => {
+ return {
+ onClick: (event) => {
+ handleOnRowClick(record);
+ },
+ };
+ }}
+ />
);
-});
+}
+
+export default connect(mapStateToProps, null)(JobsList);
diff --git a/client/src/components/jobs-notes/jobs-notes.container.jsx b/client/src/components/jobs-notes/jobs-notes.container.jsx
index 2270e13f6..a3efaa482 100644
--- a/client/src/components/jobs-notes/jobs-notes.container.jsx
+++ b/client/src/components/jobs-notes/jobs-notes.container.jsx
@@ -1,31 +1,49 @@
-import React from "react";
-import JobNotesComponent from "./jobs.notes.component";
-import { useQuery, useMutation } from "@apollo/react-hooks";
-import AlertComponent from "../../components/alert/alert.component";
+import { useMutation, useQuery } from "@apollo/react-hooks";
+import { notification } from "antd";
+import React, { useState } from "react";
//import SpinComponent from "../../components/loading-spinner/loading-spinner.component";
-import {
- QUERY_NOTES_BY_JOB_PK,
- DELETE_NOTE
-} from "../../graphql/notes.queries";
+import { useTranslation } from "react-i18next";
+import AlertComponent from "../../components/alert/alert.component";
+import { logImEXEvent } from "../../firebase/firebase.utils";
+import { DELETE_NOTE, QUERY_NOTES_BY_JOB_PK } from "../../graphql/notes.queries";
+import JobNotesComponent from "./jobs.notes.component";
+
export default function JobNotesContainer({ jobId }) {
const { loading, error, data, refetch } = useQuery(QUERY_NOTES_BY_JOB_PK, {
variables: { id: jobId },
- fetchPolicy: "network-only"
+ fetchPolicy: "network-only",
});
-
const [deleteNote] = useMutation(DELETE_NOTE);
+ const { t } = useTranslation();
+ const [deleteLoading, setDeleteLoading] = useState(false);
+ const handleNoteDelete = (id) => {
+ logImEXEvent("job_note_delete");
+ setDeleteLoading(true);
+ deleteNote({
+ variables: {
+ noteId: id,
+ },
+ }).then((r) => {
+ refetch();
+ notification["success"]({
+ message: t("notes.successes.deleted"),
+ });
+ });
+ setDeleteLoading(false);
+ };
//if (loading) return ;
- if (error) return ;
+ if (error) return ;
return (
);
}
diff --git a/client/src/components/jobs-notes/jobs.notes.component.jsx b/client/src/components/jobs-notes/jobs.notes.component.jsx
index 13f5cc0db..6c9300742 100644
--- a/client/src/components/jobs-notes/jobs.notes.component.jsx
+++ b/client/src/components/jobs-notes/jobs.notes.component.jsx
@@ -1,25 +1,33 @@
-import React, { useState } from "react";
-import { Table, Button, notification } from "antd";
import {
- WarningFilled,
- EyeInvisibleFilled,
DeleteFilled,
- EditFilled
+ EditFilled,
+ EyeInvisibleFilled,
+ WarningFilled
} from "@ant-design/icons";
+import { Button, Table } from "antd";
+import React from "react";
import { useTranslation } from "react-i18next";
import Moment from "react-moment";
+import { connect } from "react-redux";
+import { setModalContext } from "../../redux/modals/modals.actions";
import NoteUpsertModal from "../note-upsert-modal/note-upsert-modal.container";
-export default function JobNotesComponent({
+const mapDispatchToProps = (dispatch) => ({
+ setNoteUpsertContext: (context) =>
+ dispatch(setModalContext({ context: context, modal: "noteUpsert" })),
+});
+
+export function JobNotesComponent({
loading,
data,
refetch,
- deleteNote,
- jobId
+ handleNoteDelete,
+ jobId,
+ setNoteUpsertContext,
+ deleteLoading,
}) {
const { t } = useTranslation();
- const [noteModalVisible, setNoteModalVisible] = useState(false);
- const [existingNote, setExistingNote] = useState(null);
+
const columns = [
{
title: "",
@@ -28,19 +36,18 @@ export default function JobNotesComponent({
width: 80,
render: (text, record) => (
- {" "}
{record.critical ? (
) : null}
{record.private ? : null}
- )
+ ),
},
{
title: t("notes.fields.text"),
dataIndex: "text",
key: "text",
- ellipsis: true
+ ellipsis: true,
},
{
@@ -54,13 +61,13 @@ export default function JobNotesComponent({
{record.updated_at}
- )
+ ),
},
{
title: t("notes.fields.createdby"),
dataIndex: "created_by",
key: "created_by",
- width: 200
+ width: 200,
},
{
title: t("notes.actions.actions"),
@@ -70,47 +77,40 @@ export default function JobNotesComponent({
render: (text, record) => (
- )
- }
+ ),
+ },
];
return (
-
+