diff --git a/docs/FEATURE_PRIORITIZATION.md b/docs/FEATURE_PRIORITIZATION.md index bc97714b..28bbb432 100644 --- a/docs/FEATURE_PRIORITIZATION.md +++ b/docs/FEATURE_PRIORITIZATION.md @@ -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). --- diff --git a/src/api/api-paths.ts b/src/api/api-paths.ts index f209b08a..83a6b6aa 100644 --- a/src/api/api-paths.ts +++ b/src/api/api-paths.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", diff --git a/src/app/(protected)/employees/_components/employee-form.tsx b/src/app/(protected)/employees/_components/employee-form.tsx index 39db0c04..1a20929c 100644 --- a/src/app/(protected)/employees/_components/employee-form.tsx +++ b/src/app/(protected)/employees/_components/employee-form.tsx @@ -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, diff --git a/src/domain/assets/api/assets-api.ts b/src/domain/assets/api/assets-api.ts index 30d77fe1..b994e3f4 100644 --- a/src/domain/assets/api/assets-api.ts +++ b/src/domain/assets/api/assets-api.ts @@ -68,8 +68,10 @@ export const assetsApi = { }, getSiteOptions: async (): Promise => { - const data = await apiGet(API_PATHS.common.getSitesName); - const items = handleApiResponse(data); - return Array.isArray(items) ? items.map(mapSiteOption) : []; + const data = await apiGet(API_PATHS.location.getList, { + searchParams: { search: "", page: 1, pageSize: 1000 }, + }); + const paginated = handlePaginatedResponse(data, { page: 1, pageSize: 1000 }); + return paginated.items.map(mapSiteOption); }, }; diff --git a/src/domain/employees/mappers/employee-mapper.ts b/src/domain/employees/mappers/employee-mapper.ts index 974476e4..900ebc4d 100644 --- a/src/domain/employees/mappers/employee-mapper.ts +++ b/src/domain/employees/mappers/employee-mapper.ts @@ -21,6 +21,18 @@ function readId(record: Record): string | number { return ""; } +function readJobTitleId(record: Record): 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; 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 { - 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 = { + 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; } diff --git a/src/domain/employees/schemas/employee-schema.ts b/src/domain/employees/schemas/employee-schema.ts index 418b13fc..e34875a0 100644 --- a/src/domain/employees/schemas/employee-schema.ts +++ b/src/domain/employees/schemas/employee-schema.ts @@ -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: "", diff --git a/src/domain/employees/types/employee.ts b/src/domain/employees/types/employee.ts index f878c231..c281ebae 100644 --- a/src/domain/employees/types/employee.ts +++ b/src/domain/employees/types/employee.ts @@ -16,6 +16,7 @@ export interface Employee { email: string; phone: string; jobTitle: string; + jobTitleId?: number | null; role: string; status: string; hireDate: string; diff --git a/src/domain/work-orders/api/work-orders-api.ts b/src/domain/work-orders/api/work-orders-api.ts index f6d8c786..8591720b 100644 --- a/src/domain/work-orders/api/work-orders-api.ts +++ b/src/domain/work-orders/api/work-orders-api.ts @@ -99,7 +99,9 @@ export const workOrdersApi = { }, getById: async (id: string | number): Promise => { - const data = await apiGet(`${API_PATHS.workOrder.getById}/${id}`); + const data = await apiGet(API_PATHS.workOrder.getById, { + searchParams: { id }, + }); return mapWorkOrderDetail(handleApiResponse(data)); }, diff --git a/src/test/domain/assets/api/assets-api.test.ts b/src/test/domain/assets/api/assets-api.test.ts new file mode 100644 index 00000000..c1de6fee --- /dev/null +++ b/src/test/domain/assets/api/assets-api.test.ts @@ -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" }]); + }); +}); diff --git a/src/test/domain/employees/mappers/employee-mapper.test.ts b/src/test/domain/employees/mappers/employee-mapper.test.ts new file mode 100644 index 00000000..1632a57d --- /dev/null +++ b/src/test/domain/employees/mappers/employee-mapper.test.ts @@ -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"); + }); +}); diff --git a/src/test/domain/work-orders/api/work-orders-api.test.ts b/src/test/domain/work-orders/api/work-orders-api.test.ts new file mode 100644 index 00000000..32040a19 --- /dev/null +++ b/src/test/domain/work-orders/api/work-orders-api.test.ts @@ -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()); + }); +});