export interface Staff { id: string; name: string; active: boolean; } export type BusinessStatus = 'ACTIVE' | 'SOLD' | 'INACTIVE'; export interface BusinessListItem { id: string; name: string; status: BusinessStatus; } export interface BusinessList { businesses: BusinessListItem[]; counts: Record; } export interface Business { id: string; name: string; nas_path: string; status: BusinessStatus; } export interface BusinessFile { /** Relative to the business directory, posix separators. */ path: string; size: number; mtime: string; } export interface ScanResult { scanned: number; inserted: number; updated: number; 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; } 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); } } async function request(path: string, init?: RequestInit): Promise { const res = await fetch(path, { credentials: 'same-origin', ...init }); if (!res.ok) { const body = (await res.json().catch(() => null)) as { error?: string } | null; throw new ApiError(res.status, body?.error ?? `Request failed (${res.status})`); } return (await res.json()) as T; } function send(method: 'POST' | 'PATCH', path: string, body?: unknown): Promise { return request(path, { method, headers: { 'content-type': 'application/json' }, body: JSON.stringify(body ?? {}), }); } const post = (path: string, body?: unknown) => send('POST', path, body); const patch = (path: string, body: unknown) => send('PATCH', path, body); const qs = (params: Record) => new URLSearchParams(params).toString(); export const api = { me: () => request('/api/me'), staff: () => request('/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(`/api/businesses?${qs({ status, search })}`), business: (id: string) => request(`/api/businesses/${id}`), businessFiles: (id: string) => request(`/api/businesses/${id}/files`), businessDeals: (id: string) => request(`/api/businesses/${id}/deals`), scan: () => post('/api/businesses/scan'), buyers: (status: BuyerStatus | '', search: string) => request(`/api/buyers?${qs({ status, search })}`), buyer: (id: string) => request(`/api/buyers/${id}`), updateBuyer: (id: string, body: Record) => patch(`/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('/api/inquiries', body), addContact: (buyerId: string, body: Record) => post(`/api/buyers/${buyerId}/contacts`, body), updateContact: (id: string, body: Record) => patch(`/api/contacts/${id}`, body), updateNda: (id: string, body: Record) => patch(`/api/ndas/${id}`, body), addDeal: (ndaId: string, businessId: string) => post(`/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(`/api/deals/${id}/notes`), }; /** Same-origin streaming URL of one file inside a business directory. */ export function businessFileUrl(id: string, filePath: string): string { return `/api/businesses/${id}/file?path=${encodeURIComponent(filePath)}`; } /** The standalone (unbundled) pdf.js viewer page, pointed at a business file. */ export function viewerUrl(id: string, filePath: string): string { return `/viewer/index.html?file=${encodeURIComponent(businessFileUrl(id, filePath))}`; }