Breaking Changes as a part of BOD-63 on invoice enter. WIP for all invoices screen editing + lookup.
This commit is contained in:
@@ -56,10 +56,11 @@
|
|||||||
}
|
}
|
||||||
.messages ul li p {
|
.messages ul li p {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 10px 15px;
|
margin: 0px;
|
||||||
|
padding: 0px 10px;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
max-width: 205px;
|
max-width: 205px;
|
||||||
line-height: 130%;
|
//line-height: 130%;
|
||||||
}
|
}
|
||||||
@media screen and (min-width: 735px) {
|
@media screen and (min-width: 735px) {
|
||||||
.messages ul li p {
|
.messages ul li p {
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import { DatePicker, Form, Input, Switch } from "antd";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||||
|
import JobSearchSelect from "../job-search-select/job-search-select.component";
|
||||||
|
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
|
||||||
|
import InvoiceEnterModalLinesComponent from "../invoice-enter-modal/invoice-enter-modal.lines.component";
|
||||||
|
import DocumentsUploadContainer from "../documents-upload/documents-upload.container";
|
||||||
|
|
||||||
|
export default function InvoiceDetailEditComponent({
|
||||||
|
form,
|
||||||
|
roAutoCompleteOptions,
|
||||||
|
loadLines,
|
||||||
|
lineData,
|
||||||
|
responsibilityCenters,
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div style={{ display: "flex" }}>
|
||||||
|
<Form.Item
|
||||||
|
name="jobid"
|
||||||
|
label={t("invoices.fields.ro_number")}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t("general.validation.required"),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<JobSearchSelect
|
||||||
|
options={roAutoCompleteOptions}
|
||||||
|
onBlur={() => {
|
||||||
|
if (form.getFieldValue("jobid") !== null) {
|
||||||
|
//loadLines({ variables: { id: form.getFieldValue("jobid") } });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: "flex" }}>
|
||||||
|
<Form.Item
|
||||||
|
label={t("invoices.fields.invoice_number")}
|
||||||
|
name="invoice_number"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t("general.validation.required"),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("invoices.fields.date")}
|
||||||
|
name="date"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t("general.validation.required"),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<DatePicker />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("invoices.fields.is_credit_memo")}
|
||||||
|
name="is_credit_memo"
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("invoices.fields.total")}
|
||||||
|
name="total"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t("general.validation.required"),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<CurrencyInput />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
<InvoiceEnterModalLinesComponent
|
||||||
|
lineData={lineData}
|
||||||
|
discount={0.1}
|
||||||
|
form={form}
|
||||||
|
responsibilityCenters={responsibilityCenters}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Form.Item name="upload" label="Upload">
|
||||||
|
<DocumentsUploadContainer jobId={form.getFieldValue("jobid")} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
console.log(form.getFieldsValue());
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
a
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import {
|
||||||
|
QUERY_INVOICES_BY_VENDOR,
|
||||||
|
QUERY_INVOICE_BY_PK,
|
||||||
|
} from "../../graphql/invoices.queries";
|
||||||
|
import { useQuery, useLazyQuery } from "@apollo/react-hooks";
|
||||||
|
import queryString from "query-string";
|
||||||
|
import { useHistory, useLocation } from "react-router-dom";
|
||||||
|
import { Table, Input, Form } from "antd";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { alphaSort } from "../../utils/sorters";
|
||||||
|
import AlertComponent from "../alert/alert.component";
|
||||||
|
import { DateFormatter } from "../../utils/DateFormatter";
|
||||||
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
|
import InvoiceDetailEditComponent from "./invoice-detail-edit.component";
|
||||||
|
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||||
|
import moment from "moment";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { GET_JOB_LINES_TO_ENTER_INVOICE } from "../../graphql/jobs-lines.queries";
|
||||||
|
import { ACTIVE_JOBS_FOR_AUTOCOMPLETE } from "../../graphql/jobs.queries";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop,
|
||||||
|
});
|
||||||
|
|
||||||
|
export function InvoiceDetailEditContainer({ bodyshop }) {
|
||||||
|
const search = queryString.parse(useLocation().search);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
|
const { loading, error, data } = useQuery(QUERY_INVOICE_BY_PK, {
|
||||||
|
variables: { invoiceid: search.invoiceid },
|
||||||
|
skip: !!!search.invoiceid,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: RoAutoCompleteData } = useQuery(ACTIVE_JOBS_FOR_AUTOCOMPLETE, {
|
||||||
|
fetchPolicy: "network-only",
|
||||||
|
variables: { statuses: bodyshop.md_ro_statuses.open_statuses || ["Open"] },
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
loading: linesLoading,
|
||||||
|
data: lineData,
|
||||||
|
refetch: loadLines,
|
||||||
|
} = useQuery(GET_JOB_LINES_TO_ENTER_INVOICE, {
|
||||||
|
variables: { id: data && data.invoices_by_pk.jobid },
|
||||||
|
fetchPolicy: "network-only",
|
||||||
|
skip: !!!(data && data.invoices_by_pk.id),
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleFinish = (values) => {
|
||||||
|
console.log("values", values);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// if (data) {
|
||||||
|
// loadLines();
|
||||||
|
// if (lineData) //form.resetFields();
|
||||||
|
// }
|
||||||
|
}, [data, lineData]);
|
||||||
|
|
||||||
|
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||||
|
return (
|
||||||
|
<LoadingSkeleton loading={loading || linesLoading}>
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
onFinish={handleFinish}
|
||||||
|
initialValues={
|
||||||
|
data
|
||||||
|
? {
|
||||||
|
...data.invoices_by_pk,
|
||||||
|
// invoicelines: [],
|
||||||
|
date: data.invoices_by_pk
|
||||||
|
? moment(data.invoices_by_pk.date)
|
||||||
|
: null,
|
||||||
|
}
|
||||||
|
: { invoicelines: [] }
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<InvoiceDetailEditComponent
|
||||||
|
form={form}
|
||||||
|
roAutoCompleteOptions={RoAutoCompleteData && RoAutoCompleteData.jobs}
|
||||||
|
loadLines={loadLines}
|
||||||
|
lineData={lineData ? lineData.joblines : null}
|
||||||
|
responsibilityCenters={bodyshop.md_responsibility_centers || null}
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
|
</LoadingSkeleton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default connect(mapStateToProps, null)(InvoiceDetailEditContainer);
|
||||||
@@ -9,7 +9,7 @@ export default function InvoiceEnterModalLinesComponent({
|
|||||||
lineData,
|
lineData,
|
||||||
discount,
|
discount,
|
||||||
form,
|
form,
|
||||||
responsibilityCenters
|
responsibilityCenters,
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { setFieldsValue, getFieldsValue } = form;
|
const { setFieldsValue, getFieldsValue } = form;
|
||||||
@@ -25,14 +25,15 @@ export default function InvoiceEnterModalLinesComponent({
|
|||||||
acc + (value && value.actual_cost ? value.actual_cost : 0),
|
acc + (value && value.actual_cost ? value.actual_cost : 0),
|
||||||
0
|
0
|
||||||
)
|
)
|
||||||
: 0
|
: 0,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Form.List name="invoicelines" >
|
<Form.List name="invoicelines">
|
||||||
{(fields, { add, remove }) => {
|
{(fields, { add, remove }) => {
|
||||||
|
console.log("fields", fields);
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{fields.map((field, index) => (
|
{fields.map((field, index) => (
|
||||||
@@ -44,8 +45,8 @@ export default function InvoiceEnterModalLinesComponent({
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t("general.validation.required")
|
message: t("general.validation.required"),
|
||||||
}
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
@@ -55,7 +56,7 @@ export default function InvoiceEnterModalLinesComponent({
|
|||||||
onSelect={(value, opt) => {
|
onSelect={(value, opt) => {
|
||||||
setFieldsValue({
|
setFieldsValue({
|
||||||
invoicelines: getFieldsValue([
|
invoicelines: getFieldsValue([
|
||||||
"invoicelines"
|
"invoicelines",
|
||||||
]).invoicelines.map((item, idx) => {
|
]).invoicelines.map((item, idx) => {
|
||||||
if (idx === index) {
|
if (idx === index) {
|
||||||
return {
|
return {
|
||||||
@@ -71,11 +72,11 @@ export default function InvoiceEnterModalLinesComponent({
|
|||||||
? responsibilityCenters.defaults[
|
? responsibilityCenters.defaults[
|
||||||
opt.part_type
|
opt.part_type
|
||||||
] || null
|
] || null
|
||||||
: null
|
: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return item;
|
return item;
|
||||||
})
|
}),
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
showSearch
|
showSearch
|
||||||
@@ -88,7 +89,7 @@ export default function InvoiceEnterModalLinesComponent({
|
|||||||
{t("invoicelines.labels.other")}
|
{t("invoicelines.labels.other")}
|
||||||
</Select.Option>
|
</Select.Option>
|
||||||
{lineData
|
{lineData
|
||||||
? lineData.map(item => (
|
? lineData.map((item) => (
|
||||||
<Select.Option
|
<Select.Option
|
||||||
key={item.id}
|
key={item.id}
|
||||||
value={item.line_desc}
|
value={item.line_desc}
|
||||||
@@ -103,7 +104,7 @@ export default function InvoiceEnterModalLinesComponent({
|
|||||||
<Col span={4}>
|
<Col span={4}>
|
||||||
<Tag color="green">
|
<Tag color="green">
|
||||||
<CurrencyFormatter>
|
<CurrencyFormatter>
|
||||||
{item.act_price}
|
{item.act_price || 0}
|
||||||
</CurrencyFormatter>
|
</CurrencyFormatter>
|
||||||
</Tag>
|
</Tag>
|
||||||
</Col>
|
</Col>
|
||||||
@@ -113,26 +114,29 @@ export default function InvoiceEnterModalLinesComponent({
|
|||||||
: null}
|
: null}
|
||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{getFieldsValue("invoicelines").invoicelines[index] &&
|
|
||||||
getFieldsValue("invoicelines").invoicelines[index]
|
{
|
||||||
.joblinename &&
|
//TODO Will need to refactor this to use proper form components in the search select above.
|
||||||
!getFieldsValue("invoicelines").invoicelines[index]
|
getFieldsValue("invoicelines")[index] &&
|
||||||
.joblineid ? (
|
// getFieldsValue("invoicelines")[index].joblinename &&
|
||||||
<Form.Item
|
!getFieldsValue("invoicelines").invoicelines[index]
|
||||||
label={t("invoicelines.fields.line_desc")}
|
.joblineid ? (
|
||||||
key={`${index}line_desc`}
|
<Form.Item
|
||||||
name={[field.name, "line_desc"]}
|
label={t("invoicelines.fields.line_desc")}
|
||||||
rules={[
|
key={`${index}line_desc`}
|
||||||
{
|
name={[field.name, "line_desc"]}
|
||||||
required: !getFieldsValue("invoicelines")
|
rules={[
|
||||||
.invoicelines[index].joblineid,
|
{
|
||||||
message: t("general.validation.required")
|
required: !getFieldsValue("invoicelines")[index]
|
||||||
}
|
.joblineid,
|
||||||
]}
|
message: t("general.validation.required"),
|
||||||
>
|
},
|
||||||
<Input />
|
]}
|
||||||
</Form.Item>
|
>
|
||||||
) : null}
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("invoicelines.fields.actual")}
|
label={t("invoicelines.fields.actual")}
|
||||||
@@ -141,12 +145,12 @@ export default function InvoiceEnterModalLinesComponent({
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t("general.validation.required")
|
message: t("general.validation.required"),
|
||||||
}
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<CurrencyInput
|
<CurrencyInput
|
||||||
onBlur={e => {
|
onBlur={(e) => {
|
||||||
setFieldsValue({
|
setFieldsValue({
|
||||||
invoicelines: getFieldsValue(
|
invoicelines: getFieldsValue(
|
||||||
"invoicelines"
|
"invoicelines"
|
||||||
@@ -157,11 +161,11 @@ export default function InvoiceEnterModalLinesComponent({
|
|||||||
actual_cost: !!item.actual_cost
|
actual_cost: !!item.actual_cost
|
||||||
? item.actual_cost
|
? item.actual_cost
|
||||||
: parseFloat(e.target.value) *
|
: parseFloat(e.target.value) *
|
||||||
(1 - discount)
|
(1 - discount),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return item;
|
return item;
|
||||||
})
|
}),
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -173,8 +177,8 @@ export default function InvoiceEnterModalLinesComponent({
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t("general.validation.required")
|
message: t("general.validation.required"),
|
||||||
}
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<CurrencyInput onBlur={() => calculateTotals()} />
|
<CurrencyInput onBlur={() => calculateTotals()} />
|
||||||
@@ -186,12 +190,12 @@ export default function InvoiceEnterModalLinesComponent({
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t("general.validation.required")
|
message: t("general.validation.required"),
|
||||||
}
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Select style={{ width: "150px" }}>
|
<Select style={{ width: "150px" }}>
|
||||||
{responsibilityCenters.costs.map(item => (
|
{responsibilityCenters.costs.map((item) => (
|
||||||
<Select.Option key={item}>{item}</Select.Option>
|
<Select.Option key={item}>{item}</Select.Option>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { QUERY_INVOICES_BY_VENDOR } from "../../graphql/invoices.queries";
|
||||||
|
import { useQuery } from "@apollo/react-hooks";
|
||||||
|
import queryString from "query-string";
|
||||||
|
import { useHistory, useLocation } from "react-router-dom";
|
||||||
|
import { Table, Input } from "antd";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { alphaSort } from "../../utils/sorters";
|
||||||
|
import AlertComponent from "../alert/alert.component";
|
||||||
|
import { DateFormatter } from "../../utils/DateFormatter";
|
||||||
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
|
|
||||||
|
export default function InvoicesByVendorList() {
|
||||||
|
const search = queryString.parse(useLocation().search);
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const { loading, error, data } = useQuery(QUERY_INVOICES_BY_VENDOR, {
|
||||||
|
variables: { vendorId: search.vendorid },
|
||||||
|
skip: !!!search.vendorid,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const [state, setState] = useState({
|
||||||
|
sortedInfo: {},
|
||||||
|
search: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
|
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOnRowClick = (record) => {
|
||||||
|
if (record) {
|
||||||
|
if (record.id) {
|
||||||
|
search.invoiceid = record.id;
|
||||||
|
history.push({ search: queryString.stringify(search) });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
delete search.invoiceid;
|
||||||
|
history.push({ search: queryString.stringify(search) });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: t("invoices.fields.invoice_number"),
|
||||||
|
dataIndex: "invoice_number",
|
||||||
|
key: "invoice_number",
|
||||||
|
sorter: (a, b) => alphaSort(a.invoice_number, b.invoice_number),
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "invoice_number" &&
|
||||||
|
state.sortedInfo.order,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("invoices.fields.date"),
|
||||||
|
dataIndex: "date",
|
||||||
|
key: "date",
|
||||||
|
|
||||||
|
sorter: (a, b) => a.date - b.date,
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "date" && state.sortedInfo.order,
|
||||||
|
render: (text, record) => <DateFormatter>{record.date}</DateFormatter>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("invoices.fields.total"),
|
||||||
|
dataIndex: "total",
|
||||||
|
key: "total",
|
||||||
|
|
||||||
|
sorter: (a, b) => a.total - b.total,
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "total" && state.sortedInfo.order,
|
||||||
|
render: (text, record) => (
|
||||||
|
<CurrencyFormatter>{record.total}</CurrencyFormatter>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleSearch = (e) => {
|
||||||
|
setState({ ...state, search: e.target.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
const dataSource = state.search
|
||||||
|
? data.invoices.filter(
|
||||||
|
(i) =>
|
||||||
|
(i.invoice_number || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(state.search.toLowerCase()) ||
|
||||||
|
(i.amount || "").toString().includes(state.search)
|
||||||
|
)
|
||||||
|
: (data && data.invoices) || [];
|
||||||
|
|
||||||
|
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table
|
||||||
|
loading={loading}
|
||||||
|
title={() => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Input
|
||||||
|
value={state.search}
|
||||||
|
onChange={handleSearch}
|
||||||
|
placeholder={t("general.labels.search")}
|
||||||
|
allowClear
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
dataSource={dataSource}
|
||||||
|
size="small"
|
||||||
|
pagination={{ position: "top" }}
|
||||||
|
columns={columns}
|
||||||
|
rowKey="id"
|
||||||
|
onChange={handleTableChange}
|
||||||
|
rowSelection={{
|
||||||
|
onSelect: (record) => {
|
||||||
|
handleOnRowClick(record);
|
||||||
|
},
|
||||||
|
selectedRowKeys: [search.invoiceid],
|
||||||
|
type: "radio",
|
||||||
|
}}
|
||||||
|
onRow={(record, rowIndex) => {
|
||||||
|
return {
|
||||||
|
onClick: (event) => {
|
||||||
|
handleOnRowClick(record);
|
||||||
|
}, // click row
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { QUERY_ALL_VENDORS } from "../../graphql/vendors.queries";
|
||||||
|
import { useQuery } from "@apollo/react-hooks";
|
||||||
|
import queryString from "query-string";
|
||||||
|
import { useHistory, useLocation } from "react-router-dom";
|
||||||
|
import { Table, Input } from "antd";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { alphaSort } from "../../utils/sorters";
|
||||||
|
import AlertComponent from "../alert/alert.component";
|
||||||
|
|
||||||
|
export default function InvoicesVendorsList() {
|
||||||
|
const search = queryString.parse(useLocation().search);
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const { loading, error, data } = useQuery(QUERY_ALL_VENDORS);
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const [state, setState] = useState({
|
||||||
|
sortedInfo: {},
|
||||||
|
search: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
|
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: t("vendors.fields.name"),
|
||||||
|
dataIndex: "name",
|
||||||
|
key: "name",
|
||||||
|
sorter: (a, b) => alphaSort(a.name, b.name),
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "name" && state.sortedInfo.order,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("vendors.fields.cost_center"),
|
||||||
|
dataIndex: "cost_center",
|
||||||
|
key: "cost_center",
|
||||||
|
sorter: (a, b) => alphaSort(a.cost_center, b.cost_center),
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "cost_center" && state.sortedInfo.order,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("vendors.fields.city"),
|
||||||
|
dataIndex: "city",
|
||||||
|
key: "city",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleOnRowClick = (record) => {
|
||||||
|
if (record) {
|
||||||
|
delete search.invoiceid;
|
||||||
|
if (record.id) {
|
||||||
|
search.vendorid = record.id;
|
||||||
|
history.push({ search: queryString.stringify(search) });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
delete search.vendorid;
|
||||||
|
history.push({ search: queryString.stringify(search) });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearch = (e) => {
|
||||||
|
setState({ ...state, search: e.target.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||||
|
|
||||||
|
const dataSource = state.search
|
||||||
|
? data.vendors.filter(
|
||||||
|
(v) =>
|
||||||
|
(v.name || "").toLowerCase().includes(state.search.toLowerCase()) ||
|
||||||
|
(v.cost_center || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(state.search.toLowerCase()) ||
|
||||||
|
(v.city || "").toLowerCase().includes(state.search.toLowerCase())
|
||||||
|
)
|
||||||
|
: (data && data.vendors) || [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table
|
||||||
|
loading={loading}
|
||||||
|
title={() => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Input
|
||||||
|
value={state.search}
|
||||||
|
onChange={handleSearch}
|
||||||
|
placeholder={t("general.labels.search")}
|
||||||
|
allowClear
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
dataSource={dataSource}
|
||||||
|
size="small"
|
||||||
|
pagination={{ position: "top" }}
|
||||||
|
columns={columns}
|
||||||
|
rowKey="id"
|
||||||
|
onChange={handleTableChange}
|
||||||
|
rowSelection={{
|
||||||
|
onSelect: (record) => {
|
||||||
|
handleOnRowClick(record);
|
||||||
|
},
|
||||||
|
selectedRowKeys: [search.vendorid],
|
||||||
|
type: "radio",
|
||||||
|
}}
|
||||||
|
onRow={(record, rowIndex) => {
|
||||||
|
return {
|
||||||
|
onClick: (event) => {
|
||||||
|
handleOnRowClick(record);
|
||||||
|
}, // click row
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -18,15 +18,15 @@ const VendorSearchSelect = ({ value, onChange, options, onSelect }) => {
|
|||||||
showSearch
|
showSearch
|
||||||
value={option}
|
value={option}
|
||||||
style={{
|
style={{
|
||||||
width: 300
|
width: 300,
|
||||||
}}
|
}}
|
||||||
onChange={setOption}
|
onChange={setOption}
|
||||||
optionFilterProp="children"
|
optionFilterProp="name"
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
>
|
>
|
||||||
{options
|
{options
|
||||||
? options.map(o => (
|
? options.map((o) => (
|
||||||
<Option key={o.id} value={o.id} discount={o.discount}>
|
<Option key={o.id} value={o.id} name={o.name} discount={o.discount}>
|
||||||
<div style={{ display: "flex" }}>
|
<div style={{ display: "flex" }}>
|
||||||
{o.name}
|
{o.name}
|
||||||
<Tag color="green">{`${o.discount * 100}%`}</Tag>
|
<Tag color="green">{`${o.discount * 100}%`}</Tag>
|
||||||
|
|||||||
@@ -58,6 +58,24 @@ export const QUERY_INVOICES_BY_JOBID = gql`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const QUERY_INVOICES_BY_VENDOR = gql`
|
||||||
|
query QUERY_INVOICES_BY_VENDOR($vendorId: uuid!) {
|
||||||
|
invoices(
|
||||||
|
where: { vendorid: { _eq: $vendorId } }
|
||||||
|
order_by: { date: desc }
|
||||||
|
) {
|
||||||
|
id
|
||||||
|
job {
|
||||||
|
id
|
||||||
|
ro_number
|
||||||
|
}
|
||||||
|
total
|
||||||
|
invoice_number
|
||||||
|
date
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
export const QUERY_INVOICE_BY_PK = gql`
|
export const QUERY_INVOICE_BY_PK = gql`
|
||||||
query QUERY_INVOICE_BY_PK($invoiceid: uuid!) {
|
query QUERY_INVOICE_BY_PK($invoiceid: uuid!) {
|
||||||
invoices_by_pk(id: $invoiceid) {
|
invoices_by_pk(id: $invoiceid) {
|
||||||
@@ -72,6 +90,7 @@ export const QUERY_INVOICE_BY_PK = gql`
|
|||||||
total
|
total
|
||||||
updated_at
|
updated_at
|
||||||
vendor {
|
vendor {
|
||||||
|
id
|
||||||
name
|
name
|
||||||
discount
|
discount
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export const UPDATE_VENDOR = gql`
|
|||||||
|
|
||||||
export const QUERY_ALL_VENDORS = gql`
|
export const QUERY_ALL_VENDORS = gql`
|
||||||
query QUERY_ALL_VENDORS {
|
query QUERY_ALL_VENDORS {
|
||||||
vendors {
|
vendors(order_by: { name: asc }) {
|
||||||
name
|
name
|
||||||
id
|
id
|
||||||
street1
|
street1
|
||||||
|
|||||||
@@ -1,188 +0,0 @@
|
|||||||
import { Button, Descriptions, Table } from "antd";
|
|
||||||
import React, { useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
|
||||||
import { DateFormatter } from "../../utils/DateFormatter";
|
|
||||||
import { alphaSort } from "../../utils/sorters";
|
|
||||||
|
|
||||||
export default function InvoicesPageComponent({
|
|
||||||
loading,
|
|
||||||
invoices,
|
|
||||||
selectedInvoice,
|
|
||||||
handleFetchMore,
|
|
||||||
handleOnRowClick,
|
|
||||||
}) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const [state, setState] = useState({
|
|
||||||
sortedInfo: {},
|
|
||||||
});
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
title: t("invoices.fields.vendorname"),
|
|
||||||
dataIndex: "vendorname",
|
|
||||||
key: "vendorname",
|
|
||||||
sorter: (a, b) => alphaSort(a.vendor.name, b.vendor.name),
|
|
||||||
sortOrder:
|
|
||||||
state.sortedInfo.columnKey === "vendorname" && state.sortedInfo.order,
|
|
||||||
render: (text, record) => <span>{record.vendor.name}</span>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("invoices.fields.invoice_number"),
|
|
||||||
dataIndex: "invoice_number",
|
|
||||||
key: "invoice_number",
|
|
||||||
sorter: (a, b) => alphaSort(a.invoice_number, b.invoice_number),
|
|
||||||
sortOrder:
|
|
||||||
state.sortedInfo.columnKey === "invoice_number" &&
|
|
||||||
state.sortedInfo.order,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("invoices.fields.date"),
|
|
||||||
dataIndex: "date",
|
|
||||||
key: "date",
|
|
||||||
|
|
||||||
sorter: (a, b) => a.date - b.date,
|
|
||||||
sortOrder:
|
|
||||||
state.sortedInfo.columnKey === "date" && state.sortedInfo.order,
|
|
||||||
render: (text, record) => <DateFormatter>{record.date}</DateFormatter>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("invoices.fields.total"),
|
|
||||||
dataIndex: "total",
|
|
||||||
key: "total",
|
|
||||||
|
|
||||||
sorter: (a, b) => a.total - b.total,
|
|
||||||
sortOrder:
|
|
||||||
state.sortedInfo.columnKey === "total" && state.sortedInfo.order,
|
|
||||||
render: (text, record) => (
|
|
||||||
<CurrencyFormatter>{record.total}</CurrencyFormatter>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("general.labels.actions"),
|
|
||||||
dataIndex: "actions",
|
|
||||||
key: "actions",
|
|
||||||
render: (text, record) => (
|
|
||||||
<Link to={`/manage/invoices/${record.id}`}>
|
|
||||||
<Button>{t("invoices.actions.edit")}</Button>
|
|
||||||
</Link>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const handleTableChange = (pagination, filters, sorter) => {
|
|
||||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
|
||||||
};
|
|
||||||
|
|
||||||
const rowExpander = (record) => {
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
title: t("invoicelines.fields.line_desc"),
|
|
||||||
dataIndex: "line_desc",
|
|
||||||
key: "line_desc",
|
|
||||||
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
|
|
||||||
sortOrder:
|
|
||||||
state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("invoicelines.fields.retail"),
|
|
||||||
dataIndex: "actual_price",
|
|
||||||
key: "actual_price",
|
|
||||||
sorter: (a, b) => a.actual_price - b.actual_price,
|
|
||||||
sortOrder:
|
|
||||||
state.sortedInfo.columnKey === "actual_price" &&
|
|
||||||
state.sortedInfo.order,
|
|
||||||
render: (text, record) => (
|
|
||||||
<CurrencyFormatter>{record.actual_price}</CurrencyFormatter>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("invoicelines.fields.actual_cost"),
|
|
||||||
dataIndex: "actual_cost",
|
|
||||||
key: "actual_cost",
|
|
||||||
sorter: (a, b) => a.actual_cost - b.actual_cost,
|
|
||||||
sortOrder:
|
|
||||||
state.sortedInfo.columnKey === "actual_cost" &&
|
|
||||||
state.sortedInfo.order,
|
|
||||||
render: (text, record) => (
|
|
||||||
<CurrencyFormatter>{record.actual_cost}</CurrencyFormatter>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("invoicelines.fields.cost_center"),
|
|
||||||
dataIndex: "cost_center",
|
|
||||||
key: "cost_center",
|
|
||||||
sorter: (a, b) => alphaSort(a.cost_center, b.cost_center),
|
|
||||||
sortOrder:
|
|
||||||
state.sortedInfo.columnKey === "cost_center" &&
|
|
||||||
state.sortedInfo.order,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Descriptions title="User Info">
|
|
||||||
<Descriptions.Item label="UserName">Zhou Maomao</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="Telephone">1810000000</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="Live">Hangzhou, Zhejiang</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="Remark">empty</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="Address">
|
|
||||||
No. 18, Wantang Road, Xihu District, Hangzhou, Zhejiang, China
|
|
||||||
</Descriptions.Item>
|
|
||||||
</Descriptions>
|
|
||||||
<Table
|
|
||||||
size="small"
|
|
||||||
pagination={{ position: "top", defaultPageSize: 25 }}
|
|
||||||
columns={columns.map((item) => ({ ...item }))}
|
|
||||||
rowKey="id"
|
|
||||||
dataSource={record.invoicelines}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Table
|
|
||||||
loading={loading}
|
|
||||||
size="small"
|
|
||||||
expandedRowRender={rowExpander}
|
|
||||||
pagination={{
|
|
||||||
position: "top",
|
|
||||||
defaultPageSize: 1,
|
|
||||||
onChange: (page, pageSize) => {
|
|
||||||
handleOnRowClick(page * pageSize);
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
columns={columns.map((item) => ({ ...item }))}
|
|
||||||
rowKey="id"
|
|
||||||
dataSource={invoices}
|
|
||||||
onChange={handleTableChange}
|
|
||||||
expandable={{
|
|
||||||
expandedRowKeys: [selectedInvoice],
|
|
||||||
onExpand: (expanded, record) => {
|
|
||||||
handleOnRowClick(expanded ? record : null);
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
rowSelection={{
|
|
||||||
onSelect: (record) => {
|
|
||||||
handleOnRowClick(record);
|
|
||||||
},
|
|
||||||
selectedRowKeys: [selectedInvoice],
|
|
||||||
type: "radio",
|
|
||||||
}}
|
|
||||||
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
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,57 +1,23 @@
|
|||||||
import { useQuery } from "@apollo/react-hooks";
|
import { Col, Row } from "antd";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { QUERY_ALL_INVOICES_PAGINATED } from "../../graphql/invoices.queries";
|
import InvoicesByVendorList from "../../components/invoices-by-vendor-list/invoices-by-vendor-list.component";
|
||||||
import InvoicesPageComponent from "./invoices.page.component";
|
import VendorsList from "../../components/invoices-vendors-list/invoices-vendors-list.component";
|
||||||
import AlertComponent from "../../components/alert/alert.component";
|
import InvoiceDetailEditContainer from "../../components/invoice-detail-edit/invoice-detail-edit.container";
|
||||||
import queryString from "query-string";
|
|
||||||
import { useHistory, useLocation } from "react-router-dom";
|
|
||||||
|
|
||||||
export default function InvoicesPageContainer() {
|
export default function InvoicesPageContainer() {
|
||||||
const { loading, error, data, fetchMore } = useQuery(
|
|
||||||
QUERY_ALL_INVOICES_PAGINATED,
|
|
||||||
{
|
|
||||||
variables: { offset: 0, limit: 1 },
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const search = queryString.parse(useLocation().search);
|
|
||||||
const history = useHistory();
|
|
||||||
|
|
||||||
const handleOnRowClick = (record) => {
|
|
||||||
if (record) {
|
|
||||||
if (record.id) {
|
|
||||||
search.invoiceid = record.id;
|
|
||||||
history.push({ search: queryString.stringify(search) });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
delete search.invoiceid;
|
|
||||||
history.push({ search: queryString.stringify(search) });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFetchMore = (offset) => {
|
|
||||||
fetchMore({
|
|
||||||
variables: {
|
|
||||||
offset: offset,
|
|
||||||
},
|
|
||||||
updateQuery: (prev, { fetchMoreResult }) => {
|
|
||||||
if (!fetchMoreResult) return prev;
|
|
||||||
return Object.assign({}, prev, {
|
|
||||||
invoices: [...prev.invoices, ...fetchMoreResult.invoices],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<Row>
|
||||||
<InvoicesPageComponent
|
<Col span={8}>
|
||||||
loading={loading}
|
<VendorsList />
|
||||||
invoices={data ? data.invoices : null}
|
</Col>
|
||||||
selectedInvoice={search.invoiceid}
|
<Col span={16}>
|
||||||
handleFetchMore={handleFetchMore}
|
<Row>
|
||||||
handleOnRowClick={handleOnRowClick}
|
<InvoicesByVendorList />
|
||||||
/>
|
</Row>
|
||||||
</div>
|
<Row>
|
||||||
|
<InvoiceDetailEditContainer />
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,8 +60,6 @@ exports.receive = (req, res) => {
|
|||||||
Object.keys(i).map((k) => allTokens.push(k))
|
Object.keys(i).map((k) => allTokens.push(k))
|
||||||
);
|
);
|
||||||
const uniqueTokens = [...new Set(allTokens)];
|
const uniqueTokens = [...new Set(allTokens)];
|
||||||
console.log("exports.receive -> uniqueTokens", uniqueTokens);
|
|
||||||
|
|
||||||
var message = {
|
var message = {
|
||||||
notification: {
|
notification: {
|
||||||
title: `New SMS From ${phone(req.body.From)[0]}`,
|
title: `New SMS From ${phone(req.body.From)[0]}`,
|
||||||
@@ -80,7 +78,10 @@ exports.receive = (req, res) => {
|
|||||||
.sendMulticast(message)
|
.sendMulticast(message)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
// Response is a message ID string.
|
// Response is a message ID string.
|
||||||
console.log("Successfully sent message:", response);
|
console.log(
|
||||||
|
"Successfully sent message:",
|
||||||
|
JSON.stringify(response)
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.log("Error sending message:", error);
|
console.log("Error sending message:", error);
|
||||||
|
|||||||
Reference in New Issue
Block a user