feat: Implement price info card component and product categories with grid view
- Added `price-info-card.component` to display total amount, tax, and payable amount. - Integrated price masking directive for formatted price display. - Created `categories.component` to list product categories with loading skeletons. - Implemented `grid-view.component` for displaying products in a grid layout. - Enhanced `products.component` to manage category selection and product display. - Introduced new models for product categories and stock responses. - Updated `main.service.ts` to fetch product categories and stock data. - Refactored `POSStore` to manage state for products, categories, and order items. - Added utility functions for price formatting and number normalization. - Included placeholder images for product and logo.
This commit is contained in:
@@ -11,6 +11,12 @@ import {
|
||||
SimpleChanges,
|
||||
} from '@angular/core';
|
||||
import { NgControl } from '@angular/forms';
|
||||
import {
|
||||
cleanNumericString,
|
||||
formatNumber,
|
||||
formatWithCurrency,
|
||||
normalizeToNumber,
|
||||
} from '../../utils/price-mask.utils';
|
||||
|
||||
@Directive({
|
||||
selector: '[appPriceMask]',
|
||||
@@ -57,78 +63,13 @@ export class PriceMaskDirective implements OnInit, OnChanges {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
// Numeric parsing/formatting helpers moved to utils/price-mask.utils.ts
|
||||
|
||||
@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);
|
||||
const cleaned = cleanNumericString(raw);
|
||||
if (cleaned === '') {
|
||||
// clear control
|
||||
if (this.ngControl?.control) this.ngControl.control.setValue(null);
|
||||
@@ -137,7 +78,11 @@ export class PriceMaskDirective implements OnInit, OnChanges {
|
||||
const asNumber = Number(cleaned);
|
||||
if (isNaN(asNumber)) return;
|
||||
|
||||
const formatted = this.formatNumber(asNumber);
|
||||
const formatted = formatNumber(asNumber, {
|
||||
locale: this.locale,
|
||||
useComma: this.useComma,
|
||||
fraction: this.fraction,
|
||||
});
|
||||
|
||||
// preserve caret position roughly: compute delta and adjust
|
||||
const prevPos = this.inputEl.selectionStart ?? raw.length;
|
||||
@@ -168,7 +113,7 @@ export class PriceMaskDirective implements OnInit, OnChanges {
|
||||
|
||||
/** 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);
|
||||
const num = normalizeToNumber(this.priceValue);
|
||||
|
||||
// If host has an input element, set both control value (if any) and displayed value
|
||||
if (this.inputEl) {
|
||||
@@ -180,13 +125,21 @@ export class PriceMaskDirective implements OnInit, OnChanges {
|
||||
}
|
||||
}
|
||||
|
||||
const formatted = this.formatWithCurrency(num, true);
|
||||
const formatted = formatWithCurrency(num, true, this.currency, {
|
||||
locale: this.locale,
|
||||
useComma: this.useComma,
|
||||
fraction: this.fraction,
|
||||
});
|
||||
this.renderer.setProperty(this.inputEl, 'value', formatted);
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, render to host text (e.g., span/div)
|
||||
const formatted = this.formatWithCurrency(num, false);
|
||||
const formatted = formatWithCurrency(num, false, this.currency, {
|
||||
locale: this.locale,
|
||||
useComma: this.useComma,
|
||||
fraction: this.fraction,
|
||||
});
|
||||
this.renderer.setProperty(this.el.nativeElement, 'textContent', formatted);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user