mirror of
https://github.com/Sea-Haven-Industries/proposal-system.git
synced 2026-10-05 09:32:01 +00:00
Some checks failed
Deploy / Deploy to AWS (push) Has been cancelled
## Summary Implements Phase 3 of the AUDIT-2026-05-20 findings: - 29 FIX-severity items across API, web, infra, and lambdas - 7 accessibility NITs (aria-labels, document titles) - 4 code quality NITs (deduplication, constants extraction) Key changes: - API: N+1 fix, pagination clamping, idempotent transitions, upload confirm endpoint, revision TotalBidAmount carry-forward - Web: confirmation dialogs, currency formatting, error states, date range filters, document titles - Infra: S3 CORS lockdown, API Gateway throttling, AOSS network policy fix, CI concurrency - Lambdas: skip empty suggestions, remove status side-effect - Scripts: post-deploy health check ## Test plan - [x] tsc --noEmit (web + infra) - [x] dotnet build (api) - [x] ruff check + format (lambdas) - [x] Cross-review via orchestrator (no blockers) [skip deploy]
122 lines
3.5 KiB
TypeScript
122 lines
3.5 KiB
TypeScript
import apiClient from './client';
|
|
|
|
export interface CreateProposalRequest {
|
|
workOrderNumber: string;
|
|
customerName: string;
|
|
customerAddress: string;
|
|
scopeOfWork: string;
|
|
serviceCategory: string;
|
|
priority: string;
|
|
notes?: string;
|
|
}
|
|
|
|
export interface ProposalListItem {
|
|
id: string;
|
|
proposalNumber: string;
|
|
customerName: string;
|
|
workOrderNumber: string;
|
|
serviceCategory: string;
|
|
priority: string;
|
|
status: string;
|
|
totalBidAmount: number;
|
|
submittedAt: string;
|
|
submittedByName: string | null;
|
|
assignedAdminName: string | null;
|
|
}
|
|
|
|
export interface ProposalDetail {
|
|
id: string;
|
|
proposalNumber: string;
|
|
workOrderNumber: string;
|
|
customerName: string;
|
|
customerAddress: string;
|
|
scopeOfWork: string;
|
|
refinedScope: string | null;
|
|
serviceCategory: string;
|
|
priority: string;
|
|
status: string;
|
|
totalBidAmount: number;
|
|
vendorTotalCost: number | null;
|
|
notes: string;
|
|
submittedById: string;
|
|
submittedByName: string | null;
|
|
submittedAt: string;
|
|
assignedAdminId: string | null;
|
|
approvedById: string | null;
|
|
approvedAt: string | null;
|
|
sentAt: string | null;
|
|
currentRevision: number;
|
|
parentProposalId: string | null;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
}
|
|
|
|
export interface PagedResponse<T> {
|
|
items: T[];
|
|
totalCount: number;
|
|
page: number;
|
|
pageSize: number;
|
|
}
|
|
|
|
export interface ProposalFilters {
|
|
search?: string;
|
|
page?: number;
|
|
pageSize?: number;
|
|
status?: string;
|
|
serviceCategory?: string;
|
|
priority?: string;
|
|
fromDate?: string;
|
|
toDate?: string;
|
|
mine?: boolean;
|
|
}
|
|
|
|
export const proposalsApi = {
|
|
create: async (data: CreateProposalRequest): Promise<ProposalDetail> => {
|
|
const res = await apiClient.post('/proposals', data);
|
|
return res.data;
|
|
},
|
|
|
|
getAll: async (filters: ProposalFilters = {}): Promise<PagedResponse<ProposalListItem>> => {
|
|
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<ProposalDetail> => {
|
|
const res = await apiClient.get(`/proposals/${id}`);
|
|
return res.data;
|
|
},
|
|
|
|
uploadAttachment: async (proposalId: string, fileName: string, vendorName?: string): Promise<{ uploadUrl: string; s3Key: string; vendorProposalId: string }> => {
|
|
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<void> => {
|
|
await apiClient.post(`/proposals/${proposalId}/attachments/${vendorProposalId}/confirm`);
|
|
},
|
|
|
|
getStats: async (): Promise<ProposalStats> => {
|
|
const res = await apiClient.get('/proposals/stats');
|
|
return res.data;
|
|
},
|
|
};
|
|
|
|
export interface ProposalStats {
|
|
totalCount: number;
|
|
inReviewCount: number;
|
|
approvedCount: number;
|
|
sentCount: number;
|
|
}
|