Added team pay calculator.

This commit is contained in:
Patrick Fic
2023-04-06 10:36:29 -07:00
parent e00c40f2d5
commit 27a67c5f4a
3 changed files with 312 additions and 1 deletions

View File

@@ -0,0 +1,276 @@
import {
Button,
Form,
InputNumber,
Modal,
Radio,
Select,
Space,
Table,
Typography,
} from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import JobSearchSelectComponent from "../job-search-select/job-search-select.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import Dinero from "dinero.js";
import { useQuery } from "@apollo/client";
import { GET_JOB_INFO_DRAW_CALCULATIONS } from "../../graphql/jobs-lines.queries";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({});
export default connect(
mapStateToProps,
mapDispatchToProps
)(TimeTicketListTeamPay);
export function TimeTicketListTeamPay({ bodyshop, context, actions }) {
const { refetch } = actions;
const { jobId } = context;
const [visible, setVisible] = useState(false);
const [form] = Form.useForm();
const { t } = useTranslation();
const { loading, data: lineTicketData } = useQuery(
GET_JOB_INFO_DRAW_CALCULATIONS,
{
variables: { id: jobId },
skip: !jobId,
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
}
);
const handleOk = () => {
setVisible(false);
};
return (
<>
<Modal
width={"80%"}
open={visible}
destroyOnClose
onOk={handleOk}
onCancel={() => setVisible(false)}
>
<Form layout="vertical" form={form} initialValues={{ jobid: jobId }}>
<LayoutFormRow grow noDivider>
<Form.Item shouldUpdate>
{() => (
<Form.Item
name="jobid"
label={t("timetickets.fields.ro_number")}
rules={[
{
required: !(
form.getFieldValue("cost_center") ===
"timetickets.labels.shift"
),
//message: t("general.validation.required"),
},
]}
>
<JobSearchSelectComponent
convertedOnly={!bodyshop.tt_allow_post_to_invoiced}
notExported={!bodyshop.tt_allow_post_to_invoiced}
/>
</Form.Item>
)}
</Form.Item>
<Form.Item
label={t("timetickets.fields.date")}
name="date"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<FormDatePicker />
</Form.Item>
</LayoutFormRow>
<LayoutFormRow grow noDivider>
<Form.Item name="team" label="Team">
<Select
options={Teams.map((team) => ({
value: team.name,
label: team.name,
}))}
/>
</Form.Item>
<Form.Item name="hourstype">
<Radio.Group>
<Radio value="LAB">Body</Radio>
<Radio value="LAR">Refinish</Radio>
<Radio value="LAM">Mechanical</Radio>
<Radio value="LAF">Frame</Radio>
<Radio value="LAG">Glass</Radio>
</Radio.Group>
</Form.Item>
<Form.Item name="percent">
<InputNumber min={0} max={100} precision={1} addonAfter="%" />
</Form.Item>
</LayoutFormRow>
<Form.Item shouldUpdate noStyle>
{({ getFieldsValue }) => {
const formData = getFieldsValue();
let data = [];
let eligibleHours = 0;
const theTeam = Teams.find((team) => team.name === formData.team);
if (theTeam) {
eligibleHours =
lineTicketData.joblines.reduce(
(acc, val) =>
acc +
(formData.hourstype === val.mod_lbr_ty
? val.mod_lb_hrs
: 0),
0
) * (formData.percent / 100 || 0);
data = theTeam.employees.map((e) => {
return {
employeeid: e.employeeid,
percentage: e.percentage,
rate: e.rates[formData.hourstype],
cost_center:
bodyshop.md_responsibility_centers.defaults.costs[
formData.hourstype
],
productivehrs:
Math.round(eligibleHours * 100 * (e.percentage / 100)) /
100,
pay: Dinero({
amount: Math.round(
(e.rates[formData.hourstype] || 0) * 100
),
})
.multiply(
Math.round(eligibleHours * 100 * (e.percentage / 100)) /
100
)
.toFormat("$0.00"),
};
});
}
return (
<Table
dataSource={data}
rowKey={"employeeid"}
title={() => (
<Space>
<Typography.Title level={4}>
Tickets to be Created
</Typography.Title>
<span>{`(${eligibleHours} hours to split)`}</span>
</Space>
)}
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>
</Modal>
<Button onClick={() => setVisible(true)}>Assign Team Pay </Button>
</>
);
}
const Teams = [
{
name: "Team A",
employees: [
{
employeeid: "9f1bdc23-8dc2-4b6a-8ca1-5f83a6fdcc22",
percentage: 50,
rates: {
LAB: 10,
LAR: 15,
},
},
{
employeeid: "201db66c-96c7-41ec-bed4-76842ba93087",
percentage: 50,
rates: {
LAB: 20,
LAR: 25,
},
},
],
},
{
name: "Team B",
employees: [
{
employeeid: "9f1bdc23-8dc2-4b6a-8ca1-5f83a6fdcc22",
percentage: 75,
rates: {
LAB: 100,
LAR: 150,
},
},
{
employeeid: "201db66c-96c7-41ec-bed4-76842ba93087",
percentage: 25,
rates: {
LAB: 200,
LAR: 250,
},
},
],
},
];

View File

@@ -16,7 +16,9 @@ import { alphaSort, dateSort } from "../../utils/sorters";
import RbacWrapper, {
HasRbacAccess,
} from "../rbac-wrapper/rbac-wrapper.component";
import Dinero from "dinero.js";
import TimeTicketEnterButton from "../time-ticket-enter-button/time-ticket-enter-button.component";
import TimeTicketListTeamPay from "./time-ticket-list-team-pay.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
authLevel: selectAuthLevel,
@@ -193,6 +195,15 @@ export function TimeTicketList({
}
},
},
{
title: "Pay",
dataIndex: "pay",
key: "pay",
render: (text, record) =>
Dinero({ amount: Math.round(record.rate * 100) })
.multiply(record.flat_rate ? record.productivehrs : record.actualhrs)
.toFormat("$0.00"),
},
{
title: t("general.labels.actions"),
@@ -250,6 +261,10 @@ export function TimeTicketList({
title={t("timetickets.labels.timetickets")}
extra={
<Space wrap>
<TimeTicketListTeamPay
actions={{ refetch }}
context={{ jobId: jobId }}
/>
{jobId &&
(techConsole ? null : (
<TimeTicketEnterButton

View File

@@ -10,7 +10,7 @@ import { onlyUnique } from "../../utils/arrayHelper";
import { alphaSort } from "../../utils/sorters";
import { TemplateList } from "../../utils/TemplateConstants";
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
import Dinero from "dinero.js";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
@@ -129,6 +129,16 @@ const JobRelatedTicketsTable = ({
return acc;
}, 0);
const pay = item.tickets
.filter((ticket) => ticket.cost_center === costCenter)
.reduce((acc, val) => {
return acc.add(
Dinero({ amount: Math.round(val.rate * 100) }).multiply(
val.flat_rate ? val.productivehrs : val.actualhrs
)
);
}, Dinero());
return {
id: `${item.jobKey}${costCenter}`,
costCenter,
@@ -136,6 +146,7 @@ const JobRelatedTicketsTable = ({
actHrs: actHrs.toFixed(1),
prodHrs: prodHrs.toFixed(1),
clockHrs,
pay,
};
});
})
@@ -195,6 +206,15 @@ const JobRelatedTicketsTable = ({
state.sortedInfo.columnKey === "clockHrs" && state.sortedInfo.order,
render: (text, record) => record.clockHrs.toFixed(2),
},
{
title: "Pay",
dataIndex: "Pay",
key: "Pay",
sorter: (a, b) => a.clockHrs - b.clockHrs,
sortOrder:
state.sortedInfo.columnKey === "clockHrs" && state.sortedInfo.order,
render: (text, record) => record.pay.toFormat("$0.00"),
},
{
title: t("general.labels.actions"),
dataIndex: "actions",