feature/IO-3587-Commision-Cut - rebuild from master-AIO
This commit is contained in:
@@ -10,8 +10,13 @@ const mapDispatchToProps = () => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
const toFiniteNumber = (value) => {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
};
|
||||
|
||||
const ReadOnlyFormItem = ({ bodyshop, value, type = "text" }) => {
|
||||
if (!value) return null;
|
||||
if (value === null || value === undefined || value === "") return null;
|
||||
switch (type) {
|
||||
case "employee": {
|
||||
const emp = bodyshop.employees.find((e) => e.id === value);
|
||||
@@ -20,8 +25,15 @@ const ReadOnlyFormItem = ({ bodyshop, value, type = "text" }) => {
|
||||
|
||||
case "text":
|
||||
return <div style={{ wordWrap: "break-word", overflowWrap: "break-word" }}>{value}</div>;
|
||||
case "currency":
|
||||
return <div>{Dinero({ amount: Math.round(value * 100) }).toFormat()}</div>;
|
||||
case "currency": {
|
||||
const numericValue = toFiniteNumber(value);
|
||||
|
||||
if (numericValue === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <div>{Dinero({ amount: Math.round(numericValue * 100) }).toFormat()}</div>;
|
||||
}
|
||||
default:
|
||||
return <div style={{ wordWrap: "break-word", overflowWrap: "break-word" }}>{value}</div>;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DownOutlined, UpOutlined } from "@ant-design/icons";
|
||||
import { Space } from "antd";
|
||||
|
||||
export default function FormListMoveArrows({ move, index, total }) {
|
||||
export default function FormListMoveArrows({ move, index, total, orientation = "vertical" }) {
|
||||
const upDisabled = index === 0;
|
||||
const downDisabled = index === total - 1;
|
||||
|
||||
@@ -14,7 +14,7 @@ export default function FormListMoveArrows({ move, index, total }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Space orientation="vertical">
|
||||
<Space orientation={orientation}>
|
||||
<UpOutlined disabled={upDisabled} onClick={handleUp} />
|
||||
<DownOutlined disabled={downDisabled} onClick={handleDown} />
|
||||
</Space>
|
||||
|
||||
@@ -21,6 +21,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
technician: selectTechnician
|
||||
});
|
||||
|
||||
const getRequestErrorMessage = (error) => error?.response?.data?.error || error?.message || "";
|
||||
|
||||
export function PayrollLaborAllocationsTable({
|
||||
jobId,
|
||||
joblines,
|
||||
@@ -43,16 +45,23 @@ export function PayrollLaborAllocationsTable({
|
||||
});
|
||||
const notification = useNotification();
|
||||
|
||||
useEffect(() => {
|
||||
async function CalculateTotals() {
|
||||
const loadTotals = async () => {
|
||||
try {
|
||||
const { data } = await axios.post("/payroll/calculatelabor", {
|
||||
jobid: jobId
|
||||
});
|
||||
setTotals(data);
|
||||
} catch (error) {
|
||||
setTotals([]);
|
||||
notification.error({
|
||||
title: getRequestErrorMessage(error)
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!!joblines && !!timetickets && !!bodyshop) {
|
||||
CalculateTotals();
|
||||
loadTotals();
|
||||
}
|
||||
if (!jobId) setTotals([]);
|
||||
}, [joblines, timetickets, bodyshop, adjustments, jobId]);
|
||||
@@ -210,28 +219,36 @@ export function PayrollLaborAllocationsTable({
|
||||
<Button
|
||||
disabled={!hasTimeTicketAccess}
|
||||
onClick={async () => {
|
||||
const response = await axios.post("/payroll/payall", {
|
||||
jobid: jobId
|
||||
});
|
||||
try {
|
||||
const response = await axios.post("/payroll/payall", {
|
||||
jobid: jobId
|
||||
});
|
||||
|
||||
if (response.status === 200) {
|
||||
if (response.data.success !== false) {
|
||||
notification.success({
|
||||
title: t("timetickets.successes.payall")
|
||||
});
|
||||
if (response.status === 200) {
|
||||
if (response.data.success !== false) {
|
||||
notification.success({
|
||||
title: t("timetickets.successes.payall")
|
||||
});
|
||||
} else {
|
||||
notification.error({
|
||||
title: t("timetickets.errors.payall", {
|
||||
error: response.data.error
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
if (refetch) refetch();
|
||||
} else {
|
||||
notification.error({
|
||||
title: t("timetickets.errors.payall", {
|
||||
error: response.data.error
|
||||
error: JSON.stringify("")
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
if (refetch) refetch();
|
||||
} else {
|
||||
} catch (error) {
|
||||
notification.error({
|
||||
title: t("timetickets.errors.payall", {
|
||||
error: JSON.stringify("")
|
||||
error: getRequestErrorMessage(error)
|
||||
})
|
||||
});
|
||||
}
|
||||
@@ -241,10 +258,7 @@ export function PayrollLaborAllocationsTable({
|
||||
</Button>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
const { data } = await axios.post("/payroll/calculatelabor", {
|
||||
jobid: jobId
|
||||
});
|
||||
setTotals(data);
|
||||
await loadTotals();
|
||||
refetch();
|
||||
}}
|
||||
icon={<SyncOutlined />}
|
||||
|
||||
@@ -16,6 +16,43 @@ const mapDispatchToProps = () => ({
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoTaskPresets);
|
||||
|
||||
const normalizePercent = (value) => Math.round((Number(value || 0) + Number.EPSILON) * 10000) / 10000;
|
||||
|
||||
const getTaskPresetAllocationErrors = (presets = [], t) => {
|
||||
const totalsByLaborType = {};
|
||||
|
||||
presets.forEach((preset) => {
|
||||
const percent = normalizePercent(preset?.percent);
|
||||
|
||||
if (!percent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const laborTypes = Array.isArray(preset?.hourstype) ? preset.hourstype : [];
|
||||
|
||||
laborTypes.forEach((laborType) => {
|
||||
if (!laborType) {
|
||||
return;
|
||||
}
|
||||
|
||||
totalsByLaborType[laborType] = normalizePercent((totalsByLaborType[laborType] || 0) + percent);
|
||||
});
|
||||
});
|
||||
|
||||
return Object.entries(totalsByLaborType)
|
||||
.filter(([, total]) => total > 100)
|
||||
.map(([laborType, total]) => {
|
||||
const translatedLaborType = t(`joblines.fields.lbr_types.${laborType}`);
|
||||
const laborTypeLabel =
|
||||
translatedLaborType === `joblines.fields.lbr_types.${laborType}` ? laborType : translatedLaborType;
|
||||
|
||||
return t("bodyshop.errors.task_preset_allocation_exceeded", {
|
||||
laborType: laborTypeLabel,
|
||||
total
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export function ShopInfoTaskPresets({ bodyshop }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -39,8 +76,21 @@ export function ShopInfoTaskPresets({ bodyshop }) {
|
||||
</LayoutFormRow>
|
||||
|
||||
<LayoutFormRow header={t("bodyshop.labels.md_tasks_presets")}>
|
||||
<Form.List name={["md_tasks_presets", "presets"]}>
|
||||
{(fields, { add, remove, move }) => {
|
||||
<Form.List
|
||||
name={["md_tasks_presets", "presets"]}
|
||||
rules={[
|
||||
{
|
||||
validator: async (_, presets) => {
|
||||
const allocationErrors = getTaskPresetAllocationErrors(presets, t);
|
||||
|
||||
if (allocationErrors.length > 0) {
|
||||
throw new Error(allocationErrors.join(" "));
|
||||
}
|
||||
}
|
||||
}
|
||||
]}
|
||||
>
|
||||
{(fields, { add, remove, move }, { errors }) => {
|
||||
return (
|
||||
<div>
|
||||
{fields.map((field, index) => (
|
||||
@@ -189,6 +239,7 @@ export function ShopInfoTaskPresets({ bodyshop }) {
|
||||
</LayoutFormRow>
|
||||
</Form.Item>
|
||||
))}
|
||||
<Form.ErrorList errors={errors} />
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="dashed"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DeleteFilled } from "@ant-design/icons";
|
||||
import { useMutation, useQuery } from "@apollo/client/react";
|
||||
import { Button, Card, Form, Input, InputNumber, Space, Switch } from "antd";
|
||||
import { Button, Card, Col, Form, Input, InputNumber, Row, Select, Space, Switch, Tag, Typography } from "antd";
|
||||
|
||||
import querystring from "query-string";
|
||||
import { useEffect } from "react";
|
||||
@@ -26,10 +26,59 @@ import { useNotification } from "../../contexts/Notifications/notificationContex
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = () => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
const mapDispatchToProps = () => ({});
|
||||
|
||||
const LABOR_TYPES = ["LAA", "LAB", "LAD", "LAE", "LAF", "LAG", "LAM", "LAR", "LAS", "LAU", "LA1", "LA2", "LA3", "LA4"];
|
||||
|
||||
const PAYOUT_METHOD_OPTIONS = [
|
||||
{ labelKey: "employee_teams.options.hourly", value: "hourly" },
|
||||
{ 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 = {}) => ({
|
||||
...teamMember,
|
||||
payout_method: teamMember.payout_method || "hourly",
|
||||
labor_rates: teamMember.labor_rates || {},
|
||||
commission_rates: teamMember.commission_rates || {}
|
||||
});
|
||||
|
||||
const normalizeEmployeeTeam = (employeeTeam = {}) => ({
|
||||
...employeeTeam,
|
||||
employee_team_members: (employeeTeam.employee_team_members || []).map(normalizeTeamMember)
|
||||
});
|
||||
|
||||
const getSplitTotal = (teamMembers = []) =>
|
||||
teamMembers.reduce((sum, member) => sum + Number(member?.percentage || 0), 0);
|
||||
|
||||
const hasExactSplitTotal = (teamMembers = []) => Math.abs(getSplitTotal(teamMembers) - 100) < 0.00001;
|
||||
|
||||
const getPayoutMethodTagColor = (payoutMethod) => (payoutMethod === "commission" ? "gold" : "blue");
|
||||
|
||||
const getEmployeeDisplayName = (employees = [], employeeId) => {
|
||||
const employee = employees.find((currentEmployee) => currentEmployee.id === employeeId);
|
||||
if (!employee) return null;
|
||||
|
||||
const fullName = [employee.first_name, employee.last_name].filter(Boolean).join(" ").trim();
|
||||
return fullName || employee.employee_number || null;
|
||||
};
|
||||
|
||||
const formatAllocationPercentage = (percentage) => {
|
||||
if (percentage === null || percentage === undefined || percentage === "") return null;
|
||||
|
||||
const numericValue = Number(percentage);
|
||||
if (!Number.isFinite(numericValue)) return null;
|
||||
|
||||
return `${numericValue.toFixed(2).replace(/\.?0+$/, "")}%`;
|
||||
};
|
||||
|
||||
export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
||||
const { t } = useTranslation();
|
||||
const [form] = Form.useForm();
|
||||
@@ -45,38 +94,100 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.employee_teams_by_pk) form.setFieldsValue(data.employee_teams_by_pk);
|
||||
else {
|
||||
if (data?.employee_teams_by_pk) {
|
||||
form.setFieldsValue(normalizeEmployeeTeam(data.employee_teams_by_pk));
|
||||
} else {
|
||||
form.resetFields();
|
||||
}
|
||||
}, [form, data, search.employeeTeamId]);
|
||||
|
||||
const [updateEmployeeTeam] = useMutation(UPDATE_EMPLOYEE_TEAM);
|
||||
const [insertEmployeeTeam] = useMutation(INSERT_EMPLOYEE_TEAM);
|
||||
const payoutMethodOptions = PAYOUT_METHOD_OPTIONS.map(({ labelKey, value }) => ({
|
||||
label: t(labelKey),
|
||||
value
|
||||
}));
|
||||
const teamName = Form.useWatch("name", form);
|
||||
const teamMembers = Form.useWatch(["employee_team_members"], form) || [];
|
||||
const teamCardTitle = teamName?.trim() || t("employee_teams.fields.name");
|
||||
|
||||
const getTeamMemberTitle = (teamMember = {}) => {
|
||||
const employeeName =
|
||||
getEmployeeDisplayName(bodyshop.employees, teamMember.employeeid) || t("employee_teams.fields.employeeid");
|
||||
const allocation = formatAllocationPercentage(teamMember.percentage);
|
||||
const payoutMethod =
|
||||
teamMember.payout_method === "commission"
|
||||
? t("employee_teams.options.commission")
|
||||
: t("employee_teams.options.hourly");
|
||||
|
||||
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 normalizedTeamMembers = employee_team_members.map((teamMember) => {
|
||||
const nextTeamMember = normalizeTeamMember({ ...teamMember });
|
||||
delete nextTeamMember.__typename;
|
||||
return nextTeamMember;
|
||||
});
|
||||
|
||||
if (normalizedTeamMembers.length === 0) {
|
||||
notification.error({
|
||||
title: t("employee_teams.errors.minimum_one_member")
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const employeeIds = normalizedTeamMembers.map((teamMember) => teamMember.employeeid).filter(Boolean);
|
||||
const duplicateEmployeeIds = employeeIds.filter((employeeId, index) => employeeIds.indexOf(employeeId) !== index);
|
||||
|
||||
if (duplicateEmployeeIds.length > 0) {
|
||||
notification.error({
|
||||
title: t("employee_teams.errors.duplicate_member")
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasExactSplitTotal(normalizedTeamMembers)) {
|
||||
notification.error({
|
||||
title: t("employee_teams.errors.allocation_total_exact")
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const handleFinish = async ({ employee_team_members, ...values }) => {
|
||||
if (search.employeeTeamId && search.employeeTeamId !== "new") {
|
||||
//Update a record.
|
||||
logImEXEvent("shop_employee_update");
|
||||
|
||||
const result = await updateEmployeeTeam({
|
||||
variables: {
|
||||
employeeTeamId: search.employeeTeamId,
|
||||
employeeTeam: values,
|
||||
teamMemberUpdates: employee_team_members
|
||||
.filter((e) => e.id)
|
||||
.map((e) => {
|
||||
delete e.__typename;
|
||||
return { where: { id: { _eq: e.id } }, _set: e };
|
||||
}),
|
||||
teamMemberInserts: employee_team_members
|
||||
.filter((e) => e.id === null || e.id === undefined)
|
||||
.map((e) => ({ ...e, teamid: search.employeeTeamId })),
|
||||
teamMemberDeletes: data.employee_teams_by_pk.employee_team_members.filter(
|
||||
(e) => !employee_team_members.find((etm) => etm.id === e.id)
|
||||
)
|
||||
teamMemberUpdates: normalizedTeamMembers
|
||||
.filter((teamMember) => teamMember.id)
|
||||
.map((teamMember) => ({
|
||||
where: { id: { _eq: teamMember.id } },
|
||||
_set: teamMember
|
||||
})),
|
||||
teamMemberInserts: normalizedTeamMembers
|
||||
.filter((teamMember) => teamMember.id === null || teamMember.id === undefined)
|
||||
.map((teamMember) => ({ ...teamMember, teamid: search.employeeTeamId })),
|
||||
teamMemberDeletes: data.employee_teams_by_pk.employee_team_members
|
||||
.filter(
|
||||
(teamMember) => !normalizedTeamMembers.find((currentTeamMember) => currentTeamMember.id === teamMember.id)
|
||||
)
|
||||
.map((teamMember) => teamMember.id)
|
||||
}
|
||||
});
|
||||
|
||||
if (!result.errors) {
|
||||
notification.success({
|
||||
title: t("employees.successes.save")
|
||||
@@ -89,20 +200,19 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
||||
});
|
||||
}
|
||||
} else {
|
||||
//New record, insert it.
|
||||
logImEXEvent("shop_employee_insert");
|
||||
|
||||
insertEmployeeTeam({
|
||||
variables: {
|
||||
employeeTeam: {
|
||||
...values,
|
||||
employee_team_members: { data: employee_team_members },
|
||||
employee_team_members: { data: normalizedTeamMembers },
|
||||
bodyshopid: bodyshop.id
|
||||
}
|
||||
},
|
||||
refetchQueries: ["QUERY_TEAMS"]
|
||||
}).then((r) => {
|
||||
search.employeeTeamId = r.data.insert_employee_teams_one.id;
|
||||
}).then((response) => {
|
||||
search.employeeTeamId = response.data.insert_employee_teams_one.id;
|
||||
history({ search: querystring.stringify(search) });
|
||||
notification.success({
|
||||
title: t("employees.successes.save")
|
||||
@@ -116,6 +226,7 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={teamCardTitle}
|
||||
extra={
|
||||
<Button type="primary" onClick={() => form.submit()}>
|
||||
{t("general.actions.save")}
|
||||
@@ -130,7 +241,6 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
@@ -145,7 +255,6 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
@@ -156,243 +265,149 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
||||
{(fields, { add, remove, move }) => {
|
||||
return (
|
||||
<div>
|
||||
{fields.map((field, index) => (
|
||||
<Form.Item key={field.key} style={{ padding: 0, margin: 2 }}>
|
||||
<Form.Item label={t("employees.fields.id")} key={`${index}`} name={[field.name, "id"]} hidden>
|
||||
<Input type="hidden" />
|
||||
</Form.Item>
|
||||
<LayoutFormRow grow>
|
||||
<Form.Item
|
||||
label={t("employee_teams.fields.employeeid")}
|
||||
key={`${index}`}
|
||||
name={[field.name, "employeeid"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<EmployeeSearchSelectComponent options={bodyshop.employees} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("employee_teams.fields.percentage")}
|
||||
key={`${index}`}
|
||||
name={[field.name, "percentage"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} max={100} precision={2} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("joblines.fields.lbr_types.LAA")}
|
||||
key={`${index}`}
|
||||
name={[field.name, "labor_rates", "LAA"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CurrencyInput />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("joblines.fields.lbr_types.LAB")}
|
||||
key={`${index}`}
|
||||
name={[field.name, "labor_rates", "LAB"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CurrencyInput />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("joblines.fields.lbr_types.LAD")}
|
||||
key={`${index}`}
|
||||
name={[field.name, "labor_rates", "LAD"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CurrencyInput />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("joblines.fields.lbr_types.LAE")}
|
||||
key={`${index}`}
|
||||
name={[field.name, "labor_rates", "LAE"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CurrencyInput />
|
||||
</Form.Item>
|
||||
{fields.map((field, index) => {
|
||||
const teamMember = normalizeTeamMember(teamMembers[field.name]);
|
||||
|
||||
<Form.Item
|
||||
label={t("joblines.fields.lbr_types.LAF")}
|
||||
key={`${index}`}
|
||||
name={[field.name, "labor_rates", "LAF"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CurrencyInput />
|
||||
return (
|
||||
<Form.Item key={field.key} style={{ padding: 0, margin: 2 }}>
|
||||
<Form.Item label={t("employees.fields.id")} key={`${index}`} name={[field.name, "id"]} hidden>
|
||||
<Input type="hidden" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("joblines.fields.lbr_types.LAG")}
|
||||
key={`${index}`}
|
||||
name={[field.name, "labor_rates", "LAG"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
<LayoutFormRow
|
||||
grow
|
||||
title={getTeamMemberTitle(teamMember)}
|
||||
extra={
|
||||
<Space align="center" size="small">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<DeleteFilled />}
|
||||
onClick={() => {
|
||||
remove(field.name);
|
||||
}}
|
||||
/>
|
||||
<FormListMoveArrows
|
||||
move={move}
|
||||
index={index}
|
||||
total={fields.length}
|
||||
orientation="horizontal"
|
||||
/>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<CurrencyInput />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("joblines.fields.lbr_types.LAM")}
|
||||
key={`${index}`}
|
||||
name={[field.name, "labor_rates", "LAM"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CurrencyInput />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("joblines.fields.lbr_types.LAR")}
|
||||
key={`${index}`}
|
||||
name={[field.name, "labor_rates", "LAR"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CurrencyInput />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("joblines.fields.lbr_types.LAS")}
|
||||
key={`${index}`}
|
||||
name={[field.name, "labor_rates", "LAS"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CurrencyInput />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("joblines.fields.lbr_types.LAU")}
|
||||
key={`${index}`}
|
||||
name={[field.name, "labor_rates", "LAU"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CurrencyInput />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("joblines.fields.lbr_types.LA1")}
|
||||
key={`${index}`}
|
||||
name={[field.name, "labor_rates", "LA1"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CurrencyInput />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("joblines.fields.lbr_types.LA2")}
|
||||
key={`${index}`}
|
||||
name={[field.name, "labor_rates", "LA2"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CurrencyInput />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("joblines.fields.lbr_types.LA3")}
|
||||
key={`${index}`}
|
||||
name={[field.name, "labor_rates", "LA3"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CurrencyInput />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("joblines.fields.lbr_types.LA4")}
|
||||
key={`${index}`}
|
||||
name={[field.name, "labor_rates", "LA4"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CurrencyInput />
|
||||
</Form.Item>
|
||||
<Space align="center">
|
||||
<DeleteFilled
|
||||
onClick={() => {
|
||||
remove(field.name);
|
||||
}}
|
||||
/>
|
||||
<FormListMoveArrows move={move} index={index} total={fields.length} />
|
||||
</Space>
|
||||
</LayoutFormRow>
|
||||
</Form.Item>
|
||||
))}
|
||||
<div>
|
||||
<Row gutter={[16, 0]}>
|
||||
<Col {...TEAM_MEMBER_PRIMARY_FIELD_COLS.employee}>
|
||||
<Form.Item
|
||||
label={t("employee_teams.fields.employeeid")}
|
||||
key={`${index}`}
|
||||
name={[field.name, "employeeid"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
}
|
||||
]}
|
||||
>
|
||||
<EmployeeSearchSelectComponent options={bodyshop.employees} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col {...TEAM_MEMBER_PRIMARY_FIELD_COLS.allocation}>
|
||||
<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>
|
||||
</Form.Item>
|
||||
);
|
||||
})}
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="dashed"
|
||||
onClick={() => {
|
||||
add();
|
||||
add({
|
||||
percentage: 0,
|
||||
payout_method: "hourly",
|
||||
labor_rates: {},
|
||||
commission_rates: {}
|
||||
});
|
||||
}}
|
||||
style={{ width: "100%" }}
|
||||
>
|
||||
{t("employee_teams.actions.newmember")}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
<Form.Item noStyle shouldUpdate>
|
||||
{() => {
|
||||
const teamMembers = form.getFieldValue(["employee_team_members"]) || [];
|
||||
const splitTotal = getSplitTotal(teamMembers);
|
||||
|
||||
return (
|
||||
<Typography.Text type={hasExactSplitTotal(teamMembers) ? undefined : "danger"}>
|
||||
{t("employee_teams.labels.allocation_total", {
|
||||
total: splitTotal.toFixed(2)
|
||||
})}
|
||||
</Typography.Text>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
|
||||
@@ -15,6 +15,18 @@ const mapDispatchToProps = () => ({
|
||||
});
|
||||
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 }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -35,7 +47,15 @@ export function TimeTicketTaskModalComponent({ bodyshop, form, loading, complete
|
||||
<JobSearchSelectComponent convertedOnly={true} notExported={true} />
|
||||
</Form.Item>
|
||||
<Space wrap>
|
||||
<Form.Item name="task" label={t("timetickets.labels.task")}>
|
||||
<Form.Item
|
||||
name="task"
|
||||
label={t("timetickets.labels.task")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
}
|
||||
]}
|
||||
>
|
||||
{loading ? (
|
||||
<Spin />
|
||||
) : (
|
||||
@@ -93,33 +113,51 @@ export function TimeTicketTaskModalComponent({ bodyshop, form, loading, complete
|
||||
<th>{t("timetickets.fields.cost_center")}</th>
|
||||
<th>{t("timetickets.fields.ciecacode")}</th>
|
||||
<th>{t("timetickets.fields.productivehrs")}</th>
|
||||
<th>{t("timetickets.fields.payout_method")}</th>
|
||||
<th>{t("timetickets.fields.rate")}</th>
|
||||
<th>{t("timetickets.fields.amount")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{fields.map((field, index) => (
|
||||
<tr key={field.key}>
|
||||
<td>
|
||||
<Form.Item key={`${index}employeeid`} name={[field.name, "employeeid"]}>
|
||||
<ReadOnlyFormItemComponent type="employee" />
|
||||
</Form.Item>
|
||||
</td>
|
||||
<td>
|
||||
<Form.Item key={`${index}cost_center`} name={[field.name, "cost_center"]}>
|
||||
<ReadOnlyFormItemComponent />
|
||||
</Form.Item>
|
||||
</td>
|
||||
<td>
|
||||
<Form.Item key={`${index}ciecacode`} name={[field.name, "ciecacode"]}>
|
||||
<ReadOnlyFormItemComponent />
|
||||
</Form.Item>
|
||||
</td>
|
||||
<td>
|
||||
<Form.Item key={`${index}productivehrs`} name={[field.name, "productivehrs"]}>
|
||||
<ReadOnlyFormItemComponent />
|
||||
</Form.Item>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{fields.map((field, index) => {
|
||||
const payoutMethod = form.getFieldValue(["timetickets", field.name, "payout_context", "payout_method"]);
|
||||
|
||||
return (
|
||||
<tr key={field.key}>
|
||||
<td>
|
||||
<Form.Item key={`${index}employeeid`} name={[field.name, "employeeid"]}>
|
||||
<ReadOnlyFormItemComponent type="employee" />
|
||||
</Form.Item>
|
||||
</td>
|
||||
<td>
|
||||
<Form.Item key={`${index}cost_center`} name={[field.name, "cost_center"]}>
|
||||
<ReadOnlyFormItemComponent />
|
||||
</Form.Item>
|
||||
</td>
|
||||
<td>
|
||||
<Form.Item key={`${index}ciecacode`} name={[field.name, "ciecacode"]}>
|
||||
<ReadOnlyFormItemComponent />
|
||||
</Form.Item>
|
||||
</td>
|
||||
<td>
|
||||
<Form.Item key={`${index}productivehrs`} name={[field.name, "productivehrs"]}>
|
||||
<ReadOnlyFormItemComponent />
|
||||
</Form.Item>
|
||||
</td>
|
||||
<td>{getPayoutMethodLabel(payoutMethod, t)}</td>
|
||||
<td>
|
||||
<Form.Item key={`${index}rate`} name={[field.name, "rate"]}>
|
||||
<ReadOnlyFormItemComponent type="currency" />
|
||||
</Form.Item>
|
||||
</td>
|
||||
<td>
|
||||
<Form.Item key={`${index}payoutamount`} name={[field.name, "payoutamount"]}>
|
||||
<ReadOnlyFormItemComponent type="currency" />
|
||||
</Form.Item>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
<Alert type="success" title={t("timetickets.labels.payrollclaimedtasks")} />
|
||||
|
||||
@@ -25,6 +25,22 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
});
|
||||
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 }) {
|
||||
const [form] = Form.useForm();
|
||||
const { context, open, actions } = timeTicketTasksModal;
|
||||
@@ -90,7 +106,12 @@ export function TimeTickeTaskModalContainer({ currentUser, technician, timeTicke
|
||||
if (actions?.refetch) actions.refetch();
|
||||
toggleModalVisible();
|
||||
} else if (handleFinish === false) {
|
||||
form.setFieldsValue({ timetickets: data.ticketsToInsert });
|
||||
form.setFieldsValue({
|
||||
timetickets: (data.ticketsToInsert || []).map((ticket) => ({
|
||||
...ticket,
|
||||
payoutamount: getPreviewPayoutAmount(ticket)
|
||||
}))
|
||||
});
|
||||
setUnassignedHours(data.unassignedHours);
|
||||
} else {
|
||||
notification.error({
|
||||
@@ -101,7 +122,9 @@ export function TimeTickeTaskModalContainer({ currentUser, technician, timeTicke
|
||||
}
|
||||
} catch (error) {
|
||||
notification.error({
|
||||
title: t("timetickets.errors.creating", { message: error.message })
|
||||
title: t("timetickets.errors.creating", {
|
||||
message: error.response?.data?.error || error.message
|
||||
})
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
|
||||
@@ -130,7 +130,15 @@ export function TtApprovalsListComponent({
|
||||
key: "memo",
|
||||
sorter: (a, b) => alphaSort(a.memo, b.memo),
|
||||
sortOrder: state.sortedInfo.columnKey === "memo" && state.sortedInfo.order,
|
||||
render: (text, record) => (record.clockon || record.clockoff ? t(record.memo) : record.memo)
|
||||
render: (text, record) => (record.memo?.startsWith("timetickets.labels") ? t(record.memo) : record.memo)
|
||||
},
|
||||
{
|
||||
title: t("timetickets.fields.task_name"),
|
||||
dataIndex: "task_name",
|
||||
key: "task_name",
|
||||
sorter: (a, b) => alphaSort(a.task_name, b.task_name),
|
||||
sortOrder: state.sortedInfo.columnKey === "task_name" && state.sortedInfo.order,
|
||||
render: (text, record) => record.task_name || ""
|
||||
},
|
||||
{
|
||||
title: t("timetickets.fields.clockon"),
|
||||
@@ -140,12 +148,12 @@ export function TtApprovalsListComponent({
|
||||
render: (text, record) => <DateTimeFormatter>{record.clockon}</DateTimeFormatter>
|
||||
},
|
||||
{
|
||||
title: "Pay",
|
||||
title: t("timetickets.fields.pay"),
|
||||
dataIndex: "pay",
|
||||
key: "pay",
|
||||
render: (text, record) =>
|
||||
Dinero({ amount: Math.round(record.rate * 100) })
|
||||
.multiply(record.flat_rate ? record.productivehrs : record.actualhrs)
|
||||
Dinero({ amount: Math.round((record.rate || 0) * 100) })
|
||||
.multiply(record.flat_rate ? record.productivehrs || 0 : record.actualhrs || 0)
|
||||
.toFormat("$0.00")
|
||||
}
|
||||
];
|
||||
@@ -184,7 +192,7 @@ export function TtApprovalsListComponent({
|
||||
<ResponsiveTable
|
||||
loading={loading}
|
||||
columns={columns}
|
||||
mobileColumnKeys={["ro_number", "date", "employeeid", "cost_center"]}
|
||||
mobileColumnKeys={["ro_number", "date", "employeeid", "cost_center", "task_name"]}
|
||||
rowKey="id"
|
||||
scroll={{
|
||||
x: true
|
||||
|
||||
@@ -18,7 +18,15 @@ const mapStateToProps = createStructuredSelector({
|
||||
authLevel: selectAuthLevel
|
||||
});
|
||||
|
||||
export function TtApproveButton({ bodyshop, currentUser, selectedTickets, disabled, authLevel }) {
|
||||
export function TtApproveButton({
|
||||
bodyshop,
|
||||
currentUser,
|
||||
selectedTickets,
|
||||
disabled,
|
||||
authLevel,
|
||||
completedCallback,
|
||||
refetch
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const client = useApolloClient();
|
||||
const notification = useNotification();
|
||||
@@ -54,6 +62,12 @@ export function TtApproveButton({ bodyshop, currentUser, selectedTickets, disabl
|
||||
})
|
||||
});
|
||||
} else {
|
||||
if (typeof completedCallback === "function") {
|
||||
completedCallback([]);
|
||||
}
|
||||
if (typeof refetch === "function") {
|
||||
refetch();
|
||||
}
|
||||
notification.success({
|
||||
title: t("timetickets.successes.created")
|
||||
});
|
||||
@@ -68,8 +82,6 @@ export function TtApproveButton({ bodyshop, currentUser, selectedTickets, disabl
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
// if (!!completedCallback) completedCallback([]);
|
||||
// if (!!loadingCallback) loadingCallback(false);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -152,6 +152,8 @@ export const QUERY_BODYSHOP = gql`
|
||||
id
|
||||
employeeid
|
||||
labor_rates
|
||||
payout_method
|
||||
commission_rates
|
||||
percentage
|
||||
}
|
||||
}
|
||||
@@ -285,6 +287,8 @@ export const UPDATE_SHOP = gql`
|
||||
id
|
||||
employeeid
|
||||
labor_rates
|
||||
payout_method
|
||||
commission_rates
|
||||
percentage
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ export const QUERY_TEAMS = gql`
|
||||
id
|
||||
employeeid
|
||||
labor_rates
|
||||
payout_method
|
||||
commission_rates
|
||||
percentage
|
||||
}
|
||||
}
|
||||
@@ -29,6 +31,8 @@ export const UPDATE_EMPLOYEE_TEAM = gql`
|
||||
employeeid
|
||||
id
|
||||
labor_rates
|
||||
payout_method
|
||||
commission_rates
|
||||
percentage
|
||||
}
|
||||
}
|
||||
@@ -40,6 +44,8 @@ export const UPDATE_EMPLOYEE_TEAM = gql`
|
||||
employeeid
|
||||
id
|
||||
labor_rates
|
||||
payout_method
|
||||
commission_rates
|
||||
percentage
|
||||
}
|
||||
}
|
||||
@@ -52,6 +58,8 @@ export const UPDATE_EMPLOYEE_TEAM = gql`
|
||||
employeeid
|
||||
id
|
||||
labor_rates
|
||||
payout_method
|
||||
commission_rates
|
||||
percentage
|
||||
}
|
||||
}
|
||||
@@ -69,6 +77,8 @@ export const INSERT_EMPLOYEE_TEAM = gql`
|
||||
employeeid
|
||||
id
|
||||
labor_rates
|
||||
payout_method
|
||||
commission_rates
|
||||
percentage
|
||||
}
|
||||
}
|
||||
@@ -86,6 +96,8 @@ export const QUERY_EMPLOYEE_TEAM_BY_ID = gql`
|
||||
employeeid
|
||||
id
|
||||
labor_rates
|
||||
payout_method
|
||||
commission_rates
|
||||
percentage
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,6 +260,7 @@ export const INSERT_TIME_TICKET_AND_APPROVE = gql`
|
||||
id
|
||||
clockon
|
||||
clockoff
|
||||
created_by
|
||||
employeeid
|
||||
productivehrs
|
||||
actualhrs
|
||||
@@ -267,6 +268,9 @@ export const INSERT_TIME_TICKET_AND_APPROVE = gql`
|
||||
date
|
||||
memo
|
||||
flat_rate
|
||||
task_name
|
||||
payout_context
|
||||
ttapprovalqueueid
|
||||
commited_by
|
||||
committed_at
|
||||
}
|
||||
|
||||
@@ -23,7 +23,14 @@ export const QUERY_ALL_TT_APPROVALS_PAGINATED = gql`
|
||||
ciecacode
|
||||
cost_center
|
||||
date
|
||||
memo
|
||||
flat_rate
|
||||
clockon
|
||||
clockoff
|
||||
rate
|
||||
created_by
|
||||
task_name
|
||||
payout_context
|
||||
}
|
||||
tt_approval_queue_aggregate {
|
||||
aggregate {
|
||||
@@ -42,9 +49,16 @@ export const INSERT_NEW_TT_APPROVALS = gql`
|
||||
productivehrs
|
||||
actualhrs
|
||||
ciecacode
|
||||
cost_center
|
||||
date
|
||||
memo
|
||||
flat_rate
|
||||
rate
|
||||
clockon
|
||||
clockoff
|
||||
created_by
|
||||
task_name
|
||||
payout_context
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,6 +79,11 @@ export const QUERY_TT_APPROVALS_BY_IDS = gql`
|
||||
ciecacode
|
||||
bodyshopid
|
||||
cost_center
|
||||
clockon
|
||||
clockoff
|
||||
created_by
|
||||
task_name
|
||||
payout_context
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -305,7 +305,8 @@
|
||||
"creatingdefaultview": "Error creating default view.",
|
||||
"duplicate_insurance_company": "Duplicate insurance company name. Each insurance company name must be unique",
|
||||
"loading": "Unable to load shop details. Please call technical support.",
|
||||
"saving": "Error encountered while saving. {{message}}"
|
||||
"saving": "Error encountered while saving. {{message}}",
|
||||
"task_preset_allocation_exceeded": "{{laborType}} task preset total is {{total}}% and cannot exceed 100%."
|
||||
},
|
||||
"fields": {
|
||||
"ReceivableCustomField": "QBO Receivable Custom Field {{number}}",
|
||||
@@ -1175,12 +1176,28 @@
|
||||
"new": "New Team",
|
||||
"newmember": "New Team Member"
|
||||
},
|
||||
"errors": {
|
||||
"allocation_total_exact": "Team allocation must total exactly 100%.",
|
||||
"duplicate_member": "Each employee can only appear once per team.",
|
||||
"minimum_one_member": "Add at least one team member."
|
||||
},
|
||||
"fields": {
|
||||
"active": "Active",
|
||||
"allocation": "Allocation",
|
||||
"allocation_percentage": "Allocation %",
|
||||
"employeeid": "Employee",
|
||||
"max_load": "Max Load",
|
||||
"name": "Team Name",
|
||||
"payout_method": "Payout Method",
|
||||
"percentage": "Percent"
|
||||
},
|
||||
"labels": {
|
||||
"allocation_total": "Allocation Total: {{total}}%"
|
||||
},
|
||||
"options": {
|
||||
"commission": "Commission",
|
||||
"commission_percentage": "Commission %",
|
||||
"hourly": "Hourly"
|
||||
}
|
||||
},
|
||||
"employees": {
|
||||
@@ -3594,6 +3611,7 @@
|
||||
},
|
||||
"fields": {
|
||||
"actualhrs": "Actual Hours",
|
||||
"amount": "Amount",
|
||||
"ciecacode": "CIECA Code",
|
||||
"clockhours": "Clock Hours",
|
||||
"clockoff": "Clock Off",
|
||||
@@ -3608,7 +3626,10 @@
|
||||
"employee_team": "Employee Team",
|
||||
"flat_rate": "Flat Rate?",
|
||||
"memo": "Memo",
|
||||
"pay": "Pay",
|
||||
"payout_method": "Payout Method",
|
||||
"productivehrs": "Productive Hours",
|
||||
"rate": "Rate",
|
||||
"ro_number": "Job to Post Against",
|
||||
"task_name": "Task"
|
||||
},
|
||||
@@ -3627,6 +3648,10 @@
|
||||
"lunch": "Lunch",
|
||||
"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.",
|
||||
"payout_methods": {
|
||||
"commission": "Commission",
|
||||
"hourly": "Hourly"
|
||||
},
|
||||
"pmbreak": "PM Break",
|
||||
"pmshift": "PM Shift",
|
||||
"shift": "Shift",
|
||||
|
||||
@@ -305,7 +305,8 @@
|
||||
"creatingdefaultview": "",
|
||||
"duplicate_insurance_company": "",
|
||||
"loading": "No se pueden cargar los detalles de la tienda. Por favor llame al soporte técnico.",
|
||||
"saving": ""
|
||||
"saving": "",
|
||||
"task_preset_allocation_exceeded": ""
|
||||
},
|
||||
"fields": {
|
||||
"ReceivableCustomField": "",
|
||||
@@ -1175,12 +1176,28 @@
|
||||
"new": "",
|
||||
"newmember": ""
|
||||
},
|
||||
"errors": {
|
||||
"allocation_total_exact": "",
|
||||
"duplicate_member": "",
|
||||
"minimum_one_member": ""
|
||||
},
|
||||
"fields": {
|
||||
"active": "",
|
||||
"allocation": "",
|
||||
"allocation_percentage": "",
|
||||
"employeeid": "",
|
||||
"max_load": "",
|
||||
"name": "",
|
||||
"payout_method": "",
|
||||
"percentage": ""
|
||||
},
|
||||
"labels": {
|
||||
"allocation_total": ""
|
||||
},
|
||||
"options": {
|
||||
"commission": "",
|
||||
"commission_percentage": "",
|
||||
"hourly": ""
|
||||
}
|
||||
},
|
||||
"employees": {
|
||||
@@ -3594,6 +3611,7 @@
|
||||
},
|
||||
"fields": {
|
||||
"actualhrs": "",
|
||||
"amount": "",
|
||||
"ciecacode": "",
|
||||
"clockhours": "",
|
||||
"clockoff": "",
|
||||
@@ -3608,7 +3626,10 @@
|
||||
"employee_team": "",
|
||||
"flat_rate": "",
|
||||
"memo": "",
|
||||
"pay": "",
|
||||
"payout_method": "",
|
||||
"productivehrs": "",
|
||||
"rate": "",
|
||||
"ro_number": "",
|
||||
"task_name": ""
|
||||
},
|
||||
@@ -3627,6 +3648,10 @@
|
||||
"lunch": "",
|
||||
"new": "",
|
||||
"payrollclaimedtasks": "",
|
||||
"payout_methods": {
|
||||
"commission": "",
|
||||
"hourly": ""
|
||||
},
|
||||
"pmbreak": "",
|
||||
"pmshift": "",
|
||||
"shift": "",
|
||||
|
||||
@@ -305,7 +305,8 @@
|
||||
"creatingdefaultview": "",
|
||||
"duplicate_insurance_company": "",
|
||||
"loading": "Impossible de charger les détails de la boutique. Veuillez appeler le support technique.",
|
||||
"saving": ""
|
||||
"saving": "",
|
||||
"task_preset_allocation_exceeded": ""
|
||||
},
|
||||
"fields": {
|
||||
"ReceivableCustomField": "",
|
||||
@@ -1175,12 +1176,28 @@
|
||||
"new": "",
|
||||
"newmember": ""
|
||||
},
|
||||
"errors": {
|
||||
"allocation_total_exact": "",
|
||||
"duplicate_member": "",
|
||||
"minimum_one_member": ""
|
||||
},
|
||||
"fields": {
|
||||
"active": "",
|
||||
"allocation": "",
|
||||
"allocation_percentage": "",
|
||||
"employeeid": "",
|
||||
"max_load": "",
|
||||
"name": "",
|
||||
"payout_method": "",
|
||||
"percentage": ""
|
||||
},
|
||||
"labels": {
|
||||
"allocation_total": ""
|
||||
},
|
||||
"options": {
|
||||
"commission": "",
|
||||
"commission_percentage": "",
|
||||
"hourly": ""
|
||||
}
|
||||
},
|
||||
"employees": {
|
||||
@@ -3594,6 +3611,7 @@
|
||||
},
|
||||
"fields": {
|
||||
"actualhrs": "",
|
||||
"amount": "",
|
||||
"ciecacode": "",
|
||||
"clockhours": "",
|
||||
"clockoff": "",
|
||||
@@ -3608,7 +3626,10 @@
|
||||
"employee_team": "",
|
||||
"flat_rate": "",
|
||||
"memo": "",
|
||||
"pay": "",
|
||||
"payout_method": "",
|
||||
"productivehrs": "",
|
||||
"rate": "",
|
||||
"ro_number": "",
|
||||
"task_name": ""
|
||||
},
|
||||
@@ -3627,6 +3648,10 @@
|
||||
"lunch": "",
|
||||
"new": "",
|
||||
"payrollclaimedtasks": "",
|
||||
"payout_methods": {
|
||||
"commission": "",
|
||||
"hourly": ""
|
||||
},
|
||||
"pmbreak": "",
|
||||
"pmshift": "",
|
||||
"shift": "",
|
||||
|
||||
Reference in New Issue
Block a user