From 2e1099a56009dff72f2d6bc73d8b38a970de18cf Mon Sep 17 00:00:00 2001 From: Adam Moussa Date: Mon, 13 Jul 2026 18:11:05 -0400 Subject: [PATCH] =?UTF-8?q?refactor(web):=20finish=20domain-layer=20integr?= =?UTF-8?q?ation=20=E2=80=94=20migrate=20straggler=20components,=20delete?= =?UTF-8?q?=20legacy=20api=20modules?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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. --- web/src/components/admin/LineItemEditor.tsx | 2 +- .../admin/SimilarProposalsPanel.tsx | 24 +---- web/src/constants/queryKeys.ts | 11 --- web/src/hooks/usePaginatedList.ts | 76 --------------- web/src/lib/api/admin.ts | 63 ------------ web/src/lib/api/customers.ts | 51 ---------- web/src/lib/api/lineItems.ts | 32 ------ web/src/lib/api/pricingLibrary.ts | 46 --------- web/src/lib/api/proposals.ts | 97 ------------------- web/src/lib/api/sites.ts | 12 --- 10 files changed, 3 insertions(+), 411 deletions(-) delete mode 100644 web/src/constants/queryKeys.ts delete mode 100644 web/src/hooks/usePaginatedList.ts delete mode 100644 web/src/lib/api/admin.ts delete mode 100644 web/src/lib/api/customers.ts delete mode 100644 web/src/lib/api/lineItems.ts delete mode 100644 web/src/lib/api/pricingLibrary.ts delete mode 100644 web/src/lib/api/proposals.ts delete mode 100644 web/src/lib/api/sites.ts diff --git a/web/src/components/admin/LineItemEditor.tsx b/web/src/components/admin/LineItemEditor.tsx index 42e0674..0f7b7fb 100644 --- a/web/src/components/admin/LineItemEditor.tsx +++ b/web/src/components/admin/LineItemEditor.tsx @@ -19,7 +19,7 @@ import ContentCopyIcon from '@mui/icons-material/ContentCopy'; import DeleteIcon from '@mui/icons-material/Delete'; import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'; import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward'; -import type { PricingMode, LineItemSource } from '../../lib/api/lineItems'; +import type { PricingMode, LineItemSource } from '../../domain/lineItems/types'; export interface EditableLineItem { id: string | null; diff --git a/web/src/components/admin/SimilarProposalsPanel.tsx b/web/src/components/admin/SimilarProposalsPanel.tsx index 6dfad4c..fec0149 100644 --- a/web/src/components/admin/SimilarProposalsPanel.tsx +++ b/web/src/components/admin/SimilarProposalsPanel.tsx @@ -1,4 +1,3 @@ -import { useQuery } from '@tanstack/react-query'; import { Box, Typography, @@ -11,25 +10,10 @@ import { } from '@mui/material'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import ContentCopyIcon from '@mui/icons-material/ContentCopy'; -import { adminApi } from '../../lib/api/admin'; +import { useSimilarProposals } from '../../domain/admin/use-cases'; import { formatCurrency } from '../../lib/format'; import type { EditableLineItem } from './LineItemEditor'; -interface SimilarProposal { - proposalNumber: string; - customerName: string; - serviceCategory: string; - similarityScore: number; - totalBidAmount: number; - lineItems: Array<{ - description: string; - quantity: number; - unit: string; - unitPrice: number | null; - totalPrice: number; - }>; -} - interface SimilarProposalsPanelProps { proposalId: string; onPullLineItem: (item: Omit) => void; @@ -37,11 +21,7 @@ interface SimilarProposalsPanelProps { } export default function SimilarProposalsPanel({ proposalId, onPullLineItem, disabled }: SimilarProposalsPanelProps) { - const { data: similar, isLoading } = useQuery({ - queryKey: ['similarProposals', proposalId], - queryFn: () => adminApi.getSimilar(proposalId) as Promise, - enabled: !!proposalId, - }); + const { data: similar, isLoading } = useSimilarProposals(proposalId); if (isLoading) { return ( diff --git a/web/src/constants/queryKeys.ts b/web/src/constants/queryKeys.ts deleted file mode 100644 index 943ec10..0000000 --- a/web/src/constants/queryKeys.ts +++ /dev/null @@ -1,11 +0,0 @@ -export const QUERY_KEYS = { - proposals: 'proposals', - proposal: 'proposal', - proposalLineItems: 'proposalLineItems', - customers: 'customers', - customerList: 'customerList', - users: 'users', - dashboard: 'dashboard', - auditTrail: 'auditTrail', - pricingLibraryList: 'pricingLibraryList', -} as const; diff --git a/web/src/hooks/usePaginatedList.ts b/web/src/hooks/usePaginatedList.ts deleted file mode 100644 index f560c0c..0000000 --- a/web/src/hooks/usePaginatedList.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { DEBOUNCE_SEARCH, DEFAULT_PAGE, DEFAULT_PAGE_SIZE } from '../constants'; - -interface PaginatedResult { - items: T[]; - totalCount: number; -} - -type FetchFn = (params: Record) => Promise>; - -export function usePaginatedList(fetchFn: FetchFn, extraParams: Record = {}) { - const [rows, setRows] = useState([]); - const [search, setSearch] = useState(''); - const [debouncedSearch, setDebouncedSearch] = useState(''); - const [page, setPage] = useState(DEFAULT_PAGE); - const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE); - const [totalCount, setTotalCount] = useState(0); - const [loading, setLoading] = useState(true); - const [err, setErr] = useState(''); - const debounceRef = useRef | null>(null); - - const extraKey = JSON.stringify(extraParams); - - useEffect(() => { - if (debounceRef.current) clearTimeout(debounceRef.current); - debounceRef.current = setTimeout(() => { - setDebouncedSearch(search); - setPage(1); - }, DEBOUNCE_SEARCH); - return () => { - if (debounceRef.current) clearTimeout(debounceRef.current); - }; - }, [search]); - - const prevExtraKey = useRef(extraKey); - useEffect(() => { - if (prevExtraKey.current !== extraKey) { - prevExtraKey.current = extraKey; - setPage(1); - } - }, [extraKey]); - - const reload = useCallback(() => { - setLoading(true); - setErr(''); - fetchFn({ search: debouncedSearch, page, pageSize, ...extraParams }) - .then(({ items, totalCount }) => { - setRows(items); - setTotalCount(totalCount); - }) - .catch((e: Error) => setErr(e.message || 'Failed to load')) - .finally(() => setLoading(false)); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [fetchFn, debouncedSearch, page, pageSize, extraKey]); - - useEffect(() => { - reload(); - }, [reload]); - - return useMemo( - () => ({ - rows, - search, - setSearch, - page, - setPage, - pageSize, - setPageSize, - totalCount, - loading, - err, - reload, - }), - [rows, search, page, pageSize, totalCount, loading, err, reload] - ); -} diff --git a/web/src/lib/api/admin.ts b/web/src/lib/api/admin.ts deleted file mode 100644 index 918524c..0000000 --- a/web/src/lib/api/admin.ts +++ /dev/null @@ -1,63 +0,0 @@ -import apiClient from './client'; -// Fix: WEB-M5 — types from the shared contracts package. -import type { ProposalDetail, UpdateProposalRequest, DashboardStats, AuditEntry } from '@proposal-system/api-contracts'; - -export type { DashboardStats, UpdateProposalRequest, AuditEntry } from '@proposal-system/api-contracts'; - -export const adminApi = { - getDashboard: async (): Promise => { - const res = await apiClient.get('/admin/dashboard'); - return res.data; - }, - - updateProposal: async (id: string, data: UpdateProposalRequest): Promise => { - await apiClient.put(`/proposals/${id}`, data); - }, - - approveProposal: async (id: string): Promise => { - await apiClient.post(`/proposals/${id}/approve`); - }, - - returnToReview: async (id: string): Promise => { - await apiClient.post(`/proposals/${id}/return-to-review`); - }, - - sendProposal: async (id: string): Promise => { - await apiClient.post(`/proposals/${id}/send`); - }, - - reviseProposal: async (id: string): Promise => { - await apiClient.post(`/proposals/${id}/revise`); - }, - - getHistory: async (id: string): Promise => { - const res = await apiClient.get(`/proposals/${id}/history`); - return res.data; - }, - - getAudit: async (id: string): Promise => { - const res = await apiClient.get(`/proposals/${id}/audit`); - return res.data; - }, - - generateSuggestions: async (id: string): Promise => { - await apiClient.post(`/proposals/${id}/generate-suggestions`); - }, - - getSimilar: async (id: string): Promise => { - const res = await apiClient.get(`/proposals/${id}/similar`); - return res.data; - }, - - getPdf: async (id: string, regenerate = false): Promise<{ downloadUrl: string } | null> => { - const params = regenerate ? '?regenerate=true' : ''; - const res = await apiClient.get(`/proposals/${id}/pdf${params}`, { - responseType: 'blob', - validateStatus: (status) => status < 500, - }); - if (res.status === 202) return null; - const blob = res.data as Blob; - const downloadUrl = URL.createObjectURL(blob); - return { downloadUrl }; - }, -}; diff --git a/web/src/lib/api/customers.ts b/web/src/lib/api/customers.ts deleted file mode 100644 index 61ee7ee..0000000 --- a/web/src/lib/api/customers.ts +++ /dev/null @@ -1,51 +0,0 @@ -import apiClient from './client'; -// Fix: WEB-M5 — types from the shared contracts package. Note: the API takes -// addresses: string[] (CustomerDtos.cs); the old local CreateCustomerRequest -// sent a singular `address` field the API silently dropped. -import type { - Customer, - CustomerListParams, - CreateCustomerRequest, - UpdateCustomerRequest, - PagedResponse, -} from '@proposal-system/api-contracts'; - -export type { - Customer, - CustomerListParams, - CreateCustomerRequest, - UpdateCustomerRequest, -} from '@proposal-system/api-contracts'; - -export type PagedCustomerResponse = PagedResponse; - -export const customersApi = { - /** GET /api/customers?query= — search autocomplete (unchanged). */ - search: async (query?: string): Promise => { - const params = query ? `?query=${encodeURIComponent(query)}` : ''; - const res = await apiClient.get(`/customers${params}`); - return res.data; - }, - - /** GET /api/customers/list?page=&pageSize= — paginated management list (admin/sysadmin). */ - list: async (params: CustomerListParams = {}): Promise => { - const qs = new URLSearchParams(); - if (params.page) qs.append('page', String(params.page)); - if (params.pageSize) qs.append('pageSize', String(params.pageSize)); - if (params.search) qs.append('search', params.search); - const res = await apiClient.get(`/customers/list?${qs.toString()}`); - return res.data; - }, - - /** POST /api/customers — create a new customer. */ - create: async (data: CreateCustomerRequest): Promise => { - const res = await apiClient.post('/customers', data); - return res.data; - }, - - /** PUT /api/customers/{id} — update an existing customer (admin/sysadmin). */ - update: async (id: string, data: UpdateCustomerRequest): Promise => { - const res = await apiClient.put(`/customers/${id}`, data); - return res.data; - }, -}; diff --git a/web/src/lib/api/lineItems.ts b/web/src/lib/api/lineItems.ts deleted file mode 100644 index 95f4eb8..0000000 --- a/web/src/lib/api/lineItems.ts +++ /dev/null @@ -1,32 +0,0 @@ -import apiClient from './client'; -// Fix: WEB-M5 — types from the shared contracts package. -import type { LineItem, CreateLineItemRequest, UpdateLineItemEntry } from '@proposal-system/api-contracts'; - -export type { - PricingMode, - LineItemSource, - LineItem, - CreateLineItemRequest, - UpdateLineItemEntry, -} from '@proposal-system/api-contracts'; - -export const lineItemsApi = { - getAll: async (proposalId: string): Promise => { - const res = await apiClient.get(`/proposals/${proposalId}/line-items`); - return res.data; - }, - - create: async (proposalId: string, data: CreateLineItemRequest): Promise => { - const res = await apiClient.post(`/proposals/${proposalId}/line-items`, data); - return res.data; - }, - - bulkUpdate: async (proposalId: string, lineItems: UpdateLineItemEntry[]): Promise => { - const res = await apiClient.put(`/proposals/${proposalId}/line-items`, { lineItems }); - return res.data; - }, - - delete: async (proposalId: string, itemId: string): Promise => { - await apiClient.delete(`/proposals/${proposalId}/line-items/${itemId}`); - }, -}; diff --git a/web/src/lib/api/pricingLibrary.ts b/web/src/lib/api/pricingLibrary.ts deleted file mode 100644 index a0b4bb0..0000000 --- a/web/src/lib/api/pricingLibrary.ts +++ /dev/null @@ -1,46 +0,0 @@ -import apiClient from './client'; -// Fix: WEB-M5 — types from the shared contracts package. -import type { - PricingLibraryItem, - PricingLibraryListParams, - CreatePricingLibraryItemRequest, - UpdatePricingLibraryItemRequest, - PagedResponse, -} from '@proposal-system/api-contracts'; - -export type { - PricingLibraryItem, - PricingLibraryListParams, - CreatePricingLibraryItemRequest, - UpdatePricingLibraryItemRequest, -} from '@proposal-system/api-contracts'; - -export type PagedPricingLibraryResponse = PagedResponse; - -export const pricingLibraryApi = { - /** GET /api/pricing-library/list?page=&pageSize= — paginated list (admin/sysadmin). */ - list: async (params: PricingLibraryListParams = {}): Promise => { - const qs = new URLSearchParams(); - if (params.page) qs.append('page', String(params.page)); - if (params.pageSize) qs.append('pageSize', String(params.pageSize)); - const res = await apiClient.get(`/pricing-library/list?${qs.toString()}`); - return res.data; - }, - - /** POST /api/pricing-library — create a new pricing library item. */ - create: async (data: CreatePricingLibraryItemRequest): Promise => { - const res = await apiClient.post('/pricing-library', data); - return res.data; - }, - - /** PUT /api/pricing-library/{id} — update an existing pricing library item. */ - update: async (id: string, data: UpdatePricingLibraryItemRequest): Promise => { - const res = await apiClient.put(`/pricing-library/${id}`, data); - return res.data; - }, - - /** DELETE /api/pricing-library/{id} — delete a pricing library item. */ - remove: async (id: string): Promise => { - await apiClient.delete(`/pricing-library/${id}`); - }, -}; diff --git a/web/src/lib/api/proposals.ts b/web/src/lib/api/proposals.ts deleted file mode 100644 index 024d1d8..0000000 --- a/web/src/lib/api/proposals.ts +++ /dev/null @@ -1,97 +0,0 @@ -import apiClient from './client'; -// Fix: WEB-M5 — types now come from the shared contracts package (single source -// of truth aligned with the .NET DTOs); re-exported so page imports stay stable. -import type { - CreateProposalRequest, - ProposalDetail, - ProposalListItem, - ProposalFilters, - ProposalStats, - PagedResponse, - PdfVersion, - PresignedUpload, -} from '@proposal-system/api-contracts'; - -export type { - ServiceCategory, - Priority, - CreateProposalRequest, - ProposalDetail, - ProposalListItem, - ProposalFilters, - ProposalStats, - PagedResponse, - PdfVersion, - PresignedUpload, -} from '@proposal-system/api-contracts'; - -export const proposalsApi = { - create: async (data: CreateProposalRequest): Promise => { - const res = await apiClient.post('/proposals', data); - return res.data; - }, - - getAll: async (filters: ProposalFilters = {}): Promise> => { - const params = new URLSearchParams(); - if (filters.search) params.append('search', filters.search); - if (filters.page) params.append('page', String(filters.page)); - if (filters.pageSize) params.append('pageSize', String(filters.pageSize)); - if (filters.status) params.append('status', filters.status); - if (filters.serviceCategory) params.append('serviceCategory', filters.serviceCategory); - if (filters.priority) params.append('priority', filters.priority); - if (filters.fromDate) params.append('fromDate', filters.fromDate); - if (filters.toDate) params.append('toDate', filters.toDate); - if (filters.mine) params.append('mine', 'true'); - - const res = await apiClient.get(`/proposals?${params.toString()}`); - return res.data; - }, - - getById: async (id: string): Promise => { - const res = await apiClient.get(`/proposals/${id}`); - return res.data; - }, - - uploadAttachment: async (proposalId: string, fileName: string, vendorName?: string): Promise => { - const params = new URLSearchParams({ fileName }); - if (vendorName) params.append('vendorName', vendorName); - const res = await apiClient.post(`/proposals/${proposalId}/attachments?${params.toString()}`); - return res.data; - }, - - confirmUpload: async (proposalId: string, vendorProposalId: string): Promise => { - await apiClient.post(`/proposals/${proposalId}/attachments/${vendorProposalId}/confirm`); - }, - - getStats: async (): Promise => { - const res = await apiClient.get('/proposals/stats'); - return res.data; - }, - - getPdf: async (id: string): Promise<{ downloadUrl: string } | null> => { - const res = await apiClient.get(`/proposals/${id}/pdf`, { - responseType: 'blob', - validateStatus: (status) => status < 500, - }); - if (res.status === 404) return null; - const blob = res.data as Blob; - const downloadUrl = URL.createObjectURL(blob); - return { downloadUrl }; - }, - - getPdfVersions: async (id: string): Promise => { - const res = await apiClient.get(`/proposals/${id}/pdf/versions`); - return res.data; - }, - - getPdfRevision: async (id: string, revision: number): Promise<{ downloadUrl: string } | null> => { - const res = await apiClient.get(`/proposals/${id}/pdf/${revision}`, { - responseType: 'blob', - validateStatus: (status) => status < 500, - }); - if (res.status === 404) return null; - const blob = res.data as Blob; - const downloadUrl = URL.createObjectURL(blob); - return { downloadUrl }; - }, -}; diff --git a/web/src/lib/api/sites.ts b/web/src/lib/api/sites.ts deleted file mode 100644 index eb0b8a8..0000000 --- a/web/src/lib/api/sites.ts +++ /dev/null @@ -1,12 +0,0 @@ -import apiClient from './client'; -// Fix: WEB-M5 — types from the shared contracts package. -import type { Site } from '@proposal-system/api-contracts'; - -export type { Site } from '@proposal-system/api-contracts'; - -export const sitesApi = { - search: async (query: string): Promise => { - const res = await apiClient.get(`/sites?query=${encodeURIComponent(query)}`); - return res.data; - }, -};