Refactored job closing to be line based instead of totals based. BOD-383

This commit is contained in:
Patrick Fic
2020-09-14 13:54:11 -07:00
parent e3f108c567
commit eff49e3d25
34 changed files with 1030 additions and 822 deletions

View File

@@ -0,0 +1,11 @@
import React, { forwardRef } from "react";
import { useTranslation } from "react-i18next";
const LaborTypeFormItem = ({ value, onChange }, ref) => {
const { t } = useTranslation();
if (!value) return null;
return <div>{t(`joblines.fields.lbr_types.${value}`)}</div>;
};
export default forwardRef(LaborTypeFormItem);

View File

@@ -0,0 +1,11 @@
import React, { forwardRef } from "react";
import { useTranslation } from "react-i18next";
const PartTypeFormItem = ({ value, onChange }, ref) => {
const { t } = useTranslation();
if (!value) return null;
return <div>{t(`joblines.fields.part_types.${value}`)}</div>;
};
export default forwardRef(PartTypeFormItem);

View File

@@ -0,0 +1,17 @@
import Dinero from "dinero.js";
import React, { forwardRef } from "react";
const ReadOnlyFormItem = ({ value, type = "text", onChange }, ref) => {
if (!value) return null;
switch (type) {
case "text":
return <div>{value}</div>;
case "currency":
return (
<div>{Dinero({ amount: Math.round(value * 100) }).toFormat()}</div>
);
default:
return <div>{value}</div>;
}
};
export default forwardRef(ReadOnlyFormItem);

View File

@@ -1,23 +0,0 @@
import { Button } from "antd";
import React, { forwardRef } from "react";
import { useTranslation } from "react-i18next";
import AlertComponent from "../alert/alert.component";
function ResetForm({ resetFields }) {
const { t } = useTranslation();
return (
<AlertComponent
message={
<div>
{t("general.messages.unsavedchanges")}
<Button style={{ marginLeft: "20px" }} onClick={() => resetFields()}>
{t("general.actions.reset")}
</Button>
</div>
}
closable
/>
);
}
export default forwardRef(ResetForm);

View File

@@ -1,119 +0,0 @@
import { PlusCircleFilled, CloseCircleFilled } from "@ant-design/icons";
import { Button, InputNumber, Select } from "antd";
import Dinero from "dinero.js";
import React, { useState, useEffect } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { useTranslation } from "react-i18next";
import { logImEXEvent } from "../../firebase/firebase.utils";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const { Option } = Select;
export function JobsCloseLabmatAllocationButton({
remainingAmount,
allocationKey,
allocation,
setAllocations,
bodyshop,
invoiced,
}) {
const [visible, setVisible] = useState(false);
const [state, setState] = useState({ center: "", amount: 0 });
const { t } = useTranslation();
const handleAllocate = () => {
logImEXEvent("jobs_close_allocate_single");
const existingIndex = allocation.allocations.findIndex(
(e) => e.center === state.center
);
const newAllocations = allocation.allocations.slice(0);
if (existingIndex > -1) {
newAllocations[existingIndex] = {
center: state.center,
amount: newAllocations[existingIndex].amount.add(
Dinero({ amount: (state.amount || 0) * 100 })
),
};
} else {
newAllocations.push({
center: state.center,
amount: Dinero({ amount: (state.amount || 0) * 100 }),
});
}
setAllocations((labMatState) => {
return {
...labMatState,
[allocationKey]: {
...allocation,
allocations: newAllocations,
},
};
});
setState({ center: "", amount: 0 });
};
const showAllocation = Dinero(allocation.total).getAmount() > 0;
useEffect(() => {
if (remainingAmount === 0) setVisible(false);
}, [remainingAmount, setVisible]);
if (!showAllocation) return null;
return (
<div style={{ display: "flex", alignItems: "center" }}>
<div style={{ display: visible ? "" : "none" }}>
<Select
style={{ width: "200px" }}
value={state.center}
onSelect={(val) => setState({ ...state, center: val })}
>
{bodyshop.md_responsibility_centers.profits.map((r, idx) => (
<Option key={idx} value={r.name}>
{r.name}
</Option>
))}
</Select>
<InputNumber
precision={2}
min={0}
value={state.amount}
onChange={(val) => setState({ ...state, amount: val })}
max={remainingAmount / 100}
/>
<Button
onClick={handleAllocate}
disabled={
state.amount === 0 ||
state.center === "" ||
remainingAmount === 0 ||
invoiced
}
>
{t("jobs.actions.allocate")}
</Button>
</div>
<div>
{visible ? (
<CloseCircleFilled
onClick={() => setVisible(false)}
disabled={invoiced}
/>
) : (
<PlusCircleFilled
onClick={() => setVisible(true)}
disabled={invoiced}
/>
)}
</div>
</div>
);
}
export default connect(mapStateToProps, null)(JobsCloseLabmatAllocationButton);

View File

@@ -1,35 +0,0 @@
import React from "react";
import { Tag } from "antd";
import Dinero from "dinero.js";
export default function JobsCloseLabMatAllocationTags({
allocationKey,
allocation,
setAllocations,
invoiced,
}) {
return (
<div>
{allocation.allocations.map((a, idx) => (
<Tag
closable={!invoiced} //Value is whether it is invoiced.
visible
color="green"
onClose={() => {
setAllocations((state) => {
return {
...state,
[allocationKey]: {
...allocation,
allocations: allocation.allocations.filter(
(val, index) => index !== idx
),
},
};
});
}}
key={idx}
>{`${a.center} - ${Dinero(a.amount).toFormat()}`}</Tag>
))}
</div>
);
}

View File

@@ -1,76 +1,38 @@
import React from "react";
import { Button } from "antd";
import { selectBodyshop } from "../../redux/user/user.selectors";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { useTranslation } from "react-i18next";
import { logImEXEvent } from "../../firebase/firebase.utils";
import Dinero from "dinero.js";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
export function JobsCloseAutoAllocate({
bodyshop,
labmatAllocations,
setLabmatAllocations,
partsAllocations,
setPartsAllocations,
disabled,
}) {
export function JobsCloseAutoAllocate({ bodyshop, joblines, form, disabled }) {
const { t } = useTranslation();
const handleAllocate = () => {
logImEXEvent("jobs_close_allocate_auto");
const { defaults } = bodyshop.md_responsibility_centers;
Object.keys(labmatAllocations).forEach((i) => {
const defaultProfitCenter = defaults.profits[i.toUpperCase()];
if (
!!defaultProfitCenter &&
Dinero(labmatAllocations[i].total).getAmount() > 0
) {
setLabmatAllocations((st) => {
return {
...st,
[i]: {
...labmatAllocations[i],
allocations: [
{
center: defaultProfitCenter,
amount: labmatAllocations[i].total,
},
],
},
};
});
}
});
Object.keys(partsAllocations).forEach((i) => {
const defaultProfitCenter = defaults.profits[i.toUpperCase()];
if (
!!defaultProfitCenter &&
Dinero(partsAllocations[i].total).getAmount() > 0
) {
setPartsAllocations((st) => {
return {
...st,
[i]: {
...partsAllocations[i],
allocations: [
{
center: defaultProfitCenter,
amount: partsAllocations[i].total,
},
],
},
};
});
}
form.setFieldsValue({
joblines: joblines.map((jl) => {
const ret = jl;
if (jl.part_type) {
ret.profitcenter_part = defaults.profits[jl.part_type.toUpperCase()];
} else {
ret.profitcenter_part = null;
}
if (jl.mod_lbr_ty) {
ret.profitcenter_labor =
defaults.profits[jl.mod_lbr_ty.toUpperCase()];
} else {
ret.profitcenter_labor = null;
}
return ret;
}),
});
};

View File

@@ -1,94 +0,0 @@
import Dinero from "dinero.js";
import React from "react";
import { useTranslation } from "react-i18next";
import AllocationButton from "../jobs-close-allocation-button/jobs-close-allocation-button.component";
import AllocationTags from "../jobs-close-allocation-tags/jobs-close-allocation-tags.component";
export default function JobCloseLabMatAllocation({
labmatAllocations,
setLabmatAllocations,
labMatTotalAllocation,
invoiced,
}) {
const { t } = useTranslation();
return (
<div style={{ display: "flex" }}>
<table>
<thead>
<tr>
<th>{t("jobs.labels.laborallocations")}</th>
<th>{t("jobs.labels.totals")}</th>
<th>{t("jobs.labels.available")}</th>
<th>{t("jobs.actions.allocate")}</th>
<th>{t("jobs.labels.allocations")}</th>
</tr>
</thead>
<tbody>
{Object.keys(labmatAllocations).map((alloc, idx) => {
if (!alloc.includes("subtotal"))
return (
<tr key={idx}>
<td>{t(`jobs.fields.${alloc}`)}</td>
<td>
{labmatAllocations[alloc].total &&
Dinero(labmatAllocations[alloc].total).toFormat()}
</td>
<td>
{Dinero(labmatAllocations[alloc].total)
.subtract(
Dinero({
amount: labmatAllocations[alloc].allocations.reduce(
(acc, val) => {
return acc + Dinero(val.amount).getAmount();
},
0
),
})
)
.toFormat()}
</td>
<td>
<AllocationButton
allocationKey={alloc}
invoiced={invoiced}
remainingAmount={Dinero(labmatAllocations[alloc].total)
.subtract(
Dinero({
amount: labmatAllocations[alloc].allocations.reduce(
(acc, val) => {
return acc + Dinero(val.amount).getAmount();
},
0
),
})
)
.getAmount()}
allocation={labmatAllocations[alloc]}
setAllocations={setLabmatAllocations}
/>
</td>
<td>
<AllocationTags
allocationKey={alloc}
invoiced={invoiced}
allocation={labmatAllocations[alloc]}
setAllocations={setLabmatAllocations}
/>
</td>
</tr>
);
else return null;
})}
<tr>
<td></td>
<td>{Dinero(labmatAllocations.subtotal).toFormat()}</td>
<td></td>
<td></td>
<td>{Dinero(labMatTotalAllocation).toFormat()}</td>
</tr>
</tbody>
</table>
</div>
);
}

View File

@@ -0,0 +1,120 @@
import { Form, Select } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import LaborTypeFormItem from "../form-items-formatted/labor-type-form-item.component";
import PartTypeFormItem from "../form-items-formatted/part-type-form-item.component";
import ReadOnlyFormItem from "../form-items-formatted/read-only-form-item.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export function JobsCloseLines({ bodyshop, joblines }) {
const { t } = useTranslation();
return (
<div>
<Form.List name={["joblines"]}>
{(fields, { add, remove, move }) => {
return (
<div>
{fields.map((field, index) => (
<Form.Item key={field.key}>
<LayoutFormRow>
<Form.Item
label={t("joblines.fields.line_desc")}
key={`${index}line_desc`}
name={[field.name, "line_desc"]}
>
<ReadOnlyFormItem />
</Form.Item>
<Form.Item
span={2}
label={t("joblines.fields.part_type")}
key={`${index}part_type`}
name={[field.name, "part_type"]}
>
<PartTypeFormItem />
</Form.Item>
<Form.Item
span={2}
label={t("joblines.fields.act_price")}
key={`${index}act_price`}
name={[field.name, "act_price"]}
>
<ReadOnlyFormItem type="currency" />
</Form.Item>
<Form.Item
span={2}
label={t("joblines.fields.mod_lbr_ty")}
key={`${index}mod_lbr_ty`}
name={[field.name, "mod_lbr_ty"]}
>
<LaborTypeFormItem />
</Form.Item>
<Form.Item
span={2}
label={t("joblines.fields.mod_lb_hrs")}
key={`${index}mod_lb_hrs`}
name={[field.name, "mod_lb_hrs"]}
>
<ReadOnlyFormItem />
</Form.Item>
<Form.Item
label={t("joblines.fields.profitcenter_part")}
key={`${index}profitcenter_part`}
name={[field.name, "profitcenter_part"]}
rules={[
{
required:
!!joblines[index].part_type &&
!!joblines[index].act_price,
message: t("general.validation.required"),
},
]}
>
<Select allowClear>
{bodyshop.md_responsibility_centers.profits.map((p) => (
<Select.Option key={p.name} value={p.name}>
{p.name}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
label={t("joblines.fields.profitcenter_labor")}
key={`${index}profitcenter_labor`}
name={[field.name, "profitcenter_labor"]}
rules={[
{
required: !!joblines[index].mod_lbr_ty,
message: t("general.validation.required"),
},
]}
>
<Select allowClear>
{bodyshop.md_responsibility_centers.profits.map((p) => (
<Select.Option key={p.name} value={p.name}>
{p.name}
</Select.Option>
))}
</Select>
</Form.Item>
</LayoutFormRow>
</Form.Item>
))}
</div>
);
}}
</Form.List>
</div>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(JobsCloseLines);

View File

@@ -1,101 +0,0 @@
import React from "react";
import { useTranslation } from "react-i18next";
import Dinero from "dinero.js";
import AllocationButton from "../jobs-close-allocation-button/jobs-close-allocation-button.component";
import AllocationTags from "../jobs-close-allocation-tags/jobs-close-allocation-tags.component";
export default function JobsClosePartsAllocation({
partsAllocations,
setPartsAllocations,
partsAllocatedTotal,
invoiced,
}) {
const { t } = useTranslation();
return (
<div>
{
<table>
<thead>
<tr>
<th>{t("jobs.labels.laborallocations")}</th>
<th>{t("jobs.labels.totals")}</th>
<th>{t("jobs.labels.available")}</th>
<th>{t("jobs.actions.allocate")}</th>
<th>{t("jobs.labels.allocations")}</th>
</tr>
</thead>
<tbody>
{Object.keys(partsAllocations).map((alloc, idx) => {
return (
<tr key={idx}>
<td>{t(`jobs.fields.${alloc.toLowerCase()}`)}</td>
<td>
{partsAllocations[alloc].total &&
Dinero(partsAllocations[alloc].total).toFormat()}
</td>
<td>
{Dinero(partsAllocations[alloc].total)
.subtract(
Dinero({
amount: partsAllocations[alloc].allocations.reduce(
(acc, val) => {
return acc + Dinero(val.amount).getAmount();
},
0
),
})
)
.toFormat()}
</td>
<td>
<AllocationButton
allocationKey={alloc}
invoiced={invoiced}
remainingAmount={Dinero(partsAllocations[alloc].total)
.subtract(
Dinero({
amount: partsAllocations[alloc].allocations.reduce(
(acc, val) => {
return acc + Dinero(val.amount).getAmount();
},
0
),
})
)
.getAmount()}
allocation={partsAllocations[alloc]}
setAllocations={setPartsAllocations}
/>
</td>
<td>
<AllocationTags
invoiced={invoiced}
allocationKey={alloc}
allocation={partsAllocations[alloc]}
setAllocations={setPartsAllocations}
/>
</td>
</tr>
);
})}
<tr>
<td></td>
<td>
{Dinero({
amount: Object.keys(partsAllocations).reduce((acc, val) => {
return (acc =
acc + Dinero(partsAllocations[val].total).getAmount());
}, 0),
}).toFormat()}
</td>
<td></td>
<td></td>
<td>{partsAllocatedTotal.toFormat()}</td>
</tr>
</tbody>
</table>
}
</div>
);
}

View File

@@ -1,72 +0,0 @@
import { useMutation } from "@apollo/react-hooks";
import { Button, notification } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { UPDATE_JOB } from "../../graphql/jobs.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { logImEXEvent } from "../../firebase/firebase.utils";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
export function JobsCloseSaveButton({
bodyshop,
suspenseAmount,
jobId,
labMatAllocations,
partsAllocations,
setInvoicedState,
disabled,
}) {
const [loading, setLoading] = useState(false);
const { t } = useTranslation();
const [updateJob] = useMutation(UPDATE_JOB);
const handleSave = async () => {
logImEXEvent("jobs_close_save");
setLoading(true);
const result = await updateJob({
variables: {
jobId: jobId,
job: {
date_invoiced: new Date(),
status: bodyshop.md_ro_statuses.default_invoiced || "Invoiced*",
invoice_allocation: {
labMatAllocations,
partsAllocations,
},
},
},
});
if (!!!result.errors) {
notification["success"]({ message: t("jobs.successes.invoiced") });
setInvoicedState(true);
} else {
notification["error"]({
message: t("jobs.errors.invoicing", {
error: JSON.stringify(result.errors),
}),
});
}
setLoading(false);
};
return (
<Button
onClick={handleSave}
type="primary"
disabled={suspenseAmount > 0 || disabled}
loading={loading}
>
{t("general.actions.save")}
</Button>
);
}
export default connect(mapStateToProps, null)(JobsCloseSaveButton);

View File

@@ -3,14 +3,11 @@ import React from "react";
import { useTranslation } from "react-i18next";
import Dinero from "dinero.js";
export default function JobsCloseTotals({
jobTotals,
labMatTotal,
partsTotal,
}) {
export default function JobsCloseTotals({ jobTotals }) {
const { t } = useTranslation();
return (
<div>
------Should be removed-----
<Descriptions
bordered
size="small"
@@ -58,22 +55,6 @@ export default function JobsCloseTotals({
title={t("jobs.labels.net_repairs")}
value={Dinero(jobTotals.totals.net_repairs).toFormat()}
/>
<Statistic
title={t("jobs.labels.suspense")}
valueStyle={{
color:
Dinero(jobTotals.totals.subtotal)
.subtract(labMatTotal)
.subtract(partsTotal)
.getAmount() === 0
? "green"
: "red",
}}
value={Dinero(jobTotals.totals.subtotal)
.subtract(labMatTotal)
.subtract(partsTotal)
.toFormat()}
/>
</div>
);
}

View File

@@ -141,14 +141,18 @@ export function JobsDetailHeaderActions({
>
{t("jobs.actions.postInvoices")}
</Menu.Item>
<Menu.Item key="closejob">
<Link
to={{
pathname: `/manage/jobs/${job.id}/close`,
}}
>
{t("menus.jobsactions.closejob")}
</Link>
<Menu.Item disabled={!!job.date_invoiced} key="closejob">
{job.date_invoiced ? (
t("menus.jobsactions.closejob")
) : (
<Link
to={{
pathname: `/manage/jobs/${job.id}/close`,
}}
>
{t("menus.jobsactions.closejob")}
</Link>
)}
</Menu.Item>
<JobsDetaiLheaderCsi job={job} />
<Menu.Item

View File

@@ -17,6 +17,7 @@ export default function LayoutFormRow({ header, children, grow = false }) {
const rowGutter = { gutter: [16, 16] };
const colSpan = (spanOverride) => {
if (spanOverride) return { span: spanOverride };
return {
xs: {
span: 24,
@@ -52,40 +53,3 @@ export default function LayoutFormRow({ header, children, grow = false }) {
</div>
);
}
// export default function LayoutFormRow({ header, children }) {
// if (!!!children.length) {
// //We have only one element. It's going to get the whole thing.
// return children;
// }
// const rowGutter = { gutter: [16, 16] };
// const colSpan = (maxspan) => {
// return {
// xs: {
// span: 24,
// },
// md: {
// span: !!maxspan ? Math.min(12, maxspan) : 12,
// },
// lg: {
// span: !!maxspan
// ? Math.min(Math.max(24 / children.length, 6), maxspan)
// : Math.max(24 / children.length, 6),
// },
// };
// };
// return (
// <div className='imex-form-row'>
// {header ? <Typography.Title level={4}>{header}</Typography.Title> : null}
// <Row {...rowGutter}>
// {children.map((c, idx) => (
// <Col key={idx} {...colSpan(c.props && c.props.maxspan)}>
// {c}
// </Col>
// ))}
// </Row>
// </div>
// );
// }