Merged in release/2026-06-05 (pull request #3271)

feature/IO-2960-Employee-Email-Info - Fix
This commit is contained in:
Dave Richer
2026-05-26 16:18:15 +00:00
6 changed files with 114 additions and 20 deletions

View File

@@ -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 {

View File

@@ -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();
});
});

View File

@@ -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 } }) {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {