This commit is contained in:
2026-04-09 15:17:07 +03:00
parent c5f6d2f596
commit 29cc9c6f4e
23 changed files with 420 additions and 190 deletions
+4 -4
View File
@@ -30221,7 +30221,7 @@
}
}
},
"/api/import-parts": {
"/api/parts/import": {
"post": {
"tags": [
"Import Parts"
@@ -30264,7 +30264,7 @@
}
}
},
"/api/export-parts": {
"/api/parts/export": {
"post": {
"tags": [
"Export Parts"
@@ -30343,12 +30343,12 @@
}
}
},
"/api/toggle-part-status": {
"/api/parts/toggle-status": {
"post": {
"tags": [
"Toggle Part Status"
],
"summary": "POST /api/toggle-part-status",
"summary": "POST /api/parts/toggle-status",
"requestBody": {
"required": true,
"content": {
+21 -15
View File
@@ -1,6 +1,6 @@
{
"info": {
"_postman_id": "bf46649e-48e0-49d5-aa87-1790dcbc3c24",
"_postman_id": "a879ecf0-769b-47c2-86ec-dcc11bf1973d",
"name": "Reparee Collection",
"description": "Auto-generated from OpenAPI spec. Import storage/app/openapi-default.json for the full schema.",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
@@ -16122,7 +16122,7 @@
"job-cards"
]
},
"description": "Paginated list. Eager loads: labels, customer, vehicle, department, documents, attachmentFiles, serviceWriter, salesPerson, primaryTechnician. withCount: purchase_orders_count, bills_count, expenses_count, tasks_count, appointments_count, inspections_count, services_count, parts_count, expense_items_count (line-level rows on the job card). Response: `data` and `meta` only. Nested employees may include full columns in production; example uses a shortened shape. Query: status, customer_id, vehicle_id, per_page, sort_by, sort_order, start_date, end_date, service_writer_id, department_id, has_start_or_delivery_date, has_start_date, has_delivery_date, has_both_dates."
"description": "Paginated list. Eager loads: labels, customer, vehicle, department, documents, attachmentFiles, serviceWriter, salesPerson, primaryTechnician. withCount: purchase_orders_count, bills_count, expenses_count, tasks_count, appointments_count, inspections_count, services_count, parts_count, expense_items_count (line-level rows on the job card). Response: `data` and `meta` only. Nested employees may include full columns in production; example uses a shortened shape. Query: sort_by, sort_order, status, customer_id, insurer_id, sales_person_id, primary_technician_id, service_writer_id, label_ids, delivery_date, convert_invoice, check_in_date, check_out_date, vehicle_id, start_date, end_date, department_id, search, has_start_or_delivery_date, has_start_date, has_delivery_date, has_both_dates, per_page."
},
"response": [
{
@@ -22023,13 +22023,14 @@
]
},
"url": {
"raw": "{{base_url}}/api/import-parts",
"raw": "{{base_url}}/api/parts/import",
"host": [
"{{base_url}}"
],
"path": [
"api",
"import-parts"
"parts",
"import"
]
}
},
@@ -22071,13 +22072,14 @@
]
},
"url": {
"raw": "{{base_url}}/api/import-parts",
"raw": "{{base_url}}/api/parts/import",
"host": [
"{{base_url}}"
],
"path": [
"api",
"import-parts"
"parts",
"import"
]
}
},
@@ -22134,13 +22136,14 @@
}
},
"url": {
"raw": "{{base_url}}/api/export-parts",
"raw": "{{base_url}}/api/parts/export",
"host": [
"{{base_url}}"
],
"path": [
"api",
"export-parts"
"parts",
"export"
]
}
},
@@ -22179,13 +22182,14 @@
}
},
"url": {
"raw": "{{base_url}}/api/export-parts",
"raw": "{{base_url}}/api/parts/export",
"host": [
"{{base_url}}"
],
"path": [
"api",
"export-parts"
"parts",
"export"
]
}
},
@@ -22209,7 +22213,7 @@
"name": "Toggle Part Status",
"item": [
{
"name": "POST /api/toggle-part-status",
"name": "POST /api/parts/toggle-status",
"request": {
"auth": {
"type": "bearer",
@@ -22242,13 +22246,14 @@
}
},
"url": {
"raw": "{{base_url}}/api/toggle-part-status",
"raw": "{{base_url}}/api/parts/toggle-status",
"host": [
"{{base_url}}"
],
"path": [
"api",
"toggle-part-status"
"parts",
"toggle-status"
]
}
},
@@ -22287,13 +22292,14 @@
}
},
"url": {
"raw": "{{base_url}}/api/toggle-part-status",
"raw": "{{base_url}}/api/parts/toggle-status",
"host": [
"{{base_url}}"
],
"path": [
"api",
"toggle-part-status"
"parts",
"toggle-status"
]
}
},
+11 -11
View File
@@ -5,8 +5,8 @@ import type { ApiPath, ApiRequestBody } from "../infra/types"
export const PARTS_ROUTES = {
INDEX: "/api/parts",
BY_ID: "/api/parts/{id}",
IMPORT: "/api/import-parts",
EXPORT: "/api/export-parts",
IMPORT: "/api/parts/import",
EXPORT: "/api/parts/export",
TOGGLE_STATUS: "/api/toggle-part-status",
} as const satisfies Record<string, ApiPath>
@@ -15,15 +15,15 @@ export class PartsClient extends CrudClient<
typeof PARTS_ROUTES.BY_ID
> {
constructor(baseUrl?: string, defaultOptions?: ApiClientOptions) {
super(baseUrl, defaultOptions, PARTS_ROUTES.INDEX, PARTS_ROUTES.BY_ID)
}
async import(payload: ApiRequestBody<typeof PARTS_ROUTES.IMPORT, "post">) {
return this.post(PARTS_ROUTES.IMPORT, payload)
}
async export(payload: ApiRequestBody<typeof PARTS_ROUTES.EXPORT, "post">) {
return this.post(PARTS_ROUTES.EXPORT, payload)
super(
baseUrl,
defaultOptions,
PARTS_ROUTES.INDEX,
PARTS_ROUTES.BY_ID,
PARTS_ROUTES.IMPORT,
PARTS_ROUTES.EXPORT,
"POST",
)
}
async toggleStatus(payload: ApiRequestBody<typeof PARTS_ROUTES.TOGGLE_STATUS, "post">) {
+10 -10
View File
@@ -1,6 +1,6 @@
import { CrudClient } from "../infra/crud-client"
import type { ApiClientOptions } from "../infra/client"
import type { ApiPath, ApiRequestBody } from "../infra/types"
import type { ApiPath } from "../infra/types"
export const SERVICE_ROUTES = {
INDEX: "/api/services",
@@ -14,14 +14,14 @@ export class ServicesClient extends CrudClient<
typeof SERVICE_ROUTES.BY_ID
> {
constructor(baseUrl?: string, defaultOptions?: ApiClientOptions) {
super(baseUrl, defaultOptions, SERVICE_ROUTES.INDEX, SERVICE_ROUTES.BY_ID)
}
async import(payload: ApiRequestBody<typeof SERVICE_ROUTES.IMPORT, "post">) {
return this.post(SERVICE_ROUTES.IMPORT, payload)
}
async export(payload: ApiRequestBody<typeof SERVICE_ROUTES.EXPORT, "post">) {
return this.post(SERVICE_ROUTES.EXPORT, payload)
super(
baseUrl,
defaultOptions,
SERVICE_ROUTES.INDEX,
SERVICE_ROUTES.BY_ID,
SERVICE_ROUTES.IMPORT,
SERVICE_ROUTES.EXPORT,
"POST",
)
}
}
+22
View File
@@ -190,6 +190,28 @@ export class ApiClient {
return data
}
protected async fetchBlob(endpoint: string, options?: { method?: "GET" | "POST"; body?: unknown }): Promise<Blob> {
const url = `${this.normalizeBaseUrl(this.baseUrl)}${endpoint}`
const headers = new Headers(this.defaultOptions.headers as Record<string, string>)
const method = options?.method ?? "POST"
const init: RequestInit = { method, headers }
if (options?.body) {
headers.set("Content-Type", "application/json")
init.body = JSON.stringify(options.body)
}
const response = await fetch(url, init)
if (!response.ok) {
const text = await response.text()
const data = text ? JSON.parse(text) : null
throw new ApiError(response.status, response.statusText, endpoint, method.toLowerCase(), data)
}
return response.blob()
}
private toFetchOptions<Path extends ApiPath, Method extends HttpMethod>(
options: ApiRequestOptions<Path, Method>,
): Record<string, unknown> {
+21 -1
View File
@@ -6,6 +6,8 @@ export const DEFAULT_PER_PAGE = 10
type CrudIndexRoute = ApiPathByMethod<"get"> & ApiPathByMethod<"post">
type CrudByIdRoute = ApiPathByMethod<"put"> & ApiPathByMethod<"delete">
type CrudImportRoute = ApiPathByMethod<"post">
type CrudExportRoute = ApiPathByMethod<"get"> | ApiPathByMethod<"post">
export type CrudTypeOverrides = {
listResponse?: unknown
@@ -29,7 +31,10 @@ export abstract class CrudClient<
constructor(baseUrl?: string, defaultOptions?: ApiClientOptions,
public indexRoute?: IndexRoute,
public byIdRoute?: ByIdRoute) {
public byIdRoute?: ByIdRoute,
public importRoute?: CrudImportRoute,
public exportRoute?: CrudExportRoute,
public exportMethod: "GET" | "POST" = "GET") {
super(baseUrl, defaultOptions)
@@ -54,6 +59,21 @@ export abstract class CrudClient<
async destroy(id: string): Promise<Resolve<Overrides, 'destroyResponse', ApiResponse<ByIdRoute, "delete">>> {
return this.delete<ByIdRoute>(this.byIdRoute as ByIdRoute, { params: { id } } as never) as never
}
async importData(file: File): Promise<{ message?: string }> {
const formData = new FormData()
formData.append("file", file)
const route = (this.importRoute ?? `${this.indexRoute}/import`) as string
return this.postFormData(route, formData)
}
async exportData(filters?: Record<string, unknown>): Promise<Blob> {
const route = (this.exportRoute ?? `${this.indexRoute}/export`) as string
if (this.exportMethod === "POST") {
return this.fetchBlob(route, { method: "POST", body: filters ?? {} })
}
return this.fetchBlob(route)
}
}
export type BaseCrudItem = { id: number }
+4 -4
View File
@@ -20275,7 +20275,7 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/import-parts": {
"/api/parts/import": {
parameters: {
query?: never;
header?: never;
@@ -20325,7 +20325,7 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/export-parts": {
"/api/parts/export": {
parameters: {
query?: never;
header?: never;
@@ -20393,7 +20393,7 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/toggle-part-status": {
"/api/parts/toggle-status": {
parameters: {
query?: never;
header?: never;
@@ -20402,7 +20402,7 @@ export interface paths {
};
get?: never;
put?: never;
/** POST /api/toggle-part-status */
/** POST /api/parts/toggle-status */
post: {
parameters: {
query?: never;