fix:bug
This commit is contained in:
247
frontend/lib/api/client.ts
Normal file
247
frontend/lib/api/client.ts
Normal file
@@ -0,0 +1,247 @@
|
||||
/**
|
||||
* 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<string, unknown>;
|
||||
}
|
||||
|
||||
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<string, string>;
|
||||
}
|
||||
|
||||
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<T>(
|
||||
path: string,
|
||||
options: RequestInit & { params?: Record<string, string> } = {}
|
||||
): Promise<T> {
|
||||
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<T>;
|
||||
}
|
||||
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<Blob> {
|
||||
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<void> {
|
||||
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<CustomerRead[]>("/customers/"),
|
||||
get: (id: number) => request<CustomerRead>(`/customers/${id}`),
|
||||
create: (body: CustomerCreate) =>
|
||||
request<CustomerRead>("/customers/", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
update: (id: number, body: CustomerUpdate) =>
|
||||
request<CustomerRead>(`/customers/${id}`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
delete: (id: number) =>
|
||||
request<void>(`/customers/${id}`, { method: "DELETE" }),
|
||||
};
|
||||
|
||||
// --------------- Projects ---------------
|
||||
|
||||
export const projectsApi = {
|
||||
analyze: (body: RequirementAnalyzeRequest) =>
|
||||
request<RequirementAnalyzeResponse>("/projects/analyze", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
|
||||
update: (projectId: number, body: ProjectUpdate) =>
|
||||
request<ProjectRead>(`/projects/${projectId}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
|
||||
generateQuote: (projectId: number) =>
|
||||
request<QuoteGenerateResponse>(`/projects/${projectId}/generate_quote`, {
|
||||
method: "POST",
|
||||
}),
|
||||
|
||||
generateContract: (projectId: number, body: ContractGenerateRequest) =>
|
||||
request<ContractGenerateResponse>(
|
||||
`/projects/${projectId}/generate_contract`,
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify(body),
|
||||
}
|
||||
),
|
||||
};
|
||||
|
||||
export async function listProjects(): Promise<ProjectRead[]> {
|
||||
return request<ProjectRead[]>("/projects/");
|
||||
}
|
||||
|
||||
export async function getProject(projectId: number): Promise<ProjectRead> {
|
||||
return request<ProjectRead>(`/projects/${projectId}`);
|
||||
}
|
||||
|
||||
// --------------- Finance ---------------
|
||||
|
||||
export const financeApi = {
|
||||
sync: () =>
|
||||
request<FinanceSyncResponse>("/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<void> => {
|
||||
const path = `/finance/download/${month}`;
|
||||
await downloadFileAsBlob(path, `finance_${month}.zip`, "application/zip");
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user