Further UI Improvements

This commit is contained in:
Patrick Fic
2021-03-26 17:23:16 -07:00
parent 6c47918542
commit 17264ff7d6
26 changed files with 993 additions and 815 deletions

View File

@@ -19723,6 +19723,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>estimatelines</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>existing_jobs</name>
<definition_loaded>false</definition_loaded>

View File

@@ -67,11 +67,6 @@
// background-color: #188fff;
// }
.ant-table-cell {
// background-color: red;
//padding: 0.2rem !important;
}
.ant-input-number-input,
.ant-input-number,
.ant-picker-input,

View File

@@ -7,6 +7,7 @@ export default function DataLabel({
children,
vertical,
visible = true,
valueStyle = {},
...props
}) {
if (!visible || (hideIfNull && !!!children)) return null;
@@ -30,7 +31,7 @@ export default function DataLabel({
}}
>
{typeof children === "string" ? (
<Typography.Text>{children}</Typography.Text>
<Typography.Text style={valueStyle}>{children}</Typography.Text>
) : (
children
)}

View File

@@ -11,7 +11,7 @@ export default function FormsFieldChanged({ form }) {
form.resetFields();
};
const loc = useLocation();
if (!form.isFieldsTouched()) return <></>;
return (
<Form.Item shouldUpdate style={{ margin: 0, padding: 0 }}>
{() => {

View File

@@ -1,6 +1,6 @@
import { DeleteFilled, FilterFilled, SyncOutlined } from "@ant-design/icons";
import { useMutation } from "@apollo/client";
import { Button, Dropdown, Input, Menu, Space, Table } from "antd";
import { Button, Dropdown, Input, Menu, PageHeader, Space, Table } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -39,35 +39,13 @@ export function JobLinesComponent({
refetch,
jobLines,
setSearchText,
selectedLines,
setSelectedLines,
job,
setJobLineEditContext,
form,
}) {
const [deleteJobLine] = useMutation(DELETE_JOB_LINE_BY_PK);
// const {
// loading: billLinesLoading,
// error: billLinesError,
// data: billLinesData,
// } = useQuery(QUERY_BILLS_BY_JOB_REF, {
// variables: { jobId: job && job.id },
// skip: loading || !job,
// });
// const billLinesDataObj = useMemo(() => {
// if (!billLinesData) return {};
// const ret = {};
// billLinesData.billlines.map((b) => {
// if (b.joblineid) {
// ret[b.joblineid] = { ...b, total: b.actual_price * b.quantity };
// }
// return null;
// });
// return ret;
// }, [billLinesData]);
const [selectedLines, setSelectedLines] = useState([]);
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: {},
@@ -80,6 +58,7 @@ export function JobLinesComponent({
dataIndex: "line_no",
key: "line_no",
sorter: (a, b) => a.line_no - b.line_no,
fixed: "left",
sortOrder:
state.sortedInfo.columnKey === "line_no" && state.sortedInfo.order,
},
@@ -87,13 +66,16 @@ export function JobLinesComponent({
title: t("joblines.fields.line_ind"),
dataIndex: "line_ind",
key: "line_ind",
fixed: "left",
sorter: (a, b) => alphaSort(a.line_ind, b.line_ind),
sortOrder:
state.sortedInfo.columnKey === "line_ind" && state.sortedInfo.order,
responsive: ["md"],
},
{
title: t("joblines.fields.line_desc"),
dataIndex: "line_desc",
fixed: "left",
key: "line_desc",
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
sortOrder:
@@ -231,6 +213,7 @@ export function JobLinesComponent({
dataIndex: "billref",
key: "billref",
render: (text, record) => <JobLinesBillRefernece jobline={record} />,
responsive: ["md"],
},
{
title: t("joblines.fields.status"),
@@ -258,35 +241,6 @@ export function JobLinesComponent({
<JobLineStatusPopup jobline={record} disabled={jobRO} />
),
},
// {
// title: t("allocations.fields.employee"),
// dataIndex: "employee",
// key: "employee",
// sorter: (a, b) =>
// alphaSort(
// a.allocations[0] &&
// a.allocations[0].employee.first_name +
// a.allocations[0].employee.last_name,
// b.allocations[0] &&
// b.allocations[0].employee.first_name +
// b.allocations[0].employee.last_name
// ),
// sortOrder:
// state.sortedInfo.columnKey === "employee" && state.sortedInfo.order,
// render: (text, record) => (
// <span>
// {record.allocations && record.allocations.length > 0
// ? record.allocations.map((item) => (
// <AllocationsEmployeeLabelContainer
// key={item.id}
// refetch={refetch}
// allocation={item}
// />
// ))
// : null}
// </span>
// ),
// },
{
title: t("general.labels.actions"),
dataIndex: "actions",
@@ -330,14 +284,6 @@ export function JobLinesComponent({
</Button>
</Space>
)}
{
// <AllocationsAssignmentContainer
// key={record.id}
// refetch={refetch}
// jobLineId={record.id}
// hours={record.mod_lb_hrs}
// />
}
</div>
),
},
@@ -366,109 +312,82 @@ export function JobLinesComponent({
return (
<div>
<PartsOrderModalContainer />
<PageHeader
title={t("jobs.labels.estimatelines")}
extra={
<Space wrap>
<Button onClick={() => refetch()}>
<SyncOutlined />
</Button>
<Button
disabled={
(job && !job.converted) ||
(selectedLines.length > 0 ? false : true) ||
jobRO
}
onClick={() => {
setPartsOrderContext({
actions: { refetch: refetch },
context: {
jobId: job.id,
linesToOrder: selectedLines,
},
});
//Clear out the selected lines. IO-785
setSelectedLines([]);
}}
>
{t("parts.actions.order")}
</Button>
<Button
onClick={() => {
setState({
...state,
filteredInfo: {
part_type: ["PAN,PAL,PAA,PAS,PASL"],
},
});
}}
>
<FilterFilled /> {t("jobs.actions.filterpartsonly")}
</Button>
<Dropdown overlay={markMenu} trigger={["click"]}>
<Button>{t("jobs.actions.mark")}</Button>
</Dropdown>
<Button
disabled={jobRO}
onClick={() => {
setJobLineEditContext({
actions: { refetch: refetch },
context: { jobid: job.id },
});
}}
>
{t("joblines.actions.new")}
</Button>
<Input.Search
placeholder={t("general.labels.search")}
onChange={(e) => {
e.preventDefault();
setSearchText(e.target.value);
}}
/>
</Space>
}
/>
<Table
columns={columns}
rowKey="id"
loading={loading}
size="small"
pagination={{ position: "top", defaultPageSize: 50 }}
dataSource={jobLines}
onChange={handleTableChange}
scroll={{
x: true,
//y: "40rem"
}}
title={() => {
return (
<div className="imex-table-header">
<Button onClick={() => refetch()}>
<SyncOutlined />
</Button>
<Button
disabled={
(job && !job.converted) ||
(selectedLines.length > 0 ? false : true) ||
jobRO
}
onClick={() => {
setPartsOrderContext({
actions: { refetch: refetch },
context: {
jobId: job.id,
linesToOrder: selectedLines,
},
});
//Clear out the selected lines. IO-785
setSelectedLines([]);
}}
>
{t("parts.actions.order")}
</Button>
<Button
onClick={() => {
setState({
...state,
filteredInfo: {
part_type: ["PAN,PAL,PAA,PAS,PASL"],
},
});
}}
>
<FilterFilled /> {t("jobs.actions.filterpartsonly")}
</Button>
<Dropdown overlay={markMenu} trigger={["click"]}>
<Button>{t("jobs.actions.mark")}</Button>
</Dropdown>
{
// <AllocationsBulkAssignmentContainer
// jobLines={selectedLines}
// refetch={refetch}
// />
}
<Button
disabled={jobRO}
onClick={() => {
setJobLineEditContext({
actions: { refetch: refetch },
context: { jobid: job.id },
});
}}
>
{t("joblines.actions.new")}
</Button>
<div className="imex-table-header__search">
<Input.Search
placeholder={t("general.labels.search")}
onChange={(e) => {
e.preventDefault();
setSearchText(e.target.value);
}}
/>
</div>
</div>
);
}}
// expandedRowRender={(record) => (
// <div>
// <strong>{t("parts_orders.labels.orderhistory")}</strong>
// {record.parts_order_lines.map((item) => (
// <div key={item.id}>
// <Link
// to={`/manage/jobs/${job.id}?tab=partssublet&partsorderid=${item.parts_order.id}`}
// >
// {item.parts_order.order_number || ""}
// </Link>
// -
// <Link to={`/manage/shop/vendors/${item.parts_order.vendor.id}`}>
// {item.parts_order.vendor.name || ""}
// </Link>
// {` on ${item.parts_order.order_date || ""}`}
// </div>
// ))}
// </div>
// )}
rowClassName="table-small-margin"
rowSelection={{
selectedRowKeys: selectedLines.map((item) => item.id),
onSelectAll: (selected, selectedRows, changeRows) => {

View File

@@ -1,43 +1,43 @@
import React, { useState } from "react";
import React, { useMemo, useState } from "react";
import JobLinesComponent from "./job-lines.component";
function JobLinesContainer({ job, joblines, refetch, form }) {
function JobLinesContainer({ job, joblines, refetch, form, ...rest }) {
const [searchText, setSearchText] = useState("");
const [selectedLines, setSelectedLines] = useState([]);
const jobLines = joblines
? searchText
? joblines.filter(
(jl) =>
(jl.unq_seq || "")
.toString()
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(jl.line_desc || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(jl.part_type || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(jl.oem_partno || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(jl.op_code_desc || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(jl.db_price || "").toString().includes(searchText.toLowerCase()) ||
(jl.act_price || "").toString().includes(searchText.toLowerCase())
)
: joblines
: null;
const jobLines = useMemo(() => {
return joblines
? searchText
? joblines.filter(
(jl) =>
(jl.unq_seq || "")
.toString()
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(jl.line_desc || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(jl.part_type || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(jl.oem_partno || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(jl.op_code_desc || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(jl.db_price || "")
.toString()
.includes(searchText.toLowerCase()) ||
(jl.act_price || "").toString().includes(searchText.toLowerCase())
)
: joblines
: [];
}, [joblines, searchText]);
return (
<JobLinesComponent
refetch={refetch}
jobLines={jobLines}
setSearchText={setSearchText}
selectedLines={selectedLines}
setSelectedLines={setSelectedLines}
job={job}
form={form}
/>

View File

@@ -1,12 +1,13 @@
import { DeleteFilled, PlusCircleFilled } from "@ant-design/icons";
import { Button, Popover, Select, Spin } from "antd";
import React, { useState } from "react";
import DataLabel from "../data-label/data-label.component";
import { useTranslation } from "react-i18next";
import { PlusCircleFilled, MinusOutlined } from "@ant-design/icons";
import { Select, Button, Popover } from "antd";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import DataLabel from "../data-label/data-label.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
jobRO: selectJobReadOnly,
@@ -15,6 +16,8 @@ const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
const iconStyle = { marginLeft: ".3rem" };
export function JobEmployeeAssignments({
bodyshop,
jobRO,
@@ -23,6 +26,7 @@ export function JobEmployeeAssignments({
prep,
handleAdd,
handleRemove,
loading,
}) {
const { t } = useTranslation();
const [assignment, setAssignment] = useState({
@@ -68,93 +72,84 @@ export function JobEmployeeAssignments({
);
return (
<div>
<Popover destroyTooltipOnHide content={popContent} visible={visibility}>
<div style={{ display: "flex" }}>
<DataLabel
label={t("jobs.fields.employee_body")}
style={{ margin: "0rem .5rem" }}
>
{body ? (
<div>
<span>{`${body.first_name || ""} ${
body.last_name || ""
}`}</span>
<MinusOutlined
operation="body"
disabled={jobRO}
onClick={() => !jobRO && handleRemove("body")}
/>
</div>
) : (
<PlusCircleFilled
<Popover destroyTooltipOnHide content={popContent} visible={visibility}>
<Spin spinning={loading}>
<DataLabel label={t("jobs.fields.employee_body")}>
{body ? (
<div>
<span>{`${body.first_name || ""} ${body.last_name || ""}`}</span>
<DeleteFilled
operation="body"
disabled={jobRO}
onClick={() => {
if (!jobRO) {
setAssignment({ operation: "body" });
setVisibility(true);
}
}}
style={iconStyle}
onClick={() => !jobRO && handleRemove("body")}
/>
)}
</DataLabel>
<DataLabel
label={t("jobs.fields.employee_prep")}
style={{ margin: "0rem .5rem" }}
>
{prep ? (
<div>
<span>{`${prep.first_name || ""} ${
prep.last_name || ""
}`}</span>
<MinusOutlined
disabled={jobRO}
operation="prep"
onClick={() => !jobRO && handleRemove("prep")}
/>
</div>
) : (
<PlusCircleFilled
</div>
) : (
<PlusCircleFilled
disabled={jobRO}
style={iconStyle}
onClick={() => {
if (!jobRO) {
setAssignment({ operation: "body" });
setVisibility(true);
}
}}
/>
)}
</DataLabel>
<DataLabel label={t("jobs.fields.employee_prep")}>
{prep ? (
<div>
<span>{`${prep.first_name || ""} ${prep.last_name || ""}`}</span>
<DeleteFilled
disabled={jobRO}
onClick={() => {
if (!jobRO) {
setAssignment({ operation: "prep" });
setVisibility(true);
}
}}
style={iconStyle}
operation="prep"
onClick={() => !jobRO && handleRemove("prep")}
/>
)}
</DataLabel>
<DataLabel
label={t("jobs.fields.employee_refinish")}
style={{ margin: "0rem .5rem" }}
>
{refinish ? (
<div>
<span>{`${refinish.first_name || ""} ${
refinish.last_name || ""
}`}</span>
<MinusOutlined
disabled={jobRO}
operation="refinish"
onClick={() => !jobRO && handleRemove("refinish")}
/>
</div>
) : (
<PlusCircleFilled
</div>
) : (
<PlusCircleFilled
disabled={jobRO}
style={iconStyle}
onClick={() => {
if (!jobRO) {
setAssignment({ operation: "prep" });
setVisibility(true);
}
}}
/>
)}
</DataLabel>
<DataLabel label={t("jobs.fields.employee_refinish")}>
{refinish ? (
<div>
<span>{`${refinish.first_name || ""} ${
refinish.last_name || ""
}`}</span>
<DeleteFilled
disabled={jobRO}
onClick={() => {
if (!jobRO) {
setAssignment({ operation: "refinish" });
setVisibility(true);
}
}}
style={iconStyle}
operation="refinish"
onClick={() => !jobRO && handleRemove("refinish")}
/>
)}
</DataLabel>
</div>
</Popover>
</div>
</div>
) : (
<PlusCircleFilled
disabled={jobRO}
style={iconStyle}
onClick={() => {
if (!jobRO) {
setAssignment({ operation: "refinish" });
setVisibility(true);
}
}}
/>
)}
</DataLabel>
</Spin>
</Popover>
);
}
export default connect(

View File

@@ -1,16 +1,18 @@
import { useMutation } from "@apollo/client";
import { notification } from "antd";
import React from "react";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { UPDATE_JOB } from "../../graphql/jobs.queries";
import JobEmployeeAssignmentsComponent from "./job-employee-assignments.component";
import { logImEXEvent } from "../../firebase/firebase.utils";
export default function JobEmployeeAssignmentsContainer({ job, refetch }) {
const { t } = useTranslation();
const [updateJob] = useMutation(UPDATE_JOB);
const [loading, setLoading] = useState(false);
const handleAdd = async (assignment) => {
setLoading(true);
const { operation, employeeid } = assignment;
logImEXEvent("job_assign_employee", { operation });
@@ -30,8 +32,10 @@ export default function JobEmployeeAssignmentsContainer({ job, refetch }) {
}),
});
}
setLoading(false);
};
const handleRemove = async (operation) => {
setLoading(true);
logImEXEvent("job_unassign_employee", { operation });
let empAssignment = determineFieldName(operation);
@@ -48,6 +52,7 @@ export default function JobEmployeeAssignmentsContainer({ job, refetch }) {
}),
});
}
setLoading(false);
};
return (
@@ -58,6 +63,7 @@ export default function JobEmployeeAssignmentsContainer({ job, refetch }) {
prep={job.employee_prep_rel}
handleAdd={handleAdd}
handleRemove={handleRemove}
loading={loading}
/>
</div>
);

View File

@@ -0,0 +1,205 @@
import { Button, Card, Space, Table } from "antd";
import Dinero from "dinero.js";
import React, { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import { alphaSort } from "../../utils/sorters";
import { TemplateList } from "../../utils/TemplateConstants";
import DataLabel from "../data-label/data-label.component";
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
jobRO: selectJobReadOnly,
});
const mapDispatchToProps = (dispatch) => ({
setPaymentContext: (context) =>
dispatch(setModalContext({ context: context, modal: "payment" })),
});
const stripeTestEnv = process.env.REACT_APP_STRIPE_PUBLIC_KEY; //.includes("test");
export function JobPayments({
job,
jobRO,
bodyshop,
setPaymentContext,
refetch,
}) {
const { t } = useTranslation();
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: {},
});
const columns = [
{
title: t("payments.fields.created_at"),
dataIndex: "created_at",
key: "created_at",
sortOrder:
state.sortedInfo.columnKey === "created_at" && state.sortedInfo.order,
render: (text, record) => (
<DateTimeFormatter>{record.created_at}</DateTimeFormatter>
),
},
{
title: t("payments.fields.payer"),
dataIndex: "payer",
key: "payer",
sorter: (a, b) => alphaSort(a.payer, b.payer),
sortOrder:
state.sortedInfo.columnKey === "payer" && state.sortedInfo.order,
},
{
title: t("payments.fields.amount"),
dataIndex: "amount",
key: "amount",
sorter: (a, b) => a.amount - b.amount,
sortOrder:
state.sortedInfo.columnKey === "amount" && state.sortedInfo.order,
render: (text, record) => (
<CurrencyFormatter>{record.amount}</CurrencyFormatter>
),
},
{
title: t("payments.fields.memo"),
dataIndex: "memo",
key: "memo",
sorter: (a, b) => alphaSort(a.memo, b.memo),
sortOrder:
state.sortedInfo.columnKey === "memo" && state.sortedInfo.order,
},
{
title: t("payments.fields.type"),
dataIndex: "type",
key: "type",
sorter: (a, b) => alphaSort(a.type, b.type),
sortOrder:
state.sortedInfo.columnKey === "type" && state.sortedInfo.order,
},
{
title: t("payments.fields.transactionid"),
dataIndex: "transactionid",
key: "transactionid",
sorter: (a, b) => alphaSort(a.transactionid, b.transactionid),
sortOrder:
state.sortedInfo.columnKey === "transactionid" &&
state.sortedInfo.order,
},
{
title: t("payments.fields.stripeid"),
dataIndex: "stripeid",
key: "stripeid",
render: (text, record) =>
record.stripeid ? (
<a
href={
stripeTestEnv
? `https://dashboard.stripe.com/${bodyshop.stripe_acct_id}/test/payments/${record.stripeid}`
: `https://dashboard.stripe.com/${bodyshop.stripe_acct_id}/payments/${record.stripeid}`
}
>
{record.stripeid}
</a>
) : null,
},
{
title: t("general.labels.actions"),
dataIndex: "actions",
key: "actions",
render: (text, record) => (
<PrintWrapperComponent
templateObject={{
name: TemplateList("payment").payment_receipt.key,
variables: { id: record.id },
}}
messageObject={{
to: job.ownr_ea,
}}
/>
),
},
];
const total = useMemo(() => {
return (
job.payments &&
job.payments.reduce((acc, val) => {
acc = acc.add(Dinero({ amount: Math.round(val.amount * 100) }));
return acc;
}, Dinero())
);
}, [job.payments]);
const balance = useMemo(() => {
if (job && job.job_totals && job.job_totals.totals.total_repairs)
return Dinero(job.job_totals.totals.total_repairs).subtract(total);
return Dinero({ amount: 0 }).subtract(total);
}, [job, total]);
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
return (
<Card
title={t("payments.labels.title")}
extra={
<Space wrap>
<Button
disabled={jobRO}
onClick={() =>
setPaymentContext({
actions: { refetch: refetch },
context: { jobid: job.id },
})
}
>
{t("menus.header.enterpayment")}
</Button>
<DataLabel
valueStyle={{ color: balance.getAmount() !== 0 ? "red" : "green" }}
label={t("payments.labels.balance")}
>
{balance.toFormat()}
</DataLabel>
</Space>
}
>
<Table
columns={columns}
rowKey="id"
pagination={false}
onChange={handleTableChange}
dataSource={job && job.payments}
scroll={{
x: true,
}}
summary={() => (
<>
<Table.Summary.Row>
<Table.Summary.Cell>
<strong>{t("payments.labels.totalpayments")}</strong>
</Table.Summary.Cell>
<Table.Summary.Cell />
<Table.Summary.Cell>
<strong>{total.toFormat()}</strong>
</Table.Summary.Cell>
<Table.Summary.Cell />
<Table.Summary.Cell />
<Table.Summary.Cell />
</Table.Summary.Row>
</>
)}
/>
</Card>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(JobPayments);

View File

@@ -1,5 +1,4 @@
import { Col, Collapse, Result, Row, Typography } from "antd";
import Dinero from "dinero.js";
import { Card, Col, Collapse, Result, Row } from "antd";
//import { JsonEditor as Editor } from "jsoneditor-react";
//import "jsoneditor-react/es/editor.min.css";
import React from "react";
@@ -7,14 +6,16 @@ import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import JobCalculateTotals from "../job-calculate-totals/job-calculate-totals.component";
import "./job-totals-table.styles.scss";
import JobTotalsTableLabor from "./job-totals.table.labor.component";
import JobTotalsTableOther from "./job-totals.table.other.component";
import JobTotalsTableParts from "./job-totals.table.parts.component";
import JobTotalsTableTotals from "./job-totals.table.totals.component";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
jobRO: selectJobReadOnly,
bodyshop: selectBodyshop,
});
const colSpan = {
@@ -22,15 +23,17 @@ const colSpan = {
lg: { span: 12 },
};
export function JobsTotalsTableComponent({ bodyshop, jobRO, job }) {
export function JobsTotalsTableComponent({ jobRO, job }) {
const { t } = useTranslation();
if (!!!job.job_totals) {
return (
<Result
title={t("jobs.errors.nofinancial")}
extra={<JobCalculateTotals job={job} disabled={jobRO} />}
/>
<Card>
<Result
title={t("jobs.errors.nofinancial")}
extra={<JobCalculateTotals job={job} disabled={jobRO} />}
/>
</Card>
);
}
@@ -38,334 +41,50 @@ export function JobsTotalsTableComponent({ bodyshop, jobRO, job }) {
<div>
<Row gutter={[32, 32]}>
<Col {...colSpan}>
<div className="job-totals-half">
<Typography.Title level={4}>
{t("jobs.labels.labortotals")}
</Typography.Title>
<table>
<tbody>
<tr>
<td>{t("jobs.fields.rate_laa")}</td>
<td className="currency">
{Dinero(job.job_totals.rates.laa.total).toFormat()}
</td>
<td>{`(${job.job_totals.rates.laa.hours.toFixed(2)} @ ${
job.job_totals.rates.laa.rate
})`}</td>
</tr>
<tr>
<td>{t("jobs.fields.rate_lab")}</td>
<td className="currency">
{Dinero(job.job_totals.rates.lab.total).toFormat()}
</td>
<td>{`(${job.job_totals.rates.lab.hours.toFixed(2)} @ ${
job.job_totals.rates.lab.rate
})`}</td>
</tr>
<tr>
<td>{t("jobs.fields.rate_lad")}</td>
<td className="currency">
{Dinero(job.job_totals.rates.lad.total).toFormat()}
</td>
<td>{`(${job.job_totals.rates.lad.hours.toFixed(2)} @ ${
job.job_totals.rates.lad.rate
})`}</td>
</tr>
<tr>
<td>{t("jobs.fields.rate_lae")}</td>
<td className="currency">
{Dinero(job.job_totals.rates.lae.total).toFormat()}
</td>
<td>{`(${job.job_totals.rates.lae.hours.toFixed(2)} @ ${
job.job_totals.rates.lae.rate
})`}</td>
</tr>
<tr>
<td>{t("jobs.fields.rate_laf")}</td>
<td className="currency">
{Dinero(job.job_totals.rates.laf.total).toFormat()}
</td>
<td>{`(${job.job_totals.rates.laf.hours.toFixed(2)} @ ${
job.job_totals.rates.laf.rate
})`}</td>
</tr>
<tr>
<td>{t("jobs.fields.rate_lag")}</td>
<td className="currency">
{Dinero(job.job_totals.rates.lag.total).toFormat()}
</td>
<td>{`(${job.job_totals.rates.lag.hours.toFixed(2)} @ ${
job.job_totals.rates.lag.rate
})`}</td>
</tr>
<tr>
<td>{t("jobs.fields.rate_lam")}</td>
<td className="currency">
{Dinero(job.job_totals.rates.lam.total).toFormat()}
</td>
<td>{`(${job.job_totals.rates.lam.hours.toFixed(2)} @ ${
job.job_totals.rates.lam.rate
})`}</td>
</tr>
<tr>
<td>{t("jobs.fields.rate_lar")}</td>
<td className="currency">
{Dinero(job.job_totals.rates.lar.total).toFormat()}
</td>
<td>{`(${job.job_totals.rates.lar.hours.toFixed(2)} @ ${
job.job_totals.rates.lar.rate
})`}</td>
</tr>
<tr>
<td>{t("jobs.fields.rate_las")}</td>
<td className="currency">
{Dinero(job.job_totals.rates.las.total).toFormat()}
</td>
<td>{`(${job.job_totals.rates.las.hours.toFixed(2)} @ ${
job.job_totals.rates.las.rate
})`}</td>
</tr>
<tr>
<td>{t("jobs.fields.rate_lau")}</td>
<td className="currency">
{Dinero(job.job_totals.rates.lau.total).toFormat()}
</td>
<td>{`(${job.job_totals.rates.lau.hours.toFixed(2)} @ ${
job.job_totals.rates.lau.rate
})`}</td>
</tr>
<tr>
<td>{t("jobs.fields.rate_la1")}</td>
<td className="currency">
{Dinero(job.job_totals.rates.la1.total).toFormat()}
</td>
<td>{`(${job.job_totals.rates.la1.hours.toFixed(2)} @ ${
job.job_totals.rates.la1.rate
})`}</td>
</tr>
<tr>
<td>{t("jobs.fields.rate_la2")}</td>
<td className="currency">
{Dinero(job.job_totals.rates.la2.total).toFormat()}
</td>
<td>{`(${job.job_totals.rates.la2.hours.toFixed(2)} @ ${
job.job_totals.rates.la2.rate
})`}</td>
</tr>
<tr>
<td>{t("jobs.fields.rate_la3")}</td>
<td className="currency">
{Dinero(job.job_totals.rates.la3.total).toFormat()}
</td>
<td>{`(${job.job_totals.rates.la3.hours.toFixed(2)} @ ${
job.job_totals.rates.la3.rate
})`}</td>
</tr>
<tr>
<td>{t("jobs.fields.rate_la4")}</td>
<td className="currency">
{Dinero(job.job_totals.rates.la4.total).toFormat()}
</td>
<td>{`(${job.job_totals.rates.la4.hours.toFixed(2)} @ ${
job.job_totals.rates.la4.rate
})`}</td>
</tr>
<tr>
<td>{t("jobs.labels.labor_rates_subtotal")}</td>
<td className="currency">
<strong>
{Dinero(job.job_totals.rates.rates_subtotal).toFormat()}
</strong>
</td>
<td></td>
</tr>
<tr>
<td>{t("jobs.labels.mapa")}</td>
<td className="currency">
{Dinero(job.job_totals.rates.mapa.total).toFormat()}
</td>
<td>{`(${job.job_totals.rates.mapa.hours.toFixed(2)} @ ${
job.job_totals.rates.mapa.rate
})`}</td>
</tr>
<tr>
<td>{t("jobs.labels.mash")}</td>
<td className="currency">
{Dinero(job.job_totals.rates.mash.total).toFormat()}
</td>
<td>{`(${job.job_totals.rates.mash.hours.toFixed(2)} @ ${
job.job_totals.rates.mash.rate
})`}</td>
</tr>
<tr>
<td>{t("jobs.labels.rates_subtotal")}</td>
<td className="currency">
<strong>
{Dinero(job.job_totals.rates.subtotal).toFormat()}
</strong>
</td>
<td></td>
</tr>
</tbody>
</table>
</div>
<Card title={t("jobs.labels.labortotals")}>
<JobTotalsTableLabor job={job} />
</Card>
</Col>
<Col {...colSpan}>
<div className="job-totals-half">
<Typography.Title level={4}>
{t("jobs.labels.partstotal")}
</Typography.Title>
<table>
<tbody>
{Object.keys(job.job_totals.parts.parts.list).map(
(key, idx) => (
<tr key={idx}>
<td>{t(`jobs.fields.${key.toLowerCase()}`)}</td>
<td className="currency">
{Dinero(
job.job_totals.parts.parts.list[key].total
).toFormat()}
</td>
</tr>
)
)}
<tr>
<td>{t("jobs.labels.partstotal")}</td>
<td className="currency">
<strong>
{Dinero(job.job_totals.parts.parts.total).toFormat()}
</strong>
</td>
</tr>
</tbody>
</table>
<Typography.Title level={4}>
{t("jobs.labels.othertotal")}
</Typography.Title>
<table>
<tbody>
<tr>
<td>{t("jobs.labels.subletstotal")}</td>
<td className="currency">
{Dinero(job.job_totals.parts.sublets.total).toFormat()}
</td>
</tr>
<tr>
<td>{t("jobs.labels.additionaltotal")}</td>
<td className="currency">
{Dinero(job.job_totals.additional).toFormat()}
</td>
</tr>
</tbody>
</table>
<Typography.Title level={4}>
{t("jobs.labels.jobtotals")}
</Typography.Title>
<table>
<tbody>
<tr>
<td>{t("jobs.labels.subtotal")}</td>
<td className="currency">
<strong>
{Dinero(job.job_totals.totals.subtotal).toFormat()}
</strong>
</td>
</tr>
<tr>
<td>{t("jobs.labels.local_tax_amt")}</td>
<td className="currency">
{Dinero(job.job_totals.totals.local_tax).toFormat()}
</td>
</tr>
<tr>
<td>{t("jobs.labels.state_tax_amt")}</td>
<td className="currency">
{Dinero(job.job_totals.totals.state_tax).toFormat()}
</td>
</tr>
<tr>
<td>{t("jobs.labels.federal_tax_amt")}</td>
<td className="currency">
{Dinero(job.job_totals.totals.federal_tax).toFormat()}
</td>
</tr>
<tr>
<td>{t("jobs.fields.ded_amt")}</td>
<td className="currency">
{Dinero(
job.job_totals.totals.custPayable.deductible
).toFormat()}
</td>
</tr>
<tr>
<td>{t("jobs.fields.federal_tax_payable")}</td>
<td className="currency">
{Dinero(
job.job_totals.totals.custPayable.federal_tax
).toFormat()}
</td>
</tr>
<tr>
<td>{t("jobs.fields.other_amount_payable")}</td>
<td className="currency">
{Dinero(
job.job_totals.totals.custPayable.other_customer_amount
).toFormat()}
</td>
</tr>
<tr>
<td>{t("jobs.fields.depreciation_taxes")}</td>
<td className="currency">
{Dinero(
job.job_totals.totals.custPayable.dep_taxes
).toFormat()}
</td>
</tr>
<tr>
<td>{t("jobs.labels.total_repairs")}</td>
<td className="currency">
{Dinero(job.job_totals.totals.total_repairs).toFormat()}
</td>
</tr>
<tr>
<td>{t("jobs.labels.total_cust_payable")}</td>
<td className="currency">
{Dinero(job.job_totals.totals.custPayable.total).toFormat()}
</td>
</tr>
<tr>
<td>{t("jobs.labels.net_repairs")}</td>
<td className="currency">
<strong>
{Dinero(job.job_totals.totals.net_repairs).toFormat()}
</strong>
</td>
</tr>
</tbody>
</table>
<JobCalculateTotals job={job} disabled={jobRO} />
<Collapse>
<Collapse.Panel header="JSON Tree Totals">
<div>
<pre>
{JSON.stringify(
{
CIECA: job.cieca_ttl && job.cieca_ttl.data,
ImEXCalc: job.job_totals,
},
null,
2
)}
</pre>
</div>
</Collapse.Panel>
</Collapse>
</div>
<Row gutter={[0, 32]}>
<Col span={24}>
<Card title={t("jobs.labels.partstotal")}>
<JobTotalsTableParts job={job} />
</Card>
</Col>
<Col span={24}>
<Card title={t("jobs.labels.othertotal")}>
<JobTotalsTableOther job={job} />
</Card>
</Col>
<Col span={24}>
<Card title={t("jobs.labels.jobtotals")}>
<JobTotalsTableTotals job={job} />
</Card>
</Col>
<Col span={24}>
<Card title="DEVELOPMENT USE ONLY">
<JobCalculateTotals job={job} disabled={jobRO} />
<Collapse>
<Collapse.Panel header="JSON Tree Totals">
<div>
<pre>
{JSON.stringify(
{
CIECA: job.cieca_ttl && job.cieca_ttl.data,
CIECASTL: job.cieca_stl && job.cieca_stl.data,
ImEXCalc: job.job_totals,
},
null,
2
)}
</pre>
</div>
</Collapse.Panel>
</Collapse>
</Card>
</Col>
</Row>
</Col>
</Row>
</div>

View File

@@ -1,38 +1,38 @@
.job-totals-half {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
// .job-totals-half {
// flex: 1;
// display: flex;
// flex-direction: column;
// align-items: center;
table {
border: 1px solid #ccc;
border-collapse: collapse;
margin: 0;
padding: 0;
width: 80%;
table-layout: fixed;
}
// table {
// border: 1px solid #ccc;
// border-collapse: collapse;
// margin: 0;
// padding: 0;
// width: 80%;
// table-layout: fixed;
// }
table tr {
//background-color: #f8f8f8;
border: 1px solid #ddd;
padding: 0.35em;
}
// table tr {
// //background-color: #f8f8f8;
// border: 1px solid #ddd;
// padding: 0.35em;
// }
table th,
table td {
padding: 0.625em;
//text-align: center;
}
table td.currency {
text-align: right;
}
}
// table th,
// table td {
// padding: 0.625em;
// //text-align: center;
// }
// table td.currency {
// text-align: right;
// }
// }
.job-totals-stats {
margin: 1rem;
display: flex;
width: 100%;
//flex-direction: column;
justify-content: space-evenly;
}
// .job-totals-stats {
// margin: 1rem;
// display: flex;
// width: 100%;
// //flex-direction: column;
// justify-content: space-evenly;
// }

View File

@@ -0,0 +1,157 @@
import { Table } from "antd";
import Dinero from "dinero.js";
import React, { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { alphaSort } from "../../utils/sorters";
export default function JobTotalsTableLabor({ job }) {
const { t } = useTranslation();
const [state, setState] = useState({
sortedInfo: {
columnKey: "profitcenter_labor",
field: "profitcenter_labor",
order: "ascend",
},
filteredInfo: {},
});
const data = useMemo(() => {
return Object.keys(job.job_totals.rates)
.filter(
(key) =>
key !== "mapa" &&
key !== "mash" &&
key !== "subtotal" &&
key !== "rates_subtotal"
)
.map((key) => {
return {
id: key,
...job.job_totals.rates[key],
};
});
}, [job.job_totals.rates]);
const columns = [
{
title: t("joblines.fields.profitcenter_labor"),
dataIndex: "profitcenter_labor",
key: "profitcenter_labor",
defaultSortOrder: "ascend",
sorter: (a, b) =>
alphaSort(
t(`jobs.fields.rate_${a.id.toLowerCase()}`),
t(`jobs.fields.rate_${b.id.toLowerCase()}`)
),
sortOrder:
state.sortedInfo.columnKey === "profitcenter_labor" &&
state.sortedInfo.order,
render: (text, record) =>
t(`jobs.fields.rate_${record.id.toLowerCase()}`),
},
{
title: t("jobs.labels.rates"),
dataIndex: "rate",
key: "rate",
align: "right",
sorter: (a, b) => a.rate - b.rate,
sortOrder:
state.sortedInfo.columnKey === "rate" && state.sortedInfo.order,
render: (text, record) => (
<CurrencyFormatter>{record.rate}</CurrencyFormatter>
),
},
{
title: t("joblines.fields.mod_lb_hrs"),
dataIndex: "mod_lb_hrs",
key: "mod_lb_hrs",
sorter: (a, b) => a.mod_lb_hrs - b.mod_lb_hrs,
sortOrder:
state.sortedInfo.columnKey === "mod_lb_hrs" && state.sortedInfo.order,
render: (text, record) => record.hours.toFixed(1),
},
{
title: t("joblines.fields.total"),
dataIndex: "total",
key: "total",
align: "right",
sorter: (a, b) => a.total.amount - b.total.amount,
sortOrder:
state.sortedInfo.columnKey === "total" && state.sortedInfo.order,
render: (text, record) => Dinero(record.total).toFormat(),
},
];
const handleTableChange = (pagination, filters, sorter) => {
console.log("sorter :>> ", sorter);
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
return (
<Table
columns={columns}
rowKey="id"
pagination={false}
onChange={handleTableChange}
dataSource={data}
scroll={{
x: true,
}}
summary={() => (
<>
<Table.Summary.Row>
<Table.Summary.Cell>
{t("jobs.labels.labor_rates_subtotal")}
</Table.Summary.Cell>
<Table.Summary.Cell />
<Table.Summary.Cell />
<Table.Summary.Cell>
<strong>
{Dinero(job.job_totals.rates.rates_subtotal).toFormat()}
</strong>
</Table.Summary.Cell>
</Table.Summary.Row>
<Table.Summary.Row>
<Table.Summary.Cell>{t("jobs.labels.mapa")}</Table.Summary.Cell>
<Table.Summary.Cell>
{job.job_totals.rates.mapa.rate}
</Table.Summary.Cell>
<Table.Summary.Cell>
{job.job_totals.rates.mapa.hours.toFixed(2)}
</Table.Summary.Cell>
<Table.Summary.Cell>
{Dinero(job.job_totals.rates.mapa.total).toFormat()}
</Table.Summary.Cell>
</Table.Summary.Row>
<Table.Summary.Row>
<Table.Summary.Cell>{t("jobs.labels.mash")}</Table.Summary.Cell>
<Table.Summary.Cell>
{job.job_totals.rates.mash.rate}
</Table.Summary.Cell>
<Table.Summary.Cell>
{job.job_totals.rates.mash.hours.toFixed(2)}
</Table.Summary.Cell>
<Table.Summary.Cell>
{Dinero(job.job_totals.rates.mash.total).toFormat()}
</Table.Summary.Cell>
</Table.Summary.Row>
<Table.Summary.Row>
<Table.Summary.Cell>
{t("jobs.labels.labor_rates_subtotal")}
</Table.Summary.Cell>
<Table.Summary.Cell />
<Table.Summary.Cell />
<Table.Summary.Cell>
<strong>
{Dinero(job.job_totals.rates.rates_subtotal).toFormat()}
</strong>
</Table.Summary.Cell>
</Table.Summary.Row>
</>
)}
/>
);
}

View File

@@ -0,0 +1,77 @@
import { Table } from "antd";
import Dinero from "dinero.js";
import React, { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { alphaSort } from "../../utils/sorters";
export default function JobTotalsTableOther({ job }) {
const { t } = useTranslation();
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: {},
});
const data = useMemo(() => {
return [
{
key: t("jobs.labels.subletstotal"),
total: job.job_totals.parts.sublets.total,
},
{
key: t("jobs.labels.additionaltotal"),
total: job.job_totals.additional,
},
];
}, [job.job_totals, t]);
const columns = [
{
//title: t("joblines.fields.part_type"),
dataIndex: "key",
key: "key",
sorter: (a, b) => alphaSort(a.key, b.key),
sortOrder: state.sortedInfo.columnKey === "key" && state.sortedInfo.order,
width: "0%",
},
{
title: t("joblines.fields.total"),
dataIndex: "total",
key: "total",
sorter: (a, b) => a.total.amount - b.total.amount,
sortOrder:
state.sortedInfo.columnKey === "total" && state.sortedInfo.order,
width: "20%",
align: "right",
render: (text, record) => Dinero(record.total).toFormat(),
},
];
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
return (
<Table
columns={columns}
rowKey="key"
pagination={false}
onChange={handleTableChange}
dataSource={data}
scroll={{
x: true,
}}
summary={() => (
<Table.Summary.Row>
<Table.Summary.Cell>
{t("jobs.labels.additionaltotal")}
</Table.Summary.Cell>
<Table.Summary.Cell>
<strong>
{Dinero(job.job_totals.parts.parts.total).toFormat()}
</strong>
</Table.Summary.Cell>
</Table.Summary.Row>
)}
/>
);
}

View File

@@ -0,0 +1,84 @@
import { Table } from "antd";
import Dinero from "dinero.js";
import React, { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { alphaSort } from "../../utils/sorters";
export default function JobTotalsTableParts({ job }) {
const { t } = useTranslation();
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: {},
});
const data = useMemo(() => {
return Object.keys(job.job_totals.parts.parts.list)
.filter(
(key) =>
key !== "mapa" &&
key !== "mash" &&
key !== "subtotal" &&
key !== "rates_subtotal"
)
.map((key) => {
return {
id: key,
...job.job_totals.parts.parts.list[key],
};
});
}, [job.job_totals.parts.parts.list]);
const columns = [
{
title: t("joblines.fields.part_type"),
dataIndex: "id",
key: "id",
sorter: (a, b) =>
alphaSort(
t(`jobs.fields.${a.id.toLowerCase()}`),
t(`jobs.fields.${b.id.toLowerCase()}`)
),
width: "80%",
sortOrder: state.sortedInfo.columnKey === "id" && state.sortedInfo.order,
render: (text, record) => t(`jobs.fields.${record.id.toLowerCase()}`),
},
{
title: t("joblines.fields.total"),
dataIndex: "total",
key: "total",
sorter: (a, b) => a.total.amount - b.total.amount,
sortOrder:
state.sortedInfo.columnKey === "total" && state.sortedInfo.order,
width: "20%",
align: "right",
render: (text, record) => Dinero(record.total).toFormat(),
},
];
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
return (
<Table
columns={columns}
rowKey="id"
pagination={false}
onChange={handleTableChange}
dataSource={data}
scroll={{
x: true,
}}
summary={() => (
<Table.Summary.Row>
<Table.Summary.Cell>{t("jobs.labels.partstotal")}</Table.Summary.Cell>
<Table.Summary.Cell>
<strong>
{Dinero(job.job_totals.parts.parts.total).toFormat()}
</strong>
</Table.Summary.Cell>
</Table.Summary.Row>
)}
/>
);
}

View File

@@ -0,0 +1,96 @@
import { Table } from "antd";
import Dinero from "dinero.js";
import React, { useMemo } from "react";
import { useTranslation } from "react-i18next";
export default function JobTotalsTableTotals({ job }) {
const { t } = useTranslation();
const data = useMemo(() => {
return [
{
key: t("jobs.labels.subtotal"),
total: job.job_totals.totals.subtotal,
bold: true,
},
{
key: t("jobs.labels.local_tax_amt"),
total: job.job_totals.totals.local_tax,
},
{
key: t("jobs.labels.state_tax_amt"),
total: job.job_totals.totals.state_tax,
},
{
key: t("jobs.labels.federal_tax_amt"),
total: job.job_totals.totals.federal_tax,
},
{
key: t("jobs.fields.ded_amt"),
total: job.job_totals.totals.custPayable.deductible,
},
{
key: t("jobs.fields.federal_tax_payable"),
total: job.job_totals.totals.custPayable.federal_tax,
},
{
key: t("jobs.fields.other_amount_payable"),
total: job.job_totals.totals.custPayable.other_customer_amount,
},
{
key: t("jobs.fields.depreciation_taxes"),
total: job.job_totals.totals.custPayable.dep_taxes,
},
{
key: t("jobs.labels.total_repairs"),
total: job.job_totals.totals.total_repairs,
bold: true,
},
{
key: t("jobs.labels.total_cust_payable"),
total: job.job_totals.totals.custPayable.total,
},
{
key: t("jobs.labels.net_repairs"),
total: job.job_totals.totals.net_repairs,
bold: true,
},
];
}, [job.job_totals, t]);
const columns = [
{
//title: t("joblines.fields.part_type"),
dataIndex: "key",
key: "key",
width: "80%",
onCell: (record, rowIndex) => {
return { style: { fontWeight: record.bold && "bold" } };
},
},
{
title: t("joblines.fields.total"),
dataIndex: "total",
key: "total",
align: "right",
render: (text, record) => Dinero(record.total).toFormat(),
width: "20%",
onCell: (record, rowIndex) => {
return { style: { fontWeight: record.bold && "bold" } };
},
},
];
return (
<Table
columns={columns}
rowKey="key"
showHeader={false}
pagination={false}
dataSource={data}
scroll={{
x: true,
}}
/>
);
}

View File

@@ -27,7 +27,6 @@ const lossColDamage = { sm: { span: 24 }, md: { span: 6 }, lg: { span: 4 } };
export function JobsDetailGeneral({ bodyshop, jobRO, job, form }) {
const { getFieldValue } = form;
const { t } = useTranslation();
return (
<div>
<FormRow header={t("jobs.forms.claiminfo")}>

View File

@@ -10,6 +10,8 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import ChatOpenButton from "../chat-open-button/chat-open-button.component";
import DataLabel from "../data-label/data-label.component";
import JobEmployeeAssignments from "../job-employee-assignments/job-employee-assignments.container";
import ProductionListColumnProductionNote from "../production-list-columns/production-list-columns.productionnote.component";
import "./jobs-detail-header.styles.scss";
const mapStateToProps = createStructuredSelector({
@@ -22,15 +24,25 @@ const mapDispatchToProps = (dispatch) => ({
dispatch(setModalContext({ context: context, modal: "printCenter" })),
});
export function JobsDetailHeader({
setPrintCenterContext,
jobRO,
job,
refetch,
loading,
form,
bodyshop,
}) {
const colSpan = {
xs: {
span: 24,
},
sm: {
span: 24,
},
md: {
span: 12,
},
lg: {
span: 6,
},
xl: {
span: 6,
},
};
export function JobsDetailHeader({ job, bodyshop }) {
const { t } = useTranslation();
const jobInPostProduction = useMemo(() => {
@@ -39,15 +51,10 @@ export function JobsDetailHeader({
);
}, [job.status, bodyshop.md_ro_statuses.post_production_statuses]);
const gridStyle = {
flex: 1,
//textAlign: "center",
};
return (
<Row gutter={16} style={{ alignItems: "stretch" }}>
<Col span={8}>
<Card title="Job Status" style={{ height: "100%" }}>
<Row gutter={[16, 16]} style={{ alignItems: "stretch" }}>
<Col {...colSpan}>
<Card title={"Job Status"} style={{ height: "100%" }}>
<div>
<DataLabel label={t("jobs.fields.status")}>
{job.status}
@@ -68,10 +75,15 @@ export function JobsDetailHeader({
<span style={{ margin: "0rem .5rem" }}>/</span>
<CurrencyFormatter>{job.owner_owing}</CurrencyFormatter>
</DataLabel>
{(job.inproduction || jobInPostProduction) && (
<DataLabel label={t("jobs.fields.production_vars.note")}>
<ProductionListColumnProductionNote record={job} />
</DataLabel>
)}
</div>
</Card>
</Col>
<Col span={8}>
<Col {...colSpan}>
<Link to={`/manage/owners/${job.owner.id}`}>
<Card
className="ant-card-grid-hoverable"
@@ -96,7 +108,7 @@ export function JobsDetailHeader({
</Card>
</Link>
</Col>
<Col span={8}>
<Col {...colSpan}>
<Link to={`/manage/vehicles/${job.vehicle.id}`}>
<Card
className="ant-card-grid-hoverable"
@@ -118,6 +130,16 @@ export function JobsDetailHeader({
</Card>
</Link>
</Col>
<Col {...colSpan}>
<Card
style={{ height: "100%" }}
title={t("jobs.labels.employeeassignments")}
>
<div>
<JobEmployeeAssignments job={job} />
</div>
</Card>
</Col>
</Row>
);
@@ -156,7 +178,7 @@ export function JobsDetailHeader({
// </>
// )}
// <JobEmployeeAssignments job={job} />
//
// </div>
// </PageHeader>
// );

View File

@@ -15,14 +15,9 @@ export function JobsDetailRatesChangeButton({ disabled, form, bodyshop }) {
const handleClick = ({ item, key, keyPath }) => {
const rate = item.props.value;
console.log("handleClick -> rate", rate);
form.setFieldsValue(rate);
};
console.log(
"🚀 ~ file: jobs-detail-rates-change-button.component.jsx ~ line 26 ~ bodyshop.md_labor_rates",
bodyshop.md_labor_rates
);
const menu = (
<div>
<Menu onClick={handleClick}>

View File

@@ -14,9 +14,8 @@ const mapStateToProps = createStructuredSelector({
jobRO: selectJobReadOnly,
});
export function JobsDetailRates({ job, jobRO, form }) {
export function JobsDetailRates({ jobRO, form }) {
const { t } = useTranslation();
return (
<div>
<FormRow>

View File

@@ -1,143 +1,15 @@
import { Button, Divider, Space, Statistic, Typography } from "antd";
import Dinero from "dinero.js";
import React, { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import { TemplateList } from "../../utils/TemplateConstants";
import { Divider } from "antd";
import React from "react";
import JobPayments from "../job-payments/job-payments.component";
import JobTotalsTable from "../job-totals-table/job-totals-table.component";
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
jobRO: selectJobReadOnly,
});
const mapDispatchToProps = (dispatch) => ({
setPaymentContext: (context) =>
dispatch(setModalContext({ context: context, modal: "payment" })),
});
const stripeTestEnv = process.env.REACT_APP_STRIPE_PUBLIC_KEY; //.includes("test");
export function JobsDetailTotals({
job,
jobRO,
bodyshop,
setPaymentContext,
refetch,
}) {
const { t } = useTranslation();
const total = useMemo(() => {
return (
job.payments &&
job.payments.reduce((acc, val) => {
acc = acc.add(Dinero({ amount: Math.round(val.amount * 100) }));
return acc;
}, Dinero())
);
}, [job.payments]);
const balance = useMemo(() => {
if (job && job.job_totals && job.job_totals.totals.total_repairs)
return Dinero(job.job_totals.totals.total_repairs).subtract(total);
return Dinero({ amount: 0 }).subtract(total);
}, [job, total]);
export function JobsDetailTotals({ job, refetch }) {
return (
<div>
<Typography.Title level={4}>
{t("payments.labels.title")}
</Typography.Title>
<div className="imex-flex-row">
<table style={{ flex: 1 }}>
<thead>
<tr>
<th>{t("payments.fields.created_at")}</th>
<th>{t("payments.fields.payer")}</th>
<th>{t("payments.fields.amount")}</th>
<th>{t("payments.fields.memo")}</th>
<th>{t("payments.fields.type")}</th>
<th>{t("payments.fields.transactionid")}</th>
<th>{t("payments.fields.stripeid")}</th>
<th>{t("general.labels.actions")}</th>
</tr>
</thead>
<tbody>
{job.payments.map((p, idx) => (
<tr key={idx}>
<td>
<DateTimeFormatter>{p.created_at}</DateTimeFormatter>
</td>
<td>{p.payer}</td>
<td>
<CurrencyFormatter>{p.amount}</CurrencyFormatter>
</td>
<td>{p.memo}</td>
<td>{p.type}</td>
<td>{p.transactionid}</td>
<td>
{p.stripeid ? (
<a
href={
stripeTestEnv
? `https://dashboard.stripe.com/${bodyshop.stripe_acct_id}/test/payments/${p.stripeid}`
: `https://dashboard.stripe.com/${bodyshop.stripe_acct_id}/payments/${p.stripeid}`
}
>
{p.stripeid}
</a>
) : null}
</td>
<td>
<PrintWrapperComponent
templateObject={{
name: TemplateList("payment").payment_receipt.key,
variables: { id: p.id },
}}
messageObject={{
to: job.ownr_ea,
}}
/>
</td>
</tr>
))}
</tbody>
</table>
<Space direction="vertical">
<Button
disabled={jobRO}
onClick={() =>
setPaymentContext({
actions: { refetch: refetch },
context: { jobid: job.id },
})
}
>
{t("menus.header.enterpayment")}
</Button>
<Statistic
title={t("payments.labels.totalpayments")}
value={total.toFormat()}
/>
<Statistic
title={t("payments.labels.balance")}
valueStyle={{ color: balance.getAmount() !== 0 ? "red" : "green" }}
value={balance.toFormat()}
/>
</Space>
</div>
<JobPayments job={job} refetch={refetch} />
<Divider />
<JobTotalsTable job={job} />
</div>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(JobsDetailTotals);
export default JobsDetailTotals;

View File

@@ -84,4 +84,4 @@ const onServiceWorkerUpdate = (registration) => {
};
serviceWorkerRegistration.register({ onUpdate: onServiceWorkerUpdate });
reportWebVitals(console.log);
reportWebVitals();

View File

@@ -6,7 +6,15 @@ import Icon, {
PrinterFilled,
ToolFilled,
} from "@ant-design/icons";
import { Button, Form, notification, PageHeader, Space, Tabs } from "antd";
import {
Button,
Divider,
Form,
notification,
PageHeader,
Space,
Tabs,
} from "antd";
import Axios from "axios";
import moment from "moment";
import queryString from "query-string";
@@ -148,13 +156,15 @@ export function JobsDetailPage({
extra={menuExtra}
/>
<JobsDetailHeader job={job} />
<Divider type="horizontal" />
<FormFieldsChanged form={form} />
<Tabs
defaultActiveKey={search.tab}
onChange={(key) => history.push({ search: `?tab=${key}` })}
tabBarStyle={{ fontWeight: "bold", borderBottom: "10px" }}
>
<Tabs.TabPane
forceRender
tab={
<span>
<Icon component={FaShieldAlt} />
@@ -166,6 +176,7 @@ export function JobsDetailPage({
<JobsDetailGeneral job={job} form={form} />
</Tabs.TabPane>
<Tabs.TabPane
forceRender
tab={
<span>
<BarsOutlined />
@@ -182,6 +193,7 @@ export function JobsDetailPage({
/>
</Tabs.TabPane>
<Tabs.TabPane
forceRender
tab={
<span>
<DollarCircleOutlined />
@@ -226,6 +238,7 @@ export function JobsDetailPage({
<JobsDetailLaborContainer jobId={job.id} />
</Tabs.TabPane>
<Tabs.TabPane
forceRender
tab={
<span>
<CalendarFilled />

View File

@@ -1069,7 +1069,7 @@
"policy_no": "Policy #",
"ponumber": "PO Number",
"production_vars": {
"note": "Production Note:"
"note": "Production Note"
},
"rate_la1": "LA1",
"rate_la2": "LA2",
@@ -1192,6 +1192,7 @@
"documents-other": "Other Documents",
"duplicateconfirm": "Are you sure you want to duplicate this job? Some elements of this job will not be duplicated.",
"employeeassignments": "Employee Assignments",
"estimatelines": "Estimate Lines",
"existing_jobs": "Existing Jobs",
"federal_tax_amt": "Federal Taxes",
"gpdollars": "$ G.P.",

View File

@@ -1192,6 +1192,7 @@
"documents-other": "",
"duplicateconfirm": "",
"employeeassignments": "",
"estimatelines": "",
"existing_jobs": "Empleos existentes",
"federal_tax_amt": "",
"gpdollars": "",

View File

@@ -1192,6 +1192,7 @@
"documents-other": "",
"duplicateconfirm": "",
"employeeassignments": "",
"estimatelines": "",
"existing_jobs": "Emplois existants",
"federal_tax_amt": "",
"gpdollars": "",

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useRef } from "react";
import { useEffect, useRef } from "react";
function useTraceUpdate(props) {
const prev = useRef(props);
useEffect(() => {