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:
2025-12-21 19:09:13 +03:30
parent 108d192f88
commit e937c994d7
29 changed files with 716 additions and 49 deletions
@@ -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
View File
@@ -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 {}
+21
View File
@@ -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),
},
];
}
}