fix invoice informations
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { CrudClient } from "../infra/crud-client"
|
||||
import type { ApiClientOptions } from "../infra/client"
|
||||
import type { ApiPath } from "../infra/types"
|
||||
import type { ApiPath, ApiResponse } from "../infra/types"
|
||||
|
||||
export const ESTIMATE_ROUTES = {
|
||||
INDEX: "/api/estimates",
|
||||
@@ -25,8 +25,8 @@ export class EstimatesClient extends CrudClient<
|
||||
// Note: GET /api/estimates/{id} is not in the OpenAPI schema.
|
||||
// This method uses a type cast and relies on the backend supporting the route.
|
||||
async getById(id: string) {
|
||||
const data = await this.get(ESTIMATE_ROUTES.INDEX, { query: { id } } as never)
|
||||
return {...data, data: (data as any)?.data?.[0] ?? null }
|
||||
const data = await this.get(ESTIMATE_ROUTES.BY_ID, { params: { id } })
|
||||
return data;
|
||||
}
|
||||
|
||||
// ── Estimate Services ──
|
||||
@@ -48,7 +48,7 @@ export class EstimatesClient extends CrudClient<
|
||||
|
||||
// ── Estimate Parts ──
|
||||
async listParts(estimateId: string) {
|
||||
return this.get(ESTIMATE_ROUTES.PARTS, { params: { id: estimateId } } as never)
|
||||
return this.get(ESTIMATE_ROUTES.PARTS, { params: { id: estimateId } }) as Promise<ApiResponse<typeof ESTIMATE_ROUTES.PARTS, "get">>
|
||||
}
|
||||
|
||||
async addPart(estimateId: string, payload: { part_id?: number; quantity?: number; rate?: string; description?: string }) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
} from "./types"
|
||||
import createClient from "openapi-fetch"
|
||||
import type { paths } from "../../types/index"
|
||||
import { logger } from "./logger"
|
||||
|
||||
type HttpMethod = "get" | "post" | "put" | "delete" | "patch"
|
||||
|
||||
@@ -73,6 +74,8 @@ export class ApiClient {
|
||||
const opts = options as never
|
||||
const body = (options as Record<string, unknown>).body as never
|
||||
|
||||
|
||||
|
||||
switch (method) {
|
||||
case "get":
|
||||
return this.get(ep, opts) as Promise<ApiResponse<Path, Method>>
|
||||
@@ -96,12 +99,15 @@ export class ApiClient {
|
||||
options: ApiRequestOptions<Path, "get"> = {} as ApiRequestOptions<Path, "get">,
|
||||
): Promise<ApiResponse<Path, "get">> {
|
||||
const requestOptions = this.toFetchOptions(options)
|
||||
const startTime = Date.now()
|
||||
logger.debug({ method: "GET", endpoint }, "API request")
|
||||
|
||||
try {
|
||||
const { data, error, response } = await this.client.GET(endpoint, requestOptions as never)
|
||||
return this.resolveResult(endpoint, "get", data, error, response)
|
||||
return this.resolveResult(endpoint, "get", data, error, response, startTime)
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError) throw err
|
||||
logger.error({ method: "GET", endpoint, duration: Date.now() - startTime }, "Network error")
|
||||
throw this.createNetworkError(endpoint, "get")
|
||||
}
|
||||
}
|
||||
@@ -112,12 +118,15 @@ export class ApiClient {
|
||||
options: Omit<ApiRequestOptions<Path, "post">, "body"> = {} as Omit<ApiRequestOptions<Path, "post">, "body">,
|
||||
): Promise<ApiResponse<Path, "post">> {
|
||||
const requestOptions = this.toFetchOptions({ ...options, body })
|
||||
const startTime = Date.now()
|
||||
logger.debug({ method: "POST", endpoint }, "API request")
|
||||
|
||||
try {
|
||||
const { data, error, response } = await this.client.POST(endpoint, requestOptions as never)
|
||||
return this.resolveResult(endpoint, "post", data, error, response)
|
||||
return this.resolveResult(endpoint, "post", data, error, response, startTime)
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError) throw err
|
||||
logger.error({ method: "POST", endpoint, duration: Date.now() - startTime }, "Network error")
|
||||
throw this.createNetworkError(endpoint, "post")
|
||||
}
|
||||
}
|
||||
@@ -128,12 +137,15 @@ export class ApiClient {
|
||||
options: Omit<ApiRequestOptions<Path, "put">, "body"> = {} as Omit<ApiRequestOptions<Path, "put">, "body">,
|
||||
): Promise<ApiResponse<Path, "put">> {
|
||||
const requestOptions = this.toFetchOptions({ ...options, body })
|
||||
const startTime = Date.now()
|
||||
logger.debug({ method: "PUT", endpoint }, "API request")
|
||||
|
||||
try {
|
||||
const { data, error, response } = await this.client.PUT(endpoint, requestOptions as never)
|
||||
return this.resolveResult(endpoint, "put", data, error, response)
|
||||
return this.resolveResult(endpoint, "put", data, error, response, startTime)
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError) throw err
|
||||
logger.error({ method: "PUT", endpoint, duration: Date.now() - startTime }, "Network error")
|
||||
throw this.createNetworkError(endpoint, "put")
|
||||
}
|
||||
}
|
||||
@@ -143,12 +155,15 @@ export class ApiClient {
|
||||
options: ApiRequestOptions<Path, "delete"> = {} as ApiRequestOptions<Path, "delete">,
|
||||
): Promise<ApiResponse<Path, "delete">> {
|
||||
const requestOptions = this.toFetchOptions(options)
|
||||
const startTime = Date.now()
|
||||
logger.debug({ method: "DELETE", endpoint }, "API request")
|
||||
|
||||
try {
|
||||
const { data, error, response } = await this.client.DELETE(endpoint, requestOptions as never)
|
||||
return this.resolveResult(endpoint, "delete", data, error, response)
|
||||
return this.resolveResult(endpoint, "delete", data, error, response, startTime)
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError) throw err
|
||||
logger.error({ method: "DELETE", endpoint, duration: Date.now() - startTime }, "Network error")
|
||||
throw this.createNetworkError(endpoint, "delete")
|
||||
}
|
||||
}
|
||||
@@ -159,12 +174,15 @@ export class ApiClient {
|
||||
options: Omit<ApiRequestOptions<Path, "patch">, "body"> = {} as Omit<ApiRequestOptions<Path, "patch">, "body">,
|
||||
): Promise<ApiResponse<Path, "patch">> {
|
||||
const requestOptions = this.toFetchOptions({ ...options, body })
|
||||
const startTime = Date.now()
|
||||
logger.debug({ method: "PATCH", endpoint }, "API request")
|
||||
|
||||
try {
|
||||
const { data, error, response } = await this.client.PATCH(endpoint, requestOptions as never)
|
||||
return this.resolveResult(endpoint, "patch", data, error, response)
|
||||
return this.resolveResult(endpoint, "patch", data, error, response, startTime)
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError) throw err
|
||||
logger.error({ method: "PATCH", endpoint, duration: Date.now() - startTime }, "Network error")
|
||||
throw this.createNetworkError(endpoint, "patch")
|
||||
}
|
||||
}
|
||||
@@ -179,14 +197,21 @@ export class ApiClient {
|
||||
headers.set("Accept", "application/json")
|
||||
// Content-Type is intentionally omitted — fetch sets multipart/form-data + boundary automatically
|
||||
|
||||
const startTime = Date.now()
|
||||
logger.debug({ method: "POST", endpoint }, "API request (form-data)")
|
||||
|
||||
const response = await fetch(url, { method: "POST", headers, body: formData })
|
||||
const text = await response.text()
|
||||
const data = text ? JSON.parse(text) : null
|
||||
const duration = Date.now() - startTime
|
||||
|
||||
if (!response.ok) {
|
||||
const level = response.status >= 500 ? "error" : "warn"
|
||||
logger[level]({ method: "POST", endpoint, status: response.status, duration }, data?.message ?? "API request failed")
|
||||
throw new ApiError(response.status, response.statusText, endpoint, "post", data)
|
||||
}
|
||||
|
||||
logger.debug({ method: "POST", endpoint, status: response.status, duration }, "API request completed (form-data)")
|
||||
return data
|
||||
}
|
||||
|
||||
@@ -201,14 +226,21 @@ export class ApiClient {
|
||||
init.body = JSON.stringify(options.body)
|
||||
}
|
||||
|
||||
const startTime = Date.now()
|
||||
logger.debug({ method, endpoint }, "API request (blob)")
|
||||
|
||||
const response = await fetch(url, init)
|
||||
const duration = Date.now() - startTime
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text()
|
||||
const data = text ? JSON.parse(text) : null
|
||||
const level = response.status >= 500 ? "error" : "warn"
|
||||
logger[level]({ method, endpoint, status: response.status, duration }, data?.message ?? "API request failed")
|
||||
throw new ApiError(response.status, response.statusText, endpoint, method.toLowerCase(), data)
|
||||
}
|
||||
|
||||
logger.debug({ method, endpoint, status: response.status, duration }, "API request completed (blob)")
|
||||
return response.blob()
|
||||
}
|
||||
|
||||
@@ -250,17 +282,28 @@ export class ApiClient {
|
||||
data: unknown,
|
||||
error: unknown,
|
||||
response: Response,
|
||||
startTime: number,
|
||||
): ApiResponse<Path, Method> {
|
||||
const duration = Date.now() - startTime
|
||||
|
||||
if (error !== undefined) {
|
||||
const payload = this.normalizeErrorPayload(error)
|
||||
if (response.status >= 500) {
|
||||
logger.error({ method: method.toUpperCase(), endpoint, status: response.status, duration }, payload?.message ?? "API request failed")
|
||||
} else {
|
||||
logger.warn({ method: method.toUpperCase(), endpoint, status: response.status, duration }, payload?.message ?? "API request failed")
|
||||
}
|
||||
throw new ApiError(
|
||||
response.status,
|
||||
response.statusText,
|
||||
endpoint,
|
||||
method,
|
||||
this.normalizeErrorPayload(error),
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
logger.debug({ method: method.toUpperCase(), endpoint, status: response.status, duration, data }, "API request completed")
|
||||
|
||||
return data as ApiResponse<Path, Method>
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import pino from "pino"
|
||||
|
||||
/**
|
||||
* Isomorphic logger for the API client.
|
||||
*
|
||||
* - In Node.js (server / Next.js RSC / server actions): pino writes newline-delimited
|
||||
* JSON to stdout — structured and machine-parseable.
|
||||
* - In the browser (Next.js client components): pino switches to the browser
|
||||
* transport automatically and delegates to the appropriate `console.*` method
|
||||
* (console.debug, console.warn, console.error, …).
|
||||
*
|
||||
* Log level defaults:
|
||||
* development → debug (all messages)
|
||||
* production → warn (warnings + errors only)
|
||||
*
|
||||
* Override with the NEXT_PUBLIC_LOG_LEVEL env var (e.g. "info").
|
||||
*/
|
||||
const level =
|
||||
(process.env.NEXT_PUBLIC_LOG_LEVEL as pino.Level | undefined) ??
|
||||
(process.env.NODE_ENV === "production" ? "warn" : "debug")
|
||||
|
||||
export const logger = pino({
|
||||
name: "garage-api",
|
||||
level,
|
||||
browser: {
|
||||
/**
|
||||
* Pass a plain object to `console.*` so DevTools display
|
||||
* log fields (method, endpoint, status, duration) as expandable props.
|
||||
*/
|
||||
asObject: true,
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user