mirror of
https://github.com/Sea-Haven-Industries/proposal-system.git
synced 2026-10-07 15:08:58 +00:00
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.
This commit is contained in:
parent
f456006c30
commit
2e1099a560
10 changed files with 3 additions and 411 deletions
|
|
@ -19,7 +19,7 @@ import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||||
import DeleteIcon from '@mui/icons-material/Delete';
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||||||
import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
|
import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
|
||||||
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
|
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 {
|
export interface EditableLineItem {
|
||||||
id: string | null;
|
id: string | null;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Typography,
|
Typography,
|
||||||
|
|
@ -11,25 +10,10 @@ import {
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
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 { formatCurrency } from '../../lib/format';
|
||||||
import type { EditableLineItem } from './LineItemEditor';
|
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 {
|
interface SimilarProposalsPanelProps {
|
||||||
proposalId: string;
|
proposalId: string;
|
||||||
onPullLineItem: (item: Omit<EditableLineItem, 'id' | 'sortOrder' | 'source'>) => void;
|
onPullLineItem: (item: Omit<EditableLineItem, 'id' | 'sortOrder' | 'source'>) => void;
|
||||||
|
|
@ -37,11 +21,7 @@ interface SimilarProposalsPanelProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SimilarProposalsPanel({ proposalId, onPullLineItem, disabled }: SimilarProposalsPanelProps) {
|
export default function SimilarProposalsPanel({ proposalId, onPullLineItem, disabled }: SimilarProposalsPanelProps) {
|
||||||
const { data: similar, isLoading } = useQuery<SimilarProposal[]>({
|
const { data: similar, isLoading } = useSimilarProposals(proposalId);
|
||||||
queryKey: ['similarProposals', proposalId],
|
|
||||||
queryFn: () => adminApi.getSimilar(proposalId) as Promise<SimilarProposal[]>,
|
|
||||||
enabled: !!proposalId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -1,76 +0,0 @@
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
||||||
import { DEBOUNCE_SEARCH, DEFAULT_PAGE, DEFAULT_PAGE_SIZE } from '../constants';
|
|
||||||
|
|
||||||
interface PaginatedResult<T> {
|
|
||||||
items: T[];
|
|
||||||
totalCount: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
type FetchFn<T> = (params: Record<string, unknown>) => Promise<PaginatedResult<T>>;
|
|
||||||
|
|
||||||
export function usePaginatedList<T>(fetchFn: FetchFn<T>, extraParams: Record<string, unknown> = {}) {
|
|
||||||
const [rows, setRows] = useState<T[]>([]);
|
|
||||||
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<ReturnType<typeof setTimeout> | 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]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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<DashboardStats> => {
|
|
||||||
const res = await apiClient.get('/admin/dashboard');
|
|
||||||
return res.data;
|
|
||||||
},
|
|
||||||
|
|
||||||
updateProposal: async (id: string, data: UpdateProposalRequest): Promise<void> => {
|
|
||||||
await apiClient.put(`/proposals/${id}`, data);
|
|
||||||
},
|
|
||||||
|
|
||||||
approveProposal: async (id: string): Promise<void> => {
|
|
||||||
await apiClient.post(`/proposals/${id}/approve`);
|
|
||||||
},
|
|
||||||
|
|
||||||
returnToReview: async (id: string): Promise<void> => {
|
|
||||||
await apiClient.post(`/proposals/${id}/return-to-review`);
|
|
||||||
},
|
|
||||||
|
|
||||||
sendProposal: async (id: string): Promise<void> => {
|
|
||||||
await apiClient.post(`/proposals/${id}/send`);
|
|
||||||
},
|
|
||||||
|
|
||||||
reviseProposal: async (id: string): Promise<void> => {
|
|
||||||
await apiClient.post(`/proposals/${id}/revise`);
|
|
||||||
},
|
|
||||||
|
|
||||||
getHistory: async (id: string): Promise<ProposalDetail[]> => {
|
|
||||||
const res = await apiClient.get(`/proposals/${id}/history`);
|
|
||||||
return res.data;
|
|
||||||
},
|
|
||||||
|
|
||||||
getAudit: async (id: string): Promise<AuditEntry[]> => {
|
|
||||||
const res = await apiClient.get(`/proposals/${id}/audit`);
|
|
||||||
return res.data;
|
|
||||||
},
|
|
||||||
|
|
||||||
generateSuggestions: async (id: string): Promise<void> => {
|
|
||||||
await apiClient.post(`/proposals/${id}/generate-suggestions`);
|
|
||||||
},
|
|
||||||
|
|
||||||
getSimilar: async (id: string): Promise<unknown[]> => {
|
|
||||||
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 };
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -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<Customer>;
|
|
||||||
|
|
||||||
export const customersApi = {
|
|
||||||
/** GET /api/customers?query=<q> — search autocomplete (unchanged). */
|
|
||||||
search: async (query?: string): Promise<Customer[]> => {
|
|
||||||
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<PagedCustomerResponse> => {
|
|
||||||
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<Customer> => {
|
|
||||||
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<Customer> => {
|
|
||||||
const res = await apiClient.put(`/customers/${id}`, data);
|
|
||||||
return res.data;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -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<LineItem[]> => {
|
|
||||||
const res = await apiClient.get(`/proposals/${proposalId}/line-items`);
|
|
||||||
return res.data;
|
|
||||||
},
|
|
||||||
|
|
||||||
create: async (proposalId: string, data: CreateLineItemRequest): Promise<LineItem> => {
|
|
||||||
const res = await apiClient.post(`/proposals/${proposalId}/line-items`, data);
|
|
||||||
return res.data;
|
|
||||||
},
|
|
||||||
|
|
||||||
bulkUpdate: async (proposalId: string, lineItems: UpdateLineItemEntry[]): Promise<LineItem[]> => {
|
|
||||||
const res = await apiClient.put(`/proposals/${proposalId}/line-items`, { lineItems });
|
|
||||||
return res.data;
|
|
||||||
},
|
|
||||||
|
|
||||||
delete: async (proposalId: string, itemId: string): Promise<void> => {
|
|
||||||
await apiClient.delete(`/proposals/${proposalId}/line-items/${itemId}`);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -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<PricingLibraryItem>;
|
|
||||||
|
|
||||||
export const pricingLibraryApi = {
|
|
||||||
/** GET /api/pricing-library/list?page=&pageSize= — paginated list (admin/sysadmin). */
|
|
||||||
list: async (params: PricingLibraryListParams = {}): Promise<PagedPricingLibraryResponse> => {
|
|
||||||
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<PricingLibraryItem> => {
|
|
||||||
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<PricingLibraryItem> => {
|
|
||||||
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<void> => {
|
|
||||||
await apiClient.delete(`/pricing-library/${id}`);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -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<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<PresignedUpload> => {
|
|
||||||
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 };
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -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<Site[]> => {
|
|
||||||
const res = await apiClient.get(`/sites?query=${encodeURIComponent(query)}`);
|
|
||||||
return res.data;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
Loading…
Add table
Reference in a new issue