fix(api): align frontend contracts with backend review findings

Correct Work Order getById query param, asset site options via Location API,
Employee JobTitleId payload, and remove stale API paths.
This commit is contained in:
Arthur Bassi 2026-06-16 11:52:39 -03:00
parent 2a60c525ac
commit 708d946399
11 changed files with 151 additions and 43 deletions

View file

@ -1,6 +1,6 @@
# Feature Prioritization Matrix — SeaHaven
> Source of truth for legacy → `domain/` + `app/` migration order.
> Source of truth for legacy → `domain/` + `app/` migration order.
> Machine-readable config: [`src/config/feature-priorities.ts`](../src/config/feature-priorities.ts).
---

View file

@ -32,8 +32,6 @@ export const API_PATHS = {
getById: "PMSchedule/GetById",
save: "PMSchedule/Save",
delete: "PMSchedule/Delete",
create: "PmSchedule/Create",
edit: "PmSchedule/Edit",
},
followUp: {
getList: "FollowUp/GetFollowUpList",
@ -123,9 +121,6 @@ export const API_PATHS = {
dashboard: {
stats: "Dashboard/Stats",
},
common: {
getSitesName: "Common/GetSitesName",
},
dropdownOptions: {
byCategory: "DropdownOptions/ByCategory",
create: "DropdownOptions/Create",

View file

@ -125,6 +125,7 @@ export default function EmployeeFormPage() {
email: employee.email,
phone: employee.phone,
jobTitle: employee.jobTitle,
jobTitleId: employee.jobTitleId ?? null,
role: employee.role,
status: employee.status,
hireDate: employee.hireDate,

View file

@ -68,8 +68,10 @@ export const assetsApi = {
},
getSiteOptions: async (): Promise<SiteOption[]> => {
const data = await apiGet<unknown>(API_PATHS.common.getSitesName);
const items = handleApiResponse<unknown[]>(data);
return Array.isArray(items) ? items.map(mapSiteOption) : [];
const data = await apiGet<unknown>(API_PATHS.location.getList, {
searchParams: { search: "", page: 1, pageSize: 1000 },
});
const paginated = handlePaginatedResponse<unknown>(data, { page: 1, pageSize: 1000 });
return paginated.items.map(mapSiteOption);
},
};

View file

@ -21,6 +21,18 @@ function readId(record: Record<string, unknown>): string | number {
return "";
}
function readJobTitleId(record: Record<string, unknown>): number | null {
const value = record.jobTitleId ?? record.JobTitleId;
if (typeof value === "number") {
return value;
}
if (typeof value === "string" && value !== "") {
const parsed = Number(value);
return Number.isNaN(parsed) ? null : parsed;
}
return null;
}
export function mapEmployeeListItem(raw: unknown): EmployeeListItem {
const item = (typeof raw === "object" && raw !== null ? raw : {}) as Record<string, unknown>;
const firstName = readString(item, "firstName", "FirstName");
@ -58,6 +70,7 @@ export function transformEmployee(employee: unknown): Employee | null {
email: readString(item, "email", "Email"),
phone: readString(item, "phone", "Phone"),
jobTitle: readString(item, "jobTitle", "JobTitle"),
jobTitleId: readJobTitleId(item),
role: readString(item, "role", "Role") || "Technician",
status: readString(item, "status", "Status") || "Active",
hireDate: readString(item, "hireDate", "HireDate"),
@ -86,38 +99,17 @@ export function transformEmployee(employee: unknown): Employee | null {
}
export function transformToBackend(employee: Employee): Record<string, unknown> {
const zipCode = employee.zip || "";
return {
id: employee.id,
firstName: employee.firstName || "",
middleName: employee.middleName || null,
lastName: employee.lastName || "",
email: employee.email || "",
phone: employee.phone || "",
jobTitle: employee.jobTitle || "",
role: employee.role || "Technician",
status: employee.status || "Active",
hireDate: employee.hireDate || null,
dateOfBirth: employee.dateOfBirth || null,
address: employee.address || "",
street: employee.street || "",
streetLineTwo: employee.streetLineTwo || null,
city: employee.city || "",
state: employee.state || "",
zipCode,
country: employee.country || "US",
county: employee.county || null,
department: employee.department || "Field Service",
timeZone: employee.timeZone || "Eastern Standard Time",
gender: employee.gender || null,
employeeNumber: employee.employeeNumber || null,
regionName: employee.regionName || null,
userName: employee.userName || employee.email || "",
smsAddress: employee.smsAddress || null,
website: employee.website || null,
facebookUrl: employee.facebookUrl || null,
linkedInUrl: employee.linkedInUrl || null,
twitterUrl: employee.twitterUrl || null,
const payload: Record<string, unknown> = {
FirstName: employee.firstName || "",
LastName: employee.lastName || "",
Email: employee.email || null,
Phone: employee.phone || null,
JobTitleId: employee.jobTitleId ?? null,
};
if (employee.id !== undefined && employee.id !== "") {
payload.Id = Number(employee.id);
}
return payload;
}

View file

@ -8,6 +8,7 @@ export const employeeSchema = z
email: z.string().optional(),
phone: z.string().optional(),
jobTitle: z.string().optional(),
jobTitleId: z.number().nullable().optional(),
role: z.string().min(1, "Role is required"),
status: z.string().optional(),
hireDate: z.string().optional(),
@ -47,6 +48,7 @@ export const employeeDefaultValues: EmployeeFormValues = {
email: "",
phone: "",
jobTitle: "",
jobTitleId: null,
role: "Technician",
status: "Active",
hireDate: "",

View file

@ -16,6 +16,7 @@ export interface Employee {
email: string;
phone: string;
jobTitle: string;
jobTitleId?: number | null;
role: string;
status: string;
hireDate: string;

View file

@ -99,7 +99,9 @@ export const workOrdersApi = {
},
getById: async (id: string | number): Promise<WorkOrderDetail> => {
const data = await apiGet<unknown>(`${API_PATHS.workOrder.getById}/${id}`);
const data = await apiGet<unknown>(API_PATHS.workOrder.getById, {
searchParams: { id },
});
return mapWorkOrderDetail(handleApiResponse(data));
},

View file

@ -0,0 +1,31 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { API_PATHS } from "@/api/api-paths";
const apiGet = vi.fn();
vi.mock("@/api/api", () => ({
apiGet: (...args: unknown[]) => apiGet(...args),
apiPost: vi.fn(),
}));
import { assetsApi } from "@/domain/assets/api/assets-api";
describe("assetsApi.getSiteOptions", () => {
beforeEach(() => {
apiGet.mockReset();
apiGet.mockResolvedValue({
Data: [{ Id: 1, Name: "Site A", Title: "Site A Title" }],
TotalCount: 1,
});
});
it("loads locations via Location/GetLocationList instead of Common/GetSitesName", async () => {
const sites = await assetsApi.getSiteOptions();
expect(apiGet).toHaveBeenCalledWith(API_PATHS.location.getList, {
searchParams: { search: "", page: 1, pageSize: 1000 },
});
expect(apiGet).not.toHaveBeenCalledWith("Common/GetSitesName", expect.anything());
expect(sites).toEqual([{ id: 1, name: "Site A", title: "Site A Title" }]);
});
});

View file

@ -0,0 +1,49 @@
import { describe, expect, it } from "vitest";
import { transformEmployee, transformToBackend } from "@/domain/employees/mappers/employee-mapper";
import type { Employee } from "@/domain/employees/types/employee";
describe("transformToBackend", () => {
it("sends JobTitleId instead of jobTitle string", () => {
const payload = transformToBackend({
firstName: "Jane",
lastName: "Doe",
email: "jane@example.com",
phone: "555-0100",
jobTitle: "Technician",
jobTitleId: 3,
} as Employee);
expect(payload.JobTitleId).toBe(3);
expect(payload.FirstName).toBe("Jane");
expect(payload.LastName).toBe("Doe");
expect(payload).not.toHaveProperty("jobTitle");
expect(payload).not.toHaveProperty("JobTitle");
});
it("includes Id for updates", () => {
const payload = transformToBackend({
id: 42,
firstName: "Jane",
lastName: "Doe",
jobTitleId: null,
} as Employee);
expect(payload.Id).toBe(42);
expect(payload.JobTitleId).toBeNull();
});
});
describe("transformEmployee", () => {
it("reads JobTitleId from PascalCase API response", () => {
const employee = transformEmployee({
Id: 1,
FirstName: "Jane",
LastName: "Doe",
JobTitle: "Technician",
JobTitleId: 5,
});
expect(employee?.jobTitleId).toBe(5);
expect(employee?.jobTitle).toBe("Technician");
});
});

View file

@ -0,0 +1,33 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { API_PATHS } from "@/api/api-paths";
const apiGet = vi.fn();
vi.mock("@/api/api", () => ({
apiGet: (...args: unknown[]) => apiGet(...args),
apiPost: vi.fn(),
apiDelete: vi.fn(),
apiPostNoContent: vi.fn(),
}));
import { workOrdersApi } from "@/domain/work-orders/api/work-orders-api";
describe("workOrdersApi.getById", () => {
beforeEach(() => {
apiGet.mockReset();
apiGet.mockResolvedValue({
Id: 10,
title: "Test WO",
status: "Open",
});
});
it("calls GetWorkorderById with id query param, not path suffix", async () => {
await workOrdersApi.getById(10);
expect(apiGet).toHaveBeenCalledWith(API_PATHS.workOrder.getById, {
searchParams: { id: 10 },
});
expect(apiGet).not.toHaveBeenCalledWith(`${API_PATHS.workOrder.getById}/10`, expect.anything());
});
});