RO into IO merge as of 02/05/2024.
This commit is contained in:
@@ -0,0 +1,268 @@
|
||||
import {useQuery} from "@apollo/client";
|
||||
import {Button, Form, InputNumber, Modal, Radio, Select, Space, Table, Typography,} from "antd";
|
||||
import Dinero from "dinero.js";
|
||||
import React, {useState} from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {connect} from "react-redux";
|
||||
import {createStructuredSelector} from "reselect";
|
||||
import {GET_JOB_INFO_DRAW_CALCULATIONS} from "../../graphql/jobs-lines.queries";
|
||||
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";
|
||||
|
||||
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,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -1,17 +1,19 @@
|
||||
import {EditFilled} from "@ant-design/icons";
|
||||
import {Card, Space, Table} from "antd";
|
||||
import {EditFilled, SyncOutlined} from "@ant-design/icons";
|
||||
import {Button, Card, Checkbox, Space, Table} from "antd";
|
||||
import dayjs from "../../utils/day";
|
||||
import React, {useMemo, useState} from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {connect} from "react-redux";
|
||||
import {Link} from "react-router-dom";
|
||||
import {createStructuredSelector} from "reselect";
|
||||
import {setModalContext} from "../../redux/modals/modals.actions";
|
||||
import {selectAuthLevel, selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors";
|
||||
import {DateFormatter, DateTimeFormatter} from "../../utils/DateFormatter";
|
||||
import {onlyUnique} from "../../utils/arrayHelper";
|
||||
import {alphaSort, dateSort} from "../../utils/sorters";
|
||||
import RbacWrapper, {HasRbacAccess,} from "../rbac-wrapper/rbac-wrapper.component";
|
||||
import TimeTicketEnterButton from "../time-ticket-enter-button/time-ticket-enter-button.component";
|
||||
import {useSplitTreatments} from "@splitsoftware/splitio-react";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -19,12 +21,14 @@ const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
setTimeTicketTaskContext: (context) =>
|
||||
dispatch(setModalContext({context: context, modal: "timeTicketTask"})),
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(TimeTicketList);
|
||||
|
||||
export function TimeTicketList({
|
||||
bodyshop,
|
||||
setTimeTicketTaskContext,
|
||||
authLevel,
|
||||
currentUser,
|
||||
disabled,
|
||||
@@ -41,6 +45,11 @@ export function TimeTicketList({
|
||||
});
|
||||
|
||||
const {t} = useTranslation();
|
||||
const {treatments: {Enhanced_Payroll}} = useSplitTreatments({
|
||||
attributes: {},
|
||||
names: ["Enhanced_Payroll"],
|
||||
splitKey: bodyshop.imexshopid,
|
||||
});
|
||||
|
||||
const totals = useMemo(() => {
|
||||
if (timetickets)
|
||||
@@ -56,6 +65,14 @@ export function TimeTicketList({
|
||||
}, [timetickets]);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t("timetickets.fields.committed"),
|
||||
dataIndex: "committed_at",
|
||||
key: "committed_at",
|
||||
render: (text, record) => (
|
||||
<Checkbox disabled checked={record.committed_at}/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("timetickets.fields.date"),
|
||||
dataIndex: "date",
|
||||
@@ -113,38 +130,46 @@ export function TimeTicketList({
|
||||
}) || [],
|
||||
onFilter: (value, record) => value.includes(record.cost_center),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ro_number"),
|
||||
dataIndex: "ro_number",
|
||||
key: "ro_number",
|
||||
sorter: (a, b) =>
|
||||
alphaSort(a.job && a.job.ro_number, b.job && b.job.ro_number),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
|
||||
render: (text, record) =>
|
||||
record.job && (
|
||||
<Link to={"/manage/jobs/" + record.job.id}>
|
||||
{record.job.ro_number || "N/A"}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
...(jobId
|
||||
? []
|
||||
: [
|
||||
{
|
||||
title: t("jobs.fields.ro_number"),
|
||||
dataIndex: "ro_number",
|
||||
key: "ro_number",
|
||||
sorter: (a, b) =>
|
||||
alphaSort(a.job && a.job.ro_number, b.job && b.job.ro_number),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "ro_number" &&
|
||||
state.sortedInfo.order,
|
||||
render: (text, record) =>
|
||||
record.job && (
|
||||
<Link to={"/manage/jobs/" + record.job.id}>
|
||||
{record.job.ro_number || "N/A"}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
]),
|
||||
{
|
||||
title: t("timetickets.fields.productivehrs"),
|
||||
dataIndex: "productivehrs",
|
||||
key: "productivehrs",
|
||||
sorter: (a, b) => a.productivehrs - b.productivehrs,
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "productivehrs" &&
|
||||
state.sortedInfo.order,
|
||||
},
|
||||
{
|
||||
title: t("timetickets.fields.actualhrs"),
|
||||
dataIndex: "actualhrs",
|
||||
key: "actualhrs",
|
||||
sorter: (a, b) => a.actualhrs - b.actualhrs,
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "actualhrs" && state.sortedInfo.order,
|
||||
sortOrder: state.sortedInfo.columnKey === "productivehrs" && state.sortedInfo.order,
|
||||
},
|
||||
...(Enhanced_Payroll.treatment === "on"
|
||||
? []
|
||||
: [
|
||||
{
|
||||
title: t("timetickets.fields.actualhrs"),
|
||||
dataIndex: "actualhrs",
|
||||
key: "actualhrs",
|
||||
sorter: (a, b) => a.actualhrs - b.actualhrs,
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "actualhrs" &&
|
||||
state.sortedInfo.order,
|
||||
},
|
||||
]),
|
||||
{
|
||||
title: t("timetickets.fields.memo"),
|
||||
dataIndex: "memo",
|
||||
@@ -155,97 +180,127 @@ export function TimeTicketList({
|
||||
render: (text, record) =>
|
||||
record.clockon || record.clockoff ? t(record.memo) : record.memo,
|
||||
},
|
||||
{
|
||||
title: t("timetickets.fields.clockon"),
|
||||
dataIndex: "clockon",
|
||||
key: "clockon",
|
||||
...(Enhanced_Payroll.treatment === "on"
|
||||
? [
|
||||
{
|
||||
title: t("timetickets.fields.task_name"),
|
||||
dataIndex: "task_name",
|
||||
key: "task_name",
|
||||
sorter: (a, b) => alphaSort(a.task_name, b.task_name),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "task_name" &&
|
||||
state.sortedInfo.order,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(Enhanced_Payroll.treatment === "on"
|
||||
? []
|
||||
: [
|
||||
{
|
||||
title: t("timetickets.fields.clockon"),
|
||||
dataIndex: "clockon",
|
||||
key: "clockon",
|
||||
|
||||
render: (text, record) => (
|
||||
<DateTimeFormatter>{record.clockon}</DateTimeFormatter>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("timetickets.fields.clockoff"),
|
||||
dataIndex: "clockoff",
|
||||
key: "clockoff",
|
||||
render: (text, record) => (
|
||||
<DateTimeFormatter>{record.clockon}</DateTimeFormatter>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("timetickets.fields.clockoff"),
|
||||
dataIndex: "clockoff",
|
||||
key: "clockoff",
|
||||
|
||||
render: (text, record) => (
|
||||
<DateTimeFormatter>{record.clockoff}</DateTimeFormatter>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("timetickets.fields.clockhours"),
|
||||
dataIndex: "clockhours",
|
||||
key: "clockhours",
|
||||
render: (text, record) => {
|
||||
if (record.clockoff && record.clockon)
|
||||
return (
|
||||
<div>
|
||||
{dayjs(record.clockoff)
|
||||
.diff(dayjs(record.clockon), "hour", true)
|
||||
.toFixed(2)}
|
||||
</div>
|
||||
);
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t("timetickets.fields.created_by"),
|
||||
dataIndex: "created_by",
|
||||
key: "created_by",
|
||||
sorter: (a, b) => alphaSort(a.created_by, b.created_by),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "created_by" && state.sortedInfo.order,
|
||||
render: (text, record) => record.created_by,
|
||||
},
|
||||
{
|
||||
title: t("general.labels.actions"),
|
||||
dataIndex: "actions",
|
||||
key: "actions",
|
||||
render: (text, record) => (
|
||||
<Space wrap>
|
||||
{techConsole && (
|
||||
<TimeTicketEnterButton
|
||||
actions={{refetch}}
|
||||
context={{id: record.id, timeticket: record}}
|
||||
disabled={!record.job || disabled}
|
||||
>
|
||||
<EditFilled/>
|
||||
</TimeTicketEnterButton>
|
||||
)}
|
||||
{!techConsole && (
|
||||
<RbacWrapper
|
||||
action="timetickets:edit"
|
||||
noauth={() => {
|
||||
return <div/>;
|
||||
}}
|
||||
>
|
||||
<TimeTicketEnterButton
|
||||
actions={{refetch}}
|
||||
context={{
|
||||
id: record.id,
|
||||
timeticket: record,
|
||||
}}
|
||||
disabled={
|
||||
HasRbacAccess({
|
||||
bodyshop,
|
||||
authLevel: authLevel,
|
||||
action: "timetickets:shiftedit",
|
||||
})
|
||||
? disabled
|
||||
: !record.jobid
|
||||
}
|
||||
>
|
||||
<EditFilled/>
|
||||
</TimeTicketEnterButton>
|
||||
</RbacWrapper>
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
render: (text, record) => (
|
||||
<DateTimeFormatter>{record.clockoff}</DateTimeFormatter>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("timetickets.fields.clockhours"),
|
||||
dataIndex: "clockhours",
|
||||
key: "clockhours",
|
||||
render: (text, record) => {
|
||||
if (record.clockoff && record.clockon)
|
||||
return (
|
||||
<div>
|
||||
{dayjs(record.clockoff)
|
||||
.diff(dayjs(record.clockon), "hour", true)
|
||||
.toFixed(2)}
|
||||
</div>
|
||||
);
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t("timetickets.fields.created_by"),
|
||||
dataIndex: "created_by",
|
||||
key: "created_by",
|
||||
sorter: (a, b) => alphaSort(a.created_by, b.created_by),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "created_by" && state.sortedInfo.order,
|
||||
render: (text, record) => record.created_by,
|
||||
},
|
||||
// {
|
||||
// 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"),
|
||||
dataIndex: "actions",
|
||||
key: "actions",
|
||||
render: (text, record) => (
|
||||
<Space wrap>
|
||||
{techConsole && (
|
||||
<TimeTicketEnterButton
|
||||
actions={{refetch}}
|
||||
context={{id: record.id, timeticket: record}}
|
||||
disabled={!record.job || disabled}
|
||||
>
|
||||
<EditFilled/>
|
||||
</TimeTicketEnterButton>
|
||||
)}
|
||||
{!techConsole && (
|
||||
<RbacWrapper
|
||||
action="timetickets:edit"
|
||||
noauth={() => {
|
||||
return <div/>;
|
||||
}}
|
||||
>
|
||||
<TimeTicketEnterButton
|
||||
actions={{refetch}}
|
||||
context={{
|
||||
id: record.id,
|
||||
timeticket: record,
|
||||
}}
|
||||
disabled={
|
||||
HasRbacAccess({
|
||||
bodyshop,
|
||||
authLevel: authLevel,
|
||||
action: "timetickets:editcommitted",
|
||||
}) &&
|
||||
HasRbacAccess({
|
||||
bodyshop,
|
||||
authLevel: authLevel,
|
||||
action: "timetickets:shiftedit",
|
||||
})
|
||||
? disabled
|
||||
: !record.jobid
|
||||
}
|
||||
>
|
||||
<EditFilled/>
|
||||
</TimeTicketEnterButton>
|
||||
</RbacWrapper>
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
])]
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({...state, filteredInfo: filters, sortedInfo: sorter});
|
||||
@@ -256,6 +311,19 @@ export function TimeTicketList({
|
||||
title={t("timetickets.labels.timetickets")}
|
||||
extra={
|
||||
<Space wrap>
|
||||
{jobId && bodyshop.md_tasks_presets.enable_tasks && (
|
||||
<Button
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
setTimeTicketTaskContext({
|
||||
actions: {refetch: refetch},
|
||||
context: {jobid: jobId},
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("timetickets.actions.claimtasks")}
|
||||
</Button>
|
||||
)}
|
||||
{jobId &&
|
||||
(techConsole ? null : (
|
||||
<TimeTicketEnterButton
|
||||
@@ -272,6 +340,13 @@ export function TimeTicketList({
|
||||
</TimeTicketEnterButton>
|
||||
))}
|
||||
{extra}
|
||||
<Button
|
||||
onClick={async () => {
|
||||
refetch();
|
||||
}}
|
||||
>
|
||||
<SyncOutlined/>
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user