IO-3624 Polish employee and team config layouts
This commit is contained in:
@@ -8,7 +8,7 @@ import { INSERT_VACATION } from "../../graphql/employees.queries";
|
||||
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
|
||||
export default function ShopEmployeeAddVacation({ employee }) {
|
||||
export default function ShopEmployeeAddVacation({ employee, buttonProps }) {
|
||||
const { t } = useTranslation();
|
||||
const [insertVacation] = useMutation(INSERT_VACATION);
|
||||
|
||||
@@ -117,7 +117,7 @@ export default function ShopEmployeeAddVacation({ employee }) {
|
||||
|
||||
return (
|
||||
<Popover content={overlay} open={visibility}>
|
||||
<Button loading={loading} disabled={!employee?.active} onClick={handleClick}>
|
||||
<Button loading={loading} disabled={!employee?.active} onClick={handleClick} {...buttonProps}>
|
||||
{t("employees.actions.addvacation")}
|
||||
</Button>
|
||||
</Popover>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DeleteFilled } from "@ant-design/icons";
|
||||
import { useApolloClient, useMutation, useQuery } from "@apollo/client/react";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { Button, Card, Form, Input, InputNumber, Select, Space, Switch } from "antd";
|
||||
import { Button, Card, Col, Form, Input, InputNumber, Row, Select, Space, Switch } from "antd";
|
||||
import ResponsiveTable from "../responsive-table/responsive-table.component";
|
||||
import { useForm } from "antd/es/form/Form";
|
||||
import queryString from "query-string";
|
||||
@@ -30,6 +30,7 @@ import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.c
|
||||
import { getFormListItemTitle } from "../form-list-move-arrows/form-list-item-title.utils";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
import ShopEmployeeAddVacation from "./shop-employees-add-vacation.component";
|
||||
import FormItemEmail from "../form-items-formatted/email-form-item.component.jsx";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
@@ -42,6 +43,14 @@ export function ShopEmployeesFormComponent({ bodyshop }) {
|
||||
const { t } = useTranslation();
|
||||
const [form] = useForm();
|
||||
const employeeRates = Form.useWatch(["rates"], form) || [];
|
||||
const employeeOptionsColProps = {
|
||||
xs: 24,
|
||||
sm: 12,
|
||||
md: 8,
|
||||
lg: { flex: "0 0 320px" },
|
||||
xl: { flex: "0 0 280px" },
|
||||
xxl: { flex: "0 0 380px" }
|
||||
};
|
||||
const history = useNavigate();
|
||||
const search = queryString.parse(useLocation().search);
|
||||
const [deleteVacation] = useMutation(DELETE_VACATION);
|
||||
@@ -177,237 +186,280 @@ export function ShopEmployeesFormComponent({ bodyshop }) {
|
||||
}
|
||||
>
|
||||
<Form onFinish={handleFinish} autoComplete={"off"} layout="vertical" form={form}>
|
||||
<LayoutFormRow>
|
||||
<Form.Item
|
||||
name="first_name"
|
||||
label={t("employees.fields.first_name")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("employees.fields.last_name")}
|
||||
name="last_name"
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="employee_number"
|
||||
label={t("employees.fields.employee_number")}
|
||||
validateTrigger="onBlur"
|
||||
hasFeedback
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
() => ({
|
||||
async validator(rule, value) {
|
||||
if (value) {
|
||||
const response = await client.query({
|
||||
query: CHECK_EMPLOYEE_NUMBER,
|
||||
variables: {
|
||||
employeenumber: value
|
||||
}
|
||||
});
|
||||
|
||||
if (response.data.employees_aggregate.aggregate.count === 0) {
|
||||
return Promise.resolve();
|
||||
} else if (
|
||||
response.data.employees_aggregate.nodes.length === 1 &&
|
||||
response.data.employees_aggregate.nodes[0].id === form.getFieldValue("id")
|
||||
) {
|
||||
return Promise.resolve();
|
||||
<LayoutFormRow title={t("bodyshop.labels.employee_options")}>
|
||||
<div style={{ display: "grid", rowGap: 16 }}>
|
||||
<Row gutter={[16, 16]} wrap>
|
||||
<Col {...employeeOptionsColProps}>
|
||||
<Form.Item
|
||||
name="first_name"
|
||||
label={t("employees.fields.first_name")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
return Promise.reject(t("employees.validation.unique_employee_number"));
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
})
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("employees.fields.pin")}
|
||||
name="pin"
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow>
|
||||
<Form.Item label={t("employees.fields.active")} valuePropName="checked" name="active">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("employees.fields.flat_rate")} name="flat_rate" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="hire_date"
|
||||
label={t("employees.fields.hire_date")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<DateTimePicker isDateOnly />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("employees.fields.termination_date")} name="termination_date">
|
||||
<DateTimePicker isDateOnly />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("employees.fields.user_email")}
|
||||
name="user_email"
|
||||
validateTrigger="onBlur"
|
||||
rules={[
|
||||
({ getFieldValue }) => ({
|
||||
async validator(rule, value) {
|
||||
const user_email = getFieldValue("user_email");
|
||||
|
||||
if (user_email && value) {
|
||||
const response = await client.query({
|
||||
query: QUERY_USERS_BY_EMAIL,
|
||||
variables: {
|
||||
email: user_email
|
||||
}
|
||||
});
|
||||
|
||||
if (response.data.users.length === 1) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(t("bodyshop.validation.useremailmustexist"));
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
})
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("employees.fields.external_id")} name="external_id">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<Form.List name={["rates"]}>
|
||||
{(fields, { add, remove, move }) => {
|
||||
return (
|
||||
<div>
|
||||
{fields.map((field, index) => {
|
||||
const employeeRate = employeeRates[field.name] || {};
|
||||
|
||||
return (
|
||||
<Form.Item key={field.key} style={{ padding: 0, margin: 2 }}>
|
||||
<LayoutFormRow
|
||||
grow
|
||||
title={getFormListItemTitle(t("employees.fields.cost_center"), index, employeeRate.cost_center)}
|
||||
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>
|
||||
}
|
||||
>
|
||||
<Form.Item
|
||||
label={t("employees.fields.cost_center")}
|
||||
key={`${index}`}
|
||||
name={[field.name, "cost_center"]}
|
||||
valuePropName="value"
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
options={[
|
||||
{ value: "timetickets.labels.shift", label: t("timetickets.labels.shift") },
|
||||
...(bodyshop.cdk_dealerid ||
|
||||
bodyshop.pbs_serialnumber ||
|
||||
bodyshop.rr_dealerid ||
|
||||
Enhanced_Payroll.treatment === "on"
|
||||
? CiecaSelect(false, true)
|
||||
: bodyshop.md_responsibility_centers.costs.map((c) => ({
|
||||
value: c.name,
|
||||
label: c.name
|
||||
})))
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("employees.fields.rate")}
|
||||
key={`${index}`}
|
||||
name={[field.name, "rate"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} precision={2} />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
</Form.Item>
|
||||
);
|
||||
})}
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="dashed"
|
||||
onClick={() => {
|
||||
add();
|
||||
}}
|
||||
style={{ width: "100%" }}
|
||||
id="add-employee-rate-button"
|
||||
>
|
||||
<span id="new-employee-rate">{t("employees.actions.newrate")}</span>
|
||||
</Button>
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Form.List>
|
||||
</Col>
|
||||
<Col {...employeeOptionsColProps}>
|
||||
<Form.Item
|
||||
label={t("employees.fields.last_name")}
|
||||
name="last_name"
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col {...employeeOptionsColProps}>
|
||||
<Form.Item
|
||||
name="employee_number"
|
||||
label={t("employees.fields.employee_number")}
|
||||
validateTrigger="onBlur"
|
||||
hasFeedback
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
() => ({
|
||||
async validator(rule, value) {
|
||||
if (value) {
|
||||
const response = await client.query({
|
||||
query: CHECK_EMPLOYEE_NUMBER,
|
||||
variables: {
|
||||
employeenumber: value
|
||||
}
|
||||
});
|
||||
|
||||
if (response.data.employees_aggregate.aggregate.count === 0) {
|
||||
return Promise.resolve();
|
||||
} else if (
|
||||
response.data.employees_aggregate.nodes.length === 1 &&
|
||||
response.data.employees_aggregate.nodes[0].id === form.getFieldValue("id")
|
||||
) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(t("employees.validation.unique_employee_number"));
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
})
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col {...employeeOptionsColProps}>
|
||||
<Form.Item
|
||||
label={t("employees.fields.pin")}
|
||||
name="pin"
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={[16, 16]} wrap>
|
||||
<Col {...employeeOptionsColProps}>
|
||||
<Form.Item label={t("employees.fields.active")} valuePropName="checked" name="active">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col {...employeeOptionsColProps}>
|
||||
<Form.Item label={t("employees.fields.flat_rate")} name="flat_rate" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col {...employeeOptionsColProps}>
|
||||
<Form.Item
|
||||
name="hire_date"
|
||||
label={t("employees.fields.hire_date")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<DateTimePicker isDateOnly />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col {...employeeOptionsColProps}>
|
||||
<Form.Item label={t("employees.fields.termination_date")} name="termination_date">
|
||||
<DateTimePicker isDateOnly />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col {...employeeOptionsColProps}>
|
||||
<Form.Item
|
||||
label={t("employees.fields.user_email")}
|
||||
name="user_email"
|
||||
validateTrigger="onBlur"
|
||||
rules={[
|
||||
({ getFieldValue }) => ({
|
||||
async validator(rule, value) {
|
||||
const user_email = getFieldValue("user_email");
|
||||
|
||||
if (user_email && value) {
|
||||
const response = await client.query({
|
||||
query: QUERY_USERS_BY_EMAIL,
|
||||
variables: {
|
||||
email: user_email
|
||||
}
|
||||
});
|
||||
|
||||
if (response.data.users.length === 1) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(t("bodyshop.validation.useremailmustexist"));
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
})
|
||||
]}
|
||||
>
|
||||
<FormItemEmail />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col {...employeeOptionsColProps}>
|
||||
<Form.Item label={t("employees.fields.external_id")} name="external_id">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow title={t("bodyshop.labels.employee_rates")}>
|
||||
<Form.List name={["rates"]}>
|
||||
{(fields, { add, remove, move }) => {
|
||||
return (
|
||||
<div>
|
||||
{fields.map((field, index) => {
|
||||
const employeeRate = employeeRates[field.name] || {};
|
||||
|
||||
return (
|
||||
<Form.Item key={field.key} style={{ padding: 0, margin: 2 }}>
|
||||
<LayoutFormRow
|
||||
grow
|
||||
title={getFormListItemTitle(
|
||||
t("employees.fields.cost_center"),
|
||||
index,
|
||||
employeeRate.cost_center
|
||||
)}
|
||||
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>
|
||||
}
|
||||
>
|
||||
<Form.Item
|
||||
label={t("employees.fields.cost_center")}
|
||||
key={`${index}`}
|
||||
name={[field.name, "cost_center"]}
|
||||
valuePropName="value"
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
options={[
|
||||
{ value: "timetickets.labels.shift", label: t("timetickets.labels.shift") },
|
||||
...(bodyshop.cdk_dealerid ||
|
||||
bodyshop.pbs_serialnumber ||
|
||||
bodyshop.rr_dealerid ||
|
||||
Enhanced_Payroll.treatment === "on"
|
||||
? CiecaSelect(false, true)
|
||||
: bodyshop.md_responsibility_centers.costs.map((c) => ({
|
||||
value: c.name,
|
||||
label: c.name
|
||||
})))
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("employees.fields.rate")}
|
||||
key={`${index}`}
|
||||
name={[field.name, "rate"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} precision={2} />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
</Form.Item>
|
||||
);
|
||||
})}
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="dashed"
|
||||
onClick={() => {
|
||||
add();
|
||||
}}
|
||||
style={{ width: "100%" }}
|
||||
id="add-employee-rate-button"
|
||||
>
|
||||
<span id="new-employee-rate">{t("employees.actions.newrate")}</span>
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Form.List>
|
||||
</LayoutFormRow>
|
||||
</Form>
|
||||
|
||||
<ResponsiveTable
|
||||
title={() => <ShopEmployeeAddVacation employee={data && data.employees_by_pk} />}
|
||||
columns={columns}
|
||||
mobileColumnKeys={["start", "length", "actions"]}
|
||||
rowKey={"id"}
|
||||
dataSource={data?.employees_by_pk?.employee_vacations ?? []}
|
||||
/>
|
||||
<LayoutFormRow title={t("bodyshop.labels.employee_vacation")}>
|
||||
<div>
|
||||
<ResponsiveTable
|
||||
columns={columns}
|
||||
mobileColumnKeys={["start", "length", "actions"]}
|
||||
rowKey={"id"}
|
||||
dataSource={data?.employees_by_pk?.employee_vacations ?? []}
|
||||
pagination={false}
|
||||
/>
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<ShopEmployeeAddVacation
|
||||
employee={data && data.employees_by_pk}
|
||||
buttonProps={{
|
||||
type: "dashed",
|
||||
block: true
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</LayoutFormRow>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ export default function ShopEmployeesListComponent({ loading, employees }) {
|
||||
};
|
||||
const columns = [
|
||||
{
|
||||
title: t("employees.fields.employee_number"),
|
||||
title: t("employees.labels.employee_number_short"),
|
||||
dataIndex: "employee_number",
|
||||
key: "employee_number",
|
||||
sorter: (a, b) => alphaSort(a.employee_number, b.employee_number),
|
||||
|
||||
@@ -1,29 +1,47 @@
|
||||
import { useQuery } from "@apollo/client/react";
|
||||
import queryString from "query-string";
|
||||
import { connect } from "react-redux";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { QUERY_EMPLOYEES } from "../../graphql/employees.queries";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import ShopEmployeesFormComponent from "./shop-employees-form.component";
|
||||
import ShopEmployeesListComponent from "./shop-employees-list.component";
|
||||
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
|
||||
import "./shop-employees.styles.scss";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({});
|
||||
|
||||
function ShopEmployeesContainer() {
|
||||
const search = queryString.parse(useLocation().search);
|
||||
const { loading, error, data } = useQuery(QUERY_EMPLOYEES, {
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only"
|
||||
});
|
||||
const hasSelectedEmployee = Boolean(search.employeeId);
|
||||
|
||||
if (error) return <AlertComponent title={error.message} type="error" />;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<RbacWrapper action="employees:page">
|
||||
<ShopEmployeesListComponent employees={data ? data.employees : []} loading={loading} />
|
||||
<ShopEmployeesFormComponent />
|
||||
</RbacWrapper>
|
||||
</div>
|
||||
<RbacWrapper action="employees:page">
|
||||
<div
|
||||
className={[
|
||||
"shop-employees-layout",
|
||||
hasSelectedEmployee ? "shop-employees-layout--with-detail" : null
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ")}
|
||||
>
|
||||
<div className="shop-employees-layout__list">
|
||||
<ShopEmployeesListComponent employees={data ? data.employees : []} loading={loading} />
|
||||
</div>
|
||||
{hasSelectedEmployee ? (
|
||||
<div className="shop-employees-layout__details">
|
||||
<ShopEmployeesFormComponent />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</RbacWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
.shop-employees-layout {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.shop-employees-layout__list,
|
||||
.shop-employees-layout__details {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 1700px) {
|
||||
.shop-employees-layout--with-detail {
|
||||
grid-template-columns: minmax(420px, 500px) minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Card, Typography } from "antd";
|
||||
import { Card } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
@@ -15,9 +15,8 @@ function ShopInfoConsentComponent({ bodyshop }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Typography.Title level={4}>{t("settings.title")}</Typography.Title>
|
||||
{<PhoneNumberConsentList bodyshop={bodyshop} />}
|
||||
<Card title={t("settings.title")}>
|
||||
<PhoneNumberConsentList bodyshop={bodyshop} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -132,9 +132,22 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
||||
const teamName = Form.useWatch("name", form);
|
||||
const teamMembers = Form.useWatch(["employee_team_members"], form) || [];
|
||||
const isTeamHydrating = !isNewTeam && Boolean(search.employeeTeamId) && hydratedTeamId !== search.employeeTeamId;
|
||||
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")
|
||||
: teamName?.trim() || 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 =
|
||||
@@ -241,7 +254,7 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
||||
<Skeleton active title={false} paragraph={{ rows: 12 }} />
|
||||
) : (
|
||||
<Form onFinish={handleFinish} autoComplete={"off"} layout="vertical" form={form}>
|
||||
<LayoutFormRow>
|
||||
<LayoutFormRow title={t("employee_teams.labels.team_options")}>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label={t("employee_teams.fields.name")}
|
||||
@@ -268,156 +281,144 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
||||
<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);
|
||||
<LayoutFormRow title={t("employee_teams.labels.members")}>
|
||||
<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.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>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Form.List>
|
||||
</LayoutFormRow>
|
||||
</Form>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
@@ -1,36 +1,44 @@
|
||||
import { useQuery } from "@apollo/client/react";
|
||||
import queryString from "query-string";
|
||||
import { connect } from "react-redux";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { QUERY_TEAMS } from "../../graphql/employee_teams.queries";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
|
||||
import ShopEmployeeTeamsListComponent from "./shop-employee-teams.list";
|
||||
import ShopEmployeeTeamsFormComponent from "./shop-employee-teams.form.component";
|
||||
import { Col, Row } from "antd";
|
||||
import "./shop-teams.styles.scss";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({});
|
||||
|
||||
function ShopTeamsContainer() {
|
||||
const search = queryString.parse(useLocation().search);
|
||||
const { loading, error, data } = useQuery(QUERY_TEAMS, {
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only"
|
||||
});
|
||||
const hasSelectedTeam = Boolean(search.employeeTeamId);
|
||||
|
||||
if (error) return <AlertComponent title={error.message} type="error" />;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<RbacWrapper action="employee_teams:page">
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={6}>
|
||||
<ShopEmployeeTeamsListComponent employee_teams={data ? data.employee_teams : []} loading={loading} />
|
||||
</Col>
|
||||
<Col span={18}>
|
||||
<RbacWrapper action="employee_teams:page">
|
||||
<div
|
||||
className={["shop-teams-layout", hasSelectedTeam ? "shop-teams-layout--with-detail" : null]
|
||||
.filter(Boolean)
|
||||
.join(" ")}
|
||||
>
|
||||
<div className="shop-teams-layout__list">
|
||||
<ShopEmployeeTeamsListComponent employee_teams={data ? data.employee_teams : []} loading={loading} />
|
||||
</div>
|
||||
{hasSelectedTeam ? (
|
||||
<div className="shop-teams-layout__details">
|
||||
<ShopEmployeeTeamsFormComponent />
|
||||
</Col>
|
||||
</Row>
|
||||
</RbacWrapper>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</RbacWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
16
client/src/components/shop-teams/shop-teams.styles.scss
Normal file
16
client/src/components/shop-teams/shop-teams.styles.scss
Normal file
@@ -0,0 +1,16 @@
|
||||
.shop-teams-layout {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.shop-teams-layout__list,
|
||||
.shop-teams-layout__details {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 1700px) {
|
||||
.shop-teams-layout--with-detail {
|
||||
grid-template-columns: minmax(420px, 500px) minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
@@ -736,6 +736,9 @@
|
||||
},
|
||||
"emaillater": "Email Later",
|
||||
"employee_teams": "Employee Teams",
|
||||
"employee_options": "Employee Options",
|
||||
"employee_rates": "Employee Rates",
|
||||
"employee_vacation": "Employee Vacation",
|
||||
"employees": "Employees",
|
||||
"estimators": "Estimators",
|
||||
"filehandlers": "Adjusters",
|
||||
@@ -1202,7 +1205,9 @@
|
||||
"percentage": "Percent"
|
||||
},
|
||||
"labels": {
|
||||
"allocation_total": "Allocation Total: {{total}}%"
|
||||
"allocation_total": "Allocation Total: {{total}}%",
|
||||
"members": "Members",
|
||||
"team_options": "Team Options"
|
||||
},
|
||||
"options": {
|
||||
"commission": "Commission",
|
||||
@@ -1246,6 +1251,7 @@
|
||||
"labels": {
|
||||
"actions": "Actions",
|
||||
"active": "Active",
|
||||
"employee_number_short": "Employee #",
|
||||
"endmustbeafterstart": "End date must be after start date.",
|
||||
"flat_rate": "Flat Rate",
|
||||
"inactive": "Inactive",
|
||||
|
||||
@@ -736,6 +736,9 @@
|
||||
},
|
||||
"emaillater": "",
|
||||
"employee_teams": "",
|
||||
"employee_options": "",
|
||||
"employee_rates": "",
|
||||
"employee_vacation": "",
|
||||
"employees": "",
|
||||
"estimators": "",
|
||||
"filehandlers": "",
|
||||
@@ -1202,7 +1205,9 @@
|
||||
"percentage": ""
|
||||
},
|
||||
"labels": {
|
||||
"allocation_total": ""
|
||||
"allocation_total": "",
|
||||
"members": "",
|
||||
"team_options": ""
|
||||
},
|
||||
"options": {
|
||||
"commission": "",
|
||||
@@ -1246,6 +1251,7 @@
|
||||
"labels": {
|
||||
"actions": "",
|
||||
"active": "",
|
||||
"employee_number_short": "",
|
||||
"endmustbeafterstart": "",
|
||||
"flat_rate": "",
|
||||
"inactive": "",
|
||||
|
||||
@@ -736,6 +736,9 @@
|
||||
},
|
||||
"emaillater": "",
|
||||
"employee_teams": "",
|
||||
"employee_options": "",
|
||||
"employee_rates": "",
|
||||
"employee_vacation": "",
|
||||
"employees": "",
|
||||
"estimators": "",
|
||||
"filehandlers": "",
|
||||
@@ -1202,7 +1205,9 @@
|
||||
"percentage": ""
|
||||
},
|
||||
"labels": {
|
||||
"allocation_total": ""
|
||||
"allocation_total": "",
|
||||
"members": "",
|
||||
"team_options": ""
|
||||
},
|
||||
"options": {
|
||||
"commission": "",
|
||||
@@ -1246,6 +1251,7 @@
|
||||
"labels": {
|
||||
"actions": "",
|
||||
"active": "",
|
||||
"employee_number_short": "",
|
||||
"endmustbeafterstart": "",
|
||||
"flat_rate": "",
|
||||
"inactive": "",
|
||||
|
||||
Reference in New Issue
Block a user