mirror of
https://github.com/Sea-Haven-Industries/proposal-system.git
synced 2026-10-07 08:08:56 +00:00
Correctness:
- State-transition mutations now invalidate every cached view via
invalidateProposalViews (detail + line items + lists + stats + admin
dashboard) — approving no longer leaves a stale queue for the
5-minute staleTime
- Presigned S3 PUT moved to proposals/api.ts with res.ok check — a
rejected upload is no longer confirmed as uploaded
- toCustomerRequest always sends contactEmail ('' clears); API create
path normalizes empty->null to match the update path — customer
emails can now be cleared from the UI
- Shared Number-based numeric form fields (domain/shared/formFields):
'12abc' no longer silently coerces to 12 in the pricing library
- Customer create/update invalidate customersKeys.all so cached search
autocompletes see new customers
- AdminWorkspace clears dirty right after a successful implicit save,
before approve — no false unsaved-changes prompt when approve fails
- ProposalFormPage submit gate and missing-fields caption derive from
ONE checks list (missing customer is now listed)
- Empty states gated on !err in ProposalListPage/AdminDashboard — no
contradictory error + 'no proposals' UI
- VendorDataPanel migrated to useVendorProposals (kills the divergent
['vendorProposals', id] cache key and the inline apiClient query)
- useCustomerList/usePricingLibraryList get keepPreviousData — no
TablePagination out-of-range flash on page change
Cleanup:
- Dead speculative hooks removed (useCreate/BulkUpdate/DeleteLineItem,
useUpdateProposal, useProposalHistory/Audit, lineItemRowFormSchema,
toUpdateLineItemEntry); tests moved to the live save path
(useSaveProposalWorkspace)
- Shared useDebouncedValue hook replaces 4 drifted inline debounce
copies (one leaked its timer on unmount, two hardcoded 300ms);
DEBOUNCE_AUTOCOMPLETE=300 named
- Fix: WEB-H5 / WEB-H6 finding-ID markers restored at the relocated
onError handlers (CLAUDE.md traceability)
- shared/api-contracts gains an exports map; /schemas resolver alias
deduplicated from 3 copies to the tsconfig paths mapping
Verify: tsc clean, vitest 51/51 (tests updated to pin the new
invalidation/mapper behavior + new '12abc' rejection test),
vite build OK, dotnet 166/166.
50 lines
1.9 KiB
TypeScript
50 lines
1.9 KiB
TypeScript
// Customers domain — contract schema re-exports + customer form schema.
|
|
import { z } from 'zod';
|
|
import { createCustomerRequestSchema } from '@proposal-system/api-contracts/schemas';
|
|
import type { CreateCustomerRequest, UpdateCustomerRequest } from './types';
|
|
|
|
export {
|
|
customerSchema,
|
|
createCustomerRequestSchema,
|
|
updateCustomerRequestSchema,
|
|
} from '@proposal-system/api-contracts/schemas';
|
|
|
|
/** Simple email format check (client-side only) — mirrors CustomerManagementPage. */
|
|
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
|
|
/**
|
|
* Customer create/edit form. Derived from createCustomerRequestSchema — the
|
|
* form edits a single primary address string (the wire type takes
|
|
* addresses: string[]); toCustomerRequest() maps to the request shape.
|
|
*/
|
|
export const customerFormSchema = createCustomerRequestSchema
|
|
.pick({ name: true })
|
|
.extend({
|
|
name: z.string().trim().min(1, 'Name is required'),
|
|
address: z.string().trim().min(1, 'Address is required'),
|
|
contactEmail: z
|
|
.string()
|
|
.refine((v) => v.trim() === '' || EMAIL_RE.test(v.trim()), {
|
|
message: 'Enter a valid email address',
|
|
}),
|
|
});
|
|
|
|
export type CustomerFormValues = z.infer<typeof customerFormSchema>;
|
|
|
|
/**
|
|
* Map validated form values to the create/update request. The form edits the
|
|
* primary address; any additional existing addresses are preserved.
|
|
* contactEmail is always sent: the API clears the stored email on empty
|
|
* string (CustomerService normalizes '' → null), so omitting the key when the
|
|
* admin blanks the field would make the email impossible to clear.
|
|
*/
|
|
export function toCustomerRequest(
|
|
values: CustomerFormValues,
|
|
existingAddresses: string[] = []
|
|
): CreateCustomerRequest & UpdateCustomerRequest {
|
|
return {
|
|
name: values.name.trim(),
|
|
addresses: [values.address.trim(), ...existingAddresses.slice(1)],
|
|
contactEmail: values.contactEmail.trim(),
|
|
};
|
|
}
|