Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
107 lines
3.3 KiB
TypeScript
107 lines
3.3 KiB
TypeScript
import { coerceToString } from '@/helpers'
|
||
const ONES = ['', 'یک', 'دو', 'سه', 'چهار', 'پنج', 'شش', 'هفت', 'هشت', 'نه']
|
||
const TEENS = ['ده', 'یازده', 'دوازده', 'سیزده', 'چهارده', 'پانزده', 'شانزده', 'هفده', 'هجده', 'نوزده']
|
||
const TENS = ['', '', 'بیست', 'سی', 'چهل', 'پنجاه', 'شصت', 'هفتاد', 'هشتاد', 'نود']
|
||
const HUNDREDS = ['', 'صد', 'دویست', 'سیصد', 'چهارصد', 'پانصد', 'ششصد', 'هفتصد', 'هشتصد', 'نهصد']
|
||
const SCALES = ['', 'هزار', 'میلیون', 'میلیارد', 'تریلیون']
|
||
|
||
function joinParts(parts: string[]): string {
|
||
return parts.filter(Boolean).join(' و ')
|
||
}
|
||
|
||
/** تبدیل عدد صحیح ۰–۹۹۹ به حروف فارسی */
|
||
function underThousandToWords(n: number): string {
|
||
if (n === 0) return ''
|
||
|
||
const hundred = Math.floor(n / 100)
|
||
const rest = n % 100
|
||
const parts: string[] = []
|
||
|
||
if (hundred > 0) parts.push(HUNDREDS[hundred])
|
||
|
||
if (rest > 0) {
|
||
if (rest < 10) parts.push(ONES[rest])
|
||
else if (rest < 20) parts.push(TEENS[rest - 10])
|
||
else {
|
||
const ten = Math.floor(rest / 10)
|
||
const one = rest % 10
|
||
|
||
parts.push(one > 0 ? `${TENS[ten]} و ${ONES[one]}` : TENS[ten])
|
||
}
|
||
}
|
||
|
||
return joinParts(parts)
|
||
}
|
||
|
||
/**
|
||
* تبدیل عدد صحیح غیرمنفی به حروف فارسی.
|
||
* اعداد خارج از محدودهٔ امن یا غیرصحیح → رشتهٔ خالی.
|
||
*/
|
||
export function numberToPersianWords(value: number): string {
|
||
if (!Number.isFinite(value) || value < 0 || !Number.isInteger(value)) return ''
|
||
if (value === 0) return 'صفر'
|
||
|
||
const groups: number[] = []
|
||
let remaining = value
|
||
|
||
while (remaining > 0) {
|
||
groups.push(remaining % 1000)
|
||
remaining = Math.floor(remaining / 1000)
|
||
}
|
||
|
||
if (groups.length > SCALES.length) return ''
|
||
|
||
const parts: string[] = []
|
||
|
||
for (let i = groups.length - 1; i >= 0; i -= 1) {
|
||
const group = groups[i]
|
||
|
||
if (group === 0) continue
|
||
|
||
const words = underThousandToWords(group)
|
||
const scale = SCALES[i]
|
||
|
||
parts.push(scale ? `${words} ${scale}` : words)
|
||
}
|
||
|
||
return joinParts(parts)
|
||
}
|
||
|
||
/** پارس مقدار اینپوت مبلغ (عدد یا رشتهٔ رقم) به عدد صحیح غیرمنفی */
|
||
export function parseAmountInputValue(value: unknown): number | null {
|
||
if (value == null || value === '') return null
|
||
|
||
if (typeof value === 'number') {
|
||
if (!Number.isFinite(value) || value < 0) return null
|
||
|
||
return Math.trunc(value)
|
||
}
|
||
|
||
const digits = coerceToString(value)
|
||
.replace(/[۰-۹]/g, (digit) => String(digit.charCodeAt(0) - '۰'.charCodeAt(0)))
|
||
.replace(/[٠-٩]/g, (digit) => String(digit.charCodeAt(0) - '٠'.charCodeAt(0)))
|
||
.replace(/[^\d]/g, '')
|
||
|
||
if (!digits) return null
|
||
|
||
const amount = Number(digits)
|
||
|
||
if (!Number.isFinite(amount) || amount < 0) return null
|
||
|
||
return Math.trunc(amount)
|
||
}
|
||
|
||
/**
|
||
* نمایش مبلغ به حروف با واحد تومان.
|
||
* برای ۰ یا مقدار نامعتبر رشتهٔ خالی برمیگرداند (زیر اینپوت چیزی نشان نده).
|
||
*/
|
||
export function formatTomanInWords(value: unknown): string {
|
||
const amount = parseAmountInputValue(value)
|
||
|
||
if (amount == null || amount <= 0) return ''
|
||
|
||
const words = numberToPersianWords(amount)
|
||
|
||
return words ? `${words} تومان` : ''
|
||
}
|