IO-3624 Polish config empty states and admin cards
This commit is contained in:
@@ -13,6 +13,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
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 ConfigListEmptyState from "../layout-form-row/config-list-empty-state.component.jsx";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
import {
|
||||
INLINE_TITLE_GROUP_STYLE,
|
||||
@@ -20,7 +21,8 @@ import {
|
||||
INLINE_TITLE_INPUT_STYLE,
|
||||
INLINE_TITLE_LABEL_STYLE,
|
||||
INLINE_TITLE_ROW_STYLE,
|
||||
INLINE_TITLE_SEPARATOR_STYLE
|
||||
INLINE_TITLE_SEPARATOR_STYLE,
|
||||
INLINE_TITLE_SWITCH_GROUP_STYLE
|
||||
} from "../layout-form-row/inline-form-row-title.utils.js";
|
||||
|
||||
import {
|
||||
@@ -209,8 +211,8 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
||||
<Card
|
||||
title={isTeamHydrating ? undefined : teamCardTitle}
|
||||
extra={
|
||||
<Button type="primary" onClick={() => form.submit()} disabled={isTeamHydrating}>
|
||||
{t("general.actions.save")}
|
||||
<Button type="primary" onClick={() => form.submit()} disabled={isTeamHydrating} style={{ minWidth: 190 }}>
|
||||
{t("employee_teams.actions.save_team")}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
@@ -239,8 +241,7 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
...INLINE_TITLE_GROUP_STYLE,
|
||||
flex: "0 0 auto",
|
||||
...INLINE_TITLE_SWITCH_GROUP_STYLE,
|
||||
marginLeft: "auto"
|
||||
}}
|
||||
>
|
||||
@@ -277,188 +278,181 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
||||
<InputNumber min={0} precision={1} suffix="%" />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow title={t("employee_teams.labels.members")}>
|
||||
<Form.List name={["employee_team_members"]}>
|
||||
{(fields, { add, remove, move }) => {
|
||||
return (
|
||||
<Form.List name={["employee_team_members"]}>
|
||||
{(fields, { add, remove, move }) => {
|
||||
return (
|
||||
<LayoutFormRow
|
||||
title={t("employee_teams.labels.members")}
|
||||
actions={[
|
||||
<Button
|
||||
key="add-team-member"
|
||||
type="primary"
|
||||
block
|
||||
onClick={() => {
|
||||
add({
|
||||
percentage: 0,
|
||||
payout_method: "hourly",
|
||||
labor_rates: {},
|
||||
commission_rates: {}
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("employee_teams.actions.newmember")}
|
||||
</Button>
|
||||
]}
|
||||
>
|
||||
<div>
|
||||
{fields.map((field, index) => {
|
||||
return (
|
||||
<Form.Item key={field.key} style={{ padding: 0, margin: 2 }}>
|
||||
<Form.Item name={[field.name, "id"]} hidden>
|
||||
<Input type="hidden" />
|
||||
</Form.Item>
|
||||
<LayoutFormRow
|
||||
grow
|
||||
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>
|
||||
{fields.length === 0 ? (
|
||||
<ConfigListEmptyState actionLabel={t("employee_teams.actions.newmember")} />
|
||||
) : (
|
||||
fields.map((field, index) => {
|
||||
return (
|
||||
<Form.Item key={field.key} style={{ padding: 0, margin: 2 }}>
|
||||
<Form.Item name={[field.name, "id"]} hidden>
|
||||
<Input type="hidden" />
|
||||
</Form.Item>
|
||||
<LayoutFormRow
|
||||
grow
|
||||
title={
|
||||
<div style={INLINE_TITLE_ROW_STYLE}>
|
||||
<HolderOutlined style={INLINE_TITLE_HANDLE_STYLE} />
|
||||
<div style={INLINE_TITLE_GROUP_STYLE}>
|
||||
<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}>
|
||||
<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}>
|
||||
<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>
|
||||
<div aria-hidden style={INLINE_TITLE_SEPARATOR_STYLE} />
|
||||
<div
|
||||
style={{
|
||||
...INLINE_TITLE_GROUP_STYLE,
|
||||
flex: "0 1 170px"
|
||||
}}
|
||||
}
|
||||
wrapTitle
|
||||
extra={
|
||||
<Space align="center" size="small">
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
icon={<DeleteFilled />}
|
||||
onClick={() => {
|
||||
remove(field.name);
|
||||
}}
|
||||
/>
|
||||
<FormListMoveArrows
|
||||
move={move}
|
||||
index={index}
|
||||
total={fields.length}
|
||||
orientation="horizontal"
|
||||
/>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<Form.Item
|
||||
noStyle
|
||||
dependencies={[["employee_team_members", field.name, "payout_method"]]}
|
||||
>
|
||||
<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
|
||||
type="text"
|
||||
icon={<DeleteFilled />}
|
||||
onClick={() => {
|
||||
remove(field.name);
|
||||
}}
|
||||
/>
|
||||
<FormListMoveArrows
|
||||
move={move}
|
||||
index={index}
|
||||
total={fields.length}
|
||||
orientation="horizontal"
|
||||
/>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<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";
|
||||
{() => {
|
||||
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
|
||||
}
|
||||
]}
|
||||
return (
|
||||
<Row gutter={[16, 0]}>
|
||||
{LABOR_TYPES.map((laborType) => (
|
||||
<Col
|
||||
{...TEAM_MEMBER_RATE_FIELD_COLS}
|
||||
key={`${index}-${fieldName}-${laborType}`}
|
||||
>
|
||||
{payoutMethod === "commission" ? (
|
||||
<InputNumber min={0} max={100} precision={2} suffix="%" />
|
||||
) : (
|
||||
<CurrencyInput prefix="$" />
|
||||
)}
|
||||
</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
|
||||
label={t(`joblines.fields.lbr_types.${laborType}`)}
|
||||
name={[field.name, fieldName, laborType]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
}
|
||||
]}
|
||||
>
|
||||
{payoutMethod === "commission" ? (
|
||||
<InputNumber min={0} max={100} precision={2} suffix="%" />
|
||||
) : (
|
||||
<CurrencyInput prefix="$" />
|
||||
)}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
</div>
|
||||
</LayoutFormRow>
|
||||
</Form.Item>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Form.List>
|
||||
</LayoutFormRow>
|
||||
</LayoutFormRow>
|
||||
);
|
||||
}}
|
||||
</Form.List>
|
||||
</Form>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
@@ -42,9 +42,11 @@ vi.mock("react-i18next", () => ({
|
||||
"employee_teams.options.commission": "Commission",
|
||||
"employee_teams.options.commission_percentage": "Commission",
|
||||
"employee_teams.actions.newmember": "New Team Member",
|
||||
"employee_teams.actions.save_team": "Save Employee Team",
|
||||
"employee_teams.errors.minimum_one_member": "Add at least one team member.",
|
||||
"employee_teams.errors.duplicate_member": "Team members must be unique.",
|
||||
"employee_teams.errors.allocation_total_exact": "Allocation must total exactly 100%.",
|
||||
"general.labels.click_to_begin": `Click ${values.action ?? ""} to begin`,
|
||||
"general.actions.save": "Save",
|
||||
"employees.successes.save": "Saved"
|
||||
};
|
||||
@@ -101,11 +103,12 @@ vi.mock("../form-items-formatted/currency-form-item.component", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../layout-form-row/layout-form-row.component", () => ({
|
||||
default: ({ title, extra, children }) => (
|
||||
default: ({ title, extra, actions, children }) => (
|
||||
<div>
|
||||
{title}
|
||||
{extra}
|
||||
{children}
|
||||
{actions}
|
||||
</div>
|
||||
)
|
||||
}));
|
||||
@@ -211,7 +214,7 @@ describe("ShopEmployeeTeamsFormComponent", () => {
|
||||
rate: 27.5
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Save" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Save Employee Team" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(insertEmployeeTeamMock).toHaveBeenCalledWith({
|
||||
|
||||
@@ -2,6 +2,8 @@ import { Button } from "antd";
|
||||
import queryString from "query-string";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import ConfigListEmptyState from "../layout-form-row/config-list-empty-state.component.jsx";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
import ResponsiveTable from "../responsive-table/responsive-table.component";
|
||||
|
||||
export default function ShopEmployeeTeamsListComponent({ loading, employee_teams }) {
|
||||
@@ -9,13 +11,28 @@ export default function ShopEmployeeTeamsListComponent({ loading, employee_teams
|
||||
const history = useNavigate();
|
||||
const search = queryString.parse(useLocation().search);
|
||||
|
||||
const navigateToTeam = (employeeTeamId) => {
|
||||
history({
|
||||
search: queryString.stringify({
|
||||
...search,
|
||||
employeeTeamId
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
const clearTeamSelection = () => {
|
||||
const { employeeTeamId, ...nextSearch } = search;
|
||||
void employeeTeamId;
|
||||
history({
|
||||
search: queryString.stringify(nextSearch)
|
||||
});
|
||||
};
|
||||
|
||||
const handleOnRowClick = (record) => {
|
||||
if (record) {
|
||||
search.employeeTeamId = record.id;
|
||||
history({ search: queryString.stringify(search) });
|
||||
navigateToTeam(record.id);
|
||||
} else {
|
||||
delete search.employeeTeamId;
|
||||
history({ search: queryString.stringify(search) });
|
||||
clearTeamSelection();
|
||||
}
|
||||
};
|
||||
const columns = [
|
||||
@@ -27,43 +44,38 @@ export default function ShopEmployeeTeamsListComponent({ loading, employee_teams
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ResponsiveTable
|
||||
title={() => {
|
||||
return (
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
search.employeeTeamId = "new";
|
||||
history({ search: queryString.stringify(search) });
|
||||
}}
|
||||
>
|
||||
{t("employee_teams.actions.new")}
|
||||
</Button>
|
||||
);
|
||||
}}
|
||||
loading={loading}
|
||||
pagination={{ placement: "top" }}
|
||||
columns={columns}
|
||||
mobileColumnKeys={["name"]}
|
||||
rowKey="id"
|
||||
dataSource={employee_teams}
|
||||
rowSelection={{
|
||||
onSelect: (props) => {
|
||||
search.employeeTeamId = props.id;
|
||||
history({ search: queryString.stringify(search) });
|
||||
},
|
||||
type: "radio",
|
||||
selectedRowKeys: [search.employeeTeamId]
|
||||
}}
|
||||
onRow={(record) => {
|
||||
return {
|
||||
onClick: () => {
|
||||
handleOnRowClick(record);
|
||||
}
|
||||
};
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<LayoutFormRow
|
||||
title={t("bodyshop.labels.employee_teams")}
|
||||
actions={[
|
||||
<Button key="new-team" type="primary" block onClick={() => navigateToTeam("new")}>
|
||||
{t("employee_teams.actions.new")}
|
||||
</Button>
|
||||
]}
|
||||
>
|
||||
{employee_teams.length === 0 ? (
|
||||
<ConfigListEmptyState actionLabel={t("employee_teams.actions.new")} />
|
||||
) : (
|
||||
<ResponsiveTable
|
||||
loading={loading}
|
||||
pagination={{ placement: "top" }}
|
||||
columns={columns}
|
||||
mobileColumnKeys={["name"]}
|
||||
rowKey="id"
|
||||
dataSource={employee_teams}
|
||||
rowSelection={{
|
||||
onSelect: (props) => navigateToTeam(props.id),
|
||||
type: "radio",
|
||||
selectedRowKeys: [search.employeeTeamId]
|
||||
}}
|
||||
onRow={(record) => {
|
||||
return {
|
||||
onClick: () => {
|
||||
handleOnRowClick(record);
|
||||
}
|
||||
};
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</LayoutFormRow>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user