mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-06 01:52:04 +00:00
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:
parent
2a60c525ac
commit
708d946399
11 changed files with 151 additions and 43 deletions
|
|
@ -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).
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: "",
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ export interface Employee {
|
|||
email: string;
|
||||
phone: string;
|
||||
jobTitle: string;
|
||||
jobTitleId?: number | null;
|
||||
role: string;
|
||||
status: string;
|
||||
hireDate: string;
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
},
|
||||
|
||||
|
|
|
|||
31
src/test/domain/assets/api/assets-api.test.ts
Normal file
31
src/test/domain/assets/api/assets-api.test.ts
Normal 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" }]);
|
||||
});
|
||||
});
|
||||
49
src/test/domain/employees/mappers/employee-mapper.test.ts
Normal file
49
src/test/domain/employees/mappers/employee-mapper.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
33
src/test/domain/work-orders/api/work-orders-api.test.ts
Normal file
33
src/test/domain/work-orders/api/work-orders-api.test.ts
Normal 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());
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue