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.
This commit is contained in:
2025-12-09 20:17:00 +03:30
parent abf53bac03
commit 50bc9a4632
77 changed files with 1807 additions and 18834 deletions
@@ -3,10 +3,12 @@ import {
ElementRef,
HostListener,
Input,
OnChanges,
OnInit,
Optional,
Renderer2,
Self,
SimpleChanges,
} from '@angular/core';
import { NgControl } from '@angular/forms';
@@ -14,7 +16,9 @@ import { NgControl } from '@angular/forms';
selector: '[appPriceMask]',
standalone: true,
})
export class PriceMaskDirective implements OnInit {
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;
/**
@@ -22,6 +26,8 @@ export class PriceMaskDirective implements OnInit {
* 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;
@@ -38,6 +44,17 @@ export class PriceMaskDirective implements OnInit {
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 {
@@ -74,6 +91,15 @@ export class PriceMaskDirective implements OnInit {
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;
@@ -87,6 +113,17 @@ export class PriceMaskDirective implements OnInit {
}
}
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;
@@ -128,4 +165,28 @@ export class PriceMaskDirective implements OnInit {
// 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);
}
}