diff --git a/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.component.jsx b/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.component.jsx index 8f0c2235b..53400ccd7 100644 --- a/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.component.jsx +++ b/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.component.jsx @@ -256,7 +256,7 @@ export function JobsDetailHeaderActions({ onClick={() => { setTimeTicketTaskContext({ actions: {}, - context: { jobId: job.id }, + context: { jobid: job.id }, }); }} > diff --git a/client/src/components/shop-teams/shop-employee-teams.form.component.jsx b/client/src/components/shop-teams/shop-employee-teams.form.component.jsx index b9ecdb848..904acda7d 100644 --- a/client/src/components/shop-teams/shop-employee-teams.form.component.jsx +++ b/client/src/components/shop-teams/shop-employee-teams.form.component.jsx @@ -2,6 +2,7 @@ import { DeleteFilled } from "@ant-design/icons"; import { useMutation, useQuery } from "@apollo/client"; import { Button, + Space, Card, Form, Input, @@ -383,16 +384,18 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) { > - { - remove(field.name); - }} - /> - + + { + remove(field.name); + }} + /> + + ))} diff --git a/client/src/components/time-ticket-list/time-ticket-list-team-pay.component.jsx b/client/src/components/time-ticket-list/time-ticket-list-team-pay.component.jsx index 74e326faf..3c3375036 100644 --- a/client/src/components/time-ticket-list/time-ticket-list-team-pay.component.jsx +++ b/client/src/components/time-ticket-list/time-ticket-list-team-pay.component.jsx @@ -1,3 +1,4 @@ +import { useQuery } from "@apollo/client"; import { Button, Form, @@ -9,17 +10,16 @@ import { 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"; -import Dinero from "dinero.js"; -import { useQuery } from "@apollo/client"; -import { GET_JOB_INFO_DRAW_CALCULATIONS } from "../../graphql/jobs-lines.queries"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, }); @@ -30,20 +30,20 @@ export default connect( )(TimeTicketListTeamPay); export function TimeTicketListTeamPay({ bodyshop, context, actions }) { - const { refetch } = 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 { + //loading, + data: lineTicketData, + } = useQuery(GET_JOB_INFO_DRAW_CALCULATIONS, { + variables: { id: jobId }, + skip: !jobId, + fetchPolicy: "network-only", + nextFetchPolicy: "network-only", + }); const handleOk = () => { setVisible(false); @@ -124,7 +124,7 @@ export function TimeTicketListTeamPay({ bodyshop, context, actions }) { {({ getFieldsValue }) => { const formData = getFieldsValue(); - console.log("🚀 ~ file: time-ticket-list-team-pay.component.jsx:127 ~ TimeTicketListTeamPay ~ formData:", formData) + let data = []; let eligibleHours = 0; const theTeam = Teams.find((team) => team.name === formData.team); diff --git a/client/src/components/time-ticket-list/time-ticket-list.component.jsx b/client/src/components/time-ticket-list/time-ticket-list.component.jsx index fadde7f70..ba4aeda2f 100644 --- a/client/src/components/time-ticket-list/time-ticket-list.component.jsx +++ b/client/src/components/time-ticket-list/time-ticket-list.component.jsx @@ -1,35 +1,37 @@ import { EditFilled } from "@ant-design/icons"; -import { Card, Space, Table } from "antd"; +import { Button, Card, Space, Table } from "antd"; +import Dinero from "dinero.js"; import moment from "moment"; import React, { useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { Link } from "react-router-dom"; import { createStructuredSelector } from "reselect"; +import { setModalContext } from "../../redux/modals/modals.actions"; import { selectAuthLevel, selectBodyshop, } from "../../redux/user/user.selectors"; -import { onlyUnique } from "../../utils/arrayHelper"; import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter"; +import { onlyUnique } from "../../utils/arrayHelper"; import { alphaSort, dateSort } from "../../utils/sorters"; import RbacWrapper, { HasRbacAccess, } from "../rbac-wrapper/rbac-wrapper.component"; -import Dinero from "dinero.js"; import TimeTicketEnterButton from "../time-ticket-enter-button/time-ticket-enter-button.component"; -import TimeTicketListTeamPay from "./time-ticket-list-team-pay.component"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, authLevel: selectAuthLevel, }); const mapDispatchToProps = (dispatch) => ({ - //setUserLanguage: language => dispatch(setUserLanguage(language)) + setTimeTicketTaskContext: (context) => + dispatch(setModalContext({ context: context, modal: "timeTicketTask" })), }); export default connect(mapStateToProps, mapDispatchToProps)(TimeTicketList); export function TimeTicketList({ bodyshop, + setTimeTicketTaskContext, authLevel, disabled, loading, @@ -261,10 +263,23 @@ export function TimeTicketList({ title={t("timetickets.labels.timetickets")} extra={ - + { + // + } + {jobId && (techConsole ? null : ( ({ - validator(rule, value) { - if (!bodyshop.tt_enforce_hours_for_tech_console) { - return Promise.resolve(); - } - if ( - !value || - getFieldValue("cost_center") === null || - !lineTicketData - ) - return Promise.resolve(); + validator(rule, value) { + if (!bodyshop.tt_enforce_hours_for_tech_console) { + return Promise.resolve(); + } + if ( + !value || + getFieldValue("cost_center") === null || + !lineTicketData + ) + return Promise.resolve(); - //Check the cost center, - const totals = CalculateAllocationsTotals( - bodyshop, - lineTicketData.joblines, - lineTicketData.timetickets, - lineTicketData.jobs_by_pk.lbr_adjustments - ); - - const fieldTypeToCheck = - bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber - ? "mod_lbr_ty" - : "cost_center"; - - const costCenterDiff = - Math.round( - totals.find( - (total) => - total[fieldTypeToCheck] === - getFieldValue("cost_center") - )?.difference * 10 - ) / 10; - - if (value > costCenterDiff) - return Promise.reject( - t( - "timetickets.validation.hoursenteredmorethanavailable" - ) + //Check the cost center, + const totals = CalculateAllocationsTotals( + bodyshop, + lineTicketData.joblines, + lineTicketData.timetickets, + lineTicketData.jobs_by_pk.lbr_adjustments ); - else { - return Promise.resolve(); - } - }, - }), - { + + 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", @@ -371,7 +371,12 @@ export function TimeTicketModalComponent({ ); } -export function LaborAllocationContainer({ jobid, loading, lineTicketData }) { +export function LaborAllocationContainer({ + jobid, + loading, + lineTicketData, + hideTimeTickets = false, +}) { if (loading) return ; if (!lineTicketData) return null; return ( @@ -382,12 +387,13 @@ export function LaborAllocationContainer({ jobid, loading, lineTicketData }) { timetickets={lineTicketData.timetickets} adjustments={lineTicketData.jobs_by_pk.lbr_adjustments} /> - - + {!hideTimeTickets && ( + + )} ); } diff --git a/client/src/components/time-ticket-task-modal/time-ticket-task-modal.component.jsx b/client/src/components/time-ticket-task-modal/time-ticket-task-modal.component.jsx index db11dc42f..0884b656a 100644 --- a/client/src/components/time-ticket-task-modal/time-ticket-task-modal.component.jsx +++ b/client/src/components/time-ticket-task-modal/time-ticket-task-modal.component.jsx @@ -1,4 +1,15 @@ -import { Button, Form, Input, InputNumber, Radio } from "antd"; +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"; @@ -6,9 +17,12 @@ 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 FormDatePickerComponent from "../form-date-picker/form-date-picker.component"; +import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component"; import JobSearchSelectComponent from "../job-search-select/job-search-select.component"; -import LayoutFormRow from "../layout-form-row/layout-form-row.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 @@ -26,17 +40,59 @@ export function TimeTicketTaskModalComponent({ bodyshop, form, employeeAutoCompleteOptions, + lineTicketCalled, + calculateTimeTickets, + lineTicketLoading, + lineTicketData, + queryJobInfo, }) { const { t } = useTranslation(); return (
- - - {() => ( + + + + + + + + + { + const emps = + employeeAutoCompleteOptions && + employeeAutoCompleteOptions.filter((e) => e.id === value)[0]; + form.setFieldsValue({ flat_rate: emps && emps.flat_rate }); + }} + /> + + + + + - + + + + Body + + + Refinish + + + Mechanical + + + Frame + + + Glass + + + - )} - - - { - const emps = - employeeAutoCompleteOptions && - employeeAutoCompleteOptions.filter((e) => e.id === value)[0]; - form.setFieldsValue({ flat_rate: emps && emps.flat_rate }); + + + + + + + + + + {() => { + const data = form.getFieldValue("timetickets"); + return ( + { + 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", + }, + ]} + /> + ); }} - /> - - - - - - - Body - Refinish - Mechanical - Frame - Glass - - + - - - - + { + //Check the cost center, + const totals = CalculateAllocationsTotals( + bodyshop, + lineTicketData.joblines, + lineTicketData.timetickets, + lineTicketData.jobs_by_pk.lbr_adjustments + ); - - {(fields, { add, remove, move }) => { + 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 ( +
+ {errors.map((e, idx) => ( + + ))} +
+ {fields.map((field, index) => ( + + + + + + + + + + + + + + + + + + + + + + + + + + ))} +
+
+ ); + }} +
+ + + + {() => { + const jobid = form.getFieldValue("jobid"); + + if ( + (!lineTicketCalled && jobid) || + (jobid && + lineTicketData?.jobs_by_pk?.id !== jobid && + !lineTicketLoading) + ) { + queryJobInfo({ variables: { id: jobid } }).then(() => + calculateTimeTickets() + ); + } return ( -
- {fields.map((field, index) => ( - - - - - - - - - - - - - - - - - - - - - - - - - - ))} - - - -
+ ); }} -
+ ); } diff --git a/client/src/components/time-ticket-task-modal/time-ticket-task-modal.container.jsx b/client/src/components/time-ticket-task-modal/time-ticket-task-modal.container.jsx index 31e909cf7..44e17185d 100644 --- a/client/src/components/time-ticket-task-modal/time-ticket-task-modal.container.jsx +++ b/client/src/components/time-ticket-task-modal/time-ticket-task-modal.container.jsx @@ -1,16 +1,20 @@ -import React from "react"; +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 { selectTimeTicketTasks } from "../../redux/modals/modals.selectors"; -import { Modal, Form } from "antd"; -import { toggleModalVisible } from "../../redux/modals/modals.actions"; import { QUERY_ACTIVE_EMPLOYEES } from "../../graphql/employees.queries"; -import { useLazyQuery, useQuery } from "@apollo/client"; -import TimeTicketTaskModalComponent from "./time-ticket-task-modal.component"; 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 Dinero from "dinero.js"; +import TimeTicketTaskModalComponent from "./time-ticket-task-modal.component"; const mapStateToProps = createStructuredSelector({ timeTicketTasksModal: selectTimeTicketTasks, @@ -36,30 +40,63 @@ export function TimeTickeTaskModalContainer({ fetchPolicy: "network-only", nextFetchPolicy: "network-only", }); - //Query the Job Information and Prefill the Form. - const [queryJobInfo, { loading, data: lineTicketData }] = useLazyQuery( - GET_JOB_INFO_DRAW_CALCULATIONS, - { + 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 { } - ); + } - async function handleFinish(values) {} + useEffect(() => { + if (context.jobid) { + console.log("UE Fired."); + queryJobInfo({ variables: { id: context.jobid } }); + } + }, [context.jobid, queryJobInfo]); - const handleFieldsChange = async (changed, allFields) => { + const calculateTimeTickets = (presetMemo) => { const formData = form.getFieldsValue(); - if (changed[0].name[0] === "jobid") { - await queryJobInfo({ variables: { id: changed[0].value } }); - } - if ( !formData.jobid || !formData.employeeteamid || !formData.hourstype || - !formData.percent + formData.hourstype.length === 0 || + !formData.percent || + !lineTicketData ) { - console.log("Not everything populated."); return; } let data = []; @@ -68,56 +105,75 @@ export function TimeTickeTaskModalContainer({ const theTeam = JSON.parse(formData.employeeteamid); 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 = []; - data = theTeam.employee_team_members.map((e) => { - return { - employeeid: e.id, - date: 0, - percentage: e.percentage, - rate: e.labor_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.labor_rates[formData.hourstype] || 0) * 100), - }) - .multiply( - Math.round(eligibleHours * 100 * (e.percentage / 100)) / 100 - ) - .toFormat("$0.00"), - }; + 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 }); + + form.setFieldsValue({ + timetickets: data.filter((d) => d.productivehrs > 0), + }); + form.validateFields(); } }; + return ( toggleModalVisible()} width="80%" + onOk={() => form.submit()} >
diff --git a/client/src/components/time-ticket-tasks-presets/time-ticket-tasks-presets.component.jsx b/client/src/components/time-ticket-tasks-presets/time-ticket-tasks-presets.component.jsx new file mode 100644 index 000000000..f88ba4d1a --- /dev/null +++ b/client/src/components/time-ticket-tasks-presets/time-ticket-tasks-presets.component.jsx @@ -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 ( + ({ label: p.name, key: p.name })), + onClick: handleClick, + }} + > + + + ); +} + +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 }, +]; diff --git a/client/src/graphql/employee_teams.queries.js b/client/src/graphql/employee_teams.queries.js index 842fb8353..5d80abad2 100644 --- a/client/src/graphql/employee_teams.queries.js +++ b/client/src/graphql/employee_teams.queries.js @@ -7,6 +7,7 @@ export const QUERY_TEAMS = gql` name employee_team_members { id + employeeid labor_rates percentage } diff --git a/client/src/graphql/jobs-lines.queries.js b/client/src/graphql/jobs-lines.queries.js index 130214a30..26e46c470 100644 --- a/client/src/graphql/jobs-lines.queries.js +++ b/client/src/graphql/jobs-lines.queries.js @@ -95,6 +95,27 @@ export const GET_JOB_INFO_DRAW_CALCULATIONS = gql` 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