feature/IO-3587-Commision-Cut - Additional test, layout enhancements
This commit is contained in:
@@ -10,6 +10,11 @@ const mapDispatchToProps = () => ({
|
|||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const toFiniteNumber = (value) => {
|
||||||
|
const parsed = Number(value);
|
||||||
|
return Number.isFinite(parsed) ? parsed : null;
|
||||||
|
};
|
||||||
|
|
||||||
const ReadOnlyFormItem = ({ bodyshop, value, type = "text" }) => {
|
const ReadOnlyFormItem = ({ bodyshop, value, type = "text" }) => {
|
||||||
if (value === null || value === undefined || value === "") return null;
|
if (value === null || value === undefined || value === "") return null;
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@@ -20,8 +25,15 @@ const ReadOnlyFormItem = ({ bodyshop, value, type = "text" }) => {
|
|||||||
|
|
||||||
case "text":
|
case "text":
|
||||||
return <div style={{ wordWrap: "break-word", overflowWrap: "break-word" }}>{value}</div>;
|
return <div style={{ wordWrap: "break-word", overflowWrap: "break-word" }}>{value}</div>;
|
||||||
case "currency":
|
case "currency": {
|
||||||
return <div>{Dinero({ amount: Math.round(value * 100) }).toFormat()}</div>;
|
const numericValue = toFiniteNumber(value);
|
||||||
|
|
||||||
|
if (numericValue === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div>{Dinero({ amount: Math.round(numericValue * 100) }).toFormat()}</div>;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return <div style={{ wordWrap: "break-word", overflowWrap: "break-word" }}>{value}</div>;
|
return <div style={{ wordWrap: "break-word", overflowWrap: "break-word" }}>{value}</div>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { DeleteFilled } from "@ant-design/icons";
|
import { DeleteFilled } from "@ant-design/icons";
|
||||||
import { useMutation, useQuery } from "@apollo/client/react";
|
import { useMutation, useQuery } from "@apollo/client/react";
|
||||||
import { Button, Card, Form, Input, InputNumber, Select, Space, Switch, Typography } from "antd";
|
import { Button, Card, Col, Form, Input, InputNumber, Row, Select, Space, Switch, Tag, Typography } from "antd";
|
||||||
|
|
||||||
import querystring from "query-string";
|
import querystring from "query-string";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
@@ -35,6 +35,14 @@ const PAYOUT_METHOD_OPTIONS = [
|
|||||||
{ labelKey: "employee_teams.options.commission_percentage", value: "commission" }
|
{ labelKey: "employee_teams.options.commission_percentage", value: "commission" }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const TEAM_MEMBER_PRIMARY_FIELD_COLS = {
|
||||||
|
employee: { xs: 24, lg: 13, xxl: 14 },
|
||||||
|
allocation: { xs: 24, sm: 12, lg: 4, xxl: 4 },
|
||||||
|
payoutMethod: { xs: 24, sm: 12, lg: 7, xxl: 6 }
|
||||||
|
};
|
||||||
|
|
||||||
|
const TEAM_MEMBER_RATE_FIELD_COLS = { xs: 24, sm: 12, md: 8, lg: 6, xxl: 4 };
|
||||||
|
|
||||||
const normalizeTeamMember = (teamMember = {}) => ({
|
const normalizeTeamMember = (teamMember = {}) => ({
|
||||||
...teamMember,
|
...teamMember,
|
||||||
payout_method: teamMember.payout_method || "hourly",
|
payout_method: teamMember.payout_method || "hourly",
|
||||||
@@ -52,6 +60,8 @@ const getSplitTotal = (teamMembers = []) =>
|
|||||||
|
|
||||||
const hasExactSplitTotal = (teamMembers = []) => Math.abs(getSplitTotal(teamMembers) - 100) < 0.00001;
|
const hasExactSplitTotal = (teamMembers = []) => Math.abs(getSplitTotal(teamMembers) - 100) < 0.00001;
|
||||||
|
|
||||||
|
const getPayoutMethodTagColor = (payoutMethod) => (payoutMethod === "commission" ? "gold" : "blue");
|
||||||
|
|
||||||
const getEmployeeDisplayName = (employees = [], employeeId) => {
|
const getEmployeeDisplayName = (employees = [], employeeId) => {
|
||||||
const employee = employees.find((currentEmployee) => currentEmployee.id === employeeId);
|
const employee = employees.find((currentEmployee) => currentEmployee.id === employeeId);
|
||||||
if (!employee) return null;
|
if (!employee) return null;
|
||||||
@@ -102,12 +112,25 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
|||||||
const teamCardTitle = teamName?.trim() || t("employee_teams.fields.name");
|
const teamCardTitle = teamName?.trim() || t("employee_teams.fields.name");
|
||||||
|
|
||||||
const getTeamMemberTitle = (teamMember = {}) => {
|
const getTeamMemberTitle = (teamMember = {}) => {
|
||||||
const employeeName = getEmployeeDisplayName(bodyshop.employees, teamMember.employeeid) || t("employee_teams.fields.employeeid");
|
const employeeName =
|
||||||
const allocation = formatAllocationPercentage(teamMember.percentage) || t("employee_teams.fields.allocation_percentage");
|
getEmployeeDisplayName(bodyshop.employees, teamMember.employeeid) || t("employee_teams.fields.employeeid");
|
||||||
|
const allocation = formatAllocationPercentage(teamMember.percentage);
|
||||||
const payoutMethod =
|
const payoutMethod =
|
||||||
payoutMethodOptions.find((option) => option.value === teamMember.payout_method)?.label || t("employee_teams.fields.payout_method");
|
teamMember.payout_method === "commission"
|
||||||
|
? t("employee_teams.options.commission")
|
||||||
|
: t("employee_teams.options.hourly");
|
||||||
|
|
||||||
return `${employeeName} | ${allocation} | ${payoutMethod}`;
|
return (
|
||||||
|
<div style={{ display: "flex", flexWrap: "wrap", alignItems: "center", gap: 8 }}>
|
||||||
|
<Typography.Text strong>{employeeName}</Typography.Text>
|
||||||
|
<Tag bordered={false} color="geekblue">
|
||||||
|
{`${t("employee_teams.fields.allocation")}: ${allocation || "--"}`}
|
||||||
|
</Tag>
|
||||||
|
<Tag bordered={false} color={getPayoutMethodTagColor(teamMember.payout_method)}>
|
||||||
|
{payoutMethod}
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFinish = async ({ employee_team_members = [], ...values }) => {
|
const handleFinish = async ({ employee_team_members = [], ...values }) => {
|
||||||
@@ -158,7 +181,9 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
|||||||
.filter((teamMember) => teamMember.id === null || teamMember.id === undefined)
|
.filter((teamMember) => teamMember.id === null || teamMember.id === undefined)
|
||||||
.map((teamMember) => ({ ...teamMember, teamid: search.employeeTeamId })),
|
.map((teamMember) => ({ ...teamMember, teamid: search.employeeTeamId })),
|
||||||
teamMemberDeletes: data.employee_teams_by_pk.employee_team_members
|
teamMemberDeletes: data.employee_teams_by_pk.employee_team_members
|
||||||
.filter((teamMember) => !normalizedTeamMembers.find((currentTeamMember) => currentTeamMember.id === teamMember.id))
|
.filter(
|
||||||
|
(teamMember) => !normalizedTeamMembers.find((currentTeamMember) => currentTeamMember.id === teamMember.id)
|
||||||
|
)
|
||||||
.map((teamMember) => teamMember.id)
|
.map((teamMember) => teamMember.id)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -260,73 +285,95 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
|||||||
remove(field.name);
|
remove(field.name);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<FormListMoveArrows move={move} index={index} total={fields.length} orientation="horizontal" />
|
<FormListMoveArrows
|
||||||
|
move={move}
|
||||||
|
index={index}
|
||||||
|
total={fields.length}
|
||||||
|
orientation="horizontal"
|
||||||
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Form.Item
|
<div>
|
||||||
label={t("employee_teams.fields.employeeid")}
|
<Row gutter={[16, 0]}>
|
||||||
key={`${index}`}
|
<Col {...TEAM_MEMBER_PRIMARY_FIELD_COLS.employee}>
|
||||||
name={[field.name, "employeeid"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<EmployeeSearchSelectComponent options={bodyshop.employees} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("employee_teams.fields.allocation_percentage")}
|
|
||||||
key={`${index}`}
|
|
||||||
name={[field.name, "percentage"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<InputNumber min={0} max={100} precision={2} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("employee_teams.fields.payout_method")}
|
|
||||||
key={`${index}-payout-method`}
|
|
||||||
name={[field.name, "payout_method"]}
|
|
||||||
initialValue="hourly"
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Select options={payoutMethodOptions} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item noStyle dependencies={[["employee_team_members", field.name, "payout_method"]]}>
|
|
||||||
{() => {
|
|
||||||
const payoutMethod =
|
|
||||||
form.getFieldValue(["employee_team_members", field.name, "payout_method"]) || "hourly";
|
|
||||||
const fieldName = payoutMethod === "commission" ? "commission_rates" : "labor_rates";
|
|
||||||
|
|
||||||
return LABOR_TYPES.map((laborType) => (
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={payoutMethod === "commission" ? `${t(`joblines.fields.lbr_types.${laborType}`)} %` : t(`joblines.fields.lbr_types.${laborType}`)}
|
label={t("employee_teams.fields.employeeid")}
|
||||||
key={`${index}-${fieldName}-${laborType}`}
|
key={`${index}`}
|
||||||
name={[field.name, fieldName, laborType]}
|
name={[field.name, "employeeid"]}
|
||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{payoutMethod === "commission" ? (
|
<EmployeeSearchSelectComponent options={bodyshop.employees} />
|
||||||
<InputNumber min={0} max={100} precision={2} />
|
|
||||||
) : (
|
|
||||||
<CurrencyInput />
|
|
||||||
)}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
));
|
</Col>
|
||||||
}}
|
<Col {...TEAM_MEMBER_PRIMARY_FIELD_COLS.allocation}>
|
||||||
</Form.Item>
|
<Form.Item
|
||||||
|
label={t("employee_teams.fields.allocation_percentage")}
|
||||||
|
key={`${index}`}
|
||||||
|
name={[field.name, "percentage"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<InputNumber min={0} max={100} precision={2} />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col {...TEAM_MEMBER_PRIMARY_FIELD_COLS.payoutMethod}>
|
||||||
|
<Form.Item
|
||||||
|
label={t("employee_teams.fields.payout_method")}
|
||||||
|
key={`${index}-payout-method`}
|
||||||
|
name={[field.name, "payout_method"]}
|
||||||
|
initialValue="hourly"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Select options={payoutMethodOptions} />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Form.Item noStyle dependencies={[["employee_team_members", field.name, "payout_method"]]}>
|
||||||
|
{() => {
|
||||||
|
const payoutMethod =
|
||||||
|
form.getFieldValue(["employee_team_members", field.name, "payout_method"]) || "hourly";
|
||||||
|
const fieldName = payoutMethod === "commission" ? "commission_rates" : "labor_rates";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Row gutter={[16, 0]}>
|
||||||
|
{LABOR_TYPES.map((laborType) => (
|
||||||
|
<Col {...TEAM_MEMBER_RATE_FIELD_COLS} key={`${index}-${fieldName}-${laborType}`}>
|
||||||
|
<Form.Item
|
||||||
|
label={
|
||||||
|
t(`joblines.fields.lbr_types.${laborType}`)
|
||||||
|
}
|
||||||
|
name={[field.name, fieldName, laborType]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{payoutMethod === "commission" ? (
|
||||||
|
<InputNumber min={0} max={100} precision={2} />
|
||||||
|
) : (
|
||||||
|
<CurrencyInput />
|
||||||
|
)}
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,6 +15,18 @@ const mapDispatchToProps = () => ({
|
|||||||
});
|
});
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(TimeTicketTaskModalComponent);
|
export default connect(mapStateToProps, mapDispatchToProps)(TimeTicketTaskModalComponent);
|
||||||
|
|
||||||
|
const getPayoutMethodLabel = (payoutMethod, t) => {
|
||||||
|
if (!payoutMethod) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payoutMethod === "hourly" || payoutMethod === "commission") {
|
||||||
|
return t(`timetickets.labels.payout_methods.${payoutMethod}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return payoutMethod;
|
||||||
|
};
|
||||||
|
|
||||||
export function TimeTicketTaskModalComponent({ bodyshop, form, loading, completedTasks, unassignedHours }) {
|
export function TimeTicketTaskModalComponent({ bodyshop, form, loading, completedTasks, unassignedHours }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -101,45 +113,51 @@ export function TimeTicketTaskModalComponent({ bodyshop, form, loading, complete
|
|||||||
<th>{t("timetickets.fields.cost_center")}</th>
|
<th>{t("timetickets.fields.cost_center")}</th>
|
||||||
<th>{t("timetickets.fields.ciecacode")}</th>
|
<th>{t("timetickets.fields.ciecacode")}</th>
|
||||||
<th>{t("timetickets.fields.productivehrs")}</th>
|
<th>{t("timetickets.fields.productivehrs")}</th>
|
||||||
|
<th>{t("timetickets.fields.payout_method")}</th>
|
||||||
<th>{t("timetickets.fields.rate")}</th>
|
<th>{t("timetickets.fields.rate")}</th>
|
||||||
<th>{t("timetickets.fields.amount")}</th>
|
<th>{t("timetickets.fields.amount")}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{fields.map((field, index) => (
|
{fields.map((field, index) => {
|
||||||
<tr key={field.key}>
|
const payoutMethod = form.getFieldValue(["timetickets", field.name, "payout_context", "payout_method"]);
|
||||||
<td>
|
|
||||||
<Form.Item key={`${index}employeeid`} name={[field.name, "employeeid"]}>
|
return (
|
||||||
<ReadOnlyFormItemComponent type="employee" />
|
<tr key={field.key}>
|
||||||
</Form.Item>
|
<td>
|
||||||
</td>
|
<Form.Item key={`${index}employeeid`} name={[field.name, "employeeid"]}>
|
||||||
<td>
|
<ReadOnlyFormItemComponent type="employee" />
|
||||||
<Form.Item key={`${index}cost_center`} name={[field.name, "cost_center"]}>
|
</Form.Item>
|
||||||
<ReadOnlyFormItemComponent />
|
</td>
|
||||||
</Form.Item>
|
<td>
|
||||||
</td>
|
<Form.Item key={`${index}cost_center`} name={[field.name, "cost_center"]}>
|
||||||
<td>
|
<ReadOnlyFormItemComponent />
|
||||||
<Form.Item key={`${index}ciecacode`} name={[field.name, "ciecacode"]}>
|
</Form.Item>
|
||||||
<ReadOnlyFormItemComponent />
|
</td>
|
||||||
</Form.Item>
|
<td>
|
||||||
</td>
|
<Form.Item key={`${index}ciecacode`} name={[field.name, "ciecacode"]}>
|
||||||
<td>
|
<ReadOnlyFormItemComponent />
|
||||||
<Form.Item key={`${index}productivehrs`} name={[field.name, "productivehrs"]}>
|
</Form.Item>
|
||||||
<ReadOnlyFormItemComponent />
|
</td>
|
||||||
</Form.Item>
|
<td>
|
||||||
</td>
|
<Form.Item key={`${index}productivehrs`} name={[field.name, "productivehrs"]}>
|
||||||
<td>
|
<ReadOnlyFormItemComponent />
|
||||||
<Form.Item key={`${index}rate`} name={[field.name, "rate"]}>
|
</Form.Item>
|
||||||
<ReadOnlyFormItemComponent type="currency" />
|
</td>
|
||||||
</Form.Item>
|
<td>{getPayoutMethodLabel(payoutMethod, t)}</td>
|
||||||
</td>
|
<td>
|
||||||
<td>
|
<Form.Item key={`${index}rate`} name={[field.name, "rate"]}>
|
||||||
<Form.Item key={`${index}payoutamount`} name={[field.name, "payoutamount"]}>
|
<ReadOnlyFormItemComponent type="currency" />
|
||||||
<ReadOnlyFormItemComponent type="currency" />
|
</Form.Item>
|
||||||
</Form.Item>
|
</td>
|
||||||
</td>
|
<td>
|
||||||
</tr>
|
<Form.Item key={`${index}payoutamount`} name={[field.name, "payoutamount"]}>
|
||||||
))}
|
<ReadOnlyFormItemComponent type="currency" />
|
||||||
|
</Form.Item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<Alert type="success" title={t("timetickets.labels.payrollclaimedtasks")} />
|
<Alert type="success" title={t("timetickets.labels.payrollclaimedtasks")} />
|
||||||
|
|||||||
@@ -25,6 +25,22 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
});
|
});
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(TimeTickeTaskModalContainer);
|
export default connect(mapStateToProps, mapDispatchToProps)(TimeTickeTaskModalContainer);
|
||||||
|
|
||||||
|
const toFiniteNumber = (value) => {
|
||||||
|
const parsed = Number(value);
|
||||||
|
return Number.isFinite(parsed) ? parsed : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPreviewPayoutAmount = (ticket) => {
|
||||||
|
const productiveHours = toFiniteNumber(ticket?.productivehrs);
|
||||||
|
const rate = toFiniteNumber(ticket?.rate);
|
||||||
|
|
||||||
|
if (productiveHours === null || rate === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return productiveHours * rate;
|
||||||
|
};
|
||||||
|
|
||||||
export function TimeTickeTaskModalContainer({ currentUser, technician, timeTicketTasksModal, toggleModalVisible }) {
|
export function TimeTickeTaskModalContainer({ currentUser, technician, timeTicketTasksModal, toggleModalVisible }) {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const { context, open, actions } = timeTicketTasksModal;
|
const { context, open, actions } = timeTicketTasksModal;
|
||||||
@@ -93,7 +109,7 @@ export function TimeTickeTaskModalContainer({ currentUser, technician, timeTicke
|
|||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
timetickets: (data.ticketsToInsert || []).map((ticket) => ({
|
timetickets: (data.ticketsToInsert || []).map((ticket) => ({
|
||||||
...ticket,
|
...ticket,
|
||||||
payoutamount: Number(ticket.productivehrs || 0) * Number(ticket.rate || 0)
|
payoutamount: getPreviewPayoutAmount(ticket)
|
||||||
}))
|
}))
|
||||||
});
|
});
|
||||||
setUnassignedHours(data.unassignedHours);
|
setUnassignedHours(data.unassignedHours);
|
||||||
|
|||||||
@@ -1183,6 +1183,7 @@
|
|||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"active": "Active",
|
"active": "Active",
|
||||||
|
"allocation": "Allocation",
|
||||||
"allocation_percentage": "Allocation %",
|
"allocation_percentage": "Allocation %",
|
||||||
"employeeid": "Employee",
|
"employeeid": "Employee",
|
||||||
"max_load": "Max Load",
|
"max_load": "Max Load",
|
||||||
@@ -1194,6 +1195,7 @@
|
|||||||
"allocation_total": "Allocation Total: {{total}}%"
|
"allocation_total": "Allocation Total: {{total}}%"
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
|
"commission": "Commission",
|
||||||
"commission_percentage": "Commission %",
|
"commission_percentage": "Commission %",
|
||||||
"hourly": "Hourly"
|
"hourly": "Hourly"
|
||||||
}
|
}
|
||||||
@@ -3610,6 +3612,7 @@
|
|||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"actualhrs": "Actual Hours",
|
"actualhrs": "Actual Hours",
|
||||||
|
"amount": "Amount",
|
||||||
"ciecacode": "CIECA Code",
|
"ciecacode": "CIECA Code",
|
||||||
"clockhours": "Clock Hours",
|
"clockhours": "Clock Hours",
|
||||||
"clockoff": "Clock Off",
|
"clockoff": "Clock Off",
|
||||||
@@ -3625,7 +3628,9 @@
|
|||||||
"flat_rate": "Flat Rate?",
|
"flat_rate": "Flat Rate?",
|
||||||
"memo": "Memo",
|
"memo": "Memo",
|
||||||
"pay": "Pay",
|
"pay": "Pay",
|
||||||
|
"payout_method": "Payout Method",
|
||||||
"productivehrs": "Productive Hours",
|
"productivehrs": "Productive Hours",
|
||||||
|
"rate": "Rate",
|
||||||
"ro_number": "Job to Post Against",
|
"ro_number": "Job to Post Against",
|
||||||
"task_name": "Task"
|
"task_name": "Task"
|
||||||
},
|
},
|
||||||
@@ -3644,6 +3649,10 @@
|
|||||||
"lunch": "Lunch",
|
"lunch": "Lunch",
|
||||||
"new": "New Time Ticket",
|
"new": "New Time Ticket",
|
||||||
"payrollclaimedtasks": "These time tickets will be automatically entered to the system as a part of claiming this task. These numbers are calculated using the jobs assigned lines. If lines are unassigned, they will be excluded from created tickets.",
|
"payrollclaimedtasks": "These time tickets will be automatically entered to the system as a part of claiming this task. These numbers are calculated using the jobs assigned lines. If lines are unassigned, they will be excluded from created tickets.",
|
||||||
|
"payout_methods": {
|
||||||
|
"commission": "Commission",
|
||||||
|
"hourly": "Hourly"
|
||||||
|
},
|
||||||
"pmbreak": "PM Break",
|
"pmbreak": "PM Break",
|
||||||
"pmshift": "PM Shift",
|
"pmshift": "PM Shift",
|
||||||
"shift": "Shift",
|
"shift": "Shift",
|
||||||
|
|||||||
@@ -1183,6 +1183,7 @@
|
|||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"active": "",
|
"active": "",
|
||||||
|
"allocation": "",
|
||||||
"allocation_percentage": "",
|
"allocation_percentage": "",
|
||||||
"employeeid": "",
|
"employeeid": "",
|
||||||
"max_load": "",
|
"max_load": "",
|
||||||
@@ -1194,6 +1195,7 @@
|
|||||||
"allocation_total": ""
|
"allocation_total": ""
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
|
"commission": "",
|
||||||
"commission_percentage": "",
|
"commission_percentage": "",
|
||||||
"hourly": ""
|
"hourly": ""
|
||||||
}
|
}
|
||||||
@@ -3610,6 +3612,7 @@
|
|||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"actualhrs": "",
|
"actualhrs": "",
|
||||||
|
"amount": "",
|
||||||
"ciecacode": "",
|
"ciecacode": "",
|
||||||
"clockhours": "",
|
"clockhours": "",
|
||||||
"clockoff": "",
|
"clockoff": "",
|
||||||
@@ -3625,7 +3628,9 @@
|
|||||||
"flat_rate": "",
|
"flat_rate": "",
|
||||||
"memo": "",
|
"memo": "",
|
||||||
"pay": "",
|
"pay": "",
|
||||||
|
"payout_method": "",
|
||||||
"productivehrs": "",
|
"productivehrs": "",
|
||||||
|
"rate": "",
|
||||||
"ro_number": "",
|
"ro_number": "",
|
||||||
"task_name": ""
|
"task_name": ""
|
||||||
},
|
},
|
||||||
@@ -3644,6 +3649,10 @@
|
|||||||
"lunch": "",
|
"lunch": "",
|
||||||
"new": "",
|
"new": "",
|
||||||
"payrollclaimedtasks": "",
|
"payrollclaimedtasks": "",
|
||||||
|
"payout_methods": {
|
||||||
|
"commission": "",
|
||||||
|
"hourly": ""
|
||||||
|
},
|
||||||
"pmbreak": "",
|
"pmbreak": "",
|
||||||
"pmshift": "",
|
"pmshift": "",
|
||||||
"shift": "",
|
"shift": "",
|
||||||
|
|||||||
@@ -1183,6 +1183,7 @@
|
|||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"active": "",
|
"active": "",
|
||||||
|
"allocation": "",
|
||||||
"allocation_percentage": "",
|
"allocation_percentage": "",
|
||||||
"employeeid": "",
|
"employeeid": "",
|
||||||
"max_load": "",
|
"max_load": "",
|
||||||
@@ -1194,6 +1195,7 @@
|
|||||||
"allocation_total": ""
|
"allocation_total": ""
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
|
"commission": "",
|
||||||
"commission_percentage": "",
|
"commission_percentage": "",
|
||||||
"hourly": ""
|
"hourly": ""
|
||||||
}
|
}
|
||||||
@@ -3610,6 +3612,7 @@
|
|||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"actualhrs": "",
|
"actualhrs": "",
|
||||||
|
"amount": "",
|
||||||
"ciecacode": "",
|
"ciecacode": "",
|
||||||
"clockhours": "",
|
"clockhours": "",
|
||||||
"clockoff": "",
|
"clockoff": "",
|
||||||
@@ -3625,7 +3628,9 @@
|
|||||||
"flat_rate": "",
|
"flat_rate": "",
|
||||||
"memo": "",
|
"memo": "",
|
||||||
"pay": "",
|
"pay": "",
|
||||||
|
"payout_method": "",
|
||||||
"productivehrs": "",
|
"productivehrs": "",
|
||||||
|
"rate": "",
|
||||||
"ro_number": "",
|
"ro_number": "",
|
||||||
"task_name": ""
|
"task_name": ""
|
||||||
},
|
},
|
||||||
@@ -3644,6 +3649,10 @@
|
|||||||
"lunch": "",
|
"lunch": "",
|
||||||
"new": "",
|
"new": "",
|
||||||
"payrollclaimedtasks": "",
|
"payrollclaimedtasks": "",
|
||||||
|
"payout_methods": {
|
||||||
|
"commission": "",
|
||||||
|
"hourly": ""
|
||||||
|
},
|
||||||
"pmbreak": "",
|
"pmbreak": "",
|
||||||
"pmshift": "",
|
"pmshift": "",
|
||||||
"shift": "",
|
"shift": "",
|
||||||
|
|||||||
@@ -158,6 +158,22 @@ describe("payroll payout helpers", () => {
|
|||||||
)
|
)
|
||||||
).toThrow("Missing commission percent for Jane Doe on labor type LAA.");
|
).toThrow("Missing commission percent for Jane Doe on labor type LAA.");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("throws a useful error when an hourly payout rate is missing", () => {
|
||||||
|
expect(() =>
|
||||||
|
payAllModule.BuildPayoutDetails(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
labor_rates: {},
|
||||||
|
employee: {
|
||||||
|
first_name: "John",
|
||||||
|
last_name: "Smith"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"LAB"
|
||||||
|
)
|
||||||
|
).toThrow("Missing hourly payout rate for John Smith on labor type LAB.");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("payroll routes", () => {
|
describe("payroll routes", () => {
|
||||||
@@ -364,4 +380,86 @@ describe("payroll routes", () => {
|
|||||||
error: "Task preset percentages for labor type LAA total 110% and cannot exceed 100%."
|
error: "Task preset percentages for labor type LAA total 110% and cannot exceed 100%."
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("rejects claim-task when an assigned team member is missing the hourly rate for the selected labor type", async () => {
|
||||||
|
const job = buildBaseJob({
|
||||||
|
bodyshop: {
|
||||||
|
id: "shop-1",
|
||||||
|
md_responsibility_centers: {
|
||||||
|
defaults: {
|
||||||
|
costs: {
|
||||||
|
LAB: "Body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
md_tasks_presets: {
|
||||||
|
presets: [
|
||||||
|
{
|
||||||
|
name: "Teardown",
|
||||||
|
hourstype: ["LAB"],
|
||||||
|
percent: 100,
|
||||||
|
nextstatus: "In Progress",
|
||||||
|
memo: "Teardown"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
employee_teams: [
|
||||||
|
{
|
||||||
|
id: "team-1",
|
||||||
|
employee_team_members: [
|
||||||
|
{
|
||||||
|
percentage: 50,
|
||||||
|
labor_rates: {
|
||||||
|
LAB: 45
|
||||||
|
},
|
||||||
|
employee: {
|
||||||
|
id: "emp-1",
|
||||||
|
first_name: "Configured",
|
||||||
|
last_name: "Tech"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
percentage: 50,
|
||||||
|
labor_rates: {},
|
||||||
|
employee: {
|
||||||
|
id: "emp-2",
|
||||||
|
first_name: "Missing",
|
||||||
|
last_name: "Rate"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
joblines: [
|
||||||
|
{
|
||||||
|
mod_lbr_ty: "LAB",
|
||||||
|
mod_lb_hrs: 4.4,
|
||||||
|
assigned_team: "team-1",
|
||||||
|
convertedtolbr: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const { client, req, res } = buildReqRes({
|
||||||
|
job,
|
||||||
|
body: {
|
||||||
|
task: "Teardown",
|
||||||
|
calculateOnly: true,
|
||||||
|
employee: {
|
||||||
|
name: "Dave",
|
||||||
|
email: "dave@rome.test"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await claimTaskModule.claimtask(req, res);
|
||||||
|
|
||||||
|
expect(client.request).toHaveBeenCalledTimes(1);
|
||||||
|
expect(res.status).toHaveBeenCalledWith(400);
|
||||||
|
expect(res.json).toHaveBeenCalledWith({
|
||||||
|
success: false,
|
||||||
|
error: "Missing hourly payout rate for Missing Rate on labor type LAB."
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user