diff --git a/src/app/modules/bankAccounts/components/form-template.component.ts b/src/app/modules/bankAccounts/components/form-template.component.ts index 1c2abd6..d18da67 100644 --- a/src/app/modules/bankAccounts/components/form-template.component.ts +++ b/src/app/modules/bankAccounts/components/form-template.component.ts @@ -28,7 +28,7 @@ export class BankAccountFormTemplateComponent { nonNullable: true, validators: Validators.required, }), - branchId: this.fb.control(this.initialValues?.branchId || 0, { + branchId: this.fb.control(this.initialValues?.branch?.id || 0, { nonNullable: true, validators: Validators.required, }), diff --git a/src/app/modules/bankAccounts/constants/apiRoutes/index.ts b/src/app/modules/bankAccounts/constants/apiRoutes/index.ts index 547e525..39fa1cd 100644 --- a/src/app/modules/bankAccounts/constants/apiRoutes/index.ts +++ b/src/app/modules/bankAccounts/constants/apiRoutes/index.ts @@ -2,4 +2,6 @@ const baseUrl = '/api/v1/bank-accounts'; export const BANK_ACCOUNTS_API_ROUTES = { list: () => `${baseUrl}`, + single: (bankAccountId: string) => `${baseUrl}/${bankAccountId}`, + transactions: (bankAccountId: string) => `${baseUrl}/${bankAccountId}/transactions`, }; diff --git a/src/app/modules/bankAccounts/constants/routes/index.ts b/src/app/modules/bankAccounts/constants/routes/index.ts index fca6d2d..a8ec966 100644 --- a/src/app/modules/bankAccounts/constants/routes/index.ts +++ b/src/app/modules/bankAccounts/constants/routes/index.ts @@ -13,11 +13,11 @@ export const bankAccountsNamedRoutes: NamedRoutes = { }, }, bankAccount: { - path: 'bankAccount', + path: 'bankAccounts/:bankAccountId', loadComponent: () => import('../../views/single.component').then((m) => m.BankAccountComponent), meta: { title: 'حساب‌ بانکی', - pagePath: () => '/bankAccount', + pagePath: () => '/bankAccounts/:bankAccountId', }, }, }; diff --git a/src/app/modules/bankAccounts/models/io.d.ts b/src/app/modules/bankAccounts/models/io.d.ts index 3c2596f..e1d4a14 100644 --- a/src/app/modules/bankAccounts/models/io.d.ts +++ b/src/app/modules/bankAccounts/models/io.d.ts @@ -1,7 +1,17 @@ +import { Maybe } from '@/core'; +import { + IBankAccountBranchSummary, + IReferencePos, + IReferencePosRaw, + IReferencePurchase, + IReferencePurchaseRaw, +} from './types'; + export interface IBankAccountsRawResponse { id: number; name: string; - branchId: number; + branch: IBankAccountBranchSummary; + currentBalance: number; accountNumber?: string; iban?: string; cardNumber?: string; @@ -19,3 +29,20 @@ export interface IBankAccountsRequest { iban?: string; cardNumber?: string; } + +export interface IBankAccountTransactionRawResponse { + id: number; + bankAccountId: number; + type: string; + amount: string; + balanceAfter: string; + referenceId: number; + referenceType: string; + description: null; + createdAt: string; + reference: Maybe; +} + +export interface IBankAccountTransactionResponse extends IBankAccountTransactionRawResponse { + reference: Maybe; +} diff --git a/src/app/modules/bankAccounts/models/types.ts b/src/app/modules/bankAccounts/models/types.ts index e69de29..a6d0ab3 100644 --- a/src/app/modules/bankAccounts/models/types.ts +++ b/src/app/modules/bankAccounts/models/types.ts @@ -0,0 +1,46 @@ +export interface IBankAccountBranchSummary { + id: number; + name: string; + code: string; + bank: Bank; +} + +interface Bank { + id: number; + name: string; + shortName: string; +} + +interface IReferenceCommon { + id: number; + code: string; + totalAmount: string; +} + +export interface IReferencePurchaseRaw extends IReferenceCommon { + supplier: IPersonSummary; +} + +export interface IReferencePosRaw extends IReferenceCommon { + customer: IPersonSummary; +} + +export interface IReferencePurchase extends IReferenceCommon { + supplier: { + id: number; + name: string; + }; +} + +export interface IReferencePos extends IReferenceCommon { + customer: { + id: number; + name: string; + }; +} + +interface IPersonSummary { + id: number; + firstName: string; + lastName: string; +} diff --git a/src/app/modules/bankAccounts/services/main.service.ts b/src/app/modules/bankAccounts/services/main.service.ts index 3b32da0..cfc6590 100644 --- a/src/app/modules/bankAccounts/services/main.service.ts +++ b/src/app/modules/bankAccounts/services/main.service.ts @@ -1,9 +1,23 @@ +import { Maybe } from '@/core'; import { IPaginatedResponse } from '@/core/models/service.model'; +import { BankTransactionReferenceType } from '@/shared/catalog/transactionReferenceTypes'; import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { Observable } from 'rxjs'; +import { map, Observable } from 'rxjs'; import { BANK_ACCOUNTS_API_ROUTES } from '../constants'; -import { IBankAccountsRawResponse, IBankAccountsRequest, IBankAccountsResponse } from '../models'; +import { + IBankAccountsRawResponse, + IBankAccountsRequest, + IBankAccountsResponse, + IBankAccountTransactionRawResponse, + IBankAccountTransactionResponse, +} from '../models'; +import { + IReferencePos, + IReferencePosRaw, + IReferencePurchase, + IReferencePurchaseRaw, +} from '../models/types'; @Injectable({ providedIn: 'root' }) export class BankAccountsService { @@ -15,7 +29,71 @@ export class BankAccountsService { return this.http.get>(this.apiRoutes.list()); } + getSingle(bankAccountId: string): Observable { + return this.http.get(this.apiRoutes.single(bankAccountId)); + } + create(data: IBankAccountsRequest): Observable { return this.http.post(this.apiRoutes.list(), data); } + + getTransactions( + bankAccountId: string, + ): Observable> { + return this.http + .get< + IPaginatedResponse + >(this.apiRoutes.transactions(bankAccountId)) + .pipe( + map((res) => ({ + meta: res.meta, + data: res.data.map((transaction) => this.transformTransaction(transaction)), + })), + ); + } + + private transformTransaction( + transaction: IBankAccountTransactionRawResponse, + ): IBankAccountTransactionResponse { + const { reference, referenceType, ...rest } = transaction; + let transformedReference: Maybe = null; + + if (reference) { + if ( + referenceType === BankTransactionReferenceType.PURCHASE_PAYMENT || + referenceType === BankTransactionReferenceType.PURCHASE_REFUND + ) { + const purchaseRef = reference as IReferencePurchaseRaw; + transformedReference = { + id: purchaseRef.id, + code: purchaseRef.code, + totalAmount: purchaseRef.totalAmount, + supplier: { + id: purchaseRef.supplier.id, + name: `${purchaseRef.supplier.firstName} ${purchaseRef.supplier.lastName}`, + }, + }; + } else if ( + referenceType === BankTransactionReferenceType.POS_SALE || + referenceType === BankTransactionReferenceType.POS_REFUND + ) { + const posRef = reference as IReferencePosRaw; + transformedReference = { + id: posRef.id, + code: posRef.code, + totalAmount: posRef.totalAmount, + customer: { + id: posRef.customer.id, + name: `${posRef.customer.firstName} ${posRef.customer.lastName}`, + }, + }; + } + } + + return { + ...rest, + referenceType, + reference: transformedReference, + }; + } } diff --git a/src/app/modules/bankAccounts/store/bankAccount.store.ts b/src/app/modules/bankAccounts/store/bankAccount.store.ts new file mode 100644 index 0000000..f1507f6 --- /dev/null +++ b/src/app/modules/bankAccounts/store/bankAccount.store.ts @@ -0,0 +1,76 @@ +import { ToastService } from '@/core/services/toast.service'; +import { EntityState, EntityStore } from '@/core/state'; +import { Injectable } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { IBankAccountsResponse } from '../models'; +import { BankAccountsService } from '../services/main.service'; + +export interface BankAccountState extends EntityState {} + +@Injectable({ + providedIn: 'root', +}) +export class BankAccountStore extends EntityStore { + constructor( + private activeRoute: ActivatedRoute, + private service: BankAccountsService, + private toastService: ToastService, + ) { + super({ + loading: false, + initialized: false, + error: null, + isRefreshing: false, + entities: {}, + selectedId: null, + ids: [], + }); + this.initial(); + } + + supplierId!: string; + + initial() {} + + getSingle(bankAccountId: string) { + if (this.entities()[bankAccountId]) { + return; + } + this.patchState({ loading: true }); + this.service.getSingle(bankAccountId).subscribe({ + next: (res) => { + this.patchState({ + entities: { [res.id]: res }, + loading: false, + }); + }, + error: (err) => { + this.patchState({ loading: false, error: err }); + this.toastService.error({ + text: 'خطا در دریافت اطلاعات حساب بانکی', + }); + }, + }); + } + + refresh(bankAccountId: string) { + const { bankAccountId: _, ...entities } = this.entities(); + this.patchState({ + entities, + }); + this.getSingle(bankAccountId); + } + + reset(): void { + const queryParams = this.activeRoute.snapshot.queryParams; + this.setState({ + loading: false, + initialized: false, + error: null, + isRefreshing: false, + entities: {}, + selectedId: null, + ids: [], + }); + } +} diff --git a/src/app/modules/bankAccounts/views/list.component.ts b/src/app/modules/bankAccounts/views/list.component.ts index 68d0664..b316d2e 100644 --- a/src/app/modules/bankAccounts/views/list.component.ts +++ b/src/app/modules/bankAccounts/views/list.component.ts @@ -22,7 +22,7 @@ export class BankAccountsComponent { { field: 'name', header: 'نام', - minWidth: '100px', + minWidth: '160px', }, { field: 'bank', @@ -32,6 +32,12 @@ export class BankAccountsComponent { }, minWidth: '200px', }, + { + field: 'currentBalance', + header: 'مانده حساب', + type: 'price', + minWidth: '180px', + }, { field: 'accountNumber', header: 'شماره حساب بانکی', @@ -50,8 +56,7 @@ export class BankAccountsComponent { canCopy: true, minWidth: '160px', }, - { field: 'createdAt', header: 'تاریخ ایجاد', type: 'date' }, - { field: 'updatedAt', header: 'تاریخ به‌روزرسانی', type: 'date' }, + { field: 'createdAt', header: 'تاریخ ایجاد', type: 'date', minWidth: '120px' }, ] as IColumn[]; loading = signal(false); diff --git a/src/app/modules/bankAccounts/views/single.component.html b/src/app/modules/bankAccounts/views/single.component.html index 9dd2b8d..e2f77cd 100644 --- a/src/app/modules/bankAccounts/views/single.component.html +++ b/src/app/modules/bankAccounts/views/single.component.html @@ -1,8 +1,48 @@ - +
+ + + @if (!loading()) { +
+
+ +
+
+ +
+
+ + + +
+
+ } + + + + + + + + + + @if (item.referenceType === "PURCHASE_PAYMENT" || item.referenceType === "PURCHASE_REFUND") { + {{ item.reference.supplier?.name || "-" }} + } @else if (item.referenceType === "CUSTOMER_PAYMENT" || item.referenceType === "CUSTOMER_REFUND") { + {{ item.reference.customer?.name || "-" }} + } @else { + {{ item.referenceType }} + } + + + +
diff --git a/src/app/modules/bankAccounts/views/single.component.ts b/src/app/modules/bankAccounts/views/single.component.ts index aefa666..e26c8ca 100644 --- a/src/app/modules/bankAccounts/views/single.component.ts +++ b/src/app/modules/bankAccounts/views/single.component.ts @@ -1,48 +1,85 @@ +import { Maybe } from '@/core'; +import { IResponseMetadata } from '@/core/models/service.model'; +import { CatalogBankAccountTransactionReferenceTypeTagComponent } from '@/shared/catalog/transactionReferenceTypes'; +import { CatalogBankAccountTransactionTypeTagComponent } from '@/shared/catalog/transactionTypes'; +import { KeyValueComponent } from '@/shared/components'; +import { InnerPagesHeaderComponent } from '@/shared/components/innerPagesHeader/inner-pages-header.component'; import { IColumn, PageDataListComponent, } from '@/shared/components/pageDataList/page-data-list.component'; -import { Component, signal } from '@angular/core'; -import { IBankAccountsResponse } from '../models'; +import { PriceMaskDirective } from '@/shared/directives'; +import { Component, computed, inject, signal, TemplateRef, ViewChild } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { IBankAccountTransactionResponse } from '../models'; +import { BankAccountsService } from '../services/main.service'; +import { BankAccountStore } from '../store/bankAccount.store'; @Component({ selector: 'app-bank-account', templateUrl: './single.component.html', - imports: [PageDataListComponent], + imports: [ + InnerPagesHeaderComponent, + KeyValueComponent, + PriceMaskDirective, + PageDataListComponent, + CatalogBankAccountTransactionTypeTagComponent, + CatalogBankAccountTransactionReferenceTypeTagComponent, + ], }) export class BankAccountComponent { - constructor() {} + private route = inject(ActivatedRoute); + private store = inject(BankAccountStore); - columns = [ - { field: 'id', header: 'شناسه', width: '60px' }, - { - field: 'name', - header: 'نام', - minWidth: '100px', - }, + @ViewChild('typeTpl', { static: true }) typeTpl!: TemplateRef; + @ViewChild('referenceTypeTpl', { static: true }) referenceTypeTpl!: TemplateRef; + @ViewChild('counterpartyTpl', { static: true }) counterpartyTpl!: TemplateRef; - { - field: 'accountNumber', - header: 'شماره حساب بانکی', - canCopy: true, - minWidth: '160px', - }, - { - field: 'iban', - header: 'شماره شبا', - canCopy: true, - minWidth: '200px', - }, - { - field: 'cardNumber', - header: 'شماره کارت بانکی', - canCopy: true, - minWidth: '160px', - }, - { field: 'createdAt', header: 'تاریخ ایجاد', type: 'date' }, - { field: 'updatedAt', header: 'تاریخ به‌روزرسانی', type: 'date' }, - ] as IColumn[]; + bankAccountId = this.route.snapshot.paramMap.get('bankAccountId')!; - loading = signal(false); - items = signal([]); + transactionsColumn = [] as IColumn[]; + + bankAccount = computed(() => this.store.entities()[this.bankAccountId]); + loading = this.store.loading; + + transactions = signal([]); + transactionsLoading = signal(false); + transactionsPagination = signal>(null); + + constructor(private readonly service: BankAccountsService) { + this.store.getSingle(this.bankAccountId); + this.getTransactions(); + } + + pageTitle = computed(() => { + const account = this.bankAccount(); + return account ? `حساب بانک: ${account.name}` : 'حساب بانک'; + }); + + ngOnInit() { + this.transactionsColumn = [ + { field: 'id', header: 'شناسه', width: '60px' }, + { field: 'type', header: 'نوع تراکنش', customDataModel: this.typeTpl }, + { field: 'amount', header: 'مبلغ', type: 'price' }, + { field: 'balanceAfter', header: 'مانده حساب', type: 'price' }, + { field: 'type', header: 'نوع رسید', customDataModel: this.referenceTypeTpl }, + { field: 'counterparty', header: 'طرف حساب', customDataModel: this.counterpartyTpl }, + { field: 'description', header: 'توضیحات' }, + { field: 'createdAt', header: 'تاریخ ایجاد', type: 'date' }, + ]; + } + + getTransactions() { + this.transactionsLoading.set(true); + this.service.getTransactions(this.bankAccountId).subscribe({ + next: (res) => { + this.transactions.set(res.data); + this.transactionsPagination.set(res.meta); + this.transactionsLoading.set(false); + }, + error: () => { + this.transactionsLoading.set(false); + }, + }); + } } diff --git a/src/app/shared/catalog/transactionReferenceTypes/index.ts b/src/app/shared/catalog/transactionReferenceTypes/index.ts new file mode 100644 index 0000000..7d1f2fb --- /dev/null +++ b/src/app/shared/catalog/transactionReferenceTypes/index.ts @@ -0,0 +1,3 @@ +export * from './tag.component'; +export * from './types'; +export * from './utils'; diff --git a/src/app/shared/catalog/transactionReferenceTypes/tag.component.html b/src/app/shared/catalog/transactionReferenceTypes/tag.component.html new file mode 100644 index 0000000..d481d6b --- /dev/null +++ b/src/app/shared/catalog/transactionReferenceTypes/tag.component.html @@ -0,0 +1,3 @@ + + {{ text }} + diff --git a/src/app/shared/catalog/transactionReferenceTypes/tag.component.ts b/src/app/shared/catalog/transactionReferenceTypes/tag.component.ts new file mode 100644 index 0000000..00810c9 --- /dev/null +++ b/src/app/shared/catalog/transactionReferenceTypes/tag.component.ts @@ -0,0 +1,24 @@ +import { Component, Input } from '@angular/core'; +import { Tag } from 'primeng/tag'; +import { BankTransactionReferenceType } from './types'; +import { + getBankAccountTransactionReferenceTypeColor, + getBankAccountTransactionReferenceTypeText, +} from './utils'; + +@Component({ + selector: 'catalog-bank-account-transaction-reference-type-tag', + templateUrl: './tag.component.html', + imports: [Tag], +}) +export class CatalogBankAccountTransactionReferenceTypeTagComponent { + @Input() type!: BankTransactionReferenceType; + constructor() {} + + get text() { + return getBankAccountTransactionReferenceTypeText(this.type); + } + get color() { + return getBankAccountTransactionReferenceTypeColor(this.type); + } +} diff --git a/src/app/shared/catalog/transactionReferenceTypes/types.ts b/src/app/shared/catalog/transactionReferenceTypes/types.ts new file mode 100644 index 0000000..5339ca4 --- /dev/null +++ b/src/app/shared/catalog/transactionReferenceTypes/types.ts @@ -0,0 +1,11 @@ +export const BankTransactionReferenceType = { + PURCHASE_PAYMENT: 'PURCHASE_PAYMENT', + PURCHASE_REFUND: 'PURCHASE_REFUND', + POS_SALE: 'POS_SALE', + POS_REFUND: 'POS_REFUND', + BANK_TRANSFER: 'BANK_TRANSFER', + MANUAL_ADJUSTMENT: 'MANUAL_ADJUSTMENT', +} as const; + +export type BankTransactionReferenceType = + (typeof BankTransactionReferenceType)[keyof typeof BankTransactionReferenceType]; diff --git a/src/app/shared/catalog/transactionReferenceTypes/utils.ts b/src/app/shared/catalog/transactionReferenceTypes/utils.ts new file mode 100644 index 0000000..4f04099 --- /dev/null +++ b/src/app/shared/catalog/transactionReferenceTypes/utils.ts @@ -0,0 +1,40 @@ +import { TagSeverity } from '@/core/models/tag'; +import { BankTransactionReferenceType } from './types'; + +export const getBankAccountTransactionReferenceTypeText = ( + type: BankTransactionReferenceType, +): string => { + switch (type) { + case BankTransactionReferenceType.BANK_TRANSFER: + return 'انتقال بانکی'; + case BankTransactionReferenceType.MANUAL_ADJUSTMENT: + return 'تعدیل دستی'; + case BankTransactionReferenceType.POS_REFUND: + return 'بازپرداخت فروشگاه'; + case BankTransactionReferenceType.POS_SALE: + return 'فروش فروشگاه'; + case BankTransactionReferenceType.PURCHASE_REFUND: + return 'بازپرداخت خرید'; + case BankTransactionReferenceType.PURCHASE_PAYMENT: + return 'پرداخت خرید'; + } +}; + +export const getBankAccountTransactionReferenceTypeColor = ( + type: BankTransactionReferenceType, +): TagSeverity => { + switch (type) { + case BankTransactionReferenceType.BANK_TRANSFER: + return 'secondary'; + case BankTransactionReferenceType.MANUAL_ADJUSTMENT: + return 'secondary'; + case BankTransactionReferenceType.POS_REFUND: + return 'warn'; + case BankTransactionReferenceType.POS_SALE: + return 'info'; + case BankTransactionReferenceType.PURCHASE_REFUND: + return 'warn'; + case BankTransactionReferenceType.PURCHASE_PAYMENT: + return 'info'; + } +}; diff --git a/src/app/shared/catalog/transactionTypes/index.ts b/src/app/shared/catalog/transactionTypes/index.ts new file mode 100644 index 0000000..7d1f2fb --- /dev/null +++ b/src/app/shared/catalog/transactionTypes/index.ts @@ -0,0 +1,3 @@ +export * from './tag.component'; +export * from './types'; +export * from './utils'; diff --git a/src/app/shared/catalog/transactionTypes/tag.component.html b/src/app/shared/catalog/transactionTypes/tag.component.html new file mode 100644 index 0000000..d481d6b --- /dev/null +++ b/src/app/shared/catalog/transactionTypes/tag.component.html @@ -0,0 +1,3 @@ + + {{ text }} + diff --git a/src/app/shared/catalog/transactionTypes/tag.component.ts b/src/app/shared/catalog/transactionTypes/tag.component.ts new file mode 100644 index 0000000..acebf01 --- /dev/null +++ b/src/app/shared/catalog/transactionTypes/tag.component.ts @@ -0,0 +1,21 @@ +import { Component, Input } from '@angular/core'; +import { Tag } from 'primeng/tag'; +import { BankAccountTransactionType } from './types'; +import { getBankAccountTransactionTypeColor, getBankAccountTransactionTypeText } from './utils'; + +@Component({ + selector: 'catalog-bank-account-transaction-type-tag', + templateUrl: './tag.component.html', + imports: [Tag], +}) +export class CatalogBankAccountTransactionTypeTagComponent { + @Input() type!: BankAccountTransactionType; + constructor() {} + + get text() { + return getBankAccountTransactionTypeText(this.type); + } + get color() { + return getBankAccountTransactionTypeColor(this.type); + } +} diff --git a/src/app/shared/catalog/transactionTypes/types.ts b/src/app/shared/catalog/transactionTypes/types.ts new file mode 100644 index 0000000..10be377 --- /dev/null +++ b/src/app/shared/catalog/transactionTypes/types.ts @@ -0,0 +1,7 @@ +export const BankAccountTransactionType = { + DEPOSIT: 'DEPOSIT', + WITHDRAWAL: 'WITHDRAWAL', +} as const; + +export type BankAccountTransactionType = + (typeof BankAccountTransactionType)[keyof typeof BankAccountTransactionType]; diff --git a/src/app/shared/catalog/transactionTypes/utils.ts b/src/app/shared/catalog/transactionTypes/utils.ts new file mode 100644 index 0000000..7c8a17c --- /dev/null +++ b/src/app/shared/catalog/transactionTypes/utils.ts @@ -0,0 +1,22 @@ +import { TagSeverity } from '@/core/models/tag'; +import { BankAccountTransactionType } from './types'; + +export const getBankAccountTransactionTypeText = (type: BankAccountTransactionType): string => { + switch (type) { + case BankAccountTransactionType.DEPOSIT: + return 'واریز'; + case BankAccountTransactionType.WITHDRAWAL: + return 'برداشت'; + } +}; + +export const getBankAccountTransactionTypeColor = ( + type: BankAccountTransactionType, +): TagSeverity => { + switch (type) { + case BankAccountTransactionType.DEPOSIT: + return 'success'; + case BankAccountTransactionType.WITHDRAWAL: + return 'danger'; + } +};