fix invoice informations

This commit is contained in:
2026-04-16 11:42:11 +03:00
parent 973149e974
commit f17dd1486c
20 changed files with 2579 additions and 178 deletions
+4 -4
View File
@@ -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 }) {
+49 -6
View File
@@ -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>
}
+32
View File
@@ -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,
},
})