feat: implement held orders management with loading, cancel, and display functionality

This commit is contained in:
2026-01-07 15:25:41 +03:30
parent a5bcab0064
commit d0979be722
15 changed files with 261 additions and 16 deletions
@@ -9,6 +9,7 @@
[loading]="loading()" [loading]="loading()"
[showDetails]="true" [showDetails]="true"
(onAdd)="openAddForm()" (onAdd)="openAddForm()"
(onDetails)="toDetails($event)"
/> />
<bank-account-form [(visible)]="visibleForm" (onSubmit)="refresh()" /> <bank-account-form [(visible)]="visibleForm" (onSubmit)="refresh()" />
@@ -3,6 +3,7 @@ import {
PageDataListComponent, PageDataListComponent,
} from '@/shared/components/pageDataList/page-data-list.component'; } from '@/shared/components/pageDataList/page-data-list.component';
import { Component, signal } from '@angular/core'; import { Component, signal } from '@angular/core';
import { Router } from '@angular/router';
import { BankAccountFormComponent } from '../components/form.component'; import { BankAccountFormComponent } from '../components/form.component';
import { IBankAccountsResponse } from '../models'; import { IBankAccountsResponse } from '../models';
import { BankAccountsService } from '../services/main.service'; import { BankAccountsService } from '../services/main.service';
@@ -13,7 +14,10 @@ import { BankAccountsService } from '../services/main.service';
imports: [PageDataListComponent, BankAccountFormComponent], imports: [PageDataListComponent, BankAccountFormComponent],
}) })
export class BankAccountsComponent { export class BankAccountsComponent {
constructor(private service: BankAccountsService) { constructor(
private service: BankAccountsService,
private router: Router,
) {
this.getData(); this.getData();
} }
@@ -78,4 +82,8 @@ export class BankAccountsComponent {
openAddForm() { openAddForm() {
this.visibleForm.set(true); this.visibleForm.set(true);
} }
toDetails(item: IBankAccountsResponse) {
this.router.navigate(['/bankAccounts', item.id]);
}
} }
@@ -11,21 +11,21 @@
pButton pButton
type="button" type="button"
label="نقاط فروش" label="نقاط فروش"
icon="pi pi-plus" icon="pi pi-shop"
[routerLink]="['/inventories', inventoryId, 'pos-accounts']" [routerLink]="['/inventories', inventoryId, 'pos-accounts']"
></button> ></button>
<button <button
pButton pButton
type="button" type="button"
label="افزایش موجودی" label="افزایش موجودی"
icon="pi pi-plus" icon="pi pi-expand"
[routerLink]="['/inventories', inventoryId, 'purchase']" [routerLink]="['/inventories', inventoryId, 'purchase']"
></button> ></button>
<button <button
pButton pButton
type="button" type="button"
label="انتقال کالا" label="انتقال کالا"
icon="pi pi-plus" icon="pi pi-arrow-right-arrow-left"
[routerLink]="['/inventories', 'transfer']" [routerLink]="['/inventories', 'transfer']"
></button> ></button>
<button pButton type="button" label="ویرایش" icon="pi pi-pencil"></button> <button pButton type="button" label="ویرایش" icon="pi pi-pencil"></button>
@@ -0,0 +1,28 @@
<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>
@@ -0,0 +1,57 @@
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);
}
}
@@ -0,0 +1,19 @@
<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>
@@ -0,0 +1,41 @@
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,4 +1,8 @@
<div class="bg-surface-card w-md shrink-0 h-full overflow-hidden rounded-xl p-4 pb-0 flex flex-col"> <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"> <div class="shrink-0 sticky top-0">
<customers-select-field <customers-select-field
[canInsert]="true" [canInsert]="true"
@@ -81,17 +85,26 @@
</div> </div>
<div class="flex flex-col sticky bottom-0 py-2"> <div class="flex flex-col sticky bottom-0 py-2">
<pos-order-price-info-card /> <pos-order-price-info-card />
<div class="grid grid-cols grid-cols-2 sticky bottom-0 gap-2 pt-4"> <div class="grid grid-cols-2 sticky bottom-0 gap-2 pt-4">
<button <button
pButton pButton
type="button" type="button"
label="ثبت سفارش" label="ثبت موقت"
severity="primary" severity="primary"
icon="pi pi-check" icon="pi pi-check"
class="w-full" class="w-full"
(click)="submit()" (click)="submit()"
></button> ></button>
<button pButton type="button" label="لغو" outlined icon="pi pi-times" class="w-full"></button> <button
pButton
type="button"
label="ثبت و پرداخت"
severity="primary"
icon="pi pi-check"
class="w-full"
[attr.disabled]="true"
(click)="submitAndPay()"
></button>
</div> </div>
</div> </div>
</div> </div>
@@ -3,13 +3,14 @@ import { CustomersSelectComponent } from '@/modules/customers/components/select/
import { ICustomerResponse } from '@/modules/customers/models'; import { ICustomerResponse } from '@/modules/customers/models';
import { PriceMaskDirective } from '@/shared/directives'; import { PriceMaskDirective } from '@/shared/directives';
import { UikitCounterComponent } from '@/uikit'; import { UikitCounterComponent } from '@/uikit';
import { Component, computed, signal } from '@angular/core'; import { Component, computed, inject, signal } from '@angular/core';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { ButtonDirective } from 'primeng/button'; import { ButtonDirective } from 'primeng/button';
import { InplaceModule } from 'primeng/inplace'; import { InplaceModule } from 'primeng/inplace';
import { InputNumber } from 'primeng/inputnumber'; import { InputNumber } from 'primeng/inputnumber';
import images from 'src/assets/images'; import images from 'src/assets/images';
import { POSStore } from '../../store'; import { POSStore } from '../../store';
import { HeldOrdersComponent } from './held-orders.component';
import { POSOrderPriceInfoCardComponent } from './price-info-card.component'; import { POSOrderPriceInfoCardComponent } from './price-info-card.component';
@Component({ @Component({
@@ -24,13 +25,18 @@ import { POSOrderPriceInfoCardComponent } from './price-info-card.component';
FormsModule, FormsModule,
InplaceModule, InplaceModule,
InputNumber, InputNumber,
HeldOrdersComponent,
], ],
}) })
export class PosOrderCardComponent { export class PosOrderCardComponent {
constructor(private store: POSStore) { private store = inject(POSStore);
constructor() {
this.selectedCustomer.set(this.store.selectedCustomer()); this.selectedCustomer.set(this.store.selectedCustomer());
} }
posId = this.store.posId;
placeholderImage = images.placeholders.default; placeholderImage = images.placeholders.default;
inOrderProducts = computed(() => this.store.inOrderProducts()); inOrderProducts = computed(() => this.store.inOrderProducts());
@@ -64,4 +70,8 @@ export class PosOrderCardComponent {
submit() { submit() {
this.store.submitOrder(); this.store.submitOrder();
} }
submitAndPay() {
this.store.submitOrder();
}
} }
@@ -4,5 +4,10 @@ export const POS_API_ROUTES = {
info: (posId: number) => `${baseUrl}/${posId}`, info: (posId: number) => `${baseUrl}/${posId}`,
stock: (posId: number) => `${baseUrl}/${posId}/stock`, stock: (posId: number) => `${baseUrl}/${posId}/stock`,
productCategories: (posId: number) => `${baseUrl}/${posId}/product-categories`, productCategories: (posId: number) => `${baseUrl}/${posId}/product-categories`,
submitOrder: (posId: number) => `${baseUrl}/${posId}/orders/create`, 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`,
}; };
+36
View File
@@ -36,3 +36,39 @@ export interface IPosOrderRequest {
customerId?: number; customerId?: number;
items: IPosOrderItem[]; items: IPosOrderItem[];
} }
export interface IPosOrderRawResponse {
id: number;
orderNumber: string;
totalAmount: number;
paidAmount: number;
status: string;
description?: string;
createdAt: string;
updatedAt: string;
deletedAt?: string;
customer?: {
id: number;
firstName: string;
lastName: string;
};
orderItems: {
product: IPosProductSummary;
count: number;
unitPrice: number;
totalPrice: number;
}[];
}
export interface IPosOrderResponse extends Omit<IPosOrderRawResponse, 'customer'> {
customer?: {
id: number;
name: string;
};
}
export interface IPosHeldOrderRawResponse extends IPosOrderResponse {}
export interface IPosHeldOrderResponse extends IPosHeldOrderRawResponse {}
export interface IPosOrderConfirmedResponse {
invoiceId: number;
}
+1 -1
View File
@@ -5,7 +5,7 @@ export interface IPosProductSummary {
name: string; name: string;
sku: string; sku: string;
salePrice: string; salePrice: string;
category: ICategorySummary; categoryId: number;
} }
interface ICategorySummary { interface ICategorySummary {
@@ -43,4 +43,24 @@ export class PosService {
submitOrder(posId: number, payload: IPosOrderRequest): Observable<any> { submitOrder(posId: number, payload: IPosOrderRequest): Observable<any> {
return this.http.post<any>(this.apiRoutes.submitOrder(posId), payload); 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), {});
}
} }
+12 -3
View File
@@ -1,7 +1,7 @@
import { Maybe } from '@/core'; import { Maybe } from '@/core';
import { ICustomerResponse } from '@/modules/customers/models'; import { ICustomerResponse } from '@/modules/customers/models';
import { computed, Inject, Injectable, InjectionToken, signal } from '@angular/core'; import { computed, Inject, Injectable, InjectionToken, signal } from '@angular/core';
import { map } from 'rxjs'; import { map, of } from 'rxjs';
import { IPosInfoResponse, IPosProductCategoriesResponse, IPosStockResponse } from '../models'; import { IPosInfoResponse, IPosProductCategoriesResponse, IPosStockResponse } from '../models';
import { IPosInOrderProduct } from '../models/types'; import { IPosInOrderProduct } from '../models/types';
import { PosService } from '../services/main.service'; import { PosService } from '../services/main.service';
@@ -66,7 +66,7 @@ export class POSStore {
return this.state$().stock; return this.state$().stock;
} }
return this.state$().stock?.filter( return this.state$().stock?.filter(
(s) => s.product.category.id === this.state$().activeProductCategory, (s) => s.product.categoryId === this.state$().activeProductCategory,
); );
}); });
@@ -258,6 +258,15 @@ export class POSStore {
unitPrice: item.unitPrice, unitPrice: item.unitPrice,
})), })),
}; };
this.service.submitOrder(this.posId, orderPayload).subscribe(); return this.service.submitOrder(this.posId, orderPayload).subscribe({
next: (res) => {
this.resetInOrderProducts();
this.setSelectedCustomer(null);
return of(res);
},
error: (err) => {
return of(err);
},
});
} }
} }
@@ -1,7 +1,6 @@
import { CatalogPurchaseReceiptStatusTagComponent } from '@/shared/catalog'; import { CatalogPurchaseReceiptStatusTagComponent } from '@/shared/catalog';
import { KeyValueComponent } from '@/shared/components'; import { KeyValueComponent } from '@/shared/components';
import { InnerPagesHeaderComponent } from '@/shared/components/innerPagesHeader/inner-pages-header.component'; import { InnerPagesHeaderComponent } from '@/shared/components/innerPagesHeader/inner-pages-header.component';
import { PurchaseReceiptPaymentWrapperComponent } from '@/shared/components/purchaseReceiptPayment/wrapper.component';
import { PriceMaskDirective } from '@/shared/directives'; import { PriceMaskDirective } from '@/shared/directives';
import { Component, computed, inject, signal } from '@angular/core'; import { Component, computed, inject, signal } from '@angular/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
@@ -21,7 +20,6 @@ import { SupplierInvoiceStore, SupplierStore } from '../store';
SupplierInvoicePayFormComponent, SupplierInvoicePayFormComponent,
InvoicePaymentsComponent, InvoicePaymentsComponent,
PriceMaskDirective, PriceMaskDirective,
PurchaseReceiptPaymentWrapperComponent,
], ],
}) })
export class SupplierInvoiceComponent { export class SupplierInvoiceComponent {