mirror of
https://github.com/Sea-Haven-Industries/proposal-system.git
synced 2026-10-07 16:18:57 +00:00
99 lines
3.5 KiB
TypeScript
99 lines
3.5 KiB
TypeScript
|
|
// Proposals domain — TanStack Query hooks + query keys (the only public surface).
|
||
|
|
import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||
|
|
import { toast } from 'react-toastify';
|
||
|
|
import { proposalsApi } from './api';
|
||
|
|
import type { CreateProposalRequest, ProposalDetail, ProposalFilters } from './types';
|
||
|
|
|
||
|
|
export const proposalsKeys = {
|
||
|
|
all: ['proposals'] as const,
|
||
|
|
lists: () => [...proposalsKeys.all, 'list'] as const,
|
||
|
|
list: (filters: ProposalFilters) => [...proposalsKeys.lists(), filters] as const,
|
||
|
|
detail: (id: string) => [...proposalsKeys.all, id] as const,
|
||
|
|
stats: () => [...proposalsKeys.all, 'stats'] as const,
|
||
|
|
vendors: (id: string) => [...proposalsKeys.all, id, 'vendors'] as const,
|
||
|
|
};
|
||
|
|
|
||
|
|
/** Paginated/filtered proposal list (ProposalListPage, AdminDashboard queue,
|
||
|
|
* Dashboard recent). keepPreviousData mirrors usePaginatedList's
|
||
|
|
* keep-rows-while-loading behavior across page changes. */
|
||
|
|
export function useProposals(filters: ProposalFilters = {}) {
|
||
|
|
return useQuery({
|
||
|
|
queryKey: proposalsKeys.list(filters),
|
||
|
|
queryFn: () => proposalsApi.getAll(filters),
|
||
|
|
placeholderData: keepPreviousData,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
export function useProposal(id: string | undefined) {
|
||
|
|
return useQuery({
|
||
|
|
queryKey: proposalsKeys.detail(id ?? ''),
|
||
|
|
queryFn: () => proposalsApi.getById(id!),
|
||
|
|
enabled: !!id,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
export function useProposalStats() {
|
||
|
|
return useQuery({
|
||
|
|
queryKey: proposalsKeys.stats(),
|
||
|
|
queryFn: () => proposalsApi.getStats(),
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Vendor proposals attached to a proposal (VendorDataPanel). */
|
||
|
|
export function useVendorProposals(proposalId: string | undefined) {
|
||
|
|
return useQuery({
|
||
|
|
queryKey: proposalsKeys.vendors(proposalId ?? ''),
|
||
|
|
queryFn: () => proposalsApi.getVendors(proposalId!),
|
||
|
|
enabled: !!proposalId,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface CreateProposalVariables {
|
||
|
|
request: CreateProposalRequest;
|
||
|
|
/** Optional vendor PDF uploaded alongside the proposal. */
|
||
|
|
vendorFile?: File | null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Create a proposal, optionally uploading a vendor PDF via presigned URL.
|
||
|
|
* Mirrors ProposalFormPage: a failed attachment upload warns but does not
|
||
|
|
* fail the mutation; success invalidates all proposal queries and toasts.
|
||
|
|
* Page-specific side effects (navigate) belong in the call site's onSuccess.
|
||
|
|
*/
|
||
|
|
export function useCreateProposal() {
|
||
|
|
const queryClient = useQueryClient();
|
||
|
|
return useMutation({
|
||
|
|
mutationFn: async ({ request, vendorFile }: CreateProposalVariables): Promise<ProposalDetail> => {
|
||
|
|
const proposal = await proposalsApi.create(request);
|
||
|
|
|
||
|
|
if (vendorFile) {
|
||
|
|
try {
|
||
|
|
const { uploadUrl, vendorProposalId } = await proposalsApi.uploadAttachment(
|
||
|
|
proposal.id,
|
||
|
|
vendorFile.name
|
||
|
|
);
|
||
|
|
await fetch(uploadUrl, {
|
||
|
|
method: 'PUT',
|
||
|
|
body: vendorFile,
|
||
|
|
headers: { 'Content-Type': vendorFile.type || 'application/pdf' },
|
||
|
|
});
|
||
|
|
await proposalsApi.confirmUpload(proposal.id, vendorProposalId);
|
||
|
|
} catch {
|
||
|
|
toast.warning(
|
||
|
|
`Proposal ${proposal.proposalNumber} was created, but vendor PDF upload failed. You can re-upload from the proposal detail page.`
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return proposal;
|
||
|
|
},
|
||
|
|
onSuccess: (proposal) => {
|
||
|
|
queryClient.invalidateQueries({ queryKey: proposalsKeys.all });
|
||
|
|
toast.success(`Proposal ${proposal.proposalNumber} submitted`);
|
||
|
|
},
|
||
|
|
onError: (error: Error) => {
|
||
|
|
toast.error(error.message || 'Failed to submit proposal');
|
||
|
|
},
|
||
|
|
});
|
||
|
|
}
|