2026-06-18 12:49:47 -04:00
|
|
|
import { useState } from 'react';
|
refactor(web): domain-layer restructure (SHOC layering) + react-hook-form (#223)
* feat(web): adopt SHOC design system and shell layout (ADR 0003)
Port shoc-frontend-new dev's design system with its CSS-variable
single-token-source mechanism:
- src/styles/theme.css: SHOC token file ported verbatim (Montserrat/
DM Sans/JetBrains Mono, primary #1c75bc, navy #262262, full radius/
shadow/sidebar/header token layers); fonts self-hosted via @fontsource
- src/lib/theme/{css-vars,mui-theme}.ts: getCssVar -> createTheme
adapter mirroring SHOC's mui-theme.ts (palette, typography, shadows
tuple, component overrides; MUI v9 slot renames expressed as class
selectors); theme.ts is now a re-export
- Shell: SHOC composition (sidebar column + sticky gradient topbar +
scrolling main); sidebar 244px/76px collapse with brand header row,
grouped nav, SHOC active treatment (white card + 3px accent bar);
topbar 100-degree gradient, surface hamburger, gradient avatar pill
- Brand: SeahavenMark + BrandLockup ported (Tailwind re-expressed as
sx; wordmark subtitle localized to PROPOSAL SYSTEM)
- Login: SHOC auth-card treatment (centered 384px card on #f9fafb)
- Old "Sea Haven Ops" Inter/#2563EB theme and Nunito remnants removed;
remaining hardcoded hexes replaced with tokens; lucide-react for
shell/nav icons per SHOC convention
Verify: tsc clean, 26/26 vitest, vite build OK; Playwright screenshots
pixel-sampled against the extracted SHOC spec (all hard values exact,
no blocking deviations).
* feat(contracts): adopt shared api-contracts in web, add zod schemas and ProblemDetails codes
Closes WEB-M5 (web hand-duplicated wire types, standing drift risk):
- shared/api-contracts: rewritten as the authoritative superset of the
.NET DTOs (ProposalListItem/ProposalDetail with poNumber and
submittedByName, line item requests, customers, pricing library,
dashboard, audit, sites, auth, presigned upload, ApiProblem); stale
Proposal/UpdateLineItemsRequest shapes removed
- shared/api-contracts/src/schemas.ts: zod runtime schemas coupled to
every wire type via `satisfies z.ZodType<T>` (schema/type drift is now
a compile error); separate entrypoint so type-only consumers (mobile)
never pull zod
- web: imports @proposal-system/api-contracts (file: dep + tsconfig
paths + vite preserveSymlinks); all 7 lib/api modules re-export shared
types so page imports stay stable; enum unions tightened
(PricingLibraryPage form state now ServiceCategory-typed)
- fix(web): customer create/update sent a singular `address` field the
API silently dropped (contract is addresses: string[], CustomerDtos.cs)
- addresses now round-trip, extra addresses preserved on edit
- api: ProblemDetails responses carry a machine-readable top-level
`code` (SHOC error-code vocabulary): ValidationFailed,
InvalidStateTransition, NotFound, Unauthorized, InternalError; new
BusinessRuleException(code, message) maps to 422 with its code;
GlobalExceptionHandlerTests cover the full mapping (wire contract)
Cross-checked .NET DTOs vs TS types vs zod schemas with the
orchestrator scanner (Gemini): core domains consistent; internal-only
DTOs (FileDtos vendor/lambda surface, SimilarProposalDtos, UserDtos
admin surface) intentionally uncovered.
Verify: dotnet 166/166, web tsc + vitest 26/26 + build, mobile tsc,
shared tsc all green.
* feat(web): domain-layer conventions doc + react-hook-form deps (Phase 4 prep)
* feat(web): scaffold domain module layer (proposals, lineItems, customers, pricingLibrary, admin, sites)
Additive-only: pages still use lib/api/* and constants/queryKeys.ts until
the page-migration agents run. Each domain ships api.ts (HTTP moved from
lib/api), types.ts (contract re-exports + view types), schemas.ts (contract
schema re-exports + form schemas with toRequest mappers), and use-cases.ts
(TanStack Query v5 hooks + hierarchical query keys, mirroring current page
invalidations and toast-on-error behavior).
Adds an explicit vite/vitest alias for the
@proposal-system/api-contracts/schemas subpath (package has no exports map)
plus a schema/mapper smoke test suite.
* refactor(web): proposal pages on domain layer, proposal form on react-hook-form
* refactor(web): admin workspace on domain layer
* refactor(web): customer management on domain layer + react-hook-form
* refactor(web): pricing library on domain layer + react-hook-form
* refactor(web): dashboards on domain layer
* test(web): domain use-case hook coverage
* refactor(web): finish domain-layer integration — migrate straggler components, delete legacy api modules
- SimilarProposalsPanel -> useSimilarProposals (domain/admin); inline
SimilarProposal type replaced by domain/admin/types (identical shape);
query key joins the admin hierarchical key space
- LineItemEditor type imports -> domain/lineItems/types
- Delete now-orphaned lib/api/{proposals,lineItems,customers,
pricingLibrary,admin,sites}.ts, constants/queryKeys.ts,
hooks/usePaginatedList.ts (lib/api/client.ts + auth.ts stay per
domain README rule 5)
Verify: tsc clean, vitest 52/52, build OK, Playwright smoke of the
authed shell renders on domain hooks.
* fix(web): apply Phase 4 code-review findings (10 correctness + 4 cleanup)
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.
2026-07-13 20:11:11 -04:00
|
|
|
import { Controller, useForm, type DefaultValues } from 'react-hook-form';
|
|
|
|
|
import { zodResolver } from '@hookform/resolvers/zod';
|
2026-06-18 12:49:47 -04:00
|
|
|
import {
|
|
|
|
|
Box,
|
|
|
|
|
Typography,
|
|
|
|
|
Card,
|
|
|
|
|
CardContent,
|
|
|
|
|
Table,
|
|
|
|
|
TableBody,
|
|
|
|
|
TableCell,
|
|
|
|
|
TableContainer,
|
|
|
|
|
TableHead,
|
|
|
|
|
TableRow,
|
|
|
|
|
TablePagination,
|
|
|
|
|
TextField,
|
|
|
|
|
Button,
|
|
|
|
|
Skeleton,
|
|
|
|
|
IconButton,
|
|
|
|
|
Tooltip,
|
|
|
|
|
Dialog,
|
|
|
|
|
DialogTitle,
|
|
|
|
|
DialogContent,
|
|
|
|
|
DialogActions,
|
|
|
|
|
Alert,
|
|
|
|
|
Chip,
|
|
|
|
|
MenuItem,
|
|
|
|
|
} from '@mui/material';
|
|
|
|
|
import AddIcon from '@mui/icons-material/Add';
|
|
|
|
|
import EditIcon from '@mui/icons-material/Edit';
|
|
|
|
|
import DeleteIcon from '@mui/icons-material/Delete';
|
2026-06-22 16:02:43 -04:00
|
|
|
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutlined';
|
2026-06-18 12:49:47 -04:00
|
|
|
import {
|
refactor(web): domain-layer restructure (SHOC layering) + react-hook-form (#223)
* feat(web): adopt SHOC design system and shell layout (ADR 0003)
Port shoc-frontend-new dev's design system with its CSS-variable
single-token-source mechanism:
- src/styles/theme.css: SHOC token file ported verbatim (Montserrat/
DM Sans/JetBrains Mono, primary #1c75bc, navy #262262, full radius/
shadow/sidebar/header token layers); fonts self-hosted via @fontsource
- src/lib/theme/{css-vars,mui-theme}.ts: getCssVar -> createTheme
adapter mirroring SHOC's mui-theme.ts (palette, typography, shadows
tuple, component overrides; MUI v9 slot renames expressed as class
selectors); theme.ts is now a re-export
- Shell: SHOC composition (sidebar column + sticky gradient topbar +
scrolling main); sidebar 244px/76px collapse with brand header row,
grouped nav, SHOC active treatment (white card + 3px accent bar);
topbar 100-degree gradient, surface hamburger, gradient avatar pill
- Brand: SeahavenMark + BrandLockup ported (Tailwind re-expressed as
sx; wordmark subtitle localized to PROPOSAL SYSTEM)
- Login: SHOC auth-card treatment (centered 384px card on #f9fafb)
- Old "Sea Haven Ops" Inter/#2563EB theme and Nunito remnants removed;
remaining hardcoded hexes replaced with tokens; lucide-react for
shell/nav icons per SHOC convention
Verify: tsc clean, 26/26 vitest, vite build OK; Playwright screenshots
pixel-sampled against the extracted SHOC spec (all hard values exact,
no blocking deviations).
* feat(contracts): adopt shared api-contracts in web, add zod schemas and ProblemDetails codes
Closes WEB-M5 (web hand-duplicated wire types, standing drift risk):
- shared/api-contracts: rewritten as the authoritative superset of the
.NET DTOs (ProposalListItem/ProposalDetail with poNumber and
submittedByName, line item requests, customers, pricing library,
dashboard, audit, sites, auth, presigned upload, ApiProblem); stale
Proposal/UpdateLineItemsRequest shapes removed
- shared/api-contracts/src/schemas.ts: zod runtime schemas coupled to
every wire type via `satisfies z.ZodType<T>` (schema/type drift is now
a compile error); separate entrypoint so type-only consumers (mobile)
never pull zod
- web: imports @proposal-system/api-contracts (file: dep + tsconfig
paths + vite preserveSymlinks); all 7 lib/api modules re-export shared
types so page imports stay stable; enum unions tightened
(PricingLibraryPage form state now ServiceCategory-typed)
- fix(web): customer create/update sent a singular `address` field the
API silently dropped (contract is addresses: string[], CustomerDtos.cs)
- addresses now round-trip, extra addresses preserved on edit
- api: ProblemDetails responses carry a machine-readable top-level
`code` (SHOC error-code vocabulary): ValidationFailed,
InvalidStateTransition, NotFound, Unauthorized, InternalError; new
BusinessRuleException(code, message) maps to 422 with its code;
GlobalExceptionHandlerTests cover the full mapping (wire contract)
Cross-checked .NET DTOs vs TS types vs zod schemas with the
orchestrator scanner (Gemini): core domains consistent; internal-only
DTOs (FileDtos vendor/lambda surface, SimilarProposalDtos, UserDtos
admin surface) intentionally uncovered.
Verify: dotnet 166/166, web tsc + vitest 26/26 + build, mobile tsc,
shared tsc all green.
* feat(web): domain-layer conventions doc + react-hook-form deps (Phase 4 prep)
* feat(web): scaffold domain module layer (proposals, lineItems, customers, pricingLibrary, admin, sites)
Additive-only: pages still use lib/api/* and constants/queryKeys.ts until
the page-migration agents run. Each domain ships api.ts (HTTP moved from
lib/api), types.ts (contract re-exports + view types), schemas.ts (contract
schema re-exports + form schemas with toRequest mappers), and use-cases.ts
(TanStack Query v5 hooks + hierarchical query keys, mirroring current page
invalidations and toast-on-error behavior).
Adds an explicit vite/vitest alias for the
@proposal-system/api-contracts/schemas subpath (package has no exports map)
plus a schema/mapper smoke test suite.
* refactor(web): proposal pages on domain layer, proposal form on react-hook-form
* refactor(web): admin workspace on domain layer
* refactor(web): customer management on domain layer + react-hook-form
* refactor(web): pricing library on domain layer + react-hook-form
* refactor(web): dashboards on domain layer
* test(web): domain use-case hook coverage
* refactor(web): finish domain-layer integration — migrate straggler components, delete legacy api modules
- SimilarProposalsPanel -> useSimilarProposals (domain/admin); inline
SimilarProposal type replaced by domain/admin/types (identical shape);
query key joins the admin hierarchical key space
- LineItemEditor type imports -> domain/lineItems/types
- Delete now-orphaned lib/api/{proposals,lineItems,customers,
pricingLibrary,admin,sites}.ts, constants/queryKeys.ts,
hooks/usePaginatedList.ts (lib/api/client.ts + auth.ts stay per
domain README rule 5)
Verify: tsc clean, vitest 52/52, build OK, Playwright smoke of the
authed shell renders on domain hooks.
* fix(web): apply Phase 4 code-review findings (10 correctness + 4 cleanup)
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.
2026-07-13 20:11:11 -04:00
|
|
|
usePricingLibraryList,
|
|
|
|
|
useCreatePricingLibraryItem,
|
|
|
|
|
useUpdatePricingLibraryItem,
|
|
|
|
|
useDeletePricingLibraryItem,
|
|
|
|
|
} from '../../../domain/pricingLibrary/use-cases';
|
|
|
|
|
import type { PricingLibraryItem } from '../../../domain/pricingLibrary/types';
|
|
|
|
|
import {
|
|
|
|
|
pricingLibraryItemFormSchema,
|
|
|
|
|
toPricingLibraryItemRequest,
|
|
|
|
|
type PricingLibraryItemFormValues,
|
|
|
|
|
} from '../../../domain/pricingLibrary/schemas';
|
2026-06-18 12:49:47 -04:00
|
|
|
import { DEFAULT_PAGE, DEFAULT_PAGE_SIZE, SERVICE_CATEGORIES } from '../../../constants';
|
|
|
|
|
import { useDocumentTitle } from '../../../hooks/useDocumentTitle';
|
|
|
|
|
|
|
|
|
|
const COL_COUNT = 6; // Title, Category, Unit, Unit Price, Source, Actions
|
|
|
|
|
|
|
|
|
|
/** Chip color mapping for service categories. */
|
|
|
|
|
const CATEGORY_CHIP_STYLES: Record<string, { bgcolor: string; color: string; border: string }> = {
|
|
|
|
|
HVAC: { bgcolor: '#E0F2FE', color: '#075985', border: '1px solid #7DD3FC' },
|
|
|
|
|
Plumbing: { bgcolor: '#DBEAFE', color: '#1E40AF', border: '1px solid #BFDBFE' },
|
|
|
|
|
Electrical: { bgcolor: '#FEF9C3', color: '#854D0E', border: '1px solid #FDE68A' },
|
|
|
|
|
General: { bgcolor: '#F3F4F6', color: '#374151', border: '1px solid #D1D5DB' },
|
|
|
|
|
Renovation: { bgcolor: '#F3E8FF', color: '#6B21A8', border: '1px solid #C084FC' },
|
|
|
|
|
Other: { bgcolor: '#F1F5F9', color: '#475569', border: '1px solid #CBD5E1' },
|
|
|
|
|
};
|
|
|
|
|
|
refactor(web): domain-layer restructure (SHOC layering) + react-hook-form (#223)
* feat(web): adopt SHOC design system and shell layout (ADR 0003)
Port shoc-frontend-new dev's design system with its CSS-variable
single-token-source mechanism:
- src/styles/theme.css: SHOC token file ported verbatim (Montserrat/
DM Sans/JetBrains Mono, primary #1c75bc, navy #262262, full radius/
shadow/sidebar/header token layers); fonts self-hosted via @fontsource
- src/lib/theme/{css-vars,mui-theme}.ts: getCssVar -> createTheme
adapter mirroring SHOC's mui-theme.ts (palette, typography, shadows
tuple, component overrides; MUI v9 slot renames expressed as class
selectors); theme.ts is now a re-export
- Shell: SHOC composition (sidebar column + sticky gradient topbar +
scrolling main); sidebar 244px/76px collapse with brand header row,
grouped nav, SHOC active treatment (white card + 3px accent bar);
topbar 100-degree gradient, surface hamburger, gradient avatar pill
- Brand: SeahavenMark + BrandLockup ported (Tailwind re-expressed as
sx; wordmark subtitle localized to PROPOSAL SYSTEM)
- Login: SHOC auth-card treatment (centered 384px card on #f9fafb)
- Old "Sea Haven Ops" Inter/#2563EB theme and Nunito remnants removed;
remaining hardcoded hexes replaced with tokens; lucide-react for
shell/nav icons per SHOC convention
Verify: tsc clean, 26/26 vitest, vite build OK; Playwright screenshots
pixel-sampled against the extracted SHOC spec (all hard values exact,
no blocking deviations).
* feat(contracts): adopt shared api-contracts in web, add zod schemas and ProblemDetails codes
Closes WEB-M5 (web hand-duplicated wire types, standing drift risk):
- shared/api-contracts: rewritten as the authoritative superset of the
.NET DTOs (ProposalListItem/ProposalDetail with poNumber and
submittedByName, line item requests, customers, pricing library,
dashboard, audit, sites, auth, presigned upload, ApiProblem); stale
Proposal/UpdateLineItemsRequest shapes removed
- shared/api-contracts/src/schemas.ts: zod runtime schemas coupled to
every wire type via `satisfies z.ZodType<T>` (schema/type drift is now
a compile error); separate entrypoint so type-only consumers (mobile)
never pull zod
- web: imports @proposal-system/api-contracts (file: dep + tsconfig
paths + vite preserveSymlinks); all 7 lib/api modules re-export shared
types so page imports stay stable; enum unions tightened
(PricingLibraryPage form state now ServiceCategory-typed)
- fix(web): customer create/update sent a singular `address` field the
API silently dropped (contract is addresses: string[], CustomerDtos.cs)
- addresses now round-trip, extra addresses preserved on edit
- api: ProblemDetails responses carry a machine-readable top-level
`code` (SHOC error-code vocabulary): ValidationFailed,
InvalidStateTransition, NotFound, Unauthorized, InternalError; new
BusinessRuleException(code, message) maps to 422 with its code;
GlobalExceptionHandlerTests cover the full mapping (wire contract)
Cross-checked .NET DTOs vs TS types vs zod schemas with the
orchestrator scanner (Gemini): core domains consistent; internal-only
DTOs (FileDtos vendor/lambda surface, SimilarProposalDtos, UserDtos
admin surface) intentionally uncovered.
Verify: dotnet 166/166, web tsc + vitest 26/26 + build, mobile tsc,
shared tsc all green.
* feat(web): domain-layer conventions doc + react-hook-form deps (Phase 4 prep)
* feat(web): scaffold domain module layer (proposals, lineItems, customers, pricingLibrary, admin, sites)
Additive-only: pages still use lib/api/* and constants/queryKeys.ts until
the page-migration agents run. Each domain ships api.ts (HTTP moved from
lib/api), types.ts (contract re-exports + view types), schemas.ts (contract
schema re-exports + form schemas with toRequest mappers), and use-cases.ts
(TanStack Query v5 hooks + hierarchical query keys, mirroring current page
invalidations and toast-on-error behavior).
Adds an explicit vite/vitest alias for the
@proposal-system/api-contracts/schemas subpath (package has no exports map)
plus a schema/mapper smoke test suite.
* refactor(web): proposal pages on domain layer, proposal form on react-hook-form
* refactor(web): admin workspace on domain layer
* refactor(web): customer management on domain layer + react-hook-form
* refactor(web): pricing library on domain layer + react-hook-form
* refactor(web): dashboards on domain layer
* test(web): domain use-case hook coverage
* refactor(web): finish domain-layer integration — migrate straggler components, delete legacy api modules
- SimilarProposalsPanel -> useSimilarProposals (domain/admin); inline
SimilarProposal type replaced by domain/admin/types (identical shape);
query key joins the admin hierarchical key space
- LineItemEditor type imports -> domain/lineItems/types
- Delete now-orphaned lib/api/{proposals,lineItems,customers,
pricingLibrary,admin,sites}.ts, constants/queryKeys.ts,
hooks/usePaginatedList.ts (lib/api/client.ts + auth.ts stay per
domain README rule 5)
Verify: tsc clean, vitest 52/52, build OK, Playwright smoke of the
authed shell renders on domain hooks.
* fix(web): apply Phase 4 code-review findings (10 correctness + 4 cleanup)
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.
2026-07-13 20:11:11 -04:00
|
|
|
/**
|
|
|
|
|
* serviceCategory stays union-typed (ServiceCategory) in the form schema, so the
|
|
|
|
|
* create defaults leave it unset rather than using an out-of-union '' sentinel.
|
|
|
|
|
*/
|
|
|
|
|
const EMPTY_FORM_VALUES: DefaultValues<PricingLibraryItemFormValues> = {
|
2026-06-18 12:49:47 -04:00
|
|
|
title: '',
|
|
|
|
|
unit: '',
|
|
|
|
|
unitPrice: '',
|
|
|
|
|
keywords: '',
|
|
|
|
|
description: '',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** Format a number as $X.XX or return "—" for null/undefined. */
|
|
|
|
|
function formatPrice(value: number | null | undefined): string {
|
|
|
|
|
if (value == null) return '—';
|
|
|
|
|
return `$${value.toFixed(2)}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default function PricingLibraryPage() {
|
|
|
|
|
useDocumentTitle('Pricing Library');
|
|
|
|
|
|
|
|
|
|
// --- List state ---
|
|
|
|
|
const [page, setPage] = useState(DEFAULT_PAGE);
|
|
|
|
|
const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE);
|
|
|
|
|
|
refactor(web): domain-layer restructure (SHOC layering) + react-hook-form (#223)
* feat(web): adopt SHOC design system and shell layout (ADR 0003)
Port shoc-frontend-new dev's design system with its CSS-variable
single-token-source mechanism:
- src/styles/theme.css: SHOC token file ported verbatim (Montserrat/
DM Sans/JetBrains Mono, primary #1c75bc, navy #262262, full radius/
shadow/sidebar/header token layers); fonts self-hosted via @fontsource
- src/lib/theme/{css-vars,mui-theme}.ts: getCssVar -> createTheme
adapter mirroring SHOC's mui-theme.ts (palette, typography, shadows
tuple, component overrides; MUI v9 slot renames expressed as class
selectors); theme.ts is now a re-export
- Shell: SHOC composition (sidebar column + sticky gradient topbar +
scrolling main); sidebar 244px/76px collapse with brand header row,
grouped nav, SHOC active treatment (white card + 3px accent bar);
topbar 100-degree gradient, surface hamburger, gradient avatar pill
- Brand: SeahavenMark + BrandLockup ported (Tailwind re-expressed as
sx; wordmark subtitle localized to PROPOSAL SYSTEM)
- Login: SHOC auth-card treatment (centered 384px card on #f9fafb)
- Old "Sea Haven Ops" Inter/#2563EB theme and Nunito remnants removed;
remaining hardcoded hexes replaced with tokens; lucide-react for
shell/nav icons per SHOC convention
Verify: tsc clean, 26/26 vitest, vite build OK; Playwright screenshots
pixel-sampled against the extracted SHOC spec (all hard values exact,
no blocking deviations).
* feat(contracts): adopt shared api-contracts in web, add zod schemas and ProblemDetails codes
Closes WEB-M5 (web hand-duplicated wire types, standing drift risk):
- shared/api-contracts: rewritten as the authoritative superset of the
.NET DTOs (ProposalListItem/ProposalDetail with poNumber and
submittedByName, line item requests, customers, pricing library,
dashboard, audit, sites, auth, presigned upload, ApiProblem); stale
Proposal/UpdateLineItemsRequest shapes removed
- shared/api-contracts/src/schemas.ts: zod runtime schemas coupled to
every wire type via `satisfies z.ZodType<T>` (schema/type drift is now
a compile error); separate entrypoint so type-only consumers (mobile)
never pull zod
- web: imports @proposal-system/api-contracts (file: dep + tsconfig
paths + vite preserveSymlinks); all 7 lib/api modules re-export shared
types so page imports stay stable; enum unions tightened
(PricingLibraryPage form state now ServiceCategory-typed)
- fix(web): customer create/update sent a singular `address` field the
API silently dropped (contract is addresses: string[], CustomerDtos.cs)
- addresses now round-trip, extra addresses preserved on edit
- api: ProblemDetails responses carry a machine-readable top-level
`code` (SHOC error-code vocabulary): ValidationFailed,
InvalidStateTransition, NotFound, Unauthorized, InternalError; new
BusinessRuleException(code, message) maps to 422 with its code;
GlobalExceptionHandlerTests cover the full mapping (wire contract)
Cross-checked .NET DTOs vs TS types vs zod schemas with the
orchestrator scanner (Gemini): core domains consistent; internal-only
DTOs (FileDtos vendor/lambda surface, SimilarProposalDtos, UserDtos
admin surface) intentionally uncovered.
Verify: dotnet 166/166, web tsc + vitest 26/26 + build, mobile tsc,
shared tsc all green.
* feat(web): domain-layer conventions doc + react-hook-form deps (Phase 4 prep)
* feat(web): scaffold domain module layer (proposals, lineItems, customers, pricingLibrary, admin, sites)
Additive-only: pages still use lib/api/* and constants/queryKeys.ts until
the page-migration agents run. Each domain ships api.ts (HTTP moved from
lib/api), types.ts (contract re-exports + view types), schemas.ts (contract
schema re-exports + form schemas with toRequest mappers), and use-cases.ts
(TanStack Query v5 hooks + hierarchical query keys, mirroring current page
invalidations and toast-on-error behavior).
Adds an explicit vite/vitest alias for the
@proposal-system/api-contracts/schemas subpath (package has no exports map)
plus a schema/mapper smoke test suite.
* refactor(web): proposal pages on domain layer, proposal form on react-hook-form
* refactor(web): admin workspace on domain layer
* refactor(web): customer management on domain layer + react-hook-form
* refactor(web): pricing library on domain layer + react-hook-form
* refactor(web): dashboards on domain layer
* test(web): domain use-case hook coverage
* refactor(web): finish domain-layer integration — migrate straggler components, delete legacy api modules
- SimilarProposalsPanel -> useSimilarProposals (domain/admin); inline
SimilarProposal type replaced by domain/admin/types (identical shape);
query key joins the admin hierarchical key space
- LineItemEditor type imports -> domain/lineItems/types
- Delete now-orphaned lib/api/{proposals,lineItems,customers,
pricingLibrary,admin,sites}.ts, constants/queryKeys.ts,
hooks/usePaginatedList.ts (lib/api/client.ts + auth.ts stay per
domain README rule 5)
Verify: tsc clean, vitest 52/52, build OK, Playwright smoke of the
authed shell renders on domain hooks.
* fix(web): apply Phase 4 code-review findings (10 correctness + 4 cleanup)
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.
2026-07-13 20:11:11 -04:00
|
|
|
const { data, isLoading, isError, refetch } = usePricingLibraryList({ page, pageSize });
|
2026-06-18 12:49:47 -04:00
|
|
|
|
|
|
|
|
const rows = data?.items ?? [];
|
|
|
|
|
const totalCount = data?.totalCount ?? 0;
|
|
|
|
|
|
|
|
|
|
// --- Create/Edit dialog state ---
|
|
|
|
|
const [dialogOpen, setDialogOpen] = useState(false);
|
|
|
|
|
const [editingItem, setEditingItem] = useState<PricingLibraryItem | null>(null);
|
|
|
|
|
|
|
|
|
|
const isEditing = editingItem !== null;
|
|
|
|
|
|
refactor(web): domain-layer restructure (SHOC layering) + react-hook-form (#223)
* feat(web): adopt SHOC design system and shell layout (ADR 0003)
Port shoc-frontend-new dev's design system with its CSS-variable
single-token-source mechanism:
- src/styles/theme.css: SHOC token file ported verbatim (Montserrat/
DM Sans/JetBrains Mono, primary #1c75bc, navy #262262, full radius/
shadow/sidebar/header token layers); fonts self-hosted via @fontsource
- src/lib/theme/{css-vars,mui-theme}.ts: getCssVar -> createTheme
adapter mirroring SHOC's mui-theme.ts (palette, typography, shadows
tuple, component overrides; MUI v9 slot renames expressed as class
selectors); theme.ts is now a re-export
- Shell: SHOC composition (sidebar column + sticky gradient topbar +
scrolling main); sidebar 244px/76px collapse with brand header row,
grouped nav, SHOC active treatment (white card + 3px accent bar);
topbar 100-degree gradient, surface hamburger, gradient avatar pill
- Brand: SeahavenMark + BrandLockup ported (Tailwind re-expressed as
sx; wordmark subtitle localized to PROPOSAL SYSTEM)
- Login: SHOC auth-card treatment (centered 384px card on #f9fafb)
- Old "Sea Haven Ops" Inter/#2563EB theme and Nunito remnants removed;
remaining hardcoded hexes replaced with tokens; lucide-react for
shell/nav icons per SHOC convention
Verify: tsc clean, 26/26 vitest, vite build OK; Playwright screenshots
pixel-sampled against the extracted SHOC spec (all hard values exact,
no blocking deviations).
* feat(contracts): adopt shared api-contracts in web, add zod schemas and ProblemDetails codes
Closes WEB-M5 (web hand-duplicated wire types, standing drift risk):
- shared/api-contracts: rewritten as the authoritative superset of the
.NET DTOs (ProposalListItem/ProposalDetail with poNumber and
submittedByName, line item requests, customers, pricing library,
dashboard, audit, sites, auth, presigned upload, ApiProblem); stale
Proposal/UpdateLineItemsRequest shapes removed
- shared/api-contracts/src/schemas.ts: zod runtime schemas coupled to
every wire type via `satisfies z.ZodType<T>` (schema/type drift is now
a compile error); separate entrypoint so type-only consumers (mobile)
never pull zod
- web: imports @proposal-system/api-contracts (file: dep + tsconfig
paths + vite preserveSymlinks); all 7 lib/api modules re-export shared
types so page imports stay stable; enum unions tightened
(PricingLibraryPage form state now ServiceCategory-typed)
- fix(web): customer create/update sent a singular `address` field the
API silently dropped (contract is addresses: string[], CustomerDtos.cs)
- addresses now round-trip, extra addresses preserved on edit
- api: ProblemDetails responses carry a machine-readable top-level
`code` (SHOC error-code vocabulary): ValidationFailed,
InvalidStateTransition, NotFound, Unauthorized, InternalError; new
BusinessRuleException(code, message) maps to 422 with its code;
GlobalExceptionHandlerTests cover the full mapping (wire contract)
Cross-checked .NET DTOs vs TS types vs zod schemas with the
orchestrator scanner (Gemini): core domains consistent; internal-only
DTOs (FileDtos vendor/lambda surface, SimilarProposalDtos, UserDtos
admin surface) intentionally uncovered.
Verify: dotnet 166/166, web tsc + vitest 26/26 + build, mobile tsc,
shared tsc all green.
* feat(web): domain-layer conventions doc + react-hook-form deps (Phase 4 prep)
* feat(web): scaffold domain module layer (proposals, lineItems, customers, pricingLibrary, admin, sites)
Additive-only: pages still use lib/api/* and constants/queryKeys.ts until
the page-migration agents run. Each domain ships api.ts (HTTP moved from
lib/api), types.ts (contract re-exports + view types), schemas.ts (contract
schema re-exports + form schemas with toRequest mappers), and use-cases.ts
(TanStack Query v5 hooks + hierarchical query keys, mirroring current page
invalidations and toast-on-error behavior).
Adds an explicit vite/vitest alias for the
@proposal-system/api-contracts/schemas subpath (package has no exports map)
plus a schema/mapper smoke test suite.
* refactor(web): proposal pages on domain layer, proposal form on react-hook-form
* refactor(web): admin workspace on domain layer
* refactor(web): customer management on domain layer + react-hook-form
* refactor(web): pricing library on domain layer + react-hook-form
* refactor(web): dashboards on domain layer
* test(web): domain use-case hook coverage
* refactor(web): finish domain-layer integration — migrate straggler components, delete legacy api modules
- SimilarProposalsPanel -> useSimilarProposals (domain/admin); inline
SimilarProposal type replaced by domain/admin/types (identical shape);
query key joins the admin hierarchical key space
- LineItemEditor type imports -> domain/lineItems/types
- Delete now-orphaned lib/api/{proposals,lineItems,customers,
pricingLibrary,admin,sites}.ts, constants/queryKeys.ts,
hooks/usePaginatedList.ts (lib/api/client.ts + auth.ts stay per
domain README rule 5)
Verify: tsc clean, vitest 52/52, build OK, Playwright smoke of the
authed shell renders on domain hooks.
* fix(web): apply Phase 4 code-review findings (10 correctness + 4 cleanup)
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.
2026-07-13 20:11:11 -04:00
|
|
|
const {
|
|
|
|
|
control,
|
|
|
|
|
handleSubmit,
|
|
|
|
|
reset,
|
|
|
|
|
formState: { errors },
|
|
|
|
|
} = useForm<PricingLibraryItemFormValues>({
|
|
|
|
|
resolver: zodResolver(pricingLibraryItemFormSchema),
|
|
|
|
|
defaultValues: EMPTY_FORM_VALUES,
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-18 12:49:47 -04:00
|
|
|
const openCreateDialog = () => {
|
|
|
|
|
setEditingItem(null);
|
refactor(web): domain-layer restructure (SHOC layering) + react-hook-form (#223)
* feat(web): adopt SHOC design system and shell layout (ADR 0003)
Port shoc-frontend-new dev's design system with its CSS-variable
single-token-source mechanism:
- src/styles/theme.css: SHOC token file ported verbatim (Montserrat/
DM Sans/JetBrains Mono, primary #1c75bc, navy #262262, full radius/
shadow/sidebar/header token layers); fonts self-hosted via @fontsource
- src/lib/theme/{css-vars,mui-theme}.ts: getCssVar -> createTheme
adapter mirroring SHOC's mui-theme.ts (palette, typography, shadows
tuple, component overrides; MUI v9 slot renames expressed as class
selectors); theme.ts is now a re-export
- Shell: SHOC composition (sidebar column + sticky gradient topbar +
scrolling main); sidebar 244px/76px collapse with brand header row,
grouped nav, SHOC active treatment (white card + 3px accent bar);
topbar 100-degree gradient, surface hamburger, gradient avatar pill
- Brand: SeahavenMark + BrandLockup ported (Tailwind re-expressed as
sx; wordmark subtitle localized to PROPOSAL SYSTEM)
- Login: SHOC auth-card treatment (centered 384px card on #f9fafb)
- Old "Sea Haven Ops" Inter/#2563EB theme and Nunito remnants removed;
remaining hardcoded hexes replaced with tokens; lucide-react for
shell/nav icons per SHOC convention
Verify: tsc clean, 26/26 vitest, vite build OK; Playwright screenshots
pixel-sampled against the extracted SHOC spec (all hard values exact,
no blocking deviations).
* feat(contracts): adopt shared api-contracts in web, add zod schemas and ProblemDetails codes
Closes WEB-M5 (web hand-duplicated wire types, standing drift risk):
- shared/api-contracts: rewritten as the authoritative superset of the
.NET DTOs (ProposalListItem/ProposalDetail with poNumber and
submittedByName, line item requests, customers, pricing library,
dashboard, audit, sites, auth, presigned upload, ApiProblem); stale
Proposal/UpdateLineItemsRequest shapes removed
- shared/api-contracts/src/schemas.ts: zod runtime schemas coupled to
every wire type via `satisfies z.ZodType<T>` (schema/type drift is now
a compile error); separate entrypoint so type-only consumers (mobile)
never pull zod
- web: imports @proposal-system/api-contracts (file: dep + tsconfig
paths + vite preserveSymlinks); all 7 lib/api modules re-export shared
types so page imports stay stable; enum unions tightened
(PricingLibraryPage form state now ServiceCategory-typed)
- fix(web): customer create/update sent a singular `address` field the
API silently dropped (contract is addresses: string[], CustomerDtos.cs)
- addresses now round-trip, extra addresses preserved on edit
- api: ProblemDetails responses carry a machine-readable top-level
`code` (SHOC error-code vocabulary): ValidationFailed,
InvalidStateTransition, NotFound, Unauthorized, InternalError; new
BusinessRuleException(code, message) maps to 422 with its code;
GlobalExceptionHandlerTests cover the full mapping (wire contract)
Cross-checked .NET DTOs vs TS types vs zod schemas with the
orchestrator scanner (Gemini): core domains consistent; internal-only
DTOs (FileDtos vendor/lambda surface, SimilarProposalDtos, UserDtos
admin surface) intentionally uncovered.
Verify: dotnet 166/166, web tsc + vitest 26/26 + build, mobile tsc,
shared tsc all green.
* feat(web): domain-layer conventions doc + react-hook-form deps (Phase 4 prep)
* feat(web): scaffold domain module layer (proposals, lineItems, customers, pricingLibrary, admin, sites)
Additive-only: pages still use lib/api/* and constants/queryKeys.ts until
the page-migration agents run. Each domain ships api.ts (HTTP moved from
lib/api), types.ts (contract re-exports + view types), schemas.ts (contract
schema re-exports + form schemas with toRequest mappers), and use-cases.ts
(TanStack Query v5 hooks + hierarchical query keys, mirroring current page
invalidations and toast-on-error behavior).
Adds an explicit vite/vitest alias for the
@proposal-system/api-contracts/schemas subpath (package has no exports map)
plus a schema/mapper smoke test suite.
* refactor(web): proposal pages on domain layer, proposal form on react-hook-form
* refactor(web): admin workspace on domain layer
* refactor(web): customer management on domain layer + react-hook-form
* refactor(web): pricing library on domain layer + react-hook-form
* refactor(web): dashboards on domain layer
* test(web): domain use-case hook coverage
* refactor(web): finish domain-layer integration — migrate straggler components, delete legacy api modules
- SimilarProposalsPanel -> useSimilarProposals (domain/admin); inline
SimilarProposal type replaced by domain/admin/types (identical shape);
query key joins the admin hierarchical key space
- LineItemEditor type imports -> domain/lineItems/types
- Delete now-orphaned lib/api/{proposals,lineItems,customers,
pricingLibrary,admin,sites}.ts, constants/queryKeys.ts,
hooks/usePaginatedList.ts (lib/api/client.ts + auth.ts stay per
domain README rule 5)
Verify: tsc clean, vitest 52/52, build OK, Playwright smoke of the
authed shell renders on domain hooks.
* fix(web): apply Phase 4 code-review findings (10 correctness + 4 cleanup)
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.
2026-07-13 20:11:11 -04:00
|
|
|
reset(EMPTY_FORM_VALUES);
|
2026-06-18 12:49:47 -04:00
|
|
|
setDialogOpen(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const openEditDialog = (item: PricingLibraryItem) => {
|
|
|
|
|
setEditingItem(item);
|
refactor(web): domain-layer restructure (SHOC layering) + react-hook-form (#223)
* feat(web): adopt SHOC design system and shell layout (ADR 0003)
Port shoc-frontend-new dev's design system with its CSS-variable
single-token-source mechanism:
- src/styles/theme.css: SHOC token file ported verbatim (Montserrat/
DM Sans/JetBrains Mono, primary #1c75bc, navy #262262, full radius/
shadow/sidebar/header token layers); fonts self-hosted via @fontsource
- src/lib/theme/{css-vars,mui-theme}.ts: getCssVar -> createTheme
adapter mirroring SHOC's mui-theme.ts (palette, typography, shadows
tuple, component overrides; MUI v9 slot renames expressed as class
selectors); theme.ts is now a re-export
- Shell: SHOC composition (sidebar column + sticky gradient topbar +
scrolling main); sidebar 244px/76px collapse with brand header row,
grouped nav, SHOC active treatment (white card + 3px accent bar);
topbar 100-degree gradient, surface hamburger, gradient avatar pill
- Brand: SeahavenMark + BrandLockup ported (Tailwind re-expressed as
sx; wordmark subtitle localized to PROPOSAL SYSTEM)
- Login: SHOC auth-card treatment (centered 384px card on #f9fafb)
- Old "Sea Haven Ops" Inter/#2563EB theme and Nunito remnants removed;
remaining hardcoded hexes replaced with tokens; lucide-react for
shell/nav icons per SHOC convention
Verify: tsc clean, 26/26 vitest, vite build OK; Playwright screenshots
pixel-sampled against the extracted SHOC spec (all hard values exact,
no blocking deviations).
* feat(contracts): adopt shared api-contracts in web, add zod schemas and ProblemDetails codes
Closes WEB-M5 (web hand-duplicated wire types, standing drift risk):
- shared/api-contracts: rewritten as the authoritative superset of the
.NET DTOs (ProposalListItem/ProposalDetail with poNumber and
submittedByName, line item requests, customers, pricing library,
dashboard, audit, sites, auth, presigned upload, ApiProblem); stale
Proposal/UpdateLineItemsRequest shapes removed
- shared/api-contracts/src/schemas.ts: zod runtime schemas coupled to
every wire type via `satisfies z.ZodType<T>` (schema/type drift is now
a compile error); separate entrypoint so type-only consumers (mobile)
never pull zod
- web: imports @proposal-system/api-contracts (file: dep + tsconfig
paths + vite preserveSymlinks); all 7 lib/api modules re-export shared
types so page imports stay stable; enum unions tightened
(PricingLibraryPage form state now ServiceCategory-typed)
- fix(web): customer create/update sent a singular `address` field the
API silently dropped (contract is addresses: string[], CustomerDtos.cs)
- addresses now round-trip, extra addresses preserved on edit
- api: ProblemDetails responses carry a machine-readable top-level
`code` (SHOC error-code vocabulary): ValidationFailed,
InvalidStateTransition, NotFound, Unauthorized, InternalError; new
BusinessRuleException(code, message) maps to 422 with its code;
GlobalExceptionHandlerTests cover the full mapping (wire contract)
Cross-checked .NET DTOs vs TS types vs zod schemas with the
orchestrator scanner (Gemini): core domains consistent; internal-only
DTOs (FileDtos vendor/lambda surface, SimilarProposalDtos, UserDtos
admin surface) intentionally uncovered.
Verify: dotnet 166/166, web tsc + vitest 26/26 + build, mobile tsc,
shared tsc all green.
* feat(web): domain-layer conventions doc + react-hook-form deps (Phase 4 prep)
* feat(web): scaffold domain module layer (proposals, lineItems, customers, pricingLibrary, admin, sites)
Additive-only: pages still use lib/api/* and constants/queryKeys.ts until
the page-migration agents run. Each domain ships api.ts (HTTP moved from
lib/api), types.ts (contract re-exports + view types), schemas.ts (contract
schema re-exports + form schemas with toRequest mappers), and use-cases.ts
(TanStack Query v5 hooks + hierarchical query keys, mirroring current page
invalidations and toast-on-error behavior).
Adds an explicit vite/vitest alias for the
@proposal-system/api-contracts/schemas subpath (package has no exports map)
plus a schema/mapper smoke test suite.
* refactor(web): proposal pages on domain layer, proposal form on react-hook-form
* refactor(web): admin workspace on domain layer
* refactor(web): customer management on domain layer + react-hook-form
* refactor(web): pricing library on domain layer + react-hook-form
* refactor(web): dashboards on domain layer
* test(web): domain use-case hook coverage
* refactor(web): finish domain-layer integration — migrate straggler components, delete legacy api modules
- SimilarProposalsPanel -> useSimilarProposals (domain/admin); inline
SimilarProposal type replaced by domain/admin/types (identical shape);
query key joins the admin hierarchical key space
- LineItemEditor type imports -> domain/lineItems/types
- Delete now-orphaned lib/api/{proposals,lineItems,customers,
pricingLibrary,admin,sites}.ts, constants/queryKeys.ts,
hooks/usePaginatedList.ts (lib/api/client.ts + auth.ts stay per
domain README rule 5)
Verify: tsc clean, vitest 52/52, build OK, Playwright smoke of the
authed shell renders on domain hooks.
* fix(web): apply Phase 4 code-review findings (10 correctness + 4 cleanup)
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.
2026-07-13 20:11:11 -04:00
|
|
|
reset({
|
2026-06-18 12:49:47 -04:00
|
|
|
title: item.title,
|
|
|
|
|
serviceCategory: item.serviceCategory,
|
|
|
|
|
unit: item.unit ?? '',
|
|
|
|
|
unitPrice: item.unitPrice != null ? String(item.unitPrice) : '',
|
|
|
|
|
keywords: item.keywords ?? '',
|
|
|
|
|
description: item.description ?? '',
|
|
|
|
|
});
|
|
|
|
|
setDialogOpen(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const closeDialog = () => {
|
|
|
|
|
setDialogOpen(false);
|
|
|
|
|
setEditingItem(null);
|
refactor(web): domain-layer restructure (SHOC layering) + react-hook-form (#223)
* feat(web): adopt SHOC design system and shell layout (ADR 0003)
Port shoc-frontend-new dev's design system with its CSS-variable
single-token-source mechanism:
- src/styles/theme.css: SHOC token file ported verbatim (Montserrat/
DM Sans/JetBrains Mono, primary #1c75bc, navy #262262, full radius/
shadow/sidebar/header token layers); fonts self-hosted via @fontsource
- src/lib/theme/{css-vars,mui-theme}.ts: getCssVar -> createTheme
adapter mirroring SHOC's mui-theme.ts (palette, typography, shadows
tuple, component overrides; MUI v9 slot renames expressed as class
selectors); theme.ts is now a re-export
- Shell: SHOC composition (sidebar column + sticky gradient topbar +
scrolling main); sidebar 244px/76px collapse with brand header row,
grouped nav, SHOC active treatment (white card + 3px accent bar);
topbar 100-degree gradient, surface hamburger, gradient avatar pill
- Brand: SeahavenMark + BrandLockup ported (Tailwind re-expressed as
sx; wordmark subtitle localized to PROPOSAL SYSTEM)
- Login: SHOC auth-card treatment (centered 384px card on #f9fafb)
- Old "Sea Haven Ops" Inter/#2563EB theme and Nunito remnants removed;
remaining hardcoded hexes replaced with tokens; lucide-react for
shell/nav icons per SHOC convention
Verify: tsc clean, 26/26 vitest, vite build OK; Playwright screenshots
pixel-sampled against the extracted SHOC spec (all hard values exact,
no blocking deviations).
* feat(contracts): adopt shared api-contracts in web, add zod schemas and ProblemDetails codes
Closes WEB-M5 (web hand-duplicated wire types, standing drift risk):
- shared/api-contracts: rewritten as the authoritative superset of the
.NET DTOs (ProposalListItem/ProposalDetail with poNumber and
submittedByName, line item requests, customers, pricing library,
dashboard, audit, sites, auth, presigned upload, ApiProblem); stale
Proposal/UpdateLineItemsRequest shapes removed
- shared/api-contracts/src/schemas.ts: zod runtime schemas coupled to
every wire type via `satisfies z.ZodType<T>` (schema/type drift is now
a compile error); separate entrypoint so type-only consumers (mobile)
never pull zod
- web: imports @proposal-system/api-contracts (file: dep + tsconfig
paths + vite preserveSymlinks); all 7 lib/api modules re-export shared
types so page imports stay stable; enum unions tightened
(PricingLibraryPage form state now ServiceCategory-typed)
- fix(web): customer create/update sent a singular `address` field the
API silently dropped (contract is addresses: string[], CustomerDtos.cs)
- addresses now round-trip, extra addresses preserved on edit
- api: ProblemDetails responses carry a machine-readable top-level
`code` (SHOC error-code vocabulary): ValidationFailed,
InvalidStateTransition, NotFound, Unauthorized, InternalError; new
BusinessRuleException(code, message) maps to 422 with its code;
GlobalExceptionHandlerTests cover the full mapping (wire contract)
Cross-checked .NET DTOs vs TS types vs zod schemas with the
orchestrator scanner (Gemini): core domains consistent; internal-only
DTOs (FileDtos vendor/lambda surface, SimilarProposalDtos, UserDtos
admin surface) intentionally uncovered.
Verify: dotnet 166/166, web tsc + vitest 26/26 + build, mobile tsc,
shared tsc all green.
* feat(web): domain-layer conventions doc + react-hook-form deps (Phase 4 prep)
* feat(web): scaffold domain module layer (proposals, lineItems, customers, pricingLibrary, admin, sites)
Additive-only: pages still use lib/api/* and constants/queryKeys.ts until
the page-migration agents run. Each domain ships api.ts (HTTP moved from
lib/api), types.ts (contract re-exports + view types), schemas.ts (contract
schema re-exports + form schemas with toRequest mappers), and use-cases.ts
(TanStack Query v5 hooks + hierarchical query keys, mirroring current page
invalidations and toast-on-error behavior).
Adds an explicit vite/vitest alias for the
@proposal-system/api-contracts/schemas subpath (package has no exports map)
plus a schema/mapper smoke test suite.
* refactor(web): proposal pages on domain layer, proposal form on react-hook-form
* refactor(web): admin workspace on domain layer
* refactor(web): customer management on domain layer + react-hook-form
* refactor(web): pricing library on domain layer + react-hook-form
* refactor(web): dashboards on domain layer
* test(web): domain use-case hook coverage
* refactor(web): finish domain-layer integration — migrate straggler components, delete legacy api modules
- SimilarProposalsPanel -> useSimilarProposals (domain/admin); inline
SimilarProposal type replaced by domain/admin/types (identical shape);
query key joins the admin hierarchical key space
- LineItemEditor type imports -> domain/lineItems/types
- Delete now-orphaned lib/api/{proposals,lineItems,customers,
pricingLibrary,admin,sites}.ts, constants/queryKeys.ts,
hooks/usePaginatedList.ts (lib/api/client.ts + auth.ts stay per
domain README rule 5)
Verify: tsc clean, vitest 52/52, build OK, Playwright smoke of the
authed shell renders on domain hooks.
* fix(web): apply Phase 4 code-review findings (10 correctness + 4 cleanup)
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.
2026-07-13 20:11:11 -04:00
|
|
|
reset(EMPTY_FORM_VALUES);
|
2026-06-18 12:49:47 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// --- Delete confirm dialog state ---
|
|
|
|
|
const [deleteTarget, setDeleteTarget] = useState<PricingLibraryItem | null>(null);
|
|
|
|
|
|
|
|
|
|
const openDeleteDialog = (item: PricingLibraryItem) => {
|
|
|
|
|
setDeleteTarget(item);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const closeDeleteDialog = () => {
|
|
|
|
|
setDeleteTarget(null);
|
|
|
|
|
};
|
|
|
|
|
|
refactor(web): domain-layer restructure (SHOC layering) + react-hook-form (#223)
* feat(web): adopt SHOC design system and shell layout (ADR 0003)
Port shoc-frontend-new dev's design system with its CSS-variable
single-token-source mechanism:
- src/styles/theme.css: SHOC token file ported verbatim (Montserrat/
DM Sans/JetBrains Mono, primary #1c75bc, navy #262262, full radius/
shadow/sidebar/header token layers); fonts self-hosted via @fontsource
- src/lib/theme/{css-vars,mui-theme}.ts: getCssVar -> createTheme
adapter mirroring SHOC's mui-theme.ts (palette, typography, shadows
tuple, component overrides; MUI v9 slot renames expressed as class
selectors); theme.ts is now a re-export
- Shell: SHOC composition (sidebar column + sticky gradient topbar +
scrolling main); sidebar 244px/76px collapse with brand header row,
grouped nav, SHOC active treatment (white card + 3px accent bar);
topbar 100-degree gradient, surface hamburger, gradient avatar pill
- Brand: SeahavenMark + BrandLockup ported (Tailwind re-expressed as
sx; wordmark subtitle localized to PROPOSAL SYSTEM)
- Login: SHOC auth-card treatment (centered 384px card on #f9fafb)
- Old "Sea Haven Ops" Inter/#2563EB theme and Nunito remnants removed;
remaining hardcoded hexes replaced with tokens; lucide-react for
shell/nav icons per SHOC convention
Verify: tsc clean, 26/26 vitest, vite build OK; Playwright screenshots
pixel-sampled against the extracted SHOC spec (all hard values exact,
no blocking deviations).
* feat(contracts): adopt shared api-contracts in web, add zod schemas and ProblemDetails codes
Closes WEB-M5 (web hand-duplicated wire types, standing drift risk):
- shared/api-contracts: rewritten as the authoritative superset of the
.NET DTOs (ProposalListItem/ProposalDetail with poNumber and
submittedByName, line item requests, customers, pricing library,
dashboard, audit, sites, auth, presigned upload, ApiProblem); stale
Proposal/UpdateLineItemsRequest shapes removed
- shared/api-contracts/src/schemas.ts: zod runtime schemas coupled to
every wire type via `satisfies z.ZodType<T>` (schema/type drift is now
a compile error); separate entrypoint so type-only consumers (mobile)
never pull zod
- web: imports @proposal-system/api-contracts (file: dep + tsconfig
paths + vite preserveSymlinks); all 7 lib/api modules re-export shared
types so page imports stay stable; enum unions tightened
(PricingLibraryPage form state now ServiceCategory-typed)
- fix(web): customer create/update sent a singular `address` field the
API silently dropped (contract is addresses: string[], CustomerDtos.cs)
- addresses now round-trip, extra addresses preserved on edit
- api: ProblemDetails responses carry a machine-readable top-level
`code` (SHOC error-code vocabulary): ValidationFailed,
InvalidStateTransition, NotFound, Unauthorized, InternalError; new
BusinessRuleException(code, message) maps to 422 with its code;
GlobalExceptionHandlerTests cover the full mapping (wire contract)
Cross-checked .NET DTOs vs TS types vs zod schemas with the
orchestrator scanner (Gemini): core domains consistent; internal-only
DTOs (FileDtos vendor/lambda surface, SimilarProposalDtos, UserDtos
admin surface) intentionally uncovered.
Verify: dotnet 166/166, web tsc + vitest 26/26 + build, mobile tsc,
shared tsc all green.
* feat(web): domain-layer conventions doc + react-hook-form deps (Phase 4 prep)
* feat(web): scaffold domain module layer (proposals, lineItems, customers, pricingLibrary, admin, sites)
Additive-only: pages still use lib/api/* and constants/queryKeys.ts until
the page-migration agents run. Each domain ships api.ts (HTTP moved from
lib/api), types.ts (contract re-exports + view types), schemas.ts (contract
schema re-exports + form schemas with toRequest mappers), and use-cases.ts
(TanStack Query v5 hooks + hierarchical query keys, mirroring current page
invalidations and toast-on-error behavior).
Adds an explicit vite/vitest alias for the
@proposal-system/api-contracts/schemas subpath (package has no exports map)
plus a schema/mapper smoke test suite.
* refactor(web): proposal pages on domain layer, proposal form on react-hook-form
* refactor(web): admin workspace on domain layer
* refactor(web): customer management on domain layer + react-hook-form
* refactor(web): pricing library on domain layer + react-hook-form
* refactor(web): dashboards on domain layer
* test(web): domain use-case hook coverage
* refactor(web): finish domain-layer integration — migrate straggler components, delete legacy api modules
- SimilarProposalsPanel -> useSimilarProposals (domain/admin); inline
SimilarProposal type replaced by domain/admin/types (identical shape);
query key joins the admin hierarchical key space
- LineItemEditor type imports -> domain/lineItems/types
- Delete now-orphaned lib/api/{proposals,lineItems,customers,
pricingLibrary,admin,sites}.ts, constants/queryKeys.ts,
hooks/usePaginatedList.ts (lib/api/client.ts + auth.ts stay per
domain README rule 5)
Verify: tsc clean, vitest 52/52, build OK, Playwright smoke of the
authed shell renders on domain hooks.
* fix(web): apply Phase 4 code-review findings (10 correctness + 4 cleanup)
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.
2026-07-13 20:11:11 -04:00
|
|
|
// --- Mutations (domain hooks own invalidation + toasts; dialog close stays here) ---
|
|
|
|
|
const createMutation = useCreatePricingLibraryItem();
|
|
|
|
|
const updateMutation = useUpdatePricingLibraryItem();
|
|
|
|
|
const deleteMutation = useDeletePricingLibraryItem();
|
|
|
|
|
|
|
|
|
|
const onSubmit = handleSubmit((values) => {
|
|
|
|
|
const payload = toPricingLibraryItemRequest(values);
|
|
|
|
|
if (editingItem) {
|
|
|
|
|
updateMutation.mutate(
|
|
|
|
|
{ id: editingItem.id, data: payload },
|
|
|
|
|
{ onSuccess: () => closeDialog() }
|
|
|
|
|
);
|
2026-06-18 12:49:47 -04:00
|
|
|
} else {
|
refactor(web): domain-layer restructure (SHOC layering) + react-hook-form (#223)
* feat(web): adopt SHOC design system and shell layout (ADR 0003)
Port shoc-frontend-new dev's design system with its CSS-variable
single-token-source mechanism:
- src/styles/theme.css: SHOC token file ported verbatim (Montserrat/
DM Sans/JetBrains Mono, primary #1c75bc, navy #262262, full radius/
shadow/sidebar/header token layers); fonts self-hosted via @fontsource
- src/lib/theme/{css-vars,mui-theme}.ts: getCssVar -> createTheme
adapter mirroring SHOC's mui-theme.ts (palette, typography, shadows
tuple, component overrides; MUI v9 slot renames expressed as class
selectors); theme.ts is now a re-export
- Shell: SHOC composition (sidebar column + sticky gradient topbar +
scrolling main); sidebar 244px/76px collapse with brand header row,
grouped nav, SHOC active treatment (white card + 3px accent bar);
topbar 100-degree gradient, surface hamburger, gradient avatar pill
- Brand: SeahavenMark + BrandLockup ported (Tailwind re-expressed as
sx; wordmark subtitle localized to PROPOSAL SYSTEM)
- Login: SHOC auth-card treatment (centered 384px card on #f9fafb)
- Old "Sea Haven Ops" Inter/#2563EB theme and Nunito remnants removed;
remaining hardcoded hexes replaced with tokens; lucide-react for
shell/nav icons per SHOC convention
Verify: tsc clean, 26/26 vitest, vite build OK; Playwright screenshots
pixel-sampled against the extracted SHOC spec (all hard values exact,
no blocking deviations).
* feat(contracts): adopt shared api-contracts in web, add zod schemas and ProblemDetails codes
Closes WEB-M5 (web hand-duplicated wire types, standing drift risk):
- shared/api-contracts: rewritten as the authoritative superset of the
.NET DTOs (ProposalListItem/ProposalDetail with poNumber and
submittedByName, line item requests, customers, pricing library,
dashboard, audit, sites, auth, presigned upload, ApiProblem); stale
Proposal/UpdateLineItemsRequest shapes removed
- shared/api-contracts/src/schemas.ts: zod runtime schemas coupled to
every wire type via `satisfies z.ZodType<T>` (schema/type drift is now
a compile error); separate entrypoint so type-only consumers (mobile)
never pull zod
- web: imports @proposal-system/api-contracts (file: dep + tsconfig
paths + vite preserveSymlinks); all 7 lib/api modules re-export shared
types so page imports stay stable; enum unions tightened
(PricingLibraryPage form state now ServiceCategory-typed)
- fix(web): customer create/update sent a singular `address` field the
API silently dropped (contract is addresses: string[], CustomerDtos.cs)
- addresses now round-trip, extra addresses preserved on edit
- api: ProblemDetails responses carry a machine-readable top-level
`code` (SHOC error-code vocabulary): ValidationFailed,
InvalidStateTransition, NotFound, Unauthorized, InternalError; new
BusinessRuleException(code, message) maps to 422 with its code;
GlobalExceptionHandlerTests cover the full mapping (wire contract)
Cross-checked .NET DTOs vs TS types vs zod schemas with the
orchestrator scanner (Gemini): core domains consistent; internal-only
DTOs (FileDtos vendor/lambda surface, SimilarProposalDtos, UserDtos
admin surface) intentionally uncovered.
Verify: dotnet 166/166, web tsc + vitest 26/26 + build, mobile tsc,
shared tsc all green.
* feat(web): domain-layer conventions doc + react-hook-form deps (Phase 4 prep)
* feat(web): scaffold domain module layer (proposals, lineItems, customers, pricingLibrary, admin, sites)
Additive-only: pages still use lib/api/* and constants/queryKeys.ts until
the page-migration agents run. Each domain ships api.ts (HTTP moved from
lib/api), types.ts (contract re-exports + view types), schemas.ts (contract
schema re-exports + form schemas with toRequest mappers), and use-cases.ts
(TanStack Query v5 hooks + hierarchical query keys, mirroring current page
invalidations and toast-on-error behavior).
Adds an explicit vite/vitest alias for the
@proposal-system/api-contracts/schemas subpath (package has no exports map)
plus a schema/mapper smoke test suite.
* refactor(web): proposal pages on domain layer, proposal form on react-hook-form
* refactor(web): admin workspace on domain layer
* refactor(web): customer management on domain layer + react-hook-form
* refactor(web): pricing library on domain layer + react-hook-form
* refactor(web): dashboards on domain layer
* test(web): domain use-case hook coverage
* refactor(web): finish domain-layer integration — migrate straggler components, delete legacy api modules
- SimilarProposalsPanel -> useSimilarProposals (domain/admin); inline
SimilarProposal type replaced by domain/admin/types (identical shape);
query key joins the admin hierarchical key space
- LineItemEditor type imports -> domain/lineItems/types
- Delete now-orphaned lib/api/{proposals,lineItems,customers,
pricingLibrary,admin,sites}.ts, constants/queryKeys.ts,
hooks/usePaginatedList.ts (lib/api/client.ts + auth.ts stay per
domain README rule 5)
Verify: tsc clean, vitest 52/52, build OK, Playwright smoke of the
authed shell renders on domain hooks.
* fix(web): apply Phase 4 code-review findings (10 correctness + 4 cleanup)
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.
2026-07-13 20:11:11 -04:00
|
|
|
createMutation.mutate(payload, { onSuccess: () => closeDialog() });
|
2026-06-18 12:49:47 -04:00
|
|
|
}
|
refactor(web): domain-layer restructure (SHOC layering) + react-hook-form (#223)
* feat(web): adopt SHOC design system and shell layout (ADR 0003)
Port shoc-frontend-new dev's design system with its CSS-variable
single-token-source mechanism:
- src/styles/theme.css: SHOC token file ported verbatim (Montserrat/
DM Sans/JetBrains Mono, primary #1c75bc, navy #262262, full radius/
shadow/sidebar/header token layers); fonts self-hosted via @fontsource
- src/lib/theme/{css-vars,mui-theme}.ts: getCssVar -> createTheme
adapter mirroring SHOC's mui-theme.ts (palette, typography, shadows
tuple, component overrides; MUI v9 slot renames expressed as class
selectors); theme.ts is now a re-export
- Shell: SHOC composition (sidebar column + sticky gradient topbar +
scrolling main); sidebar 244px/76px collapse with brand header row,
grouped nav, SHOC active treatment (white card + 3px accent bar);
topbar 100-degree gradient, surface hamburger, gradient avatar pill
- Brand: SeahavenMark + BrandLockup ported (Tailwind re-expressed as
sx; wordmark subtitle localized to PROPOSAL SYSTEM)
- Login: SHOC auth-card treatment (centered 384px card on #f9fafb)
- Old "Sea Haven Ops" Inter/#2563EB theme and Nunito remnants removed;
remaining hardcoded hexes replaced with tokens; lucide-react for
shell/nav icons per SHOC convention
Verify: tsc clean, 26/26 vitest, vite build OK; Playwright screenshots
pixel-sampled against the extracted SHOC spec (all hard values exact,
no blocking deviations).
* feat(contracts): adopt shared api-contracts in web, add zod schemas and ProblemDetails codes
Closes WEB-M5 (web hand-duplicated wire types, standing drift risk):
- shared/api-contracts: rewritten as the authoritative superset of the
.NET DTOs (ProposalListItem/ProposalDetail with poNumber and
submittedByName, line item requests, customers, pricing library,
dashboard, audit, sites, auth, presigned upload, ApiProblem); stale
Proposal/UpdateLineItemsRequest shapes removed
- shared/api-contracts/src/schemas.ts: zod runtime schemas coupled to
every wire type via `satisfies z.ZodType<T>` (schema/type drift is now
a compile error); separate entrypoint so type-only consumers (mobile)
never pull zod
- web: imports @proposal-system/api-contracts (file: dep + tsconfig
paths + vite preserveSymlinks); all 7 lib/api modules re-export shared
types so page imports stay stable; enum unions tightened
(PricingLibraryPage form state now ServiceCategory-typed)
- fix(web): customer create/update sent a singular `address` field the
API silently dropped (contract is addresses: string[], CustomerDtos.cs)
- addresses now round-trip, extra addresses preserved on edit
- api: ProblemDetails responses carry a machine-readable top-level
`code` (SHOC error-code vocabulary): ValidationFailed,
InvalidStateTransition, NotFound, Unauthorized, InternalError; new
BusinessRuleException(code, message) maps to 422 with its code;
GlobalExceptionHandlerTests cover the full mapping (wire contract)
Cross-checked .NET DTOs vs TS types vs zod schemas with the
orchestrator scanner (Gemini): core domains consistent; internal-only
DTOs (FileDtos vendor/lambda surface, SimilarProposalDtos, UserDtos
admin surface) intentionally uncovered.
Verify: dotnet 166/166, web tsc + vitest 26/26 + build, mobile tsc,
shared tsc all green.
* feat(web): domain-layer conventions doc + react-hook-form deps (Phase 4 prep)
* feat(web): scaffold domain module layer (proposals, lineItems, customers, pricingLibrary, admin, sites)
Additive-only: pages still use lib/api/* and constants/queryKeys.ts until
the page-migration agents run. Each domain ships api.ts (HTTP moved from
lib/api), types.ts (contract re-exports + view types), schemas.ts (contract
schema re-exports + form schemas with toRequest mappers), and use-cases.ts
(TanStack Query v5 hooks + hierarchical query keys, mirroring current page
invalidations and toast-on-error behavior).
Adds an explicit vite/vitest alias for the
@proposal-system/api-contracts/schemas subpath (package has no exports map)
plus a schema/mapper smoke test suite.
* refactor(web): proposal pages on domain layer, proposal form on react-hook-form
* refactor(web): admin workspace on domain layer
* refactor(web): customer management on domain layer + react-hook-form
* refactor(web): pricing library on domain layer + react-hook-form
* refactor(web): dashboards on domain layer
* test(web): domain use-case hook coverage
* refactor(web): finish domain-layer integration — migrate straggler components, delete legacy api modules
- SimilarProposalsPanel -> useSimilarProposals (domain/admin); inline
SimilarProposal type replaced by domain/admin/types (identical shape);
query key joins the admin hierarchical key space
- LineItemEditor type imports -> domain/lineItems/types
- Delete now-orphaned lib/api/{proposals,lineItems,customers,
pricingLibrary,admin,sites}.ts, constants/queryKeys.ts,
hooks/usePaginatedList.ts (lib/api/client.ts + auth.ts stay per
domain README rule 5)
Verify: tsc clean, vitest 52/52, build OK, Playwright smoke of the
authed shell renders on domain hooks.
* fix(web): apply Phase 4 code-review findings (10 correctness + 4 cleanup)
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.
2026-07-13 20:11:11 -04:00
|
|
|
});
|
2026-06-18 12:49:47 -04:00
|
|
|
|
|
|
|
|
const handleDeleteConfirm = () => {
|
|
|
|
|
if (deleteTarget) {
|
refactor(web): domain-layer restructure (SHOC layering) + react-hook-form (#223)
* feat(web): adopt SHOC design system and shell layout (ADR 0003)
Port shoc-frontend-new dev's design system with its CSS-variable
single-token-source mechanism:
- src/styles/theme.css: SHOC token file ported verbatim (Montserrat/
DM Sans/JetBrains Mono, primary #1c75bc, navy #262262, full radius/
shadow/sidebar/header token layers); fonts self-hosted via @fontsource
- src/lib/theme/{css-vars,mui-theme}.ts: getCssVar -> createTheme
adapter mirroring SHOC's mui-theme.ts (palette, typography, shadows
tuple, component overrides; MUI v9 slot renames expressed as class
selectors); theme.ts is now a re-export
- Shell: SHOC composition (sidebar column + sticky gradient topbar +
scrolling main); sidebar 244px/76px collapse with brand header row,
grouped nav, SHOC active treatment (white card + 3px accent bar);
topbar 100-degree gradient, surface hamburger, gradient avatar pill
- Brand: SeahavenMark + BrandLockup ported (Tailwind re-expressed as
sx; wordmark subtitle localized to PROPOSAL SYSTEM)
- Login: SHOC auth-card treatment (centered 384px card on #f9fafb)
- Old "Sea Haven Ops" Inter/#2563EB theme and Nunito remnants removed;
remaining hardcoded hexes replaced with tokens; lucide-react for
shell/nav icons per SHOC convention
Verify: tsc clean, 26/26 vitest, vite build OK; Playwright screenshots
pixel-sampled against the extracted SHOC spec (all hard values exact,
no blocking deviations).
* feat(contracts): adopt shared api-contracts in web, add zod schemas and ProblemDetails codes
Closes WEB-M5 (web hand-duplicated wire types, standing drift risk):
- shared/api-contracts: rewritten as the authoritative superset of the
.NET DTOs (ProposalListItem/ProposalDetail with poNumber and
submittedByName, line item requests, customers, pricing library,
dashboard, audit, sites, auth, presigned upload, ApiProblem); stale
Proposal/UpdateLineItemsRequest shapes removed
- shared/api-contracts/src/schemas.ts: zod runtime schemas coupled to
every wire type via `satisfies z.ZodType<T>` (schema/type drift is now
a compile error); separate entrypoint so type-only consumers (mobile)
never pull zod
- web: imports @proposal-system/api-contracts (file: dep + tsconfig
paths + vite preserveSymlinks); all 7 lib/api modules re-export shared
types so page imports stay stable; enum unions tightened
(PricingLibraryPage form state now ServiceCategory-typed)
- fix(web): customer create/update sent a singular `address` field the
API silently dropped (contract is addresses: string[], CustomerDtos.cs)
- addresses now round-trip, extra addresses preserved on edit
- api: ProblemDetails responses carry a machine-readable top-level
`code` (SHOC error-code vocabulary): ValidationFailed,
InvalidStateTransition, NotFound, Unauthorized, InternalError; new
BusinessRuleException(code, message) maps to 422 with its code;
GlobalExceptionHandlerTests cover the full mapping (wire contract)
Cross-checked .NET DTOs vs TS types vs zod schemas with the
orchestrator scanner (Gemini): core domains consistent; internal-only
DTOs (FileDtos vendor/lambda surface, SimilarProposalDtos, UserDtos
admin surface) intentionally uncovered.
Verify: dotnet 166/166, web tsc + vitest 26/26 + build, mobile tsc,
shared tsc all green.
* feat(web): domain-layer conventions doc + react-hook-form deps (Phase 4 prep)
* feat(web): scaffold domain module layer (proposals, lineItems, customers, pricingLibrary, admin, sites)
Additive-only: pages still use lib/api/* and constants/queryKeys.ts until
the page-migration agents run. Each domain ships api.ts (HTTP moved from
lib/api), types.ts (contract re-exports + view types), schemas.ts (contract
schema re-exports + form schemas with toRequest mappers), and use-cases.ts
(TanStack Query v5 hooks + hierarchical query keys, mirroring current page
invalidations and toast-on-error behavior).
Adds an explicit vite/vitest alias for the
@proposal-system/api-contracts/schemas subpath (package has no exports map)
plus a schema/mapper smoke test suite.
* refactor(web): proposal pages on domain layer, proposal form on react-hook-form
* refactor(web): admin workspace on domain layer
* refactor(web): customer management on domain layer + react-hook-form
* refactor(web): pricing library on domain layer + react-hook-form
* refactor(web): dashboards on domain layer
* test(web): domain use-case hook coverage
* refactor(web): finish domain-layer integration — migrate straggler components, delete legacy api modules
- SimilarProposalsPanel -> useSimilarProposals (domain/admin); inline
SimilarProposal type replaced by domain/admin/types (identical shape);
query key joins the admin hierarchical key space
- LineItemEditor type imports -> domain/lineItems/types
- Delete now-orphaned lib/api/{proposals,lineItems,customers,
pricingLibrary,admin,sites}.ts, constants/queryKeys.ts,
hooks/usePaginatedList.ts (lib/api/client.ts + auth.ts stay per
domain README rule 5)
Verify: tsc clean, vitest 52/52, build OK, Playwright smoke of the
authed shell renders on domain hooks.
* fix(web): apply Phase 4 code-review findings (10 correctness + 4 cleanup)
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.
2026-07-13 20:11:11 -04:00
|
|
|
deleteMutation.mutate(deleteTarget.id, { onSuccess: () => closeDeleteDialog() });
|
2026-06-18 12:49:47 -04:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const isSaving = createMutation.isPending || updateMutation.isPending;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Box>
|
|
|
|
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 3 }}>
|
|
|
|
|
<Typography variant="h5">Pricing Library</Typography>
|
|
|
|
|
<Button
|
|
|
|
|
variant="contained"
|
|
|
|
|
startIcon={<AddIcon />}
|
|
|
|
|
onClick={openCreateDialog}
|
|
|
|
|
>
|
|
|
|
|
New Item
|
|
|
|
|
</Button>
|
|
|
|
|
</Box>
|
|
|
|
|
|
|
|
|
|
<Card>
|
|
|
|
|
<CardContent sx={{ pb: 0 }}>
|
|
|
|
|
{/* Error state */}
|
|
|
|
|
{isError && (
|
|
|
|
|
<Alert
|
|
|
|
|
severity="error"
|
|
|
|
|
icon={<ErrorOutlineIcon />}
|
|
|
|
|
action={
|
|
|
|
|
<Button color="inherit" size="small" onClick={() => refetch()}>
|
|
|
|
|
Retry
|
|
|
|
|
</Button>
|
|
|
|
|
}
|
|
|
|
|
sx={{ mb: 2 }}
|
|
|
|
|
>
|
|
|
|
|
Failed to load pricing library. Please try again.
|
|
|
|
|
</Alert>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Table */}
|
|
|
|
|
<TableContainer>
|
|
|
|
|
<Table size="small">
|
|
|
|
|
<TableHead>
|
|
|
|
|
<TableRow>
|
|
|
|
|
<TableCell>Title</TableCell>
|
|
|
|
|
<TableCell>Category</TableCell>
|
|
|
|
|
<TableCell>Unit</TableCell>
|
|
|
|
|
<TableCell sx={{ textAlign: 'right' }}>Unit Price</TableCell>
|
|
|
|
|
<TableCell>Source</TableCell>
|
|
|
|
|
<TableCell sx={{ width: 100 }} />
|
|
|
|
|
</TableRow>
|
|
|
|
|
</TableHead>
|
|
|
|
|
<TableBody>
|
|
|
|
|
{isLoading
|
|
|
|
|
? Array.from({ length: 5 }).map((_, i) => (
|
|
|
|
|
<TableRow key={i}>
|
|
|
|
|
{Array.from({ length: COL_COUNT }).map((_, j) => (
|
|
|
|
|
<TableCell key={j}>
|
|
|
|
|
<Skeleton variant="text" />
|
|
|
|
|
</TableCell>
|
|
|
|
|
))}
|
|
|
|
|
</TableRow>
|
|
|
|
|
))
|
|
|
|
|
: rows.map((item) => {
|
|
|
|
|
const defaultChip = { bgcolor: '#F1F5F9', color: '#475569', border: '1px solid #CBD5E1' };
|
|
|
|
|
const chipStyle = CATEGORY_CHIP_STYLES[item.serviceCategory] ?? defaultChip;
|
|
|
|
|
return (
|
|
|
|
|
<TableRow key={item.id} hover>
|
|
|
|
|
<TableCell sx={{ fontWeight: 600 }}>{item.title}</TableCell>
|
|
|
|
|
<TableCell>
|
|
|
|
|
<Chip
|
|
|
|
|
label={item.serviceCategory}
|
|
|
|
|
size="small"
|
|
|
|
|
sx={{
|
|
|
|
|
bgcolor: chipStyle.bgcolor,
|
|
|
|
|
color: chipStyle.color,
|
|
|
|
|
border: chipStyle.border,
|
|
|
|
|
fontWeight: 600,
|
|
|
|
|
fontSize: '12px',
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</TableCell>
|
|
|
|
|
<TableCell>{item.unit ?? '—'}</TableCell>
|
|
|
|
|
<TableCell sx={{ textAlign: 'right', fontVariantNumeric: 'tabular-nums' }}>
|
|
|
|
|
{formatPrice(item.unitPrice)}
|
|
|
|
|
</TableCell>
|
|
|
|
|
<TableCell>{item.source}</TableCell>
|
|
|
|
|
<TableCell>
|
|
|
|
|
<Box sx={{ display: 'flex', gap: 0.5 }}>
|
|
|
|
|
<Tooltip title="Edit item" arrow>
|
|
|
|
|
<IconButton
|
|
|
|
|
size="small"
|
|
|
|
|
onClick={() => openEditDialog(item)}
|
|
|
|
|
>
|
|
|
|
|
<EditIcon fontSize="small" />
|
|
|
|
|
</IconButton>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
<Tooltip title="Delete item" arrow>
|
|
|
|
|
<IconButton
|
|
|
|
|
size="small"
|
|
|
|
|
onClick={() => openDeleteDialog(item)}
|
|
|
|
|
color="error"
|
|
|
|
|
>
|
|
|
|
|
<DeleteIcon fontSize="small" />
|
|
|
|
|
</IconButton>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
</Box>
|
|
|
|
|
</TableCell>
|
|
|
|
|
</TableRow>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
{!isLoading && rows.length === 0 && !isError && (
|
|
|
|
|
<TableRow>
|
|
|
|
|
<TableCell colSpan={COL_COUNT} align="center" sx={{ py: 4 }}>
|
|
|
|
|
<Typography color="text.secondary">
|
|
|
|
|
No pricing items yet. Click "New Item" to add one.
|
|
|
|
|
</Typography>
|
|
|
|
|
</TableCell>
|
|
|
|
|
</TableRow>
|
|
|
|
|
)}
|
|
|
|
|
</TableBody>
|
|
|
|
|
</Table>
|
|
|
|
|
</TableContainer>
|
|
|
|
|
|
|
|
|
|
<TablePagination
|
|
|
|
|
component="div"
|
|
|
|
|
count={totalCount}
|
|
|
|
|
page={page - 1}
|
|
|
|
|
onPageChange={(_, newPage) => setPage(newPage + 1)}
|
|
|
|
|
rowsPerPage={pageSize}
|
|
|
|
|
onRowsPerPageChange={(e) => setPageSize(parseInt(e.target.value, 10))}
|
|
|
|
|
rowsPerPageOptions={[12, 24, 48]}
|
|
|
|
|
/>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
{/* Create / Edit Dialog */}
|
|
|
|
|
<Dialog open={dialogOpen} onClose={closeDialog} maxWidth="sm" fullWidth>
|
|
|
|
|
<DialogTitle>{isEditing ? 'Edit Pricing Item' : 'New Pricing Item'}</DialogTitle>
|
refactor(web): domain-layer restructure (SHOC layering) + react-hook-form (#223)
* feat(web): adopt SHOC design system and shell layout (ADR 0003)
Port shoc-frontend-new dev's design system with its CSS-variable
single-token-source mechanism:
- src/styles/theme.css: SHOC token file ported verbatim (Montserrat/
DM Sans/JetBrains Mono, primary #1c75bc, navy #262262, full radius/
shadow/sidebar/header token layers); fonts self-hosted via @fontsource
- src/lib/theme/{css-vars,mui-theme}.ts: getCssVar -> createTheme
adapter mirroring SHOC's mui-theme.ts (palette, typography, shadows
tuple, component overrides; MUI v9 slot renames expressed as class
selectors); theme.ts is now a re-export
- Shell: SHOC composition (sidebar column + sticky gradient topbar +
scrolling main); sidebar 244px/76px collapse with brand header row,
grouped nav, SHOC active treatment (white card + 3px accent bar);
topbar 100-degree gradient, surface hamburger, gradient avatar pill
- Brand: SeahavenMark + BrandLockup ported (Tailwind re-expressed as
sx; wordmark subtitle localized to PROPOSAL SYSTEM)
- Login: SHOC auth-card treatment (centered 384px card on #f9fafb)
- Old "Sea Haven Ops" Inter/#2563EB theme and Nunito remnants removed;
remaining hardcoded hexes replaced with tokens; lucide-react for
shell/nav icons per SHOC convention
Verify: tsc clean, 26/26 vitest, vite build OK; Playwright screenshots
pixel-sampled against the extracted SHOC spec (all hard values exact,
no blocking deviations).
* feat(contracts): adopt shared api-contracts in web, add zod schemas and ProblemDetails codes
Closes WEB-M5 (web hand-duplicated wire types, standing drift risk):
- shared/api-contracts: rewritten as the authoritative superset of the
.NET DTOs (ProposalListItem/ProposalDetail with poNumber and
submittedByName, line item requests, customers, pricing library,
dashboard, audit, sites, auth, presigned upload, ApiProblem); stale
Proposal/UpdateLineItemsRequest shapes removed
- shared/api-contracts/src/schemas.ts: zod runtime schemas coupled to
every wire type via `satisfies z.ZodType<T>` (schema/type drift is now
a compile error); separate entrypoint so type-only consumers (mobile)
never pull zod
- web: imports @proposal-system/api-contracts (file: dep + tsconfig
paths + vite preserveSymlinks); all 7 lib/api modules re-export shared
types so page imports stay stable; enum unions tightened
(PricingLibraryPage form state now ServiceCategory-typed)
- fix(web): customer create/update sent a singular `address` field the
API silently dropped (contract is addresses: string[], CustomerDtos.cs)
- addresses now round-trip, extra addresses preserved on edit
- api: ProblemDetails responses carry a machine-readable top-level
`code` (SHOC error-code vocabulary): ValidationFailed,
InvalidStateTransition, NotFound, Unauthorized, InternalError; new
BusinessRuleException(code, message) maps to 422 with its code;
GlobalExceptionHandlerTests cover the full mapping (wire contract)
Cross-checked .NET DTOs vs TS types vs zod schemas with the
orchestrator scanner (Gemini): core domains consistent; internal-only
DTOs (FileDtos vendor/lambda surface, SimilarProposalDtos, UserDtos
admin surface) intentionally uncovered.
Verify: dotnet 166/166, web tsc + vitest 26/26 + build, mobile tsc,
shared tsc all green.
* feat(web): domain-layer conventions doc + react-hook-form deps (Phase 4 prep)
* feat(web): scaffold domain module layer (proposals, lineItems, customers, pricingLibrary, admin, sites)
Additive-only: pages still use lib/api/* and constants/queryKeys.ts until
the page-migration agents run. Each domain ships api.ts (HTTP moved from
lib/api), types.ts (contract re-exports + view types), schemas.ts (contract
schema re-exports + form schemas with toRequest mappers), and use-cases.ts
(TanStack Query v5 hooks + hierarchical query keys, mirroring current page
invalidations and toast-on-error behavior).
Adds an explicit vite/vitest alias for the
@proposal-system/api-contracts/schemas subpath (package has no exports map)
plus a schema/mapper smoke test suite.
* refactor(web): proposal pages on domain layer, proposal form on react-hook-form
* refactor(web): admin workspace on domain layer
* refactor(web): customer management on domain layer + react-hook-form
* refactor(web): pricing library on domain layer + react-hook-form
* refactor(web): dashboards on domain layer
* test(web): domain use-case hook coverage
* refactor(web): finish domain-layer integration — migrate straggler components, delete legacy api modules
- SimilarProposalsPanel -> useSimilarProposals (domain/admin); inline
SimilarProposal type replaced by domain/admin/types (identical shape);
query key joins the admin hierarchical key space
- LineItemEditor type imports -> domain/lineItems/types
- Delete now-orphaned lib/api/{proposals,lineItems,customers,
pricingLibrary,admin,sites}.ts, constants/queryKeys.ts,
hooks/usePaginatedList.ts (lib/api/client.ts + auth.ts stay per
domain README rule 5)
Verify: tsc clean, vitest 52/52, build OK, Playwright smoke of the
authed shell renders on domain hooks.
* fix(web): apply Phase 4 code-review findings (10 correctness + 4 cleanup)
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.
2026-07-13 20:11:11 -04:00
|
|
|
<form onSubmit={onSubmit} noValidate>
|
|
|
|
|
<DialogContent>
|
|
|
|
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, mt: 1 }}>
|
|
|
|
|
<Controller
|
|
|
|
|
name="title"
|
|
|
|
|
control={control}
|
|
|
|
|
render={({ field }) => (
|
|
|
|
|
<TextField
|
|
|
|
|
{...field}
|
|
|
|
|
label="Title"
|
|
|
|
|
error={!!errors.title}
|
|
|
|
|
helperText={errors.title?.message}
|
|
|
|
|
size="small"
|
|
|
|
|
required
|
|
|
|
|
fullWidth
|
|
|
|
|
autoFocus
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
<Controller
|
|
|
|
|
name="serviceCategory"
|
|
|
|
|
control={control}
|
|
|
|
|
render={({ field }) => (
|
|
|
|
|
<TextField
|
|
|
|
|
{...field}
|
|
|
|
|
value={field.value ?? ''}
|
|
|
|
|
label="Service Category"
|
|
|
|
|
error={!!errors.serviceCategory}
|
|
|
|
|
helperText={errors.serviceCategory ? 'Service category is required' : undefined}
|
|
|
|
|
size="small"
|
|
|
|
|
required
|
|
|
|
|
fullWidth
|
|
|
|
|
select
|
|
|
|
|
>
|
|
|
|
|
{SERVICE_CATEGORIES.map((cat) => (
|
|
|
|
|
<MenuItem key={cat} value={cat}>
|
|
|
|
|
{cat}
|
|
|
|
|
</MenuItem>
|
|
|
|
|
))}
|
|
|
|
|
</TextField>
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
<Controller
|
|
|
|
|
name="unit"
|
|
|
|
|
control={control}
|
|
|
|
|
render={({ field }) => (
|
|
|
|
|
<TextField
|
|
|
|
|
{...field}
|
|
|
|
|
label="Unit"
|
|
|
|
|
helperText="e.g., each, hour, sq ft"
|
|
|
|
|
size="small"
|
|
|
|
|
fullWidth
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
<Controller
|
|
|
|
|
name="unitPrice"
|
|
|
|
|
control={control}
|
|
|
|
|
render={({ field }) => (
|
|
|
|
|
<TextField
|
|
|
|
|
{...field}
|
|
|
|
|
label="Unit Price"
|
|
|
|
|
type="number"
|
|
|
|
|
error={!!errors.unitPrice}
|
|
|
|
|
helperText={errors.unitPrice?.message || 'Optional. Leave blank if price varies.'}
|
|
|
|
|
size="small"
|
|
|
|
|
fullWidth
|
|
|
|
|
slotProps={{
|
|
|
|
|
htmlInput: { min: 0, step: '0.01' },
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
<Controller
|
|
|
|
|
name="keywords"
|
|
|
|
|
control={control}
|
|
|
|
|
render={({ field }) => (
|
|
|
|
|
<TextField
|
|
|
|
|
{...field}
|
|
|
|
|
label="Keywords"
|
|
|
|
|
helperText="Comma-separated keywords for AI matching."
|
|
|
|
|
size="small"
|
|
|
|
|
fullWidth
|
|
|
|
|
multiline
|
|
|
|
|
minRows={2}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
<Controller
|
|
|
|
|
name="description"
|
|
|
|
|
control={control}
|
|
|
|
|
render={({ field }) => (
|
|
|
|
|
<TextField
|
|
|
|
|
{...field}
|
|
|
|
|
label="Description"
|
|
|
|
|
size="small"
|
|
|
|
|
fullWidth
|
|
|
|
|
multiline
|
|
|
|
|
minRows={2}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
</Box>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
<DialogActions>
|
|
|
|
|
<Button onClick={closeDialog} disabled={isSaving}>
|
|
|
|
|
Cancel
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
type="submit"
|
|
|
|
|
variant="contained"
|
|
|
|
|
disabled={isSaving}
|
2026-06-18 12:49:47 -04:00
|
|
|
>
|
refactor(web): domain-layer restructure (SHOC layering) + react-hook-form (#223)
* feat(web): adopt SHOC design system and shell layout (ADR 0003)
Port shoc-frontend-new dev's design system with its CSS-variable
single-token-source mechanism:
- src/styles/theme.css: SHOC token file ported verbatim (Montserrat/
DM Sans/JetBrains Mono, primary #1c75bc, navy #262262, full radius/
shadow/sidebar/header token layers); fonts self-hosted via @fontsource
- src/lib/theme/{css-vars,mui-theme}.ts: getCssVar -> createTheme
adapter mirroring SHOC's mui-theme.ts (palette, typography, shadows
tuple, component overrides; MUI v9 slot renames expressed as class
selectors); theme.ts is now a re-export
- Shell: SHOC composition (sidebar column + sticky gradient topbar +
scrolling main); sidebar 244px/76px collapse with brand header row,
grouped nav, SHOC active treatment (white card + 3px accent bar);
topbar 100-degree gradient, surface hamburger, gradient avatar pill
- Brand: SeahavenMark + BrandLockup ported (Tailwind re-expressed as
sx; wordmark subtitle localized to PROPOSAL SYSTEM)
- Login: SHOC auth-card treatment (centered 384px card on #f9fafb)
- Old "Sea Haven Ops" Inter/#2563EB theme and Nunito remnants removed;
remaining hardcoded hexes replaced with tokens; lucide-react for
shell/nav icons per SHOC convention
Verify: tsc clean, 26/26 vitest, vite build OK; Playwright screenshots
pixel-sampled against the extracted SHOC spec (all hard values exact,
no blocking deviations).
* feat(contracts): adopt shared api-contracts in web, add zod schemas and ProblemDetails codes
Closes WEB-M5 (web hand-duplicated wire types, standing drift risk):
- shared/api-contracts: rewritten as the authoritative superset of the
.NET DTOs (ProposalListItem/ProposalDetail with poNumber and
submittedByName, line item requests, customers, pricing library,
dashboard, audit, sites, auth, presigned upload, ApiProblem); stale
Proposal/UpdateLineItemsRequest shapes removed
- shared/api-contracts/src/schemas.ts: zod runtime schemas coupled to
every wire type via `satisfies z.ZodType<T>` (schema/type drift is now
a compile error); separate entrypoint so type-only consumers (mobile)
never pull zod
- web: imports @proposal-system/api-contracts (file: dep + tsconfig
paths + vite preserveSymlinks); all 7 lib/api modules re-export shared
types so page imports stay stable; enum unions tightened
(PricingLibraryPage form state now ServiceCategory-typed)
- fix(web): customer create/update sent a singular `address` field the
API silently dropped (contract is addresses: string[], CustomerDtos.cs)
- addresses now round-trip, extra addresses preserved on edit
- api: ProblemDetails responses carry a machine-readable top-level
`code` (SHOC error-code vocabulary): ValidationFailed,
InvalidStateTransition, NotFound, Unauthorized, InternalError; new
BusinessRuleException(code, message) maps to 422 with its code;
GlobalExceptionHandlerTests cover the full mapping (wire contract)
Cross-checked .NET DTOs vs TS types vs zod schemas with the
orchestrator scanner (Gemini): core domains consistent; internal-only
DTOs (FileDtos vendor/lambda surface, SimilarProposalDtos, UserDtos
admin surface) intentionally uncovered.
Verify: dotnet 166/166, web tsc + vitest 26/26 + build, mobile tsc,
shared tsc all green.
* feat(web): domain-layer conventions doc + react-hook-form deps (Phase 4 prep)
* feat(web): scaffold domain module layer (proposals, lineItems, customers, pricingLibrary, admin, sites)
Additive-only: pages still use lib/api/* and constants/queryKeys.ts until
the page-migration agents run. Each domain ships api.ts (HTTP moved from
lib/api), types.ts (contract re-exports + view types), schemas.ts (contract
schema re-exports + form schemas with toRequest mappers), and use-cases.ts
(TanStack Query v5 hooks + hierarchical query keys, mirroring current page
invalidations and toast-on-error behavior).
Adds an explicit vite/vitest alias for the
@proposal-system/api-contracts/schemas subpath (package has no exports map)
plus a schema/mapper smoke test suite.
* refactor(web): proposal pages on domain layer, proposal form on react-hook-form
* refactor(web): admin workspace on domain layer
* refactor(web): customer management on domain layer + react-hook-form
* refactor(web): pricing library on domain layer + react-hook-form
* refactor(web): dashboards on domain layer
* test(web): domain use-case hook coverage
* refactor(web): finish domain-layer integration — migrate straggler components, delete legacy api modules
- SimilarProposalsPanel -> useSimilarProposals (domain/admin); inline
SimilarProposal type replaced by domain/admin/types (identical shape);
query key joins the admin hierarchical key space
- LineItemEditor type imports -> domain/lineItems/types
- Delete now-orphaned lib/api/{proposals,lineItems,customers,
pricingLibrary,admin,sites}.ts, constants/queryKeys.ts,
hooks/usePaginatedList.ts (lib/api/client.ts + auth.ts stay per
domain README rule 5)
Verify: tsc clean, vitest 52/52, build OK, Playwright smoke of the
authed shell renders on domain hooks.
* fix(web): apply Phase 4 code-review findings (10 correctness + 4 cleanup)
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.
2026-07-13 20:11:11 -04:00
|
|
|
{isSaving ? (isEditing ? 'Saving...' : 'Creating...') : isEditing ? 'Save Changes' : 'Create Item'}
|
|
|
|
|
</Button>
|
|
|
|
|
</DialogActions>
|
|
|
|
|
</form>
|
2026-06-18 12:49:47 -04:00
|
|
|
</Dialog>
|
|
|
|
|
|
|
|
|
|
{/* Delete Confirm Dialog */}
|
|
|
|
|
<Dialog open={deleteTarget !== null} onClose={closeDeleteDialog} maxWidth="xs" fullWidth>
|
|
|
|
|
<DialogTitle>Delete Pricing Item</DialogTitle>
|
|
|
|
|
<DialogContent>
|
|
|
|
|
<Typography>
|
|
|
|
|
Are you sure you want to delete <strong>{deleteTarget?.title}</strong>? This action cannot be undone.
|
|
|
|
|
</Typography>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
<DialogActions>
|
|
|
|
|
<Button onClick={closeDeleteDialog} disabled={deleteMutation.isPending}>
|
|
|
|
|
Cancel
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
variant="contained"
|
|
|
|
|
color="error"
|
|
|
|
|
onClick={handleDeleteConfirm}
|
|
|
|
|
disabled={deleteMutation.isPending}
|
|
|
|
|
>
|
|
|
|
|
{deleteMutation.isPending ? 'Deleting...' : 'Delete'}
|
|
|
|
|
</Button>
|
|
|
|
|
</DialogActions>
|
|
|
|
|
</Dialog>
|
|
|
|
|
</Box>
|
|
|
|
|
);
|
|
|
|
|
}
|