feature/IO-3587-Commision-Cut - Additional test, layout enhancements

This commit is contained in:
Dave
2026-03-17 11:03:14 -04:00
parent aebd8da4ae
commit 45688c0dde
3 changed files with 1501 additions and 1304 deletions

View File

@@ -1,9 +1,23 @@
import { DeleteFilled } from "@ant-design/icons";
import { useMutation, useQuery } from "@apollo/client/react";
import { Button, Card, Col, Form, Input, InputNumber, Row, Select, Space, Switch, Tag, Typography } from "antd";
import {
Button,
Card,
Col,
Form,
Input,
InputNumber,
Row,
Select,
Skeleton,
Space,
Switch,
Tag,
Typography
} from "antd";
import querystring from "query-string";
import { useEffect } from "react";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useLocation, useNavigate } from "react-router-dom";
@@ -85,21 +99,40 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
const history = useNavigate();
const search = querystring.parse(useLocation().search);
const notification = useNotification();
const [hydratedTeamId, setHydratedTeamId] = useState(search.employeeTeamId === "new" ? "new" : null);
const isNewTeam = search.employeeTeamId === "new";
const { error, data } = useQuery(QUERY_EMPLOYEE_TEAM_BY_ID, {
const { error, data, loading } = useQuery(QUERY_EMPLOYEE_TEAM_BY_ID, {
variables: { id: search.employeeTeamId },
skip: !search.employeeTeamId || search.employeeTeamId === "new",
skip: !search.employeeTeamId || isNewTeam,
fetchPolicy: "network-only",
nextFetchPolicy: "network-only"
nextFetchPolicy: "network-only",
notifyOnNetworkStatusChange: true
});
useEffect(() => {
if (data?.employee_teams_by_pk) {
if (!search.employeeTeamId) return;
if (isNewTeam) {
form.resetFields();
setHydratedTeamId("new");
return;
}
setHydratedTeamId(null);
}, [form, isNewTeam, search.employeeTeamId]);
useEffect(() => {
if (!search.employeeTeamId || isNewTeam || loading) return;
if (data?.employee_teams_by_pk?.id === search.employeeTeamId) {
form.setFieldsValue(normalizeEmployeeTeam(data.employee_teams_by_pk));
setHydratedTeamId(search.employeeTeamId);
} else {
form.resetFields();
setHydratedTeamId(search.employeeTeamId);
}
}, [form, data, search.employeeTeamId]);
}, [data, form, isNewTeam, loading, search.employeeTeamId]);
const [updateEmployeeTeam] = useMutation(UPDATE_EMPLOYEE_TEAM);
const [insertEmployeeTeam] = useMutation(INSERT_EMPLOYEE_TEAM);
@@ -109,7 +142,10 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
}));
const teamName = Form.useWatch("name", form);
const teamMembers = Form.useWatch(["employee_team_members"], form) || [];
const teamCardTitle = teamName?.trim() || t("employee_teams.fields.name");
const isTeamHydrating = !isNewTeam && Boolean(search.employeeTeamId) && hydratedTeamId !== search.employeeTeamId;
const teamCardTitle = isTeamHydrating
? t("employee_teams.fields.name")
: teamName?.trim() || t("employee_teams.fields.name");
const getTeamMemberTitle = (teamMember = {}) => {
const employeeName =
@@ -123,10 +159,10 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
return (
<div style={{ display: "flex", flexWrap: "wrap", alignItems: "center", gap: 8 }}>
<Typography.Text strong>{employeeName}</Typography.Text>
<Tag bordered={false} color="geekblue">
<Tag variant="filled" color="geekblue">
{`${t("employee_teams.fields.allocation")}: ${allocation || "--"}`}
</Tag>
<Tag bordered={false} color={getPayoutMethodTagColor(teamMember.payout_method)}>
<Tag variant="filled" color={getPayoutMethodTagColor(teamMember.payout_method)}>
{payoutMethod}
</Tag>
</div>
@@ -228,191 +264,194 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
<Card
title={teamCardTitle}
extra={
<Button type="primary" onClick={() => form.submit()}>
<Button type="primary" onClick={() => form.submit()} disabled={isTeamHydrating}>
{t("general.actions.save")}
</Button>
}
>
<Form onFinish={handleFinish} autoComplete={"off"} layout="vertical" form={form}>
<LayoutFormRow>
<Form.Item
name="name"
label={t("employee_teams.fields.name")}
rules={[
{
required: true
}
]}
>
<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"
rules={[
{
required: true
}
]}
>
<InputNumber min={0} precision={1} />
</Form.Item>
</LayoutFormRow>
<Form.List name={["employee_team_members"]}>
{(fields, { add, remove, move }) => {
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 label={t("employees.fields.id")} key={`${index}`} name={[field.name, "id"]} hidden>
<Input type="hidden" />
</Form.Item>
<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>
}
>
<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({
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);
{isTeamHydrating ? (
<Skeleton active title={false} paragraph={{ rows: 12 }} />
) : (
<Form onFinish={handleFinish} autoComplete={"off"} layout="vertical" form={form}>
<LayoutFormRow>
<Form.Item
name="name"
label={t("employee_teams.fields.name")}
rules={[
{
required: true
}
]}
>
<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"
rules={[
{
required: true
}
]}
>
<InputNumber min={0} precision={1} />
</Form.Item>
</LayoutFormRow>
<Form.List name={["employee_team_members"]}>
{(fields, { add, remove, move }) => {
return (
<div>
{fields.map((field, index) => {
const teamMember = normalizeTeamMember(teamMembers[field.name]);
return (
<Typography.Text type={hasExactSplitTotal(teamMembers) ? undefined : "danger"}>
{t("employee_teams.labels.allocation_total", {
total: splitTotal.toFixed(2)
})}
</Typography.Text>
<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
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>
}
>
<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>
</div>
);
}}
</Form.List>
</Form>
})}
<Form.Item>
<Button
type="dashed"
onClick={() => {
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>
);
}}
</Form.List>
</Form>
)}
</Card>
);
}