Merge branch 'feature/payroll' into feature/america

This commit is contained in:
Patrick Fic
2023-06-02 09:32:00 -07:00
19 changed files with 751 additions and 229 deletions

View File

@@ -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) => {
</Select>
);
};
export default forwardRef(EmployeeSearchSelect);
export default EmployeeSearchSelect;

View File

@@ -262,11 +262,13 @@ function Header({
{t("menus.header.timetickets")}
</Link>
</Menu.Item>
<Menu.Item key="ttapprovals" icon={<FieldTimeOutlined />}>
<Link to="/manage/ttapprovals">
{t("menus.header.ttapprovals")}
</Link>
</Menu.Item>
{bodyshop?.md_tasks_presets?.use_approvals && (
<Menu.Item key="ttapprovals" icon={<FieldTimeOutlined />}>
<Link to="/manage/ttapprovals">
{t("menus.header.ttapprovals")}
</Link>
</Menu.Item>
)}
<Menu.Item
key="entertimetickets"
icon={<Icon component={GiPlayerTime} />}

View File

@@ -250,21 +250,24 @@ export function JobsDetailHeaderActions({
>
{t("timetickets.actions.enter")}
</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>
{bodyshop.md_tasks_presets.enable_tasks && (
<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
key="enterpayments"
disabled={!job.converted}

View File

@@ -8,6 +8,7 @@ import {
InputNumber,
Row,
Space,
Switch,
} from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
@@ -18,127 +19,146 @@ export default function ShopInfoTaskPresets({ form }) {
const { t } = useTranslation();
return (
<LayoutFormRow header={t("bodyshop.labels.md_tasks_presets")}>
<Form.List name={["md_tasks_presets", "presets"]}>
{(fields, { add, remove, move }) => {
return (
<div>
{fields.map((field, index) => (
<Form.Item key={field.key}>
<LayoutFormRow noDivider>
<Form.Item
label={t("bodyshop.fields.md_tasks_presets.name")}
key={`${index}name`}
name={[field.name, "name"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.md_tasks_presets.hourstype")}
key={`${index}hourstype`}
name={[field.name, "hourstype"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Checkbox.Group>
<Row>
<Col span={8}>
<Checkbox
value="LAB"
style={{ lineHeight: "32px" }}
>
{t("joblines.fields.lbr_types.LAB")}
</Checkbox>
</Col>
<Col span={8}>
<Checkbox
value="LAR"
style={{ lineHeight: "32px" }}
>
{t("joblines.fields.lbr_types.LAR")}
</Checkbox>
</Col>
<Col span={8}>
<Checkbox
value="LAM"
style={{ lineHeight: "32px" }}
>
{t("joblines.fields.lbr_types.LAM")}
</Checkbox>
</Col>
<Col span={8}>
<Checkbox
value="LAF"
style={{ lineHeight: "32px" }}
>
{t("joblines.fields.lbr_types.LAF")}
</Checkbox>
</Col>
<Col span={8}>
<Checkbox
value="LAG"
style={{ lineHeight: "32px" }}
>
{t("joblines.fields.lbr_types.LAG")}
</Checkbox>
</Col>
</Row>
</Checkbox.Group>
</Form.Item>
<Form.Item
label={t("bodyshop.fields.md_tasks_presets.percent")}
key={`${index}percent`}
name={[field.name, "percent"]}
>
<InputNumber min={0} max={100} />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.md_tasks_presets.memo")}
key={`${index}memo`}
name={[field.name, "memo"]}
>
<Input />
</Form.Item>
<Space wrap>
<DeleteFilled
onClick={() => {
remove(field.name);
}}
/>
<FormListMoveArrows
move={move}
index={index}
total={fields.length}
/>
</Space>
</LayoutFormRow>
<>
<LayoutFormRow noDivider>
<Form.Item
label={t("bodyshop.fields.md_tasks_presets.enable_tasks")}
valuePropName="checked"
name={["md_tasks_presets", "enable_tasks"]}
>
<Switch />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.md_tasks_presets.use_approvals")}
valuePropName="checked"
name={["md_tasks_presets", "use_approvals"]}
>
<Switch />
</Form.Item>
</LayoutFormRow>
<LayoutFormRow header={t("bodyshop.labels.md_tasks_presets")}>
<Form.List name={["md_tasks_presets", "presets"]}>
{(fields, { add, remove, move }) => {
return (
<div>
{fields.map((field, index) => (
<Form.Item key={field.key}>
<LayoutFormRow noDivider>
<Form.Item
label={t("bodyshop.fields.md_tasks_presets.name")}
key={`${index}name`}
name={[field.name, "name"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.md_tasks_presets.hourstype")}
key={`${index}hourstype`}
name={[field.name, "hourstype"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Checkbox.Group>
<Row>
<Col span={8}>
<Checkbox
value="LAB"
style={{ lineHeight: "32px" }}
>
{t("joblines.fields.lbr_types.LAB")}
</Checkbox>
</Col>
<Col span={8}>
<Checkbox
value="LAR"
style={{ lineHeight: "32px" }}
>
{t("joblines.fields.lbr_types.LAR")}
</Checkbox>
</Col>
<Col span={8}>
<Checkbox
value="LAM"
style={{ lineHeight: "32px" }}
>
{t("joblines.fields.lbr_types.LAM")}
</Checkbox>
</Col>
<Col span={8}>
<Checkbox
value="LAF"
style={{ lineHeight: "32px" }}
>
{t("joblines.fields.lbr_types.LAF")}
</Checkbox>
</Col>
<Col span={8}>
<Checkbox
value="LAG"
style={{ lineHeight: "32px" }}
>
{t("joblines.fields.lbr_types.LAG")}
</Checkbox>
</Col>
</Row>
</Checkbox.Group>
</Form.Item>
<Form.Item
label={t("bodyshop.fields.md_tasks_presets.percent")}
key={`${index}percent`}
name={[field.name, "percent"]}
>
<InputNumber min={0} max={100} />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.md_tasks_presets.memo")}
key={`${index}memo`}
name={[field.name, "memo"]}
>
<Input />
</Form.Item>
<Space wrap>
<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("bodyshop.actions.add_task_preset")}
</Button>
</Form.Item>
))}
<Form.Item>
<Button
type="dashed"
onClick={() => {
add();
}}
style={{ width: "100%" }}
>
{t("bodyshop.actions.add_task_preset")}
</Button>
</Form.Item>
</div>
);
}}
</Form.List>
</LayoutFormRow>
</div>
);
}}
</Form.List>
</LayoutFormRow>
</>
);
}

View File

@@ -205,16 +205,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: "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"),
dataIndex: "actions",
@@ -282,16 +281,18 @@ export function TimeTicketList({
// context={{ jobId: jobId }}
// />
}
<Button
onClick={() => {
setTimeTicketTaskContext({
actions: {},
context: { jobid: jobId },
});
}}
>
{t("timetickets.actions.claimtasks")}
</Button>
{bodyshop.md_tasks_presets.enable_tasks && (
<Button
onClick={() => {
setTimeTicketTaskContext({
actions: {},
context: { jobid: jobId },
});
}}
>
{t("timetickets.actions.claimtasks")}
</Button>
)}
{jobId &&
(techConsole ? null : (
<TimeTicketEnterButton

View File

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

View File

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

View File

@@ -23,6 +23,7 @@ import JobSearchSelectComponent from "../job-search-select/job-search-select.com
import { CalculateAllocationsTotals } from "../labor-allocations-table/labor-allocations-table.utility";
import { LaborAllocationContainer } from "../time-ticket-modal/time-ticket-modal.component";
import TimeTicketsTasksPresets from "../time-ticket-tasks-presets/time-ticket-tasks-presets.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
@@ -54,7 +55,8 @@ export function TimeTicketTaskModalComponent({
calculateTimeTickets={calculateTimeTickets}
/>
<Row gutter={[16, 16]}>
<Col lg={12} md={24}>
<Col xl={12} lg={24}>
<Form.Item
name="jobid"
label={t("timetickets.fields.ro_number")}
@@ -77,37 +79,37 @@ export function TimeTicketTaskModalComponent({
>
<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="hourstype"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Checkbox.Group>
<Space wrap>
<Checkbox value="LAB" style={{ display: "flex" }}>
{t("jobs.fields.lab")}
</Checkbox>
<Checkbox value="LAR" style={{ display: "flex" }}>
{t("jobs.fields.lar")}
</Checkbox>
<Checkbox value="LAM" style={{ display: "flex" }}>
{t("jobs.fields.lam")}
</Checkbox>
<Checkbox value="LAF" style={{ display: "flex" }}>
{t("jobs.fields.laf")}
</Checkbox>
<Checkbox value="LAG" style={{ display: "flex" }}>
{t("jobs.fields.lag")}
</Checkbox>
</Space>
</Checkbox.Group>
</Form.Item>
<Space wrap align="start">
<Form.Item
name="percent"
rules={[
@@ -119,10 +121,13 @@ export function TimeTicketTaskModalComponent({
>
<InputNumber min={0} max={100} precision={1} addonAfter="%" />
</Form.Item>
<Button onClick={calculateTimeTickets}>
{t("tt_approvals.labels.calculate")}
</Button>
</Space>
<Button onClick={calculateTimeTickets}>Calculate</Button>
</Col>
<Col lg={12} md={24}>
<Col xl={12} lg={24}>
<Form.Item shouldUpdate>
{() => {
const data = form.getFieldValue("timetickets");
@@ -167,11 +172,11 @@ export function TimeTicketTaskModalComponent({
dataIndex: "rate",
key: "rate",
},
{
title: "Pay",
dataIndex: "pay",
key: "pay",
},
// {
// title: "Pay",
// dataIndex: "pay",
// key: "pay",
// },
]}
/>
);
@@ -231,11 +236,9 @@ export function TimeTicketTaskModalComponent({
<Alert key={idx} message={e} />
))}
<div
style={
{
//display: "none"
}
}
style={{
display: "none",
}}
>
{fields.map((field, index) => (
<Form.Item
@@ -364,6 +367,11 @@ export function TimeTicketTaskModalComponent({
);
}}
</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>
);
}

View File

@@ -52,12 +52,8 @@ export function TimeTickeTaskModalContainer({
});
async function handleFinish(values) {
console.log(
"🚀 ~ file: time-ticket-task-modal.container.jsx:55 ~ handleFinish ~ values:",
values
);
try {
if (true) {
if (bodyshop.md_tasks_presets.use_approvals) {
const result = await insertTimeTicketApproval({
variables: {
timeTicketInput: values.timetickets.map((ticket) => ({
@@ -88,6 +84,7 @@ export function TimeTickeTaskModalContainer({
_.omit(ticket, "pay")
),
},
refetchQueries: ["GET_LINE_TICKET_BY_PK"]
});
if (result.errors) {
notification.open({
@@ -105,6 +102,13 @@ export function TimeTickeTaskModalContainer({
}
}
} catch (error) {
console.log("🚀 ~ file: time-ticket-task-modal.container.jsx:104 ~ handleFinish ~ error:", error)
notification.open({
type: "error",
message: t("timetickets.errors.creating", {
message: JSON.stringify(error),
}),
});
} finally {
}
}

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

@@ -9,13 +9,16 @@ import { createStructuredSelector } from "reselect";
import { INSERT_TIME_TICKET_AND_APPROVE } from "../../graphql/timetickets.queries";
import { QUERY_TT_APPROVALS_BY_IDS } from "../../graphql/tt-approvals.queries";
import {
selectAuthLevel,
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
currentUser: selectCurrentUser,
authLevel: selectAuthLevel,
});
export function TtApproveButton({
@@ -23,6 +26,7 @@ export function TtApproveButton({
currentUser,
selectedTickets,
disabled,
authLevel,
loadingCallback,
completedCallback,
refetch,
@@ -64,7 +68,7 @@ export function TtApproveButton({
} else {
notification.open({
type: "success",
message: t("timetickets.successes.createdg"),
message: t("timetickets.successes.created"),
});
}
} catch (error) {
@@ -83,7 +87,14 @@ export function TtApproveButton({
};
return (
<Button onClick={handleQbxml} loading={loading} disabled={disabled}>
<Button
onClick={handleQbxml}
loading={loading}
disabled={
disabled ||
!HasRbacAccess({ bodyshop, authLevel, action: "ttapprovals:approve" })
}
>
{t("tt_approvals.actions.approveselected")}
</Button>
);