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

@@ -4318,6 +4318,48 @@
<folder_node>
<name>dms</name>
<children>
<concept_node>
<name>apcontrol</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>appostingaccount</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>cashierid</name>
<definition_loaded>false</definition_loaded>
@@ -5734,6 +5776,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>nextstatus</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>percent</name>
<definition_loaded>false</definition_loaded>
@@ -46488,6 +46551,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>claimtaskpreview</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>clockhours</name>
<definition_loaded>false</definition_loaded>
@@ -46677,6 +46761,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>payrollclaimedtasks</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>pmbreak</name>
<definition_loaded>false</definition_loaded>
@@ -46782,6 +46887,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>task</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>timetickets</name>
<definition_loaded>false</definition_loaded>

View File

@@ -1,9 +1,26 @@
import Dinero from "dinero.js";
import React, { forwardRef } from "react";
const ReadOnlyFormItem = ({ value, type = "text", onChange }, ref) => {
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
const ReadOnlyFormItem = (
{ bodyshop, value, type = "text", onChange },
ref
) => {
if (!value) return null;
switch (type) {
case "employee":
const emp = bodyshop.employees.find((e) => e.id === value);
return `${emp?.first_name} ${emp?.last_name}`;
case "text":
return <div>{value}</div>;
case "currency":
@@ -14,4 +31,8 @@ const ReadOnlyFormItem = ({ value, type = "text", onChange }, ref) => {
return <div>{value}</div>;
}
};
export default forwardRef(ReadOnlyFormItem);
export default connect(
mapStateToProps,
mapDispatchToProps
)(forwardRef(ReadOnlyFormItem));

View File

@@ -1,5 +1,5 @@
import { EditFilled } from "@ant-design/icons";
import { Button, Card, Col, Row, Space, Table, Typography } from "antd";
import axios from "axios";
import _ from "lodash";
import React, { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
@@ -8,12 +8,7 @@ import { createStructuredSelector } from "reselect";
import { selectTechnician } from "../../redux/tech/tech.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { alphaSort } from "../../utils/sorters";
import LaborAllocationsAdjustmentEdit from "../labor-allocations-adjustment-edit/labor-allocations-adjustment-edit.component";
import "./labor-allocations-table.styles.scss";
import { CalculateAllocationsTotals } from "./labor-allocations-table.utility";
import axios from "axios";
import { onlyUnique } from "../../utils/arrayHelper";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -211,28 +206,33 @@ export function PayrollLaborAllocationsTable({
return (
<Row gutter={[16, 16]}>
<Col span={24}>
<Button
onClick={async () => {
const { data } = await axios.post("/payroll/payall", {
jobid: jobId,
});
refetch();
}}
<Card
title={t("jobs.labels.laborallocations")}
extra={
<Space>
<Button
onClick={async () => {
await axios.post("/payroll/payall", {
jobid: jobId,
});
}}
>
(TODO: Add Label) Pay All
</Button>
<Button
onClick={async () => {
const { data } = await axios.post("/payroll/calculatelabor", {
jobid: jobId,
});
setTotals(data);
refetch();
}}
>
(TODO: Add Label) Calculate Labor
</Button>
</Space>
}
>
Pay All Test
</Button>
<Button
onClick={async () => {
const { data } = await axios.post("/payroll/calculatelabor", {
jobid: jobId,
});
setTotals(data);
refetch();
}}
>
Calculate Labor
</Button>
<Card title={t("jobs.labels.laborallocations")}>
<Table
columns={columns}
rowKey={(record) => `${record.cost_center} ${record.mod_lbr_ty}`}

View File

@@ -7,6 +7,7 @@ import {
Input,
InputNumber,
Row,
Select,
Space,
Switch,
} from "antd";
@@ -15,7 +16,21 @@ import { useTranslation } from "react-i18next";
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
export default function ShopInfoTaskPresets({ form }) {
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(ShopInfoTaskPresets);
export function ShopInfoTaskPresets({ bodyshop, form }) {
const { t } = useTranslation();
return (
@@ -59,6 +74,7 @@ export default function ShopInfoTaskPresets({ form }) {
<Input />
</Form.Item>
<Form.Item
span={12}
label={t("bodyshop.fields.md_tasks_presets.hourstype")}
key={`${index}hourstype`}
name={[field.name, "hourstype"]}
@@ -71,7 +87,15 @@ export default function ShopInfoTaskPresets({ form }) {
>
<Checkbox.Group>
<Row>
<Col span={8}>
<Col span={4}>
<Checkbox
value="LAA"
style={{ lineHeight: "32px" }}
>
{t("joblines.fields.lbr_types.LAA")}
</Checkbox>
</Col>
<Col span={4}>
<Checkbox
value="LAB"
style={{ lineHeight: "32px" }}
@@ -79,23 +103,23 @@ export default function ShopInfoTaskPresets({ form }) {
{t("joblines.fields.lbr_types.LAB")}
</Checkbox>
</Col>
<Col span={8}>
<Col span={4}>
<Checkbox
value="LAR"
value="LAD"
style={{ lineHeight: "32px" }}
>
{t("joblines.fields.lbr_types.LAR")}
{t("joblines.fields.lbr_types.LAD")}
</Checkbox>
</Col>
<Col span={8}>
<Col span={4}>
<Checkbox
value="LAM"
value="LAE"
style={{ lineHeight: "32px" }}
>
{t("joblines.fields.lbr_types.LAM")}
{t("joblines.fields.lbr_types.LAE")}
</Checkbox>
</Col>
<Col span={8}>
<Col span={4}>
<Checkbox
value="LAF"
style={{ lineHeight: "32px" }}
@@ -103,7 +127,7 @@ export default function ShopInfoTaskPresets({ form }) {
{t("joblines.fields.lbr_types.LAF")}
</Checkbox>
</Col>
<Col span={8}>
<Col span={4}>
<Checkbox
value="LAG"
style={{ lineHeight: "32px" }}
@@ -111,12 +135,90 @@ export default function ShopInfoTaskPresets({ form }) {
{t("joblines.fields.lbr_types.LAG")}
</Checkbox>
</Col>
<Col span={4}>
<Checkbox
value="LAM"
style={{ lineHeight: "32px" }}
>
{t("joblines.fields.lbr_types.LAM")}
</Checkbox>
</Col>
<Col span={4}>
<Checkbox
value="LAM"
style={{ lineHeight: "32px" }}
>
{t("joblines.fields.lbr_types.LAM")}
</Checkbox>
</Col>
<Col span={4}>
<Checkbox
value="LAR"
style={{ lineHeight: "32px" }}
>
{t("joblines.fields.lbr_types.LAR")}
</Checkbox>
</Col>
<Col span={4}>
<Checkbox
value="LAS"
style={{ lineHeight: "32px" }}
>
{t("joblines.fields.lbr_types.LAS")}
</Checkbox>
</Col>
<Col span={4}>
<Checkbox
value="LAU"
style={{ lineHeight: "32px" }}
>
{t("joblines.fields.lbr_types.LAU")}
</Checkbox>
</Col>
<Col span={4}>
<Checkbox
value="LA1"
style={{ lineHeight: "32px" }}
>
{t("joblines.fields.lbr_types.LA1")}
</Checkbox>
</Col>
<Col span={4}>
<Checkbox
value="LA2"
style={{ lineHeight: "32px" }}
>
{t("joblines.fields.lbr_types.LA2")}
</Checkbox>
</Col>
<Col span={4}>
<Checkbox
value="LA3"
style={{ lineHeight: "32px" }}
>
{t("joblines.fields.lbr_types.LA3")}
</Checkbox>
</Col>
<Col span={4}>
<Checkbox
value="LA4"
style={{ lineHeight: "32px" }}
>
{t("joblines.fields.lbr_types.LA4")}
</Checkbox>
</Col>
</Row>
</Checkbox.Group>
</Form.Item>
<Form.Item
label={t("bodyshop.fields.md_tasks_presets.percent")}
key={`${index}percent`}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
name={[field.name, "percent"]}
>
<InputNumber min={0} max={100} />
@@ -128,6 +230,17 @@ export default function ShopInfoTaskPresets({ form }) {
>
<Input />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.md_tasks_presets.nextstatus")}
key={`${index}nextstatus`}
name={[field.name, "nextstatus"]}
>
<Select
options={bodyshop.md_ro_statuses.production_statuses.map(
(o) => ({ value: o, label: o })
)}
/>
</Form.Item>
<Space wrap>
<DeleteFilled
onClick={() => {

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>
);

View File

@@ -1,30 +0,0 @@
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { useTranslation } from "react-i18next";
import { Button, Dropdown } from "antd";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(TimeTicketTaskCollector);
export function TimeTicketTaskCollector({ form, bodyshop }) {
const { t } = useTranslation();
const items = [];
return (
<Dropdown menu={{ items }}>
<Button>{t("timetickets.actions.tasks")}</Button>
</Dropdown>
);
}

View File

@@ -1,72 +0,0 @@
import { Button, Dropdown } from "antd";
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(TimeTicketsTasksPresets);
export function TimeTicketsTasksPresets({
bodyshop,
form,
calculateTimeTickets,
}) {
const handleClick = (props) => {
const preset = bodyshop.md_tasks_presets?.presets?.find((p) => {
return p.name === props.key;
});
if (preset) {
form.setFieldsValue({
percent: preset.percent,
hourstype: preset.hourstype,
});
calculateTimeTickets(preset.memo);
}
};
return (
<Dropdown
trigger="click"
menu={{
items: bodyshop.md_tasks_presets?.presets
? bodyshop.md_tasks_presets?.presets?.map((p) => ({
label: p.name,
key: p.name,
}))
: [],
onClick: handleClick,
}}
>
<Button>Presets</Button>
</Dropdown>
);
}
// const samplePresets = [
// {
// name: "Teardown",
// hourstype: ["LAB", "LAM"],
// percent: 10,
// memo: "Teardown Preset Task",
// },
// {
// name: "Disassembly",
// hourstype: ["LAB", "LAD"],
// percent: 20,
// memo: "Disassy Preset Claim",
// },
// { name: "Body", hourstype: ["LAB", "LAD"], percent: 20 },
// { name: "Prep", hourstype: ["LAR"], percent: 20 },
// ];

View File

@@ -269,6 +269,8 @@
"templates": "Delivery Templates"
},
"dms": {
"apcontrol": "",
"appostingaccount": "",
"cashierid": "Cashier ID",
"default_journal": "Default Journal",
"disablebillwip": "Disable bill WIP for A/P Posting",
@@ -347,6 +349,7 @@
"hourstype": "Hour Types",
"memo": "Time Ticket Memo",
"name": "Preset Name",
"nextstatus": "Next Status",
"percent": "Percent",
"use_approvals": "Use Time Ticket Approval Queue"
},
@@ -2759,6 +2762,7 @@
"alreadyclockedon": "You are already clocked in to the following job(s):",
"ambreak": "AM Break",
"amshift": "AM Shift",
"claimtaskpreview": "Claimed Tasks Preview",
"clockhours": "Shift Clock Hours Summary",
"clockintojob": "Clock In to Job",
"deleteconfirm": "Are you sure you want to delete this time ticket? This cannot be undone.",
@@ -2768,11 +2772,13 @@
"jobhours": "Job Related Time Tickets Summary",
"lunch": "Lunch",
"new": "New Time Ticket",
"payrollclaimedtasks": "These time tickets will be automatically entered to the system as a part of claiming this task. These numbers are calculated using the jobs assigned lines. If lines are unassigned, they will be excluded from created tickets.",
"pmbreak": "PM Break",
"pmshift": "PM Shift",
"shift": "Shift",
"shiftalreadyclockedon": "Active Shift Time Tickets",
"straight_time": "Straight Time",
"task": "Task",
"timetickets": "Time Tickets",
"zeroactualnegativeprod": "Actual hours must be 0 if entering negative productive hours."
},

View File

@@ -269,6 +269,8 @@
"templates": ""
},
"dms": {
"apcontrol": "",
"appostingaccount": "",
"cashierid": "",
"default_journal": "",
"disablebillwip": "",
@@ -347,6 +349,7 @@
"hourstype": "",
"memo": "",
"name": "",
"nextstatus": "",
"percent": "",
"use_approvals": ""
},
@@ -2759,6 +2762,7 @@
"alreadyclockedon": "",
"ambreak": "",
"amshift": "",
"claimtaskpreview": "",
"clockhours": "",
"clockintojob": "",
"deleteconfirm": "",
@@ -2768,11 +2772,13 @@
"jobhours": "",
"lunch": "",
"new": "",
"payrollclaimedtasks": "",
"pmbreak": "",
"pmshift": "",
"shift": "",
"shiftalreadyclockedon": "",
"straight_time": "",
"task": "",
"timetickets": "",
"zeroactualnegativeprod": ""
},

View File

@@ -269,6 +269,8 @@
"templates": ""
},
"dms": {
"apcontrol": "",
"appostingaccount": "",
"cashierid": "",
"default_journal": "",
"disablebillwip": "",
@@ -347,6 +349,7 @@
"hourstype": "",
"memo": "",
"name": "",
"nextstatus": "",
"percent": "",
"use_approvals": ""
},
@@ -2759,6 +2762,7 @@
"alreadyclockedon": "",
"ambreak": "",
"amshift": "",
"claimtaskpreview": "",
"clockhours": "",
"clockintojob": "",
"deleteconfirm": "",
@@ -2768,11 +2772,13 @@
"jobhours": "",
"lunch": "",
"new": "",
"payrollclaimedtasks": "",
"pmbreak": "",
"pmshift": "",
"shift": "",
"shiftalreadyclockedon": "",
"straight_time": "",
"task": "",
"timetickets": "",
"zeroactualnegativeprod": ""
},

View File

@@ -268,6 +268,7 @@ app.post(
payroll.calculatelabor
);
app.post("/payroll/payall", fb.validateFirebaseIdToken, payroll.payall);
app.post("/payroll/claimtask", fb.validateFirebaseIdToken, payroll.claimtask);
var ioevent = require("./server/ioevent/ioevent");
app.post("/ioevent", ioevent.default);

View File

@@ -1828,6 +1828,7 @@ exports.QUERY_JOB_PAYROLL_DATA = `query QUERY_JOB_PAYROLL_DATA($id: uuid!) {
bodyshop{
id
md_responsibility_centers
md_tasks_presets
employee_teams{
id
name
@@ -1909,6 +1910,8 @@ exports.QUERY_JOB_PAYROLL_DATA = `query QUERY_JOB_PAYROLL_DATA($id: uuid!) {
misc_amt
misc_tax
assigned_team
convertedtolbr
convertedtolbr_data
}
}
}`;

View File

@@ -0,0 +1,88 @@
const Dinero = require("dinero.js");
const queries = require("../graphql-client/queries");
const GraphQLClient = require("graphql-request").GraphQLClient;
const logger = require("../utils/logger");
const {
CalculateExpectedHoursForJob,
CalculateTicketsHoursForJob,
} = require("./pay-all");
// Dinero.defaultCurrency = "USD";
// Dinero.globalLocale = "en-CA";
Dinero.globalRoundingMode = "HALF_EVEN";
exports.claimtask = async function (req, res) {
const BearerToken = req.headers.authorization;
const { jobid, task, calculateOnly } = req.body;
logger.log("job-payroll-pay-all", "DEBUG", req.user.email, jobid, null);
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {
headers: {
Authorization: BearerToken,
},
});
try {
const { jobs_by_pk: job } = await client
.setHeaders({ Authorization: BearerToken })
.request(queries.QUERY_JOB_PAYROLL_DATA, {
id: jobid,
});
const theTaskPreset = job.bodyshop.md_tasks_presets.presets.find(
(tp) => tp.name === task
);
//Get all of the assignments that are filtered.
const { employeeHash } = CalculateExpectedHoursForJob(
job,
theTaskPreset.hourstype
);
const ticketsToInsert = [];
//Then add them in based on a percentage to each employee.
Object.keys(employeeHash).forEach((employeeIdKey) => {
//At the employee level.
Object.keys(employeeHash[employeeIdKey]).forEach((laborTypeKey) => {
//At the labor level
Object.keys(employeeHash[employeeIdKey][laborTypeKey]).forEach(
(rateKey) => {
//At the rate level.
const expectedHours =
employeeHash[employeeIdKey][laborTypeKey][rateKey] *
(theTaskPreset.percent / 100);
ticketsToInsert.push({
jobid: job.id,
bodyshopid: job.bodyshop.id,
employeeid: employeeIdKey,
productivehrs: expectedHours,
rate: rateKey,
ciecacode: laborTypeKey,
flat_rate: true,
cost_center:
job.bodyshop.md_responsibility_centers.defaults.costs[
laborTypeKey
],
memo: `*Claimed Task* ${theTaskPreset.memo}`,
});
}
);
});
});
if (!calculateOnly) {
//Insert the time ticekts if we're not just calculating them.
const insertResult = await client.request(queries.INSERT_TIME_TICKETS, {
timetickets: ticketsToInsert.filter(
(ticket) => ticket.productivehrs !== 0
),
});
}
res.json(ticketsToInsert);
} catch (error) {
logger.log("job-payroll-claim-task-error", "ERROR", req.user.email, jobid, {
jobid: jobid,
error,
});
res.status(503).send();
}
};

View File

@@ -85,9 +85,6 @@ exports.payall = async function (req, res) {
} else if (diff.op === "update") {
} else {
//Has to be a delete
//////
if (
typeof diff.oldVal === "object" &&
Object.keys(diff.oldVal).length > 1
@@ -211,58 +208,75 @@ function diffParser(diff) {
return ret;
}
function CalculateExpectedHoursForJob(job) {
function CalculateExpectedHoursForJob(job, filterToLbrTypes) {
const assignmentHash = { unassigned: 0 };
const employeeHash = {}; // employeeid => Cieca labor type => rate => hours. Contains how many hours each person should be paid.
job.joblines.forEach((jobline) => {
if (jobline.mod_lb_hrs != 0) {
//Check if the line is assigned. If not, keep track of it as an unassigned line by type.
if (jobline.assigned_team === null) {
assignmentHash.unassigned =
assignmentHash.unassigned + jobline.mod_lb_hrs;
} else {
//Line is assigned.
if (!assignmentHash[jobline.assigned_team]) {
assignmentHash[jobline.assigned_team] = 0;
}
assignmentHash[jobline.assigned_team] =
assignmentHash[jobline.assigned_team] + jobline.mod_lb_hrs;
//Create the assignment breakdown.
const theTeam = job.bodyshop.employee_teams.find(
(team) => team.id === jobline.assigned_team
job.joblines
.filter((jobline) => {
if (!filterToLbrTypes) return true;
else {
return (
filterToLbrTypes.includes(jobline.mod_lbr_ty) ||
(jobline.convertedtolbr &&
filterToLbrTypes.includes(jobline.convertedtolbr_data.mod_lbr_ty))
);
theTeam.employee_team_members.forEach((tm) => {
//Figure out how many hours they are owed at this line, and at what rate.
if (!employeeHash[tm.employee.id]) {
employeeHash[tm.employee.id] = {};
}
if (!employeeHash[tm.employee.id][jobline.mod_lbr_ty]) {
employeeHash[tm.employee.id][jobline.mod_lbr_ty] = {};
}
if (
!employeeHash[tm.employee.id][jobline.mod_lbr_ty][
tm.labor_rates[jobline.mod_lbr_ty]
]
) {
employeeHash[tm.employee.id][jobline.mod_lbr_ty][
tm.labor_rates[jobline.mod_lbr_ty]
] = 0;
}
const hoursOwed = (tm.percentage * jobline.mod_lb_hrs) / 100;
employeeHash[tm.employee.id][jobline.mod_lbr_ty][
tm.labor_rates[jobline.mod_lbr_ty]
] =
employeeHash[tm.employee.id][jobline.mod_lbr_ty][
tm.labor_rates[jobline.mod_lbr_ty]
] + hoursOwed;
});
}
}
});
})
.forEach((jobline) => {
if (jobline.convertedtolbr) {
// Line has been converte to labor. Temporarily re-assign the hours.
jobline.mod_lbr_ty =
jobline.mod_lbr_ty || jobline.convertedtolbr_data.mod_lbr_ty;
jobline.mod_lb_hrs += jobline.convertedtolbr_data.mod_lb_hrs;
}
if (jobline.mod_lb_hrs != 0) {
//Check if the line is assigned. If not, keep track of it as an unassigned line by type.
if (jobline.assigned_team === null) {
assignmentHash.unassigned =
assignmentHash.unassigned + jobline.mod_lb_hrs;
} else {
//Line is assigned.
if (!assignmentHash[jobline.assigned_team]) {
assignmentHash[jobline.assigned_team] = 0;
}
assignmentHash[jobline.assigned_team] =
assignmentHash[jobline.assigned_team] + jobline.mod_lb_hrs;
//Create the assignment breakdown.
const theTeam = job.bodyshop.employee_teams.find(
(team) => team.id === jobline.assigned_team
);
theTeam.employee_team_members.forEach((tm) => {
//Figure out how many hours they are owed at this line, and at what rate.
if (!employeeHash[tm.employee.id]) {
employeeHash[tm.employee.id] = {};
}
if (!employeeHash[tm.employee.id][jobline.mod_lbr_ty]) {
employeeHash[tm.employee.id][jobline.mod_lbr_ty] = {};
}
if (
!employeeHash[tm.employee.id][jobline.mod_lbr_ty][
tm.labor_rates[jobline.mod_lbr_ty]
]
) {
employeeHash[tm.employee.id][jobline.mod_lbr_ty][
tm.labor_rates[jobline.mod_lbr_ty]
] = 0;
}
const hoursOwed = (tm.percentage * jobline.mod_lb_hrs) / 100;
employeeHash[tm.employee.id][jobline.mod_lbr_ty][
tm.labor_rates[jobline.mod_lbr_ty]
] =
employeeHash[tm.employee.id][jobline.mod_lbr_ty][
tm.labor_rates[jobline.mod_lbr_ty]
] + hoursOwed;
});
}
}
});
return { assignmentHash, employeeHash };
}

View File

@@ -1,2 +1,3 @@
exports.calculatelabor = require("./calculate-totals").calculatelabor;
exports.payall = require("./pay-all").payall;
exports.claimtask = require("./claim-task").claimtask;