Add thousand separators to all CRM price and amount inputs.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-05 02:13:39 +03:30
co-authored by Cursor
parent 36e47e8af3
commit 20a9ac0d34
9 changed files with 239 additions and 25 deletions
+137
View File
@@ -0,0 +1,137 @@
<script setup lang="ts">
import { nextTick, ref, watch } from 'vue';
import {
caretIndexForDigitCount,
countDigitChars,
formatGroupedInput,
parseMoneyNumber,
} from '@/lib/money';
const model = defineModel<string | number | null>({ default: '' });
const props = withDefaults(
defineProps<{
id?: string;
placeholder?: string;
required?: boolean;
disabled?: boolean;
readonly?: boolean;
/** حداکثر رقم اعشار؛ پیش‌فرض ۲ */
decimals?: number;
/** اگر true باشد مقدار عددی emit می‌شود (نه رشته) */
asNumber?: boolean;
dir?: 'ltr' | 'rtl';
}>(),
{
required: false,
disabled: false,
readonly: false,
decimals: 2,
asNumber: false,
dir: 'ltr',
},
);
const text = ref('');
const focused = ref(false);
const inputRef = ref<HTMLInputElement | null>(null);
function readModelNumber(): number | null {
if (model.value === '' || model.value == null) return null;
const n = typeof model.value === 'number' ? model.value : Number(model.value);
return Number.isFinite(n) ? n : null;
}
function writeModel(n: number | null) {
if (props.asNumber || typeof model.value === 'number') {
model.value = n == null ? (props.asNumber ? 0 : null) : n;
return;
}
model.value = n == null ? '' : String(n);
}
function syncDisplayFromModel() {
if (focused.value) return;
const n = readModelNumber();
text.value = n == null ? '' : formatGroupedInput(n, props.decimals);
}
watch(model, syncDisplayFromModel, { immediate: true });
watch(
() => props.decimals,
() => syncDisplayFromModel(),
);
function onFocus() {
focused.value = true;
}
function onBlur() {
focused.value = false;
const n = parseMoneyNumber(text.value, props.decimals);
writeModel(n);
text.value = n == null ? '' : formatGroupedInput(n, props.decimals);
}
async function onInput(event: Event) {
const el = event.target as HTMLInputElement;
const raw = el.value;
const caret = el.selectionStart ?? raw.length;
const digitsBefore = countDigitChars(raw.slice(0, caret));
const endsWithSep = /[.\u066B]$/.test(normalizeSep(raw));
const n = parseMoneyNumber(raw, props.decimals);
if (n == null) {
// اجازهٔ تایپ ناقص مثل "-" یا خالی
const cleaned = raw.replace(/[^\d۰-۹٠-٩.\u066B٬,،\s-]/g, '');
text.value = cleaned;
writeModel(null);
return;
}
let display = formatGroupedInput(n, props.decimals);
if (endsWithSep && props.decimals > 0 && Number.isInteger(n)) {
display += '٫';
}
text.value = display;
writeModel(n);
await nextTick();
const input = inputRef.value;
if (!input) return;
const nextCaret = caretIndexForDigitCount(display, digitsBefore);
input.setSelectionRange(nextCaret, nextCaret);
}
function normalizeSep(raw: string): string {
return raw.replace(/٫/g, '.');
}
</script>
<template>
<input
:id="id"
ref="inputRef"
class="money-input"
type="text"
inputmode="decimal"
autocomplete="off"
:dir="dir"
:placeholder="placeholder"
:required="required"
:disabled="disabled"
:readonly="readonly"
:value="text"
@focus="onFocus"
@blur="onBlur"
@input="onInput"
/>
</template>
<style scoped>
.money-input {
font-variant-numeric: tabular-nums;
}
</style>
@@ -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 () => {
</label>
</div>
<div class="triple">
<label>مظنه<input v-model.number="sanad.mazaneh" type="number" step="any" required /></label>
<label>مبلغ کل<input v-model.number="sanad.mablaghKol" type="number" step="any" required /></label>
<label>
مظنه
<MoneyInput v-model="sanad.mazaneh" as-number :decimals="2" required />
</label>
<label>
مبلغ کل
<MoneyInput v-model="sanad.mablaghKol" as-number :decimals="0" required />
</label>
<label v-if="sanad.docType === 'buy_sale_gold'">
آبشده؟
<select v-model.number="sanad.isAbshode">
@@ -602,7 +609,10 @@ onMounted(async () => {
<option :value="1">بدهی</option>
</select>
</label>
<label>مبلغ<input v-model.number="sanad.mablagh" type="number" step="any" required /></label>
<label>
مبلغ
<MoneyInput v-model="sanad.mablagh" as-number :decimals="0" required />
</label>
<label>وزن (اختیاری)<input v-model.number="sanad.vazn" type="number" step="any" /></label>
<label>بابت<input v-model="sanad.baabat" /></label>
</template>
@@ -631,7 +641,10 @@ onMounted(async () => {
<option :value="0">پرداخت به مشتری</option>
</select>
</label>
<label>مبلغ<input v-model.number="sanad.mablagh" type="number" step="any" required /></label>
<label>
مبلغ
<MoneyInput v-model="sanad.mablagh" as-number :decimals="0" required />
</label>
<label>نام بانک<input v-model="sanad.nameBank" required /></label>
<label>کد رهگیری<input v-model="sanad.codeRahgiri" /></label>
</template>