Compare commits

...

3 commits

Author SHA1 Message Date
Arthur Bassi
e9e17dadec fix(work-orders): compose full user names from User list 2026-08-31 09:23:10 -03:00
Arthur Bassi
223a734b11
Merge branch 'dev' into feature/sh-127-assigned-to-ownership 2026-08-31 09:17:03 -03:00
Alexandre Brandizzi
d6755a63b6
fix(vendors): clarify blank technician draft state (#157)
Some checks failed
Frontend checks / Build and test (push) Has been cancelled
Frontend checks / governance (push) Has been cancelled
Frontend checks / Visual regression (push) Has been cancelled
Deploy / deploy (push) Has been cancelled
* fix(vendors): clarify blank technician draft state

* test(vendors): update technician section snapshots

---------

Co-authored-by: Codex Review Integration <codex-review@local.invalid>
2026-08-28 15:00:02 -04:00
8 changed files with 68 additions and 8 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

After

Width:  |  Height:  |  Size: 146 KiB

View file

@ -16,6 +16,9 @@ import {
} from "@/domain/vendors/use-cases/use-save-vendor-company-roster";
import { useVendorRosterResolver } from "./use-vendor-roster-resolver";
const VENDOR_ROSTER_NO_CHANGES_MESSAGE =
"No changes to save. Enter technician details or update a company field.";
const EDITABLE_COMPANY_FIELDS = [
"name",
"companyPhone",
@ -180,7 +183,7 @@ export function useVendorRosterForm({
if (mode === "create" && selection.selectedRoster != null) {
const selected = selection.selectedRoster;
if (!buildAdditiveRosterPatch(selected, values, selected.rowVersion, editedCompanyFields)) {
toast.error("Enter at least one technician to add to this vendor.");
toast.error(VENDOR_ROSTER_NO_CHANGES_MESSAGE);
return;
}
save.mutate(

View file

@ -358,7 +358,7 @@ function TechniciansFieldArray({
<Box>
<Stack direction="row" sx={{ alignItems: "center", justifyContent: "space-between" }}>
<Typography variant="subtitle2" className="font-semibold uppercase">
Technicians ({fields.length})
Technician details (optional)
</Typography>
<Button
size="small"

View file

@ -110,18 +110,34 @@ function toUrlSearchParams(
return sp;
}
function readOptionalString(item: Record<string, unknown>, ...keys: string[]): string {
for (const key of keys) {
const value = item[key];
if (typeof value === "string" && value.trim()) {
return value.trim();
}
}
return "";
}
/** GET User exposes `Name` as first name only; prefer FirstName + LastName when present. */
function mapUserDisplayName(item: Record<string, unknown>): string {
const firstName = readOptionalString(item, "firstName", "FirstName");
const lastName = readOptionalString(item, "lastName", "LastName");
const fallback = readOptionalString(item, "name", "Name", "userName", "UserName");
if (lastName) {
return `${firstName || fallback} ${lastName}`.trim();
}
return firstName || fallback;
}
function mapUser(raw: unknown): WorkOrderUser {
const item = (typeof raw === "object" && raw !== null ? raw : {}) as Record<string, unknown>;
const id = item.id ?? item.Id;
const name =
(typeof item.name === "string" ? item.name : undefined) ??
(typeof item.Name === "string" ? item.Name : undefined) ??
(typeof item.userName === "string" ? item.userName : "") ??
"";
return {
id: typeof id === "string" || typeof id === "number" ? id : "",
name,
name: mapUserDisplayName(item),
};
}

View file

@ -2,6 +2,7 @@ import { act, renderHook, waitFor } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import type { ReactNode } from "react";
import { useController, useFieldArray } from "react-hook-form";
import { toast } from "react-toastify";
import { beforeEach, describe, expect, it, vi } from "vitest";
const rosterGet = vi.fn();
@ -428,6 +429,7 @@ describe("useVendorRosterForm additive add flow (SH-250)", () => {
beforeEach(() => {
rosterGet.mockReset();
saveMutate.mockReset();
vi.mocked(toast.error).mockReset();
});
it("issues an additive add — never update — when an existing company is selected", async () => {
@ -498,6 +500,10 @@ describe("useVendorRosterForm additive add flow (SH-250)", () => {
});
expect(saveMutate).not.toHaveBeenCalled();
expect(vi.mocked(toast.error)).toHaveBeenCalledTimes(1);
expect(vi.mocked(toast.error)).toHaveBeenCalledWith(
"No changes to save. Enter technician details or update a company field.",
);
});
it("keeps the entered technician and retries with the fresh rowVersion after reload", async () => {

View file

@ -75,6 +75,14 @@ describe("VendorCreateModal validation", () => {
);
});
it("labels the optional technician section without counting blank drafts", () => {
renderCreateModal();
expect(screen.getByText("Technician details (optional)")).toBeInTheDocument();
expect(screen.queryByText("Technicians (1)")).not.toBeInTheDocument();
expect(screen.getByRole("textbox", { name: "Technician name (optional)" })).toBeVisible();
});
it("labels the plain company field exactly, required, with no generated asterisk", () => {
renderWithProviders(<PlainCompanyFieldsHarness />, { route: "/vendors", withAuth: false });

View file

@ -200,6 +200,33 @@ describe("workOrdersApi.fetchDispatchers", () => {
});
});
const GET_USER_SERIALIZED_DIRECTORY = {
data: [
{
Id: "s1",
Name: "Alex",
FirstName: "Alex",
LastName: "Smith",
UserName: "alex.smith",
},
],
};
describe("workOrdersApi.fetchUsers", () => {
beforeEach(() => {
apiGet.mockReset();
});
it("composes a full display name from the serialized GET User shape", async () => {
apiGet.mockResolvedValue(GET_USER_SERIALIZED_DIRECTORY);
const users = await workOrdersApi.fetchUsers();
expect(apiGet).toHaveBeenCalledWith(API_PATHS.user.list);
expect(users).toEqual([{ id: "s1", name: "Alex Smith" }]);
});
});
describe("workOrdersApi.getBoard", () => {
beforeEach(() => {
apiGet.mockReset();