Compare commits
11 Commits
release/20
...
feature/IO
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f849ea9d0a | ||
|
|
de6038038a | ||
|
|
1f8836d9d8 | ||
|
|
a267d65425 | ||
|
|
cacda3805a | ||
|
|
af757ee71e | ||
|
|
eb666f2ca1 | ||
|
|
2b8990950b | ||
|
|
3f2e05befc | ||
|
|
06bfdeb449 | ||
|
|
1b2f9fc027 |
@@ -4,7 +4,7 @@ import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
|||||||
import { Button, Card, Col, Form, Input, InputNumber, Row, 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 ResponsiveTable from "../responsive-table/responsive-table.component";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
@@ -52,6 +52,7 @@ const mapDispatchToProps = () => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export function ShopEmployeesFormComponent({ bodyshop, form, onDirtyChange, isDirty }) {
|
export function ShopEmployeesFormComponent({ bodyshop, form, onDirtyChange, isDirty }) {
|
||||||
|
const submitActionRef = useRef("save");
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [internalIsDirty, setInternalIsDirty] = useState(false);
|
const [internalIsDirty, setInternalIsDirty] = useState(false);
|
||||||
const resolvedIsDirty = typeof isDirty === "boolean" ? isDirty : internalIsDirty;
|
const resolvedIsDirty = typeof isDirty === "boolean" ? isDirty : internalIsDirty;
|
||||||
@@ -128,55 +129,113 @@ export function ShopEmployeesFormComponent({ bodyshop, form, onDirtyChange, isDi
|
|||||||
});
|
});
|
||||||
}, [clearEmployeeFormMeta, currentEmployeeData, form]);
|
}, [clearEmployeeFormMeta, currentEmployeeData, form]);
|
||||||
|
|
||||||
|
const syncEmployeeFormToSavedData = useCallback(
|
||||||
|
(employeeData) => {
|
||||||
|
if (employeeData) {
|
||||||
|
form.setFieldsValue(employeeData);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
clearEmployeeFormMeta();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[clearEmployeeFormMeta, form]
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
resetEmployeeFormToCurrentData();
|
resetEmployeeFormToCurrentData();
|
||||||
}, [resetEmployeeFormToCurrentData, search.employeeId]);
|
}, [resetEmployeeFormToCurrentData, search.employeeId]);
|
||||||
|
|
||||||
const [updateEmployee] = useMutation(UPDATE_EMPLOYEE);
|
const [updateEmployee] = useMutation(UPDATE_EMPLOYEE);
|
||||||
const [insertEmployees] = useMutation(INSERT_EMPLOYEES);
|
const [insertEmployees] = useMutation(INSERT_EMPLOYEES);
|
||||||
|
const saveAndResetSubmitAction = useCallback(() => {
|
||||||
|
const submitAction = submitActionRef.current;
|
||||||
|
submitActionRef.current = "save";
|
||||||
|
return submitAction;
|
||||||
|
}, []);
|
||||||
|
const submitEmployeeForm = useCallback(
|
||||||
|
(submitAction = "save") => {
|
||||||
|
submitActionRef.current = submitAction;
|
||||||
|
form.submit();
|
||||||
|
},
|
||||||
|
[form]
|
||||||
|
);
|
||||||
|
const navigateToEmployee = useCallback(
|
||||||
|
(employeeId) => {
|
||||||
|
history({
|
||||||
|
search: queryString.stringify({
|
||||||
|
...search,
|
||||||
|
employeeId
|
||||||
|
})
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[history, search]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleFinish = async (values) => {
|
||||||
|
const submitAction = saveAndResetSubmitAction();
|
||||||
|
const normalizedValues = {
|
||||||
|
...values,
|
||||||
|
user_email: values.user_email === "" ? null : values.user_email
|
||||||
|
};
|
||||||
|
|
||||||
const handleFinish = (values) => {
|
|
||||||
if (search.employeeId && search.employeeId !== "new") {
|
if (search.employeeId && search.employeeId !== "new") {
|
||||||
//Update a record.
|
//Update a record.
|
||||||
logImEXEvent("shop_employee_update");
|
logImEXEvent("shop_employee_update");
|
||||||
|
|
||||||
updateEmployee({
|
try {
|
||||||
variables: {
|
const result = await updateEmployee({
|
||||||
id: search.employeeId,
|
variables: {
|
||||||
employee: {
|
id: search.employeeId,
|
||||||
...values,
|
employee: normalizedValues
|
||||||
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({
|
syncEmployeeFormToSavedData(result?.data?.update_employees?.returning?.[0] ?? normalizedValues);
|
||||||
variables: { employees: [{ ...values, shopid: bodyshop.id }] },
|
void refetch();
|
||||||
refetchQueries: ["QUERY_EMPLOYEES"]
|
if (submitAction === "saveAndNew") {
|
||||||
}).then((r) => {
|
navigateToEmployee("new");
|
||||||
updateDirtyState(false);
|
}
|
||||||
search.employeeId = r.data.insert_employees.returning[0].id;
|
|
||||||
history({ search: queryString.stringify(search) });
|
|
||||||
notification.success({
|
notification.success({
|
||||||
title: t("employees.successes.save")
|
title: t("employees.successes.save")
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
notification.error({
|
||||||
|
title: t("employees.errors.save", {
|
||||||
|
message: JSON.stringify(error)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//New record, insert it.
|
||||||
|
logImEXEvent("shop_employee_insert");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await insertEmployees({
|
||||||
|
variables: { employees: [{ ...normalizedValues, shopid: bodyshop.id }] },
|
||||||
|
refetchQueries: ["QUERY_EMPLOYEES"]
|
||||||
|
});
|
||||||
|
const savedEmployee = result?.data?.insert_employees?.returning?.[0];
|
||||||
|
|
||||||
|
syncEmployeeFormToSavedData(savedEmployee ?? normalizedValues);
|
||||||
|
|
||||||
|
if (submitAction === "saveAndNew") {
|
||||||
|
navigateToEmployee("new");
|
||||||
|
} else if (savedEmployee?.id) {
|
||||||
|
navigateToEmployee(savedEmployee.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
notification.success({
|
||||||
|
title: t("employees.successes.save")
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
notification.error({
|
||||||
|
title: t("employees.errors.save", {
|
||||||
|
message: JSON.stringify(error)
|
||||||
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -240,13 +299,24 @@ export function ShopEmployeesFormComponent({ bodyshop, form, onDirtyChange, isDi
|
|||||||
<Card
|
<Card
|
||||||
title={employeeCardTitle}
|
title={employeeCardTitle}
|
||||||
extra={
|
extra={
|
||||||
<Button type="primary" onClick={() => form.submit()} disabled={!resolvedIsDirty} style={{ minWidth: 170 }}>
|
<Space wrap>
|
||||||
{t("employees.actions.save_employee")}
|
<Button onClick={() => submitEmployeeForm("saveAndNew")} disabled={!resolvedIsDirty} style={{ minWidth: 170 }}>
|
||||||
</Button>
|
{t("general.actions.saveandnew") || "Save and New"}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={() => submitEmployeeForm("save")}
|
||||||
|
disabled={!resolvedIsDirty}
|
||||||
|
style={{ minWidth: 170 }}
|
||||||
|
>
|
||||||
|
{t("employees.actions.save_employee")}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Form
|
<Form
|
||||||
onFinish={handleFinish}
|
onFinish={handleFinish}
|
||||||
|
onFinishFailed={saveAndResetSubmitAction}
|
||||||
autoComplete={"off"}
|
autoComplete={"off"}
|
||||||
layout="vertical"
|
layout="vertical"
|
||||||
form={form}
|
form={form}
|
||||||
|
|||||||
@@ -0,0 +1,345 @@
|
|||||||
|
import { useApolloClient } from "@apollo/client/react";
|
||||||
|
import { Form } from "antd";
|
||||||
|
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { DELETE_VACATION, INSERT_EMPLOYEES, QUERY_EMPLOYEE_BY_ID, UPDATE_EMPLOYEE } from "../../graphql/employees.queries";
|
||||||
|
import { ShopEmployeesFormComponent } from "./shop-employees-form.component.jsx";
|
||||||
|
|
||||||
|
const insertEmployeesMock = vi.fn();
|
||||||
|
const updateEmployeeMock = vi.fn();
|
||||||
|
const deleteVacationMock = vi.fn();
|
||||||
|
const useQueryMock = vi.fn();
|
||||||
|
const useMutationMock = vi.fn();
|
||||||
|
const navigateMock = vi.fn();
|
||||||
|
const notification = {
|
||||||
|
error: vi.fn(),
|
||||||
|
success: vi.fn()
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.mock("@apollo/client/react", async () => {
|
||||||
|
const actual = await vi.importActual("@apollo/client/react");
|
||||||
|
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
useApolloClient: vi.fn(),
|
||||||
|
useQuery: (...args) => useQueryMock(...args),
|
||||||
|
useMutation: (...args) => useMutationMock(...args)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock("@splitsoftware/splitio-react", () => ({
|
||||||
|
useTreatmentsWithConfig: () => ({
|
||||||
|
treatments: {
|
||||||
|
Enhanced_Payroll: {
|
||||||
|
treatment: "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("react-router-dom", () => ({
|
||||||
|
useLocation: () => ({
|
||||||
|
search: "?employeeId=new"
|
||||||
|
}),
|
||||||
|
useNavigate: () => navigateMock
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("react-i18next", () => ({
|
||||||
|
useTranslation: () => ({
|
||||||
|
t: (key, values = {}) => {
|
||||||
|
const translations = {
|
||||||
|
"bodyshop.labels.employee_options": "Employee Options",
|
||||||
|
"bodyshop.labels.employee_rates": "Employee Rates",
|
||||||
|
"bodyshop.labels.employee_vacation": "Employee Vacation",
|
||||||
|
"bodyshop.labels.employees": "Employees",
|
||||||
|
"employees.actions.addrate": "Add Rate",
|
||||||
|
"employees.actions.addvacation": "Add Vacation",
|
||||||
|
"employees.actions.new": "New Employee",
|
||||||
|
"employees.actions.save_employee": "Save Employee",
|
||||||
|
"employees.fields.active": "Active",
|
||||||
|
"employees.fields.employee_number": "Employee Number",
|
||||||
|
"employees.fields.external_id": "External Id",
|
||||||
|
"employees.fields.first_name": "First Name",
|
||||||
|
"employees.fields.flat_rate": "Flat Rate",
|
||||||
|
"employees.fields.hire_date": "Hire Date",
|
||||||
|
"employees.fields.last_name": "Last Name",
|
||||||
|
"employees.fields.pin": "PIN",
|
||||||
|
"employees.fields.rate": "Rate",
|
||||||
|
"employees.fields.termination_date": "Termination Date",
|
||||||
|
"employees.fields.user_email": "User Email",
|
||||||
|
"employees.labels.active": "Active",
|
||||||
|
"employees.successes.save": "Saved",
|
||||||
|
"general.actions.saveandnew": "Save and New",
|
||||||
|
"general.labels.actions": "Actions"
|
||||||
|
};
|
||||||
|
|
||||||
|
if (key === "employees.errors.save") {
|
||||||
|
return `Save failed: ${values.message ?? ""}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === "employees.validation.unique_employee_number") {
|
||||||
|
return "Employee number must be unique";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === "bodyshop.validation.useremailmustexist") {
|
||||||
|
return "User email must exist";
|
||||||
|
}
|
||||||
|
|
||||||
|
return translations[key] || key;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../../contexts/Notifications/notificationContext.jsx", () => ({
|
||||||
|
useNotification: () => notification
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../../firebase/firebase.utils", () => ({
|
||||||
|
logImEXEvent: vi.fn()
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../alert/alert.component", () => ({
|
||||||
|
default: ({ title }) => <div>{title}</div>
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../form-fields-changed-alert/form-fields-changed-alert.component.jsx", () => ({
|
||||||
|
default: () => null
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../form-date-time-picker/form-date-time-picker.component.jsx", () => ({
|
||||||
|
default: ({ id, value, onChange }) => (
|
||||||
|
<input id={id} type="text" value={value ?? ""} onChange={(event) => onChange?.(event.target.value)} />
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../form-items-formatted/email-form-item.component.jsx", () => ({
|
||||||
|
default: ({ id, value, onChange }) => (
|
||||||
|
<input id={id} type="email" value={value ?? ""} onChange={(event) => onChange?.(event.target.value)} />
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../layout-form-row/layout-form-row.component", () => ({
|
||||||
|
default: ({ title, extra, actions, children }) => (
|
||||||
|
<div>
|
||||||
|
{title}
|
||||||
|
{extra}
|
||||||
|
{children}
|
||||||
|
{actions}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../layout-form-row/inline-validated-form-row.component.jsx", () => ({
|
||||||
|
default: ({ title, extra, children }) => (
|
||||||
|
<div>
|
||||||
|
{title}
|
||||||
|
{extra}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../layout-form-row/config-list-empty-state.component.jsx", () => ({
|
||||||
|
default: ({ actionLabel }) => <div>{actionLabel}</div>
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../form-list-move-arrows/form-list-move-arrows.component", () => ({
|
||||||
|
default: () => null
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../responsive-table/responsive-table.component", () => ({
|
||||||
|
default: () => null
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("./shop-employees-add-vacation.component", () => ({
|
||||||
|
default: () => null
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../../utils/Ciecaselect", () => ({
|
||||||
|
default: () => []
|
||||||
|
}));
|
||||||
|
|
||||||
|
const bodyshop = {
|
||||||
|
id: "shop-1",
|
||||||
|
imexshopid: "split-shop-1",
|
||||||
|
md_responsibility_centers: {
|
||||||
|
costs: []
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("ShopEmployeesFormComponent", () => {
|
||||||
|
let formInstance;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
|
||||||
|
useQueryMock.mockImplementation((query) => {
|
||||||
|
if (query === QUERY_EMPLOYEE_BY_ID) {
|
||||||
|
return {
|
||||||
|
error: null,
|
||||||
|
data: null,
|
||||||
|
refetch: vi.fn(),
|
||||||
|
loading: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
error: null,
|
||||||
|
data: null,
|
||||||
|
loading: false
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
useMutationMock.mockImplementation((mutation) => {
|
||||||
|
if (mutation === INSERT_EMPLOYEES) return [insertEmployeesMock];
|
||||||
|
if (mutation === UPDATE_EMPLOYEE) return [updateEmployeeMock];
|
||||||
|
if (mutation === DELETE_VACATION) return [deleteVacationMock];
|
||||||
|
return [vi.fn()];
|
||||||
|
});
|
||||||
|
|
||||||
|
useApolloClient.mockReturnValue({
|
||||||
|
query: vi.fn().mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
employees_aggregate: {
|
||||||
|
aggregate: {
|
||||||
|
count: 0
|
||||||
|
},
|
||||||
|
nodes: []
|
||||||
|
},
|
||||||
|
users: []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
insertEmployeesMock.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
insert_employees: {
|
||||||
|
returning: [
|
||||||
|
{
|
||||||
|
id: "employee-123",
|
||||||
|
first_name: "Jamie",
|
||||||
|
last_name: "Rivera",
|
||||||
|
employee_number: "42",
|
||||||
|
active: true,
|
||||||
|
termination_date: null,
|
||||||
|
hire_date: "2026-04-20",
|
||||||
|
flat_rate: false,
|
||||||
|
rates: [],
|
||||||
|
pin: "1234",
|
||||||
|
user_email: null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function TestHarness({ onFormReady }) {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onFormReady(form);
|
||||||
|
}, [form, onFormReady]);
|
||||||
|
|
||||||
|
return <ShopEmployeesFormComponent bodyshop={bodyshop} form={form} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(
|
||||||
|
<TestHarness
|
||||||
|
onFormReady={(form) => {
|
||||||
|
formInstance = form;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("marks a new employee form clean after save", async () => {
|
||||||
|
fireEvent.change(screen.getByRole("textbox", { name: "First Name" }), {
|
||||||
|
target: { value: "Jamie" }
|
||||||
|
});
|
||||||
|
fireEvent.change(screen.getByRole("textbox", { name: "Last Name" }), {
|
||||||
|
target: { value: "Rivera" }
|
||||||
|
});
|
||||||
|
fireEvent.change(screen.getByRole("textbox", { name: "Employee Number" }), {
|
||||||
|
target: { value: "42" }
|
||||||
|
});
|
||||||
|
fireEvent.change(screen.getByRole("textbox", { name: "PIN" }), {
|
||||||
|
target: { value: "1234" }
|
||||||
|
});
|
||||||
|
fireEvent.change(screen.getByRole("textbox", { name: "Hire Date" }), {
|
||||||
|
target: { value: "2026-04-20" }
|
||||||
|
});
|
||||||
|
|
||||||
|
const saveButton = screen.getByRole("button", { name: "Save Employee" });
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(saveButton.disabled).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.click(saveButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(insertEmployeesMock).toHaveBeenCalledWith({
|
||||||
|
variables: {
|
||||||
|
employees: [
|
||||||
|
expect.objectContaining({
|
||||||
|
first_name: "Jamie",
|
||||||
|
last_name: "Rivera",
|
||||||
|
employee_number: "42",
|
||||||
|
pin: "1234",
|
||||||
|
hire_date: "2026-04-20",
|
||||||
|
shopid: "shop-1"
|
||||||
|
})
|
||||||
|
]
|
||||||
|
},
|
||||||
|
refetchQueries: ["QUERY_EMPLOYEES"]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(formInstance.isFieldsTouched()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(notification.success).toHaveBeenCalledWith({
|
||||||
|
title: "Saved"
|
||||||
|
});
|
||||||
|
expect(navigateMock).toHaveBeenCalledWith({
|
||||||
|
search: "employeeId=employee-123"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("saves a new employee and opens a fresh employee form when save and new is clicked", async () => {
|
||||||
|
fireEvent.change(screen.getByRole("textbox", { name: "First Name" }), {
|
||||||
|
target: { value: "Jamie" }
|
||||||
|
});
|
||||||
|
fireEvent.change(screen.getByRole("textbox", { name: "Last Name" }), {
|
||||||
|
target: { value: "Rivera" }
|
||||||
|
});
|
||||||
|
fireEvent.change(screen.getByRole("textbox", { name: "Employee Number" }), {
|
||||||
|
target: { value: "42" }
|
||||||
|
});
|
||||||
|
fireEvent.change(screen.getByRole("textbox", { name: "PIN" }), {
|
||||||
|
target: { value: "1234" }
|
||||||
|
});
|
||||||
|
fireEvent.change(screen.getByRole("textbox", { name: "Hire Date" }), {
|
||||||
|
target: { value: "2026-04-20" }
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Save and New" }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(insertEmployeesMock).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(formInstance.isFieldsTouched()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(navigateMock).toHaveBeenCalledWith({
|
||||||
|
search: "employeeId=new"
|
||||||
|
});
|
||||||
|
expect(notification.success).toHaveBeenCalledWith({
|
||||||
|
title: "Saved"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1582,7 +1582,6 @@ export function ShopInfoGeneral({ form }) {
|
|||||||
form={form}
|
form={form}
|
||||||
errorNames={[["md_parts_order_comment", field.name, "label"]]}
|
errorNames={[["md_parts_order_comment", field.name, "label"]]}
|
||||||
noDivider
|
noDivider
|
||||||
titleOnly
|
|
||||||
title={
|
title={
|
||||||
<div style={INLINE_TITLE_ROW_STYLE}>
|
<div style={INLINE_TITLE_ROW_STYLE}>
|
||||||
<InlineTitleListIcon style={INLINE_TITLE_HANDLE_STYLE} />
|
<InlineTitleListIcon style={INLINE_TITLE_HANDLE_STYLE} />
|
||||||
|
|||||||
@@ -810,16 +810,6 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
|
|||||||
>
|
>
|
||||||
<Input onBlur={handleBlur} />
|
<Input onBlur={handleBlur} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{!hasDMSKey && (
|
|
||||||
<Form.Item
|
|
||||||
label={t("bodyshop.fields.responsibilitycenter_accountitem")}
|
|
||||||
key={`${index}accountitem`}
|
|
||||||
name={[field.name, "accountitem"]}
|
|
||||||
rules={[{ required: true }]}
|
|
||||||
>
|
|
||||||
<Input onBlur={handleBlur} />
|
|
||||||
</Form.Item>
|
|
||||||
)}
|
|
||||||
{hasDMSKey && !bodyshop.rr_dealerid && (
|
{hasDMSKey && !bodyshop.rr_dealerid && (
|
||||||
<>
|
<>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
|
|||||||
@@ -27,12 +27,19 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export function TechAssignedProdJobs({ setTimeTicketTaskContext, technician, bodyshop }) {
|
export function TechAssignedProdJobs({ setTimeTicketTaskContext, technician, bodyshop }) {
|
||||||
|
const technicianId = technician?.id;
|
||||||
|
const teamIds = (bodyshop?.employee_teams || [])
|
||||||
|
.filter((employeeTeam) =>
|
||||||
|
employeeTeam?.employee_team_members?.some((teamMember) => teamMember?.employeeid === technicianId)
|
||||||
|
)
|
||||||
|
.map((employeeTeam) => employeeTeam.id)
|
||||||
|
.filter(Boolean);
|
||||||
|
const hasAssignedTeams = Boolean(technicianId) && teamIds.length > 0;
|
||||||
const { loading, error, data, refetch } = useQuery(QUERY_JOBS_TECH_ASIGNED_TO_BY_TEAM, {
|
const { loading, error, data, refetch } = useQuery(QUERY_JOBS_TECH_ASIGNED_TO_BY_TEAM, {
|
||||||
variables: {
|
variables: {
|
||||||
teamIds: bodyshop.employee_teams
|
teamIds
|
||||||
.filter((et) => et.employee_team_members.find((etm) => etm.employeeid === technician.id))
|
},
|
||||||
.map((et) => et.id)
|
skip: !technicianId || !hasAssignedTeams
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const searchParams = queryString.parse(useLocation().search);
|
const searchParams = queryString.parse(useLocation().search);
|
||||||
@@ -177,7 +184,7 @@ export function TechAssignedProdJobs({ setTimeTicketTaskContext, technician, bod
|
|||||||
<Card
|
<Card
|
||||||
extra={
|
extra={
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<Button onClick={() => refetch()} icon={<SyncOutlined />} />
|
<Button disabled={!hasAssignedTeams} onClick={() => refetch()} icon={<SyncOutlined />} />
|
||||||
<Input.Search
|
<Input.Search
|
||||||
placeholder={t("general.labels.search")}
|
placeholder={t("general.labels.search")}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
|||||||
@@ -1785,6 +1785,7 @@
|
|||||||
},
|
},
|
||||||
"jobs": {
|
"jobs": {
|
||||||
"actions": {
|
"actions": {
|
||||||
|
"addpayer": "Add Payer",
|
||||||
"addDocuments": "Add Job Documents",
|
"addDocuments": "Add Job Documents",
|
||||||
"addNote": "Add Note",
|
"addNote": "Add Note",
|
||||||
"addtopartsqueue": "Add to Parts Queue",
|
"addtopartsqueue": "Add to Parts Queue",
|
||||||
|
|||||||
@@ -1779,6 +1779,7 @@
|
|||||||
},
|
},
|
||||||
"jobs": {
|
"jobs": {
|
||||||
"actions": {
|
"actions": {
|
||||||
|
"addpayer": "",
|
||||||
"addDocuments": "Agregar documentos de trabajo",
|
"addDocuments": "Agregar documentos de trabajo",
|
||||||
"addNote": "Añadir la nota",
|
"addNote": "Añadir la nota",
|
||||||
"addtopartsqueue": "",
|
"addtopartsqueue": "",
|
||||||
|
|||||||
@@ -1779,6 +1779,7 @@
|
|||||||
},
|
},
|
||||||
"jobs": {
|
"jobs": {
|
||||||
"actions": {
|
"actions": {
|
||||||
|
"addpayer": "",
|
||||||
"addDocuments": "Ajouter des documents de travail",
|
"addDocuments": "Ajouter des documents de travail",
|
||||||
"addNote": "Ajouter une note",
|
"addNote": "Ajouter une note",
|
||||||
"addtopartsqueue": "",
|
"addtopartsqueue": "",
|
||||||
|
|||||||
@@ -1164,6 +1164,7 @@
|
|||||||
- notification_followers
|
- notification_followers
|
||||||
- state
|
- state
|
||||||
- md_order_statuses
|
- md_order_statuses
|
||||||
|
- md_ro_statuses
|
||||||
retry_conf:
|
retry_conf:
|
||||||
interval_sec: 10
|
interval_sec: 10
|
||||||
num_retries: 0
|
num_retries: 0
|
||||||
@@ -1184,7 +1185,8 @@
|
|||||||
"new": {
|
"new": {
|
||||||
"id": {{$body.event.data.new.id}},
|
"id": {{$body.event.data.new.id}},
|
||||||
"shopname": {{$body.event.data.new.shopname}},
|
"shopname": {{$body.event.data.new.shopname}},
|
||||||
"md_order_statuses": {{$body.event.data.new.md_order_statuses}}
|
"md_order_statuses": {{$body.event.data.new.md_order_statuses}},
|
||||||
|
"md_ro_statuses": {{$body.event.data.new.md_ro_statuses}}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"op": {{$body.event.op}},
|
"op": {{$body.event.op}},
|
||||||
|
|||||||
@@ -2956,6 +2956,7 @@ exports.GET_BODYSHOP_BY_ID = `
|
|||||||
query GET_BODYSHOP_BY_ID($id: uuid!) {
|
query GET_BODYSHOP_BY_ID($id: uuid!) {
|
||||||
bodyshops_by_pk(id: $id) {
|
bodyshops_by_pk(id: $id) {
|
||||||
id
|
id
|
||||||
|
md_ro_statuses
|
||||||
md_order_statuses
|
md_order_statuses
|
||||||
shopname
|
shopname
|
||||||
imexshopid
|
imexshopid
|
||||||
|
|||||||
187
server/rr/rr-export-logs.test.js
Normal file
187
server/rr/rr-export-logs.test.js
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
import { afterEach, describe, expect, it } from "vitest";
|
||||||
|
import { createRequire } from "node:module";
|
||||||
|
|
||||||
|
const require = createRequire(import.meta.url);
|
||||||
|
const mock = require("mock-require");
|
||||||
|
|
||||||
|
const graphqlRequestModuleId = require.resolve("graphql-request");
|
||||||
|
const queriesModuleId = require.resolve("../graphql-client/queries");
|
||||||
|
const rrLoggerModuleId = require.resolve("./rr-logger-event");
|
||||||
|
const rrExportLogsModuleId = require.resolve("./rr-export-logs");
|
||||||
|
|
||||||
|
const loadExportLogs = ({ requests }) => {
|
||||||
|
mock.stopAll();
|
||||||
|
|
||||||
|
mock(graphqlRequestModuleId, {
|
||||||
|
GraphQLClient: class MockGraphQLClient {
|
||||||
|
constructor(endpoint) {
|
||||||
|
this.endpoint = endpoint;
|
||||||
|
this.headers = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
setHeaders(headers) {
|
||||||
|
this.headers = headers;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
async request(query, variables) {
|
||||||
|
requests.push({
|
||||||
|
endpoint: this.endpoint,
|
||||||
|
headers: this.headers,
|
||||||
|
query,
|
||||||
|
variables
|
||||||
|
});
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
mock(queriesModuleId, {
|
||||||
|
INSERT_EXPORT_LOG: "INSERT_EXPORT_LOG",
|
||||||
|
MARK_JOB_EXPORTED: "MARK_JOB_EXPORTED"
|
||||||
|
});
|
||||||
|
|
||||||
|
mock(rrLoggerModuleId, () => {});
|
||||||
|
|
||||||
|
delete require.cache[rrExportLogsModuleId];
|
||||||
|
return require(rrExportLogsModuleId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const socket = {
|
||||||
|
data: { authToken: "socket-token" },
|
||||||
|
user: { email: "tech@example.com" }
|
||||||
|
};
|
||||||
|
|
||||||
|
const job = {
|
||||||
|
id: "job-1",
|
||||||
|
bodyshop: {
|
||||||
|
id: "bodyshop-1",
|
||||||
|
md_ro_statuses: {
|
||||||
|
default_exported: "Exported"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("server/rr/rr-export-logs", () => {
|
||||||
|
const originalEndpoint = process.env.GRAPHQL_ENDPOINT;
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
mock.stopAll();
|
||||||
|
delete require.cache[rrExportLogsModuleId];
|
||||||
|
process.env.GRAPHQL_ENDPOINT = originalEndpoint;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("marks Reynolds full exports as exported using the shared DMS export mutation", async () => {
|
||||||
|
process.env.GRAPHQL_ENDPOINT = "https://graphql.example.test/v1/graphql";
|
||||||
|
const requests = [];
|
||||||
|
const { markRRExportSuccess } = loadExportLogs({ requests });
|
||||||
|
|
||||||
|
await markRRExportSuccess({
|
||||||
|
socket,
|
||||||
|
jobId: job.id,
|
||||||
|
job,
|
||||||
|
bodyshop: job.bodyshop,
|
||||||
|
result: {
|
||||||
|
success: true,
|
||||||
|
roStatus: {
|
||||||
|
status: "SUCCESS",
|
||||||
|
statusCode: "0",
|
||||||
|
message: "Finalized"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(requests).toHaveLength(1);
|
||||||
|
expect(requests[0]).toMatchObject({
|
||||||
|
endpoint: "https://graphql.example.test/v1/graphql",
|
||||||
|
headers: { Authorization: "Bearer socket-token" },
|
||||||
|
query: "MARK_JOB_EXPORTED",
|
||||||
|
variables: {
|
||||||
|
jobId: "job-1",
|
||||||
|
job: {
|
||||||
|
status: "Exported",
|
||||||
|
date_exported: expect.any(Date)
|
||||||
|
},
|
||||||
|
log: {
|
||||||
|
bodyshopid: "bodyshop-1",
|
||||||
|
jobid: "job-1",
|
||||||
|
successful: true,
|
||||||
|
useremail: "tech@example.com"
|
||||||
|
},
|
||||||
|
bill: {
|
||||||
|
exported: true,
|
||||||
|
exported_at: expect.any(Date)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses the separately loaded bodyshop statuses when job.bodyshop is missing", async () => {
|
||||||
|
process.env.GRAPHQL_ENDPOINT = "https://graphql.example.test/v1/graphql";
|
||||||
|
const requests = [];
|
||||||
|
const { markRRExportSuccess } = loadExportLogs({ requests });
|
||||||
|
|
||||||
|
await markRRExportSuccess({
|
||||||
|
socket,
|
||||||
|
jobId: job.id,
|
||||||
|
job: { id: job.id },
|
||||||
|
bodyshop: job.bodyshop,
|
||||||
|
result: {
|
||||||
|
success: true,
|
||||||
|
roStatus: {
|
||||||
|
status: "SUCCESS",
|
||||||
|
statusCode: "0",
|
||||||
|
message: "Finalized"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(requests).toHaveLength(1);
|
||||||
|
expect(requests[0]).toMatchObject({
|
||||||
|
query: "MARK_JOB_EXPORTED",
|
||||||
|
variables: {
|
||||||
|
jobId: "job-1",
|
||||||
|
job: {
|
||||||
|
status: "Exported",
|
||||||
|
date_exported: expect.any(Date)
|
||||||
|
},
|
||||||
|
log: {
|
||||||
|
bodyshopid: "bodyshop-1",
|
||||||
|
jobid: "job-1",
|
||||||
|
successful: true,
|
||||||
|
useremail: "tech@example.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not mark Reynolds early RO creation as exported", async () => {
|
||||||
|
process.env.GRAPHQL_ENDPOINT = "https://graphql.example.test/v1/graphql";
|
||||||
|
const requests = [];
|
||||||
|
const { markRRExportSuccess } = loadExportLogs({ requests });
|
||||||
|
|
||||||
|
await markRRExportSuccess({
|
||||||
|
socket,
|
||||||
|
jobId: job.id,
|
||||||
|
job,
|
||||||
|
bodyshop: job.bodyshop,
|
||||||
|
result: { success: true },
|
||||||
|
isEarlyRo: true
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(requests).toHaveLength(1);
|
||||||
|
expect(requests[0]).toMatchObject({
|
||||||
|
query: "INSERT_EXPORT_LOG",
|
||||||
|
variables: {
|
||||||
|
logs: [
|
||||||
|
{
|
||||||
|
bodyshopid: "bodyshop-1",
|
||||||
|
jobid: "job-1",
|
||||||
|
successful: true,
|
||||||
|
useremail: "tech@example.com"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user