Merged in release/2023-05-05 (pull request #768)

Release/2023 05 05
This commit is contained in:
Patrick Fic
2023-05-05 22:51:16 +00:00
22 changed files with 520 additions and 212 deletions

View File

@@ -6,7 +6,7 @@ import { useTranslation } from "react-i18next";
import { DELETE_BILL } from "../../graphql/bills.queries"; import { DELETE_BILL } from "../../graphql/bills.queries";
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component"; import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
export default function BillDeleteButton({ bill }) { export default function BillDeleteButton({ bill, callback }) {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const { t } = useTranslation(); const { t } = useTranslation();
const [deleteBill] = useMutation(DELETE_BILL); const [deleteBill] = useMutation(DELETE_BILL);
@@ -36,6 +36,8 @@ export default function BillDeleteButton({ bill }) {
if (!!!result.errors) { if (!!!result.errors) {
notification["success"]({ message: t("bills.successes.deleted") }); notification["success"]({ message: t("bills.successes.deleted") });
if (callback && typeof callback === "function") callback(bill.id);
} else { } else {
//Check if it's an fkey violation. //Check if it's an fkey violation.
const error = JSON.stringify(result.errors); const error = JSON.stringify(result.errors);

View File

@@ -109,8 +109,8 @@ export function JobsConvertButton({
]} ]}
> >
<Select> <Select>
{bodyshop.md_ins_cos.map((s) => ( {bodyshop.md_ins_cos.map((s, i) => (
<Select.Option key={s.name} value={s.name}> <Select.Option key={i} value={s.name}>
{s.name} {s.name}
</Select.Option> </Select.Option>
))} ))}

View File

@@ -1,8 +1,9 @@
import { SyncOutlined } from "@ant-design/icons"; import { SyncOutlined } from "@ant-design/icons";
import { Button, Card, Input, Space, Table, Typography } from "antd"; import { Button, Card, Input, Space, Table, Typography } from "antd";
import axios from "axios";
import _ from "lodash"; import _ from "lodash";
import queryString from "query-string"; import queryString from "query-string";
import React from "react"; import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Link, useHistory, useLocation } from "react-router-dom"; import { Link, useHistory, useLocation } from "react-router-dom";
@@ -21,6 +22,8 @@ const mapDispatchToProps = (dispatch) => ({
export function JobsList({ bodyshop, refetch, loading, jobs, total }) { export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
const search = queryString.parse(useLocation().search); const search = queryString.parse(useLocation().search);
const [openSearchResults, setOpenSearchResults] = useState([]);
const [searchLoading, setSearchLoading] = useState(false);
const { page, sortcolumn, sortorder } = search; const { page, sortcolumn, sortorder } = search;
const history = useHistory(); const history = useHistory();
@@ -193,6 +196,28 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
history.push({ search: queryString.stringify(search) }); history.push({ search: queryString.stringify(search) });
}; };
useEffect(() => {
if (search.search && search.search.trim() !== "") {
searchJobs();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
async function searchJobs(value) {
try {
setSearchLoading(true);
const searchData = await axios.post("/search", {
search: value || search.search,
index: "jobs",
});
setOpenSearchResults(searchData.data.hits.hits.map((s) => s._source));
} catch (error) {
console.log("Error while fetching search results", error);
} finally {
setSearchLoading(false);
}
}
return ( return (
<Card <Card
extra={ extra={
@@ -205,6 +230,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
<Button <Button
onClick={() => { onClick={() => {
delete search.search; delete search.search;
delete search.page;
history.push({ search: queryString.stringify(search) }); history.push({ search: queryString.stringify(search) });
}} }}
> >
@@ -220,24 +246,32 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
onSearch={(value) => { onSearch={(value) => {
search.search = value; search.search = value;
history.push({ search: queryString.stringify(search) }); history.push({ search: queryString.stringify(search) });
searchJobs(value);
}} }}
loading={loading || searchLoading}
enterButton enterButton
/> />
</Space> </Space>
} }
> >
<Table <Table
loading={loading} loading={loading || searchLoading}
pagination={{ pagination={
position: "top", search?.search
pageSize: 25, ? {
current: parseInt(page || 1), pageSize: 25,
total: total, showSizeChanger: false,
showSizeChanger: false, }
}} : {
pageSize: 25,
current: parseInt(page || 1),
total: total,
showSizeChanger: false,
}
}
columns={columns} columns={columns}
rowKey="id" rowKey="id"
dataSource={jobs} dataSource={search?.search ? openSearchResults : jobs}
onChange={handleTableChange} onChange={handleTableChange}
/> />
</Card> </Card>

View File

@@ -1,20 +1,23 @@
import { EditFilled, SyncOutlined } from "@ant-design/icons"; import { EditFilled, SyncOutlined } from "@ant-design/icons";
import { useApolloClient } from "@apollo/client";
import { Button, Card, Input, Space, Table, Typography } from "antd"; import { Button, Card, Input, Space, Table, Typography } from "antd";
import axios from "axios";
import queryString from "query-string"; import queryString from "query-string";
import React, { useState } from "react"; import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Link, useHistory, useLocation } from "react-router-dom"; import { Link, useHistory, useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { QUERY_PAYMENT_BY_ID } from "../../graphql/payments.queries";
import { setModalContext } from "../../redux/modals/modals.actions"; import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter"; import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter";
import { alphaSort } from "../../utils/sorters";
import { TemplateList } from "../../utils/TemplateConstants"; import { TemplateList } from "../../utils/TemplateConstants";
import { alphaSort } from "../../utils/sorters";
import CaBcEtfTableModalContainer from "../ca-bc-etf-table-modal/ca-bc-etf-table-modal.container"; import CaBcEtfTableModalContainer from "../ca-bc-etf-table-modal/ca-bc-etf-table-modal.container";
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser //currentUser: selectCurrentUser
@@ -39,7 +42,10 @@ export function PaymentsListPaginated({
bodyshop, bodyshop,
}) { }) {
const search = queryString.parse(useLocation().search); const search = queryString.parse(useLocation().search);
const [openSearchResults, setOpenSearchResults] = useState([]);
const [searchLoading, setSearchLoading] = useState(false);
const { page, sortcolumn, sortorder } = search; const { page, sortcolumn, sortorder } = search;
const client = useApolloClient();
const history = useHistory(); const history = useHistory();
const [state, setState] = useState({ const [state, setState] = useState({
sortedInfo: {}, sortedInfo: {},
@@ -52,13 +58,17 @@ export function PaymentsListPaginated({
title: t("jobs.fields.ro_number"), title: t("jobs.fields.ro_number"),
dataIndex: "ro_number", dataIndex: "ro_number",
key: "ro_number", key: "ro_number",
sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number), // sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
sortOrder: sortcolumn === "ro_number" && sortorder, // sortOrder: sortcolumn === "ro_number" && sortorder,
render: (text, record) => ( render: (text, record) => {
<Link to={"/manage/jobs/" + record.job.id}> return record.job ? (
{record.job.ro_number || t("general.labels.na")} <Link to={"/manage/jobs/" + record.job.id}>
</Link> {record.job.ro_number || t("general.labels.na")}
), </Link>
) : (
<span>{t("general.labels.na")}</span>
);
},
}, },
{ {
title: t("payments.fields.paymentnum"), title: t("payments.fields.paymentnum"),
@@ -72,16 +82,16 @@ export function PaymentsListPaginated({
dataIndex: "owner", dataIndex: "owner",
key: "owner", key: "owner",
ellipsis: true, ellipsis: true,
sorter: (a, b) => alphaSort(a.job.ownr_ln, b.job.ownr_ln), // sorter: (a, b) => alphaSort(a.job.ownr_ln, b.job.ownr_ln),
sortOrder: sortcolumn === "owner" && sortorder, // sortOrder: sortcolumn === "owner" && sortorder,
render: (text, record) => { render: (text, record) => {
return record.job.owner ? ( return record.job?.owner ? (
<Link to={"/manage/owners/" + record.job.owner.id}> <Link to={"/manage/owners/" + record.job?.owner?.id}>
<OwnerNameDisplay ownerObject={record} /> <OwnerNameDisplay ownerObject={record.job} />
</Link> </Link>
) : ( ) : (
<span> <span>
<OwnerNameDisplay ownerObject={record} /> <OwnerNameDisplay ownerObject={record.job} />
</span> </span>
); );
}, },
@@ -147,10 +157,20 @@ export function PaymentsListPaginated({
<Space> <Space>
<Button <Button
disabled={record.exportedat} disabled={record.exportedat}
onClick={() => { onClick={async () => {
let apolloResults;
if (search.search) {
const { data } = await client.query({
query: QUERY_PAYMENT_BY_ID,
variables: {
paymentId: record.id,
},
});
apolloResults = data.payments_by_pk;
}
setPaymentContext({ setPaymentContext({
actions: { refetch: refetch }, actions: { refetch: refetch },
context: record, context: apolloResults ? apolloResults : record,
}); });
}} }}
> >
@@ -177,6 +197,28 @@ export function PaymentsListPaginated({
history.push({ search: queryString.stringify(search) }); history.push({ search: queryString.stringify(search) });
}; };
useEffect(() => {
if (search.search && search.search.trim() !== "") {
searchPayments();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
async function searchPayments(value) {
try {
setSearchLoading(true);
const searchData = await axios.post("/search", {
search: value || search.search,
index: "payments",
});
setOpenSearchResults(searchData.data.hits.hits.map((s) => s._source));
} catch (error) {
console.log("Error while fetching search results", error);
} finally {
setSearchLoading(false);
}
}
return ( return (
<Card <Card
extra={ extra={
@@ -189,6 +231,7 @@ export function PaymentsListPaginated({
<Button <Button
onClick={() => { onClick={() => {
delete search.search; delete search.search;
delete search.page;
history.push({ search: queryString.stringify(search) }); history.push({ search: queryString.stringify(search) });
}} }}
> >
@@ -212,24 +255,33 @@ export function PaymentsListPaginated({
onSearch={(value) => { onSearch={(value) => {
search.search = value; search.search = value;
history.push({ search: queryString.stringify(search) }); history.push({ search: queryString.stringify(search) });
searchPayments(value);
}} }}
loading={loading || searchLoading}
enterButton enterButton
/> />
</Space> </Space>
} }
> >
<Table <Table
loading={loading} loading={loading || searchLoading}
scroll={{ x: true }} scroll={{ x: true }}
pagination={{ pagination={
position: "top", search?.search
pageSize: 25, ? {
current: parseInt(page || 1), pageSize: 25,
total: total, showSizeChanger: false,
}} }
: {
pageSize: 25,
current: parseInt(page || 1),
total: total,
showSizeChanger: false,
}
}
columns={columns} columns={columns}
rowKey="id" rowKey="id"
dataSource={payments} dataSource={search?.search ? openSearchResults : payments}
onChange={handleTableChange} onChange={handleTableChange}
/> />
</Card> </Card>

View File

@@ -0,0 +1,51 @@
import { Col, List, Space, Typography } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
const CardColorLegend = ({ bodyshop }) => {
const { t } = useTranslation();
const data = bodyshop.ssbuckets.map((bucket) => {
let color = { r: 255, g: 255, b: 255 };
if (bucket.color) {
color = bucket.color;
if (bucket.color.rgb) {
color = bucket.color.rgb;
}
}
return {
label: bucket.label,
color,
};
});
return (
<Col>
<Typography>{t("production.labels.legend")}</Typography>
<List
grid={{
gutter: 16,
}}
dataSource={data}
renderItem={(item) => (
<List.Item>
<Space>
<div
style={{
width: "1.5rem",
aspectRatio: "1/1",
backgroundColor: `rgba(${item.color.r},${item.color.g},${item.color.b},${item.color.a})`,
}}
></div>
<div>{item.label}</div>
</Space>
</List.Item>
)}
/>
</Col>
);
};
export default CardColorLegend;

View File

@@ -18,6 +18,31 @@ import moment from "moment";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component"; import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
const cardColor = (ssbuckets, totalHrs) => {
const bucket = ssbuckets.filter(
(bucket) =>
bucket.gte <= totalHrs && (!!bucket.lt ? bucket.lt > totalHrs : true)
)[0];
let color = { r: 255, g: 255, b: 255 };
if (bucket && bucket.color) {
color = bucket.color;
if (bucket.color.rgb) {
color = bucket.color.rgb;
}
}
return color;
};
function getContrastYIQ(bgColor) {
const yiq = (bgColor.r * 299 + bgColor.g * 587 + bgColor.b * 114) / 1000;
return yiq >= 128 ? "black" : "white";
}
export default function ProductionBoardCard( export default function ProductionBoardCard(
technician, technician,
card, card,
@@ -54,10 +79,22 @@ export default function ProductionBoardCard(
.isSame(moment(card.scheduled_completion), "day") && .isSame(moment(card.scheduled_completion), "day") &&
"production-completion-soon")); "production-completion-soon"));
const totalHrs =
card.labhrs.aggregate.sum.mod_lb_hrs + card.larhrs.aggregate.sum.mod_lb_hrs;
const bgColor = cardColor(bodyshop.ssbuckets, totalHrs);
return ( return (
<Card <Card
className="react-kanban-card imex-kanban-card" className="react-kanban-card imex-kanban-card"
size="small" size="small"
style={{
backgroundColor:
cardSettings &&
cardSettings.cardcolor &&
`rgba(${bgColor.r},${bgColor.g},${bgColor.b},${bgColor.a})`,
color:
cardSettings && cardSettings.cardcolor && getContrastYIQ(bgColor),
}}
title={ title={
<Space> <Space>
<ProductionAlert record={card} key="alert" /> <ProductionAlert record={card} key="alert" />

View File

@@ -104,6 +104,13 @@ export default function ProductionBoardKanbanCardSettings({
> >
<Switch /> <Switch />
</Form.Item> </Form.Item>
<Form.Item
valuePropName="checked"
label={t("production.labels.cardcolor")}
name="cardcolor"
>
<Switch />
</Form.Item>
</Col> </Col>
<Col span={12}> <Col span={12}>
<Form.Item <Form.Item
@@ -166,7 +173,7 @@ export default function ProductionBoardKanbanCardSettings({
</div> </div>
); );
return ( return (
<Popover content={overlay} visible={visible}> <Popover content={overlay} visible={visible} placement="topRight">
<Button loading={loading} onClick={() => setVisible(true)}> <Button loading={loading} onClick={() => setVisible(true)}>
{t("production.labels.cardsettings")} {t("production.labels.cardsettings")}
</Button> </Button>

View File

@@ -22,6 +22,7 @@ import ProductionBoardKanbanCardSettings from "./production-board-kanban.card-se
//import "@asseinfo/react-kanban/dist/styles.css"; //import "@asseinfo/react-kanban/dist/styles.css";
import "./production-board-kanban.styles.scss"; import "./production-board-kanban.styles.scss";
import { createBoardData } from "./production-board-kanban.utils.js"; import { createBoardData } from "./production-board-kanban.utils.js";
import CardColorLegend from "../production-board-kanban-card/production-board-kanban-card-color-legend.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
technician: selectTechnician, technician: selectTechnician,
@@ -221,6 +222,7 @@ export function ProductionBoardKanbanComponent({
employeeassignments: true, employeeassignments: true,
scheduled_completion: true, scheduled_completion: true,
stickyheader: false, stickyheader: false,
cardcolor: false,
}; };
return ( return (
@@ -256,6 +258,11 @@ export function ProductionBoardKanbanComponent({
</Space> </Space>
} }
/> />
{cardSettings.cardcolor && (
<CardColorLegend cardSettings={cardSettings} bodyshop={bodyshop} />
)}
<ProductionListDetailComponent jobs={data} /> <ProductionListDetailComponent jobs={data} />
<StickyContainer> <StickyContainer>
<Board <Board

View File

@@ -396,7 +396,7 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
); );
} }
const ColorPicker = ({ value, onChange, style, ...restProps }) => { export const ColorPicker = ({ value, onChange, style, ...restProps }) => {
const handleChange = (color) => { const handleChange = (color) => {
if (onChange) onChange(color.rgb); if (onChange) onChange(color.rgb);
}; };

View File

@@ -15,6 +15,7 @@ import { useTranslation } from "react-i18next";
import ColorpickerFormItemComponent from "../form-items-formatted/colorpicker-form-item.component"; import ColorpickerFormItemComponent from "../form-items-formatted/colorpicker-form-item.component";
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component"; import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import { ColorPicker } from "./shop-info.rostatus.component";
export default function ShopInfoSchedulingComponent({ form }) { export default function ShopInfoSchedulingComponent({ form }) {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -277,17 +278,50 @@ export default function ShopInfoSchedulingComponent({ form }) {
> >
<InputNumber /> <InputNumber />
</Form.Item> </Form.Item>
<Space wrap>
<DeleteFilled <Space direction="horizontal">
onClick={() => { <Form.Item
remove(field.name); label={
}} <Space>
/> {t("bodyshop.fields.ssbuckets.color")}
<FormListMoveArrows <Button
move={move} size="small"
index={index} onClick={() => {
total={fields.length} form.setFieldValue([
/> "ssbuckets",
field.name,
"color",
]);
form.setFields([
{
name: ["ssbuckets", field.name, "color"],
touched: true,
},
]);
}}
>
Reset
</Button>
</Space>
}
key={`${index}color`}
name={[field.name, "color"]}
>
<ColorPicker />
</Form.Item>
<Space wrap>
<DeleteFilled
onClick={() => {
remove(field.name);
}}
/>
<FormListMoveArrows
move={move}
index={index}
total={fields.length}
/>
</Space>
</Space> </Space>
</LayoutFormRow> </LayoutFormRow>
</Form.Item> </Form.Item>

View File

@@ -20,13 +20,11 @@ export const DELETE_BILL = gql`
export const QUERY_ALL_BILLS_PAGINATED = gql` export const QUERY_ALL_BILLS_PAGINATED = gql`
query QUERY_ALL_BILLS_PAGINATED( query QUERY_ALL_BILLS_PAGINATED(
$search: String
$offset: Int $offset: Int
$limit: Int $limit: Int
$order: [bills_order_by!]! $order: [bills_order_by!]!
) { ) {
search_bills( bills(
args: { search: $search }
offset: $offset offset: $offset
limit: $limit limit: $limit
order_by: $order order_by: $order
@@ -51,7 +49,7 @@ export const QUERY_ALL_BILLS_PAGINATED = gql`
ro_number ro_number
} }
} }
search_bills_aggregate(args: { search: $search }) { bills_aggregate {
aggregate { aggregate {
count(distinct: true) count(distinct: true)
} }

View File

@@ -1781,14 +1781,12 @@ export const QUERY_ALL_JOB_FIELDS = gql`
export const QUERY_ALL_JOBS_PAGINATED_STATUS_FILTERED = gql` export const QUERY_ALL_JOBS_PAGINATED_STATUS_FILTERED = gql`
query QUERY_ALL_JOBS_PAGINATED_STATUS_FILTERED( query QUERY_ALL_JOBS_PAGINATED_STATUS_FILTERED(
$search: String
$offset: Int $offset: Int
$limit: Int $limit: Int
$order: [jobs_order_by!] $order: [jobs_order_by!]
$statusList: [String!] $statusList: [String!]
) { ) {
search_jobs( jobs(
args: { search: $search }
offset: $offset offset: $offset
limit: $limit limit: $limit
order_by: $order order_by: $order
@@ -1819,10 +1817,7 @@ export const QUERY_ALL_JOBS_PAGINATED_STATUS_FILTERED = gql`
updated_at updated_at
ded_amt ded_amt
} }
search_jobs_aggregate( jobs_aggregate(where: { status: { _in: $statusList } }) {
args: { search: $search }
where: { status: { _in: $statusList } }
) {
aggregate { aggregate {
count(distinct: true) count(distinct: true)
} }

View File

@@ -12,13 +12,11 @@ export const INSERT_NEW_PAYMENT = gql`
export const QUERY_ALL_PAYMENTS_PAGINATED = gql` export const QUERY_ALL_PAYMENTS_PAGINATED = gql`
query QUERY_ALL_PAYMENTS_PAGINATED( query QUERY_ALL_PAYMENTS_PAGINATED(
$search: String
$offset: Int $offset: Int
$limit: Int $limit: Int
$order: [payments_order_by!]! $order: [payments_order_by!]!
) { ) {
search_payments( payments(
args: { search: $search }
offset: $offset offset: $offset
limit: $limit limit: $limit
order_by: $order order_by: $order
@@ -31,9 +29,16 @@ export const QUERY_ALL_PAYMENTS_PAGINATED = gql`
job { job {
id id
ro_number ro_number
ownerid
ownr_co_nm
ownr_fn ownr_fn
ownr_ln ownr_ln
ownr_co_nm owner {
id
ownr_co_nm
ownr_fn
ownr_ln
}
} }
transactionid transactionid
memo memo
@@ -44,7 +49,7 @@ export const QUERY_ALL_PAYMENTS_PAGINATED = gql`
stripeid stripeid
payer payer
} }
search_payments_aggregate(args: { search: $search }) { payments_aggregate {
aggregate { aggregate {
count(distinct: true) count(distinct: true)
} }
@@ -109,3 +114,37 @@ export const QUERY_JOB_PAYMENT_TOTALS = gql`
} }
} }
`; `;
export const QUERY_PAYMENT_BY_ID = gql`query QUERY_PAYMENT_BY_ID($paymentId: uuid!) {
payments_by_pk(id: $paymentId) {
id
created_at
jobid
paymentnum
date
job {
id
ro_number
ownerid
ownr_co_nm
ownr_fn
ownr_ln
owner {
id
ownr_co_nm
ownr_fn
ownr_ln
}
}
transactionid
memo
type
amount
stripeid
exportedat
stripeid
payer
}
}
`

View File

@@ -1,7 +1,8 @@
import { SyncOutlined, EditFilled } from "@ant-design/icons"; import { EditFilled, SyncOutlined } from "@ant-design/icons";
import { Button, Card, Checkbox, Input, Space, Table, Typography } from "antd"; import { Button, Card, Checkbox, Input, Space, Table, Typography } from "antd";
import axios from "axios";
import queryString from "query-string"; import queryString from "query-string";
import React, { useState } from "react"; import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Link, useHistory, useLocation } from "react-router-dom"; import { Link, useHistory, useLocation } from "react-router-dom";
@@ -11,8 +12,8 @@ import PrintWrapperComponent from "../../components/print-wrapper/print-wrapper.
import { setModalContext } from "../../redux/modals/modals.actions"; import { setModalContext } from "../../redux/modals/modals.actions";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter } from "../../utils/DateFormatter"; import { DateFormatter } from "../../utils/DateFormatter";
import { alphaSort, dateSort } from "../../utils/sorters";
import { TemplateList } from "../../utils/TemplateConstants"; import { TemplateList } from "../../utils/TemplateConstants";
import { alphaSort, dateSort } from "../../utils/sorters";
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
setPartsOrderContext: (context) => setPartsOrderContext: (context) =>
@@ -29,34 +30,36 @@ export function BillsListPage({
setPartsOrderContext, setPartsOrderContext,
setBillEnterContext, setBillEnterContext,
}) { }) {
const { t } = useTranslation(); const search = queryString.parse(useLocation().search);
const [openSearchResults, setOpenSearchResults] = useState([]);
const [searchLoading, setSearchLoading] = useState(false);
const { page } = search;
const history = useHistory();
const [state, setState] = useState({ const [state, setState] = useState({
sortedInfo: {}, sortedInfo: {},
filteredInfo: { text: "" },
}); });
const history = useHistory();
const search = queryString.parse(useLocation().search);
const { page } = search;
const Templates = TemplateList("bill"); const Templates = TemplateList("bill");
const { t } = useTranslation();
const columns = [ const columns = [
{ {
title: t("bills.fields.vendorname"), title: t("bills.fields.vendorname"),
dataIndex: "vendorname", dataIndex: "vendorname",
key: "vendorname", key: "vendorname",
sortObject: (direction) => { // sortObject: (direction) => {
return { // return {
vendor: { // vendor: {
name: direction // name: direction
? direction === "descend" // ? direction === "descend"
? "desc" // ? "desc"
: "asc" // : "asc"
: "desc", // : "desc",
}, // },
}; // };
}, // },
sorter: (a, b) => alphaSort(a.vendor.name, b.vendor.name), // sorter: (a, b) => alphaSort(a.vendor.name, b.vendor.name),
sortOrder: // sortOrder:
state.sortedInfo.columnKey === "vendorname" && state.sortedInfo.order, // state.sortedInfo.columnKey === "vendorname" && state.sortedInfo.order,
render: (text, record) => <span>{record.vendor.name}</span>, render: (text, record) => <span>{record.vendor.name}</span>,
}, },
{ {
@@ -72,20 +75,20 @@ export function BillsListPage({
title: t("jobs.fields.ro_number"), title: t("jobs.fields.ro_number"),
dataIndex: "ro_number", dataIndex: "ro_number",
key: "ro_number", key: "ro_number",
sortObject: (direction) => { // sortObject: (direction) => {
return { // return {
job: { // job: {
ro_number: direction // ro_number: direction
? direction === "descend" // ? direction === "descend"
? "desc" // ? "desc"
: "asc" // : "asc"
: "desc", // : "desc",
}, // },
}; // };
}, // },
sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number), // sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
sortOrder: // sortOrder:
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order, // state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
render: (text, record) => render: (text, record) =>
record.job && ( record.job && (
<Link to={`/manage/jobs/${record.job.id}`}> <Link to={`/manage/jobs/${record.job.id}`}>
@@ -174,7 +177,15 @@ export function BillsListPage({
// {t("bills.actions.return")} // {t("bills.actions.return")}
// </Button> // </Button>
} }
<BillDeleteButton bill={record} /> <BillDeleteButton
bill={record}
callback={(deletedBillid) => {
//Filter out the state and set it again.
setOpenSearchResults((currentResults) =>
currentResults.filter((bill) => bill.id !== deletedBillid)
);
}}
/>
{record.isinhouse && ( {record.isinhouse && (
<PrintWrapperComponent <PrintWrapperComponent
templateObject={{ templateObject={{
@@ -199,11 +210,32 @@ export function BillsListPage({
search.sortcolumn = sorter.order ? sorter.columnKey : null; search.sortcolumn = sorter.order ? sorter.columnKey : null;
search.sortorder = sorter.order; search.sortorder = sorter.order;
} }
search.sort = JSON.stringify({ [sorter.columnKey]: sorter.order }); search.sort = JSON.stringify({ [sorter.columnKey]: sorter.order });
history.push({ search: queryString.stringify(search) }); history.push({ search: queryString.stringify(search) });
}; };
useEffect(() => {
if (search.search && search.search.trim() !== "") {
searchBills();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
async function searchBills(value) {
try {
setSearchLoading(true);
const searchData = await axios.post("/search", {
search: value || search.search,
index: "bills",
});
setOpenSearchResults(searchData.data.hits.hits.map((s) => s._source));
} catch (error) {
console.log("Error while fetching search results", error);
} finally {
setSearchLoading(false);
}
}
return ( return (
<Card <Card
title={t("bills.labels.bills")} title={t("bills.labels.bills")}
@@ -217,6 +249,7 @@ export function BillsListPage({
<Button <Button
onClick={() => { onClick={() => {
delete search.search; delete search.search;
delete search.page;
history.push({ search: queryString.stringify(search) }); history.push({ search: queryString.stringify(search) });
}} }}
> >
@@ -243,7 +276,10 @@ export function BillsListPage({
onSearch={(value) => { onSearch={(value) => {
search.search = value; search.search = value;
history.push({ search: queryString.stringify(search) }); history.push({ search: queryString.stringify(search) });
searchBills(value);
}} }}
loading={loading || searchLoading}
enterButton
/> />
</Space> </Space>
} }
@@ -251,19 +287,27 @@ export function BillsListPage({
<PartsOrderModalContainer /> <PartsOrderModalContainer />
<Table <Table
loading={loading} loading={loading || searchLoading}
scroll={{ // scroll={{
x: "50%", // y: "40rem" // x: "50%", // y: "40rem"
}} // }}
pagination={{ scroll={{ x: true }}
position: "top", pagination={
pageSize: 25, search?.search
current: parseInt(page || 1), ? {
total: total, pageSize: 25,
}} showSizeChanger: false,
}
: {
pageSize: 25,
current: parseInt(page || 1),
total: total,
showSizeChanger: false,
}
}
columns={columns} columns={columns}
rowKey="id" rowKey="id"
dataSource={data} dataSource={search?.search ? openSearchResults : data}
onChange={handleTableChange} onChange={handleTableChange}
/> />
</Card> </Card>

View File

@@ -1,6 +1,6 @@
import { useQuery } from "@apollo/client";
import queryString from "query-string"; import queryString from "query-string";
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { useQuery } from "@apollo/client";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { useLocation } from "react-router-dom"; import { useLocation } from "react-router-dom";
@@ -22,7 +22,7 @@ const mapDispatchToProps = (dispatch) => ({
export function BillsPageContainer({ setBreadcrumbs, setSelectedHeader }) { export function BillsPageContainer({ setBreadcrumbs, setSelectedHeader }) {
const { t } = useTranslation(); const { t } = useTranslation();
const searchParams = queryString.parse(useLocation().search); const searchParams = queryString.parse(useLocation().search);
const { page, sortcolumn, sortorder, search, searchObj } = searchParams; const { page, sortcolumn, sortorder, searchObj } = searchParams;
useEffect(() => { useEffect(() => {
document.title = t("titles.bills-list"); document.title = t("titles.bills-list");
@@ -38,7 +38,6 @@ export function BillsPageContainer({ setBreadcrumbs, setSelectedHeader }) {
fetchPolicy: "network-only", fetchPolicy: "network-only",
nextFetchPolicy: "network-only", nextFetchPolicy: "network-only",
variables: { variables: {
search: search || "",
offset: page ? (page - 1) * 25 : 0, offset: page ? (page - 1) * 25 : 0,
limit: 25, limit: 25,
order: [ order: [
@@ -61,10 +60,10 @@ export function BillsPageContainer({ setBreadcrumbs, setSelectedHeader }) {
<RbacWrapper action="bills:list"> <RbacWrapper action="bills:list">
<div> <div>
<BillsPageComponent <BillsPageComponent
data={data ? data.search_bills : []} data={data ? data.bills : []}
loading={loading} loading={loading}
refetch={refetch} refetch={refetch}
total={data ? data.search_bills_aggregate.aggregate.count : 0} total={data ? data.bills_aggregate.aggregate.count : 0}
/> />
<BillDetailEditContainer /> <BillDetailEditContainer />

View File

@@ -25,7 +25,7 @@ const mapDispatchToProps = (dispatch) => ({
export function AllJobs({ setBreadcrumbs, setSelectedHeader }) { export function AllJobs({ setBreadcrumbs, setSelectedHeader }) {
const searchParams = queryString.parse(useLocation().search); const searchParams = queryString.parse(useLocation().search);
const { page, sortcolumn, sortorder, search, statusFilters } = searchParams; const { page, sortcolumn, sortorder, statusFilters } = searchParams;
const { loading, error, data, refetch } = useQuery( const { loading, error, data, refetch } = useQuery(
QUERY_ALL_JOBS_PAGINATED_STATUS_FILTERED, QUERY_ALL_JOBS_PAGINATED_STATUS_FILTERED,
@@ -33,7 +33,6 @@ export function AllJobs({ setBreadcrumbs, setSelectedHeader }) {
fetchPolicy: "network-only", fetchPolicy: "network-only",
nextFetchPolicy: "network-only", nextFetchPolicy: "network-only",
variables: { variables: {
search: search || "",
offset: page ? (page - 1) * 25 : 0, offset: page ? (page - 1) * 25 : 0,
limit: 25, limit: 25,
...(statusFilters ? { statusList: JSON.parse(statusFilters) } : {}), ...(statusFilters ? { statusList: JSON.parse(statusFilters) } : {}),
@@ -67,8 +66,8 @@ export function AllJobs({ setBreadcrumbs, setSelectedHeader }) {
refetch={refetch} refetch={refetch}
loading={loading} loading={loading}
searchParams={searchParams} searchParams={searchParams}
total={data ? data.search_jobs_aggregate.aggregate.count : 0} total={data ? data.jobs_aggregate.aggregate.count : 0}
jobs={data ? data.search_jobs : []} jobs={data ? data.jobs : []}
/> />
</RbacWrapper> </RbacWrapper>
); );

View File

@@ -26,7 +26,7 @@ const mapDispatchToProps = (dispatch) => ({
export function AllJobs({ bodyshop, setBreadcrumbs, setSelectedHeader }) { export function AllJobs({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
const searchParams = queryString.parse(useLocation().search); const searchParams = queryString.parse(useLocation().search);
const { page, sortcolumn, sortorder, search } = searchParams; const { page, sortcolumn, sortorder, searchObj } = searchParams;
const { loading, error, data, refetch } = useQuery( const { loading, error, data, refetch } = useQuery(
QUERY_ALL_PAYMENTS_PAGINATED, QUERY_ALL_PAYMENTS_PAGINATED,
@@ -34,11 +34,12 @@ export function AllJobs({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
fetchPolicy: "network-only", fetchPolicy: "network-only",
nextFetchPolicy: "network-only", nextFetchPolicy: "network-only",
variables: { variables: {
search: search || "",
offset: page ? (page - 1) * 25 : 0, offset: page ? (page - 1) * 25 : 0,
limit: 25, limit: 25,
order: [ order: [
{ searchObj
? JSON.parse(searchObj)
: {
[sortcolumn || "date"]: sortorder [sortcolumn || "date"]: sortorder
? sortorder === "descend" ? sortorder === "descend"
? "desc" ? "desc"
@@ -66,8 +67,8 @@ export function AllJobs({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
refetch={refetch} refetch={refetch}
loading={loading} loading={loading}
searchParams={searchParams} searchParams={searchParams}
total={data ? data.search_payments_aggregate.aggregate.count : 0} total={data ? data.payments_aggregate.aggregate.count : 0}
payments={data ? data.search_payments : []} payments={data ? data.payments : []}
/> />
</RbacWrapper> </RbacWrapper>
); );

View File

@@ -508,7 +508,8 @@
"id": "ID", "id": "ID",
"label": "Label", "label": "Label",
"lt": "Less than (hrs)", "lt": "Less than (hrs)",
"target": "Target (count)" "target": "Target (count)",
"color": "Job Color"
}, },
"state": "Province/State", "state": "Province/State",
"state_tax_id": "Provincial/State Tax ID (PST, QST)", "state_tax_id": "Provincial/State Tax ID (PST, QST)",
@@ -2385,7 +2386,9 @@
"sublets": "Sublets", "sublets": "Sublets",
"totalhours": "Total Hrs ", "totalhours": "Total Hrs ",
"touchtime": "T/T", "touchtime": "T/T",
"viewname": "View Name" "viewname": "View Name",
"legend": "Legend:",
"cardcolor": "Card Colors"
}, },
"successes": { "successes": {
"removed": "Job removed from production." "removed": "Job removed from production."

View File

@@ -682,13 +682,7 @@
insert: insert:
columns: '*' columns: '*'
update: update:
columns: columns: '*'
- jobid
- invoice_number
- due_date
- vendorid
- id
- date
retry_conf: retry_conf:
interval_sec: 10 interval_sec: 10
num_retries: 3 num_retries: 3
@@ -4094,22 +4088,7 @@
insert: insert:
columns: '*' columns: '*'
update: update:
columns: columns: '*'
- v_color
- ownerid
- ownr_fn
- v_model_desc
- ownr_ln
- id
- v_make_desc
- ownr_st
- clm_no
- voided
- status
- ownr_co_nm
- v_model_yr
- v_vin
- converted
retry_conf: retry_conf:
interval_sec: 10 interval_sec: 10
num_retries: 3 num_retries: 3
@@ -4557,12 +4536,7 @@
insert: insert:
columns: '*' columns: '*'
update: update:
columns: columns: '*'
- shopid
- ownr_fn
- id
- ownr_co_nm
- ownr_ln
retry_conf: retry_conf:
interval_sec: 10 interval_sec: 10
num_retries: 3 num_retries: 3
@@ -5009,16 +4983,7 @@
insert: insert:
columns: '*' columns: '*'
update: update:
columns: columns: '*'
- paymentnum
- type
- amount
- date
- transactionid
- memo
- payer
- id
- jobid
retry_conf: retry_conf:
interval_sec: 10 interval_sec: 10
num_retries: 3 num_retries: 3
@@ -5956,14 +5921,7 @@
insert: insert:
columns: '*' columns: '*'
update: update:
columns: columns: '*'
- v_model_yr
- plate_no
- id
- v_vin
- v_model_desc
- plate_st
- shopid
retry_conf: retry_conf:
interval_sec: 10 interval_sec: 10
num_retries: 3 num_retries: 3

View File

@@ -50,7 +50,7 @@ const getClient = async () => {
async function OpenSearchUpdateHandler(req, res) { async function OpenSearchUpdateHandler(req, res) {
try { try {
var osClient = await getClient(); var osClient = await getClient();
// const osClient = new Client({ // const osClient = new Client({
// node: `https://imex:password@search-imexonline-search-ixp2stfvwp6qocjsowzjzyreoy.ca-central-1.es.amazonaws.com`, // node: `https://imex:password@search-imexonline-search-ixp2stfvwp6qocjsowzjzyreoy.ca-central-1.es.amazonaws.com`,
// }); // });
@@ -74,12 +74,19 @@ async function OpenSearchUpdateHandler(req, res) {
const jobsData = await gqlclient.request(`query{jobs{ const jobsData = await gqlclient.request(`query{jobs{
id id
bodyshopid:shopid bodyshopid:shopid
ro_number
clm_no clm_no
clm_total
comment
ins_co_nm
owner_owing
ownr_co_nm
ownr_fn ownr_fn
ownr_ln ownr_ln
ownr_ph1
ownr_ph2
plate_no
ro_number
status status
ownr_co_nm
v_model_yr v_model_yr
v_make_desc v_make_desc
v_model_desc v_model_desc
@@ -128,12 +135,12 @@ async function OpenSearchUpdateHandler(req, res) {
vehicles { vehicles {
id id
bodyshopid: shopid bodyshopid: shopid
v_model_yr plate_no
v_model_desc v_model_yr
v_make_desc v_model_desc
v_color v_make_desc
v_vin v_color
plate_no v_vin
} }
} }
`); `);
@@ -155,11 +162,26 @@ plate_no
payments { payments {
id id
amount amount
paymentnum created_at
date
exportedat
memo memo
payer
paymentnum
transactionid transactionid
type
job { job {
id id
ownerid
ownr_co_nm
ownr_fn
ownr_ln
owner {
id
ownr_co_nm
ownr_fn
ownr_ln
}
ro_number ro_number
bodyshopid: shopid bodyshopid: shopid
} }
@@ -187,9 +209,12 @@ plate_no
const billsData = await gqlclient.request(`{ const billsData = await gqlclient.request(`{
bills { bills {
id id
total
invoice_number
date date
exported
exported_at
invoice_number
is_credit_memo
total
vendor { vendor {
name name
id id
@@ -200,9 +225,7 @@ plate_no
bodyshopid: shopid bodyshopid: shopid
} }
} }
} }`);
`);
for (let i = 0; i <= billsData.bills.length / batchSize; i++) { for (let i = 0; i <= billsData.bills.length / batchSize; i++) {
const slicedArray = billsData.bills.slice( const slicedArray = billsData.bills.slice(
i * batchSize, i * batchSize,

View File

@@ -40,11 +40,7 @@ exports.default = async (req, res) => {
const specificShopIds = req.body.bodyshopIds; // ['uuid] const specificShopIds = req.body.bodyshopIds; // ['uuid]
const { start, end, skipUpload } = req.body; //YYYY-MM-DD const { start, end, skipUpload } = req.body; //YYYY-MM-DD
if ( if (req.headers["x-imex-auth"] !== process.env.AUTOHOUSE_AUTH_TOKEN) {
!start ||
!moment(start).isValid ||
req.headers["x-imex-auth"] !== process.env.AUTOHOUSE_AUTH_TOKEN
) {
res.sendStatus(401); res.sendStatus(401);
return; return;
} }

View File

@@ -70,12 +70,19 @@ async function OpenSearchUpdateHandler(req, res) {
document = _.pick(req.body.event.data.new, [ document = _.pick(req.body.event.data.new, [
"id", "id",
"bodyshopid", "bodyshopid",
"ro_number",
"clm_no", "clm_no",
"clm_total",
"comment",
"ins_co_nm",
"owner_owing",
"ownr_co_nm",
"ownr_fn", "ownr_fn",
"ownr_ln", "ownr_ln",
"ownr_ph1",
"ownr_ph2",
"plate_no",
"ro_number",
"status", "status",
"ownr_co_nm",
"v_model_yr", "v_model_yr",
"v_make_desc", "v_make_desc",
"v_model_desc", "v_model_desc",
@@ -124,17 +131,19 @@ async function OpenSearchUpdateHandler(req, res) {
`, `,
{ billId: req.body.event.data.new.id } { billId: req.body.event.data.new.id }
); );
document = { document = {
..._.pick(req.body.event.data.new, [ ..._.pick(req.body.event.data.new, [
"id", "id",
"invoice_number",
"date", "date",
"exported",
"exported_at",
"invoice_number",
"is_credit_memo",
"total"
]), ]),
...bill.bills_by_pk, ...bill.bills_by_pk,
bodyshopid: bill.bills_by_pk.job.shopid, bodyshopid: bill.bills_by_pk.job.shopid,
}; };
break; break;
case "payments": case "payments":
//Query to get the job and RO number //Query to get the job and RO number
@@ -146,21 +155,40 @@ async function OpenSearchUpdateHandler(req, res) {
id id
ro_number ro_number
shopid shopid
ownerid
ownr_co_nm
ownr_fn
ownr_ln
owner {
id
ownr_co_nm
ownr_fn
ownr_ln
}
} }
} }
} }
`,
`,
{ paymentId: req.body.event.data.new.id } { paymentId: req.body.event.data.new.id }
); );
document = { document = {
..._.pick(req.body.event.data.new, ["id", "invoice_number"]), ..._.pick(req.body.event.data.new, [
"id",
"amount",
"created_at",
"date",
"exportedat",
"memo",
"payer",
"paymentnum",
"transactionid",
"type",
]),
...payment.payments_by_pk, ...payment.payments_by_pk,
bodyshopid: bill.payments_by_pk.job.shopid, bodyshopid: payment.payments_by_pk.job.shopid,
}; };
break; break;
} }
const payload = { const payload = {
id: req.body.event.data.new.id, id: req.body.event.data.new.id,
index: req.body.table.name, index: req.body.table.name,
@@ -180,7 +208,7 @@ async function OpenSearchUpdateHandler(req, res) {
async function OpensearchSearchHandler(req, res) { async function OpensearchSearchHandler(req, res) {
try { try {
const { search, bodyshopid } = req.body; const { search, bodyshopid, index } = req.body;
if (!req.user) { if (!req.user) {
res.sendStatus(401); res.sendStatus(401);
return; return;
@@ -209,6 +237,7 @@ async function OpensearchSearchHandler(req, res) {
var osClient = await getClient(); var osClient = await getClient();
const { body } = await osClient.search({ const { body } = await osClient.search({
...(index ? { index } : {}),
body: { body: {
size: 100, size: 100,
query: { query: {