Merge branch 'feature/payroll' into feature/america
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { Select } from "antd";
|
||||
import React, { forwardRef } from "react";
|
||||
import { QUERY_TEAMS } from "../../graphql/employee_teams.queries";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
|
||||
//To be used as a form element only.
|
||||
|
||||
const EmployeeTeamSearchSelect = ({ ...props }, ref) => {
|
||||
const { loading, error, data } = useQuery(QUERY_TEAMS);
|
||||
|
||||
if (error) return <AlertComponent message={JSON.stringify(error)} />;
|
||||
return (
|
||||
<Select
|
||||
showSearch
|
||||
allowClear
|
||||
loading={loading}
|
||||
style={{
|
||||
width: 400,
|
||||
}}
|
||||
options={
|
||||
data
|
||||
? data.employee_teams.map((e) => ({
|
||||
value: JSON.stringify(e),
|
||||
label: e.name,
|
||||
}))
|
||||
: []
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export default forwardRef(EmployeeTeamSearchSelect);
|
||||
@@ -47,6 +47,8 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
dispatch(setModalContext({ context: context, modal: "jobCosting" })),
|
||||
setTimeTicketContext: (context) =>
|
||||
dispatch(setModalContext({ context: context, modal: "timeTicket" })),
|
||||
setTimeTicketTaskContext: (context) =>
|
||||
dispatch(setModalContext({ context: context, modal: "timeTicketTask" })),
|
||||
setCardPaymentContext: (context) =>
|
||||
dispatch(setModalContext({ context: context, modal: "cardPayment" })),
|
||||
});
|
||||
@@ -62,6 +64,7 @@ export function JobsDetailHeaderActions({
|
||||
setJobCostingContext,
|
||||
jobRO,
|
||||
setTimeTicketContext,
|
||||
setTimeTicketTaskContext,
|
||||
setCardPaymentContext,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
@@ -247,6 +250,21 @@ export function JobsDetailHeaderActions({
|
||||
>
|
||||
{t("timetickets.actions.enter")}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
key="claimtimetickettasks"
|
||||
disabled={
|
||||
!job.converted ||
|
||||
(!bodyshop.tt_allow_post_to_invoiced && job.date_invoiced)
|
||||
}
|
||||
onClick={() => {
|
||||
setTimeTicketTaskContext({
|
||||
actions: {},
|
||||
context: { jobid: job.id },
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("timetickets.actions.claimtasks")}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
key="enterpayments"
|
||||
disabled={!job.converted}
|
||||
|
||||
@@ -6,10 +6,6 @@ export const CalculateAllocationsTotals = (
|
||||
timetickets,
|
||||
adjustments = []
|
||||
) => {
|
||||
console.log(
|
||||
"🚀 ~ file: labor-allocations-table.utility.js ~ line 9 ~ adjustments",
|
||||
adjustments
|
||||
);
|
||||
const responsibilitycenters = bodyshop.md_responsibility_centers;
|
||||
const jobCodes = joblines.map((item) => item.mod_lbr_ty);
|
||||
//.filter((value, index, self) => self.indexOf(value) === index && !!value);
|
||||
|
||||
@@ -91,11 +91,13 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
|
||||
b.v_make_desc + b.v_model_desc
|
||||
),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "ownr" && state.sortedInfo.order,
|
||||
state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<span>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
|
||||
record.v_model_desc || ""
|
||||
} ${record.v_color || ""} ${record.plate_no || ""}`}</span>
|
||||
<Link to={`/manage/vehicles/${record.vehicleid}`}>{`${
|
||||
record.v_model_yr || ""
|
||||
} ${record.v_make_desc || ""} ${record.v_model_desc || ""} ${
|
||||
record.v_color || ""
|
||||
} ${record.plate_no || ""}`}</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -81,7 +81,7 @@ export function ProductionListTable({
|
||||
state,
|
||||
activeStatuses: bodyshop.md_ro_statuses.active_statuses,
|
||||
}).find((e) => e.key === k.key),
|
||||
width: k.width,
|
||||
width: k.width ?? 100,
|
||||
};
|
||||
})) ||
|
||||
[]
|
||||
@@ -267,6 +267,8 @@ export function ProductionListTable({
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === c.key && state.sortedInfo.order,
|
||||
title: headerItem(c),
|
||||
ellipsis: true,
|
||||
width: c.width ?? 100,
|
||||
onHeaderCell: (column) => ({
|
||||
width: column.width,
|
||||
onResize: handleResize(index),
|
||||
@@ -276,11 +278,12 @@ export function ProductionListTable({
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
dataSource={dataSource}
|
||||
// scroll={{ x: true }}
|
||||
scroll={{ x: 1000 }}
|
||||
onChange={handleTableChange}
|
||||
/>
|
||||
</ReactDragListView.DragColumn>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, null)(ProductionListTable);
|
||||
|
||||
@@ -3,8 +3,26 @@ import { Resizable } from "react-resizable";
|
||||
|
||||
export default function ResizableComponent(props) {
|
||||
const { onResize, width, ...restProps } = props;
|
||||
|
||||
if (!width) {
|
||||
return <th {...restProps} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Resizable width={width || 200} height={0} onResize={onResize}>
|
||||
<Resizable
|
||||
width={width || 200}
|
||||
height={0}
|
||||
onResize={onResize}
|
||||
draggableOpts={{ enableUserSelectHack: false }}
|
||||
handle={
|
||||
<span
|
||||
className="react-resizable-handle"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<th {...restProps} />
|
||||
</Resizable>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Card, Statistic } from "antd";
|
||||
import { Card, Divider, Statistic } from "antd";
|
||||
import moment from "moment";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
@@ -41,6 +41,9 @@ export function ScoreboardDayStats({ bodyshop, date, entries }) {
|
||||
label="P"
|
||||
value={paintHrs.toFixed(1)}
|
||||
/>
|
||||
<Divider style={{ margin: 0 }} />
|
||||
|
||||
<Statistic value={(bodyHrs + paintHrs).toFixed(1)} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CalendarOutlined } from "@ant-design/icons";
|
||||
import { Card, Col, Row, Statistic } from "antd";
|
||||
import { Card, Col, Divider, Row, Statistic } from "antd";
|
||||
import _ from "lodash";
|
||||
import moment from "moment";
|
||||
import React, { useMemo } from "react";
|
||||
@@ -177,6 +177,9 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
|
||||
<Statistic value={values.toDatePaint.toFixed(1)} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Divider style={{ margin: 5 }} />
|
||||
</Row>
|
||||
<Row>
|
||||
<Col {...statSpans}></Col>
|
||||
<Col {...statSpans}>
|
||||
@@ -184,14 +187,53 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
|
||||
value={(values.todayPaint + values.todayBody).toFixed(1)}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statSpans}></Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
value={(
|
||||
Util.WeeklyTargetHrs(
|
||||
bodyshop.scoreboard_target.dailyBodyTarget,
|
||||
bodyshop
|
||||
) +
|
||||
Util.WeeklyTargetHrs(
|
||||
bodyshop.scoreboard_target.dailyPaintTarget,
|
||||
bodyshop
|
||||
)
|
||||
).toFixed(1)}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
value={(values.weeklyPaint + values.weeklyBody).toFixed(1)}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statSpans}></Col>
|
||||
<Col {...statSpans}></Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
value={(
|
||||
Util.MonthlyTargetHrs(
|
||||
bodyshop.scoreboard_target.dailyBodyTarget,
|
||||
bodyshop
|
||||
) +
|
||||
Util.MonthlyTargetHrs(
|
||||
bodyshop.scoreboard_target.dailyPaintTarget,
|
||||
bodyshop
|
||||
)
|
||||
).toFixed(1)}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
value={(
|
||||
Util.AsOfTodayTargetHrs(
|
||||
bodyshop.scoreboard_target.dailyBodyTarget,
|
||||
bodyshop
|
||||
) +
|
||||
Util.AsOfTodayTargetHrs(
|
||||
bodyshop.scoreboard_target.dailyPaintTarget,
|
||||
bodyshop
|
||||
)
|
||||
).toFixed(1)}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
value={(values.toDatePaint + values.toDateBody).toFixed(1)}
|
||||
|
||||
@@ -81,6 +81,7 @@ export default function ScoreboardTimeTickets() {
|
||||
totalLastMonth: 0,
|
||||
totalOverPeriod: 0,
|
||||
actualTotalOverPeriod: 0,
|
||||
totalEffieciencyOverPeriod: 0,
|
||||
employees: {},
|
||||
};
|
||||
data.fixedperiod.forEach((ticket) => {
|
||||
@@ -94,6 +95,7 @@ export default function ScoreboardTimeTickets() {
|
||||
totalLastMonth: 0,
|
||||
totalOverPeriod: 0,
|
||||
actualTotalOverPeriod: 0,
|
||||
totalEffieciencyOverPeriod: 0,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -221,6 +223,28 @@ export default function ScoreboardTimeTickets() {
|
||||
|
||||
ret2.push(r);
|
||||
});
|
||||
|
||||
// Add total efficiency of employees
|
||||
const totalActualAndProductive = Object.keys(ret.employees)
|
||||
.map((key) => {
|
||||
return { employee_number: key, ...ret.employees[key] };
|
||||
})
|
||||
.reduce(
|
||||
(acc, e) => {
|
||||
return {
|
||||
totalOverPeriod: acc.totalOverPeriod + e.totalOverPeriod,
|
||||
actualTotalOverPeriod:
|
||||
acc.actualTotalOverPeriod + e.actualTotalOverPeriod,
|
||||
};
|
||||
},
|
||||
{ totalOverPeriod: 0, actualTotalOverPeriod: 0 }
|
||||
);
|
||||
|
||||
ret.totalEffieciencyOverPeriod =
|
||||
(totalActualAndProductive.totalOverPeriod /
|
||||
totalActualAndProductive.actualTotalOverPeriod) *
|
||||
100;
|
||||
|
||||
roundObject(ret);
|
||||
roundObject(totals);
|
||||
roundObject(ret2);
|
||||
|
||||
@@ -62,7 +62,7 @@ export function ScoreboardTicketsStats({ data, bodyshop }) {
|
||||
key: "efficiencyoverperiod",
|
||||
render: (text, record) =>
|
||||
`${(
|
||||
(record.totalOverPeriod / (record.actualTotalOverPeriod || .1)) *
|
||||
(record.totalOverPeriod / (record.actualTotalOverPeriod || 0.1)) *
|
||||
100
|
||||
).toFixed(1)} %`,
|
||||
},
|
||||
@@ -113,6 +113,12 @@ export function ScoreboardTicketsStats({ data, bodyshop }) {
|
||||
value={data.totalOverPeriod}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Statistic
|
||||
title={t("scoreboard.labels.efficiencyoverperiod")}
|
||||
value={`${data.totalEffieciencyOverPeriod}%`}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Typography.Text type="secondary">
|
||||
{t("scoreboard.labels.calendarperiod")}
|
||||
@@ -121,7 +127,7 @@ export function ScoreboardTicketsStats({ data, bodyshop }) {
|
||||
<Col md={24} lg={20}>
|
||||
<Table
|
||||
columns={columns}
|
||||
rowKey='employee_number'
|
||||
rowKey="employee_number"
|
||||
dataSource={tableData}
|
||||
id="employee_number"
|
||||
scroll={{ y: "300px" }}
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
Switch,
|
||||
Table,
|
||||
} from "antd";
|
||||
import { useForm } from "antd/es/form/Form";
|
||||
|
||||
import moment from "moment";
|
||||
import querystring from "query-string";
|
||||
import React, { useEffect } from "react";
|
||||
@@ -46,7 +46,7 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
|
||||
export function ShopEmployeesFormComponent({ bodyshop }) {
|
||||
const { t } = useTranslation();
|
||||
const [form] = useForm();
|
||||
const [form] = Form.useForm();
|
||||
const history = useHistory();
|
||||
const search = querystring.parse(useLocation().search);
|
||||
const [deleteVacation] = useMutation(DELETE_VACATION);
|
||||
|
||||
@@ -589,6 +589,13 @@ export default function ShopInfoGeneral({ form }) {
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["tt_enforce_hours_for_tech_console"]}
|
||||
label={t("bodyshop.fields.tt_enforce_hours_for_tech_console")}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["bill_allow_post_to_closed"]}
|
||||
label={t("bodyshop.fields.bill_allow_post_to_closed")}
|
||||
|
||||
@@ -388,6 +388,18 @@ export function ShopInfoRbacComponent({ form, bodyshop }) {
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.employee_teams.page")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
name={["md_rbac", "employee_teams:page"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.owners.list")}
|
||||
rules={[
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import React from 'react'
|
||||
|
||||
export default function ShopEmployeeTeamMember({teamMember}) {
|
||||
return (
|
||||
<div>ShopEmployeeTeamMember</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,424 @@
|
||||
import { DeleteFilled } from "@ant-design/icons";
|
||||
import { useMutation, useQuery } from "@apollo/client";
|
||||
import {
|
||||
Button,
|
||||
Space,
|
||||
Card,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Switch,
|
||||
notification,
|
||||
} from "antd";
|
||||
|
||||
import querystring from "query-string";
|
||||
import React, { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { useHistory, useLocation } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
|
||||
import {
|
||||
INSERT_EMPLOYEE_TEAM,
|
||||
QUERY_EMPLOYEE_TEAM_BY_ID,
|
||||
UPDATE_EMPLOYEE_TEAM,
|
||||
} from "../../graphql/employee_teams.queries";
|
||||
import EmployeeSearchSelectComponent from "../employee-search-select/employee-search-select.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
||||
const { t } = useTranslation();
|
||||
const [form] = Form.useForm();
|
||||
const history = useHistory();
|
||||
const search = querystring.parse(useLocation().search);
|
||||
|
||||
const { error, data } = useQuery(QUERY_EMPLOYEE_TEAM_BY_ID, {
|
||||
variables: { id: search.employeeTeamId },
|
||||
skip: !search.employeeTeamId || search.employeeTeamId === "new",
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data && data.employee_teams_by_pk)
|
||||
form.setFieldsValue(data.employee_teams_by_pk);
|
||||
else {
|
||||
form.resetFields();
|
||||
}
|
||||
}, [form, data, search.employeeTeamId]);
|
||||
|
||||
const [updateEmployeeTeam] = useMutation(UPDATE_EMPLOYEE_TEAM);
|
||||
const [insertEmployeeTeam] = useMutation(INSERT_EMPLOYEE_TEAM);
|
||||
|
||||
const handleFinish = async ({ employee_team_members, ...values }) => {
|
||||
if (search.employeeTeamId && search.employeeTeamId !== "new") {
|
||||
//Update a record.
|
||||
logImEXEvent("shop_employee_update");
|
||||
|
||||
const result = await updateEmployeeTeam({
|
||||
variables: {
|
||||
employeeTeamId: search.employeeTeamId,
|
||||
employeeTeam: values,
|
||||
teamMemberUpdates: employee_team_members
|
||||
.filter((e) => e.id)
|
||||
.map((e) => {
|
||||
delete e.__typename;
|
||||
return { where: { id: { _eq: e.id } }, _set: e };
|
||||
}),
|
||||
teamMemberInserts: employee_team_members
|
||||
.filter((e) => e.id === null || e.id === undefined)
|
||||
.map((e) => ({ ...e, teamid: search.employeeTeamId })),
|
||||
teamMemberDeletes:
|
||||
data.employee_teams_by_pk.employee_team_members.filter(
|
||||
(e) => !employee_team_members.find((etm) => etm.id === e.id)
|
||||
),
|
||||
},
|
||||
});
|
||||
if (!result.errors) {
|
||||
notification["success"]({
|
||||
message: t("employees.successes.save"),
|
||||
});
|
||||
} else {
|
||||
notification["error"]({
|
||||
message: t("employees.errors.save", {
|
||||
message: JSON.stringify(error),
|
||||
}),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
//New record, insert it.
|
||||
logImEXEvent("shop_employee_insert");
|
||||
|
||||
insertEmployeeTeam({
|
||||
variables: {
|
||||
employeeTeam: {
|
||||
...values,
|
||||
employee_team_members: { data: employee_team_members },
|
||||
bodyshopid: bodyshop.id,
|
||||
},
|
||||
},
|
||||
refetchQueries: ["QUERY_TEAMS"],
|
||||
}).then((r) => {
|
||||
search.employeeTeamId = r.data.insert_employee_teams_one.id;
|
||||
history.push({ search: querystring.stringify(search) });
|
||||
notification["success"]({
|
||||
message: t("employees.successes.save"),
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (!search.employeeTeamId) return null;
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
|
||||
return (
|
||||
<Card
|
||||
extra={
|
||||
<Button type="primary" onClick={() => form.submit()}>
|
||||
{t("general.actions.save")}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Form
|
||||
onFinish={handleFinish}
|
||||
autoComplete={"off"}
|
||||
layout="vertical"
|
||||
form={form}
|
||||
>
|
||||
<LayoutFormRow>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label={t("employee_teams.fields.name")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("employee_teams.fields.active")}
|
||||
name="active"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<Form.List name={["employee_team_members"]}>
|
||||
{(fields, { add, remove, move }) => {
|
||||
return (
|
||||
<div>
|
||||
{fields.map((field, index) => (
|
||||
<Form.Item key={field.key} style={{ padding: 0, margin: 2 }}>
|
||||
<Form.Item
|
||||
label={t("employees.fields.id")}
|
||||
key={`${index}`}
|
||||
name={[field.name, "id"]}
|
||||
hidden
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<LayoutFormRow grow>
|
||||
<Form.Item
|
||||
label={t("employee_teams.fields.employeeid")}
|
||||
key={`${index}`}
|
||||
name={[field.name, "employeeid"]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<EmployeeSearchSelectComponent
|
||||
options={bodyshop.employees}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("employee_teams.fields.percentage")}
|
||||
key={`${index}`}
|
||||
name={[field.name, "percentage"]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} max={100} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("joblines.fields.lbr_types.LAA")}
|
||||
key={`${index}`}
|
||||
name={[field.name, "labor_rates", "LAA"]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<CurrencyInput />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("joblines.fields.lbr_types.LAB")}
|
||||
key={`${index}`}
|
||||
name={[field.name, "labor_rates", "LAB"]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<CurrencyInput />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("joblines.fields.lbr_types.LAD")}
|
||||
key={`${index}`}
|
||||
name={[field.name, "labor_rates", "LAD"]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<CurrencyInput />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("joblines.fields.lbr_types.LAE")}
|
||||
key={`${index}`}
|
||||
name={[field.name, "labor_rates", "LAE"]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<CurrencyInput />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t("joblines.fields.lbr_types.LAF")}
|
||||
key={`${index}`}
|
||||
name={[field.name, "labor_rates", "LAF"]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<CurrencyInput />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("joblines.fields.lbr_types.LAG")}
|
||||
key={`${index}`}
|
||||
name={[field.name, "labor_rates", "LAG"]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<CurrencyInput />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("joblines.fields.lbr_types.LAM")}
|
||||
key={`${index}`}
|
||||
name={[field.name, "labor_rates", "LAM"]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<CurrencyInput />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("joblines.fields.lbr_types.LAR")}
|
||||
key={`${index}`}
|
||||
name={[field.name, "labor_rates", "LAR"]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<CurrencyInput />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("joblines.fields.lbr_types.LAS")}
|
||||
key={`${index}`}
|
||||
name={[field.name, "labor_rates", "LAS"]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<CurrencyInput />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("joblines.fields.lbr_types.LAU")}
|
||||
key={`${index}`}
|
||||
name={[field.name, "labor_rates", "LAU"]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<CurrencyInput />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("joblines.fields.lbr_types.LA1")}
|
||||
key={`${index}`}
|
||||
name={[field.name, "labor_rates", "LA1"]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<CurrencyInput />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("joblines.fields.lbr_types.LA2")}
|
||||
key={`${index}`}
|
||||
name={[field.name, "labor_rates", "LA2"]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<CurrencyInput />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("joblines.fields.lbr_types.LA3")}
|
||||
key={`${index}`}
|
||||
name={[field.name, "labor_rates", "LA3"]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<CurrencyInput />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("joblines.fields.lbr_types.LA4")}
|
||||
key={`${index}`}
|
||||
name={[field.name, "labor_rates", "LA4"]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<CurrencyInput />
|
||||
</Form.Item>
|
||||
<Space align="center">
|
||||
<DeleteFilled
|
||||
onClick={() => {
|
||||
remove(field.name);
|
||||
}}
|
||||
/>
|
||||
<FormListMoveArrows
|
||||
move={move}
|
||||
index={index}
|
||||
total={fields.length}
|
||||
/>
|
||||
</Space>
|
||||
</LayoutFormRow>
|
||||
</Form.Item>
|
||||
))}
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="dashed"
|
||||
onClick={() => {
|
||||
add();
|
||||
}}
|
||||
style={{ width: "100%" }}
|
||||
>
|
||||
{t("employee_teams.actions.newmember")}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Form.List>
|
||||
</Form>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ShopEmployeeTeamsFormComponent);
|
||||
@@ -0,0 +1,71 @@
|
||||
import { Button, Table } from "antd";
|
||||
import queryString from "query-string";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useHistory, useLocation } from "react-router-dom";
|
||||
|
||||
export default function ShopEmployeeTeamsListComponent({
|
||||
loading,
|
||||
employee_teams,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const history = useHistory();
|
||||
const search = queryString.parse(useLocation().search);
|
||||
|
||||
const handleOnRowClick = (record) => {
|
||||
if (record) {
|
||||
search.employeeTeamId = record.id;
|
||||
history.push({ search: queryString.stringify(search) });
|
||||
} else {
|
||||
delete search.employeeTeamId;
|
||||
history.push({ search: queryString.stringify(search) });
|
||||
}
|
||||
};
|
||||
const columns = [
|
||||
{
|
||||
title: t("employee_teams.fields.name"),
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Table
|
||||
title={() => {
|
||||
return (
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
search.employeeTeamId = "new";
|
||||
history.push({ search: queryString.stringify(search) });
|
||||
}}
|
||||
>
|
||||
{t("employee_teams.actions.new")}
|
||||
</Button>
|
||||
);
|
||||
}}
|
||||
loading={loading}
|
||||
pagination={{ position: "top" }}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
dataSource={employee_teams}
|
||||
rowSelection={{
|
||||
onSelect: (props) => {
|
||||
search.employeeTeamId = props.id;
|
||||
history.push({ search: queryString.stringify(search) });
|
||||
},
|
||||
type: "radio",
|
||||
selectedRowKeys: [search.employeeTeamId],
|
||||
}}
|
||||
onRow={(record, rowIndex) => {
|
||||
return {
|
||||
onClick: (event) => {
|
||||
handleOnRowClick(record);
|
||||
},
|
||||
};
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
43
client/src/components/shop-teams/shop-teams.container.jsx
Normal file
43
client/src/components/shop-teams/shop-teams.container.jsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { useQuery } from "@apollo/client";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { QUERY_TEAMS } from "../../graphql/employee_teams.queries";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
|
||||
import ShopEmployeeTeamsListComponent from "./shop-employee-teams.list";
|
||||
import ShopEmployeeTeamsFormComponent from "./shop-employee-teams.form.component";
|
||||
import { Col, Row } from "antd";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
|
||||
function ShopTeamsContainer({ bodyshop }) {
|
||||
const { loading, error, data } = useQuery(QUERY_TEAMS, {
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only",
|
||||
});
|
||||
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<RbacWrapper action="employee_teams:page">
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={6}>
|
||||
<ShopEmployeeTeamsListComponent
|
||||
employee_teams={data ? data.employee_teams : []}
|
||||
loading={loading}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={18}>
|
||||
<ShopEmployeeTeamsFormComponent />
|
||||
</Col>
|
||||
</Row>
|
||||
</RbacWrapper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default connect(mapStateToProps, null)(ShopTeamsContainer);
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { useMutation, useQuery } from "@apollo/client";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
@@ -21,6 +21,8 @@ import { selectTechnician } from "../../redux/tech/tech.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import TechJobClockoutDelete from "../tech-job-clock-out-delete/tech-job-clock-out-delete.component";
|
||||
import { LaborAllocationContainer } from "../time-ticket-modal/time-ticket-modal.component";
|
||||
import { GET_LINE_TICKET_BY_PK } from "../../graphql/jobs-lines.queries";
|
||||
import { CalculateAllocationsTotals } from "../labor-allocations-table/labor-allocations-table.utility";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -39,7 +41,17 @@ export function TechClockOffButton({
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [updateTimeticket] = useMutation(UPDATE_TIME_TICKET);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const { queryLoading, data: lineTicketData } = useQuery(
|
||||
GET_LINE_TICKET_BY_PK,
|
||||
{
|
||||
variables: {
|
||||
id: jobId,
|
||||
},
|
||||
skip: !jobId,
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only",
|
||||
}
|
||||
);
|
||||
const { t } = useTranslation();
|
||||
const emps = bodyshop.employees.filter(
|
||||
(e) => e.id === (technician && technician.id)
|
||||
@@ -129,6 +141,54 @@ export function TechClockOffButton({
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
({ getFieldValue }) => ({
|
||||
validator(rule, value) {
|
||||
console.log(
|
||||
bodyshop.tt_enforce_hours_for_tech_console
|
||||
);
|
||||
if (!bodyshop.tt_enforce_hours_for_tech_console) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (
|
||||
!value ||
|
||||
getFieldValue("cost_center") === null ||
|
||||
!lineTicketData
|
||||
)
|
||||
return Promise.resolve();
|
||||
|
||||
//Check the cost center,
|
||||
const totals = CalculateAllocationsTotals(
|
||||
bodyshop,
|
||||
lineTicketData.joblines,
|
||||
lineTicketData.timetickets,
|
||||
lineTicketData.jobs_by_pk.lbr_adjustments
|
||||
);
|
||||
|
||||
const fieldTypeToCheck =
|
||||
bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber
|
||||
? "mod_lbr_ty"
|
||||
: "cost_center";
|
||||
|
||||
const costCenterDiff =
|
||||
Math.round(
|
||||
totals.find(
|
||||
(total) =>
|
||||
total[fieldTypeToCheck] ===
|
||||
getFieldValue("cost_center")
|
||||
)?.difference * 10
|
||||
) / 10;
|
||||
|
||||
if (value > costCenterDiff)
|
||||
return Promise.reject(
|
||||
t(
|
||||
"timetickets.validation.hoursenteredmorethanavailable"
|
||||
)
|
||||
);
|
||||
else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
},
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} precision={1} />
|
||||
@@ -178,7 +238,11 @@ export function TechClockOffButton({
|
||||
</Col>
|
||||
{!isShiftTicket && (
|
||||
<Col span={16}>
|
||||
<LaborAllocationContainer jobid={jobId} />
|
||||
<LaborAllocationContainer
|
||||
jobid={jobId || null}
|
||||
loading={queryLoading}
|
||||
lineTicketData={lineTicketData}
|
||||
/>
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
import { DownOutlined } from "@ant-design/icons";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Col,
|
||||
Form,
|
||||
InputNumber,
|
||||
Popover,
|
||||
Radio,
|
||||
Row,
|
||||
Space,
|
||||
Spin,
|
||||
} from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { GET_JOB_INFO_DRAW_CALCULATIONS } from "../../graphql/jobs-lines.queries";
|
||||
import { useQuery } from "@apollo/client";
|
||||
|
||||
export default function TimeTicketCalculatorComponent({
|
||||
setProductiveHours,
|
||||
|
||||
jobid,
|
||||
}) {
|
||||
const { loading, data: lineTicketData } = useQuery(GET_JOB_INFO_DRAW_CALCULATIONS, {
|
||||
variables: { id: jobid },
|
||||
skip: !jobid,
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only",
|
||||
});
|
||||
|
||||
const [visible, setVisible] = useState(false);
|
||||
const handleOpenChange = (flag) => setVisible(flag);
|
||||
const handleFinish = ({ type, hourstype, percent }) => {
|
||||
//setProductiveHours(values);
|
||||
//setVisible(false);
|
||||
const eligibleHours = Array.isArray(hourstype)
|
||||
? lineTicketData.joblines.reduce(
|
||||
(acc, val) =>
|
||||
acc + (hourstype.includes(val.mod_lbr_ty) ? val.mod_lb_hrs : 0),
|
||||
0
|
||||
)
|
||||
: lineTicketData.joblines.reduce(
|
||||
(acc, val) =>
|
||||
acc + (hourstype === val.mod_lbr_ty ? val.mod_lb_hrs : 0),
|
||||
0
|
||||
);
|
||||
if (type === "draw") {
|
||||
setProductiveHours(eligibleHours * (percent / 100));
|
||||
} else if (type === "cut") {
|
||||
setProductiveHours(eligibleHours * (percent / 100));
|
||||
console.log(
|
||||
"Cut selected, rate set to: ",
|
||||
lineTicketData.jobs_by_pk[`rate_${hourstype.toLowerCase()}`]
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const popContent = (
|
||||
<Spin spinning={loading}>
|
||||
<Form onFinish={handleFinish}>
|
||||
<Form.Item name="type">
|
||||
<Radio.Group>
|
||||
<Radio.Button value="draw">Draw</Radio.Button>
|
||||
<Radio.Button value="cut">Cut of Sale</Radio.Button>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item noStyle shouldUpdate>
|
||||
{({ getFieldValue }) => (
|
||||
<Form.Item name="hourstype">
|
||||
{getFieldValue("type") === "draw" ? (
|
||||
<Checkbox.Group>
|
||||
<Row>
|
||||
<Col span={8}>
|
||||
<Checkbox value="LAB" style={{ lineHeight: "32px" }}>
|
||||
Body
|
||||
</Checkbox>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Checkbox value="LAR" style={{ lineHeight: "32px" }}>
|
||||
Refinish
|
||||
</Checkbox>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Checkbox value="LAM" style={{ lineHeight: "32px" }}>
|
||||
Mechanical
|
||||
</Checkbox>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Checkbox value="LAF" style={{ lineHeight: "32px" }}>
|
||||
Frame
|
||||
</Checkbox>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Checkbox value="LAG" style={{ lineHeight: "32px" }}>
|
||||
Glass
|
||||
</Checkbox>
|
||||
</Col>
|
||||
</Row>
|
||||
</Checkbox.Group>
|
||||
) : (
|
||||
<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>
|
||||
|
||||
<Form.Item name="percent">
|
||||
<InputNumber min={0} max={100} precision={1} addonAfter="%" />
|
||||
</Form.Item>
|
||||
<Button htmlType="submit">Calculate</Button>
|
||||
</Form>
|
||||
</Spin>
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
content={popContent}
|
||||
trigger={["click"]}
|
||||
open={visible}
|
||||
onOpenChange={handleOpenChange}
|
||||
placement="right"
|
||||
destroyTooltipOnHide
|
||||
>
|
||||
<Button onClick={(e) => e.preventDefault()}>
|
||||
<Space>
|
||||
Draw Calculator
|
||||
<DownOutlined />
|
||||
</Space>
|
||||
</Button>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
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 { Button, Card, Space, Table } from "antd";
|
||||
import Dinero from "dinero.js";
|
||||
import moment from "moment";
|
||||
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,
|
||||
} from "../../redux/user/user.selectors";
|
||||
import { onlyUnique } from "../../utils/arrayHelper";
|
||||
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import { onlyUnique } from "../../utils/arrayHelper";
|
||||
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||
import RbacWrapper, {
|
||||
HasRbacAccess,
|
||||
@@ -22,12 +24,14 @@ const mapStateToProps = createStructuredSelector({
|
||||
authLevel: selectAuthLevel,
|
||||
});
|
||||
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,
|
||||
disabled,
|
||||
loading,
|
||||
@@ -193,6 +197,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 +263,23 @@ export function TimeTicketList({
|
||||
title={t("timetickets.labels.timetickets")}
|
||||
extra={
|
||||
<Space wrap>
|
||||
{
|
||||
// <TimeTicketListTeamPay
|
||||
// actions={{ refetch }}
|
||||
// context={{ jobId: jobId }}
|
||||
// />
|
||||
}
|
||||
<Button
|
||||
onClick={() => {
|
||||
setTimeTicketTaskContext();
|
||||
setTimeTicketTaskContext({
|
||||
actions: {},
|
||||
context: { jobid: jobId },
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("timetickets.actions.claimtasks")}
|
||||
</Button>
|
||||
{jobId &&
|
||||
(techConsole ? null : (
|
||||
<TimeTicketEnterButton
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { useLazyQuery } from "@apollo/client";
|
||||
import { Form, Input, InputNumber, Select, Switch } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -14,10 +14,12 @@ import FormDatePicker from "../form-date-picker/form-date-picker.component";
|
||||
import FormDateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
|
||||
import JobSearchSelect from "../job-search-select/job-search-select.component";
|
||||
import LaborAllocationsTable from "../labor-allocations-table/labor-allocations-table.component";
|
||||
import { CalculateAllocationsTotals } from "../labor-allocations-table/labor-allocations-table.utility";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||
import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component";
|
||||
import TimeTicketList from "../time-ticket-list/time-ticket-list.component";
|
||||
import TimeTicketCalculatorComponent from "../time-ticket-calculator/time-ticket-calculator.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -38,7 +40,11 @@ export function TimeTicketModalComponent({
|
||||
employeeSelectDisabled,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [loadLineTicketData, { called, loading, data: lineTicketData }] =
|
||||
useLazyQuery(GET_LINE_TICKET_BY_PK, {
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only",
|
||||
});
|
||||
const CostCenterSelect = ({ emps, value, ...props }) => {
|
||||
return (
|
||||
<Select
|
||||
@@ -172,20 +178,73 @@ export function TimeTicketModalComponent({
|
||||
<LayoutFormRow>
|
||||
<Form.Item shouldUpdate>
|
||||
{() => (
|
||||
<Form.Item
|
||||
label={t("timetickets.fields.productivehrs")}
|
||||
name="productivehrs"
|
||||
rules={[
|
||||
{
|
||||
required:
|
||||
form.getFieldValue("cost_center") !==
|
||||
"timetickets.labels.shift",
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber precision={1} />
|
||||
</Form.Item>
|
||||
<>
|
||||
<Form.Item
|
||||
label={t("timetickets.fields.productivehrs")}
|
||||
name="productivehrs"
|
||||
rules={[
|
||||
({ getFieldValue }) => ({
|
||||
validator(rule, value) {
|
||||
if (!bodyshop.tt_enforce_hours_for_tech_console) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (
|
||||
!value ||
|
||||
getFieldValue("cost_center") === null ||
|
||||
!lineTicketData
|
||||
)
|
||||
return Promise.resolve();
|
||||
|
||||
//Check the cost center,
|
||||
const totals = CalculateAllocationsTotals(
|
||||
bodyshop,
|
||||
lineTicketData.joblines,
|
||||
lineTicketData.timetickets,
|
||||
lineTicketData.jobs_by_pk.lbr_adjustments
|
||||
);
|
||||
|
||||
const fieldTypeToCheck =
|
||||
bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber
|
||||
? "mod_lbr_ty"
|
||||
: "cost_center";
|
||||
|
||||
const costCenterDiff =
|
||||
Math.round(
|
||||
totals.find(
|
||||
(total) =>
|
||||
total[fieldTypeToCheck] ===
|
||||
getFieldValue("cost_center")
|
||||
)?.difference * 10
|
||||
) / 10;
|
||||
|
||||
if (value > costCenterDiff)
|
||||
return Promise.reject(
|
||||
t(
|
||||
"timetickets.validation.hoursenteredmorethanavailable"
|
||||
)
|
||||
);
|
||||
else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
},
|
||||
}),
|
||||
{
|
||||
required:
|
||||
form.getFieldValue("cost_center") !==
|
||||
"timetickets.labels.shift",
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber precision={1} />
|
||||
</Form.Item>
|
||||
<TimeTicketCalculatorComponent
|
||||
jobid={form.getFieldValue("jobid")}
|
||||
setProductiveHours={(productivehrs) =>
|
||||
form.setFieldsValue({ productivehrs })
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
@@ -291,23 +350,33 @@ export function TimeTicketModalComponent({
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<Form.Item dependencies={["jobid"]}>
|
||||
{() => (
|
||||
<LaborAllocationContainer
|
||||
jobid={form.getFieldValue("jobid") || null}
|
||||
/>
|
||||
)}
|
||||
{() => {
|
||||
const jobid = form.getFieldValue("jobid");
|
||||
if (
|
||||
(!called && jobid) ||
|
||||
(jobid && lineTicketData?.jobs_by_pk?.id !== jobid && !loading)
|
||||
) {
|
||||
loadLineTicketData({ variables: { id: jobid } });
|
||||
}
|
||||
return (
|
||||
<LaborAllocationContainer
|
||||
jobid={jobid || null}
|
||||
loading={loading}
|
||||
lineTicketData={lineTicketData}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function LaborAllocationContainer({ jobid }) {
|
||||
const { loading, data: lineTicketData } = useQuery(GET_LINE_TICKET_BY_PK, {
|
||||
variables: { id: jobid },
|
||||
skip: !jobid,
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only",
|
||||
});
|
||||
export function LaborAllocationContainer({
|
||||
jobid,
|
||||
loading,
|
||||
lineTicketData,
|
||||
hideTimeTickets = false,
|
||||
}) {
|
||||
if (loading) return <LoadingSkeleton />;
|
||||
if (!lineTicketData) return null;
|
||||
return (
|
||||
@@ -318,12 +387,13 @@ export function LaborAllocationContainer({ jobid }) {
|
||||
timetickets={lineTicketData.timetickets}
|
||||
adjustments={lineTicketData.jobs_by_pk.lbr_adjustments}
|
||||
/>
|
||||
|
||||
<TimeTicketList
|
||||
loading={loading}
|
||||
timetickets={lineTicketData.timetickets}
|
||||
techConsole
|
||||
/>
|
||||
{!hideTimeTickets && (
|
||||
<TimeTicketList
|
||||
loading={loading}
|
||||
timetickets={lineTicketData.timetickets}
|
||||
techConsole
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,376 @@
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Checkbox,
|
||||
Col,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Row,
|
||||
Space,
|
||||
Table,
|
||||
} from "antd";
|
||||
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 { LaborAllocationContainer } from "../time-ticket-modal/time-ticket-modal.component";
|
||||
import TimeTicketsTasksPresets from "../time-ticket-tasks-presets/time-ticket-tasks-presets.component";
|
||||
import { CalculateAllocationsTotals } from "../labor-allocations-table/labor-allocations-table.utility";
|
||||
import _ from "lodash";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(TimeTicketTaskModalComponent);
|
||||
|
||||
export function TimeTicketTaskModalComponent({
|
||||
bodyshop,
|
||||
form,
|
||||
employeeAutoCompleteOptions,
|
||||
lineTicketCalled,
|
||||
calculateTimeTickets,
|
||||
lineTicketLoading,
|
||||
lineTicketData,
|
||||
queryJobInfo,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<TimeTicketsTasksPresets
|
||||
form={form}
|
||||
calculateTimeTickets={calculateTimeTickets}
|
||||
/>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col lg={12} md={24}>
|
||||
<Form.Item
|
||||
name="jobid"
|
||||
label={t("timetickets.fields.ro_number")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<JobSearchSelectComponent
|
||||
convertedOnly={!bodyshop.tt_allow_post_to_invoiced}
|
||||
notExported={!bodyshop.tt_allow_post_to_invoiced}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="employeeid" label={t("timetickets.fields.employee")}>
|
||||
<EmployeeSearchSelectComponent
|
||||
options={employeeAutoCompleteOptions}
|
||||
allowClear
|
||||
onSelect={(value) => {
|
||||
const emps =
|
||||
employeeAutoCompleteOptions &&
|
||||
employeeAutoCompleteOptions.filter((e) => e.id === value)[0];
|
||||
form.setFieldsValue({ flat_rate: emps && emps.flat_rate });
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="employeeteamid"
|
||||
label={t("timetickets.fields.employee_team")}
|
||||
>
|
||||
<EmployeeTeamSearchSelectComponent />
|
||||
</Form.Item>
|
||||
<Space wrap>
|
||||
<Form.Item
|
||||
name="hourstype"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Checkbox.Group>
|
||||
<Space wrap>
|
||||
<Checkbox value="LAB" style={{ display: "flex" }}>
|
||||
Body
|
||||
</Checkbox>
|
||||
<Checkbox value="LAR" style={{ display: "flex" }}>
|
||||
Refinish
|
||||
</Checkbox>
|
||||
<Checkbox value="LAM" style={{ display: "flex" }}>
|
||||
Mechanical
|
||||
</Checkbox>
|
||||
<Checkbox value="LAF" style={{ display: "flex" }}>
|
||||
Frame
|
||||
</Checkbox>
|
||||
<Checkbox value="LAG" style={{ display: "flex" }}>
|
||||
Glass
|
||||
</Checkbox>
|
||||
</Space>
|
||||
</Checkbox.Group>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="percent"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} max={100} precision={1} addonAfter="%" />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
<Button onClick={calculateTimeTickets}>Calculate</Button>
|
||||
</Col>
|
||||
<Col lg={12} md={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>
|
||||
</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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
import React, { useEffect } 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 { 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";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
timeTicketTasksModal: selectTimeTicketTasks,
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
toggleModalVisible: () => dispatch(toggleModalVisible("timeTicketTask")),
|
||||
});
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(TimeTickeTaskModalContainer);
|
||||
|
||||
export function TimeTickeTaskModalContainer({
|
||||
bodyshop,
|
||||
timeTicketTasksModal,
|
||||
toggleModalVisible,
|
||||
}) {
|
||||
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 [queryJobInfo, { called, loading, data: lineTicketData }] =
|
||||
useLazyQuery(GET_JOB_INFO_DRAW_CALCULATIONS, {
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only",
|
||||
});
|
||||
|
||||
async function handleFinish(values) {
|
||||
console.log(
|
||||
"🚀 ~ file: time-ticket-task-modal.container.jsx:52 ~ handleFinish ~ values:",
|
||||
values
|
||||
);
|
||||
try {
|
||||
const result = await insertTimeTickets({
|
||||
variables: {
|
||||
timeTicketInput: values.timetickets.map((ticket) =>
|
||||
_.omit(ticket, "pay")
|
||||
),
|
||||
},
|
||||
});
|
||||
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) {
|
||||
} finally {
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (context.jobid) {
|
||||
console.log("UE Fired.");
|
||||
queryJobInfo({ variables: { id: context.jobid } });
|
||||
}
|
||||
}, [context.jobid, queryJobInfo]);
|
||||
|
||||
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) {
|
||||
data = [];
|
||||
|
||||
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: presetMemo,
|
||||
flat_rate: true,
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
form.setFieldsValue({
|
||||
timetickets: data.filter((d) => d.productivehrs > 0),
|
||||
});
|
||||
form.validateFields();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
destroyOnClose
|
||||
open={visible}
|
||||
onCancel={() => toggleModalVisible()}
|
||||
width="80%"
|
||||
onOk={() => form.submit()}
|
||||
>
|
||||
<Form
|
||||
autoComplete={"off"}
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleFinish}
|
||||
// onFieldsChange={handleFieldsChange}
|
||||
initialValues={context}
|
||||
>
|
||||
<TimeTicketTaskModalComponent
|
||||
form={form}
|
||||
employeeAutoCompleteOptions={
|
||||
EmployeeAutoCompleteData && EmployeeAutoCompleteData.employees
|
||||
}
|
||||
lineTicketData={lineTicketData}
|
||||
lineTicketLoading={loading}
|
||||
lineTicketCalled={called}
|
||||
calculateTimeTickets={calculateTimeTickets}
|
||||
queryJobInfo={queryJobInfo}
|
||||
/>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Button, Dropdown } from "antd";
|
||||
import React from "react";
|
||||
|
||||
export default function TimeTicketsTasksPresets({
|
||||
form,
|
||||
calculateTimeTickets,
|
||||
}) {
|
||||
const handleClick = (props) => {
|
||||
const preset = samplePresets.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: samplePresets.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 },
|
||||
];
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user