remove unused codes and update

This commit is contained in:
2026-05-10 09:44:30 +03:30
parent a138034c06
commit 048e292bdd
107 changed files with 317 additions and 17930 deletions
@@ -1,6 +1,6 @@
<div class="bg-white h-svh relative">
<div class="absolute inset-0 opacity-30 blur-xs flex items-center justify-center">
<img [src]="authVector" alt="background image" class="object-cover max-w-full max-h-full" />
<img [src]="authVector" alt="background image" class="w-full bg-cover bg-no-repeat" />
</div>
<div class="flex justify-center items-center relative z-10 h-svh mx-auto bg-gray-800/80">
<div
@@ -1,8 +0,0 @@
const baseUrl = '/api/v1/pos';
export const POS_API_ROUTES = {
info: (posId: number) => `${baseUrl}/${posId}`,
stock: (posId: number) => `${baseUrl}/${posId}/stock`,
productCategories: (posId: number) => `${baseUrl}/${posId}/product-categories`,
submitOrder: (posId: number) => `${baseUrl}/${posId}/orders/create`,
};
@@ -1,2 +0,0 @@
export * from './apiRoutes';
export * from './routes';
@@ -1,18 +0,0 @@
import { NamedRoutes } from '@/core';
import { Routes } from '@angular/router';
export const dashboardNamedRoutes: NamedRoutes<any> = {
POS: {
path: '',
loadComponent: () =>
import('../../views/dashboard.component').then((m) => m.DashboardComponent),
meta: {
title: 'داشبورد',
pagePath: () => ``,
},
},
};
export type TDashboardRouteNames = keyof typeof dashboardNamedRoutes;
export const DASHBOARD_ROUTES: Routes = Object.values(dashboardNamedRoutes);
@@ -1,7 +0,0 @@
<div class="">
<div class="grid grid-cols-2 gap-4">
<!-- <app-statistics-shared-top-alert-stocks class="block h-[300px]" />
<app-statistics-shared-top-sales class="block h-[300px]" />
<app-statistics-shared-top-supplier-debts class="block h-[300px]" /> -->
</div>
</div>
@@ -1,33 +0,0 @@
import { Component } from '@angular/core';
import images from 'src/assets/images';
@Component({
selector: 'app-dashboard',
templateUrl: './dashboard.component.html',
imports: [
// StatisticsSharedTopAlertStocksComponent,
// StatisticsSharedTopSalesComponent,
// StatisticsSharedTopSupplierDebtsComponent,
],
})
export class DashboardComponent {
placeholderLogo = images.placeholders.logo;
now = new Date();
// info = this.store.info;
// infoLoading = this.store.getInfoLoading;
// getInfo() {
// this.infoLoading.set(true);
// this.service.getInfo(Number(this.posId)).subscribe({
// next: (res) => {
// this.info.set(res);
// this.infoLoading.set(false);
// },
// error: () => {
// this.infoLoading.set(false);
// },
// });
// }
}
@@ -1,28 +0,0 @@
<div class="flex items-center gap-2 justify-between">
<div class="flex flex-col">
<span class="font-bold text-lg">سفارش #{{ heldOrder.orderNumber }}</span>
<span class="text-sm text-muted-color">
{{ heldOrder.orderItems.length }} کالا -
<span [appPriceMask]="heldOrder.totalAmount"></span>
</span>
</div>
<div class="flex items-center gap-2">
<button
pButton
size="small"
type="button"
label="بارگذاری"
icon="pi pi-download"
(click)="loadHeldOrder(heldOrder.id)"
></button>
<button
pButton
size="small"
type="button"
label="حذف"
icon="pi pi-trash"
severity="danger"
(click)="cancelHeldOrder(heldOrder.id)"
></button>
</div>
</div>
@@ -1,57 +0,0 @@
import { ToastService } from '@/core/services/toast.service';
import { ConfirmationDialogService } from '@/shared/components/confirmationDialog/confirmation-dialog.service';
import { PriceMaskDirective } from '@/shared/directives';
import { Component, EventEmitter, Input, Output, signal } from '@angular/core';
import { ButtonDirective } from 'primeng/button';
import { IPosHeldOrderResponse } from '../../models';
import { PosService } from '../../services/main.service';
@Component({
selector: 'app-held-order-item',
templateUrl: './held-order-item.component.html',
imports: [PriceMaskDirective, ButtonDirective],
})
export class HeldOrderItemComponent {
@Input() heldOrder!: IPosHeldOrderResponse;
@Input() posId!: number;
@Output() onCancel = new EventEmitter<number>();
@Output() onLoad = new EventEmitter<number>();
constructor(
private readonly posService: PosService,
private readonly confirmationService: ConfirmationDialogService,
private readonly toastService: ToastService,
) {}
actionLoading = signal<boolean>(false);
cancelHeldOrder(heldOrderId: number) {
this.confirmationService.confirm({
message: `آیا از لغو سفارش شماره‌ی #${this.heldOrder.orderNumber} اطمینان دارید؟`,
header: 'لغو سفارش موقت',
acceptLabel: 'بله',
rejectLabel: 'خیر',
accept: () => {
this.actionLoading.set(true);
this.posService.cancelOrder(this.posId, heldOrderId).subscribe({
next: () => {
this.onCancel.emit(heldOrderId);
this.actionLoading.set(false);
this.toastService.success({
text: `سفارش شماره‌ی #${this.heldOrder.orderNumber} با موفقیت لغو شد.`,
});
},
error: () => {
this.actionLoading.set(false);
},
});
},
reject: () => {},
});
}
loadHeldOrder(heldOrderId: number) {
this.onLoad.emit(heldOrderId);
}
}
@@ -1,19 +0,0 @@
<div class="d-flex flex-col gap-4">
<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>
</div>
@if (!heldOrders()?.length) {
<div class="grow flex flex-col gap-5 justify-center items-center text-center text-muted-color">
<span class="text-base text-muted-color pt-4">هیچ سفارش نگه‌داشته شده‌ای وجود ندارد.</span>
</div>
} @else {
<div class="flex flex-col gap-2 mt-3">
@for (heldOrder of heldOrders(); track heldOrder.id) {
<app-held-order-item [heldOrder]="heldOrder" [posId]="posId" (onCancel)="removeHeldOrder($event)" />
}
</div>
}
</div>
@@ -1,41 +0,0 @@
import { Maybe } from '@/core';
import { Component, Input, signal } from '@angular/core';
import { IPosHeldOrderResponse } from '../../models';
import { PosService } from '../../services/main.service';
import { HeldOrderItemComponent } from './held-order-item.component';
@Component({
selector: 'app-held-orders',
templateUrl: './held-orders.component.html',
imports: [HeldOrderItemComponent],
})
export class HeldOrdersComponent {
@Input() posId!: number;
constructor(private readonly posService: PosService) {}
heldOrders = signal<Maybe<IPosHeldOrderResponse[]>>(null);
loading = signal<boolean>(false);
ngOnInit() {
this.getHeldOrders();
}
getHeldOrders() {
this.loading.set(true);
this.posService.getHeldOrders(this.posId).subscribe({
next: (res) => {
this.heldOrders.set(res.data);
this.loading.set(false);
},
error: () => {
this.loading.set(false);
},
});
}
removeHeldOrder(orderId: number) {
const currentOrders = this.heldOrders() || [];
this.heldOrders.set(currentOrders.filter((order) => order.id !== orderId));
}
}
@@ -1,110 +0,0 @@
<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">
<app-held-orders [posId]="posId" />
</div>
<hr />
<div class="shrink-0 sticky top-0">
<!-- <customers-select-field
[canInsert]="true"
[value]="selectedCustomer()"
(valueChange)="updateCustomer($event)"
[isFullDataOptionValue]="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-trash" 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.unitPrice"></span>
</ng-template>
<ng-template #content let-closeCallback="closeCallback">
<p-input-number
type="text"
inputStyleClass="w-24"
[min]="0"
[(ngModel)]="inOrderProduct.unitPrice"
(onBlur)="
updateInOrderProductFee(inOrderProduct.productId, $event, inOrderProduct.unitPrice);
closeCallback()
"
/>
</ng-template>
</p-inplace>
<div class="ms-auto shrink-0">
<app-uikit-counter
[min]="1"
[max]="inOrderProduct.maxQuantity"
[value]="inOrderProduct.count"
(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-2 sticky bottom-0 gap-2 pt-4">
<button
pButton
type="button"
label="ثبت موقت"
severity="primary"
icon="pi pi-check"
class="w-full"
(click)="submit()"
></button>
<button
pButton
type="button"
label="ثبت و پرداخت"
severity="primary"
icon="pi pi-check"
class="w-full"
[attr.disabled]="true"
(click)="submitAndPay()"
></button>
</div>
</div>
</div>
@@ -1,76 +0,0 @@
// import { CustomersSelectComponent } from '@/modules/customers/components/select/select.component';
// import { ICustomerResponse } from '@/modules/customers/models';
import { PriceMaskDirective } from '@/shared/directives';
import { UikitCounterComponent } from '@/uikit';
import { Component, computed, inject } 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 { HeldOrdersComponent } from './held-orders.component';
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,
HeldOrdersComponent,
],
})
export class PosOrderCardComponent {
private store = inject(POSStore);
constructor() {
// this.selectedCustomer.set(this.store.selectedCustomer());
}
posId = this.store.posId;
placeholderImage = images.placeholders.default;
inOrderProducts = computed(() => this.store.inOrderProducts());
// selectedCustomer = signal<Maybe<ICustomerResponse>>(null);
removeProductFromOrder(productId: number) {
this.store.removeFromInOrderProducts(productId);
}
updateInOrderProductQuantity(productId: number, quantity: number) {
this.store.updateInOrderProductQuantity(productId, quantity);
}
updateInOrderProductFee(productId: number, $event: Event, prevFee: number) {
const unitPrice = $event.target
? parseFloat(($event.target as HTMLInputElement).ariaValueNow || prevFee.toString())
: prevFee;
this.store.updateInOrderProductFee(productId, unitPrice);
}
clearOrderList() {
this.store.resetInOrderProducts();
}
// updateCustomer(customer: Maybe<ICustomerResponse>) {
// this.store.setSelectedCustomer(customer);
// }
submit() {
this.store.submitOrder();
}
submitAndPay() {
this.store.submitOrder();
}
}
@@ -1,24 +0,0 @@
<p-card class="border border-surface-border">
<div class="flex flex-col gap-3">
<div class="flex justify-between items-center">
<span>جمع قیمت</span>
@if (priceInfo().totalAmount == priceInfo().totalBaseAmount) {
<span [appPriceMask]="priceInfo().totalAmount"></span>
} @else {
<div class="flex flex-col items-end">
<span class="line-through text-muted-color text-xs" [appPriceMask]="priceInfo().totalBaseAmount"></span>
<span [appPriceMask]="priceInfo().totalAmount"></span>
</div>
}
</div>
<div class="flex justify-between items-center">
<span>ارزش افزوده</span>
<span [appPriceMask]="priceInfo().taxAmount"></span>
</div>
<hr />
<div class="flex justify-between items-center font-bold text-lg">
<span>مجموع کل</span>
<span [appPriceMask]="priceInfo().payableAmount"></span>
</div>
</div>
</p-card>
@@ -1,15 +0,0 @@
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());
}
@@ -1,29 +0,0 @@
<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 shrink-0' +
(selectedCategory() === 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() === category.id ? ' bg-amber-400! text-white' : '')
"
>
<span>
{{ category.productCount }}
</span>
</div>
</div>
}
}
</div>
@@ -1,23 +0,0 @@
import { Component, EventEmitter, inject, Output } from '@angular/core';
import { Skeleton } from 'primeng/skeleton';
import { POSStore } from '../../store';
@Component({
selector: 'pos-product-categories',
templateUrl: './categories.component.html',
imports: [Skeleton],
})
export class PosProductCategoriesComponent {
@Output() categoryChange = new EventEmitter<number>();
private readonly store = inject(POSStore);
categories = this.store.productCategories;
loading = this.store.getProductCategoriesLoading;
selectedCategory = this.store.activeProductCategory;
changeSelectedCategory(category: { id: number; name: string }) {
this.store.changeActiveCategory(category.id);
}
}
@@ -1,25 +0,0 @@
<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>
@@ -1,25 +0,0 @@
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);
}
}
@@ -1,30 +0,0 @@
<div class="flex flex-col 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="w-100 h-12">
<p-skeleton width="100%" height="100%"></p-skeleton>
</div>
}
} @else {
@for (product of products(); track product.id) {
<div class="flex items-center bg-surface-card rounded-lg p-1 shadow-xs gap-2">
<div class="grow flex items-center gap-2">
<img [src]="productPlaceholder" class="w-12 h-12 object-cover rounded-md" />
<div class="flex flex-col gap-2">
<span class="text-lg font-bold">
{{ product.product.name }}
<small class="text-xs text-muted-color">({{ product.quantity }})</small>
</span>
<span class="text-sm text-muted-color">
{{ product.product.sku }}
</span>
</div>
</div>
<div class="shrink-0 flex items-center justify-between gap-3">
<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>
@@ -1,25 +0,0 @@
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-list-view',
templateUrl: './list-view.component.html',
imports: [PriceMaskDirective, ButtonDirective, Skeleton],
})
export class PosProductsListViewComponent {
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);
}
}
@@ -1,26 +0,0 @@
<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>
<div class="flex items-center gap-2">
<app-search-input (search)="onSearch($event)"></app-search-input>
<div class="card p-2! flex justify-center">
<p-selectbutton [options]="viewOptions" [(ngModel)]="viewType" optionValue="value">
<ng-template #item let-item>
<i [class]="item.icon"></i>
</ng-template>
</p-selectbutton>
</div>
</div>
</div>
<pos-product-categories (categoryChange)="onCategoryChange($event)" />
@if (viewType === "grid") {
<pos-products-grid-view />
} @else {
<pos-products-list-view />
}
</div>
@@ -1,55 +0,0 @@
import { Maybe } from '@/core';
import { SearchInputComponent } from '@/shared/components/search/search-input.component';
import { Component, computed, inject, Input, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { SelectButton } from 'primeng/selectbutton';
import { POSStore, TViewType } from '../../store';
import { PosProductCategoriesComponent } from './categories.component';
import { PosProductsGridViewComponent } from './grid-view.component';
import { PosProductsListViewComponent } from './list-view.component';
@Component({
selector: 'pos-products',
templateUrl: './products.component.html',
imports: [
PosProductCategoriesComponent,
PosProductsListViewComponent,
SelectButton,
FormsModule,
PosProductsGridViewComponent,
SearchInputComponent,
],
})
export class PosProductsComponent {
@Input() activeCategory = signal<Maybe<number>>(null);
private readonly store = inject(POSStore);
constructor() {
this.store.initial();
}
viewOptions = [
{ value: 'list', icon: 'pi pi-list' },
{ value: 'grid', icon: 'pi pi-th-large' },
];
readonly activatedCategoryProducts = computed(() => this.store.activatedCategoryProducts());
readonly stock = computed(() => this.store.stock());
get viewType() {
return this.store.viewType();
}
set viewType(val: TViewType) {
this.store.updateViewType(val);
}
onSearch(searchTerm: string) {
this.store.updateSearchQuery(searchTerm);
this.store.getStock();
}
onCategoryChange(categoryId: number) {
this.store.changeActiveCategory(categoryId);
}
}
@@ -1,13 +0,0 @@
const baseUrl = '/api/v1/pos';
export const POS_API_ROUTES = {
info: (posId: number) => `${baseUrl}/${posId}`,
stock: (posId: number) => `${baseUrl}/${posId}/stock`,
productCategories: (posId: number) => `${baseUrl}/${posId}/product-categories`,
submitOrder: (posId: number) => `${baseUrl}/${posId}/orders`,
heldOrders: (posId: number) => `${baseUrl}/${posId}/orders/held`,
order: (posId: number, orderId: number) => `${baseUrl}/${posId}/orders/${orderId}`,
cancelOrder: (posId: number, orderId: number) => `${baseUrl}/${posId}/orders/${orderId}/cancel`,
rejectOrder: (posId: number, orderId: number) => `${baseUrl}/${posId}/orders/${orderId}/reject`,
confirmOrder: (posId: number, orderId: number) => `${baseUrl}/${posId}/orders/${orderId}/confirm`,
};
-2
View File
@@ -1,2 +0,0 @@
export * from './apiRoutes';
export * from './routes';
@@ -1,17 +0,0 @@
import { NamedRoutes } from '@/core';
import { Routes } from '@angular/router';
export type TPOSRouteNames = 'POS';
export const posNamedRoutes: NamedRoutes<TPOSRouteNames> = {
POS: {
path: 'pos/:posId',
loadComponent: () => import('../../views/pos.component').then((m) => m.POSComponent),
meta: {
title: 'نقاط فروش',
pagePath: (params) => `/pos/${params.posId}`,
},
},
};
export const POS_ROUTES: Routes = Object.values(posNamedRoutes);
-5
View File
@@ -1,5 +1,4 @@
import ISummary from '@/core/models/summary';
import { IBankAccountSummary, IPosOrderItem, IPosProductSummary } from './types';
export interface IPosInfoRawResponse {
id: number;
@@ -12,7 +11,6 @@ export interface IPosInfoRawResponse {
updatedAt: string;
deletedAt: null;
inventory: ISummary;
bankAccount: IBankAccountSummary;
}
export interface IPosInfoResponse extends IPosInfoRawResponse {}
@@ -20,7 +18,6 @@ export interface IPosInfoResponse extends IPosInfoRawResponse {}
export interface IPosStockRawResponse {
id: number;
quantity: number;
product: IPosProductSummary;
}
export interface IPosStockResponse extends IPosStockRawResponse {}
@@ -34,7 +31,6 @@ export interface IPosProductCategoriesResponse extends IPosProductCategoriesRawR
export interface IPosOrderRequest {
customerId?: number;
items: IPosOrderItem[];
}
export interface IPosOrderRawResponse {
@@ -53,7 +49,6 @@ export interface IPosOrderRawResponse {
lastName: string;
};
orderItems: {
product: IPosProductSummary;
count: number;
unitPrice: number;
totalPrice: number;
-36
View File
@@ -1,36 +0,0 @@
import ISummary from '@/core/models/summary';
export interface IPosProductSummary {
id: number;
name: string;
sku: string;
salePrice: string;
categoryId: number;
}
interface ICategorySummary {
id: number;
name: string;
}
export interface IPosOrderItem {
productId: number;
count: number;
unitPrice: number;
}
export interface IPosInOrderProduct extends IPosOrderItem {
product: IPosProductSummary;
maxQuantity: number;
}
export interface IBankAccountSummary {
id: number;
name: string;
accountNumber: string;
branch: IBankAccountBranchSummary;
}
interface IBankAccountBranchSummary extends ISummary {
bank: ISummary;
}
@@ -1,66 +0,0 @@
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,
IPosOrderRequest,
IPosProductCategoriesRawResponse,
IPosProductCategoriesResponse,
IPosStockRawResponse,
IPosStockResponse,
} from '../models';
@Injectable({ providedIn: 'root' })
export class PosService {
constructor(private http: HttpClient) {}
private apiRoutes = POS_API_ROUTES;
getInfo(posId: number): Observable<IPosInfoResponse> {
return this.http.get<IPosInfoRawResponse>(this.apiRoutes.info(posId));
}
getStock(
posId: number,
searchQuery: string = '',
): Observable<IPaginatedResponse<IPosStockResponse>> {
return this.http.get<IPaginatedResponse<IPosStockRawResponse>>(this.apiRoutes.stock(posId), {
params: {
q: searchQuery,
},
});
}
getCategories(posId: number): Observable<IPaginatedResponse<IPosProductCategoriesResponse>> {
return this.http.get<IPaginatedResponse<IPosProductCategoriesRawResponse>>(
this.apiRoutes.productCategories(posId),
);
}
submitOrder(posId: number, payload: IPosOrderRequest): Observable<any> {
return this.http.post<any>(this.apiRoutes.submitOrder(posId), payload);
}
getHeldOrders(posId: number): Observable<IPaginatedResponse<any>> {
return this.http.get<IPaginatedResponse<any>>(this.apiRoutes.heldOrders(posId));
}
getOrder(posId: number, orderId: number): Observable<any> {
return this.http.get<any>(this.apiRoutes.order(posId, orderId));
}
cancelOrder(posId: number, orderId: number): Observable<any> {
return this.http.post<any>(this.apiRoutes.cancelOrder(posId, orderId), {});
}
rejectOrder(posId: number, orderId: number): Observable<any> {
return this.http.post<any>(this.apiRoutes.rejectOrder(posId, orderId), {});
}
confirmOrder(posId: number, orderId: number): Observable<any> {
return this.http.post<any>(this.apiRoutes.confirmOrder(posId, orderId), {});
}
}
-272
View File
@@ -1,272 +0,0 @@
import { Maybe } from '@/core';
// import { ICustomerResponse } from '@/modules/customers/models';
import { computed, Inject, Injectable, InjectionToken, signal } from '@angular/core';
import { map, of } from 'rxjs';
import { IPosInfoResponse, IPosProductCategoriesResponse, IPosStockResponse } from '../models';
import { IPosInOrderProduct } from '../models/types';
import { PosService } from '../services/main.service';
export const POS_ID = new InjectionToken<number>('POS_ID');
export type TViewType = 'list' | 'grid';
interface IPosState {
getStockLoading: boolean;
stock: Maybe<IPosStockResponse[]>;
getInfoLoading: boolean;
info: Maybe<IPosInfoResponse>;
getProductCategoriesLoading: boolean;
productCategories: Maybe<IPosProductCategoriesResponse[]>;
activeProductCategory: Maybe<number>;
inOrderProducts: IPosInOrderProduct[];
selectedCustomer: Maybe<any>;
viewType: TViewType;
searchQuery: string;
}
export const INITIAL_POS_STATE: IPosState = {
getStockLoading: true,
stock: null,
getInfoLoading: true,
info: null,
getProductCategoriesLoading: true,
productCategories: null,
activeProductCategory: null,
inOrderProducts: [],
selectedCustomer: null,
viewType: 'grid',
searchQuery: '',
};
@Injectable({ providedIn: 'any' })
export class POSStore {
constructor(
@Inject(POS_ID) posId: number,
private service: PosService,
) {
this.posId = posId;
this.initial();
}
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 selectedCustomer = computed(() => this.state$().selectedCustomer);
readonly viewType = computed(() => this.state$().viewType);
readonly searchQuery = computed(() => this.state$().searchQuery);
readonly activatedCategoryProducts = computed(() => {
if (this.activeProductCategory() === 0) {
return this.state$().stock;
}
return this.state$().stock?.filter(
(s) => s.product.categoryId === this.state$().activeProductCategory,
);
});
readonly orderPricingInfo = computed(() => {
const totalAmount = this.inOrderProducts().reduce(
(acc, curr) => acc + curr.unitPrice * curr.count,
0,
);
const taxAmount = totalAmount * 0.1;
return {
totalItems: this.inOrderProducts().reduce((acc, curr) => acc + curr.count, 0),
totalBaseAmount: this.inOrderProducts().reduce(
(acc, curr) => acc + parseFloat(curr.product.salePrice) * curr.count,
0,
),
totalAmount,
taxAmount,
payableAmount: totalAmount + taxAmount,
};
});
posId!: number;
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(this.posId).subscribe({
next: (res) => {
this.setState({ info: res, getInfoLoading: false });
},
error: () => {
this.setState({ getInfoLoading: false });
},
});
}
getStock() {
this.setState({ getStockLoading: true });
this.service.getStock(this.posId, this.state$().searchQuery).subscribe({
next: (res) => {
this.setState({ stock: res.data, getStockLoading: false });
},
error: () => {
this.setState({ getStockLoading: false });
},
});
}
getProductCategories() {
this.setState({ getProductCategoriesLoading: true });
this.service
.getCategories(this.posId)
.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)!.count + 1,
);
} else {
this.setState({
inOrderProducts: [
...this.state$().inOrderProducts,
{
product,
productId: product.id,
count: 1,
unitPrice: parseFloat(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, count: Math.min(quantity, p.maxQuantity) };
}
return p;
});
this.setState({ inOrderProducts: updatedProducts });
}
}
updateInOrderProductFee(productId: number, unitPrice: number) {
const inOrderProducts = this.state$().inOrderProducts;
if (inOrderProducts.some((p) => p.productId === productId)) {
const updatedProducts = inOrderProducts.map((p) => {
if (p.productId === productId) {
return { ...p, unitPrice: unitPrice };
}
return p;
});
this.setState({ inOrderProducts: updatedProducts });
}
}
updateViewType(viewType: TViewType) {
this.setState({ viewType });
}
updateSearchQuery(searchQuery: string) {
this.setState({ searchQuery });
}
resetInOrderProducts() {
this.setState({ inOrderProducts: [] });
}
setSelectedCustomer(customer: Maybe<any>) {
this.setState({ selectedCustomer: customer });
}
submitOrder() {
const orderPayload = {
customer_id: this.selectedCustomer()?.id,
items: this.inOrderProducts()?.map((item) => ({
productId: item.productId,
count: item.count,
unitPrice: item.unitPrice,
})),
};
return this.service.submitOrder(this.posId, orderPayload).subscribe({
next: (res) => {
this.resetInOrderProducts();
this.setSelectedCustomer(null);
return of(res);
},
error: (err) => {
return of(err);
},
});
}
}
@@ -1,26 +0,0 @@
<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()?.name }} - شعبه {{ info()?.inventory?.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>
@@ -1,44 +0,0 @@
import { JalaliDateDirective } from '@/shared/directives';
import { Component, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import images from 'src/assets/images';
import { PosOrderCardComponent } from '../components/order/order-card.component';
import { PosProductsComponent } from '../components/products/products.component';
import { POSStore, POS_ID } from '../store';
@Component({
selector: 'app-pos',
templateUrl: './pos.component.html',
imports: [JalaliDateDirective, PosProductsComponent, PosOrderCardComponent],
providers: [
{
provide: POS_ID,
useFactory: (route: ActivatedRoute) => Number(route.snapshot.params['posId']),
deps: [ActivatedRoute],
},
POSStore,
],
})
export class POSComponent {
private readonly store = inject(POSStore);
placeholderLogo = images.placeholders.logo;
now = new Date();
info = this.store.info;
infoLoading = this.store.getInfoLoading;
// getInfo() {
// this.infoLoading.set(true);
// this.service.getInfo(Number(this.posId)).subscribe({
// next: (res) => {
// this.info.set(res);
// this.infoLoading.set(false);
// },
// error: () => {
// this.infoLoading.set(false);
// },
// });
// }
}