From 17fa65407c296f04aeeeb7b357331caeb6c1571c Mon Sep 17 00:00:00 2001 From: ahasani194 Date: Sun, 14 Dec 2025 20:34:15 +0330 Subject: [PATCH] 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. --- .../movementList/movement-list.component.ts | 1 - .../transfer/transfer-form.component.ts | 3 - .../order/order-card.component.html | 83 +++++++ .../components/order/order-card.component.ts | 53 +++++ .../order/price-info-card.component.html | 17 ++ .../order/price-info-card.component.ts | 15 ++ .../products/categories.component.html | 29 +++ .../products/categories.component.ts | 63 +++++ .../products/grid-view.component.html | 25 ++ .../products/grid-view.component.ts | 25 ++ .../products/products.component.html | 12 + .../components/products/products.component.ts | 26 +++ .../modules/pos/constants/apiRoutes/index.ts | 11 +- src/app/modules/pos/constants/index.ts | 2 +- src/app/modules/pos/models/index.ts | 1 + src/app/modules/pos/models/io.ts | 30 +++ src/app/modules/pos/models/types.ts | 23 ++ src/app/modules/pos/services/main.service.ts | 34 +++ src/app/modules/pos/store/index.ts | 218 ++++++++++++++++++ src/app/modules/pos/views/pos.component.html | 27 ++- src/app/modules/pos/views/pos.component.ts | 35 ++- .../productBrands/views/list.component.ts | 2 - .../fullForm/full-form.component.html | 2 +- .../fullForm/full-form.component.ts | 2 +- src/app/modules/products/models/io.d.ts | 2 +- .../modules/products/views/list.component.ts | 7 +- .../products/views/single.component.ts | 12 +- .../purchases/components/form.component.ts | 3 - .../purchase-receipt-template.component.ts | 1 - src/app/modules/users/views/list.component.ts | 2 - .../details-info-card.component.html | 4 +- .../pageDataList/page-data-list.component.ts | 4 +- .../shared/directives/price-mask.directive.ts | 95 ++------ src/app/uikit/uikit-counter.component.html | 41 ++-- src/app/uikit/uikit-counter.component.ts | 20 +- src/app/utils/index.ts | 1 + src/app/utils/jalali-date.utils.ts | 2 +- src/app/utils/price-mask.utils.ts | 77 +++++++ src/assets/images/index.ts | 2 + src/assets/images/placeholders/index.ts | 7 + .../images/placeholders/logoPlaceholder.png | Bin 0 -> 5579 bytes .../placeholders/productPlaceholder.png | Bin 0 -> 1444 bytes src/assets/tailwind.css | 7 + 43 files changed, 894 insertions(+), 132 deletions(-) create mode 100644 src/app/modules/pos/components/order/order-card.component.html create mode 100644 src/app/modules/pos/components/order/order-card.component.ts create mode 100644 src/app/modules/pos/components/order/price-info-card.component.html create mode 100644 src/app/modules/pos/components/order/price-info-card.component.ts create mode 100644 src/app/modules/pos/components/products/categories.component.html create mode 100644 src/app/modules/pos/components/products/categories.component.ts create mode 100644 src/app/modules/pos/components/products/grid-view.component.html create mode 100644 src/app/modules/pos/components/products/grid-view.component.ts create mode 100644 src/app/modules/pos/components/products/products.component.html create mode 100644 src/app/modules/pos/components/products/products.component.ts create mode 100644 src/app/modules/pos/models/index.ts create mode 100644 src/app/modules/pos/models/io.ts create mode 100644 src/app/modules/pos/models/types.ts create mode 100644 src/app/modules/pos/services/main.service.ts create mode 100644 src/app/modules/pos/store/index.ts create mode 100644 src/app/utils/price-mask.utils.ts create mode 100644 src/assets/images/placeholders/index.ts create mode 100644 src/assets/images/placeholders/logoPlaceholder.png create mode 100644 src/assets/images/placeholders/productPlaceholder.png diff --git a/src/app/modules/inventories/components/movementList/movement-list.component.ts b/src/app/modules/inventories/components/movementList/movement-list.component.ts index 230c96b..66dbcce 100644 --- a/src/app/modules/inventories/components/movementList/movement-list.component.ts +++ b/src/app/modules/inventories/components/movementList/movement-list.component.ts @@ -54,7 +54,6 @@ export class InventoryMovementListComponent { getAll() { this.loading.set(true); - console.log(this.inventoryId); return this.service.getMovements(this.inventoryId, this.movementType).subscribe({ next: (res) => { diff --git a/src/app/modules/inventories/components/transfer/transfer-form.component.ts b/src/app/modules/inventories/components/transfer/transfer-form.component.ts index 52c1b77..bfaa1d7 100644 --- a/src/app/modules/inventories/components/transfer/transfer-form.component.ts +++ b/src/app/modules/inventories/components/transfer/transfer-form.component.ts @@ -92,13 +92,10 @@ export class InventoriesTransferFormComponent { count: [1, [Validators.required, Validators.min(1)]], }), ); - - console.log(this.form.controls.items); } } clearSelectedProductsInForm($e: TableRowUnSelectEvent) { - console.log($e.index); if ($e.index != null) { this.form.controls.items.removeAt($e.index); } diff --git a/src/app/modules/pos/components/order/order-card.component.html b/src/app/modules/pos/components/order/order-card.component.html new file mode 100644 index 0000000..d57ccd9 --- /dev/null +++ b/src/app/modules/pos/components/order/order-card.component.html @@ -0,0 +1,83 @@ +
+
+ +
+
+
+
+
+ + سفارش‌ها +
+ +
+ @if (!inOrderProducts().length) { +
+ + هیچ سفارشی ثبت نشده است. +
+ } @else { +
+ @for (inOrderProduct of inOrderProducts(); track inOrderProduct.productId) { +
+
+ +
+
+
+ {{ inOrderProduct.product.name }} + {{ inOrderProduct.product.sku }} +
+ +
+
+ + + + + + + + +
+ +
+
+
+
+
+ } +
+ } +
+
+ +
+ + +
+
+
diff --git a/src/app/modules/pos/components/order/order-card.component.ts b/src/app/modules/pos/components/order/order-card.component.ts new file mode 100644 index 0000000..80657fc --- /dev/null +++ b/src/app/modules/pos/components/order/order-card.component.ts @@ -0,0 +1,53 @@ +import { CustomersSelectComponent } from '@/modules/customers/components/select/select.component'; +import { PriceMaskDirective } from '@/shared/directives'; +import { UikitCounterComponent } from '@/uikit'; +import { Component, computed, signal } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { ButtonDirective } from 'primeng/button'; +import { InplaceModule } from 'primeng/inplace'; +import { InputNumber } from 'primeng/inputnumber'; +import images from 'src/assets/images'; +import { POSStore } from '../../store'; +import { POSOrderPriceInfoCardComponent } from './price-info-card.component'; + +@Component({ + selector: 'pos-order-card', + templateUrl: './order-card.component.html', + imports: [ + CustomersSelectComponent, + ButtonDirective, + PriceMaskDirective, + UikitCounterComponent, + POSOrderPriceInfoCardComponent, + FormsModule, + InplaceModule, + InputNumber, + ], +}) +export class PosOrderCardComponent { + constructor(private store: POSStore) {} + + placeholderImage = images.placeholders.default; + + inOrderProducts = computed(() => this.store.inOrderProducts()); + + value = signal(0); + + removeProductFromOrder(productId: number) { + this.store.removeFromInOrderProducts(productId); + } + + updateInOrderProductQuantity(productId: number, quantity: number) { + this.store.updateInOrderProductQuantity(productId, quantity); + } + + updateInOrderProductFee(productId: number, $event: Event, prevFee: string) { + const fee = $event.target ? String(($event.target as HTMLInputElement).ariaValueNow) : prevFee; + + this.store.updateInOrderProductFee(productId, fee); + } + + clearOrderList() { + this.store.resetInOrderProducts(); + } +} diff --git a/src/app/modules/pos/components/order/price-info-card.component.html b/src/app/modules/pos/components/order/price-info-card.component.html new file mode 100644 index 0000000..e5533f4 --- /dev/null +++ b/src/app/modules/pos/components/order/price-info-card.component.html @@ -0,0 +1,17 @@ + +
+
+ جمع قیمت + +
+
+ مالیات (۱۰٪) + +
+
+
+ مجموع کل + +
+
+
diff --git a/src/app/modules/pos/components/order/price-info-card.component.ts b/src/app/modules/pos/components/order/price-info-card.component.ts new file mode 100644 index 0000000..65ab416 --- /dev/null +++ b/src/app/modules/pos/components/order/price-info-card.component.ts @@ -0,0 +1,15 @@ +import { PriceMaskDirective } from '@/shared/directives'; +import { Component, computed } from '@angular/core'; +import { Card } from 'primeng/card'; +import { POSStore } from '../../store'; + +@Component({ + selector: 'pos-order-price-info-card', + templateUrl: './price-info-card.component.html', + imports: [Card, PriceMaskDirective], +}) +export class POSOrderPriceInfoCardComponent { + constructor(private store: POSStore) {} + + priceInfo = computed(() => this.store.orderPricingInfo()); +} diff --git a/src/app/modules/pos/components/products/categories.component.html b/src/app/modules/pos/components/products/categories.component.html new file mode 100644 index 0000000..cfd8ae0 --- /dev/null +++ b/src/app/modules/pos/components/products/categories.component.html @@ -0,0 +1,29 @@ +
+ @if (loading()) { + @for (i of [1, 2, 3, 4, 5]; track i) { + + } + } @else { + @for (category of categories(); track category.id) { +
+ {{ category.name }} +
+ + {{ category.productCount }} + +
+
+ } + } +
diff --git a/src/app/modules/pos/components/products/categories.component.ts b/src/app/modules/pos/components/products/categories.component.ts new file mode 100644 index 0000000..2dda60d --- /dev/null +++ b/src/app/modules/pos/components/products/categories.component.ts @@ -0,0 +1,63 @@ +import { Maybe } from '@/core'; +import { Component, EventEmitter, Output, signal } from '@angular/core'; +import { Skeleton } from 'primeng/skeleton'; +import { map } from 'rxjs'; +import { IPosProductCategoriesResponse } from '../../models'; +import { PosService } from '../../services/main.service'; + +@Component({ + selector: 'pos-product-categories', + templateUrl: './categories.component.html', + imports: [Skeleton], +}) +export class PosProductCategoriesComponent { + @Output() categoryChange = new EventEmitter(); + + constructor(private service: PosService) { + this.getCategories(); + } + + categories = signal>(null); + loading = signal(true); + + selectedCategory = signal>(null); + + changeSelectedCategory(category: IPosProductCategoriesResponse) { + if (category.id !== this.selectedCategory()?.id) { + this.selectedCategory.set(category); + this.categoryChange.emit(category.id); + } + } + + getCategories() { + this.loading.set(true); + this.service + .getCategories() + .pipe( + map((res) => { + return { + data: [ + { + id: 0, + name: 'همه', + productCount: res.data.reduce((acc, category) => acc + category.productCount, 0), + totalQuantity: res.data.reduce((acc, category) => acc + category.totalQuantity, 0), + }, + ...res.data, + ], + meta: res.meta, + }; + }), + ) + .subscribe({ + next: (res) => { + this.selectedCategory.set(res.data[0]); + this.categories.set(res.data); + this.loading.set(false); + }, + error: (err) => { + this.loading.set(false); + }, + }); + } +} diff --git a/src/app/modules/pos/components/products/grid-view.component.html b/src/app/modules/pos/components/products/grid-view.component.html new file mode 100644 index 0000000..b0af442 --- /dev/null +++ b/src/app/modules/pos/components/products/grid-view.component.html @@ -0,0 +1,25 @@ +
+ @if (loading()) { + @for (_ of [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15]; track $index) { +
+ +
+ } + } @else { + @for (product of products(); track product.id) { +
+ +
+ + {{ product.product.name }} + ({{ product.quantity }}) + +
+ + +
+
+
+ } + } +
diff --git a/src/app/modules/pos/components/products/grid-view.component.ts b/src/app/modules/pos/components/products/grid-view.component.ts new file mode 100644 index 0000000..6551c94 --- /dev/null +++ b/src/app/modules/pos/components/products/grid-view.component.ts @@ -0,0 +1,25 @@ +import { PriceMaskDirective } from '@/shared/directives'; +import { Component, computed } from '@angular/core'; +import { ButtonDirective } from 'primeng/button'; +import { Skeleton } from 'primeng/skeleton'; +import images from 'src/assets/images'; +import { IPosProductSummary } from '../../models/types'; +import { POSStore } from '../../store'; + +@Component({ + selector: 'pos-products-grid-view', + templateUrl: './grid-view.component.html', + imports: [PriceMaskDirective, ButtonDirective, Skeleton], +}) +export class PosProductsGridViewComponent { + constructor(private store: POSStore) {} + + products = computed(() => this.store.activatedCategoryProducts()); + loading = computed(() => this.store.getStockLoading()); + + productPlaceholder = images.placeholders.default; + + addProduct(product: IPosProductSummary) { + this.store.addToInOrderProducts(product.id); + } +} diff --git a/src/app/modules/pos/components/products/products.component.html b/src/app/modules/pos/components/products/products.component.html new file mode 100644 index 0000000..ed838e8 --- /dev/null +++ b/src/app/modules/pos/components/products/products.component.html @@ -0,0 +1,12 @@ +
+
+
+ + لیست کالاها +
+ +
+ + + +
diff --git a/src/app/modules/pos/components/products/products.component.ts b/src/app/modules/pos/components/products/products.component.ts new file mode 100644 index 0000000..cc156ff --- /dev/null +++ b/src/app/modules/pos/components/products/products.component.ts @@ -0,0 +1,26 @@ +import { Maybe } from '@/core'; +import { Component, computed, Input, signal } from '@angular/core'; +import { ButtonDirective } from 'primeng/button'; +import { POSStore } from '../../store'; +import { PosProductCategoriesComponent } from './categories.component'; +import { PosProductsGridViewComponent } from './grid-view.component'; + +@Component({ + selector: 'pos-products', + templateUrl: './products.component.html', + imports: [PosProductCategoriesComponent, ButtonDirective, PosProductsGridViewComponent], +}) +export class PosProductsComponent { + @Input() activeCategory = signal>(null); + + constructor(private store: POSStore) { + this.store.initial(); + } + + readonly activatedCategoryProducts = computed(() => this.store.activatedCategoryProducts()); + readonly stock = computed(() => this.store.stock()); + + onCategoryChange(categoryId: number) { + this.store.changeActiveCategory(categoryId); + } +} diff --git a/src/app/modules/pos/constants/apiRoutes/index.ts b/src/app/modules/pos/constants/apiRoutes/index.ts index 69bd806..1eb2500 100644 --- a/src/app/modules/pos/constants/apiRoutes/index.ts +++ b/src/app/modules/pos/constants/apiRoutes/index.ts @@ -1,6 +1,7 @@ -// const baseUrl = '/api/v1/product-categories'; +const baseUrl = '/api/v1/pos'; -// export const PRODUCT_CATEGORIES_API_ROUTES = { -// list: () => `${baseUrl}`, -// single: (categoryId: string) => `${baseUrl}/${categoryId}`, -// }; +export const POS_API_ROUTES = { + info: () => `${baseUrl}`, + stock: () => `${baseUrl}/stock`, + productCategories: () => `${baseUrl}/product-categories`, +}; diff --git a/src/app/modules/pos/constants/index.ts b/src/app/modules/pos/constants/index.ts index fdb1998..ee61bd7 100644 --- a/src/app/modules/pos/constants/index.ts +++ b/src/app/modules/pos/constants/index.ts @@ -1,2 +1,2 @@ -// export * from './apiRoutes'; +export * from './apiRoutes'; export * from './routes'; diff --git a/src/app/modules/pos/models/index.ts b/src/app/modules/pos/models/index.ts new file mode 100644 index 0000000..61a518a --- /dev/null +++ b/src/app/modules/pos/models/index.ts @@ -0,0 +1 @@ +export * from './io'; diff --git a/src/app/modules/pos/models/io.ts b/src/app/modules/pos/models/io.ts new file mode 100644 index 0000000..7d6a51a --- /dev/null +++ b/src/app/modules/pos/models/io.ts @@ -0,0 +1,30 @@ +import { IPosOrderItem, IPosProductSummary } from './types'; + +export interface IPosInfoRawResponse { + store: { + id: number; + name: string; + }; +} + +export interface IPosInfoResponse extends IPosInfoRawResponse {} + +export interface IPosStockRawResponse { + id: number; + quantity: number; + product: IPosProductSummary; +} +export interface IPosStockResponse extends IPosStockRawResponse {} + +export interface IPosProductCategoriesRawResponse { + id: number; + name: string; + totalQuantity: number; + productCount: number; +} +export interface IPosProductCategoriesResponse extends IPosProductCategoriesRawResponse {} + +export interface IPosOrderRequest { + customerId?: number; + items: IPosOrderItem[]; +} diff --git a/src/app/modules/pos/models/types.ts b/src/app/modules/pos/models/types.ts new file mode 100644 index 0000000..6c98c71 --- /dev/null +++ b/src/app/modules/pos/models/types.ts @@ -0,0 +1,23 @@ +export interface IPosProductSummary { + id: number; + name: string; + sku: string; + salePrice: string; + category: ICategorySummary; +} + +interface ICategorySummary { + id: number; + name: string; +} + +export interface IPosOrderItem { + productId: number; + quantity: number; + fee: string; +} + +export interface IPosInOrderProduct extends IPosOrderItem { + product: IPosProductSummary; + maxQuantity: number; +} diff --git a/src/app/modules/pos/services/main.service.ts b/src/app/modules/pos/services/main.service.ts new file mode 100644 index 0000000..36587a7 --- /dev/null +++ b/src/app/modules/pos/services/main.service.ts @@ -0,0 +1,34 @@ +import { IPaginatedResponse } from '@/core/models/service.model'; +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { POS_API_ROUTES } from '../constants'; +import { + IPosInfoRawResponse, + IPosInfoResponse, + IPosProductCategoriesRawResponse, + IPosProductCategoriesResponse, + IPosStockRawResponse, + IPosStockResponse, +} from '../models'; + +@Injectable({ providedIn: 'root' }) +export class PosService { + constructor(private http: HttpClient) {} + + private apiRoutes = POS_API_ROUTES; + + getInfo(): Observable { + return this.http.get(this.apiRoutes.info()); + } + + getStock(): Observable> { + return this.http.get>(this.apiRoutes.stock()); + } + + getCategories(): Observable> { + return this.http.get>( + this.apiRoutes.productCategories(), + ); + } +} diff --git a/src/app/modules/pos/store/index.ts b/src/app/modules/pos/store/index.ts new file mode 100644 index 0000000..fb750a7 --- /dev/null +++ b/src/app/modules/pos/store/index.ts @@ -0,0 +1,218 @@ +import { Maybe } from '@/core'; +import { computed, Injectable, signal } from '@angular/core'; +import { map } from 'rxjs'; +import { IPosInfoResponse, IPosProductCategoriesResponse, IPosStockResponse } from '../models'; +import { IPosInOrderProduct } from '../models/types'; +import { PosService } from '../services/main.service'; + +interface IPosState { + getStockLoading: boolean; + stock: Maybe; + getInfoLoading: boolean; + info: Maybe; + getProductCategoriesLoading: boolean; + productCategories: Maybe; + activeProductCategory: Maybe; + inOrderProducts: IPosInOrderProduct[]; +} + +export const INITIAL_POS_STATE: IPosState = { + getStockLoading: true, + stock: null, + getInfoLoading: true, + info: null, + getProductCategoriesLoading: true, + productCategories: null, + activeProductCategory: null, + inOrderProducts: [], +}; + +@Injectable({ providedIn: 'any' }) +export class POSStore { + constructor(private service: PosService) {} + + private state$ = signal({ ...INITIAL_POS_STATE }); + + readonly stock = computed(() => this.state$().stock); + readonly getStockLoading = computed(() => this.state$().getStockLoading); + readonly info = computed(() => this.state$().info); + readonly getInfoLoading = computed(() => this.state$().getInfoLoading); + readonly inOrderProducts = computed(() => this.state$().inOrderProducts); + readonly productCategories = computed(() => this.state$().productCategories); + readonly getProductCategoriesLoading = computed(() => this.state$().getProductCategoriesLoading); + readonly activeProductCategory = computed(() => this.state$().activeProductCategory); + readonly activatedCategoryProducts = computed(() => { + if (this.activeProductCategory() === 0) { + return this.state$().stock; + } + return this.state$().stock?.filter( + (s) => s.product.category.id === this.state$().activeProductCategory, + ); + }); + + readonly orderPricingInfo = computed(() => { + const totalAmount = this.inOrderProducts().reduce( + (acc, curr) => acc + parseFloat(curr.fee) * curr.quantity, + 0, + ); + const taxAmount = totalAmount * 0.1; + return { + totalItems: this.inOrderProducts().reduce((acc, curr) => acc + curr.quantity, 0), + totalBaseAmount: this.inOrderProducts().reduce( + (acc, curr) => acc + parseFloat(curr.product.salePrice) * curr.quantity, + 0, + ), + totalAmount, + taxAmount, + payableAmount: totalAmount + taxAmount, + }; + }); + + setState(partial: Partial) { + this.state$.set({ ...this.state$(), ...partial }); + } + + reset() { + this.state$.set({ ...INITIAL_POS_STATE }); + } + + initial() { + this.getInfo(); + this.getStock(); + this.getProductCategories(); + } + + fillInitial(data: Partial) { + this.state$.set({ ...INITIAL_POS_STATE, ...data }); + } + + getInfo() { + this.setState({ getInfoLoading: true }); + this.service.getInfo().subscribe({ + next: (res) => { + this.setState({ info: res, getInfoLoading: false }); + }, + error: () => { + this.setState({ getInfoLoading: false }); + }, + }); + } + + getStock() { + this.setState({ getStockLoading: true }); + this.service.getStock().subscribe({ + next: (res) => { + this.setState({ stock: res.data, getStockLoading: false }); + }, + error: () => { + this.setState({ getStockLoading: false }); + }, + }); + } + + getProductCategories() { + this.setState({ getProductCategoriesLoading: true }); + this.service + .getCategories() + .pipe( + map((res) => { + return { + data: [ + { + id: 0, + name: 'همه', + totalQuantity: res.data.reduce((acc, curr) => acc + curr.totalQuantity, 0), + productCount: res.data.reduce((acc, curr) => acc + curr.productCount, 0), + }, + ...res.data, + ], + meta: res.meta, + }; + }), + ) + .subscribe({ + next: (res) => { + this.setState({ + productCategories: res.data, + getProductCategoriesLoading: false, + activeProductCategory: res.data[0]?.id, + }); + }, + error: () => { + this.setState({ getProductCategoriesLoading: false }); + }, + }); + } + + changeActiveCategory(categoryId: number) { + this.setState({ activeProductCategory: categoryId }); + } + + addToInOrderProducts(productId: number) { + const productStock = this.state$().stock?.find((s) => s.product.id === productId); + if (!productStock) return; + const { product } = productStock; + + if (this.state$().inOrderProducts.some((p) => p.productId === productId)) { + this.updateInOrderProductQuantity( + productId, + this.state$().inOrderProducts.find((p) => p.productId === productId)!.quantity + 1, + ); + } else { + this.setState({ + inOrderProducts: [ + ...this.state$().inOrderProducts, + { + product, + productId: product.id, + quantity: 1, + fee: product.salePrice, + maxQuantity: productStock.quantity, + }, + ], + }); + } + } + + removeFromInOrderProducts(productId: number) { + this.setState({ + inOrderProducts: this.state$().inOrderProducts.filter((p) => p.productId !== productId), + }); + } + + updateInOrderProductQuantity(productId: number, quantity: number) { + const inOrderProducts = this.state$().inOrderProducts; + + if (quantity < 1) { + this.removeFromInOrderProducts(productId); + } else if (inOrderProducts.findIndex((p) => p.productId === productId) === -1) { + this.addToInOrderProducts(productId); + } else { + const updatedProducts = inOrderProducts.map((p) => { + if (p.productId === productId) { + return { ...p, quantity: Math.min(quantity, p.maxQuantity) }; + } + return p; + }); + this.setState({ inOrderProducts: updatedProducts }); + } + } + + updateInOrderProductFee(productId: number, fee: string) { + const inOrderProducts = this.state$().inOrderProducts; + + if (inOrderProducts.some((p) => p.productId === productId)) { + const updatedProducts = inOrderProducts.map((p) => { + if (p.productId === productId) { + return { ...p, fee: fee }; + } + return p; + }); + this.setState({ inOrderProducts: updatedProducts }); + } + } + + resetInOrderProducts() { + this.setState({ inOrderProducts: [] }); + } +} diff --git a/src/app/modules/pos/views/pos.component.html b/src/app/modules/pos/views/pos.component.html index 7144e26..2b1257e 100644 --- a/src/app/modules/pos/views/pos.component.html +++ b/src/app/modules/pos/views/pos.component.html @@ -1 +1,26 @@ -
+
+
+
+
+ Logo +
+ {{ info()?.store?.name }} +
+
+
+ +
+
+ فروشنده شماره‌ی ۱ +
+
+
+
+
+ +
+
+ +
+
+
diff --git a/src/app/modules/pos/views/pos.component.ts b/src/app/modules/pos/views/pos.component.ts index 07e9eec..ff52803 100644 --- a/src/app/modules/pos/views/pos.component.ts +++ b/src/app/modules/pos/views/pos.component.ts @@ -1,9 +1,40 @@ -import { Component } from '@angular/core'; +import { Maybe } from '@/core'; +import { JalaliDateDirective } from '@/shared/directives'; +import { Component, signal } from '@angular/core'; +import images from 'src/assets/images'; +import { PosOrderCardComponent } from '../components/order/order-card.component'; +import { PosProductsComponent } from '../components/products/products.component'; +import { IPosInfoResponse } from '../models'; +import { PosService } from '../services/main.service'; @Component({ selector: 'app-pos', templateUrl: './pos.component.html', + imports: [JalaliDateDirective, PosProductsComponent, PosOrderCardComponent], }) export class POSComponent { - constructor() {} + constructor(private service: PosService) { + this.getInfo(); + } + + placeholderLogo = images.placeholders.logo; + + now = new Date(); + inventoryId = 2; + + info = signal>(null); + infoLoading = signal(true); + + getInfo() { + this.infoLoading.set(true); + this.service.getInfo().subscribe({ + next: (res) => { + this.info.set(res); + this.infoLoading.set(false); + }, + error: () => { + this.infoLoading.set(false); + }, + }); + } } diff --git a/src/app/modules/productBrands/views/list.component.ts b/src/app/modules/productBrands/views/list.component.ts index f6f39f7..c60375e 100644 --- a/src/app/modules/productBrands/views/list.component.ts +++ b/src/app/modules/productBrands/views/list.component.ts @@ -37,8 +37,6 @@ export class ProductBrandsComponent { getData() { this.loading.set(true); this.productBrandService.getAll().subscribe((res) => { - console.log(res); - this.loading.set(false); this.items.set(res.data); }); diff --git a/src/app/modules/products/components/fullForm/full-form.component.html b/src/app/modules/products/components/fullForm/full-form.component.html index c3e1e6c..7c7547f 100644 --- a/src/app/modules/products/components/fullForm/full-form.component.html +++ b/src/app/modules/products/components/fullForm/full-form.component.html @@ -41,7 +41,7 @@
- +
diff --git a/src/app/modules/products/components/fullForm/full-form.component.ts b/src/app/modules/products/components/fullForm/full-form.component.ts index 3572105..403c4e2 100644 --- a/src/app/modules/products/components/fullForm/full-form.component.ts +++ b/src/app/modules/products/components/fullForm/full-form.component.ts @@ -56,7 +56,7 @@ export class ProductFullFormComponent { description: [this.initialData?.description || null], categoryId: [this.initialData?.category?.id || null], imageUrl: [null], - basePrice: [this.initialData?.basePrice || 0, [Validators.required, Validators.min(0)]], + salePrice: [this.initialData?.salePrice || 0, [Validators.required, Validators.min(0)]], }); get backRoute() { diff --git a/src/app/modules/products/models/io.d.ts b/src/app/modules/products/models/io.d.ts index 8fdfb6c..692192c 100644 --- a/src/app/modules/products/models/io.d.ts +++ b/src/app/modules/products/models/io.d.ts @@ -11,7 +11,7 @@ export interface IProductRawResponse { brand?: IProductBrand; category?: IProductCategory; // supplierId: number; - basePrice?: string; + salePrice?: string; count?: number; createdAt: Date; updatedAt: Date; diff --git a/src/app/modules/products/views/list.component.ts b/src/app/modules/products/views/list.component.ts index 299d1ad..b245d91 100644 --- a/src/app/modules/products/views/list.component.ts +++ b/src/app/modules/products/views/list.component.ts @@ -23,7 +23,7 @@ export class ProductsComponent { } columns = [ - { field: 'id', header: 'شناسه' }, + { field: 'sku', header: 'شناسه' }, { field: 'name', header: 'نام' }, { field: 'brand', @@ -39,6 +39,11 @@ export class ProductsComponent { return item.category?.name || '-'; }, }, + { + field: 'salePrice', + header: 'قیمت فروش', + type: 'price', + }, { field: 'createdAt', header: 'تاریخ ایجاد', type: 'date' }, { field: 'updatedAt', header: 'تاریخ به‌روزرسانی', type: 'date' }, ] as IColumn[]; diff --git a/src/app/modules/products/views/single.component.ts b/src/app/modules/products/views/single.component.ts index b882168..1b79e6a 100644 --- a/src/app/modules/products/views/single.component.ts +++ b/src/app/modules/products/views/single.component.ts @@ -2,6 +2,7 @@ import { Maybe } from '@/core'; import { PurchaseFormComponent } from '@/modules/purchases/components/form.component'; import { KeyValueComponent } from '@/shared/components'; import { DetailsInfoCardComponent } from '@/shared/components/detailsInfoCard/details-info-card.component'; +import priceMaskUtils from '@/utils/price-mask.utils'; import { Component, inject, signal } from '@angular/core'; import { ActivatedRoute, RouterLink } from '@angular/router'; import { Button, ButtonDirective } from 'primeng/button'; @@ -86,12 +87,17 @@ export class ProductComponent { { icon: 'pi pi-dollar', label: 'قیمت پایه', - value: this.product()?.basePrice?.toString() || 'تعیین نشده', + value: this.product()?.salePrice + ? priceMaskUtils.formatWithCurrency(this.product()!.salePrice!) + : 'تعیین نشده', }, { icon: 'pi pi-dollar', - label: 'میانگین قیمت', - value: this.product()?.avgCost?.toString() || 'تعیین نشده', + label: 'میانگین قیمت خرید', + value: this.product()?.salePrice + ? priceMaskUtils.formatWithCurrency(this.product()!.avgCost!) + : 'تعیین نشده', + type: 'price', }, { icon: 'pi pi-clock', diff --git a/src/app/modules/purchases/components/form.component.ts b/src/app/modules/purchases/components/form.component.ts index 0255f24..2b832d4 100644 --- a/src/app/modules/purchases/components/form.component.ts +++ b/src/app/modules/purchases/components/form.component.ts @@ -78,9 +78,6 @@ export class PurchaseFormComponent { submit() { this.form.markAllAsTouched(); - console.log(this.form); - console.log(this.form.value); - if (this.form.valid) { this.form.disable(); const data = this.form.value as IPurchaseRequest; diff --git a/src/app/modules/purchases/components/receiptTemplate/purchase-receipt-template.component.ts b/src/app/modules/purchases/components/receiptTemplate/purchase-receipt-template.component.ts index a581d6f..0395fd0 100644 --- a/src/app/modules/purchases/components/receiptTemplate/purchase-receipt-template.component.ts +++ b/src/app/modules/purchases/components/receiptTemplate/purchase-receipt-template.component.ts @@ -172,7 +172,6 @@ export class PurchaseReceiptTemplateComponent { return { ...res, productId: item.product?.id }; }), } as IPurchaseRequest; - console.log(payload); this.form.disable(); this.service.create(payload).subscribe({ diff --git a/src/app/modules/users/views/list.component.ts b/src/app/modules/users/views/list.component.ts index 5985482..89f02f9 100644 --- a/src/app/modules/users/views/list.component.ts +++ b/src/app/modules/users/views/list.component.ts @@ -43,8 +43,6 @@ export class UsersComponent { getData() { this.loading.set(true); this.userService.getAll().subscribe((res) => { - console.log(res); - this.loading.set(false); this.items.set(res.data); }); diff --git a/src/app/shared/components/detailsInfoCard/details-info-card.component.html b/src/app/shared/components/detailsInfoCard/details-info-card.component.html index dbe46e8..31f6b80 100644 --- a/src/app/shared/components/detailsInfoCard/details-info-card.component.html +++ b/src/app/shared/components/detailsInfoCard/details-info-card.component.html @@ -1,10 +1,10 @@ -
+
- {{ label }} + {{ label }} {{ value }}
diff --git a/src/app/shared/components/pageDataList/page-data-list.component.ts b/src/app/shared/components/pageDataList/page-data-list.component.ts index 2fdb930..802e52c 100644 --- a/src/app/shared/components/pageDataList/page-data-list.component.ts +++ b/src/app/shared/components/pageDataList/page-data-list.component.ts @@ -1,5 +1,5 @@ import { PaginatorComponent, UikitCopyComponent, UikitEmptyStateComponent } from '@/uikit'; -import { formatJalali } from '@/utils'; +import { formatJalali, formatWithCurrency } from '@/utils'; import { CommonModule } from '@angular/common'; import { Component, @@ -129,7 +129,7 @@ export class PageDataListComponent { case 'boolean': return data ? 'بله' : 'خیر'; case 'price': - return typeof data === 'number' ? data.toLocaleString() : '-'; + return formatWithCurrency(data, false, 'ریال'); default: break; } diff --git a/src/app/shared/directives/price-mask.directive.ts b/src/app/shared/directives/price-mask.directive.ts index 677a311..852fbe6 100644 --- a/src/app/shared/directives/price-mask.directive.ts +++ b/src/app/shared/directives/price-mask.directive.ts @@ -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 = { - '٠': '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); } } diff --git a/src/app/uikit/uikit-counter.component.html b/src/app/uikit/uikit-counter.component.html index a651bf2..95cf754 100644 --- a/src/app/uikit/uikit-counter.component.html +++ b/src/app/uikit/uikit-counter.component.html @@ -1,35 +1,38 @@ -
+
+ icon="pi pi-plus" + aria-label="increase" + size="small" + class="shrink-0" + outlined + (click)="increase()" + > - + icon="pi pi-minus" + aria-label="decrease" + size="small" + class="shrink-0" + outlined + (click)="decrease()" + >
diff --git a/src/app/uikit/uikit-counter.component.ts b/src/app/uikit/uikit-counter.component.ts index e748a10..5f9476f 100644 --- a/src/app/uikit/uikit-counter.component.ts +++ b/src/app/uikit/uikit-counter.component.ts @@ -1,22 +1,24 @@ import { CommonModule } from '@angular/common'; -import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { Component, Input, model } from '@angular/core'; +import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { ButtonModule } from 'primeng/button'; +import { InputNumber } from 'primeng/inputnumber'; @Component({ selector: 'app-uikit-counter', templateUrl: './uikit-counter.component.html', styleUrls: ['./uikit-counter.component.scss'], standalone: true, - imports: [CommonModule, ButtonModule], + imports: [CommonModule, ButtonModule, FormsModule, ReactiveFormsModule, FormsModule, InputNumber], }) export class UikitCounterComponent { - @Input() value = 0; + @Input() control = new FormControl(0); @Input() min: number | null = null; @Input() max: number | null = null; @Input() step = 1; @Input() disabled = false; - @Output() valueChange = new EventEmitter(); + value = model(0); clamp(v: number): number { if (this.min !== null && v < this.min) return this.min; @@ -26,20 +28,20 @@ export class UikitCounterComponent { setValue(v: number) { const clamped = this.clamp(v); - if (clamped !== this.value) { - this.value = clamped; - this.valueChange.emit(this.value); + if (clamped !== this.value()) { + this.control?.setValue(clamped); + this.value.set(clamped); } } increase() { if (this.disabled) return; - this.setValue(this.value + this.step); + this.setValue(this.value() + this.step); } decrease() { if (this.disabled) return; - this.setValue(this.value - this.step); + this.setValue(this.value() - this.step); } onInput(e: Event) { diff --git a/src/app/utils/index.ts b/src/app/utils/index.ts index af2e5ff..b3d1f7f 100644 --- a/src/app/utils/index.ts +++ b/src/app/utils/index.ts @@ -1,4 +1,5 @@ export * from './currency'; export * from './jalali-date.utils'; export * from './name'; +export * from './price-mask.utils'; export * from './time'; diff --git a/src/app/utils/jalali-date.utils.ts b/src/app/utils/jalali-date.utils.ts index 557b4f6..c961902 100644 --- a/src/app/utils/jalali-date.utils.ts +++ b/src/app/utils/jalali-date.utils.ts @@ -13,7 +13,7 @@ export function fromJalali(jalaliDate: string): Dayjs { } export function formatJalali(date: string | number | Date | Dayjs, format = 'YYYY/MM/DD'): string { - return toJalali(date).format(format); + return toJalali(date).locale('fa').format(format); } export function jalaliDiff( diff --git a/src/app/utils/price-mask.utils.ts b/src/app/utils/price-mask.utils.ts new file mode 100644 index 0000000..d132c40 --- /dev/null +++ b/src/app/utils/price-mask.utils.ts @@ -0,0 +1,77 @@ +export function toArabicDigitsAwareNumber(str: string): string { + if (!str) return ''; + const map: Record = { + '٠': '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); +} + +export function cleanNumericString(raw: string): string { + const ascii = toArabicDigitsAwareNumber(raw || ''); + return ascii.replace(/[^0-9.\-]/g, ''); +} + +export function 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 = cleanNumericString(String(raw)); + if (cleaned === '') return null; + const num = Number(cleaned); + return isNaN(num) ? null : num; +} + +export function formatNumber( + value: number, + opts?: { locale?: string; useComma?: boolean; fraction?: number }, +): string { + try { + const fmtLocale = opts?.useComma ? 'en-US' : (opts?.locale ?? 'fa-IR'); + return new Intl.NumberFormat(fmtLocale, { + maximumFractionDigits: opts?.fraction ?? 0, + minimumFractionDigits: 0, + useGrouping: true, + }).format(value); + } catch (e) { + return String(value); + } +} + +export function formatWithCurrency( + num: number | string | null, + isInputHost: boolean = false, + currency: string | null | undefined = 'ریال', + opts?: { locale?: string; useComma?: boolean; fraction?: number }, +): string { + if (num === null) return ''; + const formatted = formatNumber(parseFloat(num + ''), opts); + if (isInputHost) return formatted; + if (currency && currency.trim()) return `${formatted} ${currency.trim()}`; + return formatted; +} + +export default { + toArabicDigitsAwareNumber, + cleanNumericString, + normalizeToNumber, + formatNumber, + formatWithCurrency, +}; diff --git a/src/assets/images/index.ts b/src/assets/images/index.ts index 638f57b..3547e95 100644 --- a/src/assets/images/index.ts +++ b/src/assets/images/index.ts @@ -3,9 +3,11 @@ import errors from './errors'; import login from './login.webp'; import logo from './logo.png'; +import { placeholders } from './placeholders'; export default { logo, login, errors, + placeholders, }; diff --git a/src/assets/images/placeholders/index.ts b/src/assets/images/placeholders/index.ts new file mode 100644 index 0000000..2202d71 --- /dev/null +++ b/src/assets/images/placeholders/index.ts @@ -0,0 +1,7 @@ +import logo from './logoPlaceholder.png'; +import productPlaceholder from './productPlaceholder.png'; + +export const placeholders = { + logo, + default: productPlaceholder, +}; diff --git a/src/assets/images/placeholders/logoPlaceholder.png b/src/assets/images/placeholders/logoPlaceholder.png new file mode 100644 index 0000000000000000000000000000000000000000..cb59239e3b4cc1e59535d64c7ab1daf29e0f046d GIT binary patch literal 5579 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 literal 0 HcmV?d00001 diff --git a/src/assets/images/placeholders/productPlaceholder.png b/src/assets/images/placeholders/productPlaceholder.png new file mode 100644 index 0000000000000000000000000000000000000000..6c6877a4e4aec72907d196e591b3363d948e2063 GIT binary patch literal 1444 zcmcIk{Xf$Q0A8Vsl7wi{-7JR!SGKnsJcvl#PZ$Fg!SEtM zZ;!-GS+R0hfyE0;HE^EF*$vZ#SH4qL0!$KIvNTi}=l-v%A*rhWf!ShK6#m2&xo3G0 z;(YBwo?j+!YqK7_nWzuLIOYd0#@!qw`17wc=4%WjBV@8gXr_O*-||Nk?)T~feM`dE z0%78Juv&y|7%O7Moy%8c)Ck^pG9Mm%L2J>##f$G0vY5-LRvmXL-AY~@<8U6=mfYv5X11Kg_7~SZ9Q74rfAH18r5ofA-Yjvj`f9q&+s^~c!Qy!m16n9>iS)J@4j!{bOa)*JXJ-59 zyu9`iG3N$5fM4$OnNXmQ1{bGd7GYs=2P^#NhoaTeAoY>;j`*VG?acRfr^F$OUGNvG z8JmygB{3wfvyHK@c3;{(8?U9x)Y`k_Z?qC!PHTx%`Y1;fOR*c*TA3SX{nn3!*M-Cy zJK_0&1oFCPBly%E*|(`GC5zB>sd0nHxMqc`pe4zLNL{k@G1`=tHj!QtCW%xIEP!9Zj=3Z3@-qJMe_`yC+3)pz=2DKh5j)H;_kcgo0W#)A$IR&|x zV>RYzf6>MxthIPGrRwN}=*r{RTnXye&|)mP%%tPQWtfID@85VDV5lK;E?K4<*@?eA z_cGP-L2HpNMH{A-)khMKMI=rihF54!WC6mwqwN+Cy?`MdYc-@oX~68E$mV!1rNcAh zg4`UjMZKq8&+dTB4)YtgKJuX=-MSE&V=;}Or;mu|tY}TsJIn%RFTr*-kTT-WUr1wR zcSQ9d_vOcvYrSK4HBZ%Zd)QT_x=s}1Lj7uDg_3*2G9E+oHrD6^B1jI5pxmMgyHH!z zbjmHpzWa9m#!1~d7Q%v9Y~#jIiFWwS9X}IOc>JpPj19TX#Zp%nY8QNi)^q#&G-e4> z@bYcfTwTnQNARz3lT6?ZB>lh$dH2Y(C*u;aJtktfl>E&Hn%;L?MIOT}t$+1F?aPja zH!D8l54Gm0mXXz;5v@Y#F}OcID|fZ!T_28bp~CR;Hj(^fFMe{MCu)3MhuVE86L;#G e{?3>FCnPd8GUIAs`Apr7e|Bg$0QJ-*B>Qi}=dFzZ literal 0 HcmV?d00001 diff --git a/src/assets/tailwind.css b/src/assets/tailwind.css index 8129b61..3919836 100644 --- a/src/assets/tailwind.css +++ b/src/assets/tailwind.css @@ -35,3 +35,10 @@ --color-error: var(--p-red-500); --p-inputtext-padding-y: 1rem !important; } + +@layer utilities { + input[type="number"]::-webkit-inner-spin-button, + input[type="number"]::-webkit-outer-spin-button { + @apply appearance-none; + } +}