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,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -2,5 +2,6 @@ export * from './maybe.model';
|
||||
export * from './namedRoutes.model';
|
||||
export * from './navigation.model';
|
||||
export * from './route-utils.model';
|
||||
export * from './summary';
|
||||
export * from './tag';
|
||||
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 { Card } from 'primeng/card';
|
||||
import { ConfirmDialog } from 'primeng/confirmdialog';
|
||||
import { InventoryPosAccountFormComponent } from '../../components/posAccounts/form.component';
|
||||
import { IInventoryBankAccountResponse } from '../../models/bankAccounts.io';
|
||||
import { InventoryBankAccountsService } from '../../services';
|
||||
import { InventoryCreateBankAccountFormComponent } from './create-bank-account-form.component';
|
||||
@@ -22,7 +21,6 @@ import { InventoryBankAccountFormComponent } from './form.component';
|
||||
Card,
|
||||
ButtonDirective,
|
||||
ConfirmDialog,
|
||||
InventoryPosAccountFormComponent,
|
||||
InventoryBankAccountFormComponent,
|
||||
InventoryCreateBankAccountFormComponent,
|
||||
],
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<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' : '')
|
||||
(selectedCategory() === category.id ? ' bg-surface-card' : '')
|
||||
"
|
||||
(click)="changeSelectedCategory(category)"
|
||||
>
|
||||
@@ -16,7 +16,7 @@
|
||||
<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' : '')
|
||||
(selectedCategory() === category.id ? ' bg-amber-400! text-white' : '')
|
||||
"
|
||||
>
|
||||
<span>
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { Maybe } from '@/core';
|
||||
import { Component, EventEmitter, Output, signal } from '@angular/core';
|
||||
import { Component, EventEmitter, inject, Output } from '@angular/core';
|
||||
import { Skeleton } from 'primeng/skeleton';
|
||||
import { map } from 'rxjs';
|
||||
import { IPosProductCategoriesResponse } from '../../models';
|
||||
import { PosService } from '../../services/main.service';
|
||||
import { POSStore } from '../../store';
|
||||
|
||||
@Component({
|
||||
selector: 'pos-product-categories',
|
||||
@@ -13,51 +10,14 @@ import { PosService } from '../../services/main.service';
|
||||
export class PosProductCategoriesComponent {
|
||||
@Output() categoryChange = new EventEmitter<number>();
|
||||
|
||||
constructor(private service: PosService) {
|
||||
this.getCategories();
|
||||
}
|
||||
private readonly store = inject(POSStore);
|
||||
|
||||
categories = signal<Maybe<IPosProductCategoriesResponse[]>>(null);
|
||||
loading = signal(true);
|
||||
categories = this.store.productCategories;
|
||||
loading = this.store.getProductCategoriesLoading;
|
||||
|
||||
selectedCategory = signal<Maybe<IPosProductCategoriesResponse>>(null);
|
||||
selectedCategory = this.store.activeProductCategory;
|
||||
|
||||
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);
|
||||
},
|
||||
});
|
||||
changeSelectedCategory(category: { id: number; name: string }) {
|
||||
this.store.changeActiveCategory(category.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
<span class="text-base font-bold">لیست کالاها</span>
|
||||
</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>
|
||||
<pos-product-categories (categoryChange)="onCategoryChange($event)" />
|
||||
|
||||
@if (viewType === "grid") {
|
||||
<pos-products-grid-view />
|
||||
} @else {
|
||||
<pos-products-list-view />
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -1,25 +1,54 @@
|
||||
import { Maybe } from '@/core';
|
||||
import { Component, computed, Input, signal } from '@angular/core';
|
||||
import { ButtonDirective } from 'primeng/button';
|
||||
import { POSStore } from '../../store';
|
||||
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, ButtonDirective, PosProductsGridViewComponent],
|
||||
imports: [
|
||||
PosProductCategoriesComponent,
|
||||
PosProductsListViewComponent,
|
||||
SelectButton,
|
||||
FormsModule,
|
||||
PosProductsGridViewComponent,
|
||||
SearchInputComponent,
|
||||
],
|
||||
})
|
||||
export class PosProductsComponent {
|
||||
@Input() activeCategory = signal<Maybe<number>>(null);
|
||||
|
||||
constructor(private store: POSStore) {
|
||||
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,8 +1,8 @@
|
||||
const baseUrl = '/api/v1/pos';
|
||||
|
||||
export const POS_API_ROUTES = {
|
||||
info: () => `${baseUrl}`,
|
||||
stock: () => `${baseUrl}/stock`,
|
||||
productCategories: () => `${baseUrl}/product-categories`,
|
||||
submitOrder: () => `${baseUrl}/orders/create`,
|
||||
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`,
|
||||
};
|
||||
|
||||
@@ -5,11 +5,11 @@ export type TPOSRouteNames = 'POS';
|
||||
|
||||
export const posNamedRoutes: NamedRoutes<TPOSRouteNames> = {
|
||||
POS: {
|
||||
path: 'pos',
|
||||
path: 'pos/:posId',
|
||||
loadComponent: () => import('../../views/pos.component').then((m) => m.POSComponent),
|
||||
meta: {
|
||||
title: 'نقطه فروش',
|
||||
pagePath: () => '/pos',
|
||||
title: 'نقاط فروش',
|
||||
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 {
|
||||
store: {
|
||||
id: number;
|
||||
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 {}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import ISummary from '@/core/models/summary';
|
||||
|
||||
export interface IPosProductSummary {
|
||||
id: number;
|
||||
name: string;
|
||||
@@ -21,3 +23,14 @@ 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;
|
||||
}
|
||||
|
||||
@@ -19,21 +19,28 @@ export class PosService {
|
||||
|
||||
private apiRoutes = POS_API_ROUTES;
|
||||
|
||||
getInfo(): Observable<IPosInfoResponse> {
|
||||
return this.http.get<IPosInfoRawResponse>(this.apiRoutes.info());
|
||||
getInfo(posId: number): Observable<IPosInfoResponse> {
|
||||
return this.http.get<IPosInfoRawResponse>(this.apiRoutes.info(posId));
|
||||
}
|
||||
|
||||
getStock(): Observable<IPaginatedResponse<IPosStockResponse>> {
|
||||
return this.http.get<IPaginatedResponse<IPosStockRawResponse>>(this.apiRoutes.stock());
|
||||
getStock(
|
||||
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>>(
|
||||
this.apiRoutes.productCategories(),
|
||||
this.apiRoutes.productCategories(posId),
|
||||
);
|
||||
}
|
||||
|
||||
submitOrder(payload: IPosOrderRequest): Observable<any> {
|
||||
return this.http.post<any>(this.apiRoutes.submitOrder(), payload);
|
||||
submitOrder(posId: number, payload: IPosOrderRequest): Observable<any> {
|
||||
return this.http.post<any>(this.apiRoutes.submitOrder(posId), payload);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { Maybe } from '@/core';
|
||||
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 { 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[]>;
|
||||
@@ -16,6 +20,8 @@ interface IPosState {
|
||||
activeProductCategory: Maybe<number>;
|
||||
inOrderProducts: IPosInOrderProduct[];
|
||||
selectedCustomer: Maybe<ICustomerResponse>;
|
||||
viewType: TViewType;
|
||||
searchQuery: string;
|
||||
}
|
||||
|
||||
export const INITIAL_POS_STATE: IPosState = {
|
||||
@@ -28,11 +34,19 @@ export const INITIAL_POS_STATE: IPosState = {
|
||||
activeProductCategory: null,
|
||||
inOrderProducts: [],
|
||||
selectedCustomer: null,
|
||||
viewType: 'grid',
|
||||
searchQuery: '',
|
||||
};
|
||||
|
||||
@Injectable({ providedIn: 'any' })
|
||||
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 });
|
||||
|
||||
@@ -45,6 +59,8 @@ export class POSStore {
|
||||
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;
|
||||
@@ -72,6 +88,8 @@ export class POSStore {
|
||||
};
|
||||
});
|
||||
|
||||
posId!: number;
|
||||
|
||||
setState(partial: Partial<IPosState>) {
|
||||
this.state$.set({ ...this.state$(), ...partial });
|
||||
}
|
||||
@@ -92,7 +110,7 @@ export class POSStore {
|
||||
|
||||
getInfo() {
|
||||
this.setState({ getInfoLoading: true });
|
||||
this.service.getInfo().subscribe({
|
||||
this.service.getInfo(this.posId).subscribe({
|
||||
next: (res) => {
|
||||
this.setState({ info: res, getInfoLoading: false });
|
||||
},
|
||||
@@ -104,7 +122,7 @@ export class POSStore {
|
||||
|
||||
getStock() {
|
||||
this.setState({ getStockLoading: true });
|
||||
this.service.getStock().subscribe({
|
||||
this.service.getStock(this.posId, this.state$().searchQuery).subscribe({
|
||||
next: (res) => {
|
||||
this.setState({ stock: res.data, getStockLoading: false });
|
||||
},
|
||||
@@ -117,7 +135,7 @@ export class POSStore {
|
||||
getProductCategories() {
|
||||
this.setState({ getProductCategoriesLoading: true });
|
||||
this.service
|
||||
.getCategories()
|
||||
.getCategories(this.posId)
|
||||
.pipe(
|
||||
map((res) => {
|
||||
return {
|
||||
@@ -216,6 +234,13 @@ export class POSStore {
|
||||
}
|
||||
}
|
||||
|
||||
updateViewType(viewType: TViewType) {
|
||||
this.setState({ viewType });
|
||||
}
|
||||
updateSearchQuery(searchQuery: string) {
|
||||
this.setState({ searchQuery });
|
||||
}
|
||||
|
||||
resetInOrderProducts() {
|
||||
this.setState({ inOrderProducts: [] });
|
||||
}
|
||||
@@ -233,6 +258,6 @@ export class POSStore {
|
||||
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">
|
||||
<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>
|
||||
<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">
|
||||
|
||||
@@ -1,40 +1,44 @@
|
||||
import { Maybe } from '@/core';
|
||||
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 { PosOrderCardComponent } from '../components/order/order-card.component';
|
||||
import { PosProductsComponent } from '../components/products/products.component';
|
||||
import { IPosInfoResponse } from '../models';
|
||||
import { PosService } from '../services/main.service';
|
||||
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 {
|
||||
constructor(private service: PosService) {
|
||||
this.getInfo();
|
||||
}
|
||||
private readonly store = inject(POSStore);
|
||||
|
||||
placeholderLogo = images.placeholders.logo;
|
||||
|
||||
now = new Date();
|
||||
inventoryId = 2;
|
||||
|
||||
info = signal<Maybe<IPosInfoResponse>>(null);
|
||||
infoLoading = signal(true);
|
||||
info = this.store.info;
|
||||
infoLoading = this.store.getInfoLoading;
|
||||
|
||||
getInfo() {
|
||||
this.infoLoading.set(true);
|
||||
this.service.getInfo().subscribe({
|
||||
next: (res) => {
|
||||
this.info.set(res);
|
||||
this.infoLoading.set(false);
|
||||
},
|
||||
error: () => {
|
||||
this.infoLoading.set(false);
|
||||
},
|
||||
});
|
||||
}
|
||||
// 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);
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -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