/** * API client for the FastAPI backend (Ops-Core). * Base URL is read from NEXT_PUBLIC_API_BASE (default http://localhost:8000). */ const getBase = (): string => { if (typeof window !== "undefined") { return process.env.NEXT_PUBLIC_API_BASE ?? "http://localhost:8000"; } return process.env.NEXT_PUBLIC_API_BASE ?? "http://localhost:8000"; }; export const apiBase = getBase; // --------------- Types (mirror FastAPI schemas) --------------- export interface CustomerRead { id: number; name: string; contact_info: string | null; created_at: string; } export interface CustomerCreate { name: string; contact_info?: string | null; } export interface CustomerUpdate { name?: string; contact_info?: string | null; } export interface RequirementAnalyzeRequest { customer_id: number; raw_text: string; } export interface RequirementAnalyzeResponse { project_id: number; ai_solution_md: string; ai_solution_json: Record; } export interface QuoteGenerateResponse { quote_id: number; project_id: number; total_amount: number; excel_path: string; pdf_path: string; } export interface ContractGenerateRequest { delivery_date: string; extra_placeholders?: Record; } export interface ContractGenerateResponse { project_id: number; contract_path: string; } export interface FinanceSyncResult { id: number; month: string; type: string; file_name: string; file_path: string; } export interface FinanceSyncResponse { items: FinanceSyncResult[]; } export interface ProjectRead { id: number; customer_id: number; raw_requirement: string; ai_solution_md: string | null; status: string; created_at: string; customer?: CustomerRead; quotes?: { id: number; total_amount: string; file_path: string }[]; } export interface ProjectUpdate { ai_solution_md?: string | null; status?: string; } // --------------- Request helper --------------- async function request( path: string, options: RequestInit & { params?: Record } = {} ): Promise { const { params, ...init } = options; const base = apiBase(); let url = `${base}${path}`; if (params && Object.keys(params).length > 0) { const search = new URLSearchParams(params).toString(); url += (path.includes("?") ? "&" : "?") + search; } const res = await fetch(url, { ...init, headers: { "Content-Type": "application/json", ...init.headers, }, }); if (!res.ok) { const text = await res.text(); let detail = text; try { const j = JSON.parse(text); detail = j.detail ?? text; } catch { // keep text } throw new Error(detail); } const contentType = res.headers.get("content-type"); if (contentType?.includes("application/json")) { return res.json() as Promise; } return undefined as T; } /** Download a file from the backend and return a Blob (for Excel/PDF/Zip). */ export async function downloadBlob(path: string): Promise { const base = apiBase(); const url = path.startsWith("http") ? path : `${base}${path.startsWith("/") ? "" : "/"}${path}`; const res = await fetch(url, { credentials: "include" }); if (!res.ok) throw new Error(`Download failed: ${res.status}`); return res.blob(); } /** * Trigger download in the browser (Excel, PDF, or Zip). * path: backend-returned path e.g. "data/quotes/quote_project_1.xlsx" -> we request /data/quotes/quote_project_1.xlsx */ export function downloadFile(path: string, filename: string): void { const base = apiBase(); const normalized = path.startsWith("/") ? path : `/${path}`; const url = `${base}${normalized}`; const a = document.createElement("a"); a.href = url; a.download = filename; a.rel = "noopener noreferrer"; a.target = "_blank"; document.body.appendChild(a); a.click(); document.body.removeChild(a); } /** Same as downloadFile but using fetch + Blob for consistent CORS and filename control. */ export async function downloadFileAsBlob( path: string, filename: string, mime?: string ): Promise { const blob = await downloadBlob(path); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } // --------------- Customers --------------- export const customersApi = { list: () => request("/customers/"), get: (id: number) => request(`/customers/${id}`), create: (body: CustomerCreate) => request("/customers/", { method: "POST", body: JSON.stringify(body), }), update: (id: number, body: CustomerUpdate) => request(`/customers/${id}`, { method: "PUT", body: JSON.stringify(body), }), delete: (id: number) => request(`/customers/${id}`, { method: "DELETE" }), }; // --------------- Projects --------------- export const projectsApi = { analyze: (body: RequirementAnalyzeRequest) => request("/projects/analyze", { method: "POST", body: JSON.stringify(body), }), update: (projectId: number, body: ProjectUpdate) => request(`/projects/${projectId}`, { method: "PATCH", body: JSON.stringify(body), }), generateQuote: (projectId: number) => request(`/projects/${projectId}/generate_quote`, { method: "POST", }), generateContract: (projectId: number, body: ContractGenerateRequest) => request( `/projects/${projectId}/generate_contract`, { method: "POST", body: JSON.stringify(body), } ), }; export async function listProjects(): Promise { return request("/projects/"); } export async function getProject(projectId: number): Promise { return request(`/projects/${projectId}`); } // --------------- Finance --------------- export const financeApi = { sync: () => request("/finance/sync", { method: "POST" }), /** Returns the URL to download the zip (or use downloadFile with the path). */ getDownloadUrl: (month: string) => `${apiBase()}/finance/download/${month}`, /** Download monthly zip as blob and trigger save. */ downloadMonth: async (month: string): Promise => { const path = `/finance/download/${month}`; await downloadFileAsBlob(path, `finance_${month}.zip`, "application/zip"); }, };