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.
This commit is contained in:
+8
-1
@@ -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' },
|
||||
];
|
||||
|
||||
@@ -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<T = any> {
|
||||
loading: boolean;
|
||||
error: Maybe<string>;
|
||||
lastId?: string;
|
||||
currentPage?: number;
|
||||
items?: T;
|
||||
meta?: Record<string, any>;
|
||||
meta?: Maybe<IPaginatedQuery>;
|
||||
isRefreshing?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -21,6 +23,7 @@ export interface PaginatedState<T = any> 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<TState>);
|
||||
|
||||
@@ -5,7 +5,7 @@ export type AuthRouteNames = 'auth';
|
||||
|
||||
export const authNamedRoutes: NamedRoutes<AuthRouteNames> = {
|
||||
auth: {
|
||||
path: '',
|
||||
path: 'auth',
|
||||
loadComponent: () => import('./pages/auth.component').then((m) => m.AuthComponent),
|
||||
meta: { title: 'احراز هویت' },
|
||||
},
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<form [formGroup]="form" class="flex items-end gap-4">
|
||||
<div class="grow grid grid-cols-4 gap-2">
|
||||
<inventories-select-field [control]="form.controls.inventoryId" />
|
||||
<products-select-field [control]="form.controls.productId" />
|
||||
<uikit-datepicker [control]="form.controls.startDate" />
|
||||
<uikit-datepicker [control]="form.controls.endDate" />
|
||||
</div>
|
||||
<div class="shrink-0">
|
||||
<button pButton>جستجو</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -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'] || ''],
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './filters/filters.component';
|
||||
@@ -0,0 +1,5 @@
|
||||
const baseUrl = '/api/v1/cardex';
|
||||
|
||||
export const CARDEX_API_ROUTES = {
|
||||
cardex: () => `${baseUrl}`,
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './apiRoutes';
|
||||
export * from './routes';
|
||||
@@ -0,0 +1,17 @@
|
||||
import { NamedRoutes } from '@/core';
|
||||
import { Routes } from '@angular/router';
|
||||
|
||||
export type TCARDEXRouteNames = 'CARDEX';
|
||||
|
||||
export const cardexNamedRoutes: NamedRoutes<TCARDEXRouteNames> = {
|
||||
CARDEX: {
|
||||
path: 'cardex',
|
||||
loadComponent: () => import('../../views/cardex.component').then((m) => m.CardexPageComponent),
|
||||
meta: {
|
||||
title: 'کاردکس',
|
||||
pagePath: () => '/cardex',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const CARDEX_ROUTES: Routes = Object.values(cardexNamedRoutes);
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './io';
|
||||
export * from './types';
|
||||
+66
@@ -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<string>;
|
||||
}
|
||||
|
||||
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 {}
|
||||
@@ -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 {}
|
||||
@@ -0,0 +1,8 @@
|
||||
<div class="flex flex-col gap-4">
|
||||
<p-card>
|
||||
<cardex-filters />
|
||||
</p-card>
|
||||
<p-card>
|
||||
<uikit-empty-state title="برای مشاهدهی کاردکس اطلاعات بالا را تکمیل کنید"> </uikit-empty-state>
|
||||
</p-card>
|
||||
</div>
|
||||
@@ -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() {}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
+3
@@ -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;
|
||||
|
||||
@@ -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<IPaginatedResponse<IProductResponse>> {
|
||||
return this.http.get<IPaginatedResponse<IProductRawResponse>>(this.apiRoutes.list());
|
||||
getAll(params?: IGetProductsQueryParams): Observable<IPaginatedResponse<IProductResponse>> {
|
||||
return this.http.get<IPaginatedResponse<IProductRawResponse>>(this.apiRoutes.list(), {
|
||||
params: { ...params },
|
||||
});
|
||||
}
|
||||
|
||||
getSingle(productId: string): Observable<IProductResponse> {
|
||||
|
||||
@@ -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<IProductResponse> {
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
@@ -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)"
|
||||
/>
|
||||
|
||||
<product-form [(visible)]="visibleForm" (onSubmit)="refresh()" />
|
||||
|
||||
@@ -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<IProductResponse[]>([]);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
<p-card>
|
||||
<ng-template pTemplate="header">
|
||||
<div class="p-4 flex items-center justify-between">
|
||||
<span class="text-xl font-bold">فاکتورهای خرید</span>
|
||||
<button pButton outlined icon="pi pi-refresh" [loading]="loading()" (click)="getAll()"></button>
|
||||
</div>
|
||||
</ng-template>
|
||||
<ng-template pTemplate="content">
|
||||
<p-table [value]="items() || []" [loading]="loading()" dataKey="id" [expandedRowKeys]="expandedRows">
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th [style]="{ width: '5rem' }"></th>
|
||||
<th>شناسه رسید</th>
|
||||
<th>انبار</th>
|
||||
<th>مجموع قیمت</th>
|
||||
<th>تاریخ</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template pTemplate="body" let-item let-expanded="expanded">
|
||||
<tr>
|
||||
<td>
|
||||
<p-button
|
||||
type="button"
|
||||
pRipple
|
||||
[pRowToggler]="item"
|
||||
text
|
||||
severity="secondary"
|
||||
rounded
|
||||
[icon]="expanded ? 'pi pi-chevron-down' : 'pi pi-chevron-left'"
|
||||
/>
|
||||
</td>
|
||||
<td>{{ item.code }}</td>
|
||||
<td>{{ item.inventory.name }}</td>
|
||||
<td><span [appPriceMask]="item.totalAmount"></span></td>
|
||||
<td><span [jalaliDate]="item.createdAt"></span></td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #expandedrow let-item>
|
||||
<tr>
|
||||
<td colspan="7">
|
||||
<p-card class="p-4 pt-0 border border-primary-700">
|
||||
<p-table [value]="item.items" dataKey="receiptId">
|
||||
<ng-template #caption>
|
||||
<h5 class="mb-4">کالاهای تامین شده</h5>
|
||||
</ng-template>
|
||||
<ng-template #header>
|
||||
<tr>
|
||||
<th pSortableColumn="product.id">
|
||||
<div class="flex items-center gap-2">
|
||||
شناسه کالا
|
||||
<p-sortIcon field="product.id" />
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="product.name">
|
||||
<div class="flex items-center gap-2">
|
||||
عنوان کالا
|
||||
<p-sortIcon field="product.name" />
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="quantity">
|
||||
<div class="flex items-center gap-2">
|
||||
تعداد
|
||||
<p-sortIcon field="quantity" />
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="fee">
|
||||
<div class="flex items-center gap-2">
|
||||
قیمت واحد
|
||||
<p-sortIcon field="fee" />
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="totalCost">
|
||||
<div class="flex items-center gap-2">
|
||||
قیمت نهایی
|
||||
<p-sortIcon field="totalCost" />
|
||||
</div>
|
||||
</th>
|
||||
<th style="width: 4rem"></th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template #body let-item>
|
||||
<tr>
|
||||
<td>{{ item.product.id }}</td>
|
||||
<td>{{ item.product.name }}</td>
|
||||
<td>{{ item.count }}</td>
|
||||
<td><span [appPriceMask]="item.fee"></span></td>
|
||||
<td><span [appPriceMask]="item.total"></span></td>
|
||||
<td>
|
||||
<p-button type="button" icon="pi pi-search" [routerLink]="'/products/' + item.product.id" />
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</p-card>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="emptymessage">
|
||||
<tr>
|
||||
<td colspan="7" class="text-center! p-10!">هیچ فاکتوری ثبت نشده است.</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</ng-template>
|
||||
</p-card>
|
||||
@@ -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<Maybe<ISupplierInvoicesResponse[]>>(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);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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}`,
|
||||
};
|
||||
|
||||
+25
-3
@@ -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 {}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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<ISupplierResponse> {
|
||||
return this.http.get<ISupplierRawResponse>(this.apiRoutes.single(supplierId)).pipe(
|
||||
getSingle(supplierId: string): Observable<ISupplierFullInfoResponse> {
|
||||
return this.http.get<ISupplierFullInfoRawResponse>(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<ISupplierRawResponse>(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<IPaginatedResponse<ISupplierInvoicesResponse>> {
|
||||
return this.http.get<IPaginatedResponse<ISupplierInvoicesRawResponse>>(
|
||||
this.apiRoutes.invoices(supplierId),
|
||||
);
|
||||
}
|
||||
|
||||
getInvoice(supplierId: string, invoiceId: string): Observable<ISupplierInvoicesResponse> {
|
||||
return this.http.get<ISupplierInvoicesRawResponse>(
|
||||
this.apiRoutes.invoice(supplierId, invoiceId),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1,81 @@
|
||||
<div class=""></div>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex items-center">
|
||||
<div class="grow">
|
||||
<div class="flex items-center gap-2">
|
||||
<p-button icon="pi pi-arrow-right" aria-label="بازگشت" outlined [routerLink]="['/suppliers']" />
|
||||
<h3 class="m-0">{{ data()?.fullname }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button pButton type="button" label="خرید کالا" icon="pi pi-plus"></button>
|
||||
<button pButton type="button" label="ویرایش" icon="pi pi-pencil"></button>
|
||||
<button pButton icon="pi pi-trash" severity="danger"></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inline-flex items-center gap-5">
|
||||
<div class="">
|
||||
<app-key-value label="شماره تماس" [value]="data()?.mobileNumber" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5">
|
||||
<div class="grid grid-cols-4 gap-4">
|
||||
@for (item of topBarCardDetails; track $index) {
|
||||
<details-info-card [icon]="item.icon" [label]="item.label" [value]="item.value?.toString() || ''" />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<suppliers-invoices [supplierId]="supplierId" />
|
||||
<!--
|
||||
<p-card class="">
|
||||
<ng-template pTemplate="header">
|
||||
<div class="p-4 flex items-center justify-between">
|
||||
<span class="text-xl font-bold">کالاهای موجود در انبار</span>
|
||||
<button pButton outlined icon="pi pi-refresh" [loading]="loading()" (click)="getStock()"></button>
|
||||
</div>
|
||||
</ng-template>
|
||||
<p-table [value]="stock()" [loading]="getStockLoading()">
|
||||
<ng-template #header>
|
||||
<tr>
|
||||
<th pSortableColumn="product.id">
|
||||
<div class="flex items-center gap-2">
|
||||
شناسه کالا
|
||||
<p-sortIcon field="product.id" />
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="product.name">
|
||||
<div class="flex items-center gap-2">
|
||||
عنوان کالا
|
||||
<p-sortIcon field="product.name" />
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="quantity">
|
||||
<div class="flex items-center gap-2">
|
||||
تعداد
|
||||
<p-sortIcon field="quantity" />
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="fee">
|
||||
<div class="flex items-center gap-2">
|
||||
قیمت متوسط
|
||||
<p-sortIcon field="fee" />
|
||||
</div>
|
||||
</th>
|
||||
<th style="width: 4rem"></th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template #body let-stock>
|
||||
<tr>
|
||||
<td>{{ stock.product.sku }}</td>
|
||||
<td>{{ stock.product.name }}</td>
|
||||
<td>{{ stock.quantity }}</td>
|
||||
<td><span [appPriceMask]="stock.avgCost"></span></td>
|
||||
|
||||
<td>
|
||||
<p-button type="button" icon="pi pi-search" title="کاردکس کالا" />
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</p-card> -->
|
||||
</div>
|
||||
|
||||
@@ -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<Maybe<ISupplierFullInfoResponse>>(null);
|
||||
loading = signal<Maybe<boolean>>(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),
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
<th rowspan="2" [style]="{ textAlign: 'center', width: '6rem' }">شماره</th>
|
||||
<th rowspan="2" [style]="{ textAlign: 'center', minWidth: '14rem', width: '14rem' }">طرف حساب</th>
|
||||
<th rowspan="2" [style]="{ textAlign: 'center', width: '12rem' }">انبار</th>
|
||||
<th colspan="2" class="text-center!" [style]="{ width: '15rem' }">وارده</th>
|
||||
<th colspan="2" class="text-center!" [style]="{ width: '15rem' }">ورودی</th>
|
||||
<th colspan="2" class="text-center!" [style]="{ width: '15rem' }">خروجی</th>
|
||||
<th colspan="3" class="text-center!" [style]="{ width: '25rem' }">مانده</th>
|
||||
<th colspan="1" class="text-center!" [style]="{ width: '5rem' }">مانده</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th [style]="{ textAlign: 'center', width: '5rem' }">تعداد</th>
|
||||
@@ -24,8 +24,8 @@
|
||||
<th [style]="{ textAlign: 'center', width: '5rem' }">تعداد</th>
|
||||
<th [style]="{ textAlign: 'center', width: '10rem' }">فی</th>
|
||||
<th [style]="{ textAlign: 'center', width: '5rem' }">تعداد</th>
|
||||
<th [style]="{ textAlign: 'center', width: '10rem' }">فی</th>
|
||||
<th [style]="{ textAlign: 'center', width: '10rem' }">مجموع قیمت</th>
|
||||
<!-- <th [style]="{ textAlign: 'center', width: '10rem' }">فی</th>
|
||||
<th [style]="{ textAlign: 'center', width: '10rem' }">مجموع قیمت</th> -->
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
@@ -60,8 +60,8 @@
|
||||
}
|
||||
</td>
|
||||
<td class="text-center!">{{ item.remainedInStock }}</td>
|
||||
<td class="text-center!"><span [appPriceMask]="item.fee"></span></td>
|
||||
<td class="text-center!"><span [appPriceMask]="item.totalCost"></span></td>
|
||||
<!-- <td class="text-center!"><span [appPriceMask]="item.fee"></span></td>
|
||||
<td class="text-center!"><span [appPriceMask]="item.totalCost"></span></td> -->
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
|
||||
Reference in New Issue
Block a user