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:
@@ -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: [] });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user