module 4
This commit is contained in:
172
web/src/api.ts
172
web/src/api.ts
@@ -38,6 +38,132 @@ export interface ScanResult {
|
||||
missing: number;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ Buyer side
|
||||
export type BuyerStatus = 'ACTIVE' | 'DEACTIVATED' | 'LEGACY';
|
||||
export type NdaStatus = 'SENT' | 'SIGNED';
|
||||
export type DealStatus = 'NEW' | 'INFO_SENT' | 'DUE_DILIGENCE' | 'LOI' | 'CLOSING' | 'ENDED';
|
||||
|
||||
/** Calendar day as 'YYYY-MM-DD' — the API never sends date columns as instants. */
|
||||
export type Day = string;
|
||||
|
||||
export interface PrimaryContact {
|
||||
name: string;
|
||||
email: string | null;
|
||||
phone: string | null;
|
||||
}
|
||||
|
||||
export interface BuyerListItem {
|
||||
id: string;
|
||||
company_name: string | null;
|
||||
status: BuyerStatus;
|
||||
primary_contact: PrimaryContact | null;
|
||||
nda_count: number;
|
||||
open_deal_count: number;
|
||||
}
|
||||
|
||||
export interface BuyerList {
|
||||
buyers: BuyerListItem[];
|
||||
counts: Record<BuyerStatus, number>;
|
||||
}
|
||||
|
||||
export interface DuplicateCandidate {
|
||||
buyer_id: string;
|
||||
company_name: string | null;
|
||||
buyer_status: BuyerStatus;
|
||||
/** Any of 'email' | 'name' | 'phone' — a buyer can match on several at once. */
|
||||
matched_on: string[];
|
||||
contact_name: string;
|
||||
contact_email: string | null;
|
||||
nda_count: number;
|
||||
last_date: Day | null;
|
||||
}
|
||||
|
||||
export interface Contact {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string | null;
|
||||
phone: string | null;
|
||||
cell: string | null;
|
||||
is_primary: boolean;
|
||||
}
|
||||
|
||||
export interface Deal {
|
||||
id: string;
|
||||
status: DealStatus;
|
||||
follow_up_at: Day | null;
|
||||
note_count: number;
|
||||
business: { id: string; name: string; status: BusinessStatus };
|
||||
}
|
||||
|
||||
export interface NdaRound {
|
||||
id: string;
|
||||
status: NdaStatus;
|
||||
nas_path: string | null;
|
||||
sent_at: Day | null;
|
||||
signed_at: Day | null;
|
||||
intro_date: Day | null;
|
||||
preferred_businesses_text: string | null;
|
||||
total_purchase_price: string | null;
|
||||
down_payment: string | null;
|
||||
deals: Deal[];
|
||||
}
|
||||
|
||||
export interface Buyer {
|
||||
id: string;
|
||||
company_name: string | null;
|
||||
status: BuyerStatus;
|
||||
address: string | null;
|
||||
state: string | null;
|
||||
background_experience: string | null;
|
||||
how_heard: string | null;
|
||||
/** null = not answered on the intake sheet, distinct from an explicit false. */
|
||||
interested_in_updates: boolean | null;
|
||||
contacts: Contact[];
|
||||
ndas: NdaRound[];
|
||||
/** Only present on the PATCH response. */
|
||||
open_deal_count?: number;
|
||||
}
|
||||
|
||||
export interface DealNote {
|
||||
id: string;
|
||||
text: string;
|
||||
highlight: boolean;
|
||||
created_at: string;
|
||||
author: string | null;
|
||||
}
|
||||
|
||||
export interface BusinessDeal {
|
||||
id: string;
|
||||
status: DealStatus;
|
||||
buyer_id: string;
|
||||
company_name: string | null;
|
||||
buyer_status: BuyerStatus;
|
||||
contact_name: string | null;
|
||||
intro_date: Day | null;
|
||||
signed_at: Day | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface InquiryInput {
|
||||
buyer_id?: string;
|
||||
company_name?: string;
|
||||
contact: { name: string; email?: string; phone?: string; cell?: string };
|
||||
business_id: string;
|
||||
backfill?: {
|
||||
deal_status: DealStatus;
|
||||
nda_status: NdaStatus;
|
||||
signed_at?: string;
|
||||
nda_nas_path?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface InquiryResult {
|
||||
buyer_id: string;
|
||||
nda_id: string;
|
||||
deal_id: string;
|
||||
created: { buyer: boolean; contact: boolean };
|
||||
}
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor(readonly status: number, message: string) {
|
||||
super(message);
|
||||
@@ -53,26 +179,60 @@ async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
return (await res.json()) as T;
|
||||
}
|
||||
|
||||
function post<T>(path: string, body?: unknown): Promise<T> {
|
||||
function send<T>(method: 'POST' | 'PATCH', path: string, body?: unknown): Promise<T> {
|
||||
return request<T>(path, {
|
||||
method: 'POST',
|
||||
method,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify(body ?? {}),
|
||||
});
|
||||
}
|
||||
|
||||
const post = <T>(path: string, body?: unknown) => send<T>('POST', path, body);
|
||||
const patch = <T>(path: string, body: unknown) => send<T>('PATCH', path, body);
|
||||
|
||||
const qs = (params: Record<string, string>) =>
|
||||
new URLSearchParams(params).toString();
|
||||
|
||||
export const api = {
|
||||
me: () => request<Staff>('/api/me'),
|
||||
staff: () => request<Staff[]>('/api/staff'),
|
||||
login: (staffId: string) => post<{ ok: boolean; staff: Staff }>('/api/login', { staff_id: staffId }),
|
||||
logout: () => post<{ ok: boolean }>('/api/logout'),
|
||||
businesses: (status: BusinessStatus, search: string) =>
|
||||
request<BusinessList>(
|
||||
`/api/businesses?status=${encodeURIComponent(status)}&search=${encodeURIComponent(search)}`,
|
||||
),
|
||||
businesses: (status: BusinessStatus | '', search: string) =>
|
||||
request<BusinessList>(`/api/businesses?${qs({ status, search })}`),
|
||||
business: (id: string) => request<Business>(`/api/businesses/${id}`),
|
||||
businessFiles: (id: string) => request<BusinessFile[]>(`/api/businesses/${id}/files`),
|
||||
businessDeals: (id: string) => request<BusinessDeal[]>(`/api/businesses/${id}/deals`),
|
||||
scan: () => post<ScanResult>('/api/businesses/scan'),
|
||||
|
||||
buyers: (status: BuyerStatus | '', search: string) =>
|
||||
request<BuyerList>(`/api/buyers?${qs({ status, search })}`),
|
||||
buyer: (id: string) => request<Buyer>(`/api/buyers/${id}`),
|
||||
updateBuyer: (id: string, body: Record<string, unknown>) =>
|
||||
patch<Buyer>(`/api/buyers/${id}`, body),
|
||||
duplicates: (probe: { email?: string; name?: string; phone?: string }) =>
|
||||
request<{ candidates: DuplicateCandidate[] }>(
|
||||
`/api/buyers/duplicates?${qs({
|
||||
email: probe.email ?? '',
|
||||
name: probe.name ?? '',
|
||||
phone: probe.phone ?? '',
|
||||
})}`,
|
||||
),
|
||||
createInquiry: (body: InquiryInput) => post<InquiryResult>('/api/inquiries', body),
|
||||
addContact: (buyerId: string, body: Record<string, unknown>) =>
|
||||
post<Contact>(`/api/buyers/${buyerId}/contacts`, body),
|
||||
updateContact: (id: string, body: Record<string, unknown>) =>
|
||||
patch<Contact>(`/api/contacts/${id}`, body),
|
||||
updateNda: (id: string, body: Record<string, unknown>) =>
|
||||
patch<NdaRound>(`/api/ndas/${id}`, body),
|
||||
addDeal: (ndaId: string, businessId: string) =>
|
||||
post<Deal>(`/api/ndas/${ndaId}/deals`, { business_id: businessId }),
|
||||
setDealStatus: (id: string, status: DealStatus, comment?: string) =>
|
||||
post<{ id: string; status: DealStatus; follow_up_at: Day | null }>(
|
||||
`/api/deals/${id}/status`,
|
||||
{ status, comment },
|
||||
),
|
||||
dealNotes: (id: string) => request<DealNote[]>(`/api/deals/${id}/notes`),
|
||||
};
|
||||
|
||||
/** Same-origin streaming URL of one file inside a business directory. */
|
||||
|
||||
Reference in New Issue
Block a user