Merged in release/2026-06-05 (pull request #3271)
feature/IO-2960-Employee-Email-Info - Fix
This commit is contained in:
@@ -12,11 +12,11 @@ import { createStructuredSelector } from "reselect";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import {
|
||||
CHECK_EMPLOYEE_EMAIL,
|
||||
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";
|
||||
@@ -174,9 +174,10 @@ export function ShopEmployeesFormComponent({ bodyshop, form, onDirtyChange, isDi
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
const submitAction = saveAndResetSubmitAction();
|
||||
const userEmail = typeof values.user_email === "string" ? values.user_email.trim() : values.user_email;
|
||||
const normalizedValues = {
|
||||
...values,
|
||||
user_email: values.user_email === "" ? null : values.user_email
|
||||
user_email: userEmail === "" ? null : userEmail
|
||||
};
|
||||
|
||||
if (search.employeeId && search.employeeId !== "new") {
|
||||
@@ -491,18 +492,29 @@ export function ShopEmployeesFormComponent({ bodyshop, form, onDirtyChange, isDi
|
||||
rules={[
|
||||
({ getFieldValue }) => ({
|
||||
async validator(rule, value) {
|
||||
const user_email = getFieldValue("user_email");
|
||||
const user_email = typeof value === "string" ? value.trim() : getFieldValue("user_email");
|
||||
|
||||
if (user_email && value) {
|
||||
const response = await client.query({
|
||||
query: QUERY_USERS_BY_EMAIL,
|
||||
query: CHECK_EMPLOYEE_EMAIL,
|
||||
variables: {
|
||||
email: user_email
|
||||
email: user_email,
|
||||
shopId: bodyshop.id
|
||||
}
|
||||
});
|
||||
|
||||
if (response.data.users.length === 1) {
|
||||
return Promise.resolve();
|
||||
const matchingEmployees = response.data.employees_aggregate.nodes;
|
||||
const currentEmployeeId = form.getFieldValue("id") ?? search.employeeId;
|
||||
|
||||
if (
|
||||
response.data.employees_aggregate.aggregate.count === 0 ||
|
||||
matchingEmployees.every((employee) => employee.id === currentEmployeeId)
|
||||
) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return Promise.reject(t("employees.validation.unique_user_email"));
|
||||
}
|
||||
return Promise.reject(t("bodyshop.validation.useremailmustexist"));
|
||||
} else {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { useEffect } from "react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
CHECK_EMPLOYEE_EMAIL,
|
||||
DELETE_VACATION,
|
||||
INSERT_EMPLOYEES,
|
||||
QUERY_EMPLOYEE_BY_ID,
|
||||
@@ -16,6 +17,7 @@ const updateEmployeeMock = vi.fn();
|
||||
const deleteVacationMock = vi.fn();
|
||||
const useQueryMock = vi.fn();
|
||||
const useMutationMock = vi.fn();
|
||||
const apolloClientQueryMock = vi.fn();
|
||||
const navigateMock = vi.fn();
|
||||
const notification = {
|
||||
error: vi.fn(),
|
||||
@@ -87,6 +89,10 @@ vi.mock("react-i18next", () => ({
|
||||
return "Employee number must be unique";
|
||||
}
|
||||
|
||||
if (key === "employees.validation.unique_user_email") {
|
||||
return "User email already assigned";
|
||||
}
|
||||
|
||||
if (key === "bodyshop.validation.useremailmustexist") {
|
||||
return "User email must exist";
|
||||
}
|
||||
@@ -203,18 +209,20 @@ describe("ShopEmployeesFormComponent", () => {
|
||||
return [vi.fn()];
|
||||
});
|
||||
|
||||
useApolloClient.mockReturnValue({
|
||||
query: vi.fn().mockResolvedValue({
|
||||
data: {
|
||||
employees_aggregate: {
|
||||
aggregate: {
|
||||
count: 0
|
||||
},
|
||||
nodes: []
|
||||
apolloClientQueryMock.mockResolvedValue({
|
||||
data: {
|
||||
employees_aggregate: {
|
||||
aggregate: {
|
||||
count: 0
|
||||
},
|
||||
users: []
|
||||
}
|
||||
})
|
||||
nodes: []
|
||||
},
|
||||
users: []
|
||||
}
|
||||
});
|
||||
|
||||
useApolloClient.mockReturnValue({
|
||||
query: apolloClientQueryMock
|
||||
});
|
||||
|
||||
insertEmployeesMock.mockResolvedValue({
|
||||
@@ -356,4 +364,59 @@ describe("ShopEmployeesFormComponent", () => {
|
||||
title: "Saved"
|
||||
});
|
||||
});
|
||||
|
||||
it("blocks saving when the user email belongs to another employee in the shop", async () => {
|
||||
apolloClientQueryMock.mockImplementation(({ query }) => {
|
||||
if (query === CHECK_EMPLOYEE_EMAIL) {
|
||||
return Promise.resolve({
|
||||
data: {
|
||||
users: [{ email: "jamie@example.com" }],
|
||||
employees_aggregate: {
|
||||
aggregate: {
|
||||
count: 1
|
||||
},
|
||||
nodes: [{ id: "other-employee" }]
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
data: {
|
||||
employees_aggregate: {
|
||||
aggregate: {
|
||||
count: 0
|
||||
},
|
||||
nodes: []
|
||||
},
|
||||
users: []
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
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.change(screen.getByRole("textbox", { name: "User Email" }), {
|
||||
target: { value: "jamie@example.com" }
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Save Employee" }));
|
||||
|
||||
expect(await screen.findByText("User email already assigned")).toBeInTheDocument();
|
||||
expect(insertEmployeesMock).not.toHaveBeenCalled();
|
||||
expect(notification.success).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -49,6 +49,22 @@ export const CHECK_EMPLOYEE_NUMBER = gql`
|
||||
}
|
||||
`;
|
||||
|
||||
export const CHECK_EMPLOYEE_EMAIL = gql`
|
||||
query CHECK_EMPLOYEE_EMAIL($email: String!, $shopId: uuid!) {
|
||||
users(where: { email: { _ilike: $email } }) {
|
||||
email
|
||||
}
|
||||
employees_aggregate(where: { user_email: { _ilike: $email }, shopid: { _eq: $shopId } }) {
|
||||
aggregate {
|
||||
count
|
||||
}
|
||||
nodes {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const QUERY_ACTIVE_EMPLOYEES = gql`
|
||||
query QUERY_ACTIVE_EMPLOYEES {
|
||||
employees(where: { active: { _eq: true } }) {
|
||||
|
||||
@@ -1350,7 +1350,8 @@
|
||||
"vacationadded": "Employee vacation added."
|
||||
},
|
||||
"validation": {
|
||||
"unique_employee_number": "You must enter a unique employee number."
|
||||
"unique_employee_number": "You must enter a unique employee number.",
|
||||
"unique_user_email": "This email is already assigned to another employee."
|
||||
}
|
||||
},
|
||||
"esignature": {
|
||||
|
||||
@@ -1350,7 +1350,8 @@
|
||||
"vacationadded": ""
|
||||
},
|
||||
"validation": {
|
||||
"unique_employee_number": ""
|
||||
"unique_employee_number": "",
|
||||
"unique_user_email": "Este correo electrónico ya está asignado a otro empleado."
|
||||
}
|
||||
},
|
||||
"esignature": {
|
||||
|
||||
@@ -1350,7 +1350,8 @@
|
||||
"vacationadded": ""
|
||||
},
|
||||
"validation": {
|
||||
"unique_employee_number": ""
|
||||
"unique_employee_number": "",
|
||||
"unique_user_email": "Cette adresse courriel est déjà assignée à un autre employé."
|
||||
}
|
||||
},
|
||||
"esignature": {
|
||||
|
||||
Reference in New Issue
Block a user