Files
CRM_Montzar/apps/web/src/components/MoneyInput.vue
T
MahdiandCursor 5ef693990c feat(finance): edit payroll with month ranges, reasons, and toman
Let salary and payments be corrected after they are saved, pick a Jalali period as a month range, and show toman on finance amounts.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-08 16:53:45 +03:30

141 lines
3.5 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { nextTick, ref, watch } from 'vue';
import { Input as AInput } from 'ant-design-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';
addonAfter?: string;
}>(),
{
required: false,
disabled: false,
readonly: false,
decimals: 2,
asNumber: false,
dir: 'ltr',
addonAfter: '',
},
);
const text = ref('');
const focused = ref(false);
const inputRef = ref<{ input?: HTMLInputElement } | HTMLInputElement | null>(null);
function nativeInput(): HTMLInputElement | null {
const el = inputRef.value;
if (!el) return null;
if (el instanceof HTMLInputElement) return el;
return el.input ?? 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 = nativeInput();
if (!input) return;
const nextCaret = caretIndexForDigitCount(display, digitsBefore);
input.setSelectionRange(nextCaret, nextCaret);
}
function normalizeSep(raw: string): string {
return raw.replace(/٫/g, '.');
}
</script>
<template>
<a-input
:id="id"
ref="inputRef"
type="text"
inputmode="decimal"
autocomplete="off"
:dir="dir"
:placeholder="placeholder"
:disabled="disabled"
:readonly="readonly"
:value="text"
:addon-after="addonAfter || undefined"
style="width: 100%; font-variant-numeric: tabular-nums"
@focus="onFocus"
@blur="onBlur"
@input="onInput"
/>
</template>