diff --git a/src/app.routes.ts b/src/app.routes.ts index e7bb518..794e96c 100644 --- a/src/app.routes.ts +++ b/src/app.routes.ts @@ -40,7 +40,7 @@ export const appRoutes: Routes = [ ], }, { - path: 'pos', + path: 'pos/:posId', component: POSComponent, }, { diff --git a/src/app/core/models/index.ts b/src/app/core/models/index.ts index ccd0e59..cfe0058 100644 --- a/src/app/core/models/index.ts +++ b/src/app/core/models/index.ts @@ -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'; diff --git a/src/app/core/models/summary.ts b/src/app/core/models/summary.ts new file mode 100644 index 0000000..5818ed0 --- /dev/null +++ b/src/app/core/models/summary.ts @@ -0,0 +1,4 @@ +export default interface ISummary { + id: number; + name: string; +} diff --git a/src/app/modules/inventories/components/bankAccounts/list.component.ts b/src/app/modules/inventories/components/bankAccounts/list.component.ts index 36c4d28..204152d 100644 --- a/src/app/modules/inventories/components/bankAccounts/list.component.ts +++ b/src/app/modules/inventories/components/bankAccounts/list.component.ts @@ -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, ], diff --git a/src/app/modules/pos/components/products/categories.component.html b/src/app/modules/pos/components/products/categories.component.html index cfd8ae0..6e6b901 100644 --- a/src/app/modules/pos/components/products/categories.component.html +++ b/src/app/modules/pos/components/products/categories.component.html @@ -8,7 +8,7 @@
@@ -16,7 +16,7 @@
diff --git a/src/app/modules/pos/components/products/categories.component.ts b/src/app/modules/pos/components/products/categories.component.ts index 2dda60d..f8d28be 100644 --- a/src/app/modules/pos/components/products/categories.component.ts +++ b/src/app/modules/pos/components/products/categories.component.ts @@ -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(); - constructor(private service: PosService) { - this.getCategories(); - } + private readonly store = inject(POSStore); - categories = signal>(null); - loading = signal(true); + categories = this.store.productCategories; + loading = this.store.getProductCategoriesLoading; - selectedCategory = signal>(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); } } diff --git a/src/app/modules/pos/components/products/list-view.component.html b/src/app/modules/pos/components/products/list-view.component.html new file mode 100644 index 0000000..a842ccf --- /dev/null +++ b/src/app/modules/pos/components/products/list-view.component.html @@ -0,0 +1,30 @@ +
+ @if (loading()) { + @for (_ of [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15]; track $index) { +
+ +
+ } + } @else { + @for (product of products(); track product.id) { +
+
+ +
+ + {{ product.product.name }} + ({{ product.quantity }}) + + + {{ product.product.sku }} + +
+
+
+ + +
+
+ } + } +
diff --git a/src/app/modules/pos/components/products/list-view.component.ts b/src/app/modules/pos/components/products/list-view.component.ts new file mode 100644 index 0000000..b28bbd4 --- /dev/null +++ b/src/app/modules/pos/components/products/list-view.component.ts @@ -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); + } +} diff --git a/src/app/modules/pos/components/products/products.component.html b/src/app/modules/pos/components/products/products.component.html index ed838e8..94a88ad 100644 --- a/src/app/modules/pos/components/products/products.component.html +++ b/src/app/modules/pos/components/products/products.component.html @@ -4,9 +4,23 @@ لیست کالاها
- +
+ + +
+ + + + + +
+
- + @if (viewType === "grid") { + + } @else { + + } diff --git a/src/app/modules/pos/components/products/products.component.ts b/src/app/modules/pos/components/products/products.component.ts index cc156ff..5c7fdbe 100644 --- a/src/app/modules/pos/components/products/products.component.ts +++ b/src/app/modules/pos/components/products/products.component.ts @@ -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>(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); } diff --git a/src/app/modules/pos/constants/apiRoutes/index.ts b/src/app/modules/pos/constants/apiRoutes/index.ts index 47ea822..475439c 100644 --- a/src/app/modules/pos/constants/apiRoutes/index.ts +++ b/src/app/modules/pos/constants/apiRoutes/index.ts @@ -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`, }; diff --git a/src/app/modules/pos/constants/routes/index.ts b/src/app/modules/pos/constants/routes/index.ts index fa20cbc..b2e75f1 100644 --- a/src/app/modules/pos/constants/routes/index.ts +++ b/src/app/modules/pos/constants/routes/index.ts @@ -5,11 +5,11 @@ export type TPOSRouteNames = 'POS'; export const posNamedRoutes: NamedRoutes = { 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}`, }, }, }; diff --git a/src/app/modules/pos/models/io.ts b/src/app/modules/pos/models/io.ts index 7d6a51a..ff265ec 100644 --- a/src/app/modules/pos/models/io.ts +++ b/src/app/modules/pos/models/io.ts @@ -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; - }; + 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 {} diff --git a/src/app/modules/pos/models/types.ts b/src/app/modules/pos/models/types.ts index d177780..c0abc75 100644 --- a/src/app/modules/pos/models/types.ts +++ b/src/app/modules/pos/models/types.ts @@ -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; +} diff --git a/src/app/modules/pos/services/main.service.ts b/src/app/modules/pos/services/main.service.ts index dd824ef..06d464b 100644 --- a/src/app/modules/pos/services/main.service.ts +++ b/src/app/modules/pos/services/main.service.ts @@ -19,21 +19,28 @@ export class PosService { private apiRoutes = POS_API_ROUTES; - getInfo(): Observable { - return this.http.get(this.apiRoutes.info()); + getInfo(posId: number): Observable { + return this.http.get(this.apiRoutes.info(posId)); } - getStock(): Observable> { - return this.http.get>(this.apiRoutes.stock()); + getStock( + posId: number, + searchQuery: string = '', + ): Observable> { + return this.http.get>(this.apiRoutes.stock(posId), { + params: { + q: searchQuery, + }, + }); } - getCategories(): Observable> { + getCategories(posId: number): Observable> { return this.http.get>( - this.apiRoutes.productCategories(), + this.apiRoutes.productCategories(posId), ); } - submitOrder(payload: IPosOrderRequest): Observable { - return this.http.post(this.apiRoutes.submitOrder(), payload); + submitOrder(posId: number, payload: IPosOrderRequest): Observable { + return this.http.post(this.apiRoutes.submitOrder(posId), payload); } } diff --git a/src/app/modules/pos/store/index.ts b/src/app/modules/pos/store/index.ts index dbde203..72b6a94 100644 --- a/src/app/modules/pos/store/index.ts +++ b/src/app/modules/pos/store/index.ts @@ -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('POS_ID'); + +export type TViewType = 'list' | 'grid'; + interface IPosState { getStockLoading: boolean; stock: Maybe; @@ -16,6 +20,8 @@ interface IPosState { activeProductCategory: Maybe; inOrderProducts: IPosInOrderProduct[]; selectedCustomer: Maybe; + 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({ ...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) { 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(); } } diff --git a/src/app/modules/pos/views/pos.component.html b/src/app/modules/pos/views/pos.component.html index 2b1257e..472e418 100644 --- a/src/app/modules/pos/views/pos.component.html +++ b/src/app/modules/pos/views/pos.component.html @@ -4,7 +4,7 @@
Logo
- {{ info()?.store?.name }} + {{ info()?.name }} - فروشگاه {{ info()?.inventory?.name }}
diff --git a/src/app/modules/pos/views/pos.component.ts b/src/app/modules/pos/views/pos.component.ts index ff52803..f30f660 100644 --- a/src/app/modules/pos/views/pos.component.ts +++ b/src/app/modules/pos/views/pos.component.ts @@ -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>(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); + // }, + // }); + // } } diff --git a/src/app/shared/components/search/search-input.component.html b/src/app/shared/components/search/search-input.component.html new file mode 100644 index 0000000..11ca93b --- /dev/null +++ b/src/app/shared/components/search/search-input.component.html @@ -0,0 +1 @@ + diff --git a/src/app/shared/components/search/search-input.component.ts b/src/app/shared/components/search/search-input.component.ts new file mode 100644 index 0000000..37f43d2 --- /dev/null +++ b/src/app/shared/components/search/search-input.component.ts @@ -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(); + private destroy$ = new Subject(); + + @Output() search = new EventEmitter(); + + 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(); + } +}