IO-2244 Restrict claimable productive hours based on remaining hours

This commit is contained in:
Patrick Fic
2023-04-13 09:25:52 -07:00
parent 0e78cb47f9
commit ec45454b3d
12 changed files with 360 additions and 36 deletions

View File

@@ -8864,6 +8864,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>tt_enforce_hours_for_tech_console</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>use_fippa</name> <name>use_fippa</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -17310,6 +17331,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>total</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>totals</name> <name>totals</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -33806,6 +33848,27 @@
<folder_node> <folder_node>
<name>errors</name> <name>errors</name>
<children> <children>
<concept_node>
<name>deleting</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>noaccess</name> <name>noaccess</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -34367,6 +34430,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>deleteconfirm</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>existing_owners</name> <name>existing_owners</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -34477,6 +34561,27 @@
<folder_node> <folder_node>
<name>successes</name> <name>successes</name>
<children> <children>
<concept_node>
<name>delete</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>save</name> <name>save</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -44980,6 +45085,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>hoursenteredmorethanavailable</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children> </children>
</folder_node> </folder_node>
</children> </children>
@@ -47292,6 +47418,27 @@
<folder_node> <folder_node>
<name>errors</name> <name>errors</name>
<children> <children>
<concept_node>
<name>deleting</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>noaccess</name> <name>noaccess</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -47916,6 +48063,27 @@
<folder_node> <folder_node>
<name>labels</name> <name>labels</name>
<children> <children>
<concept_node>
<name>deleteconfirm</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>fromvehicle</name> <name>fromvehicle</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -48005,6 +48173,27 @@
<folder_node> <folder_node>
<name>successes</name> <name>successes</name>
<children> <children>
<concept_node>
<name>delete</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>save</name> <name>save</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>

View File

@@ -6,10 +6,6 @@ export const CalculateAllocationsTotals = (
timetickets, timetickets,
adjustments = [] adjustments = []
) => { ) => {
console.log(
"🚀 ~ file: labor-allocations-table.utility.js ~ line 9 ~ adjustments",
adjustments
);
const responsibilitycenters = bodyshop.md_responsibility_centers; const responsibilitycenters = bodyshop.md_responsibility_centers;
const jobCodes = joblines.map((item) => item.mod_lbr_ty); const jobCodes = joblines.map((item) => item.mod_lbr_ty);
//.filter((value, index, self) => self.indexOf(value) === index && !!value); //.filter((value, index, self) => self.indexOf(value) === index && !!value);

View File

@@ -589,6 +589,13 @@ export default function ShopInfoGeneral({ form }) {
> >
<Switch /> <Switch />
</Form.Item> </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 <Form.Item
name={["bill_allow_post_to_closed"]} name={["bill_allow_post_to_closed"]}
label={t("bodyshop.fields.bill_allow_post_to_closed")} label={t("bodyshop.fields.bill_allow_post_to_closed")}

View File

@@ -1,4 +1,4 @@
import { useMutation } from "@apollo/client"; import { useMutation, useQuery } from "@apollo/client";
import { import {
Button, Button,
Card, Card,
@@ -21,6 +21,8 @@ import { selectTechnician } from "../../redux/tech/tech.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import TechJobClockoutDelete from "../tech-job-clock-out-delete/tech-job-clock-out-delete.component"; 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 { 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({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -39,7 +41,17 @@ export function TechClockOffButton({
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [updateTimeticket] = useMutation(UPDATE_TIME_TICKET); const [updateTimeticket] = useMutation(UPDATE_TIME_TICKET);
const [form] = Form.useForm(); 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 { t } = useTranslation();
const emps = bodyshop.employees.filter( const emps = bodyshop.employees.filter(
(e) => e.id === (technician && technician.id) (e) => e.id === (technician && technician.id)
@@ -129,6 +141,51 @@ export function TechClockOffButton({
required: true, required: true,
//message: t("general.validation.required"), //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 = totals.find(
(total) =>
total[fieldTypeToCheck] ===
getFieldValue("cost_center")
)?.difference;
if (value > costCenterDiff)
return Promise.reject(
t(
"timetickets.validation.hoursenteredmorethanavailable"
)
);
else {
return Promise.resolve();
}
},
}),
]} ]}
> >
<InputNumber min={0} precision={1} /> <InputNumber min={0} precision={1} />
@@ -178,7 +235,11 @@ export function TechClockOffButton({
</Col> </Col>
{!isShiftTicket && ( {!isShiftTicket && (
<Col span={16}> <Col span={16}>
<LaborAllocationContainer jobid={jobId} /> <LaborAllocationContainer
jobid={jobId || null}
loading={queryLoading}
lineTicketData={lineTicketData}
/>
</Col> </Col>
)} )}
</Row> </Row>

View File

@@ -1,4 +1,4 @@
import { useQuery } from "@apollo/client"; import { useLazyQuery, useQuery } from "@apollo/client";
import { Form, Input, InputNumber, Select, Switch } from "antd"; import { Form, Input, InputNumber, Select, Switch } from "antd";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -18,6 +18,7 @@ import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component"; import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component"; import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component";
import TimeTicketList from "../time-ticket-list/time-ticket-list.component"; import TimeTicketList from "../time-ticket-list/time-ticket-list.component";
import { CalculateAllocationsTotals } from "../labor-allocations-table/labor-allocations-table.utility";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -38,7 +39,11 @@ export function TimeTicketModalComponent({
employeeSelectDisabled, employeeSelectDisabled,
}) { }) {
const { t } = useTranslation(); 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 }) => { const CostCenterSelect = ({ emps, value, ...props }) => {
return ( return (
<Select <Select
@@ -176,6 +181,47 @@ export function TimeTicketModalComponent({
label={t("timetickets.fields.productivehrs")} label={t("timetickets.fields.productivehrs")}
name="productivehrs" name="productivehrs"
rules={[ 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 = totals.find(
(total) =>
total[fieldTypeToCheck] === getFieldValue("cost_center")
)?.difference;
if (value > costCenterDiff)
return Promise.reject(
t(
"timetickets.validation.hoursenteredmorethanavailable"
)
);
else {
return Promise.resolve();
}
},
}),
{ {
required: required:
form.getFieldValue("cost_center") !== form.getFieldValue("cost_center") !==
@@ -291,23 +337,28 @@ export function TimeTicketModalComponent({
</Form.Item> </Form.Item>
</LayoutFormRow> </LayoutFormRow>
<Form.Item dependencies={["jobid"]}> <Form.Item dependencies={["jobid"]}>
{() => ( {() => {
<LaborAllocationContainer const jobid = form.getFieldValue("jobid");
jobid={form.getFieldValue("jobid") || null} 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> </Form.Item>
</div> </div>
); );
} }
export function LaborAllocationContainer({ jobid }) { export function LaborAllocationContainer({ jobid, loading, lineTicketData }) {
const { loading, data: lineTicketData } = useQuery(GET_LINE_TICKET_BY_PK, {
variables: { id: jobid },
skip: !jobid,
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
if (loading) return <LoadingSkeleton />; if (loading) return <LoadingSkeleton />;
if (!lineTicketData) return null; if (!lineTicketData) return null;
return ( return (

View File

@@ -116,6 +116,7 @@ export const QUERY_BODYSHOP = gql`
md_lost_sale_reasons md_lost_sale_reasons
md_parts_scan md_parts_scan
enforce_conversion_category enforce_conversion_category
tt_enforce_hours_for_tech_console
employees { employees {
user_email user_email
id id
@@ -230,6 +231,7 @@ export const UPDATE_SHOP = gql`
md_lost_sale_reasons md_lost_sale_reasons
md_parts_scan md_parts_scan
enforce_conversion_category enforce_conversion_category
tt_enforce_hours_for_tech_console
employees { employees {
id id
first_name first_name

View File

@@ -541,6 +541,7 @@
"target_touchtime": "Target Touch Time", "target_touchtime": "Target Touch Time",
"timezone": "Timezone", "timezone": "Timezone",
"tt_allow_post_to_invoiced": "Allow Time Tickets to be posted to Invoiced & Exported Jobs", "tt_allow_post_to_invoiced": "Allow Time Tickets to be posted to Invoiced & Exported Jobs",
"tt_enforce_hours_for_tech_console": "Restrict Claimable hours from Tech Console",
"use_fippa": "Use FIPPA for Names on Generated Documents?", "use_fippa": "Use FIPPA for Names on Generated Documents?",
"uselocalmediaserver": "Use Local Media Server?", "uselocalmediaserver": "Use Local Media Server?",
"website": "Website", "website": "Website",
@@ -1063,8 +1064,8 @@
"sunday": "Sunday", "sunday": "Sunday",
"text": "Text", "text": "Text",
"thursday": "Thursday", "thursday": "Thursday",
"totals": "Totals",
"total": "Total", "total": "Total",
"totals": "Totals",
"tuesday": "Tuesday", "tuesday": "Tuesday",
"unknown": "Unknown", "unknown": "Unknown",
"username": "Username", "username": "Username",
@@ -2662,7 +2663,8 @@
}, },
"validation": { "validation": {
"clockoffmustbeafterclockon": "Clock off time must be the same or after clock in time.", "clockoffmustbeafterclockon": "Clock off time must be the same or after clock in time.",
"clockoffwithoutclockon": "Clock off time cannot be set without a clock in time." "clockoffwithoutclockon": "Clock off time cannot be set without a clock in time.",
"hoursenteredmorethanavailable": "The number of hours entered is more than what is available for this cost center."
} }
}, },
"titles": { "titles": {

View File

@@ -541,6 +541,7 @@
"target_touchtime": "", "target_touchtime": "",
"timezone": "", "timezone": "",
"tt_allow_post_to_invoiced": "", "tt_allow_post_to_invoiced": "",
"tt_enforce_hours_for_tech_console": "",
"use_fippa": "", "use_fippa": "",
"uselocalmediaserver": "", "uselocalmediaserver": "",
"website": "", "website": "",
@@ -1063,6 +1064,7 @@
"sunday": "", "sunday": "",
"text": "", "text": "",
"thursday": "", "thursday": "",
"total": "",
"totals": "", "totals": "",
"tuesday": "", "tuesday": "",
"unknown": "Desconocido", "unknown": "Desconocido",
@@ -2661,7 +2663,8 @@
}, },
"validation": { "validation": {
"clockoffmustbeafterclockon": "", "clockoffmustbeafterclockon": "",
"clockoffwithoutclockon": "" "clockoffwithoutclockon": "",
"hoursenteredmorethanavailable": ""
} }
}, },
"titles": { "titles": {

View File

@@ -541,6 +541,7 @@
"target_touchtime": "", "target_touchtime": "",
"timezone": "", "timezone": "",
"tt_allow_post_to_invoiced": "", "tt_allow_post_to_invoiced": "",
"tt_enforce_hours_for_tech_console": "",
"use_fippa": "", "use_fippa": "",
"uselocalmediaserver": "", "uselocalmediaserver": "",
"website": "", "website": "",
@@ -1063,6 +1064,7 @@
"sunday": "", "sunday": "",
"text": "", "text": "",
"thursday": "", "thursday": "",
"total": "",
"totals": "", "totals": "",
"tuesday": "", "tuesday": "",
"unknown": "Inconnu", "unknown": "Inconnu",
@@ -2661,7 +2663,8 @@
}, },
"validation": { "validation": {
"clockoffmustbeafterclockon": "", "clockoffmustbeafterclockon": "",
"clockoffwithoutclockon": "" "clockoffwithoutclockon": "",
"hoursenteredmorethanavailable": ""
} }
}, },
"titles": { "titles": {

View File

@@ -935,6 +935,7 @@
- textid - textid
- timezone - timezone
- tt_allow_post_to_invoiced - tt_allow_post_to_invoiced
- tt_enforce_hours_for_tech_console
- updated_at - updated_at
- use_fippa - use_fippa
- uselocalmediaserver - uselocalmediaserver
@@ -1023,6 +1024,7 @@
- target_touchtime - target_touchtime
- timezone - timezone
- tt_allow_post_to_invoiced - tt_allow_post_to_invoiced
- tt_enforce_hours_for_tech_console
- updated_at - updated_at
- use_fippa - use_fippa
- uselocalmediaserver - uselocalmediaserver
@@ -2391,30 +2393,32 @@
- active: - active:
_eq: true _eq: true
columns: columns:
- id
- created_at
- updated_at
- jobid
- billid - billid
- bodyshopid
- created_at
- id
- jobid
- message
- metadata
- paymentid - paymentid
- successful - successful
- message - updated_at
- bodyshopid
- useremail - useremail
select_permissions: select_permissions:
- role: user - role: user
permission: permission:
columns: columns:
- successful
- message
- useremail
- created_at
- updated_at
- billid - billid
- bodyshopid - bodyshopid
- created_at
- id - id
- jobid - jobid
- message
- metadata
- paymentid - paymentid
- successful
- updated_at
- useremail
filter: filter:
bodyshop: bodyshop:
associations: associations:

View File

@@ -0,0 +1,4 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- alter table "public"."bodyshops" add column "tt_enforce_hours_for_tech_console" boolean
-- not null default 'false';

View File

@@ -0,0 +1,2 @@
alter table "public"."bodyshops" add column "tt_enforce_hours_for_tech_console" boolean
not null default 'false';