feat: add ItemOverviewDto for enriched item data in items list
feat: implement CustomerDetailPage for detailed customer view and editing feat: create CustomersPage for listing and managing customers with filters feat: add ColumnsMenu component for customizable table columns visibility feat: implement CSV export functionality for data download feat: add customer code generation logic for unique customer identifiers
This commit is contained in:
@@ -1,15 +1,35 @@
|
||||
import { apiRequest, buildQuery } from "@/lib/api-client"
|
||||
import { PagedResponse } from "@/types/common"
|
||||
import { Customer } from "@/types/customers"
|
||||
// One typed client method per customer endpoint. ETag/If-Match on update, PATCH status
|
||||
// for deactivate (masters are deactivated, not hard-deleted, FR-MD-08).
|
||||
import { apiRequest, apiRequestWithETag, buildQuery } from "@/lib/api-client"
|
||||
import { ApiResult, EntityStatus, PagedResponse } from "@/types/common"
|
||||
import { CreateCustomerRequest, Customer, CustomerType, UpdateCustomerRequest } from "@/types/customers"
|
||||
|
||||
export interface ListCustomersParams {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
q?: string
|
||||
status?: EntityStatus
|
||||
customerType?: CustomerType
|
||||
}
|
||||
|
||||
export const customersApi = {
|
||||
list(params: ListCustomersParams = {}): Promise<PagedResponse<Customer>> {
|
||||
return apiRequest<PagedResponse<Customer>>(`/customers${buildQuery(params)}`)
|
||||
},
|
||||
|
||||
get(customerId: number): Promise<ApiResult<Customer>> {
|
||||
return apiRequestWithETag<Customer>(`/customers/${customerId}`)
|
||||
},
|
||||
|
||||
create(request: CreateCustomerRequest): Promise<ApiResult<Customer>> {
|
||||
return apiRequestWithETag<Customer>("/customers", { method: "POST", body: request })
|
||||
},
|
||||
|
||||
update(customerId: number, request: UpdateCustomerRequest, ifMatch: string): Promise<ApiResult<Customer>> {
|
||||
return apiRequestWithETag<Customer>(`/customers/${customerId}`, { method: "PUT", body: request, ifMatch })
|
||||
},
|
||||
|
||||
updateStatus(customerId: number, status: EntityStatus): Promise<void> {
|
||||
return apiRequest<void>(`/customers/${customerId}/status`, { method: "PATCH", body: { status } })
|
||||
},
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
CreateItemRequest,
|
||||
Item,
|
||||
ItemListItem,
|
||||
ItemOverview,
|
||||
ItemReorderSetting,
|
||||
ItemScanResult,
|
||||
TrackingMode,
|
||||
@@ -25,11 +26,27 @@ export interface ListItemsParams {
|
||||
sort?: string
|
||||
}
|
||||
|
||||
export interface ListItemOverviewParams {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
q?: string
|
||||
status?: EntityStatus
|
||||
categoryId?: number
|
||||
subCategoryId?: number
|
||||
brandId?: number
|
||||
}
|
||||
|
||||
export const itemsApi = {
|
||||
list(params: ListItemsParams = {}): Promise<PagedResponse<ItemListItem>> {
|
||||
return apiRequest<PagedResponse<ItemListItem>>(`/items${buildQuery(params)}`)
|
||||
},
|
||||
|
||||
/** Items list enriched with stock value, low-stock, last-purchase and 30-day sales
|
||||
* aggregates — backs the Items overview table, not the lightweight document pickers. */
|
||||
overview(params: ListItemOverviewParams = {}): Promise<PagedResponse<ItemOverview>> {
|
||||
return apiRequest<PagedResponse<ItemOverview>>(`/items/overview${buildQuery(params)}`)
|
||||
},
|
||||
|
||||
get(itemId: number): Promise<ApiResult<Item>> {
|
||||
return apiRequestWithETag<Item>(`/items/${itemId}`)
|
||||
},
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
/** One column of a CSV export: how to label it and how to read a cell's value from a row. */
|
||||
export interface CsvColumn<T> {
|
||||
header: string
|
||||
value: (row: T) => string | number | boolean | null | undefined
|
||||
}
|
||||
|
||||
function escapeCsvCell(value: string | number | boolean | null | undefined): string {
|
||||
const text = value === null || value === undefined ? "" : String(value)
|
||||
return /[",\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text
|
||||
}
|
||||
|
||||
/** Builds a CSV string from rows + column definitions — header row first. */
|
||||
export function toCsv<T>(rows: T[], columns: CsvColumn<T>[]): string {
|
||||
const header = columns.map((c) => escapeCsvCell(c.header)).join(",")
|
||||
const lines = rows.map((row) => columns.map((c) => escapeCsvCell(c.value(row))).join(","))
|
||||
return [header, ...lines].join("\r\n")
|
||||
}
|
||||
|
||||
/** Triggers a browser download of `rows` as a CSV file named `filename` (".csv" appended if missing). */
|
||||
export function downloadCsv<T>(filename: string, rows: T[], columns: CsvColumn<T>[]): void {
|
||||
const csv = toCsv(rows, columns)
|
||||
const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement("a")
|
||||
link.href = url
|
||||
link.download = filename.endsWith(".csv") ? filename : `${filename}.csv`
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/** First word of the name, uppercased and stripped to alphanumerics — falls back to "CU"
|
||||
* so an empty/punctuation-only name still yields a usable base. Mirrors the vendor code
|
||||
* generator (lib/vendor-code.ts). */
|
||||
function customerCodeBase(name: string): string {
|
||||
const firstWord = name.trim().split(/\s+/)[0] ?? ""
|
||||
const cleaned = firstWord.toUpperCase().replace(/[^A-Z0-9]/g, "")
|
||||
return cleaned.slice(0, 10) || "CU"
|
||||
}
|
||||
|
||||
/** Appends a numeric suffix until the code doesn't collide with an existing one — the
|
||||
* backend enforces global uniqueness but has no generation of its own. */
|
||||
export function generateCustomerCode(name: string, existingCodes: string[]): string {
|
||||
const base = `CU-${customerCodeBase(name)}`
|
||||
if (!existingCodes.includes(base)) return base
|
||||
let suffix = 2
|
||||
while (existingCodes.includes(`${base}${suffix}`)) suffix += 1
|
||||
return `${base}${suffix}`
|
||||
}
|
||||
Reference in New Issue
Block a user