Refactor task claiming and implement basic claim functionality.

This commit is contained in:
Patrick Fic
2023-07-18 12:59:06 -07:00
parent c214168dcd
commit d7a1d5bbd2
16 changed files with 623 additions and 654 deletions

View File

@@ -1,28 +1,25 @@
import {
Alert,
Button,
Checkbox,
Col,
Form,
Input,
InputNumber,
Radio,
Row,
Select,
Skeleton,
Space,
Table,
Typography,
} from "antd";
import _ from "lodash";
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 EmployeeSearchSelectComponent from "../employee-search-select/employee-search-select.component";
import EmployeeTeamSearchSelectComponent from "../employee-team-search-select/employee-team-search-select.component";
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
import JobSearchSelectComponent from "../job-search-select/job-search-select.component";
import { CalculateAllocationsTotals } from "../labor-allocations-table/labor-allocations-table.utility";
import { LaborAllocationContainer } from "../time-ticket-modal/time-ticket-modal.component";
import TimeTicketsTasksPresets from "../time-ticket-tasks-presets/time-ticket-tasks-presets.component";
import ReadOnlyFormItemComponent from "../form-items-formatted/read-only-form-item.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
@@ -36,23 +33,11 @@ export default connect(
mapDispatchToProps
)(TimeTicketTaskModalComponent);
export function TimeTicketTaskModalComponent({
bodyshop,
form,
lineTicketCalled,
calculateTimeTickets,
lineTicketLoading,
lineTicketData,
queryJobInfo,
}) {
export function TimeTicketTaskModalComponent({ bodyshop, form, loading }) {
const { t } = useTranslation();
return (
<div>
<TimeTicketsTasksPresets
form={form}
calculateTimeTickets={calculateTimeTickets}
/>
<Row gutter={[16, 16]}>
<Col xl={12} lg={24}>
<Form.Item
@@ -70,303 +55,119 @@ export function TimeTicketTaskModalComponent({
notExported={!bodyshop.tt_allow_post_to_invoiced}
/>
</Form.Item>
<Form.Item
name="employeeteamid"
label={t("timetickets.fields.employee_team")}
>
<EmployeeTeamSearchSelectComponent />
</Form.Item>
<Form.Item
name="hourstype"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Checkbox.Group>
<Space wrap>
<Checkbox value="LAB" style={{ display: "flex" }}>
{t("jobs.fields.lab")}
</Checkbox>
<Checkbox value="LAR" style={{ display: "flex" }}>
{t("jobs.fields.lar")}
</Checkbox>
<Checkbox value="LAM" style={{ display: "flex" }}>
{t("jobs.fields.lam")}
</Checkbox>
<Checkbox value="LAF" style={{ display: "flex" }}>
{t("jobs.fields.laf")}
</Checkbox>
<Checkbox value="LAG" style={{ display: "flex" }}>
{t("jobs.fields.lag")}
</Checkbox>
</Space>
</Checkbox.Group>
</Form.Item>
<Space wrap align="start">
<Form.Item
name="percent"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<InputNumber min={0} max={100} precision={1} addonAfter="%" />
<Space wrap>
<Form.Item name="task" label={t("timetickets.labels.task")}>
<Radio.Group
optionType="button"
options={bodyshop.md_tasks_presets.presets.map((preset) => ({
value: preset.name,
label: preset.name,
}))}
/>
</Form.Item>
<Form.Item dependencies={["task"]}>
{() => {
const { task } = form.getFieldsValue();
const theTaskPreset = bodyshop.md_tasks_presets.presets.find(
(tp) => tp.name === task
);
<Button onClick={calculateTimeTickets}>
{t("tt_approvals.labels.calculate")}
</Button>
if (!task) return null;
return (
<table className="bill-inventory-table">
<tr>
<td>{t("bodyshop.fields.md_tasks_presets.percent")}</td>
<td>{`${theTaskPreset.percent || 0}%`}</td>
</tr>
<tr>
<td>{t("bodyshop.fields.md_tasks_presets.hourstype")}</td>
<td>{theTaskPreset.hourstype.join(", ")}</td>
</tr>
<tr>
<td>
{t("bodyshop.fields.md_tasks_presets.nextstatus")}
</td>
<td>{theTaskPreset.nextstatus}</td>
</tr>
</table>
);
}}
</Form.Item>
</Space>
</Col>
<Col xl={12} lg={24}>
<Form.Item shouldUpdate>
{() => {
const data = form.getFieldValue("timetickets");
return (
<Table
dataSource={data}
rowKey={"employeeid"}
columns={[
{
title: t("timetickets.fields.employee"),
dataIndex: "employee",
key: "employee",
render: (text, record) => {
const emp = bodyshop.employees.find(
(e) => e.id === record.employeeid
);
return `${emp?.first_name} ${emp?.last_name}`;
},
},
{
title: t("timetickets.fields.cost_center"),
dataIndex: "cost_center",
key: "cost_center",
render: (text, record) =>
record.cost_center === "timetickets.labels.shift"
? t(record.cost_center)
: record.cost_center,
},
{
title: t("timetickets.fields.productivehrs"),
dataIndex: "productivehrs",
key: "productivehrs",
},
{
title: "Percentage",
dataIndex: "percentage",
key: "percentage",
},
{
title: "Rate",
dataIndex: "rate",
key: "rate",
},
// {
// title: "Pay",
// dataIndex: "pay",
// key: "pay",
// },
]}
/>
);
}}
</Form.Item>
<Form.List
name={["timetickets"]}
rules={[
{
validator: (rule, value) => {
//Check the cost center,
const totals = CalculateAllocationsTotals(
bodyshop,
lineTicketData.joblines,
lineTicketData.timetickets,
lineTicketData.jobs_by_pk.lbr_adjustments
);
const grouped = _.groupBy(value, "cost_center");
let error = false;
Object.keys(grouped).forEach((key) => {
const totalProdTicketHours = grouped[key].reduce(
(acc, val) => acc + val.productivehrs,
0
);
const fieldTypeToCheck = "cost_center";
// bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber
// ? "mod_lbr_ty"
// : "cost_center";
const costCenterDiff =
Math.round(
totals.find((total) => total[fieldTypeToCheck] === key)
?.difference * 10
) / 10;
if (totalProdTicketHours > costCenterDiff) error = true;
else {
// return Promise.resolve();
}
});
if (!error) return Promise.resolve();
return Promise.reject(
"Too many hours are being claimed as a part of this task"
);
},
},
]}
>
{(fields, { add, remove, move }, { errors }) => {
return (
<div>
{errors.map((e, idx) => (
<Alert key={idx} message={e} />
))}
<div
style={{
display: "none",
}}
>
{fields.map((field, index) => (
<Form.Item
key={field.key}
style={{ padding: 0, margin: 2 }}
>
<Space wrap>
<Form.Item
label={t("timetickets.fields.employeeid")}
key={`${index}employeeid`}
name={[field.name, "employeeid"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<EmployeeSearchSelectComponent
options={bodyshop.employees}
/>
</Form.Item>
<Form.Item
label={t("timetickets.fields.date")}
key={`${index}date`}
name={[field.name, "date"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<FormDateTimePickerComponent />
</Form.Item>
<Form.Item
label={t("timetickets.fields.productivehrs")}
key={`${index}productivehrs`}
name={[field.name, "productivehrs"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<InputNumber min={0} />
</Form.Item>
<Form.Item
label={t("timetickets.fields.actualhrs")}
key={`${index}actualhrs`}
name={[field.name, "actualhrs"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<InputNumber min={0} />
</Form.Item>
<Form.Item
label={t("timetickets.fields.rate")}
key={`${index}rate`}
name={[field.name, "rate"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<InputNumber />
</Form.Item>
<Form.Item
label={t("timetickets.fields.cost_center")}
key={`${index}cost_center`}
name={[field.name, "cost_center"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("timetickets.fields.memo")}
key={`${index}memo`}
name={[field.name, "memo"]}
>
<Input />
</Form.Item>
</Space>
</Form.Item>
))}
</div>
</div>
);
}}
</Form.List>
{loading ? (
<Skeleton />
) : (
<Form.List name="timetickets">
{(fields, { add, remove, move }) => {
return (
<>
<Typography.Title level={4}>
{t("timetickets.labels.claimtaskpreview")}
</Typography.Title>
<table className="bill-cm-returns-table">
<thead>
<tr>
<th>{t("timetickets.fields.employee")}</th>
<th>{t("timetickets.fields.cost_center")}</th>
<th>{t("timetickets.fields.ciecacode")}</th>
<th>{t("timetickets.fields.productivehrs")}</th>
</tr>
</thead>
<tbody>
{fields.map((field, index) => (
<tr key={field.key}>
<td>
<Form.Item
key={`${index}employeeid`}
name={[field.name, "employeeid"]}
>
<ReadOnlyFormItemComponent type="employee" />
</Form.Item>
</td>
<td>
<Form.Item
key={`${index}cost_center`}
name={[field.name, "cost_center"]}
>
<ReadOnlyFormItemComponent />
</Form.Item>
</td>
<td>
<Form.Item
key={`${index}ciecacode`}
name={[field.name, "ciecacode"]}
>
<ReadOnlyFormItemComponent />
</Form.Item>
</td>
<td>
<Form.Item
key={`${index}productivehrs`}
name={[field.name, "productivehrs"]}
>
<ReadOnlyFormItemComponent />
</Form.Item>
</td>
</tr>
))}
</tbody>
</table>
<Alert
type="success"
message={t("timetickets.labels.payrollclaimedtasks")}
/>
</>
);
}}
</Form.List>
)}
</Col>
</Row>
<Form.Item dependencies={["jobid"]}>
{() => {
const jobid = form.getFieldValue("jobid");
if (
(!lineTicketCalled && jobid) ||
(jobid &&
lineTicketData?.jobs_by_pk?.id !== jobid &&
!lineTicketLoading)
) {
queryJobInfo({ variables: { id: jobid } }).then(() =>
calculateTimeTickets("")
);
}
return (
<LaborAllocationContainer
jobid={jobid || null}
loading={lineTicketLoading}
lineTicketData={lineTicketData}
hideTimeTickets
/>
);
}}
</Form.Item>
{bodyshop?.md_tasks_presets?.use_approvals && (
<Col span={24}>
<Col xl={12} lg={24}>
<Alert
message={t("tt_approvals.labels.approval_queue_in_use")}
type="warning"

View File

@@ -1,21 +1,14 @@
import React, { useEffect } from "react";
import React, { useState } from "react";
import { useLazyQuery, useMutation, useQuery } from "@apollo/client";
import { Form, Modal, notification } from "antd";
import Dinero from "dinero.js";
import _ from "lodash";
import moment from "moment";
import axios from "axios";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { QUERY_ACTIVE_EMPLOYEES } from "../../graphql/employees.queries";
import { GET_JOB_INFO_DRAW_CALCULATIONS } from "../../graphql/jobs-lines.queries";
import { INSERT_NEW_TIME_TICKET } from "../../graphql/timetickets.queries";
import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectTimeTicketTasks } from "../../redux/modals/modals.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import TimeTicketTaskModalComponent from "./time-ticket-task-modal.component";
import { INSERT_NEW_TT_APPROVALS } from "../../graphql/tt-approvals.queries";
const mapStateToProps = createStructuredSelector({
timeTicketTasksModal: selectTimeTicketTasks,
@@ -36,147 +29,48 @@ export function TimeTickeTaskModalContainer({
}) {
const [form] = Form.useForm();
const { context, visible } = timeTicketTasksModal;
const { data: EmployeeAutoCompleteData } = useQuery(QUERY_ACTIVE_EMPLOYEES, {
skip: !visible,
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
const { t } = useTranslation();
const [insertTimeTickets] = useMutation(INSERT_NEW_TIME_TICKET);
const [insertTimeTicketApproval] = useMutation(INSERT_NEW_TT_APPROVALS);
const [queryJobInfo, { called, loading, data: lineTicketData }] =
useLazyQuery(GET_JOB_INFO_DRAW_CALCULATIONS, {
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
const [loading, setLoading] = useState(false);
async function handleFinish(values) {
try {
if (bodyshop.md_tasks_presets.use_approvals) {
const result = await insertTimeTicketApproval({
variables: {
timeTicketInput: values.timetickets.map((ticket) => ({
..._.omit(ticket, "pay"),
bodyshopid: bodyshop.id,
})),
},
});
if (result.errors) {
notification.open({
type: "error",
message: t("timetickets.errors.creating", {
message: JSON.stringify(result.errors),
}),
});
} else {
notification.open({
type: "success",
message: t("timetickets.successes.created"),
});
form.resetFields();
toggleModalVisible();
}
} else {
const result = await insertTimeTickets({
variables: {
timeTicketInput: values.timetickets.map((ticket) =>
_.omit(ticket, "pay")
),
},
refetchQueries: ["GET_LINE_TICKET_BY_PK"]
});
if (result.errors) {
notification.open({
type: "error",
message: t("timetickets.errors.creating", {
message: JSON.stringify(result.errors),
}),
});
} else {
notification.open({
type: "success",
message: t("timetickets.successes.created"),
});
toggleModalVisible();
}
}
} catch (error) {
console.log("🚀 ~ file: time-ticket-task-modal.container.jsx:104 ~ handleFinish ~ error:", error)
notification.open({
type: "error",
message: t("timetickets.errors.creating", {
message: JSON.stringify(error),
}),
});
} finally {
calculateTickets({ values, handleFinish: true });
}
async function handleValueChange(changedValues, allValues) {
if (allValues.jobid && allValues.task) {
calculateTickets({ values: allValues, handleFinish: false });
}
}
useEffect(() => {
if (visible && context.jobid) {
queryJobInfo({ variables: { id: context.jobid } });
}
}, [context.jobid, queryJobInfo, visible]);
const calculateTimeTickets = (presetMemo) => {
const formData = form.getFieldsValue();
if (
!formData.jobid ||
!formData.employeeteamid ||
!formData.hourstype ||
formData.hourstype.length === 0 ||
!formData.percent ||
!lineTicketData
) {
return;
}
let data = [];
let eligibleHours = 0;
const theTeam = JSON.parse(formData.employeeteamid);
if (theTeam) {
formData.hourstype.forEach((hourstype) => {
eligibleHours =
lineTicketData.joblines.reduce(
(acc, val) =>
acc + (hourstype === val.mod_lbr_ty ? val.mod_lb_hrs : 0),
0
) * (formData.percent / 100 || 0);
theTeam.employee_team_members.forEach((e) => {
const newTicket = {
employeeid: e.employeeid,
bodyshopid: bodyshop.id,
date: moment().format("YYYY-MM-DD"),
jobid: formData.jobid,
rate: e.labor_rates[hourstype],
actualhrs: 0,
memo: typeof presetMemo === "string" ? presetMemo : "",
flat_rate: true,
ciecacode: hourstype,
cost_center:
bodyshop.md_responsibility_centers.defaults.costs[hourstype],
productivehrs:
Math.round(eligibleHours * 100 * (e.percentage / 100)) / 100,
pay: Dinero({
amount: Math.round((e.labor_rates[hourstype] || 0) * 100),
})
.multiply(
Math.round(eligibleHours * 100 * (e.percentage / 100)) / 100
)
.toFormat("$0.00"),
};
data.push(newTicket);
const calculateTickets = async ({ values, handleFinish }) => {
setLoading(true);
try {
const { data, ...response } = await axios.post("/payroll/claimtask", {
jobid: values.jobid,
task: values.task,
calculateOnly: !handleFinish,
});
if (response.status === 200 && handleFinish) {
//Close the modal
toggleModalVisible();
} else if (handleFinish === false) {
form.setFieldsValue({ timetickets: data });
} else {
notification.open({
type: "error",
message: t("timetickets.errors.creating", {
message: JSON.stringify(data),
}),
});
}
} catch (error) {
notification.open({
type: "error",
message: t("timetickets.errors.creating", { message: error.message }),
});
form.setFieldsValue({
timetickets: data.filter((d) => d.productivehrs > 0),
});
form.validateFields();
} finally {
setLoading(false);
}
};
@@ -197,18 +91,9 @@ export function TimeTickeTaskModalContainer({
layout="vertical"
onFinish={handleFinish}
initialValues={context}
onValuesChange={handleValueChange}
>
<TimeTicketTaskModalComponent
form={form}
employeeAutoCompleteOptions={
EmployeeAutoCompleteData && EmployeeAutoCompleteData.employees
}
lineTicketData={lineTicketData}
lineTicketLoading={loading}
lineTicketCalled={called}
calculateTimeTickets={calculateTimeTickets}
queryJobInfo={queryJobInfo}
/>
<TimeTicketTaskModalComponent form={form} loading={loading} />
</Form>
</Modal>
);