feat: enhance POS module with dynamic routing, improved state management, and new search functionality
This commit is contained in:
+1
-1
@@ -40,7 +40,7 @@ export const appRoutes: Routes = [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'pos',
|
path: 'pos/:posId',
|
||||||
component: POSComponent,
|
component: POSComponent,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,5 +2,6 @@ export * from './maybe.model';
|
|||||||
export * from './namedRoutes.model';
|
export * from './namedRoutes.model';
|
||||||
export * from './navigation.model';
|
export * from './navigation.model';
|
||||||
export * from './route-utils.model';
|
export * from './route-utils.model';
|
||||||
|
export * from './summary';
|
||||||
export * from './tag';
|
export * from './tag';
|
||||||
export * from './user.model';
|
export * from './user.model';
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export default interface ISummary {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
@@ -8,7 +8,6 @@ import { ConfirmationService, MessageService } from 'primeng/api';
|
|||||||
import { ButtonDirective } from 'primeng/button';
|
import { ButtonDirective } from 'primeng/button';
|
||||||
import { Card } from 'primeng/card';
|
import { Card } from 'primeng/card';
|
||||||
import { ConfirmDialog } from 'primeng/confirmdialog';
|
import { ConfirmDialog } from 'primeng/confirmdialog';
|
||||||
import { InventoryPosAccountFormComponent } from '../../components/posAccounts/form.component';
|
|
||||||
import { IInventoryBankAccountResponse } from '../../models/bankAccounts.io';
|
import { IInventoryBankAccountResponse } from '../../models/bankAccounts.io';
|
||||||
import { InventoryBankAccountsService } from '../../services';
|
import { InventoryBankAccountsService } from '../../services';
|
||||||
import { InventoryCreateBankAccountFormComponent } from './create-bank-account-form.component';
|
import { InventoryCreateBankAccountFormComponent } from './create-bank-account-form.component';
|
||||||
@@ -22,7 +21,6 @@ import { InventoryBankAccountFormComponent } from './form.component';
|
|||||||
Card,
|
Card,
|
||||||
ButtonDirective,
|
ButtonDirective,
|
||||||
ConfirmDialog,
|
ConfirmDialog,
|
||||||
InventoryPosAccountFormComponent,
|
|
||||||
InventoryBankAccountFormComponent,
|
InventoryBankAccountFormComponent,
|
||||||
InventoryCreateBankAccountFormComponent,
|
InventoryCreateBankAccountFormComponent,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<div
|
<div
|
||||||
[class]="
|
[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' +
|
'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' : '')
|
(selectedCategory() === category.id ? ' bg-surface-card' : '')
|
||||||
"
|
"
|
||||||
(click)="changeSelectedCategory(category)"
|
(click)="changeSelectedCategory(category)"
|
||||||
>
|
>
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
<div
|
<div
|
||||||
[class]="
|
[class]="
|
||||||
'flex items-center justify-center py-0.5 px-2 rounded-sm bg-surface-300 text-xs text-muted-color' +
|
'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' : '')
|
(selectedCategory() === category.id ? ' bg-amber-400! text-white' : '')
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import { Maybe } from '@/core';
|
import { Component, EventEmitter, inject, Output } from '@angular/core';
|
||||||
import { Component, EventEmitter, Output, signal } from '@angular/core';
|
|
||||||
import { Skeleton } from 'primeng/skeleton';
|
import { Skeleton } from 'primeng/skeleton';
|
||||||
import { map } from 'rxjs';
|
import { POSStore } from '../../store';
|
||||||
import { IPosProductCategoriesResponse } from '../../models';
|
|
||||||
import { PosService } from '../../services/main.service';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pos-product-categories',
|
selector: 'pos-product-categories',
|
||||||
@@ -13,51 +10,14 @@ import { PosService } from '../../services/main.service';
|
|||||||
export class PosProductCategoriesComponent {
|
export class PosProductCategoriesComponent {
|
||||||
@Output() categoryChange = new EventEmitter<number>();
|
@Output() categoryChange = new EventEmitter<number>();
|
||||||
|
|
||||||
constructor(private service: PosService) {
|
private readonly store = inject(POSStore);
|
||||||
this.getCategories();
|
|
||||||
}
|
|
||||||
|
|
||||||
categories = signal<Maybe<IPosProductCategoriesResponse[]>>(null);
|
categories = this.store.productCategories;
|
||||||
loading = signal(true);
|
loading = this.store.getProductCategoriesLoading;
|
||||||
|
|
||||||
selectedCategory = signal<Maybe<IPosProductCategoriesResponse>>(null);
|
selectedCategory = this.store.activeProductCategory;
|
||||||
|
|
||||||
changeSelectedCategory(category: IPosProductCategoriesResponse) {
|
changeSelectedCategory(category: { id: number; name: string }) {
|
||||||
if (category.id !== this.selectedCategory()?.id) {
|
this.store.changeActiveCategory(category.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,30 @@
|
|||||||
|
<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>
|
||||||
@@ -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-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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,9 +4,23 @@
|
|||||||
<i class="pi pi-list"></i>
|
<i class="pi pi-list"></i>
|
||||||
<span class="text-base font-bold">لیست کالاها</span>
|
<span class="text-base font-bold">لیست کالاها</span>
|
||||||
</div>
|
</div>
|
||||||
<button pButton icon="pi pi-search"></button>
|
<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>
|
</div>
|
||||||
<pos-product-categories (categoryChange)="onCategoryChange($event)" />
|
<pos-product-categories (categoryChange)="onCategoryChange($event)" />
|
||||||
|
|
||||||
<pos-products-grid-view />
|
@if (viewType === "grid") {
|
||||||
|
<pos-products-grid-view />
|
||||||
|
} @else {
|
||||||
|
<pos-products-list-view />
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,25 +1,54 @@
|
|||||||
import { Maybe } from '@/core';
|
import { Maybe } from '@/core';
|
||||||
import { Component, computed, Input, signal } from '@angular/core';
|
import { SearchInputComponent } from '@/shared/components/search/search-input.component';
|
||||||
import { ButtonDirective } from 'primeng/button';
|
import { Component, computed, inject, Input, signal } from '@angular/core';
|
||||||
import { POSStore } from '../../store';
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { SelectButton } from 'primeng/selectbutton';
|
||||||
|
import { POSStore, TViewType } from '../../store';
|
||||||
import { PosProductCategoriesComponent } from './categories.component';
|
import { PosProductCategoriesComponent } from './categories.component';
|
||||||
import { PosProductsGridViewComponent } from './grid-view.component';
|
import { PosProductsGridViewComponent } from './grid-view.component';
|
||||||
|
import { PosProductsListViewComponent } from './list-view.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pos-products',
|
selector: 'pos-products',
|
||||||
templateUrl: './products.component.html',
|
templateUrl: './products.component.html',
|
||||||
imports: [PosProductCategoriesComponent, ButtonDirective, PosProductsGridViewComponent],
|
imports: [
|
||||||
|
PosProductCategoriesComponent,
|
||||||
|
PosProductsListViewComponent,
|
||||||
|
SelectButton,
|
||||||
|
FormsModule,
|
||||||
|
PosProductsGridViewComponent,
|
||||||
|
SearchInputComponent,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class PosProductsComponent {
|
export class PosProductsComponent {
|
||||||
@Input() activeCategory = signal<Maybe<number>>(null);
|
@Input() activeCategory = signal<Maybe<number>>(null);
|
||||||
|
|
||||||
constructor(private store: POSStore) {
|
private readonly store = inject(POSStore);
|
||||||
|
|
||||||
|
constructor() {
|
||||||
this.store.initial();
|
this.store.initial();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
viewOptions = [
|
||||||
|
{ value: 'list', icon: 'pi pi-list' },
|
||||||
|
{ value: 'grid', icon: 'pi pi-th-large' },
|
||||||
|
];
|
||||||
|
|
||||||
readonly activatedCategoryProducts = computed(() => this.store.activatedCategoryProducts());
|
readonly activatedCategoryProducts = computed(() => this.store.activatedCategoryProducts());
|
||||||
readonly stock = computed(() => this.store.stock());
|
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) {
|
onCategoryChange(categoryId: number) {
|
||||||
this.store.changeActiveCategory(categoryId);
|
this.store.changeActiveCategory(categoryId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
const baseUrl = '/api/v1/pos';
|
const baseUrl = '/api/v1/pos';
|
||||||
|
|
||||||
export const POS_API_ROUTES = {
|
export const POS_API_ROUTES = {
|
||||||
info: () => `${baseUrl}`,
|
info: (posId: number) => `${baseUrl}/${posId}`,
|
||||||
stock: () => `${baseUrl}/stock`,
|
stock: (posId: number) => `${baseUrl}/${posId}/stock`,
|
||||||
productCategories: () => `${baseUrl}/product-categories`,
|
productCategories: (posId: number) => `${baseUrl}/${posId}/product-categories`,
|
||||||
submitOrder: () => `${baseUrl}/orders/create`,
|
submitOrder: (posId: number) => `${baseUrl}/${posId}/orders/create`,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ export type TPOSRouteNames = 'POS';
|
|||||||
|
|
||||||
export const posNamedRoutes: NamedRoutes<TPOSRouteNames> = {
|
export const posNamedRoutes: NamedRoutes<TPOSRouteNames> = {
|
||||||
POS: {
|
POS: {
|
||||||
path: 'pos',
|
path: 'pos/:posId',
|
||||||
loadComponent: () => import('../../views/pos.component').then((m) => m.POSComponent),
|
loadComponent: () => import('../../views/pos.component').then((m) => m.POSComponent),
|
||||||
meta: {
|
meta: {
|
||||||
title: 'نقطه فروش',
|
title: 'نقاط فروش',
|
||||||
pagePath: () => '/pos',
|
pagePath: (params) => `/pos/${params.posId}`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,18 @@
|
|||||||
import { IPosOrderItem, IPosProductSummary } from './types';
|
import ISummary from '@/core/models/summary';
|
||||||
|
import { IBankAccountSummary, IPosOrderItem, IPosProductSummary } from './types';
|
||||||
|
|
||||||
export interface IPosInfoRawResponse {
|
export interface IPosInfoRawResponse {
|
||||||
store: {
|
id: number;
|
||||||
id: number;
|
name: string;
|
||||||
name: string;
|
code: string;
|
||||||
};
|
description: string;
|
||||||
|
bankAccountId: number;
|
||||||
|
inventoryId: number;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
deletedAt: null;
|
||||||
|
inventory: ISummary;
|
||||||
|
bankAccount: IBankAccountSummary;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IPosInfoResponse extends IPosInfoRawResponse {}
|
export interface IPosInfoResponse extends IPosInfoRawResponse {}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import ISummary from '@/core/models/summary';
|
||||||
|
|
||||||
export interface IPosProductSummary {
|
export interface IPosProductSummary {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -21,3 +23,14 @@ export interface IPosInOrderProduct extends IPosOrderItem {
|
|||||||
product: IPosProductSummary;
|
product: IPosProductSummary;
|
||||||
maxQuantity: number;
|
maxQuantity: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IBankAccountSummary {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
accountNumber: string;
|
||||||
|
branch: IBankAccountBranchSummary;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IBankAccountBranchSummary extends ISummary {
|
||||||
|
bank: ISummary;
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,21 +19,28 @@ export class PosService {
|
|||||||
|
|
||||||
private apiRoutes = POS_API_ROUTES;
|
private apiRoutes = POS_API_ROUTES;
|
||||||
|
|
||||||
getInfo(): Observable<IPosInfoResponse> {
|
getInfo(posId: number): Observable<IPosInfoResponse> {
|
||||||
return this.http.get<IPosInfoRawResponse>(this.apiRoutes.info());
|
return this.http.get<IPosInfoRawResponse>(this.apiRoutes.info(posId));
|
||||||
}
|
}
|
||||||
|
|
||||||
getStock(): Observable<IPaginatedResponse<IPosStockResponse>> {
|
getStock(
|
||||||
return this.http.get<IPaginatedResponse<IPosStockRawResponse>>(this.apiRoutes.stock());
|
posId: number,
|
||||||
|
searchQuery: string = '',
|
||||||
|
): Observable<IPaginatedResponse<IPosStockResponse>> {
|
||||||
|
return this.http.get<IPaginatedResponse<IPosStockRawResponse>>(this.apiRoutes.stock(posId), {
|
||||||
|
params: {
|
||||||
|
q: searchQuery,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getCategories(): Observable<IPaginatedResponse<IPosProductCategoriesResponse>> {
|
getCategories(posId: number): Observable<IPaginatedResponse<IPosProductCategoriesResponse>> {
|
||||||
return this.http.get<IPaginatedResponse<IPosProductCategoriesRawResponse>>(
|
return this.http.get<IPaginatedResponse<IPosProductCategoriesRawResponse>>(
|
||||||
this.apiRoutes.productCategories(),
|
this.apiRoutes.productCategories(posId),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
submitOrder(payload: IPosOrderRequest): Observable<any> {
|
submitOrder(posId: number, payload: IPosOrderRequest): Observable<any> {
|
||||||
return this.http.post<any>(this.apiRoutes.submitOrder(), payload);
|
return this.http.post<any>(this.apiRoutes.submitOrder(posId), payload);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
import { Maybe } from '@/core';
|
import { Maybe } from '@/core';
|
||||||
import { ICustomerResponse } from '@/modules/customers/models';
|
import { ICustomerResponse } from '@/modules/customers/models';
|
||||||
import { computed, Injectable, signal } from '@angular/core';
|
import { computed, Inject, Injectable, InjectionToken, signal } from '@angular/core';
|
||||||
import { map } from 'rxjs';
|
import { map } 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';
|
||||||
|
|
||||||
|
export const POS_ID = new InjectionToken<number>('POS_ID');
|
||||||
|
|
||||||
|
export type TViewType = 'list' | 'grid';
|
||||||
|
|
||||||
interface IPosState {
|
interface IPosState {
|
||||||
getStockLoading: boolean;
|
getStockLoading: boolean;
|
||||||
stock: Maybe<IPosStockResponse[]>;
|
stock: Maybe<IPosStockResponse[]>;
|
||||||
@@ -16,6 +20,8 @@ interface IPosState {
|
|||||||
activeProductCategory: Maybe<number>;
|
activeProductCategory: Maybe<number>;
|
||||||
inOrderProducts: IPosInOrderProduct[];
|
inOrderProducts: IPosInOrderProduct[];
|
||||||
selectedCustomer: Maybe<ICustomerResponse>;
|
selectedCustomer: Maybe<ICustomerResponse>;
|
||||||
|
viewType: TViewType;
|
||||||
|
searchQuery: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const INITIAL_POS_STATE: IPosState = {
|
export const INITIAL_POS_STATE: IPosState = {
|
||||||
@@ -28,11 +34,19 @@ export const INITIAL_POS_STATE: IPosState = {
|
|||||||
activeProductCategory: null,
|
activeProductCategory: null,
|
||||||
inOrderProducts: [],
|
inOrderProducts: [],
|
||||||
selectedCustomer: null,
|
selectedCustomer: null,
|
||||||
|
viewType: 'grid',
|
||||||
|
searchQuery: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
@Injectable({ providedIn: 'any' })
|
@Injectable({ providedIn: 'any' })
|
||||||
export class POSStore {
|
export class POSStore {
|
||||||
constructor(private service: PosService) {}
|
constructor(
|
||||||
|
@Inject(POS_ID) posId: number,
|
||||||
|
private service: PosService,
|
||||||
|
) {
|
||||||
|
this.posId = posId;
|
||||||
|
this.initial();
|
||||||
|
}
|
||||||
|
|
||||||
private state$ = signal<IPosState>({ ...INITIAL_POS_STATE });
|
private state$ = signal<IPosState>({ ...INITIAL_POS_STATE });
|
||||||
|
|
||||||
@@ -45,6 +59,8 @@ export class POSStore {
|
|||||||
readonly getProductCategoriesLoading = computed(() => this.state$().getProductCategoriesLoading);
|
readonly getProductCategoriesLoading = computed(() => this.state$().getProductCategoriesLoading);
|
||||||
readonly activeProductCategory = computed(() => this.state$().activeProductCategory);
|
readonly activeProductCategory = computed(() => this.state$().activeProductCategory);
|
||||||
readonly selectedCustomer = computed(() => this.state$().selectedCustomer);
|
readonly selectedCustomer = computed(() => this.state$().selectedCustomer);
|
||||||
|
readonly viewType = computed(() => this.state$().viewType);
|
||||||
|
readonly searchQuery = computed(() => this.state$().searchQuery);
|
||||||
readonly activatedCategoryProducts = computed(() => {
|
readonly activatedCategoryProducts = computed(() => {
|
||||||
if (this.activeProductCategory() === 0) {
|
if (this.activeProductCategory() === 0) {
|
||||||
return this.state$().stock;
|
return this.state$().stock;
|
||||||
@@ -72,6 +88,8 @@ export class POSStore {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
posId!: number;
|
||||||
|
|
||||||
setState(partial: Partial<IPosState>) {
|
setState(partial: Partial<IPosState>) {
|
||||||
this.state$.set({ ...this.state$(), ...partial });
|
this.state$.set({ ...this.state$(), ...partial });
|
||||||
}
|
}
|
||||||
@@ -92,7 +110,7 @@ export class POSStore {
|
|||||||
|
|
||||||
getInfo() {
|
getInfo() {
|
||||||
this.setState({ getInfoLoading: true });
|
this.setState({ getInfoLoading: true });
|
||||||
this.service.getInfo().subscribe({
|
this.service.getInfo(this.posId).subscribe({
|
||||||
next: (res) => {
|
next: (res) => {
|
||||||
this.setState({ info: res, getInfoLoading: false });
|
this.setState({ info: res, getInfoLoading: false });
|
||||||
},
|
},
|
||||||
@@ -104,7 +122,7 @@ export class POSStore {
|
|||||||
|
|
||||||
getStock() {
|
getStock() {
|
||||||
this.setState({ getStockLoading: true });
|
this.setState({ getStockLoading: true });
|
||||||
this.service.getStock().subscribe({
|
this.service.getStock(this.posId, this.state$().searchQuery).subscribe({
|
||||||
next: (res) => {
|
next: (res) => {
|
||||||
this.setState({ stock: res.data, getStockLoading: false });
|
this.setState({ stock: res.data, getStockLoading: false });
|
||||||
},
|
},
|
||||||
@@ -117,7 +135,7 @@ export class POSStore {
|
|||||||
getProductCategories() {
|
getProductCategories() {
|
||||||
this.setState({ getProductCategoriesLoading: true });
|
this.setState({ getProductCategoriesLoading: true });
|
||||||
this.service
|
this.service
|
||||||
.getCategories()
|
.getCategories(this.posId)
|
||||||
.pipe(
|
.pipe(
|
||||||
map((res) => {
|
map((res) => {
|
||||||
return {
|
return {
|
||||||
@@ -216,6 +234,13 @@ export class POSStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateViewType(viewType: TViewType) {
|
||||||
|
this.setState({ viewType });
|
||||||
|
}
|
||||||
|
updateSearchQuery(searchQuery: string) {
|
||||||
|
this.setState({ searchQuery });
|
||||||
|
}
|
||||||
|
|
||||||
resetInOrderProducts() {
|
resetInOrderProducts() {
|
||||||
this.setState({ inOrderProducts: [] });
|
this.setState({ inOrderProducts: [] });
|
||||||
}
|
}
|
||||||
@@ -233,6 +258,6 @@ export class POSStore {
|
|||||||
fee: item.fee,
|
fee: item.fee,
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
this.service.submitOrder(orderPayload).subscribe();
|
this.service.submitOrder(this.posId, orderPayload).subscribe();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<div class="w-10 h-10">
|
<div class="w-10 h-10">
|
||||||
<img [src]="placeholderLogo" alt="Logo" class="w-full h-full object-cover rounded-md bg-surface-card" />
|
<img [src]="placeholderLogo" alt="Logo" class="w-full h-full object-cover rounded-md bg-surface-card" />
|
||||||
</div>
|
</div>
|
||||||
<span class="text-lg font-bold">{{ info()?.store?.name }}</span>
|
<span class="text-lg font-bold">{{ info()?.name }} - فروشگاه {{ info()?.inventory?.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2 items-center">
|
<div class="flex gap-2 items-center">
|
||||||
<div class="bg-surface-card py-1 px-3 rounded-md shadow-sm">
|
<div class="bg-surface-card py-1 px-3 rounded-md shadow-sm">
|
||||||
|
|||||||
@@ -1,40 +1,44 @@
|
|||||||
import { Maybe } from '@/core';
|
|
||||||
import { JalaliDateDirective } from '@/shared/directives';
|
import { JalaliDateDirective } from '@/shared/directives';
|
||||||
import { Component, signal } from '@angular/core';
|
import { Component, inject } from '@angular/core';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import images from 'src/assets/images';
|
import images from 'src/assets/images';
|
||||||
import { PosOrderCardComponent } from '../components/order/order-card.component';
|
import { PosOrderCardComponent } from '../components/order/order-card.component';
|
||||||
import { PosProductsComponent } from '../components/products/products.component';
|
import { PosProductsComponent } from '../components/products/products.component';
|
||||||
import { IPosInfoResponse } from '../models';
|
import { POSStore, POS_ID } from '../store';
|
||||||
import { PosService } from '../services/main.service';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-pos',
|
selector: 'app-pos',
|
||||||
templateUrl: './pos.component.html',
|
templateUrl: './pos.component.html',
|
||||||
imports: [JalaliDateDirective, PosProductsComponent, PosOrderCardComponent],
|
imports: [JalaliDateDirective, PosProductsComponent, PosOrderCardComponent],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: POS_ID,
|
||||||
|
useFactory: (route: ActivatedRoute) => Number(route.snapshot.params['posId']),
|
||||||
|
deps: [ActivatedRoute],
|
||||||
|
},
|
||||||
|
POSStore,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class POSComponent {
|
export class POSComponent {
|
||||||
constructor(private service: PosService) {
|
private readonly store = inject(POSStore);
|
||||||
this.getInfo();
|
|
||||||
}
|
|
||||||
|
|
||||||
placeholderLogo = images.placeholders.logo;
|
placeholderLogo = images.placeholders.logo;
|
||||||
|
|
||||||
now = new Date();
|
now = new Date();
|
||||||
inventoryId = 2;
|
|
||||||
|
|
||||||
info = signal<Maybe<IPosInfoResponse>>(null);
|
info = this.store.info;
|
||||||
infoLoading = signal(true);
|
infoLoading = this.store.getInfoLoading;
|
||||||
|
|
||||||
getInfo() {
|
// getInfo() {
|
||||||
this.infoLoading.set(true);
|
// this.infoLoading.set(true);
|
||||||
this.service.getInfo().subscribe({
|
// this.service.getInfo(Number(this.posId)).subscribe({
|
||||||
next: (res) => {
|
// next: (res) => {
|
||||||
this.info.set(res);
|
// this.info.set(res);
|
||||||
this.infoLoading.set(false);
|
// this.infoLoading.set(false);
|
||||||
},
|
// },
|
||||||
error: () => {
|
// error: () => {
|
||||||
this.infoLoading.set(false);
|
// this.infoLoading.set(false);
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<input pInputText type="text" placeholder="جستجو..." (input)="onSearchInput($event.target.value)" />
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core';
|
||||||
|
import { InputText } from 'primeng/inputtext';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
import { debounceTime, takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-search-input',
|
||||||
|
templateUrl: 'search-input.component.html',
|
||||||
|
imports: [InputText],
|
||||||
|
})
|
||||||
|
export class SearchInputComponent implements OnInit, OnDestroy {
|
||||||
|
private searchSubject = new Subject<string>();
|
||||||
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
|
@Output() search = new EventEmitter<string>();
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.searchSubject.pipe(debounceTime(300), takeUntil(this.destroy$)).subscribe((searchTerm) => {
|
||||||
|
this.onSearch(searchTerm);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearchInput(value: string) {
|
||||||
|
this.searchSubject.next(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearch(searchTerm: string) {
|
||||||
|
console.log('Search:', searchTerm);
|
||||||
|
this.search.emit(searchTerm);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user