Files
psp_panel/src/app/shared/directives/price-mask.directive.ts
T
ahasani 50bc9a4632 feat(purchases): implement purchase receipt functionality with form and receipt template
- Added `ProductPurchaseComponent` to handle product purchases.
- Created `PurchaseReceiptTemplateComponent` for displaying purchase receipt details.
- Developed `PurchaseFormComponent` for managing purchase form inputs and submission.
- Introduced `ProductChargeRowFormComponent` and `ProductChargeRowFormFieldsComponent` for handling product charge rows in the form.
- Implemented `PurchaseReceiptProductRowComponent` for displaying individual product rows in the receipt.
- Established API routes for purchase operations in `apiRoutes`.
- Integrated suppliers and inventories selection components.
- Added utility functions for converting prices to Persian alphabet.
- Created a utility for generating full names from name parts.
- Enhanced form validation and submission handling in the purchase form.
- Updated styles and templates for improved UI/UX.
2025-12-09 20:17:00 +03:30

193 lines
5.8 KiB
TypeScript
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.
import {
Directive,
ElementRef,
HostListener,
Input,
OnChanges,
OnInit,
Optional,
Renderer2,
Self,
SimpleChanges,
} from '@angular/core';
import { NgControl } from '@angular/forms';
@Directive({
selector: '[appPriceMask]',
standalone: true,
})
export class PriceMaskDirective implements OnInit, OnChanges {
/** Optional value to render/format when bound like [appPriceMask]="price" */
@Input('appPriceMask') priceValue: number | string | null | undefined;
@Input('appPriceMaskLocale') locale: string = 'fa-IR';
@Input('appPriceMaskFraction') fraction = 0;
/**
* When true, force comma (",") as thousands separator by using `en-US` formatting.
* If false, uses the provided `appPriceMaskLocale`.
*/
@Input('appPriceMaskUseComma') useComma = false;
/** Optional currency code to append for non-input hosts (e.g., "ریال" | "تومان") */
@Input('appPriceMaskCurrency') currency: string | null = 'ریال';
private inputEl!: HTMLInputElement | null;
constructor(
private el: ElementRef,
private renderer: Renderer2,
@Optional() @Self() private ngControl: NgControl | null,
) {}
ngOnInit(): void {
// p-inputNumber renders an input inside its host element; try to locate it
const host: HTMLElement = this.el.nativeElement;
this.inputEl =
host.tagName.toLowerCase() === 'input'
? (host as HTMLInputElement)
: host.querySelector('input');
// If a value is bound initially (e.g., span with [appPriceMask]), render it
if (this.priceValue !== undefined) {
this.renderFromBoundValue();
}
}
ngOnChanges(changes: SimpleChanges): void {
if (changes['priceValue'] && this.priceValue !== undefined) {
this.renderFromBoundValue();
}
}
private toArabicDigitsAwareNumber(str: string): string {
if (!str) return '';
// Map Arabic-Indic and Extended Arabic-Indic digits to ASCII digits
const map: Record<string, string> = {
'٠': '0',
'١': '1',
'٢': '2',
'٣': '3',
'٤': '4',
'٥': '5',
'٦': '6',
'٧': '7',
'٨': '8',
'٩': '9',
'۰': '0',
'۱': '1',
'۲': '2',
'۳': '3',
'۴': '4',
'۵': '5',
'۶': '6',
'۷': '7',
'۸': '8',
'۹': '9',
};
return str.replace(/[٠-٩۰-۹]/g, (d) => map[d] ?? d);
}
private cleanNumericString(raw: string): string {
// Convert localized digits to ASCII, then remove everything except digits, dot and minus
const ascii = this.toArabicDigitsAwareNumber(raw);
return ascii.replace(/[^0-9.\-]/g, '');
}
private normalizeToNumber(raw: number | string | null | undefined): number | null {
if (raw === null || raw === undefined) return null;
if (typeof raw === 'number') return isFinite(raw) ? raw : null;
const cleaned = this.cleanNumericString(String(raw));
if (cleaned === '') return null;
const num = Number(cleaned);
return isNaN(num) ? null : num;
}
private formatNumber(value: number): string {
try {
const fmtLocale = this.useComma ? 'en-US' : this.locale;
return new Intl.NumberFormat(fmtLocale, {
maximumFractionDigits: this.fraction,
minimumFractionDigits: 0,
useGrouping: true,
}).format(value);
} catch (e) {
return String(value);
}
}
private formatWithCurrency(num: number | null, isInputHost: boolean): string {
if (num === null) return '';
const formatted = this.formatNumber(num);
// For inputs we should NOT append currency to avoid polluting numeric entry
if (isInputHost) return formatted;
if (this.currency && this.currency.trim()) {
return `${formatted} ${this.currency.trim()}`;
}
return formatted;
}
@HostListener('input', ['$event'])
onInput(ev: Event) {
if (!this.inputEl) return;
const raw = (ev.target as HTMLInputElement).value || this.inputEl.value || '';
const cleaned = this.cleanNumericString(raw);
if (cleaned === '') {
// clear control
if (this.ngControl?.control) this.ngControl.control.setValue(null);
return;
}
const asNumber = Number(cleaned);
if (isNaN(asNumber)) return;
const formatted = this.formatNumber(asNumber);
// preserve caret position roughly: compute delta and adjust
const prevPos = this.inputEl.selectionStart ?? raw.length;
const prevLen = raw.length;
// Update FormControl value with numeric value first (suppress events)
if (this.ngControl?.control) {
try {
this.ngControl.control.setValue(asNumber, { emitEvent: false });
} catch (err) {
// fallback: ignore
}
}
// Update the displayed value (override any writeValue from control)
this.renderer.setProperty(this.inputEl, 'value', formatted);
try {
const newLen = formatted.length;
let newPos = prevPos + (newLen - prevLen);
if (newPos < 0) newPos = 0;
if (newPos > newLen) newPos = newLen;
this.inputEl.setSelectionRange(newPos, newPos);
} catch (err) {
// ignore selection errors
}
}
/** Render when bound via [appPriceMask] on non-input hosts (e.g., span) or to set initial value. */
private renderFromBoundValue() {
const num = this.normalizeToNumber(this.priceValue);
// If host has an input element, set both control value (if any) and displayed value
if (this.inputEl) {
if (this.ngControl?.control) {
try {
this.ngControl.control.setValue(num, { emitEvent: false });
} catch (err) {
// ignore
}
}
const formatted = this.formatWithCurrency(num, true);
this.renderer.setProperty(this.inputEl, 'value', formatted);
return;
}
// Otherwise, render to host text (e.g., span/div)
const formatted = this.formatWithCurrency(num, false);
this.renderer.setProperty(this.el.nativeElement, 'textContent', formatted);
}
}