mirror of
https://github.com/Sea-Haven-Industries/proposal-system.git
synced 2026-10-05 04:52:02 +00:00
- client.ts: localStorage → sessionStorage (WEB-C1 critical fix) - proposals.ts: re-add 'Other' to ServiceCategory (WEB-M4) - ProposalListPage.tsx: STATUS_COLORS → STATUS_CHIP_STYLES (visual design)
160 lines
4.8 KiB
TypeScript
160 lines
4.8 KiB
TypeScript
import apiClient from './client';
|
|
|
|
// Fix: WEB-M5 — align with shared contract types (shared/api-contracts/src/index.ts)
|
|
export type ServiceCategory = 'HVAC' | 'Plumbing' | 'Electrical' | 'General' | 'Renovation' | 'Other';
|
|
export type Priority = 'Standard' | 'Urgent' | 'Emergency';
|
|
|
|
export interface CreateProposalRequest {
|
|
workOrderNumber: string;
|
|
poNumber?: string;
|
|
customerName: string;
|
|
customerAddress: string;
|
|
scopeOfWork: string;
|
|
serviceCategory: ServiceCategory;
|
|
priority: Priority;
|
|
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;
|
|
poNumber: string | null;
|
|
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;
|
|
},
|
|
|
|
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<PdfVersion[]> => {
|
|
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 };
|
|
},
|
|
};
|
|
|
|
export interface ProposalStats {
|
|
totalCount: number;
|
|
inReviewCount: number;
|
|
approvedCount: number;
|
|
sentCount: number;
|
|
}
|
|
|
|
export interface PdfVersion {
|
|
revision: number;
|
|
generatedAt: string;
|
|
}
|