From e937c994d7587e6a56c9affeb7b73caed86ac5b5 Mon Sep 17 00:00:00 2001 From: ahasani194 Date: Sun, 21 Dec 2025 19:09:13 +0330 Subject: [PATCH] feat: Add Cardex module with filters and invoices components - Implemented Cardex routes and integrated with the main application routing. - Created filters component for Cardex to filter inventory and product data. - Developed invoices component to display supplier invoices with detailed views. - Enhanced Products module with pagination and state management using a store. - Updated Suppliers module to include invoice management and detailed supplier views. - Refactored state management in Products and Suppliers to utilize signals for reactive updates. - Added new models and services to support the Cardex functionality. - Improved UI components for better user experience in displaying data. --- src/app.routes.ts | 9 +- src/app/core/state/base-store.ts | 26 +++-- src/app/modules/auth/auth.routes.ts | 2 +- .../components/filters/filters.component.html | 11 ++ .../components/filters/filters.component.ts | 31 +++++ src/app/modules/cardex/components/index.ts | 1 + .../cardex/constants/apiRoutes/index.ts | 5 + src/app/modules/cardex/constants/index.ts | 2 + .../modules/cardex/constants/routes/index.ts | 17 +++ src/app/modules/cardex/models/index.ts | 2 + src/app/modules/cardex/models/io.d.ts | 66 +++++++++++ src/app/modules/cardex/models/types.ts | 42 +++++++ .../cardex/views/cardex.component.html | 8 ++ .../modules/cardex/views/cardex.component.ts | 13 +++ .../inventories/views/list.component.ts | 4 +- src/app/modules/products/models/io.d.ts | 3 + .../modules/products/services/main.service.ts | 13 ++- src/app/modules/products/store/main.ts | 75 ++++++++++++ .../products/views/list.component.html | 4 + .../modules/products/views/list.component.ts | 34 +++--- .../invoices/invoices.component.html | 107 ++++++++++++++++++ .../components/invoices/invoices.component.ts | 54 +++++++++ .../suppliers/constants/apiRoutes/index.ts | 3 + src/app/modules/suppliers/models/io.d.ts | 28 ++++- src/app/modules/suppliers/models/types.ts | 21 ++++ .../suppliers/services/main.service.ts | 33 +++++- .../suppliers/views/single.component.html | 82 +++++++++++++- .../suppliers/views/single.component.ts | 57 +++++++++- .../components/cardex/cardex.component.html | 12 +- 29 files changed, 716 insertions(+), 49 deletions(-) create mode 100644 src/app/modules/cardex/components/filters/filters.component.html create mode 100644 src/app/modules/cardex/components/filters/filters.component.ts create mode 100644 src/app/modules/cardex/components/index.ts create mode 100644 src/app/modules/cardex/constants/apiRoutes/index.ts create mode 100644 src/app/modules/cardex/constants/index.ts create mode 100644 src/app/modules/cardex/constants/routes/index.ts create mode 100644 src/app/modules/cardex/models/index.ts create mode 100644 src/app/modules/cardex/models/io.d.ts create mode 100644 src/app/modules/cardex/models/types.ts create mode 100644 src/app/modules/cardex/views/cardex.component.html create mode 100644 src/app/modules/cardex/views/cardex.component.ts create mode 100644 src/app/modules/products/store/main.ts create mode 100644 src/app/modules/suppliers/components/invoices/invoices.component.html create mode 100644 src/app/modules/suppliers/components/invoices/invoices.component.ts create mode 100644 src/app/modules/suppliers/models/types.ts diff --git a/src/app.routes.ts b/src/app.routes.ts index 5281e0c..bf3429f 100644 --- a/src/app.routes.ts +++ b/src/app.routes.ts @@ -1,3 +1,5 @@ +import { AuthComponent } from '@/modules/auth/pages/auth.component'; +import { CARDEX_ROUTES } from '@/modules/cardex/constants'; import { CUSTOMERS_ROUTES } from '@/modules/customers/constants'; import { INVENTORIES_ROUTES } from '@/modules/inventories/constants'; import { POSComponent } from '@/modules/pos/views/pos.component'; @@ -27,6 +29,7 @@ export const appRoutes: Routes = [ ...CUSTOMERS_ROUTES, ...INVENTORIES_ROUTES, ...PRODUCTS_ROUTES, + ...CARDEX_ROUTES, { path: 'uikit', loadChildren: () => import('./app/pages/uikit/uikit.routes') }, { path: 'documentation', component: Documentation }, { path: 'pages', loadChildren: () => import('./app/pages/pages.routes') }, @@ -36,7 +39,11 @@ export const appRoutes: Routes = [ path: 'pos', component: POSComponent, }, + { + path: 'auth', + component: AuthComponent, + }, { path: 'notfound', component: Notfound }, - { path: 'auth', loadChildren: () => import('./app/pages/auth/auth.routes') }, + // { path: 'auth', loadChildren: () => import('./app/pages/auth/auth.routes') }, { path: '**', redirectTo: '/notfound' }, ]; diff --git a/src/app/core/state/base-store.ts b/src/app/core/state/base-store.ts index b4c68c3..f83d4be 100644 --- a/src/app/core/state/base-store.ts +++ b/src/app/core/state/base-store.ts @@ -1,6 +1,7 @@ import { Signal, WritableSignal, computed, signal } from '@angular/core'; import { BehaviorSubject, Observable } from 'rxjs'; import { Maybe } from '../models'; +import { IPaginatedQuery, IResponseMetadata } from '../models/service.model'; /** * Base interface for all state objects @@ -8,9 +9,10 @@ import { Maybe } from '../models'; export interface BaseState { loading: boolean; error: Maybe; - lastId?: string; + currentPage?: number; items?: T; - meta?: Record; + meta?: Maybe; + isRefreshing?: boolean; } /** @@ -21,6 +23,7 @@ export interface PaginatedState extends BaseState { totalCount: number; currentPage: number; pageSize: number; + totalPages: number; hasMore: boolean; } @@ -246,21 +249,24 @@ export abstract class PaginatedStore< readonly currentPage = computed(() => this._state().currentPage); readonly pageSize = computed(() => this._state().pageSize); readonly hasMore = computed(() => this._state().hasMore); - readonly totalPages = computed(() => { - const state = this._state(); - return Math.ceil(state.totalCount / state.pageSize); - }); + readonly totalPages = computed(() => this._state().totalPages); + readonly paginatedMeta = computed(() => ({ + currentPage: this.currentPage(), + totalPages: this.totalPages(), + totalCount: this.totalCount(), + pageSize: this.pageSize(), + })); /** * Set items for current page */ - protected setItems(items: T[], totalCount: number, currentPage: number): void { + protected setItems(items: T[], meta: IResponseMetadata): void { const state = this._state(); this.patchState({ items, - totalCount, - currentPage, - hasMore: currentPage * state.pageSize < totalCount, + totalCount: meta.totalRecords, + currentPage: meta.page, + hasMore: meta.page < meta.totalPages, loading: false, error: null, } as Partial); diff --git a/src/app/modules/auth/auth.routes.ts b/src/app/modules/auth/auth.routes.ts index 0708d56..f4bf2f4 100644 --- a/src/app/modules/auth/auth.routes.ts +++ b/src/app/modules/auth/auth.routes.ts @@ -5,7 +5,7 @@ export type AuthRouteNames = 'auth'; export const authNamedRoutes: NamedRoutes = { auth: { - path: '', + path: 'auth', loadComponent: () => import('./pages/auth.component').then((m) => m.AuthComponent), meta: { title: 'احراز هویت' }, }, diff --git a/src/app/modules/cardex/components/filters/filters.component.html b/src/app/modules/cardex/components/filters/filters.component.html new file mode 100644 index 0000000..b134a27 --- /dev/null +++ b/src/app/modules/cardex/components/filters/filters.component.html @@ -0,0 +1,11 @@ +
+
+ + + + +
+
+ +
+
diff --git a/src/app/modules/cardex/components/filters/filters.component.ts b/src/app/modules/cardex/components/filters/filters.component.ts new file mode 100644 index 0000000..71ab15c --- /dev/null +++ b/src/app/modules/cardex/components/filters/filters.component.ts @@ -0,0 +1,31 @@ +import { InventoriesSelectComponent } from '@/modules/inventories/components'; +import { ProductsSelectComponent } from '@/modules/products/components/select/select.component'; +import { UikitFlatpickrJalaliComponent } from '@/uikit'; +import { Component, inject } from '@angular/core'; +import { FormBuilder, ReactiveFormsModule } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; +import { ButtonDirective } from 'primeng/button'; + +@Component({ + selector: 'cardex-filters', + templateUrl: './filters.component.html', + imports: [ + ReactiveFormsModule, + InventoriesSelectComponent, + ProductsSelectComponent, + UikitFlatpickrJalaliComponent, + ButtonDirective, + ], +}) +export class CardexFiltersComponent { + private route = inject(ActivatedRoute); + private fb = inject(FormBuilder); + constructor() {} + + form = this.fb.group({ + inventoryId: [Number(this.route.snapshot.queryParams['inventoryId']) || 0], + productId: [Number(this.route.snapshot.queryParams['productId']) || 0], + startDate: [this.route.snapshot.queryParams['startDate'] || ''], + endDate: [this.route.snapshot.queryParams['endDate'] || ''], + }); +} diff --git a/src/app/modules/cardex/components/index.ts b/src/app/modules/cardex/components/index.ts new file mode 100644 index 0000000..bcac1e7 --- /dev/null +++ b/src/app/modules/cardex/components/index.ts @@ -0,0 +1 @@ +export * from './filters/filters.component'; diff --git a/src/app/modules/cardex/constants/apiRoutes/index.ts b/src/app/modules/cardex/constants/apiRoutes/index.ts new file mode 100644 index 0000000..2b85bca --- /dev/null +++ b/src/app/modules/cardex/constants/apiRoutes/index.ts @@ -0,0 +1,5 @@ +const baseUrl = '/api/v1/cardex'; + +export const CARDEX_API_ROUTES = { + cardex: () => `${baseUrl}`, +}; diff --git a/src/app/modules/cardex/constants/index.ts b/src/app/modules/cardex/constants/index.ts new file mode 100644 index 0000000..ee61bd7 --- /dev/null +++ b/src/app/modules/cardex/constants/index.ts @@ -0,0 +1,2 @@ +export * from './apiRoutes'; +export * from './routes'; diff --git a/src/app/modules/cardex/constants/routes/index.ts b/src/app/modules/cardex/constants/routes/index.ts new file mode 100644 index 0000000..7a87856 --- /dev/null +++ b/src/app/modules/cardex/constants/routes/index.ts @@ -0,0 +1,17 @@ +import { NamedRoutes } from '@/core'; +import { Routes } from '@angular/router'; + +export type TCARDEXRouteNames = 'CARDEX'; + +export const cardexNamedRoutes: NamedRoutes = { + CARDEX: { + path: 'cardex', + loadComponent: () => import('../../views/cardex.component').then((m) => m.CardexPageComponent), + meta: { + title: 'کاردکس', + pagePath: () => '/cardex', + }, + }, +}; + +export const CARDEX_ROUTES: Routes = Object.values(cardexNamedRoutes); diff --git a/src/app/modules/cardex/models/index.ts b/src/app/modules/cardex/models/index.ts new file mode 100644 index 0000000..d390b4d --- /dev/null +++ b/src/app/modules/cardex/models/index.ts @@ -0,0 +1,2 @@ +export * from './io'; +export * from './types'; diff --git a/src/app/modules/cardex/models/io.d.ts b/src/app/modules/cardex/models/io.d.ts new file mode 100644 index 0000000..d213166 --- /dev/null +++ b/src/app/modules/cardex/models/io.d.ts @@ -0,0 +1,66 @@ +import { + IInventoryInfo, + IInventoryMovement, + IInventoryStockProduct, + IInventoryTransferRequestItem, +} from './types'; + +export interface IInventorySummaryRawResponse { + id: number; + name: string; + location: string; + isActive: boolean; + isPointOfSale: boolean; + createdAt: string; + updatedAt: string; + deletedAt: Maybe; +} + +export interface IInventorySummaryResponse extends IInventorySummaryRawResponse {} + +export interface IInventoryDetailRawResponse extends IInventorySummaryRawResponse { + availableProductTypes: number; + availableProductCount: number; + availableProductsCost: number; +} + +export interface IInventoryDetailResponse extends IInventoryDetailRawResponse {} + +export interface IInventoryRequest { + name: string; + location: string; + isActive: boolean; +} + +export interface IInventoryStockMovementRawResponse { + receiptId: string; + count: number; + info: IInventoryInfo; + movements: IInventoryMovement[]; +} + +export interface IInventoryStockMovementResponse extends IInventoryStockMovementRawResponse {} + +export interface IInventoryTransferRequest { + code: string; + description: string; + fromInventoryId: number; + toInventoryId: number; + items: IInventoryTransferRequestItem[]; +} + +export interface IInventoryStockRawResponse { + quantity: number; + avgCost: number; + product: IInventoryStockProduct; +} + +export interface IInventoryStockResponse extends IInventoryStockRawResponse {} + +export interface IInventoryStockQuery { + isAvailable?: boolean; +} + +export interface IInventoryProductCardexRawResponse extends IInventoryStockMovementRawResponse {} + +export interface IInventoryProductCardexResponse extends IInventoryProductCardexRawResponse {} diff --git a/src/app/modules/cardex/models/types.ts b/src/app/modules/cardex/models/types.ts new file mode 100644 index 0000000..2093824 --- /dev/null +++ b/src/app/modules/cardex/models/types.ts @@ -0,0 +1,42 @@ +import { IProductRawResponse } from '@/modules/products/models'; + +export interface IInventoryMovement { + id: number; + quantity: number; + fee: number; + totalCost: number; + avgCost: number; + product: IInventoryProduct; + remainedInStock: number; +} + +export interface IInventoryProduct { + id: number; + name: string; + description: null; + sku: string; + barcode: string; + createdAt: string; + updatedAt: string; + deletedAt: null; + brandId: number; + categoryId: number; +} + +export interface IInventoryInfo { + date: string; + type: string; + quantity: number; + fee: number; + totalCost: number; + referenceType: string; + referenceId: string; + createdAt: string; +} + +export interface IInventoryTransferRequestItem { + productId: number; + count: number; +} + +export interface IInventoryStockProduct extends IProductRawResponse {} diff --git a/src/app/modules/cardex/views/cardex.component.html b/src/app/modules/cardex/views/cardex.component.html new file mode 100644 index 0000000..1fc4011 --- /dev/null +++ b/src/app/modules/cardex/views/cardex.component.html @@ -0,0 +1,8 @@ +
+ + + + + + +
diff --git a/src/app/modules/cardex/views/cardex.component.ts b/src/app/modules/cardex/views/cardex.component.ts new file mode 100644 index 0000000..c940f9c --- /dev/null +++ b/src/app/modules/cardex/views/cardex.component.ts @@ -0,0 +1,13 @@ +import { UikitEmptyStateComponent } from '@/uikit'; +import { Component } from '@angular/core'; +import { Card } from 'primeng/card'; +import { CardexFiltersComponent } from '../components'; + +@Component({ + selector: 'app-cardex', + templateUrl: './cardex.component.html', + imports: [Card, CardexFiltersComponent, UikitEmptyStateComponent], +}) +export class CardexPageComponent { + constructor() {} +} diff --git a/src/app/modules/inventories/views/list.component.ts b/src/app/modules/inventories/views/list.component.ts index 5d5f31d..babd0e7 100644 --- a/src/app/modules/inventories/views/list.component.ts +++ b/src/app/modules/inventories/views/list.component.ts @@ -23,8 +23,8 @@ export class InventoriesComponent { { field: 'id', header: 'شناسه' }, { field: 'name', header: 'نام' }, { field: 'location', header: 'موقعیت' }, - { field: 'isPointOfSale', header: 'نقطه فروش' }, - { field: 'isActive', header: 'فعال' }, + { field: 'isPointOfSale', header: 'نقطه فروش', type: 'boolean' }, + { field: 'isActive', header: 'فعال', type: 'boolean' }, ] as IColumn[]; loading = signal(false); diff --git a/src/app/modules/products/models/io.d.ts b/src/app/modules/products/models/io.d.ts index 692192c..5ff6772 100644 --- a/src/app/modules/products/models/io.d.ts +++ b/src/app/modules/products/models/io.d.ts @@ -1,6 +1,9 @@ import { Maybe } from '@/core'; +import { IPaginatedQuery } from '@/core/models/service.model'; import { IProductBrand, IProductCategory } from './others'; +export interface IGetProductsQueryParams extends IPaginatedQuery {} + export interface IProductRawResponse { id: number; name: string; diff --git a/src/app/modules/products/services/main.service.ts b/src/app/modules/products/services/main.service.ts index 2855d4a..0458d73 100644 --- a/src/app/modules/products/services/main.service.ts +++ b/src/app/modules/products/services/main.service.ts @@ -3,7 +3,12 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { PRODUCTS_API_ROUTES } from '../constants'; -import { IProductRawResponse, IProductRequest, IProductResponse } from '../models'; +import { + IGetProductsQueryParams, + IProductRawResponse, + IProductRequest, + IProductResponse, +} from '../models'; @Injectable({ providedIn: 'root' }) export class ProductsService { @@ -11,8 +16,10 @@ export class ProductsService { private apiRoutes = PRODUCTS_API_ROUTES; - getAll(): Observable> { - return this.http.get>(this.apiRoutes.list()); + getAll(params?: IGetProductsQueryParams): Observable> { + return this.http.get>(this.apiRoutes.list(), { + params: { ...params }, + }); } getSingle(productId: string): Observable { diff --git a/src/app/modules/products/store/main.ts b/src/app/modules/products/store/main.ts new file mode 100644 index 0000000..eb2eb30 --- /dev/null +++ b/src/app/modules/products/store/main.ts @@ -0,0 +1,75 @@ +import { PaginatedState, PaginatedStore } from '@/core/state'; +import { Injectable } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { IProductResponse } from '../models'; +import { ProductsService } from '../services/main.service'; + +export interface ProductsState extends PaginatedState {} + +@Injectable({ + providedIn: 'root', +}) +export class ProductsStore extends PaginatedStore { + // Login-specific state + private readonly _productsState = { + isRefreshing: false, + }; + + constructor( + private activeRoute: ActivatedRoute, + private service: ProductsService, + ) { + const queryParams = activeRoute.snapshot.queryParams; + + super({ + loading: false, + error: null, + isRefreshing: false, + items: [], + totalCount: 0, + currentPage: Number(queryParams['page']) || 1, + pageSize: Number(queryParams['pageSize']) || 30, + totalPages: 0, + hasMore: false, + }); + this.initial(); + } + + /** + * Reset state to initial values + */ + reset(): void { + this.setState({ + loading: false, + error: null, + isRefreshing: false, + items: [], + totalCount: 0, + currentPage: Number(this.activeRoute.snapshot.queryParams['page']) || 1, + pageSize: Number(this.activeRoute.snapshot.queryParams['pageSize']) || 30, + totalPages: 0, + hasMore: false, + }); + } + + initial() { + this.getAll(); + } + + getAll() { + this.setLoading(true); + this.service.getAll({ page: this.currentPage(), pageSize: this.pageSize() }).subscribe({ + next: (res) => { + this.setItems(res.data, res.meta); + }, + error: (err) => { + this.setError(err); + }, + }); + } + + onPageChange(page: number) { + this.setCurrentPage(page); + this.getAll(); + } +} diff --git a/src/app/modules/products/views/list.component.html b/src/app/modules/products/views/list.component.html index c27dee3..4457560 100644 --- a/src/app/modules/products/views/list.component.html +++ b/src/app/modules/products/views/list.component.html @@ -8,8 +8,12 @@ [items]="items()" [loading]="loading()" [showDetails]="true" + [perPage]="paginatedMeta().pageSize" + [currentPage]="paginatedMeta().currentPage" + [totalRecords]="paginatedMeta().totalCount" (onAdd)="openAddForm()" (onDetails)="toDetails($event)" + (pageChange)="onPageChange($event)" /> diff --git a/src/app/modules/products/views/list.component.ts b/src/app/modules/products/views/list.component.ts index b245d91..229f110 100644 --- a/src/app/modules/products/views/list.component.ts +++ b/src/app/modules/products/views/list.component.ts @@ -8,18 +8,21 @@ import { ProductFormComponent } from '../components/form.component'; import { productsNamedRoutes } from '../constants'; import { IProductResponse } from '../models'; import { ProductsService } from '../services/main.service'; +import { ProductsStore } from '../store/main'; @Component({ selector: 'app-products', templateUrl: './list.component.html', imports: [PageDataListComponent, ProductFormComponent], + standalone: true, }) export class ProductsComponent { constructor( private productService: ProductsService, private router: Router, + private store: ProductsStore, ) { - this.getData(); + store.initial(); } columns = [ @@ -41,27 +44,27 @@ export class ProductsComponent { }, { field: 'salePrice', - header: 'قیمت فروش', + header: 'قیمت پایه فروش', type: 'price', }, { field: 'createdAt', header: 'تاریخ ایجاد', type: 'date' }, { field: 'updatedAt', header: 'تاریخ به‌روزرسانی', type: 'date' }, ] as IColumn[]; - loading = signal(false); - items = signal([]); + get loading() { + return this.store.loading; + } + get items() { + return this.store.items; + } + get paginatedMeta() { + return this.store.paginatedMeta; + } + visibleForm = signal(false); refresh() { - this.getData(); - } - - getData() { - this.loading.set(true); - this.productService.getAll().subscribe((res) => { - this.loading.set(false); - this.items.set(res.data); - }); + this.store.initial(); } openAddForm() { @@ -69,10 +72,13 @@ export class ProductsComponent { if (pagePathFn) { this.router.navigateByUrl(pagePathFn({})); } - // this.visibleForm.set(true); } toDetails(product: IProductResponse) { this.router.navigate(['/products', product.id]); } + + onPageChange(page: number) { + this.store.onPageChange(page); + } } diff --git a/src/app/modules/suppliers/components/invoices/invoices.component.html b/src/app/modules/suppliers/components/invoices/invoices.component.html new file mode 100644 index 0000000..e5ec3df --- /dev/null +++ b/src/app/modules/suppliers/components/invoices/invoices.component.html @@ -0,0 +1,107 @@ + + +
+ فاکتورهای خرید + +
+
+ + + + + + شناسه رسید + انبار + مجموع قیمت + تاریخ + + + + + + + + {{ item.code }} + {{ item.inventory.name }} + + + + + + + + + + + +
کالاهای تامین شده
+
+ + + +
+ شناسه کالا + +
+ + +
+ عنوان کالا + +
+ + +
+ تعداد + +
+ + +
+ قیمت واحد + +
+ + +
+ قیمت نهایی + +
+ + + +
+ + + {{ item.product.id }} + {{ item.product.name }} + {{ item.count }} + + + + + + + +
+
+ + +
+ + + + هیچ فاکتوری ثبت نشده است. + + +
+
+
diff --git a/src/app/modules/suppliers/components/invoices/invoices.component.ts b/src/app/modules/suppliers/components/invoices/invoices.component.ts new file mode 100644 index 0000000..5c64347 --- /dev/null +++ b/src/app/modules/suppliers/components/invoices/invoices.component.ts @@ -0,0 +1,54 @@ +import { Maybe } from '@/core'; +import { JalaliDateDirective, PriceMaskDirective } from '@/shared/directives'; +import { CommonModule } from '@angular/common'; +import { Component, Input, OnInit, signal } from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { Button, ButtonDirective } from 'primeng/button'; +import { Card } from 'primeng/card'; +import { Ripple } from 'primeng/ripple'; +import { TableModule } from 'primeng/table'; +import { ISupplierInvoicesResponse } from '../../models'; +import { SuppliersService } from '../../services/main.service'; + +@Component({ + selector: 'suppliers-invoices', + standalone: true, + imports: [ + CommonModule, + Card, + ButtonDirective, + TableModule, + PriceMaskDirective, + JalaliDateDirective, + Ripple, + Button, + RouterLink, + ], + templateUrl: './invoices.component.html', +}) +export class SuppliersInvoicesComponent implements OnInit { + @Input() supplierId!: string; + constructor(private service: SuppliersService) {} + + ngOnInit(): void { + this.getAll(); + } + + expandedRows = {}; + + loading = signal(true); + items = signal>(null); + + getAll() { + this.loading.set(true); + this.service.getInvoices(this.supplierId).subscribe({ + next: (res) => { + this.loading.set(false); + this.items.set(res.data); + }, + error: () => { + this.loading.set(false); + }, + }); + } +} diff --git a/src/app/modules/suppliers/constants/apiRoutes/index.ts b/src/app/modules/suppliers/constants/apiRoutes/index.ts index f4a129f..7b8fe1c 100644 --- a/src/app/modules/suppliers/constants/apiRoutes/index.ts +++ b/src/app/modules/suppliers/constants/apiRoutes/index.ts @@ -3,4 +3,7 @@ const baseUrl = '/api/v1/suppliers'; export const SUPPLIERS_API_ROUTES = { list: () => `${baseUrl}`, single: (supplierId: string) => `${baseUrl}/${supplierId}`, + invoices: (supplierId: string) => `${baseUrl}/${supplierId}/invoices`, + invoice: (supplierId: string, invoiceId: string) => + `${baseUrl}/${supplierId}/invoices/${invoiceId}`, }; diff --git a/src/app/modules/suppliers/models/io.d.ts b/src/app/modules/suppliers/models/io.d.ts index de90f8b..19eab7f 100644 --- a/src/app/modules/suppliers/models/io.d.ts +++ b/src/app/modules/suppliers/models/io.d.ts @@ -1,3 +1,5 @@ +import { IReceiptsOverview, ISupplierInventorySummary, ISupplierReceiptItemSummary } from './types'; + export interface ISupplierRawResponse { id: number; firstName: string; @@ -9,15 +11,22 @@ export interface ISupplierRawResponse { state: string; country: string; isActive: boolean; - createdAt: Date; - updatedAt: Date; - deletedAt: Date; + createdAt: string; + updatedAt: string; + deletedAt: string; } export interface ISupplierResponse extends ISupplierRawResponse { fullname: string; } +export interface ISupplierFullInfoRawResponse extends ISupplierRawResponse { + receiptsOverview: IReceiptsOverview; +} +export interface ISupplierFullInfoResponse extends ISupplierFullInfoRawResponse { + fullname: string; +} + export interface ISupplierRequest { firstName: string; lastName: string; @@ -29,3 +38,16 @@ export interface ISupplierRequest { country: string; isActive: boolean; } + +export interface ISupplierInvoicesRawResponse { + id: number; + code: string; + totalAmount: string; + description: string; + createdAt: string; + updatedAt: string; + items: ISupplierReceiptItemSummary[]; + inventory: ISupplierInventorySummary; +} + +export interface ISupplierInvoicesResponse extends ISupplierInvoicesRawResponse {} diff --git a/src/app/modules/suppliers/models/types.ts b/src/app/modules/suppliers/models/types.ts new file mode 100644 index 0000000..8a50f77 --- /dev/null +++ b/src/app/modules/suppliers/models/types.ts @@ -0,0 +1,21 @@ +export interface IReceiptsOverview { + totalPayment: string; + count: number; +} + +export interface ISupplierInventorySummary { + id: number; + name: string; +} + +export interface ISupplierReceiptItemSummary { + id: number; + count: string; + fee: string; + total: string; + product: { + id: string; + name: string; + sku: string; + }; +} diff --git a/src/app/modules/suppliers/services/main.service.ts b/src/app/modules/suppliers/services/main.service.ts index 754a502..924f808 100644 --- a/src/app/modules/suppliers/services/main.service.ts +++ b/src/app/modules/suppliers/services/main.service.ts @@ -1,9 +1,18 @@ import { IPaginatedResponse } from '@/core/models/service.model'; +import { getFullName } from '@/utils'; import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { map, Observable } from 'rxjs'; import { SUPPLIERS_API_ROUTES } from '../constants'; -import { ISupplierRawResponse, ISupplierRequest, ISupplierResponse } from '../models'; +import { + ISupplierFullInfoRawResponse, + ISupplierFullInfoResponse, + ISupplierInvoicesRawResponse, + ISupplierInvoicesResponse, + ISupplierRawResponse, + ISupplierRequest, + ISupplierResponse, +} from '../models'; @Injectable({ providedIn: 'root' }) export class SuppliersService { @@ -17,17 +26,17 @@ export class SuppliersService { ...res, data: res.data.map((item) => ({ ...item, - fullname: `${item.firstName} ${item.lastName}`, + fullname: getFullName({ firstName: item.firstName, lastName: item.lastName }), })), })), ); } - getSingle(supplierId: string): Observable { - return this.http.get(this.apiRoutes.single(supplierId)).pipe( + getSingle(supplierId: string): Observable { + return this.http.get(this.apiRoutes.single(supplierId)).pipe( map((item) => ({ ...item, - fullname: `${item.firstName} ${item.lastName}`, + fullname: getFullName({ firstName: item.firstName, lastName: item.lastName }), })), ); } @@ -36,8 +45,20 @@ export class SuppliersService { return this.http.post(this.apiRoutes.list(), data).pipe( map((item) => ({ ...item, - fullname: `${item.firstName} ${item.lastName}`, + fullname: getFullName({ firstName: item.firstName, lastName: item.lastName }), })), ); } + + getInvoices(supplierId: string): Observable> { + return this.http.get>( + this.apiRoutes.invoices(supplierId), + ); + } + + getInvoice(supplierId: string, invoiceId: string): Observable { + return this.http.get( + this.apiRoutes.invoice(supplierId, invoiceId), + ); + } } diff --git a/src/app/modules/suppliers/views/single.component.html b/src/app/modules/suppliers/views/single.component.html index 7144e26..27cee4f 100644 --- a/src/app/modules/suppliers/views/single.component.html +++ b/src/app/modules/suppliers/views/single.component.html @@ -1 +1,81 @@ -
+
+
+
+
+ +

{{ data()?.fullname }}

+
+
+
+ + + +
+
+
+
+ +
+
+ +
+
+ @for (item of topBarCardDetails; track $index) { + + } +
+
+ + +
diff --git a/src/app/modules/suppliers/views/single.component.ts b/src/app/modules/suppliers/views/single.component.ts index 4500ce7..2174fa6 100644 --- a/src/app/modules/suppliers/views/single.component.ts +++ b/src/app/modules/suppliers/views/single.component.ts @@ -1,9 +1,62 @@ -import { Component } from '@angular/core'; +import { Maybe } from '@/core'; +import { KeyValueComponent } from '@/shared/components'; +import { DetailsInfoCardComponent } from '@/shared/components/detailsInfoCard/details-info-card.component'; +import { formatWithCurrency } from '@/utils'; +import { Component, inject, signal } from '@angular/core'; +import { ActivatedRoute, RouterLink } from '@angular/router'; +import { Button, ButtonDirective } from 'primeng/button'; +import { SuppliersInvoicesComponent } from '../components/invoices/invoices.component'; +import { ISupplierFullInfoResponse } from '../models'; +import { SuppliersService } from '../services/main.service'; @Component({ selector: 'app-supplier', templateUrl: './single.component.html', + imports: [ + DetailsInfoCardComponent, + KeyValueComponent, + Button, + RouterLink, + ButtonDirective, + SuppliersInvoicesComponent, + ], }) export class SupplierComponent { - constructor() {} + private route = inject(ActivatedRoute); + constructor(private service: SuppliersService) { + this.getInfo(); + } + + supplierId = this.route.snapshot.params['supplierId']; + + data = signal>(null); + loading = signal>(true); + + getInfo() { + this.loading.set(true); + this.service.getSingle(this.supplierId).subscribe({ + next: (res) => { + this.loading.set(false); + this.data.set(res); + }, + error: (err) => { + this.loading.set(false); + }, + }); + } + + get topBarCardDetails() { + return [ + { + icon: 'pi pi-box', + label: 'تعداد فاکتورها', + value: this.data()?.receiptsOverview.count, + }, + { + icon: 'pi pi-dollar', + label: 'ارزش کلی پرداختی‌ها', + value: formatWithCurrency(this.data()?.receiptsOverview.totalPayment || 0), + }, + ]; + } } diff --git a/src/app/shared/components/cardex/cardex.component.html b/src/app/shared/components/cardex/cardex.component.html index 9a2c61f..7b6aaca 100644 --- a/src/app/shared/components/cardex/cardex.component.html +++ b/src/app/shared/components/cardex/cardex.component.html @@ -14,9 +14,9 @@ شماره طرف حساب انبار - وارده + ورودی خروجی - مانده + مانده تعداد @@ -24,8 +24,8 @@ تعداد فی تعداد - فی - مجموع قیمت + @@ -60,8 +60,8 @@ } {{ item.remainedInStock }} - - +