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:
2025-12-14 20:34:15 +03:30
parent 35be7e0298
commit 17fa65407c
43 changed files with 894 additions and 132 deletions
@@ -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) => {
@@ -92,13 +92,10 @@ export class InventoriesTransferFormComponent {
count: [1, [Validators.required, Validators.min(1)]],
}),
);
console.log(this.form.controls.items);
}
}
clearSelectedProductsInForm($e: TableRowUnSelectEvent<IInventoryProduct>) {
console.log($e.index);
if ($e.index != null) {
this.form.controls.items.removeAt($e.index);
}
@@ -0,0 +1,83 @@
<div class="bg-surface-card w-md shrink-0 h-full overflow-hidden rounded-xl p-4 pb-0 flex flex-col">
<div class="shrink-0 sticky top-0">
<customers-select-field [canInsert]="true" />
</div>
<hr />
<div class="grow overflow-auto flex flex-col">
<div class="flex items-center justify-between shrink-0">
<div class="flex items-center gap-2 text-muted-color">
<i class="pi pi-shopping-cart"></i>
<span class="text-base font-bold">سفارش‌ها</span>
</div>
<button pButton type="button" icon="pi pi-refresh" outlined size="small" (click)="clearOrderList()">
پاک کردن سفارش‌ها
</button>
</div>
@if (!inOrderProducts().length) {
<div class="grow flex flex-col gap-5 justify-center items-center text-center text-muted-color">
<i class="pi pi-inbox text-6xl!"></i>
<span class="text-lg text-muted-color">هیچ سفارشی ثبت نشده است.</span>
</div>
} @else {
<div class="flex flex-col gap-2 mt-3">
@for (inOrderProduct of inOrderProducts(); track inOrderProduct.productId) {
<div class="border border-surface-border rounded-xl p-2 shadow-sm">
<div class="flex gap-3 items-stretch">
<img [src]="placeholderImage" class="w-24 h-24 rounded-md bg-surface-100 object-cover shrink-0" />
<div class="flex flex-col grow justify-between">
<div class="flex items-center">
<div class="flex flex-col grow">
<span class="font-bold text-lg">{{ inOrderProduct.product.name }}</span>
<span class="text-sm text-muted-color">{{ inOrderProduct.product.sku }}</span>
</div>
<button
pButton
type="button"
icon="pi pi-trash"
size="small"
severity="danger"
class="ms-auto"
(click)="removeProductFromOrder(inOrderProduct.productId)"
></button>
</div>
<div class="flex items-center gap-4 mt-auto">
<p-inplace>
<ng-template #display>
<span class="font-bold text-lg shrink-0" [appPriceMask]="inOrderProduct.fee"></span>
</ng-template>
<ng-template #content let-closeCallback="closeCallback">
<p-input-number
type="text"
inputStyleClass="w-24"
[min]="0"
[(ngModel)]="inOrderProduct.fee"
(onBlur)="
updateInOrderProductFee(inOrderProduct.productId, $event, inOrderProduct.fee); closeCallback()
"
/>
</ng-template>
</p-inplace>
<div class="ms-auto shrink-0">
<app-uikit-counter
[min]="1"
[max]="inOrderProduct.maxQuantity"
[value]="inOrderProduct.quantity"
(valueChange)="updateInOrderProductQuantity(inOrderProduct.productId, $event)"
></app-uikit-counter>
</div>
</div>
</div>
</div>
</div>
}
</div>
}
</div>
<div class="flex flex-col sticky bottom-0 py-2">
<pos-order-price-info-card />
<div class="grid grid-cols grid-cols-2 sticky bottom-0 gap-2 pt-4">
<button pButton type="button" label="ثبت سفارش" severity="primary" icon="pi pi-check" class="w-full"></button>
<button pButton type="button" label="لغو" outlined icon="pi pi-times" class="w-full"></button>
</div>
</div>
</div>
@@ -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();
}
}
@@ -0,0 +1,17 @@
<p-card class="border border-surface-border">
<div class="flex flex-col gap-3">
<div class="flex justify-between">
<span>جمع قیمت</span>
<span [appPriceMask]="priceInfo().totalAmount"></span>
</div>
<div class="flex justify-between">
<span>مالیات (۱۰٪)</span>
<span [appPriceMask]="priceInfo().taxAmount"></span>
</div>
<hr />
<div class="flex justify-between font-bold text-lg">
<span>مجموع کل</span>
<span [appPriceMask]="priceInfo().payableAmount"></span>
</div>
</div>
</p-card>
@@ -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());
}
@@ -0,0 +1,29 @@
<div class="flex gap-3 overflow-auto">
@if (loading()) {
@for (i of [1, 2, 3, 4, 5]; track i) {
<p-skeleton width="6rem" height="2.5rem" />
}
} @else {
@for (category of categories(); track category.id) {
<div
[class]="
'h-10 rounded-lg px-3 flex items-center gap-3 border border-surface-border cursor-pointer bg-surface-100 hover:bg-surface-200 transition-all' +
(selectedCategory()?.id === category.id ? ' bg-surface-card' : '')
"
(click)="changeSelectedCategory(category)"
>
<span class="text-sm text-muted-color font-bold"> {{ category.name }}</span>
<div
[class]="
'flex items-center justify-center py-0.5 px-2 rounded-sm bg-surface-300 text-xs text-muted-color' +
(selectedCategory()?.id === category.id ? ' bg-amber-400! text-white' : '')
"
>
<span>
{{ category.productCount }}
</span>
</div>
</div>
}
}
</div>
@@ -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<number>();
constructor(private service: PosService) {
this.getCategories();
}
categories = signal<Maybe<IPosProductCategoriesResponse[]>>(null);
loading = signal(true);
selectedCategory = signal<Maybe<IPosProductCategoriesResponse>>(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);
},
});
}
}
@@ -0,0 +1,25 @@
<div class="grid 2xl:grid-cols-8 xl:grid-cols-6 lg:grid-cols-4 grid-cols-2 gap-3 flex-wrap">
@if (loading()) {
@for (_ of [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15]; track $index) {
<div class="flex-1 aspect-[0.9]">
<p-skeleton width="100%" height="100%"></p-skeleton>
</div>
}
} @else {
@for (product of products(); track product.id) {
<div class="flex-1 bg-surface-card rounded-lg p-1 shadow-xs">
<img [src]="productPlaceholder" class="w-full aspect-[1.3] object-cover rounded-md" />
<div class="mt-2">
<span class="text-lg font-bold">
{{ product.product.name }}
<small class="text-xs text-muted-color">({{ product.quantity }})</small>
</span>
<div class="flex items-center justify-between mt-1">
<span [appPriceMask]="product.product.salePrice" class="text-base font-bold"></span>
<button pButton type="button" icon="pi pi-plus" size="small" (click)="addProduct(product.product)"></button>
</div>
</div>
</div>
}
}
</div>
@@ -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);
}
}
@@ -0,0 +1,12 @@
<div class="flex flex-col gap-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2 text-muted-color">
<i class="pi pi-list"></i>
<span class="text-base font-bold">لیست کالاها</span>
</div>
<button pButton icon="pi pi-search"></button>
</div>
<pos-product-categories (categoryChange)="onCategoryChange($event)" />
<pos-products-grid-view />
</div>
@@ -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<Maybe<number>>(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);
}
}
@@ -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`,
};
+1 -1
View File
@@ -1,2 +1,2 @@
// export * from './apiRoutes';
export * from './apiRoutes';
export * from './routes';
+1
View File
@@ -0,0 +1 @@
export * from './io';
+30
View File
@@ -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[];
}
+23
View File
@@ -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;
}
@@ -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<IPosInfoResponse> {
return this.http.get<IPosInfoRawResponse>(this.apiRoutes.info());
}
getStock(): Observable<IPaginatedResponse<IPosStockResponse>> {
return this.http.get<IPaginatedResponse<IPosStockRawResponse>>(this.apiRoutes.stock());
}
getCategories(): Observable<IPaginatedResponse<IPosProductCategoriesResponse>> {
return this.http.get<IPaginatedResponse<IPosProductCategoriesRawResponse>>(
this.apiRoutes.productCategories(),
);
}
}
+218
View File
@@ -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<IPosStockResponse[]>;
getInfoLoading: boolean;
info: Maybe<IPosInfoResponse>;
getProductCategoriesLoading: boolean;
productCategories: Maybe<IPosProductCategoriesResponse[]>;
activeProductCategory: Maybe<number>;
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<IPosState>({ ...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<IPosState>) {
this.state$.set({ ...this.state$(), ...partial });
}
reset() {
this.state$.set({ ...INITIAL_POS_STATE });
}
initial() {
this.getInfo();
this.getStock();
this.getProductCategories();
}
fillInitial(data: Partial<IPosState>) {
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: [] });
}
}
+26 -1
View File
@@ -1 +1,26 @@
<div class=""></div>
<div class="w-svw h-svh bg-surface-ground flex flex-col gap-4 p-4">
<div class="w-full flex items-center gap-4 shrink-0 justify-between">
<div class="flex gap-2 items-center">
<div class="w-10 h-10">
<img [src]="placeholderLogo" alt="Logo" class="w-full h-full object-cover rounded-md bg-surface-card" />
</div>
<span class="text-lg font-bold">{{ info()?.store?.name }}</span>
</div>
<div class="flex gap-2 items-center">
<div class="bg-surface-card py-1 px-3 rounded-md shadow-sm">
<span [jalaliDate]="now" jalaliFormat="dddd YYYY/MM/DD" class="text-base"></span>
</div>
<div class="bg-surface-card py-1 px-3 rounded-md shadow-sm">
<span class="text-base"> فروشنده شماره‌ی ۱ </span>
</div>
</div>
</div>
<div class="flex gap-4 grow overflow-hidden">
<div class="grow h-full overflow-auto">
<pos-products />
</div>
<div class="shrink-0 h-full">
<pos-order-card />
</div>
</div>
</div>
+33 -2
View File
@@ -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<Maybe<IPosInfoResponse>>(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);
},
});
}
}
@@ -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);
});
@@ -41,7 +41,7 @@
<div class="flex flex-col gap-3 grow">
<p-card header="قیمت">
<form [formGroup]="form" (submit)="submit()" class="flex flex-col gap-4">
<app-input label="قیمت پایه‌ی فروش" [control]="form.controls.basePrice" name="basePrice" />
<app-input label="قیمت پایه‌ی فروش" [control]="form.controls.salePrice" name="salePrice" />
</form>
</p-card>
<p-card header="اطلاعات تکمیلی">
@@ -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() {
+1 -1
View File
@@ -11,7 +11,7 @@ export interface IProductRawResponse {
brand?: IProductBrand;
category?: IProductCategory;
// supplierId: number;
basePrice?: string;
salePrice?: string;
count?: number;
createdAt: Date;
updatedAt: Date;
@@ -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[];
@@ -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',
@@ -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;
@@ -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({
@@ -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);
});