Add commit and uncommit functionality.

This commit is contained in:
Patrick Fic
2023-05-25 15:23:14 -07:00
parent a56de72a6b
commit 3d03de193e
12 changed files with 381 additions and 17 deletions

View File

@@ -44838,6 +44838,48 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>commit</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>commitone</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>enter</name> <name>enter</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -44880,6 +44922,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>uncommit</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>
<folder_node> <folder_node>
@@ -45803,6 +45866,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>committed</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>created</name> <name>created</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>

View File

@@ -1,10 +1,10 @@
import { Select, Space, Tag } from "antd"; import { Select, Space, Tag } from "antd";
import React, { forwardRef } from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
const { Option } = Select; const { Option } = Select;
//To be used as a form element only. //To be used as a form element only.
const EmployeeSearchSelect = ({ options, ...props }, ref) => { const EmployeeSearchSelect = ({ options, ...props }) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
@@ -39,4 +39,4 @@ const EmployeeSearchSelect = ({ options, ...props }, ref) => {
</Select> </Select>
); );
}; };
export default forwardRef(EmployeeSearchSelect); export default EmployeeSearchSelect;

View File

@@ -37,6 +37,7 @@ export function TimeTicketModalComponent({
authLevel, authLevel,
employeeAutoCompleteOptions, employeeAutoCompleteOptions,
isEdit, isEdit,
disabled,
employeeSelectDisabled, employeeSelectDisabled,
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -50,7 +51,7 @@ export function TimeTicketModalComponent({
<Select <Select
value={value === "timetickets.labels.shift" ? t(value) : value} value={value === "timetickets.labels.shift" ? t(value) : value}
{...props} {...props}
disabled={value === "timetickets.labels.shift"} disabled={value === "timetickets.labels.shift" || disabled}
> >
{emps && {emps &&
emps.rates.map((item) => ( emps.rates.map((item) => (
@@ -73,7 +74,7 @@ export function TimeTicketModalComponent({
<Input <Input
value={value?.startsWith("timetickets.") ? t(value) : value} value={value?.startsWith("timetickets.") ? t(value) : value}
{...props} {...props}
disabled={value?.startsWith("timetickets.")} disabled={value?.startsWith("timetickets.") || disabled}
/> />
); );
}; };
@@ -126,7 +127,7 @@ export function TimeTicketModalComponent({
]} ]}
> >
<EmployeeSearchSelect <EmployeeSearchSelect
disabled={employeeSelectDisabled} disabled={employeeSelectDisabled || disabled}
options={employeeAutoCompleteOptions} options={employeeAutoCompleteOptions}
onSelect={(value) => { onSelect={(value) => {
const emps = const emps =
@@ -278,6 +279,7 @@ export function TimeTicketModalComponent({
<FormDateTimePicker <FormDateTimePicker
minuteStep={5} minuteStep={5}
disabled={ disabled={
disabled ||
!HasRbacAccess({ !HasRbacAccess({
bodyshop, bodyshop,
authLevel, authLevel,
@@ -316,6 +318,7 @@ export function TimeTicketModalComponent({
<FormDateTimePicker <FormDateTimePicker
minuteStep={5} minuteStep={5}
disabled={ disabled={
disabled ||
!HasRbacAccess({ !HasRbacAccess({
bodyshop, bodyshop,
authLevel, authLevel,

View File

@@ -14,6 +14,7 @@ import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectTimeTicket } from "../../redux/modals/modals.selectors"; import { selectTimeTicket } from "../../redux/modals/modals.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import TimeTicketModalComponent from "./time-ticket-modal.component"; import TimeTicketModalComponent from "./time-ticket-modal.component";
import TimeTicketsCommitToggleComponent from "../time-tickets-commit-toggle/time-tickets-commit-toggle.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
timeTicketModal: selectTimeTicket, timeTicketModal: selectTimeTicket,
@@ -174,7 +175,11 @@ export function TimeTicketModalContainer({
footer={ footer={
<span> <span>
<Button onClick={handleCancel}>{t("general.actions.cancel")}</Button> <Button onClick={handleCancel}>{t("general.actions.cancel")}</Button>
<Button loading={loading} onClick={() => form.submit()}> <Button
loading={loading}
disabled={timeTicketModal.context?.timeticket?.committed_at}
onClick={() => form.submit()}
>
{t("general.actions.save")} {t("general.actions.save")}
</Button> </Button>
{timeTicketModal.context && timeTicketModal.context.id ? null : ( {timeTicketModal.context && timeTicketModal.context.id ? null : (
@@ -198,6 +203,7 @@ export function TimeTicketModalContainer({
autoComplete={"off"} autoComplete={"off"}
form={form} form={form}
onFinishFailed={() => setEnterAgain(false)} onFinishFailed={() => setEnterAgain(false)}
disabled={timeTicketModal.context?.timeticket?.committed_at}
initialValues={ initialValues={
timeTicketModal.context.timeticket timeTicketModal.context.timeticket
? { ? {
@@ -218,6 +224,9 @@ export function TimeTicketModalContainer({
<PageHeader <PageHeader
extra={ extra={
<Space> <Space>
<TimeTicketsCommitToggleComponent
timeticket={timeTicketModal.context?.timeticket}
/>
<Button onClick={handleCancel}> <Button onClick={handleCancel}>
{t("general.actions.cancel")} {t("general.actions.cancel")}
</Button> </Button>
@@ -241,14 +250,16 @@ export function TimeTicketModalContainer({
<TimeTicketModalComponent <TimeTicketModalComponent
isEdit={timeTicketModal.context.id} isEdit={timeTicketModal.context.id}
form={form} form={form}
disabled={timeTicketModal.context?.timeticket?.committed_at}
employeeAutoCompleteOptions={ employeeAutoCompleteOptions={
EmployeeAutoCompleteData && EmployeeAutoCompleteData.employees EmployeeAutoCompleteData && EmployeeAutoCompleteData.employees
} }
employeeSelectDisabled={ employeeSelectDisabled={
timeTicketModal.context?.timeticket?.employeeid && timeTicketModal.context?.timeticket?.committed_at ||
(timeTicketModal.context?.timeticket?.employeeid &&
!timeTicketModal.context.id !timeTicketModal.context.id
? true ? true
: false : false)
} }
/> />
</Form> </Form>

View File

@@ -55,11 +55,7 @@ export function TimeTicketTaskModalComponent({
calculateTimeTickets={calculateTimeTickets} calculateTimeTickets={calculateTimeTickets}
/> />
<Row gutter={[16, 16]}> <Row gutter={[16, 16]}>
{bodyshop?.md_tasks_presets?.use_approvals && (
<Col span={24}>
<Alert message={t(".tt_approvals.labels.approval_queue_in_use")} />
</Col>
)}
<Col xl={12} lg={24}> <Col xl={12} lg={24}>
<Form.Item <Form.Item
name="jobid" name="jobid"
@@ -371,6 +367,11 @@ export function TimeTicketTaskModalComponent({
); );
}} }}
</Form.Item> </Form.Item>
{bodyshop?.md_tasks_presets?.use_approvals && (
<Col span={24}>
<Alert message={t("tt_approvals.labels.approval_queue_in_use")} type="warning" />
</Col>
)}
</div> </div>
); );
} }

View File

@@ -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 (
<Button onClick={handleCommit} loading={loading} disabled={!timeticket?.id}>
{timeticket?.committed_at
? t("timetickets.actions.uncommit")
: t("timetickets.actions.commitone")}
</Button>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(TimeTicketsCommit);

View File

@@ -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 (
<Button
onClick={handleCommit}
loading={loading}
disabled={disabled || timetickets?.length === 0}
>
{t("timetickets.actions.commit", { count: timetickets?.length })}
</Button>
);
}
export default connect(mapStateToProps, null)(TimeTicketsCommit);

View File

@@ -15,6 +15,8 @@ export const QUERY_TICKETS_BY_JOBID = gql`
memo memo
jobid jobid
flat_rate flat_rate
commited_by
committed_at
employee { employee {
employee_number employee_number
first_name first_name
@@ -44,6 +46,8 @@ export const QUERY_TIME_TICKETS_IN_RANGE = gql`
memo memo
jobid jobid
flat_rate flat_rate
commited_by
committed_at
job { job {
id id
ro_number ro_number
@@ -86,6 +90,8 @@ export const QUERY_TIME_TICKETS_TECHNICIAN_IN_RANGE = gql`
productivehrs productivehrs
memo memo
jobid jobid
commited_by
committed_at
flat_rate flat_rate
job { job {
id id
@@ -119,6 +125,8 @@ export const QUERY_TIME_TICKETS_TECHNICIAN_IN_RANGE = gql`
memo memo
jobid jobid
flat_rate flat_rate
commited_by
committed_at
job { job {
id id
ro_number ro_number
@@ -161,6 +169,8 @@ export const QUERY_TIME_TICKETS_IN_RANGE_SB = gql`
committed_at committed_at
commited_by commited_by
flat_rate flat_rate
commited_by
committed_at
job { job {
id id
ro_number ro_number
@@ -221,6 +231,8 @@ export const INSERT_NEW_TIME_TICKET = gql`
date date
memo memo
flat_rate flat_rate
commited_by
committed_at
} }
} }
} }
@@ -244,6 +256,8 @@ export const INSERT_TIME_TICKET_AND_APPROVE = gql`
date date
memo memo
flat_rate flat_rate
commited_by
committed_at
} }
} }
update_tt_approval_queue( update_tt_approval_queue(
@@ -254,6 +268,7 @@ export const INSERT_TIME_TICKET_AND_APPROVE = gql`
id id
approved_at approved_at
approved_at approved_at
} }
} }
} }
@@ -282,6 +297,38 @@ export const UPDATE_TIME_TICKET = gql`
date date
flat_rate flat_rate
memo 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 cost_center
flat_rate flat_rate
jobid jobid
commited_by
committed_at
job { job {
id id
ownr_fn ownr_fn

View File

@@ -19,6 +19,7 @@ import {
setBreadcrumbs, setBreadcrumbs,
setSelectedHeader, setSelectedHeader,
} from "../../redux/application/application.actions"; } from "../../redux/application/application.actions";
import TimeTicketsCommit from "../../components/time-tickets-commit/time-tickets-commit.component";
const mapStateToProps = createStructuredSelector({}); const mapStateToProps = createStructuredSelector({});
@@ -74,6 +75,7 @@ export function TimeTicketsContainer({
<Space wrap> <Space wrap>
<TimeTicketsAttendanceTable /> <TimeTicketsAttendanceTable />
<TimeTicketsPayrollTable /> <TimeTicketsPayrollTable />
<TimeTicketsCommit timetickets={data ? data.timetickets : []} />
<TimeTicketsDatesSelector /> <TimeTicketsDatesSelector />
</Space> </Space>
} }

View File

@@ -2654,8 +2654,11 @@
"claimtasks": "Claim Tasks", "claimtasks": "Claim Tasks",
"clockin": "Clock In", "clockin": "Clock In",
"clockout": "Clock Out", "clockout": "Clock Out",
"commit": "Commit Tickets ({{count}})",
"commitone": "Commit",
"enter": "Enter New Time Ticket", "enter": "Enter New Time Ticket",
"printemployee": "Print Time Tickets" "printemployee": "Print Time Tickets",
"uncommit": "Uncommit"
}, },
"errors": { "errors": {
"clockingin": "Error while clocking in. {{message}}", "clockingin": "Error while clocking in. {{message}}",
@@ -2707,6 +2710,7 @@
"successes": { "successes": {
"clockedin": "Clocked in successfully.", "clockedin": "Clocked in successfully.",
"clockedout": "Clocked out successfully.", "clockedout": "Clocked out successfully.",
"committed": "Time Tickets Committed Successfully",
"created": "Time ticket entered successfully.", "created": "Time ticket entered successfully.",
"deleted": "Time ticket deleted successfully." "deleted": "Time ticket deleted successfully."
}, },

View File

@@ -2654,8 +2654,11 @@
"claimtasks": "", "claimtasks": "",
"clockin": "", "clockin": "",
"clockout": "", "clockout": "",
"commit": "",
"commitone": "",
"enter": "", "enter": "",
"printemployee": "" "printemployee": "",
"uncommit": ""
}, },
"errors": { "errors": {
"clockingin": "", "clockingin": "",
@@ -2707,6 +2710,7 @@
"successes": { "successes": {
"clockedin": "", "clockedin": "",
"clockedout": "", "clockedout": "",
"committed": "",
"created": "", "created": "",
"deleted": "" "deleted": ""
}, },

View File

@@ -2654,8 +2654,11 @@
"claimtasks": "", "claimtasks": "",
"clockin": "", "clockin": "",
"clockout": "", "clockout": "",
"commit": "",
"commitone": "",
"enter": "", "enter": "",
"printemployee": "" "printemployee": "",
"uncommit": ""
}, },
"errors": { "errors": {
"clockingin": "", "clockingin": "",
@@ -2707,6 +2710,7 @@
"successes": { "successes": {
"clockedin": "", "clockedin": "",
"clockedout": "", "clockedout": "",
"committed": "",
"created": "", "created": "",
"deleted": "" "deleted": ""
}, },