mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-09-30 09:13:11 +00:00
fix(vendors): preserve phone data across edits
This commit is contained in:
parent
922d6c9a57
commit
6ca45bc0ae
6 changed files with 73 additions and 27 deletions
2
e2e/vendors/vendors.spec.ts
vendored
2
e2e/vendors/vendors.spec.ts
vendored
|
|
@ -289,7 +289,7 @@ test.describe("Vendor directory prototype parity", () => {
|
|||
await page.getByLabel("Technician Name").fill("New Technician");
|
||||
await page.getByRole("combobox", { name: "Company" }).click();
|
||||
await page.getByRole("option", { name: "Gateway Plumbing" }).click();
|
||||
await expect(page.getByLabel("Company Phone")).toHaveValue("(314) 555-0100");
|
||||
await expect(page.getByLabel("Company Phone")).toHaveValue("314-555-0100");
|
||||
await expect(page.getByRole("textbox", { name: "Email" })).toHaveValue("dispatch@gateway.test");
|
||||
await expect(page.getByLabel("Address")).toHaveValue("1 Market St");
|
||||
await expect(page.getByLabel("Google Maps URL")).toHaveValue("https://maps.google.com/gateway");
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ export function VendorFormFields({
|
|||
label="Technician Phone"
|
||||
placeholder="(XXX) XXX-XXXX"
|
||||
inputMode="tel"
|
||||
value={formatPhoneMask(field.value ?? "")}
|
||||
value={field.value ?? ""}
|
||||
onChange={(event) => field.onChange(formatPhoneMask(event.target.value))}
|
||||
error={Boolean(errors.phone)}
|
||||
helperText={errors.phone?.message}
|
||||
|
|
@ -208,7 +208,7 @@ export function VendorFormFields({
|
|||
label="Company Phone"
|
||||
placeholder="(XXX) XXX-XXXX"
|
||||
inputMode="tel"
|
||||
value={formatPhoneMask(field.value ?? "")}
|
||||
value={field.value ?? ""}
|
||||
onChange={(event) => field.onChange(formatPhoneMask(event.target.value))}
|
||||
error={Boolean(errors.companyPhone)}
|
||||
helperText={errors.companyPhone?.message}
|
||||
|
|
|
|||
30
src/domain/vendors/mappers/vendor-mapper.ts
vendored
30
src/domain/vendors/mappers/vendor-mapper.ts
vendored
|
|
@ -51,6 +51,14 @@ function readCompanyId(record: Record<string, unknown>): string | number | null
|
|||
return null;
|
||||
}
|
||||
|
||||
function readNumericCompanyId(record: Record<string, unknown>): number | null {
|
||||
const value = readCompanyId(record);
|
||||
if (typeof value === "number") return value;
|
||||
if (typeof value !== "string" || value.trim() === "") return null;
|
||||
const parsed = Number(value);
|
||||
return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
|
||||
}
|
||||
|
||||
function readPreferredContact(
|
||||
record: Record<string, unknown>,
|
||||
...keys: string[]
|
||||
|
|
@ -72,10 +80,12 @@ function readPreferredContact(
|
|||
}
|
||||
|
||||
export function toCanonicalPhone(value: unknown): string {
|
||||
const digits = String(value ?? "")
|
||||
.replace(/\D+/g, "")
|
||||
.slice(0, 10);
|
||||
if (digits.length !== 10) return "";
|
||||
const original = String(value ?? "");
|
||||
let digits = original.replace(/\D+/g, "");
|
||||
if (digits.length === 11 && digits.startsWith("1")) {
|
||||
digits = digits.slice(1);
|
||||
}
|
||||
if (digits.length !== 10) return original;
|
||||
return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`;
|
||||
}
|
||||
|
||||
|
|
@ -95,8 +105,8 @@ export function mapVendorListItem(raw: unknown): VendorListItem {
|
|||
companyName: readString(item, "companyName", "CompanyName"),
|
||||
contactName: readString(item, "contactName", "ContactName"),
|
||||
email: readString(item, "email", "Email"),
|
||||
phone: toCanonicalPhone(item.phone ?? item.Phone),
|
||||
companyPhone: toCanonicalPhone(item.companyPhone ?? item.CompanyPhone),
|
||||
phone: readString(item, "phone", "Phone"),
|
||||
companyPhone: readString(item, "companyPhone", "CompanyPhone"),
|
||||
googleMapsUrl: readString(item, "googleMapsUrl", "GoogleMapsUrl"),
|
||||
notes: readString(item, "notes", "Notes"),
|
||||
totalJobs: readNumber(item, "totalJobs", "TotalJobs"),
|
||||
|
|
@ -120,8 +130,8 @@ export function mapVendor(raw: unknown): Vendor {
|
|||
companyName: readString(item, "companyName", "CompanyName"),
|
||||
contactName: readString(item, "contactName", "ContactName"),
|
||||
email: readString(item, "email", "Email"),
|
||||
phone: toCanonicalPhone(item.phone ?? item.Phone),
|
||||
companyPhone: toCanonicalPhone(item.companyPhone ?? item.CompanyPhone),
|
||||
phone: readString(item, "phone", "Phone"),
|
||||
companyPhone: readString(item, "companyPhone", "CompanyPhone"),
|
||||
googleMapsUrl: readString(item, "googleMapsUrl", "GoogleMapsUrl"),
|
||||
notes: readString(item, "notes", "Notes"),
|
||||
totalJobs: readNumber(item, "totalJobs", "TotalJobs"),
|
||||
|
|
@ -139,7 +149,7 @@ export function mapVendorToBackend(vendor: unknown): Record<string, unknown> {
|
|||
const item = asRecord(vendor);
|
||||
const companyName = readString(item, "companyName", "CompanyName");
|
||||
const payload: Record<string, unknown> = {
|
||||
companyId: readCompanyId(item),
|
||||
companyId: readNumericCompanyId(item),
|
||||
companyName,
|
||||
contactName: readString(item, "contactName", "ContactName"),
|
||||
email: readString(item, "email", "Email"),
|
||||
|
|
@ -167,7 +177,7 @@ function mapFacetCompany(raw: unknown): VendorFacetCompany {
|
|||
return {
|
||||
companyId: readCompanyId(item),
|
||||
name: readString(item, "name", "Name"),
|
||||
companyPhone: toCanonicalPhone(item.companyPhone ?? item.CompanyPhone),
|
||||
companyPhone: readString(item, "companyPhone", "CompanyPhone"),
|
||||
email: readString(item, "email", "Email"),
|
||||
address: readString(item, "address", "Address"),
|
||||
city: readString(item, "city", "City"),
|
||||
|
|
|
|||
15
src/domain/vendors/schemas/vendor-schema.ts
vendored
15
src/domain/vendors/schemas/vendor-schema.ts
vendored
|
|
@ -12,19 +12,18 @@ const httpsUrl = z
|
|||
"Google Maps URL must use HTTPS",
|
||||
);
|
||||
|
||||
const canonicalPhone = z
|
||||
.string()
|
||||
.refine(
|
||||
(value) => value === "" || /^\(\d{3}\) \d{3}-\d{4}$/.test(value),
|
||||
"Enter a 10-digit phone number",
|
||||
);
|
||||
const northAmericanPhone = z.string().refine((value) => {
|
||||
if (value === "") return true;
|
||||
const digits = value.replace(/\D+/g, "");
|
||||
return digits.length === 10 || (digits.length === 11 && digits.startsWith("1"));
|
||||
}, "Enter a 10-digit phone number");
|
||||
|
||||
export const vendorSchema = z.object({
|
||||
companyName: z.string().min(1, "Company is required"),
|
||||
contactName: z.string().min(1, "Technician name is required"),
|
||||
email: z.union([z.string().email("Invalid email"), z.literal("")]).optional(),
|
||||
phone: canonicalPhone.optional(),
|
||||
companyPhone: canonicalPhone.optional(),
|
||||
phone: northAmericanPhone.optional(),
|
||||
companyPhone: northAmericanPhone.optional(),
|
||||
googleMapsUrl: httpsUrl.optional(),
|
||||
notes: z.string().optional(),
|
||||
totalJobs: z.number().optional(),
|
||||
|
|
|
|||
44
src/test/domain/vendors/api/vendors-api.test.ts
vendored
44
src/test/domain/vendors/api/vendors-api.test.ts
vendored
|
|
@ -101,7 +101,7 @@ describe("vendorsApi.getList", () => {
|
|||
expect(apiGet).toHaveBeenCalledWith(API_PATHS.vendor.facets, { searchParams: {} });
|
||||
expect(facets.companies[0]).toMatchObject({
|
||||
name: "Gateway Plumbing",
|
||||
companyPhone: "(314) 555-0100",
|
||||
companyPhone: "314-555-0100",
|
||||
googleMapsUrl: "https://maps.google.com/gateway",
|
||||
});
|
||||
expect(facets.locations[0]?.label).toBe("St. Louis, MO");
|
||||
|
|
@ -126,10 +126,10 @@ describe("vendorsApi.getList", () => {
|
|||
|
||||
it("sends all editable prototype fields when creating and updating", async () => {
|
||||
const values = {
|
||||
companyId: "co-1",
|
||||
companyId: "42",
|
||||
companyName: "Gateway Plumbing",
|
||||
contactName: "Adam Whyte",
|
||||
phone: "314-555-0198",
|
||||
phone: "+1 314-555-0198",
|
||||
companyPhone: "314-555-0100",
|
||||
email: "dispatch@gateway.test",
|
||||
address: "1 Market St",
|
||||
|
|
@ -154,7 +154,7 @@ describe("vendorsApi.getList", () => {
|
|||
expect.objectContaining({
|
||||
companyName: "Gateway Plumbing",
|
||||
notes: values.notes,
|
||||
companyId: "co-1",
|
||||
companyId: 42,
|
||||
preferredContact: "Text",
|
||||
phone: "(314) 555-0198",
|
||||
companyPhone: "(314) 555-0100",
|
||||
|
|
@ -182,6 +182,42 @@ describe("vendorsApi.getList", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("does not send a non-numeric company id to the integer backend contract", async () => {
|
||||
apiPost.mockResolvedValueOnce({ data: {} });
|
||||
|
||||
await vendorsApi.create({
|
||||
companyId: "co-1",
|
||||
companyName: "Gateway Plumbing",
|
||||
contactName: "Adam Whyte",
|
||||
});
|
||||
|
||||
expect(apiPost).toHaveBeenCalledWith(
|
||||
API_PATHS.rest.vendors,
|
||||
expect.objectContaining({ companyId: null }),
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves a legacy phone on read and unrelated write", async () => {
|
||||
apiGet.mockResolvedValueOnce({
|
||||
data: {
|
||||
id: 7,
|
||||
companyName: "Legacy Vendor",
|
||||
contactName: "Taylor",
|
||||
phone: "555-1234 ext 9",
|
||||
},
|
||||
});
|
||||
apiPut.mockResolvedValueOnce({ data: {} });
|
||||
|
||||
const vendor = await vendorsApi.getById(7);
|
||||
expect(vendor.phone).toBe("555-1234 ext 9");
|
||||
|
||||
await vendorsApi.update(7, { ...vendor, notes: "Updated only the notes" });
|
||||
expect(apiPut).toHaveBeenCalledWith(
|
||||
`${API_PATHS.rest.vendors}/7`,
|
||||
expect.objectContaining({ phone: "555-1234 ext 9", notes: "Updated only the notes" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("maps the deactivation-impact payload including linked work orders", async () => {
|
||||
apiGet.mockResolvedValueOnce({
|
||||
data: {
|
||||
|
|
|
|||
|
|
@ -35,14 +35,15 @@ describe("vendorSchema", () => {
|
|||
).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts empty phones and canonical (XXX) XXX-XXXX phones, rejecting partials", () => {
|
||||
it("accepts canonicalizable North American phones and rejects partials", () => {
|
||||
expect(vendorSchema.safeParse({ ...validVendor, phone: "", companyPhone: "" }).success).toBe(
|
||||
true,
|
||||
);
|
||||
expect(vendorSchema.safeParse({ ...validVendor, companyPhone: "(314) 555-0100" }).success).toBe(
|
||||
true,
|
||||
);
|
||||
expect(vendorSchema.safeParse({ ...validVendor, phone: "314-555-0198" }).success).toBe(false);
|
||||
expect(vendorSchema.safeParse({ ...validVendor, phone: "314-555-0198" }).success).toBe(true);
|
||||
expect(vendorSchema.safeParse({ ...validVendor, phone: "+1 314 555 0198" }).success).toBe(true);
|
||||
expect(vendorSchema.safeParse({ ...validVendor, phone: "(314)" }).success).toBe(false);
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue