IO-3624 Extract shared title-row UI and polish config forms
This commit is contained in:
@@ -1,20 +1,6 @@
|
||||
import { DeleteFilled } from "@ant-design/icons";
|
||||
import { DeleteFilled, HolderOutlined } from "@ant-design/icons";
|
||||
import { useMutation, useQuery } from "@apollo/client/react";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Row,
|
||||
Select,
|
||||
Skeleton,
|
||||
Space,
|
||||
Switch,
|
||||
Tag,
|
||||
Typography
|
||||
} from "antd";
|
||||
import { Button, Card, Col, Form, Input, InputNumber, Row, Select, Skeleton, Space, Switch, Typography } from "antd";
|
||||
|
||||
import querystring from "query-string";
|
||||
import { useEffect, useState } from "react";
|
||||
@@ -28,6 +14,14 @@ import AlertComponent from "../alert/alert.component";
|
||||
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
import {
|
||||
INLINE_TITLE_GROUP_STYLE,
|
||||
INLINE_TITLE_HANDLE_STYLE,
|
||||
INLINE_TITLE_INPUT_STYLE,
|
||||
INLINE_TITLE_LABEL_STYLE,
|
||||
INLINE_TITLE_ROW_STYLE,
|
||||
INLINE_TITLE_SEPARATOR_STYLE
|
||||
} from "../layout-form-row/inline-form-row-title.utils.js";
|
||||
|
||||
import {
|
||||
INSERT_EMPLOYEE_TEAM,
|
||||
@@ -37,11 +31,10 @@ import {
|
||||
import EmployeeSearchSelectComponent from "../employee-search-select/employee-search-select.component";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import {
|
||||
LABOR_TYPES,
|
||||
getSplitTotal,
|
||||
hasExactSplitTotal,
|
||||
LABOR_TYPES,
|
||||
normalizeEmployeeTeam,
|
||||
normalizeTeamMember,
|
||||
validateEmployeeTeamMembers
|
||||
} from "./shop-employee-teams.form.utils.js";
|
||||
|
||||
@@ -55,24 +48,8 @@ const PAYOUT_METHOD_OPTIONS = [
|
||||
{ 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 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;
|
||||
|
||||
@@ -144,41 +121,19 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
||||
const isAllocationTotalExact = hasExactSplitTotal(teamMembers);
|
||||
const allocationTotalValue = formatAllocationPercentage(getSplitTotal(teamMembers))?.replace("%", "") || "0";
|
||||
const teamNameDisplay = teamName?.trim() || t("employee_teams.fields.name");
|
||||
const teamCardTitle = isTeamHydrating
|
||||
? t("employee_teams.fields.name")
|
||||
: (
|
||||
<span>
|
||||
<span>{teamNameDisplay}</span>
|
||||
<span> - </span>
|
||||
<Typography.Text type={isAllocationTotalExact ? undefined : "danger"}>
|
||||
{t("employee_teams.labels.allocation_total", {
|
||||
total: allocationTotalValue
|
||||
})}
|
||||
</Typography.Text>
|
||||
</span>
|
||||
);
|
||||
|
||||
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 variant="filled" color="geekblue">
|
||||
{`${t("employee_teams.fields.allocation")}: ${allocation || "--"}`}
|
||||
</Tag>
|
||||
<Tag variant="filled" color={getPayoutMethodTagColor(teamMember.payout_method)}>
|
||||
{payoutMethod}
|
||||
</Tag>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const teamCardTitle = isTeamHydrating ? (
|
||||
t("employee_teams.fields.name")
|
||||
) : (
|
||||
<span>
|
||||
<span>{teamNameDisplay}</span>
|
||||
<span> - </span>
|
||||
<Typography.Text type={isAllocationTotalExact ? undefined : "danger"}>
|
||||
{t("employee_teams.labels.allocation_total", {
|
||||
total: allocationTotalValue
|
||||
})}
|
||||
</Typography.Text>
|
||||
</span>
|
||||
);
|
||||
|
||||
const handleFinish = async ({ employee_team_members = [], ...values }) => {
|
||||
const { normalizedTeamMembers, errorKey } = validateEmployeeTeamMembers(employee_team_members);
|
||||
@@ -263,7 +218,42 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
||||
<Skeleton active title={false} paragraph={{ rows: 12 }} />
|
||||
) : (
|
||||
<Form onFinish={handleFinish} autoComplete={"off"} layout="vertical" form={form}>
|
||||
<LayoutFormRow title={t("employee_teams.labels.team_options")}>
|
||||
<LayoutFormRow
|
||||
title={
|
||||
<div
|
||||
style={{
|
||||
...INLINE_TITLE_ROW_STYLE,
|
||||
justifyContent: "space-between"
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
whiteSpace: "nowrap",
|
||||
fontWeight: 500,
|
||||
fontSize: "var(--ant-font-size-lg)",
|
||||
lineHeight: 1.2,
|
||||
marginRight: "auto"
|
||||
}}
|
||||
>
|
||||
{t("employee_teams.labels.team_options")}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
...INLINE_TITLE_GROUP_STYLE,
|
||||
flex: "0 0 auto",
|
||||
marginLeft: "auto"
|
||||
}}
|
||||
>
|
||||
<div aria-hidden style={INLINE_TITLE_SEPARATOR_STYLE} />
|
||||
<div style={INLINE_TITLE_LABEL_STYLE}>{t("employee_teams.fields.active")}</div>
|
||||
<Form.Item noStyle name="active" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
wrapTitle
|
||||
>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label={t("employee_teams.fields.name")}
|
||||
@@ -275,9 +265,6 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("employee_teams.fields.active")} name="active" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("employee_teams.fields.max_load")}
|
||||
name="max_load"
|
||||
@@ -287,7 +274,7 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} precision={1} />
|
||||
<InputNumber min={0} precision={1} suffix="%" />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow title={t("employee_teams.labels.members")}>
|
||||
@@ -296,8 +283,6 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
||||
return (
|
||||
<div>
|
||||
{fields.map((field, index) => {
|
||||
const teamMember = normalizeTeamMember(teamMembers[field.name]);
|
||||
|
||||
return (
|
||||
<Form.Item key={field.key} style={{ padding: 0, margin: 2 }}>
|
||||
<Form.Item name={[field.name, "id"]} hidden>
|
||||
@@ -305,7 +290,92 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
||||
</Form.Item>
|
||||
<LayoutFormRow
|
||||
grow
|
||||
title={getTeamMemberTitle(teamMember)}
|
||||
title={
|
||||
<div style={INLINE_TITLE_ROW_STYLE}>
|
||||
<HolderOutlined style={INLINE_TITLE_HANDLE_STYLE} />
|
||||
<div
|
||||
style={{
|
||||
...INLINE_TITLE_GROUP_STYLE,
|
||||
flex: "1 1 320px"
|
||||
}}
|
||||
>
|
||||
<div style={INLINE_TITLE_LABEL_STYLE}>{t("employee_teams.fields.employeeid")}</div>
|
||||
<Form.Item
|
||||
noStyle
|
||||
name={[field.name, "employeeid"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
}
|
||||
]}
|
||||
>
|
||||
<EmployeeSearchSelectComponent options={bodyshop.employees} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
<div aria-hidden style={INLINE_TITLE_SEPARATOR_STYLE} />
|
||||
<div
|
||||
style={{
|
||||
...INLINE_TITLE_GROUP_STYLE,
|
||||
flex: "0 1 170px"
|
||||
}}
|
||||
>
|
||||
<div style={INLINE_TITLE_LABEL_STYLE}>{t("employee_teams.fields.allocation")}</div>
|
||||
<Form.Item
|
||||
noStyle
|
||||
name={[field.name, "percentage"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
max={100}
|
||||
precision={2}
|
||||
size="small"
|
||||
aria-label={t("employee_teams.fields.allocation")}
|
||||
suffix="%"
|
||||
style={{
|
||||
...INLINE_TITLE_INPUT_STYLE,
|
||||
width: "100%"
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
<div aria-hidden style={INLINE_TITLE_SEPARATOR_STYLE} />
|
||||
<div
|
||||
style={{
|
||||
...INLINE_TITLE_GROUP_STYLE,
|
||||
flex: "0 1 260px"
|
||||
}}
|
||||
>
|
||||
<div style={INLINE_TITLE_LABEL_STYLE}>{t("employee_teams.fields.payout_method")}</div>
|
||||
<Form.Item
|
||||
noStyle
|
||||
key={`${index}-payout-method`}
|
||||
name={[field.name, "payout_method"]}
|
||||
initialValue="hourly"
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
aria-label={t("employee_teams.fields.payout_method")}
|
||||
size="small"
|
||||
options={payoutMethodOptions}
|
||||
style={{ width: "100%" }}
|
||||
styles={{
|
||||
selector: INLINE_TITLE_INPUT_STYLE
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
wrapTitle
|
||||
extra={
|
||||
<Space align="center" size="small">
|
||||
<Button
|
||||
@@ -325,50 +395,10 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<Row gutter={[16, 0]}>
|
||||
<Col {...TEAM_MEMBER_PRIMARY_FIELD_COLS.employee}>
|
||||
<Form.Item
|
||||
label={t("employee_teams.fields.employeeid")}
|
||||
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")}
|
||||
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"]]}>
|
||||
<Form.Item
|
||||
noStyle
|
||||
dependencies={[["employee_team_members", field.name, "payout_method"]]}
|
||||
>
|
||||
{() => {
|
||||
const payoutMethod =
|
||||
form.getFieldValue(["employee_team_members", field.name, "payout_method"]) ||
|
||||
@@ -378,7 +408,10 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
||||
return (
|
||||
<Row gutter={[16, 0]}>
|
||||
{LABOR_TYPES.map((laborType) => (
|
||||
<Col {...TEAM_MEMBER_RATE_FIELD_COLS} key={`${index}-${fieldName}-${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]}
|
||||
@@ -389,9 +422,9 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
||||
]}
|
||||
>
|
||||
{payoutMethod === "commission" ? (
|
||||
<InputNumber min={0} max={100} precision={2} />
|
||||
<InputNumber min={0} max={100} precision={2} suffix="%" />
|
||||
) : (
|
||||
<CurrencyInput />
|
||||
<CurrencyInput prefix="$" />
|
||||
)}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
@@ -144,7 +144,7 @@ const addBaseTeamMember = ({ employeeId = "emp-1", percentage = 100, rate = 25 }
|
||||
fireEvent.change(screen.getByLabelText("Employee"), {
|
||||
target: { value: employeeId }
|
||||
});
|
||||
fireEvent.change(screen.getByRole("spinbutton", { name: "Allocation %" }), {
|
||||
fireEvent.change(screen.getByRole("spinbutton", { name: "Allocation" }), {
|
||||
target: { value: String(percentage) }
|
||||
});
|
||||
fillHourlyRates(rate);
|
||||
|
||||
Reference in New Issue
Block a user