Issue 1: repo scaffold and project structure
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* Typed API client for Ten31Portal backend.
|
||||
*/
|
||||
|
||||
// --- Types ---
|
||||
|
||||
export type UserRole = "approver" | "cfo" | "fund_admin" | "viewer";
|
||||
export type EntityType = "fund" | "spv" | "gp" | "mgmt_co";
|
||||
export type EntityStatus = "active" | "closed";
|
||||
export type RoundStatus = "draft" | "submitted" | "approved" | "returned";
|
||||
|
||||
export interface User {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
role: UserRole;
|
||||
is_active: boolean;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface Entity {
|
||||
id: number;
|
||||
name: string;
|
||||
type: EntityType;
|
||||
vintage_year: number | null;
|
||||
fund_size_cents: number | null;
|
||||
status: EntityStatus;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface Holding {
|
||||
id: number;
|
||||
entity_id: number;
|
||||
company_name: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface Position {
|
||||
id: number;
|
||||
holding_id: number;
|
||||
security_name: string;
|
||||
investment_date: string;
|
||||
shares: string | null;
|
||||
cost_cents: number;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface Valuation {
|
||||
id: number;
|
||||
round_id: number;
|
||||
position_id: number;
|
||||
value_cents: number;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface ValuationRound {
|
||||
id: number;
|
||||
entity_id: number;
|
||||
quarter_end: string;
|
||||
status: RoundStatus;
|
||||
submitted_by: number | null;
|
||||
submitted_at: string | null;
|
||||
approved_by: number | null;
|
||||
approved_at: string | null;
|
||||
return_note: string | null;
|
||||
is_seed: boolean;
|
||||
created_at: string;
|
||||
valuations: Valuation[];
|
||||
}
|
||||
|
||||
export interface AuditEntry {
|
||||
id: number;
|
||||
actor_user_id: number | null;
|
||||
action: string;
|
||||
object_type: string;
|
||||
object_id: number | null;
|
||||
detail: unknown;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
// --- API helpers ---
|
||||
|
||||
class ApiError extends Error {
|
||||
status: number;
|
||||
constructor(status: number, message: string) {
|
||||
super(message);
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
|
||||
async function request<T>(path: string, options?: RequestInit): Promise<T> {
|
||||
const res = await fetch(path, {
|
||||
...options,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...options?.headers,
|
||||
},
|
||||
});
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => ({ detail: res.statusText }));
|
||||
throw new ApiError(res.status, body.detail || res.statusText);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
// --- Auth ---
|
||||
|
||||
export const api = {
|
||||
login: (email: string, password: string) =>
|
||||
request<User>("/api/auth/login", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ email, password }),
|
||||
}),
|
||||
|
||||
logout: () => request<{ status: string }>("/api/auth/logout", { method: "POST" }),
|
||||
|
||||
me: () => request<User>("/api/auth/me"),
|
||||
|
||||
// Entities
|
||||
listEntities: () => request<Entity[]>("/api/entities"),
|
||||
getEntity: (id: number) => request<Entity>(`/api/entities/${id}`),
|
||||
createEntity: (data: Partial<Entity>) =>
|
||||
request<Entity>("/api/entities", { method: "POST", body: JSON.stringify(data) }),
|
||||
updateEntity: (id: number, data: Partial<Entity>) =>
|
||||
request<Entity>(`/api/entities/${id}`, { method: "PATCH", body: JSON.stringify(data) }),
|
||||
|
||||
// Holdings
|
||||
listHoldings: (entityId: number) =>
|
||||
request<Holding[]>(`/api/entities/${entityId}/holdings`),
|
||||
createHolding: (entityId: number, data: { company_name: string }) =>
|
||||
request<Holding>(`/api/entities/${entityId}/holdings`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
|
||||
// Positions
|
||||
listPositions: (holdingId: number) =>
|
||||
request<Position[]>(`/api/holdings/${holdingId}/positions`),
|
||||
|
||||
// Rounds
|
||||
listRounds: (entityId: number) =>
|
||||
request<ValuationRound[]>(`/api/entities/${entityId}/rounds`),
|
||||
getRound: (roundId: number) => request<ValuationRound>(`/api/rounds/${roundId}`),
|
||||
createRound: (entityId: number, quarterEnd: string) =>
|
||||
request<ValuationRound>(`/api/entities/${entityId}/rounds`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ quarter_end: quarterEnd }),
|
||||
}),
|
||||
updateValuations: (roundId: number, valuations: { position_id: number; value_cents: number }[]) =>
|
||||
request<ValuationRound>(`/api/rounds/${roundId}/valuations`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ valuations }),
|
||||
}),
|
||||
submitRound: (roundId: number) =>
|
||||
request<ValuationRound>(`/api/rounds/${roundId}/submit`, { method: "POST" }),
|
||||
approveRound: (roundId: number) =>
|
||||
request<ValuationRound>(`/api/rounds/${roundId}/approve`, { method: "POST" }),
|
||||
returnRound: (roundId: number, note: string) =>
|
||||
request<ValuationRound>(`/api/rounds/${roundId}/return`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ note }),
|
||||
}),
|
||||
|
||||
// Audit
|
||||
listAudit: (params?: { object_type?: string; object_id?: number; page?: number }) => {
|
||||
const q = new URLSearchParams();
|
||||
if (params?.object_type) q.set("object_type", params.object_type);
|
||||
if (params?.object_id) q.set("object_id", String(params.object_id));
|
||||
if (params?.page) q.set("page", String(params.page));
|
||||
return request<AuditEntry[]>(`/api/audit?${q}`);
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user