ui fixers in 10/16
This commit is contained in:
@@ -9,6 +9,7 @@ import {
|
||||
SalesInvoiceStatus,
|
||||
SalesInvoicePayment,
|
||||
SalesInvoicePostingCheck,
|
||||
SalesInvoiceListTotals,
|
||||
SalesInvoiceSummary,
|
||||
SalesListFilterParams,
|
||||
SalesReportDefinition,
|
||||
@@ -56,6 +57,11 @@ export const salesApi = {
|
||||
return apiRequest<PagedResponse<SalesInvoiceSummary>>(`/sales-invoices${buildQuery(params)}`)
|
||||
},
|
||||
|
||||
/** Count/amount totals for the same filters as `listInvoices`, across every match (not one page). */
|
||||
invoiceTotals(params: Omit<ListSalesInvoicesParams, "page" | "pageSize"> = {}): Promise<SalesInvoiceListTotals> {
|
||||
return apiRequest<SalesInvoiceListTotals>(`/sales-invoices/totals${buildQuery(params)}`)
|
||||
},
|
||||
|
||||
/** Unit price a line would get on save when left blank (flat price, else GRN/FIFO average). */
|
||||
getSuggestedPrice(itemId: number, warehouseId: number): Promise<SalesSuggestedPrice> {
|
||||
return apiRequest<SalesSuggestedPrice>(`/sales-invoices/suggested-price${buildQuery({ itemId, warehouseId })}`)
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Collapses serial / warranty numbers into consecutive runs for display:
|
||||
* SN-001, SN-002 … SN-050, SN-077 → "SN-001 – SN-050" (50) and "SN-077" (1).
|
||||
*
|
||||
* Uses the same "prefix + trailing number" reading as the GRN form's range builder, so a run
|
||||
* entered there as From/adding comes back out as exactly one range here.
|
||||
*/
|
||||
export type SerialRange = { from: string; to: string; count: number }
|
||||
|
||||
function parseTrailingNumber(s: string): { prefix: string; num: number; width: number } | null {
|
||||
const m = s.match(/^(.*?)(\d+)$/)
|
||||
if (!m) return null
|
||||
return { prefix: m[1], num: parseInt(m[2], 10), width: m[2].length }
|
||||
}
|
||||
|
||||
/** Natural order: by prefix, then by the trailing number (so SN-9 sorts before SN-10). */
|
||||
function compareSerials(a: string, b: string): number {
|
||||
const pa = parseTrailingNumber(a)
|
||||
const pb = parseTrailingNumber(b)
|
||||
if (pa && pb && pa.prefix === pb.prefix) return pa.num - pb.num
|
||||
return a.localeCompare(b, undefined, { numeric: true })
|
||||
}
|
||||
|
||||
export function toSerialRanges(values: string[]): SerialRange[] {
|
||||
const sorted = [...new Set(values.map((v) => v.trim()).filter(Boolean))].sort(compareSerials)
|
||||
const ranges: SerialRange[] = []
|
||||
let run: { start: string; prefix: string; width: number; last: number; lastText: string; count: number } | null = null
|
||||
|
||||
for (const value of sorted) {
|
||||
const parsed = parseTrailingNumber(value)
|
||||
// Consecutive = same prefix, next number, written with the run's zero-padding.
|
||||
const continues =
|
||||
run !== null &&
|
||||
parsed !== null &&
|
||||
parsed.prefix === run.prefix &&
|
||||
parsed.num === run.last + 1 &&
|
||||
value === `${run.prefix}${String(parsed.num).padStart(run.width, "0")}`
|
||||
|
||||
if (continues && run && parsed) {
|
||||
run.last = parsed.num
|
||||
run.lastText = value
|
||||
run.count += 1
|
||||
continue
|
||||
}
|
||||
if (run) ranges.push({ from: run.start, to: run.lastText, count: run.count })
|
||||
run = parsed
|
||||
? { start: value, prefix: parsed.prefix, width: parsed.width, last: parsed.num, lastText: value, count: 1 }
|
||||
: { start: value, prefix: `\u0000${value}`, width: 0, last: Number.NaN, lastText: value, count: 1 }
|
||||
}
|
||||
if (run) ranges.push({ from: run.start, to: run.lastText, count: run.count })
|
||||
return ranges
|
||||
}
|
||||
|
||||
/** One-line summary, e.g. "SN-001 – SN-050" or "SN-001 – SN-050, SN-077 +2 more". */
|
||||
export function formatSerialRanges(ranges: SerialRange[], max = 2): string {
|
||||
const label = (r: SerialRange) => (r.count === 1 ? r.from : `${r.from} – ${r.to}`)
|
||||
const shown = ranges.slice(0, max).map(label).join(", ")
|
||||
return ranges.length > max ? `${shown} +${ranges.length - max} more` : shown
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Sri Lankan phone number rule shared by master-data forms (warehouses, …). Mirrors the
|
||||
* backend's `PhoneRules.Pattern` so the client rejects exactly what the API would.
|
||||
*
|
||||
* Accepted: 10 digits starting with 0 (0771234567, 011 234 5678) or +94 followed by 9 digits
|
||||
* (+94771234567, +94 77 123 4567). Spaces and hyphens are allowed between digits.
|
||||
*/
|
||||
export const PHONE_PATTERN = /^(?:0(?:[\s-]*\d){9}|\+94(?:[\s-]*\d){9})[\s-]*$/
|
||||
|
||||
/** Digit count allowed after the prefix's own digits, per format. */
|
||||
const LOCAL_DIGITS = 10 // 0 + 9
|
||||
const INTL_DIGITS = 11 // 94 + 9
|
||||
|
||||
const PHONE_EXAMPLE = "e.g. 0771234567 or +94 77 123 4567"
|
||||
|
||||
/**
|
||||
* Input filter for a phone field's onChange: drops disallowed characters and stops accepting
|
||||
* digits once the number is full, so the limit is enforced while typing rather than on save.
|
||||
*/
|
||||
export function limitPhoneInput(raw: string): string {
|
||||
let out = ""
|
||||
let digits = 0
|
||||
for (const ch of raw) {
|
||||
if (ch === "+") {
|
||||
if (out.length === 0) out += ch // "+" only as the very first character
|
||||
continue
|
||||
}
|
||||
if (ch === " " || ch === "-") {
|
||||
out += ch
|
||||
continue
|
||||
}
|
||||
if (!/\d/.test(ch)) continue
|
||||
const max = out.startsWith("+") ? INTL_DIGITS : LOCAL_DIGITS
|
||||
if (digits >= max) continue
|
||||
out += ch
|
||||
digits += 1
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/** Error message for an invalid phone, or null when valid. Blank is valid unless `required`. */
|
||||
export function phoneError(value: string, { required = false }: { required?: boolean } = {}): string | null {
|
||||
const phone = value.trim()
|
||||
if (!phone) return required ? "Enter a phone number" : null
|
||||
if (/[^\d\s\-+]/.test(phone)) return "Phone number can only contain digits, spaces, - and a leading +"
|
||||
const digits = phone.replace(/\D/g, "")
|
||||
if (phone.startsWith("+")) {
|
||||
if (!digits.startsWith("94")) return `International numbers must start with +94 — ${PHONE_EXAMPLE}`
|
||||
if (digits.length !== INTL_DIGITS) return `+94 numbers need exactly 9 digits after +94 (${digits.length - 2} entered)`
|
||||
} else {
|
||||
if (!digits.startsWith("0")) return `Phone number must start with 0 or +94 — ${PHONE_EXAMPLE}`
|
||||
if (digits.length !== LOCAL_DIGITS) return `Phone number must be exactly 10 digits (${digits.length} entered)`
|
||||
}
|
||||
if (!PHONE_PATTERN.test(phone)) return `Enter a valid phone number, ${PHONE_EXAMPLE}`
|
||||
return null
|
||||
}
|
||||
Reference in New Issue
Block a user