diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index ab5b328f4..96e28fd18 100644 --- a/bodyshop_translations.babel +++ b/bodyshop_translations.babel @@ -44838,6 +44838,48 @@ + + commit + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + commitone + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + enter false @@ -44880,6 +44922,27 @@ + + uncommit + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + @@ -45803,6 +45866,27 @@ + + committed + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + created false diff --git a/client/src/components/employee-search-select/employee-search-select.component.jsx b/client/src/components/employee-search-select/employee-search-select.component.jsx index 85dcc05e4..659a3556e 100644 --- a/client/src/components/employee-search-select/employee-search-select.component.jsx +++ b/client/src/components/employee-search-select/employee-search-select.component.jsx @@ -1,10 +1,10 @@ import { Select, Space, Tag } from "antd"; -import React, { forwardRef } from "react"; +import React from "react"; import { useTranslation } from "react-i18next"; const { Option } = Select; //To be used as a form element only. -const EmployeeSearchSelect = ({ options, ...props }, ref) => { +const EmployeeSearchSelect = ({ options, ...props }) => { const { t } = useTranslation(); return ( @@ -39,4 +39,4 @@ const EmployeeSearchSelect = ({ options, ...props }, ref) => { ); }; -export default forwardRef(EmployeeSearchSelect); +export default EmployeeSearchSelect; diff --git a/client/src/components/time-ticket-modal/time-ticket-modal.component.jsx b/client/src/components/time-ticket-modal/time-ticket-modal.component.jsx index 3f4519eb0..7b5647d6d 100644 --- a/client/src/components/time-ticket-modal/time-ticket-modal.component.jsx +++ b/client/src/components/time-ticket-modal/time-ticket-modal.component.jsx @@ -37,6 +37,7 @@ export function TimeTicketModalComponent({ authLevel, employeeAutoCompleteOptions, isEdit, + disabled, employeeSelectDisabled, }) { const { t } = useTranslation(); @@ -50,7 +51,7 @@ export function TimeTicketModalComponent({ ); }; @@ -126,7 +127,7 @@ export function TimeTicketModalComponent({ ]} > { const emps = @@ -278,6 +279,7 @@ export function TimeTicketModalComponent({ - {timeTicketModal.context && timeTicketModal.context.id ? null : ( @@ -198,6 +203,7 @@ export function TimeTicketModalContainer({ autoComplete={"off"} form={form} onFinishFailed={() => setEnterAgain(false)} + disabled={timeTicketModal.context?.timeticket?.committed_at} initialValues={ timeTicketModal.context.timeticket ? { @@ -218,6 +224,9 @@ export function TimeTicketModalContainer({ + @@ -241,14 +250,16 @@ export function TimeTicketModalContainer({ 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 721311443..8608a26da 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 @@ -55,11 +55,7 @@ export function TimeTicketTaskModalComponent({ calculateTimeTickets={calculateTimeTickets} /> - {bodyshop?.md_tasks_presets?.use_approvals && ( - - - - )} + + {bodyshop?.md_tasks_presets?.use_approvals && ( + + + + )} ); } diff --git a/client/src/components/time-tickets-commit-toggle/time-tickets-commit-toggle.component.jsx b/client/src/components/time-tickets-commit-toggle/time-tickets-commit-toggle.component.jsx new file mode 100644 index 000000000..9a85f6507 --- /dev/null +++ b/client/src/components/time-tickets-commit-toggle/time-tickets-commit-toggle.component.jsx @@ -0,0 +1,107 @@ +import { useMutation } from "@apollo/client"; +import { Button, notification } from "antd"; +import moment from "moment"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { + UPDATE_TIME_TICKET, + UPDATE_TIME_TICKETS, +} from "../../graphql/timetickets.queries"; +import { + selectBodyshop, + selectCurrentUser, +} from "../../redux/user/user.selectors"; +import { setModalContext } from "../../redux/modals/modals.actions"; + +const mapStateToProps = createStructuredSelector({ + bodyshop: selectBodyshop, + currentUser: selectCurrentUser, +}); +const mapDispatchToProps = (dispatch) => ({ + setTimeTicketContext: (context) => + dispatch(setModalContext({ context: context, modal: "timeTicket" })), +}); +export function TimeTicketsCommit({ + bodyshop, + currentUser, + timeticket, + disabled, + refetch, + setTimeTicketContext, +}) { + const { t } = useTranslation(); + const [updateTimeTicket] = useMutation(UPDATE_TIME_TICKET); + + const [loading, setLoading] = useState(false); + const handleCommit = async () => { + setLoading(true); + try { + const ticketUpdate = timeticket.committed_at + ? { commited_by: null, committed_at: null } + : { + commited_by: currentUser.email, + committed_at: moment(), + }; + + const result = await updateTimeTicket({ + variables: { + timeticketId: timeticket.id, + timeticket: ticketUpdate, + }, + update(cache) { + cache.modify({ + fields: { + timeTickets(existingtickets, { readField }) { + return existingtickets.map((ticket) => { + if (timeticket.id === readField("id", ticket)) { + return { + ...ticket, + ...ticketUpdate, + }; + } + return ticket; + }); + }, + }, + }); + }, + }); + if (result.errors) { + notification.open({ + type: "error", + message: t("timetickets.errors.creating", { + message: JSON.stringify(result.errors), + }), + }); + } else { + setTimeTicketContext({ + context: { + id: timeticket.id, + timeticket: result.data.update_timetickets.returning[0], + }, + }); + notification.open({ + type: "success", + message: t("timetickets.successes.committed"), + }); + } + } catch (error) { + } finally { + setLoading(false); + } + }; + + if (!timeticket?.id) return null; + + return ( + + ); +} + +export default connect(mapStateToProps, mapDispatchToProps)(TimeTicketsCommit); diff --git a/client/src/components/time-tickets-commit/time-tickets-commit.component.jsx b/client/src/components/time-tickets-commit/time-tickets-commit.component.jsx new file mode 100644 index 000000000..87f550360 --- /dev/null +++ b/client/src/components/time-tickets-commit/time-tickets-commit.component.jsx @@ -0,0 +1,95 @@ +import { useMutation } from "@apollo/client"; +import { Button, notification } from "antd"; +import moment from "moment"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { UPDATE_TIME_TICKETS } from "../../graphql/timetickets.queries"; +import { + selectBodyshop, + selectCurrentUser, +} from "../../redux/user/user.selectors"; + +const mapStateToProps = createStructuredSelector({ + bodyshop: selectBodyshop, + currentUser: selectCurrentUser, +}); + +export function TimeTicketsCommit({ + bodyshop, + currentUser, + timetickets, + disabled, + loadingCallback, + completedCallback, + refetch, +}) { + const { t } = useTranslation(); + const [updateTimeTickets] = useMutation(UPDATE_TIME_TICKETS); + + const [loading, setLoading] = useState(false); + const handleCommit = async () => { + setLoading(true); + try { + const result = await updateTimeTickets({ + variables: { + timeticketIds: timetickets.map((ticket) => ticket.id), + timeticket: { + commited_by: currentUser.email, + committed_at: moment(), + }, + }, + update(cache) { + cache.modify({ + fields: { + timeTickets(existingtickets, { readField }) { + const modifiedIds = timetickets.map((ticket) => ticket.id); + return existingtickets.map((ticket) => { + if (modifiedIds.includes(readField("id", ticket))) { + return { + ...ticket, + commited_by: currentUser.email, + committed_at: moment(), + }; + } + return ticket; + }); + }, + }, + }); + }, + }); + 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.committed"), + }); + if (!!completedCallback) completedCallback([]); + if (!!loadingCallback) loadingCallback(false); + } + } catch (error) { + } finally { + setLoading(false); + } + }; + + return ( + + ); +} + +export default connect(mapStateToProps, null)(TimeTicketsCommit); diff --git a/client/src/graphql/timetickets.queries.js b/client/src/graphql/timetickets.queries.js index 2d8c0c452..a62032f40 100644 --- a/client/src/graphql/timetickets.queries.js +++ b/client/src/graphql/timetickets.queries.js @@ -15,6 +15,8 @@ export const QUERY_TICKETS_BY_JOBID = gql` memo jobid flat_rate + commited_by + committed_at employee { employee_number first_name @@ -44,6 +46,8 @@ export const QUERY_TIME_TICKETS_IN_RANGE = gql` memo jobid flat_rate + commited_by + committed_at job { id ro_number @@ -86,6 +90,8 @@ export const QUERY_TIME_TICKETS_TECHNICIAN_IN_RANGE = gql` productivehrs memo jobid + commited_by + committed_at flat_rate job { id @@ -119,6 +125,8 @@ export const QUERY_TIME_TICKETS_TECHNICIAN_IN_RANGE = gql` memo jobid flat_rate + commited_by + committed_at job { id ro_number @@ -161,6 +169,8 @@ export const QUERY_TIME_TICKETS_IN_RANGE_SB = gql` committed_at commited_by flat_rate + commited_by + committed_at job { id ro_number @@ -221,6 +231,8 @@ export const INSERT_NEW_TIME_TICKET = gql` date memo flat_rate + commited_by + committed_at } } } @@ -244,6 +256,8 @@ export const INSERT_TIME_TICKET_AND_APPROVE = gql` date memo flat_rate + commited_by + committed_at } } update_tt_approval_queue( @@ -254,6 +268,7 @@ export const INSERT_TIME_TICKET_AND_APPROVE = gql` id approved_at approved_at + } } } @@ -282,6 +297,38 @@ export const UPDATE_TIME_TICKET = gql` date flat_rate memo + committed_at + commited_by + } + } + } +`; + +export const UPDATE_TIME_TICKETS = gql` + mutation UPDATE_TIME_TICKETS( + $timeticketIds: [uuid!]! + $timeticket: timetickets_set_input! + ) { + update_timetickets( + where: { id: { _in: $timeticketIds } } + _set: $timeticket + ) { + returning { + id + clockon + clockoff + employeeid + productivehrs + actualhrs + ciecacode + created_at + updated_at + jobid + date + flat_rate + memo + committed_at + commited_by } } } @@ -306,6 +353,8 @@ export const QUERY_ACTIVE_TIME_TICKETS = gql` cost_center flat_rate jobid + commited_by + committed_at job { id ownr_fn diff --git a/client/src/pages/time-tickets/time-tickets.container.jsx b/client/src/pages/time-tickets/time-tickets.container.jsx index 03cfdda50..0730fca95 100644 --- a/client/src/pages/time-tickets/time-tickets.container.jsx +++ b/client/src/pages/time-tickets/time-tickets.container.jsx @@ -19,6 +19,7 @@ import { setBreadcrumbs, setSelectedHeader, } from "../../redux/application/application.actions"; +import TimeTicketsCommit from "../../components/time-tickets-commit/time-tickets-commit.component"; const mapStateToProps = createStructuredSelector({}); @@ -74,6 +75,7 @@ export function TimeTicketsContainer({ + } diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index 716e0f4f3..0715e728e 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -2654,8 +2654,11 @@ "claimtasks": "Claim Tasks", "clockin": "Clock In", "clockout": "Clock Out", + "commit": "Commit Tickets ({{count}})", + "commitone": "Commit", "enter": "Enter New Time Ticket", - "printemployee": "Print Time Tickets" + "printemployee": "Print Time Tickets", + "uncommit": "Uncommit" }, "errors": { "clockingin": "Error while clocking in. {{message}}", @@ -2707,6 +2710,7 @@ "successes": { "clockedin": "Clocked in successfully.", "clockedout": "Clocked out successfully.", + "committed": "Time Tickets Committed Successfully", "created": "Time ticket entered successfully.", "deleted": "Time ticket deleted successfully." }, diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index 6dbf16601..1a1d7eb30 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -2654,8 +2654,11 @@ "claimtasks": "", "clockin": "", "clockout": "", + "commit": "", + "commitone": "", "enter": "", - "printemployee": "" + "printemployee": "", + "uncommit": "" }, "errors": { "clockingin": "", @@ -2707,6 +2710,7 @@ "successes": { "clockedin": "", "clockedout": "", + "committed": "", "created": "", "deleted": "" }, diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index 66d5df8c9..ee4cf4e7c 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -2654,8 +2654,11 @@ "claimtasks": "", "clockin": "", "clockout": "", + "commit": "", + "commitone": "", "enter": "", - "printemployee": "" + "printemployee": "", + "uncommit": "" }, "errors": { "clockingin": "", @@ -2707,6 +2710,7 @@ "successes": { "clockedin": "", "clockedout": "", + "committed": "", "created": "", "deleted": "" },