{
+ onSelect={(index, image) => {
setgalleryImages(
galleryImages.map((g, idx) =>
index === idx ? { ...g, isSelected: !g.isSelected } : g
diff --git a/client/src/components/jobs-documents-local-gallery/jobs-documents-local-gallery.external.component.jsx b/client/src/components/jobs-documents-local-gallery/jobs-documents-local-gallery.external.component.jsx
index 46625f419..49fef085e 100644
--- a/client/src/components/jobs-documents-local-gallery/jobs-documents-local-gallery.external.component.jsx
+++ b/client/src/components/jobs-documents-local-gallery/jobs-documents-local-gallery.external.component.jsx
@@ -1,5 +1,5 @@
import React, { useEffect } from "react";
-import Gallery from "react-grid-gallery";
+import { Gallery } from "react-grid-gallery";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -38,7 +38,7 @@ function JobDocumentsLocalGalleryExternal({
const { t } = useTranslation();
useEffect(() => {
- if ( jobId) {
+ if (jobId) {
getJobMedia(jobId);
}
}, [jobId, getJobMedia]);
@@ -65,8 +65,7 @@ function JobDocumentsLocalGalleryExternal({
{
+ onSelect={(index, image) => {
setgalleryImages(
galleryImages.map((g, idx) =>
index === idx ? { ...g, isSelected: !g.isSelected } : g
diff --git a/client/src/components/labor-allocations-table/labor-allocations-table.utility.js b/client/src/components/labor-allocations-table/labor-allocations-table.utility.js
index 2ce5b4a0b..d236de913 100644
--- a/client/src/components/labor-allocations-table/labor-allocations-table.utility.js
+++ b/client/src/components/labor-allocations-table/labor-allocations-table.utility.js
@@ -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);
diff --git a/client/src/components/production-list-columns/production-list-columns.data.js b/client/src/components/production-list-columns/production-list-columns.data.js
index bd2f15817..7d122200d 100644
--- a/client/src/components/production-list-columns/production-list-columns.data.js
+++ b/client/src/components/production-list-columns/production-list-columns.data.js
@@ -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) => (
- {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
- record.v_model_desc || ""
- } ${record.v_color || ""} ${record.plate_no || ""}`}
+ {`${
+ record.v_model_yr || ""
+ } ${record.v_make_desc || ""} ${record.v_model_desc || ""} ${
+ record.v_color || ""
+ } ${record.plate_no || ""}`}
),
},
{
diff --git a/client/src/components/production-list-table/production-list-table.component.jsx b/client/src/components/production-list-table/production-list-table.component.jsx
index 6595476d5..746d956ab 100644
--- a/client/src/components/production-list-table/production-list-table.component.jsx
+++ b/client/src/components/production-list-table/production-list-table.component.jsx
@@ -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}
/>
);
}
+
export default connect(mapStateToProps, null)(ProductionListTable);
diff --git a/client/src/components/production-list-table/production-list-table.resizeable.component.jsx b/client/src/components/production-list-table/production-list-table.resizeable.component.jsx
index 2f1324999..618e9e8cd 100644
--- a/client/src/components/production-list-table/production-list-table.resizeable.component.jsx
+++ b/client/src/components/production-list-table/production-list-table.resizeable.component.jsx
@@ -3,8 +3,26 @@ import { Resizable } from "react-resizable";
export default function ResizableComponent(props) {
const { onResize, width, ...restProps } = props;
+
+ if (!width) {
+ return | ;
+ }
+
return (
-
+ {
+ e.stopPropagation();
+ }}
+ />
+ }
+ >
|
);
diff --git a/client/src/components/scoreboard-day-stats/scoreboard-day-stats.component.jsx b/client/src/components/scoreboard-day-stats/scoreboard-day-stats.component.jsx
index 36b4a028b..cfd6d945d 100644
--- a/client/src/components/scoreboard-day-stats/scoreboard-day-stats.component.jsx
+++ b/client/src/components/scoreboard-day-stats/scoreboard-day-stats.component.jsx
@@ -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)}
/>
+
+
+
);
}
diff --git a/client/src/components/scoreboard-targets-table/scoreboard-targets-table.component.jsx b/client/src/components/scoreboard-targets-table/scoreboard-targets-table.component.jsx
index 6f22b3b8a..112d441f6 100644
--- a/client/src/components/scoreboard-targets-table/scoreboard-targets-table.component.jsx
+++ b/client/src/components/scoreboard-targets-table/scoreboard-targets-table.component.jsx
@@ -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 }) {
+
+
+
@@ -184,14 +187,53 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
value={(values.todayPaint + values.todayBody).toFixed(1)}
/>
-
+
+
+
-
-
+
+
+
+
+
+
{
@@ -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);
diff --git a/client/src/components/scoreboard-timetickets/scoreboard-timetickets.stats.component.jsx b/client/src/components/scoreboard-timetickets/scoreboard-timetickets.stats.component.jsx
index 0b1b9b6bf..e31cec3af 100644
--- a/client/src/components/scoreboard-timetickets/scoreboard-timetickets.stats.component.jsx
+++ b/client/src/components/scoreboard-timetickets/scoreboard-timetickets.stats.component.jsx
@@ -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}
/>
+
+
+
{t("scoreboard.labels.calendarperiod")}
@@ -121,7 +127,7 @@ export function ScoreboardTicketsStats({ data, bodyshop }) {
({
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);
diff --git a/client/src/components/shop-info/shop-info.general.component.jsx b/client/src/components/shop-info/shop-info.general.component.jsx
index 3e9b1ed2c..32ac5aa5d 100644
--- a/client/src/components/shop-info/shop-info.general.component.jsx
+++ b/client/src/components/shop-info/shop-info.general.component.jsx
@@ -589,6 +589,13 @@ export default function ShopInfoGeneral({ form }) {
>
+
+
+
+
+
+
ShopEmployeeTeamMember
+ )
+}
diff --git a/client/src/components/shop-teams/shop-employee-teams.form.component.jsx b/client/src/components/shop-teams/shop-employee-teams.form.component.jsx
new file mode 100644
index 000000000..904acda7d
--- /dev/null
+++ b/client/src/components/shop-teams/shop-employee-teams.form.component.jsx
@@ -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 ;
+
+ return (
+ form.submit()}>
+ {t("general.actions.save")}
+
+ }
+ >
+
+ {(fields, { add, remove, move }) => {
+ return (
+
+ {fields.map((field, index) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ remove(field.name);
+ }}
+ />
+
+
+
+
+ ))}
+
+
+
+
+ );
+ }}
+
+
+
+ );
+}
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(ShopEmployeeTeamsFormComponent);
diff --git a/client/src/components/shop-teams/shop-employee-teams.list.jsx b/client/src/components/shop-teams/shop-employee-teams.list.jsx
new file mode 100644
index 000000000..402f8c9bf
--- /dev/null
+++ b/client/src/components/shop-teams/shop-employee-teams.list.jsx
@@ -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 (
+
+
{
+ return (
+
+ );
+ }}
+ 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);
+ },
+ };
+ }}
+ />
+
+ );
+}
diff --git a/client/src/components/shop-teams/shop-teams.container.jsx b/client/src/components/shop-teams/shop-teams.container.jsx
new file mode 100644
index 000000000..702f15e37
--- /dev/null
+++ b/client/src/components/shop-teams/shop-teams.container.jsx
@@ -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 ;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+export default connect(mapStateToProps, null)(ShopTeamsContainer);
diff --git a/client/src/components/tech-job-clock-out-button/tech-job-clock-out-button.component.jsx b/client/src/components/tech-job-clock-out-button/tech-job-clock-out-button.component.jsx
index 7ea864ad5..5c0830d5e 100644
--- a/client/src/components/tech-job-clock-out-button/tech-job-clock-out-button.component.jsx
+++ b/client/src/components/tech-job-clock-out-button/tech-job-clock-out-button.component.jsx
@@ -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)
@@ -59,7 +71,7 @@ export function TechClockOffButton({
emps &&
emps.rates.filter((r) => r.cost_center === values.cost_center)[0]
?.rate,
- flat_rate: emps.flat_rate,
+ flat_rate: emps && emps.flat_rate,
ciecacode:
bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber
? values.cost_center
@@ -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();
+ }
+ },
+ }),
]}
>
@@ -178,7 +238,11 @@ export function TechClockOffButton({
{!isShiftTicket && (
-
+
)}
diff --git a/client/src/components/time-ticket-calculator/time-ticket-calculator.component.jsx b/client/src/components/time-ticket-calculator/time-ticket-calculator.component.jsx
new file mode 100644
index 000000000..a36f1dd49
--- /dev/null
+++ b/client/src/components/time-ticket-calculator/time-ticket-calculator.component.jsx
@@ -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 = (
+
+
+
+ Draw
+ Cut of Sale
+
+
+
+
+ {({ getFieldValue }) => (
+
+ {getFieldValue("type") === "draw" ? (
+
+
+
+
+ Body
+
+
+
+
+ Refinish
+
+
+
+
+ Mechanical
+
+
+
+
+ Frame
+
+
+
+
+ Glass
+
+
+
+
+ ) : (
+
+ Body
+
+ Refinish
+
+ Mechanical
+
+ Frame
+
+ Glass
+
+ )}
+
+ )}
+
+
+
+
+
+
+
+
+ );
+
+ return (
+
+
+
+ );
+}
diff --git a/client/src/components/time-ticket-list/time-ticket-list-team-pay.component.jsx b/client/src/components/time-ticket-list/time-ticket-list-team-pay.component.jsx
new file mode 100644
index 000000000..3c3375036
--- /dev/null
+++ b/client/src/components/time-ticket-list/time-ticket-list-team-pay.component.jsx
@@ -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 (
+ <>
+ setVisible(false)}
+ >
+
+ {({ 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 (
+ (
+
+
+ Tickets to be Created
+
+ {`(${eligibleHours} hours to split)`}
+
+ )}
+ 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",
+ },
+ ]}
+ />
+ );
+ }}
+
+
+
+
+ >
+ );
+}
+
+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,
+ },
+ },
+ ],
+ },
+];
diff --git a/client/src/components/time-ticket-list/time-ticket-list.component.jsx b/client/src/components/time-ticket-list/time-ticket-list.component.jsx
index 7cebf2672..ba4aeda2f 100644
--- a/client/src/components/time-ticket-list/time-ticket-list.component.jsx
+++ b/client/src/components/time-ticket-list/time-ticket-list.component.jsx
@@ -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={
+ {
+ //
+ }
+
{jobId &&
(techConsole ? null : (
{
return (