Sms & Email

This commit is contained in:
2026-10-20 10:45:03 +05:30
parent a40e463e8b
commit 0e423b806c
29 changed files with 13845 additions and 26 deletions
@@ -0,0 +1,37 @@
// Day-end owner report settings: recipients, schedule, preview, test send and send log
// (docs/DAY_END_OWNER_REPORT_PLAN.md §3.5). Singleton settings — no id; saves need the ETag.
import { apiRequest, apiRequestWithETag, buildQuery } from "@/lib/api-client"
import { ApiResult, PagedResponse } from "@/types/common"
import {
OwnerReportConfig,
OwnerReportDispatch,
OwnerReportPreview,
OwnerReportSendResult,
UpdateOwnerReportConfigRequest,
} from "@/types/owner-report"
const BASE = "/settings/owner-report"
export const ownerReportApi = {
get(): Promise<ApiResult<OwnerReportConfig>> {
return apiRequestWithETag<OwnerReportConfig>(BASE)
},
update(request: UpdateOwnerReportConfigRequest, ifMatch: string): Promise<ApiResult<OwnerReportConfig>> {
return apiRequestWithETag<OwnerReportConfig>(BASE, { method: "PUT", body: request, ifMatch })
},
/** The report as it would be sent for `date` (yyyy-MM-dd; default today) — nothing is sent. */
preview(date?: string): Promise<OwnerReportPreview> {
return apiRequest<OwnerReportPreview>(`${BASE}/preview${buildQuery({ date })}`)
},
/** Sends the report now to every active recipient (logged as a test). One per minute. */
sendTest(date?: string): Promise<OwnerReportSendResult[]> {
return apiRequest<OwnerReportSendResult[]>(`${BASE}/test`, { method: "POST", body: { date: date || null } })
},
dispatches(params: { page?: number; pageSize?: number } = {}): Promise<PagedResponse<OwnerReportDispatch>> {
return apiRequest<PagedResponse<OwnerReportDispatch>>(`${BASE}/dispatches${buildQuery(params)}`)
},
}
+15 -13
View File
@@ -1,17 +1,18 @@
/**
* 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.
* Sri Lankan phone number rule shared by master-data forms (warehouses, owner report, …). 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.
* Accepted: 10 digits starting with 0 (0771234567, 011 234 5678), +94 followed by 9 digits
* (+94771234567, +94 77 123 4567), or 94 followed by 9 digits without the "+" (94771234567 — the
* SMS gateway's own format). Spaces and hyphens are allowed between digits.
*/
export const PHONE_PATTERN = /^(?:0(?:[\s-]*\d){9}|\+94(?:[\s-]*\d){9})[\s-]*$/
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"
const PHONE_EXAMPLE = "e.g. 0771234567, 94771234567 or +94 77 123 4567"
/**
* Input filter for a phone field's onChange: drops disallowed characters and stops accepting
@@ -19,7 +20,7 @@ const PHONE_EXAMPLE = "e.g. 0771234567 or +94 77 123 4567"
*/
export function limitPhoneInput(raw: string): string {
let out = ""
let digits = 0
let digits = ""
for (const ch of raw) {
if (ch === "+") {
if (out.length === 0) out += ch // "+" only as the very first character
@@ -30,10 +31,11 @@ export function limitPhoneInput(raw: string): string {
continue
}
if (!/\d/.test(ch)) continue
const max = out.startsWith("+") ? INTL_DIGITS : LOCAL_DIGITS
if (digits >= max) continue
// International (+94… or 94…) numbers carry two more digits than a local 0… number.
const max = out.startsWith("+") || digits.startsWith("94") || (digits === "9" && ch === "4") ? INTL_DIGITS : LOCAL_DIGITS
if (digits.length >= max) continue
out += ch
digits += 1
digits += ch
}
return out
}
@@ -44,11 +46,11 @@ export function phoneError(value: string, { required = false }: { required?: boo
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 (phone.startsWith("+") || digits.startsWith("94")) {
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)`
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.startsWith("0")) return `Phone number must start with 0, 94 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}`