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:
Dhananjaya99
2026-09-22 00:32:40 +05:30
parent 72819e5f7b
commit fdd104284e
44 changed files with 2186 additions and 381 deletions
+23 -3
View File
@@ -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 } })
},
}
+17
View File
@@ -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}`)
},
+31
View File
@@ -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)
}
+18
View File
@@ -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}`
}