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 }}