Added team pay calculator.
This commit is contained in:
@@ -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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -16,7 +16,9 @@ import { alphaSort, dateSort } from "../../utils/sorters";
|
|||||||
import RbacWrapper, {
|
import RbacWrapper, {
|
||||||
HasRbacAccess,
|
HasRbacAccess,
|
||||||
} from "../rbac-wrapper/rbac-wrapper.component";
|
} from "../rbac-wrapper/rbac-wrapper.component";
|
||||||
|
import Dinero from "dinero.js";
|
||||||
import TimeTicketEnterButton from "../time-ticket-enter-button/time-ticket-enter-button.component";
|
import TimeTicketEnterButton from "../time-ticket-enter-button/time-ticket-enter-button.component";
|
||||||
|
import TimeTicketListTeamPay from "./time-ticket-list-team-pay.component";
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
authLevel: selectAuthLevel,
|
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"),
|
title: t("general.labels.actions"),
|
||||||
@@ -250,6 +261,10 @@ export function TimeTicketList({
|
|||||||
title={t("timetickets.labels.timetickets")}
|
title={t("timetickets.labels.timetickets")}
|
||||||
extra={
|
extra={
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
|
<TimeTicketListTeamPay
|
||||||
|
actions={{ refetch }}
|
||||||
|
context={{ jobId: jobId }}
|
||||||
|
/>
|
||||||
{jobId &&
|
{jobId &&
|
||||||
(techConsole ? null : (
|
(techConsole ? null : (
|
||||||
<TimeTicketEnterButton
|
<TimeTicketEnterButton
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { onlyUnique } from "../../utils/arrayHelper";
|
|||||||
import { alphaSort } from "../../utils/sorters";
|
import { alphaSort } from "../../utils/sorters";
|
||||||
import { TemplateList } from "../../utils/TemplateConstants";
|
import { TemplateList } from "../../utils/TemplateConstants";
|
||||||
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
|
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
|
||||||
|
import Dinero from "dinero.js";
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
});
|
});
|
||||||
@@ -129,6 +129,16 @@ const JobRelatedTicketsTable = ({
|
|||||||
return acc;
|
return acc;
|
||||||
}, 0);
|
}, 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 {
|
return {
|
||||||
id: `${item.jobKey}${costCenter}`,
|
id: `${item.jobKey}${costCenter}`,
|
||||||
costCenter,
|
costCenter,
|
||||||
@@ -136,6 +146,7 @@ const JobRelatedTicketsTable = ({
|
|||||||
actHrs: actHrs.toFixed(1),
|
actHrs: actHrs.toFixed(1),
|
||||||
prodHrs: prodHrs.toFixed(1),
|
prodHrs: prodHrs.toFixed(1),
|
||||||
clockHrs,
|
clockHrs,
|
||||||
|
pay,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@@ -195,6 +206,15 @@ const JobRelatedTicketsTable = ({
|
|||||||
state.sortedInfo.columnKey === "clockHrs" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "clockHrs" && state.sortedInfo.order,
|
||||||
render: (text, record) => record.clockHrs.toFixed(2),
|
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"),
|
title: t("general.labels.actions"),
|
||||||
dataIndex: "actions",
|
dataIndex: "actions",
|
||||||
|
|||||||
Reference in New Issue
Block a user