@@ -6116,6 +6116,32 @@
|
|||||||
</concept_node>
|
</concept_node>
|
||||||
</children>
|
</children>
|
||||||
</folder_node>
|
</folder_node>
|
||||||
|
<folder_node>
|
||||||
|
<name>employee_teams</name>
|
||||||
|
<children>
|
||||||
|
<concept_node>
|
||||||
|
<name>page</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>
|
||||||
|
</folder_node>
|
||||||
<folder_node>
|
<folder_node>
|
||||||
<name>employees</name>
|
<name>employees</name>
|
||||||
<children>
|
<children>
|
||||||
@@ -8864,6 +8890,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>
|
||||||
@@ -9404,6 +9451,27 @@
|
|||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>employee_teams</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>employees</name>
|
<name>employees</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<definition_loaded>false</definition_loaded>
|
||||||
@@ -14583,6 +14651,84 @@
|
|||||||
</folder_node>
|
</folder_node>
|
||||||
</children>
|
</children>
|
||||||
</folder_node>
|
</folder_node>
|
||||||
|
<folder_node>
|
||||||
|
<name>employee_teams</name>
|
||||||
|
<children>
|
||||||
|
<folder_node>
|
||||||
|
<name>actions</name>
|
||||||
|
<children>
|
||||||
|
<concept_node>
|
||||||
|
<name>new</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>
|
||||||
|
</folder_node>
|
||||||
|
<folder_node>
|
||||||
|
<name>fields</name>
|
||||||
|
<children>
|
||||||
|
<concept_node>
|
||||||
|
<name>active</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>name</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>
|
||||||
|
</folder_node>
|
||||||
|
</children>
|
||||||
|
</folder_node>
|
||||||
<folder_node>
|
<folder_node>
|
||||||
<name>employees</name>
|
<name>employees</name>
|
||||||
<children>
|
<children>
|
||||||
@@ -17310,6 +17456,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>
|
||||||
@@ -33848,6 +34015,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>
|
||||||
@@ -34409,6 +34597,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>
|
||||||
@@ -34519,6 +34728,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>
|
||||||
@@ -43968,6 +44198,27 @@
|
|||||||
<folder_node>
|
<folder_node>
|
||||||
<name>actions</name>
|
<name>actions</name>
|
||||||
<children>
|
<children>
|
||||||
|
<concept_node>
|
||||||
|
<name>claimtasks</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>clockin</name>
|
<name>clockin</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<definition_loaded>false</definition_loaded>
|
||||||
@@ -45022,6 +45273,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>
|
||||||
@@ -47334,6 +47606,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>
|
||||||
@@ -47958,6 +48251,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>
|
||||||
@@ -48047,6 +48361,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>
|
||||||
|
|||||||
@@ -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" })),
|
dispatch(setModalContext({ context: context, modal: "jobCosting" })),
|
||||||
setTimeTicketContext: (context) =>
|
setTimeTicketContext: (context) =>
|
||||||
dispatch(setModalContext({ context: context, modal: "timeTicket" })),
|
dispatch(setModalContext({ context: context, modal: "timeTicket" })),
|
||||||
|
setTimeTicketTaskContext: (context) =>
|
||||||
|
dispatch(setModalContext({ context: context, modal: "timeTicketTask" })),
|
||||||
setCardPaymentContext: (context) =>
|
setCardPaymentContext: (context) =>
|
||||||
dispatch(setModalContext({ context: context, modal: "cardPayment" })),
|
dispatch(setModalContext({ context: context, modal: "cardPayment" })),
|
||||||
});
|
});
|
||||||
@@ -62,6 +64,7 @@ export function JobsDetailHeaderActions({
|
|||||||
setJobCostingContext,
|
setJobCostingContext,
|
||||||
jobRO,
|
jobRO,
|
||||||
setTimeTicketContext,
|
setTimeTicketContext,
|
||||||
|
setTimeTicketTaskContext,
|
||||||
setCardPaymentContext,
|
setCardPaymentContext,
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -247,6 +250,21 @@ export function JobsDetailHeaderActions({
|
|||||||
>
|
>
|
||||||
{t("timetickets.actions.enter")}
|
{t("timetickets.actions.enter")}
|
||||||
</Menu.Item>
|
</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
|
<Menu.Item
|
||||||
key="enterpayments"
|
key="enterpayments"
|
||||||
disabled={!job.converted}
|
disabled={!job.converted}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import Gallery from "react-grid-gallery";
|
import { Gallery } from "react-grid-gallery";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { GenerateSrcUrl, GenerateThumbUrl } from "./job-documents.utility";
|
import { GenerateSrcUrl, GenerateThumbUrl } from "./job-documents.utility";
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ function JobsDocumentGalleryExternal({
|
|||||||
<Gallery
|
<Gallery
|
||||||
images={galleryImages}
|
images={galleryImages}
|
||||||
backdropClosesModal={true}
|
backdropClosesModal={true}
|
||||||
onSelectImage={(index, image) => {
|
onSelect={(index, image) => {
|
||||||
setgalleryImages(
|
setgalleryImages(
|
||||||
galleryImages.map((g, idx) =>
|
galleryImages.map((g, idx) =>
|
||||||
index === idx ? { ...g, isSelected: !g.isSelected } : g
|
index === idx ? { ...g, isSelected: !g.isSelected } : g
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import Gallery from "react-grid-gallery";
|
import { Gallery } from "react-grid-gallery";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
@@ -38,7 +38,7 @@ function JobDocumentsLocalGalleryExternal({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if ( jobId) {
|
if (jobId) {
|
||||||
getJobMedia(jobId);
|
getJobMedia(jobId);
|
||||||
}
|
}
|
||||||
}, [jobId, getJobMedia]);
|
}, [jobId, getJobMedia]);
|
||||||
@@ -65,8 +65,7 @@ function JobDocumentsLocalGalleryExternal({
|
|||||||
<div className="clearfix">
|
<div className="clearfix">
|
||||||
<Gallery
|
<Gallery
|
||||||
images={galleryImages}
|
images={galleryImages}
|
||||||
backdropClosesModal={true}
|
onSelect={(index, image) => {
|
||||||
onSelectImage={(index, image) => {
|
|
||||||
setgalleryImages(
|
setgalleryImages(
|
||||||
galleryImages.map((g, idx) =>
|
galleryImages.map((g, idx) =>
|
||||||
index === idx ? { ...g, isSelected: !g.isSelected } : g
|
index === idx ? { ...g, isSelected: !g.isSelected } : g
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -91,11 +91,13 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
|
|||||||
b.v_make_desc + b.v_model_desc
|
b.v_make_desc + b.v_model_desc
|
||||||
),
|
),
|
||||||
sortOrder:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === "ownr" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
|
||||||
render: (text, record) => (
|
render: (text, record) => (
|
||||||
<span>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
|
<Link to={`/manage/vehicles/${record.vehicleid}`}>{`${
|
||||||
record.v_model_desc || ""
|
record.v_model_yr || ""
|
||||||
} ${record.v_color || ""} ${record.plate_no || ""}`}</span>
|
} ${record.v_make_desc || ""} ${record.v_model_desc || ""} ${
|
||||||
|
record.v_color || ""
|
||||||
|
} ${record.plate_no || ""}`}</Link>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ export function ProductionListTable({
|
|||||||
state,
|
state,
|
||||||
activeStatuses: bodyshop.md_ro_statuses.active_statuses,
|
activeStatuses: bodyshop.md_ro_statuses.active_statuses,
|
||||||
}).find((e) => e.key === k.key),
|
}).find((e) => e.key === k.key),
|
||||||
width: k.width,
|
width: k.width ?? 100,
|
||||||
};
|
};
|
||||||
})) ||
|
})) ||
|
||||||
[]
|
[]
|
||||||
@@ -267,6 +267,8 @@ export function ProductionListTable({
|
|||||||
sortOrder:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === c.key && state.sortedInfo.order,
|
state.sortedInfo.columnKey === c.key && state.sortedInfo.order,
|
||||||
title: headerItem(c),
|
title: headerItem(c),
|
||||||
|
ellipsis: true,
|
||||||
|
width: c.width ?? 100,
|
||||||
onHeaderCell: (column) => ({
|
onHeaderCell: (column) => ({
|
||||||
width: column.width,
|
width: column.width,
|
||||||
onResize: handleResize(index),
|
onResize: handleResize(index),
|
||||||
@@ -276,11 +278,12 @@ export function ProductionListTable({
|
|||||||
rowKey="id"
|
rowKey="id"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
dataSource={dataSource}
|
dataSource={dataSource}
|
||||||
// scroll={{ x: true }}
|
scroll={{ x: 1000 }}
|
||||||
onChange={handleTableChange}
|
onChange={handleTableChange}
|
||||||
/>
|
/>
|
||||||
</ReactDragListView.DragColumn>
|
</ReactDragListView.DragColumn>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(mapStateToProps, null)(ProductionListTable);
|
export default connect(mapStateToProps, null)(ProductionListTable);
|
||||||
|
|||||||
@@ -3,8 +3,26 @@ import { Resizable } from "react-resizable";
|
|||||||
|
|
||||||
export default function ResizableComponent(props) {
|
export default function ResizableComponent(props) {
|
||||||
const { onResize, width, ...restProps } = props;
|
const { onResize, width, ...restProps } = props;
|
||||||
|
|
||||||
|
if (!width) {
|
||||||
|
return <th {...restProps} />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
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} />
|
<th {...restProps} />
|
||||||
</Resizable>
|
</Resizable>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Card, Statistic } from "antd";
|
import { Card, Divider, Statistic } from "antd";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
@@ -41,6 +41,9 @@ export function ScoreboardDayStats({ bodyshop, date, entries }) {
|
|||||||
label="P"
|
label="P"
|
||||||
value={paintHrs.toFixed(1)}
|
value={paintHrs.toFixed(1)}
|
||||||
/>
|
/>
|
||||||
|
<Divider style={{ margin: 0 }} />
|
||||||
|
|
||||||
|
<Statistic value={(bodyHrs + paintHrs).toFixed(1)} />
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { CalendarOutlined } from "@ant-design/icons";
|
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 _ from "lodash";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import React, { useMemo } from "react";
|
import React, { useMemo } from "react";
|
||||||
@@ -177,6 +177,9 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
|
|||||||
<Statistic value={values.toDatePaint.toFixed(1)} />
|
<Statistic value={values.toDatePaint.toFixed(1)} />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<Divider style={{ margin: 5 }} />
|
||||||
|
</Row>
|
||||||
<Row>
|
<Row>
|
||||||
<Col {...statSpans}></Col>
|
<Col {...statSpans}></Col>
|
||||||
<Col {...statSpans}>
|
<Col {...statSpans}>
|
||||||
@@ -184,14 +187,53 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
|
|||||||
value={(values.todayPaint + values.todayBody).toFixed(1)}
|
value={(values.todayPaint + values.todayBody).toFixed(1)}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</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}>
|
<Col {...statSpans}>
|
||||||
<Statistic
|
<Statistic
|
||||||
value={(values.weeklyPaint + values.weeklyBody).toFixed(1)}
|
value={(values.weeklyPaint + values.weeklyBody).toFixed(1)}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col {...statSpans}></Col>
|
<Col {...statSpans}>
|
||||||
<Col {...statSpans}></Col>
|
<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}>
|
<Col {...statSpans}>
|
||||||
<Statistic
|
<Statistic
|
||||||
value={(values.toDatePaint + values.toDateBody).toFixed(1)}
|
value={(values.toDatePaint + values.toDateBody).toFixed(1)}
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ export default function ScoreboardTimeTickets() {
|
|||||||
totalLastMonth: 0,
|
totalLastMonth: 0,
|
||||||
totalOverPeriod: 0,
|
totalOverPeriod: 0,
|
||||||
actualTotalOverPeriod: 0,
|
actualTotalOverPeriod: 0,
|
||||||
|
totalEffieciencyOverPeriod: 0,
|
||||||
employees: {},
|
employees: {},
|
||||||
};
|
};
|
||||||
data.fixedperiod.forEach((ticket) => {
|
data.fixedperiod.forEach((ticket) => {
|
||||||
@@ -94,6 +95,7 @@ export default function ScoreboardTimeTickets() {
|
|||||||
totalLastMonth: 0,
|
totalLastMonth: 0,
|
||||||
totalOverPeriod: 0,
|
totalOverPeriod: 0,
|
||||||
actualTotalOverPeriod: 0,
|
actualTotalOverPeriod: 0,
|
||||||
|
totalEffieciencyOverPeriod: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,6 +223,28 @@ export default function ScoreboardTimeTickets() {
|
|||||||
|
|
||||||
ret2.push(r);
|
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(ret);
|
||||||
roundObject(totals);
|
roundObject(totals);
|
||||||
roundObject(ret2);
|
roundObject(ret2);
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export function ScoreboardTicketsStats({ data, bodyshop }) {
|
|||||||
key: "efficiencyoverperiod",
|
key: "efficiencyoverperiod",
|
||||||
render: (text, record) =>
|
render: (text, record) =>
|
||||||
`${(
|
`${(
|
||||||
(record.totalOverPeriod / (record.actualTotalOverPeriod || .1)) *
|
(record.totalOverPeriod / (record.actualTotalOverPeriod || 0.1)) *
|
||||||
100
|
100
|
||||||
).toFixed(1)} %`,
|
).toFixed(1)} %`,
|
||||||
},
|
},
|
||||||
@@ -113,6 +113,12 @@ export function ScoreboardTicketsStats({ data, bodyshop }) {
|
|||||||
value={data.totalOverPeriod}
|
value={data.totalOverPeriod}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<Statistic
|
||||||
|
title={t("scoreboard.labels.efficiencyoverperiod")}
|
||||||
|
value={`${data.totalEffieciencyOverPeriod}%`}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Typography.Text type="secondary">
|
<Typography.Text type="secondary">
|
||||||
{t("scoreboard.labels.calendarperiod")}
|
{t("scoreboard.labels.calendarperiod")}
|
||||||
@@ -121,7 +127,7 @@ export function ScoreboardTicketsStats({ data, bodyshop }) {
|
|||||||
<Col md={24} lg={20}>
|
<Col md={24} lg={20}>
|
||||||
<Table
|
<Table
|
||||||
columns={columns}
|
columns={columns}
|
||||||
rowKey='employee_number'
|
rowKey="employee_number"
|
||||||
dataSource={tableData}
|
dataSource={tableData}
|
||||||
id="employee_number"
|
id="employee_number"
|
||||||
scroll={{ y: "300px" }}
|
scroll={{ y: "300px" }}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
Switch,
|
Switch,
|
||||||
Table,
|
Table,
|
||||||
} from "antd";
|
} from "antd";
|
||||||
import { useForm } from "antd/es/form/Form";
|
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import querystring from "query-string";
|
import querystring from "query-string";
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
@@ -46,7 +46,7 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
|
|
||||||
export function ShopEmployeesFormComponent({ bodyshop }) {
|
export function ShopEmployeesFormComponent({ bodyshop }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [form] = useForm();
|
const [form] = Form.useForm();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const search = querystring.parse(useLocation().search);
|
const search = querystring.parse(useLocation().search);
|
||||||
const [deleteVacation] = useMutation(DELETE_VACATION);
|
const [deleteVacation] = useMutation(DELETE_VACATION);
|
||||||
|
|||||||
@@ -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")}
|
||||||
|
|||||||
@@ -388,6 +388,18 @@ export function ShopInfoRbacComponent({ form, bodyshop }) {
|
|||||||
>
|
>
|
||||||
<InputNumber />
|
<InputNumber />
|
||||||
</Form.Item>
|
</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
|
<Form.Item
|
||||||
label={t("bodyshop.fields.rbac.owners.list")}
|
label={t("bodyshop.fields.rbac.owners.list")}
|
||||||
rules={[
|
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 {
|
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)
|
||||||
@@ -59,7 +71,7 @@ export function TechClockOffButton({
|
|||||||
emps &&
|
emps &&
|
||||||
emps.rates.filter((r) => r.cost_center === values.cost_center)[0]
|
emps.rates.filter((r) => r.cost_center === values.cost_center)[0]
|
||||||
?.rate,
|
?.rate,
|
||||||
flat_rate: emps.flat_rate,
|
flat_rate: emps && emps.flat_rate,
|
||||||
ciecacode:
|
ciecacode:
|
||||||
bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber
|
bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber
|
||||||
? values.cost_center
|
? values.cost_center
|
||||||
@@ -129,6 +141,54 @@ 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 =
|
||||||
|
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} />
|
<InputNumber min={0} precision={1} />
|
||||||
@@ -178,7 +238,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>
|
||||||
|
|||||||
@@ -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 { 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 moment from "moment";
|
||||||
import React, { useMemo, useState } from "react";
|
import React, { useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
import {
|
import {
|
||||||
selectAuthLevel,
|
selectAuthLevel,
|
||||||
selectBodyshop,
|
selectBodyshop,
|
||||||
} from "../../redux/user/user.selectors";
|
} from "../../redux/user/user.selectors";
|
||||||
import { onlyUnique } from "../../utils/arrayHelper";
|
|
||||||
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter";
|
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter";
|
||||||
|
import { onlyUnique } from "../../utils/arrayHelper";
|
||||||
import { alphaSort, dateSort } from "../../utils/sorters";
|
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||||
import RbacWrapper, {
|
import RbacWrapper, {
|
||||||
HasRbacAccess,
|
HasRbacAccess,
|
||||||
@@ -22,12 +24,14 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
authLevel: selectAuthLevel,
|
authLevel: selectAuthLevel,
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
setTimeTicketTaskContext: (context) =>
|
||||||
|
dispatch(setModalContext({ context: context, modal: "timeTicketTask" })),
|
||||||
});
|
});
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(TimeTicketList);
|
export default connect(mapStateToProps, mapDispatchToProps)(TimeTicketList);
|
||||||
|
|
||||||
export function TimeTicketList({
|
export function TimeTicketList({
|
||||||
bodyshop,
|
bodyshop,
|
||||||
|
setTimeTicketTaskContext,
|
||||||
authLevel,
|
authLevel,
|
||||||
disabled,
|
disabled,
|
||||||
loading,
|
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"),
|
title: t("general.labels.actions"),
|
||||||
@@ -250,6 +263,23 @@ export function TimeTicketList({
|
|||||||
title={t("timetickets.labels.timetickets")}
|
title={t("timetickets.labels.timetickets")}
|
||||||
extra={
|
extra={
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
|
{
|
||||||
|
// <TimeTicketListTeamPay
|
||||||
|
// actions={{ refetch }}
|
||||||
|
// context={{ jobId: jobId }}
|
||||||
|
// />
|
||||||
|
}
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setTimeTicketTaskContext();
|
||||||
|
setTimeTicketTaskContext({
|
||||||
|
actions: {},
|
||||||
|
context: { jobid: jobId },
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("timetickets.actions.claimtasks")}
|
||||||
|
</Button>
|
||||||
{jobId &&
|
{jobId &&
|
||||||
(techConsole ? null : (
|
(techConsole ? null : (
|
||||||
<TimeTicketEnterButton
|
<TimeTicketEnterButton
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useQuery } from "@apollo/client";
|
import { useLazyQuery } 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";
|
||||||
@@ -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 FormDateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
|
||||||
import JobSearchSelect from "../job-search-select/job-search-select.component";
|
import JobSearchSelect from "../job-search-select/job-search-select.component";
|
||||||
import LaborAllocationsTable from "../labor-allocations-table/labor-allocations-table.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 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 TimeTicketCalculatorComponent from "../time-ticket-calculator/time-ticket-calculator.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -38,7 +40,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
|
||||||
@@ -172,20 +178,73 @@ export function TimeTicketModalComponent({
|
|||||||
<LayoutFormRow>
|
<LayoutFormRow>
|
||||||
<Form.Item shouldUpdate>
|
<Form.Item shouldUpdate>
|
||||||
{() => (
|
{() => (
|
||||||
<Form.Item
|
<>
|
||||||
label={t("timetickets.fields.productivehrs")}
|
<Form.Item
|
||||||
name="productivehrs"
|
label={t("timetickets.fields.productivehrs")}
|
||||||
rules={[
|
name="productivehrs"
|
||||||
{
|
rules={[
|
||||||
required:
|
({ getFieldValue }) => ({
|
||||||
form.getFieldValue("cost_center") !==
|
validator(rule, value) {
|
||||||
"timetickets.labels.shift",
|
if (!bodyshop.tt_enforce_hours_for_tech_console) {
|
||||||
//message: t("general.validation.required"),
|
return Promise.resolve();
|
||||||
},
|
}
|
||||||
]}
|
if (
|
||||||
>
|
!value ||
|
||||||
<InputNumber precision={1} />
|
getFieldValue("cost_center") === null ||
|
||||||
</Form.Item>
|
!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>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
@@ -291,23 +350,33 @@ 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({
|
||||||
const { loading, data: lineTicketData } = useQuery(GET_LINE_TICKET_BY_PK, {
|
jobid,
|
||||||
variables: { id: jobid },
|
loading,
|
||||||
skip: !jobid,
|
lineTicketData,
|
||||||
fetchPolicy: "network-only",
|
hideTimeTickets = false,
|
||||||
nextFetchPolicy: "network-only",
|
}) {
|
||||||
});
|
|
||||||
if (loading) return <LoadingSkeleton />;
|
if (loading) return <LoadingSkeleton />;
|
||||||
if (!lineTicketData) return null;
|
if (!lineTicketData) return null;
|
||||||
return (
|
return (
|
||||||
@@ -318,12 +387,13 @@ export function LaborAllocationContainer({ jobid }) {
|
|||||||
timetickets={lineTicketData.timetickets}
|
timetickets={lineTicketData.timetickets}
|
||||||
adjustments={lineTicketData.jobs_by_pk.lbr_adjustments}
|
adjustments={lineTicketData.jobs_by_pk.lbr_adjustments}
|
||||||
/>
|
/>
|
||||||
|
{!hideTimeTickets && (
|
||||||
<TimeTicketList
|
<TimeTicketList
|
||||||
loading={loading}
|
loading={loading}
|
||||||
timetickets={lineTicketData.timetickets}
|
timetickets={lineTicketData.timetickets}
|
||||||
techConsole
|
techConsole
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</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 { alphaSort } from "../../utils/sorters";
|
||||||
import { TemplateList } from "../../utils/TemplateConstants";
|
import { TemplateList } from "../../utils/TemplateConstants";
|
||||||
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
|
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
|
||||||
|
import Dinero from "dinero.js";
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
});
|
});
|
||||||
@@ -129,6 +129,16 @@ const JobRelatedTicketsTable = ({
|
|||||||
return acc;
|
return acc;
|
||||||
}, 0);
|
}, 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 {
|
return {
|
||||||
id: `${item.jobKey}${costCenter}`,
|
id: `${item.jobKey}${costCenter}`,
|
||||||
costCenter,
|
costCenter,
|
||||||
@@ -136,6 +146,7 @@ const JobRelatedTicketsTable = ({
|
|||||||
actHrs: actHrs.toFixed(1),
|
actHrs: actHrs.toFixed(1),
|
||||||
prodHrs: prodHrs.toFixed(1),
|
prodHrs: prodHrs.toFixed(1),
|
||||||
clockHrs,
|
clockHrs,
|
||||||
|
pay,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@@ -195,6 +206,15 @@ const JobRelatedTicketsTable = ({
|
|||||||
state.sortedInfo.columnKey === "clockHrs" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "clockHrs" && state.sortedInfo.order,
|
||||||
render: (text, record) => record.clockHrs.toFixed(2),
|
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"),
|
title: t("general.labels.actions"),
|
||||||
dataIndex: "actions",
|
dataIndex: "actions",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
91
client/src/graphql/employee_teams.queries.js
Normal file
91
client/src/graphql/employee_teams.queries.js
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import { gql } from "@apollo/client";
|
||||||
|
|
||||||
|
export const QUERY_TEAMS = gql`
|
||||||
|
query QUERY_TEAMS {
|
||||||
|
employee_teams(order_by: { name: asc }) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
employee_team_members {
|
||||||
|
id
|
||||||
|
employeeid
|
||||||
|
labor_rates
|
||||||
|
percentage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const UPDATE_EMPLOYEE_TEAM = gql`
|
||||||
|
mutation UPDATE_EMPLOYEE_TEAM(
|
||||||
|
$employeeTeamId: uuid!
|
||||||
|
$employeeTeam: employee_teams_set_input
|
||||||
|
$teamMemberDeletes: [uuid!]
|
||||||
|
$teamMemberUpdates: [employee_team_members_updates!]!
|
||||||
|
$teamMemberInserts: [employee_team_members_insert_input!]!
|
||||||
|
) {
|
||||||
|
update_employee_team_members_many(updates: $teamMemberUpdates) {
|
||||||
|
returning {
|
||||||
|
id
|
||||||
|
labor_rates
|
||||||
|
employeeid
|
||||||
|
percentage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete_employee_team_members(where: { id: { _in: $teamMemberDeletes } }) {
|
||||||
|
affected_rows
|
||||||
|
}
|
||||||
|
insert_employee_team_members(objects: $teamMemberInserts) {
|
||||||
|
returning {
|
||||||
|
id
|
||||||
|
labor_rates
|
||||||
|
employeeid
|
||||||
|
percentage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
update_employee_teams_by_pk(
|
||||||
|
pk_columns: { id: $employeeTeamId }
|
||||||
|
_set: $employeeTeam
|
||||||
|
) {
|
||||||
|
active
|
||||||
|
name
|
||||||
|
id
|
||||||
|
employee_team_members {
|
||||||
|
percentage
|
||||||
|
labor_rates
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const INSERT_EMPLOYEE_TEAM = gql`
|
||||||
|
mutation INSERT_EMPLOYEE_TEAM($employeeTeam: employee_teams_insert_input!) {
|
||||||
|
insert_employee_teams_one(object: $employeeTeam) {
|
||||||
|
active
|
||||||
|
name
|
||||||
|
id
|
||||||
|
employee_team_members {
|
||||||
|
employeeid
|
||||||
|
percentage
|
||||||
|
labor_rates
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const QUERY_EMPLOYEE_TEAM_BY_ID = gql`
|
||||||
|
query QUERY_EMPLOYEE_TEAM_BY_ID($id: uuid!) {
|
||||||
|
employee_teams_by_pk(id: $id) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
active
|
||||||
|
employee_team_members {
|
||||||
|
employeeid
|
||||||
|
percentage
|
||||||
|
labor_rates
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -76,6 +76,65 @@ export const GET_LINE_TICKET_BY_PK = gql`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const GET_JOB_INFO_DRAW_CALCULATIONS = gql`
|
||||||
|
query GET_JOB_INFO_DRAW_CALCULATIONS($id: uuid!) {
|
||||||
|
jobs_by_pk(id: $id) {
|
||||||
|
id
|
||||||
|
lbr_adjustments
|
||||||
|
converted
|
||||||
|
rate_lab
|
||||||
|
rate_lad
|
||||||
|
rate_laa
|
||||||
|
rate_la1
|
||||||
|
rate_la2
|
||||||
|
rate_la3
|
||||||
|
rate_la4
|
||||||
|
rate_lau
|
||||||
|
rate_lar
|
||||||
|
rate_lag
|
||||||
|
rate_laf
|
||||||
|
rate_lam
|
||||||
|
}
|
||||||
|
timetickets(where: { jobid: { _eq: $id } }) {
|
||||||
|
actualhrs
|
||||||
|
ciecacode
|
||||||
|
cost_center
|
||||||
|
date
|
||||||
|
id
|
||||||
|
jobid
|
||||||
|
employeeid
|
||||||
|
memo
|
||||||
|
flat_rate
|
||||||
|
clockon
|
||||||
|
clockoff
|
||||||
|
rate
|
||||||
|
employee {
|
||||||
|
id
|
||||||
|
first_name
|
||||||
|
last_name
|
||||||
|
employee_number
|
||||||
|
}
|
||||||
|
productivehrs
|
||||||
|
}
|
||||||
|
joblines(where: { jobid: { _eq: $id }, removed: { _eq: false } }) {
|
||||||
|
id
|
||||||
|
line_desc
|
||||||
|
part_type
|
||||||
|
oem_partno
|
||||||
|
db_price
|
||||||
|
act_price
|
||||||
|
part_qty
|
||||||
|
mod_lbr_ty
|
||||||
|
db_hrs
|
||||||
|
mod_lb_hrs
|
||||||
|
lbr_op
|
||||||
|
lbr_amt
|
||||||
|
op_code_desc
|
||||||
|
convertedtolbr
|
||||||
|
convertedtolbr_data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
export const UPDATE_JOB_LINE_STATUS = gql`
|
export const UPDATE_JOB_LINE_STATUS = gql`
|
||||||
mutation UPDATE_JOB_LINE_STATUS(
|
mutation UPDATE_JOB_LINE_STATUS(
|
||||||
$ids: [uuid!]!
|
$ids: [uuid!]!
|
||||||
|
|||||||
@@ -284,6 +284,7 @@ export const QUERY_JOBS_IN_PRODUCTION = gql`
|
|||||||
clm_no
|
clm_no
|
||||||
v_make_desc
|
v_make_desc
|
||||||
v_color
|
v_color
|
||||||
|
vehicleid
|
||||||
plate_no
|
plate_no
|
||||||
actual_in
|
actual_in
|
||||||
scheduled_completion
|
scheduled_completion
|
||||||
|
|||||||
@@ -98,6 +98,11 @@ const BillEnterModalContainer = lazy(() =>
|
|||||||
const TimeTicketModalContainer = lazy(() =>
|
const TimeTicketModalContainer = lazy(() =>
|
||||||
import("../../components/time-ticket-modal/time-ticket-modal.container")
|
import("../../components/time-ticket-modal/time-ticket-modal.container")
|
||||||
);
|
);
|
||||||
|
const TimeTicketModalTask = lazy(() =>
|
||||||
|
import(
|
||||||
|
"../../components/time-ticket-task-modal/time-ticket-task-modal.container"
|
||||||
|
)
|
||||||
|
);
|
||||||
const PaymentModalContainer = lazy(() =>
|
const PaymentModalContainer = lazy(() =>
|
||||||
import("../../components/payment-modal/payment-modal.container")
|
import("../../components/payment-modal/payment-modal.container")
|
||||||
);
|
);
|
||||||
@@ -207,6 +212,7 @@ export function Manage({ match, conflict, bodyshop }) {
|
|||||||
<ReportCenterModal />
|
<ReportCenterModal />
|
||||||
<EmailOverlayContainer />
|
<EmailOverlayContainer />
|
||||||
<TimeTicketModalContainer />
|
<TimeTicketModalContainer />
|
||||||
|
<TimeTicketModalTask />
|
||||||
<PrintCenterModalContainer />
|
<PrintCenterModalContainer />
|
||||||
<Route exact path={`${match.path}/_test`} component={TestComponent} />
|
<Route exact path={`${match.path}/_test`} component={TestComponent} />
|
||||||
<Route exact path={`${match.path}`} component={ManageRootPage} />
|
<Route exact path={`${match.path}`} component={ManageRootPage} />
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import {
|
|||||||
} from "../../redux/application/application.actions";
|
} from "../../redux/application/application.actions";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import ShopInfoUsersComponent from "../../components/shop-users/shop-users.component";
|
import ShopInfoUsersComponent from "../../components/shop-users/shop-users.component";
|
||||||
|
import ShopTeamsContainer from "../../components/shop-teams/shop-teams.container";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
});
|
});
|
||||||
@@ -44,6 +46,9 @@ export function ShopPage({ bodyshop, setSelectedHeader, setBreadcrumbs }) {
|
|||||||
<Tabs.TabPane tab={t("bodyshop.labels.employees")} key="employees">
|
<Tabs.TabPane tab={t("bodyshop.labels.employees")} key="employees">
|
||||||
<ShopEmployeesContainer />
|
<ShopEmployeesContainer />
|
||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
|
<Tabs.TabPane tab={t("bodyshop.labels.employee_teams")} key="teams">
|
||||||
|
<ShopTeamsContainer />
|
||||||
|
</Tabs.TabPane>
|
||||||
<Tabs.TabPane tab={t("bodyshop.labels.licensing")} key="licensing">
|
<Tabs.TabPane tab={t("bodyshop.labels.licensing")} key="licensing">
|
||||||
<ShopInfoUsersComponent />
|
<ShopInfoUsersComponent />
|
||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const INITIAL_STATE = {
|
|||||||
schedule: { ...baseModal },
|
schedule: { ...baseModal },
|
||||||
partsOrder: { ...baseModal },
|
partsOrder: { ...baseModal },
|
||||||
timeTicket: { ...baseModal },
|
timeTicket: { ...baseModal },
|
||||||
|
timeTicketTask: { ...baseModal },
|
||||||
printCenter: { ...baseModal },
|
printCenter: { ...baseModal },
|
||||||
reconciliation: { ...baseModal },
|
reconciliation: { ...baseModal },
|
||||||
payment: { ...baseModal },
|
payment: { ...baseModal },
|
||||||
|
|||||||
@@ -36,6 +36,10 @@ export const selectTimeTicket = createSelector(
|
|||||||
[selectModals],
|
[selectModals],
|
||||||
(modals) => modals.timeTicket
|
(modals) => modals.timeTicket
|
||||||
);
|
);
|
||||||
|
export const selectTimeTicketTasks = createSelector(
|
||||||
|
[selectModals],
|
||||||
|
(modals) => modals.timeTicketTask
|
||||||
|
);
|
||||||
|
|
||||||
export const selectPrintCenter = createSelector(
|
export const selectPrintCenter = createSelector(
|
||||||
[selectModals],
|
[selectModals],
|
||||||
|
|||||||
@@ -374,6 +374,9 @@
|
|||||||
"export": "CSI -> Export",
|
"export": "CSI -> Export",
|
||||||
"page": "CSI -> Page"
|
"page": "CSI -> Page"
|
||||||
},
|
},
|
||||||
|
"employee_teams": {
|
||||||
|
"page": "Employee Teams -> List"
|
||||||
|
},
|
||||||
"employees": {
|
"employees": {
|
||||||
"page": "Employees -> List"
|
"page": "Employees -> List"
|
||||||
},
|
},
|
||||||
@@ -541,6 +544,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",
|
||||||
@@ -572,6 +576,7 @@
|
|||||||
"title": "DMS"
|
"title": "DMS"
|
||||||
},
|
},
|
||||||
"emaillater": "Email Later",
|
"emaillater": "Email Later",
|
||||||
|
"employee_teams": "Employee Teams",
|
||||||
"employees": "Employees",
|
"employees": "Employees",
|
||||||
"estimators": "Estimators",
|
"estimators": "Estimators",
|
||||||
"filehandlers": "File Handlers",
|
"filehandlers": "File Handlers",
|
||||||
@@ -905,6 +910,15 @@
|
|||||||
"sent": "Email sent successfully."
|
"sent": "Email sent successfully."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"employee_teams": {
|
||||||
|
"actions": {
|
||||||
|
"new": "New Team"
|
||||||
|
},
|
||||||
|
"fields": {
|
||||||
|
"active": "Active",
|
||||||
|
"name": "Team Name"
|
||||||
|
}
|
||||||
|
},
|
||||||
"employees": {
|
"employees": {
|
||||||
"actions": {
|
"actions": {
|
||||||
"addvacation": "Add Vacation",
|
"addvacation": "Add Vacation",
|
||||||
@@ -1063,6 +1077,7 @@
|
|||||||
"sunday": "Sunday",
|
"sunday": "Sunday",
|
||||||
"text": "Text",
|
"text": "Text",
|
||||||
"thursday": "Thursday",
|
"thursday": "Thursday",
|
||||||
|
"total": "Total",
|
||||||
"totals": "Totals",
|
"totals": "Totals",
|
||||||
"tuesday": "Tuesday",
|
"tuesday": "Tuesday",
|
||||||
"unknown": "Unknown",
|
"unknown": "Unknown",
|
||||||
@@ -2605,6 +2620,7 @@
|
|||||||
},
|
},
|
||||||
"timetickets": {
|
"timetickets": {
|
||||||
"actions": {
|
"actions": {
|
||||||
|
"claimtasks": "Claim Tasks",
|
||||||
"clockin": "Clock In",
|
"clockin": "Clock In",
|
||||||
"clockout": "Clock Out",
|
"clockout": "Clock Out",
|
||||||
"enter": "Enter New Time Ticket",
|
"enter": "Enter New Time Ticket",
|
||||||
@@ -2663,7 +2679,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": {
|
||||||
|
|||||||
@@ -374,6 +374,9 @@
|
|||||||
"export": "",
|
"export": "",
|
||||||
"page": ""
|
"page": ""
|
||||||
},
|
},
|
||||||
|
"employee_teams": {
|
||||||
|
"page": ""
|
||||||
|
},
|
||||||
"employees": {
|
"employees": {
|
||||||
"page": ""
|
"page": ""
|
||||||
},
|
},
|
||||||
@@ -541,6 +544,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": "",
|
||||||
@@ -572,6 +576,7 @@
|
|||||||
"title": ""
|
"title": ""
|
||||||
},
|
},
|
||||||
"emaillater": "",
|
"emaillater": "",
|
||||||
|
"employee_teams": "",
|
||||||
"employees": "",
|
"employees": "",
|
||||||
"estimators": "",
|
"estimators": "",
|
||||||
"filehandlers": "",
|
"filehandlers": "",
|
||||||
@@ -905,6 +910,15 @@
|
|||||||
"sent": "Correo electrónico enviado con éxito."
|
"sent": "Correo electrónico enviado con éxito."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"employee_teams": {
|
||||||
|
"actions": {
|
||||||
|
"new": ""
|
||||||
|
},
|
||||||
|
"fields": {
|
||||||
|
"active": "",
|
||||||
|
"name": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
"employees": {
|
"employees": {
|
||||||
"actions": {
|
"actions": {
|
||||||
"addvacation": "",
|
"addvacation": "",
|
||||||
@@ -1063,6 +1077,7 @@
|
|||||||
"sunday": "",
|
"sunday": "",
|
||||||
"text": "",
|
"text": "",
|
||||||
"thursday": "",
|
"thursday": "",
|
||||||
|
"total": "",
|
||||||
"totals": "",
|
"totals": "",
|
||||||
"tuesday": "",
|
"tuesday": "",
|
||||||
"unknown": "Desconocido",
|
"unknown": "Desconocido",
|
||||||
@@ -2605,6 +2620,7 @@
|
|||||||
},
|
},
|
||||||
"timetickets": {
|
"timetickets": {
|
||||||
"actions": {
|
"actions": {
|
||||||
|
"claimtasks": "",
|
||||||
"clockin": "",
|
"clockin": "",
|
||||||
"clockout": "",
|
"clockout": "",
|
||||||
"enter": "",
|
"enter": "",
|
||||||
@@ -2663,7 +2679,8 @@
|
|||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
"clockoffmustbeafterclockon": "",
|
"clockoffmustbeafterclockon": "",
|
||||||
"clockoffwithoutclockon": ""
|
"clockoffwithoutclockon": "",
|
||||||
|
"hoursenteredmorethanavailable": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"titles": {
|
"titles": {
|
||||||
|
|||||||
@@ -374,6 +374,9 @@
|
|||||||
"export": "",
|
"export": "",
|
||||||
"page": ""
|
"page": ""
|
||||||
},
|
},
|
||||||
|
"employee_teams": {
|
||||||
|
"page": ""
|
||||||
|
},
|
||||||
"employees": {
|
"employees": {
|
||||||
"page": ""
|
"page": ""
|
||||||
},
|
},
|
||||||
@@ -541,6 +544,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": "",
|
||||||
@@ -572,6 +576,7 @@
|
|||||||
"title": ""
|
"title": ""
|
||||||
},
|
},
|
||||||
"emaillater": "",
|
"emaillater": "",
|
||||||
|
"employee_teams": "",
|
||||||
"employees": "",
|
"employees": "",
|
||||||
"estimators": "",
|
"estimators": "",
|
||||||
"filehandlers": "",
|
"filehandlers": "",
|
||||||
@@ -905,6 +910,15 @@
|
|||||||
"sent": "E-mail envoyé avec succès."
|
"sent": "E-mail envoyé avec succès."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"employee_teams": {
|
||||||
|
"actions": {
|
||||||
|
"new": ""
|
||||||
|
},
|
||||||
|
"fields": {
|
||||||
|
"active": "",
|
||||||
|
"name": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
"employees": {
|
"employees": {
|
||||||
"actions": {
|
"actions": {
|
||||||
"addvacation": "",
|
"addvacation": "",
|
||||||
@@ -1063,6 +1077,7 @@
|
|||||||
"sunday": "",
|
"sunday": "",
|
||||||
"text": "",
|
"text": "",
|
||||||
"thursday": "",
|
"thursday": "",
|
||||||
|
"total": "",
|
||||||
"totals": "",
|
"totals": "",
|
||||||
"tuesday": "",
|
"tuesday": "",
|
||||||
"unknown": "Inconnu",
|
"unknown": "Inconnu",
|
||||||
@@ -2605,6 +2620,7 @@
|
|||||||
},
|
},
|
||||||
"timetickets": {
|
"timetickets": {
|
||||||
"actions": {
|
"actions": {
|
||||||
|
"claimtasks": "",
|
||||||
"clockin": "",
|
"clockin": "",
|
||||||
"clockout": "",
|
"clockout": "",
|
||||||
"enter": "",
|
"enter": "",
|
||||||
@@ -2663,7 +2679,8 @@
|
|||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
"clockoffmustbeafterclockon": "",
|
"clockoffmustbeafterclockon": "",
|
||||||
"clockoffwithoutclockon": ""
|
"clockoffwithoutclockon": "",
|
||||||
|
"hoursenteredmorethanavailable": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"titles": {
|
"titles": {
|
||||||
|
|||||||
@@ -761,6 +761,13 @@
|
|||||||
table:
|
table:
|
||||||
name: email_audit_trail
|
name: email_audit_trail
|
||||||
schema: public
|
schema: public
|
||||||
|
- name: employee_teams
|
||||||
|
using:
|
||||||
|
foreign_key_constraint_on:
|
||||||
|
column: bodyshopid
|
||||||
|
table:
|
||||||
|
name: employee_teams
|
||||||
|
schema: public
|
||||||
- name: employees
|
- name: employees
|
||||||
using:
|
using:
|
||||||
foreign_key_constraint_on:
|
foreign_key_constraint_on:
|
||||||
@@ -928,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
|
||||||
@@ -1016,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
|
||||||
@@ -1945,6 +1954,165 @@
|
|||||||
- active:
|
- active:
|
||||||
_eq: true
|
_eq: true
|
||||||
check: null
|
check: null
|
||||||
|
- table:
|
||||||
|
name: employee_team_members
|
||||||
|
schema: public
|
||||||
|
object_relationships:
|
||||||
|
- name: employee
|
||||||
|
using:
|
||||||
|
foreign_key_constraint_on: employeeid
|
||||||
|
- name: employee_team
|
||||||
|
using:
|
||||||
|
foreign_key_constraint_on: teamid
|
||||||
|
insert_permissions:
|
||||||
|
- role: user
|
||||||
|
permission:
|
||||||
|
check:
|
||||||
|
employee_team:
|
||||||
|
bodyshop:
|
||||||
|
associations:
|
||||||
|
_and:
|
||||||
|
- user:
|
||||||
|
authid:
|
||||||
|
_eq: X-Hasura-User-Id
|
||||||
|
- active:
|
||||||
|
_eq: true
|
||||||
|
columns:
|
||||||
|
- labor_rates
|
||||||
|
- percentage
|
||||||
|
- created_at
|
||||||
|
- updated_at
|
||||||
|
- employeeid
|
||||||
|
- id
|
||||||
|
- teamid
|
||||||
|
select_permissions:
|
||||||
|
- role: user
|
||||||
|
permission:
|
||||||
|
columns:
|
||||||
|
- labor_rates
|
||||||
|
- percentage
|
||||||
|
- created_at
|
||||||
|
- updated_at
|
||||||
|
- employeeid
|
||||||
|
- id
|
||||||
|
- teamid
|
||||||
|
filter:
|
||||||
|
employee_team:
|
||||||
|
bodyshop:
|
||||||
|
associations:
|
||||||
|
_and:
|
||||||
|
- user:
|
||||||
|
authid:
|
||||||
|
_eq: X-Hasura-User-Id
|
||||||
|
- active:
|
||||||
|
_eq: true
|
||||||
|
update_permissions:
|
||||||
|
- role: user
|
||||||
|
permission:
|
||||||
|
columns:
|
||||||
|
- labor_rates
|
||||||
|
- percentage
|
||||||
|
- created_at
|
||||||
|
- updated_at
|
||||||
|
- employeeid
|
||||||
|
- id
|
||||||
|
- teamid
|
||||||
|
filter:
|
||||||
|
employee_team:
|
||||||
|
bodyshop:
|
||||||
|
associations:
|
||||||
|
_and:
|
||||||
|
- user:
|
||||||
|
authid:
|
||||||
|
_eq: X-Hasura-User-Id
|
||||||
|
- active:
|
||||||
|
_eq: true
|
||||||
|
check: null
|
||||||
|
delete_permissions:
|
||||||
|
- role: user
|
||||||
|
permission:
|
||||||
|
backend_only: false
|
||||||
|
filter:
|
||||||
|
employee_team:
|
||||||
|
bodyshop:
|
||||||
|
associations:
|
||||||
|
_and:
|
||||||
|
- user:
|
||||||
|
authid:
|
||||||
|
_eq: X-Hasura-User-Id
|
||||||
|
- active:
|
||||||
|
_eq: true
|
||||||
|
- table:
|
||||||
|
name: employee_teams
|
||||||
|
schema: public
|
||||||
|
object_relationships:
|
||||||
|
- name: bodyshop
|
||||||
|
using:
|
||||||
|
foreign_key_constraint_on: bodyshopid
|
||||||
|
array_relationships:
|
||||||
|
- name: employee_team_members
|
||||||
|
using:
|
||||||
|
foreign_key_constraint_on:
|
||||||
|
column: teamid
|
||||||
|
table:
|
||||||
|
name: employee_team_members
|
||||||
|
schema: public
|
||||||
|
insert_permissions:
|
||||||
|
- role: user
|
||||||
|
permission:
|
||||||
|
check:
|
||||||
|
bodyshop:
|
||||||
|
associations:
|
||||||
|
_and:
|
||||||
|
- user:
|
||||||
|
authid:
|
||||||
|
_eq: X-Hasura-User-Id
|
||||||
|
- active:
|
||||||
|
_eq: true
|
||||||
|
columns:
|
||||||
|
- active
|
||||||
|
- name
|
||||||
|
- created_at
|
||||||
|
- updated_at
|
||||||
|
- bodyshopid
|
||||||
|
- id
|
||||||
|
select_permissions:
|
||||||
|
- role: user
|
||||||
|
permission:
|
||||||
|
columns:
|
||||||
|
- active
|
||||||
|
- name
|
||||||
|
- created_at
|
||||||
|
- updated_at
|
||||||
|
- bodyshopid
|
||||||
|
- id
|
||||||
|
filter:
|
||||||
|
bodyshop:
|
||||||
|
associations:
|
||||||
|
_and:
|
||||||
|
- user:
|
||||||
|
authid:
|
||||||
|
_eq: X-Hasura-User-Id
|
||||||
|
- active:
|
||||||
|
_eq: true
|
||||||
|
update_permissions:
|
||||||
|
- role: user
|
||||||
|
permission:
|
||||||
|
columns:
|
||||||
|
- active
|
||||||
|
- bodyshopid
|
||||||
|
- name
|
||||||
|
- updated_at
|
||||||
|
filter:
|
||||||
|
bodyshop:
|
||||||
|
associations:
|
||||||
|
_and:
|
||||||
|
- user:
|
||||||
|
authid:
|
||||||
|
_eq: X-Hasura-User-Id
|
||||||
|
- active:
|
||||||
|
_eq: true
|
||||||
|
check: null
|
||||||
- table:
|
- table:
|
||||||
name: employee_vacation
|
name: employee_vacation
|
||||||
schema: public
|
schema: public
|
||||||
@@ -2045,6 +2213,13 @@
|
|||||||
table:
|
table:
|
||||||
name: allocations
|
name: allocations
|
||||||
schema: public
|
schema: public
|
||||||
|
- name: employee_team_members
|
||||||
|
using:
|
||||||
|
foreign_key_constraint_on:
|
||||||
|
column: employeeid
|
||||||
|
table:
|
||||||
|
name: employee_team_members
|
||||||
|
schema: public
|
||||||
- name: employee_vacations
|
- name: employee_vacations
|
||||||
using:
|
using:
|
||||||
foreign_key_constraint_on:
|
foreign_key_constraint_on:
|
||||||
@@ -2218,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:
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE "public"."employee_teams";
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
CREATE TABLE "public"."employee_teams" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), "bodyshopid" uuid NOT NULL, "name" text NOT NULL, "active" boolean NOT NULL DEFAULT true, PRIMARY KEY ("id") , FOREIGN KEY ("bodyshopid") REFERENCES "public"."bodyshops"("id") ON UPDATE cascade ON DELETE cascade);
|
||||||
|
CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
DECLARE
|
||||||
|
_new record;
|
||||||
|
BEGIN
|
||||||
|
_new := NEW;
|
||||||
|
_new."updated_at" = NOW();
|
||||||
|
RETURN _new;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
CREATE TRIGGER "set_public_employee_teams_updated_at"
|
||||||
|
BEFORE UPDATE ON "public"."employee_teams"
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"();
|
||||||
|
COMMENT ON TRIGGER "set_public_employee_teams_updated_at" ON "public"."employee_teams"
|
||||||
|
IS 'trigger to set value of column "updated_at" to current timestamp on row update';
|
||||||
|
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE "public"."employee_team_members";
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
CREATE TABLE "public"."employee_team_members" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), "teamid" uuid NOT NULL, "employeeid" uuid NOT NULL, "labor_rates" jsonb NOT NULL DEFAULT jsonb_build_object(), "percentage" numeric NOT NULL DEFAULT 0, PRIMARY KEY ("id") , FOREIGN KEY ("teamid") REFERENCES "public"."employee_teams"("id") ON UPDATE cascade ON DELETE cascade, FOREIGN KEY ("employeeid") REFERENCES "public"."employees"("id") ON UPDATE cascade ON DELETE cascade);
|
||||||
|
CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
DECLARE
|
||||||
|
_new record;
|
||||||
|
BEGIN
|
||||||
|
_new := NEW;
|
||||||
|
_new."updated_at" = NOW();
|
||||||
|
RETURN _new;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
CREATE TRIGGER "set_public_employee_team_members_updated_at"
|
||||||
|
BEFORE UPDATE ON "public"."employee_team_members"
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"();
|
||||||
|
COMMENT ON TRIGGER "set_public_employee_team_members_updated_at" ON "public"."employee_team_members"
|
||||||
|
IS 'trigger to set value of column "updated_at" to current timestamp on row update';
|
||||||
|
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- Could not auto-generate a down migration.
|
||||||
|
-- Please write an appropriate down migration for the SQL below:
|
||||||
|
-- alter table "public"."exportlog" add column "metadata" jsonb
|
||||||
|
-- null;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
alter table "public"."exportlog" add column "metadata" jsonb
|
||||||
|
null;
|
||||||
@@ -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';
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
alter table "public"."bodyshops" add column "tt_enforce_hours_for_tech_console" boolean
|
||||||
|
not null default 'false';
|
||||||
@@ -249,10 +249,10 @@ async function QueryCustomersFromDms(socket) {
|
|||||||
SerialNumber: socket.JobData.bodyshop.pbs_serialnumber,
|
SerialNumber: socket.JobData.bodyshop.pbs_serialnumber,
|
||||||
//ContactId: "00000000000000000000000000000000",
|
//ContactId: "00000000000000000000000000000000",
|
||||||
// ContactCode: socket.JobData.owner.accountingid,
|
// ContactCode: socket.JobData.owner.accountingid,
|
||||||
FirstName: socket.JobData.ownr_co_nm
|
FirstName: socket.JobData.ownr_fn,
|
||||||
|
LastName: socket.JobData.ownr_co_nm
|
||||||
? socket.JobData.ownr_co_nm
|
? socket.JobData.ownr_co_nm
|
||||||
: socket.JobData.ownr_fn,
|
: socket.JobData.ownr_ln,
|
||||||
LastName: socket.JobData.ownr_ln,
|
|
||||||
PhoneNumber: socket.JobData.ownr_ph1,
|
PhoneNumber: socket.JobData.ownr_ph1,
|
||||||
EmailAddress: socket.JobData.ownr_ea,
|
EmailAddress: socket.JobData.ownr_ea,
|
||||||
// ModifiedSince: "0001-01-01T00:00:00.0000000Z",
|
// ModifiedSince: "0001-01-01T00:00:00.0000000Z",
|
||||||
|
|||||||
@@ -224,6 +224,7 @@ async function CdkSelectedCustomer(socket, selectedCustomerId) {
|
|||||||
} finally {
|
} finally {
|
||||||
//Ensure we always insert logEvents
|
//Ensure we always insert logEvents
|
||||||
//GQL to insert logevents.
|
//GQL to insert logevents.
|
||||||
|
|
||||||
CdkBase.createLogEvent(
|
CdkBase.createLogEvent(
|
||||||
socket,
|
socket,
|
||||||
"DEBUG",
|
"DEBUG",
|
||||||
@@ -1213,6 +1214,7 @@ async function GenerateTransWips(socket) {
|
|||||||
|
|
||||||
wips.push(item);
|
wips.push(item);
|
||||||
});
|
});
|
||||||
|
socket.transWips = wips;
|
||||||
return wips;
|
return wips;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1388,6 +1390,7 @@ async function MarkJobExported(socket, jobid) {
|
|||||||
jobid: jobid,
|
jobid: jobid,
|
||||||
successful: true,
|
successful: true,
|
||||||
useremail: socket.user.email,
|
useremail: socket.user.email,
|
||||||
|
metadata: socket.transWips,
|
||||||
},
|
},
|
||||||
bill: {
|
bill: {
|
||||||
exported: true,
|
exported: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user