594 lines
21 KiB
JavaScript
594 lines
21 KiB
JavaScript
import { DeleteFilled } from "@ant-design/icons";
|
|
import { useApolloClient, useMutation, useQuery } from "@apollo/client/react";
|
|
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
|
import { Button, Card, Col, Form, Input, InputNumber, Row, Select, Space, Switch } from "antd";
|
|
import ResponsiveTable from "../responsive-table/responsive-table.component";
|
|
import queryString from "query-string";
|
|
import { useCallback, useEffect, useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { connect } from "react-redux";
|
|
import { useLocation, useNavigate } from "react-router-dom";
|
|
import { createStructuredSelector } from "reselect";
|
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
|
import {
|
|
CHECK_EMPLOYEE_NUMBER,
|
|
DELETE_VACATION,
|
|
INSERT_EMPLOYEES,
|
|
QUERY_EMPLOYEE_BY_ID,
|
|
QUERY_USERS_BY_EMAIL,
|
|
UPDATE_EMPLOYEE
|
|
} from "../../graphql/employees.queries";
|
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
|
import CiecaSelect from "../../utils/Ciecaselect";
|
|
import { DateFormatter } from "../../utils/DateFormatter";
|
|
import dayjs from "../../utils/day";
|
|
import AlertComponent from "../alert/alert.component";
|
|
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
|
|
import FormsFieldChanged from "../form-fields-changed-alert/form-fields-changed-alert.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 InlineValidatedFormRow from "../layout-form-row/inline-validated-form-row.component.jsx";
|
|
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,
|
|
INLINE_TITLE_SWITCH_GROUP_STYLE,
|
|
INLINE_TITLE_TEXT_STYLE,
|
|
InlineTitleListIcon
|
|
} from "../layout-form-row/inline-form-row-title.utils.js";
|
|
import ShopEmployeeAddVacation from "./shop-employees-add-vacation.component";
|
|
import FormItemEmail from "../form-items-formatted/email-form-item.component.jsx";
|
|
|
|
const mapStateToProps = createStructuredSelector({
|
|
bodyshop: selectBodyshop
|
|
});
|
|
const mapDispatchToProps = () => ({
|
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
|
});
|
|
|
|
export function ShopEmployeesFormComponent({ bodyshop, form, onDirtyChange, isDirty }) {
|
|
const { t } = useTranslation();
|
|
const [internalIsDirty, setInternalIsDirty] = useState(false);
|
|
const resolvedIsDirty = typeof isDirty === "boolean" ? isDirty : internalIsDirty;
|
|
const employeeNumber = Form.useWatch("employee_number", form);
|
|
const firstName = Form.useWatch("first_name", form);
|
|
const lastName = Form.useWatch("last_name", form);
|
|
const employeeOptionsColProps = {
|
|
xs: 24,
|
|
sm: 12,
|
|
md: 12,
|
|
lg: 8,
|
|
xl: 8,
|
|
xxl: 8
|
|
};
|
|
const history = useNavigate();
|
|
const search = queryString.parse(useLocation().search);
|
|
const [deleteVacation] = useMutation(DELETE_VACATION);
|
|
const { error, data, refetch } = useQuery(QUERY_EMPLOYEE_BY_ID, {
|
|
variables: { id: search.employeeId },
|
|
skip: !search.employeeId || search.employeeId === "new",
|
|
fetchPolicy: "network-only",
|
|
nextFetchPolicy: "network-only"
|
|
});
|
|
const notification = useNotification();
|
|
const isNewEmployee = search.employeeId === "new";
|
|
const currentEmployeeData = data?.employees_by_pk?.id === search.employeeId ? data.employees_by_pk : null;
|
|
const employeeTitleName = [firstName, lastName].filter(Boolean).join(" ").trim();
|
|
const employeeCardTitle =
|
|
[employeeNumber, employeeTitleName].filter(Boolean).join(" - ") ||
|
|
(isNewEmployee ? t("employees.actions.new") : t("bodyshop.labels.employees"));
|
|
|
|
const {
|
|
treatments: { Enhanced_Payroll }
|
|
} = useTreatmentsWithConfig({
|
|
attributes: {},
|
|
names: ["Enhanced_Payroll"],
|
|
splitKey: bodyshop.imexshopid
|
|
});
|
|
|
|
const updateDirtyState = useCallback(
|
|
(nextDirtyState) => {
|
|
setInternalIsDirty(nextDirtyState);
|
|
onDirtyChange?.(nextDirtyState);
|
|
},
|
|
[onDirtyChange]
|
|
);
|
|
|
|
const client = useApolloClient();
|
|
const clearEmployeeFormMeta = useCallback(() => {
|
|
const fieldMeta = form.getFieldsError().map(({ name }) => ({
|
|
name,
|
|
touched: false,
|
|
validating: false,
|
|
errors: [],
|
|
warnings: []
|
|
}));
|
|
|
|
if (fieldMeta.length > 0) {
|
|
form.setFields(fieldMeta);
|
|
}
|
|
|
|
updateDirtyState(false);
|
|
}, [form, updateDirtyState]);
|
|
|
|
const resetEmployeeFormToCurrentData = useCallback(() => {
|
|
form.resetFields();
|
|
|
|
if (currentEmployeeData) {
|
|
form.setFieldsValue(currentEmployeeData);
|
|
}
|
|
|
|
window.requestAnimationFrame(() => {
|
|
clearEmployeeFormMeta();
|
|
});
|
|
}, [clearEmployeeFormMeta, currentEmployeeData, form]);
|
|
|
|
useEffect(() => {
|
|
resetEmployeeFormToCurrentData();
|
|
}, [resetEmployeeFormToCurrentData, search.employeeId]);
|
|
|
|
const [updateEmployee] = useMutation(UPDATE_EMPLOYEE);
|
|
const [insertEmployees] = useMutation(INSERT_EMPLOYEES);
|
|
|
|
const handleFinish = (values) => {
|
|
if (search.employeeId && search.employeeId !== "new") {
|
|
//Update a record.
|
|
logImEXEvent("shop_employee_update");
|
|
|
|
updateEmployee({
|
|
variables: {
|
|
id: search.employeeId,
|
|
employee: {
|
|
...values,
|
|
user_email: values.user_email === "" ? null : values.user_email
|
|
}
|
|
}
|
|
})
|
|
.then(() => {
|
|
updateDirtyState(false);
|
|
void refetch();
|
|
notification.success({
|
|
title: t("employees.successes.save")
|
|
});
|
|
})
|
|
.catch((error) => {
|
|
notification.error({
|
|
title: t("employees.errors.save", {
|
|
message: JSON.stringify(error)
|
|
})
|
|
});
|
|
});
|
|
} else {
|
|
//New record, insert it.
|
|
logImEXEvent("shop_employee_insert");
|
|
|
|
insertEmployees({
|
|
variables: { employees: [{ ...values, shopid: bodyshop.id }] },
|
|
refetchQueries: ["QUERY_EMPLOYEES"]
|
|
}).then((r) => {
|
|
updateDirtyState(false);
|
|
search.employeeId = r.data.insert_employees.returning[0].id;
|
|
history({ search: queryString.stringify(search) });
|
|
notification.success({
|
|
title: t("employees.successes.save")
|
|
});
|
|
});
|
|
}
|
|
};
|
|
|
|
if (!search.employeeId) return null;
|
|
if (error) return <AlertComponent title={error.message} type="error" />;
|
|
|
|
const columns = [
|
|
{
|
|
title: t("employees.fields.vacation.start"),
|
|
dataIndex: "start",
|
|
key: "start",
|
|
render: (text) => <DateFormatter>{text}</DateFormatter>
|
|
},
|
|
{
|
|
title: t("employees.fields.vacation.end"),
|
|
dataIndex: "end",
|
|
key: "end",
|
|
render: (text) => <DateFormatter>{text}</DateFormatter>
|
|
},
|
|
{
|
|
title: t("employees.fields.vacation.length"),
|
|
dataIndex: "length",
|
|
key: "length",
|
|
render: (text, record) => dayjs(record.end).diff(dayjs(record.start), "day", true).toFixed(1)
|
|
},
|
|
{
|
|
title: t("general.labels.actions"),
|
|
dataIndex: "actions",
|
|
key: "actions",
|
|
render: (text, record) => (
|
|
<Button
|
|
type="text"
|
|
danger
|
|
onClick={async () => {
|
|
await deleteVacation({
|
|
variables: { id: record.id },
|
|
update(cache) {
|
|
cache.modify({
|
|
id: cache.identify({
|
|
id: data.employees_by_pk.id,
|
|
__typename: "employees"
|
|
}),
|
|
|
|
fields: {
|
|
employee_vacations(ex, { readField }) {
|
|
return ex.filter((vacaref) => record.id !== readField("id", vacaref));
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}}
|
|
icon={<DeleteFilled />}
|
|
/>
|
|
)
|
|
}
|
|
];
|
|
|
|
return (
|
|
<Card
|
|
title={employeeCardTitle}
|
|
extra={
|
|
<Button type="primary" onClick={() => form.submit()} disabled={!resolvedIsDirty} style={{ minWidth: 170 }}>
|
|
{t("employees.actions.save_employee")}
|
|
</Button>
|
|
}
|
|
>
|
|
<Form
|
|
onFinish={handleFinish}
|
|
autoComplete={"off"}
|
|
layout="vertical"
|
|
form={form}
|
|
onValuesChange={() => {
|
|
updateDirtyState(form.isFieldsTouched());
|
|
}}
|
|
>
|
|
<FormsFieldChanged form={form} onReset={resetEmployeeFormToCurrentData} onDirtyChange={updateDirtyState} />
|
|
<LayoutFormRow
|
|
title={
|
|
<div
|
|
style={{
|
|
...INLINE_TITLE_ROW_STYLE,
|
|
justifyContent: "space-between"
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
...INLINE_TITLE_TEXT_STYLE,
|
|
marginRight: "auto"
|
|
}}
|
|
>
|
|
{t("bodyshop.labels.employee_options")}
|
|
</div>
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: 4,
|
|
flexWrap: "wrap",
|
|
marginLeft: "auto"
|
|
}}
|
|
>
|
|
<div aria-hidden style={INLINE_TITLE_SEPARATOR_STYLE} />
|
|
<div
|
|
style={{
|
|
...INLINE_TITLE_SWITCH_GROUP_STYLE
|
|
}}
|
|
>
|
|
<div style={INLINE_TITLE_LABEL_STYLE}>{t("employees.labels.active")}</div>
|
|
<Form.Item noStyle valuePropName="checked" name="active">
|
|
<Switch />
|
|
</Form.Item>
|
|
</div>
|
|
<div aria-hidden style={INLINE_TITLE_SEPARATOR_STYLE} />
|
|
<div
|
|
style={{
|
|
...INLINE_TITLE_SWITCH_GROUP_STYLE
|
|
}}
|
|
>
|
|
<div style={INLINE_TITLE_LABEL_STYLE}>{t("employees.fields.flat_rate")}</div>
|
|
<Form.Item noStyle valuePropName="checked" name="flat_rate">
|
|
<Switch />
|
|
</Form.Item>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
wrapTitle
|
|
>
|
|
<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"),
|
|
}
|
|
]}
|
|
>
|
|
<Input />
|
|
</Form.Item>
|
|
</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>
|
|
<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>
|
|
</LayoutFormRow>
|
|
<Form.List name={["rates"]}>
|
|
{(fields, { add, remove, move }) => {
|
|
return (
|
|
<LayoutFormRow
|
|
title={t("bodyshop.labels.employee_rates")}
|
|
actions={[
|
|
<Button
|
|
key="add-rate"
|
|
type="primary"
|
|
block
|
|
onClick={() => {
|
|
add();
|
|
}}
|
|
id="add-employee-rate-button"
|
|
>
|
|
<span id="new-employee-rate">{t("employees.actions.addrate")}</span>
|
|
</Button>
|
|
]}
|
|
>
|
|
<div>
|
|
{fields.length === 0 ? (
|
|
<ConfigListEmptyState actionLabel={t("employees.actions.addrate")} />
|
|
) : (
|
|
fields.map((field, index) => {
|
|
return (
|
|
<Form.Item noStyle key={field.key}>
|
|
<InlineValidatedFormRow
|
|
form={form}
|
|
errorNames={[["rates", field.name, "cost_center"]]}
|
|
noDivider
|
|
title={
|
|
<div style={INLINE_TITLE_ROW_STYLE}>
|
|
<InlineTitleListIcon style={INLINE_TITLE_HANDLE_STYLE} />
|
|
<div style={INLINE_TITLE_GROUP_STYLE}>
|
|
<div style={INLINE_TITLE_LABEL_STYLE}>{t("employees.fields.cost_center")}</div>
|
|
<Form.Item
|
|
noStyle
|
|
name={[field.name, "cost_center"]}
|
|
rules={[
|
|
{
|
|
required: true
|
|
}
|
|
]}
|
|
>
|
|
<Select
|
|
size="small"
|
|
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
|
|
})))
|
|
]}
|
|
style={{ width: "100%" }}
|
|
styles={{
|
|
selector: INLINE_TITLE_INPUT_STYLE
|
|
}}
|
|
/>
|
|
</Form.Item>
|
|
</div>
|
|
</div>
|
|
}
|
|
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>
|
|
}
|
|
>
|
|
<Form.Item
|
|
label={t("employees.fields.rate")}
|
|
name={[field.name, "rate"]}
|
|
rules={[
|
|
{
|
|
required: true
|
|
}
|
|
]}
|
|
style={{ marginBottom: 0 }}
|
|
>
|
|
<InputNumber min={0} precision={2} style={{ width: "100%" }} />
|
|
</Form.Item>
|
|
</InlineValidatedFormRow>
|
|
</Form.Item>
|
|
);
|
|
})
|
|
)}
|
|
</div>
|
|
</LayoutFormRow>
|
|
);
|
|
}}
|
|
</Form.List>
|
|
</Form>
|
|
|
|
<LayoutFormRow
|
|
title={t("bodyshop.labels.employee_vacation")}
|
|
actions={[
|
|
<ShopEmployeeAddVacation
|
|
key="add-vacation"
|
|
employee={data && data.employees_by_pk}
|
|
buttonProps={{
|
|
type: "primary",
|
|
block: true
|
|
}}
|
|
/>
|
|
]}
|
|
>
|
|
{(data?.employees_by_pk?.employee_vacations ?? []).length === 0 ? (
|
|
<ConfigListEmptyState actionLabel={t("employees.actions.addvacation")} />
|
|
) : (
|
|
<div>
|
|
<ResponsiveTable
|
|
columns={columns}
|
|
mobileColumnKeys={["start", "length", "actions"]}
|
|
rowKey={"id"}
|
|
dataSource={data?.employees_by_pk?.employee_vacations ?? []}
|
|
pagination={false}
|
|
/>
|
|
</div>
|
|
)}
|
|
</LayoutFormRow>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
export default connect(mapStateToProps, mapDispatchToProps)(ShopEmployeesFormComponent);
|