From 20a9ac0d34bf714d1c75aca86e31eba94b101739 Mon Sep 17 00:00:00 2001 From: Mahdi Date: Sat, 5 Sep 2026 02:13:39 +0330 Subject: [PATCH] Add thousand separators to all CRM price and amount inputs. Co-authored-by: Cursor --- apps/web/src/components/MoneyInput.vue | 137 ++++++++++++++++++ .../person-assets/PersonAssetsPanel.vue | 21 ++- apps/web/src/lib/money.ts | 65 +++++++++ apps/web/src/views/CurrencySettingsView.vue | 7 +- apps/web/src/views/DealFormView.vue | 9 +- apps/web/src/views/InvoiceFormView.vue | 16 +- apps/web/src/views/ProductsSettingsView.vue | 3 +- apps/web/src/views/QuoteFormView.vue | 3 +- apps/web/src/views/SalesForecastView.vue | 3 +- 9 files changed, 239 insertions(+), 25 deletions(-) create mode 100644 apps/web/src/components/MoneyInput.vue diff --git a/apps/web/src/components/MoneyInput.vue b/apps/web/src/components/MoneyInput.vue new file mode 100644 index 0000000..0885c91 --- /dev/null +++ b/apps/web/src/components/MoneyInput.vue @@ -0,0 +1,137 @@ + + + + + diff --git a/apps/web/src/components/person-assets/PersonAssetsPanel.vue b/apps/web/src/components/person-assets/PersonAssetsPanel.vue index 3f10e9c..8efc08c 100644 --- a/apps/web/src/components/person-assets/PersonAssetsPanel.vue +++ b/apps/web/src/components/person-assets/PersonAssetsPanel.vue @@ -3,6 +3,7 @@ import { computed, onMounted, ref, watch } from 'vue'; import axios from 'axios'; import { api } from '@/lib/api'; import { formatMoney } from '@/lib/money'; +import MoneyInput from '@/components/MoneyInput.vue'; import type { PersonAccountingLinkDto, PersonAssetsDto, @@ -578,8 +579,14 @@ onMounted(async () => {
- - + + - + @@ -631,7 +641,10 @@ onMounted(async () => { - + diff --git a/apps/web/src/lib/money.ts b/apps/web/src/lib/money.ts index 6071d8b..ea3a845 100644 --- a/apps/web/src/lib/money.ts +++ b/apps/web/src/lib/money.ts @@ -39,3 +39,68 @@ export function formatCompactMoney(amount: number, currency = 'IRR'): string { } return formatMoney(amount, currency); } + +const PERSIAN_DIGITS = '۰۱۲۳۴۵۶۷۸۹'; +const ARABIC_DIGITS = '٠١٢٣٤٥٦٧٨٩'; + +/** تبدیل ارقام فارسی/عربی به لاتین */ +export function normalizeDigits(raw: string): string { + return raw.replace(/[۰-۹٠-٩]/g, (ch) => { + const p = PERSIAN_DIGITS.indexOf(ch); + if (p >= 0) return String(p); + return String(ARABIC_DIGITS.indexOf(ch)); + }); +} + +/** نمایش عدد با جداکنندهٔ هزارگان (fa-IR) برای فیلدهای ورودی */ +export function formatGroupedInput(value: number, maxFractionDigits = 2): string { + if (!Number.isFinite(value)) return ''; + return new Intl.NumberFormat('fa-IR', { + maximumFractionDigits: maxFractionDigits, + minimumFractionDigits: 0, + }).format(value); +} + +/** + * پارس متن ورودی مبلغ/قیمت. + * جداکننده‌ها و ارقام فارسی را می‌پذیرد و مقدار عددی برمی‌گرداند. + */ +export function parseMoneyNumber(raw: string, maxFractionDigits = 2): number | null { + const cleaned = normalizeDigits(raw) + .replace(/[٬,،\s]/g, '') + .replace(/٫/g, '.') + .replace(/[^\d.-]/g, ''); + + if (!cleaned || cleaned === '-' || cleaned === '.' || cleaned === '-.') return null; + + const n = Number(cleaned); + if (!Number.isFinite(n)) return null; + + if (maxFractionDigits <= 0) return Math.round(n); + const factor = 10 ** maxFractionDigits; + return Math.round(n * factor) / factor; +} + +/** رشتهٔ خام بدون جداکننده برای v-modelهای متنی فرم‌ها */ +export function parseMoneyToRawString(raw: string, maxFractionDigits = 2): string { + const n = parseMoneyNumber(raw, maxFractionDigits); + return n == null ? '' : String(n); +} + +export function countDigitChars(value: string): number { + return normalizeDigits(value).replace(/\D/g, '').length; +} + +/** موقعیت کرسر را بر اساس تعداد ارقام قبل از کرسر حفظ می‌کند */ +export function caretIndexForDigitCount(formatted: string, digitCount: number): number { + if (digitCount <= 0) return 0; + let seen = 0; + const normalized = normalizeDigits(formatted); + for (let i = 0; i < normalized.length; i++) { + if (/\d/.test(normalized[i]!)) { + seen += 1; + if (seen >= digitCount) return i + 1; + } + } + return formatted.length; +} diff --git a/apps/web/src/views/CurrencySettingsView.vue b/apps/web/src/views/CurrencySettingsView.vue index 7b8dcb9..f1fdce2 100644 --- a/apps/web/src/views/CurrencySettingsView.vue +++ b/apps/web/src/views/CurrencySettingsView.vue @@ -3,6 +3,7 @@ import { computed, onMounted, ref } from 'vue'; import axios from 'axios'; import { api } from '@/lib/api'; import { CURRENCY_OPTIONS, formatMoney } from '@/lib/money'; +import MoneyInput from '@/components/MoneyInput.vue'; import type { OrganizationDto, UpdateOrgCurrenciesRequest } from '@mento/shared'; const loading = ref(false); @@ -152,11 +153,9 @@ onMounted(load);

نرخ به {{ baseCurrency }}

۱ {{ code }} = - {{ baseCurrency }} diff --git a/apps/web/src/views/DealFormView.vue b/apps/web/src/views/DealFormView.vue index e55fe7a..c952750 100644 --- a/apps/web/src/views/DealFormView.vue +++ b/apps/web/src/views/DealFormView.vue @@ -9,6 +9,7 @@ import { currencySuffix, formatMoney } from '@/lib/money'; import CustomFieldsSection from '@/components/CustomFieldsSection.vue'; import EntityTimeline from '@/components/EntityTimeline.vue'; import JalaliDateInput from '@/components/JalaliDateInput.vue'; +import MoneyInput from '@/components/MoneyInput.vue'; import UiIcon from '@/components/UiIcon.vue'; import UserAvatar from '@/components/UserAvatar.vue'; import type { @@ -531,12 +532,10 @@ onMounted(load);
- @@ -708,7 +707,7 @@ onMounted(load); - +

diff --git a/apps/web/src/views/InvoiceFormView.vue b/apps/web/src/views/InvoiceFormView.vue index 1f89d65..4061710 100644 --- a/apps/web/src/views/InvoiceFormView.vue +++ b/apps/web/src/views/InvoiceFormView.vue @@ -6,6 +6,7 @@ import { api } from '@/lib/api'; import { formatMoney as formatMoneyLib } from '@/lib/money'; import { formatLocalDateInput } from '@/lib/jalali'; import JalaliDateInput from '@/components/JalaliDateInput.vue'; +import MoneyInput from '@/components/MoneyInput.vue'; import type { CompanyDto, CreateInvoiceRequest, @@ -476,11 +477,9 @@ onMounted(load); step="0.01" placeholder="تعداد" /> -

جمع: {{ formatMoney(totalAmount) }}

diff --git a/apps/web/src/views/SalesForecastView.vue b/apps/web/src/views/SalesForecastView.vue index 705897e..229e508 100644 --- a/apps/web/src/views/SalesForecastView.vue +++ b/apps/web/src/views/SalesForecastView.vue @@ -8,6 +8,7 @@ import { canReassignRecords, canViewAllRecords } from '@/lib/roles'; import { formatMoney as formatMoneyLib } from '@/lib/money'; import { formatLocalDateInput } from '@/lib/jalali'; import JalaliDateInput from '@/components/JalaliDateInput.vue'; +import MoneyInput from '@/components/MoneyInput.vue'; import type { CreateSalesGoalRequest, OrgUserDto, @@ -209,7 +210,7 @@ onMounted(() => {
- +