This commit is contained in:
2026-07-27 15:26:17 -05:00
parent 32c8ccc3ed
commit 6cb13650ed
16 changed files with 1924 additions and 148 deletions

View File

@@ -164,6 +164,92 @@ export interface InquiryResult {
created: { buyer: boolean; contact: boolean };
}
// ------------------------------------------------- Notes / todos / Today
export type TodoKind = 'TASK' | 'REVIEW';
export type TodoStatus = 'OPEN' | 'DONE';
export interface Author {
id: string;
name: string;
}
/** What a note or todo hangs off, with the ids needed to link to it. */
export interface RefContext {
type: 'buyer' | 'deal' | 'business' | 'nda';
id: string;
label: string;
buyer_id: string | null;
business_id: string | null;
}
/** Exactly one of these for a note, at most one for a todo. */
export interface Ref {
buyer_id?: string;
deal_id?: string;
business_id?: string;
nda_id?: string;
}
export interface Note {
id: string;
text: string;
highlight: boolean;
created_at: string;
author: Author | null;
context: RefContext | null;
}
export interface Todo {
id: string;
text: string;
kind: TodoKind;
status: TodoStatus;
due_at: Day | null;
document_id: string | null;
document_name: string | null;
done_at: string | null;
done_by_name: string | null;
assigned_to: Author;
author: Author | null;
context: RefContext | null;
}
export interface FollowUp {
id: string;
status: DealStatus;
follow_up_at: Day;
buyer_id: string;
buyer_name: string;
business_id: string;
business_name: string;
created_by_id: string | null;
created_by_name: string | null;
}
export interface PendingNda {
id: string;
sent_at: Day;
buyer_id: string;
buyer_name: string;
created_by_id: string | null;
created_by_name: string | null;
}
export interface TodayCounts {
overdue_todos: number;
due_todos: number;
follow_ups: number;
pending_ndas: number;
total: number;
}
export interface Today {
todos: (Todo & { overdue: boolean })[];
follow_ups: FollowUp[];
pending_ndas: PendingNda[];
counts: TodayCounts;
}
export class ApiError extends Error {
constructor(readonly status: number, message: string) {
super(message);
@@ -189,9 +275,12 @@ function send<T>(method: 'POST' | 'PATCH', path: string, body?: unknown): Promis
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 del = <T>(path: string) => request<T>(path, { method: 'DELETE' });
const qs = (params: Record<string, string>) =>
new URLSearchParams(params).toString();
const qs = (params: Record<string, string | undefined>) =>
new URLSearchParams(
Object.entries(params).filter(([, value]) => value) as [string, string][],
).toString();
export const api = {
me: () => request<Staff>('/api/me'),
@@ -233,6 +322,36 @@ export const api = {
{ status, comment },
),
dealNotes: (id: string) => request<DealNote[]>(`/api/deals/${id}/notes`),
notes: (ref: Ref, includeRelated = false) =>
request<Note[]>(
`/api/notes?${qs({ ...ref, include_related: includeRelated ? 'true' : undefined })}`,
),
createNote: (ref: Ref, text: string, highlight: boolean) =>
post<Note>('/api/notes', { ...ref, text, highlight }),
updateNote: (id: string, body: { text?: string; highlight?: boolean }) =>
patch<Note>(`/api/notes/${id}`, body),
deleteNote: (id: string) => del<{ ok: boolean }>(`/api/notes/${id}`),
todos: (params: Ref & { assigned_to?: string; status?: TodoStatus }) =>
request<Todo[]>(`/api/todos?${qs({ ...params })}`),
createTodo: (body: Ref & Record<string, unknown>) => post<Todo>('/api/todos', body),
updateTodo: (id: string, body: Record<string, unknown>) => patch<Todo>(`/api/todos/${id}`, body),
todoDone: (id: string) => post<Todo>(`/api/todos/${id}/done`),
todoReopen: (id: string) => post<Todo>(`/api/todos/${id}/reopen`),
today: (staffId?: string) => request<Today>(`/api/today?${qs({ staff_id: staffId })}`),
followUpSent: (dealId: string, comment: string, rearm: boolean) =>
post<{ id: string; status: DealStatus; follow_up_at: Day | null }>(
`/api/deals/${dealId}/follow-up-sent`,
{ comment, rearm },
),
/** Pins a business file so a REVIEW todo can point at it. */
createDocument: (businessId: string, filePath: string) =>
post<{ id: string; nas_path: string }>('/api/documents', {
business_id: businessId,
path: filePath,
}),
};
/** Same-origin streaming URL of one file inside a business directory. */